mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge branch 'Wei-Shaw:main' into main
This commit is contained in:
@@ -65,6 +65,44 @@ const TOKEN_COUNT_PATHS = new Set([
|
||||
'/droid/claude/v1/messages/count_tokens'
|
||||
])
|
||||
|
||||
function extractApiKey(req) {
|
||||
const candidates = [
|
||||
req.headers['x-api-key'],
|
||||
req.headers['x-goog-api-key'],
|
||||
req.headers['authorization'],
|
||||
req.headers['api-key'],
|
||||
req.query?.key
|
||||
]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
let value = candidate
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value = value.find((item) => typeof item === 'string' && item.trim())
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
continue
|
||||
}
|
||||
|
||||
let trimmed = value.trim()
|
||||
if (!trimmed) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (/^Bearer\s+/i.test(trimmed)) {
|
||||
trimmed = trimmed.replace(/^Bearer\s+/i, '').trim()
|
||||
if (!trimmed) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return trimmed
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function normalizeRequestPath(value) {
|
||||
if (!value) {
|
||||
return '/'
|
||||
@@ -95,18 +133,18 @@ const authenticateApiKey = async (req, res, next) => {
|
||||
|
||||
try {
|
||||
// 安全提取API Key,支持多种格式(包括Gemini CLI支持)
|
||||
const apiKey =
|
||||
req.headers['x-api-key'] ||
|
||||
req.headers['x-goog-api-key'] ||
|
||||
req.headers['authorization']?.replace(/^Bearer\s+/i, '') ||
|
||||
req.headers['api-key'] ||
|
||||
req.query.key
|
||||
const apiKey = extractApiKey(req)
|
||||
|
||||
if (apiKey) {
|
||||
req.headers['x-api-key'] = apiKey
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
logger.security(`🔒 Missing API key attempt from ${req.ip || 'unknown'}`)
|
||||
return res.status(401).json({
|
||||
error: 'Missing API key',
|
||||
message: 'Please provide an API key in the x-api-key header or Authorization header'
|
||||
message:
|
||||
'Please provide an API key in the x-api-key, x-goog-api-key, or Authorization header'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -950,6 +988,7 @@ const corsMiddleware = (req, res, next) => {
|
||||
'Accept',
|
||||
'Authorization',
|
||||
'x-api-key',
|
||||
'x-goog-api-key',
|
||||
'api-key',
|
||||
'x-admin-token',
|
||||
'anthropic-version',
|
||||
|
||||
@@ -7,12 +7,38 @@ const logger = require('../utils/logger')
|
||||
const browserFallbackMiddleware = (req, res, next) => {
|
||||
const userAgent = req.headers['user-agent'] || ''
|
||||
const origin = req.headers['origin'] || ''
|
||||
const authHeader = req.headers['authorization'] || req.headers['x-api-key'] || ''
|
||||
|
||||
const extractHeader = (value) => {
|
||||
let candidate = value
|
||||
|
||||
if (Array.isArray(candidate)) {
|
||||
candidate = candidate.find((item) => typeof item === 'string' && item.trim())
|
||||
}
|
||||
|
||||
if (typeof candidate !== 'string') {
|
||||
return ''
|
||||
}
|
||||
|
||||
let trimmed = candidate.trim()
|
||||
if (!trimmed) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (/^Bearer\s+/i.test(trimmed)) {
|
||||
trimmed = trimmed.replace(/^Bearer\s+/i, '').trim()
|
||||
}
|
||||
|
||||
return trimmed
|
||||
}
|
||||
|
||||
const apiKeyHeader =
|
||||
extractHeader(req.headers['x-api-key']) || extractHeader(req.headers['x-goog-api-key'])
|
||||
const normalizedKey = extractHeader(req.headers['authorization']) || apiKeyHeader
|
||||
|
||||
// 检查是否为Chrome插件或浏览器请求
|
||||
const isChromeExtension = origin.startsWith('chrome-extension://')
|
||||
const isBrowserRequest = userAgent.includes('Mozilla/') && userAgent.includes('Chrome/')
|
||||
const hasApiKey = authHeader.startsWith('cr_') // 我们的API Key格式
|
||||
const hasApiKey = normalizedKey.startsWith('cr_') // 我们的API Key格式
|
||||
|
||||
if ((isChromeExtension || isBrowserRequest) && hasApiKey) {
|
||||
// 为Chrome插件请求添加特殊标记
|
||||
@@ -23,8 +49,8 @@ const browserFallbackMiddleware = (req, res, next) => {
|
||||
req.headers['user-agent'] = 'claude-cli/1.0.110 (external, cli, browser-fallback)'
|
||||
|
||||
// 确保设置正确的认证头
|
||||
if (!req.headers['authorization'] && req.headers['x-api-key']) {
|
||||
req.headers['authorization'] = `Bearer ${req.headers['x-api-key']}`
|
||||
if (!req.headers['authorization'] && apiKeyHeader) {
|
||||
req.headers['authorization'] = `Bearer ${apiKeyHeader}`
|
||||
}
|
||||
|
||||
// 添加必要的Anthropic头
|
||||
|
||||
@@ -9058,6 +9058,109 @@ router.put('/droid-accounts/:id/toggle-schedulable', authenticateAdmin, async (r
|
||||
}
|
||||
})
|
||||
|
||||
// 获取单个 Droid 账户详细信息
|
||||
router.get('/droid-accounts/:id', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
|
||||
// 获取账户基本信息
|
||||
const account = await droidAccountService.getAccount(id)
|
||||
if (!account) {
|
||||
return res.status(404).json({
|
||||
error: 'Not Found',
|
||||
message: 'Droid account not found'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取使用统计信息
|
||||
let usageStats
|
||||
try {
|
||||
usageStats = await redis.getAccountUsageStats(account.id, 'droid')
|
||||
} catch (error) {
|
||||
logger.debug(`Failed to get usage stats for Droid account ${account.id}:`, error)
|
||||
usageStats = {
|
||||
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
||||
total: { tokens: 0, requests: 0, allTokens: 0 },
|
||||
averages: { rpm: 0, tpm: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分组信息
|
||||
let groupInfos = []
|
||||
try {
|
||||
groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
} catch (error) {
|
||||
logger.debug(`Failed to get group infos for Droid account ${account.id}:`, error)
|
||||
groupInfos = []
|
||||
}
|
||||
|
||||
// 获取绑定的 API Key 数量
|
||||
const allApiKeys = await redis.getAllApiKeys()
|
||||
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)
|
||||
|
||||
// 获取解密的 API Keys(用于管理界面)
|
||||
let decryptedApiKeys = []
|
||||
try {
|
||||
decryptedApiKeys = await droidAccountService.getDecryptedApiKeyEntries(id)
|
||||
} catch (error) {
|
||||
logger.debug(`Failed to get decrypted API keys for Droid account ${account.id}:`, error)
|
||||
decryptedApiKeys = []
|
||||
}
|
||||
|
||||
// 返回完整的账户信息,包含实际的 API Keys
|
||||
const accountDetails = {
|
||||
...account,
|
||||
// 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt
|
||||
expiresAt: account.subscriptionExpiresAt || null,
|
||||
schedulable: account.schedulable === 'true',
|
||||
boundApiKeysCount,
|
||||
groupInfos,
|
||||
// 包含实际的 API Keys(用于管理界面)
|
||||
apiKeys: decryptedApiKeys.map((entry) => ({
|
||||
key: entry.key,
|
||||
id: entry.id,
|
||||
usageCount: entry.usageCount || 0,
|
||||
lastUsedAt: entry.lastUsedAt || null,
|
||||
status: entry.status || 'active', // 使用实际的状态,默认为 active
|
||||
errorMessage: entry.errorMessage || '', // 包含错误信息
|
||||
createdAt: entry.createdAt || null
|
||||
})),
|
||||
usage: {
|
||||
daily: usageStats.daily,
|
||||
total: usageStats.total,
|
||||
averages: usageStats.averages
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: accountDetails
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get Droid account ${req.params.id}:`, error)
|
||||
return res.status(500).json({
|
||||
error: 'Failed to get Droid account',
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 删除 Droid 账户
|
||||
router.delete('/droid-accounts/:id', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -13,13 +13,10 @@ const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
|
||||
|
||||
// 生成会话哈希
|
||||
function generateSessionHash(req) {
|
||||
const sessionData = [
|
||||
req.headers['user-agent'],
|
||||
req.ip,
|
||||
req.headers['x-api-key']?.substring(0, 10)
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(':')
|
||||
const apiKeyPrefix =
|
||||
req.headers['x-api-key']?.substring(0, 10) || req.headers['x-goog-api-key']?.substring(0, 10)
|
||||
|
||||
const sessionData = [req.headers['user-agent'], req.ip, apiKeyPrefix].filter(Boolean).join(':')
|
||||
|
||||
return crypto.createHash('sha256').update(sessionData).digest('hex')
|
||||
}
|
||||
|
||||
@@ -9,11 +9,10 @@ const crypto = require('crypto')
|
||||
|
||||
// 生成会话哈希
|
||||
function generateSessionHash(req) {
|
||||
const sessionData = [
|
||||
req.headers['user-agent'],
|
||||
req.ip,
|
||||
req.headers['authorization']?.substring(0, 20)
|
||||
]
|
||||
const authSource =
|
||||
req.headers['authorization'] || req.headers['x-api-key'] || req.headers['x-goog-api-key']
|
||||
|
||||
const sessionData = [req.headers['user-agent'], req.ip, authSource?.substring(0, 20)]
|
||||
.filter(Boolean)
|
||||
.join(':')
|
||||
|
||||
|
||||
@@ -183,7 +183,10 @@ class DroidAccountService {
|
||||
? []
|
||||
: normalizedExisting
|
||||
.filter((entry) => entry && entry.id && entry.encryptedKey)
|
||||
.map((entry) => ({ ...entry }))
|
||||
.map((entry) => ({
|
||||
...entry,
|
||||
status: entry.status || 'active' // 确保有默认状态
|
||||
}))
|
||||
|
||||
const hashSet = new Set(entries.map((entry) => entry.hash).filter(Boolean))
|
||||
|
||||
@@ -214,7 +217,9 @@ class DroidAccountService {
|
||||
encryptedKey: this._encryptSensitiveData(trimmed),
|
||||
createdAt: now,
|
||||
lastUsedAt: '',
|
||||
usageCount: '0'
|
||||
usageCount: '0',
|
||||
status: 'active', // 新增状态字段
|
||||
errorMessage: '' // 新增错误信息字段
|
||||
})
|
||||
}
|
||||
|
||||
@@ -230,7 +235,9 @@ class DroidAccountService {
|
||||
id: entry.id,
|
||||
createdAt: entry.createdAt || '',
|
||||
lastUsedAt: entry.lastUsedAt || '',
|
||||
usageCount: entry.usageCount || '0'
|
||||
usageCount: entry.usageCount || '0',
|
||||
status: entry.status || 'active', // 新增状态字段
|
||||
errorMessage: entry.errorMessage || '' // 新增错误信息字段
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -252,7 +259,9 @@ class DroidAccountService {
|
||||
hash: entry.hash || '',
|
||||
createdAt: entry.createdAt || '',
|
||||
lastUsedAt: entry.lastUsedAt || '',
|
||||
usageCount: Number.isFinite(usageCountNumber) && usageCountNumber >= 0 ? usageCountNumber : 0
|
||||
usageCount: Number.isFinite(usageCountNumber) && usageCountNumber >= 0 ? usageCountNumber : 0,
|
||||
status: entry.status || 'active', // 新增状态字段
|
||||
errorMessage: entry.errorMessage || '' // 新增错误信息字段
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,6 +357,56 @@ class DroidAccountService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记指定的 Droid API Key 条目为异常状态
|
||||
*/
|
||||
async markApiKeyAsError(accountId, keyId, errorMessage = '') {
|
||||
if (!accountId || !keyId) {
|
||||
return { marked: false, error: '参数无效' }
|
||||
}
|
||||
|
||||
try {
|
||||
const accountData = await redis.getDroidAccount(accountId)
|
||||
if (!accountData) {
|
||||
return { marked: false, error: '账户不存在' }
|
||||
}
|
||||
|
||||
const entries = this._parseApiKeyEntries(accountData.apiKeys)
|
||||
if (!entries || entries.length === 0) {
|
||||
return { marked: false, error: '无API Key条目' }
|
||||
}
|
||||
|
||||
let marked = false
|
||||
const updatedEntries = entries.map((entry) => {
|
||||
if (entry && entry.id === keyId) {
|
||||
marked = true
|
||||
return {
|
||||
...entry,
|
||||
status: 'error',
|
||||
errorMessage: errorMessage || 'API Key异常'
|
||||
}
|
||||
}
|
||||
return entry
|
||||
})
|
||||
|
||||
if (!marked) {
|
||||
return { marked: false, error: '未找到指定的API Key' }
|
||||
}
|
||||
|
||||
accountData.apiKeys = JSON.stringify(updatedEntries)
|
||||
await redis.setDroidAccount(accountId, accountData)
|
||||
|
||||
logger.warn(
|
||||
`⚠️ 已标记 Droid API Key ${keyId} 为异常状态(Account: ${accountId}):${errorMessage}`
|
||||
)
|
||||
|
||||
return { marked: true }
|
||||
} catch (error) {
|
||||
logger.error(`❌ 标记 Droid API Key 异常状态失败:${keyId}(Account: ${accountId})`, error)
|
||||
return { marked: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 WorkOS Refresh Token 刷新并验证凭证
|
||||
*/
|
||||
@@ -979,7 +1038,7 @@ class DroidAccountService {
|
||||
? updates.apiKeyUpdateMode.trim().toLowerCase()
|
||||
: ''
|
||||
|
||||
let apiKeyUpdateMode = ['append', 'replace', 'delete'].includes(rawApiKeyMode)
|
||||
let apiKeyUpdateMode = ['append', 'replace', 'delete', 'update'].includes(rawApiKeyMode)
|
||||
? rawApiKeyMode
|
||||
: ''
|
||||
|
||||
@@ -1041,6 +1100,60 @@ class DroidAccountService {
|
||||
} else if (removeApiKeysInput.length > 0) {
|
||||
logger.warn(`⚠️ 删除模式未收到有效的 Droid API Key: ${accountId}`)
|
||||
}
|
||||
} else if (apiKeyUpdateMode === 'update') {
|
||||
// 更新模式:根据提供的 key 匹配现有条目并更新状态
|
||||
mergedApiKeys = [...existingApiKeyEntries]
|
||||
const updatedHashes = new Set()
|
||||
|
||||
for (const updateItem of newApiKeysInput) {
|
||||
if (!updateItem || typeof updateItem !== 'object') {
|
||||
continue
|
||||
}
|
||||
|
||||
const key = updateItem.key || updateItem.apiKey || ''
|
||||
if (!key || typeof key !== 'string') {
|
||||
continue
|
||||
}
|
||||
|
||||
const trimmed = key.trim()
|
||||
if (!trimmed) {
|
||||
continue
|
||||
}
|
||||
|
||||
const hash = crypto.createHash('sha256').update(trimmed).digest('hex')
|
||||
updatedHashes.add(hash)
|
||||
|
||||
// 查找现有条目
|
||||
const existingIndex = mergedApiKeys.findIndex((entry) => entry && entry.hash === hash)
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// 更新现有条目的状态信息
|
||||
const existingEntry = mergedApiKeys[existingIndex]
|
||||
mergedApiKeys[existingIndex] = {
|
||||
...existingEntry,
|
||||
status: updateItem.status || existingEntry.status || 'active',
|
||||
errorMessage:
|
||||
updateItem.errorMessage !== undefined
|
||||
? updateItem.errorMessage
|
||||
: existingEntry.errorMessage || '',
|
||||
lastUsedAt:
|
||||
updateItem.lastUsedAt !== undefined
|
||||
? updateItem.lastUsedAt
|
||||
: existingEntry.lastUsedAt || '',
|
||||
usageCount:
|
||||
updateItem.usageCount !== undefined
|
||||
? String(updateItem.usageCount)
|
||||
: existingEntry.usageCount || '0'
|
||||
}
|
||||
apiKeysUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!apiKeysUpdated) {
|
||||
logger.warn(
|
||||
`⚠️ 更新模式未匹配任何 Droid API Key: ${accountId} (提供 ${updatedHashes.size} 个哈希)`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const clearExisting = apiKeyUpdateMode === 'replace' || wantsClearApiKeys
|
||||
const baselineCount = clearExisting ? 0 : existingApiKeyEntries.length
|
||||
@@ -1063,6 +1176,10 @@ class DroidAccountService {
|
||||
logger.info(
|
||||
`🔑 删除模式更新 Droid API keys for ${accountId}: 已移除 ${removedCount} 条,剩余 ${mergedApiKeys.length}`
|
||||
)
|
||||
} else if (apiKeyUpdateMode === 'update') {
|
||||
logger.info(
|
||||
`🔑 更新模式更新 Droid API keys for ${accountId}: 更新了 ${newApiKeysInput.length} 个 API Key 的状态信息`
|
||||
)
|
||||
} else if (apiKeyUpdateMode === 'replace' || wantsClearApiKeys) {
|
||||
logger.info(
|
||||
`🔑 覆盖模式更新 Droid API keys for ${accountId}: 当前总数 ${mergedApiKeys.length},新增 ${addedCount}`
|
||||
|
||||
@@ -121,12 +121,18 @@ class DroidRelayService {
|
||||
throw new Error(`Droid account ${account.id} 未配置任何 API Key`)
|
||||
}
|
||||
|
||||
// 过滤掉异常状态的API Key
|
||||
const activeEntries = entries.filter((entry) => entry.status !== 'error')
|
||||
if (!activeEntries || activeEntries.length === 0) {
|
||||
throw new Error(`Droid account ${account.id} 没有可用的 API Key(所有API Key均已异常)`)
|
||||
}
|
||||
|
||||
const stickyKey = this._composeApiKeyStickyKey(account.id, endpointType, sessionHash)
|
||||
|
||||
if (stickyKey) {
|
||||
const mappedKeyId = await redis.getSessionAccountMapping(stickyKey)
|
||||
if (mappedKeyId) {
|
||||
const mappedEntry = entries.find((entry) => entry.id === mappedKeyId)
|
||||
const mappedEntry = activeEntries.find((entry) => entry.id === mappedKeyId)
|
||||
if (mappedEntry) {
|
||||
await redis.extendSessionAccountMappingTTL(stickyKey)
|
||||
await droidAccountService.touchApiKeyUsage(account.id, mappedEntry.id)
|
||||
@@ -138,7 +144,7 @@ class DroidRelayService {
|
||||
}
|
||||
}
|
||||
|
||||
const selectedEntry = entries[Math.floor(Math.random() * entries.length)]
|
||||
const selectedEntry = activeEntries[Math.floor(Math.random() * activeEntries.length)]
|
||||
if (!selectedEntry) {
|
||||
throw new Error(`Droid account ${account.id} 没有可用的 API Key`)
|
||||
}
|
||||
@@ -150,7 +156,7 @@ class DroidRelayService {
|
||||
await droidAccountService.touchApiKeyUsage(account.id, selectedEntry.id)
|
||||
|
||||
logger.info(
|
||||
`🔐 随机选取 Droid API Key ${selectedEntry.id}(Account: ${account.id}, Keys: ${entries.length})`
|
||||
`🔐 随机选取 Droid API Key ${selectedEntry.id}(Account: ${account.id}, Active Keys: ${activeEntries.length}/${entries.length})`
|
||||
)
|
||||
|
||||
return selectedEntry
|
||||
@@ -1144,39 +1150,50 @@ class DroidRelayService {
|
||||
|
||||
if (authMethod === 'api_key') {
|
||||
if (selectedAccountApiKey?.id) {
|
||||
let removalResult = null
|
||||
let markResult = null
|
||||
const errorMessage = `${statusCode}`
|
||||
|
||||
try {
|
||||
removalResult = await droidAccountService.removeApiKeyEntry(
|
||||
// 标记API Key为异常状态而不是删除
|
||||
markResult = await droidAccountService.markApiKeyAsError(
|
||||
accountId,
|
||||
selectedAccountApiKey.id
|
||||
selectedAccountApiKey.id,
|
||||
errorMessage
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`❌ 移除 Droid API Key ${selectedAccountApiKey.id}(Account: ${accountId})失败:`,
|
||||
`❌ 标记 Droid API Key ${selectedAccountApiKey.id} 异常状态(Account: ${accountId})失败:`,
|
||||
error
|
||||
)
|
||||
}
|
||||
|
||||
await this._clearApiKeyStickyMapping(accountId, normalizedEndpoint, sessionHash)
|
||||
|
||||
if (removalResult?.removed) {
|
||||
if (markResult?.marked) {
|
||||
logger.warn(
|
||||
`🚫 上游返回 ${statusCode},已移除 Droid API Key ${selectedAccountApiKey.id}(Account: ${accountId})`
|
||||
`⚠️ 上游返回 ${statusCode},已标记 Droid API Key ${selectedAccountApiKey.id} 为异常状态(Account: ${accountId})`
|
||||
)
|
||||
} else {
|
||||
logger.warn(
|
||||
`⚠️ 上游返回 ${statusCode},但未能移除 Droid API Key ${selectedAccountApiKey.id}(Account: ${accountId})`
|
||||
`⚠️ 上游返回 ${statusCode},但未能标记 Droid API Key ${selectedAccountApiKey.id} 异常状态(Account: ${accountId}):${markResult?.error || '未知错误'}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!removalResult || removalResult.remainingCount === 0) {
|
||||
await this._stopDroidAccountScheduling(accountId, statusCode, 'API Key 已全部失效')
|
||||
// 检查是否还有可用的API Key
|
||||
try {
|
||||
const availableEntries = await droidAccountService.getDecryptedApiKeyEntries(accountId)
|
||||
const activeEntries = availableEntries.filter((entry) => entry.status !== 'error')
|
||||
|
||||
if (activeEntries.length === 0) {
|
||||
await this._stopDroidAccountScheduling(accountId, statusCode, '所有API Key均已异常')
|
||||
await this._clearAccountStickyMapping(normalizedEndpoint, sessionHash, clientApiKeyId)
|
||||
} else {
|
||||
logger.info(`ℹ️ Droid 账号 ${accountId} 仍有 ${activeEntries.length} 个可用 API Key`)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`❌ 检查可用API Key失败(Account: ${accountId}):`, error)
|
||||
await this._stopDroidAccountScheduling(accountId, statusCode, 'API Key检查失败')
|
||||
await this._clearAccountStickyMapping(normalizedEndpoint, sessionHash, clientApiKeyId)
|
||||
} else {
|
||||
logger.info(
|
||||
`ℹ️ Droid 账号 ${accountId} 仍有 ${removalResult.remainingCount} 个 API Key 可用`
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@@ -28,8 +28,25 @@ class UnifiedClaudeScheduler {
|
||||
return true // 没有指定模型时,默认支持
|
||||
}
|
||||
|
||||
// Claude OAuth 账户的 Opus 模型检查
|
||||
// Claude OAuth 账户的模型检查
|
||||
if (accountType === 'claude-official') {
|
||||
// 1. 首先检查是否为 Claude 官方支持的模型
|
||||
// Claude Official API 只支持 Anthropic 自己的模型,不支持第三方模型(如 deepseek-chat)
|
||||
const isClaudeOfficialModel =
|
||||
requestedModel.startsWith('claude-') ||
|
||||
requestedModel.includes('claude') ||
|
||||
requestedModel.includes('sonnet') ||
|
||||
requestedModel.includes('opus') ||
|
||||
requestedModel.includes('haiku')
|
||||
|
||||
if (!isClaudeOfficialModel) {
|
||||
logger.info(
|
||||
`🚫 Claude official account ${account.name} does not support non-Claude model ${requestedModel}${context ? ` ${context}` : ''}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. Opus 模型的订阅级别检查
|
||||
if (requestedModel.toLowerCase().includes('opus')) {
|
||||
if (account.subscriptionInfo) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user