dsh-notice-center 1.2.0 → 1.3.0
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/README.en.md +211 -0
- package/README.md +67 -16
- package/lib/client.cjs +156 -37
- package/lib/index.mjs +2 -0
- package/package.json +3 -2
package/README.en.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# dsh-notice-center 🔔🐋
|
|
2
|
+
|
|
3
|
+
[中文](./README.md) | **English**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/SCP-QQ/dsh-notice-center/actions/workflows/test.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/dsh-notice-center)
|
|
7
|
+
|
|
8
|
+
A **notification center** for DeepSeek Harness: it turns the tab's whale icon into a status light and gets your attention while you are looking elsewhere — a session finished, or something is waiting for your decision.
|
|
9
|
+
|
|
10
|
+
Package `dsh-notice-center`, plugin instance id `notice-center`, settings namespace `notice-center`.
|
|
11
|
+
|
|
12
|
+
## Screenshots
|
|
13
|
+
|
|
14
|
+
Settings → **Notification center** (a sidebar entry):
|
|
15
|
+
|
|
16
|
+
<img src="docs/images/settings.png" width="440" alt="Notification center settings">
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
### 1. Whale status light (favicon state machine)
|
|
21
|
+
|
|
22
|
+
| Whale colour | Meaning | When it clears |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| 🟢 Green | A session **finished** while you were not watching it | Clears once you open that session; back to the default state once you have opened them all |
|
|
25
|
+
| 🟠 Amber | A session **awaits you**: question / approval / plan review | Clears once handled |
|
|
26
|
+
| ⚫ Black | All clear (the stock icon) | Default state |
|
|
27
|
+
|
|
28
|
+
In the tab (green = a session finished):
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
The colours come from **the same official signal** as the sidebar's green and amber dots, so they never drift apart. Only main sessions count; subagents do not affect it. When both apply, **amber wins** — a pending interaction is the primary status, so a session waiting on you is never masked by another session that just finished.
|
|
33
|
+
|
|
34
|
+
All three colours are configurable, including the default colour — leave it unset to keep the stock icon.
|
|
35
|
+
|
|
36
|
+
### 2. System notifications
|
|
37
|
+
|
|
38
|
+
- **Completion** — fires when a session finishes (the official `completed` goes false → true; the session you have open never flips it, so its run state going true → false counts instead)
|
|
39
|
+
- **Pending** — fires when a new interaction starts waiting for you
|
|
40
|
+
- **Text** — the title is the session name (with `+N` when several merge), the body is the kind of notification (finished → Session finished; pending is **refined per interaction**: approval → *Approval · Bash* (plain *Approval needed* when no tool name is available, truncated past 32 characters), a question → *Choose an option* / *Choose options* / *Type an answer*, or *2 questions* for a batch, plan review → *Plan review*; unknown kinds fall back to a generic line); a **single** finished notification also carries how long the turn took (*Session finished · turn took 2m13s*), formatted like the official in-conversation "turn time" readout and dropped when several merge. The start is observed from `running`, so a turn already in flight when the page reloads has no start and shows no figure
|
|
41
|
+
- **Quiet by default while you are looking** — *in the foreground* means the tab is visible **and** the window has focus; switching tabs, minimising, or leaving the browser behind another app all notify you. To be notified in the foreground too (still sitting in a session but away from the screen), turn on *Notify in the foreground* under *More notification options*
|
|
42
|
+
- **Batching and de-duplication** — events within a 300ms window are merged; the same session and kind only fires once; several sessions merge into *title +N*
|
|
43
|
+
- **Clicking the notification** focuses the window and opens that session
|
|
44
|
+
- **Sound** — finished and pending each have their own picker (**hover to preview**) and a shared volume (adjusting the volume replays the current finished sound at the new level); turning sound on silences the system notification sound and uses the plugin's own
|
|
45
|
+
- **Optional persistence** — turn *Auto hide* off and the notification stays on screen until you dismiss it (auto hide is on by default, so the OS collects it)
|
|
46
|
+
- The notification icon follows your configured status-light colour
|
|
47
|
+
|
|
48
|
+
| Finished (green) | Pending (amber) |
|
|
49
|
+
|---|---|
|
|
50
|
+
|  |  |
|
|
51
|
+
|
|
52
|
+
> Notifications are raised by the browser (Chrome here), so the card shows the browser and the origin `127.0.0.1:3080`.
|
|
53
|
+
|
|
54
|
+
### 3. Sound library
|
|
55
|
+
|
|
56
|
+
Finished and pending each have a picker with **47 entries**: 45 taken from opencode's built-in sound library (the same `Alert` / `Bip-bop` / `Staplebops` / `Nope` / `Yup` set, with the same numbering as opencode's settings) plus two tones the plugin synthesises itself.
|
|
57
|
+
|
|
58
|
+
| Pack | Entries |
|
|
59
|
+
|---|---|
|
|
60
|
+
| Built-in (synthesised) | Chime Up (default for finished, a rising two-tone), Chime Down (default for pending, a falling two-tone) |
|
|
61
|
+
| Alert | Alert 01–10 |
|
|
62
|
+
| Bip-bop | Bip-bop 01–10 |
|
|
63
|
+
| Staplebops | Staplebops 01–07 |
|
|
64
|
+
| Nope | Nope 01–12 |
|
|
65
|
+
| Yup | Yup 01–06 |
|
|
66
|
+
|
|
67
|
+
- **Hover to preview** — moving over an entry plays it (120ms debounce, so sweeping the list does not machine-gun)
|
|
68
|
+
- **Click to apply** — the chosen sound is what that kind of notification plays; the current one is ticked `✓`
|
|
69
|
+
- **Styled like the official control** — the trigger is the official language selector's `.selector` (36px tall, 18px pill radius); the panel is the official `Menu` (`bg-layer-3`, 0.5px hairline, 6px radius), and entries highlight with the official hover fill
|
|
70
|
+
- The host half serves the files at `/notice-center-sounds/<id>.mp3`; if that route is unavailable it **falls back to the built-in synthesised tone**, so you are never left silent
|
|
71
|
+
- No migration needed: without a stored choice it still uses Chime Up / Chime Down
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
1. **Open the settings page** — Settings → **Notification center**
|
|
76
|
+
2. **Turn system notifications on** — flip the master switch; the browser asks for permission, choose Allow (if it was denied, the page says so and you have to re-allow it in the browser's site settings)
|
|
77
|
+
3. **Volume, sounds and foreground alerts** — expand *More notification options*: drag the volume slider (**releasing it replays the finished sound at the new level**); in the finished / pending pickers **hovering an entry previews it**, clicking selects it; turn on *Notify in the foreground* to be alerted while the page is in front
|
|
78
|
+
4. **Colours (optional)** — expand *Tab whale light*: click the swatch or type a `#RRGGBB`, and ↺ at the end of the row restores the default
|
|
79
|
+
5. **That is it** — from then on, sessions finishing or waiting for you notify you while you are away; clicking the notification jumps back to that session
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
`dsh plugin` is a **thin forwarder to pnpm** (it runs pnpm in the profile directory, then adds the new bundle to `dsh.profile.bundles`), so a registry name, a git repository, a tarball or a local path all work.
|
|
84
|
+
|
|
85
|
+
### 1. From npm (recommended)
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
dsh plugin --profile web add dsh-notice-center
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Package page: <https://www.npmjs.com/package/dsh-notice-center> (`latest` is the stable line; pre-releases live on the `next` tag)
|
|
92
|
+
|
|
93
|
+
### 2. Straight from GitHub (no publish needed)
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
dsh plugin --profile web add github:SCP-QQ/dsh-notice-center
|
|
97
|
+
# or over SSH (when a proxy interferes with HTTPS on your machine)
|
|
98
|
+
dsh plugin --profile web add git+ssh://git@github.com/SCP-QQ/dsh-notice-center.git
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 3. From a tarball (offline / internal distribution)
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
npm pack # produces dsh-notice-center-<version>.tgz
|
|
105
|
+
dsh plugin --profile web add ./dsh-notice-center-<version>.tgz
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 4. Local link (while editing the code)
|
|
109
|
+
|
|
110
|
+
Add `"dsh-notice-center": "link:<project dir>/dsh-notice-center"` to the profile's `dependencies`, and `"dsh-notice-center"` to `dsh.profile.bundles`.
|
|
111
|
+
|
|
112
|
+
> The first three need a **Harness restart** afterwards. The host half's only runtime dependency is `@deepseek-ai/schemastery` (which exists on npm), so the profile does not have to provide any `@deepseek-ai/*` package.
|
|
113
|
+
|
|
114
|
+
**Restart rules differ per half**: editing the browser half `lib/client.cjs` needs no restart (HMR stat-polls the bundle and tells the browser to reload it; refreshing the page forces a clean reload); **editing the host half `lib/index.mjs` requires a Harness restart** (host-half hot reload depends on `cordis-plugin-hmr`, which needs the loader to run with `--expose-internals` — measured, it does not take effect). A first install, or any rename, also requires a restart.
|
|
115
|
+
|
|
116
|
+
### 5. A local `.npmrc` for manual publishing (optional)
|
|
117
|
+
|
|
118
|
+
CI publishes over OIDC and needs **no token at all**. Only if you want to run `npm publish` from this machine, drop a `.npmrc` into the project root:
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
//registry.npmjs.org/:_authToken=<your granular token>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
It is already listed in `.gitignore` — **never commit it** (especially not when it holds a real token).
|
|
125
|
+
|
|
126
|
+
## Requirements
|
|
127
|
+
|
|
128
|
+
- A stock DeepSeek Harness (measured here on Harness 0.1.5-rc.1 + client packages 0.1.5-rc.2; supported from 0.1.2-rc.1)
|
|
129
|
+
- System notifications need a **secure context**: `http://127.0.0.1:3080` and `localhost` work; over a **LAN IP** the browser's Notification API is unavailable
|
|
130
|
+
|
|
131
|
+
## Uninstall
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
dsh plugin --profile web remove dsh-notice-center
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Removing the dependency and the `dsh.profile.bundles` entry leaves nothing behind after a restart.
|
|
138
|
+
|
|
139
|
+
## Known limitations
|
|
140
|
+
|
|
141
|
+
- **No notifications once the tab is closed** (Web Push needs a server plus HTTPS, which a local plugin cannot reasonably provide; covering that would need a host-side toast)
|
|
142
|
+
- The Notification API is unavailable in a non-secure context (LAN IP)
|
|
143
|
+
- The green light is lost on page reload: that is the memory-only semantics of the official finished signal, not a bug
|
|
144
|
+
- System-wide do-not-disturb (Windows Focus Assist and friends) swallows notifications and the plugin cannot tell
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
npm test # host-half + client-half smoke tests
|
|
150
|
+
npm run test:host # host half only
|
|
151
|
+
npm run test:client # client half only
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
CI runs the same suite on push and pull requests (`.github/workflows/test.yml`).
|
|
155
|
+
|
|
156
|
+
## Releasing
|
|
157
|
+
|
|
158
|
+
Releases are driven by **tags** through [`.github/workflows/publish.yml`](.github/workflows/publish.yml) — everyday commits and pushes **never publish**:
|
|
159
|
+
|
|
160
|
+
| Intent | Command | Result |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| Commit only (feature not verified yet) | `git commit` + `git push` | `test.yml` runs; npm is untouched |
|
|
163
|
+
| Pre-release (for testers) | set the version to `1.3.0-rc.1` → commit → `git tag v1.3.0-rc.1 && git push origin v1.3.0-rc.1` | published to the **`next`** tag (`npm i dsh-notice-center@next`); `latest` stays put |
|
|
164
|
+
| Real release | set the version to `1.3.0` → commit → `git tag v1.3.0 && git push origin v1.3.0` | published to **`latest`**, with provenance attached |
|
|
165
|
+
| Validate the pipeline only | Actions → publish → Run workflow | install + tests + `npm pack --dry-run`, **no publish** |
|
|
166
|
+
|
|
167
|
+
Three gates are built into the workflow: **the tag must equal the `package.json` version**, **the tagged commit must already be on `main`**, and **the tests must pass**. Any of them failing stops the publish.
|
|
168
|
+
|
|
169
|
+
### One-time setup: npm Trusted Publishing (OIDC)
|
|
170
|
+
|
|
171
|
+
Publishing needs no npm token at all, so there is nothing to leak or rotate.
|
|
172
|
+
|
|
173
|
+
Open the package management page → **Publishing Access** → *Trusted publishers* → **Add a trusted publisher** → **GitHub Actions**:
|
|
174
|
+
|
|
175
|
+
<https://www.npmjs.com/package/dsh-notice-center/access>
|
|
176
|
+
|
|
177
|
+
| Field | Value |
|
|
178
|
+
|---|---|
|
|
179
|
+
| Organization or user | `SCP-QQ` |
|
|
180
|
+
| Repository | `dsh-notice-center` |
|
|
181
|
+
| Workflow filename | `publish.yml` (the **filename only**, no path; renaming the file means updating this, or publishes get a 403) |
|
|
182
|
+
| Environment name | `npm` (must match the `environment:` in the workflow) |
|
|
183
|
+
| **Allowed actions → Allow npm publish** | ✅ **must be checked** |
|
|
184
|
+
|
|
185
|
+
> ⚠️ That checkbox is new npm behaviour: a trusted publisher may only `npm stage publish` unless *Allow npm publish* is ticked, and this workflow's `npm publish` would be rejected.
|
|
186
|
+
> If you would rather have it stage-only and publish by hand in the browser, leave it unchecked and change the workflow's last step to `npm stage publish`.
|
|
187
|
+
|
|
188
|
+
- Publishing over OIDC makes npm attach **provenance automatically** (no `--provenance` flag), and the package page shows it
|
|
189
|
+
- To require a human click even after a tag is pushed, add **Required reviewers** to the `npm` environment under the repository's Settings → Environments; publishing works without it too
|
|
190
|
+
- Manual local publishing can still fall back to a granular token (see the `.npmrc` usage in Installation, step 5)
|
|
191
|
+
|
|
192
|
+
## Layout
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
package.json package metadata (dsh-notice-center)
|
|
196
|
+
cordis.patch.yml bundle patch layer (insert id: notice-center)
|
|
197
|
+
lib/index.mjs host half: settings schema + the /notice-center-sounds route
|
|
198
|
+
lib/client.cjs browser half: favicon state machine + notifications + sounds + settings UI
|
|
199
|
+
assets/audio/*.mp3 45 opencode sound clips (MIT; see assets/audio/README.md)
|
|
200
|
+
docs/images/*.png screenshots used by the READMEs
|
|
201
|
+
test/host-half.smoke.mjs host-half smoke test
|
|
202
|
+
test/client-half.smoke.mjs client-half smoke test (npm test runs both)
|
|
203
|
+
.github/workflows/test.yml CI: pnpm test on push / PR
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT; the full text is in [`LICENSE`](./LICENSE).
|
|
209
|
+
|
|
210
|
+
The sound clips (`assets/audio/*.mp3`, 45 files) come from [anomalyco/opencode](https://github.com/anomalyco/opencode),
|
|
211
|
+
specifically `packages/ui/src/assets/audio/`. Upstream ships them under the MIT licence, and the same licence and attribution are kept here; if upstream changes its licensing, replace or drop this directory (see [`assets/audio/README.md`](./assets/audio/README.md)).
|
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# dsh-notice-center 🔔🐋
|
|
2
2
|
|
|
3
|
+
**中文** | [English](./README.en.md)
|
|
4
|
+
|
|
3
5
|
[](https://github.com/SCP-QQ/dsh-notice-center/actions/workflows/test.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/dsh-notice-center)
|
|
4
7
|
|
|
5
|
-
DeepSeek Harness
|
|
8
|
+
DeepSeek Harness 的**通知中心**:把标签页上的鲸鱼图标变成状态灯,并在你不在看的时候主动提醒你 —— 会话跑完了,或者有事情等你拍板。
|
|
6
9
|
|
|
7
10
|
包名 `dsh-notice-center`、插件实例 id `notice-center`、设置命名空间 `notice-center`。
|
|
8
11
|
|
|
@@ -18,28 +21,28 @@ DeepSeek Harness 的**通知中心**:把标签页鲸鱼图标变成状态灯
|
|
|
18
21
|
|
|
19
22
|
| 鲸鱼颜色 | 含义 | 什么时候恢复 |
|
|
20
23
|
|---|---|---|
|
|
21
|
-
| 🟢 绿色 |
|
|
22
|
-
| 🟠 琥珀色 |
|
|
24
|
+
| 🟢 绿色 | 有会话在你没看它的时候**跑完了** | 该会话点开后自动恢复;全部点开过一遍即回到默认状态 |
|
|
25
|
+
| 🟠 琥珀色 | 有会话**有事等你处理**:提问 / 审批 / 计划审核 | 处理完后自动消失 |
|
|
23
26
|
| ⚫ 黑色 | 一切正常(官方原版图标) | 默认状态 |
|
|
24
27
|
|
|
25
28
|
标签页上的样子(绿 = 有会话跑完了):
|
|
26
29
|
|
|
27
30
|

|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
颜色与侧边栏的绿点、琥珀点**用的是同一份官方信号**,永远同步。只统计主会话,子代理不计入。绿色与琥珀同时存在时**琥珀优先**——待处理是首要状态,有会话卡住等你时不会因为别的会话刚跑完而被掩盖。
|
|
30
33
|
|
|
31
34
|
三种颜色都可以在设置页改(含「默认色」,不配置则沿用官方原版图标)。
|
|
32
35
|
|
|
33
36
|
### 2. 系统通知
|
|
34
37
|
|
|
35
|
-
-
|
|
38
|
+
- **完成通知**:会话跑完时触发(官方 `completed` 由 false 变为 true;你正打开的那个会话官方不置位,由运行状态 true → false 判定)
|
|
36
39
|
- **待处理通知**:出现新的待处理交互时触发
|
|
37
|
-
- **通知文案**:标题=会话名(多条聚合时补 `+N
|
|
38
|
-
-
|
|
39
|
-
- **聚合与去重**:300ms
|
|
40
|
+
- **通知文案**:标题=会话名(多条聚合时补 `+N`),正文=通知类型(完成=「会话已完成」;待处理**按交互类型细化**=审批「待审批 · <工具名>」(拿不到工具名时就是「待审批」,工具名超 32 字截断)、提问按形态分「请你选择 / 请你多选 / 请你填写」或批量「向你提问(N 个)」、计划审核「计划待审核」,认不出的类型回退到「有交互等待处理」);**完成通知单独一条时**附本轮总用时(如「会话已完成 · 本轮总用时 2分13秒」),多条聚合时不附。格式与官方会话气泡的「本轮总用时」一致;起点靠观察 `running` 得到,刷新页面时已经在跑的会话算不出起点,就不显示这个数
|
|
41
|
+
- **默认只在你不看的时候弹**——「前台」= 标签页可见**且**窗口有焦点;切到别的标签页、最小化、被别的应用压在后面时都会提醒。想在前台也提醒(人还停在会话里、却已离开屏幕,例如在刷手机),打开「更多通知设置 → 前台提醒」
|
|
42
|
+
- **聚合与去重**:300ms 内的通知合并,同一会话同一类型只弹一次;多条合并为「标题 +N」
|
|
40
43
|
- **点击通知** = 聚焦窗口并打开对应会话
|
|
41
|
-
- **提示音**:完成 /
|
|
42
|
-
-
|
|
44
|
+
- **提示音**:完成 / 待处理各自可下拉选音效(**悬浮即试听**),音量可调(**调完音量会按新音量试听当前的完成提示音**);开启后改播插件自带音效,系统默认提示音会被静音
|
|
45
|
+
- **常驻可选**:可关闭「自动隐藏」,通知就一直留在屏幕上,直到你点掉它(默认开启自动隐藏,由系统自行收起)
|
|
43
46
|
- 通知图标跟随你配置的状态灯颜色
|
|
44
47
|
|
|
45
48
|
| 完成通知(绿) | 待处理通知(琥珀) |
|
|
@@ -50,7 +53,7 @@ DeepSeek Harness 的**通知中心**:把标签页鲸鱼图标变成状态灯
|
|
|
50
53
|
|
|
51
54
|
### 3. 音效库
|
|
52
55
|
|
|
53
|
-
完成提示音与待处理提示音各有一个下拉,共 **47 个条目**:45
|
|
56
|
+
完成提示音与待处理提示音各有一个下拉,共 **47 个条目**:45 个取自 opencode 内置音效库(就是 opencode 设置里那套 `Alert` / `Bip-bop` / `Staplebops` / `Nope` / `Yup`,同名同序号)+ 2 个插件自带的合成音。
|
|
54
57
|
|
|
55
58
|
| 音效包 | 条目 |
|
|
56
59
|
|---|---|
|
|
@@ -64,14 +67,14 @@ DeepSeek Harness 的**通知中心**:把标签页鲸鱼图标变成状态灯
|
|
|
64
67
|
- **悬浮即试听**:鼠标划过下拉里的条目就播该音效(120ms 防抖,扫过整列不会连成一片)
|
|
65
68
|
- **点选即生效**:选中后该类型通知就播这个音,当前项打 `✓`
|
|
66
69
|
- **样式照官方**:触发按钮=官方语言下拉的 `.selector`(36px 高、18px 圆角胶囊);面板=官方 `Menu`(`bg-layer-3` + 0.5px 描边 + 6px 圆角),条目标高用官方的 hover 填充色
|
|
67
|
-
-
|
|
70
|
+
- 宿主半通过 `/notice-center-sounds/<id>.mp3` 路由提供音效;路由不可用时**自动回退内置合成音**,不会变哑
|
|
68
71
|
- 老配置无需迁移:没配过音效时仍用 Chime Up / Chime Down
|
|
69
72
|
|
|
70
73
|
## 使用
|
|
71
74
|
|
|
72
75
|
1. **打开设置页** —— 设置 → **通知中心**
|
|
73
76
|
2. **开启系统通知** —— 打开「系统通知」总开关,浏览器会弹出授权请求,选「允许」(被拒绝时设置页会提示,需要到浏览器的站点设置里恢复)
|
|
74
|
-
3. **调音量 /
|
|
77
|
+
3. **调音量 / 选音效 / 前台提醒** —— 展开「更多通知设置」:拖音量条,**松手后会按新音量试听完成提示音**;在「完成提示音 / 待处理提示音」下拉里**鼠标划过即可试听**,点一下选中;想让正看着页面时也提醒,打开「前台提醒」
|
|
75
78
|
4. **改颜色(可选)** —— 展开「鲸鱼状态灯」:点色块或直接填 `#RRGGBB`,行尾 ↺ 恢复默认
|
|
76
79
|
5. **就这样** —— 之后你切走或最小化时,会话跑完 / 有事等你都会收到通知;点通知直接跳回那个会话
|
|
77
80
|
|
|
@@ -86,6 +89,8 @@ DeepSeek Harness 的**通知中心**:把标签页鲸鱼图标变成状态灯
|
|
|
86
89
|
dsh plugin --profile web add dsh-notice-center
|
|
87
90
|
```
|
|
88
91
|
|
|
92
|
+
包页:<https://www.npmjs.com/package/dsh-notice-center>(`latest` 为稳定版;预发布版在 `next` 通道)
|
|
93
|
+
|
|
89
94
|
### 2. 直接从 GitHub 装(无需先发布)
|
|
90
95
|
|
|
91
96
|
```sh
|
|
@@ -97,8 +102,8 @@ dsh plugin --profile web add git+ssh://git@github.com/SCP-QQ/dsh-notice-center.g
|
|
|
97
102
|
### 3. 从 tarball 装(离线 / 内网分发)
|
|
98
103
|
|
|
99
104
|
```sh
|
|
100
|
-
npm pack # 生成 dsh-notice-center
|
|
101
|
-
dsh plugin --profile web add ./dsh-notice-center
|
|
105
|
+
npm pack # 生成 dsh-notice-center-<版本>.tgz
|
|
106
|
+
dsh plugin --profile web add ./dsh-notice-center-<版本>.tgz
|
|
102
107
|
```
|
|
103
108
|
|
|
104
109
|
### 4. 本地 link(改代码时用)
|
|
@@ -112,6 +117,16 @@ profile 的 `dependencies` 加
|
|
|
112
117
|
|
|
113
118
|
**重启要求分两半**:改浏览器半 `lib/client.cjs` 免重启(HMR 会 stat-poll 并通知浏览器重载该插件 bundle,刷新页面可确保干净重载);**改宿主半 `lib/index.mjs` 必须重启 Harness**(宿主半热重载依赖 `cordis-plugin-hmr`,需 loader 带 `--expose-internals`,实测未生效)。首次安装与任何改名也必须重启。
|
|
114
119
|
|
|
120
|
+
### 5. 本机手动发布用的 `.npmrc`(可选)
|
|
121
|
+
|
|
122
|
+
CI 走 OIDC 发布,**不需要任何 token**。只有当你打算从本机手动 `npm publish` 时,才在项目根目录放一个 `.npmrc`:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
//registry.npmjs.org/:_authToken=<你的 granular token>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
该文件已在 `.gitignore` 里,**绝不要提交**(仓库里有真实 token 时尤其注意)。
|
|
129
|
+
|
|
115
130
|
## 要求
|
|
116
131
|
|
|
117
132
|
- 官方版本 DeepSeek Harness(本机实测:Harness 0.1.5-rc.1 + 客户端包 0.1.5-rc.2,适配范围 0.1.2-rc.1+)
|
|
@@ -129,7 +144,7 @@ dsh plugin --profile web remove dsh-notice-center
|
|
|
129
144
|
|
|
130
145
|
- 浏览器**标签页关闭后收不到通知**(Web Push 需要服务端 + HTTPS,本地插件场景不现实;要覆盖需宿主 Toast 方案)
|
|
131
146
|
- 非安全上下文(局域网 IP)下 Notification API 不可用
|
|
132
|
-
-
|
|
147
|
+
- 刷新页面后绿色状态丢失:该状态只存在内存中,刷新即丢,属官方信号本身的行为
|
|
133
148
|
- 系统级勿扰模式(Windows 专注助手等)会吞掉通知,插件无法感知
|
|
134
149
|
|
|
135
150
|
## 开发
|
|
@@ -142,6 +157,42 @@ npm run test:client # 只跑浏览器半
|
|
|
142
157
|
|
|
143
158
|
CI 在 push / PR 时跑同一套(`.github/workflows/test.yml`)。
|
|
144
159
|
|
|
160
|
+
## 发布
|
|
161
|
+
|
|
162
|
+
发布由 **tag 触发**的 [`.github/workflows/publish.yml`](.github/workflows/publish.yml) 负责 —— 日常 commit / push **不会发包**:
|
|
163
|
+
|
|
164
|
+
| 意图 | 命令 | 结果 |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| 只提交(功能还没验证完) | `git commit` + `git push` | 只跑 `test.yml`,npm 上毫无动静 |
|
|
167
|
+
| 预发布(给人试用) | 版本改 `1.3.0-rc.1` → commit → `git tag v1.3.0-rc.1 && git push origin v1.3.0-rc.1` | 发到 **`next`** 通道(`npm i dsh-notice-center@next`),`latest` 不变 |
|
|
168
|
+
| 正式发版 | 版本改 `1.3.0` → commit → `git tag v1.3.0 && git push origin v1.3.0` | 发到 **`latest`**,自动附带 provenance |
|
|
169
|
+
| 只验证发布流程 | Actions → publish → Run workflow | 装依赖 + 跑测试 + `npm pack --dry-run`,**不发布** |
|
|
170
|
+
|
|
171
|
+
工作流内建三道闸:**tag 与 `package.json` 版本必须一致**、**tag 指向的提交必须已在 `main` 上**、**测试必须通过**。任一不过就发不出去。
|
|
172
|
+
|
|
173
|
+
### 一次性配置:npm Trusted Publishing (OIDC)
|
|
174
|
+
|
|
175
|
+
发布不需要任何 npm 令牌(也就没有令牌过期 / 泄漏的问题)。
|
|
176
|
+
|
|
177
|
+
打开包管理页 → **Publishing Access** → *Trusted publishers* → **Add a trusted publisher** → 选 **GitHub Actions**:
|
|
178
|
+
|
|
179
|
+
<https://www.npmjs.com/package/dsh-notice-center/access>
|
|
180
|
+
|
|
181
|
+
| 字段 | 值 |
|
|
182
|
+
|---|---|
|
|
183
|
+
| Organization or user | `SCP-QQ` |
|
|
184
|
+
| Repository | `dsh-notice-center` |
|
|
185
|
+
| Workflow filename | `publish.yml`(**只填文件名**,不带路径;重命名该文件必须同步改这里,否则 403) |
|
|
186
|
+
| Environment name | `npm`(与工作流里的 `environment:` 一致) |
|
|
187
|
+
| **Allowed actions → Allow npm publish** | ✅ **必须勾** |
|
|
188
|
+
|
|
189
|
+
> ⚠️ 那个勾选框是 npm 新版行为:trusted publisher **默认只允许 `npm stage publish`(暂存)**,不勾 *Allow npm publish* 的话,本工作流里的 `npm publish` 会被拒。
|
|
190
|
+
> 若你更想要「只能暂存、必须人工在浏览器批准才真发布」,就别勾,并把工作流末步改成 `npm stage publish`。
|
|
191
|
+
|
|
192
|
+
- 走 OIDC 发布时 npm **自动附带 provenance**(无需 `--provenance`),包页会显示来源证明
|
|
193
|
+
- 想要「tag 推上去还要人工点一下才发」:仓库 Settings → Environments → `npm` → 加 **Required reviewers**;不加也能正常发布
|
|
194
|
+
- 本机手动发布仍可用 granular token 兜底(见上文「安装」第 5 节的 `.npmrc` 用法)
|
|
195
|
+
|
|
145
196
|
## 目录结构
|
|
146
197
|
|
|
147
198
|
```
|
package/lib/client.cjs
CHANGED
|
@@ -29,6 +29,7 @@ const NOTIFY_DEFAULTS = {
|
|
|
29
29
|
notifySound: true,
|
|
30
30
|
notifyVolume: 0.6,
|
|
31
31
|
notifyAutoHide: true,
|
|
32
|
+
notifyForeground: false,
|
|
32
33
|
notifyDoneSound: "builtin-up",
|
|
33
34
|
notifyPendingSound: "builtin-down"
|
|
34
35
|
};
|
|
@@ -441,6 +442,24 @@ function playSound(id, volume, kind) {
|
|
|
441
442
|
const notifySound = value.notifySound ?? NOTIFY_DEFAULTS.notifySound;
|
|
442
443
|
const notifyVolume = normalizeVolume(value.notifyVolume ?? NOTIFY_DEFAULTS.notifyVolume);
|
|
443
444
|
const notifyAutoHide = value.notifyAutoHide ?? NOTIFY_DEFAULTS.notifyAutoHide;
|
|
445
|
+
const notifyForeground = value.notifyForeground ?? NOTIFY_DEFAULTS.notifyForeground;
|
|
446
|
+
/** 音量试听的防抖计时器(拖动过程中不连响,停手后放一遍)。 */
|
|
447
|
+
const volumeTimer = (0, react.useRef)(void 0);
|
|
448
|
+
(0, react.useEffect)(() => () => {
|
|
449
|
+
if (volumeTimer.current !== void 0) clearTimeout(volumeTimer.current);
|
|
450
|
+
}, []);
|
|
451
|
+
/** 改音量后试听当前设置的完成提示音:250ms 防抖,只在停手后按新音量放一次。
|
|
452
|
+
* primeChime 必须在 onChange 这个用户手势里调,否则防抖后 resume 可能被自动播放策略拦。 */
|
|
453
|
+
const previewVolume = (next) => {
|
|
454
|
+
scope.set("notifyVolume", next);
|
|
455
|
+
primeChime();
|
|
456
|
+
if (volumeTimer.current !== void 0) clearTimeout(volumeTimer.current);
|
|
457
|
+
volumeTimer.current = setTimeout(() => {
|
|
458
|
+
volumeTimer.current = void 0;
|
|
459
|
+
const snapshot = scope.getSnapshot().value ?? {};
|
|
460
|
+
playSound(snapshot.notifyDoneSound ?? NOTIFY_DEFAULTS.notifyDoneSound, next, "done");
|
|
461
|
+
}, 250);
|
|
462
|
+
};
|
|
444
463
|
/** 官方设置行规格(对齐「通用设置 → 权限」字段的 row/title/desc 样式)。 */
|
|
445
464
|
const rowStyle = {
|
|
446
465
|
display: "flex",
|
|
@@ -484,9 +503,16 @@ function playSound(id, volume, kind) {
|
|
|
484
503
|
color: "var(--dsw-alias-label-secondary)",
|
|
485
504
|
cursor: "pointer"
|
|
486
505
|
};
|
|
487
|
-
/** 分组条目:主标题 + 描述 + 官方 Switch + 折叠展开箭头(子项由展开态控制显示)。
|
|
506
|
+
/** 分组条目:主标题 + 描述 + 官方 Switch + 折叠展开箭头(子项由展开态控制显示)。
|
|
507
|
+
* 整行可点:鼠标落在行内任意处都能折叠/展开,不必去够最右边的箭头。
|
|
508
|
+
* 开关与箭头各自拦住冒泡 —— 否则点开关会连带折叠,点箭头会折叠两次(等于没反应)。
|
|
509
|
+
* 箭头仍是带 aria-expanded 的真按钮,键盘可达;整行点击只是给鼠标加热区。 */
|
|
488
510
|
const sectionRow = (key, title, hint, checked, onChange, expanded, onToggle, expandLabel) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
489
|
-
style:
|
|
511
|
+
style: {
|
|
512
|
+
...rowStyle,
|
|
513
|
+
cursor: "pointer"
|
|
514
|
+
},
|
|
515
|
+
onClick: onToggle,
|
|
490
516
|
children: [
|
|
491
517
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
492
518
|
style: rowTextStyle,
|
|
@@ -501,17 +527,27 @@ function playSound(id, volume, kind) {
|
|
|
501
527
|
})
|
|
502
528
|
]
|
|
503
529
|
}),
|
|
504
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
530
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
531
|
+
style: {
|
|
532
|
+
flex: "0 0 auto",
|
|
533
|
+
display: "inline-flex"
|
|
534
|
+
},
|
|
535
|
+
onClick: (event) => event?.stopPropagation?.(),
|
|
536
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(primitives.Switch, {
|
|
537
|
+
checked,
|
|
538
|
+
label: title,
|
|
539
|
+
onChange: (next) => onChange(typeof next === "boolean" ? next : !checked)
|
|
540
|
+
})
|
|
508
541
|
}),
|
|
509
542
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
510
543
|
type: "button",
|
|
511
544
|
"aria-expanded": expanded,
|
|
512
545
|
"aria-label": expandLabel,
|
|
513
546
|
title: expandLabel,
|
|
514
|
-
onClick:
|
|
547
|
+
onClick: (event) => {
|
|
548
|
+
event?.stopPropagation?.();
|
|
549
|
+
onToggle();
|
|
550
|
+
},
|
|
515
551
|
style: iconButtonStyle,
|
|
516
552
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
517
553
|
style: {
|
|
@@ -572,7 +608,7 @@ function playSound(id, volume, kind) {
|
|
|
572
608
|
step: 5,
|
|
573
609
|
value: Math.round(notifyVolume * 100),
|
|
574
610
|
"aria-label": t("notifyVolume"),
|
|
575
|
-
onChange: (e) =>
|
|
611
|
+
onChange: (e) => previewVolume(Number(e.target.value) / 100),
|
|
576
612
|
style: {
|
|
577
613
|
flex: "0 0 auto",
|
|
578
614
|
width: 140,
|
|
@@ -716,6 +752,7 @@ function playSound(id, volume, kind) {
|
|
|
716
752
|
colorsExpanded ? colorRow("black", t("blackLabel"), () => scope.unset("black")) : null,
|
|
717
753
|
sectionRow("notifyEnabled", t("groupNotify"), t("notifyEnabledHint"), notifyEnabled, setNotifyEnabled, notifyExpanded, () => setNotifyExpanded((current) => !current), t("notifyExpand")),
|
|
718
754
|
notifyExpanded ? switchRow("notifyAutoHide", t("notifyAutoHide"), t("notifyAutoHideHint"), notifyAutoHide, (next) => scope.set("notifyAutoHide", next)) : null,
|
|
755
|
+
notifyExpanded ? switchRow("notifyForeground", t("notifyForeground"), t("notifyForegroundHint"), notifyForeground, (next) => scope.set("notifyForeground", next)) : null,
|
|
719
756
|
notifyExpanded ? switchRow("notifySound", t("notifySound"), void 0, notifySound, (next) => scope.set("notifySound", next)) : null,
|
|
720
757
|
notifyExpanded ? volumeRow() : null,
|
|
721
758
|
notifyExpanded ? soundRow("done", t("notifyDoneSound")) : null,
|
|
@@ -747,6 +784,22 @@ function playSound(id, volume, kind) {
|
|
|
747
784
|
"locale",
|
|
748
785
|
"settingsScope"
|
|
749
786
|
];
|
|
787
|
+
/**
|
|
788
|
+
* 本轮用时文案(格式照官方 formatRunDuration:满 60 秒进位到分,秒补零)。
|
|
789
|
+
* 官方在会话气泡的「本轮用时和速度」里用的是同一套模板,两处读起来一致。
|
|
790
|
+
* @param ms - 毫秒;负数按 0 处理。
|
|
791
|
+
* @param t - 命名空间文案函数。
|
|
792
|
+
* @returns 形如「13秒」/「2分05秒」。
|
|
793
|
+
*/
|
|
794
|
+
function formatRunDuration(ms, t) {
|
|
795
|
+
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
796
|
+
const minutes = Math.floor(total / 60);
|
|
797
|
+
const seconds = total % 60;
|
|
798
|
+
return minutes > 0 ? t("durationMinutes", {
|
|
799
|
+
minutes,
|
|
800
|
+
seconds: String(seconds).padStart(2, "0")
|
|
801
|
+
}) : t("durationSeconds", { seconds });
|
|
802
|
+
}
|
|
750
803
|
function apply(ctx) {
|
|
751
804
|
const list = ctx.sessions.list;
|
|
752
805
|
/** 绑定 `notice-center` 命名空间:读快照 + 写用户配置(宿主 schema 校验)。 */
|
|
@@ -766,6 +819,11 @@ function playSound(id, volume, kind) {
|
|
|
766
819
|
let notifyTimer;
|
|
767
820
|
/** 已通知过的会话完成态,用于识别 false → true 跃迁。 */
|
|
768
821
|
const prevCompleted = new Map();
|
|
822
|
+
/** 本轮运行起点(running false→true 时记下)。首次观察时已在跑的会话不记 ——
|
|
823
|
+
* 那样只能算出「我们看了多久」而不是本轮用时,宁可不出这个数。 */
|
|
824
|
+
const runStartedAt = new Map();
|
|
825
|
+
/** 每会话最近一次运行的用时(毫秒):completed 跃迁那条路径晚于 running 边沿,需要它。 */
|
|
826
|
+
const lastRunMs = new Map();
|
|
769
827
|
/** 上一轮待处理会话集合与首帧标记(首帧不通知,避免刷新页面时轰炸)。 */
|
|
770
828
|
let prevPending = new Set();
|
|
771
829
|
let pendingSeen = false;
|
|
@@ -783,6 +841,7 @@ function playSound(id, volume, kind) {
|
|
|
783
841
|
sound: snapshot.notifySound ?? NOTIFY_DEFAULTS.notifySound,
|
|
784
842
|
volume: normalizeVolume(snapshot.notifyVolume ?? NOTIFY_DEFAULTS.notifyVolume),
|
|
785
843
|
autoHide: snapshot.notifyAutoHide ?? NOTIFY_DEFAULTS.notifyAutoHide,
|
|
844
|
+
foreground: snapshot.notifyForeground ?? NOTIFY_DEFAULTS.notifyForeground,
|
|
786
845
|
doneSound: snapshot.notifyDoneSound ?? NOTIFY_DEFAULTS.notifyDoneSound,
|
|
787
846
|
pendingSound: snapshot.notifyPendingSound ?? NOTIFY_DEFAULTS.notifyPendingSound
|
|
788
847
|
};
|
|
@@ -793,8 +852,9 @@ function playSound(id, volume, kind) {
|
|
|
793
852
|
* @param sessionId - 会话 id。
|
|
794
853
|
* @param label - 会话显示名;聚合后作为通知**标题**。
|
|
795
854
|
* @param typeLabel - 通知**正文**(仅 pending 用):交互类型文案;缺省回退到通用标题。
|
|
855
|
+
* @param durationMs - 本轮用时(仅 done 用);算不出起点时传 undefined,正文就不带这个数。
|
|
796
856
|
*/
|
|
797
|
-
function queueNotification(kind, sessionId, label, typeLabel) {
|
|
857
|
+
function queueNotification(kind, sessionId, label, typeLabel, durationMs) {
|
|
798
858
|
if (!notifyConfig().enabled) return;
|
|
799
859
|
const key = sessionId + ":" + kind;
|
|
800
860
|
if (notified.has(key)) return;
|
|
@@ -803,7 +863,8 @@ function playSound(id, volume, kind) {
|
|
|
803
863
|
kind,
|
|
804
864
|
sessionId,
|
|
805
865
|
label,
|
|
806
|
-
typeLabel
|
|
866
|
+
typeLabel,
|
|
867
|
+
durationMs
|
|
807
868
|
});
|
|
808
869
|
if (notifyTimer === void 0) notifyTimer = setTimeout(flushNotifications, 300);
|
|
809
870
|
}
|
|
@@ -815,10 +876,10 @@ function playSound(id, volume, kind) {
|
|
|
815
876
|
if (entries.length === 0) return;
|
|
816
877
|
if (notificationSupport() !== "granted") return;
|
|
817
878
|
const config = notifyConfig();
|
|
818
|
-
/*
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
if (isForeground()) return;
|
|
879
|
+
/* 前台策略:「前台」= 标签页可见 且 窗口有焦点(见 isForeground)。默认只在不在
|
|
880
|
+
前台时打扰;开启「前台也提醒」(notifyForeground) 后正在看也弹 —— 用于人还停在
|
|
881
|
+
当前会话、却已离开屏幕(刷手机等)的场景。 */
|
|
882
|
+
if (!config.foreground && isForeground()) return;
|
|
822
883
|
const t = ctx.locale.bind(SETTINGS_NAMESPACE);
|
|
823
884
|
const grouped = new Map();
|
|
824
885
|
for (const entry of entries) {
|
|
@@ -832,7 +893,9 @@ function playSound(id, volume, kind) {
|
|
|
832
893
|
/* 标题=会话名(聚合时补「 +N」);正文=通知类型。 */
|
|
833
894
|
let title = head.label ?? head.sessionId;
|
|
834
895
|
if (extra > 0) title = title + " +" + String(extra);
|
|
835
|
-
|
|
896
|
+
let type = kind === "done" ? t("notifyDoneTitle") : head.typeLabel ?? t("notifyPendingTitle");
|
|
897
|
+
/* 用时只在「单独一条完成通知」时附加:聚合时一条正文挂 N 个用时无从表达。 */
|
|
898
|
+
if (kind === "done" && extra === 0 && head.durationMs !== void 0) type = type + " · " + t("notifyDuration", { duration: formatRunDuration(head.durationMs, t) });
|
|
836
899
|
try {
|
|
837
900
|
const notification = new Notification(title, {
|
|
838
901
|
body: type,
|
|
@@ -864,15 +927,39 @@ function playSound(id, volume, kind) {
|
|
|
864
927
|
question: "pendingKindQuestion",
|
|
865
928
|
"plan-review": "pendingKindPlanReview"
|
|
866
929
|
};
|
|
930
|
+
/** 工具名上限:超长会撑破通知正文(Windows 通知一两行就截断),超出截断加省略号。 */
|
|
931
|
+
const TOOL_NAME_LIMIT = 32;
|
|
867
932
|
/**
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
*
|
|
871
|
-
*
|
|
933
|
+
* 待处理交互 → 通知正文文案。
|
|
934
|
+
*
|
|
935
|
+
* kind 只到「审批 / 提问 / 计划待审核」这一层(官方 SessionPendingInteractionStatus
|
|
936
|
+
* 就这三种),更细的信息在 entry 的 domain 字段上:审批的 toolName、提问的 questions
|
|
937
|
+
* 批次。这里读这些字段做细化,**全部防御式读取** —— bundle 不 import 那些类型,
|
|
938
|
+
* 字段缺席、类型不符(第三方域自造 kind)时逐级降级,绝不显示错误的类型名。
|
|
939
|
+
* @param interaction - pendingInteractions 的 entry;缺席时为 undefined。
|
|
940
|
+
* @returns 正文文案;完全认不出时 undefined,调用方回退「有交互等待处理」。
|
|
872
941
|
*/
|
|
873
|
-
function pendingTypeLabel(
|
|
874
|
-
const
|
|
875
|
-
|
|
942
|
+
function pendingTypeLabel(interaction) {
|
|
943
|
+
const t = ctx.locale.bind(SETTINGS_NAMESPACE);
|
|
944
|
+
const kind = interaction?.kind;
|
|
945
|
+
if (kind === "approval") {
|
|
946
|
+
const tool = interaction.toolName;
|
|
947
|
+
if (typeof tool !== "string" || tool === "") return t("pendingKindApproval");
|
|
948
|
+
const shown = tool.length > TOOL_NAME_LIMIT ? tool.slice(0, TOOL_NAME_LIMIT) + "…" : tool;
|
|
949
|
+
return t("pendingApprovalTool", { tool: shown });
|
|
950
|
+
}
|
|
951
|
+
if (kind === "question") {
|
|
952
|
+
const questions = Array.isArray(interaction.questions) ? interaction.questions : [];
|
|
953
|
+
if (questions.length > 1) return t("pendingQuestionBatch", { count: questions.length });
|
|
954
|
+
const first = questions[0];
|
|
955
|
+
/* 只有一条也要能读出「要不要动键盘」:有选项=选择,无选项=自己写。 */
|
|
956
|
+
if (first === null || typeof first !== "object") return t("pendingKindQuestion");
|
|
957
|
+
const options = Array.isArray(first.options) ? first.options : [];
|
|
958
|
+
if (options.length === 0) return t("pendingQuestionFill");
|
|
959
|
+
return first.multiSelect === true ? t("pendingQuestionMulti") : t("pendingQuestionChoose");
|
|
960
|
+
}
|
|
961
|
+
const key = PENDING_KIND_KEYS[kind];
|
|
962
|
+
return key === void 0 ? void 0 : t(key);
|
|
876
963
|
}
|
|
877
964
|
/** 状态跃迁检测:官方 completed 由 false 变 true、以及 pending store 新增会话。 */
|
|
878
965
|
function detectTransitions(state) {
|
|
@@ -880,7 +967,7 @@ function playSound(id, volume, kind) {
|
|
|
880
967
|
if (row.origin === "subagent") continue;
|
|
881
968
|
const before = prevCompleted.get(row.id);
|
|
882
969
|
const now = row.completed === true;
|
|
883
|
-
if (before === false && now) queueNotification("done", row.id, row.displayTitle ?? row.title ?? row.id);
|
|
970
|
+
if (before === false && now) queueNotification("done", row.id, row.displayTitle ?? row.title ?? row.id, void 0, lastRunMs.get(row.id));
|
|
884
971
|
if (!now) notified.delete(row.id + ":done");
|
|
885
972
|
prevCompleted.set(row.id, now);
|
|
886
973
|
}
|
|
@@ -895,7 +982,7 @@ function playSound(id, volume, kind) {
|
|
|
895
982
|
const row = state.byId[id];
|
|
896
983
|
if (row !== void 0 && row.origin === "subagent") continue;
|
|
897
984
|
const label = row?.displayTitle ?? row?.title ?? id;
|
|
898
|
-
queueNotification("pending", id, label, pendingTypeLabel(pendingMap?.get(id)
|
|
985
|
+
queueNotification("pending", id, label, pendingTypeLabel(pendingMap?.get(id)));
|
|
899
986
|
}
|
|
900
987
|
for (const id of prevPending) if (!current.has(id)) notified.delete(id + ":pending");
|
|
901
988
|
prevPending = current;
|
|
@@ -980,10 +1067,11 @@ function playSound(id, volume, kind) {
|
|
|
980
1067
|
};
|
|
981
1068
|
}
|
|
982
1069
|
const uri = (hex) => `data:image/svg+xml,${encodeURIComponent(whaleSvg(hex))}`;
|
|
983
|
-
/** running true→false
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
*
|
|
1070
|
+
/** running true→false 边沿跟踪(只跟踪主会话):完成时恰好被选中 →
|
|
1071
|
+
* 绿灯记入 finishedWhileHidden(仅当标签页不在台前;台前完成不记,V0-02);
|
|
1072
|
+
* 通知则无条件入队,由 flush 按「前台也提醒」统一裁决。官方 syncCompletedNotifications
|
|
1073
|
+
* 只报"未选中时完成"(`sessionId !== selected` 才 arm),选中的空缺在这里补齐。
|
|
1074
|
+
* 重新运行、会话移除均清除。 */
|
|
987
1075
|
function trackEdges(state) {
|
|
988
1076
|
for (const row of Object.values(state.byId)) {
|
|
989
1077
|
if (row.origin === "subagent") continue;
|
|
@@ -992,17 +1080,25 @@ function playSound(id, volume, kind) {
|
|
|
992
1080
|
prevRunning.set(row.id, row.running);
|
|
993
1081
|
continue;
|
|
994
1082
|
}
|
|
1083
|
+
if (!prev && row.running) runStartedAt.set(row.id, Date.now());
|
|
995
1084
|
if (prev && !row.running) {
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1085
|
+
const startedAt = runStartedAt.get(row.id);
|
|
1086
|
+
const elapsed = startedAt === void 0 ? void 0 : Date.now() - startedAt;
|
|
1087
|
+
runStartedAt.delete(row.id);
|
|
1088
|
+
if (elapsed !== void 0) lastRunMs.set(row.id, elapsed);
|
|
1089
|
+
/* 绿灯只记「没看着时跑完」;台前完成不记(V0-02)。 */
|
|
1090
|
+
if (row.id === state.current && !isForeground()) finishedWhileHidden.add(row.id);
|
|
1091
|
+
/* 通知另算:选中会话完成时官方不置 completed,这条是它的唯一来源,
|
|
1092
|
+
因此「前台要不要打扰」交给 flush 按 notifyForeground 统一裁决。 */
|
|
1093
|
+
if (row.id === state.current) queueNotification("done", row.id, row.displayTitle ?? row.title ?? row.id, void 0, elapsed);
|
|
1000
1094
|
} else if (row.running) finishedWhileHidden.delete(row.id);
|
|
1001
1095
|
prevRunning.set(row.id, row.running);
|
|
1002
1096
|
}
|
|
1003
1097
|
for (const id of [...prevRunning.keys()]) if (!(id in state.byId)) {
|
|
1004
1098
|
prevRunning.delete(id);
|
|
1005
1099
|
finishedWhileHidden.delete(id);
|
|
1100
|
+
runStartedAt.delete(id);
|
|
1101
|
+
lastRunMs.delete(id);
|
|
1006
1102
|
}
|
|
1007
1103
|
}
|
|
1008
1104
|
/** 回到前台(标签页可见且窗口有焦点)→ 选中会话的绿灯熄灭(V0-02)。 */
|
|
@@ -1015,18 +1111,21 @@ function playSound(id, volume, kind) {
|
|
|
1015
1111
|
};
|
|
1016
1112
|
document.addEventListener("visibilitychange", onForeground);
|
|
1017
1113
|
window.addEventListener("focus", onForeground);
|
|
1018
|
-
/** 绿/琥珀判定:主会话 only
|
|
1114
|
+
/** 绿/琥珀判定:主会话 only;**琥珀优先于绿**。返回目标 href;null = 官方原版。 */
|
|
1019
1115
|
function targetOf(state) {
|
|
1020
1116
|
if ((scope.getSnapshot().value ?? {}).colorsEnabled === false) return null;
|
|
1021
1117
|
const c = colors();
|
|
1022
1118
|
const pendingMap = pendingStore?.getSnapshot();
|
|
1023
|
-
let
|
|
1119
|
+
let green = false;
|
|
1120
|
+
/* 顺序照官方 sessionStatuses:pending interaction 是首要状态,live activity
|
|
1121
|
+
压过 completion reminder。favicon 是跨会话聚合,套用同一原则 —— 任意会话
|
|
1122
|
+
「卡住等你」都压过任意会话「跑完了」,否则审批会被完成提醒整片掩盖。 */
|
|
1024
1123
|
for (const row of Object.values(state.byId)) {
|
|
1025
1124
|
if (row.origin === "subagent") continue;
|
|
1026
|
-
if (row.
|
|
1027
|
-
if (
|
|
1125
|
+
if (pendingMap?.has(row.id) || row.pendingInteraction !== void 0) return uri(c.amber);
|
|
1126
|
+
if (row.completed === true || finishedWhileHidden.has(row.id)) green = true;
|
|
1028
1127
|
}
|
|
1029
|
-
if (
|
|
1128
|
+
if (green) return uri(c.green);
|
|
1030
1129
|
return c.black ? uri(c.black) : null;
|
|
1031
1130
|
}
|
|
1032
1131
|
function sync() {
|
|
@@ -1077,6 +1176,8 @@ trackEdges(state);
|
|
|
1077
1176
|
notifySound: "提示音",
|
|
1078
1177
|
notifyAutoHide: "自动隐藏",
|
|
1079
1178
|
notifyAutoHideHint: "关闭后通知常驻,需手动关闭",
|
|
1179
|
+
notifyForeground: "前台提醒",
|
|
1180
|
+
notifyForegroundHint: "开启后,即使你正看着该页面也会提醒",
|
|
1080
1181
|
notifyVolume: "音量",
|
|
1081
1182
|
notifyDoneSound: "完成提示音",
|
|
1082
1183
|
notifyPendingSound: "待处理提示音",
|
|
@@ -1088,9 +1189,17 @@ trackEdges(state);
|
|
|
1088
1189
|
permissionUnsupported: "当前浏览器不支持系统通知",
|
|
1089
1190
|
notifyDoneTitle: "会话已完成",
|
|
1090
1191
|
notifyPendingTitle: "有交互等待处理",
|
|
1192
|
+
notifyDuration: "本轮总用时 {duration}",
|
|
1193
|
+
durationSeconds: "{seconds}秒",
|
|
1194
|
+
durationMinutes: "{minutes}分{seconds}秒",
|
|
1091
1195
|
pendingKindApproval: "待审批",
|
|
1092
1196
|
pendingKindQuestion: "向你提问",
|
|
1093
1197
|
pendingKindPlanReview: "计划待审核",
|
|
1198
|
+
pendingApprovalTool: "待审批 · {tool}",
|
|
1199
|
+
pendingQuestionChoose: "请你选择",
|
|
1200
|
+
pendingQuestionMulti: "请你多选",
|
|
1201
|
+
pendingQuestionFill: "请你填写",
|
|
1202
|
+
pendingQuestionBatch: "向你提问({count} 个)",
|
|
1094
1203
|
},
|
|
1095
1204
|
en: {
|
|
1096
1205
|
nav: "Notification center",
|
|
@@ -1110,6 +1219,8 @@ trackEdges(state);
|
|
|
1110
1219
|
notifySound: "Sound",
|
|
1111
1220
|
notifyAutoHide: "Auto hide",
|
|
1112
1221
|
notifyAutoHideHint: "Off keeps it on screen until you close it",
|
|
1222
|
+
notifyForeground: "Notify in the foreground",
|
|
1223
|
+
notifyForegroundHint: "Also notify while you are looking at the page",
|
|
1113
1224
|
notifyVolume: "Volume",
|
|
1114
1225
|
notifyDoneSound: "Done sound",
|
|
1115
1226
|
notifyPendingSound: "Pending sound",
|
|
@@ -1121,9 +1232,17 @@ trackEdges(state);
|
|
|
1121
1232
|
permissionUnsupported: "This browser does not support notifications",
|
|
1122
1233
|
notifyDoneTitle: "Session finished",
|
|
1123
1234
|
notifyPendingTitle: "Something awaits you",
|
|
1235
|
+
notifyDuration: "turn took {duration}",
|
|
1236
|
+
durationSeconds: "{seconds}s",
|
|
1237
|
+
durationMinutes: "{minutes}m{seconds}s",
|
|
1124
1238
|
pendingKindApproval: "Approval needed",
|
|
1125
1239
|
pendingKindQuestion: "Question",
|
|
1126
1240
|
pendingKindPlanReview: "Plan review",
|
|
1241
|
+
pendingApprovalTool: "Approval · {tool}",
|
|
1242
|
+
pendingQuestionChoose: "Choose an option",
|
|
1243
|
+
pendingQuestionMulti: "Choose options",
|
|
1244
|
+
pendingQuestionFill: "Type an answer",
|
|
1245
|
+
pendingQuestionBatch: "{count} questions",
|
|
1127
1246
|
}
|
|
1128
1247
|
}));
|
|
1129
1248
|
ctx.slots.inject("settings.section", () => {
|
package/lib/index.mjs
CHANGED
|
@@ -48,6 +48,8 @@ const NoticeSettingsSchema = z.object({
|
|
|
48
48
|
notifyEnabled: z.boolean().default(false),
|
|
49
49
|
/** 通知是否自动隐藏(默认 true=系统自行收起;false=常驻,需用户手动关闭)。 */
|
|
50
50
|
notifyAutoHide: z.boolean().default(true),
|
|
51
|
+
/** 前台也提醒(默认 false=沿用原固定策略,页面在前台时保持安静;true=正在看也弹)。 */
|
|
52
|
+
notifyForeground: z.boolean().default(false),
|
|
51
53
|
/** 通知提示音:完成与待处理为两种不同的预置音。 */
|
|
52
54
|
notifySound: z.boolean().default(true),
|
|
53
55
|
/** 提示音音量(0–1)。 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-notice-center",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "DeepSeek Harness 通知中心:标签页鲸鱼状态灯(绿=跑完 / 琥珀=待处理)+ 浏览器系统通知(完成/待处理提示、45 个 opencode 音效可选、音量可配)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"cordis.patch.yml",
|
|
27
27
|
"assets",
|
|
28
28
|
"LICENSE",
|
|
29
|
-
"README.md"
|
|
29
|
+
"README.md",
|
|
30
|
+
"README.en.md"
|
|
30
31
|
],
|
|
31
32
|
"dependencies": {
|
|
32
33
|
"@deepseek-ai/schemastery": "^3.18.2"
|