#!/usr/bin/env python3 """根据 catalog.json 中每个条目的 week 字段,在 plan/weekNN-*.md 中生成“本周相关专题”一节。 - 生成内容放在 和 之间,重复运行会替换,不会重复插入 - 第一次插入的位置:“## 本周自查清单 / ## 本周验收”之前(代码块之外);没有这两个标题时放在文末 - 先运行 tools/build_catalog.py 生成 catalog.json 用法:python3 tools/gen_week_topics.py [--check] (--check 只检查是否需要更新,需要时返回 1) """ from __future__ import annotations import json import re import sys from collections import defaultdict from pathlib import Path ROOT = Path(__file__).resolve().parents[1] START, END = "", "" ANCHOR = re.compile(r"^## (本周自查清单|本周验收)") FENCE = re.compile(r"^\s*(```|~~~)") TYPE_ORDER = {"lesson": 0, "scenario": 1, "interview": 2} TYPE_NAME = {"lesson": "课程", "scenario": "场景", "interview": "面试题"} AI_CODING_NOTE = ("- **AI 编程练习(每周)**:本周至少一个功能用 AI 编程工具(Cursor / Claude Code / Codex 等)完成," "按 [AI 编程:工作流](../topics/ai/ai-coding/01-workflow-rules-skills.md) 写清需求和规则," "按 [验证与证据](../topics/ai/ai-coding/02-verify-evidence.md) 保存测试结果和会话记录(脱敏后)," "第 15 周整理成“AI 编程 + 验证方法”的应聘案例。") def build_section(week: int, items: list[dict], stacks: dict[str, dict]) -> str: by_stack: dict[str, list[dict]] = defaultdict(list) cross: list[dict] = [] for it in items: if it["type"] == "scenario" and it["domain"] == "cross": cross.append(it) elif it["type"] in TYPE_ORDER and it.get("stack"): by_stack[it["path"].split("/")[2]].append(it) lines = [START, "", "## 本周相关专题", "", "> 本节由 `tools/gen_week_topics.py` 根据专题文件的 `week` 字段自动生成,请勿手改;" "想调整就修改对应专题文件的 frontmatter 后重新生成。", ""] if week <= 14: lines += [AI_CODING_NOTE, ""] if by_stack: lines += ["| 技术栈 | 课程 / 场景 / 面试题 |", "| --- | --- |"] for sid in sorted(by_stack, key=lambda s: (stacks.get(s, {}).get("domain", ""), s)): st = stacks.get(sid, {}) name = f"[{st.get('title', sid)}](../{st['index']})" if st.get("index") else sid links = [] for it in sorted(by_stack[sid], key=lambda x: (TYPE_ORDER[x["type"]], x.get("order") or 0, x["path"])): title = TYPE_NAME[it["type"]] if it["type"] == "interview" else it["title"].split(":")[0] links.append(f"{TYPE_NAME[it['type']]}:[{title}](../{it['path']})" if it["type"] != "interview" else f"[面试题](../{it['path']})") lines.append(f"| {name} | {';'.join(links)} |") lines.append("") if cross: lines += ["**跨技术栈场景**:" + "、".join(f"[{c['title'].split(':')[0]}](../{c['path']})" for c in sorted(cross, key=lambda c: c["path"])), ""] lines.append(END) return "\n".join(lines) def insert(text: str, section: str) -> str: if START in text and END in text: return re.sub(re.escape(START) + r".*?" + re.escape(END), lambda _: section, text, count=1, flags=re.S) in_fence = False lines = text.split("\n") for i, line in enumerate(lines): if FENCE.match(line): in_fence = not in_fence elif not in_fence and ANCHOR.match(line): return "\n".join(lines[:i] + [section, ""] + lines[i:]) return text.rstrip("\n") + "\n\n" + section + "\n" def main() -> int: check = "--check" in sys.argv cat = json.loads((ROOT / "catalog.json").read_text(encoding="utf-8")) stacks = {s["stack"]: s for s in cat["stacks"]} per_week: dict[int, list[dict]] = defaultdict(list) for it in cat["items"]: if it["path"].startswith(("topics/", "scenarios/")): for w in it.get("week") or []: per_week[int(w)].append(it) stale = [] for f in sorted((ROOT / "plan").glob("week*.md")): week = int(re.match(r"week(\d+)", f.name).group(1)) old = f.read_text(encoding="utf-8") new = insert(old, build_section(week, per_week.get(week, []), stacks)) if new != old: stale.append(f.name) if not check: f.write_text(new, encoding="utf-8") if check: if stale: print("需要重新生成:" + "、".join(stale)) return 1 print("各周“本周相关专题”已是最新") return 0 print(f"已更新 {len(stale)} 个周计划文件") return 0 if __name__ == "__main__": sys.exit(main())