dsh-ppt-studio 1.1.0 → 1.1.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.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  在 DeepSeek Harness 里说需求,它把 PPT 做出来:先定大纲和版式,再逐页制作,每页过一遍自动检查,最后交付一个能用 PowerPoint 打开、继续改的 `.pptx`。
4
4
 
5
- 当前版本 **1.0.4**,安装包 **866 kB**(1.0.3 及更早的版本是 35 MB,因为随包带着 4 套没人用过的导入模板,1.0.4 把它们删了)。
5
+ 当前版本 **1.1.1**,安装包约 **890 kB**(1.0.3 及更早的版本是 35 MB,因为随包带着 4 套没人用过的导入模板,1.0.4 把它们删了)。
6
6
 
7
7
  装上之后,**这个 profile 的所有会话都能用**,不需要专门切到某个预设。预设「PPT 工作室」提供的是人格和身份,不是"开关"。
8
8
 
@@ -14,26 +14,32 @@
14
14
 
15
15
  一台能跑 `dsh web` 的机器,或者 **DeepSeek Harness 桌面端**(桌面端见 §1.7,装法不同)。宿主版本要求 **DSH `>= 0.1.7-rc.1`**(本包 1.0.7 起;`0.1.7-rc.1` 命令行版验过,`0.1.7-rc.2` 命令行版与**桌面端**都验过)。Office、Edge/Chrome、python-pptx 都是**可选增强**:没有它们插件照常工作,只是真渲染、截图、兜底引擎这些能力自动降级,交付说明里会写明。
16
16
 
17
- ### 1.2 一条命令
17
+ ### 1.2 一条命令(默认走 npm)
18
18
 
19
- 从 [Releases 页面](https://github.com/zbsph/dsh-ppt-studio/releases) 复制**最新那条** `.tgz` 资产的完整链接,然后:
19
+ **npm 是本插件的默认安装途径**(web 端与桌面端都支持):包名就是 `dsh-ppt-studio`,不需要账号、不需要配 registry,升级只需再跑一次同名命令。
20
+
21
+ **web 端**:
20
22
 
21
23
  ```powershell
22
- dsh plugin --profile web add <粘贴那条资产 URL>
24
+ dsh plugin --profile web add dsh-ppt-studio
23
25
  # 装完重启 dsh web
24
26
  ```
25
27
 
26
- 本插件声明了 `dsh.bundle.patch`(仓库根的 `cordis.patch.yml`),所以**装完就挂上了**:不用手工建链接,不用跑安装器,也不用 npm 账号(资产是公开的,装的人不需要账号)。
28
+ **桌面端**:在「插件管理」里填同一个包名 `dsh-ppt-studio`(详见 §1.7)。
29
+
30
+ 本插件声明了 `dsh.bundle.patch`(仓库根的 `cordis.patch.yml`),所以**装完就挂上了**:不用手工建链接,不用跑安装器,装的人也不需要任何账号(公开包)。
27
31
 
28
- 资产名形如 `dsh-ppt-studio-1.1.0-20260926-0238-72517e53.tgz`:版本、构建时间、构建内容的前 8 位哈希。页面会保留历次构建,所以**按上传时间取最新那条**。懒得翻页面就用这行拿名字:
32
+ 想走归档通道(离线、或不想依赖 npm)也一样可以:从 [Releases 页面](https://github.com/zbsph/dsh-ppt-studio/releases) 复制**最新那条** `.tgz` 资产的完整链接即可。两条通道是**同一份字节**(可用 `sha256` 逐字节核对):
29
33
 
30
34
  ```powershell
31
- gh release view v1.1.0 --json assets --jq '.assets | sort_by(.createdAt) | last | .name'
35
+ dsh plugin --profile web add <粘贴那条资产 URL>
32
36
  ```
33
37
 
34
- > **npm 上还没有这个包**。包名 `dsh-ppt-studio` 已经占好(未被别人使用),首次发布被 npm 的 2FA 写入策略挡下
35
- > (`E403 … Two-factor authentication or granular access token with bypass 2fa enabled is required`),需要账号持有人提供验证码或建一个可绕过 2FA 的 token。
36
- > **在发布成功之前,别用 `dsh plugin add dsh-ppt-studio` —— 那会 404。** 等上架后这里会换成 npm 命令,两条通道的内容是同一份字节。
38
+ 资产名形如 `dsh-ppt-studio-<版本>-<YYYYMMDD-HHmm>-<构建戳前 8 位>.tgz`:版本、构建时间、构建内容的前 8 位哈希。页面会保留历次构建,所以**按上传时间取最新那条**。懒得翻页面就用这行拿名字(把 `<版本>` 换成当前 tag,例如 `v1.1.1`):
39
+
40
+ ```powershell
41
+ gh release view v1.1.0 --json assets --jq '.assets | sort_by(.createdAt) | last | .name'
42
+ ```
37
43
 
38
44
  也可以直接装仓库地址,产物和 tgz 一样(`lib/` 已入库):
39
45
 
@@ -105,16 +111,20 @@ node D:\plugins\package\scripts\install.mjs
105
111
  桌面版把 harness 打在自己的应用目录里,**profile 是独立的 `desktop`**(`~/.dsh/profiles/desktop`),
106
112
  所以你之前给 `web` profile 装的那份不会自动出现在桌面端。
107
113
 
108
- 装法不同,因为桌面端有两条限制:
114
+ **默认装法同样是 npm**:在桌面端的「插件管理」里填包名 `dsh-ppt-studio` 即可(它内部就是 `pnpm add`,
115
+ 装完会把这个包写进 profile 的 `dependencies` 与 `dsh.profile.bundles`)。装完**必须重启桌面端**——
116
+ bundle 层在启动时组合,不热更。这就是"一句话无脑安装"在桌面端的形态。
117
+
118
+ 两条历史限制仍然存在,但它们只影响**归档通道**(不想用 npm 的时候):
109
119
 
110
120
  - **命令行不让你碰 `desktop` profile**。`dsh plugin --profile desktop ...` 会直接报
111
- `profile "desktop" is managed exclusively by the Electron application`,
112
- 启动和插件管理都是这样,没有绕过开关。
121
+ `profile "desktop" is managed exclusively by the Electron application`;桌面端的插件安装走插件管理入口。
113
122
  - **桌面端自带的 pnpm 装不了 tarball 网址**。它内置 pnpm 11.7.0,而桌面 profile 用的是
114
123
  `nodeLinker: hoisted`;这个组合下 `pnpm add <https…tgz>` 会报
115
124
  `ERR_PNPM_MISSING_TARBALL_INTEGRITY`。同样的 URL 换成 pnpm 11.21 或去掉 `nodeLinker` 都能装上。
116
125
 
117
- 所以桌面端用专门的安装器(它只调用**应用自带的** pnpm 和 Node,不会用系统 Node 去重编 profile 里的原生模块):
126
+ 所以只有当你要用 `.tgz` 资产(离线、或不经过 registry)时才需要专门的安装器——它只调用**应用自带的**
127
+ pnpm 和 Node,不会用系统 Node 去重编 profile 里的原生模块:
118
128
 
119
129
  ```powershell
120
130
  # 从发布包解压后(或直接对源码目录)
@@ -125,8 +135,7 @@ node D:\plugins\package\scripts\install-desktop.mjs
125
135
  ```
126
136
 
127
137
  它会:把本包打成一个 tgz 放进 `<profile>/.dsh-plugins/`,以相对 `file:` 规格装进 profile
128
- (这样不会因为外部目录被删而让桌面端下次装依赖时失败),再把包名补进 `dsh.profile.bundles`
129
- (桌面端启动只认这个列表)。装完**必须重启桌面端**——bundle 层在启动时组合,不热更。
138
+ (这样不会因为外部目录被删而让桌面端下次装依赖时失败),再把包名补进 `dsh.profile.bundles`。
130
139
 
131
140
  > 显式给 `--spec <https…tgz>` 时,安装器会先按原样试;撞上上面那个缺陷就自动回退
132
141
  > (有可用的系统 pnpm 就用它,否则下载到 profile 内走 `file:`)。
@@ -430,7 +439,7 @@ ppt_visual # Office 真渲染复核(有 Office 时;audit
430
439
 
431
440
  ```bash
432
441
  node scripts/build.mjs # 免 tsc:src → lib 复制(纯 ESM JS,源码即产物)
433
- npm test # build + LF 守卫 + smoke(262 断言)+ 预设漂移自检
442
+ npm test # build + LF 守卫 + smoke(270 断言)+ 预设漂移自检
434
443
  npm run check:eol # 发行字节守卫:跟踪的文本文件必须全 LF(--fix 就地修)
435
444
  npm run fresh # 用户视角终验:干净克隆 npm test + 真装一遍
436
445
  npm run test:bundle # 安装路径自证:隔离 DSH_HOME + 真 dsh plugin add + dump-config
@@ -450,24 +459,23 @@ npm run sync -- --local # 日常迭代:不发 GitHub,本机跑刚构建的
450
459
  npm run sync # 发版:上传资产 + 本机与 GitHub 拉齐 + 用户视角终验
451
460
  ```
452
461
 
453
- **发版四步,顺序不能换**。`npm run sync` 在"本地有未推送提交"或"tag 不指向 HEAD"时会拒绝继续。
462
+ **发版三步 + npm 自动发布(顺序不能换)**。`npm run sync` 在"本地有未推送提交"或"tag 不指向 HEAD"时会拒绝继续。
454
463
 
455
464
  ```bash
456
465
  git push origin main # ① 推提交
457
466
  gh release create v<版本> --target "$(git rev-parse HEAD)" --title ... --notes-file ... # ② tag 显式指到这一提交
458
467
  npm run sync # ③ 构建 → 上传资产 → git 前置检查 + 用户视角终验
459
- npm publish "<③ 产出的那个 tgz>" # ④ 发 npm(用同一个 tgz,保证两条通道同一份字节)
460
468
  ```
461
469
 
462
- `gh release create` 不指定 `--target` 时会按默认分支 HEAD 建 tag,本地没推就把 tag 建在旧提交上。v1.0.1 第一次发布就是这么错的:资产哈希全绿,而 GitHub 上的 tag 指向 v1.0.0 的代码。所以加了机器判据。`--skip-fresh` 能跳过终验,但状态文件里会留 `freshInstall: null`,不把"没验"伪装成"验过"。
470
+ **④ 不用你做**:资产一上传,`.github/workflows/publish.yml` 就被 `release: published` 唤醒 → 等 Release 资产出现 → **下载那个 tgz**(不重新打包,保证两条通道同一份字节)→ 用 **OIDC(Trusted Publishing)** 发到 npm → 发后自证 `registry dist.integrity == 本地 tgz 的 sha512`。**零长期 token,也不需要点任何按钮。**
463
471
 
464
- **npm 通道的现状(2026-09-26)**:包名是**无 scope** 的 `dsh-ppt-studio`(`private` 已去掉,`publishConfig.access=public`),**但还没有发布成功**:
465
- - ✅ 账号已注册、`npm login` 已通(`npm whoami` = `zbsph`);
466
- - ✅ 包名未被占用(`npm view dsh-ppt-studio` → 404),构件已备好(就是第 ③ 步产出的那个 tgz,sha 与 Release 资产一致);
467
- - ❌ 首次 `npm publish` 返回 **E403**:`Two-factor authentication or granular access token with bypass 2fa enabled is required to publish packages` —— 账号开了 2FA,而 `npm login` 的普通凭据不能写。需要发布时给 `--otp=<6 位码>`,或建一个勾了 **Bypass 2FA** 的 granular token。
472
+ `gh release create` 不指定 `--target` 时会按默认分支 HEAD 建 tag,本地没推就把 tag 建在旧提交上。v1.0.1 第一次发布就是这么错的:资产哈希全绿,而 GitHub 上的 tag 指向 v1.0.0 的代码。所以加了机器判据。`--skip-fresh` 能跳过终验,但状态文件里会留 `freshInstall: null`,不把"没验"伪装成"验过"。
468
473
 
469
- 所以**现在不要**用 `dsh plugin --profile web add dsh-ppt-studio`(会 404);先用 §1.2 的资产 URL。发布成功后,这里会改成 npm 命令。
470
- > 最早那次卡点是 npm **网站**对这个出口 IP 返回 403(注册/建 token 走不通,registry API 一直正常);2026-09-26 换出口后网站这关过了,卡点才转到 2FA 写入策略。
474
+ **npm 通道的现状(2026-09-26)**:`dsh-ppt-studio@1.1.0` **已发布**(`dist-tags.latest`、public、发布者 `zbsph`、`fileCount` 179);registry 的 tarball 与 GitHub Release 资产**逐字节相同**(sha256 `9303cc5c…`,`dist.integrity` 等于本地重算的 sha512)。
475
+ - 首版是 `npm publish <发版 tgz>` 直发完成的;**从下一版起改走 Trusted Publishing(OIDC)全自动通道** —— npm 已公告 Bypass-2FA 直发将于 2027-01 移除。
476
+ - 通道的**一次性人工前提**(npmjs.com 上那条 Trusted Publisher 连接)与失败排查清单写在 `docs/05-迭代流程.md`「发布-同步纪律」;通道**前提本身**由 `npm test` 的 smoke §51 四条断言钉住。
477
+ - 手动补发 / 重发:GitHub → Actions → *Publish to npm* → *Run workflow*(填 tag)。
478
+ > 留档:最早的卡点是 npm **网站**对出口 IP 返回 403(registry API 一直正常);换出口后卡点转为 2FA 写入策略;最终按"直发一发版 + OIDC 常态化"两步走通。
471
479
 
472
480
  **`lib/` 为什么提交**:`dsh plugin add <仓库 URL>` 只能拿到 git 里已提交的内容,而 `exports` 指向 `./lib/index.js`。lib 不入库,git 装出来的包就缺入口文件、挂不上。代价是"改了 src 忘了 build 就提交"会静默发旧代码,所以配了守卫:`npm run check:lib` 加 smoke 里一条同义断言(lib 必须逐字节等于 src,且不能被 `.gitignore` 忽略、必须被 git 跟踪)。改码流程就是改 `src/` → `node scripts/build.mjs` → 连 `lib/` 一起提交。
473
481
 
package/cordis.patch.yml CHANGED
@@ -1,8 +1,10 @@
1
1
  # DSH profile bundle patch —— 「PPT 工作室」插件行(package.json 的 dsh.bundle.patch 指向本文件)
2
2
  #
3
- # 安装(一条命令,不需要 npm 账号):
4
- # dsh plugin --profile web add dsh-ppt-studio # npm(2026-09-18 起,推荐:短、可升级)
5
- # dsh plugin --profile web add https://github.com/zbsph/dsh-ppt-studio/releases/download/<tag>/<资产名>.tgz # 离线/归档通道
3
+ # **默认安装途径 = npm(2026-09-26 起;web 与桌面端都支持)**,一条命令、不需要账号:
4
+ # dsh plugin --profile web add dsh-ppt-studio # web
5
+ # (桌面端:在「插件管理」里填同一个包名 dsh-ppt-studio)
6
+ # 归档/离线备选(同一份字节,sha256 可核对):
7
+ # dsh plugin --profile web add https://github.com/zbsph/dsh-ppt-studio/releases/download/<tag>/<资产名>.tgz
6
8
  # `dsh plugin` 会在 profile 目录里跑 pnpm,然后核对 package.json 的 dsh.profile.bundles:
7
9
  # **声明了 dsh.bundle.patch 的依赖自动入栈**(本文件即由 package.json 的 dsh.bundle.patch 指向)。
8
10
  #
@@ -414,7 +414,7 @@
414
414
 
415
415
  | 层 | 手段 | 触发点 | 覆盖 |
416
416
  |---|---|---|---|
417
- | L0 单测 | `scripts/smoke.mjs`(262 断言) | 每次改动后 | schema/渲染/校验/导出/导入/媒体/背景/声明制(含闭包)/美学/Quick/引擎/样式回环/噪声过滤/**连线方向** |
417
+ | L0 单测 | `scripts/smoke.mjs`(270 断言) | 每次改动后 | schema/渲染/校验/导出/导入/媒体/背景/声明制(含闭包)/美学/Quick/引擎/样式回环/噪声过滤/**连线方向** |
418
418
  | L1 真实回归 | `scripts/regression-real.mjs` | 每次改动后(有资产则跑) | 19 页产能 deck(0 错误 + 0 缩字)、WPS UTF-16 文件导入 |
419
419
  | L2 数字门禁 | render→verify 逐页 | 每页制作后(工作流内) | overlap/出界/溢出/对齐 |
420
420
  | L3 视觉审阅 | shot + read_image / **Office 真渲染 + read_image**(v0.8,有条件) | 每页/整体(工作流内) | 构图/断行/对比度/孤字(估算漏报兜底)/真实观感 |
@@ -5,6 +5,96 @@
5
5
 
6
6
  ---
7
7
 
8
+ ## [1.1.1] 2026-09-26 —— 修复桌面端激活失败(`webServer.register` 真契约 + 晚激活改走 `inject`);npm 定为默认安装通道
9
+
10
+ ### 触发(用户原话)
11
+ "我在当前的桌面版dsh上安装了这个插件,却在启用时发现了如图所示的报错。请你修改一下。然后我想明确一点:目前dsh的web端和桌面端都支持npm,因此我们将npm安装作为默认安装途径,以此实现'一句话(一条命令)无脑安装'的要求。"
12
+
13
+ ### 事故与根因(桌面端真机日志)
14
+ ```
15
+ dsh: warning: 1 entry did not activate ppt-studio (dsh-ppt-studio): TypeError:
16
+ ws.register(...).then is not a function at registerPreviewRoute
17
+ (…/profiles/desktop/node_modules/dsh-ppt-studio/lib/preview-server.js:145:6) at new apply (…/lib/index.js:100:3)
18
+ ```
19
+ - **真契约**(本机实测 DSH 0.1.7-rc.2 源码 `dsh-host-webserver/lib/index.js:177-184`):`register(route)` 是**同步**方法——成功返回 disposer **函数**,重复注册**同步抛错**;它从来不是 Promise。
20
+ - 插件历史实现对返回值调 `.then()` ⇒ TypeError,而它是在 `apply()` 里**同步抛出**的 ⇒ 宿主判定整个插件条目激活失败。用户侧症状是"装了却什么都没挂上",连诊断工具 `ppt_state` 都调不到。
21
+ - **为什么单测没抓到**:smoke §29 的假 webServer 把 `register` 写成 `async`(返回 Promise)—— 假对象复刻了**错的契约**,等于给 bug 背书。这是 docs/05「假对象必须复刻真契约」那条教训的**第二次发生**(第一次是 `ctx.effect` 语义写反)。
22
+
23
+ ### 改动
24
+ - `src/preview-server.js`:新增 `attemptRegister()`,把三种返回形态(同步函数 / `Promise<函数>` / `{dispose()}`)归一化,并把"同步抛错"内化成 `{thrown}`(重复注册走原有的表判据 + refcount 幂等分支);**不再向外抛**。
25
+ - `src/index.js`:全局管道(预览路由 + 预设声明)改用 `pipeStep` 包裹——与既有"能力面注册失败不阻断装配"同一纪律:**附加能力坏掉只该降级,不该让整个插件装不上**;失败写 logger + 诊断文件。
26
+ - **第二处真问题(同一次真宿主验证抓到的)**:`webServer` 比本插件**晚激活**时,直接 `ctx.get('webServer')` 拿不到服务 ⇒ 预览路由被**静默跳过**(插件自己的落盘诊断原文:`预览路由=skipped(no webServer)`)⇒ 用户点预览链接必然 404。改为与 `agentPresets` 同一姿势:`ctx.inject(['webServer'], child => registerPreviewRoute(child))`,服务出现即注册、服务消失即释放。诊断同时把"registered / skipped / pending"分开记。
27
+ - `scripts/smoke.mjs`:§29 的假 webServer **改成真契约**(同步返回函数、重复注册抛错);新增 §52 三条断言(同步抛错不得扔出 + 全局管道抛错时 `apply` 仍完成 22 工具/命令面/4 技能装配 + **webServer 晚激活时走 `inject` 仍要真的挂上路由**)。
28
+ - **安装通道**:npm 定为**默认**途径(web 端与桌面端),Release `.tgz` 资产与 `install*.mjs` 降级为**离线/归档备选**。落地位置:README §1.2/§1.7、`cordis.patch.yml` 头部、`scripts/install.mjs` 与 `scripts/install-desktop.mjs` 头部、docs/05「装配与安装」、答疑手册 §7;并新增 smoke §53 钉住三处口径一致(README 以 npm 命令为首选、且排在资产 URL 之前)。
29
+
30
+ ### 验证
31
+ - `npm test` = **270 通过 / 0 失败**(266 + §52×3 + §53×1;文档计数自证同步到 270);预设自检漂移 0。
32
+ - **真宿主验证**(隔离 `DSH_HOME` + 真 `dsh --profile web --host 127.0.0.1 --port 0 --no-open`,插件 `PPT_STUDIO_DEBUG=1` 落盘诊断):
33
+
34
+ | 观测点 | 结果 |
35
+ |---|---|
36
+ | 宿主是否报"未激活" | **无**(修复前桌面端的形态正是这行警告) |
37
+ | 插件自己的诊断 | `apply …|agentPresets=不可见` / `pipeline 预览路由=pending(inject)|预设声明=OK` / `faces … 失败=无` |
38
+ | 服务晚激活 | `previewRoute inject 就绪 → active`(webServer 起来后路由**真的挂上**) |
39
+ | 路由探针 | `GET /ppt-preview/probe00000/pages/x.html` → `404 + Content-Type: text/plain; charset=utf-8 + body "not found"` = **插件自己的处理器**;对照组 `GET /__missing__/x` → 404 但无 Content-Type、无 body ⇒ 路由确实由本插件挂载 |
40
+
41
+ - 代码层证据:`registerPreviewRoute` 对同步返回值不再调用 `.then`;`webServer` 晚激活时走 `inject` 等它(§52 三条行为断言覆盖)。
42
+ - **尚未验证(诚实标注)**:桌面端真机复验要等 **1.1.1 发布后**在桌面端升级一次——本轮真宿主验证跑在 CLI web profile 上(同一个真 `dsh-host-webserver`),但桌面端是 Electron 宿主,值得再确认一遍。
43
+ - 根因证据链:用户截图/日志 + 本机 DSH 0.1.7-rc.2 的 `dsh-host-webserver` 源码 + 桌面 profile 里实际安装的 1.1.0 副本(`~/.dsh/profiles/desktop/node_modules/dsh-ppt-studio`,由桌面端插件管理 `pnpm add dsh-ppt-studio` 装的)。
44
+
45
+ ### 影响
46
+ - 修复前:桌面端"装了但一个工具都没有"(预览路由的异常把整个条目带下水)。修复后:预览路由与其余能力面**都不再能让装配整体失败**。
47
+ - 安装口径统一后用户只记一句话:**web** `dsh plugin --profile web add dsh-ppt-studio`;**桌面端**「插件管理」里填 `dsh-ppt-studio`。
48
+ - 版本:`package.json` → **1.1.1**(patch:修复 + 文档)。发布走上一轮建好的 OIDC 全自动通道(`release: published` → 下载 Release 资产 → `npm publish`),零手工、零长期 token。
49
+
50
+ ## [发布通道 · npm 全自动] 2026-09-26 —— npm 已发布 1.1.0(逐字节核对);发布改走 Trusted Publishing(OIDC),发版零手工
51
+
52
+ ### 触发(用户原话)
53
+ "我已经按你说的建立了链接,现在我们就按照全自动的来吧。主要是因为我完全不熟悉npm的相关操作,所以需要我动手的地方越少越好。"
54
+ (上一条「npm 通道」记录写的是"尚未发布 / E403";本条把**已发生的发布**落档,并把发布通道换成 OIDC 全自动。)
55
+
56
+ ### 事实:1.1.0 已在 npm 上,且与 Release 资产逐字节相同(实测值,不是计划)
57
+ | 判据 | 实测值 |
58
+ |---|---|
59
+ | registry | `dsh-ppt-studio` HTTP 200;`versions = 1.1.0`;`dist-tags.latest = 1.1.0`;`access = public` |
60
+ | 发布时间 / 发布者 | `2026-09-25T19:22:46Z` | `_npmUser = zbsph <1186788349@qq.com>` |
61
+ | 内容 | `fileCount = 179`、`unpackedSize = 2158398` |
62
+ | 同字节 | registry tarball `sha256 = 9303cc5c…` == Release 最新资产 == 本地 `D:\plugins\_artifacts\…0258-9303cc5c.tgz`;`dist.integrity`(sha512) == 本地重算值 |
63
+ | 真机安装 | 隔离 `DSH_HOME` 里 `dsh plugin --profile web add dsh-ppt-studio` → exit 0、依赖规格 `^1.1.0`、`bundles` 自动含本包、装到的 `lib/index.js`/`cordis.patch.yml`/`preset.yml`/`install.mjs` 与仓库逐字节相同、`--dump-config` 出现 `- id: ppt-studio` |
64
+
65
+ > 顺带更正一处旧记录:上一条写的 `localTgz = …0238-72517e53.tgz` 已被后续 sync 覆盖为 `…0258-9303cc5c.tgz`(`.sync-state.json` 现值);**实际发到 npm 的是 0258**(registry 的 sha256 为证)。
66
+
67
+ ### 改动
68
+ - 新增 `.github/workflows/publish.yml`:`release: published` 触发(+ `workflow_dispatch` 手动兜底)→ 等 Release 资产出现(≤15 分钟)→ **按上传时间取最新那条 tgz 并下载**(不二次打包)→ OIDC `npm publish` → 发后自证 `dist.integrity == 本地 sha512`。`permissions: id-token: write`;job 级 `GH_REPO`(不 checkout 时 `gh` 需要它);不传 `registry-url`、发布前 `unset NODE_AUTH_TOKEN`(后两条是预检跑出来的,见下)。
69
+ - `scripts/smoke.mjs` 新增 §51 四条断言,把这条通道的前提钉住(触发方式 / OIDC 前提且不注入凭据(禁 YAML 赋值、要求 `unset` 兜底、禁 `registry-url`)/ 直发而非 stage / 取最新资产且禁止二次打包)。
70
+ - 文档同步:README(版本线 1.0.4→1.1.0、§1.2 改为 npm 优先、发版"四步"→"三步 + 自动第 ④ 步"、npm 现状重写)、`package.json` 的 `//publish`、docs/04(npm 项勾选并记录 OIDC 通道)、docs/05(新增「npm 公共通道」小节;更正 `.gitignore` 里关于 `lib/` 的说法与版本号 1.0.3→1.1.0)、docs/06(测试矩阵增列该通道;计数 245/262→266)。
71
+ - 未改 `src/`:本包发布通道属 CI/文档层,插件行为零变化(lib 与 src 仍逐字节一致)。
72
+
73
+ ### 验证
74
+ - `npm test` = **266 通过 / 0 失败**(含新增 §51 四条 + 文档计数自证 = 266);预设自检漂移 0。
75
+ - registry 侧:`npm view dsh-ppt-studio version dist.integrity` 与本地 tgz 对照(见上表)。
76
+ - 隔离 `DSH_HOME` 真装一遍(见上表"真机安装")。
77
+
78
+ ### 预检(`workflow_dispatch` 打现成的 v1.1.0 资产;版本已存在 ⇒ npm 必然拒绝覆盖,**不可能改动 registry**)
79
+ 两次真机跑,run 记录都在:
80
+
81
+ | 次 | 结果 | 抓到什么 |
82
+ |---|---|---|
83
+ | ① `36189160940` | 红(10 秒) | 不 checkout 时 `gh` 报 `fatal: not a git repository`(第一步就挂)⇒ 补 job 级 `GH_REPO` |
84
+ | ② `36190325252` | 前置 6 步全绿,发布步骤红 | 见下 |
85
+
86
+ 第 ② 次的关键证据:
87
+ - **同字节(CI 侧独立复核)**:发布步骤打印 `name: dsh-ppt-studio / version: 1.1.0 / package size: 900.6 kB / shasum: 631cdd53… / integrity: sha512-tGTfBw46… / total files: 179` —— 与 npm 上 1.1.0 的 `shasum`、`dist.integrity` **逐字段相同** ⇒ CI 从 Release 取到并交给 registry 的正是同一份字节;"按上传时间取最新那条"也从该 tag 的 **4 条** tgz 里选对了(0258)。
88
+ - **认证层没有报 `ENEEDAUTH`**,请求到达 registry 后按"版本已存在"被拒:`npm error You cannot publish over the previously published versions: 1.1.0.` ⇒ **强烈提示信任关系已放行直接发布**(若只给 stage 权限,预期是 `E_STAGE_REQUIRED`)。**这不是终局证明**——确定性验证只能等下一次**新版本号**的真实发版。
89
+ - 运行期事实(第一次预检的 env 里直接可见):`actions/setup-node` 一旦传 `registry-url`,就会注入 `NPM_CONFIG_USERCONFIG` 与一个占位 `NODE_AUTH_TOKEN` —— 正是会顶掉 OIDC 的东西。已去掉 `registry-url`,并在发布前 `unset` 兜底(只打印"有没有",不打印值)。
90
+
91
+ > 这两条红是**故意**的预检(拿已存在的版本触发,物理上不可能改 registry),不是事故;要清掉可直接删除这两条 run。
92
+
93
+ ### 影响
94
+ - 发版流程:`git push` → `gh release create --target HEAD` → `npm run sync`,随后 npm 由 CI 自动发布;用户装法改为 `dsh plugin --profile web add dsh-ppt-studio`(README §1.2)。
95
+ - 安全:发布不再依赖长期 token(OIDC 短生存凭据);公开仓库 + 公开包 ⇒ 自动生成 provenance;后续可开包级"禁止 token 发布"。
96
+ - 回滚:删掉 `.github/workflows/publish.yml` 与该条 Trusted Publisher 连接即回到手工 `npm publish`。
97
+
8
98
  ## [1.1.0 修订·npm 通道] 2026-09-26 —— 构件与流程就绪,但**首次 `npm publish` 被 2FA 策略挡下(E403),包尚未发布**
9
99
 
10
100
  ### 触发(用户原话)
@@ -42,8 +42,17 @@
42
42
  真机自证 `npm run test:bundle`(隔离 DSH_HOME);**本机已把旧装法(junction + 预设插件行)卸载并切到这条路径**,
43
43
  终态:`--dump-config` 出 `ppt-studio` 行、预设 0 行残留、挂载副本 == 部署副本(逐文件 sha256)。**不需要 npm 账号。**
44
44
  资产名带构建戳(实测"同 URL 覆盖后包管理器不重取",固定名会让升级静默失败);发布流程 ⑤b 自动同步挂载副本并自证同源。
45
- - [ ] **npm registry 发布 —— 用户已决定不做**(2026-09-14:"算了,只要确保能用 `dsh plugin` 一键安装")。
46
- 如将来要做:删 `package.json` 的 `private`、加 `publishConfig.access=public`、`npm login && npm publish`;包名三处需同改(smoke 有断言盯住)。
45
+ - [x] **npm registry 发布 —— 2026-09-26 完成首次发布**(2026-09-14 曾决定"不做",v1.0.3 起改判、用户拍板发布)。
46
+ 现状:`dsh-ppt-studio@1.1.0` 在 npm 上(`dist-tags.latest`、public、发布者 `zbsph`),
47
+ 且 registry 的 tarball 与 GitHub Release 资产**逐字节相同**(sha256 `9303cc5c…`,`dist.integrity` = 本地重算 sha512)。
48
+ - [x] **发布通道改为 Trusted Publishing(OIDC)· 全自动**(2026-09-26 用户拍板):`.github/workflows/publish.yml`
49
+ 由 `release: published` 触发 → 等 Release 资产 → **下载资产(不二次打包)** → OIDC `npm publish` → 发后 integrity 自证。
50
+ 零长期 token;npm 公告(Bypass-2FA 直发 2027-01 移除)下的推荐路径。一次性人工前提:npmjs.com 上的 Trusted Publisher 连接。
51
+ - [x] **桌面端支持(v1.1.0)+ 安装通道统一为 npm(2026-09-26)**:桌面 profile 是独立的 `desktop`;CLI 拒绝 `--profile desktop`
52
+ (`managed exclusively by the Electron application`),且桌面自带 pnpm 11.7 在 hoisted 下装不了 tarball URL。
53
+ **默认通道 = npm**:web 用 `dsh plugin --profile web add dsh-ppt-studio`,桌面端在「插件管理」里填同一个包名
54
+ (内部即 `pnpm add`,实测写进 profile 的 `dependencies` + `bundles`)。`.tgz` 资产与 `scripts/install-desktop.mjs`
55
+ 降级为**离线/归档备选**,只影响不走 registry 的场景。
47
56
  - [ ] 预设目录出厂化(`agent presets copy` 流程文档化)。
48
57
 
49
58
  ## M6 · 内置制作技能(2026-09-14 完成 —— 用户诉求"skills 能不能提升制作能力")
@@ -23,7 +23,8 @@
23
23
  ## Git 约定
24
24
 
25
25
  - 仓库根 `D:\SharkCode\dsh-ppt-studio`;远程 `https://github.com/zbsph/dsh-ppt-studio`(发布与同步纪律见下文"发布-同步纪律"一节,别再写成"无远程")。
26
- - `.gitignore`:`node_modules/`、`lib/`(build 生成)、`examples/*/preview/`、`examples/*/imported/`、`examples/*/out-*.pptx`、`examples/*/media-test.png`、构建产物目录(import-realtest* 等)。
26
+ - `.gitignore`:`node_modules/`、`*.tgz`、`.sync-state.json`、`examples/*/preview/`、`examples/*/imported/`、`examples/*/out-*.pptx`、`examples/*/media-test.png`、构建产物目录(import-realtest* 等)。
27
+ **`lib/` 不在此列**——它是**提交**的构建产物(`dsh plugin add <仓库 URL>` 只拿 git 里已提交的内容,而 `exports` 指向 `./lib/index.js`);守卫是 `npm run check:lib` 与 smoke 断言(lib 必须逐字节等于 src 且被 git 跟踪)。
27
28
  - 提交粒度:一个功能/修复一个 commit;message:`v0.x.y: <一句话目的>(反馈 Dx)`。
28
29
  - **基线 commit**(v0.3.0 + 文档体系)已于 2026-09-04 建立——从此任何改动可 diff/回滚。
29
30
 
@@ -48,6 +49,11 @@
48
49
  - **yaml@2 节点 API 教训**:`parse()`(纯值)/ `Document.toJS()`(Document 级纯值,推荐)/ `doc.get()`+`node.toJS()`(AST 层,裸 toJS 抛 "A document argument is required")——parseDocument 场景读值一律 `doc.toJS()`。
49
50
 
50
51
  ## 装配与安装(2026-09-14 新增 profile bundle 路径后)
52
+ - **默认安装通道 = npm(2026-09-26 用户拍板)**:包名 `dsh-ppt-studio`(无 scope)。
53
+ web 端 `dsh plugin --profile web add dsh-ppt-studio`;**桌面端在「插件管理」里填同一个包名**(内部即 `pnpm add`,
54
+ 写进 profile 的 `dependencies` + `dsh.profile.bundles`)。目标口径是"**一句话无脑安装**"——用户不需要账号、不需要配 registry、
55
+ 不需要知道资产 URL。GitHub Release 的 `.tgz` 资产与 `scripts/install*.mjs` 降级为**离线/归档备选**,
56
+ 两条通道的字节由发布流程与 smoke 断言保证相同(README §1.2/§1.7 是用户版口径,答疑手册同步)。
51
57
  - **两条路径,二选一**:① `dsh plugin --profile <p> add <包>`(走包里的 `cordis.patch.yml`,**profile 级**);② `install.mjs` 写 agent preset 行(**会话级**)。
52
58
  同时装 = 同进程挂两次 → 插件有**装配防重**(首个生效 + 告警)不会崩,但纪律是二选一(README §0.7)。
53
59
  - **"能不能用标准姿势装"取决于包自己**:依赖必须声明 `dsh.bundle.patch`(且该文件在 `files` 里),否则 `dsh plugin add` 只装不挂、仅打一行警告。
@@ -57,6 +63,10 @@
57
63
  写反的后果不是报错而是**静默失效**(路由 refcount 就这么失效了很久)。
58
64
  - **假对象必须复刻真契约**:smoke 的假 ctx 曾把 `effect` 语义写反,于是"清理体写在 cb 里"的真 bug 在测试里看起来是对的。
59
65
  **测试替身的契约错了,测试就在替 bug 背书**——契约可疑时,先用真库写一个最小探针(本轮就是这么定位的)。
66
+ **第二次发生(2026-09-26「桌面端激活失败」)**:smoke §29 的假 webServer 把**同步**的 `webServer.register`
67
+ (真源码 `dsh-host-webserver/lib/index.js:177`,成功返回 disposer **函数**、重复注册同步抛错)写成 `async`,
68
+ 于是"对返回值调 `.then`"的 TypeError 只在真宿主上爆发——**整个插件条目激活失败,而单测全绿**。
69
+ 已把假对象改成真契约,并新增 smoke §52 两条(同步抛错不得扔出 + 全局管道抛错时装配仍完成)。
60
70
 
61
71
  ## 新反馈的处理模板
62
72
 
@@ -94,7 +104,7 @@
94
104
  想要会话级隔离就切后者(`dsh plugin --profile web remove <包>` → `node scripts/install.mjs` → 重启;README §10 有切换与回退命令),
95
105
  日常迭代照旧 `--local`——它现在**两种挂载模式都自证**(非 bundle 走"穿过 junction 读到的字节")。
96
106
  插件**无法**在 bundle 挂载下自己按预设过滤:注册发生在插件 `apply`(profile 级),要按预设隔离就得从预设组合里注册。
97
- - 版本号:未获用户指示不改(当前 **1.0.3**;1.0.0–1.0.2 的历史值见 docs/03);**资产名带构建时间 + 构建戳**(2026-09-15 起):`dsh-ppt-studio-<版本>-<YYYYMMDD-HHmm>-<sha256 前 8 位>.tgz`。
107
+ - 版本号:未获用户指示不改(当前 **1.1.1**;历史值见 docs/03);**资产名带构建时间 + 构建戳**(2026-09-15 起):`dsh-ppt-studio-<版本>-<YYYYMMDD-HHmm>-<sha256 前 8 位>.tgz`。
98
108
  > 2026-09-18 改名说明:包名由 `@dsh-external/dsh-ppt-studio` 改为**无 scope** 的 `dsh-ppt-studio`(发布到 npm),
99
109
  > 于是 `npm pack` 产出的资产名也从 `dsh-external-dsh-ppt-studio-…` 变成 `dsh-ppt-studio-…`;v1.0.2 及更早的资产仍在 Releases 页上(老用户重装不受影响)。
100
110
  **时间段的用处**:老资产**不删**(已装用户靠它重装),页面上会并排多条名字近似者,sha 段看不出新旧 ⇒ 取时间最新那条即当前版本;发布日志里同时打印本轮的完整资产名。
@@ -103,6 +113,22 @@
103
113
  ⇒ 用户"重跑同一条命令升级"会拿到旧版本。换名 ⇒ URL 必变 ⇒ 必然重取。因此**取消"同 tag 资产原地更新"这条口径**:
104
114
  每次构建产出新资产名,旧资产保留(历史可回溯)。
105
115
 
116
+ ### npm 公共通道(2026-09-26 起:Trusted Publishing / OIDC,全自动)
117
+
118
+ - **一次性人工前提**(只有这一次要动手):npmjs.com → 本包 → Settings → **Trusted publishing** → Add trusted publisher → GitHub Actions:
119
+ `Organization or user = zbsph` | `Repository = dsh-ppt-studio` | `Workflow filename = publish.yml`(只填文件名、区分大小写、必须与 `.github/workflows/publish.yml` 一致)| `Environment name` 留空 |
120
+ **Allowed actions 必须勾上 `npm publish`**(全自动档;只给 stage 权限时 CI 会以 `E_STAGE_REQUIRED` 失败)。
121
+ 官方两条硬约束:① 保存时**不校验**,字段错只在首次发布时以 `ENEEDAUTH` 暴露;② 已建立的连接**不可编辑**,要改只能删除后重建。
122
+ 命令行等效(仍会要求浏览器 2FA):`npm trust github dsh-ppt-studio --repo zbsph/dsh-ppt-studio --file publish.yml --allow-publish`。
123
+ - **之后每次发版零手工**:`.github/workflows/publish.yml` 由 `release: published` 触发 → 等 Release 资产出现(≤15 分钟——资产的 `gh release upload` 在 `npm run sync` 里,**晚于**该事件)→ **按上传时间取最新那条 tgz**(同一 tag 上可能有历次重建,与 README §1.2 同一口径;**不二次打包**,保住"两条通道同一份字节")→ OIDC `npm publish` → 发后自证 `registry dist.integrity == 本地 tgz 的 sha512`。
124
+ - **触发顺序陷阱(务必记住)**:`gh release create` 早于资产上传,所以**不能**写成"事件触发 + 立刻在 CI 里打包"。本工作流用"等资产"把顺序问题变成可控等待;替代方案是先建 draft、`npm run sync` 之后再 `gh release edit --draft=false`(GitHub 官方:draft 被 publish 时 `published` 事件会触发)。
125
+ - **手动补发 / 重发**:Actions → *Publish to npm* → *Run workflow*(填 tag)。
126
+ - **为什么不在 CI 里重新打包**:同一份源码在 CI 里打包会因文件时间戳与命名差异产出**不同字节**,直接破坏"registry 字节 == Release 资产字节"这条不变量;smoke §51 有断言禁止二次打包。
127
+ - **为什么第 ④ 步不再需要人工批准**(2026-09-26 用户拍板):OIDC + `npm publish` 是"零长期凭据"的自动发布通道;代价是放弃"每次上线必须本人 2FA"。若要恢复人工闸门:把信任关系权限收窄为 stage,并把工作流改成先 stage、再人工批准——npm 官方明确 `npm stage approve` **不能**用 OIDC 完成,必须交互式 2FA。
128
+ - **安全加固(按官方 migration tip 的顺序)**:① 先让这条通道真跑通一次;② 再到本包 Settings → Publishing access 选 "Require two-factor authentication and disallow tokens" → Update Package Settings(官方明文:这条**不影响** trusted publisher);③ 然后删除不再需要的长期 token。
129
+ - **能力边界**:仅支持云托管 runner(self-hosted 不支持);`npm whoami` 不反映 OIDC 状态;OIDC 只能跑 `npm publish` / `npm stage publish`,其它 stage 子命令与 `install`/`view`/`access` 仍需传统认证;npm CLI ≥ 11.5.1(OIDC)/ ≥ 11.15.0(stage 命令)、Node ≥ 22.14.0 ⇒ 工作流用 node 24 并带版本门禁。
130
+ - **两条已实测的运行期坑**(2026-09-26 两次预检跑,run `36189160940` / `36190325252`):① 工作流**不 checkout** 时 `gh` 会报 `fatal: not a git repository` ⇒ 必须给 job 级 `GH_REPO: ${{ github.repository }}`;② `actions/setup-node` 一旦传 `registry-url`,运行期会注入 `NPM_CONFIG_USERCONFIG` 与一个占位 `NODE_AUTH_TOKEN`(env 里可见)——**正是会顶掉 OIDC 的东西** ⇒ 不要传 `registry-url`,并在发布前 `unset NODE_AUTH_TOKEN` 兜底。两条都由 smoke §51 钉住。
131
+
106
132
  ## 宿主升级纪律(DSH 换版本时必做,2026-09-12 起)
107
133
 
108
134
  - **先确认版本再动手**:`dsh --version` + `npm view @deepseek-ai/dsh dist-tags`(`latest` 与 `next` 可能不同;本机基线写进 README §0.1 与 package.json 的 peer 范围)。
@@ -5,7 +5,7 @@
5
5
  > 结论一句话:**达到发布标准。** 五项原则均"良好"及以上;发现并修复 3 个严谨性问题;自包含测试体系(smoke 181 + preflight 11 + E2E 13 + real 2)可在无外部资产下复现全部验证。
6
6
 
7
7
  > **计数说明(2026-09-14 加)**:§一 是生成于 2026-09-05 的**历史快照**,其中 `smoke 181` 是当时的套件规模,**不改写**。
8
- > 套件规模会随每轮增量断言增长;**当前规模与最新结果只认 §二 2.1(持续维护)与 §六 发布标准核对**——现为 **smoke 245**。
8
+ > 套件规模会随每轮增量断言增长;**当前规模与最新结果只认 §二 2.1(持续维护)与 §六 发布标准核对**——现为 **smoke 270**。
9
9
 
10
10
  ---
11
11
 
@@ -70,10 +70,10 @@
70
70
 
71
71
  | 场景 | 类型 | 结果 |
72
72
  |---|---|---|
73
- | smoke.mjs(262 断言:全链路 + 路由幂等 + autodeclare 回归 + splice/slice zip 级 + 度量/声明/防呆 + **连线方向/箭头/包围盒** + **内置技能包与"老用户不受影响"超集断言** + **手册 vs 源码事实一致性 13 条** + **讲稿备注/实体解码/图表配色** + **答疑手册触发纪律 3 条** + **lib/ 入库一致性** + **M2 真产物页号契约与注入自证 4 条** + **python-pptx 兜底生成物过真消费者 2 条** + **表格字号三层同源与 audit 下限 2 条** + **质量档降级可见 3 条** + **引擎不静默切换 3 条** + **出界分级两码可分 1 条** + **surgicalMap 消费自证 2 条** + **工具描述页数守卫 1 条** + **表格门禁 2 条** + **换行鲁棒(CRLF+BOM)3 条** + **用户模板库两层目录与入库/物化/删除 5 条** + **DSH 0.1.7:预设声明 / 门禁新参照格式 / 安装器路径** + **桌面端安装器(桩工具链 dry-run,不碰真桌面端)**) | 自动化 | 262/262 |
73
+ | smoke.mjs(270 断言:全链路 + 路由幂等 + autodeclare 回归 + splice/slice zip 级 + 度量/声明/防呆 + **连线方向/箭头/包围盒** + **内置技能包与"老用户不受影响"超集断言** + **手册 vs 源码事实一致性 13 条** + **讲稿备注/实体解码/图表配色** + **答疑手册触发纪律 3 条** + **lib/ 入库一致性** + **M2 真产物页号契约与注入自证 4 条** + **python-pptx 兜底生成物过真消费者 2 条** + **表格字号三层同源与 audit 下限 2 条** + **质量档降级可见 3 条** + **引擎不静默切换 3 条** + **出界分级两码可分 1 条** + **surgicalMap 消费自证 2 条** + **工具描述页数守卫 1 条** + **表格门禁 2 条** + **换行鲁棒(CRLF+BOM)3 条** + **用户模板库两层目录与入库/物化/删除 5 条** + **DSH 0.1.7:预设声明 / 门禁新参照格式 / 安装器路径** + **桌面端安装器(桩工具链 dry-run,不碰真桌面端)**) | 自动化 | 270/270 |
74
74
  | **讲稿(备注)真消费者验证**(2026-09-14):① **真 PowerPoint COM** 打开带备注的导出件并逐页读回讲稿文本(多行 18 字 / 无备注页 NONE / 单行含 `<特殊> &` 15 字);② **python-pptx 1.0.2** 独立读回同样三页文本 + 重新打包成功(smoke 里以可选断言固化,缺 python 时跳过) | 真机/第三方 | ✓ 两路一致 |
75
75
  | **python-pptx 兜底引擎真消费者验证**(2026-09-18 新增,smoke §43):生成一段覆盖「shape 无填充 / 对象填充 `{color,alpha}` / 带描边 + line 连接线 + table + text」的 deck → `genPythonScript` → ① `python -m py_compile` 必须过;② `runPythonExport` 必须真跑出 .pptx,且 **python-pptx 自己打开它并读回**(1 页 ≥ 6 形状)。**为什么必须补**:这条路径此前在 `scripts/` 里**零调用**,而它是"pptd 硬失败时"的最后保险;python 是可选依赖,"跳过"与"通过"印同一个颜色,于是它连编译都过不去也没人知道 | 真机/第三方 | 2 条全绿 |
76
- | 文档计数自证(smoke 内):README/docs02/docs06/手册里所有"smoke N 断言 / N/N"必须等于真实断言数 | 自动化 | 245 全中 |
76
+ | 文档计数自证(smoke 内):README/docs02/docs06/手册里所有"smoke N 断言 / N/N"必须等于真实断言数 | 自动化 | 270 全中 |
77
77
  | **M2 通道真产物自证**(2026-09-18 新增,smoke §27.4~27.7):由 `ppt_render` + `ppt_measure` 真跑产出的 `layout.json`/`measured.json` 驱动——① 两档 1 基 `pageNo` 一一对应;② 在真产物上给**真实元素**注入溢出,必被捕获且 id 正确;③ 第 1 页的注入**不得串到第 2 页**(错位一页是原 bug 最危险的形态);④ 缺 `pageNo` 的旧版产物必须报 `measured-unpaired` 而不是静默返空。**为什么必须真产物**:原 bug 的全部成因就是"夹具照着函数期望的形状写、从没调用过真生产者"(夹具两侧同写 `index: 1` 掩盖了 0 基 vs 1 基的错配)| 自动化(真产物) | 4 条全绿 |
78
78
  | **安装路径真机自证**(`npm run test:bundle`,2026-09-14):隔离 `DSH_HOME` 起临时 profile → 真 `dsh plugin --profile web add <spec>` → 断言"无 no-dsh.bundle 警告 / 已自动进 `dsh.profile.bundles` / 已物化 / `--dump-config` 出现 ppt-studio 行";**两种 spec 都验过**:本地 tgz,**GitHub 资产 URL(用户实际命令)**。**2026-09-15 补升级路径**:同版本号、内容不同的第二个 tgz 再 add → 真换成新字节(marker 在位)+ 依赖规格改指新包 + bundles 不丢 + dump-config 仍在(**不碰用户 profiles**)。**2026-09-18 补 `--local` 迭代模式**:同一个隔离 profile 里跑 `release-sync --local` → 全程无上传 + 状态文件 `mode=local`/`mount=bundle-local`/`mountMatch`/`ok` + 规格变成指向 `_artifacts/` 的 `file:`(且恰好只留当前那一个)+ **挂载副本 `lib/index.js` 与仓库刚构建的逐字节相同**。**同日再补「预设行 + junction」模式**(用户要求"只有 PPT 工作室预设才有这些工具")→ **2026-09-18 事故后作废并重写为「预设健康」判据**:那条路在本 DSH 版本上**必然产出 broken 预设**(行的裸包名从 harness 解析,不是 profile)⇒ 前端选择器只渲染健康预设 ⇒ **用户根本选不到该预设**,而 profile 里又没 bundle ⇒ 两边都没工具(现场:245 个会话里 `agentPreset=ppt` 的 **0** 个,而所有文件级检查全绿)。**2026-09-24 再为 DSH 0.1.7 重写第 7 步**(预设改为向 `agentPresets` 注册表**声明**,目录发现被上游删除;`discoverPresets` 那个包已不存在):① 安装器 bundle 模式 exit 0 且**不再产出** `.agent-presets/`、并清理 0.1.6 遗留目录;② 校验"声明预设"所需的包内资产(`agent-presets/ppt/*` + `lib/preset-delivery.js`);③ `--isolate` **明确拒绝**(exit 1 + 两处源码依据);④ 它**不**把 pnpm 物化的包目录换成 junction;⑤ **预设健康的新判据**——组合里每一行的 `name` 都能从**注册表 mount 的 baseUrl**(= `dsh-web-app` 包目录)解析(实测 29 行全解析成功);⑥ 默认不镜像技能、显式 `--mirror-skills` 才镜像 | 真机/launcher | 28/28 |
79
79
  | **质量档降级可见**(2026-09-18 新增,smoke §45):把项目 `state.json` 写成非法 JSON → ① 必须**上报**而不是静默回落 `standard`;② 诊断必须 `degraded=true` 且带原因(供工具输出提示"audit 额外门禁未生效");③ **反例**:文件不存在必须 `error=null`(防把每个新工程都判成降级) | 自动化 | 3 条全绿 |
@@ -83,6 +83,9 @@
83
83
  | check-preset.mjs(预设行 vs 随包 standard 逐行比对;**允许声明过的有意差异**——目前只有 persona 一行,理由写在脚本白名单里) | 自动化 | 漂移 0 / 缺行 0 / 有意差异 1 |
84
84
  | **e2e-1.0.mjs(13 断言,自包含夹具)**:fx-pro 12 页门禁 0 错误 / 声明命中 10 条 / 装饰豁免 / A8 无误报 / **材料包覆盖全 12 页正文且无 groups·status 判定字段(M3 重写 2026-09-18)** / **M2 实测 12 页零分歧(真浏览器)** / export 12 slide 无 chart 部件 + **parity 自证(表·图·线方向)** / **splice 2 条目变化 32 不变 / slice 单页 / Office COM 真渲染 spliced[3]+single[1] / 自产 seed 回导无重复 id** | 端到端真机 | 13/13 |
85
85
  | regression-real.mjs | 真实资产 | 2/2(19 页 deck 缺资产跳过) |
86
+ | **npm 全自动发布通道**(2026-09-26 新增):`.github/workflows/publish.yml` = `release: published` 触发 + 等 Release 资产出现(≤15 分钟)+ **下载资产而非二次打包** + OIDC `npm publish`(零长期 token)+ 发后自证(registry `dist.integrity` == 本地 tgz 的 sha512)。npm 侧是 Trusted Publisher 连接(一次性人工、**建后不可编辑**);前提由 smoke §51 四条断言钉住(触发/`id-token: write` 且无长期凭据/直发而非 stage/只发下载的资产) | CI(GitHub-hosted)+ 真机 | 首次发版时验证 |
87
+ | **桌面端 webServer 契约事故 + 装配韧性 + 晚激活路由**(2026-09-26 用户实测反馈,smoke §52):DSH 0.1.7-rc.2 的 `webServer.register` 是**同步**的(返回 disposer 函数、重复注册同步抛错),插件历史上对它调 `.then()` ⇒ `TypeError` 从 `apply()` 同步抛出 ⇒ **整个插件条目激活失败**(桌面端:"1 entry did not activate ppt-studio")。修:`attemptRegister` 归一化三种返回形态 + 同步抛错内化;全局管道 step 化(坏掉只降级)。真宿主验证又抓到第二处:`webServer` 比插件**晚激活**时 `ctx.get('webServer')` 拿不到 ⇒ 路由被静默跳过(诊断原文 `预览路由=skipped(no webServer)`)⇒ 改走 `ctx.inject(['webServer'], …)`。**§29 的假 webServer 同步改成真契约**(它此前写成 async,给这个 bug 背了书) | 自动化(行为)+ 真宿主 | 3 条全绿;桌面端真机复验待 1.1.1 发版后 |
88
+ | **安装通道统一为 npm**(2026-09-26 用户拍板,smoke §53):web 与桌面端都以 **npm 包名**为默认通道("一句话无脑安装");Release `.tgz` 资产与 `install*.mjs` 降级为离线/归档备选。断言钉住 README(npm 命令为首选、排在资产 URL 之前)/ 答疑手册 / docs/05 三处口径一致 | 文档 + 自动化 | 1 条全绿 |
86
89
 
87
90
  ### 2.2 测试场景设计(自包含案例 = examples/fx)
88
91
 
Binary file
Binary file
package/lib/index.js CHANGED
@@ -97,10 +97,40 @@ export function apply(ctx, config = {}) {
97
97
  ctx.effect(() => () => { CORE.count-- }, 'ppt-studio: core mount (ref)')
98
98
 
99
99
  // ── 全局管道(与会话无关)──────────────────────────────────────────────────────
100
- registerPreviewRoute(ctx) // /ppt-preview/ 路由(无 webServer 环境自动跳过)
100
+ // 2026-09-26 桌面端事故(宿主警告原文:"1 entry did not activate ppt-studio (dsh-ppt-studio):
101
+ // TypeError: ws.register(...).then is not a function"):`registerPreviewRoute` 里一处"对同步返回值
102
+ // 调 .then"的 TypeError 从 apply **同步抛出** ⇒ 整个插件条目激活失败,用户连 ppt_state 都调不到
103
+ // (能看到的只有那一行宿主警告)。根因已在 preview-server.js 修掉;这里再兜一层,理由与下方 faces 一致:
104
+ // 全局管道是**附加能力**,坏掉只该降级(预览链接/预设声明失效),绝不该让整个能力面消失。
105
+ const pipeline = []
106
+ const pipeStep = (label, fn) => {
107
+ try { return fn() } catch (e) { pipeline.push(`${label}: ${String(e?.message ?? e)}`); return undefined }
108
+ }
109
+ // 诊断要把"跳过了"和"注册了"分开记:`registerPreviewRoute` 在拿不到 webServer 时返回 null
110
+ // (极简装配、或组合顺序排在 webServer 之前都会这样)——这与"注册失败"是两回事。
111
+ let routeState = 'pending(inject)'
112
+ pipeStep('previewRoute', () => {
113
+ // webServer 可能比本插件**晚激活**:2026-09-26 真宿主实测(隔离 DSH_HOME + `dsh --profile web`,
114
+ // 插件自己的落盘诊断原文 `预览路由=skipped(no webServer)`)——直接 `ctx.get('webServer')` 在那种
115
+ // 组合顺序下拿不到服务,路由会被**静默跳过**,用户点预览链接必然 404。
116
+ // 与 agentPresets 同一姿势:用 `inject` 等它出现再注册,服务消失时子 ctx 一并释放。
117
+ if (typeof ctx.inject === 'function') {
118
+ ctx.inject(['webServer'], (child) => {
119
+ const disposer = registerPreviewRoute(child)
120
+ routeState = disposer ? 'active' : 'skipped(子 ctx 无 webServer)'
121
+ diag('previewRoute', `inject 就绪 → ${routeState}`)
122
+ return disposer ?? undefined
123
+ })
124
+ } else {
125
+ routeState = registerPreviewRoute(ctx) ? 'active' : 'skipped(宿主无 inject)'
126
+ }
127
+ })
101
128
  // 声明「PPT 工作室」预设(0.1.7 起:注册表声明,不再写预设目录——目录已无发现路径)。
102
129
  // 失败只告警不抛;状态经 ppt_state 的 presetDelivery 暴露,避免"预设没出现"只能翻日志。
103
- declareAgentPreset(ctx, config)
130
+ pipeStep('agentPreset', () => declareAgentPreset(ctx, config))
131
+ if (pipeline.length) loggerOf(ctx)?.warn?.(`[ppt-studio] 全局管道部分失败(已降级,能力面仍可用):${pipeline.join(';')}`)
132
+ diag('pipeline', `预览路由=${routeState}|预设声明=${pipeline.some((l) => l.startsWith('agentPreset')) ? '失败' : 'OK'}`
133
+ + (pipeline.length ? `|失败=${pipeline.join('; ')}` : ''))
104
134
 
105
135
  // 语义路由:user/message → 激活/退出(auto 模式)。只写会话状态文件,不暴露任何能力面。
106
136
  ctx.on('session/event', async (session, event) => {
@@ -103,6 +103,27 @@ async function releaseRouteWhenZero() {
103
103
  }
104
104
  }
105
105
 
106
+ /**
107
+ * 调用 `webServer.register` 并**归一化**它的返回值与异常。
108
+ *
109
+ * 真契约(2026-09-26 实测 DSH 0.1.7-rc.2,源码 `dsh-host-webserver/lib/index.js:177-184`):
110
+ * register 是**同步**方法 —— 成功返回 disposer **函数**,重复注册**同步抛错**(不是 Promise!)。
111
+ * 历史实现写的是 `ws.register({...}).then(...)`:
112
+ * 对同步返回的函数调 `.then` 立即 TypeError,而它是在 `apply()` 里**同步抛出**的
113
+ * ⇒ **整个插件条目激活失败**(桌面端实测:"1 entry did not activate ppt-studio"),
114
+ * 用户连 `ppt_state` 都调不到。假对象当时写成 async,给这个 bug 背了书(smoke §29 已改真契约)。
115
+ *
116
+ * 这里吃三种返回形态:同步函数 / Promise<函数> / `{dispose()}` 对象;同步抛错归一成 `{ thrown }`
117
+ * (不向外抛:预览只是附加能力,不该让插件整体装不上)。
118
+ */
119
+ function attemptRegister(ws, makeRoute) {
120
+ try {
121
+ return { value: ws.register(makeRoute()) }
122
+ } catch (error) {
123
+ return { thrown: error }
124
+ }
125
+ }
126
+
106
127
  /** 注册预览路由(返回 disposer;无 webServer 环境返回 null)。 */
107
128
  export function registerPreviewRoute(ctx) {
108
129
  const ws = ctx.get('webServer')
@@ -118,7 +139,7 @@ export function registerPreviewRoute(ctx) {
118
139
  return ctx.effect(() => () => { ROUTE_REG.count--; void releaseRouteWhenZero() }, 'ppt-studio: preview route (ref)')
119
140
  }
120
141
  let cancelled = false
121
- const unregPromise = ws.register({
142
+ const reg = attemptRegister(ws, () => ({
122
143
  kind: 'prefix',
123
144
  path: '/ppt-preview', // prefix 语义:path 不带尾斜杠(实测:带斜杠不命中)
124
145
  async handler(req, res) {
@@ -142,10 +163,17 @@ export function registerPreviewRoute(ctx) {
142
163
  res.end('preview error')
143
164
  }
144
165
  },
145
- }).then(async (u) => {
146
- if (cancelled) { try { await u() } catch { /* 幂等 */ } ; return null }
147
- ROUTE_REG.unreg = u
148
- return u
166
+ }))
167
+ if (reg.thrown) {
168
+ // 重复注册(并发装配的另一路已抢先注册)⇒ 按上方表判据语义:只计数,不再注册。
169
+ ROUTE_REG.count++
170
+ return ctx.effect(() => () => { ROUTE_REG.count--; void releaseRouteWhenZero() }, 'ppt-studio: preview route (dup)')
171
+ }
172
+ const unregPromise = Promise.resolve(reg.value).then(async (u) => {
173
+ const dispose = typeof u === 'function' ? u : (u && typeof u.dispose === 'function' ? () => u.dispose() : null)
174
+ if (cancelled) { if (dispose) { try { await dispose() } catch { /* 幂等 */ } } ; return null }
175
+ ROUTE_REG.unreg = dispose
176
+ return dispose
149
177
  }).catch(() => null)
150
178
  ROUTE_REG.count++
151
179
  // 同上:cb 立即执行、返回值是 disposer —— 清理逻辑必须包在**返回的函数**里。
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-ppt-studio",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "在 DeepSeek Harness 里做 PPT:说需求 → 逐页制作 → 导出可编辑的 .pptx。含布局检查、Office 真渲染复核,以及可扩展的本地模板库",
5
- "//publish": "2026-09-18 定:发 npm,**无 scope** 包名 `dsh-ppt-studio`。**状态(2026-09-26):尚未发布** —— `npm login` 已通(`npm whoami` = `zbsph`)、包名未被占用(`npm view` 404)、构件已备好(发版那个 tgz,sha 与 Release 资产一致),但首次 `npm publish` 返回 **E403**:账号开了 2FA,普通登录凭据不能写入,需 `--otp=<6 位码>` 或勾了 Bypass 2FA 的 granular token。发布纪律:**用发版时那个 tgz**(`npm publish \"<.sync-state.json 里 localTgz 指向的文件>\"`),保证 registry 字节与 GitHub Release 资产**逐字节相同**;**别在 main 前进后跑裸 `npm publish`**(会按当前工作区重新打包)。成功后才做:核对 `npm view dsh-ppt-studio version dist.integrity`,并在隔离 DSH_HOME 里 `dsh plugin add dsh-ppt-studio` 真装一遍。见 docs/06 与 docs/03 的 1.1.0 条目。",
5
+ "//publish": "2026-09-18 定:发 npm,**无 scope** 包名 `dsh-ppt-studio`。**状态(2026-09-26):已发布** —— `dsh-ppt-studio@1.1.0` 在 npm 上(`dist-tags.latest`、public,发布者 `zbsph`),registry 的 tarball 与 GitHub Release 资产**逐字节相同**(sha256 `9303cc5c…`)。首个版本是 `npm publish <发版 tgz>` 直发的;**从下一版起改走 Trusted Publishing(OIDC)全自动通道**:`.github/workflows/publish.yml` 由 release 事件触发 → 下载 Release 资产 → OIDC `npm publish` → 发后自证 integrity —— 零长期 token、零手工(npm 已公告:Bypass-2FA 直发 2027-01 移除)。发布纪律不变:**用发版时那个 tgz**(`.sync-state.json` 的 `localTgz`),**别在 main 前进后跑裸 `npm publish`**(会按当前工作区重新打包 ⇒ 破坏'两条通道同一份字节')。手动补发入口:Actions → Publish to npm → Run workflow。见 README §10 与 docs/05「发布-同步纪律」。",
6
6
  "repository": { "type": "git", "url": "git+https://github.com/zbsph/dsh-ppt-studio.git" },
7
7
  "homepage": "https://github.com/zbsph/dsh-ppt-studio#readme",
8
8
  "bugs": { "url": "https://github.com/zbsph/dsh-ppt-studio/issues" },
@@ -2,7 +2,12 @@
2
2
  /**
3
3
  * dsh-ppt-studio 桌面端安装器(DSH Electron 应用)
4
4
  *
5
- * ── 为什么需要单独一个安装器(2026-09-26 实测,两条都是硬事实)──────────────────────
5
+ * ── 何时用它:**只在归档通道**(2026-09-26 起)──────────────────────────────────────
6
+ * **默认安装途径是 npm**:桌面端在「插件管理」里填包名 `dsh-ppt-studio` 即可(内部就是 `pnpm add`,
7
+ * 会写进 profile 的 `dependencies` + `dsh.profile.bundles`)。本脚本是**离线/归档备选**——
8
+ * 只有当你要用 GitHub Release 的 `.tgz` 资产、或干脆不经过 registry 时才需要它。
9
+ *
10
+ * ── 为什么"用资产 URL 装"在桌面端还得有单独一个安装器(2026-09-26 实测,两条都是硬事实)────
6
11
  * ① **独立 CLI 拒绝桌面 profile**:`@deepseek-ai/dsh` 的 `lib/bin.js` 写死了
7
12
  * if (profile.toLowerCase() === "desktop") program.error('profile "desktop" is managed exclusively by the Electron application')
8
13
  * 启动与 `plugin` 两条路都过这道守卫,且**没有环境变量旁路**
@@ -12,7 +17,7 @@
12
17
  * [ERR_PNPM_MISSING_TARBALL_INTEGRITY] … its lockfile entry has no "integrity" field
13
18
  * 隔离复现:同一份 pnpm/工作区,**去掉 nodeLinker** 就成功;**换 pnpm 11.21** 也成功。
14
19
  * ⇒ 这是 pnpm 11.7 在 hoisted + tarball-URL 组合上的缺陷,不是本插件的代码问题;
15
- * 但它让"用发布资产 URL 装"这条**我们唯一公开的安装通道**在桌面端直接不可用。
20
+ * 但它让"用发布资产 URL 装"这条归档通道在桌面端直接不可用(npm 通道不受影响)。
16
21
  *
17
22
  * ── 本脚本怎么做 ────────────────────────────────────────────────────────────────
18
23
  * 1) 只用**应用自带的 pnpm/Node**(`resources/runtime/…`)——和应用的插件管理器同一套工具链,
@@ -6,6 +6,10 @@
6
6
  * 3) 校验"预设**声明**"所需的包内资产在位,并清理历史遗留产物
7
7
  * 装完:重启 dsh web → 新建会话时选「PPT 工作室」→ 直接提需求。
8
8
  *
9
+ * ── 何时用它:**只在归档通道**(2026-09-26 起)──────────────────────────────────────
10
+ * **默认安装途径是 npm**:web 端 `dsh plugin --profile web add dsh-ppt-studio`;桌面端在插件管理里填
11
+ * 同名包。本脚本是**离线/归档备选**——解压发布包后跑它,用于不进 registry / 内网 / 需要留档的场景。
12
+ *
9
13
  * ── 2026-09-24 适配 DSH 0.1.7-rc.1:**安装器不再写预设目录** ──────────────────────
10
14
  * 0.1.6 及以前,预设靠"往 `<dshHome>/.agent-presets/ppt/` 写文件"交付(宿主启动时扫描该目录)。
11
15
  * 0.1.7 把那条发现路径**整个删掉了**,预设改为向 `agentPresets` 注册表**声明**——本插件的
package/scripts/smoke.mjs CHANGED
@@ -1135,10 +1135,15 @@ const routeCalls = []
1135
1135
  const fakePrefixes = new Map()
1136
1136
  const fakeWs = {
1137
1137
  prefixes: fakePrefixes, // 真实语义:webServer 暴露前缀表(hasRoute 判据)
1138
- register: async (r) => {
1138
+ // 【2026-09-26 修正:假对象曾给真 bug 背书】真契约是**同步**的(DSH 0.1.7-rc.2
1139
+ // `dsh-host-webserver/lib/index.js:177-184`):成功返回 disposer **函数**,重复注册**同步抛错**。
1140
+ // 旧假对象写成 `async`(返回 Promise),于是"对返回值调 .then"在真宿主上 TypeError、整个插件条目
1141
+ // 激活失败(桌面端实测),而在这里全绿。见 docs/05「假对象必须复刻真契约」。
1142
+ register: (r) => {
1143
+ if (fakePrefixes.has(r.path)) throw new Error(`webserver: duplicate ${r.kind} route "${r.path}"`)
1139
1144
  routeCalls.push(r.path)
1140
1145
  fakePrefixes.set(r.path, { kind: r.kind, path: r.path, handler: r.handler })
1141
- return async () => { fakePrefixes.delete(r.path) } // 卸载也是 async(真实语义)
1146
+ return () => { fakePrefixes.delete(r.path) } // 同步 disposer(真契约)
1142
1147
  },
1143
1148
  }
1144
1149
  // Cordis 语义(2026-09-14 用真库探针实测,见 docs/02 §2.12):`ctx.effect(cb)` **立即执行 cb**,
@@ -2482,6 +2487,131 @@ ok('制作手册零指向答疑手册:craft/data/copy 正文不出现 `ppt-stu
2482
2487
  crossBookPointers.length === 0,
2483
2488
  crossBookPointers.length ? `仍指向:${crossBookPointers.join('、')}` : '三本均无指向')
2484
2489
 
2490
+ // ── 51. npm 全自动发布通道(2026-09-26:Trusted Publishing / OIDC)─────────────────────
2491
+ // 要防的事故形状:本包的发布件是 Release 资产 tgz,而「registry 字节 == Release 资产字节」是不变量。
2492
+ // 谁把 CI 改成重新打包、去掉 id-token、注入传统 npm 凭据、或退回 stage-only,都会**静默**破坏这条通道——
2493
+ // 症状只在下一次发版时出现(而且那时已经晚了:npm 的版本不可覆盖)。所以把前提钉成机器断言。
2494
+ const publishYml = readFileSync(join(root, '.github', 'workflows', 'publish.yml'), 'utf8')
2495
+ ok('npm 发布通道:`.github/workflows/publish.yml` 由 release(published) 触发,并留 workflow_dispatch 兜底',
2496
+ /types:\s*\[published\]/.test(publishYml) && /^\s*workflow_dispatch:/m.test(publishYml),
2497
+ /types:\s*\[published\]/.test(publishYml) ? '触发 = release:published + 手动兜底' : '缺少 release:published 触发')
2498
+ // 断言口径:禁的是"在 YAML 里给它赋值"(`NODE_AUTH_TOKEN: …`)——注释里出现这个名字是在解释为什么不能赋值,不算违规。
2499
+ const noInjectedToken = !/^\s*-?\s*NODE_AUTH_TOKEN\s*[:=]/m.test(publishYml) && /unset NODE_AUTH_TOKEN/.test(publishYml) && !/^\s*registry-url\s*:/m.test(publishYml)
2500
+ ok('npm 发布通道:OIDC 前提齐(id-token: write),且不注入 npm 长期凭据(不写 NODE_AUTH_TOKEN、不给 setup-node 传 registry-url、发布前 unset 兜底)',
2501
+ /id-token:\s*write/.test(publishYml) && noInjectedToken,
2502
+ `id-token: write=${/id-token:\s*write/.test(publishYml)}|无注入凭据=${noInjectedToken}|registry-url=${/registry-url\s*:/.test(publishYml)}`)
2503
+ ok('npm 发布通道:全自动档用 `npm publish`(不是 stage-only),且带 npm >= 11.15.0 门禁',
2504
+ /npm publish/.test(publishYml) && !/npm stage publish/.test(publishYml) && /11\.15\.0/.test(publishYml),
2505
+ `npm publish=${/npm publish/.test(publishYml)}|stage-only=${/npm stage publish/.test(publishYml)}|版本门禁=${/11\.15\.0/.test(publishYml)}`)
2506
+ const newestAsset = /sort_by\(\.createdAt\)\s*\|\s*last/.test(publishYml)
2507
+ ok('npm 发布通道:发布的是**下载下来的 Release 资产**(等 .tgz + 按上传时间取最新 + gh release download),CI 里不得二次打包',
2508
+ /endswith\("\.tgz"\)/.test(publishYml) && newestAsset && /gh release download/.test(publishYml) && !/npm pack/.test(publishYml),
2509
+ `等资产=${/endswith\("\.tgz"\)/.test(publishYml)}|取最新=${newestAsset}|下载资产=${/gh release download/.test(publishYml)}|二次打包=${/npm pack/.test(publishYml)}`)
2510
+
2511
+ // ── 52. 真实 webServer 契约(2026-09-26 桌面端事故的回归)───────────────────────────────
2512
+ // 宿主警告原文(用户桌面端实测):"1 entry did not activate ppt-studio (dsh-ppt-studio):
2513
+ // TypeError: ws.register(...).then is not a function at registerPreviewRoute"。
2514
+ // 机理:真库 register 是**同步**的(返回 disposer 函数),而历史实现对返回值调 .then;异常从 apply 同步
2515
+ // 抛出 ⇒ 整个插件条目激活失败,用户连 ppt_state 都调不到。两条防线各钉一条断言。
2516
+ {
2517
+ delete globalThis.__pptRouteReg
2518
+ const dupPrefixes = new Map()
2519
+ const dupWs = { prefixes: dupPrefixes, register: () => { throw new Error('webserver: duplicate prefix route "/ppt-preview"') } }
2520
+ const dupCtx = { get: () => dupWs, effect: (cb) => { const c = cb(); return () => { if (typeof c === 'function') c() } } }
2521
+ let dupThrew = null
2522
+ let dupDisposer = null
2523
+ try { dupDisposer = rpr(dupCtx) } catch (e) { dupThrew = e }
2524
+ let dupDisposeOk = true
2525
+ try { dupDisposer?.() } catch { dupDisposeOk = false }
2526
+ ok('§52 预览路由:`webServer.register` **同步抛错**(重复注册)时不得把异常扔出,返回的 disposer 可安全调用',
2527
+ dupThrew === null && typeof dupDisposer === 'function' && dupDisposeOk,
2528
+ `threw=${dupThrew ? String(dupThrew.message) : 'null'}|disposer=${typeof dupDisposer}|dispose 安全=${dupDisposeOk}`)
2529
+
2530
+ // 反向:连 `ctx.get('webServer')` 都抛错(最坏情况)时,apply 仍必须完成能力面装配。
2531
+ const indexMod2 = await import('../lib/index.js')
2532
+ let pTools = 0, pCmds = 0, pSkills = 0
2533
+ const pSkillsSvc = { register: () => { pSkills++ } }
2534
+ const pRegistry = { register: () => Promise.resolve(async () => {}) }
2535
+ const pipeCtx = {
2536
+ tools: { register: () => { pTools++ } },
2537
+ commands: { register: () => { pCmds++ } },
2538
+ skills: pSkillsSvc,
2539
+ get: (k) => {
2540
+ if (k === 'webServer') throw new Error('webserver: get 抛错(最坏情况)')
2541
+ return k === 'skills' ? pSkillsSvc : k === 'agentPresets' ? pRegistry : undefined
2542
+ },
2543
+ agentPresets: pRegistry,
2544
+ // 最坏情况:连 inject 都抛错(webServer 这一路)——装配仍必须完成
2545
+ inject: (deps, fn) => {
2546
+ if (deps.includes('webServer')) throw new Error('inject 抛错(最坏情况)')
2547
+ if (deps.includes('agentPresets')) fn({ get: (k) => (k === 'agentPresets' ? pRegistry : undefined) })
2548
+ },
2549
+ on: () => {},
2550
+ effect: (fn) => { fn(); return () => {} },
2551
+ logger: () => ({ info: () => {}, warn: () => {} }),
2552
+ }
2553
+ delete globalThis.__pptCoreReg
2554
+ delete globalThis.__pptRouteReg
2555
+ let applyThrew = null
2556
+ try { indexMod2.apply(pipeCtx, { presetIds: ['ppt'] }) } catch (e) { applyThrew = e }
2557
+ ok('§52 装配韧性:全局管道抛错时 apply 仍完成装配(22 工具 / 命令面 / 4 技能)——附加能力坏掉只该降级',
2558
+ applyThrew === null && pTools === 22 && pCmds >= 1 && pSkills === 4,
2559
+ `threw=${applyThrew ? String(applyThrew.message) : 'null'}|tools=${pTools} cmds=${pCmds} skills=${pSkills}`)
2560
+
2561
+ // ③ 真宿主实测的组合顺序:webServer 比本插件**晚激活**。此时直接 `ctx.get('webServer')` 拿不到服务,
2562
+ // 路由会被静默跳过(诊断原文 `预览路由=skipped(no webServer)`)⇒ 预览链接必然 404。
2563
+ // 修法 = 与 agentPresets 同一姿势走 `inject` 等它出现。这条断言钉住"最终真的挂上了"。
2564
+ const injPrefixes = new Map()
2565
+ const injWs = {
2566
+ prefixes: injPrefixes,
2567
+ register: (r) => { injPrefixes.set(r.path, r); return () => { injPrefixes.delete(r.path) } }, // 真契约:同步返回函数
2568
+ }
2569
+ const injChild = { get: (k) => (k === 'webServer' ? injWs : undefined), effect: (cb) => { const c = cb(); return () => { if (typeof c === 'function') c() } } }
2570
+ let iTools = 0, iCmds = 0, iSkills = 0
2571
+ const iSkillsSvc = { register: () => { iSkills++ } }
2572
+ const iRegistry = { register: () => Promise.resolve(async () => {}) }
2573
+ const injCtx = {
2574
+ tools: { register: () => { iTools++ } },
2575
+ commands: { register: () => { iCmds++ } },
2576
+ skills: iSkillsSvc,
2577
+ // 关键:本 ctx **查不到** webServer(模拟"服务还没注册")
2578
+ get: (k) => (k === 'skills' ? iSkillsSvc : k === 'agentPresets' ? iRegistry : undefined),
2579
+ agentPresets: iRegistry,
2580
+ inject: (deps, fn) => {
2581
+ if (deps.includes('webServer')) fn(injChild)
2582
+ else if (deps.includes('agentPresets')) fn({ get: (k) => (k === 'agentPresets' ? iRegistry : undefined) })
2583
+ },
2584
+ on: () => {},
2585
+ effect: (fn) => { fn(); return () => {} },
2586
+ logger: () => ({ info: () => {}, warn: () => {} }),
2587
+ }
2588
+ delete globalThis.__pptCoreReg
2589
+ delete globalThis.__pptRouteReg
2590
+ let injThrew = null
2591
+ try { indexMod2.apply(injCtx, { presetIds: ['ppt'] }) } catch (e) { injThrew = e }
2592
+ await new Promise((r) => setTimeout(r, 10))
2593
+ ok('§52 预览路由:`webServer` 比插件晚激活时(真宿主实测的组合顺序)走 `inject` 仍要真的挂上路由',
2594
+ injThrew === null && iTools === 22 && injPrefixes.has('/ppt-preview'),
2595
+ `threw=${injThrew ? String(injThrew.message) : 'null'}|tools=${iTools}|路由已挂=${injPrefixes.has('/ppt-preview')}`)
2596
+ }
2597
+
2598
+ // ── 53. 安装通道事实:npm 是**默认**通道(web 与桌面端都支持)────────────────────────────
2599
+ // 用户 2026-09-26 拍板:"把 npm 安装作为默认安装途径,实现一句话无脑安装"。
2600
+ // 这条事实同时在三处出现(README=用户读 / docs/05=维护者读 / 答疑手册=模型读),这里钉住口径一致:
2601
+ // README 的第一条安装命令必须是 npm 包名,且要排在归档资产 URL **之前**;手册用同一条命令(`<p>` 占位)。
2602
+ {
2603
+ const readmeAuth = readFileSync(join(root, 'README.md'), 'utf8')
2604
+ const npmCmd = 'dsh plugin --profile web add dsh-ppt-studio'
2605
+ const manualCmd = 'dsh plugin --profile <p> add dsh-ppt-studio'
2606
+ const npmAt = readmeAuth.indexOf(npmCmd)
2607
+ const assetAt = readmeAuth.indexOf('dsh plugin --profile web add <粘贴那条资产 URL>')
2608
+ const manualText53 = readFileSync(join(root, 'skills', 'ppt-studio-manual', 'SKILL.md'), 'utf8')
2609
+ const docs05Text = readFileSync(join(root, 'docs', '05-迭代流程.md'), 'utf8')
2610
+ ok('§53 安装通道:README 以 **npm 包名**为第一条安装命令(排在归档资产 URL 之前),答疑手册与 docs/05 同口径',
2611
+ npmAt > 0 && assetAt > npmAt && manualText53.includes(manualCmd) && docs05Text.includes(npmCmd),
2612
+ `README npm 位置=${npmAt}|资产 URL 位置=${assetAt}|手册=${manualText53.includes(manualCmd)}|docs/05=${docs05Text.includes(npmCmd)}`)
2613
+ }
2614
+
2485
2615
  // 37.10 【必须是最后一条断言】引用计数自证:文档里 "smoke … N 断言" 必须等于本次真实断言总数。
2486
2616
  // 历史形状:加断言后 README×3 + docs/02 + docs/06×2 + 手册 全靠人工同步,迟早漏一处。
2487
2617
  // 只扫"当前状态"文档(README / 技术报告 / 评审测试矩阵 / 使用手册);docs/01/03/04 里的历史数字是记录,不动。
@@ -97,8 +97,8 @@ DSH 上做 PPT 的工作区:说需求 → 四类任务工作流 → 「数字
97
97
  ## 7. 开发维护(仅改插件时用)
98
98
 
99
99
  - 改码:`src/` → `node scripts/build.mjs` → **重启 host**(插件经 agent preset 会话装配;`dev_reload_package` 只覆盖注入器装配包,且注入器 junction 已存在时会指向旧安装根)。
100
- - 回归:`node scripts/smoke.mjs`(262 断言)→ `node scripts/preflight-1.0.mjs`(发布预检)→ `node scripts/regression-real.mjs`。
100
+ - 回归:`node scripts/smoke.mjs`(270 断言)→ `node scripts/preflight-1.0.mjs`(发布预检)→ `node scripts/regression-real.mjs`。
101
101
  - 跨层验证纪律:**预览层与成品层必须互相验证**——`ppt_render`+`ppt_verify` 只管 HTML/估算层,OOXML 层靠 `ppt_export` 的 parity 自证(表/图/线方向)+ `ppt_visual` 真渲染抽检;只跑单层会漏掉"预览对、成品错"(2026-09-14 连线方向事故)。
102
102
  - 文档链:改需求/决策 → docs/01;改机制 → docs/02;每次 → docs/03;验收 → docs/04;发布前 → docs/06。
103
- - 装配:**标准装法是 profile bundle 行**(`dsh plugin --profile <p> add <Releases 资产 URL>`,profile 级、随 profile 启动装配);preset 插件行只用于"离线 junction / 不想动 profile"场景,且与前者**互斥**(`scripts/install.mjs` 按环境二选一,检测到 bundle 安装会主动删掉 preset 行——手工删行会被下次同步装回来,所以互斥逻辑在安装器里)。排查:`dsh plugin --profile web list` → `dsh --profile web --dump-config` 找 `ppt-studio` 行 → 重启 `dsh web`。
103
+ - 装配:**默认装法是 npm 包名**(`dsh plugin --profile <p> add dsh-ppt-studio`,profile 级、随 profile 启动装配;**桌面端**在插件管理里填同一个包名 `dsh-ppt-studio`;GitHub Release 的资产 URL 与 `scripts/install*.mjs` 是离线/归档备选)。装完**必须重启 host**。排查:`dsh plugin --profile web list` → `dsh --profile web --dump-config` 找 `ppt-studio` 行。preset 插件行只在"离线 junction"场景用,与 bundle 装法**互斥**。
104
104
  - 宿主升级(DSH 换版本):本手册 §开发维护 与 docs/05「宿主升级纪律」——契约逐项 Inspect 核对 + headless 覆盖层真进程验证,别只看单测。