@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.
Files changed (87) hide show
  1. package/bin/cli.js +213 -0
  2. package/package.json +11 -0
  3. package/templates/cli.js +198 -0
  4. package/templates/qoder/commands/wl-code.md +43 -0
  5. package/templates/qoder/commands/wl-commit.md +30 -0
  6. package/templates/qoder/commands/wl-init.md +80 -0
  7. package/templates/qoder/commands/wl-insight.md +51 -0
  8. package/templates/qoder/commands/wl-prd.md +199 -0
  9. package/templates/qoder/commands/wl-report.md +166 -0
  10. package/templates/qoder/commands/wl-search.md +52 -0
  11. package/templates/qoder/commands/wl-spec.md +18 -0
  12. package/templates/qoder/commands/wl-status.md +51 -0
  13. package/templates/qoder/commands/wl-task.md +71 -0
  14. package/templates/qoder/commands/wl-test.md +42 -0
  15. package/templates/qoder/config.toml +5 -0
  16. package/templates/qoder/config.yaml +141 -0
  17. package/templates/qoder/hooks/inject-workflow-state.py +117 -0
  18. package/templates/qoder/hooks/session-start.py +204 -0
  19. package/templates/qoder/rules/wl-pipeline.md +105 -0
  20. package/templates/qoder/scripts/add_session.py +245 -0
  21. package/templates/qoder/scripts/benchmark.py +209 -0
  22. package/templates/qoder/scripts/build_style_index.py +268 -0
  23. package/templates/qoder/scripts/code_index.py +41 -0
  24. package/templates/qoder/scripts/collect_prds.py +31 -0
  25. package/templates/qoder/scripts/common/__init__.py +0 -0
  26. package/templates/qoder/scripts/common/active_task.py +230 -0
  27. package/templates/qoder/scripts/common/atomicio.py +172 -0
  28. package/templates/qoder/scripts/common/developer.py +161 -0
  29. package/templates/qoder/scripts/common/eval_api.py +144 -0
  30. package/templates/qoder/scripts/common/feishu.py +278 -0
  31. package/templates/qoder/scripts/common/filelock.py +211 -0
  32. package/templates/qoder/scripts/common/identity.py +285 -0
  33. package/templates/qoder/scripts/common/mentions.py +134 -0
  34. package/templates/qoder/scripts/common/paths.py +311 -0
  35. package/templates/qoder/scripts/common/reqid.py +218 -0
  36. package/templates/qoder/scripts/common/search_engine.py +205 -0
  37. package/templates/qoder/scripts/common/task_utils.py +342 -0
  38. package/templates/qoder/scripts/common/terms.py +234 -0
  39. package/templates/qoder/scripts/common/utf8.py +38 -0
  40. package/templates/qoder/scripts/context_pack.py +196 -0
  41. package/templates/qoder/scripts/eval_prd.py +225 -0
  42. package/templates/qoder/scripts/export.py +487 -0
  43. package/templates/qoder/scripts/git_sync.py +1087 -0
  44. package/templates/qoder/scripts/handoff.py +22 -0
  45. package/templates/qoder/scripts/init_developer.py +76 -0
  46. package/templates/qoder/scripts/init_doctor.py +527 -0
  47. package/templates/qoder/scripts/install_qoderwork.py +339 -0
  48. package/templates/qoder/scripts/learn.py +67 -0
  49. package/templates/qoder/scripts/notify.py +5 -0
  50. package/templates/qoder/scripts/parse_prds.py +33 -0
  51. package/templates/qoder/scripts/report.py +281 -0
  52. package/templates/qoder/scripts/role.py +39 -0
  53. package/templates/qoder/scripts/run_weekly_update.bat +17 -0
  54. package/templates/qoder/scripts/run_weekly_update.sh +20 -0
  55. package/templates/qoder/scripts/search_index.py +352 -0
  56. package/templates/qoder/scripts/setup.py +453 -0
  57. package/templates/qoder/scripts/setup_weekly_cron.bat +22 -0
  58. package/templates/qoder/scripts/setup_weekly_cron.sh +19 -0
  59. package/templates/qoder/scripts/status.py +389 -0
  60. package/templates/qoder/scripts/syncgate.py +330 -0
  61. package/templates/qoder/scripts/task.py +954 -0
  62. package/templates/qoder/scripts/team.py +29 -0
  63. package/templates/qoder/scripts/team_sync.py +419 -0
  64. package/templates/qoder/scripts/workspace_init.py +102 -0
  65. package/templates/qoder/settings.json +53 -0
  66. package/templates/qoder/skills/design-review/SKILL.md +25 -0
  67. package/templates/qoder/skills/prd-generator/SKILL.md +180 -0
  68. package/templates/qoder/skills/prd-review/SKILL.md +36 -0
  69. package/templates/qoder/skills/prototype-generator/SKILL.md +141 -0
  70. package/templates/qoder/skills/spec-coder/SKILL.md +69 -0
  71. package/templates/qoder/skills/spec-generator/SKILL.md +67 -0
  72. package/templates/qoder/skills/test-generator/SKILL.md +72 -0
  73. package/templates/qoder/skills/wl-commit/SKILL.md +76 -0
  74. package/templates/qoder/skills/wl-init/SKILL.md +67 -0
  75. package/templates/qoder/skills/wl-insight/SKILL.md +81 -0
  76. package/templates/qoder/skills/wl-report/SKILL.md +87 -0
  77. package/templates/qoder/skills/wl-search/SKILL.md +75 -0
  78. package/templates/qoder/skills/wl-status/SKILL.md +61 -0
  79. package/templates/qoder/skills/wl-task/SKILL.md +58 -0
  80. package/templates/qoder/templates/prd-full-template.md +103 -0
  81. package/templates/qoder/templates/prd-quick-template.md +69 -0
  82. package/templates/qoder/templates/prototype-app.html +344 -0
  83. package/templates/qoder/templates/prototype-web.html +310 -0
  84. package/templates/root/AGENTS.md +182 -0
  85. package/templates/root/README-pipeline.md +56 -0
  86. package/templates/root/ROLES.md +85 -0
  87. 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())