dsh-speak 1.7.4 → 1.8.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/docs/DESIGN.md CHANGED
@@ -94,8 +94,13 @@ Processing pipeline (in order):
94
94
  3. **Strip emoji / non-printable** — keep CJK, CJK punctuation, full-width ranges,
95
95
  ASCII printable (regex `[^一-龥 -〿＀-￯ - -~]`).
96
96
  4. **Collapse whitespace.**
97
- 5. **Length guard** — if cleaned text exceeds `MaxChars` (default 300), replace with
98
- `LongTextMessage` (default: `本次播报内容较长,请自行阅读。`).
97
+ 5. **Length guard** — over `MaxChars` (default 300) the text is handled by
98
+ `LongTextMode`: `message` (the default) replaces it with `LongTextMessage`
99
+ (default: `本次播报内容较长,请自行阅读。`); `heading` speaks the largest
100
+ markdown heading, or — when the text has no heading at all — a coherent
101
+ opening: the leading `MaxChars` window trimmed back to its last sentence end
102
+ (that fallback used to speak only the first line, which sounded like the
103
+ narration was cut off).
99
104
  6. **Speak** — `System.Speech.Synthesis.SpeechSynthesizer`, volume/rate applied,
100
105
  best zh natural voice selected, then `Speak()`.
101
106
 
@@ -135,14 +140,20 @@ no "reply finished" hook, so the plugin observes the session event stream:
135
140
  - **Optional event announcements** (1.6.0, all off by default): `turn/end`,
136
141
  `command/done`, `goal/change`, `tool/result` (on error), and `todo/write` each
137
142
  have an independent toggle and announce a fixed phrase on fire (see §5).
138
- - **Settings namespace registration** (1.6.0): one timer tick after apply the
139
- plugin calls `installSettingsSection(ctx, 'dsh-speak', schema, patchConfig,
140
- hooks)`, resolving config as schema default → patch `config` → UI user layer.
141
- `onChange` re-derives `cfg` from a saved `settingsSource()` thunk (note:
142
- `installSettingsSection` only calls `setSource` on attach/detach, so changes
143
- must be re-read in `onChange`). On hosts without a settings service (dsh <
144
- 0.1.0-rc.7 or no provider mounted) the registration is skipped silently and
145
- the plugin works purely from the patch config — backward compatible.
143
+ - **Settings namespace registration** (1.6.0): the plugin wires its namespace
144
+ through the settings *service* — `ctx.inject(['settings'])` →
145
+ `settings.register('dsh-speak', schema, { base: patchConfig })` → re-derive
146
+ `cfg` from `scope.get()` on every `scope.watch` notification, and restore the
147
+ composed patch config when the fiber unloads. Resolution stays schema default
148
+ → patch `config` → UI user layer.
149
+ The plugin never imports `@deepseek-ai/dsh-settings`: DSH 0.1.2-alpha.1 deleted
150
+ the `installSettingsSection` / `settingsNamespace` helpers, and referencing
151
+ them is fatal — a missing named export is a module-evaluation error, and the
152
+ old lazy call threw `settingsNamespace is not a function` inside a timer
153
+ callback, which crashed the host (dsh exited 1 instead of booting). The service
154
+ itself never changed. On hosts without a settings service the inject callback
155
+ never runs and the plugin works purely from the patch config — graceful
156
+ degradation with no version check.
146
157
 
147
158
  Registration snippet (also automated by `install.ps1`; npm installs use the bare
148
159
  package name `'dsh-speak'` — this is the file-install path):
@@ -168,7 +179,13 @@ that registers two pieces of UI:
168
179
  Clicking 🔊 POSTs to `/dsh-speak/control` to replay that final message; clicking
169
180
  again stops; clicking another switches. The button's speaking/paused state is
170
181
  derived from the authoritative host state over the `/dsh-speak/ws` WebSocket
171
- (matched by session + turn identity).
182
+ (matched by session + turn identity). The replayed text is resolved through the
183
+ Chat target selector hook `useChat` (`@deepseek-ai/dsh-client-ui-chat` declares
184
+ it for every session-scoped slot): DSH 0.1.2 excluded Conversation target data
185
+ from the Session snapshot, so `useSession(s => s.chat.nodes)` no longer yields
186
+ the chat nodes. The two selectors return primitives only, because a fresh
187
+ object per read would churn the subscription, and a missing `useChat` prop
188
+ degrades the button to disabled instead of throwing inside its row.
172
189
  - **Settings → dsh-speak settings page** (1.7.0): registered into the
173
190
  `settings.section` slot, drawn with `@deepseek-ai/dsh-client-ui-primitives`
174
191
  (Button / DisclosureRow / Input; Toggle / Options / SettingInput helpers). Every
@@ -179,7 +196,12 @@ that registers two pieces of UI:
179
196
 
180
197
  - The package declares its browser half via `package.json`
181
198
  `dsh.client: { platform: 'web' }` + `exports['./client']`; DSH's client-modules
182
- scanner picks it up and loads it automatically.
199
+ scanner picks it up and loads it automatically. `dsh.client.inject` names the
200
+ package rows that DECLARE the two slots it occupies
201
+ (`@deepseek-ai/dsh-client-ui-chat`, `@deepseek-ai/dsh-client-ui-settings`) so
202
+ their factories arrive first; `dsh.client.external` lists
203
+ `@deepseek-ai/dsh-client-ui-primitives`, which the shell seeds in its static
204
+ module table.
183
205
  - **Deliberately handwritten, zero build**: it only uses platform seed modules
184
206
  and official primitives (the bundle-purity gate allows primitives but forbids
185
207
  importing official package internals), matching the built bundles' contract.
@@ -206,7 +228,7 @@ returns immediately. (Async spawning is safe here — the nested-spawn restricti
206
228
  | `turn/end` | 🟡 off by default; announces "第 N 轮对话完成/中断/异常结束" |
207
229
  | `command/done` | 🟡 off by default; announces "命令执行完成/失败" |
208
230
  | `goal/change` | 🟡 off by default; announces "已创建目标/目标已完成…" (head) |
209
- | `tool/result` | 🟡 off by default; announces "工具调用出错" only when `error` or an `isError` content block is present (English details / technical codes are dropped, Chinese details kept) |
231
+ | `tool/result` | 🟡 off by default; announces "工具调用出错" only for a structured failure (`error`, or a result block with `isError === true`). A non-zero shell exit is result data (`exit code: N`), not an error — pwsh/bash deliberately settle it as a completed call, so only infrastructure failures (spawn errors, aborts) and structured tool failures (e.g. fs) announce. Since 0.1.2 the `ToolResultBlock` wrapper nests the text under `content[]`, so the detail is read from there (English details / technical codes dropped, Chinese details kept) |
210
232
  | `todo/write` | 🟡 off by default; announces "待办已更新:n/m 完成" |
211
233
  | `assistant/message` (queueAllMessages on) | ✅ every message enqueued immediately (intermediate spoken too) |
212
234
  | manual replay (per-message 🔊) | ✅ clear queue → stop current → speak that turn |
@@ -223,7 +245,8 @@ returns immediately. (Async spawning is safe here — the nested-spawn restricti
223
245
  | `-Rate` | `1` | speech rate (SAPI scale) |
224
246
  | `-MaxChars` | platform | beyond this, replaced by `LongTextMessage` (macOS default 0 = unlimited) |
225
247
  | `-LongTextMessage`| `本次播报内容较长,请自行阅读。` | spoken instead of over-long text |
226
- | `-LongTextMode` | `message` | `message` (fixed prompt) \| `heading` (speak the largest markdown heading) |
248
+ | `-LongTextMode` | `message` | `message` (fixed prompt) \| `heading` (speak the largest markdown heading; **with no heading, speak a coherent opening**: the leading `MaxChars` window trimmed back to its last sentence end, kept whole when that would drop more than half the window. Full-width `。!?;…` always end a sentence; half-width `.!?;` only when followed by whitespace/a closing quote or bracket — read one character PAST the window for the last position, so an English `period + space` at the edge counts while `Version 0.1.` does not) |
249
+ | `-DryRun` | `0` | print the text that WOULD be spoken as UTF-8 on stdout and exit without audio (maintainer aid for diffing the cleaning pipeline and the long-text guard) |
227
250
  | `-CleanMarkdownFormatting` | `true` | convert Markdown to natural speech (link labels kept, URLs stripped) |
228
251
  | `-ReadInlineCode` | `true` | read inline code without backtick markers |
229
252
  | `-CodeBlocks` | `smart` | `all` \| `smart` \| `replace` (fenced code blocks) |
@@ -281,6 +304,7 @@ Full configuration guide: the README's Configuration section.
281
304
  | 6.6 | Reading/writing speech text as ANSI | mojibake or empty speech | always UTF-8 (`[System.IO.File]::ReadAllText(..., UTF8)`) |
282
305
  | 6.7 | A repo `.sh` checked out as CRLF by `core.autocrlf=true`; `npm pack` bundles the **working-tree** file | the published `speak.sh` dies in bash on macOS (`command not found`, `syntax error near {`), silent failure | `.gitattributes` pins `*.sh text eol=lf` (check `file engine/speak.sh` for CRLF before publishing) |
283
306
  | 6.8 | Log path hard-coded as `/tmp` | on macOS `os.tmpdir()` is `/var/folders/.../T`, the log is not at `/tmp` | look for the log at `os.tmpdir()` (= `$TMPDIR`) |
307
+ | 6.9 | Engine `.ps1` saved **without the UTF-8 BOM** (any editor or script that rewrites the file drops it — it is byte metadata, not content, and nothing in the file records the requirement) | Windows PowerShell 5.1 decodes the file with the system ANSI code page, so Chinese **literals in code** become mojibake: the emoji/CJK filter then drops real text (silent no audio) or the sentence-end classes stop matching (silent wrong trimming). Comments only look garbled | two rules: (a) the engine's CODE stays ASCII-only — PowerShell punctuation is built from `[char]` code points and ranges are written as `\u` escapes, and `speak.sh`'s Perl guard writes punctuation as `\x{...}` escapes (Perl source is bytes without `use utf8`, so a Chinese literal in a pattern is read as Latin-1 and matches nothing — a macOS-only "trims nothing" bug 1.8.0 shipped and `npm test` caught it on a real machine) — so a lost BOM only garbles comments and the two default prompts; (b) `scripts/test-engine-static.js` asserts the BOM on every `engine/*.ps1` plus a PowerShell parse check (also in `prepublishOnly`), and `scripts/test-engine-longtext.js` asserts the extracted Perl guard is ASCII-only |
284
308
 
285
309
  ## 7. Extending
286
310
 
@@ -327,6 +351,28 @@ dependencies in the profile). This repository is prepared for that path:
327
351
  Because the engine rides inside the npm package, `dsh plugin --profile web add
328
352
  dsh-speak` alone is sufficient — no separate copying step.
329
353
 
354
+ ### Host requirement (`engines.dsh`)
355
+
356
+ `package.json` declares `engines.dsh: >=0.1.5-rc.1`. That one field is what
357
+ dsh-market reads to label the catalog card (`DSH >=0.1.5-rc.1`) and to decide
358
+ whether the plugin survives its "compatible with current DSH" filter:
359
+
360
+ - the facts come from the package's npm `latest` manifest
361
+ (`{registry}/<pkg>/latest`, cached ~24 h) — the catalog YAML in
362
+ `awesome-dsh-plugin` has no host field, so a PR there cannot declare this;
363
+ - an `engines.dsh` value is an `engine` declaration. Lockstep `@deepseek-ai/dsh*`
364
+ **peers** count too, but non-lockstep host packages are skipped on purpose
365
+ (`@deepseek-ai/schemastery`, `@deepseek-ai/cordis` — the schemastery peer above
366
+ therefore declares nothing about the DSH version);
367
+ - all declarations are conjunctive and compared prerelease-aware, so
368
+ `>=0.1.5-rc.1` matches a `0.1.5-rc.1` host; a missing declaration shows up as
369
+ "host requirement undeclared", never as "incompatible";
370
+ - npm itself only enforces `engines.node` / `engines.npm`, so this key never
371
+ blocks an install — it is marketplace metadata;
372
+ - move the floor only after a release has been verified against the new host, and
373
+ update both READMEs with it: `scripts/test-manifest.js` asserts the same string
374
+ appears in `package.json`, `README.md` and `README.zh-CN.md`.
375
+
330
376
  ### Publish steps (maintainer)
331
377
 
332
378
  ```powershell
@@ -86,8 +86,11 @@ Agent 工具会跑长任务(构建、测试、迁移、批量修改),而
86
86
  3. **剥离 emoji / 不可打印字符** — 只保留中文汉字、中文标点、全角区间、
87
87
  ASCII 可打印(正则 `[^一-龥 -〿＀-￯ - -~]`)。
88
88
  4. **压缩空白。**
89
- 5. **长度守卫** — 清洗后文本超过 `MaxChars`(默认 300)时,替换为
90
- `LongTextMessage`(默认:`本次播报内容较长,请自行阅读。`)。
89
+ 5. **长度守卫** — 超过 `MaxChars`(默认 300)的文本由 `LongTextMode` 决定:
90
+ `message`(默认)替换为 `LongTextMessage`(默认:`本次播报内容较长,请自行
91
+ 阅读。`);`heading` 念最大字号的 markdown 标题,而**整段没有标题时**改念
92
+ "有头有尾的开头"——取开头 `MaxChars` 窗口并回退到窗口内最后一个句末标点
93
+ (旧实现这里只念第一个非空行,听感上就是"从第二行开始不念了")。
91
94
  6. **朗读** — `System.Speech.Synthesis.SpeechSynthesizer`,应用音量/语速,
92
95
  选择最佳 zh 自然语音,然后 `Speak()`。
93
96
 
@@ -119,13 +122,17 @@ Agent 工具会跑长任务(构建、测试、迁移、批量修改),而
119
122
  事件;开 = 每条 assistant 消息立即入队朗读(中间消息也读)。
120
123
  - **可选事件播报**(1.6.0,默认全关):`turn/end`、`command/done`、
121
124
  `goal/change`、`tool/result`(出错时)、`todo/write` 各自独立开关(见 §5)。
122
- - **settings namespace 注册**(1.6.0):apply 后在一个 timer tick 里调用
123
- `installSettingsSection(ctx, 'dsh-speak', schema, patchConfig, hooks)`,配置
124
- 解析为 schema 默认 → patch `config` → UI 用户设置三层。`onChange` 时通过
125
- `settingsSource()` 重新解析 cfg(注意:`installSettingsSection` 只在
126
- attach/detach 时调 `setSource`,变更时需自己在 `onChange` 里重读 source)。
127
- 若宿主没有 settings 服务(dsh < 0.1.0-rc.7 或未挂载 provider),注册静默
128
- 跳过,插件完全按 patch `config` 工作——向后兼容。
125
+ - **settings namespace 注册**(1.6.0):插件通过 settings **服务**接线——
126
+ `ctx.inject(['settings'])` → `settings.register('dsh-speak', schema, { base:
127
+ patchConfig })` → 每次 `scope.watch` 通知时从 `scope.get()` 重新解析 cfg,
128
+ fiber 卸载时恢复成组合层的 patch config。配置解析仍是 schema 默认 → patch
129
+ `config` → UI 用户设置三层。
130
+ 插件**不再 import** `@deepseek-ai/dsh-settings`:DSH 0.1.2-alpha.1 删除了
131
+ `installSettingsSection` / `settingsNamespace` 两个辅助导出,引用它们是致命
132
+ 的——缺失的具名导出会在模块求值期直接报错;旧代码在 timer 回调里懒调用,抛
133
+ `settingsNamespace is not a function` 把宿主打崩(dsh 直接退出 1,起不来)。
134
+ 服务本身从未变过。宿主没有 settings 服务时 inject 回调永不执行,插件完全按
135
+ patch `config` 工作——优雅退化,无需版本判断。
129
136
 
130
137
  注册片段(`install.ps1` 也会自动完成;npm 安装用裸包名 `'dsh-speak'` 即可,
131
138
  这是文件安装方式用的路径):
@@ -150,7 +157,12 @@ factory })`),注册两条 UI:
150
157
  `conversation.chat.assistant-actions` slot(该回合最终回复的操作栏)。点击 🔊
151
158
  调 `/dsh-speak/control` 重播该条最终回复,再点停止,点另一条切换;按钮状态
152
159
  (播放中/暂停)由 `/dsh-speak/ws` WebSocket 的 host 权威状态推导(session +
153
- turn 身份匹配)。
160
+ turn 身份匹配)。重播文本改为通过 Chat 目标的 selector hook `useChat` 取
161
+ (`@deepseek-ai/dsh-client-ui-chat` 为所有 session 作用域 slot 声明了它):
162
+ DSH 0.1.2 起 Session snapshot 不再携带 Conversation 目标数据,
163
+ `useSession(s => s.chat.nodes)` 已经取不到聊天节点。两个 selector 只返回原始值
164
+ (每次读返回新对象会让订阅反复失效),拿不到 `useChat` 时按钮退化为禁用,而不是
165
+ 在自己的操作栏里抛错。
154
166
  - **设置 → dsh-speak 设置独立设置页**(1.7.0):注册进 `settings.section` slot。
155
167
  用 `@deepseek-ai/dsh-client-ui-primitives` 的 Button/DisclosureRow/Input
156
168
  绘制(Toggle/Options/SettingInput 组件),所有配置项(总开关、自动朗读、
@@ -160,6 +172,10 @@ factory })`),注册两条 UI:
160
172
 
161
173
  - 包通过 `package.json` 的 `dsh.client: { platform: 'web' }` +
162
174
  `exports['./client']` 声明浏览器端;DSH 的 client-modules 扫描到后自动加载。
175
+ `dsh.client.inject` 列出**声明**它占用那两个 slot 的包 row
176
+ (`@deepseek-ai/dsh-client-ui-chat`、`@deepseek-ai/dsh-client-ui-settings`),
177
+ 保证它们的 factory 先到达;`dsh.client.external` 列出
178
+ `@deepseek-ai/dsh-client-ui-primitives`(shell 的静态模块表里已 seed)。
163
179
  - **刻意手写、零构建**:只用平台 seed 模块 + 官方 primitives(bundle-purity
164
180
  gate 允许用 primitives,禁止 import 官方包内部组件),与构建出来的 bundle
165
181
  契约一致。
@@ -184,7 +200,7 @@ Claude Code *确实*有 Stop hook。hook JSON(含 `transcript_path`)从 stdi
184
200
  | `turn/end`(回合结束) | 🟡 默认关;开则播报"第 N 轮对话完成/中断/异常结束" |
185
201
  | `command/done`(命令完成) | 🟡 默认关;开则播报"命令执行完成/失败" |
186
202
  | `goal/change`(目标变更) | 🟡 默认关;开则播报"已创建目标/目标已完成…(前 40 字)" |
187
- | `tool/result`(工具结果) | 🟡 默认关;开则仅当带 `error` 或 `isError` 内容块时播报"工具调用出错"(英文详情/技术 code 截掉,只保留中文详情) |
203
+ | `tool/result`(工具结果) | 🟡 默认关;开则仅对**结构化失败**播报"工具调用出错"(带 `error`,或结果块 `isError === true`)。shell 命令非零退出是结果数据(`exit code: N`)而非错误——pwsh/bash 有意按"已完成调用"结算,所以只有基础设施失败(spawn 错误、abort)和 fs 这类结构化失败才播报。0.1.2 起结果块被包进 `ToolResultBlock`,文字在其嵌套的 `content[]` 里(英文详情/技术 code 截掉,只保留中文详情) |
188
204
  | `todo/write`(待办更新) | 🟡 默认关;开则播报"待办已更新:n/m 完成" |
189
205
 
190
206
  | `assistant/message`(queueAllMessages 开)| ✅ 每条立即入队(中间消息也读) |
@@ -202,7 +218,8 @@ Claude Code *确实*有 Stop hook。hook JSON(含 `transcript_path`)从 stdi
202
218
  | `-Rate` | `1` | 语速(SAPI 刻度) |
203
219
  | `-MaxChars` | 平台相关 | 超过此长度时替换为 `LongTextMessage`(macOS 默认 0 = 不限) |
204
220
  | `-LongTextMessage` | `本次播报内容较长,请自行阅读。` | 超长文本时改念这句 |
205
- | `-LongTextMode` | `message` | `message`(固定提示语)\| `heading`(念最大字号 markdown 标题) |
221
+ | `-LongTextMode` | `message` | `message`(固定提示语)\| `heading`(念最大字号 markdown 标题;**整段没有标题时改念"有头有尾的开头"**:取开头 `MaxChars` 窗口并回退到窗口内最后一个句末标点,若这样会砍掉半个窗口以上则保留整窗。全角 `。!?;…` 无条件算句末;半角 `.!?;` 只在后面跟空白、右引号/右括号时才算——最后一位会**多读一位**判断,所以英文「句号+空格」在边缘照样算,`Version 0.1.` 这种小数点不算) |
222
+ | `-DryRun` | `0` | 把「将要朗读的文本」按 UTF-8 打到 stdout 后直接退出、完全不出声(调试清洗与长文守卫用) |
206
223
  | `-CleanMarkdownFormatting` | `true` | Markdown 转自然语音(保留链接文字去 URL) |
207
224
  | `-ReadInlineCode` | `true` | 朗读行内代码(去掉反引号) |
208
225
  | `-CodeBlocks` | `smart` | `all` \| `smart` \| `replace`(围栏代码块) |
@@ -259,6 +276,7 @@ config:
259
276
  | 6.6 | 用 ANSI 读写播报文本 | 乱码或完全无声 | 一律 UTF-8(`[System.IO.File]::ReadAllText(..., UTF8)`) |
260
277
  | 6.7 | 仓库内 `.sh` 被 git 按 `core.autocrlf=true` 检出为 CRLF,`npm pack` 打包的是**工作区**文件 | 发布包里的 `speak.sh` 在 macOS 上 bash 语法错误(`command not found`、`syntax error near {`),静默失败 | `.gitattributes` 里 `*.sh text eol=lf` 锁定 LF(发布前 `file engine/speak.sh` 确认无 CRLF) |
261
278
  | 6.8 | 日志路径写死 `/tmp` | macOS 上 `os.tmpdir()` 是 `/var/folders/.../T`,`/tmp` 里找不到日志 | 日志路径按 `os.tmpdir()`(= `$TMPDIR`)查找 |
279
+ | 6.9 | 引擎 `.ps1` 被**丢掉 UTF-8 BOM** 保存(任何重写文件的编辑器/脚本都会丢——BOM 是字节级元数据而不是内容,文件里没有任何东西记录"我需要 BOM") | Windows PowerShell 5.1 改用系统 ANSI 代码页解码,**代码里的中文标点**变乱码:emoji/CJK 过滤范围错乱会连中文一起丢掉(**静默无声**),句末标点类失配则修剪静默失效;中文注释只是显示为乱码 | 两条规则:(a) 引擎**代码保持纯 ASCII** ——PowerShell 的中文标点用 `[char]` 码位拼、范围写成 `\u` 转义,`speak.sh` 的 Perl 守卫也是同理用 `\x{...}` 码位转义(Perl 源码没有 `use utf8` 时按字节处理,模式里直接写中文会被当成 Latin-1 而**完全匹配不到**——这是 1.8.0 实际发布出去的 macOS 专属"完全不修剪"bug,由真机 `npm test` 抓出);这样丢 BOM 只影响注释和两个默认提示语;(b) `scripts/test-engine-static.js` 对每个 `engine/*.ps1` 断言 BOM + PowerShell 语法解析(也挂 `prepublishOnly`),`scripts/test-engine-longtext.js` 断言抽出的 Perl 守卫是纯 ASCII |
262
280
 
263
281
  ## 7. 扩展
264
282
 
@@ -303,6 +321,24 @@ DSH 的插件机制基于 Cordis,官方安装树外插件的路径是
303
321
  因为引擎随 npm 包分发,用户只需 `dsh plugin --profile web add dsh-speak`
304
322
  一条命令,无需额外拷贝。
305
323
 
324
+ ### 宿主要求(`engines.dsh`)
325
+
326
+ `package.json` 声明了 `engines.dsh: >=0.1.5-rc.1`。dsh-market 只读这一个字段来标注
327
+ 目录卡片(`DSH >=0.1.5-rc.1`)并决定插件能否通过「适配当前 DSH」筛选:
328
+
329
+ - 数据源是该包 npm `latest` manifest(`{registry}/<pkg>/latest`,缓存约 24 小时)
330
+ ——`awesome-dsh-plugin` 里那份目录 YAML 没有宿主字段,往那里提 PR 声明不了;
331
+ - `engines.dsh` 属于 `engine` 声明;同版本线的 `@deepseek-ai/dsh*` **peer** 也算,
332
+ 但非同一版本线的宿主包会被有意跳过(`@deepseek-ai/schemastery`、
333
+ `@deepseek-ai/cordis`)——所以上面那条 schemastery peer 不构成任何 DSH 版本声明;
334
+ - 所有声明取交集,比较时带 prerelease 语义,`>=0.1.5-rc.1` 能匹配 `0.1.5-rc.1`
335
+ 宿主;完全没声明只会显示「未声明宿主要求」,不会显示成不兼容;
336
+ - npm 本身只强制 `engines.node` / `engines.npm`,这个键不会挡住安装,它只是市场
337
+ 元数据;
338
+ - 只有在某个宿主版本上实测通过后才移动下限,并同步两份 README:
339
+ `scripts/test-manifest.js` 断言同一个字符串同时出现在 `package.json`、
340
+ `README.md`、`README.zh-CN.md`。
341
+
306
342
  ### 发布步骤(维护者)
307
343
 
308
344
  ```powershell