@hupan56/wlkj 2.7.12 → 3.1.2

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 (253) hide show
  1. package/bin/cli.js +1435 -1107
  2. package/package.json +29 -29
  3. package/templates/qoder/agents/insight-planning.md +1 -1
  4. package/templates/qoder/agents/insight-research.md +28 -15
  5. package/templates/qoder/commands/optional/wl-insight.md +161 -161
  6. package/templates/qoder/commands/optional/wl-report.md +20 -39
  7. package/templates/qoder/commands/optional/wl-status.md +4 -4
  8. package/templates/qoder/commands/wl-code.md +2 -2
  9. package/templates/qoder/commands/wl-design.md +6 -6
  10. package/templates/qoder/commands/wl-init.md +3 -3
  11. package/templates/qoder/commands/wl-prd.md +26 -13
  12. package/templates/qoder/commands/wl-req.md +43 -0
  13. package/templates/qoder/commands/wl-search.md +78 -47
  14. package/templates/qoder/commands/wl-task.md +343 -40
  15. package/templates/qoder/commands/wl-test.md +43 -17
  16. package/templates/qoder/config.yaml +52 -14
  17. package/templates/qoder/hooks/post-tool-use.py +181 -0
  18. package/templates/qoder/hooks/session-start.py +94 -23
  19. package/templates/qoder/hooks/stop-eval.py +207 -0
  20. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  21. package/templates/qoder/rules/wl-pipeline.md +85 -49
  22. package/templates/qoder/scripts/README.md +102 -0
  23. package/templates/qoder/scripts/capability/__init__.py +26 -0
  24. package/templates/qoder/scripts/capability/__main__.py +72 -0
  25. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  26. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  27. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  28. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  29. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  30. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  31. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  32. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  33. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  34. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  35. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  36. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  37. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  38. package/templates/qoder/scripts/capability/chain.py +92 -0
  39. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  40. package/templates/qoder/scripts/capability/registry.py +246 -0
  41. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  42. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  43. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  44. package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
  45. package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
  46. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  47. package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
  48. package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
  49. package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
  50. package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
  51. package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
  52. package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
  53. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  54. package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
  55. package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
  56. package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
  57. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  58. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
  59. package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
  60. package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
  61. package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
  62. package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
  63. package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
  64. package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
  65. package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
  66. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  68. package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
  69. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  70. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  71. package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
  72. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  73. package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
  74. package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
  75. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  76. package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
  77. package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
  78. package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
  79. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  80. package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
  81. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  82. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  83. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  84. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  85. package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
  86. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  87. package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
  88. package/templates/qoder/scripts/domain/report/__init__.py +11 -0
  89. package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
  90. package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
  91. package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
  92. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  93. package/templates/qoder/scripts/domain/report/role.py +39 -0
  94. package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
  95. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  96. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  97. package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
  98. package/templates/qoder/scripts/domain/requirement/req.py +228 -0
  99. package/templates/qoder/scripts/domain/task/__init__.py +11 -0
  100. package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
  101. package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
  102. package/templates/qoder/scripts/domain/task/task.py +229 -0
  103. package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
  104. package/templates/qoder/scripts/domain/task/task_query.py +162 -0
  105. package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
  106. package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
  107. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  108. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  109. package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
  110. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  111. package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
  112. package/templates/qoder/scripts/foundation/core/config.py +187 -0
  113. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
  114. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  115. package/templates/qoder/scripts/foundation/data/contract.py +317 -0
  116. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  117. package/templates/qoder/scripts/foundation/data/result.py +223 -0
  118. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
  119. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  120. package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
  121. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
  122. package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
  123. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
  124. package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
  125. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  126. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  127. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
  128. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  129. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  130. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  131. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  132. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  133. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  134. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  135. package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
  136. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  137. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  138. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  139. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  140. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  141. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  142. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  143. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  144. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  145. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  146. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  147. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  148. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  153. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  154. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  155. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  156. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  157. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
  158. package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
  159. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  160. package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
  161. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  162. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  163. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  164. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  165. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  166. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  167. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  168. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  169. package/templates/qoder/scripts/validation/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  171. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  172. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  173. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  174. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  175. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  176. package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
  177. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  178. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  179. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  180. package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
  181. package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
  182. package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
  183. package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
  184. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  185. package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
  186. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  187. package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
  188. package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
  189. package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
  190. package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
  191. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  192. package/templates/qoder/settings.json +23 -2
  193. package/templates/qoder/skills/design-import/SKILL.md +235 -226
  194. package/templates/qoder/skills/design-review/SKILL.md +91 -82
  195. package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
  196. package/templates/qoder/skills/prd-review/SKILL.md +8 -6
  197. package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
  198. package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
  199. package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
  200. package/templates/qoder/skills/test-generator/SKILL.md +9 -7
  201. package/templates/qoder/skills/wl-code/SKILL.md +25 -13
  202. package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
  203. package/templates/qoder/skills/wl-design/SKILL.md +7 -5
  204. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  205. package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
  206. package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
  207. package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
  208. package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
  209. package/templates/qoder/skills/wl-report/SKILL.md +27 -25
  210. package/templates/qoder/skills/wl-search/SKILL.md +151 -80
  211. package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
  212. package/templates/qoder/skills/wl-status/SKILL.md +50 -18
  213. package/templates/qoder/skills/wl-task/SKILL.md +94 -12
  214. package/templates/qoder/skills/wl-test/SKILL.md +154 -45
  215. package/templates/qoder/templates/prd-full-template.md +7 -0
  216. package/templates/root/AGENTS.md +275 -237
  217. package/templates/root/requirements.txt +3 -0
  218. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  219. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  220. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  221. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  222. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  223. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  224. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  225. package/templates/qoder/scripts/check_carriers.py +0 -238
  226. package/templates/qoder/scripts/check_mcp.py +0 -298
  227. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  228. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  229. package/templates/qoder/scripts/collect_prds.py +0 -31
  230. package/templates/qoder/scripts/common/mentions.py +0 -134
  231. package/templates/qoder/scripts/common/utf8.py +0 -38
  232. package/templates/qoder/scripts/extract_api_params.py +0 -246
  233. package/templates/qoder/scripts/extract_routes.py +0 -54
  234. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  235. package/templates/qoder/scripts/handoff.py +0 -22
  236. package/templates/qoder/scripts/init_developer.py +0 -76
  237. package/templates/qoder/scripts/kg_mcp_server.py +0 -801
  238. package/templates/qoder/scripts/kg_semantic.py +0 -150
  239. package/templates/qoder/scripts/mcp_launcher.py +0 -414
  240. package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
  241. package/templates/qoder/scripts/parse_prds.py +0 -33
  242. package/templates/qoder/scripts/role.py +0 -51
  243. package/templates/qoder/scripts/sync_carriers.py +0 -259
  244. package/templates/qoder/scripts/task.py +0 -1261
  245. package/templates/qoder/scripts/workspace_init.py +0 -102
  246. package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
  247. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  248. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  249. /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
  250. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  251. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  252. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  253. /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
@@ -1,327 +1,338 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
- """kg_build_db.py - MySQL 数据库 schema 导入 kg.duckdb (数据层图谱)。
4
-
5
- kg_build.py 对称:
6
- kg_build.py: 扫 data/code/* → kg.duckdb (代码图谱: entities/fields/...)
7
- kg_build_db.py: MySQL schema → kg.duckdb (数据图谱: DB_TABLE/db_columns/db_table_fields)
8
-
9
- 产出 3 张新表 (复用现有 kg.duckdb, 不另建文件):
10
- - db_tables: 表级实体 (db, table, table_comment, entity_count)
11
- - db_columns: 列级真相 (db, table, column_name, data_type, is_nullable, column_comment)
12
- eval_prd.py A1 第二层锚定读这张表
13
- - db_table_fields: 表↔字段边 (table_id, column_name) 给图谱遍历用
14
-
15
- 同时在 entities 表插入 DB_TABLE 类型实体 (让图谱认识"数据库表"这个概念),
16
- 并在 edges 表连 DB_TABLE → 同名 ENDPOINT (若有), 做"改表影响哪些接口"的影响分析。
17
-
18
- 安全: 只读 MySQL (INFORMATION_SCHEMA), 不碰业务数据行。
19
-
20
- 增量策略 (库级签名对比, 不再全量重扫):
21
- - 每个库算签名 (表名列表 + 列总数), 存 build_meta。
22
- - 下次跑: 签名没变 = 该库 schema 没变 = 跳过。变了才重建该库。
23
- - MySQL update_time 准 (后者数据变更也会刷新, 不代表 schema 变)。
24
-
25
- 用法:
26
- python kg_build_db.py # 增量: 只重建有变化的库
27
- python kg_build_db.py --db fywl_ics_quality # 只扫指定库 (强制重建它)
28
- python kg_build_db.py --rebuild # 全量重建 (清所有签名 + 数据)
29
- python kg_build_db.py --stats # 看统计
30
- """
31
- import os
32
- import sys
33
- import csv
34
- import socket
35
- import tempfile
36
- import time
37
-
38
- # 顶部 import (避免函数内延迟 import 触发 GIL 冲突: pymysql 的 C 扩展
39
- # duckdb 在同进程内延迟加载时线程状态会打架)
40
- import duckdb
41
- import pymysql
42
-
43
- SCRIPTS = os.path.dirname(os.path.abspath(__file__))
44
- sys.path.insert(0, SCRIPTS)
45
-
46
- from common.paths import DATA_INDEX_DIR
47
-
48
- # MySQL 配置 (复用 mysql_mcp_server 的环境变量)
49
- MYSQL_HOST = os.environ.get('MYSQL_HOST', '10.54.6.9')
50
- MYSQL_PORT = int(os.environ.get('MYSQL_PORT', '3306'))
51
- MYSQL_USER = os.environ.get('MYSQL_USER', 'fywl_ics_test')
52
- MYSQL_PASSWORD = os.environ.get('MYSQL_PASSWORD', 'Fywl_ics_test56476')
53
- DB_PATH = str(DATA_INDEX_DIR / 'kg_db.duckdb') # 数据层图谱独立文件 (不和 kg.duckdb 抢写锁)
54
-
55
- # 只扫业务库 (跳过 information_schema/mysql/sys/performance_schema)
56
- DB_NAME_PREFIX = 'fywl_ics_'
57
-
58
-
59
- def check_intranet():
60
- """TCP 连通性校验 ( mysql_mcp_server)。"""
61
- try:
62
- with socket.create_connection((MYSQL_HOST, MYSQL_PORT), timeout=3):
63
- return True
64
- except (socket.timeout, OSError):
65
- return False
66
-
67
-
68
- def _connect_mysql():
69
- return pymysql.connect(
70
- host=MYSQL_HOST, port=MYSQL_PORT,
71
- user=MYSQL_USER, password=MYSQL_PASSWORD,
72
- charset='utf8mb4',
73
- cursorclass=pymysql.cursors.DictCursor,
74
- connect_timeout=5, read_timeout=30,
75
- )
76
-
77
-
78
- def _discover_databases(cur):
79
- """列出所有 fywl_ics_* 业务库。"""
80
- cur.execute(
81
- "SELECT schema_name FROM information_schema.schemata "
82
- "WHERE schema_name LIKE %s ORDER BY schema_name",
83
- (DB_NAME_PREFIX + '%',)
84
- )
85
- return [r['schema_name'] for r in cur.fetchall()]
86
-
87
-
88
- def _fetch_tables(cur, db):
89
- """某库的所有表 + 注释。"""
90
- cur.execute(
91
- "SELECT table_name, table_comment "
92
- "FROM information_schema.tables "
93
- "WHERE table_schema = %s AND table_type = 'BASE TABLE' "
94
- "ORDER BY table_name",
95
- (db,)
96
- )
97
- return cur.fetchall()
98
-
99
-
100
- def _fetch_columns(cur, db):
101
- """某库所有表的所有列。"""
102
- cur.execute(
103
- "SELECT table_name, column_name, data_type, column_type, "
104
- "is_nullable, column_key, column_comment "
105
- "FROM information_schema.columns "
106
- "WHERE table_schema = %s ORDER BY table_name, ordinal_position",
107
- (db,)
108
- )
109
- return cur.fetchall()
110
-
111
-
112
- def _init_db_tables(con):
113
- """建数据层表 (IF NOT EXISTS)。独立文件, 自带 build_meta。"""
114
- con.execute("""CREATE TABLE IF NOT EXISTS db_tables (
115
- db TEXT, table_name TEXT, table_comment TEXT, column_count INTEGER,
116
- PRIMARY KEY (db, table_name)
117
- )""")
118
- con.execute("""CREATE TABLE IF NOT EXISTS db_columns (
119
- db TEXT, table_name TEXT, column_name TEXT, data_type TEXT,
120
- column_type TEXT, is_nullable TEXT, column_key TEXT, column_comment TEXT
121
- )""")
122
- con.execute("""CREATE TABLE IF NOT EXISTS db_table_fields (
123
- table_id TEXT, column_name TEXT
124
- )""")
125
- con.execute("CREATE TABLE IF NOT EXISTS build_meta(key TEXT PRIMARY KEY, value TEXT)")
126
- con.execute("CREATE INDEX IF NOT EXISTS idx_dbcol_name ON db_columns(column_name)")
127
- con.execute("CREATE INDEX IF NOT EXISTS idx_dbcol_table ON db_columns(table_name)")
128
-
129
-
130
- def _bulk_insert(con, table, columns, rows):
131
- """批量插入: JSONL duckdb COPY (万行级 0.01s)。
132
- executemany 在 DuckDB 1.4 有性能 bug (每行 ~22ms), JSONL COPY 快 5000 倍。
133
- JSON 天然处理逗号/引号/换行符, CSV 转义问题。
134
- rows: list of tuples, 顺序与 columns 对应。"""
135
- if not rows:
136
- return
137
- import json as _json
138
- tmp = tempfile.NamedTemporaryFile(suffix='.jsonl', delete=False,
139
- mode='w', encoding='utf-8')
140
- for r in rows:
141
- obj = {columns[i]: ('' if r[i] is None else str(r[i]))
142
- for i in range(len(columns))}
143
- tmp.write(_json.dumps(obj, ensure_ascii=False) + '\n')
144
- tmp.close()
145
- try:
146
- sql = "COPY %s FROM '%s' (FORMAT JSON)" % (
147
- table, tmp.name.replace('\\', '/'))
148
- con.execute(sql)
149
- finally:
150
- os.unlink(tmp.name)
151
-
152
-
153
- def _db_signature(mcur, db):
154
- """算某库的 schema 签名 (表名列表 + 列总数)
155
- 用于增量: 签名没变 = 该库 schema 没变, 跳过。变了才重建该库。
156
- 比 update_time 准 (后者数据变更也会刷新)。"""
157
- mcur.execute(
158
- "SELECT table_name FROM information_schema.tables "
159
- "WHERE table_schema=%s AND table_type='BASE TABLE' ORDER BY table_name",
160
- (db,)
161
- )
162
- tables = sorted(r['table_name'] for r in mcur.fetchall())
163
- mcur.execute(
164
- "SELECT COUNT(*) c FROM information_schema.columns WHERE table_schema=%s",
165
- (db,)
166
- )
167
- col_cnt = mcur.fetchone()['c']
168
- return '%d:%s' % (col_cnt, ','.join(tables))
169
-
170
-
171
- def _load_sig(con, db):
172
- """读上次该库的签名。无记录返回 None。"""
173
- row = con.execute(
174
- "SELECT value FROM build_meta WHERE key=?", ['db_sig:' + db]
175
- ).fetchone()
176
- return row[0] if row else None
177
-
178
-
179
- def build(dbs=None, rebuild=False):
180
- """扫 MySQL schema → 写 kg_db.duckdb。
181
- 增量策略: 库级签名对比, 只重建 schema 有变化的库。
182
- rebuild=True: 忽略签名, 全量重建。
183
- """
184
- if not check_intranet():
185
- print('⚠️ 无法连接内网 MySQL (%s:%s)。请连接公司内网后重试。' % (MYSQL_HOST, MYSQL_PORT))
186
- return 0, 0
187
-
188
- mcon = _connect_mysql()
189
- try:
190
- mcur = mcon.cursor()
191
- all_dbs = dbs or _discover_databases(mcur)
192
-
193
- # 预读 duckdb 现有签名 (增量对比用)
194
- con = duckdb.connect(DB_PATH)
195
- _init_db_tables(con)
196
- if rebuild:
197
- con.execute("DELETE FROM db_tables")
198
- con.execute("DELETE FROM db_columns")
199
- con.execute("DELETE FROM db_table_fields")
200
- con.execute("DELETE FROM build_meta WHERE key LIKE 'db_sig:%'")
201
-
202
- changed_dbs = []
203
- skipped_dbs = []
204
- for db in all_dbs:
205
- sig = _db_signature(mcur, db)
206
- old_sig = _load_sig(con, db)
207
- if not rebuild and old_sig == sig:
208
- skipped_dbs.append(db)
209
- continue
210
- changed_dbs.append((db, sig))
211
-
212
- con.close() # 释放, 后面重新开写
213
-
214
- if skipped_dbs:
215
- print('跳过 %d 个未变库: %s' % (len(skipped_dbs), ', '.join(skipped_dbs)))
216
- if not changed_dbs:
217
- print('✓ 全部库 schema 未变化, 无需重建。')
218
- return 0, 0
219
- print('重建 %d 个有变化的库: %s'
220
- % (len(changed_dbs), ', '.join(d for d, _ in changed_dbs)))
221
-
222
- all_tables = []
223
- all_columns = []
224
- for db, sig in changed_dbs:
225
- tables = _fetch_tables(mcur, db)
226
- cols = _fetch_columns(mcur, db)
227
- col_count = {}
228
- for c in cols:
229
- t = c['table_name']
230
- col_count[t] = col_count.get(t, 0) + 1
231
- for t in tables:
232
- tn = t['table_name']
233
- all_tables.append((db, tn, t.get('table_comment', '') or '',
234
- col_count.get(tn, 0)))
235
- for c in cols:
236
- all_columns.append((
237
- db, c['table_name'], c['column_name'], c['data_type'],
238
- c['column_type'], c['is_nullable'], c['column_key'],
239
- c.get('column_comment', '') or '',
240
- ))
241
- print(' %s: %d 表, %d 列' % (db, len(tables), len(cols)))
242
- finally:
243
- mcon.close()
244
-
245
- # 写 kg_db.duckdb (只写变化的库的数据)
246
- con = duckdb.connect(DB_PATH)
247
- try:
248
- _init_db_tables(con)
249
- # 删掉变化库的旧数据 (库级增量: 只删该库的, 不动其它库)
250
- for db, _ in changed_dbs:
251
- con.execute("DELETE FROM db_tables WHERE db=?", [db])
252
- con.execute("DELETE FROM db_columns WHERE db=?", [db])
253
- con.execute("DELETE FROM db_table_fields WHERE table_id LIKE ?", ['dbt:%s:%%' % db])
254
-
255
- if all_tables:
256
- _bulk_insert(con, 'db_tables',
257
- ['db', 'table_name', 'table_comment', 'column_count'],
258
- all_tables)
259
- if all_columns:
260
- _bulk_insert(con, 'db_columns',
261
- ['db', 'table_name', 'column_name', 'data_type',
262
- 'column_type', 'is_nullable', 'column_key', 'column_comment'],
263
- all_columns)
264
- tf_rows = [('dbt:%s:%s' % (row[0], row[1]), row[2])
265
- for row in all_columns]
266
- _bulk_insert(con, 'db_table_fields', ['table_id', 'column_name'], tf_rows)
267
-
268
- # 记录每个变化库的新签名 + 总构建时间
269
- for db, sig in changed_dbs:
270
- con.execute(
271
- "INSERT OR REPLACE INTO build_meta(key, value) VALUES (?,?)",
272
- ['db_sig:' + db, sig]
273
- )
274
- con.execute(
275
- "INSERT OR REPLACE INTO build_meta(key, value) VALUES (?,?)",
276
- ['db_schema_last_build', str(time.time())]
277
- )
278
- print('\n增量导入完成: 重建 %d 库, %d 表, %d 列 → %s'
279
- % (len(changed_dbs), len(all_tables), len(all_columns), os.path.basename(DB_PATH)))
280
- return len(all_tables), len(all_columns)
281
- finally:
282
- con.close()
283
-
284
-
285
- def query_table_columns(table):
286
- """查询某表的所有列 ( eval_prd 等用)。返回 {column_name_lower}。"""
287
- try:
288
- con = duckdb.connect(DB_PATH, read_only=True)
289
- rows = con.execute(
290
- "SELECT DISTINCT column_name FROM db_columns WHERE table_name = ?",
291
- [table]
292
- ).fetchall()
293
- con.close()
294
- return {r[0].lower() for r in rows if r[0]}
295
- except Exception:
296
- return set()
297
-
298
-
299
- def stats():
300
- """数据层图谱统计。"""
301
- try:
302
- con = duckdb.connect(DB_PATH, read_only=True)
303
- t = con.execute("SELECT COUNT(*) FROM db_tables").fetchone()[0]
304
- c = con.execute("SELECT COUNT(*) FROM db_columns").fetchone()[0]
305
- by_db = con.execute(
306
- "SELECT db, COUNT(*) FROM db_tables GROUP BY db ORDER BY COUNT(*) DESC"
307
- ).fetchall()
308
- con.close()
309
- print('数据层图谱 (%s): %d 表 / %d 列' % (os.path.basename(DB_PATH), t, c))
310
- for db, cnt in by_db:
311
- print(' %s: %d 表' % (db, cnt))
312
- except Exception as e:
313
- print('统计失败: %s' % e)
314
-
315
-
316
- if __name__ == '__main__':
317
- args = sys.argv[1:]
318
- rebuild = '--rebuild' in args
319
- db_filter = None
320
- for i, a in enumerate(args):
321
- if a == '--db' and i + 1 < len(args):
322
- db_filter = [args[i + 1]]
323
- if '--stats' in args:
324
- stats()
325
- else:
326
- build(dbs=db_filter, rebuild=rebuild)
327
- stats()
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _f = _o.path.abspath(__file__)
6
+ for _ in range(10):
7
+ _f = _o.path.dirname(_f)
8
+ _cp = _o.path.join(_f, 'foundation')
9
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
10
+ break
11
+ if _cp not in _s.path: _s.path.insert(0, _cp)
12
+ from bootstrap import setup; setup()
13
+
14
+ """kg_build_db.py - 把 MySQL 数据库 schema 导入 kg.duckdb (数据层图谱)。
15
+
16
+ kg_build.py 对称:
17
+ kg_build.py: 扫 data/code/* → kg.duckdb (代码图谱: entities/fields/...)
18
+ kg_build_db.py: MySQL schema → kg.duckdb (数据图谱: DB_TABLE/db_columns/db_table_fields)
19
+
20
+ 产出 3 张新表 (复用现有 kg.duckdb, 不另建文件):
21
+ - db_tables: 表级实体 (db, table, table_comment, entity_count)
22
+ - db_columns: 列级真相 (db, table, column_name, data_type, is_nullable, column_comment)
23
+ eval_prd.py A1 第二层锚定读这张表
24
+ - db_table_fields: 表↔字段边 (table_id, column_name) 给图谱遍历用
25
+
26
+ 同时在 entities 表插入 DB_TABLE 类型实体 (让图谱认识"数据库表"这个概念),
27
+ 并在 edges 表连 DB_TABLE 同名 ENDPOINT (若有), 做"改表影响哪些接口"的影响分析。
28
+
29
+ 安全: 只读 MySQL (INFORMATION_SCHEMA), 不碰业务数据行。
30
+
31
+ 增量策略 (库级签名对比, 不再全量重扫):
32
+ - 每个库算签名 (表名列表 + 列总数), 存 build_meta。
33
+ - 下次跑: 签名没变 = 该库 schema 没变 = 跳过。变了才重建该库。
34
+ - 比 MySQL update_time 准 (后者数据变更也会刷新, 不代表 schema 变)。
35
+
36
+ 用法:
37
+ python kg_build_db.py # 增量: 只重建有变化的库
38
+ python kg_build_db.py --db fywl_ics_quality # 只扫指定库 (强制重建它)
39
+ python kg_build_db.py --rebuild # 全量重建 (清所有签名 + 数据)
40
+ python kg_build_db.py --stats # 看统计
41
+ """
42
+ import os
43
+ import sys
44
+ import csv
45
+ import socket
46
+ import tempfile
47
+ import time
48
+
49
+ # 顶部 import (避免函数内延迟 import 触发 GIL 冲突: pymysql 的 C 扩展
50
+ # duckdb 在同进程内延迟加载时线程状态会打架)
51
+ import duckdb
52
+ import pymysql
53
+
54
+ SCRIPTS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts/根
55
+ sys.path.insert(0, SCRIPTS)
56
+
57
+ from foundation.core.paths import DATA_INDEX_DIR
58
+
59
+ # MySQL 配置 (复用 mysql_mcp_server 的环境变量)
60
+ MYSQL_HOST = os.environ.get('MYSQL_HOST', '10.54.6.9')
61
+ MYSQL_PORT = int(os.environ.get('MYSQL_PORT', '3306'))
62
+ MYSQL_USER = os.environ.get('MYSQL_USER', 'fywl_ics_test')
63
+ MYSQL_PASSWORD = os.environ.get('MYSQL_PASSWORD', 'Fywl_ics_test56476')
64
+ DB_PATH = str(DATA_INDEX_DIR / 'kg_db.duckdb') # 数据层图谱独立文件 (不和 kg.duckdb 抢写锁)
65
+
66
+ # 只扫业务库 (跳过 information_schema/mysql/sys/performance_schema)
67
+ DB_NAME_PREFIX = 'fywl_ics_'
68
+
69
+
70
+ def check_intranet():
71
+ """TCP 连通性校验 (同 mysql_mcp_server)。"""
72
+ try:
73
+ with socket.create_connection((MYSQL_HOST, MYSQL_PORT), timeout=3):
74
+ return True
75
+ except (socket.timeout, OSError):
76
+ return False
77
+
78
+
79
+ def _connect_mysql():
80
+ return pymysql.connect(
81
+ host=MYSQL_HOST, port=MYSQL_PORT,
82
+ user=MYSQL_USER, password=MYSQL_PASSWORD,
83
+ charset='utf8mb4',
84
+ cursorclass=pymysql.cursors.DictCursor,
85
+ connect_timeout=5, read_timeout=30,
86
+ )
87
+
88
+
89
+ def _discover_databases(cur):
90
+ """列出所有 fywl_ics_* 业务库。"""
91
+ cur.execute(
92
+ "SELECT schema_name FROM information_schema.schemata "
93
+ "WHERE schema_name LIKE %s ORDER BY schema_name",
94
+ (DB_NAME_PREFIX + '%',)
95
+ )
96
+ return [r['schema_name'] for r in cur.fetchall()]
97
+
98
+
99
+ def _fetch_tables(cur, db):
100
+ """某库的所有表 + 注释。"""
101
+ cur.execute(
102
+ "SELECT table_name, table_comment "
103
+ "FROM information_schema.tables "
104
+ "WHERE table_schema = %s AND table_type = 'BASE TABLE' "
105
+ "ORDER BY table_name",
106
+ (db,)
107
+ )
108
+ return cur.fetchall()
109
+
110
+
111
+ def _fetch_columns(cur, db):
112
+ """某库所有表的所有列。"""
113
+ cur.execute(
114
+ "SELECT table_name, column_name, data_type, column_type, "
115
+ "is_nullable, column_key, column_comment "
116
+ "FROM information_schema.columns "
117
+ "WHERE table_schema = %s ORDER BY table_name, ordinal_position",
118
+ (db,)
119
+ )
120
+ return cur.fetchall()
121
+
122
+
123
+ def _init_db_tables(con):
124
+ """建数据层表 (IF NOT EXISTS)。独立文件, 自带 build_meta。"""
125
+ con.execute("""CREATE TABLE IF NOT EXISTS db_tables (
126
+ db TEXT, table_name TEXT, table_comment TEXT, column_count INTEGER,
127
+ PRIMARY KEY (db, table_name)
128
+ )""")
129
+ con.execute("""CREATE TABLE IF NOT EXISTS db_columns (
130
+ db TEXT, table_name TEXT, column_name TEXT, data_type TEXT,
131
+ column_type TEXT, is_nullable TEXT, column_key TEXT, column_comment TEXT
132
+ )""")
133
+ con.execute("""CREATE TABLE IF NOT EXISTS db_table_fields (
134
+ table_id TEXT, column_name TEXT
135
+ )""")
136
+ con.execute("CREATE TABLE IF NOT EXISTS build_meta(key TEXT PRIMARY KEY, value TEXT)")
137
+ con.execute("CREATE INDEX IF NOT EXISTS idx_dbcol_name ON db_columns(column_name)")
138
+ con.execute("CREATE INDEX IF NOT EXISTS idx_dbcol_table ON db_columns(table_name)")
139
+
140
+
141
+ def _bulk_insert(con, table, columns, rows):
142
+ """批量插入: JSONL duckdb COPY (万行级 0.01s)
143
+ executemany 在 DuckDB 1.4 有性能 bug (每行 ~22ms), JSONL COPY 快 5000 倍。
144
+ JSON 天然处理逗号/引号/换行符, 无 CSV 转义问题。
145
+ rows: list of tuples, 顺序与 columns 对应。"""
146
+ if not rows:
147
+ return
148
+ import json as _json
149
+ tmp = tempfile.NamedTemporaryFile(suffix='.jsonl', delete=False,
150
+ mode='w', encoding='utf-8')
151
+ for r in rows:
152
+ obj = {columns[i]: ('' if r[i] is None else str(r[i]))
153
+ for i in range(len(columns))}
154
+ tmp.write(_json.dumps(obj, ensure_ascii=False) + '\n')
155
+ tmp.close()
156
+ try:
157
+ sql = "COPY %s FROM '%s' (FORMAT JSON)" % (
158
+ table, tmp.name.replace('\\', '/'))
159
+ con.execute(sql)
160
+ finally:
161
+ os.unlink(tmp.name)
162
+
163
+
164
+ def _db_signature(mcur, db):
165
+ """算某库的 schema 签名 (表名列表 + 列总数)
166
+ 用于增量: 签名没变 = 该库 schema 没变, 跳过。变了才重建该库。
167
+ update_time (后者数据变更也会刷新)。"""
168
+ mcur.execute(
169
+ "SELECT table_name FROM information_schema.tables "
170
+ "WHERE table_schema=%s AND table_type='BASE TABLE' ORDER BY table_name",
171
+ (db,)
172
+ )
173
+ tables = sorted(r['table_name'] for r in mcur.fetchall())
174
+ mcur.execute(
175
+ "SELECT COUNT(*) c FROM information_schema.columns WHERE table_schema=%s",
176
+ (db,)
177
+ )
178
+ col_cnt = mcur.fetchone()['c']
179
+ return '%d:%s' % (col_cnt, ','.join(tables))
180
+
181
+
182
+ def _load_sig(con, db):
183
+ """读上次该库的签名。无记录返回 None。"""
184
+ row = con.execute(
185
+ "SELECT value FROM build_meta WHERE key=?", ['db_sig:' + db]
186
+ ).fetchone()
187
+ return row[0] if row else None
188
+
189
+
190
+ def build(dbs=None, rebuild=False):
191
+ """扫 MySQL schema 写 kg_db.duckdb。
192
+ 增量策略: 库级签名对比, 只重建 schema 有变化的库。
193
+ rebuild=True: 忽略签名, 全量重建。
194
+ """
195
+ if not check_intranet():
196
+ print('⚠️ 无法连接内网 MySQL (%s:%s)。请连接公司内网后重试。' % (MYSQL_HOST, MYSQL_PORT))
197
+ return 0, 0
198
+
199
+ mcon = _connect_mysql()
200
+ try:
201
+ mcur = mcon.cursor()
202
+ all_dbs = dbs or _discover_databases(mcur)
203
+
204
+ # 预读 duckdb 现有签名 (增量对比用)
205
+ con = duckdb.connect(DB_PATH)
206
+ _init_db_tables(con)
207
+ if rebuild:
208
+ con.execute("DELETE FROM db_tables")
209
+ con.execute("DELETE FROM db_columns")
210
+ con.execute("DELETE FROM db_table_fields")
211
+ con.execute("DELETE FROM build_meta WHERE key LIKE 'db_sig:%'")
212
+
213
+ changed_dbs = []
214
+ skipped_dbs = []
215
+ for db in all_dbs:
216
+ sig = _db_signature(mcur, db)
217
+ old_sig = _load_sig(con, db)
218
+ if not rebuild and old_sig == sig:
219
+ skipped_dbs.append(db)
220
+ continue
221
+ changed_dbs.append((db, sig))
222
+
223
+ con.close() # 释放, 后面重新开写
224
+
225
+ if skipped_dbs:
226
+ print('跳过 %d 个未变库: %s' % (len(skipped_dbs), ', '.join(skipped_dbs)))
227
+ if not changed_dbs:
228
+ print('✓ 全部库 schema 未变化, 无需重建。')
229
+ return 0, 0
230
+ print('重建 %d 个有变化的库: %s'
231
+ % (len(changed_dbs), ', '.join(d for d, _ in changed_dbs)))
232
+
233
+ all_tables = []
234
+ all_columns = []
235
+ for db, sig in changed_dbs:
236
+ tables = _fetch_tables(mcur, db)
237
+ cols = _fetch_columns(mcur, db)
238
+ col_count = {}
239
+ for c in cols:
240
+ t = c['table_name']
241
+ col_count[t] = col_count.get(t, 0) + 1
242
+ for t in tables:
243
+ tn = t['table_name']
244
+ all_tables.append((db, tn, t.get('table_comment', '') or '',
245
+ col_count.get(tn, 0)))
246
+ for c in cols:
247
+ all_columns.append((
248
+ db, c['table_name'], c['column_name'], c['data_type'],
249
+ c['column_type'], c['is_nullable'], c['column_key'],
250
+ c.get('column_comment', '') or '',
251
+ ))
252
+ print(' %s: %d 表, %d 列' % (db, len(tables), len(cols)))
253
+ finally:
254
+ mcon.close()
255
+
256
+ # 写 kg_db.duckdb (只写变化的库的数据)
257
+ con = duckdb.connect(DB_PATH)
258
+ try:
259
+ _init_db_tables(con)
260
+ # 删掉变化库的旧数据 (库级增量: 只删该库的, 不动其它库)
261
+ for db, _ in changed_dbs:
262
+ con.execute("DELETE FROM db_tables WHERE db=?", [db])
263
+ con.execute("DELETE FROM db_columns WHERE db=?", [db])
264
+ con.execute("DELETE FROM db_table_fields WHERE table_id LIKE ?", ['dbt:%s:%%' % db])
265
+
266
+ if all_tables:
267
+ _bulk_insert(con, 'db_tables',
268
+ ['db', 'table_name', 'table_comment', 'column_count'],
269
+ all_tables)
270
+ if all_columns:
271
+ _bulk_insert(con, 'db_columns',
272
+ ['db', 'table_name', 'column_name', 'data_type',
273
+ 'column_type', 'is_nullable', 'column_key', 'column_comment'],
274
+ all_columns)
275
+ tf_rows = [('dbt:%s:%s' % (row[0], row[1]), row[2])
276
+ for row in all_columns]
277
+ _bulk_insert(con, 'db_table_fields', ['table_id', 'column_name'], tf_rows)
278
+
279
+ # 记录每个变化库的新签名 + 总构建时间
280
+ for db, sig in changed_dbs:
281
+ con.execute(
282
+ "INSERT OR REPLACE INTO build_meta(key, value) VALUES (?,?)",
283
+ ['db_sig:' + db, sig]
284
+ )
285
+ con.execute(
286
+ "INSERT OR REPLACE INTO build_meta(key, value) VALUES (?,?)",
287
+ ['db_schema_last_build', str(time.time())]
288
+ )
289
+ print('\n增量导入完成: 重建 %d 库, %d 表, %d 列 → %s'
290
+ % (len(changed_dbs), len(all_tables), len(all_columns), os.path.basename(DB_PATH)))
291
+ return len(all_tables), len(all_columns)
292
+ finally:
293
+ con.close()
294
+
295
+
296
+ def query_table_columns(table):
297
+ """查询某表的所有列 (给 eval_prd 等用)。返回 {column_name_lower}。"""
298
+ try:
299
+ con = duckdb.connect(DB_PATH, read_only=True)
300
+ rows = con.execute(
301
+ "SELECT DISTINCT column_name FROM db_columns WHERE table_name = ?",
302
+ [table]
303
+ ).fetchall()
304
+ con.close()
305
+ return {r[0].lower() for r in rows if r[0]}
306
+ except Exception:
307
+ return set()
308
+
309
+
310
+ def stats():
311
+ """数据层图谱统计。"""
312
+ try:
313
+ con = duckdb.connect(DB_PATH, read_only=True)
314
+ t = con.execute("SELECT COUNT(*) FROM db_tables").fetchone()[0]
315
+ c = con.execute("SELECT COUNT(*) FROM db_columns").fetchone()[0]
316
+ by_db = con.execute(
317
+ "SELECT db, COUNT(*) FROM db_tables GROUP BY db ORDER BY COUNT(*) DESC"
318
+ ).fetchall()
319
+ con.close()
320
+ print('数据层图谱 (%s): %d / %d 列' % (os.path.basename(DB_PATH), t, c))
321
+ for db, cnt in by_db:
322
+ print(' %s: %d 表' % (db, cnt))
323
+ except Exception as e:
324
+ print('统计失败: %s' % e)
325
+
326
+
327
+ if __name__ == '__main__':
328
+ args = sys.argv[1:]
329
+ rebuild = '--rebuild' in args
330
+ db_filter = None
331
+ for i, a in enumerate(args):
332
+ if a == '--db' and i + 1 < len(args):
333
+ db_filter = [args[i + 1]]
334
+ if '--stats' in args:
335
+ stats()
336
+ else:
337
+ build(dbs=db_filter, rebuild=rebuild)
338
+ stats()