@hupan56/wlkj 2.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.
- package/bin/cli.js +213 -0
- package/package.json +11 -0
- package/templates/cli.js +198 -0
- package/templates/qoder/commands/wl-code.md +43 -0
- package/templates/qoder/commands/wl-commit.md +30 -0
- package/templates/qoder/commands/wl-init.md +80 -0
- package/templates/qoder/commands/wl-insight.md +51 -0
- package/templates/qoder/commands/wl-prd.md +199 -0
- package/templates/qoder/commands/wl-report.md +166 -0
- package/templates/qoder/commands/wl-search.md +52 -0
- package/templates/qoder/commands/wl-spec.md +18 -0
- package/templates/qoder/commands/wl-status.md +51 -0
- package/templates/qoder/commands/wl-task.md +71 -0
- package/templates/qoder/commands/wl-test.md +42 -0
- package/templates/qoder/config.toml +5 -0
- package/templates/qoder/config.yaml +141 -0
- package/templates/qoder/hooks/inject-workflow-state.py +117 -0
- package/templates/qoder/hooks/session-start.py +204 -0
- package/templates/qoder/rules/wl-pipeline.md +105 -0
- package/templates/qoder/scripts/add_session.py +245 -0
- package/templates/qoder/scripts/benchmark.py +209 -0
- package/templates/qoder/scripts/build_style_index.py +268 -0
- package/templates/qoder/scripts/code_index.py +41 -0
- package/templates/qoder/scripts/collect_prds.py +31 -0
- package/templates/qoder/scripts/common/__init__.py +0 -0
- package/templates/qoder/scripts/common/active_task.py +230 -0
- package/templates/qoder/scripts/common/atomicio.py +172 -0
- package/templates/qoder/scripts/common/developer.py +161 -0
- package/templates/qoder/scripts/common/eval_api.py +144 -0
- package/templates/qoder/scripts/common/feishu.py +278 -0
- package/templates/qoder/scripts/common/filelock.py +211 -0
- package/templates/qoder/scripts/common/identity.py +285 -0
- package/templates/qoder/scripts/common/mentions.py +134 -0
- package/templates/qoder/scripts/common/paths.py +311 -0
- package/templates/qoder/scripts/common/reqid.py +218 -0
- package/templates/qoder/scripts/common/search_engine.py +205 -0
- package/templates/qoder/scripts/common/task_utils.py +342 -0
- package/templates/qoder/scripts/common/terms.py +234 -0
- package/templates/qoder/scripts/common/utf8.py +38 -0
- package/templates/qoder/scripts/context_pack.py +196 -0
- package/templates/qoder/scripts/eval_prd.py +225 -0
- package/templates/qoder/scripts/export.py +487 -0
- package/templates/qoder/scripts/git_sync.py +1087 -0
- package/templates/qoder/scripts/handoff.py +22 -0
- package/templates/qoder/scripts/init_developer.py +76 -0
- package/templates/qoder/scripts/init_doctor.py +527 -0
- package/templates/qoder/scripts/install_qoderwork.py +339 -0
- package/templates/qoder/scripts/learn.py +67 -0
- package/templates/qoder/scripts/notify.py +5 -0
- package/templates/qoder/scripts/parse_prds.py +33 -0
- package/templates/qoder/scripts/report.py +281 -0
- package/templates/qoder/scripts/role.py +39 -0
- package/templates/qoder/scripts/run_weekly_update.bat +17 -0
- package/templates/qoder/scripts/run_weekly_update.sh +20 -0
- package/templates/qoder/scripts/search_index.py +352 -0
- package/templates/qoder/scripts/setup.py +453 -0
- package/templates/qoder/scripts/setup_weekly_cron.bat +22 -0
- package/templates/qoder/scripts/setup_weekly_cron.sh +19 -0
- package/templates/qoder/scripts/status.py +389 -0
- package/templates/qoder/scripts/syncgate.py +330 -0
- package/templates/qoder/scripts/task.py +954 -0
- package/templates/qoder/scripts/team.py +29 -0
- package/templates/qoder/scripts/team_sync.py +419 -0
- package/templates/qoder/scripts/workspace_init.py +102 -0
- package/templates/qoder/settings.json +53 -0
- package/templates/qoder/skills/design-review/SKILL.md +25 -0
- package/templates/qoder/skills/prd-generator/SKILL.md +180 -0
- package/templates/qoder/skills/prd-review/SKILL.md +36 -0
- package/templates/qoder/skills/prototype-generator/SKILL.md +141 -0
- package/templates/qoder/skills/spec-coder/SKILL.md +69 -0
- package/templates/qoder/skills/spec-generator/SKILL.md +67 -0
- package/templates/qoder/skills/test-generator/SKILL.md +72 -0
- package/templates/qoder/skills/wl-commit/SKILL.md +76 -0
- package/templates/qoder/skills/wl-init/SKILL.md +67 -0
- package/templates/qoder/skills/wl-insight/SKILL.md +81 -0
- package/templates/qoder/skills/wl-report/SKILL.md +87 -0
- package/templates/qoder/skills/wl-search/SKILL.md +75 -0
- package/templates/qoder/skills/wl-status/SKILL.md +61 -0
- package/templates/qoder/skills/wl-task/SKILL.md +58 -0
- package/templates/qoder/templates/prd-full-template.md +103 -0
- package/templates/qoder/templates/prd-quick-template.md +69 -0
- package/templates/qoder/templates/prototype-app.html +344 -0
- package/templates/qoder/templates/prototype-web.html +310 -0
- package/templates/root/AGENTS.md +182 -0
- package/templates/root/README-pipeline.md +56 -0
- package/templates/root/ROLES.md +85 -0
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
export.py - 集成导出工具 (阶段 C)
|
|
5
|
+
|
|
6
|
+
把 QODER 的任务/PRD/API 数据导出为外部系统可消费的格式:
|
|
7
|
+
- jira : Jira CSV importer 格式 (任务 -> Jira issues)
|
|
8
|
+
- openapi: OpenAPI 3.0 YAML (api-index -> API 契约)
|
|
9
|
+
- callgraph : Java 调用图 JSON (@Autowired 依赖)
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
python export.py jira [--mine] [--sprint YYYY-WW]
|
|
13
|
+
python export.py openapi [--module 保险] [--project fywl-ics]
|
|
14
|
+
python export.py callgraph [--class ClassName]
|
|
15
|
+
|
|
16
|
+
输出到 workspace/members/{dev}/exports/。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import csv
|
|
21
|
+
import io
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import sys
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
31
|
+
except (AttributeError, TypeError, OSError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
35
|
+
sys.path.insert(0, THIS_DIR)
|
|
36
|
+
from common.paths import get_repo_root, get_developer, get_tasks_dir
|
|
37
|
+
from common.task_utils import load_task_json
|
|
38
|
+
from common.atomicio import safe_read_json
|
|
39
|
+
|
|
40
|
+
BASE = get_repo_root()
|
|
41
|
+
INDEX_DIR = BASE / 'data' / 'index'
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _export_dir():
|
|
45
|
+
"""导出目录: workspace/members/{dev}/exports/"""
|
|
46
|
+
dev = get_developer(BASE) or 'unknown'
|
|
47
|
+
d = BASE / 'workspace' / 'members' / dev / 'exports'
|
|
48
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
return d
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ============================================================
|
|
53
|
+
# C1: Jira CSV 导出
|
|
54
|
+
# ============================================================
|
|
55
|
+
|
|
56
|
+
# Jira Priority 映射
|
|
57
|
+
JIRA_PRIORITY = {
|
|
58
|
+
'P0': 'Highest', 'P1': 'High', 'P2': 'Medium', 'P3': 'Low',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Jira Issue Type 映射 (从 task tags 或 title 推断)
|
|
62
|
+
def _infer_issue_type(data):
|
|
63
|
+
tags = data.get('tags') or []
|
|
64
|
+
title = (data.get('title') or '').lower()
|
|
65
|
+
if 'bug' in tags or '缺陷' in title or 'bug' in title:
|
|
66
|
+
return 'Bug'
|
|
67
|
+
if 'task' in tags or '任务' in title:
|
|
68
|
+
return 'Task'
|
|
69
|
+
if 'story' in tags or '需求' in title or 'feature' in title:
|
|
70
|
+
return 'Story'
|
|
71
|
+
return 'Task'
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def export_jira(mine_only=False, sprint=None):
|
|
75
|
+
"""导出任务为 Jira CSV。"""
|
|
76
|
+
tasks_dir = get_tasks_dir(BASE)
|
|
77
|
+
if not tasks_dir.is_dir():
|
|
78
|
+
print('No tasks directory')
|
|
79
|
+
return 1
|
|
80
|
+
|
|
81
|
+
dev = get_developer(BASE)
|
|
82
|
+
rows = []
|
|
83
|
+
for d in sorted(tasks_dir.iterdir()):
|
|
84
|
+
if not d.is_dir():
|
|
85
|
+
continue
|
|
86
|
+
data = load_task_json(d)
|
|
87
|
+
if not data:
|
|
88
|
+
continue
|
|
89
|
+
# 归档的不导出
|
|
90
|
+
if data.get('status') == 'completed' and data.get('archived_at'):
|
|
91
|
+
continue
|
|
92
|
+
# --mine 过滤
|
|
93
|
+
if mine_only and dev not in (data.get('assignee'), data.get('creator')):
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
# 读 PRD 摘要作为 Description
|
|
97
|
+
desc = ''
|
|
98
|
+
prd_file = d / 'prd.md'
|
|
99
|
+
if prd_file.is_file():
|
|
100
|
+
try:
|
|
101
|
+
content = prd_file.read_text(encoding='utf-8')
|
|
102
|
+
# 取前 500 字符作为描述
|
|
103
|
+
desc = content[:500].replace('"', "'").replace('\n', '\\n')
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
rows.append({
|
|
108
|
+
'Summary': data.get('title', d.name),
|
|
109
|
+
'Issue Type': _infer_issue_type(data),
|
|
110
|
+
'Priority': JIRA_PRIORITY.get(data.get('priority', 'P2'), 'Medium'),
|
|
111
|
+
'Assignee': data.get('assignee', ''),
|
|
112
|
+
'Reporter': data.get('creator', ''),
|
|
113
|
+
'Due Date': data.get('due_date') or '',
|
|
114
|
+
'Description': desc,
|
|
115
|
+
'Labels': ';'.join(data.get('tags') or []),
|
|
116
|
+
'Status': _jira_status(data.get('status', 'planning')),
|
|
117
|
+
'External ID': d.name, # 用于后续同步 (避免重复导入)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
if not rows:
|
|
121
|
+
print('No tasks to export')
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
# 写 CSV
|
|
125
|
+
out_file = _export_dir() / f'jira-{datetime.now().strftime("%Y%m%d-%H%M")}.csv'
|
|
126
|
+
buf = io.StringIO()
|
|
127
|
+
writer = csv.DictWriter(buf, fieldnames=list(rows[0].keys()))
|
|
128
|
+
writer.writeheader()
|
|
129
|
+
writer.writerows(rows)
|
|
130
|
+
out_file.write_text(buf.getvalue(), encoding='utf-8-sig') # BOM 让 Excel 正确识别中文
|
|
131
|
+
|
|
132
|
+
print(f'导出 {len(rows)} 个任务到: {out_file}')
|
|
133
|
+
print(f'Jira 导入: System > CSV Importer > 选择此文件')
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _jira_status(status):
|
|
138
|
+
"""task status -> Jira status."""
|
|
139
|
+
return {
|
|
140
|
+
'planning': 'To Do',
|
|
141
|
+
'in_progress': 'In Progress',
|
|
142
|
+
'completed': 'Done',
|
|
143
|
+
}.get(status, 'To Do')
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ============================================================
|
|
147
|
+
# C2: OpenAPI 契约生成
|
|
148
|
+
# ============================================================
|
|
149
|
+
|
|
150
|
+
# Java 类型 -> OpenAPI schema 类型
|
|
151
|
+
JAVA_TO_OPENAPI = {
|
|
152
|
+
'String': {'type': 'string'},
|
|
153
|
+
'Integer': {'type': 'integer', 'format': 'int32'},
|
|
154
|
+
'int': {'type': 'integer', 'format': 'int32'},
|
|
155
|
+
'Long': {'type': 'integer', 'format': 'int64'},
|
|
156
|
+
'long': {'type': 'integer', 'format': 'int64'},
|
|
157
|
+
'Double': {'type': 'number', 'format': 'double'},
|
|
158
|
+
'double': {'type': 'number', 'format': 'double'},
|
|
159
|
+
'BigDecimal': {'type': 'number'},
|
|
160
|
+
'Boolean': {'type': 'boolean'},
|
|
161
|
+
'boolean': {'type': 'boolean'},
|
|
162
|
+
'LocalDate': {'type': 'string', 'format': 'date'},
|
|
163
|
+
'LocalDateTime': {'type': 'string', 'format': 'date-time'},
|
|
164
|
+
'Date': {'type': 'string', 'format': 'date'},
|
|
165
|
+
'List': {'type': 'array'},
|
|
166
|
+
'Map': {'type': 'object'},
|
|
167
|
+
'Object': {'type': 'object'},
|
|
168
|
+
'void': None,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def export_openapi(module=None, project='fywl-ics'):
|
|
173
|
+
"""从 api-index + Java 源码生成 OpenAPI 3.0 YAML。
|
|
174
|
+
|
|
175
|
+
扫描指定 project 的 Controller, 提取 @*Mapping + 方法签名。
|
|
176
|
+
"""
|
|
177
|
+
api_index = safe_read_json(INDEX_DIR / 'api-index.json', default={}) or {}
|
|
178
|
+
if not api_index:
|
|
179
|
+
print('api-index.json 不存在或为空, 先跑 git_sync.py')
|
|
180
|
+
return 1
|
|
181
|
+
|
|
182
|
+
# 按模块/项目过滤 API
|
|
183
|
+
filtered = {}
|
|
184
|
+
module_lower = module.lower() if module else None
|
|
185
|
+
for endpoint, filepath in api_index.items():
|
|
186
|
+
if not filepath.startswith(project + '/'):
|
|
187
|
+
continue
|
|
188
|
+
if module_lower and module_lower not in endpoint.lower() and module_lower not in filepath.lower():
|
|
189
|
+
continue
|
|
190
|
+
filtered[endpoint] = filepath
|
|
191
|
+
|
|
192
|
+
if not filtered:
|
|
193
|
+
print(f'未找到匹配的 API (project={project}, module={module})')
|
|
194
|
+
print(f'api-index 共 {len(api_index)} 个 endpoint, project={project} 下 {sum(1 for f in api_index.values() if f.startswith(project+"/"))} 个')
|
|
195
|
+
return 1
|
|
196
|
+
|
|
197
|
+
# 解析 Java 文件提取方法签名 (best-effort, regex)
|
|
198
|
+
paths = {}
|
|
199
|
+
code_dir = BASE / 'data' / 'code'
|
|
200
|
+
for endpoint, rel_path in filtered.items():
|
|
201
|
+
full_path = code_dir / rel_path
|
|
202
|
+
if not full_path.is_file():
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
content = full_path.read_text(encoding='utf-8', errors='replace')
|
|
206
|
+
except Exception:
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
# 解析 HTTP method + path
|
|
210
|
+
# endpoint 格式通常是 "GET /api/foo/bar" 或 "/api/foo/bar"
|
|
211
|
+
ep_match = re.match(r'^(GET|POST|PUT|DELETE|PATCH)\s+(.+)$', endpoint, re.IGNORECASE)
|
|
212
|
+
if ep_match:
|
|
213
|
+
method = ep_match.group(1).lower()
|
|
214
|
+
path = ep_match.group(2).strip()
|
|
215
|
+
else:
|
|
216
|
+
# 无方法前缀, 默认 GET
|
|
217
|
+
method = 'get'
|
|
218
|
+
path = endpoint.strip()
|
|
219
|
+
|
|
220
|
+
# 从源码找方法 (best-effort)
|
|
221
|
+
operation = _extract_operation(content, endpoint, path)
|
|
222
|
+
|
|
223
|
+
path_item = paths.setdefault(path, {})
|
|
224
|
+
path_item[method] = operation
|
|
225
|
+
|
|
226
|
+
# 生成 OpenAPI YAML
|
|
227
|
+
spec = {
|
|
228
|
+
'openapi': '3.0.3',
|
|
229
|
+
'info': {
|
|
230
|
+
'title': f'{project}' + (f' / {module}' if module else ''),
|
|
231
|
+
'version': datetime.now().strftime('%Y-%m-%d'),
|
|
232
|
+
'description': f'从 api-index 自动生成 ({len(filtered)} endpoints)',
|
|
233
|
+
},
|
|
234
|
+
'paths': dict(sorted(paths.items())),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# 写 YAML (简单序列化, 不依赖 PyYAML; 失败回退 JSON)
|
|
238
|
+
name_parts = [f'openapi-{project}']
|
|
239
|
+
if module:
|
|
240
|
+
name_parts.append(module)
|
|
241
|
+
out_file = _export_dir() / ('-'.join(name_parts) + '.yaml')
|
|
242
|
+
try:
|
|
243
|
+
yaml_text = _to_yaml(spec)
|
|
244
|
+
out_file.write_text(yaml_text, encoding='utf-8')
|
|
245
|
+
except Exception:
|
|
246
|
+
out_file = out_file.with_suffix('.json')
|
|
247
|
+
out_file.write_text(json.dumps(spec, indent=2, ensure_ascii=False), encoding='utf-8')
|
|
248
|
+
|
|
249
|
+
print(f'导出 {len(paths)} 个路径 ({len(filtered)} endpoints) 到: {out_file}')
|
|
250
|
+
return 0
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _extract_operation(content, endpoint, path):
|
|
254
|
+
"""从 Controller 源码提取操作信息 (best-effort)。"""
|
|
255
|
+
# 提取类上的 @RequestMapping 前缀
|
|
256
|
+
cls_prefix = ''
|
|
257
|
+
m = re.search(r'@RequestMapping\s*\(\s*["\']([^"\']+)["\']', content)
|
|
258
|
+
if m:
|
|
259
|
+
cls_prefix = m.group(1)
|
|
260
|
+
|
|
261
|
+
# 简化: 返回基本操作结构
|
|
262
|
+
summary = path.rsplit('/', 1)[-1].replace('-', ' ').title() or endpoint
|
|
263
|
+
return {
|
|
264
|
+
'summary': summary,
|
|
265
|
+
'operationId': re.sub(r'[^a-zA-Z0-9]', '_', path).strip('_'),
|
|
266
|
+
'tags': [path.split('/')[2] if len(path.split('/')) > 2 else 'default'],
|
|
267
|
+
'responses': {
|
|
268
|
+
'200': {'description': '成功'},
|
|
269
|
+
'400': {'description': '参数错误'},
|
|
270
|
+
'401': {'description': '未认证'},
|
|
271
|
+
'500': {'description': '服务器错误'},
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _to_yaml(obj, indent=0):
|
|
277
|
+
"""简易 YAML 序列化 (无第三方依赖)。支持 dict/list/str/num/bool/None。"""
|
|
278
|
+
pad = ' ' * indent
|
|
279
|
+
lines = []
|
|
280
|
+
if isinstance(obj, dict):
|
|
281
|
+
for k, v in obj.items():
|
|
282
|
+
if isinstance(v, (dict, list)) and v:
|
|
283
|
+
lines.append(f'{pad}{k}:')
|
|
284
|
+
lines.append(_to_yaml(v, indent + 1))
|
|
285
|
+
elif v is None:
|
|
286
|
+
lines.append(f'{pad}{k}: null')
|
|
287
|
+
elif isinstance(v, bool):
|
|
288
|
+
lines.append(f'{pad}{k}: {"true" if v else "false"}')
|
|
289
|
+
elif isinstance(v, (int, float)):
|
|
290
|
+
lines.append(f'{pad}{k}: {v}')
|
|
291
|
+
else:
|
|
292
|
+
lines.append(f'{pad}{k}: "{str(v).replace(chr(34), chr(92)+chr(34))}"')
|
|
293
|
+
elif isinstance(obj, list):
|
|
294
|
+
for item in obj:
|
|
295
|
+
if isinstance(item, (dict, list)):
|
|
296
|
+
lines.append(f'{pad}-')
|
|
297
|
+
lines.append(_to_yaml(item, indent + 1))
|
|
298
|
+
else:
|
|
299
|
+
lines.append(f'{pad}- "{str(item).replace(chr(34), chr(92)+chr(34))}"')
|
|
300
|
+
return '\n'.join(lines)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ============================================================
|
|
304
|
+
# C3: Java 调用图
|
|
305
|
+
# ============================================================
|
|
306
|
+
|
|
307
|
+
def export_callgraph(class_name=None):
|
|
308
|
+
"""生成/查询 Java 调用图 (@Autowired 依赖)。
|
|
309
|
+
|
|
310
|
+
首次运行会扫描源码构建 callgraph.json; 后续直接查索引。
|
|
311
|
+
"""
|
|
312
|
+
cg_path = INDEX_DIR / 'callgraph.json'
|
|
313
|
+
if not cg_path.is_file():
|
|
314
|
+
print('callgraph.json 不存在, 构建中...')
|
|
315
|
+
cg = _build_callgraph()
|
|
316
|
+
if not cg:
|
|
317
|
+
print('构建失败 (无 Java 源码或解析失败)')
|
|
318
|
+
return 1
|
|
319
|
+
else:
|
|
320
|
+
cg = safe_read_json(cg_path, default={}) or {}
|
|
321
|
+
|
|
322
|
+
if class_name:
|
|
323
|
+
# 查询模式: 显示该类的依赖
|
|
324
|
+
return _query_callgraph(cg, class_name)
|
|
325
|
+
else:
|
|
326
|
+
# 全量导出
|
|
327
|
+
out_file = _export_dir() / f'callgraph-{datetime.now().strftime("%Y%m%d")}.json'
|
|
328
|
+
out_file.write_text(json.dumps(cg, indent=2, ensure_ascii=False), encoding='utf-8')
|
|
329
|
+
print(f'导出 {len(cg)} 个类的调用图到: {out_file}')
|
|
330
|
+
print(f'依赖关系总数: {sum(len(c.get("injects", [])) for c in cg.values())}')
|
|
331
|
+
return 0
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _build_callgraph():
|
|
335
|
+
"""扫描 Java 源码构建调用图。"""
|
|
336
|
+
code_dir = BASE / 'data' / 'code'
|
|
337
|
+
if not code_dir.is_dir():
|
|
338
|
+
return {}
|
|
339
|
+
|
|
340
|
+
cg = {}
|
|
341
|
+
SKIP_DIRS = {'node_modules', 'target', 'build', 'dist', '.git', '__pycache__'}
|
|
342
|
+
|
|
343
|
+
for project in code_dir.iterdir():
|
|
344
|
+
if not project.is_dir():
|
|
345
|
+
continue
|
|
346
|
+
for root, dirs, files in os.walk(project):
|
|
347
|
+
dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith('.')]
|
|
348
|
+
for f in files:
|
|
349
|
+
if not f.endswith('.java'):
|
|
350
|
+
continue
|
|
351
|
+
filepath = os.path.join(root, f)
|
|
352
|
+
try:
|
|
353
|
+
content = open(filepath, encoding='utf-8', errors='replace').read()
|
|
354
|
+
except Exception:
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
# 提取类名
|
|
358
|
+
cls_match = re.search(r'\b(?:public\s+)?(?:abstract\s+)?class\s+(\w+)', content)
|
|
359
|
+
if not cls_match:
|
|
360
|
+
continue
|
|
361
|
+
cls = cls_match.group(1)
|
|
362
|
+
|
|
363
|
+
# 提取 @Autowired/@Resource 字段类型
|
|
364
|
+
injects = set()
|
|
365
|
+
# 模式: @Autowired\n private XxxService xxx;
|
|
366
|
+
for m in re.finditer(
|
|
367
|
+
r'@(?:Autowired|Resource)\s*(?:\([^)]*\))?\s*(?:private|protected|public)?\s*(\w+)\s+\w+\s*[;=]',
|
|
368
|
+
content
|
|
369
|
+
):
|
|
370
|
+
injects.add(m.group(1))
|
|
371
|
+
# 构造器注入: private final XxxService xxx; 在构造器参数
|
|
372
|
+
for m in re.finditer(
|
|
373
|
+
r'(?:private|protected|final)\s+(\w+(?:Service|Mapper|Repository|Component|Client|Manager|Helper))\s+\w+\s*[;=]',
|
|
374
|
+
content
|
|
375
|
+
):
|
|
376
|
+
injects.add(m.group(1))
|
|
377
|
+
|
|
378
|
+
# 提取 @RequestMapping 类级前缀
|
|
379
|
+
prefix = ''
|
|
380
|
+
pm = re.search(r'@RequestMapping\s*\(\s*(?:value\s*=\s*)?["\']([^"\']+)["\']', content)
|
|
381
|
+
if pm:
|
|
382
|
+
prefix = pm.group(1)
|
|
383
|
+
|
|
384
|
+
# 提取 @*Mapping 端点
|
|
385
|
+
endpoints = []
|
|
386
|
+
for m in re.finditer(
|
|
387
|
+
r'@(Get|Post|Put|Delete|Patch|Request)Mapping\s*\(\s*(?:value\s*=\s*)?["\']([^"\']+)["\']',
|
|
388
|
+
content
|
|
389
|
+
):
|
|
390
|
+
method = m.group(1).upper()
|
|
391
|
+
if method == 'REQUEST':
|
|
392
|
+
method = 'ANY'
|
|
393
|
+
ep_path = prefix + m.group(2) if prefix else m.group(2)
|
|
394
|
+
endpoints.append(f'{method} {ep_path}')
|
|
395
|
+
|
|
396
|
+
# 提取 package
|
|
397
|
+
pkg_match = re.search(r'^package\s+([\w.]+);', content, re.MULTILINE)
|
|
398
|
+
pkg = pkg_match.group(1) if pkg_match else ''
|
|
399
|
+
|
|
400
|
+
rel_path = os.path.relpath(filepath, code_dir).replace(os.sep, '/')
|
|
401
|
+
cg[cls] = {
|
|
402
|
+
'package': pkg,
|
|
403
|
+
'file': rel_path,
|
|
404
|
+
'injects': sorted(injects),
|
|
405
|
+
'endpoints': endpoints,
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# 持久化到 data/index/callgraph.json (供后续查询复用)
|
|
409
|
+
try:
|
|
410
|
+
from common.atomicio import atomic_write_json
|
|
411
|
+
atomic_write_json(str(INDEX_DIR / 'callgraph.json'), cg)
|
|
412
|
+
except Exception as e:
|
|
413
|
+
sys.stderr.write(f'[export] callgraph 持久化失败 (不影响导出): {e}\n')
|
|
414
|
+
|
|
415
|
+
return cg
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _query_callgraph(cg, class_name):
|
|
419
|
+
"""查询某类的依赖关系 (谁依赖它 / 它依赖谁)。"""
|
|
420
|
+
cls = cg.get(class_name)
|
|
421
|
+
if not cls:
|
|
422
|
+
# 模糊匹配
|
|
423
|
+
matches = [k for k in cg if class_name.lower() in k.lower()]
|
|
424
|
+
if not matches:
|
|
425
|
+
print(f'类 {class_name} 不在调用图中')
|
|
426
|
+
return 1
|
|
427
|
+
if len(matches) == 1:
|
|
428
|
+
class_name = matches[0]
|
|
429
|
+
cls = cg[class_name]
|
|
430
|
+
else:
|
|
431
|
+
print(f'类名歧义, 匹配到 {len(matches)} 个:')
|
|
432
|
+
for m in matches[:10]:
|
|
433
|
+
print(f' {m}')
|
|
434
|
+
return 1
|
|
435
|
+
|
|
436
|
+
print(f'类: {class_name}')
|
|
437
|
+
print(f'包: {cls.get("package", "?")}')
|
|
438
|
+
print(f'文件: {cls.get("file", "?")}')
|
|
439
|
+
print(f'\n依赖 (注入): {len(cls.get("injects", []))} 个')
|
|
440
|
+
for inj in cls.get('injects', []):
|
|
441
|
+
print(f' -> {inj}')
|
|
442
|
+
print(f'\n端点: {len(cls.get("endpoints", []))} 个')
|
|
443
|
+
for ep in cls.get('endpoints', []):
|
|
444
|
+
print(f' {ep}')
|
|
445
|
+
|
|
446
|
+
# 反向: 谁依赖这个类
|
|
447
|
+
dependents = [k for k, v in cg.items() if class_name in v.get('injects', [])]
|
|
448
|
+
print(f'\n被依赖 (谁注入了它): {len(dependents)} 个')
|
|
449
|
+
for dep in dependents[:20]:
|
|
450
|
+
print(f' <- {dep}')
|
|
451
|
+
return 0
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# ============================================================
|
|
455
|
+
# Main
|
|
456
|
+
# ============================================================
|
|
457
|
+
|
|
458
|
+
def main():
|
|
459
|
+
parser = argparse.ArgumentParser(description='集成导出工具')
|
|
460
|
+
sub = parser.add_subparsers(dest='format', help='导出格式')
|
|
461
|
+
|
|
462
|
+
p_jira = sub.add_parser('jira', help='导出 Jira CSV')
|
|
463
|
+
p_jira.add_argument('--mine', action='store_true', help='只导出我的任务')
|
|
464
|
+
p_jira.add_argument('--sprint', help='按 sprint 过滤 (YYYY-WW)')
|
|
465
|
+
|
|
466
|
+
p_openapi = sub.add_parser('openapi', help='导出 OpenAPI YAML')
|
|
467
|
+
p_openapi.add_argument('--module', help='模块名 (如 保险)')
|
|
468
|
+
p_openapi.add_argument('--project', default='fywl-ics', help='项目 (默认 fywl-ics)')
|
|
469
|
+
|
|
470
|
+
p_cg = sub.add_parser('callgraph', help='Java 调用图')
|
|
471
|
+
p_cg.add_argument('--class', dest='class_name', help='查询指定类的依赖')
|
|
472
|
+
|
|
473
|
+
args = parser.parse_args()
|
|
474
|
+
if not args.format:
|
|
475
|
+
parser.print_help()
|
|
476
|
+
return 1
|
|
477
|
+
|
|
478
|
+
if args.format == 'jira':
|
|
479
|
+
return export_jira(mine_only=args.mine, sprint=args.sprint)
|
|
480
|
+
elif args.format == 'openapi':
|
|
481
|
+
return export_openapi(module=args.module, project=args.project)
|
|
482
|
+
elif args.format == 'callgraph':
|
|
483
|
+
return export_callgraph(class_name=args.class_name)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
if __name__ == '__main__':
|
|
487
|
+
sys.exit(main())
|