From 16e2bcfedbca5fae0ffaec5c6cf5e7b7e41c524a Mon Sep 17 00:00:00 2001 From: enzyme2013 Date: Thu, 22 Jan 2026 17:31:11 +0800 Subject: [PATCH 01/13] fix: allow new session binding after /clear command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 isOldSession 检查,信任客户端的 session ID 作为新会话标识 - 将 sessionBindingTtlDays 默认值从 30 天改为 1 天,避免 Redis 内存累积 - 添加新会话绑定的监控日志(包含 sessionId、messages 数量、accountId 等) - 完美支持 Claude Code /clear 等合法的新会话场景 - 同步更新前端界面的默认值配置 问题背景: 用户在 Claude Code 中执行 /clear 后,会生成新的 session ID, 但旧的逻辑会检查请求内容判定为"旧会话",导致返回"本地session已污染"错误。 修复方案: 采用方案2(放宽新会话检测)+ TTL 优化,信任客户端的 session ID, 不再检查请求内容是否"看起来像旧会话",由 1 天的 TTL 自动清理过期绑定。 影响范围: - src/routes/api.js (流式和非流式两处) - src/services/claudeRelayConfigService.js - web/admin-spa/src/views/SettingsView.vue --- src/routes/api.js | 40 +++++++++--------------- src/services/claudeRelayConfigService.js | 2 +- web/admin-spa/src/views/SettingsView.vue | 4 +-- 3 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/routes/api.js b/src/routes/api.js index 03a97013..2dae7783 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -370,19 +370,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 { @@ -928,19 +922,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/services/claudeRelayConfigService.js b/src/services/claudeRelayConfigService.js index 4fa2b411..e0aa55d8 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/web/admin-spa/src/views/SettingsView.vue b/web/admin-spa/src/views/SettingsView.vue index b9b260a2..4f18edab 100644 --- a/web/admin-spa/src/views/SettingsView.vue +++ b/web/admin-spa/src/views/SettingsView.vue @@ -1676,7 +1676,7 @@ const claudeConfig = ref({ claudeCodeOnlyEnabled: false, globalSessionBindingEnabled: false, sessionBindingErrorMessage: '你的本地session已污染,请清理后使用。', - sessionBindingTtlDays: 30, + sessionBindingTtlDays: 1, userMessageQueueEnabled: false, // 与后端默认值保持一致 userMessageQueueDelayMs: 200, userMessageQueueTimeoutMs: 5000, // 与后端默认值保持一致(优化后锁持有时间短无需长等待) @@ -1952,7 +1952,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, // 与后端默认值保持一致 From 9d701101394a1abd589469bbb2d90131e7c7dc71 Mon Sep 17 00:00:00 2001 From: Junming Chen Date: Thu, 22 Jan 2026 21:32:49 -0500 Subject: [PATCH 02/13] fix: add missing crypto module import in geminiAccountService --- src/services/geminiAccountService.js | 1 + 1 file changed, 1 insertion(+) 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') From 4ed5cc631a31527122c31716c135d40cc9b0c1cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Jan 2026 02:41:20 +0000 Subject: [PATCH 03/13] chore: sync VERSION file with release v1.1.264 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 9fb406cd..48da3448 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.263 +1.1.264 From d812af915976ae739b2cea568d6fc545171f69d8 Mon Sep 17 00:00:00 2001 From: "jett.gao" Date: Fri, 23 Jan 2026 18:19:34 +0800 Subject: [PATCH 04/13] =?UTF-8?q?fix:=20Claude=20Console=20=E9=85=8D?= =?UTF-8?q?=E9=A2=9D=E8=B6=85=E9=99=90=E7=8A=B6=E6=80=81=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E4=B8=BB=E5=8A=A8=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 rateLimitCleanupService 配额超限恢复检查(每5分钟) - 调度器预检查配额超限账户,到达重置时间自动恢复 - 前端显示"余额不足"替代默认的"手动停止调度" Co-Authored-By: Claude Opus 4.5 --- src/services/rateLimitCleanupService.js | 66 +++++++++++++++++++++++- src/services/unifiedClaudeScheduler.js | 17 ++++++ web/admin-spa/src/views/AccountsView.vue | 4 ++ 3 files changed, 85 insertions(+), 2 deletions(-) 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/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue index 7147b3fd..fcfcf94b 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -4119,6 +4119,10 @@ const getSchedulableReason = (account) => { if (account.status === 'unauthorized') { return 'API Key无效或已过期(401错误)' } + // 检查配额超限状态 + if (account.status === 'quota_exceeded') { + return '余额不足' + } if (account.overloadStatus === 'overloaded') { return '服务过载(529错误)' } From 2ac31a5706d3970c9daa34c7b65ee85eaec8cbfd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Jan 2026 11:16:24 +0000 Subject: [PATCH 05/13] chore: sync VERSION file with release v1.1.265 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 48da3448..8bbb9beb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.264 +1.1.265 From d16b75293d7f46e4e9247f614cb1bc955319dda2 Mon Sep 17 00:00:00 2001 From: gaozitian Date: Sat, 24 Jan 2026 12:06:09 +0800 Subject: [PATCH 06/13] fix: optimize Claude Console quota exceeded status display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Keep account status as 'active' when quota exceeded (not 'quota_exceeded') - Keep isActive as true, only use quotaStoppedAt to mark quota exceeded - Show green status in UI for quota exceeded accounts (normal state) - Show '余额不足' as unschedulable reason instead of '已暂停' - Simplify resetDailyUsage() to only check quotaStoppedAt field Co-Authored-By: Claude Opus 4.5 --- src/services/claudeConsoleAccountService.js | 37 +++++---------------- web/admin-spa/package-lock.json | 2 +- web/admin-spa/src/views/AccountsView.vue | 29 +++++++++++++--- 3 files changed, 33 insertions(+), 35 deletions(-) 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/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..b29960fd 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -4125,6 +4125,14 @@ const getSchedulableReason = (account) => { 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 +4211,15 @@ const getSchedulableReason = (account) => { return '手动停止调度' } +// 检查是否是配额超限状态(用于状态显示判断) +const isQuotaExceeded = (account) => { + return ( + account.quotaAutoStopped === 'true' || + account.quotaAutoStopped === true || + !!account.quotaStoppedAt + ) +} + // 获取账户状态文本 const getAccountStatusText = (account) => { // 检查是否被封锁 @@ -4221,9 +4238,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 +4266,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 +4295,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' From 6c4670213ecce53834c802e76f1aa2967727c6f2 Mon Sep 17 00:00:00 2001 From: QTom Date: Sat, 24 Jan 2026 17:35:33 +0800 Subject: [PATCH 07/13] =?UTF-8?q?fix(auth):=20=E4=BF=AE=E5=A4=8D=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E9=99=90=E5=88=B6=E7=BB=95=E8=BF=87=E6=BC=8F?= =?UTF-8?q?=E6=B4=9E=EF=BC=8C=E6=B7=BB=E5=8A=A0=E8=B7=AF=E5=BE=84=E7=99=BD?= =?UTF-8?q?=E5=90=8D=E5=8D=95=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当 API Key 启用客户端限制(如仅允许 Claude Code)时,攻击者可通过 /api/v1/chat/completions 等 OpenAI 兼容端点绕过验证。原因是 ClaudeCodeValidator 对非 messages 路径仅检查 User-Agent。 修复方案: - 为每个客户端类型定义允许的路径白名单 - 在客户端验证前进行路径检查 - 路径不在白名单中则直接拒绝,无需继续验证 修改文件: - src/validators/clientDefinitions.js:添加 allowedPathPrefixes 配置 - src/validators/clientValidator.js:添加路径白名单前置检查 Claude Code 限制时的路由保护: - 允许访问:/api/v1/messages, /claude/v1/messages 等原生端点 - 拒绝访问:/api/v1/chat/completions, /openai/claude/v1/chat/completions 等 - 其他客户端类型(Gemini CLI、Codex CLI、Droid CLI)也同样适用 相关问题:/api/v1/chat/completions 端点在启用 Claude Code 限制后 依然可以使用,深入分析原因并提供修复方案 #security #client-restriction --- src/validators/clientDefinitions.js | 64 ++++++++++++++++++++++++++--- src/validators/clientValidator.js | 24 +++++++++-- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src/validators/clientDefinitions.js b/src/validators/clientDefinitions.js index 89c3e528..0ca9619d 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 = Object.values(CLIENT_DEFINITIONS).find((d) => d.id === 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..1f655c06 100644 --- a/src/validators/clientValidator.js +++ b/src/validators/clientValidator.js @@ -4,7 +4,11 @@ */ const logger = require('../utils/logger') -const { CLIENT_DEFINITIONS, getAllClientDefinitions } = require('./clientDefinitions') +const { + CLIENT_DEFINITIONS, + getAllClientDefinitions, + isPathAllowedForClient +} = require('./clientDefinitions') const ClaudeCodeValidator = require('./clients/claudeCodeValidator') const GeminiCliValidator = require('./clients/geminiCliValidator') const CodexCliValidator = require('./clients/codexCliValidator') @@ -67,6 +71,7 @@ class ClientValidator { /** * 验证请求是否来自允许的客户端列表中的任一客户端 + * 包含路径白名单检查,防止通过其他兼容端点绕过客户端限制 * @param {Array} allowedClients - 允许的客户端ID列表 * @param {Object} req - Express请求对象 * @returns {Object} 验证结果对象 @@ -74,10 +79,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 +96,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,6 +109,7 @@ class ClientValidator { // 验证成功 logger.api(`✅ Client validated: ${validator.getName()} (${clientId})`) logger.api(` Matched User-Agent: "${userAgent}"`) + logger.api(` Allowed path: "${requestPath}"`) return { allowed: true, @@ -111,11 +125,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 } } From 6dc85b39c96f7e98d479728cd8ecbe0fcd20ce6c Mon Sep 17 00:00:00 2001 From: QTom Date: Sat, 24 Jan 2026 17:44:10 +0800 Subject: [PATCH 08/13] =?UTF-8?q?refactor(validators):=20=E6=B6=88?= =?UTF-8?q?=E9=99=A4=E9=87=8D=E5=A4=8D=E4=BB=A3=E7=A0=81=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E6=98=A0=E5=B0=84=E8=A1=A8=E5=92=8C=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 代码审查后的重构: - isPathAllowedForClient 复用 getClientDefinitionById 避免重复查找 - validateRequest 中使用 getClientDefinitionById 替代内联查找 - 使用 VALIDATOR_MAP 映射表替代 switch 语句 - getSupportedClients 改为从映射表动态获取,避免硬编码 - 导入 CLIENT_IDS 枚举,提高类型安全性 这些改动提高了代码的可维护性,添加新客户端时只需修改映射表。 --- src/validators/clientDefinitions.js | 2 +- src/validators/clientValidator.js | 32 +++++++++++++++-------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/validators/clientDefinitions.js b/src/validators/clientDefinitions.js index 0ca9619d..fe2be5f5 100644 --- a/src/validators/clientDefinitions.js +++ b/src/validators/clientDefinitions.js @@ -97,7 +97,7 @@ function isValidClientId(clientId) { * @returns {boolean} 是否允许 */ function isPathAllowedForClient(clientId, path) { - const definition = Object.values(CLIENT_DEFINITIONS).find((d) => d.id === clientId) + const definition = getClientDefinitionById(clientId) if (!definition) { return false } diff --git a/src/validators/clientValidator.js b/src/validators/clientValidator.js index 1f655c06..29a5e8f3 100644 --- a/src/validators/clientValidator.js +++ b/src/validators/clientValidator.js @@ -5,8 +5,9 @@ const logger = require('../utils/logger') const { - CLIENT_DEFINITIONS, + CLIENT_IDS, getAllClientDefinitions, + getClientDefinitionById, isPathAllowedForClient } = require('./clientDefinitions') const ClaudeCodeValidator = require('./clients/claudeCodeValidator') @@ -14,6 +15,14 @@ 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 +} + /** * 客户端验证器类 */ @@ -24,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 } /** @@ -44,7 +46,7 @@ class ClientValidator { * @returns {Array} 客户端ID列表 */ static getSupportedClients() { - return ['claude_code', 'gemini_cli', 'codex_cli', 'droid_cli'] + return Object.keys(VALIDATOR_MAP) } /** @@ -115,7 +117,7 @@ class ClientValidator { allowed: true, matchedClient: clientId, clientName: validator.getName(), - clientInfo: Object.values(CLIENT_DEFINITIONS).find((def) => def.id === clientId) + clientInfo: getClientDefinitionById(clientId) } } } catch (error) { From c97bfb6478c5d5d54c1a22052efe5321c9a46e17 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 24 Jan 2026 12:22:09 +0000 Subject: [PATCH 09/13] chore: sync VERSION file with release v1.1.266 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 8bbb9beb..b380e6e0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.265 +1.1.266 From 816c47b51d17ee41249bc1d3e028c65d7c6c664b Mon Sep 17 00:00:00 2001 From: QTom Date: Sun, 25 Jan 2026 12:23:40 +0800 Subject: [PATCH 10/13] =?UTF-8?q?feat(codex):=20=E6=B7=BB=E5=8A=A0=20codex?= =?UTF-8?q?=5Fexec=20=E7=94=A8=E6=88=B7=E4=BB=A3=E7=90=86=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持 Codex CLI 的非交互式/脚本模式(codex exec),使其与 codex_vscode 和 codex_cli_rs 共享相同的验证逻辑和权限配置。修复 codex exec 0.89.0 版本因客户端限制导致的 403 错误。 --- src/routes/openaiRoutes.js | 3 ++- src/validators/clients/codexCliValidator.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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/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) { From 03dfedc3d97b5c00a4c710e214ef89619fa6d6b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 25 Jan 2026 05:12:51 +0000 Subject: [PATCH 11/13] chore: sync VERSION file with release v1.1.267 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index b380e6e0..fa771f6d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.266 +1.1.267 From 56eb7c3c7dc3fdf40286bce4646c55397e6bcf94 Mon Sep 17 00:00:00 2001 From: shaw Date: Tue, 27 Jan 2026 14:46:00 +0800 Subject: [PATCH 12/13] docs: update readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index a7ba59a3..30609772 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,7 @@ | 平台 | 类型 | 服务 | 介绍 | |:---|:---|:---|:---| -| **[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美金额度 | From 2c2039d1a1b3a8bb69fc67403e4d5dcb780ff206 Mon Sep 17 00:00:00 2001 From: shaw Date: Tue, 27 Jan 2026 14:48:52 +0800 Subject: [PATCH 13/13] docs: update readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 30609772..e9f2740f 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,9 @@
-| 平台 | 类型 | 服务 | 介绍 | -|:---|:---|:---|:---| -| **[pincc.ai](https://pincc.ai/)** | 🏆 **官方运营** | ✅ Claude Code
✅ Codex CLI
| 项目直营,提供稳定的 Codex CLI 拼车服务

🆕 **全新上线 2API 渠道**:接入CC的效果媲美官方 Anthropic Console 账号,暂不支持 Websearch 和 PDF 识别功能(Websearch 后期会支持)
💰 单价:0.8元=1美金额度 | +| 平台 | 服务 | 介绍 | +|:---|:---|:---| +| **[pincc.ai](https://pincc.ai/)** | ✅ Claude Code
✅ Codex CLI
| 提供稳定的 Codex CLI 拼车服务

**全新上线 2API 渠道**:接入CC的效果媲美官方 Anthropic Console 账号,暂不支持 Websearch 和 PDF 识别功能(Websearch 后期会支持)
💰 单价:0.8元=1美金额度 |