- Startseite
- Plugins
- Modelle & Anbieter
- dsh-provider-proxy
dsh-provider-proxy
hpfs0/dsh-provider-proxy
一个让provider会走代理的插件
Installation
dsh plugin --profile web add github:hpfs0/dsh-provider-proxyREADME
dsh-provider-proxy 
给 DeepSeek Harness(0.2.0-rc.1 及以上)用的插件:在 设置 里为每个 provider 单独配置「这个 provider 要不要走代理」,填写代理类型 + IP/端口。只有被你打开开关的 provider 走代理,其余 provider 和 DSH 的其它流量保持原来的直连路径。
支持官方桌面版(DSH Desktop)与 dsh web。
安装
先确保已构建过产物(lib/index.js、lib/client.js):
npm install
npm run build
桌面版(必须先完整打开一次桌面版以初始化 profile,然后完全退出,否则安装命令会拒绝执行):
dsh plugin --profile desktop add dsh-provider-proxy
Web:
dsh plugin --profile web add dsh-provider-proxy
安装后重启对应应用。设置项写在 $DSH_HOME/profiles/<profile>/cordis.patch.yml。
使用
打开 设置,左侧导航里会出现一项 Provider 代理(英文界面为 Provider Proxy)。 页面里每个 provider 一张卡片,各自独立配置:
| 字段 | 说明 |
|---|---|
| 开关 | 该 provider 是否走代理 |
| 使用默认代理 | 勾选后本卡片的传输字段整体取自页面顶部「默认代理」区块,无需逐个重填 |
| 类型 | HTTP / HTTPS / SOCKS5 |
| IP / 主机 | 只填 IPv4、IPv6 或 ASCII 域名;不要带协议前缀、端口、路径或凭据。IDN 需先转 punycode |
| 端口 | 1–65535 的整数 |
| 用户名 | 可选(与密码成对填写) |
| 密码 | 可选(与用户名成对填写)。作为代理认证口令发给代理(HTTP / HTTPS / SOCKS5 均支持)。默认隐藏,可用密码框内右侧的眼睛图标临时查看,切换不会改写或保存密码 |
同一个代理要给多张卡片用时,只在页面顶部的 默认代理 区块填一次,再在每张卡片勾上 使用默认代理。 勾上之后该卡片的传输字段(类型 / 主机 / 端口 / 用户名 / 密码)整体取自默认区块,卡片不再显示这些输入框, 改为显示它实际会用的地址——所以不会出现「填了一半、另一半不知道从哪来」的模糊状态。目标域名规则与自检目标 始终是卡片自己的。默认区块本身不可用时(未填地址、地址不是合法 IPv4/IPv6/域名、端口非法、凭据只填了一半), 勾了继承的卡片会被拒绝并说明原因(列在「连接诊断」里),卡片上也直接显示这条原因;默认块的「配置完整」 状态与批量按钮用的是同一个判定,所以不会出现「亮着绿灯却一张卡也用不上」。
端点输入不随开关变化:关闭的卡片照样可以填写、也可以测试。这样就能先验证再启用,避开了 「必须先打开代理才能测,而配置是坏的」这个窗口。
卡片头上的状态只描述路由:
| 状态 | 含义 |
|---|---|
| 配置被拒 | 已启用,但端点 / 凭据不完整或 Host 无法建立路由。该 provider 的请求不会经过本插件代理;原因直接显示在卡片内,也列在「连接诊断」里 |
| 走代理 | 已启用且配置可建立路由。单次请求仍可能因目标域名规则直连;它不代表代理可达,连通性看「测试」与「最近调用」 |
| 直连 | 未启用,流量走宿主原有路径 |
Host catalog 是否列出某个 provider(live)不影响已保存路由的卡片状态;它只决定未配置的 provider 是否出现在列表里。
页面上的几处便利:
- 过滤框:provider 多时按 id 或名称筛掉不必看的卡片。它纯粹是视图,不写任何设置。
- 粘贴完整代理 URL:把
socks5://user:pass@host:1080粘进任一「IP / 主机」输入框,会自动拆成 协议 / 主机 / 端口 / 用户名 / 密码,并且是一次写入(不是五个字段各写一次)。显式写出的:80/:443会保留;完全没写端口时保持「未配置」,不会自动补默认端口。 - 设为默认代理(卡片上,仅在该卡片有自己的端点时出现):把它当前的端点一次性提升为顶部的默认端点。 如果默认端点已配置且有卡片正在继承,会先弹确认框;取消后不写入、不改草稿。
- 所有 provider 使用默认代理(N)(默认区块里):N 是全部 provider 的数量,不受过滤框影响。 一条批量写把每张卡片都指向默认端点;默认端点不可用时按钮禁用。
- 最近一次调用:卡片头上会显示「最近成功 3 分钟前」,失败时显示「最近失败 ECONNREFUSED」。
命中计数只说明这条路由被用过,这一行说明它现在是否还工作。已启用但还没代理过请求的路由显示
「尚无代理记录」;测试按钮不计入——它证明的是「代理这一跳通」,不是「这条路由承载过真实流量」,两者混在一起会让这一行的含义不可信。
「最近成功」的口径是拿到了 <500 的响应,而不是「整个流跑完」——流式中途被切断仍会记为成功,
因为成功判据在响应头到达时就已成立(流式转发不能靠消费 body 来判断)。
「最近调用」的时间取请求发起时刻;「最近成功 / 失败」的结果与时间来自最近一次结束的请求——
两者分开记录,并发调用下不会把一次请求的发起时间配到另一次的结果上。
这条记录只属于当前路由配置:改代理端点、凭据、
useDefault的默认端点、目标域名规则或localDirect后,该 provider 的「最近调用」会变成「尚无代理记录」;testUrl/testTarget只影响测试,不会清掉它。仅关闭再打开代理开关也保留记录。修改配置前已经启动的请求不会把旧结果 写回新配置;删除后重新添加同名 provider 同样视为一条新路由。hits命中计数是历史累计, 上述配置变化都不会清空它。
页面顶部与顶层还有三项整页设置(写在 cordis.patch.yml,也可以直接编辑):
| 顶层设置 | 默认 | 说明 |
|---|---|---|
defaultProxy | 未配置 | 供勾选「使用默认代理」的卡片复用的端点 |
localDirect | true | 本机 / 内网目标是否直连(见「目标域名规则」一节) |
testUrl | https://api.ipify.org?format=json | 自检的最后兜底目标;可写多个(逗号或空格分隔,上限 5 个),按序回退 |
其中的 localDirect 与 testUrl 也可以在默认代理区块下方的 全局设置(高级) 折叠块里修改:
testUrl 留空时 Host 使用内置默认值,非法候选或超过 5 项只会提示、不会阻止保存;
localDirect 默认勾选。修改任一项都会清空页面上所有自检结果,并且点「测试」前会把待写设置先落盘。
列表只包含已激活的 provider(llm.listProviders() 能列出的那些)。仅在配置里声明、
但没有实际注册的可配置 provider 不显示——它的请求根本不会被发出去。
已经在 providers 里配过代理的条目即使暂时不激活也仍然保留,避免已保存的设置悄悄消失;
catalog 的 live 只影响未配置 provider 是否出现在列表里,不参与已保存卡片的状态判定。
如果 Host 目录读取成功,但某个已保存的 provider 完全没有出现在目录里,页面不会隐藏它,
而是在「连接诊断」里列出这些 provider,并说明代理配置仍会保留。目录读取中或读取失败时不会显示
这条提示,避免把所有已保存 provider 误报为缺失。live: false 只表示目录中存在但当前未激活,
不算缺失。
保存立即对新请求生效,不需要重启;正在进行中的流会继续用它启动时的那条连接。
设置写入失败时,字段会回滚,并在页面顶部显示「保存失败:<字段> — <原因>」和关闭按钮;下一次成功写入或点击关闭后消失。多次失败只保留最新一条。写入成功与失败的诊断都不会包含密码值,密码字段固定显示「值已隐藏」。
每张卡片还有一个 测试 按钮:点它会让 Host 通过该 provider 自己的代理发一次请求,回报 HTTP 状态、延迟、脱敏后的探测目标与(探测目标返回时)出口 IP;冷却命中时标注「缓存」。只要该卡片有完整端点它就会出现——不必先启用;未启用的检查会标注为「未启用,仅测试」。代理不可达时它会明确失败,而不是假通过。结果与当前配置绑定:改端点 / 凭据 / 自检目标 / 域名规则 / 全局设置后旧结果会立即消失;点「测试」前会先把防抖中的设置写入,Host 测的是刚填的值。
探测目标在 Host 侧脱敏后才进入响应:合法 URL 只保留 origin + pathname,userinfo、query 与 fragment 都不会出现在结果、诊断或错误文本里;非法 URL 也会去掉 userinfo / query / fragment 并截断长度。网络错误同样不会回显原始目标。
目标与判定口径:目标按三级取用——卡片的 自检目标 → 该 provider 的真实上游(见下一节)→ 顶层 testUrl(可写多个候选,按序回退,命中任一即停;407 会提前终止,因为那是代理的问题)。结果按状态码分类:
结果行会标出这次用的是哪一级:(本卡片自检目标) / (该 provider 的真实上游) / (全局自检地址)。
所以「改了 testUrl 但某张卡片没变」是可以解释的——该 provider 自己发布了真实上游,本就不读 testUrl;
要覆盖它,请在那张卡片的「高级 → 自检目标」里显式填写。
| 状态 | 含义 |
|---|---|
407 | 代理要求认证 |
2xx / 4xx(非 407) | 已达上游,链路通。真实上游对一次裸 GET 正常就是回 401/404,那正是「通了」的证据 |
5xx | 疑似代理或上游故障。HTTP 代理够不到上游时会自己回 502/503/504 |
抛出(ECONNREFUSED、超时等) | 按失败原因分类,不会假通过 |
延迟数值按快慢着色——≤500 ms 绿、≤1500 ms 黄、更慢红——但只有延迟数字带这个颜色, 状态码、目标与其它文字用普通标签色。一次超过 1500 ms 的成功检查仍然是成功,不会整行变红。
自检遵循与真实流量相同的代理 / 直连判定。 目标命中本机 / 内网地址、bypassHosts,或落在 onlyHosts 之外时,自检与路由一样直连——不把一个到不了该目标的请求塞给代理,也就不会把一条健康路由报成「代理坏了」。直连成功时卡片会标注「· 直连(未经过代理)」,直连失败则报「目标不可达」而非指向从未使用的代理。
卡片的「高级」里还有一个 自检目标(testTarget):留空时先用该 provider 适配器发布的上游地址(Host 经 ctx.settings 按该 provider 的 settings 路径读 baseURL,取不到再退回顶层 testUrl);填写后该卡片一律请求这个地址。它只被自检读取,路由本身不受影响;填错了只会让一次检查失败,不会拒绝该路由。
卡片里还有一个折叠的 高级:目标域名规则,按目标主机决定是否走代理:
| 字段 | 说明 |
|---|---|
仅走代理(onlyHosts) | 逗号或空格分隔。非空时,只有匹配这些主机的请求走代理,其余直连。 |
直连(bypassHosts) | 逗号或空格分隔。匹配这些主机的请求直连,优先于 onlyHosts。 |
两者都按后缀匹配(example.com 覆盖 api.example.com),* 表示全部,大小写不敏感。默认是 fail-closed:启用了代理的路由默认走代理,只有三种显式例外才直连——「目标命中本机 / 内网地址」「bypassHosts 命中」「onlyHosts 非空且未命中」;解析不出目标主机时仍走代理,避免畸形 URL 把带凭据的请求悄悄直连出去。
指向本机或内网的请求默认直连。 目标主机命中 localhost(含子域)、127.0.0.0/8、::1、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、169.254.0.0/16 时不经代理。这类目标通常是跑在同一台机器上的中转(LiteLLM、Ollama、本地 mock),正向代理根本到不了它,硬走代理只会失败,而且失败看起来像「代理坏了」。顶层 localDirect: false 可关掉这条(例如代理本身就是目标)。
带协议前缀(https://…)、路径(example.com/v1)、端口(example.com:443)或杂散通配符(*foo、foo*)的条目永远匹配不上——规则只认主机名,通配符只能是单独的 * 或以 *. 开头;单独的 *. 也视为死条目。方括号 IPv6 字面量(如 [::1])是合法的。这类条目会在输入框下以错误色标出,因为后果是静默的:onlyHosts 里的死条目会让 fail-closed 把流量直连出去,而用户以为已经走代理。Host 与设置页使用同一份共享解析 / 判定逻辑。
当组成里存在 web 服务时,列表还会多出一张 Web 搜索 卡片(保留 id web-search,字段与其它卡片完全相同,也支持自检、计数与域名规则)。它承载 web_search 工具的流量;不启用它,搜索保持直连。
为什么是独立设置页,而不是内嵌在 Models 的 provider 卡片里? 本插件最早按卡片内嵌(
settings.models.provider-card)实现,但那个槽位是 keyed 的、 按 provider 命名空间派发,第三方注册在 0.2.0-rc.1 上始终匹配不上——不报错、不发请求、 界面上什么都不出现。独立分区(settings.section)是同一 profile 下dsh-llm-retry-settings已被验证可用的机制。
它是怎么工作的
0.2.0 没有「给单个 provider 注入 transport」的官方口子,这是核对过源码的结论:
llm-deepseek直接调用全局fetch,请求参数里没有dispatcher;llm-pi-ai走 pi-ai SDK 自己的 HTTP,DSH 无法配置;LlmCallControls只有reasoningEffort/temperature/maxTokens/stop;llm/stream拦截的是已解码的StreamChunk,发生在 HTTP 之后;ctx.web只服务web_search/web_fetch,与 provider 流量无关;- 官方的
@deepseek-ai/dsh-http-proxy是库不是插件:installGlobalProxy是模块私有函数,没有 service、没有 Config、只有从环境变量解析出的一份进程级策略。
所以请求在唯一还能拿到线索的地方被路由:llm/stream waterfall 的 options.provider。用一个 AsyncLocalStorage 把选中的 dispatcher 带到流上,再由一层很薄的 globalThis.fetch 包装把它交给 fetch。
两个关键点:
- 每个 provider 各自持有一个 dispatcher(而不是共用一个),所以即使两个 provider 指向同一个上游主机,也不可能互相串用对方的代理。
- 流的惰性消费必须被处理:adapter 的 HTTP 调用通常发生在后续的
next()里,那时als.run()早已返回。所以每次迭代器的next/return/throw都会被重新包进作用域里 —— 这正是旧版实现在这里踩过的坑,仓库里有对应的回归测试。
代价是这一个全局 fetch 包装:作用域之外的行为与原生完全一致,作用域之内若调用方已经自带 dispatcher 也不会被覆盖。
已知限制
- SOCKS5 由
undici内置的Socks5ProxyAgent实现(RFC 1928 / RFC 1929),API 稳定性标记为 Experimental;undici若在未来移除,需要同步调整。 - 代理地址只接受 IPv4、IPv6 或 ASCII DNS 域名。 IDN 需先转 punycode;下划线、纯数字单标签(如
127),以及把端口写进主机字段(127.0.0.1:7890)都会被拒绝。粘贴完整 URL 时显式:80/:443会保留,未写端口仍保持「未配置」。旧配置里不合规的主机名升级后会显示「配置被拒」,需要改成合法地址。 - 代理认证字段成对填写:用户名与密码要么都填,要么都不填。 只填任一项都会被拒绝并给出提示;用户名在配对前会
trim(),纯空白的用户名视为未填。SOCKS5 侧的依据是Socks5ProxyAgent仅在两者齐备时才发起认证——只填用户名会静默广告无认证,只填密码则密码根本不会被发送(对开放代理能用,对要求认证的代理必然失败,且失败原因看起来像密码错误);HTTP/HTTPS 侧基本认证本可只发用户名(alice:,空密码),这里仍按成对拒绝:半填的凭据几乎总是漏填,三种传输共用一条规则比「哪种传输能承载哪种组合」更好记。代价如实写明——凭据真的使用空密码的代理无法在这里配置。不合规则的配置会被拒绝,原因列在设置页的「连接诊断」面板里(该路由的「测试」也会返回同一条),而不是被静默忽略——否则在强制认证的代理端,报错会指向代理而真正原因是设置缺项。 - SOCKS5 不支持
socks://别名与 SOCKS5 over TLS(proxyTls)。 bypassHosts命中时由本机解析并连接目标(DNS 不经过代理)。SOCKS5 下这一点后果最明显:命中的域名不再由代理端解析,本机 DNS 会暴露该次查询。web_search可以走代理,web_fetch不行。 搜索 provider(dsh-web-search-deepseek)用全局fetch,插件通过包裹ctx.web.search把它送进代理作用域;这条成立的前提是:工具每次调用都重新取ctx.web.search,且 provider 的search()是 await-to-completion(不是惰性消费的流)。web_fetch则用undici自带的dispatcher(来自官方的进程级 env 策略),没有 per-provider 注入点,本插件覆盖不到——需要覆盖它请改用HTTP_PROXY之类的环境变量。- 保留 id
web-search。 它的路由存在providers字典里;真实 provider 若恰好撞名,会与该路由共用同一份配置,无害。 - 上游若绕过
globalThis.fetch自行建连,则不受影响。llm-deepseek确认是调用时解析全局fetch;llm-pi-ai走外部 SDK,抓取fetch的时机未能在源码层面确证。 - 不支持 PAC,也不读取系统代理设置。
- 不改写
baseURL—— 反向代理/中转请直接在 provider 上配置。 - 代理不可达时请求照常失败,DSH 既有的重试与错误码语义不变。
- 连接自检的目标可能不可达。 默认先尝试该 provider 适配器发布的上游地址,取不到才退回顶层
testUrl(默认是回显服务,部分网络下不可达,请换自建服务)。自检只接受POST,同一 provider 的在飞请求会合并;配置未变时,完成后的 3 秒内重复点击直接返回上次结果(标记cached),一旦改动配置(端口 / 地址 / 凭据 / 自检目标 / 域名规则 /localDirect/testUrl)即重新外呼。未启用的检查用完即释放临时 dispatcher——不会被当成外呼放大器。 - 代理密码以明文随设置写入
cordis.patch.yml。 HTTP/HTTPS 的密码会拼进交给undici的代理 URL(ProxyAgent由此生成Proxy-Authorization),SOCKS5 的密码只作为选项传给Socks5ProxyAgent;两者都不会出现在日志或诊断里的代理 URL——那里只显示不含凭据的 origin。 - 设置页的两个端点没有自己的鉴权。
/dsh-provider-proxy/providers(provider 列表、计数、被拒原因)与/dsh-provider-proxy/test的访问边界完全由 DSH 的 webServer 决定。webServer 只绑定本机回环时没有顾虑;若它绑定到非 loopback 地址,同网络的其它主机可以读到这些信息并触发自检外呼(自检有在飞合并、8 KiB 请求体上限与 3 秒冷却,但仍是可触发的外呼)。
如何验证代理确实生效
插件本身不显示出口 IP。可靠的判断方式是在代理端看日志,或临时把端口改成
一个没人监听的端口——如果请求失败并报 ECONNREFUSED 127.0.0.1:<该端口>,
就说明请求确实是被代理承载的(直连时报错会指向目标站点)。
仓库里的 tests/proxy-routing-proof.test.ts 就是用这个思路做的:起一个会记录
连接的假代理,断言「启用的路由收到 1 次连接、未配置的 provider 收到 0 次」。
开发
npm run build # 构建 lib/index.js 与 lib/client.js
npm test # 274 个用例,跑的是构建产物
npm run typecheck # 类型检查(仅覆盖 src/)
npm run verify # 上面三件事,按顺序
lib/ 是构建产物,不入库;包上的 prepare 脚本会在 npm publish 以及从 git
地址安装时自动重建。直接 dsh plugin add <克隆目录> 前请先执行
npm install && npm run build。
浏览器半必须输出 client module system 的 lazy-CJS factory 格式(脚本本身只注册工厂,所有副作用都在工厂闭包里执行),scripts/build.mjs 负责这件事,格式与 DSH 自带插件一致。
发布
npm run verify # 先确认全绿
npm version patch # 0.2.0 -> 0.2.1
npm publish
git push --follow-tags
Ähnliche Plugins
dsh-our-free-model
ebony-vinyl/dsh-our-free-model
dsh-routing-suite
yjh051108/dsh-routing-suite
dsh-workbuddy-connect
corrinehu/dsh-workbuddy-connect
dsh-connect-workbuddy
dingminhua/dsh-connect-workbuddy