weflow-cli 1.16.0 → 1.17.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +16 -4
  3. package/bin/weflow-cli.ts +348 -6
  4. package/dist/bin/weflow-cli.js +381 -8
  5. package/dist/bin/weflow-cli.js.map +1 -1
  6. package/dist/src/core/ntCore.d.ts +47 -0
  7. package/dist/src/core/ntCore.d.ts.map +1 -1
  8. package/dist/src/core/ntCore.js +24 -0
  9. package/dist/src/core/ntCore.js.map +1 -1
  10. package/dist/src/services/assistantService.d.ts +8 -1
  11. package/dist/src/services/assistantService.d.ts.map +1 -1
  12. package/dist/src/services/assistantService.js +47 -8
  13. package/dist/src/services/assistantService.js.map +1 -1
  14. package/dist/src/services/assistantTools.d.ts +8 -0
  15. package/dist/src/services/assistantTools.d.ts.map +1 -1
  16. package/dist/src/services/assistantTools.js +67 -11
  17. package/dist/src/services/assistantTools.js.map +1 -1
  18. package/dist/src/services/configService.d.ts.map +1 -1
  19. package/dist/src/services/configService.js +3 -2
  20. package/dist/src/services/configService.js.map +1 -1
  21. package/dist/src/services/cursorProbe.d.ts +44 -0
  22. package/dist/src/services/cursorProbe.d.ts.map +1 -0
  23. package/dist/src/services/cursorProbe.js +183 -0
  24. package/dist/src/services/cursorProbe.js.map +1 -0
  25. package/dist/src/services/qchatImport.d.ts +89 -0
  26. package/dist/src/services/qchatImport.d.ts.map +1 -0
  27. package/dist/src/services/qchatImport.js +273 -0
  28. package/dist/src/services/qchatImport.js.map +1 -0
  29. package/dist/src/services/setupVenv.d.ts +43 -0
  30. package/dist/src/services/setupVenv.d.ts.map +1 -0
  31. package/dist/src/services/setupVenv.js +198 -0
  32. package/dist/src/services/setupVenv.js.map +1 -0
  33. package/dist/src/utils/python.d.ts +10 -0
  34. package/dist/src/utils/python.d.ts.map +1 -1
  35. package/dist/src/utils/python.js +27 -4
  36. package/dist/src/utils/python.js.map +1 -1
  37. package/docs/DATA_CONTRACT.md +25 -0
  38. package/docs/DECISIONS.md +92 -30
  39. package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +365 -365
  40. package/docs/HEALTH-CHECK.md +104 -104
  41. package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -310
  42. package/docs/NEXT_ACTIONS.md +236 -236
  43. package/docs/NPM-PUBLISH.md +130 -130
  44. package/docs/POSITIONING.md +68 -68
  45. package/docs/PROJECT_STATE.md +6 -3
  46. package/docs/REPLAY.md +169 -0
  47. package/docs/SYNC_CONTRACT.md +197 -180
  48. package/package.json +89 -88
  49. package/requirements-3x.txt +4 -4
  50. package/requirements-voice.txt +15 -15
  51. package/resources/js/graph3d/NOTICE.txt +35 -35
  52. package/resources/js/graph3d/d3-binarytree.min.js +2 -2
  53. package/resources/js/graph3d/d3-dispatch.min.js +2 -2
  54. package/resources/js/graph3d/d3-force-3d.min.js +2 -2
  55. package/resources/js/graph3d/d3-octree.min.js +2 -2
  56. package/resources/js/graph3d/d3-quadtree.min.js +2 -2
  57. package/resources/js/graph3d/d3-timer.min.js +2 -2
  58. package/resources/js/graph3d/three.min.js +6 -6
  59. package/resources/js/graph3d/viewer2d.js +550 -448
  60. package/resources/panel/quick-menu.cjs +197 -197
  61. package/scripts/_utils.py +1338 -1338
  62. package/scripts/article_notes.py +423 -423
  63. package/scripts/chat_notes.py +86 -3
  64. package/scripts/check_readme_commands.py +427 -426
  65. package/scripts/contact_schema.py +339 -339
  66. package/scripts/create_reading_notes.py +11 -11
  67. package/scripts/daily_task.ps1 +213 -213
  68. package/scripts/fav_notes.py +321 -321
  69. package/scripts/graph_2d.py +156 -151
  70. package/scripts/graph_3d.py +672 -646
  71. package/scripts/health_check.py +473 -473
  72. package/scripts/imported_chats.py +68 -0
  73. package/scripts/install_tasks.ps1 +105 -105
  74. package/scripts/leads.py +179 -179
  75. package/scripts/network.py +206 -206
  76. package/scripts/nt_decrypt.py +135 -1
  77. package/scripts/on_this_day.py +178 -178
  78. package/scripts/opinion_timeline.py +138 -138
  79. package/scripts/promises.py +185 -185
  80. package/scripts/replay.py +803 -0
  81. package/scripts/run_hidden.vbs +25 -0
  82. package/scripts/semantic_search.py +73 -5
  83. package/scripts/user_notes.py +315 -315
  84. package/scripts/voice_transcribe.py +139 -139
  85. package/scripts/watch_issues.py +212 -212
  86. package/scripts/watch_mail.py +131 -131
  87. package/src/core/ntCore.ts +59 -0
  88. package/src/services/assistantService.ts +48 -8
  89. package/src/services/assistantTools.ts +73 -10
  90. package/src/services/configService.ts +3 -2
  91. package/src/services/cursorProbe.ts +222 -0
  92. package/src/services/qchatImport.ts +327 -0
  93. package/src/services/setupVenv.ts +238 -0
  94. package/src/services/syncService.ts +363 -363
  95. package/src/services/syncState.ts +275 -275
  96. package/src/services/wechatMedia.ts +166 -166
  97. package/src/utils/python.ts +33 -4
@@ -1,151 +1,156 @@
1
- # -*- coding: utf-8 -*-
2
- """把知识库导成一张**2D** 图谱页面(更像 Obsidian 那种平面图;同样自包含、不联网)。
3
-
4
- 与 3D 那份(`graph_3d.py`)的分工:**图怎么建**是同一套(点 = 概念页,边 = 正文里的 `[[链接]]`,
5
- 排除 `## 来源` 那一段,也共用 `--min-degree` / `--line` 的过滤),直接 import 过来;
6
- 这里只换两件事:
7
-
8
- 1. **布局按 2D 算** —— 不是把 3D 的 x/y 拍扁。z 那一维也承载结构,拍扁会把两团本不相干的点叠在一起;
9
- 2D 的解也快得多,所以同一张图重算得起。
10
- 2. **渲染换成 canvas**(`resources/js/graph3d/viewer2d.js`)——不需要 three.js,这页一张图都不引。
11
-
12
- 为什么单独一个文件、而不是给 3D 那份加个开关:两套渲染器各两百来行,塞进一个文件就没人看得完了;
13
- 共用的部分留在 `graph_3d.py`,两个入口都从那里取。
14
- """
15
- import argparse
16
- import hashlib
17
- import json
18
- import os
19
- import subprocess
20
- import sys
21
- from pathlib import Path
22
-
23
- REPO = Path(__file__).resolve().parent.parent
24
- sys.path.insert(0, str(REPO / 'scripts'))
25
- sys.path.insert(0, str(REPO))
26
- # 先切到仓库根(`wiki_lint.CARD_DIRS` 是导入时按 cwd 算的,见 graph_3d.py 里那段)
27
- os.chdir(REPO)
28
- import graph_3d as g # noqa: E402
29
-
30
- VIEWER2D = g.LIB_DIR / 'viewer2d.js'
31
- LAYOUT = g.LIB_DIR / 'layout.mjs'
32
- DEFAULT_OUT = REPO / 'output' / 'knowledge-graph-2d.html'
33
-
34
-
35
- def _parse_args(argv):
36
- ap = argparse.ArgumentParser(description='把知识库导成一张自包含的 2D 图谱页面(只读本地、不联网)')
37
- ap.add_argument('--vault', default=str(g.DEFAULT_VAULT), help='Vault 根目录(默认 output/wechat-vault)')
38
- ap.add_argument('--out', default=str(DEFAULT_OUT), help='页面写到哪(默认 output/knowledge-graph-2d.html)')
39
- ap.add_argument('--min-degree', dest='min_degree', metavar='N', type=int, default=0,
40
- help='只画在原图里连接数 ≥N 的概念(去掉细枝,看骨架);0 = 全画')
41
- ap.add_argument('--line', dest='line', metavar='WHICH', choices=['all', 'wiki', 'chat'], default='all',
42
- help='只画某一条线:文章线的概念页 / 聊天线的概念页 / 两条都画')
43
- ap.add_argument('--ticks', type=int, default=250, help='力导向迭代次数(2D 比 3D 快得多)')
44
- ap.add_argument('--cache', default=str(g.CACHE_DIR), help='布局缓存目录(派生数据;测试请指到临时目录)')
45
- ap.add_argument('--dry-run', action='store_true', help='只报概念与链接数:不写文件、也不算布局')
46
- ap.add_argument('--json', action='store_true', help='输出机器可读结果')
47
- return ap.parse_args(argv)
48
-
49
-
50
- def _missing():
51
- # 2D 页面**不需要 three.js**:布局用 d3,渲染是裸 canvas。缺件要在跑布局之前说清楚,
52
- # 否则报错会长得像"库坏了"。
53
- wanted = [VIEWER2D, LAYOUT] + [g.LIB_DIR / f for f in g.LIB_D3]
54
- return [p.name for p in wanted if not p.exists()]
55
-
56
-
57
- def main(argv=None):
58
- # 手跑时 stdout 是本机编码(这台机器是 GBK),中文会乱码、非 GBK 字符直接崩掉整次生成
59
- sys.stdout.reconfigure(encoding='utf-8', errors='replace')
60
- args = _parse_args(sys.argv[1:] if argv is None else argv)
61
- vault = Path(args.vault)
62
- # 与 `graph_3d.build_graph` 一样,清单从 `_utils.KNOWLEDGE_LINES` 读(经 `wiki_lint` 透出),
63
- # 不在这里硬编码——硬编码的地方不会跟着清单变,只会静默少算。
64
- if not any((vault / concepts).is_dir() for _line, _root, concepts in g.wl.KNOWLEDGE_LINES):
65
- return g._fail('Vault 里没有概念目录:%s(先跑 `wiki compile`,或用 --vault 指定)' % vault, args.json)
66
-
67
- nodes, links = g.build_graph(vault, min_degree=args.min_degree, line=args.line)
68
- if args.dry_run:
69
- if args.json:
70
- print(json.dumps({'success': True, 'action': 'graph-2d', 'dryRun': True, 'dims': 2,
71
- 'nodes': len(nodes), 'links': len(links), 'vault': str(vault),
72
- 'minDegree': args.min_degree, 'line': args.line,
73
- 'readsLocalData': True, 'invokesAI': False, 'sendsNothing': True},
74
- ensure_ascii=False))
75
- else:
76
- print('概念 %d 个、链接 %d 条(--dry-run:没有写文件)' % (len(nodes), len(links)))
77
- return 0
78
-
79
- missing = _missing()
80
- if missing:
81
- return g._fail('缺件:%s(应当在 %s 下随包发)' % ('、'.join(missing), g.LIB_DIR), args.json)
82
-
83
- data = json.dumps({'nodes': nodes, 'links': links}, ensure_ascii=False, separators=(',', ':'))
84
- # 页面数据一份(含线的展示名),缓存键用抽掉展示名的那一份 —— 理由见 graph_3d.py
85
- html_data = json.dumps({'nodes': nodes, 'links': links, 'lineLabels': g._utils.LINE_LABELS},
86
- ensure_ascii=False, separators=(',', ':'))
87
- # 与 3D 分开分槽:同一张图的 2D 与 3D 坐标不通用,各存一份(见 graph_3d.py 里那段注释)
88
- digest = g.layout_key(data, 2, args.ticks)
89
- cache = Path(args.cache)
90
- cache.mkdir(parents=True, exist_ok=True)
91
- graph_file = cache / ('graph-%s.json' % digest)
92
- pos_file = cache / ('positions-%s.json' % digest)
93
- same = graph_file.exists() and graph_file.read_text(encoding='utf-8') == data
94
- if same and pos_file.exists():
95
- print('图没变,沿用已有布局')
96
- else:
97
- graph_file.write_text(data, encoding='utf-8')
98
- g.run_layout(cache, digest, 2, args.ticks)
99
- pos = pos_file.read_text(encoding='utf-8')
100
-
101
- viewer = VIEWER2D.read_text(encoding='utf-8')
102
- page = f"""<!doctype html>
103
- <html lang="zh"><head><meta charset="utf-8">
104
- <title>知识库 2D({len(nodes)} 个概念)</title>
105
- <style>
106
- html,body {{ margin:0; height:100%; background:#05070e; color:#c9d3e6;
107
- font:13px/1.5 -apple-system,"Segoe UI","Microsoft YaHei",system-ui,sans-serif;
108
- overflow:hidden; user-select:none; }}
109
- /* canvas 是**替换元素**:`inset:0` 不会让它铺满(right/bottom 被忽略,它保持固有的 300x150),
110
- 所以必须显式给 width/height。少了这一条,视图按 300x150 算,整张图画在左上角一小块里。 */
111
- #cv {{ position:fixed; inset:0; width:100%; height:100%; display:block; cursor:grab; touch-action:none; }}
112
- #hud {{ position:fixed; left:14px; top:12px; pointer-events:none; max-width:62vw; }}
113
- #stats {{ color:#6d7a93; }}
114
- #info {{ margin-top:6px; font-size:15px; min-height:22px; }}
115
- #q {{ position:fixed; right:14px; top:12px; width:220px; padding:6px 10px;
116
- background:#121826; color:#c9d3e6; border:1px solid #26304a; border-radius:8px; outline:none; }}
117
- #q:focus {{ border-color:#3f6ad8; }}
118
- #tag {{ position:fixed; right:14px; bottom:10px; color:#4d5a72; text-align:right; }}
119
- #boot {{ position:fixed; inset:0; display:flex; align-items:center; justify-content:center;
120
- background:#05070e; color:#6d7a93; }}
121
- </style></head>
122
- <body>
123
- <canvas id="cv"></canvas>
124
- <div id="hud"><div id="stats"></div><div id="info">点一个概念看它连着谁 · 拖动平移 · 滚轮缩放</div></div>
125
- <input id="q" placeholder="搜概念,回车跳过去">
126
- <div id="tag">2D · 坐标构建期算好 · 双击空白复位</div>
127
- <div id="boot">正在画…</div>
128
- <script>window.__DATA__ = {html_data};</script>
129
- <script>window.__POS__ = "{pos}";</script>
130
- <script>{viewer}</script>
131
- </body></html>
132
- """
133
- out = Path(args.out)
134
- out.parent.mkdir(parents=True, exist_ok=True)
135
- out.write_text(page, encoding='utf-8')
136
- mb = out.stat().st_size / 1048576
137
- if args.json:
138
- print(json.dumps({'success': True, 'action': 'graph-2d', 'dims': 2,
139
- 'nodes': len(nodes), 'links': len(links), 'out': str(out),
140
- 'vault': str(vault), 'mb': round(mb, 2),
141
- 'minDegree': args.min_degree, 'line': args.line,
142
- 'selfContained': True, 'readsLocalData': True, 'invokesAI': False,
143
- 'sendsNothing': True}, ensure_ascii=False))
144
- else:
145
- print('已生成 %s' % out)
146
- print(' 概念 %d / 链接 %d / 文件 %.1f MB · 2D' % (len(nodes), len(links), mb))
147
- return 0
148
-
149
-
150
- if __name__ == '__main__':
151
- sys.exit(main())
1
+ # -*- coding: utf-8 -*-
2
+ """把知识库导成一张**2D** 图谱页面(更像 Obsidian 那种平面图;同样自包含、不联网)。
3
+
4
+ 与 3D 那份(`graph_3d.py`)的分工:**图怎么建**是同一套(点 = 概念页,边 = 正文里的 `[[链接]]`,
5
+ 排除 `## 来源` 那一段,也共用 `--min-degree` / `--line` 的过滤),直接 import 过来;
6
+ 这里只换两件事:
7
+
8
+ 1. **布局按 2D 算** —— 不是把 3D 的 x/y 拍扁。z 那一维也承载结构,拍扁会把两团本不相干的点叠在一起;
9
+ 2D 的解也快得多,所以同一张图重算得起。
10
+ 2. **渲染换成 canvas**(`resources/js/graph3d/viewer2d.js`)——不需要 three.js,这页一张图都不引。
11
+
12
+ 为什么单独一个文件、而不是给 3D 那份加个开关:两套渲染器各两百来行,塞进一个文件就没人看得完了;
13
+ 共用的部分留在 `graph_3d.py`,两个入口都从那里取。
14
+ """
15
+ import argparse
16
+ import hashlib
17
+ import json
18
+ import os
19
+ import subprocess
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ REPO = Path(__file__).resolve().parent.parent
24
+ sys.path.insert(0, str(REPO / 'scripts'))
25
+ sys.path.insert(0, str(REPO))
26
+ # 先切到仓库根(`wiki_lint.CARD_DIRS` 是导入时按 cwd 算的,见 graph_3d.py 里那段)
27
+ os.chdir(REPO)
28
+ import graph_3d as g # noqa: E402
29
+
30
+ VIEWER2D = g.LIB_DIR / 'viewer2d.js'
31
+ LAYOUT = g.LIB_DIR / 'layout.mjs'
32
+ DEFAULT_OUT = REPO / 'output' / 'knowledge-graph-2d.html'
33
+
34
+
35
+ def _parse_args(argv):
36
+ ap = argparse.ArgumentParser(description='把知识库导成一张自包含的 2D 图谱页面(只读本地、不联网)')
37
+ ap.add_argument('--vault', default=str(g.DEFAULT_VAULT), help='Vault 根目录(默认 output/wechat-vault)')
38
+ ap.add_argument('--out', default=str(DEFAULT_OUT), help='页面写到哪(默认 output/knowledge-graph-2d.html)')
39
+ ap.add_argument('--min-degree', dest='min_degree', metavar='N', type=int, default=0,
40
+ help='只画在原图里连接数 ≥N 的概念(去掉细枝,看骨架);0 = 全画')
41
+ ap.add_argument('--line', dest='line', metavar='WHICH', choices=['all', 'wiki', 'chat'], default='all',
42
+ help='只画某一条线:文章线的概念页 / 聊天线的概念页 / 两条都画')
43
+ ap.add_argument('--ticks', type=int, default=250, help='力导向迭代次数(2D 比 3D 快得多)')
44
+ ap.add_argument('--cache', default=str(g.CACHE_DIR), help='布局缓存目录(派生数据;测试请指到临时目录)')
45
+ ap.add_argument('--dry-run', action='store_true', help='只报概念与链接数:不写文件、也不算布局')
46
+ ap.add_argument('--json', action='store_true', help='输出机器可读结果')
47
+ return ap.parse_args(argv)
48
+
49
+
50
+ def _missing():
51
+ # 2D 页面**不需要 three.js**:布局用 d3,渲染是裸 canvas。缺件要在跑布局之前说清楚,
52
+ # 否则报错会长得像"库坏了"。
53
+ wanted = [VIEWER2D, LAYOUT] + [g.LIB_DIR / f for f in g.LIB_D3]
54
+ return [p.name for p in wanted if not p.exists()]
55
+
56
+
57
+ def main(argv=None):
58
+ # 手跑时 stdout 是本机编码(这台机器是 GBK),中文会乱码、非 GBK 字符直接崩掉整次生成
59
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
60
+ args = _parse_args(sys.argv[1:] if argv is None else argv)
61
+ vault = Path(args.vault)
62
+ # 与 `graph_3d.build_graph` 一样,清单从 `_utils.KNOWLEDGE_LINES` 读(经 `wiki_lint` 透出),
63
+ # 不在这里硬编码——硬编码的地方不会跟着清单变,只会静默少算。
64
+ if not any((vault / concepts).is_dir() for _line, _root, concepts in g.wl.KNOWLEDGE_LINES):
65
+ return g._fail('Vault 里没有概念目录:%s(先跑 `wiki compile`,或用 --vault 指定)' % vault, args.json)
66
+
67
+ nodes, links, evidence = g.build_graph(vault, min_degree=args.min_degree, line=args.line,
68
+ with_evidence=True)
69
+ if args.dry_run:
70
+ if args.json:
71
+ print(json.dumps({'success': True, 'action': 'graph-2d', 'dryRun': True, 'dims': 2,
72
+ 'nodes': len(nodes), 'links': len(links), 'vault': str(vault),
73
+ 'minDegree': args.min_degree, 'line': args.line,
74
+ 'readsLocalData': True, 'invokesAI': False, 'sendsNothing': True},
75
+ ensure_ascii=False))
76
+ else:
77
+ print('概念 %d 个、链接 %d 条(--dry-run:没有写文件)' % (len(nodes), len(links)))
78
+ return 0
79
+
80
+ missing = _missing()
81
+ if missing:
82
+ return g._fail('缺件:%s(应当在 %s 下随包发)' % ('、'.join(missing), g.LIB_DIR), args.json)
83
+
84
+ data = json.dumps({'nodes': nodes, 'links': links}, ensure_ascii=False, separators=(',', ':'))
85
+ # 页面数据一份(含线的展示名与**边证据**),缓存键用抽掉展示名的那一份 —— 理由见 graph_3d.py。
86
+ # 证据不进 `data`:布局用不到它,混进缓存键会让所有旧坐标一夜作废。
87
+ html_data = json.dumps({'nodes': nodes, 'links': links, 'lineLabels': g._utils.LINE_LABELS,
88
+ 'edgeEvidence': evidence},
89
+ ensure_ascii=False, separators=(',', ':'))
90
+ # 与 3D 分开分槽:同一张图的 2D 与 3D 坐标不通用,各存一份(见 graph_3d.py 里那段注释)
91
+ digest = g.layout_key(data, 2, args.ticks)
92
+ cache = Path(args.cache)
93
+ cache.mkdir(parents=True, exist_ok=True)
94
+ graph_file = cache / ('graph-%s.json' % digest)
95
+ pos_file = cache / ('positions-%s.json' % digest)
96
+ same = graph_file.exists() and graph_file.read_text(encoding='utf-8') == data
97
+ if same and pos_file.exists():
98
+ print('图没变,沿用已有布局')
99
+ else:
100
+ graph_file.write_text(data, encoding='utf-8')
101
+ g.run_layout(cache, digest, 2, args.ticks)
102
+ pos = pos_file.read_text(encoding='utf-8')
103
+
104
+ viewer = VIEWER2D.read_text(encoding='utf-8')
105
+ page = f"""<!doctype html>
106
+ <html lang="zh"><head><meta charset="utf-8">
107
+ <title>知识库 2D({len(nodes)} 个概念)</title>
108
+ <style>
109
+ html,body {{ margin:0; height:100%; background:#05070e; color:#c9d3e6;
110
+ font:13px/1.5 -apple-system,"Segoe UI","Microsoft YaHei",system-ui,sans-serif;
111
+ overflow:hidden; user-select:none; }}
112
+ /* canvas 是**替换元素**:`inset:0` 不会让它铺满(right/bottom 被忽略,它保持固有的 300x150),
113
+ 所以必须显式给 width/height。少了这一条,视图按 300x150 算,整张图画在左上角一小块里。 */
114
+ #cv {{ position:fixed; inset:0; width:100%; height:100%; display:block; cursor:grab; touch-action:none; }}
115
+ #hud {{ position:fixed; left:14px; top:12px; pointer-events:none; max-width:62vw; }}
116
+ #stats {{ color:#6d7a93; }}
117
+ #info {{ margin-top:6px; font-size:15px; min-height:22px; }}
118
+ #q {{ position:fixed; right:14px; top:12px; width:220px; padding:6px 10px;
119
+ background:#121826; color:#c9d3e6; border:1px solid #26304a; border-radius:8px; outline:none; }}
120
+ #q:focus {{ border-color:#3f6ad8; }}
121
+ #tag {{ position:fixed; right:14px; bottom:10px; color:#4d5a72; text-align:right; }}
122
+ #boot {{ position:fixed; inset:0; display:flex; align-items:center; justify-content:center;
123
+ background:#05070e; color:#6d7a93; }}
124
+ </style></head>
125
+ <body>
126
+ <canvas id="cv"></canvas>
127
+ <div id="hud"><div id="stats"></div><div id="info">点一个概念看它连着谁 · 点一条边看它凭什么连着 · 拖动平移 · 滚轮缩放</div></div>
128
+ <input id="q" placeholder="搜概念,回车跳过去">
129
+ <div id="tag">2D · 坐标构建期算好 · 双击空白复位</div>
130
+ <div id="boot">正在画…</div>
131
+ <script>window.__DATA__ = {html_data};</script>
132
+ <script>window.__POS__ = "{pos}";</script>
133
+ <script>{viewer}</script>
134
+ </body></html>
135
+ """
136
+ out = Path(args.out)
137
+ out.parent.mkdir(parents=True, exist_ok=True)
138
+ out.write_text(page, encoding='utf-8')
139
+ mb = out.stat().st_size / 1048576
140
+ if args.json:
141
+ print(json.dumps({'success': True, 'action': 'graph-2d', 'dims': 2,
142
+ 'nodes': len(nodes), 'links': len(links), 'out': str(out),
143
+ 'vault': str(vault), 'mb': round(mb, 2),
144
+ 'minDegree': args.min_degree, 'line': args.line,
145
+ 'evidenceEdges': len(evidence),
146
+ 'selfContained': True, 'readsLocalData': True, 'invokesAI': False,
147
+ 'sendsNothing': True}, ensure_ascii=False))
148
+ else:
149
+ print('已生成 %s' % out)
150
+ print(' 概念 %d / 链接 %d / 带共同来源证据的边 %d / 文件 %.1f MB · 2D'
151
+ % (len(nodes), len(links), len(evidence), mb))
152
+ return 0
153
+
154
+
155
+ if __name__ == '__main__':
156
+ sys.exit(main())