chenbhao commited on
Commit
eb3a949
·
1 Parent(s): e5056d6

chore: WebFetchTool note

Browse files
src/tools/WebFetchTool/UI.tsx CHANGED
@@ -1,10 +1,12 @@
1
  /* TUI/CLI 中渲染 Web 工具调用过程的 UI 组件集合, 基于 React(类似于Ink的终端 React 框架)
2
- * 显示
3
- * 「工具调用参数」
4
- * 「工具执行中」状态
5
- * 「工具返回结果」
6
  * 本质
7
  * 「Tool UI Renderer」工具可视化层
 
 
 
 
 
 
8
  */
9
  import React from 'react';
10
  import { MessageResponse } from '../../components/MessageResponse.js'; // UI 组件: 用来包裹一条“消息" (类似聊天气泡)
@@ -47,7 +49,7 @@ export function renderToolUseProgressMessage(
47
  // 无参数函数
48
  ): React.ReactNode {
49
  return <MessageResponse height={1}>
50
- <Text dimColor>Fetching…</Text>
51
  </MessageResponse>; // 渲染一个消息容器, 高度 1 行, 显示灰色文本, 最后结束组件
52
  }
53
 
@@ -64,25 +66,27 @@ export function renderToolResultMessage({
64
  }: {
65
  verbose: boolean; // 是否详细模式
66
  }): React.ReactNode { // 返回 React 节点
67
- const formattedSize = formatFileSize(bytes);
68
- if (verbose) {
69
  return <Box flexDirection="column">
70
- <MessageResponse height={1}>
71
- <Text>
72
- Received <Text bold>{formattedSize}</Text> ({code} {codeText})
73
- </Text>
74
- </MessageResponse>
75
- <Box flexDirection="column">
76
- <Text>{result}</Text>
77
- </Box>
78
- </Box>;
79
  }
80
  return <MessageResponse height={1}>
81
  <Text>
82
  Received <Text bold>{formattedSize}</Text> ({code} {codeText})
83
  </Text>
84
- </MessageResponse>;
85
  }
 
 
86
  export function getToolUseSummary(input: Partial<{
87
  url: string;
88
  prompt: string;
@@ -90,6 +94,6 @@ export function getToolUseSummary(input: Partial<{
90
  if (!input?.url) {
91
  return null;
92
  }
93
- return truncate(input.url, TOOL_SUMMARY_MAX_LENGTH);
94
  }
95
  //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIk1lc3NhZ2VSZXNwb25zZSIsIlRPT0xfU1VNTUFSWV9NQVhfTEVOR1RIIiwiQm94IiwiVGV4dCIsIlRvb2xQcm9ncmVzc0RhdGEiLCJQcm9ncmVzc01lc3NhZ2UiLCJmb3JtYXRGaWxlU2l6ZSIsInRydW5jYXRlIiwiT3V0cHV0IiwicmVuZGVyVG9vbFVzZU1lc3NhZ2UiLCJ1cmwiLCJwcm9tcHQiLCJQYXJ0aWFsIiwidmVyYm9zZSIsInRoZW1lIiwiUmVhY3ROb2RlIiwicmVuZGVyVG9vbFVzZVByb2dyZXNzTWVzc2FnZSIsInJlbmRlclRvb2xSZXN1bHRNZXNzYWdlIiwiYnl0ZXMiLCJjb2RlIiwiY29kZVRleHQiLCJyZXN1bHQiLCJfcHJvZ3Jlc3NNZXNzYWdlc0Zvck1lc3NhZ2UiLCJmb3JtYXR0ZWRTaXplIiwiZ2V0VG9vbFVzZVN1bW1hcnkiLCJpbnB1dCJdLCJzb3VyY2VzIjpbIlVJLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBNZXNzYWdlUmVzcG9uc2UgfSBmcm9tICcuLi8uLi9jb21wb25lbnRzL01lc3NhZ2VSZXNwb25zZS5qcydcbmltcG9ydCB7IFRPT0xfU1VNTUFSWV9NQVhfTEVOR1RIIH0gZnJvbSAnLi4vLi4vY29uc3RhbnRzL3Rvb2xMaW1pdHMuanMnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQgdHlwZSB7IFRvb2xQcm9ncmVzc0RhdGEgfSBmcm9tICcuLi8uLi9Ub29sLmpzJ1xuaW1wb3J0IHR5cGUgeyBQcm9ncmVzc01lc3NhZ2UgfSBmcm9tICcuLi8uLi90eXBlcy9tZXNzYWdlLmpzJ1xuaW1wb3J0IHsgZm9ybWF0RmlsZVNpemUsIHRydW5jYXRlIH0gZnJvbSAnLi4vLi4vdXRpbHMvZm9ybWF0LmpzJ1xuaW1wb3J0IHR5cGUgeyBPdXRwdXQgfSBmcm9tICcuL1dlYkZldGNoVG9vbC5qcydcblxuZXhwb3J0IGZ1bmN0aW9uIHJlbmRlclRvb2xVc2VNZXNzYWdlKFxuICB7IHVybCwgcHJvbXB0IH06IFBhcnRpYWw8eyB1cmw6IHN0cmluZzsgcHJvbXB0OiBzdHJpbmcgfT4sXG4gIHsgdmVyYm9zZSB9OiB7IHRoZW1lPzogc3RyaW5nOyB2ZXJib3NlOiBib29sZWFuIH0sXG4pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBpZiAoIXVybCkge1xuICAgIHJldHVybiBudWxsXG4gIH1cbiAgaWYgKHZlcmJvc2UpIHtcbiAgICByZXR1cm4gYHVybDogXCIke3VybH1cIiR7dmVyYm9zZSAmJiBwcm9tcHQgPyBgLCBwcm9tcHQ6IFwiJHtwcm9tcHR9XCJgIDogJyd9YFxuICB9XG4gIHJldHVybiB1cmxcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHJlbmRlclRvb2xVc2VQcm9ncmVzc01lc3NhZ2UoKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgcmV0dXJuIChcbiAgICA8TWVzc2FnZVJlc3BvbnNlIGhlaWdodD17MX0+XG4gICAgICA8VGV4dCBkaW1Db2xvcj5GZXRjaGluZ+KApjwvVGV4dD5cbiAgICA8L01lc3NhZ2VSZXNwb25zZT5cbiAgKVxufVxuXG5leHBvcnQgZnVuY3Rpb24gcmVuZGVyVG9vbFJlc3VsdE1lc3NhZ2UoXG4gIHsgYnl0ZXMsIGNvZGUsIGNvZGVUZXh0LCByZXN1bHQgfTogT3V0cHV0LFxuICBfcHJvZ3Jlc3NNZXNzYWdlc0Zvck1lc3NhZ2U6IFByb2dyZXNzTWVzc2FnZTxUb29sUHJvZ3Jlc3NEYXRhPltdLFxuICB7IHZlcmJvc2UgfTogeyB2ZXJib3NlOiBib29sZWFuIH0sXG4pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBmb3JtYXR0ZWRTaXplID0gZm9ybWF0RmlsZVNpemUoYnl0ZXMpXG4gIGlmICh2ZXJib3NlKSB7XG4gICAgcmV0dXJuIChcbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICA8TWVzc2FnZVJlc3BvbnNlIGhlaWdodD17MX0+XG4gICAgICAgICAgPFRleHQ+XG4gICAgICAgICAgICBSZWNlaXZlZCA8VGV4dCBib2xkPntmb3JtYXR0ZWRTaXplfTwvVGV4dD4gKHtjb2RlfSB7Y29kZVRleHR9KVxuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgPC9NZXNzYWdlUmVzcG9uc2U+XG4gICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICAgIDxUZXh0PntyZXN1bHR9PC9UZXh0PlxuICAgICAgICA8L0JveD5cbiAgICAgIDwvQm94PlxuICAgIClcbiAgfVxuICByZXR1cm4gKFxuICAgIDxNZXNzYWdlUmVzcG9uc2UgaGVpZ2h0PXsxfT5cbiAgICAgIDxUZXh0PlxuICAgICAgICBSZWNlaXZlZCA8VGV4dCBib2xkPntmb3JtYXR0ZWRTaXplfTwvVGV4dD4gKHtjb2RlfSB7Y29kZVRleHR9KVxuICAgICAgPC9UZXh0PlxuICAgIDwvTWVzc2FnZVJlc3BvbnNlPlxuICApXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBnZXRUb29sVXNlU3VtbWFyeShcbiAgaW5wdXQ6IFBhcnRpYWw8eyB1cmw6IHN0cmluZzsgcHJvbXB0OiBzdHJpbmcgfT4gfCB1bmRlZmluZWQsXG4pOiBzdHJpbmcgfCBudWxsIHtcbiAgaWYgKCFpbnB1dD8udXJsKSB7XG4gICAgcmV0dXJuIG51bGxcbiAgfVxuICByZXR1cm4gdHJ1bmNhdGUoaW5wdXQudXJsLCBUT09MX1NVTU1BUllfTUFYX0xFTkdUSClcbn1cbiJdLCJtYXBwaW5ncyI6IkFBQUEsT0FBT0EsS0FBSyxNQUFNLE9BQU87QUFDekIsU0FBU0MsZUFBZSxRQUFRLHFDQUFxQztBQUNyRSxTQUFTQyx1QkFBdUIsUUFBUSwrQkFBK0I7QUFDdkUsU0FBU0MsR0FBRyxFQUFFQyxJQUFJLFFBQVEsY0FBYztBQUN4QyxjQUFjQyxnQkFBZ0IsUUFBUSxlQUFlO0FBQ3JELGNBQWNDLGVBQWUsUUFBUSx3QkFBd0I7QUFDN0QsU0FBU0MsY0FBYyxFQUFFQyxRQUFRLFFBQVEsdUJBQXVCO0FBQ2hFLGNBQWND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
 
1
  /* TUI/CLI 中渲染 Web 工具调用过程的 UI 组件集合, 基于 React(类似于Ink的终端 React 框架)
 
 
 
 
2
  * 本质
3
  * 「Tool UI Renderer」工具可视化层
4
+ * 生命周期
5
+ * 「工具调用参数」renderToolUseMessage (开始)
6
+ * 「工具执行中」状态 renderToolUseProgressMessage (加载中)
7
+ * 「工具返回结果」renderToolResultMessage (结束)
8
+ * 核心变量
9
+ * verbose 控制 详细模式 用户模式 vs 调试模式
10
  */
11
  import React from 'react';
12
  import { MessageResponse } from '../../components/MessageResponse.js'; // UI 组件: 用来包裹一条“消息" (类似聊天气泡)
 
49
  // 无参数函数
50
  ): React.ReactNode {
51
  return <MessageResponse height={1}>
52
+ <Text dimColor>Fetching…</Text>
53
  </MessageResponse>; // 渲染一个消息容器, 高度 1 行, 显示灰色文本, 最后结束组件
54
  }
55
 
 
66
  }: {
67
  verbose: boolean; // 是否详细模式
68
  }): React.ReactNode { // 返回 React 节点
69
+ const formattedSize = formatFileSize(bytes); // 转换bytes: 1024 -> 1KB
70
+ if (verbose) { // 如果是 详细模式
71
  return <Box flexDirection="column">
72
+ <MessageResponse height={1}>
73
+ <Text>
74
+ Received <Text bold>{formattedSize}</Text> ({code} {codeText})
75
+ </Text>
76
+ </MessageResponse>
77
+ <Box flexDirection="column">
78
+ <Text>{result}</Text>
79
+ </Box>
80
+ </Box>; // 垂直布局, 显示实际内容 (网页文本)
81
  }
82
  return <MessageResponse height={1}>
83
  <Text>
84
  Received <Text bold>{formattedSize}</Text> ({code} {codeText})
85
  </Text>
86
+ </MessageResponse>; // 如果不是详细模式, 不显示实际内容
87
  }
88
+
89
+ // 获取工具使用摘要
90
  export function getToolUseSummary(input: Partial<{
91
  url: string;
92
  prompt: string;
 
94
  if (!input?.url) {
95
  return null;
96
  }
97
+ return truncate(input.url, TOOL_SUMMARY_MAX_LENGTH); // 截断 url, eg: https://very-long-url.com/abc...(限制长度)
98
  }
99
  //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIk1lc3NhZ2VSZXNwb25zZSIsIlRPT0xfU1VNTUFSWV9NQVhfTEVOR1RIIiwiQm94IiwiVGV4dCIsIlRvb2xQcm9ncmVzc0RhdGEiLCJQcm9ncmVzc01lc3NhZ2UiLCJmb3JtYXRGaWxlU2l6ZSIsInRydW5jYXRlIiwiT3V0cHV0IiwicmVuZGVyVG9vbFVzZU1lc3NhZ2UiLCJ1cmwiLCJwcm9tcHQiLCJQYXJ0aWFsIiwidmVyYm9zZSIsInRoZW1lIiwiUmVhY3ROb2RlIiwicmVuZGVyVG9vbFVzZVByb2dyZXNzTWVzc2FnZSIsInJlbmRlclRvb2xSZXN1bHRNZXNzYWdlIiwiYnl0ZXMiLCJjb2RlIiwiY29kZVRleHQiLCJyZXN1bHQiLCJfcHJvZ3Jlc3NNZXNzYWdlc0Zvck1lc3NhZ2UiLCJmb3JtYXR0ZWRTaXplIiwiZ2V0VG9vbFVzZVN1bW1hcnkiLCJpbnB1dCJdLCJzb3VyY2VzIjpbIlVJLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBNZXNzYWdlUmVzcG9uc2UgfSBmcm9tICcuLi8uLi9jb21wb25lbnRzL01lc3NhZ2VSZXNwb25zZS5qcydcbmltcG9ydCB7IFRPT0xfU1VNTUFSWV9NQVhfTEVOR1RIIH0gZnJvbSAnLi4vLi4vY29uc3RhbnRzL3Rvb2xMaW1pdHMuanMnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQgdHlwZSB7IFRvb2xQcm9ncmVzc0RhdGEgfSBmcm9tICcuLi8uLi9Ub29sLmpzJ1xuaW1wb3J0IHR5cGUgeyBQcm9ncmVzc01lc3NhZ2UgfSBmcm9tICcuLi8uLi90eXBlcy9tZXNzYWdlLmpzJ1xuaW1wb3J0IHsgZm9ybWF0RmlsZVNpemUsIHRydW5jYXRlIH0gZnJvbSAnLi4vLi4vdXRpbHMvZm9ybWF0LmpzJ1xuaW1wb3J0IHR5cGUgeyBPdXRwdXQgfSBmcm9tICcuL1dlYkZldGNoVG9vbC5qcydcblxuZXhwb3J0IGZ1bmN0aW9uIHJlbmRlclRvb2xVc2VNZXNzYWdlKFxuICB7IHVybCwgcHJvbXB0IH06IFBhcnRpYWw8eyB1cmw6IHN0cmluZzsgcHJvbXB0OiBzdHJpbmcgfT4sXG4gIHsgdmVyYm9zZSB9OiB7IHRoZW1lPzogc3RyaW5nOyB2ZXJib3NlOiBib29sZWFuIH0sXG4pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBpZiAoIXVybCkge1xuICAgIHJldHVybiBudWxsXG4gIH1cbiAgaWYgKHZlcmJvc2UpIHtcbiAgICByZXR1cm4gYHVybDogXCIke3VybH1cIiR7dmVyYm9zZSAmJiBwcm9tcHQgPyBgLCBwcm9tcHQ6IFwiJHtwcm9tcHR9XCJgIDogJyd9YFxuICB9XG4gIHJldHVybiB1cmxcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHJlbmRlclRvb2xVc2VQcm9ncmVzc01lc3NhZ2UoKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgcmV0dXJuIChcbiAgICA8TWVzc2FnZVJlc3BvbnNlIGhlaWdodD17MX0+XG4gICAgICA8VGV4dCBkaW1Db2xvcj5GZXRjaGluZ+KApjwvVGV4dD5cbiAgICA8L01lc3NhZ2VSZXNwb25zZT5cbiAgKVxufVxuXG5leHBvcnQgZnVuY3Rpb24gcmVuZGVyVG9vbFJlc3VsdE1lc3NhZ2UoXG4gIHsgYnl0ZXMsIGNvZGUsIGNvZGVUZXh0LCByZXN1bHQgfTogT3V0cHV0LFxuICBfcHJvZ3Jlc3NNZXNzYWdlc0Zvck1lc3NhZ2U6IFByb2dyZXNzTWVzc2FnZTxUb29sUHJvZ3Jlc3NEYXRhPltdLFxuICB7IHZlcmJvc2UgfTogeyB2ZXJib3NlOiBib29sZWFuIH0sXG4pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBmb3JtYXR0ZWRTaXplID0gZm9ybWF0RmlsZVNpemUoYnl0ZXMpXG4gIGlmICh2ZXJib3NlKSB7XG4gICAgcmV0dXJuIChcbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICA8TWVzc2FnZVJlc3BvbnNlIGhlaWdodD17MX0+XG4gICAgICAgICAgPFRleHQ+XG4gICAgICAgICAgICBSZWNlaXZlZCA8VGV4dCBib2xkPntmb3JtYXR0ZWRTaXplfTwvVGV4dD4gKHtjb2RlfSB7Y29kZVRleHR9KVxuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgPC9NZXNzYWdlUmVzcG9uc2U+XG4gICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICAgIDxUZXh0PntyZXN1bHR9PC9UZXh0PlxuICAgICAgICA8L0JveD5cbiAgICAgIDwvQm94PlxuICAgIClcbiAgfVxuICByZXR1cm4gKFxuICAgIDxNZXNzYWdlUmVzcG9uc2UgaGVpZ2h0PXsxfT5cbiAgICAgIDxUZXh0PlxuICAgICAgICBSZWNlaXZlZCA8VGV4dCBib2xkPntmb3JtYXR0ZWRTaXplfTwvVGV4dD4gKHtjb2RlfSB7Y29kZVRleHR9KVxuICAgICAgPC9UZXh0PlxuICAgIDwvTWVzc2FnZVJlc3BvbnNlPlxuICApXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBnZXRUb29sVXNlU3VtbWFyeShcbiAgaW5wdXQ6IFBhcnRpYWw8eyB1cmw6IHN0cmluZzsgcHJvbXB0OiBzdHJpbmcgfT4gfCB1bmRlZmluZWQsXG4pOiBzdHJpbmcgfCBudWxsIHtcbiAgaWYgKCFpbnB1dD8udXJsKSB7XG4gICAgcmV0dXJuIG51bGxcbiAgfVxuICByZXR1cm4gdHJ1bmNhdGUoaW5wdXQudXJsLCBUT09MX1NVTU1BUllfTUFYX0xFTkdUSClcbn1cbiJdLCJtYXBwaW5ncyI6IkFBQUEsT0FBT0EsS0FBSyxNQUFNLE9BQU87QUFDekIsU0FBU0MsZUFBZSxRQUFRLHFDQUFxQztBQUNyRSxTQUFTQyx1QkFBdUIsUUFBUSwrQkFBK0I7QUFDdkUsU0FBU0MsR0FBRyxFQUFFQyxJQUFJLFFBQVEsY0FBYztBQUN4QyxjQUFjQyxnQkFBZ0IsUUFBUSxlQUFlO0FBQ3JELGNBQWNDLGVBQWUsUUFBUSx3QkFBd0I7QUFDN0QsU0FBU0MsY0FBYyxFQUFFQyxRQUFRLFFBQVEsdUJBQXVCO0FBQ2hFLGNBQWND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
src/tools/WebFetchTool/WebFetchTool.ts CHANGED
@@ -1,106 +1,112 @@
1
- import { z } from 'zod/v4'
2
- import { buildTool, type ToolDef } from '../../Tool.js'
3
- import type { PermissionUpdate } from '../../types/permissions.js'
4
- import { formatFileSize } from '../../utils/format.js'
5
- import { lazySchema } from '../../utils/lazySchema.js'
6
- import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js'
7
- import { getRuleByContentsForTool } from '../../utils/permissions/permissions.js'
8
- import { isPreapprovedHost } from './preapproved.js'
9
- import { DESCRIPTION, WEB_FETCH_TOOL_NAME } from './prompt.js'
10
  import {
11
  getToolUseSummary,
12
  renderToolResultMessage,
13
  renderToolUseMessage,
14
  renderToolUseProgressMessage,
15
- } from './UI.js'
16
  import {
17
  applyPromptToMarkdown,
18
  type FetchedContent,
19
  getURLMarkdownContent,
20
  isPreapprovedUrl,
21
  MAX_MARKDOWN_LENGTH,
22
- } from './utils.js'
23
 
24
  const inputSchema = lazySchema(() =>
25
- z.strictObject({
26
- url: z.string().url().describe('The URL to fetch content from'),
27
- prompt: z.string().describe('The prompt to run on the fetched content'),
28
- }),
29
  )
30
- type InputSchema = ReturnType<typeof inputSchema>
 
31
 
32
  const outputSchema = lazySchema(() =>
33
  z.object({
34
- bytes: z.number().describe('Size of the fetched content in bytes'),
35
- code: z.number().describe('HTTP response code'),
36
- codeText: z.string().describe('HTTP response code text'),
37
  result: z
38
- .string()
39
  .describe('Processed result from applying the prompt to the content'),
40
  durationMs: z
41
- .number()
42
  .describe('Time taken to fetch and process the content'),
43
- url: z.string().describe('The URL that was fetched'),
44
  }),
45
  )
46
  type OutputSchema = ReturnType<typeof outputSchema>
47
 
48
- export type Output = z.infer<OutputSchema>
49
 
50
- function webFetchToolInputToPermissionRuleContent(input: {
51
  [k: string]: unknown
52
  }): string {
53
  try {
54
- const parsedInput = WebFetchTool.inputSchema.safeParse(input)
55
- if (!parsedInput.success) {
56
- return `input:${input.toString()}`
57
  }
58
  const { url } = parsedInput.data
59
- const hostname = new URL(url).hostname
60
- return `domain:${hostname}`
61
  } catch {
62
  return `input:${input.toString()}`
63
  }
64
  }
65
 
 
66
  export const WebFetchTool = buildTool({
67
- name: WEB_FETCH_TOOL_NAME,
68
- searchHint: 'fetch and extract content from a URL',
69
  // 100K chars - tool result persistence threshold
70
  maxResultSizeChars: 100_000,
71
  shouldDefer: true,
72
  async description(input) {
73
- const { url } = input as { url: string }
74
  try {
75
  const hostname = new URL(url).hostname
76
- return `Claude wants to fetch content from ${hostname}`
77
  } catch {
78
- return `Claude wants to fetch content from this URL`
79
  }
80
  },
81
  userFacingName() {
82
- return 'Fetch'
83
  },
 
84
  getToolUseSummary,
85
  getActivityDescription(input) {
86
  const summary = getToolUseSummary(input)
87
  return summary ? `Fetching ${summary}` : 'Fetching web page'
88
  },
 
89
  get inputSchema(): InputSchema {
90
  return inputSchema()
91
  },
92
  get outputSchema(): OutputSchema {
93
  return outputSchema()
94
  },
 
95
  isConcurrencySafe() {
96
- return true
97
  },
98
  isReadOnly() {
99
- return true
100
  },
101
  toAutoClassifierInput(input) {
102
  return input.prompt ? `${input.url}: ${input.prompt}` : input.url
103
  },
 
104
  async checkPermissions(input, context): Promise<PermissionDecision> {
105
  const appState = context.getAppState()
106
  const permissionContext = appState.toolPermissionContext
@@ -109,7 +115,7 @@ export const WebFetchTool = buildTool({
109
  try {
110
  const { url } = input as { url: string }
111
  const parsedUrl = new URL(url)
112
- if (isPreapprovedHost(parsedUrl.hostname, parsedUrl.pathname)) {
113
  return {
114
  behavior: 'allow',
115
  updatedInput: input,
@@ -147,7 +153,7 @@ export const WebFetchTool = buildTool({
147
  if (askRule) {
148
  return {
149
  behavior: 'ask',
150
- message: `Claude requested permissions to use ${WebFetchTool.name}, but you haven't granted it yet.`,
151
  decisionReason: {
152
  type: 'rule',
153
  rule: askRule,
@@ -174,7 +180,7 @@ export const WebFetchTool = buildTool({
174
 
175
  return {
176
  behavior: 'ask',
177
- message: `Claude requested permissions to use ${WebFetchTool.name}, but you haven't granted it yet.`,
178
  suggestions: buildSuggestions(ruleContent),
179
  }
180
  },
 
1
+ import { z } from 'zod/v4' // 引入 Zod: 定义 & 检验输入输出结构
2
+ import { buildTool, type ToolDef } from '../../Tool.js' // 构建工具对象, 工具类型约束
3
+ import type { PermissionUpdate } from '../../types/permissions.js' // 权限系统, 用于"添加规则" 的类型
4
+ import { formatFileSize } from '../../utils/format.js' // 字节 -> 可读格式(KB/MB)
5
+ import { lazySchema } from '../../utils/lazySchema.js' // 延迟初始化 schema (避免循环依赖)
6
+ import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js' // 权限检查返回结构
7
+ import { getRuleByContentsForTool } from '../../utils/permissions/permissions.js' // 根据规则内容查权限 (allow / deny / ask)
8
+ import { isPreapprovedHost } from './preapproved.js' // 判断域名是否白名单
9
+ import { DESCRIPTION, WEB_FETCH_TOOL_NAME } from './prompt.js' // 工具描述 & 名字
10
  import {
11
  getToolUseSummary,
12
  renderToolResultMessage,
13
  renderToolUseMessage,
14
  renderToolUseProgressMessage,
15
+ } from './UI.js' // UI 渲染函数
16
  import {
17
  applyPromptToMarkdown,
18
  type FetchedContent,
19
  getURLMarkdownContent,
20
  isPreapprovedUrl,
21
  MAX_MARKDOWN_LENGTH,
22
+ } from './utils.js' // 抓网页, 处理 markdown, 判断 url 是否可信
23
 
24
  const inputSchema = lazySchema(() =>
25
+ z.strictObject({ // 严格对象 不允许多字段
26
+ url: z.string().url().describe('The URL to fetch content from'), // url: string, 必须是合法 url
27
+ prompt: z.string().describe('The prompt to run on the fetched content'), // 对网页内容执行的任务 总结/提取
28
+ }), // 延迟创建 schema
29
  )
30
+
31
+ type InputSchema = ReturnType<typeof inputSchema> // 推导类型
32
 
33
  const outputSchema = lazySchema(() =>
34
  z.object({
35
+ bytes: z.number().describe('Size of the fetched content in bytes'), // 返回数据大小
36
+ code: z.number().describe('HTTP response code'), // HTTP 状态码
37
+ codeText: z.string().describe('HTTP response code text'), // 状态描述
38
  result: z
39
+ .string() // 最终结果
40
  .describe('Processed result from applying the prompt to the content'),
41
  durationMs: z
42
+ .number() // 执行耗时
43
  .describe('Time taken to fetch and process the content'),
44
+ url: z.string().describe('The URL that was fetched'), // url
45
  }),
46
  )
47
  type OutputSchema = ReturnType<typeof outputSchema>
48
 
49
+ export type Output = z.infer<OutputSchema> // 输出类型推导
50
 
51
+ function webFetchToolInputToPermissionRuleContent(input: { // 权限规则生成 input -> 权限规则key
52
  [k: string]: unknown
53
  }): string {
54
  try {
55
+ const parsedInput = WebFetchTool.inputSchema.safeParse(input) // 安全解析
56
+ if (!parsedInput.success) { // 解析失败
57
+ return `input:${input.toString()}` // fallback
58
  }
59
  const { url } = parsedInput.data
60
+ const hostname = new URL(url).hostname // 提取域名
61
+ return `domain:${hostname}` // 生成规则
62
  } catch {
63
  return `input:${input.toString()}`
64
  }
65
  }
66
 
67
+ // Tool 定义开始
68
  export const WebFetchTool = buildTool({
69
+ name: WEB_FETCH_TOOL_NAME, // 工具名
70
+ searchHint: 'fetch and extract content from a URL', // 给 llm 的提示
71
  // 100K chars - tool result persistence threshold
72
  maxResultSizeChars: 100_000,
73
  shouldDefer: true,
74
  async description(input) {
75
+ const { url } = input as { url: string } // 类型断言
76
  try {
77
  const hostname = new URL(url).hostname
78
+ return `VersperClaw wants to fetch content from ${hostname}` // 用户提示
79
  } catch {
80
+ return `VersperClaw wants to fetch content from this URL`
81
  }
82
  },
83
  userFacingName() {
84
+ return 'Fetch' // UI 名称
85
  },
86
+ // 摘要 & 活动描述
87
  getToolUseSummary,
88
  getActivityDescription(input) {
89
  const summary = getToolUseSummary(input)
90
  return summary ? `Fetching ${summary}` : 'Fetching web page'
91
  },
92
+ // schema getter
93
  get inputSchema(): InputSchema {
94
  return inputSchema()
95
  },
96
  get outputSchema(): OutputSchema {
97
  return outputSchema()
98
  },
99
+ // 工具属性
100
  isConcurrencySafe() {
101
+ return true // 支持并发
102
  },
103
  isReadOnly() {
104
+ return true // 不修改系统
105
  },
106
  toAutoClassifierInput(input) {
107
  return input.prompt ? `${input.url}: ${input.prompt}` : input.url
108
  },
109
+ // 权限检查
110
  async checkPermissions(input, context): Promise<PermissionDecision> {
111
  const appState = context.getAppState()
112
  const permissionContext = appState.toolPermissionContext
 
115
  try {
116
  const { url } = input as { url: string }
117
  const parsedUrl = new URL(url)
118
+ if (isPreapprovedHost(parsedUrl.hostname, parsedUrl.pathname)) { // 白名单
119
  return {
120
  behavior: 'allow',
121
  updatedInput: input,
 
153
  if (askRule) {
154
  return {
155
  behavior: 'ask',
156
+ message: `VersperClaw requested permissions to use ${WebFetchTool.name}, but you haven't granted it yet.`,
157
  decisionReason: {
158
  type: 'rule',
159
  rule: askRule,
 
180
 
181
  return {
182
  behavior: 'ask',
183
+ message: `VersperClaw requested permissions to use ${WebFetchTool.name}, but you haven't granted it yet.`,
184
  suggestions: buildSuggestions(ruleContent),
185
  }
186
  },
src/tools/WebFetchTool/preapproved.ts CHANGED
@@ -11,6 +11,8 @@
11
  // See test/utils/sandbox/webfetch-preapproved-separation.test.ts for verification
12
  // that sandbox network restrictions require explicit user permission rules.
13
 
 
 
14
  export const PREAPPROVED_HOSTS = new Set([
15
  // Anthropic
16
  'platform.claude.com',
 
11
  // See test/utils/sandbox/webfetch-preapproved-separation.test.ts for verification
12
  // that sandbox network restrictions require explicit user permission rules.
13
 
14
+
15
+ // 允许访问的白名单
16
  export const PREAPPROVED_HOSTS = new Set([
17
  // Anthropic
18
  'platform.claude.com',