dsh-ppt-studio 1.1.2 → 1.1.4

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 (55) hide show
  1. package/README.md +38 -18
  2. package/agent-presets/ppt/agent.cordis.yml +22 -15
  3. package/cordis.patch.yml +19 -14
  4. package/docs/01-/351/234/200/346/261/202/344/270/216/347/233/256/346/240/207.md +1 -1
  5. package/docs/02-/346/212/200/346/234/257/346/212/245/345/221/212.md +5 -5
  6. package/docs/03-/346/233/264/346/226/260/346/227/245/345/277/227.md +105 -0
  7. package/docs/05-/350/277/255/344/273/243/346/265/201/347/250/213.md +34 -3
  8. package/docs/06-/350/257/204/345/256/241/344/270/216/346/265/213/350/257/225.md +5 -3
  9. package/examples/fx/fx-pro/preview/04_pages_04_chart.html +1 -1
  10. package/examples/fx/fx-pro/preview/deck.html +1 -1
  11. package/examples/fx/fx-pro/preview/measured.json +1 -1
  12. package/examples/fx/fx-pro.pptx +0 -0
  13. package/examples/fx/seed.pptx +0 -0
  14. package/examples/fx/single.pptx +0 -0
  15. package/examples/fx/spliced.pptx +0 -0
  16. package/examples/smoke/imported/deck.yaml +6 -6
  17. package/examples/smoke/imported/import-styles.json +1 -33
  18. package/examples/smoke/imported/loss-audit.json +99 -0
  19. package/examples/smoke/imported/pages/slide_03.yaml +5 -36
  20. package/examples/smoke/imported/preview-rt/03_pages_slide_03.html +1 -1
  21. package/examples/smoke/imported/preview-rt/deck.html +1 -1
  22. package/examples/smoke/imported/preview-rt/layout.json +26 -78
  23. package/examples/smoke/imported/reference/previews/03.png +0 -0
  24. package/examples/smoke/imported/source.pptx +0 -0
  25. package/examples/smoke/out-smoke.pptx +0 -0
  26. package/examples/smoke/preview/03_pages_03_chart.html +1 -1
  27. package/examples/smoke/preview/deck.html +1 -1
  28. package/lib/capabilities.js +189 -0
  29. package/lib/home.js +32 -0
  30. package/lib/index.js +278 -112
  31. package/lib/office-qa.js +111 -0
  32. package/lib/pptd/export-pptx.js +159 -10
  33. package/lib/pptd/import-pptx.js +12 -0
  34. package/lib/pptd/loss-audit.js +363 -0
  35. package/lib/pptd/nativeChart.js +454 -0
  36. package/lib/pptd/schema.js +48 -1
  37. package/lib/pptd/svgCharts.js +180 -37
  38. package/lib/pptxPy.js +96 -11
  39. package/lib/preset-delivery.js +107 -9
  40. package/lib/preview-server.js +7 -5
  41. package/lib/router.js +3 -2
  42. package/lib/state.js +8 -6
  43. package/lib/templates.js +4 -4
  44. package/lib/tools.js +81 -21
  45. package/package.json +2 -1
  46. package/scripts/audit-manual-facts.mjs +4 -2
  47. package/scripts/e2e-1.0.mjs +2 -2
  48. package/scripts/fixtures/workflow-baseline.json +10 -10
  49. package/scripts/install.mjs +13 -11
  50. package/scripts/smoke.mjs +390 -66
  51. package/scripts/verify-loss-audit.mjs +245 -0
  52. package/scripts/verify-native-charts.mjs +529 -0
  53. package/scripts/verify-preset-scope.mjs +593 -0
  54. package/skills/ppt-studio-data/SKILL.md +5 -7
  55. package/skills/ppt-studio-manual/SKILL.md +2 -2
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * PPTD → PPTX 导出器(主引擎):生成最小可打开的 OOXML。
3
3
  * - 元素:text / shape(rect,ellipse,triangle) / line(箭头) / image / table
4
- * / chart(矢量拼绘:bar=矩形、line=连接线+圆点、pie=饼形)
4
+ * / chart(第三轮起**默认原生可编辑图表**:ppt/charts/chartN.xml + 内嵌数据工作簿,
5
+ * PowerPoint 里可"编辑数据"/换类型;`chart.render: 'vector'` 保留矢量拼绘降级:bar=矩形、line=连线+圆点、pie=饼形)
5
6
  * - 文本自动 fit:与 verify 同一度量与阈值(overflow > 1px 才缩),按比例缩字号;
6
7
  * 下限语义(2026-09-06 用户拍板):theme.minFontSize 显式 = 用户给出的字号下限(严格遵守,且不超过原字号);
7
8
  * 未设置 = 无强制下限(仅 60% 原字号防荒谬保底),绝不升字;到下限仍溢出记 floorHit。
@@ -15,7 +16,8 @@ import { join, isAbsolute } from 'node:path'
15
16
  import { createHash } from 'node:crypto'
16
17
  import { zipWrite } from '../zips.js'
17
18
  import { normalizePage } from './layout.js'
18
- import { chartData, chartColors, chartIssue } from './svgCharts.js'
19
+ import { chartData, chartColors, chartIssue, resolveChart } from './svgCharts.js'
20
+ import { auditNativeCharts, chartFrameXml, chartPartNames, embeddedWorkbook, nativeChartRels, nativeChartXml } from './nativeChart.js'
19
21
 
20
22
  const EMU = 12700
21
23
  const xm = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
@@ -73,7 +75,9 @@ export async function exportPptx(ctx, { out = 'out.pptx', engine = 'pptd' } = {}
73
75
  const shapes = []
74
76
  const media = []
75
77
  const mediaSeen = new Map()
76
- const counts = { table: 0, image: 0, chart: 0 }
78
+ const counts = { table: 0, image: 0, chart: 0, chartNative: 0, chartVector: 0 }
79
+ // 原生图表:收集本页要产出的图表部件(rId / 部件名在"媒体发现完之后"才分配,见下方 relBase)
80
+ const chartRefs = []
77
81
  const addMedia = (srcPath) => {
78
82
  if (mediaSeen.has(srcPath)) return mediaSeen.get(srcPath)
79
83
  const rId = `rId${media.length + 2}`
@@ -102,7 +106,17 @@ export async function exportPptx(ctx, { out = 'out.pptx', engine = 'pptd' } = {}
102
106
  counts.chart++
103
107
  const issue = chartIssue(el.chart)
104
108
  if (issue) report.chartInfos.push(`第 ${page.index + 1} 页(${page.name})${el.id}: ${issue}`)
105
- shapes.push(...chartSp(el))
109
+ // 第三轮:默认原生可编辑图表;`render: 'vector'` 才走旧的矢量拼绘(形状计数自证照旧)
110
+ if (resolveChart(el.chart).opts.render === 'vector') {
111
+ counts.chartVector++
112
+ shapes.push(...chartSp(el))
113
+ } else {
114
+ counts.chartNative++
115
+ const k = chartRefs.length
116
+ chartRefs.push({ el, pageNo: page.index + 1, pageName: page.name })
117
+ // rId 先占位:媒体是在本循环里陆续发现的,图表关系必须排在媒体(与讲稿)之后
118
+ shapes.push(chartFrameXml(el, nid(), `__CHART_RID_${k}__`))
119
+ }
106
120
  break
107
121
  }
108
122
  }
@@ -115,13 +129,28 @@ export async function exportPptx(ctx, { out = 'out.pptx', engine = 'pptd' } = {}
115
129
  bg.xml = bg.xml.replace('BGPLACEHOLDER', bg.rId)
116
130
  }
117
131
 
132
+ // ── 关系编号(媒体之后):讲稿 → 图表 ──────────────────────────────────
133
+ // 不产图表的工程:编号与旧实现**逐字节一致**(rId1=slideLayout,rId2..=media,notesRel=media+2)。
134
+ const notes = typeof page.page.notes === 'string' && page.page.notes.trim() ? page.page.notes.trim() : null
135
+ const relBase = media.length + 2
136
+ const notesRId = notes ? `rId${relBase}` : null
137
+ chartRefs.forEach((c, k) => {
138
+ c.rId = `rId${relBase + (notes ? 1 : 0) + k}`
139
+ })
140
+ for (let k = 0; k < chartRefs.length; k++) {
141
+ const idx = shapes.findIndex((s) => typeof s === 'string' && s.includes(`__CHART_RID_${k}__`))
142
+ if (idx >= 0) shapes[idx] = shapes[idx].replace(`__CHART_RID_${k}__`, chartRefs[k].rId)
143
+ }
144
+
118
145
  slides.push({
119
146
  page,
120
147
  shapes,
121
148
  media,
122
149
  counts,
150
+ chartRefs,
123
151
  // 讲稿:只有非空字符串才产备注页(空/未写 = 与旧行为完全一致,不生成部件)
124
- notes: typeof page.page.notes === 'string' && page.page.notes.trim() ? page.page.notes.trim() : null,
152
+ notes,
153
+ notesRId,
125
154
  notesNo: 0,
126
155
  spTree: `<p:spTree>${spTreeHeader()}${shapes.join('')}</p:spTree>`,
127
156
  bg,
@@ -131,8 +160,21 @@ export async function exportPptx(ctx, { out = 'out.pptx', engine = 'pptd' } = {}
131
160
  let notesCount = 0
132
161
  for (const s of slides) if (s.notes) s.notesNo = ++notesCount
133
162
 
163
+ // ── 原生图表编号(跨页递增,chart1..chartN)与部件生成 ──────────────────────
164
+ // 每张原生图表 = 一个 chartN.xml + 一个内嵌工作簿(Microsoft_Excel_SheetN.xlsx),与 python-pptx 产物同构。
165
+ const nativeCharts = []
166
+ for (const s of slides) {
167
+ for (const c of s.chartRefs ?? []) {
168
+ const chartNo = nativeCharts.length + 1
169
+ const names = chartPartNames(chartNo)
170
+ c.chartNo = chartNo
171
+ c.names = names
172
+ nativeCharts.push({ id: c.el.id, chart: c.el.chart, pageNo: c.pageNo, pageName: c.pageName, chartNo, names })
173
+ }
174
+ }
175
+
134
176
  const files = {}
135
- files['[Content_Types].xml'] = contentTypes(slides)
177
+ files['[Content_Types].xml'] = contentTypes(slides, nativeCharts)
136
178
  files['_rels/.rels'] = rootRels()
137
179
  files['docProps/core.xml'] = coreProps(ctx)
138
180
  files['docProps/app.xml'] = appProps(ctx)
@@ -147,13 +189,19 @@ export async function exportPptx(ctx, { out = 'out.pptx', engine = 'pptd' } = {}
147
189
  files['ppt/notesMasters/notesMaster1.xml'] = notesMasterXml()
148
190
  files['ppt/notesMasters/_rels/notesMaster1.xml.rels'] = notesMasterRels()
149
191
  }
192
+ // 图表部件三件套(chartN.xml / chartN.xml.rels / 内嵌 xlsx)
193
+ for (const ch of nativeCharts) {
194
+ files[ch.names.chart] = nativeChartXml({ chart: ch.chart, chartNo: ch.chartNo })
195
+ files[ch.names.rels] = nativeChartRels(ch.chartNo)
196
+ files[ch.names.embed] = embeddedWorkbook(ch.chart)
197
+ }
150
198
 
151
199
  for (let i = 0; i < slides.length; i++) {
152
200
  const s = slides[i]
153
201
  const n = i + 1
154
202
  files[`ppt/slides/slide${n}.xml`] = slideXml(s)
155
- // 备注页关系:rId 取媒体之后的下一个空闲号(媒体用 rId2..rId(1+media))
156
- const notesRel = s.notes ? `<Relationship Id="rId${s.media.length + 2}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide" Target="../notesSlides/notesSlide${s.notesNo}.xml"/>` : ''
203
+ // 备注页关系:rId 取媒体之后的下一个空闲号(媒体用 rId2..rId(1+media))——见页面循环里的统一分配
204
+ const notesRel = s.notesRId ? `<Relationship Id="${s.notesRId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide" Target="../notesSlides/notesSlide${s.notesNo}.xml"/>` : ''
157
205
  if (s.notes) {
158
206
  files[`ppt/notesSlides/notesSlide${s.notesNo}.xml`] = notesSlideXml(s.notes)
159
207
  // 关键:notesSlide **必须自带 rels**(→ notesMaster + 回指本页幻灯)。
@@ -170,6 +218,7 @@ export async function exportPptx(ctx, { out = 'out.pptx', engine = 'pptd' } = {}
170
218
  '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>',
171
219
  ...s.media.map((m) => `<Relationship Id="${m.rId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/${m.part}"/>`),
172
220
  notesRel,
221
+ ...(s.chartRefs ?? []).map((c) => `<Relationship Id="${c.rId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" Target="../charts/chart${c.chartNo}.xml"/>`),
173
222
  '</Relationships>',
174
223
  ].join('')
175
224
  files[`ppt/slides/_rels/slide${n}.xml.rels`] = srels
@@ -209,14 +258,84 @@ export async function exportPptx(ctx, { out = 'out.pptx', engine = 'pptd' } = {}
209
258
  const mediaExpSet = new Set()
210
259
  for (const s of slides) for (const m of s.media) mediaExpSet.add(m.srcPath)
211
260
  const mediaOut = Object.keys(files).filter((k) => /^ppt\/media\//.test(k)).length
261
+ // 内容级回读(2026-09-26 第二轮 启发 A):**产物里到底有没有我们打算写的那句话、那格数据**。
262
+ // 为什么必须补:此前 parity 只有**计数**(表/图/线/媒体)+ audit 的页数/尺寸/字号 ⇒
263
+ // "预览层对、成品层丢字/串行/表格漏格"这一类抓不到。官方 office 技能同款纪律:
264
+ // "Reopen the file to check the requested edits." 本检查**从产物反推内容**,不是读中间层。
265
+ const textMissing = []
266
+ const chartFramesMissing = []
267
+ let textExp = 0
268
+ let chartShapesExp = 0
269
+ let chartShapesOut = 0
270
+ for (let i = 0; i < slides.length; i++) {
271
+ const slot = (ctx.pages ?? [])[i]
272
+ if (slot === undefined) continue
273
+ // `ctx.pages[i]` = `{ file, ref, page, name, index }`,元素在 `slot.page.elements`(DSL 原文:`elementId`)
274
+ const elements = slot.page?.elements ?? []
275
+ const pageNo = (slot.index ?? i) + 1
276
+ const raw = files[`ppt/slides/slide${i + 1}.xml`]
277
+ const txt = typeof raw === 'string' ? raw : String(raw ?? '')
278
+ for (const el of elements) {
279
+ const id = String(el.elementId ?? el.id ?? '?')
280
+ const want = []
281
+ if (el.elementType === 'text') {
282
+ for (const line of String(el.content?.text ?? '').split('\n')) if (line.trim() !== '') want.push(line)
283
+ } else if (el.elementType === 'table') {
284
+ for (const v of [...(el.cols ?? []), ...((el.rows ?? []).flat())]) {
285
+ if (v !== null && v !== undefined && String(v) !== '') want.push(String(v))
286
+ }
287
+ }
288
+ for (const w of want) {
289
+ textExp++
290
+ if (!txt.includes(xm(w))) textMissing.push(`第 ${pageNo} 页/${id}: ${w.slice(0, 24)}`)
291
+ }
292
+ if (el.elementType === 'chart') {
293
+ if (resolveChart(el.chart).opts.render === 'vector') {
294
+ // 矢量拼绘(形状不是图表对象)⇒ 内容级判据只能是"该画的图形个数对不对"。
295
+ chartShapesExp += chartShapeCountOf(el)
296
+ const re = new RegExp(`name="${escapeRe(xm(id))}-`, 'g')
297
+ chartShapesOut += (txt.match(re) ?? []).length
298
+ } else {
299
+ // 原生图表:判据在包级(部件数 + externalData 关系 + 内嵌工作簿数据 == deck 数据),见下面的 auditNativeCharts。
300
+ // 幻灯片级还要自证"这个图表帧真的在产物里"(防止 graphicFrame 被静默丢弃/参数写错)。
301
+ const frameOk = txt.includes(`name="${xm(id)}"`) && txt.includes('drawingml/2006/chart')
302
+ if (!frameOk) chartFramesMissing.push(`第 ${pageNo} 页/${id}: slide XML 里没有图表帧(graphicFrame + graphicData uri=.../chart)`)
303
+ }
304
+ }
305
+ }
306
+ }
307
+ // 原生图表部件自证(第三轮):声明的 chart 元素数 == ppt/charts/*.xml 数;
308
+ // 每个图表帧的 rId 真的指向 chartN.xml;chartN 的 externalData 关系真的指向存在的内嵌工作簿;
309
+ // 内嵌工作簿 `xl/worksheets/sheet1.xml` 取值 == deck 数据(逐格比对)。
310
+ const chartAudit = auditNativeCharts(files, nativeCharts.map((c) => ({ id: `${c.id}(第 ${c.pageNo} 页)`, chart: c.chart, chartPart: c.names.chart })))
311
+ const chartRelMissing = []
312
+ // 图表帧 → slide rels → chartN.xml 的链路自证(逐条从产物反查)
313
+ for (let i = 0; i < slides.length; i++) {
314
+ for (const c of slides[i].chartRefs ?? []) {
315
+ const srels = String(files[`ppt/slides/_rels/slide${i + 1}.xml.rels`] ?? '')
316
+ if (!srels.includes(`Id="${c.rId}"`) || !srels.includes(`../charts/chart${c.chartNo}.xml`)) {
317
+ chartRelMissing.push(`第 ${i + 1} 页/${c.el.id}: slide rels 缺 ${c.rId} → chart${c.chartNo}.xml`)
318
+ }
319
+ if (!String(files[`ppt/slides/slide${i + 1}.xml`]).includes(`r:id="${c.rId}"`)) {
320
+ chartRelMissing.push(`第 ${i + 1} 页/${c.el.id}: 图表帧没有引用 ${c.rId}`)
321
+ }
322
+ }
323
+ }
212
324
  report.parity = {
213
325
  tablesExp, tablesOut, imagesExp, imagesOut, illegalFrames: illegal,
214
326
  linesExp: lineProof.exp, linesOut: lineProof.out, linesWrong: lineProof.wrong.length,
215
327
  notesExp, notesOut,
216
328
  mediaExp: mediaExpSet.size, mediaOut,
329
+ textExp, textMissing, chartShapesExp, chartShapesOut,
330
+ chartFramesMissing,
331
+ chartsExp: chartAudit.chartsExp, chartsOut: chartAudit.chartsOut,
332
+ chartEmbedsOut: chartAudit.embedsOut, chartSheetsChecked: chartAudit.sheetsChecked,
333
+ chartDataMismatch: chartAudit.dataMismatch, chartRelsMissing: [...chartAudit.relsMissing, ...chartRelMissing],
217
334
  ok: tablesExp === tablesOut && imagesExp === imagesOut && illegal === 0 &&
218
335
  lineProof.exp === lineProof.out && lineProof.wrong.length === 0 &&
219
- notesExp === notesOut && notesRelOk && mediaExpSet.size === mediaOut,
336
+ notesExp === notesOut && notesRelOk && mediaExpSet.size === mediaOut &&
337
+ textMissing.length === 0 && chartShapesExp === chartShapesOut &&
338
+ chartFramesMissing.length === 0 && chartAudit.ok && chartRelMissing.length === 0,
220
339
  }
221
340
 
222
341
  // out:绝对路径原样使用;相对路径相对 deck 目录(反馈 E1 ★)
@@ -225,6 +344,10 @@ export async function exportPptx(ctx, { out = 'out.pptx', engine = 'pptd' } = {}
225
344
  report.file = outPath
226
345
  report.slides = slides.length
227
346
  report.engine = engine
347
+ // 原生图表清单(可观测,便于 smoke/验证脚本核对"第几个 chartN 是哪一页的哪个元素")
348
+ if (nativeCharts.length) {
349
+ report.nativeCharts = nativeCharts.map((c) => ({ id: c.id, page: c.pageNo, chartNo: c.chartNo, part: c.names.chart, embed: c.names.embed }))
350
+ }
228
351
  // 改名必须**可见**(不静默):非 ASCII/空格或重名时部件名变了,报告里说明改成了什么。
229
352
  if (renamedMedia.size) report.mediaRenamed = [...renamedMedia].map(([src, part]) => `${src} → ppt/media/${part}`)
230
353
  return report
@@ -462,6 +585,24 @@ function tableFrame(el) {
462
585
  }
463
586
 
464
587
  // ── chart(矢量拼绘)───────────────────────────────────────────────────────
588
+ /** 正则元字符转义(元素 id 可能含 `(`、`.` 等;用于"这个图表的形状有没有真的写进产物")。 */
589
+ function escapeRe(s) {
590
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
591
+ }
592
+
593
+ /**
594
+ * 一个 chart 元素**应当**在产物里画出多少个形状(与下面的 `chartSp` 一一对应;改 chartSp 必须同步改这里)。
595
+ * bar:每个数值一根矩形;line:每系列 (点数−1) 条连接线 + 每个点一个圆点;pie:每个正数一个扇区。
596
+ */
597
+ function chartShapeCountOf(el) {
598
+ const data = chartData(el.chart)
599
+ const kind = el.chart?.type
600
+ if (kind === 'bar') return data.series.reduce((n, s) => n + s.values.length, 0)
601
+ if (kind === 'line') return data.series.reduce((n, s) => n + Math.max(0, s.values.length - 1) + s.values.length, 0)
602
+ if (kind === 'pie') return (data.series[0]?.values ?? []).filter((v) => v > 0).length
603
+ return 0
604
+ }
605
+
465
606
  function chartSp(el) {
466
607
  const x = el.bounds.x
467
608
  const y = el.bounds.y
@@ -531,7 +672,13 @@ function chartSp(el) {
531
672
  }
532
673
 
533
674
  // ── package parts ─────────────────────────────────────────────────────────
534
- function contentTypes(slides) {
675
+ /**
676
+ * `[Content_Types].xml`。
677
+ * `nativeCharts`(第三轮):每个 `ppt/charts/chartN.xml` 一条 Override(`drawingml.chart+xml`),
678
+ * 并补 `<Default Extension="xlsx">`(内嵌数据工作簿是 `ppt/embeddings/*.xlsx`)。
679
+ * 无原生图表的工程**逐字节不变**(老工程/回归样例零变化)。
680
+ */
681
+ function contentTypes(slides, nativeCharts = []) {
535
682
  const notes = slides.filter((s) => s.notes)
536
683
  const overrides = [
537
684
  '<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>',
@@ -543,6 +690,7 @@ function contentTypes(slides) {
543
690
  ...(notes.length ? ['<Override PartName="/ppt/notesMasters/notesMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml"/>'] : []),
544
691
  ...slides.map((_, i) => '<Override PartName="/ppt/slides/slide' + (i + 1) + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>'),
545
692
  ...notes.map((s) => '<Override PartName="/ppt/notesSlides/notesSlide' + s.notesNo + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml"/>'),
693
+ ...nativeCharts.map((c) => '<Override PartName="/' + c.names.chart + '" ContentType="application/vnd.openxmlformats-officedocument.drawingml.chart+xml"/>'),
546
694
  ]
547
695
  return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
548
696
  + '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
@@ -550,6 +698,7 @@ function contentTypes(slides) {
550
698
  + '<Default Extension="png" ContentType="image/png"/><Default Extension="jpeg" ContentType="image/jpeg"/>'
551
699
  + '<Default Extension="jpg" ContentType="image/jpeg"/><Default Extension="gif" ContentType="image/gif"/>'
552
700
  + '<Default Extension="webp" ContentType="image/webp"/>'
701
+ + (nativeCharts.length ? '<Default Extension="xlsx" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"/>' : '')
553
702
  + overrides.join('') + '</Types>'
554
703
  }
555
704
 
@@ -9,6 +9,7 @@ import { existsSync } from 'node:fs'
9
9
  import { join, basename } from 'node:path'
10
10
  import { zipRead, decodeXml } from '../zips.js'
11
11
  import { parseXml, children, first, allText } from '../xmljs.js'
12
+ import { auditLoss, lossAuditLine, LOSS_AUDIT_FILE } from './loss-audit.js'
12
13
 
13
14
  const EMU = 12700
14
15
  const px = (emu) => Math.round(emu / EMU)
@@ -152,11 +153,22 @@ export async function importPptx(pptxPath, outDir) {
152
153
  elements: pages.flatMap((p) => p.elements.map((el) => ({ page: p.index + 1, id: el.elementId, ...(el._styleRaw ?? {}) }))),
153
154
  }
154
155
  await writeFile(join(outDir, 'import-styles.json'), JSON.stringify(stylesJson, null, 2))
156
+ // 启发 B(审计半):重建有损审计——源稿里 DSL/导出无法保真的特性(动画/阴影/超链接/切换/
157
+ // SmartArt/不可嵌入媒体/主题渐变)。单独成文 loss-audit.json(理由见下方 return 注释),
158
+ // 工具输出用 lossAuditLine 给一行结论,支撑"只改某几页 ⇒ 优先 ppt_splice"的选择。
159
+ const lossAudit = auditLoss(files, { source: basename(pptxPath) })
160
+ await writeFile(join(outDir, LOSS_AUDIT_FILE), JSON.stringify(lossAudit, null, 2))
155
161
  const bgCount = pages.filter((p) => p.background).length
156
162
  const styleCount = Object.keys(stats.colors).length
157
163
  return {
158
164
  outDir, pages: pages.length, media: [...mediaNames], size: { width, height },
159
165
  reference: refPreviews?.length ? { previews: refPreviews, source: 'source.pptx' } : null,
166
+ // 启发 B:损失审计报告 + 一行结论(tools.js 的 ppt_import 只加这一行输出)。
167
+ // JSON 独立成 loss-audit.json(而非塞进 import-styles.json):① 语义相反——styles 记"我们保住了什么",
168
+ // audit 记"我们保不住什么",混在一起会让"改版求一致直接复用 styles"的既有使用者读到无关字段;
169
+ // ② 生命周期不同:audit 是决策输入(改哪几页 / 要不要 splice),styles 是素材账本;
170
+ // ③ 独立文件名便于模型/用户直接定位与断言(smoke 无需解析既有 schema,向后兼容、零破坏)。
171
+ lossAudit, lossLine: lossAuditLine(lossAudit),
160
172
  warnings: ['chart 降级为文本占位;未映射 prst 形状近似为矩形(import-styles.json 有记录)',
161
173
  ...(missingMedia.length ? [`⚠ 原稿 ${missingMedia.length} 个媒体引用未提取(${missingMedia.join('、')}——rels 指向包外/特殊格式):已在 media/ 生成 1×1 白色占位图,导出可用;建议人工替换或删除对应页面元素`] : []),
162
174
  ...(existsSync(join(outDir, 'source.pptx')) ? ['已保留原始 pptx ⊳ source.pptx(零失真真相层;参考双轨 v0.9.1)'] : []),