@hupan56/wlkj 3.1.32 → 3.3.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 (178) hide show
  1. package/bin/cli.js +117 -0
  2. package/package.json +1 -1
  3. package/templates/qoder/agents/insight-planning.md +67 -67
  4. package/templates/qoder/agents/prd-reference.md +47 -47
  5. package/templates/qoder/commands/optional/wl-insight.md +4 -4
  6. package/templates/qoder/commands/optional/wl-report.md +1 -1
  7. package/templates/qoder/commands/optional/wl-spec.md +23 -3
  8. package/templates/qoder/commands/optional/wl-status.md +12 -1
  9. package/templates/qoder/commands/wl-code.md +138 -7
  10. package/templates/qoder/commands/wl-commit.md +12 -1
  11. package/templates/qoder/commands/wl-design.md +70 -6
  12. package/templates/qoder/commands/wl-init.md +27 -0
  13. package/templates/qoder/commands/wl-prd.md +230 -15
  14. package/templates/qoder/commands/wl-req.md +10 -3
  15. package/templates/qoder/commands/wl-search.md +74 -20
  16. package/templates/qoder/commands/wl-task.md +3 -3
  17. package/templates/qoder/commands/wl-test.md +17 -2
  18. package/templates/qoder/contracts/CHANGELOG.md +418 -0
  19. package/templates/qoder/contracts/README.md +180 -0
  20. package/templates/qoder/contracts/code.md +82 -0
  21. package/templates/qoder/contracts/commit.md +86 -0
  22. package/templates/qoder/contracts/contract-header.md +76 -0
  23. package/templates/qoder/contracts/design.md +106 -0
  24. package/templates/qoder/contracts/fallback.md +126 -0
  25. package/templates/qoder/contracts/isolation.md +119 -0
  26. package/templates/qoder/contracts/prd.md +118 -0
  27. package/templates/qoder/contracts/schemas/design-spec.schema.json +46 -0
  28. package/templates/qoder/contracts/schemas/prd.schema.json +36 -0
  29. package/templates/qoder/contracts/schemas/test-cases.schema.json +40 -0
  30. package/templates/qoder/contracts/spec.md +116 -0
  31. package/templates/qoder/contracts/task.md +125 -0
  32. package/templates/qoder/contracts/test.md +112 -0
  33. package/templates/qoder/hooks/post-tool-use.py +61 -0
  34. package/templates/qoder/hooks/session-start.py +34 -66
  35. package/templates/qoder/hooks/stop-eval.py +47 -0
  36. package/templates/qoder/rules/wl-pipeline.md +37 -0
  37. package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
  38. package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
  39. package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
  40. package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
  41. package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
  42. package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
  43. package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
  44. package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
  45. package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
  46. package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
  47. package/templates/qoder/scripts/capability/caps/__init__.py +1 -1
  48. package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
  49. package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
  50. package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
  51. package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
  52. package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
  53. package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
  54. package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
  55. package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
  56. package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
  57. package/templates/qoder/scripts/capability/caps/memory.py +1 -1
  58. package/templates/qoder/scripts/capability/registry.py +21 -23
  59. package/templates/qoder/scripts/capability/registry_mcp.py +69 -5
  60. package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
  61. package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
  62. package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
  63. package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +11 -0
  64. package/templates/qoder/scripts/deployment/setup/wlkj_shim.py +104 -0
  65. package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
  66. package/templates/qoder/scripts/domain/deployment/deploy_to_test.py +298 -0
  67. package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
  68. package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
  69. package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
  70. package/templates/qoder/scripts/domain/integration/return_to_platform.py +392 -0
  71. package/templates/qoder/scripts/domain/integration/spec_upload.py +209 -0
  72. package/templates/qoder/scripts/domain/kg/build/kg_build.py +268 -25
  73. package/templates/qoder/scripts/domain/kg/build/kg_incremental.py +108 -3
  74. package/templates/qoder/scripts/domain/kg/build/kg_signatures.py +169 -0
  75. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
  76. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
  77. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
  78. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
  79. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
  80. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
  81. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
  82. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
  83. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
  84. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
  85. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
  86. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
  87. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
  88. package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
  89. package/templates/qoder/scripts/domain/kg/extract/extract.py.bak +430 -0
  90. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
  91. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
  92. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
  93. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
  94. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
  95. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
  96. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
  97. package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
  98. package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
  99. package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
  100. package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
  101. package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
  102. package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
  103. package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
  104. package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
  105. package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
  106. package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
  107. package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
  108. package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
  109. package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
  110. package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
  111. package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
  112. package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
  113. package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
  114. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
  115. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
  116. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
  117. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
  118. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
  119. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
  120. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
  121. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
  122. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
  123. package/templates/qoder/scripts/domain/kg/extract/ts_extract.py +111 -0
  124. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
  125. package/templates/qoder/scripts/domain/kg/kg.py +42 -5
  126. package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
  127. package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
  128. package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
  129. package/templates/qoder/scripts/domain/kg/storage/kg_duckdb.py +43 -0
  130. package/templates/qoder/scripts/domain/kg/switch_project.py +159 -0
  131. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
  132. package/templates/qoder/scripts/domain/requirement/req.py +134 -28
  133. package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/domain/task/wlkj_panel.py +1348 -0
  135. package/templates/qoder/scripts/engine/poller.py +219 -0
  136. package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
  137. package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
  138. package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
  139. package/templates/qoder/scripts/foundation/core/paths.py +102 -0
  140. package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
  141. package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
  142. package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
  143. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +24 -11
  144. package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
  145. package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
  146. package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
  147. package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
  148. package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
  149. package/templates/qoder/scripts/protocol/transports/http.py +7 -1
  150. package/templates/qoder/scripts/validation/eval/qwork_harness.py +1 -1
  151. package/templates/qoder/scripts/validation/eval/report-commands.md +2 -2
  152. package/templates/qoder/settings.json +27 -9
  153. package/templates/qoder/skills/design-import/SKILL.md +3 -3
  154. package/templates/qoder/skills/design-review/SKILL.md +1 -1
  155. package/templates/qoder/skills/prd-generator/SKILL.md +4 -4
  156. package/templates/qoder/skills/prd-review/SKILL.md +1 -1
  157. package/templates/qoder/skills/prototype-generator/SKILL.md +3 -3
  158. package/templates/qoder/skills/spec-coder/SKILL.md +1 -1
  159. package/templates/qoder/skills/spec-generator/SKILL.md +80 -23
  160. package/templates/qoder/skills/test-generator/SKILL.md +1 -1
  161. package/templates/qoder/skills/wl-code/SKILL.md +13 -1
  162. package/templates/qoder/skills/wl-commit/SKILL.md +1 -1
  163. package/templates/qoder/skills/wl-design/SKILL.md +6 -6
  164. package/templates/qoder/skills/wl-init/SKILL.md +2 -2
  165. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  166. package/templates/qoder/skills/wl-prd/SKILL.md +60 -0
  167. package/templates/qoder/skills/wl-report/SKILL.md +2 -2
  168. package/templates/qoder/skills/wl-search/SKILL.md +1 -1
  169. package/templates/qoder/skills/wl-spec/SKILL.md +2 -2
  170. package/templates/qoder/skills/wl-status/SKILL.md +2 -2
  171. package/templates/qoder/skills/wl-task/SKILL.md +3 -3
  172. package/templates/qoder/skills/wl-test/SKILL.md +2 -2
  173. package/templates/qoder/templates/spec-template.md +124 -0
  174. package/templates/root/AGENTS.md +41 -14
  175. package/templates/qoder/scripts/domain/task/zentao_panel.py +0 -451
  176. package/templates/qoder/skills/wl-prd-full/SKILL.md +0 -121
  177. package/templates/qoder/skills/wl-prd-quick/SKILL.md +0 -50
  178. package/templates/qoder/skills/wl-prd-review/SKILL.md +0 -47
@@ -0,0 +1,141 @@
1
+ # -*- coding: utf-8 -*-
2
+ """类级注解提取器: Spring + Dubbo + Sa-Token + MyBatis-Plus。
3
+
4
+ 一次遍历类节点 → 识别 4 类后端组件 + 注入关系。
5
+ """
6
+ from __future__ import annotations
7
+ from . import _parser as P
8
+
9
+
10
+ def extract(root, repo_id: str, filepath: str):
11
+ """提取类级实体 + 边。
12
+
13
+ 返回 (entities: list[dict], edges: list[dict])。
14
+ 实体类型: CONTROLLER / SERVICE / MAPPER / ENTITY / CONFIG
15
+ 边类型: implements / injects
16
+ """
17
+ entities, edges = [], []
18
+ if root is None:
19
+ return entities, edges
20
+
21
+ for cls in P.get_class_decls(root):
22
+ name = P.get_class_name(cls)
23
+ if not name:
24
+ continue
25
+ anns = P.get_annotations(cls)
26
+ implements = P.get_super_interfaces(cls)
27
+ superclass = P.get_superclass(cls)
28
+
29
+ # 按注解判定类型
30
+ etype = _classify(name, anns)
31
+ if not etype:
32
+ continue
33
+
34
+ # ===== 实体 =====
35
+ props = {
36
+ 'annotations': sorted(anns.keys()),
37
+ 'file': filepath,
38
+ }
39
+ if implements:
40
+ props['implements'] = implements
41
+ if superclass:
42
+ props['extends'] = superclass
43
+ # @TableName → 把表名塞 props
44
+ if 'TableName' in anns and anns['TableName']:
45
+ props['table_name'] = anns['TableName'].strip('"')
46
+ # @RequestMapping 类级 → 基础路径
47
+ if 'RequestMapping' in anns and anns['RequestMapping']:
48
+ props['base_path'] = anns['RequestMapping'].strip('"')
49
+
50
+ entities.append({
51
+ 'id': P.entity_id(repo_id, _etype_prefix(etype), name),
52
+ 'repo_id': repo_id,
53
+ 'type': etype,
54
+ 'canonical': name,
55
+ 'cn': '',
56
+ 'props': props,
57
+ })
58
+
59
+ # ===== implements 边 (ServiceImpl → 接口) =====
60
+ for iface in implements:
61
+ edges.append({
62
+ 'from_id': P.entity_id(repo_id, _etype_prefix(etype), name),
63
+ 'to_id': '%s:%s:%s' % (_etype_prefix(etype), repo_id, iface),
64
+ 'edge_type': 'implements',
65
+ 'from_repo': repo_id, 'to_repo': repo_id,
66
+ 'confidence': 1.0, 'source': 'explicit',
67
+ })
68
+
69
+ # ===== extends 边 =====
70
+ if superclass and superclass != 'Object':
71
+ edges.append({
72
+ 'from_id': P.entity_id(repo_id, _etype_prefix(etype), name),
73
+ 'to_id': '%s:%s:%s' % (_etype_prefix(etype), repo_id, superclass),
74
+ 'edge_type': 'extends',
75
+ 'from_repo': repo_id, 'to_repo': repo_id,
76
+ 'confidence': 1.0, 'source': 'explicit',
77
+ })
78
+
79
+ # ===== 注入边: @Autowired / @Resource / @DubboReference + final 字段 =====
80
+ has_required_args = 'RequiredArgsConstructor' in anns
81
+ for f in P.get_fields(cls):
82
+ fan = f['annotations']
83
+ is_inject = ('Autowired' in fan or 'Resource' in fan
84
+ or 'DubboReference' in fan
85
+ or (has_required_args and f['is_final']))
86
+ if is_inject and f['type']:
87
+ edge_type = 'dubbo_injects' if 'DubboReference' in fan else 'injects'
88
+ edges.append({
89
+ 'from_id': P.entity_id(repo_id, _etype_prefix(etype), name),
90
+ 'to_id': '%s:%s:%s' % (_etype_prefix(_guess_dep_type(f['type'])), repo_id, f['type']),
91
+ 'edge_type': edge_type,
92
+ 'from_repo': repo_id, 'to_repo': repo_id,
93
+ 'confidence': 1.0, 'source': 'explicit',
94
+ 'props': {'field': f['name'], 'dep_type': f['type'],
95
+ 'via_dubbo': 'DubboReference' in fan},
96
+ })
97
+
98
+ return entities, edges
99
+
100
+
101
+ def _classify(name: str, anns: dict) -> str | None:
102
+ """按注解 + 命名规则判定实体类型。"""
103
+ if 'RestController' in anns or 'Controller' in anns:
104
+ return 'CONTROLLER'
105
+ if 'DubboService' in anns or 'Service' in anns:
106
+ return 'SERVICE'
107
+ if 'Mapper' in anns or (name.endswith('Mapper') and 'Repository' in anns):
108
+ return 'MAPPER'
109
+ if 'TableName' in anns:
110
+ return 'ENTITY'
111
+ # 命名兜底 (没注解但命名特征明显的)
112
+ if name.endswith('Mapper') and 'interface' not in str(anns):
113
+ return 'MAPPER'
114
+ if name.endswith('Controller'):
115
+ return 'CONTROLLER'
116
+ if name.endswith('ServiceImpl'):
117
+ return 'SERVICE'
118
+ if 'Configuration' in anns:
119
+ return 'CONFIG'
120
+ return None
121
+
122
+
123
+ def _etype_prefix(etype: str) -> str:
124
+ """entity_id 前缀(区分 Java 后端各层)。
125
+ SVC=Service, MAP=Mapper, ENT=Entity, CTRL=Controller, CFG=Config
126
+ """
127
+ return {
128
+ 'CONTROLLER': 'CTRL', 'SERVICE': 'SVC', 'MAPPER': 'MAP',
129
+ 'ENTITY': 'ENT', 'CONFIG': 'CFG',
130
+ }.get(etype, 'J')
131
+
132
+
133
+ def _guess_dep_type(simple_type: str) -> str:
134
+ """根据字段类型名猜它属于哪层(用于注入边的 to_id 前缀)。"""
135
+ if simple_type.endswith('Mapper'):
136
+ return 'MAPPER'
137
+ if simple_type.endswith('ServiceImpl') or simple_type.endswith('Service'):
138
+ return 'SERVICE'
139
+ if simple_type.endswith('Controller'):
140
+ return 'CONTROLLER'
141
+ return 'SVC' # 默认当 Service
@@ -0,0 +1,148 @@
1
+ # -*- coding: utf-8 -*-
2
+ """领域对象提取器: Entity/BO/VO/Form → DTO/ENTITY + 字段→列映射 + 校验规则。
3
+
4
+ 处理 class_extractor 没识别为后端组件的类(DTO、BO、VO、Form)。
5
+ 关键:
6
+ - @TableName 的类 → ENTITY + 建 TABLE 实体 + maps_to 边
7
+ - 字段的 @TableField("user_id") → COLUMN 实体 + 列名映射
8
+ - 字段的 @NotNull/@Size → VALIDATION 实体 + validates 边
9
+ - 字段 javadoc /** 用户ID */ → cn (字段中文含义, 喂给 query_schema)
10
+ """
11
+ from __future__ import annotations
12
+ from . import _parser as P
13
+
14
+ _VALIDATION = {'NotNull', 'NotBlank', 'NotEmpty', 'Size', 'Min', 'Max',
15
+ 'Email', 'Pattern', 'Positive', 'Negative', 'Digits'}
16
+
17
+
18
+ def extract(root, repo_id: str, filepath: str):
19
+ """提取领域对象 + 字段映射 + 校验规则。
20
+
21
+ Returns: (entities, edges)
22
+ """
23
+ entities, edges = [], []
24
+ if root is None:
25
+ return entities, edges
26
+
27
+ for cls in P.get_class_decls(root):
28
+ name = P.get_class_name(cls)
29
+ if not name:
30
+ continue
31
+ anns = P.get_annotations(cls)
32
+ fields = P.get_fields(cls)
33
+ # enum 的常量也算"字段" (枚举值)
34
+ is_enum = cls.type == 'enum_declaration'
35
+ if is_enum:
36
+ enum_consts = P.find_all(cls, 'enum_constant')
37
+ for ec in enum_consts:
38
+ ec_name = P._text(P.find_first(ec, 'identifier') or ec)
39
+ if ec_name:
40
+ fields.append({'name': ec_name, 'type': name, 'annotations': {},
41
+ 'is_final': True, 'is_static': True, 'comment': ''})
42
+ # 路径判定 (放宽: domain/dto/vo 目录下的类即使无字段也算领域对象)
43
+ fp_norm = filepath.replace('\\', '/').lower()
44
+ in_domain = '/domain/' in fp_norm
45
+ in_dto_dir = any('/'+d+'/' in fp_norm for d in ['vo','bo','dto','form','qry','param','query'])
46
+ if not fields and not (in_domain or in_dto_dir or is_enum):
47
+ continue # 非领域目录且无字段 → 跳过
48
+
49
+ # @TableName → ENTITY + TABLE 实体 + maps_to
50
+ table_name = ''
51
+ if 'TableName' in anns and anns['TableName']:
52
+ table_name = anns['TableName'].strip('"').strip("'")
53
+
54
+ # 判定是否领域对象 (有 @TableName 或 命名特征 或 在 domain/dto 目录下)
55
+ is_entity = bool(table_name)
56
+ is_dto = (name.endswith('Vo') or name.endswith('BO') or name.endswith('Bo')
57
+ or name.endswith('Form') or name.endswith('DTO') or name.endswith('Dto')
58
+ or name.endswith('Body') or name.endswith('Request')
59
+ or name.endswith('Response') or name.endswith('Result'))
60
+ # 放宽: domain/ 或 dto 子目录下的类/枚举默认归 DTO
61
+ if not is_entity and not is_dto and (in_domain or in_dto_dir or is_enum):
62
+ is_dto = True
63
+
64
+ if not is_entity and not is_dto:
65
+ continue
66
+
67
+ dto_type = 'ENTITY' if is_entity else 'DTO'
68
+ dto_id = ('ENT:%s:%s' if is_entity else 'DTO:%s:%s') % (repo_id, name)
69
+ field_infos = []
70
+ for f in fields:
71
+ field_infos.append({
72
+ 'name': f['name'], 'type': f['type'],
73
+ 'comment': f.get('comment', ''),
74
+ 'column': _field_column(f, table_name),
75
+ })
76
+ entities.append({
77
+ 'id': dto_id, 'repo_id': repo_id, 'type': dto_type,
78
+ 'canonical': name, 'cn': '',
79
+ 'props': {'fields': field_infos, 'table': table_name,
80
+ 'file': filepath},
81
+ })
82
+
83
+ # ENTITY → TABLE 实体 + maps_to 边
84
+ if is_entity and table_name:
85
+ tbl_id = 'TBL:%s:%s' % (repo_id, table_name)
86
+ entities.append({
87
+ 'id': tbl_id, 'repo_id': repo_id, 'type': 'TABLE',
88
+ 'canonical': table_name, 'cn': '',
89
+ 'props': {'from_entity': name},
90
+ })
91
+ edges.append({
92
+ 'from_id': dto_id, 'to_id': tbl_id,
93
+ 'edge_type': 'maps_to',
94
+ 'from_repo': repo_id, 'to_repo': repo_id,
95
+ 'confidence': 1.0, 'source': 'explicit',
96
+ })
97
+
98
+ # 字段 → COLUMN + VALIDATION
99
+ for f in fields:
100
+ col_name = _field_column(f, table_name)
101
+ if not col_name:
102
+ continue
103
+ col_id = 'COL:%s:%s.%s' % (repo_id, table_name, col_name)
104
+ entities.append({
105
+ 'id': col_id, 'repo_id': repo_id, 'type': 'COLUMN',
106
+ 'canonical': col_name, 'cn': f.get('comment', ''),
107
+ 'props': {'table': table_name, 'field': f['name'],
108
+ 'java_type': f['type']},
109
+ })
110
+ # 校验
111
+ for v in _VALIDATION:
112
+ if v in f['annotations']:
113
+ val_id = 'VAL:%s:%s.%s.%s' % (repo_id, table_name, col_name, v)
114
+ entities.append({
115
+ 'id': val_id, 'repo_id': repo_id, 'type': 'VALIDATION',
116
+ 'canonical': '%s.%s.%s' % (table_name, col_name, v), 'cn': '',
117
+ 'props': {'rule': v, 'arg': f['annotations'][v],
118
+ 'table': table_name, 'column': col_name},
119
+ })
120
+ edges.append({
121
+ 'from_id': val_id, 'to_id': col_id,
122
+ 'edge_type': 'validates',
123
+ 'from_repo': repo_id, 'to_repo': repo_id,
124
+ 'confidence': 1.0, 'source': 'explicit',
125
+ })
126
+
127
+ return entities, edges
128
+
129
+
130
+ def _field_column(field: dict, table_name: str) -> str:
131
+ """从字段提取数据库列名。
132
+ @TableField("user_id") → 'user_id'
133
+ @TableId → 'id' (或 value)
134
+ 无注解 → 驼峰转下划线 (userId → user_id)
135
+ """
136
+ fan = field['annotations']
137
+ if 'TableField' in fan and fan['TableField']:
138
+ return fan['TableField'].strip('"').strip("'").split('.')[0]
139
+ if 'TableId' in fan:
140
+ return fan['TableId'].strip('"').strip("'") or 'id'
141
+ # 驼峰转下划线
142
+ name = field['name']
143
+ out = []
144
+ for i, c in enumerate(name):
145
+ if c.isupper() and i > 0:
146
+ out.append('_')
147
+ out.append(c.lower())
148
+ return ''.join(out)
@@ -0,0 +1,33 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Dubbo RPC 提取器(@DubboService/@DubboReference)。
3
+
4
+ 职责: 识别 Dubbo 服务暴露/消费关系(跨进程 RPC 边)。
5
+ 本项目用 Dubbo 不是纯 Spring Autowired, DubboReference 字段是关键注入来源。
6
+ 共享 class_extractor 的提取逻辑, 这里按 Dubbo 关注点导出。
7
+ """
8
+ from __future__ import annotations
9
+ from . import _parser as P
10
+ from . import class_extractor as _ce
11
+
12
+ DUBBO_EXPOSE = {'DubboService'}
13
+ DUBBO_CONSUME = {'DubboReference'}
14
+
15
+
16
+ def is_dubbo_component(annotations: dict) -> bool:
17
+ """类是否 @DubboService 暴露。"""
18
+ return bool(set(annotations.keys()) & DUBBO_EXPOSE)
19
+
20
+
21
+ def has_dubbo_reference(class_annotations: dict, fields: list) -> bool:
22
+ """类或字段是否有 @DubboReference 消费。"""
23
+ if set(class_annotations.keys()) & DUBBO_CONSUME:
24
+ return True
25
+ for f in fields:
26
+ if set(f.get('annotations', {}).keys()) & DUBBO_CONSUME:
27
+ return True
28
+ return False
29
+
30
+
31
+ def extract(root, repo_id: str, filepath: str):
32
+ """Dubbo 类级提取(转发到 class_extractor, 行为一致)。"""
33
+ return _ce.extract(root, repo_id, filepath)
@@ -0,0 +1,36 @@
1
+ # -*- coding: utf-8 -*-
2
+ """HTTP 端点提取器(@GetMapping/@PostMapping/@PutMapping/@DeleteMapping)。
3
+
4
+ 职责: 从方法注解提取 HTTP 端点 → ENDPOINT 实体 + handled_by 边。
5
+ 共享 member_extractor 的方法遍历逻辑。
6
+ """
7
+ from __future__ import annotations
8
+ from . import _parser as P
9
+ from . import member_extractor as _me
10
+
11
+ # 端点注解 → HTTP 动词
12
+ ENDPOINT_ANN_TO_VERB = {
13
+ 'GetMapping': 'GET', 'PostMapping': 'POST',
14
+ 'PutMapping': 'PUT', 'DeleteMapping': 'DELETE',
15
+ 'PatchMapping': 'PATCH', 'RequestMapping': 'ANY',
16
+ }
17
+
18
+
19
+ def is_endpoint(annotations: dict) -> bool:
20
+ """方法是否是 HTTP 端点。"""
21
+ return bool(set(annotations.keys()) & set(ENDPOINT_ANN_TO_VERB.keys()))
22
+
23
+
24
+ def extract_path(method_annotations: dict, class_annotations: dict) -> str:
25
+ """提取端点路径(类级 @RequestMapping + 方法级 @GetMapping)。"""
26
+ return _me._extract_endpoint_path(method_annotations, class_annotations)
27
+
28
+
29
+ def extract_verb(ann_names: list) -> str:
30
+ """提取 HTTP 动词。"""
31
+ return _me._extract_http_verb(ann_names)
32
+
33
+
34
+ def extract(root, repo_id: str, filepath: str, owner_classes: list = None):
35
+ """端点提取(转发到 member_extractor, ENDPOINT 实体在那里产出)。"""
36
+ return _me.extract(root, repo_id, filepath, owner_classes or [])
@@ -0,0 +1,110 @@
1
+ # -*- coding: utf-8 -*-
2
+ """D1-② 提取 Java 类级 Javadoc → 填 entities.cn。
3
+
4
+ 读 Java 文件的 /** 类注释 */ 第一句, 匹配到 entities 表的 SERVICE/CONTROLLER/MAPPER/ENTITY/DTO 实体。
5
+ 免 LLM, 纯文本解析 (tree-sitter 提注释 + 正则取首句)。
6
+ """
7
+ from __future__ import annotations
8
+ import os
9
+ import sys
10
+ import re
11
+ import glob
12
+ import time
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); break
19
+ _THIS = _p
20
+
21
+ # 类级 Javadoc 正则: /** ... */ 紧跟 class/interface/enum
22
+ _RE_JAVADOC = re.compile(
23
+ r'/\*\*\s*\n((?:\s*\*.*\n)+?)\s*\*/\s*\n\s*((?:public|private|abstract|final|\s)*)'
24
+ r'(?:class|interface|enum)\s+(\w+)',
25
+ re.MULTILINE
26
+ )
27
+
28
+
29
+ def extract_javadoc_first_line(content: str) -> dict:
30
+ """从 Java 文件提取 {类名: javadoc首句}。
31
+
32
+ 只取类级 /** */ (紧跟 class/interface/enum 的), 不取方法级。
33
+ 首句 = 去掉 * 前缀后的第一行非空文本。
34
+ """
35
+ out = {}
36
+ for m in _RE_JAVADOC.finditer(content):
37
+ cls_name = m.group(3)
38
+ javadoc_body = m.group(1)
39
+ # 清理 * 前缀 + @author/@param 等标签
40
+ lines = []
41
+ for line in javadoc_body.split('\n'):
42
+ line = line.strip().lstrip('*').strip()
43
+ if not line or line.startswith('@'):
44
+ continue
45
+ lines.append(line)
46
+ if lines:
47
+ out[cls_name] = lines[0][:80] # 首句, 截断80字
48
+ return out
49
+
50
+
51
+ def sync_javadoc_to_cn(repo_dir: str, repo_id: str, logger=print):
52
+ """扫 repo 下所有 Java 文件 → 提 Javadoc → 填 entities.cn。
53
+
54
+ 只填 cn 为空的实体 (不覆盖已有)。
55
+ """
56
+ from domain.kg.extract.java import pg_upsert
57
+ from sqlalchemy import text
58
+
59
+ t0 = time.time()
60
+ java_files = glob.glob(os.path.join(repo_dir, '**', '*.java'), recursive=True)
61
+ java_files = [f for f in java_files if '/test/' not in f.replace('\\', '/').lower()
62
+ and not f.endswith('Test.java')]
63
+ logger(' [javadoc] 扫描 %d 个 .java (repo=%s)' % (len(java_files), repo_id))
64
+
65
+ # 提取所有类的 Javadoc
66
+ class_javadoc = {} # {ClassName: javadoc首句}
67
+ for jf in java_files:
68
+ try:
69
+ content = open(jf, encoding='utf-8', errors='replace').read()
70
+ class_javadoc.update(extract_javadoc_first_line(content))
71
+ except Exception:
72
+ pass
73
+ logger(' [javadoc] 提取到 %d 个类的 Javadoc' % len(class_javadoc))
74
+
75
+ if not class_javadoc:
76
+ return {'javadoc_extracted': 0, 'cn_filled': 0}
77
+
78
+ # 填 entities.cn (只填空的)
79
+ engine, _ = pg_upsert._get_pg_engine()
80
+ if engine is None:
81
+ return {'error': 'PG 不可用'}
82
+ ok = 0
83
+ with engine.begin() as conn:
84
+ for cls_name, javadoc in class_javadoc.items():
85
+ # 匹配 entities.canonical = cls_name (各种类型)
86
+ r = conn.execute(text("""
87
+ UPDATE entities SET cn = :cn
88
+ WHERE canonical = :name AND (cn IS NULL OR cn = '')
89
+ AND type IN ('SERVICE','CONTROLLER','MAPPER','ENTITY','DTO','CONFIG')
90
+ """), {'cn': javadoc, 'name': cls_name})
91
+ ok += r.rowcount or 0
92
+ dt = time.time() - t0
93
+ logger(' [javadoc] 填充 %d 条 cn / %.1fs' % (ok, dt))
94
+ return {'javadoc_extracted': len(class_javadoc), 'cn_filled': ok, 'seconds': round(dt, 1)}
95
+
96
+
97
+ if __name__ == '__main__':
98
+ import argparse, json
99
+ ap = argparse.ArgumentParser()
100
+ ap.add_argument('--repo-dir', required=True)
101
+ ap.add_argument('--repo-id', default='')
102
+ args = ap.parse_args()
103
+ try:
104
+ from dotenv import load_dotenv
105
+ for p in ['wlinkj-workspace/backend/.env', '.env']:
106
+ if os.path.isfile(p): load_dotenv(p); break
107
+ except: pass
108
+ rid = args.repo_id or os.path.basename(args.repo_dir.rstrip('/\\'))
109
+ r = sync_javadoc_to_cn(args.repo_dir, rid)
110
+ print(json.dumps(r, ensure_ascii=False, indent=2))
@@ -0,0 +1,150 @@
1
+ # -*- coding: utf-8 -*-
2
+ """D1-③ LLM 批量补 cn: 对没有 Javadoc 的实体用 qwen-plus 推断中文含义。
3
+
4
+ 批量 10 个/次, 并发 4 线程。
5
+ """
6
+ from __future__ import annotations
7
+ import os
8
+ import sys
9
+ import json
10
+ import time
11
+ import urllib.request
12
+ from concurrent.futures import ThreadPoolExecutor
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); break
19
+ _THIS = _p
20
+
21
+ _CN_PROMPT = """你是 Java 业务架构师。根据类名推断中文业务含义。
22
+ 对每个类, 返回一行 "类名|中文含义" (含义不超过20字, 描述这个类做什么)。
23
+ 只返回结果, 不要解释。
24
+
25
+ 示例:
26
+ AssetController|资产管理控制器,提供资产CRUD接口
27
+ QrCodeLoginServiceImpl|二维码登录服务实现
28
+ EmployeeOnline|员工在线信息实体
29
+
30
+ 类列表:
31
+ %s"""
32
+
33
+
34
+ def _call_qwen(prompt: str, timeout: int = 30) -> str:
35
+ key = os.environ.get('DASHSCOPE_API_KEY', '')
36
+ base = os.environ.get('ALI_CHAT_BASE', '').rstrip('/')
37
+ model = os.environ.get('ALI_CHAT_MODEL', 'qwen-plus')
38
+ if not key or not base:
39
+ return ''
40
+ req = urllib.request.Request(
41
+ base + '/chat/completions',
42
+ data=json.dumps({'model': model, 'messages': [{'role': 'user', 'content': prompt}],
43
+ 'max_tokens': 800, 'temperature': 0.1}).encode('utf-8'),
44
+ headers={'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json'},
45
+ )
46
+ try:
47
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
48
+ return json.loads(resp.read())['choices'][0]['message']['content']
49
+ except Exception:
50
+ return ''
51
+
52
+
53
+ def _parse_cn_response(text: str) -> dict:
54
+ """解析 "类名|中文" 格式 → {类名: 中文}"""
55
+ out = {}
56
+ for line in (text or '').split('\n'):
57
+ line = line.strip()
58
+ if '|' not in line:
59
+ continue
60
+ parts = line.split('|', 1)
61
+ if len(parts) == 2:
62
+ cls = parts[0].strip()
63
+ cn = parts[1].strip()[:80]
64
+ if cls and cn:
65
+ out[cls] = cn
66
+ return out
67
+
68
+
69
+ def llm_fill_cn(batch_size: int = 10, limit: int = 3000, logger=print):
70
+ """批量 LLM 补 cn。
71
+
72
+ Args:
73
+ batch_size: 每次 API 传几个类 (10)
74
+ limit: 总上限 (避免全量 5000+ 调太久)
75
+ """
76
+ from domain.kg.extract.java import pg_upsert
77
+ from sqlalchemy import text
78
+
79
+ engine, _ = pg_upsert._get_pg_engine()
80
+ if engine is None:
81
+ return {'error': 'PG 不可用'}
82
+
83
+ # 取 cn 为空的后端实体
84
+ with engine.connect() as conn:
85
+ rows = conn.execute(text("""
86
+ SELECT id, canonical, type FROM entities
87
+ WHERE type IN ('SERVICE','CONTROLLER','MAPPER','ENTITY','DTO','TABLE')
88
+ AND (cn IS NULL OR cn = '')
89
+ LIMIT :lim
90
+ """), {'lim': limit}).all()
91
+ logger(' [llm-cn] %d 个实体待补 cn' % len(rows))
92
+ if not rows:
93
+ return {'filled': 0}
94
+
95
+ # 分批
96
+ batches = [rows[i:i+batch_size] for i in range(0, len(rows), batch_size)]
97
+
98
+ def process_batch(batch):
99
+ """处理一批: 构造prompt → 调LLM → 解析 → 返回 [(id, cn)]"""
100
+ names = [r[1] for r in batch]
101
+ prompt = _CN_PROMPT % '\n'.join(names)
102
+ resp = _call_qwen(prompt)
103
+ cn_map = _parse_cn_response(resp)
104
+ results = []
105
+ for eid, canonical, etype in batch:
106
+ cn = cn_map.get(canonical)
107
+ if cn:
108
+ results.append((eid, cn))
109
+ return results
110
+
111
+ # 并发 4 线程
112
+ t0 = time.time()
113
+ all_updates = []
114
+ with ThreadPoolExecutor(max_workers=4) as ex:
115
+ futures = [ex.submit(process_batch, b) for b in batches]
116
+ for i, fut in enumerate(futures):
117
+ try:
118
+ all_updates += fut.result()
119
+ except Exception:
120
+ pass
121
+ if (i+1) % 10 == 0:
122
+ logger(' [llm-cn] %d/%d 批 (cn=%d)' % (i+1, len(batches), len(all_updates)))
123
+
124
+ # 批量写 PG
125
+ ok = 0
126
+ with engine.begin() as conn:
127
+ for eid, cn in all_updates:
128
+ try:
129
+ conn.execute(text("UPDATE entities SET cn=:cn WHERE id=:id AND (cn IS NULL OR cn='')"),
130
+ {'cn': cn, 'id': eid})
131
+ ok += 1
132
+ except Exception:
133
+ pass
134
+ dt = time.time() - t0
135
+ logger(' [llm-cn] 填充 %d 条 cn / %.1fs' % (ok, dt))
136
+ return {'filled': ok, 'seconds': round(dt, 1)}
137
+
138
+
139
+ if __name__ == '__main__':
140
+ import argparse
141
+ ap = argparse.ArgumentParser()
142
+ ap.add_argument('--limit', type=int, default=3000)
143
+ args = ap.parse_args()
144
+ try:
145
+ from dotenv import load_dotenv
146
+ for p in ['wlinkj-workspace/backend/.env', '.env']:
147
+ if os.path.isfile(p): load_dotenv(p); break
148
+ except: pass
149
+ r = llm_fill_cn(limit=args.limit)
150
+ print(json.dumps(r, ensure_ascii=False, indent=2))