From 8093dfb11c376fff6cd5867ede9dc73839bd48ac Mon Sep 17 00:00:00 2001 From: "liangjie.wanglj" <122603020@qq.com> Date: Mon, 13 Oct 2025 10:55:19 +0800 Subject: [PATCH 01/15] =?UTF-8?q?=E4=BC=98=E5=8C=96Claude=20OAuth=20?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E7=9A=84=E6=A8=A1=E5=9E=8B=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/unifiedClaudeScheduler.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/services/unifiedClaudeScheduler.js b/src/services/unifiedClaudeScheduler.js index fc80a3b1..fcfb0453 100644 --- a/src/services/unifiedClaudeScheduler.js +++ b/src/services/unifiedClaudeScheduler.js @@ -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 { From 268f04158877ea228c913bcd9591c75ffb3bde08 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 13 Oct 2025 03:41:53 +0000 Subject: [PATCH 02/15] chore: sync VERSION file with release v1.1.173 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 5b7cfb35..ae88434e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.172 +1.1.173 From 1f9afc788bd64ded183d2f8ee9743aaa9fdced6f Mon Sep 17 00:00:00 2001 From: AAEE86 Date: Mon, 13 Oct 2025 18:19:17 +0800 Subject: [PATCH 03/15] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Droid=E8=B4=A6?= =?UTF-8?q?=E6=88=B7API=20Key=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 0cf3ca6c7eafcf28a2da7e8bfd6814b4883bb752) --- src/routes/admin.js | 103 +++++ src/services/droidAccountService.js | 120 +++++- src/services/droidRelayService.js | 51 ++- .../src/components/accounts/AccountForm.vue | 43 +- .../accounts/ApiKeyManagementModal.vue | 406 ++++++++++++++++++ web/admin-spa/src/views/AccountsView.vue | 34 +- 6 files changed, 716 insertions(+), 41 deletions(-) create mode 100644 web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue diff --git a/src/routes/admin.js b/src/routes/admin.js index fc212c87..fe49e72e 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -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 { diff --git a/src/services/droidAccountService.js b/src/services/droidAccountService.js index c6baecbf..7414686b 100644 --- a/src/services/droidAccountService.js +++ b/src/services/droidAccountService.js @@ -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,53 @@ 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 +1169,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}` diff --git a/src/services/droidRelayService.js b/src/services/droidRelayService.js index 1072b869..8e892834 100644 --- a/src/services/droidRelayService.js +++ b/src/services/droidRelayService.js @@ -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,52 @@ 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 diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue index 67ab4390..2518f421 100644 --- a/web/admin-spa/src/components/accounts/AccountForm.vue +++ b/web/admin-spa/src/components/accounts/AccountForm.vue @@ -1817,7 +1817,7 @@
  • 新会话将随机命中一个 Key,并在会话有效期内保持粘性。
  • 若某 Key 失效,会自动切换到剩余可用 Key,最大化成功率。
  • - 若上游返回 4xx 错误码,该 Key 会被自动移除;全部 Key 清空后账号将暂停调度。 + 若上游返回 4xx 错误码,该 Key 会被自动标记为异常;全部 Key 异常后账号将暂停调度。
  • @@ -3011,10 +3011,18 @@ > -
    -
    - 更新 API Key -
    +
    +
    +
    更新 API Key
    + +

    当前已保存 {{ existingApiKeyCount }} 条 API Key。您可以追加新的 Key,或通过下方模式快速覆盖、删除指定 Key。 @@ -3187,6 +3195,15 @@ @close="showGroupManagement = false" @refresh="handleGroupRefresh" /> + + + @@ -3200,6 +3217,7 @@ import ProxyConfig from './ProxyConfig.vue' import OAuthFlow from './OAuthFlow.vue' import ConfirmModal from '@/components/common/ConfirmModal.vue' import GroupManagementModal from './GroupManagementModal.vue' +import ApiKeyManagementModal from './ApiKeyManagementModal.vue' const props = defineProps({ account: { @@ -3239,6 +3257,9 @@ const clearingCache = ref(false) // 平台分组状态 const platformGroup = ref('') +// API Key 管理模态框 +const showApiKeyManagement = ref(false) + // 根据现有平台确定分组 const determinePlatformGroup = (platform) => { if (['claude', 'claude-console', 'ccr', 'bedrock'].includes(platform)) { @@ -4816,6 +4837,18 @@ const handleGroupRefresh = async () => { await loadGroups() } +// 处理 API Key 管理模态框刷新 +const handleApiKeyRefresh = async () => { + // 刷新账户信息以更新 API Key 数量 + if (props.account?.id) { + try { + await accountsStore.fetchAccounts() + } catch (error) { + console.error('Failed to refresh account data:', error) + } + } +} + // 监听平台变化,重置表单 watch( () => form.value.platform, diff --git a/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue b/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue new file mode 100644 index 00000000..d23efafc --- /dev/null +++ b/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue @@ -0,0 +1,406 @@ + + + diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue index dad086e2..6528787a 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -3108,6 +3108,25 @@ const getDroidApiKeyCount = (account) => { return 0 } + // 优先使用 apiKeys 数组来计算正常状态的 API Keys + if (Array.isArray(account.apiKeys)) { + // 只计算状态不是 'error' 的 API Keys + return account.apiKeys.filter(apiKey => apiKey.status !== 'error').length + } + + // 如果是字符串格式的 apiKeys,尝试解析 + if (typeof account.apiKeys === 'string' && account.apiKeys.trim()) { + try { + const parsed = JSON.parse(account.apiKeys) + if (Array.isArray(parsed)) { + // 只计算状态不是 'error' 的 API Keys + return parsed.filter(apiKey => apiKey.status !== 'error').length + } + } catch (error) { + // 忽略解析错误,继续使用其他字段 + } + } + const candidates = [ account.apiKeyCount, account.api_key_count, @@ -3122,21 +3141,6 @@ const getDroidApiKeyCount = (account) => { } } - if (Array.isArray(account.apiKeys)) { - return account.apiKeys.length - } - - if (typeof account.apiKeys === 'string' && account.apiKeys.trim()) { - try { - const parsed = JSON.parse(account.apiKeys) - if (Array.isArray(parsed)) { - return parsed.length - } - } catch (error) { - // 忽略解析错误,维持默认值 - } - } - return 0 } From 46ba514801d53c183d604b4453c5cb2103a17ad6 Mon Sep 17 00:00:00 2001 From: DokiDoki1103 <1666888816@qq.com> Date: Mon, 13 Oct 2025 19:01:24 +0800 Subject: [PATCH 04/15] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=B4=A6?= =?UTF-8?q?=E6=88=B7=E5=88=B0=E6=9C=9F=E6=97=B6=E9=97=B4=E7=9A=84=E6=97=B6?= =?UTF-8?q?=E5=8C=BA=E5=81=8F=E5=B7=AE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复了在编辑账户到期时间时,保存后显示时间相差8小时的问题。 问题原因: - datetime-local 输入框的值使用 new Date(string) 解析时 - 部分浏览器会错误地将其解释为 UTC 时间 - 导致保存和显示时出现时区转换不一致 解决方案: - 手动解析日期时间字符串的各个部分 - 使用 Date 构造函数明确创建本地时间对象 - 然后统一转换为 UTC ISO 字符串存储 - 确保时区转换的一致性 修改文件: - web/admin-spa/src/components/accounts/AccountExpiryEditModal.vue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/components/accounts/AccountExpiryEditModal.vue | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/admin-spa/src/components/accounts/AccountExpiryEditModal.vue b/web/admin-spa/src/components/accounts/AccountExpiryEditModal.vue index 046c3332..61f2438c 100644 --- a/web/admin-spa/src/components/accounts/AccountExpiryEditModal.vue +++ b/web/admin-spa/src/components/accounts/AccountExpiryEditModal.vue @@ -304,7 +304,14 @@ const selectQuickOption = (value) => { // 更新自定义过期时间 const updateCustomExpiryPreview = () => { if (localForm.customExpireDate) { - localForm.expiresAt = new Date(localForm.customExpireDate).toISOString() + // 手动解析日期时间字符串,确保它被正确解释为本地时间 + const [datePart, timePart] = localForm.customExpireDate.split('T') + const [year, month, day] = datePart.split('-').map(Number) + const [hours, minutes] = timePart.split(':').map(Number) + + // 使用构造函数创建本地时间的 Date 对象,然后转换为 UTC ISO 字符串 + const localDate = new Date(year, month - 1, day, hours, minutes, 0, 0) + localForm.expiresAt = localDate.toISOString() } } From ea3ad2157fded3aed4707603062f8793de0a58ca Mon Sep 17 00:00:00 2001 From: AAEE86 Date: Tue, 14 Oct 2025 00:53:19 +0800 Subject: [PATCH 05/15] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96API=20Key?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E7=8A=B6=E6=80=81=E7=A0=81=E7=9A=84=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/droidRelayService.js | 2 +- .../accounts/ApiKeyManagementModal.vue | 76 +++++++++---------- 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/src/services/droidRelayService.js b/src/services/droidRelayService.js index 8e892834..857f677b 100644 --- a/src/services/droidRelayService.js +++ b/src/services/droidRelayService.js @@ -1151,7 +1151,7 @@ class DroidRelayService { if (authMethod === 'api_key') { if (selectedAccountApiKey?.id) { let markResult = null - const errorMessage = `上游返回 ${statusCode} 错误` + const errorMessage = `${statusCode}` try { // 标记API Key为异常状态而不是删除 diff --git a/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue b/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue index d23efafc..1342b6af 100644 --- a/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue +++ b/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue @@ -49,11 +49,29 @@

    + +
    + + {{ apiKey.errorMessage }} + +
    +
    -
    +
    +
    - - -
    -
    -
    - - {{ apiKey.errorMessage }} -
    - -
    -
    From e051ade27e5438d017f155b8c53a05cf693fcc16 Mon Sep 17 00:00:00 2001 From: AAEE86 Date: Tue, 14 Oct 2025 01:05:22 +0800 Subject: [PATCH 06/15] =?UTF-8?q?feat:=20=E6=8C=89=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=97=B6=E9=97=B4=E6=8E=92=E5=BA=8FAPI=20Key?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../accounts/ApiKeyManagementModal.vue | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue b/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue index 1342b6af..da9970e0 100644 --- a/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue +++ b/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue @@ -261,7 +261,7 @@ const loadApiKeys = async () => { } // 转换为统一格式 - apiKeys.value = parsedKeys.map((item) => { + const formattedKeys = parsedKeys.map((item) => { if (typeof item === 'string') { // 对于字符串类型的API Key,保持默认状态为active return { @@ -290,6 +290,24 @@ const loadApiKeys = async () => { errorMessage: '' } }) + + // 按最新使用时间排序(最近使用的在前,未使用的在后) + apiKeys.value = formattedKeys.sort((a, b) => { + // 如果都有 lastUsedAt,按时间降序排序 + if (a.lastUsedAt && b.lastUsedAt) { + return new Date(b.lastUsedAt) - new Date(a.lastUsedAt) + } + // 如果 a 有时间,b 没有,a 排在前面 + if (a.lastUsedAt && !b.lastUsedAt) { + return -1 + } + // 如果 b 有时间,a 没有,b 排在前面 + if (!a.lastUsedAt && b.lastUsedAt) { + return 1 + } + // 如果都没有时间,按使用次数降序排序 + return (b.usageCount || 0) - (a.usageCount || 0) + }) } catch (error) { console.error('Failed to load API keys:', error) showToast('加载 API Key 失败', 'error') From 8d84e2fa6e180fc22ce6a1eae7005edf08c09db5 Mon Sep 17 00:00:00 2001 From: AAEE86 Date: Tue, 14 Oct 2025 09:33:17 +0800 Subject: [PATCH 07/15] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96API=20Key?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9B=B4=E6=96=B0=E5=92=8C=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/droidAccountService.js | 19 +++++++++++++------ src/services/droidRelayService.js | 8 +++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/services/droidAccountService.js b/src/services/droidAccountService.js index 7414686b..34fc2c59 100644 --- a/src/services/droidAccountService.js +++ b/src/services/droidAccountService.js @@ -1124,9 +1124,7 @@ class DroidAccountService { updatedHashes.add(hash) // 查找现有条目 - const existingIndex = mergedApiKeys.findIndex( - (entry) => entry && entry.hash === hash - ) + const existingIndex = mergedApiKeys.findIndex((entry) => entry && entry.hash === hash) if (existingIndex !== -1) { // 更新现有条目的状态信息 @@ -1134,9 +1132,18 @@ class DroidAccountService { 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' + 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 } diff --git a/src/services/droidRelayService.js b/src/services/droidRelayService.js index 857f677b..38cd9a6b 100644 --- a/src/services/droidRelayService.js +++ b/src/services/droidRelayService.js @@ -1182,15 +1182,13 @@ class DroidRelayService { // 检查是否还有可用的API Key try { const availableEntries = await droidAccountService.getDecryptedApiKeyEntries(accountId) - const activeEntries = availableEntries.filter(entry => entry.status !== 'error') - + 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` - ) + logger.info(`ℹ️ Droid 账号 ${accountId} 仍有 ${activeEntries.length} 个可用 API Key`) } } catch (error) { logger.error(`❌ 检查可用API Key失败(Account: ${accountId}):`, error) From 38c61e1018c88ed1f0d7b11f6552e2dff250f9d9 Mon Sep 17 00:00:00 2001 From: AAEE86 Date: Tue, 14 Oct 2025 09:37:46 +0800 Subject: [PATCH 08/15] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96API=20Key?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E8=BF=87=E6=BB=A4=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E4=BB=A3=E7=A0=81=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../accounts/ApiKeyManagementModal.vue | 59 ++++++++++++------- web/admin-spa/src/views/AccountsView.vue | 4 +- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue b/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue index da9970e0..f8201e23 100644 --- a/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue +++ b/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue @@ -53,7 +53,9 @@ >
    {{ maskApiKey(apiKey.key) }} @@ -88,7 +90,7 @@
    -
    +
    @@ -138,15 +145,18 @@ apiKey.status === 'active' ? '正常' : apiKey.status === 'error' - ? '异常' - : apiKey.status === 'disabled' - ? '禁用' - : apiKey.status || '未知' + ? '异常' + : apiKey.status === 'disabled' + ? '禁用' + : apiKey.status || '未知' }}
    - 使用: {{ apiKey.usageCount || 0 }} + 使用: {{ apiKey.usageCount || 0 }}
    {{ formatTime(apiKey.lastUsedAt) }} @@ -159,18 +169,21 @@
    - 显示 {{ (currentPage - 1) * pageSize + 1 }}-{{ Math.min(currentPage * pageSize, totalItems) }} 项,共 {{ totalItems }} 项 + 显示 {{ (currentPage - 1) * pageSize + 1 }}-{{ + Math.min(currentPage * pageSize, totalItems) + }} + 项,共 {{ totalItems }} 项
    diff --git a/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue b/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue index f8201e23..fda2f94a 100644 --- a/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue +++ b/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue @@ -341,7 +341,7 @@ const deleteApiKey = async (apiKey, index) => { try { // 准备更新数据:删除指定的 key const updateData = { - apiKeys: [apiKey.key], + removeApiKeys: [apiKey.key], apiKeyUpdateMode: 'delete' } From 8dd07919f4636ed1584572d0535cc56971835695 Mon Sep 17 00:00:00 2001 From: AAEE86 Date: Tue, 14 Oct 2025 10:34:38 +0800 Subject: [PATCH 10/15] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20API=20Key=20?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=95=8C=E9=9D=A2=E6=8C=89=E9=92=AE=E7=9A=84?= =?UTF-8?q?=E7=82=B9=E5=87=BB=E4=BA=8B=E4=BB=B6=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/admin-spa/src/components/accounts/AccountForm.vue | 2 +- .../src/components/accounts/ApiKeyManagementModal.vue | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue index 572e3a0a..0bea28a8 100644 --- a/web/admin-spa/src/components/accounts/AccountForm.vue +++ b/web/admin-spa/src/components/accounts/AccountForm.vue @@ -3016,8 +3016,8 @@
    更新 API Key
    @@ -459,11 +459,9 @@
    -
    echo $env:CODE_ASSIST_ENDPOINT
    -
    - echo $env:GOOGLE_CLOUD_ACCESS_TOKEN -
    -
    echo $env:GOOGLE_GENAI_USE_GCA
    +
    echo $env:GOOGLE_GEMINI_BASE_URL
    +
    echo $env:GEMINI_API_KEY
    +
    echo $env:GEMINI_MODEL
    @@ -1047,13 +1045,13 @@ class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" >
    - export CODE_ASSIST_ENDPOINT="{{ geminiBaseUrl }}" + export GOOGLE_GEMINI_BASE_URL="{{ geminiBaseUrl }}"
    - export GOOGLE_CLOUD_ACCESS_TOKEN="你的API密钥" + export GEMINI_API_KEY="你的API密钥"
    - export GOOGLE_GENAI_USE_GCA="true" + export GEMINI_MODEL="gemini-2.5-pro"

    @@ -1075,13 +1073,13 @@ >

    # 对于 zsh (默认)
    - echo 'export CODE_ASSIST_ENDPOINT="{{ geminiBaseUrl }}"' >> ~/.zshrc + echo 'export GOOGLE_GEMINI_BASE_URL="{{ geminiBaseUrl }}"' >> ~/.zshrc
    - echo 'export GOOGLE_CLOUD_ACCESS_TOKEN="你的API密钥"' >> ~/.zshrc + echo 'export GEMINI_API_KEY="你的API密钥"' >> ~/.zshrc
    - echo 'export GOOGLE_GENAI_USE_GCA="true"' >> ~/.zshrc + echo 'export GEMINI_MODEL="gemini-2.5-pro"' >> ~/.zshrc
    source ~/.zshrc
    @@ -1090,13 +1088,13 @@ >
    # 对于 bash
    - echo 'export CODE_ASSIST_ENDPOINT="{{ geminiBaseUrl }}"' >> ~/.bash_profile + echo 'export GOOGLE_GEMINI_BASE_URL="{{ geminiBaseUrl }}"' >> ~/.bash_profile
    - echo 'export GOOGLE_CLOUD_ACCESS_TOKEN="你的API密钥"' >> ~/.bash_profile + echo 'export GEMINI_API_KEY="你的API密钥"' >> ~/.bash_profile
    - echo 'export GOOGLE_GENAI_USE_GCA="true"' >> ~/.bash_profile + echo 'export GEMINI_MODEL="gemini-2.5-pro"' >> ~/.bash_profile
    source ~/.bash_profile
    @@ -1112,9 +1110,9 @@
    -
    echo $CODE_ASSIST_ENDPOINT
    -
    echo $GOOGLE_CLOUD_ACCESS_TOKEN
    -
    echo $GOOGLE_GENAI_USE_GCA
    +
    echo $GOOGLE_GEMINI_BASE_URL
    +
    echo $GEMINI_API_KEY
    +
    echo $GEMINI_MODEL
    @@ -1660,13 +1658,13 @@ class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" >
    - export CODE_ASSIST_ENDPOINT="{{ geminiBaseUrl }}" + export GOOGLE_GEMINI_BASE_URL="{{ geminiBaseUrl }}"
    - export GOOGLE_CLOUD_ACCESS_TOKEN="你的API密钥" + export GEMINI_API_KEY="你的API密钥"
    - export GOOGLE_GENAI_USE_GCA="true" + export GEMINI_MODEL="gemini-2.5-pro"

    @@ -1688,13 +1686,13 @@ >

    # 对于 bash (默认)
    - echo 'export CODE_ASSIST_ENDPOINT="{{ geminiBaseUrl }}"' >> ~/.bashrc + echo 'export GOOGLE_GEMINI_BASE_URL="{{ geminiBaseUrl }}"' >> ~/.bashrc
    - echo 'export GOOGLE_CLOUD_ACCESS_TOKEN="你的API密钥"' >> ~/.bashrc + echo 'export GEMINI_API_KEY="你的API密钥"' >> ~/.bashrc
    - echo 'export GOOGLE_GENAI_USE_GCA="true"' >> ~/.bashrc + echo 'export GEMINI_MODEL="gemini-2.5-pro"' >> ~/.bashrc
    source ~/.bashrc
    @@ -1703,13 +1701,13 @@ >
    # 对于 zsh
    - echo 'export CODE_ASSIST_ENDPOINT="{{ geminiBaseUrl }}"' >> ~/.zshrc + echo 'export GOOGLE_GEMINI_BASE_URL="{{ geminiBaseUrl }}"' >> ~/.zshrc
    - echo 'export GOOGLE_CLOUD_ACCESS_TOKEN="你的API密钥"' >> ~/.zshrc + echo 'export GEMINI_API_KEY="你的API密钥"' >> ~/.zshrc
    - echo 'export GOOGLE_GENAI_USE_GCA="true"' >> ~/.zshrc + echo 'export GEMINI_MODEL="gemini-2.5-pro"' >> ~/.zshrc
    source ~/.zshrc
    @@ -1725,9 +1723,9 @@
    -
    echo $CODE_ASSIST_ENDPOINT
    -
    echo $GOOGLE_CLOUD_ACCESS_TOKEN
    -
    echo $GOOGLE_GENAI_USE_GCA
    +
    echo $GOOGLE_GEMINI_BASE_URL
    +
    echo $GEMINI_API_KEY
    +
    echo $GEMINI_MODEL
    From 86ccb0273eb57fe32f124348ebfb31e9f116fe9c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 14 Oct 2025 06:24:00 +0000 Subject: [PATCH 14/15] chore: sync VERSION file with release v1.1.174 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index ae88434e..32c833ff 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.173 +1.1.174 From b4658e5d5fbfc9085fea9135a1064ad095628829 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 14 Oct 2025 06:34:22 +0000 Subject: [PATCH 15/15] chore: sync VERSION file with release v1.1.175 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 32c833ff..01793f68 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.174 +1.1.175