const { v4: uuidv4 } = require('uuid') const crypto = require('crypto') const axios = require('axios') const redis = require('../models/redis') const config = require('../../config/config') const logger = require('../utils/logger') const { maskToken } = require('../utils/tokenMask') const ProxyHelper = require('../utils/proxyHelper') const LRUCache = require('../utils/lruCache') /** * Droid 账户管理服务 * * 支持 WorkOS OAuth 集成,管理 Droid (Factory.ai) 账户 * 提供账户创建、token 刷新、代理配置等功能 */ class DroidAccountService { constructor() { // WorkOS OAuth 配置 this.oauthTokenUrl = 'https://api.workos.com/user_management/authenticate' this.factoryApiBaseUrl = 'https://app.factory.ai/api/llm' this.workosClientId = 'client_01HNM792M5G5G1A2THWPXKFMXB' // Token 刷新策略 this.refreshIntervalHours = 6 // 每6小时刷新一次 this.tokenValidHours = 8 // Token 有效期8小时 // 加密相关常量 this.ENCRYPTION_ALGORITHM = 'aes-256-cbc' this.ENCRYPTION_SALT = 'droid-account-salt' // 🚀 性能优化:缓存派生的加密密钥 this._encryptionKeyCache = null // 🔄 解密结果缓存 this._decryptCache = new LRUCache(500) // 🧹 定期清理缓存(每10分钟) setInterval( () => { this._decryptCache.cleanup() logger.info('🧹 Droid decrypt cache cleanup completed', this._decryptCache.getStats()) }, 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' } _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) } /** * 生成加密密钥(缓存优化) */ _generateEncryptionKey() { if (!this._encryptionKeyCache) { this._encryptionKeyCache = crypto.scryptSync( config.security.encryptionKey, this.ENCRYPTION_SALT, 32 ) logger.info('🔑 Droid encryption key derived and cached for performance optimization') } return this._encryptionKeyCache } /** * 加密敏感数据 */ _encryptSensitiveData(text) { if (!text) { return '' } const key = this._generateEncryptionKey() const iv = crypto.randomBytes(16) const cipher = crypto.createCipheriv(this.ENCRYPTION_ALGORITHM, key, iv) let encrypted = cipher.update(text, 'utf8', 'hex') encrypted += cipher.final('hex') return `${iv.toString('hex')}:${encrypted}` } /** * 解密敏感数据(带缓存) */ _decryptSensitiveData(encryptedText) { if (!encryptedText) { return '' } // 🎯 检查缓存 const cacheKey = crypto.createHash('sha256').update(encryptedText).digest('hex') const cached = this._decryptCache.get(cacheKey) if (cached !== undefined) { return cached } try { const key = this._generateEncryptionKey() const parts = encryptedText.split(':') const iv = Buffer.from(parts[0], 'hex') const encrypted = parts[1] const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv) let decrypted = decipher.update(encrypted, 'hex', 'utf8') decrypted += decipher.final('utf8') // 💾 存入缓存(5分钟过期) this._decryptCache.set(cacheKey, decrypted, 5 * 60 * 1000) return decrypted } catch (error) { logger.error('❌ Failed to decrypt Droid data:', error) return '' } } _parseApiKeyEntries(rawEntries) { if (!rawEntries) { return [] } if (Array.isArray(rawEntries)) { return rawEntries } if (typeof rawEntries === 'string') { try { const parsed = JSON.parse(rawEntries) return Array.isArray(parsed) ? parsed : [] } catch (error) { logger.warn('⚠️ Failed to parse Droid API Key entries:', error.message) return [] } } return [] } _buildApiKeyEntries(apiKeys, existingEntries = [], clearExisting = false) { const now = new Date().toISOString() const normalizedExisting = Array.isArray(existingEntries) ? existingEntries : [] const entries = clearExisting ? [] : normalizedExisting .filter((entry) => entry && entry.id && entry.encryptedKey) .map((entry) => ({ ...entry, status: entry.status || 'active' // 确保有默认状态 })) const hashSet = new Set(entries.map((entry) => entry.hash).filter(Boolean)) if (!Array.isArray(apiKeys) || apiKeys.length === 0) { return entries } for (const rawKey of apiKeys) { if (typeof rawKey !== 'string') { continue } const trimmed = rawKey.trim() if (!trimmed) { continue } const hash = crypto.createHash('sha256').update(trimmed).digest('hex') if (hashSet.has(hash)) { continue } hashSet.add(hash) entries.push({ id: uuidv4(), hash, encryptedKey: this._encryptSensitiveData(trimmed), createdAt: now, lastUsedAt: '', usageCount: '0', status: 'active', // 新增状态字段 errorMessage: '' // 新增错误信息字段 }) } return entries } _maskApiKeyEntries(entries) { if (!Array.isArray(entries)) { return [] } return entries.map((entry) => ({ id: entry.id, createdAt: entry.createdAt || '', lastUsedAt: entry.lastUsedAt || '', usageCount: entry.usageCount || '0', status: entry.status || 'active', // 新增状态字段 errorMessage: entry.errorMessage || '' // 新增错误信息字段 })) } _decryptApiKeyEntry(entry) { if (!entry || !entry.encryptedKey) { return null } const apiKey = this._decryptSensitiveData(entry.encryptedKey) if (!apiKey) { return null } const usageCountNumber = Number(entry.usageCount) return { id: entry.id, key: apiKey, hash: entry.hash || '', createdAt: entry.createdAt || '', lastUsedAt: entry.lastUsedAt || '', usageCount: Number.isFinite(usageCountNumber) && usageCountNumber >= 0 ? usageCountNumber : 0, status: entry.status || 'active', // 新增状态字段 errorMessage: entry.errorMessage || '' // 新增错误信息字段 } } async getDecryptedApiKeyEntries(accountId) { if (!accountId) { return [] } const accountData = await redis.getDroidAccount(accountId) if (!accountData) { return [] } const entries = this._parseApiKeyEntries(accountData.apiKeys) return entries .map((entry) => this._decryptApiKeyEntry(entry)) .filter((entry) => entry && entry.key) } async touchApiKeyUsage(accountId, keyId) { if (!accountId || !keyId) { return } try { const accountData = await redis.getDroidAccount(accountId) if (!accountData) { return } const entries = this._parseApiKeyEntries(accountData.apiKeys) const index = entries.findIndex((entry) => entry.id === keyId) if (index === -1) { return } const updatedEntry = { ...entries[index] } updatedEntry.lastUsedAt = new Date().toISOString() const usageCount = Number(updatedEntry.usageCount) updatedEntry.usageCount = String( Number.isFinite(usageCount) && usageCount >= 0 ? usageCount + 1 : 1 ) entries[index] = updatedEntry accountData.apiKeys = JSON.stringify(entries) accountData.apiKeyCount = String(entries.length) await redis.setDroidAccount(accountId, accountData) } catch (error) { logger.warn(`⚠️ Failed to update API key usage for Droid account ${accountId}:`, error) } } /** * 删除指定的 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 } } } /** * 标记指定的 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 刷新并验证凭证 */ async _refreshTokensWithWorkOS(refreshToken, proxyConfig = null, organizationId = null) { if (!refreshToken || typeof refreshToken !== 'string') { throw new Error('Refresh Token 无效') } const formData = new URLSearchParams() formData.append('grant_type', 'refresh_token') formData.append('refresh_token', refreshToken) formData.append('client_id', this.workosClientId) if (organizationId) { formData.append('organization_id', organizationId) } const requestOptions = { method: 'POST', url: this.oauthTokenUrl, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: formData.toString(), timeout: 30000 } if (proxyConfig) { const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig) if (proxyAgent) { requestOptions.httpAgent = proxyAgent requestOptions.httpsAgent = proxyAgent requestOptions.proxy = false logger.info( `🌐 使用代理验证 Droid Refresh Token: ${ProxyHelper.getProxyDescription(proxyConfig)}` ) } } const response = await axios(requestOptions) if (!response.data || !response.data.access_token) { throw new Error('WorkOS OAuth 返回数据无效') } const { access_token, refresh_token, user, organization_id, expires_in, token_type, authentication_method } = response.data let expiresAt = response.data.expires_at || '' if (!expiresAt) { const expiresInSeconds = typeof expires_in === 'number' && Number.isFinite(expires_in) ? expires_in : this.tokenValidHours * 3600 expiresAt = new Date(Date.now() + expiresInSeconds * 1000).toISOString() } return { accessToken: access_token, refreshToken: refresh_token || refreshToken, expiresAt, expiresIn: typeof expires_in === 'number' && Number.isFinite(expires_in) ? expires_in : null, user: user || null, organizationId: organization_id || '', tokenType: token_type || 'Bearer', authenticationMethod: authentication_method || '' } } /** * 使用 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 requestOptions.proxy = false } } 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 账户 * * @param {Object} options - 账户配置选项 * @returns {Promise} 创建的账户信息 */ async createAccount(options = {}) { const { name = 'Unnamed Droid Account', description = '', refreshToken = '', // WorkOS refresh token accessToken = '', // WorkOS access token (可选) expiresAt = '', // Token 过期时间 proxy = null, // { type: 'socks5', host: 'localhost', port: 1080, username: '', password: '' } isActive = true, accountType = 'shared', // 'dedicated' or 'shared' platform = 'droid', priority = 50, // 调度优先级 (1-100) schedulable = true, // 是否可被调度 endpointType = 'anthropic', // 默认端点类型: 'anthropic' 或 'openai' organizationId = '', ownerEmail = '', ownerName = '', userId = '', tokenType = 'Bearer', authenticationMethod = '', expiresIn = null, apiKeys = [] } = options const accountId = uuidv4() const normalizedEndpointType = this._sanitizeEndpointType(endpointType) let normalizedRefreshToken = refreshToken let normalizedAccessToken = accessToken let normalizedExpiresAt = expiresAt || '' let normalizedExpiresIn = expiresIn let normalizedOrganizationId = organizationId || '' let normalizedOwnerEmail = ownerEmail || '' let normalizedOwnerName = ownerName || '' let normalizedOwnerDisplayName = ownerName || ownerEmail || '' let normalizedUserId = userId || '' let normalizedTokenType = tokenType || 'Bearer' let normalizedAuthenticationMethod = authenticationMethod || '' let lastRefreshAt = accessToken ? new Date().toISOString() : '' let status = accessToken ? 'active' : 'created' const apiKeyEntries = this._buildApiKeyEntries(apiKeys) const hasApiKeys = apiKeyEntries.length > 0 if (hasApiKeys) { normalizedAuthenticationMethod = 'api_key' normalizedAccessToken = '' normalizedRefreshToken = '' normalizedExpiresAt = '' normalizedExpiresIn = null lastRefreshAt = '' status = 'active' } const normalizedAuthMethod = typeof normalizedAuthenticationMethod === 'string' ? normalizedAuthenticationMethod.toLowerCase().trim() : '' const isApiKeyProvision = normalizedAuthMethod === 'api_key' const isManualProvision = normalizedAuthMethod === 'manual' const provisioningMode = isApiKeyProvision ? 'api_key' : isManualProvision ? 'manual' : 'oauth' if (isApiKeyProvision) { logger.info( `🔍 [Droid api_key] 初始密钥 - AccountName: ${name}, KeyCount: ${apiKeyEntries.length}` ) } else { 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 (!isApiKeyProvision && normalizedRefreshToken && isManualProvision) { try { 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 normalizedRefreshToken = refreshed.refreshToken normalizedExpiresAt = refreshed.expiresAt || normalizedExpiresAt normalizedTokenType = refreshed.tokenType || normalizedTokenType normalizedAuthenticationMethod = refreshed.authenticationMethod || normalizedAuthenticationMethod if (refreshed.expiresIn !== null) { normalizedExpiresIn = refreshed.expiresIn } if (refreshed.organizationId) { normalizedOrganizationId = refreshed.organizationId } 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' logger.success(`✅ 使用 Refresh Token 成功验证并刷新 Droid 账户: ${name} (${accountId})`) } catch (error) { logger.error('❌ 使用 Refresh Token 验证 Droid 账户失败:', error) throw new Error(`Refresh Token 验证失败:${error.message}`) } } else if (!isApiKeyProvision && 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 (!isApiKeyProvision && !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 = { id: accountId, name, description, refreshToken: this._encryptSensitiveData(normalizedRefreshToken), accessToken: this._encryptSensitiveData(normalizedAccessToken), expiresAt: normalizedExpiresAt || '', // OAuth Token 过期时间(技术字段,自动刷新) // ✅ 新增:账户订阅到期时间(业务字段,手动管理) subscriptionExpiresAt: options.subscriptionExpiresAt || null, proxy: proxy ? JSON.stringify(proxy) : '', isActive: isActive.toString(), accountType, platform, priority: priority.toString(), createdAt: new Date().toISOString(), lastUsedAt: '', lastRefreshAt, status, // created, active, expired, error errorMessage: '', schedulable: schedulable.toString(), endpointType: normalizedEndpointType, // anthropic 或 openai organizationId: normalizedOrganizationId || '', owner: normalizedOwnerName || normalizedOwnerEmail || '', ownerEmail: normalizedOwnerEmail || '', ownerName: normalizedOwnerName || '', ownerDisplayName: normalizedOwnerDisplayName || normalizedOwnerName || normalizedOwnerEmail || '', userId: normalizedUserId || '', tokenType: normalizedTokenType || 'Bearer', authenticationMethod: normalizedAuthenticationMethod || '', expiresIn: normalizedExpiresIn !== null && normalizedExpiresIn !== undefined ? String(normalizedExpiresIn) : '', apiKeys: hasApiKeys ? JSON.stringify(apiKeyEntries) : '', apiKeyCount: hasApiKeys ? String(apiKeyEntries.length) : '0', apiKeyStrategy: hasApiKeys ? 'random_sticky' : '' } await redis.setDroidAccount(accountId, accountData) 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 } } /** * 获取 Droid 账户信息 */ async getAccount(accountId) { const account = await redis.getDroidAccount(accountId) if (!account || Object.keys(account).length === 0) { return null } // 解密敏感数据 const apiKeyEntries = this._parseApiKeyEntries(account.apiKeys) return { ...account, id: accountId, endpointType: this._sanitizeEndpointType(account.endpointType), refreshToken: this._decryptSensitiveData(account.refreshToken), accessToken: this._decryptSensitiveData(account.accessToken), apiKeys: this._maskApiKeyEntries(apiKeyEntries), apiKeyCount: apiKeyEntries.length } } /** * 获取所有 Droid 账户 */ async getAllAccounts() { const accounts = await redis.getAllDroidAccounts() return accounts.map((account) => ({ ...account, endpointType: this._sanitizeEndpointType(account.endpointType), // 不解密完整 token,只返回掩码 refreshToken: account.refreshToken ? '***ENCRYPTED***' : '', accessToken: account.accessToken ? maskToken(this._decryptSensitiveData(account.accessToken)) : '', // ✅ 前端显示订阅过期时间(业务字段) expiresAt: account.subscriptionExpiresAt || null, platform: account.platform || 'droid', apiKeyCount: (() => { const parsedCount = this._parseApiKeyEntries(account.apiKeys).length if (account.apiKeyCount === undefined || account.apiKeyCount === null) { return parsedCount } const numeric = Number(account.apiKeyCount) return Number.isFinite(numeric) && numeric >= 0 ? numeric : parsedCount })() })) } /** * 更新 Droid 账户 */ async updateAccount(accountId, updates) { const account = await this.getAccount(accountId) if (!account) { 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') { sanitizedUpdates.accessToken = sanitizedUpdates.accessToken.trim() } if (typeof sanitizedUpdates.refreshToken === 'string') { sanitizedUpdates.refreshToken = sanitizedUpdates.refreshToken.trim() } if (sanitizedUpdates.endpointType) { sanitizedUpdates.endpointType = this._sanitizeEndpointType(sanitizedUpdates.endpointType) } const parseProxyConfig = (value) => { if (!value) { return null } if (typeof value === 'object') { return value } if (typeof value === 'string' && value.trim()) { try { return JSON.parse(value) } catch (error) { logger.warn('⚠️ Failed to parse stored Droid proxy config:', error.message) } } return null } let proxyConfig = null if (updates.proxy !== undefined) { if (updates.proxy && typeof updates.proxy === 'object') { proxyConfig = updates.proxy sanitizedUpdates.proxy = JSON.stringify(updates.proxy) } else if (typeof updates.proxy === 'string' && updates.proxy.trim()) { proxyConfig = parseProxyConfig(updates.proxy) sanitizedUpdates.proxy = updates.proxy } else { sanitizedUpdates.proxy = '' } } else if (account.proxy) { proxyConfig = parseProxyConfig(account.proxy) } const hasNewRefreshToken = typeof sanitizedUpdates.refreshToken === 'string' && sanitizedUpdates.refreshToken if (hasNewRefreshToken) { try { const refreshed = await this._refreshTokensWithWorkOS( sanitizedUpdates.refreshToken, proxyConfig ) sanitizedUpdates.accessToken = refreshed.accessToken sanitizedUpdates.refreshToken = refreshed.refreshToken || sanitizedUpdates.refreshToken sanitizedUpdates.expiresAt = refreshed.expiresAt || sanitizedUpdates.expiresAt || account.expiresAt || '' if (refreshed.expiresIn !== null && refreshed.expiresIn !== undefined) { sanitizedUpdates.expiresIn = String(refreshed.expiresIn) } sanitizedUpdates.tokenType = refreshed.tokenType || account.tokenType || 'Bearer' sanitizedUpdates.authenticationMethod = refreshed.authenticationMethod || account.authenticationMethod || '' sanitizedUpdates.organizationId = sanitizedUpdates.organizationId || refreshed.organizationId || account.organizationId || '' sanitizedUpdates.lastRefreshAt = new Date().toISOString() sanitizedUpdates.status = 'active' sanitizedUpdates.errorMessage = '' if (refreshed.user) { const userInfo = refreshed.user const email = typeof userInfo.email === 'string' ? userInfo.email.trim() : '' if (email) { sanitizedUpdates.ownerEmail = email } 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) { sanitizedUpdates.ownerName = derivedName sanitizedUpdates.ownerDisplayName = derivedName sanitizedUpdates.owner = derivedName } else if (sanitizedUpdates.ownerEmail) { sanitizedUpdates.ownerName = sanitizedUpdates.ownerName || sanitizedUpdates.ownerEmail sanitizedUpdates.ownerDisplayName = sanitizedUpdates.ownerDisplayName || sanitizedUpdates.ownerEmail sanitizedUpdates.owner = sanitizedUpdates.owner || sanitizedUpdates.ownerEmail } if (typeof userInfo.id === 'string' && userInfo.id.trim()) { sanitizedUpdates.userId = userInfo.id.trim() } } } catch (error) { logger.error('❌ 使用新的 Refresh Token 更新 Droid 账户失败:', error) throw new Error(`Refresh Token 验证失败:${error.message || '未知错误'}`) } } // ✅ 如果通过路由映射更新了 subscriptionExpiresAt,直接保存 // subscriptionExpiresAt 是业务字段,与 token 刷新独立 if (sanitizedUpdates.subscriptionExpiresAt !== undefined) { // 直接保存,不做任何调整 } if (sanitizedUpdates.proxy === undefined) { sanitizedUpdates.proxy = account.proxy || '' } // 使用 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', 'update'].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 } if (sanitizedUpdates.clearApiKeys !== undefined) { delete sanitizedUpdates.clearApiKeys } if (sanitizedUpdates.apiKeyUpdateMode !== undefined) { delete sanitizedUpdates.apiKeyUpdateMode } if (sanitizedUpdates.removeApiKeys !== undefined) { delete sanitizedUpdates.removeApiKeys } 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 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 mergedApiKeys = this._buildApiKeyEntries( newApiKeysInput, existingApiKeyEntries, clearExisting ) 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 === '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}` ) } else { logger.info( `🔑 追加模式更新 Droid API keys for ${accountId}: 当前总数 ${mergedApiKeys.length},新增 ${addedCount}` ) } if (mergedApiKeys.length > 0) { sanitizedUpdates.authenticationMethod = 'api_key' sanitizedUpdates.status = sanitizedUpdates.status || 'active' } 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 } } const encryptedUpdates = { ...sanitizedUpdates } if (sanitizedUpdates.refreshToken !== undefined) { encryptedUpdates.refreshToken = this._encryptSensitiveData(sanitizedUpdates.refreshToken) } if (sanitizedUpdates.accessToken !== undefined) { encryptedUpdates.accessToken = this._encryptSensitiveData(sanitizedUpdates.accessToken) } const baseAccountData = hasStoredAccount ? { ...storedAccount } : { id: accountId } const updatedData = { ...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) logger.info(`✅ Updated Droid account: ${accountId}`) return this.getAccount(accountId) } /** * 删除 Droid 账户 */ async deleteAccount(accountId) { await redis.deleteDroidAccount(accountId) logger.success(`🗑️ Deleted Droid account: ${accountId}`) } /** * 刷新 Droid 账户的 access token * * 使用 WorkOS OAuth refresh token 刷新 access token */ async refreshAccessToken(accountId, proxyConfig = null) { const account = await this.getAccount(accountId) if (!account) { throw new Error(`Droid account not found: ${accountId}`) } if (!account.refreshToken) { throw new Error(`Droid account ${accountId} has no refresh token`) } logger.info(`🔄 Refreshing Droid account token: ${account.name} (${accountId})`) try { const proxy = proxyConfig || (account.proxy ? JSON.parse(account.proxy) : null) const refreshed = await this._refreshTokensWithWorkOS( account.refreshToken, proxy, account.organizationId || null ) // 更新账户信息 await this.updateAccount(accountId, { accessToken: refreshed.accessToken, refreshToken: refreshed.refreshToken || account.refreshToken, expiresAt: refreshed.expiresAt, expiresIn: refreshed.expiresIn !== null && refreshed.expiresIn !== undefined ? String(refreshed.expiresIn) : account.expiresIn, tokenType: refreshed.tokenType || account.tokenType || 'Bearer', authenticationMethod: refreshed.authenticationMethod || account.authenticationMethod || '', organizationId: refreshed.organizationId || account.organizationId, lastRefreshAt: new Date().toISOString(), status: 'active', errorMessage: '' }) // 记录用户信息 if (refreshed.user) { const { user } = refreshed const updates = {} logger.info( `✅ Droid token refreshed for: ${user.email} (${user.first_name} ${user.last_name})` ) logger.info(` Organization ID: ${refreshed.organizationId || 'N/A'}`) if (typeof user.email === 'string' && user.email.trim()) { updates.ownerEmail = user.email.trim() } const nameParts = [] if (typeof user.first_name === 'string' && user.first_name.trim()) { nameParts.push(user.first_name.trim()) } if (typeof user.last_name === 'string' && user.last_name.trim()) { nameParts.push(user.last_name.trim()) } const derivedName = nameParts.join(' ').trim() || (typeof user.name === 'string' ? user.name.trim() : '') || (typeof user.display_name === 'string' ? user.display_name.trim() : '') if (derivedName) { updates.ownerName = derivedName updates.ownerDisplayName = derivedName updates.owner = derivedName } else if (updates.ownerEmail) { updates.owner = updates.ownerEmail updates.ownerName = updates.ownerEmail updates.ownerDisplayName = updates.ownerEmail } if (typeof user.id === 'string' && user.id.trim()) { updates.userId = user.id.trim() } if (Object.keys(updates).length > 0) { await this.updateAccount(accountId, updates) } } logger.success(`✅ Droid account token refreshed successfully: ${accountId}`) return { accessToken: refreshed.accessToken, refreshToken: refreshed.refreshToken || account.refreshToken, expiresAt: refreshed.expiresAt } } catch (error) { logger.error(`❌ Failed to refresh Droid account token: ${accountId}`, error) // 更新账户状态为错误 await this.updateAccount(accountId, { status: 'error', errorMessage: error.message || 'Token refresh failed' }) throw error } } /** * 检查 token 是否需要刷新 */ shouldRefreshToken(account) { if (!account.lastRefreshAt) { return true // 从未刷新过 } const lastRefreshTime = new Date(account.lastRefreshAt).getTime() const hoursSinceRefresh = (Date.now() - lastRefreshTime) / (1000 * 60 * 60) return hoursSinceRefresh >= this.refreshIntervalHours } /** * 检查账户订阅是否过期 * @param {Object} account - 账户对象 * @returns {boolean} - true: 已过期, false: 未过期 */ isSubscriptionExpired(account) { if (!account.subscriptionExpiresAt) { return false // 未设置视为永不过期 } const expiryDate = new Date(account.subscriptionExpiresAt) return expiryDate <= new Date() } /** * 获取有效的 access token(自动刷新) */ async getValidAccessToken(accountId) { let account = await this.getAccount(accountId) if (!account) { throw new Error(`Droid account not found: ${accountId}`) } if ( typeof account.authenticationMethod === 'string' && account.authenticationMethod.toLowerCase().trim() === 'api_key' ) { throw new Error(`Droid account ${accountId} 已配置为 API Key 模式,不能获取 Access Token`) } // 检查是否需要刷新 if (this.shouldRefreshToken(account)) { logger.info(`🔄 Droid account token needs refresh: ${accountId}`) const proxyConfig = account.proxy ? JSON.parse(account.proxy) : null await this.refreshAccessToken(accountId, proxyConfig) account = await this.getAccount(accountId) } if (!account.accessToken) { throw new Error(`Droid account ${accountId} has no valid access token`) } return account.accessToken } /** * 获取可调度的 Droid 账户列表 */ async getSchedulableAccounts(endpointType = null) { const allAccounts = await redis.getAllDroidAccounts() const normalizedFilter = endpointType ? this._sanitizeEndpointType(endpointType) : null return allAccounts .filter((account) => { const isActive = this._isTruthy(account.isActive) const isSchedulable = this._isTruthy(account.schedulable) const status = typeof account.status === 'string' ? account.status.toLowerCase() : '' // ✅ 检查账户订阅是否过期 if (this.isSubscriptionExpired(account)) { logger.debug( `⏰ Skipping expired Droid account: ${account.name}, expired at ${account.subscriptionExpiresAt}` ) return false } if (!isActive || !isSchedulable || status !== 'active') { return false } 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) => ({ ...account, endpointType: this._sanitizeEndpointType(account.endpointType), priority: parseInt(account.priority, 10) || 50, // 解密 accessToken 用于使用 accessToken: this._decryptSensitiveData(account.accessToken) })) .sort((a, b) => a.priority - b.priority) // 按优先级排序 } /** * 选择一个可用的 Droid 账户(简单轮询) */ async selectAccount(endpointType = null) { let accounts = await this.getSchedulableAccounts(endpointType) if (accounts.length === 0 && endpointType) { logger.warn( `No Droid accounts found for endpoint ${endpointType}, falling back to any available account` ) accounts = await this.getSchedulableAccounts(null) } if (accounts.length === 0) { throw new Error( `No schedulable Droid accounts available${endpointType ? ` for endpoint type: ${endpointType}` : ''}` ) } // 简单轮询:选择最高优先级且最久未使用的账户 let selectedAccount = accounts[0] for (const account of accounts) { if (account.priority < selectedAccount.priority) { selectedAccount = account } else if (account.priority === selectedAccount.priority) { // 相同优先级,选择最久未使用的 const selectedLastUsed = new Date(selectedAccount.lastUsedAt || 0).getTime() const accountLastUsed = new Date(account.lastUsedAt || 0).getTime() if (accountLastUsed < selectedLastUsed) { selectedAccount = account } } } // 更新最后使用时间 await this.updateAccount(selectedAccount.id, { lastUsedAt: new Date().toISOString() }) logger.info( `✅ Selected Droid account: ${selectedAccount.name} (${selectedAccount.id}) - Endpoint: ${this._sanitizeEndpointType(selectedAccount.endpointType)}` ) return selectedAccount } /** * 获取 Factory.ai API 的完整 URL */ getFactoryApiUrl(endpointType, endpoint) { const normalizedType = this._sanitizeEndpointType(endpointType) const baseUrls = { anthropic: `${this.factoryApiBaseUrl}/a${endpoint}`, openai: `${this.factoryApiBaseUrl}/o${endpoint}` } 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) } } } // 导出单例 module.exports = new DroidAccountService()