chore: opus周限提示增加重置时间

This commit is contained in:
shaw
2025-10-04 11:10:55 +08:00
parent d44582dc31
commit cd72a29674

View File

@@ -11,6 +11,7 @@ const config = require('../../config/config')
const claudeCodeHeadersService = require('./claudeCodeHeadersService') const claudeCodeHeadersService = require('./claudeCodeHeadersService')
const redis = require('../models/redis') const redis = require('../models/redis')
const ClaudeCodeValidator = require('../validators/clients/claudeCodeValidator') const ClaudeCodeValidator = require('../validators/clients/claudeCodeValidator')
const { formatDateWithTimezone } = require('../utils/dateHelper')
class ClaudeRelayService { class ClaudeRelayService {
constructor() { constructor() {
@@ -21,6 +22,14 @@ class ClaudeRelayService {
this.claudeCodeSystemPrompt = "You are Claude Code, Anthropic's official CLI for Claude." this.claudeCodeSystemPrompt = "You are Claude Code, Anthropic's official CLI for Claude."
} }
_buildOpusLimitMessage(resetTime) {
if (!resetTime) {
return '此专属账号的Opus模型已达到周使用限制请尝试切换其他模型后再试。'
}
const formattedReset = formatDateWithTimezone(resetTime)
return `此专属账号的Opus模型已达到周使用限制将于 ${formattedReset} 自动恢复,请尝试切换其他模型后再试。`
}
// 🔍 判断是否是真实的 Claude Code 请求 // 🔍 判断是否是真实的 Claude Code 请求
isRealClaudeCodeRequest(requestBody, clientHeaders) { isRealClaudeCodeRequest(requestBody, clientHeaders) {
// 使用 claudeCodeValidator 来进行完整的验证 // 使用 claudeCodeValidator 来进行完整的验证
@@ -83,6 +92,7 @@ class ClaudeRelayService {
} }
const isDedicatedOfficialAccount = const isDedicatedOfficialAccount =
accountType === 'claude-official' &&
apiKeyData.claudeAccountId && apiKeyData.claudeAccountId &&
!apiKeyData.claudeAccountId.startsWith('group:') && !apiKeyData.claudeAccountId.startsWith('group:') &&
apiKeyData.claudeAccountId === accountId apiKeyData.claudeAccountId === accountId
@@ -95,6 +105,7 @@ class ClaudeRelayService {
} }
if (isOpusModelRequest && isDedicatedOfficialAccount && opusRateLimitActive) { if (isOpusModelRequest && isDedicatedOfficialAccount && opusRateLimitActive) {
const limitMessage = this._buildOpusLimitMessage(opusRateLimitEndAt)
logger.warn( logger.warn(
`🚫 Dedicated account ${account?.name || accountId} is under Opus weekly limit until ${opusRateLimitEndAt}` `🚫 Dedicated account ${account?.name || accountId} is under Opus weekly limit until ${opusRateLimitEndAt}`
) )
@@ -103,7 +114,7 @@ class ClaudeRelayService {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
error: 'opus_weekly_limit', error: 'opus_weekly_limit',
message: '此专属账号的Opus模型已达到本周使用限制请尝试切换其他模型后再试。' message: limitMessage
}), }),
accountId accountId
} }
@@ -226,6 +237,19 @@ class ClaudeRelayService {
logger.warn( logger.warn(
`🚫 Account ${accountId} hit Opus limit, resets at ${new Date(parsedResetTimestamp * 1000).toISOString()}` `🚫 Account ${accountId} hit Opus limit, resets at ${new Date(parsedResetTimestamp * 1000).toISOString()}`
) )
if (isDedicatedOfficialAccount) {
const limitMessage = this._buildOpusLimitMessage(parsedResetTimestamp)
return {
statusCode: 403,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'opus_weekly_limit',
message: limitMessage
}),
accountId
}
}
} else { } else {
isRateLimited = true isRateLimited = true
if (!Number.isNaN(parsedResetTimestamp)) { if (!Number.isNaN(parsedResetTimestamp)) {
@@ -883,6 +907,7 @@ class ClaudeRelayService {
} }
const isDedicatedOfficialAccount = const isDedicatedOfficialAccount =
accountType === 'claude-official' &&
apiKeyData.claudeAccountId && apiKeyData.claudeAccountId &&
!apiKeyData.claudeAccountId.startsWith('group:') && !apiKeyData.claudeAccountId.startsWith('group:') &&
apiKeyData.claudeAccountId === accountId apiKeyData.claudeAccountId === accountId
@@ -893,6 +918,7 @@ class ClaudeRelayService {
} }
if (isOpusModelRequest && isDedicatedOfficialAccount && opusRateLimitActive) { if (isOpusModelRequest && isDedicatedOfficialAccount && opusRateLimitActive) {
const limitMessage = this._buildOpusLimitMessage(account?.opusRateLimitEndAt)
if (!responseStream.headersSent) { if (!responseStream.headersSent) {
responseStream.status(403) responseStream.status(403)
responseStream.setHeader('Content-Type', 'application/json') responseStream.setHeader('Content-Type', 'application/json')
@@ -900,7 +926,7 @@ class ClaudeRelayService {
responseStream.write( responseStream.write(
JSON.stringify({ JSON.stringify({
error: 'opus_weekly_limit', error: 'opus_weekly_limit',
message: '此专属账号的Opus模型已达到本周使用限制请尝试切换其他模型后再试。' message: limitMessage
}) })
) )
responseStream.end() responseStream.end()
@@ -931,7 +957,8 @@ class ClaudeRelayService {
accountType, accountType,
sessionHash, sessionHash,
streamTransformer, streamTransformer,
options options,
isDedicatedOfficialAccount
) )
} catch (error) { } catch (error) {
logger.error(`❌ Claude stream relay with usage capture failed:`, error) logger.error(`❌ Claude stream relay with usage capture failed:`, error)
@@ -951,7 +978,8 @@ class ClaudeRelayService {
accountType, accountType,
sessionHash, sessionHash,
streamTransformer = null, streamTransformer = null,
requestOptions = {} requestOptions = {},
isDedicatedOfficialAccount = false
) { ) {
// 获取账户信息用于统一 User-Agent // 获取账户信息用于统一 User-Agent
const account = await claudeAccountService.getAccount(accountId) const account = await claudeAccountService.getAccount(accountId)
@@ -1016,11 +1044,43 @@ class ClaudeRelayService {
options.headers['anthropic-beta'] = betaHeader options.headers['anthropic-beta'] = betaHeader
} }
const req = https.request(options, (res) => { const req = https.request(options, async (res) => {
logger.debug(`🌊 Claude stream response status: ${res.statusCode}`) logger.debug(`🌊 Claude stream response status: ${res.statusCode}`)
// 错误响应处理 // 错误响应处理
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
if (res.statusCode === 429 && isOpusModelRequest) {
const resetHeader = res.headers
? res.headers['anthropic-ratelimit-unified-reset']
: null
const parsedResetTimestamp = resetHeader ? parseInt(resetHeader, 10) : NaN
if (!Number.isNaN(parsedResetTimestamp)) {
await claudeAccountService.markAccountOpusRateLimited(accountId, parsedResetTimestamp)
logger.warn(
`🚫 [Stream] Account ${accountId} hit Opus limit, resets at ${new Date(parsedResetTimestamp * 1000).toISOString()}`
)
}
if (isDedicatedOfficialAccount) {
const limitMessage = this._buildOpusLimitMessage(parsedResetTimestamp)
if (!responseStream.headersSent) {
responseStream.status(403)
responseStream.setHeader('Content-Type', 'application/json')
}
responseStream.write(
JSON.stringify({
error: 'opus_weekly_limit',
message: limitMessage
})
)
responseStream.end()
res.resume()
resolve()
return
}
}
// 将错误处理逻辑封装在一个异步函数中 // 将错误处理逻辑封装在一个异步函数中
const handleErrorResponse = async () => { const handleErrorResponse = async () => {
if (res.statusCode === 401) { if (res.statusCode === 401) {