diff --git a/packages/core/src/planner.test.ts b/packages/core/src/planner.test.ts index b7efcf25..7f680692 100644 --- a/packages/core/src/planner.test.ts +++ b/packages/core/src/planner.test.ts @@ -176,6 +176,36 @@ describe('Planner LLM-based plan generation', () => { expect(createStep).toBeDefined(); }); + it('preserves LLM-planned web_fetch actions and URL params', async () => { + const planner = createPlanner({ useLLM: true }); + mockLLMGeneratePlan(planner, async () => ({ + summary: '查阅官方文档', + steps: [ + { + description: '抓取 API 文档', + action: 'web_fetch', + tool: 'web_fetch', + phase: '阶段1-分析', + params: defaultParams({ url: 'https://docs.example.com/api' }), + reasoning: '确认外部 API 行为', + needsCodeGeneration: false, + }, + ], + risks: [], + alternatives: [], + })); + + const result = await planner.plan(createTask({ type: 'create' }), emptyContext(), []); + + const webFetchStep = result.plan!.steps.find((s) => s.action === 'web_fetch'); + expect(result.needsMoreContext).toBe(false); + expect(webFetchStep).toMatchObject({ + action: 'web_fetch', + tool: 'web_fetch', + params: expect.objectContaining({ url: 'https://docs.example.com/api' }), + }); + }); + it('injects project instructions into the planning prompt context', async () => { const planner = createPlanner({ useLLM: true }); const calls: Array<{ context: string }> = []; @@ -343,6 +373,44 @@ describe('Planner step ordering and dependencies', () => { expect(searchStep!.dependencies).not.toContain(readStep!.stepId); }); + it('allows web_fetch to share the read-only planning dependency policy', async () => { + const planner = createPlanner({ useLLM: true }); + mockLLMGeneratePlan(planner, async () => ({ + summary: '查阅资料并分析代码', + steps: [ + { + description: '抓取官方文档', + action: 'web_fetch', + tool: 'web_fetch', + phase: '阶段1-分析', + params: defaultParams({ url: 'https://docs.example.com/api' }), + reasoning: '确认外部 API 行为', + needsCodeGeneration: false, + }, + { + description: '搜索本地调用', + action: 'search_code', + tool: 'search_code', + phase: '阶段1-分析', + params: defaultParams({ query: 'createClient', maxResults: 10 }), + reasoning: '定位本地调用方式', + needsCodeGeneration: false, + }, + ], + risks: [], + alternatives: [], + })); + + const result = await planner.plan(createTask({ type: 'create' }), emptyContext(), []); + + const steps = result.plan!.steps; + const webFetchStep = steps.find((s) => s.action === 'web_fetch'); + const searchStep = steps.find((s) => s.action === 'search_code'); + expect(webFetchStep).toBeDefined(); + expect(searchStep).toBeDefined(); + expect(searchStep!.dependencies).not.toContain(webFetchStep!.stepId); + }); + it('generates correct dependencies for modify task (rule-based)', async () => { const planner = createPlanner({ useLLM: false }); const result = await planner.plan( diff --git a/packages/core/src/planner.ts b/packages/core/src/planner.ts index ced4f252..04641e27 100644 --- a/packages/core/src/planner.ts +++ b/packages/core/src/planner.ts @@ -335,7 +335,7 @@ export class Planner { } private isReadOnlyPlanningAction(action: ExecutionStep['action']): boolean { - return ['read_file', 'search_code', 'list_directory', 'get_ast'].includes(action); + return ['read_file', 'search_code', 'list_directory', 'get_ast', 'web_fetch'].includes(action); } /** @@ -355,6 +355,7 @@ export class Planner { browser_click: 'browser_click', browser_type: 'browser_type', browser_screenshot: 'browser_screenshot', + web_fetch: 'web_fetch', }; return actionMap[action] ?? 'read_file'; diff --git a/packages/core/src/security.test.ts b/packages/core/src/security.test.ts index 376ecb43..7bb0011c 100644 --- a/packages/core/src/security.test.ts +++ b/packages/core/src/security.test.ts @@ -295,6 +295,17 @@ describe('SecurityManager', () => { expect(result.reasonCode).toBe('unknown_tool_requires_approval'); expect(result.riskLevel).toBe('medium'); }); + + it('asks for approval for web_fetch by default', () => { + const result = security.evaluate({ + toolName: 'web_fetch', + args: { url: 'https://docs.example.com/api' }, + projectRoot, + }); + expect(result.decision).toBe('ask'); + expect(result.reasonCode).toBe('unknown_tool_requires_approval'); + expect(result.riskLevel).toBe('medium'); + }); }); // ------------------------------------------------------------------------- diff --git a/packages/shared/src/types/task.ts b/packages/shared/src/types/task.ts index 03273d82..a77067fe 100644 --- a/packages/shared/src/types/task.ts +++ b/packages/shared/src/types/task.ts @@ -19,6 +19,7 @@ export type ActionType = | 'browser_type' | 'browser_screenshot' | 'get_page_structure' + | 'web_fetch' | 'filesense_sync_and_summarize' | 'filesense_query' | 'filesense_navigate';