dsh-connect-workbuddy 4.4.6 → 4.5.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 +43 -8
- package/lib/bin.js +1 -1
- package/lib/client.js +310 -6
- package/lib/{host-heartbeat-B2jrUDhg.js → host-heartbeat-Vd2tNdsv.js} +1 -1
- package/lib/index.d.ts +40 -0
- package/lib/index.js +20 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.5.1 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- **`6004`(账号频率超限)在对话里被显示成「当前请求的额度已用尽」,而账号余额还剩 6,788 —— 现在那句话下面补了一句说明。**(issue #63;维护者判定:**保持不进重试**,只补文案)
|
|
8
|
+
- **根因(实测,不是读代码猜的)**:宿主对失败按**码**路由,而插件这条链是 —— shim 的错误响应 → pi-ai 抛出的 message → `classifyPiAiError(message)` → 宿主码 → 客户端文案。三个环节的出货代码取出来直接跑之后:`quota_exceeded` 这个拼写里的下划线**恰好命中** `@deepseek-ai/dsh-llm` 的 `isQuotaExceededError` 正则(`quota[\s_-]+exceeded` 的 `[\s_-]+` 能匹配 `_`),于是归成 `QUOTA`;而 `@deepseek-ai/dsh-client-ui-chat` 对 `QUOTA`/`ACCOUNT_QUOTA` **硬编码**成那句固定文案、并**丢弃**插件自己的 message。**这一版之前我在 #56/#61 里把它说成「插件挑了宿主认识的词」,那个解释是错的。**
|
|
9
|
+
- **为什么不改分类(维护者的决定,也是实测支持的)**:把拼写换成任何一个不再被 quota 正则命中的值,都会落进 `RATE_LIMIT` —— 而它在 `DEFAULT_RETRYABLE_CODES` 里(`EMPTY_RESPONSE`/`RATE_LIMIT`/`SERVER`/`TIMEOUT`/`TRANSPORT`)。按出货默认(`initialDelayMs=500`、指数退避、封顶 10s、`maxRetries=5`)这是**5 次原地重试、标称 15.5 秒退避**,而 `6004` 的恢复点在报文里明写了是**几小时后** —— 退避窗口内不可能恢复。所以「不进重试」保留,「那句话不够准确」用**补充**解决。
|
|
10
|
+
- **修法**:用宿主**专门为此留的扩展点** `shell.quota-notice`(链式插槽,其自有文档写着「the first entry whose selector claims its code takes over the surface」)。本插件注册一条 `select: owner => code === 'QUOTA' ? owner : null` 的链项:
|
|
11
|
+
- **只 claim `QUOTA`** —— 其余码(`AUTH`、`ACCOUNT_SIGNED_OUT`、`ACCOUNT_QUOTA` 等)继续走宿主自己的兜底 Toast,本插件不接管自己说不清的提示;
|
|
12
|
+
- **宿主那句话原样保留**(`matched.message`),补充是**第二行**而不是改写 —— 那句话在别处也显示,重写会让插件与 shell 的用词互相矛盾;
|
|
13
|
+
- 补充文案:**「部分模型另有每日额度限制,与积分余额无关 —— 也可能确实是本账号积分耗尽。账号池那一行会写明上游给出的恢复时刻;也可以先换个账号或换个模型继续。」** 措辞对 `QUOTA` 覆盖的**两种**情况都成立,因为上游是用中文散文区分的,而插件不允许解析它来下政策判断;中英双语都写。
|
|
14
|
+
- 带一个「知道了」按钮,调用宿主自己的 `dismiss`;宿主没给 `dismiss` 时只渲染文字而不崩 —— 这是**全窗口**的提示,在这里抛错会连别的插件的提示一起带走。
|
|
15
|
+
- **组件用本插件自己的 DOM 与样式类**,不引 `@deepseek-ai/dsh-client-ui-primitives`:客户端刻意不依赖它(那条 stale 的 `inject` 曾让整张卡片的到达依赖上宿主最易变的包,见 `tests/client-host-contract.spec.ts` 的说明),所以这里沿用本插件其它组件的做法。
|
|
16
|
+
- **实机核对**:构建后用 Slot 检查器读运行中的插槽树,`shell.quota-notice` 的 occupants 已包含 `dsh-connect-workbuddy-client`(`active: true`),与宿主自己的 `mf` 并列;插槽目录给出的 `ownerProps`(`code`/`message`/`dismiss`/`keepOpen`)与组件读取的字段一致。
|
|
17
|
+
- **守卫**:`tests/issue63-quota-notice.spec.tsx`(7 例)覆盖:claim 恰好是 `QUOTA`、其余码(含 `QUOTA_EXCEEDED`、空串、数字、对象)一律不 claim、宿主句子原样保留且补充可见、dismiss 按钮调用宿主回调、无 `dismiss` 时不崩、宿主没给 message 时回退到自己的标题。**变异验证**:把 claim 扩到 `AUTH`/`ACCOUNT_QUOTA` → 2 例红;删掉补充那一行 → 2 例红。
|
|
18
|
+
- **未做**:没有改任何分类/HTTP 状态,也没有动 `KIND_HOST_CODE` 里 `soft_rate`/`model_busy` → `quota_exceeded` 的翻译(那正是 #56 修复的核心)。
|
|
19
|
+
|
|
20
|
+
## 4.5.0 (2026-10-14)
|
|
21
|
+
|
|
22
|
+
> 版本号是 **minor** 而不是 patch:这一版新增了一个**用户可见的设置**(积分读数的位置)和一个**新的渲染席位**,而且配置 schema 多了一个字段 —— 按语义化版本这是特性,不是修错。`4.4.6` 之后没有任何以 4.4.7 发布过的产物或 Release(tag 已撤回重打)。
|
|
23
|
+
|
|
24
|
+
### Features
|
|
25
|
+
|
|
26
|
+
- **积分读数只有一个固定位置:装了别的插件后它就不显示了 —— 现在位置可选(输入框工具栏 / 输入框下方)**(issue #62 报告者)
|
|
27
|
+
- **现象**:报告者装了第三方插件(`dsh-auto-continue`)之后,对话输入框里的 WorkBuddy 积分余额消失。根因是那个位置本身:`conversation.input.left`(输入框工具栏)是一个**共享的 list 插槽**,shell 自己的控件和任何其他 bundle 都注册在那里 —— 而 list 插槽无法保证每个占用者都可见,别人挤进来就可能把本插件的读数挤没。原注释里其实已经记着同类问题(更早放在侧边栏底部时,两个连接器插件互相挤压,才迁到输入框这一行)。
|
|
28
|
+
- **修法**:把「位置」做成**每区域一个选项**,两个席位都注册、由 gate 决定在哪个席位渲染:
|
|
29
|
+
- `composer`(默认)—— 输入框工具栏 `conversation.input.left`,与模型选择同一行;
|
|
30
|
+
- `stats` —— 输入框下方 `conversation.composer.dock`,shell 自己渲染「轮次 / 步数 · tok/s · token · 缓存命中」那一行;`order: 10` 排在 shell 的统计之后,所以官方那几颗统计仍在最左,读数是**追加**而不是顶替。
|
|
31
|
+
- 选默认值是为了**升级不动任何人的界面**:设置在 schema 里默认 `composer`,升级前写下的 profile 读出来仍是 `composer`。
|
|
32
|
+
- **为什么两个席位都注册**:位置这种东西用户是「一边看着结果一边改」的,注册哪个席位取决于设置就意味着每次改设置都要**重新注册插槽**;而本仓库的注释里已经写明这种绑定是脆的(晚到的 settings 命名空间必须产生新的注册而不是改闭包)。现在**决定权在 gate 里一处**:席位不符就 `return null`,另一个席位照常渲染。
|
|
33
|
+
- **判定的顺序**:先归属(是不是本插件的 provider)、再两个开关(区域是否启用、读数是否显示)、**最后**才是席位。所以一个被关掉的区域在**两个席位都不渲染**,而不是先在非当前席位画一帧再被拒。
|
|
34
|
+
- **卡片新增一个两选一控件**(就在「对话框显示积分」那个勾选旁边,只作用于当前标签页的区域):读取走 Host 的 usage 应答(与那个勾选同源,因为浏览器 settings 镜像在本命名空间上是过期的那份),写完**重新读一次 Host** —— 否则控件会一直显示旧席位,看起来像没生效。
|
|
35
|
+
- **守卫**:`tests/issue62-credit-position.spec.tsx`(9 例)覆盖:默认 `composer`(含升级前的 profile 与手改的未知值)、按区域而非全局读取、写入只改该区域该字段(模型列表/预算/兄弟区域都不动)、席位与显示开关相互独立、gate 在**指定席位渲染 / 另一席位渲染 0 个 / 两个席位都不渲染(隐藏或区域关闭)/ 别的 provider 两个席位都不渲染**。**变异验证**:把 gate 的席位判定删掉(两个席位都渲染)→ 2 例红;把读取的默认值去掉(升级前的 profile 读成 stats)→ 2 例红。
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
- **新增「上游请求体构成」诊断(`WORKBUDDY_BODY_REPORT`),用于定案 issue #59:两份请求体到底是不是同一份历史。**(issue #59)
|
|
39
|
+
- **为什么需要**:报告里同一份历史被国内网关算约 70.8 万 token、国际网关算约 137.4 万(1.94 倍),切区域即 `code 11115 prompt is too long`。两条独立测量已经把范围压到唯一一个问题:报告者在**两个区域**跑本仓库的内容类别探针,逐字节相同的散文/代码/JSON 六格比值全在 **0.98~1.06**(同一段字节两边计价一致);代码链路核对又表明**历史 reasoning 根本没进请求体**(pi-ai 只在 `compat.requiresThinkingAsText` 为真时把 `thinking` 当文本转发,本插件从未设置;`reasoning_details` 那条路需要流式解析从不写出的签名)。**剩下唯一可能是两份请求体不同**,而这件事**不需要任何区域有积分**就能查:请求体是本地拼好的,`14018 Credits exhausted` 是发出去之后才可能发生的。
|
|
40
|
+
- **做法**:`src/body-report.ts` 在 shim 把 body 交给上游**之前**记录它的**构成** —— 每条消息的 `role`、逐块的类型(`system`/`text`/`tool-call`/`tool-result`/`reasoning`/`image`)、字符数与其中**中日韩字符**数(实测中文 0.669、英文 0.224 token/字符,差 3 倍,混在一起看不出来)、`tools` 数组、`messages.length` 与 body 总字节数。按区域写 `<dir>/<region>.jsonl`,一次请求一行(一轮对话有很多次上游调用,**最后一次**才是要比的那份)。
|
|
41
|
+
- **只记计数、不记文本**:不写任何消息内容、账号名或令牌 —— 一个要被贴进公开 issue 的诊断不能有泄漏用户会话的能力。`tests/issue59-body-report.spec.ts` 里有一条专门的断言:用带特征串的 body 生成报告后,序列化结果**不含**该特征串(变异验证:让 `block()` 把 `text` 一起带上 → 该例与 reasoning 那例同时变红)。
|
|
42
|
+
- **开关**:环境变量 `WORKBUDDY_BODY_REPORT` 未设置或为空白即**关闭**;`1`/`true` 写到 `<DSH home>/body-reports/`;其他值按**目录路径**解释。写文件失败只走日志,**绝不影响聊天请求**(有专门断言)。
|
|
43
|
+
- **守卫**:`tests/issue59-body-report.spec.ts`(16 例)。
|
|
44
|
+
- **配套**:`scripts/probe-region-token-accounting.mjs` 新增 `--language en|zh` 与 `--kind prose|code|json`(六种组合确定性填充、跨区域逐字节相同),并补上国内侧实测基线:prose 214.0 / code 287.0 / json 288.3(en),prose 671.0 / code 480.7 / json 397.3(zh),单位 tokens/1k chars。
|
|
45
|
+
|
|
3
46
|
## 4.4.6 (2026-10-14)
|
|
4
47
|
|
|
5
48
|
### Fixes
|
|
@@ -17,14 +60,6 @@
|
|
|
17
60
|
|
|
18
61
|
### Features
|
|
19
62
|
|
|
20
|
-
- **新增「上游请求体构成」诊断(`WORKBUDDY_BODY_REPORT`),用于定案 issue #59:两份请求体到底是不是同一份历史。**(issue #59)
|
|
21
|
-
- **为什么需要**:报告里同一份历史被国内网关算约 70.8 万 token、国际网关算约 137.4 万(1.94 倍),切区域即 `code 11115 prompt is too long`。两条独立测量已经把范围压到唯一一个问题:报告者在**两个区域**跑本仓库的内容类别探针,逐字节相同的散文/代码/JSON 六格比值全在 **0.98~1.06**(同一段字节两边计价一致);代码链路核对又表明**历史 reasoning 根本没进请求体**(pi-ai 只在 `compat.requiresThinkingAsText` 为真时把 `thinking` 当文本转发,本插件从未设置;`reasoning_details` 那条路需要流式解析从不写出的签名)。**剩下唯一可能是两份请求体不同**,而这件事**不需要任何区域有积分**就能查:请求体是本地拼好的,`14018 Credits exhausted` 是发出去之后才可能发生的。
|
|
22
|
-
- **做法**:`src/body-report.ts` 在 shim 把 body 交给上游**之前**记录它的**构成** —— 每条消息的 `role`、逐块的类型(`system`/`text`/`tool-call`/`tool-result`/`reasoning`/`image`)、字符数与其中**中日韩字符**数(实测中文 0.669、英文 0.224 token/字符,差 3 倍,混在一起看不出来)、`tools` 数组、`messages.length` 与 body 总字节数。按区域写 `<dir>/<region>.jsonl`,一次请求一行(一轮对话有很多次上游调用,**最后一次**才是要比的那份)。
|
|
23
|
-
- **只记计数、不记文本**:不写任何消息内容、账号名或令牌 —— 一个要被贴进公开 issue 的诊断不能有泄漏用户会话的能力。`tests/issue59-body-report.spec.ts` 里有一条专门的断言:用带特征串的 body 生成报告后,序列化结果**不含**该特征串(变异验证:让 `block()` 把 `text` 一起带上 → 该例与 reasoning 那例同时变红)。
|
|
24
|
-
- **开关**:环境变量 `WORKBUDDY_BODY_REPORT` 未设置或为空白即**关闭**;`1`/`true` 写到 `<DSH home>/body-reports/`;其他值按**目录路径**解释。写文件失败只走日志,**绝不影响聊天请求**(有专门断言)。
|
|
25
|
-
- **守卫**:`tests/issue59-body-report.spec.ts`(16 例)。
|
|
26
|
-
- **配套**:`scripts/probe-region-token-accounting.mjs` 新增 `--language en|zh` 与 `--kind prose|code|json`(六种组合确定性填充、跨区域逐字节相同),并补上国内侧实测基线:prose 214.0 / code 287.0 / json 288.3(en),prose 671.0 / code 480.7 / json 397.3(zh),单位 tokens/1k chars。
|
|
27
|
-
|
|
28
63
|
## 4.4.5 (2026-10-14)
|
|
29
64
|
|
|
30
65
|
### Fixes
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, K as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, Q as findWorkbuddyAppExecutableWithSource, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, ot as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-
|
|
2
|
+
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, K as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, Q as findWorkbuddyAppExecutableWithSource, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, ot as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-Vd2tNdsv.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
|
@@ -233,6 +233,23 @@ window.__ModuleLoader__.load({
|
|
|
233
233
|
function regionCreditsShownOf(value, region) {
|
|
234
234
|
return regionSlotOf(value, region)["showCreditsInMainUi"] !== false;
|
|
235
235
|
}
|
|
236
|
+
/** The seat a region's readout uses, defaulting to the composer row. */
|
|
237
|
+
function regionCreditPositionOf(value, region) {
|
|
238
|
+
return regionSlotOf(value, region)["creditPosition"] === "stats" ? "stats" : "composer";
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Build the next `regions` settings value for one region's credit seat.
|
|
242
|
+
*
|
|
243
|
+
* Same merge rule as {@link nextRegionCreditsShown}: only the target region's
|
|
244
|
+
* field changes, so every other field of that slot and every other region's slot
|
|
245
|
+
* survive the write.
|
|
246
|
+
*/
|
|
247
|
+
function nextRegionCreditPosition(value, region, position) {
|
|
248
|
+
return nextRegionSlots(regionsMapOf(value), region, {
|
|
249
|
+
...regionSlotOf(value, region),
|
|
250
|
+
creditPosition: position
|
|
251
|
+
});
|
|
252
|
+
}
|
|
236
253
|
/**
|
|
237
254
|
* Build the next `regions` settings value for the sidebar-credit on/off toggle.
|
|
238
255
|
*
|
|
@@ -632,6 +649,20 @@ window.__ModuleLoader__.load({
|
|
|
632
649
|
});
|
|
633
650
|
}
|
|
634
651
|
/**
|
|
652
|
+
* Write one region's credit SEAT (issue #62), verifying it landed.
|
|
653
|
+
*
|
|
654
|
+
* Sibling of {@link writeRegionCreditsShown}: same whole-slot merge, same
|
|
655
|
+
* landed check, same reason for both. The seat decides only WHERE the readout
|
|
656
|
+
* renders, so a lost write would leave the user looking at the old position
|
|
657
|
+
* while the control claimed the new one.
|
|
658
|
+
*/
|
|
659
|
+
async function writeRegionCreditPosition(scope, region, position, slot) {
|
|
660
|
+
await writeField(scope, "regions", region, slot, (readBack) => {
|
|
661
|
+
const stored = readBack;
|
|
662
|
+
return stored !== null && stored !== void 0 && stored["creditPosition"] === position;
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
635
666
|
* Write one region's account-pool PREFERENCES, verifying they landed.
|
|
636
667
|
*
|
|
637
668
|
* Routed through {@link writeField} like every other settings write, so it gets
|
|
@@ -3044,6 +3075,22 @@ window.__ModuleLoader__.load({
|
|
|
3044
3075
|
render (no Host pool state), so there is no heading row to share. This is the
|
|
3045
3076
|
spacing the control carried before it moved beside that heading. */
|
|
3046
3077
|
.dsm-workbuddy-credits-switch-row{display:flex;align-items:center;margin:8px 0 2px}
|
|
3078
|
+
/* The credit readout's seat picker (issue #62). Narrower than the pool's model
|
|
3079
|
+
select so the show/hide box and this control share one heading row, and it
|
|
3080
|
+
wraps onto its own line rather than squashing the pool title. */
|
|
3081
|
+
/* Its OWN label class, deliberately not the switch's: a query for "the
|
|
3082
|
+
sidebar-credit switch" must not be able to pick this control up, and the two
|
|
3083
|
+
are different widgets (a checkbox and a two-way picker). */
|
|
3084
|
+
/* The supplement to the host's fixed quota sentence (issue #63). It sits in the
|
|
3085
|
+
frame-wide overlay seat, so it must read on its own and stay narrow: a column
|
|
3086
|
+
with the host's sentence first (verbatim) and the supplement under it. */
|
|
3087
|
+
.dsm-workbuddy-quota-notice{display:flex;flex-direction:column;gap:4px;max-width:420px;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,#23242a);border:1px solid var(--dsw-alias-border-l1,#2c2d31);font-size:12px;line-height:17px}
|
|
3088
|
+
.dsm-workbuddy-quota-notice-head{font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
3089
|
+
.dsm-workbuddy-quota-notice-body{color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
3090
|
+
.dsm-workbuddy-quota-notice-dismiss{align-self:flex-start;margin-top:2px;padding:2px 10px;font-size:12px}
|
|
3091
|
+
.dsm-workbuddy-credit-position-switch{display:inline-flex;align-items:center;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
3092
|
+
.dsm-workbuddy-credit-position{max-width:150px;padding:2px 6px;font-size:12px}
|
|
3093
|
+
.dsm-workbuddy-credits-switch-row .dsm-workbuddy-credit-position-switch{margin-left:12px}
|
|
3047
3094
|
.dsm-workbuddy-credits-switch input{margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
|
|
3048
3095
|
.dsm-workbuddy-credits-switch input:disabled{opacity:.4;cursor:default}
|
|
3049
3096
|
.dsm-workbuddy-tab-off{opacity:.55}
|
|
@@ -4140,6 +4187,40 @@ window.__ModuleLoader__.load({
|
|
|
4140
4187
|
* hide never drops the directory, model picks, budgets or pool preferences
|
|
4141
4188
|
* that live alongside it.
|
|
4142
4189
|
*/
|
|
4190
|
+
/**
|
|
4191
|
+
* WHERE the active region's credit readout sits (issue #62).
|
|
4192
|
+
*
|
|
4193
|
+
* Read off the Host's usage answer, like {@link creditsShownOn} and for the
|
|
4194
|
+
* same measured reason: the browser's settings mirror for this namespace is
|
|
4195
|
+
* documented as stale on the affected 0.1.7 deployments, so a control reading
|
|
4196
|
+
* it would keep showing the previous seat after a successful write. Absent
|
|
4197
|
+
* means `composer`, which is what the Host resolves for a profile written
|
|
4198
|
+
* before the setting existed.
|
|
4199
|
+
*/
|
|
4200
|
+
const creditPositionOn = (item) => {
|
|
4201
|
+
return statusByRegion[item]?.creditPosition === "stats" ? "stats" : "composer";
|
|
4202
|
+
};
|
|
4203
|
+
/**
|
|
4204
|
+
* Move one region's credit readout between its two seats.
|
|
4205
|
+
*
|
|
4206
|
+
* Same shape as {@link toggleCredits}: whole region slot in, landed check,
|
|
4207
|
+
* then a Host re-read. The re-read is load-bearing here for the same reason —
|
|
4208
|
+
* the control renders the Host's answer, so without it the picker keeps
|
|
4209
|
+
* showing the seat it had and the change looks like it did nothing.
|
|
4210
|
+
*/
|
|
4211
|
+
const changeCreditPosition = async (item, position) => {
|
|
4212
|
+
if (settingsScope === void 0) return;
|
|
4213
|
+
setTogglingCredits(item);
|
|
4214
|
+
try {
|
|
4215
|
+
const slot = nextRegionCreditPosition(settingsScope.getSnapshot().value, item, position)[item];
|
|
4216
|
+
await writeRegionCreditPosition(settingsScope, item, position, typeof slot === "object" && slot !== null ? slot : {});
|
|
4217
|
+
await refreshUsage(item);
|
|
4218
|
+
} catch (error) {
|
|
4219
|
+
if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
4220
|
+
} finally {
|
|
4221
|
+
if (mounted.current) setTogglingCredits(void 0);
|
|
4222
|
+
}
|
|
4223
|
+
};
|
|
4143
4224
|
const toggleCredits = async (item, shown) => {
|
|
4144
4225
|
if (settingsScope === void 0) return;
|
|
4145
4226
|
setTogglingCredits(item);
|
|
@@ -4446,6 +4527,40 @@ window.__ModuleLoader__.load({
|
|
|
4446
4527
|
}
|
|
4447
4528
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: activeRegion === "cn" ? t("row.showCreditsCn") : t("row.showCreditsGlobal") })]
|
|
4448
4529
|
});
|
|
4530
|
+
/**
|
|
4531
|
+
* WHERE the readout sits — a two-way choice, active tab's region only
|
|
4532
|
+
* (issue #62).
|
|
4533
|
+
*
|
|
4534
|
+
* It sits directly under the show/hide switch because the two answer one
|
|
4535
|
+
* question together ("show it, and where"), and because a user only reaches
|
|
4536
|
+
* for this when the default seat is unavailable — typically after installing
|
|
4537
|
+
* another plugin that occupies the composer row. Rendered as a `<select>`
|
|
4538
|
+
* rather than a pair of radios so it stays one compact row on the pool
|
|
4539
|
+
* heading, matching the switch beside it.
|
|
4540
|
+
*
|
|
4541
|
+
* Disabled with the region, exactly like the switch: a region that is off has
|
|
4542
|
+
* no readout to place.
|
|
4543
|
+
*/
|
|
4544
|
+
const creditPositionPicker = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4545
|
+
className: "dsm-workbuddy-credit-position-switch",
|
|
4546
|
+
title: t("row.creditPositionHint"),
|
|
4547
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
4548
|
+
className: "dsm-workbuddy-pool-select dsm-workbuddy-credit-position",
|
|
4549
|
+
value: activeRegionOn ? creditPositionOn(activeRegion) : "composer",
|
|
4550
|
+
disabled: !activeRegionOn || togglingCredits === activeRegion || !canWrite || saving || poolBusy,
|
|
4551
|
+
"aria-label": t("row.creditPositionAria", { region: activeRegion === "cn" ? t("row.tabCn") : t("row.tabGlobal") }),
|
|
4552
|
+
onChange: (event) => {
|
|
4553
|
+
changeCreditPosition(activeRegion, event.currentTarget.value === "stats" ? "stats" : "composer");
|
|
4554
|
+
},
|
|
4555
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4556
|
+
value: "composer",
|
|
4557
|
+
children: t("row.creditPositionComposer")
|
|
4558
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4559
|
+
value: "stats",
|
|
4560
|
+
children: t("row.creditPositionStats")
|
|
4561
|
+
})]
|
|
4562
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.creditPositionLabel") })]
|
|
4563
|
+
});
|
|
4449
4564
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
4450
4565
|
className: `dsm-plugin-card${open ? " dsm-plugin-card-open" : ""}`,
|
|
4451
4566
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
@@ -4586,15 +4701,15 @@ window.__ModuleLoader__.load({
|
|
|
4586
4701
|
},
|
|
4587
4702
|
selectingAccount: switchingAccount,
|
|
4588
4703
|
accountTools,
|
|
4589
|
-
...status.pool === void 0 ? {} : { headerExtra: creditsSwitch },
|
|
4704
|
+
...status.pool === void 0 ? {} : { headerExtra: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [creditsSwitch, creditPositionPicker] }) },
|
|
4590
4705
|
siblingBusy: saving || togglingRegion !== void 0,
|
|
4591
4706
|
onBusyChange: setPoolBusy,
|
|
4592
4707
|
onSaved: async () => await refreshUsage(activeRegion) !== void 0,
|
|
4593
4708
|
onRefresh: () => refreshUsage(activeRegion)
|
|
4594
4709
|
}),
|
|
4595
|
-
status.pool === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
4710
|
+
status.pool === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4596
4711
|
className: "dsm-workbuddy-credits-switch-row",
|
|
4597
|
-
children: creditsSwitch
|
|
4712
|
+
children: [creditsSwitch, creditPositionPicker]
|
|
4598
4713
|
}) : null,
|
|
4599
4714
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
4600
4715
|
className: "dsm-workbuddy-models",
|
|
@@ -5380,6 +5495,17 @@ window.__ModuleLoader__.load({
|
|
|
5380
5495
|
* rows). The composer row is per-session and provider-scoped, so exactly one
|
|
5381
5496
|
* plugin's credits are relevant at a time and nothing overlaps.
|
|
5382
5497
|
*
|
|
5498
|
+
* WHY THERE IS A SECOND SEAT (issue #62). The composer tool row is a LIST seat,
|
|
5499
|
+
* so it is shared not only with the shell's own controls but with any other
|
|
5500
|
+
* plugin that registers there — and one report is exactly that: with a
|
|
5501
|
+
* third-party plugin installed the readout stopped appearing at all. A list seat
|
|
5502
|
+
* cannot promise that every occupant is visible, so the seat is the USER's
|
|
5503
|
+
* choice: `composer` (default, unchanged for anyone who does not choose) or
|
|
5504
|
+
* `stats`, the ambient dock below the composer card where the shell renders its
|
|
5505
|
+
* own round/token pills. The gate is registered in BOTH seats and renders in the
|
|
5506
|
+
* one the setting names, which keeps the decision in one function instead of
|
|
5507
|
+
* re-registering slots when the setting changes.
|
|
5508
|
+
*
|
|
5383
5509
|
* Kept separate from {@link ComposerCredits} (and from the browser-plugin
|
|
5384
5510
|
* entry) so the decision can be unit-tested without a Host: the entry imports
|
|
5385
5511
|
* browser-only DSH packages the test environment cannot load, while this module
|
|
@@ -5416,6 +5542,16 @@ window.__ModuleLoader__.load({
|
|
|
5416
5542
|
* the authority; the scope is the fallback for a Host line that predates the
|
|
5417
5543
|
* field.
|
|
5418
5544
|
*/
|
|
5545
|
+
/**
|
|
5546
|
+
* The settings tree this plugin writes, unwrapped to a plain object.
|
|
5547
|
+
*
|
|
5548
|
+
* `unwrapVolatileDeep` first: the scope's value is a live cosmokit reference on
|
|
5549
|
+
* the 0.1.7 line, and reading fields off it without unwrapping yields proxies
|
|
5550
|
+
* whose `===` comparisons against a string are false.
|
|
5551
|
+
*/
|
|
5552
|
+
function scopeValueOf(scope) {
|
|
5553
|
+
return scope === void 0 ? void 0 : unwrapVolatileDeep(scope.getSnapshot().value);
|
|
5554
|
+
}
|
|
5419
5555
|
function composerCreditsRegionOf(projection, scope, hostValue) {
|
|
5420
5556
|
const provider = selectedProviderOf(projection);
|
|
5421
5557
|
const region = provider === void 0 ? void 0 : WORKBUDDY_COMPOSER_PROVIDERS[provider];
|
|
@@ -5427,7 +5563,7 @@ window.__ModuleLoader__.load({
|
|
|
5427
5563
|
return region;
|
|
5428
5564
|
}
|
|
5429
5565
|
function ComposerCreditsGate(props) {
|
|
5430
|
-
const { t, settingsScope, useProjection } = props;
|
|
5566
|
+
const { t, position, settingsScope, useProjection } = props;
|
|
5431
5567
|
const [, setRevision] = (0, react.useState)(0);
|
|
5432
5568
|
(0, react.useEffect)(() => settingsScope?.subscribe(() => {
|
|
5433
5569
|
setRevision((value) => value + 1);
|
|
@@ -5435,6 +5571,7 @@ window.__ModuleLoader__.load({
|
|
|
5435
5571
|
const projection = useProjection === void 0 ? void 0 : useProjection("modelSelection");
|
|
5436
5572
|
const region = composerCreditsRegionOf(projection, settingsScope);
|
|
5437
5573
|
if (region === void 0) return null;
|
|
5574
|
+
if (regionCreditPositionOf(scopeValueOf(settingsScope), region) !== (position ?? "composer")) return null;
|
|
5438
5575
|
const provider = selectedProviderOf(projection);
|
|
5439
5576
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerCredits, {
|
|
5440
5577
|
t,
|
|
@@ -5444,6 +5581,77 @@ window.__ModuleLoader__.load({
|
|
|
5444
5581
|
});
|
|
5445
5582
|
}
|
|
5446
5583
|
//#endregion
|
|
5584
|
+
//#region src/client/WorkBuddyQuotaNotice.tsx
|
|
5585
|
+
/**
|
|
5586
|
+
* The supplement this plugin adds to the host's quota notice (issue #63).
|
|
5587
|
+
*
|
|
5588
|
+
* WHY THIS EXISTS. When an upstream failure is classified `QUOTA`, the host
|
|
5589
|
+
* renders ONE fixed sentence — 「当前请求的额度已用尽」 — and (measured on the
|
|
5590
|
+
* shipped client) it DISCARDS the provider's own message to do so:
|
|
5591
|
+
*
|
|
5592
|
+
* function failureMessage(message, code, t) {
|
|
5593
|
+
* if (code === 'QUOTA' || code === 'ACCOUNT_QUOTA') return t('message.failure.quota')
|
|
5594
|
+
* return code === 'AUTH' ? t('message.failure.auth') : message
|
|
5595
|
+
* }
|
|
5596
|
+
*
|
|
5597
|
+
* That sentence is right for a genuinely exhausted account (14018), and wrong
|
|
5598
|
+
* for `6004` — the upstream's own words are 「您的使用量已超出频率限制,将在
|
|
5599
|
+
* <时刻> 重置」, and the reporter's account still held 6,788 credits. The
|
|
5600
|
+
* plugin cannot fix that by re-classifying: measured against the shipped
|
|
5601
|
+
* classifier, every spelling that stops reading as `QUOTA` falls into
|
|
5602
|
+
* `RATE_LIMIT`, which IS in `DEFAULT_RETRYABLE_CODES` — so it would buy an
|
|
5603
|
+
* honest sentence with 5 in-place retries and ~15.5s of backoff on a limit that
|
|
5604
|
+
* resets hours later. Staying out of the retry set is the behaviour we keep
|
|
5605
|
+
* (the maintainer's call on issue #63); the sentence is what we supplement.
|
|
5606
|
+
*
|
|
5607
|
+
* HOW. The host ships a CHAIN seat for exactly this: `shell.quota-notice`, a
|
|
5608
|
+
* frame-wide overlay whose own comment says "a `shell.quota-notice` entry may
|
|
5609
|
+
* replace the generic Toast for codes it claims". The chain hands a claiming
|
|
5610
|
+
* entry the host's own owner object — `{ code, message, dismiss, keepOpen }` —
|
|
5611
|
+
* so this component can keep the host's sentence VERBATIM and append the one
|
|
5612
|
+
* fact the sentence is missing. Claiming only `QUOTA` leaves every other code on
|
|
5613
|
+
* the host's fallback Toast.
|
|
5614
|
+
*
|
|
5615
|
+
* WHY THE HOST'S SENTENCE IS KEPT, not rewritten: it is the string the shell
|
|
5616
|
+
* shows everywhere else for this code, and a plugin that reworded it would
|
|
5617
|
+
* contradict the shell's own vocabulary. The supplement is additive, and it is
|
|
5618
|
+
* written to be TRUE FOR BOTH CASES the code covers — a model's own daily quota
|
|
5619
|
+
* (not credits) and a genuinely empty balance — because the plugin cannot always
|
|
5620
|
+
* tell which one it is: the upstream states it in Chinese prose the plugin is
|
|
5621
|
+
* not allowed to parse for policy.
|
|
5622
|
+
*
|
|
5623
|
+
* Rendered as plain DOM with this plugin's own classes: the client half
|
|
5624
|
+
* deliberately does not depend on `@deepseek-ai/dsh-client-ui-primitives` (see
|
|
5625
|
+
* the note in `src/client/index.tsx` — a stale inject entry there once made the
|
|
5626
|
+
* whole card's arrival depend on the churn-heaviest host package), so the
|
|
5627
|
+
* notice is built from the same primitives every other component here uses.
|
|
5628
|
+
*/
|
|
5629
|
+
/**
|
|
5630
|
+
* Which notice codes this plugin claims.
|
|
5631
|
+
*
|
|
5632
|
+
* `QUOTA` only — it is what the shipped classifier produces for BOTH failures
|
|
5633
|
+
* this plugin raises (`6004` account rate limit and `14018` exhausted credits),
|
|
5634
|
+
* and claiming anything else would take over a notice this plugin knows nothing
|
|
5635
|
+
* about. Exported so the claim is unit-testable without a host.
|
|
5636
|
+
*/
|
|
5637
|
+
const WORKBUDDY_QUOTA_NOTICE_CODES = ["QUOTA"];
|
|
5638
|
+
/** Whether this plugin should take over the notice for one owner object. */
|
|
5639
|
+
function claimsQuotaNotice(code) {
|
|
5640
|
+
return typeof code === "string" && WORKBUDDY_QUOTA_NOTICE_CODES.includes(code);
|
|
5641
|
+
}
|
|
5642
|
+
/** The frame-wide supplement shown under the host's sentence. */
|
|
5643
|
+
function WorkBuddyQuotaNotice(props) {
|
|
5644
|
+
const { matched, t } = props;
|
|
5645
|
+
return (0, react.createElement)("div", {
|
|
5646
|
+
className: "dsm-workbuddy-quota-notice",
|
|
5647
|
+
role: "status"
|
|
5648
|
+
}, (0, react.createElement)("span", { className: "dsm-workbuddy-quota-notice-head" }, matched.message ?? t("quotaNotice.fallbackHead")), (0, react.createElement)("span", { className: "dsm-workbuddy-quota-notice-body" }, t("quotaNotice.supplement")), matched.dismiss === void 0 ? null : (0, react.createElement)("button", {
|
|
5649
|
+
type: "button",
|
|
5650
|
+
className: "dsm-btn dsm-btn-outline dsm-workbuddy-quota-notice-dismiss",
|
|
5651
|
+
onClick: () => matched.dismiss?.()
|
|
5652
|
+
}, t("quotaNotice.dismiss")));
|
|
5653
|
+
}
|
|
5654
|
+
//#endregion
|
|
5447
5655
|
//#region src/client/locales.ts
|
|
5448
5656
|
/**
|
|
5449
5657
|
* Plugin-card copy registered under the settings.workbuddy locale namespace.
|
|
@@ -5704,6 +5912,14 @@ window.__ModuleLoader__.load({
|
|
|
5704
5912
|
"row.showCreditsCn": "WB CN credits in composer",
|
|
5705
5913
|
"row.showCreditsGlobal": "WB AI credits in composer",
|
|
5706
5914
|
"row.showCreditsCnHint": "Show the domestic (WB CN) credit balance in the composer toolbar, but only while this session uses a WB CN model. Click it for per-account balances and a manual refresh (refreshes by itself every 5 minutes).",
|
|
5915
|
+
"row.creditPositionLabel": "position",
|
|
5916
|
+
"row.creditPositionComposer": "composer toolbar",
|
|
5917
|
+
"row.creditPositionStats": "below the input box",
|
|
5918
|
+
"row.creditPositionAria": "Credit readout position for {region}",
|
|
5919
|
+
"quotaNotice.fallbackHead": "The quota for this request is exhausted",
|
|
5920
|
+
"quotaNotice.supplement": "Some models have their own daily limit that is unrelated to your credit balance — check the account pool row for the stated reset time, or switch account or model to continue.",
|
|
5921
|
+
"quotaNotice.dismiss": "Got it",
|
|
5922
|
+
"row.creditPositionHint": "Where the credit readout sits. \"composer toolbar\" is the default (the row with the model picker). Choose \"below the input box\" — the row carrying the round / step / token statistics — if another plugin occupies the composer toolbar and the readout does not appear.",
|
|
5707
5923
|
"row.showCreditsGlobalHint": "Show the international (WB AI) credit balance in the composer toolbar, but only while this session uses a WB AI model. Click it for per-account balances and a manual refresh (refreshes by itself every 5 minutes).",
|
|
5708
5924
|
"composer.pointsCn": "WB CN",
|
|
5709
5925
|
"composer.pointsGlobal": "WB AI",
|
|
@@ -5975,6 +6191,14 @@ window.__ModuleLoader__.load({
|
|
|
5975
6191
|
"row.showCreditsCn": "对话框显示 WB CN 积分",
|
|
5976
6192
|
"row.showCreditsGlobal": "对话框显示 WB AI 积分",
|
|
5977
6193
|
"row.showCreditsCnHint": "在对话输入框工具栏显示国内版(WB CN)剩余积分,仅当本会话使用 WB CN 模型时出现。点击可看各账号余额并手动刷新(每 5 分钟也会自动刷新)。",
|
|
6194
|
+
"row.creditPositionLabel": "位置",
|
|
6195
|
+
"row.creditPositionComposer": "输入框工具栏",
|
|
6196
|
+
"row.creditPositionStats": "输入框下方",
|
|
6197
|
+
"row.creditPositionAria": "{region} 的积分显示位置",
|
|
6198
|
+
"quotaNotice.fallbackHead": "当前请求的额度已用尽",
|
|
6199
|
+
"quotaNotice.supplement": "部分模型另有每日额度限制,与积分余额无关 —— 也可能确实是本账号积分耗尽。账号池那一行会写明上游给出的恢复时刻;也可以先换个账号或换个模型继续。",
|
|
6200
|
+
"quotaNotice.dismiss": "知道了",
|
|
6201
|
+
"row.creditPositionHint": "积分显示在哪里。「输入框工具栏」是默认位置(与模型选择同一行)。如果装了别的插件后读数不显示,改成「输入框下方」—— 那是「轮次 / 步数 / token」统计所在的那一行。",
|
|
5978
6202
|
"row.showCreditsGlobalHint": "在对话输入框工具栏显示国际版(WB AI)剩余积分,仅当本会话使用 WB AI 模型时出现。点击可看各账号余额并手动刷新(每 5 分钟也会自动刷新)。",
|
|
5979
6203
|
"composer.pointsCn": "WB CN",
|
|
5980
6204
|
"composer.pointsGlobal": "WB AI",
|
|
@@ -6098,8 +6322,12 @@ window.__ModuleLoader__.load({
|
|
|
6098
6322
|
id: "dsh-connect-workbuddy-composer-credits",
|
|
6099
6323
|
order: 4,
|
|
6100
6324
|
locale: namespace,
|
|
6101
|
-
inject: () => settingsScope === void 0 ? {
|
|
6325
|
+
inject: () => settingsScope === void 0 ? {
|
|
6102
6326
|
t,
|
|
6327
|
+
position: "composer"
|
|
6328
|
+
} : {
|
|
6329
|
+
t,
|
|
6330
|
+
position: "composer",
|
|
6103
6331
|
settingsScope
|
|
6104
6332
|
}
|
|
6105
6333
|
}, ComposerCreditsGate));
|
|
@@ -6109,6 +6337,80 @@ window.__ModuleLoader__.load({
|
|
|
6109
6337
|
}
|
|
6110
6338
|
};
|
|
6111
6339
|
/**
|
|
6340
|
+
* The SAME readout in the ambient dock below the composer card (issue #62).
|
|
6341
|
+
*
|
|
6342
|
+
* Registered unconditionally, like the composer seat, because the CHOICE
|
|
6343
|
+
* between the two seats lives in the gate: it renders in the seat the
|
|
6344
|
+
* setting names and returns null in the other. Registering only the chosen
|
|
6345
|
+
* seat would mean re-registering on every settings change, and the seat is
|
|
6346
|
+
* exactly the kind of thing a user flips once while looking at the result.
|
|
6347
|
+
*
|
|
6348
|
+
* WHY THIS SEAT EXISTS: the composer tool row is a list seat shared with any
|
|
6349
|
+
* other plugin that registers there, and issue #62 is one report of the
|
|
6350
|
+
* readout disappearing when another plugin was installed. This row is where
|
|
6351
|
+
* the shell renders its own round/step/token pills, so it is a place a user
|
|
6352
|
+
* already reads session numbers from.
|
|
6353
|
+
*
|
|
6354
|
+
* `order` places it after the shell's own pills (`stats` registers at 0), so
|
|
6355
|
+
* the shipped statistics stay leftmost and the readout reads as an addition
|
|
6356
|
+
* rather than a replacement.
|
|
6357
|
+
*/
|
|
6358
|
+
const registerStatsCredits = () => {
|
|
6359
|
+
try {
|
|
6360
|
+
return ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
|
|
6361
|
+
name: "conversation.composer.dock",
|
|
6362
|
+
id: "dsh-connect-workbuddy-stats-credits",
|
|
6363
|
+
order: 10,
|
|
6364
|
+
locale: namespace,
|
|
6365
|
+
inject: () => settingsScope === void 0 ? {
|
|
6366
|
+
t,
|
|
6367
|
+
position: "stats"
|
|
6368
|
+
} : {
|
|
6369
|
+
t,
|
|
6370
|
+
position: "stats",
|
|
6371
|
+
settingsScope
|
|
6372
|
+
}
|
|
6373
|
+
}, ComposerCreditsGate));
|
|
6374
|
+
} catch (error) {
|
|
6375
|
+
console.error("[dsh-connect-workbuddy] stats credits slot failed to register (host provider unaffected):", error);
|
|
6376
|
+
return;
|
|
6377
|
+
}
|
|
6378
|
+
};
|
|
6379
|
+
/**
|
|
6380
|
+
* The frame-wide quota notice, claimed so the supplement can be shown
|
|
6381
|
+
* (issue #63).
|
|
6382
|
+
*
|
|
6383
|
+
* The host renders ONE fixed sentence for a `QUOTA` failure — 「当前请求的
|
|
6384
|
+
* 额度已用尽」 — and discards the provider's message while doing it. For
|
|
6385
|
+
* `6004` that sentence is wrong (the upstream's own words say 「频率限制」,
|
|
6386
|
+
* with a reset time, and the reporting account still held thousands of
|
|
6387
|
+
* credits), but re-classifying is NOT the fix: measured against the shipped
|
|
6388
|
+
* classifier, every spelling that stops reading as `QUOTA` lands in
|
|
6389
|
+
* `RATE_LIMIT`, which is IN the host's retry set — 5 in-place retries and
|
|
6390
|
+
* ~15.5s of backoff on a limit that resets hours later. So the behaviour
|
|
6391
|
+
* (no retry) stays and the sentence gets a SUPPLEMENT.
|
|
6392
|
+
*
|
|
6393
|
+
* `select` claims `QUOTA` only, and the chain is the host's own designed
|
|
6394
|
+
* extension point for this ("a shell.quota-notice entry may replace the
|
|
6395
|
+
* generic Toast for codes it claims") — every other code keeps the host's
|
|
6396
|
+
* fallback Toast untouched.
|
|
6397
|
+
*/
|
|
6398
|
+
const registerQuotaNotice = () => {
|
|
6399
|
+
try {
|
|
6400
|
+
return ctx.slots.inject("shell.quota-notice", () => ctx.slots.register({
|
|
6401
|
+
name: "shell.quota-notice",
|
|
6402
|
+
locale: namespace,
|
|
6403
|
+
select: (owner) => {
|
|
6404
|
+
return claimsQuotaNotice(typeof owner === "object" && owner !== null ? owner.code : void 0) ? owner : null;
|
|
6405
|
+
},
|
|
6406
|
+
inject: () => ({ t })
|
|
6407
|
+
}, WorkBuddyQuotaNotice));
|
|
6408
|
+
} catch (error) {
|
|
6409
|
+
console.error("[dsh-connect-workbuddy] quota notice slot failed to register (host provider unaffected):", error);
|
|
6410
|
+
return;
|
|
6411
|
+
}
|
|
6412
|
+
};
|
|
6413
|
+
/**
|
|
6112
6414
|
* Every slot this plugin contributes, and how to (re)create them.
|
|
6113
6415
|
*
|
|
6114
6416
|
* Kept as one list because the binding of `settingsScope` and the slot
|
|
@@ -6126,7 +6428,9 @@ window.__ModuleLoader__.load({
|
|
|
6126
6428
|
const registerAllSlots = () => [
|
|
6127
6429
|
...WORKBUDDY_CARD_KEYS.map((key) => registerCard("plugins.bundle.config", key)),
|
|
6128
6430
|
...WORKBUDDY_CARD_KEYS.map((key) => registerCard("plugins.row.config", `${key}#${key}`)),
|
|
6129
|
-
registerComposerCredits()
|
|
6431
|
+
registerComposerCredits(),
|
|
6432
|
+
registerStatsCredits(),
|
|
6433
|
+
registerQuotaNotice()
|
|
6130
6434
|
].filter((disposer) => disposer !== void 0);
|
|
6131
6435
|
/**
|
|
6132
6436
|
* Disposers of the slots registered for the CURRENT binding.
|
|
@@ -3825,7 +3825,7 @@ function generationOf(version) {
|
|
|
3825
3825
|
//#endregion
|
|
3826
3826
|
//#region src/version.ts
|
|
3827
3827
|
/** The npm package version this build was produced from. */
|
|
3828
|
-
const WORKBUDDY_CONNECT_VERSION = "4.
|
|
3828
|
+
const WORKBUDDY_CONNECT_VERSION = "4.5.1";
|
|
3829
3829
|
//#endregion
|
|
3830
3830
|
//#region src/host-heartbeat.ts
|
|
3831
3831
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -2173,6 +2173,16 @@ declare function regionEnabledOf(value: unknown, region: WorkBuddyWebRegion): bo
|
|
|
2173
2173
|
/** Build the next `regions` settings value for a signed-in tab's save. */
|
|
2174
2174
|
declare function nextRegionSlots<Slot extends object>(regions: unknown, region: WorkBuddyWebRegion, slot: Slot): Record<string, unknown>;
|
|
2175
2175
|
declare function nextRegionEnabled(value: unknown, region: WorkBuddyWebRegion, enabled: boolean): Record<string, unknown>;
|
|
2176
|
+
/**
|
|
2177
|
+
* The two seats the credit readout can occupy (issue #62).
|
|
2178
|
+
*
|
|
2179
|
+
* `composer` is the composer tool row (`conversation.input.left`); `stats` is
|
|
2180
|
+
* the ambient dock below the composer card (`conversation.composer.dock`),
|
|
2181
|
+
* beside the shell's own round/step/token pills. Named as a union rather than a
|
|
2182
|
+
* boolean because a third seat is plausible and `showAtTop: boolean` would have
|
|
2183
|
+
* to be migrated rather than extended.
|
|
2184
|
+
*/
|
|
2185
|
+
type WorkBuddyCreditPosition = 'composer' | 'stats';
|
|
2176
2186
|
/** One credit package as the upstream returns it, node-free. */
|
|
2177
2187
|
interface WorkBuddyWebCreditPackage {
|
|
2178
2188
|
packageName: string;
|
|
@@ -2361,6 +2371,8 @@ type WorkBuddyWebUsage =
|
|
|
2361
2371
|
* "关闭不了" report.
|
|
2362
2372
|
*/
|
|
2363
2373
|
showCreditsInMainUi?: boolean;
|
|
2374
|
+
/** WHERE this region's readout sits (issue #62); absent means `composer`. */
|
|
2375
|
+
creditPosition?: WorkBuddyCreditPosition;
|
|
2364
2376
|
message?: string;
|
|
2365
2377
|
/** The persisted account id matches no local account. */
|
|
2366
2378
|
selectionLost?: boolean;
|
|
@@ -2395,6 +2407,8 @@ type WorkBuddyWebUsage =
|
|
|
2395
2407
|
* settings mirror.
|
|
2396
2408
|
*/
|
|
2397
2409
|
showCreditsInMainUi?: boolean;
|
|
2410
|
+
/** WHERE this region's readout sits (issue #62); absent means `composer`. */
|
|
2411
|
+
creditPosition?: WorkBuddyCreditPosition;
|
|
2398
2412
|
source?: 'desktop' | 'dsh';
|
|
2399
2413
|
tokenExpiresAtMs: number;
|
|
2400
2414
|
/** A saved per-region choice is in effect (false = following the app). */
|
|
@@ -2784,6 +2798,13 @@ interface WorkBuddyStatusRouteOptions {
|
|
|
2784
2798
|
* absent reads as ON, matching the schema's `default(true)`.
|
|
2785
2799
|
*/
|
|
2786
2800
|
showCreditsInMainUi?(region: WorkBuddyRegion): boolean;
|
|
2801
|
+
/**
|
|
2802
|
+
* WHERE this region's credit readout sits (issue #62).
|
|
2803
|
+
*
|
|
2804
|
+
* Optional, and absent reads as `composer` — the only seat before the setting
|
|
2805
|
+
* existed, so a Host that predates it renders exactly as it used to.
|
|
2806
|
+
*/
|
|
2807
|
+
creditPosition?(region: WorkBuddyRegion): WorkBuddyCreditPosition;
|
|
2787
2808
|
/**
|
|
2788
2809
|
* Send one real-volume request to each named model of a region and report what
|
|
2789
2810
|
* came back.
|
|
@@ -3107,6 +3128,25 @@ interface WorkBuddyRegionState {
|
|
|
3107
3128
|
* written before this switch existed keeps whatever the default was.
|
|
3108
3129
|
*/
|
|
3109
3130
|
showCreditsInMainUi?: boolean;
|
|
3131
|
+
/**
|
|
3132
|
+
* WHERE this region's credit readout sits (issue #62).
|
|
3133
|
+
*
|
|
3134
|
+
* Two seats, because the first one is not always available:
|
|
3135
|
+
*
|
|
3136
|
+
* - `composer` — the composer tool row (`conversation.input.left`), shared
|
|
3137
|
+
* with the shell's own controls. It is the closer, more glanceable seat,
|
|
3138
|
+
* and it is the default so nothing moves for anyone who does not choose.
|
|
3139
|
+
* - `stats` — the ambient dock below the composer card
|
|
3140
|
+
* (`conversation.composer.dock`), where the shell renders its own
|
|
3141
|
+
* round/step/token pills. That row is a LIST seat whose shipped occupant
|
|
3142
|
+
* registers separately, so a third-party plugin that ALSO parks something
|
|
3143
|
+
* in the composer row cannot crowd this readout out of existence.
|
|
3144
|
+
*
|
|
3145
|
+
* The report behind this (issue #62) was exactly that: with another plugin
|
|
3146
|
+
* installed, the readout vanished from the composer row. Making the seat a
|
|
3147
|
+
* choice is what keeps one plugin's presence from deciding another's.
|
|
3148
|
+
*/
|
|
3149
|
+
creditPosition?: WorkBuddyCreditPosition;
|
|
3110
3150
|
/** The last-refreshed directory for this region; what the card displays. */
|
|
3111
3151
|
lastCatalog?: WorkBuddyModelInfo[];
|
|
3112
3152
|
/** The user's selection in this region, as model ids. */
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-
|
|
1
|
+
import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-Vd2tNdsv.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -4207,6 +4207,7 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
4207
4207
|
deps.regionUsable?.(region, accounts.length > 0);
|
|
4208
4208
|
const enabled = deps.regionEnabled(region);
|
|
4209
4209
|
const showCreditsInMainUi = deps.showCreditsInMainUi?.(region) ?? true;
|
|
4210
|
+
const creditPosition = deps.creditPosition?.(region) ?? "composer";
|
|
4210
4211
|
const authStatus = await store.status();
|
|
4211
4212
|
const selectionExplicit = store.hasExplicitSelection();
|
|
4212
4213
|
if (authStatus.state !== "signed-out" && accounts.length === 0) return {
|
|
@@ -4228,6 +4229,7 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
4228
4229
|
selectionExplicit,
|
|
4229
4230
|
enabled,
|
|
4230
4231
|
showCreditsInMainUi,
|
|
4232
|
+
creditPosition,
|
|
4231
4233
|
...await store.selectionLost() ? { selectionLost: true } : {},
|
|
4232
4234
|
...webAccounts.length === 0 ? await searchedPaths(store) : {}
|
|
4233
4235
|
};
|
|
@@ -4243,6 +4245,7 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
4243
4245
|
selectionExplicit,
|
|
4244
4246
|
enabled,
|
|
4245
4247
|
showCreditsInMainUi,
|
|
4248
|
+
creditPosition,
|
|
4246
4249
|
accounts: accounts.map(toWebAccount),
|
|
4247
4250
|
models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))),
|
|
4248
4251
|
enabledModelIds: [...deps.enabledModelIds(region)],
|
|
@@ -5013,6 +5016,7 @@ const regionStateConfig = z.object({
|
|
|
5013
5016
|
* which is reachable exactly where the feature renders.
|
|
5014
5017
|
*/
|
|
5015
5018
|
showCreditsInMainUi: z.boolean().default(true).description("Show this region's remaining credits at the DSH sidebar foot (opt-out; the line carries its own hide button)"),
|
|
5019
|
+
creditPosition: z.union([z.const("composer"), z.const("stats")]).default("composer").description("Where the credit readout sits: \"composer\" = the composer tool row (default), \"stats\" = the ambient row below the composer card, next to the shell's own token pills. Choose \"stats\" if another plugin occupies the composer row."),
|
|
5016
5020
|
lastCatalog: z.array(modelConfig).default([]),
|
|
5017
5021
|
enabledModelIds: z.array(z.string()).default([]),
|
|
5018
5022
|
imageModelIds: z.array(z.string()).default([]),
|
|
@@ -5405,6 +5409,13 @@ function apply(ctx, config) {
|
|
|
5405
5409
|
*/
|
|
5406
5410
|
showCreditsInMainUi: (region) => regionStateOf(current(), region).showCreditsInMainUi !== false,
|
|
5407
5411
|
/**
|
|
5412
|
+
* WHERE the readout sits, resolved here rather than read from the browser
|
|
5413
|
+
* settings mirror, for the same measured reason as the flag above: on the
|
|
5414
|
+
* affected 0.1.7 deployments that mirror never picks up this plugin's
|
|
5415
|
+
* writes, and a control reading it would keep showing the old seat.
|
|
5416
|
+
*/
|
|
5417
|
+
creditPosition: (region) => creditPositionOf(current(), region),
|
|
5418
|
+
/**
|
|
5408
5419
|
* One minimal request per named model, through the region's own credential.
|
|
5409
5420
|
*
|
|
5410
5421
|
* Resolved here rather than in the route so the browser half never touches a
|
|
@@ -5950,6 +5961,14 @@ function apply(ctx, config) {
|
|
|
5950
5961
|
return resolveTargetModel(displayModels(current(), region), targetModelPreference(poolPreferencesOf(current(), region), accountId)).modelId;
|
|
5951
5962
|
};
|
|
5952
5963
|
/** One region's pool preferences, with the schema's defaults applied. */
|
|
5964
|
+
/**
|
|
5965
|
+
* WHERE this region's credit readout sits (issue #62).
|
|
5966
|
+
*
|
|
5967
|
+
* A local reader rather than a schema default so a config written before the
|
|
5968
|
+
* setting existed reads as `composer` — the seat that was the only one, so an
|
|
5969
|
+
* upgrade moves nothing on screen.
|
|
5970
|
+
*/
|
|
5971
|
+
const creditPositionOf = (config, region) => regionStateOf(config, region).creditPosition === "stats" ? "stats" : "composer";
|
|
5953
5972
|
const poolPreferencesOf = (config, region) => {
|
|
5954
5973
|
const pool = regionStateOf(config, region).pool;
|
|
5955
5974
|
const probeInputTokens = pool?.probeInputTokens;
|
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": "4.
|
|
5
|
+
"version": "4.5.1",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|