From 1e372dd3652dee9750daebf7e94cfb4409208371 Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 23 Jul 2025 15:56:27 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=B5=81=E5=BC=8F?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E7=BC=93=E5=86=B2=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=9C=9F=E6=AD=A3=E7=9A=84=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E6=B5=81=E4=BC=A0=E8=BE=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 配置 compression 中间件排除 SSE 流式响应,避免压缩导致的缓冲 - 添加 X-Accel-Buffering: no 响应头,禁用 Nginx 等代理的缓冲 - 使用 res.flushHeaders() 立即发送响应头 - 禁用 Nagle 算法确保数据立即发送 - 在每次写入流数据后调用 flush() 确保实时传输 这些修复确保了流式请求能够正常显示打字机效果,数据从上游 Claude API 接收后能够立即转发给客户端。 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app.js | 13 +++- src/routes/api.js | 9 +++ src/services/claudeRelayService.js | 96 ++++++++++++++++++------------ 3 files changed, 79 insertions(+), 39 deletions(-) diff --git a/src/app.js b/src/app.js index 26ed5141..42168da5 100644 --- a/src/app.js +++ b/src/app.js @@ -64,8 +64,17 @@ class Application { this.app.use(corsMiddleware); } - // 📦 压缩 - this.app.use(compression()); + // 📦 压缩 - 排除流式响应(SSE) + this.app.use(compression({ + filter: (req, res) => { + // 不压缩 Server-Sent Events + if (res.getHeader('Content-Type') === 'text/event-stream') { + return false; + } + // 使用默认的压缩判断 + return compression.filter(req, res); + } + })); // 🚦 全局速率限制(仅在生产环境启用) if (process.env.NODE_ENV === 'production') { diff --git a/src/routes/api.js b/src/routes/api.js index 272766b5..ddf92714 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -45,6 +45,15 @@ async function handleMessagesRequest(req, res) { res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('X-Accel-Buffering', 'no'); // 禁用 Nginx 缓冲 + + // 立即发送响应头,防止缓冲 + res.flushHeaders(); + + // 禁用 Nagle 算法,确保数据立即发送 + if (res.socket && res.socket.setNoDelay) { + res.socket.setNoDelay(true); + } // 流式响应不需要额外处理,中间件已经设置了监听器 diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index a70e2dea..fbf15247 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -36,17 +36,22 @@ class ClaudeRelayService { _hasClaudeCodeSystemPrompt(requestBody) { if (!requestBody || !requestBody.system) return false; - let systemText = ''; + // 如果是字符串格式,一定不是真实的 Claude Code 请求 if (typeof requestBody.system === 'string') { - systemText = requestBody.system; - } else if (Array.isArray(requestBody.system)) { - systemText = requestBody.system - .filter(item => item && item.type === 'text' && item.text) - .map(item => item.text) - .join(' '); + return false; + } + + // 处理数组格式 + if (Array.isArray(requestBody.system) && requestBody.system.length > 0) { + const firstItem = requestBody.system[0]; + // 检查第一个元素是否包含 Claude Code 提示词 + return firstItem && + firstItem.type === 'text' && + firstItem.text && + firstItem.text === this.claudeCodeSystemPrompt; } - return systemText.includes(this.claudeCodeSystemPrompt); + return false; } // 🚀 转发请求到Claude API @@ -203,24 +208,47 @@ class ClaudeRelayService { if (!isRealClaudeCode) { const claudeCodePrompt = { type: 'text', - text: this.claudeCodeSystemPrompt + text: this.claudeCodeSystemPrompt, + cache_control: { + type: 'ephemeral' + } }; if (processedBody.system) { - if (Array.isArray(processedBody.system)) { - // 检查是否已经有 Claude Code 系统提示词 - const hasClaudeCodePrompt = processedBody.system.some(item => - item && item.text && item.text.includes(this.claudeCodeSystemPrompt) - ); + if (typeof processedBody.system === 'string') { + // 字符串格式:转换为数组,Claude Code 提示词在第一位 + const userSystemPrompt = { + type: 'text', + text: processedBody.system + }; + // 如果用户的提示词与 Claude Code 提示词相同,只保留一个 + if (processedBody.system.trim() === this.claudeCodeSystemPrompt) { + processedBody.system = [claudeCodePrompt]; + } else { + processedBody.system = [claudeCodePrompt, userSystemPrompt]; + } + } else if (Array.isArray(processedBody.system)) { + // 检查第一个元素是否是 Claude Code 系统提示词 + const firstItem = processedBody.system[0]; + const isFirstItemClaudeCode = firstItem && + firstItem.type === 'text' && + firstItem.text === this.claudeCodeSystemPrompt; - if (!hasClaudeCodePrompt) { - // 添加 Claude Code 系统提示词到开头 - processedBody.system.unshift(claudeCodePrompt); + if (!isFirstItemClaudeCode) { + // 如果第一个不是 Claude Code 提示词,需要在开头插入 + // 同时检查数组中是否有其他位置包含 Claude Code 提示词,如果有则移除 + const filteredSystem = processedBody.system.filter(item => + !(item && item.type === 'text' && item.text === this.claudeCodeSystemPrompt) + ); + processedBody.system = [claudeCodePrompt, ...filteredSystem]; } } else { - throw new Error('system field must be an array'); + // 其他格式,记录警告但不抛出错误,尝试处理 + logger.warn('⚠️ Unexpected system field type:', typeof processedBody.system); + processedBody.system = [claudeCodePrompt]; } } else { + // 用户没有传递 system,需要添加 Claude Code 提示词 processedBody.system = [claudeCodePrompt]; } } @@ -232,27 +260,17 @@ class ClaudeRelayService { text: this.systemPrompt }; - if (processedBody.system) { - if (Array.isArray(processedBody.system)) { - // 如果system数组存在但为空,或者没有有效内容,则添加系统提示 - const hasValidContent = processedBody.system.some(item => - item && item.text && item.text.trim() - ); - if (!hasValidContent) { - processedBody.system = [systemPrompt]; - } else { - // 不要重复添加相同的系统提示 - const hasSystemPrompt = processedBody.system.some(item => - item && item.text && item.text === this.systemPrompt - ); - if (!hasSystemPrompt) { - processedBody.system.push(systemPrompt); - } - } - } else { - throw new Error('system field must be an array'); + // 经过上面的处理,system 现在应该总是数组格式 + if (processedBody.system && Array.isArray(processedBody.system)) { + // 不要重复添加相同的系统提示 + const hasSystemPrompt = processedBody.system.some(item => + item && item.text && item.text === this.systemPrompt + ); + if (!hasSystemPrompt) { + processedBody.system.push(systemPrompt); } } else { + // 理论上不应该走到这里,但为了安全起见 processedBody.system = [systemPrompt]; } } else { @@ -691,9 +709,13 @@ class ClaudeRelayService { const transformed = streamTransformer(linesToForward); if (transformed) { responseStream.write(transformed); + // 立即刷新数据,确保实时发送 + if (responseStream.flush) responseStream.flush(); } } else { responseStream.write(linesToForward); + // 立即刷新数据,确保实时发送 + if (responseStream.flush) responseStream.flush(); } }