dsh-tizi-switch
bzyyyy-china/dsh-tizi-switch
在 DSH 运行中随时开关出网代理,不用重启也不改动操作系统代理;代理客户端退出时自动释放回直连。
安装
dsh plugin --profile web add github:bzyyyy-china/dsh-tizi-switchREADME
梯子开关
在 DeepSeek Harness 运行中随时切换出网代理 —— 不重启、不改系统代理、不用配端口。
它解决什么问题
DSH 只在启动时解析一次代理策略(HTTP_PROXY / HTTPS_PROXY),之后整个进程生命周期都不再改变。于是只有两个都不好的选择:
- 启动时代理客户端没开 → 整个进程一直是直连,想翻墙只能重启 DSH
- 把代理写死在环境变量里 → 客户端一关,所有请求指向死端口,DSH 直接瘫痪
本插件复用 DSH 自带的 @deepseek-ai/dsh-http-proxy 包(它的 install 本就返回 disposer),在会话继续运行的同时替换进程级的 undici dispatcher。
它会自己找到你的代理端口
Clash Verge 监听 7897,v2rayN 的 HTTP 入站在 10809,sing-box 在 2080,裸 shadowsocks 客户端在 1080。把其中任何一个当成默认值,都会让用别的客户端的人装完即失效——所以插件不假设端口,而是按顺序问这台机器:
- 代理环境变量 —
HTTP_PROXY/HTTPS_PROXY/ALL_PROXY - Windows 系统代理 — 客户端自己发布到注册表的地址
- 常见客户端默认端口 — 十五个,覆盖 Clash Verge、v2rayN、mihomo、sing-box、v2rayA、Hiddify 等
探测只在插件加载时做一次,结果缓存到进程结束。之后每次开/关代理都用缓存,不会再扫端口——反复遍历本机端口正是安全软件判定端口扫描的行为。proxy_status 会显示「本次已探测 N/3 次」,正常恒为 1/3;整个进程最多 3 次,不会因为你反复点「开代理」就反复探测。
探测结果只存内存,不写你的配置文件。 说「开代理」时直接用这个结果装上,不用你手填端口。
两种情况会自动重探,都不做全端口扫描:
- 加载时什么都没找到(先开 DSH、后开客户端)→ 下次开代理时重探
- 缓存的端口已经不通(换了客户端,或客户端关过)→ 重探到新端口
第 2 条很关键:只信加载时的缓存的话,换了客户端之后插件会一直报「找不到」,哪怕新客户端跑得好好的。
但装之前一定确认端口是通的。 缓存只是「上次看到它在哪」,不是承诺。装死端口会让每个请求 ECONNREFUSED,那正是这插件要防的事。所以 proxy_on 安装前做一次回环连接确认(只连这一个已知端口,不枚举任何东西)。通了才装,不通就说明「客户端可能已退出」并保持直连。
可选:扫描全部监听端口
客户端改到了非常见端口、上面三步都找不到时,可以显式开启:
- id: tizi-switch
config:
scanListeningPorts: true
默认关闭,因为全端口扫描更容易触发安全软件告警;常见端口已覆盖绝大多数人。
开启后整个进程最多扫一次(加载那次),后续重探只读环境变量、系统代理和那 15 个常见端口。proxy_status 会显示「已用过一次全端口扫描」。
端口能连上,不等于它是代理
DSH 自己的 Web 界面、开发服务器、本地数据库都接受连接。把 dispatcher 指过去,每个出网请求都会失败——所以候选端口必须应答 HTTP CONNECT 握手,这是 HTTP 代理才实现的协议,普通 Web 服务不会正确回应。
探测的目标固定为 127.0.0.1:9(没有服务的丢弃端口),因此握手不会真的建立隧道,探测过程不会向你当前的代理节点发送任何流量。
SOCKS 端口会被识别,但不会被使用
DSH 只接受 http:// / https:// 代理 URL。如果只找到 SOCKS 端口(v2rayN 的 10808、v2rayA 的 20171),插件会明确告诉你「这是 SOCKS 端口,请在客户端里再开一个 HTTP / mixed 入站」,而不是假装切换成功。
四个工具
| 工具 | 作用 |
|---|---|
proxy_on | 自动找到本机代理端口;通了就装上并立即生效,不通则说明原因并保持原路由不变 |
proxy_off | 卸掉代理,恢复直连,立即生效 |
proxy_status | 只读:现在走不走代理、用的是哪个端口、端口通不通 |
proxy_detect | 只读:列出本机找到的所有代理端口,以及每个端口被接受或拒绝的原因 |
自动回退:客户端退出后不再断网
代理装上后,本进程的每个请求都指向那个端口。客户端一关,请求会以 ECONNREFUSED 在毫秒级失败——从使用者的角度看就是「网断了」。
所以插件每 3 秒探测一次端口,发现不通就自动卸掉代理、回到直连:
| 阶段 | 结果 |
|---|---|
| 装代理后请求 | HTTP 200 |
| 关掉代理端口 | ECONNREFUSED(约 2 ms) |
| 健康检查触发(≤3 s) | 自动回退到直连 |
| 回退后请求 | HTTP 200 |
为什么不自动重新开启:代理客户端的端口在启动阶段可能反复抖动,自动恢复会让路由来回切,状态不可预期。所以回退是单向的;客户端重新就绪后,说一句「开代理」即可。
用 healthCheckMs: 0 关闭自动检测。
安装
在 插件 页面安装,或者:
dsh plugin add dsh-tizi-switch
配置
无需配置。 端口由自动探测处理。
想固定端口、跳过探测:
- id: tizi-switch
config:
port: 7897
即使固定了端口,安装前仍会校验:死端口会被拒绝并保持直连,而不是把 DSH 弄瘫。
| 配置项 | 默认值 | 说明 |
|---|---|---|
host | 127.0.0.1 | 代理主机,探测和安装都用它 |
port | 不设=自动探测 | 指定后跳过自动探测 |
probeTimeoutMs | 400 | 单个端口的探测预算 |
healthCheckMs | 3000 | 对已启用端口的健康检查间隔;0 关闭 |
scanListeningPorts | false | 是否扫描全部监听端口(可能触发安全软件告警) |
reportLimit | 5 | proxy_status 最多显示几个端口 |
覆盖范围与边界
覆盖:模型调用、web_fetch、web_search、HTTP MCP —— 任何走 globalThis.fetch 的请求。
不覆盖(DSH 上游既定边界,不是本插件的取舍):
- 模型自己写的脚本子进程:它们刻意不继承可能带凭据的代理 URL
- 使用
node:http的遥测通道 - 指向字面私有地址的
web_fetch
只作用于本进程:改的是本进程的全局 dispatcher,不影响机器上任何其它程序。插件会读 Windows 系统代理来了解客户端监听在哪,但从不写入。
不支持:需要用户名口令的代理。会被识别并报告,但不会安装。
关键设计
切换前为什么先验证端口:死端口会快速 ECONNREFUSED。无脑装代理就等于把「忘了开客户端」变成「DSH 瘫掉」。所以 proxy_on 先完成 HTTP 握手,答不对就拒绝,并且不改动当前路由。
proxy_off 为什么不等 dispose:dispose() 尾部的 agent.close() 要等在途连接结束,可能几十秒;但头部还原 setGlobalDispatcher 是同步的——路由早就切回去了。等整个 dispose 只会让工具调用看起来卡死。
为什么用动态导入:插件以本地 link 或 profile 包形式存在时,profile 的 node_modules 解析不到 @deepseek-ai/*,所以经 DSH_INSTALL_DIR 用 file:// URL 导入。
探测为什么零依赖:所有探针都是裸 node:net socket,插件保持无第三方依赖。
兼容性
需要 DSH 0.2.2-rc.2 或更高。它依赖 @deepseek-ai/dsh-http-proxy 的 installProxyFromEnvironment 返回 disposer,这是热切换的前提。该 API 若变更,插件需要同步更新。