@bill_nee/dwg-reader 1.1.0 → 1.2.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/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # @bill_nee/dwg-reader
2
2
 
3
- DSH (DeepSeek Harness) 插件:可直接读取 AutoCAD DWG 格式图纸,无需安装 AutoCAD。
3
+ DSH(DeepSeek Harness)插件:可直接读取 AutoCAD DWG 格式图纸,无需安装 AutoCAD。
4
4
 
5
5
  基于 [libredwg-web](https://github.com/mlightcad/libredwg-web) 0.8.2 WASM 编译,在浏览器/Node 环境中解析 DWG 文件,返回图层、实体、版本、编码、图幅等信息,并可导出 SVG 预览。
6
6
 
7
7
  ## 安装
8
8
 
9
+ 在 DSH 工作台或终端执行:
10
+
9
11
  ```powershell
10
12
  dsh plugin --profile desktop add @bill_nee/dwg-reader@1.1.0
11
13
  ```
@@ -14,11 +16,84 @@ dsh plugin --profile desktop add @bill_nee/dwg-reader@1.1.0
14
16
 
15
17
  ## 主要能力
16
18
 
17
- - 读取 DWG(AutoCAD R14~R2018 等常见版本)
18
- - 返回图层列表、实体类型与数量、图纸版本(ACADVER)、代码页、图幅
19
- - 导出 SVG 预览到指定路径
20
- - 对无效/损坏文件给出明确错误
19
+ | 能力 | 说明 |
20
+ |------|------|
21
+ | 读取 DWG | 支持 AutoCAD R14~R2018 等常见版本(AC1015~AC1032) |
22
+ | 图层信息 | 返回所有图层名称、颜色、冻结/关闭/锁定状态 |
23
+ | 实体统计 | 返回实体总数及按类型(LINE/TEXT/INSERT/HATCH/DIMENSION 等)分类统计 |
24
+ | 图纸版本 | 返回 ACADVER、版本号、版本描述(如 r2018/AC1032/AutoCAD Release 2018) |
25
+ | 代码页 | 返回 DWG 代码页(如 CP_ANSI_936) |
26
+ | 图幅信息 | 返回 EXTMIN/EXTMAX 边界框 |
27
+ | 导出 SVG | 可将 DWG 内容导出为 SVG 预览文件到指定路径 |
28
+ | 错误处理 | 对无效/损坏/非 DWG 文件给出明确错误提示 |
29
+
30
+ ## 使用示例
31
+
32
+ 在 DSH 工作台调用 `read_dwg` 工具:
33
+
34
+ ```json
35
+ {
36
+ "file_path": "E:\\2026 工程资料\\16 天喔蜜饯工厂\\03 CAD DESIGN\\02 HVAC DESIGN PLAN.dwg",
37
+ "entities": true,
38
+ "entity_limit": 50,
39
+ "export_svg": true,
40
+ "svg_output_path": "F:\\HAYNESS 工作区1\\hvac-plan.svg"
41
+ }
42
+ ```
43
+
44
+ 返回示例:
45
+
46
+ ```json
47
+ {
48
+ "file_path": "E:\\...\\02 HVAC DESIGN PLAN.dwg",
49
+ "file_size": 123456,
50
+ "version": { "type": "r2018", "hdr": "AC1032", "description": "AutoCAD Release 2018", "version": 33 },
51
+ "codepage": 39,
52
+ "codepageName": "CP_ANSI_936",
53
+ "layer_count": 126,
54
+ "entity_count": 1514,
55
+ "unknown_entity_count": 0,
56
+ "extents": { "min": { "x": 0, "y": 0, "z": 0 }, "max": { "x": 266000, "y": 286000, "z": 0 } },
57
+ "layers": [ { "name": "HEPA BOX", "colorIndex": 5, ... }, ... ],
58
+ "entities": [
59
+ { "type": "INSERT", "handle": "...", "layer": "HEPA BOX", "blockName": "...", ... },
60
+ ...
61
+ ],
62
+ "svg_path": "F:\\HAYNESS 工作区1\\hvac-plan.svg"
63
+ }
64
+ ```
65
+
66
+ ## 测试实例
67
+
68
+ 已对真实工程图纸进行验证:
69
+
70
+ - **天喔蜜饯工厂暖通图纸**(r2018/AC1032):1514 个实体,126 个图层,其中高效送风口(HEPA BOX 图层)108 个,方形散流器 18 个,射流空调 5 个。
71
+ - **植本清源空调设备图**(r2007/AC1021):1762 个实体,14 个图层。
72
+ - **无效/损坏文件**:返回明确错误,不崩溃。
73
+
74
+ ## 文件结构
75
+
76
+ ```
77
+ dwg-reader-plugin/
78
+ ├── lib/index.js # 插件入口,注册 read_dwg 工具
79
+ ├── cordis.patch.yml # DSH bundle 插件清单
80
+ ├── package.json # npm 包清单
81
+ ├── LICENSE # GPL-3.0-only
82
+ ├── README.md # 本文件
83
+ ├── icon.svg # 插件图标
84
+ ├── locale/en.json # 英文元数据
85
+ ├── locale/zh.json # 中文元数据
86
+ └── vendor/libredwg-web/ # libredwg-web 0.8.2 WASM 库(已 vendored)
87
+ ├── lib/ # JS 封装层
88
+ └── wasm/ # libredwg-web.wasm(WebAssembly 解析核心)
89
+ ```
21
90
 
22
91
  ## 许可
23
92
 
24
93
  GPL-3.0-only。依赖 libredwg-web 同样为 GPL,公开分发请保留 LICENSE 并提供对应源码。
94
+
95
+ ## 安装路径
96
+
97
+ - npm 公开包:`@bill_nee/dwg-reader@1.1.0`
98
+ - 安装命令:`dsh plugin --profile desktop add @bill_nee/dwg-reader@1.1.0`
99
+ - GitHub 源码:https://github.com/NIDEXIANG/dwg-reader-plugin-20261029
package/lib/index.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { fileURLToPath } from "node:url";
2
+ import { spawn } from "node:child_process";
2
3
  import { LibreDwg, Dwg_File_Type, DwgCodePage } from "../vendor/libredwg-web/lib/index.js";
3
4
 
4
5
  const WASM_DIR = fileURLToPath(new URL("../vendor/libredwg-web/wasm", import.meta.url));
5
6
  const MAX_DWG_BYTES = 512 * 1024 * 1024; // 512 MB 上限,超大图纸会先于 WASM 内存耗尽给出明确报错
6
7
  const MAX_ENTITY_DETAIL = 2000;
8
+ const PYTHON_EXE = "D:\\dsh-home\\dsh-runtimes\\dsh-primary-runtime\\dependencies\\python\\python.exe";
9
+ const DXF_SCRIPT = fileURLToPath(new URL("../scripts/read_dxf.py", import.meta.url));
7
10
 
8
11
  let libreDwgPromise;
9
12
 
@@ -272,14 +275,38 @@ function buildReadDwgTool(ctx) {
272
275
 
273
276
  const limit = Math.min(Math.max(1, args.entity_limit || 200), MAX_ENTITY_DETAIL);
274
277
  const detailEntities = args.entities ? database.entities.slice(0, limit) : [];
275
- const entities = detailEntities.map((entity) => ({
276
- type: entity.type || "UNKNOWN",
277
- handle: entity.handle != null ? entity.handle : null,
278
- layer: entity.layer != null ? entity.layer : null,
279
- colorIndex: typeof entity.colorIndex === "number" ? entity.colorIndex : null,
280
- lineType: entity.lineType != null ? entity.lineType : null,
281
- isInPaperSpace: !!entity.isInPaperSpace,
282
- }));
278
+ const entities = detailEntities.map((entity) => {
279
+ const base = {
280
+ type: entity.type || "UNKNOWN",
281
+ handle: entity.handle != null ? entity.handle : null,
282
+ layer: entity.layer != null ? entity.layer : null,
283
+ colorIndex: typeof entity.colorIndex === "number" ? entity.colorIndex : null,
284
+ lineType: entity.lineType != null ? entity.lineType : null,
285
+ isInPaperSpace: !!entity.isInPaperSpace,
286
+ };
287
+ if (entity.type === "INSERT") {
288
+ base.blockName = entity.name != null ? String(entity.name) : null;
289
+ base.insertionPoint = entity.insertionPoint
290
+ ? { x: entity.insertionPoint.x, y: entity.insertionPoint.y, z: entity.insertionPoint.z }
291
+ : null;
292
+ base.rotation = typeof entity.rotation === "number" ? entity.rotation : null;
293
+ base.scale = entity.scale
294
+ ? { x: entity.scale.x, y: entity.scale.y, z: entity.scale.z }
295
+ : null;
296
+ base.attribs = Array.isArray(entity.attribs)
297
+ ? entity.attribs.map((a) => ({
298
+ tag: a.tag != null ? String(a.tag) : null,
299
+ text: a.text != null ? String(a.text) : null,
300
+ prompt: a.prompt != null ? String(a.prompt) : null,
301
+ value: a.value != null ? String(a.value) : null,
302
+ insertionPoint: a.insertionPoint
303
+ ? { x: a.insertionPoint.x, y: a.insertionPoint.y, z: a.insertionPoint.z }
304
+ : null,
305
+ }))
306
+ : [];
307
+ }
308
+ return base;
309
+ });
283
310
 
284
311
  let svgPath = null;
285
312
  if (args.export_svg) {
@@ -347,9 +374,202 @@ function buildReadDwgTool(ctx) {
347
374
  };
348
375
  }
349
376
 
377
+ function buildReadDxfTool(ctx) {
378
+ return {
379
+ name: "read_dxf",
380
+ description:
381
+ "读取 DXF 图纸文件(基于 Python ezdxf 桥接,无需安装 AutoCAD)。" +
382
+ "返回 DXF 版本、图层表、块数量、实体统计(按类型计数)与可选实体明细。",
383
+ parameters: {
384
+ type: "object",
385
+ additionalProperties: true,
386
+ properties: {
387
+ file_path: {
388
+ type: "string",
389
+ description: "DXF 文件路径(绝对路径或相对当前工作目录)。",
390
+ },
391
+ entities: {
392
+ type: "boolean",
393
+ description: "是否返回实体明细列表(默认 false,只返回统计)。",
394
+ },
395
+ entity_limit: {
396
+ type: "integer",
397
+ description: `实体明细最多返回条数(默认 200,上限 ${MAX_ENTITY_DETAIL})。`,
398
+ },
399
+ },
400
+ required: ["file_path"],
401
+ },
402
+ output: {
403
+ schema: {
404
+ type: "object",
405
+ additionalProperties: true,
406
+ properties: {
407
+ path: { type: "string", description: "解析后的文件路径。" },
408
+ fileName: { type: "string", description: "文件名。" },
409
+ fileSizeBytes: { type: "integer", description: "文件字节数。" },
410
+ format: { type: "string", description: "文件格式(固定为 dxf)。" },
411
+ dxfVersion: { type: "string", description: "DXF 版本代号(如 R2010)。" },
412
+ header: { type: "object", additionalProperties: true, description: "图头关键变量。" },
413
+ layerCount: { type: "integer", description: "图层数量。" },
414
+ layers: {
415
+ type: "array",
416
+ items: { type: "object", additionalProperties: true },
417
+ description: "图层表(名称/颜色/线型/开关状态)。",
418
+ },
419
+ blockCount: { type: "integer", description: "块定义数量。" },
420
+ entityCount: { type: "integer", description: "实体总数。" },
421
+ entityTypeCounts: {
422
+ type: "object",
423
+ additionalProperties: true,
424
+ description: "按实体类型统计数量(如 { LINE: 120, TEXT: 8 })。",
425
+ },
426
+ entities: {
427
+ type: "array",
428
+ items: { type: "object", additionalProperties: true },
429
+ description: "实体明细(仅当 entities=true 时非空)。",
430
+ },
431
+ entitiesTruncated: { type: "boolean", description: "实体明细是否被截断。" },
432
+ error: { type: "string", description: "错误信息;成功时为 null。" },
433
+ },
434
+ },
435
+ render: (args, value) => {
436
+ const lines = [];
437
+ lines.push(`<path>${value.path}</path>`);
438
+ lines.push(`<type>dxf</type>`);
439
+ if (value.error) {
440
+ lines.push(`<content>读取 DXF 失败:${value.error}</content>`);
441
+ return [{ type: "text", text: lines.join("\n") }];
442
+ }
443
+ const sizeMb = (value.fileSizeBytes / 1024 / 1024).toFixed(2);
444
+ lines.push(`<content>已解析 DXF:${value.fileName}(${sizeMb} MB)`);
445
+ lines.push(`版本:${value.dxfVersion}`);
446
+ lines.push(`图层:${value.layerCount} 个${value.layers.length ? " — " + value.layers.map((l) => l.name).join(", ") : ""}`);
447
+ lines.push(`块定义:${value.blockCount} 个;实体:${value.entityCount} 个`);
448
+ const typeLines = Object.entries(value.entityTypeCounts)
449
+ .sort((a, b) => b[1] - a[1])
450
+ .slice(0, 20)
451
+ .map(([t, n]) => `${t} ${n}`)
452
+ .join(",");
453
+ if (typeLines) lines.push(`实体构成:${typeLines}${Object.keys(value.entityTypeCounts).length > 20 ? ",…" : ""}`);
454
+ if (value.header && value.header.$ACADVER) lines.push(`图版:${value.header.$ACADVER}`);
455
+ if (value.entities && value.entities.length) {
456
+ lines.push(`实体明细(${value.entities.length}${value.entitiesTruncated ? ",已截断" : ""}):`);
457
+ const shown = value.entities.slice(0, 50);
458
+ for (const [i, e] of shown.entries()) {
459
+ lines.push(
460
+ ` ${i + 1}. ${e.type} handle=${e.handle} layer=${e.layer}` +
461
+ (e.colorIndex != null ? ` color=${e.colorIndex}` : "") +
462
+ (e.lineType != null ? ` linetype=${e.lineType}` : "")
463
+ );
464
+ }
465
+ if (value.entities.length > shown.length) lines.push(` …(其余 ${value.entities.length - shown.length} 条未显示)`);
466
+ }
467
+ lines.push(`</content>`);
468
+ return [{ type: "text", text: lines.join("\n") }];
469
+ },
470
+ presentationMeta: () => null,
471
+ },
472
+ isConcurrencySafe: () => false,
473
+ presentCall: (args) => ({
474
+ card: "generic",
475
+ title: `读取 DXF ${args.file_path || "(未指定路径)"}`,
476
+ kind: "read",
477
+ locations: [{ path: args.file_path || "" }],
478
+ }),
479
+ presentResult: (args, result) => {
480
+ if (result && result.isError) return undefined;
481
+ return {
482
+ card: "generic",
483
+ title: `DXF 解析完成:${args.file_path || ""}`,
484
+ kind: "read",
485
+ locations: [{ path: args.file_path || "" }],
486
+ };
487
+ },
488
+ async execute(args, exec) {
489
+ if (typeof args !== "object" || args === null) {
490
+ throw new Error('invalid arguments: expected an object with a "file_path" string');
491
+ }
492
+ if (typeof args.file_path !== "string" || !args.file_path.trim()) {
493
+ throw new Error('invalid arguments: "file_path" must be a non-empty string');
494
+ }
495
+ if (args.entities !== undefined && typeof args.entities !== "boolean") {
496
+ throw new Error('invalid arguments: "entities" must be a boolean');
497
+ }
498
+ if (args.entity_limit !== undefined && (typeof args.entity_limit !== "number" || !Number.isInteger(args.entity_limit))) {
499
+ throw new Error('invalid arguments: "entity_limit" must be an integer');
500
+ }
501
+
502
+ const resolveOptions = await sessionResolveOptions(ctx, exec);
503
+ const target = await ctx.fs.resolve(args.file_path.trim(), resolveOptions);
504
+ const info = await ctx.fs.stat(target, exec.signal);
505
+ const displayPath = ctx.fs.processPath(target);
506
+ if (!info) {
507
+ ctx.emit("fs/observed", target, { kind: "absent" }, exec);
508
+ throw new Error(`cannot read "${displayPath}": not found`);
509
+ }
510
+ if (info.type !== "file") {
511
+ throw new Error(`cannot read "${displayPath}": not a regular file`);
512
+ }
513
+
514
+ const pythonArgs = [
515
+ DXF_SCRIPT,
516
+ args.file_path.trim(),
517
+ String(args.entity_limit || 200),
518
+ String(args.entities === true),
519
+ ];
520
+
521
+ const result = await new Promise((resolve, reject) => {
522
+ const proc = spawn(PYTHON_EXE, pythonArgs, { stdio: ["pipe", "pipe", "pipe"] });
523
+ let stdout = "";
524
+ let stderr = "";
525
+ proc.stdout.on("data", (d) => (stdout += d.toString()));
526
+ proc.stderr.on("data", (d) => (stderr += d.toString()));
527
+ proc.on("close", (code) => {
528
+ if (code !== 0) {
529
+ reject(new Error(`Python 进程退出码 ${code}: ${stderr || stdout}`));
530
+ return;
531
+ }
532
+ try {
533
+ resolve(JSON.parse(stdout));
534
+ } catch (err) {
535
+ reject(new Error(`DXF 结果解析失败: ${err.message}; stdout=${stdout.slice(0, 200)}`));
536
+ }
537
+ });
538
+ proc.on("error", (err) => reject(new Error(`无法启动 Python 进程: ${err.message}`)));
539
+ });
540
+
541
+ if (result.error) {
542
+ throw new Error(`cannot read "${displayPath}": ${result.error}`);
543
+ }
544
+
545
+ ctx.emit("fs/observed", target, { kind: "present", version: info.version }, exec);
546
+
547
+ return toJson({
548
+ path: displayPath,
549
+ fileName: displayPath.split(/[\\/]/).pop(),
550
+ fileSizeBytes: result.fileSizeBytes ?? info.size !== undefined ? info.size : 0,
551
+ format: result.format ?? "dxf",
552
+ dxfVersion: result.dxfVersion ?? "",
553
+ header: result.header ?? {},
554
+ layerCount: result.layerCount ?? 0,
555
+ layers: result.layers ?? [],
556
+ blockCount: result.blockCount ?? 0,
557
+ entityCount: result.entityCount ?? 0,
558
+ entityTypeCounts: result.entityTypeCounts ?? {},
559
+ entities: result.entities ?? [],
560
+ entitiesTruncated: !!result.entitiesTruncated,
561
+ });
562
+ },
563
+ };
564
+ }
565
+
350
566
  export const inject = ["tools", "fs", "workingDirectory"];
351
567
 
352
568
  export function apply(ctx, config) {
353
- const dispose = ctx.tools.register(buildReadDwgTool(ctx));
354
- return () => dispose();
569
+ const disposeDwg = ctx.tools.register(buildReadDwgTool(ctx));
570
+ const disposeDxf = ctx.tools.register(buildReadDxfTool(ctx));
571
+ return () => {
572
+ disposeDwg();
573
+ disposeDxf();
574
+ };
355
575
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bill_nee/dwg-reader",
3
- "version": "1.1.0",
4
- "description": "直接读取 AutoCAD DWG 图纸的 DSH 插件(基于 libredwg-web 0.8.2 WASM,GPL-3.0)。",
3
+ "version": "1.2.0",
4
+ "description": "直接读取 AutoCAD DWG/DXF 图纸的 DSH 插件(DWG 基于 libredwg-web 0.8.2 WASM,DXF 基于 Python ezdxf 桥接,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
- "url": "https://github.com/dwg-reader-plugin/dwg-reader"
9
+ "url": "https://github.com/NIDEXIANG/dwg-reader-plugin-20261029.git"
10
10
  },
11
11
  "keywords": ["dwg", "dwg-reader", "autocad", "cad", "dsh-plugin", "libredwg", "wasm"],
12
12
  "type": "module",
@@ -21,6 +21,7 @@
21
21
  "files": [
22
22
  "lib/index.js",
23
23
  "vendor",
24
+ "scripts/*.py",
24
25
  "cordis.patch.yml",
25
26
  "locale/*.json",
26
27
  "icon.svg",
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ """读取 DXF 文件并输出 JSON(供 DSH dwg-reader 插件调用)。"""
3
+ import sys
4
+ import json
5
+ import os
6
+
7
+ try:
8
+ import ezdxf
9
+ except ImportError as exc:
10
+ print(json.dumps({"error": f"ezdxf 未安装: {exc}"}))
11
+ sys.exit(1)
12
+
13
+ DXF_PATH = sys.argv[1] if len(sys.argv) > 1 else None
14
+ ENTITY_LIMIT = int(sys.argv[2]) if len(sys.argv) > 2 else 200
15
+ WANT_ENTITIES = sys.argv[3] == "true" if len(sys.argv) > 3 else False
16
+
17
+
18
+ def point_to_list(p):
19
+ if p is None:
20
+ return None
21
+ return [float(p.x), float(p.y), float(p.z)]
22
+
23
+
24
+ def main():
25
+ if not DXF_PATH or not os.path.exists(DXF_PATH):
26
+ print(json.dumps({"error": f"文件不存在: {DXF_PATH}"}))
27
+ sys.exit(1)
28
+
29
+ try:
30
+ doc = ezdxf.readfile(DXF_PATH)
31
+ except Exception as exc:
32
+ print(json.dumps({"error": f"无法读取 DXF: {exc}"}))
33
+ sys.exit(1)
34
+
35
+ # 图层
36
+ layers = []
37
+ for layer in doc.layers:
38
+ layers.append({
39
+ "name": layer.dxf.name,
40
+ "colorIndex": layer.dxf.color,
41
+ "lineType": layer.dxf.linetype,
42
+ "frozen": bool(layer.is_frozen()),
43
+ "off": bool(layer.is_off()),
44
+ "locked": bool(layer.is_locked()),
45
+ })
46
+
47
+ # 块记录
48
+ block_count = len(list(doc.blocks))
49
+
50
+ # 实体统计
51
+ msp = doc.modelspace()
52
+ entities = list(msp)
53
+ entity_type_counts = {}
54
+ entity_details = []
55
+ for ent in entities:
56
+ etype = ent.dxftype() or "UNKNOWN"
57
+ entity_type_counts[etype] = entity_type_counts.get(etype, 0) + 1
58
+ if WANT_ENTITIES and len(entity_details) < ENTITY_LIMIT:
59
+ detail = {
60
+ "type": etype,
61
+ "handle": ent.dxf.handle if hasattr(ent.dxf, 'handle') else None,
62
+ "layer": ent.dxf.layer if hasattr(ent.dxf, 'layer') else None,
63
+ "colorIndex": ent.dxf.color if hasattr(ent.dxf, 'color') else None,
64
+ "lineType": ent.dxf.linetype if hasattr(ent.dxf, 'linetype') else None,
65
+ }
66
+ if etype == "INSERT":
67
+ detail["blockName"] = ent.dxf.name if hasattr(ent.dxf, 'name') else None
68
+ detail["insertionPoint"] = point_to_list(ent.dxf.insert) if hasattr(ent.dxf, 'insert') else None
69
+ entity_details.append(detail)
70
+
71
+ # 图头
72
+ header = {}
73
+ try:
74
+ header_vars = doc.header
75
+ for key in ("$ACADVER", "$DWGCODEPAGE", "$INSUNITS", "$EXTMIN", "$EXTMAX", "$TDCREATE", "$TDUPDATE", "$CLAYER"):
76
+ if key in header_vars:
77
+ val = header_vars[key]
78
+ if key == "$EXTMIN" or key == "$EXTMAX":
79
+ header[key] = point_to_list(val) if val else None
80
+ else:
81
+ header[key] = str(val)
82
+ except Exception:
83
+ pass
84
+
85
+ result = {
86
+ "path": os.path.abspath(DXF_PATH),
87
+ "fileName": os.path.basename(DXF_PATH),
88
+ "fileSizeBytes": os.path.getsize(DXF_PATH),
89
+ "format": "dxf",
90
+ "dxfVersion": doc.dxfversion,
91
+ "header": header,
92
+ "layerCount": len(layers),
93
+ "layers": layers,
94
+ "blockCount": block_count,
95
+ "entityCount": len(entities),
96
+ "entityTypeCounts": entity_type_counts,
97
+ "entities": entity_details,
98
+ "entitiesTruncated": WANT_ENTITIES and len(entities) > ENTITY_LIMIT,
99
+ }
100
+ print(json.dumps(result, ensure_ascii=False))
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()