@paircode/tool-office 1.0.3 → 1.0.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/index.js +335 -892
- package/package.json +1 -1
- package/bin/tool-office.exe +0 -0
package/index.js
CHANGED
|
@@ -1,892 +1,335 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════
|
|
2
|
-
// tool-office — 办公文档(csv_read/csv_write/json_to_table/table_stats/text_report/word_read)
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
"
|
|
161
|
-
"
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
"
|
|
214
|
-
"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
"
|
|
240
|
-
"
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
"
|
|
267
|
-
"
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
"
|
|
293
|
-
"
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
} else if (c === '\n') {
|
|
337
|
-
row.push(field); rows.push(row); row = []; field = ''; fieldStart = true
|
|
338
|
-
} else if (c === '\r') {
|
|
339
|
-
// 忽略(\r\n 随 \n 处理;单独 \r 作普通字符保留)
|
|
340
|
-
} else {
|
|
341
|
-
field += c; fieldStart = false
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
if (field !== '' || row.length > 0) { row.push(field); rows.push(row) }
|
|
345
|
-
return rows
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// 生成 CSV(转义:含引号/逗号/换行的字段加引号包裹)。
|
|
349
|
-
function writeCSV(records, delim) {
|
|
350
|
-
return records.map(row =>
|
|
351
|
-
row.map(cell => {
|
|
352
|
-
const s = String(cell == null ? '' : cell)
|
|
353
|
-
return /["\n\r]/.test(s) || s.includes(delim) ? '"' + s.replace(/"/g, '""') + '"' : s
|
|
354
|
-
}).join(delim)
|
|
355
|
-
).join('\n') + '\n'
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function csvDelim(d) {
|
|
359
|
-
const s = String(d || '').trim().toLowerCase()
|
|
360
|
-
return (s === 'tab' || s === '\t' || s === '制表符') ? '\t' : ','
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function parseColIndex(spec, maxCols) {
|
|
364
|
-
const s = String(spec || '').trim()
|
|
365
|
-
if (!s) return null
|
|
366
|
-
const idx = []
|
|
367
|
-
for (const part of s.split(',')) {
|
|
368
|
-
const p = part.trim()
|
|
369
|
-
const i = Number(p)
|
|
370
|
-
if (Number.isInteger(i) && i >= 0 && i < maxCols) idx.push(i)
|
|
371
|
-
}
|
|
372
|
-
return idx
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function padRight(s, n) {
|
|
376
|
-
s = String(s || '')
|
|
377
|
-
return s.length >= n ? s : s + ' '.repeat(n - s.length)
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// writeMarkdownTable 渲染二维数组为 Markdown 表格(colIdx 为 null 时全列)。
|
|
381
|
-
function writeMarkdownTable(records, colIdx) {
|
|
382
|
-
if (!records || records.length === 0) return ''
|
|
383
|
-
const selectCols = (row) => {
|
|
384
|
-
if (colIdx == null) return row
|
|
385
|
-
return colIdx.map(idx => (idx < row.length ? row[idx] : ''))
|
|
386
|
-
}
|
|
387
|
-
const header = selectCols(records[0])
|
|
388
|
-
const widths = header.map(h => String(h || '').length)
|
|
389
|
-
for (const row of records.slice(1)) {
|
|
390
|
-
const cols = selectCols(row)
|
|
391
|
-
cols.forEach((c, i) => {
|
|
392
|
-
if (i < widths.length && String(c || '').length > widths[i]) widths[i] = String(c || '').length
|
|
393
|
-
})
|
|
394
|
-
while (widths.length < cols.length) widths.push(0)
|
|
395
|
-
}
|
|
396
|
-
while (header.length < widths.length) header.push('')
|
|
397
|
-
let out = '| '
|
|
398
|
-
header.forEach((h, i) => { out += padRight(h, widths[i]) + ' | ' })
|
|
399
|
-
out += '\n| '
|
|
400
|
-
widths.forEach(w => { out += '-'.repeat(Math.max(w, 3)) + ' | ' })
|
|
401
|
-
out += '\n'
|
|
402
|
-
for (const row of records.slice(1)) {
|
|
403
|
-
const cols = selectCols(row)
|
|
404
|
-
out += '| '
|
|
405
|
-
cols.forEach((c, i) => { out += (i < widths.length ? padRight(c, widths[i]) : String(c || '')) + ' | ' })
|
|
406
|
-
out += '\n'
|
|
407
|
-
}
|
|
408
|
-
return out
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// parseMarkdownTable 从 Markdown 文本提取表格数据(简单实现,同 Go 版)。
|
|
412
|
-
function parseMarkdownTable(text) {
|
|
413
|
-
const records = []
|
|
414
|
-
let inTable = false
|
|
415
|
-
for (const ln of String(text).split('\n')) {
|
|
416
|
-
const line = ln.trim()
|
|
417
|
-
if (!line || !line.startsWith('|')) {
|
|
418
|
-
if (inTable) break
|
|
419
|
-
continue
|
|
420
|
-
}
|
|
421
|
-
const stripped = line.replace(/-/g, '').replace(/\|/g, '').replace(/ /g, '')
|
|
422
|
-
if (stripped === '') continue
|
|
423
|
-
inTable = true
|
|
424
|
-
let row = line.split('|').map(p => p.trim())
|
|
425
|
-
if (row.length > 0 && row[0] === '') row = row.slice(1)
|
|
426
|
-
if (row.length > 0 && row[row.length - 1] === '') row = row.slice(0, -1)
|
|
427
|
-
if (row.length > 0) records.push(row)
|
|
428
|
-
}
|
|
429
|
-
return records
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// csv_read:读 CSV/TSV → Markdown 表格
|
|
433
|
-
function csvRead(ctx, args) {
|
|
434
|
-
const path = String(args.path || '').trim()
|
|
435
|
-
if (!path) throw new Error('path 不能为空')
|
|
436
|
-
const data = ctx.fs.readFile(path)
|
|
437
|
-
if (data.length > 100 << 20) throw new Error('文件超过 100MB,请缩小范围后用 search_content 搜索特定内容')
|
|
438
|
-
const delim = csvDelim(args.delimiter)
|
|
439
|
-
let limit = Number(args.limit || 100)
|
|
440
|
-
let offset = Number(args.offset || 0)
|
|
441
|
-
const colSpec = String(args.columns || '').trim()
|
|
442
|
-
const records = readCSV(data, delim)
|
|
443
|
-
if (records.length === 0) return '(空文件)'
|
|
444
|
-
const colIdx = parseColIndex(colSpec, records[0].length)
|
|
445
|
-
if (colSpec && (!colIdx || colIdx.length === 0)) throw new Error('无效的 columns 参数: "' + colSpec + '",应为逗号分隔的列索引(从 0 开始)')
|
|
446
|
-
const total = records.length
|
|
447
|
-
if (offset > 0 && offset < records.length) records.splice(0, offset)
|
|
448
|
-
else if (offset >= records.length) return '(offset 超出文件行数)'
|
|
449
|
-
if (limit > 0 && limit < records.length) records.length = limit
|
|
450
|
-
let out = '**CSV 文件**: `' + path + '` · 共 ' + total + ' 行 × ' + records[0].length + ' 列'
|
|
451
|
-
if (limit > 0 && total > limit) out += ' · 显示前 ' + limit + ' 行'
|
|
452
|
-
if (offset > 0) out += ' · 跳过 ' + offset + ' 行'
|
|
453
|
-
if (colSpec) out += ' · 显示列 [' + colSpec + ']'
|
|
454
|
-
out += '\n\n' + writeMarkdownTable(records, colIdx)
|
|
455
|
-
return out
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// csv_write:表格数据 → CSV/TSV 文件
|
|
459
|
-
function csvWrite(ctx, args) {
|
|
460
|
-
const path = String(args.path || '').trim()
|
|
461
|
-
if (!path) throw new Error('path 不能为空')
|
|
462
|
-
const dataStr = String(args.data || '')
|
|
463
|
-
const delim = csvDelim(args.delimiter)
|
|
464
|
-
let records = null
|
|
465
|
-
try { records = JSON.parse(dataStr) } catch { records = parseMarkdownTable(dataStr) }
|
|
466
|
-
if (!Array.isArray(records) || records.length === 0 || !Array.isArray(records[0])) throw new Error('data 格式无效:无法解析为 JSON 二维数组或 Markdown 表格')
|
|
467
|
-
const headerJSON = String(args.header || '').trim()
|
|
468
|
-
if (headerJSON) {
|
|
469
|
-
let header
|
|
470
|
-
try { header = JSON.parse(headerJSON) } catch { throw new Error('header JSON 解析失败') }
|
|
471
|
-
if (!Array.isArray(header)) throw new Error('header 必须是 JSON 数组')
|
|
472
|
-
records = [header].concat(records)
|
|
473
|
-
}
|
|
474
|
-
const csvText = writeCSV(records, delim)
|
|
475
|
-
ctx.fs.writeFile(path, csvText)
|
|
476
|
-
return '已写入 CSV 文件 `' + path + '`(' + records.length + ' 行 × ' + records[0].length + ' 列,' + csvText.length + ' 字节)'
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// json_to_table:JSON 数组 → Markdown 表格
|
|
480
|
-
function jsonToTable(ctx, args) {
|
|
481
|
-
const jsonStr = String(args.json || '')
|
|
482
|
-
let records
|
|
483
|
-
try { records = JSON.parse(jsonStr) } catch { throw new Error('JSON 解析失败') }
|
|
484
|
-
if (!Array.isArray(records)) throw new Error('JSON 必须是数组')
|
|
485
|
-
if (records.length === 0) return '(空数组)'
|
|
486
|
-
const colSpec = String(args.columns || '').trim()
|
|
487
|
-
let limit = Number(args.limit || 100)
|
|
488
|
-
const title = String(args.title || '').trim()
|
|
489
|
-
let cols
|
|
490
|
-
if (colSpec) cols = colSpec.split(',').map(s => s.trim())
|
|
491
|
-
else cols = Object.keys(records[0] || {}).sort()
|
|
492
|
-
const total = records.length
|
|
493
|
-
if (limit > 0 && limit < records.length) records = records.slice(0, limit)
|
|
494
|
-
const rows = [cols]
|
|
495
|
-
for (const rec of records) {
|
|
496
|
-
rows.push(cols.map(col => (rec[col] !== undefined && rec[col] !== null) ? String(rec[col]) : ''))
|
|
497
|
-
}
|
|
498
|
-
let out = (title ? '**' + title + '**\n\n' : '') + '共 ' + total + ' 条记录'
|
|
499
|
-
if (limit > 0 && total > limit) out += ',显示前 ' + limit + ' 条'
|
|
500
|
-
out += '\n\n' + writeMarkdownTable(rows, null)
|
|
501
|
-
return out
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// table_stats:表格数值列统计(求和/均值/最大/最小/计数,group_by 分组)
|
|
505
|
-
function toRowsFromData(ctx, args, format, dataStr) {
|
|
506
|
-
if (format === 'file') {
|
|
507
|
-
const p = String(args.data || '').trim()
|
|
508
|
-
if (!p) throw new Error('data(文件路径)不能为空')
|
|
509
|
-
return readCSV(ctx.fs.readFile(p), ',')
|
|
510
|
-
}
|
|
511
|
-
if (format === 'json') {
|
|
512
|
-
const objs = JSON.parse(dataStr)
|
|
513
|
-
if (!Array.isArray(objs)) throw new Error('JSON 必须是数组')
|
|
514
|
-
if (objs.length === 0) return []
|
|
515
|
-
const cols = Object.keys(objs[0]).sort()
|
|
516
|
-
const rows = [cols]
|
|
517
|
-
for (const obj of objs) rows.push(cols.map(c => (obj[c] != null ? String(obj[c]) : '')))
|
|
518
|
-
return rows
|
|
519
|
-
}
|
|
520
|
-
return readCSV(dataStr, ',')
|
|
521
|
-
}
|
|
522
|
-
function writeStatsTable(stats) {
|
|
523
|
-
const rows = [['列名', '计数', '求和', '均值', '最小值', '最大值']]
|
|
524
|
-
for (const s of stats) {
|
|
525
|
-
const avg = s.count > 0 ? s.sum / s.count : 0
|
|
526
|
-
rows.push([s.name, String(s.count), s.sum.toFixed(2), avg.toFixed(2), s.min.toFixed(2), s.max.toFixed(2)])
|
|
527
|
-
}
|
|
528
|
-
return writeMarkdownTable(rows, null)
|
|
529
|
-
}
|
|
530
|
-
function tableStats(ctx, args) {
|
|
531
|
-
const dataStr = String(args.data || '')
|
|
532
|
-
const format = String(args.format || '').trim().toLowerCase() || 'csv'
|
|
533
|
-
const groupBy = String(args.group_by || '').trim()
|
|
534
|
-
const records = toRowsFromData(ctx, args, format, dataStr)
|
|
535
|
-
if (records.length < 2) return '(数据不足,至少需要表头 + 1 行数据)'
|
|
536
|
-
const header = records[0]
|
|
537
|
-
const dataRows = records.slice(1)
|
|
538
|
-
const colIdx = {}
|
|
539
|
-
header.forEach((h, i) => { colIdx[h] = i })
|
|
540
|
-
let groupColIdx = -1
|
|
541
|
-
if (groupBy && colIdx[groupBy] !== undefined) groupColIdx = colIdx[groupBy]
|
|
542
|
-
// 自动识别数值列
|
|
543
|
-
const numCols = []
|
|
544
|
-
if (dataRows.length > 0) {
|
|
545
|
-
header.forEach((h, ci) => {
|
|
546
|
-
if (ci === groupColIdx) return
|
|
547
|
-
let allNumeric = true
|
|
548
|
-
for (const row of dataRows) {
|
|
549
|
-
if (ci >= row.length) { allNumeric = false; break }
|
|
550
|
-
const v = String(row[ci]).trim()
|
|
551
|
-
if (v === '' || v === '-' || v === 'N/A') continue
|
|
552
|
-
if (isNaN(Number(v))) { allNumeric = false; break }
|
|
553
|
-
}
|
|
554
|
-
if (allNumeric) numCols.push({ name: h, sum: 0, count: 0, min: 0, max: 0 })
|
|
555
|
-
})
|
|
556
|
-
}
|
|
557
|
-
if (numCols.length === 0) return '(未找到数值列,无法统计)'
|
|
558
|
-
// 累计器
|
|
559
|
-
const accum = (st, row) => {
|
|
560
|
-
const ci = colIdx[st.name]
|
|
561
|
-
if (ci >= row.length) return
|
|
562
|
-
const v = String(row[ci]).trim()
|
|
563
|
-
if (v === '' || v === '-' || v === 'N/A' || isNaN(Number(v))) return
|
|
564
|
-
const f = Number(v)
|
|
565
|
-
st.sum += f
|
|
566
|
-
st.count++
|
|
567
|
-
if (st.count === 1) { st.min = f; st.max = f }
|
|
568
|
-
else { if (f < st.min) st.min = f; if (f > st.max) st.max = f }
|
|
569
|
-
}
|
|
570
|
-
if (groupColIdx >= 0) {
|
|
571
|
-
const groups = {}
|
|
572
|
-
dataRows.forEach(row => { const k = groupColIdx < row.length ? row[groupColIdx] : ''; (groups[k] || (groups[k] = [])).push(row) })
|
|
573
|
-
let out = '**分组统计**(按 `' + groupBy + '` 分组)\n\n'
|
|
574
|
-
Object.keys(groups).sort().forEach(gk => {
|
|
575
|
-
const stats = numCols.map(nc => {
|
|
576
|
-
const st = { name: nc.name, sum: 0, count: 0, min: 0, max: 0 }
|
|
577
|
-
groups[gk].forEach(row => accum(st, row))
|
|
578
|
-
return st
|
|
579
|
-
})
|
|
580
|
-
out += '**' + gk + '**(' + groups[gk].length + ' 行)\n\n' + writeStatsTable(stats) + '\n'
|
|
581
|
-
})
|
|
582
|
-
return out
|
|
583
|
-
}
|
|
584
|
-
let out = '**统计结果** · ' + dataRows.length + ' 行数据\n\n'
|
|
585
|
-
numCols.forEach(nc => { dataRows.forEach(row => accum(nc, row)) })
|
|
586
|
-
out += writeStatsTable(numCols)
|
|
587
|
-
return out
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// text_report:目录树代码行数统计
|
|
591
|
-
const SKIP_DIRS2 = { '.git': 1, 'node_modules': 1, 'vendor': 1, '.pair': 1, 'dist': 1, 'build': 1, 'target': 1, '.next': 1, '__pycache__': 1, 'coverage': 1, '.idea': 1, '.vscode': 1, '.cache': 1, '.venv': 1, 'venv': 1, 'Pods': 1 }
|
|
592
|
-
function countFileLines(lines) {
|
|
593
|
-
let total = 0, code = 0, comment = 0, blank = 0
|
|
594
|
-
for (const line of lines) {
|
|
595
|
-
const t = line.trim()
|
|
596
|
-
total++
|
|
597
|
-
if (!t) blank++
|
|
598
|
-
else if (t.startsWith('//') || t.startsWith('#') || t.startsWith('--') || t.startsWith('/*') || t.startsWith('*') || t.startsWith('<!--')) comment++
|
|
599
|
-
else code++
|
|
600
|
-
}
|
|
601
|
-
return { total, code, comment, blank }
|
|
602
|
-
}
|
|
603
|
-
function textReport(ctx, args) {
|
|
604
|
-
const scanPath = String(args.path || '').trim() || '.'
|
|
605
|
-
const extStr = String(args.extensions || '').trim()
|
|
606
|
-
let groupBy = String(args.group_by || '').trim().toLowerCase() || 'ext'
|
|
607
|
-
const maxFiles = Number(args.max_files || 5000)
|
|
608
|
-
const extSet = {}
|
|
609
|
-
if (extStr) {
|
|
610
|
-
for (let e of extStr.split(',')) {
|
|
611
|
-
e = e.trim()
|
|
612
|
-
if (!e.startsWith('.')) e = '.' + e
|
|
613
|
-
extSet[e.toLowerCase()] = true
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
const extStats = {}, dirStats = {}
|
|
617
|
-
const total = { total: 0, code: 0, comment: 0, blank: 0 }
|
|
618
|
-
let fileCount = 0
|
|
619
|
-
const processFile = (p, rel) => {
|
|
620
|
-
if (fileCount >= maxFiles) return
|
|
621
|
-
let st
|
|
622
|
-
try { st = ctx.fs.stat(p) } catch { return }
|
|
623
|
-
if (st.isDir) return
|
|
624
|
-
const ext = p.toLowerCase().match(/(\.[a-z0-9]+)$/)?.[1] || '(无扩展名)'
|
|
625
|
-
if (Object.keys(extSet).length > 0 && !extSet[ext.toLowerCase()]) return
|
|
626
|
-
let data
|
|
627
|
-
try { data = ctx.fs.readFile(p) } catch { return }
|
|
628
|
-
fileCount++
|
|
629
|
-
const fs2 = countFileLines(String(data).split('\n'))
|
|
630
|
-
const dirKey = rel.includes('/') ? rel.slice(0, rel.lastIndexOf('/')) : '(根目录)'
|
|
631
|
-
if (!extStats[ext]) extStats[ext] = { total: 0, code: 0, comment: 0, blank: 0 }
|
|
632
|
-
const es = extStats[ext]
|
|
633
|
-
es.total += fs2.total; es.code += fs2.code; es.comment += fs2.comment; es.blank += fs2.blank
|
|
634
|
-
if (!dirStats[dirKey]) dirStats[dirKey] = { total: 0, code: 0, comment: 0, blank: 0 }
|
|
635
|
-
const ds = dirStats[dirKey]
|
|
636
|
-
ds.total += fs2.total; ds.code += fs2.code; ds.comment += fs2.comment; ds.blank += fs2.blank
|
|
637
|
-
total.total += fs2.total; total.code += fs2.code; total.comment += fs2.comment; total.blank += fs2.blank
|
|
638
|
-
}
|
|
639
|
-
const walk = (dir, relPrefix) => {
|
|
640
|
-
if (fileCount >= maxFiles) return
|
|
641
|
-
let names = []
|
|
642
|
-
try { names = ctx.fs.readdir(dir) } catch { return }
|
|
643
|
-
for (const n of names.sort()) {
|
|
644
|
-
if (fileCount >= maxFiles) return
|
|
645
|
-
if (SKIP_DIRS2[n]) continue
|
|
646
|
-
const p = (dir === '.' ? '' : dir + '/') + n
|
|
647
|
-
const rel = relPrefix ? relPrefix + '/' + n : n
|
|
648
|
-
let st
|
|
649
|
-
try { st = ctx.fs.stat(p) } catch { continue }
|
|
650
|
-
if (st.isDir) walk(p, rel)
|
|
651
|
-
else processFile(p, rel)
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
let scanSt
|
|
655
|
-
try { scanSt = ctx.fs.stat(scanPath) } catch { }
|
|
656
|
-
if (scanSt && !scanSt.isDir) processFile(scanPath, scanPath)
|
|
657
|
-
else walk(scanPath, '')
|
|
658
|
-
if (fileCount === 0) return '(未找到匹配的文件)'
|
|
659
|
-
let out = '**代码统计报告** · 扫描目录: `' + scanPath + '`\n\n共计 ' + fileCount + ' 个文件,' + total.total + ' 行(代码 ' + total.code + ' / 注释 ' + total.comment + ' / 空行 ' + total.blank + ')\n\n'
|
|
660
|
-
const rows = []
|
|
661
|
-
if (groupBy === 'dir') {
|
|
662
|
-
rows.push(['目录', '文件数(估算)', '总行数', '代码行', '注释行', '空行'])
|
|
663
|
-
Object.keys(dirStats).sort().forEach(d => {
|
|
664
|
-
const s = dirStats[d]
|
|
665
|
-
const estFiles = Math.ceil(s.total / 50)
|
|
666
|
-
rows.push([d, '~' + estFiles, String(s.total), String(s.code), String(s.comment), String(s.blank)])
|
|
667
|
-
})
|
|
668
|
-
} else {
|
|
669
|
-
rows.push(['扩展名', '文件数(估算)', '总行数', '代码行', '注释行', '空行'])
|
|
670
|
-
Object.keys(extStats).sort().forEach(e => {
|
|
671
|
-
const s = extStats[e]
|
|
672
|
-
const estFiles = Math.ceil(s.total / 50)
|
|
673
|
-
rows.push([e, '~' + estFiles, String(s.total), String(s.code), String(s.comment), String(s.blank)])
|
|
674
|
-
})
|
|
675
|
-
}
|
|
676
|
-
out += writeMarkdownTable(rows, null)
|
|
677
|
-
return out
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const impls = {
|
|
681
|
-
csv_read: csvRead,
|
|
682
|
-
csv_write: csvWrite,
|
|
683
|
-
json_to_table: jsonToTable,
|
|
684
|
-
table_stats: tableStats,
|
|
685
|
-
text_report: textReport,
|
|
686
|
-
word_read: wordRead,
|
|
687
|
-
read_xlsx: readXlsx,
|
|
688
|
-
// word_write / write_xlsx / read_pdf / markdown_to_html 保留独立二进制(生成/PDF 引擎)
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// ── read_xlsx 辅助 ─────────────────────────────────────────
|
|
692
|
-
// Excel 列字母 → 索引(A→1, Z→26, AA→27)
|
|
693
|
-
function parseColLetter(letters) {
|
|
694
|
-
if (!letters) return 0
|
|
695
|
-
let col = 0
|
|
696
|
-
for (const ch of String(letters).toUpperCase()) col = col * 26 + (ch.charCodeAt(0) - 64)
|
|
697
|
-
return col
|
|
698
|
-
}
|
|
699
|
-
// 剥离 XML 命名空间声明(对齐 Go 版 clean)
|
|
700
|
-
function cfgStripXmlns(s) { return String(s || '').replace(/xmlns="[^"]*"/g, '') }
|
|
701
|
-
|
|
702
|
-
// ── word_read:.docx 纯文本/Markdown 提取(zipReadEntry → JS XML 解析)─────
|
|
703
|
-
function wordRead(ctx, args) {
|
|
704
|
-
const path = String(args.path || '')
|
|
705
|
-
const format = (String(args.format || 'text') || 'text').toLowerCase() === 'markdown' ? 'markdown' : 'text'
|
|
706
|
-
const limit = parseInt(args.limit, 10)
|
|
707
|
-
const lim = isNaN(limit) ? 10000 : limit
|
|
708
|
-
let xml
|
|
709
|
-
try { xml = ctx.fs.zipReadEntry(path, 'word/document.xml') } catch (e) { throw new Error('无法打开 .docx 文件(不是有效的 ZIP 压缩包): ' + (e && e.message || e)) }
|
|
710
|
-
if (!xml || !xml.includes('document')) xml = String(xml || '')
|
|
711
|
-
// 先剥离表格块(表格单元格内的段落不算文档段落)
|
|
712
|
-
const tbls = []
|
|
713
|
-
let body = String(xml).replace(/<w:tbl>[\s\S]*?<\/w:tbl>/g, (m) => { tbls.push(m); return '\n@@TBL@@\n' })
|
|
714
|
-
// 提取段落
|
|
715
|
-
const paras = []
|
|
716
|
-
const reP = /<w:p\b[^>]*>([\s\S]*?)<\/w:p>/g
|
|
717
|
-
let m
|
|
718
|
-
while ((m = reP.exec(body)) !== null) {
|
|
719
|
-
const inner = m[1]
|
|
720
|
-
const styleM = inner.match(/<w:pStyle[^>]*w:val="([^"]+)"[^>]*\/?>/)
|
|
721
|
-
const style = styleM ? styleM[1] : ''
|
|
722
|
-
let text = ''
|
|
723
|
-
const reT = /<w:t[^>]*>([^<]*)<\/w:t>/g
|
|
724
|
-
let tm
|
|
725
|
-
while ((tm = reT.exec(inner)) !== null) text += tm[1]
|
|
726
|
-
paras.push({ text: text, style: style })
|
|
727
|
-
}
|
|
728
|
-
// 输出
|
|
729
|
-
let out = ''
|
|
730
|
-
for (const p of paras) {
|
|
731
|
-
if (!p.text) { out += format === 'markdown' ? '\n\n' : '\n'; continue }
|
|
732
|
-
if (format === 'markdown') {
|
|
733
|
-
if (/^(Heading1|1)/.test(p.style)) out += '# ' + p.text + '\n\n'
|
|
734
|
-
else if (/^(Heading2|2)/.test(p.style)) out += '## ' + p.text + '\n\n'
|
|
735
|
-
else if (/^(Heading3|3)/.test(p.style)) out += '### ' + p.text + '\n\n'
|
|
736
|
-
else if (/ListBullet/.test(p.style)) out += '- ' + p.text + '\n'
|
|
737
|
-
else if (/ListNumber/.test(p.style)) out += '1. ' + p.text + '\n'
|
|
738
|
-
else out += p.text + '\n\n'
|
|
739
|
-
} else {
|
|
740
|
-
out += p.text + '\n'
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
// 表格
|
|
744
|
-
for (const tb of tbls) {
|
|
745
|
-
if (format === 'markdown') {
|
|
746
|
-
const rows = []
|
|
747
|
-
const reTr = /<w:tr[^>]*>([\s\S]*?)<\/w:tr>/g
|
|
748
|
-
let tm, tr
|
|
749
|
-
while ((tm = reTr.exec(tb)) !== null) {
|
|
750
|
-
const cells = []
|
|
751
|
-
const reTc = /<w:tc[^>]*>([\s\S]*?)<\/w:tc>/g
|
|
752
|
-
let cm, tc
|
|
753
|
-
while ((cm = reTc.exec(tm[1])) !== null) {
|
|
754
|
-
let ct = ''
|
|
755
|
-
const reCt = /<w:t[^>]*>([^<]*)<\/w:t>/g
|
|
756
|
-
let xt
|
|
757
|
-
while ((xt = reCt.exec(cm[1])) !== null) ct += xt[1]
|
|
758
|
-
cells.push(ct.trim())
|
|
759
|
-
}
|
|
760
|
-
if (cells.length) rows.push(cells)
|
|
761
|
-
}
|
|
762
|
-
if (rows.length) { out += '\n' + writeMarkdownTable(rows, null) + '\n' }
|
|
763
|
-
} else {
|
|
764
|
-
out += '\n[表格]\n'
|
|
765
|
-
const reTr = /<w:tr[^>]*>([\s\S]*?)<\/w:tr>/g
|
|
766
|
-
let tm, tr
|
|
767
|
-
while ((tm = reTr.exec(tb)) !== null) {
|
|
768
|
-
const cells = []
|
|
769
|
-
const reTc = /<w:tc[^>]*>([\s\S]*?)<\/w:tc>/g
|
|
770
|
-
let cm, tc
|
|
771
|
-
while ((cm = reTc.exec(tm[1])) !== null) {
|
|
772
|
-
let ct = ''
|
|
773
|
-
const reCt = /<w:t[^>]*>([^<]*)<\/w:t>/g
|
|
774
|
-
let xt
|
|
775
|
-
while ((xt = reCt.exec(cm[1])) !== null) ct += xt[1]
|
|
776
|
-
cells.push(ct.trim())
|
|
777
|
-
}
|
|
778
|
-
if (cells.length) out += '| ' + cells.join(' | ') + ' |\n'
|
|
779
|
-
}
|
|
780
|
-
out += '\n'
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
let result = out.trim()
|
|
784
|
-
if (lim > 0 && result.length > lim) result = result.slice(0, lim) + '…'
|
|
785
|
-
return result
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// ── read_xlsx:.xlsx → Markdown 表格(zip 条目 → JS XML 解析)─────────────
|
|
789
|
-
function readXlsx(ctx, args) {
|
|
790
|
-
const path = String(args.path || '')
|
|
791
|
-
const sheetName = String(args.sheet || '').trim()
|
|
792
|
-
const limit = parseInt(args.limit, 10)
|
|
793
|
-
const lim = isNaN(limit) ? 200 : limit
|
|
794
|
-
// 共享字符串表
|
|
795
|
-
let sharedStrings = []
|
|
796
|
-
try {
|
|
797
|
-
const ssx = ctx.fs.zipReadEntry(path, 'xl/sharedStrings.xml')
|
|
798
|
-
const siRe = /<si>([\s\S]*?)<\/si>/g
|
|
799
|
-
let m
|
|
800
|
-
while ((m = siRe.exec(ssx)) !== null) {
|
|
801
|
-
let t = ''
|
|
802
|
-
const tRe = /<t[^>]*>([^<]*)<\/t>/g
|
|
803
|
-
let tm
|
|
804
|
-
while ((tm = tRe.exec(m[1])) !== null) t += tm[1]
|
|
805
|
-
sharedStrings.push(t)
|
|
806
|
-
}
|
|
807
|
-
} catch (e) { /* 无共享字符串表 */ }
|
|
808
|
-
// 找第一个 sheet(★ 与 Go 版一致:跳过 workbook 解析,取 sheet*.xml 首个)
|
|
809
|
-
const entries = ctx.fs.zipEntries(path)
|
|
810
|
-
const sheets = entries.filter(n => n.indexOf('xl/worksheets/sheet') === 0 && n.endsWith('.xml')).sort()
|
|
811
|
-
if (sheets.length === 0) return '(空文件或未找到数据)'
|
|
812
|
-
const sx = cfgStripXmlns(ctx.fs.zipReadEntry(path, sheets[0]))
|
|
813
|
-
// 行 → cell
|
|
814
|
-
const rows = []
|
|
815
|
-
const reRow = /<row[^>]*>([\s\S]*?)<\/row>/g
|
|
816
|
-
let m2
|
|
817
|
-
while ((m2 = reRow.exec(sx)) !== null) {
|
|
818
|
-
const cells = []
|
|
819
|
-
const reC = /<c\b([^>]*)>([\s\S]*?)<\/c>|<c\b([^>]*)\/>/g
|
|
820
|
-
let cm
|
|
821
|
-
while ((cm = reC.exec(m2[1])) !== null) {
|
|
822
|
-
const attrs = cm[1] || cm[3] || ''
|
|
823
|
-
const inner = cm[2] || ''
|
|
824
|
-
const refM = attrs.match(/r="([A-Za-z]+)\d+"/)
|
|
825
|
-
const typeM = attrs.match(/t="([^"]+)"/)
|
|
826
|
-
const colLetter = refM ? refM[1] : ''
|
|
827
|
-
let col = parseColLetter(colLetter)
|
|
828
|
-
if (col === 0) col = cells.length + 1
|
|
829
|
-
let val = ''
|
|
830
|
-
const vM = inner.match(/<v>([^<]*)<\/v>/)
|
|
831
|
-
if (vM) val = vM[1]
|
|
832
|
-
const isM = inner.match(/<is>([\s\S]*?)<\/is>/)
|
|
833
|
-
if (!val && isM) {
|
|
834
|
-
let t = ''
|
|
835
|
-
const tRe = /<t[^>]*>([^<]*)<\/t>/g
|
|
836
|
-
let tm
|
|
837
|
-
while ((tm = tRe.exec(isM[1])) !== null) t += tm[1]
|
|
838
|
-
val = t
|
|
839
|
-
}
|
|
840
|
-
const type = typeM ? typeM[1] : ''
|
|
841
|
-
if (type === 's') {
|
|
842
|
-
const idx = parseInt(val, 10)
|
|
843
|
-
val = (!isNaN(idx) && idx >= 0 && idx < sharedStrings.length) ? sharedStrings[idx] : val
|
|
844
|
-
}
|
|
845
|
-
cells.push({ col: col, val: val })
|
|
846
|
-
}
|
|
847
|
-
if (cells.length) rows.push(cells)
|
|
848
|
-
}
|
|
849
|
-
if (rows.length === 0) return '(空文件或未找到数据)'
|
|
850
|
-
// 最大列数
|
|
851
|
-
let maxCol = 0
|
|
852
|
-
for (const row of rows) for (const c of row.cells || row) maxCol = Math.max(maxCol, (c && c.col) || 0)
|
|
853
|
-
if (maxCol === 0) { for (const row of rows) maxCol = Math.max(maxCol, row.length) }
|
|
854
|
-
// 构造记录
|
|
855
|
-
let shown = rows
|
|
856
|
-
if (lim > 0 && lim < rows.length) shown = rows.slice(0, lim)
|
|
857
|
-
const records = []
|
|
858
|
-
for (const row of shown) {
|
|
859
|
-
const rec = new Array(maxCol).fill('')
|
|
860
|
-
for (const c of (row.cells || row)) {
|
|
861
|
-
const idx = (c && c.col || 0) - 1
|
|
862
|
-
if (idx >= 0 && idx < maxCol) rec[idx] = c.val
|
|
863
|
-
}
|
|
864
|
-
records.push(rec)
|
|
865
|
-
}
|
|
866
|
-
let out = `工作表: **${sheetName}** · 共 ${rows.length} 行 × ${maxCol} 列`
|
|
867
|
-
if (lim > 0 && rows.length > lim) out += ` · 显示前 ${lim} 行`
|
|
868
|
-
out += '\n\n' + writeMarkdownTable(records, null)
|
|
869
|
-
return out
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
return {
|
|
874
|
-
name: 'tool-office',
|
|
875
|
-
inject: ['fs'],
|
|
876
|
-
purpose: '办公文档(csv_read/csv_write/json_to_table/table_stats/text_report/word_read/read_xlsx JS 原生化;word_write/write_xlsx/read_pdf/markdown_to_html 保留独立二进制)——2026-08-22 迁移',
|
|
877
|
-
apply(ctx) {
|
|
878
|
-
for (const t of tools) {
|
|
879
|
-
ctx.tools.register({
|
|
880
|
-
name: t.name,
|
|
881
|
-
description: t.description,
|
|
882
|
-
usageGuide: t.usageGuide,
|
|
883
|
-
category: t.category,
|
|
884
|
-
readOnly: t.readOnly,
|
|
885
|
-
requiresApproval: t.requiresApproval,
|
|
886
|
-
systemTool: t.systemTool,
|
|
887
|
-
parameters: t.parameters,
|
|
888
|
-
execute: (args) => (impls[t.name] ? impls[t.name](ctx, args || {}) : ctx.binary.exec(t.name, args || {})),
|
|
889
|
-
})
|
|
890
|
-
}
|
|
891
|
-
},
|
|
892
|
-
}
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// tool-office — 办公文档(csv_read/csv_write/json_to_table/table_stats/text_report/word_read)
|
|
3
|
+
//
|
|
4
|
+
// 生成来源(2026-08-16):内置 Go 工具组 → 磁盘外置插件(tool_plugin_gen.go
|
|
5
|
+
// 自动生成,schema 完整外置拷贝)。api 声明在插件,execute 调 ctx.binary 复用本插件目录 bin/ 下的独立二进制(源码 plugins-src/plugins/<name>/,改实现重编译即更换)。
|
|
6
|
+
// 工具清单:csv_read、csv_write、json_to_table、table_stats、text_report、word_read、word_write、read_xlsx、write_xlsx、read_pdf、markdown_to_html
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════
|
|
8
|
+
const tools = [
|
|
9
|
+
{
|
|
10
|
+
"name": "csv_read",
|
|
11
|
+
"description": "读取 CSV/TSV 文件并以 Markdown 表格形式返回内容。参数 delimiter 可选 \"comma\"(逗号, 默认)或 \"tab\"(制表符)。columns 按列索引过滤(从 0 开始,逗号分隔,如 \"0,2,3\")。limit 限制返回行数(默认 100,-1=全部),offset 跳过前 N 行。",
|
|
12
|
+
"usageGuide": "读取 CSV/TSV 文件并以 Markdown 表格形式返回。比直接 read 读 CSV 更友好(自动解析分隔符+格式化表格)。delimiter 可指定 comma/tab。",
|
|
13
|
+
"parameters": {
|
|
14
|
+
"properties": {
|
|
15
|
+
"columns": {
|
|
16
|
+
"description": "可选:要显示的列索引(从 0 开始,逗号分隔),省略显示全部",
|
|
17
|
+
"type": "string"
|
|
18
|
+
},
|
|
19
|
+
"delimiter": {
|
|
20
|
+
"description": "可选:分隔符,\"comma\"(逗号)或 \"tab\"(制表符),默认 \"comma\"",
|
|
21
|
+
"type": "string"
|
|
22
|
+
},
|
|
23
|
+
"limit": {
|
|
24
|
+
"description": "可选:最大返回行数(默认 100,-1 表示全部)",
|
|
25
|
+
"type": "integer"
|
|
26
|
+
},
|
|
27
|
+
"offset": {
|
|
28
|
+
"description": "可选:跳过前 N 行(默认 0)",
|
|
29
|
+
"type": "integer"
|
|
30
|
+
},
|
|
31
|
+
"path": {
|
|
32
|
+
"description": "文件路径(工作区内);相对主项目根解析,跨项目请传绝对路径",
|
|
33
|
+
"type": "string"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"required": [
|
|
37
|
+
"path"
|
|
38
|
+
],
|
|
39
|
+
"type": "object"
|
|
40
|
+
},
|
|
41
|
+
"readOnly": true
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "csv_write",
|
|
45
|
+
"description": "将表格数据写入 CSV/TSV 文件。data 为 JSON 二维数组(如 [[\"列1\",\"列2\"],[\"值1\",\"值2\"]])或 Markdown 表格文本。delimiter 可选 \"comma\"(逗号, 默认)或 \"tab\"(制表符)。header 为可选的表头行 JSON 数组,省略则从 data 首行自动提取。",
|
|
46
|
+
"usageGuide": "将表格数据写入 CSV/TSV 文件。data 参数支持 JSON 二维数组或 Markdown 表格文本。比手动拼接 CSV 更高效(自动处理转义+分隔符)。需审核批准。",
|
|
47
|
+
"parameters": {
|
|
48
|
+
"properties": {
|
|
49
|
+
"data": {
|
|
50
|
+
"description": "表格数据:JSON 二维数组字符串,或 Markdown 表格文本",
|
|
51
|
+
"type": "string"
|
|
52
|
+
},
|
|
53
|
+
"delimiter": {
|
|
54
|
+
"description": "可选:分隔符 \"comma\" 或 \"tab\",默认 \"comma\"",
|
|
55
|
+
"type": "string"
|
|
56
|
+
},
|
|
57
|
+
"header": {
|
|
58
|
+
"description": "可选:表头行 JSON 数组,如 \"[\"姓名\",\"年龄\"]\"",
|
|
59
|
+
"type": "string"
|
|
60
|
+
},
|
|
61
|
+
"path": {
|
|
62
|
+
"description": "文件路径(工作区内);相对主项目根解析,跨项目请传绝对路径",
|
|
63
|
+
"type": "string"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"required": [
|
|
67
|
+
"path",
|
|
68
|
+
"data"
|
|
69
|
+
],
|
|
70
|
+
"type": "object"
|
|
71
|
+
},
|
|
72
|
+
"requiresApproval": true
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "json_to_table",
|
|
76
|
+
"description": "将 JSON 数组字符串转为 Markdown 表格。columns 指定列名和顺序(逗号分隔,如 \"name,age\"),省略则使用全部键并按字母序排列。limit 限制行数(默认 100,-1=全部),title 可选表格标题。",
|
|
77
|
+
"usageGuide": "将 JSON 数组字符串转为 Markdown 表格。columns 参数指定列名和顺序。比手动格式化更高效(自动生成表头+对齐)。",
|
|
78
|
+
"parameters": {
|
|
79
|
+
"properties": {
|
|
80
|
+
"columns": {
|
|
81
|
+
"description": "可选:显示的键名(逗号分隔),省略则用全部键",
|
|
82
|
+
"type": "string"
|
|
83
|
+
},
|
|
84
|
+
"json": {
|
|
85
|
+
"description": "JSON 数组字符串(必填,如 [{\"name\":\"张三\",\"age\":30}])",
|
|
86
|
+
"type": "string"
|
|
87
|
+
},
|
|
88
|
+
"limit": {
|
|
89
|
+
"description": "可选:最大行数(默认 100,-1=全部)",
|
|
90
|
+
"type": "integer"
|
|
91
|
+
},
|
|
92
|
+
"title": {
|
|
93
|
+
"description": "可选:表格标题",
|
|
94
|
+
"type": "string"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"required": [
|
|
98
|
+
"json"
|
|
99
|
+
],
|
|
100
|
+
"type": "object"
|
|
101
|
+
},
|
|
102
|
+
"readOnly": true
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "table_stats",
|
|
106
|
+
"description": "对表格数据的数值列做基本统计(求和、均值、最大值、最小值、计数)。data 为 CSV 文本、JSON 数组或文件路径。format 指定数据格式:\"csv\"(CSV 文本)\"json\"(JSON 数组)\"file\"(文件路径)。group_by 按指定列分组统计(可选)。",
|
|
107
|
+
"usageGuide": "对表格数据的数值列做基本统计(求和/均值/最大/最小/计数)。group_by 可按某列分组统计。比手动计算更快(自动识别数值列+分组聚合)。",
|
|
108
|
+
"parameters": {
|
|
109
|
+
"properties": {
|
|
110
|
+
"data": {
|
|
111
|
+
"description": "数据:CSV 文本、JSON 数组字符串、或文件路径(根据 format)",
|
|
112
|
+
"type": "string"
|
|
113
|
+
},
|
|
114
|
+
"format": {
|
|
115
|
+
"description": "数据格式:\"csv\"(默认)/ \"json\" / \"file\"",
|
|
116
|
+
"type": "string"
|
|
117
|
+
},
|
|
118
|
+
"group_by": {
|
|
119
|
+
"description": "可选:按此列名分组统计",
|
|
120
|
+
"type": "string"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"required": [
|
|
124
|
+
"data"
|
|
125
|
+
],
|
|
126
|
+
"type": "object"
|
|
127
|
+
},
|
|
128
|
+
"readOnly": true
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "text_report",
|
|
132
|
+
"description": "扫描工作区目录树,按文件扩展名分组统计行数。支持统计总行数、代码行(非空非纯注释)、注释行、空行。path 限定扫描子目录(默认主项目根;相对主项目根解析,跨项目请传绝对路径);extensions 限定文件扩展名(逗号分隔,如 \".go,.ts,.vue\");group_by 分组方式:\"ext\"(按扩展名,默认)或 \"dir\"(按目录)。自动跳过依赖库(node_modules/vendor/.venv…)、构建产物、VCS 与项目根下的 IDE 运行数据目录。",
|
|
133
|
+
"usageGuide": "扫描目录树,按文件扩展名或目录分组统计代码行数。快速了解项目规模和技术栈分布。path 相对主项目根解析(跨项目传绝对路径)。比 bash wc -l 更智能(自动跳过依赖库/构建产物/VCS/IDE 运行数据目录+按类型分组)。",
|
|
134
|
+
"parameters": {
|
|
135
|
+
"properties": {
|
|
136
|
+
"extensions": {
|
|
137
|
+
"description": "可选:限定文件扩展名,逗号分隔(如 \".go,.ts,.vue\")",
|
|
138
|
+
"type": "string"
|
|
139
|
+
},
|
|
140
|
+
"group_by": {
|
|
141
|
+
"description": "可选:分组方式 \"ext\"(按扩展名,默认)或 \"dir\"(按目录)",
|
|
142
|
+
"type": "string"
|
|
143
|
+
},
|
|
144
|
+
"max_files": {
|
|
145
|
+
"description": "可选:最大扫描文件数(默认 5000)",
|
|
146
|
+
"type": "integer"
|
|
147
|
+
},
|
|
148
|
+
"path": {
|
|
149
|
+
"description": "可选:要扫描的目录路径(默认工作区根);相对主项目根解析,跨项目请传绝对路径",
|
|
150
|
+
"type": "string"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"type": "object"
|
|
154
|
+
},
|
|
155
|
+
"readOnly": true
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "word_read",
|
|
159
|
+
"description": "读取 Microsoft Word (.docx) 文件的内容,以纯文本或 Markdown 格式返回。支持段落文本、表格、列表等基本结构提取。format 可选 \"text\"(纯文本,默认)或 \"markdown\"(Markdown 格式)。limit 限制返回字符数(默认 10000,防止内容过长)。",
|
|
160
|
+
"usageGuide": "读取 Microsoft Word (.docx) 文件内容,以纯文本或 Markdown 格式返回。比手动打开 Word 更高效(直接提取文本到上下文)。",
|
|
161
|
+
"parameters": {
|
|
162
|
+
"properties": {
|
|
163
|
+
"format": {
|
|
164
|
+
"description": "可选:输出格式 \"text\"(纯文本,默认)或 \"markdown\"",
|
|
165
|
+
"type": "string"
|
|
166
|
+
},
|
|
167
|
+
"limit": {
|
|
168
|
+
"description": "可选:最大返回字符数(默认 10000,-1=全部)",
|
|
169
|
+
"type": "integer"
|
|
170
|
+
},
|
|
171
|
+
"path": {
|
|
172
|
+
"description": "Word 文件路径(工作区内,.docx 格式);相对主项目根解析,跨项目请传绝对路径",
|
|
173
|
+
"type": "string"
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
"required": [
|
|
177
|
+
"path"
|
|
178
|
+
],
|
|
179
|
+
"type": "object"
|
|
180
|
+
},
|
|
181
|
+
"readOnly": true
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"name": "word_write",
|
|
185
|
+
"description": "生成 Microsoft Word (.docx) 文档。content 为 Markdown 格式文本(支持 # 标题、普通段落、- 列表项、| 表格),系统自动将其转换为 OOXML 格式写入 .docx 文件。title 为可选的文档标题(默认无)。",
|
|
186
|
+
"usageGuide": "生成 Microsoft Word (.docx) 文档。content 为 Markdown 格式文本。用于输出报告/文档。比手动排版更高效(Markdown 转 Word 格式)。需审核批准。",
|
|
187
|
+
"parameters": {
|
|
188
|
+
"properties": {
|
|
189
|
+
"content": {
|
|
190
|
+
"description": "文档内容(Markdown 格式:标题用 #、列表用 -、表格用 |)",
|
|
191
|
+
"type": "string"
|
|
192
|
+
},
|
|
193
|
+
"path": {
|
|
194
|
+
"description": "输出文件路径(工作区内,.docx 扩展名);相对主项目根解析,跨项目请传绝对路径",
|
|
195
|
+
"type": "string"
|
|
196
|
+
},
|
|
197
|
+
"title": {
|
|
198
|
+
"description": "可选:文档标题",
|
|
199
|
+
"type": "string"
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
"required": [
|
|
203
|
+
"path",
|
|
204
|
+
"content"
|
|
205
|
+
],
|
|
206
|
+
"type": "object"
|
|
207
|
+
},
|
|
208
|
+
"requiresApproval": true
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "read_xlsx",
|
|
212
|
+
"description": "读取 Microsoft Excel (.xlsx) 文件的内容,以 Markdown 表格形式返回各工作表。sheet 指定工作表名称(默认第一个);limit 限制行数(默认 200,-1=全部)。纯 Go 标准库实现(解析 ZIP + XML),零外部依赖。",
|
|
213
|
+
"usageGuide": "读取 Excel (.xlsx) 文件内容,以 Markdown 表格形式返回。sheet 参数指定工作表名。比直接 read 更友好(自动解析+多 sheet 支持)。",
|
|
214
|
+
"parameters": {
|
|
215
|
+
"properties": {
|
|
216
|
+
"limit": {
|
|
217
|
+
"description": "可选:最大行数(默认 200,-1=全部)",
|
|
218
|
+
"type": "integer"
|
|
219
|
+
},
|
|
220
|
+
"path": {
|
|
221
|
+
"description": "Excel 文件路径(工作区内,.xlsx 格式);相对主项目根解析,跨项目请传绝对路径",
|
|
222
|
+
"type": "string"
|
|
223
|
+
},
|
|
224
|
+
"sheet": {
|
|
225
|
+
"description": "可选:工作表名称(默认第一个工作表)",
|
|
226
|
+
"type": "string"
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
"required": [
|
|
230
|
+
"path"
|
|
231
|
+
],
|
|
232
|
+
"type": "object"
|
|
233
|
+
},
|
|
234
|
+
"readOnly": true
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"name": "write_xlsx",
|
|
238
|
+
"description": "创建 Microsoft Excel (.xlsx) 文件。data 为 JSON 二维数组(如 [[\"列1\",\"列2\"],[\"值1\",\"值2\"]])或 Markdown 表格文本。sheet 为工作表名称(默认 \"Sheet1\")。纯 Go 标准库实现(生成 ZIP + XML),零外部依赖。",
|
|
239
|
+
"usageGuide": "创建 Excel (.xlsx) 文件。data 为 JSON 二维数组或 Markdown 表格文本。比手动拼接 CSV 更专业(支持多 sheet+格式)。需审核批准。",
|
|
240
|
+
"parameters": {
|
|
241
|
+
"properties": {
|
|
242
|
+
"data": {
|
|
243
|
+
"description": "表格数据:JSON 二维数组字符串 或 Markdown 表格文本",
|
|
244
|
+
"type": "string"
|
|
245
|
+
},
|
|
246
|
+
"path": {
|
|
247
|
+
"description": "输出文件路径(工作区内,.xlsx 扩展名);相对主项目根解析,跨项目请传绝对路径",
|
|
248
|
+
"type": "string"
|
|
249
|
+
},
|
|
250
|
+
"sheet": {
|
|
251
|
+
"description": "可选:工作表名称(默认 \"Sheet1\")",
|
|
252
|
+
"type": "string"
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
"required": [
|
|
256
|
+
"path",
|
|
257
|
+
"data"
|
|
258
|
+
],
|
|
259
|
+
"type": "object"
|
|
260
|
+
},
|
|
261
|
+
"requiresApproval": true
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"name": "read_pdf",
|
|
265
|
+
"description": "提取 PDF 文件的文本内容。先尝试纯文本提取(解析 PDF 流对象);如提取结果为空或内容极少(\u003c50 字符),自动调用 pdftoppm/mutool 将页面渲染为图片 + Tesseract OCR 识别文字。page 指定页码(从 1 开始,默认全部);limit 限制返回字符数(默认 10000)。",
|
|
266
|
+
"usageGuide": "提取 PDF 文本内容。先尝试纯文本提取,失败则回退 OCR。比手动打开 PDF 复制更高效(直接提取到上下文)。",
|
|
267
|
+
"parameters": {
|
|
268
|
+
"properties": {
|
|
269
|
+
"limit": {
|
|
270
|
+
"description": "可选:最大返回字符数(默认 10000,-1=全部)",
|
|
271
|
+
"type": "integer"
|
|
272
|
+
},
|
|
273
|
+
"page": {
|
|
274
|
+
"description": "可选:页码(从 1 开始),省略则提取全部页面",
|
|
275
|
+
"type": "integer"
|
|
276
|
+
},
|
|
277
|
+
"path": {
|
|
278
|
+
"description": "PDF 文件路径(工作区内);相对主项目根解析,跨项目请传绝对路径",
|
|
279
|
+
"type": "string"
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
"required": [
|
|
283
|
+
"path"
|
|
284
|
+
],
|
|
285
|
+
"type": "object"
|
|
286
|
+
},
|
|
287
|
+
"readOnly": true
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"name": "markdown_to_html",
|
|
291
|
+
"description": "将 Markdown 文本转换为 HTML 片段。支持 # 标题、**粗体**、*斜体*、`行内代码`、```代码块```、- 无序列表、1. 有序列表、| 表格、\u003e 引用、[链接](url)、。full_html 为 true 时输出完整 HTML 文档(含 DOCTYPE + head + body),否则只输出 body 内的 HTML 片段。",
|
|
292
|
+
"usageGuide": "将 Markdown 文本转为 HTML。支持 full_html 参数输出完整 HTML 文档(含 title)。比手动转换更快(内置渲染器+代码高亮)。",
|
|
293
|
+
"parameters": {
|
|
294
|
+
"properties": {
|
|
295
|
+
"full_html": {
|
|
296
|
+
"description": "可选:是否输出完整 HTML 文档(默认 false,只输出片段)",
|
|
297
|
+
"type": "boolean"
|
|
298
|
+
},
|
|
299
|
+
"markdown": {
|
|
300
|
+
"description": "Markdown 文本(必填)",
|
|
301
|
+
"type": "string"
|
|
302
|
+
},
|
|
303
|
+
"title": {
|
|
304
|
+
"description": "可选:完整 HTML 时的页面标题",
|
|
305
|
+
"type": "string"
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
"required": [
|
|
309
|
+
"markdown"
|
|
310
|
+
],
|
|
311
|
+
"type": "object"
|
|
312
|
+
},
|
|
313
|
+
"readOnly": true
|
|
314
|
+
}
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
name: 'tool-office',
|
|
319
|
+
purpose: '办公文档(csv_read/csv_write/json_to_table/table_stats/text_report/word_read)(自动生成,迁移自内置 Go 工具组)',
|
|
320
|
+
apply(ctx) {
|
|
321
|
+
for (const t of tools) {
|
|
322
|
+
ctx.tools.register({
|
|
323
|
+
name: t.name,
|
|
324
|
+
description: t.description,
|
|
325
|
+
usageGuide: t.usageGuide,
|
|
326
|
+
category: t.category,
|
|
327
|
+
readOnly: t.readOnly,
|
|
328
|
+
requiresApproval: t.requiresApproval,
|
|
329
|
+
systemTool: t.systemTool,
|
|
330
|
+
parameters: t.parameters,
|
|
331
|
+
execute: (args) => ctx.binary.exec(t.name, args || {}),
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
}
|
package/package.json
CHANGED
package/bin/tool-office.exe
DELETED
|
Binary file
|