chenbhao commited on
Commit
607aefb
·
1 Parent(s): c13a998

test: WebSearchTool log

Browse files
src/tools/WebSearchTool/WebSearchTool.ts CHANGED
@@ -286,9 +286,10 @@ export const WebSearchTool = buildTool({
286
  },
287
 
288
  async checkPermissions(): Promise<PermissionResult> {
289
- return {
290
- behavior: 'allow',
291
- }
 
292
  },
293
 
294
  async prompt() {
@@ -321,6 +322,7 @@ export const WebSearchTool = buildTool({
321
 
322
  logToMarkdown('INFO', 'WebSearchTool.call() 被调用')
323
  logToMarkdown('DEBUG', '输入参数', input)
 
324
 
325
  // 断言:input 必须存在
326
  console.assert(
@@ -391,6 +393,7 @@ export const WebSearchTool = buildTool({
391
  }
392
  logToMarkdown('INFO', '搜索成功完成')
393
  logToMarkdown('INFO', '最终统计', finalStats)
 
394
 
395
  return {
396
  query: input.query,
@@ -418,7 +421,12 @@ export const WebSearchTool = buildTool({
418
  },
419
 
420
  mapToolResultToToolResultBlockParam(output, toolUseID) {
 
 
 
 
421
  if (!output) {
 
422
  return {
423
  tool_use_id: toolUseID,
424
  type: 'tool_result',
@@ -426,22 +434,50 @@ export const WebSearchTool = buildTool({
426
  }
427
  }
428
 
 
 
 
 
429
  let text = `Results for "${output.query}"\n\n`
430
 
431
- for (const r of output.results) {
 
 
 
 
 
 
 
432
  if (typeof r === 'string') {
433
  text += r + '\n\n'
 
434
  } else {
435
- r.content.forEach((item: any, i: number) => {
436
- text += `${i + 1}. ${item.title}\n${item.url}\n${item.snippet || ''}\n\n`
 
 
 
 
 
 
 
 
 
437
  })
438
  }
439
  }
440
 
441
- return {
442
  tool_use_id: toolUseID,
443
  type: 'tool_result',
444
  content: text,
445
  }
 
 
 
 
 
 
 
446
  },
447
  }) satisfies ToolDef<any, Output, WebSearchProgress>
 
286
  },
287
 
288
  async checkPermissions(): Promise<PermissionResult> {
289
+ logToMarkdown('DEBUG', 'checkPermissions() 被调用')
290
+ const result = { behavior: 'allow' as const }
291
+ logToMarkdown('DEBUG', '权限检查结果', result)
292
+ return result
293
  },
294
 
295
  async prompt() {
 
322
 
323
  logToMarkdown('INFO', 'WebSearchTool.call() 被调用')
324
  logToMarkdown('DEBUG', '输入参数', input)
325
+ logToMarkdown('DEBUG', '上下文信息', { hasContext: !!_context, hasCanUseTool: !!_canUseTool, hasParentMessage: !!_parentMessage, hasOnProgress: !!onProgress })
326
 
327
  // 断言:input 必须存在
328
  console.assert(
 
393
  }
394
  logToMarkdown('INFO', '搜索成功完成')
395
  logToMarkdown('INFO', '最终统计', finalStats)
396
+ logToMarkdown('DEBUG', '返回结果结构', output)
397
 
398
  return {
399
  query: input.query,
 
421
  },
422
 
423
  mapToolResultToToolResultBlockParam(output, toolUseID) {
424
+ logToMarkdown('INFO', 'mapToolResultToToolResultBlockParam() 被调用')
425
+ logToMarkdown('DEBUG', 'toolUseID', toolUseID)
426
+ logToMarkdown('DEBUG', 'output 参数', output)
427
+
428
  if (!output) {
429
+ logToMarkdown('WARN', 'output 为空,返回错误')
430
  return {
431
  tool_use_id: toolUseID,
432
  type: 'tool_result',
 
434
  }
435
  }
436
 
437
+ logToMarkdown('DEBUG', '开始格式化结果')
438
+ logToMarkdown('DEBUG', '查询内容', output.query)
439
+ logToMarkdown('DEBUG', '结果数量', output.results.length)
440
+
441
  let text = `Results for "${output.query}"\n\n`
442
 
443
+ for (let i = 0; i < output.results.length; i++) {
444
+ const r = output.results[i]
445
+ logToMarkdown('DEBUG', `处理结果 ${i + 1}/${output.results.length}`, {
446
+ type: typeof r,
447
+ isString: typeof r === 'string',
448
+ hasContent: !!(r as any).content
449
+ })
450
+
451
  if (typeof r === 'string') {
452
  text += r + '\n\n'
453
+ logToMarkdown('DEBUG', `结果 ${i + 1} 是字符串:`, r)
454
  } else {
455
+ const content = (r as any).content
456
+ logToMarkdown('DEBUG', `结果 ${i + 1} 是对象,内容数量:`, content?.length || 0)
457
+
458
+ content.forEach((item: any, j: number) => {
459
+ const itemText = `${j + 1}. ${item.title}\n${item.url}\n${item.snippet || ''}\n\n`
460
+ text += itemText
461
+ logToMarkdown('DEBUG', `添加项目 ${j + 1}:`, {
462
+ hasTitle: !!item.title,
463
+ hasUrl: !!item.url,
464
+ hasSnippet: !!item.snippet
465
+ })
466
  })
467
  }
468
  }
469
 
470
+ const finalResult = {
471
  tool_use_id: toolUseID,
472
  type: 'tool_result',
473
  content: text,
474
  }
475
+
476
+ logToMarkdown('INFO', '格式化完成')
477
+ logToMarkdown('DEBUG', '最终结果类型', typeof finalResult.content)
478
+ logToMarkdown('DEBUG', '最终结果长度', finalResult.content.length)
479
+ logToMarkdown('DEBUG', '最终结果预览 (前100字符)', finalResult.content.slice(0, 100))
480
+
481
+ return finalResult
482
  },
483
  }) satisfies ToolDef<any, Output, WebSearchProgress>
src/tools/WebSearchTool/WebSearchTool.ts.backup ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
2
+ import { z } from 'zod/v4'
3
+ import { buildTool, type ToolDef } from '../../Tool.js'
4
+ import { lazySchema } from '../../utils/lazySchema.js'
5
+ import { logError } from '../../utils/log.js'
6
+ import { getWebSearchPrompt, WEB_SEARCH_TOOL_NAME } from './prompt.js'
7
+ import {
8
+ getToolUseSummary,
9
+ renderToolResultMessage,
10
+ renderToolUseMessage,
11
+ renderToolUseProgressMessage,
12
+ } from './UI.js'
13
+ import { readFileSync, writeFileSync, existsSync } from 'fs'
14
+ import { join } from 'path'
15
+
16
+ /**
17
+ * 日志记录函数 - 将日志写入 log.md 文件
18
+ */
19
+ function logToMarkdown(level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG', message: string, data?: any) {
20
+ const timestamp = new Date().toISOString()
21
+ const emoji = {
22
+ INFO: '🔵',
23
+ WARN: '⚠️',
24
+ ERROR: '❌',
25
+ DEBUG: '🔍'
26
+ }[level]
27
+
28
+ let logEntry = `\n[${timestamp}] [${level}] ${emoji} ${message}`
29
+
30
+ if (data !== undefined) {
31
+ if (typeof data === 'string') {
32
+ logEntry += `\n\`\`\`\n${data}\n\`\`\``
33
+ } else {
34
+ logEntry += `\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``
35
+ }
36
+ }
37
+
38
+ const logPath = join(process.cwd(), 'log.md')
39
+
40
+ try {
41
+ // 追加日志到文件
42
+ if (existsSync(logPath)) {
43
+ const existingContent = readFileSync(logPath, 'utf-8')
44
+ writeFileSync(logPath, existingContent + logEntry, 'utf-8')
45
+ } else {
46
+ writeFileSync(logPath, logEntry, 'utf-8')
47
+ }
48
+ } catch (error) {
49
+ // 如果写入日志失败,仍然输出到控制台
50
+ console.error(`[WebSearch] 无法写入日志文件: ${error}`)
51
+ }
52
+
53
+ // 同时输出到控制台
54
+ console.log(`[WebSearch] ${level}: ${message}`, data !== undefined ? data : '')
55
+ }
56
+
57
+ const inputSchema = lazySchema(() =>
58
+ z.strictObject({
59
+ query: z.string().min(2).describe('The search query to use'),
60
+ }),
61
+ )
62
+
63
+ type Input = z.infer<ReturnType<typeof inputSchema>>
64
+
65
+ const searchResultSchema = lazySchema(() => {
66
+ const searchHitSchema = z.object({
67
+ title: z.string(),
68
+ url: z.string(),
69
+ snippet: z.string().optional(),
70
+ })
71
+
72
+ return z.object({
73
+ tool_use_id: z.string(),
74
+ content: z.array(searchHitSchema),
75
+ })
76
+ })
77
+
78
+ export type SearchResult = z.infer<ReturnType<typeof searchResultSchema>>
79
+
80
+ const outputSchema = lazySchema(() =>
81
+ z.object({
82
+ query: z.string(),
83
+ results: z.array(z.union([searchResultSchema(), z.string()])),
84
+ durationSeconds: z.number(),
85
+ }),
86
+ )
87
+
88
+ export type Output = z.infer<ReturnType<typeof outputSchema>>
89
+
90
+ export type { WebSearchProgress } from '../../types/tools.js'
91
+ import type { WebSearchProgress } from '../../types/tools.js'
92
+
93
+ /**
94
+ * ✅ 使用 SearXNG 本地搜索
95
+ */
96
+ async function searchSearXNG(
97
+ query: string
98
+ ): Promise<Array<{ title: string; url: string; snippet?: string }>> {
99
+ logToMarkdown('INFO', '开始搜索 SearXNG')
100
+ logToMarkdown('DEBUG', '搜索查询', query)
101
+
102
+ // 断言:查询参数必须有效
103
+ console.assert(
104
+ typeof query === 'string' && query.trim().length > 0,
105
+ `[WebSearch] ❌ 无效的查询参数: ${JSON.stringify(query)}`
106
+ )
107
+
108
+ try {
109
+ const url = new URL('http://localhost:8080/search')
110
+ url.searchParams.set('q', query)
111
+ url.searchParams.set('format', 'json')
112
+
113
+ const finalUrl = url.toString()
114
+ logToMarkdown('DEBUG', '请求 URL', finalUrl)
115
+
116
+ const controller = new AbortController()
117
+ const timeout = setTimeout(() => controller.abort(), 10000)
118
+
119
+ logToMarkdown('INFO', '发起 HTTP 请求 (10秒超时)')
120
+ const startTime = Date.now()
121
+ const res = await fetch(finalUrl, {
122
+ signal: controller.signal,
123
+ headers: {
124
+ 'User-Agent': 'Mozilla/5.0 (compatible; WebSearchTool/1.0)',
125
+ 'Accept': 'application/json',
126
+ },
127
+ })
128
+ const elapsed = Date.now() - startTime
129
+
130
+ clearTimeout(timeout)
131
+
132
+ const statusInfo = `${res.status} ${res.statusText} (${elapsed}ms)`
133
+ logToMarkdown('INFO', 'HTTP 响应状态', statusInfo)
134
+ logToMarkdown('DEBUG', '响应头 Content-Type', res.headers.get('content-type'))
135
+
136
+ // 断言:HTTP 状态码应该是 2xx
137
+ console.assert(
138
+ res.ok,
139
+ `[WebSearch] ❌ HTTP 请求失败: ${res.status} ${res.statusText}`
140
+ )
141
+
142
+ if (!res.ok) {
143
+ const errorText = await res.text()
144
+ logToMarkdown('ERROR', '错误响应内容', errorText)
145
+ throw new Error(`HTTP ${res.status}: ${errorText}`)
146
+ }
147
+
148
+ logToMarkdown('INFO', '解析 JSON 响应')
149
+ const data = await res.json()
150
+
151
+ const resultStats = {
152
+ query: data.query,
153
+ totalResults: data.number_of_results,
154
+ resultsCount: data.results?.length || 0,
155
+ }
156
+ logToMarkdown('INFO', '返回结果统计', resultStats)
157
+
158
+ // 断言:响应数据应该包含 results 数组
159
+ console.assert(
160
+ Array.isArray(data.results),
161
+ `[WebSearch] ❌ 响应数据格式错误: results 不是数组, 类型: ${typeof data.results}`
162
+ )
163
+
164
+ const results = (data.results || [])
165
+ .slice(0, 10)
166
+ .map((r: any, index: number) => {
167
+ // 断言:每个结果必须有 title 和 url
168
+ console.assert(
169
+ r.title && r.url,
170
+ `[WebSearch] ⚠️ 结果 ${index} 缺少必要字段: ${JSON.stringify(r)}`
171
+ )
172
+
173
+ return {
174
+ title: r.title,
175
+ url: r.url,
176
+ snippet: r.content,
177
+ }
178
+ })
179
+
180
+ logToMarkdown('INFO', `搜索完成,返回 ${results.length} 条结果`)
181
+ return results
182
+ } catch (error) {
183
+ const errorMessage = error instanceof Error ? error.message : String(error)
184
+ const errorStack = error instanceof Error ? error.stack : undefined
185
+
186
+ logToMarkdown('ERROR', '搜索失败', errorMessage)
187
+ if (errorStack) {
188
+ logToMarkdown('ERROR', '错误堆栈', errorStack)
189
+ }
190
+
191
+ logError('SearXNG search failed', error)
192
+
193
+ // 根据错误类型提供更详细的诊断信息
194
+ if (error instanceof Error) {
195
+ if (error.name === 'AbortError') {
196
+ const timeoutReasons = [
197
+ 'SearXNG 服务响应过慢',
198
+ '网络连接问题',
199
+ 'SearXNG 服务未运行'
200
+ ]
201
+ logToMarkdown('ERROR', '请求超时 (10秒),可能原因', timeoutReasons)
202
+ } else if (error.message.includes('ECONNREFUSED')) {
203
+ const connectionReasons = [
204
+ 'SearXNG 服务未启动',
205
+ '端口 8080 未开放',
206
+ '防火墙阻止连接'
207
+ ]
208
+ logToMarkdown('ERROR', '连接被拒绝,可能原因', connectionReasons)
209
+ } else if (error.message.includes('HTTP 403')) {
210
+ const forbiddenReasons = [
211
+ 'SearXNG 配置不允许 JSON 格式',
212
+ 'IP 被限制',
213
+ '需要认证'
214
+ ]
215
+ logToMarkdown('ERROR', 'HTTP 403 禁止访问,可能原因', forbiddenReasons)
216
+ }
217
+ }
218
+
219
+ throw new Error(
220
+ `SearXNG search failed: ${errorMessage}`
221
+ )
222
+ }
223
+ }
224
+
225
+ /**
226
+ * 文本清洗
227
+ */
228
+ function stripTags(text: string): string {
229
+ return text
230
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
231
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
232
+ .replace(/<[^>]+>/g, '')
233
+ .replace(/&amp;/g, '&')
234
+ .replace(/&lt;/g, '<')
235
+ .replace(/&gt;/g, '>')
236
+ .replace(/&quot;/g, '"')
237
+ .replace(/&#39;/g, "'")
238
+ .replace(/&nbsp;/g, ' ')
239
+ .trim()
240
+ }
241
+
242
+ function normalizeText(text: string): string {
243
+ return text.replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim()
244
+ }
245
+
246
+ function cleanSearchResult(result: any) {
247
+ return {
248
+ title: result.title ? normalizeText(stripTags(result.title)) : undefined,
249
+ snippet: result.snippet ? normalizeText(stripTags(result.snippet)) : undefined,
250
+ }
251
+ }
252
+
253
+ export const WebSearchTool = buildTool({
254
+ name: WEB_SEARCH_TOOL_NAME,
255
+ description: 'Search the web using local SearXNG',
256
+
257
+ getToolUseSummary,
258
+ getActivityDescription(input) {
259
+ return input?.query ? `Searching for "${input.query}"` : 'Searching the web'
260
+ },
261
+
262
+ isEnabled() {
263
+ const enabled = true
264
+ logToMarkdown('DEBUG', `isEnabled() = ${enabled}`)
265
+ return enabled
266
+ },
267
+
268
+ get inputSchema() {
269
+ return inputSchema()
270
+ },
271
+
272
+ get outputSchema() {
273
+ return outputSchema()
274
+ },
275
+
276
+ isConcurrencySafe() {
277
+ return true
278
+ },
279
+
280
+ isReadOnly() {
281
+ return true
282
+ },
283
+
284
+ toAutoClassifierInput(input) {
285
+ return input?.query ?? ''
286
+ },
287
+
288
+ async checkPermissions(): Promise<PermissionResult> {
289
+ return {
290
+ behavior: 'allow',
291
+ }
292
+ },
293
+
294
+ async prompt() {
295
+ return getWebSearchPrompt()
296
+ },
297
+
298
+ renderToolUseMessage,
299
+ renderToolUseProgressMessage,
300
+ renderToolResultMessage,
301
+
302
+ extractSearchText() {
303
+ return ''
304
+ },
305
+
306
+ async validateInput(input) {
307
+ logToMarkdown('DEBUG', 'validateInput() 被调用')
308
+ logToMarkdown('DEBUG', '输入参数', input)
309
+
310
+ if (!input?.query) {
311
+ logToMarkdown('ERROR', '验证失败: Missing query')
312
+ return { result: false, message: 'Missing query', errorCode: 1 }
313
+ }
314
+
315
+ logToMarkdown('INFO', '验证通过')
316
+ return { result: true }
317
+ },
318
+
319
+ async call(input, _context, _canUseTool, _parentMessage, onProgress) {
320
+ const start = performance.now()
321
+
322
+ logToMarkdown('INFO', 'WebSearchTool.call() 被调用')
323
+ logToMarkdown('DEBUG', '输入参数', input)
324
+
325
+ // 断言:input 必须存在
326
+ console.assert(
327
+ input !== null && input !== undefined,
328
+ `[WebSearch] ❌ input 参数为 null/undefined`
329
+ )
330
+
331
+ try {
332
+ // 验证查询参数
333
+ if (!input?.query || input.query.trim() === '') {
334
+ logToMarkdown('WARN', '查询参数为空,返回错误')
335
+ return {
336
+ query: input?.query || '',
337
+ results: ['Error: Missing query'],
338
+ durationSeconds: (performance.now() - start) / 1000,
339
+ }
340
+ }
341
+
342
+ // 断言:查询长度必须至少 2 个字符(根据 inputSchema 定义)
343
+ console.assert(
344
+ input.query.trim().length >= 2,
345
+ `[WebSearch] ❌ 查询长度不足: "${input.query}" (长度: ${input.query.length})`
346
+ )
347
+
348
+ logToMarkdown('INFO', `查询参数验证通过: "${input.query}"`)
349
+
350
+ if (onProgress) {
351
+ logToMarkdown('DEBUG', '发送进度更新')
352
+ onProgress({
353
+ toolUseID: 'search-start',
354
+ data: { type: 'query_update', query: input.query },
355
+ })
356
+ }
357
+
358
+ logToMarkdown('INFO', '调用 searchSearXNG()')
359
+ const results = await searchSearXNG(input.query)
360
+
361
+ logToMarkdown('INFO', '清洗搜索结果')
362
+ const cleaned = results.map(r => {
363
+ const cleanedResult = cleanSearchResult(r)
364
+ logToMarkdown('DEBUG', '清洗结果', {
365
+ originalTitle: r.title,
366
+ cleanedTitle: cleanedResult.title,
367
+ hasSnippet: !!r.snippet,
368
+ })
369
+ return {
370
+ ...r,
371
+ ...cleanedResult,
372
+ }
373
+ })
374
+
375
+ const output =
376
+ cleaned.length === 0
377
+ ? [`No results for: ${input.query}`]
378
+ : [
379
+ {
380
+ tool_use_id: 'search-1',
381
+ content: cleaned,
382
+ },
383
+ ]
384
+
385
+ const duration = (performance.now() - start) / 1000
386
+
387
+ const finalStats = {
388
+ query: input.query,
389
+ resultsCount: cleaned.length,
390
+ durationSeconds: duration.toFixed(3),
391
+ }
392
+ logToMarkdown('INFO', '搜索成功完成')
393
+ logToMarkdown('INFO', '最终统计', finalStats)
394
+
395
+ return {
396
+ query: input.query,
397
+ results: output,
398
+ durationSeconds: duration,
399
+ }
400
+ } catch (error) {
401
+ const duration = (performance.now() - start) / 1000
402
+ const errorMessage = error instanceof Error ? error.message : String(error)
403
+
404
+ logToMarkdown('ERROR', 'WebSearchTool.call() 发生异常')
405
+ logToMarkdown('ERROR', '异常信息', errorMessage)
406
+ if (error instanceof Error && error.stack) {
407
+ logToMarkdown('ERROR', '异常堆栈', error.stack)
408
+ }
409
+
410
+ logError(error)
411
+
412
+ return {
413
+ query: input?.query || '',
414
+ results: [`Error: ${errorMessage}`],
415
+ durationSeconds: duration,
416
+ }
417
+ }
418
+ },
419
+
420
+ mapToolResultToToolResultBlockParam(output, toolUseID) {
421
+ if (!output) {
422
+ return {
423
+ tool_use_id: toolUseID,
424
+ type: 'tool_result',
425
+ content: 'Error',
426
+ }
427
+ }
428
+
429
+ let text = `Results for "${output.query}"\n\n`
430
+
431
+ for (const r of output.results) {
432
+ if (typeof r === 'string') {
433
+ text += r + '\n\n'
434
+ } else {
435
+ r.content.forEach((item: any, i: number) => {
436
+ text += `${i + 1}. ${item.title}\n${item.url}\n${item.snippet || ''}\n\n`
437
+ })
438
+ }
439
+ }
440
+
441
+ return {
442
+ tool_use_id: toolUseID,
443
+ type: 'tool_result',
444
+ content: text,
445
+ }
446
+ },
447
+ }) satisfies ToolDef<any, Output, WebSearchProgress>