dsh-anysearch-fallback
Manifest valid让 web_fetch 在上游 AnySearch 抽取失败(HTTP 422)时自动降级为受约束的浏览器 UA 直抓;零运行时依赖,只读 ctx.web.fetchProviders,不改任何 DSH 私有字段
dsh-anysearch-fallback
给 DSH 的 web_fetch 加一条降级兜底:AnySearch 抽不出内容(HTTP 422)时,自动改用浏览器 UA 直抓。
Gives DSH's web_fetch a fallback: when AnySearch cannot extract a page (HTTP 422), fetch it directly with a browser-like user agent instead of failing the whole call.
中文
它解决什么问题
DSH 的 web_fetch 走 AnySearch 的 /v1/extract。有些页面(openai.com、知乎问题页、不存在的路径……)上游会返回 HTTP 422 extract_failed,于是整次 web_fetch 直接报错。本插件在那个位置接一手:先照旧走 AnySearch,上游一失败就直抓原文兜底(唯一例外是"取消"),并在正文首行写明上游失败的真实原因。
工作机制
ctx.web 注册的 provider 链:
anysearch-fallback (provider id: anysearch-fallback) ← web_fetch 选中它
│
├─ 代理到官方 AnySearch provider 的 extract
│ ├─ 成功 → 原样返回(逐字段不变)
│ ├─ 任何失败(422 / 401 / 402 / 403 / 429 / 超时 / 网络…)→ 进入直抓兜底
│ │ 兜底结果的正文首行写明上游失败原因(状态 + 摘要)
│ └─ 取消 / abort → 原样抛错,绝不降级(调用方已要求停止)
│
└─ 直抓兜底:浏览器 UA + 协议/内网校验 + 5 MB 字节上限 + 15s 超时
→ 返回带标记的纯文本(正文顶部写入降级标记,模型一眼可辨)
- 本插件不修改官方
@anysearch/anysearch-dsh的任何文件,也不改 DSH 私有字段;它以自己的 idanysearch-fallback注册 provider,内部再按 id 调官方anysearchprovider。 - 官方插件升级/重装对本插件无影响(若官方改了 provider id,需要同步改
index.mjs里的UPSTREAM_PROVIDER_ID)。 - 兜底失败时,抛出的错误里同时带上「上游原始错误」与「直抓失败原因」+ 一句可行动提示,两侧信息都不吞。
关键行为
| 项目 | 取值 |
|---|---|
| provider id | anysearch-fallback(由本插件的 - id: web 覆盖把 fetchProvider 指到它;上游仍是官方 anysearch) |
| 触发降级 | 任何上游错误:422 / 401(凭据)/ 402(额度)/ 403 / 429(限流)/ 5xx / 超时 / 网络错误 / 非 JSON 响应,无一例外 |
| 不降级(唯一例外) | 取消 / abort:调用方的 signal 已中止,或错误链上有 name: AbortError、code: ABORT_ERR / WEB_ABORTED / extract_canceled、消息含 abort/cancel → 原样抛,不直抓 |
| 协议白名单 | 仅 http: / https:(file: / data: / ftp: / blob: 等拒绝) |
| 内网拒绝 | localhost / *.localhost / *.local / .internal;IPv4 0.0.0.0、127/8、10/8、172.16/12、192.168/16、169.254/16、100.64/10;IPv6 ::1、fc00::/7、fe80::/10;IPv4-mapped IPv6(::ffff:127.0.0.1) |
| 字节上限 | 5 MB,流式读取(response.body),超限立即 cancel() |
| 字符上限 | 与 DSH 工具侧一致(DEFAULT_FETCH_MAX_OUTPUT_CHARS),超出裁剪并置 truncated: true |
| 超时 | 自带 15s,串联上游 signal,任一触发即中止并说明来源 |
| 请求头 | 固定桌面 Chrome UA + accept + accept-language;不带 Authorization / Cookie / Referer |
| 重定向 | redirect: 'manual',最多 3 跳,每一跳都重做协议与内网校验 |
| 磁盘缓存 | 无 |
降级标记(永远在返回正文的第一行)
标记会写明上游失败的真实原因(状态 + 截断到 240 字符的摘要)和直抓实际拿到的状态;正文为空时说清"没有正文",免得"只有一行标记"被读成"这页面是空的"。
| 情形 | 第一行 |
|---|---|
| 上游 422,直抓 2xx 有正文 | [anysearch-fallback] 上游抽取失败(HTTP 422):AnySearch extract failed: … (HTTP 422, auth credential);直抓成功(HTTP 200),以下为未经 AnySearch 清洗的直抓原文。 |
| 上游 429,直抓 2xx 有正文 | [anysearch-fallback] 上游错误(HTTP 429 rate limit):AnySearch extract failed: "too many requests";直抓成功(HTTP 200),以下为未经 AnySearch 清洗的直抓原文。 |
| 直抓非 2xx,有正文 | [anysearch-fallback] 上游错误(HTTP 429 rate limit):…;直抓返回 HTTP 403(站点可能拒绝匿名访问),以下为站点返回的原始内容,未经 AnySearch 清洗。 |
| 直抓非 2xx,正文为空 | [anysearch-fallback] 上游错误(HTTP 429 rate limit):…;直抓返回 HTTP 403 且站点未返回任何正文内容(该页可能需要登录或被反爬拦截,建议换来源)。 |
| 直抓 2xx,正文为空 | [anysearch-fallback] 上游错误(HTTP 500):…;直抓成功(HTTP 200)但站点未返回任何正文内容(可能是空页或正文由脚本渲染,建议换来源)。 |
- 状态取上游错误里真实的
httpStatus;命中"抽取失败"那三种精确形状(httpStatus === 422/errorCode字段值extract_failed/ 那句Unable to extract content from the URL.)时措辞为上游抽取失败,否则为上游错误(并在拿得到时附上rate limit/auth credential这类标签)。 - 摘要压平空白并截断到 240 字符(
MAX_MARKER_UPSTREAM_CHARS),标记行因此永远是一眼能读完的一行,不是日志。 - 每次降级还会记一条
warn日志(含上游原因与直抓结果)。
安装
作为本地插件挂进某个 DSH profile:
# 1) 把本目录放到 <DSH_HOME>/plugins/dsh-anysearch-fallback/
# 2) 在 profile 的 package.json 里加依赖(相对路径写法)
# "dsh-anysearch-fallback": "file:C:/Users/you/.dsh/plugins/dsh-anysearch-fallback"
# 3) 把它追加到 profile 的 dsh.profile.bundles 末尾
# (放最后 = 它的 `- id: web` 覆盖在官方 anysearch 之后生效)
# 4) 在 profile 的 node_modules 下建同名入口(本机既有形状:真目录 + 硬链接)
# 5) 重启 DSH —— 新开一个对话不够,profile 的配置树是启动时组合的
本插件的 cordis.patch.yml 会 insert 自己的 bundle(id dsh-anysearch-fallback),并把 web 服务的 fetchProvider 覆盖为 anysearch-fallback(searchProvider 必须一起原样写上——patch 对 config 是整体替换、不深合并)。具体层内覆盖请按你的 profile 现状调整。
测试
npm test # 离线单测:63 通过 / 1 跳过(联网用例默认跳过)
test/integration.test.mjs 是联网实测用例(真实 422 → 真实直抓):它需要 @anysearch/anysearch-dsh 及其 peer 包可被解析。仓库内默认跳过;在本机已装好的环境里可按该文件头部的注释搭一个临时 staging 目录(node_modules 里为 @deepseek-ai/* 建 junction)后运行。
已知边界 / 未做
- DNS rebinding 只防一半:除字面 IP / 主机名判定外,会在 DNS 解析后再核一遍解析到的 IP(任一解析结果非公网即拒);但没有地址钉扎 —— 校验用的解析结果与真正建连之间有时间差(TOCTOU),极端的 rebinding 仍可能穿过。
(对比:
dsh-web自带的dsh-web-fetch-http用ipaddr.js+ undici pinned agent 做了完整钉扎;本插件刻意零依赖,故未复刻。) - 带 userinfo 的 URL 会被拒绝:
http://user:pw@example.com/(只给用户名也算)一律以WEB_BLOCKED_URL拒掉,错误消息里点名 userinfo;不做静默剥离 —— 用户显式给了带凭证的 URL 就明确报错,比悄悄改写更可预测。 - 保守拒绝:
168.63/16、198.18/15、192.0.0/24等非公网段被判为内网并拒绝,宁可误拒。 - 提取质量:朴素 HTML 降噪(去 script/style、取标题、解实体、折叠空白),质量低于 AnySearch 的正文清洗;结果顶部有降级标记,便于人工/模型识别。
- 系统性问题现在也会降级(这是宽兜底的代价):上游 401(凭据失效)、402(额度耗尽)、429(限流)这类问题不再整次报错,而是静默降级为直抓结果 —— 直抓往往仍能成功,于是"接口已经不可用了,但抓取照常返回"。要判断集成是否正常,看正文首行的标记(里面写明上游错误,例如
上游错误(HTTP 429 rate limit):…)与日志里的warn行。 - 不做重试:无论上游还是直抓都不重试。
- 不改超时预算:
web_fetch工具预算 30 s 由 DSH 工具层硬编码,本插件不碰;兜底直抓自带 15 s,若上游已耗尽大部分预算,兜底可能来不及完成。 - 非 2xx 不假装成功:直抓拿到 4xx/5xx 时按真实状态码返回(并保留正文)。
- 反爬限制:目标站可能对匿名直抓返回 403 或直接给反爬页(例:
openai.com返回 403 且正文为空;知乎问题页返回 403 + 一句反爬文案)。此时降级正文可能为空或根本不是真正文 —— 标记行会把这种情况说清楚(含"站点未返回任何正文内容"),但拿不到内容就是拿不到,建议换来源。 - 改完必须重启 DSH:已启动的 DSH 进程加载的是启动时的代码;升级或改动本插件(包括只改了降级标记文案)后,必须重启 DSH(新开一个对话不够)新代码才生效。
License
MIT
English
Problem. web_fetch goes through AnySearch /v1/extract; for some pages the upstream answers HTTP 422 extract_failed and the whole fetch call fails. This plugin registers its own anysearch-fallback fetch provider, points the web service at it, delegates to the official anysearch provider first, and degrades to a guarded direct fetch on any upstream failure — the only exception is cancellation.
Guarantees.
- Success path unchanged, field for field.
- Cancellation (
signalaborted,AbortError,ABORT_ERR/WEB_ABORTED/extract_canceled, abort/cancel wording) is never degraded: the original error surfaces untouched and no new request is sent. - Any other upstream failure (401/402/403/429/timeouts/network) degrades, and the reason is written into the first line of the returned body plus a
warnlog line — never silently swallowed. - Only
http:/https:; localhost, private, link-local, CGNAT and IPv4-mapped IPv6 targets are refused. - A URL carrying userinfo (
user:pw@host) is refused asWEB_BLOCKED_URLand named as such — never silently stripped. - Hard 5 MB byte cap read as a stream (
response.body), neverresponse.text(); character cap matches DSH's tool layer. - 15 s own timeout chained to the upstream abort signal.
- Fixed desktop Chrome UA plus
accept/accept-language; no Authorization, Cookie, Referer; no TLS bypass; no disk cache. - Manual redirects, at most 3 hops, re-validated at every hop.
- Fallback output carries a visible marker line naming both real statuses, so a degraded result is never mistaken for cleaned text and an empty body is never mistaken for an empty page.
Not done. No address pinning after resolution (one re-check, then a separate connect — TOCTOU remains), no retry, no timeout-budget change, naive HTML reduction (lower quality than AnySearch extraction). Anti-bot sites may answer 403 with an empty body or a challenge page: the marker says so, but there is no content to return — change the source instead.
Cost of the broad fallback. Systemic upstream problems (401 credentials, 402 quota, 429 rate limit) now degrade instead of failing the call, so a broken integration can still return pages. Watch the marker line (it names the upstream error) and the warn log line.
Restart required. A running DSH process keeps the code it booted with; restart DSH after changing this plugin (a new chat is not enough).
Test. npm test — 63 passed / 1 skipped offline. The network integration test is skipped unless the installed official bundle and its peer packages are resolvable.
License. MIT
Comments
Loading…
From the same category
DeepSeek Harness plugin for Reactive Resume: bridges your resumes and job applications into a Harness session over MCP.
★ 41.7k
↓ 242/wk
MIT
Aug 24, 2026
dsh plugin --profile web add dsh-plugin-reactive-resumeby Tencent
Let AI agents use your real, logged-in browser without interrupting your work. CLI + extension for browser automation across any shell-capable AI agent.
★ 9.1k
↓ 7.5k/wk
MIT
TypeScript
Oct 15, 2026
dsh plugin --profile terminal add @wxg-prc-cpg/browser-skill-dsh-pluginby dsh-market
The plugin market inside DeepSeek Harness — browse, search, one-click install · DSH 可视化插件市场
★ 7k
↓ 160k/wk
MIT
TypeScript
Oct 17, 2026
dsh plugin --profile web add dshmarketby yjh051108
dsh-routing-suite — injector + router-standard kit: install the runtime injector first, then the task-aware reasoning-mode router preset (measured P1-P23).
★ 7k
MIT
JavaScript
Sep 18, 2026
dsh plugin --profile web add @dsh-external/dsh-super-injectorby Q00
Agent OS: the agent gets smarter on its own. We just hold the line: Interview-gated, staged evaluation, budgeted evolution loop. MCP server, 14 runtimes: Claude Code, Codex CLI, Gemini CLI, OpenCode,
★ 6.2k
MIT
Python
Oct 17, 2026
by superdesigndev
OpenRouter for agent tools. Join community here: https://discord.gg/6mQYYfFMAn
★ 5.7k
NOASSERTION
Python
Oct 17, 2026
dsh plugin --profile web add treg-dsh