@deepwhale-cn/dsh-doc-batch 0.0.0-stage → 0.1.4
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 +21 -0
- package/README.md +51 -2
- package/assets/deepwhale-badge.png +0 -0
- package/bin/cli-docx.mjs +245 -0
- package/bin/cli-pdf.mjs +581 -0
- package/bin/cli-xlsx.mjs +225 -0
- package/bin/cli.mjs +63 -0
- package/cordis.patch.yml +4 -0
- package/lib/api.js +162 -0
- package/lib/client.js +235 -0
- package/lib/docx_tool.py +651 -0
- package/lib/index.js +26 -0
- package/lib/pdf_ops.py +431 -0
- package/lib/xlsx_tool.py +554 -0
- package/package.json +65 -4
- package/skill/doc-batch.md +202 -0
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,52 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 深鲸文档批量处理(@deepwhale-cn/dsh-doc-batch)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
一个插件、一个面板,处理三类文档:**Word / Excel / PDF**。
|
|
4
|
+
|
|
5
|
+
## 面板
|
|
6
|
+
|
|
7
|
+
侧栏「深鲸文档批量」→ 选格式(Word / Excel / PDF)→ 选操作 → 填文件或目录 → 开始处理。
|
|
8
|
+
结果直接显示在面板里;所有处理都在本机完成,不上传文件。
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
dsh plugin --profile web add @deepwhale-cn/dsh-doc-batch
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
也可源码安装:`dsh plugin --profile web add github:feely0208/dsh-doc-batch`。
|
|
17
|
+
|
|
18
|
+
## 命令
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
dsh-doc-batch docx replace --input ./合同 --from 深鲸科技 --to 深海鲸
|
|
22
|
+
dsh-doc-batch docx extract --input ./合同 --out ./文本
|
|
23
|
+
dsh-doc-batch docx merge --input ./周报 --out ./合并
|
|
24
|
+
dsh-doc-batch docx headers --input ./合同
|
|
25
|
+
dsh-doc-batch docx to-md --input ./合同 --out ./md
|
|
26
|
+
|
|
27
|
+
dsh-doc-batch xlsx merge --input ./报表 --out ./合并
|
|
28
|
+
dsh-doc-batch xlsx clean --input ./报表 --out ./清洗
|
|
29
|
+
dsh-doc-batch xlsx summary --input ./报表
|
|
30
|
+
dsh-doc-batch xlsx csv --input ./报表 --out ./csv
|
|
31
|
+
|
|
32
|
+
dsh-doc-batch pdf merge --input ./扫描件 --out ./合并
|
|
33
|
+
dsh-doc-batch pdf split --input a.pdf --out ./拆分 --action page
|
|
34
|
+
dsh-doc-batch pdf compress --input a.pdf --out ./压缩
|
|
35
|
+
dsh-doc-batch pdf watermark --input a.pdf --out ./水印 --text 机密
|
|
36
|
+
dsh-doc-batch pdf extract-text --input ./扫描件 --out ./文本
|
|
37
|
+
dsh-doc-batch pdf extract-images --input a.pdf --out ./图片 --dpi 150
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
格式别名:`word` = `docx`,`excel` = `xlsx`。
|
|
41
|
+
|
|
42
|
+
## 依赖
|
|
43
|
+
|
|
44
|
+
`python3` + 对应库:`python-docx`(Word)、`openpyxl`(Excel)、`pypdf`(PDF)。
|
|
45
|
+
缺库时 CLI 会给安装提示,面板会把提示原样显示出来。
|
|
46
|
+
|
|
47
|
+
## 说明
|
|
48
|
+
|
|
49
|
+
- 本插件由原先的三个单格式插件(docx-batch / xlsx-batch / pdf-kit)合并而来,
|
|
50
|
+
**能力完全一致**,只是入口收成一个;
|
|
51
|
+
- 三种格式的参数解析与处理内核仍各自独立(`bin/cli-docx|xlsx|pdf.mjs` + `lib/*.py`),
|
|
52
|
+
统一入口只做分发,避免"合并后行为漂移"。
|
|
Binary file
|
package/bin/cli-docx.mjs
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Word 批量处理 CLI
|
|
4
|
+
*
|
|
5
|
+
* 形态:
|
|
6
|
+
* dsh-docx-batch --help (无参数也打印 help,退出 0)
|
|
7
|
+
* dsh-docx-batch --demo (自造样例跑完整流程,退出 0)
|
|
8
|
+
* dsh-docx-batch <action> --input <文件或目录> [--out <目录>] [--json]
|
|
9
|
+
*
|
|
10
|
+
* 真实逻辑全部委托 python3 + python-docx(lib/docx_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', 'docx_tool.py');
|
|
19
|
+
const TITLE = 'Word 批量处理';
|
|
20
|
+
const CMD = 'dsh-docx-batch';
|
|
21
|
+
const ACTIONS = {
|
|
22
|
+
replace: '批量替换文本(--from 旧 --to 新,可多组)',
|
|
23
|
+
extract: '提取正文文本到 .txt',
|
|
24
|
+
merge: '按文件名顺序合并成一个 docx',
|
|
25
|
+
headers: '列出页眉页脚文本',
|
|
26
|
+
'to-md': '段落/表格转 Markdown',
|
|
27
|
+
};
|
|
28
|
+
const PY_HINT = [
|
|
29
|
+
'需要 python3 + python-docx。安装:',
|
|
30
|
+
' python3 -m pip install python-docx',
|
|
31
|
+
'没有 python3 时:',
|
|
32
|
+
' macOS: brew install python3',
|
|
33
|
+
' Ubuntu: sudo apt install python3',
|
|
34
|
+
].join('\n');
|
|
35
|
+
const LINE = '─'.repeat(56);
|
|
36
|
+
|
|
37
|
+
function help() {
|
|
38
|
+
console.log(`${TITLE}
|
|
39
|
+
|
|
40
|
+
用法:
|
|
41
|
+
${CMD} <action> --input <文件或目录> [--out <目录>] [--json]
|
|
42
|
+
${CMD} --demo
|
|
43
|
+
${CMD} --help
|
|
44
|
+
|
|
45
|
+
子命令:
|
|
46
|
+
${Object.entries(ACTIONS).map(([k, v]) => ` ${k.padEnd(9)}${v}`).join('\n')}
|
|
47
|
+
|
|
48
|
+
参数:
|
|
49
|
+
--input, -i .docx 文件或目录(目录递归处理)
|
|
50
|
+
--out, -o 输出目录;默认 <输入目录>/_输出;同名文件不覆盖,自动加序号
|
|
51
|
+
--from 要替换的旧文本(可重复,与 --to 按顺序配对)
|
|
52
|
+
--to 替换后的新文本(可重复)
|
|
53
|
+
--json 输出 JSON(给脚本/CI 用)
|
|
54
|
+
--demo 自造样例文件,跑完整流程并打印产物路径
|
|
55
|
+
|
|
56
|
+
示例:
|
|
57
|
+
${CMD} replace --input ./合同 --from 深鲸科技 --to 深海鲸
|
|
58
|
+
${CMD} extract --input ./合同 --out ./文本
|
|
59
|
+
${CMD} merge --input ./周报 --out ./合并
|
|
60
|
+
${CMD} headers --input ./合同
|
|
61
|
+
${CMD} to-md --input ./合同 --out ./md
|
|
62
|
+
|
|
63
|
+
依赖:python3 + python-docx(缺失时会提示安装命令)`);
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fail(message, hint = '', code = 2, json = false) {
|
|
68
|
+
if (json) {
|
|
69
|
+
console.log(JSON.stringify({ ok: false, code, message, hint }, null, 2));
|
|
70
|
+
} else {
|
|
71
|
+
console.error(`❌ ${message}`);
|
|
72
|
+
if (hint) console.error(`\n${hint}`);
|
|
73
|
+
}
|
|
74
|
+
process.exit(code);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function findPython() {
|
|
78
|
+
const candidates = [
|
|
79
|
+
process.env.DSH_PYTHON,
|
|
80
|
+
process.env.DOCX_BATCH_PYTHON,
|
|
81
|
+
'python3',
|
|
82
|
+
'/opt/homebrew/bin/python3',
|
|
83
|
+
'/usr/local/bin/python3',
|
|
84
|
+
'/usr/bin/python3',
|
|
85
|
+
].filter(Boolean);
|
|
86
|
+
for (const cmd of candidates) {
|
|
87
|
+
const r = spawnSync(cmd, ['-c', 'import sys; print(sys.version_info[0])'], {
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
timeout: 10000,
|
|
90
|
+
});
|
|
91
|
+
if (!r.error && r.status === 0) return cmd;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseArgs(argv) {
|
|
97
|
+
const opt = { action: null, input: null, out: null, from: [], to: [], json: false, demo: false };
|
|
98
|
+
for (let i = 0; i < argv.length; i++) {
|
|
99
|
+
const raw = argv[i];
|
|
100
|
+
const eq = raw.indexOf('=');
|
|
101
|
+
const flag = eq > 0 ? raw.slice(0, eq) : raw;
|
|
102
|
+
const inline = eq > 0 ? raw.slice(eq + 1) : undefined;
|
|
103
|
+
if (flag === '--json') {
|
|
104
|
+
opt.json = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (flag === '--demo') {
|
|
108
|
+
opt.demo = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (flag === '--help' || flag === '-h') help();
|
|
112
|
+
if (flag === '--input' || flag === '-i' || flag === '--out' || flag === '-o' ||
|
|
113
|
+
flag === '--from' || flag === '--to') {
|
|
114
|
+
let value = inline;
|
|
115
|
+
if (value === undefined) {
|
|
116
|
+
value = argv[i + 1];
|
|
117
|
+
if (value === undefined || value.startsWith('--')) {
|
|
118
|
+
fail(`参数 ${flag} 缺少取值`, `例:${flag} <值>`);
|
|
119
|
+
}
|
|
120
|
+
i += 1;
|
|
121
|
+
}
|
|
122
|
+
if (flag === '--input' || flag === '-i') opt.input = value;
|
|
123
|
+
else if (flag === '--out' || flag === '-o') opt.out = value;
|
|
124
|
+
else if (flag === '--from') opt.from.push(value);
|
|
125
|
+
else opt.to.push(value);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (flag.startsWith('-')) {
|
|
129
|
+
fail(`未知参数:${raw}`, `用 ${CMD} --help 查看支持的参数。`);
|
|
130
|
+
}
|
|
131
|
+
if (!opt.action) {
|
|
132
|
+
opt.action = raw;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
fail(`多余的参数:${raw}`, '一次只能执行一个子命令。');
|
|
136
|
+
}
|
|
137
|
+
return opt;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function human(res) {
|
|
141
|
+
console.log('');
|
|
142
|
+
console.log(` ${TITLE} · ${res.action}`);
|
|
143
|
+
console.log(` ${LINE}`);
|
|
144
|
+
if (res.input) console.log(` 输入 ${res.input}`);
|
|
145
|
+
if (res.out) console.log(` 输出目录 ${res.out}`);
|
|
146
|
+
for (const f of res.files || []) {
|
|
147
|
+
let extra = '';
|
|
148
|
+
if (f.replacements !== undefined) extra = ` · 替换 ${f.replacements} 处`;
|
|
149
|
+
else if (f.chars !== undefined) extra = ` · ${f.chars} 字符`;
|
|
150
|
+
else if (f.sections !== undefined) extra = ` · ${f.sections} 节`;
|
|
151
|
+
else if (f.paragraphs !== undefined) extra = ` · ${f.paragraphs} 段`;
|
|
152
|
+
console.log(` ✓ ${f.input_name} → ${f.output_name}${extra}`);
|
|
153
|
+
}
|
|
154
|
+
if (res.summary) console.log(`\n ${res.summary}`);
|
|
155
|
+
if (res.artifacts && res.artifacts.length) {
|
|
156
|
+
console.log('\n 产物:');
|
|
157
|
+
for (const a of res.artifacts) console.log(` ${a}`);
|
|
158
|
+
}
|
|
159
|
+
console.log('');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function humanDemo(res) {
|
|
163
|
+
console.log('');
|
|
164
|
+
console.log(` ${TITLE} · 演示(--demo)`);
|
|
165
|
+
console.log(` ${LINE}`);
|
|
166
|
+
console.log(` 演示目录 ${res.demoRoot}`);
|
|
167
|
+
console.log('');
|
|
168
|
+
for (const s of res.steps || []) {
|
|
169
|
+
console.log(` ✓ ${String(s.action).padEnd(8)} ${s.summary}`);
|
|
170
|
+
}
|
|
171
|
+
console.log('\n 产物:');
|
|
172
|
+
for (const a of res.artifacts || []) console.log(` ${a}`);
|
|
173
|
+
console.log('');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function callPython(py, action, payload) {
|
|
177
|
+
const r = spawnSync(py, [PY_SCRIPT, action, JSON.stringify(payload)], {
|
|
178
|
+
encoding: 'utf8',
|
|
179
|
+
maxBuffer: 128 * 1024 * 1024,
|
|
180
|
+
});
|
|
181
|
+
if (r.error) {
|
|
182
|
+
fail(`调用 python 失败:${r.error.message}`, PY_HINT, 3);
|
|
183
|
+
}
|
|
184
|
+
const lines = (r.stdout || '').trim().split('\n').filter(Boolean);
|
|
185
|
+
let result = null;
|
|
186
|
+
try {
|
|
187
|
+
result = JSON.parse(lines[lines.length - 1] || '');
|
|
188
|
+
} catch {
|
|
189
|
+
/* 下面统一报错 */
|
|
190
|
+
}
|
|
191
|
+
if (!result) {
|
|
192
|
+
const detail = (r.stderr || '').trim().split('\n').slice(-5).join('\n');
|
|
193
|
+
fail('Python 处理失败(没有拿到有效结果)', detail || PY_HINT, r.status || 1);
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const argv = process.argv.slice(2);
|
|
199
|
+
if (argv.length === 0) help();
|
|
200
|
+
if (argv.includes('--help') || argv.includes('-h')) help();
|
|
201
|
+
|
|
202
|
+
const opt = parseArgs(argv);
|
|
203
|
+
const python = findPython();
|
|
204
|
+
if (!python) {
|
|
205
|
+
fail('未找到 python3,无法处理 Word 文件', PY_HINT, 3, opt.json);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (opt.demo) {
|
|
209
|
+
const res = callPython(python, 'demo', {});
|
|
210
|
+
if (!res.ok) fail(res.message, res.hint, res.code || 1, opt.json);
|
|
211
|
+
if (opt.json) console.log(JSON.stringify(res, null, 2));
|
|
212
|
+
else humanDemo(res);
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!opt.action) {
|
|
217
|
+
fail('缺少子命令', `可用子命令:${Object.keys(ACTIONS).join(' / ')}\n用 ${CMD} --help 查看详情。`, 2, opt.json);
|
|
218
|
+
}
|
|
219
|
+
if (!Object.hasOwn(ACTIONS, opt.action)) {
|
|
220
|
+
fail(`未知子命令:${opt.action}`, `可用子命令:${Object.keys(ACTIONS).join(' / ')}`, 2, opt.json);
|
|
221
|
+
}
|
|
222
|
+
if (!opt.input) {
|
|
223
|
+
fail('缺少 --input(文件或目录)', `例:${CMD} ${opt.action} --input ./文档目录`, 2, opt.json);
|
|
224
|
+
}
|
|
225
|
+
if (opt.action === 'replace') {
|
|
226
|
+
if (opt.from.length === 0) {
|
|
227
|
+
fail('缺少 --from(要替换的旧文本)', `例:${CMD} replace --input ./合同 --from 旧文本 --to 新文本`, 2, opt.json);
|
|
228
|
+
}
|
|
229
|
+
if (opt.from.length !== opt.to.length) {
|
|
230
|
+
fail(
|
|
231
|
+
`--from 与 --to 数量不一致(${opt.from.length} vs ${opt.to.length})`,
|
|
232
|
+
'每一组 --from 都要配一个 --to,按顺序一一对应。',
|
|
233
|
+
2,
|
|
234
|
+
opt.json,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const payload = { input: opt.input, out: opt.out, from: opt.from, to: opt.to };
|
|
240
|
+
const res = callPython(python, opt.action, payload);
|
|
241
|
+
if (!res.ok) fail(res.message, res.hint, res.code || 1, opt.json);
|
|
242
|
+
|
|
243
|
+
if (opt.json) console.log(JSON.stringify(res, null, 2));
|
|
244
|
+
else human(res);
|
|
245
|
+
process.exit(0);
|