dsh-plugin-browserskill
Tencent/BrowserSkillBrowserSkill 的 DeepSeek Harness 浏览器自动化桥接插件,通过原生浏览器工具控制可见的 Chrome 和 Edge Agent Window,支持可访问性与 VOM 页面观察、截图、隔离的多会话控制和 Web UI 实时观察浮层。
DSH 插件:web_fetch 经本地代理(Mihomo / Clash 等)出网以绕过 TUN fake-ip;代理关闭后,同一请求自动改走直连,模型不再对失效代理反复重试。
English: README.en.md
开着 Mihomo/Clash 的 TUN + fake-ip 打开 DSH 后,web_fetch 报:
URL hostname "raw.githubusercontent.com" resolves to a non-public IP address用 dsh-web-fetch-proxy 一类插件把 web_fetch 接到本地代理后,抓取恢复了;但只要关掉代理(DSH 配置没动、也没重启), 模型请求就会卡在重试里,最后整轮任务中断。
两件事叠在一起:
198.18.0.22、fdfe:dcba:9876::ed)。web_fetch 会先本地解析、再逐条校验地址是否公网, 任一条不合格就整体拒绝,连接根本没有发出去。
@deepseek-ai/dsh-http-proxy 把一条进程级 代理策略装进 undici 的全局 dispatcher。web_fetch 正是靠 proxyRouteFor() 看到「已代理」才跳过本地 DNS 校验。 现有插件(如 dsh-web-fetch-proxy) 把真实代理地址(如 http://127.0.0.1:7890)直接装了进来; 代理关掉后这条路由仍在,之后所有出站请求(包括模型 API)继续拨一个已经不存在的端口, 连接被拒 → dsh-llm-retry 退避重试 → 仍然失败 → 回合中断。
换句话说:web_fetch 需要的「代理路由」,和模型需要的「代理关闭后能直连」,是同一个全局开关。 把真实代理写死进去,就只能在两者之间二选一。
dsh-web-fetch-proxy 已经解决了 fake-ip 这一半问题:它把本地代理(如 http://127.0.0.1:7897)装成 @deepseek-ai/dsh-http-proxy 的进程级策略,web_fetch 因此看到 proxied 路由、跳过本地 DNS 校验,抓取恢复正常。这个思路本插件照旧沿用。
剩下的问题是这条路由的生命周期:策略属于 DSH 进程,而不是属于代理进程。一经安装,它就与 DSH 进程同寿;代理关闭后,路由仍然指向那个已经没人监听的端口。此后所有出站请求——包括模型 API,以及 dsh-llm-retry 的重试——都会拨这个死端口,拿到 ECONNREFUSED,退避后再试,仍然 失败,直到当前回合中断;只有重启 DSH 或让插件重新探测到代理,才会恢复。
本插件补的正是这一段:
resolveTimeoutMs。
因此本插件在功能上是它的超集:既保留 fake-ip 修复,也补上代理关闭后的同请求直连与自动切回。 两个插件都会安装进程级路由,不要同时启用(原因见下方「和 dsh-web-fetch-proxy 的关系」)。
插件不再把真实代理装成全局路由,而是先在本机起一个只监听回环地址的小型 HTTP 代理(relay), 把 relay 作为进程级路由。relay 在每次请求时决定怎么走:
┌──────────────────────────────┐
模型 API ──────┤ ├── 上游代理(Mihomo :7890)→ 网络
web_fetch ─────┤ relay 127.0.0.1:<随机端口> │
MCP / 搜索 ────┤ ├── 直连(上游不可用时,同一请求内切换)
└──────────────────────────────┘web_fetch 依然拿到 proxied 路由,由代理侧解析域名,fake-ip 问题照旧被绕过。
同时把上游标记为 down,后续请求不再徒劳去拨它。
让客户端的连接池立即丢弃死连接,下一次请求直接走直连,不会复用一条已经断掉的隧道。
因为客户端连接的是「永远在线」的 relay,代理的生死只会影响 relay 到上游那一段: 路由本身不会失效,新的模型请求和 web_fetch 请求会由 relay 直接改走直连, 既不会反复重试死端口,也不会因为路由还指着一个没人的端口而卡住。 只有「代理断掉的那一刻已经在传输中」的请求会断一次,这是传输层事实,见下一节。
任何进程都无法把这条 TLS 流迁移到直连。dsh-llm-retry 会立即重跑这一步。
dns.lookup() 会卡满resolveTimeoutMs(5 秒)后才失败,于是出现第二次失败。现在 OS 解析与 c-ares 并行,c-ares 使用系统 DNS 服务器直接发新查询,通常在毫秒级返回,所以第一次重跑 就能成功(relay.dns.lastSource = "cares" 可以看到)。
noProxy(例如 api.deepseek.com)。这些主机的流量会直接走 relay 的直连分支,不再经过 本地代理,代理关闭对它们没有任何影响。设置页里可以直接改 noProxy。
dsh plugin --profile web add link:/path/to/dsh-proxy-failover仓库已公开:。 npm 尚未发布,所以除本地目录外只有 GitHub 源可用:
dsh plugin --profile web add github:PILIHU2022/dsh-proxy-failover也可以让插件管理器的 install_bundle 指向本地目录。安装后 bundle patch 会插入一行:
- insert:
- id: proxy-failover
name: dsh-proxy-failover
config:
proxy: auto更新插件代码后,重新启用该行(或重启 DSH)即可加载新代码。
不要同时启用两个插件。 两者都会安装进程级路由,语义会互相覆盖:
reconcile 时看到已有 proxied 路由,会直接让路,通常不会打架;对方那条「指向真实代理」的路由,代理关掉后又会复发。
推荐保留本插件、禁用 dsh-web-fetch-proxy(如果已经安装)。 本插件在功能上是它的超集:既修 fake-ip,也修代理关闭后的模型重试。
cordis.patch.yml 的 config(部署级):
字段 · 默认 · 说明 enabled · true · false 时完全不介入,保持宿主原有路由 proxy · auto · auto 自动发现;off/none/direct 强制直连;也可写死 http://127.0.0.1:7890 noProxy · "" · 追加到 no_proxy,逗号分隔;回环地址始终绕过 probeIntervalMs · 1500 · 上游探活 / 重新发现间隔(默认 1.5 秒) probeTimeoutMs · 400 · 单个候选端口 TCP 探测超时 connectTimeoutMs · 10000 · relay 连接上游 / 目标的超时 handshakeTimeoutMs · 10000 · 上游 CONNECT 握手超时 resolveTimeoutMs · 5000 · 直连路径的 DNS 总超时(OS 与 c-ares 并行,先到先用) listenPort · 0 · relay 端口;0 表示随机端口,仅监听 127.0.0.1 candidatePorts · 常见代理端口 · 7890, 7897, 7891, 7899, 10809, 10808, 1080, 2080, 20171, 8889
proxy: auto 的发现顺序:显式配置 → HTTPS_PROXY/HTTP_PROXY/ALL_PROXY (大小写都读)→ Clash/Mihomo/Clash Verge 配置里的 mixed-port → 常见端口。 环境变量和 Clash 配置视为可信;端口探测出来的候选会额外做一次 CONNECT 握手, 避免把恰好占用 7890 的普通服务误当成代理。
代理容错出网 会出现在 设置 → 常规(settings.general.item),提供:
lastError;noProxy 绕过列表;重新检测 按钮。保存走 proxy-failover 设置命名空间(两个字段都是 volatile,由 Loader 原地更新, 不会重启插件或更换 relay 端口)。宿主侧另有:
# 读取状态
curl -s http://127.0.0.1:3080/proxy-failover/status | jq
# 重新探测上游
curl -s -X POST -H 'content-type: application/json' \
--data '{"action":"redetect"}' http://127.0.0.1:3080/proxy-failover/api
# 立即应用配置(不持久化;持久化请用设置页)
curl -s -X POST -H 'content-type: application/json' \
--data '{"action":"configure","config":{"proxy":"auto","noProxy":""}}' \
http://127.0.0.1:3080/proxy-failover/api首次加入设置页需要重启一次 DSH。 当前进程已经加载了旧的客户端模块清单,
Web 客户端的包扫描带有“非客户端包”负缓存;重启后 lib/client.js 才会进入boot graph。宿主端的状态接口和设置命名空间已经生效,不依赖重启。
只读状态接口(仅回环地址可访问,GET):
curl -s http://127.0.0.1:3080/proxy-failover/status | jq{
"ok": true,
"value": {
"installed": true,
"plugin": "dsh-proxy-failover",
"routed": true,
"relay": {
"listen": "http://127.0.0.1:43957",
"requests": 42,
"fallbacks": 1,
"upstream": { "url": "http://127.0.0.1:7890", "source": "port-probe:7890" },
"upstreamUp": false,
"lastError": "连接上游失败: connect ECONNREFUSED 127.0.0.1:7890",
"tunnels": { "active": 2, "closedOnProxyLoss": 3 },
"dns": { "fallbacks": 1, "failures": 0, "lastMs": 0, "lastSource": "cares" }
}
}
}routed: true:proxyRouteFor() 确实指向本 relay,web_fetch 会跳过本地 DNS 校验;upstreamUp: false + fallbacks > 0:代理已经关闭,最近的请求走了直连;requests 包含模型 API、web_fetch、MCP 等所有经全局 dispatcher 的请求;tunnels.closedOnProxyLoss:代理失联时主动关闭的旧隧道数;dns.lastSource = "cares":这次直连是 c-ares 先返回的(说明系统解析正好卡住,备用解析救了场)。日志里能看到的状态迁移:
[proxy-failover] 上游代理已启用:http://127.0.0.1:7890(来源:port-probe:7890)
[proxy-failover] 上游代理 http://127.0.0.1:7890 不可用,后续请求自动直连:连接上游失败: connect ECONNREFUSED ...
[proxy-failover] 上游代理已启用:http://127.0.0.1:7890(来源:port-probe:7890)CONNECT/请求转发到回环、链路本地(含 169.254.169.254 云元数据)、组播、6to4、NAT64 等地址;
这与真实代理的直连行为一致。需要更严格时可在 noProxy 或代理规则里限制;
web_fetch 的 SSRF 校验在「已代理」时本来就会被跳过,本插件沿用这一语义;上游代理可用时由代理决定去向,上游不可用时由 relay 做上述地址过滤;
apply()、探活、状态接口全部包在 try/catch 里;