@hupan56/wlkj 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +261 -11
- package/package.json +1 -1
- package/templates/qoder/agents/insight-planning.md +67 -0
- package/templates/qoder/agents/insight-research.md +61 -0
- package/templates/qoder/agents/prd-reference.md +8 -2
- package/templates/qoder/commands/optional/wl-insight.md +275 -0
- package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
- package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
- package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
- package/templates/qoder/commands/wl-code.md +10 -2
- package/templates/qoder/commands/wl-commit.md +1 -1
- package/templates/qoder/commands/wl-design-draw.md +78 -0
- package/templates/qoder/commands/wl-design-scan.md +108 -0
- package/templates/qoder/commands/wl-design-spec.md +154 -0
- package/templates/qoder/commands/wl-design.md +32 -0
- package/templates/qoder/commands/wl-init.md +24 -3
- package/templates/qoder/commands/wl-prd-full.md +226 -0
- package/templates/qoder/commands/wl-prd-quick.md +134 -0
- package/templates/qoder/commands/wl-prd-review.md +104 -0
- package/templates/qoder/commands/wl-prd.md +17 -311
- package/templates/qoder/commands/wl-search.md +66 -30
- package/templates/qoder/commands/wl-task.md +290 -59
- package/templates/qoder/commands/wl-test.md +92 -24
- package/templates/qoder/config.yaml +51 -15
- package/templates/qoder/hooks/inject-workflow-state.py +2 -2
- package/templates/qoder/hooks/session-start.py +82 -51
- package/templates/qoder/rules/wl-pipeline.md +216 -105
- package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/archive_prd.py +377 -0
- package/templates/qoder/scripts/autotest.py +1715 -0
- package/templates/qoder/scripts/autotest_batch.py +224 -0
- package/templates/qoder/scripts/autotest_run.py +297 -0
- package/templates/qoder/scripts/benchmark.py +210 -209
- package/templates/qoder/scripts/build_style_index.py +444 -4
- package/templates/qoder/scripts/check_carriers.py +238 -0
- package/templates/qoder/scripts/check_mcp.py +298 -0
- package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
- package/templates/qoder/scripts/common/events.py +46 -0
- package/templates/qoder/scripts/common/extract.py +419 -0
- package/templates/qoder/scripts/common/graph_traverse.py +533 -0
- package/templates/qoder/scripts/common/paths.py +89 -0
- package/templates/qoder/scripts/common/pip_install.py +144 -0
- package/templates/qoder/scripts/common/platform_guard.py +61 -0
- package/templates/qoder/scripts/common/search_engine.py +205 -205
- package/templates/qoder/scripts/common/terms.py +57 -0
- package/templates/qoder/scripts/common/ts_extract.py +536 -0
- package/templates/qoder/scripts/context_pack.py +73 -13
- package/templates/qoder/scripts/enrich_prompt.py +226 -0
- package/templates/qoder/scripts/eval_prd.py +318 -231
- package/templates/qoder/scripts/export.py +487 -487
- package/templates/qoder/scripts/extract_api_params.py +246 -0
- package/templates/qoder/scripts/extract_routes.py +54 -0
- package/templates/qoder/scripts/extract_routes_tree.py +78 -0
- package/templates/qoder/scripts/fill_prototype.py +707 -0
- package/templates/qoder/scripts/gen_design_doc.py +394 -0
- package/templates/qoder/scripts/git_sync.py +27 -15
- package/templates/qoder/scripts/init_doctor.py +292 -40
- package/templates/qoder/scripts/install_qoderwork.py +366 -9
- package/templates/qoder/scripts/kg.py +708 -0
- package/templates/qoder/scripts/kg_auto_login.py +196 -0
- package/templates/qoder/scripts/kg_build.py +612 -0
- package/templates/qoder/scripts/kg_build_db.py +327 -0
- package/templates/qoder/scripts/kg_duckdb.py +549 -0
- package/templates/qoder/scripts/kg_incremental.py +393 -0
- package/templates/qoder/scripts/kg_link_db.py +224 -0
- package/templates/qoder/scripts/kg_mcp_server.py +801 -0
- package/templates/qoder/scripts/kg_semantic.py +150 -0
- package/templates/qoder/scripts/kg_test_runner.py +241 -0
- package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
- package/templates/qoder/scripts/learn.py +118 -39
- package/templates/qoder/scripts/learn_aggregate.py +201 -0
- package/templates/qoder/scripts/mcp_launcher.py +359 -0
- package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
- package/templates/qoder/scripts/repo_root.py +106 -0
- package/templates/qoder/scripts/role.py +12 -0
- package/templates/qoder/scripts/run_weekly_update.bat +5 -0
- package/templates/qoder/scripts/run_weekly_update.sh +5 -0
- package/templates/qoder/scripts/search_index.py +307 -60
- package/templates/qoder/scripts/secure-ls.js +5640 -0
- package/templates/qoder/scripts/setup.py +706 -641
- package/templates/qoder/scripts/setup_lanhu.py +963 -0
- package/templates/qoder/scripts/status.py +250 -11
- package/templates/qoder/scripts/sync_carriers.py +259 -0
- package/templates/qoder/scripts/syncgate.py +2 -2
- package/templates/qoder/scripts/task.py +75 -0
- package/templates/qoder/scripts/team_sync.py +60 -4
- package/templates/qoder/scripts/workspace_init.py +1 -1
- package/templates/qoder/skills/design-import/SKILL.md +226 -0
- package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
- package/templates/qoder/skills/design-review/SKILL.md +82 -25
- package/templates/qoder/skills/prd-generator/SKILL.md +184 -60
- package/templates/qoder/skills/prd-review/SKILL.md +18 -1
- package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
- package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
- package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
- package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
- package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
- package/templates/qoder/skills/test-generator/SKILL.md +15 -2
- package/templates/qoder/skills/wl-code/SKILL.md +55 -36
- package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
- package/templates/qoder/skills/wl-design/SKILL.md +55 -0
- package/templates/qoder/skills/wl-init/SKILL.md +76 -67
- package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
- package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
- package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
- package/templates/qoder/skills/wl-report/SKILL.md +131 -107
- package/templates/qoder/skills/wl-search/SKILL.md +141 -75
- package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
- package/templates/qoder/skills/wl-status/SKILL.md +83 -61
- package/templates/qoder/skills/wl-task/SKILL.md +132 -58
- package/templates/qoder/skills/wl-test/SKILL.md +406 -40
- package/templates/qoder/templates/prototype-app.html +13 -8
- package/templates/qoder/templates/prototype-web.html +376 -93
- package/templates/root/AGENTS.md +89 -34
- package/templates/root/requirements.txt +21 -0
- package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
- package/templates/qoder/agents/prd-planning.md +0 -57
- package/templates/qoder/agents/prd-research.md +0 -34
- package/templates/qoder/commands/wl-insight.md +0 -51
- package/templates/qoder/scripts/__pycache__/init_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/setup.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/common/__pycache__/developer.cpython-39.pyc +0 -0
- package/templates/qoder/skills/wl-prd/SKILL.md +0 -92
|
@@ -0,0 +1,377 @@
|
|
|
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()
|