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, // 与后端默认值保持一致