diff --git a/src/converter.ts b/src/converter.ts index d605fd1..5ff64c8 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -248,21 +248,113 @@ export async function convertToCursorRequest(req: AnthropicRequest): Promise CORE_TOOL_PATTERNS.some(p => p.test(name)); + + // 分类:核心编程工具 vs 第三方工具(MCP/Skills/Plugins) + const coreTools = tools.filter(t => isCoreToolName(t.name)); + const thirdPartyTools = tools.filter(t => !isCoreToolName(t.name)); + + // 为工具生成示例参数 + const makeExampleParams = (tool: AnthropicTool): Record => { + if (/^(Read|read_file|ReadFile)$/i.test(tool.name)) return { file_path: 'src/index.ts' }; + if (/^(Bash|execute_command|RunCommand|run_command)$/i.test(tool.name)) return { command: 'ls -la' }; + if (/^(Write|write_to_file|WriteFile|write_file)$/i.test(tool.name)) return { file_path: 'output.txt', content: '...' }; + if (/^(ListDir|list_dir|list_directory|ListDirectory|list_files)$/i.test(tool.name)) return { path: '.' }; + if (/^(Search|search_files|SearchFiles|grep_search|codebase_search)$/i.test(tool.name)) return { query: 'TODO' }; + if (/^(Edit|edit_file|EditFile|replace_in_file)$/i.test(tool.name)) return { file_path: 'src/main.ts', old_text: 'old', new_text: 'new' }; + // 第三方工具:从 schema 中提取前 2 个参数名 + if (tool.input_schema?.properties) { + return Object.fromEntries( + Object.entries(tool.input_schema.properties as Record) + .slice(0, 2) + .map(([k, v]) => [k, v.type === 'boolean' ? true : v.type === 'number' ? 1 : 'value']) + ); + } + return { input: 'value' }; + }; + + // 选取 few-shot 工具集:按工具来源/命名空间分组,每个组选一个代表 + // 确保 MCP 工具、Skills、Plugins 等不同类别各有代表 (#67) + const fewShotTools: AnthropicTool[] = []; + + // 1) 核心工具:优先 Read,其次 Bash const readTool = tools.find(t => /^(Read|read_file|ReadFile)$/i.test(t.name)); - const bashTool = tools.find(t => /^(Bash|execute_command|RunCommand)$/i.test(t.name)); - const fewShotTool = readTool || bashTool || tools[0]; - const fewShotParams = fewShotTool.name.match(/^(Read|read_file|ReadFile)$/i) - ? { file_path: 'src/index.ts' } - : fewShotTool.name.match(/^(Bash|execute_command|RunCommand)$/i) - ? { command: 'ls -la' } - : fewShotTool.input_schema?.properties - ? Object.fromEntries( - Object.entries(fewShotTool.input_schema.properties as Record) - .slice(0, 2) - .map(([k]) => [k, 'value']) - ) - : { input: 'value' }; + const bashTool = tools.find(t => /^(Bash|execute_command|RunCommand|run_command)$/i.test(t.name)); + if (readTool) fewShotTools.push(readTool); + else if (bashTool) fewShotTools.push(bashTool); + else if (coreTools.length > 0) fewShotTools.push(coreTools[0]); + + // 2) 第三方工具:按命名空间/来源分组,每组取一个代表 + // 命名空间提取规则: + // - MCP 工具: "mcp__server__tool" → namespace = "mcp__server" + // - 双下划线分隔: "prefix__action" → namespace = "prefix" + // - 蛇形命名: "prefix_action_name" → namespace = 第一段 + // - 驼峰命名: "PrefixActionName" → namespace = 驼峰前缀 + // - 其他: 用工具名自身 + const getToolNamespace = (name: string): string => { + // MCP 双下划线格式: mcp__server__tool → "mcp__server" + const mcpMatch = name.match(/^(mcp__[^_]+)/); + if (mcpMatch) return mcpMatch[1]; + // 通用双下划线: prefix__tool → "prefix" + const doubleUnder = name.match(/^([^_]+)__/); + if (doubleUnder) return doubleUnder[1]; + // 蛇形命名前缀: some_prefix_action → "some"(至少要有2段才取前缀) + const snakeParts = name.split('_'); + if (snakeParts.length >= 3) return snakeParts[0]; + // 驼峰命名前缀: SuperPowersDoSomething → "SuperPowers" + const camelMatch = name.match(/^([A-Z][a-z]+(?:[A-Z][a-z]+)?)/); + if (camelMatch && camelMatch[1] !== name) return camelMatch[1]; + // 兜底:整个工具名就是 namespace + return name; + }; + + // 按 namespace 分组 + const namespaceGroups = new Map(); + for (const tp of thirdPartyTools) { + const ns = getToolNamespace(tp.name); + if (!namespaceGroups.has(ns)) namespaceGroups.set(ns, []); + namespaceGroups.get(ns)!.push(tp); + } + + // 每个 namespace 选一个代表(优先选有描述的) + const MAX_THIRDPARTY_FEWSHOT = 4; // 最多 4 个第三方工具代表 + const namespaceEntries = [...namespaceGroups.entries()] + .sort((a, b) => b[1].length - a[1].length); // 工具多的 namespace 优先 + + for (const [ns, nsTools] of namespaceEntries) { + if (fewShotTools.length >= 1 + MAX_THIRDPARTY_FEWSHOT) break; // 1 核心 + N 第三方 + // 选该 namespace 中描述最长的工具作为代表 + const representative = nsTools.sort((a, b) => + (b.description?.length || 0) - (a.description?.length || 0) + )[0]; + fewShotTools.push(representative); + } + + // 如果连一个都没选到,用 tools[0] + if (fewShotTools.length === 0 && tools.length > 0) { + fewShotTools.push(tools[0]); + } + + if (thirdPartyTools.length > 0) { + console.log(`[Converter] Few-shot 工具选择: ${fewShotTools.map(t => t.name).join(', ')} (${namespaceGroups.size} 个命名空间, ${thirdPartyTools.length} 个第三方工具)`); + } + + // 构建多工具 few-shot 回复 + const fewShotActions = fewShotTools.map(t => + `\`\`\`json action\n${JSON.stringify({ tool: t.name, parameters: makeExampleParams(t) }, null, 2)}\n\`\`\`` + ).join('\n\n'); // 自然的 few-shot:模拟一次真实的 IDE 交互 messages.push({ @@ -272,10 +364,9 @@ export async function convertToCursorRequest(req: AnthropicRequest): Promise 标签 // few-shot 是让模型遵循输出格式最强力的手段 - const fewShotAction = `\`\`\`json action\n${JSON.stringify({ tool: fewShotTool.name, parameters: fewShotParams }, null, 2)}\n\`\`\``; const fewShotResponse = thinkingEnabled - ? `\nThe user wants me to help with their project. I should start by examining the project structure to understand what we're working with.\n\n\nLet me start by examining the project structure.\n\n${fewShotAction}` - : `Understood. I'll use the structured format for actions. Here's how I'll respond:\n\n${fewShotAction}`; + ? `\nThe user wants me to help with their project. I should start by examining the project structure and using the available tools to understand what we're working with.\n\n\nLet me start by using multiple tools to gather information.\n\n${fewShotActions}` + : `Understood. I'll use all available actions as appropriate. Here are my first steps:\n\n${fewShotActions}`; messages.push({ parts: [{ type: 'text', text: fewShotResponse }], id: shortId(), @@ -293,7 +384,7 @@ export async function convertToCursorRequest(req: AnthropicRequest): Promise