diff --git a/README.md b/README.md index a7ba59a3..e9f2740f 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,9 @@
-| 平台 | 类型 | 服务 | 介绍 | -|:---|:---|:---|:---| -| **[pincc.ai](https://pincc.ai/)** | 🏆 **官方运营** | ✅ Claude Code
✅ Codex CLI
| 项目直营,提供稳定的 Claude Code / Codex CLI 拼车服务 | -| **[ctok.ai](https://ctok.ai/)** | 🤝 合作伙伴 | ✅ Claude Code
✅ Codex CLI
| 社区认证,提供 Claude Code / Codex CLI 拼车 | +| 平台 | 服务 | 介绍 | +|:---|:---|:---| +| **[pincc.ai](https://pincc.ai/)** | ✅ Claude Code
✅ Codex CLI
| 提供稳定的 Codex CLI 拼车服务

**全新上线 2API 渠道**:接入CC的效果媲美官方 Anthropic Console 账号,暂不支持 Websearch 和 PDF 识别功能(Websearch 后期会支持)
💰 单价:0.8元=1美金额度 |
diff --git a/VERSION b/VERSION index 9fb406cd..fa771f6d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.263 +1.1.267 diff --git a/src/routes/api.js b/src/routes/api.js index 539451d3..4d5647e4 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -377,19 +377,13 @@ async function handleMessagesRequest(req, res) { accountId && accountType === 'claude-official' ) { - // 🚫 检测旧会话(污染的会话) - if (isOldSession(req.body)) { - const cfg = await claudeRelayConfigService.getConfig() - logger.warn( - `🚫 Old session rejected: sessionId=${originalSessionIdForBinding}, messages.length=${req.body?.messages?.length}, tools.length=${req.body?.tools?.length || 0}, isOldSession=true` - ) - return res.status(400).json({ - error: { - type: 'session_binding_error', - message: cfg.sessionBindingErrorMessage || '你的本地session已污染,请清理后使用。' - } - }) - } + // 🆕 允许新 session ID 创建绑定(支持 Claude Code /clear 等场景) + // 信任客户端的 session ID 作为新会话的标识,不再检查请求内容 + logger.info( + `🔗 Creating new session binding: sessionId=${originalSessionIdForBinding}, ` + + `messages.length=${req.body?.messages?.length}, tools.length=${req.body?.tools?.length || 0}, ` + + `accountId=${accountId}, accountType=${accountType}` + ) // 创建绑定 try { @@ -944,19 +938,13 @@ async function handleMessagesRequest(req, res) { accountId && accountType === 'claude-official' ) { - // 🚫 检测旧会话(污染的会话) - if (isOldSession(req.body)) { - const cfg = await claudeRelayConfigService.getConfig() - logger.warn( - `🚫 Old session rejected (non-stream): sessionId=${originalSessionIdForBindingNonStream}, messages.length=${req.body?.messages?.length}, tools.length=${req.body?.tools?.length || 0}, isOldSession=true` - ) - return res.status(400).json({ - error: { - type: 'session_binding_error', - message: cfg.sessionBindingErrorMessage || '你的本地session已污染,请清理后使用。' - } - }) - } + // 🆕 允许新 session ID 创建绑定(支持 Claude Code /clear 等场景) + // 信任客户端的 session ID 作为新会话的标识,不再检查请求内容 + logger.info( + `🔗 Creating new session binding (non-stream): sessionId=${originalSessionIdForBindingNonStream}, ` + + `messages.length=${req.body?.messages?.length}, tools.length=${req.body?.tools?.length || 0}, ` + + `accountId=${accountId}, accountType=${accountType}` + ) // 创建绑定 try { diff --git a/src/routes/openaiRoutes.js b/src/routes/openaiRoutes.js index 3e651496..6610bd98 100644 --- a/src/routes/openaiRoutes.js +++ b/src/routes/openaiRoutes.js @@ -264,8 +264,9 @@ const handleResponses = async (req, res) => { const isStream = req.body?.stream !== false // 默认为流式(兼容现有行为) // 判断是否为 Codex CLI 的请求(基于 User-Agent) + // 支持: codex_vscode, codex_cli_rs, codex_exec (非交互式/脚本模式) const userAgent = req.headers['user-agent'] || '' - const codexCliPattern = /^(codex_vscode|codex_cli_rs)\/[\d.]+/i + const codexCliPattern = /^(codex_vscode|codex_cli_rs|codex_exec)\/[\d.]+/i const isCodexCLI = codexCliPattern.test(userAgent) // 如果不是 Codex CLI 请求,则进行适配 diff --git a/src/services/claudeConsoleAccountService.js b/src/services/claudeConsoleAccountService.js index c0770f34..e6c25c24 100644 --- a/src/services/claudeConsoleAccountService.js +++ b/src/services/claudeConsoleAccountService.js @@ -1295,7 +1295,7 @@ class ClaudeConsoleAccountService { } // 检查是否已经因额度停用(避免重复操作) - if (!accountData.isActive && accountData.quotaStoppedAt) { + if (accountData.quotaStoppedAt) { return } @@ -1311,9 +1311,9 @@ class ClaudeConsoleAccountService { return // 已经被其他进程处理 } - // 超过额度,停用账户 + // 超过额度,停止调度但保持账户状态正常 + // 不修改 isActive 和 status,只用独立字段标记配额超限 const updates = { - isActive: false, quotaStoppedAt: new Date().toISOString(), errorMessage: `Daily quota exceeded: $${currentDailyCost.toFixed(2)} / $${dailyQuota.toFixed(2)}`, schedulable: false, // 停止调度 @@ -1321,13 +1321,6 @@ class ClaudeConsoleAccountService { quotaAutoStopped: 'true' } - // 只有当前状态是active时才改为quota_exceeded - // 如果是rate_limited等其他状态,保持原状态不变 - const currentStatus = await client.hget(accountKey, 'status') - if (currentStatus === 'active') { - updates.status = 'quota_exceeded' - } - await this.updateAccount(accountId, updates) logger.warn( @@ -1371,15 +1364,10 @@ class ClaudeConsoleAccountService { lastResetDate: today } - // 如果账户是因为超额被停用的,恢复账户 - // 注意:状态可能是 quota_exceeded 或 rate_limited(如果429错误时也超额了) - if ( - accountData.quotaStoppedAt && - accountData.isActive === false && - (accountData.status === 'quota_exceeded' || accountData.status === 'rate_limited') - ) { - updates.isActive = true - updates.status = 'active' + // 如果账户因配额超限被停用,恢复账户 + // 新逻辑:不再依赖 isActive === false 和 status 判断 + // 只要有 quotaStoppedAt 就说明是因配额超限被停止的 + if (accountData.quotaStoppedAt) { updates.errorMessage = '' updates.quotaStoppedAt = '' @@ -1389,16 +1377,7 @@ class ClaudeConsoleAccountService { updates.quotaAutoStopped = '' } - // 如果是rate_limited状态,也清除限流相关字段 - if (accountData.status === 'rate_limited') { - const client = redis.getClientSafe() - const accountKey = `${this.ACCOUNT_KEY_PREFIX}${accountId}` - await client.hdel(accountKey, 'rateLimitedAt', 'rateLimitStatus', 'rateLimitAutoStopped') - } - - logger.info( - `✅ Restored account ${accountId} after daily reset (was ${accountData.status})` - ) + logger.info(`✅ Restored account ${accountId} after daily quota reset`) } await this.updateAccount(accountId, updates) diff --git a/src/services/claudeRelayConfigService.js b/src/services/claudeRelayConfigService.js index 49ffe166..8a6f26a3 100644 --- a/src/services/claudeRelayConfigService.js +++ b/src/services/claudeRelayConfigService.js @@ -14,7 +14,7 @@ const DEFAULT_CONFIG = { claudeCodeOnlyEnabled: false, globalSessionBindingEnabled: false, sessionBindingErrorMessage: '你的本地session已污染,请清理后使用。', - sessionBindingTtlDays: 30, // 会话绑定 TTL(天),默认30天 + sessionBindingTtlDays: 1, // 会话绑定 TTL(天),默认1天(支持 /clear 场景,避免 Redis 累积) // 用户消息队列配置 userMessageQueueEnabled: false, // 是否启用用户消息队列(默认关闭) userMessageQueueDelayMs: 200, // 请求间隔(毫秒) diff --git a/src/services/geminiAccountService.js b/src/services/geminiAccountService.js index 250fd684..9850d2b2 100644 --- a/src/services/geminiAccountService.js +++ b/src/services/geminiAccountService.js @@ -1,5 +1,6 @@ const redisClient = require('../models/redis') const { v4: uuidv4 } = require('uuid') +const crypto = require('crypto') const https = require('https') const logger = require('../utils/logger') const { OAuth2Client } = require('google-auth-library') diff --git a/src/services/rateLimitCleanupService.js b/src/services/rateLimitCleanupService.js index 0775b650..d3b90df6 100644 --- a/src/services/rateLimitCleanupService.js +++ b/src/services/rateLimitCleanupService.js @@ -73,6 +73,7 @@ class RateLimitCleanupService { openai: { checked: 0, cleared: 0, errors: [] }, claude: { checked: 0, cleared: 0, errors: [] }, claudeConsole: { checked: 0, cleared: 0, errors: [] }, + quotaExceeded: { checked: 0, cleared: 0, errors: [] }, tokenRefresh: { checked: 0, refreshed: 0, errors: [] } } @@ -85,13 +86,22 @@ class RateLimitCleanupService { // 清理 Claude Console 账号 await this.cleanupClaudeConsoleAccounts(results.claudeConsole) + // 清理 Claude Console 配额超限状态 + await this.cleanupClaudeConsoleQuotaExceeded(results.quotaExceeded) + // 主动刷新等待重置的 Claude 账户 Token(防止 5小时/7天 等待期间 Token 过期) await this.proactiveRefreshClaudeTokens(results.tokenRefresh) const totalChecked = - results.openai.checked + results.claude.checked + results.claudeConsole.checked + results.openai.checked + + results.claude.checked + + results.claudeConsole.checked + + results.quotaExceeded.checked const totalCleared = - results.openai.cleared + results.claude.cleared + results.claudeConsole.cleared + results.openai.cleared + + results.claude.cleared + + results.claudeConsole.cleared + + results.quotaExceeded.cleared const duration = Date.now() - startTime if (totalCleared > 0 || results.tokenRefresh.refreshed > 0) { @@ -103,6 +113,9 @@ class RateLimitCleanupService { logger.info( ` Claude Console: ${results.claudeConsole.cleared}/${results.claudeConsole.checked}` ) + logger.info( + ` Quota Exceeded: ${results.quotaExceeded.cleared}/${results.quotaExceeded.checked}` + ) if (results.tokenRefresh.checked > 0 || results.tokenRefresh.refreshed > 0) { logger.info( ` Token Refresh: ${results.tokenRefresh.refreshed}/${results.tokenRefresh.checked} refreshed` @@ -124,6 +137,7 @@ class RateLimitCleanupService { ...results.openai.errors, ...results.claude.errors, ...results.claudeConsole.errors, + ...results.quotaExceeded.errors, ...results.tokenRefresh.errors ] if (allErrors.length > 0) { @@ -358,6 +372,54 @@ class RateLimitCleanupService { } } + /** + * 检查并恢复 Claude Console 账号的配额超限状态 + */ + async cleanupClaudeConsoleQuotaExceeded(result) { + try { + const accounts = await claudeConsoleAccountService.getAllAccounts() + + for (const account of accounts) { + // 检查是否处于配额超限状态 + if (account.status === 'quota_exceeded' || account.quotaStoppedAt) { + result.checked++ + + try { + // 使用 isAccountQuotaExceeded 方法,它会自动触发恢复 + const isStillExceeded = await claudeConsoleAccountService.isAccountQuotaExceeded( + account.id + ) + + if (!isStillExceeded) { + result.cleared++ + logger.info( + `🧹 Auto-recovered quota exceeded for Claude Console account: ${account.name} (${account.id})` + ) + + // 记录已恢复的账户信息 + this.clearedAccounts.push({ + platform: 'Claude Console', + accountId: account.id, + accountName: account.name, + previousStatus: 'quota_exceeded', + currentStatus: 'active' + }) + } + } catch (error) { + result.errors.push({ + accountId: account.id, + accountName: account.name, + error: error.message + }) + } + } + } + } catch (error) { + logger.error('Failed to cleanup Claude Console quota exceeded accounts:', error) + result.errors.push({ error: error.message }) + } + } + /** * 主动刷新 Claude 账户 Token(防止等待重置期间 Token 过期) * 仅对因限流/配额限制而等待重置的账户执行刷新: diff --git a/src/services/unifiedClaudeScheduler.js b/src/services/unifiedClaudeScheduler.js index 0d39ad68..f56e4e7c 100644 --- a/src/services/unifiedClaudeScheduler.js +++ b/src/services/unifiedClaudeScheduler.js @@ -673,6 +673,23 @@ class UnifiedClaudeScheduler { } } + // 主动检查配额超限状态并尝试恢复(在过滤之前执行,确保可以恢复配额超限的账户) + if (currentAccount.status === 'quota_exceeded') { + // 触发配额检查,如果已到重置时间会自动恢复账户 + const isStillExceeded = await claudeConsoleAccountService.isAccountQuotaExceeded( + currentAccount.id + ) + if (!isStillExceeded) { + // 重新获取账户最新状态 + const refreshedAccount = await claudeConsoleAccountService.getAccount(currentAccount.id) + if (refreshedAccount) { + // 更新当前循环中的账户数据 + currentAccount = refreshedAccount + logger.info(`✅ Account ${currentAccount.name} recovered from quota_exceeded status`) + } + } + } + logger.info( `🔍 Checking Claude Console account: ${currentAccount.name} - isActive: ${currentAccount.isActive}, status: ${currentAccount.status}, accountType: ${currentAccount.accountType}, schedulable: ${currentAccount.schedulable}` ) diff --git a/src/validators/clientDefinitions.js b/src/validators/clientDefinitions.js index 89c3e528..fe2be5f5 100644 --- a/src/validators/clientDefinitions.js +++ b/src/validators/clientDefinitions.js @@ -1,6 +1,10 @@ /** * 客户端定义配置 * 定义所有支持的客户端类型和它们的属性 + * + * allowedPathPrefixes: 允许访问的路径前缀白名单 + * - 当启用客户端限制时,只有匹配白名单的路径才允许访问 + * - 防止通过其他兼容端点(如 /v1/chat/completions)绕过客户端限制 */ const CLIENT_DEFINITIONS = { @@ -9,7 +13,27 @@ const CLIENT_DEFINITIONS = { name: 'Claude Code', displayName: 'Claude Code CLI', description: 'Claude Code command-line interface', - icon: '🤖' + icon: '🤖', + // Claude Code 仅允许访问 Claude 原生端点,禁止访问 OpenAI 兼容端点 + allowedPathPrefixes: [ + '/api/v1/messages', + '/api/v1/models', + '/api/v1/me', + '/api/v1/usage', + '/api/v1/key-info', + '/api/v1/organizations', + '/claude/v1/messages', + '/claude/v1/models', + '/antigravity/api/', + '/gemini-cli/api/', + '/api/event_logging', + '/v1/messages', + '/v1/models', + '/v1/me', + '/v1/usage', + '/v1/key-info', + '/v1/organizations' + ] }, GEMINI_CLI: { @@ -17,7 +41,9 @@ const CLIENT_DEFINITIONS = { name: 'Gemini CLI', displayName: 'Gemini Command Line Tool', description: 'Google Gemini API command-line interface', - icon: '💎' + icon: '💎', + // Gemini CLI 仅允许访问 Gemini 端点 + allowedPathPrefixes: ['/gemini/'] }, CODEX_CLI: { @@ -25,7 +51,9 @@ const CLIENT_DEFINITIONS = { name: 'Codex CLI', displayName: 'Codex Command Line Tool', description: 'Cursor/Codex command-line interface', - icon: '🔷' + icon: '🔷', + // Codex CLI 仅允许访问 OpenAI Responses 和 Azure 端点 + allowedPathPrefixes: ['/openai/responses', '/openai/v1/responses', '/azure/'] }, DROID_CLI: { @@ -33,7 +61,9 @@ const CLIENT_DEFINITIONS = { name: 'Droid CLI', displayName: 'Factory Droid CLI', description: 'Factory Droid platform command-line interface', - icon: '🤖' + icon: '🤖', + // Droid CLI 仅允许访问 Droid 端点 + allowedPathPrefixes: ['/droid/'] } } @@ -60,10 +90,34 @@ function isValidClientId(clientId) { return Object.values(CLIENT_IDS).includes(clientId) } +/** + * 检查路径是否允许指定客户端访问 + * @param {string} clientId - 客户端ID + * @param {string} path - 请求路径 (originalUrl 或 path) + * @returns {boolean} 是否允许 + */ +function isPathAllowedForClient(clientId, path) { + const definition = getClientDefinitionById(clientId) + if (!definition) { + return false + } + + // 如果没有定义 allowedPathPrefixes,则不限制路径(向后兼容) + if (!definition.allowedPathPrefixes || definition.allowedPathPrefixes.length === 0) { + return true + } + + const normalizedPath = (path || '').toLowerCase() + return definition.allowedPathPrefixes.some((prefix) => + normalizedPath.startsWith(prefix.toLowerCase()) + ) +} + module.exports = { CLIENT_DEFINITIONS, CLIENT_IDS, getAllClientDefinitions, getClientDefinitionById, - isValidClientId + isValidClientId, + isPathAllowedForClient } diff --git a/src/validators/clientValidator.js b/src/validators/clientValidator.js index 13cb38eb..29a5e8f3 100644 --- a/src/validators/clientValidator.js +++ b/src/validators/clientValidator.js @@ -4,12 +4,25 @@ */ const logger = require('../utils/logger') -const { CLIENT_DEFINITIONS, getAllClientDefinitions } = require('./clientDefinitions') +const { + CLIENT_IDS, + getAllClientDefinitions, + getClientDefinitionById, + isPathAllowedForClient +} = require('./clientDefinitions') const ClaudeCodeValidator = require('./clients/claudeCodeValidator') const GeminiCliValidator = require('./clients/geminiCliValidator') const CodexCliValidator = require('./clients/codexCliValidator') const DroidCliValidator = require('./clients/droidCliValidator') +// 客户端ID到验证器的映射表 +const VALIDATOR_MAP = { + [CLIENT_IDS.CLAUDE_CODE]: ClaudeCodeValidator, + [CLIENT_IDS.GEMINI_CLI]: GeminiCliValidator, + [CLIENT_IDS.CODEX_CLI]: CodexCliValidator, + [CLIENT_IDS.DROID_CLI]: DroidCliValidator +} + /** * 客户端验证器类 */ @@ -20,19 +33,12 @@ class ClientValidator { * @returns {Object|null} 验证器实例 */ static getValidator(clientId) { - switch (clientId) { - case 'claude_code': - return ClaudeCodeValidator - case 'gemini_cli': - return GeminiCliValidator - case 'codex_cli': - return CodexCliValidator - case 'droid_cli': - return DroidCliValidator - default: - logger.warn(`Unknown client ID: ${clientId}`) - return null + const validator = VALIDATOR_MAP[clientId] + if (!validator) { + logger.warn(`Unknown client ID: ${clientId}`) + return null } + return validator } /** @@ -40,7 +46,7 @@ class ClientValidator { * @returns {Array} 客户端ID列表 */ static getSupportedClients() { - return ['claude_code', 'gemini_cli', 'codex_cli', 'droid_cli'] + return Object.keys(VALIDATOR_MAP) } /** @@ -67,6 +73,7 @@ class ClientValidator { /** * 验证请求是否来自允许的客户端列表中的任一客户端 + * 包含路径白名单检查,防止通过其他兼容端点绕过客户端限制 * @param {Array} allowedClients - 允许的客户端ID列表 * @param {Object} req - Express请求对象 * @returns {Object} 验证结果对象 @@ -74,10 +81,12 @@ class ClientValidator { static validateRequest(allowedClients, req) { const userAgent = req.headers['user-agent'] || '' const clientIP = req.ip || req.connection?.remoteAddress || 'unknown' + const requestPath = req.originalUrl || req.path || '' // 记录验证开始 logger.api(`🔍 Starting client validation for User-Agent: "${userAgent}"`) logger.api(` Allowed clients: ${allowedClients.join(', ')}`) + logger.api(` Request path: ${requestPath}`) logger.api(` Request from IP: ${clientIP}`) // 遍历所有允许的客户端进行验证 @@ -89,6 +98,12 @@ class ClientValidator { continue } + // 路径白名单检查:先检查路径是否允许该客户端访问 + if (!isPathAllowedForClient(clientId, requestPath)) { + logger.debug(`Path "${requestPath}" not allowed for ${validator.getName()}, skipping`) + continue + } + logger.debug(`Checking against ${validator.getName()}...`) try { @@ -96,12 +111,13 @@ class ClientValidator { // 验证成功 logger.api(`✅ Client validated: ${validator.getName()} (${clientId})`) logger.api(` Matched User-Agent: "${userAgent}"`) + logger.api(` Allowed path: "${requestPath}"`) return { allowed: true, matchedClient: clientId, clientName: validator.getName(), - clientInfo: Object.values(CLIENT_DEFINITIONS).find((def) => def.id === clientId) + clientInfo: getClientDefinitionById(clientId) } } } catch (error) { @@ -111,11 +127,15 @@ class ClientValidator { } // 没有匹配的客户端 - logger.api(`❌ No matching client found for User-Agent: "${userAgent}"`) + logger.api( + `❌ No matching client found for User-Agent: "${userAgent}" and path: "${requestPath}"` + ) return { allowed: false, matchedClient: null, - reason: 'No matching client found' + reason: 'No matching client found or path not allowed', + userAgent, + requestPath } } diff --git a/src/validators/clients/codexCliValidator.js b/src/validators/clients/codexCliValidator.js index d8922bd2..a0fae4bd 100644 --- a/src/validators/clients/codexCliValidator.js +++ b/src/validators/clients/codexCliValidator.js @@ -42,7 +42,8 @@ class CodexCliValidator { // Codex CLI 的 UA 格式: // - codex_vscode/0.35.0 (Windows 10.0.26100; x86_64) unknown (Cursor; 0.4.10) // - codex_cli_rs/0.38.0 (Ubuntu 22.4.0; x86_64) WindowsTerminal - const codexCliPattern = /^(codex_vscode|codex_cli_rs)\/[\d.]+/i + // - codex_exec/0.89.0 (Mac OS 26.2.0; arm64) xterm-256color (非交互式/脚本模式) + const codexCliPattern = /^(codex_vscode|codex_cli_rs|codex_exec)\/[\d.]+/i const uaMatch = userAgent.match(codexCliPattern) if (!uaMatch) { diff --git a/web/admin-spa/package-lock.json b/web/admin-spa/package-lock.json index 481df56a..7aa4b2e1 100644 --- a/web/admin-spa/package-lock.json +++ b/web/admin-spa/package-lock.json @@ -3789,7 +3789,7 @@ }, "node_modules/prettier-plugin-tailwindcss": { "version": "0.6.14", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz", + "resolved": "https://registry.npmmirror.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz", "integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==", "dev": true, "license": "MIT", diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue index 7147b3fd..8f91486a 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -4119,12 +4119,24 @@ const getSchedulableReason = (account) => { if (account.status === 'unauthorized') { return 'API Key无效或已过期(401错误)' } + // 检查配额超限状态 + if (account.status === 'quota_exceeded') { + return '余额不足' + } if (account.overloadStatus === 'overloaded') { return '服务过载(529错误)' } if (account.rateLimitStatus === 'limited') { return '触发限流(429错误)' } + // 检查配额超限状态(quotaAutoStopped 或 quotaStoppedAt 任一存在即表示配额超限) + if ( + account.quotaAutoStopped === 'true' || + account.quotaAutoStopped === true || + account.quotaStoppedAt + ) { + return '余额不足' + } if (account.status === 'blocked' && account.errorMessage) { return account.errorMessage } @@ -4203,6 +4215,15 @@ const getSchedulableReason = (account) => { return '手动停止调度' } +// 检查是否是配额超限状态(用于状态显示判断) +const isQuotaExceeded = (account) => { + return ( + account.quotaAutoStopped === 'true' || + account.quotaAutoStopped === true || + !!account.quotaStoppedAt + ) +} + // 获取账户状态文本 const getAccountStatusText = (account) => { // 检查是否被封锁 @@ -4221,9 +4242,9 @@ const getAccountStatusText = (account) => { if (account.status === 'temp_error') return '临时异常' // 检查是否错误 if (account.status === 'error' || !account.isActive) return '错误' - // 检查是否可调度 - if (account.schedulable === false) return '已暂停' - // 否则正常 + // 配额超限时显示"正常"(不显示"已暂停") + if (account.schedulable === false && !isQuotaExceeded(account)) return '已暂停' + // 否则正常(包括配额超限状态) return '正常' } @@ -4249,7 +4270,8 @@ const getAccountStatusClass = (account) => { if (account.status === 'error' || !account.isActive) { return 'bg-red-100 text-red-800' } - if (account.schedulable === false) { + // 配额超限时显示绿色(正常) + if (account.schedulable === false && !isQuotaExceeded(account)) { return 'bg-gray-100 text-gray-800' } return 'bg-green-100 text-green-800' @@ -4277,7 +4299,8 @@ const getAccountStatusDotClass = (account) => { if (account.status === 'error' || !account.isActive) { return 'bg-red-500' } - if (account.schedulable === false) { + // 配额超限时显示绿色(正常) + if (account.schedulable === false && !isQuotaExceeded(account)) { return 'bg-gray-500' } return 'bg-green-500' diff --git a/web/admin-spa/src/views/SettingsView.vue b/web/admin-spa/src/views/SettingsView.vue index f1efa1d5..70fad88c 100644 --- a/web/admin-spa/src/views/SettingsView.vue +++ b/web/admin-spa/src/views/SettingsView.vue @@ -1904,7 +1904,7 @@ const claudeConfig = ref({ claudeCodeOnlyEnabled: false, globalSessionBindingEnabled: false, sessionBindingErrorMessage: '你的本地session已污染,请清理后使用。', - sessionBindingTtlDays: 30, + sessionBindingTtlDays: 1, userMessageQueueEnabled: false, // 与后端默认值保持一致 userMessageQueueDelayMs: 200, userMessageQueueTimeoutMs: 5000, // 与后端默认值保持一致(优化后锁持有时间短无需长等待) @@ -2203,7 +2203,7 @@ const loadClaudeConfig = async () => { globalSessionBindingEnabled: response.config?.globalSessionBindingEnabled ?? false, sessionBindingErrorMessage: response.config?.sessionBindingErrorMessage || '你的本地session已污染,请清理后使用。', - sessionBindingTtlDays: response.config?.sessionBindingTtlDays ?? 30, + sessionBindingTtlDays: response.config?.sessionBindingTtlDays ?? 1, userMessageQueueEnabled: response.config?.userMessageQueueEnabled ?? false, // 与后端默认值保持一致 userMessageQueueDelayMs: response.config?.userMessageQueueDelayMs ?? 200, userMessageQueueTimeoutMs: response.config?.userMessageQueueTimeoutMs ?? 5000, // 与后端默认值保持一致