dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
在 dsh 运行过程中热切换出站代理(直连/系统/自定义),无需重启即可生效
在 dsh 运行过程中 热切换出站代理:直连 / 跟随系统环境变量 / 使用已保存的多个代理 (HTTP · HTTPS · SOCKS5)。切换入口在 dsh 设置 → Proxy,改完立即生效,不需要重启 dsh。
dsh 的出站请求(LLM 对话、网页搜索、web_fetch、模型发现、MCP over HTTP)都走宿主进程的
全局 fetch。它背后的代理是 Node 的 NODE_USE_ENV_PROXY + HTTP(S)_PROXY,而 这个选择在
进程内只会被解析一次:
process.env.HTTP_PROXY → 无效(实测:指向一个死端口仍然照常走原代理);所以过去换代理只能重启 dsh。本插件把「用哪个代理」从 启动时决定 变成 请求时决定。
Node 内置 fetch 从 globalThis[Symbol.for('undici.globalDispatcher.1')] 取传输层;npm 版
undici 的 setGlobalDispatcher() 写的正是这个槽位(以及 .2)。因此:
node_modules/undici 的 setGlobalDispatcher(agent) → 内置 fetch 立即改走 agent
实测(probe/probe-dispatcher-placements.mjs,Electron-as-Node 24.18.1):
| 放置方式 | 结果 |
|---|---|
全局槽位 + Agent / Agent{factory} / ProxyAgent | ✅ 全部生效 |
全局槽位 + ProxyAgent("socks5://…") | ✅ SOCKS5 生效 |
显式 fetch(url, { dispatcher }) + 8.x dispatcher | ❌ UND_ERR_INVALID_ARG: invalid onRequestStart |
undici.fetch(url, { dispatcher }) 同实例 | ✅ 生效(dsh-web-fetch-http 用的就是这种) |
原因:内置 fetch 自带一份 undici(24.18.1 内是 7.29.0),而宿主 undici 是 8.11.2。
全局槽位由 Node 做版本适配,显式传入则原样使用,跨代就报错。所以本插件 只通过全局槽位
切换,不包装 fetch。
0.2.x 的 dsh 已经自带 @deepseek-ai/dsh-http-proxy(README 明说"SOCKS/PAC/系统代理检测不支持")。
插件优先调用它导出的 installProxyFromEnvironment(env, report):
proxyRouteFor()(dsh-web-fetch-http 靠它决定是否 pin 地址)、子进程继承的代理环境变量。ProxyAgent 原生支持
SOCKS5;7.x 则回退到内置的手写 SOCKS5 connector)。没有 dsh-http-proxy 的老版本 dsh(例如本地 0.1.0-rc.7 检出)会全部走自建路径,插件仍可用。
切换时旧的 dispatcher 用 close() 释放(不是 destroy()):已在途的请求在旧传输上跑完
(流式回答不会被打断),新请求用新策略。
安装的传输层是策略快照(这也是官方 seam 的设计,保证只有一个真值来源)。所以改了
http_proxy 之后,点设置页的 Re-read env & apply(或 /proxy on)即可,无需重启。
pwsh -File install.ps1 # 默认 desktop profile
pwsh -File install.ps1 -Profile web
脚本做三件事,全部可逆:备份 profile 的 package.json → 在
<profile>/node_modules/ 建一个指向本目录的 junction → 把包名写入 dependencies 和
dsh.profile.bundles。不跑 pnpm、不联网、不动 lockfile。
安装后需要让 dsh 重新加载 loader,插件才会出现在 设置 → Proxy。在此之前它只是磁盘上的文件, 不影响任何行为。
也可以用 dsh 官方安装器:
dsh plugin --profile desktop add link:<本目录>,它会用 pnpm 接管 同样的三个步骤。
pwsh -File uninstall.ps1 # 删 junction + 撤销 package.json 改动
pwsh -File uninstall.ps1 -RestoreBackup # 另外还原安装前的 package.json
卸载后同样需要 dsh 重载 loader。卸载不影响 dsh 原有功能:插件在 unload 时会把启动时的 dispatcher 和代理环境变量原样放回(有测试覆盖,见验收 8)。
若要彻底忘记保存的代理,删掉 $DSH_HOME/proxy-switcher.json 即可。
升级:直接用新版本覆盖本目录(junction 指向的就是它),然后重载 loader。
回滚:保留旧版本目录,把 junction 重新指过去,或用 -RestoreBackup 还原 profile。
Settings → Proxy 一个页面,包含:
Use a proxy 总开关 + 三选一(Direct / System / Saved proxy)。Test / Edit;Add proxy 新增。no_proxy。可单独 Test this proxy。no_proxy、探测 URL、超时、代理缺失时的行为(拒绝请求 / 直连)、
连续失败自动回退。一键切换(开关、模式单选、代理单选)会立即生效并保存;编辑器里的改动需要 Save and apply。
页面上的 temporary 语义由 API 的 persist:false 提供(当前进程内生效,不写入文件)。
配置文件:$DSH_HOME/proxy-switcher.json(可用 DSH_PROXY_SWITCHER_CONFIG 覆盖)。
示例见 config.example.json。
字段:enabled · mode(direct|system|custom) · activeProfile · profiles[]
(id,name,protocol,host,port,username,password,passwordEnv,noProxy) ·
noProxy · bypassLoopback · probeUrl · timeoutMs · onMissingProfile(block|direct) ·
fallback{mode,failureThreshold} · historyLimit。
运行时设置(设置页 / /proxy / API) > 环境变量 > 配置文件 > 默认值
环境变量:
| 变量 | 作用 |
|---|---|
DSH_PROXY_SWITCHER_ENABLED | 1/0 总开关 |
DSH_PROXY_SWITCHER_MODE | direct / system / custom / off(=关闭代理) |
DSH_PROXY_SWITCHER_PROFILE | 选择已保存代理的 id |
DSH_PROXY_SWITCHER_URL | 直接用一条代理 URL(最高优先,隐式 custom) |
DSH_PROXY_SWITCHER_NO_PROXY | 覆盖 no_proxy |
DSH_PROXY_SWITCHER_CONFIG | 配置文件路径 |
/proxy 命令:status · on · off · direct · system · use <id> · temp <id> · test [id]ctx.proxySwitcher.status()/config()/configure(patch)/applyRuntime(patch)/reset()/test(candidate)GET state|status,POST apply|reset|reapply|test
于 /dsh-proxy-switcher/*password: "" + hasPassword + 掩码预览
(socks5://user:***@host:port),未重填时回传哨兵值表示"保持不变"。redactProxyUrl() 统一处理,含错误的 cause 链。
(这一条是实测发现并修掉的真实缺陷:状态曾把实时代理环境变量原样带出。)passwordEnv:密码留在环境变量里,配置文件不落盘,且每次请求重新读取,轮换无需重启。
写文件时用 0600 权限(Windows 上依赖用户目录 ACL)。| 项 | 支持范围 |
|---|---|
| dsh | 0.2.0-rc.1+(自动复用 dsh-http-proxy);更早版本走自建路径 |
| 运行时 | Node 22.19+ / 24+(实测 Electron 44 ↔ Node 24.18.1,bundled Node 24.21.0) |
| 操作系统 | Windows 实测;实现只用 node:net / node:tls / undici,无平台专属代码 |
| 代理协议 | http · https · socks5 / socks5h(域名远端解析) |
| 覆盖的流量 | 所有走全局 fetch 的路径:LLM(deepseek / pi-ai)、web search、web_fetch、MCP HTTP |
node:http 自建 agent(dsh 官方也这么记录)。需要时用
DSH_TELEMETRY_MODE=DISABLED 关掉。web_fetch 会直连。dsh-http-proxy 按设计拒绝 SOCKS,于是
proxyRouteFor() 报"未代理",web_fetch 就用它自己 pin 地址的 agent 直连。其他链路
(LLM、搜索)在 SOCKS5 下正常。要覆盖它必须改 dsh 自身。system 模式不是每请求重读环境变量(见 2.4)。onMissingProfile 默认 block(宁可明确报错也不静默直连,避免误以为走了代理)。安装后 无需重启:loader 直接把插件挂进了运行中的 dsh。实测接口返回:
GET /dsh-proxy-switcher/status
ready=true configuredMode=system effectiveKind=proxied
effectiveProxy=http://127.0.0.1:7890 reason="env proxy"
transport.strategy=delegate
transport.capabilities.dshHttpProxyVersion=0.2.0-rc.2
transport.capabilities.undiciSource=dsh-http-proxy -> ...\undici\index.js
transport.capabilities.undiciVersion=8.11.2 nativeSocks5=true
即插件确实复用了官方 seam,并且取的是宿主那份 undici 8.11.2(不是 profile 提升的 7.30.0)。
热切换:POST /apply {persist:false, override:{mode:'direct'}} → 14ms 内 effectiveKind 变为
direct;POST /reset → 立即回到 http://127.0.0.1:7890,无错误、无回退。切换全程未重启 dsh。
未能确证的一点:切到 direct 后,用 dsh 自己的 web_fetch 访问 Google 仍然成功。可能原因是本机
直连 Google 本来就可通(或 web_fetch 有缓存),因此「切 direct 后连接真的走直连」这一条
没有在本机得到决定性验证。单测里有决定性的等价验证(test/transport.test.mjs
用本地假代理证明请求确实换了出口),但真机上的这最后一步需要你在能直连/不能直连的网络里各试一次。
pwsh -File test/run-tests.ps1
42 个用例 / 4 个文件,全部用本地假代理,不联网、不碰真实代理:
| 文件 | 覆盖 |
|---|---|
test/config.test.mjs | 代理 URL 解析、优先级链、no_proxy 匹配、脱敏、密码哨兵、原子落盘 |
test/transport.test.mjs | 运行时热切换、直连、恢复、SOCKS5(含鉴权与拒绝)、凭证、no_proxy、undici 实例选择、直连 dispatcher 回归 |
test/switcher.test.mjs | 逐条验收(1–8):开关/模式/保存生效/状态/错误提示/重启恢复/停止还原 |
test/plugin-contract.test.mjs | 宿主半:路由/命令/服务/卸载;浏览器半:__ModuleLoader__ 契约、slot 注册、可渲染、缺 primitive 降级 |
probe/ 下保留四个决定性探针(环境变量是否可变、全局槽位是否可热切换、dispatcher 放置组合、
undici 实例选择),它们是本文件所有机制断言的原始证据。
lib/
proxy.js 代理规格解析、no_proxy 匹配、SOCKS5 connector、脱敏
config.js 默认值/校验/四层优先级/原子持久化
transport.js 全局 dispatcher 安装(委托官方 seam 或自建)、环境变量发布、undici 实例选择
switcher.js 策略解析、应用、状态、历史、连通性测试
routes.js 设置页的 HTTP 接口(同源校验、脱敏)
index.js 宿主半:Cordis 生命周期、路由、/proxy 命令、plugin API
client.js 浏览器半:设置页(手写 classic script,无需构建)
cordis.patch.yml 挂载行
notes/ 两篇实现分析报告(代理路径 / 插件与设置扩展机制)