dsh-ppt 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/LICENSE +21 -21
- package/cordis.patch.yml +19 -19
- package/docs/FEATURE-PLAN-2026-10-01.md +50 -50
- package/docs/LIBREOFFICE-INTEGRATION-2026-10-01.md +69 -69
- package/docs/VALIDATION.md +2 -0
- package/docs/validation-language-2026-10-03.md +16 -0
- package/lib/client.js +136 -41
- package/lib/workbench-language.d.ts +4 -0
- package/lib/workbench-language.js +65 -0
- package/lib/workbench.js +7 -3
- package/package.json +1 -1
- package/skills/dsh-ppt/package.json +11 -11
- package/skills/dsh-ppt/references/copywriting.md +59 -59
- package/skills/dsh-ppt/references/syntax.md +44 -44
- package/skills/dsh-ppt/references/themes.md +61 -61
- package/skills/dsh-ppt/scripts/build-deck.mjs +184 -184
- package/skills/dsh-ppt/scripts/deck-core.mjs +2378 -2378
- package/skills/dsh-ppt/scripts/deck-delivery.mjs +104 -104
- package/skills/dsh-ppt/scripts/deck-media.mjs +160 -160
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
# dsh-ppt 输入语法参考(Markdown / 结构化 slides / 动效)
|
|
2
|
-
|
|
3
|
-
只在需要查具体语法时读本文件;常规流程在 SKILL.md。
|
|
4
|
-
|
|
5
|
-
## Markdown 解析规则
|
|
6
|
-
|
|
7
|
-
`ppt_create` 与 `build-deck.mjs` 按以下规则解析 Markdown:
|
|
8
|
-
|
|
9
|
-
- 第一个 `# 标题` 会成为封面标题;其后的段落/列表成为封面副标题来源。
|
|
10
|
-
- 每个 `## 小节`(或更深标题)生成一页:
|
|
11
|
-
- 有 `- ` / `* ` / `1.` 列表 → `bullets` 页;
|
|
12
|
-
- 无列表且无正文 → `section` 过渡页;
|
|
13
|
-
- 有正文段落 → 正文拆句后作为 bullets。
|
|
14
|
-
- 没有标题的纯文本:第一段第一句为封面副标题,后续段落按 5 句一组拆成要点页。
|
|
15
|
-
- 只有一句话:自动生成「封面 → 核心观点 → 结束页」三页。
|
|
16
|
-
- 需要更精细控制时使用结构化 `slides` JSON,不要和 Markdown 混用。
|
|
17
|
-
|
|
18
|
-
## 进阶语法(自动识别)
|
|
19
|
-
|
|
20
|
-
- **表格**:`| 列 | 列 |` 连续行(第二行 `| --- | --- |` 分隔线)→ 独立 `table` 页(主题色表头;每页 8 行数据,长表格自动续页并重复表头;最多 8 列、每格 60 字符,超限先报错)。数据对比优先用表格,别堆要点。
|
|
21
|
-
- **金句**:`>` 引用块 → 独立 `quote` 页;最后一行写 `—— 出处` 会成为署名。
|
|
22
|
-
- **演讲者备注**:`<!-- 备注: 这页要讲的内容 -->`(或 `<!-- note: ... -->`)附着到该页。备注不进正文:HTML 放映按 `S` 呼出备注面板,PPTX 写入原生备注(演示者视图可见)。为汇报/演讲类 deck 主动写备注(口径、数据出处、应答预案)。
|
|
23
|
-
|
|
24
|
-
## 结构化 slides(高级用法)
|
|
25
|
-
|
|
26
|
-
`ppt_create` 的 `slides` 与 CLI `--slides` 接受 JSON 数组:
|
|
27
|
-
|
|
28
|
-
```json
|
|
29
|
-
[
|
|
30
|
-
{ "layout": "cover", "title": "标题", "subtitle": "副标题", "kicker": "开场", "notes": "开场先自我介绍 30 秒" },
|
|
31
|
-
{ "layout": "section", "kicker": "01", "title": "背景" },
|
|
32
|
-
{ "layout": "bullets", "title": "三个论点", "bullets": ["论点一", "论点二", "论点三"] },
|
|
33
|
-
{ "layout": "table", "title": "业绩对比", "rows": [["大区", "营收"], ["华东", "1.2 亿"]], "notes": "强调华东增速" },
|
|
34
|
-
{ "layout": "quote", "title": "慢就是快。", "subtitle": "创始人" },
|
|
35
|
-
{ "layout": "statement", "title": "核心观点一句话", "subtitle": "支撑说明" },
|
|
36
|
-
{ "layout": "closing", "title": "谢谢", "subtitle": "行动号召" }
|
|
37
|
-
]
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
`layout` 支持 `cover | section | bullets | statement | quote | table | closing | image | image-left | image-right | chart`;每页可选 `notes`,表格用 `rows`(首行为表头)。结构化表格自动分页,备注只放第一页。分页后超过 maxSlides 会报错,最多120页。图文、图表、品牌及按页编辑见 `advanced.md`。
|
|
41
|
-
|
|
42
|
-
## 动效(motion)
|
|
43
|
-
|
|
44
|
-
默认 `motion: on`:HTML 放映有页间淡入与要点逐条入场;PPTX 每页原生淡入转场,`bullets` 页的要点按「点击一次出现一条」放映。交付严肃/打印场景或用户明确要静态时传 `motion: 'off'`(CLI `--motion off`),产物不含任何动画代码。
|
|
1
|
+
# dsh-ppt 输入语法参考(Markdown / 结构化 slides / 动效)
|
|
2
|
+
|
|
3
|
+
只在需要查具体语法时读本文件;常规流程在 SKILL.md。
|
|
4
|
+
|
|
5
|
+
## Markdown 解析规则
|
|
6
|
+
|
|
7
|
+
`ppt_create` 与 `build-deck.mjs` 按以下规则解析 Markdown:
|
|
8
|
+
|
|
9
|
+
- 第一个 `# 标题` 会成为封面标题;其后的段落/列表成为封面副标题来源。
|
|
10
|
+
- 每个 `## 小节`(或更深标题)生成一页:
|
|
11
|
+
- 有 `- ` / `* ` / `1.` 列表 → `bullets` 页;
|
|
12
|
+
- 无列表且无正文 → `section` 过渡页;
|
|
13
|
+
- 有正文段落 → 正文拆句后作为 bullets。
|
|
14
|
+
- 没有标题的纯文本:第一段第一句为封面副标题,后续段落按 5 句一组拆成要点页。
|
|
15
|
+
- 只有一句话:自动生成「封面 → 核心观点 → 结束页」三页。
|
|
16
|
+
- 需要更精细控制时使用结构化 `slides` JSON,不要和 Markdown 混用。
|
|
17
|
+
|
|
18
|
+
## 进阶语法(自动识别)
|
|
19
|
+
|
|
20
|
+
- **表格**:`| 列 | 列 |` 连续行(第二行 `| --- | --- |` 分隔线)→ 独立 `table` 页(主题色表头;每页 8 行数据,长表格自动续页并重复表头;最多 8 列、每格 60 字符,超限先报错)。数据对比优先用表格,别堆要点。
|
|
21
|
+
- **金句**:`>` 引用块 → 独立 `quote` 页;最后一行写 `—— 出处` 会成为署名。
|
|
22
|
+
- **演讲者备注**:`<!-- 备注: 这页要讲的内容 -->`(或 `<!-- note: ... -->`)附着到该页。备注不进正文:HTML 放映按 `S` 呼出备注面板,PPTX 写入原生备注(演示者视图可见)。为汇报/演讲类 deck 主动写备注(口径、数据出处、应答预案)。
|
|
23
|
+
|
|
24
|
+
## 结构化 slides(高级用法)
|
|
25
|
+
|
|
26
|
+
`ppt_create` 的 `slides` 与 CLI `--slides` 接受 JSON 数组:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
[
|
|
30
|
+
{ "layout": "cover", "title": "标题", "subtitle": "副标题", "kicker": "开场", "notes": "开场先自我介绍 30 秒" },
|
|
31
|
+
{ "layout": "section", "kicker": "01", "title": "背景" },
|
|
32
|
+
{ "layout": "bullets", "title": "三个论点", "bullets": ["论点一", "论点二", "论点三"] },
|
|
33
|
+
{ "layout": "table", "title": "业绩对比", "rows": [["大区", "营收"], ["华东", "1.2 亿"]], "notes": "强调华东增速" },
|
|
34
|
+
{ "layout": "quote", "title": "慢就是快。", "subtitle": "创始人" },
|
|
35
|
+
{ "layout": "statement", "title": "核心观点一句话", "subtitle": "支撑说明" },
|
|
36
|
+
{ "layout": "closing", "title": "谢谢", "subtitle": "行动号召" }
|
|
37
|
+
]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`layout` 支持 `cover | section | bullets | statement | quote | table | closing | image | image-left | image-right | chart`;每页可选 `notes`,表格用 `rows`(首行为表头)。结构化表格自动分页,备注只放第一页。分页后超过 maxSlides 会报错,最多120页。图文、图表、品牌及按页编辑见 `advanced.md`。
|
|
41
|
+
|
|
42
|
+
## 动效(motion)
|
|
43
|
+
|
|
44
|
+
默认 `motion: on`:HTML 放映有页间淡入与要点逐条入场;PPTX 每页原生淡入转场,`bullets` 页的要点按「点击一次出现一条」放映。交付严肃/打印场景或用户明确要静态时传 `motion: 'off'`(CLI `--motion off`),产物不含任何动画代码。
|
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
# dsh-ppt 内置主题参考
|
|
2
|
-
|
|
3
|
-
主题系统移植自 hyperframes 技能的 `visual-styles.md` 视觉风格库。每套主题在 HTML 与 PPTX 中使用同一色板、同一字体族、同一情绪规则。
|
|
4
|
-
|
|
5
|
-
> 想先看效果:运行 `ppt_themes { preview: true, outputDir: "dist" }`,会生成 `themes-preview.html` 与每套主题一张 `theme-<id>.svg`;README/npm 首屏可以直接引用 SVG。
|
|
6
|
-
|
|
7
|
-
## 速查
|
|
8
|
-
|
|
9
|
-
| ID | 中文名 | English | 明暗 | 底色 | 前景 | 主强调 | 次强调 | 情绪 | 适用 |
|
|
10
|
-
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
11
|
-
| `data` | 数据漂移 | Data Drift | 暗 | `#070B14` | `#E8F1FF` | `#7C3AED` | `#06B6D4` | 未来/沉浸 | AI、数据、研究、技术发布 |
|
|
12
|
-
| `swiss` | 瑞士脉冲 | Swiss Pulse | 暗 | `#10151B` | `#F5F7FA` | `#2F6BFF` | `#FFB300` | 精准/理性 | SaaS、数据、开发者工具 |
|
|
13
|
-
| `velvet` | 天鹅绒标准 | Velvet Standard | 暗 | `#111316` | `#F4EFE6` | `#C9A84C` | `#3D4A63` | 高级/克制 | 高管汇报、品牌、融资路演 |
|
|
14
|
-
| `soft` | 柔和信号 | Soft Signal | 亮 | `#FFF8EC` | `#3B2F2A` | `#E58A2F` | `#8FAF8C` | 温暖/人本 | 品牌故事、培训、个人分享 |
|
|
15
|
-
| `bold` | 极繁大字 | Maximalist Type | 暗 | `#0D0D0D` | `#FFFFFF` | `#E63946` | `#FFD60A` | 大声/动能 | 产品发布、活动、大事件 |
|
|
16
|
-
|
|
17
|
-
## 逐主题说明
|
|
18
|
-
|
|
19
|
-
### data — 数据漂移(默认)
|
|
20
|
-
|
|
21
|
-
- 深黑底 + 电光紫 + 青色,细体未来感。
|
|
22
|
-
- 适合 AI / ML / 数据 / 前沿技术;大标题少字,用对比数字。
|
|
23
|
-
- 反模式:不要加多余暖色;不要堆长段落。
|
|
24
|
-
|
|
25
|
-
### swiss — 瑞士脉冲
|
|
26
|
-
|
|
27
|
-
- 黑白灰 + 一个电蓝(或琥珀),栅格感、大数字。
|
|
28
|
-
- 适合 SaaS / 指标 / 开发者工具;标题短、要点像 spec。
|
|
29
|
-
- 反模式:不要装饰性渐变文字;不要圆角卡片堆叠。
|
|
30
|
-
|
|
31
|
-
### velvet — 天鹅绒标准
|
|
32
|
-
|
|
33
|
-
- 黑、米白、香槟金,衬线标题 + 无衬线正文,宽字距,留白多。
|
|
34
|
-
- 适合高管汇报 / 品牌 / 融资;节奏慢,页数少而精。
|
|
35
|
-
- 反模式:不要动画感强烈的颜色;不要超过一个强调色。
|
|
36
|
-
|
|
37
|
-
### soft — 柔和信号
|
|
38
|
-
|
|
39
|
-
- 暖奶油底、深棕字、琥珀 + 鼠尾草绿,亮色唯一浅底主题。
|
|
40
|
-
- 适合品牌故事 / 培训 / 个人分享;文字可以更口语。
|
|
41
|
-
- 反模式:不要用纯黑正文;不要高饱和撞色。
|
|
42
|
-
|
|
43
|
-
### bold — 极繁大字
|
|
44
|
-
|
|
45
|
-
- 黑底、正红 + 明黄,超粗标题,字就是视觉主体。
|
|
46
|
-
- 适合产品发布 / 活动 / 大事件;短标题、单页单一冲击。
|
|
47
|
-
- 反模式:不要把页面塞满要点;不要让红黄之外再出现第三种强调色。
|
|
48
|
-
|
|
49
|
-
## 选主题决策树
|
|
50
|
-
|
|
51
|
-
1. 情绪是未来/科技 → `data`
|
|
52
|
-
2. 情绪是理性/数据/精准 → `swiss`
|
|
53
|
-
3. 情绪是高级/信任/正式 → `velvet`
|
|
54
|
-
4. 情绪是温暖/人本/故事 → `soft`
|
|
55
|
-
5. 情绪是大音量/发布会 → `bold`
|
|
56
|
-
|
|
57
|
-
无法判断时用 `data`,不要自造主题 ID。
|
|
58
|
-
|
|
59
|
-
## 视觉引擎复用(hyperframes)
|
|
60
|
-
|
|
61
|
-
如果 `hyperframes` 技能已安装,生成前先读它的 `visual-styles.md` / `house-style.md` 作为视觉判断依据;本技能的 5 套主题就是把其中 Swiss Pulse / Velvet Standard / Data Drift / Soft Signal / Maximalist Type 五套风格落成确定性 HTML+PPTX 实现。需要高级动画视频版时,才转用 hyperframes;普通演示不要上视频引擎。
|
|
1
|
+
# dsh-ppt 内置主题参考
|
|
2
|
+
|
|
3
|
+
主题系统移植自 hyperframes 技能的 `visual-styles.md` 视觉风格库。每套主题在 HTML 与 PPTX 中使用同一色板、同一字体族、同一情绪规则。
|
|
4
|
+
|
|
5
|
+
> 想先看效果:运行 `ppt_themes { preview: true, outputDir: "dist" }`,会生成 `themes-preview.html` 与每套主题一张 `theme-<id>.svg`;README/npm 首屏可以直接引用 SVG。
|
|
6
|
+
|
|
7
|
+
## 速查
|
|
8
|
+
|
|
9
|
+
| ID | 中文名 | English | 明暗 | 底色 | 前景 | 主强调 | 次强调 | 情绪 | 适用 |
|
|
10
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
11
|
+
| `data` | 数据漂移 | Data Drift | 暗 | `#070B14` | `#E8F1FF` | `#7C3AED` | `#06B6D4` | 未来/沉浸 | AI、数据、研究、技术发布 |
|
|
12
|
+
| `swiss` | 瑞士脉冲 | Swiss Pulse | 暗 | `#10151B` | `#F5F7FA` | `#2F6BFF` | `#FFB300` | 精准/理性 | SaaS、数据、开发者工具 |
|
|
13
|
+
| `velvet` | 天鹅绒标准 | Velvet Standard | 暗 | `#111316` | `#F4EFE6` | `#C9A84C` | `#3D4A63` | 高级/克制 | 高管汇报、品牌、融资路演 |
|
|
14
|
+
| `soft` | 柔和信号 | Soft Signal | 亮 | `#FFF8EC` | `#3B2F2A` | `#E58A2F` | `#8FAF8C` | 温暖/人本 | 品牌故事、培训、个人分享 |
|
|
15
|
+
| `bold` | 极繁大字 | Maximalist Type | 暗 | `#0D0D0D` | `#FFFFFF` | `#E63946` | `#FFD60A` | 大声/动能 | 产品发布、活动、大事件 |
|
|
16
|
+
|
|
17
|
+
## 逐主题说明
|
|
18
|
+
|
|
19
|
+
### data — 数据漂移(默认)
|
|
20
|
+
|
|
21
|
+
- 深黑底 + 电光紫 + 青色,细体未来感。
|
|
22
|
+
- 适合 AI / ML / 数据 / 前沿技术;大标题少字,用对比数字。
|
|
23
|
+
- 反模式:不要加多余暖色;不要堆长段落。
|
|
24
|
+
|
|
25
|
+
### swiss — 瑞士脉冲
|
|
26
|
+
|
|
27
|
+
- 黑白灰 + 一个电蓝(或琥珀),栅格感、大数字。
|
|
28
|
+
- 适合 SaaS / 指标 / 开发者工具;标题短、要点像 spec。
|
|
29
|
+
- 反模式:不要装饰性渐变文字;不要圆角卡片堆叠。
|
|
30
|
+
|
|
31
|
+
### velvet — 天鹅绒标准
|
|
32
|
+
|
|
33
|
+
- 黑、米白、香槟金,衬线标题 + 无衬线正文,宽字距,留白多。
|
|
34
|
+
- 适合高管汇报 / 品牌 / 融资;节奏慢,页数少而精。
|
|
35
|
+
- 反模式:不要动画感强烈的颜色;不要超过一个强调色。
|
|
36
|
+
|
|
37
|
+
### soft — 柔和信号
|
|
38
|
+
|
|
39
|
+
- 暖奶油底、深棕字、琥珀 + 鼠尾草绿,亮色唯一浅底主题。
|
|
40
|
+
- 适合品牌故事 / 培训 / 个人分享;文字可以更口语。
|
|
41
|
+
- 反模式:不要用纯黑正文;不要高饱和撞色。
|
|
42
|
+
|
|
43
|
+
### bold — 极繁大字
|
|
44
|
+
|
|
45
|
+
- 黑底、正红 + 明黄,超粗标题,字就是视觉主体。
|
|
46
|
+
- 适合产品发布 / 活动 / 大事件;短标题、单页单一冲击。
|
|
47
|
+
- 反模式:不要把页面塞满要点;不要让红黄之外再出现第三种强调色。
|
|
48
|
+
|
|
49
|
+
## 选主题决策树
|
|
50
|
+
|
|
51
|
+
1. 情绪是未来/科技 → `data`
|
|
52
|
+
2. 情绪是理性/数据/精准 → `swiss`
|
|
53
|
+
3. 情绪是高级/信任/正式 → `velvet`
|
|
54
|
+
4. 情绪是温暖/人本/故事 → `soft`
|
|
55
|
+
5. 情绪是大音量/发布会 → `bold`
|
|
56
|
+
|
|
57
|
+
无法判断时用 `data`,不要自造主题 ID。
|
|
58
|
+
|
|
59
|
+
## 视觉引擎复用(hyperframes)
|
|
60
|
+
|
|
61
|
+
如果 `hyperframes` 技能已安装,生成前先读它的 `visual-styles.md` / `house-style.md` 作为视觉判断依据;本技能的 5 套主题就是把其中 Swiss Pulse / Velvet Standard / Data Drift / Soft Signal / Maximalist Type 五套风格落成确定性 HTML+PPTX 实现。需要高级动画视频版时,才转用 hyperframes;普通演示不要上视频引擎。
|
|
@@ -1,184 +1,184 @@
|
|
|
1
|
-
#! /usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* build-deck.mjs —— dsh-ppt 的跨 harness 裸 CLI。
|
|
4
|
-
*
|
|
5
|
-
* DSH 插件内请优先使用 ppt_create 工具;把 skills/dsh-ppt 目录复制到
|
|
6
|
-
* Claude Code / Cursor / Gemini CLI / Codex 等 agent 时,用本脚本生成三件套。
|
|
7
|
-
*
|
|
8
|
-
* 示例:
|
|
9
|
-
* node build-deck.mjs --title "产品发布" --content "deck.md" --theme data --lang zh --out dist/deck
|
|
10
|
-
* node build-deck.mjs --title "Pitch" --content "一句话介绍我们的产品。" --theme velvet --lang bilingual
|
|
11
|
-
* node build-deck.mjs --list-themes
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
15
|
-
import { resolve as resolvePath } from 'node:path'
|
|
16
|
-
import { pathToFileURL } from 'node:url'
|
|
17
|
-
import { listThemes, THEME_IDS, DEFAULT_THEME } from './deck-core.mjs'
|
|
18
|
-
import { buildDeckAsync, listTemplates, editDeck, undoDeck, checkDeckAsync, renderDeck } from './deck-advanced.mjs'
|
|
19
|
-
|
|
20
|
-
const HELP = `dsh-ppt —— 一句话 / 一篇文档 → HTML 放映 + PPTX 导出
|
|
21
|
-
|
|
22
|
-
用法:
|
|
23
|
-
node build-deck.mjs --title <标题> --content <Markdown 或文件路径> [选项]
|
|
24
|
-
|
|
25
|
-
选项:
|
|
26
|
-
--title <text> 演示文稿标题(必填)
|
|
27
|
-
--content <text|@path> Markdown 正文;或文件路径(自动读取);或 @- 从 stdin 读
|
|
28
|
-
--slides <json> 结构化 slides JSON(可选,与 --content 二选一)
|
|
29
|
-
--template <id> weekly / defense / project / pitch;仅模板会生成草稿
|
|
30
|
-
--brand <json|@path> 品牌颜色、字体、Logo、页脚
|
|
31
|
-
--edit <deck.json> 修改指定页,配合 --edits <json|@path>
|
|
32
|
-
--undo <deck.json> 撤销最近一次修改
|
|
33
|
-
--check <deck.json> 查看质量报告和页码 / ID
|
|
34
|
-
--render <deck.pptx> 用可选官方引擎生成逐页 PNG / PDF
|
|
35
|
-
--format <png|pdf|both> 渲染格式,默认 png
|
|
36
|
-
--width <n> PNG 目标宽度,默认 1440
|
|
37
|
-
--timeout <ms> 渲染整体期限,默认 120000 毫秒
|
|
38
|
-
--render-receipt <path> 配合 --check 核验当前渲染收据
|
|
39
|
-
--revision <n> 编辑 / 撤销预期修订号,防止覆盖他人修改
|
|
40
|
-
--list-templates 列出场景结构参考
|
|
41
|
-
--theme <id> 视觉主题:${THEME_IDS.join(' / ')}(默认 ${DEFAULT_THEME})
|
|
42
|
-
--lang <id> 界面语言:zh / en / bilingual(默认 zh)
|
|
43
|
-
--motion <on|off> 页间转场与要点入场动画(默认 on;off 产出纯静态)
|
|
44
|
-
--out <dir> 输出目录(默认当前目录)
|
|
45
|
-
--file <name> 文件名前缀(默认取标题)
|
|
46
|
-
--overwrite 显式覆盖同名三件套(默认自动追加 -1/-2…)
|
|
47
|
-
--list-themes 列出内置主题
|
|
48
|
-
--help 显示本帮助
|
|
49
|
-
|
|
50
|
-
输出:
|
|
51
|
-
<file>.html 独立网页放映(无外链,可直接双击打开)
|
|
52
|
-
<file>.pptx 可编辑 PPTX(16:9)
|
|
53
|
-
<file>.json deck manifest
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
export function parseArgv(argv) {
|
|
57
|
-
const args = { content: '', slides: null, lang: 'zh', theme: undefined, motion: undefined, out: '.', file: '', title: '', overwrite: false }
|
|
58
|
-
const positional = []
|
|
59
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
60
|
-
const raw = String(argv[i])
|
|
61
|
-
const eq = raw.indexOf('=')
|
|
62
|
-
const key = eq >= 0 ? raw.slice(0, eq) : raw
|
|
63
|
-
const inline = eq >= 0 ? raw.slice(eq + 1) : null
|
|
64
|
-
const nextValue = () => {
|
|
65
|
-
if (inline !== null) return inline
|
|
66
|
-
if (i + 1 >= argv.length) throw new Error('dsh-ppt CLI:' + key + ' 需要一个值')
|
|
67
|
-
i += 1
|
|
68
|
-
return String(argv[i])
|
|
69
|
-
}
|
|
70
|
-
if (key === '--help' || key === '-h') args.help = true
|
|
71
|
-
else if (key === '--list-themes') args.listThemes = true
|
|
72
|
-
else if (key === '--list-templates') args.listTemplates = true
|
|
73
|
-
else if (key === '--overwrite') args.overwrite = true
|
|
74
|
-
else if (key === '--title') args.title = nextValue()
|
|
75
|
-
else if (key === '--content') args.content = nextValue()
|
|
76
|
-
else if (key === '--slides') args.slides = readJson(nextValue())
|
|
77
|
-
else if (key === '--template') args.template = nextValue()
|
|
78
|
-
else if (key === '--brand') args.brand = readJson(nextValue())
|
|
79
|
-
else if (key === '--edit') args.edit = nextValue()
|
|
80
|
-
else if (key === '--edits') args.edits = readJson(nextValue())
|
|
81
|
-
else if (key === '--undo') args.undo = nextValue()
|
|
82
|
-
else if (key === '--check') args.check = nextValue()
|
|
83
|
-
else if (key === '--render') args.render = nextValue()
|
|
84
|
-
else if (key === '--format') args.format = nextValue()
|
|
85
|
-
else if (key === '--width') args.width = Number(nextValue())
|
|
86
|
-
else if (key === '--timeout') args.timeoutMs = Number(nextValue())
|
|
87
|
-
else if (key === '--render-receipt') args.renderReceipt = nextValue()
|
|
88
|
-
else if (key === '--revision') args.revision = Number(nextValue())
|
|
89
|
-
else if (key === '--theme') args.theme = nextValue()
|
|
90
|
-
else if (key === '--lang') args.lang = nextValue()
|
|
91
|
-
else if (key === '--motion') args.motion = nextValue()
|
|
92
|
-
else if (key === '--out') { args.out = nextValue(); args.explicitOut = true }
|
|
93
|
-
else if (key === '--file') args.file = nextValue()
|
|
94
|
-
else positional.push(raw)
|
|
95
|
-
}
|
|
96
|
-
args.positional = positional
|
|
97
|
-
return args
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function readJson(value) { return JSON.parse(value.startsWith('@') ? readFileSync(resolvePath(value.slice(1)), 'utf8') : value) }
|
|
101
|
-
|
|
102
|
-
async function readStdin(stream) {
|
|
103
|
-
let text = ''
|
|
104
|
-
stream.setEncoding('utf8')
|
|
105
|
-
for await (const chunk of stream) text += chunk
|
|
106
|
-
return text
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export async function main(argv = process.argv.slice(2), io = {}) {
|
|
110
|
-
const log = io.log ?? ((message) => console.log(message))
|
|
111
|
-
const args = parseArgv(argv)
|
|
112
|
-
|
|
113
|
-
if (args.help) {
|
|
114
|
-
log(HELP.trim())
|
|
115
|
-
return 0
|
|
116
|
-
}
|
|
117
|
-
if (args.listThemes) {
|
|
118
|
-
for (const theme of listThemes('zh')) {
|
|
119
|
-
log('- ' + theme.id.padEnd(8) + theme.name + '|' + theme.mood + '|适合:' + theme.bestFor)
|
|
120
|
-
}
|
|
121
|
-
return 0
|
|
122
|
-
}
|
|
123
|
-
if (args.listTemplates) { log(JSON.stringify({ templates: listTemplates(args.lang) }, null, 2)); return 0 }
|
|
124
|
-
if ([args.edit, args.undo, args.check, args.render].filter(Boolean).length > 1) throw new Error('dsh-ppt CLI:--edit / --undo / --check / --render 只能选择一项')
|
|
125
|
-
if (args.renderReceipt !== undefined && !args.check) throw new Error('dsh-ppt CLI:--render-receipt 需配合 --check')
|
|
126
|
-
if (!args.render && [args.format, args.width, args.timeoutMs].some(v => v !== undefined)) throw new Error('dsh-ppt CLI:--format / --width / --timeout 需配合 --render')
|
|
127
|
-
if (args.check) {
|
|
128
|
-
const report = await checkDeckAsync({ deckPath: args.check, renderReceipt: args.renderReceipt })
|
|
129
|
-
log(JSON.stringify(report, null, 2)); return report.ok && (args.renderReceipt === undefined || report.render?.status === 'rendered') ? 0 : 1
|
|
130
|
-
}
|
|
131
|
-
if (args.render) {
|
|
132
|
-
const rendered = await (io.renderDeck ?? renderDeck)({ pptxPath: args.render, ...(args.explicitOut ? { outputDir: args.out } : {}), format: args.format, width: args.width, timeoutMs: args.timeoutMs })
|
|
133
|
-
log(JSON.stringify(rendered, null, 2)); return rendered.ok ? 0 : 1
|
|
134
|
-
}
|
|
135
|
-
if (args.edit || args.undo) {
|
|
136
|
-
const result = args.edit ? await editDeck({ deckPath: args.edit, edits: args.edits, brand: args.brand, expectedRevision: args.revision })
|
|
137
|
-
: await undoDeck({ deckPath: args.undo, expectedRevision: args.revision })
|
|
138
|
-
log(JSON.stringify(result, null, 2)); return 0
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
let content = String(args.content ?? '')
|
|
142
|
-
let slides = args.slides
|
|
143
|
-
if (content === '@-') {
|
|
144
|
-
content = await readStdin(io.stdin ?? process.stdin)
|
|
145
|
-
} else if (content.startsWith('@')) {
|
|
146
|
-
content = readFileSync(resolvePath(content.slice(1)), 'utf8')
|
|
147
|
-
} else if (content.trim() !== '' && existsSync(resolvePath(content.trim()))) {
|
|
148
|
-
// README / SKILL.md 的示例直接传文件路径(不带 @)时也按文件读取
|
|
149
|
-
content = readFileSync(resolvePath(content.trim()), 'utf8')
|
|
150
|
-
} else if (content.trim() === '' && slides === null && !args.template) {
|
|
151
|
-
throw new Error('dsh-ppt CLI:--content 不能为空(或用 --slides 传结构化幻灯片)')
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const options = {
|
|
155
|
-
title: args.title,
|
|
156
|
-
content,
|
|
157
|
-
slides,
|
|
158
|
-
theme: args.theme,
|
|
159
|
-
lang: args.lang,
|
|
160
|
-
motion: args.motion,
|
|
161
|
-
outputDir: resolvePath(args.out || '.'),
|
|
162
|
-
fileName: args.file,
|
|
163
|
-
overwrite: args.overwrite,
|
|
164
|
-
template: args.template,
|
|
165
|
-
brand: args.brand,
|
|
166
|
-
}
|
|
167
|
-
const result = await buildDeckAsync(options)
|
|
168
|
-
log('dsh-ppt 已生成 ' + result.slideCount + ' 页演示文稿:')
|
|
169
|
-
log(' HTML 放映:' + result.htmlPath)
|
|
170
|
-
log(' PPTX 导出:' + result.pptxPath)
|
|
171
|
-
log(' Manifest :' + result.jsonPath)
|
|
172
|
-
if (result.deliveryStatus === 'draft') log(' 草稿:还有待填写内容,不可作为成品交付。')
|
|
173
|
-
log(' 质量检查:' + result.quality.errorCount + ' 个错误 / ' + result.quality.warningCount + ' 个提醒;修订号 ' + result.revision)
|
|
174
|
-
return 0
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
178
|
-
main().then((code) => {
|
|
179
|
-
process.exitCode = code ?? 0
|
|
180
|
-
}).catch((err) => {
|
|
181
|
-
console.error('[dsh-ppt] ' + (err instanceof Error ? err.message : String(err)))
|
|
182
|
-
process.exitCode = 1
|
|
183
|
-
})
|
|
184
|
-
}
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* build-deck.mjs —— dsh-ppt 的跨 harness 裸 CLI。
|
|
4
|
+
*
|
|
5
|
+
* DSH 插件内请优先使用 ppt_create 工具;把 skills/dsh-ppt 目录复制到
|
|
6
|
+
* Claude Code / Cursor / Gemini CLI / Codex 等 agent 时,用本脚本生成三件套。
|
|
7
|
+
*
|
|
8
|
+
* 示例:
|
|
9
|
+
* node build-deck.mjs --title "产品发布" --content "deck.md" --theme data --lang zh --out dist/deck
|
|
10
|
+
* node build-deck.mjs --title "Pitch" --content "一句话介绍我们的产品。" --theme velvet --lang bilingual
|
|
11
|
+
* node build-deck.mjs --list-themes
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
15
|
+
import { resolve as resolvePath } from 'node:path'
|
|
16
|
+
import { pathToFileURL } from 'node:url'
|
|
17
|
+
import { listThemes, THEME_IDS, DEFAULT_THEME } from './deck-core.mjs'
|
|
18
|
+
import { buildDeckAsync, listTemplates, editDeck, undoDeck, checkDeckAsync, renderDeck } from './deck-advanced.mjs'
|
|
19
|
+
|
|
20
|
+
const HELP = `dsh-ppt —— 一句话 / 一篇文档 → HTML 放映 + PPTX 导出
|
|
21
|
+
|
|
22
|
+
用法:
|
|
23
|
+
node build-deck.mjs --title <标题> --content <Markdown 或文件路径> [选项]
|
|
24
|
+
|
|
25
|
+
选项:
|
|
26
|
+
--title <text> 演示文稿标题(必填)
|
|
27
|
+
--content <text|@path> Markdown 正文;或文件路径(自动读取);或 @- 从 stdin 读
|
|
28
|
+
--slides <json> 结构化 slides JSON(可选,与 --content 二选一)
|
|
29
|
+
--template <id> weekly / defense / project / pitch;仅模板会生成草稿
|
|
30
|
+
--brand <json|@path> 品牌颜色、字体、Logo、页脚
|
|
31
|
+
--edit <deck.json> 修改指定页,配合 --edits <json|@path>
|
|
32
|
+
--undo <deck.json> 撤销最近一次修改
|
|
33
|
+
--check <deck.json> 查看质量报告和页码 / ID
|
|
34
|
+
--render <deck.pptx> 用可选官方引擎生成逐页 PNG / PDF
|
|
35
|
+
--format <png|pdf|both> 渲染格式,默认 png
|
|
36
|
+
--width <n> PNG 目标宽度,默认 1440
|
|
37
|
+
--timeout <ms> 渲染整体期限,默认 120000 毫秒
|
|
38
|
+
--render-receipt <path> 配合 --check 核验当前渲染收据
|
|
39
|
+
--revision <n> 编辑 / 撤销预期修订号,防止覆盖他人修改
|
|
40
|
+
--list-templates 列出场景结构参考
|
|
41
|
+
--theme <id> 视觉主题:${THEME_IDS.join(' / ')}(默认 ${DEFAULT_THEME})
|
|
42
|
+
--lang <id> 界面语言:zh / en / bilingual(默认 zh)
|
|
43
|
+
--motion <on|off> 页间转场与要点入场动画(默认 on;off 产出纯静态)
|
|
44
|
+
--out <dir> 输出目录(默认当前目录)
|
|
45
|
+
--file <name> 文件名前缀(默认取标题)
|
|
46
|
+
--overwrite 显式覆盖同名三件套(默认自动追加 -1/-2…)
|
|
47
|
+
--list-themes 列出内置主题
|
|
48
|
+
--help 显示本帮助
|
|
49
|
+
|
|
50
|
+
输出:
|
|
51
|
+
<file>.html 独立网页放映(无外链,可直接双击打开)
|
|
52
|
+
<file>.pptx 可编辑 PPTX(16:9)
|
|
53
|
+
<file>.json deck manifest
|
|
54
|
+
`
|
|
55
|
+
|
|
56
|
+
export function parseArgv(argv) {
|
|
57
|
+
const args = { content: '', slides: null, lang: 'zh', theme: undefined, motion: undefined, out: '.', file: '', title: '', overwrite: false }
|
|
58
|
+
const positional = []
|
|
59
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
60
|
+
const raw = String(argv[i])
|
|
61
|
+
const eq = raw.indexOf('=')
|
|
62
|
+
const key = eq >= 0 ? raw.slice(0, eq) : raw
|
|
63
|
+
const inline = eq >= 0 ? raw.slice(eq + 1) : null
|
|
64
|
+
const nextValue = () => {
|
|
65
|
+
if (inline !== null) return inline
|
|
66
|
+
if (i + 1 >= argv.length) throw new Error('dsh-ppt CLI:' + key + ' 需要一个值')
|
|
67
|
+
i += 1
|
|
68
|
+
return String(argv[i])
|
|
69
|
+
}
|
|
70
|
+
if (key === '--help' || key === '-h') args.help = true
|
|
71
|
+
else if (key === '--list-themes') args.listThemes = true
|
|
72
|
+
else if (key === '--list-templates') args.listTemplates = true
|
|
73
|
+
else if (key === '--overwrite') args.overwrite = true
|
|
74
|
+
else if (key === '--title') args.title = nextValue()
|
|
75
|
+
else if (key === '--content') args.content = nextValue()
|
|
76
|
+
else if (key === '--slides') args.slides = readJson(nextValue())
|
|
77
|
+
else if (key === '--template') args.template = nextValue()
|
|
78
|
+
else if (key === '--brand') args.brand = readJson(nextValue())
|
|
79
|
+
else if (key === '--edit') args.edit = nextValue()
|
|
80
|
+
else if (key === '--edits') args.edits = readJson(nextValue())
|
|
81
|
+
else if (key === '--undo') args.undo = nextValue()
|
|
82
|
+
else if (key === '--check') args.check = nextValue()
|
|
83
|
+
else if (key === '--render') args.render = nextValue()
|
|
84
|
+
else if (key === '--format') args.format = nextValue()
|
|
85
|
+
else if (key === '--width') args.width = Number(nextValue())
|
|
86
|
+
else if (key === '--timeout') args.timeoutMs = Number(nextValue())
|
|
87
|
+
else if (key === '--render-receipt') args.renderReceipt = nextValue()
|
|
88
|
+
else if (key === '--revision') args.revision = Number(nextValue())
|
|
89
|
+
else if (key === '--theme') args.theme = nextValue()
|
|
90
|
+
else if (key === '--lang') args.lang = nextValue()
|
|
91
|
+
else if (key === '--motion') args.motion = nextValue()
|
|
92
|
+
else if (key === '--out') { args.out = nextValue(); args.explicitOut = true }
|
|
93
|
+
else if (key === '--file') args.file = nextValue()
|
|
94
|
+
else positional.push(raw)
|
|
95
|
+
}
|
|
96
|
+
args.positional = positional
|
|
97
|
+
return args
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readJson(value) { return JSON.parse(value.startsWith('@') ? readFileSync(resolvePath(value.slice(1)), 'utf8') : value) }
|
|
101
|
+
|
|
102
|
+
async function readStdin(stream) {
|
|
103
|
+
let text = ''
|
|
104
|
+
stream.setEncoding('utf8')
|
|
105
|
+
for await (const chunk of stream) text += chunk
|
|
106
|
+
return text
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function main(argv = process.argv.slice(2), io = {}) {
|
|
110
|
+
const log = io.log ?? ((message) => console.log(message))
|
|
111
|
+
const args = parseArgv(argv)
|
|
112
|
+
|
|
113
|
+
if (args.help) {
|
|
114
|
+
log(HELP.trim())
|
|
115
|
+
return 0
|
|
116
|
+
}
|
|
117
|
+
if (args.listThemes) {
|
|
118
|
+
for (const theme of listThemes('zh')) {
|
|
119
|
+
log('- ' + theme.id.padEnd(8) + theme.name + '|' + theme.mood + '|适合:' + theme.bestFor)
|
|
120
|
+
}
|
|
121
|
+
return 0
|
|
122
|
+
}
|
|
123
|
+
if (args.listTemplates) { log(JSON.stringify({ templates: listTemplates(args.lang) }, null, 2)); return 0 }
|
|
124
|
+
if ([args.edit, args.undo, args.check, args.render].filter(Boolean).length > 1) throw new Error('dsh-ppt CLI:--edit / --undo / --check / --render 只能选择一项')
|
|
125
|
+
if (args.renderReceipt !== undefined && !args.check) throw new Error('dsh-ppt CLI:--render-receipt 需配合 --check')
|
|
126
|
+
if (!args.render && [args.format, args.width, args.timeoutMs].some(v => v !== undefined)) throw new Error('dsh-ppt CLI:--format / --width / --timeout 需配合 --render')
|
|
127
|
+
if (args.check) {
|
|
128
|
+
const report = await checkDeckAsync({ deckPath: args.check, renderReceipt: args.renderReceipt })
|
|
129
|
+
log(JSON.stringify(report, null, 2)); return report.ok && (args.renderReceipt === undefined || report.render?.status === 'rendered') ? 0 : 1
|
|
130
|
+
}
|
|
131
|
+
if (args.render) {
|
|
132
|
+
const rendered = await (io.renderDeck ?? renderDeck)({ pptxPath: args.render, ...(args.explicitOut ? { outputDir: args.out } : {}), format: args.format, width: args.width, timeoutMs: args.timeoutMs })
|
|
133
|
+
log(JSON.stringify(rendered, null, 2)); return rendered.ok ? 0 : 1
|
|
134
|
+
}
|
|
135
|
+
if (args.edit || args.undo) {
|
|
136
|
+
const result = args.edit ? await editDeck({ deckPath: args.edit, edits: args.edits, brand: args.brand, expectedRevision: args.revision })
|
|
137
|
+
: await undoDeck({ deckPath: args.undo, expectedRevision: args.revision })
|
|
138
|
+
log(JSON.stringify(result, null, 2)); return 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let content = String(args.content ?? '')
|
|
142
|
+
let slides = args.slides
|
|
143
|
+
if (content === '@-') {
|
|
144
|
+
content = await readStdin(io.stdin ?? process.stdin)
|
|
145
|
+
} else if (content.startsWith('@')) {
|
|
146
|
+
content = readFileSync(resolvePath(content.slice(1)), 'utf8')
|
|
147
|
+
} else if (content.trim() !== '' && existsSync(resolvePath(content.trim()))) {
|
|
148
|
+
// README / SKILL.md 的示例直接传文件路径(不带 @)时也按文件读取
|
|
149
|
+
content = readFileSync(resolvePath(content.trim()), 'utf8')
|
|
150
|
+
} else if (content.trim() === '' && slides === null && !args.template) {
|
|
151
|
+
throw new Error('dsh-ppt CLI:--content 不能为空(或用 --slides 传结构化幻灯片)')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const options = {
|
|
155
|
+
title: args.title,
|
|
156
|
+
content,
|
|
157
|
+
slides,
|
|
158
|
+
theme: args.theme,
|
|
159
|
+
lang: args.lang,
|
|
160
|
+
motion: args.motion,
|
|
161
|
+
outputDir: resolvePath(args.out || '.'),
|
|
162
|
+
fileName: args.file,
|
|
163
|
+
overwrite: args.overwrite,
|
|
164
|
+
template: args.template,
|
|
165
|
+
brand: args.brand,
|
|
166
|
+
}
|
|
167
|
+
const result = await buildDeckAsync(options)
|
|
168
|
+
log('dsh-ppt 已生成 ' + result.slideCount + ' 页演示文稿:')
|
|
169
|
+
log(' HTML 放映:' + result.htmlPath)
|
|
170
|
+
log(' PPTX 导出:' + result.pptxPath)
|
|
171
|
+
log(' Manifest :' + result.jsonPath)
|
|
172
|
+
if (result.deliveryStatus === 'draft') log(' 草稿:还有待填写内容,不可作为成品交付。')
|
|
173
|
+
log(' 质量检查:' + result.quality.errorCount + ' 个错误 / ' + result.quality.warningCount + ' 个提醒;修订号 ' + result.revision)
|
|
174
|
+
return 0
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
178
|
+
main().then((code) => {
|
|
179
|
+
process.exitCode = code ?? 0
|
|
180
|
+
}).catch((err) => {
|
|
181
|
+
console.error('[dsh-ppt] ' + (err instanceof Error ? err.message : String(err)))
|
|
182
|
+
process.exitCode = 1
|
|
183
|
+
})
|
|
184
|
+
}
|