mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
feat: droid平台账户数据统计及调度能力
This commit is contained in:
@@ -438,6 +438,7 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
geminiAccountId: validation.keyData.geminiAccountId,
|
geminiAccountId: validation.keyData.geminiAccountId,
|
||||||
openaiAccountId: validation.keyData.openaiAccountId, // 添加 OpenAI 账号ID
|
openaiAccountId: validation.keyData.openaiAccountId, // 添加 OpenAI 账号ID
|
||||||
bedrockAccountId: validation.keyData.bedrockAccountId, // 添加 Bedrock 账号ID
|
bedrockAccountId: validation.keyData.bedrockAccountId, // 添加 Bedrock 账号ID
|
||||||
|
droidAccountId: validation.keyData.droidAccountId,
|
||||||
permissions: validation.keyData.permissions,
|
permissions: validation.keyData.permissions,
|
||||||
concurrencyLimit: validation.keyData.concurrencyLimit,
|
concurrencyLimit: validation.keyData.concurrencyLimit,
|
||||||
rateLimitWindow: validation.keyData.rateLimitWindow,
|
rateLimitWindow: validation.keyData.rateLimitWindow,
|
||||||
|
|||||||
@@ -858,7 +858,9 @@ class RedisClient {
|
|||||||
|
|
||||||
// 获取账户创建时间来计算平均值 - 支持不同类型的账号
|
// 获取账户创建时间来计算平均值 - 支持不同类型的账号
|
||||||
let accountData = {}
|
let accountData = {}
|
||||||
if (accountType === 'openai') {
|
if (accountType === 'droid') {
|
||||||
|
accountData = await this.client.hgetall(`droid:account:${accountId}`)
|
||||||
|
} else if (accountType === 'openai') {
|
||||||
accountData = await this.client.hgetall(`openai:account:${accountId}`)
|
accountData = await this.client.hgetall(`openai:account:${accountId}`)
|
||||||
} else if (accountType === 'openai-responses') {
|
} else if (accountType === 'openai-responses') {
|
||||||
accountData = await this.client.hgetall(`openai_responses_account:${accountId}`)
|
accountData = await this.client.hgetall(`openai_responses_account:${accountId}`)
|
||||||
@@ -874,6 +876,9 @@ class RedisClient {
|
|||||||
if (!accountData.createdAt) {
|
if (!accountData.createdAt) {
|
||||||
accountData = await this.client.hgetall(`openai_account:${accountId}`)
|
accountData = await this.client.hgetall(`openai_account:${accountId}`)
|
||||||
}
|
}
|
||||||
|
if (!accountData.createdAt) {
|
||||||
|
accountData = await this.client.hgetall(`droid:account:${accountId}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const createdAt = accountData.createdAt ? new Date(accountData.createdAt) : new Date()
|
const createdAt = accountData.createdAt ? new Date(accountData.createdAt) : new Date()
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|||||||
@@ -539,6 +539,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
geminiAccountId,
|
geminiAccountId,
|
||||||
openaiAccountId,
|
openaiAccountId,
|
||||||
bedrockAccountId,
|
bedrockAccountId,
|
||||||
|
droidAccountId,
|
||||||
permissions,
|
permissions,
|
||||||
concurrencyLimit,
|
concurrencyLimit,
|
||||||
rateLimitWindow,
|
rateLimitWindow,
|
||||||
@@ -676,6 +677,18 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证服务权限字段
|
||||||
|
if (
|
||||||
|
permissions !== undefined &&
|
||||||
|
permissions !== null &&
|
||||||
|
permissions !== '' &&
|
||||||
|
!['claude', 'gemini', 'openai', 'droid', 'all'].includes(permissions)
|
||||||
|
) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const newKey = await apiKeyService.generateApiKey({
|
const newKey = await apiKeyService.generateApiKey({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -686,6 +699,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
geminiAccountId,
|
geminiAccountId,
|
||||||
openaiAccountId,
|
openaiAccountId,
|
||||||
bedrockAccountId,
|
bedrockAccountId,
|
||||||
|
droidAccountId,
|
||||||
permissions,
|
permissions,
|
||||||
concurrencyLimit,
|
concurrencyLimit,
|
||||||
rateLimitWindow,
|
rateLimitWindow,
|
||||||
@@ -727,6 +741,7 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
geminiAccountId,
|
geminiAccountId,
|
||||||
openaiAccountId,
|
openaiAccountId,
|
||||||
bedrockAccountId,
|
bedrockAccountId,
|
||||||
|
droidAccountId,
|
||||||
permissions,
|
permissions,
|
||||||
concurrencyLimit,
|
concurrencyLimit,
|
||||||
rateLimitWindow,
|
rateLimitWindow,
|
||||||
@@ -761,6 +776,17 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
.json({ error: 'Base name must be less than 90 characters to allow for numbering' })
|
.json({ error: 'Base name must be less than 90 characters to allow for numbering' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
permissions !== undefined &&
|
||||||
|
permissions !== null &&
|
||||||
|
permissions !== '' &&
|
||||||
|
!['claude', 'gemini', 'openai', 'droid', 'all'].includes(permissions)
|
||||||
|
) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 生成批量API Keys
|
// 生成批量API Keys
|
||||||
const createdKeys = []
|
const createdKeys = []
|
||||||
const errors = []
|
const errors = []
|
||||||
@@ -778,6 +804,7 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
geminiAccountId,
|
geminiAccountId,
|
||||||
openaiAccountId,
|
openaiAccountId,
|
||||||
bedrockAccountId,
|
bedrockAccountId,
|
||||||
|
droidAccountId,
|
||||||
permissions,
|
permissions,
|
||||||
concurrencyLimit,
|
concurrencyLimit,
|
||||||
rateLimitWindow,
|
rateLimitWindow,
|
||||||
@@ -860,6 +887,15 @@ router.put('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
updates.permissions !== undefined &&
|
||||||
|
!['claude', 'gemini', 'openai', 'droid', 'all'].includes(updates.permissions)
|
||||||
|
) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`🔄 Admin batch editing ${keyIds.length} API keys with updates: ${JSON.stringify(updates)}`
|
`🔄 Admin batch editing ${keyIds.length} API keys with updates: ${JSON.stringify(updates)}`
|
||||||
)
|
)
|
||||||
@@ -945,6 +981,9 @@ router.put('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
if (updates.bedrockAccountId !== undefined) {
|
if (updates.bedrockAccountId !== undefined) {
|
||||||
finalUpdates.bedrockAccountId = updates.bedrockAccountId
|
finalUpdates.bedrockAccountId = updates.bedrockAccountId
|
||||||
}
|
}
|
||||||
|
if (updates.droidAccountId !== undefined) {
|
||||||
|
finalUpdates.droidAccountId = updates.droidAccountId || ''
|
||||||
|
}
|
||||||
|
|
||||||
// 处理标签操作
|
// 处理标签操作
|
||||||
if (updates.tags !== undefined) {
|
if (updates.tags !== undefined) {
|
||||||
@@ -1031,6 +1070,7 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
geminiAccountId,
|
geminiAccountId,
|
||||||
openaiAccountId,
|
openaiAccountId,
|
||||||
bedrockAccountId,
|
bedrockAccountId,
|
||||||
|
droidAccountId,
|
||||||
permissions,
|
permissions,
|
||||||
enableModelRestriction,
|
enableModelRestriction,
|
||||||
restrictedModels,
|
restrictedModels,
|
||||||
@@ -1122,12 +1162,17 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
updates.bedrockAccountId = bedrockAccountId || ''
|
updates.bedrockAccountId = bedrockAccountId || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (droidAccountId !== undefined) {
|
||||||
|
// 空字符串表示解绑,null或空字符串都设置为空字符串
|
||||||
|
updates.droidAccountId = droidAccountId || ''
|
||||||
|
}
|
||||||
|
|
||||||
if (permissions !== undefined) {
|
if (permissions !== undefined) {
|
||||||
// 验证权限值
|
// 验证权限值
|
||||||
if (!['claude', 'gemini', 'openai', 'all'].includes(permissions)) {
|
if (!['claude', 'gemini', 'openai', 'droid', 'all'].includes(permissions)) {
|
||||||
return res
|
return res.status(400).json({
|
||||||
.status(400)
|
error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all'
|
||||||
.json({ error: 'Invalid permissions value. Must be claude, gemini, openai, or all' })
|
})
|
||||||
}
|
}
|
||||||
updates.permissions = permissions
|
updates.permissions = permissions
|
||||||
}
|
}
|
||||||
@@ -4393,6 +4438,7 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
openaiAccounts,
|
openaiAccounts,
|
||||||
ccrAccounts,
|
ccrAccounts,
|
||||||
openaiResponsesAccounts,
|
openaiResponsesAccounts,
|
||||||
|
droidAccounts,
|
||||||
todayStats,
|
todayStats,
|
||||||
systemAverages,
|
systemAverages,
|
||||||
realtimeMetrics
|
realtimeMetrics
|
||||||
@@ -4406,6 +4452,7 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
redis.getAllOpenAIAccounts(),
|
redis.getAllOpenAIAccounts(),
|
||||||
ccrAccountService.getAllAccounts(),
|
ccrAccountService.getAllAccounts(),
|
||||||
openaiResponsesAccountService.getAllAccounts(true),
|
openaiResponsesAccountService.getAllAccounts(true),
|
||||||
|
droidAccountService.getAllAccounts(),
|
||||||
redis.getTodayStats(),
|
redis.getTodayStats(),
|
||||||
redis.getSystemAverages(),
|
redis.getSystemAverages(),
|
||||||
redis.getRealtimeSystemMetrics()
|
redis.getRealtimeSystemMetrics()
|
||||||
@@ -4413,6 +4460,42 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
// 处理Bedrock账户数据
|
// 处理Bedrock账户数据
|
||||||
const bedrockAccounts = bedrockAccountsResult.success ? bedrockAccountsResult.data : []
|
const bedrockAccounts = bedrockAccountsResult.success ? bedrockAccountsResult.data : []
|
||||||
|
const normalizeBoolean = (value) => value === true || value === 'true'
|
||||||
|
const isRateLimitedFlag = (status) => {
|
||||||
|
if (!status) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof status === 'string') {
|
||||||
|
return status === 'limited'
|
||||||
|
}
|
||||||
|
if (typeof status === 'object') {
|
||||||
|
return status.isRateLimited === true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalDroidAccounts = droidAccounts.filter(
|
||||||
|
(acc) =>
|
||||||
|
normalizeBoolean(acc.isActive) &&
|
||||||
|
acc.status !== 'blocked' &&
|
||||||
|
acc.status !== 'unauthorized' &&
|
||||||
|
normalizeBoolean(acc.schedulable) &&
|
||||||
|
!isRateLimitedFlag(acc.rateLimitStatus)
|
||||||
|
).length
|
||||||
|
const abnormalDroidAccounts = droidAccounts.filter(
|
||||||
|
(acc) =>
|
||||||
|
!normalizeBoolean(acc.isActive) || acc.status === 'blocked' || acc.status === 'unauthorized'
|
||||||
|
).length
|
||||||
|
const pausedDroidAccounts = droidAccounts.filter(
|
||||||
|
(acc) =>
|
||||||
|
!normalizeBoolean(acc.schedulable) &&
|
||||||
|
normalizeBoolean(acc.isActive) &&
|
||||||
|
acc.status !== 'blocked' &&
|
||||||
|
acc.status !== 'unauthorized'
|
||||||
|
).length
|
||||||
|
const rateLimitedDroidAccounts = droidAccounts.filter((acc) =>
|
||||||
|
isRateLimitedFlag(acc.rateLimitStatus)
|
||||||
|
).length
|
||||||
|
|
||||||
// 计算使用统计(统一使用allTokens)
|
// 计算使用统计(统一使用allTokens)
|
||||||
const totalTokensUsed = apiKeys.reduce(
|
const totalTokensUsed = apiKeys.reduce(
|
||||||
@@ -4660,7 +4743,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
abnormalBedrockAccounts +
|
abnormalBedrockAccounts +
|
||||||
abnormalOpenAIAccounts +
|
abnormalOpenAIAccounts +
|
||||||
abnormalOpenAIResponsesAccounts +
|
abnormalOpenAIResponsesAccounts +
|
||||||
abnormalCcrAccounts,
|
abnormalCcrAccounts +
|
||||||
|
abnormalDroidAccounts,
|
||||||
pausedAccounts:
|
pausedAccounts:
|
||||||
pausedClaudeAccounts +
|
pausedClaudeAccounts +
|
||||||
pausedClaudeConsoleAccounts +
|
pausedClaudeConsoleAccounts +
|
||||||
@@ -4668,7 +4752,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
pausedBedrockAccounts +
|
pausedBedrockAccounts +
|
||||||
pausedOpenAIAccounts +
|
pausedOpenAIAccounts +
|
||||||
pausedOpenAIResponsesAccounts +
|
pausedOpenAIResponsesAccounts +
|
||||||
pausedCcrAccounts,
|
pausedCcrAccounts +
|
||||||
|
pausedDroidAccounts,
|
||||||
rateLimitedAccounts:
|
rateLimitedAccounts:
|
||||||
rateLimitedClaudeAccounts +
|
rateLimitedClaudeAccounts +
|
||||||
rateLimitedClaudeConsoleAccounts +
|
rateLimitedClaudeConsoleAccounts +
|
||||||
@@ -4676,7 +4761,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
rateLimitedBedrockAccounts +
|
rateLimitedBedrockAccounts +
|
||||||
rateLimitedOpenAIAccounts +
|
rateLimitedOpenAIAccounts +
|
||||||
rateLimitedOpenAIResponsesAccounts +
|
rateLimitedOpenAIResponsesAccounts +
|
||||||
rateLimitedCcrAccounts,
|
rateLimitedCcrAccounts +
|
||||||
|
rateLimitedDroidAccounts,
|
||||||
// 各平台详细统计
|
// 各平台详细统计
|
||||||
accountsByPlatform: {
|
accountsByPlatform: {
|
||||||
claude: {
|
claude: {
|
||||||
@@ -4727,6 +4813,13 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
abnormal: abnormalOpenAIResponsesAccounts,
|
abnormal: abnormalOpenAIResponsesAccounts,
|
||||||
paused: pausedOpenAIResponsesAccounts,
|
paused: pausedOpenAIResponsesAccounts,
|
||||||
rateLimited: rateLimitedOpenAIResponsesAccounts
|
rateLimited: rateLimitedOpenAIResponsesAccounts
|
||||||
|
},
|
||||||
|
droid: {
|
||||||
|
total: droidAccounts.length,
|
||||||
|
normal: normalDroidAccounts,
|
||||||
|
abnormal: abnormalDroidAccounts,
|
||||||
|
paused: pausedDroidAccounts,
|
||||||
|
rateLimited: rateLimitedDroidAccounts
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 保留旧字段以兼容
|
// 保留旧字段以兼容
|
||||||
@@ -4737,7 +4830,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
normalBedrockAccounts +
|
normalBedrockAccounts +
|
||||||
normalOpenAIAccounts +
|
normalOpenAIAccounts +
|
||||||
normalOpenAIResponsesAccounts +
|
normalOpenAIResponsesAccounts +
|
||||||
normalCcrAccounts,
|
normalCcrAccounts +
|
||||||
|
normalDroidAccounts,
|
||||||
totalClaudeAccounts: claudeAccounts.length + claudeConsoleAccounts.length,
|
totalClaudeAccounts: claudeAccounts.length + claudeConsoleAccounts.length,
|
||||||
activeClaudeAccounts: normalClaudeAccounts + normalClaudeConsoleAccounts,
|
activeClaudeAccounts: normalClaudeAccounts + normalClaudeConsoleAccounts,
|
||||||
rateLimitedClaudeAccounts: rateLimitedClaudeAccounts + rateLimitedClaudeConsoleAccounts,
|
rateLimitedClaudeAccounts: rateLimitedClaudeAccounts + rateLimitedClaudeConsoleAccounts,
|
||||||
@@ -4775,6 +4869,7 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
redisConnected: redis.isConnected,
|
redisConnected: redis.isConnected,
|
||||||
claudeAccountsHealthy: normalClaudeAccounts + normalClaudeConsoleAccounts > 0,
|
claudeAccountsHealthy: normalClaudeAccounts + normalClaudeConsoleAccounts > 0,
|
||||||
geminiAccountsHealthy: normalGeminiAccounts > 0,
|
geminiAccountsHealthy: normalGeminiAccounts > 0,
|
||||||
|
droidAccountsHealthy: normalDroidAccounts > 0,
|
||||||
uptime: process.uptime()
|
uptime: process.uptime()
|
||||||
},
|
},
|
||||||
systemTimezone: config.system.timezoneOffset || 8
|
systemTimezone: config.system.timezoneOffset || 8
|
||||||
@@ -8490,15 +8585,44 @@ router.post('/droid-accounts/exchange-code', authenticateAdmin, async (req, res)
|
|||||||
router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
|
router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const accounts = await droidAccountService.getAllAccounts()
|
const accounts = await droidAccountService.getAllAccounts()
|
||||||
|
const allApiKeys = await redis.getAllApiKeys()
|
||||||
|
|
||||||
// 添加使用统计
|
// 添加使用统计
|
||||||
const accountsWithStats = await Promise.all(
|
const accountsWithStats = await Promise.all(
|
||||||
accounts.map(async (account) => {
|
accounts.map(async (account) => {
|
||||||
try {
|
try {
|
||||||
const usageStats = await redis.getAccountUsageStats(account.id, 'droid')
|
const usageStats = await redis.getAccountUsageStats(account.id, 'droid')
|
||||||
|
let groupInfos = []
|
||||||
|
try {
|
||||||
|
groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
} catch (groupError) {
|
||||||
|
logger.debug(`Failed to get group infos for Droid account ${account.id}:`, groupError)
|
||||||
|
groupInfos = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupIds = groupInfos.map((group) => group.id)
|
||||||
|
const boundApiKeysCount = allApiKeys.reduce((count, key) => {
|
||||||
|
const binding = key.droidAccountId
|
||||||
|
if (!binding) {
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
if (binding === account.id) {
|
||||||
|
return count + 1
|
||||||
|
}
|
||||||
|
if (binding.startsWith('group:')) {
|
||||||
|
const groupId = binding.substring('group:'.length)
|
||||||
|
if (groupIds.includes(groupId)) {
|
||||||
|
return count + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}, 0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
schedulable: account.schedulable === 'true',
|
schedulable: account.schedulable === 'true',
|
||||||
|
boundApiKeysCount,
|
||||||
|
groupInfos,
|
||||||
usage: {
|
usage: {
|
||||||
daily: usageStats.daily,
|
daily: usageStats.daily,
|
||||||
total: usageStats.total,
|
total: usageStats.total,
|
||||||
@@ -8509,6 +8633,8 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
logger.warn(`Failed to get stats for Droid account ${account.id}:`, error.message)
|
logger.warn(`Failed to get stats for Droid account ${account.id}:`, error.message)
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
|
boundApiKeysCount: 0,
|
||||||
|
groupInfos: [],
|
||||||
usage: {
|
usage: {
|
||||||
daily: { tokens: 0, requests: 0 },
|
daily: { tokens: 0, requests: 0 },
|
||||||
total: { tokens: 0, requests: 0 },
|
total: { tokens: 0, requests: 0 },
|
||||||
|
|||||||
@@ -1,29 +1,47 @@
|
|||||||
|
const crypto = require('crypto')
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const { authenticateApiKey } = require('../middleware/auth')
|
const { authenticateApiKey } = require('../middleware/auth')
|
||||||
const droidRelayService = require('../services/droidRelayService')
|
const droidRelayService = require('../services/droidRelayService')
|
||||||
|
const sessionHelper = require('../utils/sessionHelper')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
|
function hasDroidPermission(apiKeyData) {
|
||||||
|
const permissions = apiKeyData?.permissions || 'all'
|
||||||
|
return permissions === 'all' || permissions === 'droid'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Droid API 转发路由
|
* Droid API 转发路由
|
||||||
*
|
*
|
||||||
* 支持多种 Factory.ai 端点:
|
* 支持的 Factory.ai 端点:
|
||||||
* - /droid/claude - Anthropic (Claude) Messages API
|
* - /droid/claude - Anthropic (Claude) Messages API
|
||||||
* - /droid/openai - OpenAI Responses API
|
* - /droid/openai - OpenAI Responses API
|
||||||
* - /droid/chat - OpenAI Chat Completions API (通用)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Claude (Anthropic) 端点 - /v1/messages
|
// Claude (Anthropic) 端点 - /v1/messages
|
||||||
router.post('/claude/v1/messages', authenticateApiKey, async (req, res) => {
|
router.post('/claude/v1/messages', authenticateApiKey, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const sessionHash = sessionHelper.generateSessionHash(req.body)
|
||||||
|
|
||||||
|
if (!hasDroidPermission(req.apiKey)) {
|
||||||
|
logger.security(
|
||||||
|
`🚫 API Key ${req.apiKey?.id || 'unknown'} 缺少 Droid 权限,拒绝访问 ${req.originalUrl}`
|
||||||
|
)
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'permission_denied',
|
||||||
|
message: '此 API Key 未启用 Droid 权限'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const result = await droidRelayService.relayRequest(
|
const result = await droidRelayService.relayRequest(
|
||||||
req.body,
|
req.body,
|
||||||
req.apiKey,
|
req.apiKey,
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
req.headers,
|
req.headers,
|
||||||
{ endpointType: 'anthropic' }
|
{ endpointType: 'anthropic', sessionHash }
|
||||||
)
|
)
|
||||||
|
|
||||||
// 如果是流式响应,已经在 relayService 中处理了
|
// 如果是流式响应,已经在 relayService 中处理了
|
||||||
@@ -45,13 +63,34 @@ router.post('/claude/v1/messages', authenticateApiKey, async (req, res) => {
|
|||||||
// OpenAI 端点 - /v1/responses
|
// OpenAI 端点 - /v1/responses
|
||||||
router.post('/openai/v1/responses', authenticateApiKey, async (req, res) => {
|
router.post('/openai/v1/responses', authenticateApiKey, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const sessionId =
|
||||||
|
req.headers['session_id'] ||
|
||||||
|
req.headers['x-session-id'] ||
|
||||||
|
req.body?.session_id ||
|
||||||
|
req.body?.conversation_id ||
|
||||||
|
null
|
||||||
|
|
||||||
|
const sessionHash = sessionId
|
||||||
|
? crypto.createHash('sha256').update(String(sessionId)).digest('hex')
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (!hasDroidPermission(req.apiKey)) {
|
||||||
|
logger.security(
|
||||||
|
`🚫 API Key ${req.apiKey?.id || 'unknown'} 缺少 Droid 权限,拒绝访问 ${req.originalUrl}`
|
||||||
|
)
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'permission_denied',
|
||||||
|
message: '此 API Key 未启用 Droid 权限'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const result = await droidRelayService.relayRequest(
|
const result = await droidRelayService.relayRequest(
|
||||||
req.body,
|
req.body,
|
||||||
req.apiKey,
|
req.apiKey,
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
req.headers,
|
req.headers,
|
||||||
{ endpointType: 'openai' }
|
{ endpointType: 'openai', sessionHash }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (result.streaming) {
|
if (result.streaming) {
|
||||||
@@ -68,32 +107,6 @@ router.post('/openai/v1/responses', authenticateApiKey, async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 通用 OpenAI Chat Completions 端点
|
|
||||||
router.post('/chat/v1/chat/completions', authenticateApiKey, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const result = await droidRelayService.relayRequest(
|
|
||||||
req.body,
|
|
||||||
req.apiKey,
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
req.headers,
|
|
||||||
{ endpointType: 'common' }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result.streaming) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(result.statusCode).set(result.headers).send(result.body)
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Droid Chat relay error:', error)
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'internal_server_error',
|
|
||||||
message: error.message
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 模型列表端点(兼容性)
|
// 模型列表端点(兼容性)
|
||||||
router.get('/*/v1/models', authenticateApiKey, async (req, res) => {
|
router.get('/*/v1/models', authenticateApiKey, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ class AccountGroupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证平台类型
|
// 验证平台类型
|
||||||
if (!['claude', 'gemini', 'openai'].includes(platform)) {
|
if (!['claude', 'gemini', 'openai', 'droid'].includes(platform)) {
|
||||||
throw new Error('平台类型必须是 claude、gemini 或 openai')
|
throw new Error('平台类型必须是 claude、gemini、openai 或 droid')
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
@@ -311,7 +311,8 @@ class AccountGroupService {
|
|||||||
keyData &&
|
keyData &&
|
||||||
(keyData.claudeAccountId === groupKey ||
|
(keyData.claudeAccountId === groupKey ||
|
||||||
keyData.geminiAccountId === groupKey ||
|
keyData.geminiAccountId === groupKey ||
|
||||||
keyData.openaiAccountId === groupKey)
|
keyData.openaiAccountId === groupKey ||
|
||||||
|
keyData.droidAccountId === groupKey)
|
||||||
) {
|
) {
|
||||||
boundApiKeys.push({
|
boundApiKeys.push({
|
||||||
id: keyId,
|
id: keyId,
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ class ApiKeyService {
|
|||||||
openaiAccountId = null,
|
openaiAccountId = null,
|
||||||
azureOpenaiAccountId = null,
|
azureOpenaiAccountId = null,
|
||||||
bedrockAccountId = null, // 添加 Bedrock 账号ID支持
|
bedrockAccountId = null, // 添加 Bedrock 账号ID支持
|
||||||
permissions = 'all', // 'claude', 'gemini', 'openai', 'all'
|
droidAccountId = null,
|
||||||
|
permissions = 'all', // 可选值:'claude'、'gemini'、'openai'、'droid' 或 'all'
|
||||||
isActive = true,
|
isActive = true,
|
||||||
concurrencyLimit = 0,
|
concurrencyLimit = 0,
|
||||||
rateLimitWindow = null,
|
rateLimitWindow = null,
|
||||||
@@ -64,6 +65,7 @@ class ApiKeyService {
|
|||||||
openaiAccountId: openaiAccountId || '',
|
openaiAccountId: openaiAccountId || '',
|
||||||
azureOpenaiAccountId: azureOpenaiAccountId || '',
|
azureOpenaiAccountId: azureOpenaiAccountId || '',
|
||||||
bedrockAccountId: bedrockAccountId || '', // 添加 Bedrock 账号ID
|
bedrockAccountId: bedrockAccountId || '', // 添加 Bedrock 账号ID
|
||||||
|
droidAccountId: droidAccountId || '',
|
||||||
permissions: permissions || 'all',
|
permissions: permissions || 'all',
|
||||||
enableModelRestriction: String(enableModelRestriction),
|
enableModelRestriction: String(enableModelRestriction),
|
||||||
restrictedModels: JSON.stringify(restrictedModels || []),
|
restrictedModels: JSON.stringify(restrictedModels || []),
|
||||||
@@ -109,6 +111,7 @@ class ApiKeyService {
|
|||||||
openaiAccountId: keyData.openaiAccountId,
|
openaiAccountId: keyData.openaiAccountId,
|
||||||
azureOpenaiAccountId: keyData.azureOpenaiAccountId,
|
azureOpenaiAccountId: keyData.azureOpenaiAccountId,
|
||||||
bedrockAccountId: keyData.bedrockAccountId, // 添加 Bedrock 账号ID
|
bedrockAccountId: keyData.bedrockAccountId, // 添加 Bedrock 账号ID
|
||||||
|
droidAccountId: keyData.droidAccountId,
|
||||||
permissions: keyData.permissions,
|
permissions: keyData.permissions,
|
||||||
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
||||||
restrictedModels: JSON.parse(keyData.restrictedModels),
|
restrictedModels: JSON.parse(keyData.restrictedModels),
|
||||||
@@ -256,6 +259,7 @@ class ApiKeyService {
|
|||||||
openaiAccountId: keyData.openaiAccountId,
|
openaiAccountId: keyData.openaiAccountId,
|
||||||
azureOpenaiAccountId: keyData.azureOpenaiAccountId,
|
azureOpenaiAccountId: keyData.azureOpenaiAccountId,
|
||||||
bedrockAccountId: keyData.bedrockAccountId, // 添加 Bedrock 账号ID
|
bedrockAccountId: keyData.bedrockAccountId, // 添加 Bedrock 账号ID
|
||||||
|
droidAccountId: keyData.droidAccountId,
|
||||||
permissions: keyData.permissions || 'all',
|
permissions: keyData.permissions || 'all',
|
||||||
tokenLimit: parseInt(keyData.tokenLimit),
|
tokenLimit: parseInt(keyData.tokenLimit),
|
||||||
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
|
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
|
||||||
@@ -382,6 +386,7 @@ class ApiKeyService {
|
|||||||
openaiAccountId: keyData.openaiAccountId,
|
openaiAccountId: keyData.openaiAccountId,
|
||||||
azureOpenaiAccountId: keyData.azureOpenaiAccountId,
|
azureOpenaiAccountId: keyData.azureOpenaiAccountId,
|
||||||
bedrockAccountId: keyData.bedrockAccountId,
|
bedrockAccountId: keyData.bedrockAccountId,
|
||||||
|
droidAccountId: keyData.droidAccountId,
|
||||||
permissions: keyData.permissions || 'all',
|
permissions: keyData.permissions || 'all',
|
||||||
tokenLimit: parseInt(keyData.tokenLimit),
|
tokenLimit: parseInt(keyData.tokenLimit),
|
||||||
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
|
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
|
||||||
@@ -553,6 +558,7 @@ class ApiKeyService {
|
|||||||
'openaiAccountId',
|
'openaiAccountId',
|
||||||
'azureOpenaiAccountId',
|
'azureOpenaiAccountId',
|
||||||
'bedrockAccountId', // 添加 Bedrock 账号ID
|
'bedrockAccountId', // 添加 Bedrock 账号ID
|
||||||
|
'droidAccountId',
|
||||||
'permissions',
|
'permissions',
|
||||||
'expiresAt',
|
'expiresAt',
|
||||||
'activationDays', // 新增:激活后有效天数
|
'activationDays', // 新增:激活后有效天数
|
||||||
@@ -1211,6 +1217,7 @@ class ApiKeyService {
|
|||||||
userId: key.userId,
|
userId: key.userId,
|
||||||
userUsername: key.userUsername,
|
userUsername: key.userUsername,
|
||||||
createdBy: key.createdBy,
|
createdBy: key.createdBy,
|
||||||
|
droidAccountId: key.droidAccountId,
|
||||||
// Include deletion fields for deleted keys
|
// Include deletion fields for deleted keys
|
||||||
isDeleted: key.isDeleted,
|
isDeleted: key.isDeleted,
|
||||||
deletedAt: key.deletedAt,
|
deletedAt: key.deletedAt,
|
||||||
@@ -1254,7 +1261,8 @@ class ApiKeyService {
|
|||||||
createdBy: keyData.createdBy,
|
createdBy: keyData.createdBy,
|
||||||
permissions: keyData.permissions,
|
permissions: keyData.permissions,
|
||||||
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
|
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
|
||||||
totalCostLimit: parseFloat(keyData.totalCostLimit || 0)
|
totalCostLimit: parseFloat(keyData.totalCostLimit || 0),
|
||||||
|
droidAccountId: keyData.droidAccountId
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to get API key by ID:', error)
|
logger.error('❌ Failed to get API key by ID:', error)
|
||||||
@@ -1401,6 +1409,7 @@ class ApiKeyService {
|
|||||||
'openai-responses': 'openaiAccountId', // 特殊处理,带 responses: 前缀
|
'openai-responses': 'openaiAccountId', // 特殊处理,带 responses: 前缀
|
||||||
azure_openai: 'azureOpenaiAccountId',
|
azure_openai: 'azureOpenaiAccountId',
|
||||||
bedrock: 'bedrockAccountId',
|
bedrock: 'bedrockAccountId',
|
||||||
|
droid: 'droidAccountId',
|
||||||
ccr: null // CCR 账号没有对应的 API Key 字段
|
ccr: null // CCR 账号没有对应的 API Key 字段
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,25 @@ class DroidAccountService {
|
|||||||
},
|
},
|
||||||
10 * 60 * 1000
|
10 * 60 * 1000
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.supportedEndpointTypes = new Set(['anthropic', 'openai'])
|
||||||
|
}
|
||||||
|
|
||||||
|
_sanitizeEndpointType(endpointType) {
|
||||||
|
if (!endpointType) {
|
||||||
|
return 'anthropic'
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = String(endpointType).toLowerCase()
|
||||||
|
if (normalized === 'openai' || normalized === 'common') {
|
||||||
|
return 'openai'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.supportedEndpointTypes.has(normalized)) {
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'anthropic'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,7 +136,7 @@ class DroidAccountService {
|
|||||||
/**
|
/**
|
||||||
* 使用 WorkOS Refresh Token 刷新并验证凭证
|
* 使用 WorkOS Refresh Token 刷新并验证凭证
|
||||||
*/
|
*/
|
||||||
async _refreshTokensWithWorkOS(refreshToken, proxyConfig = null) {
|
async _refreshTokensWithWorkOS(refreshToken, proxyConfig = null, organizationId = null) {
|
||||||
if (!refreshToken || typeof refreshToken !== 'string') {
|
if (!refreshToken || typeof refreshToken !== 'string') {
|
||||||
throw new Error('Refresh Token 无效')
|
throw new Error('Refresh Token 无效')
|
||||||
}
|
}
|
||||||
@@ -126,6 +145,9 @@ class DroidAccountService {
|
|||||||
formData.append('grant_type', 'refresh_token')
|
formData.append('grant_type', 'refresh_token')
|
||||||
formData.append('refresh_token', refreshToken)
|
formData.append('refresh_token', refreshToken)
|
||||||
formData.append('client_id', this.workosClientId)
|
formData.append('client_id', this.workosClientId)
|
||||||
|
if (organizationId) {
|
||||||
|
formData.append('organization_id', organizationId)
|
||||||
|
}
|
||||||
|
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -184,6 +206,49 @@ class DroidAccountService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 Factory CLI 接口获取组织 ID 列表
|
||||||
|
*/
|
||||||
|
async _fetchFactoryOrgIds(accessToken, proxyConfig = null) {
|
||||||
|
if (!accessToken) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
url: 'https://app.factory.ai/api/cli/org',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
'x-factory-client': 'cli',
|
||||||
|
'User-Agent': this.userAgent
|
||||||
|
},
|
||||||
|
timeout: 15000
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxyConfig) {
|
||||||
|
const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig)
|
||||||
|
if (proxyAgent) {
|
||||||
|
requestOptions.httpAgent = proxyAgent
|
||||||
|
requestOptions.httpsAgent = proxyAgent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios(requestOptions)
|
||||||
|
const data = response.data || {}
|
||||||
|
if (Array.isArray(data.workosOrgIds) && data.workosOrgIds.length > 0) {
|
||||||
|
return data.workosOrgIds
|
||||||
|
}
|
||||||
|
logger.warn('⚠️ 未从 Factory CLI 接口获取到 workosOrgIds')
|
||||||
|
return []
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('⚠️ 获取 Factory 组织信息失败:', error.message)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建 Droid 账户
|
* 创建 Droid 账户
|
||||||
*
|
*
|
||||||
@@ -203,7 +268,7 @@ class DroidAccountService {
|
|||||||
platform = 'droid',
|
platform = 'droid',
|
||||||
priority = 50, // 调度优先级 (1-100)
|
priority = 50, // 调度优先级 (1-100)
|
||||||
schedulable = true, // 是否可被调度
|
schedulable = true, // 是否可被调度
|
||||||
endpointType = 'anthropic', // 默认端点类型: 'anthropic', 'openai', 'common'
|
endpointType = 'anthropic', // 默认端点类型: 'anthropic' 或 'openai'
|
||||||
organizationId = '',
|
organizationId = '',
|
||||||
ownerEmail = '',
|
ownerEmail = '',
|
||||||
ownerName = '',
|
ownerName = '',
|
||||||
@@ -215,6 +280,8 @@ class DroidAccountService {
|
|||||||
|
|
||||||
const accountId = uuidv4()
|
const accountId = uuidv4()
|
||||||
|
|
||||||
|
const normalizedEndpointType = this._sanitizeEndpointType(endpointType)
|
||||||
|
|
||||||
let normalizedRefreshToken = refreshToken
|
let normalizedRefreshToken = refreshToken
|
||||||
let normalizedAccessToken = accessToken
|
let normalizedAccessToken = accessToken
|
||||||
let normalizedExpiresAt = expiresAt || ''
|
let normalizedExpiresAt = expiresAt || ''
|
||||||
@@ -229,22 +296,40 @@ class DroidAccountService {
|
|||||||
let lastRefreshAt = accessToken ? new Date().toISOString() : ''
|
let lastRefreshAt = accessToken ? new Date().toISOString() : ''
|
||||||
let status = accessToken ? 'active' : 'created'
|
let status = accessToken ? 'active' : 'created'
|
||||||
|
|
||||||
if (normalizedRefreshToken) {
|
const isManualProvision =
|
||||||
try {
|
typeof authenticationMethod === 'string' &&
|
||||||
let proxyConfig = null
|
authenticationMethod.toLowerCase().trim() === 'manual'
|
||||||
if (proxy && typeof proxy === 'object') {
|
|
||||||
proxyConfig = proxy
|
|
||||||
} else if (typeof proxy === 'string' && proxy.trim()) {
|
|
||||||
try {
|
|
||||||
proxyConfig = JSON.parse(proxy)
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn('⚠️ Droid 手动账号代理配置解析失败,已忽略:', error.message)
|
|
||||||
proxyConfig = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const provisioningMode = isManualProvision ? 'manual' : 'oauth'
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`🔍 [Droid ${provisioningMode}] 初始令牌 - AccountName: ${name}, AccessToken: ${normalizedAccessToken || '[empty]'}, RefreshToken: ${normalizedRefreshToken || '[empty]'}`
|
||||||
|
)
|
||||||
|
|
||||||
|
let proxyConfig = null
|
||||||
|
if (proxy && typeof proxy === 'object') {
|
||||||
|
proxyConfig = proxy
|
||||||
|
} else if (typeof proxy === 'string' && proxy.trim()) {
|
||||||
|
try {
|
||||||
|
proxyConfig = JSON.parse(proxy)
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('⚠️ Droid 代理配置解析失败,已忽略:', error.message)
|
||||||
|
proxyConfig = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedRefreshToken && isManualProvision) {
|
||||||
|
try {
|
||||||
const refreshed = await this._refreshTokensWithWorkOS(normalizedRefreshToken, proxyConfig)
|
const refreshed = await this._refreshTokensWithWorkOS(normalizedRefreshToken, proxyConfig)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`🔍 [Droid manual] 刷新后令牌 - AccountName: ${name}, AccessToken: ${refreshed.accessToken || '[empty]'}, RefreshToken: ${refreshed.refreshToken || '[empty]'}, ExpiresAt: ${refreshed.expiresAt || '[empty]'}, ExpiresIn: ${
|
||||||
|
refreshed.expiresIn !== null && refreshed.expiresIn !== undefined
|
||||||
|
? refreshed.expiresIn
|
||||||
|
: '[empty]'
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
|
||||||
normalizedAccessToken = refreshed.accessToken
|
normalizedAccessToken = refreshed.accessToken
|
||||||
normalizedRefreshToken = refreshed.refreshToken
|
normalizedRefreshToken = refreshed.refreshToken
|
||||||
normalizedExpiresAt = refreshed.expiresAt || normalizedExpiresAt
|
normalizedExpiresAt = refreshed.expiresAt || normalizedExpiresAt
|
||||||
@@ -296,8 +381,113 @@ class DroidAccountService {
|
|||||||
logger.error('❌ 使用 Refresh Token 验证 Droid 账户失败:', error)
|
logger.error('❌ 使用 Refresh Token 验证 Droid 账户失败:', error)
|
||||||
throw new Error(`Refresh Token 验证失败:${error.message}`)
|
throw new Error(`Refresh Token 验证失败:${error.message}`)
|
||||||
}
|
}
|
||||||
|
} else if (normalizedRefreshToken && !isManualProvision) {
|
||||||
|
try {
|
||||||
|
const orgIds = await this._fetchFactoryOrgIds(normalizedAccessToken, proxyConfig)
|
||||||
|
const selectedOrgId =
|
||||||
|
normalizedOrganizationId ||
|
||||||
|
(Array.isArray(orgIds)
|
||||||
|
? orgIds.find((id) => typeof id === 'string' && id.trim())
|
||||||
|
: null) ||
|
||||||
|
''
|
||||||
|
|
||||||
|
if (!selectedOrgId) {
|
||||||
|
logger.warn(`⚠️ [Droid oauth] 未获取到组织ID,跳过 WorkOS 刷新: ${name} (${accountId})`)
|
||||||
|
} else {
|
||||||
|
const refreshed = await this._refreshTokensWithWorkOS(
|
||||||
|
normalizedRefreshToken,
|
||||||
|
proxyConfig,
|
||||||
|
selectedOrgId
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`🔍 [Droid oauth] 组织刷新后令牌 - AccountName: ${name}, AccessToken: ${refreshed.accessToken || '[empty]'}, RefreshToken: ${refreshed.refreshToken || '[empty]'}, OrganizationId: ${
|
||||||
|
refreshed.organizationId || selectedOrgId
|
||||||
|
}, ExpiresAt: ${refreshed.expiresAt || '[empty]'}`
|
||||||
|
)
|
||||||
|
|
||||||
|
normalizedAccessToken = refreshed.accessToken
|
||||||
|
normalizedRefreshToken = refreshed.refreshToken
|
||||||
|
normalizedExpiresAt = refreshed.expiresAt || normalizedExpiresAt
|
||||||
|
normalizedTokenType = refreshed.tokenType || normalizedTokenType
|
||||||
|
normalizedAuthenticationMethod =
|
||||||
|
refreshed.authenticationMethod || normalizedAuthenticationMethod
|
||||||
|
if (refreshed.expiresIn !== null && refreshed.expiresIn !== undefined) {
|
||||||
|
normalizedExpiresIn = refreshed.expiresIn
|
||||||
|
}
|
||||||
|
if (refreshed.organizationId) {
|
||||||
|
normalizedOrganizationId = refreshed.organizationId
|
||||||
|
} else {
|
||||||
|
normalizedOrganizationId = selectedOrgId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshed.user) {
|
||||||
|
const userInfo = refreshed.user
|
||||||
|
if (typeof userInfo.email === 'string' && userInfo.email.trim()) {
|
||||||
|
normalizedOwnerEmail = userInfo.email.trim()
|
||||||
|
}
|
||||||
|
const nameParts = []
|
||||||
|
if (typeof userInfo.first_name === 'string' && userInfo.first_name.trim()) {
|
||||||
|
nameParts.push(userInfo.first_name.trim())
|
||||||
|
}
|
||||||
|
if (typeof userInfo.last_name === 'string' && userInfo.last_name.trim()) {
|
||||||
|
nameParts.push(userInfo.last_name.trim())
|
||||||
|
}
|
||||||
|
const derivedName =
|
||||||
|
nameParts.join(' ').trim() ||
|
||||||
|
(typeof userInfo.name === 'string' ? userInfo.name.trim() : '') ||
|
||||||
|
(typeof userInfo.display_name === 'string' ? userInfo.display_name.trim() : '')
|
||||||
|
|
||||||
|
if (derivedName) {
|
||||||
|
normalizedOwnerName = derivedName
|
||||||
|
normalizedOwnerDisplayName = derivedName
|
||||||
|
} else if (normalizedOwnerEmail) {
|
||||||
|
normalizedOwnerName = normalizedOwnerName || normalizedOwnerEmail
|
||||||
|
normalizedOwnerDisplayName =
|
||||||
|
normalizedOwnerDisplayName || normalizedOwnerEmail || normalizedOwnerName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof userInfo.id === 'string' && userInfo.id.trim()) {
|
||||||
|
normalizedUserId = userInfo.id.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRefreshAt = new Date().toISOString()
|
||||||
|
status = 'active'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`⚠️ [Droid oauth] 初始化刷新失败: ${name} (${accountId}) - ${error.message}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!normalizedExpiresAt) {
|
||||||
|
let expiresInSeconds = null
|
||||||
|
if (typeof normalizedExpiresIn === 'number' && Number.isFinite(normalizedExpiresIn)) {
|
||||||
|
expiresInSeconds = normalizedExpiresIn
|
||||||
|
} else if (
|
||||||
|
typeof normalizedExpiresIn === 'string' &&
|
||||||
|
normalizedExpiresIn.trim() &&
|
||||||
|
!Number.isNaN(Number(normalizedExpiresIn))
|
||||||
|
) {
|
||||||
|
expiresInSeconds = Number(normalizedExpiresIn)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isFinite(expiresInSeconds) || expiresInSeconds <= 0) {
|
||||||
|
expiresInSeconds = this.tokenValidHours * 3600
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedExpiresAt = new Date(Date.now() + expiresInSeconds * 1000).toISOString()
|
||||||
|
normalizedExpiresIn = expiresInSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`🔍 [Droid ${provisioningMode}] 写入前令牌快照 - AccountName: ${name}, AccessToken: ${normalizedAccessToken || '[empty]'}, RefreshToken: ${normalizedRefreshToken || '[empty]'}, ExpiresAt: ${normalizedExpiresAt || '[empty]'}, ExpiresIn: ${
|
||||||
|
normalizedExpiresIn !== null && normalizedExpiresIn !== undefined
|
||||||
|
? normalizedExpiresIn
|
||||||
|
: '[empty]'
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
|
||||||
const accountData = {
|
const accountData = {
|
||||||
id: accountId,
|
id: accountId,
|
||||||
name,
|
name,
|
||||||
@@ -316,7 +506,7 @@ class DroidAccountService {
|
|||||||
status, // created, active, expired, error
|
status, // created, active, expired, error
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
schedulable: schedulable.toString(),
|
schedulable: schedulable.toString(),
|
||||||
endpointType, // anthropic, openai, common
|
endpointType: normalizedEndpointType, // anthropic 或 openai
|
||||||
organizationId: normalizedOrganizationId || '',
|
organizationId: normalizedOrganizationId || '',
|
||||||
owner: normalizedOwnerName || normalizedOwnerEmail || '',
|
owner: normalizedOwnerName || normalizedOwnerEmail || '',
|
||||||
ownerEmail: normalizedOwnerEmail || '',
|
ownerEmail: normalizedOwnerEmail || '',
|
||||||
@@ -334,7 +524,20 @@ class DroidAccountService {
|
|||||||
|
|
||||||
await redis.setDroidAccount(accountId, accountData)
|
await redis.setDroidAccount(accountId, accountData)
|
||||||
|
|
||||||
logger.success(`🏢 Created Droid account: ${name} (${accountId}) - Endpoint: ${endpointType}`)
|
logger.success(
|
||||||
|
`🏢 Created Droid account: ${name} (${accountId}) - Endpoint: ${normalizedEndpointType}`
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const verifyAccount = await this.getAccount(accountId)
|
||||||
|
logger.info(
|
||||||
|
`🔍 [Droid ${provisioningMode}] Redis 写入后验证 - AccountName: ${name}, AccessToken: ${verifyAccount?.accessToken || '[empty]'}, RefreshToken: ${verifyAccount?.refreshToken || '[empty]'}, ExpiresAt: ${verifyAccount?.expiresAt || '[empty]'}`
|
||||||
|
)
|
||||||
|
} catch (verifyError) {
|
||||||
|
logger.warn(
|
||||||
|
`⚠️ [Droid ${provisioningMode}] 写入后验证失败: ${name} (${accountId}) - ${verifyError.message}`
|
||||||
|
)
|
||||||
|
}
|
||||||
return { id: accountId, ...accountData }
|
return { id: accountId, ...accountData }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,6 +553,8 @@ class DroidAccountService {
|
|||||||
// 解密敏感数据
|
// 解密敏感数据
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
|
id: accountId,
|
||||||
|
endpointType: this._sanitizeEndpointType(account.endpointType),
|
||||||
refreshToken: this._decryptSensitiveData(account.refreshToken),
|
refreshToken: this._decryptSensitiveData(account.refreshToken),
|
||||||
accessToken: this._decryptSensitiveData(account.accessToken)
|
accessToken: this._decryptSensitiveData(account.accessToken)
|
||||||
}
|
}
|
||||||
@@ -362,6 +567,7 @@ class DroidAccountService {
|
|||||||
const accounts = await redis.getAllDroidAccounts()
|
const accounts = await redis.getAllDroidAccounts()
|
||||||
return accounts.map((account) => ({
|
return accounts.map((account) => ({
|
||||||
...account,
|
...account,
|
||||||
|
endpointType: this._sanitizeEndpointType(account.endpointType),
|
||||||
// 不解密完整 token,只返回掩码
|
// 不解密完整 token,只返回掩码
|
||||||
refreshToken: account.refreshToken ? '***ENCRYPTED***' : '',
|
refreshToken: account.refreshToken ? '***ENCRYPTED***' : '',
|
||||||
accessToken: account.accessToken
|
accessToken: account.accessToken
|
||||||
@@ -388,6 +594,10 @@ class DroidAccountService {
|
|||||||
sanitizedUpdates.refreshToken = sanitizedUpdates.refreshToken.trim()
|
sanitizedUpdates.refreshToken = sanitizedUpdates.refreshToken.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sanitizedUpdates.endpointType) {
|
||||||
|
sanitizedUpdates.endpointType = this._sanitizeEndpointType(sanitizedUpdates.endpointType)
|
||||||
|
}
|
||||||
|
|
||||||
const parseProxyConfig = (value) => {
|
const parseProxyConfig = (value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null
|
return null
|
||||||
@@ -547,7 +757,11 @@ class DroidAccountService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const proxy = proxyConfig || (account.proxy ? JSON.parse(account.proxy) : null)
|
const proxy = proxyConfig || (account.proxy ? JSON.parse(account.proxy) : null)
|
||||||
const refreshed = await this._refreshTokensWithWorkOS(account.refreshToken, proxy)
|
const refreshed = await this._refreshTokensWithWorkOS(
|
||||||
|
account.refreshToken,
|
||||||
|
proxy,
|
||||||
|
account.organizationId || null
|
||||||
|
)
|
||||||
|
|
||||||
// 更新账户信息
|
// 更新账户信息
|
||||||
await this.updateAccount(accountId, {
|
await this.updateAccount(accountId, {
|
||||||
@@ -673,6 +887,8 @@ class DroidAccountService {
|
|||||||
async getSchedulableAccounts(endpointType = null) {
|
async getSchedulableAccounts(endpointType = null) {
|
||||||
const allAccounts = await redis.getAllDroidAccounts()
|
const allAccounts = await redis.getAllDroidAccounts()
|
||||||
|
|
||||||
|
const normalizedFilter = endpointType ? this._sanitizeEndpointType(endpointType) : null
|
||||||
|
|
||||||
return allAccounts
|
return allAccounts
|
||||||
.filter((account) => {
|
.filter((account) => {
|
||||||
// 基本过滤条件
|
// 基本过滤条件
|
||||||
@@ -681,15 +897,29 @@ class DroidAccountService {
|
|||||||
account.schedulable === 'true' &&
|
account.schedulable === 'true' &&
|
||||||
account.status === 'active'
|
account.status === 'active'
|
||||||
|
|
||||||
// 如果指定了端点类型,进一步过滤
|
if (!isSchedulable) {
|
||||||
if (endpointType) {
|
return false
|
||||||
return isSchedulable && account.endpointType === endpointType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return isSchedulable
|
if (!normalizedFilter) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountEndpoint = this._sanitizeEndpointType(account.endpointType)
|
||||||
|
|
||||||
|
if (normalizedFilter === 'openai') {
|
||||||
|
return accountEndpoint === 'openai' || accountEndpoint === 'anthropic'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedFilter === 'anthropic') {
|
||||||
|
return accountEndpoint === 'anthropic' || accountEndpoint === 'openai'
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountEndpoint === normalizedFilter
|
||||||
})
|
})
|
||||||
.map((account) => ({
|
.map((account) => ({
|
||||||
...account,
|
...account,
|
||||||
|
endpointType: this._sanitizeEndpointType(account.endpointType),
|
||||||
priority: parseInt(account.priority, 10) || 50,
|
priority: parseInt(account.priority, 10) || 50,
|
||||||
// 解密 accessToken 用于使用
|
// 解密 accessToken 用于使用
|
||||||
accessToken: this._decryptSensitiveData(account.accessToken)
|
accessToken: this._decryptSensitiveData(account.accessToken)
|
||||||
@@ -737,7 +967,7 @@ class DroidAccountService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`✅ Selected Droid account: ${selectedAccount.name} (${selectedAccount.id}) - Endpoint: ${selectedAccount.endpointType}`
|
`✅ Selected Droid account: ${selectedAccount.name} (${selectedAccount.id}) - Endpoint: ${this._sanitizeEndpointType(selectedAccount.endpointType)}`
|
||||||
)
|
)
|
||||||
|
|
||||||
return selectedAccount
|
return selectedAccount
|
||||||
@@ -747,13 +977,26 @@ class DroidAccountService {
|
|||||||
* 获取 Factory.ai API 的完整 URL
|
* 获取 Factory.ai API 的完整 URL
|
||||||
*/
|
*/
|
||||||
getFactoryApiUrl(endpointType, endpoint) {
|
getFactoryApiUrl(endpointType, endpoint) {
|
||||||
|
const normalizedType = this._sanitizeEndpointType(endpointType)
|
||||||
const baseUrls = {
|
const baseUrls = {
|
||||||
anthropic: `${this.factoryApiBaseUrl}/a${endpoint}`,
|
anthropic: `${this.factoryApiBaseUrl}/a${endpoint}`,
|
||||||
openai: `${this.factoryApiBaseUrl}/o${endpoint}`,
|
openai: `${this.factoryApiBaseUrl}/o${endpoint}`
|
||||||
common: `${this.factoryApiBaseUrl}/o${endpoint}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseUrls[endpointType] || baseUrls.common
|
return baseUrls[normalizedType] || baseUrls.openai
|
||||||
|
}
|
||||||
|
|
||||||
|
async touchLastUsedAt(accountId) {
|
||||||
|
if (!accountId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = redis.getClientSafe()
|
||||||
|
await client.hset(`droid:account:${accountId}`, 'lastUsedAt', new Date().toISOString())
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`⚠️ Failed to update lastUsedAt for Droid account ${accountId}:`, error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
const https = require('https')
|
const https = require('https')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const ProxyHelper = require('../utils/proxyHelper')
|
const ProxyHelper = require('../utils/proxyHelper')
|
||||||
|
const droidScheduler = require('./droidScheduler')
|
||||||
const droidAccountService = require('./droidAccountService')
|
const droidAccountService = require('./droidAccountService')
|
||||||
|
const apiKeyService = require('./apiKeyService')
|
||||||
const redis = require('../models/redis')
|
const redis = require('../models/redis')
|
||||||
|
const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
|
|
||||||
const SYSTEM_PROMPT =
|
const SYSTEM_PROMPT =
|
||||||
@@ -28,8 +31,7 @@ class DroidRelayService {
|
|||||||
|
|
||||||
this.endpoints = {
|
this.endpoints = {
|
||||||
anthropic: '/a/v1/messages',
|
anthropic: '/a/v1/messages',
|
||||||
openai: '/o/v1/responses',
|
openai: '/o/v1/responses'
|
||||||
common: '/o/v1/chat/completions'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userAgent = 'factory-cli/0.19.4'
|
this.userAgent = 'factory-cli/0.19.4'
|
||||||
@@ -45,6 +47,46 @@ class DroidRelayService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_normalizeEndpointType(endpointType) {
|
||||||
|
if (!endpointType) {
|
||||||
|
return 'anthropic'
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = String(endpointType).toLowerCase()
|
||||||
|
if (normalized === 'openai' || normalized === 'common') {
|
||||||
|
return 'openai'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === 'anthropic') {
|
||||||
|
return 'anthropic'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'anthropic'
|
||||||
|
}
|
||||||
|
|
||||||
|
async _applyRateLimitTracking(rateLimitInfo, usageSummary, model, context = '') {
|
||||||
|
if (!rateLimitInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { totalTokens, totalCost } = await updateRateLimitCounters(
|
||||||
|
rateLimitInfo,
|
||||||
|
usageSummary,
|
||||||
|
model
|
||||||
|
)
|
||||||
|
|
||||||
|
if (totalTokens > 0) {
|
||||||
|
logger.api(`📊 Updated rate limit token count${context}: +${totalTokens}`)
|
||||||
|
}
|
||||||
|
if (typeof totalCost === 'number' && totalCost > 0) {
|
||||||
|
logger.api(`💰 Updated rate limit cost count${context}: +$${totalCost.toFixed(6)}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to update rate limit counters${context}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async relayRequest(
|
async relayRequest(
|
||||||
requestBody,
|
requestBody,
|
||||||
apiKeyData,
|
apiKeyData,
|
||||||
@@ -53,26 +95,29 @@ class DroidRelayService {
|
|||||||
clientHeaders,
|
clientHeaders,
|
||||||
options = {}
|
options = {}
|
||||||
) {
|
) {
|
||||||
const { endpointType = 'anthropic' } = options
|
const { endpointType = 'anthropic', sessionHash = null } = options
|
||||||
const keyInfo = apiKeyData || {}
|
const keyInfo = apiKeyData || {}
|
||||||
|
const normalizedEndpoint = this._normalizeEndpointType(endpointType)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info(
|
logger.info(
|
||||||
`📤 Processing Droid API request for key: ${keyInfo.name || keyInfo.id || 'unknown'}, endpoint: ${endpointType}`
|
`📤 Processing Droid API request for key: ${
|
||||||
|
keyInfo.name || keyInfo.id || 'unknown'
|
||||||
|
}, endpoint: ${normalizedEndpoint}${sessionHash ? `, session: ${sessionHash}` : ''}`
|
||||||
)
|
)
|
||||||
|
|
||||||
// 选择一个可用的 Droid 账户
|
// 选择一个可用的 Droid 账户(支持粘性会话和分组调度)
|
||||||
const account = await droidAccountService.selectAccount(endpointType)
|
const account = await droidScheduler.selectAccount(keyInfo, normalizedEndpoint, sessionHash)
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error(`No available Droid account for endpoint type: ${endpointType}`)
|
throw new Error(`No available Droid account for endpoint type: ${normalizedEndpoint}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取有效的 access token(自动刷新)
|
// 获取有效的 access token(自动刷新)
|
||||||
const accessToken = await droidAccountService.getValidAccessToken(account.id)
|
const accessToken = await droidAccountService.getValidAccessToken(account.id)
|
||||||
|
|
||||||
// 获取 Factory.ai API URL
|
// 获取 Factory.ai API URL
|
||||||
const endpoint = this.endpoints[endpointType]
|
const endpoint = this.endpoints[normalizedEndpoint]
|
||||||
const apiUrl = `${this.factoryApiBaseUrl}${endpoint}`
|
const apiUrl = `${this.factoryApiBaseUrl}${endpoint}`
|
||||||
|
|
||||||
logger.info(`🌐 Forwarding to Factory.ai: ${apiUrl}`)
|
logger.info(`🌐 Forwarding to Factory.ai: ${apiUrl}`)
|
||||||
@@ -86,10 +131,15 @@ class DroidRelayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 构建请求头
|
// 构建请求头
|
||||||
const headers = this._buildHeaders(accessToken, requestBody, endpointType, clientHeaders)
|
const headers = this._buildHeaders(
|
||||||
|
accessToken,
|
||||||
|
requestBody,
|
||||||
|
normalizedEndpoint,
|
||||||
|
clientHeaders
|
||||||
|
)
|
||||||
|
|
||||||
// 处理请求体(注入 system prompt 等)
|
// 处理请求体(注入 system prompt 等)
|
||||||
const processedBody = this._processRequestBody(requestBody, endpointType)
|
const processedBody = this._processRequestBody(requestBody, normalizedEndpoint)
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
const isStreaming = processedBody.stream !== false
|
const isStreaming = processedBody.stream !== false
|
||||||
@@ -102,11 +152,12 @@ class DroidRelayService {
|
|||||||
headers,
|
headers,
|
||||||
processedBody,
|
processedBody,
|
||||||
proxyAgent,
|
proxyAgent,
|
||||||
|
clientRequest,
|
||||||
clientResponse,
|
clientResponse,
|
||||||
account,
|
account,
|
||||||
keyInfo,
|
keyInfo,
|
||||||
requestBody,
|
requestBody,
|
||||||
endpointType
|
normalizedEndpoint
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// 非流式响应:使用 axios
|
// 非流式响应:使用 axios
|
||||||
@@ -128,7 +179,14 @@ class DroidRelayService {
|
|||||||
logger.info(`✅ Factory.ai response status: ${response.status}`)
|
logger.info(`✅ Factory.ai response status: ${response.status}`)
|
||||||
|
|
||||||
// 处理非流式响应
|
// 处理非流式响应
|
||||||
return this._handleNonStreamResponse(response, account, keyInfo, requestBody)
|
return this._handleNonStreamResponse(
|
||||||
|
response,
|
||||||
|
account,
|
||||||
|
keyInfo,
|
||||||
|
requestBody,
|
||||||
|
clientRequest,
|
||||||
|
normalizedEndpoint
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ Droid relay error: ${error.message}`, error)
|
logger.error(`❌ Droid relay error: ${error.message}`, error)
|
||||||
@@ -167,6 +225,7 @@ class DroidRelayService {
|
|||||||
headers,
|
headers,
|
||||||
processedBody,
|
processedBody,
|
||||||
proxyAgent,
|
proxyAgent,
|
||||||
|
clientRequest,
|
||||||
clientResponse,
|
clientResponse,
|
||||||
account,
|
account,
|
||||||
apiKeyData,
|
apiKeyData,
|
||||||
@@ -181,6 +240,7 @@ class DroidRelayService {
|
|||||||
...headers,
|
...headers,
|
||||||
'content-length': contentLength.toString()
|
'content-length': contentLength.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
let responseStarted = false
|
let responseStarted = false
|
||||||
let responseCompleted = false
|
let responseCompleted = false
|
||||||
let settled = false
|
let settled = false
|
||||||
@@ -298,12 +358,13 @@ class DroidRelayService {
|
|||||||
|
|
||||||
// 转发数据到客户端
|
// 转发数据到客户端
|
||||||
clientResponse.write(chunk)
|
clientResponse.write(chunk)
|
||||||
|
hasForwardedData = true
|
||||||
|
|
||||||
// 解析 usage 数据(根据端点类型)
|
// 解析 usage 数据(根据端点类型)
|
||||||
if (endpointType === 'anthropic') {
|
if (endpointType === 'anthropic') {
|
||||||
// Anthropic Messages API 格式
|
// Anthropic Messages API 格式
|
||||||
this._parseAnthropicUsageFromSSE(chunkStr, buffer, currentUsageData)
|
this._parseAnthropicUsageFromSSE(chunkStr, buffer, currentUsageData)
|
||||||
} else if (endpointType === 'openai' || endpointType === 'common') {
|
} else if (endpointType === 'openai') {
|
||||||
// OpenAI Chat Completions 格式
|
// OpenAI Chat Completions 格式
|
||||||
this._parseOpenAIUsageFromSSE(chunkStr, buffer, currentUsageData)
|
this._parseOpenAIUsageFromSSE(chunkStr, buffer, currentUsageData)
|
||||||
}
|
}
|
||||||
@@ -320,7 +381,26 @@ class DroidRelayService {
|
|||||||
clientResponse.end()
|
clientResponse.end()
|
||||||
|
|
||||||
// 记录 usage 数据
|
// 记录 usage 数据
|
||||||
await this._recordUsageFromStreamData(currentUsageData, apiKeyData, account, model)
|
const normalizedUsage = await this._recordUsageFromStreamData(
|
||||||
|
currentUsageData,
|
||||||
|
apiKeyData,
|
||||||
|
account,
|
||||||
|
model
|
||||||
|
)
|
||||||
|
|
||||||
|
const usageSummary = {
|
||||||
|
inputTokens: normalizedUsage.input_tokens || 0,
|
||||||
|
outputTokens: normalizedUsage.output_tokens || 0,
|
||||||
|
cacheCreateTokens: normalizedUsage.cache_creation_input_tokens || 0,
|
||||||
|
cacheReadTokens: normalizedUsage.cache_read_input_tokens || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._applyRateLimitTracking(
|
||||||
|
clientRequest?.rateLimitInfo,
|
||||||
|
usageSummary,
|
||||||
|
model,
|
||||||
|
' [stream]'
|
||||||
|
)
|
||||||
|
|
||||||
logger.success(`✅ Droid stream completed - Account: ${account.name}`)
|
logger.success(`✅ Droid stream completed - Account: ${account.name}`)
|
||||||
resolveOnce({ statusCode: 200, streaming: true })
|
resolveOnce({ statusCode: 200, streaming: true })
|
||||||
@@ -432,7 +512,7 @@ class DroidRelayService {
|
|||||||
|
|
||||||
const data = JSON.parse(jsonStr)
|
const data = JSON.parse(jsonStr)
|
||||||
|
|
||||||
// OpenAI 格式在流结束时可能包含 usage
|
// 兼容传统 Chat Completions usage 字段
|
||||||
if (data.usage) {
|
if (data.usage) {
|
||||||
currentUsageData.input_tokens = data.usage.prompt_tokens || 0
|
currentUsageData.input_tokens = data.usage.prompt_tokens || 0
|
||||||
currentUsageData.output_tokens = data.usage.completion_tokens || 0
|
currentUsageData.output_tokens = data.usage.completion_tokens || 0
|
||||||
@@ -440,6 +520,17 @@ class DroidRelayService {
|
|||||||
|
|
||||||
logger.debug('📊 Droid OpenAI usage:', currentUsageData)
|
logger.debug('📊 Droid OpenAI usage:', currentUsageData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 新 Response API 在 response.usage 中返回统计
|
||||||
|
if (data.response && data.response.usage) {
|
||||||
|
const { usage } = data.response
|
||||||
|
currentUsageData.input_tokens =
|
||||||
|
usage.input_tokens || usage.prompt_tokens || usage.total_tokens || 0
|
||||||
|
currentUsageData.output_tokens = usage.output_tokens || usage.completion_tokens || 0
|
||||||
|
currentUsageData.total_tokens = usage.total_tokens || 0
|
||||||
|
|
||||||
|
logger.debug('📊 Droid OpenAI response usage:', currentUsageData)
|
||||||
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
// 忽略解析错误
|
// 忽略解析错误
|
||||||
}
|
}
|
||||||
@@ -471,7 +562,7 @@ class DroidRelayService {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endpointType === 'openai' || endpointType === 'common') {
|
if (endpointType === 'openai') {
|
||||||
if (lower.includes('data: [done]')) {
|
if (lower.includes('data: [done]')) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -479,6 +570,17 @@ class DroidRelayService {
|
|||||||
if (compact.includes('"finish_reason"')) {
|
if (compact.includes('"finish_reason"')) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lower.includes('event: response.done') || lower.includes('event: response.completed')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
compact.includes('"type":"response.done"') ||
|
||||||
|
compact.includes('"type":"response.completed"')
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -488,23 +590,107 @@ class DroidRelayService {
|
|||||||
* 记录从流中解析的 usage 数据
|
* 记录从流中解析的 usage 数据
|
||||||
*/
|
*/
|
||||||
async _recordUsageFromStreamData(usageData, apiKeyData, account, model) {
|
async _recordUsageFromStreamData(usageData, apiKeyData, account, model) {
|
||||||
const inputTokens = usageData.input_tokens || 0
|
const normalizedUsage = this._normalizeUsageSnapshot(usageData)
|
||||||
const outputTokens = usageData.output_tokens || 0
|
await this._recordUsage(apiKeyData, account, model, normalizedUsage)
|
||||||
const cacheCreateTokens = usageData.cache_creation_input_tokens || 0
|
return normalizedUsage
|
||||||
const cacheReadTokens = usageData.cache_read_input_tokens || 0
|
}
|
||||||
const totalTokens = inputTokens + outputTokens
|
|
||||||
|
|
||||||
if (totalTokens > 0) {
|
/**
|
||||||
await this._recordUsage(
|
* 标准化 usage 数据,确保字段完整且为数字
|
||||||
apiKeyData,
|
*/
|
||||||
account,
|
_normalizeUsageSnapshot(usageData = {}) {
|
||||||
model,
|
const toNumber = (value) => {
|
||||||
inputTokens,
|
if (value === undefined || value === null || value === '') {
|
||||||
outputTokens,
|
return 0
|
||||||
cacheCreateTokens,
|
}
|
||||||
cacheReadTokens
|
const num = Number(value)
|
||||||
)
|
if (!Number.isFinite(num)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return Math.max(0, num)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inputTokens = toNumber(
|
||||||
|
usageData.input_tokens ??
|
||||||
|
usageData.prompt_tokens ??
|
||||||
|
usageData.inputTokens ??
|
||||||
|
usageData.total_input_tokens
|
||||||
|
)
|
||||||
|
const outputTokens = toNumber(
|
||||||
|
usageData.output_tokens ?? usageData.completion_tokens ?? usageData.outputTokens
|
||||||
|
)
|
||||||
|
const cacheReadTokens = toNumber(
|
||||||
|
usageData.cache_read_input_tokens ??
|
||||||
|
usageData.cacheReadTokens ??
|
||||||
|
usageData.input_tokens_details?.cached_tokens
|
||||||
|
)
|
||||||
|
|
||||||
|
const rawCacheCreateTokens =
|
||||||
|
usageData.cache_creation_input_tokens ??
|
||||||
|
usageData.cacheCreateTokens ??
|
||||||
|
usageData.cache_tokens ??
|
||||||
|
0
|
||||||
|
let cacheCreateTokens = toNumber(rawCacheCreateTokens)
|
||||||
|
|
||||||
|
const ephemeral5m = toNumber(
|
||||||
|
usageData.cache_creation?.ephemeral_5m_input_tokens ?? usageData.ephemeral_5m_input_tokens
|
||||||
|
)
|
||||||
|
const ephemeral1h = toNumber(
|
||||||
|
usageData.cache_creation?.ephemeral_1h_input_tokens ?? usageData.ephemeral_1h_input_tokens
|
||||||
|
)
|
||||||
|
|
||||||
|
if (cacheCreateTokens === 0 && (ephemeral5m > 0 || ephemeral1h > 0)) {
|
||||||
|
cacheCreateTokens = ephemeral5m + ephemeral1h
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = {
|
||||||
|
input_tokens: inputTokens,
|
||||||
|
output_tokens: outputTokens,
|
||||||
|
cache_creation_input_tokens: cacheCreateTokens,
|
||||||
|
cache_read_input_tokens: cacheReadTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ephemeral5m > 0 || ephemeral1h > 0) {
|
||||||
|
normalized.cache_creation = {
|
||||||
|
ephemeral_5m_input_tokens: ephemeral5m,
|
||||||
|
ephemeral_1h_input_tokens: ephemeral1h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算 usage 对象的总 token 数
|
||||||
|
*/
|
||||||
|
_getTotalTokens(usageObject = {}) {
|
||||||
|
const toNumber = (value) => {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const num = Number(value)
|
||||||
|
if (!Number.isFinite(num)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return Math.max(0, num)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
toNumber(usageObject.input_tokens) +
|
||||||
|
toNumber(usageObject.output_tokens) +
|
||||||
|
toNumber(usageObject.cache_creation_input_tokens) +
|
||||||
|
toNumber(usageObject.cache_read_input_tokens)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取账户 ID
|
||||||
|
*/
|
||||||
|
_extractAccountId(account) {
|
||||||
|
if (!account || typeof account !== 'object') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return account.id || account.accountId || account.account_id || null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -534,7 +720,7 @@ class DroidRelayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OpenAI 特定头
|
// OpenAI 特定头
|
||||||
if (endpointType === 'openai' || endpointType === 'common') {
|
if (endpointType === 'openai') {
|
||||||
headers['x-api-provider'] = 'azure_openai'
|
headers['x-api-provider'] = 'azure_openai'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,34 +822,40 @@ class DroidRelayService {
|
|||||||
/**
|
/**
|
||||||
* 处理非流式响应
|
* 处理非流式响应
|
||||||
*/
|
*/
|
||||||
async _handleNonStreamResponse(response, account, apiKeyData, requestBody) {
|
async _handleNonStreamResponse(
|
||||||
|
response,
|
||||||
|
account,
|
||||||
|
apiKeyData,
|
||||||
|
requestBody,
|
||||||
|
clientRequest,
|
||||||
|
endpointType
|
||||||
|
) {
|
||||||
const { data } = response
|
const { data } = response
|
||||||
|
|
||||||
// 从响应中提取 usage 数据
|
// 从响应中提取 usage 数据
|
||||||
const usage = data.usage || {}
|
const usage = data.usage || {}
|
||||||
|
|
||||||
// Anthropic 格式
|
|
||||||
const inputTokens = usage.input_tokens || 0
|
|
||||||
const outputTokens = usage.output_tokens || 0
|
|
||||||
const cacheCreateTokens = usage.cache_creation_input_tokens || 0
|
|
||||||
const cacheReadTokens = usage.cache_read_input_tokens || 0
|
|
||||||
|
|
||||||
const totalTokens = inputTokens + outputTokens
|
|
||||||
const model = requestBody.model || 'unknown'
|
const model = requestBody.model || 'unknown'
|
||||||
|
|
||||||
// 记录使用统计
|
const normalizedUsage = this._normalizeUsageSnapshot(usage)
|
||||||
if (totalTokens > 0) {
|
await this._recordUsage(apiKeyData, account, model, normalizedUsage)
|
||||||
await this._recordUsage(
|
|
||||||
apiKeyData,
|
const totalTokens = this._getTotalTokens(normalizedUsage)
|
||||||
account,
|
|
||||||
model,
|
const usageSummary = {
|
||||||
inputTokens,
|
inputTokens: normalizedUsage.input_tokens || 0,
|
||||||
outputTokens,
|
outputTokens: normalizedUsage.output_tokens || 0,
|
||||||
cacheCreateTokens,
|
cacheCreateTokens: normalizedUsage.cache_creation_input_tokens || 0,
|
||||||
cacheReadTokens
|
cacheReadTokens: normalizedUsage.cache_read_input_tokens || 0
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this._applyRateLimitTracking(
|
||||||
|
clientRequest?.rateLimitInfo,
|
||||||
|
usageSummary,
|
||||||
|
model,
|
||||||
|
endpointType === 'anthropic' ? ' [anthropic]' : ' [openai]'
|
||||||
|
)
|
||||||
|
|
||||||
logger.success(`✅ Droid request completed - Account: ${account.name}, Tokens: ${totalTokens}`)
|
logger.success(`✅ Droid request completed - Account: ${account.name}, Tokens: ${totalTokens}`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -676,51 +868,38 @@ class DroidRelayService {
|
|||||||
/**
|
/**
|
||||||
* 记录使用统计
|
* 记录使用统计
|
||||||
*/
|
*/
|
||||||
async _recordUsage(
|
async _recordUsage(apiKeyData, account, model, usageObject = {}) {
|
||||||
apiKeyData,
|
const totalTokens = this._getTotalTokens(usageObject)
|
||||||
account,
|
|
||||||
model,
|
if (totalTokens <= 0) {
|
||||||
inputTokens,
|
logger.debug('🪙 Droid usage 数据为空,跳过记录')
|
||||||
outputTokens,
|
return
|
||||||
cacheCreateTokens = 0,
|
}
|
||||||
cacheReadTokens = 0
|
|
||||||
) {
|
|
||||||
const totalTokens = inputTokens + outputTokens
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyId = apiKeyData?.id
|
const keyId = apiKeyData?.id
|
||||||
// 记录 API Key 级别的使用统计
|
const accountId = this._extractAccountId(account)
|
||||||
|
|
||||||
if (keyId) {
|
if (keyId) {
|
||||||
await redis.incrementTokenUsage(
|
await apiKeyService.recordUsageWithDetails(keyId, usageObject, model, accountId, 'droid')
|
||||||
keyId,
|
} else if (accountId) {
|
||||||
|
await redis.incrementAccountUsage(
|
||||||
|
accountId,
|
||||||
totalTokens,
|
totalTokens,
|
||||||
inputTokens,
|
usageObject.input_tokens || 0,
|
||||||
outputTokens,
|
usageObject.output_tokens || 0,
|
||||||
cacheCreateTokens,
|
usageObject.cache_creation_input_tokens || 0,
|
||||||
cacheReadTokens,
|
usageObject.cache_read_input_tokens || 0,
|
||||||
model,
|
model,
|
||||||
0, // ephemeral5mTokens
|
false
|
||||||
0, // ephemeral1hTokens
|
|
||||||
false // isLongContextRequest
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
logger.warn('⚠️ Skipping API Key usage recording: missing apiKeyData.id')
|
logger.warn('⚠️ 无法记录 Droid usage:缺少 API Key 和账户标识')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录账户级别的使用统计
|
|
||||||
await redis.incrementAccountUsage(
|
|
||||||
account.id,
|
|
||||||
totalTokens,
|
|
||||||
inputTokens,
|
|
||||||
outputTokens,
|
|
||||||
cacheCreateTokens,
|
|
||||||
cacheReadTokens,
|
|
||||||
model,
|
|
||||||
false // isLongContextRequest
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`📊 Droid usage recorded - Key: ${keyId || 'unknown'}, Account: ${account.id}, Model: ${model}, Input: ${inputTokens}, Output: ${outputTokens}, Total: ${totalTokens}`
|
`📊 Droid usage recorded - Key: ${keyId || 'unknown'}, Account: ${accountId || 'unknown'}, Model: ${model}, Input: ${usageObject.input_tokens || 0}, Output: ${usageObject.output_tokens || 0}, Cache Create: ${usageObject.cache_creation_input_tokens || 0}, Cache Read: ${usageObject.cache_read_input_tokens || 0}, Total: ${totalTokens}`
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to record Droid usage:', error)
|
logger.error('❌ Failed to record Droid usage:', error)
|
||||||
|
|||||||
218
src/services/droidScheduler.js
Normal file
218
src/services/droidScheduler.js
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
const droidAccountService = require('./droidAccountService')
|
||||||
|
const accountGroupService = require('./accountGroupService')
|
||||||
|
const redis = require('../models/redis')
|
||||||
|
const logger = require('../utils/logger')
|
||||||
|
|
||||||
|
class DroidScheduler {
|
||||||
|
constructor() {
|
||||||
|
this.STICKY_PREFIX = 'droid'
|
||||||
|
}
|
||||||
|
|
||||||
|
_normalizeEndpointType(endpointType) {
|
||||||
|
if (!endpointType) {
|
||||||
|
return 'anthropic'
|
||||||
|
}
|
||||||
|
const normalized = String(endpointType).toLowerCase()
|
||||||
|
if (normalized === 'openai' || normalized === 'common') {
|
||||||
|
return 'openai'
|
||||||
|
}
|
||||||
|
return 'anthropic'
|
||||||
|
}
|
||||||
|
|
||||||
|
_isTruthy(value) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.toLowerCase() === 'true'
|
||||||
|
}
|
||||||
|
return Boolean(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
_isAccountActive(account) {
|
||||||
|
if (!account) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const isActive = this._isTruthy(account.isActive)
|
||||||
|
if (!isActive) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = (account.status || 'active').toLowerCase()
|
||||||
|
const unhealthyStatuses = new Set(['error', 'unauthorized', 'blocked'])
|
||||||
|
return !unhealthyStatuses.has(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
_isAccountSchedulable(account) {
|
||||||
|
return this._isTruthy(account?.schedulable ?? true)
|
||||||
|
}
|
||||||
|
|
||||||
|
_matchesEndpoint(account, endpointType) {
|
||||||
|
const normalizedEndpoint = this._normalizeEndpointType(endpointType)
|
||||||
|
const accountEndpoint = this._normalizeEndpointType(account?.endpointType)
|
||||||
|
if (normalizedEndpoint === accountEndpoint) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharedEndpoints = new Set(['anthropic', 'openai'])
|
||||||
|
return sharedEndpoints.has(normalizedEndpoint) && sharedEndpoints.has(accountEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
_sortCandidates(candidates) {
|
||||||
|
return [...candidates].sort((a, b) => {
|
||||||
|
const priorityA = parseInt(a.priority, 10) || 50
|
||||||
|
const priorityB = parseInt(b.priority, 10) || 50
|
||||||
|
|
||||||
|
if (priorityA !== priorityB) {
|
||||||
|
return priorityA - priorityB
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastUsedA = a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0
|
||||||
|
const lastUsedB = b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0
|
||||||
|
|
||||||
|
if (lastUsedA !== lastUsedB) {
|
||||||
|
return lastUsedA - lastUsedB
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdA = a.createdAt ? new Date(a.createdAt).getTime() : 0
|
||||||
|
const createdB = b.createdAt ? new Date(b.createdAt).getTime() : 0
|
||||||
|
return createdA - createdB
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_composeStickySessionKey(endpointType, sessionHash, apiKeyId) {
|
||||||
|
if (!sessionHash) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const normalizedEndpoint = this._normalizeEndpointType(endpointType)
|
||||||
|
const apiKeyPart = apiKeyId || 'default'
|
||||||
|
return `${this.STICKY_PREFIX}:${normalizedEndpoint}:${apiKeyPart}:${sessionHash}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async _loadGroupAccounts(groupId) {
|
||||||
|
const memberIds = await accountGroupService.getGroupMembers(groupId)
|
||||||
|
if (!memberIds || memberIds.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const accounts = await Promise.all(
|
||||||
|
memberIds.map(async (memberId) => {
|
||||||
|
try {
|
||||||
|
return await droidAccountService.getAccount(memberId)
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`⚠️ 获取 Droid 分组成员账号失败: ${memberId}`, error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return accounts.filter(
|
||||||
|
(account) => account && this._isAccountActive(account) && this._isAccountSchedulable(account)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async _ensureLastUsedUpdated(accountId) {
|
||||||
|
try {
|
||||||
|
await droidAccountService.touchLastUsedAt(accountId)
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`⚠️ 更新 Droid 账号最后使用时间失败: ${accountId}`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _cleanupStickyMapping(stickyKey) {
|
||||||
|
if (!stickyKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await redis.deleteSessionAccountMapping(stickyKey)
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`⚠️ 清理 Droid 粘性会话映射失败: ${stickyKey}`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectAccount(apiKeyData, endpointType, sessionHash) {
|
||||||
|
const normalizedEndpoint = this._normalizeEndpointType(endpointType)
|
||||||
|
const stickyKey = this._composeStickySessionKey(normalizedEndpoint, sessionHash, apiKeyData?.id)
|
||||||
|
|
||||||
|
let candidates = []
|
||||||
|
let isDedicatedBinding = false
|
||||||
|
|
||||||
|
if (apiKeyData?.droidAccountId) {
|
||||||
|
const binding = apiKeyData.droidAccountId
|
||||||
|
if (binding.startsWith('group:')) {
|
||||||
|
const groupId = binding.substring('group:'.length)
|
||||||
|
logger.info(
|
||||||
|
`🤖 API Key ${apiKeyData.name || apiKeyData.id} 绑定 Droid 分组 ${groupId},按分组调度`
|
||||||
|
)
|
||||||
|
candidates = await this._loadGroupAccounts(groupId, normalizedEndpoint)
|
||||||
|
} else {
|
||||||
|
const account = await droidAccountService.getAccount(binding)
|
||||||
|
if (account) {
|
||||||
|
candidates = [account]
|
||||||
|
isDedicatedBinding = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!candidates || candidates.length === 0) {
|
||||||
|
candidates = await droidAccountService.getSchedulableAccounts(normalizedEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = candidates.filter(
|
||||||
|
(account) =>
|
||||||
|
account &&
|
||||||
|
this._isAccountActive(account) &&
|
||||||
|
this._isAccountSchedulable(account) &&
|
||||||
|
this._matchesEndpoint(account, normalizedEndpoint)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`No available Droid accounts for endpoint ${normalizedEndpoint}${apiKeyData?.droidAccountId ? ' (respecting binding)' : ''}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stickyKey && !isDedicatedBinding) {
|
||||||
|
const mappedAccountId = await redis.getSessionAccountMapping(stickyKey)
|
||||||
|
if (mappedAccountId) {
|
||||||
|
const mappedAccount = filtered.find((account) => account.id === mappedAccountId)
|
||||||
|
if (mappedAccount) {
|
||||||
|
await redis.extendSessionAccountMappingTTL(stickyKey)
|
||||||
|
logger.info(
|
||||||
|
`🤖 命中 Droid 粘性会话: ${sessionHash} -> ${mappedAccount.name || mappedAccount.id}`
|
||||||
|
)
|
||||||
|
await this._ensureLastUsedUpdated(mappedAccount.id)
|
||||||
|
return mappedAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._cleanupStickyMapping(stickyKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = this._sortCandidates(filtered)
|
||||||
|
const selected = sorted[0]
|
||||||
|
|
||||||
|
if (!selected) {
|
||||||
|
throw new Error(
|
||||||
|
`No schedulable Droid account available after sorting (${normalizedEndpoint})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stickyKey && !isDedicatedBinding) {
|
||||||
|
await redis.setSessionAccountMapping(stickyKey, selected.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._ensureLastUsedUpdated(selected.id)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`🤖 选择 Droid 账号 ${selected.name || selected.id}(endpoint: ${normalizedEndpoint}, priority: ${selected.priority || 50})`
|
||||||
|
)
|
||||||
|
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new DroidScheduler()
|
||||||
@@ -26,6 +26,14 @@ const CLIENT_DEFINITIONS = {
|
|||||||
displayName: 'Codex Command Line Tool',
|
displayName: 'Codex Command Line Tool',
|
||||||
description: 'Cursor/Codex command-line interface',
|
description: 'Cursor/Codex command-line interface',
|
||||||
icon: '🔷'
|
icon: '🔷'
|
||||||
|
},
|
||||||
|
|
||||||
|
DROID_CLI: {
|
||||||
|
id: 'droid_cli',
|
||||||
|
name: 'Droid CLI',
|
||||||
|
displayName: 'Factory Droid CLI',
|
||||||
|
description: 'Factory Droid platform command-line interface',
|
||||||
|
icon: '🤖'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +41,8 @@ const CLIENT_DEFINITIONS = {
|
|||||||
const CLIENT_IDS = {
|
const CLIENT_IDS = {
|
||||||
CLAUDE_CODE: 'claude_code',
|
CLAUDE_CODE: 'claude_code',
|
||||||
GEMINI_CLI: 'gemini_cli',
|
GEMINI_CLI: 'gemini_cli',
|
||||||
CODEX_CLI: 'codex_cli'
|
CODEX_CLI: 'codex_cli',
|
||||||
|
DROID_CLI: 'droid_cli'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取所有客户端定义
|
// 获取所有客户端定义
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const { CLIENT_DEFINITIONS, getAllClientDefinitions } = require('./clientDefinit
|
|||||||
const ClaudeCodeValidator = require('./clients/claudeCodeValidator')
|
const ClaudeCodeValidator = require('./clients/claudeCodeValidator')
|
||||||
const GeminiCliValidator = require('./clients/geminiCliValidator')
|
const GeminiCliValidator = require('./clients/geminiCliValidator')
|
||||||
const CodexCliValidator = require('./clients/codexCliValidator')
|
const CodexCliValidator = require('./clients/codexCliValidator')
|
||||||
|
const DroidCliValidator = require('./clients/droidCliValidator')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 客户端验证器类
|
* 客户端验证器类
|
||||||
@@ -26,6 +27,8 @@ class ClientValidator {
|
|||||||
return GeminiCliValidator
|
return GeminiCliValidator
|
||||||
case 'codex_cli':
|
case 'codex_cli':
|
||||||
return CodexCliValidator
|
return CodexCliValidator
|
||||||
|
case 'droid_cli':
|
||||||
|
return DroidCliValidator
|
||||||
default:
|
default:
|
||||||
logger.warn(`Unknown client ID: ${clientId}`)
|
logger.warn(`Unknown client ID: ${clientId}`)
|
||||||
return null
|
return null
|
||||||
@@ -37,7 +40,7 @@ class ClientValidator {
|
|||||||
* @returns {Array<string>} 客户端ID列表
|
* @returns {Array<string>} 客户端ID列表
|
||||||
*/
|
*/
|
||||||
static getSupportedClients() {
|
static getSupportedClients() {
|
||||||
return ['claude_code', 'gemini_cli', 'codex_cli']
|
return ['claude_code', 'gemini_cli', 'codex_cli', 'droid_cli']
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ class ClaudeCodeValidator {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const systemEntries = Array.isArray(body.system) ? body.system : []
|
const systemEntries = Array.isArray(body.system) ? body.system : null
|
||||||
|
if (!systemEntries) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
for (const entry of systemEntries) {
|
for (const entry of systemEntries) {
|
||||||
const rawText = typeof entry?.text === 'string' ? entry.text : ''
|
const rawText = typeof entry?.text === 'string' ? entry.text : ''
|
||||||
const { bestScore } = bestSimilarityByTemplates(rawText)
|
const { bestScore } = bestSimilarityByTemplates(rawText)
|
||||||
|
|||||||
57
src/validators/clients/droidCliValidator.js
Normal file
57
src/validators/clients/droidCliValidator.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
const logger = require('../../utils/logger')
|
||||||
|
const { CLIENT_DEFINITIONS } = require('../clientDefinitions')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Droid CLI 验证器
|
||||||
|
* 检查请求是否来自 Factory Droid CLI
|
||||||
|
*/
|
||||||
|
class DroidCliValidator {
|
||||||
|
static getId() {
|
||||||
|
return CLIENT_DEFINITIONS.DROID_CLI.id
|
||||||
|
}
|
||||||
|
|
||||||
|
static getName() {
|
||||||
|
return CLIENT_DEFINITIONS.DROID_CLI.name
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDescription() {
|
||||||
|
return CLIENT_DEFINITIONS.DROID_CLI.description
|
||||||
|
}
|
||||||
|
|
||||||
|
static validate(req) {
|
||||||
|
try {
|
||||||
|
const userAgent = req.headers['user-agent'] || ''
|
||||||
|
const factoryClientHeader = (req.headers['x-factory-client'] || '').toString().toLowerCase()
|
||||||
|
|
||||||
|
const uaMatch = /factory-cli\/(\d+\.\d+\.\d+)/i.exec(userAgent)
|
||||||
|
const hasFactoryClientHeader =
|
||||||
|
typeof factoryClientHeader === 'string' &&
|
||||||
|
(factoryClientHeader.includes('droid') || factoryClientHeader.includes('factory-cli'))
|
||||||
|
|
||||||
|
if (!uaMatch && !hasFactoryClientHeader) {
|
||||||
|
logger.debug(`Droid CLI validation failed - UA mismatch: ${userAgent}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 允许,通过基础验证
|
||||||
|
logger.debug(
|
||||||
|
`Droid CLI validation passed (UA: ${userAgent || 'N/A'}, header: ${factoryClientHeader || 'N/A'})`
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in DroidCliValidator:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInfo() {
|
||||||
|
return {
|
||||||
|
id: this.getId(),
|
||||||
|
name: this.getName(),
|
||||||
|
description: this.getDescription(),
|
||||||
|
icon: CLIENT_DEFINITIONS.DROID_CLI.icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DroidCliValidator
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
<!-- 平台分组选择器 -->
|
<!-- 平台分组选择器 -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<!-- 分组选择器 -->
|
<!-- 分组选择器 -->
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||||
<!-- Claude 分组 -->
|
<!-- Claude 分组 -->
|
||||||
<div
|
<div
|
||||||
class="group relative cursor-pointer overflow-hidden rounded-lg border-2 transition-all duration-200"
|
class="group relative cursor-pointer overflow-hidden rounded-lg border-2 transition-all duration-200"
|
||||||
@@ -173,6 +173,37 @@
|
|||||||
<p class="text-xs text-gray-600 dark:text-gray-400">Google AI</p>
|
<p class="text-xs text-gray-600 dark:text-gray-400">Google AI</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Droid 分组 -->
|
||||||
|
<div
|
||||||
|
class="group relative cursor-pointer overflow-hidden rounded-lg border-2 transition-all duration-200"
|
||||||
|
:class="[
|
||||||
|
platformGroup === 'droid'
|
||||||
|
? 'border-rose-500 bg-gradient-to-br from-rose-50 to-orange-50 shadow-md dark:from-rose-900/20 dark:to-orange-900/20'
|
||||||
|
: 'border-gray-200 bg-white hover:border-rose-300 hover:shadow dark:border-gray-700 dark:bg-gray-800 dark:hover:border-rose-600'
|
||||||
|
]"
|
||||||
|
@click="selectPlatformGroup('droid')"
|
||||||
|
>
|
||||||
|
<div class="p-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="flex h-8 w-8 items-center justify-center rounded-md bg-gradient-to-br from-rose-500 to-orange-500"
|
||||||
|
>
|
||||||
|
<i class="fas fa-robot text-sm text-white"></i>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="platformGroup === 'droid'"
|
||||||
|
class="flex h-5 w-5 items-center justify-center rounded-full bg-rose-500"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check text-xs text-white"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h4 class="mt-2 text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
Droid
|
||||||
|
</h4>
|
||||||
|
<p class="text-xs text-gray-600 dark:text-gray-400">Claude Droid</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 子平台选择器 -->
|
<!-- 子平台选择器 -->
|
||||||
@@ -447,6 +478,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Droid 子选项 -->
|
||||||
|
<template v-if="platformGroup === 'droid'">
|
||||||
|
<label
|
||||||
|
class="group relative flex cursor-pointer items-center rounded-md border p-2 transition-all"
|
||||||
|
:class="[
|
||||||
|
form.platform === 'droid'
|
||||||
|
? 'border-rose-500 bg-rose-50 dark:border-rose-400 dark:bg-rose-900/30'
|
||||||
|
: 'border-gray-300 bg-white hover:border-rose-400 hover:bg-rose-50/50 dark:border-gray-600 dark:bg-gray-700 dark:hover:border-rose-500 dark:hover:bg-rose-900/20'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<input v-model="form.platform" class="sr-only" type="radio" value="droid" />
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<i class="fas fa-robot text-sm text-rose-600 dark:text-rose-400"></i>
|
||||||
|
<div>
|
||||||
|
<span class="block text-xs font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>Droid 专属</span
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">官方</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="form.platform === 'droid'"
|
||||||
|
class="absolute right-1 top-1 flex h-4 w-4 items-center justify-center rounded-full bg-rose-500"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check text-xs text-white"></i>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2992,6 +3052,8 @@ const selectPlatformGroup = (group) => {
|
|||||||
form.value.platform = 'openai'
|
form.value.platform = 'openai'
|
||||||
} else if (group === 'gemini') {
|
} else if (group === 'gemini') {
|
||||||
form.value.platform = 'gemini'
|
form.value.platform = 'gemini'
|
||||||
|
} else if (group === 'droid') {
|
||||||
|
form.value.platform = 'droid'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,10 @@
|
|||||||
<input v-model="createForm.platform" class="mr-2" type="radio" value="openai" />
|
<input v-model="createForm.platform" class="mr-2" type="radio" value="openai" />
|
||||||
<span class="text-sm text-gray-700">OpenAI</span>
|
<span class="text-sm text-gray-700">OpenAI</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="flex cursor-pointer items-center">
|
||||||
|
<input v-model="createForm.platform" class="mr-2" type="radio" value="droid" />
|
||||||
|
<span class="text-sm text-gray-700">Droid</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -120,7 +124,9 @@
|
|||||||
? 'bg-purple-100 text-purple-700'
|
? 'bg-purple-100 text-purple-700'
|
||||||
: group.platform === 'gemini'
|
: group.platform === 'gemini'
|
||||||
? 'bg-blue-100 text-blue-700'
|
? 'bg-blue-100 text-blue-700'
|
||||||
: 'bg-gray-100 text-gray-700'
|
: group.platform === 'openai'
|
||||||
|
? 'bg-gray-100 text-gray-700'
|
||||||
|
: 'bg-cyan-100 text-cyan-700'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
@@ -128,7 +134,9 @@
|
|||||||
? 'Claude'
|
? 'Claude'
|
||||||
: group.platform === 'gemini'
|
: group.platform === 'gemini'
|
||||||
? 'Gemini'
|
? 'Gemini'
|
||||||
: 'OpenAI'
|
: group.platform === 'openai'
|
||||||
|
? 'OpenAI'
|
||||||
|
: 'Droid'
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -311,6 +311,10 @@
|
|||||||
<input v-model="form.permissions" class="mr-2" type="radio" value="openai" />
|
<input v-model="form.permissions" class="mr-2" type="radio" value="openai" />
|
||||||
<span class="text-sm text-gray-700">仅 OpenAI</span>
|
<span class="text-sm text-gray-700">仅 OpenAI</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="flex cursor-pointer items-center">
|
||||||
|
<input v-model="form.permissions" class="mr-2" type="radio" value="droid" />
|
||||||
|
<span class="text-sm text-gray-700">仅 Droid</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -345,7 +349,7 @@
|
|||||||
<select
|
<select
|
||||||
v-model="form.claudeAccountId"
|
v-model="form.claudeAccountId"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||||
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
|
:disabled="form.permissions && !['all', 'claude'].includes(form.permissions)"
|
||||||
>
|
>
|
||||||
<option value="">不修改</option>
|
<option value="">不修改</option>
|
||||||
<option value="SHARED_POOL">使用共享账号池</option>
|
<option value="SHARED_POOL">使用共享账号池</option>
|
||||||
@@ -380,7 +384,7 @@
|
|||||||
<select
|
<select
|
||||||
v-model="form.geminiAccountId"
|
v-model="form.geminiAccountId"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||||
:disabled="form.permissions === 'claude' || form.permissions === 'openai'"
|
:disabled="form.permissions && !['all', 'gemini'].includes(form.permissions)"
|
||||||
>
|
>
|
||||||
<option value="">不修改</option>
|
<option value="">不修改</option>
|
||||||
<option value="SHARED_POOL">使用共享账号池</option>
|
<option value="SHARED_POOL">使用共享账号池</option>
|
||||||
@@ -411,7 +415,7 @@
|
|||||||
<select
|
<select
|
||||||
v-model="form.openaiAccountId"
|
v-model="form.openaiAccountId"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||||
:disabled="form.permissions === 'claude' || form.permissions === 'gemini'"
|
:disabled="form.permissions && !['all', 'openai'].includes(form.permissions)"
|
||||||
>
|
>
|
||||||
<option value="">不修改</option>
|
<option value="">不修改</option>
|
||||||
<option value="SHARED_POOL">使用共享账号池</option>
|
<option value="SHARED_POOL">使用共享账号池</option>
|
||||||
@@ -442,7 +446,7 @@
|
|||||||
<select
|
<select
|
||||||
v-model="form.bedrockAccountId"
|
v-model="form.bedrockAccountId"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||||
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
|
:disabled="form.permissions && !['all', 'openai'].includes(form.permissions)"
|
||||||
>
|
>
|
||||||
<option value="">不修改</option>
|
<option value="">不修改</option>
|
||||||
<option value="SHARED_POOL">使用共享账号池</option>
|
<option value="SHARED_POOL">使用共享账号池</option>
|
||||||
@@ -457,6 +461,37 @@
|
|||||||
</optgroup>
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||||
|
>Droid 专属账号</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
v-model="form.droidAccountId"
|
||||||
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||||
|
:disabled="form.permissions && !['all', 'droid'].includes(form.permissions)"
|
||||||
|
>
|
||||||
|
<option value="">不修改</option>
|
||||||
|
<option value="SHARED_POOL">使用共享账号池</option>
|
||||||
|
<optgroup v-if="localAccounts.droidGroups.length > 0" label="账号分组">
|
||||||
|
<option
|
||||||
|
v-for="group in localAccounts.droidGroups"
|
||||||
|
:key="group.id"
|
||||||
|
:value="`group:${group.id}`"
|
||||||
|
>
|
||||||
|
分组 - {{ group.name }}
|
||||||
|
</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup v-if="localAccounts.droid.length > 0" label="专属账号">
|
||||||
|
<option
|
||||||
|
v-for="account in localAccounts.droid"
|
||||||
|
:key="account.id"
|
||||||
|
:value="account.id"
|
||||||
|
>
|
||||||
|
{{ account.name }}
|
||||||
|
</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -497,7 +532,17 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
accounts: {
|
accounts: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({ claude: [], gemini: [], openai: [], bedrock: [] })
|
default: () => ({
|
||||||
|
claude: [],
|
||||||
|
gemini: [],
|
||||||
|
openai: [],
|
||||||
|
bedrock: [],
|
||||||
|
droid: [],
|
||||||
|
claudeGroups: [],
|
||||||
|
geminiGroups: [],
|
||||||
|
openaiGroups: [],
|
||||||
|
droidGroups: []
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -511,9 +556,11 @@ const localAccounts = ref({
|
|||||||
gemini: [],
|
gemini: [],
|
||||||
openai: [],
|
openai: [],
|
||||||
bedrock: [],
|
bedrock: [],
|
||||||
|
droid: [],
|
||||||
claudeGroups: [],
|
claudeGroups: [],
|
||||||
geminiGroups: [],
|
geminiGroups: [],
|
||||||
openaiGroups: []
|
openaiGroups: [],
|
||||||
|
droidGroups: []
|
||||||
})
|
})
|
||||||
|
|
||||||
// 标签相关
|
// 标签相关
|
||||||
@@ -542,6 +589,7 @@ const form = reactive({
|
|||||||
geminiAccountId: '',
|
geminiAccountId: '',
|
||||||
openaiAccountId: '',
|
openaiAccountId: '',
|
||||||
bedrockAccountId: '',
|
bedrockAccountId: '',
|
||||||
|
droidAccountId: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
isActive: null // null表示不修改
|
isActive: null // null表示不修改
|
||||||
})
|
})
|
||||||
@@ -571,15 +619,23 @@ const removeTag = (index) => {
|
|||||||
const refreshAccounts = async () => {
|
const refreshAccounts = async () => {
|
||||||
accountsLoading.value = true
|
accountsLoading.value = true
|
||||||
try {
|
try {
|
||||||
const [claudeData, claudeConsoleData, geminiData, openaiData, bedrockData, groupsData] =
|
const [
|
||||||
await Promise.all([
|
claudeData,
|
||||||
apiClient.get('/admin/claude-accounts'),
|
claudeConsoleData,
|
||||||
apiClient.get('/admin/claude-console-accounts'),
|
geminiData,
|
||||||
apiClient.get('/admin/gemini-accounts'),
|
openaiData,
|
||||||
apiClient.get('/admin/openai-accounts'),
|
bedrockData,
|
||||||
apiClient.get('/admin/bedrock-accounts'),
|
droidData,
|
||||||
apiClient.get('/admin/account-groups')
|
groupsData
|
||||||
])
|
] = await Promise.all([
|
||||||
|
apiClient.get('/admin/claude-accounts'),
|
||||||
|
apiClient.get('/admin/claude-console-accounts'),
|
||||||
|
apiClient.get('/admin/gemini-accounts'),
|
||||||
|
apiClient.get('/admin/openai-accounts'),
|
||||||
|
apiClient.get('/admin/bedrock-accounts'),
|
||||||
|
apiClient.get('/admin/droid-accounts'),
|
||||||
|
apiClient.get('/admin/account-groups')
|
||||||
|
])
|
||||||
|
|
||||||
// 合并Claude OAuth账户和Claude Console账户
|
// 合并Claude OAuth账户和Claude Console账户
|
||||||
const claudeAccounts = []
|
const claudeAccounts = []
|
||||||
@@ -627,12 +683,21 @@ const refreshAccounts = async () => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (droidData.success) {
|
||||||
|
localAccounts.value.droid = (droidData.data || []).map((account) => ({
|
||||||
|
...account,
|
||||||
|
platform: 'droid',
|
||||||
|
isDedicated: account.accountType === 'dedicated'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
// 处理分组数据
|
// 处理分组数据
|
||||||
if (groupsData.success) {
|
if (groupsData.success) {
|
||||||
const allGroups = groupsData.data || []
|
const allGroups = groupsData.data || []
|
||||||
localAccounts.value.claudeGroups = allGroups.filter((g) => g.platform === 'claude')
|
localAccounts.value.claudeGroups = allGroups.filter((g) => g.platform === 'claude')
|
||||||
localAccounts.value.geminiGroups = allGroups.filter((g) => g.platform === 'gemini')
|
localAccounts.value.geminiGroups = allGroups.filter((g) => g.platform === 'gemini')
|
||||||
localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
|
localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
|
||||||
|
localAccounts.value.droidGroups = allGroups.filter((g) => g.platform === 'droid')
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast('账号列表已刷新', 'success')
|
showToast('账号列表已刷新', 'success')
|
||||||
@@ -720,6 +785,14 @@ const batchUpdateApiKeys = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (form.droidAccountId !== '') {
|
||||||
|
if (form.droidAccountId === 'SHARED_POOL') {
|
||||||
|
updates.droidAccountId = null
|
||||||
|
} else {
|
||||||
|
updates.droidAccountId = form.droidAccountId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 激活状态
|
// 激活状态
|
||||||
if (form.isActive !== null) {
|
if (form.isActive !== null) {
|
||||||
updates.isActive = form.isActive
|
updates.isActive = form.isActive
|
||||||
@@ -774,9 +847,11 @@ onMounted(async () => {
|
|||||||
gemini: props.accounts.gemini || [],
|
gemini: props.accounts.gemini || [],
|
||||||
openai: props.accounts.openai || [],
|
openai: props.accounts.openai || [],
|
||||||
bedrock: props.accounts.bedrock || [],
|
bedrock: props.accounts.bedrock || [],
|
||||||
|
droid: props.accounts.droid || [],
|
||||||
claudeGroups: props.accounts.claudeGroups || [],
|
claudeGroups: props.accounts.claudeGroups || [],
|
||||||
geminiGroups: props.accounts.geminiGroups || [],
|
geminiGroups: props.accounts.geminiGroups || [],
|
||||||
openaiGroups: props.accounts.openaiGroups || []
|
openaiGroups: props.accounts.openaiGroups || [],
|
||||||
|
droidGroups: props.accounts.droidGroups || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -616,6 +616,15 @@
|
|||||||
/>
|
/>
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300">仅 OpenAI</span>
|
<span class="text-sm text-gray-700 dark:text-gray-300">仅 OpenAI</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="flex cursor-pointer items-center">
|
||||||
|
<input
|
||||||
|
v-model="form.permissions"
|
||||||
|
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
type="radio"
|
||||||
|
value="droid"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">仅 Droid</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
控制此 API Key 可以访问哪些服务
|
控制此 API Key 可以访问哪些服务
|
||||||
@@ -653,7 +662,7 @@
|
|||||||
v-model="form.claudeAccountId"
|
v-model="form.claudeAccountId"
|
||||||
:accounts="localAccounts.claude"
|
:accounts="localAccounts.claude"
|
||||||
default-option-text="使用共享账号池"
|
default-option-text="使用共享账号池"
|
||||||
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
|
:disabled="form.permissions !== 'all' && form.permissions !== 'claude'"
|
||||||
:groups="localAccounts.claudeGroups"
|
:groups="localAccounts.claudeGroups"
|
||||||
placeholder="请选择Claude账号"
|
placeholder="请选择Claude账号"
|
||||||
platform="claude"
|
platform="claude"
|
||||||
@@ -667,7 +676,7 @@
|
|||||||
v-model="form.geminiAccountId"
|
v-model="form.geminiAccountId"
|
||||||
:accounts="localAccounts.gemini"
|
:accounts="localAccounts.gemini"
|
||||||
default-option-text="使用共享账号池"
|
default-option-text="使用共享账号池"
|
||||||
:disabled="form.permissions === 'claude' || form.permissions === 'openai'"
|
:disabled="form.permissions !== 'all' && form.permissions !== 'gemini'"
|
||||||
:groups="localAccounts.geminiGroups"
|
:groups="localAccounts.geminiGroups"
|
||||||
placeholder="请选择Gemini账号"
|
placeholder="请选择Gemini账号"
|
||||||
platform="gemini"
|
platform="gemini"
|
||||||
@@ -681,7 +690,7 @@
|
|||||||
v-model="form.openaiAccountId"
|
v-model="form.openaiAccountId"
|
||||||
:accounts="localAccounts.openai"
|
:accounts="localAccounts.openai"
|
||||||
default-option-text="使用共享账号池"
|
default-option-text="使用共享账号池"
|
||||||
:disabled="form.permissions === 'claude' || form.permissions === 'gemini'"
|
:disabled="form.permissions !== 'all' && form.permissions !== 'openai'"
|
||||||
:groups="localAccounts.openaiGroups"
|
:groups="localAccounts.openaiGroups"
|
||||||
placeholder="请选择OpenAI账号"
|
placeholder="请选择OpenAI账号"
|
||||||
platform="openai"
|
platform="openai"
|
||||||
@@ -695,12 +704,26 @@
|
|||||||
v-model="form.bedrockAccountId"
|
v-model="form.bedrockAccountId"
|
||||||
:accounts="localAccounts.bedrock"
|
:accounts="localAccounts.bedrock"
|
||||||
default-option-text="使用共享账号池"
|
default-option-text="使用共享账号池"
|
||||||
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
|
:disabled="form.permissions !== 'all' && form.permissions !== 'openai'"
|
||||||
:groups="[]"
|
:groups="[]"
|
||||||
placeholder="请选择Bedrock账号"
|
placeholder="请选择Bedrock账号"
|
||||||
platform="bedrock"
|
platform="bedrock"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||||
|
>Droid 专属账号</label
|
||||||
|
>
|
||||||
|
<AccountSelector
|
||||||
|
v-model="form.droidAccountId"
|
||||||
|
:accounts="localAccounts.droid"
|
||||||
|
default-option-text="使用共享账号池"
|
||||||
|
:disabled="form.permissions !== 'all' && form.permissions !== 'droid'"
|
||||||
|
:groups="localAccounts.droidGroups"
|
||||||
|
placeholder="请选择Droid账号"
|
||||||
|
platform="droid"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
选择专属账号后,此API Key将只使用该账号,不选择则使用共享账号池
|
选择专属账号后,此API Key将只使用该账号,不选择则使用共享账号池
|
||||||
@@ -875,7 +898,17 @@ import AccountSelector from '@/components/common/AccountSelector.vue'
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
accounts: {
|
accounts: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({ claude: [], gemini: [] })
|
default: () => ({
|
||||||
|
claude: [],
|
||||||
|
gemini: [],
|
||||||
|
openai: [],
|
||||||
|
bedrock: [],
|
||||||
|
droid: [],
|
||||||
|
claudeGroups: [],
|
||||||
|
geminiGroups: [],
|
||||||
|
openaiGroups: [],
|
||||||
|
droidGroups: []
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -889,10 +922,12 @@ const localAccounts = ref({
|
|||||||
claude: [],
|
claude: [],
|
||||||
gemini: [],
|
gemini: [],
|
||||||
openai: [],
|
openai: [],
|
||||||
bedrock: [], // 添加 Bedrock 账号列表
|
bedrock: [],
|
||||||
|
droid: [],
|
||||||
claudeGroups: [],
|
claudeGroups: [],
|
||||||
geminiGroups: [],
|
geminiGroups: [],
|
||||||
openaiGroups: []
|
openaiGroups: [],
|
||||||
|
droidGroups: []
|
||||||
})
|
})
|
||||||
|
|
||||||
// 表单验证状态
|
// 表单验证状态
|
||||||
@@ -935,7 +970,8 @@ const form = reactive({
|
|||||||
claudeAccountId: '',
|
claudeAccountId: '',
|
||||||
geminiAccountId: '',
|
geminiAccountId: '',
|
||||||
openaiAccountId: '',
|
openaiAccountId: '',
|
||||||
bedrockAccountId: '', // 添加 Bedrock 账号ID
|
bedrockAccountId: '',
|
||||||
|
droidAccountId: '',
|
||||||
enableModelRestriction: false,
|
enableModelRestriction: false,
|
||||||
restrictedModels: [],
|
restrictedModels: [],
|
||||||
modelInput: '',
|
modelInput: '',
|
||||||
@@ -973,10 +1009,15 @@ onMounted(async () => {
|
|||||||
claude: props.accounts.claude || [],
|
claude: props.accounts.claude || [],
|
||||||
gemini: props.accounts.gemini || [],
|
gemini: props.accounts.gemini || [],
|
||||||
openai: openaiAccounts,
|
openai: openaiAccounts,
|
||||||
bedrock: props.accounts.bedrock || [], // 添加 Bedrock 账号
|
bedrock: props.accounts.bedrock || [],
|
||||||
|
droid: (props.accounts.droid || []).map((account) => ({
|
||||||
|
...account,
|
||||||
|
platform: 'droid'
|
||||||
|
})),
|
||||||
claudeGroups: props.accounts.claudeGroups || [],
|
claudeGroups: props.accounts.claudeGroups || [],
|
||||||
geminiGroups: props.accounts.geminiGroups || [],
|
geminiGroups: props.accounts.geminiGroups || [],
|
||||||
openaiGroups: props.accounts.openaiGroups || []
|
openaiGroups: props.accounts.openaiGroups || [],
|
||||||
|
droidGroups: props.accounts.droidGroups || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -995,6 +1036,7 @@ const refreshAccounts = async () => {
|
|||||||
openaiData,
|
openaiData,
|
||||||
openaiResponsesData,
|
openaiResponsesData,
|
||||||
bedrockData,
|
bedrockData,
|
||||||
|
droidData,
|
||||||
groupsData
|
groupsData
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
apiClient.get('/admin/claude-accounts'),
|
apiClient.get('/admin/claude-accounts'),
|
||||||
@@ -1002,7 +1044,8 @@ const refreshAccounts = async () => {
|
|||||||
apiClient.get('/admin/gemini-accounts'),
|
apiClient.get('/admin/gemini-accounts'),
|
||||||
apiClient.get('/admin/openai-accounts'),
|
apiClient.get('/admin/openai-accounts'),
|
||||||
apiClient.get('/admin/openai-responses-accounts'), // 获取 OpenAI-Responses 账号
|
apiClient.get('/admin/openai-responses-accounts'), // 获取 OpenAI-Responses 账号
|
||||||
apiClient.get('/admin/bedrock-accounts'), // 添加 Bedrock 账号获取
|
apiClient.get('/admin/bedrock-accounts'),
|
||||||
|
apiClient.get('/admin/droid-accounts'),
|
||||||
apiClient.get('/admin/account-groups')
|
apiClient.get('/admin/account-groups')
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -1070,12 +1113,21 @@ const refreshAccounts = async () => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (droidData.success) {
|
||||||
|
localAccounts.value.droid = (droidData.data || []).map((account) => ({
|
||||||
|
...account,
|
||||||
|
platform: 'droid',
|
||||||
|
isDedicated: account.accountType === 'dedicated'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
// 处理分组数据
|
// 处理分组数据
|
||||||
if (groupsData.success) {
|
if (groupsData.success) {
|
||||||
const allGroups = groupsData.data || []
|
const allGroups = groupsData.data || []
|
||||||
localAccounts.value.claudeGroups = allGroups.filter((g) => g.platform === 'claude')
|
localAccounts.value.claudeGroups = allGroups.filter((g) => g.platform === 'claude')
|
||||||
localAccounts.value.geminiGroups = allGroups.filter((g) => g.platform === 'gemini')
|
localAccounts.value.geminiGroups = allGroups.filter((g) => g.platform === 'gemini')
|
||||||
localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
|
localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
|
||||||
|
localAccounts.value.droidGroups = allGroups.filter((g) => g.platform === 'droid')
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast('账号列表已刷新', 'success')
|
showToast('账号列表已刷新', 'success')
|
||||||
@@ -1346,6 +1398,9 @@ const createApiKey = async () => {
|
|||||||
if (form.bedrockAccountId) {
|
if (form.bedrockAccountId) {
|
||||||
baseData.bedrockAccountId = form.bedrockAccountId
|
baseData.bedrockAccountId = form.bedrockAccountId
|
||||||
}
|
}
|
||||||
|
if (form.droidAccountId) {
|
||||||
|
baseData.droidAccountId = form.droidAccountId
|
||||||
|
}
|
||||||
|
|
||||||
if (form.createType === 'single') {
|
if (form.createType === 'single') {
|
||||||
// 单个创建
|
// 单个创建
|
||||||
|
|||||||
@@ -449,6 +449,15 @@
|
|||||||
/>
|
/>
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300">仅 OpenAI</span>
|
<span class="text-sm text-gray-700 dark:text-gray-300">仅 OpenAI</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="flex cursor-pointer items-center">
|
||||||
|
<input
|
||||||
|
v-model="form.permissions"
|
||||||
|
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
type="radio"
|
||||||
|
value="droid"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">仅 Droid</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
控制此 API Key 可以访问哪些服务
|
控制此 API Key 可以访问哪些服务
|
||||||
@@ -486,7 +495,7 @@
|
|||||||
v-model="form.claudeAccountId"
|
v-model="form.claudeAccountId"
|
||||||
:accounts="localAccounts.claude"
|
:accounts="localAccounts.claude"
|
||||||
default-option-text="使用共享账号池"
|
default-option-text="使用共享账号池"
|
||||||
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
|
:disabled="form.permissions !== 'all' && form.permissions !== 'claude'"
|
||||||
:groups="localAccounts.claudeGroups"
|
:groups="localAccounts.claudeGroups"
|
||||||
placeholder="请选择Claude账号"
|
placeholder="请选择Claude账号"
|
||||||
platform="claude"
|
platform="claude"
|
||||||
@@ -500,7 +509,7 @@
|
|||||||
v-model="form.geminiAccountId"
|
v-model="form.geminiAccountId"
|
||||||
:accounts="localAccounts.gemini"
|
:accounts="localAccounts.gemini"
|
||||||
default-option-text="使用共享账号池"
|
default-option-text="使用共享账号池"
|
||||||
:disabled="form.permissions === 'claude' || form.permissions === 'openai'"
|
:disabled="form.permissions !== 'all' && form.permissions !== 'gemini'"
|
||||||
:groups="localAccounts.geminiGroups"
|
:groups="localAccounts.geminiGroups"
|
||||||
placeholder="请选择Gemini账号"
|
placeholder="请选择Gemini账号"
|
||||||
platform="gemini"
|
platform="gemini"
|
||||||
@@ -514,7 +523,7 @@
|
|||||||
v-model="form.openaiAccountId"
|
v-model="form.openaiAccountId"
|
||||||
:accounts="localAccounts.openai"
|
:accounts="localAccounts.openai"
|
||||||
default-option-text="使用共享账号池"
|
default-option-text="使用共享账号池"
|
||||||
:disabled="form.permissions === 'claude' || form.permissions === 'gemini'"
|
:disabled="form.permissions !== 'all' && form.permissions !== 'openai'"
|
||||||
:groups="localAccounts.openaiGroups"
|
:groups="localAccounts.openaiGroups"
|
||||||
placeholder="请选择OpenAI账号"
|
placeholder="请选择OpenAI账号"
|
||||||
platform="openai"
|
platform="openai"
|
||||||
@@ -528,12 +537,26 @@
|
|||||||
v-model="form.bedrockAccountId"
|
v-model="form.bedrockAccountId"
|
||||||
:accounts="localAccounts.bedrock"
|
:accounts="localAccounts.bedrock"
|
||||||
default-option-text="使用共享账号池"
|
default-option-text="使用共享账号池"
|
||||||
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
|
:disabled="form.permissions !== 'all' && form.permissions !== 'openai'"
|
||||||
:groups="[]"
|
:groups="[]"
|
||||||
placeholder="请选择Bedrock账号"
|
placeholder="请选择Bedrock账号"
|
||||||
platform="bedrock"
|
platform="bedrock"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||||
|
>Droid 专属账号</label
|
||||||
|
>
|
||||||
|
<AccountSelector
|
||||||
|
v-model="form.droidAccountId"
|
||||||
|
:accounts="localAccounts.droid"
|
||||||
|
default-option-text="使用共享账号池"
|
||||||
|
:disabled="form.permissions !== 'all' && form.permissions !== 'droid'"
|
||||||
|
:groups="localAccounts.droidGroups"
|
||||||
|
placeholder="请选择Droid账号"
|
||||||
|
platform="droid"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
修改绑定账号将影响此API Key的请求路由
|
修改绑定账号将影响此API Key的请求路由
|
||||||
@@ -717,7 +740,18 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
accounts: {
|
accounts: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({ claude: [], gemini: [] })
|
default: () => ({
|
||||||
|
claude: [],
|
||||||
|
gemini: [],
|
||||||
|
openai: [],
|
||||||
|
bedrock: [],
|
||||||
|
droid: [],
|
||||||
|
claudeGroups: [],
|
||||||
|
geminiGroups: [],
|
||||||
|
openaiGroups: [],
|
||||||
|
droidGroups: [],
|
||||||
|
openaiResponses: []
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -732,10 +766,12 @@ const localAccounts = ref({
|
|||||||
claude: [],
|
claude: [],
|
||||||
gemini: [],
|
gemini: [],
|
||||||
openai: [],
|
openai: [],
|
||||||
bedrock: [], // 添加 Bedrock 账号列表
|
bedrock: [],
|
||||||
|
droid: [],
|
||||||
claudeGroups: [],
|
claudeGroups: [],
|
||||||
geminiGroups: [],
|
geminiGroups: [],
|
||||||
openaiGroups: []
|
openaiGroups: [],
|
||||||
|
droidGroups: []
|
||||||
})
|
})
|
||||||
|
|
||||||
// 支持的客户端列表
|
// 支持的客户端列表
|
||||||
@@ -768,7 +804,8 @@ const form = reactive({
|
|||||||
claudeAccountId: '',
|
claudeAccountId: '',
|
||||||
geminiAccountId: '',
|
geminiAccountId: '',
|
||||||
openaiAccountId: '',
|
openaiAccountId: '',
|
||||||
bedrockAccountId: '', // 添加 Bedrock 账号ID
|
bedrockAccountId: '',
|
||||||
|
droidAccountId: '',
|
||||||
enableModelRestriction: false,
|
enableModelRestriction: false,
|
||||||
restrictedModels: [],
|
restrictedModels: [],
|
||||||
modelInput: '',
|
modelInput: '',
|
||||||
@@ -930,6 +967,12 @@ const updateApiKey = async () => {
|
|||||||
data.bedrockAccountId = null
|
data.bedrockAccountId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (form.droidAccountId) {
|
||||||
|
data.droidAccountId = form.droidAccountId
|
||||||
|
} else {
|
||||||
|
data.droidAccountId = null
|
||||||
|
}
|
||||||
|
|
||||||
// 模型限制 - 始终提交这些字段
|
// 模型限制 - 始终提交这些字段
|
||||||
data.enableModelRestriction = form.enableModelRestriction
|
data.enableModelRestriction = form.enableModelRestriction
|
||||||
data.restrictedModels = form.restrictedModels
|
data.restrictedModels = form.restrictedModels
|
||||||
@@ -972,14 +1015,16 @@ const refreshAccounts = async () => {
|
|||||||
openaiData,
|
openaiData,
|
||||||
openaiResponsesData,
|
openaiResponsesData,
|
||||||
bedrockData,
|
bedrockData,
|
||||||
|
droidData,
|
||||||
groupsData
|
groupsData
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
apiClient.get('/admin/claude-accounts'),
|
apiClient.get('/admin/claude-accounts'),
|
||||||
apiClient.get('/admin/claude-console-accounts'),
|
apiClient.get('/admin/claude-console-accounts'),
|
||||||
apiClient.get('/admin/gemini-accounts'),
|
apiClient.get('/admin/gemini-accounts'),
|
||||||
apiClient.get('/admin/openai-accounts'),
|
apiClient.get('/admin/openai-accounts'),
|
||||||
apiClient.get('/admin/openai-responses-accounts'), // 获取 OpenAI-Responses 账号
|
apiClient.get('/admin/openai-responses-accounts'),
|
||||||
apiClient.get('/admin/bedrock-accounts'), // 添加 Bedrock 账号获取
|
apiClient.get('/admin/bedrock-accounts'),
|
||||||
|
apiClient.get('/admin/droid-accounts'),
|
||||||
apiClient.get('/admin/account-groups')
|
apiClient.get('/admin/account-groups')
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -1047,12 +1092,21 @@ const refreshAccounts = async () => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (droidData.success) {
|
||||||
|
localAccounts.value.droid = (droidData.data || []).map((account) => ({
|
||||||
|
...account,
|
||||||
|
platform: 'droid',
|
||||||
|
isDedicated: account.accountType === 'dedicated'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
// 处理分组数据
|
// 处理分组数据
|
||||||
if (groupsData.success) {
|
if (groupsData.success) {
|
||||||
const allGroups = groupsData.data || []
|
const allGroups = groupsData.data || []
|
||||||
localAccounts.value.claudeGroups = allGroups.filter((g) => g.platform === 'claude')
|
localAccounts.value.claudeGroups = allGroups.filter((g) => g.platform === 'claude')
|
||||||
localAccounts.value.geminiGroups = allGroups.filter((g) => g.platform === 'gemini')
|
localAccounts.value.geminiGroups = allGroups.filter((g) => g.platform === 'gemini')
|
||||||
localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
|
localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
|
||||||
|
localAccounts.value.droidGroups = allGroups.filter((g) => g.platform === 'droid')
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast('账号列表已刷新', 'success')
|
showToast('账号列表已刷新', 'success')
|
||||||
@@ -1128,10 +1182,15 @@ onMounted(async () => {
|
|||||||
claude: props.accounts.claude || [],
|
claude: props.accounts.claude || [],
|
||||||
gemini: props.accounts.gemini || [],
|
gemini: props.accounts.gemini || [],
|
||||||
openai: openaiAccounts,
|
openai: openaiAccounts,
|
||||||
bedrock: props.accounts.bedrock || [], // 添加 Bedrock 账号
|
bedrock: props.accounts.bedrock || [],
|
||||||
|
droid: (props.accounts.droid || []).map((account) => ({
|
||||||
|
...account,
|
||||||
|
platform: 'droid'
|
||||||
|
})),
|
||||||
claudeGroups: props.accounts.claudeGroups || [],
|
claudeGroups: props.accounts.claudeGroups || [],
|
||||||
geminiGroups: props.accounts.geminiGroups || [],
|
geminiGroups: props.accounts.geminiGroups || [],
|
||||||
openaiGroups: props.accounts.openaiGroups || []
|
openaiGroups: props.accounts.openaiGroups || [],
|
||||||
|
droidGroups: props.accounts.droidGroups || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1168,7 +1227,8 @@ onMounted(async () => {
|
|||||||
// 处理 OpenAI 账号 - 直接使用后端传来的值(已包含 responses: 前缀)
|
// 处理 OpenAI 账号 - 直接使用后端传来的值(已包含 responses: 前缀)
|
||||||
form.openaiAccountId = props.apiKey.openaiAccountId || ''
|
form.openaiAccountId = props.apiKey.openaiAccountId || ''
|
||||||
|
|
||||||
form.bedrockAccountId = props.apiKey.bedrockAccountId || '' // 添加 Bedrock 账号ID初始化
|
form.bedrockAccountId = props.apiKey.bedrockAccountId || ''
|
||||||
|
form.droidAccountId = props.apiKey.droidAccountId || ''
|
||||||
form.restrictedModels = props.apiKey.restrictedModels || []
|
form.restrictedModels = props.apiKey.restrictedModels || []
|
||||||
form.allowedClients = props.apiKey.allowedClients || []
|
form.allowedClients = props.apiKey.allowedClients || []
|
||||||
form.tags = props.apiKey.tags || []
|
form.tags = props.apiKey.tags || []
|
||||||
|
|||||||
@@ -104,7 +104,9 @@
|
|||||||
? 'Claude OAuth 专属账号'
|
? 'Claude OAuth 专属账号'
|
||||||
: platform === 'openai'
|
: platform === 'openai'
|
||||||
? 'OpenAI 专属账号'
|
? 'OpenAI 专属账号'
|
||||||
: 'OAuth 专属账号'
|
: platform === 'droid'
|
||||||
|
? 'Droid 专属账号'
|
||||||
|
: 'OAuth 专属账号'
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -241,7 +243,7 @@ const props = defineProps({
|
|||||||
platform: {
|
platform: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
validator: (value) => ['claude', 'gemini', 'openai', 'bedrock'].includes(value)
|
validator: (value) => ['claude', 'gemini', 'openai', 'bedrock', 'droid'].includes(value)
|
||||||
},
|
},
|
||||||
accounts: {
|
accounts: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -383,6 +385,8 @@ const filteredOAuthAccounts = computed(() => {
|
|||||||
} else if (props.platform === 'openai') {
|
} else if (props.platform === 'openai') {
|
||||||
// 对于 OpenAI,只显示 openai 类型的账号
|
// 对于 OpenAI,只显示 openai 类型的账号
|
||||||
accounts = sortedAccounts.value.filter((a) => a.platform === 'openai')
|
accounts = sortedAccounts.value.filter((a) => a.platform === 'openai')
|
||||||
|
} else if (props.platform === 'droid') {
|
||||||
|
accounts = sortedAccounts.value.filter((a) => a.platform === 'droid')
|
||||||
} else {
|
} else {
|
||||||
// 其他平台显示所有非特殊类型的账号
|
// 其他平台显示所有非特殊类型的账号
|
||||||
accounts = sortedAccounts.value.filter(
|
accounts = sortedAccounts.value.filter(
|
||||||
|
|||||||
@@ -1740,13 +1740,15 @@ const groupOptions = computed(() => {
|
|||||||
accountGroups.value.forEach((group) => {
|
accountGroups.value.forEach((group) => {
|
||||||
options.push({
|
options.push({
|
||||||
value: group.id,
|
value: group.id,
|
||||||
label: `${group.name} (${group.platform === 'claude' ? 'Claude' : group.platform === 'gemini' ? 'Gemini' : 'OpenAI'})`,
|
label: `${group.name} (${group.platform === 'claude' ? 'Claude' : group.platform === 'gemini' ? 'Gemini' : group.platform === 'openai' ? 'OpenAI' : 'Droid'})`,
|
||||||
icon:
|
icon:
|
||||||
group.platform === 'claude'
|
group.platform === 'claude'
|
||||||
? 'fa-brain'
|
? 'fa-brain'
|
||||||
: group.platform === 'gemini'
|
: group.platform === 'gemini'
|
||||||
? 'fa-robot'
|
? 'fa-robot'
|
||||||
: 'fa-openai'
|
: group.platform === 'openai'
|
||||||
|
? 'fa-openai'
|
||||||
|
: 'fa-robot'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return options
|
return options
|
||||||
@@ -2303,8 +2305,11 @@ const loadAccounts = async (forceReload = false) => {
|
|||||||
// Droid 账户
|
// Droid 账户
|
||||||
if (droidData && droidData.success) {
|
if (droidData && droidData.success) {
|
||||||
const droidAccounts = (droidData.data || []).map((acc) => {
|
const droidAccounts = (droidData.data || []).map((acc) => {
|
||||||
// Droid 不支持 API Key 绑定,固定为 0
|
return {
|
||||||
return { ...acc, platform: 'droid', boundApiKeysCount: 0 }
|
...acc,
|
||||||
|
platform: 'droid',
|
||||||
|
boundApiKeysCount: acc.boundApiKeysCount ?? 0
|
||||||
|
}
|
||||||
})
|
})
|
||||||
allAccounts.push(...droidAccounts)
|
allAccounts.push(...droidAccounts)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -511,6 +511,18 @@
|
|||||||
{{ getBedrockBindingInfo(key) }}
|
{{ getBedrockBindingInfo(key) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Droid 绑定 -->
|
||||||
|
<div v-if="key.droidAccountId" class="flex items-center gap-1 text-xs">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center rounded bg-cyan-100 px-1.5 py-0.5 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300"
|
||||||
|
>
|
||||||
|
<i class="fas fa-robot mr-1 text-[10px]" />
|
||||||
|
Droid
|
||||||
|
</span>
|
||||||
|
<span class="truncate text-gray-600 dark:text-gray-400">
|
||||||
|
{{ getDroidBindingInfo(key) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<!-- 共享池 -->
|
<!-- 共享池 -->
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
@@ -518,7 +530,8 @@
|
|||||||
!key.claudeConsoleAccountId &&
|
!key.claudeConsoleAccountId &&
|
||||||
!key.geminiAccountId &&
|
!key.geminiAccountId &&
|
||||||
!key.openaiAccountId &&
|
!key.openaiAccountId &&
|
||||||
!key.bedrockAccountId
|
!key.bedrockAccountId &&
|
||||||
|
!key.droidAccountId
|
||||||
"
|
"
|
||||||
class="text-xs text-gray-500 dark:text-gray-400"
|
class="text-xs text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
@@ -1182,6 +1195,18 @@
|
|||||||
{{ getBedrockBindingInfo(key) }}
|
{{ getBedrockBindingInfo(key) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Droid 绑定 -->
|
||||||
|
<div v-if="key.droidAccountId" class="flex flex-wrap items-center gap-1 text-xs">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center rounded bg-cyan-100 px-2 py-0.5 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300"
|
||||||
|
>
|
||||||
|
<i class="fas fa-robot mr-1" />
|
||||||
|
Droid
|
||||||
|
</span>
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">
|
||||||
|
{{ getDroidBindingInfo(key) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<!-- 无绑定时显示共享池 -->
|
<!-- 无绑定时显示共享池 -->
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
@@ -1189,7 +1214,8 @@
|
|||||||
!key.claudeConsoleAccountId &&
|
!key.claudeConsoleAccountId &&
|
||||||
!key.geminiAccountId &&
|
!key.geminiAccountId &&
|
||||||
!key.openaiAccountId &&
|
!key.openaiAccountId &&
|
||||||
!key.bedrockAccountId
|
!key.bedrockAccountId &&
|
||||||
|
!key.droidAccountId
|
||||||
"
|
"
|
||||||
class="text-xs text-gray-500 dark:text-gray-400"
|
class="text-xs text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
@@ -1921,9 +1947,11 @@ const accounts = ref({
|
|||||||
openai: [],
|
openai: [],
|
||||||
openaiResponses: [], // 添加 OpenAI-Responses 账号列表
|
openaiResponses: [], // 添加 OpenAI-Responses 账号列表
|
||||||
bedrock: [],
|
bedrock: [],
|
||||||
|
droid: [],
|
||||||
claudeGroups: [],
|
claudeGroups: [],
|
||||||
geminiGroups: [],
|
geminiGroups: [],
|
||||||
openaiGroups: []
|
openaiGroups: [],
|
||||||
|
droidGroups: []
|
||||||
})
|
})
|
||||||
const editingExpiryKey = ref(null)
|
const editingExpiryKey = ref(null)
|
||||||
const expiryEditModalRef = ref(null)
|
const expiryEditModalRef = ref(null)
|
||||||
@@ -2031,12 +2059,17 @@ const getBindingDisplayStrings = (key) => {
|
|||||||
appendBindingRow('Bedrock', getBedrockBindingInfo(key))
|
appendBindingRow('Bedrock', getBedrockBindingInfo(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key.droidAccountId) {
|
||||||
|
appendBindingRow('Droid', getDroidBindingInfo(key))
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!key.claudeAccountId &&
|
!key.claudeAccountId &&
|
||||||
!key.claudeConsoleAccountId &&
|
!key.claudeConsoleAccountId &&
|
||||||
!key.geminiAccountId &&
|
!key.geminiAccountId &&
|
||||||
!key.openaiAccountId &&
|
!key.openaiAccountId &&
|
||||||
!key.bedrockAccountId
|
!key.bedrockAccountId &&
|
||||||
|
!key.droidAccountId
|
||||||
) {
|
) {
|
||||||
collect('共享池')
|
collect('共享池')
|
||||||
}
|
}
|
||||||
@@ -2196,6 +2229,7 @@ const loadAccounts = async () => {
|
|||||||
openaiData,
|
openaiData,
|
||||||
openaiResponsesData,
|
openaiResponsesData,
|
||||||
bedrockData,
|
bedrockData,
|
||||||
|
droidData,
|
||||||
groupsData
|
groupsData
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
apiClient.get('/admin/claude-accounts'),
|
apiClient.get('/admin/claude-accounts'),
|
||||||
@@ -2204,6 +2238,7 @@ const loadAccounts = async () => {
|
|||||||
apiClient.get('/admin/openai-accounts'),
|
apiClient.get('/admin/openai-accounts'),
|
||||||
apiClient.get('/admin/openai-responses-accounts'), // 加载 OpenAI-Responses 账号
|
apiClient.get('/admin/openai-responses-accounts'), // 加载 OpenAI-Responses 账号
|
||||||
apiClient.get('/admin/bedrock-accounts'),
|
apiClient.get('/admin/bedrock-accounts'),
|
||||||
|
apiClient.get('/admin/droid-accounts'),
|
||||||
apiClient.get('/admin/account-groups')
|
apiClient.get('/admin/account-groups')
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -2260,12 +2295,21 @@ const loadAccounts = async () => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (droidData.success) {
|
||||||
|
accounts.value.droid = (droidData.data || []).map((account) => ({
|
||||||
|
...account,
|
||||||
|
platform: 'droid',
|
||||||
|
isDedicated: account.accountType === 'dedicated'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
if (groupsData.success) {
|
if (groupsData.success) {
|
||||||
// 处理分组数据
|
// 处理分组数据
|
||||||
const allGroups = groupsData.data || []
|
const allGroups = groupsData.data || []
|
||||||
accounts.value.claudeGroups = allGroups.filter((g) => g.platform === 'claude')
|
accounts.value.claudeGroups = allGroups.filter((g) => g.platform === 'claude')
|
||||||
accounts.value.geminiGroups = allGroups.filter((g) => g.platform === 'gemini')
|
accounts.value.geminiGroups = allGroups.filter((g) => g.platform === 'gemini')
|
||||||
accounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
|
accounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
|
||||||
|
accounts.value.droidGroups = allGroups.filter((g) => g.platform === 'droid')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error('加载账户列表失败:', error)
|
// console.error('加载账户列表失败:', error)
|
||||||
@@ -2381,6 +2425,11 @@ const getBoundAccountName = (accountId) => {
|
|||||||
return `分组-${openaiGroup.name}`
|
return `分组-${openaiGroup.name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const droidGroup = accounts.value.droidGroups.find((g) => g.id === groupId)
|
||||||
|
if (droidGroup) {
|
||||||
|
return `分组-${droidGroup.name}`
|
||||||
|
}
|
||||||
|
|
||||||
// 如果找不到分组,返回分组ID的前8位
|
// 如果找不到分组,返回分组ID的前8位
|
||||||
return `分组-${groupId.substring(0, 8)}`
|
return `分组-${groupId.substring(0, 8)}`
|
||||||
}
|
}
|
||||||
@@ -2428,6 +2477,11 @@ const getBoundAccountName = (accountId) => {
|
|||||||
return `${bedrockAccount.name}`
|
return `${bedrockAccount.name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const droidAccount = accounts.value.droid.find((acc) => acc.id === accountId)
|
||||||
|
if (droidAccount) {
|
||||||
|
return `${droidAccount.name}`
|
||||||
|
}
|
||||||
|
|
||||||
// 如果找不到,返回账户ID的前8位
|
// 如果找不到,返回账户ID的前8位
|
||||||
return `${accountId.substring(0, 8)}`
|
return `${accountId.substring(0, 8)}`
|
||||||
}
|
}
|
||||||
@@ -2530,6 +2584,24 @@ const getBedrockBindingInfo = (key) => {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDroidBindingInfo = (key) => {
|
||||||
|
if (key.droidAccountId) {
|
||||||
|
const info = getBoundAccountName(key.droidAccountId)
|
||||||
|
if (key.droidAccountId.startsWith('group:')) {
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
const account = accounts.value.droid.find((acc) => acc.id === key.droidAccountId)
|
||||||
|
if (!account) {
|
||||||
|
return `⚠️ ${info} (账户不存在)`
|
||||||
|
}
|
||||||
|
if (account.accountType === 'dedicated') {
|
||||||
|
return `🔒 专属-${info}`
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
// 检查API Key是否过期
|
// 检查API Key是否过期
|
||||||
const isApiKeyExpired = (expiresAt) => {
|
const isApiKeyExpired = (expiresAt) => {
|
||||||
if (!expiresAt) return false
|
if (!expiresAt) return false
|
||||||
@@ -3654,7 +3726,9 @@ const exportToExcel = () => {
|
|||||||
? '仅Gemini'
|
? '仅Gemini'
|
||||||
: key.permissions === 'openai'
|
: key.permissions === 'openai'
|
||||||
? '仅OpenAI'
|
? '仅OpenAI'
|
||||||
: key.permissions || '',
|
: key.permissions === 'droid'
|
||||||
|
? '仅Droid'
|
||||||
|
: key.permissions || '',
|
||||||
|
|
||||||
// 限制配置
|
// 限制配置
|
||||||
令牌限制: key.tokenLimit === '0' || key.tokenLimit === 0 ? '无限制' : key.tokenLimit || '',
|
令牌限制: key.tokenLimit === '0' || key.tokenLimit === 0 ? '无限制' : key.tokenLimit || '',
|
||||||
@@ -3686,6 +3760,7 @@ const exportToExcel = () => {
|
|||||||
OpenAI专属账户: key.openaiAccountId || '',
|
OpenAI专属账户: key.openaiAccountId || '',
|
||||||
'Azure OpenAI专属账户': key.azureOpenaiAccountId || '',
|
'Azure OpenAI专属账户': key.azureOpenaiAccountId || '',
|
||||||
Bedrock专属账户: key.bedrockAccountId || '',
|
Bedrock专属账户: key.bedrockAccountId || '',
|
||||||
|
Droid专属账户: key.droidAccountId || '',
|
||||||
|
|
||||||
// 模型和客户端限制
|
// 模型和客户端限制
|
||||||
启用模型限制: key.enableModelRestriction ? '是' : '否',
|
启用模型限制: key.enableModelRestriction ? '是' : '否',
|
||||||
|
|||||||
Reference in New Issue
Block a user