Files
droid2api/routes.js
Claude Code dec2f26b5c fix: 修复 system 提示词中敏感词导致的 403 错误
- 修改 docker-compose.yml:将宿主机端口从 3000 改为 3001
- 修改 routes.js:增强 system 字段过滤逻辑,过滤所有项中的敏感词
- 修改 transformers/request-anthropic.js:添加 filterSensitiveKeywords 函数
- 修改 user-agent-updater.js:优化错误日志输出,增加超时时间

过滤规则:
- "Claude Code" → "AI Assistant"
- "Claude" → "AI"
- "Anthropic" → "Factory"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 18:56:26 +00:00

748 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import express from 'express';
import fetch from 'node-fetch';
import { getConfig, getModelById, getEndpointByType, getSystemPrompt, getModelReasoning, getRedirectedModelId, getModelProvider } from './config.js';
import { logInfo, logDebug, logError, logRequest, logResponse } from './logger.js';
import { transformToAnthropic, getAnthropicHeaders } from './transformers/request-anthropic.js';
import { transformToOpenAI, getOpenAIHeaders } from './transformers/request-openai.js';
import { transformToCommon, getCommonHeaders } from './transformers/request-common.js';
import { AnthropicResponseTransformer } from './transformers/response-anthropic.js';
import { OpenAIResponseTransformer } from './transformers/response-openai.js';
import { getApiKey } from './auth.js';
import { getNextProxyAgent } from './proxy-manager.js';
const router = express.Router();
/**
* Convert a /v1/responses API result to a /v1/chat/completions-compatible format.
* Works for non-streaming responses.
*/
function convertResponseToChatCompletion(resp) {
if (!resp || typeof resp !== 'object') {
throw new Error('Invalid response object');
}
const outputMsg = (resp.output || []).find(o => o.type === 'message');
const textBlocks = outputMsg?.content?.filter(c => c.type === 'output_text') || [];
const content = textBlocks.map(c => c.text).join('');
const chatCompletion = {
id: resp.id ? resp.id.replace(/^resp_/, 'chatcmpl-') : `chatcmpl-${Date.now()}`,
object: 'chat.completion',
created: resp.created_at || Math.floor(Date.now() / 1000),
model: resp.model || 'unknown-model',
choices: [
{
index: 0,
message: {
role: outputMsg?.role || 'assistant',
content: content || ''
},
finish_reason: resp.status === 'completed' ? 'stop' : 'unknown'
}
],
usage: {
prompt_tokens: resp.usage?.input_tokens ?? 0,
completion_tokens: resp.usage?.output_tokens ?? 0,
total_tokens: resp.usage?.total_tokens ?? 0
}
};
return chatCompletion;
}
router.get('/v1/models', (req, res) => {
logInfo('GET /v1/models');
try {
const config = getConfig();
const models = config.models.map(model => ({
id: model.id,
object: 'model',
created: Date.now(),
owned_by: model.type,
permission: [],
root: model.id,
parent: null
}));
const response = {
object: 'list',
data: models
};
logResponse(200, null, response);
res.json(response);
} catch (error) {
logError('Error in GET /v1/models', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// 标准 OpenAI 聊天补全处理函数(带格式转换)
async function handleChatCompletions(req, res) {
logInfo('POST /v1/chat/completions');
try {
const openaiRequest = req.body;
const modelId = getRedirectedModelId(openaiRequest.model);
if (!modelId) {
return res.status(400).json({ error: 'model is required' });
}
const model = getModelById(modelId);
if (!model) {
return res.status(404).json({ error: `Model ${modelId} not found` });
}
const endpoint = getEndpointByType(model.type);
if (!endpoint) {
return res.status(500).json({ error: `Endpoint type ${model.type} not found` });
}
logInfo(`Routing to ${model.type} endpoint: ${endpoint.base_url}`);
// Get API key (will auto-refresh if needed)
let authHeader;
try {
authHeader = await getApiKey(req.headers.authorization);
} catch (error) {
logError('Failed to get API key', error);
return res.status(500).json({
error: 'API key not available',
message: 'Failed to get or refresh API key. Please check server logs.'
});
}
let transformedRequest;
let headers;
const clientHeaders = req.headers;
// Log received client headers for debugging
logDebug('Client headers received', {
'x-factory-client': clientHeaders['x-factory-client'],
'x-session-id': clientHeaders['x-session-id'],
'x-assistant-message-id': clientHeaders['x-assistant-message-id'],
'user-agent': clientHeaders['user-agent']
});
// Update request body with redirected model ID before transformation
const requestWithRedirectedModel = { ...openaiRequest, model: modelId };
// Get provider from model config
const provider = getModelProvider(modelId);
if (model.type === 'anthropic') {
transformedRequest = transformToAnthropic(requestWithRedirectedModel);
const isStreaming = openaiRequest.stream === true;
headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming, modelId, provider);
} else if (model.type === 'openai') {
transformedRequest = transformToOpenAI(requestWithRedirectedModel);
headers = getOpenAIHeaders(authHeader, clientHeaders, provider);
} else if (model.type === 'common') {
transformedRequest = transformToCommon(requestWithRedirectedModel);
headers = getCommonHeaders(authHeader, clientHeaders, provider);
} else {
return res.status(500).json({ error: `Unknown endpoint type: ${model.type}` });
}
logRequest('POST', endpoint.base_url, headers, transformedRequest);
const proxyAgentInfo = getNextProxyAgent(endpoint.base_url);
const fetchOptions = {
method: 'POST',
headers,
body: JSON.stringify(transformedRequest)
};
if (proxyAgentInfo?.agent) {
fetchOptions.agent = proxyAgentInfo.agent;
}
const response = await fetch(endpoint.base_url, fetchOptions);
logInfo(`Response status: ${response.status}`);
if (!response.ok) {
const errorText = await response.text();
logError(`Endpoint error: ${response.status}`, new Error(errorText));
return res.status(response.status).json({
error: `Endpoint returned ${response.status}`,
details: errorText
});
}
const isStreaming = transformedRequest.stream === true;
if (isStreaming) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// common 类型直接转发,不使用 transformer
if (model.type === 'common') {
try {
for await (const chunk of response.body) {
res.write(chunk);
}
res.end();
logInfo('Stream forwarded (common type)');
} catch (streamError) {
logError('Stream error', streamError);
res.end();
}
} else {
// anthropic 和 openai 类型使用 transformer
let transformer;
if (model.type === 'anthropic') {
transformer = new AnthropicResponseTransformer(modelId, `chatcmpl-${Date.now()}`);
} else if (model.type === 'openai') {
transformer = new OpenAIResponseTransformer(modelId, `chatcmpl-${Date.now()}`);
}
try {
for await (const chunk of transformer.transformStream(response.body)) {
res.write(chunk);
}
res.end();
logInfo('Stream completed');
} catch (streamError) {
logError('Stream error', streamError);
res.end();
}
}
} else {
const data = await response.json();
if (model.type === 'openai') {
try {
const converted = convertResponseToChatCompletion(data);
logResponse(200, null, converted);
res.json(converted);
} catch (e) {
// 如果转换失败,回退为原始数据
logResponse(200, null, data);
res.json(data);
}
} else {
// anthropic/common: 保持现有逻辑,直接转发
logResponse(200, null, data);
res.json(data);
}
}
} catch (error) {
logError('Error in /v1/chat/completions', error);
res.status(500).json({
error: 'Internal server error',
message: error.message
});
}
}
// 直接转发 OpenAI 请求(不做格式转换)
async function handleDirectResponses(req, res) {
logInfo('POST /v1/responses');
try {
const openaiRequest = req.body;
const modelId = getRedirectedModelId(openaiRequest.model);
if (!modelId) {
return res.status(400).json({ error: 'model is required' });
}
const model = getModelById(modelId);
if (!model) {
return res.status(404).json({ error: `Model ${modelId} not found` });
}
// 只允许 openai 类型端点
if (model.type !== 'openai') {
return res.status(400).json({
error: 'Invalid endpoint type',
message: `/v1/responses 接口只支持 openai 类型端点,当前模型 ${modelId}${model.type} 类型`
});
}
const endpoint = getEndpointByType(model.type);
if (!endpoint) {
return res.status(500).json({ error: `Endpoint type ${model.type} not found` });
}
logInfo(`Direct forwarding to ${model.type} endpoint: ${endpoint.base_url}`);
// Get API key - support client x-api-key for anthropic endpoint
let authHeader;
try {
const clientAuthFromXApiKey = req.headers['x-api-key']
? `Bearer ${req.headers['x-api-key']}`
: null;
authHeader = await getApiKey(req.headers.authorization || clientAuthFromXApiKey);
} catch (error) {
logError('Failed to get API key', error);
return res.status(500).json({
error: 'API key not available',
message: 'Failed to get or refresh API key. Please check server logs.'
});
}
const clientHeaders = req.headers;
// Get provider from model config
const provider = getModelProvider(modelId);
// 获取 headers
const headers = getOpenAIHeaders(authHeader, clientHeaders, provider);
// 注入系统提示到 instructions 字段并更新重定向后的模型ID
const systemPrompt = getSystemPrompt();
const modifiedRequest = { ...openaiRequest, model: modelId };
if (systemPrompt) {
// 如果已有 instructions则在前面添加系统提示
if (modifiedRequest.instructions) {
modifiedRequest.instructions = systemPrompt + modifiedRequest.instructions;
} else {
// 否则直接设置系统提示
modifiedRequest.instructions = systemPrompt;
}
}
// 处理reasoning字段
const reasoningLevel = getModelReasoning(modelId);
if (reasoningLevel === 'auto') {
// Auto模式保持原始请求的reasoning字段不变
// 如果原始请求有reasoning字段就保留没有就不添加
} else if (reasoningLevel && ['low', 'medium', 'high', 'xhigh'].includes(reasoningLevel)) {
modifiedRequest.reasoning = {
effort: reasoningLevel,
summary: 'auto'
};
} else {
// 如果配置是off或无效移除reasoning字段
delete modifiedRequest.reasoning;
}
// 删除 claude-cli 特有字段,避免 Factory API 返回 403
delete modifiedRequest.context_management;
// 过滤 Claude Code 特有的 MCP 工具
if (modifiedRequest.tools && Array.isArray(modifiedRequest.tools)) {
modifiedRequest.tools = modifiedRequest.tools.filter(tool => {
if (!tool.name) return true;
// 过滤 Claude Code 特有工具
const claudeCodeTools = [
'Skill',
'EnterPlanMode',
'ExitPlanMode',
'AskUserQuestion',
'TodoWrite'
];
if (claudeCodeTools.includes(tool.name)) return false;
// 过滤所有 mcp__ 开头的工具和 MCP 相关工具
return !tool.name.startsWith('mcp__') &&
!tool.name.includes('Mcp') &&
!tool.name.includes('MCP');
});
}
logRequest('POST', endpoint.base_url, headers, modifiedRequest);
// 转发修改后的请求
const proxyAgentInfo = getNextProxyAgent(endpoint.base_url);
const fetchOptions = {
method: 'POST',
headers,
body: JSON.stringify(modifiedRequest)
};
if (proxyAgentInfo?.agent) {
fetchOptions.agent = proxyAgentInfo.agent;
}
const response = await fetch(endpoint.base_url, fetchOptions);
logInfo(`Response status: ${response.status}`);
if (!response.ok) {
const errorText = await response.text();
logError(`Endpoint error: ${response.status}`, new Error(errorText));
return res.status(response.status).json({
error: `Endpoint returned ${response.status}`,
details: errorText
});
}
const isStreaming = openaiRequest.stream === true;
if (isStreaming) {
// 直接转发流式响应,不做任何转换
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
// 直接将原始响应流转发给客户端
for await (const chunk of response.body) {
res.write(chunk);
}
res.end();
logInfo('Stream forwarded successfully');
} catch (streamError) {
logError('Stream error', streamError);
res.end();
}
} else {
// 直接转发非流式响应,不做任何转换
const data = await response.json();
logResponse(200, null, data);
res.json(data);
}
} catch (error) {
logError('Error in /v1/responses', error);
res.status(500).json({
error: 'Internal server error',
message: error.message
});
}
}
// 直接转发 Anthropic 请求(不做格式转换)
async function handleDirectMessages(req, res) {
logInfo('POST /v1/messages');
try {
const anthropicRequest = req.body;
const modelId = getRedirectedModelId(anthropicRequest.model);
if (!modelId) {
return res.status(400).json({ error: 'model is required' });
}
const model = getModelById(modelId);
if (!model) {
return res.status(404).json({ error: `Model ${modelId} not found` });
}
// 只允许 anthropic 类型端点
if (model.type !== 'anthropic') {
return res.status(400).json({
error: 'Invalid endpoint type',
message: `/v1/messages 接口只支持 anthropic 类型端点,当前模型 ${modelId}${model.type} 类型`
});
}
const endpoint = getEndpointByType(model.type);
if (!endpoint) {
return res.status(500).json({ error: `Endpoint type ${model.type} not found` });
}
logInfo(`Direct forwarding to ${model.type} endpoint: ${endpoint.base_url}`);
// Get API key - support client x-api-key for anthropic endpoint
let authHeader;
try {
const clientAuthFromXApiKey = req.headers['x-api-key']
? `Bearer ${req.headers['x-api-key']}`
: null;
authHeader = await getApiKey(req.headers.authorization || clientAuthFromXApiKey);
} catch (error) {
logError('Failed to get API key', error);
return res.status(500).json({
error: 'API key not available',
message: 'Failed to get or refresh API key. Please check server logs.'
});
}
const clientHeaders = req.headers;
// Get provider from model config
const provider = getModelProvider(modelId);
// 获取 headers
const isStreaming = anthropicRequest.stream === true;
const headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming, modelId, provider);
// 注入系统提示到 system 字段并更新重定向后的模型ID
const systemPrompt = getSystemPrompt();
const modifiedRequest = { ...anthropicRequest, model: modelId };
if (systemPrompt) {
if (modifiedRequest.system && Array.isArray(modifiedRequest.system)) {
// 如果已有 system 数组,则在最前面插入系统提示
modifiedRequest.system = [
{ type: 'text', text: systemPrompt },
...modifiedRequest.system
];
} else {
// 否则创建新的 system 数组
modifiedRequest.system = [
{ type: 'text', text: systemPrompt }
];
}
}
// 将 Claude Code 格式转换为 Droid 格式 (修复 403 错误)
if (modifiedRequest.system && Array.isArray(modifiedRequest.system)) {
modifiedRequest.system = modifiedRequest.system.map((item, index) => {
const newItem = { ...item };
// 删除所有 cache_control
if (newItem.cache_control) {
delete newItem.cache_control;
}
// 过滤所有 system 项中的敏感词
if (newItem.text && typeof newItem.text === 'string') {
// 替换敏感词以避免 403 错误
newItem.text = newItem.text
.replace(/Claude Code/g, 'AI Assistant')
.replace(/claude code/g, 'AI assistant')
.replace(/\bClaude\b/g, 'AI')
.replace(/\bclaude\b/g, 'AI')
.replace(/Anthropic/g, 'Factory')
.replace(/anthropic/g, 'factory');
}
return newItem;
});
}
// 处理thinking字段
const reasoningLevel = getModelReasoning(modelId);
if (reasoningLevel === 'auto') {
// Auto模式保持原始请求的thinking字段不变
// 如果原始请求有thinking字段就保留没有就不添加
} else if (reasoningLevel && ['low', 'medium', 'high', 'xhigh'].includes(reasoningLevel)) {
const budgetTokens = {
'low': 4096,
'medium': 12288,
'high': 24576,
'xhigh': 40960
};
modifiedRequest.thinking = {
type: 'enabled',
budget_tokens: budgetTokens[reasoningLevel]
};
} else {
// 如果配置是off或无效移除thinking字段
delete modifiedRequest.thinking;
}
// 过滤 messages 中的 Claude Code 特有标识
if (modifiedRequest.messages && Array.isArray(modifiedRequest.messages)) {
modifiedRequest.messages = modifiedRequest.messages.map(msg => {
if (msg.content && Array.isArray(msg.content)) {
msg.content = msg.content.filter(item => {
if (item.type === 'text' && item.text) {
// 过滤包含 Claude Code 特征的内容
const claudeCodePatterns = [
'<system-reminder>',
'# claudeMd',
'/Users/',
'/.claude/',
'CLAUDE.md',
'<command-name>',
'<command-message>',
'<local-command-stdout>',
'CodeX MCP',
'codex MCP'
];
return !claudeCodePatterns.some(pattern => item.text.includes(pattern));
}
return true;
});
}
return msg;
});
}
// 删除 claude-cli 特有字段,避免 Factory API 返回 403
delete modifiedRequest.context_management;
// 过滤 Claude Code 特有的 MCP 工具
if (modifiedRequest.tools && Array.isArray(modifiedRequest.tools)) {
modifiedRequest.tools = modifiedRequest.tools.filter(tool => {
if (!tool.name) return true;
// 过滤 Claude Code 特有工具
const claudeCodeTools = [
'Skill',
'EnterPlanMode',
'ExitPlanMode',
'AskUserQuestion',
'TodoWrite'
];
if (claudeCodeTools.includes(tool.name)) return false;
// 过滤所有 mcp__ 开头的工具和 MCP 相关工具
return !tool.name.startsWith('mcp__') &&
!tool.name.includes('Mcp') &&
!tool.name.includes('MCP');
});
}
logRequest('POST', endpoint.base_url, headers, modifiedRequest);
// 转发修改后的请求
const proxyAgentInfo = getNextProxyAgent(endpoint.base_url);
const fetchOptions = {
method: 'POST',
headers,
body: JSON.stringify(modifiedRequest)
};
if (proxyAgentInfo?.agent) {
fetchOptions.agent = proxyAgentInfo.agent;
}
const response = await fetch(endpoint.base_url, fetchOptions);
logInfo(`Response status: ${response.status}`);
if (!response.ok) {
const errorText = await response.text();
logError(`Endpoint error: ${response.status}`, new Error(errorText));
return res.status(response.status).json({
error: `Endpoint returned ${response.status}`,
details: errorText
});
}
if (isStreaming) {
// 直接转发流式响应,不做任何转换
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
// 直接将原始响应流转发给客户端
for await (const chunk of response.body) {
res.write(chunk);
}
res.end();
logInfo('Stream forwarded successfully');
} catch (streamError) {
logError('Stream error', streamError);
res.end();
}
} else {
// 直接转发非流式响应,不做任何转换
const data = await response.json();
logResponse(200, null, data);
res.json(data);
}
} catch (error) {
logError('Error in /v1/messages', error);
res.status(500).json({
error: 'Internal server error',
message: error.message
});
}
}
// 处理 Anthropic count_tokens 请求
async function handleCountTokens(req, res) {
logInfo('POST /v1/messages/count_tokens');
try {
const anthropicRequest = req.body;
const modelId = getRedirectedModelId(anthropicRequest.model);
if (!modelId) {
return res.status(400).json({ error: 'model is required' });
}
const model = getModelById(modelId);
if (!model) {
return res.status(404).json({ error: `Model ${modelId} not found` });
}
// 只允许 anthropic 类型端点
if (model.type !== 'anthropic') {
return res.status(400).json({
error: 'Invalid endpoint type',
message: `/v1/messages/count_tokens 接口只支持 anthropic 类型端点,当前模型 ${modelId}${model.type} 类型`
});
}
const endpoint = getEndpointByType('anthropic');
if (!endpoint) {
return res.status(500).json({ error: 'Endpoint type anthropic not found' });
}
// Get API key
let authHeader;
try {
const clientAuthFromXApiKey = req.headers['x-api-key']
? `Bearer ${req.headers['x-api-key']}`
: null;
authHeader = await getApiKey(req.headers.authorization || clientAuthFromXApiKey);
} catch (error) {
logError('Failed to get API key', error);
return res.status(500).json({
error: 'API key not available',
message: 'Failed to get or refresh API key. Please check server logs.'
});
}
const clientHeaders = req.headers;
// Get provider from model config
const provider = getModelProvider(modelId);
const headers = getAnthropicHeaders(authHeader, clientHeaders, false, modelId, provider);
// 构建 count_tokens 端点 URL
const countTokensUrl = endpoint.base_url.replace('/v1/messages', '/v1/messages/count_tokens');
// 更新请求体中的模型ID为重定向后的ID
const modifiedRequest = { ...anthropicRequest, model: modelId };
logInfo(`Forwarding to count_tokens endpoint: ${countTokensUrl}`);
logRequest('POST', countTokensUrl, headers, modifiedRequest);
const proxyAgentInfo = getNextProxyAgent(countTokensUrl);
const fetchOptions = {
method: 'POST',
headers,
body: JSON.stringify(modifiedRequest)
};
if (proxyAgentInfo?.agent) {
fetchOptions.agent = proxyAgentInfo.agent;
}
const response = await fetch(countTokensUrl, fetchOptions);
logInfo(`Response status: ${response.status}`);
if (!response.ok) {
const errorText = await response.text();
logError(`Count tokens error: ${response.status}`, new Error(errorText));
return res.status(response.status).json({
error: `Endpoint returned ${response.status}`,
details: errorText
});
}
const data = await response.json();
logResponse(200, null, data);
res.json(data);
} catch (error) {
logError('Error in /v1/messages/count_tokens', error);
res.status(500).json({
error: 'Internal server error',
message: error.message
});
}
}
// 注册路由
router.post('/v1/chat/completions', handleChatCompletions);
router.post('/v1/responses', handleDirectResponses);
router.post('/v1/messages', handleDirectMessages);
router.post('/v1/messages/count_tokens', handleCountTokens);
export default router;