dsh-notice-center 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +271 -104
- package/README.md +260 -99
- package/lib/client.cjs +70 -2
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -5,172 +5,337 @@
|
|
|
5
5
|
[](https://github.com/SCP-QQ/dsh-notice-center/actions/workflows/test.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/dsh-notice-center)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
<img src="docs/images/banner-en.jpg" alt="dsh-notice-center — Get Every Message, Never Miss a Thing" width="100%">
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
**In one line**: 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.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
| | |
|
|
13
|
+
|---|---|
|
|
14
|
+
| Package | `dsh-notice-center` |
|
|
15
|
+
| Plugin instance id | `notice-center` |
|
|
16
|
+
| Settings namespace | `notice-center` |
|
|
17
|
+
| Install shapes | npm package / GitHub repo / tarball / local link — all four supported |
|
|
18
|
+
|
|
19
|
+
## Contents
|
|
20
|
+
|
|
21
|
+
**Usage**
|
|
22
|
+
|
|
23
|
+
- [Why you need it](#why-you-need-it)
|
|
24
|
+
- [Features](#features)
|
|
25
|
+
- [Installation](#installation)
|
|
26
|
+
- [Getting started in three steps](#getting-started-in-three-steps)
|
|
27
|
+
- [Settings reference](#settings-reference)
|
|
28
|
+
- [Troubleshooting](#troubleshooting)
|
|
29
|
+
- [Known limitations](#known-limitations)
|
|
30
|
+
|
|
31
|
+
**Development**
|
|
13
32
|
|
|
14
|
-
|
|
33
|
+
- [Architecture](#architecture)
|
|
34
|
+
- [Project layout](#project-layout)
|
|
35
|
+
- [Life of a notification](#life-of-a-notification)
|
|
36
|
+
- [Local development](#local-development)
|
|
37
|
+
- [Tests](#tests)
|
|
38
|
+
- [Releasing](#releasing)
|
|
39
|
+
- [Design constraints](#design-constraints)
|
|
15
40
|
|
|
16
|
-
|
|
41
|
+
## Why you need it
|
|
42
|
+
|
|
43
|
+
Inside Harness, the two signals that matter — "a session finished" and "something is waiting for your decision" — only show up on the **sidebar dots** and **inside the session itself**. The moment you switch to another tab or another app, they are invisible.
|
|
44
|
+
|
|
45
|
+
Long tasks make this worse: you go do something else, come back, and the session finished ages ago — or an approval request has been sitting there for ten minutes.
|
|
46
|
+
|
|
47
|
+
This plugin moves both signals somewhere you will actually see them:
|
|
48
|
+
|
|
49
|
+
| While you are away from the DSH page | What you get |
|
|
50
|
+
|---|---|
|
|
51
|
+
| A session finished | The tab whale turns **green**, plus a system notification (with the turn duration when it is a single notification) |
|
|
52
|
+
| Something needs you | The tab whale turns **amber**, plus a system notification (saying whether it is an approval / question / plan review) |
|
|
17
53
|
|
|
18
54
|
## Features
|
|
19
55
|
|
|
20
|
-
### 1. Whale status light
|
|
56
|
+
### 1. Whale status light
|
|
57
|
+
|
|
58
|
+
Three colours are encoded directly into the tab icon, driven by **the same official signal** as the sidebar dots, so they can never drift apart:
|
|
21
59
|
|
|
22
60
|
| Whale colour | Meaning | When it clears |
|
|
23
61
|
|---|---|---|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
28
|
-
In the tab (green = a session finished):
|
|
29
|
-
|
|
30
|
-

|
|
62
|
+
| <img src="docs/images/whale-green.svg" width="20" alt="Green"> Green | A session **finished** while you were not watching it | Clears once you open that session; back to default once you have opened them all |
|
|
63
|
+
| <img src="docs/images/whale-amber.svg" width="20" alt="Amber"> Amber | A session **awaits you**: question / approval / plan review | Clears once handled |
|
|
64
|
+
| <img src="docs/images/whale-black.svg" width="20" alt="Black"> Black | All clear (the stock icon) | Default state |
|
|
31
65
|
|
|
32
|
-
|
|
66
|
+

|
|
33
67
|
|
|
34
|
-
|
|
68
|
+
- Only **main sessions** count; subagents do not affect it
|
|
69
|
+
- When both apply, **amber wins** — a pending interaction is the primary status, so a session stuck waiting on you is never masked by another session that just finished
|
|
70
|
+
- All three colours are configurable (including the default colour: leave it unset to keep the stock icon)
|
|
35
71
|
|
|
36
72
|
### 2. System notifications
|
|
37
73
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
74
|
+
Two events raise a notification, and by default **only while you are not looking**:
|
|
75
|
+
|
|
76
|
+
| Event | Trigger | What the notification says |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| **Finished** | A session completes | Title = session name; body = `Session finished`, with the turn duration appended when it is a single notification (e.g. `Session finished · turn took 3s`) |
|
|
79
|
+
| **Pending** | A new pending interaction appears | Title = session name; body = the interaction type: `Approval needed · <tool>` / `Choose an option` (`Choose options` for multi-select, `Type an answer` when there are no options, `N questions` for a batch) / `Plan review`; unknown types fall back to `Something awaits you` |
|
|
47
80
|
|
|
48
81
|
| Finished (green) | Pending (amber) |
|
|
49
82
|
|---|---|
|
|
50
|
-
|  |  |
|
|
83
|
+
|  |  |
|
|
84
|
+
|
|
85
|
+
> Notifications are raised by the browser (Chrome here), so the card shows the browser and the site `127.0.0.1:3080` — that is expected.
|
|
86
|
+
> The screenshots show the English UI; notification copy is localised at runtime.
|
|
87
|
+
|
|
88
|
+
Other behaviour:
|
|
51
89
|
|
|
52
|
-
|
|
90
|
+
- **Click a notification** = focus the window and open that session
|
|
91
|
+
- **Only while you are not looking** — "foreground" means the tab is *visible* **and** the window has focus. Switching tabs, minimising, or having another app on top all notify. To also notify while you are looking (you are parked in a session but away from the screen, e.g. on your phone), turn on **Notify in the foreground**
|
|
92
|
+
- **Aggregation**: events within 300ms are merged into one notification, with `+N` appended to the title
|
|
93
|
+
- **Optional persistence**: turn off **Auto hide** and the notification stays on screen until you dismiss it
|
|
94
|
+
- The notification icon follows your configured status-light colours
|
|
53
95
|
|
|
54
|
-
### 3.
|
|
96
|
+
### 3. Sounds
|
|
55
97
|
|
|
56
|
-
Finished and pending each have
|
|
98
|
+
Finished and pending each have their own dropdown, **47 entries** in total: 2 built-in synthesised chimes plus 45 mp3s taken from opencode's bundled sound library.
|
|
57
99
|
|
|
58
100
|
| Pack | Entries |
|
|
59
101
|
|---|---|
|
|
60
|
-
| Built-in (synthesised) | Chime Up (default
|
|
102
|
+
| Built-in (synthesised) | `Chime Up` (finished default, rising two-tone), `Chime Down` (pending default, falling two-tone) |
|
|
61
103
|
| Alert | Alert 01–10 |
|
|
62
104
|
| Bip-bop | Bip-bop 01–10 |
|
|
63
105
|
| Staplebops | Staplebops 01–07 |
|
|
64
106
|
| Nope | Nope 01–12 |
|
|
65
107
|
| Yup | Yup 01–06 |
|
|
66
108
|
|
|
67
|
-
- **Hover to preview
|
|
68
|
-
- **
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
- No migration needed:
|
|
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
|
|
109
|
+
- **Hover to preview**: hovering a dropdown entry plays it (120ms debounce, so sweeping the list does not turn into noise)
|
|
110
|
+
- **Preview after adjusting volume**: releasing the volume slider plays the finished chime at the new volume
|
|
111
|
+
- With sound on, **the system notification sound is muted** and the plugin sound is used instead
|
|
112
|
+
- Audio is served by the host half at `/notice-center-sounds/<id>.mp3`; if that route is unavailable it **falls back to the built-in chime**, never to silence
|
|
113
|
+
- No migration needed: existing configs keep using `Chime Up` / `Chime Down`
|
|
80
114
|
|
|
81
115
|
## Installation
|
|
82
116
|
|
|
83
|
-
|
|
117
|
+
### Requirements
|
|
118
|
+
|
|
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
|
+
- 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)
|
|
84
121
|
|
|
85
|
-
### 1
|
|
122
|
+
### Option 1: from npm (recommended)
|
|
86
123
|
|
|
87
124
|
```sh
|
|
88
125
|
dsh plugin --profile web add dsh-notice-center
|
|
89
126
|
```
|
|
90
127
|
|
|
91
|
-
Package page: <https://www.npmjs.com/package/dsh-notice-center> (`latest` is
|
|
128
|
+
Package page: <https://www.npmjs.com/package/dsh-notice-center> (`latest` is stable; prereleases go to the `next` tag — install with `dsh-notice-center@next`)
|
|
92
129
|
|
|
93
|
-
### 2
|
|
130
|
+
### Option 2: straight from GitHub
|
|
94
131
|
|
|
95
132
|
```sh
|
|
96
133
|
dsh plugin --profile web add github:SCP-QQ/dsh-notice-center
|
|
97
134
|
# or over SSH (when a proxy interferes with HTTPS on your machine)
|
|
98
|
-
dsh plugin --profile web add git+ssh://git@github.com
|
|
135
|
+
dsh plugin --profile web add git+ssh://git@github.com:SCP-QQ/dsh-notice-center.git
|
|
99
136
|
```
|
|
100
137
|
|
|
101
|
-
### 3
|
|
138
|
+
### Option 3: from a tarball (offline / internal distribution)
|
|
102
139
|
|
|
103
140
|
```sh
|
|
104
141
|
npm pack # produces dsh-notice-center-<version>.tgz
|
|
105
142
|
dsh plugin --profile web add ./dsh-notice-center-<version>.tgz
|
|
106
143
|
```
|
|
107
144
|
|
|
108
|
-
### 4
|
|
145
|
+
### Option 4: local link (while editing the code)
|
|
109
146
|
|
|
110
|
-
|
|
147
|
+
In the profile's `package.json`: add `"dsh-notice-center": "link:<project dir>"` to `dependencies` and `"dsh-notice-center"` to `dsh.profile.bundles`, then run `pnpm install` inside the profile directory.
|
|
111
148
|
|
|
112
|
-
>
|
|
149
|
+
> **All four options require a Harness restart to take effect.**
|
|
150
|
+
> Installing from npm / GitHub needs no extra `@deepseek-ai/*` packages from the profile — the host half's only runtime dependency is `@deepseek-ai/schemastery` (available on npm).
|
|
113
151
|
|
|
114
|
-
|
|
152
|
+
### Uninstall
|
|
115
153
|
|
|
116
|
-
|
|
154
|
+
```sh
|
|
155
|
+
dsh plugin --profile web remove dsh-notice-center
|
|
156
|
+
```
|
|
117
157
|
|
|
118
|
-
|
|
158
|
+
Remove the dependency and the `dsh.profile.bundles` entry, then restart — nothing is left behind.
|
|
159
|
+
|
|
160
|
+
## Getting started in three steps
|
|
161
|
+
|
|
162
|
+
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)
|
|
164
|
+
3. **Tune as you like** — expand the two groups: change colours, pick sounds, set volume, toggle **Notify in the foreground** / **Auto hide**
|
|
165
|
+
|
|
166
|
+
<img src="docs/images/settings-en.png" width="440" alt="Notification center settings page">
|
|
167
|
+
|
|
168
|
+
After that, whenever you switch away or minimise, a finished session or a pending interaction notifies you — and clicking the notification jumps straight back to that session.
|
|
169
|
+
|
|
170
|
+
## Settings reference
|
|
171
|
+
|
|
172
|
+
| Group | Setting | Key | Default | Notes |
|
|
173
|
+
|---|---|---|---|---|
|
|
174
|
+
| Whale status light | Master switch | `colorsEnabled` | on | When off, the stock icon is kept |
|
|
175
|
+
| | Finished | `green` | `#22C55E` | Finished status-light colour (official sidebar colour) |
|
|
176
|
+
| | Pending | `amber` | `#F59E0B` | Pending status-light colour (official sidebar colour) |
|
|
177
|
+
| | 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 |
|
|
179
|
+
| | Auto hide | `notifyAutoHide` | on | Off = the notification stays until you dismiss it |
|
|
180
|
+
| | Notify in the foreground | `notifyForeground` | off | On = also notify while you are looking at the page |
|
|
181
|
+
| | Sound | `notifySound` | on | When on, the system sound is muted and the plugin sound plays |
|
|
182
|
+
| | Volume | `notifyVolume` | `0.6` | 0–1 |
|
|
183
|
+
| | Finished sound | `notifyDoneSound` | `builtin-up` | one of 47 |
|
|
184
|
+
| | Pending sound | `notifyPendingSound` | `builtin-down` | one of 47 |
|
|
185
|
+
|
|
186
|
+
Settings are persisted by Harness' settings service into the `notice-center:` section of `<DSH_HOME>/settings.yaml`.
|
|
187
|
+
The settings page also shows a one-line footer at the very top with the plugin name and version (e.g. "Notification center v1.3.0") — clicking it opens the repository in a new tab.
|
|
188
|
+
|
|
189
|
+
## Troubleshooting
|
|
190
|
+
|
|
191
|
+
| Symptom | Cause and fix |
|
|
192
|
+
|---|---|
|
|
193
|
+
| 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
|
+
| 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 |
|
|
195
|
+
| 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
|
+
| 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
|
+
| Nothing after closing the tab | Known limitation, see below |
|
|
198
|
+
| The green light disappears after a refresh | Expected. That state is the official signal's in-memory state and is lost on refresh |
|
|
199
|
+
| The notification flashes by too fast | Turn **Auto hide** off and it stays until dismissed |
|
|
200
|
+
| No sound | Check in order: sound switch, volume not 0, whether the sound route is available (it falls back to the built-in chime, so it should never be fully silent) |
|
|
201
|
+
| Notifications swallowed by the OS | Windows Focus Assist / Do Not Disturb intercepts them; the plugin cannot detect it |
|
|
202
|
+
|
|
203
|
+
## Known limitations
|
|
204
|
+
|
|
205
|
+
- **No notifications once the browser tab is closed** — Web Push needs a server plus HTTPS, which is not realistic for a local plugin
|
|
206
|
+
- **The Notification API is unavailable in a non-secure context** (LAN IP)
|
|
207
|
+
- **The green state is lost on refresh** — it lives in memory only, which is the behaviour of the official signal itself
|
|
208
|
+
- **System-level Do Not Disturb swallows notifications** (Windows Focus Assist etc.); the plugin cannot detect it
|
|
209
|
+
- **The auto-hide duration is not configurable** — the Web Notification spec has no duration parameter; the desktop decides (roughly 5–20 seconds)
|
|
210
|
+
- **The notification card cannot be styled** — the card is rendered by the browser with no CSS or theming hook; only title, body and icon are yours
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
# Development
|
|
215
|
+
|
|
216
|
+
> Everything below is for maintainers: architecture, local development, tests and releasing. If you only want to use the plugin, you can stop here.
|
|
217
|
+
|
|
218
|
+
## Architecture
|
|
219
|
+
|
|
220
|
+
The plugin has **two halves** running in different processes, exchanging configuration through Harness' settings service:
|
|
221
|
+
|
|
222
|
+
| Part | File | Runs in | Responsibility |
|
|
223
|
+
|---|---|---|---|
|
|
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 |
|
|
227
|
+
|
|
228
|
+
**The host half's two optional channels**:
|
|
229
|
+
|
|
230
|
+
- `ctx.inject(["settings"], …)` — registers the schema; a success line is logged to the console as proof
|
|
231
|
+
- `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
|
+
|
|
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.
|
|
234
|
+
|
|
235
|
+
**Where the browser half gets its data** (all official client services; the plugin holds no state of its own):
|
|
236
|
+
|
|
237
|
+
| Service | Provided by | Used for |
|
|
238
|
+
|---|---|---|
|
|
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, … }>` |
|
|
241
|
+
|
|
242
|
+
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
|
+
|
|
244
|
+
## Project layout
|
|
119
245
|
|
|
120
|
-
```
|
|
121
|
-
|
|
246
|
+
```
|
|
247
|
+
package.json package metadata (scripts: test / test:host / test:client)
|
|
248
|
+
cordis.patch.yml bundle patch layer (insert id: notice-center)
|
|
249
|
+
lib/index.mjs host half: settings schema + sound static route
|
|
250
|
+
lib/client.cjs browser half: favicon machine + notification machine + sound library + settings page
|
|
251
|
+
assets/audio/*.mp3 45 opencode sounds (MIT; see assets/audio/README.md)
|
|
252
|
+
docs/images/ README screenshots, banners and the social-preview card
|
|
253
|
+
test/host-half.smoke.mjs host-half smoke test (164 lines)
|
|
254
|
+
test/client-half.smoke.mjs browser-half smoke test: state machine + real settings-page render (687 lines)
|
|
255
|
+
.github/workflows/test.yml CI: runs pnpm test on push / PR
|
|
256
|
+
.github/workflows/publish.yml release: tag-triggered, OIDC, no token
|
|
122
257
|
```
|
|
123
258
|
|
|
124
|
-
|
|
259
|
+
## Life of a notification
|
|
125
260
|
|
|
126
|
-
|
|
261
|
+
```
|
|
262
|
+
sessions.list changes
|
|
263
|
+
└─ sync()
|
|
264
|
+
├─ trackEdges() running true->false edge (compensation when the current session finishes)
|
|
265
|
+
├─ detectTransitions() official completed false->true; new sessions in pendingInteractions
|
|
266
|
+
│ └─ queueNotification(kind, sessionId, label, typeLabel)
|
|
267
|
+
│ ├─ master switch off -> dropped
|
|
268
|
+
│ ├─ this same completion already queued -> dropped (no double-send)
|
|
269
|
+
│ └─ enqueued + 300ms aggregation window
|
|
270
|
+
└─ targetOf() -> favicon becomes green / amber / default
|
|
271
|
+
|
|
272
|
+
300ms later, flushNotifications()
|
|
273
|
+
├─ permission not granted -> dropped
|
|
274
|
+
├─ page in foreground and foreground notify off -> dropped
|
|
275
|
+
├─ group by kind -> one Notification per group (title = session name, body = event type)
|
|
276
|
+
└─ play the selected sound
|
|
277
|
+
```
|
|
127
278
|
|
|
128
|
-
|
|
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
|
|
279
|
+
Notifications deliberately carry **no `tag`**: the same session reuses the same tag across runs, and Windows/Chrome treats the later one as an *update* to an existing notification — it replaces silently and never raises a banner again.
|
|
130
280
|
|
|
131
|
-
##
|
|
281
|
+
## Local development
|
|
132
282
|
|
|
133
283
|
```sh
|
|
134
|
-
|
|
284
|
+
pnpm install # the only runtime dependency is @deepseek-ai/schemastery
|
|
285
|
+
npm test # host-half + browser-half smoke tests
|
|
135
286
|
```
|
|
136
287
|
|
|
137
|
-
|
|
288
|
+
**The restart rule has two halves — this is the easiest thing to trip over:**
|
|
138
289
|
|
|
139
|
-
|
|
290
|
+
| What you changed | Restart Harness? | Why |
|
|
291
|
+
|---|---|---|
|
|
292
|
+
| `lib/client.cjs` (browser half) | **No** — a page refresh is cleanest | Client HMR stat-polls the bundle and tells the browser to reload that plugin |
|
|
293
|
+
| `lib/index.mjs` (host half) | **Yes, mandatory** | Host-half hot reload relies on `cordis-plugin-hmr` (needs the loader to run with `--expose-internals`), which does not work in practice. **Schema changes (adding/removing settings) also need a restart** — otherwise the settings page write fails validation, which looks like "the switch does nothing" |
|
|
294
|
+
| First install, any rename | **Yes, mandatory** | The plugin tree and bundle list changed |
|
|
140
295
|
|
|
141
|
-
-
|
|
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
|
|
296
|
+
Debugging: browser-half errors land in the DevTools console; a successful host-half registration logs `dsh-notice-center: settings namespace "notice-center" registered` to the Harness console.
|
|
145
297
|
|
|
146
|
-
##
|
|
298
|
+
## Tests
|
|
147
299
|
|
|
148
300
|
```sh
|
|
149
|
-
npm test #
|
|
301
|
+
npm test # both (this is what CI runs)
|
|
150
302
|
npm run test:host # host half only
|
|
151
|
-
npm run test:client #
|
|
303
|
+
npm run test:client # browser half only
|
|
152
304
|
```
|
|
153
305
|
|
|
154
|
-
|
|
306
|
+
Both smoke tests are **zero-dependency, pure Node** — no browser needed:
|
|
307
|
+
|
|
308
|
+
- **Host half** (164 lines): actually executes `apply()`, asserts only the expected services are injected, checks schema defaults and validation, exercises the sound route (serves an mp3 / 404 for unknown / rejects path traversal / all 45 packaged files present) and verifies that an existing `settings.yaml` passes the schema **with every field preserved verbatim**
|
|
309
|
+
- **Browser half** (687 lines): stubs `window.__ModuleLoader__` / `document` / `Notification`, evaluates the bundle and takes its factory, assembles `apply()` with a fake `ctx`, then drives state transitions and asserts the `Notification` constructor arguments. The settings page is **really rendered** against a recording stub (React element tree), asserting switch order, that each control writes only its own field, collapse behaviour, the footer, and more
|
|
310
|
+
|
|
311
|
+
Invariants pinned by the tests — do not break these:
|
|
312
|
+
|
|
313
|
+
- `PLUGIN_VERSION` / `PLUGIN_NAME` / `PLUGIN_REPO` in `lib/client.cjs` must match `package.json`
|
|
314
|
+
- The `SOUND_PACKS` library must map **one-to-one** onto the files in `assets/audio/`; adding a sound means updating it in **two places**: `lib/client.cjs` and `test/client-half.smoke.mjs`
|
|
315
|
+
- The `zh` and `en` dictionaries must have identical key sets
|
|
155
316
|
|
|
156
317
|
## Releasing
|
|
157
318
|
|
|
158
|
-
|
|
319
|
+
Releasing is driven by a **tag-triggered** [`publish.yml`](.github/workflows/publish.yml) — ordinary commits and pushes **never publish**:
|
|
159
320
|
|
|
160
321
|
| Intent | Command | Result |
|
|
161
322
|
|---|---|---|
|
|
162
|
-
| Commit only (feature not
|
|
163
|
-
|
|
|
164
|
-
|
|
|
165
|
-
| Validate the pipeline only | Actions → publish → Run workflow |
|
|
323
|
+
| Commit only (feature not validated yet) | `git commit` + `git push` | Only `test.yml` runs; npm sees nothing |
|
|
324
|
+
| Prerelease (for testers) | set 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; `latest` untouched |
|
|
325
|
+
| Stable release | set version to `1.3.0` → commit → `git tag v1.3.0 && git push origin v1.3.0` | Published to **`latest`**, with provenance attached |
|
|
326
|
+
| Validate the pipeline only | Actions → publish → Run workflow | Installs, tests and runs `npm pack --dry-run` — **publishes nothing** |
|
|
166
327
|
|
|
167
|
-
|
|
328
|
+
The workflow has **three gates**; failing any one blocks the release:
|
|
168
329
|
|
|
169
|
-
|
|
330
|
+
1. The tag must match the `package.json` version
|
|
331
|
+
2. The tagged commit must already be on `main`
|
|
332
|
+
3. Tests must pass
|
|
333
|
+
|
|
334
|
+
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.
|
|
170
335
|
|
|
171
|
-
|
|
336
|
+
### One-time setup: npm Trusted Publishing (OIDC)
|
|
172
337
|
|
|
173
|
-
Open the package
|
|
338
|
+
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**:
|
|
174
339
|
|
|
175
340
|
<https://www.npmjs.com/package/dsh-notice-center/access>
|
|
176
341
|
|
|
@@ -178,34 +343,36 @@ Open the package management page → **Publishing Access** → *Trusted publishe
|
|
|
178
343
|
|---|---|
|
|
179
344
|
| Organization or user | `SCP-QQ` |
|
|
180
345
|
| Repository | `dsh-notice-center` |
|
|
181
|
-
| Workflow filename | `publish.yml` (
|
|
182
|
-
| Environment name | `npm` (must match the `environment:`
|
|
183
|
-
| **Allowed actions → Allow npm publish** | ✅ **
|
|
346
|
+
| Workflow filename | `publish.yml` (**filename only**, no path; renaming the file requires updating this or publishing returns 403) |
|
|
347
|
+
| Environment name | `npm` (must match the workflow's `environment:`) |
|
|
348
|
+
| **Allowed actions → Allow npm publish** | ✅ **required** |
|
|
184
349
|
|
|
185
|
-
> ⚠️ That checkbox is new npm behaviour: a trusted publisher may only `npm stage publish
|
|
186
|
-
> If you would rather have
|
|
350
|
+
> ⚠️ That checkbox is new npm behaviour: a trusted publisher may **only `npm stage publish`** by default, so without *Allow npm publish* the workflow's `npm publish` is rejected.
|
|
351
|
+
> If you would rather have "staged only, human approves in the browser", leave it unchecked and change the last step to `npm stage publish`.
|
|
187
352
|
|
|
188
|
-
-
|
|
189
|
-
- To require a
|
|
190
|
-
-
|
|
353
|
+
- With OIDC, npm attaches **provenance automatically** (no `--provenance` flag); the package page shows the attestation
|
|
354
|
+
- To require a manual approval after pushing a tag: repository Settings → Environments → `npm` → add **Required reviewers** (optional)
|
|
355
|
+
- Publishing from your own machine is still possible with a granular token: put `//registry.npmjs.org/:_authToken=<token>` in an `.npmrc` at the project root. That file is already in `.gitignore` — **never commit it**
|
|
191
356
|
|
|
192
|
-
##
|
|
357
|
+
## Design constraints
|
|
193
358
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
359
|
+
Read these before changing code — each one came from a real bug:
|
|
360
|
+
|
|
361
|
+
1. **No `tag` on notifications** — the same tag is treated as an *update* and silently replaces (see "Life of a notification")
|
|
362
|
+
2. **"Foreground" is `tab visible && window focused`** — using `visibilityState` alone misreads "browser buried behind another app" as "the user is looking", which is exactly when a notification matters most
|
|
363
|
+
3. **Amber beats green** — matching the official `sessionStatuses`; a pending interaction is the primary status
|
|
364
|
+
4. **The turn duration comes from observing the `running` edge** — a session already running when the page loads has no known start, so no duration is shown (never guessed)
|
|
365
|
+
5. **The host half keeps zero `@deepseek-ai` runtime dependencies** — only `schemastery`, otherwise a registry install breaks on peer resolution
|
|
366
|
+
6. **The sound route only allows `^[a-z0-9-]+\.mp3$`** — the filename goes straight into a path join, so it must be whitelisted
|
|
367
|
+
|
|
368
|
+
## Feedback and contributing
|
|
369
|
+
|
|
370
|
+
- Issues and ideas: <https://github.com/SCP-QQ/dsh-notice-center/issues>
|
|
371
|
+
- Run `npm test` before proposing a change; settings-related changes should update the assertions in `test/*.smoke.mjs`
|
|
372
|
+
- Sound assets come from [anomalyco/opencode](https://github.com/anomalyco/opencode) (MIT) — keep [`assets/audio/README.md`](./assets/audio/README.md) in sync when replacing or removing them
|
|
205
373
|
|
|
206
374
|
## License
|
|
207
375
|
|
|
208
|
-
MIT
|
|
376
|
+
MIT — see [`LICENSE`](./LICENSE) for the full text.
|
|
209
377
|
|
|
210
|
-
The sound
|
|
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)).
|
|
378
|
+
The 45 sound files in `assets/audio/*.mp3` come from [anomalyco/opencode](https://github.com/anomalyco/opencode) (`packages/ui/src/assets/audio/`), released upstream under MIT; the same licence and attribution are kept here. If upstream changes its terms, replace or remove this directory.
|
package/README.md
CHANGED
|
@@ -5,93 +5,128 @@
|
|
|
5
5
|
[](https://github.com/SCP-QQ/dsh-notice-center/actions/workflows/test.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/dsh-notice-center)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
<img src="docs/images/banner.jpg" alt="dsh-notice-center 会话通知 —— 让每一条消息,都不错过" width="100%">
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
**一句话**:DeepSeek Harness 的**通知中心**——把标签页上的鲸鱼图标变成状态灯,并在你不在看的时候主动提醒你。
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
| | |
|
|
13
|
+
|---|---|
|
|
14
|
+
| 包名 | `dsh-notice-center` |
|
|
15
|
+
| 插件实例 id | `notice-center` |
|
|
16
|
+
| 设置命名空间 | `notice-center` |
|
|
17
|
+
| 安装形态 | npm 包 / GitHub 仓库 / tarball / 本地 link,四种都支持 |
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
## 目录
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
**使用**
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
- [为什么需要它](#为什么需要它)
|
|
24
|
+
- [功能特性](#功能特性)
|
|
25
|
+
- [安装](#安装)
|
|
26
|
+
- [上手三步](#上手三步)
|
|
27
|
+
- [设置项详解](#设置项详解)
|
|
28
|
+
- [常见问题](#常见问题)
|
|
29
|
+
- [已知限制](#已知限制)
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
**开发**
|
|
21
32
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
- [架构总览](#架构总览)
|
|
34
|
+
- [目录结构](#目录结构)
|
|
35
|
+
- [一条通知的生命周期](#一条通知的生命周期)
|
|
36
|
+
- [本地开发](#本地开发)
|
|
37
|
+
- [测试](#测试)
|
|
38
|
+
- [发版](#发版)
|
|
39
|
+
- [设计约束](#设计约束)
|
|
40
|
+
|
|
41
|
+
## 为什么需要它
|
|
27
42
|
|
|
28
|
-
|
|
43
|
+
在 Harness 里,「会话跑完了」和「有事等你拍板」这两个信号,只出现在**侧边栏的小圆点**和**会话本身**上。你一旦切到别的标签页、别的应用,就看不见了。
|
|
29
44
|
|
|
30
|
-
|
|
45
|
+
长任务尤其难受:你去干别的,回来才发现早就跑完;或者一个审批请求已经晾了十分钟。
|
|
31
46
|
|
|
32
|
-
|
|
47
|
+
这个插件把这两个信号搬到你能看到的地方:
|
|
33
48
|
|
|
34
|
-
|
|
49
|
+
| 你不在 DSH 页面时 | 你会看到 |
|
|
50
|
+
|---|---|
|
|
51
|
+
| 会话跑完了 | 标签页鲸鱼变**绿**,并弹出系统通知(单独一条时附本轮总用时) |
|
|
52
|
+
| 有事等你处理 | 标签页鲸鱼变**琥珀**,并弹出系统通知(写明是审批 / 提问 / 计划审核) |
|
|
53
|
+
|
|
54
|
+
## 功能特性
|
|
55
|
+
|
|
56
|
+
### 1. 鲸鱼状态灯
|
|
57
|
+
|
|
58
|
+
三种颜色直接编码在标签页图标上,和侧边栏的绿点、琥珀点**用的是同一份官方信号**,永远同步:
|
|
59
|
+
|
|
60
|
+
| 鲸鱼颜色 | 含义 | 什么时候恢复 |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| <img src="docs/images/whale-green.svg" width="20" alt="绿色"> 绿色 | 有会话在你没看它的时候**跑完了** | 该会话点开后自动恢复;全部点开过一遍即回到默认状态 |
|
|
63
|
+
| <img src="docs/images/whale-amber.svg" width="20" alt="琥珀色"> 琥珀色 | 有会话**有事等你处理**:提问 / 审批 / 计划审核 | 处理完后自动消失 |
|
|
64
|
+
| <img src="docs/images/whale-black.svg" width="20" alt="黑色"> 黑色 | 一切正常(官方原版图标) | 默认状态 |
|
|
65
|
+
|
|
66
|
+

|
|
67
|
+
|
|
68
|
+
- 只统计**主会话**,子代理不计入
|
|
69
|
+
- 绿色与琥珀同时存在时**琥珀优先**——待处理是首要状态,有会话卡住等你时,不会因为别的会话刚跑完而被掩盖
|
|
70
|
+
- 三种颜色都能改(含「默认色」:不配置就沿用官方原版图标)
|
|
35
71
|
|
|
36
72
|
### 2. 系统通知
|
|
37
73
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- **提示音**:完成 / 待处理各自可下拉选音效(**悬浮即试听**),音量可调(**调完音量会按新音量试听当前的完成提示音**);开启后改播插件自带音效,系统默认提示音会被静音
|
|
45
|
-
- **常驻可选**:可关闭「自动隐藏」,通知就一直留在屏幕上,直到你点掉它(默认开启自动隐藏,由系统自行收起)
|
|
46
|
-
- 通知图标跟随你配置的状态灯颜色
|
|
74
|
+
两种事件会触发通知,**默认只在你不在看的时候弹**:
|
|
75
|
+
|
|
76
|
+
| 事件 | 触发时机 | 通知长什么样 |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| **完成通知** | 会话跑完 | 标题=会话名;正文=`会话已完成`,单独一条时附本轮总用时(如 `会话已完成 · 本轮总用时 2分13秒`) |
|
|
79
|
+
| **待处理通知** | 出现新的待处理交互 | 标题=会话名;正文=交互类型:`待审批 · <工具名>` / `请你选择`(多选则 `请你多选`、无选项则 `请你填写`、多问题则 `向你提问(N 个)`)/ `计划待审核`;认不出的类型回退为 `有交互等待处理` |
|
|
47
80
|
|
|
48
81
|
| 完成通知(绿) | 待处理通知(琥珀) |
|
|
49
82
|
|---|---|
|
|
50
83
|
|  |  |
|
|
51
84
|
|
|
52
|
-
> 通知由浏览器发出(本机是 Chrome
|
|
85
|
+
> 通知由浏览器发出(本机是 Chrome),所以卡片上显示浏览器与站点 `127.0.0.1:3080`——这是正常的。
|
|
86
|
+
|
|
87
|
+
其余行为:
|
|
53
88
|
|
|
54
|
-
|
|
89
|
+
- **点击通知** = 聚焦窗口并打开对应会话
|
|
90
|
+
- **只在你不看的时候弹**——「前台」=标签页可见**且**窗口有焦点;切到别的标签页、最小化、被别的应用压在后面时都会提醒。想在前台也提醒(人还停在会话里、却已离开屏幕,例如在刷手机),打开设置里的「前台提醒」
|
|
91
|
+
- **多条聚合**:300ms 内的事件合并为一条,标题补 `+N`
|
|
92
|
+
- **可常驻**:关掉「自动隐藏」后,通知会一直留在屏幕上,直到你点掉它
|
|
93
|
+
- 通知图标跟随你配置的状态灯颜色
|
|
94
|
+
|
|
95
|
+
### 3. 提示音
|
|
55
96
|
|
|
56
|
-
|
|
97
|
+
完成与待处理各有独立下拉,共 **47 个条目**:2 个插件内置合成音 + 45 个取自 opencode 内置音效库的 mp3。
|
|
57
98
|
|
|
58
99
|
| 音效包 | 条目 |
|
|
59
100
|
|---|---|
|
|
60
|
-
| 内置(合成音) | Chime Up
|
|
101
|
+
| 内置(合成音) | `Chime Up`(完成默认,上行双音)、`Chime Down`(待处理默认,下行双音) |
|
|
61
102
|
| Alert | Alert 01–10 |
|
|
62
103
|
| Bip-bop | Bip-bop 01–10 |
|
|
63
104
|
| Staplebops | Staplebops 01–07 |
|
|
64
105
|
| Nope | Nope 01–12 |
|
|
65
106
|
| Yup | Yup 01–06 |
|
|
66
107
|
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
- 宿主半通过 `/notice-center-sounds/<id>.mp3`
|
|
71
|
-
- 老配置无需迁移:没配过音效时仍用 Chime Up / Chime Down
|
|
72
|
-
|
|
73
|
-
## 使用
|
|
74
|
-
|
|
75
|
-
1. **打开设置页** —— 设置 → **通知中心**
|
|
76
|
-
2. **开启系统通知** —— 打开「系统通知」总开关,浏览器会弹出授权请求,选「允许」(被拒绝时设置页会提示,需要到浏览器的站点设置里恢复)
|
|
77
|
-
3. **调音量 / 选音效 / 前台提醒** —— 展开「更多通知设置」:拖音量条,**松手后会按新音量试听完成提示音**;在「完成提示音 / 待处理提示音」下拉里**鼠标划过即可试听**,点一下选中;想让正看着页面时也提醒,打开「前台提醒」
|
|
78
|
-
4. **改颜色(可选)** —— 展开「鲸鱼状态灯」:点色块或直接填 `#RRGGBB`,行尾 ↺ 恢复默认
|
|
79
|
-
5. **就这样** —— 之后你切走或最小化时,会话跑完 / 有事等你都会收到通知;点通知直接跳回那个会话
|
|
108
|
+
- **悬浮即试听**:鼠标划过下拉条目就播该音效(120ms 防抖,扫过整列不会连成一片)
|
|
109
|
+
- **调完音量即试听**:拖动音量条松手后,会按新音量播一次完成提示音
|
|
110
|
+
- 开启提示音后,**系统默认提示音会被静音**,改播插件音效
|
|
111
|
+
- 宿主半通过 `/notice-center-sounds/<id>.mp3` 提供音频;路由不可用时**自动回退内置合成音**,不会变哑
|
|
112
|
+
- 老配置无需迁移:没配过音效时仍用 `Chime Up` / `Chime Down`
|
|
80
113
|
|
|
81
114
|
## 安装
|
|
82
115
|
|
|
83
|
-
|
|
84
|
-
`dsh.profile.bundles`),所以 registry 包名、git 仓库、tarball、本地路径都能直接装。
|
|
116
|
+
### 前置要求
|
|
85
117
|
|
|
86
|
-
|
|
118
|
+
- **官方版本的 DeepSeek Harness**(适配范围 `0.1.2-rc.1+`;本机实测 `0.1.5-rc.1` + 客户端包 `0.1.5-rc.2`)
|
|
119
|
+
- 系统通知需要**安全上下文**——`http://127.0.0.1:3080` 或 `localhost` 可用;用**局域网 IP** 访问时浏览器的 Notification API 不可用(这是浏览器限制,不是插件问题)
|
|
120
|
+
|
|
121
|
+
### 方式一:从 npm 安装(推荐)
|
|
87
122
|
|
|
88
123
|
```sh
|
|
89
124
|
dsh plugin --profile web add dsh-notice-center
|
|
90
125
|
```
|
|
91
126
|
|
|
92
|
-
包页:<https://www.npmjs.com/package/dsh-notice-center>(`latest`
|
|
127
|
+
包页:<https://www.npmjs.com/package/dsh-notice-center>(`latest` 为稳定版,预发布版在 `next` 通道,用 `dsh-notice-center@next` 装)
|
|
93
128
|
|
|
94
|
-
###
|
|
129
|
+
### 方式二:直接从 GitHub 安装
|
|
95
130
|
|
|
96
131
|
```sh
|
|
97
132
|
dsh plugin --profile web add github:SCP-QQ/dsh-notice-center
|
|
@@ -99,82 +134,207 @@ dsh plugin --profile web add github:SCP-QQ/dsh-notice-center
|
|
|
99
134
|
dsh plugin --profile web add git+ssh://git@github.com/SCP-QQ/dsh-notice-center.git
|
|
100
135
|
```
|
|
101
136
|
|
|
102
|
-
###
|
|
137
|
+
### 方式三:从 tarball 安装(离线 / 内网分发)
|
|
103
138
|
|
|
104
139
|
```sh
|
|
105
140
|
npm pack # 生成 dsh-notice-center-<版本>.tgz
|
|
106
141
|
dsh plugin --profile web add ./dsh-notice-center-<版本>.tgz
|
|
107
142
|
```
|
|
108
143
|
|
|
109
|
-
###
|
|
144
|
+
### 方式四:本地 link(改代码时用)
|
|
110
145
|
|
|
111
|
-
profile 的 `dependencies` 加
|
|
112
|
-
`"dsh-notice-center": "link:<项目目录>/dsh-notice-center"`,
|
|
113
|
-
`dsh.profile.bundles` 加 `"dsh-notice-center"`。
|
|
146
|
+
在 profile 的 `package.json` 里:`dependencies` 加 `"dsh-notice-center": "link:<项目目录>"`,`dsh.profile.bundles` 加 `"dsh-notice-center"`,然后在 profile 目录跑 `pnpm install`。
|
|
114
147
|
|
|
115
|
-
>
|
|
116
|
-
>
|
|
148
|
+
> **四种方式装完都要重启 Harness 才生效。**
|
|
149
|
+
> 从 npm / GitHub 安装时不需要额外提供任何 `@deepseek-ai/*` 包——宿主半的运行时依赖只有 `@deepseek-ai/schemastery`(npm 上有)。
|
|
117
150
|
|
|
118
|
-
|
|
151
|
+
### 卸载
|
|
119
152
|
|
|
120
|
-
|
|
153
|
+
```sh
|
|
154
|
+
dsh plugin --profile web remove dsh-notice-center
|
|
155
|
+
```
|
|
121
156
|
|
|
122
|
-
|
|
157
|
+
移除依赖与 `dsh.profile.bundles` 条目后重启,即无残留。
|
|
123
158
|
|
|
124
|
-
|
|
125
|
-
|
|
159
|
+
## 上手三步
|
|
160
|
+
|
|
161
|
+
1. **打开设置页** —— 设置 → **通知中心**(侧栏导航项)
|
|
162
|
+
2. **开启系统通知** —— 打开「系统通知」总开关,浏览器会弹出授权请求,选「允许」。被拒绝时设置页会提示,需要到浏览器的**站点设置**里恢复(浏览器不会二次弹窗)
|
|
163
|
+
3. **按需微调** —— 展开两个分组:调颜色、选音效、调音量、开关「前台提醒」/「自动隐藏」
|
|
164
|
+
|
|
165
|
+
<img src="docs/images/settings.png" width="440" alt="通知中心设置页">
|
|
166
|
+
|
|
167
|
+
之后你切走或最小化时,会话跑完 / 有事等你都会收到通知,点通知直接跳回那个会话。
|
|
168
|
+
|
|
169
|
+
## 设置项详解
|
|
170
|
+
|
|
171
|
+
| 分组 | 设置项 | 配置键 | 默认值 | 说明 |
|
|
172
|
+
|---|---|---|---|---|
|
|
173
|
+
| 鲸鱼状态灯 | 总开关 | `colorsEnabled` | 开 | 关闭后保持官方原版图标 |
|
|
174
|
+
| | 完成 | `green` | `#22C55E` | 完成状态灯颜色(官方侧边栏色) |
|
|
175
|
+
| | 待处理 | `amber` | `#F59E0B` | 待处理状态灯颜色(官方侧边栏色) |
|
|
176
|
+
| | 默认色 | `black` | 未设置 | 不设置=沿用官方原版 `/favicon.svg` |
|
|
177
|
+
| 系统通知 | 总开关 | `notifyEnabled` | **关** | opt-in;开启需浏览器授权 |
|
|
178
|
+
| | 自动隐藏 | `notifyAutoHide` | 开 | 关=通知常驻屏幕,需手动关闭 |
|
|
179
|
+
| | 前台提醒 | `notifyForeground` | 关 | 开=你正看着页面时也提醒 |
|
|
180
|
+
| | 提示音 | `notifySound` | 开 | 开启后静音系统提示音,改播插件音效 |
|
|
181
|
+
| | 音量 | `notifyVolume` | `0.6` | 0–1 |
|
|
182
|
+
| | 完成提示音 | `notifyDoneSound` | `builtin-up` | 47 个可选 |
|
|
183
|
+
| | 待处理提示音 | `notifyPendingSound` | `builtin-down` | 47 个可选 |
|
|
184
|
+
|
|
185
|
+
配置由 Harness 的 settings 服务持久化到 `<DSH_HOME>/settings.yaml` 的 `notice-center:` 段。
|
|
186
|
+
设置页**最上方**还有一行页脚:插件名 + 版本号(如「通知中心 v1.3.0」),点一下在新标签页打开仓库。
|
|
187
|
+
|
|
188
|
+
## 常见问题
|
|
189
|
+
|
|
190
|
+
| 现象 | 原因与处理 |
|
|
191
|
+
|---|---|
|
|
192
|
+
| 设置里找不到「通知中心」 | 插件没加载成功。确认装完**重启过 Harness**,再看 Harness 控制台有没有 `settings namespace "notice-center" registered` 这行日志 |
|
|
193
|
+
| 开关点了没反应 / 加了新设置项不生效 | 改过**宿主半**(`lib/index.mjs`)后必须重启 Harness——设置写入要过宿主 schema 校验 |
|
|
194
|
+
| 完全不弹通知 | 三个条件缺一不可:① 系统通知总开关已打开;② 浏览器权限为「允许」;③ 触发时页面不在前台(默认策略,可用「前台提醒」放宽) |
|
|
195
|
+
| 用局域网 IP 打开时不弹 | 非安全上下文,浏览器禁用 Notification API,系统通知**完全不可用**(与「前台提醒」无关)。改用 `127.0.0.1` / `localhost`;局域网下只能靠标签页状态灯 |
|
|
196
|
+
| 标签页关掉后收不到 | 已知限制,见下文 |
|
|
197
|
+
| 刷新页面后绿灯变回黑色 | 正常。该状态是官方信号的内存态,刷新即丢 |
|
|
198
|
+
| 通知一闪而过看不到 | 关掉「自动隐藏」,通知会常驻直到你点掉它 |
|
|
199
|
+
| 没有声音 | 依次查:提示音开关、音量是否为 0、系统音效路由是否可用(不可用时会回退内置合成音,不会完全没声) |
|
|
200
|
+
| 通知被系统吞掉 | Windows「专注助手 / 勿扰」会拦截,插件无法感知 |
|
|
201
|
+
|
|
202
|
+
## 已知限制
|
|
203
|
+
|
|
204
|
+
- **浏览器标签页关闭后收不到通知**——Web Push 需要服务端 + HTTPS,本地插件场景不现实
|
|
205
|
+
- **非安全上下文(局域网 IP)下 Notification API 不可用**
|
|
206
|
+
- **刷新页面后绿色状态丢失**——该状态只存在内存中,属官方信号本身的行为
|
|
207
|
+
- **系统级勿扰模式会吞掉通知**(Windows 专注助手等),插件无法感知
|
|
208
|
+
- **自动隐藏的时长不可配**——Web Notification 规范没有时长参数,桌面端由系统决定(约 5–20 秒)
|
|
209
|
+
- **通知卡片的外观不可定制**——卡片由浏览器渲染,没有 CSS / 主题入口,只能控制标题、正文与图标
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
# 开发文档
|
|
214
|
+
|
|
215
|
+
> 以下是给维护者的内容:架构、本地开发、测试与发版。只想用插件的读者可以到此为止。
|
|
216
|
+
|
|
217
|
+
## 架构总览
|
|
218
|
+
|
|
219
|
+
插件由**两半**组成,各自跑在不同进程里,通过 Harness 的 settings 服务交换配置:
|
|
220
|
+
|
|
221
|
+
| 组成 | 文件 | 跑在哪 | 职责 |
|
|
222
|
+
|---|---|---|---|
|
|
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` |
|
|
226
|
+
|
|
227
|
+
**宿主半的两个「可选通道」**:
|
|
228
|
+
|
|
229
|
+
- `ctx.inject(["settings"], …)` —— 注册 schema。注册成功会在控制台留一行自证日志
|
|
230
|
+
- `ctx.inject(["webServer"], …)` —— 挂载音效路由。服务缺席时不挂载,浏览器半自动回退到内置合成音
|
|
231
|
+
|
|
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 干净安装」的关键。
|
|
233
|
+
|
|
234
|
+
**浏览器半的数据来源**(都是官方客户端服务,插件不自持状态):
|
|
235
|
+
|
|
236
|
+
| 服务 | 提供包 | 用途 |
|
|
237
|
+
|---|---|---|
|
|
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, … }>` |
|
|
240
|
+
|
|
241
|
+
音效路由的安全约束:只放行 `^[a-z0-9-]+\.mp3$` 的文件名(杜绝路径穿越),音频是随包常量故用 `immutable` 长缓存。
|
|
242
|
+
|
|
243
|
+
## 目录结构
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
package.json 包元数据(scripts: test / test:host / test:client)
|
|
247
|
+
cordis.patch.yml bundle patch 层(insert id: notice-center)
|
|
248
|
+
lib/index.mjs 宿主半:settings schema + 音效静态路由
|
|
249
|
+
lib/client.cjs 浏览器半:favicon 状态机 + 通知状态机 + 音效库 + 设置页
|
|
250
|
+
assets/audio/*.mp3 45 个 opencode 音效(MIT,来源见 assets/audio/README.md)
|
|
251
|
+
docs/images/ README 截图、主图与社交预览图
|
|
252
|
+
test/host-half.smoke.mjs 宿主半冒烟(164 行)
|
|
253
|
+
test/client-half.smoke.mjs 浏览器半冒烟:状态机 + 设置页真渲染(687 行)
|
|
254
|
+
.github/workflows/test.yml CI:push / PR 跑 pnpm test
|
|
255
|
+
.github/workflows/publish.yml 发布:打 tag 触发,OIDC 无令牌
|
|
126
256
|
```
|
|
127
257
|
|
|
128
|
-
|
|
258
|
+
## 一条通知的生命周期
|
|
129
259
|
|
|
130
|
-
|
|
260
|
+
```
|
|
261
|
+
sessions.list 变化
|
|
262
|
+
└─ sync()
|
|
263
|
+
├─ trackEdges() 运行状态 true→false 边沿(当前会话在前台跑完的补偿路径)
|
|
264
|
+
├─ detectTransitions() 官方 completed false→true;pendingInteractions 新增会话
|
|
265
|
+
│ └─ queueNotification(kind, sessionId, label, typeLabel)
|
|
266
|
+
│ ├─ 总开关关闭 → 丢弃
|
|
267
|
+
│ ├─ 同一次完成已入队 → 丢弃(两条检测路径不得重复发)
|
|
268
|
+
│ └─ 入队 + 300ms 聚合窗口
|
|
269
|
+
└─ targetOf() → favicon 换成 绿 / 琥珀 / 默认
|
|
270
|
+
|
|
271
|
+
300ms 后 flushNotifications()
|
|
272
|
+
├─ 权限未授权 → 丢弃
|
|
273
|
+
├─ 页面在前台且未开「前台提醒」→ 丢弃
|
|
274
|
+
├─ 按 kind 分组 → 每组一条 Notification(标题=会话名,正文=通知类型)
|
|
275
|
+
└─ 播放所选音效
|
|
276
|
+
```
|
|
131
277
|
|
|
132
|
-
|
|
133
|
-
- 系统通知需要**安全上下文**:`http://127.0.0.1:3080` 或 `localhost` 可用;用**局域网 IP** 访问时浏览器 Notification API 不可用
|
|
278
|
+
通知**不设置 `tag`**:同一会话反复跑完会复用同一个 tag,Windows/Chrome 会把后一条当成「更新已有通知」而静默替换、不再弹横幅。
|
|
134
279
|
|
|
135
|
-
##
|
|
280
|
+
## 本地开发
|
|
136
281
|
|
|
137
282
|
```sh
|
|
138
|
-
|
|
283
|
+
pnpm install # 唯一运行时依赖是 @deepseek-ai/schemastery
|
|
284
|
+
npm test # 宿主半 + 浏览器半冒烟
|
|
139
285
|
```
|
|
140
286
|
|
|
141
|
-
|
|
287
|
+
**重启要求分两半——这是本项目最容易踩的坑:**
|
|
142
288
|
|
|
143
|
-
|
|
289
|
+
| 改了什么 | 要不要重启 Harness | 为什么 |
|
|
290
|
+
|---|---|---|
|
|
291
|
+
| `lib/client.cjs`(浏览器半) | **不用**,刷新页面最干净 | 客户端 HMR 会 stat-poll 到 bundle 变化并通知浏览器重载该插件 |
|
|
292
|
+
| `lib/index.mjs`(宿主半) | **必须重启** | 宿主半热重载依赖 `cordis-plugin-hmr`(需 loader 带 `--expose-internals`),实测未生效。**改 schema(增删设置项)同样必须重启**——否则设置页写入过不了校验,表现为「开关点不动」 |
|
|
293
|
+
| 首次安装、任何改名 | **必须重启** | 插件树与 bundle 列表变了 |
|
|
144
294
|
|
|
145
|
-
|
|
146
|
-
- 非安全上下文(局域网 IP)下 Notification API 不可用
|
|
147
|
-
- 刷新页面后绿色状态丢失:该状态只存在内存中,刷新即丢,属官方信号本身的行为
|
|
148
|
-
- 系统级勿扰模式(Windows 专注助手等)会吞掉通知,插件无法感知
|
|
295
|
+
调试提示:浏览器半的报错都在 DevTools Console;宿主半注册成功会在 Harness 控制台打一行 `dsh-notice-center: settings namespace "notice-center" registered`。
|
|
149
296
|
|
|
150
|
-
##
|
|
297
|
+
## 测试
|
|
151
298
|
|
|
152
299
|
```sh
|
|
153
|
-
npm test #
|
|
300
|
+
npm test # 两个都跑(CI 跑的也是这个)
|
|
154
301
|
npm run test:host # 只跑宿主半
|
|
155
302
|
npm run test:client # 只跑浏览器半
|
|
156
303
|
```
|
|
157
304
|
|
|
158
|
-
|
|
305
|
+
两个冒烟测试都是**零依赖、纯 Node**,不需要浏览器:
|
|
306
|
+
|
|
307
|
+
- **宿主半**(164 行):`apply()` 真执行一遍、断言只注入预期服务、schema 默认值与校验、音效路由(能取到 mp3 / 不存在 404 / 拒绝路径穿越 / 随包 45 个齐全)、settings.yaml 存量配置过 schema 且**逐项原样保留**
|
|
308
|
+
- **浏览器半**(687 行):桩掉 `window.__ModuleLoader__` / `document` / `Notification`,evaluate bundle 后取 factory,用假 `ctx` 装配 `apply()`,然后驱动状态跃迁断言 Notification 的构造参数;设置页用**记录型桩**真渲染(React 元素树),断言开关顺序、只写对应字段、折叠行为、页脚等
|
|
309
|
+
|
|
310
|
+
被测试钉死的不变量(改代码时别破坏):
|
|
311
|
+
|
|
312
|
+
- `lib/client.cjs` 里的 `PLUGIN_VERSION` / `PLUGIN_NAME` / `PLUGIN_REPO` 必须与 `package.json` 一致
|
|
313
|
+
- 音效库 `SOUND_PACKS` 必须与 `assets/audio/` 里的文件**一一对应**;新增音效要同步改 `lib/client.cjs` 与 `test/client-half.smoke.mjs` **两处**
|
|
314
|
+
- `zh` / `en` 两套文案的 key 集合必须一致
|
|
159
315
|
|
|
160
|
-
##
|
|
316
|
+
## 发版
|
|
161
317
|
|
|
162
|
-
发布由 **tag 触发**的 [
|
|
318
|
+
发布由 **tag 触发**的 [`publish.yml`](.github/workflows/publish.yml) 负责——日常 commit / push **不会发包**:
|
|
163
319
|
|
|
164
320
|
| 意图 | 命令 | 结果 |
|
|
165
321
|
|---|---|---|
|
|
166
|
-
|
|
|
167
|
-
| 预发布(给人试用) | 版本改 `1.3.0-rc.1` → commit → `git tag v1.3.0-rc.1 && git push origin v1.3.0-rc.1` | 发到 **`next`**
|
|
322
|
+
| 只提交(功能没验证完) | `git commit` + `git push` | 只跑 `test.yml`,npm 上毫无动静 |
|
|
323
|
+
| 预发布(给人试用) | 版本改 `1.3.0-rc.1` → commit → `git tag v1.3.0-rc.1 && git push origin v1.3.0-rc.1` | 发到 **`next`** 通道,`latest` 不变 |
|
|
168
324
|
| 正式发版 | 版本改 `1.3.0` → commit → `git tag v1.3.0 && git push origin v1.3.0` | 发到 **`latest`**,自动附带 provenance |
|
|
169
325
|
| 只验证发布流程 | Actions → publish → Run workflow | 装依赖 + 跑测试 + `npm pack --dry-run`,**不发布** |
|
|
170
326
|
|
|
171
|
-
|
|
327
|
+
工作流内建**三道闸**,任一不过就发不出去:
|
|
172
328
|
|
|
173
|
-
|
|
329
|
+
1. tag 与 `package.json` 版本必须一致
|
|
330
|
+
2. tag 指向的提交必须已在 `main` 上
|
|
331
|
+
3. 测试必须通过
|
|
332
|
+
|
|
333
|
+
另外发布步骤**幂等**:该版本已在 registry 上就跳过,重复打 tag / 重跑工作流不会红。
|
|
174
334
|
|
|
175
|
-
|
|
335
|
+
### 一次性配置:npm Trusted Publishing (OIDC)
|
|
176
336
|
|
|
177
|
-
|
|
337
|
+
发布不需要任何 npm 令牌(也就没有令牌过期 / 泄漏问题)。打开包管理页 → **Publishing Access** → *Trusted publishers* → **Add a trusted publisher** → 选 **GitHub Actions**:
|
|
178
338
|
|
|
179
339
|
<https://www.npmjs.com/package/dsh-notice-center/access>
|
|
180
340
|
|
|
@@ -186,31 +346,32 @@ CI 在 push / PR 时跑同一套(`.github/workflows/test.yml`)。
|
|
|
186
346
|
| Environment name | `npm`(与工作流里的 `environment:` 一致) |
|
|
187
347
|
| **Allowed actions → Allow npm publish** | ✅ **必须勾** |
|
|
188
348
|
|
|
189
|
-
> ⚠️ 那个勾选框是 npm
|
|
349
|
+
> ⚠️ 那个勾选框是 npm 的新行为:trusted publisher **默认只允许 `npm stage publish`(暂存)**,不勾 *Allow npm publish* 的话,工作流里的 `npm publish` 会被拒。
|
|
190
350
|
> 若你更想要「只能暂存、必须人工在浏览器批准才真发布」,就别勾,并把工作流末步改成 `npm stage publish`。
|
|
191
351
|
|
|
192
352
|
- 走 OIDC 发布时 npm **自动附带 provenance**(无需 `--provenance`),包页会显示来源证明
|
|
193
353
|
- 想要「tag 推上去还要人工点一下才发」:仓库 Settings → Environments → `npm` → 加 **Required reviewers**;不加也能正常发布
|
|
194
|
-
- 本机手动发布仍可用 granular token
|
|
354
|
+
- 本机手动发布仍可用 granular token 兜底:在项目根放一个 `.npmrc` 写 `//registry.npmjs.org/:_authToken=<token>`。该文件已在 `.gitignore` 里,**绝不要提交**
|
|
195
355
|
|
|
196
|
-
##
|
|
356
|
+
## 设计约束
|
|
197
357
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
358
|
+
改代码前请先读这几条,它们都是踩过坑换来的:
|
|
359
|
+
|
|
360
|
+
1. **通知不带 `tag`**——同 tag 会被系统当成「更新已有通知」静默替换(详见上文「一条通知的生命周期」)
|
|
361
|
+
2. **「前台」判定是 `标签页可见 && 窗口有焦点`**——只看 `visibilityState` 会把「浏览器被别的应用压在后面」误判成正在看,而那恰恰是最该提醒的时候
|
|
362
|
+
3. **状态灯琥珀优先于绿**——与官方 `sessionStatuses` 一致,待处理是首要状态
|
|
363
|
+
4. **通知时长来自观察 `running` 边沿**——刷新页面时已经在跑的会话算不出起点,就不显示这个数(不猜)
|
|
364
|
+
5. **宿主半保持零 `@deepseek-ai` 运行时依赖**——只 import `schemastery`,否则 registry 安装会因 peer 解析失败而挂掉
|
|
365
|
+
6. **音效路由只放行 `^[a-z0-9-]+\.mp3$`**——文件名直接参与 path join,必须白名单
|
|
366
|
+
|
|
367
|
+
## 反馈与贡献
|
|
368
|
+
|
|
369
|
+
- 问题与建议:<https://github.com/SCP-QQ/dsh-notice-center/issues>
|
|
370
|
+
- 改动请先跑 `npm test`;涉及设置项的改动记得同步 `test/*.smoke.mjs` 的断言
|
|
371
|
+
- 音效素材来自 [anomalyco/opencode](https://github.com/anomalyco/opencode)(MIT),替换或移除请同步 [`assets/audio/README.md`](./assets/audio/README.md)
|
|
209
372
|
|
|
210
373
|
## License
|
|
211
374
|
|
|
212
375
|
MIT,完整许可文本见 [`LICENSE`](./LICENSE)。
|
|
213
376
|
|
|
214
|
-
音效素材(`assets/audio/*.mp3`,45 个)取自 [anomalyco/opencode](https://github.com/anomalyco/opencode)
|
|
215
|
-
的 `packages/ui/src/assets/audio/`,上游以 MIT 许可发布,这里保留同样的许可与署名;
|
|
216
|
-
若上游调整授权,请同步替换或移除本目录(详见 [`assets/audio/README.md`](./assets/audio/README.md))。
|
|
377
|
+
音效素材(`assets/audio/*.mp3`,45 个)取自 [anomalyco/opencode](https://github.com/anomalyco/opencode) 的 `packages/ui/src/assets/audio/`,上游以 MIT 许可发布,这里保留同样的许可与署名;若上游调整授权,请同步替换或移除本目录。
|
package/lib/client.cjs
CHANGED
|
@@ -18,6 +18,13 @@ window.__ModuleLoader__.load({
|
|
|
18
18
|
* 保证浏览器 bundle 只 import 类型)。
|
|
19
19
|
*/
|
|
20
20
|
const SETTINGS_NAMESPACE = "notice-center";
|
|
21
|
+
/** 插件身份:设置页页脚展示用。版本号必须与 package.json 的 version 一致 ——
|
|
22
|
+
* 这份 bundle 是预构建产物,运行时读不到 package.json,漂移由宿主半冒烟测试
|
|
23
|
+
* (test/host-half.smoke.mjs)守护,别在发版流程之外单独改这里。 */
|
|
24
|
+
const PLUGIN_NAME = "dsh-notice-center";
|
|
25
|
+
const PLUGIN_VERSION = "1.3.1";
|
|
26
|
+
/** 插件仓库:页脚点击后在新标签页打开。 */
|
|
27
|
+
const PLUGIN_REPO = "https://github.com/SCP-QQ/dsh-notice-center";
|
|
21
28
|
/** 官方侧边栏状态点颜色(静态色板,明暗主题同值)。 */
|
|
22
29
|
const DEFAULT_GREEN = "#22C55E";
|
|
23
30
|
const DEFAULT_AMBER = "#F59E0B";
|
|
@@ -746,6 +753,14 @@ function playSound(id, volume, kind) {
|
|
|
746
753
|
flexDirection: "column"
|
|
747
754
|
},
|
|
748
755
|
children: [
|
|
756
|
+
/* 页脚占标题那一行:「鲸鱼状态灯」分组行之上,官方 .options 自身没有标题。
|
|
757
|
+
它不是设置项,所以不带 borderBottom,只是居中的一行小字链接。 */
|
|
758
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginFooter, {
|
|
759
|
+
name: t("nav"),
|
|
760
|
+
version: PLUGIN_VERSION,
|
|
761
|
+
repo: PLUGIN_REPO,
|
|
762
|
+
t
|
|
763
|
+
}),
|
|
749
764
|
sectionRow("colorsEnabled", t("groupColors"), t("colorsHint"), colorsEnabled, (next) => scope.set("colorsEnabled", next), colorsExpanded, () => setColorsExpanded((current) => !current), t("colorsExpand")),
|
|
750
765
|
colorsExpanded ? colorRow("green", t("greenLabel"), () => scope.unset("green")) : null,
|
|
751
766
|
colorsExpanded ? colorRow("amber", t("amberLabel"), () => scope.unset("amber")) : null,
|
|
@@ -775,6 +790,57 @@ function playSound(id, volume, kind) {
|
|
|
775
790
|
]
|
|
776
791
|
});
|
|
777
792
|
}
|
|
793
|
+
/**
|
|
794
|
+
* 设置页页脚(标题行):插件名 + 版本号,整块是打开仓库的链接。
|
|
795
|
+
*
|
|
796
|
+
* 位置:设置区第一项,即「鲸鱼状态灯」分组行上方。官方 .options 自身没有标题,
|
|
797
|
+
* 这一行就占标题位 —— 左对齐(justifyContent:flex-start,与分组标题文字同一条左边缘;
|
|
798
|
+
* 外层无左 padding,容器 .options 的 24px 就是共同左边界)。"贴底"那套
|
|
799
|
+
* (minHeight:100% + marginTop:auto)已随位置调整一并去掉。
|
|
800
|
+
* 独立组件而非内联,原因同 SoundPicker —— hover 态要 useState,而设置在
|
|
801
|
+
* WhaleSettingsSection 里多一个布尔 state 会污染冒烟测试对展开态 setter 的计数
|
|
802
|
+
* (test/client-half.smoke.mjs 断言「两个折叠分组各有一个展开态 setter」)。
|
|
803
|
+
*/
|
|
804
|
+
function PluginFooter({ name, version, repo, t }) {
|
|
805
|
+
const [hover, setHover] = (0, react.useState)(false);
|
|
806
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
807
|
+
"data-plugin": PLUGIN_NAME,
|
|
808
|
+
style: {
|
|
809
|
+
display: "flex",
|
|
810
|
+
alignItems: "center",
|
|
811
|
+
justifyContent: "flex-start",
|
|
812
|
+
padding: "2px 0 14px"
|
|
813
|
+
},
|
|
814
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
815
|
+
href: repo,
|
|
816
|
+
target: "_blank",
|
|
817
|
+
rel: "noreferrer",
|
|
818
|
+
title: t("footerRepoTitle"),
|
|
819
|
+
"aria-label": t("footerRepoTitle"),
|
|
820
|
+
onMouseEnter: () => setHover(true),
|
|
821
|
+
onMouseLeave: () => setHover(false),
|
|
822
|
+
onFocus: () => setHover(true),
|
|
823
|
+
onBlur: () => setHover(false),
|
|
824
|
+
style: {
|
|
825
|
+
display: "inline-flex",
|
|
826
|
+
alignItems: "center",
|
|
827
|
+
gap: 6,
|
|
828
|
+
color: hover ? "var(--dsw-alias-brand-primary)" : "var(--dsw-alias-label-tertiary)",
|
|
829
|
+
fontSize: 12,
|
|
830
|
+
lineHeight: "18px",
|
|
831
|
+
textDecoration: hover ? "underline" : "none",
|
|
832
|
+
cursor: "pointer"
|
|
833
|
+
},
|
|
834
|
+
children: [
|
|
835
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name }, "name"),
|
|
836
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
837
|
+
style: { fontVariantNumeric: "tabular-nums" },
|
|
838
|
+
children: "v" + version
|
|
839
|
+
}, "version")
|
|
840
|
+
]
|
|
841
|
+
})
|
|
842
|
+
});
|
|
843
|
+
}
|
|
778
844
|
//#endregion
|
|
779
845
|
//#region src/client.ts
|
|
780
846
|
const DEFAULT_HREF = "/favicon.svg";
|
|
@@ -1175,7 +1241,7 @@ trackEdges(state);
|
|
|
1175
1241
|
notifyEnabledHint: "会话完成及有交互等待处理时发送通知",
|
|
1176
1242
|
notifySound: "提示音",
|
|
1177
1243
|
notifyAutoHide: "自动隐藏",
|
|
1178
|
-
notifyAutoHideHint: "
|
|
1244
|
+
notifyAutoHideHint: "开启后自动收起;关闭后常驻屏幕,需手动关闭",
|
|
1179
1245
|
notifyForeground: "前台提醒",
|
|
1180
1246
|
notifyForegroundHint: "开启后,即使你正看着该页面也会提醒",
|
|
1181
1247
|
notifyVolume: "音量",
|
|
@@ -1187,6 +1253,7 @@ trackEdges(state);
|
|
|
1187
1253
|
soundBuiltinDown: "Chime Down",
|
|
1188
1254
|
permissionDenied: "已被浏览器拒绝,请在站点设置中恢复通知权限",
|
|
1189
1255
|
permissionUnsupported: "当前浏览器不支持系统通知",
|
|
1256
|
+
footerRepoTitle: "在 GitHub 上打开 dsh-notice-center 仓库",
|
|
1190
1257
|
notifyDoneTitle: "会话已完成",
|
|
1191
1258
|
notifyPendingTitle: "有交互等待处理",
|
|
1192
1259
|
notifyDuration: "本轮总用时 {duration}",
|
|
@@ -1218,7 +1285,7 @@ trackEdges(state);
|
|
|
1218
1285
|
notifyEnabledHint: "Sent when a session finishes or something awaits you",
|
|
1219
1286
|
notifySound: "Sound",
|
|
1220
1287
|
notifyAutoHide: "Auto hide",
|
|
1221
|
-
notifyAutoHideHint: "Off
|
|
1288
|
+
notifyAutoHideHint: "On: it fades away on its own. Off: it stays on screen until you close it",
|
|
1222
1289
|
notifyForeground: "Notify in the foreground",
|
|
1223
1290
|
notifyForegroundHint: "Also notify while you are looking at the page",
|
|
1224
1291
|
notifyVolume: "Volume",
|
|
@@ -1230,6 +1297,7 @@ trackEdges(state);
|
|
|
1230
1297
|
soundBuiltinDown: "Chime Down",
|
|
1231
1298
|
permissionDenied: "Blocked by the browser - re-enable notifications in the site settings",
|
|
1232
1299
|
permissionUnsupported: "This browser does not support notifications",
|
|
1300
|
+
footerRepoTitle: "Open the dsh-notice-center repository on GitHub",
|
|
1233
1301
|
notifyDoneTitle: "Session finished",
|
|
1234
1302
|
notifyPendingTitle: "Something awaits you",
|
|
1235
1303
|
notifyDuration: "turn took {duration}",
|