@hupan56/wlkj 2.7.12 → 3.1.2

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 (253) hide show
  1. package/bin/cli.js +1435 -1107
  2. package/package.json +29 -29
  3. package/templates/qoder/agents/insight-planning.md +1 -1
  4. package/templates/qoder/agents/insight-research.md +28 -15
  5. package/templates/qoder/commands/optional/wl-insight.md +161 -161
  6. package/templates/qoder/commands/optional/wl-report.md +20 -39
  7. package/templates/qoder/commands/optional/wl-status.md +4 -4
  8. package/templates/qoder/commands/wl-code.md +2 -2
  9. package/templates/qoder/commands/wl-design.md +6 -6
  10. package/templates/qoder/commands/wl-init.md +3 -3
  11. package/templates/qoder/commands/wl-prd.md +26 -13
  12. package/templates/qoder/commands/wl-req.md +43 -0
  13. package/templates/qoder/commands/wl-search.md +78 -47
  14. package/templates/qoder/commands/wl-task.md +343 -40
  15. package/templates/qoder/commands/wl-test.md +43 -17
  16. package/templates/qoder/config.yaml +52 -14
  17. package/templates/qoder/hooks/post-tool-use.py +181 -0
  18. package/templates/qoder/hooks/session-start.py +94 -23
  19. package/templates/qoder/hooks/stop-eval.py +207 -0
  20. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  21. package/templates/qoder/rules/wl-pipeline.md +85 -49
  22. package/templates/qoder/scripts/README.md +102 -0
  23. package/templates/qoder/scripts/capability/__init__.py +26 -0
  24. package/templates/qoder/scripts/capability/__main__.py +72 -0
  25. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  26. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  27. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  28. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  29. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  30. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  31. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  32. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  33. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  34. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  35. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  36. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  37. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  38. package/templates/qoder/scripts/capability/chain.py +92 -0
  39. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  40. package/templates/qoder/scripts/capability/registry.py +246 -0
  41. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  42. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  43. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  44. package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
  45. package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
  46. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  47. package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
  48. package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
  49. package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
  50. package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
  51. package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
  52. package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
  53. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  54. package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
  55. package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
  56. package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
  57. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  58. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
  59. package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
  60. package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
  61. package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
  62. package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
  63. package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
  64. package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
  65. package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
  66. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  68. package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
  69. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  70. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  71. package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
  72. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  73. package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
  74. package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
  75. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  76. package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
  77. package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
  78. package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
  79. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  80. package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
  81. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  82. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  83. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  84. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  85. package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
  86. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  87. package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
  88. package/templates/qoder/scripts/domain/report/__init__.py +11 -0
  89. package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
  90. package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
  91. package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
  92. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  93. package/templates/qoder/scripts/domain/report/role.py +39 -0
  94. package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
  95. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  96. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  97. package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
  98. package/templates/qoder/scripts/domain/requirement/req.py +228 -0
  99. package/templates/qoder/scripts/domain/task/__init__.py +11 -0
  100. package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
  101. package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
  102. package/templates/qoder/scripts/domain/task/task.py +229 -0
  103. package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
  104. package/templates/qoder/scripts/domain/task/task_query.py +162 -0
  105. package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
  106. package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
  107. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  108. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  109. package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
  110. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  111. package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
  112. package/templates/qoder/scripts/foundation/core/config.py +187 -0
  113. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
  114. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  115. package/templates/qoder/scripts/foundation/data/contract.py +317 -0
  116. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  117. package/templates/qoder/scripts/foundation/data/result.py +223 -0
  118. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
  119. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  120. package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
  121. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
  122. package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
  123. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
  124. package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
  125. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  126. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  127. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
  128. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  129. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  130. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  131. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  132. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  133. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  134. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  135. package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
  136. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  137. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  138. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  139. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  140. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  141. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  142. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  143. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  144. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  145. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  146. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  147. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  148. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  153. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  154. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  155. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  156. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  157. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
  158. package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
  159. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  160. package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
  161. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  162. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  163. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  164. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  165. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  166. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  167. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  168. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  169. package/templates/qoder/scripts/validation/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  171. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  172. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  173. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  174. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  175. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  176. package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
  177. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  178. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  179. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  180. package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
  181. package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
  182. package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
  183. package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
  184. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  185. package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
  186. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  187. package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
  188. package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
  189. package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
  190. package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
  191. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  192. package/templates/qoder/settings.json +23 -2
  193. package/templates/qoder/skills/design-import/SKILL.md +235 -226
  194. package/templates/qoder/skills/design-review/SKILL.md +91 -82
  195. package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
  196. package/templates/qoder/skills/prd-review/SKILL.md +8 -6
  197. package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
  198. package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
  199. package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
  200. package/templates/qoder/skills/test-generator/SKILL.md +9 -7
  201. package/templates/qoder/skills/wl-code/SKILL.md +25 -13
  202. package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
  203. package/templates/qoder/skills/wl-design/SKILL.md +7 -5
  204. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  205. package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
  206. package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
  207. package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
  208. package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
  209. package/templates/qoder/skills/wl-report/SKILL.md +27 -25
  210. package/templates/qoder/skills/wl-search/SKILL.md +151 -80
  211. package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
  212. package/templates/qoder/skills/wl-status/SKILL.md +50 -18
  213. package/templates/qoder/skills/wl-task/SKILL.md +94 -12
  214. package/templates/qoder/skills/wl-test/SKILL.md +154 -45
  215. package/templates/qoder/templates/prd-full-template.md +7 -0
  216. package/templates/root/AGENTS.md +275 -237
  217. package/templates/root/requirements.txt +3 -0
  218. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  219. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  220. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  221. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  222. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  223. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  224. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  225. package/templates/qoder/scripts/check_carriers.py +0 -238
  226. package/templates/qoder/scripts/check_mcp.py +0 -298
  227. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  228. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  229. package/templates/qoder/scripts/collect_prds.py +0 -31
  230. package/templates/qoder/scripts/common/mentions.py +0 -134
  231. package/templates/qoder/scripts/common/utf8.py +0 -38
  232. package/templates/qoder/scripts/extract_api_params.py +0 -246
  233. package/templates/qoder/scripts/extract_routes.py +0 -54
  234. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  235. package/templates/qoder/scripts/handoff.py +0 -22
  236. package/templates/qoder/scripts/init_developer.py +0 -76
  237. package/templates/qoder/scripts/kg_mcp_server.py +0 -801
  238. package/templates/qoder/scripts/kg_semantic.py +0 -150
  239. package/templates/qoder/scripts/mcp_launcher.py +0 -414
  240. package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
  241. package/templates/qoder/scripts/parse_prds.py +0 -33
  242. package/templates/qoder/scripts/role.py +0 -51
  243. package/templates/qoder/scripts/sync_carriers.py +0 -259
  244. package/templates/qoder/scripts/task.py +0 -1261
  245. package/templates/qoder/scripts/workspace_init.py +0 -102
  246. package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
  247. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  248. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  249. /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
  250. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  251. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  252. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  253. /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
@@ -1,377 +1,389 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- archive_prd.py - PRD 按时间归档到个人工作区
5
-
6
- 把刚生成的 PRD 复制一份到当前成员的个人归档区,按"天"分文件夹:
7
- workspace/members/{name}/archive/{YYYY-MM-DD}/REQ-{编号}-{中文标题}.md
8
- 并在 archive/_index.md 汇总成"周/月"视图,方便人翻查"我那天做了啥"。
9
-
10
- 设计原则(非侵入):
11
- - 只**复制**,不移动;drafts/ 原件保留,data/docs/prd/ 完全不碰
12
- - 归属看当前操作者(.developer),不依赖 PRD 的"产品负责人"字段 → 开发/测试/产品都能用
13
- - 全程 try/except,失败只 print 一句提示,绝不阻塞 PRD 生成
14
- - 用户没用 PRD 命令就完全不触发(本脚本只在 prd-generator 调用时跑)
15
-
16
- Usage:
17
- python .qoder/scripts/archive_prd.py <prd 文件路径>
18
- python .qoder/scripts/archive_prd.py <prd1> <prd2> ... # 批量
19
- """
20
-
21
- import os
22
- import re
23
- import sys
24
- from datetime import datetime, date, timedelta
25
- from pathlib import Path
26
-
27
- # UTF-8 stdio(防御性:Windows cmd 默认 GBK,stdout 被捕获时不崩)
28
- try:
29
- sys.stdout.reconfigure(encoding="utf-8", errors="replace")
30
- except (AttributeError, TypeError, OSError, IOError):
31
- try:
32
- sys.stdout.reconfigure(encoding="utf-8")
33
- except Exception:
34
- pass
35
-
36
- # common/ 可被 import(直接当脚本跑时)
37
- _THIS_DIR = os.path.dirname(os.path.abspath(__file__))
38
- _COMMON = os.path.join(_THIS_DIR, "common")
39
- if _COMMON not in sys.path:
40
- sys.path.insert(0, _COMMON)
41
-
42
- try:
43
- from paths import get_repo_root, get_developer, MEMBERS_DIR
44
- except ImportError:
45
- # 极端兜底:直接算路径
46
- def get_repo_root():
47
- current = Path(_THIS_DIR).resolve()
48
- for _ in range(10):
49
- if (current / ".qoder").is_dir():
50
- return current
51
- parent = current.parent
52
- if parent == current:
53
- break
54
- current = parent
55
- return Path(_THIS_DIR).parent.parent
56
-
57
- def get_developer(repo_root=None):
58
- dev = (repo_root or get_repo_root()) / ".qoder" / ".developer"
59
- if not dev.is_file():
60
- return None
61
- for enc in ("utf-8", "gbk"):
62
- try:
63
- for line in dev.read_text(encoding=enc).splitlines():
64
- line = line.strip()
65
- if "=" in line and line.split("=")[0].strip() == "name":
66
- return line.split("=", 1)[1].strip() or None
67
- if ":" in line and line.split(":")[0].strip() == "name":
68
- return line.split(":", 1)[1].strip() or None
69
- except (OSError, IOError, UnicodeDecodeError):
70
- continue
71
- return None
72
-
73
- MEMBERS_DIR = None # 下面用 repo_root 拼
74
-
75
-
76
- # ============================================================
77
- # 解析:中文标题 + 日期(带 fallback 链,健壮)
78
- # ============================================================
79
-
80
- # REQ-2026-005(标准)/ REQ-012(老格式单段)都能匹配
81
- _REQ_RE = re.compile(r"REQ-(\d{2,4})(?:-(\d{2,4}))?", re.IGNORECASE)
82
- # 创建日期: 2026-06-12 / 2026.06.12 / 2026/06/12
83
- _DATE_RE = re.compile(
84
- r"(20\d{2})[./\-年](\d{1,2})[./\-月](\d{1,2})"
85
- )
86
-
87
-
88
- def _read_text_safe(path: Path) -> str:
89
- """读文件,UTF-8 优先,GBK 兜底。"""
90
- for enc in ("utf-8", "utf-8-sig", "gbk"):
91
- try:
92
- return path.read_text(encoding=enc)
93
- except (OSError, IOError, UnicodeDecodeError):
94
- continue
95
- return ""
96
-
97
-
98
- def extract_title(content: str, fallback_name: str) -> str:
99
- """提取 PRD 中文标题。fallback 链:
100
- H1 标题(去掉 REQ-xxxx 前缀)
101
- 正文「需求名称」「需求标题」
102
- 文件名 slug
103
- ④ "未命名"
104
- """
105
- # H1:# REQ-2026-005 异常中心全国看板 → 取「异常中心全国看板」
106
- for line in content.splitlines():
107
- s = line.strip()
108
- if s.startswith("# "):
109
- title = s[2:].strip()
110
- # 去掉开头的 REQ-xxxx-xxx 编号前缀
111
- title = _REQ_RE.sub("", title).strip(" -—|")
112
- if title:
113
- return _clean_title(title)
114
- break # H1 存在但没正文,走下一条 fallback
115
- # ② 正文 - **需求名称**: xxx / **需求标题**: xxx
116
- m = re.search(r"\*{0,2}\s*(?:需求名称|需求标题|标题)\s*\*{0,2}\s*[::]\s*(.+)", content)
117
- if m:
118
- return _clean_title(m.group(1).strip())
119
- # 文件名 slug
120
- return fallback_name
121
-
122
-
123
- def _clean_title(title: str) -> str:
124
- """清理标题:去尾部 markdown / 标点 / 多余空白。"""
125
- title = title.splitlines()[0].strip()
126
- title = re.sub(r"\s+", " ", title)
127
- # 去掉行尾的 | 或多余修饰
128
- title = title.rstrip("| -—·•")
129
- return title or "未命名"
130
-
131
-
132
- def extract_date(content: str, file_mtime: float) -> str:
133
- """提取 PRD 创建日期 (YYYY-MM-DD)。fallback 链:
134
- ① 正文「创建日期 / 创建时间」
135
- 正文任意 2026-06-12 格式日期
136
- 文件 mtime
137
- 今天
138
- """
139
- # 显式「创建日期」/「创建时间」
140
- m = re.search(
141
- r"(?:创建日期|创建时间|创建日期)\s*[::]\s*" + _DATE_RE.pattern, content
142
- )
143
- if m:
144
- d = _normalize_date(m.group(1), m.group(2), m.group(3))
145
- if d:
146
- return d
147
- # 任意日期(取第一个)
148
- m = _DATE_RE.search(content)
149
- if m:
150
- d = _normalize_date(m.group(1), m.group(2), m.group(3))
151
- if d:
152
- return d
153
- # mtime
154
- try:
155
- return datetime.fromtimestamp(file_mtime).strftime("%Y-%m-%d")
156
- except (OSError, ValueError, OverflowError):
157
- pass
158
- # ④ 今天
159
- return date.today().strftime("%Y-%m-%d")
160
-
161
-
162
- def _normalize_date(y: str, m: str, d: str) -> str:
163
- try:
164
- return "%s-%02d-%02d" % (y.zfill(4), int(m), int(d))
165
- except (ValueError, TypeError):
166
- return ""
167
-
168
-
169
- def extract_reqno(filename: str) -> str:
170
- """从文件名提取 REQ 编号段,如 'REQ-2026-005'。取不到则用文件名去扩展名。
171
- 兼容标准格式 REQ-2026-005 和老格式 REQ-012(单段)。
172
- """
173
- m = _REQ_RE.search(filename)
174
- if m:
175
- year = m.group(1).zfill(4)
176
- if m.group(2): # 标准格式 REQ-2026-005
177
- return "REQ-%s-%s" % (year, m.group(2))
178
- return "REQ-%s" % year # 老格式 REQ-012
179
- base = os.path.splitext(filename)[0]
180
- return base[:40]
181
-
182
-
183
- # ============================================================
184
- # 文件名安全化(中文保留,去掉文件系统非法字符)
185
- # ============================================================
186
-
187
- _UNSAFE_FS = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
188
-
189
-
190
- def _sanitize_filename(name: str, max_len: int = 60) -> str:
191
- """文件名安全化:保留中文/字母/数字/连字符,去掉非法字符,限长。"""
192
- name = _UNSAFE_FS.sub("", name).strip()
193
- name = re.sub(r"\s+", "-", name) # 空格转连字符
194
- if len(name) > max_len:
195
- name = name[:max_len].rstrip("-")
196
- return name or "未命名"
197
-
198
-
199
- # ============================================================
200
- # 核心:归档单个 PRD
201
- # ============================================================
202
-
203
- def archive_one(prd_path: str, repo_root: Path) -> bool:
204
- """把一个 PRD 归档到当前成员的个人区。返回是否成功。"""
205
- src = Path(prd_path)
206
- if not src.is_absolute():
207
- src = (Path.cwd() / src).resolve()
208
- if not src.is_file():
209
- print("[archive] 跳过: 文件不存在 %s" % prd_path)
210
- return False
211
-
212
- developer = get_developer(repo_root)
213
- if not developer:
214
- print("[archive] 跳过: 未初始化开发者身份 (跑 /wl-init)")
215
- return False
216
-
217
- members = MEMBERS_DIR if MEMBERS_DIR else (repo_root / "workspace" / "members")
218
- archive_root = Path(members) / developer / "archive"
219
-
220
- content = _read_text_safe(src)
221
- fname = src.name
222
- reqno = extract_reqno(fname)
223
- # 文件名里的 slug(REQ-2026-005-xxx.md 的 xxx 部分)作为最后兜底标题
224
- slug_fallback = _REQ_RE.sub("", os.path.splitext(fname)[0]).lstrip("-_") or "未命名"
225
- title = extract_title(content, slug_fallback)
226
- prd_date = extract_date(content, src.stat().st_mtime)
227
-
228
- # 目标:members/{dev}/archive/{YYYY-MM-DD}/REQ-{编号}-{中文标题}.md
229
- day_dir = archive_root / prd_date
230
- day_dir.mkdir(parents=True, exist_ok=True)
231
- safe_title = _sanitize_filename(title)
232
- dst_name = "%s-%s.md" % (reqno, safe_title)
233
- dst = day_dir / dst_name
234
-
235
- import shutil
236
- shutil.copy2(src, dst)
237
-
238
- # 更新当天 _index.md
239
- _write_day_index(day_dir, prd_date)
240
- # 更新 archive 根的 _index.md(周/月汇总)
241
- _write_overview_index(archive_root, developer)
242
-
243
- rel = dst.relative_to(repo_root) if str(dst).startswith(str(repo_root)) else dst
244
- print("[archive] 已归档: %s" % rel)
245
- return True
246
-
247
-
248
- def _collect_day_prds(day_dir: Path):
249
- """收集某天文件夹里的 PRD(.md 且不是 _index)。返回 [(reqno, title, fname), ...]"""
250
- items = []
251
- for f in sorted(day_dir.iterdir()):
252
- if not f.is_file() or not f.name.lower().endswith(".md"):
253
- continue
254
- if f.name.startswith("_"):
255
- continue
256
- stem = f.stem # REQ-2026-005-异常中心全国看板
257
- m = _REQ_RE.search(stem)
258
- reqno = "REQ-%s-%s" % (m.group(1).zfill(4), m.group(2)) if m else stem
259
- title = _REQ_RE.sub("", stem).lstrip("-_ ") or stem
260
- items.append((reqno, title, f.name))
261
- return items
262
-
263
-
264
- def _write_day_index(day_dir: Path, prd_date: str):
265
- """写当天的 _index.md:一个清爽的当天需求清单。"""
266
- items = _collect_day_prds(day_dir)
267
- lines = ["# %s 的需求产出\n" % prd_date, "> 共 %d 个需求\n" % len(items)]
268
- if items:
269
- for reqno, title, fname in items:
270
- lines.append("- **%s** %s — [`%s`](./%s)" % (reqno, title, fname, fname))
271
- else:
272
- lines.append("(无)")
273
- (day_dir / "_index.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
274
-
275
-
276
- def _iso_week_range(d: date):
277
- """返回 d 所在自然周的 周一~周日(ISO 周,周一为起点)。"""
278
- monday = d - timedelta(days=d.weekday()) # weekday() 周一=0
279
- sunday = monday + timedelta(days=6)
280
- return monday, sunday
281
-
282
-
283
- def _write_overview_index(archive_root: Path, developer: str):
284
- """写 archive 根的 _index.md:按 周 / 月 汇总,给「我这阵子做了啥」一个总览。"""
285
- if not archive_root.is_dir():
286
- return
287
- # 扫所有日期文件夹
288
- days = []
289
- for d in sorted(archive_root.iterdir()):
290
- if not d.is_dir():
291
- continue
292
- try:
293
- day = date.fromisoformat(d.name) # 只认 YYYY-MM-DD
294
- except ValueError:
295
- continue
296
- items = _collect_day_prds(d)
297
- if items:
298
- days.append((day, items))
299
- if not days:
300
- return
301
-
302
- lines = ["# %s 的需求归档\n" % developer, "> 按时间翻查你的需求产出。物理按天归档,下面是周/月汇总。\n"]
303
-
304
- # ---- 周视图(按自然周聚合)----
305
- lines.append("## 按周\n")
306
- # 把连续的天聚到同一周
307
- weeks = {} # week_key(周一) -> [(day, items)]
308
- order = []
309
- for day, items in days:
310
- monday, _ = _iso_week_range(day)
311
- if monday not in weeks:
312
- weeks[monday] = []
313
- order.append(monday)
314
- weeks[monday].append((day, items))
315
- for monday in sorted(order, reverse=True):
316
- sunday = monday + timedelta(days=6)
317
- week_items = weeks[monday]
318
- total = sum(len(it) for _, it in week_items)
319
- lines.append(
320
- "### %s 第 %d 周(%s ~ %s)— %d 个需求\n"
321
- % (monday.year, monday.isocalendar()[1], monday.isoformat(), sunday.isoformat(), total)
322
- )
323
- for day, items in sorted(week_items, reverse=True):
324
- summary = "、".join("%s %s" % (r, t) for r, t, _ in items)
325
- lines.append("- **%s**:%s" % (day.isoformat(), summary))
326
- lines.append("")
327
-
328
- # ---- 月视图 ----
329
- lines.append("## 按月\n")
330
- months = {}
331
- morder = []
332
- for day, items in days:
333
- mk = (day.year, day.month)
334
- if mk not in months:
335
- months[mk] = []
336
- morder.append(mk)
337
- months[mk].append((day, items))
338
- for mk in sorted(morder, reverse=True):
339
- m_items = months[mk]
340
- total = sum(len(it) for _, it in m_items)
341
- lines.append("### %d年%d月 — %d 个需求" % (mk[0], mk[1], total))
342
- for day, items in sorted(m_items, reverse=True):
343
- summary = "、".join("%s" % t for _, t, _ in items)
344
- lines.append(" - %s:%s" % (day.isoformat(), summary))
345
- lines.append("")
346
-
347
- (archive_root / "_index.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
348
-
349
-
350
- # ============================================================
351
- # main
352
- # ============================================================
353
-
354
- def main():
355
- args = sys.argv[1:]
356
- # 过滤 flag
357
- prd_args = [a for a in args if not a.startswith("-")]
358
- if not prd_args:
359
- print("Usage: python .qoder/scripts/archive_prd.py <prd 文件路径> [<prd2> ...]")
360
- print("把 PRD 归档到当前成员的 workspace/members/{name}/archive/{日期}/")
361
- sys.exit(1)
362
-
363
- repo_root = get_repo_root()
364
- ok = 0
365
- for p in prd_args:
366
- try:
367
- if archive_one(p, repo_root):
368
- ok += 1
369
- except Exception as e:
370
- # 非侵入:归档失败绝不影响 PRD 生成
371
- print("[archive] 跳过 %s: %s" % (p, str(e)[:120]))
372
- print("[archive] 完成: %d/%d 已归档" % (ok, len(prd_args)))
373
- sys.exit(0)
374
-
375
-
376
- if __name__ == "__main__":
377
- 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
+ _f = _o.path.abspath(__file__)
6
+ for _ in range(10):
7
+ _f = _o.path.dirname(_f)
8
+ _cp = _o.path.join(_f, 'foundation')
9
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
10
+ break
11
+ if _cp not in _s.path: _s.path.insert(0, _cp)
12
+ from bootstrap import setup; setup()
13
+ from foundation.core.paths import get_repo_root
14
+
15
+ """
16
+ archive_prd.py - PRD 按时间归档到个人工作区
17
+
18
+ 把刚生成的 PRD 复制一份到当前成员的个人归档区,按"天"分文件夹:
19
+ workspace/members/{name}/archive/{YYYY-MM-DD}/REQ-{编号}-{中文标题}.md
20
+ 并在 archive/_index.md 汇总成"周/月"视图,方便人翻查"我那天做了啥"。
21
+
22
+ 设计原则(非侵入):
23
+ - 只**复制**,不移动;drafts/ 原件保留,data/docs/prd/ 完全不碰
24
+ - 归属看当前操作者(.developer),不依赖 PRD 的"产品负责人"字段 开发/测试/产品都能用
25
+ - 全程 try/except,失败只 print 一句提示,绝不阻塞 PRD 生成
26
+ - 用户没用 PRD 命令就完全不触发(本脚本只在 prd-generator 调用时跑)
27
+
28
+ Usage:
29
+ python .qoder/scripts/archive_prd.py <prd 文件路径>
30
+ python .qoder/scripts/archive_prd.py <prd1> <prd2> ... # 批量
31
+ """
32
+
33
+ import os
34
+ import re
35
+ import sys
36
+ from datetime import datetime, date, timedelta
37
+ from pathlib import Path
38
+
39
+ # UTF-8 stdio(防御性:Windows cmd 默认 GBK,stdout 被捕获时不崩)
40
+ try:
41
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
42
+ except (AttributeError, TypeError, OSError, IOError):
43
+ try:
44
+ sys.stdout.reconfigure(encoding="utf-8")
45
+ except Exception:
46
+ pass
47
+
48
+ # common/ 可被 import(直接当脚本跑时)
49
+ _THIS_DIR = os.path.dirname(os.path.abspath(__file__))
50
+ _COMMON = os.path.join(_THIS_DIR, "foundation")
51
+ if _COMMON not in sys.path:
52
+ sys.path.insert(0, _COMMON)
53
+
54
+ try:
55
+ from paths import get_repo_root, get_developer, MEMBERS_DIR
56
+ except ImportError:
57
+ # 极端兜底:直接算路径
58
+ def get_repo_root():
59
+ current = Path(_THIS_DIR).resolve()
60
+ for _ in range(10):
61
+ if (current / ".qoder").is_dir():
62
+ return current
63
+ parent = current.parent
64
+ if parent == current:
65
+ break
66
+ current = parent
67
+ return Path(_THIS_DIR).parent.parent
68
+
69
+ def get_developer(repo_root=None):
70
+ dev = (repo_root or get_repo_root()) / ".qoder" / ".developer"
71
+ if not dev.is_file():
72
+ return None
73
+ for enc in ("utf-8", "gbk"):
74
+ try:
75
+ for line in dev.read_text(encoding=enc).splitlines():
76
+ line = line.strip()
77
+ if "=" in line and line.split("=")[0].strip() == "name":
78
+ return line.split("=", 1)[1].strip() or None
79
+ if ":" in line and line.split(":")[0].strip() == "name":
80
+ return line.split(":", 1)[1].strip() or None
81
+ except (OSError, IOError, UnicodeDecodeError):
82
+ continue
83
+ return None
84
+
85
+ MEMBERS_DIR = None # 下面用 repo_root 拼
86
+
87
+
88
+ # ============================================================
89
+ # 解析:中文标题 + 日期(带 fallback 链,健壮)
90
+ # ============================================================
91
+
92
+ # REQ-2026-005(标准)/ REQ-012(老格式单段)都能匹配
93
+ _REQ_RE = re.compile(r"REQ-(\d{2,4})(?:-(\d{2,4}))?", re.IGNORECASE)
94
+ # 创建日期: 2026-06-12 / 2026.06.12 / 2026/06/12
95
+ _DATE_RE = re.compile(
96
+ r"(20\d{2})[./\-年](\d{1,2})[./\-月](\d{1,2})"
97
+ )
98
+
99
+
100
+ def _read_text_safe(path: Path) -> str:
101
+ """读文件,UTF-8 优先,GBK 兜底。"""
102
+ for enc in ("utf-8", "utf-8-sig", "gbk"):
103
+ try:
104
+ return path.read_text(encoding=enc)
105
+ except (OSError, IOError, UnicodeDecodeError):
106
+ continue
107
+ return ""
108
+
109
+
110
+ def extract_title(content: str, fallback_name: str) -> str:
111
+ """提取 PRD 中文标题。fallback 链:
112
+ H1 标题(去掉 REQ-xxxx 前缀)
113
+ 正文「需求名称」「需求标题」
114
+ 文件名 slug
115
+ "未命名"
116
+ """
117
+ # ① H1:# REQ-2026-005 异常中心全国看板 → 取「异常中心全国看板」
118
+ for line in content.splitlines():
119
+ s = line.strip()
120
+ if s.startswith("# "):
121
+ title = s[2:].strip()
122
+ # 去掉开头的 REQ-xxxx-xxx 编号前缀
123
+ title = _REQ_RE.sub("", title).strip(" -—|")
124
+ if title:
125
+ return _clean_title(title)
126
+ break # H1 存在但没正文,走下一条 fallback
127
+ # 正文 - **需求名称**: xxx / **需求标题**: xxx
128
+ m = re.search(r"\*{0,2}\s*(?:需求名称|需求标题|标题)\s*\*{0,2}\s*[::]\s*(.+)", content)
129
+ if m:
130
+ return _clean_title(m.group(1).strip())
131
+ # ③ 文件名 slug
132
+ return fallback_name
133
+
134
+
135
+ def _clean_title(title: str) -> str:
136
+ """清理标题:去尾部 markdown / 标点 / 多余空白。"""
137
+ title = title.splitlines()[0].strip()
138
+ title = re.sub(r"\s+", " ", title)
139
+ # 去掉行尾的 | 或多余修饰
140
+ title = title.rstrip("| -—·•")
141
+ return title or "未命名"
142
+
143
+
144
+ def extract_date(content: str, file_mtime: float) -> str:
145
+ """提取 PRD 创建日期 (YYYY-MM-DD)。fallback 链:
146
+ 正文「创建日期 / 创建时间」
147
+ 正文任意 2026-06-12 格式日期
148
+ 文件 mtime
149
+ 今天
150
+ """
151
+ # ① 显式「创建日期」/「创建时间」
152
+ m = re.search(
153
+ r"(?:创建日期|创建时间|创建日期)\s*[::]\s*" + _DATE_RE.pattern, content
154
+ )
155
+ if m:
156
+ d = _normalize_date(m.group(1), m.group(2), m.group(3))
157
+ if d:
158
+ return d
159
+ # ② 任意日期(取第一个)
160
+ m = _DATE_RE.search(content)
161
+ if m:
162
+ d = _normalize_date(m.group(1), m.group(2), m.group(3))
163
+ if d:
164
+ return d
165
+ # mtime
166
+ try:
167
+ return datetime.fromtimestamp(file_mtime).strftime("%Y-%m-%d")
168
+ except (OSError, ValueError, OverflowError):
169
+ pass
170
+ # 今天
171
+ return date.today().strftime("%Y-%m-%d")
172
+
173
+
174
+ def _normalize_date(y: str, m: str, d: str) -> str:
175
+ try:
176
+ return "%s-%02d-%02d" % (y.zfill(4), int(m), int(d))
177
+ except (ValueError, TypeError):
178
+ return ""
179
+
180
+
181
+ def extract_reqno(filename: str) -> str:
182
+ """从文件名提取 REQ 编号段,如 'REQ-2026-005'。取不到则用文件名去扩展名。
183
+ 兼容标准格式 REQ-2026-005 和老格式 REQ-012(单段)。
184
+ """
185
+ m = _REQ_RE.search(filename)
186
+ if m:
187
+ year = m.group(1).zfill(4)
188
+ if m.group(2): # 标准格式 REQ-2026-005
189
+ return "REQ-%s-%s" % (year, m.group(2))
190
+ return "REQ-%s" % year # 老格式 REQ-012
191
+ base = os.path.splitext(filename)[0]
192
+ return base[:40]
193
+
194
+
195
+ # ============================================================
196
+ # 文件名安全化(中文保留,去掉文件系统非法字符)
197
+ # ============================================================
198
+
199
+ _UNSAFE_FS = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
200
+
201
+
202
+ def _sanitize_filename(name: str, max_len: int = 60) -> str:
203
+ """文件名安全化:保留中文/字母/数字/连字符,去掉非法字符,限长。"""
204
+ name = _UNSAFE_FS.sub("", name).strip()
205
+ name = re.sub(r"\s+", "-", name) # 空格转连字符
206
+ if len(name) > max_len:
207
+ name = name[:max_len].rstrip("-")
208
+ return name or "未命名"
209
+
210
+
211
+ # ============================================================
212
+ # 核心:归档单个 PRD
213
+ # ============================================================
214
+
215
+ def archive_one(prd_path: str, repo_root: Path) -> bool:
216
+ """把一个 PRD 归档到当前成员的个人区。返回是否成功。"""
217
+ src = Path(prd_path)
218
+ if not src.is_absolute():
219
+ src = (Path.cwd() / src).resolve()
220
+ if not src.is_file():
221
+ print("[archive] 跳过: 文件不存在 %s" % prd_path)
222
+ return False
223
+
224
+ developer = get_developer(repo_root)
225
+ if not developer:
226
+ print("[archive] 跳过: 未初始化开发者身份 ( /wl-init)")
227
+ return False
228
+
229
+ members = MEMBERS_DIR if MEMBERS_DIR else (repo_root / "workspace" / "members")
230
+ archive_root = Path(members) / developer / "archive"
231
+
232
+ content = _read_text_safe(src)
233
+ fname = src.name
234
+ reqno = extract_reqno(fname)
235
+ # 文件名里的 slug(REQ-2026-005-xxx.md 的 xxx 部分)作为最后兜底标题
236
+ slug_fallback = _REQ_RE.sub("", os.path.splitext(fname)[0]).lstrip("-_") or "未命名"
237
+ title = extract_title(content, slug_fallback)
238
+ prd_date = extract_date(content, src.stat().st_mtime)
239
+
240
+ # 目标:members/{dev}/archive/{YYYY-MM-DD}/REQ-{编号}-{中文标题}.md
241
+ day_dir = archive_root / prd_date
242
+ day_dir.mkdir(parents=True, exist_ok=True)
243
+ safe_title = _sanitize_filename(title)
244
+ dst_name = "%s-%s.md" % (reqno, safe_title)
245
+ dst = day_dir / dst_name
246
+
247
+ import shutil
248
+ shutil.copy2(src, dst)
249
+
250
+ # 更新当天 _index.md
251
+ _write_day_index(day_dir, prd_date)
252
+ # 更新 archive 根的 _index.md(周/月汇总)
253
+ _write_overview_index(archive_root, developer)
254
+
255
+ rel = dst.relative_to(repo_root) if str(dst).startswith(str(repo_root)) else dst
256
+ print("[archive] 已归档: %s" % rel)
257
+ return True
258
+
259
+
260
+ def _collect_day_prds(day_dir: Path):
261
+ """收集某天文件夹里的 PRD(.md 且不是 _index)。返回 [(reqno, title, fname), ...]"""
262
+ items = []
263
+ for f in sorted(day_dir.iterdir()):
264
+ if not f.is_file() or not f.name.lower().endswith(".md"):
265
+ continue
266
+ if f.name.startswith("_"):
267
+ continue
268
+ stem = f.stem # REQ-2026-005-异常中心全国看板
269
+ m = _REQ_RE.search(stem)
270
+ reqno = "REQ-%s-%s" % (m.group(1).zfill(4), m.group(2)) if m else stem
271
+ title = _REQ_RE.sub("", stem).lstrip("-_ ") or stem
272
+ items.append((reqno, title, f.name))
273
+ return items
274
+
275
+
276
+ def _write_day_index(day_dir: Path, prd_date: str):
277
+ """写当天的 _index.md:一个清爽的当天需求清单。"""
278
+ items = _collect_day_prds(day_dir)
279
+ lines = ["# %s 的需求产出\n" % prd_date, "> 共 %d 个需求\n" % len(items)]
280
+ if items:
281
+ for reqno, title, fname in items:
282
+ lines.append("- **%s** %s — [`%s`](./%s)" % (reqno, title, fname, fname))
283
+ else:
284
+ lines.append("(无)")
285
+ (day_dir / "_index.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
286
+
287
+
288
+ def _iso_week_range(d: date):
289
+ """返回 d 所在自然周的 周一~周日(ISO 周,周一为起点)。"""
290
+ monday = d - timedelta(days=d.weekday()) # weekday() 周一=0
291
+ sunday = monday + timedelta(days=6)
292
+ return monday, sunday
293
+
294
+
295
+ def _write_overview_index(archive_root: Path, developer: str):
296
+ """写 archive 根的 _index.md:按 周 / 月 汇总,给「我这阵子做了啥」一个总览。"""
297
+ if not archive_root.is_dir():
298
+ return
299
+ # 扫所有日期文件夹
300
+ days = []
301
+ for d in sorted(archive_root.iterdir()):
302
+ if not d.is_dir():
303
+ continue
304
+ try:
305
+ day = date.fromisoformat(d.name) # 只认 YYYY-MM-DD
306
+ except ValueError:
307
+ continue
308
+ items = _collect_day_prds(d)
309
+ if items:
310
+ days.append((day, items))
311
+ if not days:
312
+ return
313
+
314
+ lines = ["# %s 的需求归档\n" % developer, "> 按时间翻查你的需求产出。物理按天归档,下面是周/月汇总。\n"]
315
+
316
+ # ---- 周视图(按自然周聚合)----
317
+ lines.append("## 按周\n")
318
+ # 把连续的天聚到同一周
319
+ weeks = {} # week_key(周一) -> [(day, items)]
320
+ order = []
321
+ for day, items in days:
322
+ monday, _ = _iso_week_range(day)
323
+ if monday not in weeks:
324
+ weeks[monday] = []
325
+ order.append(monday)
326
+ weeks[monday].append((day, items))
327
+ for monday in sorted(order, reverse=True):
328
+ sunday = monday + timedelta(days=6)
329
+ week_items = weeks[monday]
330
+ total = sum(len(it) for _, it in week_items)
331
+ lines.append(
332
+ "### %s %d 周(%s ~ %s)— %d 个需求\n"
333
+ % (monday.year, monday.isocalendar()[1], monday.isoformat(), sunday.isoformat(), total)
334
+ )
335
+ for day, items in sorted(week_items, reverse=True):
336
+ summary = "、".join("%s %s" % (r, t) for r, t, _ in items)
337
+ lines.append("- **%s**:%s" % (day.isoformat(), summary))
338
+ lines.append("")
339
+
340
+ # ---- 月视图 ----
341
+ lines.append("## 按月\n")
342
+ months = {}
343
+ morder = []
344
+ for day, items in days:
345
+ mk = (day.year, day.month)
346
+ if mk not in months:
347
+ months[mk] = []
348
+ morder.append(mk)
349
+ months[mk].append((day, items))
350
+ for mk in sorted(morder, reverse=True):
351
+ m_items = months[mk]
352
+ total = sum(len(it) for _, it in m_items)
353
+ lines.append("### %d年%d月 — %d 个需求" % (mk[0], mk[1], total))
354
+ for day, items in sorted(m_items, reverse=True):
355
+ summary = "、".join("%s" % t for _, t, _ in items)
356
+ lines.append(" - %s:%s" % (day.isoformat(), summary))
357
+ lines.append("")
358
+
359
+ (archive_root / "_index.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
360
+
361
+
362
+ # ============================================================
363
+ # main
364
+ # ============================================================
365
+
366
+ def main():
367
+ args = sys.argv[1:]
368
+ # 过滤 flag
369
+ prd_args = [a for a in args if not a.startswith("-")]
370
+ if not prd_args:
371
+ print("Usage: python .qoder/scripts/archive_prd.py <prd 文件路径> [<prd2> ...]")
372
+ print(" PRD 归档到当前成员的 workspace/members/{name}/archive/{日期}/")
373
+ sys.exit(1)
374
+
375
+ repo_root = get_repo_root()
376
+ ok = 0
377
+ for p in prd_args:
378
+ try:
379
+ if archive_one(p, repo_root):
380
+ ok += 1
381
+ except Exception as e:
382
+ # 非侵入:归档失败绝不影响 PRD 生成
383
+ print("[archive] 跳过 %s: %s" % (p, str(e)[:120]))
384
+ print("[archive] 完成: %d/%d 已归档" % (ok, len(prd_args)))
385
+ sys.exit(0)
386
+
387
+
388
+ if __name__ == "__main__":
389
+ main()