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 |
-
|
| 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 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 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 |
-
|
|
|
|
| 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 `
|
| 77 |
} catch {
|
| 78 |
-
return `
|
| 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: `
|
| 151 |
decisionReason: {
|
| 152 |
type: 'rule',
|
| 153 |
rule: askRule,
|
|
@@ -174,7 +180,7 @@ export const WebFetchTool = buildTool({
|
|
| 174 |
|
| 175 |
return {
|
| 176 |
behavior: 'ask',
|
| 177 |
-
message: `
|
| 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',
|