mirror of
https://github.com/7836246/cursor2api.git
synced 2026-09-03 07:20:02 +08:00
fix: 支持 Roo Code + 请求重试 + 调试日志
- CORE_TOOL_NAMES 同时覆盖 Claude Code 和 Roo Code 工具名 - 添加请求重试机制(最多2次,间隔2s) - 动态 few-shot 示例(适应不同客户端工具名) - 每条用户消息追加格式提醒 - 添加原始响应调试日志
This commit is contained in:
@@ -20,12 +20,18 @@ import type {
|
||||
} from './types.js';
|
||||
import { getConfig } from './config.js';
|
||||
|
||||
// Claude Code 传大量 MCP 工具(~94个),只保留核心工具降低上下文大小
|
||||
// 核心工具白名单 — 同时支持 Claude Code 和 Roo Code 工具名
|
||||
const CORE_TOOL_NAMES = new Set([
|
||||
// Claude Code
|
||||
'Bash', 'Read', 'Write', 'Edit', 'MultiEdit',
|
||||
'Glob', 'Grep', 'Agent',
|
||||
'WebFetch', 'WebSearch', 'AskFollowupQuestion',
|
||||
'TodoRead', 'TodoWrite',
|
||||
// Roo Code
|
||||
'execute_command', 'read_file', 'write_to_file', 'apply_diff',
|
||||
'search_files', 'list_files', 'read_command_output',
|
||||
'ask_followup_question', 'attempt_completion',
|
||||
'switch_mode', 'new_task', 'update_todo_list', 'skill',
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -33,8 +39,7 @@ const CORE_TOOL_NAMES = new Set([
|
||||
*/
|
||||
function filterCoreTools(tools: AnthropicTool[]): AnthropicTool[] {
|
||||
const filtered = tools.filter(t => CORE_TOOL_NAMES.has(t.name));
|
||||
// 如果过滤后为空(可能工具名不匹配),回退到所有工具,但限制数量
|
||||
if (filtered.length === 0) return tools.slice(0, 15);
|
||||
if (filtered.length === 0) return tools.slice(0, 20);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
@@ -53,9 +58,8 @@ function buildToolInstructions(tools: AnthropicTool[]): string {
|
||||
return `- ${tool.name}(${params})`;
|
||||
}).join('\n');
|
||||
|
||||
return `You are being used as an API backend. You must respond ONLY in the following structured format when the user asks you to perform an action. Do not add explanations outside the format.
|
||||
return `You are being used as an API backend. EVERY response you give MUST include at least one action block in this format:
|
||||
|
||||
Format:
|
||||
<antml_tool_call>
|
||||
<tool_name>ACTION_NAME</tool_name>
|
||||
<tool_input>
|
||||
@@ -66,7 +70,11 @@ Format:
|
||||
Available actions:
|
||||
${toolList}
|
||||
|
||||
If you want to provide text before an action, that's fine, but always include the action block when performing an operation.`;
|
||||
CRITICAL RULES:
|
||||
1. EVERY response MUST contain at least one action block. Never respond with only text.
|
||||
2. You may include explanatory text before your action block, but must always end with an action.
|
||||
3. If the user asks a question and you want to provide information, use attempt_completion or ask_followup_question action.
|
||||
4. If the user asks you to do something, use the appropriate action.`;
|
||||
}
|
||||
|
||||
// ==================== 请求转换 ====================
|
||||
@@ -88,27 +96,34 @@ export function convertToCursorRequest(req: AnthropicRequest): CursorChatRequest
|
||||
|
||||
const toolInstructions = buildToolInstructions(coreTools);
|
||||
|
||||
// 动态选取第一个工具做 few-shot 示例
|
||||
const exampleTool = coreTools[0];
|
||||
const exampleParams = exampleTool.input_schema?.properties
|
||||
? Object.fromEntries(
|
||||
Object.entries(exampleTool.input_schema.properties as Record<string, { type?: string }>)
|
||||
.slice(0, 2)
|
||||
.map(([k]) => [k, 'example_value'])
|
||||
)
|
||||
: { input: 'example' };
|
||||
|
||||
// 3 轮 few-shot in-context learning
|
||||
// 1. 用户给出格式要求
|
||||
messages.push({
|
||||
parts: [{ type: 'text', text: toolInstructions }],
|
||||
id: shortId(),
|
||||
role: 'user',
|
||||
});
|
||||
// 2. 助手同意并给出示例
|
||||
messages.push({
|
||||
parts: [{ type: 'text', text: 'Understood. Here is an example of how I will respond:\n\n<antml_tool_call>\n<tool_name>Bash</tool_name>\n<tool_input>\n{"command": "echo hello"}\n</tool_input>\n</antml_tool_call>\n\nI will always use this exact format. What do you need?' }],
|
||||
parts: [{ type: 'text', text: `Understood. Here is how I will always respond:\n\n<antml_tool_call>\n<tool_name>${exampleTool.name}</tool_name>\n<tool_input>\n${JSON.stringify(exampleParams)}\n</tool_input>\n</antml_tool_call>\n\nEvery response will include an action block. What do you need?` }],
|
||||
id: shortId(),
|
||||
role: 'assistant',
|
||||
});
|
||||
// 3. 用户确认,过渡到实际任务
|
||||
messages.push({
|
||||
parts: [{ type: 'text', text: 'Perfect format. Now here is my actual request:' }],
|
||||
parts: [{ type: 'text', text: 'Perfect. Now here is my actual request:' }],
|
||||
id: shortId(),
|
||||
role: 'user',
|
||||
});
|
||||
messages.push({
|
||||
parts: [{ type: 'text', text: 'Ready to help. I will use the structured format for all actions.' }],
|
||||
parts: [{ type: 'text', text: 'Ready. Every response will include an action block.' }],
|
||||
id: shortId(),
|
||||
role: 'assistant',
|
||||
});
|
||||
@@ -116,9 +131,14 @@ export function convertToCursorRequest(req: AnthropicRequest): CursorChatRequest
|
||||
|
||||
// 转换实际的用户/助手消息
|
||||
for (const msg of req.messages) {
|
||||
const text = extractMessageText(msg);
|
||||
let text = extractMessageText(msg);
|
||||
if (!text) continue;
|
||||
|
||||
// 在每条用户消息末尾追加格式提醒
|
||||
if (hasTools && msg.role === 'user') {
|
||||
text += '\n\n[Remember: respond with <antml_tool_call> action blocks]';
|
||||
}
|
||||
|
||||
messages.push({
|
||||
parts: [{ type: 'text', text }],
|
||||
id: shortId(),
|
||||
|
||||
@@ -172,11 +172,33 @@ export async function getXIsHumanToken(): Promise<string> {
|
||||
// ==================== API 请求 ====================
|
||||
|
||||
/**
|
||||
* 发送请求到 Cursor /api/chat 并以流式方式处理响应
|
||||
* 发送请求到 Cursor /api/chat 并以流式方式处理响应(带重试)
|
||||
*/
|
||||
export async function sendCursorRequest(
|
||||
req: CursorChatRequest,
|
||||
onChunk: (event: CursorSSEEvent) => void,
|
||||
): Promise<void> {
|
||||
const maxRetries = 2;
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
await sendCursorRequestInner(req, onChunk);
|
||||
return;
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[Cursor] 请求失败 (${attempt}/${maxRetries}): ${msg}`);
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`[Cursor] ${2}s 后重试...`);
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendCursorRequestInner(
|
||||
req: CursorChatRequest,
|
||||
onChunk: (event: CursorSSEEvent) => void,
|
||||
): Promise<void> {
|
||||
const token = await getXIsHumanToken();
|
||||
const headers = getChromeHeaders(token);
|
||||
|
||||
@@ -222,6 +222,8 @@ async function handleNonStream(res: Response, cursorReq: ReturnType<typeof conve
|
||||
const fullText = await sendCursorRequestFull(cursorReq);
|
||||
const hasTools = (body.tools?.length ?? 0) > 0;
|
||||
|
||||
console.log(`[Handler] 原始响应 (${fullText.length} chars): ${fullText.substring(0, 300)}...`);
|
||||
|
||||
const contentBlocks: AnthropicContentBlock[] = [];
|
||||
let stopReason = 'end_turn';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user