Skip to main content
Glama
web-reach

web-reach

by FnSGit
README.md
# web-reach

分层网页能力 MCP。四层策略内置,对外只暴露 3 个工具。

一个工具胜过四个:模型不需要在 Exa / Jina / 各家搜索 API 之间选,也不需要在
「先搜还是先抓」上试错。质量信号、去重、降级链都由服务内部处理。

## 为什么分层

实测(2026-11-14,同一批 5 条 query + 一道事实核查题,真实调用):

| 后端 | 优点 | 硬伤 |
|---|---|---|
| DeepSeek Search | 0.5s;`rerank_score` 0–3、`authority` 0–4、`site:/lang:/after:` 算子 | 同域名版本页重复严重(10 条仅覆盖 6 个域名) |
| Exa | 来源多样(与 DeepSeek 唯一域名只重叠 20–43%) | 慢 8 倍、无质量信号 |
| DeepSeek Fetch | 0.2–0.35s、正文干净(去掉导航与链接) | 14 个 URL 中 7 个 404:Wikipedia、HN、docs.anthropic.com 整站失败 |
| Jina Reader | 覆盖率高 | 慢 6–20 倍、噪声多 |

单用任何一个都有肉眼可见的缺口,所以按层组合,缺口由下一层补。

## 四层

```mermaid
flowchart TD
  Q[query] --> L1["L1 DeepSeek Search<br/>rerank_score / authority"]
  L1 --> F["过滤:score ≥ min_score<br/>authority ≥ min_authority<br/>按域名+路径前缀去重"]
  F --> C{hits 够数?}
  C -->|不够 / diversify=always| L2[L2 Exa 补新域名]
  C -->|够| OUT[hits]
  L2 --> OUT
  U[url] --> FD[L3 DeepSeek Fetch]
  FD -->|404 / 空 / 短于 400 字符| FJ[L3' Jina 兜底]
  FD -->|ok| BODY[markdown]
  FJ --> BODY
  QQ[question] --> L4["L4 服务端深搜<br/>多步检索 + 引用"]
```

## 工具

| 工具 | 用途 | 何时不用 |
|---|---|---|
| `web_search` | 检索;返回带 score / authority / date 的命中,已过滤去重 | 要整篇原文时先用它再 `web_fetch` |
| `web_fetch` | 单 URL 取正文;双后端自动降级 | 只有摘要需求时不必抓 |
| `web_research` | 服务端多步深搜,返回带引用答案 | 需要原文留存 / 引用原句 / 可复现采集 |

### web_search

| 参数 | 默认 | 说明 |
|---|---|---|
| `query` | — | 支持 `site:` `-site:` `lang:` `after:` `before:` `"精确短语"` |
| `question` | — | 与 `query` 不同时填,改善 query 理解 |
| `max_results` | 8 | 1–20 |
| `min_score` | 1.5 | rerank_score 下限(文档称 ≥1.5 为高质量) |
| `min_authority` | 0 | authority 下限;查官方结论时给 3 |
| `diversify` | `auto` | `auto` 不足才补;`always` 预留一半名额给第二索引;`never` 只走第一层 |

### web_fetch

先走快速提取器;404 / 空正文 / 正文短于 400 字符时自动切第二个提取器。
`TRACE:` 行说明走了哪层、各层耗时。

### web_research

服务端最多约 20 次工具调用,由 `deepseek-flash` 决定检索策略。
`USAGE:` 行给出计费口径(searches / fetches / billed tokens)。
页面正文不会返回(服务端加密在 `signature` 里),要原文用 `web_fetch`。

## 失败语义

统一 `WEB_REACH_ERROR code=<code>`:

| code | 含义 | 处置 |
|---|---|---|
| `no_key` | 缺 API key | 检查密钥来源 |
| `rate_limited` | 429,已自动重试 | 仍失败则降低并发 |
| `not_found` | 目标无可用内容 | 换 URL 或用 `web_search` 找替代源 |
| `blocked` | 鉴权 / 策略 / 内容审查拒绝 | 换源 |
| `timeout` | 上游超时,已重试 | 该站可能不可达 |
| `upstream` | 其它上游错误 | 看 message |

## 配置

密钥来源:环境变量优先,其次 `~/.config/sextant/secrets.env`(只读 `KEY=VALUE`,不执行、不打印)。

| 变量 | 必需 | 用途 |
|---|---|---|
| `DEEPSEEK_API_KEY` | 是 | L1 检索、L3 抓取、L4 深搜 |
| `EXA_API_KEY` | 否 | L2 多样性;缺失时 L2 跳过 |
| `JINA_API_KEY` | 否 | 提高 `r.jina.ai` 配额 |
| `WEB_REACH_PROXY` | 否 | 显式代理 |

**代理**:宿主启动 MCP 子进程时常常只传精简 env,`HTTP(S)_PROXY` 丢失。实测缺少代理时
`r.jina.ai` 直连 20s 超时、带代理 3.9s 成功,而 Bun 只在进程启动时读取代理环境变量。
因此本服务自行解析:环境变量 → `WEB_REACH_PROXY` → 探测 `127.0.0.1:7890` 是否可达。
Bun 不支持 socks5 代理(`UnsupportedProxyProtocol`),配置里会被跳过。

### omp

`~/.omp/agent/mcp.json`:

```json
{
  "mcpServers": {
    "web-reach": {
      "type": "stdio",
      "command": "bun",
      "args": ["/absolute/path/to/web-reach/src/index.ts"]
    }
  }
}
```

## 开发

```sh
bun install
bun run smoke     # 真实 stdio 握手,三个工具各打一次真实请求
npx tsc --noEmit
```

冒烟覆盖:第一层命中、`diversify=always` 的跨索引补充、DeepSeek 抓取成功、
Wikipedia 走 Jina 兜底、`web_research` 引用、不存在的域名报错。