mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix: 修复强制会话绑定首次会话的bug
This commit is contained in:
@@ -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) {
|
async function handleMessagesRequest(req, res) {
|
||||||
try {
|
try {
|
||||||
@@ -233,19 +300,18 @@ async function handleMessagesRequest(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔗 在成功调度后建立会话绑定(仅 claude-official 类型)
|
// 🔗 在成功调度后建立会话绑定(仅 claude-official 类型)
|
||||||
// claude-official 只接受:1) 新会话(messages.length=1) 2) 已绑定的会话
|
// claude-official 只接受:1) 新会话 2) 已绑定的会话
|
||||||
if (
|
if (
|
||||||
needSessionBinding &&
|
needSessionBinding &&
|
||||||
originalSessionIdForBinding &&
|
originalSessionIdForBinding &&
|
||||||
accountId &&
|
accountId &&
|
||||||
accountType === 'claude-official'
|
accountType === 'claude-official'
|
||||||
) {
|
) {
|
||||||
// 🚫 新会话必须 messages.length === 1
|
// 🚫 检测旧会话(污染的会话)
|
||||||
const messages = req.body?.messages
|
if (isOldSession(req.body)) {
|
||||||
if (messages && messages.length > 1) {
|
|
||||||
const cfg = await claudeRelayConfigService.getConfig()
|
const cfg = await claudeRelayConfigService.getConfig()
|
||||||
logger.warn(
|
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({
|
return res.status(400).json({
|
||||||
error: {
|
error: {
|
||||||
@@ -684,19 +750,18 @@ async function handleMessagesRequest(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔗 在成功调度后建立会话绑定(非流式,仅 claude-official 类型)
|
// 🔗 在成功调度后建立会话绑定(非流式,仅 claude-official 类型)
|
||||||
// claude-official 只接受:1) 新会话(messages.length=1) 2) 已绑定的会话
|
// claude-official 只接受:1) 新会话 2) 已绑定的会话
|
||||||
if (
|
if (
|
||||||
needSessionBindingNonStream &&
|
needSessionBindingNonStream &&
|
||||||
originalSessionIdForBindingNonStream &&
|
originalSessionIdForBindingNonStream &&
|
||||||
accountId &&
|
accountId &&
|
||||||
accountType === 'claude-official'
|
accountType === 'claude-official'
|
||||||
) {
|
) {
|
||||||
// 🚫 新会话必须 messages.length === 1
|
// 🚫 检测旧会话(污染的会话)
|
||||||
const messages = req.body?.messages
|
if (isOldSession(req.body)) {
|
||||||
if (messages && messages.length > 1) {
|
|
||||||
const cfg = await claudeRelayConfigService.getConfig()
|
const cfg = await claudeRelayConfigService.getConfig()
|
||||||
logger.warn(
|
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({
|
return res.status(400).json({
|
||||||
error: {
|
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}`)
|
logger.info(`🔢 Processing token count request for key: ${req.apiKey.name}`)
|
||||||
|
|
||||||
const sessionHash = sessionHelper.generateSessionHash(req.body)
|
const sessionHash = sessionHelper.generateSessionHash(req.body)
|
||||||
|
|||||||
@@ -283,12 +283,13 @@ class ClaudeRelayConfigService {
|
|||||||
|
|
||||||
const account = await accountService.getAccount(accountId)
|
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})`)
|
logger.warn(`Session binding account not found: ${accountId} (${accountType})`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const accountData = account.data
|
const accountData = account
|
||||||
|
|
||||||
// 检查账户是否激活
|
// 检查账户是否激活
|
||||||
if (accountData.isActive === false || accountData.isActive === 'false') {
|
if (accountData.isActive === false || accountData.isActive === 'false') {
|
||||||
|
|||||||
15
web/admin-spa/package-lock.json
generated
15
web/admin-spa/package-lock.json
generated
@@ -1157,7 +1157,6 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/lodash": "*"
|
"@types/lodash": "*"
|
||||||
}
|
}
|
||||||
@@ -1352,7 +1351,6 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -1589,7 +1587,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001726",
|
"caniuse-lite": "^1.0.30001726",
|
||||||
"electron-to-chromium": "^1.5.173",
|
"electron-to-chromium": "^1.5.173",
|
||||||
@@ -3063,15 +3060,13 @@
|
|||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash-es": {
|
"node_modules/lodash-es": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
|
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash-unified": {
|
"node_modules/lodash-unified": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
@@ -3623,7 +3618,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -3770,7 +3764,6 @@
|
|||||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@@ -4035,7 +4028,6 @@
|
|||||||
"integrity": "sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ==",
|
"integrity": "sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@@ -4533,7 +4525,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -4924,7 +4915,6 @@
|
|||||||
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
@@ -5125,7 +5115,6 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.18.tgz",
|
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.18.tgz",
|
||||||
"integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==",
|
"integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.18",
|
"@vue/compiler-dom": "3.5.18",
|
||||||
"@vue/compiler-sfc": "3.5.18",
|
"@vue/compiler-sfc": "3.5.18",
|
||||||
|
|||||||
@@ -720,7 +720,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200">
|
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200">
|
||||||
全局会话绑定
|
强制会话绑定
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||||
启用后,系统会将原始会话 ID 绑定到首次使用的账户,确保上下文的一致性
|
启用后,系统会将原始会话 ID 绑定到首次使用的账户,确保上下文的一致性
|
||||||
@@ -777,7 +777,7 @@
|
|||||||
@change="saveClaudeConfig"
|
@change="saveClaudeConfig"
|
||||||
></textarea>
|
></textarea>
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
当绑定的账户不可用(状态异常、过载等)时,返回给客户端的错误消息
|
当检测到为旧的sessionId且未在系统中有调度记录时提示,返回给客户端的错误消息
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -794,7 +794,7 @@
|
|||||||
的请求将自动路由到同一账户。
|
的请求将自动路由到同一账户。
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-sm text-purple-700 dark:text-purple-300">
|
<p class="mt-2 text-sm text-purple-700 dark:text-purple-300">
|
||||||
<strong>新会话识别:</strong>如果是已存在的绑定会话但请求中
|
<strong>新会话识别:</strong>如果绑定会话历史中没有该sessionId但请求中
|
||||||
<code class="rounded bg-purple-100 px-1 dark:bg-purple-800"
|
<code class="rounded bg-purple-100 px-1 dark:bg-purple-800"
|
||||||
>messages.length > 1</code
|
>messages.length > 1</code
|
||||||
>, 系统会认为这是一个污染的会话并拒绝请求。
|
>, 系统会认为这是一个污染的会话并拒绝请求。
|
||||||
|
|||||||
Reference in New Issue
Block a user