dsh-ppt-studio 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +480 -0
  3. package/agent-presets/ppt/agent.cordis.yml +308 -0
  4. package/agent-presets/ppt/preset.yml +3 -0
  5. package/cordis.patch.yml +29 -0
  6. package/docs/00-/350/256/276/350/256/241/346/272/220.md +537 -0
  7. package/docs/01-/351/234/200/346/261/202/344/270/216/347/233/256/346/240/207.md +136 -0
  8. package/docs/02-/346/212/200/346/234/257/346/212/245/345/221/212.md +483 -0
  9. package/docs/03-/346/233/264/346/226/260/346/227/245/345/277/227.md +2898 -0
  10. package/docs/04-/350/267/257/347/272/277/345/233/276/344/270/216/351/207/214/347/250/213/347/242/221.md +113 -0
  11. package/docs/05-/350/277/255/344/273/243/346/265/201/347/250/213.md +210 -0
  12. package/docs/06-/350/257/204/345/256/241/344/270/216/346/265/213/350/257/225.md +296 -0
  13. package/docs/fidelity-report.md +225 -0
  14. package/examples/fx/fx-mini/deck.yaml +39 -0
  15. package/examples/fx/fx-mini/pages/01.yaml +6 -0
  16. package/examples/fx/fx-mini/pages/02.yaml +2 -0
  17. package/examples/fx/fx-mini/pages/03.yaml +6 -0
  18. package/examples/fx/fx-pro/deck.yaml +48 -0
  19. package/examples/fx/fx-pro/pages/01_cover.yaml +40 -0
  20. package/examples/fx/fx-pro/pages/02_arch.yaml +55 -0
  21. package/examples/fx/fx-pro/pages/03_table.yaml +20 -0
  22. package/examples/fx/fx-pro/pages/04_chart.yaml +40 -0
  23. package/examples/fx/fx-pro/pages/05_timeline.yaml +71 -0
  24. package/examples/fx/fx-pro/pages/06_longtext.yaml +13 -0
  25. package/examples/fx/fx-pro/pages/07_safearea.yaml +33 -0
  26. package/examples/fx/fx-pro/pages/08_contrast.yaml +22 -0
  27. package/examples/fx/fx-pro/pages/09_custgeom.yaml +32 -0
  28. package/examples/fx/fx-pro/pages/10_numbers.yaml +25 -0
  29. package/examples/fx/fx-pro/pages/11_section.yaml +20 -0
  30. package/examples/fx/fx-pro/pages/12_summary.yaml +57 -0
  31. package/examples/fx/fx-pro/preview/01_pages_01_cover.html +1 -0
  32. package/examples/fx/fx-pro/preview/02_pages_02_arch.html +3 -0
  33. package/examples/fx/fx-pro/preview/03_pages_03_table.html +1 -0
  34. package/examples/fx/fx-pro/preview/04_pages_04_chart.html +3 -0
  35. package/examples/fx/fx-pro/preview/05_pages_05_timeline.html +2 -0
  36. package/examples/fx/fx-pro/preview/06_pages_06_longtext.html +4 -0
  37. package/examples/fx/fx-pro/preview/07_pages_07_safearea.html +2 -0
  38. package/examples/fx/fx-pro/preview/08_pages_08_contrast.html +1 -0
  39. package/examples/fx/fx-pro/preview/09_pages_09_custgeom.html +1 -0
  40. package/examples/fx/fx-pro/preview/10_pages_10_numbers.html +1 -0
  41. package/examples/fx/fx-pro/preview/11_pages_11_section.html +1 -0
  42. package/examples/fx/fx-pro/preview/12_pages_12_summary.html +1 -0
  43. package/examples/fx/fx-pro/preview/deck.html +10 -0
  44. package/examples/fx/fx-pro/preview/layout.json +2325 -0
  45. package/examples/fx/fx-pro/preview/measured.json +814 -0
  46. package/examples/fx/fx-pro.pptx +0 -0
  47. package/examples/fx/ren-single/01.png +0 -0
  48. package/examples/fx/ren-spliced/01.png +0 -0
  49. package/examples/fx/ren-spliced/03.png +0 -0
  50. package/examples/fx/seed.pptx +0 -0
  51. package/examples/fx/single.pptx +0 -0
  52. package/examples/fx/spliced.pptx +0 -0
  53. package/examples/smoke/deck.yaml +26 -0
  54. package/examples/smoke/imported/deck.yaml +30 -0
  55. package/examples/smoke/imported/import-styles.json +126 -0
  56. package/examples/smoke/imported/pages/slide_01.yaml +31 -0
  57. package/examples/smoke/imported/pages/slide_02.yaml +38 -0
  58. package/examples/smoke/imported/pages/slide_03.yaml +55 -0
  59. package/examples/smoke/imported/preview-rt/01_pages_slide_01.html +1 -0
  60. package/examples/smoke/imported/preview-rt/02_pages_slide_02.html +1 -0
  61. package/examples/smoke/imported/preview-rt/03_pages_slide_03.html +1 -0
  62. package/examples/smoke/imported/preview-rt/deck.html +1 -0
  63. package/examples/smoke/imported/preview-rt/layout.json +434 -0
  64. package/examples/smoke/imported/reference/previews/01.png +0 -0
  65. package/examples/smoke/imported/reference/previews/02.png +0 -0
  66. package/examples/smoke/imported/reference/previews/03.png +0 -0
  67. package/examples/smoke/imported/source.pptx +0 -0
  68. package/examples/smoke/media-test.png +0 -0
  69. package/examples/smoke/out-smoke.pptx +0 -0
  70. package/examples/smoke/pages/01_cover.yaml +27 -0
  71. package/examples/smoke/pages/02_content.yaml +42 -0
  72. package/examples/smoke/pages/03_chart.yaml +35 -0
  73. package/examples/smoke/preview/01_pages_01_cover.html +1 -0
  74. package/examples/smoke/preview/02_pages_02_content.html +1 -0
  75. package/examples/smoke/preview/03_pages_03_chart.html +1 -0
  76. package/examples/smoke/preview/deck.html +1 -0
  77. package/examples/smoke/preview/layout.json +378 -0
  78. package/lib/autodeclare.js +40 -0
  79. package/lib/commands.js +161 -0
  80. package/lib/crosscheck.js +300 -0
  81. package/lib/imgmeta.js +55 -0
  82. package/lib/index.js +239 -0
  83. package/lib/measurement.js +209 -0
  84. package/lib/msrender.js +123 -0
  85. package/lib/pptd/export-pptx.js +686 -0
  86. package/lib/pptd/import-pptx.js +774 -0
  87. package/lib/pptd/layout.js +209 -0
  88. package/lib/pptd/render-html.js +254 -0
  89. package/lib/pptd/schema.js +518 -0
  90. package/lib/pptd/svgCharts.js +147 -0
  91. package/lib/pptxPy.js +205 -0
  92. package/lib/preset-delivery.js +195 -0
  93. package/lib/preview-server.js +164 -0
  94. package/lib/router.js +165 -0
  95. package/lib/scaffold.js +315 -0
  96. package/lib/skill.js +174 -0
  97. package/lib/splice.js +219 -0
  98. package/lib/state.js +88 -0
  99. package/lib/surgical.js +369 -0
  100. package/lib/templates.js +400 -0
  101. package/lib/tools.js +955 -0
  102. package/lib/verify.js +828 -0
  103. package/lib/xmljs.js +112 -0
  104. package/lib/zips.js +117 -0
  105. package/package.json +81 -0
  106. package/scripts/audit-manual-facts.mjs +294 -0
  107. package/scripts/audit-template-fidelity.mjs +162 -0
  108. package/scripts/bench-m2.mjs +48 -0
  109. package/scripts/build-templates.mjs +63 -0
  110. package/scripts/build.mjs +14 -0
  111. package/scripts/build.sh +7 -0
  112. package/scripts/check-lib-freshness.mjs +70 -0
  113. package/scripts/check-preset.mjs +189 -0
  114. package/scripts/check-tracked-lf.mjs +84 -0
  115. package/scripts/dbg-import.mjs +13 -0
  116. package/scripts/dbg-media.mjs +10 -0
  117. package/scripts/dbg-tpl-inventory.mjs +23 -0
  118. package/scripts/dump-workflow-baseline.mjs +38 -0
  119. package/scripts/e2e-1.0.mjs +129 -0
  120. package/scripts/eval-skills-blind.mjs +91 -0
  121. package/scripts/eval-skills-score.mjs +129 -0
  122. package/scripts/export-orig-templates.ps1 +17 -0
  123. package/scripts/fixtures/workflow-baseline.json +300 -0
  124. package/scripts/fixtures.mjs +501 -0
  125. package/scripts/import-user-templates.mjs +60 -0
  126. package/scripts/install-desktop.mjs +312 -0
  127. package/scripts/install.mjs +209 -0
  128. package/scripts/migrate-templates-dualtrack.mjs +71 -0
  129. package/scripts/preflight-1.0.mjs +198 -0
  130. package/scripts/probe-asset-freshness.mjs +57 -0
  131. package/scripts/probe-effect-semantics.mjs +69 -0
  132. package/scripts/probe-reparse-safety.mjs +32 -0
  133. package/scripts/rebuild-templates-c11.mjs +69 -0
  134. package/scripts/regression-real.mjs +84 -0
  135. package/scripts/release-sync.mjs +425 -0
  136. package/scripts/shot-templates.mjs +49 -0
  137. package/scripts/smoke.mjs +2512 -0
  138. package/scripts/verify-bundle-install.mjs +281 -0
  139. package/scripts/verify-fresh-install.mjs +260 -0
  140. package/skills/ppt-studio-copy/SKILL.md +139 -0
  141. package/skills/ppt-studio-craft/SKILL.md +171 -0
  142. package/skills/ppt-studio-data/SKILL.md +173 -0
  143. package/skills/ppt-studio-manual/SKILL.md +104 -0
  144. package/templates/academic-white/deck.yaml +24 -0
  145. package/templates/academic-white/pages/_chart.yaml +43 -0
  146. package/templates/academic-white/pages/_compare.yaml +37 -0
  147. package/templates/academic-white/pages/_content.yaml +35 -0
  148. package/templates/academic-white/pages/_cover.yaml +28 -0
  149. package/templates/academic-white/pages/_ending.yaml +28 -0
  150. package/templates/academic-white/pages/_flow.yaml +61 -0
  151. package/templates/academic-white/preview.png +0 -0
  152. package/templates/academic-white/template.yaml +14 -0
  153. package/templates/business-blue/deck.yaml +28 -0
  154. package/templates/business-blue/pages/_chart.yaml +36 -0
  155. package/templates/business-blue/pages/_compare.yaml +45 -0
  156. package/templates/business-blue/pages/_content.yaml +80 -0
  157. package/templates/business-blue/pages/_cover.yaml +39 -0
  158. package/templates/business-blue/pages/_ending.yaml +32 -0
  159. package/templates/business-blue/pages/_flow.yaml +87 -0
  160. package/templates/business-blue/preview.png +0 -0
  161. package/templates/business-blue/template.yaml +14 -0
  162. package/templates/pitch-bold/deck.yaml +23 -0
  163. package/templates/pitch-bold/pages/_chart.yaml +32 -0
  164. package/templates/pitch-bold/pages/_compare.yaml +36 -0
  165. package/templates/pitch-bold/pages/_content.yaml +58 -0
  166. package/templates/pitch-bold/pages/_cover.yaml +31 -0
  167. package/templates/pitch-bold/pages/_ending.yaml +27 -0
  168. package/templates/pitch-bold/pages/_flow.yaml +46 -0
  169. package/templates/pitch-bold/preview.png +0 -0
  170. package/templates/pitch-bold/template.yaml +14 -0
  171. package/templates/tech-dark/deck.yaml +24 -0
  172. package/templates/tech-dark/pages/_chart.yaml +27 -0
  173. package/templates/tech-dark/pages/_compare.yaml +38 -0
  174. package/templates/tech-dark/pages/_content.yaml +51 -0
  175. package/templates/tech-dark/pages/_cover.yaml +30 -0
  176. package/templates/tech-dark/pages/_ending.yaml +25 -0
  177. package/templates/tech-dark/pages/_flow.yaml +80 -0
  178. package/templates/tech-dark/preview.png +0 -0
  179. package/templates/tech-dark/template.yaml +14 -0
@@ -0,0 +1,300 @@
1
+ /**
2
+ * 内容审阅材料包(M3 重写,2026-09-18 用户决定)——
3
+ *
4
+ * 旧版(v0.13.0 M3,2026-09-04)做两件事:跨页数字对账(同数字/百分比入组)+
5
+ * 证据核查表(页面 source → grounded / unmapped)。**2026-09-18 用户质疑后实测,该抽象层不成立**:
6
+ * ① 数字必须放回整句/整页语境才有含义——按"数字字符串相同"分组只能确认**本来就一致**的数,
7
+ * 看不见**同指标不同值**。实测:故意在第 2 页写 毛利率 32%、第 3 页写 38% → 0 命中,
8
+ * 反而报了无害的 12%;fx-pro 12 页出 6 组、0 组可行动(40 字以内 vs 40px 混义成一组、
9
+ * 版本号 v1.0.0 被拆成 "1.0" 与 "0" 两组、提示文案里的数字入组)。
10
+ * ② page.source 是作者自己写的一行自由文本,机器只判"有没有填"——它既不是证据(没有任何环节
11
+ * 去取/读/核对那份来源),也不进成品(全库唯一消费者就是这张表),grounded/unmapped 因此是
12
+ * 伪状态;且它按"页"判,封面/章节页也标 unmapped。
13
+ *
14
+ * 现职责(**只做精确、零判断的事**):把审阅者需要一次看全的材料端上桌——
15
+ * 全页正文(阅读顺序,含表格/图表数据/讲稿/作者自述出处)+ 工作区实扫的外部素材清单 +
16
+ * 审阅协议(判定四分类:一致/冲突/无来源支撑/无法核实)。
17
+ * 判断交给人/模型(有独立子代理更佳,且**用户禁止子代理时严格自审**)。
18
+ * **本文件不产生任何判定、状态、比例或阈值**,因此既不进门禁,也不可能与门禁机制冲突。
19
+ */
20
+ import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync } from 'node:fs'
21
+ import { join } from 'node:path'
22
+
23
+ /** 内联正文上限(字符):超过则内联前若干页 + 指明完整包路径(完整包永不截断)。 */
24
+ const INLINE_BODY_CAP = 24000
25
+ /** 素材扫描:可据以核对"有无依据"的文档类扩展名。 */
26
+ const DATA_EXT = /\.(pptx|docx|xlsx|xls|csv|pdf|md|txt|json|html?)$/i
27
+ /** 根目录里属于"本工具的产物"而非素材的文件(导出物不是依据)。 */
28
+ const ARTIFACT_EXT = /^(out|out-surgical|out-claude|.*-spliced|.*-single|.*-rendered)\.pptx$/i
29
+
30
+ const fmtBytes = (n) => (n >= 1024 * 1024 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`)
31
+
32
+ /** 单元格文本:markdown 表格里转义竖线、压掉换行(表格结构不能被我自己的渲染破坏)。 */
33
+ const cell = (v) => String(v ?? '').replace(/\|/g, '\\|').replace(/\s*\n\s*/g, ' ')
34
+
35
+ /**
36
+ * 工作区实扫素材清单(客观事实,不含判断)。
37
+ * 只回答一个问题:**审阅者手上到底有没有可核对的外部依据**——这决定了外部主张该判
38
+ * "无来源支撑"还是"无法核实"(旧版把这两件事混成 unmapped 是错的)。
39
+ */
40
+ export function scanMaterials(dir) {
41
+ const out = []
42
+ const push = (rel, role) => {
43
+ const abs = join(dir, rel)
44
+ if (!existsSync(abs)) return
45
+ let st
46
+ try { st = statSync(abs) } catch { return }
47
+ if (!st.isFile()) return
48
+ out.push({ path: rel, bytes: st.size, role })
49
+ }
50
+ const listDir = (rel) => {
51
+ const abs = join(dir, rel)
52
+ if (!existsSync(abs)) return []
53
+ try { return readdirSync(abs).sort() } catch { return [] }
54
+ }
55
+ // 双轨真相层(ppt_import / 模板工作区自带)
56
+ push('reference/source.pptx', '导入原稿(内容真相层)')
57
+ push('source.pptx', '导入原稿副本')
58
+ push('reference/template.pptx', '模板真相层')
59
+ push('reference/audit.yaml', '模板风格审计缓存')
60
+ for (const f of listDir(join('reference'))) {
61
+ if (f === 'source.pptx' || f === 'template.pptx' || f === 'audit.yaml') continue
62
+ push(join('reference', f), 'reference/ 其他文件')
63
+ }
64
+ for (const f of listDir(join('reference', 'previews'))) push(join('reference', 'previews', f), '原稿真渲染整页(需读图能力)')
65
+ // 用户素材(约定目录名)
66
+ for (const sub of ['materials', 'sources', '素材', 'docs']) {
67
+ for (const f of listDir(sub)) push(join(sub, f), '用户素材')
68
+ }
69
+ // 工程根目录的文档类文件(素材常常直接扔在根上)
70
+ for (const f of listDir('.')) {
71
+ if (!DATA_EXT.test(f) || ARTIFACT_EXT.test(f)) continue
72
+ push(f, '工程内文件')
73
+ }
74
+ // media/:图片素材,按目录聚合(不是数据依据,标注清楚免得审阅者误用)
75
+ const mediaFiles = listDir('media')
76
+ if (mediaFiles.length) {
77
+ let total = 0
78
+ for (const f of mediaFiles) { try { total += statSync(join(dir, 'media', f)).size } catch { /* 忽略单个文件错误 */ } }
79
+ out.push({ path: 'media/', bytes: total, role: `页面图片素材 ×${mediaFiles.length}(媒体,非数据依据)` })
80
+ }
81
+ return out
82
+ }
83
+
84
+ /**
85
+ * 汇总材料包数据(**纯函数、无副作用、无判定字段**)。
86
+ * 返回 { dir, title, size, pages: [...], materials: [...] }。
87
+ */
88
+ export function crosscheckDeck(ctx) {
89
+ const dir = ctx.dir
90
+ const pages = (ctx.pages ?? []).map((p, i) => {
91
+ const page = p.page ?? {}
92
+ const els = Array.isArray(page.elements) ? page.elements : []
93
+ const texts = []
94
+ const tables = []
95
+ const charts = []
96
+ const images = []
97
+ for (const e of els) {
98
+ const id = e.elementId ?? '(无 id)'
99
+ const type = e.elementType ?? e.kind
100
+ if (type === 'text') {
101
+ const t = String(e.content?.text ?? e.text ?? '')
102
+ if (t) texts.push({ id, text: t })
103
+ } else if (type === 'table') {
104
+ tables.push({
105
+ id,
106
+ cols: (e.cols ?? []).map(String),
107
+ rows: (e.rows ?? []).map((r) => (Array.isArray(r) ? r : []).map(String)),
108
+ })
109
+ } else if (type === 'chart') {
110
+ const c = e.chart ?? {}
111
+ charts.push({
112
+ id,
113
+ type: String(c.type ?? '?'),
114
+ cols: (c.data?.cols ?? []).map(String),
115
+ rows: (c.data?.rows ?? []).map((r) => (Array.isArray(r) ? r : []).map(String)),
116
+ })
117
+ } else if (type === 'image') {
118
+ images.push({ id, src: String(e.src ?? e.content?.src ?? ''), fit: String(e.fit ?? e.content?.fit ?? '') })
119
+ }
120
+ }
121
+ return {
122
+ index: i + 1,
123
+ ref: p.ref ?? '',
124
+ pageType: String(page.pageType ?? '(未写)'),
125
+ title: page.title ? String(page.title) : null,
126
+ notes: page.notes ? String(page.notes) : '',
127
+ // 作者自述出处:**未经核实**,只当审阅线索(旧版据此判 grounded/unmapped 已废除)
128
+ authorSource: page.source ? String(page.source) : null,
129
+ texts,
130
+ tables,
131
+ charts,
132
+ images,
133
+ }
134
+ })
135
+ return {
136
+ dir,
137
+ title: String(ctx.deck?.title ?? ''),
138
+ size: ctx.size ?? null,
139
+ pages,
140
+ materials: scanMaterials(dir),
141
+ }
142
+ }
143
+
144
+ function renderMeta(d) {
145
+ const wh = d.size ? `${d.size.width}×${d.size.height}px` : '(未知尺寸)'
146
+ const lines = [
147
+ '# 内容审阅材料包',
148
+ '',
149
+ `- 工程:**${d.title || '(无标题)'}**|页面 ${wh}|共 **${d.pages.length} 页**`,
150
+ '- 生成:`ppt_crosscheck`(**只汇总材料,不做任何判定**——旧版"跨页数字分组 + grounded/unmapped 状态"已于 2026-09-18 废除:数字必须放回整句语境才有含义,来源标注是作者自述而非证据)',
151
+ '- 用途:把判定所需材料一次看全。**判定在下文第三节的审阅协议里产生,由审阅者给出**。',
152
+ '- 下一步:按协议执行内容审阅——**默认独立子代理**(全新上下文,只给本包与素材;用户明确禁止子代理时改自审并标注)。本工具不构成门禁。',
153
+ '',
154
+ ]
155
+ return lines.join('\n')
156
+ }
157
+
158
+ function renderMaterials(d) {
159
+ const lines = ['## 二、可核对的外部素材(工作区实扫,客观清单)', '']
160
+ if (!d.materials.length) {
161
+ lines.push('**未发现任何外部素材。**', '')
162
+ lines.push('→ 由此决定判定口径:涉及外部事实/数字的主张本次**只能判「无法核实」**——不得判「一致」(没有依据可依),也不得判「无来源支撑」(那是猜,不是核实)。')
163
+ return lines.join('\n')
164
+ }
165
+ lines.push('| 文件 | 大小 | 用途 |', '|---|---|---|')
166
+ for (const m of d.materials) lines.push(`| \`${m.path}\` | ${fmtBytes(m.bytes)} | ${m.role} |`)
167
+ lines.push('')
168
+ const checkable = d.materials.filter((m) => /导入原稿|模板真相层|用户素材|工程内文件/.test(m.role))
169
+ const visualOnly = d.materials.filter((m) => /真渲染整页|图片素材/.test(m.role))
170
+ lines.push(checkable.length
171
+ ? `可据以判定"有无依据"的素材:**${checkable.length} 项**(原稿/文档类)。审阅者应当实际打开它们比对,而不是凭印象。`
172
+ : `**只有图片/预览类素材(${visualOnly.length} 项)**:图片里的数字需读图能力才能核对;读不到就判「无法核实」,不要猜。`)
173
+ return lines.join('\n')
174
+ }
175
+
176
+ function renderPageSection(p) {
177
+ const lines = [`### 第 ${p.index} 页 · \`${p.ref}\``, '']
178
+ const head = [`类型 \`${p.pageType}\``]
179
+ if (p.title) head.push(`标题 \`${p.title}\``)
180
+ lines.push(`- ${head.join('|')}`)
181
+ if (p.authorSource) lines.push(`- 作者自述出处(**未核实**,仅线索):\`${p.authorSource}\``)
182
+ if (!p.texts.length && !p.tables.length && !p.charts.length && !p.images.length) {
183
+ lines.push('- (本页无文字/表格/图表/图片元素)')
184
+ }
185
+ for (const t of p.texts) {
186
+ if (t.text.includes('\n')) {
187
+ lines.push(`- 文本 \`${t.id}\`:`)
188
+ for (const l of t.text.split('\n')) lines.push(` > ${l}`)
189
+ } else {
190
+ lines.push(`- 文本 \`${t.id}\`:${t.text}`)
191
+ }
192
+ }
193
+ for (const t of p.tables) {
194
+ lines.push(`- 表格 \`${t.id}\`(${t.cols.length} 列 × ${t.rows.length} 行):`)
195
+ lines.push(` | ${t.cols.map(cell).join(' | ')} |`)
196
+ lines.push(` |${' --- |'.repeat(Math.max(1, t.cols.length))}`)
197
+ for (const r of t.rows) lines.push(` | ${r.map(cell).join(' | ')} |`)
198
+ }
199
+ for (const c of p.charts) {
200
+ lines.push(`- 图表 \`${c.id}\`:${c.type}|列 \`${c.cols.join('、')}\``)
201
+ for (const r of c.rows) lines.push(` - ${r.map(cell).join('|')}`)
202
+ }
203
+ for (const im of p.images) lines.push(`- 图片 \`${im.id}\`:\`${im.src}\`${im.fit ? `(fit ${im.fit})` : ''}`)
204
+ if (p.notes) {
205
+ lines.push('- 讲稿(notes,会导出为备注页):')
206
+ for (const l of p.notes.split('\n')) lines.push(` > ${l}`)
207
+ }
208
+ lines.push('')
209
+ return lines.join('\n')
210
+ }
211
+
212
+ /**
213
+ * 审阅协议:**工具本身不判定**,判定标准与方法在这里写给审阅者(人 或 模型/子代理)。
214
+ * 注意:本节内不出现反引号,避免与模板字符串转义纠缠(纯文本,读起来等价)。
215
+ */
216
+ const PROTOCOL = [
217
+ '## 四、审阅协议(给审阅者:判定在这里产生,本工具不判定)',
218
+ '',
219
+ '### 目标(三件事)',
220
+ '1. **内容前后一致**:同一事实、指标、名称、时间范围、口径在不同页之间不得互相矛盾。',
221
+ '2. **数据不冲突**:同一指标在同一口径/同一期间下,不得在不同页出现不同值。',
222
+ '3. **无编造/曲解**:具体数字与事实须能在上面素材中找到依据;结论不得强于所引数据(不得把相关性说成因果、不得丢掉限定条件、不得断章取义、不得把目标写成已完成)。',
223
+ '',
224
+ '### 审阅方式(按优先级,且必须服从用户指令)',
225
+ '1. **用户明确要求"不要使用子代理"(或同义要求)→ 自审**:在同一上下文里逐条核对,报告开头必须写「自审(同上下文,可信度较低)」。**此时不得改用子代理。**',
226
+ '2. 用户未禁止、且当前会话有独立子代理能力(如 subagent 工具)→ **优先用独立子代理审阅**:全新上下文、只给本材料包(preview/review-pack.md)与素材文件、**不给**作者的推理过程或结论;报告标注「独立子代理审阅」。',
227
+ '3. 两者都没有 → 自审,并同样标注。',
228
+ '无论哪种方式:**审阅必须真的发生**——不得跳过,也不得只回一句"已核查、无问题"。',
229
+ '',
230
+ '### 判定四分类(每条只能取其一,且必须给页码 + 原句)',
231
+ '- **一致**:与素材或其他页一致。',
232
+ '- **冲突**:同一指标/事实在不同页出现不同值或相反表述——必须同时列出两处原文。',
233
+ '- **无来源支撑**:**仅当**上面确实列有可核对素材、且在其中找不到依据时使用(疑似编造或曲解)。',
234
+ '- **无法核实**:未提供素材,或素材未覆盖该主张——**不得因为"没有素材"就判「一致」,也不得判「无来源支撑」**。',
235
+ '',
236
+ '### 必须逐条回答(避免空转式结论)',
237
+ '1. 列出正文中全部具体数字/百分比与页码,逐条给判定(并说明单位与口径是否写清)。',
238
+ '2. 指出同一指标在多页出现的位置,判定值/单位/时间范围是否一致。',
239
+ '3. 列出无法在素材中找到依据的具体主张(有素材时)。',
240
+ '4. 列出结论强于数据的表述(如"显著领先/第一/翻倍"而没有支撑数据)。',
241
+ '5. 声明本次未覆盖的范围(素材缺失、图片无法读、正文被截断等)——**不得把未覆盖说成已通过**。',
242
+ '',
243
+ '### 输出格式与纪律',
244
+ '- markdown 表:`主张/数字 | 页码 | 判定 | 依据(原句或素材出处)`,其后给「必须修改项」与「建议项」。',
245
+ '- 审阅者**不得修改任何文件**;发现问题只报告,不改写引文来凑判定。',
246
+ '- 边界:本协议只管**语义一致性**;版式/溢出/重叠/出界由 `ppt_verify` 等门禁负责,不要混。',
247
+ '- 什么时候跑:有具体数字/事实主张的稿子,交付前跑一次;纯装饰稿可跳过(省成本)。',
248
+ '',
249
+ ].join('\n')
250
+
251
+ /**
252
+ * 工具输出(markdown):完整包落盘 `preview/review-pack.md`,内联部分超上限时明确声明未内联的页
253
+ * (**绝不静默截断**——审阅者必须知道自己没看到什么)。
254
+ */
255
+ export function crosscheckReport(ctx) {
256
+ const d = crosscheckDeck(ctx)
257
+ const meta = renderMeta(d)
258
+ const materials = renderMaterials(d)
259
+ const bodyHead = `## 三、正文(阅读顺序,共 ${d.pages.length} 页)\n`
260
+ const secs = d.pages.map(renderPageSection)
261
+ const fullBody = bodyHead + '\n' + secs.join('\n')
262
+ const pack = `${meta}\n${materials}\n\n${fullBody.trimEnd()}\n\n${PROTOCOL}`
263
+
264
+ let saved = null
265
+ let saveErr = null
266
+ try {
267
+ mkdirSync(join(d.dir, 'preview'), { recursive: true })
268
+ writeFileSync(join(d.dir, 'preview', 'review-pack.md'), pack, 'utf8')
269
+ saved = 'preview/review-pack.md'
270
+ } catch (e) { saveErr = String(e?.message ?? e) }
271
+
272
+ let inlineBody = fullBody
273
+ let trunc = null
274
+ if (saved && fullBody.length > INLINE_BODY_CAP) {
275
+ let acc = ''
276
+ let kept = 0
277
+ for (const s of secs) {
278
+ if (acc.length + s.length > INLINE_BODY_CAP) break
279
+ acc += s
280
+ kept += 1
281
+ }
282
+ const omitted = d.pages.slice(kept).map((p) => p.index)
283
+ trunc = [
284
+ `> ⚠ 正文 ${fullBody.length} 字符 > 内联上限 ${INLINE_BODY_CAP}:**以下只内联前 ${kept} 页**(第 ${omitted.join('、')} 页未内联)。`,
285
+ `> 审阅者**必须**读完整包 \`${saved}\`(含全部 ${d.pages.length} 页);未读全不得判"全册一致",并须在第 5 条里声明未覆盖页。`,
286
+ '',
287
+ ].join('\n')
288
+ inlineBody = bodyHead + '\n' + trunc + '\n' + acc
289
+ }
290
+ if (!saved) {
291
+ inlineBody = `${fullBody}\n> ⚠ 材料包未能落盘(${saveErr ?? '未知原因'})——以上为完整内联,请直接据本文审阅。\n`
292
+ }
293
+
294
+ const pointer = saved
295
+ ? `- 完整包已落盘:\`${join(d.dir, 'preview', 'review-pack.md')}\`(同一内容;子代理审阅请让它直接读该文件)`
296
+ : ''
297
+ const head = [meta.trimEnd(), pointer].filter(Boolean).join('\n')
298
+ // 分隔线前必须留空行:正文最后一行是列表项,紧贴 `---` 会被 markdown 当成 setext 标题下划线
299
+ return `${head}\n\n---\n\n${materials}\n\n---\n\n${inlineBody.trimEnd()}\n\n---\n\n${PROTOCOL}`
300
+ }
package/lib/imgmeta.js ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 图片元数据:从文件头读宽高/格式(无需视觉模型;任何环境可用)。
3
+ * 支持 PNG / JPEG / GIF / WebP。
4
+ */
5
+
6
+ export function imageInfo(file) {
7
+ const head = file.subarray(0, 64)
8
+ if (head.length >= 24 && head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4e && head[3] === 0x47) {
9
+ return { format: 'png', width: head.readUInt32BE(16), height: head.readUInt32BE(20) }
10
+ }
11
+ if (head.length >= 10 && head[0] === 0xff && head[1] === 0xd8) {
12
+ return jpegSize(file)
13
+ }
14
+ if (head.length >= 10 && head[0] === 0x47 && head[1] === 0x49 && head[2] === 0x46) {
15
+ return { format: 'gif', width: head.readUInt16LE(6), height: head.readUInt16LE(8) }
16
+ }
17
+ if (head.length >= 30 && head[0] === 0x52 && head[1] === 0x49 && head[2] === 0x46 && head[3] === 0x46) {
18
+ if (head.subarray(8, 12).toString() === 'WEBP') return webpSize(file)
19
+ }
20
+ return { format: 'unknown', width: 0, height: 0 }
21
+ }
22
+
23
+ function jpegSize(file) {
24
+ let p = 2
25
+ while (p + 9 < file.length) {
26
+ if (file[p] !== 0xff) { p++; continue }
27
+ const marker = file[p + 1]
28
+ if (marker >= 0xc0 && marker <= 0xc3 || marker >= 0xc5 && marker <= 0xc7 || marker >= 0xc9 && marker <= 0xcb || marker >= 0xcd && marker <= 0xcf) {
29
+ return { format: 'jpeg', width: file.readUInt16BE(p + 7), height: file.readUInt16BE(p + 5) }
30
+ }
31
+ const len = file.readUInt16BE(p + 2)
32
+ p += 2 + len
33
+ }
34
+ return { format: 'jpeg', width: 0, height: 0 }
35
+ }
36
+
37
+ function webpSize(file) {
38
+ const kind = file.subarray(12, 16).toString()
39
+ if (kind === 'VP8 ') {
40
+ return { format: 'webp', width: file.readUInt16LE(26) & 0x3fff, height: file.readUInt16LE(28) & 0x3fff }
41
+ }
42
+ if (kind === 'VP8L') {
43
+ const b = file.subarray(20, 25)
44
+ const bits = b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24) | (b[4] << 32)
45
+ return { format: 'webp', width: (bits & 0x3fff) + 1, height: ((bits >>> 14) & 0x3fff) + 1 }
46
+ }
47
+ if (kind === 'VP8X') {
48
+ return {
49
+ format: 'webp',
50
+ width: 1 + (file[24] | (file[25] << 8) | (file[26] << 16)),
51
+ height: 1 + (file[27] | (file[28] << 8) | (file[29] << 16)),
52
+ }
53
+ }
54
+ return { format: 'webp', width: 0, height: 0 }
55
+ }
package/lib/index.js ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * dsh-ppt-studio —— PPT 工作室插件(host)。
3
+ *
4
+ * 装配模型(2026-09-24 **第三次修订:适配 DSH 0.1.7-rc.1**):
5
+ * `apply()` 做四件事——① 装配防重;② **全局管道**(/ppt-preview 路由、预设**声明**、语义路由);
6
+ * ③ 工作流提示段注入(`system-prompt/assemble`);④ **在本 ctx 注册全部能力面**:
7
+ * 22 个 `ppt_*` 工具 / `/ppt` 命令面 / `ppt_state` / 4 本内嵌技能。
8
+ * ⇒ **装上插件,所有会话都能用**(= 1.0.0 的行为)。
9
+ *
10
+ * 0.1.7 的那一处真适配是**预设交付**:宿主不再扫描 `<dshHome>/.agent-presets/`(上游原文
11
+ * "the harness discovers no preset on disk"),预设改为向 `agentPresets` 注册表**声明**。
12
+ * 详见 src/preset-delivery.js 顶部(含 `!!js` 保真与相对名解析这两条源码级细节)。
13
+ *
14
+ * 为什么放弃了"只让「PPT 工作室」预设看到"(两件事,都有实测与源码依据):
15
+ * ① **切换预设拿不到**:空白会话切预设时 `agent-presets.swap` 确实会 `recompose(agent.ctx, id)`,
16
+ * 但那一刻该 agent **已经存在**,而我们的 `agent/created` 监听者是在这次组合里才注册的
17
+ * ⇒ 它永远不会为这个已有 agent 触发 ⇒ "先建会话再切到该预设"永远看不到工具
18
+ * (用户实测:只有一开始就建在该预设上才有工具)。
19
+ * ② **技能在父层读不到**:技能注册表分层,技能工具在**预设层**读;我们只能注册到 `agent.ctx`(子层)
20
+ * 或 profile 根 ⇒ 预设层读不到 ⇒ 技能永不出现。而"用预设行挂载"这条路同样封死
21
+ * (行里的裸包名按 **harness base** 解析,不是 profile ⇒ 预设被判 `broken` ⇒ 选择器不显示它)。
22
+ * ⇒ 在这版 DSH 上,"按预设隔离"无法可靠交付。宁可**功能完整、行为可预期**,也不要"看起来隔离、
23
+ * 实际一半会话没能力面"。隔离代码留在 git 历史(`4ea7037` 起的提交)里,等 DSH 提供稳定的
24
+ * "预设已组合/已切换"信号再恢复。
25
+ *
26
+ * 「PPT 工作室」预设仍然存在并自交付:它负责**身份/人格**(名字、简介、standard 能力面副本)。
27
+ */
28
+ import { registerTools, defineTool } from './tools.js'
29
+ import { registerCommands } from './commands.js'
30
+ import { loadSession, saveSession } from './state.js'
31
+ import { isPptIntent, isPptOff, isQuickIntent, detectTaskType, workflowSection } from './router.js'
32
+ import { registerPreviewRoute } from './preview-server.js'
33
+ import { registerManualSkill, manualSkillStatus } from './skill.js'
34
+ import { declareAgentPreset, presetDeliveryStatus } from './preset-delivery.js'
35
+ import { existsSync, statSync, mkdirSync, appendFileSync, readFileSync, writeFileSync } from 'node:fs'
36
+ import { join } from 'node:path'
37
+
38
+ export const name = 'dsh-ppt-studio'
39
+ // skills 为可选依赖(ctx.get('skills')):极简装配缺 dsh-skill 时插件仍完整可用
40
+ export const inject = ['tools', 'commands', 'systemPrompt']
41
+
42
+ function loggerOf(ctx) {
43
+ try { return typeof ctx.logger === 'function' ? ctx.logger('ppt-studio') : ctx.logger } catch { return undefined }
44
+ }
45
+
46
+ /**
47
+ * 按需诊断日志(**默认关闭**):`PPT_STUDIO_DEBUG=1`,或存在 `<dshHome>/ppt-studio/debug.on` 时启用。
48
+ *
49
+ * 为什么需要它(2026-09-18 的真实排查困境):本插件的"不生效"有三种完全不同的原因——
50
+ * ① 插件没被加载(装法/装配问题);② 加载了,但门控判成"这个 agent 不属于本插件";
51
+ * ③ 门控放行,但能力面挂到 agent 作用域时失败。
52
+ * 三者在 UI 上**长得一模一样**(都看不到工具),而会话里一个工具都没有时**什么都问不了**
53
+ * (连诊断工具本身都没挂上)——所以诊断必须写到**文件**,不依赖"模型肯不肯说"。
54
+ * 每次判定一行;超过 256KB 时截断保留尾部。
55
+ */
56
+ function diag(event, detail = '') {
57
+ try {
58
+ const home = process.env.DSH_HOME || join(process.env.USERPROFILE || process.env.HOME || '', '.dsh')
59
+ const dir = join(home, 'ppt-studio')
60
+ const on = process.env.PPT_STUDIO_DEBUG === '1' || existsSync(join(dir, 'debug.on'))
61
+ if (!on) return
62
+ mkdirSync(dir, { recursive: true })
63
+ const file = join(dir, 'debug.log')
64
+ try { if (existsSync(file) && statSync(file).size > 256 * 1024) {
65
+ const buf = readFileSync(file); writeFileSync(file, buf.subarray(buf.length - 128 * 1024))
66
+ } } catch { /* 忽略 */ }
67
+ appendFileSync(file, `${new Date().toISOString()} ${event} ${detail}\n`, 'utf8')
68
+ } catch { /* 诊断永不抛 */ }
69
+ }
70
+
71
+ export function apply(ctx, config = {}) {
72
+ diag('apply', `baseUrl=${String(ctx?.baseUrl ?? '(无)').slice(0, 90)}`
73
+ + `|agentPresets=${(() => { try { return ctx.get('agentPresets') ? '可见' : '不可见' } catch { return '取用抛错' } })()}`
74
+ + `|presetIds=${JSON.stringify(config?.presetIds ?? ['ppt'])}|pid=${process.pid}`)
75
+ // ── 装配防重(2026-09-14 新增 profile bundle 安装路径后必需)────────────────────────
76
+ // 同一个包可能在**同一进程**里被挂两次:
77
+ // ① profile bundle 行(包的 cordis.patch.yml,`dsh plugin add` 装完即挂);
78
+ // ② 预设内的插件行(`--isolate` 模式把本包挂进「PPT 工作室」预设的组合里)。
79
+ // 首个挂载真正装配,后续只计数(注册仍归各自 ctx.effect 所有,owner ctx 卸载即释放)。
80
+ // 第二条在本版本上更重要:预设声明会随第二次装配**重复注册**,而注册表对重复 id 直接
81
+ // throw(`Duplicate agent preset`)——防重同时挡住了这个双挂载事故。
82
+ const CORE = globalThis.__pptCoreReg ?? { count: 0 }
83
+ globalThis.__pptCoreReg = CORE
84
+ if (CORE.count > 0) {
85
+ CORE.count++
86
+ try {
87
+ loggerOf(ctx)?.warn?.('[ppt-studio] 本包在同一进程被挂载了两次(profile bundle 行 + 预设内的插件行?)——'
88
+ + '已按"首个生效"跳过本次装配。两者是替代关系,建议二选一:`dsh plugin --profile <p> remove <包>` 或改用 `--isolate` 安装(见 README §0.7)。')
89
+ } catch { /* 无 logger 时静默 */ }
90
+ ctx.effect(() => () => { CORE.count-- }, 'ppt-studio: duplicate mount (ref)')
91
+ return
92
+ }
93
+ CORE.count++
94
+ // 注意 Cordis 语义:`ctx.effect(cb)` **立即执行 cb**,并用 cb 的**返回值**当 disposer。
95
+ // 所以计数递减必须写成"返回一个函数"(写成 `() => { count-- }` 会当场递减,等于没防重——
96
+ // 这个错误由 smoke 的行为断言当场抓到)。
97
+ ctx.effect(() => () => { CORE.count-- }, 'ppt-studio: core mount (ref)')
98
+
99
+ // ── 全局管道(与会话无关)──────────────────────────────────────────────────────
100
+ registerPreviewRoute(ctx) // /ppt-preview/ 路由(无 webServer 环境自动跳过)
101
+ // 声明「PPT 工作室」预设(0.1.7 起:注册表声明,不再写预设目录——目录已无发现路径)。
102
+ // 失败只告警不抛;状态经 ppt_state 的 presetDelivery 暴露,避免"预设没出现"只能翻日志。
103
+ declareAgentPreset(ctx, config)
104
+
105
+ // 语义路由:user/message → 激活/退出(auto 模式)。只写会话状态文件,不暴露任何能力面。
106
+ ctx.on('session/event', async (session, event) => {
107
+ if (event.type !== 'user/message') return
108
+ const data = event.data ?? {}
109
+ if (data.source?.kind !== 'user' && data.source?.kind !== undefined) return
110
+ const text = extractText(data)
111
+ const attachments = data.attachments ?? []
112
+ try {
113
+ const state = await loadSession(session.id)
114
+ if (state.routing === 'off') return
115
+ if (state.routing === 'on') { state.workflowActive = true; await saveSession(session.id, state); return }
116
+ // auto:语义判断
117
+ if (!state.workflowActive && isPptIntent(text, attachments)) {
118
+ state.workflowActive = true
119
+ state.taskType = detectTaskType(text)
120
+ if (isQuickIntent(text)) state.quick = true
121
+ await saveSession(session.id, state)
122
+ } else if (state.workflowActive && isPptOff(text) && !attachments.some((a) => /\.pptx?$/i.test(a.name ?? a.path ?? ''))) {
123
+ state.workflowActive = false
124
+ await saveSession(session.id, state)
125
+ } else if (state.workflowActive && isQuickIntent(text, true)) {
126
+ state.quick = true
127
+ await saveSession(session.id, state)
128
+ }
129
+ } catch { /* 防缺失目录等异常 */ }
130
+ })
131
+
132
+ // 工作流提示词注入(**不按预设门控**——见下"回滚"说明)
133
+ ctx.on('system-prompt/assemble', async (assembly, context, next) => {
134
+ const assembled = await next()
135
+ const agent = context.agent
136
+ if (!agent?.session?.id) return assembled
137
+ const sessionId = agent.session.id
138
+ let state
139
+ try { state = await loadSession(sessionId) } catch { return assembled }
140
+ if (!state.workflowActive) return assembled
141
+ const sections = [...(assembled.sections ?? [])]
142
+ const idx = sections.findIndex((s) => s?.name === 'ppt-workflow')
143
+ if (idx >= 0) sections.splice(idx, 1)
144
+ sections.push(workflowSection(state.taskType ?? 'unknown', state))
145
+ return { ...assembled, sections }
146
+ })
147
+
148
+ // ── 能力面:注册在**本 ctx**(= 装它的那一层)——2026-09-18 第二轮回滚 ──────────────────
149
+ // 背景(用户实测 + DSH 源码实锤,两件事一起推翻了"按预设隔离"这条路):
150
+ // ① **切换预设拿不到能力面**:空白会话切预设(`agent-presets.swap`)确实会
151
+ // `recompose(agent.ctx, id)` 重新组合,但那一刻**该 agent 已经存在**——我们的
152
+ // `agent/created` 监听者是在这次组合中才注册的,**永远不会**为这个已有 agent 触发。
153
+ // 于是"先建会话再切过去"= 工具永远不出现(用户原话:只有一开始就是该预设才有工具)。
154
+ // ② **技能在预设层读不到**:注册表是**分层**的(`dsh-skill` 的 preset 层),而技能工具
155
+ // (`tool-skill`)在**预设层**读;我们注册进 `agent.ctx`(子层)⇒ 父层读不到 ⇒ 技能永远不出现。
156
+ // 在 profile bundle 装法下我们的 ctx 就是 profile 根,**够不到预设层**,
157
+ // 而预设行又无法解析本包(行按 harness base 解析,见 preset 模板末尾说明)⇒ 这条路封死。
158
+ // 结论:本 DSH 版本上"只让某个预设看到工具/技能"**无法可靠交付**。按用户指示回滚到
159
+ // 「装上插件 → 所有会话都能用」这一久经验证的行为(= 1.0.0 的行为),并把预设保留为**身份/人格**。
160
+ const faces = []
161
+ const step = (label, fn) => { try { fn() } catch (e) { faces.push(`${label}: ${String(e?.message ?? e)}`) } }
162
+ step('tools', () => registerTools(ctx))
163
+ step('commands', () => registerCommands(ctx))
164
+ step('ppt_state', () => statusToolFor(ctx))
165
+ step('skills', () => registerManualSkill(ctx))
166
+ if (faces.length) loggerOf(ctx)?.warn?.(`[ppt-studio] 部分能力面注册失败:${faces.join(';')}`)
167
+ diag('faces', `tools/commands/ppt_state/skills 已注册在 apply ctx(回滚为全局可见)|失败=${faces.length ? faces.join(';') : '无'}`)
168
+ }
169
+
170
+ /**
171
+ * 【已删除】`mountForAgent` / `agentIsOurs` / `computeOurs`(2026-09-18 第二次修订回滚)
172
+ *
173
+ * 它们实现了"只让「PPT 工作室」预设的会话看到工具/技能",但在这版 DSH 上**无法可靠交付**:
174
+ * ① 空白会话**切换**预设时,`agent-presets.swap` 会 `recompose(agent.ctx, id)`,可那一刻该 agent
175
+ * 已经存在——我们的 `agent/created` 监听者是这次组合才注册的,**不会**为它触发 ⇒ 工具永不出现
176
+ * (用户实测:只有一开始就是该预设才有工具);
177
+ * ② 技能注册表是**分层**的,技能工具在**预设层**读,而我们只能注册到 `agent.ctx`(子层)或
178
+ * profile 根 ⇒ 父层读不到 ⇒ 技能永不出现;而"预设行挂载"这条路又被
179
+ * "行按 harness base 解析"封死(见 agent-presets/ppt/agent.cordis.yml 末尾说明)。
180
+ * 代码留在 git 历史里(提交 4ea7037 起)。
181
+ *
182
+ * 【2026-09-24 补注(0.1.7-rc.1)】当时封死"预设行挂载"的那条判据**依然成立**:注册表 mount 预设时
183
+ * 用 `scope.ctx.extend({ baseUrl: <注册表所在 ctx 的 baseUrl> })`(= harness base),裸包名
184
+ * `dsh-ppt-studio` 在那里解析不到 ⇒ 预设会被判 broken。所以现在的 `--isolate` 走的是
185
+ * 另一条路:**不挂 profile bundle**,把插件行写进预设声明的 `plugins` 里,并给插件行
186
+ * `autoPreset:false`(预设已由该声明本身提供,插件不能再声明一次)。本注释里的
187
+ * `mountForAgent` 是"插件自己在运行时按 agent 门控"那套,与上面这条不同,仍然不恢复。
188
+ */
189
+
190
+ function extractText(data) {
191
+ const c = data.content
192
+ if (typeof c === 'string') return c
193
+ if (Array.isArray(c)) return c.filter((b) => b.type === 'text').map((b) => b.text ?? '').join(' ')
194
+ return ''
195
+ }
196
+
197
+ /** 调试工具(agent 可见):当前工作流状态。 */
198
+ export function statusToolFor(ctx) {
199
+ return ctx.effect(() => ctx.tools.register(defineTool({
200
+ name: 'ppt_state',
201
+ description: '查看/更新 PPT 工作流会话状态(档位/激活)。debug/自优化用',
202
+ parameters: {
203
+ set: { type: 'string', description: '可选 key=value(如 mode=strict workflowActive=true)' },
204
+ },
205
+ output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: String(v) }] },
206
+ async execute(args, exec) {
207
+ // 会话 id 取自工具执行的 agent(DSH 的 execute(args, exec) 第二参数);
208
+ // 历史坑:ctx.get('agent') 恒为 undefined(DSH 只有 agents 服务),会误落到 'default' 会话文件。
209
+ const agent = exec?.agent
210
+ const sid = agent?.session?.id ?? agent?.id ?? 'default'
211
+ const state = await loadSession(sid)
212
+ if (args.set) {
213
+ for (const kv of String(args.set).split(/\s+/)) {
214
+ const [k, ...rest] = kv.split('=')
215
+ const v = rest.join('=')
216
+ if (k && v !== undefined) state[k] = v === 'true' ? true : v === 'false' ? false : v === 'null' ? null : v
217
+ }
218
+ await saveSession(sid, state)
219
+ }
220
+ // 交付通道自检:内嵌技能在技能注册表里是否可见(自包含诊断,不依赖外部工具)
221
+ const delivery = manualSkillStatus()
222
+ try {
223
+ const skills = ctx.get('skills')
224
+ if (skills === undefined) delivery.visible = 'skills 服务缺失'
225
+ else {
226
+ const rows = []
227
+ for (const s of delivery.skills) {
228
+ const one = await skills.get(s.name)
229
+ rows.push(`${s.name}=${one === undefined ? 'MISSING' : `${String(one.provider)}/${String(one.source)}/len=${String(one.content).length}`}`)
230
+ }
231
+ delivery.visible = rows.length ? rows.join(' · ') : 'MISSING'
232
+ }
233
+ } catch (error) {
234
+ delivery.visible = `查询失败:${error?.message ?? error}`
235
+ }
236
+ return JSON.stringify({ session: sid, state, manualSkill: delivery, presetDelivery: presetDeliveryStatus() }, null, 2)
237
+ },
238
+ })), 'ppt-studio: ppt_state')
239
+ }