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:
shaw
2025-07-22 21:07:25 +08:00
parent e2cece6162
commit d6675a4d8e
3 changed files with 143 additions and 30 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,13 +409,33 @@ class ClaudeRelayService {
}
// 🔗 发送请求到Claude API
async _makeClaudeRequest(body, accessToken, proxyAgent, clientHeaders, onRequest, requestOptions = {}) {
return new Promise((resolve, reject) => {
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
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,
port: url.port || 443,
@@ -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 = {}) {
return new Promise((resolve, reject) => {
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
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);
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);
}
}