dsh-ppt 0.4.1 → 0.4.3
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 +18 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/package.json +70 -70
- package/skills/dsh-ppt/scripts/build-deck.mjs +4 -1
- package/skills/dsh-ppt/scripts/deck-core.mjs +139 -46
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.3(2026-09-18)
|
|
4
|
+
|
|
5
|
+
- 修复:中文分句导致正文丢内容;`--content deck.md` 不读文件;>8 条要点静默丢弃;`deck.json` 版本写死;英文 deck 被标 zh-CN。
|
|
6
|
+
- 稳健性:三件套改为 tmp + rename 原子提交(失败回滚);过滤 Windows 保留名;文件名探测改一次目录快照。
|
|
7
|
+
- 测试 48 → 60 项。
|
|
8
|
+
## 0.4.2(2026-09-11)
|
|
9
|
+
|
|
10
|
+
- 适配并验证官方 Harness 0.1.5-rc.1:整套同载、工具/技能契约与 Web 鉴权检查通过。
|
|
11
|
+
- Node 要求与宿主统一为 `^22.19.0 || >=24.0.0`;更新中英文兼容性说明。
|
|
12
|
+
|
|
13
|
+
## 0.4.1
|
|
14
|
+
|
|
15
|
+
- 修复中文正文分页:中英文标点都作为句子边界,无标题纯文本严格按「每 5 句一页」分页,不再把整段中文当一句而截断丢正文。
|
|
16
|
+
- CLI `--content <path>` 与 `--content @<path>` 行为一致:路径存在即读取文件,README / SKILL.md 示例可直接复制运行。
|
|
17
|
+
- 单节要点超过 8 条时自动续页(第二页起标题带「(续)」),不再静默丢弃第 9 条之后的要点。
|
|
18
|
+
- manifest `version` 改从 `package.json` 读取,消除与发版号的漂移;测试同步改为动态断言。
|
|
19
|
+
- PPTX 文本与备注 run 的语言跟随 `lang`(zh-CN / en-US),英文 deck 不再被 PowerPoint 标成中文。
|
|
20
|
+
|
|
3
21
|
## 0.4.0
|
|
4
22
|
|
|
5
23
|
- **动效(motion,默认开)**:HTML 放映页间淡入 + 要点按序逐条入场(打印自动兜底为静态);PPTX 每页原生淡入转场,`bullets` 页按段落构建、放映时点击一次出现一条要点(PowerPoint「出现,按段落」标准时序)。
|
package/README.en.md
CHANGED
|
@@ -30,7 +30,7 @@ Example:
|
|
|
30
30
|
|
|
31
31
|
## Compatibility
|
|
32
32
|
|
|
33
|
-
Verified
|
|
33
|
+
Verified with official `@deepseek-ai/dsh@0.1.5-rc.1` and Node `24.16.0` on 2026-09-11: all 18 components load alongside Modlens, with passing tool-schema, skill-registration and offline read-only invocation checks. Uses the `cordis.patch.yml` + `dsh.bundle.patch` bundle model. Node requirements match this Harness release: 22.19 or later within 22.x, or 24 or later. Live external-service workflows require separate configuration and validation.
|
|
34
34
|
|
|
35
35
|
## Installation
|
|
36
36
|
|
|
@@ -128,7 +128,7 @@ Output paths resolve against each tool call's `exec.agent.session.header.cwd`: o
|
|
|
128
128
|
|
|
129
129
|
- `lang` argument: `zh` (default) / `en` / `bilingual` controls the player UI, page numbers, and closing defaults.
|
|
130
130
|
- Content language is up to the author: for a bilingual deck, prefer "Chinese title + English subtitle" or generate two decks from the same outline.
|
|
131
|
-
- Plugin docs
|
|
131
|
+
- Plugin docs come in Chinese (README.md) and English (this file); CLI and skill error messages are in Chinese.
|
|
132
132
|
|
|
133
133
|
## Engineering quality
|
|
134
134
|
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ DSH(DeepSeek Harness)演示文稿技能 + 工具插件:把一句话、一
|
|
|
32
32
|
|
|
33
33
|
## 兼容性
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
已在官方 `@deepseek-ai/dsh@0.1.5-rc.1`、Node `24.16.0` 上验证(2026-09-11):18 个组件与 Modlens 同载,工具 schema、技能注册及离线只读调用检查通过。采用 `cordis.patch.yml` + `dsh.bundle.patch` 组合包模型。Node 要求与该版本 Harness 一致:22.19 及以上的 22.x,或 24 及以上。外部服务的实际业务操作需按各组件配置单独验证。
|
|
36
36
|
|
|
37
37
|
## 安装
|
|
38
38
|
|
|
@@ -130,7 +130,7 @@ dsh plugin --profile web remove dsh-ppt
|
|
|
130
130
|
|
|
131
131
|
- `lang` 参数:`zh`(默认)/ `en` / `bilingual`,控制播放器界面、页码与结束页默认文案。
|
|
132
132
|
- 内容语言由你撰写:双语 deck 推荐「中文标题 + 英文副标题」,或同一大纲分别生成中英两份。
|
|
133
|
-
-
|
|
133
|
+
- 插件文档提供中英两版(README.md / README.en.md);CLI 与技能的报错提示为中文。
|
|
134
134
|
|
|
135
135
|
## 工程质量
|
|
136
136
|
|
package/package.json
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-ppt",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "DSH
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"types": "lib/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./lib/index.d.ts",
|
|
11
|
-
"default": "./lib/index.js"
|
|
12
|
-
},
|
|
13
|
-
"./skills/*": "./skills/*",
|
|
14
|
-
"./deck-core": "./skills/dsh-ppt/scripts/deck-core.mjs",
|
|
15
|
-
"./build-deck": "./skills/dsh-ppt/scripts/build-deck.mjs",
|
|
16
|
-
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
17
|
-
"./package.json": "./package.json"
|
|
18
|
-
},
|
|
19
|
-
"files": [
|
|
20
|
-
"lib",
|
|
21
|
-
"skills",
|
|
22
|
-
"cordis.patch.yml",
|
|
23
|
-
"README.md",
|
|
24
|
-
"README.en.md",
|
|
25
|
-
"CHANGELOG.md",
|
|
26
|
-
"LICENSE"
|
|
27
|
-
],
|
|
28
|
-
"scripts": {
|
|
29
|
-
"build": "tsc -p tsconfig.json",
|
|
30
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
31
|
-
"test": "pnpm run build && node --test \"test/*.test.mjs\"",
|
|
32
|
-
"test:core": "node --test \"test/*.test.mjs\"",
|
|
33
|
-
"smoke:cli": "node skills/dsh-ppt/scripts/build-deck.mjs --title \"DSH PPT Smoke\" --content @smoke/sample-deck.md --theme data --lang zh --out .smoke-deck"
|
|
34
|
-
},
|
|
35
|
-
"dsh": {
|
|
36
|
-
"bundle": {
|
|
37
|
-
"patch": "./cordis.patch.yml"
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
"keywords": [
|
|
41
|
-
"dsh",
|
|
42
|
-
"deepseek-harness",
|
|
43
|
-
"dsh-plugin",
|
|
44
|
-
"plugin",
|
|
45
|
-
"skills",
|
|
46
|
-
"ppt",
|
|
47
|
-
"pptx",
|
|
48
|
-
"presentation",
|
|
49
|
-
"slides",
|
|
50
|
-
"html"
|
|
51
|
-
],
|
|
52
|
-
"license": "MIT",
|
|
53
|
-
"engines": {
|
|
54
|
-
"node": ">=
|
|
55
|
-
},
|
|
56
|
-
"devDependencies": {
|
|
57
|
-
"@types/node": "^24.0.0",
|
|
58
|
-
"typescript": "^5.6.0"
|
|
59
|
-
},
|
|
60
|
-
"repository": {
|
|
61
|
-
"type": "git",
|
|
62
|
-
"url": "git+https://github.com/STARDUSTLC666/dsh-ppt.git"
|
|
63
|
-
},
|
|
64
|
-
"bugs": {
|
|
65
|
-
"url": "https://github.com/STARDUSTLC666/dsh-ppt/issues"
|
|
66
|
-
},
|
|
67
|
-
"homepage": "https://github.com/STARDUSTLC666/dsh-ppt#readme",
|
|
68
|
-
"author": "stardustlc",
|
|
69
|
-
"packageManager": "pnpm@11.7.0"
|
|
70
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-ppt",
|
|
3
|
+
"version": "0.4.3",
|
|
4
|
+
"description": "DSH 演示文稿插件:一句话或一篇 Markdown → 网页放映 + 可编辑 PPTX,内置 5 套主题与转场动画。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./skills/*": "./skills/*",
|
|
14
|
+
"./deck-core": "./skills/dsh-ppt/scripts/deck-core.mjs",
|
|
15
|
+
"./build-deck": "./skills/dsh-ppt/scripts/build-deck.mjs",
|
|
16
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib",
|
|
21
|
+
"skills",
|
|
22
|
+
"cordis.patch.yml",
|
|
23
|
+
"README.md",
|
|
24
|
+
"README.en.md",
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.json",
|
|
30
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
31
|
+
"test": "pnpm run build && node --test \"test/*.test.mjs\"",
|
|
32
|
+
"test:core": "node --test \"test/*.test.mjs\"",
|
|
33
|
+
"smoke:cli": "node skills/dsh-ppt/scripts/build-deck.mjs --title \"DSH PPT Smoke\" --content @smoke/sample-deck.md --theme data --lang zh --out .smoke-deck"
|
|
34
|
+
},
|
|
35
|
+
"dsh": {
|
|
36
|
+
"bundle": {
|
|
37
|
+
"patch": "./cordis.patch.yml"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"dsh",
|
|
42
|
+
"deepseek-harness",
|
|
43
|
+
"dsh-plugin",
|
|
44
|
+
"plugin",
|
|
45
|
+
"skills",
|
|
46
|
+
"ppt",
|
|
47
|
+
"pptx",
|
|
48
|
+
"presentation",
|
|
49
|
+
"slides",
|
|
50
|
+
"html"
|
|
51
|
+
],
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^24.0.0",
|
|
58
|
+
"typescript": "^5.6.0"
|
|
59
|
+
},
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "git+https://github.com/STARDUSTLC666/dsh-ppt.git"
|
|
63
|
+
},
|
|
64
|
+
"bugs": {
|
|
65
|
+
"url": "https://github.com/STARDUSTLC666/dsh-ppt/issues"
|
|
66
|
+
},
|
|
67
|
+
"homepage": "https://github.com/STARDUSTLC666/dsh-ppt#readme",
|
|
68
|
+
"author": "stardustlc",
|
|
69
|
+
"packageManager": "pnpm@11.7.0"
|
|
70
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* node build-deck.mjs --list-themes
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { readFileSync } from 'node:fs'
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
15
15
|
import { resolve as resolvePath } from 'node:path'
|
|
16
16
|
import { pathToFileURL } from 'node:url'
|
|
17
17
|
import { buildDeck, listThemes, THEME_IDS, DEFAULT_THEME } from './deck-core.mjs'
|
|
@@ -99,6 +99,9 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
99
99
|
content = await readStdin(io.stdin ?? process.stdin)
|
|
100
100
|
} else if (content.startsWith('@')) {
|
|
101
101
|
content = readFileSync(resolvePath(content.slice(1)), 'utf8')
|
|
102
|
+
} else if (content.trim() !== '' && existsSync(resolvePath(content.trim()))) {
|
|
103
|
+
// README / SKILL.md 的示例直接传文件路径(不带 @)时也按文件读取
|
|
104
|
+
content = readFileSync(resolvePath(content.trim()), 'utf8')
|
|
102
105
|
} else if (content.trim() === '' && slides === null) {
|
|
103
106
|
throw new Error('dsh-ppt CLI:--content 不能为空(或用 --slides 传结构化幻灯片)')
|
|
104
107
|
}
|
|
@@ -14,11 +14,25 @@
|
|
|
14
14
|
* 零运行时依赖,仅使用 node:fs / node:path / node:zlib。
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
17
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
18
18
|
import { resolve as resolvePath } from 'node:path'
|
|
19
19
|
import { deflateRawSync } from 'node:zlib'
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* manifest 版本跟随发布包 package.json;把 skills/dsh-ppt 独立复制到其他
|
|
23
|
+
* harness(没有 package.json)时回退为 0.0.0,避免版本号悄悄漂移。
|
|
24
|
+
*/
|
|
25
|
+
function readDeckVersion() {
|
|
26
|
+
try {
|
|
27
|
+
const pkg = JSON.parse(readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'))
|
|
28
|
+
const version = String(pkg.version ?? '').trim()
|
|
29
|
+
return version !== '' ? version : '0.0.0'
|
|
30
|
+
} catch {
|
|
31
|
+
return '0.0.0'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const DECK_VERSION = readDeckVersion()
|
|
22
36
|
|
|
23
37
|
// ---------------------------------------------------------------------------
|
|
24
38
|
// 主题
|
|
@@ -239,13 +253,17 @@ export function clampInt(value, fallback, min, max) {
|
|
|
239
253
|
return Math.min(max, Math.max(min, n))
|
|
240
254
|
}
|
|
241
255
|
|
|
256
|
+
// Windows 保留设备名:首个点之前的主名命中即不可用(NUL、com1、aux.report.html 都算)。
|
|
257
|
+
const WINDOWS_RESERVED_FILE_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i
|
|
258
|
+
|
|
242
259
|
export function sanitizeFileName(input) {
|
|
243
260
|
const base = String(input ?? 'deck').trim().replace(/\.(html?|pptx|json)$/i, '')
|
|
244
|
-
|
|
261
|
+
let cleaned = base
|
|
245
262
|
.replace(/[<>:"/\\|?*\u0000-\u001F]/g, '-')
|
|
246
263
|
.replace(/\s+/g, '-')
|
|
247
264
|
.replace(/-+/g, '-')
|
|
248
265
|
.replace(/^-+|-+$/g, '')
|
|
266
|
+
if (WINDOWS_RESERVED_FILE_NAME.test(cleaned)) cleaned = 'deck-' + cleaned
|
|
249
267
|
return cleaned.slice(0, 120) || 'deck'
|
|
250
268
|
}
|
|
251
269
|
|
|
@@ -256,7 +274,10 @@ const DECK_ARTIFACT_EXTENSIONS = ['.html', '.pptx', '.json']
|
|
|
256
274
|
* 整组改用 -1/-2… 后缀,避免新旧 deck 被静默混写。
|
|
257
275
|
*/
|
|
258
276
|
function resolveDeckFileName(outputDir, requestedFileName, overwrite) {
|
|
259
|
-
|
|
277
|
+
// 一次 readdir 建集,替代逐个候选 existsSync 探测(最坏 ~3000 次 stat);
|
|
278
|
+
// 统一小写比较,避免 Windows 大小写不敏感文件系统把已占用的名字判成可用。
|
|
279
|
+
const existing = new Set(readdirSync(outputDir).map((name) => name.toLowerCase()))
|
|
280
|
+
const isAvailable = (candidate) => DECK_ARTIFACT_EXTENSIONS.every((ext) => !existing.has((candidate + ext).toLowerCase()))
|
|
260
281
|
if (overwrite || isAvailable(requestedFileName)) return requestedFileName
|
|
261
282
|
for (let index = 1; index < 1000; index += 1) {
|
|
262
283
|
const candidate = requestedFileName + '-' + index
|
|
@@ -299,7 +320,7 @@ export function stripInlineMarkdown(value) {
|
|
|
299
320
|
export function splitSentences(value) {
|
|
300
321
|
const text = String(value ?? '').trim()
|
|
301
322
|
if (text === '') return []
|
|
302
|
-
const parts = text.split(/(?<=[
|
|
323
|
+
const parts = text.split(/(?<=[。!?])|(?<=[.!?…])\s+/).map((part) => part.trim()).filter(Boolean)
|
|
303
324
|
return parts.length > 0 ? parts : [text]
|
|
304
325
|
}
|
|
305
326
|
|
|
@@ -552,16 +573,16 @@ export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
|
552
573
|
if (section.heading === '') {
|
|
553
574
|
const points = [...section.bullets, ...section.paragraphs]
|
|
554
575
|
.flatMap((part) => splitSentences(part))
|
|
555
|
-
|
|
556
|
-
|
|
576
|
+
// 超过 8 条自动续页,不再静默丢弃;备注只挂在第一页
|
|
577
|
+
for (const [pageIndex, pagePoints] of chunk(points, 8).entries()) {
|
|
557
578
|
pointIndex += 1
|
|
558
579
|
const slide = {
|
|
559
580
|
layout: 'bullets',
|
|
560
581
|
kicker: ui.pointKicker + ' ' + pointIndex,
|
|
561
|
-
title: truncate(
|
|
562
|
-
bullets:
|
|
582
|
+
title: truncate(pagePoints[0], 40) || ui.pointKicker + ' ' + pointIndex,
|
|
583
|
+
bullets: pagePoints,
|
|
563
584
|
}
|
|
564
|
-
if (specials.length === 0 && notes !== '') slide.notes = notes
|
|
585
|
+
if (pageIndex === 0 && specials.length === 0 && notes !== '') slide.notes = notes
|
|
565
586
|
slides.push(slide)
|
|
566
587
|
}
|
|
567
588
|
continue
|
|
@@ -569,11 +590,16 @@ export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
|
569
590
|
const points = [
|
|
570
591
|
...section.bullets,
|
|
571
592
|
...section.paragraphs.flatMap((part) => splitSentences(part)),
|
|
572
|
-
]
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
593
|
+
]
|
|
594
|
+
const pages = chunk(points, 8)
|
|
595
|
+
if (pages.length > 0) {
|
|
596
|
+
// 超过 8 条自动续页,不再静默丢弃;备注只挂在第一页
|
|
597
|
+
pages.forEach((pagePoints, pageIndex) => {
|
|
598
|
+
const label = pageIndex === 0 ? section.heading : section.heading + '(续)'
|
|
599
|
+
const slide = { layout: 'bullets', kicker: label, title: label, bullets: pagePoints }
|
|
600
|
+
if (pageIndex === 0 && specials.length === 0 && notes !== '') slide.notes = notes
|
|
601
|
+
slides.push(slide)
|
|
602
|
+
})
|
|
577
603
|
} else if (specials.length === 0) {
|
|
578
604
|
const slide = { layout: 'section', kicker: section.heading, title: section.heading }
|
|
579
605
|
if (notes !== '') slide.notes = notes
|
|
@@ -674,6 +700,64 @@ export function normalizeBuildOptions(options = {}) {
|
|
|
674
700
|
return { title, theme, language, motion, deck, outputDir, fileName, overwrite }
|
|
675
701
|
}
|
|
676
702
|
|
|
703
|
+
/**
|
|
704
|
+
* 三件套落盘:三份内容全部先写同目录临时文件,再逐个 rename 提交。
|
|
705
|
+
* 任一步失败都清理临时文件,并把已提交的目标恢复为调用前的内容,
|
|
706
|
+
* 避免出现「json/html 已更新、pptx 还是旧的」这类混版状态。
|
|
707
|
+
*/
|
|
708
|
+
function commitDeckArtifacts(artifacts) {
|
|
709
|
+
const stamp = process.pid.toString(36) + '-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8)
|
|
710
|
+
const items = artifacts.map((item, index) => ({ ...item, tempPath: item.path + '.' + stamp + '-' + index + '.tmp' }))
|
|
711
|
+
const removeTemp = (item) => {
|
|
712
|
+
try {
|
|
713
|
+
rmSync(item.tempPath, { force: true })
|
|
714
|
+
} catch {}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// 阶段 0:记下已有产物内容用于回滚;读不出来(如被同名目录占用)先报错,不碰任何文件。
|
|
718
|
+
const previous = new Map()
|
|
719
|
+
for (const item of items) {
|
|
720
|
+
if (!existsSync(item.path)) continue
|
|
721
|
+
try {
|
|
722
|
+
previous.set(item.path, readFileSync(item.path))
|
|
723
|
+
} catch (error) {
|
|
724
|
+
throw new Error('dsh-ppt:三件套写入失败(' + item.path + ' 已存在但无法读取),原文件未改动:' + error.message)
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// 阶段 1:全部写临时文件;这一步失败时目标文件尚未被改动。
|
|
729
|
+
let written = 0
|
|
730
|
+
try {
|
|
731
|
+
for (const item of items) {
|
|
732
|
+
writeFileSync(item.tempPath, item.data, item.encoding)
|
|
733
|
+
written += 1
|
|
734
|
+
}
|
|
735
|
+
} catch (error) {
|
|
736
|
+
for (const item of items.slice(0, written + 1)) removeTemp(item)
|
|
737
|
+
throw new Error('dsh-ppt:写入临时文件失败(' + items[written].path + '),原三件套未改动:' + error.message)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// 阶段 2:逐个 rename 提交;中途失败则把已提交的目标回滚成旧内容。
|
|
741
|
+
const committed = []
|
|
742
|
+
try {
|
|
743
|
+
for (const item of items) {
|
|
744
|
+
renameSync(item.tempPath, item.path)
|
|
745
|
+
committed.push(item)
|
|
746
|
+
}
|
|
747
|
+
} catch (error) {
|
|
748
|
+
const failedPath = items[committed.length].path
|
|
749
|
+
for (const item of items) removeTemp(item)
|
|
750
|
+
for (const item of committed) {
|
|
751
|
+
try {
|
|
752
|
+
const previousData = previous.get(item.path)
|
|
753
|
+
if (previousData === undefined) rmSync(item.path, { force: true })
|
|
754
|
+
else writeFileSync(item.path, previousData)
|
|
755
|
+
} catch {}
|
|
756
|
+
}
|
|
757
|
+
throw new Error('dsh-ppt:提交三件套失败(' + failedPath + '),已回滚为原文件:' + error.message)
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
677
761
|
export function buildDeck(options = {}) {
|
|
678
762
|
const normalized = normalizeBuildOptions(options)
|
|
679
763
|
const { title, theme, language, motion, deck, outputDir, overwrite } = normalized
|
|
@@ -693,9 +777,12 @@ export function buildDeck(options = {}) {
|
|
|
693
777
|
const htmlPath = resolvePath(outputDir, fileName + '.html')
|
|
694
778
|
const pptxPath = resolvePath(outputDir, fileName + '.pptx')
|
|
695
779
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
780
|
+
// 先生成三份内容再统一提交:生成阶段抛错时也不会留下半套文件。
|
|
781
|
+
commitDeckArtifacts([
|
|
782
|
+
{ path: jsonPath, data: JSON.stringify(manifest, null, 2) + '\n', encoding: 'utf8' },
|
|
783
|
+
{ path: htmlPath, data: renderHtml(manifest, theme, language), encoding: 'utf8' },
|
|
784
|
+
{ path: pptxPath, data: buildPptx(manifest, theme, language) },
|
|
785
|
+
])
|
|
699
786
|
|
|
700
787
|
return {
|
|
701
788
|
ok: true,
|
|
@@ -1058,6 +1145,7 @@ function paragraphXml(text, options = {}) {
|
|
|
1058
1145
|
size = 2000,
|
|
1059
1146
|
color = 'FFFFFF',
|
|
1060
1147
|
bold = false,
|
|
1148
|
+
lang = 'zh-CN',
|
|
1061
1149
|
align = 'l',
|
|
1062
1150
|
bullet = false,
|
|
1063
1151
|
font = 'Arial',
|
|
@@ -1076,7 +1164,7 @@ function paragraphXml(text, options = {}) {
|
|
|
1076
1164
|
pPr = '<a:pPr' + (parts.length > 0 ? ' ' + parts.join(' ') : '') + '><a:buNone/></a:pPr>'
|
|
1077
1165
|
}
|
|
1078
1166
|
return '<a:p>' + pPr +
|
|
1079
|
-
'<a:r><a:rPr lang="
|
|
1167
|
+
'<a:r><a:rPr lang="' + lang + '" sz="' + size + '" b="' + (bold ? 1 : 0) + '" dirty="0">' +
|
|
1080
1168
|
'<a:solidFill><a:srgbClr val="' + hex(color) + '"/></a:solidFill>' +
|
|
1081
1169
|
'<a:latin typeface="' + escapeXml(runFont) + '"/><a:ea typeface="' + escapeXml(runFont) + '"/></a:rPr>' +
|
|
1082
1170
|
'<a:t>' + escapeXml(text) + '</a:t></a:r></a:p>'
|
|
@@ -1103,8 +1191,9 @@ function accentBarXml(id, name, box, color) {
|
|
|
1103
1191
|
}
|
|
1104
1192
|
|
|
1105
1193
|
/** 原生 OOXML 表格(graphicFrame + a:tbl):首行主题色表头,隔行面板色。 */
|
|
1106
|
-
function tableGraphicXml(id, name, box, rows, theme) {
|
|
1194
|
+
function tableGraphicXml(id, name, box, rows, theme, langId) {
|
|
1107
1195
|
const p = theme.palette
|
|
1196
|
+
const lang = langId || 'zh-CN'
|
|
1108
1197
|
const font = pptxFontName(theme.fonts.body)
|
|
1109
1198
|
const safeRows = rows.length > 0 ? rows : [['']]
|
|
1110
1199
|
const cols = Math.max(1, safeRows[0].length)
|
|
@@ -1113,7 +1202,7 @@ function tableGraphicXml(id, name, box, rows, theme) {
|
|
|
1113
1202
|
const rowH = 420000
|
|
1114
1203
|
const cellXml = (text, fill, color, bold) =>
|
|
1115
1204
|
'<a:tc><a:txBody><a:bodyPr anchor="ctr"/><a:lstStyle/><a:p><a:pPr algn="l"><a:buNone/></a:pPr>' +
|
|
1116
|
-
'<a:r><a:rPr lang="
|
|
1205
|
+
'<a:r><a:rPr lang="' + lang + '" sz="1400" b="' + (bold ? 1 : 0) + '" dirty="0">' +
|
|
1117
1206
|
'<a:solidFill><a:srgbClr val="' + hex(color) + '"/></a:solidFill>' +
|
|
1118
1207
|
'<a:latin typeface="' + escapeXml(font) + '"/><a:ea typeface="' + escapeXml(font) + '"/></a:rPr>' +
|
|
1119
1208
|
'<a:t>' + escapeXml(text) + '</a:t></a:r></a:p></a:txBody>' +
|
|
@@ -1142,6 +1231,9 @@ function slideShapeList(slide, index, total, theme, langId) {
|
|
|
1142
1231
|
const p = theme.palette
|
|
1143
1232
|
const font = pptxFontName(theme.fonts.heading)
|
|
1144
1233
|
const bodyFont = pptxFontName(theme.fonts.body) // 保留:v0.1 后续用于统一正文字体
|
|
1234
|
+
const lang = langId || 'zh-CN'
|
|
1235
|
+
// PPTX 每个文本 run 的语言跟随 deck.lang(zh-CN / en-US),不再写死中文
|
|
1236
|
+
const para = (content, options = {}) => paragraphXml(content, { lang, ...options })
|
|
1145
1237
|
const shapes = []
|
|
1146
1238
|
const kicker = slide.kicker || ''
|
|
1147
1239
|
const title = slide.title || ''
|
|
@@ -1153,95 +1245,95 @@ function slideShapeList(slide, index, total, theme, langId) {
|
|
|
1153
1245
|
if (slide.layout === 'cover') {
|
|
1154
1246
|
shapes.push(accentBarXml(idBase + 1, 'Accent bar', { x: 914400, y: 2250000, w: 240000, h: 1600000 }, p.accent))
|
|
1155
1247
|
shapes.push(textShapeXml(idBase + 2, 'Title', { x: 1550000, y: 2160000, w: 9250000, h: 1800000 }, [
|
|
1156
|
-
|
|
1248
|
+
para(title, { size: 4400, color: p.fg, bold: true, font }),
|
|
1157
1249
|
]))
|
|
1158
1250
|
if (subtitle !== '') {
|
|
1159
1251
|
shapes.push(textShapeXml(idBase + 3, 'Subtitle', { x: 1570000, y: 4150000, w: 9000000, h: 1200000 }, [
|
|
1160
|
-
|
|
1252
|
+
para(subtitle, { size: 2200, color: p.muted, font: theme.fonts.body }),
|
|
1161
1253
|
]))
|
|
1162
1254
|
}
|
|
1163
1255
|
if (kicker !== '') {
|
|
1164
1256
|
shapes.push(textShapeXml(idBase + 4, 'Kicker', { x: 1570000, y: 5800000, w: 7000000, h: 500000 }, [
|
|
1165
|
-
|
|
1257
|
+
para(kicker, { size: 1400, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
1166
1258
|
]))
|
|
1167
1259
|
}
|
|
1168
1260
|
} else if (slide.layout === 'section') {
|
|
1169
1261
|
if (kicker !== '') {
|
|
1170
1262
|
shapes.push(textShapeXml(idBase + 1, 'Kicker', { x: 1050000, y: 2250000, w: 9000000, h: 500000 }, [
|
|
1171
|
-
|
|
1263
|
+
para(kicker, { size: 1600, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
1172
1264
|
]))
|
|
1173
1265
|
}
|
|
1174
1266
|
shapes.push(accentBarXml(idBase + 2, 'Accent bar', { x: 1050000, y: 2850000, w: 1600000, h: 160000 }, p.accent2))
|
|
1175
1267
|
shapes.push(textShapeXml(idBase + 3, 'Title', { x: 1050000, y: 3150000, w: 10200000, h: 1400000 }, [
|
|
1176
|
-
|
|
1268
|
+
para(title, { size: 4000, color: p.fg, bold: true, font }),
|
|
1177
1269
|
]))
|
|
1178
1270
|
if (subtitle !== '') {
|
|
1179
1271
|
shapes.push(textShapeXml(idBase + 4, 'Subtitle', { x: 1070000, y: 4750000, w: 9000000, h: 900000 }, [
|
|
1180
|
-
|
|
1272
|
+
para(subtitle, { size: 1800, color: p.muted, font: theme.fonts.body }),
|
|
1181
1273
|
]))
|
|
1182
1274
|
}
|
|
1183
1275
|
} else if (slide.layout === 'statement') {
|
|
1184
1276
|
shapes.push(accentBarXml(idBase + 1, 'Accent bar', { x: 914400, y: 1900000, w: 200000, h: 2800000 }, p.accent))
|
|
1185
1277
|
shapes.push(textShapeXml(idBase + 2, 'Statement', { x: 1500000, y: 1950000, w: 9400000, h: 2700000 }, [
|
|
1186
|
-
|
|
1278
|
+
para(title || slide.text || '', { size: 3600, color: p.fg, bold: true, font }),
|
|
1187
1279
|
]))
|
|
1188
1280
|
if (subtitle !== '') {
|
|
1189
1281
|
shapes.push(textShapeXml(idBase + 3, 'Subtitle', { x: 1520000, y: 4900000, w: 9000000, h: 800000 }, [
|
|
1190
|
-
|
|
1282
|
+
para(subtitle, { size: 1800, color: p.muted, font: theme.fonts.body }),
|
|
1191
1283
|
]))
|
|
1192
1284
|
}
|
|
1193
1285
|
} else if (slide.layout === 'quote') {
|
|
1194
1286
|
shapes.push(textShapeXml(idBase + 1, 'Quote mark', { x: 900000, y: 1050000, w: 1700000, h: 1900000 }, [
|
|
1195
|
-
|
|
1287
|
+
para('\u201C', { size: 9600, color: p.accent, bold: true, font }),
|
|
1196
1288
|
]))
|
|
1197
1289
|
shapes.push(textShapeXml(idBase + 2, 'Quote', { x: 1050000, y: 2550000, w: 10100000, h: 2600000 }, [
|
|
1198
|
-
|
|
1290
|
+
para(title || slide.text || '', { size: 3000, color: p.fg, bold: true, font }),
|
|
1199
1291
|
]))
|
|
1200
1292
|
if (subtitle !== '') {
|
|
1201
1293
|
shapes.push(textShapeXml(idBase + 3, 'Attribution', { x: 1070000, y: 5300000, w: 9000000, h: 600000 }, [
|
|
1202
|
-
|
|
1294
|
+
para('\u2014\u2014 ' + subtitle, { size: 1600, color: p.muted, font: theme.fonts.body }),
|
|
1203
1295
|
]))
|
|
1204
1296
|
}
|
|
1205
1297
|
} else if (slide.layout === 'table') {
|
|
1206
1298
|
const hasTitle = title !== '' && title !== kicker
|
|
1207
1299
|
if (kicker !== '') {
|
|
1208
1300
|
shapes.push(textShapeXml(idBase + 1, 'Kicker', { x: 900000, y: 420000, w: 10000000, h: 420000 }, [
|
|
1209
|
-
|
|
1301
|
+
para(kicker, { size: 1400, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
1210
1302
|
]))
|
|
1211
1303
|
}
|
|
1212
1304
|
if (hasTitle) {
|
|
1213
1305
|
shapes.push(textShapeXml(idBase + 2, 'Title', { x: 900000, y: 920000, w: 10300000, h: 800000 }, [
|
|
1214
|
-
|
|
1306
|
+
para(title, { size: 3400, color: p.fg, bold: true, font }),
|
|
1215
1307
|
]))
|
|
1216
1308
|
}
|
|
1217
|
-
shapes.push(tableGraphicXml(idBase + 3, 'Table', { x: 900000, y: hasTitle ? 1950000 : 900000, w: 10300000 }, Array.isArray(slide.rows) ? slide.rows : [], theme))
|
|
1309
|
+
shapes.push(tableGraphicXml(idBase + 3, 'Table', { x: 900000, y: hasTitle ? 1950000 : 900000, w: 10300000 }, Array.isArray(slide.rows) ? slide.rows : [], theme, lang))
|
|
1218
1310
|
} else if (slide.layout === 'closing') {
|
|
1219
1311
|
shapes.push(textShapeXml(idBase + 1, 'Title', { x: 1050000, y: 2300000, w: 10200000, h: 1700000 }, [
|
|
1220
|
-
|
|
1312
|
+
para(title || '谢谢', { size: 5200, color: p.fg, bold: true, align: 'ctr', font }),
|
|
1221
1313
|
]))
|
|
1222
1314
|
if (subtitle !== '') {
|
|
1223
1315
|
shapes.push(textShapeXml(idBase + 2, 'Subtitle', { x: 1050000, y: 4200000, w: 10200000, h: 900000 }, [
|
|
1224
|
-
|
|
1316
|
+
para(subtitle, { size: 2000, color: p.accent, bold: true, align: 'ctr', font: theme.fonts.body }),
|
|
1225
1317
|
]))
|
|
1226
1318
|
}
|
|
1227
1319
|
} else {
|
|
1228
1320
|
// bullets(默认布局)
|
|
1229
1321
|
if (kicker !== '') {
|
|
1230
1322
|
shapes.push(textShapeXml(idBase + 1, 'Kicker', { x: 900000, y: 420000, w: 10000000, h: 420000 }, [
|
|
1231
|
-
|
|
1323
|
+
para(kicker, { size: 1400, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
1232
1324
|
]))
|
|
1233
1325
|
}
|
|
1234
1326
|
shapes.push(textShapeXml(idBase + 2, 'Title', { x: 900000, y: 920000, w: 10300000, h: 800000 }, [
|
|
1235
|
-
|
|
1327
|
+
para(title, { size: 3400, color: p.fg, bold: true, font }),
|
|
1236
1328
|
]))
|
|
1237
1329
|
const bodyParagraphs = bullets.length > 0
|
|
1238
|
-
? bullets.map((bullet) =>
|
|
1239
|
-
: [
|
|
1330
|
+
? bullets.map((bullet) => para(bullet, { size: 2000, color: p.fg, bullet: true, font: theme.fonts.body, spaceAfter: 600 }))
|
|
1331
|
+
: [para(subtitle || '', { size: 2000, color: p.fg, font: theme.fonts.body })]
|
|
1240
1332
|
shapes.push(textShapeXml(idBase + 3, 'Body', { x: 1050000, y: 1850000, w: 10100000, h: 4500000 }, bodyParagraphs))
|
|
1241
1333
|
}
|
|
1242
1334
|
|
|
1243
1335
|
shapes.push(textShapeXml(idBase + 9, 'Page number', { x: 10600000, y: 6250000, w: 1200000, h: 400000 }, [
|
|
1244
|
-
|
|
1336
|
+
para(footer, { size: 1000, color: p.muted, align: 'r', font: theme.fonts.body }),
|
|
1245
1337
|
]))
|
|
1246
1338
|
|
|
1247
1339
|
return shapes.join('')
|
|
@@ -1366,10 +1458,11 @@ function notesMasterRelXml() {
|
|
|
1366
1458
|
'</Relationships>'
|
|
1367
1459
|
}
|
|
1368
1460
|
|
|
1369
|
-
function notesSlideXml(notes) {
|
|
1461
|
+
function notesSlideXml(notes, langId) {
|
|
1462
|
+
const lang = langId || 'zh-CN'
|
|
1370
1463
|
const paragraphs = String(notes ?? '').split('\n')
|
|
1371
1464
|
.filter((line) => line.trim() !== '')
|
|
1372
|
-
.map((line) => '<a:p><a:r><a:rPr lang="
|
|
1465
|
+
.map((line) => '<a:p><a:r><a:rPr lang="' + lang + '" sz="1200" dirty="0"/><a:t>' + escapeXml(line) + '</a:t></a:r></a:p>')
|
|
1373
1466
|
.join('')
|
|
1374
1467
|
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1375
1468
|
'<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
@@ -1635,10 +1728,10 @@ export function buildPptx(manifest, themeInput, languageInput) {
|
|
|
1635
1728
|
}
|
|
1636
1729
|
slides.forEach((slide, index) => {
|
|
1637
1730
|
const hasNotes = noteIndexes.includes(index)
|
|
1638
|
-
entries.push({ name: 'ppt/slides/slide' + (index + 1) + '.xml', data: slideXml(slide, index, slides.length, theme, language.
|
|
1731
|
+
entries.push({ name: 'ppt/slides/slide' + (index + 1) + '.xml', data: slideXml(slide, index, slides.length, theme, language.attr, motion) })
|
|
1639
1732
|
entries.push({ name: 'ppt/slides/_rels/slide' + (index + 1) + '.xml.rels', data: slideRelXml(hasNotes, index + 1) })
|
|
1640
1733
|
if (hasNotes) {
|
|
1641
|
-
entries.push({ name: 'ppt/notesSlides/notesSlide' + (index + 1) + '.xml', data: notesSlideXml(slide.notes) })
|
|
1734
|
+
entries.push({ name: 'ppt/notesSlides/notesSlide' + (index + 1) + '.xml', data: notesSlideXml(slide.notes, language.attr) })
|
|
1642
1735
|
entries.push({ name: 'ppt/notesSlides/_rels/notesSlide' + (index + 1) + '.xml.rels', data: notesSlideRelXml(index + 1) })
|
|
1643
1736
|
}
|
|
1644
1737
|
})
|