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,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模板导入保真审计:源 pptx(XML 逐页)vs 导入模板工程(yaml)同口径对比。
|
|
3
|
+
* 输出每套的逐页差异表 + 汇总保真率与主要失真类别,落盘 docs/fidelity-report.md。
|
|
4
|
+
*
|
|
5
|
+
* 【2026-09-18 状态(1.0.4)】本脚本默认源目录 `桌面/ppt模板库` **已不存在**,它审计的那 4 套模板
|
|
6
|
+
* 也已从随包发行物移除。保留本脚本是因为它是 docs/fidelity-report.md 里那些结论的**复现工具**——
|
|
7
|
+
* 要再跑,需自备原始 pptx 并把那 4 套收纳回 `templates/<id>/`(来源见 docs/03 的 1.0.4 记录)。
|
|
8
|
+
*/
|
|
9
|
+
import { join, dirname, basename } from 'node:path'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
12
|
+
import { zipRead, decodeXml } from '../lib/zips.js'
|
|
13
|
+
import { parseXml, children, first, allText } from '../lib/xmljs.js'
|
|
14
|
+
import { resolveDeck } from '../lib/pptd/schema.js'
|
|
15
|
+
import { templateWorkspace } from '../lib/templates.js'
|
|
16
|
+
|
|
17
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
18
|
+
const SRC = process.argv[2] ?? 'C:/Users/11867/Desktop/ppt模板库'
|
|
19
|
+
const SRC_TO_TPL = {
|
|
20
|
+
'实用毕业设计论文答辩PPT模板.pptx': '实用毕业设计论文答辩ppt模板',
|
|
21
|
+
'极简实用部门工作总结PPT模板.pptx': '极简实用部门工作总结ppt模板',
|
|
22
|
+
'深蓝质感论文答辩PPT模板.pptx': '深蓝质感论文答辩ppt模板',
|
|
23
|
+
'简约商务.pptx': '简约商务',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const report = []
|
|
27
|
+
report.push('# 模板导入保真审计报告(v0.7.1 对照)\n')
|
|
28
|
+
report.push(`> 生成时间:${new Date().toISOString()}|源:${SRC}|导入版:templates/<id>/\n`)
|
|
29
|
+
report.push('## 说明\n- **源计数**:原始 pptx XML(pptx 内部结构,权威)逐页统计;**导入计数**:模板工程 yaml。\n- **保真率** = 导入元素数 / 源形状级元素数(sp/pic/cxnSp/grpSp 内子元素合并为"形状级元素";grpSp 嵌套子元素计入源侧计数)。\n- 文本保真 = 导入文本字符数 / 源文本字符数(tl 文本 run 汇总;不含表格/图表内文本)。\n')
|
|
30
|
+
|
|
31
|
+
function slideStats(xmlText) {
|
|
32
|
+
const root0 = parseXml(xmlText)
|
|
33
|
+
const sld = first(root0, 'sld') ?? root0
|
|
34
|
+
const F = (n, t) => (n ? first(n, t) : null)
|
|
35
|
+
const cSld = F(sld, 'cSld')
|
|
36
|
+
const spTree = cSld ? F(cSld, 'spTree') : null
|
|
37
|
+
const st = { sp: 0, pic: 0, cxn: 0, grpChildren: 0, custGeom: 0, grad: 0, ph: 0, cropPic: 0, smartArt: 0, table: 0, chart: 0, textChars: 0, bgImage: false, bgSolid: false }
|
|
38
|
+
const walk = (node, inGroup) => {
|
|
39
|
+
for (const c of node?.children ?? []) {
|
|
40
|
+
if (c.tag === 'sp') {
|
|
41
|
+
st.sp++
|
|
42
|
+
const spPr = F(c, 'spPr')
|
|
43
|
+
if (F(spPr, 'custGeom')) st.custGeom++
|
|
44
|
+
if (F(spPr, 'gradFill')) st.grad++
|
|
45
|
+
const nv = F(c, 'nvSpPr') ?? c
|
|
46
|
+
if (F(nv, 'ph')) st.ph++
|
|
47
|
+
const tx = F(c, 'txBody')
|
|
48
|
+
if (tx) st.textChars += allText(tx).replace(/\s/g, '').length
|
|
49
|
+
if (inGroup) st.grpChildren++
|
|
50
|
+
} else if (c.tag === 'pic') {
|
|
51
|
+
st.pic++
|
|
52
|
+
const blipFill = F(c, 'blipFill')
|
|
53
|
+
if (F(blipFill, 'srcRect')) st.cropPic++
|
|
54
|
+
if (inGroup) st.grpChildren++
|
|
55
|
+
} else if (c.tag === 'cxnSp') {
|
|
56
|
+
st.cxn++
|
|
57
|
+
st.grpChildren += inGroup ? 1 : 0
|
|
58
|
+
} else if (c.tag === 'grpSp') {
|
|
59
|
+
walk(c, true)
|
|
60
|
+
} else if (c.tag === 'graphicFrame') {
|
|
61
|
+
const graphic = F(c, 'graphic')
|
|
62
|
+
const gd = graphic ? F(graphic, 'graphicData') : null
|
|
63
|
+
const uri = gd?.attrs?.uri ?? ''
|
|
64
|
+
if (uri.includes('/table')) st.table++
|
|
65
|
+
else if (uri.includes('diagram')) st.smartArt++
|
|
66
|
+
else st.chart++
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
walk(spTree, false)
|
|
71
|
+
const bgPr = F(F(cSld, 'bg'), 'bgPr')
|
|
72
|
+
if (bgPr) {
|
|
73
|
+
if (F(bgPr, 'blipFill')) st.bgImage = true
|
|
74
|
+
if (F(bgPr, 'solidFill')) st.bgSolid = true
|
|
75
|
+
}
|
|
76
|
+
return st
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function importedStats(tplId) {
|
|
80
|
+
const t = await templateWorkspace(tplId)
|
|
81
|
+
const ctx = await resolveDeck(t.dir)
|
|
82
|
+
const st = { sp: 0, pic: 0, cxn: 0, grad: 0, textChars: 0 }
|
|
83
|
+
let pages = 0
|
|
84
|
+
let bgCount = 0
|
|
85
|
+
for (const p of ctx.pages) {
|
|
86
|
+
pages++
|
|
87
|
+
if (p.page.background) bgCount++
|
|
88
|
+
for (const e of p.page.elements ?? []) {
|
|
89
|
+
if (e.elementType === 'shape') st.sp++
|
|
90
|
+
else if (e.elementType === 'image') st.pic++
|
|
91
|
+
else if (e.elementType === 'line') st.cxn++
|
|
92
|
+
else if (e.elementType === 'text') st.textChars += String(e.content?.text ?? '').replace(/\s/g, '').length
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { pages, bgCount, ...st, media: t.media.length }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let overallRows = []
|
|
99
|
+
for (const [file, tplId] of Object.entries(SRC_TO_TPL)) {
|
|
100
|
+
const pptx = join(SRC, file)
|
|
101
|
+
let buf
|
|
102
|
+
try { buf = await readFile(pptx) } catch { report.push(`\n## ${file}\n(源文件缺失,跳过)\n`); continue }
|
|
103
|
+
const zip = zipRead(buf)
|
|
104
|
+
const entries = [...zip.keys()].filter((k) => /^ppt\/slides\/slide\d+\.xml$/.test(k)).sort((a, b) => Number(a.match(/\d+/)[0]) - Number(b.match(/\d+/)[0]))
|
|
105
|
+
const per = []
|
|
106
|
+
const sum = { sp: 0, pic: 0, cxn: 0, grpChildren: 0, custGeom: 0, grad: 0, ph: 0, cropPic: 0, smartArt: 0, table: 0, chart: 0, textChars: 0, bgImage: 0, bgSolid: 0 }
|
|
107
|
+
for (const e of entries) {
|
|
108
|
+
const st = slideStats(decodeXml(zip.get(e)))
|
|
109
|
+
for (const k of Object.keys(sum)) sum[k] += st[k]
|
|
110
|
+
per.push(st)
|
|
111
|
+
}
|
|
112
|
+
const imp = await importedStats(tplId)
|
|
113
|
+
report.push(`\n## ${file} → 模板「${tplId}」(${per.length} 页源 vs ${imp.pages} 页导入)\n`)
|
|
114
|
+
report.push('| 类别 | 源(XML) | 导入(yaml) | 备注 |')
|
|
115
|
+
report.push('|---|---|---|---|')
|
|
116
|
+
const elSrc = sum.sp + sum.pic + sum.cxn + sum.grpChildren
|
|
117
|
+
const elImp = imp.sp + imp.pic + imp.cxn
|
|
118
|
+
const rate = elSrc > 0 ? Math.round((elImp / elSrc) * 100) : 0
|
|
119
|
+
report.push(`| 形状级元素 | ${elSrc} | ${elImp} | **保真率约 ${rate}%**(组合子元素并入源计数) |`)
|
|
120
|
+
report.push(`| sp(含矩形/自选) | ${sum.sp} | ${imp.sp} | 自选形状(箭头/圆角等 prst)按 rect 近似 |`)
|
|
121
|
+
report.push(`| pic(图片) | ${sum.pic} | ${imp.pic} | 裁剪图(crop ×${sum.cropPic})拉伸处理 |`)
|
|
122
|
+
report.push(`| cxnSp(连接线) | ${sum.cxn} | ${imp.cxn} | v0.7.1 起解析 |`)
|
|
123
|
+
report.push(`| 组合 grpSp 子元素 | ${sum.grpChildren} | (并入上) | v0.7.1 起展开 |`)
|
|
124
|
+
report.push(`| 自定义几何(custGeom) | ${sum.custGeom} | 0 | 自选图形按矩形近似 |`)
|
|
125
|
+
report.push(`| 渐变填充(gradFill) | ${sum.grad} | 0 | 归一为主色(stops 在 import-styles.json) |`)
|
|
126
|
+
report.push(`| 占位符(placeholder) | ${sum.ph} | 0 | 占位符元素并入形状近似 |`)
|
|
127
|
+
report.push(`| SmartArt(diagram) | ${sum.smartArt} | 0 | 降级为文本占位 |`)
|
|
128
|
+
report.push(`| 表格 / 图表 | ${sum.table} / ${sum.chart} | 保留近似 | chart→占位 |`)
|
|
129
|
+
report.push(`| 页面背景(图片/纯色) | ${sum.bgImage} 图 / ${sum.bgSolid} 色 | ${imp.bgCount} | 背景提取(满页图≥95% 提升) |`)
|
|
130
|
+
report.push(`| 文本字符数 | ${sum.textChars} | ${imp.textChars} | **文本保真率约 ${sum.textChars ? Math.round((imp.textChars / sum.textChars) * 100) : 0}%** |`)
|
|
131
|
+
report.push(`| 媒体文件 | — | ${imp.media} | 组合内图片 v0.7.1 起提取 |`)
|
|
132
|
+
overallRows.push({ file, rate, textRate: sum.textChars ? Math.round((imp.textChars / sum.textChars) * 100) : 0, elSrc, elImp })
|
|
133
|
+
// 页级:每页源元素 vs 导入元素(只打印头尾 8 行避免报告过长 → 全量写)
|
|
134
|
+
const implines = []
|
|
135
|
+
for (let i = 0; i < per.length; i++) {
|
|
136
|
+
const s = per[i]
|
|
137
|
+
const srcN = s.sp + s.pic + s.cxn + s.grpChildren
|
|
138
|
+
implines.push(`| 第${i + 1}页 | ${srcN} | ${s.pic}图/${s.custGeom}自/${s.grad}渐/${s.smartArt}智/${s.ph}占 | ${s.bgImage ? '背景图' : ''}${s.bgSolid ? '背景色' : ''} |`)
|
|
139
|
+
}
|
|
140
|
+
report.push('\n### 页级明细(源侧特征)\n')
|
|
141
|
+
report.push('| 页 | 元素 | 特征 | 背景 |')
|
|
142
|
+
report.push('|---|---|---|---|')
|
|
143
|
+
report.push(...implines)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
report.push('\n\n## 汇总\n')
|
|
147
|
+
for (const r of overallRows) {
|
|
148
|
+
report.push(`- **${r.file}**:元素保真率 ${r.rate}%(${r.elImp}/${r.elSrc})|文本保真率 ${r.textRate}%`)
|
|
149
|
+
}
|
|
150
|
+
report.push('\n## 已知导致视觉失真的因素(按影响排序)\n')
|
|
151
|
+
report.push('1. **自选图形 prst → 矩形**(箭头/圆角/图标等 100+ 形状按 rect;custGeom 同理)——影响图标/箭头/装饰形状。')
|
|
152
|
+
report.push('2. **渐变填充归一为末 stop 主色**(gradFill → 单色;stops 记录在 import-styles.json)——影响按钮/胶囊/背景弧带。')
|
|
153
|
+
report.push('3. **特殊字体回退**(PPT 内置字体/手写体缺失 → 浏览器渲染 tofu □□□)——文本内容保留,视觉字形不同。')
|
|
154
|
+
report.push('4. **背景层不完整**:仅识别 slide 级 p:bg 与 ≥95% 满页图;蒙层/前景盖图/背景组图按元素近似。')
|
|
155
|
+
report.push('5. **SmartArt → 文本占位**;图表 → 文本占位(数据在 xml 中,可重建)。')
|
|
156
|
+
report.push('6. **文本块宽度 vs 字体度量差** → 断行位置与源不同(制作时可调框宽)。')
|
|
157
|
+
report.push('7. **裁剪图 srcRect 忽略**(按 fit 拉伸)。')
|
|
158
|
+
|
|
159
|
+
const outPath = join(root, 'docs', 'fidelity-report.md')
|
|
160
|
+
await writeFile(outPath, report.join('\n'))
|
|
161
|
+
console.log(`审计完成 → ${outPath}`)
|
|
162
|
+
for (const r of overallRows) console.log(`${r.file}: 元素 ${r.rate}% / 文本 ${r.textRate}%`)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** M2 性能基准:100 页 deck 渲染+实测耗时(D5:先量化再决策)→ docs/02 §6 记录 */
|
|
2
|
+
import { writeFile, mkdir, rm } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
const { resolveDeck } = await import('../src/pptd/schema.js')
|
|
6
|
+
const { renderDeck } = await import('../src/pptd/render-html.js')
|
|
7
|
+
const { measureLayout } = await import('../src/measurement.js')
|
|
8
|
+
|
|
9
|
+
const d = join(homedir(), '.dsh', 'ppt-studio', 'bench-100')
|
|
10
|
+
await rm(d, { recursive: true, force: true })
|
|
11
|
+
await mkdir(join(d, 'pages'), { recursive: true })
|
|
12
|
+
const N = 100
|
|
13
|
+
const refs = []
|
|
14
|
+
for (let i = 1; i <= N; i++) {
|
|
15
|
+
const name = `p${String(i).padStart(3, '0')}.yaml`
|
|
16
|
+
refs.push(` - pages/${name}`)
|
|
17
|
+
await writeFile(join(d, 'pages', name), [
|
|
18
|
+
'pageType: content',
|
|
19
|
+
'elements:',
|
|
20
|
+
` - elementId: t${i}`, ' elementType: text', ' bounds: [60, 60, 700, 60]',
|
|
21
|
+
` content: {text: "第 ${i} 页:用于性能基准的长文本段落,包含多个中文句子与数字 12345、百分比 45.5%、年份 2026 等常见内容形态,验证真实任务的渲染与测量开销。", fontSize: 22, color: "#1E4E8C"}`,
|
|
22
|
+
` - elementId: s${i}`, ' elementType: shape', ' kind: roundRect', ' bounds: [60, 160, 300, 120]', ' fill: "#EAF1F8"',
|
|
23
|
+
'', ''].join('\n'))
|
|
24
|
+
}
|
|
25
|
+
await writeFile(join(d, 'deck.yaml'), `version: 1
|
|
26
|
+
title: bench-100
|
|
27
|
+
size: [960, 540]
|
|
28
|
+
theme: {colors: {primary: "#1E4E8C", accent: "#EAF1F8"}}
|
|
29
|
+
pages:
|
|
30
|
+
${refs.join('\n')}
|
|
31
|
+
`)
|
|
32
|
+
const t0 = Date.now()
|
|
33
|
+
const ctx = await resolveDeck(d)
|
|
34
|
+
const tRender = Date.now()
|
|
35
|
+
const r = await renderDeck(ctx, {})
|
|
36
|
+
const tMeasure0 = Date.now()
|
|
37
|
+
const m = await measureLayout(d)
|
|
38
|
+
const tEnd = Date.now()
|
|
39
|
+
console.log(JSON.stringify({
|
|
40
|
+
pages: N,
|
|
41
|
+
renderMs: tMeasure0 - tRender,
|
|
42
|
+
measureMs: tEnd - tMeasure0,
|
|
43
|
+
totalMs: tEnd - t0,
|
|
44
|
+
perPageMs: Math.round((tEnd - t0) / N * 10) / 10,
|
|
45
|
+
measuredPages: m.pages,
|
|
46
|
+
notes: m.notes,
|
|
47
|
+
}, null, 2))
|
|
48
|
+
await rm(d, { recursive: true, force: true })
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模板库维护脚本:对每套模板的封面页渲染 + 截图 → templates/<id>/preview.png。
|
|
3
|
+
* 规则:模板封面 = deck.yaml 的第一页引用(templates 的模板 deck 引 _cover 为主页)。
|
|
4
|
+
*/
|
|
5
|
+
import { join, dirname } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { readdir, rm, mkdir, access } from 'node:fs/promises'
|
|
8
|
+
import { existsSync } from 'node:fs'
|
|
9
|
+
import { spawn } from 'node:child_process'
|
|
10
|
+
import { resolveDeck } from '../lib/pptd/schema.js'
|
|
11
|
+
import { renderDeck } from '../lib/pptd/render-html.js'
|
|
12
|
+
|
|
13
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
14
|
+
const TPL_DIR = join(root, 'templates')
|
|
15
|
+
const tmp = join(root, 'examples', 'tpl-preview-work')
|
|
16
|
+
|
|
17
|
+
let pass = 0
|
|
18
|
+
let fail = 0
|
|
19
|
+
const ok = (name, cond, extra = '') => {
|
|
20
|
+
console.log(`${cond ? '✓' : '✗'} ${name}${extra ? ' — ' + extra : ''}`)
|
|
21
|
+
cond ? pass++ : fail++
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const edge = ['C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
25
|
+
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
26
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
27
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'].find((p) => existsSync(p))
|
|
28
|
+
|
|
29
|
+
for (const tpl of (await readdir(TPL_DIR, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)) {
|
|
30
|
+
const ws = join(tmp, tpl)
|
|
31
|
+
await rm(ws, { recursive: true, force: true })
|
|
32
|
+
await mkdir(join(ws, 'pages'), { recursive: true })
|
|
33
|
+
const { copyFile } = await import('node:fs/promises')
|
|
34
|
+
await copyFile(join(TPL_DIR, tpl, 'deck.yaml'), join(ws, 'deck.yaml'))
|
|
35
|
+
try {
|
|
36
|
+
// 临时引用全部母版页(模板 deck.yaml 只引用 1-2 页)
|
|
37
|
+
const { templateWorkspace } = await import('../lib/templates.js')
|
|
38
|
+
const tT = await templateWorkspace(tpl)
|
|
39
|
+
const refs = tT.meta.pages ?? []
|
|
40
|
+
const deckTxt = tT.deck.replace(/ - pages\/[^\n]+(?:\n - pages\/[^\n]+)*/, refs.map((r) => ` - ${r}`).join('\n'))
|
|
41
|
+
await (await import('node:fs/promises')).writeFile(join(ws, 'deck.yaml'), deckTxt)
|
|
42
|
+
for (const p of tT.pages) await copyFile(join(TPL_DIR, tpl, p.ref), join(ws, p.ref))
|
|
43
|
+
const ctx = await resolveDeck(ws)
|
|
44
|
+
const r = await renderDeck(ctx, {})
|
|
45
|
+
// 缩略图 = 第一页(封面)
|
|
46
|
+
const firstHtml = r.htmlFiles[0]
|
|
47
|
+
const outPng = join(TPL_DIR, tpl, 'preview.png')
|
|
48
|
+
if (!edge) { ok(`模板 ${tpl}:无浏览器,跳过截图`, true, '仅渲染检查'); continue }
|
|
49
|
+
const url = 'file:///' + join(ws, 'preview', firstHtml).replace(/\\/g, '/')
|
|
50
|
+
await new Promise((resolve, reject) => {
|
|
51
|
+
const child = spawn(edge, ['--headless=new', '--disable-gpu', '--no-first-run', '--hide-scrollbars', `--window-size=${ctx.size.width},${ctx.size.height}`, `--screenshot=${outPng}`, url], { stdio: 'ignore', windowsHide: true })
|
|
52
|
+
child.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`Edge 退出码 ${code}`)))
|
|
53
|
+
child.on('error', reject)
|
|
54
|
+
})
|
|
55
|
+
ok(`模板 ${tpl}:渲染 + 预览图生成`, existsSync(outPng))
|
|
56
|
+
} catch (e) {
|
|
57
|
+
ok(`模板 ${tpl}:渲染/截图`, false, e?.message)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await rm(tmp, { recursive: true, force: true })
|
|
62
|
+
console.log(`\n==== 模板构建:${pass} 通过 / ${fail} 失败 ====`)
|
|
63
|
+
process.exit(fail > 0 ? 1 : 0)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build: copy src/ → lib/(免编译形态:源码即产物,纯 ESM JS)。
|
|
4
|
+
* 依赖(yaml)从 node_modules 解析,构建时无需 tsc / DSH_CHECKOUT。
|
|
5
|
+
*/
|
|
6
|
+
import { cpSync, mkdirSync, rmSync } from 'node:fs'
|
|
7
|
+
import { dirname, join } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
11
|
+
rmSync(join(root, 'lib'), { recursive: true, force: true })
|
|
12
|
+
mkdirSync(join(root, 'lib'), { recursive: true })
|
|
13
|
+
cpSync(join(root, 'src'), join(root, 'lib'), { recursive: true })
|
|
14
|
+
console.log('build: src → lib copied')
|
package/scripts/build.sh
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-lib-freshness —— 守卫「提交的 lib/ 与 src/ 一致」。
|
|
4
|
+
*
|
|
5
|
+
* 为什么要这条守卫(2026-09-16):
|
|
6
|
+
* `lib/` 从这一版起**提交进 git**,因为 `dsh plugin --profile web add <本仓库 git URL>`
|
|
7
|
+
* 只能拿到 git 里已提交的内容,而 package.json 的 exports 指向 ./lib/index.js。
|
|
8
|
+
* lib/ 不提交时,git 安装出来的包缺入口文件、插件挂不上(实测:装到的包没有 lib/)。
|
|
9
|
+
* 代价是"改了 src 忘了 build + commit lib"会静默发出旧代码——本脚本把这条钉死。
|
|
10
|
+
*
|
|
11
|
+
* 用法:node scripts/check-lib-freshness.mjs (smoke 内也有一条同义断言)
|
|
12
|
+
* 退出码:0 = 逐字节一致;1 = 有缺、有多、或内容不一致(打印前若干条差异)。
|
|
13
|
+
*/
|
|
14
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
15
|
+
import { dirname, join, relative } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
|
|
18
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
19
|
+
|
|
20
|
+
/** 递归列出目录下所有文件的相对路径(POSIX 分隔符,便于比较)。 */
|
|
21
|
+
function walk(dir) {
|
|
22
|
+
const out = []
|
|
23
|
+
const stack = [dir]
|
|
24
|
+
while (stack.length) {
|
|
25
|
+
const cur = stack.pop()
|
|
26
|
+
for (const entry of readdirSync(cur, { withFileTypes: true })) {
|
|
27
|
+
const full = join(cur, entry.name)
|
|
28
|
+
if (entry.isDirectory()) stack.push(full)
|
|
29
|
+
else if (entry.isFile()) out.push(relative(dir, full).split('\\').join('/'))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return out.sort()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const libDir = join(root, 'lib')
|
|
36
|
+
const srcDir = join(root, 'src')
|
|
37
|
+
|
|
38
|
+
let libFiles
|
|
39
|
+
let srcFiles
|
|
40
|
+
try {
|
|
41
|
+
libFiles = walk(libDir)
|
|
42
|
+
} catch {
|
|
43
|
+
console.error('check-lib-freshness: lib/ 不存在——先跑 `node scripts/build.mjs`(lib/ 必须提交,见 .gitignore 注释)')
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
srcFiles = walk(srcDir)
|
|
48
|
+
} catch {
|
|
49
|
+
console.error('check-lib-freshness: src/ 不存在')
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const libSet = new Set(libFiles)
|
|
54
|
+
const srcSet = new Set(srcFiles)
|
|
55
|
+
const missing = srcFiles.filter((f) => !libSet.has(f)) // src 有、lib 没有(漏 build)
|
|
56
|
+
const extra = libFiles.filter((f) => !srcSet.has(f)) // lib 有、src 没有(残留/陈旧)
|
|
57
|
+
const changed = srcFiles.filter((f) => libSet.has(f) && !readFileSync(join(srcDir, f)).equals(readFileSync(join(libDir, f))))
|
|
58
|
+
|
|
59
|
+
const detail = []
|
|
60
|
+
if (missing.length) detail.push(`lib 缺 ${missing.length} 个:${missing.slice(0, 5).join('、')}`)
|
|
61
|
+
if (extra.length) detail.push(`lib 多 ${extra.length} 个:${extra.slice(0, 5).join('、')}`)
|
|
62
|
+
if (changed.length) detail.push(`内容不一致 ${changed.length} 个:${changed.slice(0, 5).join('、')}`)
|
|
63
|
+
|
|
64
|
+
if (detail.length) {
|
|
65
|
+
console.error(`check-lib-freshness: ✗ 提交的 lib/ 与 src/ 不一致——请跑 \`node scripts/build.mjs\` 并提交 lib/\n ${detail.join('\n ')}`)
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const bytes = libFiles.reduce((sum, f) => sum + statSync(join(libDir, f)).size, 0)
|
|
70
|
+
console.log(`check-lib-freshness: ✓ lib/ 与 src/ 逐字节一致(${libFiles.length} 个文件 / ${(bytes / 1024).toFixed(0)}KB)`)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-preset.mjs —— 预设自检(DSH 升级后必跑)
|
|
4
|
+
*
|
|
5
|
+
* 背景(2026-09-12 真实故障):本预设是**随包 standard 预设的全量副本 + 本插件的人格**。
|
|
6
|
+
* DSH 0.1.5-rc.2 把 `@deepseek-ai/dsh-persona` 的配置从单一 `text` 改成 `prefix`(required)+`suffix`,
|
|
7
|
+
* 随包 standard 跟着改了,我们的副本没跟着改 → 一切到「PPT 工作室」就报
|
|
8
|
+
* failed to apply loader entry persona: invalid config: - $.prefix missing required value
|
|
9
|
+
* 而单测/契约核对都发现不了——因为坏的是**预设行配置**,不是插件代码。
|
|
10
|
+
*
|
|
11
|
+
* 本脚本逐行比对"我们的预设"与"随包 standard 预设":
|
|
12
|
+
* - 两边都有、但 config 不一致 → FAIL(就是本轮这类漂移)
|
|
13
|
+
* - 只存在于随包 standard 的行 → WARN(我们可能漏了新版新增行)
|
|
14
|
+
* - 只存在于我们的行 → INFO(预期:人格那一行是有意差异)
|
|
15
|
+
*
|
|
16
|
+
* ── 2026-09-24 适配 DSH 0.1.7-rc.1:**参照物的位置和格式都变了** ────────────────────
|
|
17
|
+
* 0.1.6-:`@deepseek-ai/dsh-agent-presets/presets/standard/agent.cordis.yml`
|
|
18
|
+
* —— 一份 Loader 方言的**条目数组**(`dsh-agent-presets` 目录发现时代的随包预设)。
|
|
19
|
+
* 0.1.7+:`@deepseek-ai/dsh-web-app/presets/standard.patch.yml`
|
|
20
|
+
* —— 一份 **patch 列表**,里面 `- insert:` 一行 `@deepseek-ai/dsh-agent-preset`,
|
|
21
|
+
* 预设的行藏在 `config.plugins` 里(`dsh-agent-presets` 复数包已被
|
|
22
|
+
* `dsh-agent-preset` + `dsh-agent-preset-registry` 取代,旧路径不存在了)。
|
|
23
|
+
* 两种都认(老宿主上跑也不瞎),但**认到哪种会在输出里写明**。
|
|
24
|
+
*
|
|
25
|
+
* 找不到参照物时**不再静默跳过**:打印醒目 ⚠ 并给出"这条门禁本轮没生效"的结论;
|
|
26
|
+
* 加 `--require` 时直接 FAIL(发布/干净安装门禁用它——静默跳过正是 2026-09-24 那次
|
|
27
|
+
* "npm test 250/0 假绿"的成因:整套 DSH 相关检查都在跳过)。
|
|
28
|
+
*
|
|
29
|
+
* `--ref <文件>` 可**钉住参照物**(自动识别新旧格式):本机装了 DSH 时无法制造"找不到参照"的
|
|
30
|
+
* 场景,测试要靠它;CI 也可用它固定一份参照做回归。
|
|
31
|
+
*
|
|
32
|
+
* 用法:node scripts/check-preset.mjs [--preset <文件>] [--ref <文件>] [--verbose] [--require]
|
|
33
|
+
*/
|
|
34
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
35
|
+
import { join, dirname, resolve } from 'node:path'
|
|
36
|
+
import { fileURLToPath } from 'node:url'
|
|
37
|
+
import { execSync } from 'node:child_process'
|
|
38
|
+
import YAML from 'yaml'
|
|
39
|
+
|
|
40
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
41
|
+
const args = process.argv.slice(2)
|
|
42
|
+
const opt = (k, d) => {
|
|
43
|
+
const i = args.indexOf(k)
|
|
44
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : d
|
|
45
|
+
}
|
|
46
|
+
const oursPath = opt('--preset', join(root, 'agent-presets', 'ppt', 'agent.cordis.yml'))
|
|
47
|
+
const verbose = args.includes('--verbose')
|
|
48
|
+
const requireRef = args.includes('--require')
|
|
49
|
+
/** `--ref <文件>`:钉住参照物(自动识别新旧格式)。本机装了 DSH 时无法制造"找不到参照"的场景,测试靠它。 */
|
|
50
|
+
const refPin = opt('--ref', null) === null ? null : resolve(opt('--ref'))
|
|
51
|
+
|
|
52
|
+
/** 参照物的相对路径(相对 DSH 安装根):新格式与旧格式各一条。 */
|
|
53
|
+
const REF_MODERN = join('node_modules', '@deepseek-ai', 'dsh-web-app', 'presets', 'standard.patch.yml')
|
|
54
|
+
const REF_LEGACY = join('node_modules', '@deepseek-ai', 'dsh-agent-presets', 'presets', 'standard', 'agent.cordis.yml')
|
|
55
|
+
|
|
56
|
+
/** DSH 安装根候选:DSH_CHECKOUT → npm 全局 → DSH_HOME/profiles/<profile>/。 */
|
|
57
|
+
function installRoots() {
|
|
58
|
+
const roots = []
|
|
59
|
+
if (process.env.DSH_CHECKOUT) roots.push(process.env.DSH_CHECKOUT)
|
|
60
|
+
try {
|
|
61
|
+
const globalRoot = execSync('npm root -g', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
62
|
+
if (globalRoot) roots.push(join(globalRoot, '@deepseek-ai', 'dsh'), globalRoot)
|
|
63
|
+
} catch { /* npm 不可用则跳过 */ }
|
|
64
|
+
const dshHome = process.env.DSH_HOME || join(process.env.USERPROFILE || process.env.HOME || '', '.dsh')
|
|
65
|
+
for (const profile of ['web', 'headless', 'tui']) roots.push(join(dshHome, 'profiles', profile))
|
|
66
|
+
return roots
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 定位随包 standard 预设:返回 {path, kind:'modern'|'legacy'} 或 null。 */
|
|
70
|
+
function findShippedStandard() {
|
|
71
|
+
const roots = installRoots()
|
|
72
|
+
for (const [rel, kind] of [[REF_MODERN, 'modern'], [REF_LEGACY, 'legacy']]) {
|
|
73
|
+
for (const base of roots) {
|
|
74
|
+
const p = join(base, rel)
|
|
75
|
+
if (existsSync(p)) return { path: p, kind }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 按文档形状自动识别格式:条目数组 ⇒ legacy;patch 列表(含 insert)⇒ modern。 */
|
|
82
|
+
function detectKind(file) {
|
|
83
|
+
const doc = YAML.parse(readFileSync(file, 'utf8'))
|
|
84
|
+
if (Array.isArray(doc) && doc.every((r) => r && typeof r === 'object' && typeof r.id === 'string')) return 'legacy'
|
|
85
|
+
return 'modern'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 取 Loader **方言**的条目数组:两种参照格式都归一到这里(`!!js` 只当数据,不比对求值)。 */
|
|
89
|
+
const rowsOf = (file, kind) => {
|
|
90
|
+
const doc = YAML.parse(readFileSync(file, 'utf8'))
|
|
91
|
+
if (kind === 'legacy') {
|
|
92
|
+
if (!Array.isArray(doc)) throw new Error(`${file} 不是 loader 方言的条目数组`)
|
|
93
|
+
return doc.filter((r) => r && typeof r === 'object' && typeof r.id === 'string')
|
|
94
|
+
}
|
|
95
|
+
// modern:patch 列表 → insert 项 → 声明行 → config.plugins
|
|
96
|
+
const list = Array.isArray(doc) ? doc : [doc]
|
|
97
|
+
for (const patch of list) {
|
|
98
|
+
const inserts = Array.isArray(patch?.insert) ? patch.insert : []
|
|
99
|
+
for (const row of inserts) {
|
|
100
|
+
const rows = row?.config?.plugins
|
|
101
|
+
if (Array.isArray(rows)) return rows.filter((r) => r && typeof r === 'object' && typeof r.id === 'string')
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`${file} 里没有找到 \`- insert:\` 的 preset 声明(config.plugins)——上游格式又变了?`)
|
|
105
|
+
}
|
|
106
|
+
const norm = (v) => JSON.stringify(v ?? null)
|
|
107
|
+
|
|
108
|
+
const ref = refPin !== null
|
|
109
|
+
? (existsSync(refPin) ? { path: refPin, kind: detectKind(refPin) } : null)
|
|
110
|
+
: findShippedStandard()
|
|
111
|
+
if (ref === null && !existsSync(oursPath)) {
|
|
112
|
+
console.error(`✗ 找不到本预设:${oursPath}`)
|
|
113
|
+
process.exit(1)
|
|
114
|
+
}
|
|
115
|
+
if (ref === null) {
|
|
116
|
+
console.log('⚠ 未找到随包 standard 预设 —— **预设漂移门禁本轮没有生效**')
|
|
117
|
+
if (refPin !== null) console.log(` 你钉的参照物不存在:${refPin}`)
|
|
118
|
+
else {
|
|
119
|
+
console.log(' 查过这些位置(相对 DSH 安装根):')
|
|
120
|
+
for (const base of installRoots()) {
|
|
121
|
+
console.log(` · ${join(base, REF_MODERN)}`)
|
|
122
|
+
console.log(` · ${join(base, REF_LEGACY)}`)
|
|
123
|
+
}
|
|
124
|
+
console.log(' 修法:装好 DSH 再跑,或显式给 DSH_CHECKOUT=<DSH 安装根> / --ref <参照文件>。')
|
|
125
|
+
}
|
|
126
|
+
process.exit(requireRef ? 1 : 0)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const std = new Map(rowsOf(ref.path, ref.kind).map((r) => [r.id, r]))
|
|
130
|
+
const ours = new Map(rowsOf(oursPath, 'legacy').map((r) => [r.id, r]))
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* **声明过的有意差异**:本预设故意与随包 standard 不同的行(其余仍必须逐行一致——那才是防无意漂移)。
|
|
134
|
+
*
|
|
135
|
+
* 为什么需要这个白名单(2026-09-18 用户提问:"我们的预设和标准模式有啥区别?"):
|
|
136
|
+
* 本预设原来是 standard 的**逐行副本**,连 persona 都是通用编码人格
|
|
137
|
+
* ⇒ 它在界面上只是"改了名字的 standard",没有任何身份。而"人格"正是预设**唯一**还能可靠交付的东西
|
|
138
|
+
* (工具/技能按预设隔离在这版 DSH 上做不到:切换预设时 agent 已存在 ⇒ `agent/created` 钩子不触发;
|
|
139
|
+
* 技能注册表在父层读 ⇒ 子层注册看不见。见 docs/03 最新条目)。
|
|
140
|
+
* 所以:只允许这一行有意不同,且必须在这里写明理由——新增第二处差异会被门禁拦下。
|
|
141
|
+
*/
|
|
142
|
+
const INTENTIONAL = new Map([
|
|
143
|
+
['persona', '「PPT 工作室」的人格:把开工状态定成"演示文稿工程"(先定纲再排版、每页一个论点、数字要口径、导出前过门禁)——预设唯一的身份差异'],
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
console.log(`本预设:${oursPath}`)
|
|
147
|
+
console.log(`参照: ${ref.path}`)
|
|
148
|
+
console.log(`格式: ${ref.kind === 'modern' ? '0.1.7+(dsh-web-app patch 的 config.plugins)' : '0.1.6-(dsh-agent-presets 条目数组)'}\n`)
|
|
149
|
+
|
|
150
|
+
let fail = 0
|
|
151
|
+
let warn = 0
|
|
152
|
+
let intentional = 0
|
|
153
|
+
for (const [id, r] of ours) {
|
|
154
|
+
const s = std.get(id)
|
|
155
|
+
if (s === undefined) {
|
|
156
|
+
console.log(` · [本预设新增] ${id}(${r.name ?? '—'})`)
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
if (norm(r.config) !== norm(s.config)) {
|
|
160
|
+
if (INTENTIONAL.has(id)) {
|
|
161
|
+
intentional++
|
|
162
|
+
console.log(` ◆ [有意差异] ${id}(${r.name ?? '—'})— ${INTENTIONAL.get(id)}`)
|
|
163
|
+
if (verbose) {
|
|
164
|
+
console.log(` 随包 standard: ${norm(s.config)}`)
|
|
165
|
+
console.log(` 本预设: ${norm(r.config)}`)
|
|
166
|
+
}
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
fail++
|
|
170
|
+
console.log(` ✗ [配置漂移] ${id}(${r.name ?? '—'})`)
|
|
171
|
+
console.log(` 随包 standard: ${norm(s.config)}`)
|
|
172
|
+
console.log(` 本预设: ${norm(r.config)}`)
|
|
173
|
+
} else if (verbose) {
|
|
174
|
+
console.log(` ✓ ${id}`)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const [id, s] of std) {
|
|
178
|
+
if (!ours.has(id)) {
|
|
179
|
+
warn++
|
|
180
|
+
console.log(` ⚠ [随包含有、本预设缺少] ${id}(${s.name ?? '—'})——新版新增行?确认是否要补`)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(`\n==== 预设自检:漂移 ${fail} / 缺行 ${warn} / 有意差异 ${intentional}(参照格式 ${ref.kind}) ====`)
|
|
185
|
+
if (fail > 0) {
|
|
186
|
+
console.log('修复方式:把漂移行的 config 改成随包 standard 的形状(预设是它的全量副本),再 npm pack 发版;')
|
|
187
|
+
console.log('若这是**有意**差异,请加进本脚本顶部的 INTENTIONAL 白名单并写明理由(否则它会在下次升级时被当成漂移)。')
|
|
188
|
+
}
|
|
189
|
+
process.exit(fail > 0 ? 1 : 0)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 发行字节守卫:所有**跟踪的文本文件必须是 LF**。
|
|
4
|
+
*
|
|
5
|
+
* 为什么需要这条(2026-09-18,创意工坊 PR 审核把问题翻出来的那一类):
|
|
6
|
+
* 本插件有两条"一条命令安装"的路径,用户拿到的东西必须是**同一份字节**:
|
|
7
|
+
* ① 创意工坊卡片那条 `dsh plugin add <仓库 URL>` → pnpm 走 **git 检出**已提交内容;
|
|
8
|
+
* ② GitHub Release 的 `.tgz` 资产 → `npm pack` 打包**工作区**内容。
|
|
9
|
+
* ②永远是 LF(脚本写出来的),而①在 Windows 上会被 `core.autocrlf=true`
|
|
10
|
+
* (**Git for Windows 的系统级默认值**,用户不需要配置任何东西)转成 CRLF。
|
|
11
|
+
* CRLF 会让 src/templates.js 里按 `\n` 做的行锚定文本手术**静默失效**——
|
|
12
|
+
* 物化出来的模板工作区 deck.yaml 引用一个故意没被复制的母版页,resolveDeck 抛
|
|
13
|
+
* `page file missing: pages/_cover.yaml`,干净克隆里 `npm test` 直接崩。
|
|
14
|
+
*
|
|
15
|
+
* 三道防线各管一段,缺一不可:
|
|
16
|
+
* · `.gitattributes`(`* text=auto eol=lf`)——把检出字节钉成 LF(正常情况由它解决);
|
|
17
|
+
* · 本脚本——`eol=lf` 被误覆盖(例如有人给某类文件写了 `-text`、或把文本文件当 binary 声明)时当场红;
|
|
18
|
+
* · src/templates.js 的 `normalizeText()`——用户**手改过**的 CRLF/BOM 文件也不失效(这道防线管的是用户内容,
|
|
19
|
+
* 不是发行物,所以不能拿它替代前两道)。
|
|
20
|
+
*
|
|
21
|
+
* 判定看**内容里的 `\r\n`**,不看 git 属性:查的是"安装路径真正拿到的字节"。
|
|
22
|
+
* 二进制按扩展名 + NUL 字节跳过(.pptx/.png/.woff…)。
|
|
23
|
+
*
|
|
24
|
+
* 用法:node scripts/check-tracked-lf.mjs # 有问题 exit 1
|
|
25
|
+
* node scripts/check-tracked-lf.mjs --fix # 就地改写成 LF(作者/贡献者在 Windows 上被编辑器改坏时用)
|
|
26
|
+
*/
|
|
27
|
+
import { execFileSync } from 'node:child_process'
|
|
28
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
29
|
+
import { extname, join, dirname } from 'node:path'
|
|
30
|
+
import { fileURLToPath } from 'node:url'
|
|
31
|
+
|
|
32
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
33
|
+
const FIX = process.argv.includes('--fix')
|
|
34
|
+
|
|
35
|
+
/** 二进制扩展名(不参与换行检查)。 */
|
|
36
|
+
const BINARY = new Set([
|
|
37
|
+
'.pptx', '.zip', '.tgz', '.gz', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico',
|
|
38
|
+
'.woff', '.woff2', '.ttf', '.otf', '.pdf', '.xlsx', '.docx', '.mp4', '.webp',
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
let files
|
|
42
|
+
try {
|
|
43
|
+
files = execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf8' })
|
|
44
|
+
.split('\0')
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.log(`⚠ 跳过:拿不到 git 跟踪清单(${String(e?.message ?? e).split('\n')[0].slice(0, 100)})`)
|
|
48
|
+
process.exit(0)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const offenders = []
|
|
52
|
+
let checked = 0
|
|
53
|
+
for (const rel of files) {
|
|
54
|
+
if (BINARY.has(extname(rel).toLowerCase())) continue
|
|
55
|
+
let buf
|
|
56
|
+
try {
|
|
57
|
+
buf = readFileSync(join(root, rel))
|
|
58
|
+
} catch {
|
|
59
|
+
continue // 工作区缺失(稀疏检出等):不是本检查的职责
|
|
60
|
+
}
|
|
61
|
+
if (buf.includes(0)) continue // NUL ⇒ 二进制
|
|
62
|
+
checked++
|
|
63
|
+
// 只看 CRLF:单独出现的 \r(老 Mac 风格)在本仓库不存在,不必处理
|
|
64
|
+
if (buf.includes('\r\n')) offenders.push(rel)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (offenders.length) {
|
|
68
|
+
if (FIX) {
|
|
69
|
+
for (const rel of offenders) {
|
|
70
|
+
const p = join(root, rel)
|
|
71
|
+
writeFileSync(p, readFileSync(p, 'utf8').replace(/\r\n/g, '\n'))
|
|
72
|
+
}
|
|
73
|
+
console.log(`✓ 已把 ${offenders.length} 个文件改写成 LF(工作区字节现在与提交内容一致)`)
|
|
74
|
+
process.exit(0)
|
|
75
|
+
}
|
|
76
|
+
console.error(`✗ 有 ${offenders.length} 个跟踪的文本文件是 CRLF(应为 LF):`)
|
|
77
|
+
for (const f of offenders.slice(0, 30)) console.error(` ${f}`)
|
|
78
|
+
if (offenders.length > 30) console.error(` …还有 ${offenders.length - 30} 个`)
|
|
79
|
+
console.error(' 为什么必须修:CRLF 检出会让行锚定文本手术静默失效 ⇒ git 安装(创意工坊那条命令)')
|
|
80
|
+
console.error(' 与 tgz 安装不是同一份字节。修法:node scripts/check-tracked-lf.mjs --fix')
|
|
81
|
+
console.error(' (脏的只是工作区字节——提交内容一直要求 LF;改完 git status 应仍为干净。)')
|
|
82
|
+
process.exit(1)
|
|
83
|
+
}
|
|
84
|
+
console.log(`✓ 发行字节:${checked} 个跟踪的文本文件全部是 LF(git 安装 == tgz 安装的前提)`)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { zipRead } from '../lib/zips.js'
|
|
2
|
+
import { parseXml } from '../lib/xmljs.js'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
|
|
5
|
+
const buf = await readFile('examples/smoke/out-smoke.pptx')
|
|
6
|
+
const files = zipRead(buf)
|
|
7
|
+
const xml = files.get('ppt/presentation.xml').toString('utf8')
|
|
8
|
+
console.log('xml head:', xml.slice(0, 200))
|
|
9
|
+
console.log('xml len:', xml.length)
|
|
10
|
+
const pres = parseXml(xml)
|
|
11
|
+
const p = pres.children[0]
|
|
12
|
+
console.log('root child:', p.tag, 'children:', p.children.length, 'text len:', p.text.length)
|
|
13
|
+
if (p.children[0]) console.log('first child:', p.children[0].tag, JSON.stringify(p.children[0].attrs).slice(0, 120))
|