dsh-connect-workbuddy 5.3.0 → 5.3.1

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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.3.1 (2026-10-22)
4
+
5
+ > 补丁版本:5.3.0 已经发布了负载徽标,这一版修它的**消失**问题,并把它的**呈现方式**换成更好认的样子。 **没有新增任何功能**:模型目录、请求路径、provider 路由、计费与凭据一律未变,5.3.0 的全部能力原样保留。
6
+
7
+
8
+ ### Fixes
9
+
10
+ - **负载徽标不再「刚出现就消失」**:等级以前只活在**运行时目录**里,而任何一次配置变更(切区、改模型选择、改思考档、开关、换号)都会触发 `applySelection()` 用**已保存的 `lastCatalog` 重算目录**——而 `lastCatalog` 里没有 `busyLevel`(负载**刻意不落盘**:`src/busy.ts` 写着「存下来的负载等级是谎言」)。于是你一动任何设置,徽标就被抹掉,上游其实什么都没变。 **现在改为「重放后重新问一次」**:探针只读、免费、整份名单约 190 ms,徽标因此在设置变更后活下来。 不落盘这件事仍然保留——负载是**这一分钟**的事实,跨重启后那个数字可能早就过期了,比不显示更危险。 探针失败照旧只是少一枚徽标,绝不会连模型列表一起丢。
11
+
12
+ - **负载徽标改用图标**:拥堵不再是四个字,而是一枚一眼能认的标记。
13
+ - **四种状态、四种标记**:**正常 `normal` = 1 个黄人**、**轻微拥堵 `busy` = 2 个橙人**、**极度拥堵 `congested` = 3 个红人**、**空闲 `idle` = 一枚绿色火箭**(没人排队 = 可以全速跑)。 **人数 = 排队的人**,**颜色 = 严重程度**(绿→黄→橙→红是眼睛本来就认识的顺序,扫一眼就能比出两行谁更堵)。 空闲之所以是**另一种形状**而不是「绿色的 1 个人」:在 12px 下「没人等」和「有 1 个人在等」必须一眼分开,光靠颜色不够。
14
+ - **小人跟 `🧍` emoji 是同一个人**:黄头发(带刘海)、肤色脸和白上衣、灰裤、黑鞋。 **换色只改「上衣+裤子」**,头发和皮肤保留本色,所以每一档都是「同一个人换了件衣服」。 **五官不画**——模型行里高度只有 12px,眼睛画出来只有 0.2px,是噪点不是脸,靠轮廓认人。 横向步进 9.6 个单位(约半个身位),肩挨肩,**高度固定、只向右变宽**,不会被压扁。
15
+ - **模型选择器里也用彩色小人**:宿主提供了 `conversation.input.model.label` 插槽(契约原文:「渲染非交互的内联内容,例如带颜色的名称或徽标」;官方自己就用它挂 DeepSeek 的「1017」发布徽标),插件在那里注册了 SVG 标记。 **模型名里不再挂 emoji**——那会变成同一件事用两套视觉语言说两遍;emoji 的颜色本来就由字体决定(永远是黄的),拿不到绿/黄/橙/红。 插槽里**保留目录里的模型名**,只在旁边加徽标;**别人的提供方一个徽标都不画、连请求都不发**;徽标按 provider 取**对应区域**的等级(`workbuddy`→cn、`workbuddy-global`→global,否则会把一个网关的负载贴到另一个网关的模型上)。
16
+ - 颜色跟随主题,并**保留 `title` 悬停文字**(「极度拥堵」四个字还在),所以省地方但不丢信息。
17
+
18
+ ### Tests
19
+
20
+ - 新增 `tests/busy-badge.spec.ts`(8 条)、`tests/busy-icon-card.spec.tsx`(11 条)与 `tests/model-label-slot.spec.tsx`(5 条),覆盖徽标的合并语义、图标的人数与颜色、以及插槽的「别人的提供方不画、模型名要保留、没等级就不画」。
21
+ - 全量:**119 文件 / 1946 条通过(另 2 条 skip)**;两个 tsconfig 均 exit 0;build 成功。
22
+
3
23
  ## 5.3.0 (2026-10-22)
4
24
 
5
25
  > 新增能力属 **minor**,本次定版 **5.3.0**。**默认关闭,关闭时插件行为与 5.2.3 完全一致**:不多一个工具、不多一条指令、不多一个请求,provider 路由、模型、凭据与计费一律未变。
@@ -68,7 +88,7 @@
68
88
  - 换号:只有 401 才换、**超时/崩溃不换**、最后一条 401 也不重试、空凭据列表报 `no credential worked`
69
89
  - 脱敏:token 与 `Bearer …` 形态既不进正文也不进 errors
70
90
  - 主 spec 的新增/重写部分:`resolveWorkingDir` 的 9 条真目录断言(不存在 / 是文件不是目录 / 相对路径 / 允许根之外 / `allowedDirs` 放行 / 前缀相同的兄弟目录拒绝)、`TurnGate` 的 7 条(假时钟注入、并发 busy、配额拒绝、窗口滑过恢复、`0` = 不限、`clear()`)、`resolveCliEntry` 的新签名与「显式路径失败不得回退 PATH」、`looksLikeAuthFailure` 的新签名。
71
- - 全量:**116 文件 / 1920 条通过(另 2 条 skip)**;`tsc -p tsconfig.json` 与 `tsc -p tsconfig.client.json` 均 exit 0;`tsdown` build 成功(`lib/index.js` 446.54 kB)。
91
+ - 全量:**118 文件 / 1935 条通过(另 2 条 skip)**;`tsc -p tsconfig.json` 与 `tsc -p tsconfig.client.json` 均 exit 0;`tsdown` build 成功(`lib/index.js` 451.31 kB)。
72
92
  - **另做了一轮真机联调**(`@tencent-ai/codebuddy-code` 2.171.0,真实 vault 账号,非 mock),六项全过:①`dontAsk` 默认 9.9 秒一轮,Glob/Bash 放行、Write 被拒且如实汇报;②`acceptEdits` 下 Bash 被拒后 CLI 换 PowerShell 完成任务(12.2 秒、`ok:true`),验证 `interrupt` 不能对所有工具开;③`plan` 的 `ExitPlanMode` 被带 `interrupt:true` 拒掉后 12.8 秒结束,不再挂满 `timeoutSecs`;④`acceptEdits` 真的写文件,且落在我们指定的 cwd(`hello.txt` 内容一字不差);⑤6 秒超时 → 6.0 秒抛 `codebuddy exceeded its 6s budget`,**事后无残留 codebuddy 进程**(`taskkill /T` 生效);⑥凭据链真实可用:global 令牌得到 `401 Authentication required`、换 cn 后 8.1 秒成功,且全程 token 未进正文/errors。
73
93
 
74
94
  ### Tests(同期并入 main:issue #102 / #108)
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as defaultDesktopAuthCandidates, Ct as findWorkbuddyAppExecutableWithSource, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, _t as WORKBUDDY_APP_EXECUTABLE_ENV, a as readHostHeartbeat, b as WorkBuddyCredentialStore, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, jt as hostPeersInfo, k as legacyWorkbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, w as defaultDesktopAuthDirs, y as WORKBUDDY_AUTH_FILE_ENV } from "./host-heartbeat-_EvGiAek.js";
2
+ import { At as hostPeersInfo, C as defaultDesktopAuthCandidates, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, St as findWorkbuddyAppExecutableWithSource, a as readHostHeartbeat, b as WorkBuddyCredentialStore, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, gt as WORKBUDDY_APP_EXECUTABLE_ENV, k as legacyWorkbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, w as defaultDesktopAuthDirs, y as WORKBUDDY_AUTH_FILE_ENV } from "./host-heartbeat-BHESQ6Ah.js";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/bin.ts
package/lib/client.js CHANGED
@@ -3952,6 +3952,221 @@ window.__ModuleLoader__.load({
3952
3952
  });
3953
3953
  }
3954
3954
  //#endregion
3955
+ //#region src/client/busy-icon.tsx
3956
+ /** The word for a level, in the card's language, for the tooltip. */
3957
+ const TITLES_ZH = {
3958
+ idle: "空闲",
3959
+ normal: "正常",
3960
+ busy: "轻微拥堵",
3961
+ congested: "极度拥堵",
3962
+ unavailable: "不可用"
3963
+ };
3964
+ const TITLES_EN = {
3965
+ idle: "Idle",
3966
+ normal: "Normal",
3967
+ busy: "Slightly congested",
3968
+ congested: "Extremely congested",
3969
+ unavailable: "Unavailable"
3970
+ };
3971
+ /**
3972
+ * The CLOTHES colour per level, as a CSS custom property.
3973
+ *
3974
+ * These are the ramp: green → yellow → orange → red. The hair and the skin
3975
+ * keep their own colours in every level, so a row is recognisably the same
3976
+ * person in a different outfit — which is what makes the COUNT readable as
3977
+ * "more people, not different people".
3978
+ */
3979
+ const COLORS = {
3980
+ idle: "var(--dsm-workbuddy-busy-idle, #3aa76d)",
3981
+ normal: "var(--dsm-workbuddy-busy-normal, #d9b300)",
3982
+ busy: "var(--dsm-workbuddy-busy-busy, #e08a1e)",
3983
+ congested: "var(--dsm-workbuddy-busy-congested, #d93a2b)",
3984
+ unavailable: "var(--dsm-workbuddy-busy-unavailable, #8a8f98)"
3985
+ };
3986
+ /** How many people each level draws. */
3987
+ const PEOPLE = {
3988
+ normal: 1,
3989
+ busy: 2,
3990
+ congested: 3
3991
+ };
3992
+ /**
3993
+ * The icon for a level, or `undefined` when the level is not drawn.
3994
+ *
3995
+ * Only `unknown` is left out: it is the plugin's own "the probe did not
3996
+ * answer", and a badge that guesses is worse than no badge. Every level the
3997
+ * upstream DID report is drawn — `idle` included, in green, because "the model
3998
+ * is free right now" is useful information.
3999
+ */
4000
+ function busyIconOf(level) {
4001
+ if (level === void 0) return void 0;
4002
+ if (level === "unknown") return void 0;
4003
+ return level;
4004
+ }
4005
+ /**
4006
+ * One person, on the icon's own grid: 7.9 units wide, 19.3 units tall.
4007
+ *
4008
+ * The five parts are the emoji's own — hair, face, shirt, trousers, shoes —
4009
+ * because a stick figure would not read as the same mark as the `🧍` the
4010
+ * picker shows.
4011
+ *
4012
+ * NO FACE FEATURES. At the 12px the model row actually uses, an eye is 0.2px
4013
+ * wide: drawing it produces a smudge, not a face. So the face is a plain
4014
+ * skin-toned shape and the character is carried by the silhouette.
4015
+ */
4016
+ function person(x, key, shirt, shirtEdge, trousers) {
4017
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
4018
+ transform: `translate(${x} 0)`,
4019
+ children: [
4020
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4021
+ d: "M2.7 3.1c0-1.6 1-2.6 2.5-2.6s2.5 1 2.5 2.6c0 .8-.2 1.4-.6 1.9-.2-1-.7-1.5-1.3-1.7-.5.6-1.3.8-2 .7-.7-.1-1.2-.6-1.3-1.4-.5.1-.8.2-.8.2z",
4022
+ fill: "#f0c04a"
4023
+ }),
4024
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4025
+ d: "M3 3.4h3.4v2.1c0 1.15-.76 2-1.7 2s-1.7-.85-1.7-2z",
4026
+ fill: "#ffd9a0"
4027
+ }),
4028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4029
+ d: "M4.4 6.6h.6v.9h-.6z",
4030
+ fill: "#f3c493"
4031
+ }),
4032
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4033
+ d: "M1.75 8.9c-.45.45-.65 1.25-.65 2.15v2.9c0 .4.3.7.7.7s.7-.3.7-.7v-2.8c0-.6.1-1 .25-1.35zM7.55 8.9c.45.45.65 1.25.65 2.15v2.9c0 .4-.3.7-.7.7s-.7-.3-.7-.7v-2.8c0-.6-.1-1-.25-1.35z",
4034
+ fill: "#ffd9a0"
4035
+ }),
4036
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4037
+ d: "M2.8 7.7h3.7c.75 0 1.3.55 1.3 1.3v5.1H1.5V9c0-.75.55-1.3 1.3-1.3z",
4038
+ fill: shirt
4039
+ }),
4040
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4041
+ d: "M3.9 7.7l.75 1 .75-1",
4042
+ stroke: shirtEdge,
4043
+ strokeWidth: ".45",
4044
+ fill: "none",
4045
+ strokeLinejoin: "round"
4046
+ }),
4047
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4048
+ d: "M1.9 14.1h2.05l-.3 4.3H1.6zM5.85 14.1h2.05l.3 4.3H5.5z",
4049
+ fill: trousers
4050
+ }),
4051
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4052
+ d: "M2.15 18.4h1.4v.9H1.7c-.3 0-.45-.3-.3-.6zM5.85 18.4h1.4l.55.55c.2.2 0 .35-.3.35H5.85z",
4053
+ fill: "#2f3136"
4054
+ })
4055
+ ]
4056
+ }, key);
4057
+ }
4058
+ /**
4059
+ * A ROCKET, for the `idle` level: nobody is queuing, so the model is free to
4060
+ * run at full speed.
4061
+ *
4062
+ * A different SHAPE, not a different colour, and that is the point: the level
4063
+ * has to be readable at 12px without reading a word, and a rocket next to one,
4064
+ * two or three people separates "nobody is waiting" from "one person is
4065
+ * waiting" instantly. The people count the queue; the rocket says there is
4066
+ * none.
4067
+ *
4068
+ * SOLID, not outlined: a stroked rocket at this size turns into a smudge. The
4069
+ * body is filled in the level's colour, the window is white so it reads at
4070
+ * 12px, and the flame is a fixed orange (a flame is a flame).
4071
+ */
4072
+ function rocket(color) {
4073
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [
4074
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4075
+ d: "M6.1 15.6c.2 1.6.8 2.8 1.9 3.7 1.1-.9 1.7-2.1 1.9-3.7z",
4076
+ fill: "#f5a623"
4077
+ }),
4078
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4079
+ d: "M4.4 11.8 2 16.1h2.5zM11.6 11.8l2.4 4.3h-2.5z",
4080
+ fill: shadeOf(color)
4081
+ }),
4082
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4083
+ d: "M8 .9c2.7 2.9 4.2 6.6 4.2 10.4 0 1.7-.4 3.2-1 4.5H4.8c-.6-1.3-1-2.8-1-4.5 0-3.8 1.5-7.5 4.2-10.4z",
4084
+ fill: color
4085
+ }),
4086
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
4087
+ cx: "8",
4088
+ cy: "6.6",
4089
+ r: "2.1",
4090
+ fill: "#ffffff"
4091
+ }),
4092
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4093
+ d: "M6.4 3.6c-.7 1.2-1.2 2.6-1.4 4.1",
4094
+ stroke: "#ffffff",
4095
+ strokeOpacity: ".35",
4096
+ strokeWidth: ".7",
4097
+ fill: "none",
4098
+ strokeLinecap: "round"
4099
+ })
4100
+ ] });
4101
+ }
4102
+ /** The shaded edge of the clothes, one step darker than the clothes. */
4103
+ function shadeOf(color) {
4104
+ return `color-mix(in srgb, ${color} 78%, #000)`;
4105
+ }
4106
+ /**
4107
+ * The load-level icon for one model row.
4108
+ *
4109
+ * Returns `null` for a level that is not drawn — including `undefined`, which
4110
+ * means "not measured", and `unknown`, which must never be turned into a
4111
+ * claim the upstream did not make.
4112
+ */
4113
+ function busyIcon(level, language = "zh") {
4114
+ const icon = busyIconOf(level);
4115
+ if (icon === void 0) return null;
4116
+ const color = COLORS[icon];
4117
+ const title = (language.toLowerCase().startsWith("en") ? TITLES_EN : TITLES_ZH)[icon];
4118
+ if (icon === "unavailable") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4119
+ className: "dsm-workbuddy-busy dsm-workbuddy-busy-unavailable",
4120
+ title,
4121
+ style: { "--dsm-workbuddy-busy-color": color },
4122
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
4123
+ viewBox: "0 0 19.3 19.3",
4124
+ width: 12,
4125
+ height: 12,
4126
+ "aria-hidden": "true",
4127
+ focusable: "false",
4128
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
4129
+ opacity: ".5",
4130
+ children: person(0, "p", "#8a8f98", "#6f747c", "#6f747c")
4131
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4132
+ d: "M0.6 19.4 19.4 0.6",
4133
+ stroke: "currentColor",
4134
+ strokeWidth: "1.8",
4135
+ strokeLinecap: "round",
4136
+ fill: "none"
4137
+ })]
4138
+ })
4139
+ });
4140
+ if (icon === "idle") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4141
+ className: "dsm-workbuddy-busy dsm-workbuddy-busy-idle",
4142
+ title,
4143
+ style: { "--dsm-workbuddy-busy-color": color },
4144
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
4145
+ viewBox: "0 0 16 19.3",
4146
+ width: 12,
4147
+ height: 12,
4148
+ "aria-hidden": "true",
4149
+ focusable: "false",
4150
+ children: rocket(color)
4151
+ })
4152
+ });
4153
+ const count = PEOPLE[icon] ?? 1;
4154
+ const width = (count - 1) * 9.6 + 7.9;
4155
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4156
+ className: `dsm-workbuddy-busy dsm-workbuddy-busy-${icon}`,
4157
+ title,
4158
+ style: { "--dsm-workbuddy-busy-color": color },
4159
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
4160
+ viewBox: `0 0 ${width} 19.3`,
4161
+ width: 12,
4162
+ height: 12,
4163
+ "aria-hidden": "true",
4164
+ focusable: "false",
4165
+ children: Array.from({ length: count }, (_, index) => person(index * 9.6, `p${index}`, color, shadeOf(color), shadeOf(color)))
4166
+ })
4167
+ });
4168
+ }
4169
+ //#endregion
3955
4170
  //#region src/client/promo-badge.ts
3956
4171
  /**
3957
4172
  * The UI's language, as the browser reports it.
@@ -4387,6 +4602,9 @@ window.__ModuleLoader__.load({
4387
4602
  one the brand colour stands in. "flex:none" keeps the badge whole while the
4388
4603
  model name beside it truncates with an ellipsis. The plain background comes
4389
4604
  first so an engine without color-mix still gets a readable chip. */
4605
+ .dsm-workbuddy-model-label{display:inline-flex;align-items:center;gap:4px;white-space:nowrap;vertical-align:baseline}
4606
+ .dsm-workbuddy-busy{flex:none;display:inline-flex;align-items:center;block-size:12px;color:var(--dsm-workbuddy-busy-color);vertical-align:-2px}
4607
+ .dsm-workbuddy-busy svg{display:block;block-size:12px;inline-size:auto}
4390
4608
  .dsm-workbuddy-model-promo{flex:none;display:inline-flex;align-items:center;padding:0 7px;border-radius:999px;font-size:10px;line-height:16px;font-weight:600;white-space:nowrap;color:var(--dsm-workbuddy-promo-color,var(--dsw-alias-brand-primary,#5686fe));background:rgba(255,255,255,.08);background:color-mix(in srgb,var(--dsm-workbuddy-promo-color,var(--dsw-alias-brand-primary,#5686fe)) 16%,transparent);border:1px solid transparent;border-color:color-mix(in srgb,var(--dsm-workbuddy-promo-color,var(--dsw-alias-brand-primary,#5686fe)) 40%,transparent)}
4391
4609
  .dsm-workbuddy-model-id{color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4392
4610
  .dsm-workbuddy-model-desc{margin:0;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
@@ -6001,6 +6219,15 @@ window.__ModuleLoader__.load({
6001
6219
  };
6002
6220
  const draft = drafts[activeRegion];
6003
6221
  const visibleModels = draft?.models ?? (status.status === "signed-in" ? status.models : []);
6222
+ /**
6223
+ * The LIVE load levels, keyed by model id.
6224
+ *
6225
+ * Read off the Host's answer rather than from the model list: the list is the
6226
+ * SAVED roster the user edits, and a saved roster has no load level in it
6227
+ * (load is a fact about this minute, not a setting — `src/busy.ts`). A model
6228
+ * the probe did not answer for simply has no entry, and draws nothing.
6229
+ */
6230
+ const busyLevels = status.status === "signed-in" ? status.busyLevels ?? {} : {};
6004
6231
  const savedEnabledIds = status.status === "signed-in" ? new Set(status.enabledModelIds) : /* @__PURE__ */ new Set();
6005
6232
  const activeEnabledIds = draft?.enabledIds ?? savedEnabledIds;
6006
6233
  const savedImageIds = status.status === "signed-in" ? new Set(status.imageModelIds) : /* @__PURE__ */ new Set();
@@ -6845,17 +7072,21 @@ window.__ModuleLoader__.load({
6845
7072
  }
6846
7073
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6847
7074
  className: "dsm-workbuddy-model-copy",
6848
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6849
- className: "dsm-workbuddy-model-name",
6850
- children: [model.name, model.creditMultiplier === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6851
- className: "dsm-workbuddy-model-name-rate",
6852
- children: [
6853
- "(",
6854
- model.creditMultiplier.toFixed(2),
6855
- "x)"
6856
- ]
6857
- })]
6858
- }), promoChip(model)]
7075
+ children: [
7076
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7077
+ className: "dsm-workbuddy-model-name",
7078
+ children: [model.name, model.creditMultiplier === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7079
+ className: "dsm-workbuddy-model-name-rate",
7080
+ children: [
7081
+ "(",
7082
+ model.creditMultiplier.toFixed(2),
7083
+ "x)"
7084
+ ]
7085
+ })]
7086
+ }),
7087
+ promoChip(model),
7088
+ busyIcon(busyLevels[model.id], uiLanguage())
7089
+ ]
6859
7090
  })]
6860
7091
  }),
6861
7092
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -8040,6 +8271,110 @@ window.__ModuleLoader__.load({
8040
8271
  });
8041
8272
  }
8042
8273
  //#endregion
8274
+ //#region src/client/WorkBuddyModelLabel.tsx
8275
+ /**
8276
+ * The load-level badge in the HOST's model picker.
8277
+ *
8278
+ * WHY A SLOT. The host renders the model name as a plain string, so the only
8279
+ * thing a provider can put there is a character — and a character's colour is
8280
+ * the emoji font's business, not ours. The host therefore offers exactly one
8281
+ * seam for this: the `conversation.input.model.label` slot, whose contract is
8282
+ * "render non-interactive inline content such as a coloured name or badge"
8283
+ * while the picker keeps owning clicks, focus, check marks and the rest. The
8284
+ * shipped example is the official DeepSeek "1017" release badge — a coloured
8285
+ * marker next to a model name — which is the same job.
8286
+ *
8287
+ * WHY A SEPARATE FETCH. The slot is rendered by the host, outside the plugin's
8288
+ * card, so it cannot read the card's React state. It therefore asks the Host
8289
+ * for the load levels itself. The levels are a fact about this minute, so the
8290
+ * answer is cached briefly: a picker that re-fetches on every re-render would
8291
+ * be wasteful, and a level that is a few seconds old is still true.
8292
+ *
8293
+ * WHY THE HOST STILL OWNS THE NAME. The contract says to keep `model.name` as
8294
+ * the text and render the addition beside it, so this component returns the
8295
+ * name followed by the badge — it never replaces the name, and it renders
8296
+ * nothing at all for a provider that is not ours.
8297
+ */
8298
+ /** The provider ids this plugin registers, and the region each one serves. */
8299
+ const PROVIDER_REGIONS = /* @__PURE__ */ new Map([["workbuddy", "cn"], ["workbuddy-global", "global"]]);
8300
+ /**
8301
+ * The region a provider id serves, or `undefined` for a provider that is not
8302
+ * this plugin's.
8303
+ *
8304
+ * Exported because the mapping is the part that can be silently wrong: asking
8305
+ * the Host without the region answers for the DEFAULT region, which would put
8306
+ * one gateway's load on the other gateway's models.
8307
+ */
8308
+ function regionOfProvider(providerId) {
8309
+ return PROVIDER_REGIONS.get(providerId);
8310
+ }
8311
+ /** How long a fetched level set is trusted, in milliseconds. */
8312
+ const CACHE_MS = 6e4;
8313
+ /** The levels per region, by model id, plus when each set was read. */
8314
+ const caches = /* @__PURE__ */ new Map();
8315
+ /**
8316
+ * The levels, read from the Host once and reused briefly.
8317
+ *
8318
+ * `undefined` means "not known" — the caller renders nothing rather than
8319
+ * guessing, because a badge that claims a level the upstream never stated is
8320
+ * worse than no badge.
8321
+ */
8322
+ async function levelsNow(region) {
8323
+ const cached = caches.get(region);
8324
+ if (cached !== void 0 && Date.now() - cached.at < CACHE_MS) return cached.levels;
8325
+ try {
8326
+ const response = await fetch(withWorkBuddyRegion(WORKBUDDY_USAGE_PATH, region), {
8327
+ headers: { accept: "application/json" },
8328
+ credentials: "same-origin"
8329
+ });
8330
+ if (!response.ok) return void 0;
8331
+ const levels = (await response.json()).busyLevels;
8332
+ if (levels === void 0 || levels === null) return void 0;
8333
+ caches.set(region, {
8334
+ levels,
8335
+ at: Date.now()
8336
+ });
8337
+ return levels;
8338
+ } catch {
8339
+ return;
8340
+ }
8341
+ }
8342
+ /**
8343
+ * The label for one model row in the host's picker.
8344
+ *
8345
+ * @param props - The host's model identity and the placement of this label.
8346
+ * @returns The catalog name, followed by the load badge when there is one.
8347
+ */
8348
+ function WorkBuddyModelLabel(props) {
8349
+ const { provider, model } = props;
8350
+ const region = regionOfProvider(provider.id);
8351
+ if (region === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name });
8352
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkBuddyModelLabelInner, {
8353
+ modelId: model.id,
8354
+ name: model.name,
8355
+ region
8356
+ });
8357
+ }
8358
+ /** The stateful half: fetches the levels, then renders the badge. */
8359
+ function WorkBuddyModelLabelInner(props) {
8360
+ const { modelId, name, region } = props;
8361
+ const [level, setLevel] = (0, react.useState)(void 0);
8362
+ (0, react.useEffect)(() => {
8363
+ let live = true;
8364
+ levelsNow(region).then((levels) => {
8365
+ if (!live || levels === void 0) return;
8366
+ setLevel(levels[modelId]);
8367
+ });
8368
+ return () => {
8369
+ live = false;
8370
+ };
8371
+ }, [modelId, region]);
8372
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8373
+ className: "dsm-workbuddy-model-label",
8374
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name }), level === void 0 ? null : busyIcon(level, "zh")]
8375
+ });
8376
+ }
8377
+ //#endregion
8043
8378
  //#region src/client/WorkBuddyQuotaNotice.tsx
8044
8379
  /**
8045
8380
  * The supplement this plugin adds to a quota failure (issue #63).
@@ -8993,6 +9328,29 @@ window.__ModuleLoader__.load({
8993
9328
  }
8994
9329
  };
8995
9330
  /**
9331
+ * The load badge in the HOST's model picker.
9332
+ *
9333
+ * The picker renders the model name as a plain string, so a provider can
9334
+ * only put a CHARACTER there — and a character's colour is the emoji font's
9335
+ * business, not ours. The host therefore offers exactly one seam for this:
9336
+ * the `conversation.input.model.label` slot, whose contract is "render
9337
+ * non-interactive inline content such as a coloured name or badge" while
9338
+ * the picker keeps owning clicks, focus and check marks. The shipped
9339
+ * example is the official DeepSeek "1017" release badge, which is the same
9340
+ * job — a coloured marker beside a model name.
9341
+ *
9342
+ * Registered unconditionally: the component itself renders nothing for a
9343
+ * provider that is not ours, so there is nothing to gate on.
9344
+ */
9345
+ const registerModelLabel = () => {
9346
+ try {
9347
+ return ctx.slots.inject("conversation.input.model.label", () => ctx.slots.register({ name: "conversation.input.model.label" }, WorkBuddyModelLabel));
9348
+ } catch (error) {
9349
+ console.error("[dsh-connect-workbuddy] model label slot failed to register (host provider unaffected):", error);
9350
+ return;
9351
+ }
9352
+ };
9353
+ /**
8996
9354
  * The SAME readout in the ambient dock below the composer card (issue #62).
8997
9355
  *
8998
9356
  * Registered unconditionally, like the composer seat, because the CHOICE
@@ -9096,7 +9454,8 @@ window.__ModuleLoader__.load({
9096
9454
  ...WORKBUDDY_CARD_KEYS.map((key) => registerCard("plugins.row.config", `${key}#${key}`)),
9097
9455
  registerComposerCredits(),
9098
9456
  registerStatsCredits(),
9099
- registerQuotaNotice()
9457
+ registerQuotaNotice(),
9458
+ registerModelLabel()
9100
9459
  ].filter((disposer) => disposer !== void 0);
9101
9460
  /**
9102
9461
  * Disposers of the slots registered for the CURRENT binding.
@@ -1684,63 +1684,6 @@ function batchModelIds(ids, size = 50) {
1684
1684
  for (let index = 0; index < unique.length; index += size) batches.push(unique.slice(index, index + size));
1685
1685
  return batches;
1686
1686
  }
1687
- /** The app's own Chinese wording, per level. `normal` and `unknown` have none. */
1688
- const BUSY_LABELS_ZH = {
1689
- idle: "空闲",
1690
- busy: "轻微拥堵",
1691
- congested: "极度拥堵",
1692
- unavailable: "不可用"
1693
- };
1694
- /** The app's own English wording, per level. */
1695
- const BUSY_LABELS_EN = {
1696
- idle: "Idle",
1697
- busy: "Slightly congested",
1698
- congested: "Extremely congested",
1699
- unavailable: "Unavailable"
1700
- };
1701
- /**
1702
- * The word this level is drawn with, or `undefined` when it is not worth drawing.
1703
- *
1704
- * `normal` returns `undefined` on purpose: the app's selector shows a busy line
1705
- * only for a level that differs from the healthy default, and a badge that reads
1706
- * "正常" on fifteen rows out of twenty is noise, not information. `unknown`
1707
- * returns `undefined` for the same reason plus one more — the plugin must not
1708
- * invent a claim the upstream never made.
1709
- */
1710
- function busyLabel(level, locale = "zh") {
1711
- return (locale === "en" ? BUSY_LABELS_EN : BUSY_LABELS_ZH)[level];
1712
- }
1713
- /**
1714
- * Suffix appended to a model's DSH-facing display name, prefixed with a middle
1715
- * dot, or `''` when the level is not worth drawing.
1716
- *
1717
- * This is the "badge to the RIGHT of the model name" the DSH model selector
1718
- * already does for its own variants (`deepseek-v4.1-flash · 1017`): DSH renders
1719
- * `name` verbatim and offers no separate badge slot to a provider, so the only
1720
- * way to put load beside the name — instead of in a second line, or inside the
1721
- * name — is to extend the name. The name stays display-only: every DSH-side join
1722
- * keys on the model ID, and `workBuddyDisplayName` is the single place that
1723
- * builds it.
1724
- *
1725
- * ONLY A LEVEL THAT ASKS FOR A DECISION IS DRAWN — `busy`, `congested`,
1726
- * `unavailable`. `idle` deliberately gets no suffix here even though it has a
1727
- * word: on the measured CN roster seven of twenty models are idle, and a picker
1728
- * whose every row carries a badge reads as decoration rather than as a warning.
1729
- * That is also how the app's own selector behaves in its compact mode, where only
1730
- * `congested` is drawn at all. The card's detail panel is where the full scale
1731
- * belongs.
1732
- */
1733
- function busyDisplaySuffix(level, locale = "zh") {
1734
- if (level === void 0 || !DRAWN_IN_PICKER.has(level)) return "";
1735
- const label = busyLabel(level, locale);
1736
- return label === void 0 ? "" : ` · ${label}`;
1737
- }
1738
- /** The levels that change a model-picking decision; see {@link busyDisplaySuffix}. */
1739
- const DRAWN_IN_PICKER = /* @__PURE__ */ new Set([
1740
- "busy",
1741
- "congested",
1742
- "unavailable"
1743
- ]);
1744
1687
  /**
1745
1688
  * Attach each model's level to its catalog entry.
1746
1689
  *
@@ -5861,7 +5804,7 @@ function generationOf(version) {
5861
5804
  //#endregion
5862
5805
  //#region src/version.ts
5863
5806
  /** The npm package version this build was produced from. */
5864
- const WORKBUDDY_CONNECT_VERSION = "5.3.0";
5807
+ const WORKBUDDY_CONNECT_VERSION = "5.3.1";
5865
5808
  //#endregion
5866
5809
  //#region src/host-heartbeat.ts
5867
5810
  /**
@@ -6019,4 +5962,4 @@ async function clearHostHeartbeat() {
6019
5962
  await rm(workbuddyHostHeartbeatPath(), { force: true });
6020
5963
  }
6021
5964
  //#endregion
6022
- export { regionOf as $, parseWorkBuddyAuth as A, workbuddyAppExecutableCandidates as At, forceNeutralHead as B, defaultDesktopAuthCandidates as C, findWorkbuddyAppExecutableWithSource as Ct, isEncryptedCredentialError as D, macosNestedAppBundles as Dt, hasEncryptedCredentialFields as E, macosBundleExecutable as Et, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as F, knownRegionOf as G, isConversationIdGateBody as H, WorkBuddyCredentialRejectedError as I, parseCreditMultiplier as J, modelIdOfBody as K, WorkBuddyUpstreamClient as L, workbuddyOwnAuthPath as M, resolveDshHome as Mt, CONTEXT_OVERFLOW_MARKER as N, withFileLock as Nt, isVaultAccountId as O, openEncryptedField as Ot, CREDENTIAL_REJECTED_CODE as P, writeFileAtomic as Pt, prepareChatBody as Q, classifyUpstreamError as R, authFileName as S, findWorkbuddyAppExecutable as St, defaultDesktopAuthPath as T, isWorkbuddyBundle as Tt, isCredentialRejectedError as U, isContextTooLongBody as V, isFirstMessageGateBody as W, parseUpstreamModel as X, parseReasoning as Y, perturbChannelFingerprints as Z, ENCRYPTED_CREDENTIAL_CODE as _, WORKBUDDY_APP_EXECUTABLE_ENV as _t, readHostHeartbeat as a, offDefaultFor as at, WorkBuddyCredentialStore as b, deriveAtRestKeyId as bt, WORKBUDDY_CONNECT_VERSION as c, withEffectiveOff as ct, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, pickImageModel as dt, attachBusyLevels as et, WorkBuddyCatalog as f, parseQueueAnswer as ft, withoutRouters as g, nativeModalityOf as gt, fallbackModelsFor as h, imageDefaultFor as ht, processStartTimeMs as i, lastRefusalFor as it, workbuddyAccountId as j, hostPeersInfo as jt, legacyWorkbuddyOwnAuthPath as k, readAtRestKey as kt, piAiRuntimeInfo as l, DEFAULT_IMAGE_SIZE as lt, deriveCatalog as m, NATIVE_MODALITY_BY_MODEL_ID as mt, clearHostHeartbeat as n, REFUSES_OFF_MODEL_IDS as nt, workbuddyHostHeartbeatPath as o, offRefused as ot, applyContextBudgets as p, queueNoticeText as pt, openingEvidenceOfBody as q, isHeartbeatProcessAlive as r, defaultOffModelIds as rt, writeHostHeartbeat as s, recordRefusal as st, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, busyDisplaySuffix as tt, FALLBACK_WORKBUDDY_MODELS as u, WorkBuddyImageError as ut, WORKBUDDY_AUTH_FILENAME as v, clearAtRestKeyCache as vt, defaultDesktopAuthDirs as w, isEncryptedFieldWrapper as wt, WorkBuddyEncryptedCredentialError as x, fetchAtRestKeyPayload as xt, WORKBUDDY_AUTH_FILE_ENV as y, deriveAtRestKey as yt, declaredTools as z };
5965
+ export { regionOf as $, parseWorkBuddyAuth as A, hostPeersInfo as At, forceNeutralHead as B, defaultDesktopAuthCandidates as C, isEncryptedFieldWrapper as Ct, isEncryptedCredentialError as D, openEncryptedField as Dt, hasEncryptedCredentialFields as E, macosNestedAppBundles as Et, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as F, knownRegionOf as G, isConversationIdGateBody as H, WorkBuddyCredentialRejectedError as I, parseCreditMultiplier as J, modelIdOfBody as K, WorkBuddyUpstreamClient as L, workbuddyOwnAuthPath as M, withFileLock as Mt, CONTEXT_OVERFLOW_MARKER as N, writeFileAtomic as Nt, isVaultAccountId as O, readAtRestKey as Ot, CREDENTIAL_REJECTED_CODE as P, prepareChatBody as Q, classifyUpstreamError as R, authFileName as S, findWorkbuddyAppExecutableWithSource as St, defaultDesktopAuthPath as T, macosBundleExecutable as Tt, isCredentialRejectedError as U, isContextTooLongBody as V, isFirstMessageGateBody as W, parseUpstreamModel as X, parseReasoning as Y, perturbChannelFingerprints as Z, ENCRYPTED_CREDENTIAL_CODE as _, clearAtRestKeyCache as _t, readHostHeartbeat as a, offRefused as at, WorkBuddyCredentialStore as b, fetchAtRestKeyPayload as bt, WORKBUDDY_CONNECT_VERSION as c, DEFAULT_IMAGE_SIZE as ct, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, parseQueueAnswer as dt, attachBusyLevels as et, WorkBuddyCatalog as f, queueNoticeText as ft, withoutRouters as g, WORKBUDDY_APP_EXECUTABLE_ENV as gt, fallbackModelsFor as h, nativeModalityOf as ht, processStartTimeMs as i, offDefaultFor as it, workbuddyAccountId as j, resolveDshHome as jt, legacyWorkbuddyOwnAuthPath as k, workbuddyAppExecutableCandidates as kt, piAiRuntimeInfo as l, WorkBuddyImageError as lt, deriveCatalog as m, imageDefaultFor as mt, clearHostHeartbeat as n, defaultOffModelIds as nt, workbuddyHostHeartbeatPath as o, recordRefusal as ot, applyContextBudgets as p, NATIVE_MODALITY_BY_MODEL_ID as pt, openingEvidenceOfBody as q, isHeartbeatProcessAlive as r, lastRefusalFor as rt, writeHostHeartbeat as s, withEffectiveOff as st, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, REFUSES_OFF_MODEL_IDS as tt, FALLBACK_WORKBUDDY_MODELS as u, pickImageModel as ut, WORKBUDDY_AUTH_FILENAME as v, deriveAtRestKey as vt, defaultDesktopAuthDirs as w, isWorkbuddyBundle as wt, WorkBuddyEncryptedCredentialError as x, findWorkbuddyAppExecutable as xt, WORKBUDDY_AUTH_FILE_ENV as y, deriveAtRestKeyId as yt, declaredTools as z };
package/lib/index.d.ts CHANGED
@@ -2974,6 +2974,18 @@ interface WorkBuddyWebModel {
2974
2974
  nativeContextWindow: number;
2975
2975
  maxTokens: number;
2976
2976
  creditMultiplier?: number;
2977
+ /**
2978
+ * The model's load level, as read from the upstream's own probe.
2979
+ *
2980
+ * Carried so the CARD can draw it as an ICON. The host's model picker cannot
2981
+ * (its name is a plain string), which is why the picker gets a character and
2982
+ * the card gets a picture.
2983
+ *
2984
+ * ABSENT means "not measured right now" and is NOT the same as `normal`: the
2985
+ * card must not draw a level the upstream never stated. It is deliberately
2986
+ * not persisted into `lastCatalog` — load is a fact about this minute.
2987
+ */
2988
+ busyLevel?: WorkBuddyBusyLevel;
2977
2989
  /**
2978
2990
  * Upstream's own image-input default from the latest refresh. Used ONLY to
2979
2991
  * pre-fill the image checkboxes when "Refresh from WorkBuddy" overwrites the
@@ -3141,6 +3153,14 @@ type WorkBuddyWebUsage =
3141
3153
  * must not be read as consent.
3142
3154
  */
3143
3155
  cliBridgeEnabled?: boolean;
3156
+ /**
3157
+ * The LIVE load level per model id, for the card's icon.
3158
+ *
3159
+ * A SEPARATE field from `models`, because the card's model list is the
3160
+ * SAVED roster (the one the user edits) and a saved roster has no load
3161
+ * level in it. Absent means "not measured right now" — never "normal".
3162
+ */
3163
+ busyLevels?: Readonly<Record<string, WorkBuddyBusyLevel>>;
3144
3164
  /** WHERE this region's readout sits (issue #62); absent means `composer`. */
3145
3165
  creditPosition?: WorkBuddyCreditPosition;
3146
3166
  message?: string;
@@ -3197,6 +3217,14 @@ type WorkBuddyWebUsage =
3197
3217
  * must not be read as consent.
3198
3218
  */
3199
3219
  cliBridgeEnabled?: boolean;
3220
+ /**
3221
+ * The LIVE load level per model id, for the card's icon.
3222
+ *
3223
+ * A SEPARATE field from `models`, because the card's model list is the
3224
+ * SAVED roster (the one the user edits) and a saved roster has no load
3225
+ * level in it. Absent means "not measured right now" — never "normal".
3226
+ */
3227
+ busyLevels?: Readonly<Record<string, WorkBuddyBusyLevel>>;
3200
3228
  /** WHERE this region's readout sits (issue #62); absent means `composer`. */
3201
3229
  creditPosition?: WorkBuddyCreditPosition;
3202
3230
  source?: 'desktop' | 'dsh';
@@ -3690,6 +3718,14 @@ interface WorkBuddyStatusRouteOptions {
3690
3718
  * deployments, and a stale `true` here would claim a spending capability is on.
3691
3719
  */
3692
3720
  cliBridgeEnabled?(): boolean;
3721
+ /**
3722
+ * The LIVE load level per model id, for the card's icon.
3723
+ *
3724
+ * Optional, and absent means "not measured": the card draws no badge rather
3725
+ * than a made-up one. A SEPARATE read because the card's model list is the
3726
+ * SAVED roster, which carries no load level (`src/busy.ts`).
3727
+ */
3728
+ busyLevels?(region: WorkBuddyRegion): Readonly<Record<string, WorkBuddyBusyLevel>>;
3693
3729
  /**
3694
3730
  * Send one real-volume request to each named model of a region and report what
3695
3731
  * came back.
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as regionOf, A as parseWorkBuddyAuth, At as workbuddyAppExecutableCandidates, B as forceNeutralHead, C as defaultDesktopAuthCandidates, D as isEncryptedCredentialError, Dt as macosNestedAppBundles, E as hasEncryptedCredentialFields, Et as macosBundleExecutable, F as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, G as knownRegionOf, H as isConversationIdGateBody, I as WorkBuddyCredentialRejectedError, J as parseCreditMultiplier, K as modelIdOfBody, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, Mt as resolveDshHome, N as CONTEXT_OVERFLOW_MARKER, Nt as withFileLock, O as isVaultAccountId, Ot as openEncryptedField, P as CREDENTIAL_REJECTED_CODE, Pt as writeFileAtomic, Q as prepareChatBody, R as classifyUpstreamError, S as authFileName, St as findWorkbuddyAppExecutable, T as defaultDesktopAuthPath, Tt as isWorkbuddyBundle, U as isCredentialRejectedError, V as isContextTooLongBody, W as isFirstMessageGateBody, X as parseUpstreamModel, Y as parseReasoning, Z as perturbChannelFingerprints, _ as ENCRYPTED_CREDENTIAL_CODE, _t as WORKBUDDY_APP_EXECUTABLE_ENV, a as readHostHeartbeat, at as offDefaultFor, b as WorkBuddyCredentialStore, bt as deriveAtRestKeyId, c as WORKBUDDY_CONNECT_VERSION, ct as withEffectiveOff, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, dt as pickImageModel, et as attachBusyLevels, f as WorkBuddyCatalog, ft as parseQueueAnswer, g as withoutRouters, gt as nativeModalityOf, h as fallbackModelsFor, ht as imageDefaultFor, i as processStartTimeMs, it as lastRefusalFor, j as workbuddyAccountId, k as legacyWorkbuddyOwnAuthPath, kt as readAtRestKey, l as piAiRuntimeInfo, lt as DEFAULT_IMAGE_SIZE, m as deriveCatalog, mt as NATIVE_MODALITY_BY_MODEL_ID, n as clearHostHeartbeat, nt as REFUSES_OFF_MODEL_IDS, o as workbuddyHostHeartbeatPath, ot as offRefused, p as applyContextBudgets, pt as queueNoticeText, q as openingEvidenceOfBody, r as isHeartbeatProcessAlive, rt as defaultOffModelIds, s as writeHostHeartbeat, st as recordRefusal, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as busyDisplaySuffix, u as FALLBACK_WORKBUDDY_MODELS, ut as WorkBuddyImageError, v as WORKBUDDY_AUTH_FILENAME, vt as clearAtRestKeyCache, w as defaultDesktopAuthDirs, wt as isEncryptedFieldWrapper, x as WorkBuddyEncryptedCredentialError, xt as fetchAtRestKeyPayload, y as WORKBUDDY_AUTH_FILE_ENV, yt as deriveAtRestKey, z as declaredTools } from "./host-heartbeat-_EvGiAek.js";
1
+ import { $ as regionOf, A as parseWorkBuddyAuth, B as forceNeutralHead, C as defaultDesktopAuthCandidates, Ct as isEncryptedFieldWrapper, D as isEncryptedCredentialError, Dt as openEncryptedField, E as hasEncryptedCredentialFields, Et as macosNestedAppBundles, F as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, G as knownRegionOf, H as isConversationIdGateBody, I as WorkBuddyCredentialRejectedError, J as parseCreditMultiplier, K as modelIdOfBody, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, Mt as withFileLock, N as CONTEXT_OVERFLOW_MARKER, Nt as writeFileAtomic, O as isVaultAccountId, Ot as readAtRestKey, P as CREDENTIAL_REJECTED_CODE, Q as prepareChatBody, R as classifyUpstreamError, S as authFileName, T as defaultDesktopAuthPath, Tt as macosBundleExecutable, U as isCredentialRejectedError, V as isContextTooLongBody, W as isFirstMessageGateBody, X as parseUpstreamModel, Y as parseReasoning, Z as perturbChannelFingerprints, _ as ENCRYPTED_CREDENTIAL_CODE, _t as clearAtRestKeyCache, a as readHostHeartbeat, at as offRefused, b as WorkBuddyCredentialStore, bt as fetchAtRestKeyPayload, c as WORKBUDDY_CONNECT_VERSION, ct as DEFAULT_IMAGE_SIZE, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, dt as parseQueueAnswer, et as attachBusyLevels, f as WorkBuddyCatalog, ft as queueNoticeText, g as withoutRouters, gt as WORKBUDDY_APP_EXECUTABLE_ENV, h as fallbackModelsFor, ht as nativeModalityOf, i as processStartTimeMs, it as offDefaultFor, j as workbuddyAccountId, jt as resolveDshHome, k as legacyWorkbuddyOwnAuthPath, kt as workbuddyAppExecutableCandidates, l as piAiRuntimeInfo, lt as WorkBuddyImageError, m as deriveCatalog, mt as imageDefaultFor, n as clearHostHeartbeat, nt as defaultOffModelIds, o as workbuddyHostHeartbeatPath, ot as recordRefusal, p as applyContextBudgets, pt as NATIVE_MODALITY_BY_MODEL_ID, q as openingEvidenceOfBody, r as isHeartbeatProcessAlive, rt as lastRefusalFor, s as writeHostHeartbeat, st as withEffectiveOff, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as REFUSES_OFF_MODEL_IDS, u as FALLBACK_WORKBUDDY_MODELS, ut as pickImageModel, v as WORKBUDDY_AUTH_FILENAME, vt as deriveAtRestKey, w as defaultDesktopAuthDirs, wt as isWorkbuddyBundle, x as WorkBuddyEncryptedCredentialError, xt as findWorkbuddyAppExecutable, y as WORKBUDDY_AUTH_FILE_ENV, yt as deriveAtRestKeyId, z as declaredTools } from "./host-heartbeat-BHESQ6Ah.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
@@ -310,7 +310,7 @@ function workBuddyModelInput(info) {
310
310
  * `x0.00`, matching WorkBuddy's rendering of free models.
311
311
  */
312
312
  function workBuddyDisplayName(info) {
313
- return `${info.creditMultiplier === void 0 ? info.name : `${info.name} · x${info.creditMultiplier.toFixed(2)}`}${busyDisplaySuffix(info.busyLevel)}`;
313
+ return info.creditMultiplier === void 0 ? info.name : `${info.name} · x${info.creditMultiplier.toFixed(2)}`;
314
314
  }
315
315
  /**
316
316
  * Map only levels advertised by WorkBuddy; undeclared DSH levels stay unavailable.
@@ -7762,6 +7762,7 @@ function toWebModel(model, budgets) {
7762
7762
  nativeContextWindow: model.contextWindow,
7763
7763
  maxTokens: model.maxTokens,
7764
7764
  ...model.creditMultiplier === void 0 ? {} : { creditMultiplier: model.creditMultiplier },
7765
+ ...model.busyLevel === void 0 ? {} : { busyLevel: model.busyLevel },
7765
7766
  ...model.supportsImages === void 0 ? {} : { supportsImages: model.supportsImages },
7766
7767
  ...model.multimodal === void 0 ? {} : { multimodal: model.multimodal },
7767
7768
  ...model.reasoning === void 0 ? {} : { reasoning: {
@@ -7899,6 +7900,7 @@ async function workBuddyWebStatus(deps, region, options = {}) {
7899
7900
  const creditPosition = deps.creditPosition?.(region) ?? "composer";
7900
7901
  const imageToolEnabled = deps.imageToolEnabled?.(region) ?? false;
7901
7902
  const cliBridgeEnabled = deps.cliBridgeEnabled?.() === true;
7903
+ const busyLevels = deps.busyLevels?.(region) ?? {};
7902
7904
  const authStatus = await store.status();
7903
7905
  const selectionExplicit = store.hasExplicitSelection();
7904
7906
  if (authStatus.state !== "signed-out" && accounts.length === 0) return {
@@ -7908,7 +7910,8 @@ async function workBuddyWebStatus(deps, region, options = {}) {
7908
7910
  enabled,
7909
7911
  showCreditsInMainUi,
7910
7912
  imageToolEnabled,
7911
- cliBridgeEnabled
7913
+ cliBridgeEnabled,
7914
+ busyLevels
7912
7915
  };
7913
7916
  let credential;
7914
7917
  try {
@@ -7926,6 +7929,7 @@ async function workBuddyWebStatus(deps, region, options = {}) {
7926
7929
  creditPosition,
7927
7930
  imageToolEnabled,
7928
7931
  cliBridgeEnabled,
7932
+ busyLevels,
7929
7933
  ...await store.selectionLost() ? { selectionLost: true } : {},
7930
7934
  ...webAccounts.length === 0 ? await searchedPaths(store) : {}
7931
7935
  };
@@ -7944,6 +7948,7 @@ async function workBuddyWebStatus(deps, region, options = {}) {
7944
7948
  creditPosition,
7945
7949
  imageToolEnabled,
7946
7950
  cliBridgeEnabled,
7951
+ busyLevels,
7947
7952
  accounts: accounts.map(toWebAccount),
7948
7953
  models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))).map((model) => model.reasoning === void 0 || lastRefusalFor(model.id) === void 0 ? model : {
7949
7954
  ...model,
@@ -8095,7 +8100,8 @@ async function workBuddyWebPool(deps, region, fast = false) {
8095
8100
  catalog: deps.displayModels(region).map((model) => ({
8096
8101
  id: model.id,
8097
8102
  name: model.name,
8098
- ...model.creditMultiplier === void 0 ? {} : { creditMultiplier: model.creditMultiplier }
8103
+ ...model.creditMultiplier === void 0 ? {} : { creditMultiplier: model.creditMultiplier },
8104
+ ...model.busyLevel === void 0 ? {} : { busyLevel: model.busyLevel }
8099
8105
  }))
8100
8106
  };
8101
8107
  }
@@ -9024,6 +9030,25 @@ function apply(ctx, config) {
9024
9030
  const live = stacks[region]?.catalog?.current();
9025
9031
  return live?.length ? live : fallbackModelsFor(region);
9026
9032
  };
9033
+ /**
9034
+ * The load levels of the LIVE roster, keyed by model id.
9035
+ *
9036
+ * A SEPARATE read on purpose. `displayModels()` prefers the SAVED
9037
+ * `lastCatalog` (it is what the card edits), and a saved roster carries no
9038
+ * load level — load is not persisted, see `src/busy.ts`. So the card cannot
9039
+ * get the level from the model list it already has; it needs the live roster
9040
+ * beside it.
9041
+ *
9042
+ * Returning a MAP (not a merged roster) keeps the two apart: the card's list
9043
+ * stays the saved one the user edits, and the badge is a decoration that may
9044
+ * be absent for a model the probe did not answer for.
9045
+ */
9046
+ const liveBusyLevels = (region) => {
9047
+ const live = stacks[region]?.catalog?.current() ?? [];
9048
+ const levels = {};
9049
+ for (const model of live) if (model.busyLevel !== void 0) levels[model.id] = model.busyLevel;
9050
+ return levels;
9051
+ };
9027
9052
  let current = () => config;
9028
9053
  let invalidateCatalog = () => {};
9029
9054
  /**
@@ -9065,6 +9090,35 @@ function apply(ctx, config) {
9065
9090
  return models;
9066
9091
  }
9067
9092
  };
9093
+ /**
9094
+ * Re-stamp the LIVE load levels onto a roster that was just recomputed.
9095
+ *
9096
+ * WHY A REPLAY NEEDS THIS. `configuredModels()` rebuilds the roster from the
9097
+ * SAVED `lastCatalog`, and a saved roster has no load level in it — load is
9098
+ * deliberately not persisted (`src/busy.ts`: a cached level is a lie the
9099
+ * moment the settings page closes). So every config change — switching
9100
+ * region, changing the model selection, toggling a level, moving an account —
9101
+ * used to wipe the badges that were on screen a second earlier, and the user
9102
+ * saw "极度拥堵" appear and then vanish without touching the upstream at all.
9103
+ *
9104
+ * The fix is to ask again rather than to remember: the probe is read-only,
9105
+ * free, and answers in ~190 ms for the whole roster. Keeping the level out
9106
+ * of disk is the point, so the honest place to get it back is the upstream.
9107
+ *
9108
+ * Fail-soft, like the other probe: a level that cannot be read is a missing
9109
+ * badge, never a missing model list.
9110
+ */
9111
+ const restampBusyLevels = async (region, models) => {
9112
+ if (models.length === 0) return models;
9113
+ try {
9114
+ const credential = await stacks[region].store.resolve();
9115
+ const levels = await client.fetchModelBusy(credential, models.map((model) => model.id));
9116
+ return levels.size === 0 ? models : attachBusyLevels(models, levels);
9117
+ } catch (error) {
9118
+ ctx.logger.warn("dsh-connect-workbuddy: load probe unavailable while re-applying the selection; the roster keeps no load levels", error);
9119
+ return models;
9120
+ }
9121
+ };
9068
9122
  /** Push the current config into every region's store selection and catalog. */
9069
9123
  const applySelection = (value) => {
9070
9124
  const authFile = unwrapVolatile(value.authFile);
@@ -9074,6 +9128,12 @@ function apply(ctx, config) {
9074
9128
  stacks[region].catalog.set(configuredModels(value, region, stacks[region].catalog.current()));
9075
9129
  }
9076
9130
  invalidateCatalog();
9131
+ for (const region of REGION_KEYS) (async () => {
9132
+ const live = await restampBusyLevels(region, stacks[region].catalog.current());
9133
+ if (live.length === 0) return;
9134
+ stacks[region].catalog.set(live);
9135
+ invalidateCatalog();
9136
+ })();
9077
9137
  syncRegionRegistration(value);
9078
9138
  syncImageTool(value);
9079
9139
  syncCliBridge(value);
@@ -9248,6 +9308,14 @@ function apply(ctx, config) {
9248
9308
  return out;
9249
9309
  },
9250
9310
  displayModels: (region) => displayModels(current(), region),
9311
+ /**
9312
+ * The LIVE load levels, read beside the saved roster the card lists.
9313
+ *
9314
+ * The card's model list is the SAVED `lastCatalog`, which carries no load
9315
+ * level (load is not persisted — `src/busy.ts`), so the badge has to come
9316
+ * from the live roster instead.
9317
+ */
9318
+ busyLevels: (region) => liveBusyLevels(region),
9251
9319
  enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
9252
9320
  imageModelIds: (region) => regionStateOf(current(), region).imageModelIds ?? [],
9253
9321
  offModelIds: (region) => savedOffModelIds(regionStateOf(current(), region), displayModels(current(), region)),
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-workbuddy",
3
3
  "displayName": "DSH Connect WorkBuddy",
4
4
  "description": "把本机登录的 WorkBuddy 模型(国内版与国际版)接入 DeepSeek Harness:每次刷新按上游目录自动读取每个模型的积分倍率,支持逐模型图片输入开关、多账号切换与账号池自动换号,并提供每日签到、卡片与输入框里的胶囊积分读数等只读积分概览。",
5
- "version": "5.3.0",
5
+ "version": "5.3.1",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",