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,10 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { verifyDeck } from '../lib/verify.js'
4
+
5
+ // 开发调试脚本(不在发布 files);layout 路径经环境变量兜底,避免机器绑定
6
+ const layout = process.env.DBG_LAYOUT ?? 'D:/SharkCode/dsh-ppt-studio/examples/overlap-smoke/preview/layout.json'
7
+ const l = JSON.parse(await readFile(layout, 'utf8'))
8
+ console.log(JSON.stringify(l.pages[0].elements.map((e) => ({ id: e.id, kind: e.kind, role: e.role, b: e.bounds })), null, 1))
9
+ const v = verifyDeck(l)
10
+ console.log(v.text)
@@ -0,0 +1,23 @@
1
+ // 模板保真度盘点(临时诊断脚本)
2
+ import { resolveDeck } from '../lib/pptd/schema.js'
3
+ import { templateWorkspace, TEMPLATES_DIR } from '../lib/templates.js'
4
+ import { readdir } from 'node:fs/promises'
5
+ for (const id of (await readdir(TEMPLATES_DIR)).filter((n) => !n.startsWith('.'))) {
6
+ try {
7
+ const t = await templateWorkspace(id)
8
+ const ctx = await resolveDeck(t.dir)
9
+ const stats = { els: 0, lines: 0, texts: 0, shapes: 0, imgs: 0 }
10
+ for (const p of ctx.pages) {
11
+ for (const e of p.page.elements ?? []) {
12
+ stats.els++
13
+ if (e.elementType === 'line') stats.lines++
14
+ else if (e.elementType === 'text') stats.texts++
15
+ else if (e.elementType === 'shape') stats.shapes++
16
+ else if (e.elementType === 'image') stats.imgs++
17
+ }
18
+ }
19
+ console.log(`${id}: ${ctx.pages.length}页 ${stats.els}元素 (线${stats.lines}/文${stats.texts}/形${stats.shapes}/图${stats.imgs}) media=${t.media.length}`)
20
+ } catch (e) {
21
+ console.log(`${id}: FAIL ${e.message}`)
22
+ }
23
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 生成"工作流提示词基线"快照(用于「老用户不受影响」的可验证断言)。
3
+ *
4
+ * 为什么需要:用户反复强调"老用户更新后不能发现自己用不惯了"。所以每轮改动工作流段时,
5
+ * 必须有机器断言证明:**新段 ⊇ 旧段**(逐行超集)——只允许新增行,不允许改写/删除旧行。
6
+ * 本脚本在改动前跑一次,把当轮基线落到 scripts/fixtures/workflow-baseline.json。
7
+ *
8
+ * 用法:node scripts/dump-workflow-baseline.mjs
9
+ */
10
+ import { writeFileSync, mkdirSync } from 'node:fs'
11
+ import { join, dirname } from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { workflowSection } from '../src/router.js'
14
+
15
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..')
16
+ const variants = []
17
+ for (const quick of [false, true]) {
18
+ for (const taskType of ['from-scratch', 'augment', 'edit', 'summarize', 'unknown']) {
19
+ const cfg = {
20
+ quick,
21
+ mode: 'auto',
22
+ fidelity: 'auto',
23
+ review: 'points',
24
+ quality: 'standard',
25
+ engine: 'auto',
26
+ template: null,
27
+ pauseAfter: [],
28
+ workflowActive: true,
29
+ }
30
+ const sec = workflowSection(taskType, cfg)
31
+ variants.push({ quick, taskType, name: sec.name, lines: sec.text.split('\n') })
32
+ }
33
+ }
34
+ const out = join(root, 'scripts', 'fixtures', 'workflow-baseline.json')
35
+ mkdirSync(dirname(out), { recursive: true })
36
+ writeFileSync(out, JSON.stringify({ note: '工作流提示词基线:新段必须是逐行超集(只增不改不删)', variants }, null, 2) + '\n', 'utf8')
37
+ const total = variants.reduce((n, v) => n + v.lines.length, 0)
38
+ console.log(`baseline 写入 ${out}:${variants.length} 个变体 / ${total} 行`)
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 1.0.0 自包含端到端验证(不需要任何外部资产——夹具即案例):
4
+ * ① 生成夹具 fx-pro(12页)/fx-mini(3页)/seed.pptx
5
+ * ② fx-pro:render → verify(断言 0 错误 / 声明命中 / 建议级仅提示)
6
+ * ③ crosscheck:审阅材料包(全页正文覆盖 + 不做判定;判定交审阅者)
7
+ * ④ measure(浏览器实测兜底:无浏览器自动降级并注明)
8
+ * ⑤ export → zip 结构(12 张 slide、无 chart 部件依赖)
9
+ * ⑥ splice(seed 第 3 页 ← fx-pro 第 6 页)→ 仅 2 条目变化;slice → 单页
10
+ * ⑦ Office COM(可用时)真渲染 spliced[3] / single[1]
11
+ * ⑧ 自产 seed 再导入 → 元素 id 唯一(导入去重回环验证)
12
+ */
13
+ import { rm, mkdir, writeFile, readFile } from 'node:fs/promises'
14
+ import { join, dirname } from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+ import { existsSync } from 'node:fs'
17
+ import { genFixtures } from './fixtures.mjs'
18
+ import { resolveDeck } from '../lib/pptd/schema.js'
19
+ import { renderDeck } from '../lib/pptd/render-html.js'
20
+ import { verifyDeck, measuredCrossCheck } from '../lib/verify.js'
21
+ import { crosscheckDeck } from '../lib/crosscheck.js'
22
+ import { exportPptx } from '../lib/pptd/export-pptx.js'
23
+ import { importPptx } from '../lib/pptd/import-pptx.js'
24
+ import { zipRead } from '../lib/zips.js'
25
+ import { spliceIntoSource, sliceSource, zipDigests } from '../lib/splice.js'
26
+ import { measureLayout } from '../lib/measurement.js'
27
+
28
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..')
29
+ let pass = 0
30
+ let fail = 0
31
+ const ok = (name, cond, extra = '') => {
32
+ console.log(`${cond ? '✓' : '✗'} ${name}${extra ? ` — ${extra}` : ''}`)
33
+ cond ? pass++ : fail++
34
+ }
35
+
36
+ const { fx, seed, pages: seedPages } = await genFixtures()
37
+ const pro = join(fx, 'fx-pro')
38
+ const mini = join(fx, 'fx-mini')
39
+
40
+ // ② fx-pro 全链路
41
+ const ctx = await resolveDeck(pro)
42
+ const rnd = await renderDeck(ctx, {})
43
+ const v = verifyDeck(rnd.layout)
44
+ const errLines = v.text.split('\n').filter((l) => l.includes('[✗]'))
45
+ const confirmNotes = v.text.split('\n').filter((l) => l.includes('预期重叠') || l.includes('预期出界'))
46
+ ok('② fx-pro verify 门禁 0 错误(声明制夹具)', errLines.length === 0, errLines.slice(0, 6).join(' | ').slice(0, 220))
47
+ ok('② fx-pro 声明命中(预期重叠/预期出界确认出现)', confirmNotes.length > 0, `${confirmNotes.length} 条`)
48
+ const hasDecor = v.text.includes('decoration') || v.text.includes('装饰')
49
+ ok('② fx-pro 装饰豁免生效(09 页 deco 无重叠错误)', !errLines.some((l) => l.includes('deco')), '')
50
+ const v08 = v.text.split('第 8 页')[1]?.split('## 第')[0] ?? ''
51
+ ok('② fx-pro A8 场景无误报(08 页无 aesthetic-contrast——渐变深底白字不落背景)', !v08.includes('aesthetic-contrast'),
52
+ v08.match(/aesthetic-contrast|aesthetic-theme/g)?.join(',') ?? '')
53
+
54
+ // ③ crosscheck = 内容审阅材料包(M3 重写 2026-09-18:数字必须放回整句语境,判定交审阅者)
55
+ const cc = crosscheckDeck(ctx)
56
+ const ccText = cc.pages.flatMap((p) => p.texts.map((t) => t.text)).join('\n')
57
+ ok('③ 材料包覆盖全 12 页正文(含跨页 45.6% 原句;表格/图表/讲稿/作者出处字段齐备)',
58
+ cc.pages.length === seedPages && /45\.6%/.test(ccText)
59
+ && cc.pages.every((p) => Array.isArray(p.texts) && Array.isArray(p.tables) && Array.isArray(p.charts)
60
+ && Array.isArray(p.images) && typeof p.notes === 'string' && 'authorSource' in p),
61
+ `${cc.pages.length} 页 / 正文 ${ccText.length} 字符 / 素材清单 ${cc.materials.length} 项`)
62
+ ok('③ 材料包不做判定(无 groups、无 status 状态机)——判定由审阅者按协议给出',
63
+ cc.groups === undefined && cc.pages.every((p) => p.status === undefined),
64
+ Object.keys(cc).join(', '))
65
+
66
+ // ④ measure(浏览器;降级则标注不判失败)
67
+ const m = await measureLayout(pro)
68
+ if (m.measured) {
69
+ const x = measuredCrossCheck(rnd.layout, m.measured)
70
+ const bad = x.filter((f) => ['error', 'warning'].includes(f.severity))
71
+ ok('④ M2 实测(浏览器)→ 与估算零分歧', bad.length === 0, bad.slice(0, 3).map((f) => f.code).join(',') || `${m.pages} 页`)
72
+ } else {
73
+ ok('④ M2 实测不可用(无浏览器/降级)——标注后继续', true, m.notes?.[0] ?? '')
74
+ }
75
+
76
+ // ⑤ export 结构
77
+ const exp = await exportPptx(ctx, { out: join(fx, 'fx-pro.pptx'), engine: 'pptd' })
78
+ const z = zipRead(await readFile(exp.file))
79
+ const slides = [...z.keys()].filter((k) => /^ppt\/slides\/slide\d+\.xml$/.test(k))
80
+ const chartParts = [...z.keys()].filter((k) => k.startsWith('ppt/charts/'))
81
+ ok('⑤ export = 12 张 slide + 无 chart 部件(矢量拼绘)', slides.length === seedPages && chartParts.length === 0, `${slides.length} slides`)
82
+ // 连线方向自证(2026-09-14 真实反馈:预览对、PowerPoint 里线镜像/× 掉一笔)
83
+ ok('⑤ export parity 自证:表/图/线方向全绿(线逐条从 OOXML 反推端点)',
84
+ exp.parity?.ok === true && exp.parity.linesExp > 0 && exp.parity.linesExp === exp.parity.linesOut && exp.parity.linesWrong === 0,
85
+ JSON.stringify(exp.parity))
86
+
87
+ // ⑥ splice / slice(自产 seed:12 页同源工程)
88
+ const spl = await spliceIntoSource({ deckDir: pro, source: seed, page: 6, sourcePage: 3, out: join(fx, 'spliced.pptx') })
89
+ const sd = zipDigests(await readFile(seed))
90
+ const od = zipDigests(await readFile(spl.out))
91
+ const delta = [...sd.keys()].filter((k) => sd.get(k) !== od.get(k))
92
+ ok('⑥ splice:仅目标页 2 条目变化(其余 SHA256 一致)', delta.length === 2 && spl.unchangedCount > 0,
93
+ `delta=${JSON.stringify(delta)} unchanged=${spl.unchangedCount}`)
94
+ const slc = await sliceSource({ source: spl.out, page: 3, out: join(fx, 'single.pptx') })
95
+ const zs = zipRead(await readFile(slc.out))
96
+ const sSingle = [...zs.keys()].filter((k) => /^ppt\/slides\/slide\d+\.xml$/.test(k))
97
+ ok('⑥ slice:仅 1 张 slide + 母版布局保留', sSingle.length === 1 && slc.masters >= 1 && slc.layouts >= 1, `m=${slc.masters} l=${slc.layouts}`)
98
+
99
+ // ⑦ Office COM(可选)
100
+ const { findPowerPoint, renderPptxToPng } = await import('../lib/msrender.js')
101
+ if (findPowerPoint()) {
102
+ // P5 回归:多页参数 pages=[1,3] 应产出两页 PNG(此前 "4,1" 被 PS 解析成 41)
103
+ const r1 = await renderPptxToPng(spl.out, join(fx, 'ren-spliced'), { pages: [1, 3], timeoutMs: 150000 })
104
+ const r2 = await renderPptxToPng(slc.out, join(fx, 'ren-single'), { pages: [1], timeoutMs: 150000 })
105
+ ok('⑦ Office COM 真渲染:spliced pages=[1,3](多页 P5 回归)+ single[1] 产出 PNG',
106
+ r1.files.length === 2 && r2.files.length === 1,
107
+ `spliced=${r1.files.map((f) => f.split(/[\\/]/).pop()).join(',')} single=${r2.files[0]?.split(/[\\/]/).pop()}`)
108
+ } else {
109
+ ok('⑦ Office COM 不可用——标注后继续', true, '无 PowerPoint')
110
+ }
111
+
112
+ // ⑧ 自产 seed 再导入 → id 唯一 + 可渲染(导入去重回环)
113
+ const reDir = join(fx, 'reimport')
114
+ const ri = await importPptx(exp.file, reDir)
115
+ const fsp = await import('node:fs/promises')
116
+ const pageFiles = (await fsp.readdir(join(reDir, 'pages'))).filter((f) => f.endsWith('.yaml'))
117
+ let dup = null
118
+ for (const f of pageFiles) {
119
+ const txt = await readFile(join(reDir, 'pages', f), 'utf8')
120
+ const ids = (txt.match(/elementId: .+$/gm) ?? []).map((s) => s.replace(/^.*elementId: /, '').trim())
121
+ const seen = new Set()
122
+ for (const id of ids) { if (seen.has(id)) { dup = id; break } seen.add(id) }
123
+ if (dup) break
124
+ }
125
+ ok('⑧ 自产 seed 回导:无重复 elementId(导入去重 P5 有效)', dup === null, dup ? `dup=${dup}` : `${pageFiles.length} pages`)
126
+ await rm(reDir, { recursive: true, force: true })
127
+
128
+ console.log(`\n==== E2E(1.0.0) 结果:${pass} 通过 / ${fail} 失败 ====`)
129
+ process.exit(fail > 0 ? 1 : 0)
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * eval-skills-blind.mjs —— 把两个 arm 的产物**匿名化 + 随机化**,生成盲评材料(配合 docs/06 §7.6 协议)。
4
+ *
5
+ * 为什么需要:内置技能是"软收益",自评容易有偏;照 docs/06 的方法做**结构化盲评**时,
6
+ * 必须让评审员只看到产物、看不到来源(否则会从目录名/命名习惯猜到哪份是"新版")。
7
+ *
8
+ * 产出(<out> 下):
9
+ * <out>/盲评-1/deck/{deck.yaml,pages/*.yaml} 源描述(可读文本)
10
+ * <out>/盲评-1/render/NN.png Office 真渲染整页(有 Office 时;无则跳过并提示)
11
+ * <out>/盲评-2/… 另一份
12
+ * <out>/mapping.json 真实映射(**只留档,不给评审员**)
13
+ *
14
+ * 用法:node scripts/eval-skills-blind.mjs <deckA 目录> <deckB 目录> [--out <目录>]
15
+ */
16
+ import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, readdirSync, copyFileSync } from 'node:fs'
17
+ import { join, resolve } from 'node:path'
18
+ import { dirname } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+ import { resolveDeck } from '../lib/pptd/schema.js'
21
+ import { exportPptx } from '../lib/pptd/export-pptx.js'
22
+ import { renderPptxToPng, findPowerPoint } from '../lib/msrender.js'
23
+
24
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..')
25
+ const args = process.argv.slice(2)
26
+ const opt = (k, d) => {
27
+ const i = args.indexOf(k)
28
+ return i >= 0 && args[i + 1] ? args[i + 1] : d
29
+ }
30
+ const positional = args.filter((a, i) => !a.startsWith('--') && !(i > 0 && args[i - 1].startsWith('--')))
31
+ if (positional.length < 2) {
32
+ console.error('用法:node scripts/eval-skills-blind.mjs <deckA 目录> <deckB 目录> [--out <目录>]')
33
+ process.exit(2)
34
+ }
35
+ const [dirA, dirB] = positional
36
+ const outRoot = resolve(opt('--out', join(root, 'examples', 'blind-review')))
37
+
38
+ /** 找到目录下的 deck(本身是 deck 目录,或第一层子目录里含 deck.yaml)。 */
39
+ function deckOf(dir) {
40
+ const base = resolve(dir)
41
+ if (existsSync(join(base, 'deck.yaml'))) return base
42
+ for (const e of readdirSync(base, { withFileTypes: true })) {
43
+ if (e.isDirectory() && existsSync(join(base, e.name, 'deck.yaml'))) return join(base, e.name)
44
+ }
45
+ throw new Error(`找不到 deck.yaml:${dir}`)
46
+ }
47
+
48
+ rmSync(outRoot, { recursive: true, force: true })
49
+ mkdirSync(outRoot, { recursive: true })
50
+
51
+ // 随机分配:真 = 第一份放「盲评-1」
52
+ const aFirst = Math.random() < 0.5
53
+ const order = aFirst ? [['A', dirA], ['B', dirB]] : [['B', dirB], ['A', dirA]]
54
+ const hasOffice = findPowerPoint() !== null
55
+
56
+ const mapping = {}
57
+ for (let i = 0; i < order.length; i++) {
58
+ const [arm, dir] = order[i]
59
+ const label = `盲评-${i + 1}`
60
+ mapping[label] = { arm, source: resolve(dir) }
61
+ const deckDir = deckOf(dir)
62
+ const target = join(outRoot, label)
63
+ mkdirSync(join(target, 'deck', 'pages'), { recursive: true })
64
+ copyFileSync(join(deckDir, 'deck.yaml'), join(target, 'deck', 'deck.yaml'))
65
+ for (const f of readdirSync(join(deckDir, 'pages'))) {
66
+ if (f.endsWith('.yaml')) copyFileSync(join(deckDir, 'pages', f), join(target, 'deck', 'pages', f))
67
+ }
68
+ // 取 pptx:优先复用已有 out.pptx,否则现导一份(不污染原目录的 deck.yaml)
69
+ let pptx = join(deckDir, 'out.pptx')
70
+ if (!existsSync(pptx)) {
71
+ const ctx = await resolveDeck(deckDir)
72
+ const r = await exportPptx(ctx, { out: join(target, 'blind-export.pptx'), engine: 'pptd' })
73
+ pptx = r.file
74
+ const out = ctx.pages.length === 1 ? 1 : r.slides
75
+ void out
76
+ }
77
+ if (hasOffice) {
78
+ try {
79
+ const r = await renderPptxToPng(pptx, join(target, 'render'), { noWatermark: true })
80
+ console.log(`${label}: ${arm} → Office 真渲染 ${r.pages} 页`)
81
+ } catch (error) {
82
+ console.log(`${label}: ${arm} → Office 渲染失败(${String(error?.message ?? error).split('\n')[0].slice(0, 80)})——评审退化为只看文本`)
83
+ }
84
+ } else {
85
+ console.log(`${label}: ${arm} → 本机无 Office,跳过真渲染(评审只看文本,需在结论中标注)`)
86
+ }
87
+ console.log(` 源 deck:${deckDir}`)
88
+ }
89
+ writeFileSync(join(outRoot, 'mapping.json'), JSON.stringify({ aFirst, hasOffice, mapping }, null, 2) + '\n', 'utf8')
90
+ console.log('\n真实映射(只留档,勿给评审员):', JSON.stringify(mapping))
91
+ console.log('盲评材料:', outRoot)
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * eval-skills-score.mjs —— 内置技能效果对照的**打分器**(纯本地,不调用模型)
4
+ *
5
+ * 用途:把两个 arm 各自产出的 deck 目录按同一口径度量,输出对照表。配合 docs/06 §7 的手工流程使用:
6
+ * ① 用**前一版**包跑一次(无新技能)→ 产物 deckA
7
+ * ② 用**当前**包跑一次(含新技能)→ 产物 deckB(两臂输入必须是同一份 brief)
8
+ * ③ node scripts/eval-skills-score.mjs --a <deckA 目录> --b <deckB 目录>
9
+ * 门禁指标由本仓库的同一个 lib 事后复算,避免"自己评自己"。
10
+ *
11
+ * 指标:页数 / 元素数 / 文本字数 / 图表与表格数 / [✗]错误 / [⚠]警告 / [·]建议 /
12
+ * AI 味信号命中(按 ppt-studio-copy §4-5 的信号表扫描页面文本)/ 标注 source 的页数。
13
+ *
14
+ * 用法:node scripts/eval-skills-score.mjs --a <dir> --b <dir> [--label-a X] [--label-b Y] [--out RESULT.md]
15
+ */
16
+ import { writeFileSync, existsSync, readdirSync, statSync } from 'node:fs'
17
+ import { join, dirname, resolve } from 'node:path'
18
+ import { fileURLToPath } from 'node:url'
19
+ import { resolveDeck } from '../lib/pptd/schema.js'
20
+ import { renderDeck } from '../lib/pptd/render-html.js'
21
+ import { verifyDeck } from '../lib/verify.js'
22
+
23
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..')
24
+ const args = process.argv.slice(2)
25
+ const opt = (k, d) => {
26
+ const i = args.indexOf(k)
27
+ return i >= 0 && args[i + 1] ? args[i + 1] : d
28
+ }
29
+
30
+ /** AI 味信号(与 ppt-studio-copy §4/§5 对齐;成组才算 tell,这里只做粗计数)。 */
31
+ const AI_SIGNALS = [
32
+ ['里程碑/仪式感', /里程碑|标志着|见证了|是.{0,6}的体现|新篇章/],
33
+ ['宣传腔', /充满活力|令人瞩目|业界领先|坐落于|蓬勃发展|未来可期|迈上新台阶/],
34
+ ['模糊归因', /行业报告显示|专家认为|普遍认为|相关研究表明/],
35
+ ['否定式排比', /不仅.{0,18}(?:更|而且|还)|不是.{0,14}而是/],
36
+ ['填充连接词', /此外|与此同时|值得注意的是|深入探讨|格局/],
37
+ ['破折号揭示', /——/],
38
+ ['通用积极结尾', /坚定信心|再创佳绩|持续向好|奠定了坚实基础/],
39
+ ['系动词回避', /作为.{0,10}(?:的)?(?:重要|关键|核心)|拥有.{0,8}能力/],
40
+ ]
41
+
42
+ /** 允许直接给 deck 目录,或给一个"里面第一个含 deck.yaml 的子目录"的父目录。 */
43
+ function deckOf(dir) {
44
+ if (existsSync(join(dir, 'deck.yaml'))) return dir
45
+ try {
46
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
47
+ if (e.isDirectory() && existsSync(join(dir, e.name, 'deck.yaml'))) return join(dir, e.name)
48
+ }
49
+ } catch { /* 目录不存在 */ }
50
+ return null
51
+ }
52
+
53
+ async function score(label, dir) {
54
+ const out = { label, deck: null }
55
+ const deckDir = deckOf(resolve(dir))
56
+ if (deckDir === null) return { ...out, error: `未在 ${dir} 下找到 deck.yaml` }
57
+ out.deck = deckDir
58
+ const ctx = await resolveDeck(deckDir)
59
+ const r = await renderDeck(ctx, {})
60
+ const v = verifyDeck(r.layout)
61
+ const lines = v.text.split('\n')
62
+ out.gates = {
63
+ errors: lines.filter((l) => l.includes('[✗]')).length,
64
+ warnings: lines.filter((l) => l.includes('[⚠]')).length,
65
+ suggestions: lines.filter((l) => l.includes('[·]')).length,
66
+ }
67
+ const texts = []
68
+ let elements = 0
69
+ let charts = 0
70
+ let tables = 0
71
+ for (const p of ctx.pages) {
72
+ for (const el of p.page.elements ?? []) {
73
+ elements++
74
+ if (el.elementType === 'chart') charts++
75
+ if (el.elementType === 'table') tables++
76
+ if (el.elementType === 'text') texts.push(String(el.content?.text ?? ''))
77
+ }
78
+ }
79
+ const all = texts.join('\n')
80
+ out.scale = { pages: ctx.pages.length, elements, charts, tables, textChars: all.length }
81
+ out.aiSignals = AI_SIGNALS
82
+ .map(([name, re]) => [name, (all.match(new RegExp(re.source, 'g')) ?? []).length])
83
+ .filter(([, n]) => n > 0)
84
+ out.aiTotal = out.aiSignals.reduce((n, [, c]) => n + c, 0)
85
+ out.dataQuality = {
86
+ pagesWithSource: ctx.pages.filter((p) => typeof p.page.source === 'string' && p.page.source.trim().length > 0).length,
87
+ unitKindsPresent: ['%', '小时', '分钟', '园区', '人'].filter((u) => all.includes(u)).length,
88
+ hasChart: charts > 0,
89
+ hasTable: tables > 0,
90
+ valueMentions: (all.match(/\d+/g) ?? []).length,
91
+ }
92
+ return out
93
+ }
94
+
95
+ const dirA = opt('--a')
96
+ const dirB = opt('--b')
97
+ if (dirA === undefined || dirB === undefined) {
98
+ console.error('用法:node scripts/eval-skills-score.mjs --a <deckA 目录> --b <deckB 目录> [--label-a X] [--label-b Y] [--out RESULT.md]')
99
+ process.exit(2)
100
+ }
101
+ const A = await score(opt('--label-a', 'A'), dirA)
102
+ const Bd = await score(opt('--label-b', 'B'), dirB)
103
+
104
+ const md = []
105
+ md.push('# 内置技能效果对照(同一 brief、同一模型、同一装配下的两个 arm)', '')
106
+ md.push('| 指标 | A | B |', '|---|---|---|')
107
+ const row = (k, f) => md.push(`| ${k} | ${f(A)} | ${f(Bd)} |`)
108
+ row('页数', (x) => x.scale?.pages ?? '—')
109
+ row('元素数', (x) => x.scale?.elements ?? '—')
110
+ row('文本总字数', (x) => x.scale?.textChars ?? '—')
111
+ row('图表数 / 表格数', (x) => `${x.scale?.charts ?? '—'} / ${x.scale?.tables ?? '—'}`)
112
+ row('门禁错误 [✗]', (x) => x.gates?.errors ?? '—')
113
+ row('门禁警告 [⚠]', (x) => x.gates?.warnings ?? '—')
114
+ row('审美建议 [·]', (x) => x.gates?.suggestions ?? '—')
115
+ row('AI 味信号命中', (x) => x.aiTotal ?? '—')
116
+ row('标注 source 的页数', (x) => x.dataQuality?.pagesWithSource ?? '—')
117
+ row('出现的单位种类', (x) => x.dataQuality?.unitKindsPresent ?? '—')
118
+ md.push('', '## 明细', '')
119
+ for (const x of [A, Bd]) md.push(`### ${x.label}${x.deck ? `(${x.deck})` : ''}`, '', '```json', JSON.stringify(x, null, 2), '```', '')
120
+ const text = md.join('\n')
121
+ console.log(text)
122
+ const outFile = opt('--out')
123
+ if (outFile !== undefined) {
124
+ writeFileSync(outFile, text + '\n', 'utf8')
125
+ console.log(`\n(已写入 ${outFile})`)
126
+ }
127
+ if (A.error !== undefined || Bd.error !== undefined) process.exit(1)
128
+ void statSync
129
+ void root
@@ -0,0 +1,17 @@
1
+ # 原始模板逐页 PNG 导出(保真对照用):PowerPoint COM(本机有 Office 时可用)
2
+ param([string]$Src = 'C:\Users\11867\Desktop\ppt模板库', [string]$Out = 'C:\Users\11867\.dsh\ppt-studio\fidelity\orig')
3
+ $pp = New-Object -ComObject PowerPoint.Application
4
+ $pp.Visible = -1
5
+ foreach ($f in Get-ChildItem $Src -Filter '*.pptx') {
6
+ $dir = Join-Path $Out $f.BaseName
7
+ New-Item -ItemType Directory -Force -Path $dir | Out-Null
8
+ $pres = $pp.Presentations.Open($f.FullName, -1, 0, 0)
9
+ $n = $pres.Slides.Count
10
+ for ($i = 1; $i -le $n; $i++) {
11
+ $pres.Slides.Item($i).Export((Join-Path $dir ('{0:D2}.png' -f $i)), 'PNG', 1920, 1080)
12
+ }
13
+ $pres.Close()
14
+ Write-Output ("{0}: {1} slides" -f $f.Name, $n)
15
+ }
16
+ $pp.Quit()
17
+ Write-Output 'DONE'