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 +21 -1
- package/lib/bin.js +1 -1
- package/lib/client.js +371 -12
- package/lib/{host-heartbeat-_EvGiAek.js → host-heartbeat-BHESQ6Ah.js} +2 -59
- package/lib/index.d.ts +36 -0
- package/lib/index.js +72 -4
- package/package.json +1 -1
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
|
-
- 全量:**
|
|
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 {
|
|
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: [
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
|
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.
|
|
5
|
+
"version": "5.3.1",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|