dsh-providers-hub
本包原名
dsh-codearts-auth(0.2.0-rc.11起改名)。老用户的 DSH profile 里 还锁着旧名时,插件会加载失败并被自动禁用(宿主报N entry did not activate, 界面里也就看不到它)—— 升级前先看 ⚠️ 从旧包名dsh-codearts-auth升级。
使用声明:本项目仅供学习研究使用,请遵守各服务提供方的服务条款。
deepseek-harness 插件:统一管理多个 AI 编程服务的账号,并把它们的模型接进 DSH 的模型选择器。支持多账号池(同一服务登录多个账号,按可用性自动选号)与 凭据静默续期(到期前自动刷新,无需再次打开浏览器)。
- 16 个 provider 路由,外加 1 个聚合路由(见下)
- 登录、状态、续期、积分领取全部在 Jet Hub 设置页完成,不注册任何斜杠命令
- 附带一个本机 OpenAI 兼容网关,供其它客户端复用已登录账号
📌 本 README 只写「怎么用」。 各 provider 的协议、端点、登录流程、请求头 构造、实测数据与事故史等实现细节,统一记录在不入库的内部文档
docs/implementation-notes.md中。需要改动实现或排查协议问题时读它。⚠️ accio 的章节该文件里还没有(
implementation-notes.md目前零处 accio)—— 读它时不要以为 accio 没有实现笔记。accio 的实测结论与坑点当前登记在docs/agent-notes/providers-accio.md(含三条与参考实现相反的结论、两个真缺陷、 区域选择与接线陷阱),补写implementation-notes.md的 accio 章节是独立任务。
目录
安装
该包尚未发布到 npm registry。提供两种安装方式。
⚠️ 从旧包名 dsh-codearts-auth 升级
本包在 0.2.0-rc.11 把包名从 dsh-codearts-auth 改成了 dsh-providers-hub。
仓库里改完了不代表你的机器上改完了 —— DSH 是在 profile 里按包名字符串
寻址 bundle 的,而老 profile 两处都还写着旧名:
// ~/.dsh/profiles/<name>/package.json
{
"dependencies": { "dsh-codearts-auth": "link:…" }, // ← 旧键
"dsh": { "profile": { "bundles": [ …, "dsh-codearts-auth" ] } } // ← 旧项
}
两处必须一起改,缺一处就是静默失效 —— 而且**「两处都不改」同样会坏**, 只是坏的位置不同:
| profile 里的状态 | 后果 |
|---|---|
| 两处都是旧名(老 profile 的现状) | bundle 目录解析得到(node_modules/dsh-codearts-auth 这个旧名别名还在),但包里 patch 的加载行写的是新包名,而宿主把加载行的解析基准设成 profile 目录 ⇒ 新名在那里解析不到(本机实测 MODULE_NOT_FOUND)⇒ 该加载行 import 失败 ⇒ 宿主报 N entry did not activate,桌面端落进恢复模式并把插件自动禁用(Jet Hub 界面自然也不出现,即 issue IKLUME 的症状) |
只改 dependencies |
dsh.profile.bundles[] 里的旧名在 profile 里解析不到 ⇒ resolveBundleDir() 抛错 ⇒ 该 bundle 进 skippedBundles ⇒ 整个插件(连宿主侧)都不加载,只在 stderr 留一行:无异常、无 UI 提示 |
只改 dsh.profile.bundles |
与上一格同因:node_modules/ 里只有旧名,bundles[] 里的新名解析不到 ⇒ 同样进 skippedBundles |
实测(本机
~/.dsh/profiles/web):require.resolve('dsh-codearts-auth/package.json')成功解析到仓库根,而require.resolve('dsh-providers-hub/package.json')报MODULE_NOT_FOUND。宿主把加载行的解析基准设成 profile 目录 (dsh-app-boot对 patch 行调prepareProfilePatches(…, pathToFileURL(dirname(absoluteConfigPath))…), 同一个值也是boot()里ctx.baseUrl的来源),所以「旧名能解析」与「新名解析不到」 会同时成立 —— 这正是第一格的判断依据是按名字解析、而不是「目录在不在」的原因。⚠️ 第一格的落点不在客户端:宿主不会替用户把这种行摘掉 ——
preflight()只在「兼容性冲突」时 deny 一行,其余失败保留 cordis Loader 自己的诊断 (源码注释:every other failure keeps the Loader's own diagnosis),于是该行走进 Loader 并在宿主这一侧导入失败。所以排障的第一现场是宿主日志里的N entry did not activate与「插件已被禁用」,不是「宿主一切照常、只是客户端 entry 没建立」——后者会让排查方向整个偏到前端产物上去。
推荐做法是不要手改,直接用 dsh plugin --profile <name> 的 remove / add
各走一遍,让工具自己写这两处:
dsh plugin --profile <name> remove dsh-codearts-auth
dsh plugin --profile <name> add "https://gitee.com/iJetLi/deepseek-harness-codearts.git"
⚠️ <name> 要逐个 profile 走一遍:本机 ~/.dsh/profiles/ 下的 desktop /
headless / tui / web 四个 profile 的 package.json 都还锁着旧名,且
dsh.profile.bundles 里也都有它。只有既无依赖项也无 node_modules 的 profile
(如 default / rescue)可以跳过。
⚠️ 老版本可能残留 node_modules/dsh-codearts-auth 与 pnpm-lock.yaml 里的同名
依赖项;add 之后确认 profile 里不再有旧名的依赖键。
方式一:从 git 仓库安装(推荐)
add 以 git 仓库方式安装(pnpm 内部走 git+https: 协议),会运行本包的 prepare
脚本自动构建 lib/,无需手动 pnpm build。
⚠️ pnpm 10 起会拦截依赖的构建脚本,必须先在 profile 的 pnpm-workspace.yaml
(路径形如 ~/.dsh/profiles/<name>/pnpm-workspace.yaml)里放行;而放行键的写法
在 pnpm 10 与 pnpm 11 之间互不兼容,写错就装不上。
1. 先跑一次安装(这一次必然失败,为的是让 pnpm 打印它期望的键):
dsh plugin --profile <name> add "https://gitee.com/iJetLi/deepseek-harness-codearts.git"
2. 按第 1 步的报错选一种写法,写进 profile 的 pnpm-workspace.yaml。
报 ERR_PNPM_INVALID_VERSION_UNION 的(pnpm 10.x)—— 只认纯包名键:
allowBuilds:
dsh-providers-hub: true
报 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 的(pnpm 11.x)—— git 托管包
不认纯包名键,必须用第 1 步 pnpm 打印的完整键(含 #<commit>):
allowBuilds:
dsh-providers-hub@git+https://gitee.com/iJetLi/deepseek-harness-codearts.git#<第 1 步打印的 commit>: true
两代通用(省事,但放宽了权限):
dangerouslyAllowAllBuilds: true
它会放行该 profile 里所有依赖的构建脚本(不止本插件)。
3. 重跑第 1 步的命令:这次会拉取、构建并安装成功。之后每次升级重新 add 即可。
⚠️ 不要写 dsh-providers-hub@git+https://gitee.com/…(不带 #<commit>)——
这个"看起来最自然"的键在两代 pnpm 上都不工作。
⚠️ pnpm 11.x 的精确键里带 commit,因此升级(重新 add 拉到新 commit)后该键
失效,按新报错里的键替换即可;用 dangerouslyAllowAllBuilds 则不必改。
方式二:从源码目录安装(本地开发)
先在本仓库中构建 lib/,再安装为 pnpm link: 依赖:
pnpm build:all
dsh plugin --profile <name> install <path-to-this-repo>
dsh plugin install以link:方式安装,pnpm 不会为link:依赖运行prepare脚本,因此必须先手动执行pnpm build:all生成lib/。 注意必须用build:all而非build:后者只编译宿主侧,不产出lib/client/jet-hub.js。
每次修改 src/ 或 plugin-src/ 后都需要重新执行 pnpm build:all —— dsh 启动时
不会自动重建。
通用说明
该包声明了 dsh.bundle 补丁(cordis.patch.yml),因此 profile 的 layer 栈会
自动拾取 codearts-auth 行。插件注入由 dsh base 提供的 credentials、
commands 和 llm 服务。
用法
登录入口:Jet Hub 设置页的对应 provider 面板。
⚠️ 不注册任何斜杠命令 —— 登录、状态查看、续期与积分领取统一在 Jet Hub 完成。
基本流程:
- 打开 Jet Hub 设置页(侧边 rail 选择要用的渠道);
- 点「+ 新建账号」→ 浏览器打开授权页 → 完成登录;
- 需要多个账号时重复第 2 步(账号池会自动选号);
- 回到 dsh 的模型选择器,选中该渠道的模型即可开始对话。
「+ 新建账号」是两步式的:先返回 loginUrl 由前端弹窗打开,后台再轮询换取
令牌 —— 这样不会阻塞浏览器手势窗口,弹窗不会被拦截。
账号卡片支持:测试 / 重测 / 重置 / 停用 / 代理 / 指纹 / 删除,以及拖拽排序 (顺序即选号优先级)。停用只影响自动选号,不影响凭据续期与积分领取。
支持的渠道
16 个 provider 路由,互不覆盖,可同时使用。
| id | 渠道 | 登录方式 | 积分能力 |
|---|---|---|---|
codearts |
CodeArts(华为云) | 浏览器回调 | 余额 + 每日签到 |
buddy |
CodeBuddy(腾讯) | 浏览器授权 + 轮询 | 余额 + 每日签到 + 成长任务 + 锁定永久积分 |
workbuddy |
WorkBuddy(国际版) | 同上 | 余额 + 锁定永久积分(无签到) |
lobsterai |
LobsterAI(有道) | 浏览器回调 | 余额 + 每日签到 |
qoder |
Qoder(阿里系) | 设备码轮询 | 余额 + 每日领取 |
qodercn |
Qoder 中国版 | 同上(共用实现) | 余额 + 每日领取 |
trae |
TRAE(字节跳动) | 浏览器回调 | 余额 + 签到 |
cline |
Cline | WorkOS 设备码轮询 | 余额 |
loomy |
Loomy(讯飞) | 短信验证码 | 余额(两个池)+ 每日额度 + 新手任务 |
raccoon |
Raccoon Work(商汤) | 微信扫码 / 短信 | 余额 + 一次性登录奖励 |
minimax |
MiniMax Code(中国版) | OAuth 设备码 + PKCE | 余额 + 每日签到 |
zcode |
ZCode(智谱 z.ai) | OAuth 设备码 | 余额 + 每日领取 |
opencode |
OpenCode | — | 余额 |
gemini |
Gemini Code Assist | 浏览器回调 | 余额 |
officeace |
OfficeAce(华为 MaaS / 果办) | 华为云 OAuth PKCE | 余额 + 自动领取(无手动签到入口) |
accio |
Accio(阿里) | 浏览器回调 | 余额(三类桶;无签到) |
各渠道的能力与限制要点:
- 图片输入:
buddy/workbuddy/trae(逐模型)/minimax(部分模型)支持;lobsterai、zcode不支持;accio未实测(面板会明确报错而不是静默丢图); 其余以模型声明为准。 - 思考档位:多数渠道支持,档位名按各渠道自己的定义下发;未声明的渠道不发送该字段。
loomy是唯一用短信验证码登录、且不能自动续期的渠道。zcode的领取路径需要一次性验证参数,由本机浏览器环境产出(web 版需要 chromium,桌面版用自带内核)。 ⚠ 推理(对话)路径是否也要它,由服务端开关决定:官方 3.14.5 起GET /api/v1/client/configs会下发captcha.skip_model_request—— 当前实测是true(不校验,故按最省的方式「先探后取」: 不带验证头先发一发,被3007拒了才产);一旦服务端翻成false, 插件会自动改为首发直接带上,不再白吃那发注定失败的往返 (判据是三态:缺席 = 不知道 = 保持先探,见requiresCaptchaUpfront)。qoder/qodercn共用同一套实现,差异仅在产品配置。minimax是唯一的 Anthropic Messages 协议族渠道。accio登录走浏览器回调。⚠️ 新建账号时必须先选登录区域(中国版www.accio-ai.com/ 国际版zh.accio.com):两个区域的登录页域名不同, 而区域由服务端按账号下发、登录前无法自动判断 ⇒ 选错会打开打不开的登录页, 登录不会成功。区域会写进凭据,续期与推理按它选域名。 ⚠️ 它没有签到按钮 —— 每日积分由服务端在登录时发放(24 小时有效、 不累积,未登录的那天不补),没有可调用的领取接口。面板上的积分按服务端下发的 三类桶(每日 / 每月 / 推荐)分列显示,并给出每日额度的重置倒计时。 ⚠️ 「刷新每日额度」入口尚未实现:它本质是重新登录既有账号, 需要宿主侧新增一条 RPC(现有的「+ 新建账号」每次都会新造账号,拿它重登只会 多出一个空账号而原账号额度不变)。当前要刷新请删除该账号后重新登录一次。 ⚠️ 模型倍率跨度极大(0.013 ~ 2.8,215 倍),故倍率直接写进模型展示名 (模型名 · x倍率),选型时看名字即可。 ⚠️ 图片输入未实测,面板会明确报「暂不支持图片输入」而不是静默丢掉。 ⚠️ 该渠道不产生限流标记(也不会被限流探针测出东西),故「重测 / 重置」这组 按钮对它整组不渲染 —— 渲染出来只会是点不动的死控件。
一键领取积分
在对应面板标题栏点「一键领取积分」,插件会顺序处理该面板下的所有账号 (顺序执行以避免并发请求),并逐账号显示结果与原因。
⚠️ 汇总按单位分列,不跨量纲求和 —— ZCode 的额度单位是 token、其余渠道是
积分,两者不可折算,因此显示成 +100.00MToken, +100积分 而不是加成一个数。
模型列表开关
每个 provider 的面板都有「显示列表」按钮,可逐个开关模型,控制它是否出现在 对话框的模型选择器里(黑名单制,默认全部显示)。开关按 provider 隔离。
锁定永久积分
buddy / workbuddy / loomy 提供「锁定永久积分」按钮,用来保住不会马上
作废的积分。判定规则:距扣费截止不足 15 天的算临时(优先消耗),其余算永久。
锁定后只消耗临时积分,用完则明确报错而不是偷偷烧掉永久积分。
聚合路由(aggregate)
同一个真实模型往往在多个渠道都能用(例如 DeepSeek V4.1 Flash 在 buddy、 codearts、cline 等都有)。聚合路由把「同一个真实模型」在各渠道的条目归到一个 规范名下,请求时自动挑一个渠道发出去。
解决什么:各家渠道送的免费额度有到期时间,用不完就作废。聚合层按 「最快作废的额度优先用」自动排序,把临期积分先烧掉。
怎么用:
- 在模型选择器里选
aggregate分组; - 想不挑模型就选该分组下的
auto—— 它用全部渠道的全部模型当候选池; - 想指定模型就选该分组下的具体规范名(如
Deepseek-V4.1-Flash)。
选中后模型名旁会附一个 · <渠道> 后缀,那是首选渠道(临期最早的那个)。
⚠️ 它不是「本次实际发往的渠道」 —— 真实历史由用量徽标显示。
自动切换:某个渠道失败时会换下一个候选重试,但只在还没吐出任何可见内容 时才切换(已输出的内容收不回来,拼接会产生畸形回答)。
参与轮换的渠道:buddy / workbuddy / loomy / codearts / zcode /
lobsterai / trae 七家。其余渠道暂不参与(插件对它们的额度到期时刻没有
可靠信息,宁可少一个候选,不可把未知当可用)—— 它们仍可直连正常使用。
⚠️ zcode 的额度是「按模型」分桶的,故它的可用性是逐模型判定的:
赠送的 8M 实测分成 GLM-5.3 3M 与 GLM-5.3-Flash 5M 两个专属桶
(上游在余额接口里下发 capabilities: ["model:<模型>"])。某个模型烧光而另一个
还有额度时,只会把烧光的那个模型在该账号上标记为当日用尽 ——
另一个模型照常可用,不会因为「账号整体还有额度」而误派发到已尽的模型上。
若账号另有不限定模型的通用赠送额度(如 1 亿 token),且它还有余额,
则两个模型都仍可用(通用额度能承接它们)。
上下文窗口:聚合模型声明的是候选渠道里的最大值。同一个模型在 buddy /
codearts 上都是 1M,那么 aggregate 下选它也声明 1M(与直连一致)。⚠️ 若某次
请求恰好被路由到窗口更小的候选,由失败切换兜住(上游报超长 ⇒ 换下一个候选)。
思考档位:声明各渠道档位的并集(官方中文名),默认 high —— 与你选的
档位会逐候选翻译到目标渠道(精确命中就原样下发,写法不同就就近落到同族,
目标没有这一族就不下发该字段让它走自己的默认)。
聚合有自己的设置面板(Jet Hub 左侧 rail 的「聚合」项),可逐条开关 「参与轮换」。它没有账号与凭据(复用各渠道的账号)。
本机 OpenAI 网关
插件启动后会在 127.0.0.1:8326 提供两套标准 OpenAI 接口,供 Pi、Continue、
Cline、OpenCode、Codex 或其他兼容客户端使用。网关复用 Jet Hub 已登录账号和现有
provider 适配器,不把上游凭据复制到客户端。
GET http://127.0.0.1:8326/v1/models
GET http://127.0.0.1:8326/v1/reasoning-efforts ← 思考档位对照表
POST http://127.0.0.1:8326/v1/chat/completions ← OpenAI Chat Completions
POST http://127.0.0.1:8326/v1/responses ← OpenAI Responses API
⚠️ 两个端点同时可用,没有「格式开关」:客户端用哪一套协议,由它自己请求的 路径决定。
⚠️ 上面那两个地址里的 host 与端口都可被环境变量改掉
(DSH_OPENAI_GATEWAY_HOST / DSH_OPENAI_GATEWAY_PORT);设置页里的
「API 请求地址」显示的是实际监听地址,照它抄即可。
容器化部署(Docker / Compose)
默认只绑 127.0.0.1,因此端口映射对网关无效:容器里 curl 127.0.0.1:8326
能通只是假象,docker-proxy 与同网络的其它容器走的是容器桥接 IP(eth0),
那个地址上没有监听,于是宿主侧看到 Connection reset by peer
(对照:Web 端口绑的是容器 IP,所以映射可用)。
修法是让网关绑容器自身的接口地址:
environment:
- DSH_OPENAI_GATEWAY_ENABLED=1
- DSH_OPENAI_GATEWAY_HOST=172.21.0.8 # 容器自身 IP:hostname -i 或 ip -4 addr show eth0
- DSH_OPENAI_GATEWAY_PORT=8326
$ docker exec dsh ss -tlnp | grep 8326
LISTEN 172.21.0.8:8326 # 绑上桥接 IP 后,端口映射才可达
⚠️ 不许填 0.0.0.0 / :: 这类通配地址:会被显式拒绝并在启动前报错
(dsh 本体的 --host 也是同一姿态)。绑通配等于把网关暴露给整个网络,
真正的隔离只剩 API Key —— 而本网关承担的是「用你已登录的账号发请求」。
容器场景填容器自身 IP 即可。
⚠️ 暴露面不止「同一容器网络内的其它容器」,还包括宿主:经端口映射(-p)
或 bridge 网关,宿主本身就能连到该端口。跨 bridge 网络的容器才被隔离。
设置页里绑了非回环地址时会显示这条提示;不打算对外提供时请保持默认
127.0.0.1(或删除该环境变量)。
拿到地址与密钥
鉴权使用 Authorization: Bearer <网关 API Key>。
⚠️ 因此在浏览器地址栏直接打开这些地址会返回 401(没有带 Bearer 头)——
这是预期行为,不是网关没启动。请用客户端或 curl 带上传头访问。
优先从 DSH_OPENAI_GATEWAY_API_KEY 读取;未设置时,插件首次启动会在 DSH home 的
openai-gateway/api-key 生成并持久化随机密钥,重启后保持不变。
设置页里的开关状态存在 $DSH_HOME/jet-hub/state.json 的 gatewayEnabled 字段
(缺省为启用)。它不进账号备份/恢复。
关闭网关
网关是旁路功能:它启动失败或被关闭,都不影响插件其余功能(登录、积分、模型 目录照常)。三种关闭方式:
| 方式 | 做法 |
|---|---|
| 环境变量 | DSH_OPENAI_GATEWAY_ENABLED=0(完全不启动,连 API Key 文件都不生成) |
| 设置页开关 | 关掉后不再监听 |
| 改端口 | DSH_OPENAI_GATEWAY_PORT 填非法值会记录错误并跳过启动,不会静默改用其它端口 |
| 改地址 | DSH_OPENAI_GATEWAY_HOST 填通配地址(0.0.0.0 / ::)同样记录错误并跳过启动 —— 不会静默绑到全部网卡 |
请求体上限与 keep-alive
| 项 | 值 | 超限时的行为 |
|---|---|---|
| 请求体 | 300 MiB | HTTP 413 + invalid_request_error / request_too_large,响应体里带上限值 |
| 空闲连接(keep-alive) | 120 秒 | 服务端发 FIN;此后复用该连接会得到 Connection reset |
⚠️ 每轮把全量历史重新回传的客户端(Codex 等),历史里并入大截图后可能撞上 413: 新建会话、或删掉那条带大图的回合即可恢复。
安全边界
- 默认只绑定
127.0.0.1,且拒绝通配地址(0.0.0.0/::)。 只有容器/端口映射这类「客户端不在同一网络命名空间」的场景才需要改DSH_OPENAI_GATEWAY_HOST,且应填具体接口地址(如容器自身 IP)而不是通配。 ⚠️ 改了之后暴露面包含宿主(端口映射/bridge 网关都能连到), 同机与同容器网络的其它用户或进程也能连到该端口 —— 真正的隔离靠 API Key, 不要把它配置进任何浏览器端工具或扩展。 - 密钥以明文写在
$DSH_HOME/openai-gateway/api-key。POSIX 下的0600权限在 Windows 上不生效,多用户机器请改用DSH_OPENAI_GATEWAY_API_KEY环境变量。
用量徽标与 Token 用量
用量徽标
会话输入区右侧、模型选择器左边有一枚薄胶囊,显示当前渠道一共有多少可用额度; 点开是完整浮层(逐账号明细 / 订阅窗口 / 刷新 / 签到 / 显示偏好)。 只在选中本插件渠道的模型时渲染,非本插件的模型不渲染、不发请求。
折叠态按 渠道 • 读数 两段显示,例如:
CodeBuddy (腾讯) • 2434.96积分
CodeArts (华为云) • 9499.84积分
ZCode (智谱) • 94.54MToken
- 默认显示余额(一共能用的额度);可在浮层里切换成「优先订阅」或「只看积分」。
- 多账号只显示一个数字(所有启用账号的余额合计),不会变成一长串。
- 账号读不到时合计会静默少报,故胶囊会挂一个警示角标(
441.78积分 ⚠)。 - 浮层里逐账号列表按余额降序,超过 5 个折叠成「展开其余 N 个」。
Token 用量
点 Jet Hub 页头的「Token 用量」按钮查看。数据分两份:
| 数据 | 位置 | 回答的问题 | 重启后 |
|---|---|---|---|
| 实时明细 + 汇总树 | 进程内存(上限 500 条) | 每一笔长什么样、这次会话花了多少 | 清空 |
| 历史日聚合 | ~/.dsh/jet-hub/token-ledger.json |
本周 / 本月用了多少 | 保留(按 UTC+8 日界,90 天) |
在历史图表中点选某一天并展开,可删除该日的历史累计。删除前会二次确认,只移除该日聚合,不影响实时明细、其他日期或账号设置;之后该日的新请求会重新累计。
- 计数是 wire 口径:上游 usage 报什么记什么,不做本地估算。
- 总处理量 = 未命中缓存输入 + 缓存命中输入 + 输出;输出已含思考 token,缓存写入另列且不重复计入。不同渠道价格不同,因此这里不把 token 数冒充账单金额。
- 没收到 usage(失败 / 中断)显示
—,与真实的0严格区分。 - 输出速率(tok/s)的分子含思考 token;汇总显示算术均值及完整样本的 P50/P90 估值。
- 日聚合文件升级到 schema v2 增加 TPS 对数直方图;旧 v1 数据照常读取(计数全保留)。⚠️ 升级前已累计过「输出速率样本」的日期不会显示 P50/P90 —— 判据要求「直方图样本数 = 累计样本数」,而旧桶的
tpsCount在升级前就已累加、直方图为空,两者之差恒定 ⇒ 该日期在 90 天保留期内一直无分位数(avgTps均值仍照常显示)。完全在 v2 之后累计满样本的日期正常显示;升级前存在但从未有过速率样本的日期(如当天全是失败请求)也会正常显示。
配置项
| 项 | 位置 | 说明 |
|---|---|---|
| 账号池 | $DSH_HOME/jet-hub/state.json |
账号、有效期、限流标记、模型黑名单、provider 顺序 |
| 网关开关 | 同上,gatewayEnabled |
缺省启用 |
| 供应商显示顺序 | 同上,providerOrder |
纯展示偏好,拖动行调整 |
| 永久积分锁定 | $DSH_HOME/jet-hub/*.json(独立文档) |
不与账号池同放 —— 避免多 profile 互相覆盖 |
| Token 账本 | ~/.dsh/jet-hub/token-ledger.json |
历史日聚合 |
常见环境变量
| 变量 | 默认 | 说明 |
|---|---|---|
DSH_HIDE_MODELS_WITHOUT_ACCOUNT |
关闭 | 设为真值时,没有已登录账号的渠道在模型选择器里整体隐藏 |
DSH_OPENAI_GATEWAY_ENABLED |
1 |
设为 0 / false / off / no 时完全不启动网关 |
DSH_OPENAI_GATEWAY_HOST |
127.0.0.1 |
网关监听地址。容器/端口映射场景填容器自身 IP(hostname -i);填 0.0.0.0 / :: 等通配地址会被拒绝并跳过启动 |
DSH_OPENAI_GATEWAY_PORT |
8326 |
网关监听端口 |
DSH_OPENAI_GATEWAY_API_KEY |
自动生成 | 网关 Bearer 密钥 |
DSH_REASONING_LOOP_GUARD |
开启 | 思考死循环守卫总开关 |
DSH_REASONING_LOOP_AUTO_RESUME |
开启 | 中止后是否自动补一句「继续」并重跑 |
DSH_REASONING_LOOP_AUTO_RESUME_MAX |
2 |
连续自动续跑上限(最大 10) |
DSH_TRAE_MAX_COMPLETION_TOKENS |
64000 |
TRAE 单次输出上限(设 0 关闭收敛) |
更多 provider 专属变量(如 DSH_ZCODE_INTERNAL_CARRIER、
DSH_TRAE_ROTATE_MACHINE_ID 等)见内部文档。
账号备份与迁移
Jet Hub 设置页底部的备份区提供导出 / 导入,用于两种情况:换 DSH 版本时整体迁移, 以及从别处单独搬一个账号进来。
备份文件是自包含的 JSON(账号索引 + 凭据原文 + 模型黑名单 + 永久积分锁定表), 与 DSH 版本无关 —— 导入时按当前版本的存储契约重建,因此天然跨版本。
- 加密在浏览器侧完成(PBKDF2 + AES-GCM),RPC 只收发明文载荷,明文与口令 都不经过宿主持久化与日志。可选加密,不勾选即导出明文。
- 账号凭据、
refresh_token都在文件里,等同于明文密钥,请自行妥善保管。 - ⚠️ 不含的就是本机偏好:备份只搬账号与凭据,不搬本机 OpenAI 网关开关、
用量徽标偏好、自动签到记账等本机状态(导入时它们保持当前值,不会被备份覆盖)。
账号条目上的字段(含 OpenCode 的
opencodeProxy/ 指纹代次)则原样随快照往返。
导出:可以只导出选中的账号
导出弹窗列出当前账号池并可逐个勾选(backup.export 的 accountIds),
默认「全部账号」。只导一个账号时文件名会带上该账号 id(dsh-codearts-backup-buddy-xxxx-日期.json),
避免被误当成整份备份收着。
⚠️ 过滤只作用于 accounts 与 credentials 两个字段:模型黑名单与永久积分锁定表
仍按全量写出。这样「只导一个账号」的产物依旧是一份可整体还原的自包含快照 ——
让过滤只承担一种语义,两条路径就不会互相污染。
导入:两种模式(replace / merge)
| 模式 | 语义 | 用于 |
|---|---|---|
replace(默认) |
整体替换账号池与模型黑名单,还原快照 | 「恢复」按钮、换 DSH 版本后整体迁移 |
merge |
只追加:已存在的账号一律跳过,不碰模型黑名单与锁定表 | 「单独导入一个账号」、从别人的备份里挑账号 |
导入弹窗可勾选要落地哪些账号,并选择模式。
⚠️ merge 下裁剪发生在发请求之前(plugin-src/client/backup-payload.js)。
判重虽然在后端,但后端只知道「这份载荷里有哪些账号」,不知道用户勾了哪几个;
不先裁剪的话,从整份备份里只勾一个账号,其余账号仍会被追加,更糟的是备份里那些
不在勾选范围内的 credentialRef 可能已被本机现有账号占用,写进去等于
覆盖现有账号的凭据,与「只增加不覆盖」的承诺相反。故裁剪必须同时裁掉
accounts、credentials 与 disabledModels 三处。
⚠️ 合并导入判重的三层口径(缺一层就会堆出重复账号)
src/backup.ts 的 importBackup({ mode: 'merge' }) 依次判:
- 池里已有的
id—— 账号的唯一键; - 池里已有的身份(
provider:+ 凭据里的身份字段,见下表); - 备份文件内部重复的
id—— 同一份文件手工编辑后可能出现两条同 id。
⚠️ 只按 id 判重是不够的,第 2 层才是关键:插件自己登录时 id 由 shortId()
随机生成(buddy-a1b2c3d4),而从第三方文件转换来的 id 是确定性哈希
(见下节)。同一个人因此有两套 id,只比 id 会把他重复加进池里 —— 而用户在
「从别人的备份里挑账号」时,最常见的情形恰恰是「这个号我已经手登过了」。
⚠️ 身份字段逐 provider 对齐(漏一个字段,那个 provider 的「同一个号我已手登过」 场景就必然多一条重复账号):
| 字段 | 哪些 provider 的凭据里有 |
|---|---|
user_id |
CodeBuddy / WorkBuddy / LobsterAI / Raccoon / ZCode |
uid |
Qoder(含中国版)/ TRAE |
userid |
Loomy |
account_id |
Cline / MiniMax |
sub |
Gemini(OIDC sub;不取 email —— 它会随改邮箱而变) |
api_key |
OpenCode(仅账号槽) |
⚠️ Gemini 只认 sub,不认 email:两者都在它的凭据里,但 sub 是 OIDC 的
稳定主体标识(换邮箱、改昵称都不变,上游就拿它当账号 id),而 email 会随用户
改邮箱而变 —— 拿它判重会让同一个账号在改邮箱后多出一条重复账号。
⚠️ OpenCode 的匿名通道要排除:它的凭据只有 api_key,而匿名通道的 api_key
恒为字面量 public —— 全部匿名槽共用一个值。若拿它当身份,用户刻意添加的
第二条匿名通道会被判成「已存在」而拒绝导入(「多条匿名通道各走各的出口」
是上游明确提供的能力)。故匿名槽不产生身份,退回按 id 判重。
⚠️ 轮换令牌(access_token / refresh_token)不能当身份:它们每次续期都会变,
拿来判重会让续期后的同一账号被判成新账号。
⚠️ id 与身份都可能判不出来:凭据缺失/损坏时按「无法判断」跳过该条,
不让一次判重失败阻断整体导入。
⚠️ 第三方账号列表可以直接导入(自动转换)
很多用户手上的是别的工具(账号池 / 卡号池脚本、IDE 本地缓存导出)产生的 CodeBuddy 账号列表,而不是本插件备份。两者结构完全不同:
| 第三方账号列表 | 本插件备份 | |
|---|---|---|
| 顶层 | 裸数组 [ {...} ](或 { accounts / data / items / list: [...] }) |
对象 { format, version, ... } |
| 格式标记 | 无 | format: 'dsh-codearts-auth/backup' |
| 凭据 | 账号对象里直接带 access_token / refresh_token |
独立 credentials 字典(ref → 凭据 JSON 原文字符串) |
| 账号索引 | id 是 uid 或工具自己的编号 |
accounts[],id 形如 buddy-a1b2c3d4 |
直接把它丢给「恢复」只会得到「该备份文件里没有账号」—— 因为取不到 accounts。
故选中这类文件时会自动识别并转换(plugin-src/client/backup-convert.js),
转换结果显示在弹窗里供核对,并默认落到 merge 模式(别人的新号,避免误覆盖本机账号)。
⚠️ 转换只改结构,不改写令牌:access_token / refresh_token 原样搬运。
其余字段以「本机一个真实可用的 CodeBuddy 凭据」为基准对齐:
| 字段 | 来源 |
|---|---|
expires_at |
原值(毫秒时间戳),统一成字符串毫秒 |
refresh_expires_at |
优先 auth_raw 的 refreshExpiresIn - expiresIn 差值补算,退回 refresh_token 的 JWT exp |
domain |
access_token 的 JWT iss 主机名(它会被当作 X-Domain 头发出去) |
user_id |
access_token 的 JWT sub |
token_type / account_type |
原值,缺省 Bearer / personal |
enterprise_id |
缺省 ''(个人版为空串,与真实凭据一致) |
⚠️ provider 按 JWT iss 的主机名分派:含 workbuddy → 国际版,否则 → buddy。
两套 provider 是不同路由,认错会让凭据发到错误的端点。
⚠️ 账号 id 由 uid 经 FNV-1a 哈希确定性生成(stableShortId),与插件自己登录
生成的形式一致。这样同一份文件导入两次,第二次会全部被判为「已存在」而跳过,
而不是堆出两份账号。但 id 判重挡不住「同一个人的账号已用随机 id 登录在本机」——
那正是靠上文第 2 层的身份字段判重。
⚠️ 不写 scope:真实凭据里有它,但插件从不读取 scope 发请求
(只在续期时原样带回),而第三方文件里也没有 —— 与其猜一个可能不准的值,不如省略。
⚠️ 合并导入只写「真正要新增的账号」的凭据
merge 模式下绝不回写备份里其余凭据(含各 provider 的默认单凭据 ref)。
replace 模式才按字典逐条写入 —— 备份文件里可能含账号池之外的独立凭据 ref,
导出时一并纳入,导入时同样还原。
⚠️ 写入顺序是先凭据、后账号池:账号池写入成功后门控(hasLoggedInAccount)
与黑名单立即反映新状态;凭据写入中途失败(非法 ref 等)只跳过该条、不中断整体。
⚠️ 模式必须取白名单(req?.mode === 'merge' ? 'merge' : 'replace'),
不能把请求里的任意字符串透传下去:未知值会被 ?? 'replace' 静默当成整体替换 ——
那时用户以为只是「加一个账号」,实际账号池被清空,属于不可逆的数据损失。
测试
tests/unit/backup.spec.ts(44 条)—— 选择性导出、合并导入三层判重、merge不回写其余凭据、replace兼容旧行为、各 provider 的身份字段覆盖度;tests/unit/backup-payload.spec.ts(13 条)—— 载荷裁剪与账号摘要;tests/unit/backup-convert.spec.ts(28 条)—— 第三方列表识别与转换;tests/unit/account-pool.spec.ts的mergeAccounts段(6 条)—— 只追加、 不改黑名单与锁定、坏条目归一化、文件内重复 id。
⚠️ 与上游同步(2026-10-02)
本章功能已随上游 62 个提交(新增 OpenCode provider、本机 OpenAI 网关、用量徽标、
自动签到、供应商开关)重新落地:只有 plugin-src/client/jet-hub.js 的 import 块
需要手工合并,BackupPanel 与 src/backup.ts 上游未改动。唯一的适配点是
OpenCode 的身份字段(见上表与匿名槽说明)。细节与反向验证记录见 AGENTS.md
的「上游同步记录」一节。
⚠️ 已做反向验证(2026-09-30):禁用身份判重 → 1 条变红;让 merge 回写全部
凭据 → 3 条变红;mergeAccounts 去掉文件内去重 → 1 条变红。
开发
pnpm test # 单元测试(快速,无网络,全部 mock)
pnpm typecheck # 只做类型检查
pnpm build:all # 完整构建(宿主 tsc + 资源拷贝 + 客户端 esbuild)
构建
pnpm build— 用 tsc 将src/编译到lib/。插件宿主侧入口是lib/index.js。pnpm build:client— 用 esbuild 将plugin-src/client/打包为lib/client/jet-hub.js(Jet Hub 设置页的客户端 bundle)。pnpm build:all— 依次执行上述步骤,是完整的构建。pnpm typecheck— 只做类型检查,不产出文件。
lib/ 已被 gitignore,因此构建是安装或运行前的必需步骤。
E2E 测试
E2E 探针按 provider 分列(如 pnpm test:e2e:codearts、pnpm test:e2e:buddy、
pnpm test:e2e:loomy),均有闸门、默认全部跳过 —— 需要显式设置对应环境变量
(部分还需 ..._CONFIRM=yes)才会真正发出请求。详见 tests/e2e/README.md。
⚠️ 带 -claim / -chat / -tools 后缀的探针会真实消耗额度或领取机会;
带 -refresh 后缀的会真实轮换一次凭据(如 accio-refresh)。运行前请确认。
项目结构
| 路径 | 说明 |
|---|---|
src/ |
TypeScript 源码(宿主侧) |
plugin-src/client/ |
Jet Hub 客户端源码(esbuild 打包) |
lib/ |
编译产物(已 gitignore) |
tests/unit/ |
单元测试 |
tests/e2e/ |
端到端测试(默认跳过) |
docs/agent-notes/ |
逐 provider 的事故史与约定(开发向) |
docs/implementation-notes.md |
全部实现细节与协议笔记(不入库)。⚠️ 目前没有 accio 章节 —— accio 的实测笔记在 docs/agent-notes/providers-accio.md,补写该章节是独立任务 |
cordis.patch.yml |
DSH bundle 补丁 |
No comments yet. Be the first to write one.