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.
- package/LICENSE +21 -0
- package/README.md +480 -0
- package/agent-presets/ppt/agent.cordis.yml +308 -0
- package/agent-presets/ppt/preset.yml +3 -0
- package/cordis.patch.yml +29 -0
- package/docs/00-/350/256/276/350/256/241/346/272/220.md +537 -0
- package/docs/01-/351/234/200/346/261/202/344/270/216/347/233/256/346/240/207.md +136 -0
- package/docs/02-/346/212/200/346/234/257/346/212/245/345/221/212.md +483 -0
- package/docs/03-/346/233/264/346/226/260/346/227/245/345/277/227.md +2898 -0
- 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
- package/docs/05-/350/277/255/344/273/243/346/265/201/347/250/213.md +210 -0
- package/docs/06-/350/257/204/345/256/241/344/270/216/346/265/213/350/257/225.md +296 -0
- package/docs/fidelity-report.md +225 -0
- package/examples/fx/fx-mini/deck.yaml +39 -0
- package/examples/fx/fx-mini/pages/01.yaml +6 -0
- package/examples/fx/fx-mini/pages/02.yaml +2 -0
- package/examples/fx/fx-mini/pages/03.yaml +6 -0
- package/examples/fx/fx-pro/deck.yaml +48 -0
- package/examples/fx/fx-pro/pages/01_cover.yaml +40 -0
- package/examples/fx/fx-pro/pages/02_arch.yaml +55 -0
- package/examples/fx/fx-pro/pages/03_table.yaml +20 -0
- package/examples/fx/fx-pro/pages/04_chart.yaml +40 -0
- package/examples/fx/fx-pro/pages/05_timeline.yaml +71 -0
- package/examples/fx/fx-pro/pages/06_longtext.yaml +13 -0
- package/examples/fx/fx-pro/pages/07_safearea.yaml +33 -0
- package/examples/fx/fx-pro/pages/08_contrast.yaml +22 -0
- package/examples/fx/fx-pro/pages/09_custgeom.yaml +32 -0
- package/examples/fx/fx-pro/pages/10_numbers.yaml +25 -0
- package/examples/fx/fx-pro/pages/11_section.yaml +20 -0
- package/examples/fx/fx-pro/pages/12_summary.yaml +57 -0
- package/examples/fx/fx-pro/preview/01_pages_01_cover.html +1 -0
- package/examples/fx/fx-pro/preview/02_pages_02_arch.html +3 -0
- package/examples/fx/fx-pro/preview/03_pages_03_table.html +1 -0
- package/examples/fx/fx-pro/preview/04_pages_04_chart.html +3 -0
- package/examples/fx/fx-pro/preview/05_pages_05_timeline.html +2 -0
- package/examples/fx/fx-pro/preview/06_pages_06_longtext.html +4 -0
- package/examples/fx/fx-pro/preview/07_pages_07_safearea.html +2 -0
- package/examples/fx/fx-pro/preview/08_pages_08_contrast.html +1 -0
- package/examples/fx/fx-pro/preview/09_pages_09_custgeom.html +1 -0
- package/examples/fx/fx-pro/preview/10_pages_10_numbers.html +1 -0
- package/examples/fx/fx-pro/preview/11_pages_11_section.html +1 -0
- package/examples/fx/fx-pro/preview/12_pages_12_summary.html +1 -0
- package/examples/fx/fx-pro/preview/deck.html +10 -0
- package/examples/fx/fx-pro/preview/layout.json +2325 -0
- package/examples/fx/fx-pro/preview/measured.json +814 -0
- package/examples/fx/fx-pro.pptx +0 -0
- package/examples/fx/ren-single/01.png +0 -0
- package/examples/fx/ren-spliced/01.png +0 -0
- package/examples/fx/ren-spliced/03.png +0 -0
- package/examples/fx/seed.pptx +0 -0
- package/examples/fx/single.pptx +0 -0
- package/examples/fx/spliced.pptx +0 -0
- package/examples/smoke/deck.yaml +26 -0
- package/examples/smoke/imported/deck.yaml +30 -0
- package/examples/smoke/imported/import-styles.json +126 -0
- package/examples/smoke/imported/pages/slide_01.yaml +31 -0
- package/examples/smoke/imported/pages/slide_02.yaml +38 -0
- package/examples/smoke/imported/pages/slide_03.yaml +55 -0
- package/examples/smoke/imported/preview-rt/01_pages_slide_01.html +1 -0
- package/examples/smoke/imported/preview-rt/02_pages_slide_02.html +1 -0
- package/examples/smoke/imported/preview-rt/03_pages_slide_03.html +1 -0
- package/examples/smoke/imported/preview-rt/deck.html +1 -0
- package/examples/smoke/imported/preview-rt/layout.json +434 -0
- package/examples/smoke/imported/reference/previews/01.png +0 -0
- package/examples/smoke/imported/reference/previews/02.png +0 -0
- package/examples/smoke/imported/reference/previews/03.png +0 -0
- package/examples/smoke/imported/source.pptx +0 -0
- package/examples/smoke/media-test.png +0 -0
- package/examples/smoke/out-smoke.pptx +0 -0
- package/examples/smoke/pages/01_cover.yaml +27 -0
- package/examples/smoke/pages/02_content.yaml +42 -0
- package/examples/smoke/pages/03_chart.yaml +35 -0
- package/examples/smoke/preview/01_pages_01_cover.html +1 -0
- package/examples/smoke/preview/02_pages_02_content.html +1 -0
- package/examples/smoke/preview/03_pages_03_chart.html +1 -0
- package/examples/smoke/preview/deck.html +1 -0
- package/examples/smoke/preview/layout.json +378 -0
- package/lib/autodeclare.js +40 -0
- package/lib/commands.js +161 -0
- package/lib/crosscheck.js +300 -0
- package/lib/imgmeta.js +55 -0
- package/lib/index.js +239 -0
- package/lib/measurement.js +209 -0
- package/lib/msrender.js +123 -0
- package/lib/pptd/export-pptx.js +686 -0
- package/lib/pptd/import-pptx.js +774 -0
- package/lib/pptd/layout.js +209 -0
- package/lib/pptd/render-html.js +254 -0
- package/lib/pptd/schema.js +518 -0
- package/lib/pptd/svgCharts.js +147 -0
- package/lib/pptxPy.js +205 -0
- package/lib/preset-delivery.js +195 -0
- package/lib/preview-server.js +164 -0
- package/lib/router.js +165 -0
- package/lib/scaffold.js +315 -0
- package/lib/skill.js +174 -0
- package/lib/splice.js +219 -0
- package/lib/state.js +88 -0
- package/lib/surgical.js +369 -0
- package/lib/templates.js +400 -0
- package/lib/tools.js +955 -0
- package/lib/verify.js +828 -0
- package/lib/xmljs.js +112 -0
- package/lib/zips.js +117 -0
- package/package.json +81 -0
- package/scripts/audit-manual-facts.mjs +294 -0
- package/scripts/audit-template-fidelity.mjs +162 -0
- package/scripts/bench-m2.mjs +48 -0
- package/scripts/build-templates.mjs +63 -0
- package/scripts/build.mjs +14 -0
- package/scripts/build.sh +7 -0
- package/scripts/check-lib-freshness.mjs +70 -0
- package/scripts/check-preset.mjs +189 -0
- package/scripts/check-tracked-lf.mjs +84 -0
- package/scripts/dbg-import.mjs +13 -0
- package/scripts/dbg-media.mjs +10 -0
- package/scripts/dbg-tpl-inventory.mjs +23 -0
- package/scripts/dump-workflow-baseline.mjs +38 -0
- package/scripts/e2e-1.0.mjs +129 -0
- package/scripts/eval-skills-blind.mjs +91 -0
- package/scripts/eval-skills-score.mjs +129 -0
- package/scripts/export-orig-templates.ps1 +17 -0
- package/scripts/fixtures/workflow-baseline.json +300 -0
- package/scripts/fixtures.mjs +501 -0
- package/scripts/import-user-templates.mjs +60 -0
- package/scripts/install-desktop.mjs +312 -0
- package/scripts/install.mjs +209 -0
- package/scripts/migrate-templates-dualtrack.mjs +71 -0
- package/scripts/preflight-1.0.mjs +198 -0
- package/scripts/probe-asset-freshness.mjs +57 -0
- package/scripts/probe-effect-semantics.mjs +69 -0
- package/scripts/probe-reparse-safety.mjs +32 -0
- package/scripts/rebuild-templates-c11.mjs +69 -0
- package/scripts/regression-real.mjs +84 -0
- package/scripts/release-sync.mjs +425 -0
- package/scripts/shot-templates.mjs +49 -0
- package/scripts/smoke.mjs +2512 -0
- package/scripts/verify-bundle-install.mjs +281 -0
- package/scripts/verify-fresh-install.mjs +260 -0
- package/skills/ppt-studio-copy/SKILL.md +139 -0
- package/skills/ppt-studio-craft/SKILL.md +171 -0
- package/skills/ppt-studio-data/SKILL.md +173 -0
- package/skills/ppt-studio-manual/SKILL.md +104 -0
- package/templates/academic-white/deck.yaml +24 -0
- package/templates/academic-white/pages/_chart.yaml +43 -0
- package/templates/academic-white/pages/_compare.yaml +37 -0
- package/templates/academic-white/pages/_content.yaml +35 -0
- package/templates/academic-white/pages/_cover.yaml +28 -0
- package/templates/academic-white/pages/_ending.yaml +28 -0
- package/templates/academic-white/pages/_flow.yaml +61 -0
- package/templates/academic-white/preview.png +0 -0
- package/templates/academic-white/template.yaml +14 -0
- package/templates/business-blue/deck.yaml +28 -0
- package/templates/business-blue/pages/_chart.yaml +36 -0
- package/templates/business-blue/pages/_compare.yaml +45 -0
- package/templates/business-blue/pages/_content.yaml +80 -0
- package/templates/business-blue/pages/_cover.yaml +39 -0
- package/templates/business-blue/pages/_ending.yaml +32 -0
- package/templates/business-blue/pages/_flow.yaml +87 -0
- package/templates/business-blue/preview.png +0 -0
- package/templates/business-blue/template.yaml +14 -0
- package/templates/pitch-bold/deck.yaml +23 -0
- package/templates/pitch-bold/pages/_chart.yaml +32 -0
- package/templates/pitch-bold/pages/_compare.yaml +36 -0
- package/templates/pitch-bold/pages/_content.yaml +58 -0
- package/templates/pitch-bold/pages/_cover.yaml +31 -0
- package/templates/pitch-bold/pages/_ending.yaml +27 -0
- package/templates/pitch-bold/pages/_flow.yaml +46 -0
- package/templates/pitch-bold/preview.png +0 -0
- package/templates/pitch-bold/template.yaml +14 -0
- package/templates/tech-dark/deck.yaml +24 -0
- package/templates/tech-dark/pages/_chart.yaml +27 -0
- package/templates/tech-dark/pages/_compare.yaml +38 -0
- package/templates/tech-dark/pages/_content.yaml +51 -0
- package/templates/tech-dark/pages/_cover.yaml +30 -0
- package/templates/tech-dark/pages/_ending.yaml +25 -0
- package/templates/tech-dark/pages/_flow.yaml +80 -0
- package/templates/tech-dark/preview.png +0 -0
- package/templates/tech-dark/template.yaml +14 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 1.0.0 发布前预检(preflight):
|
|
4
|
+
* 在 smoke(139 断言)之外补"鲁棒性/边界/重复调用/性能"簇——坏输入不崩溃、防呆到位、
|
|
5
|
+
* 幂等成立、百页仍快、媒体/几何元组经 splice 链路不坏。
|
|
6
|
+
* 独立于 smoke 运行:`node scripts/preflight-1.0.mjs`(全绿方可用于发布)。
|
|
7
|
+
*/
|
|
8
|
+
import { join, dirname } from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import { rm, mkdir, writeFile, readFile } from 'node:fs/promises'
|
|
11
|
+
import { existsSync } from 'node:fs'
|
|
12
|
+
import { tmpdir } from 'node:os'
|
|
13
|
+
import { resolveDeck, PptError } from '../lib/pptd/schema.js'
|
|
14
|
+
import { renderDeck } from '../lib/pptd/render-html.js'
|
|
15
|
+
import { verifyDeck } from '../lib/verify.js'
|
|
16
|
+
import { exportPptx } from '../lib/pptd/export-pptx.js'
|
|
17
|
+
import { zipRead, decodeXml } from '../lib/zips.js'
|
|
18
|
+
import { spliceIntoSource, sliceSource } from '../lib/splice.js'
|
|
19
|
+
|
|
20
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
21
|
+
let pass = 0
|
|
22
|
+
let fail = 0
|
|
23
|
+
const ok = (name, cond, extra = '') => {
|
|
24
|
+
console.log(`${cond ? '✓' : '✗'} ${name}${extra ? ` — ${extra}` : ''}`)
|
|
25
|
+
cond ? pass++ : fail++
|
|
26
|
+
}
|
|
27
|
+
const fixture = async (name, deckYaml, pages) => {
|
|
28
|
+
const dir = join(tmpdir(), `ppt-preflight-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`)
|
|
29
|
+
await mkdir(join(dir, 'pages'), { recursive: true })
|
|
30
|
+
await writeFile(join(dir, 'deck.yaml'), deckYaml)
|
|
31
|
+
for (const [ref, text] of Object.entries(pages)) await writeFile(join(dir, ref), text)
|
|
32
|
+
return dir
|
|
33
|
+
}
|
|
34
|
+
const rmFixture = (dir) => rm(dir, { recursive: true, force: true }).catch(() => {})
|
|
35
|
+
|
|
36
|
+
// ── A. 坏输入不崩溃 ─────────────────────────────────────────────
|
|
37
|
+
{
|
|
38
|
+
// A1 无 deck.yaml
|
|
39
|
+
let e1 = null
|
|
40
|
+
try { await resolveDeck(join(tmpdir(), 'no-such-deck-xx')) } catch (e) { e1 = e }
|
|
41
|
+
ok('A1 缺 deck.yaml → 明确报错(非崩溃)', e1 !== null && /deck\.yaml not found/.test(String(e1)),
|
|
42
|
+
String(e1).slice(0, 40))
|
|
43
|
+
// A2 空 pages(1.0.0 拦截:0 页会产出打不开的 pptx)
|
|
44
|
+
const d2 = await fixture('empty', ['version: 1', 'title: empty', 'size: [960, 540]', 'pages: []', ''].join('\n'), {})
|
|
45
|
+
let e2 = null
|
|
46
|
+
try { await resolveDeck(d2) } catch (e) { e2 = e }
|
|
47
|
+
ok('A2 空 pages → 拦截(防静默产 0 页 pptx)', e2 !== null && /at least 1 page/.test(String(e2)),
|
|
48
|
+
e2 ? 'blocked' : 'NOT BLOCKED (bug)')
|
|
49
|
+
await rmFixture(d2)
|
|
50
|
+
// A3 YAML 语法错误 → B2 指引
|
|
51
|
+
const d3 = await fixture('badyaml', ['version: 1', 'title: x', 'size: [960, 540]', 'pages:', ' - pages/01.yaml', ''].join('\n'),
|
|
52
|
+
{ 'pages/01.yaml': ['pageType: content', 'elements: [', ''].join('\n') })
|
|
53
|
+
let e3 = null
|
|
54
|
+
try { await resolveDeck(d3) } catch (e) { e3 = e }
|
|
55
|
+
ok('A3 页面 YAML 语法错误 → 提示(非 JSON 狗血堆栈)', e3 !== null && /YAML 解析失败/.test(String(e3)),
|
|
56
|
+
String(e3).slice(0, 60))
|
|
57
|
+
await rmFixture(d3)
|
|
58
|
+
// A4 expectedOverlaps 指向不存在 id → 防呆(1.0.0 新增)
|
|
59
|
+
const d4 = await fixture('ghostid', ['version: 1', 'title: x', 'size: [960, 540]', 'pages:', ' - pages/01.yaml', ''].join('\n'),
|
|
60
|
+
{ 'pages/01.yaml': ['pageType: content', 'elements:', ' - elementId: a', ' elementType: text', ' bounds: [10, 20, 100, 30]', ' content: {text: "hi"}', 'expectedOverlaps:', ' - {pair: [a, ghost]}', ''].join('\n') })
|
|
61
|
+
let e4 = null
|
|
62
|
+
try { await resolveDeck(d4) } catch (e) { e4 = e }
|
|
63
|
+
ok('A4 expectedOverlaps id 防呆(ghost 元素 → 报错)', e4 !== null && /ghost.*不是本页元素 id/.test(String(e4)),
|
|
64
|
+
e4 ? 'blocked' : 'NOT BLOCKED (bug)')
|
|
65
|
+
await rmFixture(d4)
|
|
66
|
+
// A5 极长文本(2000 字)不崩溃
|
|
67
|
+
const longText = '标'.repeat(2000)
|
|
68
|
+
const d5 = await fixture('long', ['version: 1', 'title: x', 'size: [960, 540]', 'pages:', ' - pages/01.yaml', ''].join('\n'),
|
|
69
|
+
{ 'pages/01.yaml': [`pageType: content`, 'elements:', ' - elementId: t', ' elementType: text', ' bounds: [10, 20, 200, 60]', ` content: {text: ${JSON.stringify(longText)}, fontSize: 14}`, ''].join('\n') })
|
|
70
|
+
let e5 = null
|
|
71
|
+
let v5 = null
|
|
72
|
+
try {
|
|
73
|
+
const c = await resolveDeck(d5)
|
|
74
|
+
const r = await renderDeck(c, {})
|
|
75
|
+
v5 = verifyDeck(r.layout)
|
|
76
|
+
} catch (e) { e5 = e }
|
|
77
|
+
ok('A5 2000 字文本 → 渲染/校验不崩溃(估算保守报溢出)', e5 === null && v5 !== null && v5.text.includes('text-overflow'),
|
|
78
|
+
e5 ? String(e5).slice(0, 60) : '')
|
|
79
|
+
await rmFixture(d5)
|
|
80
|
+
// A6 零元素页面 → 审阅无断言错误
|
|
81
|
+
const d6 = await fixture('noel', ['version: 1', 'title: x', 'size: [960, 540]', 'pages:', ' - pages/01.yaml', ''].join('\n'),
|
|
82
|
+
{ 'pages/01.yaml': ['pageType: content', '', ''].join('\n') })
|
|
83
|
+
let e6 = null
|
|
84
|
+
try {
|
|
85
|
+
const c = await resolveDeck(d6)
|
|
86
|
+
const r = await renderDeck(c, {})
|
|
87
|
+
const t = verifyDeck(r.layout).text
|
|
88
|
+
if (/✗/.test(t)) e6 = new Error('unexpected errors in empty page')
|
|
89
|
+
} catch (e) { e6 = e }
|
|
90
|
+
ok('A6 空页面 → 渲染/校验正常(无断言崩溃)', e6 === null, '')
|
|
91
|
+
await rmFixture(d6)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── B. 边界形态 ─────────────────────────────────────────────────
|
|
95
|
+
{
|
|
96
|
+
// B1 单页 deck 全链路(render → verify → export → zip 有 1 张 slide)
|
|
97
|
+
const d1 = await fixture('single', ['version: 1', 'title: single', 'size: [960, 540]',
|
|
98
|
+
'theme: {colors: {ink: "#3E4E63"}, textStyles: {body: {fontSize: 14, color: "$ink"}}}',
|
|
99
|
+
'pages:', ' - pages/01.yaml', ''].join('\n'),
|
|
100
|
+
{ 'pages/01.yaml': ['pageType: content', 'elements:', ' - elementId: t', ' elementType: text', ' bounds: [10, 20, 300, 40]', ' content: {text: "单页文档", style: "$body"}', ''].join('\n') })
|
|
101
|
+
let b1 = null
|
|
102
|
+
try {
|
|
103
|
+
const c = await resolveDeck(d1)
|
|
104
|
+
await renderDeck(c, {})
|
|
105
|
+
const r = await exportPptx(c, { out: join(d1, 'out.pptx'), engine: 'pptd' })
|
|
106
|
+
const z = zipRead(await readFile(r.file))
|
|
107
|
+
const slides = [...z.keys()].filter((k) => /^ppt\/slides\/slide\d+\.xml$/.test(k))
|
|
108
|
+
if (slides.length !== 1) b1 = new Error(`slides=${slides.length}`)
|
|
109
|
+
} catch (e) { b1 = e }
|
|
110
|
+
ok('B1 单页 deck → export 恰 1 张 slide', b1 === null, b1 ? String(b1) : '')
|
|
111
|
+
await rmFixture(d1)
|
|
112
|
+
// B2 wrap=false 水平溢出门禁
|
|
113
|
+
const d2 = await fixture('wrap', ['version: 1', 'title: x', 'size: [960, 540]', 'pages:', ' - pages/01.yaml', ''].join('\n'),
|
|
114
|
+
{ 'pages/01.yaml': ['pageType: content', 'elements:', ' - elementId: t', ' elementType: text', ' bounds: [10, 20, 100, 30]', ' content: {text: "这是一段非常长的文本用来触发水平溢出判断", fontSize: 14, wrap: false}', ''].join('\n') })
|
|
115
|
+
let b2 = null
|
|
116
|
+
try {
|
|
117
|
+
const c = await resolveDeck(d2)
|
|
118
|
+
const r = await renderDeck(c, {})
|
|
119
|
+
if (!verifyDeck(r.layout).text.includes('水平溢出')) b2 = new Error('no horizontal overflow error')
|
|
120
|
+
} catch (e) { b2 = e }
|
|
121
|
+
ok('B2 wrap=false 水平溢出 → 门禁 error', b2 === null, b2 ? String(b2) : '')
|
|
122
|
+
await rmFixture(d2)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── C. 幂等 / 重复调用 ──────────────────────────────────────────
|
|
126
|
+
{
|
|
127
|
+
// C1 同一导出路径写两次(覆盖)→ 成功且 zip 可读
|
|
128
|
+
const d1 = await fixture('idem', ['version: 1', 'title: idem', 'size: [960, 540]', 'pages:', ' - pages/01.yaml', ''].join('\n'),
|
|
129
|
+
{ 'pages/01.yaml': ['pageType: content', 'elements:', ' - elementId: t', ' elementType: text', ' bounds: [10, 20, 200, 40]', ' content: {text: "幂等"}', ''].join('\n') })
|
|
130
|
+
let c1 = null
|
|
131
|
+
try {
|
|
132
|
+
const c = await resolveDeck(d1)
|
|
133
|
+
await exportPptx(c, { out: join(d1, 'x.pptx'), engine: 'pptd' })
|
|
134
|
+
const r = await exportPptx(c, { out: join(d1, 'x.pptx'), engine: 'pptd' })
|
|
135
|
+
const z = zipRead(await readFile(r.file))
|
|
136
|
+
if (!z.has('ppt/presentation.xml')) c1 = new Error('rewritten zip unreadable')
|
|
137
|
+
} catch (e) { c1 = e }
|
|
138
|
+
ok('C1 同路径导出两次(覆盖)→ 产物可读', c1 === null, c1 ? String(c1) : '')
|
|
139
|
+
await rmFixture(d1)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── D. 性能上限(100 页渲染 < 10s)───────────────────────────────
|
|
143
|
+
{
|
|
144
|
+
const pagesRefs = []
|
|
145
|
+
const pages = {}
|
|
146
|
+
for (let i = 1; i <= 100; i++) {
|
|
147
|
+
pagesRefs.push(` - pages/${String(i).padStart(2, '0')}.yaml`)
|
|
148
|
+
pages[`pages/${String(i).padStart(2, '0')}.yaml`] = [
|
|
149
|
+
'pageType: content', 'elements:',
|
|
150
|
+
' - elementId: t', ' elementType: text', ' bounds: [60, 60, 400, 40]',
|
|
151
|
+
` content: {text: "第 ${i} 页 / 性能基线样本 0123456789"}`, ''].join('\n')
|
|
152
|
+
}
|
|
153
|
+
const d = await fixture('perf', ['version: 1', 'title: perf', 'size: [960, 540]', 'pages:', ...pagesRefs, ''].join('\n'), pages)
|
|
154
|
+
let d1 = null
|
|
155
|
+
let ms = 0
|
|
156
|
+
try {
|
|
157
|
+
const c = await resolveDeck(d)
|
|
158
|
+
const t0 = Date.now()
|
|
159
|
+
const r = await renderDeck(c, {})
|
|
160
|
+
ms = Date.now() - t0
|
|
161
|
+
if (r.htmlFiles.length !== 100) d1 = new Error(`html=${r.htmlFiles.length}`)
|
|
162
|
+
} catch (e) { d1 = e }
|
|
163
|
+
ok('D1 100 页渲染 < 10s(性能上限)', d1 === null && ms < 10000, `${ms}ms`)
|
|
164
|
+
await rmFixture(d)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── E. 媒体元组经 splice 链路不坏(image 页 → splice 进 seed)─────
|
|
168
|
+
{
|
|
169
|
+
const tinyPng = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64')
|
|
170
|
+
const d = await fixture('media', ['version: 1', 'title: media', 'size: [960, 540]',
|
|
171
|
+
'theme: {colors: {ink: "#3E4E63"}, textStyles: {body: {fontSize: 14, color: "$ink"}}}',
|
|
172
|
+
'pages:', ' - pages/01.yaml', ' - pages/02.yaml', ''].join('\n'),
|
|
173
|
+
{
|
|
174
|
+
'pages/01.yaml': ['pageType: content', 'elements:', ' - elementId: img', ' elementType: image', ' bounds: [10, 20, 120, 90]', ' src: "media/dot.png"', ' fit: cover', ''].join('\n'),
|
|
175
|
+
'pages/02.yaml': ['pageType: content', 'elements:', ' - elementId: t', ' elementType: text', ' bounds: [10, 20, 200, 40]', ' content: {text: "第二页"}', ''].join('\n'),
|
|
176
|
+
})
|
|
177
|
+
await mkdir(join(d, 'media'), { recursive: true })
|
|
178
|
+
await writeFile(join(d, 'media', 'dot.png'), tinyPng)
|
|
179
|
+
let e1 = null
|
|
180
|
+
let spl = null
|
|
181
|
+
try {
|
|
182
|
+
const c = await resolveDeck(d)
|
|
183
|
+
const seed = await exportPptx(c, { out: join(d, 'seed.pptx'), engine: 'pptd' })
|
|
184
|
+
spl = await spliceIntoSource({ deckDir: d, source: seed.file, page: 1, sourcePage: 1, out: join(d, 'spliced.pptx') })
|
|
185
|
+
const z = zipRead(await readFile(spl.out))
|
|
186
|
+
if (!z.has('ppt/media/dot.png')) e1 = new Error('media not merged')
|
|
187
|
+
if (![...z.keys()].some((k) => k.includes('_rels/slide1.xml.rels'))) e1 = e1 ?? new Error('rels missing')
|
|
188
|
+
const rels = decodeXml(z.get('ppt/slides/_rels/slide1.xml.rels'))
|
|
189
|
+
if (!rels.includes('ppt/media/dot.png') && !rels.includes('../media/dot.png')) e1 = e1 ?? new Error('media rel not referenced')
|
|
190
|
+
} catch (e) { e1 = e }
|
|
191
|
+
ok('E1 含图片页 splice → 媒体合并 + rels 引用 + 产物可读', e1 === null && spl !== null,
|
|
192
|
+
e1 ? String(e1).slice(0, 120) : `${spl?.mediaAdded} added`)
|
|
193
|
+
await rmFixture(d)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── F. 鲁棒性统计输出 ───────────────────────────────────────────
|
|
197
|
+
console.log(`\n==== 预检结果:${pass} 通过 / ${fail} 失败 ====`)
|
|
198
|
+
process.exit(fail > 0 ? 1 : 0)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 探针:同一 release 资产 URL 在中途被 --clobber 覆盖后,**这个 URL 到底还能不能拿到新字节**?
|
|
3
|
+
* 背景:release-sync 的发布纪律是"同 tag 资产原地更新(--clobber,updated_at 变化即新包)",
|
|
4
|
+
* 而 `dsh plugin add <URL>` 装到的却是旧字节(挂载副本缺最新代码)。这里绕开 pnpm 直接 GET,
|
|
5
|
+
* 判断是 **CDN 缓存** 还是 **pnpm 按 URL 复用**。
|
|
6
|
+
*
|
|
7
|
+
* 判定口径(2026-09-15 修):**sha256 说了算**。原来只有"marker 在不在包里"一条,而 marker 是按
|
|
8
|
+
* `scripts/release-sync.mjs` 这一个文件查的——传一个不在该文件里的 marker 会打印"URL 提供旧字节"这种
|
|
9
|
+
* **假阴性判决**(本机实测踩到:sha 明明等于新资产)。所以:
|
|
10
|
+
* - 传 `<url> <marker> <期望 sha256>` → 判决完全按 sha 比对(权威);
|
|
11
|
+
* - 只传 marker 时,未命中会显式提示"marker 可能选错了文件",不再直接断言"旧字节"。
|
|
12
|
+
* 用法:
|
|
13
|
+
* node scripts/probe-asset-freshness.mjs <asset-url> # 默认 marker '⑤b'
|
|
14
|
+
* node scripts/probe-asset-freshness.mjs <asset-url> 构建戳 # 指定 marker(须在 release-sync.mjs 里)
|
|
15
|
+
* node scripts/probe-asset-freshness.mjs <asset-url> 构建戳 <期望sha256> # **权威判决**:sha 比对
|
|
16
|
+
*/
|
|
17
|
+
import { writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { tmpdir } from 'node:os'
|
|
20
|
+
import { createHash } from 'node:crypto'
|
|
21
|
+
import { execSync } from 'node:child_process'
|
|
22
|
+
|
|
23
|
+
const url = process.argv[2]
|
|
24
|
+
if (!url) { console.error('用法: node probe-asset-freshness.mjs <asset-url> [marker] [期望sha256]'); process.exit(2) }
|
|
25
|
+
const marker = process.argv[3] ?? '⑤b'
|
|
26
|
+
const expectSha = (process.argv[4] ?? '').trim().toLowerCase().replace(/^sha256:/, '')
|
|
27
|
+
const shaDecided = /^[0-9a-f]{64}$/.test(expectSha)
|
|
28
|
+
const MARKER_FILE = 'scripts/release-sync.mjs'
|
|
29
|
+
|
|
30
|
+
const out = join(tmpdir(), `asset-probe-${Date.now()}.tgz`)
|
|
31
|
+
console.log('URL:', url)
|
|
32
|
+
const t0 = Date.now()
|
|
33
|
+
const res = await fetch(url, { redirect: 'follow', headers: { 'cache-control': 'no-cache' } })
|
|
34
|
+
console.log('HTTP', res.status, '→', res.url, `(${Date.now() - t0}ms)`)
|
|
35
|
+
const buf = Buffer.from(await res.arrayBuffer())
|
|
36
|
+
writeFileSync(out, buf)
|
|
37
|
+
const gotSha = createHash('sha256').update(buf).digest('hex')
|
|
38
|
+
console.log('下载字节数 =', buf.length, '| sha256 =', gotSha)
|
|
39
|
+
|
|
40
|
+
// 解包找 marker(tar -xzf 到一个临时目录)
|
|
41
|
+
const dir = out + '-x'
|
|
42
|
+
execSync(`mkdir "${dir}" 2>nul & tar -xzf "${out}" -C "${dir}"`, { stdio: 'ignore', shell: 'cmd.exe' })
|
|
43
|
+
const target = join(dir, 'package', ...MARKER_FILE.split('/'))
|
|
44
|
+
const hit = existsSync(target) ? readFileSync(target, 'utf8').includes(marker) : null
|
|
45
|
+
console.log(`包内 ${MARKER_FILE} 含 "${marker}":${hit === null ? '(文件缺失)' : hit}`)
|
|
46
|
+
rmSync(dir, { recursive: true, force: true })
|
|
47
|
+
rmSync(out, { force: true })
|
|
48
|
+
|
|
49
|
+
if (shaDecided) {
|
|
50
|
+
const same = gotSha === expectSha
|
|
51
|
+
console.log(`sha256 比对:期望 ${expectSha}\n 实际 ${gotSha} → ${same ? '一致' : '不一致'}`)
|
|
52
|
+
console.log(`\n==== 判定(按 sha256,权威):URL 现在提供的是${same ? '**新字节**' : '**旧字节**'}${same ? '(CDN 未缓存旧版 → 若包管理器仍装到旧版,问题在 pnpm 按 URL 复用)' : '(URL 层面就是旧的 → GitHub 资产 CDN 缓存)'} ====`)
|
|
53
|
+
} else {
|
|
54
|
+
console.log(hit
|
|
55
|
+
? `\n==== 判定(按 marker,仅供线索):URL 现在提供的是**新字节**(marker 命中 ${MARKER_FILE})====`
|
|
56
|
+
: `\n==== 判定:**未按 sha 判定**——marker 未命中 ${MARKER_FILE},这**不等于**"URL 是旧字节"(marker 可能选错文件)。\n 要权威判定请传第 3 个参数:<期望 sha256>(可从 .sync-state.json 的 remoteSha 取)====`)
|
|
57
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cordis `ctx.effect` 语义探针(docs/01 决策 12 / docs/02 §2.12 的证据)
|
|
3
|
+
*
|
|
4
|
+
* 结论(本脚本可复跑自证):`ctx.effect(cb)` **立即执行 cb**,并把 **cb 的返回值**当作 disposer;
|
|
5
|
+
* 它**不是**"把 cb 当清理函数"。
|
|
6
|
+
*
|
|
7
|
+
* 为什么值得留一个脚本:本仓库曾有两处按相反理解写代码——
|
|
8
|
+
* ① `preview-server` 的路由 refcount 把清理体写在 cb 体内 ⇒ 注册当刻就 `count--` 并置 `cancelled=true`,
|
|
9
|
+
* 文档承诺的"首个注册者真注册、最后卸载者真卸载"从未成立(路由注册后立即被取消);
|
|
10
|
+
* ② 新增的装配防重第一版同样写法 ⇒ 防重完全失效(smoke §40 行为断言当场抓到)。
|
|
11
|
+
* 而 smoke 里的**假 ctx** 当时也写反了契约,于是真 bug 在测试里看起来是对的——**测试替身错了,测试就在替 bug 背书**。
|
|
12
|
+
* 所以:契约可疑时跑这个探针,别靠读注释。
|
|
13
|
+
*
|
|
14
|
+
* 用法:`node scripts/probe-effect-semantics.mjs`(找不到 cordis 时跳过,退出码 0)。
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import { pathToFileURL } from 'node:url'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import { execSync } from 'node:child_process'
|
|
21
|
+
|
|
22
|
+
const candidates = []
|
|
23
|
+
// 候选路径(不依赖能 spawn npm:Windows 上 npm 是 npm.cmd,execFileSync 不带 shell 会 ENOENT)
|
|
24
|
+
const relCordis = join('@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai', 'cordis', 'lib', 'index.js')
|
|
25
|
+
if (process.env.APPDATA) candidates.push(join(process.env.APPDATA, 'npm', 'node_modules', relCordis))
|
|
26
|
+
for (const cmd of ['npm', 'npm.cmd']) {
|
|
27
|
+
try {
|
|
28
|
+
// 用字符串命令(shell 执行)——避免 execFileSync(args)+shell 的 DEP0190 警告
|
|
29
|
+
const globalRoot = execSync(`${cmd} root -g`, { encoding: 'utf8' }).trim()
|
|
30
|
+
if (globalRoot) candidates.push(join(globalRoot, relCordis))
|
|
31
|
+
break
|
|
32
|
+
} catch { /* 该写法不可用,试下一个 */ }
|
|
33
|
+
}
|
|
34
|
+
const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
35
|
+
candidates.push(join(dshHome, 'profiles', 'web', 'node_modules', '@deepseek-ai', 'cordis', 'lib', 'index.js'))
|
|
36
|
+
|
|
37
|
+
const found = candidates.find((p) => existsSync(p))
|
|
38
|
+
if (!found) {
|
|
39
|
+
console.log('⚠ 跳过:找不到 @deepseek-ai/cordis(本探针需要一个可 import 的真库)')
|
|
40
|
+
console.log(' 候选路径:\n - ' + candidates.join('\n - '))
|
|
41
|
+
process.exit(0)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { Context } = await import(pathToFileURL(found).href)
|
|
45
|
+
const ctx = new Context()
|
|
46
|
+
const log = []
|
|
47
|
+
|
|
48
|
+
const disposer = ctx.effect(() => {
|
|
49
|
+
log.push('cb-body')
|
|
50
|
+
return () => log.push('returned-cleanup')
|
|
51
|
+
}, 'probe')
|
|
52
|
+
|
|
53
|
+
const immediate = log.includes('cb-body')
|
|
54
|
+
console.log(`cordis = ${found}`)
|
|
55
|
+
console.log(`调用 effect 后立即观察到的回调执行:${immediate ? '已执行(真语义:cb 立即跑)' : '未执行'}`)
|
|
56
|
+
console.log(`effect 返回值类型:${typeof disposer}`)
|
|
57
|
+
|
|
58
|
+
let cleanupRan = false
|
|
59
|
+
if (typeof disposer === 'function') {
|
|
60
|
+
const r = disposer()
|
|
61
|
+
if (r && typeof r.then === 'function') await r
|
|
62
|
+
await new Promise((res) => setTimeout(res, 10))
|
|
63
|
+
cleanupRan = log.includes('returned-cleanup')
|
|
64
|
+
}
|
|
65
|
+
console.log(`dispose 后"cb 返回的函数"被执行:${cleanupRan ? '是(返回值 = disposer)' : '否'}`)
|
|
66
|
+
|
|
67
|
+
const okAll = immediate && typeof disposer === 'function' && cleanupRan
|
|
68
|
+
console.log(`\n==== 语义探针:${okAll ? '符合预期(cb 立即执行 + 返回值为 disposer)' : '与预期不符——若 Cordis 升级改了语义,须复核 index.js 防重与 preview-server refcount'} ====`)
|
|
69
|
+
process.exit(okAll ? 0 : 1)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 重解析点安全探针:Node 的 rmSync(recursive) 会不会**跟随 junction**把目标目录也删掉?
|
|
3
|
+
* 背景:release-sync 会 `rmSync(D:\plugins\package, {recursive:true})`,而该目录里 install.mjs 建过
|
|
4
|
+
* `node_modules/yaml` → profile 的 yaml 目录(junction)。若 Node 跟随重解析点,就会**清空 profile 的 yaml**
|
|
5
|
+
*(2026-09-14 的真实事故形态——那次是 PowerShell 的 Remove-Item -Recurse 干的)。
|
|
6
|
+
* 结论直接决定 release-sync 的删除写法是否安全。
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync, symlinkSync, lstatSync } from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { tmpdir } from 'node:os'
|
|
11
|
+
|
|
12
|
+
const base = join(tmpdir(), `reparse-probe-${Date.now()}`)
|
|
13
|
+
const canary = join(base, 'canary') // 被指向的"重要目录"
|
|
14
|
+
const victim = join(base, 'victim') // 将被递归删除的目录
|
|
15
|
+
mkdirSync(canary, { recursive: true })
|
|
16
|
+
mkdirSync(join(victim, 'node_modules'), { recursive: true })
|
|
17
|
+
writeFileSync(join(canary, 'IMPORTANT.txt'), 'must survive', 'utf8')
|
|
18
|
+
writeFileSync(join(victim, 'node_modules', 'filler.txt'), 'x', 'utf8')
|
|
19
|
+
symlinkSync(canary, join(victim, 'node_modules', 'linked'), 'junction')
|
|
20
|
+
|
|
21
|
+
const st = lstatSync(join(victim, 'node_modules', 'linked'))
|
|
22
|
+
console.log(`junction 建立:isSymbolicLink=${st.isSymbolicLink()} isDirectory=${st.isDirectory()}`)
|
|
23
|
+
|
|
24
|
+
rmSync(victim, { recursive: true, force: true })
|
|
25
|
+
|
|
26
|
+
const canaryAlive = existsSync(join(canary, 'IMPORTANT.txt'))
|
|
27
|
+
const victimGone = !existsSync(victim)
|
|
28
|
+
console.log(`victim 已删除:${victimGone}`)
|
|
29
|
+
console.log(`canary 内容仍在:${canaryAlive}${canaryAlive ? `(内容="${readFileSync(join(canary, 'IMPORTANT.txt'), 'utf8')}")` : ' ← **被删空了!**'}`)
|
|
30
|
+
console.log(`\n==== 结论:Node rmSync(recursive) ${canaryAlive ? '**不跟随** junction(release-sync 的删法安全)' : '**会跟随** junction(必须改成先 rmdir 链接)'} ====`)
|
|
31
|
+
rmSync(base, { recursive: true, force: true })
|
|
32
|
+
process.exit(canaryAlive ? 0 : 1)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.11.0 模板骨架层重建:4 套外部模板重导(custGeom/alpha/形状文本保真升级),
|
|
3
|
+
* 保留 styleAudit + 双轨字段(template.pptx/previews/),幂等。
|
|
4
|
+
* 流程:备份 template.yaml(styleAudit)→ rm 模板目录 → importPptx(桌面源) → registerTemplate →
|
|
5
|
+
* 迁移脚本补双轨资产 → 合并 styleAudit。
|
|
6
|
+
*
|
|
7
|
+
* 【2026-09-18 状态(1.0.4)】它重建的那 4 套模板已移出随包发行物,桌面源目录
|
|
8
|
+
* `C:/Users/11867/Desktop/ppt模板库` 也已不存在 ⇒ 默认调用必然失败。保留为**一次完整重建的参考流程**
|
|
9
|
+
* (收纳自己的模板时按同样顺序走:importPptx → ppt_template_add → 可选补双轨)。
|
|
10
|
+
*/
|
|
11
|
+
import { readFile, writeFile, mkdir, rm, readdir, copyFile } from 'node:fs/promises'
|
|
12
|
+
import { join, dirname, basename } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { homedir } from 'node:os'
|
|
15
|
+
import { existsSync } from 'node:fs'
|
|
16
|
+
import YAML from 'yaml'
|
|
17
|
+
|
|
18
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
19
|
+
const TPL_DIR = join(ROOT, 'templates')
|
|
20
|
+
const DESK = 'C:\\Users\\11867\\Desktop\\ppt模板库'
|
|
21
|
+
const ORIG = join(homedir(), '.dsh', 'ppt-studio', 'fidelity', 'orig')
|
|
22
|
+
|
|
23
|
+
const MODELS = [
|
|
24
|
+
{ file: '实用毕业设计论文答辩PPT模板.pptx', id: '实用毕业设计论文答辩ppt模板', name: '实用毕业设计论文答辩PPT模板', style: '学术答辩', scene: '毕业设计论文答辩' },
|
|
25
|
+
{ file: '极简实用部门工作总结PPT模板.pptx', id: '极简实用部门工作总结ppt模板', name: '极简实用部门工作总结PPT模板', style: '工作总结', scene: '部门工作总结' },
|
|
26
|
+
{ file: '深蓝质感论文答辩PPT模板.pptx', id: '深蓝质感论文答辩ppt模板', name: '深蓝质感论文答辩PPT模板', style: '学术答辩', scene: '论文答辩' },
|
|
27
|
+
{ file: '简约商务.pptx', id: '简约商务', name: '简约商务', style: '商务', scene: '商务汇报' },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const { importPptx } = await import('../src/pptd/import-pptx.js')
|
|
31
|
+
const { registerTemplate } = await import('../src/templates.js')
|
|
32
|
+
|
|
33
|
+
for (const m of MODELS) {
|
|
34
|
+
const tplDir = join(TPL_DIR, m.id)
|
|
35
|
+
if (!existsSync(tplDir)) { console.log(`SKIP(模板目录不存在):${m.id}`); continue }
|
|
36
|
+
// ① 备份 styleAudit
|
|
37
|
+
const metaFile = join(tplDir, 'template.yaml')
|
|
38
|
+
const oldMeta = existsSync(metaFile) ? (YAML.parse(await readFile(metaFile, 'utf8')) ?? {}) : {}
|
|
39
|
+
// ② rm 模板目录(重建)
|
|
40
|
+
await rm(tplDir, { recursive: true, force: true })
|
|
41
|
+
// ③ 重导 + 收纳
|
|
42
|
+
const tmp = join(homedir(), '.dsh', 'ppt-studio', 'rebuild-tmp', m.id)
|
|
43
|
+
await rm(tmp, { recursive: true, force: true })
|
|
44
|
+
const src = join(DESK, m.file)
|
|
45
|
+
const imp = await importPptx(src, tmp)
|
|
46
|
+
const reg = await registerTemplate(tmp, { id: m.id, name: m.name, style: m.style, scene: m.scene }, {})
|
|
47
|
+
console.log(`+ 重建 ${m.id}:${imp.pages} 页(reg id=${reg.id} 冲突=${reg.id !== m.id ? reg.id : '无'})`)
|
|
48
|
+
// ④ 双轨资产(template.pptx + previews/ 从桌面/已渲染 PNG 补)
|
|
49
|
+
const tplNew = join(TPL_DIR, reg.id)
|
|
50
|
+
if (!existsSync(join(tplNew, 'template.pptx')) && existsSync(src)) await copyFile(src, join(tplNew, 'template.pptx'))
|
|
51
|
+
if (!existsSync(join(tplNew, 'previews')) && existsSync(join(ORIG, m.file.replace(/\.pptx$/i, '')))) {
|
|
52
|
+
await mkdir(join(tplNew, 'previews'), { recursive: true })
|
|
53
|
+
for (const f of (await readdir(join(ORIG, m.file.replace(/\.pptx$/i, '')))).filter((x) => /\.png$/i.test(x))) {
|
|
54
|
+
await copyFile(join(ORIG, m.file.replace(/\.pptx$/i, ''), f), join(tplNew, 'previews', f))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// ⑤ 合并 styleAudit + 双轨字段
|
|
58
|
+
const metaNew = YAML.parse(await readFile(join(tplNew, 'template.yaml'), 'utf8')) ?? {}
|
|
59
|
+
const merged = {
|
|
60
|
+
...metaNew,
|
|
61
|
+
...(existsSync(join(tplNew, 'template.pptx')) ? { sourcePptx: 'template.pptx' } : {}),
|
|
62
|
+
...(existsSync(join(tplNew, 'previews')) ? { previews: 'previews' } : {}),
|
|
63
|
+
...(oldMeta.styleAudit ? { styleAudit: oldMeta.styleAudit } : {}),
|
|
64
|
+
}
|
|
65
|
+
await writeFile(join(tplNew, 'template.yaml'), YAML.stringify(merged))
|
|
66
|
+
console.log(` meta:双轨字段 ✓ / styleAudit ${oldMeta.styleAudit ? '保留 ✓' : '(无)'}`)
|
|
67
|
+
await rm(tmp, { recursive: true, force: true }).catch(() => {})
|
|
68
|
+
}
|
|
69
|
+
console.log('\n完成')
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 真实资产回归(L1 层,非 smoke 的合成样例):
|
|
3
|
+
* 1. 19 页产能 deck(若存在):resolveDeck → render → verify(0 错误)→ export(0 autoFit)→ zip 结构
|
|
4
|
+
* 2. WPS UTF-16 fixture:import → 页数/媒体正确
|
|
5
|
+
* 资产缺失时打印 skip(本机路径可配置)。
|
|
6
|
+
*/
|
|
7
|
+
import { join, dirname } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import { readFile } from 'node:fs/promises'
|
|
11
|
+
import { resolveDeck } from '../lib/pptd/schema.js'
|
|
12
|
+
import { renderDeck } from '../lib/pptd/render-html.js'
|
|
13
|
+
import { verifyDeck } from '../lib/verify.js'
|
|
14
|
+
import { exportPptx } from '../lib/pptd/export-pptx.js'
|
|
15
|
+
import { importPptx } from '../lib/pptd/import-pptx.js'
|
|
16
|
+
import { zipRead } from '../lib/zips.js'
|
|
17
|
+
|
|
18
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
19
|
+
const REAL_DECK = process.env.PPT_REAL_DECK ?? 'D:/SharkCode/fighter_capacity/deck'
|
|
20
|
+
const WPS_PPTX = process.env.PPT_WPS_FIXTURE ?? join(root, 'fixtures', 'Anomaly_Detection_Figures.pptx')
|
|
21
|
+
|
|
22
|
+
let pass = 0
|
|
23
|
+
let fail = 0
|
|
24
|
+
let skip = 0
|
|
25
|
+
const ok = (name, cond, extra = '') => {
|
|
26
|
+
console.log(`${cond ? '✓' : '✗'} ${name}${extra ? ' — ' + extra : ''}`)
|
|
27
|
+
cond ? pass++ : fail++
|
|
28
|
+
}
|
|
29
|
+
const skipNote = (name, path) => {
|
|
30
|
+
console.log(`⏭ ${name}(资产缺失,跳过:${path})`)
|
|
31
|
+
skip++
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 1. 真实 deck(产能项目 19 页)
|
|
35
|
+
if (existsSync(join(REAL_DECK, 'deck.yaml'))) {
|
|
36
|
+
try {
|
|
37
|
+
const ctx = await resolveDeck(REAL_DECK)
|
|
38
|
+
const r = await renderDeck(ctx, {})
|
|
39
|
+
const v = verifyDeck(r.layout)
|
|
40
|
+
const errors = v.text.split('\n').filter((l) => l.includes('[✗]')).length
|
|
41
|
+
ok('真实 deck:resolveDeck + render', ctx.pages.length >= 19, `${ctx.pages.length} pages`)
|
|
42
|
+
ok('真实 deck:verify 0 错误(门禁)', errors === 0, `errors=${errors}`)
|
|
43
|
+
const exp = await exportPptx(ctx, { out: 'out-real-regression.pptx', engine: 'pptd' })
|
|
44
|
+
ok('真实 deck:导出 0 缩字(E2 承诺)', exp.autoFit.length === 0, `autoFit=${JSON.stringify(exp.autoFit)}`)
|
|
45
|
+
const parts = zipRead(await readFile(exp.file))
|
|
46
|
+
ok('真实 deck:OOXML 结构完整', parts.has('[Content_Types].xml') && parts.has('ppt/presentation.xml'))
|
|
47
|
+
// 表格导出回读断言(1.0.1:表格空白事故 → graphicFrame 嵌套 a:xfrm 非法结构 / 缺 tableStyleId)
|
|
48
|
+
const tables = ctx.pages.flatMap((p) => (p.page.elements ?? []).filter((e) => e.elementType === 'table').map((e) => e.elementId))
|
|
49
|
+
if (tables.length) {
|
|
50
|
+
const slidesXml = []
|
|
51
|
+
for (let i = 1; i <= ctx.pages.length; i++) slidesXml.push(parts.get(`ppt/slides/slide${i}.xml`)?.toString('utf8') ?? '')
|
|
52
|
+
const badFrame = slidesXml.filter((x) => /<p:xfrm><a:xfrm>/.test(x))
|
|
53
|
+
ok('真实 deck:表格 graphicFrame 无嵌套 a:xfrm', badFrame.length === 0, `违规帧=${badFrame.length}`)
|
|
54
|
+
const styled = slidesXml.filter((x) => x.includes('<a:tableStyleId>'))
|
|
55
|
+
ok('真实 deck:表槽含 tableStyleId', styled.length >= tables.length, `tables=${tables.length} styled=${styled.length}`)
|
|
56
|
+
}
|
|
57
|
+
} catch (e) {
|
|
58
|
+
ok('真实 deck:全链回归', false, e?.message)
|
|
59
|
+
} finally {
|
|
60
|
+
const { rm } = await import('node:fs/promises')
|
|
61
|
+
await rm(join(REAL_DECK, 'out-real-regression.pptx'), { force: true })
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
skipNote('真实 deck 回归', REAL_DECK)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. WPS UTF-16 fixture
|
|
68
|
+
if (existsSync(WPS_PPTX)) {
|
|
69
|
+
try {
|
|
70
|
+
const outDir = join(root, 'examples', 'import-realtest-regression')
|
|
71
|
+
const imp = await importPptx(WPS_PPTX, outDir)
|
|
72
|
+
ok('WPS fixture:导入页数/媒体', imp.pages === 2 && imp.media.length === 2, `${imp.pages} pages, ${imp.media.length} media`)
|
|
73
|
+
const ctxW = await resolveDeck(outDir)
|
|
74
|
+
const rW = await renderDeck(ctxW, { out: 'preview-regression' })
|
|
75
|
+
ok('WPS fixture:导入项目可渲染', rW.htmlFiles.length === 2)
|
|
76
|
+
} catch (e) {
|
|
77
|
+
ok('WPS fixture:导入回归', false, e?.message)
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
skipNote('WPS fixture 回归', WPS_PPTX)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(`\n==== 真实回归:${pass} 通过 / ${fail} 失败 / ${skip} 跳过 ====`)
|
|
84
|
+
process.exit(fail > 0 ? 1 : 0)
|