dsh-plan-bridge
DeepSeek Harness (dsh) plugin: one local bridge for ChatGPT Codex / Claude / ZCode (GLM) / Qwen subscription plans — settings status card, one-click provisioning, zero core changes. npm: @taylorswitiger/dsh-plan-bridge
- Stars
- 1
- Language
- JavaScript
- Created
- Sep 3, 2026
- Updated
- Sep 4, 2026
Introduction
dsh-plan-bridge
A DeepSeek Harness (dsh) plugin that bridges existing vendor subscription plans — ChatGPT Codex (Plus/Pro), Claude Code (Pro/Max), ZCode (BigModel GLM Coding Plan), Qwen (Aliyun Bailian Token Plan) — into dsh model groups through one local loopback process. Settings-page status card, one-click provisioning, no dsh core changes, zero runtime npm dependencies. 中文文档如下。
dsh 的多套餐桥接插件。一个本地进程监听 127.0.0.1:8417,按路径前缀分流,同时承载多家订阅套餐; 每家套餐在 dsh 里是独立的模型分组,是否接入由你在设置页决定。
dsh (llm-pi-ai 多 provider 路由)
├─ codex-plan → http://127.0.0.1:8417/codex/v1
├─ claude-plan → http://127.0.0.1:8417/claude
├─ zcode-plan → http://127.0.0.1:8417/zcode/v1
└─ qwen-plan → http://127.0.0.1:8417/qwen/v1
│
├─ codex OAuth:读 ~/.codex/auth.json,续期写回,转发 ChatGPT codex 后端
├─ claude OAuth:读 ~/.claude/.credentials.json,续期写回,转发 api.anthropic.com
├─ zcode API-key:转发 open.bigmodel.cn coding 端点
└─ qwen API-key:转发百炼 token-plan 端点
套餐分两类。OAuth 型(codex、claude)复用厂商 CLI 自己的凭据文件,token 快过期时自动续期并写回原文件,CLI 和桥接共享同一份登录态。API-key 型(zcode、qwen)的 key 存在 dsh 凭据库里,桥接只转发请求和 Authorization 头:key 不进入桥接进程,也不落在插件自己的任何文件里。
桥接不代做登录。OAuth 套餐要求先跑过厂商 CLI 的 login;API-key 套餐要求 dsh 凭据库里已有 key。检测不到凭据的套餐在卡片上显示「不可用」并附凭据指引。
安装
dsh plugin --profile <name> add -w @taylorswitiger/dsh-plan-bridge
# 或直接从 GitHub / 本地目录
dsh plugin --profile <name> add -w https://github.com/TaylorSwitiger/dsh-plan-bridge
dsh plugin --profile <name> add -w file:/path/to/dsh-plan-bridge
装完重启 host。桥接默认监听 127.0.0.1:8417,可用插件配置 host / port 覆盖。上游需要代理时,给 host 进程设 NODE_USE_ENV_PROXY=1 和 HTTPS_PROXY(桥接走 Node 自带的 env-proxy 出网)。
设置页卡片
设置 → 套餐接入,每套餐一行,接入与否由该行按钮决定,没有自动接入。
| 状态 | 含义 | 按钮 |
|---|---|---|
| 服务中 | 已接入且本机凭据可用 | 停用、重新检测 |
| 另一实例服务中 | 同上,但 8417 被另一个 host 实例持有 | 停用、重新检测 |
| 未接入 | 凭据在,provider 段还没写 | 一键接入、重新检测 |
| 需先登录厂商 CLI | OAuth 凭据文件缺失,附 login 命令指引 | 重新检测 |
| 不可用 | 本机没有该套餐的凭据,附添加指引 | 重新检测 |
| 错误 | 桥接监听失败 | 重新检测 |
OAuth 型行内显示账号掩码(如 use***@example.com)、subscriptionType、token 过期时间;API-key 型显示「API key 凭据已配置」。遇到 429 时该行显示额度重置时间,上游错误体里带重置时间戳的(codex、claude)会给出具体时刻,其余只透出错误文本。
停用会从 settings.yaml 移除该套餐的 provider 段,模型目录立即失去该分组,凭据保留不动;再点一键接入随时恢复。该套餐若正被 agent-default-model 指向,停用会被拒绝,先切默认。
一键接入做了什么
provision(卡片按钮或 RPC)幂等地做三件事:
- 校验本机凭据。缺失时返回结构化错误(
cli-login-required或credential-missing),卡片显示对应指引。 - 通过 dsh 的 settings 服务写
llm-pi-ai.providers.<key>段。已有且一致则跳过;不一致返回字段 diff,需要你在卡片上确认后才覆盖。baseURL 按适配器接受的路径拼写归一比较,等价的写法不算差异。 - OAuth 型经 credentials 服务补一个占位凭据(桥接不校验值);API-key 型不写任何凭据。
接入不改变 agent-default-model。
RPC 端点
渠道 /dsh-plan-bridge(loopback):
plans.list {} 卡片首屏
plan.status {planId} 单套餐详情
provision.begin {planId, overwrite?} 接入
provision.disable {planId} 停用
HTTP 直测(method 须与 URL 末段一致):
curl -s http://127.0.0.1:3080/dsh-plan-bridge/plans.list \
-H 'content-type: application/json' \
-d '{"type":"client-request","rpcId":"1","method":"plans.list","payload":{}}'
桥接健康:GET http://127.0.0.1:8417/health,单套餐 GET /<planId>/health。
内置套餐
| planId | providerKey | 类型 | 凭据 | 模型 |
|---|---|---|---|---|
| codex | codex-plan | OAuth | ~/.codex/auth.json | gpt-5.6-sol |
| claude | claude-plan | OAuth | ~/.claude/.credentials.json | claude-opus-5 / sonnet-5 / haiku-4-5 |
| zcode | zcode-plan | API-key | ZAI_CODING_PLAN_API_KEY | glm-5.3 / 5.3-flash / 5.2 / 5-turbo / 5.1 / 4.7 |
| qwen | qwen-plan | API-key | QWEN_TOKEN_PLAN_CN_API_KEY | qwen3.7 / 3.6 系及 deepseek、kimi、glm、minimax 共 15 个 |
已知问题:
- claude 标 beta:转发链路完整可用,但 Apple / App Store 渠道订阅的账号会被 Anthropic 组织策略拒绝(403
oauth_not_allowed_for_organization),网页直付账号不受影响。卡片对该错误有专门的解释文案。 - qwen 标 beta:端点与模型目录取自 pi-ai 的
qwen-token-plan-cncatalog,上游转发未经真实 key 验证;添加 key 后即可使用。 - zcode、qwen 走的是套餐专用网关(
open.bigmodel.cn/api/coding、token-plan.*.maas.aliyuncs.com),用量计入套餐额度,不扣按量余额。
新增一个适配器
框架只管端口、路径分流、生命周期和状态聚合,厂商细节全在适配器里。加一家套餐 = 一个适配器文件 + 注册表一行,路由、卡片行、provision、状态端点自动生效。
OAuth 型适配器导出如下结构的对象(参考实现见 codex.js):
export const fooAdapter = {
id: 'foo', // 路径前缀 /foo/v1 的来源;planId
providerKey: 'foo-plan', // provision 写入 settings.yaml 的 providers 键名
displayName: 'Foo 套餐',
protocol: 'openai-responses', // llm-pi-ai 的 api 名
apiKeyEnv: 'FOO_PLAN_BRIDGE_KEY',
authKind: 'oauth', // 'oauth' | 'api-key'
basePath: '/foo/v1',
settingsPath: '/foo', // 写进 baseURL 的路径,取决于 SDK 是否自己追加 /v1
acceptedBasePaths: ['/foo', '/foo/v1'],
cli: { name: 'Foo CLI', loginCommand: 'foo login', installHint: 'npm i -g foo',
credentialHint: 'dsh 设置 → 凭据 → 添加 FOO_KEY' },
models: [/* { id, name, contextWindow, maxTokens, reasoningEfforts?, input? } */],
profileExtras: {}, // 并入 provider 段的额外字段,如 compat
beta: false,
detectAuth() { // 探测本机可用性,可异步;api-key 型查 dsh 凭据服务
return { ok, accountMasked?, expiresAt?, reason? }
// reason 'cli-login-required' → 卡片「需先登录厂商 CLI」
// reason 'credential-missing' → 卡片「不可用」
},
async forward({ subpath, request, response, log }) {},
parseUpstreamError(status, text, headers) {}, // → { type, resetsAt?, detail? } | null
status() { return { accountMasked?, accessTokenExpiresAt?, lastUpstreamError, note? } },
}
API-key 型不必手写,lib/adapters/apikey-plan.js 的工厂一份 spec 换一个完整适配器(凭据探测、Authorization 透传、上游错误缓存都内置):
export const fooAdapter = apiKeyPlanAdapter({
id: 'foo', providerKey: 'foo-plan', displayName: 'Foo 套餐',
apiKeyEnv: 'FOO_KEY', upstreamBase: 'https://vendor.example/v4',
cli: { name: 'Foo', credentialHint: 'dsh 设置 → 凭据 → 添加 FOO_KEY' },
models: [/* … */],
})
注册(lib/adapters/index.js):
export const adapters = [codexAdapter, claudeAdapter, zcodeAdapter, qwenAdapter, fooAdapter]
然后 pnpm run check,重新装到 profile,重启 host。
上游端点和模型元数据优先从 pi-ai 的内置 catalog 抄(zai-coding-cn、qwen-token-plan-cn 等),与 dsh 原生路由同源,contextWindow、maxTokens、input 模态都有现成数据。
兼容性
- 不修改 dsh 本体的任何文件,只用公开插件 API:Connection RPC、settings / credentials 服务、settings 插槽。
- 凭据写入全部走 credentials 服务,保持
~/.dsh/.credentials.yaml的 version 1 + refs 格式。 agent-default-model只能由用户改动,插件的任何端点都不碰它。
设计说明
改代码前值得知道的几件事:
- 客户端一侧是手写的 module-table bundle(
window.__ModuleLoader__.load),react 通过工厂函数的require('react')拿宿主共享的实例。因此不需要打包器,pnpm run check只做解析和打包契约校验;代价是不能用 JSX 和打包期优化。 - 所有 RPC 端点统一返回
{ok, value}/{ok, error}结构。曾有端点漏包 value,客户端解包得到 undefined,渲染时导致整个设置分区渲染失败。加端点时保持返回结构一致,客户端的 unwrap 也要对「ok 但缺 value」抛出带错误码的异常。 - settings 写入再读回会补上空默认值(model 的
input: []、compat: {chatTemplateKwargs:{}}),幂等比较前必须先归一(util.js的normalizeForDiff),否则自己刚写的配置会被误报成冲突。 - 凭据事实与端口持有是两个维度。缺 key、缺登录是机器级事实,闲置实例(8417 被别的 host 持有)也要如实报「不可用」/「需登录」,状态计算先按凭据归类,再看端口。
- Claude 订阅调用所需请求头与普通 API key 调用不同:OAuth Bearer 之外还需
anthropic-beta: claude-code-20250219,oauth-2025-04-20(与调用方自带值合并)、Claude Code 身份的系统提示首块、anthropic-version: 2023-06-01。token 在platform.claude.com/v1/oauth2/token续期,refresh grant 会轮换 refresh_token,写回时保留响应省略的字段。oauth 拒绝码在error.details.error_code,不在 message 里。 - codex 的 token 过期时间单位是 epoch 秒,claude 的是毫秒,状态聚合层统一归一成秒。
- 调试:host 进程设
DSH_PLAN_BRIDGE_DEBUG=<文件路径>,适配器把每个请求和上游错误首段追加写入该文件。
License
MIT