@hupan56/wlkj 3.2.0 → 3.3.1

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 (130) hide show
  1. package/bin/cli.js +117 -0
  2. package/package.json +2 -2
  3. package/templates/.qoder/.runtime/hook-errors.log +4 -0
  4. package/templates/qoder/commands/optional/wl-insight.md +276 -276
  5. package/templates/qoder/commands/optional/wl-report.md +1 -1
  6. package/templates/qoder/commands/optional/wl-spec.md +13 -1
  7. package/templates/qoder/commands/optional/wl-status.md +14 -1
  8. package/templates/qoder/commands/wl-code.md +111 -4
  9. package/templates/qoder/commands/wl-commit.md +12 -1
  10. package/templates/qoder/commands/wl-design.md +69 -5
  11. package/templates/qoder/commands/wl-init.md +129 -102
  12. package/templates/qoder/commands/wl-prd.md +162 -6
  13. package/templates/qoder/commands/wl-search.md +106 -20
  14. package/templates/qoder/commands/wl-task.md +617 -613
  15. package/templates/qoder/commands/wl-test.md +20 -1
  16. package/templates/qoder/contracts/spec.md +4 -0
  17. package/templates/qoder/hooks/post-tool-use.py +71 -0
  18. package/templates/qoder/hooks/pre-tool-use.py +136 -0
  19. package/templates/qoder/hooks/session-start.py +365 -397
  20. package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
  21. package/templates/qoder/scripts/capability/__pycache__/present_html.cpython-39.pyc +0 -0
  22. package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
  23. package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
  24. package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
  25. package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
  26. package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
  27. package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
  28. package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
  29. package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
  30. package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
  31. package/templates/qoder/scripts/capability/caps/__init__.py +5 -5
  32. package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
  33. package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
  34. package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
  35. package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
  36. package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
  37. package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
  38. package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
  39. package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
  40. package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
  41. package/templates/qoder/scripts/capability/caps/memory.py +1 -1
  42. package/templates/qoder/scripts/capability/caps/notify.py +64 -0
  43. package/templates/qoder/scripts/capability/caps/sandbox.py +38 -0
  44. package/templates/qoder/scripts/capability/present_html.py +68 -0
  45. package/templates/qoder/scripts/capability/registry.py +27 -25
  46. package/templates/qoder/scripts/capability/registry_mcp.py +67 -5
  47. package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
  48. package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
  49. package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
  50. package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
  51. package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
  52. package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
  53. package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
  54. package/templates/qoder/scripts/domain/integration/return_to_platform.py +395 -0
  55. package/templates/qoder/scripts/domain/integration/spec_upload.py +208 -0
  56. package/templates/qoder/scripts/domain/kg/build/kg_build.py +27 -3
  57. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
  58. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
  59. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
  60. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
  61. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
  62. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
  63. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
  64. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
  65. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
  66. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
  67. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
  68. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
  69. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
  70. package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
  71. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
  72. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
  73. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
  74. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
  75. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
  76. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
  77. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
  78. package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
  79. package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
  80. package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
  81. package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
  82. package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
  83. package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
  84. package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
  85. package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
  86. package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
  87. package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
  88. package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
  89. package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
  90. package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
  91. package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
  92. package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
  93. package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
  94. package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
  95. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
  96. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
  97. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
  98. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
  99. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
  100. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
  101. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
  102. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
  103. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
  104. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
  105. package/templates/qoder/scripts/domain/kg/kg.py +42 -5
  106. package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
  107. package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
  108. package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
  109. package/templates/qoder/scripts/domain/kg/switch_project.py +158 -0
  110. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
  111. package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
  112. package/templates/qoder/scripts/domain/task/{zentao_panel.py → wlkj_panel.py} +315 -53
  113. package/templates/qoder/scripts/engine/poller.py +219 -0
  114. package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
  115. package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
  116. package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
  117. package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
  118. package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
  119. package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
  120. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1 -1
  121. package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
  122. package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
  123. package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
  124. package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
  125. package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
  126. package/templates/qoder/scripts/protocol/transports/http.py +7 -1
  127. package/templates/qoder/scripts/validation/metrics/__pycache__/present_board.cpython-39.pyc +0 -0
  128. package/templates/qoder/scripts/validation/metrics/present_board.py +180 -0
  129. package/templates/qoder/settings.json +10 -0
  130. package/templates/root/AGENTS.md +9 -10
@@ -0,0 +1,157 @@
1
+ # -*- coding: utf-8 -*-
2
+ """方法/字段级提取器: 端点 + 权限 + 校验 + 调用链 + 字段映射。
3
+
4
+ 和 class_extractor 互补: class_extractor 处理类级(实体+注入),
5
+ 本文件处理类内成员(方法→端点/权限, 字段→校验/列)。
6
+ """
7
+ from __future__ import annotations
8
+ from . import _parser as P
9
+
10
+
11
+ # Sa-Token 权限注解
12
+ _SATOKEN_PERM = {'SaCheckPermission', 'SaCheckRole', 'SaCheckLogin', 'SaMode', 'SaIgnore'}
13
+ # 校验注解
14
+ _VALIDATION = {'NotNull', 'NotBlank', 'NotEmpty', 'Size', 'Min', 'Max',
15
+ 'Email', 'Pattern', 'Positive', 'Negative', 'Digits', 'Past', 'Future'}
16
+ # MyBatis-Plus 字段注解
17
+ _MP_FIELD = {'TableId', 'TableField', 'TableLogic'}
18
+
19
+
20
+ def extract(root, repo_id: str, filepath: str, owner_classes: list):
21
+ """提取方法/字段级实体 + 边。
22
+
23
+ Args:
24
+ owner_classes: class_extractor 已提取的类信息 [{name, etype, id, anns}]
25
+ 方法级边要锚到所属类上。
26
+ Returns:
27
+ (entities, edges)
28
+ """
29
+ entities, edges = [], []
30
+ if root is None:
31
+ return entities, edges
32
+
33
+ # 建 name → owner 映射(方法/字段属于哪个类)
34
+ owner_map = {c['name']: c for c in owner_classes}
35
+
36
+ for cls in P.get_class_decls(root):
37
+ cls_name = P.get_class_name(cls)
38
+ owner = owner_map.get(cls_name)
39
+ if not owner:
40
+ continue # 不是后端组件类(如普通 DTO),由 domain_extractor 处理
41
+ owner_id = owner['id']
42
+
43
+ # ===== 方法级: 端点 + 权限 + 调用链 =====
44
+ for m in P.get_methods(cls):
45
+ method_name = m['name']
46
+ m_anns = m['annotations']
47
+
48
+ # 端点: @GetMapping/@PostMapping 等
49
+ endpoint_path = _extract_endpoint_path(m_anns, owner.get('anns_dict', {}))
50
+ if endpoint_path:
51
+ ep_id = 'EP:%s:%s' % (repo_id, endpoint_path)
52
+ entities.append({
53
+ 'id': ep_id, 'repo_id': repo_id, 'type': 'ENDPOINT',
54
+ 'canonical': endpoint_path, 'cn': '',
55
+ 'props': {'method': _extract_http_verb(list(m_anns.keys())),
56
+ 'handler': '%s.%s' % (cls_name, method_name),
57
+ 'returns': m.get('return_type', '')},
58
+ })
59
+ edges.append({
60
+ 'from_id': ep_id, 'to_id': owner_id,
61
+ 'edge_type': 'handled_by',
62
+ 'from_repo': repo_id, 'to_repo': repo_id,
63
+ 'confidence': 1.0, 'source': 'explicit',
64
+ })
65
+
66
+ # 权限: @SaCheckPermission("asset:list")
67
+ for sa in _SATOKEN_PERM:
68
+ if sa in m_anns:
69
+ perm_code = m_anns[sa].strip('"').strip("'") or sa
70
+ perm_id = 'PERM:%s:%s' % (repo_id, perm_code)
71
+ entities.append({
72
+ 'id': perm_id, 'repo_id': repo_id, 'type': 'PERMISSION',
73
+ 'canonical': perm_code, 'cn': '',
74
+ 'props': {'check_type': sa, 'method': '%s.%s' % (cls_name, method_name)},
75
+ })
76
+ edges.append({
77
+ 'from_id': owner_id, 'to_id': perm_id,
78
+ 'edge_type': 'guards',
79
+ 'from_repo': repo_id, 'to_repo': repo_id,
80
+ 'confidence': 1.0, 'source': 'explicit',
81
+ })
82
+
83
+ # 调用链: 方法体内 remoteClientService.getById()
84
+ # 要把 object 名映射回字段类型(字段类型在 owner 的 fields 里)
85
+ field_types = {f['name']: f['type'] for f in P.get_fields(cls)}
86
+ for call in P.get_method_calls(m['body_node']):
87
+ obj_name = call['object']
88
+ method = call['method']
89
+ if not obj_name:
90
+ continue
91
+ dep_type = field_types.get(obj_name) or _guess_type_by_name(obj_name)
92
+ if not dep_type:
93
+ continue
94
+ dep_prefix = _guess_dep_prefix(dep_type)
95
+ edges.append({
96
+ 'from_id': owner_id,
97
+ 'to_id': '%s:%s:%s' % (dep_prefix, repo_id, dep_type),
98
+ 'edge_type': 'calls',
99
+ 'from_repo': repo_id, 'to_repo': repo_id,
100
+ 'confidence': 1.0, 'source': 'explicit',
101
+ 'props': {'method': method, 'callee_field': obj_name},
102
+ })
103
+
104
+ return entities, edges
105
+
106
+
107
+ # ===== 端点相关 =====
108
+ _ENDPOINT_ANN_TO_VERB = {
109
+ 'GetMapping': 'GET', 'PostMapping': 'POST',
110
+ 'PutMapping': 'PUT', 'DeleteMapping': 'DELETE',
111
+ 'PatchMapping': 'PATCH', 'RequestMapping': 'ANY',
112
+ }
113
+
114
+
115
+ def _extract_endpoint_path(method_anns: dict, class_anns: dict) -> str:
116
+ """提取端点路径: 类级 @RequestMapping + 方法级 @GetMapping。"""
117
+ base = ''
118
+ if 'RequestMapping' in class_anns:
119
+ base = class_anns['RequestMapping'].strip('"').strip("'")
120
+ for ann, _ in method_anns.items():
121
+ if ann in _ENDPOINT_ANN_TO_VERB:
122
+ path = method_anns[ann].strip('"').strip("'")
123
+ if base and path and not path.startswith('/'):
124
+ path = base.rstrip('/') + '/' + path
125
+ elif base and not path:
126
+ path = base
127
+ return path or '/'
128
+ return ''
129
+
130
+
131
+ def _extract_http_verb(ann_names: list) -> str:
132
+ for ann in ann_names:
133
+ if ann in _ENDPOINT_ANN_TO_VERB:
134
+ return _ENDPOINT_ANN_TO_VERB[ann]
135
+ return 'ANY'
136
+
137
+
138
+ def _guess_type_by_name(field_name: str) -> str:
139
+ """从字段名猜类型(如 xxxMapper → XxxMapper)。
140
+ 字段类型有时在 javadoc/import 里找不到, 用命名兜底。"""
141
+ parts = field_name.split('_')
142
+ guessed = ''.join(p.capitalize() for p in parts if p)
143
+ if 'mapper' in field_name.lower():
144
+ return guessed if guessed.endswith('Mapper') else guessed + 'Mapper'
145
+ if 'service' in field_name.lower():
146
+ return guessed if guessed.endswith('Service') else guessed + 'Service'
147
+ return ''
148
+
149
+
150
+ def _guess_dep_prefix(type_name: str) -> str:
151
+ if type_name.endswith('Mapper'):
152
+ return 'MAP'
153
+ if type_name.endswith('ServiceImpl') or type_name.endswith('Service'):
154
+ return 'SVC'
155
+ if type_name.endswith('Controller'):
156
+ return 'CTRL'
157
+ return 'SVC'
@@ -0,0 +1,34 @@
1
+ # -*- coding: utf-8 -*-
2
+ """MyBatis-Plus ORM 提取器(@TableName/@TableId/@TableField/@TableLogic)。
3
+
4
+ 职责: 识别 Entity→Table 映射 + 字段→列映射。
5
+ 本项目用 MyBatis-Plus 不是 JPA, @TableName("employee_online") 是表名真源。
6
+ 实体识别在 class_extractor(Entity 类型) + domain_extractor(字段映射),
7
+ 这里按 MyBatis-Plus 关注点导出。
8
+ """
9
+ from __future__ import annotations
10
+ from . import _parser as P
11
+ from . import class_extractor as _ce
12
+ from . import domain_extractor as _de
13
+
14
+ MP_TABLE = {'TableName'}
15
+ MP_ID = {'TableId'}
16
+ MP_FIELD = {'TableField'}
17
+ MP_LOGIC = {'TableLogic'}
18
+ MP_ALL = MP_TABLE | MP_ID | MP_FIELD | MP_LOGIC
19
+
20
+
21
+ def is_mybatis_entity(annotations: dict) -> bool:
22
+ """类是否 @TableName(MyBatis-Plus Entity)。"""
23
+ return 'TableName' in annotations
24
+
25
+
26
+ def extract_table_name(annotations: dict) -> str:
27
+ """从 @TableName 注解取表名。"""
28
+ v = annotations.get('TableName', '')
29
+ return v.strip('"').strip("'") if v else ''
30
+
31
+
32
+ def extract(root, repo_id: str, filepath: str):
33
+ """MyBatis-Plus Entity 提取(= domain_extractor, 处理 Entity/字段/列映射)。"""
34
+ return _de.extract(root, repo_id, filepath)
@@ -0,0 +1,165 @@
1
+ # -*- coding: utf-8 -*-
2
+ """PG 批量 upsert: 把 Java extractor 的 entities/edges 写进 PostgreSQL。
3
+
4
+ 独立于 DuckDB 写入路径 —— GOAL 2 直接写 PG(KNOWLEDGE_BACKEND=pg 时)。
5
+ 用 executemany + ON CONFLICT 幂等 upsert。
6
+ """
7
+ from __future__ import annotations
8
+ import os
9
+ import sys
10
+ import json
11
+ import time
12
+
13
+ # 路径自举
14
+ _THIS = os.path.dirname(os.path.abspath(__file__))
15
+ for _i in range(8):
16
+ _p = os.path.dirname(_THIS)
17
+ if os.path.isfile(os.path.join(_p, 'foundation', 'bootstrap.py')):
18
+ sys.path.insert(0, _p)
19
+ break
20
+ _THIS = _p
21
+
22
+
23
+ def _get_pg_engine():
24
+ """获取 PG 引擎(优先用工作台的 SessionLocal 引擎)。"""
25
+ # 优先工作台 backend(KNOWLEDGE_BACKEND=pg 时)
26
+ ws_backend = os.path.join(os.getcwd(), 'wlinkj-workspace', 'backend')
27
+ if os.path.isfile(os.path.join(ws_backend, '.env')) or os.path.isfile(os.path.join(ws_backend, '..', '..', '.env')):
28
+ try:
29
+ from dotenv import load_dotenv
30
+ for env_path in [os.path.join(ws_backend, '.env'),
31
+ os.path.join(ws_backend, '..', '..', '.env')]:
32
+ if os.path.isfile(env_path):
33
+ load_dotenv(env_path); break
34
+ except Exception:
35
+ pass
36
+ # 用工作台 db 引擎
37
+ sys.path.insert(0, ws_backend)
38
+ try:
39
+ from app.db import engine
40
+ return engine, 'app.db'
41
+ except Exception:
42
+ pass
43
+ # 兜底: 直接从环境变量建引擎
44
+ db_url = os.environ.get('DATABASE_URL', '')
45
+ if db_url and 'postgresql' in db_url:
46
+ from sqlalchemy import create_engine
47
+ return create_engine(db_url), 'env'
48
+ return None, None
49
+
50
+
51
+ def upsert_batch(entities: list, edges: list, batch_size: int = 500):
52
+ """批量 upsert entities + edges 到 PG。
53
+
54
+ entities: [{id, repo_id, type, canonical, cn, props}]
55
+ edges: [{from_id, to_id, edge_type, from_repo, to_repo, confidence, source, props}]
56
+ 幂等: ON CONFLICT 覆盖。
57
+ """
58
+ engine, src = _get_pg_engine()
59
+ if engine is None:
60
+ return {'ok': False, 'error': 'PG 引擎不可用 (检查 DATABASE_URL/工作台路径)'}
61
+
62
+ from sqlalchemy import text
63
+ ent_ok = ent_fail = ed_ok = ed_fail = 0
64
+ t0 = time.time()
65
+
66
+ with engine.begin() as conn:
67
+ # === 先确保所有 repo_id 在 repos 表注册 (entities.repo_id 有FK约束) ===
68
+ repo_ids = set()
69
+ for e in entities:
70
+ if e.get('repo_id'):
71
+ repo_ids.add(e['repo_id'])
72
+ for e in edges:
73
+ if e.get('from_repo'):
74
+ repo_ids.add(e['from_repo'])
75
+ if e.get('to_repo'):
76
+ repo_ids.add(e['to_repo'])
77
+ for rid in repo_ids:
78
+ try:
79
+ conn.execute(text("""
80
+ INSERT INTO repos (id, name) VALUES (:id, :id)
81
+ ON CONFLICT (id) DO NOTHING
82
+ """), {'id': rid})
83
+ except Exception:
84
+ pass
85
+
86
+ # === entities ===
87
+ for i in range(0, len(entities), batch_size):
88
+ batch = entities[i:i + batch_size]
89
+ for e in batch:
90
+ try:
91
+ props_json = e.get('props') or {}
92
+ if not isinstance(props_json, str):
93
+ props_json = json.dumps(props_json, ensure_ascii=False)
94
+ conn.execute(text("""
95
+ INSERT INTO entities (id, repo_id, type, canonical, cn, props)
96
+ VALUES (:id, :repo_id, :type, :canonical, :cn, CAST(:props AS JSONB))
97
+ ON CONFLICT (id) DO UPDATE SET
98
+ type=EXCLUDED.type, canonical=EXCLUDED.canonical,
99
+ cn=EXCLUDED.cn, props=EXCLUDED.props
100
+ """), {
101
+ 'id': e['id'], 'repo_id': e.get('repo_id', ''),
102
+ 'type': e['type'], 'canonical': e.get('canonical', ''),
103
+ 'cn': e.get('cn', ''), 'props': props_json,
104
+ })
105
+ ent_ok += 1
106
+ except Exception:
107
+ ent_fail += 1
108
+
109
+ # === edges ===
110
+ for i in range(0, len(edges), batch_size):
111
+ batch = edges[i:i + batch_size]
112
+ for e in batch:
113
+ try:
114
+ props_json = e.get('props') or {}
115
+ if not isinstance(props_json, str):
116
+ props_json = json.dumps(props_json, ensure_ascii=False)
117
+ conn.execute(text("""
118
+ INSERT INTO edges (from_id, to_id, edge_type, from_repo, to_repo,
119
+ confidence, source, expires_at, fingerprint, props)
120
+ VALUES (:from_id, :to_id, :edge_type, :from_repo, :to_repo,
121
+ :confidence, :source,
122
+ CAST(:expires_at AS TIMESTAMPTZ), :fingerprint,
123
+ CAST(:props AS JSONB))
124
+ ON CONFLICT DO NOTHING
125
+ """), {
126
+ 'from_id': e['from_id'], 'to_id': e['to_id'],
127
+ 'edge_type': e['edge_type'],
128
+ 'from_repo': e.get('from_repo', ''),
129
+ 'to_repo': e.get('to_repo', ''),
130
+ 'confidence': e.get('confidence', 1.0),
131
+ 'source': e.get('source', 'explicit'),
132
+ 'expires_at': e.get('expires_at_iso') or e.get('expires_at'),
133
+ 'fingerprint': e.get('fingerprint'),
134
+ 'props': props_json,
135
+ })
136
+ ed_ok += 1
137
+ except Exception:
138
+ ed_fail += 1
139
+
140
+ return {
141
+ 'ok': True, 'engine_source': src,
142
+ 'entities_ok': ent_ok, 'entities_fail': ent_fail,
143
+ 'edges_ok': ed_ok, 'edges_fail': ed_fail,
144
+ 'seconds': round(time.time() - t0, 1),
145
+ }
146
+
147
+
148
+ def upsert_freshness(entities: list, fingerprint_map: dict):
149
+ """更新 knowledge_freshness 表(增量构建依赖)。
150
+
151
+ fingerprint_map: {entity_id: content_hash}(可选, 没传则不写指纹)
152
+ """
153
+ engine, _ = _get_pg_engine()
154
+ if engine is None:
155
+ return
156
+ from sqlalchemy import text
157
+ with engine.begin() as conn:
158
+ for e in entities:
159
+ fp = fingerprint_map.get(e['id'], '')
160
+ conn.execute(text("""
161
+ INSERT INTO knowledge_freshness (entity_id, source, fingerprint, extracted_at)
162
+ VALUES (:id, 'java_ast', :fp, now())
163
+ ON CONFLICT (entity_id) DO UPDATE SET
164
+ source='java_ast', fingerprint=EXCLUDED.fingerprint, extracted_at=now()
165
+ """), {'id': e['id'], 'fp': fp})
@@ -0,0 +1,30 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Sa-Token 权限提取器(@SaCheckPermission/@SaCheckRole/@SaCheckLogin)。
3
+
4
+ 职责: 识别 Sa-Token 权限注解 → PERMISSION 实体 + guards 边。
5
+ 本项目用 Sa-Token 不是 Spring Security, @SaCheckPermission 是真实权限点来源。
6
+ 方法级权限提取在 member_extractor, 这里是类级判定 + 常量定义。
7
+ """
8
+ from __future__ import annotations
9
+ from . import _parser as P
10
+ from . import class_extractor as _ce
11
+
12
+ # Sa-Token 权限注解
13
+ SATOKEN_PERM = {'SaCheckPermission'}
14
+ SATOKEN_ROLE = {'SaCheckRole'}
15
+ SATOKEN_LOGIN = {'SaCheckLogin'}
16
+ SATOKEN_IGNORE = {'SaIgnore'}
17
+ SATOKEN_ALL = SATOKEN_PERM | SATOKEN_ROLE | SATOKEN_LOGIN | SATOKEN_IGNORE
18
+
19
+
20
+ def has_satoken_guard(annotations: dict) -> bool:
21
+ """类/方法是否有 Sa-Token 权限注解。"""
22
+ return bool(set(annotations.keys()) & SATOKEN_ALL)
23
+
24
+
25
+ def extract(root, repo_id: str, filepath: str):
26
+ """Sa-Token 类级提取(转发到 class_extractor, 行为一致)。
27
+
28
+ PERMISSION 实体 + guards 边在 member_extractor 的方法级遍历里产出。
29
+ """
30
+ return _ce.extract(root, repo_id, filepath)
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Spring 注解提取器(@RestController/@Service/@Mapper/@Autowired/@Bean)。
3
+
4
+ 职责: 识别 Spring 后端组件类型 + 构造器/Autowired 注入关系。
5
+ 本文件按 GOAL 2 目标要求独立拆分, 与 class_extractor.py 共享同一套提取逻辑
6
+ (单一事实源在 class_extractor, 这里导出 Spring 相关部分)。
7
+ """
8
+ from __future__ import annotations
9
+ from . import _parser as P
10
+ from . import class_extractor as _ce
11
+
12
+ # Spring 注解集合(供外部判定用)
13
+ SPRING_SERVICE = {'Service', 'DubboService'}
14
+ SPRING_CONTROLLER = {'RestController', 'Controller'}
15
+ SPRING_MAPPER = {'Mapper', 'Repository'}
16
+ SPRING_CONFIG = {'Configuration', 'Component', 'ConfigurationProperties'}
17
+ SPRING_INJECT = {'Autowired', 'Resource'}
18
+
19
+
20
+ def is_spring_component(annotations: dict) -> bool:
21
+ """类是否带 Spring 组件注解。"""
22
+ keys = set(annotations.keys())
23
+ return bool(keys & (SPRING_SERVICE | SPRING_CONTROLLER | SPRING_MAPPER | SPRING_CONFIG))
24
+
25
+
26
+ def classify(name: str, annotations: dict):
27
+ """按 Spring 注解 + 命名规则判定实体类型(与 class_extractor._classify 一致)。"""
28
+ return _ce._classify(name, annotations)
29
+
30
+
31
+ # 复用 class_extractor 的完整提取入口(保证行为一致)
32
+ def extract(root, repo_id: str, filepath: str):
33
+ """提取 Spring 类级实体 + 边(= class_extractor.extract 的 Spring 部分)。
34
+
35
+ 实际实现见 class_extractor.extract(一次遍历识别 Spring/Dubbo/Sa-Token/MyBatis-Plus),
36
+ 本函数等价于对结果按注解来源过滤 Spring 部分。为保证 zero-behavior-change,
37
+ 这里直接转发到 class_extractor.extract。
38
+ """
39
+ return _ce.extract(root, repo_id, filepath)
@@ -0,0 +1,33 @@
1
+ # -*- coding: utf-8 -*-
2
+ """校验规则提取器(@NotNull/@NotBlank/@Size/@Email/@Pattern 等)。
3
+
4
+ 职责: 从字段注解提取校验规则 → VALIDATION 实体 + validates 边。
5
+ 按 GOAL 2 目标要求独立拆分, 共享 domain_extractor 的字段遍历逻辑
6
+ (字段校验在 domain_extractor 处理 Entity 字段时一并产出)。
7
+ """
8
+ from __future__ import annotations
9
+ from . import _parser as P
10
+ from . import domain_extractor as _de
11
+
12
+ VALIDATION_ANN = {'NotNull', 'NotBlank', 'NotEmpty', 'Size', 'Min', 'Max',
13
+ 'Email', 'Pattern', 'Positive', 'Negative', 'Digits',
14
+ 'Past', 'Future', 'DecimalMin', 'DecimalMax'}
15
+
16
+
17
+ def has_validation(annotations: dict) -> bool:
18
+ """字段是否有校验注解。"""
19
+ return bool(set(annotations.keys()) & VALIDATION_ANN)
20
+
21
+
22
+ def rule_summary(annotations: dict) -> list:
23
+ """汇总字段的校验规则 → [{rule, arg}]。"""
24
+ out = []
25
+ for ann, arg in annotations.items():
26
+ if ann in VALIDATION_ANN:
27
+ out.append({'rule': ann, 'arg': arg})
28
+ return out
29
+
30
+
31
+ def extract(root, repo_id: str, filepath: str):
32
+ """校验规则提取(转发到 domain_extractor, Entity 字段的校验在那里产出)。"""
33
+ return _de.extract(root, repo_id, filepath)
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+ """MyBatis mapper XML 提取器 (GOAL 3)。
3
+
4
+ 子模块:
5
+ mapper_parser XML namespace + resultMap + 方法→SQL
6
+ sql_extractor SQL 文本 → 表名 + 列引用
7
+ relation_builder Mapper ↔ Entity ↔ Table 三向关联
8
+ all 协调器: 扫所有 mapper XML → 提取 → 返回 (entities, edges)
9
+ """
@@ -0,0 +1,79 @@
1
+ # -*- coding: utf-8 -*-
2
+ """MyBatis XML 提取协调器。
3
+
4
+ 扫所有 *Mapper.xml → 解析 → 建关系 → 返回 (entities, edges)。
5
+ """
6
+ from __future__ import annotations
7
+ import os
8
+ import sys
9
+ import glob
10
+ import time
11
+
12
+ _THIS = os.path.dirname(os.path.abspath(__file__))
13
+ for _i in range(8):
14
+ _p = os.path.dirname(_THIS)
15
+ if os.path.isfile(os.path.join(_p, 'foundation', 'bootstrap.py')):
16
+ sys.path.insert(0, _p); break
17
+ _THIS = _p
18
+
19
+ from domain.kg.extract.mybatis import mapper_parser as mp
20
+ from domain.kg.extract.mybatis import relation_builder as rb
21
+
22
+
23
+ def extract_file(filepath: str, repo_id: str):
24
+ """单文件提取 → (entities, edges)。"""
25
+ parsed = mp.parse_mapper_xml(filepath)
26
+ if not parsed.get('namespace_short'):
27
+ return [], []
28
+ return rb.build_relations(parsed, repo_id, filepath)
29
+
30
+
31
+ def extract_repo(repo_dir: str, repo_id: str, logger=print):
32
+ """扫 repo 下所有 mapper XML → 提取。"""
33
+ t0 = time.time()
34
+ patterns = [
35
+ os.path.join(repo_dir, '**', '*Mapper.xml'),
36
+ os.path.join(repo_dir, '**', '*mapper*.xml'),
37
+ os.path.join(repo_dir, '**', 'mapper', '*.xml'),
38
+ ]
39
+ xml_files = set()
40
+ for pat in patterns:
41
+ xml_files.update(glob.glob(pat, recursive=True))
42
+ # 跳过编译产物 target/classes 和测试资源
43
+ xml_files = [f for f in xml_files if '/target/' not in f.replace('\\', '/')
44
+ and '\\target\\' not in f.lower()
45
+ and '/test/' not in f.replace('\\', '/').lower()]
46
+ total = len(xml_files)
47
+ logger(' [mybatis] 扫描 %d 个 mapper XML (repo=%s)' % (total, repo_id))
48
+
49
+ all_entities, all_edges = [], []
50
+ err = 0
51
+ for xf in xml_files:
52
+ try:
53
+ es, eds = extract_file(xf, repo_id)
54
+ all_entities += es
55
+ all_edges += eds
56
+ except Exception as e:
57
+ err += 1
58
+ if err <= 2:
59
+ logger(' [mybatis] 失败 %s: %s' % (xf, str(e)[:50]))
60
+
61
+ dt = time.time() - t0
62
+ tables = set()
63
+ for e in all_entities:
64
+ if e['type'] == 'TABLE':
65
+ tables.add(e['canonical'])
66
+ logger(' [mybatis] 完成: %d 实体 / %d 边 / %d 表 / %d 错 / %.1fs' % (
67
+ len(all_entities), len(all_edges), len(tables), err, dt))
68
+ return all_entities, all_edges
69
+
70
+
71
+ if __name__ == '__main__':
72
+ import argparse, json
73
+ ap = argparse.ArgumentParser()
74
+ ap.add_argument('repo_dir')
75
+ ap.add_argument('--repo-id', default='')
76
+ args = ap.parse_args()
77
+ rid = args.repo_id or os.path.basename(args.repo_dir.rstrip('/\\'))
78
+ es, eds = extract_repo(args.repo_dir, rid)
79
+ print(json.dumps({'entities': len(es), 'edges': len(eds)}, ensure_ascii=False, indent=2))
@@ -0,0 +1,99 @@
1
+ # -*- coding: utf-8 -*-
2
+ """mapper XML 解析器: namespace + resultMap + 方法→SQL 映射。
3
+
4
+ 输出每个 mapper 文件的结构化信息, 供 relation_builder 建 queries 边 + COLUMN 实体。
5
+ """
6
+ from __future__ import annotations
7
+ import os
8
+ import re
9
+ import xml.etree.ElementTree as ET
10
+
11
+ from . import sql_extractor as _sq
12
+
13
+
14
+ def _extract_table_names(sql):
15
+ """从 SQL 提取表名 (转发到 sql_extractor)。"""
16
+ return _sq.extract_table_names(sql)
17
+
18
+
19
+ def parse_mapper_xml(filepath: str) -> dict:
20
+ """解析一个 mapper XML。
21
+
22
+ Returns:
23
+ {
24
+ 'namespace': 'com.fywl.ics.demo.mapper.TestDemoMapper',
25
+ 'namespace_short': 'TestDemoMapper',
26
+ 'statements': [{'id':'customPageList','type':'select','sql':'SELECT * FROM test_demo...', 'tables':['test_demo']}],
27
+ 'result_maps': {'TestDemoResult': [{'property':'id','column':'id'}, ...]},
28
+ 'result_types': ['com.fywl.ics.demo.domain.vo.TestDemoVo'],
29
+ }
30
+ """
31
+ try:
32
+ tree = ET.parse(filepath)
33
+ root = tree.getroot()
34
+ except Exception:
35
+ return _empty_result()
36
+
37
+ namespace = root.get('namespace', '') or ''
38
+ namespace_short = namespace.rsplit('.', 1)[-1] if namespace else os.path.basename(filepath).replace('.xml', '')
39
+
40
+ statements = []
41
+ result_maps = {}
42
+ result_types = set()
43
+
44
+ for tag in ('select', 'insert', 'update', 'delete'):
45
+ for stmt in root.findall(tag):
46
+ sid = stmt.get('id', '')
47
+ sql_text = _extract_sql_text(stmt)
48
+ tables = _extract_table_names(sql_text)
49
+ if stmt.get('resultType'):
50
+ result_types.add(stmt.get('resultType'))
51
+ statements.append({
52
+ 'id': sid, 'type': tag, 'sql': sql_text, 'tables': tables,
53
+ })
54
+
55
+ for rm in root.findall('resultMap'):
56
+ rm_id = rm.get('id', '')
57
+ cols = []
58
+ for child_tag in ('id', 'result'):
59
+ for child in rm.findall(child_tag):
60
+ cols.append({
61
+ 'property': child.get('property', ''),
62
+ 'column': child.get('column', ''),
63
+ })
64
+ if rm.get('type'):
65
+ result_types.add(rm.get('type'))
66
+ result_maps[rm_id] = cols
67
+
68
+ return {
69
+ 'namespace': namespace,
70
+ 'namespace_short': namespace_short,
71
+ 'statements': statements,
72
+ 'result_maps': result_maps,
73
+ 'result_types': list(result_types),
74
+ }
75
+
76
+
77
+ def _extract_sql_text(node) -> str:
78
+ """提取 statement 节点的完整 SQL 文本(含 <if>/<foreach> 展开)。"""
79
+ parts = []
80
+ if node.text:
81
+ parts.append(node.text)
82
+ for child in node:
83
+ # <if>/<foreach>/<choose>/<where>/<set>/<trim> 等, 取 text + tail
84
+ if child.text:
85
+ parts.append(child.text)
86
+ # 递归子节点 (foreach 里可能有 if)
87
+ for sub in child:
88
+ if sub.text:
89
+ parts.append(sub.text)
90
+ if sub.tail:
91
+ parts.append(sub.tail)
92
+ if child.tail:
93
+ parts.append(child.tail)
94
+ return ' '.join(p.strip() for p in parts if p and p.strip())
95
+
96
+
97
+ def _empty_result():
98
+ return {'namespace': '', 'namespace_short': '', 'statements': [],
99
+ 'result_maps': {}, 'result_types': []}