render_kb_md.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. #!/usr/bin/env python3
  2. """派生 MD 视图:以知识库 JSON 为唯一事实源,生成 Markdown 表格视图。
  3. 自 v3.0 起,所有子库已并入「统一知识库.json」(整体知识库基础)。本脚本改为从
  4. 统一知识库内的对应 section 读取并渲染。
  5. 用法:
  6. python3 render_kb_md.py [知识库目录] [输出目录]
  7. python3 render_kb_md.py --check [知识库目录] [输出目录]
  8. 输出:<输出目录>/views/{supplements,clinical_indicators,exercises,interactions,protocols}.md
  9. 约定:JSON 为单一事实源,MD 为派生视图;改动数据只改 JSON,随后重跑本脚本。
  10. --check 模式:重生成到临时目录并与现视图对比,不一致退出码 1(供 CI/提交流程)。
  11. """
  12. import json
  13. import sys
  14. import tempfile
  15. import difflib
  16. from pathlib import Path
  17. DEFAULT_KB = "docs/参考资料/知识库"
  18. UNIFIED = "统一知识库.json"
  19. def load(path: Path):
  20. return json.loads(path.read_text(encoding="utf-8"))
  21. def md_table(headers, rows):
  22. out = ["| " + " | ".join(headers) + " |", "| " + " | ".join(["---"] * len(headers)) + " |"]
  23. for r in rows:
  24. cells = [str(c).replace("|", "\\|").replace("\n", " ") for c in r]
  25. out.append("| " + " | ".join(cells) + " |")
  26. return "\n".join(out)
  27. def render_supplements(doc):
  28. rows = []
  29. for s in doc["supplements"]:
  30. ings = ", ".join(f"{i['name']} {i['amount']}" for i in s["key_ingredients"])
  31. rows.append([s["id"], s["name"], s.get("brand") or "—", s["category"], s["form"],
  32. ings, s["dose_reference"], s["timing"], s.get("interaction_notes") or "—",
  33. s.get("contraindications") or "—", s.get("evidence_level", "—"),
  34. "✔" if s.get("enabled", True) else "✘"])
  35. return ("# 补充剂库(视图)\n\n> 由 supplements.json 派生,勿手工编辑。\n\n"
  36. + md_table(["id", "名称", "品牌", "类别", "剂型", "关键成分", "参考剂量", "时段",
  37. "相互作用注意", "禁忌", "证据", "启用"], rows))
  38. def render_indicators(doc):
  39. rows = []
  40. for ind in doc["indicators"]:
  41. nr = ind.get("normal_range", {})
  42. lo, hi = nr.get("low"), nr.get("high")
  43. rng = f"{lo}~{hi}" if lo is not None and hi is not None else (str(lo or hi))
  44. rt = ind.get("risk_thresholds", {})
  45. warn = "; ".join(x for x in [f"低<{rt['low_warn']}" if rt.get("low_warn") else "",
  46. f"高>{rt['high_warn']}" if rt.get("high_warn") else "",
  47. rt.get("note") or ""] if x)
  48. rows.append([ind["id"], ind["name"], ind.get("english_name", ""), ind["unit"], rng,
  49. warn, ind.get("monitoring_frequency_hint") or "—",
  50. ind.get("related_condition") or "—", ind.get("population_note") or "—"])
  51. return ("# 临床监测指标参考库(视图)\n\n> 由 clinical_indicators.json 派生,勿手工编辑。\n\n"
  52. + md_table(["id", "指标", "英文", "单位", "参考范围", "干预阈值", "复查频率",
  53. "相关疾病", "人群差异"], rows))
  54. def render_exercises(doc):
  55. rows = []
  56. for e in doc["exercises"]:
  57. rows.append([e["id"], e["name"], "/".join(e.get("aliases", [])), e["type"], e["intensity"],
  58. e["duration_min"], e["frequency"], e.get("timing_pref") or "—",
  59. e.get("suitable_population") or "—", e.get("safety_notes") or "—"])
  60. return ("# 运动处方库(视图)\n\n> 由 exercises.json 派生,勿手工编辑。\n\n"
  61. + md_table(["id", "名称", "别名", "类型", "强度", "时长(分)", "频率", "时段偏好",
  62. "适宜人群", "安全注意"], rows))
  63. def render_interactions(doc):
  64. def subj(s):
  65. return s.get("supplement_id") or s.get("substance", "")
  66. rows = []
  67. for ix in doc["interactions"]:
  68. rows.append([ix["id"], ix["type"], subj(ix["subjects_a"]), subj(ix["subjects_b"]),
  69. ix["rule"], ix["detail"], ix["severity"], ix.get("source") or "—"])
  70. return ("# 补剂相互作用库(视图)\n\n> 由 interactions.json 派生,勿手工编辑。\n\n"
  71. + md_table(["id", "类型", "主体A", "主体B", "规则", "说明", "等级", "来源"], rows))
  72. def render_protocols(docs, supplement_names):
  73. out = ["# 干预方案库(视图)\n", "> 由 protocols/*.json 派生,勿手工编辑。\n"]
  74. for doc in docs:
  75. out.append(f"\n## {doc['name']}(`{doc['id']}`)\n")
  76. out.append(f"> 来源文档:{doc.get('source_doc', '—')}\n")
  77. cycles = {c["id"]: c for c in doc.get("cycles", [])}
  78. out.append(md_table(["周期", "阶段", "起", "止"], [[cid, c.get("name"), c.get("start"), c.get("end")]
  79. for cid, c in cycles.items()]))
  80. for member in doc.get("members", []):
  81. out.append(f"\n### 成员:{member['name']}(family {member['family_id']},user {member['user_id']},手机 {member['phone']})\n")
  82. for phase in member.get("phases", []):
  83. cyc = cycles.get(phase["cycle_id"], {})
  84. out.append(f"\n#### {cyc.get('name', phase['cycle_id'])} 补剂\n")
  85. rows = []
  86. for slot in phase.get("time_slots", []):
  87. names = "、".join(supplement_names.get(sid, sid) for sid in slot.get("supplement_ids", []))
  88. rows.append([slot["time"], slot["label"], names, slot.get("dose", ""), slot.get("notes", "")])
  89. out.append(md_table(["时段", "标签", "补剂", "剂量", "备注"], rows))
  90. dm = member.get("daily_monitoring", [])
  91. if dm:
  92. out.append("\n#### 每日监测\n")
  93. out.append(md_table(["时间", "内容", "描述", "类型"],
  94. [[d["time"], d["name"], d.get("description", ""), d.get("kind", "")] for d in dm]))
  95. wm = member.get("weekly_monitoring", [])
  96. if wm:
  97. out.append("\n#### 每周监测\n")
  98. out.append(md_table(["周期", "时间", "内容", "描述", "关联指标"],
  99. [[w["weekday"], w.get("time", ""), w["name"], w.get("description", ""),
  100. "、".join(w.get("indicator_ids", []))] for w in wm]))
  101. fu = member.get("followups", [])
  102. if fu:
  103. out.append("\n#### 复查节点\n")
  104. out.append(md_table(["周期", "日期", "项目", "关联指标", "说明"],
  105. [[f.get("week", ""), f.get("due", ""), f["name"],
  106. "、".join(f.get("indicator_ids", [])), f.get("note", "")] for f in fu]))
  107. ex = member.get("exercise_tasks", [])
  108. if ex:
  109. out.append("\n#### 运动任务\n")
  110. out.append(md_table(["时间", "任务", "描述", "引用运动"],
  111. [[e["time"], e["title"], e.get("description", ""),
  112. "、".join(e.get("exercise_ids", []))] for e in ex]))
  113. return "\n".join(out)
  114. def main():
  115. argv = sys.argv[1:]
  116. check = False
  117. if argv and argv[0] == "--check":
  118. check = True
  119. argv = argv[1:]
  120. kb_dir = Path(argv[0]) if argv else Path(DEFAULT_KB)
  121. out_dir = Path(argv[1]) if len(argv) > 1 else kb_dir
  122. unified = load(kb_dir / UNIFIED)
  123. supplements = unified["补充剂库"]
  124. indicators = unified["临床监测指标库"]
  125. exercises = unified["运动处方库"]
  126. interactions = unified["补剂相互作用库"]
  127. protocols = unified["干预方案库"]["protocols"]
  128. names = {s["id"]: s["name"] for s in supplements["supplements"]}
  129. rendered = {
  130. "supplements.md": render_supplements(supplements),
  131. "clinical_indicators.md": render_indicators(indicators),
  132. "exercises.md": render_exercises(exercises),
  133. "interactions.md": render_interactions(interactions),
  134. "protocols.md": render_protocols(protocols, names),
  135. }
  136. target = out_dir / "views"
  137. if check:
  138. with tempfile.TemporaryDirectory() as tmp:
  139. tmp_dir = Path(tmp) / "views"
  140. tmp_dir.mkdir(parents=True, exist_ok=True)
  141. for name, text in rendered.items():
  142. (tmp_dir / name).write_text(text + "\n", encoding="utf-8")
  143. diff_count = 0
  144. for name in rendered:
  145. existing = target / name
  146. if not existing.exists():
  147. print(f"[DIFF] {name} 缺失")
  148. diff_count += 1
  149. continue
  150. a = (tmp_dir / name).read_text(encoding="utf-8").splitlines()
  151. b = existing.read_text(encoding="utf-8").splitlines()
  152. if a != b:
  153. diffs = list(difflib.unified_diff(b, a, fromfile=name, tofile=name, lineterm=""))
  154. print(f"[DIFF] {name}: {len(diffs)} 行差异(JSON 与视图不同步)")
  155. print("\n".join(diffs[:20]))
  156. diff_count += 1
  157. print("RESULT:", "FAIL" if diff_count else "PASS")
  158. sys.exit(1 if diff_count else 0)
  159. else:
  160. target.mkdir(parents=True, exist_ok=True)
  161. for name, text in rendered.items():
  162. (target / name).write_text(text + "\n", encoding="utf-8")
  163. print(f"wrote {target / name}")
  164. print("RESULT: PASS")
  165. if __name__ == "__main__":
  166. main()