dsh-ppt-studio 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +480 -0
  3. package/agent-presets/ppt/agent.cordis.yml +308 -0
  4. package/agent-presets/ppt/preset.yml +3 -0
  5. package/cordis.patch.yml +29 -0
  6. package/docs/00-/350/256/276/350/256/241/346/272/220.md +537 -0
  7. package/docs/01-/351/234/200/346/261/202/344/270/216/347/233/256/346/240/207.md +136 -0
  8. package/docs/02-/346/212/200/346/234/257/346/212/245/345/221/212.md +483 -0
  9. package/docs/03-/346/233/264/346/226/260/346/227/245/345/277/227.md +2898 -0
  10. package/docs/04-/350/267/257/347/272/277/345/233/276/344/270/216/351/207/214/347/250/213/347/242/221.md +113 -0
  11. package/docs/05-/350/277/255/344/273/243/346/265/201/347/250/213.md +210 -0
  12. package/docs/06-/350/257/204/345/256/241/344/270/216/346/265/213/350/257/225.md +296 -0
  13. package/docs/fidelity-report.md +225 -0
  14. package/examples/fx/fx-mini/deck.yaml +39 -0
  15. package/examples/fx/fx-mini/pages/01.yaml +6 -0
  16. package/examples/fx/fx-mini/pages/02.yaml +2 -0
  17. package/examples/fx/fx-mini/pages/03.yaml +6 -0
  18. package/examples/fx/fx-pro/deck.yaml +48 -0
  19. package/examples/fx/fx-pro/pages/01_cover.yaml +40 -0
  20. package/examples/fx/fx-pro/pages/02_arch.yaml +55 -0
  21. package/examples/fx/fx-pro/pages/03_table.yaml +20 -0
  22. package/examples/fx/fx-pro/pages/04_chart.yaml +40 -0
  23. package/examples/fx/fx-pro/pages/05_timeline.yaml +71 -0
  24. package/examples/fx/fx-pro/pages/06_longtext.yaml +13 -0
  25. package/examples/fx/fx-pro/pages/07_safearea.yaml +33 -0
  26. package/examples/fx/fx-pro/pages/08_contrast.yaml +22 -0
  27. package/examples/fx/fx-pro/pages/09_custgeom.yaml +32 -0
  28. package/examples/fx/fx-pro/pages/10_numbers.yaml +25 -0
  29. package/examples/fx/fx-pro/pages/11_section.yaml +20 -0
  30. package/examples/fx/fx-pro/pages/12_summary.yaml +57 -0
  31. package/examples/fx/fx-pro/preview/01_pages_01_cover.html +1 -0
  32. package/examples/fx/fx-pro/preview/02_pages_02_arch.html +3 -0
  33. package/examples/fx/fx-pro/preview/03_pages_03_table.html +1 -0
  34. package/examples/fx/fx-pro/preview/04_pages_04_chart.html +3 -0
  35. package/examples/fx/fx-pro/preview/05_pages_05_timeline.html +2 -0
  36. package/examples/fx/fx-pro/preview/06_pages_06_longtext.html +4 -0
  37. package/examples/fx/fx-pro/preview/07_pages_07_safearea.html +2 -0
  38. package/examples/fx/fx-pro/preview/08_pages_08_contrast.html +1 -0
  39. package/examples/fx/fx-pro/preview/09_pages_09_custgeom.html +1 -0
  40. package/examples/fx/fx-pro/preview/10_pages_10_numbers.html +1 -0
  41. package/examples/fx/fx-pro/preview/11_pages_11_section.html +1 -0
  42. package/examples/fx/fx-pro/preview/12_pages_12_summary.html +1 -0
  43. package/examples/fx/fx-pro/preview/deck.html +10 -0
  44. package/examples/fx/fx-pro/preview/layout.json +2325 -0
  45. package/examples/fx/fx-pro/preview/measured.json +814 -0
  46. package/examples/fx/fx-pro.pptx +0 -0
  47. package/examples/fx/ren-single/01.png +0 -0
  48. package/examples/fx/ren-spliced/01.png +0 -0
  49. package/examples/fx/ren-spliced/03.png +0 -0
  50. package/examples/fx/seed.pptx +0 -0
  51. package/examples/fx/single.pptx +0 -0
  52. package/examples/fx/spliced.pptx +0 -0
  53. package/examples/smoke/deck.yaml +26 -0
  54. package/examples/smoke/imported/deck.yaml +30 -0
  55. package/examples/smoke/imported/import-styles.json +126 -0
  56. package/examples/smoke/imported/pages/slide_01.yaml +31 -0
  57. package/examples/smoke/imported/pages/slide_02.yaml +38 -0
  58. package/examples/smoke/imported/pages/slide_03.yaml +55 -0
  59. package/examples/smoke/imported/preview-rt/01_pages_slide_01.html +1 -0
  60. package/examples/smoke/imported/preview-rt/02_pages_slide_02.html +1 -0
  61. package/examples/smoke/imported/preview-rt/03_pages_slide_03.html +1 -0
  62. package/examples/smoke/imported/preview-rt/deck.html +1 -0
  63. package/examples/smoke/imported/preview-rt/layout.json +434 -0
  64. package/examples/smoke/imported/reference/previews/01.png +0 -0
  65. package/examples/smoke/imported/reference/previews/02.png +0 -0
  66. package/examples/smoke/imported/reference/previews/03.png +0 -0
  67. package/examples/smoke/imported/source.pptx +0 -0
  68. package/examples/smoke/media-test.png +0 -0
  69. package/examples/smoke/out-smoke.pptx +0 -0
  70. package/examples/smoke/pages/01_cover.yaml +27 -0
  71. package/examples/smoke/pages/02_content.yaml +42 -0
  72. package/examples/smoke/pages/03_chart.yaml +35 -0
  73. package/examples/smoke/preview/01_pages_01_cover.html +1 -0
  74. package/examples/smoke/preview/02_pages_02_content.html +1 -0
  75. package/examples/smoke/preview/03_pages_03_chart.html +1 -0
  76. package/examples/smoke/preview/deck.html +1 -0
  77. package/examples/smoke/preview/layout.json +378 -0
  78. package/lib/autodeclare.js +40 -0
  79. package/lib/commands.js +161 -0
  80. package/lib/crosscheck.js +300 -0
  81. package/lib/imgmeta.js +55 -0
  82. package/lib/index.js +239 -0
  83. package/lib/measurement.js +209 -0
  84. package/lib/msrender.js +123 -0
  85. package/lib/pptd/export-pptx.js +686 -0
  86. package/lib/pptd/import-pptx.js +774 -0
  87. package/lib/pptd/layout.js +209 -0
  88. package/lib/pptd/render-html.js +254 -0
  89. package/lib/pptd/schema.js +518 -0
  90. package/lib/pptd/svgCharts.js +147 -0
  91. package/lib/pptxPy.js +205 -0
  92. package/lib/preset-delivery.js +195 -0
  93. package/lib/preview-server.js +164 -0
  94. package/lib/router.js +165 -0
  95. package/lib/scaffold.js +315 -0
  96. package/lib/skill.js +174 -0
  97. package/lib/splice.js +219 -0
  98. package/lib/state.js +88 -0
  99. package/lib/surgical.js +369 -0
  100. package/lib/templates.js +400 -0
  101. package/lib/tools.js +955 -0
  102. package/lib/verify.js +828 -0
  103. package/lib/xmljs.js +112 -0
  104. package/lib/zips.js +117 -0
  105. package/package.json +81 -0
  106. package/scripts/audit-manual-facts.mjs +294 -0
  107. package/scripts/audit-template-fidelity.mjs +162 -0
  108. package/scripts/bench-m2.mjs +48 -0
  109. package/scripts/build-templates.mjs +63 -0
  110. package/scripts/build.mjs +14 -0
  111. package/scripts/build.sh +7 -0
  112. package/scripts/check-lib-freshness.mjs +70 -0
  113. package/scripts/check-preset.mjs +189 -0
  114. package/scripts/check-tracked-lf.mjs +84 -0
  115. package/scripts/dbg-import.mjs +13 -0
  116. package/scripts/dbg-media.mjs +10 -0
  117. package/scripts/dbg-tpl-inventory.mjs +23 -0
  118. package/scripts/dump-workflow-baseline.mjs +38 -0
  119. package/scripts/e2e-1.0.mjs +129 -0
  120. package/scripts/eval-skills-blind.mjs +91 -0
  121. package/scripts/eval-skills-score.mjs +129 -0
  122. package/scripts/export-orig-templates.ps1 +17 -0
  123. package/scripts/fixtures/workflow-baseline.json +300 -0
  124. package/scripts/fixtures.mjs +501 -0
  125. package/scripts/import-user-templates.mjs +60 -0
  126. package/scripts/install-desktop.mjs +312 -0
  127. package/scripts/install.mjs +209 -0
  128. package/scripts/migrate-templates-dualtrack.mjs +71 -0
  129. package/scripts/preflight-1.0.mjs +198 -0
  130. package/scripts/probe-asset-freshness.mjs +57 -0
  131. package/scripts/probe-effect-semantics.mjs +69 -0
  132. package/scripts/probe-reparse-safety.mjs +32 -0
  133. package/scripts/rebuild-templates-c11.mjs +69 -0
  134. package/scripts/regression-real.mjs +84 -0
  135. package/scripts/release-sync.mjs +425 -0
  136. package/scripts/shot-templates.mjs +49 -0
  137. package/scripts/smoke.mjs +2512 -0
  138. package/scripts/verify-bundle-install.mjs +281 -0
  139. package/scripts/verify-fresh-install.mjs +260 -0
  140. package/skills/ppt-studio-copy/SKILL.md +139 -0
  141. package/skills/ppt-studio-craft/SKILL.md +171 -0
  142. package/skills/ppt-studio-data/SKILL.md +173 -0
  143. package/skills/ppt-studio-manual/SKILL.md +104 -0
  144. package/templates/academic-white/deck.yaml +24 -0
  145. package/templates/academic-white/pages/_chart.yaml +43 -0
  146. package/templates/academic-white/pages/_compare.yaml +37 -0
  147. package/templates/academic-white/pages/_content.yaml +35 -0
  148. package/templates/academic-white/pages/_cover.yaml +28 -0
  149. package/templates/academic-white/pages/_ending.yaml +28 -0
  150. package/templates/academic-white/pages/_flow.yaml +61 -0
  151. package/templates/academic-white/preview.png +0 -0
  152. package/templates/academic-white/template.yaml +14 -0
  153. package/templates/business-blue/deck.yaml +28 -0
  154. package/templates/business-blue/pages/_chart.yaml +36 -0
  155. package/templates/business-blue/pages/_compare.yaml +45 -0
  156. package/templates/business-blue/pages/_content.yaml +80 -0
  157. package/templates/business-blue/pages/_cover.yaml +39 -0
  158. package/templates/business-blue/pages/_ending.yaml +32 -0
  159. package/templates/business-blue/pages/_flow.yaml +87 -0
  160. package/templates/business-blue/preview.png +0 -0
  161. package/templates/business-blue/template.yaml +14 -0
  162. package/templates/pitch-bold/deck.yaml +23 -0
  163. package/templates/pitch-bold/pages/_chart.yaml +32 -0
  164. package/templates/pitch-bold/pages/_compare.yaml +36 -0
  165. package/templates/pitch-bold/pages/_content.yaml +58 -0
  166. package/templates/pitch-bold/pages/_cover.yaml +31 -0
  167. package/templates/pitch-bold/pages/_ending.yaml +27 -0
  168. package/templates/pitch-bold/pages/_flow.yaml +46 -0
  169. package/templates/pitch-bold/preview.png +0 -0
  170. package/templates/pitch-bold/template.yaml +14 -0
  171. package/templates/tech-dark/deck.yaml +24 -0
  172. package/templates/tech-dark/pages/_chart.yaml +27 -0
  173. package/templates/tech-dark/pages/_compare.yaml +38 -0
  174. package/templates/tech-dark/pages/_content.yaml +51 -0
  175. package/templates/tech-dark/pages/_cover.yaml +30 -0
  176. package/templates/tech-dark/pages/_ending.yaml +25 -0
  177. package/templates/tech-dark/pages/_flow.yaml +80 -0
  178. package/templates/tech-dark/preview.png +0 -0
  179. package/templates/tech-dark/template.yaml +14 -0
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dsh-ppt-studio 桌面端安装器(DSH Electron 应用)
4
+ *
5
+ * ── 为什么需要单独一个安装器(2026-09-26 实测,两条都是硬事实)──────────────────────
6
+ * ① **独立 CLI 拒绝桌面 profile**:`@deepseek-ai/dsh` 的 `lib/bin.js` 写死了
7
+ * if (profile.toLowerCase() === "desktop") program.error('profile "desktop" is managed exclusively by the Electron application')
8
+ * 启动与 `plugin` 两条路都过这道守卫,且**没有环境变量旁路**
9
+ * ⇒ README 那条 `dsh plugin --profile desktop add <URL>` 在桌面端一定失败。
10
+ * ② **桌面端自带的 pnpm 装不了 tarball URL**:应用内置 pnpm 11.7.0 + Electron 的 Node 24.18.1,
11
+ * 而它的 profile 用 `nodeLinker: hoisted`。hoisted 下 `pnpm add <https…tgz>` 会失败:
12
+ * [ERR_PNPM_MISSING_TARBALL_INTEGRITY] … its lockfile entry has no "integrity" field
13
+ * 隔离复现:同一份 pnpm/工作区,**去掉 nodeLinker** 就成功;**换 pnpm 11.21** 也成功。
14
+ * ⇒ 这是 pnpm 11.7 在 hoisted + tarball-URL 组合上的缺陷,不是本插件的代码问题;
15
+ * 但它让"用发布资产 URL 装"这条**我们唯一公开的安装通道**在桌面端直接不可用。
16
+ *
17
+ * ── 本脚本怎么做 ────────────────────────────────────────────────────────────────
18
+ * 1) 只用**应用自带的 pnpm/Node**(`resources/runtime/…`)——和应用的插件管理器同一套工具链,
19
+ * 避免用系统 Node 重编 profile 里的原生模块(cpu-features / ssh2 等);
20
+ * 2) **默认把本包打成一个 tgz 放进 profile 内的 `.dsh-plugins/`,再以相对 `file:` 规格安装**。
21
+ * 理由:`file:` 由 pnpm 本地算 integrity ⇒ 在 hoisted 下实测可用,且 tarball 落在 **profile 内部**,
22
+ * 不会因为外部目录(解压目录 / `_artifacts`)被删而让桌面端下次 `pnpm install` 失败——
23
+ * 那种悬空依赖会让**应用起不来**,是最糟的失败形状;
24
+ * 3) 显式 `--spec <https…tgz>` 时先按原样试;撞上上面那个 integrity 错误就回退:
25
+ * 有可用系统 pnpm 就用它再试一次(实测它写的锁文件带 `resolution.integrity`,
26
+ * 之后桌面端自带 pnpm 跑 `install` 会 "Already up to date");否则下载进 `.dsh-plugins/` 转 `file:`;
27
+ * 4) 全程**先备份** `package.json` / `pnpm-lock.yaml` / `pnpm-workspace.yaml`,失败即还原
28
+ * (应用自己的插件管理器也是这个纪律:失败恢复这几个文件);
29
+ * 5) 收尾把包名补进 `dsh.profile.bundles`——桌面端启动只认这个列表,装完不补 = 装了不加载。
30
+ *
31
+ * 用法:
32
+ * node scripts/install-desktop.mjs # 默认 DSH_HOME/~/.dsh + profile desktop,规格=本包
33
+ * node scripts/install-desktop.mjs --spec <URL|tgz|目录> # 指定安装规格
34
+ * node scripts/install-desktop.mjs --prefix <DSH_HOME> # 指定 DSH_HOME(测试/多实例)
35
+ * node scripts/install-desktop.mjs --app-dir <目录> # 指定桌面端安装目录(探测失败时)
36
+ * node scripts/install-desktop.mjs --dry-run # 只探测并打印计划,不写任何东西
37
+ * node scripts/install-desktop.mjs --uninstall # 卸载(摘依赖 + 摘 bundles 条目)
38
+ */
39
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, rmSync, statSync, readdirSync } from 'node:fs'
40
+ import { spawnSync } from 'node:child_process'
41
+ import { homedir, platform } from 'node:os'
42
+ import { join, dirname, resolve, basename } from 'node:path'
43
+ import { fileURLToPath, pathToFileURL } from 'node:url'
44
+
45
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..')
46
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
47
+
48
+ const args = process.argv.slice(2)
49
+ const opt = (k, d) => { const i = args.indexOf(k); return i >= 0 && args[i + 1] ? args[i + 1] : d }
50
+ const PROFILE = opt('--profile', 'desktop')
51
+ const prefix = resolve(opt('--prefix', process.env.DSH_HOME || join(homedir(), '.dsh')))
52
+ const profileDir = join(prefix, 'profiles', PROFILE)
53
+ const dryRun = args.includes('--dry-run')
54
+ const uninstall = args.includes('--uninstall')
55
+ const spec = opt('--spec', root)
56
+
57
+ const say = (s) => console.log(s)
58
+ const tail = (out, n = 6) => out.trim().split('\n').slice(-n).join('\n')
59
+ const die = (s) => { console.error(`✗ ${s}`); process.exit(1) }
60
+ /**
61
+ * 安装期的失败出口:只要已经备份过,就先**还原 profile** 再退出
62
+ * (应用自己的插件管理器也是这个纪律:pnpm 一失败就把 package.json / pnpm-lock.yaml 恢复原状)。
63
+ */
64
+ let BACKUP_BOX = null
65
+ const dieInstall = (s) => {
66
+ if (BACKUP_BOX !== null && existsSync(BACKUP_BOX)) {
67
+ restoreProfile(profileDir, BACKUP_BOX)
68
+ say(' (已还原 profile 文件)')
69
+ }
70
+ console.error(`✗ ${s}`)
71
+ process.exit(1)
72
+ }
73
+
74
+ // ── 应用目录与内置工具链 ──────────────────────────────────────────────────────
75
+ /** 桌面端默认安装位置(探测失败时用 --app-dir 指定)。 */
76
+ function defaultAppDirs() {
77
+ const out = []
78
+ if (process.env.DSH_DESKTOP_APP_DIR) out.push(process.env.DSH_DESKTOP_APP_DIR)
79
+ if (platform() === 'win32') {
80
+ const local = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local')
81
+ out.push(join(local, 'Programs', 'DeepSeek Harness'), join(local, 'DeepSeek Harness'))
82
+ } else if (platform() === 'darwin') {
83
+ out.push('/Applications/DeepSeek Harness.app/Contents/Resources')
84
+ } else {
85
+ out.push('/opt/DeepSeek Harness/resources', join(homedir(), '.local/share/DeepSeek Harness/resources'))
86
+ }
87
+ return out
88
+ }
89
+
90
+ /** 定位 resources 目录(含 runtime/pnpm/bin/pnpm.mjs 才算)。 */
91
+ function findAppDir() {
92
+ const explicit = opt('--app-dir', null)
93
+ const candidates = explicit ? [resolve(explicit)] : defaultAppDirs()
94
+ for (const c of candidates) for (const r of [join(c, 'resources'), c]) {
95
+ if (existsSync(join(r, 'runtime', 'pnpm', 'bin', 'pnpm.mjs'))) return r
96
+ }
97
+ return null
98
+ }
99
+
100
+ /** 内置工具链:Node 垫片、pnpm 入口、Electron 可执行文件(垫片靠它当 Node 用)。 */
101
+ function appToolchain(resourcesDir) {
102
+ const isWin = platform() === 'win32'
103
+ const nodeShim = join(resourcesDir, 'runtime', 'bin', isWin ? 'node.cmd' : 'node')
104
+ const pnpmEntry = join(resourcesDir, 'runtime', 'pnpm', 'bin', 'pnpm.mjs')
105
+ const appRoot = dirname(resourcesDir)
106
+ const exeCandidates = isWin
107
+ ? [join(appRoot, 'DeepSeek Harness.exe')]
108
+ : [join(dirname(appRoot), 'MacOS', 'DeepSeek Harness'), join(appRoot, 'DeepSeek Harness')]
109
+ return { nodeShim, pnpmEntry, exe: exeCandidates.find((p) => existsSync(p)) ?? null }
110
+ }
111
+
112
+ // ── profile 读写 ─────────────────────────────────────────────────────────────
113
+ const readJson = (file) => JSON.parse(readFileSync(file, 'utf8').replace(/^\uFEFF/, ''))
114
+ const writeJson = (file, obj) => writeFileSync(file, `${JSON.stringify(obj, null, 2)}\n`, 'utf8')
115
+
116
+ /** workspace 设置里的 nodeLinker(缺省 = pnpm 的 isolated)。 */
117
+ function nodeLinkerOf(dir) {
118
+ const f = join(dir, 'pnpm-workspace.yaml')
119
+ if (!existsSync(f)) return 'isolated'
120
+ const m = /^nodeLinker:\s*(\S+)\s*$/m.exec(readFileSync(f, 'utf8'))
121
+ return m ? m[1] : 'isolated'
122
+ }
123
+
124
+ const BACKUP_FILES = ['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']
125
+ function backupProfile(dir) {
126
+ const box = join(dir, `.dsh-install-backup-${new Date().toISOString().replace(/[:.]/g, '-')}`)
127
+ mkdirSync(box, { recursive: true })
128
+ for (const f of BACKUP_FILES) if (existsSync(join(dir, f))) copyFileSync(join(dir, f), join(box, f))
129
+ return box
130
+ }
131
+ function restoreProfile(dir, box) {
132
+ for (const f of BACKUP_FILES) if (existsSync(join(box, f))) copyFileSync(join(box, f), join(dir, f))
133
+ }
134
+
135
+ // ── 两套 pnpm ───────────────────────────────────────────────────────────────
136
+ // 必须自己引号化:Node 的 `shell: true` **不引号化命令本身**,而桌面端的路径含空格
137
+ // (`…\Programs\DeepSeek Harness\resources\runtime\bin\node.cmd`)——实测会退化成
138
+ // `'C:\Users\11867\AppData\Local\Programs\DeepSeek' is not recognized`。
139
+ const SHQ = (s) => {
140
+ const str = String(s)
141
+ if (!/[\s"']/.test(str)) return str
142
+ return platform() === 'win32' ? `"${str.replace(/"/g, '""')}"` : `'${str.replace(/'/g, `'\\''`)}'`
143
+ }
144
+ const run = (cmd, cmdArgs, opts = {}) => spawnSync([cmd, ...cmdArgs].map(SHQ).join(' '), { encoding: 'utf8', shell: true, ...opts })
145
+
146
+ function runAppPnpm(tc, dir, pnpmArgs) {
147
+ const env = { ...process.env }
148
+ if (tc.exe !== null) env.DSH_DESKTOP_NODE_EXECUTABLE = tc.exe
149
+ const r = run(tc.nodeShim, [tc.pnpmEntry, ...pnpmArgs], { cwd: dir, env })
150
+ return { code: r.status, out: `${r.stdout ?? ''}\n${r.stderr ?? ''}` }
151
+ }
152
+
153
+ /** 系统 pnpm(桌面端单机用户不一定有)。 */
154
+ function systemPnpm() {
155
+ const v = run('pnpm', ['--version'])
156
+ if (v.status !== 0) return null
157
+ return {
158
+ version: String(v.stdout ?? '').trim(),
159
+ run: (dir, pnpmArgs) => {
160
+ const r = run('pnpm', pnpmArgs, { cwd: dir })
161
+ return { code: r.status, out: `${r.stdout ?? ''}\n${r.stderr ?? ''}` }
162
+ },
163
+ }
164
+ }
165
+
166
+ const INTEGRITY_BUG = /ERR_PNPM_MISSING_TARBALL_INTEGRITY/
167
+
168
+ /** 在 profile 内的 `.dsh-plugins/` 放一份 tgz,返回相对规格(相对 profile,不依赖任何外部路径)。 */
169
+ function stageTarball(tc, dir, fromDir) {
170
+ const box = join(dir, '.dsh-plugins')
171
+ mkdirSync(box, { recursive: true })
172
+ const before = new Set(readdirSync(box))
173
+ // 注意 cwd:`pnpm pack` 打的是**当前目录**这个包,所以要 cd 到源目录,而不是 profile
174
+ const r = runAppPnpm(tc, fromDir, ['pack', '--pack-destination', box])
175
+ if (r.code !== 0) { say(tail(r.out)); dieInstall(`打包失败:${fromDir}`) }
176
+ const fresh = readdirSync(box).filter((f) => f.endsWith('.tgz') && !before.has(f))
177
+ const tgz = fresh.sort().pop() ?? readdirSync(box).filter((f) => f.endsWith('.tgz')).sort().pop()
178
+ if (!tgz) dieInstall('打包后没找到 tgz')
179
+ // 只留当前这一个:历史 tgz 会让 profile 里堆垃圾,也会让"装的是哪一版"说不清
180
+ for (const f of readdirSync(box)) if (f.endsWith('.tgz') && f !== tgz) rmSync(join(box, f), { force: true })
181
+ return { spec: `file:./.dsh-plugins/${tgz}`, tgz }
182
+ }
183
+
184
+ /**
185
+ * 把规格装进 profile。
186
+ * @returns {string} 实际用到的策略(供输出与测试)
187
+ */
188
+ function installSpec(tc, dir, theSpec) {
189
+ const isUrl = /^https?:\/\//i.test(theSpec)
190
+ const isDir = existsSync(theSpec) && statSync(theSpec).isDirectory()
191
+
192
+ if (isUrl) {
193
+ const first = runAppPnpm(tc, dir, ['add', theSpec])
194
+ if (first.code === 0) return `应用自带 pnpm(URL 规格)`
195
+ if (!INTEGRITY_BUG.test(first.out)) { say(tail(first.out)); dieInstall(`应用自带 pnpm 安装失败(规格 ${theSpec})`) }
196
+ say(' ⚠ 撞上桌面端自带 pnpm 的已知缺陷(hoisted + tarball URL 缺 integrity),回退…')
197
+ const sys = systemPnpm()
198
+ if (sys !== null) {
199
+ const second = sys.run(dir, ['add', theSpec])
200
+ if (second.code === 0) return `系统 pnpm ${sys.version}(绕开自带 pnpm 的 hoisted+URL 缺陷)`
201
+ say(tail(second.out))
202
+ }
203
+ const cache = join(dir, '.dsh-plugins')
204
+ mkdirSync(cache, { recursive: true })
205
+ const local = join(cache, basename(new URL(theSpec).pathname) || 'plugin.tgz')
206
+ const dl = run(process.execPath, ['--input-type=module', '-e',
207
+ `const {writeFileSync}=await import('node:fs');const r=await fetch(${JSON.stringify(theSpec)});if(!r.ok)throw new Error(String(r.status));writeFileSync(${JSON.stringify(local)},Buffer.from(await r.arrayBuffer()))`])
208
+ if (dl.status !== 0 || !existsSync(local)) { say(tail(String(dl.stderr ?? ''))); dieInstall('下载 tgz 失败(无法走 file: 兜底)') }
209
+ const third = runAppPnpm(tc, dir, ['add', `file:./.dsh-plugins/${basename(local)}`])
210
+ if (third.code !== 0) { say(tail(third.out)); dieInstall('file: 规格兜底也失败') }
211
+ return '应用自带 pnpm + profile 内 file: 兜底'
212
+ }
213
+
214
+ // 目录 → 用应用自带 pnpm 打成 tgz 放进 profile;已是 tgz → 直接放进 profile
215
+ let staged
216
+ if (isDir) staged = stageTarball(tc, dir, theSpec)
217
+ else if (/\.tgz$/i.test(theSpec) && existsSync(theSpec)) {
218
+ const box = join(dir, '.dsh-plugins')
219
+ mkdirSync(box, { recursive: true })
220
+ const target = join(box, basename(theSpec))
221
+ copyFileSync(theSpec, target)
222
+ staged = { spec: `file:./.dsh-plugins/${basename(theSpec)}`, tgz: basename(theSpec) }
223
+ } else {
224
+ // 其它形态(git/registry 等)交给 pnpm 自己判断
225
+ const r = runAppPnpm(tc, dir, ['add', theSpec])
226
+ if (r.code !== 0) { say(tail(r.out)); dieInstall(`应用自带 pnpm 安装失败(规格 ${theSpec})`) }
227
+ return '应用自带 pnpm(原样规格)'
228
+ }
229
+ const r = runAppPnpm(tc, dir, ['add', staged.spec])
230
+ if (r.code !== 0) { say(tail(r.out)); dieInstall(`安装失败(规格 ${staged.spec})`) }
231
+ return `应用自带 pnpm + profile 内 ${staged.spec}`
232
+ }
233
+
234
+ /** 把包名补进(或从)dsh.profile.bundles——桌面端启动只认这个列表。 */
235
+ function reconcileBundles(dir, name, remove = false) {
236
+ const file = join(dir, 'package.json')
237
+ const manifest = readJson(file)
238
+ const list = manifest.dsh?.profile?.bundles ?? []
239
+ const next = remove ? list.filter((n) => n !== name) : (list.includes(name) ? list : [...list, name])
240
+ if (JSON.stringify(next) === JSON.stringify(list)) return false
241
+ manifest.dsh = manifest.dsh ?? {}
242
+ manifest.dsh.profile = manifest.dsh.profile ?? {}
243
+ manifest.dsh.profile.bundles = next
244
+ writeJson(file, manifest)
245
+ return true
246
+ }
247
+
248
+ // ── 主流程 ──────────────────────────────────────────────────────────────────
249
+ if (!existsSync(profileDir)) {
250
+ die(`桌面 profile 不存在:${profileDir}\n 桌面端至少要启动过一次;或用 --prefix 指定 DSH_HOME`)
251
+ }
252
+ const appResources = findAppDir()
253
+ if (appResources === null) die(`没找到桌面端安装目录(找过:${defaultAppDirs().join(' / ')})\n 用 --app-dir <应用目录> 指定`)
254
+ const tc = appToolchain(appResources)
255
+ if (!existsSync(tc.nodeShim) || !existsSync(tc.pnpmEntry)) die(`内置工具链不完整:${appResources}`)
256
+
257
+ const profilePkgFile = join(profileDir, 'package.json')
258
+ const profileName = readJson(profilePkgFile).name ?? '(未知)'
259
+ say(`桌面端:${appResources}`)
260
+ say(`profile:${profileDir}(${profileName})`)
261
+ say(`内置工具链:${tc.nodeShim}${tc.exe === null ? '(未定位到可执行文件,沿用环境变量)' : ''}`)
262
+ say(`nodeLinker:${nodeLinkerOf(profileDir)}`)
263
+ if (profileName !== `dsh-profile-${PROFILE}`) say(` ⚠ profile 名不是 dsh-profile-${PROFILE}——确认这是桌面端的 profile`)
264
+
265
+ if (uninstall) {
266
+ if (dryRun) { say(`\n(dry-run)将执行:应用自带 pnpm remove ${pkg.name};并从 dsh.profile.bundles 摘掉它`); process.exit(0) }
267
+ BACKUP_BOX = backupProfile(profileDir)
268
+ const r = runAppPnpm(tc, profileDir, ['remove', pkg.name])
269
+ if (r.code !== 0) { say(tail(r.out)); dieInstall('卸载失败,已还原 profile 文件') }
270
+ reconcileBundles(profileDir, pkg.name, true)
271
+ rmSync(join(profileDir, '.dsh-plugins'), { recursive: true, force: true })
272
+ rmSync(BACKUP_BOX, { recursive: true, force: true })
273
+ say(`\n✅ 已卸载 ${pkg.name}。重启桌面端后生效。`)
274
+ process.exit(0)
275
+ }
276
+
277
+ const isUrl = /^https?:\/\//i.test(spec)
278
+ const isDir = existsSync(spec) && statSync(spec).isDirectory()
279
+ say(`安装规格:${spec}${spec === resolve(root) ? '(本包目录 → 会先打成 tgz 放进 profile)' : ''}`)
280
+ if (dryRun) {
281
+ const plan = isUrl
282
+ ? `应用自带 pnpm 直接装 URL;仅在撞上 hoisted+URL 缺陷时回退${systemPnpm() === null ? '(本机没有系统 pnpm ⇒ 会走 profile 内 file: 兜底)' : ''}`
283
+ : isDir ? '应用自带 pnpm 打包本目录 → 装 profile 内 file: 规格'
284
+ : `应用自带 pnpm 装 ${spec}`
285
+ say(`\n(dry-run)计划:${plan}`)
286
+ say(`(dry-run)随后把 ${pkg.name} 补进 dsh.profile.bundles,并核验 dsh.bundle.patch 在位`)
287
+ process.exit(0)
288
+ }
289
+
290
+ BACKUP_BOX = backupProfile(profileDir)
291
+ say(`已备份 profile 文件 → ${BACKUP_BOX}`)
292
+ const strategy = installSpec(tc, profileDir, spec)
293
+ say(` ✓ 安装完成(${strategy})`)
294
+
295
+ // 校验装到的包 + 补 bundles
296
+ const name = pkg.name
297
+ const installedDir = join(profileDir, 'node_modules', ...name.split('/'))
298
+ if (!existsSync(join(installedDir, 'package.json'))) dieInstall(`装完仍找不到 ${installedDir}——已还原 profile 文件`)
299
+ const installed = readJson(join(installedDir, 'package.json'))
300
+ const patchRel = installed.dsh?.bundle?.patch
301
+ if (typeof patchRel !== 'string' || !existsSync(join(installedDir, patchRel.replace(/^\.\//, '')))) {
302
+ dieInstall('装到的包没有可用的 dsh.bundle.patch——不是 bundle,装了也不会加载(已还原 profile 文件)')
303
+ }
304
+ const added = reconcileBundles(profileDir, name)
305
+ say(` ✓ ${name}@${installed.version} 已就位;dsh.bundle.patch=${patchRel}${added ? ';已补进 dsh.profile.bundles' : '(bundles 里已有)'}`)
306
+ if (installed.dsh?.engines?.dsh) say(` · 声明 dsh.engines.dsh = ${installed.dsh.engines.dsh}(桌面端 0.1.7-rc.2 满足)`)
307
+ rmSync(BACKUP_BOX, { recursive: true, force: true })
308
+
309
+ say(`
310
+ ✅ 装好了。**必须重启桌面端**(bundle 层在启动时组合,不热更)。
311
+ 重启后:新建会话的预设选择器里能看到「PPT 工作室」;
312
+ 在会话里调一次 ppt_state,presetDelivery 字段会写明预设声明状态。`)
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dsh-ppt-studio 一键安装器(随发布包提供——新用户"下载→可用"的最后一步):
4
+ * 1) 确认本包是 **profile bundle**;不是就**替你装**(`dsh plugin --profile <p> add <规格>`)
5
+ * 2) 校验包在 profile 里可解析(包本体归 pnpm 管,本安装器**不建 junction**)
6
+ * 3) 校验"预设**声明**"所需的包内资产在位,并清理历史遗留产物
7
+ * 装完:重启 dsh web → 新建会话时选「PPT 工作室」→ 直接提需求。
8
+ *
9
+ * ── 2026-09-24 适配 DSH 0.1.7-rc.1:**安装器不再写预设目录** ──────────────────────
10
+ * 0.1.6 及以前,预设靠"往 `<dshHome>/.agent-presets/ppt/` 写文件"交付(宿主启动时扫描该目录)。
11
+ * 0.1.7 把那条发现路径**整个删掉了**,预设改为向 `agentPresets` 注册表**声明**——本插件的
12
+ * `lib/preset-delivery.js` 在激活时自己声明。所以安装器现在的职责变成:
13
+ * · 保证**装配**(profile bundle)——这条没变;
14
+ * · 校验**声明所需的包内资产**(`agent-presets/ppt/*` + `lib/preset-delivery.js`)真的装到了;
15
+ * · **清理**旧版本留下的 `.agent-presets/ppt/`(在 0.1.7 上它是惰性垃圾,留着只会让人以为
16
+ * "预设是靠这个目录生效的")。
17
+ *
18
+ * ── `--isolate` 在 0.1.7 上**被拒绝**(不是被遗忘)────────────────────────────────
19
+ * 隔离模式原本靠"预设目录里的 junction + **相对路径**插件行"实现:相对行按**组合文件所在目录**解析
20
+ * (本机 liangshen/j-space 等预设正是用 `name: ./xxx.mjs` 引用自己的文件)。
21
+ * 这条路在 0.1.7 上断了两处:
22
+ * ① 预设目录**不再被扫描**(同上)——没有"组合文件所在目录"这回事了;
23
+ * ② 预设的行现在由注册表在**注册表自己 ctx 的 baseUrl** 下 mount(源码:
24
+ * `mountPreset(scope.ctx.extend({ baseUrl: record.context.baseUrl }), …)`,而 `register()` 里
25
+ * `context = this.ctx` = 注册表所在层 ⇒ 那是 **dsh-web-app 的包目录**,不是 profile、更不是预设目录)
26
+ * ⇒ 裸包名 `dsh-ppt-studio` 在那里解析不到 ⇒ 预设被判 `broken` ⇒ 选择器过滤掉它。
27
+ * 所以 `--isolate` 现在会**明确失败并说明原因**,而不是写出一个永远不会被读到的预设(那正是
28
+ * 1.0.6 及以前在 0.1.7 上的静默故障形状)。要做隔离需要把插件行放进"预设声明"里、并给插件行一个
29
+ * **绝对 file:// 名字**(install 期算出来)——那是一块独立工作,不与默认路径共享代码,见 README §10。
30
+ *
31
+ * 用法:
32
+ * node scripts/install.mjs # 默认 DSH_HOME/~/.dsh + profile web
33
+ * node scripts/install.mjs --spec <URL|路径> # 用指定安装规格装成 bundle(发布流程会传)
34
+ * node scripts/install.mjs --prefix <dir> # 指定 DSH_HOME(测试/多实例)
35
+ * node scripts/install.mjs --profile <name> # 指定 profile
36
+ * node scripts/install.mjs --no-preset # 只保证 bundle,不校验预设资产
37
+ */
38
+ import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync, readdirSync } from 'node:fs'
39
+ import { spawnSync } from 'node:child_process'
40
+ import { homedir } from 'node:os'
41
+ import { join, dirname, resolve } from 'node:path'
42
+ import { fileURLToPath } from 'node:url'
43
+
44
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..')
45
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
46
+
47
+ // ── 参数 ────────────────────────────────────────────────────────────
48
+ const args = process.argv.slice(2)
49
+ const opt = (k, d) => {
50
+ const i = args.indexOf(k)
51
+ return i >= 0 && args[i + 1] ? args[i + 1] : d
52
+ }
53
+ const prefix = resolve(opt('--prefix', process.env.DSH_HOME || join(homedir(), '.dsh')))
54
+ const profile = opt('--profile', 'web')
55
+ const noPreset = args.includes('--no-preset')
56
+ // 技能镜像默认**关闭**(跨预设泄漏,见第 3 步注释);要给非 PPT 会话留兜底才显式开启。
57
+ const mirrorSkills = args.includes('--mirror-skills')
58
+
59
+ const profileDir = join(prefix, 'profiles', profile)
60
+ // 包在 profile 里的落点:**按 pkg.name 拼**(2026-09-18 改无 scope 名 `dsh-ppt-studio` 后,
61
+ // 硬编码 '@dsh-external/dsh-ppt-studio' 会指向一个不存在的目录)。split('/') 同时兼容 scoped 名。
62
+ const pkgDir = join(profileDir, 'node_modules', ...pkg.name.split('/'))
63
+ // 旧名(scoped)时代的 junction 落点——只在提示/兼容检查里用到,装新名后它是历史遗留物
64
+ const legacyPkgDir = join(profileDir, 'node_modules', '@dsh-external', 'dsh-ppt-studio')
65
+ // 0.1.6 的预设交付目录:0.1.7 上惰性,安装器负责清理
66
+ const legacyPresetDir = join(prefix, '.agent-presets', 'ppt')
67
+
68
+ if (args.includes('--isolate')) {
69
+ console.error(`✗ --isolate 在 DSH 0.1.7 上不再可用(本安装器拒绝写出一个永远不会被读到的预设)
70
+
71
+ 原因(两处,都有源码依据):
72
+ · 宿主不再扫描 \`<dshHome>/.agent-presets/\`——上游原文 "the harness discovers no preset on disk",
73
+ 预设改为向 agentPresets 注册表声明;旧写法写完等于没写,且**静默**。
74
+ · 预设的行现在由注册表在它自己 ctx 的 baseUrl 下 mount(= dsh-web-app 包目录,不是 profile、
75
+ 不是预设目录)⇒ 预设里的裸包名 \`dsh-ppt-studio\` 解析不到 ⇒ 预设被判 broken ⇒ 选择器看不到它。
76
+
77
+ 可选做法:
78
+ · 默认(全局)安装:直接跑本脚本不带 --isolate —— 「PPT 工作室」预设会由插件自己声明,
79
+ 该 profile 的**所有会话**都能用 ppt_* 工具(这也是当前发布的装配形状)。
80
+ · 真要"只在「PPT 工作室」里生效":需要把插件行放进**预设声明**里、并用安装期算出的绝对
81
+ \`file://\` 名字(见 README §10「隔离安装」)。这条路尚未实现——宁可明确拒绝,也不静默失败。`)
82
+ process.exit(1)
83
+ }
84
+
85
+ if (!existsSync(profileDir)) {
86
+ console.error(`✗ profile 不存在:${profileDir}\n 请确认已安装 dsh web(或 --prefix/--profile 正确)`)
87
+ process.exit(1)
88
+ }
89
+ if (!existsSync(join(root, 'lib', 'index.js'))) {
90
+ console.error(`✗ 当前目录缺少 lib/(发布包应自带;源码模式请先 node scripts/build.mjs)`)
91
+ process.exit(1)
92
+ }
93
+
94
+ const steps = []
95
+ steps.push(`装配模式:**全局**(profile bundle:该 profile 的所有会话都能用)`)
96
+
97
+ // ── 1) 包本体可达性 + bundle 装配 ─────────────────────────────────────────────
98
+ const profilePkgFile = join(profileDir, 'package.json')
99
+ let profilePkg = {}
100
+ // 去 BOM 再解析:Windows 上被 PowerShell/记事本编辑过的 JSON 可能带 UTF-8 BOM,JSON.parse 会直接抛
101
+ try { profilePkg = JSON.parse(readFileSync(profilePkgFile, 'utf8').replace(/^\uFEFF/, '')) } catch { /* 无/坏 → 视为非 bundle */ }
102
+ let bundleInstalled = Boolean(profilePkg.dependencies?.[pkg.name]) || (profilePkg.dsh?.profile?.bundles ?? []).includes(pkg.name)
103
+
104
+ if (!bundleInstalled) {
105
+ // 确认/建立 profile bundle(包本体交给 pnpm;规格优先级 --spec > 包本目录)
106
+ const spec = opt('--spec', root)
107
+ steps.push(`未检测到 profile bundle → 先装成 bundle:dsh plugin --profile ${profile} add ${spec}`)
108
+ // **必须把 DSH_HOME 传给子进程**:`--prefix` 指的就是 DSH home,不传会让 dsh 去动真实 ~/.dsh
109
+ const r = spawnSync('dsh', ['plugin', '--profile', profile, 'add', spec], {
110
+ stdio: 'inherit', shell: process.platform === 'win32', env: { ...process.env, DSH_HOME: prefix },
111
+ })
112
+ if (r.status !== 0) {
113
+ console.error(`✗ 安装 profile bundle 失败(dsh 退出码 ${r.status})。请手动执行:\n dsh plugin --profile ${profile} add ${spec}`)
114
+ process.exit(1)
115
+ }
116
+ try { profilePkg = JSON.parse(readFileSync(profilePkgFile, 'utf8').replace(/^\uFEFF/, '')) } catch { profilePkg = {} }
117
+ bundleInstalled = Boolean(profilePkg.dependencies?.[pkg.name]) || (profilePkg.dsh?.profile?.bundles ?? []).includes(pkg.name)
118
+ if (!bundleInstalled) { console.error(`✗ dsh 报告成功,但 profile 里仍没有本包(${profilePkgFile})`); process.exit(1) }
119
+ steps.push(`已装成 profile bundle(${profilePkg.dependencies?.[pkg.name] ?? 'dsh.profile.bundles'})`)
120
+ } else {
121
+ steps.push(`已确认 profile bundle(${profilePkg.dependencies?.[pkg.name] ?? 'dsh.profile.bundles'})——包本体归 pnpm 管`)
122
+ }
123
+ if (!existsSync(join(pkgDir, 'package.json'))) {
124
+ console.error(`✗ profile 里找不到本包:${join(pkgDir, 'package.json')}\n 请重跑:dsh plugin --profile ${profile} add ${opt('--spec', root)}`)
125
+ process.exit(1)
126
+ }
127
+ // 改名(@dsh-external/dsh-ppt-studio → dsh-ppt-studio)之后的双重挂载风险:旧包若仍在 profile 里,
128
+ // 两行 patch 会各挂一次。插件有装配防重(不会崩),但纪律是二选一 ⇒ 主动提示清掉。
129
+ if (existsSync(join(legacyPkgDir, 'package.json'))) {
130
+ steps.push(`⚠ 检测到旧包名残留:${legacyPkgDir}\n 建议执行:dsh plugin --profile ${profile} remove @dsh-external/dsh-ppt-studio(避免新旧双挂载)`)
131
+ }
132
+
133
+ // ── 2) 清理历史遗留:0.1.6 的预设目录 ────────────────────────────────────────
134
+ // 在 0.1.7 上它是惰性垃圾(没有任何发现路径扫它)。留着会让"改了预设为什么没生效"变成
135
+ // 一个假线索,也会与注册表里的那条声明**看起来像两处真相**。里面的文件全是安装器从包里拷的
136
+ // (agent.cordis.yml / preset.yml)与安装器自己建的 junction(plugin)⇒ 清理是安全的;已存在的
137
+ // `plugin` junction 也一并删掉(升级到 0.1.7 时它可能还指着旧路径)。
138
+ if (existsSync(legacyPresetDir)) {
139
+ const entries = readdirSync(legacyPresetDir)
140
+ rmSync(legacyPresetDir, { recursive: true, force: true })
141
+ steps.push(`已清理 0.1.6 遗留的预设目录(0.1.7 不再扫描它):${legacyPresetDir}(原含 ${entries.join('、') || '空'})`)
142
+ }
143
+
144
+ // ── 3) 预设**声明**资产校验(2026-09-24)──────────────────────────────────────
145
+ // 预设不再由安装器"写"出来,而是插件激活时向 agentPresets 注册表声明。安装器能验的是:
146
+ // **声明要用的包内资产确实装到了 profile 里**(否则插件会告警"预设组合不可用"而静默没有预设)。
147
+ if (!noPreset) {
148
+ const installedPreset = join(pkgDir, 'agent-presets', 'ppt', 'agent.cordis.yml')
149
+ const installedMeta = join(pkgDir, 'agent-presets', 'ppt', 'preset.yml')
150
+ const installedDelivery = join(pkgDir, 'lib', 'preset-delivery.js')
151
+ if (!existsSync(installedPreset) || !existsSync(installedMeta)) {
152
+ console.error(`✗ 装到 profile 的包缺少预设组合(发布包应自带 \`agent-presets/ppt/\`):\n ${installedPreset}`)
153
+ process.exit(1)
154
+ }
155
+ if (!existsSync(installedDelivery)) {
156
+ console.error(`✗ 装到 profile 的包缺少 lib/preset-delivery.js——那是"声明预设"的实现,缺了就没有预设:\n ${installedDelivery}`)
157
+ process.exit(1)
158
+ }
159
+ // 组合里**不得**含本包插件行:插件已由 profile bundle 挂载,预设里再来一行 = 同进程双挂载。
160
+ const presetText = readFileSync(installedPreset, 'utf8')
161
+ if (/^-\s*id:\s*ppt-studio\s*$/m.test(presetText)) {
162
+ console.error('✗ 随包预设含本包插件行——同进程会被挂两次(profile bundle + 预设)。发布包不该带这一行。')
163
+ process.exit(1)
164
+ }
165
+ steps.push('预设资产已就位:agent-presets/ppt/{agent.cordis.yml,preset.yml} + lib/preset-delivery.js ✓(预设由插件声明,不由安装器写)')
166
+ steps.push('重启 dsh web 后:「PPT 工作室」由插件向 agentPresets 注册表声明,选择器里可选;状态可用 `ppt_state` 查 presetDelivery')
167
+ }
168
+
169
+ // ── 4) 内置 skill 文件镜像(**默认关闭**,见下)──────────────────────────────────
170
+ // 技能包的**主通道**是插件内嵌注册(lib/skill.js → ctx.skills.register,落装它的那一层,随插件同生共死)。
171
+ // 把 skills/*/SKILL.md 镜像到 <dshHome>/skills/ **会让该 profile 的所有会话(含官方标准预设)在技能目录里
172
+ // 看到它们**——那是"跨预设泄漏",与"只有选「PPT 工作室」时才有这些技能"直接冲突(2026-09-18 用户报告)。
173
+ // 因此改为 **opt-in**:要给非 PPT 会话留"提问即用"的兜底,显式加 `--mirror-skills`。
174
+ // 未开启时会**清理历史镜像**——否则早期安装留下的副本会继续泄漏,且此后每次 release-sync 都把它装回来,
175
+ // 表现为"改了没生效"的静默问题。
176
+ const skillsRoot = join(root, 'skills')
177
+ if (!noPreset) {
178
+ if (!existsSync(skillsRoot)) {
179
+ console.warn('⚠ 包内缺少 skills/(此包打包不完整)——内置技能不可用')
180
+ } else {
181
+ const dirs = readdirSync(skillsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
182
+ if (mirrorSkills) {
183
+ const names = []
184
+ for (const name of dirs) {
185
+ const src = join(skillsRoot, name, 'SKILL.md')
186
+ if (!existsSync(src)) continue
187
+ const dstDir = join(prefix, 'skills', name)
188
+ mkdirSync(dstDir, { recursive: true })
189
+ writeFileSync(join(dstDir, 'SKILL.md'), readFileSync(src, 'utf8'), 'utf8')
190
+ names.push(name)
191
+ }
192
+ steps.push(names.length
193
+ ? `内置技能镜像已同步(--mirror-skills 显式开启,共 ${names.length} 本):${names.join('、')}`
194
+ : '⚠ 包内 skills/ 下没有可用技能')
195
+ } else {
196
+ // 清理历史镜像(fs.rmSync 不跟随重解析点;这些本来就是普通目录)
197
+ let removed = 0
198
+ for (const name of dirs) {
199
+ const dstDir = join(prefix, 'skills', name)
200
+ if (existsSync(dstDir)) { rmSync(dstDir, { recursive: true, force: true }); removed++ }
201
+ }
202
+ steps.push(removed
203
+ ? `内置技能:**已移除 ${removed} 本历史镜像**——技能只在装插件的层可见(插件内嵌注册);要给别的会话留兜底请加 --mirror-skills`
204
+ : '内置技能:未镜像(默认)——技能随插件注册,随插件同生共死')
205
+ }
206
+ }
207
+ }
208
+
209
+ console.log(`dsh-ppt-studio v${pkg.version} 安装/校验完成(${prefix} / profile=${profile})\n- ` + steps.join('\n- ') + '\n\n下一步:重启 dsh web → 新建会话选「PPT 工作室」→ 直接提需求。')
@@ -0,0 +1,71 @@
1
+ /**
2
+ * v0.9.0 双轨迁移(幂等):为外部模板补齐真相层——
3
+ * ① 原始 pptx → templates/<id>/template.pptx(用户桌面模板库)
4
+ * ② 已渲染 COM 整页 PNG(~/.dsh/ppt-studio/fidelity/orig/<模板名>/NN.png)→ templates/<id>/previews/
5
+ * ③ template.yaml 补 sourcePptx/previews 元数据
6
+ * 用法:node scripts/migrate-templates-dualtrack.mjs
7
+ *
8
+ * 【2026-09-18 状态(1.0.4)】它服务的那 4 套导入模板已移出随包发行物、桌面源目录也已不存在 ⇒
9
+ * 默认调用会找不到源。保留为**双轨真相层的迁移参考实现**(收纳自己的模板时仍可照此补齐
10
+ * template.pptx + previews/;生成侧现在有 `ppt_template_add` 覆盖)。
11
+ */
12
+ import { readFile, writeFile, copyFile, mkdir, readdir } from 'node:fs/promises'
13
+ import { existsSync } from 'node:fs'
14
+ import { join, dirname, basename } from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+ import { homedir } from 'node:os'
17
+ import YAML from 'yaml'
18
+
19
+ const TPL_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'templates')
20
+ const DESK = 'C:\\Users\\11867\\Desktop\\ppt模板库'
21
+ const ORIG = join(homedir(), '.dsh', 'ppt-studio', 'fidelity', 'orig')
22
+
23
+ // 源文件名(桌面/orig 目录名,中文+大小写差异)→ 模板 id
24
+ const MAP = {
25
+ '实用毕业设计论文答辩PPT模板': '实用毕业设计论文答辩ppt模板',
26
+ '极简实用部门工作总结PPT模板': '极简实用部门工作总结ppt模板',
27
+ '深蓝质感论文答辩PPT模板': '深蓝质感论文答辩ppt模板',
28
+ '简约商务': '简约商务',
29
+ }
30
+
31
+ let done = 0
32
+ for (const [srcName, tplId] of Object.entries(MAP)) {
33
+ const tplDir = join(TPL_DIR, tplId)
34
+ if (!existsSync(tplDir)) { console.log(`SKIP(模板目录不存在):${tplId}`); continue }
35
+ // ① 原始 pptx
36
+ const pptxSrc = join(DESK, srcName + '.pptx')
37
+ const pptxDst = join(tplDir, 'template.pptx')
38
+ if (existsSync(pptxSrc) && !existsSync(pptxDst)) {
39
+ await copyFile(pptxSrc, pptxDst)
40
+ console.log(`+ template.pptx ← ${basename(pptxSrc)}`)
41
+ } else if (!existsSync(pptxSrc)) {
42
+ console.log(`! 源 pptx 缺失(跳过真相层):${pptxSrc}`)
43
+ } else {
44
+ console.log(`= template.pptx 已存在(保持)`)
45
+ }
46
+ // ② 整页 PNG
47
+ const pvSrc = join(ORIG, srcName)
48
+ const pvDst = join(tplDir, 'previews')
49
+ if (existsSync(pvSrc) && !existsSync(pvDst)) {
50
+ await mkdir(pvDst, { recursive: true })
51
+ const files = (await readdir(pvSrc)).filter((f) => /\.png$/i.test(f)).sort()
52
+ for (const f of files) await copyFile(join(pvSrc, f), join(pvDst, f))
53
+ console.log(`+ previews/ ← ${files.length} 页(${srcName})`)
54
+ } else if (!existsSync(pvSrc)) {
55
+ console.log(`! 已渲染 PNG 缺失:${pvSrc}`)
56
+ } else {
57
+ console.log(`= previews/ 已存在(保持)`)
58
+ }
59
+ // ③ 元数据
60
+ const metaFile = join(tplDir, 'template.yaml')
61
+ if (existsSync(metaFile)) {
62
+ const doc = YAML.parseDocument(await readFile(metaFile, 'utf8'))
63
+ let changed = false
64
+ if (existsSync(pptxDst) && !doc.get('sourcePptx')) { doc.set('sourcePptx', 'template.pptx'); changed = true }
65
+ if (existsSync(pvDst) && !doc.get('previews')) { doc.set('previews', 'previews'); changed = true }
66
+ if (changed) { await writeFile(metaFile, String(doc)); console.log(`~ template.yaml 元数据补充`) }
67
+ else console.log(`= template.yaml 已含双轨字段`)
68
+ }
69
+ done++
70
+ }
71
+ console.log(`\n完成:处理 ${done} 个模板`)