# @epoch-agent/plugin-web 中文文档

规范地址：https://npmcn.com/package/%40epoch-agent/plugin-web/

版本：6\.2\.0
npm 元数据更新时间：2026-11-13T08:31:36.148Z
中文译文更新时间：2026-11-13T19:50:35.539Z
软件包许可：Apache-2\.0

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40epoch-agent/plugin-web
原文对照：https://npmcn.com/package/%40epoch-agent/plugin-web/?lang=original
项目主页：https://github.com/Ddbor/epoch-agent#readme
源码仓库：https://github.com/Ddbor/epoch-agent

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：79.5 kB，5 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=22\.0\.0
- 运行依赖：2 个
- 首次发布：2026-08-19T10:31:56.920Z
- 最新版本发布：2026-11-13T08:31:35.856Z
- 近 30 天下载量：4398（2026-10-14 至 2026-11-12，npm 官方下载统计）
- 已发布版本数：57（近 12 个月 57 个）

## 中文简介

网页 — URL + HTML + Markdown + SSRF →

## 安装

```sh
npm install @epoch-agent/plugin-web
```

也可以使用：`pnpm add @epoch-agent/plugin-web`、`yarn add @epoch-agent/plugin-web`、`bun add @epoch-agent/plugin-web`

## README

# @epoch-agent/plugin-web

网页抓取与搜索插件。**两个工具**：`web_fetch`（总是有）和 `web_search` （**有可用后端才注册** —— 缺省状态下就有，见下）。

-   ✅ **做**：HTTP GET → HTML→Markdown/Text/HTML、SSRF 校验、逐跳重定向校验、大小限制； web 搜索（native / tavily / brave / searxng / keyless 五个后端）
-   ❌ **不做**：不执行 JS、不渲染页面、不带浏览器；**不抓搜索引擎的 HTML 结果页** （违反 ToS、随时会碎、要处理验证码 —— 要搜就走后端，不走爬虫）
-   **依赖**：`@epoch-agent/protocol` + `@epoch-agent/infra`，两个都是 `dependencies` （本包**没有 `peerDependencies`**），HTML 转换是手写的（无第三方库）

## 工具参数

```json
{ "url": "https://example.com/doc", "format": "markdown", "timeout": 30 }
{ "query": "node 22 fetch keepalive", "maxResults": 5, "recencyDays": 30 }
```

**两个工具的参数表在 [docs/TOOLS.md](https://npmcn.com/docs/TOOLS.md#%E7%BD%91%E9%A1%B5)** —— 那份是工具清单 的唯一真源，这里只写这个包自己的实现取舍。

## `web_search`：搜什么，不抓什么

**只返回标题 / URL / 摘要，不返回正文。** 一次搜索 5 条结果全抓正文轻松几万 token， 而其中多半是模型看一眼摘要就丢掉的。分工是：搜 → 模型挑 → `web_fetch` 抓那一条。

### 五个后端，按凭据自动挑

五档（`native` / `tavily` / `brave` / `searxng` / `keyless`）各要什么凭据见 [docs/TOOLS.md](https://npmcn.com/docs/TOOLS.md#web_search-%E8%A6%81%E4%B8%80%E4%B8%AA%E6%90%9C%E7%B4%A2%E5%90%8E%E7%AB%AF)。

不配 `search.provider` 时按那张表的顺序探测，第一个可用的赢。**显式配了哪家就只认哪家** —— 缺 key 时报诊断而不是悄悄换一家：换一家意味着查询发去了另一家公司， 那是用户没同意的事。

一个后端都没有时 `web_search` **不注册**，启动诊断给 `skipped` 并逐档列出缺什么。 注册一个一调就报错的工具比不注册更糟：模型看见工具表里有它就会反复试，每次烧一轮。

#### 两档不用办账号的（2026-09-15）

上面那句「没有后端就不注册」原来的实际效果是**大多数嵌入宿主的用户都没有搜索** —— 三家老后端全要办账号，而宿主多半不会去配。这一档漏配是静默的：模型不知道自己本该 有搜索，就改用浏览器去抓搜索引擎结果页（正是本包第一节说「不做」的那件事）。

-   **`native`**（`search/native.ts`）借 `provider.apiKey`，让当前 LLM provider 自己的 服务端搜索去干活。首版认 `deepseek` / `anthropic`（共用 Anthropic Messages 协议）。 它的结果项**没有明文摘要**（正文是 `encrypted_content`），信息走 `SearchOutcome.summary`。
-   **`keyless`**（`search/keyless.ts`）打 Exa / Parallel 的匿名 MCP 端点， 带游标轮转摊平两家免费额度。**查询词会发给第三方**，所以它排在最后、且能整档关掉。

两个开关**缺省开**，只有显式写 `search.native: false` / `search.keyless: false` 才关 （实现是 `=== false ? 关 : 开`，**不是「真值才开」**）。

#### 为什么是这两家（2026-09-15 实探，不是照抄）

四家里只有 epoch 原来会「什么都没有就把工具拿掉」，所以选型时把同级项目的做法 和这几家的实际表现一起量了一遍：

| 结论 | 证据 |
| --- | --- |
| Exa / Parallel 公开 MCP 端点**匿名可用** | 无 Authorization，`tools/call` 返回 200 |
| **直连可达**，不需要代理 | 绕开本机代理重测：两域名 200 / 0.7s，结果一致 |
| Exa 中文覆盖**不差**、但**质量差** | 中文查 8/8 是中文结果，其中 3 条品牌软文、2 条豆丁网 |
| Parallel 有语义漂移 | 英文查 10 条里 3 条是 Curtain **Wall**（建筑幕墙） |
| **DeepSeek 服务端搜索更好**（所以 native 排第一） | `POST /anthropic/v1/messages` + `web_search_20250305`：10 条全中文、**0 条软文**，还捞到了 Exa / Parallel 都没有的平台维度数据 |

横向对照：codex 用 OpenAI Responses 的服务端 `web_search`、gemini-cli 用 Gemini grounding（两家都是 `native` 那一档），opencode 打 Exa / Parallel 公开端点（就是 `keyless` 这一档），hermes-agent 做了最全的探测级联 + keyless ring。

> ⚠️ **native 排在 tavily 前面是有代价的决定**：只设了 `TAVILY_API_KEY` 却没写 `search.provider` 的人，从这一版起会改走 native，那把 key 变成静默失效的配置。 要钉住一家就显式写 `search.provider`。

> SearXNG **默认没开** JSON 输出，实例的 `settings.yml` 里要有 `search: { formats: [html, json] }`。没开时它返回 403，我们把这句话写进了错误里 —— 否则用户会以为是自己地址写错了。

### 搜索结果是**风险最高**的一种不可信内容

它和抓来的网页一样带 `openWorldHint: true`（见下一节），但风险更高一档： 网页要等 agent 主动去抓，而搜索结果是攻击者可以针对某个查询词**主动投放**的 （SEO 投毒 + 提示注入）。所以 [core 的 system prompt](https://npmcn.com/package/core/src/agent/prompt.ts) 在不可信内容策略里单独点了它的名。

结果里的 URL **不自动跟随**。模型要抓时走 `web_fetch`，SSRF 校验和 `network` 权限判定一道都不会因为「这是搜出来的」而放松。

失败（key 错 / 配额用尽 / 网络不通）返回一条**说明原因**的失败结果，不抛异常 —— 搜索挂了不该让整轮对话断在这儿。同一个查询在**同一会话内**缓存，省配额。

## SSRF 防护是三层，不是一层

1.  **协议白名单**——只放 `http` / `https`
2.  **主机名与 IP 字面量黑名单**——`localhost`、云元数据域名、私网段 （10/8、127/8、169.254/16、172.16/12、192.168/16、CGNAT、组播与保留段，IPv6 同理）
3.  **DNS 解析后再校验一次**——攻击者控制的域名可以解析到 `127.0.0.1`， 光看主机名字面量挡不住。这是最常见的绕过手法

外加**逐跳重定向校验**：不用 `fetch` 的 `redirect: 'follow'`，而是手动跟随、每一跳 重新做完整校验（最多 5 跳）。默认跟随只校验第一个 URL，一个公网地址 302 到 `169.254.169.254` 就能直接读到云元数据。

其余限制：响应上限 5 MB，Cloudflare 挑战会重试一次。

## 回执语言：给模型读的一律英文

`web_fetch` / `web_search` 的**工具回执**——失败说明、被拦截的原因、provider 报错、 「一条都没搜到」这类说明——**全部是英文**，判据同 [core 的 `code-mode/notice.ts`](https://npmcn.com/package/core/src/code-mode/notice.ts) 文件头（2026-11-05 拍的口径：回执的读者是模型，跟工具 description 同一档，不跟界面语言走）。 所以这几处 `t()` 调用显式传了 `'en'`，`check:i18n` 那边也不要指望它们跟着 `zh` 变。

反过来，这些**不**在这一档里：

-   **抓回来的网页正文 / 搜索结果的标题、摘要**——那是别人的数据，原样回给模型
-   **`unavailableReason()` 与 `selectSearchProvider()` 的 `detail`**——启动诊断， 用户读的，照旧跟着界面语言走
-   **`native` 档的 `buildPrompt()`**——发给对端模型的指令，用户读的正是它的回答， 界面切成英文却用中文问它，答案多半也是中文

拦截原因那一格有个额外要求：**URL 和判定原因都得留着**。`url-safety` 只给原因 （带主机名 / 解析到的地址），被拒的那个完整 URL 由 `web-fetch` 补在前面 —— 少了 URL 模型不知道是第一跳还是第五跳被拒，少了原因它只能盲目换个地址重试。

## 三个 annotation 不只是元数据

```ts
annotations: { readOnlyHint: true, idempotentHint: false, openWorldHint: true }
```

`openWorldHint: true` 会让 [core](https://npmcn.com/package/core) 的 tool-executor 把输出包进 `<tool_output untrusted="true">`。抓来的网页内容作者既不是用户也不是我们，必须标成 「数据」而不是「指令」再回灌上下文。

`describeTarget: (args) => args.url` 让审批缓存按 URL 记——不给它就只能退回参数 JSON， 「批准抓这个站」下次照样弹窗。

## 文件

| 文件 | 内容 |
| --- | --- |
| `index.ts` | `webPlugin` + `createWebSearchTool`（条件注册） |
| `tools/web-fetch.ts` | 抓取 + 重定向 + 大小限制 + 重试 |
| `tools/web-search.ts` | 搜索工具：参数钳制 + 会话内缓存 + 失败不炸轮 |
| `search/provider.ts` | 后端接口 + 共用的 HTTP / 错误分类 |
| `search/registry.ts` | 按配置 + 凭据挑后端（挑不到就不注册） |
| `search/{tavily,brave,searxng}.ts` | 三个要办账号的后端各自的请求 / 响应翻译 |
| `search/native.ts` | 借当前 LLM provider 的服务端搜索 |
| `search/keyless.ts` | Exa / Parallel 免费端点环 + 游标轮转 |
| `search/mcp.ts` | MCP `tools/call`（纯 JSON 和 SSE 两种回法） |
| `utils/url-safety.ts` | 三层 SSRF 校验 |
| `utils/html-to-markdown.ts` | 手写 HTML→Markdown / →Text |

## 开发

```bash
pnpm --filter @epoch-agent/plugin-web test
```

四个用例文件：`ssrf.test.ts`、`url-safety.test.ts`、`html-to-markdown.test.ts`、 `web-search.test.ts`。改 SSRF 判定必须先看前两个——里面记的是具体的绕过手法， 不是凑数的。搜索那份**全部用假 `fetch`**：真打网络的用例要么依赖别人的 API key、 要么在墙内直接红，两种都不该进 `pnpm check`。

## 常见问题

### @epoch-agent/plugin-web 是什么？

网页 — URL \+ HTML \+ Markdown \+ SSRF →

### 如何安装 @epoch-agent/plugin-web？

在项目目录运行 npm install @epoch-agent/plugin-web，或使用 pnpm add / yarn add / bun add。

### @epoch-agent/plugin-web 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @epoch-agent/plugin-web 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @epoch-agent/plugin-web 需要什么版本的 Node\.js？

package\.json 的 engines 字段要求 Node\.js \>=22\.0\.0。版本不符时 npm 默认只给出警告，开启 engine-strict 后会拒绝安装。

### @epoch-agent/plugin-web 可以商用吗？

@epoch-agent/plugin-web 使用 Apache-2\.0 许可。宽松许可：允许商业使用、修改和再分发，并附带明确的专利授权；分发时需保留许可与 NOTICE 文件，修改过的文件要注明改动。以上为概括，不构成法律意见，请以许可原文为准。

### @epoch-agent/plugin-web 的下载量有多少？

根据 npm 官方下载统计，@epoch-agent/plugin-web 在 2026/10/14 – 2026/11/12 这 30 天内被下载 4,398 次，日均约 147 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @epoch-agent/plugin-web 还在维护吗？

最新版本 6\.2\.0 发布于 2026/11/13（今天）；至今共发布 57 个版本，其中过去 12 个月发布了 57 个；首次发布于 2026/08/19。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

