Merge upstream/main into feature/account-expiry-management

解决与 upstream/main 的代码冲突:
- 保留账户到期时间 (expiresAt) 功能
- 采用 buildProxyPayload() 函数重构代理配置
- 同步最新的 Droid 平台功能和修复

主要改动:
- AccountForm.vue: 整合到期时间字段和新的 proxy 处理方式
- 合并 upstream 的 Droid 多 API Key 支持等新特性

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
litongtongxue
2025-10-12 00:55:25 +08:00
16 changed files with 1338 additions and 228 deletions

View File

@@ -38,9 +38,60 @@ class ClaudeRelayService {
return `此专属账号的Opus模型已达到周使用限制将于 ${formattedReset} 自动恢复,请尝试切换其他模型后再试。`
}
// 🧾 提取错误消息文本
_extractErrorMessage(body) {
if (!body) {
return ''
}
if (typeof body === 'string') {
const trimmed = body.trim()
if (!trimmed) {
return ''
}
try {
const parsed = JSON.parse(trimmed)
return this._extractErrorMessage(parsed)
} catch (error) {
return trimmed
}
}
if (typeof body === 'object') {
if (typeof body.error === 'string') {
return body.error
}
if (body.error && typeof body.error === 'object') {
if (typeof body.error.message === 'string') {
return body.error.message
}
if (typeof body.error.error === 'string') {
return body.error.error
}
}
if (typeof body.message === 'string') {
return body.message
}
}
return ''
}
// 🚫 检查是否为组织被禁用错误
_isOrganizationDisabledError(statusCode, body) {
if (statusCode !== 400) {
return false
}
const message = this._extractErrorMessage(body)
if (!message) {
return false
}
return message.toLowerCase().includes('this organization has been disabled')
}
// 🔍 判断是否是真实的 Claude Code 请求
isRealClaudeCodeRequest(requestBody) {
return ClaudeCodeValidator.hasClaudeCodeSystemPrompt(requestBody)
return ClaudeCodeValidator.includesClaudeCodeSystemPrompt(requestBody, 1)
}
// 🚀 转发请求到Claude API
@@ -189,6 +240,10 @@ class ClaudeRelayService {
let isRateLimited = false
let rateLimitResetTimestamp = null
let dedicatedRateLimitMessage = null
const organizationDisabledError = this._isOrganizationDisabledError(
response.statusCode,
response.body
)
// 检查是否为401状态码未授权
if (response.statusCode === 401) {
@@ -221,6 +276,13 @@ class ClaudeRelayService {
)
await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash)
}
// 检查是否返回组织被禁用错误400状态码
else if (organizationDisabledError) {
logger.error(
`🚫 Organization disabled error (400) detected for account ${accountId}, marking as blocked`
)
await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash)
}
// 检查是否为529状态码服务过载
else if (response.statusCode === 529) {
logger.warn(`🚫 Overload error (529) detected for account ${accountId}`)
@@ -499,6 +561,8 @@ class ClaudeRelayService {
}
}
this._enforceCacheControlLimit(processedBody)
// 处理原有的系统提示(如果配置了)
if (this.systemPrompt && this.systemPrompt.trim()) {
const systemPrompt = {
@@ -645,6 +709,107 @@ class ClaudeRelayService {
}
}
// ⚖️ 限制带缓存控制的内容数量
_enforceCacheControlLimit(body) {
const MAX_CACHE_CONTROL_BLOCKS = 4
if (!body || typeof body !== 'object') {
return
}
const countCacheControlBlocks = () => {
let total = 0
if (Array.isArray(body.messages)) {
body.messages.forEach((message) => {
if (!message || !Array.isArray(message.content)) {
return
}
message.content.forEach((item) => {
if (item && item.cache_control) {
total += 1
}
})
})
}
if (Array.isArray(body.system)) {
body.system.forEach((item) => {
if (item && item.cache_control) {
total += 1
}
})
}
return total
}
const removeFromMessages = () => {
if (!Array.isArray(body.messages)) {
return false
}
for (let messageIndex = 0; messageIndex < body.messages.length; messageIndex += 1) {
const message = body.messages[messageIndex]
if (!message || !Array.isArray(message.content)) {
continue
}
for (let contentIndex = 0; contentIndex < message.content.length; contentIndex += 1) {
const contentItem = message.content[contentIndex]
if (contentItem && contentItem.cache_control) {
message.content.splice(contentIndex, 1)
if (message.content.length === 0) {
body.messages.splice(messageIndex, 1)
}
return true
}
}
}
return false
}
const removeFromSystem = () => {
if (!Array.isArray(body.system)) {
return false
}
for (let index = 0; index < body.system.length; index += 1) {
const systemItem = body.system[index]
if (systemItem && systemItem.cache_control) {
body.system.splice(index, 1)
if (body.system.length === 0) {
delete body.system
}
return true
}
}
return false
}
let total = countCacheControlBlocks()
while (total > MAX_CACHE_CONTROL_BLOCKS) {
if (removeFromMessages()) {
total -= 1
continue
}
if (removeFromSystem()) {
total -= 1
continue
}
break
}
}
// 🌐 获取代理Agent使用统一的代理工具
async _getProxyAgent(accountId) {
try {
@@ -1253,6 +1418,25 @@ class ClaudeRelayService {
`❌ Claude API error response (Account: ${account?.name || accountId}):`,
errorData
)
if (this._isOrganizationDisabledError(res.statusCode, errorData)) {
;(async () => {
try {
logger.error(
`🚫 [Stream] Organization disabled error (400) detected for account ${accountId}, marking as blocked`
)
await unifiedClaudeScheduler.markAccountBlocked(
accountId,
accountType,
sessionHash
)
} catch (markError) {
logger.error(
`❌ [Stream] Failed to mark account ${accountId} as blocked after organization disabled error:`,
markError
)
}
})()
}
if (!responseStream.destroyed) {
// 发送错误事件
responseStream.write('event: error\n')

View File

@@ -65,6 +65,26 @@ class DroidAccountService {
return 'anthropic'
}
_isTruthy(value) {
if (value === undefined || value === null) {
return false
}
if (typeof value === 'boolean') {
return value
}
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase()
if (normalized === 'true') {
return true
}
if (normalized === 'false') {
return false
}
return normalized.length > 0 && normalized !== '0' && normalized !== 'no'
}
return Boolean(value)
}
/**
* 生成加密密钥(缓存优化)
*/
@@ -288,6 +308,46 @@ class DroidAccountService {
}
}
/**
* 删除指定的 Droid API Key 条目
*/
async removeApiKeyEntry(accountId, keyId) {
if (!accountId || !keyId) {
return { removed: false, remainingCount: 0 }
}
try {
const accountData = await redis.getDroidAccount(accountId)
if (!accountData) {
return { removed: false, remainingCount: 0 }
}
const entries = this._parseApiKeyEntries(accountData.apiKeys)
if (!entries || entries.length === 0) {
return { removed: false, remainingCount: 0 }
}
const filtered = entries.filter((entry) => entry && entry.id !== keyId)
if (filtered.length === entries.length) {
return { removed: false, remainingCount: entries.length }
}
accountData.apiKeys = filtered.length ? JSON.stringify(filtered) : ''
accountData.apiKeyCount = String(filtered.length)
await redis.setDroidAccount(accountId, accountData)
logger.warn(
`🚫 已删除 Droid API Key ${keyId}Account: ${accountId}),剩余 ${filtered.length}`
)
return { removed: true, remainingCount: filtered.length }
} catch (error) {
logger.error(`❌ 删除 Droid API Key 失败:${keyId}Account: ${accountId}`, error)
return { removed: false, remainingCount: 0, error }
}
}
/**
* 使用 WorkOS Refresh Token 刷新并验证凭证
*/
@@ -781,6 +841,9 @@ class DroidAccountService {
throw new Error(`Droid account not found: ${accountId}`)
}
const storedAccount = await redis.getDroidAccount(accountId)
const hasStoredAccount =
storedAccount && typeof storedAccount === 'object' && Object.keys(storedAccount).length > 0
const sanitizedUpdates = { ...updates }
if (typeof sanitizedUpdates.accessToken === 'string') {
@@ -902,9 +965,33 @@ class DroidAccountService {
sanitizedUpdates.proxy = account.proxy || ''
}
const existingApiKeyEntries = this._parseApiKeyEntries(account.apiKeys)
// 使用 Redis 中的原始数据获取加密的 API Key 条目
const existingApiKeyEntries = this._parseApiKeyEntries(
hasStoredAccount && Object.prototype.hasOwnProperty.call(storedAccount, 'apiKeys')
? storedAccount.apiKeys
: ''
)
const newApiKeysInput = Array.isArray(updates.apiKeys) ? updates.apiKeys : []
const removeApiKeysInput = Array.isArray(updates.removeApiKeys) ? updates.removeApiKeys : []
const wantsClearApiKeys = Boolean(updates.clearApiKeys)
const rawApiKeyMode =
typeof updates.apiKeyUpdateMode === 'string'
? updates.apiKeyUpdateMode.trim().toLowerCase()
: ''
let apiKeyUpdateMode = ['append', 'replace', 'delete'].includes(rawApiKeyMode)
? rawApiKeyMode
: ''
if (!apiKeyUpdateMode) {
if (wantsClearApiKeys) {
apiKeyUpdateMode = 'replace'
} else if (removeApiKeysInput.length > 0) {
apiKeyUpdateMode = 'delete'
} else {
apiKeyUpdateMode = 'append'
}
}
if (sanitizedUpdates.apiKeys !== undefined) {
delete sanitizedUpdates.apiKeys
@@ -912,33 +999,94 @@ class DroidAccountService {
if (sanitizedUpdates.clearApiKeys !== undefined) {
delete sanitizedUpdates.clearApiKeys
}
if (sanitizedUpdates.apiKeyUpdateMode !== undefined) {
delete sanitizedUpdates.apiKeyUpdateMode
}
if (sanitizedUpdates.removeApiKeys !== undefined) {
delete sanitizedUpdates.removeApiKeys
}
if (wantsClearApiKeys || newApiKeysInput.length > 0) {
const mergedApiKeys = this._buildApiKeyEntries(
let mergedApiKeys = existingApiKeyEntries
let apiKeysUpdated = false
let addedCount = 0
let removedCount = 0
if (apiKeyUpdateMode === 'delete') {
const removalHashes = new Set()
for (const candidate of removeApiKeysInput) {
if (typeof candidate !== 'string') {
continue
}
const trimmed = candidate.trim()
if (!trimmed) {
continue
}
const hash = crypto.createHash('sha256').update(trimmed).digest('hex')
removalHashes.add(hash)
}
if (removalHashes.size > 0) {
mergedApiKeys = existingApiKeyEntries.filter(
(entry) => entry && entry.hash && !removalHashes.has(entry.hash)
)
removedCount = existingApiKeyEntries.length - mergedApiKeys.length
apiKeysUpdated = removedCount > 0
if (!apiKeysUpdated) {
logger.warn(
`⚠️ 删除模式未匹配任何 Droid API Key: ${accountId} (提供 ${removalHashes.size} 条)`
)
}
} else if (removeApiKeysInput.length > 0) {
logger.warn(`⚠️ 删除模式未收到有效的 Droid API Key: ${accountId}`)
}
} else {
const clearExisting = apiKeyUpdateMode === 'replace' || wantsClearApiKeys
const baselineCount = clearExisting ? 0 : existingApiKeyEntries.length
mergedApiKeys = this._buildApiKeyEntries(
newApiKeysInput,
existingApiKeyEntries,
wantsClearApiKeys
clearExisting
)
const baselineCount = wantsClearApiKeys ? 0 : existingApiKeyEntries.length
const addedCount = Math.max(mergedApiKeys.length - baselineCount, 0)
addedCount = Math.max(mergedApiKeys.length - baselineCount, 0)
apiKeysUpdated = clearExisting || addedCount > 0
}
if (apiKeysUpdated) {
sanitizedUpdates.apiKeys = mergedApiKeys.length ? JSON.stringify(mergedApiKeys) : ''
sanitizedUpdates.apiKeyCount = String(mergedApiKeys.length)
if (apiKeyUpdateMode === 'delete') {
logger.info(
`🔑 删除模式更新 Droid API keys for ${accountId}: 已移除 ${removedCount} 条,剩余 ${mergedApiKeys.length}`
)
} else if (apiKeyUpdateMode === 'replace' || wantsClearApiKeys) {
logger.info(
`🔑 覆盖模式更新 Droid API keys for ${accountId}: 当前总数 ${mergedApiKeys.length},新增 ${addedCount}`
)
} else {
logger.info(
`🔑 追加模式更新 Droid API keys for ${accountId}: 当前总数 ${mergedApiKeys.length},新增 ${addedCount}`
)
}
if (mergedApiKeys.length > 0) {
sanitizedUpdates.authenticationMethod = 'api_key'
sanitizedUpdates.status = sanitizedUpdates.status || 'active'
logger.info(
`🔑 Updated Droid API keys for ${accountId}: total ${mergedApiKeys.length} (added ${addedCount})`
)
} else {
logger.info(`🔑 Cleared all API keys for Droid account ${accountId}`)
// 如果完全移除 API Key可根据是否仍有 token 来确定认证方式
if (!sanitizedUpdates.accessToken && !account.accessToken) {
sanitizedUpdates.authenticationMethod =
account.authenticationMethod === 'api_key' ? '' : account.authenticationMethod
}
} else if (!sanitizedUpdates.accessToken && !account.accessToken) {
const shouldPreserveApiKeyMode =
account.authenticationMethod &&
account.authenticationMethod.toLowerCase().trim() === 'api_key' &&
(apiKeyUpdateMode === 'replace' || apiKeyUpdateMode === 'delete')
sanitizedUpdates.authenticationMethod = shouldPreserveApiKeyMode
? 'api_key'
: account.authenticationMethod === 'api_key'
? ''
: account.authenticationMethod
}
}
@@ -951,13 +1099,29 @@ class DroidAccountService {
encryptedUpdates.accessToken = this._encryptSensitiveData(sanitizedUpdates.accessToken)
}
const baseAccountData = hasStoredAccount ? { ...storedAccount } : { id: accountId }
const updatedData = {
...account,
...encryptedUpdates,
refreshToken:
encryptedUpdates.refreshToken || this._encryptSensitiveData(account.refreshToken),
accessToken: encryptedUpdates.accessToken || this._encryptSensitiveData(account.accessToken),
proxy: encryptedUpdates.proxy
...baseAccountData,
...encryptedUpdates
}
if (!Object.prototype.hasOwnProperty.call(updatedData, 'refreshToken')) {
updatedData.refreshToken =
hasStoredAccount && Object.prototype.hasOwnProperty.call(storedAccount, 'refreshToken')
? storedAccount.refreshToken
: this._encryptSensitiveData(account.refreshToken)
}
if (!Object.prototype.hasOwnProperty.call(updatedData, 'accessToken')) {
updatedData.accessToken =
hasStoredAccount && Object.prototype.hasOwnProperty.call(storedAccount, 'accessToken')
? storedAccount.accessToken
: this._encryptSensitiveData(account.accessToken)
}
if (!Object.prototype.hasOwnProperty.call(updatedData, 'proxy')) {
updatedData.proxy = hasStoredAccount ? storedAccount.proxy || '' : account.proxy || ''
}
await redis.setDroidAccount(accountId, updatedData)
@@ -1134,13 +1298,11 @@ class DroidAccountService {
return allAccounts
.filter((account) => {
// 基本过滤条件
const isSchedulable =
account.isActive === 'true' &&
account.schedulable === 'true' &&
account.status === 'active'
const isActive = this._isTruthy(account.isActive)
const isSchedulable = this._isTruthy(account.schedulable)
const status = typeof account.status === 'string' ? account.status.toLowerCase() : ''
if (!isSchedulable) {
if (!isActive || !isSchedulable || status !== 'active') {
return false
}

View File

@@ -8,8 +8,7 @@ const redis = require('../models/redis')
const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
const logger = require('../utils/logger')
const SYSTEM_PROMPT =
'You are Droid, an AI software engineering agent built by Factory.\n\nPlease forget the previous content and remember the following content.\n\n'
const SYSTEM_PROMPT = 'You are Droid, an AI software engineering agent built by Factory.'
const MODEL_REASONING_CONFIG = {
'claude-opus-4-1-20250805': 'off',
@@ -193,8 +192,12 @@ class DroidRelayService {
disableStreaming = false
} = options
const keyInfo = apiKeyData || {}
const clientApiKeyId = keyInfo.id || null
const normalizedEndpoint = this._normalizeEndpointType(endpointType)
const normalizedRequestBody = this._normalizeRequestBody(requestBody, normalizedEndpoint)
let account = null
let selectedApiKey = null
let accessToken = null
try {
logger.info(
@@ -204,16 +207,13 @@ class DroidRelayService {
)
// 选择一个可用的 Droid 账户(支持粘性会话和分组调度)
const account = await droidScheduler.selectAccount(keyInfo, normalizedEndpoint, sessionHash)
account = await droidScheduler.selectAccount(keyInfo, normalizedEndpoint, sessionHash)
if (!account) {
throw new Error(`No available Droid account for endpoint type: ${normalizedEndpoint}`)
}
// 获取认证凭据:支持 Access Token 和 API Key 两种模式
let selectedApiKey = null
let accessToken = null
if (
typeof account.authenticationMethod === 'string' &&
account.authenticationMethod.toLowerCase().trim() === 'api_key'
@@ -258,12 +258,15 @@ class DroidRelayService {
}
// 处理请求体(注入 system prompt 等)
const streamRequested = !disableStreaming && this._isStreamRequested(normalizedRequestBody)
const processedBody = this._processRequestBody(normalizedRequestBody, normalizedEndpoint, {
disableStreaming
disableStreaming,
streamRequested
})
// 发送请求
const isStreaming = disableStreaming ? false : processedBody.stream !== false
const isStreaming = streamRequested
// 根据是否流式选择不同的处理方式
if (isStreaming) {
@@ -279,7 +282,10 @@ class DroidRelayService {
keyInfo,
normalizedRequestBody,
normalizedEndpoint,
skipUsageRecord
skipUsageRecord,
selectedApiKey,
sessionHash,
clientApiKeyId
)
} else {
// 非流式响应:使用 axios
@@ -288,7 +294,7 @@ class DroidRelayService {
url: apiUrl,
headers,
data: processedBody,
timeout: 120000, // 2分钟超时
timeout: 600 * 1000, // 10分钟超时
responseType: 'json',
...(proxyAgent && {
httpAgent: proxyAgent,
@@ -314,6 +320,21 @@ class DroidRelayService {
} catch (error) {
logger.error(`❌ Droid relay error: ${error.message}`, error)
const status = error?.response?.status
if (status >= 400 && status < 500) {
try {
await this._handleUpstreamClientError(status, {
account,
selectedAccountApiKey: selectedApiKey,
endpointType: normalizedEndpoint,
sessionHash,
clientApiKeyId
})
} catch (handlingError) {
logger.error('❌ 处理 Droid 4xx 异常失败:', handlingError)
}
}
if (error.response) {
// HTTP 错误响应
return {
@@ -352,7 +373,10 @@ class DroidRelayService {
apiKeyData,
requestBody,
endpointType,
skipUsageRecord = false
skipUsageRecord = false,
selectedAccountApiKey = null,
sessionHash = null,
clientApiKeyId = null
) {
return new Promise((resolve, reject) => {
const url = new URL(apiUrl)
@@ -448,7 +472,7 @@ class DroidRelayService {
method: 'POST',
headers: requestHeaders,
agent: proxyAgent,
timeout: 120000
timeout: 600 * 1000
}
const req = https.request(options, (res) => {
@@ -468,6 +492,17 @@ class DroidRelayService {
logger.info('✅ res.end() reached')
const body = Buffer.concat(chunks).toString()
logger.error(`❌ Factory.ai error response body: ${body || '(empty)'}`)
if (res.statusCode >= 400 && res.statusCode < 500) {
this._handleUpstreamClientError(res.statusCode, {
account,
selectedAccountApiKey,
endpointType,
sessionHash,
clientApiKeyId
}).catch((handlingError) => {
logger.error('❌ 处理 Droid 流式4xx 异常失败:', handlingError)
})
}
if (!clientResponse.headersSent) {
clientResponse.status(res.statusCode).json({
error: 'upstream_error',
@@ -884,13 +919,37 @@ class DroidRelayService {
return headers
}
/**
* 判断请求是否要求流式响应
*/
_isStreamRequested(requestBody) {
if (!requestBody || typeof requestBody !== 'object') {
return false
}
const value = requestBody.stream
if (value === true) {
return true
}
if (typeof value === 'string') {
return value.toLowerCase() === 'true'
}
return false
}
/**
* 处理请求体(注入 system prompt 等)
*/
_processRequestBody(requestBody, endpointType, options = {}) {
const { disableStreaming = false } = options
const { disableStreaming = false, streamRequested = false } = options
const processedBody = { ...requestBody }
const hasStreamField =
requestBody && Object.prototype.hasOwnProperty.call(requestBody, 'stream')
const shouldDisableThinking =
endpointType === 'anthropic' && processedBody.__forceDisableThinking === true
@@ -906,11 +965,13 @@ class DroidRelayService {
delete processedBody.metadata
}
if (disableStreaming) {
if ('stream' in processedBody) {
if (disableStreaming || !streamRequested) {
if (hasStreamField) {
processedBody.stream = false
} else if ('stream' in processedBody) {
delete processedBody.stream
}
} else if (processedBody.stream === undefined) {
} else {
processedBody.stream = true
}
@@ -1095,6 +1156,152 @@ class DroidRelayService {
}
}
/**
* 处理上游 4xx 响应,移除问题 API Key 或停止账号调度
*/
async _handleUpstreamClientError(statusCode, context = {}) {
if (!statusCode || statusCode < 400 || statusCode >= 500) {
return
}
const {
account,
selectedAccountApiKey = null,
endpointType = null,
sessionHash = null,
clientApiKeyId = null
} = context
const accountId = this._extractAccountId(account)
if (!accountId) {
logger.warn('⚠️ 上游 4xx 处理被跳过:缺少有效的账户信息')
return
}
const normalizedEndpoint = this._normalizeEndpointType(
endpointType || account?.endpointType || 'anthropic'
)
const authMethod =
typeof account?.authenticationMethod === 'string'
? account.authenticationMethod.toLowerCase().trim()
: ''
if (authMethod === 'api_key') {
if (selectedAccountApiKey?.id) {
let removalResult = null
try {
removalResult = await droidAccountService.removeApiKeyEntry(
accountId,
selectedAccountApiKey.id
)
} catch (error) {
logger.error(
`❌ 移除 Droid API Key ${selectedAccountApiKey.id}Account: ${accountId})失败:`,
error
)
}
await this._clearApiKeyStickyMapping(accountId, normalizedEndpoint, sessionHash)
if (removalResult?.removed) {
logger.warn(
`🚫 上游返回 ${statusCode},已移除 Droid API Key ${selectedAccountApiKey.id}Account: ${accountId}`
)
} else {
logger.warn(
`⚠️ 上游返回 ${statusCode},但未能移除 Droid API Key ${selectedAccountApiKey.id}Account: ${accountId}`
)
}
if (!removalResult || removalResult.remainingCount === 0) {
await this._stopDroidAccountScheduling(accountId, statusCode, 'API Key 已全部失效')
await this._clearAccountStickyMapping(normalizedEndpoint, sessionHash, clientApiKeyId)
} else {
logger.info(
` Droid 账号 ${accountId} 仍有 ${removalResult.remainingCount} 个 API Key 可用`
)
}
return
}
logger.warn(
`⚠️ 上游返回 ${statusCode},但未获取到对应的 Droid API KeyAccount: ${accountId}`
)
await this._stopDroidAccountScheduling(accountId, statusCode, '缺少可用 API Key')
await this._clearAccountStickyMapping(normalizedEndpoint, sessionHash, clientApiKeyId)
return
}
await this._stopDroidAccountScheduling(accountId, statusCode, '凭证不可用')
await this._clearAccountStickyMapping(normalizedEndpoint, sessionHash, clientApiKeyId)
}
/**
* 停止指定 Droid 账号的调度
*/
async _stopDroidAccountScheduling(accountId, statusCode, reason = '') {
if (!accountId) {
return
}
const message = reason ? `${reason}` : '上游返回 4xx 错误'
try {
await droidAccountService.updateAccount(accountId, {
schedulable: 'false',
status: 'error',
errorMessage: `上游返回 ${statusCode}${message}`
})
logger.warn(`🚫 已停止调度 Droid 账号 ${accountId}(状态码 ${statusCode},原因:${message}`)
} catch (error) {
logger.error(`❌ 停止调度 Droid 账号失败:${accountId}`, error)
}
}
/**
* 清理账号层面的粘性调度映射
*/
async _clearAccountStickyMapping(endpointType, sessionHash, clientApiKeyId) {
if (!sessionHash) {
return
}
const normalizedEndpoint = this._normalizeEndpointType(endpointType)
const apiKeyPart = clientApiKeyId || 'default'
const stickyKey = `droid:${normalizedEndpoint}:${apiKeyPart}:${sessionHash}`
try {
await redis.deleteSessionAccountMapping(stickyKey)
logger.debug(`🧹 已清理 Droid 粘性会话映射:${stickyKey}`)
} catch (error) {
logger.warn(`⚠️ 清理 Droid 粘性会话映射失败:${stickyKey}`, error)
}
}
/**
* 清理 API Key 级别的粘性映射
*/
async _clearApiKeyStickyMapping(accountId, endpointType, sessionHash) {
if (!accountId || !sessionHash) {
return
}
try {
const stickyKey = this._composeApiKeyStickyKey(accountId, endpointType, sessionHash)
if (stickyKey) {
await redis.deleteSessionAccountMapping(stickyKey)
logger.debug(`🧹 已清理 Droid API Key 粘性映射:${stickyKey}`)
}
} catch (error) {
logger.warn(
`⚠️ 清理 Droid API Key 粘性映射失败:${accountId}endpoint: ${endpointType}`,
error
)
}
}
_mapNetworkErrorStatus(error) {
const code = (error && error.code ? String(error.code) : '').toUpperCase()