fix: 修复强制会话绑定首次会话的bug

This commit is contained in:
shaw
2025-12-08 21:04:53 +08:00
parent 698f3d7daa
commit aa71c58400
4 changed files with 118 additions and 28 deletions

View File

@@ -38,6 +38,73 @@ function queueRateLimitUpdate(rateLimitInfo, usageSummary, model, context = '')
})
}
/**
* 判断是否为旧会话(污染的会话)
* Claude Code 发送的请求特点:
* - messages 数组通常只有 1 个元素
* - 历史对话记录嵌套在单个 message 的 content 数组中
* - content 数组中包含 <system-reminder> 开头的系统注入内容
*
* 污染会话的特征:
* 1. messages.length > 1
* 2. messages.length === 1 但 content 中有多个用户输入
* 3. "warmup" 请求:单条简单消息 + 无 tools真正新会话会带 tools
*
* @param {Object} body - 请求体
* @returns {boolean} 是否为旧会话
*/
function isOldSession(body) {
const messages = body?.messages
const tools = body?.tools
if (!messages || messages.length === 0) {
return false
}
// 1. 多条消息 = 旧会话
if (messages.length > 1) {
return true
}
// 2. 单条消息,分析 content
const firstMessage = messages[0]
const content = firstMessage?.content
if (!content) {
return false
}
// 如果 content 是字符串,只有一条输入,需要检查 tools
if (typeof content === 'string') {
// 有 tools = 正常新会话,无 tools = 可疑
return !tools || tools.length === 0
}
// 如果 content 是数组,统计非 system-reminder 的元素
if (Array.isArray(content)) {
const userInputs = content.filter((item) => {
if (item.type !== 'text') {
return false
}
const text = item.text || ''
// 剔除以 <system-reminder> 开头的
return !text.trimStart().startsWith('<system-reminder>')
})
// 多个用户输入 = 旧会话
if (userInputs.length > 1) {
return true
}
// Warmup 检测:单个消息 + 无 tools = 旧会话
if (userInputs.length === 1 && (!tools || tools.length === 0)) {
return true
}
}
return false
}
// 🔧 共享的消息处理函数
async function handleMessagesRequest(req, res) {
try {
@@ -233,19 +300,18 @@ async function handleMessagesRequest(req, res) {
}
// 🔗 在成功调度后建立会话绑定(仅 claude-official 类型)
// claude-official 只接受1) 新会话(messages.length=1) 2) 已绑定的会话
// claude-official 只接受1) 新会话 2) 已绑定的会话
if (
needSessionBinding &&
originalSessionIdForBinding &&
accountId &&
accountType === 'claude-official'
) {
// 🚫 新会话必须 messages.length === 1
const messages = req.body?.messages
if (messages && messages.length > 1) {
// 🚫 检测旧会话(污染的会话)
if (isOldSession(req.body)) {
const cfg = await claudeRelayConfigService.getConfig()
logger.warn(
`🚫 New session with messages.length > 1 rejected: sessionId=${originalSessionIdForBinding}, messages.length=${messages.length}`
`🚫 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: {
@@ -684,19 +750,18 @@ async function handleMessagesRequest(req, res) {
}
// 🔗 在成功调度后建立会话绑定(非流式,仅 claude-official 类型)
// claude-official 只接受1) 新会话(messages.length=1) 2) 已绑定的会话
// claude-official 只接受1) 新会话 2) 已绑定的会话
if (
needSessionBindingNonStream &&
originalSessionIdForBindingNonStream &&
accountId &&
accountType === 'claude-official'
) {
// 🚫 新会话必须 messages.length === 1
const messages = req.body?.messages
if (messages && messages.length > 1) {
// 🚫 检测旧会话(污染的会话)
if (isOldSession(req.body)) {
const cfg = await claudeRelayConfigService.getConfig()
logger.warn(
`🚫 New session with messages.length > 1 rejected (non-stream): sessionId=${originalSessionIdForBindingNonStream}, messages.length=${messages.length}`
`🚫 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: {
@@ -1157,6 +1222,41 @@ router.post('/v1/messages/count_tokens', authenticateApiKey, async (req, res) =>
})
}
// 🔗 会话绑定验证(与 messages 端点保持一致)
const originalSessionId = claudeRelayConfigService.extractOriginalSessionId(req.body)
const sessionValidation = await claudeRelayConfigService.validateNewSession(
req.body,
originalSessionId
)
if (!sessionValidation.valid) {
logger.warn(
`🚫 Session binding validation failed (count_tokens): ${sessionValidation.code} for session ${originalSessionId}`
)
return res.status(400).json({
error: {
type: 'session_binding_error',
message: sessionValidation.error
}
})
}
// 🔗 检测旧会话(污染的会话)- 仅对需要绑定的新会话检查
if (sessionValidation.isNewSession && originalSessionId) {
if (isOldSession(req.body)) {
const cfg = await claudeRelayConfigService.getConfig()
logger.warn(
`🚫 Old session rejected (count_tokens): sessionId=${originalSessionId}, 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已污染请清理后使用。'
}
})
}
}
logger.info(`🔢 Processing token count request for key: ${req.apiKey.name}`)
const sessionHash = sessionHelper.generateSessionHash(req.body)

View File

@@ -283,12 +283,13 @@ class ClaudeRelayConfigService {
const account = await accountService.getAccount(accountId)
if (!account || !account.success || !account.data) {
// getAccount() 直接返回账户数据对象或 null不是 { success, data } 格式
if (!account) {
logger.warn(`Session binding account not found: ${accountId} (${accountType})`)
return false
}
const accountData = account.data
const accountData = account
// 检查账户是否激活
if (accountData.isActive === false || accountData.isActive === 'false') {