| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- #!/usr/bin/env python3
- """派生 MD 视图:以知识库 JSON 为唯一事实源,生成 Markdown 表格视图。
- 用法:
- python3 render_kb_md.py [知识库目录] [输出目录]
- python3 render_kb_md.py --check [知识库目录] [输出目录]
- 输出:<输出目录>/views/{supplements,clinical_indicators,exercises,interactions,protocols}.md
- 约定:JSON 为单一事实源,MD 为派生视图;改动数据只改 JSON,随后重跑本脚本。
- --check 模式:重生成到临时目录并与现视图对比,不一致退出码 1(供 CI/提交流程)。
- """
- import json
- import sys
- import tempfile
- from pathlib import Path
- DEFAULT_KB = "docs/参考资料/知识库"
- def load(path: Path):
- return json.loads(path.read_text(encoding="utf-8"))
- def md_table(headers, rows):
- out = ["| " + " | ".join(headers) + " |", "| " + " | ".join(["---"] * len(headers)) + " |"]
- for r in rows:
- cells = [str(c).replace("|", "\\|").replace("\n", " ") for c in r]
- out.append("| " + " | ".join(cells) + " |")
- return "\n".join(out)
- def render_supplements(doc):
- rows = []
- for s in doc["supplements"]:
- ings = ", ".join(f"{i['name']} {i['amount']}" for i in s["key_ingredients"])
- rows.append([s["id"], s["name"], s.get("brand") or "—", s["category"], s["form"],
- ings, s["dose_reference"], s["timing"], s.get("interaction_notes") or "—",
- s.get("contraindications") or "—", s.get("evidence_level", "—"),
- "✔" if s.get("enabled", True) else "✘"])
- return ("# 补充剂库(视图)\n\n> 由 supplements.json 派生,勿手工编辑。\n\n"
- + md_table(["id", "名称", "品牌", "类别", "剂型", "关键成分", "参考剂量", "时段",
- "相互作用注意", "禁忌", "证据", "启用"], rows))
- def render_indicators(doc):
- rows = []
- for ind in doc["indicators"]:
- nr = ind.get("normal_range", {})
- lo, hi = nr.get("low"), nr.get("high")
- rng = f"{lo}~{hi}" if lo is not None and hi is not None else (str(lo or hi))
- rt = ind.get("risk_thresholds", {})
- warn = "; ".join(x for x in [f"低<{rt['low_warn']}" if rt.get("low_warn") else "",
- f"高>{rt['high_warn']}" if rt.get("high_warn") else "",
- rt.get("note") or ""] if x)
- rows.append([ind["id"], ind["name"], ind.get("english_name", ""), ind["unit"], rng,
- warn, ind.get("monitoring_frequency_hint") or "—",
- ind.get("related_condition") or "—", ind.get("population_note") or "—"])
- return ("# 临床监测指标参考库(视图)\n\n> 由 clinical_indicators.json 派生,勿手工编辑。\n\n"
- + md_table(["id", "指标", "英文", "单位", "参考范围", "干预阈值", "复查频率",
- "相关疾病", "人群差异"], rows))
- def render_exercises(doc):
- rows = []
- for e in doc["exercises"]:
- rows.append([e["id"], e["name"], "/".join(e.get("aliases", [])), e["type"], e["intensity"],
- e["duration_min"], e["frequency"], e.get("timing_pref") or "—",
- e.get("suitable_population") or "—", e.get("safety_notes") or "—"])
- return ("# 运动处方库(视图)\n\n> 由 exercises.json 派生,勿手工编辑。\n\n"
- + md_table(["id", "名称", "别名", "类型", "强度", "时长(分)", "频率", "时段偏好",
- "适宜人群", "安全注意"], rows))
- def render_interactions(doc):
- def subj(s):
- return s.get("supplement_id") or s.get("substance", "")
- rows = []
- for ix in doc["interactions"]:
- rows.append([ix["id"], ix["type"], subj(ix["subjects_a"]), subj(ix["subjects_b"]),
- ix["rule"], ix["detail"], ix["severity"], ix.get("source") or "—"])
- return ("# 补剂相互作用库(视图)\n\n> 由 interactions.json 派生,勿手工编辑。\n\n"
- + md_table(["id", "类型", "主体A", "主体B", "规则", "说明", "等级", "来源"], rows))
- def render_protocols(docs, supplement_names):
- out = ["# 干预方案库(视图)\n", "> 由 protocols/*.json 派生,勿手工编辑。\n"]
- for doc in docs:
- out.append(f"\n## {doc['name']}(`{doc['id']}`)\n")
- out.append(f"> 来源文档:{doc.get('source_doc', '—')}\n")
- cycles = {c["id"]: c for c in doc.get("cycles", [])}
- out.append(md_table(["周期", "阶段", "起", "止"], [[cid, c.get("name"), c.get("start"), c.get("end")]
- for cid, c in cycles.items()]))
- for member in doc.get("members", []):
- out.append(f"\n### 成员:{member['name']}(family {member['family_id']},user {member['user_id']},手机 {member['phone']})\n")
- for phase in member.get("phases", []):
- cyc = cycles.get(phase["cycle_id"], {})
- out.append(f"\n#### {cyc.get('name', phase['cycle_id'])} 补剂\n")
- rows = []
- for slot in phase.get("time_slots", []):
- names = "、".join(supplement_names.get(sid, sid) for sid in slot.get("supplement_ids", []))
- rows.append([slot["time"], slot["label"], names, slot.get("dose", ""), slot.get("notes", "")])
- out.append(md_table(["时段", "标签", "补剂", "剂量", "备注"], rows))
- dm = member.get("daily_monitoring", [])
- if dm:
- out.append("\n#### 每日监测\n")
- out.append(md_table(["时间", "内容", "描述", "类型"],
- [[d["time"], d["name"], d.get("description", ""), d.get("kind", "")] for d in dm]))
- wm = member.get("weekly_monitoring", [])
- if wm:
- out.append("\n#### 每周监测\n")
- out.append(md_table(["周期", "时间", "内容", "描述", "关联指标"],
- [[w["weekday"], w.get("time", ""), w["name"], w.get("description", ""),
- "、".join(w.get("indicator_ids", []))] for w in wm]))
- fu = member.get("followups", [])
- if fu:
- out.append("\n#### 复查节点\n")
- out.append(md_table(["周期", "日期", "项目", "关联指标", "说明"],
- [[f.get("week", ""), f.get("due", ""), f["name"],
- "、".join(f.get("indicator_ids", [])), f.get("note", "")] for f in fu]))
- ex = member.get("exercise_tasks", [])
- if ex:
- out.append("\n#### 运动任务\n")
- out.append(md_table(["时间", "任务", "描述", "引用运动"],
- [[e["time"], e["title"], e.get("description", ""),
- "、".join(e.get("exercise_ids", []))] for e in ex]))
- return "\n".join(out)
- def main():
- argv = sys.argv[1:]
- check = False
- if argv and argv[0] == "--check":
- check = True
- argv = argv[1:]
- kb_dir = Path(argv[0]) if argv else Path(DEFAULT_KB)
- out_dir = Path(argv[1]) if len(argv) > 1 else kb_dir
- supplements = load(kb_dir / "supplements.json")
- indicators = load(kb_dir / "clinical_indicators.json")
- exercises = load(kb_dir / "exercises.json")
- interactions = load(kb_dir / "interactions.json")
- protocols = [load(p) for p in sorted((kb_dir / "protocols").glob("*.json"))]
- names = {s["id"]: s["name"] for s in supplements["supplements"]}
- rendered = {
- "supplements.md": render_supplements(supplements),
- "clinical_indicators.md": render_indicators(indicators),
- "exercises.md": render_exercises(exercises),
- "interactions.md": render_interactions(interactions),
- "protocols.md": render_protocols(protocols, names),
- }
- target = out_dir / "views"
- if check:
- with tempfile.TemporaryDirectory() as tmp:
- tmp_dir = Path(tmp) / "views"
- tmp_dir.mkdir(parents=True, exist_ok=True)
- for name, text in rendered.items():
- (tmp_dir / name).write_text(text + "\n", encoding="utf-8")
- diff_count = 0
- for name in rendered:
- existing = target / name
- if not existing.exists():
- print(f"[DIFF] {name} 缺失")
- diff_count += 1
- continue
- a = (tmp_dir / name).read_text(encoding="utf-8").splitlines()
- b = existing.read_text(encoding="utf-8").splitlines()
- if a != b:
- diffs = list(difflib.unified_diff(b, a, fromfile=name, tofile=name, lineterm=""))
- print(f"[DIFF] {name}: {len(diffs)} 行差异(JSON 与视图不同步)")
- print("\n".join(diffs[:20]))
- diff_count += 1
- print("RESULT:", "FAIL" if diff_count else "PASS")
- sys.exit(1 if diff_count else 0)
- else:
- target.mkdir(parents=True, exist_ok=True)
- for name, text in rendered.items():
- (target / name).write_text(text + "\n", encoding="utf-8")
- print(f"wrote {target / name}")
- print("RESULT: PASS")
- if __name__ == "__main__":
- main()
|