dsh-notice-center 1.3.1 → 1.4.2

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 ADDED
@@ -0,0 +1,143 @@
1
+ # 更新记录
2
+
3
+ 每个**已发布**版本的改动都记在这里;本文件随包一起发布(`files` 里已包含),
4
+ 发版流程见 [README → 发版](./README.md#发版)。最上面一条就是 npm 上的当前版本。
5
+
6
+ 约定:正式版发到 `latest`,`-rc.N` 只发 `next`(不动 `latest`);括号里的短哈希是那次改动的提交。
7
+
8
+ ## 1.4.2 — 2026-09-27
9
+
10
+ > **兼容性**:仍是 DSH `0.1.7-rc.2` 起(区间未变,见 1.4.0 那条)。本版有一处**行为变更**:
11
+ > `notifyEnabled` 默认值从 `false` 改为 `true` —— 升级后默认就订阅通知;不想收的把「系统通知」
12
+ > 开关关掉即可,无需任何配置迁移。该改动在**宿主半**(schema 默认值),**重启 Harness 后生效**;
13
+ > 浏览器半 `Ctrl+F5` 刷新即可。
14
+
15
+ ### 变更
16
+
17
+ - **系统通知默认开启,并在首次交互时自动请求授权**(`db2fa91`)
18
+ `notifyEnabled` 默认改为 `true`。打开页面后,借用户的**第一次点击 / 按键**自动请求一次浏览器
19
+ 通知授权 —— 浏览器不允许没有用户手势的请求(MDN:Firefox 72 起、Safari 更早直接拒绝),页面
20
+ 自己弹不出来,这是能做到的最"自动"的形态。频率:每次打开页面最多问一次、**不落任何持久标记**:
21
+ 在授权窗里选「允许」/「阻止」后不再打扰;只点 × / Esc(什么也没选)不算做选择,下次刷新会再
22
+ 问一次,直到做出选择或把开关关掉。
23
+
24
+ ### 修复
25
+
26
+ - **任何配置项改完都要等一会儿才生效**(`db2fa91`)
27
+ 与 1.4.0 里音量、颜色那两条同源,这次把设置区**全字段**统一到一张当帧回显草稿表:官方
28
+ `Switch` 是 fully controlled(只认 `props.checked`)、音效下拉只认受控 `value`、滑杆与取色器
29
+ 的受控 `value` 也会被旧快照拽回,而每笔写都要重写整份 profile 补丁并热重载插件条目
30
+ (实测约 1s/笔)—— 只读快照的控件在回显到达前一直显示旧值。现在写入当帧先画、快照追平后
31
+ 交还;滑杆 / 取色器仍按手势合并(首笔立即 + 静默 150ms 补最后一笔)。
32
+
33
+ - **连点几次开关后,最后落在开启上却「自己关掉」**(`db2fa91`)
34
+ 回显草稿是 React state,设置区随热重载重建就丢;写被拒时又会把画面摔回旧快照;排队中的尾笔
35
+ 写也可能随实例一起没掉。现在照官方「代码工作工具」那一行(`DeveloperToolsRow`)的写法:一笔
36
+ 写在途就把开关 `disabled`(原生 `<button disabled>`,浏览器不派发点击 → 连点只产生一笔写),
37
+ 写被拒时行内 `role="alert"` 报「保存失败,请重试」并回滚画面;同时删掉上一版的后台重推
38
+ (它会和用户下一次点击抢着写)。「最后一次操作」另提到模块级,组件重建时不闪回更早的宿主值。
39
+
40
+ ### 新增
41
+
42
+ - **通知权限状态 chip**(`db2fa91`)
43
+ 「系统通知」开关**正下方**常驻短状态,长解释交给官方 `Tooltip`(hover 与 focus 都触发,键盘
44
+ 可达):未授权 =「未授权 · 授权」可点(点了重新弹授权窗);已拒绝 = 叹号 +「已被拒绝」+
45
+ 恢复路径(浏览器不会二次弹窗,网页也无权打开站点设置页);不支持 = 一行小字(多为非安全
46
+ 上下文,例如用局域网 IP 打开);已授权 = 不显示。权限态用 Permissions API 的 `onchange` +
47
+ `focus` / `visibilitychange` 实时刷新;授权成功当场弹一条「通知已开启」确认。
48
+
49
+ ## 1.4.0 — 2026-09-27
50
+
51
+ > **兼容性**:要求 DSH `0.1.7-rc.2` 起 —— 0.1.x 线内放行,跨到 0.2 线(含其预发布)须重新
52
+ > 验证后再放行(声明原文 `>=0.1.7-rc.2 <0.2.0-0`)。设置表单改用 0.1.7 的
53
+ > `runtime.Config` 派生,**宿主 0.1.6 及更早请继续用 1.3.1**。
54
+
55
+ ### 修复
56
+
57
+ - **浅色系统上标签页状态灯永远不显色**(`981e8cd`)
58
+ 宿主 `index.html` 挂了两枚 `<link rel="icon">`(`favicon-dark.svg` / `favicon.svg`,
59
+ 靠 `media="(prefers-color-scheme: …)"` 区分),浏览器按系统主题只渲染其中一枚;旧代码
60
+ `querySelector("link[rel~=icon]")` 只拿到第一枚,而浅色主题下渲染的恰恰是第二枚 ——
61
+ 琥珀、绿、默认色全画在没人用的那枚上,标签页看着一直是默认图标。现在改**全部** icon,
62
+ 逐枚记住各自原始 `href`(还原时对号入座),页面上一枚都没有时自建一枚再上色。
63
+
64
+ - **音量滑杆「改了没反应、还回弹」**(`2cfc6b4`)
65
+ 每笔 `set` 都要重写整份 profile 的 `cordis.patch.yml` 并热重载插件条目(实测约 1s/笔),
66
+ 而原生滑杆每挪一格就发一次 `input`,一次拖动几十笔全排进写队列,回显被拖到几秒之后,
67
+ 受控滑杆又被旧快照拽回 —— 表现为「点一下固定差 5%,右滑也是下降」。现在按**手势合并写**:
68
+ 首笔立即写(点一下就生效),拖动中只攒最新值,静默 150ms 后补最后一笔(一次手势 1–2 笔),
69
+ 同时当帧画回显草稿,写入最终没落盘才回滚。
70
+
71
+ - **点「↺ 恢复默认」/ 取色后,预览鲸鱼要等一会儿才变色**(`c49231d`)
72
+ 与上一条同源:预览和取色器都只读快照。现在颜色也有当帧草稿,写入落定后交还快照、失败回滚;
73
+ 取色同样按手势合并(原生取色器拖动时会连发 `input`);复位是整笔覆盖,先丢掉排队中的取色值
74
+ 再立即 `unset`。
75
+
76
+ ### 变更
77
+
78
+ - 设置页迁移到 DSH 0.1.7 的设置 API:改读 `ctx.configForms.get("notice-center")`,`loading`
79
+ 不渲染,`unavailable` / 只读时给一行说明;宿主半只声明 Config schema,旧 `settings.yaml`
80
+ 由 `dsh-settings` 按同名条目 id 迁移。
81
+ - 宿主半每个字段补 `.volatile()`(`volatileForm` 只投影「最近的可变祖先」下的字段,漏标一个
82
+ 那项就会在设置页整条消失),`schemastery` 升到 `^3.18.4`;保留 `/notice-center-sounds`
83
+ 音效路由。
84
+ - 修正图标名:`…Outline14` → `…OutlineRegular`。宿主静态模块表里没有 `…14` 那套导出,
85
+ `jsx(undefined)` 会被插槽错误边界吞掉,整个设置页会变成空白(配置项一条都不显示)。
86
+ - 导航图标换成自定义铃铛(`viewBox 0 0 1024 1024`):铃身 + 底座合并成一条 `currentColor`
87
+ 路径跟随主题,高光保留 `#3399FF`;官方回退齿轮的 path 不够用时只追加克隆节点(不删 React
88
+ 管的节点),幂等跳过避免 rAF 死循环。
89
+ - `engines` / `peerDependencies` 收到 `0.1.7-rc.2` 起(上限锁在 0.2 线之前,声明原文
90
+ `>=0.1.7-rc.2 <0.2.0-0`)。
91
+
92
+ ### 测试与工程
93
+
94
+ - 新增 `test/compat-range.smoke.mjs`:0.1.4 ~ 0.2.0 宿主兼容区间,`npm test` 一并跑。
95
+ - `primitives` 桩改成导出白名单(名单外的名字直接抛错,原来的 Proxy 对任何名字都造得出假组件);
96
+ `rAF` 桩改为入队 + 手动 flush,对齐真浏览器时序;新增导航图标 9 条断言。
97
+ - 浏览器半冒烟补:两枚 favicon 都换色 / 各自还原 / 一枚都没有时自建一枚;音量手势合并写与
98
+ 回显草稿;颜色当帧回显与合并写(第 17–19 节)。
99
+
100
+ ## 1.3.1 — 2026-09-24
101
+
102
+ - fix: 「自动隐藏」的提示文案只讲了关闭侧,改为两侧都写清楚(`92b0dae`)
103
+ - feat: 设置页页脚(插件名 + 版本号,点击开仓库)(`ce8109e`)
104
+ - test: 补「同一会话连续完成两次 → 弹两次」回归用例,把去重语义写精确 —— 管的是
105
+ 「同一次完成」,不是「同一会话」(`d44ae93`、`cd8f2f2`)
106
+ - docs: README 重构为「使用 / 开发」两部分并加目录导航,补齐中英文主图与真实设置页截图
107
+
108
+ ## 1.3.0 — 2026-09-19
109
+
110
+ - fix: 状态灯改为**待处理优先**,与官方 `sessionStatuses` 一致(`735007f`)
111
+ - feat: 完成通知附本轮总用时;待处理通知按交互类型细化;新增「前台提醒」开关;调完音量试听
112
+ 完成提示音;设置页分组行整行可点(不必去够最右边的箭头)
113
+ - test: 新增设置页渲染测试(记录型桩,零新依赖)(`ed7acd6`)
114
+ - docs: 新增英文 README(`README.en.md`)
115
+
116
+ ## 1.3.0-rc.1 — 2026-09-19
117
+
118
+ - ci: 新增 tag 触发的 OIDC 发布工作流(npm Trusted Publishing,无令牌 + 自动 provenance),
119
+ 并加幂等保护:该版本已在 registry 上就跳过(`a8a2f38`、`de49005`)
120
+ - 首次实测 OIDC 发布链路
121
+
122
+ ## 1.2.0 — 2026-09-19
123
+
124
+ - feat: 提示音可选 —— 45 个 opencode 内置音效,下拉里悬浮即试听(`40521ef`)
125
+ - docs: README 重写(功能 / 使用 / 截图)
126
+
127
+ ## 1.1.0 — 2026-09-18
128
+
129
+ - test: 补浏览器半冒烟测试并接入 CI(`d388890`)
130
+ - feat: 通知标题改为会话名、正文改为通知类型(`9a541e9`);新增「自动隐藏」开关,关闭则常驻
131
+ - fix: 通知去掉 `tag`、前台判定纳入窗口焦点 —— 修「同一对话只有第一条会弹」(`8b75353`)
132
+
133
+ ## 0.2.2 — 2026-08-22
134
+
135
+ - 设置页与「选中会话」信号灯的修复版(`f278787`)
136
+
137
+ ## 0.1.0 — 2026-08-16
138
+
139
+ - 首个正式版:标签页鲸鱼状态灯 + 浏览器系统通知(`83a2860`)
140
+
141
+ ---
142
+
143
+ > 0.1.0 – 1.1.0 是改名 / 独立收敛之前的产物;npm 上的 `dsh-notice-center` 从 1.2.0 开始。
package/README.en.md CHANGED
@@ -36,6 +36,7 @@
36
36
  - [Local development](#local-development)
37
37
  - [Tests](#tests)
38
38
  - [Releasing](#releasing)
39
+ - [Changelog](./CHANGELOG.md)
39
40
  - [Design constraints](#design-constraints)
40
41
 
41
42
  ## Why you need it
@@ -116,7 +117,9 @@ Finished and pending each have their own dropdown, **47 entries** in total: 2 bu
116
117
 
117
118
  ### Requirements
118
119
 
119
- - **Official DeepSeek Harness** (supported range `0.1.2-rc.1+`; verified here on `0.1.5-rc.1` with client packages `0.1.5-rc.2`)
120
+ - **Official DeepSeek Harness**: `1.4.0` requires **`0.1.7-rc.2+`**; `1.3.x` supports `0.1.2-rc.1+`
121
+ - The range is declared in `engines.dsh` and the `@deepseek-ai/dsh-*` peers, guarded by `test/compat-range.smoke.mjs`. The two lines split by host version: **the Market never offers 1.4.0 to an older host, and the DSH core refuses to load it** — older hosts simply stay on `1.3.x` with no update prompt.
122
+ - The version gate only compares version strings and cannot detect a removed API (0.1.7 dropped `settings.register` while the old declaration still passed), so never treat it as a compatibility guarantee on its own.
120
123
  - System notifications need a **secure context** — `http://127.0.0.1:3080` or `localhost` works; over a **LAN IP** the browser's Notification API is unavailable (a browser restriction, not a plugin issue)
121
124
 
122
125
  ### Option 1: from npm (recommended)
@@ -160,7 +163,23 @@ Remove the dependency and the `dsh.profile.bundles` entry, then restart — noth
160
163
  ## Getting started in three steps
161
164
 
162
165
  1. **Open the settings page** — Settings → **Notification center** (a sidebar entry)
163
- 2. **Turn on system notifications** — flip the **System notifications** master switch; the browser asks for permission, choose *Allow*. If it was denied, the settings page tells you, and you must re-enable it in the browser's **site settings** (browsers do not ask twice)
166
+ 2. **Grant system notifications** — the master switch is **on by default**: after the page loads, your first click or key press makes the plugin ask the browser for notification permission once (browsers **refuse** requests without a user gesture, so the page cannot pop it on its own — it borrows your first interaction). Choose *Allow* and you also get a "Notifications enabled" confirmation. Frequency rules:
167
+
168
+ - **at most once per page load** (clicking more in the same session never re-prompts);
169
+ - choose *Allow* or *Block* → **never asked again** (the permission is settled);
170
+ - merely closing the prompt with **× / Esc** (neither choice) → not a decision, so the **next page load asks again** until you decide;
171
+ - to stop it entirely: turn the **System notifications** switch off, or choose *Block*.
172
+
173
+ Later, use the status chip right **below the switch** (the short state is always visible; hover it or focus it with the keyboard for the full explanation):
174
+
175
+ | State | Chip | What to do |
176
+ |---|---|---|
177
+ | **Not granted** (you dismissed the prompt) | "Not granted - Grant", clickable | Click it and the browser asks again |
178
+ | **Blocked** | warning icon + "Blocked" | Browsers do **not** ask twice (and a page may not open site settings): hover for the recovery path — click the icon at the left of the address bar → Notifications → Allow, or open `chrome://settings/content/notifications` |
179
+ | **Unsupported** | one small line | Usually a **secure context** problem (see *Install → Requirements*): use `http://127.0.0.1:3080` |
180
+ | Granted | hidden (the row stays clean) | — |
181
+
182
+ The switch itself always works (you can turn notifications off even while unauthorised — and turning it off stops the automatic request); change the permission in site settings and the chip follows immediately.
164
183
  3. **Tune as you like** — expand the two groups: change colours, pick sounds, set volume, toggle **Notify in the foreground** / **Auto hide**
165
184
 
166
185
  <img src="docs/images/settings-en.png" width="440" alt="Notification center settings page">
@@ -175,7 +194,7 @@ After that, whenever you switch away or minimise, a finished session or a pendin
175
194
  | | Finished | `green` | `#22C55E` | Finished status-light colour (official sidebar colour) |
176
195
  | | Pending | `amber` | `#F59E0B` | Pending status-light colour (official sidebar colour) |
177
196
  | | Default colour | `black` | unset | Unset = keep the stock `/favicon.svg` |
178
- | System notifications | Master switch | `notifyEnabled` | **off** | opt-in; turning it on asks for browser permission |
197
+ | System notifications | Master switch | `notifyEnabled` | **on** | On by default; the first interaction asks for browser permission, and a status chip appears below the switch while it is missing |
179
198
  | | Auto hide | `notifyAutoHide` | on | Off = the notification stays until you dismiss it |
180
199
  | | Notify in the foreground | `notifyForeground` | off | On = also notify while you are looking at the page |
181
200
  | | Sound | `notifySound` | on | When on, the system sound is muted and the plugin sound plays |
@@ -192,6 +211,7 @@ The settings page also shows a one-line footer at the very top with the plugin n
192
211
  |---|---|
193
212
  | No "Notification center" entry in Settings | The plugin did not load. Make sure you **restarted Harness** after installing, then look for `settings namespace "notice-center" registered` in the Harness console |
194
213
  | A switch does nothing / a new setting has no effect | After changing the **host half** (`lib/index.mjs`) you must restart Harness — settings writes go through the host schema |
214
+ | A permission prompt popped up on my very first click after installing | The master switch is on by default, so the plugin borrows your **first interaction** (click / key press) to ask once — browsers refuse permission requests without a user gesture. Choose *Block* if you do not want it, and use the status chip in the settings later |
195
215
  | No notifications at all | Three conditions are required: ① the master switch is on; ② browser permission is *Allow*; ③ the page is not in the foreground when the event fires (use **Notify in the foreground** to relax this) |
196
216
  | No notifications over a LAN IP | Non-secure context: the browser disables the Notification API, so system notifications are **entirely unavailable** (unrelated to *Notify in the foreground*). Use `127.0.0.1` / `localhost`; over a LAN only the tab status light works |
197
217
  | Nothing after closing the tab | Known limitation, see below |
@@ -221,23 +241,30 @@ The plugin has **two halves** running in different processes, exchanging configu
221
241
 
222
242
  | Part | File | Runs in | Responsibility |
223
243
  |---|---|---|---|
224
- | **Host half** | `lib/index.mjs` (128 lines) | Harness main process (Node) | ① Register the `notice-center` settings namespace schema; ② serve `/notice-center-sounds/<id>.mp3` |
225
- | **Browser half** | `lib/client.cjs` (1344 lines) | Browser (plugin bundle) | favicon state machine + notification state machine + sound library + settings page |
226
- | **Bundle patch** | `cordis.patch.yml` | Loader layer | Insert one row `id: notice-center` into the plugin tree |
244
+ | **Host half** | `lib/index.mjs` | Harness main process (Node) | ① Declare the `Config` schema (settings form, write validation and the legacy `settings.yaml` import all derive from it); ② serve `/notice-center-sounds/<id>.mp3` |
245
+ | **Browser half** | `lib/client.cjs` | Browser (plugin bundle) | favicon state machine + notification state machine + sound library + settings page |
246
+ | **Bundle patch** | `cordis.patch.yml` | Loader layer | Insert one row `id: notice-center` into the plugin tree (that id IS the settings namespace) |
227
247
 
228
248
  **The host half's two optional channels**:
229
249
 
230
- - `ctx.inject(["settings"], …)` — registers the schema; a success line is logged to the console as proof
250
+ - `ctx.inject(["settings"], …)` — declares "this plugin ships its own page": `settings.configure({ auto: false }, ctx.fiber)`. The owner must be the plugin's own fiber (`describe()` looks the policy up by `entry.fiber`, and the `inject` child is a different fiber)
231
251
  - `ctx.inject(["webServer"], …)` — mounts the sound route. When the service is absent the route is not mounted and the browser half falls back to the built-in chime
232
252
 
233
- **Why the host half does not import `@deepseek-ai/dsh-settings`**: since `0.1.2-rc.1` that package no longer exports `settingsNamespace()`, and `register` validates the kebab-case string itself, so passing a plain string is enough. **That keeps the host half's only runtime dependency at `@deepseek-ai/schemastery`** — and because profiles default to `autoInstallPeers: false`, resolving any `@deepseek-ai` peer would actually fail on a version mismatch and break loading. This is what makes a clean registry install possible.
253
+ **The settings contract (since 0.1.7 — the easiest thing to break silently here)**:
254
+
255
+ - The form is derived from the **plugin's own Config**: dsh-settings' `describe()` reads the entry fiber's `runtime.Config`, and the entry id is the namespace — the plugin **no longer registers a namespace** (the old `settings.register(ns, schema)` was removed in 0.1.7).
256
+ - **Every field must be `.volatile()`**: `volatileForm()` only projects fields under the nearest volatile ancestor, so one unmarked field disappears from the settings page entirely (`test/host-half.smoke.mjs` asserts this field by field). `.volatile()` requires **schemastery ≥ 3.18.4**.
257
+ - Existing users need no manual migration: at startup dsh-settings reads `<DSH_HOME>/settings.yaml`, writes each section into the entry carrying the **same id**, then renames the file to `settings.yaml.imported`. Our section name equals the entry id (`notice-center`).
258
+
259
+ **Why the host half does not import `@deepseek-ai/dsh-settings`**: it keeps the host half's only runtime dependency at `@deepseek-ai/schemastery` — and because profiles default to `autoInstallPeers: false`, resolving any `@deepseek-ai` peer would actually fail on a version mismatch and break loading. This is what makes a clean registry install possible.
234
260
 
235
261
  **Where the browser half gets its data** (all official client services; the plugin holds no state of its own):
236
262
 
237
263
  | Service | Provided by | Used for |
238
264
  |---|---|---|
239
- | `sessions` | `@deepseek-ai/dsh-api-session-controller/client` | Session list snapshot: `completed` / `origin` / `running` / `current` |
240
- | `uiSession` | `@deepseek-ai/dsh-client-ui-session` | `pendingInteractions`: `Map<sessionId, { kind, … }>` |
265
+ | `configForms` | `@deepseek-ai/dsh-client-ui-settings` | `configForms.get(entryId)` reads and writes settings (the old `settingsScope` was removed in 0.1.7); the snapshot carries `status` / `writable` |
266
+ | `sessions` | `@deepseek-ai/dsh-api-session-controller/client` | Session list snapshot: `origin` / `running` / `displayTitle` |
267
+ | `uiSession` | `@deepseek-ai/dsh-client-ui-session` | `sessionStatus`: `Map<sessionId, { running, pendingInteraction, completionUnread }>` — 0.1.7 removed both the row fields `completed` / `pendingInteraction` and the `pendingInteractions` store; the official workspace session rows use the same mapping (`completed` ≡ `completionUnread`) |
241
268
 
242
269
  The sound route is deliberately narrow: only `^[a-z0-9-]+\.mp3$` filenames are served (no path traversal), and because the audio is a package constant it is cached `immutable`.
243
270
 
@@ -333,6 +360,8 @@ The workflow has **three gates**; failing any one blocks the release:
333
360
 
334
361
  The publish step is also **idempotent**: if that version already exists on the registry it is skipped, so re-tagging or re-running never fails the build.
335
362
 
363
+ Before tagging, add this release to [`CHANGELOG.md`](./CHANGELOG.md) (user-visible changes plus compatibility notes) — it is the source of the release notes shown on the npm page and the GitHub Release.
364
+
336
365
  ### One-time setup: npm Trusted Publishing (OIDC)
337
366
 
338
367
  Publishing needs no npm token at all (so there is nothing to expire or leak). Open the package page → **Publishing Access** → *Trusted publishers* → **Add a trusted publisher** → choose **GitHub Actions**:
package/README.md CHANGED
@@ -36,6 +36,7 @@
36
36
  - [本地开发](#本地开发)
37
37
  - [测试](#测试)
38
38
  - [发版](#发版)
39
+ - [更新记录](./CHANGELOG.md)
39
40
  - [设计约束](#设计约束)
40
41
 
41
42
  ## 为什么需要它
@@ -115,7 +116,9 @@
115
116
 
116
117
  ### 前置要求
117
118
 
118
- - **官方版本的 DeepSeek Harness**(适配范围 `0.1.2-rc.1+`;本机实测 `0.1.5-rc.1` + 客户端包 `0.1.5-rc.2`)
119
+ - **官方版本的 DeepSeek Harness**:`1.4.0` 起要求 **`0.1.7-rc.2+`**;`1.3.x` 支持 `0.1.2-rc.1+`
120
+ - 兼容范围写在 `engines.dsh` 与 `@deepseek-ai/dsh-*` peer 上,由 `test/compat-range.smoke.mjs` 守护。两条线按宿主版本自动分流:**插件市场不会把 1.4.0 推给旧宿主,DSH 核心在加载前也会拒绝它**——旧宿主原地留在 `1.3.x`,不会收到更新提示。
121
+ - 版本闸门只比版本号,查不出 API 被删(0.1.7 删掉了 `settings.register`,旧声明在那上面照样 PASS),所以别只靠它当兼容性保障。
119
122
  - 系统通知需要**安全上下文**——`http://127.0.0.1:3080` 或 `localhost` 可用;用**局域网 IP** 访问时浏览器的 Notification API 不可用(这是浏览器限制,不是插件问题)
120
123
 
121
124
  ### 方式一:从 npm 安装(推荐)
@@ -159,7 +162,23 @@ dsh plugin --profile web remove dsh-notice-center
159
162
  ## 上手三步
160
163
 
161
164
  1. **打开设置页** —— 设置 → **通知中心**(侧栏导航项)
162
- 2. **开启系统通知** —— 打开「系统通知」总开关,浏览器会弹出授权请求,选「允许」。被拒绝时设置页会提示,需要到浏览器的**站点设置**里恢复(浏览器不会二次弹窗)
165
+ 2. **授权系统通知** —— 总开关**默认就是开的**:打开页面后,你**第一次点击或按键**时,插件会自动向浏览器请求一次通知授权(浏览器**不允许**没有用户手势的请求,页面自己弹不出来,所以只能借你的一次交互),选「允许」,同时会收到一条「通知已开启」的确认。频率规则:
166
+
167
+ - **每次打开页面最多问一次**(同一次会话里点多少下都只问一次,不会连环弹);
168
+ - 你在授权窗上选了「允许」或「阻止」→ **不会再问**(权限有定论了);
169
+ - 你只是点了 **× / Esc**(既没允许也没阻止)→ 不算做出选择,**下次刷新页面会再问一次**,直到你选为止;
170
+ - 想彻底不被打扰:把「系统通知」开关**关掉**,或在授权窗里选「阻止」。
171
+
172
+ 之后想补授权,用开关**正下方**那枚状态 chip(短状态常驻、鼠标悬浮或键盘聚焦问号/叹号看完整说明):
173
+
174
+ | 状态 | chip 长什么样 | 怎么办 |
175
+ |---|---|---|
176
+ | **未授权**(自动请求被你关掉了,或从没问过) | 「未授权 · 授权」可点 | 点它,浏览器会重新弹授权窗 |
177
+ | **已被拒绝** | 叹号 +「已被拒绝」 | 浏览器**不会二次弹窗**(网页也无权打开站点设置页):悬浮看恢复路径 —— 点地址栏左侧的图标 → 通知 → 允许,或打开 `chrome://settings/content/notifications` |
178
+ | **当前浏览器不支持** | 一行小字 | 多半是**安全上下文**问题(见「安装 → 前置要求」),用 `http://127.0.0.1:3080` 访问即可 |
179
+ | 已授权 | 不显示(行保持干净) | — |
180
+
181
+ 开关本身始终可切(未授权时也能把通知关掉,关掉后就不会再自动请求);你在站点设置里改完权限回到页面,chip 会立刻跟上。
163
182
  3. **按需微调** —— 展开两个分组:调颜色、选音效、调音量、开关「前台提醒」/「自动隐藏」
164
183
 
165
184
  <img src="docs/images/settings.png" width="440" alt="通知中心设置页">
@@ -174,7 +193,7 @@ dsh plugin --profile web remove dsh-notice-center
174
193
  | | 完成 | `green` | `#22C55E` | 完成状态灯颜色(官方侧边栏色) |
175
194
  | | 待处理 | `amber` | `#F59E0B` | 待处理状态灯颜色(官方侧边栏色) |
176
195
  | | 默认色 | `black` | 未设置 | 不设置=沿用官方原版 `/favicon.svg` |
177
- | 系统通知 | 总开关 | `notifyEnabled` | **关** | opt-in;开启需浏览器授权 |
196
+ | 系统通知 | 总开关 | `notifyEnabled` | **开** | 默认开启;首次交互自动请求浏览器授权,未授权时开关下方给状态 chip |
178
197
  | | 自动隐藏 | `notifyAutoHide` | 开 | 关=通知常驻屏幕,需手动关闭 |
179
198
  | | 前台提醒 | `notifyForeground` | 关 | 开=你正看着页面时也提醒 |
180
199
  | | 提示音 | `notifySound` | 开 | 开启后静音系统提示音,改播插件音效 |
@@ -191,7 +210,9 @@ dsh plugin --profile web remove dsh-notice-center
191
210
  |---|---|
192
211
  | 设置里找不到「通知中心」 | 插件没加载成功。确认装完**重启过 Harness**,再看 Harness 控制台有没有 `settings namespace "notice-center" registered` 这行日志 |
193
212
  | 开关点了没反应 / 加了新设置项不生效 | 改过**宿主半**(`lib/index.mjs`)后必须重启 Harness——设置写入要过宿主 schema 校验 |
213
+ | 装完插件后第一次点页面就弹出授权窗 | 通知开关默认开启,插件借你**第一次交互**(点击/按键)自动请求一次授权 —— 浏览器不允许没有用户手势的请求,页面自己弹不出来。不想授权就点「阻止」,之后可在设置页用状态 chip 补授权 |
194
214
  | 完全不弹通知 | 三个条件缺一不可:① 系统通知总开关已打开;② 浏览器权限为「允许」;③ 触发时页面不在前台(默认策略,可用「前台提醒」放宽) |
215
+ | 打开开关没弹授权窗 | 只有权限处于「还没问过」时浏览器才会弹;已经「拒绝」过就**不会再弹**,页面会给出站点设置的恢复路径。另外授权请求必须在点击里发起、且要安全上下文(`127.0.0.1` / `localhost`) |
195
216
  | 用局域网 IP 打开时不弹 | 非安全上下文,浏览器禁用 Notification API,系统通知**完全不可用**(与「前台提醒」无关)。改用 `127.0.0.1` / `localhost`;局域网下只能靠标签页状态灯 |
196
217
  | 标签页关掉后收不到 | 已知限制,见下文 |
197
218
  | 刷新页面后绿灯变回黑色 | 正常。该状态是官方信号的内存态,刷新即丢 |
@@ -220,23 +241,30 @@ dsh plugin --profile web remove dsh-notice-center
220
241
 
221
242
  | 组成 | 文件 | 跑在哪 | 职责 |
222
243
  |---|---|---|---|
223
- | **宿主半** | `lib/index.mjs`(128 行) | Harness 主进程(Node) | ① 注册 `notice-center` 设置命名空间 schema;② 挂载 `/notice-center-sounds/<id>.mp3` 静态音效路由 |
224
- | **浏览器半** | `lib/client.cjs`(1344 行) | 浏览器(插件 bundle) | favicon 状态机 + 通知状态机 + 音效库 + 设置页 |
225
- | **bundle patch** | `cordis.patch.yml` | Loader 层 | 往插件树里插入一行 `id: notice-center` |
244
+ | **宿主半** | `lib/index.mjs` | Harness 主进程(Node) | ① 声明 `Config` schema(设置表单、写入校验、旧 `settings.yaml` 迁移都由它派生);② 挂载 `/notice-center-sounds/<id>.mp3` 静态音效路由 |
245
+ | **浏览器半** | `lib/client.cjs` | 浏览器(插件 bundle) | favicon 状态机 + 通知状态机 + 音效库 + 设置页 |
246
+ | **bundle patch** | `cordis.patch.yml` | Loader 层 | 往插件树里插入一行 `id: notice-center`(这个 id 就是设置命名空间) |
226
247
 
227
248
  **宿主半的两个「可选通道」**:
228
249
 
229
- - `ctx.inject(["settings"], …)` —— 注册 schema。注册成功会在控制台留一行自证日志
250
+ - `ctx.inject(["settings"], …)` —— 声明「本插件自带设置页」:`settings.configure({ auto: false }, ctx.fiber)`。owner 必须是插件自身的 fiber(`describe()` 按 `entry.fiber` 查这份策略,而 `inject` 子 fiber 是另一个 fiber)
230
251
  - `ctx.inject(["webServer"], …)` —— 挂载音效路由。服务缺席时不挂载,浏览器半自动回退到内置合成音
231
252
 
232
- **为什么宿主半不 import `@deepseek-ai/dsh-settings`**:自 `0.1.2-rc.1` 起该包不再导出 `settingsNamespace()`,注册处自己按 kebab-case 校验字符串,因此直接传纯字符串即可。**这样宿主半的运行时依赖只剩 `@deepseek-ai/schemastery`**——而 profile 默认 `autoInstallPeers: false`,真去解析任何 `@deepseek-ai` peer 反而会因 npm 上的版本不匹配而加载失败。这是「能从 registry 干净安装」的关键。
253
+ **设置契约(0.1.7 起,这是本插件最容易静默踩坏的地方)**:
254
+
255
+ - 表单由**插件自己的 Config** 派生:dsh-settings 的 `describe()` 读入口 fiber 的 `runtime.Config`,条目 id 就是命名空间 —— 插件**不再注册命名空间**(旧的 `settings.register(ns, schema)` 在 0.1.7 已删除)。
256
+ - **每个字段都要 `.volatile()`**:`volatileForm()` 只投影「最近的可变祖先」下的字段,漏标一个就等于那个设置项在设置页里整条消失(`test/host-half.smoke.mjs` 为此逐字段断言)。`.volatile()` 需要 **schemastery ≥ 3.18.4**。
257
+ - 老用户设置无需手工迁移:dsh-settings 启动时会读 `<DSH_HOME>/settings.yaml`,把每个 section 按名字写进**同 id** 的条目,再把原文件改名为 `settings.yaml.imported`。我们的段名正好等于条目 id(`notice-center`)。
258
+
259
+ **为什么宿主半不 import `@deepseek-ai/dsh-settings`**:宿主半的运行时依赖只剩 `@deepseek-ai/schemastery`——而 profile 默认 `autoInstallPeers: false`,真去解析任何 `@deepseek-ai` peer 反而会因 npm 上的版本不匹配而加载失败。这是「能从 registry 干净安装」的关键。
233
260
 
234
261
  **浏览器半的数据来源**(都是官方客户端服务,插件不自持状态):
235
262
 
236
263
  | 服务 | 提供包 | 用途 |
237
264
  |---|---|---|
238
- | `sessions` | `@deepseek-ai/dsh-api-session-controller/client` | 会话列表快照:`completed` / `origin` / `running` / `current` |
239
- | `uiSession` | `@deepseek-ai/dsh-client-ui-session` | `pendingInteractions`:`Map<sessionId, { kind, … }>` |
265
+ | `configForms` | `@deepseek-ai/dsh-client-ui-settings` | `configForms.get(entryId)` 读写设置(旧的 `settingsScope` 在 0.1.7 已删除);快照带 `status` / `writable` |
266
+ | `sessions` | `@deepseek-ai/dsh-api-session-controller/client` | 会话列表快照:`origin` / `running` / `displayTitle` |
267
+ | `uiSession` | `@deepseek-ai/dsh-client-ui-session` | `sessionStatus`:`Map<sessionId, { running, pendingInteraction, completionUnread }>` —— 0.1.7 的 `completed` / `pendingInteraction` 行字段与 `pendingInteractions` store 都已删除,官方 workspace 的会话行用的是同一套映射(`completed` ≡ `completionUnread`) |
240
268
 
241
269
  音效路由的安全约束:只放行 `^[a-z0-9-]+\.mp3$` 的文件名(杜绝路径穿越),音频是随包常量故用 `immutable` 长缓存。
242
270
 
@@ -332,6 +360,9 @@ npm run test:client # 只跑浏览器半
332
360
 
333
361
  另外发布步骤**幂等**:该版本已在 registry 上就跳过,重复打 tag / 重跑工作流不会红。
334
362
 
363
+ 发版前先在本仓库 [`CHANGELOG.md`](./CHANGELOG.md) 补上这一版(用户可见改动 + 兼容性)再打 tag ——
364
+ 它是 npm 包页与 GitHub Release 上那份「更新说明」的来源。
365
+
335
366
  ### 一次性配置:npm Trusted Publishing (OIDC)
336
367
 
337
368
  发布不需要任何 npm 令牌(也就没有令牌过期 / 泄漏问题)。打开包管理页 → **Publishing Access** → *Trusted publishers* → **Add a trusted publisher** → 选 **GitHub Actions**:
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-368 -368 1760 1760">
2
+ <!-- 通知中心的插件图标:官方插件页(侧栏 Plugins 面板)每张卡片/每一行用的就是它。
3
+ 宿主 (dsh-app-boot) 读 package.json 顶层 icon,校验后转成 data:URL 交给 <img> 渲染,
4
+ 所以写死颜色、不能用 currentColor 跟随主题(与 lib/client.cjs 里那颗侧栏导航铃铛不同)。
5
+ 铃身 #3399FF 是品牌蓝,浅色与深色背景都看得清;高光 #E8F2FF。
6
+ 想要原图那种深灰铃身,把第一条 path 的 fill 换成 #333333 即可。
7
+ viewBox 按官方插件图标的留白对齐(官方两款同样画在 36x36 网格上,实测:「自动审查」
8
+ 内容占 52.7% x 66.6%、留白 23.6%/16.7%;「智能体团队」内容占 50% x 50%、留白 25%/24%)。
9
+ 本图铃铛本体实测 922 x 1025(比宽略高),取外框 1760 并居中 —— 内容占 52.4% x 58.2%,
10
+ 横向留白 23.8%(与官方一致)、纵向 20.9%(介于官方两款之间),缩到插件页的 30/36px
11
+ 观感与官方图标齐平;文件 <=256 KiB。 -->
12
+ <path fill="#3399FF" d="M921.6 880.64h-819.2A51.2 51.2 0 0 1 51.2 829.44v-66.56A107.7248 107.7248 0 0 1 158.72 655.36a30.72 30.72 0 0 0 30.72-30.72V376.832A319.6928 319.6928 0 0 1 399.36 78.2336a112.0256 112.0256 0 0 1 213.6064 0A319.6928 319.6928 0 0 1 824.32 376.832v247.808a30.72 30.72 0 0 0 30.72 30.72h10.24A107.7248 107.7248 0 0 1 972.8 762.88v66.56a51.2 51.2 0 0 1-51.2 51.2zM112.64 819.2h798.72v-56.32A46.08 46.08 0 0 0 865.28 716.8h-10.24a92.16 92.16 0 0 1-92.16-92.16V376.832a257.8432 257.8432 0 0 0-184.32-245.76l-19.0464-5.7344-2.6624-20.48a50.7904 50.7904 0 0 0-100.7616 0l-2.6624 20.48-19.0464 5.7344a257.8432 257.8432 0 0 0-184.32 245.76v247.808A92.16 92.16 0 0 1 158.72 716.8 46.08 46.08 0 0 0 112.64 762.88z M512 1024a174.2848 174.2848 0 0 1-174.08-174.08h61.44a112.64 112.64 0 0 0 225.28 0h61.44a174.2848 174.2848 0 0 1-174.08 174.08z"/>
13
+ <path fill="#E8F2FF" d="M665.6 353.6896a30.72 30.72 0 0 1-25.6-13.9264A259.4816 259.4816 0 0 0 542.72 251.904a30.72 30.72 0 0 1 28.2624-54.4768 320.9216 320.9216 0 0 1 120.6272 108.7488A30.72 30.72 0 0 1 665.6 353.6896z"/>
14
+ </svg>