dsh-connect-workbuddy 5.8.1 → 5.8.3
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 +66 -0
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +347 -8
- package/lib/{host-heartbeat-CM-09T4U.js → host-heartbeat-DzfWuVsm.js} +147 -13
- package/lib/index.d.ts +61 -1
- package/lib/index.js +189 -40
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.8.3 (2026-10-24)
|
|
4
|
+
|
|
5
|
+
> 体验版本:把 #132/#133 那几项「只能手改 `cordis.patch.yml` 才生效」的开关搬进插件卡片,用户可以直接勾选,不用再编辑 profile 里的 YAML、也不用知道那个文件在哪。**请求路径、默认字节、计费行为均未改动**:四个开关的默认值与采样时机与 5.8.0 完全一致(三个布尔默认关闭),这次只新增了一条写入它们的 UI 路径。
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **上游标识开关进入设置卡片(#133 建议 2/3)。**
|
|
10
|
+
- 卡片「能力」页新增一组开关:`usageIdentity`(让用量页把调用记成官方客户端的样子)、`keepPromptLast`(让用量页记录你真正输入的话)、`sessionTitle`(会话标题请求改用轻量模型)。这三项此前只存在于 `cordis.patch.yml`,用户要手改 profile 里的 YAML 才知道有这个功能。
|
|
11
|
+
- 每个开关都带一行说明和一个「改完需重载插件或重启 DSH」的统一提示——这四项都在插件挂载时采样进 shim,配置改了但没重载就看不到变化。`sessionTitle` 开启时额外显示当前目标模型(只读:该 id 仍由 `sessionTitle.model` 配置,不在这里做第二个未经校验的编辑入口)。
|
|
12
|
+
- `extraInjectedPrefixes` **故意不给输入框**:它是一个字符串数组,塞进复选框形状的 UI 只会比配置文件更难用。改为在 `keepPromptLast` 开启时**报告**当前生效条数,让手写过前缀的用户能确认插件确实看见了。
|
|
13
|
+
- 新增 Host 端点字段:`__save` 现在接受 `usageIdentity` / `keepPromptLast` / `extraInjectedPrefixes` / `sessionTitle` 四个顶层字段,各自 `set` 自己的路径。**不是**塞进 `cliBridge` 块:那四项管的是所有 provider 共享的请求形态,塞进去会让「关掉 CLI 路由」看起来像把它们一起重置了。
|
|
14
|
+
- 写入沿用既有两条写入器(Host 端点优先、设置作用域兜底):该命名空间的浏览器镜像在有问题的 0.1.7 部署上被文档标注为陈旧,只「看起来翻过去了」的开关正是「关闭不了」那一类报告。
|
|
15
|
+
|
|
16
|
+
### Tests
|
|
17
|
+
|
|
18
|
+
- 新增 [`tests/card-upstream-switches.spec.tsx`](tests/card-upstream-switches.spec.tsx)(9 条):三个开关按**已提交值**渲染、缺省一律读成 OFF(不是「truthy 就算开」)、翻动只写**本字段**、`sessionTitle` 写入保留 `model`(它的字段是对象,退化成布尔就会把用户设的目标模型抹掉)、POST 到 Host 端点、以及重载提示必须在屏上。读取器另有 3 条单独测(volatile 的 `{get()}` 解包、`sessionTitle` 的缺省/畸形回退、前缀列表的去空去重与非数组防御)。
|
|
19
|
+
- [`tests/web-status.spec.ts`](tests/web-status.spec.ts) 增 3 条端点用例:四个字段各自只写自己的路径、白名单外的字段被 400 拒绝且不触达设置服务、期望对象的字段收到标量时拒绝。
|
|
20
|
+
- 变异验证:把 `sessionTitle` 的写入退化成裸布尔 → 1 条红;把「缺省读 OFF」改成「缺省读 ON」→ 4 条红;把新开关的 class 改回 `.dsm-workbuddy-pref-check` → `card-credits-switch` 5 条红(它按「卡片里只有一个」读那个类)。
|
|
21
|
+
- 完整套件 **2268 通过 / 2 条按平台跳过**(此前 2256),`typecheck` 与 `build` 通过。
|
|
22
|
+
|
|
23
|
+
### Docs
|
|
24
|
+
|
|
25
|
+
- 卡片文案中英双语同步新增 8 条 key;`styles.ts` 补上两个新 class 的定义(`client-host-contract` 的「每个渲染出的 class 都必须有样式」守卫要求)。
|
|
26
|
+
|
|
27
|
+
## 5.8.2 (2026-10-24)
|
|
28
|
+
|
|
29
|
+
> 适配版本:跟随宿主 `0.2.1-rc.3`。rc.2 在 web-app 捆绑里新增了官方插件 `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017`(给 DeepSeek 官方 Flash 行画「1017」发布徽章),它与本插件注册**同一个 `single` 槽**、**同一个默认优先级 0**——于是本插件的注册**抛错**,模型选择器里的负载徽章**静默消失**。本版修掉这一处,其余全部兼容。
|
|
30
|
+
|
|
31
|
+
### Fixes
|
|
32
|
+
|
|
33
|
+
- **模型选择器的标签槽位不再与宿主自带的 1017 徽章插件抢同一格(0.2.1-rc.2+)。**
|
|
34
|
+
- 该槽是 `kind: "single"`,`SlotCore.register()` 对同一优先级上的第二次注册**直接抛错**:`single slot "conversation.input.model.label" already has a registration at priority 0 … (lowest renders)`。两个插件都用默认 `0`,谁后注册谁抛。实测是本插件抛——应用自己的崩溃日志里逐字记着这句,以及本插件的 `model label slot failed to register`。因为 `catch` 只 `console.error`(刻意的:标签失注册不该让整页挂掉),页面上**只是徽章不见了**,没有任何可见报错,所以一直没被发现。
|
|
35
|
+
- 修法只做一件事:**换一个不碰撞的优先级**。`-0.5`(分数优先级合法)与宿主的默认 `0`、以及 `dsh-connect-qoder` 占用的 `-1` 都不碰撞,三个插件于是排成 `-1`(qoder)→ `-0.5`(本插件)→ `0`(宿主 1017)。取值只要求「不与别人碰撞」,不需要知道别人用了几。
|
|
36
|
+
- **刻意不引入插件之间的联动**(项目不变量,见 [`docs/DESIGN.md`](docs/DESIGN.md) §零):不读 `slots.entries(...)` 枚举同槽的其它注册、不把别家的行「转交」给别的插件的组件渲染、不为对方合成 `t` 席位、不依赖别人的优先级。曾经的转交方案能算「更客气」,但它要代渲染器合成对方的 props(`t` 来自对方注册时声明的 `locale:` 命名空间),还得约定一条跨插件转交方向否则递归——实测两个插件都转交时同一行被来回传递,直接 `RangeError: Maximum call stack size exceeded`。这不是本插件该承担的责任,整体撤除。
|
|
37
|
+
- **代价(已接受并记录)**:该槽是 `single`,一格只有一个赢家。「最低优先级渲染」意味着 `-0.5` 实际上**赢过**宿主的 `0`,因此本插件赢下该格时,别的插件在**它自己的行**上的装饰不会被合成(宿主的「1017」徽章不会出现在本插件渲染的那一行)。不是自己的模型行,一律给宿主契约的下限 `model.name`。这是**放弃联动换来的确定性**,不是待修缺陷;要改成联动,先改 `DESIGN.md` §零。
|
|
38
|
+
- **取值 `-0.5` 的定夺依据(不需要用户来选)**:宿主一方早有书面立场——`dsh-client-ui-deepseek-model-label-1017` 的 README「Known Limitations and Deferred Work」逐字写着「the two presentations do not compose automatically」,并把处置办法写成**在 profile patch 里禁用它**(配置层,不是运行时协商)。所以「两个徽标不合成」是宿主的官方立场,本项目的 §零 不变量与之一致。其余三条:1017 的 package description 就是「**Temporary** 1017 release badge」,README 承诺结构化发布标签上线后移除该包;它只装饰**一个**模型(`DeepSeek-V4.1-Flash`,限 `deepseek-official`/`deepseek-account` 两条线路),而本插件装饰**自己全部 WorkBuddy 模型行**(负载等级、促销价、夜间折扣、新模型标记);`single` 槽「最低优先级渲染」,`-0.5` 实际赢过宿主的 `0`——但选它是为了**不碰撞**而非谦让,且 1017 被移除后本插件自动成为该格唯一占用者。
|
|
39
|
+
|
|
40
|
+
### Docs
|
|
41
|
+
|
|
42
|
+
- 新增 [`docs/DSH-0.2.1-RC3-FOLLOWUP.md`](docs/DSH-0.2.1-RC3-FOLLOWUP.md):rc.3 复核全文,含逐面包比对与本次回归的完整证据链。
|
|
43
|
+
- [`docs/DSH-0.2.1-RC1-FOLLOWUP.md`](docs/DSH-0.2.1-RC1-FOLLOWUP.md) 补 §2.4 **订正**:原文把「rc 宿主上照常加载」归因为「闸门只校验 `@deepseek-ai/dsh*` + profile 用 `link:`」,**这是错的**——闸门确实校验,只是用 `semver.satisfies(rt, range, { includePrerelease: true })` 放行。范围本身无需改(裸 semver 判 false 的事实仍成立)。
|
|
44
|
+
|
|
45
|
+
### Tests
|
|
46
|
+
|
|
47
|
+
- 新增 [`tests/model-label-slot-coexistence.spec.tsx`](tests/model-label-slot-coexistence.spec.tsx)(6 条),跑**真实的 `SlotCore`**:宿主 1017 在场时注册不抛错、优先级与 `-1`/`0` 均不碰撞、自家行仍有徽章;外加「不做联动」一组——别家行只给纯目录名(**即使**另一个贡献者就在账本上)、源码不得出现 `slots.entries(...)` / 渲染别家组件 / `locale.bind`、注册只声明自家常量。
|
|
48
|
+
- 变异验证:把 `MODEL_LABEL_PRIORITY` 改回 `0` → 多条红并**复现同一句崩溃原文**;把注册换成会读账本的写法 → 「不做联动」那组红。
|
|
49
|
+
- `tests/dsh-line.spec.ts` 里「已知预发布悬崖」的注释按实测订正归因(断言本身不变,仍钉「裸 semver 判 false」)。
|
|
50
|
+
- 完整套件 **2225 通过 / 2 条按平台跳过**(此前 2219),`typecheck` 与 `build` 通过。
|
|
51
|
+
|
|
3
52
|
## 5.8.1 (2026-10-24)
|
|
4
53
|
|
|
5
54
|
> 修复版本:解决 DSH 会话在国内区**每一步**都 `400 / 11128` 的死循环。根因不是账号、额度、网络,也不是「宿主提示词要你改」——是插件的救援梯子对网关的**精确子串门**有一个可证明的盲区:`You are a coding agent powered by <model> model.` 这条 agent 身份从句,稀疏零宽扰动打不到,于是每次请求都逐字节原样上线被拒。**凭据、计费、provider 注册均未改动**;请求路径新增的定向破坏只插不可见字符(实测真实提示 **+0.26% 字节**),不改模型读到的任何一个词。
|
|
@@ -55,6 +104,13 @@
|
|
|
55
104
|
- `current()` 会**先看**池的运行时轮换标记(`rotatedAccountId`),再看用户保存的选择;而 `accounts()` 回答 `selected` 时只看保存的选择。池子把请求轮换到 B 号后,shim 记在 B 号账上,卡片却把 A 号标成「当前选用」——截图里那枚标记落在被限流的号上,而胶囊与行内数字都属于另一个号。
|
|
56
105
|
- 同一份用量文档还会因此**点名两个账号**:`credential` 取自 `current()`(真正计费的那个),而 `account.accountId` / `accountName` 取自 `accounts()`(保存的那个)。现在 `accounts()` 用与 `current()` **完全相同**的优先级解析标记:有活跃轮换且该账号仍在本地,就标它;轮换指向已消失的账号时与 `current()` 一样回落;其余情况仍按「显式选择 / 默认最新」判断。清除轮换后标记随 `current()` 回到用户自己的选择,轮换本身仍然从不写设置。
|
|
57
106
|
- 面板里还有第二处同源分裂:行的**底色**读 credits 端点的 `selected`(保存的选择),**「当前选用」文字**读池文档的 `current`(被计费的账号)。池子一轮换,两者就指向不同行——报告者看到的「底色在一个号、文字在另一个号」。现在整行(底色、文字、切换按钮的禁用态)都读同一个答案;`selected` 只在没有池文档时才作为兜底。
|
|
107
|
+
- **新增 `sessionTitle` 开关:把每个新会话的标题请求改到隐藏轻量模型(#132/#133,报告者实测;本插件未复现)。**
|
|
108
|
+
- DSH 宿主每个新会话发一次标题生成请求,走与对话相同的 provider,按主付费模型费率计费。开启 `sessionTitle.enabled` 后,识别为标题请求的 body(宿主标题模板 `Generate the session title from this JSON array of human messages:` 打头、整条 body 只有这一条 user 消息、无 assistant/tool 消息)被改写到 `sessionTitle.model`(默认 `wb-lite`),并删除 `reasoning_effort` / `reasoning` 字段。报告者实测该模型对这类请求 **0 积分**,但仍是真实上游请求、计入请求数、仅新会话首条触发一次。
|
|
109
|
+
- 识别依据是**本机安装的 DSH 桌面端 0.2.1-rc.2 `app.asar` 的字节级取证**:请求构造与宿主自己的格式校验器都断言这一模板与单 user 消息形状;宿主 `GenerateOptions.purpose: 'session-title'` 不随 OpenAI 线上协议传输,body 形状是 shim 唯一可用的判据。识别 **fail-closed**:宿主改模板则改写静默停止、回到原计费,普通对话不可能命中(模板前缀须从文本开头匹配,正文引用模板的对话不会被改写——各有测试钉住)。
|
|
110
|
+
- **默认 `false`**,与 #132 两项同一理由:这笔请求无论改写与否都会发出,但改写改变的是上游把这笔记进哪个计费桶(0 积分 vs 主模型费率),升级后不该静默变化;且 wb-lite 是隐藏模型、能过渠道闸是报告者账号级的实测。目标模型可配置(`sessionTitle.model`),防 id 变化或区域差异;上游拒绝时仅该会话标题失败一次,不影响主对话。识别先用模板的纯 ASCII 头做预过滤(真实标题 body 必含、普通对话几乎不含),开关开启时普通请求不再为识别付一次全量 JSON 解析;预过滤只跳过工作,不改判定(含模板引用的对话仍由完整检测拒绝)。**生效时机**:`sessionTitle` 与 `usageIdentity` / `keepPromptLast` / `extraInjectedPrefixes` 都在插件挂载时采样进 shim,修改配置后需重载插件或重启 DSH(loader 的 volatile 更新不重建 shim)。
|
|
111
|
+
- **`keepPromptLast` 认得 billion-context 注入的两类上下文块(#133)。**
|
|
112
|
+
- 5.8.0 的注入前缀清单只覆盖宿主自家的三类(时间采样、`Current runtime context.`、`<system-reminder>`),而 billion-context 压缩插件同样以 `role=user` 注入内容:压缩摘要块以 `[Compressed conversation section]` 标题行打头,效率提示以 `This is an efficiency` 打头。开了 `keepPromptLast` 后这两类漏网,被当成真实提问留在末尾,用量页「请求」列随之记错——报告者已在本地产物上逐字节验证此修法并实测恢复。
|
|
113
|
+
- 注入前缀同时做成**可配置**:新增 `extraInjectedPrefixes`(字符串数组,每项按**字面**前缀匹配、正则元字符会被转义,默认空),压缩插件或宿主将来改文案时不必等插件发版。它只在 `keepPromptLast` 开启时参与匹配,默认行为不变。
|
|
58
114
|
- **企业/教育版账号在账号池里丢失 `enterprise` 标记(#129)。**
|
|
59
115
|
- 存储层早就算出了 `enterprise: true`,但三个转发点把它丢了:`otherAccounts`、`localPoolMembers`、`poolMembersOf`(`poolTargets` 在 5.6.0 就已带上)。结果企业账号在池卡片上显示一个**根本不存在的「领取」按钮**和一列「签到」,而企业成员点这两样都只会被 `10001` 拒绝。
|
|
60
116
|
- 现在这三个转发点都带上了标记,池行据此签到列显示「不适用(企业版)」、权益块整段隐藏;领取路由在**发起任何上游请求之前**就用 409 拒绝(`enterprise-account-unsupported`)。**Web 侧只到账号行这一层**:`toWebAccount` 与用量页的池账号行带标记,用量页顶部那个 `account` 对象没有——它这次修的是**名字**(改用 `preferredAccountName`,此前只取 `credential.nickname` 导致企业账号显示为空),不是标记;那里也不渲染签到/领取。输入框胶囊(`workBuddyAccountCredits`)同样不带标记,它不显示这两个按钮,因此没有对应缺陷。
|
|
@@ -64,15 +120,25 @@
|
|
|
64
120
|
- 设置其实**已经落盘并通过回读校验**,卡住的只是随后那次用量刷新:写入与刷新共用同一个 `saving` 状态,而刷新可能因为账号被限制、网络抖动或上游慢响应而迟迟不返回,用户于是看到「没保存成功」的假象,草稿也被锁死。
|
|
65
121
|
- 现在 `saving` 只覆盖「已校验的设置写入」这一件事:写成功立刻解除 busy 并丢弃草稿,然后 **`await`** 那次回读(不是「再异步刷新」——它是串行等到的,只是结果只影响显示)。这带来一个明确的取舍:回读完成前,卡片上的行还是旧值;`refreshUsage` 失败时不抛而是 resolve,所以「没抛异常」不等于「读到了新值」,旧契约里「保留草稿直到刷新成功」的写法会在这个窗口里拿陈旧的 `saved` 去比对——因此回读结果与保存判定彻底分开。
|
|
66
122
|
- 刷新失败不影响「已保存」的判定,只显示一条**独立**提示(`row.poolSavedStaleRefresh`:「已保存,但面板未能重新读取。设置已写入;下次刷新后会显示。」),它与「保存失败」是两个节点:前者 `role="status"`、后者 `role="alert"`,不再复用 `row.poolSaveFailed`,也就不会出现「保存失败:已保存,但……」这种自相矛盾的一行。只有写入失败才保留草稿。模型选择的保存路径(`WorkBuddyCard.saveModels`)同步修正。
|
|
123
|
+
- **`/cd` 换过目录之后,CLI 与 `/codebuddy` 仍把 CodeBuddy 拉回创建时的那个目录。**
|
|
124
|
+
- **症状**:在一个会话里打开 `~/repos/api`,然后用 `working_directory` 工具 `/cd` 到 `~/repos/web`,接着调用 CodeBuddy 或 `/codebuddy`——CodeBuddy 仍然在 `~/repos/api` 里跑,模型在当前上下文里看到的却是 `~/repos/web`。两个入口还互相不一致:工具侧会退到配置里的 `cliBridge.workingDir` 或临时目录,命令侧只有一级、拿不到就直接报错「这个会话没有工作区路径」。
|
|
125
|
+
- **根因**:两个入口都读 `agent.session.header.cwd`,而 Host 自己的文档把 session header 写成 *detached, deep-frozen creation metadata (format version, cwd, lineage...)*——它是**创建时**的快照,之后再也不更新。活的目录根本不在 header 上:Host 用 `@deepseek-ai/dsh-working-directory` 把它维护成一条 session 投影(`key: "workingDirectory"`,`init: header => header.cwd ?? null`,每次 `working-directory/change` 事件 apply 一次),`/cd` 工具发的就是这种事件。Host 也把这条投影写进模型的系统提示("Current working directory: ..."),官方插件(auto-review、dsh-tools、user-questions)读的都是投影,不是 header。
|
|
126
|
+
- **修法**:新增 `DirectoryOf` 依赖,`sessionDirOf()` 优先读 Host 的 `workingDirectory` 投影,读不到(未发布 / 非字符串 / 空白 / 非绝对路径)才回退 `header.cwd`。`resolveWorkingDir()` 的其余四级阶梯与三重校验(`existsSync` / `statSync.isDirectory` / `isInside` 用 realpath 比对)一行未动。
|
|
127
|
+
- **两个入口同一个裁决器**:`/codebuddy` 改成调用 `sessionDirOf()`,与工具共用同一段逻辑,两边从此不可能再各说各话;都拿不到时的报错文案改为说明「投影与回退都为空」。
|
|
128
|
+
- **降级是硬约束**:Host 没有发布 `sessionProjections` 时,`cliDirectoryOf` 保持 `undefined`,行为与改动前逐字节一致(`src/index.ts` 里 reactive `inject(['sessionProjections'])` 而非 apply 期采样一次,避免在 Desktop 上重演 tools/commands 被丢的历史)。**故意不回退到投影服务自己的 `defaultDirectory`**——Host 的 `get()` 是 `stateOf ?? defaultDirectory`,而那个值是 `process.cwd()`,照抄等于把「不知道」偷换成「用这个」,是本次唯一不许做的事,测试 `degrades to the configured default, not to the Host process directory` 钉死这条。
|
|
129
|
+
- **装配顺序缺陷(审核时发现,随本条一并修复)**:`createCliBridgeRuntime` 在 tools/commands 行触发,可能早于 `sessionProjections` 到位;而 `syncCliBridge` 的重建键是序列化后的设置——投影到位时设置没变,重建是 no-op,runtime 里带着 build 时的 `undefined`,投影**永远不生效且无报错**(症状原样保留)。修法:runtime 接的不是值而是**探针闭包** `cliDirectoryProbe`(每回合读当前格),顺序依赖从此消失;投影服务缺席或卸载时格子清零,探针返回 `null`,回退行为不变。测试 `honours a projection that appears AFTER the runtime was built` 钉死这条时序。
|
|
130
|
+
- **行为变化(读投影的必然后果)**:目录阶梯 roots 白名单的第一项现在是**投影目录**(`/cd` 之后的当前目录),不再是创建时的 header 目录。因此显式传 `working_dir` 指向旧目录(header 目录的子树)在 `/cd` 之后会被三重校验拒绝,报错文案会列出当前 roots——这是「会话在哪、CLI 就在哪」的正确形态,不是回归。**凭据、计费、provider 注册与普通请求路径均未改动**;工作目录阶梯的顺序与校验规则不变,只是第一档从「会话创建时的目录」换成了「会话当前的目录」。
|
|
67
131
|
|
|
68
132
|
### Docs
|
|
69
133
|
|
|
70
134
|
- [`docs/DESIGN.md`](docs/DESIGN.md) 补充 `11128` / `11140` 的分类依据(**按业务码,不按 `displayTips`,也不按裸 `request illegal`**)、`account-restricted` 的停权语义与它唯一实测的回池路径(手动「测试」),以及「渠道拒绝不换号、账号限制要停权」的分界。
|
|
135
|
+
- `src/cli-bridge/services.ts` 记下为什么读 Host 的 `workingDirectory` 投影而不是 `session.header.cwd`,以及 `stateOf` 同步、无文件系统 IO 这两个性质如何让目录阶梯仍然是 `execute` 的同步第一步。
|
|
71
136
|
|
|
72
137
|
### Tests
|
|
73
138
|
|
|
74
139
|
- 新增 11128/11140 分类与账号限制切分(含按信封读码、转义/断行措辞、混合体归属三组反向测试)、企业标记贯穿三个池内转发点与 Web 账号行、领取 409 拒绝、保存后草稿丢弃与陈旧刷新提示(两条 UI 测试原先钉住的正是缺陷本身,已改正)、以及「`selected` 必须跟随 `current()` 的轮换优先级」两条(轮换中只许一个标记、轮换指向已消失账号时回落);会话身份三件套一节 9 条(默认形状不变 / 开启后的 v7+`X-IDE-*`+`X-Request-ID`+`User-Agent` 不变 / 生图不受开关影响);`keepPromptLast` 一节 10 条(默认不动、搬移正确、`system-reminder` 与 `<acp>` 形状、**取自真实会话的组合注入形状**、**不重排普通对话**这条反向测试、system 仍居首、只剥离被记录那一条的前导标记、array content 两种形状、无注入块时不剥离)。
|
|
75
140
|
- 完整套件 **2205 通过 / 2 条按平台跳过**,`typecheck` 与 `build` 通过。所有「守卫能失败」的说法都做过变异验证,逐条结果:X-IDE 或三件套改成「无视开关」→ 默认形状测试变红;开关改成永不生效 → 对齐测试变红;生图 `'image'` 改成 `'chat'`、或删掉 `shape === 'chat'` 判据 → 生图守卫变红(这条只有把 identity 真正传进 `imageHeaders` 之后才成立——`shape` 曾经是空转参数,那时改它不会变红,是真的假阳性,已修);删掉 `isFailoverWorthy` 里的 `account_restricted` → 新增的 shim 换号测试变红(此前全绿);把 11140 的码判定退回紧凑字面量或退回裸 `11140` 文本 → 新增的「按信封读码」测试变红(分别命中 `spaced: client` 与十六进制假阳性);让 `accounts()` 重新忽略轮换标记 → 新增的「标记跟随被计费账号」测试变红;把面板的底色改回读 `selected` → 新增的「底色与文字指向同一行」测试变红。
|
|
141
|
+
- `tests/cli-bridge.spec.ts` 增 7 条(该文件 82 条全过):投影优先于 header、投影为空回退 header、非绝对路径的投影被忽略、不得退到 Host 进程目录(`defaultDirectory`)、`/codebuddy` 跑在投影目录里、投影与 header 都为空时拒绝,加一条时序回归(runtime 先建、投影后发布、不重挂也不改设置,下一回合仍须落在投影目录——把 runtime 接回 build 时的快照值,这条就落回 `header.cwd`,当场变红)。本条并入本版后在 Windows 上重跑:完整套件 **132 files / 2214 tests 全过、0 跳过**(上面两条记录的 2205 与 2 条按平台跳过都落在同一批:那 2 条在本平台不跳),`typecheck`(`tsconfig.json` + `tsconfig.client.json`)与 `tsdown` 构建通过。
|
|
76
142
|
|
|
77
143
|
## 5.7.0 (2026-10-23)
|
|
78
144
|
|
package/README.en.md
CHANGED
|
@@ -50,6 +50,7 @@ It depends on the WorkBuddy client's own endpoints (not a public API), so the pl
|
|
|
50
50
|
- **Account pool: batch actions across several accounts, plus automatic failover** — tick the accounts you want into a pool and you get **one-click check-in for every account**, **one-click testing of every account against a model you choose** (defaulting to the region's zero-multiplier free model), and with **Enable the account pool** switched on, **automatic failover**: a request that fails upstream is retried against the pool's other usable accounts, in order, until one serves it or none is left. Membership starts **empty and is an explicit opt-in**: "signed in on this machine" is not the same as "spend this account's credits and claim its rewards". The pool's **ranking decides who serves every request** (usable first, then expiring-credits-first, higher credits, freshest credential), and on a failure the next usable member is tried. It **never writes back your saved selection** — switch the pool off and your own account applies again immediately. **The switch only decides whether the PLUGIN chooses** — with it off, the plugin stops picking and switching accounts for you, but check-in and testing stay available (they are manual actions). Testing reuses the model rows' real-volume probe, so it **does cost real credits**; **automatic check-in is deliberately not offered** — a check-in claims real rewards, so it only ever runs when you press the button. See [Account pool](#account-pool).
|
|
51
51
|
- **Enterprise / education accounts: check-in and Claim do not apply** — a seat carrying an `enterpriseId` shows **no check-in column, no benefit campaigns, and no Claim request** in the pool (5.8.0, issue #129): upstream refuses the personal-entitlement **write** endpoints for such members (measured `10001`), while its **read** endpoint still lists the campaigns as unclaimed — so "the read said available" can never be the reason to write. Their credits live in a different book too (enterprise usage = limit − used), and the personal credit table simply returns an empty list. The flag now reaches the pool rows (`localPoolMembers`, `poolMembersOf`, `otherAccounts`) and the web account rows, and it is attached the moment an account enters the pool rather than after a click discovers it is unsupported. It is deliberately **not** on the usage page's top-level `account` object (that one was fixed for the display **name** only, via `preferredAccountName`) and not on the composer pill, which renders neither button.
|
|
52
52
|
- **Optional: make the usage page record these calls like the official client's (5.8.0, issue #132, OFF by default)** — with `usageIdentity` on, chat requests carry `X-IDE-Name: WorkBuddy` (plus `X-IDE-Type: CLI` and a version taken from the existing product UA, not a second hard-coded number), so the usage page's client column is no longer blank, and the conversation-identity triple becomes the official **32-character lower-case hex UUIDv7** with `X-Request-ID` alongside it — the gateway echoes that id as the SSE `id` and writes it into the usage page, so hyphenated v4 ids made one account's rows look like two different clients — while "fresh per request, all distinct" has not been relaxed. **`User-Agent` is untouched under either setting**: billing and rate limits recognise the CLI channel. The image endpoint does not follow this switch; it keeps the shape measured on 2026-10-21. It is off by default because the shape fixes nothing on its own (the three `11128` doors read message openings, a missing conversation id, and whether the first message is a system prompt — no header moves any of them) and the only measurement is a reporter's on an older build, so a purely cosmetic alignment does not get to put every account at risk first. There is also an opt-in `keepPromptLast` (**off by default**): the usage page takes the request text from the LAST `user` message, and the host appends its time samples, runtime context and `<system-reminder>` blocks AFTER what you actually typed, so that column recorded injected text; when enabled it moves those injected blocks **above the real prompt** — order only, except that the recorded prompt's own leading `<acp …>mNNNNN</acp>` marker is stripped (leave it and that column still opens with a marker, i.e. nothing was fixed); earlier messages **keep** theirs, because the model cites history by those ids and the only id that can never matter is the message being answered now. It stays off by default because the order is something the upstream counts and a few leading characters are removed from the real prompt. The move matches only the host's own injected prefixes, and a conversation with no injected tail is untouched byte for byte — marker included — pinned by tests written for both.
|
|
53
|
+
- **Optional: move each new session's title request onto the hidden lightweight model (issue #133, OFF by default)** — the DSH host fires one title-generation request per new session through the same provider as the conversation, so it is billed at the main paid model's rate. With `sessionTitle.enabled` on, a request the plugin recognises as a title (its only user message opens with the host's template `Generate the session title from this JSON array of human messages:`) is re-pointed at `sessionTitle.model` (default `wb-lite`). The reporter measured 0 credits for those calls, with three caveats: it is still a real upstream request and counts toward request volume; the measurement is the reporter's account, not reproduced here; detection is fail-closed — if the host rewords the template the rewrite silently stops and billing reverts, and ordinary conversations are never touched. If the upstream rejects the model, only that session's title fails once; the conversation is unaffected. Like `usageIdentity` / `keepPromptLast` / `extraInjectedPrefixes` in the bullets above, `sessionTitle` is read once when the plugin mounts: **after editing `cordis.patch.yml`, reload the plugin or restart DSH for the change to take effect** — hot edits do not apply.
|
|
53
54
|
- **Benefits: a marker for what is unclaimed, plus a per-account Claim button** — the card marks, account by account, anything in the upstream's benefit campaigns (compensation / gifts) that **has not been claimed yet**. The data comes from the upstream's own two status endpoints; in the marking stage the plugin is **read-only** and never calls the two write endpoints. Three display rules all follow the upstream's state: `active && !claimed` shows the row, `claimed` removes it entirely, and an expired campaign (upstream flips `active` to `false`) disappears by itself — the plugin **never compares `end_time` against the local clock**, so a skewed machine cannot argue with the server; `end_time` only drives the "three days left" highlight. `active` alone is not enough to decide: an already-claimed account still reports `active: true`, and reading it alone would leave a claimed campaign hanging there forever. The read reuses the check-in pipeline (once per account, cached for 30 minutes, no request while the account pool is off), and a failing endpoint counts as "unread" rather than caching "nobody has benefits". Each row ends in a solid amber **Claim pill**, and one click claims **one campaign of one account**: there is no "claim everything", because a click spends one irrevocable real entitlement — and the write route **requires `accountId`**, so the request itself cannot batch. Before writing it **re-reads** the status and decides again; anything already claimed or expired returns 409 without touching upstream. After a successful claim it answers with the upstream's **fresh reading** and refreshes the card, with no optimistic update — even a silent upstream failure cannot leave a false "claimed" behind — and it also drops that account's cached benefit state, so the pill cannot linger on a stale status.
|
|
54
55
|
- **Actionable advice when a credential is refused** — when the upstream rejects the selected account's token, the plugin actually **probes** the other local sign-ins: if one still answers, it tells you to switch to it (with a one-click switch) instead of vaguely asking you to sign in again — re-authenticating fixes nothing while you are pinned to a revoked backup. Only when there is genuinely no other local account does it ask you to sign in again.
|
|
55
56
|
- **Add accounts by QR sign-in, import/export them in bulk, test or delete them one by one** — the WorkBuddy desktop app is no longer a prerequisite: the card's **Sign in with QR code** button (labelled **Sign in on the web** for the international build — that region uses a browser login page, there is no QR; it sits just above **More settings** and acts immediately, with nothing to save) opens a dialog and takes you **straight to the authorization page** in your browser; if the browser blocks that, the dialog's link, copy and **Open page again** are the fallback. Every credential lands in the plugin's own vault (`$DSH_HOME/workbuddy-vault/<region>/<accountId>.json`, one file per account), and the desktop app's auth files stay **read-only and are never modified**. Each row of the account table gains **Test** and **Remove**: a row test reuses the same probe as the batch run, so the two are comparable; removing deletes only the copy the plugin stored and **hides** the account: it disappears at once and the automatic poll cannot bring it back, while **Detect accounts again** restores it (signing in or importing the same account clears the hide too). **The desktop app's sign-in is never touched** — removing means "this plugin no longer lists it", not "sign out of the app". **Export / import accounts** moves credentials to another machine: pick accounts to export a JSON file → choose a file and read a **redacted preview** (field names and whether a token is present; the token itself never enters the preview) → tick what to import. An import overwrites per account rather than duplicating, and **only a record positively identified as belonging to that region is stored** — a domain the plugin cannot classify is skipped and counted as skipped. See [Account pool](#account-pool).
|
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
- **账号池:多账号批量操作与自动换号** —— 把要用的账号勾进池子,即可**签到勾选的账号**、**测试勾选的账号 × 你指定的模型**(默认挑该区域倍率为 0 的免费模型);开启**启用账号池**即同时打开**自动换号**——某个请求在上游失败时,插件会依次用池内其他可用账号重试,直到有一个接手、或全部不可用为止,然后才把错误报给你。成员**默认一个都不含**、必须显式勾选(「本机有登录」不等于「要拿它去签到和花积分」);**开启后由池子的排序决定每一次请求用谁**(可用优先,其次快过期先花、积分高、凭据更新的);失败时依次用池内其他可用账号重试。排序**从不写回你保存的选择**——关掉开关,你选定的账号立刻完全生效。只有请求本身不合法(HTTP 400)不换号:同一个请求体换谁都是 400,重试只会白等。**这个开关只管「插件要不要自己决定」**:关掉它,插件不再替你选账号、也不再替你换号,但**签到与测试仍然可用**(它们是手动动作)。测试走的是与模型行同一套真实体积探测,因此**有真实成本**;**不提供自动签到**——签到会真实领取奖励,只在你点按钮时执行。详见[账号池](#账号池)。
|
|
58
58
|
- **企业版 / 教育版账号:签到与「领取」不适用** —— 带 `enterpriseId` 的席位(企业版或教育版)在池子里**不显示签到列、不显示福利活动,也不会发出领取请求**(5.8.0,issue #129):上游对这类成员的个人权益**写**接口一律拒绝(实测 `10001`),而它的**读**接口照样把活动列出来、标着「未领」——所以「读到了」绝不能当成「可以领」。积分也在另一本账上(企业用量 = 上限 − 已用),个人积分表对这种账号返回空列表。这个判断贯穿池卡片、输入框胶囊与 Web 状态页,并且在账号进入池子的那一刻就带上标记,而不是等用户点了按钮才发现不支持。
|
|
59
59
|
- **可选:让上游用量页把这笔记成官方客户端发的那笔(5.8.0,issue #132,默认关闭)** —— 开 `usageIdentity` 后 chat 请求带上 `X-IDE-Name: WorkBuddy`(连同 `X-IDE-Type: CLI` 与版本号;版本号取自现有的产品 UA,不留第二个硬编码数字),用量页的「使用端」由此不再空白;会话身份三件套同时改成官方客户端那种 **32 位十六进制 UUIDv7** 并补上 `X-Request-ID`——网关把这个 id 回显成 SSE 的 `id` 并写进用量页,此前带连字符的 v4 与官方记录摆在同一页上像两种客户端;**每请求现生成、互不相同**这条没有放松。**`User-Agent` 在两种取值下都一字未动**——账单与限流认的是 CLI 通道;生图路径也不跟着这个开关变,它保持自己 2026-10-21 实测过的形状。**默认关**的理由:形状本身不修任何东西(`11128` 的三道门读的是消息开头、会话 id 有没有、首条是不是 system,头改不动其中任何一条),而实测证据来自报告者的账号与旧版本、本插件未在真机复现——一次纯显示对齐不该让所有账号先承担风险。另有可选的 `keepPromptLast`(**默认关闭**):用量页的「请求」列取自 body 里**最后一条** `user` 消息,而宿主把时间采样、运行时上下文、`<system-reminder>` 追加在你真正输入的那句**之后**,那一列于是常年记的是注入块;开启后把这些注入块**搬到真实输入之前**(只变顺序;另外会剥掉**被记录那一条**真实输入前面的 `<acp …>mNNNNN</acp>` 标记,否则那一列仍然以标记开头而等于没修,更早的消息一律保留自己的标记,因为模型靠它们引用历史)。默认关的理由是它会改变上游会数到的消息顺序、并删掉真实输入的几个前导字符;搬移只匹配宿主自己的注入前缀,而普通对话在没有注入尾块时逐字节不变(连标记也不剥)——这两点各有专门测试钉住。
|
|
60
|
+
- **可选:把每个新会话的标题请求改到隐藏轻量模型(issue #133,默认关闭)** —— DSH 宿主每个新会话会发一次标题生成请求,走的是与对话相同的 provider,于是这笔按主付费模型的费率计费。开启 `sessionTitle.enabled` 后,插件把能按宿主标题模板(`Generate the session title from this JSON array of human messages:` 开头、整条 body 只有这一条 user 消息)识别出的标题请求改写到 `sessionTitle.model`(默认 `wb-lite`)。报告者实测该模型对这类请求 **0 积分**,但注意三点:这仍是**真实上游请求**,请求数照算;证据是报告者账号级的,本插件未复现;识别按 fail-closed 设计——宿主若改模板,改写静默停止、回到原计费,不会误伤普通对话。若上游拒绝该模型,只是该会话标题生成失败一次,不影响主对话。另外,`sessionTitle` 与本条及上一条的 `usageIdentity` / `keepPromptLast` / `extraInjectedPrefixes` 一样,都是插件挂载时读一次配置:**改完 `cordis.patch.yml` 后需要重载插件或重启 DSH 才生效**,热改不生效。
|
|
60
61
|
- **福利活动:待领取标记 + 单账号「领取」按钮** —— 卡片会逐账号标出上游活动(补偿 / 赠礼)里**还没领的那一份**,数据来自上游自己的两个状态接口,标记阶段**只读**、两个写接口一次都不调用。显示规则完全由上游状态决定:`active && !claimed` 才显示,`claimed` 整行消失,活动过期(`active` 翻 `false`)自动消失——**不用本地时钟去比 `end_time`**,本机时间偏了也不会与服务端打架;`end_time` 只用于 3 天内临期高亮。`active` 单独看不行:已领取的账号 `active` 仍为 `true`,只看它会让领过的账号永远挂着待领取。读取复用签到的管线(每账号一次、30 分钟缓存、账号池关闭时不发请求),任一接口失败按「未读」处理,不会把一次失败缓存成「全都没福利」。行尾是一个实心琥珀色的**胶囊按钮「领取」**,一次只领**一个账号的一个活动**:没有「全部领取」,因为一次点击花掉的是一份不可撤销的真实权益,写路由也**强制要求 `accountId`**,请求本身无法批量领取。领取前**重新读取**状态再判定,已领取或已过期一律 409 且不调用上游;领取后用上游的**新读数**作答并刷新卡片,不做乐观更新——即使上游静默失败也不会留下「已领取」的假象,成功后同时清掉该账号的福利缓存,按钮不会顶着旧状态赖着不走。
|
|
61
62
|
- **凭据失效时给出可执行的建议** —— 上游拒绝当前账号的 token 时,插件会**实际探测**本机其他账号:若有账号仍然可用,就直接告诉你「切换到它即可」(并附一键切换),而不是笼统地让你重新登录——被钉在一个作废的旧备份上时,重新登录并不能解决问题。只有本机确实没有别的账号可切换时,才会提示重新登录。
|
|
62
63
|
- **扫码添加账号、批量导入导出、逐账号测试/删除** —— 不必先装 WorkBuddy 桌面 App:卡片里的「扫码登录」按钮(国际版为「网页登录」——那边是浏览器登录页,没有二维码;这一行就在「更多设置」上方,点一下立即生效,不需要保存)点开即弹出对话框,并在浏览器里**自动打开**授权页;被浏览器拦截时对话框内提供链接、复制与「重新打开页面」兜底。所有凭据统一落到插件自己的凭据库(`$DSH_HOME/workbuddy-vault/<region>/<accountId>.json`,一账号一文件),**桌面 App 的认证文件始终只读、永不被改动**。账号表格每一行右侧有「测试」与「删除」:测试与批量测试走同一套探测,结果可比;删除只删插件自己存的那份,并把这个账号**暂时隐藏**:删掉后立刻消失,也不会被自动轮询带回来;点「重新检测账号」即可找回(重新登录或导入同一账号也会清除隐藏记录)。**桌面 App 的登录始终不动**——删除是「本插件不再列出它」,不是退出 App 登录。「导出 / 导入账号」可把凭据搬到另一台机器:多选导出 JSON → 选文件后先看**脱敏预览**(只显示字段与是否有 token,明文不进预览)→ 勾选导入。导入按账号去重覆盖,**只有确认属于该区域**的记录才入库,认不出的域名会被跳过并如实计入「已跳过」。详见[账号池](#账号池)。
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as WorkBuddyCredentialStore, D as defaultDesktopAuthDirs, E as defaultDesktopAuthCandidates, I as workbuddyOwnAuthPath,
|
|
2
|
+
import { C as WorkBuddyCredentialStore, Ct as WORKBUDDY_APP_EXECUTABLE_ENV, D as defaultDesktopAuthDirs, E as defaultDesktopAuthCandidates, I as workbuddyOwnAuthPath, Lt as hostPeersInfo, M as legacyWorkbuddyOwnAuthPath, S as WORKBUDDY_AUTH_FILE_ENV, V as WorkBuddyUpstreamClient, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as rosterSeenPath, f as piAiRuntimeInfo, kt as findWorkbuddyAppExecutableWithSource, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, o as workbuddyHostHeartbeatPath, p as FALLBACK_WORKBUDDY_MODELS, r as isHeartbeatProcessAlive, u as describeRosterSeenPayload } from "./host-heartbeat-DzfWuVsm.js";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
package/lib/client.js
CHANGED
|
@@ -734,6 +734,97 @@ window.__ModuleLoader__.load({
|
|
|
734
734
|
return typeof bridge === "object" && bridge !== null && bridge.enabled === true;
|
|
735
735
|
}
|
|
736
736
|
/**
|
|
737
|
+
* Read one upstream-identity boolean switch out of a settings snapshot.
|
|
738
|
+
*
|
|
739
|
+
* Only an explicit `true` is ON, exactly like {@link cliBridgeEnabledOf} and for
|
|
740
|
+
* the same reason: these switches change what gets billed or what bytes go
|
|
741
|
+
* upstream, so a missing or malformed value must read as OFF, never as consent.
|
|
742
|
+
*
|
|
743
|
+
* @param configured - the whole settings snapshot.
|
|
744
|
+
* @param field - which switch to read.
|
|
745
|
+
* @returns whether the switch is on.
|
|
746
|
+
*/
|
|
747
|
+
function upstreamSwitchOf(configured, field) {
|
|
748
|
+
const value = configured?.[field];
|
|
749
|
+
return unwrapVolatileDeep(value) === true;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Read the `sessionTitle` switch's two values out of a settings snapshot.
|
|
753
|
+
*
|
|
754
|
+
* Returns the schema's own defaults (`enabled: false`, `model: 'wb-lite'`) for
|
|
755
|
+
* anything missing or malformed, so the card shows what the plugin will actually
|
|
756
|
+
* do rather than an empty control.
|
|
757
|
+
*
|
|
758
|
+
* @param configured - the whole settings snapshot.
|
|
759
|
+
* @returns the switch state and target model.
|
|
760
|
+
*/
|
|
761
|
+
function sessionTitleOf(configured) {
|
|
762
|
+
const raw = unwrapVolatileDeep(configured?.["sessionTitle"]);
|
|
763
|
+
const block = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
|
|
764
|
+
const model = block["model"];
|
|
765
|
+
return {
|
|
766
|
+
enabled: block["enabled"] === true,
|
|
767
|
+
model: typeof model === "string" && model.trim() !== "" ? model.trim() : "wb-lite"
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Read the extra injected-prefix list out of a settings snapshot.
|
|
772
|
+
*
|
|
773
|
+
* A non-array or a list containing non-strings reads as empty: a malformed entry
|
|
774
|
+
* must not silently start matching messages it was never meant to match.
|
|
775
|
+
*
|
|
776
|
+
* @param configured - the whole settings snapshot.
|
|
777
|
+
* @returns the literal prefixes, trimmed, without blanks or duplicates.
|
|
778
|
+
*/
|
|
779
|
+
function extraInjectedPrefixesOf(configured) {
|
|
780
|
+
const raw = unwrapVolatileDeep(configured?.["extraInjectedPrefixes"]);
|
|
781
|
+
if (!Array.isArray(raw)) return [];
|
|
782
|
+
const seen = /* @__PURE__ */ new Set();
|
|
783
|
+
for (const entry of raw) {
|
|
784
|
+
if (typeof entry !== "string") continue;
|
|
785
|
+
const trimmed = entry.trim();
|
|
786
|
+
if (trimmed !== "") seen.add(trimmed);
|
|
787
|
+
}
|
|
788
|
+
return [...seen];
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Write one upstream-identity switch, verifying it landed.
|
|
792
|
+
*
|
|
793
|
+
* Same two writers as {@link writeCliBridgeEnabled} and for the same measured
|
|
794
|
+
* reason (the browser mirror of this namespace is documented as stale on the
|
|
795
|
+
* affected 0.1.7 deployments, so a switch that merely *looks* flipped is the
|
|
796
|
+
* "关闭不了" class of report): the Host endpoint first, the settings scope as a
|
|
797
|
+
* fallback. The Host writes only this field's own path, so siblings survive.
|
|
798
|
+
*
|
|
799
|
+
* @param scope - the bound settings scope for this plugin's namespace.
|
|
800
|
+
* @param field - which switch to write.
|
|
801
|
+
* @param value - the desired value.
|
|
802
|
+
* @throws {WorkBuddySettingsWriteError} when neither writer persists the value.
|
|
803
|
+
*/
|
|
804
|
+
async function writeUpstreamSwitch(scope, field, value) {
|
|
805
|
+
let hostError;
|
|
806
|
+
try {
|
|
807
|
+
const authoritative = await saveViaHostEndpoint(field, void 0, value);
|
|
808
|
+
if (authoritative !== void 0) try {
|
|
809
|
+
await scope.set(field, authoritative[field]);
|
|
810
|
+
} catch {}
|
|
811
|
+
return;
|
|
812
|
+
} catch (error) {
|
|
813
|
+
hostError = error;
|
|
814
|
+
}
|
|
815
|
+
let delivered = false;
|
|
816
|
+
try {
|
|
817
|
+
delivered = await scope.set(field, value) !== false;
|
|
818
|
+
} catch {
|
|
819
|
+
delivered = false;
|
|
820
|
+
}
|
|
821
|
+
if (delivered) {
|
|
822
|
+
const after = scope.getSnapshot().value;
|
|
823
|
+
if (field === "usageIdentity" || field === "keepPromptLast" ? upstreamSwitchOf(after, field) === value : field === "sessionTitle" ? JSON.stringify(sessionTitleOf(after)) === JSON.stringify(value) : JSON.stringify(extraInjectedPrefixesOf(after)) === JSON.stringify([...value])) return;
|
|
824
|
+
}
|
|
825
|
+
throw hostError instanceof Error ? hostError : new WorkBuddySettingsWriteError(field, "neither the Host save endpoint nor the settings scope persisted the value");
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
737
828
|
* Write the GLOBAL CodeBuddy-CLI switch, verifying it landed.
|
|
738
829
|
*
|
|
739
830
|
* Deliberately NOT a region field: one route, one budget, one switch. The two
|
|
@@ -2218,6 +2309,7 @@ window.__ModuleLoader__.load({
|
|
|
2218
2309
|
return sorted.find((promo) => promo.badge.display !== "activeOnly" && promoWithinAbsoluteWindow(promo, nowMs) && promo.schedule?.dailyUnusable !== true);
|
|
2219
2310
|
}
|
|
2220
2311
|
"WorkBuddy/5.8.3 WorkBuddy/5.8.3 CLI/2.161.0".match(/WorkBuddy\/([^\s]+)/u)?.[1];
|
|
2312
|
+
new RegExp(`^(?:<acp\\b[^>]*>m\\d{1,8}<\\/acp>[ \\t]*\\r?\\n?)*(?:Time sampled while preparing turn \\d+, step \\d+:|Current runtime context\\.|<system-reminder>|\\[Compressed conversation section\\]|This is an efficiency nudge)`, "u");
|
|
2221
2313
|
//#endregion
|
|
2222
2314
|
//#region src/probe.ts
|
|
2223
2315
|
/**
|
|
@@ -4847,7 +4939,13 @@ window.__ModuleLoader__.load({
|
|
|
4847
4939
|
CONTRAST colour, so a ticked box painted itself as a heavy black pill — the
|
|
4848
4940
|
loudest object in a section made of plain 13px text. */
|
|
4849
4941
|
.dsm-workbuddy-pref input{flex:none;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
|
|
4850
|
-
.dsm-workbuddy-pref-check,.dsm-workbuddy-image-tool-check,.dsm-workbuddy-cli-bridge-check{inline-size:15px;block-size:15px;margin:3px 0 0}
|
|
4942
|
+
.dsm-workbuddy-pref-check,.dsm-workbuddy-image-tool-check,.dsm-workbuddy-cli-bridge-check,.dsm-workbuddy-upstream-check{inline-size:15px;block-size:15px;margin:3px 0 0}
|
|
4943
|
+
/* The upstream-identity switches are their own group, so they get a divider
|
|
4944
|
+
above them instead of relying on spacing alone: they change the request the
|
|
4945
|
+
whole plugin sends, not a per-region display choice, and reading them as the
|
|
4946
|
+
tail of the preference list would misrepresent what flipping one costs. */
|
|
4947
|
+
.dsm-workbuddy-upstream-pref{margin-top:4px}
|
|
4948
|
+
.dsm-workbuddy-upstream-pref:first-of-type{border-top:1px solid var(--dsw-alias-bg-layer-3,#2e3034);padding-top:10px}
|
|
4851
4949
|
.dsm-workbuddy-pref input:disabled{cursor:default}
|
|
4852
4950
|
.dsm-workbuddy-pref:has(input:disabled){opacity:.6;cursor:default}
|
|
4853
4951
|
/* The NAME and what it does are TWO lines, not one wrapped blob: first what the
|
|
@@ -6295,6 +6393,21 @@ window.__ModuleLoader__.load({
|
|
|
6295
6393
|
const [cliBridgeRegionOrder, setCliBridgeRegionOrder] = (0, react.useState)(void 0);
|
|
6296
6394
|
const [cliBridgeRegionOrderBusy, setCliBridgeRegionOrderBusy] = (0, react.useState)(false);
|
|
6297
6395
|
/**
|
|
6396
|
+
* The upstream-identity switches the user flipped in this session.
|
|
6397
|
+
*
|
|
6398
|
+
* `undefined` means "not touched here, read the committed settings"; a flipped
|
|
6399
|
+
* value wins until the next snapshot agrees (same rule as `cliBridgeOn`). Each
|
|
6400
|
+
* switch has its own busy flag so flipping one does not disable the others —
|
|
6401
|
+
* they are independent writes, and a shared flag would make the card feel
|
|
6402
|
+
* frozen while one of them is in flight.
|
|
6403
|
+
*/
|
|
6404
|
+
const [usageIdentityOn, setUsageIdentityOn] = (0, react.useState)(void 0);
|
|
6405
|
+
const [usageIdentityBusy, setUsageIdentityBusy] = (0, react.useState)(false);
|
|
6406
|
+
const [keepPromptLastOn, setKeepPromptLastOn] = (0, react.useState)(void 0);
|
|
6407
|
+
const [keepPromptLastBusy, setKeepPromptLastBusy] = (0, react.useState)(false);
|
|
6408
|
+
const [sessionTitleOn, setSessionTitleOn] = (0, react.useState)(void 0);
|
|
6409
|
+
const [sessionTitleBusy, setSessionTitleBusy] = (0, react.useState)(false);
|
|
6410
|
+
/**
|
|
6298
6411
|
* Probe results, keyed by region then model id.
|
|
6299
6412
|
*
|
|
6300
6413
|
* Kept per region for the same reason drafts are: the two tabs are separate
|
|
@@ -6756,6 +6869,51 @@ window.__ModuleLoader__.load({
|
|
|
6756
6869
|
}
|
|
6757
6870
|
};
|
|
6758
6871
|
/**
|
|
6872
|
+
* The three upstream-identity switches, read the same three-level way the CLI
|
|
6873
|
+
* switch is: this card's own successful write, then the settings snapshot.
|
|
6874
|
+
*
|
|
6875
|
+
* Deliberately NOT reading them from the status document: these are
|
|
6876
|
+
* plugin-wide request-shape switches that the Host does not report per region,
|
|
6877
|
+
* and inventing a status field for them would create a second source of truth
|
|
6878
|
+
* for a value the settings document already owns.
|
|
6879
|
+
*/
|
|
6880
|
+
const usageIdentityValue = () => usageIdentityOn ?? upstreamSwitchOf(settingsScope?.getSnapshot().value, "usageIdentity");
|
|
6881
|
+
const keepPromptLastValue = () => keepPromptLastOn ?? upstreamSwitchOf(settingsScope?.getSnapshot().value, "keepPromptLast");
|
|
6882
|
+
const sessionTitleValue = () => {
|
|
6883
|
+
const committed = sessionTitleOf(settingsScope?.getSnapshot().value);
|
|
6884
|
+
return sessionTitleOn === void 0 ? committed : {
|
|
6885
|
+
...committed,
|
|
6886
|
+
enabled: sessionTitleOn
|
|
6887
|
+
};
|
|
6888
|
+
};
|
|
6889
|
+
/** The custom injected prefixes in effect, used only to REPORT their count. */
|
|
6890
|
+
const extraInjectedPrefixes = () => extraInjectedPrefixesOf(settingsScope?.getSnapshot().value);
|
|
6891
|
+
/**
|
|
6892
|
+
* Flip one upstream-identity switch.
|
|
6893
|
+
*
|
|
6894
|
+
* No `refreshUsage` afterwards, unlike the CLI switch: these change the shape
|
|
6895
|
+
* of the NEXT request, not any value the usage document currently reports, so
|
|
6896
|
+
* re-reading the usage would only cost an upstream round-trip (the exact
|
|
6897
|
+
* stall #131 was about) for a number that cannot have moved.
|
|
6898
|
+
*/
|
|
6899
|
+
const toggleUpstreamSwitch = async (field, enabled) => {
|
|
6900
|
+
if (settingsScope === void 0) return;
|
|
6901
|
+
const setBusy = field === "usageIdentity" ? setUsageIdentityBusy : field === "keepPromptLast" ? setKeepPromptLastBusy : setSessionTitleBusy;
|
|
6902
|
+
const setOwn = field === "usageIdentity" ? setUsageIdentityOn : field === "keepPromptLast" ? setKeepPromptLastOn : setSessionTitleOn;
|
|
6903
|
+
setBusy(true);
|
|
6904
|
+
try {
|
|
6905
|
+
await writeUpstreamSwitch(settingsScope, field, field === "sessionTitle" ? {
|
|
6906
|
+
...sessionTitleOf(settingsScope.getSnapshot().value),
|
|
6907
|
+
enabled
|
|
6908
|
+
} : enabled);
|
|
6909
|
+
if (mounted.current) setOwn(enabled);
|
|
6910
|
+
} catch (error) {
|
|
6911
|
+
if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
6912
|
+
} finally {
|
|
6913
|
+
if (mounted.current) setBusy(false);
|
|
6914
|
+
}
|
|
6915
|
+
};
|
|
6916
|
+
/**
|
|
6759
6917
|
* The pinned model / effort of the CLI route, rendered the same way the
|
|
6760
6918
|
* switch is: this card's own successful write first, then the Host's answer
|
|
6761
6919
|
* (the authoritative mirror), then the settings snapshot. `''` is a real
|
|
@@ -7528,6 +7686,91 @@ window.__ModuleLoader__.load({
|
|
|
7528
7686
|
})]
|
|
7529
7687
|
})
|
|
7530
7688
|
});
|
|
7689
|
+
/**
|
|
7690
|
+
* The upstream-identity switches (issue #133 suggestions 2 and 3).
|
|
7691
|
+
*
|
|
7692
|
+
* These govern what the gateway SEES and how it files the call, not which
|
|
7693
|
+
* provider serves it, so they sit in their own group with their own heading
|
|
7694
|
+
* instead of the preference list below (which is all about the card's own
|
|
7695
|
+
* display choices). Every one of them was already settable by hand in
|
|
7696
|
+
* `cordis.patch.yml`; this is only a UI route to the same fields.
|
|
7697
|
+
*
|
|
7698
|
+
* ONE shared hint about reloading, not one per switch: all four are sampled
|
|
7699
|
+
* when the plugin mounts into the shim, so the rule and its reason are the
|
|
7700
|
+
* same sentence for each, and repeating it four times would bury it.
|
|
7701
|
+
*/
|
|
7702
|
+
const upstreamSwitches = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7703
|
+
className: "dsm-workbuddy-prefs",
|
|
7704
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7705
|
+
className: "dsm-workbuddy-pref-hint",
|
|
7706
|
+
children: t("row.upstreamHint")
|
|
7707
|
+
}), [
|
|
7708
|
+
[
|
|
7709
|
+
"usageIdentity",
|
|
7710
|
+
"row.usageIdentityLabel",
|
|
7711
|
+
"row.usageIdentityHint",
|
|
7712
|
+
usageIdentityValue(),
|
|
7713
|
+
usageIdentityBusy,
|
|
7714
|
+
setUsageIdentityOn
|
|
7715
|
+
],
|
|
7716
|
+
[
|
|
7717
|
+
"keepPromptLast",
|
|
7718
|
+
"row.keepPromptLastLabel",
|
|
7719
|
+
"row.keepPromptLastHint",
|
|
7720
|
+
keepPromptLastValue(),
|
|
7721
|
+
keepPromptLastBusy,
|
|
7722
|
+
setKeepPromptLastOn
|
|
7723
|
+
],
|
|
7724
|
+
[
|
|
7725
|
+
"sessionTitle",
|
|
7726
|
+
"row.sessionTitleLabel",
|
|
7727
|
+
"row.sessionTitleHint",
|
|
7728
|
+
sessionTitleValue().enabled,
|
|
7729
|
+
sessionTitleBusy,
|
|
7730
|
+
setSessionTitleOn
|
|
7731
|
+
]
|
|
7732
|
+
].map(([field, labelKey, hintKey, on, busy, setOwn]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7733
|
+
className: "dsm-workbuddy-pref dsm-workbuddy-upstream-pref",
|
|
7734
|
+
title: t(hintKey),
|
|
7735
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
7736
|
+
type: "checkbox",
|
|
7737
|
+
className: "dsm-workbuddy-upstream-check",
|
|
7738
|
+
checked: on,
|
|
7739
|
+
disabled: !canWrite || saving || poolBusy || busy,
|
|
7740
|
+
onChange: (event) => {
|
|
7741
|
+
toggleUpstreamSwitch(field, event.target.checked);
|
|
7742
|
+
}
|
|
7743
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7744
|
+
className: "dsm-workbuddy-pref-copy",
|
|
7745
|
+
children: [
|
|
7746
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7747
|
+
className: "dsm-workbuddy-pref-label",
|
|
7748
|
+
children: t(labelKey)
|
|
7749
|
+
}),
|
|
7750
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7751
|
+
className: "dsm-workbuddy-pref-hint",
|
|
7752
|
+
children: t(hintKey)
|
|
7753
|
+
}),
|
|
7754
|
+
field === "sessionTitle" && on ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7755
|
+
className: "dsm-workbuddy-pref-hint",
|
|
7756
|
+
children: [
|
|
7757
|
+
t("row.sessionTitleModel"),
|
|
7758
|
+
" ",
|
|
7759
|
+
sessionTitleValue().model
|
|
7760
|
+
]
|
|
7761
|
+
}) : null,
|
|
7762
|
+
field === "keepPromptLast" && on && extraInjectedPrefixes().length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7763
|
+
className: "dsm-workbuddy-pref-hint",
|
|
7764
|
+
children: [
|
|
7765
|
+
t("row.extraPrefixesCount"),
|
|
7766
|
+
" ",
|
|
7767
|
+
extraInjectedPrefixes().length
|
|
7768
|
+
]
|
|
7769
|
+
}) : null
|
|
7770
|
+
]
|
|
7771
|
+
})]
|
|
7772
|
+
}, field))]
|
|
7773
|
+
});
|
|
7531
7774
|
const creditPreferences = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7532
7775
|
className: "dsm-workbuddy-prefs",
|
|
7533
7776
|
children: [
|
|
@@ -8081,10 +8324,14 @@ window.__ModuleLoader__.load({
|
|
|
8081
8324
|
id: "dsm-workbuddy-panel-capabilities",
|
|
8082
8325
|
"aria-labelledby": "dsm-workbuddy-tab-capabilities",
|
|
8083
8326
|
hidden: signedIn && panel !== "capabilities",
|
|
8084
|
-
children: [
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8327
|
+
children: [
|
|
8328
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8329
|
+
className: "dsm-workbuddy-panel-note",
|
|
8330
|
+
children: t("row.panelCapabilitiesHint")
|
|
8331
|
+
}),
|
|
8332
|
+
creditPreferences,
|
|
8333
|
+
upstreamSwitches
|
|
8334
|
+
]
|
|
8088
8335
|
}),
|
|
8089
8336
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8090
8337
|
className: "dsm-workbuddy-panel",
|
|
@@ -9416,8 +9663,64 @@ window.__ModuleLoader__.load({
|
|
|
9416
9663
|
});
|
|
9417
9664
|
}
|
|
9418
9665
|
/**
|
|
9666
|
+
* The priority this plugin registers the model-label slot at.
|
|
9667
|
+
*
|
|
9668
|
+
* WHY A DISTINCT PRIORITY AT ALL. `conversation.input.model.label` is a
|
|
9669
|
+
* `single` slot: it holds ONE cell, and `SlotCore.register()` THROWS when a
|
|
9670
|
+
* second registration claims one priority — "single slot … already has a
|
|
9671
|
+
* registration at priority 0 — register at a different priority to shadow it
|
|
9672
|
+
* (lowest renders)". The default priority is `0`, which is also what the host's
|
|
9673
|
+
* own shipped contributor uses
|
|
9674
|
+
* (`@deepseek-ai/dsh-client-ui-deepseek-model-label-1017`, in the web-app
|
|
9675
|
+
* bundle from 0.2.1-rc.2 on), so registering at `0` made the two race. Measured
|
|
9676
|
+
* on 0.2.1-rc.3 that is US: the app's own crash log records
|
|
9677
|
+
* `single slot "conversation.input.model.label" already has a registration at
|
|
9678
|
+
* priority 0 (registered by Kb)` and this entry's catch-all line
|
|
9679
|
+
* `model label slot failed to register`, and because that catch only logs, the
|
|
9680
|
+
* badge simply vanished from the picker with no visible error.
|
|
9681
|
+
*
|
|
9682
|
+
* WHY NOT `-1`. That is `dsh-connect-qoder`'s slot priority, and two plugins on
|
|
9683
|
+
* one priority collide for exactly the same reason `0` did.
|
|
9684
|
+
*
|
|
9685
|
+
* WHY `-0.5` IS SAFE AND SELF-CONTAINED. The value only has to be distinct from
|
|
9686
|
+
* whatever else claims the cell; `-0.5` is legal, sorts without collision
|
|
9687
|
+
* against both `-1` and `0`, and requires knowing nothing about the other
|
|
9688
|
+
* plugins. See the "不做插件联动" invariant in `docs/DESIGN.md`.
|
|
9689
|
+
*
|
|
9690
|
+
* WHAT THIS PLUGIN DELIBERATELY DOES NOT DO. It does not read the slot ledger,
|
|
9691
|
+
* does not look for another plugin's registration, and does not render another
|
|
9692
|
+
* plugin's component. Foreign rows get the plain catalog name, which is exactly
|
|
9693
|
+
* the slot's documented minimum. Inter-plugin chaining is out of scope by
|
|
9694
|
+
* project policy (`docs/DESIGN.md` §零) — and that policy is NOT stricter than
|
|
9695
|
+
* the Host's own: the shipped `dsh-client-ui-deepseek-model-label-1017` README
|
|
9696
|
+
* lists, under "Known Limitations and Deferred Work", that "another model-label
|
|
9697
|
+
* plugin at the same priority conflicts; the two presentations do not compose
|
|
9698
|
+
* automatically", and the remedy it documents is DISABLING it in the profile
|
|
9699
|
+
* patch — a configuration decision, not a runtime negotiation. A consequence is
|
|
9700
|
+
* that when this plugin wins the cell, another contributor's decoration on ITS
|
|
9701
|
+
* rows is not composed; that is an accepted, documented trade-off rather than
|
|
9702
|
+
* something to work around at runtime.
|
|
9703
|
+
*
|
|
9704
|
+
* WHY WINNING IS THE RIGHT SIDE OF THAT TRADE. 1017 is a TEMPORARY marker
|
|
9705
|
+
* (its own package description: "Temporary 1017 release badge …"; its README
|
|
9706
|
+
* says to remove the package and its bundle row once structured release labels
|
|
9707
|
+
* replace it) and it decorates exactly ONE model, on two provider routes. This
|
|
9708
|
+
* plugin decorates every WorkBuddy model row with load level, promo price and
|
|
9709
|
+
* the new-model mark — its core surface. Trading a long-lived feature for a
|
|
9710
|
+
* temporary marker is the wrong way round, and when 1017 is removed this
|
|
9711
|
+
* plugin is already the only occupant of the cell.
|
|
9712
|
+
*
|
|
9713
|
+
* Exported so the test asserts against the value that actually ships rather
|
|
9714
|
+
* than a copy of it.
|
|
9715
|
+
*/
|
|
9716
|
+
const MODEL_LABEL_PRIORITY = -.5;
|
|
9717
|
+
/**
|
|
9419
9718
|
* The label for one model row in the host's picker.
|
|
9420
9719
|
*
|
|
9720
|
+
* Self-contained by design: a model this plugin owns gets the full label, and
|
|
9721
|
+
* anything else gets the plain catalog name. There is no forwarding and no
|
|
9722
|
+
* consultation of other plugins' registrations — see `MODEL_LABEL_PRIORITY`.
|
|
9723
|
+
*
|
|
9421
9724
|
* @param props - The host's model identity and the placement of this label.
|
|
9422
9725
|
* @returns The catalog name, the promotion chip when one holds, and the load
|
|
9423
9726
|
* badge when the probe reported one.
|
|
@@ -10174,6 +10477,15 @@ window.__ModuleLoader__.load({
|
|
|
10174
10477
|
"row.poolRowRemoved": "{accountName}: hidden from the list. 「Detect accounts again」 re-adds it; the desktop app stays signed in.",
|
|
10175
10478
|
"row.poolRowRemoving": "Removing the account…",
|
|
10176
10479
|
"row.poolRowRemoveDesktopOnly": "{accountName}: hidden from the list — this account comes from the desktop app's own sign-in, which is left untouched. 「Detect accounts again」 re-adds it.",
|
|
10480
|
+
"row.upstreamHint": "These change what the gateway sees, not which provider serves the call. All of them are sampled when the plugin mounts, so reload the plugin (or restart DSH) after changing one.",
|
|
10481
|
+
"row.usageIdentityLabel": "Make the usage page record these calls like the official client",
|
|
10482
|
+
"row.usageIdentityHint": "Adds the X-IDE-* headers (which fill the 使用端 column, otherwise blank) and sends conversation ids as 32-hex UUIDv7. Display alignment only: the User-Agent and the request body never change. Off by default because the evidence comes from a reporter, not from this plugin.",
|
|
10483
|
+
"row.keepPromptLastLabel": "Record what you actually typed, not the injected context",
|
|
10484
|
+
"row.keepPromptLastHint": "Moves the harness-injected runtime context above your real prompt, so the usage page stops showing \"Time sampled…\" as your question. Message count and text are preserved. Off by default because it changes the message order the upstream counts.",
|
|
10485
|
+
"row.sessionTitleLabel": "Send session titles to the lightweight model",
|
|
10486
|
+
"row.sessionTitleHint": "The host fires one title request per new session and bills it at the main model rate. This re-points that one request at a hidden lightweight model. The request still happens either way. Off by default.",
|
|
10487
|
+
"row.sessionTitleModel": "Target model:",
|
|
10488
|
+
"row.extraPrefixesCount": "Custom injected prefixes in effect:",
|
|
10177
10489
|
"row.cliBridgeLabel": "Let the local CodeBuddy CLI do the work",
|
|
10178
10490
|
"row.cliBridgeHint": "Adds the codebuddy_ask tool and the /codebuddy command. Each call spends CodeBuddy quota. injectResult only affects /codebuddy — its result is injected back into the session, costing one DSH turn when the session is idle; tool calls never read it. Rate-limited: 6 turns per session per hour, 20 per process per hour, one turn at a time. Off by default.",
|
|
10179
10491
|
"row.cliBridgeModelLabel": "CLI model",
|
|
@@ -10577,6 +10889,15 @@ window.__ModuleLoader__.load({
|
|
|
10577
10889
|
"row.poolRowRemoved": "{accountName}:已从列表中隐藏。点「重新检测账号」会重新加入,桌面 App 的登录不受影响。",
|
|
10578
10890
|
"row.poolRowRemoving": "正在删除账号…",
|
|
10579
10891
|
"row.poolRowRemoveDesktopOnly": "{accountName}:已从列表中隐藏——这个账号来自桌面 App 自己的登录,该登录未被改动。点「重新检测账号」会重新加入。",
|
|
10892
|
+
"row.upstreamHint": "这一组改的是网关看到什么,不是由哪个 provider 提供服务。四项都在插件挂载时采样,改完需重载插件或重启 DSH。",
|
|
10893
|
+
"row.usageIdentityLabel": "让用量页把调用记成官方客户端的样子",
|
|
10894
|
+
"row.usageIdentityHint": "补上 X-IDE-* 头(「使用端」那一列就不会空白),并把会话 id 改成 32 位十六进制 UUIDv7。只影响显示:User-Agent 与请求正文永不变。默认关闭——证据来自报告者,本插件未在真机复现。",
|
|
10895
|
+
"row.keepPromptLastLabel": "记录你真正输入的话,而不是注入的上下文",
|
|
10896
|
+
"row.keepPromptLastHint": "把宿主注入的运行上下文挪到真实提问前面,用量页就不会再把「Time sampled…」当成你的提问。消息条数与文本都不变。默认关闭——它会改变上游会数到的消息顺序。",
|
|
10897
|
+
"row.sessionTitleLabel": "会话标题请求改用轻量模型",
|
|
10898
|
+
"row.sessionTitleHint": "宿主每个新会话都会发一次标题请求,并按主模型费率计费;开启后这一次改投隐藏的轻量模型。请求无论开不开都会发出。默认关闭。",
|
|
10899
|
+
"row.sessionTitleModel": "目标模型:",
|
|
10900
|
+
"row.extraPrefixesCount": "生效的自定义注入前缀:",
|
|
10580
10901
|
"row.cliBridgeLabel": "允许调用本机 CodeBuddy CLI 干活",
|
|
10581
10902
|
"row.cliBridgeHint": "开启后多出 codebuddy_ask 工具与 /codebuddy 指令;每次调用花 CodeBuddy 配额。injectResult 只影响 /codebuddy:其结果回注会话,会话空闲时多花一次 DSH 回合;工具调用不读它。限流:每会话每小时 6 回合、每进程每小时 20 回合、同时只跑一个回合。默认关闭。",
|
|
10582
10903
|
"row.cliBridgeModelLabel": "CLI 模型",
|
|
@@ -10775,12 +11096,30 @@ window.__ModuleLoader__.load({
|
|
|
10775
11096
|
* example is the official DeepSeek "1017" release badge, which is the same
|
|
10776
11097
|
* job — a coloured marker beside a model name.
|
|
10777
11098
|
*
|
|
10778
|
-
*
|
|
10779
|
-
*
|
|
11099
|
+
* WHY A NEGATIVE PRIORITY (host 0.2.1-rc.2 and later). This is a `single`
|
|
11100
|
+
* slot: it holds ONE cell, and `SlotCore.register()` THROWS when a second
|
|
11101
|
+
* registration claims the same priority — "single slot … already has a
|
|
11102
|
+
* registration at priority 0 — register at a different priority to shadow
|
|
11103
|
+
* it (lowest renders)". The host itself now ships a contributor here
|
|
11104
|
+
* (`@deepseek-ai/dsh-client-ui-deepseek-model-label-1017`, in the web-app
|
|
11105
|
+
* bundle from rc.2 on) that registers at the default 0, so registering at
|
|
11106
|
+
* 0 makes the two race: whichever lands second throws. Measured on
|
|
11107
|
+
* 0.2.1-rc.3 that is US — the app's own crash log records, verbatim,
|
|
11108
|
+
* "single slot \"conversation.input.model.label\" already has a
|
|
11109
|
+
* registration at priority 0 (registered by Kb)" plus this entry's
|
|
11110
|
+
* catch-all line "model label slot failed to register" twice per page
|
|
11111
|
+
* load, which is the badge silently disappearing from the picker.
|
|
11112
|
+
*
|
|
11113
|
+
* See `MODEL_LABEL_PRIORITY` in `./WorkBuddyModelLabel.tsx` for the full
|
|
11114
|
+
* ordering, the measured evidence, and the deliberate absence of any
|
|
11115
|
+
* inter-plugin coordination (project invariant, `docs/DESIGN.md` §六).
|
|
10780
11116
|
*/
|
|
10781
11117
|
const registerModelLabel = () => {
|
|
10782
11118
|
try {
|
|
10783
|
-
return ctx.slots.inject("conversation.input.model.label", () => ctx.slots.register({
|
|
11119
|
+
return ctx.slots.inject("conversation.input.model.label", () => ctx.slots.register({
|
|
11120
|
+
name: "conversation.input.model.label",
|
|
11121
|
+
priority: MODEL_LABEL_PRIORITY
|
|
11122
|
+
}, WorkBuddyModelLabel));
|
|
10784
11123
|
} catch (error) {
|
|
10785
11124
|
console.error("[dsh-connect-workbuddy] model label slot failed to register (host provider unaffected):", error);
|
|
10786
11125
|
return;
|