@hupan56/wlkj 2.7.12 → 3.0.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 (152) hide show
  1. package/bin/cli.js +344 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +113 -4
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -1,224 +1,230 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
- """kg_link_db.py - 扫代码 @TableName 注解, Entity 类 ↔ 数据库表 的语义桥。
4
-
5
- 解决"数据层孤岛"问题: kg_db.duckdb 有表结构, 但不知道对应代码里的哪个 Entity。
6
- 本脚本扫 Java 源码里的 @TableName("xxx") 注解, 提取:
7
- Entity 类名 数据库表名 (关联到) db_columns 的真实列
8
-
9
- 调研依据 (FalkorDB "结构遍历 > 语义匹配"):
10
- - 不靠表名子串猜 endpoint (验证过 0 命中, 不准)
11
- - 靠代码显式声明的关系 (@TableName 注解) 建边, 这才准
12
-
13
- 产出表 (写进 kg_db.duckdb, 不动 kg.duckdb 写锁):
14
- entity_table_map:
15
- entity_class TEXT -- QualityCaseBo (代码层类名)
16
- table_name TEXT -- quality_case (数据库表名, 关联 db_tables)
17
- source_file TEXT -- 哪个 .java 读到的
18
- annotation TEXT -- @TableName("quality_case") 原文
19
-
20
- 建好后可遍历:
21
- 数据库表 entity_table_map → Entity 类 → (kg.duckdb) Controller/接口
22
-
23
- 用法:
24
- python kg_link_db.py # 扫 data/code/ 全量
25
- python kg_link_db.py --rebuild # 清旧重建
26
- python kg_link_db.py --stats # 看统计
27
- """
28
- import os
29
- import re
30
- import sys
31
- import time
32
-
33
- SCRIPTS = os.path.dirname(os.path.abspath(__file__))
34
- sys.path.insert(0, SCRIPTS)
35
-
36
- import duckdb
37
- from common.paths import DATA_INDEX_DIR, PROJECT_ROOT
38
-
39
- CODE_DIR = str(PROJECT_ROOT / 'data' / 'code')
40
- DB_PATH = str(DATA_INDEX_DIR / 'kg_db.duckdb')
41
-
42
- # @TableName 的两种格式:
43
- # @TableName("quality_case")
44
- # @TableName(value = "quality_case")
45
- RE_TABLENAME = re.compile(
46
- r'@TableName\s*\(\s*(?:value\s*=\s*)?"([^"]+)"',
47
- re.IGNORECASE
48
- )
49
- # 紧跟注解的 class 声明: class QualityCaseBo
50
- RE_CLASS = re.compile(r'class\s+(\w+)')
51
-
52
-
53
- def _normalize_table_name(raw):
54
- """归一化表名, 提升和数据库真实表的匹配率。
55
- 处理注解写法不规范的情况:
56
- fywl_ics_cloud.sys_dept sys_dept (去库名前缀)
57
- empContractImport → emp_contract_import (camelCase→snake_case)
58
- quality_case → quality_case (已是规范, 不变)
59
- """
60
- t = raw.strip().lower()
61
- # 去库名前缀 (db.table → table)
62
- if '.' in t:
63
- t = t.rsplit('.', 1)[-1]
64
- # 去常见前缀 t_/tb_/t (数据库表常加这些, 注解有时不加)
65
- # 注意: 不在这里去前缀, 因为 db_tables 里有的带 t_ 有的不带, 留原样让 JOIN 双向兜
66
- # camelCase → snake_case (Java 注解里有时写驼峰表名)
67
- if re.search(r'[a-z][A-Z]', t):
68
- t = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', t).lower()
69
- return t
70
-
71
-
72
- def _scan_java_files():
73
- """扫 data/code/ 下所有 .java, 返回 [(entity_class, table_name, normalized_table, source_file, annotation)]。"""
74
- mappings = []
75
- scanned = 0
76
- for root, dirs, files in os.walk(CODE_DIR):
77
- # 跳过非业务目录
78
- dirs[:] = [d for d in dirs if d not in (
79
- '.git', 'node_modules', '__pycache__', 'target', 'test')]
80
- for fn in files:
81
- if not fn.endswith('.java'):
82
- continue
83
- scanned += 1
84
- fpath = os.path.join(root, fn)
85
- try:
86
- for enc in ('utf-8', 'gbk'):
87
- try:
88
- with open(fpath, encoding=enc) as f:
89
- content = f.read()
90
- break
91
- except (UnicodeDecodeError, OSError):
92
- content = ''
93
- continue
94
- except OSError:
95
- continue
96
- if '@TableName' not in content:
97
- continue
98
- # 找所有 @TableName 注解
99
- for m in RE_TABLENAME.finditer(content):
100
- table_name = m.group(1)
101
- normalized = _normalize_table_name(table_name)
102
- annotation = content[m.start():m.end() + 1]
103
- # 找注解之后最近的 class 声明 (Entity 类名)
104
- after = content[m.end():m.end() + 500]
105
- cm = RE_CLASS.search(after)
106
- entity_class = cm.group(1) if cm else os.path.splitext(fn)[0]
107
- rel = os.path.relpath(fpath, PROJECT_ROOT).replace('\\', '/')
108
- mappings.append((entity_class, table_name, normalized, rel, annotation))
109
- return mappings, scanned
110
-
111
-
112
- def build(rebuild=False):
113
- """扫代码 @TableName entity_table_map。"""
114
- t0 = time.time()
115
- print('扫描 data/code/ 下的 @TableName 注解...')
116
- mappings, scanned = _scan_java_files()
117
- print(' 扫描 %d 个 .java 文件, 找到 %d 个 @TableName 映射 (%.1fs)'
118
- % (scanned, len(mappings), time.time() - t0))
119
-
120
- if not mappings:
121
- print('未找到任何 @TableName 注解。检查 data/code/ 是否有 Java 源码。')
122
- return 0
123
-
124
- con = duckdb.connect(DB_PATH)
125
- try:
126
- con.execute("""CREATE TABLE IF NOT EXISTS entity_table_map (
127
- entity_class TEXT,
128
- table_name TEXT,
129
- normalized_table TEXT,
130
- source_file TEXT,
131
- annotation TEXT
132
- )""")
133
- con.execute("CREATE INDEX IF NOT EXISTS idx_etm_table ON entity_table_map(table_name)")
134
- con.execute("CREATE INDEX IF NOT EXISTS idx_etm_norm ON entity_table_map(normalized_table)")
135
- con.execute("CREATE INDEX IF NOT EXISTS idx_etm_class ON entity_table_map(entity_class)")
136
-
137
- if rebuild:
138
- con.execute("DELETE FROM entity_table_map")
139
-
140
- # JSONL COPY 批量插入 (DuckDB 1.4 executemany 慢)
141
- import json as _json
142
- import tempfile
143
- tmp = tempfile.NamedTemporaryFile(suffix='.jsonl', delete=False,
144
- mode='w', encoding='utf-8')
145
- cols = ['entity_class', 'table_name', 'normalized_table', 'source_file', 'annotation']
146
- for row in mappings:
147
- obj = {cols[i]: (row[i] if row[i] is not None else '')
148
- for i in range(len(cols))}
149
- tmp.write(_json.dumps(obj, ensure_ascii=False) + '\n')
150
- tmp.close()
151
- try:
152
- con.execute("COPY entity_table_map FROM '%s' (FORMAT JSON)"
153
- % tmp.name.replace('\\', '/'))
154
- finally:
155
- os.unlink(tmp.name)
156
-
157
- con.execute("INSERT OR REPLACE INTO build_meta(key, value) VALUES (?,?)",
158
- ['entity_table_map_built', str(time.time())])
159
- print('写入 %d 条 Entity→表 映射到 entity_table_map' % len(mappings))
160
- return len(mappings)
161
- finally:
162
- con.close()
163
-
164
-
165
- def query_impact(table_name):
166
- """查某张表关联的 Entity 类 (给"改表影响分析"用)。"""
167
- con = duckdb.connect(DB_PATH, read_only=True)
168
- try:
169
- rows = con.execute(
170
- "SELECT DISTINCT entity_class, source_file FROM entity_table_map "
171
- "WHERE table_name = ?", [table_name]
172
- ).fetchall()
173
- return rows
174
- finally:
175
- con.close()
176
-
177
-
178
- def stats():
179
- """映射统计 + 和 db_tables/db_columns 的关联率。"""
180
- con = duckdb.connect(DB_PATH, read_only=True)
181
- try:
182
- total = con.execute("SELECT COUNT(*) FROM entity_table_map").fetchone()[0]
183
- classes = con.execute("SELECT COUNT(DISTINCT entity_class) FROM entity_table_map").fetchone()[0]
184
- tables_mapped = con.execute("SELECT COUNT(DISTINCT table_name) FROM entity_table_map").fetchone()[0]
185
- # 关联率: 映射的表名 vs 数据库真实表名 (原始 + 归一化)
186
- try:
187
- raw_matched = con.execute("""
188
- SELECT COUNT(DISTINCT m.table_name) FROM entity_table_map m
189
- JOIN db_tables t ON m.table_name = t.table_name
190
- """).fetchone()[0]
191
- norm_matched = con.execute("""
192
- SELECT COUNT(DISTINCT m.normalized_table) FROM entity_table_map m
193
- JOIN db_tables t ON m.normalized_table = t.table_name
194
- """).fetchone()[0]
195
- db_total = con.execute("SELECT COUNT(DISTINCT table_name) FROM db_tables").fetchone()[0]
196
- except Exception:
197
- raw_matched, norm_matched, db_total = 0, 0, 0
198
- print('entity_table_map 统计:')
199
- print(' 总映射: %d' % total)
200
- print(' Entity 类数: %d' % classes)
201
- print(' 映射的表名数: %d' % tables_mapped)
202
- if db_total:
203
- print(' 原始表名匹配: %d/%d (%.0f%%)'
204
- % (raw_matched, db_total, raw_matched / db_total * 100))
205
- print(' 归一化后匹配: %d/%d (%.0f%%)'
206
- % (norm_matched, db_total, norm_matched / db_total * 100))
207
- # 抽样
208
- sample = con.execute(
209
- "SELECT entity_class, table_name FROM entity_table_map LIMIT 8"
210
- ).fetchall()
211
- print(' 抽样:')
212
- for ec, tn in sample:
213
- print(' %s → %s' % (ec, tn))
214
- finally:
215
- con.close()
216
-
217
-
218
- if __name__ == '__main__':
219
- args = sys.argv[1:]
220
- if '--stats' in args:
221
- stats()
222
- else:
223
- build(rebuild='--rebuild' in args)
224
- 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
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
6
+ if _cp not in _s.path: _s.path.insert(0, _cp)
7
+ from bootstrap import setup; setup()
8
+
9
+ """kg_link_db.py - 扫代码 @TableName 注解, 建 Entity 类 ↔ 数据库表 的语义桥。
10
+
11
+ 解决"数据层孤岛"问题: kg_db.duckdb 有表结构, 但不知道对应代码里的哪个 Entity。
12
+ 本脚本扫 Java 源码里的 @TableName("xxx") 注解, 提取:
13
+ Entity 类名 数据库表名 (关联到) db_columns 的真实列
14
+
15
+ 调研依据 (FalkorDB "结构遍历 > 语义匹配"):
16
+ - 不靠表名子串猜 endpoint (验证过 0 命中, 不准)
17
+ - 靠代码显式声明的关系 (@TableName 注解) 建边, 这才准
18
+
19
+ 产出表 (写进 kg_db.duckdb, 不动 kg.duckdb 写锁):
20
+ entity_table_map:
21
+ entity_class TEXT -- QualityCaseBo (代码层类名)
22
+ table_name TEXT -- quality_case (数据库表名, 关联 db_tables)
23
+ source_file TEXT -- 哪个 .java 读到的
24
+ annotation TEXT -- @TableName("quality_case") 原文
25
+
26
+ 建好后可遍历:
27
+ 数据库表 → entity_table_map → Entity 类 → (kg.duckdb) Controller/接口
28
+
29
+ 用法:
30
+ python kg_link_db.py # 扫 data/code/ 全量
31
+ python kg_link_db.py --rebuild # 清旧重建
32
+ python kg_link_db.py --stats # 看统计
33
+ """
34
+ import os
35
+ import re
36
+ import sys
37
+ import time
38
+
39
+ SCRIPTS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts/根
40
+ sys.path.insert(0, SCRIPTS)
41
+
42
+ import duckdb
43
+ from common.paths import DATA_INDEX_DIR, PROJECT_ROOT
44
+
45
+ CODE_DIR = str(PROJECT_ROOT / 'data' / 'code')
46
+ DB_PATH = str(DATA_INDEX_DIR / 'kg_db.duckdb')
47
+
48
+ # @TableName 的两种格式:
49
+ # @TableName("quality_case")
50
+ # @TableName(value = "quality_case")
51
+ RE_TABLENAME = re.compile(
52
+ r'@TableName\s*\(\s*(?:value\s*=\s*)?"([^"]+)"',
53
+ re.IGNORECASE
54
+ )
55
+ # 紧跟注解的 class 声明: class QualityCaseBo
56
+ RE_CLASS = re.compile(r'class\s+(\w+)')
57
+
58
+
59
+ def _normalize_table_name(raw):
60
+ """归一化表名, 提升和数据库真实表的匹配率。
61
+ 处理注解写法不规范的情况:
62
+ fywl_ics_cloud.sys_dept sys_dept (去库名前缀)
63
+ empContractImport emp_contract_import (camelCase→snake_case)
64
+ quality_case quality_case (已是规范, 不变)
65
+ """
66
+ t = raw.strip().lower()
67
+ # 去库名前缀 (db.table → table)
68
+ if '.' in t:
69
+ t = t.rsplit('.', 1)[-1]
70
+ # 去常见前缀 t_/tb_/t (数据库表常加这些, 注解有时不加)
71
+ # 注意: 不在这里去前缀, 因为 db_tables 里有的带 t_ 有的不带, 留原样让 JOIN 双向兜
72
+ # camelCase → snake_case (Java 注解里有时写驼峰表名)
73
+ if re.search(r'[a-z][A-Z]', t):
74
+ t = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', t).lower()
75
+ return t
76
+
77
+
78
+ def _scan_java_files():
79
+ """扫 data/code/ 下所有 .java, 返回 [(entity_class, table_name, normalized_table, source_file, annotation)]。"""
80
+ mappings = []
81
+ scanned = 0
82
+ for root, dirs, files in os.walk(CODE_DIR):
83
+ # 跳过非业务目录
84
+ dirs[:] = [d for d in dirs if d not in (
85
+ '.git', 'node_modules', '__pycache__', 'target', 'test')]
86
+ for fn in files:
87
+ if not fn.endswith('.java'):
88
+ continue
89
+ scanned += 1
90
+ fpath = os.path.join(root, fn)
91
+ try:
92
+ for enc in ('utf-8', 'gbk'):
93
+ try:
94
+ with open(fpath, encoding=enc) as f:
95
+ content = f.read()
96
+ break
97
+ except (UnicodeDecodeError, OSError):
98
+ content = ''
99
+ continue
100
+ except OSError:
101
+ continue
102
+ if '@TableName' not in content:
103
+ continue
104
+ # 找所有 @TableName 注解
105
+ for m in RE_TABLENAME.finditer(content):
106
+ table_name = m.group(1)
107
+ normalized = _normalize_table_name(table_name)
108
+ annotation = content[m.start():m.end() + 1]
109
+ # 找注解之后最近的 class 声明 (Entity 类名)
110
+ after = content[m.end():m.end() + 500]
111
+ cm = RE_CLASS.search(after)
112
+ entity_class = cm.group(1) if cm else os.path.splitext(fn)[0]
113
+ rel = os.path.relpath(fpath, PROJECT_ROOT).replace('\\', '/')
114
+ mappings.append((entity_class, table_name, normalized, rel, annotation))
115
+ return mappings, scanned
116
+
117
+
118
+ def build(rebuild=False):
119
+ """扫代码 @TableName → 写 entity_table_map。"""
120
+ t0 = time.time()
121
+ print('扫描 data/code/ 下的 @TableName 注解...')
122
+ mappings, scanned = _scan_java_files()
123
+ print(' 扫描 %d 个 .java 文件, 找到 %d 个 @TableName 映射 (%.1fs)'
124
+ % (scanned, len(mappings), time.time() - t0))
125
+
126
+ if not mappings:
127
+ print('未找到任何 @TableName 注解。检查 data/code/ 是否有 Java 源码。')
128
+ return 0
129
+
130
+ con = duckdb.connect(DB_PATH)
131
+ try:
132
+ con.execute("""CREATE TABLE IF NOT EXISTS entity_table_map (
133
+ entity_class TEXT,
134
+ table_name TEXT,
135
+ normalized_table TEXT,
136
+ source_file TEXT,
137
+ annotation TEXT
138
+ )""")
139
+ con.execute("CREATE INDEX IF NOT EXISTS idx_etm_table ON entity_table_map(table_name)")
140
+ con.execute("CREATE INDEX IF NOT EXISTS idx_etm_norm ON entity_table_map(normalized_table)")
141
+ con.execute("CREATE INDEX IF NOT EXISTS idx_etm_class ON entity_table_map(entity_class)")
142
+
143
+ if rebuild:
144
+ con.execute("DELETE FROM entity_table_map")
145
+
146
+ # JSONL COPY 批量插入 (DuckDB 1.4 executemany 慢)
147
+ import json as _json
148
+ import tempfile
149
+ tmp = tempfile.NamedTemporaryFile(suffix='.jsonl', delete=False,
150
+ mode='w', encoding='utf-8')
151
+ cols = ['entity_class', 'table_name', 'normalized_table', 'source_file', 'annotation']
152
+ for row in mappings:
153
+ obj = {cols[i]: (row[i] if row[i] is not None else '')
154
+ for i in range(len(cols))}
155
+ tmp.write(_json.dumps(obj, ensure_ascii=False) + '\n')
156
+ tmp.close()
157
+ try:
158
+ con.execute("COPY entity_table_map FROM '%s' (FORMAT JSON)"
159
+ % tmp.name.replace('\\', '/'))
160
+ finally:
161
+ os.unlink(tmp.name)
162
+
163
+ con.execute("INSERT OR REPLACE INTO build_meta(key, value) VALUES (?,?)",
164
+ ['entity_table_map_built', str(time.time())])
165
+ print('写入 %d 条 Entity→表 映射到 entity_table_map' % len(mappings))
166
+ return len(mappings)
167
+ finally:
168
+ con.close()
169
+
170
+
171
+ def query_impact(table_name):
172
+ """查某张表关联的 Entity 类 (给"改表影响分析"用)。"""
173
+ con = duckdb.connect(DB_PATH, read_only=True)
174
+ try:
175
+ rows = con.execute(
176
+ "SELECT DISTINCT entity_class, source_file FROM entity_table_map "
177
+ "WHERE table_name = ?", [table_name]
178
+ ).fetchall()
179
+ return rows
180
+ finally:
181
+ con.close()
182
+
183
+
184
+ def stats():
185
+ """映射统计 + db_tables/db_columns 的关联率。"""
186
+ con = duckdb.connect(DB_PATH, read_only=True)
187
+ try:
188
+ total = con.execute("SELECT COUNT(*) FROM entity_table_map").fetchone()[0]
189
+ classes = con.execute("SELECT COUNT(DISTINCT entity_class) FROM entity_table_map").fetchone()[0]
190
+ tables_mapped = con.execute("SELECT COUNT(DISTINCT table_name) FROM entity_table_map").fetchone()[0]
191
+ # 关联率: 映射的表名 vs 数据库真实表名 (原始 + 归一化)
192
+ try:
193
+ raw_matched = con.execute("""
194
+ SELECT COUNT(DISTINCT m.table_name) FROM entity_table_map m
195
+ JOIN db_tables t ON m.table_name = t.table_name
196
+ """).fetchone()[0]
197
+ norm_matched = con.execute("""
198
+ SELECT COUNT(DISTINCT m.normalized_table) FROM entity_table_map m
199
+ JOIN db_tables t ON m.normalized_table = t.table_name
200
+ """).fetchone()[0]
201
+ db_total = con.execute("SELECT COUNT(DISTINCT table_name) FROM db_tables").fetchone()[0]
202
+ except Exception:
203
+ raw_matched, norm_matched, db_total = 0, 0, 0
204
+ print('entity_table_map 统计:')
205
+ print(' 总映射: %d' % total)
206
+ print(' Entity 类数: %d' % classes)
207
+ print(' 映射的表名数: %d' % tables_mapped)
208
+ if db_total:
209
+ print(' 原始表名匹配: %d/%d (%.0f%%)'
210
+ % (raw_matched, db_total, raw_matched / db_total * 100))
211
+ print(' 归一化后匹配: %d/%d (%.0f%%)'
212
+ % (norm_matched, db_total, norm_matched / db_total * 100))
213
+ # 抽样
214
+ sample = con.execute(
215
+ "SELECT entity_class, table_name FROM entity_table_map LIMIT 8"
216
+ ).fetchall()
217
+ print(' 抽样:')
218
+ for ec, tn in sample:
219
+ print(' %s %s' % (ec, tn))
220
+ finally:
221
+ con.close()
222
+
223
+
224
+ if __name__ == '__main__':
225
+ args = sys.argv[1:]
226
+ if '--stats' in args:
227
+ stats()
228
+ else:
229
+ build(rebuild='--rebuild' in args)
230
+ stats()