kdczyz-dsh-model
Verifiedkdczyz-dsh-model · v1.3.0 · MIT · Web UI
Installable wrapper for the DSH plugin in dsh-kdczyz-mode/: WorkBuddy model pool + Trae connector + Qoder account pool as three independent providers, plus one settings page with six tabs (XD Pool / Trae config [provider switch, account binding, model sel
Install
dsh plugin add kdczyz-dsh-model Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-kdczyz-mode
把 WorkBuddy 与 Trae 两个桌面 App 的模型接进 DeepSeek Harness。
两边已合并为一个插件:dsh-kdczyz-mode。
| 插件 | 状态 | 作用 |
|---|---|---|
[email protected] |
合并版 · 已部署 | WorkBuddy 模型池 + Trae 接入 + Qoder 号池,三个独立 provider 家族,共用一张三标签页管理页(三个配置页,各带「刷新账号 / 添加账号」) |
[email protected] |
归档(已被合并版取代) | 合并前的 WorkBuddy 单插件 |
[email protected] |
归档(已被合并版取代) | 合并前的 Trae 单插件,原版未改动 |
「WOTR」= WorkBuddy + Trae。
合并版提供的四个 provider:workbuddy-xdpool、workbuddy-xdpool-global、
trae、trae-global —— 彼此独立,可同时使用。
2026-10-09 变更
- 两个插件合并为
dsh-kdczyz-mode,用两个 Loader entry 指向不同模块 (原生两套 Config schema,字段不会互相覆盖)。详见 dsh-kdczyz-mode/README.md。- profile 已切换:旧的两个包从
dependencies/bundles/node_modules与pnpm-lock.yaml中全部移除,现在只挂dsh-kdczyz-mode(link: 到本仓库)。- 合并过程中修掉一个真实缺陷:
console.mjs的useEffect依赖数组在loadTrae的const定义之前引用它(TDZ),渲染期抛错 → 设置面板白屏。- WorkBuddy 的独立 Web 控制台(
/wb-console)此前已移除,形态对齐 Trae —— 只注册 LLM provider。- DSH 原生管理页(设置面板内)为三标签页:workbuddy / trae / qoder —— 三边的配置并了进来;每页页头都带「刷新账号」与「添加账号」,账号管理就近落在各自的配置页里。设置里不再有独立的池页面、Trae 卡片或单独的账号页。
- 仓库根新增
package.json(安装壳):让 DSH 插件管理页能直接从这个 GitHub 地址安装。 详见下面「零、安装」。
零、安装
前置:机器上要有 git。 插件管理页装的是 github: 规格,pnpm 解析时会先跑
git ls-remote 问远端 ref —— 没有 git 就会得到这个错,而且失败的痕迹会留在 profile 里
(依赖已经写进去了、但没装成),于是每次启动 DSH 都再报一次:
pnpm: Command failed with ENOENT: git ls-remote 'https://github.com/kdczyz/dsh-kdczyz-mode.git' HEAD 'HEAD^{}'
spawn git ENOENT
Windows 一条命令装上(macOS 用 brew install git,Linux 用发行版自带的包管理器):
winget install --id Git.Git -e --source winget
装完完全退出 DSH 再重新打开 —— DSH 进程要能看到 git;从桌面/开始菜单启动的 DSH 继承的是登录时的 PATH,所以必要时重启一次系统。
方式一:DSH 插件管理页(推荐)
设置 → 插件 → 安装,填 github:kdczyz/dsh-kdczyz-mode(或
https://github.com/kdczyz/dsh-kdczyz-mode),装完重启 DSH。
装之前先看一眼旧包:合并前的 dsh-workbuddy-console、dsh-connect-trae
(以及单独的 Qoder 插件)如果还在,先在管理页卸掉 —— 同一批 Loader entry id
装两份会被加载两次。
两个坑,以前各踩过一次:
git必须能被 DSH 看到(见上面的前置)。装完没重启 DSH,报的还是老错。- peer 版本必须选得中宿主的版本。 pnpm 默认
auto-install-peers=true,会按peerDependencies从公共 npm 另装一份宿主内包。原先的范围是>=0.1.7-rc.1 <0.3.0-0,按 semver 规则排除所有预发布版,于是它装了@deepseek-ai/[email protected]/@earendil-works/[email protected], 而宿主自己用的是0.2.0-rc.2/0.87.1—— 那份副本在解析链上比宿主更近, 会直接遮住宿主那份,表现是「管理页能看到账号,但模型选择器里一个 provider 都没有」。 现在范围写成两条版本线的并集(^0.1.7-rc.1 || ^0.2.0-rc.1,已用真 semver 核对); 而且 DSH 给 profile 写的pnpm-workspace.yaml里本来就带autoInstallPeers: false, 正常不会真去另装 peer。
为什么以前连装都装不上:插件管理页 pnpm add <git 地址> 装的是仓库根,
装完后它读包根的 package.json 找 dsh.bundle.patch —— 组合包的判定标准就是这一个字段
(DSH 文档原文:A bundle is a package whose package.json declares dsh.bundle.patch)。
而这个仓库以前根目录没有 package.json,真正的插件在子目录 dsh-kdczyz-mode/,
于是 pnpm 生成占位包、DSH 找不到组合包声明,报「这个包没有声明组合包,不能作为插件管理」。
现在为什么能装上:仓库根的 package.json 是一个薄壳——
| 字段 | 值 | 作用 |
|---|---|---|
name |
dsh-kdczyz-mode |
与已部署的包名保持一致,profile 里存的 entry id / 设置项全部沿用 |
main / exports["."] |
./dsh-kdczyz-mode/lib/index.js |
WorkBuddy 半边入口 |
exports["./trae"] |
./dsh-kdczyz-mode/lib/trae.js |
Trae 半边入口(第二个 Loader entry) |
exports["./qoder"] |
./dsh-kdczyz-mode/lib/qoder.js |
Qoder 半边入口(第三个 Loader entry) |
exports["./client"] |
./dsh-kdczyz-mode/lib/client.js |
浏览器端 bundle(三标签页) |
dsh.bundle.patch |
./dsh-kdczyz-mode/cordis.patch.yml |
组合包声明,管理页据此识别 |
dsh.client |
platform: web + inject 列表 |
客户端注入目标 |
壳只在包根做转发,插件代码仍在 dsh-kdczyz-mode/ 下,三个 Loader entry 的名字
(dsh-kdczyz-mode、dsh-kdczyz-mode/trae、dsh-kdczyz-mode/qoder)与 profile
里已保存的配置完全不变。
方式二:本地开发部署(link:)
./tools/apply-wotr.sh # 部署到 desktop profile
./tools/apply-wotr.sh --dry-run # 只看计划
这条路径把 dsh-kdczyz-mode/ 直接同步进 ~/.dsh/profiles/desktop/node_modules/dsh-kdczyz-mode,
并在 profile 的 dependencies(link:)与 dsh.profile.bundles 里登记。
改代码即时生效(要重启 DSH)。它是 macOS 专用的(用了 rsync 与 .app 内的绝对路径),
新设备请用方式一。
两种方式的包名都是
dsh-kdczyz-mode,但同时只能走一条:管理页装了 git 版本后, 本地link:会被替换掉,反之亦然。
宿主内包不用你管。 三个入口在模块顶层 import 的 @earendil-works/pi-ai、
@deepseek-ai/dsh-llm-pi-ai、@deepseek-ai/schemastery 等包只存在于 DSH 安装目录里,
DSH 会在内存里把它们路由给每个 profile:@deepseek-ai/dsh-app-boot/lib/worker/profile-resolution-bootstrap.js
(In-memory profile package routing for Node's default ESM and CommonJS loaders)
把 installation scope 的包与 profile 自己装的包合成一张解析表,Node 自身解析不到时兜底。
所以管理页装完即可用,不需要往 profile 里复制或软链任何宿主包。
(本仓库 2026-10-11 之前为此写过一套「装配宿主包 + 免 git 安装」的工具,确认多余后已删除;
tools/link-host-deps.mjs 留着给上面这条本机开发部署用 —— 它让脱离 DSH 的裸 Node
也能解析到宿主内包,本仓库的自检与单测依赖这一点。)
不装 git、只想把插件文件拿到手:backup/ 里有一份整体快照
(dsh-wotr-console-1.1.0-20261011.tar.gz,1.1 MB,插件本体 + 安装壳 + 全部说明),
raw 直链可直接下载,见 backup/README.md。
它是应急/留档用的死文件,不是第二条维护中的安装路 —— 正常仍走方式一。
一、目录结构
dsh-kdczyz-mode/
├── README.md ← 本文件
├── dsh-kdczyz-mode/ ← **合并版插件(当前部署的就是它)**
│ ├── lib/index.js WorkBuddy 半边:provider 注册 + 管理页接口 + 热重载兜底
│ ├── lib/trae.js Trae 半边(上游宿主 lib/index.js 并入;浏览器半边已移除)
│ ├── lib/wb-auth.mjs WorkBuddy 登录协议(客户端 OAuth 轮询换 token)
│ ├── lib/trae-auth.mjs Trae 登录协议(ExchangeToken + 设备绑定)
│ ├── lib/client.js 浏览器端唯一 bundle(三块内联 + settings 作用域解析)
│ ├── lib/console.mjs 三标签页管理页源码(唯一事实来源)
│ ├── lib/credit-store.mjs 积分与历史记录的落盘存储
│ ├── cordis.patch.yml 三个 Loader entry(勿改 id,见文件内注释)
│ ├── scripts/gen-console-inline.mjs 把 console.mjs 内联进 client.js(支持 --check)
│ ├── scripts/gen-xdpool-client.mjs 把 vendor/xdpool/lib/client.js 内联进 client.js
│ ├── test/credit-store.test.mjs 数据层单测(14 项)
│ ├── test/client-apply.test.mjs 仿浏览器调用 apply 的单测(34 项,防启动失败)
│ ├── test/console-render.test.mjs 真正渲染三个 tab(三个配置页 + 活句柄 + 胶囊卡片)的单测(防白屏)
│ ├── test/wb-auth.test.mjs WorkBuddy 登录协议单测(64 项)
│ ├── test/trae-auth.test.mjs Trae 登录协议单测(71 项)
│ ├── test/trae-pool.test.mjs Trae 账号池与凭据落盘单测(46 项)
│ └── vendor/xdpool/ WorkBuddy 模型池(含已修复的核心逻辑)
├── dsh-workbuddy-console/ ← 合并前的 WorkBuddy 单插件(归档)
├── dsh-connect-trae/ ← 合并前的 Trae 插件原版归档(v3.0.0,未改动)
├── patches/ ← WorkBuddy 相对官方 2.0.48 的补丁
│ ├── 03-vendor-pool.patch 工具定义丢失修复 + 流式桥接接线
│ ├── 04-dsml-bridge.mjs 新增文件:DSML → tool_calls 桥接层
│ ├── README.md 补丁说明(哪些还在用、哪些已失效)
│ ├── obsolete-console/ 控制台时代的历史补丁与原文(已失效)
│ └── originals/ 官方 2.0.48 原文(回滚/对照用)
├── docs/
│ └── upstream-issue-workbuddy.md 给插件作者的问题报告草稿(复现步骤 + 根因 + 修复建议,可直接粘贴)
├── backup/ ← **整体快照**:一个压缩包把插件全部文件整合在一起
│ ├── dsh-wotr-console-1.1.0-20261011.tar.gz 插件本体 + 安装壳 + README(解包即用,含 sha256)
│ └── README.md 快照说明:raw 直链下载、怎么校验、里面没有什么
├── tools/ ← 部署与验证工具
│ ├── apply-wotr.sh **合并版:一键部署(含门禁 + 备份 + 清旧包)**
│ ├── apply.sh 合并前的 WorkBuddy 单插件部署脚本
│ ├── install-trae.sh 合并前的 Trae 安装/恢复脚本
│ ├── link-host-deps.mjs 把宿主内包挂进 profile(**只给本机开发部署用**;
│ │ 新设备安装不需要它 —— DSH 自己会路由,见「零、安装」)
│ ├── lib/ host-deps.mjs(装配宿主包)+ asar.mjs(纯 JS 读 app.asar)
│ ├── dsml-test.mjs 44 项单元测试
│ ├── raw-probe.mjs 直连上游抓原始 SSE(绕过 DSH 排查用)
│ ├── read-session.mjs 解压并检索会话日志
│ ├── stub/ 独立进程运行插件所需的依赖打桩
│ └── stub-probe/ 扩展打桩(含 home-paths / atomic-write,供合并版探针用)
└── .backup-before-lite/ ← 精简前的完整备份(含 web/ 与全部后端模块)
二、修了什么
1. 「乱码」的真正根因:工具定义根本没发到上游
现象:用 WorkBuddy 模型对话,回复里出现 <|DSML|tool_calls> 之类的标记文本;工具从不执行。
根因:pi-ai 处理请求时走 resolveTranscript() → collapseSystemMessages(),它只用 context.messages 重建 transcript,会直接丢掉顶层的 context.systemPrompt 和 context.tools。pi-ai 自带的 normalizeContext() 会把这两者折成「首条 system 消息 + toolsAdded」,但插件是直接调用 api.stream(),绕过了那层归一化。
结果:发往 WorkBuddy 上游的请求体里没有 tools。模型拿不到工具 schema,只能自己编一套调用格式(DeepSeek 的 DSML 标记),DSH 只认原生 tool_calls,于是把标记当普通文本渲染出来。
修复(vendor/xdpool/lib/index.js 的 legacyContext()):把系统提示词与工具折成 pi-ai 真正读取的形状。
验证依据:
- 修复前请求日志:
keys=model|messages|stream|...(无tools) - 修复后请求日志:
keys=model|messages|stream|...|tools✓ - 让模型用
write工具写文件 → 去磁盘核对内容,4 个 WorkBuddy 模型全部写对
2. 文本每个 token 重复两遍
根因:WorkBuddy 网关在每一帧都带 finish_reason: "",而桥接层只把 null/undefined 当作"未结束"。于是每个内容帧被"改写一遍 + 原帧又透传一遍"。
修复(vendor/xdpool/lib/dsml-bridge.mjs):空字符串也按"未结束"处理;凡是被改写过的帧绝不再原样转发。
验证依据:抓一段真实上游 SSE,按 1/3/7/64/4096 五种切块方式过一遍桥接层,输出与上游文本逐字节一致。
3. 误报「所有账号均已失效,需要重新登录」
根因:原来是控制台自己读凭证只做 JSON.parse,而 WorkBuddy 5.6+ 把 accessToken 写成了 $wbEncrypted 信封。于是拿对象当 token 发出去 → Bearer [object Object] → 上游 401 → 被误判为账号失效。账号其实是好的。
修复:控制台读凭证那段代码已随控制台一并移除;现在凭证完全由 vendored xdpool 管理,它用 currentApi().pool 拿的是已解密的凭证,问题从根上不存在了。
4. 点「拉起 WorkBuddy 桌面版」导致 DSH 崩溃
根因:spawn('/Applications/WorkBuddy.app') 在 macOS 上触发 EACCES,而异步 error 事件无人监听 → 宿主 fatal 崩溃。
修复:该按钮属于控制台,已随控制台一并移除,此功能不再存在,崩溃路径消失。
5. 新增保险层:DSML 桥接
即使上游哪天又把工具调用漏成文本,dsml-bridge.mjs 也能在 SSE 流里实时识别并改写成标准 tool_calls。它认四种方言:DeepSeek DSML(全角竖线单/双写、ASCII 竖线)、Hunyuan 式 tool_calls:ID、Kimi 式 <tool>名字</tool>。解析不出来就原样透传,不会比不装更差。
三、部署
cd dsh-kdczyz-mode # 改成你实际克隆下来的目录
./tools/apply-wotr.sh --dry-run # 先看计划(不改 profile)
./tools/apply-wotr.sh # 真正部署
合并版是一次替换(不是原地更新),脚本依次做:
- 源码门禁 —— 校验
client.js内联块与console.mjs同步;在仓库内的插件目录挂一层宿主内包软链(裸 Node 跑单测用,见「零、安装」);再跑 24 套单测 + host-assembly(npm test一条链跑完 25 套);任一失败即停,不把坏版本推上去(失败会复跑一次并把失败的断言打出来)。 - 备份 profile 的
package.json/cordis.patch.yml/pnpm-lock.yaml与两个旧包到~/.dsh/wotr-merge-backup/<时间戳>/。 rsync --delete同步dsh-kdczyz-mode/。- 改 profile
package.json:依赖换成link:指向本仓库,bundles里两个旧包换成新包。 - 改 profile
cordis.patch.yml:旧包名替换为dsh-kdczyz-mode,移除独立的dsh-connect-trae条目(entry id 保持不变)。 - 移除三个旧包目录(
dsh-workbuddy-console、dsh-connect-trae、dsh-workbuddy-xdpool)—— 必须删,否则与新包争用同一批 entry id 会导致加载两次。 - 校验目标文件语法与
exports["./qoder"]白名单。 - 把宿主内包装配进 profile,再跑
host-assembly自检(三个入口在该 profile 下真的能加载)。
脚本不跑 pnpm install:它会重算 lockfile 并清掉旧包,所以要手动补一步(否则下次 DSH 启动的 install 可能把旧包拉回来):
NODE=~/.dsh/dsh-runtimes/dsh-primary-runtime/dependencies/node/bin/node
PNPM=~/.dsh/dsh-runtimes/dsh-primary-runtime/dependencies/pnpm/bin/pnpm.mjs
cd ~/.dsh/profiles/desktop && "$NODE" "$PNPM" install # 应显示 Packages: -3
部署后必须重启 DSH(退出后重新打开):这些是 ES module,热重载不会重新加载代码。
配置方式:重启后 → 设置 → WOTR(一个入口,三个标签页):
workbuddy标签页 —— WorkBuddy 侧配置:管理账号池、勾选模型、设置分配策略trae标签页 —— Trae 侧配置:区域开关、刷新账号与 Token、模型勾选与测试qoder标签页 —— Qoder 侧配置:设备码登录、号池与每日签到、模型勾选- 每页页头都有「刷新账号」(重扫本机账号)与「添加账号」(浏览器登录入池)
合并前
workbuddy是设置面板里的独立页、Trae是「设置 → 插件」里的卡片; 现在三者都并进了WOTR 管理,设置里不再有单独的池页面、Trae 卡片或 Qoder 页面。
回滚:把 ~/.dsh/wotr-merge-backup/<时间戳>/ 里的 package.json、cordis.patch.yml、pnpm-lock.yaml 拷回 profile,恢复 node_modules-* 两个旧包目录,再 pnpm install。
只想要补丁、想自己打到新版插件上:见
patches/README.md。 注意:控制台移除与管理页新增都是文件级改动,不能用 patch 表达,需要直接替换/同步lib/下的文件。
改界面:改
dsh-kdczyz-mode/lib/console.mjs(唯一事实来源),然后运行node dsh-kdczyz-mode/scripts/gen-console-inline.mjs把源码内联进lib/client.js。 忘了跑这步界面上不会有任何变化,也不报错 ——apply-wotr.sh会拦下来。
四、验证方法(怎么确认真的好了)
NODE="/Applications/DeepSeek Harness.app/Contents/Resources/runtime/primary-runtime/dependencies/node/bin/node"
# 1) 桥接层单测(44 项,含文本重复 bug 的回归)
cd tools && "$NODE" dsml-test.mjs
# 2) 全部单测 = 部署门禁(24 套 + 装配后的 host-assembly = 25 套,见「三、部署」)
cd .. && ./tools/apply-wotr.sh --dry-run # 一次跑完所有套件
cd dsh-kdczyz-mode # 或单独复跑某几套:
"$NODE" test/credit-store.test.mjs # 数据层
"$NODE" test/client-apply.test.mjs # **防 DSH 启动失败**
"$NODE" test/console-render.test.mjs # **防设置面板白屏**
# 注意:直接 import lib/trae.js、lib/qoder.js 的套件需要仓库内的宿主内包软链,
# 缺了就先跑一次 ./tools/apply-wotr.sh(它会自动补,见「零、安装」)
# 3) 三块内联内容是否与源码同步
"$NODE" scripts/gen-console-inline.mjs --check
# 4) 请求是否带上了工具定义(关键判定)
tail -5 ~/.dsh/.workbuddy-xdpool/upstream-request-shape.log
# 键名里应出现 tools
# 5) 直连上游抓原始 SSE(绕过 DSH,用来区分"上游问题"还是"中间层问题")
cd ../tools && "$NODE" --import ./stub/register.mjs raw-probe.mjs
# 6) 解压并检索会话日志
"$NODE" read-session.mjs ~/.dsh/sessions/<工作区>/session-*/session.v4.jsonl.zstd
# 7) 直接读落盘的使用记录
ls -la ~/.dsh/plugin-data/dsh-workbuddy-console/
# 8) 四个 provider 是否都活着(DSH 运行中才可查)
# 前缀必须与下面**逐字一致**(都带 dsh- 前缀)。写错前缀只会得到 404,
# 与「插件没加载」的表现一样,很容易误判 —— 踩过。
curl -s "http://127.0.0.1:19387/plugins/dsh-workbuddy-xdpool/status?region=cn" | head -c 300
curl -s "http://127.0.0.1:19387/plugins/dsh-workbuddy-xdpool/status?region=global" | head -c 300
curl -s "http://127.0.0.1:19387/plugins/dsh-connect-trae/usage?region=cn" | head -c 300
curl -s "http://127.0.0.1:19387/plugins/dsh-connect-trae/usage?region=ai" | head -c 300
# 管理页自己的数据接口(前缀是**合并后的包名**;sample 只收 POST,GET 返回 405 正常)
curl -s "http://127.0.0.1:19387/plugins/dsh-kdczyz-mode/history-summary"
# 9) profile 里应只剩合并版一个包
ls ~/.dsh/profiles/desktop/node_modules/
最硬的验收方式:让模型用 write 工具写一个文件,然后去磁盘核对内容是否正确——不看模型自己说"已完成"。
验收管理页:打开 设置 → WOTR,三个 tab(workbuddy / trae / qoder)应各有数据(默认停在 workbuddy)。每页页头都有「刷新账号」与「添加账号」两个按钮。
路由前缀有两套,别搞混:
/plugins/dsh-workbuddy-xdpool/*、/plugins/dsh-connect-trae/*—— 有意沿用旧名(都带dsh-前缀), 因为 vendored 的池卡片与 Trae 卡片按各自原本的 id 注册、并按该 id 解析设置作用域,改名会让卡片变只读。/plugins/dsh-kdczyz-mode/*—— 管理页自己的两条接口(history-summary、sample),用合并后的包名。来源校验:这些接口只信任回环来源 —— 不带
Origin或带127.0.0.1/localhost都放行, 外域来源返回403 {"error":"origin-not-trusted"}。用 curl 排查时不必特意加Origin头。
五、Trae 半边(原 dsh-connect-trae)
上游宿主代码未做改动——它本身就把工具循环做对了,不需要打补丁。合并时以
lib/trae.js 逐字节并入,已核对零差异;上游的浏览器半边
(vendor/trae/client.js)已整体移除,Trae 配置页改由本包自写(见下)。
它做什么:把本机已登录的 Trae 账号接进 DSH。国内版注册为 trae provider,国际版注册为 trae-global,两边可同时使用。会自动发现本机的 Trae CN / TRAE SOLO CN / Trae / TRAE SOLO 四种安装的登录凭据,也会读 CLI 的 ~/.trae-cn/trae-jwt-token,并按凭证自带的 userRegion 自动判定区域。
为什么它不需要像 WorkBuddy 那样修:它的实现是把上游返回的结构化 tool_calls 解析出来再交给 DSH 本地执行(README 里明确写了这条工具循环),而 WorkBuddy 那个插件的路径是把上游字节原样转发、且工具定义在中间层被丢掉。
本机状态(2026-10-09 实测):
| 检查项 | 结果 |
|---|---|
| 上游版本 | [email protected](MIT,作者 dingminhua) |
| 合并后位置 | dsh-kdczyz-mode/lib/trae.js(与原文件 diff 为空)✓ |
| provider 注册 | trae、trae-global 都在 DSH 里 ✓ |
| 登录态 | signed-in,账号 1412,来源 TRAE SOLO CN(edition solo),区域 cn ✓ |
| 令牌到期 | 2026-10-17(未过期,自动续期)✓ |
| 可用额度 | 总 2900,可用 2383.77 ✓ |
配账号(重启后):设置 → 插件 → Trae 卡片 → 国内版 tab → 点「刷新账号与 Token」→ 选账号 → 勾选要用的模型。之后在输入框模型选择器里就能看到 trae 分组。
凭据位置:只读桌面端 User/globalStorage/storage.json 或 CLI 的 trae-jwt-token;刷新后写到 $DSH_HOME/.trae-auth.cn.json 与 $DSH_HOME/.trae-auth.ai.json,不写进 DSH 设置。本仓库不含任何凭据文件(只含插件代码)。
合并前的单插件归档在
dsh-connect-trae/,仅供回滚与版本留存; 恢复单插件形态用./tools/install-trae.sh。
六、WOTR 管理页(三个标签页)
设置 → WOTR 是唯一入口:配置与查看都在这一页里,设置里不再有单独的池页面或 Trae 卡片。
| 入口 | 管什么 |
|---|---|
workbuddy 标签页 |
配置:WorkBuddy 的模型勾选、账号分配策略、积分自动化 |
trae 标签页 |
配置:Trae 的区域开关、账号与 Token、模型勾选与测试 |
qoder 标签页 |
配置:Qoder 的设备码登录、号池、每日签到与额度刷新、模型勾选 |
三个标签页的组件本体都是本包自写(
PoolConfigTab/TraeConfigTab/QoderPoolPanel),对齐同一套模块样式;vendored 的PoolCard仍被内联执行, 但只用于提供类型与不参与渲染。每页页头都带「刷新账号」与「添加账号」。
三个 tab
| Tab | 内容 | 数据来源 |
|---|---|---|
| workbuddy | 账号池状态、账号启用/移出、模型勾选与上下文、分配策略、积分自动化 | vendored /status |
| trae | 区域开关、账号与 Token 刷新、模型勾选与测试、输入框读数开关 | Trae 半边 /usage 等 |
| qoder | 设备码登录、号池、每日签到与额度刷新、模型勾选 | Qoder 半边 /qoder/pool 等 |
「账号」(三列汇总)与「积分」(积分包明细)两个只读标签页已删除:它们的内容 与三个配置页重复,却各自维护一份取数与刷新节奏 —— 同一份
/usage被拉两次、 两处显示的余额还可能不一致。现在每个平台的账号与余额只在它自己的配置页 和 composer 那颗胶囊的卡片里出现(见下一节)。
打开本页默认停在 workbuddy 标签页。
每页都是单列自上而下:页头 → 状态说明 → 统计小卡 → 「账号」区 → 「模型」区。 页头按钮固定为「刷新账号」(重扫本机账号)与「添加账号」(浏览器登录入池)。
三页共用同一套模块样式:列头、统计小卡、账号卡、KV 明细、空态都由
console.mjs里同一组共用组件(ColumnHead/StatsRow/AccountCard/KV/EmptyBox)渲染。单测里直接比对各页的元素骨架与 class 集合 来守住这一点(只看文本抓不到 —— 各写各的也能对上文案)。签到的粒度是按账号(每张卡签自己那个账号、不切换正在服务的账号): WorkBuddy 上游自己认
accountId;Trae 靠「拿那个账号的凭据走同一条流程」; Qoder 走活动线me/campaigns → claim(见dsh-kdczyz-mode/README.md的「每日签到(活动线)」一节,含Cosy-ClientType必须是10这个静默坑)。
composer 的积分胶囊:点一下看这个平台的全部账号
输入框下面那颗「积分 1,104 · 1412」胶囊是按钮:点开是一张卡片, 列出当前模型所属那一个反代平台的账号 —— 不是「所有平台的账号」:
| 平台 | 卡片里列什么 | 胶囊数字是谁的 |
|---|---|---|
| WorkBuddy | 国内版 + 国际版两个区域,各一行组标题(国内版 2 个 · 本轮用 1412) |
当前 provider 那个区域里正在服务的账号(activeAccountId) |
| Trae | **国内版(cn)+ 国际版(ai)**两个区域 | 当前区域里 lastServedId 那个账号,没有就退回「当前账号」 |
| Qoder | 单个号池(没有区域概念,所以不画组标题) | serving.accountId,没有就用当前账号 |
每一行有两个控件(用户明确要的两件不同的事):
- 「参与轮换」开关 —— 把这个账号移出/放回轮换。WorkBuddy 走
POST /plugins/dsh-workbuddy-xdpool/accounts/disabled(服务端对当前值做合并); Trae / Qoder 是设置里那份disabledAccountIds(读-改-写合并,不整段覆盖)。 - 「切换到此账号」按钮 —— 让下一次请求优先用它。WorkBuddy 走
POST …/accounts/select?region=cn|global;Trae 打/pool/account?region=并且 必须同时写accounts设置(那个路由只改内存里的活池,重启就回去了)。 - 已经是「当前账号」的那一行:WorkBuddy 把按钮换成「取消固定」(它的池把 「没有当前账号」当正常状态,取消即交回轮换);Trae / Qoder 不给按钮 —— 这两家的池永远有一个当前账号,「切换到这个已经是当前的账号」是纯 no-op。
几个刻意的取舍(都写进了 console.mjs 的注释与单测):
| 取舍 | 为什么 |
|---|---|
| 只在这几个时刻取数:打开卡片、手动「刷新」、写完一次启停/切换之后 | 上游按账号计费的读(Trae 的 /usage 是一次实时计费调用)。卡片开着也不轮询 —— 单测里钉着「展开卡片不新增任何定时器」 |
| 胶囊自己 30 秒一刷,卡片打开时顺手带一次新的 | 胶囊那颗数字是「我现在能用多少」,长期停在旧值会误导;但只刷胶囊,不刷整张卡片 |
Trae 行显示 available/generalAvailable,不是 total |
Trae 的 credits.total 是发放总量(3,050),available = total - consumed 才是剩余(796.88)。真机上曾经把发放总量当余额显示 |
| 写失败只占一行红字,不清空已显示的行 | 上游 4xx/断网时把卡片清成空态,用户会以为"账号没了" |
disabledAccountIds 在 WorkBuddy 是两个区域共用的一份扁平数组 |
配置页只看得见本区域的账号,照本区域拼一份完整名单写回设置,会把另一个区域已停用的账号静默重新启用。所以这个开关只能走上面的路由,由服务端合并 |
卡片本体是 createPortal(…, document.body) + position: fixed(拿不到 react-dom
时退回行内渲染):composer 外面在嵌入形态下有 overflow: hidden 的容器
(.ST7X_W_embeddedBody),行内浮层会被裁掉;内置的 ContextMeter 用的也是
「fixed + 端口到 body + z-index: 1100」这条路。
「登录账号」
WorkBuddy 可以直接在浏览器里登录添加(workbuddy 页页头「添加账号」), 也可以沿用原来的方式:在桌面版登录后在管理页点「刷新账号」。
浏览器登录不是 OAuth 授权码流程(没有 client_id / redirect_uri / PKCE,
不需要本地回调服务),而是「服务端发 authUrl → 浏览器登录 → 拿 state 轮询换 token」。
详见 dsh-kdczyz-mode/README.md 的「浏览器登录添加账号」一节,含凭据明文落盘的
风险说明与「已验证 / 未验证」清单 —— 国际版与 Trae 侧目前未做实测。
插件全程不接触你的密码:登录在官方页面里完成,插件只接收登录后的令牌。
采集链路(2026-10-09 恢复)
控制台移除时,写 credit-samples.jsonl / history.jsonl 的模块一并删掉了,数据停在 16:47。现已由 lib/credit-store.mjs 恢复,并沿用原文件格式,所以旧数据直接续上。
采集时机:打开管理页时(不做后台轮询)。这样既不产生无谓的网络请求,也避免后台常驻任务。
新增 daily.jsonl 记录每个账号每天的首末值 —— 因为 credit-samples.jsonl 按 (day, uid) 去重后只剩最后一条,当天最初值丢失,净变化会恒算成 0。两个文件分工:samples 看「最新快照」,daily 看「趋势」。
数据结构
~/.dsh/plugin-data/dsh-workbuddy-console/
├── credit-samples.jsonl { ts, day, uid, nickname, credit } (最近 120 天)
├── history.jsonl { ts, day, uid, nickname, credit,
│ checkedIn, streakDays, source } (最近 400 天)
└── daily.jsonl { day, uid, nickname,
first, firstTs, last, lastTs, n } (最近 400 天)
目录名有意保留旧插件的名字(
dsh-workbuddy-console,不是合并后的包名): 合并前采集的历史就在这个路径下,改名会让过去的记录凭空消失。
实现要点
- DSH 原生组件,不是独立页面:没有
web/目录、不开/wb-console - 组件源码是
lib/console.mjs(普通 ES 模块,便于阅读测试),由scripts/gen-console-inline.mjs内联进lib/client.js(后者是 classic script,不能有顶层import) → 改界面请改console.mjs,然后重跑生成脚本;apply-wotr.sh会校验两者是否同步 lib/client.js是两块生成内容的容器:管理页(console.mjs)与 账号池客户端(vendor/xdpool/lib/client.js)。后者以window.__ModuleLoader__.load({id, factory})原样内联,用一次性替身加载器 捕获其 factory 产物 —— 上游代码一行都不用改(Trae 浏览器半边已移除, 不再有第三块内联)- 内联产物是真函数(
const __WBC_CONSOLE_FACTORY__ = (react, jsxRuntime) => {...}), 不是new Function(源码字符串)—— 后者可能被渲染进程的 CSP 拒掉,且语法错误只在运行期暴露 - 两条接口按需自注入
webServer(与 vendored 账号池路由同样做法), 宿主没提供时管理页降级为「暂无记录」,provider 功能不受影响:GET /plugins/dsh-workbuddy-console/history-summaryPOST /plugins/dsh-workbuddy-console/sample
- 两条接口都做回环来源校验(拒绝非
127.0.0.1/localhost的 Origin) apply整体被 try/catch 兜住:一个纯 UI 页面的问题绝不能让整个应用起不来 (cordis 的 fiber 一旦 apply 抛出就变failed,宿主会弹「应用无法启动」)
测试(都是 apply-wotr.sh 的硬性门禁)
cd dsh-kdczyz-mode/dsh-kdczyz-mode
node test/credit-store.test.mjs # 14 项:积分/历史记录的读写、去重、坏行容错
node test/client-apply.test.mjs # 仿浏览器执行 client.js,真正调用 apply
node test/console-render.test.mjs # 444 项:真正渲染三个 tab 与 composer 胶囊卡片,空数据/坏数据/边界值
node test/wb-auth.test.mjs # WorkBuddy 登录(state 轮询)、凭据落盘
node test/trae-auth.test.mjs # Trae 登录(PKCE+回环回调,自动回调)、凭据落盘
后两层是必须有的,各对应一次真实事故,合并后又各抓到一次新问题:
① client-apply —— 防「应用无法启动」
2026-10-09 出过一次 DSH 启动失败(弹窗「应用无法启动或已意外停止 /
web boot: 1 entry did not activate」)。根因是 lib/client.js 的包装模块里
用了未声明的 react(它只在 vendored 内部存在),apply 抛 ReferenceError
→ cordis fiber 变 failed → 宿主拒绝启动。
当时只测了「内联工厂能求值」,没有驱动 apply,所以完全没抓到。
卡片并入管理页后,这一层还负责守一条跨模块契约:截获到的 vendored 组件
必须真的渲染进管理页,且拿到 settingsScope 与 view: 'page'。
否则两个配置标签页就是空白 —— 注册项全部正确、不报任何错。
用「把 pickCard 改成恒返回 null」反向验证过:5 条断言会失败。
② console-render —— 防设置面板白屏
更早一批改动里组件用 status?.accounts ?? [] 取数组。?? 只挡 null/undefined,
接口若返回字符串或对象就漏过去,随后 .map/.reduce 在渲染期抛错 ——
启动阶段一切正常,只有真的调用组件才暴露。现已统一用 asArray() 收口。
合并过程中这一层抓到一个新的真实缺陷:console.mjs 里
useEffect 的依赖数组 [tab, loadTrae] 写在 const loadTrae = useCallback(...)
之前,同一函数作用域内触发 TDZ,抛
Cannot access 'loadTrae' before initialization —— 渲染期抛错,正是白屏。
把该 effect 移到 loadTrae 定义之后即修复。当时 30 个渲染用例全灭。
卡片并入时这一层扩到 73 项,覆盖嵌入组件的多种坏形状(embedded 整体缺失、
单项为 null、Component 不是函数)都不许抛 —— 上游改坏或某一半加载失败时,
后果只应是这一个标签页显示降级说明,而不是整个设置面板白屏。
另有一条断言守住 settingsScope / t 的原样透传:
丢掉它两张卡片会静默退化成只读(改完点保存、值弹回原样)。
「账号」页合成三列后又加了五条:列的顺序与归属(直接查元素树,
三列若塌成上下三块文本一模一样,只看文本抓不到)、三列互不连坐
(一侧数据坏掉时另外两侧照常渲染),以及三列同一套模块样式 ——
把每列展开成「标签 + className」序列,断言三列用到的模块 class 集合一致、
列头骨架逐项对应。三条都用反向验证确认过会失败
(删掉一侧统计行 → class 集合不等;删掉一侧列头按钮 → 列头骨架不等)。
这一层还抓到过一个关于同步性的真实缺陷:ctx.slots.inject(key, cb)
不是同步调用 cb 的 —— 它订阅槽位的 declaration epoch,声明到了才跑。
而 captureCards() 原先在 apply 返回时就把捕获结果当快照读出并交给管理页,
那通常一张卡片都还没有,于是两个配置标签页永远显示「配置组件未能载入」
(WOTR 页本身、标签栏、其余三页全部正常,也不报错)。
改成活句柄(get() + subscribe(),管理页在渲染期取值)后修复;
console-render 的「晚到」用例守住它 —— 把 useSlot 退回快照式实现,
该用例会以「订阅者 0 个」失败。
两层都可以反向验证:
WBC_CLIENT_PATH=/tmp/x.js node test/client-apply.test.mjs可测任意 client.js 副本; 故意注回 bug 时,前者以「管理页已注册 settings.section — got 0」失败, 后者以「组件渲染抛错」失败。
七、合并前后的形态对比
dsh-kdczyz-mode(合并版) |
合并前的两个插件 | |
|---|---|---|
| 包数量 | 1 个 | 2 个 |
| Loader entry | 3 个(指向不同模块,三套独立 Config) | 各 1 个 |
| 独立 Web 控制台 | ❌ 无 | ❌ 无 |
| 管理页 | ✅ WOTR 管理,三标签页(三边配置各自带「刷新账号 / 添加账号」) |
WorkBuddy 三标签页;Trae 无 |
| 配置入口 | 并入 WOTR 管理 的 workbuddy / trae 标签页 |
XD Pool 独立页 + Trae 卡片 |
| provider | workbuddy-xdpool、workbuddy-xdpool-global、trae、trae-global |
各占一半 |
| 注册的 HTTP 路由 | 前缀沿用 /plugins/dsh-workbuddy-xdpool 与 /plugins/dsh-connect-trae |
同名 |
| 是否被修改 | WorkBuddy 侧已打补丁;Trae 侧一字未改 | 同 |
八、已知局限
- 改动落在
node_modules里(当前是link:指向本仓库),插件升级会被覆盖。 - 依赖各客户端的私有接口(WorkBuddy 走
v2/chat/completions,Trae 走llm_utils_chat)。官方改协议都可能失效;WorkBuddy 的桥接层失效时会退化成"原样透传",不会比不装更糟。 - 建议把「工具定义被 pi-ai 归一化丢弃」和「5.6+ 凭证未解密」这两个问题反馈给 WorkBuddy 插件作者,这是长期修法。
- 「积分」与「使用记录」两个 tab 已移除(2026-10-12):采集接口
/sample与/history-summary仍在服务端保留,但管理页不再读取,daily.jsonl因此不再增长。 - 每日净变化的语义是「当天最后观测 − 当天最初观测」。若当天只在消耗之后才打开过页面,最初值就是消耗后的值,那天的净变化会显示为 0 而不是负数。想更准就要提高采集频率。
- 合并版未发布到 npm,profile 里用
link:指向本仓库;换机器要带上这个目录,或先发布再改用版本号。 - 两个 entry 的 id(
llm-workbuddy-xdpool、dsh-connect-trae)不能改名:前者是 vendored 池卡片精确匹配设置命名空间的键,后者匹配 Trae 半边的/trae/i探测并保留已存设置。改名会让对应卡片变只读。