@hupan56/wlkj 2.7.12 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/bin/cli.js +344 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +113 -4
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -1,281 +1,287 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- report.py - 工作日报/周报生成 + 飞书推送 (阶段 D3)
5
-
6
- 支撑 /wl-report 命令的数据生成。AI 执行 markdown 指令, 本脚本提供数据。
7
-
8
- Usage:
9
- python report.py daily # 生成今日日报 (打印)
10
- python report.py weekly # 生成本周周报 (打印)
11
- python report.py daily --push-feishu # 生成并推送到飞书
12
- python report.py weekly --push-feishu
13
-
14
- 输出: 中文格式, 保存到 workspace/members/{dev}/journal/
15
- """
16
-
17
- import argparse
18
- import os
19
- import subprocess
20
- import sys
21
- from datetime import datetime, date, timedelta
22
- from pathlib import Path
23
-
24
- try:
25
- sys.stdout.reconfigure(encoding='utf-8', errors='replace')
26
- except (AttributeError, TypeError, OSError):
27
- pass
28
-
29
- THIS_DIR = os.path.dirname(os.path.abspath(__file__))
30
- sys.path.insert(0, THIS_DIR)
31
- from common.paths import get_repo_root, get_developer, get_tasks_dir
32
- from common.task_utils import load_task_json
33
-
34
- BASE = get_repo_root()
35
-
36
-
37
- def _ensure_developer():
38
- """必须有开发者身份。"""
39
- dev = get_developer(BASE)
40
- if not dev:
41
- print('[ERROR] 未设置开发者身份。先跑 /wl-init 注册。', file=sys.stderr)
42
- sys.exit(4)
43
- return dev
44
-
45
-
46
- def _git_commits(dev: str, since_date: str) -> list:
47
- """获取开发者指定日期后的 git 提交。"""
48
- try:
49
- r = subprocess.run(
50
- ['git', 'log', '--author', dev, '--since', since_date,
51
- '--pretty=format:%h|%s|%ar', '--no-merges'],
52
- capture_output=True, text=True, encoding='utf-8', errors='replace',
53
- cwd=str(BASE),
54
- )
55
- if r.returncode != 0:
56
- return []
57
- commits = []
58
- for line in r.stdout.strip().splitlines():
59
- parts = line.split('|', 2)
60
- if len(parts) == 3:
61
- commits.append({'hash': parts[0], 'msg': parts[1], 'when': parts[2]})
62
- return commits
63
- except Exception:
64
- return []
65
-
66
-
67
- def _my_tasks(dev: str, statuses=None) -> list:
68
- """获取我的任务。"""
69
- tasks_dir = get_tasks_dir(BASE)
70
- if not tasks_dir.is_dir():
71
- return []
72
- out = []
73
- for d in tasks_dir.iterdir():
74
- if not d.is_dir():
75
- continue
76
- data = load_task_json(d)
77
- if not data:
78
- continue
79
- if dev not in (data.get('assignee'), data.get('creator')):
80
- continue
81
- if statuses and data.get('status') not in statuses:
82
- continue
83
- out.append({'name': d.name, 'data': data})
84
- return out
85
-
86
-
87
- def _finished_today(dev: str) -> list:
88
- """今天完成的任务 (从 stage_ts.completed 判断)。"""
89
- today_str = date.today().isoformat()
90
- out = []
91
- tasks_dir = get_tasks_dir(BASE)
92
- # 也查归档 (今天 finish 的会归档)
93
- archive_dir = BASE / '.qoder' / 'archive'
94
- search_dirs = [tasks_dir, archive_dir] if archive_dir.is_dir() else [tasks_dir]
95
- for sd in search_dirs:
96
- if not sd.is_dir():
97
- continue
98
- for d in sd.iterdir():
99
- if not d.is_dir():
100
- continue
101
- data = load_task_json(d)
102
- if not data:
103
- continue
104
- if dev not in (data.get('assignee'), data.get('creator')):
105
- continue
106
- completed_ts = (data.get('stage_ts') or {}).get('completed') or data.get('updated_at', '')
107
- if completed_ts.startswith(today_str):
108
- out.append({'name': d.name, 'data': data})
109
- return out
110
-
111
-
112
- def generate_daily(dev: str) -> str:
113
- """生成日报 markdown。"""
114
- today = date.today().isoformat()
115
- commits = _git_commits(dev, today)
116
- finished = _finished_today(dev)
117
- in_progress = _my_tasks(dev, statuses=['in_progress'])
118
- planning = _my_tasks(dev, statuses=['planning'])
119
-
120
- lines = [
121
- f'# 工作日报 - {dev} - {today}',
122
- '',
123
- '## 今日完成',
124
- ]
125
- if finished:
126
- for t in finished:
127
- title = t['data'].get('title', t['name'])
128
- lines.append(f'- [{t["name"]}] {title}')
129
- elif commits:
130
- # 无 finish 任务但有提交
131
- seen = set()
132
- for c in commits:
133
- msg = c['msg']
134
- if msg not in seen:
135
- lines.append(f'- {msg} ({c["hash"]})')
136
- seen.add(msg)
137
- else:
138
- lines.append('- (今日无完成任务或提交)')
139
-
140
- lines.extend(['', '## 进行中'])
141
- if in_progress:
142
- for t in in_progress:
143
- title = t['data'].get('title', t['name'])
144
- due = t['data'].get('due_date', '')
145
- due_str = f' (截止 {due})' if due else ''
146
- lines.append(f'- [{t["name"]}] {title}{due_str}')
147
- else:
148
- lines.append('- (无)')
149
-
150
- lines.extend(['', '## 明日计划'])
151
- if planning:
152
- for t in planning[:3]:
153
- title = t['data'].get('title', t['name'])
154
- lines.append(f'- [{t["name"]}] {title}')
155
- elif in_progress:
156
- lines.append('- (继续进行中的任务)')
157
- else:
158
- lines.append('- (待规划)')
159
-
160
- lines.extend(['', f'## 提交统计', f'- 今日提交: {len(commits)}'])
161
-
162
- return '\n'.join(lines)
163
-
164
-
165
- def generate_weekly(dev: str) -> str:
166
- """生成周报 markdown。"""
167
- # 周一为起点
168
- today = date.today()
169
- monday = today - timedelta(days=today.weekday())
170
- week_str = f'{monday.isoformat()} ~ {today.isoformat()}'
171
- since = monday.isoformat()
172
-
173
- commits = _git_commits(dev, since)
174
- finished = _finished_this_week(dev, since)
175
- in_progress = _my_tasks(dev, statuses=['in_progress'])
176
-
177
- lines = [
178
- f'# 周报 - {dev} - {week_str}',
179
- '',
180
- '## 本周完成',
181
- ]
182
- if finished:
183
- for t in finished:
184
- title = t['data'].get('title', t['name'])
185
- lines.append(f'- [{t["name"]}] {title}')
186
- else:
187
- lines.append('- (本周无完成任务)')
188
-
189
- lines.extend([
190
- '', '## 关键产出',
191
- f'- 提交数: {len(commits)}',
192
- ])
193
- # 提取 REQ/PRD 相关提交
194
- prd_commits = [c for c in commits if 'REQ-' in c['msg'].upper() or 'PRD' in c['msg'].upper()]
195
- if prd_commits:
196
- lines.append(f'- PRD 相关提交: {len(prd_commits)} 次')
197
-
198
- lines.extend(['', '## 进行中'])
199
- if in_progress:
200
- for t in in_progress:
201
- title = t['data'].get('title', t['name'])
202
- lines.append(f'- [{t["name"]}] {title}')
203
- else:
204
- lines.append('- (无)')
205
-
206
- lines.extend(['', '## 下周计划', '- (待规划)'])
207
-
208
- return '\n'.join(lines)
209
-
210
-
211
- def _finished_this_week(dev: str, since: str) -> list:
212
- """本周完成的任务。"""
213
- out = []
214
- tasks_dir = get_tasks_dir(BASE)
215
- archive_dir = BASE / '.qoder' / 'archive'
216
- search_dirs = [tasks_dir, archive_dir] if archive_dir.is_dir() else [tasks_dir]
217
- for sd in search_dirs:
218
- if not sd.is_dir():
219
- continue
220
- for d in sd.iterdir():
221
- if not d.is_dir():
222
- continue
223
- data = load_task_json(d)
224
- if not data:
225
- continue
226
- if dev not in (data.get('assignee'), data.get('creator')):
227
- continue
228
- completed_ts = (data.get('stage_ts') or {}).get('completed') or data.get('updated_at', '')
229
- if completed_ts >= since and data.get('status') == 'completed':
230
- out.append({'name': d.name, 'data': data})
231
- return out
232
-
233
-
234
- def save_and_maybe_push(content: str, mode: str, push_feishu: bool):
235
- """保存报告到 journal 目录, 可选推送飞书。"""
236
- dev = get_developer(BASE)
237
- journal_dir = BASE / 'workspace' / 'members' / dev / 'journal'
238
- journal_dir.mkdir(parents=True, exist_ok=True)
239
-
240
- today = date.today().isoformat()
241
- filename = f'{today}-{mode}.md'
242
- out_path = journal_dir / filename
243
- out_path.write_text(content, encoding='utf-8')
244
- print(f'报告已保存: workspace/members/{dev}/journal/{filename}')
245
- print('---')
246
- print(content)
247
-
248
- if push_feishu:
249
- try:
250
- from common.feishu import send_card, is_enabled
251
- if is_enabled():
252
- # 报告作为长文本卡片推送
253
- title = f'{dev} 的{"周报" if mode == "weekly" else "日报"} - {today}'
254
- # 飞书卡片内容截断 (避免过长)
255
- content_short = content[:2000] + ('\n...(截断)' if len(content) > 2000 else '')
256
- send_card(title, [content_short], event_type=None, theme='blue')
257
- print('\n[feishu] 报告已推送到飞书群')
258
- else:
259
- print('\n[feishu] 未启用, 跳过推送 (在 config.yaml feishu.enabled: true)')
260
- except Exception as e:
261
- print(f'\n[feishu] 推送失败 (不影响报告): {e}')
262
-
263
-
264
- def main():
265
- parser = argparse.ArgumentParser(description='工作日报/周报生成')
266
- parser.add_argument('mode', choices=['daily', 'weekly'], help='日报 or 周报')
267
- parser.add_argument('--push-feishu', action='store_true', help='推送到飞书群')
268
- args = parser.parse_args()
269
-
270
- dev = _ensure_developer()
271
-
272
- if args.mode == 'daily':
273
- content = generate_daily(dev)
274
- else:
275
- content = generate_weekly(dev)
276
-
277
- save_and_maybe_push(content, args.mode, args.push_feishu)
278
-
279
-
280
- if __name__ == '__main__':
281
- main()
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
6
+ if _cp not in _s.path: _s.path.insert(0, _cp)
7
+ from bootstrap import setup; setup()
8
+
9
+ """
10
+ report.py - 工作日报/周报生成 + 飞书推送 (阶段 D3)
11
+
12
+ 支撑 /wl-report 命令的数据生成。AI 执行 markdown 指令, 本脚本提供数据。
13
+
14
+ Usage:
15
+ python report.py daily # 生成今日日报 (打印)
16
+ python report.py weekly # 生成本周周报 (打印)
17
+ python report.py daily --push-feishu # 生成并推送到飞书
18
+ python report.py weekly --push-feishu
19
+
20
+ 输出: 中文格式, 保存到 workspace/members/{dev}/journal/
21
+ """
22
+
23
+ import argparse
24
+ import os
25
+ import subprocess
26
+ import sys
27
+ from datetime import datetime, date, timedelta
28
+ from pathlib import Path
29
+
30
+ try:
31
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
32
+ except (AttributeError, TypeError, OSError):
33
+ pass
34
+
35
+ THIS_DIR = os.path.dirname(os.path.abspath(__file__))
36
+ sys.path.insert(0, THIS_DIR)
37
+ from common.paths import get_repo_root, get_developer, get_tasks_dir
38
+ from common.task_utils import load_task_json
39
+
40
+ BASE = get_repo_root()
41
+
42
+
43
+ def _ensure_developer():
44
+ """必须有开发者身份。"""
45
+ dev = get_developer(BASE)
46
+ if not dev:
47
+ print('[ERROR] 未设置开发者身份。先跑 /wl-init 注册。', file=sys.stderr)
48
+ sys.exit(4)
49
+ return dev
50
+
51
+
52
+ def _git_commits(dev: str, since_date: str) -> list:
53
+ """获取开发者指定日期后的 git 提交。"""
54
+ try:
55
+ r = subprocess.run(
56
+ ['git', 'log', '--author', dev, '--since', since_date,
57
+ '--pretty=format:%h|%s|%ar', '--no-merges'],
58
+ capture_output=True, text=True, encoding='utf-8', errors='replace',
59
+ cwd=str(BASE),
60
+ )
61
+ if r.returncode != 0:
62
+ return []
63
+ commits = []
64
+ for line in r.stdout.strip().splitlines():
65
+ parts = line.split('|', 2)
66
+ if len(parts) == 3:
67
+ commits.append({'hash': parts[0], 'msg': parts[1], 'when': parts[2]})
68
+ return commits
69
+ except Exception:
70
+ return []
71
+
72
+
73
+ def _my_tasks(dev: str, statuses=None) -> list:
74
+ """获取我的任务。"""
75
+ tasks_dir = get_tasks_dir(BASE)
76
+ if not tasks_dir.is_dir():
77
+ return []
78
+ out = []
79
+ for d in tasks_dir.iterdir():
80
+ if not d.is_dir():
81
+ continue
82
+ data = load_task_json(d)
83
+ if not data:
84
+ continue
85
+ if dev not in (data.get('assignee'), data.get('creator')):
86
+ continue
87
+ if statuses and data.get('status') not in statuses:
88
+ continue
89
+ out.append({'name': d.name, 'data': data})
90
+ return out
91
+
92
+
93
+ def _finished_today(dev: str) -> list:
94
+ """今天完成的任务 (从 stage_ts.completed 判断)。"""
95
+ today_str = date.today().isoformat()
96
+ out = []
97
+ tasks_dir = get_tasks_dir(BASE)
98
+ # 也查归档 (今天 finish 的会归档)
99
+ archive_dir = BASE / '.qoder' / 'archive'
100
+ search_dirs = [tasks_dir, archive_dir] if archive_dir.is_dir() else [tasks_dir]
101
+ for sd in search_dirs:
102
+ if not sd.is_dir():
103
+ continue
104
+ for d in sd.iterdir():
105
+ if not d.is_dir():
106
+ continue
107
+ data = load_task_json(d)
108
+ if not data:
109
+ continue
110
+ if dev not in (data.get('assignee'), data.get('creator')):
111
+ continue
112
+ completed_ts = (data.get('stage_ts') or {}).get('completed') or data.get('updated_at', '')
113
+ if completed_ts.startswith(today_str):
114
+ out.append({'name': d.name, 'data': data})
115
+ return out
116
+
117
+
118
+ def generate_daily(dev: str) -> str:
119
+ """生成日报 markdown。"""
120
+ today = date.today().isoformat()
121
+ commits = _git_commits(dev, today)
122
+ finished = _finished_today(dev)
123
+ in_progress = _my_tasks(dev, statuses=['in_progress'])
124
+ planning = _my_tasks(dev, statuses=['planning'])
125
+
126
+ lines = [
127
+ f'# 工作日报 - {dev} - {today}',
128
+ '',
129
+ '## 今日完成',
130
+ ]
131
+ if finished:
132
+ for t in finished:
133
+ title = t['data'].get('title', t['name'])
134
+ lines.append(f'- [{t["name"]}] {title}')
135
+ elif commits:
136
+ # 无 finish 任务但有提交
137
+ seen = set()
138
+ for c in commits:
139
+ msg = c['msg']
140
+ if msg not in seen:
141
+ lines.append(f'- {msg} ({c["hash"]})')
142
+ seen.add(msg)
143
+ else:
144
+ lines.append('- (今日无完成任务或提交)')
145
+
146
+ lines.extend(['', '## 进行中'])
147
+ if in_progress:
148
+ for t in in_progress:
149
+ title = t['data'].get('title', t['name'])
150
+ due = t['data'].get('due_date', '')
151
+ due_str = f' (截止 {due})' if due else ''
152
+ lines.append(f'- [{t["name"]}] {title}{due_str}')
153
+ else:
154
+ lines.append('- (无)')
155
+
156
+ lines.extend(['', '## 明日计划'])
157
+ if planning:
158
+ for t in planning[:3]:
159
+ title = t['data'].get('title', t['name'])
160
+ lines.append(f'- [{t["name"]}] {title}')
161
+ elif in_progress:
162
+ lines.append('- (继续进行中的任务)')
163
+ else:
164
+ lines.append('- (待规划)')
165
+
166
+ lines.extend(['', f'## 提交统计', f'- 今日提交: {len(commits)} 次'])
167
+
168
+ return '\n'.join(lines)
169
+
170
+
171
+ def generate_weekly(dev: str) -> str:
172
+ """生成周报 markdown。"""
173
+ # 周一为起点
174
+ today = date.today()
175
+ monday = today - timedelta(days=today.weekday())
176
+ week_str = f'{monday.isoformat()} ~ {today.isoformat()}'
177
+ since = monday.isoformat()
178
+
179
+ commits = _git_commits(dev, since)
180
+ finished = _finished_this_week(dev, since)
181
+ in_progress = _my_tasks(dev, statuses=['in_progress'])
182
+
183
+ lines = [
184
+ f'# 周报 - {dev} - {week_str}',
185
+ '',
186
+ '## 本周完成',
187
+ ]
188
+ if finished:
189
+ for t in finished:
190
+ title = t['data'].get('title', t['name'])
191
+ lines.append(f'- [{t["name"]}] {title}')
192
+ else:
193
+ lines.append('- (本周无完成任务)')
194
+
195
+ lines.extend([
196
+ '', '## 关键产出',
197
+ f'- 提交数: {len(commits)} 次',
198
+ ])
199
+ # 提取 REQ/PRD 相关提交
200
+ prd_commits = [c for c in commits if 'REQ-' in c['msg'].upper() or 'PRD' in c['msg'].upper()]
201
+ if prd_commits:
202
+ lines.append(f'- PRD 相关提交: {len(prd_commits)} ')
203
+
204
+ lines.extend(['', '## 进行中'])
205
+ if in_progress:
206
+ for t in in_progress:
207
+ title = t['data'].get('title', t['name'])
208
+ lines.append(f'- [{t["name"]}] {title}')
209
+ else:
210
+ lines.append('- (无)')
211
+
212
+ lines.extend(['', '## 下周计划', '- (待规划)'])
213
+
214
+ return '\n'.join(lines)
215
+
216
+
217
+ def _finished_this_week(dev: str, since: str) -> list:
218
+ """本周完成的任务。"""
219
+ out = []
220
+ tasks_dir = get_tasks_dir(BASE)
221
+ archive_dir = BASE / '.qoder' / 'archive'
222
+ search_dirs = [tasks_dir, archive_dir] if archive_dir.is_dir() else [tasks_dir]
223
+ for sd in search_dirs:
224
+ if not sd.is_dir():
225
+ continue
226
+ for d in sd.iterdir():
227
+ if not d.is_dir():
228
+ continue
229
+ data = load_task_json(d)
230
+ if not data:
231
+ continue
232
+ if dev not in (data.get('assignee'), data.get('creator')):
233
+ continue
234
+ completed_ts = (data.get('stage_ts') or {}).get('completed') or data.get('updated_at', '')
235
+ if completed_ts >= since and data.get('status') == 'completed':
236
+ out.append({'name': d.name, 'data': data})
237
+ return out
238
+
239
+
240
+ def save_and_maybe_push(content: str, mode: str, push_feishu: bool):
241
+ """保存报告到 journal 目录, 可选推送飞书。"""
242
+ dev = get_developer(BASE)
243
+ journal_dir = BASE / 'workspace' / 'members' / dev / 'journal'
244
+ journal_dir.mkdir(parents=True, exist_ok=True)
245
+
246
+ today = date.today().isoformat()
247
+ filename = f'{today}-{mode}.md'
248
+ out_path = journal_dir / filename
249
+ out_path.write_text(content, encoding='utf-8')
250
+ print(f'报告已保存: workspace/members/{dev}/journal/{filename}')
251
+ print('---')
252
+ print(content)
253
+
254
+ if push_feishu:
255
+ try:
256
+ from common.feishu import send_card, is_enabled
257
+ if is_enabled():
258
+ # 报告作为长文本卡片推送
259
+ title = f'{dev} 的{"周报" if mode == "weekly" else "日报"} - {today}'
260
+ # 飞书卡片内容截断 (避免过长)
261
+ content_short = content[:2000] + ('\n...(截断)' if len(content) > 2000 else '')
262
+ send_card(title, [content_short], event_type=None, theme='blue')
263
+ print('\n[feishu] 报告已推送到飞书群')
264
+ else:
265
+ print('\n[feishu] 未启用, 跳过推送 (在 config.yaml 设 feishu.enabled: true)')
266
+ except Exception as e:
267
+ print(f'\n[feishu] 推送失败 (不影响报告): {e}')
268
+
269
+
270
+ def main():
271
+ parser = argparse.ArgumentParser(description='工作日报/周报生成')
272
+ parser.add_argument('mode', choices=['daily', 'weekly'], help='日报 or 周报')
273
+ parser.add_argument('--push-feishu', action='store_true', help='推送到飞书群')
274
+ args = parser.parse_args()
275
+
276
+ dev = _ensure_developer()
277
+
278
+ if args.mode == 'daily':
279
+ content = generate_daily(dev)
280
+ else:
281
+ content = generate_weekly(dev)
282
+
283
+ save_and_maybe_push(content, args.mode, args.push_feishu)
284
+
285
+
286
+ if __name__ == '__main__':
287
+ main()