From d6675a4d8ea9494b2cf103438299d4cbeffb3a49 Mon Sep 17 00:00:00 2001 From: shaw Date: Tue, 22 Jul 2025 21:07:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20/claude/v1/message?= =?UTF-8?q?s=20=E8=B7=AF=E7=94=B1=E5=88=AB=E5=90=8D=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=9D=9E=20Claude=20Code=20=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 /claude 路由作为 /api 的别名,支持 /claude/v1/messages 端点 - 实现智能判断请求来源,通过 user-agent 和系统提示词识别真实的 Claude Code 请求 - 为非 Claude Code 客户端自动设置系统提示词和必要的 headers - 优化 headers 更新逻辑,只有真实的 Claude Code 请求才更新缓存 - 确保 /api 和 /claude 路由功能完全一致 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app.js | 1 + src/routes/api.js | 16 ++- src/services/claudeRelayService.js | 156 ++++++++++++++++++++++++----- 3 files changed, 143 insertions(+), 30 deletions(-) diff --git a/src/app.js b/src/app.js index d64f8223..647a407d 100644 --- a/src/app.js +++ b/src/app.js @@ -98,6 +98,7 @@ class Application { // 🛣️ 路由 this.app.use('/api', apiRoutes); + this.app.use('/claude', apiRoutes); // /claude 路由别名,与 /api 功能相同 this.app.use('/admin', adminRoutes); this.app.use('/web', webRoutes); this.app.use('/gemini', geminiRoutes); diff --git a/src/routes/api.js b/src/routes/api.js index ed70ed2a..272766b5 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -7,8 +7,8 @@ const redis = require('../models/redis'); const router = express.Router(); -// 🚀 Claude API messages 端点 -router.post('/v1/messages', authenticateApiKey, async (req, res) => { +// 🔧 共享的消息处理函数 +async function handleMessagesRequest(req, res) { try { const startTime = Date.now(); @@ -199,7 +199,13 @@ router.post('/v1/messages', authenticateApiKey, async (req, res) => { } } } -}); +} + +// 🚀 Claude API messages 端点 - /api/v1/messages +router.post('/v1/messages', authenticateApiKey, handleMessagesRequest); + +// 🚀 Claude API messages 端点 - /claude/v1/messages (别名) +router.post('/claude/v1/messages', authenticateApiKey, handleMessagesRequest); // 🏥 健康检查端点 router.get('/health', async (req, res) => { @@ -223,7 +229,7 @@ router.get('/health', async (req, res) => { } }); -// 📊 API Key状态检查端点 +// 📊 API Key状态检查端点 - /api/v1/key-info router.get('/v1/key-info', authenticateApiKey, async (req, res) => { try { const usage = await apiKeyService.getUsageStats(req.apiKey.id); @@ -246,7 +252,7 @@ router.get('/v1/key-info', authenticateApiKey, async (req, res) => { } }); -// 📈 使用统计端点 +// 📈 使用统计端点 - /api/v1/usage router.get('/v1/usage', authenticateApiKey, async (req, res) => { try { const usage = await apiKeyService.getUsageStats(req.apiKey.id); diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index 11ccaaa3..a70e2dea 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -16,6 +16,37 @@ class ClaudeRelayService { this.apiVersion = config.claude.apiVersion; this.betaHeader = config.claude.betaHeader; this.systemPrompt = config.claude.systemPrompt; + this.claudeCodeSystemPrompt = 'You are Claude Code, Anthropic\'s official CLI for Claude.'; + } + + // 🔍 判断是否是真实的 Claude Code 请求 + isRealClaudeCodeRequest(requestBody, clientHeaders) { + // 检查 user-agent 是否匹配 Claude Code 格式 + const userAgent = clientHeaders?.['user-agent'] || clientHeaders?.['User-Agent'] || ''; + const isClaudeCodeUserAgent = /claude-cli\/\d+\.\d+\.\d+/.test(userAgent); + + // 检查系统提示词是否包含 Claude Code 标识 + const hasClaudeCodeSystemPrompt = this._hasClaudeCodeSystemPrompt(requestBody); + + // 只有当 user-agent 匹配且系统提示词正确时,才认为是真实的 Claude Code 请求 + return isClaudeCodeUserAgent && hasClaudeCodeSystemPrompt; + } + + // 🔍 检查请求中是否包含 Claude Code 系统提示词 + _hasClaudeCodeSystemPrompt(requestBody) { + if (!requestBody || !requestBody.system) return false; + + let systemText = ''; + 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 systemText.includes(this.claudeCodeSystemPrompt); } // 🚀 转发请求到Claude API @@ -62,8 +93,8 @@ class ClaudeRelayService { // 获取有效的访问token const accessToken = await claudeAccountService.getValidAccessToken(accountId); - // 处理请求体 - const processedBody = this._processRequestBody(requestBody); + // 处理请求体(传递 clientHeaders 以判断是否需要设置 Claude Code 系统提示词) + const processedBody = this._processRequestBody(requestBody, clientHeaders); // 获取代理配置 const proxyAgent = await this._getProxyAgent(accountId); @@ -90,6 +121,7 @@ class ClaudeRelayService { accessToken, proxyAgent, clientHeaders, + accountId, (req) => { upstreamRequest = req; }, options ); @@ -130,8 +162,8 @@ class ClaudeRelayService { await claudeAccountService.removeAccountRateLimit(accountId); } - // 存储成功请求的 Claude Code headers - if (clientHeaders && Object.keys(clientHeaders).length > 0) { + // 只有真实的 Claude Code 请求才更新 headers + if (clientHeaders && Object.keys(clientHeaders).length > 0 && this.isRealClaudeCodeRequest(requestBody, clientHeaders)) { await claudeCodeHeadersService.storeAccountHeaders(accountId, clientHeaders); } } @@ -152,7 +184,7 @@ class ClaudeRelayService { } // 🔄 处理请求体 - _processRequestBody(body) { + _processRequestBody(body, clientHeaders = {}) { if (!body) return body; // 深拷贝请求体 @@ -164,7 +196,36 @@ class ClaudeRelayService { // 移除cache_control中的ttl字段 this._stripTtlFromCacheControl(processedBody); - // 只有在配置了系统提示时才添加 + // 判断是否是真实的 Claude Code 请求 + const isRealClaudeCode = this.isRealClaudeCodeRequest(processedBody, clientHeaders); + + // 如果不是真实的 Claude Code 请求,需要设置 Claude Code 系统提示词 + if (!isRealClaudeCode) { + const claudeCodePrompt = { + type: 'text', + text: this.claudeCodeSystemPrompt + }; + + 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 (!hasClaudeCodePrompt) { + // 添加 Claude Code 系统提示词到开头 + processedBody.system.unshift(claudeCodePrompt); + } + } else { + throw new Error('system field must be an array'); + } + } else { + processedBody.system = [claudeCodePrompt]; + } + } + + // 处理原有的系统提示(如果配置了) if (this.systemPrompt && this.systemPrompt.trim()) { const systemPrompt = { type: 'text', @@ -180,7 +241,13 @@ class ClaudeRelayService { if (!hasValidContent) { processedBody.system = [systemPrompt]; } else { - processedBody.system.unshift(systemPrompt); + // 不要重复添加相同的系统提示 + 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'); @@ -342,12 +409,32 @@ class ClaudeRelayService { } // 🔗 发送请求到Claude API - async _makeClaudeRequest(body, accessToken, proxyAgent, clientHeaders, onRequest, requestOptions = {}) { - return new Promise((resolve, reject) => { - const url = new URL(this.claudeApiUrl); + async _makeClaudeRequest(body, accessToken, proxyAgent, clientHeaders, accountId, onRequest, requestOptions = {}) { + const url = new URL(this.claudeApiUrl); + + // 获取过滤后的客户端 headers + const filteredHeaders = this._filterClientHeaders(clientHeaders); + + // 判断是否是真实的 Claude Code 请求 + const isRealClaudeCode = this.isRealClaudeCodeRequest(body, clientHeaders); + + // 如果不是真实的 Claude Code 请求,需要使用从账户获取的 Claude Code headers + let finalHeaders = { ...filteredHeaders }; + + if (!isRealClaudeCode) { + // 获取该账号存储的 Claude Code headers + const claudeCodeHeaders = await claudeCodeHeadersService.getAccountHeaders(accountId); - // 获取过滤后的客户端 headers - const filteredHeaders = this._filterClientHeaders(clientHeaders); + // 只添加客户端没有提供的 headers + Object.keys(claudeCodeHeaders).forEach(key => { + const lowerKey = key.toLowerCase(); + if (!finalHeaders[key] && !finalHeaders[lowerKey]) { + finalHeaders[key] = claudeCodeHeaders[key]; + } + }); + } + + return new Promise((resolve, reject) => { const options = { hostname: url.hostname, @@ -358,15 +445,15 @@ class ClaudeRelayService { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'anthropic-version': this.apiVersion, - ...filteredHeaders + ...finalHeaders }, agent: proxyAgent, timeout: config.proxy.timeout }; // 如果客户端没有提供 User-Agent,使用默认值 - if (!filteredHeaders['User-Agent'] && !filteredHeaders['user-agent']) { - options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)'; + if (!options.headers['User-Agent'] && !options.headers['user-agent']) { + options.headers['User-Agent'] = 'claude-cli/1.0.57 (external, cli)'; } // 使用自定义的 betaHeader 或默认值 @@ -507,8 +594,8 @@ class ClaudeRelayService { // 获取有效的访问token const accessToken = await claudeAccountService.getValidAccessToken(accountId); - // 处理请求体 - const processedBody = this._processRequestBody(requestBody); + // 处理请求体(传递 clientHeaders 以判断是否需要设置 Claude Code 系统提示词) + const processedBody = this._processRequestBody(requestBody, clientHeaders); // 获取代理配置 const proxyAgent = await this._getProxyAgent(accountId); @@ -523,12 +610,31 @@ class ClaudeRelayService { // 🌊 发送流式请求到Claude API(带usage数据捕获) async _makeClaudeStreamRequestWithUsageCapture(body, accessToken, proxyAgent, clientHeaders, responseStream, usageCallback, accountId, sessionHash, streamTransformer = null, requestOptions = {}) { + // 获取过滤后的客户端 headers + const filteredHeaders = this._filterClientHeaders(clientHeaders); + + // 判断是否是真实的 Claude Code 请求 + const isRealClaudeCode = this.isRealClaudeCodeRequest(body, clientHeaders); + + // 如果不是真实的 Claude Code 请求,需要使用从账户获取的 Claude Code headers + let finalHeaders = { ...filteredHeaders }; + + if (!isRealClaudeCode) { + // 获取该账号存储的 Claude Code headers + const claudeCodeHeaders = await claudeCodeHeadersService.getAccountHeaders(accountId); + + // 只添加客户端没有提供的 headers + Object.keys(claudeCodeHeaders).forEach(key => { + const lowerKey = key.toLowerCase(); + if (!finalHeaders[key] && !finalHeaders[lowerKey]) { + finalHeaders[key] = claudeCodeHeaders[key]; + } + }); + } + return new Promise((resolve, reject) => { const url = new URL(this.claudeApiUrl); - // 获取过滤后的客户端 headers - const filteredHeaders = this._filterClientHeaders(clientHeaders); - const options = { hostname: url.hostname, port: url.port || 443, @@ -538,15 +644,15 @@ class ClaudeRelayService { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'anthropic-version': this.apiVersion, - ...filteredHeaders + ...finalHeaders }, agent: proxyAgent, timeout: config.proxy.timeout }; // 如果客户端没有提供 User-Agent,使用默认值 - if (!filteredHeaders['User-Agent'] && !filteredHeaders['user-agent']) { - options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)'; + if (!options.headers['User-Agent'] && !options.headers['user-agent']) { + options.headers['User-Agent'] = 'claude-cli/1.0.57 (external, cli)'; } // 使用自定义的 betaHeader 或默认值 @@ -668,8 +774,8 @@ class ClaudeRelayService { await claudeAccountService.removeAccountRateLimit(accountId); } - // 存储成功请求的 Claude Code headers(流式请求) - if (clientHeaders && Object.keys(clientHeaders).length > 0) { + // 只有真实的 Claude Code 请求才更新 headers(流式请求) + if (clientHeaders && Object.keys(clientHeaders).length > 0 && this.isRealClaudeCodeRequest(body, clientHeaders)) { await claudeCodeHeadersService.storeAccountHeaders(accountId, clientHeaders); } }