@bill_nee/dwg-reader 1.2.0 → 1.3.0
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/lib/index.js +167 -0
- package/package.json +14 -3
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import { PDFParse } from "pdf-parse";
|
|
3
4
|
import { LibreDwg, Dwg_File_Type, DwgCodePage } from "../vendor/libredwg-web/lib/index.js";
|
|
4
5
|
|
|
5
6
|
const WASM_DIR = fileURLToPath(new URL("../vendor/libredwg-web/wasm", import.meta.url));
|
|
@@ -7,6 +8,7 @@ const MAX_DWG_BYTES = 512 * 1024 * 1024; // 512 MB 上限,超大图纸会先
|
|
|
7
8
|
const MAX_ENTITY_DETAIL = 2000;
|
|
8
9
|
const PYTHON_EXE = "D:\\dsh-home\\dsh-runtimes\\dsh-primary-runtime\\dependencies\\python\\python.exe";
|
|
9
10
|
const DXF_SCRIPT = fileURLToPath(new URL("../scripts/read_dxf.py", import.meta.url));
|
|
11
|
+
const MAX_PDF_BYTES = 100 * 1024 * 1024; // 100 MB PDF 上限
|
|
10
12
|
|
|
11
13
|
let libreDwgPromise;
|
|
12
14
|
|
|
@@ -568,8 +570,173 @@ export const inject = ["tools", "fs", "workingDirectory"];
|
|
|
568
570
|
export function apply(ctx, config) {
|
|
569
571
|
const disposeDwg = ctx.tools.register(buildReadDwgTool(ctx));
|
|
570
572
|
const disposeDxf = ctx.tools.register(buildReadDxfTool(ctx));
|
|
573
|
+
const disposePdf = ctx.tools.register(buildReadPdfTool(ctx));
|
|
571
574
|
return () => {
|
|
572
575
|
disposeDwg();
|
|
573
576
|
disposeDxf();
|
|
577
|
+
disposePdf();
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function buildReadPdfTool(ctx) {
|
|
582
|
+
return {
|
|
583
|
+
name: "read_pdf",
|
|
584
|
+
description:
|
|
585
|
+
"读取 PDF 文件(基于 pdf-parse,无需安装 Adobe Acrobat)。" +
|
|
586
|
+
"返回 PDF 页数、元数据(标题/作者/创建日期等)与可选全文文本。",
|
|
587
|
+
parameters: {
|
|
588
|
+
type: "object",
|
|
589
|
+
additionalProperties: true,
|
|
590
|
+
properties: {
|
|
591
|
+
file_path: {
|
|
592
|
+
type: "string",
|
|
593
|
+
description: "PDF 文件路径(绝对路径或相对当前工作目录)。",
|
|
594
|
+
},
|
|
595
|
+
extract_text: {
|
|
596
|
+
type: "boolean",
|
|
597
|
+
description: "是否提取全文文本(默认 true)。",
|
|
598
|
+
},
|
|
599
|
+
max_text_chars: {
|
|
600
|
+
type: "integer",
|
|
601
|
+
description: "提取文本的最大字符数(默认 50000,0 表示不限制)。",
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
required: ["file_path"],
|
|
605
|
+
},
|
|
606
|
+
output: {
|
|
607
|
+
schema: {
|
|
608
|
+
type: "object",
|
|
609
|
+
additionalProperties: true,
|
|
610
|
+
properties: {
|
|
611
|
+
path: { type: "string", description: "解析后的文件路径。" },
|
|
612
|
+
fileName: { type: "string", description: "文件名。" },
|
|
613
|
+
fileSizeBytes: { type: "integer", description: "文件字节数。" },
|
|
614
|
+
format: { type: "string", description: "文件格式(固定为 pdf)。" },
|
|
615
|
+
pageCount: { type: "integer", description: "PDF 页数。" },
|
|
616
|
+
metadata: { type: "object", additionalProperties: true, description: "PDF 元数据(标题、作者、创建日期等)。" },
|
|
617
|
+
text: { type: "string", description: "提取的全文文本(可能被截断)。" },
|
|
618
|
+
textLength: { type: "integer", description: "提取文本的字符数。" },
|
|
619
|
+
textTruncated: { type: "boolean", description: "文本是否被截断。" },
|
|
620
|
+
error: { type: "string", description: "错误信息;成功时为 null。" },
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
render: (args, value) => {
|
|
624
|
+
const lines = [];
|
|
625
|
+
lines.push(`<path>${value.path}</path>`);
|
|
626
|
+
lines.push(`<type>pdf</type>`);
|
|
627
|
+
if (value.error) {
|
|
628
|
+
lines.push(`<content>读取 PDF 失败:${value.error}</content>`);
|
|
629
|
+
return [{ type: "text", text: lines.join("\n") }];
|
|
630
|
+
}
|
|
631
|
+
const sizeMb = (value.fileSizeBytes / 1024 / 1024).toFixed(2);
|
|
632
|
+
lines.push(`<content>已解析 PDF:${value.fileName}(${sizeMb} MB)`);
|
|
633
|
+
lines.push(`页数:${value.pageCount}`);
|
|
634
|
+
if (value.metadata && Object.keys(value.metadata).length) {
|
|
635
|
+
const metaLines = Object.entries(value.metadata)
|
|
636
|
+
.filter(([, v]) => v != null && String(v).trim())
|
|
637
|
+
.map(([k, v]) => ` ${k}: ${v}`)
|
|
638
|
+
.join("\n");
|
|
639
|
+
if (metaLines) lines.push(`元数据:\n${metaLines}`);
|
|
640
|
+
}
|
|
641
|
+
lines.push(`文本长度:${value.textLength} 字符${value.textTruncated ? "(已截断)" : ""}`);
|
|
642
|
+
if (value.text) {
|
|
643
|
+
const preview = value.text.slice(0, 300);
|
|
644
|
+
lines.push(`文本预览:${preview}${value.text.length > 300 ? "…" : ""}`);
|
|
645
|
+
}
|
|
646
|
+
lines.push(`</content>`);
|
|
647
|
+
return [{ type: "text", text: lines.join("\n") }];
|
|
648
|
+
},
|
|
649
|
+
presentationMeta: () => null,
|
|
650
|
+
},
|
|
651
|
+
isConcurrencySafe: () => false,
|
|
652
|
+
presentCall: (args) => ({
|
|
653
|
+
card: "generic",
|
|
654
|
+
title: `读取 PDF ${args.file_path || "(未指定路径)"}`,
|
|
655
|
+
kind: "read",
|
|
656
|
+
locations: [{ path: args.file_path || "" }],
|
|
657
|
+
}),
|
|
658
|
+
presentResult: (args, result) => {
|
|
659
|
+
if (result && result.isError) return undefined;
|
|
660
|
+
return {
|
|
661
|
+
card: "generic",
|
|
662
|
+
title: `PDF 解析完成:${args.file_path || ""}`,
|
|
663
|
+
kind: "read",
|
|
664
|
+
locations: [{ path: args.file_path || "" }],
|
|
665
|
+
};
|
|
666
|
+
},
|
|
667
|
+
async execute(args, exec) {
|
|
668
|
+
if (typeof args !== "object" || args === null) {
|
|
669
|
+
throw new Error('invalid arguments: expected an object with a "file_path" string');
|
|
670
|
+
}
|
|
671
|
+
if (typeof args.file_path !== "string" || !args.file_path.trim()) {
|
|
672
|
+
throw new Error('invalid arguments: "file_path" must be a non-empty string');
|
|
673
|
+
}
|
|
674
|
+
if (args.extract_text !== undefined && typeof args.extract_text !== "boolean") {
|
|
675
|
+
throw new Error('invalid arguments: "extract_text" must be a boolean');
|
|
676
|
+
}
|
|
677
|
+
if (args.max_text_chars !== undefined && (typeof args.max_text_chars !== "number" || !Number.isInteger(args.max_text_chars))) {
|
|
678
|
+
throw new Error('invalid arguments: "max_text_chars" must be an integer');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const resolveOptions = await sessionResolveOptions(ctx, exec);
|
|
682
|
+
const target = await ctx.fs.resolve(args.file_path.trim(), resolveOptions);
|
|
683
|
+
const info = await ctx.fs.stat(target, exec.signal);
|
|
684
|
+
const displayPath = ctx.fs.processPath(target);
|
|
685
|
+
if (!info) {
|
|
686
|
+
ctx.emit("fs/observed", target, { kind: "absent" }, exec);
|
|
687
|
+
throw new Error(`cannot read "${displayPath}": not found`);
|
|
688
|
+
}
|
|
689
|
+
if (info.type !== "file") {
|
|
690
|
+
throw new Error(`cannot read "${displayPath}": not a regular file`);
|
|
691
|
+
}
|
|
692
|
+
if (info.size !== undefined && info.size > MAX_PDF_BYTES) {
|
|
693
|
+
throw new Error(
|
|
694
|
+
`cannot read "${displayPath}": file is ${(info.size / 1024 / 1024).toFixed(1)} MB, ` +
|
|
695
|
+
`超过 ${MAX_PDF_BYTES / 1024 / 1024} MB 上限`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
const bytes = await ctx.fs.readBytes(target, exec.signal, MAX_PDF_BYTES);
|
|
699
|
+
ctx.emit("fs/observed", target, { kind: "present", version: info.version }, exec);
|
|
700
|
+
|
|
701
|
+
let parser;
|
|
702
|
+
try {
|
|
703
|
+
parser = new PDFParse({ data: bytes });
|
|
704
|
+
} catch (error) {
|
|
705
|
+
throw new Error(`cannot parse "${displayPath}": 初始化 PDF 解析器失败: ${error.message}`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
let text = "";
|
|
709
|
+
let textTruncated = false;
|
|
710
|
+
let pageCount = 0;
|
|
711
|
+
let metadata = {};
|
|
712
|
+
try {
|
|
713
|
+
const infoResult = await parser.getInfo();
|
|
714
|
+
pageCount = infoResult.info?.Pages != null ? Number(infoResult.info.Pages) : 0;
|
|
715
|
+
metadata = infoResult.metadata ?? {};
|
|
716
|
+
if (args.extract_text !== false) {
|
|
717
|
+
const textResult = await parser.getText();
|
|
718
|
+
text = textResult?.text ?? "";
|
|
719
|
+
const maxChars = args.max_text_chars ?? 50000;
|
|
720
|
+
if (maxChars > 0 && text.length > maxChars) {
|
|
721
|
+
text = text.slice(0, maxChars);
|
|
722
|
+
textTruncated = true;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
} catch (error) {
|
|
726
|
+
throw new Error(`cannot parse "${displayPath}": PDF 内容提取失败: ${error.message}`);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return toJson({
|
|
730
|
+
path: displayPath,
|
|
731
|
+
fileName: displayPath.split(/[\\/]/).pop(),
|
|
732
|
+
fileSizeBytes: info.size !== undefined ? info.size : bytes.byteLength,
|
|
733
|
+
format: "pdf",
|
|
734
|
+
pageCount,
|
|
735
|
+
metadata,
|
|
736
|
+
text,
|
|
737
|
+
textLength: text.length,
|
|
738
|
+
textTruncated,
|
|
739
|
+
});
|
|
740
|
+
},
|
|
574
741
|
};
|
|
575
742
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bill_nee/dwg-reader",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "直接读取 AutoCAD DWG/DXF
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "直接读取 AutoCAD DWG/DXF 与 PDF 文件的 DSH 插件(DWG 基于 libredwg-web 0.8.2 WASM,DXF 基于 Python ezdxf 桥接,PDF 基于 pdf-parse,GPL-3.0)。",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"author": "MEB 工程造价项目助手 <meb@example.com>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/NIDEXIANG/dwg-reader-plugin-20261029.git"
|
|
10
10
|
},
|
|
11
|
-
"keywords": [
|
|
11
|
+
"keywords": [
|
|
12
|
+
"dwg",
|
|
13
|
+
"dwg-reader",
|
|
14
|
+
"autocad",
|
|
15
|
+
"cad",
|
|
16
|
+
"dsh-plugin",
|
|
17
|
+
"libredwg",
|
|
18
|
+
"wasm"
|
|
19
|
+
],
|
|
12
20
|
"type": "module",
|
|
13
21
|
"engines": {
|
|
14
22
|
"node": ">=20"
|
|
@@ -32,5 +40,8 @@
|
|
|
32
40
|
"bundle": {
|
|
33
41
|
"patch": "./cordis.patch.yml"
|
|
34
42
|
}
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"pdf-parse": "^2.4.5"
|
|
35
46
|
}
|
|
36
47
|
}
|