dsh-connect-workbuddy 4.5.1 → 4.6.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 +96 -0
- package/docs/assets/quota-notice-inline-mock.html +96 -0
- package/docs/assets/quota-notice-inline-mock.png +0 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +436 -158
- package/lib/{host-heartbeat-Vd2tNdsv.js → host-heartbeat-BxBvvAZq.js} +1 -1
- package/lib/index.d.ts +31 -12
- package/lib/index.js +182 -88
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,101 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.6.0 (2026-10-15)
|
|
4
|
+
|
|
5
|
+
> 版本号是 **minor** 而不是 patch:这一版新增了**用户可见的界面结论与控件**(池级「模型整体繁忙」判断、积分弹窗的打开期自动刷新、额度提示的呈现位置调整),按语义化版本属于特性。主线是**三处「界面上说了错话」的修正** —— 窗口左上角那个本来没有的方框、池卡片把模型问题说成账号问题、以及同一个账号在两个界面上状态不一致。三处都不会让测试或 CI 变红,只有人看界面才会发现,这也是它们能一路发出去的原因。
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- **「状态不合拍」的真正根因:同一个账号,积分弹窗和账号池卡片说出了两个不同的状态**(维护者第二次报同一句话,这次带了三张**同一秒**的截图)
|
|
10
|
+
- **先证明这不是「先后两个时刻」的错觉**:维护者这次附的两张卡片截图,文件写入时间都是 **17:12:47**,同一个面板上的两个视图在**同一秒**画出了不同的世界 —— 池卡片说 `dingxiyue` 是「模型繁忙 · 10/15, 17:13 之后可再用 · 约 2 分钟后」,积分弹窗对**同一个账号**一个字都没画。同一份 store、同一条 `exclusionOf` 规则,两边唯一能不同的只有**时间**。
|
|
11
|
+
- **三个各自独立的缺陷叠成了这一张图**,逐个修:
|
|
12
|
+
- **① 弹窗的表格是一次性快照,开完就不动了。**`fetchAccountCredits` 只在 `[open, activeRegion]` 变化时跑一次,而池卡片是每 60 秒重新排一次(`POLL_INTERVAL_MS`)。`dingxiyue` 那条 `live-request` 失败发生在 **17:11:47**,弹窗的读取早就结束了,所以它画不出这个标记 —— 两边对**各自手上那份数据**都没说错,这正是它难读的原因。改为在面板打开期间按 **60 秒**(卡片同一节奏)重读,最后**一次读完之后**才起定时器(避免与打开时那次读重叠)。**为什么不用读数自己的 5 分钟**:这张表要渲染的最短冷却就是 `MODEL_BUSY_COOLDOWN_MS`(2 分钟),5 分钟的周期意味着它能在卡片已经改口之后**继续谎报「模型繁忙」最多 4 分钟**。关闭面板即停(关闭后不再读,成本规则不变)。
|
|
13
|
+
- **② 写着「刷新」的按钮刷新的不是它所在的那张表。**它只调 `fetchUsage`,于是按下去只动了触发器上的余额,**表格每一行(连标记一起)原封不动**。按钮就在账号表的表头里,用户按它的意思是「刷新这张表」;一个刷新别处的东西比没有这个控件更糟。改为 `refresh()` 并发刷新两半(读数 + 表格),因为 `fetchUsage` 自己吞掉失败、不会抛,所以一半出错也不会让另一半被跳过。
|
|
14
|
+
- **③ 上游明明说了时间,却被丢掉了。**`dingxiaoman2016` 存下来的报文是 code 6004 的**英文**写法:`usage exceeds frequency limit, but don't worry, your usage will reset at 2026-10-16 11:34:04 UTC+8, …`。而 `UPSTREAM_RESET_PATTERN` 要求中文关键词 `重置`,于是 `parseUpstreamResetAt` 返回 `undefined` → `cooldownOf` 返回 `{}` → 记录**没有** `retryAtMs`,尽管它自己的 `message` 就写着那一刻。卡片于是渲染「被限流 · **上游未给出何时恢复**」,而 `retryDue` 退回 30 分钟的 `POOL_UNKNOWN_COOLDOWN_MS`,把账号提前约 **18 小时**放回候选池,再花一次请求重新撞上同一堵墙。这是「诚实缺席」契约坏在了**更糟的那一侧**:不是编了一个时间,而是**把真的时间扔了**。
|
|
15
|
+
- **英文写法不是个例**:issue #63 的正文里就带着同一句式(`will reset at 2026-10-14 23:28:38 UTC+8`),所以这是这项服务**真的在用**的第二种措辞。两种措辞的**标记位置相反**(中文 `重置` 在时间戳**后面**,英文 `reset at` 在**前面**),所以不是一个正则放宽就能覆盖的 —— 拆成 `UPSTREAM_RESET_PATTERN_ZH` / `_EN` 两条,共用一个 `RESET_STAMP` 片段,避免两份时间戳形状各自漂移。英文那条仍然**锚定在时间戳紧前面**,与中文条一样严格。
|
|
16
|
+
- **存量记录在读取时被治好**(`retryAtMsOf`,`src/account-pool-store.ts`):光修解析器只能救**以后**的写入,而维护者屏幕上那条记录已经躺在文件里了。`readPoolProbes` 是所有消费者拿到 probe 的**唯一入口**(`accountExclusions` 弹窗 / `poolMembersOf` 卡片**和故障转移路由** / `memberNames`),在这里治一次就全部生效;分散到三个消费点重推就是同一规则的三份拷贝,而且**以后新加的消费者会静默漏掉**。这不是永久的第二套推导:`writePoolProbes` 是「读→合并→写」,所以**下一次写入就把治好值落盘**。
|
|
17
|
+
- **只治 `rate-limited` / `out-of-credit` 两种结局**,因为只有这两种的语义**存在**「上游说的时间」。放宽它会出事:`AccountPool.tsx` 会渲染任何存在的 `retryAtMs`,给一个 `ok` 的账号补上时间就会显示「可用 · <某时刻> 之后可再用」—— 在一个**什么都没在等**的健康账号上。`model-busy` 同理被排除:它的冷却是一个本地量级,永远不是报文里的时间戳。
|
|
18
|
+
- **守卫**:新增 `tests/state-mismatch-surfaces.spec.ts`(10 例,用**逐字的真实英文 6004 报文**,裁剪过的 fixture 证明不了任何事)与 `tests/composer-panel-freshness.spec.tsx`(8 例,**执行真实组件** + 假定时器,因为这两个缺陷都是「哪段代码在哪个时刻跑」,正则守卫会被改写绕过)。**变异验证**(逐个改回、各自变红):① 解析器去掉英文分支 → 红 **5** 例;② 关掉读取时治疗 → 红 **3** 例;③ 去掉结局范围限制 → 红 1 例(这条是安全属性);④ 恢复「只在打开时读一次」→ 红 1 例;⑤ 刷新按钮改回只调 `fetchUsage` → 红 1 例;⑥ 关闭面板不清定时器 → 红 1 例;⑦ 把在途守卫改成无条件(**连用户的手动刷新一起吞掉**)→ 红 1 例;⑧ 彻底删掉在途守卫 → 红 1 例。**注意**:新测试用 `vi.useFakeTimers({ toFake: ['setInterval', 'clearInterval'] })` —— 默认连 `setTimeout` 一起假掉会**死锁共用 harness 的 `settle()`**(它靠真实 `setTimeout(…, 0)` 排空 React)。
|
|
19
|
+
- **一个「定时器守卫」的分寸**:表格的重读分两条路 —— **定时器**的问句是「有变化吗」,所以一次在途的读取已经回答了它,**跳过**这次 tick;而**用户按下的动作**(刷新按钮、`switchTo` 写完之后的重读)**必须真的发生**,所以带 `force` 并且认领最新的 generation。这个区分不是洁癖:定时器读得慢时用户按下刷新,如果被「已有读取在跑」吞掉,控件就是在**没有问过上游**的情况下宣称「表是最新的」—— 与「刷新按钮刷新的是别的地方」属于同一类缺陷。**这一条是被变异验证逼出来的**:先写的版本守卫是无条件的,第 ⑦ 项变异(把 `!force &&` 去掉)暴露出它会吞掉手动刷新;第 ⑧ 项变异(彻底删掉守卫)又暴露出**「跳过 tick」那个测试本身是假的** —— 定时器只在打开时那次读**结束之后**才装上,所以把打开时那次的 promise 挂住,根本没有 tick 会来,那个用例无论如何都通过。改成挂住**第一次 tick** 的读取之后,第 ⑧ 项才真正变红。
|
|
20
|
+
- **顺带一条**:弹窗页脚的时间戳现在跟着**表格**那次读取走(`fetchAccountCredits` 里也写 `lastRefresh`)。原来它只反映读数那次读取,于是「刚刷过的表」可能顶着**一个没动过的时刻** —— 与那些冻住的标记是同一个「这数据还新吗」的问题。
|
|
21
|
+
|
|
22
|
+
- **数据损坏:账号池的 `modelId` 写入后被读丢,并被后续每次写入抹掉**(维护者报「账号池表和积分弹窗状态没对齐」时查出来的)
|
|
23
|
+
- **根因不是显示,是读写不对称**:`readPoolProbes` 的白名单里没有 `modelId`,而 `writePoolProbes` 是**读→合并→写回**。所以这个字段不是"看不见"——它会被**下一次写入永久抹掉**,之后再写也回不来。已复现:写入 `modelId` 后读回是 `undefined`。
|
|
24
|
+
- **为什么危险**:这不是少显示一个字段。#61 加这个字段是为了让「哪个模型的测量」可核对(`6004` 是「账号 × 模型」条件,同一时刻 A 账号在 `hy4-preview-f` 可用、`step-5-preview` 却返回 429/6004)。字段一丢,池子就把一次**单模型**的 `ok` 当成了**账号级**结论——正是这个字段要防的误读。卡片上的「未记录模型」只是它的可见症状。
|
|
25
|
+
- **改了什么**:`readPoolProbes` 补读 `modelId`;`isProbe` 补校验(非字符串丢弃,与 `accountName` 同理由:它作为标签进 UI)。
|
|
26
|
+
- **守卫**:`tests/account-pool-store.spec.ts` 新增 2 例。**关键点**:单次写入的往返测试**抓不到**这个 bug(第一次写入是完整的)——必须**第二次写入**才能暴露,所以第二例专门写「无关账号的写入不得破坏已存的 modelId」。**变异验证**:删掉补读那行 → 两例都变红。
|
|
27
|
+
|
|
28
|
+
### 改进
|
|
29
|
+
|
|
30
|
+
- **池级状态:所有成员都卡在同一个模型上时,卡片直接说「这是模型的问题,不是你的账号」(issue #64 的第 ① 条)**
|
|
31
|
+
- **报告者的问题**:池内 4 个账号全被同一模型 14003 挡住,卡片上**每一行都写着「模型繁忙」——每行都对,加起来却是错的**。用户读到的是「我的池子坏了」,于是去找一个不存在的坏账号(正是 #56 报告的那场搜寻),而换账号恰好是唯一救不了的动作 —— 限制跟随模型。
|
|
32
|
+
- **改了什么**:新增 `allMembersBlockedByModel()`(`src/client/pool-state.ts`)与两条文案(`row.poolModelWideBusy` / `Hint`)。命中时卡片渲染「所有成员都卡在同一个模型上 —— 这是模型的问题,不是你的账号」并建议**换模型**;未命中时仍是原来那条「账号池里目前没有可用的账号」。两支**刻意分开**:那条说的是**账号**不行,这条说的是**账号没问题、模型才是那堵墙**。模型级分支先判、优先胜出。
|
|
33
|
+
- **判据是 `every` 而不是 `some`**:必须有成员、且每个被排除的成员原因**都是** `model-busy`。一个账号被限流、其余是模型繁忙的池子**不算**模型级结论 —— 那个限流确实说的是那个账号,此时说「不是你的账号」就是同一类虚假安慰。
|
|
34
|
+
- **一条被证伪的注释与随之删掉的死代码**:最初把「非空」与「无可用成员」写成两个并列条件,但**后者不可达** —— 可用成员的 `excludedBy` 是 `undefined`,所以 `every(... === 'model-busy')` 在任何成员可用时已经为假。(已按「三个成员以内所有组合」穷举验证:`every` 成立且存在可用成员的状态数为 0。)留着它就等于留一段测不住的代码,所以删掉,把这条性质写在文档注释里而不是写成跑不到的分支。
|
|
35
|
+
- **守卫**:`tests/pool-state.spec.ts` 新增 6 例。**变异验证**:① `every`→`some` → 红 3 例(这条最关键,防止模型级结论在混合池上误报);② 删掉非空判断 → 红 1 例;③ 判定整张表而非成员 → 红 2 例。
|
|
36
|
+
|
|
37
|
+
- **额度失败的两条提示都改成可行动的中文**(维护者要求:「换号成功要显示切换了账号,换号失败要给原因并建议换模型」)
|
|
38
|
+
- **换号成功**:`failoverNoticeText` 由 `「xxx」was rate limited — this answer came from another account` 改为 `「xxx」被限流,已改用池中另一个账号回答`;`failoverReasonText` 整条改为中文(`被限流` / `积分已耗尽` / `登录已失效` / `该模型当前繁忙(不是账号问题)` 等)。**按维护者的选择,不点名接管账号**——代码原本就有一条明确注释反对点名(改名会诱导用户以为要去管理那个账号),这条理由保留。
|
|
39
|
+
- **失败计数**:`(after trying 3 accounts)` → `(已尝试 3 个账号)`。
|
|
40
|
+
- **换号失败时补「为什么没号可换」**:新增可选回调 `failoverShortfall`(shim 侧)与 `explainPoolShortfall`(宿主侧)。宿主用**与路由同一个** `exclusionOf` 读每个成员的去向,按原因归并计数,输出如 `池中已无可用账号(2 个账号被限流)`,并接一句 `建议换一个模型继续,或稍后等待该账号的额度恢复`。
|
|
41
|
+
- **为什么用「不要再换账号」的口径**:池子刚刚搜过所有成员,用户的下一个动作应该是**搜索替不了他做的那一个**。原本文案只说"请求失败",把四种补救(等额度/加账号/重新登录/换模型)全留给用户猜,而其中三种插件已经排除过了。
|
|
42
|
+
- **热路径代价为零**:`failoverShortfall` **只在请求已经失败之后**才问一次,成功路径完全不碰(有专门用例断言:成功时回调调用次数为 0)。回调抛错时**降级为不提原因**,绝不把真实的 429 换成插件自己的错误。
|
|
43
|
+
- **守卫**:`tests/shim-failover.spec.ts` 新增 5 例(原因入正文、失败类别不丢、宿主不给原因时保持原样、回调抛错时不污染、成功时不问)。`tests/pool-e2e.spec.ts` 新增 1 例源码守卫:确认接线存在、受池子开关门控、复用 `exclusionOf`(不得自己重推)、且失败路径不发起网络调用。**变异验证**(逐个改回、各自变红):① 回调异常泄漏进回复 → 红;② 删掉「建议换模型」 → 红;③ 成功路径也去问解释器 → 红 9 例;④ 解释器去掉池子开关门控 → 红;⑤ 解释器改用原始 `probe.outcome` 而非 `exclusionOf` → 红。
|
|
44
|
+
|
|
45
|
+
- **额度提示:把占用的 `shell.quota-notice` 座位还回去 —— 左上角那个「本来没有」的方框就是这个插件画的**(issue #63 的第二次后续;维护者看截图后的判定)
|
|
46
|
+
- **维护者看到的**:窗口**左上角**出现一个带边框的方框,内容正是宿主那句「当前请求的额度已用尽」加一句括号,而且不走。维护者说「这个本来没有的啊」—— **判断是对的,那确实是本插件画的。**
|
|
47
|
+
- **它是怎么来的(根因)**:上一版为了「接着宿主那句话补一句」,注册进了 `shell.quota-notice` 这条链。链的契约写得很清楚:**认领该 code 的条目是「接管整个界面」(take over the surface),不是给宿主界面加装饰** —— 全拒时宿主才渲染它自带的 `Toast`。于是:宿主的 `Toast`(`position:fixed; top:40px; left:50%`,会自动淡出)**根本没被渲染**,顶替它的却是个 `display:inline` 的 `span` —— 没有定位、没有居中、没有背景、不会淡出。它落在 `shell.overlay`(`AppFrame` 里一个 `absolute; inset:0` 的层)的**左上角**,一直留着。**「本来没有」是真的:那个坐标上原本是宿主的 Toast,被换掉之后就成了一个没人见过的盒子。**
|
|
48
|
+
- **改了什么**:`select` 由「认领 `QUOTA`」改为**一律返回 `null`**(新增导出 `selectQuotaNoticeClaim`,规则只有一处、只有一个测试)。全拒让链回到宿主自己的 `Toast`,**插件不再往 `shell.overlay` 里画任何东西**。组件本身只留那句括号:不再重绘宿主那句(上一版重绘它,才在对话里造出**第二份**同一句话),`-head` 类与 `quotaNotice.fallbackHead` 的渲染一并去掉 —— 宿主自己那句仍然一字未改地由宿主画。
|
|
49
|
+
- **为什么不改成「只加样式、不接管」**:做不到。链只有「接管」与「不接管」两种结果,没有「叠加」;想让那条括号跟着宿主的 Toast 走,就必须接管,接管就必然失去 Toast 的定位与淡出。所以选择归还座位,括号去**对话里**补 —— 那本来就是用户失败时真正在看的地方。
|
|
50
|
+
- **文案与样式**:`quotaNotice.supplement` 不变(`(部分模型另有每日额度,与积分无关)`);`.dsm-workbuddy-quota-notice` 仍是 `display:inline` 且**不许画盒子**;`.dsm-workbuddy-quota-notice-head` 删除(它存在的唯一理由是给被接管的那句话上色)。
|
|
51
|
+
- **守卫**:`tests/issue63-quota-notice.spec.tsx` 重写为 6 例,其中**两例是形态/决策守卫**:`selectQuotaNoticeClaim` 对任何 owner 都必须返回 `null`(**这条是本次修复的核心**),根节点必须是 `span`、**一个 `button` 都没有**、**恰好一个子节点**(第二个子节点就是宿主那句话又溜回来了)、且**不存在** `.dsm-workbuddy-quota-notice-head`。**变异验证**(逐个改回、各自变红):① 让 `select` 重新认领 `QUOTA` → 红;② 把宿主句子重新渲染进组件 → 红(连红 3 例);③ 加回 `-head` 类 → 红。**注意**:上一版的文案守卫**全部**在出问题的版本上照样绿 —— 文案对、形态错,这就是那个方框能发出去的原因。
|
|
52
|
+
- **产物**:`docs/assets/quota-notice-inline-mock.html` + 同名 PNG 重做,新增「v2 占用 overlay 后的真实落点」一栏,用 `absolute; inset:0` 的容器**复刻**出维护者截图里那个左上角落点,并并排给出「归还座位后宿主 Toast 回来」的样子。
|
|
53
|
+
|
|
54
|
+
### 已核对(不是本次改动,是维护者的第二个问题)
|
|
55
|
+
|
|
56
|
+
- **「额度已用尽时自动换池子里的账号继续」这条链路是通的 —— 逐环节复核并补了**端到端**守卫**(`tests/quota-auto-switch-chain.spec.ts`,6 例)
|
|
57
|
+
- **这条链的五环各有自己的测试**(分类 `upstream.spec.ts`、结果 `outcomeOfFailure`、冷却 `cooldownOf`/`quotaRefreshOf`、排名 `rankPool`、失败转移 `shim-failover.spec.ts`),但**没有一条测试把它们连起来跑**:换掉分类器,失败转移的用例照样绿(它自己造 `kind`);换掉 `exclusionOf`,分类用例照样绿(它不问池子拿这个结论做什么)。新文件把**上游真实报文**一路喂到「下一个请求会发给谁」,覆盖 `6004`(带 `将在 … UTC+8 重置` 的 429)与 `14018`(积分耗尽)两种拼写,并反向钉住 `14003` 是**模型级**冷却(`upstream-tips`,2 分钟)而非账号判决。**结论:通。** 两个前提同时成立才会不换号 —— **账号池开关打开**、且**池子里有第二个可用成员**;只有这一个账号时 `rankPool` 仍返回该行(卡片要能解释它为什么出局),但它不是候选,`failoverAccountFor` 因此返回 `undefined`,用户看到的就是带括号的那句失败提示。这一点也由新用例钉住。
|
|
58
|
+
- **顺带记一条容易误读的事实**:`14003` 的账号在排名里**确实**被记为 `model-busy` 排到候选之后(排在 `fresh` 之后,不是我之前以为的「仍排第一」)。真正阻止它换号的是另一道闸 —— `isFailoverWorthy` 按 `kind` 拒绝。两处是同一决定的两道防线,别只看排名就以为它会换号。
|
|
59
|
+
|
|
60
|
+
## 4.5.2 (2026-10-14)
|
|
61
|
+
|
|
62
|
+
> 本节含三件事,**一次发布**:先修了「每行下拉框挤坏布局 + 命名看不懂」,随后维护者判定**把这个下拉框连同逐账号设置整体撤掉**;并**新增两个一键遍历按钮**(对齐同族 trae 插件)——撤掉的是「逐账号指定测试哪一个模型」,加回的是「一次把模型测一遍」,两者不冲突。4.4.6 之后没有任何以 4.5.2 发布过的产物,所以合并为一版。
|
|
63
|
+
|
|
64
|
+
### Features
|
|
65
|
+
|
|
66
|
+
- **新增两个一键遍历按钮:「一键测试启用模型」与「一键测试所有模型」**(维护者要求在对比同族 `trae` 插件后补齐;`trae` 3.5.0 提供同一对按钮)
|
|
67
|
+
- **为什么这两个按钮与「3.0.0 删掉批量按钮」不矛盾**:3.0.0 删它是因为「一次覆盖整个已勾选列表就是一次不可控支出」,并**在路由层加固**(多于 1 个模型直接 400,防止陈旧 bundle 或手写客户端把按钮变成消费循环)。这一版**保留那道闸门**,把循环放在**卡片端**:一个模型一次请求、串行、每个结果一出来就显示 —— 也就是 `trae` 的做法。路由仍然只接受 1 个模型。
|
|
68
|
+
- **成本按维护者的选择写在提示里(不做二次确认)**:每次探测都发真实体积的请求,真花积分。**本机实测**(默认 25000 token 体积)每次约 **0.01 积分**,目录 16~20 个模型 → 「所有模型」一轮约 **0.16~0.2 积分**。历史实测曾达 0.02~0.72/次(体积不同),所以提示里写明「含未启用的模型,明显更贵」而不是只给一个数。
|
|
69
|
+
- **两个按钮的范围不同,这一点是成本控制而不是措辞**:「启用模型」= 用户勾选的那几个(尊重他刚做的选择,不花在他关掉的模型上);「所有模型」= 本区域 live 目录里的每一个。取的是**可见目录**而不是配置里的任意 id,所以不可能花在只存在于配置文件里的名字上。
|
|
70
|
+
- **串行而非并发**:探测的目的就是发现限流,一次把整个名册打出去是最容易制造限流的做法 —— 与宿主自己那条循环同样的理由。
|
|
71
|
+
- **凭据被拒时立刻停止并明说**:被拒的凭据对剩下每个模型都会给出同一个答案,继续跑只是花钱重复收集同一个答案。**模型级失败(如某模型被限流)不停** —— 那是关于那个模型的信息,停下来会把它藏掉。汇总只统计**实际测到**的数量,所以提前停止的一次遍历不会读成「整册全绿」。
|
|
72
|
+
- **顺带修掉一处过期注释**:`probeModels` 的文档还在描述「宿主端一次跑完整个批量」(那是 3.0.0 之前的形态),与新的事实(路由只收 1 个、由卡片端循环)相反,已更正。
|
|
73
|
+
- **守卫**:`tests/card-model-sweep.spec.tsx`(10 例)钉住三件事:**一个请求一个模型**(断言每次 `/probe` 请求体的 `modelIds` 长度为 1)、**两个按钮范围不同**(「所有」= 全部三个;「启用」= 只有勾选的,且断言**不含**未启用的那个)、**汇总只算测到的**(一个模型被限流时不得读成全绿)。另含:无启用项时「启用」按钮置灰而「所有」仍可用、凭据被拒只发一次请求、模型级失败不中断、跑完按钮恢复可用。**变异验证**:把「所有模型」改成只遍历启用项 → 2 例红;去掉凭据被拒的提前停止 → 2 例红。
|
|
74
|
+
- **布局**:模型区头部现在有三个按钮(刷新 + 两个遍历),中文标签较长(「一键测试所有模型」),所以按钮组允许换行且标签 `nowrap` —— 与之前账号池行那个「测试按钮被拆成两行」是同一类失败,这次先防住。
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
### Fixes(同一次发布,见上)
|
|
78
|
+
|
|
79
|
+
- **(已随本节撤除)账号池每行那个「测试模型」下拉框把行挤坏了:「测试」按钮被压成一列一个字(「测」/「试」),选项名也让人看不懂。**(维护者看截图发现,属 4.5.0 的 #61 改动引入)
|
|
80
|
+
- **布局**:那一行的动作列是按「只有两个按钮」的宽度设计的网格轨道(有签到列时仅 `.8fr`),4.5.0 往里塞了一个**约 150px、不可再压缩**的下拉框,于是按钮被挤到只剩一个字符宽 —— 每一行都变成两行「测 / 试」。**注意:这个缺陷的修法(加最小宽度轨道等)随后连同下拉框一起被撤掉了** —— 保留下来的是 `white-space:nowrap`,它独立成立:这个动作列的宽度是表格的一个分数轨,一个固定的两字标签(测试)不该被拆行,窄窗口下**裁剪**才是正确的失败方式。
|
|
81
|
+
- **文案**:那个下拉框的第一个选项原本叫**「区域目标」** —— 维护者看到它直接问「这目标区域是做什么用的?」,这本身就是命名失败的证据:它读起来像一个**区域**,而它表达的是「**跟随账号池的目标模型**」。当时先改为**「同账号池」**(en: `same as pool`)并把 `title` 重写为**先回答「它是做什么的」**;随后整个下拉框被撤除,这两个字符串也一并删掉了。**留下的教训是命名那条**:一个选项名如果让人需要问「这是做什么用的」,那它写的就是「值从哪来」而不是「它做什么」。
|
|
82
|
+
- **为什么这件事值得记**:这两处都不是逻辑错,而是**只有人看界面才会发现**的错 —— 自动测试全绿、CI 全绿、发布会成功。它提醒本仓库的验证手段(源码级守卫 + 变异验证)覆盖不了「读起来对不对、排得下排不下」,而这两类恰恰需要真机截图或使用者提问才能暴露。
|
|
83
|
+
- **守卫**:`tests/client-host-contract.spec.ts` 新增一例钉住 `nowrap`;随撤除一并改写为「不再为已移除的下拉框预留宽度」(见下)。**变异验证**:删掉 `nowrap` → 该例变红。
|
|
84
|
+
|
|
85
|
+
### 撤除
|
|
86
|
+
|
|
87
|
+
- **撤掉「逐账号测试模型」(`pool.targetModelIdByAccount`)—— 它是 4.4.6 采纳报告者提议时加进来的,维护者判定应撤。**(issue #61 的后续)
|
|
88
|
+
- **为什么撤**:**池设置里本来就有统一的测试模型**(`pool.targetModelId`),而报告的诉求(「你用 `step5` 就要测这个模型」)用它就能达成 —— 报告者当时把它配成 `hy4-preview-f`(实际在用 `step-5-preview`),**根因是那个统一设置指错了模型,不是缺少逐账号能力**。
|
|
89
|
+
- **更重要的理由**:逐账号让**同一张表的「全绿」不再可比** —— A 的 `ok` 是模型 X 上的、B 的 `ok` 是模型 Y 上的,而「全绿不代表可用」正是这个 issue 抱怨的东西。为了「方便对比某个号」引入一个削弱「整表可比」的设置,方向是反的。
|
|
90
|
+
- **撤掉的东西**:配置字段 `pool.targetModelIdByAccount`(含 schema)、`targetModelPreference()`、批量路由的**逐账号解析器**(`testAllAccounts` 回到「整批一个模型」的 `string` 形态,`WorkBuddyPoolDeps.test` 的联合类型一并收窄)、池文件与 usage 载荷里的逐账号映射、每行那个「测试模型」下拉框(含 `setRowModel`、draft 比较、写入与**写入后校验**、三个 locale 键)、以及为容纳它而加的网格最小宽度。
|
|
91
|
+
- **完整保留的东西(#61 里真正解决问题的那一半,不需要任何新配置)**:
|
|
92
|
+
1. 探针记录**写入 `modelId`**(`probeModel()` 一直返回它,此前落盘被丢掉),所以每行的结论**自带适用范围**;批量与实时聊天失败两条路径都写;
|
|
93
|
+
2. 每行**显示**「模型 X」(`poolProbeModel`),以及未记录时的显式说明;
|
|
94
|
+
3. `resolveTargetModel` 的**陈旧目标如实报告、绝不静默替换**(指向已下架模型时报 `stale`,不换一个用户没选的模型去花积分)。
|
|
95
|
+
- **升级影响**:老配置里若已写了 `targetModelIdByAccount`,升级后**被忽略**(未知字段),池的实际测试模型回到 `targetModelId` —— 也就是「所有号测同一个模型」,这正是撤除的目的。**不影响聊天用哪个模型**(那个设置从来不参与选路)。
|
|
96
|
+
- **守卫**:`tests/issue61-probe-model-scope.spec.ts` 重写为钉住「保留的那一半」+「整批只能一个模型」(`probes every account on the SAME model` 直接断言两次探测拿到同一个 id,若有人把它改回逐账号,这条会红);`tests/pool-route.spec.ts` 删掉逐账号解析器的路由断言与 `askedModel()` 联合类型收窄辅助函数;`tests/client-host-contract.spec.ts` 的布局守卫改写为「不再为已移除的下拉框预留宽度」(并保留 `nowrap`,它与下拉框无关、独立成立)。
|
|
97
|
+
- 全量 **1249 通过 / 2 跳过**。
|
|
98
|
+
|
|
3
99
|
## 4.5.1 (2026-10-14)
|
|
4
100
|
|
|
5
101
|
### Fixes
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN"><head><meta charset="utf-8">
|
|
3
|
+
<title>额度提示 · 撤掉 overlay 占用,只在对话里补充</title>
|
|
4
|
+
<style>
|
|
5
|
+
:root{
|
|
6
|
+
--bg:#141519; --layer:#1b1c21; --line:#2c2d31;
|
|
7
|
+
--label-primary:#e6e6e6; --label-secondary:#c6c9d0; --label-tertiary:#8b8f98;
|
|
8
|
+
--error:#e5484d; --warn:#f5a524;
|
|
9
|
+
--font:-apple-system,"PingFang SC","Microsoft YaHei",system-ui,sans-serif;
|
|
10
|
+
--mono:ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
11
|
+
}
|
|
12
|
+
*{box-sizing:border-box}
|
|
13
|
+
body{margin:0;padding:28px 32px 48px;background:var(--bg);color:var(--label-primary);font-family:var(--font);font-size:14px;line-height:22px}
|
|
14
|
+
.wrap{max-width:880px;margin:0 auto}
|
|
15
|
+
h1{font-size:15px;font-weight:600;margin:0 0 4px}
|
|
16
|
+
.sub{color:var(--label-tertiary);font-size:12px;line-height:18px;margin:0 0 22px}
|
|
17
|
+
.block{border:1px solid var(--line);border-radius:10px;background:var(--layer);padding:16px 18px 18px;margin:0 0 16px}
|
|
18
|
+
.block > .cap{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--label-tertiary);margin:0 0 12px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
19
|
+
.cap .tag{font-family:var(--mono);font-size:10px;letter-spacing:0;text-transform:none;padding:1px 6px;border:1px solid var(--line);border-radius:20px;color:var(--label-secondary)}
|
|
20
|
+
.old{border-color:#5a2327;background:#1d1416}
|
|
21
|
+
.old .cap{color:#e78387}
|
|
22
|
+
.strike{text-decoration:line-through;opacity:.9}
|
|
23
|
+
|
|
24
|
+
/* 宿主当前渲染:本轮运行失败 行(复刻,未改动) */
|
|
25
|
+
.turnErrorRow{display:grid;grid-template-columns:10px minmax(0,1fr) auto;align-items:start;gap:8px;padding:2px 0;font-size:13px;line-height:20px}
|
|
26
|
+
.turnErrorDot{width:6px;height:6px;border-radius:50%;background:var(--error);margin-top:7px;justify-self:center}
|
|
27
|
+
.turnErrorCopy{min-width:0;overflow-wrap:anywhere}
|
|
28
|
+
.turnErrorTitle{color:var(--error);font-weight:600;margin-right:6px}
|
|
29
|
+
.turnErrorMessage{color:var(--label-secondary)}
|
|
30
|
+
.turnErrorCode{color:var(--label-tertiary);font-family:var(--mono);font-size:11px}
|
|
31
|
+
|
|
32
|
+
/* ---- 本插件现在真实渲染的样式(与 src/client/styles.ts 一致) ---- */
|
|
33
|
+
.dsm-workbuddy-quota-notice{display:inline}
|
|
34
|
+
.dsm-workbuddy-quota-notice-body{color:var(--dsw-alias-label-tertiary,var(--label-tertiary));font-size:12px}
|
|
35
|
+
|
|
36
|
+
/* 被否决的旧对话框(仅作对照) */
|
|
37
|
+
.modal{max-width:420px;padding:10px 12px;border-radius:8px;background:#23242a;border:1px solid var(--line);font-size:12px;line-height:17px}
|
|
38
|
+
.modal .head{font-weight:600;color:var(--label-primary)}
|
|
39
|
+
.modal .body{color:var(--label-secondary);margin-top:4px}
|
|
40
|
+
.modal .btn{margin-top:8px;padding:2px 10px;border:1px solid var(--line);background:transparent;color:var(--label-secondary);border-radius:6px;font:inherit;font-size:12px}
|
|
41
|
+
|
|
42
|
+
/* 宿主的通用 Toast(本轮改动后它回来了)。定位取自 Toast.module.css。 */
|
|
43
|
+
.hostToast{position:relative;width:max-content;max-width:min(640px,100%);display:flex;align-items:center;gap:10px;padding:12px 16px;border-radius:12px;background:#2b3038;color:#e6e6e6;font-size:14px;line-height:22px;box-shadow:0 8px 28px rgba(0,0,0,.45)}
|
|
44
|
+
.hostToast .ic{display:grid;place-items:center;flex:none;color:var(--warn)}
|
|
45
|
+
|
|
46
|
+
/* overlay 层的复刻(AppFrame:absolute + inset:0),用来展示被占用时的落点 */
|
|
47
|
+
.overlaySim{position:relative;height:96px;border:1px dashed var(--line);border-radius:8px;background:#17181c;overflow:hidden}
|
|
48
|
+
.overlaySim .seatLabel{position:absolute;right:8px;bottom:6px;font-family:var(--mono);font-size:10px;color:var(--label-tertiary)}
|
|
49
|
+
</style>
|
|
50
|
+
</head><body><div class="wrap">
|
|
51
|
+
|
|
52
|
+
<h1>额度提示 · 撤掉 overlay 占用,只在对话里补充</h1>
|
|
53
|
+
<p class="sub">宿主那句「当前请求的额度已用尽」由宿主自己画,插件不再抢它的座位。插件只保留那句括号。<b>注意标题里的「本轮运行失败」和它后面那句,都是宿主画的,不是本插件的输出。</b></p>
|
|
54
|
+
|
|
55
|
+
<div class="block old">
|
|
56
|
+
<p class="cap"><span class="strike">已撤除</span><span class="tag">v1 · shell.quota-notice 卡片</span></p>
|
|
57
|
+
<div class="modal">
|
|
58
|
+
<div class="head">当前请求的额度已用尽</div>
|
|
59
|
+
<div class="body">部分模型另有每日额度限制,与积分余额无关 —— 也可能确实是本账号积分耗尽。账号池那一行会写明上游给出的恢复时刻;也可以先换个账号或换个模型继续。</div>
|
|
60
|
+
<button class="btn">知道了</button>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="block old">
|
|
65
|
+
<p class="cap"><span class="strike">已撤除</span><span class="tag">v2 · 占用 overlay 后的真实落点</span></p>
|
|
66
|
+
<div class="overlaySim">
|
|
67
|
+
<span class="dsm-workbuddy-quota-notice" role="status"><span class="dsm-workbuddy-quota-notice-head">当前请求的额度已用尽</span><span class="dsm-workbuddy-quota-notice-body">(部分模型另有每日额度,与积分无关)</span></span>
|
|
68
|
+
<span class="seatLabel">shell.overlay(absolute, inset:0)</span>
|
|
69
|
+
</div>
|
|
70
|
+
<p class="sub" style="margin:10px 0 0">这就是维护者截图里的左上角方框:占用链座位后,宿主的 Toast 不再渲染,而这个 <code>span</code> 既没有 <code>position:fixed</code> 也没有居中、没有背景、不会淡出,于是停在 overlay 的左上角不走。</p>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="block">
|
|
74
|
+
<p class="cap">现在 · 宿主的 Toast 回来了<span class="tag">链座位已归还</span></p>
|
|
75
|
+
<div style="display:flex;justify-content:center">
|
|
76
|
+
<div class="hostToast"><span class="ic">⚠</span><span>当前请求的额度已用尽</span></div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="block">
|
|
81
|
+
<p class="cap">现在 · 对话里的失败行<span class="tag">宿主画行,插件只补括号</span></p>
|
|
82
|
+
<div class="turnErrorRow">
|
|
83
|
+
<span class="turnErrorDot"></span>
|
|
84
|
+
<span class="turnErrorCopy">
|
|
85
|
+
<span class="turnErrorTitle">本轮运行失败</span><span class="turnErrorMessage">当前请求的额度已用尽<span class="dsm-workbuddy-quota-notice" role="status"><span class="dsm-workbuddy-quota-notice-body">(部分模型另有每日额度,与积分无关)</span></span></span>
|
|
86
|
+
</span>
|
|
87
|
+
<code class="turnErrorCode">QUOTA</code>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="block">
|
|
92
|
+
<p class="cap">英文<span class="tag">en locale</span></p>
|
|
93
|
+
<p style="margin:0">Request quota exhausted.<span class="dsm-workbuddy-quota-notice" role="status"><span class="dsm-workbuddy-quota-notice-body"> (some models also have a daily limit that is unrelated to your credits)</span></span></p>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
</div></body></html>
|
|
Binary file
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, K as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, Q as findWorkbuddyAppExecutableWithSource, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, ot as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-
|
|
2
|
+
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, K as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, Q as findWorkbuddyAppExecutableWithSource, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, ot as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-BxBvvAZq.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|