Compare commits

...

8 Commits

Author SHA1 Message Date
github-actions[bot]
2ac31a5706 chore: sync VERSION file with release v1.1.265 [skip ci] 2026-01-23 11:16:24 +00:00
Wesley Liddick
a3a922ac09 Merge pull request #919 from arksou/hotfix/claude-console-quota-exceeded-recovery
fix: Claude Console 配额超限状态优化,支持主动自动恢复
2026-01-23 19:16:10 +08:00
Wesley Liddick
0073d40299 Merge pull request #916 from enzyme2013/fix/allow-new-session-after-clear [skip ci]
fix: allow new session binding after /clear command
2026-01-23 19:15:59 +08:00
jett.gao
d812af9159 fix: Claude Console 配额超限状态优化,支持主动自动恢复
- 新增 rateLimitCleanupService 配额超限恢复检查(每5分钟)
- 调度器预检查配额超限账户,到达重置时间自动恢复
- 前端显示"余额不足"替代默认的"手动停止调度"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 18:19:34 +08:00
github-actions[bot]
4ed5cc631a chore: sync VERSION file with release v1.1.264 [skip ci] 2026-01-23 02:41:20 +00:00
Wesley Liddick
4019b043ec Merge pull request #918 from Chapoly1305/fix/crypto-import
fix: add missing crypto module import in geminiAccountService
2026-01-23 10:41:04 +08:00
Junming Chen
9d70110139 fix: add missing crypto module import in geminiAccountService 2026-01-22 21:32:49 -05:00
enzyme2013
16e2bcfedb fix: allow new session binding after /clear command
- 移除 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
2026-01-22 17:31:11 +08:00
8 changed files with 104 additions and 32 deletions

View File

@@ -1 +1 @@
1.1.263 1.1.265

View File

@@ -377,19 +377,13 @@ async function handleMessagesRequest(req, res) {
accountId && accountId &&
accountType === 'claude-official' accountType === 'claude-official'
) { ) {
// 🚫 检测旧会话(污染的会话 // 🆕 允许新 session ID 创建绑定(支持 Claude Code /clear 等场景
if (isOldSession(req.body)) { // 信任客户端的 session ID 作为新会话的标识,不再检查请求内容
const cfg = await claudeRelayConfigService.getConfig() logger.info(
logger.warn( `🔗 Creating new session binding: sessionId=${originalSessionIdForBinding}, ` +
`🚫 Old session rejected: sessionId=${originalSessionIdForBinding}, messages.length=${req.body?.messages?.length}, tools.length=${req.body?.tools?.length || 0}, isOldSession=true` `messages.length=${req.body?.messages?.length}, tools.length=${req.body?.tools?.length || 0}, ` +
) `accountId=${accountId}, accountType=${accountType}`
return res.status(400).json({ )
error: {
type: 'session_binding_error',
message: cfg.sessionBindingErrorMessage || '你的本地session已污染请清理后使用。'
}
})
}
// 创建绑定 // 创建绑定
try { try {
@@ -944,19 +938,13 @@ async function handleMessagesRequest(req, res) {
accountId && accountId &&
accountType === 'claude-official' accountType === 'claude-official'
) { ) {
// 🚫 检测旧会话(污染的会话 // 🆕 允许新 session ID 创建绑定(支持 Claude Code /clear 等场景
if (isOldSession(req.body)) { // 信任客户端的 session ID 作为新会话的标识,不再检查请求内容
const cfg = await claudeRelayConfigService.getConfig() logger.info(
logger.warn( `🔗 Creating new session binding (non-stream): sessionId=${originalSessionIdForBindingNonStream}, ` +
`🚫 Old session rejected (non-stream): sessionId=${originalSessionIdForBindingNonStream}, messages.length=${req.body?.messages?.length}, tools.length=${req.body?.tools?.length || 0}, isOldSession=true` `messages.length=${req.body?.messages?.length}, tools.length=${req.body?.tools?.length || 0}, ` +
) `accountId=${accountId}, accountType=${accountType}`
return res.status(400).json({ )
error: {
type: 'session_binding_error',
message: cfg.sessionBindingErrorMessage || '你的本地session已污染请清理后使用。'
}
})
}
// 创建绑定 // 创建绑定
try { try {

View File

@@ -14,7 +14,7 @@ const DEFAULT_CONFIG = {
claudeCodeOnlyEnabled: false, claudeCodeOnlyEnabled: false,
globalSessionBindingEnabled: false, globalSessionBindingEnabled: false,
sessionBindingErrorMessage: '你的本地session已污染请清理后使用。', sessionBindingErrorMessage: '你的本地session已污染请清理后使用。',
sessionBindingTtlDays: 30, // 会话绑定 TTL默认30天 sessionBindingTtlDays: 1, // 会话绑定 TTL默认1天支持 /clear 场景,避免 Redis 累积)
// 用户消息队列配置 // 用户消息队列配置
userMessageQueueEnabled: false, // 是否启用用户消息队列(默认关闭) userMessageQueueEnabled: false, // 是否启用用户消息队列(默认关闭)
userMessageQueueDelayMs: 200, // 请求间隔(毫秒) userMessageQueueDelayMs: 200, // 请求间隔(毫秒)

View File

@@ -1,5 +1,6 @@
const redisClient = require('../models/redis') const redisClient = require('../models/redis')
const { v4: uuidv4 } = require('uuid') const { v4: uuidv4 } = require('uuid')
const crypto = require('crypto')
const https = require('https') const https = require('https')
const logger = require('../utils/logger') const logger = require('../utils/logger')
const { OAuth2Client } = require('google-auth-library') const { OAuth2Client } = require('google-auth-library')

View File

@@ -73,6 +73,7 @@ class RateLimitCleanupService {
openai: { checked: 0, cleared: 0, errors: [] }, openai: { checked: 0, cleared: 0, errors: [] },
claude: { checked: 0, cleared: 0, errors: [] }, claude: { checked: 0, cleared: 0, errors: [] },
claudeConsole: { checked: 0, cleared: 0, errors: [] }, claudeConsole: { checked: 0, cleared: 0, errors: [] },
quotaExceeded: { checked: 0, cleared: 0, errors: [] },
tokenRefresh: { checked: 0, refreshed: 0, errors: [] } tokenRefresh: { checked: 0, refreshed: 0, errors: [] }
} }
@@ -85,13 +86,22 @@ class RateLimitCleanupService {
// 清理 Claude Console 账号 // 清理 Claude Console 账号
await this.cleanupClaudeConsoleAccounts(results.claudeConsole) await this.cleanupClaudeConsoleAccounts(results.claudeConsole)
// 清理 Claude Console 配额超限状态
await this.cleanupClaudeConsoleQuotaExceeded(results.quotaExceeded)
// 主动刷新等待重置的 Claude 账户 Token防止 5小时/7天 等待期间 Token 过期) // 主动刷新等待重置的 Claude 账户 Token防止 5小时/7天 等待期间 Token 过期)
await this.proactiveRefreshClaudeTokens(results.tokenRefresh) await this.proactiveRefreshClaudeTokens(results.tokenRefresh)
const totalChecked = 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 = 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 const duration = Date.now() - startTime
if (totalCleared > 0 || results.tokenRefresh.refreshed > 0) { if (totalCleared > 0 || results.tokenRefresh.refreshed > 0) {
@@ -103,6 +113,9 @@ class RateLimitCleanupService {
logger.info( logger.info(
` Claude Console: ${results.claudeConsole.cleared}/${results.claudeConsole.checked}` ` 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) { if (results.tokenRefresh.checked > 0 || results.tokenRefresh.refreshed > 0) {
logger.info( logger.info(
` Token Refresh: ${results.tokenRefresh.refreshed}/${results.tokenRefresh.checked} refreshed` ` Token Refresh: ${results.tokenRefresh.refreshed}/${results.tokenRefresh.checked} refreshed`
@@ -124,6 +137,7 @@ class RateLimitCleanupService {
...results.openai.errors, ...results.openai.errors,
...results.claude.errors, ...results.claude.errors,
...results.claudeConsole.errors, ...results.claudeConsole.errors,
...results.quotaExceeded.errors,
...results.tokenRefresh.errors ...results.tokenRefresh.errors
] ]
if (allErrors.length > 0) { 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 过期) * 主动刷新 Claude 账户 Token防止等待重置期间 Token 过期)
* 仅对因限流/配额限制而等待重置的账户执行刷新: * 仅对因限流/配额限制而等待重置的账户执行刷新:

View File

@@ -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( logger.info(
`🔍 Checking Claude Console account: ${currentAccount.name} - isActive: ${currentAccount.isActive}, status: ${currentAccount.status}, accountType: ${currentAccount.accountType}, schedulable: ${currentAccount.schedulable}` `🔍 Checking Claude Console account: ${currentAccount.name} - isActive: ${currentAccount.isActive}, status: ${currentAccount.status}, accountType: ${currentAccount.accountType}, schedulable: ${currentAccount.schedulable}`
) )

View File

@@ -4119,6 +4119,10 @@ const getSchedulableReason = (account) => {
if (account.status === 'unauthorized') { if (account.status === 'unauthorized') {
return 'API Key无效或已过期401错误' return 'API Key无效或已过期401错误'
} }
// 检查配额超限状态
if (account.status === 'quota_exceeded') {
return '余额不足'
}
if (account.overloadStatus === 'overloaded') { if (account.overloadStatus === 'overloaded') {
return '服务过载529错误' return '服务过载529错误'
} }

View File

@@ -1904,7 +1904,7 @@ const claudeConfig = ref({
claudeCodeOnlyEnabled: false, claudeCodeOnlyEnabled: false,
globalSessionBindingEnabled: false, globalSessionBindingEnabled: false,
sessionBindingErrorMessage: '你的本地session已污染请清理后使用。', sessionBindingErrorMessage: '你的本地session已污染请清理后使用。',
sessionBindingTtlDays: 30, sessionBindingTtlDays: 1,
userMessageQueueEnabled: false, // 与后端默认值保持一致 userMessageQueueEnabled: false, // 与后端默认值保持一致
userMessageQueueDelayMs: 200, userMessageQueueDelayMs: 200,
userMessageQueueTimeoutMs: 5000, // 与后端默认值保持一致(优化后锁持有时间短无需长等待) userMessageQueueTimeoutMs: 5000, // 与后端默认值保持一致(优化后锁持有时间短无需长等待)
@@ -2203,7 +2203,7 @@ const loadClaudeConfig = async () => {
globalSessionBindingEnabled: response.config?.globalSessionBindingEnabled ?? false, globalSessionBindingEnabled: response.config?.globalSessionBindingEnabled ?? false,
sessionBindingErrorMessage: sessionBindingErrorMessage:
response.config?.sessionBindingErrorMessage || '你的本地session已污染请清理后使用。', response.config?.sessionBindingErrorMessage || '你的本地session已污染请清理后使用。',
sessionBindingTtlDays: response.config?.sessionBindingTtlDays ?? 30, sessionBindingTtlDays: response.config?.sessionBindingTtlDays ?? 1,
userMessageQueueEnabled: response.config?.userMessageQueueEnabled ?? false, // 与后端默认值保持一致 userMessageQueueEnabled: response.config?.userMessageQueueEnabled ?? false, // 与后端默认值保持一致
userMessageQueueDelayMs: response.config?.userMessageQueueDelayMs ?? 200, userMessageQueueDelayMs: response.config?.userMessageQueueDelayMs ?? 200,
userMessageQueueTimeoutMs: response.config?.userMessageQueueTimeoutMs ?? 5000, // 与后端默认值保持一致 userMessageQueueTimeoutMs: response.config?.userMessageQueueTimeoutMs ?? 5000, // 与后端默认值保持一致