funny0A0/dsh-egress-router

dsh-egress-router

为单个目标开本机端口,经本机代理转发;含配置自愈与声明式兼容补丁

Uncategorizeddshdsh-plugin-marketdsh-plugins

dsh-egress-router

只让需要代理的端点走代理,其余全部直连。
一个 DSH 插件:给单个外连目标开一个本机端口,请求经它转发;没开端口的目标照旧直连。

为什么需要它

DSH 的传输策略是「每个进程只有一个答案」:@deepseek-ai/dsh-http-proxy 在进程启动时读一次 HTTPS_PROXY,之后所有出站 HTTPS 都走同一个代理,没有 per-provider 开关。

于是只要设了全局代理,而代理客户端没在跑(或节点被墙、被限速),就会一起坏:

走代理的请求 · 后果 platform.deepseek.com(账号资料、余额) · 设置页显示「账号资料暂不可用」 插件市场目录(awesome-dsh-plugin.com、npm registry) · 「插件目录加载失败」 pnpm add 安装/升级插件 · 装不上,插件版本停在旧的 会话本身 · 正常——因为 api.deepseek.com 在 NO_PROXY 里直连

三种做法对比:

做法 · 故障半径 · 配置粒度 全局 HTTPS_PROXY · 代理一挂,账号 / 市场 / 安装 全挂 · 进程级,不可分 全部直连 · 需要代理的端点(Google 系等)不可用 · 无 本插件 · 只有一个中转挂了才影响那一个目标 · 每个目标

它做什么

能力 · 说明 逐目标路由 · 每个目标一个 127.0.0.1 端口,可选「直连」或某个上游代理;端口可自定义 发现 · 从 cordis.patch.yml、.env、已装插件的默认配置里找工作区外的 HTTPS 目标,标出「已路由 / 待确认 / 受保护」 探活 · 同一目标分别试直连与经代理,返回能否连通、耗时、是否只到 TLS 改写 + 回滚 · 只改 provider 的 baseURL 一行(保留注释与锚点),改前备份、改后校验,失败自动还原 应用后自检 · 起监听 → 真实请求一次目标 → 不通就回滚,不会「看起来成功、用起来失败」 指标 · 每个端口的请求数、上/下行字节、失败数、状态码分布、首字节延迟 p50/p95 机器人代理 · Telegram 这类把地址写死在代码里的插件:补丁 + DSH_TELEGRAM_PROXY,只注入本进程(写进 $DSH_HOME/.env 会让 DSH 拒绝启动) 兼容补丁 · 声明式补丁(包名 / 版本 / 文件 / 唯一锚点),重装后被冲掉会在下次启动自动修复 漂移自愈 · 配置里的 baseURL 与运行监听不一致时自动校正;被外部反复改写则暂停并显示原因 宿主契约探测 · 不靠版本号猜接口形状,依次尝试三种注册方式,结果记入状态 中英双语 · 跟随浏览器语言

工作原理

┌──────────────────────── DSH 宿主进程 ────────────────────────┐
 provider ───▶│ baseURL = http://127.0.0.1:8792/v1beta/openai                │
 (cordis      │            │                                                  │
  patch)      │            ▼                                                  │
              │   RelayManager(每个路由一个 http.Server,只绑 127.0.0.1)    │
              │            │  keep-alive,TLS 复用                            │
              └────────────┼──────────────────────────────────────────────────┘
                           ▼
                 CONNECT 隧道 ──▶ 上游代理(可选,如 clash 127.0.0.1:7897) ──▶ 目标

三个要点:

  1. 路由存在 `state.json`,不写进配置。 cordis.patch.yml 里只留一条 id 覆盖(proxies 等)。
  2. 重启自动重建。 监听由持久化的 routes 恢复;配置漂移会被校正。
  3. 写配置是外科手术。 只替换 provider 的 baseURL 值,其余字节不动;同时留备份与账本。

安装

前置:Node ≥ 20、DSH 桌面版 ≥ 0.2.1-rc.2、pnpm。

Windows

cd "$env:USERPROFILE\.dsh\profiles\desktop"
pnpm add file:F:\projects_all\AI_projects\dsh-egress-router
# 或在发布后: pnpm add dsh-egress-router

Linux / macOS

cd ~/.dsh/profiles/desktop
pnpm add file:/path/to/dsh-egress-router
# 或在发布后: pnpm add dsh-egress-router

然后在 profiles/desktop/package.json 的 dsh.profile.bundles 里加入 "dsh-egress-router", 重启 DSH。页面:设置 → 插件 → 出口路由。

安装与升级的完整说明见 docs/install.md。

安装后自检

# 1) 版本
Get-Content "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-egress-router\package.json" | Select-String version
# 2) 接口(端口随实例变化,从设置页地址栏取)
Invoke-RestMethod "http://127.0.0.1:<GUI端口>/plugins/egress-router?op=state"
# 3) 某个中转是否活着
Invoke-RestMethod "http://127.0.0.1:8792/__health"

快速上手

  1. 扫描 —— 列出模型 provider、Telegram、以及配置里的其它 HTTPS 目标。
  2. 检查 —— 看「直连」和「经代理」哪个通;不确定的点「找端口」拿一个空闲端口。
  3. 选代理 —— 每行一个下拉:直连 或 config.proxies 里的名字(如 clash)。
  4. 应用 —— 写入 baseURL、起监听、真实请求一次自检;成功后再点一次刷新即可看到「运行中」。
  5. 重启 DSH(首次或改端口后)。之后监听随宿主重建,不需要再动配置。

设置页说明

顶部:扫描(重新发现)、端口检查(批量看占用)、上游(逐路由探一次代理链路)、诊断(下载脱敏现场 JSON)、回滚上次改动(最近一次应用的逆操作)。

每一行

元素 · 含义 模型 / 消息 / 外连 / 自定义 · 目标类型;外连 表示从配置里扫到但无法判定用途(标「待确认」);自定义 来自 config.targets,只可检查、不可改写 已路由 · 这个目标当前指向本机中转 运行中 :8792 / 未运行 · 监听是否真的起来了 42 次 · ↑14.9KB ↓86.1KB · p50 231.4ms · 5xx 1 · 累计 1.2k 次 · 本次监听的指标 + 跨重启累计(metricsTotals);暂无流量 表示还没请求过 经 clash / 直连 · 该行选中的上游 新建中转 复制地址 删除 · 建一条不改目标配置的本机中转、复制回环地址、删除监听 检查 预检 找端口 应用 测试 撤销 · 探活、看 diff、取空闲端口、落盘生效、经中转发一次真实请求、按项回滚

兼容补丁:列出声明式补丁的状态(已生效 / 待应用 / 锚点缺失 / 包未安装)。 已生效 且无备份时说明补丁来自 pnpm patch,只读。

最近操作:最近 10 条审计(应用、回滚、补丁、启动、配置校正、暂停)。

配置

cordis.patch.yml(只留代理表)

- id: egress-router
  config:
    proxies:
      clash: http://127.0.0.1:7897

config.targets:声明要观察的端点

有些域名不在任何 provider 的 baseURL 里(自建服务、上游 API、暂时未接入的模型供应商),但你仍想一键检查。把它们写进 targets:

- id: egress-router
  config:
    proxies:
      clash: http://127.0.0.1:7897
    targets:
      - { id: github, name: GitHub API, host: api.github.com, port: 443 }
      - { id: hf, target: huggingface.co:443 }
      - { id: ghcr, url: https://ghcr.io/v2/ }

三种写法等价(host + port / target: "host:port" / url)。它们会以「自定义」徽章出现在扫描结果里,可以 检查 与 测试,但 apply 会返回 not_patchable:没有可改写的 baseURL。

不要写 name: dsh-egress-router:bundles 已经加载了本插件,再写 name 会被当成第二个实例。

受保护域名与已知端点(0.11.0)

- id: egress-router
  config:
    guarded:
      - sso.mycorp.test                      # 追加到内置清单
    origins:
      acme: https://gateway.acme.test/v2     # provider → 真实公网端点
  • guarded:内置 platform.deepseek.com、api.deepseek.com、registry.npmjs.org、awesome-dsh-plugin.com、dsh-plugin-catalog。追加项与内置项一样,只在页面列出、不提供改写。写成 URL(https://sso.mycorp.test/login)也可以,会自动归一成 host;匹配包含子域名,但不匹配 evil-sso.mycorp.test 这类伪后缀。
  • origins:当账本里没有某个 provider 的原始地址时用它兜底(刚换机器、备份被清空、历史路由已被撤销都可能发生)。恢复回环路由时优先级是:路由记录 → 备份账本 → config.origins → 内置端点(openrouter / gemini / moonshot / deepseek)。

供应商没有写 baseURL 时(0.13.0)

从「第三方模型供应商」导入的 provider 往往只有 models 与 apiKeyEnv:

moonshotai-cn:
        apiKeyEnv: MOONSHOTAI_CN_API_KEY
        models: [kimi-k2.6]

宿主运行时会用内置 pi-ai 目录补出 baseURL/api,配置文件里却没有这两个值。0.13.0 起插件按同一顺序解析端点,页面因此仍能显示目标并「应用」:

  1. cordis.patch.yml 里字面写出的 baseURL;
  2. 本插件记录过的真实端点(路由 originBaseURL、备份账本);
  3. config.providerDefaults.(可只写 URL,或写 { baseURL, api });
  4. 宿主内置目录 pi-ai/dist/providers/data/.json(桌面版直接读 app.asar,dsh web/CI 用同一个解析器);
  5. 内置的公共端点表(deepseek、moonshot、google、openai、anthropic、xai、openrouter 等 17 项)。

命中 3–5 的行带「端点来自…」徽章;点「应用」时插件把解析出的 baseURL 写进该 provider 块,仍然只改这一处、仍然有备份与回滚。解析不到的行保持只读,不会猜地址。

0.13.1 起不再写 `api`:llm-pi-ai 的设置 schema 只接受 openai-completions、openai-responses、anthropic-messages 三个协议名,而宿主目录里的 google-generative-ai 一类名字只能在目录内部使用。0.13.0 把目录里的协议名写进了配置,整条 llm-pi-ai 因此校验失败、所有供应商卡片(连同「添加供应商」)一起消失。现在:

  • 宿主目录能实现的 provider 一律只改 baseURL,api 保持缺省(这才走宿主的 catalog 复用);
  • config.providerDefaults..api 只接受上面三个名字,其他值直接 400 unsupported_api;
  • 启动时自检:api 非法的 provider 若在宿主目录里能找到,就删掉这一行并备份(历史记 config-repair),找不到的保留原样只记日志——不会把 schema 错误换成运行时错误。
- id: egress-router
  config:
    providerDefaults:
      moonshotai-cn: https://api.moonshot.cn/v1
      acme: { baseURL: https://gateway.acme.test/v2, api: openai-completions }

本机中转(relay 路由,0.10.0)

没有 baseURL 的目标(CLI、别的插件、webhook、抓取脚本)改不了配置,但仍然可以走本机端口:

curl -s -X POST "http://127.0.0.1:<GUI 端口>/plugins/egress-router?op=routes" \
  -H 'content-type: application/json' -H 'sec-fetch-site: same-origin' \
  -d '{"action":"upsert","id":"relay:hf","target":"huggingface.co:443","listenPort":8793,"proxy":"clash"}'

之后把调用方地址换成 http://127.0.0.1:8793,请求经 Clash 转发到 huggingface.co:443;删除用 {"action":"delete","id":"relay:hf"}。这类路由不写 cordis.patch.yml,所以 restartRequired 恒为 false。

页面上的「本机中转(不改目标配置)」区块就是这两个调用的界面版:填目标与端口、选代理、点「新建中转」,之后可复制地址或删除。

config.discovery:声明式发现规则(0.11.0)

发现器默认用 baseURL、包依赖与已知供应商来推断用途。推断不准时不必等上游支持,写规则覆盖:

- id: egress-router
  config:
    discovery:
      ignore:
        - telemetry.example.com
      rules:
        - id: internal-api
          match: { hostSuffix: corp.test }
          kind: custom            # llm | im | other | custom
          name: 内部网关
          confidence: certain     # certain | probable | low
          note: 走网关才能出网
        - id: no-apply
          match: { id: "llm:gemini" }
          patchable: false
  • match 字段:id(尾部 * 作前缀匹配)、kind、host、hostSuffix(自动去掉前导 *.)、urlIncludes;同一行的多个条件是 AND。
  • 同一行命中第一条规则就停止,可覆盖 kind / name / patchable / guarded / confidence / note。
  • ignore 里的 host 直接不出现;想「看得见但不动」请用 guarded。
  • ?op=discover 的响应用 rulesApplied 报告命中条数,被规则改过的行带 rule: 。

$DSH_HOME/storages/egress-router/state.json

version: 10、schemaVersion: 1。主要字段:

字段 · 说明 routes[] · { id, target, listenPort, proxy, tls, tuning, originBaseURL } listeners[] · 最近一次同步结果(ok / error),重启后可见 metrics{} · 每个路由的计数与延迟分位(样本在内存,重启从零累计) metricsTotals{} · 跨重启累计的 requests / bytesIn / bytesOut / errors 与首次计数时间 since backups[] · 配置备份账本(最多 20 条),回滚依据 patches{} · 补丁账本(自修复只处理这里记录过的) history[] · 审计流水(最多 50 条,重复行 60s 内折叠) hostContract / hostEnv · 宿主接受的注册形状与当时的平台/Node 版本 schemaVersion · 状态文档的对外形状版本(当前 1),由 lib/schema.js 校验;?op=state 的 schemaOk 为 false 时页面会显示具体字段 reconcilePaused{} · 漂移自愈的暂停窗口(原因 + 到期时间)

bots{} 从 v10 起移到同目录的 bots.json({ version: 1, updatedAt, bots }),旧 state.bots 会在首次启动时自动迁移一次。机器人映射单独成文件,重装插件或重置状态时不会误丢。

机器人代理与环境变量

本插件不写 `$DSH_HOME/.env`。 该文件里的 DSH_TELEGRAM_PROXY 会让 DSH 启动器直接拒绝启动 (0.7.1 曾这样做,导致桌面版与 Web 版同时无法启动),所以代理只做两件事:

  1. 应用时注入 process.env,当前会话立即生效;
  2. 记录在 bots.json,下次启动由插件在本进程重新注入。

如果你希望变量在 DSH 启动之前就存在(例如别的进程也要用),请设置操作系统环境变量:

# Windows:用户级,重启 DSH 生效
[Environment]::SetEnvironmentVariable("DSH_TELEGRAM_PROXY", "http://127.0.0.1:7897", "User")
# Linux/macOS:写进 shell profile
export DSH_TELEGRAM_PROXY=http://127.0.0.1:7897

如果你从 0.7.0/0.7.1 升级上来,启动时会自动清掉旧 .env 中的受管块(history 里有一条 env-cleanup),只删我们写过的行,不动你自己的配置。

HTTP 接口

GET|POST http://127.0.0.1:/plugins/egress-router?op=

op · 方法 · 说明 state · GET · 页面所需的全部状态(26 个键,含 apiVersion: 1 与 schemaOk / schemaErrors;键集合锁定在 test/contract/api-surface.json) diagnose · GET · 脱敏现场包(代理 userinfo、机器人地址、备份路径都已处理) upstream · POST · 逐路由探一次上游链路(用该路由自己的代理,6s 超时) discover · GET · 重新扫描目标并缓存;响应含 rulesApplied 与 guardedHosts(config.discovery / config.guarded 生效时) probe · POST · 探活;接受已扫描到的目标与 config.targets,拒绝私有地址 plan · POST · 只算 diff / 端口 / 警告,不写文件 ports · GET · ?op=ports&port=8791&port=8792 批量查占用 suggest · POST · 取一个空闲端口(跳过本插件已占用的) apply · POST · 应用:起监听 → 写 baseURL → 自检;失败自动回滚 routes · GET / POST · 路由列表 / 新建或更新 / 删除;relay: 前缀是「仅中转」路由,不改目标配置 batch · POST · 批量应用 / 撤销(≤20 条,先全量预检,失败逆序补偿) rollback · POST · 按项或全局回滚最近一次改动 test · POST · 经已完成的中转发一次真实请求 patches / patch · GET / POST · 兼容补丁列表 / 应用 / 复原 / 复查 bot · POST · 给机器人设代理(注入本进程环境变量) reconcile · POST · 处理配置漂移(或解除暂停)

写操作要求 POST + content-type: application/json;来自明确外来 web Origin 的请求返回 403。

常见错误码:invalid_port、port_in_use、provider_not_found、not_patchable、 private_target、unknown_target、unknown_proxy、apply_unverified、listener_failed、 file_changed_since_backup(409)、no_backup、stale_plan(409)、invalid_tuning、 unauthorized(403)、reconcile_paused(409)。

故障排查

症状 · 原因 · 处理 设置页「账号资料暂不可用」「插件目录加载失败」 · 进程里有指向未运行代理的 HTTPS_PROXY · 删掉 $DSH_HOME/.env 里的全局代理,改用本插件按目标代理 「接口不可用:Unexpected end of JSON input」 · 页面请求拿到空响应(插件 API 未挂载/宿主重载中) · 完全退出 DSH 再启动;页面会显示契约降级提示 「应用失败:port … is already in use」 · 端口被别的进程占用 · 点「找端口」取一个空闲端口再应用 「目标连不上,已自动回滚」 · 上游代理没起、节点被墙、目标域名错 · 先点「检查」;/__health 看中转是否在跑 「宿主没有接受任何已知的接口注册形状」 · 宿主契约变了 · 路由与中转仍工作;把 state.hostContract 反馈到 issue 配置反复被外部改写 · 宿主与插件互相纠正 · 页面黄条点「恢复检查」;60s 内 3 次会自动暂停 10 分钟 Telegram 机器人连不上 · 其传输只读进程环境变量 · 该行选代理 →「用这个代理」(补丁 + 进程环境变量),重启 应用成功但没生效 · 改端口 / 改目标后需要重建监听 · 重启 DSH 市场 / npm 装不上 · 安装走 registry 直连 · 确认 registry.npmjs.org 可达;本插件不代理它