104 lines
4.9 KiB
Python
104 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
||
"""根据 catalog.json 中每个条目的 week 字段,在 plan/weekNN-*.md 中生成“本周相关专题”一节。
|
||
|
||
- 生成内容放在 <!-- week-topics:start --> 和 <!-- week-topics:end --> 之间,重复运行会替换,不会重复插入
|
||
- 第一次插入的位置:“## 本周自查清单 / ## 本周验收”之前(代码块之外);没有这两个标题时放在文末
|
||
- 先运行 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 = "<!-- week-topics:start -->", "<!-- week-topics: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())
|