dsh-ppt 0.4.0 → 0.4.2

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.2(2026-09-11)
4
+
5
+ - 适配并验证官方 Harness 0.1.5-rc.1:整套同载、工具/技能契约与 Web 鉴权检查通过。
6
+ - Node 要求与宿主统一为 `^22.19.0 || >=24.0.0`;更新中英文兼容性说明。
7
+
3
8
  ## 0.4.0
4
9
 
5
10
  - **动效(motion,默认开)**:HTML 放映页间淡入 + 要点按序逐条入场(打印自动兜底为静态);PPTX 每页原生淡入转场,`bullets` 页按段落构建、放映时点击一次出现一条要点(PowerPoint「出现,按段落」标准时序)。
package/README.en.md CHANGED
@@ -30,7 +30,7 @@ Example:
30
30
 
31
31
  ## Compatibility
32
32
 
33
- Verified against `@deepseek-ai/dsh@0.1.2-alpha.3` on 2026-09-01. Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
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
 
@@ -79,6 +79,8 @@ node <skill-dir>/scripts/build-deck.mjs \
79
79
 
80
80
  Artifacts:
81
81
 
82
+ Existing artifacts are not overwritten by default: if any member of the trio already exists, the whole set receives a shared `-1`, `-2`, ... suffix. Replacement requires explicit `overwrite: true` (or `--overwrite` in the CLI).
83
+
82
84
  | File | Purpose |
83
85
  | --- | --- |
84
86
  | `*.html` | Standalone web slideshow: arrow keys/wheel/touch navigation, F fullscreen, G overview, P print/save as PDF |
@@ -103,7 +105,7 @@ Themes are derived from [dsh-hyperframes](https://github.com/STARDUSTLC666/dsh-h
103
105
  - Each `## section` becomes one slide: lists produce `bullets` slides; empty sections produce `section` dividers.
104
106
  - Plain text without headings: the first paragraph is the cover, then every 5 sentences become one slide.
105
107
  - A single sentence automatically produces a complete 3-slide structure: cover → core idea → closing.
106
- - For precise control, use structured `slides` (`cover | section | bullets | statement | closing`).
108
+ - For precise control, use structured `slides` (`cover | section | bullets | statement | quote | table | closing`), with `rows` for tables and `notes` for speaker notes.
107
109
 
108
110
  ## Configuration
109
111
 
@@ -120,6 +122,8 @@ No required configuration. Optional:
120
122
 
121
123
  The `DSH_PPT_OUTPUT_DIR` env var can also set the default output directory; the `ppt_create` `outputDir`/`theme`/`lang` arguments have the highest priority.
122
124
 
125
+ Output paths resolve against each tool call's `exec.agent.session.header.cwd`: omitted paths use the session directory, relative paths (including plugin configuration) resolve within it, and absolute paths keep their meaning. Direct calls without a session fall back to the process working directory; concurrent sessions never change process cwd. Tools check `exec.signal` before and after loading the engine, so cancelled calls do not proceed to generation. Rendering and writing are synchronous and finish the artifact trio once started.
126
+
123
127
  ## Bilingual support
124
128
 
125
129
  - `lang` argument: `zh` (default) / `en` / `bilingual` controls the player UI, page numbers, and closing defaults.
@@ -147,7 +151,7 @@ pnpm run smoke:cli # bare CLI smoke test, generates .smoke-deck
147
151
  - The PPTX uses a blank layout plus text boxes: text is editable in PowerPoint / WPS, but no smart master placeholders yet.
148
152
  - A one-sentence input produces a minimal 3-slide structure; for richer decks, expand the content into a Markdown outline first.
149
153
  - `bilingual` only localizes the player UI; it does not translate content.
150
- - Charts, images, speaker notes, and PPT animations are planned for v0.2+.
154
+ - Charts and images are not supported yet; speaker notes and HTML/PPTX motion are already supported.
151
155
 
152
156
  ## License
153
157
 
package/README.md CHANGED
@@ -32,7 +32,7 @@ DSH(DeepSeek Harness)演示文稿技能 + 工具插件:把一句话、一
32
32
 
33
33
  ## 兼容性
34
34
 
35
- `@deepseek-ai/dsh@0.1.2-alpha.3` 上验证(2026-09-01 全量冒烟通过)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
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
 
@@ -72,6 +72,8 @@ node <skill-dir>/scripts/build-deck.mjs \
72
72
 
73
73
  输出三件套:
74
74
 
75
+ 默认不会覆盖已有同名产物:三件套中任一文件存在时会整组追加 `-1`、`-2`… 后缀;只有显式传 `overwrite: true`(CLI 为 `--overwrite`)才会覆盖。
76
+
75
77
  | 文件 | 用途 |
76
78
  | --- | --- |
77
79
  | `*.html` | 独立网页放映:方向键/滚轮/触屏翻页,F 全屏,G 总览,P 打印或另存 PDF |
@@ -96,7 +98,7 @@ node <skill-dir>/scripts/build-deck.mjs \
96
98
  - 每个 `## 小节` → 一页:有列表生成 `bullets` 页,无内容生成 `section` 过渡页。
97
99
  - 没有标题的纯文本 → 第一段作封面,后续每 5 句一页。
98
100
  - 只有一句话 → 自动生成「封面 → 核心观点 → 结束页」三页完整结构。
99
- - 需要精确控制时用结构化 `slides`(`cover | section | bullets | statement | closing`)。
101
+ - 需要精确控制时用结构化 `slides`(`cover | section | bullets | statement | quote | table | closing`),表格使用 `rows`,备注使用 `notes`。
100
102
 
101
103
  ## 配置
102
104
 
@@ -113,6 +115,8 @@ node <skill-dir>/scripts/build-deck.mjs \
113
115
 
114
116
  也可用环境变量 `DSH_PPT_OUTPUT_DIR` 指定默认输出目录;`ppt_create` 的 `outputDir`/`theme`/`lang` 参数优先级最高。
115
117
 
118
+ 输出目录按每次工具调用的 `exec.agent.session.header.cwd` 解析:不指定目录时写入当前会话工作目录,相对目录(包括插件配置)相对此目录解析,绝对目录保持原意。直接调用工具而未提供会话时,回退到进程工作目录;并行会话不会修改进程 cwd。工具在加载引擎前和加载完成后检查 `exec.signal`,取消的调用不会继续生成文件;生成和写入阶段为同步操作,开始后会完成本次三件套。
119
+
116
120
 
117
121
  ## 卸载
118
122
 
@@ -149,7 +153,7 @@ pnpm run smoke:cli # 裸 CLI 冒烟,生成 .smoke-deck
149
153
  - PPTX 采用空白版式 + 文本框实现:PowerPoint / WPS 中可正常编辑文字,但暂不生成智能母版占位符。
150
154
  - 一句话输入自动生成三页最小结构;更丰富的内容需要先扩写成 Markdown 大纲再调用 `ppt_create`。
151
155
  - `bilingual` 只双语化播放器界面,不自动翻译内容。
152
- - 暂不支持图表、图片、演讲者备注与 PPT 动画;这些在 v0.2+ 规划。
156
+ - 暂不支持图表与图片;演讲者备注及 HTML/PPTX 动效已经支持。
153
157
 
154
158
  ## 协议
155
159
 
@@ -159,4 +163,4 @@ MIT。社区插件,与 DeepSeek 官方无关;`@deepseek-ai/*` 为官方保
159
163
 
160
164
  - [dsh-hyperframes](https://github.com/STARDUSTLC666/dsh-hyperframes) — HTML 视频创作技能(本插件视觉风格来源)
161
165
  - [dsh-remotion](https://github.com/STARDUSTLC666/dsh-remotion) — React 编程式视频技能
162
- - [dsh-email](https://github.com/STARDUSTLC666/dsh-email) — 邮件六件套
166
+ - [dsh-email](https://github.com/STARDUSTLC666/dsh-email) — 邮件六件套
package/lib/config.js CHANGED
@@ -2,17 +2,39 @@ export const PPT_OUTPUT_DIR_ENV = 'DSH_PPT_OUTPUT_DIR';
2
2
  export const DEFAULT_MAX_SLIDES = 60;
3
3
  /** 解析并校验插件行配置。本插件无必填项,空配置永远可用。 */
4
4
  export function resolvePptConfig(config) {
5
+ if (config !== undefined && config !== null && (typeof config !== 'object' || Array.isArray(config))) {
6
+ throw new Error('dsh-ppt 配置必须是对象。');
7
+ }
5
8
  const raw = config ?? {};
9
+ if (raw.outputDir !== undefined && typeof raw.outputDir !== 'string') {
10
+ throw new Error('outputDir 必须是字符串。');
11
+ }
12
+ if (raw.defaultTheme !== undefined && typeof raw.defaultTheme !== 'string') {
13
+ throw new Error('defaultTheme 必须是字符串。');
14
+ }
15
+ if (raw.defaultLang !== undefined && typeof raw.defaultLang !== 'string') {
16
+ throw new Error('defaultLang 必须是字符串。');
17
+ }
6
18
  const outputDir = (typeof raw.outputDir === 'string' ? raw.outputDir : '').trim()
7
19
  || (process.env[PPT_OUTPUT_DIR_ENV] ?? '').trim();
8
20
  const defaultTheme = (typeof raw.defaultTheme === 'string' ? raw.defaultTheme : '').trim();
9
21
  const defaultLang = (typeof raw.defaultLang === 'string' ? raw.defaultLang : '').trim();
22
+ if (defaultTheme !== '' && !['swiss', 'velvet', 'data', 'soft', 'bold'].includes(defaultTheme)) {
23
+ throw new Error('defaultTheme 只支持 swiss / velvet / data / soft / bold。');
24
+ }
10
25
  if (defaultLang !== '' && defaultLang !== 'zh' && defaultLang !== 'en' && defaultLang !== 'bilingual') {
11
26
  throw new Error('defaultLang 只支持 zh / en / bilingual。');
12
27
  }
28
+ let maxSlides = DEFAULT_MAX_SLIDES;
29
+ if (raw.maxSlides !== undefined) {
30
+ if (typeof raw.maxSlides !== 'number' || !Number.isInteger(raw.maxSlides) || raw.maxSlides < 3 || raw.maxSlides > 120) {
31
+ throw new Error('maxSlides 必须是 3–120 的整数。');
32
+ }
33
+ maxSlides = raw.maxSlides;
34
+ }
13
35
  return {
14
36
  outputDir,
15
- maxSlides: clampInt(raw.maxSlides, DEFAULT_MAX_SLIDES, 3, 120),
37
+ maxSlides,
16
38
  defaultTheme,
17
39
  defaultLang,
18
40
  };
@@ -0,0 +1,18 @@
1
+ import type { ResolvedPptConfig } from './config.js';
2
+ import type { DeckEngine, PptThemesResult } from './types.js';
3
+ /** The Harness execution fields used by this plugin; optional for direct callers. */
4
+ export interface PptExecution {
5
+ readonly signal?: AbortSignal;
6
+ readonly agent?: {
7
+ readonly session: {
8
+ readonly header: {
9
+ readonly cwd?: string;
10
+ };
11
+ };
12
+ };
13
+ }
14
+ /** Build executors with a shared engine loader; resolve paths separately for each call. */
15
+ export declare function createPptExecutors(config: ResolvedPptConfig, loadEngine?: () => Promise<DeckEngine>): {
16
+ themes(rawArgs: unknown, exec?: PptExecution): Promise<PptThemesResult>;
17
+ create(rawArgs: unknown, exec?: PptExecution): Promise<import("./types.js").PptCreateResult>;
18
+ };
@@ -0,0 +1,56 @@
1
+ import { resolve } from 'node:path';
2
+ let enginePromise = null;
3
+ function getEngine() {
4
+ if (enginePromise === null) {
5
+ const engineUrl = new URL('../skills/dsh-ppt/scripts/deck-core.mjs', import.meta.url);
6
+ enginePromise = import(engineUrl.href).then((module) => module)
7
+ .catch((error) => {
8
+ enginePromise = null;
9
+ throw error;
10
+ });
11
+ }
12
+ return enginePromise;
13
+ }
14
+ function asRecord(value) {
15
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
16
+ }
17
+ /** Build executors with a shared engine loader; resolve paths separately for each call. */
18
+ export function createPptExecutors(config, loadEngine = getEngine) {
19
+ return {
20
+ async themes(rawArgs, exec) {
21
+ exec?.signal?.throwIfAborted();
22
+ const args = asRecord(rawArgs);
23
+ const lang = typeof args.lang === 'string' && args.lang.trim() !== '' ? args.lang.trim() : 'zh';
24
+ const engine = await loadEngine();
25
+ exec?.signal?.throwIfAborted();
26
+ return { ok: true, themes: engine.listThemes(lang) };
27
+ },
28
+ async create(rawArgs, exec) {
29
+ exec?.signal?.throwIfAborted();
30
+ const args = asRecord(rawArgs);
31
+ if (typeof args.title !== 'string' || args.title.trim() === '')
32
+ throw new Error('dsh-ppt:title 不能为空');
33
+ const hasContent = typeof args.content === 'string' && args.content.trim() !== '';
34
+ const hasSlides = Array.isArray(args.slides) && args.slides.length > 0;
35
+ if (!hasContent && !hasSlides)
36
+ throw new Error('dsh-ppt:content 不能为空(或用 slides 传结构化幻灯片)');
37
+ const engine = await loadEngine();
38
+ // Loading can yield to cancellation. Rendering/writing then runs synchronously.
39
+ exec?.signal?.throwIfAborted();
40
+ const cwd = exec?.agent?.session.header.cwd ?? process.cwd();
41
+ const outputDir = typeof args.outputDir === 'string' && args.outputDir.trim() !== '' ? args.outputDir.trim() : config.outputDir;
42
+ return engine.buildDeck({
43
+ title: args.title.trim(),
44
+ content: typeof args.content === 'string' ? args.content : '',
45
+ slides: hasSlides ? args.slides : undefined,
46
+ theme: typeof args.theme === 'string' && args.theme.trim() !== '' ? args.theme.trim() : (config.defaultTheme || undefined),
47
+ lang: typeof args.lang === 'string' && args.lang.trim() !== '' ? args.lang.trim() : (config.defaultLang || undefined),
48
+ motion: args.motion,
49
+ outputDir: resolve(cwd, outputDir || '.'),
50
+ fileName: typeof args.fileName === 'string' && args.fileName.trim() !== '' ? args.fileName.trim() : undefined,
51
+ overwrite: args.overwrite === true,
52
+ maxSlides: config.maxSlides,
53
+ });
54
+ },
55
+ };
56
+ }
package/lib/index.d.ts CHANGED
@@ -1,22 +1,12 @@
1
1
  import { type SkillsService } from './skill.js';
2
2
  import type { PptConfig } from './types.js';
3
+ import { type ToolDefinition } from './tools.js';
4
+ export type { ToolDefinition } from './tools.js';
5
+ export type { PptExecution } from './execution.js';
3
6
  /** cordis 服务注入:apply 里要使用 ctx.tools 与 ctx.skills。 */
4
7
  export declare const inject: string[];
5
8
  export declare const name = "dsh-ppt";
6
9
  export type Config = PptConfig;
7
- export interface ToolDefinition {
8
- name: string;
9
- description: string;
10
- parameters: Record<string, unknown>;
11
- output: {
12
- schema: Record<string, unknown>;
13
- render: (args: unknown, value: unknown) => Array<{
14
- type: 'text';
15
- text: string;
16
- }>;
17
- };
18
- execute: (rawArgs: unknown) => Promise<unknown>;
19
- }
20
10
  export interface PptPluginContext {
21
11
  tools: {
22
12
  register(definition: ToolDefinition): () => void;
@@ -30,4 +20,4 @@ export interface PptPluginContext {
30
20
  export declare function apply(ctx: PptPluginContext, config?: Config): void;
31
21
  export { resolvePptConfig, PPT_OUTPUT_DIR_ENV, DEFAULT_MAX_SLIDES, clampInt } from './config.js';
32
22
  export { bundledSkillsDir, parseSkillFile, registerPptSkill, SKILL_NAMES } from './skill.js';
33
- export type { PptConfig, PptCreateArgs, PptCreateResult, PptSlideSpec, PptThemeInfo, PptThemesResult } from './types.js';
23
+ export type { PptConfig, PptCreateArgs, PptCreateResult, PptLanguage, PptSlideLayout, PptSlideSpec, PptThemeId, PptThemeInfo, PptThemesResult, } from './types.js';
package/lib/index.js CHANGED
@@ -17,112 +17,10 @@
17
17
  */
18
18
  import { resolvePptConfig } from './config.js';
19
19
  import { registerPptSkill } from './skill.js';
20
+ import { buildPptTools } from './tools.js';
20
21
  /** cordis 服务注入:apply 里要使用 ctx.tools 与 ctx.skills。 */
21
22
  export const inject = ['tools', 'skills'];
22
23
  export const name = 'dsh-ppt';
23
- /**
24
- * 把作者 DSL 映射编译成原生 JSON Schema 对象,作为 defineTool 的
25
- * definition.parameters 原样下发。原生 wire 请求会逐字携带该值。
26
- */
27
- function compileParameters(spec) {
28
- const properties = {};
29
- const required = [];
30
- for (const [key, prop] of Object.entries(spec)) {
31
- if (prop?.required === true)
32
- required.push(key);
33
- const node = {};
34
- if (typeof prop?.type === 'string')
35
- node.type = prop.type;
36
- if (typeof prop?.description === 'string')
37
- node.description = prop.description;
38
- if (Array.isArray(prop?.enum))
39
- node.enum = prop.enum;
40
- if (prop?.type === 'array' && prop.items !== null && typeof prop.items === 'object') {
41
- node.items = {
42
- type: prop.items.type === 'object' ? 'object' : 'string',
43
- ...(prop.items.type === 'object' ? { additionalProperties: true } : {}),
44
- };
45
- }
46
- properties[key] = node;
47
- }
48
- return { type: 'object', properties, ...(required.length > 0 ? { required } : {}) };
49
- }
50
- const themeInfoSchema = {
51
- type: 'object',
52
- properties: {
53
- id: { type: 'string' },
54
- name: { type: 'string' },
55
- mood: { type: 'string' },
56
- bestFor: { type: 'string' },
57
- dark: { type: 'boolean' },
58
- palette: { type: 'object', additionalProperties: true },
59
- fonts: { type: 'object', additionalProperties: true },
60
- },
61
- additionalProperties: true,
62
- };
63
- const themesResultSchema = {
64
- type: 'object',
65
- properties: {
66
- ok: { type: 'boolean' },
67
- themes: { type: 'array', items: themeInfoSchema },
68
- },
69
- additionalProperties: true,
70
- };
71
- const createResultSchema = {
72
- type: 'object',
73
- properties: {
74
- ok: { type: 'boolean' },
75
- title: { type: 'string' },
76
- theme: { type: 'string' },
77
- language: { type: 'string' },
78
- slideCount: { type: 'integer' },
79
- outputDir: { type: 'string' },
80
- files: {
81
- type: 'object',
82
- properties: {
83
- html: { type: 'string' },
84
- pptx: { type: 'string' },
85
- json: { type: 'string' },
86
- },
87
- additionalProperties: true,
88
- },
89
- htmlPath: { type: 'string' },
90
- pptxPath: { type: 'string' },
91
- jsonPath: { type: 'string' },
92
- },
93
- additionalProperties: true,
94
- };
95
- function oneText(text) {
96
- return [{ type: 'text', text }];
97
- }
98
- function renderThemes(value) {
99
- if (value.themes.length === 0)
100
- return oneText('dsh-ppt 没有可用主题。');
101
- const lines = value.themes.map((theme) => '- ' + theme.id + ':' + theme.name + '(' + theme.mood + ')|适合:' + theme.bestFor + '|' + (theme.dark ? '深色' : '浅色'));
102
- return oneText('dsh-ppt 内置主题:\n\n' + lines.join('\n') + '\n\nppt_create 的 theme 参数填其中的 id(默认 data)。');
103
- }
104
- function renderCreate(value) {
105
- return oneText('dsh-ppt 已生成 ' + value.slideCount + ' 页演示文稿(主题 ' + value.theme + ',语言 ' + value.language + '):\n' +
106
- 'HTML 网页放映:' + value.htmlPath + '\n' +
107
- 'PPTX 导出:' + value.pptxPath + '\n' +
108
- 'Manifest:' + value.jsonPath + '\n' +
109
- 'HTML 双击即可放映(方向键翻页 / F 全屏 / G 总览 / P 打印);PPTX 可用 PowerPoint / WPS / Keynote 打开。');
110
- }
111
- let enginePromise = null;
112
- function getEngine() {
113
- if (enginePromise === null) {
114
- const engineUrl = new URL('../skills/dsh-ppt/scripts/deck-core.mjs', import.meta.url);
115
- enginePromise = import(engineUrl.href).then((module) => module)
116
- .catch((error) => {
117
- enginePromise = null;
118
- throw error;
119
- });
120
- }
121
- return enginePromise;
122
- }
123
- function isRecord(value) {
124
- return value !== null && typeof value === 'object';
125
- }
126
24
  export function apply(ctx, config = {}) {
127
25
  const resolved = resolvePptConfig(config);
128
26
  const warn = (message) => { ctx.logger?.warn?.(message); };
@@ -134,63 +32,9 @@ export function apply(ctx, config = {}) {
134
32
  catch (error) {
135
33
  warn('[dsh-ppt] 技能加载失败:' + (error instanceof Error ? error.message : String(error)));
136
34
  }
137
- disposers.push(ctx.tools.register({
138
- name: 'ppt_themes',
139
- description: 'List the built-in visual themes of dsh-ppt (id, name, mood, best-for, light/dark palette) before building a deck. Use a theme id as the theme argument of ppt_create. 中文:列出 dsh-ppt 内置视觉主题(id、名称、情绪、适用场景、明暗色板),用于选择 ppt_create 的 theme 参数。',
140
- parameters: compileParameters({
141
- lang: { type: 'string', description: 'Theme description language: zh (default), en, or bilingual.' },
142
- }),
143
- output: {
144
- schema: themesResultSchema,
145
- render: (_args, value) => renderThemes(value),
146
- },
147
- async execute(rawArgs) {
148
- const args = isRecord(rawArgs) ? rawArgs : {};
149
- const lang = typeof args.lang === 'string' && args.lang.trim() !== '' ? args.lang.trim() : 'zh';
150
- const engine = await getEngine();
151
- const themes = engine.listThemes(lang);
152
- return { ok: true, themes };
153
- },
154
- }));
155
- disposers.push(ctx.tools.register({
156
- name: 'ppt_create',
157
- description: 'Build a complete presentation deck from one sentence or a Markdown document and write three artifacts to outputDir: a standalone HTML web slideshow, an editable 16:9 PPTX, and a deck.json manifest. Five built-in visual themes are available (see ppt_themes). content is Markdown text (recommended); advanced callers may pass structured slides instead. 中文:把一句话或一篇 Markdown 文档生成完整演示文稿,写入 outputDir 三个文件:独立 HTML 网页放映、可编辑 16:9 PPTX、deck.json manifest;内置 5 套视觉主题。',
158
- parameters: compileParameters({
159
- title: { type: 'string', required: true, description: 'Deck title (used for the cover and file names).' },
160
- content: { type: 'string', required: true, description: 'Markdown content: one sentence, a paragraph, or a full document. First # heading becomes the cover title; ## headings become slides; -/* lists become bullets; | ... | tables become table slides; > blockquotes become quote slides; <!-- 备注: ... --> comments become speaker notes. Required unless slides is provided.' },
161
- theme: { type: 'string', description: 'Visual theme id: swiss / velvet / data / soft / bold. Default data. See ppt_themes.' },
162
- lang: { type: 'string', description: 'UI language of the generated player: zh (default), en, or bilingual. Content language is whatever you write.' },
163
- motion: { type: 'string', enum: ['on', 'off'], description: 'Slide transitions plus bullet entrance animations: on (default) or off for a fully static deck.' },
164
- slides: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Optional structured slides: [{ layout: cover|section|bullets|statement|quote|table|closing, title, subtitle, kicker, bullets: [], rows: [][] (for table), notes: "speaker notes" }]. Use this for precise control instead of content.' },
165
- outputDir: { type: 'string', description: 'Directory to write the files into. Default: session working directory (or the plugin outputDir config).' },
166
- fileName: { type: 'string', description: 'Base file name for the three artifacts. Default: sanitized deck title.' },
167
- }),
168
- output: {
169
- schema: createResultSchema,
170
- render: (_args, value) => renderCreate(value),
171
- },
172
- async execute(rawArgs) {
173
- const args = (isRecord(rawArgs) ? rawArgs : {});
174
- if (typeof args.title !== 'string' || args.title.trim() === '')
175
- throw new Error('dsh-ppt:title 不能为空');
176
- const hasContent = typeof args.content === 'string' && args.content.trim() !== '';
177
- const hasSlides = Array.isArray(args.slides) && args.slides.length > 0;
178
- if (!hasContent && !hasSlides)
179
- throw new Error('dsh-ppt:content 不能为空(或用 slides 传结构化幻灯片)');
180
- const engine = await getEngine();
181
- return engine.buildDeck({
182
- title: args.title.trim(),
183
- content: typeof args.content === 'string' ? args.content : '',
184
- slides: hasSlides ? args.slides : undefined,
185
- theme: typeof args.theme === 'string' && args.theme.trim() !== '' ? args.theme.trim() : (resolved.defaultTheme || undefined),
186
- lang: typeof args.lang === 'string' && args.lang.trim() !== '' ? args.lang.trim() : (resolved.defaultLang || undefined),
187
- motion: args.motion,
188
- outputDir: typeof args.outputDir === 'string' && args.outputDir.trim() !== '' ? args.outputDir.trim() : (resolved.outputDir || undefined),
189
- fileName: typeof args.fileName === 'string' && args.fileName.trim() !== '' ? args.fileName.trim() : undefined,
190
- maxSlides: resolved.maxSlides,
191
- });
192
- },
193
- }));
35
+ for (const definition of buildPptTools(resolved)) {
36
+ disposers.push(ctx.tools.register(definition));
37
+ }
194
38
  if (typeof ctx.on === 'function') {
195
39
  ctx.on('dispose', () => {
196
40
  for (const dispose of disposers)
package/lib/tools.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { ResolvedPptConfig } from './config.js';
2
+ import { type PptExecution } from './execution.js';
3
+ export interface ToolDefinition {
4
+ name: string;
5
+ description: string;
6
+ parameters: Record<string, unknown>;
7
+ output: {
8
+ schema: Record<string, unknown>;
9
+ render: (args: unknown, value: unknown) => Array<{
10
+ type: 'text';
11
+ text: string;
12
+ }>;
13
+ };
14
+ execute: (rawArgs: unknown, exec?: PptExecution) => Promise<unknown>;
15
+ }
16
+ /** Assemble presentation schemas, rendering and the session-aware executors. */
17
+ export declare function buildPptTools(config: ResolvedPptConfig): ToolDefinition[];
package/lib/tools.js ADDED
@@ -0,0 +1,123 @@
1
+ import { createPptExecutors } from './execution.js';
2
+ function compileParameters(spec) {
3
+ const properties = {};
4
+ const required = [];
5
+ for (const [key, prop] of Object.entries(spec)) {
6
+ if (prop?.required === true)
7
+ required.push(key);
8
+ const node = {};
9
+ if (typeof prop?.type === 'string')
10
+ node.type = prop.type;
11
+ if (typeof prop?.description === 'string')
12
+ node.description = prop.description;
13
+ if (Array.isArray(prop?.enum))
14
+ node.enum = prop.enum;
15
+ if (prop?.type === 'array' && prop.items !== null && typeof prop.items === 'object') {
16
+ node.items = {
17
+ type: prop.items.type === 'object' ? 'object' : 'string',
18
+ ...(prop.items.type === 'object' ? { additionalProperties: true } : {}),
19
+ };
20
+ }
21
+ properties[key] = node;
22
+ }
23
+ return { type: 'object', properties, ...(required.length > 0 ? { required } : {}) };
24
+ }
25
+ const themeInfoSchema = {
26
+ type: 'object',
27
+ properties: {
28
+ id: { type: 'string' },
29
+ name: { type: 'string' },
30
+ mood: { type: 'string' },
31
+ bestFor: { type: 'string' },
32
+ dark: { type: 'boolean' },
33
+ palette: { type: 'object', additionalProperties: true },
34
+ fonts: { type: 'object', additionalProperties: true },
35
+ },
36
+ additionalProperties: true,
37
+ };
38
+ const themesResultSchema = {
39
+ type: 'object',
40
+ properties: {
41
+ ok: { type: 'boolean' },
42
+ themes: { type: 'array', items: themeInfoSchema },
43
+ },
44
+ additionalProperties: true,
45
+ };
46
+ const createResultSchema = {
47
+ type: 'object',
48
+ properties: {
49
+ ok: { type: 'boolean' },
50
+ title: { type: 'string' },
51
+ theme: { type: 'string' },
52
+ language: { type: 'string' },
53
+ slideCount: { type: 'integer' },
54
+ outputDir: { type: 'string' },
55
+ files: {
56
+ type: 'object',
57
+ properties: {
58
+ html: { type: 'string' },
59
+ pptx: { type: 'string' },
60
+ json: { type: 'string' },
61
+ },
62
+ additionalProperties: true,
63
+ },
64
+ htmlPath: { type: 'string' },
65
+ pptxPath: { type: 'string' },
66
+ jsonPath: { type: 'string' },
67
+ },
68
+ additionalProperties: true,
69
+ };
70
+ function oneText(text) {
71
+ return [{ type: 'text', text }];
72
+ }
73
+ function renderThemes(value) {
74
+ if (value.themes.length === 0)
75
+ return oneText('dsh-ppt 没有可用主题。');
76
+ const lines = value.themes.map((theme) => '- ' + theme.id + ':' + theme.name + '(' + theme.mood + ')|适合:' + theme.bestFor + '|' + (theme.dark ? '深色' : '浅色'));
77
+ return oneText('dsh-ppt 内置主题:\n\n' + lines.join('\n') + '\n\nppt_create 的 theme 参数填其中的 id(默认 data)。');
78
+ }
79
+ function renderCreate(value) {
80
+ return oneText('dsh-ppt 已生成 ' + value.slideCount + ' 页演示文稿(主题 ' + value.theme + ',语言 ' + value.language + '):\n' +
81
+ 'HTML 网页放映:' + value.htmlPath + '\n' +
82
+ 'PPTX 导出:' + value.pptxPath + '\n' +
83
+ 'Manifest:' + value.jsonPath + '\n' +
84
+ 'HTML 双击即可放映(方向键翻页 / F 全屏 / G 总览 / P 打印);PPTX 可用 PowerPoint / WPS / Keynote 打开。');
85
+ }
86
+ /** Assemble presentation schemas, rendering and the session-aware executors. */
87
+ export function buildPptTools(config) {
88
+ const executors = createPptExecutors(config);
89
+ return [
90
+ {
91
+ name: 'ppt_themes',
92
+ description: 'List the built-in visual themes of dsh-ppt (id, name, mood, best-for, light/dark palette) before building a deck. Use a theme id as the theme argument of ppt_create. 中文:列出 dsh-ppt 内置视觉主题(id、名称、情绪、适用场景、明暗色板),用于选择 ppt_create 的 theme 参数。',
93
+ parameters: compileParameters({
94
+ lang: { type: 'string', description: 'Theme description language: zh (default), en, or bilingual.' },
95
+ }),
96
+ output: {
97
+ schema: themesResultSchema,
98
+ render: (_args, value) => renderThemes(value),
99
+ },
100
+ execute: executors.themes,
101
+ },
102
+ {
103
+ name: 'ppt_create',
104
+ description: 'Build a complete presentation deck from one sentence or a Markdown document and write three artifacts to outputDir: a standalone HTML web slideshow, an editable 16:9 PPTX, and a deck.json manifest. Five built-in visual themes are available (see ppt_themes). content is Markdown text (recommended); advanced callers may pass structured slides instead. 中文:把一句话或一篇 Markdown 文档生成完整演示文稿,写入 outputDir 三个文件:独立 HTML 网页放映、可编辑 16:9 PPTX、deck.json manifest;内置 5 套视觉主题。',
105
+ parameters: compileParameters({
106
+ title: { type: 'string', required: true, description: 'Deck title (used for the cover and file names).' },
107
+ content: { type: 'string', description: 'Markdown content: one sentence, a paragraph, or a full document. First # heading becomes the cover title; ## headings become slides; -/* lists become bullets; | ... | tables become table slides; > blockquotes become quote slides; <!-- 备注: ... --> comments become speaker notes. Required unless slides is provided.' },
108
+ theme: { type: 'string', description: 'Visual theme id: swiss / velvet / data / soft / bold. Default data. See ppt_themes.' },
109
+ lang: { type: 'string', description: 'UI language of the generated player: zh (default), en, or bilingual. Content language is whatever you write.' },
110
+ motion: { type: 'string', enum: ['on', 'off'], description: 'Slide transitions plus bullet entrance animations: on (default) or off for a fully static deck.' },
111
+ slides: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Optional structured slides: [{ layout: cover|section|bullets|statement|quote|table|closing, title, subtitle, kicker, bullets: [], rows: [][] (for table), notes: "speaker notes" }]. Use this for precise control instead of content.' },
112
+ outputDir: { type: 'string', description: 'Directory to write the files into. Default: session working directory (or the plugin outputDir config).' },
113
+ fileName: { type: 'string', description: 'Base file name for the three artifacts. Default: sanitized deck title.' },
114
+ overwrite: { type: 'boolean', description: 'Replace an existing same-name HTML/PPTX/JSON trio. Default false: choose a unique numeric suffix instead.' },
115
+ }),
116
+ output: {
117
+ schema: createResultSchema,
118
+ render: (_args, value) => renderCreate(value),
119
+ },
120
+ execute: executors.create,
121
+ },
122
+ ];
123
+ }
package/lib/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** dsh-ppt 的公共类型。 */
2
2
  export type PptThemeId = 'swiss' | 'velvet' | 'data' | 'soft' | 'bold';
3
3
  export type PptLanguage = 'zh' | 'en' | 'bilingual';
4
- export type PptSlideLayout = 'cover' | 'section' | 'bullets' | 'statement' | 'closing';
4
+ export type PptSlideLayout = 'cover' | 'section' | 'bullets' | 'statement' | 'quote' | 'table' | 'closing';
5
5
  export interface PptSlideSpec {
6
6
  layout?: PptSlideLayout;
7
7
  title?: string;
@@ -9,9 +9,13 @@ export interface PptSlideSpec {
9
9
  kicker?: string;
10
10
  text?: string;
11
11
  bullets?: string[];
12
+ /** 表格页数据;第一行作为表头。 */
13
+ rows?: Array<Array<string | number>>;
14
+ /** 演讲者备注;写入 HTML 备注面板与 PPTX 原生备注页。 */
15
+ notes?: string;
12
16
  }
13
17
  export interface PptConfig {
14
- /** 默认输出目录;调用 ppt_create 时可用 outputDir 覆盖。默认当前工作目录。 */
18
+ /** 默认输出目录;调用 ppt_create 时可用 outputDir 覆盖。相对路径按会话工作目录解析。 */
15
19
  outputDir?: string;
16
20
  /** 单次生成幻灯片上限,默认 60(3–120)。 */
17
21
  maxSlides?: number;
@@ -32,6 +36,8 @@ export interface PptCreateArgs {
32
36
  motion?: 'on' | 'off';
33
37
  outputDir?: string;
34
38
  fileName?: string;
39
+ /** 是否覆盖同名三件套;默认 false,同名时自动选择唯一后缀。 */
40
+ overwrite?: boolean;
35
41
  }
36
42
  export interface PptThemeInfo {
37
43
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ppt",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "DSH 演示文稿技能 + 工具插件:一句话或一篇 Markdown 文档 → 完整演示文稿(独立 HTML 网页放映 + 可编辑 PPTX 导出),内置 5 套视觉主题与页间转场/要点入场动画(motion 可关),可配置默认主题/语言,中英双语,SKILL.md 薄路由 + references 按需装载,跨 harness。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -51,7 +51,7 @@
51
51
  ],
52
52
  "license": "MIT",
53
53
  "engines": {
54
- "node": ">=22"
54
+ "node": "^22.19.0 || >=24.0.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/node": "^24.0.0",
@@ -30,6 +30,7 @@ const HELP = `dsh-ppt —— 一句话 / 一篇文档 → HTML 放映 + PPTX 导
30
30
  --motion <on|off> 页间转场与要点入场动画(默认 on;off 产出纯静态)
31
31
  --out <dir> 输出目录(默认当前目录)
32
32
  --file <name> 文件名前缀(默认取标题)
33
+ --overwrite 显式覆盖同名三件套(默认自动追加 -1/-2…)
33
34
  --list-themes 列出内置主题
34
35
  --help 显示本帮助
35
36
 
@@ -40,7 +41,7 @@ const HELP = `dsh-ppt —— 一句话 / 一篇文档 → HTML 放映 + PPTX 导
40
41
  `
41
42
 
42
43
  export function parseArgv(argv) {
43
- const args = { content: '', slides: null, lang: 'zh', theme: DEFAULT_THEME, motion: undefined, out: '.', file: '', title: '' }
44
+ const args = { content: '', slides: null, lang: 'zh', theme: DEFAULT_THEME, motion: undefined, out: '.', file: '', title: '', overwrite: false }
44
45
  const positional = []
45
46
  for (let i = 0; i < argv.length; i += 1) {
46
47
  const raw = String(argv[i])
@@ -55,6 +56,7 @@ export function parseArgv(argv) {
55
56
  }
56
57
  if (key === '--help' || key === '-h') args.help = true
57
58
  else if (key === '--list-themes') args.listThemes = true
59
+ else if (key === '--overwrite') args.overwrite = true
58
60
  else if (key === '--title') args.title = nextValue()
59
61
  else if (key === '--content') args.content = nextValue()
60
62
  else if (key === '--slides') args.slides = JSON.parse(nextValue())
@@ -110,6 +112,7 @@ export async function main(argv = process.argv.slice(2), io = {}) {
110
112
  motion: args.motion,
111
113
  outputDir: resolvePath(args.out || '.'),
112
114
  fileName: args.file,
115
+ overwrite: args.overwrite,
113
116
  }
114
117
  const result = buildDeck(options)
115
118
  log('dsh-ppt 已生成 ' + result.slideCount + ' 页演示文稿:')
@@ -14,7 +14,7 @@
14
14
  * 零运行时依赖,仅使用 node:fs / node:path / node:zlib。
15
15
  */
16
16
 
17
- import { mkdirSync, writeFileSync } from 'node:fs'
17
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
18
18
  import { resolve as resolvePath } from 'node:path'
19
19
  import { deflateRawSync } from 'node:zlib'
20
20
 
@@ -249,6 +249,22 @@ export function sanitizeFileName(input) {
249
249
  return cleaned.slice(0, 120) || 'deck'
250
250
  }
251
251
 
252
+ const DECK_ARTIFACT_EXTENSIONS = ['.html', '.pptx', '.json']
253
+
254
+ /**
255
+ * 为三件套选择同一个可用文件名前缀。默认不覆盖:任一产物已存在时,
256
+ * 整组改用 -1/-2… 后缀,避免新旧 deck 被静默混写。
257
+ */
258
+ function resolveDeckFileName(outputDir, requestedFileName, overwrite) {
259
+ const isAvailable = (candidate) => DECK_ARTIFACT_EXTENSIONS.every((ext) => !existsSync(resolvePath(outputDir, candidate + ext)))
260
+ if (overwrite || isAvailable(requestedFileName)) return requestedFileName
261
+ for (let index = 1; index < 1000; index += 1) {
262
+ const candidate = requestedFileName + '-' + index
263
+ if (isAvailable(candidate)) return candidate
264
+ }
265
+ throw new Error('dsh-ppt:找不到可用的输出文件名(同名前缀超过 999 个),请更换 fileName 或显式允许 overwrite。')
266
+ }
267
+
252
268
  export function escapeXml(value) {
253
269
  return String(value ?? '')
254
270
  .replace(/&/g, '&amp;')
@@ -654,13 +670,15 @@ export function normalizeBuildOptions(options = {}) {
654
670
  if (deck.slides.length < 1) throw new Error('dsh-ppt:没有可生成的幻灯片')
655
671
  const outputDir = resolvePath(String(options.outputDir ?? '.').trim() || '.')
656
672
  const fileName = sanitizeFileName(options.fileName ?? title)
657
- return { title, theme, language, motion, deck, outputDir, fileName }
673
+ const overwrite = options.overwrite === true
674
+ return { title, theme, language, motion, deck, outputDir, fileName, overwrite }
658
675
  }
659
676
 
660
677
  export function buildDeck(options = {}) {
661
678
  const normalized = normalizeBuildOptions(options)
662
- const { title, theme, language, motion, deck, outputDir, fileName } = normalized
679
+ const { title, theme, language, motion, deck, outputDir, overwrite } = normalized
663
680
  mkdirSync(outputDir, { recursive: true })
681
+ const fileName = resolveDeckFileName(outputDir, normalized.fileName, overwrite)
664
682
 
665
683
  const manifest = {
666
684
  version: DECK_VERSION,