Skip to content

Commit 7bbd6d1

Browse files
committed
🐛 修复 web_fetch/web_search 合约不一致
1 parent 40e7eb0 commit 7bbd6d1

File tree

4 files changed

+55
-32
lines changed

4 files changed

+55
-32
lines changed

src/app/service/agent/core/tools/web_fetch.test.ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,19 @@ describe("WebFetchExecutor", () => {
4848
await expect(executor.execute({})).rejects.toThrow('缺少必填参数 "url"');
4949
});
5050

51+
it("should throw for missing prompt", async () => {
52+
const executor = new WebFetchExecutor(mockSender);
53+
await expect(executor.execute({ url: "https://example.com" })).rejects.toThrow('缺少必填参数 "prompt"');
54+
});
55+
5156
it("should throw for invalid url", async () => {
5257
const executor = new WebFetchExecutor(mockSender);
53-
await expect(executor.execute({ url: "not-a-url" })).rejects.toThrow("Invalid URL");
58+
await expect(executor.execute({ url: "not-a-url", prompt: "fetch" })).rejects.toThrow("Invalid URL");
5459
});
5560

5661
it("should throw for non-http protocol", async () => {
5762
const executor = new WebFetchExecutor(mockSender);
58-
await expect(executor.execute({ url: "ftp://example.com" })).rejects.toThrow("Only http/https");
63+
await expect(executor.execute({ url: "ftp://example.com", prompt: "fetch" })).rejects.toThrow("Only http/https");
5964
});
6065

6166
it("should handle JSON response", async () => {
@@ -67,7 +72,9 @@ describe("WebFetchExecutor", () => {
6772
vi.stubGlobal("fetch", mockFetch);
6873

6974
const executor = new WebFetchExecutor(mockSender);
70-
const result = JSON.parse((await executor.execute({ url: "https://api.example.com/data" })) as string);
75+
const result = JSON.parse(
76+
(await executor.execute({ url: "https://api.example.com/data", prompt: "extract data" })) as string
77+
);
7178

7279
expect(result.content_type).toBe("json");
7380
expect(JSON.parse(result.content)).toEqual({ key: "value" });
@@ -84,7 +91,9 @@ describe("WebFetchExecutor", () => {
8491
mockExtractReturnValue = "Hello World long content here for testing extracted properly by offscreen";
8592

8693
const executor = new WebFetchExecutor(mockSender);
87-
const result = JSON.parse((await executor.execute({ url: "https://example.com" })) as string);
94+
const result = JSON.parse(
95+
(await executor.execute({ url: "https://example.com", prompt: "extract content" })) as string
96+
);
8897

8998
expect(result.content_type).toBe("html");
9099
expect(result.content).toContain("Hello World");
@@ -100,7 +109,9 @@ describe("WebFetchExecutor", () => {
100109
mockExtractReturnValue = null;
101110

102111
const executor = new WebFetchExecutor(mockSender);
103-
const result = JSON.parse((await executor.execute({ url: "https://example.com" })) as string);
112+
const result = JSON.parse(
113+
(await executor.execute({ url: "https://example.com", prompt: "extract content" })) as string
114+
);
104115

105116
expect(result.content_type).toBe("text");
106117
expect(result.content).toBe("Simple text");
@@ -115,7 +126,9 @@ describe("WebFetchExecutor", () => {
115126
vi.stubGlobal("fetch", mockFetch);
116127

117128
const executor = new WebFetchExecutor(mockSender);
118-
const result = JSON.parse((await executor.execute({ url: "https://example.com", max_length: 50 })) as string);
129+
const result = JSON.parse(
130+
(await executor.execute({ url: "https://example.com", prompt: "extract content", max_length: 50 })) as string
131+
);
119132

120133
expect(result.content.length).toBe(50);
121134
expect(result.truncated).toBe(true);
@@ -130,7 +143,7 @@ describe("WebFetchExecutor", () => {
130143
vi.stubGlobal("fetch", mockFetch);
131144

132145
const executor = new WebFetchExecutor(mockSender);
133-
await expect(executor.execute({ url: "https://example.com" })).rejects.toThrow("HTTP 404");
146+
await expect(executor.execute({ url: "https://example.com", prompt: "fetch" })).rejects.toThrow("HTTP 404");
134147
});
135148

136149
it("should fallback to stripHtmlTags when offscreen extraction throws", async () => {
@@ -143,7 +156,9 @@ describe("WebFetchExecutor", () => {
143156
mockExtractShouldThrow = true;
144157

145158
const executor = new WebFetchExecutor(mockSender);
146-
const result = JSON.parse((await executor.execute({ url: "https://example.com" })) as string);
159+
const result = JSON.parse(
160+
(await executor.execute({ url: "https://example.com", prompt: "extract content" })) as string
161+
);
147162

148163
expect(result.content_type).toBe("text");
149164
expect(result.content).toBe("Fallback content");
@@ -159,7 +174,9 @@ describe("WebFetchExecutor", () => {
159174
mockExtractReturnValue = "Hi"; // shorter than 50 chars
160175

161176
const executor = new WebFetchExecutor(mockSender);
162-
const result = JSON.parse((await executor.execute({ url: "https://example.com" })) as string);
177+
const result = JSON.parse(
178+
(await executor.execute({ url: "https://example.com", prompt: "extract content" })) as string
179+
);
163180

164181
expect(result.content_type).toBe("text");
165182
expect(result.content).toBe("Hi");
@@ -174,7 +191,9 @@ describe("WebFetchExecutor", () => {
174191
vi.stubGlobal("fetch", mockFetch);
175192

176193
const executor = new WebFetchExecutor(mockSender);
177-
const result = JSON.parse((await executor.execute({ url: "https://api.example.com" })) as string);
194+
const result = JSON.parse(
195+
(await executor.execute({ url: "https://api.example.com", prompt: "extract data" })) as string
196+
);
178197

179198
// Should fall back to text
180199
expect(result.content_type).toBe("text");
@@ -193,7 +212,9 @@ describe("WebFetchExecutor", () => {
193212
mockExtractReturnValue = "Long enough content for extraction to work properly and pass the threshold";
194213

195214
const executor = new WebFetchExecutor(mockSender);
196-
const result = JSON.parse((await executor.execute({ url: "https://example.com" })) as string);
215+
const result = JSON.parse(
216+
(await executor.execute({ url: "https://example.com", prompt: "extract content" })) as string
217+
);
197218

198219
expect(result.content_type).toBe("html");
199220
expect(mockSender.sendMessage).toHaveBeenCalled();
@@ -208,7 +229,9 @@ describe("WebFetchExecutor", () => {
208229
vi.stubGlobal("fetch", mockFetch);
209230

210231
const executor = new WebFetchExecutor(mockSender);
211-
const result = JSON.parse((await executor.execute({ url: "https://example.com/file.txt" })) as string);
232+
const result = JSON.parse(
233+
(await executor.execute({ url: "https://example.com/file.txt", prompt: "extract content" })) as string
234+
);
212235

213236
expect(result.content_type).toBe("text");
214237
expect(result.content).toBe("Just plain text");
@@ -224,7 +247,7 @@ describe("WebFetchExecutor", () => {
224247
vi.stubGlobal("fetch", mockFetch);
225248

226249
const executor = new WebFetchExecutor(mockSender);
227-
await executor.execute({ url: "https://example.com" });
250+
await executor.execute({ url: "https://example.com", prompt: "fetch" });
228251

229252
expect(mockFetch).toHaveBeenCalledWith("https://example.com", {
230253
headers: { "User-Agent": "Mozilla/5.0 (compatible; ScriptCat Agent)" },
@@ -242,7 +265,7 @@ describe("WebFetchExecutor", () => {
242265
vi.stubGlobal("fetch", mockFetch);
243266

244267
const executor = new WebFetchExecutor(mockSender);
245-
const result = JSON.parse((await executor.execute({ url: "https://example.com" })) as string);
268+
const result = JSON.parse((await executor.execute({ url: "https://example.com", prompt: "fetch" })) as string);
246269

247270
expect(result.final_url).toBe("https://example.com/redirected");
248271
});
@@ -257,7 +280,7 @@ describe("WebFetchExecutor", () => {
257280
vi.stubGlobal("fetch", mockFetch);
258281

259282
const executor = new WebFetchExecutor(mockSender);
260-
const result = JSON.parse((await executor.execute({ url: "https://example.com" })) as string);
283+
const result = JSON.parse((await executor.execute({ url: "https://example.com", prompt: "fetch" })) as string);
261284

262285
expect(result.final_url).toBeUndefined();
263286
});
@@ -272,7 +295,7 @@ describe("WebFetchExecutor", () => {
272295
vi.stubGlobal("fetch", mockFetch);
273296

274297
const executor = new WebFetchExecutor(mockSender);
275-
const result = JSON.parse((await executor.execute({ url: "https://example.com" })) as string);
298+
const result = JSON.parse((await executor.execute({ url: "https://example.com", prompt: "fetch" })) as string);
276299

277300
expect(result.content.length).toBe(15000);
278301
expect(result.truncated).toBe(false);

src/app/service/agent/core/tools/web_fetch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class WebFetchExecutor implements ToolExecutor {
4949

5050
async execute(args: Record<string, unknown>): Promise<string> {
5151
const url = requireString(args, "url");
52-
const prompt = args.prompt as string | undefined;
52+
const prompt = requireString(args, "prompt");
5353
const maxLength = optionalNumber(args, "max_length");
5454

5555
// 校验 URL
@@ -122,7 +122,7 @@ export class WebFetchExecutor implements ToolExecutor {
122122
}
123123

124124
// LLM 摘要
125-
if (prompt && this.summarize) {
125+
if (this.summarize) {
126126
content = await this.summarize(content, prompt);
127127
truncated = false;
128128
}

src/app/service/agent/core/tools/web_search.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ describe("WebSearchExecutor", () => {
4646
const executor = new WebSearchExecutor(mockSender, createMockConfigRepo("duckduckgo"));
4747
const result = JSON.parse((await executor.execute({ query: "test search" })) as string);
4848

49-
expect(result).toHaveLength(2);
50-
expect(result[0].title).toBe("Result 1");
49+
expect(result.results).toHaveLength(2);
50+
expect(result.results[0].title).toBe("Result 1");
5151
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining("html.duckduckgo.com"), expect.any(Object));
5252
});
5353

@@ -68,7 +68,7 @@ describe("WebSearchExecutor", () => {
6868
const executor = new WebSearchExecutor(mockSender, createMockConfigRepo("duckduckgo"));
6969
const result = JSON.parse((await executor.execute({ query: "test", max_results: 3 })) as string);
7070

71-
expect(result).toHaveLength(3);
71+
expect(result.results).toHaveLength(3);
7272
});
7373

7474
it("should cap max_results at 10", async () => {
@@ -88,7 +88,7 @@ describe("WebSearchExecutor", () => {
8888
const executor = new WebSearchExecutor(mockSender, createMockConfigRepo("duckduckgo"));
8989
const result = JSON.parse((await executor.execute({ query: "test", max_results: 20 })) as string);
9090

91-
expect(result).toHaveLength(10);
91+
expect(result.results).toHaveLength(10);
9292
});
9393

9494
it("should search Google Custom Search API", async () => {
@@ -104,8 +104,8 @@ describe("WebSearchExecutor", () => {
104104
const executor = new WebSearchExecutor(mockSender, createMockConfigRepo("google_custom"));
105105
const result = JSON.parse((await executor.execute({ query: "google test" })) as string);
106106

107-
expect(result).toHaveLength(1);
108-
expect(result[0].title).toBe("Google Result");
107+
expect(result.results).toHaveLength(1);
108+
expect(result.results[0].title).toBe("Google Result");
109109
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining("googleapis.com/customsearch"), expect.any(Object));
110110
});
111111

@@ -156,7 +156,7 @@ describe("WebSearchExecutor", () => {
156156
const executor = new WebSearchExecutor(mockSender, createMockConfigRepo("google_custom"));
157157
const result = JSON.parse((await executor.execute({ query: "test" })) as string);
158158

159-
expect(result).toEqual([]);
159+
expect(result.results).toEqual([]);
160160
});
161161

162162
it("should pass AbortSignal to DuckDuckGo fetch for 15s timeout", async () => {
@@ -225,8 +225,8 @@ describe("WebSearchExecutor", () => {
225225
const executor = new WebSearchExecutor(mockSender, createMockConfigRepo("bing"));
226226
const result = JSON.parse((await executor.execute({ query: "bing test" })) as string);
227227

228-
expect(result).toHaveLength(1);
229-
expect(result[0].title).toBe("Bing Result");
228+
expect(result.results).toHaveLength(1);
229+
expect(result.results[0].title).toBe("Bing Result");
230230
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining("bing.com/search"), expect.any(Object));
231231
});
232232

@@ -273,8 +273,8 @@ describe("WebSearchExecutor", () => {
273273
const executor = new WebSearchExecutor(mockSender, createMockConfigRepo("baidu"));
274274
const result = JSON.parse((await executor.execute({ query: "百度测试" })) as string);
275275

276-
expect(result).toHaveLength(1);
277-
expect(result[0].title).toBe("Baidu Result");
276+
expect(result.results).toHaveLength(1);
277+
expect(result.results[0].title).toBe("Baidu Result");
278278
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining("baidu.com/s"), expect.any(Object));
279279
});
280280

@@ -306,6 +306,6 @@ describe("WebSearchExecutor", () => {
306306
const executor = new WebSearchExecutor(mockSender, createMockConfigRepo("duckduckgo"));
307307
const result = JSON.parse((await executor.execute({ query: "test" })) as string);
308308

309-
expect(result).toHaveLength(5);
309+
expect(result.results).toHaveLength(5);
310310
});
311311
});

src/app/service/agent/core/tools/web_search.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function formatSearchResults(
3737
warning: `Result extraction failed or timed out (engine: ${engine}). Try a different search engine or rephrase the query.`,
3838
});
3939
}
40-
return JSON.stringify(results);
40+
return JSON.stringify({ results });
4141
}
4242

4343
/** 搜索结果条目类型 */
@@ -135,6 +135,6 @@ export class WebSearchExecutor implements ToolExecutor {
135135
snippet: item.snippet || "",
136136
}));
137137

138-
return JSON.stringify(results.slice(0, maxResults));
138+
return formatSearchResults(results.slice(0, maxResults), false, "Google");
139139
}
140140
}

0 commit comments

Comments
 (0)