mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +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('/api', apiRoutes);
|
||||||
|
this.app.use('/claude', apiRoutes); // /claude 路由别名,与 /api 功能相同
|
||||||
this.app.use('/admin', adminRoutes);
|
this.app.use('/admin', adminRoutes);
|
||||||
this.app.use('/web', webRoutes);
|
this.app.use('/web', webRoutes);
|
||||||
this.app.use('/gemini', geminiRoutes);
|
this.app.use('/gemini', geminiRoutes);
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ const redis = require('../models/redis');
|
|||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// 🚀 Claude API messages 端点
|
// 🔧 共享的消息处理函数
|
||||||
router.post('/v1/messages', authenticateApiKey, async (req, res) => {
|
async function handleMessagesRequest(req, res) {
|
||||||
try {
|
try {
|
||||||
const startTime = Date.now();
|
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) => {
|
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) => {
|
router.get('/v1/key-info', authenticateApiKey, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const usage = await apiKeyService.getUsageStats(req.apiKey.id);
|
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) => {
|
router.get('/v1/usage', authenticateApiKey, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const usage = await apiKeyService.getUsageStats(req.apiKey.id);
|
const usage = await apiKeyService.getUsageStats(req.apiKey.id);
|
||||||
|
|||||||
@@ -16,6 +16,37 @@ class ClaudeRelayService {
|
|||||||
this.apiVersion = config.claude.apiVersion;
|
this.apiVersion = config.claude.apiVersion;
|
||||||
this.betaHeader = config.claude.betaHeader;
|
this.betaHeader = config.claude.betaHeader;
|
||||||
this.systemPrompt = config.claude.systemPrompt;
|
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
|
// 🚀 转发请求到Claude API
|
||||||
@@ -62,8 +93,8 @@ class ClaudeRelayService {
|
|||||||
// 获取有效的访问token
|
// 获取有效的访问token
|
||||||
const accessToken = await claudeAccountService.getValidAccessToken(accountId);
|
const accessToken = await claudeAccountService.getValidAccessToken(accountId);
|
||||||
|
|
||||||
// 处理请求体
|
// 处理请求体(传递 clientHeaders 以判断是否需要设置 Claude Code 系统提示词)
|
||||||
const processedBody = this._processRequestBody(requestBody);
|
const processedBody = this._processRequestBody(requestBody, clientHeaders);
|
||||||
|
|
||||||
// 获取代理配置
|
// 获取代理配置
|
||||||
const proxyAgent = await this._getProxyAgent(accountId);
|
const proxyAgent = await this._getProxyAgent(accountId);
|
||||||
@@ -90,6 +121,7 @@ class ClaudeRelayService {
|
|||||||
accessToken,
|
accessToken,
|
||||||
proxyAgent,
|
proxyAgent,
|
||||||
clientHeaders,
|
clientHeaders,
|
||||||
|
accountId,
|
||||||
(req) => { upstreamRequest = req; },
|
(req) => { upstreamRequest = req; },
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
@@ -130,8 +162,8 @@ class ClaudeRelayService {
|
|||||||
await claudeAccountService.removeAccountRateLimit(accountId);
|
await claudeAccountService.removeAccountRateLimit(accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储成功请求的 Claude Code headers
|
// 只有真实的 Claude Code 请求才更新 headers
|
||||||
if (clientHeaders && Object.keys(clientHeaders).length > 0) {
|
if (clientHeaders && Object.keys(clientHeaders).length > 0 && this.isRealClaudeCodeRequest(requestBody, clientHeaders)) {
|
||||||
await claudeCodeHeadersService.storeAccountHeaders(accountId, clientHeaders);
|
await claudeCodeHeadersService.storeAccountHeaders(accountId, clientHeaders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,7 +184,7 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔄 处理请求体
|
// 🔄 处理请求体
|
||||||
_processRequestBody(body) {
|
_processRequestBody(body, clientHeaders = {}) {
|
||||||
if (!body) return body;
|
if (!body) return body;
|
||||||
|
|
||||||
// 深拷贝请求体
|
// 深拷贝请求体
|
||||||
@@ -164,7 +196,36 @@ class ClaudeRelayService {
|
|||||||
// 移除cache_control中的ttl字段
|
// 移除cache_control中的ttl字段
|
||||||
this._stripTtlFromCacheControl(processedBody);
|
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()) {
|
if (this.systemPrompt && this.systemPrompt.trim()) {
|
||||||
const systemPrompt = {
|
const systemPrompt = {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -180,7 +241,13 @@ class ClaudeRelayService {
|
|||||||
if (!hasValidContent) {
|
if (!hasValidContent) {
|
||||||
processedBody.system = [systemPrompt];
|
processedBody.system = [systemPrompt];
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
throw new Error('system field must be an array');
|
throw new Error('system field must be an array');
|
||||||
@@ -342,12 +409,32 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔗 发送请求到Claude API
|
// 🔗 发送请求到Claude API
|
||||||
async _makeClaudeRequest(body, accessToken, proxyAgent, clientHeaders, onRequest, requestOptions = {}) {
|
async _makeClaudeRequest(body, accessToken, proxyAgent, clientHeaders, accountId, onRequest, requestOptions = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
const url = new URL(this.claudeApiUrl);
|
||||||
const url = new URL(this.claudeApiUrl);
|
|
||||||
|
|
||||||
// 获取过滤后的客户端 headers
|
// 获取过滤后的客户端 headers
|
||||||
const filteredHeaders = this._filterClientHeaders(clientHeaders);
|
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 = {
|
const options = {
|
||||||
hostname: url.hostname,
|
hostname: url.hostname,
|
||||||
@@ -358,15 +445,15 @@ class ClaudeRelayService {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
'anthropic-version': this.apiVersion,
|
'anthropic-version': this.apiVersion,
|
||||||
...filteredHeaders
|
...finalHeaders
|
||||||
},
|
},
|
||||||
agent: proxyAgent,
|
agent: proxyAgent,
|
||||||
timeout: config.proxy.timeout
|
timeout: config.proxy.timeout
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果客户端没有提供 User-Agent,使用默认值
|
// 如果客户端没有提供 User-Agent,使用默认值
|
||||||
if (!filteredHeaders['User-Agent'] && !filteredHeaders['user-agent']) {
|
if (!options.headers['User-Agent'] && !options.headers['user-agent']) {
|
||||||
options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)';
|
options.headers['User-Agent'] = 'claude-cli/1.0.57 (external, cli)';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用自定义的 betaHeader 或默认值
|
// 使用自定义的 betaHeader 或默认值
|
||||||
@@ -507,8 +594,8 @@ class ClaudeRelayService {
|
|||||||
// 获取有效的访问token
|
// 获取有效的访问token
|
||||||
const accessToken = await claudeAccountService.getValidAccessToken(accountId);
|
const accessToken = await claudeAccountService.getValidAccessToken(accountId);
|
||||||
|
|
||||||
// 处理请求体
|
// 处理请求体(传递 clientHeaders 以判断是否需要设置 Claude Code 系统提示词)
|
||||||
const processedBody = this._processRequestBody(requestBody);
|
const processedBody = this._processRequestBody(requestBody, clientHeaders);
|
||||||
|
|
||||||
// 获取代理配置
|
// 获取代理配置
|
||||||
const proxyAgent = await this._getProxyAgent(accountId);
|
const proxyAgent = await this._getProxyAgent(accountId);
|
||||||
@@ -523,12 +610,31 @@ class ClaudeRelayService {
|
|||||||
|
|
||||||
// 🌊 发送流式请求到Claude API(带usage数据捕获)
|
// 🌊 发送流式请求到Claude API(带usage数据捕获)
|
||||||
async _makeClaudeStreamRequestWithUsageCapture(body, accessToken, proxyAgent, clientHeaders, responseStream, usageCallback, accountId, sessionHash, streamTransformer = null, requestOptions = {}) {
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const url = new URL(this.claudeApiUrl);
|
const url = new URL(this.claudeApiUrl);
|
||||||
|
|
||||||
// 获取过滤后的客户端 headers
|
|
||||||
const filteredHeaders = this._filterClientHeaders(clientHeaders);
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
hostname: url.hostname,
|
hostname: url.hostname,
|
||||||
port: url.port || 443,
|
port: url.port || 443,
|
||||||
@@ -538,15 +644,15 @@ class ClaudeRelayService {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
'anthropic-version': this.apiVersion,
|
'anthropic-version': this.apiVersion,
|
||||||
...filteredHeaders
|
...finalHeaders
|
||||||
},
|
},
|
||||||
agent: proxyAgent,
|
agent: proxyAgent,
|
||||||
timeout: config.proxy.timeout
|
timeout: config.proxy.timeout
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果客户端没有提供 User-Agent,使用默认值
|
// 如果客户端没有提供 User-Agent,使用默认值
|
||||||
if (!filteredHeaders['User-Agent'] && !filteredHeaders['user-agent']) {
|
if (!options.headers['User-Agent'] && !options.headers['user-agent']) {
|
||||||
options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)';
|
options.headers['User-Agent'] = 'claude-cli/1.0.57 (external, cli)';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用自定义的 betaHeader 或默认值
|
// 使用自定义的 betaHeader 或默认值
|
||||||
@@ -668,8 +774,8 @@ class ClaudeRelayService {
|
|||||||
await claudeAccountService.removeAccountRateLimit(accountId);
|
await claudeAccountService.removeAccountRateLimit(accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储成功请求的 Claude Code headers(流式请求)
|
// 只有真实的 Claude Code 请求才更新 headers(流式请求)
|
||||||
if (clientHeaders && Object.keys(clientHeaders).length > 0) {
|
if (clientHeaders && Object.keys(clientHeaders).length > 0 && this.isRealClaudeCodeRequest(body, clientHeaders)) {
|
||||||
await claudeCodeHeadersService.storeAccountHeaders(accountId, clientHeaders);
|
await claudeCodeHeadersService.storeAccountHeaders(accountId, clientHeaders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user