dsh-thesis 1.0.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 (188) hide show
  1. package/DESIGN.md +390 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +10 -0
  4. package/README.md +253 -0
  5. package/cordis.patch.yml +17 -0
  6. package/docs/host-api.md +640 -0
  7. package/lib/codewalk/commands.d.ts +23 -0
  8. package/lib/codewalk/commands.js +168 -0
  9. package/lib/codewalk/engine.d.ts +108 -0
  10. package/lib/codewalk/engine.js +449 -0
  11. package/lib/codewalk/errors.d.ts +31 -0
  12. package/lib/codewalk/errors.js +20 -0
  13. package/lib/codewalk/fs-types.d.ts +34 -0
  14. package/lib/codewalk/fs-types.js +10 -0
  15. package/lib/codewalk/gate.d.ts +30 -0
  16. package/lib/codewalk/gate.js +60 -0
  17. package/lib/codewalk/host-types.d.ts +198 -0
  18. package/lib/codewalk/host-types.js +11 -0
  19. package/lib/codewalk/prestep.d.ts +17 -0
  20. package/lib/codewalk/prestep.js +62 -0
  21. package/lib/codewalk/register.d.ts +23 -0
  22. package/lib/codewalk/register.js +31 -0
  23. package/lib/codewalk/skill.d.ts +15 -0
  24. package/lib/codewalk/skill.js +57 -0
  25. package/lib/codewalk/tools.d.ts +18 -0
  26. package/lib/codewalk/tools.js +314 -0
  27. package/lib/codewalk/types.d.ts +151 -0
  28. package/lib/codewalk/types.js +9 -0
  29. package/lib/commands.d.ts +33 -0
  30. package/lib/commands.js +60 -0
  31. package/lib/config.d.ts +125 -0
  32. package/lib/config.js +139 -0
  33. package/lib/dedup/commands.d.ts +21 -0
  34. package/lib/dedup/commands.js +93 -0
  35. package/lib/dedup/index.d.ts +75 -0
  36. package/lib/dedup/index.js +331 -0
  37. package/lib/dedup/normalize.d.ts +125 -0
  38. package/lib/dedup/normalize.js +329 -0
  39. package/lib/dedup/report.d.ts +114 -0
  40. package/lib/dedup/report.js +309 -0
  41. package/lib/dedup/rewrite.d.ts +71 -0
  42. package/lib/dedup/rewrite.js +228 -0
  43. package/lib/dedup/similarity.d.ts +169 -0
  44. package/lib/dedup/similarity.js +323 -0
  45. package/lib/gates/dangerous.d.ts +22 -0
  46. package/lib/gates/dangerous.js +47 -0
  47. package/lib/gates/detect.d.ts +54 -0
  48. package/lib/gates/detect.js +106 -0
  49. package/lib/gates/messages.d.ts +39 -0
  50. package/lib/gates/messages.js +74 -0
  51. package/lib/gates/prompt.d.ts +21 -0
  52. package/lib/gates/prompt.js +56 -0
  53. package/lib/gates/writing-intent.d.ts +49 -0
  54. package/lib/gates/writing-intent.js +68 -0
  55. package/lib/index.d.ts +36 -0
  56. package/lib/index.js +76 -0
  57. package/lib/ingest/code.d.ts +71 -0
  58. package/lib/ingest/code.js +356 -0
  59. package/lib/ingest/commands.d.ts +23 -0
  60. package/lib/ingest/commands.js +74 -0
  61. package/lib/ingest/docx.d.ts +29 -0
  62. package/lib/ingest/docx.js +92 -0
  63. package/lib/ingest/index.d.ts +179 -0
  64. package/lib/ingest/index.js +640 -0
  65. package/lib/ingest/ooxml.d.ts +58 -0
  66. package/lib/ingest/ooxml.js +393 -0
  67. package/lib/ingest/pdf.d.ts +86 -0
  68. package/lib/ingest/pdf.js +1016 -0
  69. package/lib/ingest/pptx.d.ts +32 -0
  70. package/lib/ingest/pptx.js +116 -0
  71. package/lib/ingest/text.d.ts +25 -0
  72. package/lib/ingest/text.js +180 -0
  73. package/lib/ingest/types.d.ts +48 -0
  74. package/lib/ingest/types.js +47 -0
  75. package/lib/ingest/xlsx.d.ts +72 -0
  76. package/lib/ingest/xlsx.js +411 -0
  77. package/lib/ingest/zip.d.ts +76 -0
  78. package/lib/ingest/zip.js +393 -0
  79. package/lib/intake/definitions.d.ts +65 -0
  80. package/lib/intake/definitions.js +42 -0
  81. package/lib/intake/gate-rules.d.ts +57 -0
  82. package/lib/intake/gate-rules.js +118 -0
  83. package/lib/intake/gate.d.ts +24 -0
  84. package/lib/intake/gate.js +86 -0
  85. package/lib/intake/index.d.ts +76 -0
  86. package/lib/intake/index.js +468 -0
  87. package/lib/intake/questions.d.ts +169 -0
  88. package/lib/intake/questions.js +720 -0
  89. package/lib/intake/spec.d.ts +66 -0
  90. package/lib/intake/spec.js +221 -0
  91. package/lib/intake/state.d.ts +192 -0
  92. package/lib/intake/state.js +420 -0
  93. package/lib/memory/recall.d.ts +69 -0
  94. package/lib/memory/recall.js +177 -0
  95. package/lib/memory/register.d.ts +37 -0
  96. package/lib/memory/register.js +52 -0
  97. package/lib/memory/service.d.ts +39 -0
  98. package/lib/memory/service.js +39 -0
  99. package/lib/memory/store.d.ts +45 -0
  100. package/lib/memory/store.js +123 -0
  101. package/lib/memory/tools.d.ts +19 -0
  102. package/lib/memory/tools.js +195 -0
  103. package/lib/memory/types.d.ts +55 -0
  104. package/lib/memory/types.js +6 -0
  105. package/lib/paper/commands.d.ts +13 -0
  106. package/lib/paper/commands.js +84 -0
  107. package/lib/paper/lib/aicheck.d.ts +38 -0
  108. package/lib/paper/lib/aicheck.js +241 -0
  109. package/lib/paper/lib/bibtex.d.ts +21 -0
  110. package/lib/paper/lib/bibtex.js +114 -0
  111. package/lib/paper/lib/check.d.ts +32 -0
  112. package/lib/paper/lib/check.js +215 -0
  113. package/lib/paper/lib/defense.d.ts +48 -0
  114. package/lib/paper/lib/defense.js +220 -0
  115. package/lib/paper/lib/docx.d.ts +35 -0
  116. package/lib/paper/lib/docx.js +213 -0
  117. package/lib/paper/lib/layout.d.ts +41 -0
  118. package/lib/paper/lib/layout.js +572 -0
  119. package/lib/paper/lib/ledger.d.ts +112 -0
  120. package/lib/paper/lib/ledger.js +349 -0
  121. package/lib/paper/lib/lit-api.d.ts +59 -0
  122. package/lib/paper/lib/lit-api.js +296 -0
  123. package/lib/paper/lib/lit-cache.d.ts +32 -0
  124. package/lib/paper/lib/lit-cache.js +91 -0
  125. package/lib/paper/lib/project.d.ts +29 -0
  126. package/lib/paper/lib/project.js +93 -0
  127. package/lib/paper/lib/review.d.ts +37 -0
  128. package/lib/paper/lib/review.js +194 -0
  129. package/lib/paper/lib/zip.d.ts +14 -0
  130. package/lib/paper/lib/zip.js +124 -0
  131. package/lib/paper/register.d.ts +20 -0
  132. package/lib/paper/register.js +183 -0
  133. package/lib/paper/tools/aicheck.d.ts +6 -0
  134. package/lib/paper/tools/aicheck.js +38 -0
  135. package/lib/paper/tools/build.d.ts +27 -0
  136. package/lib/paper/tools/build.js +166 -0
  137. package/lib/paper/tools/check.d.ts +7 -0
  138. package/lib/paper/tools/check.js +79 -0
  139. package/lib/paper/tools/decision.d.ts +13 -0
  140. package/lib/paper/tools/decision.js +53 -0
  141. package/lib/paper/tools/defense.d.ts +11 -0
  142. package/lib/paper/tools/defense.js +74 -0
  143. package/lib/paper/tools/init.d.ts +23 -0
  144. package/lib/paper/tools/init.js +99 -0
  145. package/lib/paper/tools/lit.d.ts +27 -0
  146. package/lib/paper/tools/lit.js +236 -0
  147. package/lib/paper/tools/progress.d.ts +18 -0
  148. package/lib/paper/tools/progress.js +77 -0
  149. package/lib/paper/tools/review.d.ts +11 -0
  150. package/lib/paper/tools/review.js +41 -0
  151. package/lib/ppt/actions.d.ts +97 -0
  152. package/lib/ppt/actions.js +263 -0
  153. package/lib/ppt/check.d.ts +64 -0
  154. package/lib/ppt/check.js +185 -0
  155. package/lib/ppt/commands.d.ts +28 -0
  156. package/lib/ppt/commands.js +71 -0
  157. package/lib/ppt/convert.d.ts +146 -0
  158. package/lib/ppt/convert.js +423 -0
  159. package/lib/ppt/index.d.ts +45 -0
  160. package/lib/ppt/index.js +57 -0
  161. package/lib/ppt/marp.d.ts +74 -0
  162. package/lib/ppt/marp.js +165 -0
  163. package/lib/ppt/outline.d.ts +206 -0
  164. package/lib/ppt/outline.js +695 -0
  165. package/lib/shared/define-tool.d.ts +77 -0
  166. package/lib/shared/define-tool.js +235 -0
  167. package/lib/shared/fs-errors.d.ts +50 -0
  168. package/lib/shared/fs-errors.js +90 -0
  169. package/lib/shared/index.d.ts +31 -0
  170. package/lib/shared/index.js +32 -0
  171. package/lib/shared/spawn.d.ts +38 -0
  172. package/lib/shared/spawn.js +50 -0
  173. package/lib/shared/text.d.ts +14 -0
  174. package/lib/shared/text.js +26 -0
  175. package/package.json +82 -0
  176. package/skills/code-walkthrough/SKILL.md +94 -0
  177. package/skills/code-walkthrough/references/examples.md +129 -0
  178. package/skills/paper-dedup/SKILL.md +58 -0
  179. package/skills/paper-intake/SKILL.md +64 -0
  180. package/skills/thesis-citation/SKILL.md +67 -0
  181. package/skills/thesis-defense/SKILL.md +87 -0
  182. package/skills/thesis-eng-design/SKILL.md +97 -0
  183. package/skills/thesis-inquiry/SKILL.md +55 -0
  184. package/skills/thesis-inquiry/references/intent-spec-template.md +39 -0
  185. package/skills/thesis-literature/SKILL.md +56 -0
  186. package/skills/thesis-opener/SKILL.md +77 -0
  187. package/skills/thesis-pipeline/SKILL.md +121 -0
  188. package/skills/thesis-writing/SKILL.md +86 -0
@@ -0,0 +1,146 @@
1
+ /**
2
+ * 外部转换器探测与执行(`thesis_slides action=convert|guide` 的核心)。
3
+ *
4
+ * 本插件**不自己生成 pptx**:Markdown 是唯一真源,pptx 由外部工具产出。
5
+ * 这里沿用 `thesis_build` 的引擎探测模式(spawnSync + 超时 + 落盘构建说明),
6
+ * 但把"降级"换成"如实报告":
7
+ *
8
+ * - {@link detectEngines} 探测 `marp` / `npx --no-install @marp-team/marp-cli` / `pandoc`;
9
+ * 探测不到**不是错误**(返回 false 即可),探测本身也有超时保护。
10
+ * - {@link convert} 按 `auto|marp|pandoc|none` 执行,每一步的命令、退出码、
11
+ * stdout/stderr 摘要、耗时都记录在 {@link ConversionResult.commands} 里;
12
+ * 失败**必须抛错**并带上确切原因,绝不静默假装成功。
13
+ * - {@link conversionGuide} 在没有任何引擎时给出可直接复制执行的命令清单
14
+ * (PowerShell / macOS-Linux / npx 免安装 / 手工兜底 / 离线替代)。
15
+ *
16
+ * @module dsh-thesis/ppt
17
+ */
18
+ /** 本机可用的转换引擎(按优先级)。 */
19
+ export type EngineName = 'marp' | 'marp-npx' | 'pandoc';
20
+ /** 探测结果。 */
21
+ export interface EngineDetection {
22
+ /** 本机是否装了 `marp`(`marp --version` 成功)。 */
23
+ readonly marp: boolean;
24
+ /** `npx --no-install @marp-team/marp-cli --version` 是否可用(免安装版)。 */
25
+ readonly marpNpx: boolean;
26
+ /** 本机是否装了 `pandoc`(`pandoc --version` 成功)。 */
27
+ readonly pandoc: boolean;
28
+ /** 各命令的版本首行(探测到的才有)。 */
29
+ readonly versions: Readonly<Partial<Record<EngineName, string>>>;
30
+ /** 探测失败的原因摘要(用于排查,不是错误)。 */
31
+ readonly problems: readonly string[];
32
+ }
33
+ /** 一次命令执行的记录(写入构建说明)。 */
34
+ export interface CommandRecord {
35
+ readonly label: string;
36
+ readonly command: string;
37
+ readonly args: readonly string[];
38
+ readonly status: number | null;
39
+ readonly ok: boolean;
40
+ readonly stdout: string;
41
+ readonly stderr: string;
42
+ readonly elapsedMs: number;
43
+ readonly timedOut: boolean;
44
+ }
45
+ /** 可注入的执行器(默认 spawnSync;测试传替身)。 */
46
+ export interface SpawnOptions {
47
+ readonly cwd?: string;
48
+ readonly encoding?: string;
49
+ readonly timeout?: number;
50
+ readonly maxBuffer?: number;
51
+ }
52
+ export interface SpawnResult {
53
+ readonly status: number | null;
54
+ readonly stdout: string | null;
55
+ readonly stderr: string | null;
56
+ readonly error?: Error;
57
+ }
58
+ export type SpawnFn = (command: string, args: readonly string[], options: SpawnOptions) => SpawnResult;
59
+ /** 引擎选择(与 PptOptions.engine 同义)。 */
60
+ export type EngineChoice = 'auto' | 'marp' | 'pandoc' | 'none';
61
+ /** {@link convert} 选项。 */
62
+ export interface ConvertOptions {
63
+ readonly engine: EngineChoice;
64
+ readonly timeoutMs: number;
65
+ /** 注入的 spawn 实现(测试用)。 */
66
+ readonly spawn?: SpawnFn;
67
+ /** 注入的探测结果(测试用;缺省现场探测)。 */
68
+ readonly detection?: EngineDetection;
69
+ /** 产物存在性检查(测试用;缺省用 node:fs 判断)。 */
70
+ readonly exists?: (path: string) => boolean;
71
+ /**
72
+ * 产物字节数检查(测试用;缺省用 node:fs 的 stat)。
73
+ *
74
+ * 返回 `undefined` 表示无法确定大小——那时只接受「存在」这一条判据,
75
+ * 并把「未能校验非空」写进失败原因,绝不宣称验证过。
76
+ */
77
+ readonly size?: (path: string) => number | undefined;
78
+ /** 外部命令的工作目录。 */
79
+ readonly cwd?: string;
80
+ }
81
+ /** 转换结果。 */
82
+ export interface ConversionResult {
83
+ readonly ok: boolean;
84
+ readonly engine: EngineName | 'manual' | null;
85
+ readonly slidesPath: string;
86
+ readonly outPath: string;
87
+ readonly written: boolean;
88
+ readonly commands: readonly CommandRecord[];
89
+ /** 未产出 pptx 时,指向可执行的兜底说明。 */
90
+ readonly manualFallback: boolean;
91
+ readonly reason?: string;
92
+ }
93
+ /** 默认探测超时:10 秒(探测本身也必须有时限)。 */
94
+ export declare const DETECT_TIMEOUT_MS = 10000;
95
+ /**
96
+ * 探测本机转换器。
97
+ *
98
+ * 三条命令各自独立探测:找不到(status ≠ 0 / ENOENT)只记 `problems` 摘要,
99
+ * **不算错误**;每条命令 10 秒超时,避免卡死。
100
+ */
101
+ export declare function detectEngines(spawn?: SpawnFn, timeoutMs?: number): EngineDetection;
102
+ /** 引擎 → 是否可用;`auto` 的候选顺序是 marp → marp-npx → pandoc。 */
103
+ export declare function availableOrder(detection: EngineDetection, choice: EngineChoice): EngineName[];
104
+ /**
105
+ * 执行转换。
106
+ *
107
+ * `auto` 按 marp → marp-npx → pandoc 顺序尝试;某一步失败时记录输出并继续尝试
108
+ * 下一个引擎(与 thesis_build 的降级链一致);全部失败或引擎不可用时抛错,
109
+ * 错误里带上每个命令的退出码与 stderr 摘要——绝不返回"假装成功"的结果。
110
+ *
111
+ * 产出文件不存在同样视为失败(`--pptx` 退出码为 0 但没写出文件是常见的静默失败)。
112
+ */
113
+ export declare function convert(slidesPath: string, outPath: string, options: ConvertOptions): ConversionResult;
114
+ /** 渲染「PPT 转换说明」的输入。 */
115
+ export interface ConversionNotesInput {
116
+ readonly slidesPath: string;
117
+ readonly outPath: string;
118
+ readonly detection: EngineDetection;
119
+ readonly engine: EngineChoice;
120
+ readonly result?: ConversionResult;
121
+ readonly error?: string;
122
+ readonly theme: string;
123
+ readonly now?: Date;
124
+ }
125
+ /**
126
+ * 渲染构建说明(无论成功、失败还是只有 Markdown,都要落盘)。
127
+ * 记录:探测结果、每次命令的退出码/stdout/stderr 摘要/耗时、下一步命令。
128
+ */
129
+ export declare function renderConversionNotes(input: ConversionNotesInput, rel?: (p: string) => string): string;
130
+ /** 引擎可用时的一条命令(用于说明文档与 guide)。 */
131
+ export interface GuideCommand {
132
+ readonly engine: EngineName | 'manual';
133
+ /** 提示词或说明(中文)。 */
134
+ readonly label: string;
135
+ /** `pwsh`(Windows PowerShell)命令。 */
136
+ readonly powershell: string;
137
+ /** `bash`/`zsh`(macOS/Linux)命令。 */
138
+ readonly unix: string;
139
+ }
140
+ /** 与探测结果无关的固定命令清单(可直接复制执行,与 {@link conversionGuide} 同源)。 */
141
+ export declare function guideCommands(slidesPath: string, outPath: string): string[];
142
+ /**
143
+ * 转换指引:在没有任何可用引擎时,给出可直接复制执行的完整命令清单。
144
+ * 覆盖 PowerShell 与 macOS/Linux 两套 shell、npx 免安装版、手工兜底与离线方案。
145
+ */
146
+ export declare function conversionGuide(slidesPath: string, outPath: string, detection?: EngineDetection): string;
@@ -0,0 +1,423 @@
1
+ /**
2
+ * 外部转换器探测与执行(`thesis_slides action=convert|guide` 的核心)。
3
+ *
4
+ * 本插件**不自己生成 pptx**:Markdown 是唯一真源,pptx 由外部工具产出。
5
+ * 这里沿用 `thesis_build` 的引擎探测模式(spawnSync + 超时 + 落盘构建说明),
6
+ * 但把"降级"换成"如实报告":
7
+ *
8
+ * - {@link detectEngines} 探测 `marp` / `npx --no-install @marp-team/marp-cli` / `pandoc`;
9
+ * 探测不到**不是错误**(返回 false 即可),探测本身也有超时保护。
10
+ * - {@link convert} 按 `auto|marp|pandoc|none` 执行,每一步的命令、退出码、
11
+ * stdout/stderr 摘要、耗时都记录在 {@link ConversionResult.commands} 里;
12
+ * 失败**必须抛错**并带上确切原因,绝不静默假装成功。
13
+ * - {@link conversionGuide} 在没有任何引擎时给出可直接复制执行的命令清单
14
+ * (PowerShell / macOS-Linux / npx 免安装 / 手工兜底 / 离线替代)。
15
+ *
16
+ * @module dsh-thesis/ppt
17
+ */
18
+ import { spawnSync } from 'node:child_process';
19
+ import { existsSync, statSync } from 'node:fs';
20
+ import * as nodePath from 'node:path';
21
+ /** 命令输出摘录上限(避免把整份日志塞进工具返回值)。 */
22
+ const EXCERPT = 600;
23
+ /** 构建说明里每条命令的输出上限。 */
24
+ const NOTE_EXCERPT = 1500;
25
+ const defaultSpawn = (command, args, options) => spawnSync(command, [...args], {
26
+ ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
27
+ encoding: options.encoding ?? 'utf8',
28
+ ...(options.timeout !== undefined ? { timeout: options.timeout } : {}),
29
+ maxBuffer: options.maxBuffer ?? 8 * 1024 * 1024,
30
+ });
31
+ /** 默认的产物存在性检查(node:fs)。 */
32
+ function fsExists(path) {
33
+ return existsSync(path);
34
+ }
35
+ /** 默认的产物大小检查(node:fs);文件不存在或 stat 失败时返回 undefined。 */
36
+ function fsSize(path) {
37
+ try {
38
+ return statSync(path).size;
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ }
44
+ /**
45
+ * 判断外部引擎是否真的产出了可用的 pptx。
46
+ *
47
+ * 三条判据:存在 → 是文件(非目录)→ 非空。`size` 未知时只走到第二条,
48
+ * 并在结论里显式说明「未校验非空」,而不是假装验证过。
49
+ */
50
+ function probeOutput(path, exists, size) {
51
+ if (!exists(path))
52
+ return { ok: false, reason: `退出码 0,但未写出 ${path}` };
53
+ const bytes = size(path);
54
+ if (bytes === undefined)
55
+ return { ok: true };
56
+ if (bytes === 0)
57
+ return { ok: false, reason: `退出码 0,但写出的 ${path} 是 0 字节(引擎静默失败)` };
58
+ return { ok: true };
59
+ }
60
+ function excerpt(text, limit = NOTE_EXCERPT) {
61
+ const trimmed = (text ?? '').trim();
62
+ if (trimmed.length <= limit)
63
+ return trimmed;
64
+ return `${trimmed.slice(0, limit)}\n…(输出已截断,共 ${trimmed.length} 字符)`;
65
+ }
66
+ function firstLine(text) {
67
+ return (text ?? '').split(/\r?\n/).map(l => l.trim()).find(l => l !== '') ?? '';
68
+ }
69
+ /** 命令的可复制形态(Windows 上仍用 POSIX 引号,便于记录与跨平台复现)。 */
70
+ function displayCommand(command, args) {
71
+ const quote = (value) => (/\s/.test(value) ? `"${value}"` : value);
72
+ return [command, ...args].map(quote).join(' ');
73
+ }
74
+ function run(label, command, args, opts) {
75
+ const started = Date.now();
76
+ let result;
77
+ try {
78
+ result = opts.spawn(command, args, {
79
+ ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
80
+ encoding: 'utf8',
81
+ timeout: opts.timeoutMs,
82
+ });
83
+ }
84
+ catch (error) {
85
+ return {
86
+ label,
87
+ command,
88
+ args,
89
+ status: null,
90
+ ok: false,
91
+ stdout: '',
92
+ stderr: String(error),
93
+ elapsedMs: Date.now() - started,
94
+ timedOut: false,
95
+ };
96
+ }
97
+ const elapsedMs = Date.now() - started;
98
+ const stdout = result.stdout ?? '';
99
+ const stderr = result.stderr ?? '';
100
+ const timedOut = result.status === null && result.error !== undefined && /ETIMEDOUT|timed? ?out/i.test(result.error.message);
101
+ return {
102
+ label,
103
+ command,
104
+ args,
105
+ status: result.status,
106
+ ok: result.status === 0,
107
+ stdout: excerpt(stdout),
108
+ stderr: excerpt(stderr !== '' ? stderr : result.error?.message),
109
+ elapsedMs,
110
+ timedOut,
111
+ };
112
+ }
113
+ /** 默认探测超时:10 秒(探测本身也必须有时限)。 */
114
+ export const DETECT_TIMEOUT_MS = 10_000;
115
+ /**
116
+ * 探测本机转换器。
117
+ *
118
+ * 三条命令各自独立探测:找不到(status ≠ 0 / ENOENT)只记 `problems` 摘要,
119
+ * **不算错误**;每条命令 10 秒超时,避免卡死。
120
+ */
121
+ export function detectEngines(spawn = defaultSpawn, timeoutMs = DETECT_TIMEOUT_MS) {
122
+ const problems = [];
123
+ const versions = {};
124
+ const marp = run('探测 marp', 'marp', ['--version'], { timeoutMs, spawn });
125
+ if (!marp.ok)
126
+ problems.push(`marp 不可用:${marp.timedOut ? '探测超时' : marp.stderr.split('\n')[0] ?? ''}`);
127
+ else
128
+ versions.marp = firstLine(marp.stdout);
129
+ const marpNpx = run('探测 npx marp-cli', 'npx', ['--no-install', '@marp-team/marp-cli', '--version'], { timeoutMs, spawn });
130
+ if (!marpNpx.ok)
131
+ problems.push(`npx marp-cli 不可用:${marpNpx.timedOut ? '探测超时' : marpNpx.stderr.split('\n')[0] ?? ''}`);
132
+ else
133
+ versions['marp-npx'] = firstLine(marpNpx.stdout);
134
+ const pandoc = run('探测 pandoc', 'pandoc', ['--version'], { timeoutMs, spawn });
135
+ if (!pandoc.ok)
136
+ problems.push(`pandoc 不可用:${pandoc.timedOut ? '探测超时' : pandoc.stderr.split('\n')[0] ?? ''}`);
137
+ else
138
+ versions.pandoc = firstLine(pandoc.stdout);
139
+ return { marp: marp.ok, marpNpx: marpNpx.ok, pandoc: pandoc.ok, versions, problems };
140
+ }
141
+ const ENGINE_PLANS = {
142
+ marp: {
143
+ engine: 'marp',
144
+ label: 'marp-cli(本机已安装)',
145
+ command: 'marp',
146
+ args: (slides, out) => [slides, '--pptx', '--output', out, '--allow-local-files'],
147
+ },
148
+ 'marp-npx': {
149
+ engine: 'marp-npx',
150
+ label: 'npx 免安装 @marp-team/marp-cli',
151
+ command: 'npx',
152
+ args: (slides, out) => ['--no-install', '@marp-team/marp-cli', slides, '--pptx', '--output', out, '--allow-local-files'],
153
+ },
154
+ pandoc: {
155
+ engine: 'pandoc',
156
+ label: 'pandoc(Markdown → pptx)',
157
+ command: 'pandoc',
158
+ args: (slides, out) => [slides, '-o', out],
159
+ },
160
+ };
161
+ /** 引擎 → 是否可用;`auto` 的候选顺序是 marp → marp-npx → pandoc。 */
162
+ export function availableOrder(detection, choice) {
163
+ const order = ['marp', 'marp-npx', 'pandoc'];
164
+ if (choice === 'marp')
165
+ return order.filter(name => name !== 'pandoc' && isAvailable(detection, name));
166
+ if (choice === 'pandoc')
167
+ return detection.pandoc ? ['pandoc'] : [];
168
+ if (choice === 'none')
169
+ return [];
170
+ return order.filter(name => isAvailable(detection, name));
171
+ }
172
+ function isAvailable(detection, engine) {
173
+ if (engine === 'marp')
174
+ return detection.marp;
175
+ if (engine === 'marp-npx')
176
+ return detection.marpNpx;
177
+ return detection.pandoc;
178
+ }
179
+ function engineChoiceReason(choice, detection) {
180
+ if (choice === 'marp') {
181
+ return `请求 marp 引擎,但本机既没有 marp(${detection.marp ? '有' : '无'})也没有可用的 npx @marp-team/marp-cli(${detection.marpNpx ? '有' : '无'})。`;
182
+ }
183
+ if (choice === 'pandoc') {
184
+ return '请求 pandoc 引擎但本机未安装 pandoc(pandoc --version 未通过)。';
185
+ }
186
+ return '本机未探测到 marp、npx @marp-team/marp-cli 或 pandoc,无法执行转换。';
187
+ }
188
+ /**
189
+ * 执行转换。
190
+ *
191
+ * `auto` 按 marp → marp-npx → pandoc 顺序尝试;某一步失败时记录输出并继续尝试
192
+ * 下一个引擎(与 thesis_build 的降级链一致);全部失败或引擎不可用时抛错,
193
+ * 错误里带上每个命令的退出码与 stderr 摘要——绝不返回"假装成功"的结果。
194
+ *
195
+ * 产出文件不存在同样视为失败(`--pptx` 退出码为 0 但没写出文件是常见的静默失败)。
196
+ */
197
+ export function convert(slidesPath, outPath, options) {
198
+ const spawn = options.spawn ?? defaultSpawn;
199
+ const detection = options.detection ?? detectEngines(spawn);
200
+ const exists = options.exists ?? ((path) => fsExists(path));
201
+ const size = options.size ?? ((path) => fsSize(path));
202
+ const commands = [];
203
+ const candidates = availableOrder(detection, options.engine);
204
+ if (candidates.length === 0) {
205
+ const reason = engineChoiceReason(options.engine, detection);
206
+ const error = new Error(`${reason}\n` +
207
+ `幻灯片 Markdown:${slidesPath}\n` +
208
+ '下一步:运行 thesis_slides action=guide 获取可复制执行的转换命令(含 PowerShell/Unix/npx 与手工兜底),' +
209
+ '或直接用编辑器打开 Markdown 手工成稿。本工具不会假装成功。');
210
+ throw error;
211
+ }
212
+ const workDir = options.cwd ?? nodePath.dirname(slidesPath);
213
+ const failures = [];
214
+ for (const name of candidates) {
215
+ const plan = ENGINE_PLANS[name];
216
+ const args = plan.args(slidesPath, outPath);
217
+ const record = run(plan.label, plan.command, args, { cwd: workDir, timeoutMs: options.timeoutMs, spawn });
218
+ commands.push(record);
219
+ if (!record.ok) {
220
+ failures.push(`- ${displayCommand(record.command, record.args)} → 退出码 ${record.status ?? 'null'}${record.timedOut ? '(超时)' : ''}:${record.stderr.split('\n')[0] ?? ''}`);
221
+ continue;
222
+ }
223
+ // 退出码 0 不等于成功:产物必须存在、是文件、且**非空**。
224
+ // 只看存在性的话,一个 0 字节的 pptx 也会被报成「转换成功」,学生打开才发现是空的。
225
+ const written = probeOutput(outPath, exists, size);
226
+ if (!written.ok) {
227
+ failures.push(`- ${displayCommand(record.command, record.args)} → ${written.reason}`);
228
+ continue;
229
+ }
230
+ return {
231
+ ok: true,
232
+ engine: plan.engine,
233
+ slidesPath,
234
+ outPath,
235
+ written: true,
236
+ commands,
237
+ manualFallback: false,
238
+ };
239
+ }
240
+ throw new Error(`转换失败:按 ${options.engine} 依次尝试了 ${candidates.length} 个引擎,均未产出 ${outPath}。\n` +
241
+ `${failures.join('\n')}\n` +
242
+ '完整命令与输出摘要见 07-答辩/PPT转换说明.md;也可运行 thesis_slides action=guide 获取手工兜底方案。');
243
+ }
244
+ /**
245
+ * 渲染构建说明(无论成功、失败还是只有 Markdown,都要落盘)。
246
+ * 记录:探测结果、每次命令的退出码/stdout/stderr 摘要/耗时、下一步命令。
247
+ */
248
+ export function renderConversionNotes(input, rel = (p) => p) {
249
+ const now = input.now ?? new Date();
250
+ const lines = [];
251
+ lines.push('# PPT 转换说明');
252
+ lines.push('');
253
+ lines.push(`> 由 thesis_slides 生成;时间:${now.toISOString()}`);
254
+ lines.push('> Markdown 是唯一真源(07-答辩/PPT.md),pptx 由外部工具生成;本插件不自己造 pptx。');
255
+ lines.push('');
256
+ lines.push('## 一、输入与产物');
257
+ lines.push('');
258
+ lines.push(`- 幻灯 Markdown:${rel(input.slidesPath)}`);
259
+ lines.push(`- 目标 pptx:${rel(input.outPath)}`);
260
+ lines.push(`- 配置引擎:${input.engine};主题:${input.theme}`);
261
+ lines.push('');
262
+ lines.push('## 二、引擎探测结果');
263
+ lines.push('');
264
+ lines.push(`- marp(\`marp --version\`):${input.detection.marp ? `可用(${input.detection.versions.marp ?? ''})` : '不可用'}`);
265
+ lines.push(`- npx 免安装(\`npx --no-install @marp-team/marp-cli --version\`):${input.detection.marpNpx ? `可用(${input.detection.versions['marp-npx'] ?? ''})` : '不可用'}`);
266
+ lines.push(`- pandoc(\`pandoc --version\`):${input.detection.pandoc ? `可用(${input.detection.versions.pandoc ?? ''})` : '不可用'}`);
267
+ if (input.detection.problems.length > 0) {
268
+ lines.push('');
269
+ lines.push('探测失败摘要(不是错误,只是说明本机没装):');
270
+ for (const problem of input.detection.problems)
271
+ lines.push(`- ${problem}`);
272
+ }
273
+ lines.push('');
274
+ lines.push('## 三、执行记录');
275
+ lines.push('');
276
+ if (input.result !== undefined && input.result.commands.length > 0) {
277
+ for (const record of input.result.commands) {
278
+ lines.push(`### ${record.label}`);
279
+ lines.push('');
280
+ lines.push(`- 命令:\`${displayCommand(record.command, record.args)}\``);
281
+ lines.push(`- 退出码:${record.status ?? 'null'};耗时:${record.elapsedMs} ms${record.timedOut ? '(超时)' : ''}`);
282
+ if (record.stdout !== '') {
283
+ lines.push('- stdout 摘要:');
284
+ lines.push('');
285
+ lines.push('```text');
286
+ lines.push(record.stdout);
287
+ lines.push('```');
288
+ }
289
+ if (record.stderr !== '') {
290
+ lines.push('- stderr 摘要:');
291
+ lines.push('');
292
+ lines.push('```text');
293
+ lines.push(record.stderr);
294
+ lines.push('```');
295
+ }
296
+ lines.push('');
297
+ }
298
+ }
299
+ else {
300
+ lines.push('(未执行任何转换命令)');
301
+ lines.push('');
302
+ }
303
+ if (input.result !== undefined && input.result.ok) {
304
+ lines.push(`结果:成功,引擎 ${input.result.engine} 已产出 ${rel(input.outPath)}。`);
305
+ }
306
+ else if (input.error !== undefined) {
307
+ lines.push('结果:**未产出 pptx**,原因:');
308
+ lines.push('');
309
+ lines.push('```text');
310
+ lines.push(excerpt(input.error));
311
+ lines.push('```');
312
+ }
313
+ else {
314
+ lines.push('结果:尚未执行转换(只生成了 Markdown)。');
315
+ }
316
+ lines.push('');
317
+ lines.push('## 四、下一步(可直接复制执行)');
318
+ lines.push('');
319
+ for (const command of guideCommands(input.slidesPath, input.outPath))
320
+ lines.push(`- \`${command}\``);
321
+ lines.push('');
322
+ lines.push('完整指引(含手工兜底与离线方案)见本工具 `thesis_slides action=guide` 的输出。');
323
+ lines.push('');
324
+ return lines.join('\n');
325
+ }
326
+ /** 与探测结果无关的固定命令清单(可直接复制执行,与 {@link conversionGuide} 同源)。 */
327
+ export function guideCommands(slidesPath, outPath) {
328
+ return [
329
+ `npx --yes @marp-team/marp-cli "${slidesPath}" --pptx --output "${outPath}" --allow-local-files`,
330
+ `marp "${slidesPath}" --pptx --output "${outPath}" --allow-local-files`,
331
+ `pandoc "${slidesPath}" -o "${outPath}"`,
332
+ ];
333
+ }
334
+ /**
335
+ * 转换指引:在没有任何可用引擎时,给出可直接复制执行的完整命令清单。
336
+ * 覆盖 PowerShell 与 macOS/Linux 两套 shell、npx 免安装版、手工兜底与离线方案。
337
+ */
338
+ export function conversionGuide(slidesPath, outPath, detection) {
339
+ const slides = slidesPath;
340
+ const out = outPath;
341
+ const lines = [];
342
+ lines.push('# 答辩 PPT 转换指引');
343
+ lines.push('');
344
+ lines.push('本插件只产出标准 Marp Markdown(唯一真源),pptx 由外部工具生成;不自研 pptx 生成器,也不假装转换成功。');
345
+ lines.push('');
346
+ lines.push('## 一、本机探测结果');
347
+ lines.push('');
348
+ if (detection === undefined) {
349
+ lines.push('(未执行探测——运行 thesis_slides action=convert 时会自动探测)');
350
+ }
351
+ else {
352
+ lines.push(`- marp:${detection.marp ? `可用(${detection.versions.marp ?? ''})` : '未安装'}`);
353
+ lines.push(`- npx marp-cli:${detection.marpNpx ? `可用(${detection.versions['marp-npx'] ?? ''})` : '未安装/不可用'}`);
354
+ lines.push(`- pandoc:${detection.pandoc ? `可用(${detection.versions.pandoc ?? ''})` : '未安装'}`);
355
+ if (!detection.marp && !detection.marpNpx && !detection.pandoc) {
356
+ lines.push('');
357
+ lines.push('**结论:三种转换器都不可用**——请从下面任选一条路径(推荐第 1 条,无需联网安装)。');
358
+ }
359
+ }
360
+ lines.push('');
361
+ lines.push('## 二、路径 1(推荐):手工兜底,零安装');
362
+ lines.push('');
363
+ lines.push(`1. 用编辑器打开 \`${slides}\`,全选复制。`);
364
+ lines.push('2. 打开 https://marp.app/(网页版,粘贴 Markdown 即得预览),导出 PDF/PPTX;或把 Markdown 粘贴进支持 Marp 的编辑器预览。');
365
+ lines.push('3. 若只想快速成稿:把每页 `## 标题` 与要点复制进 WPS 演示 / PowerPoint,套用学校模板后另存为 pptx。');
366
+ lines.push(`4. 无论哪条路径,最后都把成品存为 \`${out}\`,并与 \`07-答辩/PPT.md\` 的内容保持一致。`);
367
+ lines.push('');
368
+ lines.push('## 三、路径 2:npx 免安装(需能访问 npm 源)');
369
+ lines.push('');
370
+ lines.push('```powershell');
371
+ lines.push('# Windows PowerShell');
372
+ lines.push(`npx --yes @marp-team/marp-cli "${slides}" --pptx --output "${out}" --allow-local-files`);
373
+ lines.push('```');
374
+ lines.push('');
375
+ lines.push('```bash');
376
+ lines.push('# macOS / Linux');
377
+ lines.push(`npx --yes @marp-team/marp-cli "${slides}" --pptx --output "${out}" --allow-local-files`);
378
+ lines.push('```');
379
+ lines.push('');
380
+ lines.push('## 四、路径 3:安装 marp-cli(推荐长期使用)');
381
+ lines.push('');
382
+ lines.push('```powershell');
383
+ lines.push('# Windows PowerShell(全局安装后直接可用)');
384
+ lines.push('npm install -g @marp-team/marp-cli');
385
+ lines.push(`marp "${slides}" --pptx --output "${out}" --allow-local-files`);
386
+ lines.push('# 也可顺便导出 PDF 与 PNG(答辩备份最稳)');
387
+ lines.push(`marp "${slides}" --pdf --output "${nodePath.join(nodePath.dirname(out), 'PPT.pdf')}" --allow-local-files`);
388
+ lines.push('```');
389
+ lines.push('');
390
+ lines.push('```bash');
391
+ lines.push('# macOS / Linux');
392
+ lines.push('npm install -g @marp-team/marp-cli');
393
+ lines.push(`marp "${slides}" --pptx --output "${out}" --allow-local-files`);
394
+ lines.push(`marp "${slides}" --pdf --output "${nodePath.join(nodePath.dirname(out), 'PPT.pdf')}" --allow-local-files`);
395
+ lines.push('```');
396
+ lines.push('');
397
+ lines.push('## 五、路径 4:pandoc(装了 pandoc 时最省事)');
398
+ lines.push('');
399
+ lines.push('```powershell');
400
+ lines.push(`pandoc "${slides}" -o "${out}"`);
401
+ lines.push('```');
402
+ lines.push('');
403
+ lines.push('```bash');
404
+ lines.push(`pandoc "${slides}" -o "${out}"`);
405
+ lines.push('```');
406
+ lines.push('');
407
+ lines.push('> 注意:pandoc 走的是它自己的 Markdown → pptx 规则,Marp 的 frontmatter 与主题不会生效,版面较朴素;');
408
+ lines.push('> 优先用 marp-cli,pandoc 只作为兜底。');
409
+ lines.push('');
410
+ lines.push('## 六、离线环境的替代方案');
411
+ lines.push('');
412
+ lines.push('1. 在能联网的机器上跑路径 2/3,把生成的 pptx(或 PDF/PNG)拷回本机,再按学校模板手工微调。');
413
+ lines.push('2. `npm install -g @marp-team/marp-cli` 在有网机器上安装,把整包目录拷到本机后用其绝对路径调用。');
414
+ lines.push('3. 完全离线且无外部工具时:直接用编辑器打开 Markdown,按第 1 节手工成稿——Markdown 本身就是要讲的内容,绝不因工具缺失而阻塞答辩准备。');
415
+ lines.push('');
416
+ lines.push('## 七、验证转换是否成功');
417
+ lines.push('');
418
+ lines.push(`- 检查 \`${out}\` 是否存在且大小合理(几 MB 级);`);
419
+ lines.push('- 打开 pptx 抽查 3 页:封面占位是否已替换、图表是否可见、备注里是否有讲稿;');
420
+ lines.push('- 转换命令的输出摘要会写入 `07-答辩/PPT转换说明.md`,对照退出码即可判断。');
421
+ lines.push('');
422
+ return lines.join('\n');
423
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 答辩幻灯装配层:注册 `thesis_slides` 工具(outline / convert / check / guide)。
3
+ *
4
+ * 分层与论文流水线一致:
5
+ * - 业务逻辑在 `ppt/actions.ts`、`ppt/outline.ts`、`ppt/marp.ts`、`ppt/check.ts`、
6
+ * `ppt/convert.ts`(零 cordis 依赖,可独立单测,测试不需要安装宿主包);
7
+ * - 本文件只做 `defineTool` 胶水。
8
+ *
9
+ * 产品决策:**不自研 pptx 生成器**。Markdown 是唯一真源,pptx 由 marp-cli /
10
+ * pandoc 产出;两者都没有时给出可直接复制执行的命令与手工兜底,绝不假装成功。
11
+ *
12
+ * @module dsh-thesis/ppt
13
+ */
14
+ import type { Context } from '@deepseek-ai/cordis';
15
+ import type { PptOptions } from '../config.ts';
16
+ import { type PptDeps } from './actions.ts';
17
+ export { buildSlidePlan, parseDefenseMaterials, parseChapterFallback, truncateBullet, truncateTitle, audienceProfile, titleFromLedger, loadOutlineInputs, summarizePlan, composeSlides, MAX_BULLET_CHARS, MAX_TITLE_CHARS, MAX_BULLETS_PER_SLIDE, MIN_BULLETS_PER_SLIDE, SECONDS_PER_SLIDE_MIN, SECONDS_PER_SLIDE_MAX } from './outline.ts';
18
+ export type { Audience, PlannedSlide, PlanBullet, SlidePlan, SlideKind, DefenseMaterials, OutlineInputs } from './outline.ts';
19
+ export { renderMarp, parseMarp, parseFrontmatter, findUnsafeMarkdown, sanitizeTheme, sanitizeYamlScalar, MARP_THEMES } from './marp.ts';
20
+ export type { MarpOptions, ParsedDeck, ParsedSlide } from './marp.ts';
21
+ export { detectEngines, convert, conversionGuide, renderConversionNotes, guideCommands } from './convert.ts';
22
+ export type { ConversionResult, EngineChoice, EngineDetection, EngineName, CommandRecord, SpawnFn } from './convert.ts';
23
+ export { checkDeck, renderCheckReport, verifyAnchors } from './check.ts';
24
+ export type { CheckResult, SlideIssue } from './check.ts';
25
+ export { runPptAction, runOutline, runCheckAction, runConvertAction, runGuideAction, planFromInputs, PPT_RELS } from './actions.ts';
26
+ export type { PptArgs, PptDeps, PptActionResult, DeckCheck } from './actions.ts';
27
+ export { handlePptCommand, parsePptCommandInput } from './commands.ts';
28
+ /** `thesis_slides` 的工具描述(中文,面向模型与用户)。 */
29
+ export declare const PPT_TOOL_DESCRIPTION: string;
30
+ /**
31
+ * 注册 `thesis_slides` 工具与 `/thesis-defense` 命令。
32
+ *
33
+ * 二进制例外(与 `thesis_build` 一致,文档化):pptx 由外部命令直接写在目标路径,
34
+ * `ctx.fs` 无二进制写入能力,故落盘钩子用 node:fs({@link writeBinary});
35
+ * 文本产物(PPT.md、PPT转换说明.md)全部走 `ctx.fs`。
36
+ */
37
+ export declare function registerPpt(ctx: Context, options: {
38
+ ppt: PptOptions;
39
+ }, deps?: PptDeps): void;
40
+ export declare const PPT_REL_PATHS: {
41
+ readonly slides: "07-答辩/PPT.md";
42
+ readonly pptx: "07-答辩/PPT.pptx";
43
+ readonly notes: "07-答辩/PPT转换说明.md";
44
+ };
45
+ export type { PptOptions };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 答辩幻灯装配层:注册 `thesis_slides` 工具(outline / convert / check / guide)。
3
+ *
4
+ * 分层与论文流水线一致:
5
+ * - 业务逻辑在 `ppt/actions.ts`、`ppt/outline.ts`、`ppt/marp.ts`、`ppt/check.ts`、
6
+ * `ppt/convert.ts`(零 cordis 依赖,可独立单测,测试不需要安装宿主包);
7
+ * - 本文件只做 `defineTool` 胶水。
8
+ *
9
+ * 产品决策:**不自研 pptx 生成器**。Markdown 是唯一真源,pptx 由 marp-cli /
10
+ * pandoc 产出;两者都没有时给出可直接复制执行的命令与手工兜底,绝不假装成功。
11
+ *
12
+ * @module dsh-thesis/ppt
13
+ */
14
+ import { definePaperTool, sessionCwd, textOutput, writeBinary } from "../shared/index.js";
15
+ import { runPptAction } from "./actions.js";
16
+ export { buildSlidePlan, parseDefenseMaterials, parseChapterFallback, truncateBullet, truncateTitle, audienceProfile, titleFromLedger, loadOutlineInputs, summarizePlan, composeSlides, MAX_BULLET_CHARS, MAX_TITLE_CHARS, MAX_BULLETS_PER_SLIDE, MIN_BULLETS_PER_SLIDE, SECONDS_PER_SLIDE_MIN, SECONDS_PER_SLIDE_MAX } from "./outline.js";
17
+ export { renderMarp, parseMarp, parseFrontmatter, findUnsafeMarkdown, sanitizeTheme, sanitizeYamlScalar, MARP_THEMES } from "./marp.js";
18
+ export { detectEngines, convert, conversionGuide, renderConversionNotes, guideCommands } from "./convert.js";
19
+ export { checkDeck, renderCheckReport, verifyAnchors } from "./check.js";
20
+ export { runPptAction, runOutline, runCheckAction, runConvertAction, runGuideAction, planFromInputs, PPT_RELS } from "./actions.js";
21
+ export { handlePptCommand, parsePptCommandInput } from "./commands.js";
22
+ /** `thesis_slides` 的工具描述(中文,面向模型与用户)。 */
23
+ export const PPT_TOOL_DESCRIPTION = '答辩幻灯(Markdown 为准,pptx 交给外部工具):outline 依据 07-答辩/答辩素材.md 生成 Marp 兼容的 07-答辩/PPT.md(10-12 页骨架、每页 3-6 条要点、讲稿注释、证据锚点;默认不覆盖已有文件,force 时先备份);' +
24
+ 'convert 探测 marp / npx marp-cli / pandoc 并执行转换产出 07-答辩/PPT.pptx(失败抛错并给出确切命令,绝不假装成功);' +
25
+ 'check 做确定性质量检查(页数/要点数/字数/讲稿/锚点/待补充标记/预计时长 8-12 分钟)并逐条定位;guide 只输出转换指引。' +
26
+ '无论成败都写 07-答辩/PPT转换说明.md(探测结果 + 命令 + 退出码 + 输出摘要)。';
27
+ /**
28
+ * 注册 `thesis_slides` 工具与 `/thesis-defense` 命令。
29
+ *
30
+ * 二进制例外(与 `thesis_build` 一致,文档化):pptx 由外部命令直接写在目标路径,
31
+ * `ctx.fs` 无二进制写入能力,故落盘钩子用 node:fs({@link writeBinary});
32
+ * 文本产物(PPT.md、PPT转换说明.md)全部走 `ctx.fs`。
33
+ */
34
+ export function registerPpt(ctx, options, deps = {}) {
35
+ const fullDeps = { writeBinary, ...deps };
36
+ ctx.tools.register(definePaperTool({
37
+ name: 'thesis_slides',
38
+ description: PPT_TOOL_DESCRIPTION,
39
+ parameters: {
40
+ action: { type: 'string', required: true, description: 'outline(生成/刷新幻灯 Markdown)| convert(执行转换)| check(质量检查)| guide(只输出转换指引)' },
41
+ pages: { type: 'number', description: '目标页数,默认 11;本科骨架允许 10-12 页,超出会自动夹取并在结果里说明' },
42
+ audience: { type: 'string', description: '听众档位:undergrad(默认,40-60 秒/页)| master(45-70 秒/页,骨架上限更高)' },
43
+ force: { type: 'boolean', description: 'outline 时覆盖已存在的 07-答辩/PPT.md,默认 false(覆盖前把旧文件另存为 07-答辩/PPT-<时间戳>.md)' },
44
+ },
45
+ output: textOutput(),
46
+ async execute(rawArgs, exec) {
47
+ const args = { ...(rawArgs ?? {}) };
48
+ const outcome = await runPptAction(ctx.fs, sessionCwd(exec), args, options, exec.signal, fullDeps);
49
+ return outcome.text;
50
+ },
51
+ }));
52
+ }
53
+ export const PPT_REL_PATHS = {
54
+ slides: '07-答辩/PPT.md',
55
+ pptx: '07-答辩/PPT.pptx',
56
+ notes: '07-答辩/PPT转换说明.md',
57
+ };