dsh-connect-workbuddy 3.1.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +186 -0
- package/README.en.md +49 -20
- package/README.md +56 -26
- package/THIRD_PARTY_NOTICES.md +4 -2
- package/docs/WINDOWS.md +1 -0
- package/docs/assets/account-pool-demo.html +207 -262
- package/lib/bin.js +1 -1
- package/lib/client.js +457 -725
- package/lib/{host-heartbeat-DZnPggVT.js → host-heartbeat-C2_Zmuqq.js} +96 -3
- package/lib/index.d.ts +126 -32
- package/lib/index.js +1441 -278
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,191 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.4.0 (2026-10-02)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- **账号池不再被一次瞬时故障永久打死:`unavailable` / `failed` 现在有 30 分钟冷却。**
|
|
8
|
+
- **原来的形状**:`exclusionOf` 只给 `rate-limited` / `out-of-credit` 判冷却到期,`unavailable` / `failed` 一律无条件出池且**不带恢复时间**。而自动复测定时器已在 3.2.0 删除,能刷新它的只剩手动「测试」——**一次 502 或一次网络抖动就能把一个健康账号永久踢出池子**。
|
|
9
|
+
- **症状**:池内每个成员都被这样剔除后,池子没有任何候选,`failoverAccountFor` 第一轮即返回 `undefined`,自动换号无处可去,上游原始 429/502 直接报给用户——界面上「账号池已启用」与「请求失败」同时成立,且报错没有 `(after trying N accounts)`(因为只试了 1 个账号)。
|
|
10
|
+
- **现在的形状**:`unavailable` / `failed` 与限流同形,走 `retryDue()`;上游没给时间就按 `POOL_UNKNOWN_COOLDOWN_MS`(30 分钟)兜底。冷却在**排序时求值**,所以不需要定时器——到点的成员在下一次请求自动变回候选。`credential-rejected` 与 `not-found` 保持永不冷却:一个要重新登录,一个是本区域不再提供的模型,等待都修不好。
|
|
11
|
+
- 反转了 `tests/account-pool.spec.ts` 里一条**明确钉住旧行为**的测试(其原注释即写明「这是留给未来主动决定的反转」),并在测试内记录反转理由。
|
|
12
|
+
|
|
13
|
+
- **自己中止的请求不再被记成账号故障。** 客户端断开时 `res.on('close')` 会 abort 上游调用,`fetch` 抛出的 `AbortError` 经 `chatStream` 统一成 `status: 0, kind: 'server'`,读起来与「网络真的断了」完全一样——于是**关一次面板就可能把正在服务的账号记成 `unavailable`**。换号循环本来就有 `controller.signal.aborted` 守卫,但那只管「要不要继续换号」,不管「要不要把这次失败记成测量」;`reportFailures()` 现在同样短路。
|
|
14
|
+
|
|
15
|
+
- **换号路径不再抓积分。** `failoverAccountFor` 用的是 `poolMembersOf`(逐成员 `fetchCredits`),而它自己上方的注释写着「不得再花请求去决定谁服务」——一次失败的对话因此先变成 N 个额外上游请求才发出重试。改用 `localPoolMembers`,与 `applyPoolSelection` 对齐;有新鲜快照时积分仍然参与排序。
|
|
16
|
+
|
|
17
|
+
- **探测请求补上超时(10 秒)。** `probeChat` 在没有 signal 时完全没有上限,而池的批量测试是**串行**且不传 signal——一个挂住的连接会阻塞整批,且界面上看不出是哪一个成员。现在给「等待响应」设 10 秒上限(远高于正常网关的首字节,远低于用户的耐心),并与调用方传入的 signal 用 `AbortSignal.any` 合并,单模型路由的取消行为不变。
|
|
18
|
+
- **上限只覆盖等待响应**:`fetch` 一 resolve 就 `clearTimeout`,绝不在读取 body 期间继续计时——否则一个健康但偏慢的模型会和死端点共用同一块表,这正是此处不能用裸 `AbortSignal.timeout` 的原因。
|
|
19
|
+
|
|
20
|
+
- **两句会误导人的文案改掉了。** `row.probeUnavailable` 去掉了「这是网络问题,**不是模型问题**」——`unavailable` 这个分类里同时装着网关 5xx、DNS 失败与请求超时,对后两者这个断言并不成立(5xx 恰恰是上游侧的问题)。新文案只陈述可核实的事实:「连不上上游——上游没有给出可用答复」,具体原因由旁边记录的 `message` 给出。
|
|
21
|
+
- `row.poolNoCandidateHint` 不再叫人「重新检测账号,或在桌面端重新登录」。池内全不可用时那个建议是错的:限流、积分耗尽与瞬时故障都会**自动回池**(上游给的时间,没给就 30 分钟兜底),只有「凭据被拒」才需要重新登录。原话会让用户去做一件没有任何用处的事。
|
|
22
|
+
- 顺带修掉 3 处本地化文案里的字面量 Markdown(`**真实体积**`、`**不会**`、`**手动动作**`)。React 不解析 Markdown,它们此前是原样把星号画在界面上的——与本轮改掉的两处属于同一类缺陷。
|
|
23
|
+
- **池表与模型行不再把上游报文原样贴一遍。** 限流账号的记录文本就是 `{"code":6004,"msg":"…将在 2026-10-02 06:53:43 重置…","requestId":"…"}`——与旁边刚显示的绝对时刻完全重复;加上新的相对说法后会变成同一时间印三次,后面还跟一个没人看的 requestId。规则现在只在 `src/client/probe-reason.ts` 里写一次、两处共用:
|
|
24
|
+
- **标签本身已说清的结果**(`rate-limited` / `out-of-credit`,它们都带恢复时刻)**不内联任何正文**;
|
|
25
|
+
- 其余结果内联**短**形式:JSON 信封里取 `msg` 那句人话,`transport error: ` 这个英文前缀也去掉(中文卡片上它只是噪声),整段封顶 72 字符——一行的病态正文不该把整张表挤出屏幕;
|
|
26
|
+
- 完整原文挂在 `title` 上,**缩短不等于丢掉**(含 requestId),悬停即可看到。
|
|
27
|
+
- 例外是有意的:`credential-rejected` / `not-found` 不在「已说清」之列——它们的标签只给结论(「重新登录」「没有这个模型」),上游原话才是让结论可核实的东西。
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
|
|
31
|
+
- **池表能说清「为什么出池」了。** `WorkBuddyPoolProbe` 新增 `message`:落盘前经 `redactUpstreamText` 脱敏(与探测路径共用同一实现),卡片在结果后追加 `· <上游原话>`。此前 DNS 失败、网关 502、被中止的请求在池表里渲染成**逐字相同**的一句话,用户拿到的是无法行动的提示。字段可选,旧记录照常读取而不被当成损坏。
|
|
32
|
+
|
|
33
|
+
- **冷却时间现在同时给出「还要等多久」。** 卡片一直只印上游给出的绝对时刻(`05:23:27`)——它回答「何时」,但要读者自己拿当前时间去减,这一步正是让冷却难以行动的地方。现在它旁边多一个相对说法:`05:23:27 之后可再用 · 约 1 小时后`。
|
|
34
|
+
- 粒度按距离切换:不足 1 分钟 → 「即将恢复」;不足 1 小时 → 「约 N 分钟后」;不足 2 天 → 「约 N 小时后」;再远 → 「约 N 天后」。**小时档从整点开始**(不是 90 分钟)——因为用户自己描述那个 69 分钟后的时刻时写的就是「约 1 小时后」,阈值放高反而会把触发这个改动的那个例子渲染成「约 69 分钟」。天档切在 **2 天**而不是 1 天,否则 36 小时会印成「约 1 天后」,少说 12 小时。
|
|
35
|
+
- **只对上游给过的时间生效**:`cooldownOf` 返回 `{}`(上游没说时间)时仍然只说「上游未给出何时恢复」,绝不编造倒计时——这是本功能一贯的红线,本次没有新增任何时间来源,只是把已有的那个换个说法。已经到点的时刻返回空串而不是「0 分钟后」。
|
|
36
|
+
- 模型行与池表两处都接上了;绝对时刻仍是主文案,相对说法是附加的便利。
|
|
37
|
+
|
|
38
|
+
### Docs
|
|
39
|
+
|
|
40
|
+
- README 的「账号为什么出池」表按新规则改写:区分「限流/欠费/瞬时故障会自动回池」与「凭据被拒/模型不存在只能手动重测」,并删掉已不存在的「定时器」表述。
|
|
41
|
+
- 清理死代码:`POOL_TICK_MS`、`POOL_MIN_INTERVAL_MINUTES`、`poolDueAt`、`duePoolRegions`、`PoolScheduleInput` / `PoolScheduleResult` 及其单元测试全部删除。它们自 3.2.0 起已无任何生产调用(构建时已被 tree-shake),却仍被测试覆盖——于是「测试全绿」对「账号会不会回来」这个问题什么也没说明。
|
|
42
|
+
|
|
43
|
+
## 3.3.0 (2026-10-01)
|
|
44
|
+
|
|
45
|
+
### Features
|
|
46
|
+
|
|
47
|
+
- **DSML 恢复:模型把工具调用写进正文时,插件现在会把它转回真正的 `tool_calls`。**
|
|
48
|
+
- **背景**:上游模型在长上下文下偶尔漏掉工具调用的起始包裹,把 `<|DSML|invoke name="…">` 这类标记直接写进 `delta.content`。DSH 因此**没有任何东西可以路由**——标记被当正文渲染出来,也就是用户看到的「一堵乱码墙」。这是模型族的有名缺陷,不是本插件的 bug,但本插件正好站在「拿到字节之后、交给客户端之前」这一层,是唯一能动手的地方。
|
|
49
|
+
- **四道闸**(缺任何一道,上游都真的出过生产事故:把空参数调用发出去、把**讨论**这段标记的正文执行成真实调用、把请求根本没声明的工具提升成可执行调用):
|
|
50
|
+
1. **闭合才提交** —— 只有闭标签到达才产出调用;被 token 上限截断的块按原文显示,绝不猜。
|
|
51
|
+
2. **名字必须在本次请求声明的工具里** —— 名字取自 `prepareChatBody` 已经解析过的那份请求体,不再解析第二遍。
|
|
52
|
+
3. **没声明工具就完全不做** —— `tool_choice: "none"` 会在预处理时删掉 `tools`,于是「本次没声明工具」是一读即得的既成事实,而不是另一条要记得同步的规则。
|
|
53
|
+
4. **任何可疑一律回滚成正文** —— 必填参数缺失、名字对不上、解析失败:整块**原样显示**。注意是「显示」不是「删掉」:静默吞掉一段文本会丢内容而用户看不出来——「检查永远不命中」与「检查通过」在界面上长得一模一样,这个坑本项目已经付过一次学费。
|
|
54
|
+
- **只有整轮都是标记时才重发一次**:能恢复的形态当场转换(零重新生成、不加延迟、不重复计费)。本仓库真实捕获的那一类——工具名被写坏成 `<|DSML| validate>`、拿不到任何工具名——注定过不了第二道闸,按原文显示;只有当**整轮去除标记后没有任何正文**时才重发一次,并复用既有的账号池换号语义。判据是可复核的既成事实,不是猜测。
|
|
55
|
+
- **普通回答不受影响**:只有在「回答到目前为止全是标记」时才扣住字节,一旦出现真正的正文或调用就立刻按序放行并恢复原样透传。因此正常请求的延迟没有变化,也不会出现「先丢字节再反悔」——那会造成「两段回答拼接」,是既有换号逻辑明确拒绝的事。
|
|
56
|
+
- **上游已经会发结构化调用时一律不插手**(粘性判定):一次回答里两条调用通道互相竞争,比任一条单独出问题更糟。
|
|
57
|
+
- 解析器移植自 [`hawklithm/workbuddy2api`](https://github.com/hawklithm/workbuddy2api) 的 `dsml_parser.py`(MIT),并补上它**没有的**第 2、3 道闸;逐条偏离见 `src/dsml-recovery.ts` 头部与 `THIRD_PARTY_NOTICES.md`。
|
|
58
|
+
|
|
59
|
+
- **`scripts/verify-dsml.mjs` 新增 `--through-shim`**:让真上游响应**经过 shim** 再判定,量的才是「客户端最终看到什么」。原有的 `--live` 直连 `client.chatStream`、绕过转换,对已经能恢复的泄漏会给出**「仍在泄漏」的假结论**——而这正是该脚本最不该犯的错。同一次改动里,它的静态探针也从「响应路径是否仍是 `body.pipe(res)`」改为「响应路径是否已被接管」,否则实现之后它会反过来陈述事实。
|
|
60
|
+
|
|
61
|
+
### Docs
|
|
62
|
+
|
|
63
|
+
- **`docs/DSML-LEAK-RESEARCH.md` 第五节改写**:原口径是「本仓库不实现恢复(只诊断)」,其理由里有两条后来不再成立——声明了哪些工具、必填参数是什么,`prepareChatBody` 早就解析过了;「闭合才提交」由解析器自己判定。该节现在说明定位已改为**诊断 + 恢复**,并指明恢复必须照抄第三、四节那四道闸。
|
|
64
|
+
- 新增 `docs/DSML-RECOVERY-PLAN.md`:移植清单、四道闸落点、改动点、测试矩阵(含「移除任一闸必然变红」的变异清单)与文档口径修正。
|
|
65
|
+
- `THIRD_PARTY_NOTICES.md`:新增 `hawklithm/workbuddy2api` 条目,并**修正第四节**——它原本写「未复制、修改或再分发任何参考项目的源文件」,而本次是**代码移植**,那句话必须如实改掉。
|
|
66
|
+
- 两份 README 的致谢章节补上 `hawklithm/workbuddy2api`,并标明它是名单里**唯一一项代码移植**(发布清单要求致谢章节列出全部参考项目;漏掉它等于把本版的主线改动排除在署名之外)。
|
|
67
|
+
- `docs/WINDOWS.md` §5 登记诊断脚本的跨平台写法:`verify-dsml.mjs` 改用 Node 读源码代替 `grep`,仓库根改用 `fileURLToPath`。
|
|
68
|
+
- 新增 `docs/DSML-RECOVERY-WINDOWS-VERIFY.md`:面向「到 Windows 机器上再验一次」的操作手册,取证点全部是机器可读的,并把两条最容易被误判成失败的事实写在最前面(工具名被写坏的那一类仍以正文显示;模型会摇摆)。
|
|
69
|
+
|
|
70
|
+
## 3.2.0 (2026-10-01)
|
|
71
|
+
|
|
72
|
+
### Features
|
|
73
|
+
|
|
74
|
+
- **账号池的开关从此就是「智能路由 + 自动换号」:排序决定每一次请求用谁,失败时依次用池内其他可用账号重试,直到有人接手或全部不可用。**
|
|
75
|
+
- **触发与顺序**:某个请求失败 → 按池子的排序依次取下一个**可用**成员重试,全部失败才把错误报出来,并在错误里注明 `(after trying N accounts)`(不注明的话,「换过号仍然失败」与「你选的账号失败了」在界面上长得一模一样)。
|
|
76
|
+
- **哪些失败会换**:429 限流、402 无积分、401 凭据失效、502 网关/网络——这些都是**某个账号**的状态,换一个很可能就过了。**请求本身不合法(HTTP 400)不换**:同一个请求体换哪个账号都是同样的 400,走完整池只是让用户多等 N 轮才看到同一个错误。**流中途断开也不换**——字节已经发出去了,重放会让客户端看到两段拼接的回答(这条写在 `src/shim.ts` 的重试循环里,是它只覆盖 `!result.ok` 的原因)。
|
|
77
|
+
- **排序决定「谁服务」,不只是「换给谁」**:开启账号池后,**每一次请求**都由池子的排序选出服务账号(可用优先 → 积分高 → 快过期 → 凭据新 → 账号 id)。所以卡片上的「当前使用」是排序的结果,不一定是用户手动选的那个——这也正是「在测试之后有一个智能排序」的意思。
|
|
78
|
+
- 实现上是**每请求一次**选择:shim 在 `resolve()` **之前**先问宿主(`prepareAccount`),宿主用**纯本地数据**排序,把胜者写进 store 的运行时覆盖(`setRotatedAccount`),然后照旧走 `resolve()`。这样 token 刷新仍然只有一处,不会在路由路径上被改写成「发一个过期 token」。
|
|
79
|
+
- **请求路径绝不抓积分**:积分是排序的第二键,但每个成员一次上游调用。所以请求路径只用**新鲜读数**(卡片轮询 / 批量测试 / 保存时读到的那份,10 分钟内有效;由 `poolMembersOf` 顺手写进快照),过期就**跳过这一键**,而不是为了排序去打上游——否则一页对话会变成 N 个额外请求。
|
|
80
|
+
- **从不写回用户的选择**:覆盖只存在于运行时(从不落盘),关掉账号池立即清空、用户选定的账号完全恢复生效。卡片因此必须显示「当前使用」。而账号选择器**只在池关闭时**渲染:池开着时由排序决定,那时候摆一个改了不生效的下拉就是在骗人。
|
|
81
|
+
- **候选走 `credentialFor()` 而不是 `resolve()`**:换号与路由都不改选择状态,只是在运行时覆盖上取凭据。
|
|
82
|
+
|
|
83
|
+
- **跳过已知不可用的成员,但「没测过」算可用**:仍在冷却期内的限流账号、凭据被上游拒绝的账号都不参与(免得花一次往返去重新学到测量结果已经写着的事);**从没测试过的账号默认视为可用**——刚发现的登录不该因为「还没测」就被排除在外。这正是「一键测试」的意义所在:测试结果直接决定谁能被选中。
|
|
84
|
+
- **循环有刹车**:`triedAccountIds` 是唯一的刹车,因此 shim **自己**也会拒绝一个「已经试过的」候选(不只依赖策略层的自觉)——否则一个策略缺陷会变成无上限的重复发送,真实消耗额度。客户端断开后停止重试(`AbortSignal`)。
|
|
85
|
+
- **修掉一个一直存在的缺陷**:`req.on('close')` 无法表达「客户端挂了」——Node 在**请求体读完**时就触发它,所以在 `readBody` 之后才挂的监听**永远不触发**(实测:真实断开后 signal 仍是 un-aborted),而挂在 `readBody` **之前**则会立刻中止每一个请求。改用 `res.on('close')`:正常完成时它发生在 `res.end` 之后(无影响),中途断开时才触发(于是上游调用被真正取消)。这同时让上面那条「断开即停止重试」从「写了但不可能生效」变成真的生效。
|
|
86
|
+
|
|
87
|
+
- **删除「按积分自动轮换」与「自动测试间隔」两个设置项,以及它们背后的机制。**
|
|
88
|
+
- 轮换(按「可用性 → 积分 → 可用时间」挑计费账号)与它的运行时覆盖(`setRotatedAccount`/`rotatedAccount`/`rotationGuard`/`createLatestWins` 用法)、自动测试定时器(`setInterval` + `duePoolRegions` + `runScheduledPoolTest` + `poolLastRunMs`)整体移除。设置 schema 少两个字段:读旧配置时它们被静默忽略(zod 的默认 strip 行为),不需要迁移。
|
|
89
|
+
- **为什么不留着作为可选项**:两者与「换号」是同一件事的两种相反做法——轮换是**持久**改选、换号是**按请求**借用。同时存在会让人无法回答「现在到底谁在计费」,而卡片只能显示一个答案。用户裁定:要换号,不要轮换。
|
|
90
|
+
- 顺带清掉随之成为死代码的客户端部分:`rotationLockState`、`rotatedToAccountId` 观测字段与那段「宿主侧轮换记录」的日志逻辑、`intervalEditOnInput`/`parseIntervalInput`/`POOL_INTERVAL_MIN|MAX`、以及两语言里对应的文案键。
|
|
91
|
+
|
|
92
|
+
- **海外区域不再提供签到**(`checkinSupported` 能力位)。
|
|
93
|
+
- 国内版 App 每天有签到奖励,国际区域没有对应的东西——所以海外版的卡片**不渲染**「一键签到所有账号」按钮,也不再渲染每行的「签到」列(空列会暗示「有这个状态、只是没读到」)。
|
|
94
|
+
- 后端同样拒绝该动作(`409 checkin-unsupported`),而不是只把按钮藏起来:否则会留下一个「界面看不见、接口却还能打」的死角。判定放在宿主(`region === 'cn'`)而不是浏览器比较区域名——策略属于宿主,卡片不该知道哪个区域碰巧有这个功能。
|
|
95
|
+
|
|
96
|
+
- **卡片上「单账号时代」的装饰全部退场:账号选择下拉、已登录状态卡、积分面板都不再渲染。**
|
|
97
|
+
- **账号选择下拉**:有了账号池之后它是**自相矛盾**的——池开启时由排序决定谁服务,它改什么都不会生效,却看起来像权威;而它还可能和别处显示的账号不一致(真机截图里就出现过)。
|
|
98
|
+
- **已登录状态卡(账号名 + token 过期)**:多个账号同时登录是常态,用单数说「已登录:A」本身就不成立;而 token 会自动续期,那个到期时间**不是用户能据以行动的东西**。真正出问题时,凭据失效面板会带着补救动作(「切换到可用账号」)说出来。
|
|
99
|
+
- **积分面板与它的单账号签到**:积分已经在账号池的成员表里**逐账号**显示(还带最近到期时间),并且是排序的第二键;而那个签到按钮只对当前账号生效,还**绕过了账号池的勾选**——「本机有登录 ≠ 要拿它去签到和花积分」正是这个池子要守护的同意。
|
|
100
|
+
- **账号选择并没有失去入口**:它搬进了账号池区块,且**只在池关闭时渲染**——那才是「手动模式」,也正是那个下拉唯一说得通的状态(见下方 Bug Fixes)。
|
|
101
|
+
- 随之删除的四个文案键与只服务于这些元素的样式规则一并清理;「切换账号」的代码路径保留(凭据失效时的「切换到可用账号」仍在用它)。
|
|
102
|
+
|
|
103
|
+
- **模型管理移到卡片最后。**
|
|
104
|
+
- 顺序改为:区域标签 → **账号池** → **模型管理**。账号池是日常要碰的东西,不该被一个可折叠的长列表压在下面;模型列表是一次性配置,放最后更顺。(原先夹在中间的积分面板已整体移除,理由见上。)
|
|
105
|
+
|
|
106
|
+
- **「重新检测账号」移到账号池区块里。**
|
|
107
|
+
- 理由:账号池的成员表**就是**现在管理账号的地方,新登录的账号会以「一行待勾选」的形式出现在那里,所以「再看一眼本机登录」属于它旁边,而不是卡片顶部的状态栏。原来它和状态栏里那句「已登录:X」放一起,还会造成一种误导——有账号池之后**多个账号同时登录**是常态,用单数说「已登录」并不准确。
|
|
108
|
+
- **但未登录状态必须保留一个副本**:账号池区块只在「有账号的区域」渲染,而「我刚在桌面端登录了,你再看看」恰恰是**未登录时**才需要的动作。少了这个副本,那条路就没有入口了。守卫里为此专门有一条用例。
|
|
109
|
+
|
|
110
|
+
### Bug Fixes
|
|
111
|
+
|
|
112
|
+
- **修正:账号池的开关被错误地同时管住了「手动动作」。**
|
|
113
|
+
- 现象:关掉账号池之后,卡片的「一键签到 / 一键测试」都置灰了,而 Host 也会以 `409 pool-disabled` 拒绝同一个请求——于是「关闭」等于「再也没法签到或测试」。
|
|
114
|
+
- 根因:把「插件是否自动路由」与「用户是否可以操作」当成了一个条件。它们是两件事。
|
|
115
|
+
- 修法:开关只保留它在路由上的作用(排序选谁、失败换谁);签到、测试、目标模型选择与勾选在任何状态下都可用。`409 pool-disabled` 这条原因连同它的文案键一并删除。
|
|
116
|
+
- **唯一保留的硬约束**:池里没勾选任何成员时批量动作一律拒绝——空的池绝不退化成「那就对全部执行」。
|
|
117
|
+
|
|
118
|
+
- **修正:关闭账号池后没有任何地方可以选择要用的账号。**
|
|
119
|
+
- 现象:把账号池关掉之后,想换一个服务账号却找不到入口——卡片头部的账号下拉已被移除,而账号池区块也没有提供选择。
|
|
120
|
+
- 根因:删掉那个下拉时只论证了「池开启时它改了不生效」,漏了另一半——**池关闭时它是唯一的选择入口**。于是「关闭」变成了「卡在之前选定的那个账号上」。
|
|
121
|
+
- 修法:把账号选择放进账号池区块,**只在池关闭时渲染**(那就是手动模式),选项来自本区域已检测到的账号,当前生效的那个为选中值;写入走与「切换到可用账号」同一条已验证路径。**池开启时该行不渲染**——那时由排序决定,一个改了不生效的下拉比没有更糟。
|
|
122
|
+
- 顺带给它一个专属 class(`dsm-workbuddy-pool-account-select`):它与「目标测试模型」共用 `pool-select`,导致按类名查询会命中先渲染的那个——这个坑在测试里当场就被踩到了。
|
|
123
|
+
|
|
124
|
+
- **修正:线上碰到的 429 没有留下任何测量,于是下一个请求又会先撞同一个账号。**
|
|
125
|
+
- 现象:某个账号刚被限流,但下一个请求仍然先发给它、撞一次失败、再换号——每个请求都白付一次往返。虽然结果仍然是成功(换号救回来了),但「发现有可用的就继续用它」只对**当前这一次请求**成立。
|
|
126
|
+
- 根因:测量只在**手动「一键测试」**时写盘;聊天路径碰到失败时什么都不记。而排序读的正是这些已存测量,所以一个刚被限流的账号在排序里仍然是「未测试 → 候选」。
|
|
127
|
+
- 修法:失败发生时把它记成测量——新增 `onAccountFailure`(shim → 宿主)与 `recordAccountFailure`(宿主侧落盘),用 `outcomeOfFailure` 归类、用 `cooldownOf` 从上游报文里解析它**自己给出的重置时间**(「将在 … 重置」)。于是该账号在冷却期内被排序排除,下一个请求直接走可用账号。
|
|
128
|
+
- **只对每个失败尝试上报一次,包含最后一次**:没有任何后继可试的那个账号,恰恰是下一个请求最该避开的——只上报「有后继」的那些会漏掉最要紧的一例。
|
|
129
|
+
- **两类刻意不记**:`client`(HTTP 400)——上游拒绝的是**请求本身**,与账号无关,记了会因为一个坏 body 而闲置一个好账号;以及**未被勾选进池**的账号——勾选才是「可以花这个账号的积分」的显式同意,而测量会影响路由。池关闭时也不记:那时没有任何路由在读它。
|
|
130
|
+
- **上报不阻塞重试**:写盘只影响后续请求,而用户正等着这次重试;回调抛错也只记一行 warn,不影响请求本身。
|
|
131
|
+
|
|
132
|
+
- **修正:所有成员都被限流时,卡片仍把其中一个报成「当前使用」。**
|
|
133
|
+
- 现象:池里每个账号都在冷却期内时,卡片上方写「当前使用:A」,而正下方的成员表同时写着「A 被限流」——同一屏自相矛盾,而「池子换到了可用账号」这件事也就无法被验证。
|
|
134
|
+
- 根因:请求与显示混用了同一个答案。没人可用时 `applyPoolSelection` 会清空运行时覆盖(好让**请求**落到你保存的那个账号上),而卡片读的也是 `store.current()`,于是把那个「兜底账号」当成了在服务的账号。
|
|
135
|
+
- 修法:把两个问题分开。请求该去哪就去哪(兜底不变,失败照实报),但**显示**在「当前账号已被测量为不可用」时报「没有可用的账号」——卡片本来就有这个状态,而且那才是事实。不是池成员的账号不算在内:那时池没有意见,就不该替它表态。
|
|
136
|
+
|
|
137
|
+
- **修正:DSML 检测器只认双竖线,对真实泄漏报「一切正常」。**(仓库内诊断工具;**不影响插件行为,也不随 npm 包发布**——`src/markup-diagnosis.ts` 无人 import,不进 `lib/`)
|
|
138
|
+
- 现象:`|DSML|` 的**竖线个数是写法变量,不是 token 的一部分**。一次真实捕获的正文块里
|
|
139
|
+
五个标记混用三种写法——`<|DSML| validate>`、`<|DSML| parameter name="spec">`、
|
|
140
|
+
`</|DSML| invoke>` 都是单竖线,只有收尾 `</||DSML|| calls>` 是双竖线。
|
|
141
|
+
只按双竖线匹配,会对一个真的泄漏了四次的会话给出零命中——而「检查永不命中」看起来与
|
|
142
|
+
「检查通过」一模一样。
|
|
143
|
+
- 修法:新增 `BARS`(1–2 个全角竖线)与 `MARKUP_RE`,定位与计数一律改按它匹配
|
|
144
|
+
(`MARKUP_TOKEN` 保留为双竖线的参考写法);`CALL_ATTEMPT` 除 `invoke name="…"` 外
|
|
145
|
+
再接受 `parameter name=` 子句——那次泄漏的开头被写坏成 `<|DSML| validate>`(工具名占了
|
|
146
|
+
`invoke name="…"` 的位置),但每个参数子句都完整。
|
|
147
|
+
- 配套:「在行内反引号里引用标记」也计为 `mention`。参数子句既可能是调用、也可能是在**解释**
|
|
148
|
+
语法,而解释性引用写在反引号里;不区分就会把解释当成事发,数字随即失去意义。
|
|
149
|
+
- 效果(同一份会话记录):判定为真实泄漏 **6 次 / 2 会话 → 12 次 / 4 会话**;另单独计出 13 次
|
|
150
|
+
「讨论/引用」不计入。`scripts/verify-dsml.mjs` 的抬头同步改正——它此前把「双竖线常量」
|
|
151
|
+
印成「检测的 token」,与实际匹配口径不符。
|
|
152
|
+
- 外部依据与上游各层修法见 `docs/DSML-LEAK-RESEARCH.md`(含 vLLM 那些**关闭未合并**的
|
|
153
|
+
尝试,以及急切恢复炸出的三类生产事故)。
|
|
154
|
+
|
|
155
|
+
- **账号池的「保存 / 放弃修改」行动作行改为与模型管理一致:按钮靠右,左侧放「鼓励一下」。**
|
|
156
|
+
- 同一张卡片上两处「提交你的修改」行为长得不一样——模型管理那行是左侧鼓励链接(★)、右侧按钮,
|
|
157
|
+
账号池这行却是按钮在左、提示在右。同类动作行不该教出两套肌肉记忆。
|
|
158
|
+
- 现在两行同构:左侧鼓励链接、右侧按钮组(放弃修改 → 保存,次要动作在前,与模型行同序);
|
|
159
|
+
样式也对齐(相同的上边框与间距,`justify-content: space-between` 把按钮顶到右侧)。
|
|
160
|
+
- 该行原有的状态提示(「修改任一设置后即可保存」/「有未保存的修改」)**整句删除**,两行因此完全等形。
|
|
161
|
+
它本来就是多余的:**保存按钮自身的可用状态**已经区分了这两种情况;而一句只在 dirty 时才出现的文字,会让这行在每次编辑时抖动一下。
|
|
162
|
+
- `WORKBUDDY_GITHUB_URL` 提到 `src/status-paths.ts`:现在两个区块都链接到它,复制一份 URL 就是多一处会忘记同步的地方。
|
|
163
|
+
|
|
164
|
+
- **账号池顶部重新设计:删掉两处重复的说明,按钮收成一行两簇。**
|
|
165
|
+
- **副标题整行删除**(「本区域 N 个账号 · 目标模型 X」):账号数就是成员表的行数,目标模型就是下面
|
|
166
|
+
那个控件的当前值。首个控件之前先读两行说明,成本不小。
|
|
167
|
+
- **徽章旁那 4 行说明整行删除**:它讲的「失败会借用另一个成员重试」与「启用账号池」自己的说明重叠,
|
|
168
|
+
而且池关闭时那件事根本不会发生。
|
|
169
|
+
- **目标模型的来源移进「目标测试模型」这一行**(`当前:deepseek-v4.1-flash(你指定)`),
|
|
170
|
+
而不是像原来那样在副标题与底部各说一遍——信息没丢,但只剩一处。
|
|
171
|
+
- **按钮收成一行两簇**:左簇是「对勾选的账号做的事」(一键签到、一键测试),右簇是「重新发现账号」
|
|
172
|
+
(重新检测账号)。此前重新检测独占一行靠右、签到/测试在下一行靠左,两行两种对齐却没有理由。
|
|
173
|
+
- 随之删除:2 个文案键(`poolSummary`/`poolCurrentHint`,两语言共 4 行)、2 条只服务于它们的样式规则
|
|
174
|
+
与 1 条已无使用者的 `pool-current` 容器规则;新增 2 个来源文案键。
|
|
175
|
+
- 设计过程的形态校对稿留在 `docs/pool-header-redesign.html`(不进发布包)。
|
|
176
|
+
- **模型区不再可折叠:去掉「点击收起」与那个折叠头。**
|
|
177
|
+
- 原来它是原生 `<details>`:表头可点、带方向指示箭头、中间一行「点击收起 / 点击展开」。
|
|
178
|
+
- 去掉的理由:模型列表正是用户来看的东西,折叠它只会把想看的内容藏起来;而且这让模型区成了卡片上
|
|
179
|
+
**唯一**一个「点一下就能藏起来」的区块,读起来像另一种性质的区块,而不是同一张卡片里的一段。
|
|
180
|
+
- 表头与列表照常渲染,表头仍写明「已启用 N 个」;**未保存标记仍留在表头**(这一条比折叠时代更重要——
|
|
181
|
+
列表长的时候,用户不必滚到底部的按钮才知道有改动没保存)。
|
|
182
|
+
- 随之删除:3 个文案键(`modelsFoldHint`/`modelsFoldShow`/`modelsFoldHide`,两语言共 6 行)、
|
|
183
|
+
2 条样式规则、以及表头为 `<details>`/`<summary>` 而写的原生三角抑制、焦点环与手型光标。
|
|
184
|
+
- 「探测过哪些路径」那个折叠列表**不受影响**——它仍是 `<details>`,那是另一件事。
|
|
185
|
+
### Docs
|
|
186
|
+
|
|
187
|
+
- README 双语与 `docs/ACCOUNT_POOL_PLAN.md` 同步改版说明;`docs/assets/account-pool-demo.html` 演示原型同步(去掉两个开关、改写启用文案、海外版不显示签到)。
|
|
188
|
+
|
|
3
189
|
## 3.1.0 (2026-10-01)
|
|
4
190
|
|
|
5
191
|
### Bug Fixes
|
package/README.en.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">dsh-connect-workbuddy</h1>
|
|
6
6
|
|
|
7
|
-
<p align="center"><b>Connect locally signed-in WorkBuddy models to DeepSeek Harness, with
|
|
7
|
+
<p align="center"><b>Connect locally signed-in WorkBuddy models to DeepSeek Harness, with an account pool and model management.</b></p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://www.npmjs.com/package/dsh-connect-workbuddy"><img src="https://img.shields.io/npm/v/dsh-connect-workbuddy?style=flat-square&label=npm&color=cb3837" alt="npm version"></a>
|
|
@@ -17,21 +17,21 @@
|
|
|
17
17
|
|
|
18
18
|
[English](README.en.md) | 中文
|
|
19
19
|
|
|
20
|
-
A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle plugin that connects locally signed-in WorkBuddy models to the DSH model picker — **both the CN app and the international WorkBuddy AI app are supported** — with
|
|
20
|
+
A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle plugin that connects locally signed-in WorkBuddy models to the DSH model picker — **both the CN app and the international WorkBuddy AI app are supported** — with an account pool and selectable model management. **The CN and international sides are two parallel providers (`workbuddy` / `workbuddy-global`) that can be used at the same time**; the settings card separates them with tabs for convenient management.
|
|
21
21
|
|
|
22
22
|
## Features
|
|
23
23
|
|
|
24
|
-
- **Dual parallel providers (CN + international)** — the CN side registers as the `workbuddy` provider (`GLM-5.3`, `DeepSeek-V4-Pro`, `Kimi-K3`, `MiniMax-M3`, `Hy3`, etc.), the international side as `workbuddy-global` (`GPT-5.6`, `Gemini-3.5-Flash`, `GLM-5.3`, `Kimi-K3`, etc.). **A region's models appear in the DSH model picker when that region has an account**: with accounts on both sides, both rosters are selectable at once, and different sessions can each pick a side without interfering. **A region with no local sign-in lists no models** — it cannot serve a single request, so listing them would offer nothing but failures. Sign in once for that region and its models come back on "detect accounts again" or after a restart; the provider itself stays registered either way, because its settings card
|
|
25
|
-
- **Tabbed settings card** — the card's top carries a "Domestic / Global" tab bar; each tab holds its own account
|
|
24
|
+
- **Dual parallel providers (CN + international)** — the CN side registers as the `workbuddy` provider (`GLM-5.3`, `DeepSeek-V4-Pro`, `Kimi-K3`, `MiniMax-M3`, `Hy3`, etc.), the international side as `workbuddy-global` (`GPT-5.6`, `Gemini-3.5-Flash`, `GLM-5.3`, `Kimi-K3`, etc.). **A region's models appear in the DSH model picker when that region has an account**: with accounts on both sides, both rosters are selectable at once, and different sessions can each pick a side without interfering. **A region with no local sign-in lists no models** — it cannot serve a single request, so listing them would offer nothing but failures. Sign in once for that region and its models come back on "detect accounts again" or after a restart; the provider itself stays registered either way, because its settings card is how you sign in (re-detection lives in the pool block). Model names carry the upstream credit multiplier (e.g. `GLM-5.3 · x0.79`), matching WorkBuddy's own model menu.
|
|
25
|
+
- **Tabbed settings card** — the card's top carries a "Domestic / Global" tab bar; each tab holds its own account pool and model management. Account, directory, selection, and unsaved drafts are fully isolated per tab — switching accounts or refreshing models on one tab never touches the other side's runtime catalog or sessions.
|
|
26
26
|
- **Switch either provider off on its own** — each tab carries an on/off checkbox (both checked by default). Unchecking one **withdraws that provider from DSH's model picker entirely** (it is not merely a hidden tab): its adapter route and its "Settings → Models" directory entry are both pulled, and startup no longer sends pointless requests for it. Account, credits, directory, and selection are all kept, and re-checking restores everything. The typical use is "I have no use for the international side" — switch it off and keep the picker clean. Note: a session that had already selected a model from the switched-off provider will report `NO_ADAPTER` on its next call; the card states this plainly on the switched-off tab instead of failing silently.
|
|
27
27
|
- **Model management** — refresh the full catalog from upstream and enable or disable each model individually; refresh is a draft operation that only takes effect on save. Upstream also reports credit multiplier, context/output limits, and reasoning efforts; **image input is decided by a vendor-verified table**: a refresh pre-checks only models the vendor's own documentation calls natively multimodal, leaves documented text-only models unchecked, and treats anything without a vendor source as unverified and unchecked rather than guessing. Upstream's `supportsImages` is a *platform* declaration, not the model's capability, so it does not drive that decision at all. You can still tick any box by hand, and your selection is kept locally.
|
|
28
28
|
- **Model availability testing, including cooldown recovery times** — working out when a limited model frees up used to mean guessing through repeated failures. Now every model row has a **Test** button that gives you a direct answer for whether the model works right now and, if not, when it comes back. **It sends a real-volume request (about 25k input tokens)**, rather than "ping it and see if it answers" — because the upstream throttles on request **SIZE** (measured: about 20k tokens pass, about 30k are refused), so a small request looks fine while every request in a long conversation may be refused. That is exactly where "the test passes but I am limited" came from. **Where the recovery time comes from**: the upstream's 429 carries **no** `Retry-After` header (and no `X-RateLimit-*`), but it writes the reset time into the response **body** — `{"code":6004,"msg":"…将在 2026-09-30 02:30:30 UTC+8 重置…"}`. The plugin parses that time and converts it using the UTC offset the message states, so the result does not depend on your machine's timezone; the card then shows “✗ Rate limited · usable again after 2026/9/30 02:30:30”. When none of the sources states a time, the plugin **never invents a countdown** and says plainly that no time was given. A rate limit deliberately does **not** borrow the monthly refresh time, which would tell you to wait weeks for a limit that clears in seconds. Probes run in the Host, so **the credential never reaches the page**, and results are not written to settings — your saved selection is untouched.
|
|
29
29
|
|
|
30
30
|
**Two things to know.** ① **It really does spend credits** — one measured probe reported `credit` 0.02 (a refused one 429s and costs almost nothing). So testing is **manual and per-model only**: there is deliberately no “Test selected” batch button, because sweeping the whole list from one click is an uncontrolled spend — and the Host enforces the same rule, **accepting a single model only** and refusing multi-model requests. ② The upstream advertises a 1M context yet throttles far below it, and the plugin advertises up to 200K to DSH, so DSH compacts very late — hitting the throttle in a long session follows directly from that.
|
|
31
31
|
- **Local account switching** — discovers the multiple sign-in credentials WorkBuddy's desktop app leaves behind and lets you switch per region. Tokens are never written to DSH settings.
|
|
32
|
-
- **Account pool: batch actions across several accounts, plus
|
|
32
|
+
- **Account pool: batch actions across several accounts, plus automatic failover** — tick the accounts you want into a pool and you get **one-click check-in for every account**, **one-click testing of every account against a model you choose** (defaulting to the region's zero-multiplier free model), and with **Enable the account pool** switched on, **automatic failover**: a request that fails upstream is retried against the pool's other usable accounts, in order, until one serves it or none is left. Membership starts **empty and is an explicit opt-in**: "signed in on this machine" is not the same as "spend this account's credits and claim its rewards". The pool's **ranking decides who serves every request** (usable first, then higher credits, soonest-expiring, freshest credential), and on a failure the next usable member is tried. It **never writes back your saved selection** — switch the pool off and your own account applies again immediately. **The switch only decides whether the PLUGIN chooses** — with it off, the plugin stops picking and switching accounts for you, but check-in and testing stay available (they are manual actions). Testing reuses the model rows' real-volume probe, so it **does cost real credits**; **automatic check-in is deliberately not offered** — a check-in claims real rewards, so it only ever runs when you press the button. See [Account pool](#account-pool).
|
|
33
33
|
- **Actionable advice when a credential is refused** — when the upstream rejects the selected account's token, the plugin actually **probes** the other local sign-ins: if one still answers, it tells you to switch to it (with a one-click switch) instead of vaguely asking you to sign in again — re-authenticating fixes nothing while you are pinned to a revoked backup. Only when there is genuinely no other local account does it ask you to sign in again.
|
|
34
|
-
- **
|
|
34
|
+
- **Credits live in the account pool** — the member table shows each account's remaining credits and nearest expiry, and credits are the ranking's second key (more credits serves first). The queries are **read-only** and consume no credits.
|
|
35
35
|
- **Multi-candidate credential paths** — probes the platform defaults for macOS / Windows / Linux in turn, overridable by environment variable or directly in the card. **When nothing is found, the card lists which paths were probed and why each one failed**: five distinct reasons (absent / unreadable / no usable token / encrypted with no key available / belongs to the other region), so the easiest case to misdiagnose — signed in, but the desktop app is not present — says the app must be there instead of telling you to sign in again. The **belongs to the other region** reason is different in kind: that sign-in is real and usable, it is simply filed under the other tab, so the card states the fix outright — switch tabs, no need to sign in again. The list says it once: the paragraph states the conclusion, the collapsed list supplies the per-path detail, and the two never repeat each other.
|
|
36
36
|
- **Secure loopback shim** — one random port + in-process random secret per region; the real WorkBuddy token is never handed to pi-ai.
|
|
37
37
|
- **Command-line diagnostics** — `status` / `doctor` / `logout`, reporting sign-in and credits per region, without a browser.
|
|
@@ -59,12 +59,12 @@ Credentials are read (read-only) from the WorkBuddy desktop app's own auth file.
|
|
|
59
59
|
## Account pool
|
|
60
60
|
|
|
61
61
|
<p align="center">
|
|
62
|
-
<img src="docs/assets/dsh-connect-workbuddy-account-pool.png" width="900" alt="dsh-connect-workbuddy account pool: check-in all, test all, membership selection and
|
|
62
|
+
<img src="docs/assets/dsh-connect-workbuddy-account-pool.png" width="900" alt="dsh-connect-workbuddy account pool: check-in all, test all, membership selection and automatic failover" />
|
|
63
63
|
</p>
|
|
64
64
|
|
|
65
|
-
A machine usually carries more than one WorkBuddy sign-in. The **account pool** turns that into a single click: tick the accounts you want into the pool, then press **Check in all accounts** or **Test all accounts**;
|
|
65
|
+
A machine usually carries more than one WorkBuddy sign-in. The **account pool** turns that into a single click: tick the accounts you want into the pool, then press **Check in all accounts** or **Test all accounts**; switch on **Enable the account pool** and the plugin will carry on through a failure by itself.
|
|
66
66
|
|
|
67
|
-
**Each region (domestic / international) has its own independent pool** — membership
|
|
67
|
+
**Each region (domestic / international) has its own independent pool** — membership and target model never affect the other side, because the two sides' accounts belong to different upstream stacks.
|
|
68
68
|
|
|
69
69
|
### The two batch actions
|
|
70
70
|
|
|
@@ -81,25 +81,47 @@ A machine usually carries more than one WorkBuddy sign-in. The **account pool**
|
|
|
81
81
|
|
|
82
82
|
Ticking boxes is a **draft edit**: it lands on **Save**, the save button carries a dirty marker until then, and the draft survives switching tabs or closing the card.
|
|
83
83
|
|
|
84
|
-
###
|
|
84
|
+
### Automatic routing and failover: what "Enable the account pool" turns on
|
|
85
85
|
|
|
86
|
-
This plugin's standing rule is that it **never
|
|
86
|
+
This plugin's standing rule is that it **never rewrites who pays** — your saved choice is yours. What the switch changes is **who serves**, decided by the pool's ranking, request by request:
|
|
87
87
|
|
|
88
|
-
1.
|
|
89
|
-
2. **
|
|
90
|
-
3. **
|
|
88
|
+
1. **The ranking picks the serving account for every request** — so the card's "in use" account is the ranking's answer, which is not necessarily the one you picked above it. The ranking is below.
|
|
89
|
+
2. **On a failure the next usable member is tried**, then the next, until one serves it or none is left. Only then is the error reported — annotated with how many accounts were tried, so a pool that failed over and still lost does not read like a single account failing.
|
|
90
|
+
3. **Your saved selection is never rewritten.** Turning the pool off restores it immediately and completely (the pool steers through a runtime override, never through the settings). That is also why the card states which account is actually in use: it may be a different one, and you should be able to see that.
|
|
91
|
+
4. **Which failures trigger a retry:** 429 rate limits, 402 exhausted credits, 401 dead sessions, 502 gateway/network errors — all of these describe **one account's** state, and another may well survive them.
|
|
92
|
+
5. **Which do not:** a malformed request (HTTP 400). Every account answers the same 400, so walking the pool would only multiply the wait before the user sees an error they must act on anyway. A stream that dies **mid-flight** is not retried either — bytes are already on the wire, and replaying would splice two answers together.
|
|
93
|
+
6. **Members already known to be unusable are skipped** — a rate-limited account still inside its stated cooldown, or one whose credential the upstream rejected. **An account that was never tested counts as usable**: a freshly discovered sign-in must not be invisible just because nobody has measured it yet.
|
|
94
|
+
7. **A live failure is RECORDED as a measurement** (with the reset time the upstream stated), so the NEXT request starts from an available account instead of re-hitting the account that just refused it. Before this, only the manual batch test wrote measurements — so a just-limited account still read as untested and kept being picked first. HTTP 400 is the exception: that is the REQUEST being rejected, which says nothing about the account, and recording it would sideline a good one. Accounts not checked into the pool are not measured either, since the tick is what grants "you may spend this account's credits".
|
|
95
|
+
7. Every retry leaves a `warn` line in the host log naming the failure and the account that follows.
|
|
91
96
|
|
|
92
|
-
|
|
97
|
+
### What the switch actually controls
|
|
93
98
|
|
|
94
|
-
|
|
99
|
+
**It controls whether the PLUGIN decides by itself — not whether YOU may act.**
|
|
95
100
|
|
|
96
|
-
|
|
101
|
+
| | Pool **on** | Pool **off** |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| Who serves | Chosen **automatically** by the ranking (usable → credits → soonest-expiring → freshest credential) | The account you selected; the plugin does not choose |
|
|
104
|
+
| On a failure | **Automatically** retried on the next usable account | Reported as-is, no retry |
|
|
105
|
+
| Check-in all / test all | Available | **Still available** |
|
|
106
|
+
| Target test model | Editable | **Still editable** |
|
|
97
107
|
|
|
98
|
-
|
|
108
|
+
The last two rows are deliberate: check-in and testing are MANUAL actions over the members you ticked, which is a different question from "does the plugin route automatically". Gating them on the switch meant switching the pool off ALSO removed the ability to check in or test — while "off" should mean "I'll do it by hand", not "nothing is allowed".
|
|
109
|
+
|
|
110
|
+
**The one hard rule**: with nothing checked in the pool, batch actions are refused. An empty pool **never** degrades into "then run on every account" — that is the one reading which would spend credits you never authorized.
|
|
111
|
+
|
|
112
|
+
**Where manual selection lives**: in the account pool section. **With the pool off** that section shows an "Account in use" dropdown; picking one takes effect immediately (it writes the region's account slot through the same verified path the credential-recovery "switch to a usable account" action uses). **With the pool on the row is absent** — the ranking decides there, and a dropdown that changes nothing is worse than no dropdown. Account discovery ("detect accounts again"), membership, check-in and testing all sit in that one section, so accounts are managed in exactly one place.
|
|
113
|
+
|
|
114
|
+
### The ranking (what decides who serves)
|
|
115
|
+
|
|
116
|
+
1. **Usable first** — an account a measurement rules out (limited with its cooldown unexpired, credential rejected, upstream unreachable) sorts last and is not chosen;
|
|
117
|
+
2. **Higher credit balance first.** Credits take part **only when a fresh reading exists** (what the card's poll, a batch test or a save collected; valid for 10 minutes). The request path **never** fetches credits — that is an upstream call per member, and one page of chat would turn into N extra requests;
|
|
118
|
+
3. **Soonest-expiring credits first** — spend what lapses in three days before it is lost;
|
|
119
|
+
4. **Freshest credential first**;
|
|
120
|
+
5. The account id closes the order, making it **total** and reproducible rather than jittering with the roster's order.
|
|
99
121
|
|
|
100
122
|
### Why an account leaves the pool, and when it comes back
|
|
101
123
|
|
|
102
|
-
| Probe outcome | What it means for
|
|
124
|
+
| Probe outcome | What it means for failover |
|
|
103
125
|
| --- | --- |
|
|
104
126
|
| Usable | Eligible |
|
|
105
127
|
| Rate limited | Out of the pool **temporarily**, returning on the reset time the upstream stated (and when it states none, the card says so rather than **inventing a countdown**) |
|
|
@@ -108,11 +130,17 @@ A timer ticks every 60 seconds and runs a "test all accounts" pass on your inter
|
|
|
108
130
|
|
|
109
131
|
The key line: **"rate limited" is not "unusable"**. It is precisely the state most worth waiting on — treating it as permanently dead throws away the accounts that would have served you best.
|
|
110
132
|
|
|
133
|
+
### Manual selection: there is no account dropdown any more
|
|
134
|
+
|
|
135
|
+
Accounts are managed **in the account pool**: membership checkboxes are the selection surface, and re-detection lives there too. The old "account" dropdown had become self-contradictory once the pool existed — with the pool on, the ranking decides who serves, so changing it did nothing yet looked authoritative, and the status line above could name one account while the dropdown showed another.
|
|
136
|
+
|
|
137
|
+
The region status line changed with it: **while the pool is on it does not name a single account** (several signed in at once is the normal case, so "Signed in: A" was simply the wrong claim) — it states that the region is usable and that the pool picks. **While the pool is off it does name the account**, because then it really is the only answer. The dot and the token expiry beside it are about "can this region be used", which is a different question from "who is being billed" — the pool block answers that one.
|
|
138
|
+
|
|
111
139
|
### Saving and drafts: why the card has two Save buttons
|
|
112
140
|
|
|
113
141
|
One for model management, one for the pool — **deliberately kept apart** rather than merged: the two draft domains differ, and merging them means one failed write drags the other down (a catalog that will not save would stop you saving pool preferences too); a shared Discard would also throw away both drafts on a single misclick. Both sides use a **verified write** — **a write that did not land never discards the draft**, or your edits are lost for good.
|
|
114
142
|
|
|
115
|
-
One class of state in the pool **does not go through a draft**: probe results, cooldowns, and the account
|
|
143
|
+
One class of state in the pool **does not go through a draft**: probe results, cooldowns, and the account that served a request. Those are **observations**, written frequently by the plugin itself. The reason is practical — drafts are overwrite-based, so if observations lived in one, a single manual **Save** could roll the result a timer had just written **back to a several-minute-old value**.
|
|
116
144
|
|
|
117
145
|
## Install
|
|
118
146
|
|
|
@@ -240,6 +268,7 @@ This project builds on work others have published. Sources and licenses are cred
|
|
|
240
268
|
|
|
241
269
|
- [corrinehu/dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect) (MIT, Copyright (c) 2026 Corrine Hu) — **the principal reference for this project.** That project first validated a working WorkBuddy-to-DSH integration: desktop credential discovery and refresh, the upstream protocol and header conventions, loopback shim hardening, pi-ai provider assembly, and the status CLI were all studied from it. This project reimplements those proven capabilities while focusing on improving the experience.
|
|
242
270
|
- [Sliverkiss/workbuddy2api](https://github.com/Sliverkiss/workbuddy2api) (MIT) — reference implementation of the WorkBuddy upstream protocol (the `copilot.tencent.com` wire behavior), referenced via `dsh-workbuddy-connect`.
|
|
271
|
+
- [hawklithm/workbuddy2api](https://github.com/hawklithm/workbuddy2api) (MIT, Copyright (c) 2026 Mayer) — **the only entry in this list that is a CODE PORT rather than a design reference**: its `src/codebuddy_proxy/dsml_parser.py` is a DSML tool-call parser for a proxy in front of the same WorkBuddy/CodeBuddy upstream, and this project ported it to `src/dsml-recovery.ts` (the tag state machine, ignored-region detection, missing-wrapper repair, recursive parameter parsing, and the streaming buffer), adding the two gates it does not have (a tool name must be declared in THIS request; no declared tools means no recovery at all). The three deliberate deviations are listed in that file's header; the formal attribution is in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
|
243
272
|
|
|
244
273
|
### Baseline for the plugin presentation and structure
|
|
245
274
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">dsh-connect-workbuddy</h1>
|
|
6
6
|
|
|
7
|
-
<p align="center"><b>把本机登录的 WorkBuddy 模型接入 DeepSeek Harness
|
|
7
|
+
<p align="center"><b>把本机登录的 WorkBuddy 模型接入 DeepSeek Harness,并提供账号池与模型管理。</b></p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="README.en.md">English</a> ·
|
|
@@ -24,21 +24,21 @@
|
|
|
24
24
|
<a href="https://dshfind.com/plugins/dingminhua/dsh-connect-workbuddy"><img src="https://dshfind.com/api/badge/dingminhua/dsh-connect-workbuddy" alt="dshfind plugin"></a>
|
|
25
25
|
</p>
|
|
26
26
|
|
|
27
|
-
一个独立的 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle 插件。它把本机已登录的 WorkBuddy 账号(**国内版与国际版 WorkBuddy AI 均支持**)接到 DSH
|
|
27
|
+
一个独立的 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle 插件。它把本机已登录的 WorkBuddy 账号(**国内版与国际版 WorkBuddy AI 均支持**)接到 DSH 的模型选择器,同时提供账号池与可勾选的模型管理界面。**国内版与国际版是两个并行的供应商(`workbuddy` / `workbuddy-global`),可同时使用**;插件设置卡片以 tab 区分两者,方便统一管理。
|
|
28
28
|
|
|
29
29
|
## 功能特性
|
|
30
30
|
|
|
31
|
-
- **双供应商并行接入(国内版 + 国际版)** —— 国内版注册为 `workbuddy` provider(`GLM-5.3`、`DeepSeek-V4-Pro`、`Kimi-K3`、`MiniMax-M3`、`Hy3` 等),国际版注册为 `workbuddy-global` provider(`GPT-5.6`、`Gemini-3.5-Flash`、`GLM-5.3`、`Kimi-K3` 等)。**有账号的区域其模型会出现在 DSH 模型选择器里**:两边都有账号时两组同时可选,不同会话可以各选一边,互不干扰。**本机没有账号的区域不列出任何模型**(该区域一个请求也发不出去,列出来只会点到就失败);只要在该区域登录一次,「重新检测账号」或重启后模型就会自动出现——provider
|
|
32
|
-
- **插件卡片 tab 切换** —— 设置卡片顶部为「国内版 / 国际版」两个 tab
|
|
31
|
+
- **双供应商并行接入(国内版 + 国际版)** —— 国内版注册为 `workbuddy` provider(`GLM-5.3`、`DeepSeek-V4-Pro`、`Kimi-K3`、`MiniMax-M3`、`Hy3` 等),国际版注册为 `workbuddy-global` provider(`GPT-5.6`、`Gemini-3.5-Flash`、`GLM-5.3`、`Kimi-K3` 等)。**有账号的区域其模型会出现在 DSH 模型选择器里**:两边都有账号时两组同时可选,不同会话可以各选一边,互不干扰。**本机没有账号的区域不列出任何模型**(该区域一个请求也发不出去,列出来只会点到就失败);只要在该区域登录一次,「重新检测账号」或重启后模型就会自动出现——provider 本身始终注册着,因为它的设置卡片正是登录的入口(重新检测在账号池区块里)。模型名内嵌上游积分倍率(如 `GLM-5.3 · x0.79`),与 WorkBuddy 自身模型菜单一致。
|
|
32
|
+
- **插件卡片 tab 切换** —— 设置卡片顶部为「国内版 / 国际版」两个 tab,各含独立的账号池与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。
|
|
33
33
|
- **可单独关闭任一版本供应商** —— 每个 tab 右侧有一个勾选框(默认都勾选)。**取消勾选即把该供应商从 DSH 模型选择器里彻底撤掉**(不是只藏起 tab):它的路由与「设置 → 模型」页的目录条目一并摘除,启动时也不再为它发无用请求。账号、积分、目录与勾选**全部保留**,重新勾选即完整恢复。典型用法是「我根本用不到国际版」——关掉它,让选择器干净。注意:若某会话此前选中的正是被关闭供应商的模型,该会话再调用会报 `NO_ADAPTER`,卡片会在关闭态的 tab 上明确提示这一点,不会静默失败。
|
|
34
34
|
- **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;**图片输入按厂商核验表判定**:刷新时只有厂商文档明确写为原生多模态的模型才自动勾选,文档写明纯文本的不勾,**没有厂商资料的一律按「未核实」不猜**(上游的 `supportsImages` 是平台侧声明,不等于模型能看图,不参与判定)。用户仍可随时手动勾选,勾选状态保存在本地。
|
|
35
35
|
- **模型可用性测试(含冷却恢复时间)** —— 限量模型的冷却时间过去只能靠反复试错去猜。现在每个模型行有一个「测试」按钮,直接告诉你它此刻能否使用、不能用时何时恢复。**它发的是一条真实体积的请求(约 25k 输入 token)**,而不是「发个 ping 看通不通」——因为上游的限流**按请求体积触发**(实测约 20k 能过、约 30k 被拒),小请求看着正常、长对话里却可能每次都被拒,那正是「测试通过、实际被限」的来源。**恢复时间的来源**:上游的 429 **不带** `Retry-After` 响应头(也没有 `X-RateLimit-*`),但它把重置时间写在**响应体**里——`{"code":6004,"msg":"…将在 2026-09-30 02:30:30 UTC+8 重置…"}`。插件从响应体解析该时间(并按其声明的 UTC 偏移换算,不受你本机时区影响),因此卡片能直接显示「✗ 被限流 · 2026/9/30 02:30:30 之后可再用」。三处来源都没说时,插件**不编造倒计时**,如实显示「上游未给出何时恢复」。限流**不会**去借月周期包的刷新时间(那会让人为几秒的限流去等几周)。探测在后台发起,**凭据不会传到页面**;结果不写入设置,也不改变你已保存的勾选。
|
|
36
36
|
|
|
37
37
|
**两点需要知道**:① **它真的花钱**——实测一次报 `credit` 0.02(被限流时直接 429,几乎不花)。因此**只能按单个模型手工点**,没有「测试已勾选」这类批量按钮:一次覆盖整个列表就是一次不可控支出(服务端也据此**只接受单个模型**,多模型请求会被拒)。② 上游虽宣称 1M 上下文,但节流线远低于此;插件向 DSH 宣称的窗口最高 200K,所以 DSH 很晚才压缩对话——长会话撞限流是这条机制的直接结果。
|
|
38
38
|
- **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。
|
|
39
|
-
-
|
|
39
|
+
- **账号池:多账号批量操作与自动换号** —— 把要用的账号勾进池子,即可**一键签到所有账号**、**一键测试所有账号 × 你指定的模型**(默认挑该区域倍率为 0 的免费模型);开启**启用账号池**即同时打开**自动换号**——某个请求在上游失败时,插件会依次用池内其他可用账号重试,直到有一个接手、或全部不可用为止,然后才把错误报给你。成员**默认一个都不含**、必须显式勾选(「本机有登录」不等于「要拿它去签到和花积分」);**开启后由池子的排序决定每一次请求用谁**(可用优先,其次积分高、快过期的积分包、凭据更新的);失败时依次用池内其他可用账号重试。排序**从不写回你保存的选择**——关掉开关,你选定的账号立刻完全生效。只有请求本身不合法(HTTP 400)不换号:同一个请求体换谁都是 400,重试只会白等。**这个开关只管「插件要不要自己决定」**:关掉它,插件不再替你选账号、也不再替你换号,但**签到与测试仍然可用**(它们是手动动作)。测试走的是与模型行同一套真实体积探测,因此**有真实成本**;**不提供自动签到**——签到会真实领取奖励,只在你点按钮时执行。详见[账号池](#账号池)。
|
|
40
40
|
- **凭据失效时给出可执行的建议** —— 上游拒绝当前账号的 token 时,插件会**实际探测**本机其他账号:若有账号仍然可用,就直接告诉你「切换到它即可」(并附一键切换),而不是笼统地让你重新登录——被钉在一个作废的旧备份上时,重新登录并不能解决问题。只有本机确实没有别的账号可切换时,才会提示重新登录。
|
|
41
|
-
-
|
|
41
|
+
- **积分信息在账号池里** —— 成员表逐账号显示剩余积分与最近到期时间,排序的第二键就是积分(谁多谁优先)。查询是**只读**的、不消耗积分。
|
|
42
42
|
- **凭据路径多候选** —— macOS / Windows / Linux 逐一探测默认位置,也支持环境变量与卡片内直接指定。**未登录时卡片会列出「探测过哪些路径、各自为什么没成」**:五档原因(不存在 / 读不了 / 没有可用 token / 字段已加密且拿不到密钥 / 属于另一个地区)分开呈现,因此「已经登录了、但桌面 App 不在场」这种最容易误判的情况会明说需要 App 在场,而不是叫你重新登录一次。**属于另一个地区**那一档尤其不同:登录信息是真实可用的,只是挂在另一个标签页下,卡片会直接给出结论「切换标签页即可,不用重新登录」。列表只讲一次——提示段落说结论,折叠列表给路径细节,两处不重复同一件事。
|
|
43
43
|
- **安全 loopback shim** —— 每区域一个随机端口 + 进程内随机 secret,真实 WorkBuddy token 不交给 pi-ai。
|
|
44
44
|
- **命令行诊断** —— `status` / `doctor` / `logout`,按区域报告登录与积分,无需浏览器即可确认宿主状态。
|
|
@@ -66,12 +66,48 @@ DSH PiAiAdapter(每个 provider 一套)
|
|
|
66
66
|
## 账号池
|
|
67
67
|
|
|
68
68
|
<p align="center">
|
|
69
|
-
<img src="docs/assets/dsh-connect-workbuddy-account-pool.png" width="900" alt="dsh-connect-workbuddy
|
|
69
|
+
<img src="docs/assets/dsh-connect-workbuddy-account-pool.png" width="900" alt="dsh-connect-workbuddy 账号池区块:一键签到所有账号、一键测试所有账号、成员勾选与自动换号" />
|
|
70
70
|
</p>
|
|
71
71
|
|
|
72
|
-
一台机器上往往登录过多个 WorkBuddy
|
|
72
|
+
一台机器上往往登录过多个 WorkBuddy 账号。**账号池**把这件事收成一次点击:把要用的账号勾进池子,然后「一键签到所有账号」或「一键测试所有账号」;再打开**启用账号池**,插件就会在请求失败时自动换号接着跑。
|
|
73
73
|
|
|
74
|
-
**每个区域(国内版 /
|
|
74
|
+
**每个区域(国内版 / 国际版)各有独立的一池**:成员与目标模型互不影响——两边的账号本来就属于不同的上游栈。**签到只有国内版有**:海外区域不显示签到按钮,也没有那一列。
|
|
75
|
+
|
|
76
|
+
### 自动换号(开启账号池即生效)
|
|
77
|
+
|
|
78
|
+
- **谁来服务由排序决定**:开启账号池后,**每一次请求**都发给池子里排序最靠前的可用账号——所以「当前使用」显示的是排序的结果,不一定是你手动选的那个。排序规则见下一节。
|
|
79
|
+
- **失败时继续往下试**:某个请求在上游失败时,插件会**依次**用池内其他可用账号重试,直到有一个接手、或全部不可用为止;全部失败才把错误报出来(并在错误里注明尝试过几个账号)。
|
|
80
|
+
- **哪些失败会换号**:429 限流、402 无积分、401 凭据失效、502 网关/网络——这些都是**某个账号**的状态,换一个账号很可能就过了。
|
|
81
|
+
- **哪些不会**:请求本身不合法(HTTP 400)。同一个请求体换哪个账号都是同样的 400,重试只是让用户多等几轮才看到同一个错误。**流中途断开也不会重试**——字节已经发出去了,重放会让客户端看到两段拼接的回答。
|
|
82
|
+
- **已知不可用的账号会被跳过**:正在限流冷却期内、或凭据已被上游拒绝的成员不会参与(这也是「一键测试」的意义:测试结果直接决定谁能被选中)。**从没测试过的账号按「默认可用」参与**——刚发现的登录不该因为「还没测」就被排除。
|
|
83
|
+
- **手动选择不会被改写**:池子通过运行时覆盖决定「谁服务」,**从不写回你保存的选择**;关闭账号池,你选定的账号立刻完全恢复生效。这也是为什么卡片要显示「当前使用」——它可能不是你在上面选的那个,而你应该看得出来。
|
|
84
|
+
- **失败会被记下来**:某个请求撞上失败时,插件把这次失败写成该账号的测量(含上游报文里给出的重置时间),所以**下一个请求直接从可用账号开始**,不会每次都先撞一遍同一个限流账号。只有需要你手动「一键测试」时才不写测量这件事,从此不成立了。HTTP 400 例外——那是**请求**不合法,与账号无关,记下来会白白闲置一个好账号;未勾选进池的账号也不记(勾选才是「可以花它的积分」的同意)。
|
|
85
|
+
- 每次换号都会在宿主日志里留一行 `warn`,写明因何失败、换到了下一个账号。
|
|
86
|
+
|
|
87
|
+
### 这个开关到底控制什么
|
|
88
|
+
|
|
89
|
+
**它只控制「插件要不要自己决定」,不控制「你能不能操作」。**
|
|
90
|
+
|
|
91
|
+
| | 账号池 **开启** | 账号池 **关闭** |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| 谁服务 | 由排序**自动**选出(可用优先 → 积分高 → 快过期 → 凭据新) | **你在账号池里手动选**「使用哪个账号」 |
|
|
94
|
+
| 请求失败 | **自动**换下一个可用账号重试 | 不换号,如实报错 |
|
|
95
|
+
| 一键签到 / 一键测试 | 可用 | **仍然可用** |
|
|
96
|
+
| 目标测试模型 | 可改 | **仍然可改** |
|
|
97
|
+
|
|
98
|
+
最后两行是刻意的:签到与测试是**手动动作**,它们作用于你勾选的成员,与「插件是否自动路由」是两件事。之前把按钮也挂在开关上,导致关掉账号池就**再也没法签到或测试**——而「关闭」本该是「我手动来」,不是「什么都不许做」。
|
|
99
|
+
|
|
100
|
+
**手动选择账号在哪里**:账号池区块里。**关闭账号池时**,该区块会显示一行「使用哪个账号」的下拉,选定即生效(写回本区域的账号槽,与凭据失效时的「切换到可用账号」走同一条已验证的写入路径);**开启账号池时这一行不显示**——那时由排序决定,一个改了不生效的下拉比没有更糟。账号的发现(「重新检测账号」)、勾选、签到与测试全部集中在这个区块里,所以账号管理只有一处。
|
|
101
|
+
|
|
102
|
+
**唯一的硬约束**:池里一个成员都没勾选时,批量动作一律拒绝。空的池**绝不**退化成「那就对全部账号执行」——那是唯一会花掉你没授权过的积分的解读。
|
|
103
|
+
|
|
104
|
+
### 排序规则(开启账号池后决定谁服务)
|
|
105
|
+
|
|
106
|
+
1. **可用优先** —— 被测量判定为不可用(限流未到恢复时间、凭据被拒、连通性失败等)的排到最后,不参与选择;
|
|
107
|
+
2. **积分高者优先** —— 在可用的账号里选积分最多的。**积分只在有新鲜读数时参与**(卡片轮询、批量测试或保存时读到的那份,10 分钟内有效):请求路径**不会**为了排序去打上游,否则一页对话就会变成 N 个额外请求;
|
|
108
|
+
3. **快过期的积分包优先** —— 3 天内到期的先用,过期即作废;
|
|
109
|
+
4. **凭据更晚过期的优先**;
|
|
110
|
+
5. 最后按账号 id 收尾,保证顺序是**全序**、可复现,不会因为账号表顺序变化而抖动。
|
|
75
111
|
|
|
76
112
|
### 两个批量动作
|
|
77
113
|
|
|
@@ -88,38 +124,31 @@ DSH PiAiAdapter(每个 provider 一套)
|
|
|
88
124
|
|
|
89
125
|
勾选是一次**草稿编辑**:点「保存」才落盘,未保存时保存按钮带 dirty 指示;切走或关掉卡片,草稿仍在。
|
|
90
126
|
|
|
91
|
-
###
|
|
92
|
-
|
|
93
|
-
插件此前的原则是**绝不为了找积分而重排账号**——账单落在谁身上必须可预期。自动轮换是**你显式打开的**例外:
|
|
94
|
-
|
|
95
|
-
1. 打开后,插件在可用的成员里按 **① 积分余额高者优先 → ② 快过期的积分优先用完 → ③ 平手时凭据更新鲜者优先** 排序,把得分最高的那个作为当前计费账号。
|
|
96
|
-
2. **它与手动选择互斥**:自动轮换开启时,账号下拉**立即置灰**,并就地给出原因与一个「关闭自动轮换」的按钮。否则你会看到最坏的一种状态——**下拉里写着账号 A,实际计费的却是账号 B**。
|
|
97
|
-
3. **轮换只在运行时决定用谁,绝不写回你的手动选择**(不写 `config.accounts[region]`)。所以关掉开关后,你上次手动选的账号原封不动地回来。
|
|
127
|
+
### 手动选择:卡片上没有账号下拉了
|
|
98
128
|
|
|
99
|
-
|
|
129
|
+
账号**一律在账号池里管**:成员勾选就是现在的账号选择面,「重新检测账号」也在那一块。原先那个「账号选择」下拉在有账号池之后是自相矛盾的——池开启时由排序决定谁服务,它改什么都不生效,却看起来像权威;顶部状态行还可能显示成 A、下拉显示成 B。
|
|
100
130
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
定时器每 60 秒走一次,按你设定的间隔(**5 ~ 1440 分钟,默认 30**)执行一轮「测试所有账号」。下限是 5 分钟而非更短,是刻意的:更密只会把整池打成限流。
|
|
104
|
-
|
|
105
|
-
**一个容易忽略的边界**:若你指定的模型已离开目录,定时轮次**不会就此停摆**——它会回落到该区域的免费模型继续刷新测量。这必须如此:成员能否回池靠的是**新的探测结果**(见下),一个陈旧的显示偏好不该把整池的测量冻住。手动动作仍然拒绝,理由同上。
|
|
131
|
+
所以区域状态行也跟着改:**池开启时不再报出某一个账号名**(多个账号同时登录是常态,用单数说「已登录:A」本身就是错的),只陈述本区域可用、由池决定用谁;**池关闭时才报出那个账号**——那时它确实是唯一答案。状态行旁边的圆点与 token 过期时间讲的是「这个区域能不能用」,和「谁在计费」是两件事,后者由账号池区块负责显示。
|
|
106
132
|
|
|
107
133
|
### 账号为什么出池,以及什么时候回来
|
|
108
134
|
|
|
109
135
|
| 探测结果 | 对轮换的意义 |
|
|
110
136
|
| --- | --- |
|
|
111
137
|
| 可用 | 可入选 |
|
|
112
|
-
| 被限流 |
|
|
138
|
+
| 被限流 | **暂时**出池,按上游给出的重置时间自动回池(上游没说时间就按 30 分钟兜底,**不编造倒计时**) |
|
|
113
139
|
| 积分耗尽 | 出池,按月周期包的刷新时间回池 |
|
|
114
|
-
|
|
|
140
|
+
| 连不上上游 / 失败 | **暂时**出池,30 分钟后自动回池——这类结果说的往往不是账号,而是一次网络抖动或上游 5xx |
|
|
141
|
+
| 凭据被拒 / 模型不存在 | 出池,**只能靠手动「测试」回池**:一个需要重新登录,一个是本区域不再提供该模型,等待都修不好 |
|
|
142
|
+
|
|
143
|
+
关键一条:**「被限流」「连不上上游」都不等于不可用**。它们恰恰是最值得等的那两类状态——当成永久失效剔除,正好丢掉池子里本来最能用的账号,而且**当池内每个成员都被这样剔除时,池子就没有任何候选,自动换号会无处可去**,你只会看到上游原始报错。
|
|
115
144
|
|
|
116
|
-
|
|
145
|
+
**为什么只有限流/欠费/瞬时故障会自己回池**:这三类要么由上游写明了恢复时间,要么由插件按 30 分钟兜底;而「凭据被拒」「模型不存在」等待都无意义,只能由你手动重测或重新登录来刷新。插件**不再有自动复测定时器**(见下),所以后两类的唯一出路就是手动动作。
|
|
117
146
|
|
|
118
147
|
### 保存与草稿:为什么卡片上有两个「保存」
|
|
119
148
|
|
|
120
149
|
模型管理一个、账号池一个,这是**刻意保留**而非漏合并:两者的草稿域不同,合并后一次写入失败会连累另一边(模型目录写不进去,就连池偏好也存不了);一个共享的「放弃」还会在一次误点里同时丢掉两份草稿。两边都是**验证写入**——**写不成功不丢弃草稿**,否则用户的编辑就永久丢了。
|
|
121
150
|
|
|
122
|
-
|
|
151
|
+
池里还有一类状态**不走草稿**:探测结果、冷却时间、当前轮换到的账号。它们是**观测事实**,由插件自己高频写入。原因很实际——草稿是覆盖式的,若观测结果也进草稿,你手工点一次「保存」就可能把插件刚写进去的结果**回退成几分钟前的旧值**。
|
|
123
152
|
|
|
124
153
|
## 安装
|
|
125
154
|
|
|
@@ -249,6 +278,7 @@ dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-workbuddy
|
|
|
249
278
|
|
|
250
279
|
- [corrinehu/dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect)(MIT,Copyright (c) 2026 Corrine Hu)— **本项目的主要参照**。WorkBuddy 接入 DeepSeek Harness 的完整可行方案由该项目首先验证:桌面端凭据的发现与刷新机制、上游协议与请求头约定、loopback shim 的入站加固、pi-ai provider 的装配方式、以及状态诊断 CLI,均以其为参照。本项目在保留这些已验证能力的基础上重写,着重改善使用体验。
|
|
251
280
|
- [Sliverkiss/workbuddy2api](https://github.com/Sliverkiss/workbuddy2api)(MIT)— WorkBuddy 上游协议(`copilot.tencent.com` 的 wire behavior)的参照实现,经 `dsh-workbuddy-connect` 转引。
|
|
281
|
+
- [hawklithm/workbuddy2api](https://github.com/hawklithm/workbuddy2api)(MIT,Copyright (c) 2026 Mayer)— **本项目的致谢名单里唯一一项「代码移植」而非「思路参照」**:其 `src/codebuddy_proxy/dsml_parser.py` 是一份代理同一 WorkBuddy/CodeBuddy 上游的 DSML 工具调用解析器,本项目把它移植为 `src/dsml-recovery.ts`(标签状态机、忽略区域判定、缺包裹修复、参数递归解析、流式缓冲),并在其上补了它没有的两道闸(工具名必须在本次请求声明的工具里、无声明工具完全不做恢复)。三处有意偏离逐条写在该文件头部,义务声明见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。
|
|
252
282
|
|
|
253
283
|
### 插件呈现与结构的基线
|
|
254
284
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
| --- | --- | --- | --- |
|
|
26
26
|
| `dsh-workbuddy-connect` | <https://github.com/corrinehu/dsh-workbuddy-connect> | WorkBuddy 接入的完整参照:凭据发现、上游协议、shim、适配器、CLI | **MIT**(Copyright (c) 2026 Corrine Hu) |
|
|
27
27
|
| `workbuddy2api` | <https://github.com/Sliverkiss/workbuddy2api> | WorkBuddy 上游协议(`copilot.tencent.com` 的 wire behavior)的参照实现 | **MIT** |
|
|
28
|
+
| `workbuddy2api`(DSML 解析器) | <https://github.com/hawklithm/workbuddy2api> | **代码移植**(本清单中唯一一项不只是思路参照):`src/codebuddy_proxy/dsml_parser.py` 的标签状态机、忽略区域判定(Markdown 围栏 / 行内代码 / CDATA / 注释)、缺包裹修复、参数递归解析与流式缓冲,移植为 `src/dsml-recovery.ts`;并在其上补了该实现没有的两道闸(工具名必须在本次请求声明的工具里、无声明工具完全不做恢复)。逐条改动与理由见该文件头部注释与 `docs/DSML-RECOVERY-PLAN.md` | **MIT**(Copyright (c) 2026 Mayer) |
|
|
28
29
|
|
|
29
30
|
### 与 DSH 插件结构 / 外观相关
|
|
30
31
|
|
|
@@ -48,8 +49,9 @@
|
|
|
48
49
|
## 四、本项目的使用方式
|
|
49
50
|
|
|
50
51
|
- **借鉴设计思路 + 独立实现**:关键模块均为本项目独立编写,并在各源文件头部注释中标注所参考的具体项目与模式。
|
|
51
|
-
-
|
|
52
|
-
-
|
|
52
|
+
- **唯一一处代码移植**:`src/dsml-recovery.ts` 是 [`hawklithm/workbuddy2api`](https://github.com/hawklithm/workbuddy2api) 的 `src/codebuddy_proxy/dsml_parser.py`(MIT,Copyright (c) 2026 Mayer)的 **TypeScript 移植**,不是思路参照。该文件的头部注释逐条列出了三处**有意偏离**(参考实现里重复定义的 `find_invoke_blocks`、其流式注入与自身解析器输出形状不一致、以及它没有的两道闸)。按 MIT 条款,本项目保留其版权与许可声明,并在上表与本条中同时标注。
|
|
53
|
+
- **除此之外不复制源码**:其余各参考项目均只借鉴设计思路,未复制、修改或再分发其源文件。
|
|
54
|
+
- **版权声明保留**:所有参考项目的版权归各自作者所有。
|
|
53
55
|
- **后续引入依赖时**:引入任何新的 WorkBuddy 相关外部依赖、或复用其他项目代码时,必须同步更新本文件,并遵守对应许可证的署名与声明要求。
|
|
54
56
|
|
|
55
57
|
## 五、随包分发
|