dsh-connect-workbuddy 2.3.0 → 3.0.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 CHANGED
@@ -1,5 +1,99 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.0 (2026-09-30)
4
+
5
+ > **主版本号升级的理由**:账号池是第一个会改变「哪个账号被计费」的功能。在此之前插件只有一条硬规则——**绝不为了找积分而重排账号**(`src/auth.ts` 的原注释:*This is NOT credit-seeking*)。账号池把这条规则变成**用户显式授权的例外**:打开「按积分自动轮换」后,被计费的账号可能不是你手动选的那个。这是语义级的变化,因此走主版本号。
6
+
7
+ ### Features
8
+
9
+ #### 账号池:多账号批量操作与自动轮换
10
+
11
+ - **一键签到所有账号**:逐个账号签到,**签到前先查当天状态**,已签到的直接标记、**不发写入请求**(幂等,不重复领取);单个账号失败只标记该行,**不中断整批**。
12
+ - **一键测试所有账号**:对每个成员发一次**真实体积**的探测(约 25k 输入 token,复用模型行的 `probeModel`),得出该账号此刻能否用这个模型。默认目标是**该区域倍率为 0 的免费模型**(国内版即 `deepseek-v4.1-flash`),也可指定具体模型;指定的模型若离开目录,手动动作**明确拒绝**(`target-model-stale`)而不是偷换一个。
13
+ - **按积分自动轮换**:在可用成员中按 **① 积分余额高者优先 → ② 快过期的积分优先用完 → ③ 平手时凭据更新鲜者优先** 选出计费账号。**与手动选择互斥**:开启后手动下拉立即置灰并就地给出原因与关闭按钮(否则会出现「下拉写着 A、实际计费 B」)。**轮换只在运行时决定用谁,绝不写回 `config.accounts[region]`**,关掉开关即原样恢复你上次的手动选择;每次换号都在卡片上留一条记录。
14
+ - **成员是显式勾选、默认空**:「本机有登录」不等于「要拿它去签到和花积分」。**空池不会退化成「全部账号」**——未勾选时两个批量按钮直接拒绝。
15
+ - **定时自动测试**:间隔 **5 ~ 1440 分钟(默认 30)**,定时器每 60 秒走一次。下限 5 分钟是刻意的:更密只会把整池打成限流。
16
+ - **每个区域各有独立的一池**:国内版与国际版的成员、目标模型、间隔与轮换开关互不影响。
17
+ - **不提供自动签到**(明确边界):签到是写操作、会真实领取奖励,只在你点按钮时执行;卡片上也不放一个永远点不动的开关。
18
+ - **成本如实标注**:测试是真实请求,指向免费模型(倍率 0)时几乎不花钱,但**不是零成本空转**。
19
+
20
+ ### Bug Fixes
21
+
22
+ - **卡片里的时间戳全部改为 24 小时制**:四个 `Intl.DateTimeFormat` 都漏了 `hourCycle`,于是跟随浏览器语言的默认偏好——en-US 渲染成 `09/30, 01:23 PM`。现统一为 `hourCycle: 'h23'`(不用 `hour12: false`:后者在部分引擎会落到 `h24`,把半夜显示成 `24:00`)。新增渲染回归测试钉住这一点,并用变异体确认该测试有判别力(去掉 `hourCycle` 即变红)。
23
+ - **陈旧的「目标模型」不再冻结整池的测量**:定时轮次原先在 `resolveTargetModel` 解不出 id 时直接返回,而「你指定的模型已离开目录」正是产出「无 id」的情形之一。于是一处显示偏好的错误会让整个区域的测量停摆:`被限流` 的成员靠时钟就能回池,而 `连不上上游` 的成员只能靠**新的探测**清除——探测不再写入,该成员便**永久挂起**。现在定时轮次会回落到该区域的**免费**模型继续刷新;没有免费模型时仍然整轮跳过(**绝不在定时器上烧收费模型**)。手动动作依旧拒绝陈旧目标。
24
+ - **一次刷新失败不再抹掉整个面板**:`refreshUsage` 的失败路径原先把整个区域的状态塌缩成 `{status:'error', message}`,而面板主体 gate 在 `signed-in` 上——于是**一次网络抖动就让正在编辑的账号池连草稿一起消失**。现在沿用项目既有的「快照 + 分区错误叠加」范式:**有数据就叠加 `refreshError`,没数据才塌缩**。
25
+ - **卡片不再永远渲染未登录占位**:`usageGuard` 的极性写反了(`begin()` 返回的探针报告的是**过期**),导致每一个未被更新的响应都被丢弃。已修正为 `const stale = …; if (stale()) return`,并由真实渲染测试(jsdom + react-dom)钉住。
26
+ - **间隔输入框可以正常输入**:逐键 clamp 会让 `120` 被记成 `520`、清空即跳回 `5`。现在只把**合法的整数**折进草稿,显示始终是原始输入,失焦/回车时**恰好收敛一次**。
27
+ - **保存失败不再丢弃草稿**:写入失败或「回读没有带来新 props」时保留草稿并说明原因;**草稿是用户编辑的唯一副本**。
28
+ - **批量动作后重新读取用量**:签到会领取积分、测试会写入探测结果,而面板数字来自用量路由——现在批量成功后**恰好重新读取一次**,且**失败时不重读**(避免掩盖错误)。
29
+ - **同一区域的用量请求只允许最新一次写入**:轮询 / 保存 / 批量三种刷新会并发,慢的响应原先可以覆盖新的快照。
30
+ - **成员勾选不再静默丢账号**:`editDraft` 的依赖数组缺了保存的成员列表,勾选会以**旧成员列表**为基准,保存即静默移出成员。
31
+ - **池路由的错误按原因本地化**:四个 503/500 位置现在都带结构化的 `reason`,中文用户不再只看到英文。
32
+ - **幽灵池(成员已消失)给出可执行的恢复指引**,而不是笼统失败。
33
+
34
+ ### Tests
35
+
36
+ - **引入真实渲染测试(jsdom + react-dom)**:第四轮独立对抗验证证明「读源码文本」的守卫与「住在行为里」的缺陷之间必然有缝——18 个变异体恢复了**用户可见**的缺陷而整套测试仍全绿。本轮把 `AccountPool.tsx` / `WorkBuddyCard.tsx` 真正挂载进 DOM 断言,从根上结束这一整类问题,并据此击毙了其中 16 个(余 2 个经逐字论证为**等价变异体**,不恢复任何缺陷)。
37
+ - 逐条收紧被证明可绕过的守卫:`stripLiterals()` 连**字符串字面量**一起去(原先只去注释,一个字符串就能顶替一次真实调用);「恰好一次」改为对**标识符**计数;A-8 增加「`committed` 只能被写一次且不在 `catch` 里」。
38
+ - 修掉一个会**伪造结论**的工装缺陷:调度器用例对负载敏感,固定轮数的排空在 CPU 争用下会读到 0,变异工装会把它读成「守卫抓住了变异体」,从而报告出并不存在的覆盖。现在改为**排空到条件成立**。
39
+
40
+ ### Docs
41
+
42
+ - README 两份均新增**账号池**章节(含产品截图):两个批量动作、成员为何必须显式勾选、自动轮换与手动选择的互斥、目标模型与间隔、四种探测结果与回池条件、以及为什么卡片上有两个「保存」。
43
+ - 新增 `docs/audit/` 审计与修复档案:A–I 九份独立报告 + MASTER 汇总 + E-fix-log 逐轮修复记录。全部 51 条审计条目经独立验证**100% 可复现**(0 条误报)。
44
+
45
+ ## 2.3.1 (2026-09-29)
46
+
47
+ ### Features
48
+
49
+ #### 模型「测试」改为真实体积:揭穿「测试通过、实际被限」
50
+
51
+ **上游的 6004 限流按请求体积触发**,而此前的探针只发 `max_tokens: 1` + `ping`(约 18 token)——它永远够不到那条线。于是出现一个自相矛盾的现象:**卡片测出来模型可用,真实会话却每次都 429**。这不是探针算错,而是它**问的问题太轻**。
52
+
53
+ 真机量化(2026-09-29,同一账号、同一模型,只改输入体积):
54
+
55
+ | 输入 | 上游计到的 prompt_tokens | 结果 |
56
+ | --- | --- | --- |
57
+ | ~10 | 38 | ok |
58
+ | ~1,000 | 1,138 | ok |
59
+ | ~10,000 | 11,138 | ok |
60
+ | ~18,000 | 20,018 | ok |
61
+ | **~25,000** | **25,023** | ok |
62
+ | ~30,000(干净账号只发这一次) | ~32k | **429 / 6004** |
63
+
64
+ - **阈值在 20k 与 30k 之间,单次大请求即触发**(不是累计),且**每个账号各有自己的重置时间**(实测同一时刻两个账号分别报 `22:20:33` 与 `00:32:51`)。
65
+ - **为什么这是用户会踩到的**:插件向 DSH 宣称的上下文窗口最高 200K(`web-status.ts` 把 1M 的模型按默认预算压到 200K),DSH 因此要到 20 万 token 才压缩——**远超上游 ~20–30k 的节流线**。长会话里请求一路涨过阈值,之后每一次都失败,而小探针始终显示可用。
66
+ - **改动**:**只保留一种测试**,即每个模型行上的「测试」按钮,它现在发一条 **~25k 输入 token** 的真实体积请求。真机验证:请求体 112,713 B,上游计到 `prompt_tokens: 25023`。
67
+ - **删掉了两样东西**:① 原先的最小探针(`probeRequestBody` 的 `ping` 版本、`heavy` 开关、`PROBE_USER_PROMPT`);② 模型区顶部的**「测试已勾选」批量按钮**。后者不是排版调整而是成本决定——每次探测都真花钱(实测 `credit` 约 0.02~0.72),一次覆盖整个已勾选列表就是一次不可控支出。路由相应改为**只接受 1 个模型**(多于 1 个直接 400):卡片只发这一个形状,多模型请求不是卡片发的,拒绝它正是防止陈旧 bundle 或手写客户端把按钮变成消费循环的那道闸。
68
+ - **实测代价**:一次「测试」报 `credit` 0.02(健康账号、25k 输入);被限流时立即 429 并给出重置时间。此前的最小探针是 0~0.01。
69
+
70
+ ### Bug Fixes
71
+
72
+ #### 「上游未给出何时恢复」是错的:恢复时间一直在响应体里
73
+
74
+ 2.3.0 的模型测试上线后,真实使用暴露了一个要命的偏差:**探针说模型可用、用户点「测试」也看到通过,但真正发请求却被限流**;而被限流的那一行显示的是「上游未给出何时恢复」——**上游其实说了,就在响应体里**。
75
+
76
+ - **根因**:`cooldownOf()` 只读两个来源——`Retry-After` 响应头、以及积分耗尽时的月度刷新点。而本服务的 429 **根本没有 `Retry-After` 头**,重置时间写在响应体的 `msg` 文本里。实测抓到的原文(2026-09-29,逐字):
77
+
78
+ ```json
79
+ {"code":6004,"msg":"您的使用量已超出频率限制,将在 2026-09-30 02:30:30 UTC+8 重置,您也可以切换其他模型继续使用。"}
80
+ ```
81
+
82
+ 2.3.0 的注释写「上游不提供限流元数据」,这个结论**是从响应头单独得出的**——就响应头而言是对的,作为结论是错的。
83
+ - **为什么「测试通过、实际被限」不是探针的错**:探针与真实请求的判定本来就一致。用真实凭据实测,同一模型用**四种请求形态**(`max_tokens:1` + `ping`、`max_tokens:2000` + 长 prompt、以及两者交叉)全部返回**同一个 429**;13 个模型里只有 `deepseek-v4.1-flash` 被限,其余 12 个(含默认的 `glm-5.3-flash` 一类)全部 `ok`。所以「测试通过」看到的是**别的模型**,而被限的那个恰好是配置里用着的。限流由账户/模型维度的配额决定,与请求大小无关——这也回答了「加大请求能不能测出来」:不能。
84
+ - **修法**:新增 `parseUpstreamResetAt()`,从失败响应体解析 `将在 <时间> UTC<偏移> 重置`;`cooldownOf()` 增加第三个来源 `upstream-message`(优先级:响应头 > 响应体 > 月度刷新点)。`probeModel()` 把 body 传给它。
85
+ - **时区必须按偏移量算,不能被本机时区左右**:`2026-09-30 02:30:30 UTC+8` 是 UTC 的 `2026-09-29T18:30:30Z`。直接 `Date.parse` 会**按本地时区**解读——在 UTC+8 的机器上恰好正确,换台机器就静默错一个偏移量。因此用 `Date.UTC` 组装后减去声明的偏移,实测 UTC / Asia/Shanghai / America/New_York 三种 `TZ` 下输出一致。
86
+ - **宁可不说,也不说错**:无法解析时返回 `undefined`,卡片继续如实显示「上游未给出何时恢复」。`Date.UTC` 会把 13 月、32 日**静默归一化**成另一个真实时刻,所以解析后逐字段回验,不合法就丢弃;`UTC+99` 这类越界偏移同样拒绝。这些都配了用例。
87
+ - **守卫与变异验证**:`tests/probe.spec.ts`(28 → 38 例)覆盖:真机原文端到端(经 `probeModel`)、响应头优先于响应体、`T` 分隔符与 `UTC+0800` 紧凑写法、负偏移、四种「不该解析出时间」的形态、三种非法日期、越界偏移。**变异验证**:把偏移量那一步去掉(当成 UTC 算)→ **6 例变红**。
88
+ - 文案无需改动:`row.probeRateLimitedAt`(`✗ 被限流 · {at} 之后可再用`)本就存在,此前只是永远走不到。
89
+
90
+ ### Tests
91
+
92
+ - `tests/probe.spec.ts`(28 → 42 例):探测请求体**恒为真实体积**且随目标常量缩放;`parseUpstreamResetAt` 的真机原文、`T` 分隔符、`UTC+0800` 紧凑写法、负偏移、越界偏移、三种非法日期、四种「不该解析出时间」的形态;`cooldownOf` 三级来源的优先级(响应头 > 响应体 > 月度刷新点)。
93
+ - `tests/web-status.spec.ts`(49 → 50 例):路由**只接受单个模型**,多模型请求 400。
94
+ - **变异验证**:把偏移量换算去掉(当成 UTC 算)→ **6 例变红**;去掉单模型强制 → 对应用例变红。
95
+ - 全量:**449 例全绿**(2.3.0 为 434 例),typecheck 与 build 通过。
96
+
3
97
  ## 2.3.0 (2026-09-29)
4
98
 
5
99
  ### Features
@@ -25,6 +119,8 @@
25
119
  | 积分耗尽,无月周期包 | `✗ 积分不足 · 上游未给出何时恢复` | 无 |
26
120
  | 401/403 凭据被拒 | `✗ WorkBuddy 拒绝了这个账号的 token——请重新登录或换一个账号` | — |
27
121
 
122
+ > ⚠️ **本节的第 2 条来源此后有更正(见 Unreleased)**:本节写「上游不提供这个数据」,那是**只看响应头**得出的结论——响应头里确实没有,但上游把重置时间写在**响应体**里(`将在 … UTC+8 重置`)。「上游未给时间」那一行因此在真实 429 上从不该出现。解析与实测见 Unreleased 的 Bug Fixes 一节。
123
+
28
124
  限流**不会**去借月周期包的刷新时间:那会让用户为几秒的限流去等几周。这个区分由 `cooldownOf` 强制,并有专门用例钉住。
29
125
 
30
126
  **为何每个探测请求都必须以 system 消息开头**:国际网关(`www.workbuddy.ai`)对 `[{role:'user'}]` 一律返回 HTTP 400 / 业务码 `11128`(`first message is not system prompt`)。按国内版那样只发一条 user 消息,会把**整个国际版误报成全部不可用**。实测加上 system 消息后立即 200,故探测请求始终带一条 system 消息。
@@ -38,7 +134,7 @@
38
134
 
39
135
  ### Tests
40
136
 
41
- - `tests/probe.spec.ts`(新增,28 例):请求体必带 system 首条且 `max_tokens: 1` / `stream:true`;`Retry-After` 的秒数与 HTTP-date 两形态、以及 0 / 负数 / 过期时间不被当作冷却;`cooldownOf` 的四种组合(含**限流绝不编造时间**、**限流不借月周期刷新时间**);七类失败分类;成功流不被截断影响;**抛出异常不逃逸**;失败正文脱敏。
137
+ - `tests/probe.spec.ts`(新增,28 例;此后在 Unreleased 增至 38 例):请求体必带 system 首条且 `max_tokens: 1` / `stream:true`;`Retry-After` 的秒数与 HTTP-date 两形态、以及 0 / 负数 / 过期时间不被当作冷却;`cooldownOf` 的四种组合(含**限流绝不编造时间**、**限流不借月周期刷新时间**);七类失败分类;成功流不被截断影响;**抛出异常不逃逸**;失败正文脱敏。
42
138
  - `tests/web-status.spec.ts`(+10 例):逐模型串行调用(并发变异会红)、region 透传、空列表 / 非数组 / 超限批量一律 400、无能力时 503、非回环来源 403、非 POST 405、未知 region 400、抛错转 500 且不泄露 token。
43
139
  - **变异验证**(5 处,逐一实测变红):给限流编造固定倒计时 → 3 例红;删掉 system 首条消息 → 2 例红;让限流借用月周期刷新时间 → 1 例红;把串行批改为并发扇出 → 1 例红;其余对照组全绿。
44
140
 
package/README.en.md CHANGED
@@ -25,8 +25,11 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
25
25
  - **Tabbed settings card** — the card's top carries a "Domestic / Global" tab bar; each tab holds its own account picker, credits overview, 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
- - **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 and the models header has **Test selected**: each chosen model gets one minimal request (`max_tokens: 1`), and you get a direct answer for whether it works right now and, if not, when it comes back. Measured cost is negligible: the response reports `credit` between 0 and 0.01, and one probe left the balance unchanged (933 → 933), against 0.79x and up for a real reply; each probe took 0.8–3.4s. **On "when can I use this again", the upstream does not provide that data**: measured responses carry no `Retry-After` and no `X-RateLimit-*`, and 12 rapid requests to the same model all succeeded. So the plugin **never invents a countdown** — it shows a time only for the two cases where the upstream actually states one (a `Retry-After` header, or the monthly quota's refresh point when the quota is exhausted) and says plainly that no time was given otherwise. 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.
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
+
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.
29
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 credit-aware rotation** — 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 — when you want it — **rotation by credits**, letting the plugin pick which pool member gets billed. 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". Rotation and manual selection are **mutually exclusive** and the card says why, in place; rotation only decides who serves at runtime and **never writes back your manual choice**, so switching it off restores that choice verbatim. 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).
30
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.
31
34
  - **Read-only credits overview** — remaining credit aggregated per package, plus each model's credit multiplier. Queries consume no credits.
32
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.
@@ -53,6 +56,64 @@ Credentials are read (read-only) from the WorkBuddy desktop app's own auth file.
53
56
 
54
57
  **Encrypted credential fields**: newer desktop builds (Windows first) replace the auth file's `accessToken` / `refreshToken` strings with an AES-256-GCM envelope (`{"$wbEncrypted":1,"envelope":"…"}`), and the account's **display name (`nickname`) is encrypted the same way** — left unopened it degrades to a bare uin number. The envelope is sealed with a field key **compiled into the app itself**, not a user secret — so the plugin ships no key copy. When it meets an encrypted document it runs the installed app with `ELECTRON_RUN_AS_NODE` and calls that app's own native binding (`electron_browser_workbuddy_storage.loggerGet()`) to fetch the same payload and derive the key locally: it asks the very build that wrote the file. The key is cached in process memory only — never on disk, never in logs. Plain-string documents (macOS, older Windows builds) take the original path and never spawn a child process. `account.phoneNumber` is encrypted there too, and the plugin deliberately **neither reads nor displays it**. If the app lives somewhere unusual, point `WORKBUDDY_APP_EXECUTABLE` at its executable.
55
58
 
59
+ ## Account pool
60
+
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 credit-aware rotation" />
63
+ </p>
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**; open **rotation by credits** when you want the plugin to choose the billed account for you.
66
+
67
+ **Each region (domestic / international) has its own independent pool** — membership, target model, interval, and the rotation switch never affect the other side, because the two sides' accounts belong to different upstream stacks.
68
+
69
+ ### The two batch actions
70
+
71
+ - **Check in all accounts** — signs in account by account. It **reads today's status first**, so an account that is already checked in is marked as such and receives **no write request at all** (idempotent — no duplicate claims); a single account failing marks only that row and **never aborts the batch**.
72
+ - **Test all accounts** — sends one **real-volume** probe per member (about 25k input tokens; the same `probeModel` the model rows use, so it inherits both measured facts: throttling is triggered by request **size**, and the reset time is written into the response **body** rather than a header), reporting whether that account can serve that model right now.
73
+
74
+ **Which model is tested:** by default the **zero-multiplier free model in that region's catalog** (`deepseek-v4.1-flash` on the domestic side), or a specific model you pick in the dropdown. If the model you named has left the catalog, the **manual action refuses outright** (`target-model-stale`) instead of quietly substituting another one — you asked to test *that* model, and swapping it answers a question you did not ask.
75
+
76
+ **Cost and boundaries:** testing is a real request. Pointed at the free model (multiplier 0) it costs virtually nothing, but it is **not a free no-op**; check-in claims that day's real reward. Both actions run **only when you press the button** — the plugin **does not offer automatic check-in**, and the card does not show a switch that can never be flipped.
77
+
78
+ ### Membership is an explicit opt-in, empty by default
79
+
80
+ "signed in on this machine" is not "spend this account's credits and claim its rewards". Membership starts **empty** and must be ticked one by one (or with **Select all**); **an empty pool never degrades into "all accounts"** — with nothing ticked, both batch actions are refused rather than running every account on your behalf.
81
+
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
+
84
+ ### Rotation by credits: turning "pick an account by balance" into an explicit grant
85
+
86
+ This plugin's standing rule is that it **never reorders accounts to seek credits** — which account gets billed has to be predictable. Rotation is the exception **you switch on yourself**:
87
+
88
+ 1. Once on, the plugin ranks the usable members by **① highest credit balance first → ② credits expiring soonest first → ③ freshest credential as the tie-break** and bills the winner.
89
+ 2. **It is mutually exclusive with manual selection**: turning it on **greys out the account dropdown immediately** and explains why in place, with a switch-off button right there. Otherwise you get the worst possible state — **the dropdown reads account A while account B is actually billed**.
90
+ 3. **Rotation only decides who serves at runtime and never writes back your manual choice** (it does not touch `config.accounts[region]`). Switch it off and your last manual pick returns untouched.
91
+
92
+ **Every rotation leaves a line in the card** (when, and from whom to whom) rather than happening silently.
93
+
94
+ ### The automatic test interval
95
+
96
+ A timer ticks every 60 seconds and runs a "test all accounts" pass on your interval (**5 to 1440 minutes, default 30**). The 5-minute floor is deliberate: anything denser only throttles the whole pool.
97
+
98
+ **One easily missed edge:** if the model you named has left the catalog, the scheduled pass **does not stop** — it falls back to the region's free model and keeps the measurements fresh. It has to: a member comes back into the pool through a **new probe result** (below), and a stale display preference must not freeze the whole pool's measurements. The manual action still refuses, for the reason given above.
99
+
100
+ ### Why an account leaves the pool, and when it comes back
101
+
102
+ | Probe outcome | What it means for rotation |
103
+ | --- | --- |
104
+ | Usable | Eligible |
105
+ | 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**) |
106
+ | Out of credit | Out of the pool, returning at the monthly package's refresh time |
107
+ | Credential rejected / model not found / upstream unreachable / failed | Out of the pool until a later probe refreshes the result |
108
+
109
+ 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
+
111
+ ### Saving and drafts: why the card has two Save buttons
112
+
113
+ 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
+
115
+ One class of state in the pool **does not go through a draft**: probe results, cooldowns, and the account rotation currently picked. 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
+
56
117
  ## Install
57
118
 
58
119
  > ⚠️ **Version requirement: DSH 0.1.7-rc.1 or newer (current window: `0.1.7-rc.1` – `0.2.x`).** Since v2.1.0 this plugin only supports hosts on DSH 0.1.7-rc.1 and above (older hosts cannot resolve the dependencies to install this version); hosts on 0.1.5 and earlier should stay on v2.0.15. Since v2.1.2 the upper bound is **`<0.3.0-0`**: the **whole 0.2.0 line, GA included, is verified**; the 0.3.0 line is explicitly refused. See [Supported host range](#supported-host-range) below.
package/README.md CHANGED
@@ -10,6 +10,7 @@
10
10
  <a href="README.en.md">English</a> ·
11
11
  <a href="#安装">安装</a> ·
12
12
  <a href="#工作原理">工作原理</a> ·
13
+ <a href="#账号池">账号池</a> ·
13
14
  <a href="CHANGELOG.md">更新日志</a> ·
14
15
  <a href="https://github.com/dingminhua/dsh-connect-workbuddy/issues">问题反馈</a>
15
16
  </p>
@@ -31,8 +32,11 @@
31
32
  - **插件卡片 tab 切换** —— 设置卡片顶部为「国内版 / 国际版」两个 tab,各含独立的账号选择、积分概览与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。
32
33
  - **可单独关闭任一版本供应商** —— 每个 tab 右侧有一个勾选框(默认都勾选)。**取消勾选即把该供应商从 DSH 模型选择器里彻底撤掉**(不是只藏起 tab):它的路由与「设置 → 模型」页的目录条目一并摘除,启动时也不再为它发无用请求。账号、积分、目录与勾选**全部保留**,重新勾选即完整恢复。典型用法是「我根本用不到国际版」——关掉它,让选择器干净。注意:若某会话此前选中的正是被关闭供应商的模型,该会话再调用会报 `NO_ADAPTER`,卡片会在关闭态的 tab 上明确提示这一点,不会静默失败。
33
34
  - **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;**图片输入按厂商核验表判定**:刷新时只有厂商文档明确写为原生多模态的模型才自动勾选,文档写明纯文本的不勾,**没有厂商资料的一律按「未核实」不猜**(上游的 `supportsImages` 是平台侧声明,不等于模型能看图,不参与判定)。用户仍可随时手动勾选,勾选状态保存在本地。
34
- - **模型可用性测试(含冷却恢复时间)** —— 限量模型的冷却时间过去只能靠反复试错去猜。现在每个模型行有一个「测试」按钮,模型区顶部有「测试已勾选」,对勾选的模型**各发一条最小请求**(`max_tokens: 1`),直接告诉你它此刻能否使用、不能用时何时恢复。实测代价可忽略:`credit` 报 0~0.01,单次探测前后余额不变(933 → 933),而正常一次回答是 0.79x 起;单次耗时 0.8~3.4s。**关于「多久之后可以再次使用」,上游并不提供这个数据**:实测响应头里没有 `Retry-After`、也没有 `X-RateLimit-*`,对同一模型连续快速打 12 次全部成功。因此插件**不编造倒计时**,只在两种上游真的给出时间的情形下显示时间——`Retry-After` 头,或积分耗尽时该区域月周期包的刷新点;两者都没有时如实显示「上游未给出何时恢复」。限流**不会**去借月周期包的刷新时间(那会让人为几秒的限流去等几周)。探测在后台发起,**凭据不会传到页面**;结果不写入设置,也不改变你已保存的勾选。
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
+
37
+ **两点需要知道**:① **它真的花钱**——实测一次报 `credit` 0.02(被限流时直接 429,几乎不花)。因此**只能按单个模型手工点**,没有「测试已勾选」这类批量按钮:一次覆盖整个列表就是一次不可控支出(服务端也据此**只接受单个模型**,多模型请求会被拒)。② 上游虽宣称 1M 上下文,但节流线远低于此;插件向 DSH 宣称的窗口最高 200K,所以 DSH 很晚才压缩对话——长会话撞限流是这条机制的直接结果。
35
38
  - **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。
39
+ - **账号池:多账号批量操作与自动轮换** —— 把要用的账号勾进池子,即可**一键签到所有账号**、**一键测试所有账号 × 你指定的模型**(默认挑该区域倍率为 0 的免费模型),需要时再打开**按积分自动轮换**让插件在池内自行挑一个计费账号。成员**默认一个都不含**、必须显式勾选(「本机有登录」不等于「要拿它去签到和花积分」);自动轮换与手动选择**互斥**并就地说明原因;轮换只决定运行时用谁,**永不写回你的手动选择**,关掉开关即原样恢复。测试走的是与模型行同一套真实体积探测,因此**有真实成本**;**不提供自动签到**——签到会真实领取奖励,只在你点按钮时执行。详见[账号池](#账号池)。
36
40
  - **凭据失效时给出可执行的建议** —— 上游拒绝当前账号的 token 时,插件会**实际探测**本机其他账号:若有账号仍然可用,就直接告诉你「切换到它即可」(并附一键切换),而不是笼统地让你重新登录——被钉在一个作废的旧备份上时,重新登录并不能解决问题。只有本机确实没有别的账号可切换时,才会提示重新登录。
37
41
  - **只读积分概览** —— 按套餐聚合展示剩余积分,并列出每个模型的积分倍率。查询不消耗积分。
38
42
  - **凭据路径多候选** —— macOS / Windows / Linux 逐一探测默认位置,也支持环境变量与卡片内直接指定。**未登录时卡片会列出「探测过哪些路径、各自为什么没成」**:五档原因(不存在 / 读不了 / 没有可用 token / 字段已加密且拿不到密钥 / 属于另一个地区)分开呈现,因此「已经登录了、但桌面 App 不在场」这种最容易误判的情况会明说需要 App 在场,而不是叫你重新登录一次。**属于另一个地区**那一档尤其不同:登录信息是真实可用的,只是挂在另一个标签页下,卡片会直接给出结论「切换标签页即可,不用重新登录」。列表只讲一次——提示段落说结论,折叠列表给路径细节,两处不重复同一件事。
@@ -59,6 +63,64 @@ DSH PiAiAdapter(每个 provider 一套)
59
63
 
60
64
  **加密凭据字段**:新版桌面端(Windows 先行)把 auth 文件里的 `accessToken` / `refreshToken` 从纯字符串改为 `{"$wbEncrypted":1,"envelope":"…"}` 的 AES-256-GCM 信封,**账号昵称 `nickname` 同样被加密**(不解密就会退回成一串 uin 数字)。信封用的是 App **自身构建期内置**的字段密钥,并非用户密钥,因此插件不内置任何密钥副本,而是在需要时用 `ELECTRON_RUN_AS_NODE` 调用已安装 App 的原生绑定(`electron_browser_workbuddy_storage.loggerGet()`)取回同一份载荷并就地派生密钥——也就是「向写下这个文件的那个构建本身询问」。密钥只在本进程内存中缓存,不落盘、不进日志;纯字符串文件(macOS 与旧版 Windows)走原路径,一次子进程都不会启动。其中的 `account.phoneNumber` 虽同样加密,插件**不读取也不显示**。若 App 安装在非常规位置,用 `WORKBUDDY_APP_EXECUTABLE` 指定其可执行文件。
61
65
 
66
+ ## 账号池
67
+
68
+ <p align="center">
69
+ <img src="docs/assets/dsh-connect-workbuddy-account-pool.png" width="900" alt="dsh-connect-workbuddy 账号池区块:一键签到所有账号、一键测试所有账号、成员勾选与自动轮换" />
70
+ </p>
71
+
72
+ 一台机器上往往登录过多个 WorkBuddy 账号。**账号池**把这件事收成一次点击:把要用的账号勾进池子,然后「一键签到所有账号」或「一键测试所有账号」;需要时再打开**按积分自动轮换**,让插件在池内自行挑一个计费账号。
73
+
74
+ **每个区域(国内版 / 国际版)各有独立的一池**:成员、目标模型、间隔与轮换开关互不影响——两边的账号本来就属于不同的上游栈。
75
+
76
+ ### 两个批量动作
77
+
78
+ - **一键签到所有账号** —— 逐个账号签到。**签到前先查当天状态**,已签到的直接标记为「已签到」而**不发写入请求**(幂等,不重复领取);任何单个账号失败只标记该行,**不中断整批**。
79
+ - **一键测试所有账号** —— 对每个成员发一次**真实体积**的探测请求(约 25k 输入 token;与模型行的「测试」是同一套 `probeModel`,因而同样继承两个实测结论:限流**按请求体积**触发、恢复时间写在**响应体**里而不是响应头),得出该账号此刻能否用这个模型。
80
+
81
+ **测试目标是哪个模型**:默认是**该区域目录里倍率为 0 的免费模型**(国内版即 `deepseek-v4.1-flash`),也可以在下拉里指定一个具体模型。指定的模型若已离开目录,**手动动作会明确拒绝**(`target-model-stale`),而不是偷偷换一个——你点的是「测这个模型」,换掉就是答非所问。
82
+
83
+ **成本与边界**:测试是真实请求,指向免费模型时(倍率 0)几乎不花钱,但它**不是零成本的空转**;签到则会真实领取当日奖励。两个动作都**只在你点按钮时执行**——插件**不提供自动签到**,卡片上也不放一个永远点不动的开关。
84
+
85
+ ### 池成员是显式勾选,默认一个都不含
86
+
87
+ 「本机有登录」不等于「要拿它去签到和花积分」。成员默认**空**,必须逐个勾选(或点「全选」);**空池不会退化成「全部账号」**——没勾选时两个批量动作直接拒绝,而不是替你把所有账号跑一遍。
88
+
89
+ 勾选是一次**草稿编辑**:点「保存」才落盘,未保存时保存按钮带 dirty 指示;切走或关掉卡片,草稿仍在。
90
+
91
+ ### 自动轮换:把「按积分选账号」变成一次显式授权
92
+
93
+ 插件此前的原则是**绝不为了找积分而重排账号**——账单落在谁身上必须可预期。自动轮换是**你显式打开的**例外:
94
+
95
+ 1. 打开后,插件在可用的成员里按 **① 积分余额高者优先 → ② 快过期的积分优先用完 → ③ 平手时凭据更新鲜者优先** 排序,把得分最高的那个作为当前计费账号。
96
+ 2. **它与手动选择互斥**:自动轮换开启时,账号下拉**立即置灰**,并就地给出原因与一个「关闭自动轮换」的按钮。否则你会看到最坏的一种状态——**下拉里写着账号 A,实际计费的却是账号 B**。
97
+ 3. **轮换只在运行时决定用谁,绝不写回你的手动选择**(不写 `config.accounts[region]`)。所以关掉开关后,你上次手动选的账号原封不动地回来。
98
+
99
+ **每次轮换都在卡片上留下一条记录**(何时、从谁到谁),而不是悄悄发生。
100
+
101
+ ### 自动测试的时间间隔
102
+
103
+ 定时器每 60 秒走一次,按你设定的间隔(**5 ~ 1440 分钟,默认 30**)执行一轮「测试所有账号」。下限是 5 分钟而非更短,是刻意的:更密只会把整池打成限流。
104
+
105
+ **一个容易忽略的边界**:若你指定的模型已离开目录,定时轮次**不会就此停摆**——它会回落到该区域的免费模型继续刷新测量。这必须如此:成员能否回池靠的是**新的探测结果**(见下),一个陈旧的显示偏好不该把整池的测量冻住。手动动作仍然拒绝,理由同上。
106
+
107
+ ### 账号为什么出池,以及什么时候回来
108
+
109
+ | 探测结果 | 对轮换的意义 |
110
+ | --- | --- |
111
+ | 可用 | 可入选 |
112
+ | 被限流 | **暂时**出池,按上游给出的重置时间自动回池(上游没说时间就如实说没说,**不编造倒计时**) |
113
+ | 积分耗尽 | 出池,按月周期包的刷新时间回池 |
114
+ | 凭据被拒 / 模型不存在 / 连不上上游 / 失败 | 出池,直到下一次探测刷新结果 |
115
+
116
+ 关键一条:**「被限流」不等于不可用**。它恰恰是最值得等的那一类状态——把它当成永久失效剔除,正好丢掉池子里本来最能用的账号。
117
+
118
+ ### 保存与草稿:为什么卡片上有两个「保存」
119
+
120
+ 模型管理一个、账号池一个,这是**刻意保留**而非漏合并:两者的草稿域不同,合并后一次写入失败会连累另一边(模型目录写不进去,就连池偏好也存不了);一个共享的「放弃」还会在一次误点里同时丢掉两份草稿。两边都是**验证写入**——**写不成功不丢弃草稿**,否则用户的编辑就永久丢了。
121
+
122
+ 池里还有一类状态**不走草稿**:探测结果、冷却时间、当前轮换到的账号。它们是**观测事实**,由插件自己高频写入。原因很实际——草稿是覆盖式的,若观测结果也进草稿,你手工点一次「保存」就可能把定时器刚写进去的结果**回退成几分钟前的旧值**。
123
+
62
124
  ## 安装
63
125
 
64
126
  > ⚠️ **版本要求:DSH 0.1.7-rc.1 及以上(当前窗口:`0.1.7-rc.1` ~ `0.2.x`)。** 自 v2.1.0 起本插件只支持 DSH 0.1.7-rc.1 及以上的宿主(旧版宿主无法通过依赖解析安装本版);0.1.5 及更早的宿主请停留在 v2.0.15。自 v2.1.2 起上界放宽为 **`<0.3.0-0`**——**0.2.0 全线(含正式版)已实测支持**,0.3.0 线则被明确拒绝。详见下方[受支持的宿主范围](#受支持的宿主范围)。