@deepwhale-cn/dsh-pptx-batch 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 深鲸 DeepWhale
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,100 @@
1
- # Temporary Holding Version
1
+ # @deepwhale-cn/dsh-pptx-batch
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/feely0208/dsh-pptx-batch)
4
+
5
+ **整目录的 PPT 一次处理完**:批量填模板占位符、按形状名换图片、提取每页文本、看页数/形状数/图片数 —— 不打开 PowerPoint,不动原文件。
6
+
7
+ **关键词**:PPT 批量填模板 · pptx 批量替换 · PPT 批量换 Logo · 提取 PPT 文本 · PPT 页数统计 · 汇报批量生成 · 模板占位符替换 · 命令行处理 PPT
8
+
9
+ ## 不装会怎样
10
+
11
+ 30 份给不同客户的报价 PPT,模板里写着 `{{客户}}`、`{{金额}}`:不装就得一份份打开、双击文本框、输入、保存、关掉,
12
+ 一小时起步,改到第 20 份时手一滑把上一位客户的名字留在下一份里,也没人发现。
13
+
14
+ 五十份材料的 Logo 要换新版:人工做法是逐份删图、插图、再对齐一遍位置,版面还容易歪。
15
+
16
+ 要核对一批 PPT 每页写了什么、各有多少页多少张图:只能一份份翻,翻完还记不住。
17
+
18
+ 这些都属于「机械但量大、还容易漏」的活。更麻烦的是直接让 AI 改原文件,改坏了没有退路 ——
19
+ 这个插件**只读输入**,产物写到单独的输出目录,同名文件自动加序号,不覆盖任何东西。
20
+
21
+ ## 能做什么
22
+
23
+ | 子命令 | 作用 | 产物(可核对) |
24
+ |---|---|---|
25
+ | `fill` | 按 `--set key=value` 批量填幻灯片里的 `{{key}}` 占位符(文本框、占位符、表格单元格、组合形状) | 填好的 `.pptx` 副本;JSON 里给出每份替换了几处、哪个 key 几处 |
26
+ | `replace-image` | 把名为 `--shape` 的图片形状换成 `--image` 指定的图片 | 换图后的 `.pptx` 副本;图片数量不变、位置尺寸不变、形状名不变 |
27
+ | `extract-text` | 提取每页文本:标题 + 正文 + 表格 + 备注 | 每份 PPT 一个同名 `.txt`,按「第 N 页」分块 |
28
+ | `info` | 输出每份 PPT 的页数、形状数、图片数 | 终端表格;`--json` 给脚本用 |
29
+
30
+ 共同约定:
31
+
32
+ - **不修改输入文件**,默认输出到 `<输入目录>/_输出`,可用 `--out` 指定;
33
+ - **同名文件不覆盖**:已存在时自动变成 `简报_1.pptx`、`简报_2.pptx`;
34
+ - 目录递归处理,自动跳过 PowerPoint 的 `~$` 临时文件和 `_输出` 目录;
35
+ - `--json` 输出结构化结果,方便脚本 / CI 断言;出错时退出码非 0 并说明缺什么;
36
+ - `fill` 填完会报出**还剩哪些 `{{...}}` 没给值**,模板里拼错 key 一眼能看出来。
37
+
38
+ ```bash
39
+ # 批量填模板:一份模板 + 一个客户一套值
40
+ dsh-pptx-batch fill --input ./模板 --out ./已填 \
41
+ --set 客户=客户甲 --set 金额=128000 --set 日期=2026-10-24
42
+
43
+ # 批量换 Logo(形状名要和 PowerPoint「选择窗格」里显示的一致)
44
+ dsh-pptx-batch replace-image --input ./已填 --out ./换图 \
45
+ --shape Logo --image ./logo-新.png
46
+
47
+ # 提取每页文本
48
+ dsh-pptx-batch extract-text --input ./模板 --out ./文本
49
+
50
+ # 看一眼结构
51
+ dsh-pptx-batch info --input ./模板 --json
52
+ ```
53
+
54
+ `fill` 保留格式的方式:占位符整段落在一个 run 里时,这个 run 的字体、字号、颜色完全不动;
55
+ 只有跨多个 run 的占位符(同一段字被 PowerPoint 拆成几块)才会把这几段并到起始 run 的格式上。
56
+
57
+ ## 不能做什么
58
+
59
+ - ❌ 只支持 `.pptx`;`.ppt` 老格式、`.pptm`、加密/需要密码打开的文档不支持 —— `.ppt` 请先用 PowerPoint/WPS 另存为 `.pptx`。
60
+ - ❌ `fill` **不处理**图表文字、SmartArt 文字、图片里的文字、批注、母版/版式上的文字,也不跨段落匹配。
61
+ - ❌ `fill` 是**字面匹配**(`{{key}}`,key 大小写敏感,允许 `{{ key }}` 这种空格),不支持正则、不支持一次给同名 key 多个值。
62
+ - ❌ **不保证排版 100% 不变**:跨 run 的占位符替换会合并该段文字的 run,段落内局部加粗/字号/颜色可能丢失;
63
+ 重要模板请先跑一份对比再批量用。
64
+ - ❌ `replace-image` 只换图片内容:新图比例和原图不同时会被**拉伸**(裁剪设置沿用原来的);
65
+ 只按形状名精确匹配,不支持按内容或页码挑选,同名形状会全部替换。
66
+ - ❌ `extract-text` 不提取图表数据、SmartArt、页码与母版文字;图片只留位置不留文字(没有 OCR)。
67
+ - ❌ `info` 的「形状数」是每页顶层形状数之和(组合形状算 1 个,其内部形状不计入)。
68
+ - ❌ 不是格式转换器:不导出 PDF、不做 OCR、不做批量重命名。
69
+ - ✅ 全部在本机由 python-pptx 处理,不联网、不上传文件。
70
+
71
+ ## 安装
72
+
73
+ ```bash
74
+ dsh plugin --profile web add @deepwhale-cn/dsh-pptx-batch
75
+ ```
76
+
77
+ 运行依赖本机的 **python3 + python-pptx**(插件不会替你装)。缺依赖时 CLI 会直接打印安装命令:
78
+
79
+ ```bash
80
+ python3 -m pip install python-pptx
81
+ # macOS 没有 python3 时
82
+ brew install python3
83
+ ```
84
+
85
+ ## 用法
86
+
87
+ ```bash
88
+ dsh-pptx-batch --help
89
+ dsh-pptx-batch --demo # 自造样例 PPT 跑完整流程,打印产物路径
90
+ dsh-pptx-batch <action> --input <文件或目录> [--out <目录>] [--json]
91
+ ```
92
+
93
+ `--demo` 会在系统临时目录里造两份带占位符、表格和图片形状的样例 PPT,
94
+ 依次跑 fill / replace-image / extract-text / info,不动你的任何文件,跑完直接看产物路径就知道它能干什么。
95
+
96
+ 退出码:`0` 成功 · `1` 运行失败 · `2` 参数或输入问题 · `3` 缺依赖。
97
+
98
+ ## License
99
+
100
+ MIT © 温州深鲸智能科技有限公司
package/bin/cli.mjs ADDED
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PPT 批量处理 CLI
4
+ *
5
+ * 形态:
6
+ * dsh-pptx-batch --help (无参数也打印 help,退出 0)
7
+ * dsh-pptx-batch --demo (自造样例跑完整流程,退出 0)
8
+ * dsh-pptx-batch <action> --input <文件或目录> [--out <目录>] [--json]
9
+ *
10
+ * 真实逻辑全部委托 python3 + python-pptx(lib/pptx_tool.py),
11
+ * Node 只负责参数校验、调用与人类可读输出。
12
+ */
13
+ import { spawnSync } from 'node:child_process';
14
+ import { dirname, join } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
18
+ const PY_SCRIPT = join(PKG_ROOT, 'lib', 'pptx_tool.py');
19
+ const TITLE = 'PPT 批量处理';
20
+ const CMD = 'dsh-pptx-batch';
21
+ const ACTIONS = {
22
+ fill: '按 --set key=value 批量填模板里的 {{key}} 占位符',
23
+ 'replace-image': '把名为 --shape 的图片形状换成 --image 指定的图片',
24
+ 'extract-text': '提取每页文本(标题 + 正文 + 表格)到同名 .txt',
25
+ info: '输出每份 PPT 的页数 / 形状数 / 图片数',
26
+ };
27
+ const PY_HINT = [
28
+ '需要 python3 + python-pptx。安装:',
29
+ ' python3 -m pip install python-pptx',
30
+ '没有 python3 时:',
31
+ ' macOS: brew install python3',
32
+ ' Ubuntu: sudo apt install python3',
33
+ ].join('\n');
34
+ const LINE = '─'.repeat(56);
35
+
36
+ function help() {
37
+ console.log(`${TITLE}
38
+
39
+ 用法:
40
+ ${CMD} <action> --input <文件或目录> [--out <目录>] [--json]
41
+ ${CMD} --demo
42
+ ${CMD} --help
43
+
44
+ 子命令:
45
+ ${Object.entries(ACTIONS).map(([k, v]) => ` ${k.padEnd(14)}${v}`).join('\n')}
46
+
47
+ 参数:
48
+ --input, -i .pptx 文件或目录(目录递归处理)
49
+ --out, -o 输出目录;默认 <输入目录>/_输出;同名文件不覆盖,自动加序号
50
+ --set key=value,可重复;对应幻灯片里的 {{key}} 占位符
51
+ --shape 要替换的图片形状名(精确匹配,大小写敏感)
52
+ --image 新图片文件(png / jpg / gif / bmp / tiff)
53
+ --json 输出 JSON(给脚本 / CI 用)
54
+ --demo 自造样例 PPT,跑完整流程并打印产物路径
55
+
56
+ 示例:
57
+ ${CMD} fill --input ./模板 --set 客户=客户甲 --set 金额=128000
58
+ ${CMD} replace-image --input ./已填 --shape Logo --image ./logo-新.png
59
+ ${CMD} extract-text --input ./模板 --out ./文本
60
+ ${CMD} info --input ./模板 --json
61
+
62
+ 依赖:python3 + python-pptx(缺失时会提示安装命令)`);
63
+ process.exit(0);
64
+ }
65
+
66
+ function fail(message, hint = '', code = 2, json = false) {
67
+ if (json) {
68
+ console.log(JSON.stringify({ ok: false, code, message, hint }, null, 2));
69
+ } else {
70
+ console.error(`❌ ${message}`);
71
+ if (hint) console.error(`\n${hint}`);
72
+ }
73
+ process.exit(code);
74
+ }
75
+
76
+ function findPython() {
77
+ const candidates = [
78
+ process.env.DSH_PYTHON,
79
+ process.env.PPTX_BATCH_PYTHON,
80
+ '/opt/homebrew/bin/python3',
81
+ 'python3',
82
+ '/usr/local/bin/python3',
83
+ '/usr/bin/python3',
84
+ ].filter(Boolean);
85
+ for (const cmd of candidates) {
86
+ const r = spawnSync(cmd, ['-c', 'import sys; print(sys.version_info[0])'], {
87
+ encoding: 'utf8',
88
+ timeout: 10000,
89
+ });
90
+ if (!r.error && r.status === 0) return cmd;
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function parseArgs(argv) {
96
+ const opt = {
97
+ action: null, input: null, out: null, set: [], shape: null, image: null,
98
+ json: false, demo: false,
99
+ };
100
+ const VALUE_FLAGS = new Set(['--input', '-i', '--out', '-o', '--set', '--shape', '--image']);
101
+ for (let i = 0; i < argv.length; i++) {
102
+ const raw = argv[i];
103
+ const eq = raw.indexOf('=');
104
+ const flag = eq > 0 ? raw.slice(0, eq) : raw;
105
+ const inline = eq > 0 ? raw.slice(eq + 1) : undefined;
106
+ if (flag === '--json') {
107
+ opt.json = true;
108
+ continue;
109
+ }
110
+ if (flag === '--demo') {
111
+ opt.demo = true;
112
+ continue;
113
+ }
114
+ if (flag === '--help' || flag === '-h') help();
115
+ if (VALUE_FLAGS.has(flag)) {
116
+ let value = inline;
117
+ if (value === undefined) {
118
+ value = argv[i + 1];
119
+ if (value === undefined || value.startsWith('--')) {
120
+ fail(`参数 ${flag} 缺少取值`, `例:${flag} <值>`);
121
+ }
122
+ i += 1;
123
+ }
124
+ if (flag === '--input' || flag === '-i') opt.input = value;
125
+ else if (flag === '--out' || flag === '-o') opt.out = value;
126
+ else if (flag === '--shape') opt.shape = value;
127
+ else if (flag === '--image') opt.image = value;
128
+ else opt.set.push(value);
129
+ continue;
130
+ }
131
+ if (flag.startsWith('-')) {
132
+ fail(`未知参数:${raw}`, `用 ${CMD} --help 查看支持的参数。`);
133
+ }
134
+ if (!opt.action) {
135
+ opt.action = raw;
136
+ continue;
137
+ }
138
+ fail(`多余的参数:${raw}`, '一次只能执行一个子命令。');
139
+ }
140
+ return opt;
141
+ }
142
+
143
+ function human(res) {
144
+ console.log('');
145
+ console.log(` ${TITLE} · ${res.action}`);
146
+ console.log(` ${LINE}`);
147
+ if (res.input) console.log(` 输入 ${res.input}`);
148
+ if (res.out) console.log(` 输出目录 ${res.out}`);
149
+ if (res.image) console.log(` 新图片 ${res.image}`);
150
+ console.log('');
151
+ for (const f of res.files || []) {
152
+ switch (res.action) {
153
+ case 'fill': {
154
+ const keys = Object.entries(f.keys || {})
155
+ .map(([k, v]) => `${k}×${v}`).join(' ');
156
+ console.log(` ✓ ${f.input_name} → ${f.output_name} · 替换 ${f.replacements} 处${keys ? `(${keys})` : ''}`);
157
+ break;
158
+ }
159
+ case 'replace-image':
160
+ if (f.replaced === 0) {
161
+ console.log(` – ${f.input_name} → ${f.output_name} · 跳过(没有该形状)`);
162
+ } else {
163
+ console.log(` ✓ ${f.input_name} → ${f.output_name} · 替换图片 ${f.replaced} 处`);
164
+ }
165
+ break;
166
+ case 'extract-text':
167
+ console.log(` ✓ ${f.input_name} → ${f.output_name} · ${f.slides} 页 · ${f.chars} 字符`);
168
+ break;
169
+ case 'info':
170
+ console.log(` ✓ ${f.input_name} · ${f.pages} 页 · ${f.shapes} 个形状 · ${f.pictures} 张图片`);
171
+ break;
172
+ default:
173
+ console.log(` ✓ ${f.input_name} → ${f.output_name}`);
174
+ }
175
+ }
176
+ if (res.unfilled && res.unfilled.length) {
177
+ console.log(`\n ⚠ 还有 ${res.unfilled.length} 个占位符没给值:${res.unfilled.join('、')}`);
178
+ console.log(' 确认 --set 的 key 和模板里的 {{key}} 完全一致(大小写、空格)。');
179
+ }
180
+ if (res.summary) console.log(`\n ${res.summary}`);
181
+ if (res.artifacts && res.artifacts.length) {
182
+ console.log('\n 产物:');
183
+ for (const a of res.artifacts) console.log(` ${a}`);
184
+ }
185
+ console.log('');
186
+ }
187
+
188
+ function humanDemo(res) {
189
+ console.log('');
190
+ console.log(` ${TITLE} · 演示(--demo)`);
191
+ console.log(` ${LINE}`);
192
+ console.log(` 演示目录 ${res.demoRoot}`);
193
+ console.log('');
194
+ for (const s of res.steps || []) {
195
+ console.log(` ✓ ${String(s.step).padEnd(6)} ${s.action.padEnd(14)} ${s.summary}`);
196
+ }
197
+ console.log('\n 产物:');
198
+ for (const a of res.artifacts || []) console.log(` ${a}`);
199
+ console.log('');
200
+ }
201
+
202
+ function callPython(py, action, payload) {
203
+ const r = spawnSync(py, [PY_SCRIPT, action, JSON.stringify(payload)], {
204
+ encoding: 'utf8',
205
+ maxBuffer: 128 * 1024 * 1024,
206
+ });
207
+ if (r.error) {
208
+ fail(`调用 python 失败:${r.error.message}`, PY_HINT, 3);
209
+ }
210
+ const lines = (r.stdout || '').trim().split('\n').filter(Boolean);
211
+ let result = null;
212
+ try {
213
+ result = JSON.parse(lines[lines.length - 1] || '');
214
+ } catch {
215
+ /* 下面统一报错 */
216
+ }
217
+ if (!result) {
218
+ const detail = (r.stderr || '').trim().split('\n').slice(-5).join('\n');
219
+ fail('Python 处理失败(没有拿到有效结果)', detail || PY_HINT, r.status || 1);
220
+ }
221
+ return result;
222
+ }
223
+
224
+ const argv = process.argv.slice(2);
225
+ if (argv.length === 0) help();
226
+ if (argv.includes('--help') || argv.includes('-h')) help();
227
+
228
+ const opt = parseArgs(argv);
229
+ const python = findPython();
230
+ if (!python) {
231
+ fail('未找到 python3,无法处理 PPT 文件', PY_HINT, 3, opt.json);
232
+ }
233
+
234
+ if (opt.demo) {
235
+ const res = callPython(python, 'demo', {});
236
+ if (!res.ok) fail(res.message, res.hint, res.code || 1, opt.json);
237
+ if (opt.json) console.log(JSON.stringify(res, null, 2));
238
+ else humanDemo(res);
239
+ process.exit(0);
240
+ }
241
+
242
+ if (!opt.action) {
243
+ fail('缺少子命令', `可用子命令:${Object.keys(ACTIONS).join(' / ')}\n用 ${CMD} --help 查看详情。`, 2, opt.json);
244
+ }
245
+ if (!Object.hasOwn(ACTIONS, opt.action)) {
246
+ fail(`未知子命令:${opt.action}`, `可用子命令:${Object.keys(ACTIONS).join(' / ')}`, 2, opt.json);
247
+ }
248
+ if (!opt.input) {
249
+ fail('缺少 --input(文件或目录)', `例:${CMD} ${opt.action} --input ./PPT目录`, 2, opt.json);
250
+ }
251
+ if (opt.action === 'fill') {
252
+ if (opt.set.length === 0) {
253
+ fail('缺少 --set(要填的占位符)',
254
+ `例:${CMD} fill --input ./模板 --set 客户=客户甲 --set 金额=128000`, 2, opt.json);
255
+ }
256
+ const bad = opt.set.find((s) => !s.includes('=') || !s.split('=')[0].trim());
257
+ if (bad !== undefined) {
258
+ fail(`--set 需要 key=value 形式:${bad}`, '例:--set 客户=客户甲', 2, opt.json);
259
+ }
260
+ }
261
+ if (opt.action === 'replace-image') {
262
+ if (!opt.shape) {
263
+ fail('缺少 --shape(要替换的图片形状名)',
264
+ `例:${CMD} replace-image --input ./模板 --shape Logo --image ./logo.png`, 2, opt.json);
265
+ }
266
+ if (!opt.image) {
267
+ fail('缺少 --image(新图片文件)',
268
+ `例:${CMD} replace-image --input ./模板 --shape Logo --image ./logo.png`, 2, opt.json);
269
+ }
270
+ }
271
+
272
+ const payload = {
273
+ input: opt.input,
274
+ out: opt.out,
275
+ set: opt.set,
276
+ shape: opt.shape,
277
+ image: opt.image,
278
+ };
279
+ const res = callPython(python, opt.action, payload);
280
+ if (!res.ok) fail(res.message, res.hint, res.code || 1, opt.json);
281
+
282
+ if (opt.json) console.log(JSON.stringify(res, null, 2));
283
+ else human(res);
284
+ process.exit(0);
@@ -0,0 +1,4 @@
1
+ # @deepwhale-cn/dsh-pptx-batch 的组合层:装上即注册 pptx-batch skill
2
+ - insert:
3
+ - id: pptx-batch
4
+ name: '@deepwhale-cn/dsh-pptx-batch'
package/lib/api.js ADDED
@@ -0,0 +1,27 @@
1
+ /** 深鲸 PPT 批量 · 宿主出口 */
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { readFileSync } from 'node:fs';
5
+ import { run } from './core.mjs';
6
+
7
+ const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
8
+ const ROUTE = '/dsh-pptx-batch';
9
+ const VERSION = (() => { try { return JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version || ''; } catch { return ''; } })();
10
+ const 回 = (res, code, obj) => { const b = JSON.stringify(obj); res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'content-length': Buffer.byteLength(b) }); res.end(b); };
11
+ const 读体 = async (req) => { const c = []; for await (const x of req) c.push(x); const t = Buffer.concat(c).toString('utf8'); if (!t) return {}; try { return JSON.parse(t); } catch { return {}; } };
12
+
13
+ export function registerApi(ctx) {
14
+ const handler = async (req, res) => {
15
+ try {
16
+ const u = new URL(req.url || '/', 'http://127.0.0.1');
17
+ if (u.pathname === `${ROUTE}/api/info`) return 回(res, 200, { ok: true, version: VERSION });
18
+ if (u.pathname === `${ROUTE}/api/run` && req.method === 'POST') {
19
+ const b = await 读体(req);
20
+ const r = await run(b);
21
+ return 回(res, 200, r);
22
+ }
23
+ return 回(res, 404, { ok: false, error: 'not found' });
24
+ } catch (e) { return 回(res, 500, { ok: false, text: e?.message || String(e) }); }
25
+ };
26
+ ctx.webServer.register({ kind: 'prefix', path: ROUTE, handler });
27
+ }
package/lib/client.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * 深鲸 PPT 批量 —— 面板(统一模板:表单 → /api/run → 文本结果)
3
+ */
4
+ window.__ModuleLoader__.load({
5
+ id: '@deepwhale-cn/dsh-pptx-batch',
6
+ factory: (require) => {
7
+ var module = { exports: {} }; var exports = module.exports;
8
+ const react = require('react'); const h = react.createElement;
9
+ const SLOT_ICON = 'sidebar.panellist'; const SLOT_MAIN = 'main'; const PANEL_ID = 'pptx-batch';
10
+ const API = '/dsh-pptx-batch/api'; const BADGE = 'data:image/png;base64,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';
11
+ const OPS = [{"key": "extract-text", "label": "提取文字"}, {"key": "extract-images", "label": "导出图片"}, {"key": "list", "label": "统计页数"}];
12
+ const FIELDS = [{"k": "input", "label": "文件/目录", "ph": "例如 /Users/mac/提案"}, {"k": "out", "label": "图片输出目录", "ph": "导出图片时用;留空 = 输入目录/_图片"}];
13
+ const Ico = ({ size = 18 } = {}) => h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round' }, [
14
+ h('path', { key: 'a', d: 'M4 5h16v14H4z' }), h('path', { key: 'b', d: 'M8 9h8M8 13h8M8 17h5' }),
15
+ ]);
16
+ const CSS = `
17
+ .gn-root{position:relative;display:flex;flex-direction:column;gap:14px;padding:18px 22px 52px;height:100%;overflow:auto}
18
+ .gn-head{display:flex;align-items:center;gap:10px}
19
+ .gn-title{font-size:16px;font-weight:600}
20
+ .gn-sub{color:var(--dsw-alias-text-secondary,#8b98a8);font-size:12px}
21
+ .gn-ver{margin-left:8px;font-size:11px;opacity:.75;border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.12));border-radius:6px;padding:0 5px}
22
+ .gn-card{border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.08));border-radius:12px;background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.03));padding:14px;display:flex;flex-direction:column;gap:10px}
23
+ .gn-row{display:flex;gap:8px;align-items:center}
24
+ .gn-label{font-size:12px;color:var(--dsw-alias-text-secondary,#8b98a8);width:92px;flex:none}
25
+ .gn-input,.gn-select{flex:1;min-width:0;padding:7px 9px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.12));background:var(--dsw-alias-bg-base,transparent);color:inherit;font-size:13px;font-family:inherit}
26
+ .gn-btn{padding:7px 16px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.14));background:var(--dsw-alias-brand-primary,#2f6f9f);color:#fff;font-size:13px;cursor:pointer}
27
+ .gn-btn[disabled]{opacity:.55;cursor:default}
28
+ .gn-out{white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,Menlo,monospace;font-size:12px;line-height:1.65;max-height:420px;overflow:auto;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.18));border-radius:8px;padding:10px}
29
+ .gn-ok{color:var(--dsw-alias-state-success-primary,#46b47a)}.gn-err{color:var(--dsw-alias-state-error-primary,#e0645a)}
30
+ .gn-tip{font-size:12px;color:var(--dsw-alias-text-secondary,#8b98a8)}
31
+ .gn-badge{position:absolute;right:16px;bottom:12px;display:inline-flex;opacity:.9}
32
+ .gn-badge img{width:121px;height:20px;display:block}
33
+ `;
34
+ function 面板() {
35
+ const [op, 设op] = react.useState(OPS[0] ? OPS[0].key : '');
36
+ const [values, 设values] = react.useState(() => Object.fromEntries(FIELDS.map((f) => [f.k, ''])));
37
+ const [版本, 设版本] = react.useState('');
38
+ const [跑, 设跑] = react.useState(false);
39
+ const [结果, 设结果] = react.useState(null);
40
+ react.useEffect(() => { void (async () => { try { const r = await fetch(`${API}/info`); const j = await r.json(); if (j.version) 设版本(String(j.version)); } catch {} })(); }, []);
41
+ const 执行 = async () => {
42
+ 设跑(true); 设结果(null);
43
+ try {
44
+ const res = await fetch(`${API}/run`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ op, ...values }) });
45
+ 设结果(await res.json());
46
+ } catch (e) { 设结果({ ok: false, text: String(e) }); } finally { 设跑(false); }
47
+ };
48
+ return h('div', { className: 'gn-root' }, [
49
+ h('style', { key: 'c' }, CSS),
50
+ h('div', { key: 'h', className: 'gn-head' }, [
51
+ h(Ico, { key: 'i' }),
52
+ h('div', { key: 't' }, [
53
+ h('div', { key: 'a', className: 'gn-title' }, '深鲸 PPT 批量'),
54
+ h('div', { key: 'b', className: 'gn-sub' }, ['提取文字 / 导出图片 / 看页数,一次处理整目录', 版本 ? h('span', { key: 'v', className: 'gn-ver' }, 'v' + 版本) : null]),
55
+ ]),
56
+ ]),
57
+ h('div', { key: 'f', className: 'gn-card' }, [
58
+ OPS.length > 1 ? h('div', { key: 'op', className: 'gn-row' }, [
59
+ h('span', { key: 'l', className: 'gn-label' }, '操作'),
60
+ h('select', { key: 's', className: 'gn-select', value: op, onChange: (e) => 设op(e.target.value) }, OPS.map((o) => h('option', { key: o.key, value: o.key }, o.label))),
61
+ ]) : null,
62
+ ...FIELDS.map((f) => h('div', { key: f.k, className: 'gn-row' }, [
63
+ h('span', { key: 'l', className: 'gn-label' }, f.label),
64
+ h('input', { key: 'i', className: 'gn-input', placeholder: f.ph || '', value: values[f.k], onChange: (e) => 设values({ ...values, [f.k]: e.target.value }) }),
65
+ ])),
66
+ h('div', { key: 'a', className: 'gn-row', style: { justifyContent: 'flex-end' } }, [
67
+ h('button', { key: 'b', className: 'gn-btn', disabled: 跑, onClick: 执行 }, 跑 ? '处理中…' : '开始处理'),
68
+ ]),
69
+ ]),
70
+ 结果 ? h('div', { key: 'r', className: 'gn-card' }, [
71
+ h('div', { key: 's', className: 结果.ok === false ? 'gn-err' : 'gn-ok' }, 结果.ok === false ? ('失败:' + (结果.text || '未知错误')) : '完成,结果见下方'),
72
+ 结果.ok === false ? null : h('div', { key: 'o', className: 'gn-out' }, 结果.text || ''),
73
+ ]) : null,
74
+ h('div', { key: 'tip', className: 'gn-tip' }, '需要 python3 + python-pptx(缺库会给安装提示)。只读 pptx,不修改原文件。'),
75
+ h('a', { key: 'bg', className: 'gn-badge', href: 'https://deepwhale.org.cn', target: '_blank', rel: 'noopener noreferrer', title: '深鲸桌面 · deepwhale.org.cn' },
76
+ h('img', { src: BADGE, width: 121, height: 20, alt: 'powered by dwh' })),
77
+ ]);
78
+ }
79
+ const inject = ['slots'];
80
+ function apply(ctx) {
81
+ const 试 = (n, f) => { try { f(); } catch (e) { try { console.warn('[pptx-batch] 插槽注册失败:' + n, e && e.message); } catch {} } };
82
+ 试('侧栏图标', () => ctx.slots.inject(SLOT_ICON, function* () { yield ctx.slots.register({ name: SLOT_ICON, id: PANEL_ID, order: 41, label: '深鲸 PPT 批量' }, Ico); }));
83
+ 试('主面板', () => ctx.slots.inject(SLOT_MAIN, function* () { yield ctx.slots.register({ name: SLOT_MAIN, key: PANEL_ID }, 面板); }));
84
+ }
85
+ exports.inject = inject; exports.apply = apply; return module.exports;
86
+ },
87
+ });
package/lib/core.mjs ADDED
@@ -0,0 +1,52 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ const PY = String.raw`
3
+ import sys, json, os
4
+ op = sys.argv[1] if len(sys.argv) > 1 else 'extract-text'
5
+ inp = sys.argv[2] if len(sys.argv) > 2 else ''
6
+ out = sys.argv[3] if len(sys.argv) > 3 else ''
7
+ try:
8
+ from pptx import Presentation
9
+ except Exception:
10
+ print(json.dumps({"ok": False, "text": "需要 python3 + python-pptx:pip3 install python-pptx"}, ensure_ascii=False)); sys.exit(0)
11
+
12
+ def fail(msg):
13
+ print(json.dumps({"ok": False, "text": msg}, ensure_ascii=False)); sys.exit(0)
14
+ files = []
15
+ if os.path.isdir(inp):
16
+ for n in sorted(os.listdir(inp)):
17
+ if n.lower().endswith('.pptx') and not n.startswith('~$'): files.append(os.path.join(inp, n))
18
+ elif inp.lower().endswith('.pptx'):
19
+ files = [inp]
20
+ if not files:
21
+ print(json.dumps({"ok": False, "text": "没有找到 .pptx 文件"}, ensure_ascii=False)); sys.exit(0)
22
+ lines = []
23
+ try:
24
+ if op == 'list':
25
+ for f in files:
26
+ lines.append(os.path.basename(f) + ': ' + str(len(Presentation(f).slides)) + ' 页')
27
+ elif op == 'extract-images':
28
+ if not out: out = os.path.join(os.path.dirname(files[0]), '_图片')
29
+ os.makedirs(out, exist_ok=True); n = 0
30
+ for f in files:
31
+ for si, s in enumerate(Presentation(f).slides, 1):
32
+ for sh in s.shapes:
33
+ if sh.shape_type == 13:
34
+ n += 1
35
+ open(os.path.join(out, str(si) + '-' + str(n) + '.png'), 'wb').write(sh.image.blob)
36
+ lines.append('导出 ' + str(n) + ' 张图片到 ' + out)
37
+ else:
38
+ for f in files:
39
+ prs = Presentation(f); lines.append('## ' + os.path.basename(f))
40
+ for i, s in enumerate(prs.slides, 1):
41
+ texts = [sh.text_frame.text.strip() for sh in s.shapes if sh.has_text_frame and sh.text_frame.text.strip()]
42
+ lines.append('### 第 ' + str(i) + ' 页')
43
+ lines.extend(texts if texts else ['(无文字)'])
44
+ except Exception as e:
45
+ fail('无法读取 PPT(可能不是有效的 .pptx):' + str(e)[:200])
46
+ print(json.dumps({"ok": True, "text": chr(10).join(lines)}, ensure_ascii=False))
47
+ `;
48
+ export function run({ op = 'extract-text', input = '', out = '' } = {}) {
49
+ if (!input) return { ok: false, text: '请填写 PPT 文件或目录' };
50
+ const r = spawnSync('python3', ['-c', PY, op, input, out], { encoding: 'utf8' });
51
+ try { return JSON.parse((r.stdout || '').trim() || '{}'); } catch { return { ok: false, text: String((r.stderr || r.stdout || '执行失败')).slice(-400) }; }
52
+ }
package/lib/index.js ADDED
@@ -0,0 +1,10 @@
1
+ import { createPlugin, pkgRootFrom } from '@deepwhale-cn/dsh-guard-core/plugin';
2
+
3
+ export const { name, inject, apply } = createPlugin({
4
+ name: 'pptx-batch',
5
+ pkgRoot: pkgRootFrom(import.meta.url),
6
+ skillFile: 'skill/pptx-batch.md',
7
+ binFile: 'bin/cli.mjs',
8
+ description: 'PPT 批量填模板 / 替换文字与图片 / 导出内容清单(python-pptx)',
9
+ whenToUse: '当用户要批量处理整目录的 PowerPoint(.pptx)时使用:按 {{key}} 批量填模板(客户名、金额、日期)、按形状名批量换图片(Logo)、提取每页文本到 txt、统计页数/形状数/图片数。典型说法:「这套模板有 30 份要按客户发」「把每份 PPT 里的旧 Logo 换掉」「把这批 pptx 的文字导出来核对」。不用于 .ppt 老格式、加密文档、需要保持复杂动画与版式的场景。',
10
+ });