dsh-speak 1.8.1 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -54
- package/README.zh-CN.md +104 -52
- package/adapters/dsh/install.ps1 +32 -7
- package/adapters/dsh/speech-hook.js +195 -107
- package/client/client.js +59 -12
- package/cordis.patch.yml +29 -5
- package/docs/DESIGN.md +87 -23
- package/docs/DESIGN.zh-CN.md +71 -22
- package/package.json +2 -7
package/README.md
CHANGED
|
@@ -93,19 +93,28 @@ macOS:
|
|
|
93
93
|
|
|
94
94
|
DSH web app:
|
|
95
95
|
|
|
96
|
-
- Tested against **DSH 0.1.
|
|
97
|
-
handled here
|
|
98
|
-
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
96
|
+
- Tested against **DSH 0.1.7-rc.2**. Host/client APIs changed twice since 0.1.1,
|
|
97
|
+
both handled here:
|
|
98
|
+
- **0.1.7 replaced the settings provider API with Config projection**: the
|
|
99
|
+
settings service now reads each active Loader entry's own exported `Config`
|
|
100
|
+
schema and projects its *volatile* fields into the settings UI
|
|
101
|
+
(`ctx.settings.describe()` on the host, `ctx.configForms` in the browser).
|
|
102
|
+
`settings.register(namespace, schema, { base })` — the 1.6.0–1.8.x wiring —
|
|
103
|
+
is gone, and the browser service `settingsScope` was replaced by
|
|
104
|
+
`configForms`. This plugin therefore exports its schema as `Config` and the
|
|
105
|
+
settings namespace is its **entry id** (`dsh-speak`; a leftover
|
|
106
|
+
`speech-hook` row from 1.8.x is still bound, see below).
|
|
107
|
+
- **0.1.2** stopped putting Conversation target data into the Session snapshot
|
|
108
|
+
(the 🔊 button resolves the clicked message through the Chat target hook
|
|
109
|
+
`useChat`), and deleted `@deepseek-ai/dsh-settings`'s `installSettingsSection`
|
|
110
|
+
/ `settingsNamespace` helpers.
|
|
105
111
|
- The host floor lives where dsh-market reads it: `engines.dsh` in `package.json`
|
|
106
|
-
(`>=0.1.
|
|
112
|
+
(`>=0.1.7-rc.2`). The catalog card and its "compatible with current DSH" filter
|
|
107
113
|
read exactly that field, so the floor moves only after a release has been
|
|
108
114
|
verified against the new host.
|
|
115
|
+
- 1.8.2 requires the settings model introduced in 0.1.7: on an older host the
|
|
116
|
+
browser half would wait forever for the `configForms` service. Use 1.8.1 for
|
|
117
|
+
DSH ≤ 0.1.6.
|
|
109
118
|
|
|
110
119
|
## Install & quick start
|
|
111
120
|
|
|
@@ -113,18 +122,35 @@ DSH web app:
|
|
|
113
122
|
|
|
114
123
|
```powershell
|
|
115
124
|
# 1. install the plugin into your web profile (adds dsh-speak to
|
|
116
|
-
# ~/.dsh/profiles/web/package.json dependencies)
|
|
125
|
+
# ~/.dsh/profiles/web/package.json dependencies AND to dsh.profile.bundles)
|
|
117
126
|
dsh plugin --profile web add dsh-speak
|
|
118
127
|
|
|
119
|
-
# 2.
|
|
120
|
-
#
|
|
121
|
-
# -
|
|
122
|
-
#
|
|
123
|
-
#
|
|
128
|
+
# 2. that is all — the package ships its own bundle patch, which registers the
|
|
129
|
+
# `dsh-speak` entry. Do NOT also hand-write an `insert:` row for it:
|
|
130
|
+
# registering the same entry twice makes the id non-unique, and DSH's config
|
|
131
|
+
# editor then rejects every settings write with
|
|
132
|
+
# `settings/rejected: Configuration for "dsh-speak" is overridden by a home
|
|
133
|
+
# patch or command-line overlay` (speech keeps working, the settings page
|
|
134
|
+
# silently bounces).
|
|
135
|
+
#
|
|
136
|
+
# To pin options in YAML anyway, add ONLY this top-level row (the settings page
|
|
137
|
+
# edits it in place; `config` on a top-level row is the shape the editor
|
|
138
|
+
# supports — see "DSH plugin config"):
|
|
139
|
+
# - id: dsh-speak
|
|
140
|
+
# name: 'dsh-speak'
|
|
141
|
+
# config: {}
|
|
142
|
+
#
|
|
143
|
+
# The entry id IS the settings namespace on DSH >= 0.1.7: your options are
|
|
144
|
+
# stored under that key in this same file. A row left over from 1.8.x
|
|
145
|
+
# (id: speech-hook) keeps working — the browser half binds either id.
|
|
124
146
|
|
|
125
147
|
# 3. restart the DSH web app — replies are now announced automatically
|
|
126
148
|
```
|
|
127
149
|
|
|
150
|
+
> Installing from the in-app marketplace is the same path: it adds the package to
|
|
151
|
+
> `dsh.profile.bundles`. Only the manual installs below need hand-written rows.
|
|
152
|
+
> **One registration path per profile** — never both.
|
|
153
|
+
|
|
128
154
|
> **No pnpm?** `dsh plugin` forwards to pnpm, which is not installed on every
|
|
129
155
|
> machine. The exact same install can be done with npm directly:
|
|
130
156
|
>
|
|
@@ -137,6 +163,10 @@ dsh plugin --profile web add dsh-speak
|
|
|
137
163
|
> ```bash
|
|
138
164
|
> npm install --prefix "$HOME/.dsh/profiles/web" dsh-speak
|
|
139
165
|
> ```
|
|
166
|
+
>
|
|
167
|
+
> An `npm install` alone does **not** register the plugin: add either the bundle
|
|
168
|
+
> name to `~/.dsh/profiles/web/package.json` → `dsh.profile.bundles`, or the two
|
|
169
|
+
> rows from the file-install section — not both.
|
|
140
170
|
|
|
141
171
|
The engine ships inside the package (`node_modules/dsh-speak/engine/`), so no extra
|
|
142
172
|
copying is needed.
|
|
@@ -182,8 +212,11 @@ npm install --prefix "$HOME/.dsh/profiles/web" dsh-speak
|
|
|
182
212
|
|
|
183
213
|
# 2. register in ~/.dsh/profiles/web/cordis.patch.yml (bare package name — no file:/// URL):
|
|
184
214
|
# - insert:
|
|
185
|
-
# - id:
|
|
215
|
+
# - id: dsh-speak
|
|
186
216
|
# name: 'dsh-speak'
|
|
217
|
+
# - id: dsh-speak
|
|
218
|
+
# name: 'dsh-speak'
|
|
219
|
+
# config: {}
|
|
187
220
|
|
|
188
221
|
# 3. no restart needed — the patch watcher hot-reloads; replies are announced
|
|
189
222
|
# after the throttle (~1.5 s); tool-calling replies are announced at turn end
|
|
@@ -267,8 +300,8 @@ speak.ps1 -Text "…" -Volume 50 -Rate 1 -MaxChars 300 -LongTextMessage "本次
|
|
|
267
300
|
|
|
268
301
|
### DSH plugin config
|
|
269
302
|
|
|
270
|
-
**Either way works, and they stay in sync** (both write the same
|
|
271
|
-
document):
|
|
303
|
+
**Either way works, and they stay in sync** (both write the same profile patch
|
|
304
|
+
document — the settings service persists UI edits into the row it read them from):
|
|
272
305
|
|
|
273
306
|
1. **Web UI (1.7.0, recommended)**: a dedicated Settings → dsh-speak settings
|
|
274
307
|
page. Every option is editable and saved there (visible in `dsh --dump-config`,
|
|
@@ -277,41 +310,71 @@ document):
|
|
|
277
310
|
|
|
278
311
|
```yaml
|
|
279
312
|
# ~/.dsh/profiles/web/cordis.patch.yml
|
|
313
|
+
# TWO rows, and the shape matters: `insert` provides the entry, the TOP-LEVEL row
|
|
314
|
+
# carries the config the settings page edits. DSH's config editor rewrites a
|
|
315
|
+
# config in place only on a top-level row; a config nested inside the insert row
|
|
316
|
+
# (what 1.8.x profiles and the 1.8.2 notes first showed) makes the UI accept an
|
|
317
|
+
# edit, apply it live, and then silently roll it back on disk — the value reverts
|
|
318
|
+
# on the next boot.
|
|
280
319
|
- insert:
|
|
281
|
-
- id:
|
|
320
|
+
- id: dsh-speak # the entry id IS the settings namespace (0.1.7+)
|
|
282
321
|
name: 'dsh-speak'
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
322
|
+
- id: dsh-speak
|
|
323
|
+
name: 'dsh-speak'
|
|
324
|
+
config:
|
|
325
|
+
enabled: true # master switch: false silences everything
|
|
326
|
+
automaticSpeech: true # auto-speak final replies
|
|
327
|
+
queueAllMessages: false # true = enqueue every assistant message as it arrives
|
|
328
|
+
replayFullRead: false # true = manual replay skips the long-text truncation, reads everything
|
|
329
|
+
cleanMarkdownFormatting: true # convert Markdown to natural speech
|
|
330
|
+
readInlineCode: true # read inline code without backticks
|
|
331
|
+
codeBlocks: smart # all | smart | replace (fenced code blocks)
|
|
332
|
+
codeBlockMaxChars: 300 # smart-mode code block character limit
|
|
333
|
+
codeBlockReplacementText: 'You can see the code in our history.' # replace-mode text
|
|
334
|
+
throttleMs: 1500 # merge delay before announcing (ms)
|
|
335
|
+
engine: '' # engine path override; '' = auto-resolve
|
|
336
|
+
announceApprovals: true # speak approval requests
|
|
337
|
+
announceQuestions: true # speak ask_user_question content
|
|
338
|
+
stripApprovalPrefix: true # strip "escalate sandbox to ...: " prefix
|
|
339
|
+
questionGapMs: 2000 # pause between multiple question announcements (ms)
|
|
340
|
+
longTextMode: message # message | heading (speak largest md heading)
|
|
341
|
+
longTextMessage: '本次播报内容较长,请自行阅读。' # fixed prompt for message mode
|
|
342
|
+
maxChars: 300 # per-utterance ceiling (macOS default 0 = unlimited)
|
|
343
|
+
volume: 50 # Windows only
|
|
344
|
+
rate: 0 # 0 = engine default (Windows SAPI scale / macOS wpm)
|
|
345
|
+
# —— optional event announcements (1.6.0, all off by default) ——
|
|
346
|
+
announceTurnEnd: false # turn/end — "第 N 轮对话完成"
|
|
347
|
+
announceCommandDone: false # command/done — command finished/failed
|
|
348
|
+
announceGoalChange: false # goal/change — goal created/updated/completed
|
|
349
|
+
announceToolErrors: false # tool/result error — announce (english dropped)
|
|
350
|
+
announceTodoWrite: false # todo/write — todo list updated
|
|
310
351
|
```
|
|
311
352
|
|
|
312
353
|
> Resolution order: schema default → patch `config` → UI user settings. Fields
|
|
313
354
|
> written in YAML show up in the UI too. Platform note: `maxChars` defaults to
|
|
314
355
|
> 0 on macOS (`say` has no ceiling) and 300 on Windows (SAPI safe limit).
|
|
356
|
+
>
|
|
357
|
+
> Values outside the ranges in the table (`volume` 0-100, Windows `rate` -10..10)
|
|
358
|
+
> are clamped before they reach the engine — SAPI throws on an out-of-range
|
|
359
|
+
> `Volume`/`Rate`, which surfaces as silence and nothing else. A clamped value is
|
|
360
|
+
> logged as `settings 值超出范围,已钳制: <field> <given> -> <used>`. An explicit
|
|
361
|
+
> `0` is always honored (`volume: 0` silences, `maxChars: 0` means unlimited,
|
|
362
|
+
> `throttleMs: 0` announces without merging).
|
|
363
|
+
|
|
364
|
+
> **Keep the config on the top-level row.** It is not a style preference: with the
|
|
365
|
+
> `config:` block nested inside the `insert` row, the settings page still renders
|
|
366
|
+
> and the running plugin still obeys an edit, but DSH's config editor cannot rewrite
|
|
367
|
+
> that row — it appends a new top-level row and then rolls the write back, so the
|
|
368
|
+
> value silently reverts the next time dsh starts. Check with
|
|
369
|
+
> `python scripts/settings-ui-check.py`, which asserts the write lands in the patch.
|
|
370
|
+
|
|
371
|
+
> **Upgrading from 1.8.x:** edit the options through the settings page, or move
|
|
372
|
+
> your old `config:` block onto the new top-level row (see the shape above). Before
|
|
373
|
+
> 0.1.7 the options lived in a `dsh-speak:` section of `~/.dsh/settings.yaml`; that
|
|
374
|
+
> file was migrated to `settings.yaml.imported` by DSH, and a section whose name
|
|
375
|
+
> matched no Loader entry (1.8.x registered the namespace in code, so `dsh-speak:`
|
|
376
|
+
> matched nothing) was left behind. If you had custom values, copy them into that
|
|
377
|
+
> `config:` block — the entry id (`dsh-speak`) is now the key DSH looks for.
|
|
315
378
|
|
|
316
379
|
#### Option reference
|
|
317
380
|
|
|
@@ -391,11 +454,11 @@ You can tune behavior without forking, and your changes **survive `npm update`**
|
|
|
391
454
|
> run `node scripts/test-engine-static.js`.
|
|
392
455
|
|
|
393
456
|
```yaml
|
|
394
|
-
- insert
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
457
|
+
# edit the top-level row (NOT the insert row — see "DSH plugin config")
|
|
458
|
+
- id: dsh-speak
|
|
459
|
+
name: 'dsh-speak'
|
|
460
|
+
config:
|
|
461
|
+
engine: 'C:/Users/<you>/.dsh/hooks/my-speak.ps1' # or ~/.dsh/hooks/my-speak.sh on macOS
|
|
399
462
|
```
|
|
400
463
|
|
|
401
464
|
The plugin resolves the engine as `config.engine` → package engine → `~/.dsh/hooks/`,
|
|
@@ -415,6 +478,10 @@ You can tune behavior without forking, and your changes **survive `npm update`**
|
|
|
415
478
|
| `工具调用出错:Error: cannot read …` spoken | the "is this Chinese?" detail filter only checked for the presence of a CJK character, so a Chinese directory name inside an English error passed it (1.8.0 regression) | fixed in 1.8.0 — the detail now needs more Chinese characters than Latin letters |
|
|
416
479
|
| Emoji-heavy text silent | SAPI fails silently on emoji | already stripped by the engine |
|
|
417
480
|
| Plugin not loading | raw Windows path as plugin name | use the `file:///C:/…` URL form (installer does this) |
|
|
481
|
+
| Settings page missing after upgrade to 1.8.2 | profile patch row `disabled: true`, or the entry id is neither `dsh-speak` nor the legacy `speech-hook` | enable the row and use one of those two ids as its `id` |
|
|
482
|
+
| Settings page renders but every change bounces back | DSH < 0.1.7 (the `configForms` service replaced `settingsScope`) | update DSH, or stay on dsh-speak 1.8.1 |
|
|
483
|
+
| Every change bounces back on DSH 0.1.7+, and the plugin log says `已有实例在运行` | the entry is registered **twice** — e.g. the package is in `dsh.profile.bundles` *and* a hand-written `insert:` row exists. The id stops being unique, and the config editor rejects each write (`settings/rejected: Configuration for "dsh-speak" is overridden by a home patch or command-line overlay`); speech keeps working, which is what makes it confusing | keep exactly one registration path (see Option A), then restart |
|
|
484
|
+
| A setting applies immediately but is back to the old value after a restart | the `config:` block sits INSIDE the profile patch's `insert:` row — DSH's config editor rewrites config in place only on a top-level row, and silently rolls the nested write back (it still answers `ok: true`) | split it into the two-row shape from [DSH plugin config](#dsh-plugin-config); `python scripts/settings-ui-check.py` asserts the write lands |
|
|
418
485
|
| macOS: voice suddenly became "婷婷" | opening the "Spoken Content / Siri Voice" pane drifted the system voice | re-pick via Settings → Accessibility → Spoken Content → System Voice → ⓘ entry |
|
|
419
486
|
| macOS: no log at `/tmp` | `os.tmpdir()` is `/var/folders/.../T`, not `/tmp` | log is at `$TMPDIR/dsh-speech-hook.log` |
|
|
420
487
|
|
|
@@ -429,7 +496,7 @@ engine/ harness-agnostic speech engine (PowerShell + SAPI5 / ba
|
|
|
429
496
|
speech-summary.ps1 blocking reply-summary announcement
|
|
430
497
|
adapters/
|
|
431
498
|
dsh/ DSH web plugin + one-command installer
|
|
432
|
-
speech-hook.js session-event trigger (throttle/cancel + optional events + FIFO speech queue + WebSocket + settings
|
|
499
|
+
speech-hook.js session-event trigger (throttle/cancel + optional events + FIFO speech queue + WebSocket + Config-projected settings form)
|
|
433
500
|
install.ps1 copies + registers + backs up
|
|
434
501
|
claude-code/
|
|
435
502
|
stop-hook.ps1 Claude Code Stop hook trigger
|
|
@@ -442,7 +509,7 @@ scripts/ tests + manual dev helpers (not shipped in the npm pack
|
|
|
442
509
|
test-engine-longtext.js long-text guard contract for BOTH engines (speak.ps1 -DryRun / speak.sh's perl)
|
|
443
510
|
test-speech-hook.js host plugin: event triggers, queue, tool-error detail filter
|
|
444
511
|
test-client-bundle.js browser bundle: slot registration + component rendering
|
|
445
|
-
test-settings-integration.js
|
|
512
|
+
test-settings-integration.js Config-projection wiring (volatile refs + live writes) + removed-API guard
|
|
446
513
|
session-log-dump.js read a DSH session log (manual: what text reached the engine)
|
|
447
514
|
settings-ui-check.py Playwright UI check (manual: needs a running, authenticated dsh)
|
|
448
515
|
dsh-events-check.py Playwright disclosure check (manual)
|
package/README.zh-CN.md
CHANGED
|
@@ -82,35 +82,53 @@ harness 事件(DSH 会话事件 / Claude Code Stop hook / 任意方式)
|
|
|
82
82
|
|
|
83
83
|
### DSH 版本
|
|
84
84
|
|
|
85
|
-
- 已在 **DSH 0.1.
|
|
86
|
-
1.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
namespace
|
|
90
|
-
`
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
- 已在 **DSH 0.1.7-rc.2** 上验证。0.1.1 之后有两轮 host/客户端 API 变更,本插件均已适配:
|
|
86
|
+
- **0.1.7 把 settings provider API 换成了 Config 投影**:settings 服务改为读取每个
|
|
87
|
+
已激活 Loader 条目自己导出的 `Config` schema,把其中的 *volatile* 字段投影成设置
|
|
88
|
+
界面(宿主侧 `ctx.settings.describe()`,浏览器侧 `ctx.configForms`)。1.6.0–1.8.x
|
|
89
|
+
使用的 `settings.register(namespace, schema, { base })` 已删除,浏览器服务
|
|
90
|
+
`settingsScope` 也被 `configForms` 取代。因此本插件把 schema 作为 `Config` 导出,
|
|
91
|
+
而设置 namespace 就是它的**条目 id**(`dsh-speak`;1.8.x 遗留的 `speech-hook`
|
|
92
|
+
条目 id 依然可以绑定,见下)。
|
|
93
|
+
- **0.1.2** 起 Session snapshot 不再携带会话视图(Conversation target)数据——
|
|
94
|
+
🔊 按钮改为通过 Chat 目标的 hook `useChat` 取被点击消息的文本;同一版本还删除了
|
|
95
|
+
`@deepseek-ai/dsh-settings` 的 `installSettingsSection` / `settingsNamespace`。
|
|
94
96
|
- 宿主要求声明在 dsh-market 实际读取的位置:`package.json` 的 `engines.dsh`
|
|
95
|
-
(`>=0.1.
|
|
97
|
+
(`>=0.1.7-rc.2`)。市场卡片与「适配当前 DSH」筛选读的正是这个字段,因此只有在
|
|
96
98
|
某个宿主版本上实测通过后,这个下限才会移动。
|
|
99
|
+
- 1.8.2 依赖 0.1.7 引入的设置模型:在更老的宿主上浏览器半侧会一直等待
|
|
100
|
+
`configForms` 服务。DSH ≤ 0.1.6 请用 1.8.1。
|
|
97
101
|
|
|
98
102
|
## 安装与快速开始
|
|
99
103
|
|
|
100
104
|
### DSH — 方式 A:npm 插件(推荐)
|
|
101
105
|
|
|
102
106
|
```powershell
|
|
103
|
-
# 1. 把插件装进你的 web profile(会写入 ~/.dsh/profiles/web/package.json 的
|
|
107
|
+
# 1. 把插件装进你的 web profile(会写入 ~/.dsh/profiles/web/package.json 的
|
|
108
|
+
# dependencies **和** dsh.profile.bundles)
|
|
104
109
|
dsh plugin --profile web add dsh-speak
|
|
105
110
|
|
|
106
|
-
# 2.
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
111
|
+
# 2. 到此为止——包自带 bundle patch,条目(id: dsh-speak)由它注册。
|
|
112
|
+
# 不要再手写一行 insert:同一个条目注册两次会让 id 不唯一,DSH 的 config-editor
|
|
113
|
+
# 随后会拒绝每一次设置写入:
|
|
114
|
+
# settings/rejected: Configuration for "dsh-speak" is overridden by a home patch or command-line overlay
|
|
115
|
+
# (语音照常工作,只有设置页静默弹回——所以特别难查。)
|
|
116
|
+
#
|
|
117
|
+
# 如果仍想在 YAML 里固定选项,只加这一行**顶层行**(设置页就地改写它;
|
|
118
|
+
# config 放顶层行才是编辑器支持的形状,见「DSH 插件配置」):
|
|
119
|
+
# - id: dsh-speak
|
|
120
|
+
# name: 'dsh-speak'
|
|
121
|
+
# config: {}
|
|
122
|
+
#
|
|
123
|
+
# 0.1.7 起条目 id 就是设置 namespace:你的选项会以这个 key 存在同一个文件里。
|
|
124
|
+
# 1.8.x 遗留的 id(speech-hook)继续可用——浏览器半侧两个 id 都认。
|
|
110
125
|
|
|
111
126
|
# 3. 重启 DSH web 应用 — 之后回复会被自动播报
|
|
112
127
|
```
|
|
113
128
|
|
|
129
|
+
> 在应用内插件市场安装是**同一条路径**(会把包加进 `dsh.profile.bundles`);
|
|
130
|
+
> 只有下面两种手动安装才需要手写行。**一个 profile 只能有一种注册方式**,绝不能两种都有。
|
|
131
|
+
|
|
114
132
|
> **没有 pnpm?** `dsh plugin` 内部转发给 pnpm,并非所有机器都装了。可以用 npm
|
|
115
133
|
> 直接完成同样的安装:
|
|
116
134
|
>
|
|
@@ -123,6 +141,10 @@ dsh plugin --profile web add dsh-speak
|
|
|
123
141
|
> ```bash
|
|
124
142
|
> npm install --prefix "$HOME/.dsh/profiles/web" dsh-speak
|
|
125
143
|
> ```
|
|
144
|
+
>
|
|
145
|
+
> 单纯 `npm install` **不会**注册插件:要么把包名加进
|
|
146
|
+
> `~/.dsh/profiles/web/package.json` 的 `dsh.profile.bundles`,要么用文件安装那两行
|
|
147
|
+
> ——不要两者都做。
|
|
126
148
|
|
|
127
149
|
引擎随包分发(`node_modules/dsh-speak/engine/`),无需额外拷贝。
|
|
128
150
|
|
|
@@ -166,8 +188,11 @@ npm install --prefix "$HOME/.dsh/profiles/web" dsh-speak
|
|
|
166
188
|
|
|
167
189
|
# 2. 在 ~/.dsh/profiles/web/cordis.patch.yml 末尾注册(裸包名即可,无需 file:/// URL):
|
|
168
190
|
# - insert:
|
|
169
|
-
# - id:
|
|
191
|
+
# - id: dsh-speak
|
|
170
192
|
# name: 'dsh-speak'
|
|
193
|
+
# - id: dsh-speak
|
|
194
|
+
# name: 'dsh-speak'
|
|
195
|
+
# config: {}
|
|
171
196
|
|
|
172
197
|
# 3. 无需重启——patch 监视器会热更新;回复在节流后(约 1.5 秒)自动播报;
|
|
173
198
|
# 带工具调用的回复会在回合结束时补播最终回复
|
|
@@ -247,7 +272,8 @@ speak.ps1 -Text "…" -Volume 50 -Rate 1 -MaxChars 300 -LongTextMessage "本次
|
|
|
247
272
|
|
|
248
273
|
### DSH 插件配置
|
|
249
274
|
|
|
250
|
-
**两种改法,任选其一**(改 UI 或改 YAML
|
|
275
|
+
**两种改法,任选其一**(改 UI 或改 YAML 都落进同一份 profile patch——设置服务把 UI 的
|
|
276
|
+
修改写回它读到的那一行,彼此同步):
|
|
251
277
|
|
|
252
278
|
1. **Web UI(1.7.0,推荐)**:设置 → dsh-speak 设置独立设置页。所有配置项都能直接改并
|
|
253
279
|
保存(`dsh --dump-config` 可见、按 profile 隔离、升级不丢)。
|
|
@@ -255,41 +281,63 @@ speak.ps1 -Text "…" -Volume 50 -Rate 1 -MaxChars 300 -LongTextMessage "本次
|
|
|
255
281
|
|
|
256
282
|
```yaml
|
|
257
283
|
# ~/.dsh/profiles/web/cordis.patch.yml
|
|
284
|
+
# 必须写成两行:insert 行负责“提供条目”,顶层行承载设置页要改写的 config。
|
|
285
|
+
# DSH 的 config-editor 只会就地改写顶层行;config 嵌在 insert 里时,UI 会接受修改、
|
|
286
|
+
# 运行中的插件也会立刻生效,但这次写入随后会被回滚——下次启动 dsh 时值就悄悄变回去了。
|
|
258
287
|
- insert:
|
|
259
|
-
- id:
|
|
288
|
+
- id: dsh-speak # 0.1.7 起条目 id 就是设置 namespace
|
|
260
289
|
name: 'dsh-speak'
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
290
|
+
- id: dsh-speak
|
|
291
|
+
name: 'dsh-speak'
|
|
292
|
+
config:
|
|
293
|
+
enabled: true # 总开关:false 时完全不播报
|
|
294
|
+
automaticSpeech: true # 自动朗读最终回复
|
|
295
|
+
queueAllMessages: false # true = 所有 assistant 消息立即入队朗读(中间消息也读)
|
|
296
|
+
replayFullRead: false # true = 手动重播跳过超长文本截断,完整朗读
|
|
297
|
+
cleanMarkdownFormatting: true # Markdown 转自然语音
|
|
298
|
+
readInlineCode: true # 朗读行内代码(去掉反引号)
|
|
299
|
+
codeBlocks: smart # all | smart | replace(围栏代码块)
|
|
300
|
+
codeBlockMaxChars: 300 # smart 模式下的代码块字数上限
|
|
301
|
+
codeBlockReplacementText: 'You can see the code in our history.' # replace 时的替代文本
|
|
302
|
+
throttleMs: 1500 # 播报前的合并延迟(毫秒)
|
|
303
|
+
engine: '' # 引擎路径覆盖;'' = 自动解析
|
|
304
|
+
announceApprovals: true # 播报审批请求
|
|
305
|
+
announceQuestions: true # 播报 ask_user_question 提问内容
|
|
306
|
+
stripApprovalPrefix: true # 剥离审批原因里的 "escalate sandbox to ...: " 前缀
|
|
307
|
+
questionGapMs: 2000 # 多个提问播报之间的停顿(毫秒)
|
|
308
|
+
longTextMode: message # message | heading(念最大字号 markdown 标题)
|
|
309
|
+
longTextMessage: '本次播报内容较长,请自行阅读。' # message 模式下的固定提示语
|
|
310
|
+
maxChars: 300 # 引擎单次朗读字数上限(macOS 默认 0 = 不限)
|
|
311
|
+
volume: 50 # 仅 Windows
|
|
312
|
+
rate: 0 # 0 = 引擎默认(Windows SAPI 刻度 / macOS wpm)
|
|
313
|
+
# —— 可选事件播报(1.6.0,默认全关)——
|
|
314
|
+
announceTurnEnd: false # 回合结束("第 N 轮对话完成")
|
|
315
|
+
announceCommandDone: false # 命令完成/失败(command/done)
|
|
316
|
+
announceGoalChange: false # 目标创建/更新/完成(goal/change)
|
|
317
|
+
announceToolErrors: false # 工具调用出错时播报(英文详情截掉,tool/result)
|
|
318
|
+
announceTodoWrite: false # 待办列表更新(todo/write)
|
|
288
319
|
```
|
|
289
320
|
|
|
290
321
|
> 解析顺序:schema 默认值 → patch `config` → UI 用户设置。写进 YAML 的字段
|
|
291
322
|
> 同样出现在 UI 中。平台差异:`maxChars` 在 macOS 默认 0(`say` 无上限),
|
|
292
323
|
> Windows 默认 300(SAPI 安全上限)。
|
|
324
|
+
>
|
|
325
|
+
> 超出上表范围的取值(`volume` 0-100、Windows `rate` -10..10)会在送进引擎前被**钳制**
|
|
326
|
+
> ——SAPI 遇到越界的 `Volume`/`Rate` 会抛异常,表现就是"没声音且没有任何提示"。被钳制
|
|
327
|
+
> 时会记一行日志:`settings 值超出范围,已钳制: <字段> <给出值> -> <实际值>`。
|
|
328
|
+
> 显式写 `0` 一律尊重原意(`volume: 0` 静音、`maxChars: 0` 不限长、`throttleMs: 0` 不合并)。
|
|
329
|
+
|
|
330
|
+
> **`config` 必须写在顶层行上。** 这不是风格问题:如果把 `config:` 嵌在 `insert`
|
|
331
|
+
> 行里,设置页照样能显示、改完运行中的插件也立刻生效,但 DSH 的 config-editor
|
|
332
|
+
> 无法就地改写那一行——它会追加一个顶层行再把写入回滚,于是下次启动 dsh 时值会
|
|
333
|
+
> 悄悄变回去。可以用 `python scripts/settings-ui-check.py` 验证(它会断言写入真的
|
|
334
|
+
> 落进了 profile patch)。
|
|
335
|
+
|
|
336
|
+
> **从 1.8.x 升级:** 直接在设置页里改,或把旧的 `config:` 块挪到新的顶层行上(形状见上)。
|
|
337
|
+
> 0.1.7 之前选项存在 `~/.dsh/settings.yaml` 的 `dsh-speak:` 段里;该文件已被 DSH 迁移为
|
|
338
|
+
> `settings.yaml.imported`,而段名对不上任何 Loader 条目的段(1.8.x 是在代码里注册
|
|
339
|
+
> namespace,所以 `dsh-speak:` 谁也匹配不到)会被留在原地。如果你改过默认值,把那些值
|
|
340
|
+
> 抄进上面那个 `config:` 块即可——现在 DSH 找的 key 就是条目 id(`dsh-speak`)。
|
|
293
341
|
|
|
294
342
|
#### 选项说明
|
|
295
343
|
|
|
@@ -362,11 +410,11 @@ speak.ps1 -Text "…" -Volume 50 -Rate 1 -MaxChars 300 -LongTextMessage "本次
|
|
|
362
410
|
> 或跑 `node scripts/test-engine-static.js`。
|
|
363
411
|
|
|
364
412
|
```yaml
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
413
|
+
# 改的是顶层行(不是 insert 行——见「DSH 插件配置」)
|
|
414
|
+
- id: dsh-speak
|
|
415
|
+
name: 'dsh-speak'
|
|
416
|
+
config:
|
|
417
|
+
engine: 'C:/Users/<你>/.dsh/hooks/my-speak.ps1' # macOS 用 ~/.dsh/hooks/my-speak.sh
|
|
370
418
|
```
|
|
371
419
|
|
|
372
420
|
插件按 `config.engine` → 包内引擎 → `~/.dsh/hooks/` 的顺序解析引擎,所以你的副本
|
|
@@ -386,6 +434,10 @@ speak.ps1 -Text "…" -Volume 50 -Rate 1 -MaxChars 300 -LongTextMessage "本次
|
|
|
386
434
|
| 听到 `工具调用出错:Error: cannot read …` | "是否中文"的详情判据只检查"含有汉字",英文报错里夹着中文目录名就能骗过它(1.8.0 引入的回归) | 1.8.0 已修——详情需满足"汉字数量多于拉丁字母数量" |
|
|
387
435
|
| 含大量 emoji 的文本静默 | SAPI 遇到 emoji 会静默失败 | 引擎已自动剥离 |
|
|
388
436
|
| 插件加载失败 | 插件名用了 Windows 原始路径 | 改用 `file:///C:/…` URL 形式(安装脚本会自动处理) |
|
|
437
|
+
| 升级到 1.8.2 后设置页不出现 | profile patch 那行写着 `disabled: true`,或条目 id 既不是 `dsh-speak` 也不是旧 id `speech-hook` | 去掉 `disabled`,并把 `id` 改成两者之一 |
|
|
438
|
+
| 设置页能开、但每次改动都被弹回 | DSH < 0.1.7(`configForms` 已取代 `settingsScope`) | 升级 DSH,或继续用 dsh-speak 1.8.1 |
|
|
439
|
+
| DSH 0.1.7+ 上每次改动都被弹回,且插件日志出现 `已有实例在运行` | 条目被注册了**两次**(例如包既在 `dsh.profile.bundles` 里、又手写了一行 `insert:`):id 不再唯一,config-editor 会拒绝每次写入(`settings/rejected: Configuration for "dsh-speak" is overridden by a home patch or command-line overlay`);语音照常工作,所以特别难查 | 只保留一种注册方式(见「方式 A」),然后重启 |
|
|
440
|
+
| 改完立刻生效,但重启后变回旧值 | `config:` 块嵌在 profile patch 的 `insert:` 行里——DSH 的 config-editor 只会就地改写**顶层**行,嵌套写入会被静默回滚(仍然回 `ok: true`) | 按「DSH 插件配置」把 `config` 放到顶层行;`python scripts/settings-ui-check.py` 会断言写入真的落盘 |
|
|
389
441
|
| macOS:音色突然变成"婷婷" | 打开过"朗读内容 / Siri 声音"设置面板导致系统朗读声音漂移 | 系统设置 → 辅助功能 → 阅读与朗读 → 系统声音 → ⓘ 入口重新选择 |
|
|
390
442
|
| macOS:在 `/tmp` 找不到日志 | `os.tmpdir()` 是 `/var/folders/.../T`,不是 `/tmp` | 日志在 `$TMPDIR/dsh-speech-hook.log` |
|
|
391
443
|
|
|
@@ -400,7 +452,7 @@ engine/ 与 harness 无关的语音引擎(PowerShell + SAPI5
|
|
|
400
452
|
speech-summary.ps1 阻塞式回复总结播报
|
|
401
453
|
adapters/
|
|
402
454
|
dsh/ DSH web 插件 + 一键安装脚本
|
|
403
|
-
speech-hook.js 会话事件触发器(节流/取消 + 可选事件 + FIFO 语音队列 + WebSocket +
|
|
455
|
+
speech-hook.js 会话事件触发器(节流/取消 + 可选事件 + FIFO 语音队列 + WebSocket + Config 投影的设置表单)
|
|
404
456
|
install.ps1 拷贝 + 注册 + 备份
|
|
405
457
|
claude-code/
|
|
406
458
|
stop-hook.ps1 Claude Code Stop hook 触发器
|
|
@@ -413,7 +465,7 @@ scripts/ 测试 + 手动开发辅助脚本(不随 npm 包发
|
|
|
413
465
|
test-engine-longtext.js 两个引擎的长文守卫契约(speak.ps1 -DryRun / speak.sh 的 perl)
|
|
414
466
|
test-speech-hook.js 宿主插件:事件触发、队列、工具出错详情过滤
|
|
415
467
|
test-client-bundle.js 浏览器 bundle:slot 注册 + 组件渲染
|
|
416
|
-
test-settings-integration.js
|
|
468
|
+
test-settings-integration.js Config 投影接线(volatile 引用 + 实时写入)+ 已删除 API 回归守卫
|
|
417
469
|
session-log-dump.js 读取 DSH 会话日志(手动:看引擎究竟收到了什么文本)
|
|
418
470
|
settings-ui-check.py Playwright UI 检查(手动:需要运行中且已鉴权的 dsh)
|
|
419
471
|
dsh-events-check.py Playwright 折叠行检查(手动)
|
package/adapters/dsh/install.ps1
CHANGED
|
@@ -37,11 +37,28 @@ Copy-Item -Force (Join-Path $here 'speech-hook.js') $PluginsDir
|
|
|
37
37
|
$pluginUrl = 'file:///' + ((Join-Path $PluginsDir 'speech-hook.js') -replace '\\', '/' -replace ' ', '%20')
|
|
38
38
|
|
|
39
39
|
# ---------- 3. register in cordis.patch.yml ----------
|
|
40
|
+
# The entry id doubles as the settings namespace on DSH >= 0.1.7 (the settings
|
|
41
|
+
# service projects each entry's own Config), so the installer uses the id this
|
|
42
|
+
# package documents: dsh-speak. A profile still carrying the pre-1.8.2 row
|
|
43
|
+
# (id: speech-hook) is left alone — the browser half binds either id — so a
|
|
44
|
+
# re-run never registers the plugin twice.
|
|
45
|
+
#
|
|
46
|
+
# Two rows, deliberately: `insert` provides the entry, and the TOP-LEVEL row is the
|
|
47
|
+
# one the settings page persists into. DSH's config editor rewrites a `config` in
|
|
48
|
+
# place only on a top-level row; a `config` nested inside the insert is accepted by
|
|
49
|
+
# the UI, applied to the running plugin, and then silently rolled back on disk.
|
|
40
50
|
Write-Host "==> Registering plugin in cordis.patch.yml"
|
|
41
51
|
if (Test-Path $cordisPatch) {
|
|
42
52
|
$existing = Get-Content $cordisPatch -Raw -Encoding UTF8
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
$registered = $existing -match '(?m)^\s*- id:\s*dsh-speak\b'
|
|
54
|
+
$legacy = $existing -match '(?m)^\s*- id:\s*speech-hook\b'
|
|
55
|
+
if ($registered -or $legacy) {
|
|
56
|
+
if ($legacy -and -not $registered) {
|
|
57
|
+
Write-Host " an older 'id: speech-hook' row already registers dsh-speak — skipping."
|
|
58
|
+
Write-Host " (optional) rename that row's id to dsh-speak so your saved options live under the documented key."
|
|
59
|
+
} else {
|
|
60
|
+
Write-Host " dsh-speak already registered — skipping (nothing to do)."
|
|
61
|
+
}
|
|
45
62
|
Write-Host ""
|
|
46
63
|
Write-Host "Done. Restart the DSH web app to pick up the plugin."
|
|
47
64
|
exit 0
|
|
@@ -52,21 +69,29 @@ if (Test-Path $cordisPatch) {
|
|
|
52
69
|
Write-Host " backup -> $backup"
|
|
53
70
|
$block = @"
|
|
54
71
|
|
|
55
|
-
#
|
|
72
|
+
# dsh-speak: auto voice-announce assistant replies (installed by dsh-speak)
|
|
73
|
+
# insert = the entry itself; the top-level row is what the settings page edits.
|
|
56
74
|
- insert:
|
|
57
|
-
- id:
|
|
75
|
+
- id: dsh-speak
|
|
58
76
|
name: '$pluginUrl'
|
|
77
|
+
- id: dsh-speak
|
|
78
|
+
name: '$pluginUrl'
|
|
79
|
+
config: {}
|
|
59
80
|
"@
|
|
60
81
|
Add-Content -Path $cordisPatch -Value $block -Encoding UTF8
|
|
61
|
-
Write-Host " appended insert entry -> $cordisPatch"
|
|
82
|
+
Write-Host " appended insert + settings entry -> $cordisPatch"
|
|
62
83
|
} else {
|
|
63
84
|
New-Item -ItemType Directory -Force -Path (Split-Path $cordisPatch) | Out-Null
|
|
64
85
|
$content = @"
|
|
65
86
|
# dsh profile patch layer (created by dsh-speak installer)
|
|
66
|
-
#
|
|
87
|
+
# dsh-speak: auto voice-announce assistant replies
|
|
88
|
+
# insert = the entry itself; the top-level row is what the settings page edits.
|
|
67
89
|
- insert:
|
|
68
|
-
- id:
|
|
90
|
+
- id: dsh-speak
|
|
69
91
|
name: '$pluginUrl'
|
|
92
|
+
- id: dsh-speak
|
|
93
|
+
name: '$pluginUrl'
|
|
94
|
+
config: {}
|
|
70
95
|
"@
|
|
71
96
|
Set-Content -Path $cordisPatch -Value $content -Encoding UTF8
|
|
72
97
|
Write-Host " created -> $cordisPatch"
|