mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 添加 /claude/v1/messages 路由别名并优化非 Claude Code 客户端支持
- 添加 /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 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user