# 更新日志
本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [Unreleased]
### 文档
- **`docs/plugin-development.md` 修正 4 处会误导人的内容**(2026-10-16 实测后同步):
- §1:`inject` 举例更新为实际值,并说明 `configForms` 为什么是
「声明 + `ctx.get` 探测」并存的;
- §8.3 **整节重写**:旧版教「注册到 `settings.section`」是**错的** ——
那是官方领域基座的位置。改为 `plugins.bundle.config`(插件详情页),
并给出三个 `plugins.*` 子插槽的分工表、权威原文与官方范式;
- 新增 §9.5「注入了但**没调用**:最隐蔽的一类静默失效」—— 记录本次
「点击无效」的真因(`loadReactDom` 定义了但 `apply` 从没调用它,
导致面板的 `createPortal` 分支永不成立),含静态守卫写法与两条取证纪律
(诊断自身不得派发合成事件、诊断必须按 sessionId 分桶)。
## [0.2.0] - 2026-10-16
按 `dsh-plugin-standards/docs/UI-STANDARDS.md` 与 DSH 插件开发规范**重构 UI 层**。
### 变更(破坏性)
- **UI 全面改用 `@deepseek-ai/dsh-client-ui-primitives`**,删掉所有自写控件。
它是前端 boot 的冻结 seed 表(`gM()`)里的基座模块,`require` 必定成功;
官方 10 个 `dsh-client-ui-settings-*` 里 9 个都 require 它。
- 输入框读数:自写 `
` + 自写 CSS → `Button`(`variant="ghost"`)
- 读数展开面板:自写材质 → `MenuSurface`(共享菜单材质),
定位与"点外部收起"改用官方 `useAnchoredPosition` / `useDismissOnOutsidePointer`
(此前手写了两份功能等价的实现,属白维护,且少了 `overlayTopMargin` 等细节)
- 设置卡片:自写开关/输入框/按钮 → `SettingsForm` + `SettingsFormModel` +
`SettingsValueField` + `SettingsSecretField` + `Switch` + `Button` + `Tag`
- ⚠️ **旧说法"第三方插件禁止 require 任何 Harness Client 包、只能抄 markup/CSS"已作废** ——
那是把白屏事故归错了因(真因是漏声明 `inject` 服务名)。唯一的红线是
**seed 表 9 模块白名单**,现由构建期守卫强制。
- **设置从 `settings.section` 迁到 `plugins.bundle.config`(插件详情页)**。
权威原句(`dsh-client-ui-plugin-manager/README.zh.md`):「注册了配置页的插件在这里、
在它自己的页面上编辑;设置里只保留只读的插件列表。」
用官方范式 `ctx.configForms.whileServed(...)` 跟随命名空间上线/下线边沿。
为此 `inject` 新增 `configForms`(由 `@deepseek-ai/dsh-client-ui-settings` 提供,
该包已在 `dsh.client.inject` 里)。
⚠️ **不要用 `plugins.item`**:按 README 原文那是「用于**官方插件**,按其 label 列在
**内置插件配置分组**里」—— 第三方插件注册到那里会出现在「内置插件配置」下(实测踩过)。
- 新增构建期守卫白名单(`SEED_MODULES`,9 个),并给客户端 bundle 加
`dsh.client.immediately: true`(见下)。
- 删除 `src/settings-section.js`(867 行自写 UI),由 `src/plugin-settings-card.js` 取代。
### 修复
- **刷新页面偶发整站白屏**(`web boot: 1 entry did not activate` +
`<包名>: import failed (see console for the import error)`)。
同一个 bundle 一次崩、一次不崩 —— 是竞态:DSH 把客户端 bundle 分两批,
`bootstrap` 阶段(**只有官方固定清单**)阻塞执行,`application` 阶段
(**所有第三方插件**)只发 `script-preload`、**不执行**;于是第三方插件的代码
由 `entries.start()` 之后的 `internal.import → arrive()` 惰性拉取,与 entry 创建并发。
修法:`dsh.client.immediately: true` —— web boot 的 `prefetchImmediateTier()`
会在建 entry **之前** `await` 预取完毕,从构造上消除竞态。
官方所有客户端包都设了它,第三方插件普遍不设。
- **cookie 不再明文回显**。旧实现把已保存的 sessionCookie 放进 `type=password` 的
`value` 回显,屏幕上被看到等同泄露。改用 `SettingsSecretField`(写-only)。
⚠️ 代价:无法再靠"清空输入框"**删除**已保存的 cookie(secret 空草稿不产生写入,
这是 primitives 的既有契约)。
### 变更(行为)
- **三个开关与各输入框统一为"暂存后保存"**:拨动只改草稿,点保存才落盘
(旧实现是拨一下即写)。保存落地后同步本地镜像,输入框控件仍零延迟反映。
- 刷新间隔从滑块改为数值输入框(primitives 的字段组件只有这一种),区间校验仍在。
- 启动降级提示改用 primitives `Tag`(`tone="danger"`)+ 明细列表,不再自写徽章。
## [Unreleased]
### 修复
- **插件此前在真机上从未注册过任何表面**(严重)。`loadReact()` 走的是
`ctx.import ?? ctx.require ?? globalThis.__DSH_IMPORT__`,而 DSH 的 cordis 上下文里
**没有** `import` / `require` 这两个成员 —— `ctx.` 代理只放行 Context 自带成员
与插件自己 `inject` 里声明过的服务名,读它会抛
`cannot get property "import" without inject`。这条异常被 `try/catch` 吞掉后
`apply` 直接 `return`,于是**输入框控件与设置页都不注册**,日志里只有一行 warn,
表现得像"插件装了但什么都没发生"。
- 现在 react 从 **bundle 外壳的 `require('react')`** 取,也就是宿主那张
`staticModules` 基座表(官方客户端插件用的同一条通道);
- 已在运行中的 DSH 里实测:`dsh.opendesign-usage.boot` 报告 `ok: true, failures: []`。
- **`ctx.configForms` 改为 `ctx.get('configForms')`**。前者是要声明进 `inject` 的服务,
未声明时读它直接抛;而设置命名空间本来就"可能还没到",不该变成必需依赖。
(⚠️ 0.2.0 起此条**部分回退**:`configForms` 已正式声明进 `inject`,因为设置卡片
改用官方 `whileServed` 范式读服务属性;`ctx.get` 探测作为降级路径同时保留。)
- Host 侧 `ctx.webServer` 同样改成 `ctx.get?.('webServer')`,不留同类隐患。
### 变更(2026-10-15,按用户反馈收窄读数)
- **折叠态读数改为只显示两个额度窗口**:`OD · 90% · 97%`。
原先 `OD · 50% · 12k · $1.50` 四段挤在一格,用户反馈"太长了"。
钱包余额与本会话 token 移入面板明细(点开仍可见,含重置倒计时与分项)。
- 伴随的语义收窄:`flagVisibilityOf().any` 现在等价于 `planQuota`
(折叠态只剩额度,所以"有没有东西可显示"完全由它决定)。
`enableTokenUsage` 仍然生效 —— 它控制面板明细里的 token 分项。
- 取不到的窗口显示 `—` 而**不是省略**:两个位置恒定,避免把 7 天错读成 5 小时。
### 新增
- **启动自检与降级**(`src/boot-diagnostics.js`):`apply` 每一步各自守卫,
**任何一步失败都只降级、只记录,绝不向外抛异常**——一个插件的接线错误不该让用户白屏。
报告写进 `localStorage`(键 `dsh.opendesign-usage.boot`),用
`~/.dsh/bin/dsh-boot-report --key odp` 读;设置页顶部也会列出降级详情。
- **静态守卫测试**(`test/inject-coverage.test.mjs`):
- 扫描客户端源码里所有 `ctx.` 访问,断言每个都出现在导出的 `inject` 里,
并反向断言 `inject` 无冗余项;
- 禁止源码里再出现 `ctx.import` / `ctx.require`;
- `apply` 在「服务全抛错 / 空 ctx / 拿不到 react」下都不得外抛。
- **接线层改为测真实 bundle**(`test/helpers/load-client.mjs`):此前测试直接
`import('../src/client.js')` 并靠假 `ctx.import` 提供 react,于是"测试全绿、真机全废"。
现在从 `lib/client.js` 装载,react 走生产那条 `require('react')`。
`npm test` 会先重建 bundle。
## [0.1.0] - 2026-10-15
首个版本。
### 新增
- 输入框工具行(`conversation.input.left`)的只读用量读数,仅在会话选中
`opendesign` provider 时显示。
- Open Design 套餐额度显示:5 小时与 7 天两个滚动窗口的剩余百分比、
剩余额度、重置倒计时。
- **钱包余额显示**:控制台「Design Plan 额度用尽后,自动使用此余额按量付费」那个数字。
与额度窗口同一次请求取回(宿主并发打两个上游接口),两个数据源互相独立失败。
- 本会话 token 用量显示(读宿主 `tokenUsage` 投影,无需凭据)。
- 设置页:三个独立开关、workspaceId 与 sessionCookie 输入、
「测试连接」按钮(用草稿值验证,不必先保存)。
- Host 侧 loopback-only 同源路由,凭据不出宿主进程。
- 中英双语界面文案。
### 说明
- 上游接口 `GET /api/v1/workspaces/{id}/billing/coding-plan-usage` 于
2026-10-15 实测可用;`usedCredits` / `limitCredits` / `remainingCredits`
是**字符串**,插件已做数值归一。
- 钱包余额走 `GET /api/v1/billing/summary`(**注意路径不含 workspaceId**,
与额度接口不同);金额同样是字符串,且美元与 credits 两种单位都保留。
- 上游文档里写的 `https://api.amr.dev/v1` 域名同日实测**无 DNS 记录**,
是旧构件的残留;真实接口在 `amr-api.open-design.ai`。这正是本插件把
API 根写成常量的原因,便于将来一处修改。