Browse Source

feat(kb): P4 干预方案驱动任务生成(协议 v1.1 + 离线生成器)

- protocols/gut_joint_intervention_v1.json 升级 v1.1:补 generation/notifications 块、
  监测 description、weekly time、followups due、exercise_ids/exercise_tasks
- schema/protocol.schema.json 同步 v1.1 字段
- validate_kb.py 支持 v1.1(exercise_ids/exercise_tasks 交叉校验)
- render_kb_md.py 协议视图适配 v1.1
- 新增 gen_tasks_from_protocol.py:由 protocols/*.json 生成幂等任务种子 SQL,
  替代硬编码 gen_seed.py;已与生产 45 条任务逐字段比对完全一致(幂等)
Xiaogang Liao 1 week ago
parent
commit
fa88a7542f

+ 167 - 0
docs/scripts/gen_tasks_from_protocol.py

@@ -0,0 +1,167 @@
+#!/usr/bin/env python3
+"""由干预方案库(protocols/*.json)生成任务种子 SQL,替代硬编码的 gen_seed.py。
+
+用法:
+    python3 gen_tasks_from_protocol.py [知识库目录] [选项]
+    python3 gen_tasks_from_protocol.py --out /tmp/protocol_seed.sql
+    python3 gen_tasks_from_protocol.py --summary
+
+默认读取 <知识库目录>/protocols/*.json(默认 docs/参考资料/知识库),
+输出幂等 SQL 到 stdout(或 --out 指定文件)。SQL 结构与历史 gen_seed.py 一致:
+- 时段补剂 -> 每日重复模板(title=【周期名】{time} {label}:{dose},desc=notes)
+- 每日监测 -> 每日重复模板(title=name,desc=description,跨全周期)
+- 每周监测 -> 每周重复模板(按 weekday,跨全周期)
+- 运动任务 -> 每日重复模板(title=title,desc=description,跨全周期)
+- 复查节点 -> 一次性任务(title=name,desc=note,deadline=due 09:00)
+- 提醒配置 -> 每成员一条 task_reminder_config(取 protocol.notifications)
+
+幂等:INSERT ... SELECT ... WHERE NOT EXISTS(title+family_id+executor_id+is_template),
+重复执行不产生重复行。生产执行需另经 go/no-go(本脚本仅生成 SQL,不连库)。
+"""
+import argparse
+import datetime as dt
+import json
+import re
+import sys
+from pathlib import Path
+
+DEFAULT_KB = "docs/参考资料/知识库"
+
+WEEKDAY = {"周一": 0, "周二": 1, "周三": 2, "周四": 3, "周五": 4, "周六": 5, "周日": 6, "周天": 6}
+
+COLS = ("family_id,creator_id,child_id,executor_type,executor_id,family_member_id,title,description,"
+        "points,category,need_review,review_by_category,status,is_template,task_type,repeat_type,frequency,"
+        "earliest_start,deadline,source_type,is_daily_progress,member_only,require_input,start_required,"
+        "min_duration_seconds,points_expire_days,duration")
+
+
+def esc(s):
+    return s.replace("'", "''") if s else s
+
+
+def ts(d, t):
+    return f"{d} {t}:00"
+
+
+def _tpl(base, title, desc, slot, start, end, freq, wd=None, category="health", source="health_plan"):
+    if freq == "weekly":
+        d = dt.date.fromisoformat(end)
+        while d.weekday() != wd:
+            d -= dt.timedelta(days=1)
+        deadline = ts(d.isoformat(), slot)
+    else:
+        deadline = ts(end, slot)
+    return dict(kind="tpl", **base, title=title, desc=desc, deadline=deadline,
+                earliest=ts(start, "00:00"), freq=freq, task_type="recurring",
+                category=category, source=source)
+
+
+def _once(base, title, desc, due, slot="09:00", category="health", source="health_plan"):
+    return dict(kind="once", **base, title=title, desc=desc, deadline=ts(due, slot),
+                earliest=None, freq="none", task_type="onetime", category=category, source=source)
+
+
+def build_rows(proto):
+    cycles = {c["id"]: c for c in proto["cycles"]}
+    gen = proto.get("generation", {})
+    category = gen.get("category", "health")
+    source = gen.get("source_type", "health_plan")
+    period_start = min(c["start"] for c in proto["cycles"])
+    period_end = max(c["end"] for c in proto["cycles"])
+    rows = []
+    for m in proto["members"]:
+        base = dict(user=m["user_id"], family=m["family_id"], fm=m["family_member_id"])
+        prefix_cycle = len(m["phases"]) > 1
+        for phase in m["phases"]:
+            cyc = cycles[phase["cycle_id"]]
+            for slot in phase["time_slots"]:
+                title = f"{slot['time']} {slot['label']}:{slot['dose']}"
+                if prefix_cycle:
+                    title = f"【{cyc['name']}】" + title
+                rows.append(_tpl(base, title, slot["notes"], slot["time"],
+                                 cyc["start"], cyc["end"], "daily", category=category, source=source))
+        for d in m.get("daily_monitoring", []):
+            rows.append(_tpl(base, d["name"], d.get("description", ""), d["time"],
+                             period_start, period_end, "daily", category=category, source=source))
+        for w in m.get("weekly_monitoring", []):
+            rows.append(_tpl(base, w["name"], w.get("description", ""), w.get("time", "09:00"),
+                             period_start, period_end, "weekly", wd=WEEKDAY[w["weekday"]],
+                             category=category, source=source))
+        for e in m.get("exercise_tasks", []):
+            rows.append(_tpl(base, e["title"], e.get("description", ""), e["time"],
+                             period_start, period_end, "daily", category=category, source=source))
+        for f in m.get("followups", []):
+            rows.append(_once(base, f["name"], f.get("note", ""), f["due"],
+                              category=category, source=source))
+    return rows
+
+
+def render_sql(protos):
+    out = ["-- CFC 干预方案库 -> 任务种子(幂等;由 gen_tasks_from_protocol.py 生成,勿手工编辑)"]
+    total_tpl = total_once = 0
+    for proto in protos:
+        rows = build_rows(proto)
+        out.append(f"-- 方案:{proto['name']}({proto['id']})"
+                   f"  周期 {min(c['start'] for c in proto['cycles'])}~{max(c['end'] for c in proto['cycles'])}")
+        out.append("")
+        for r in rows:
+            is_tpl = 1 if r["kind"] == "tpl" else 0
+            if is_tpl:
+                total_tpl += 1
+            else:
+                total_once += 1
+            earliest_sql = f"'{r['earliest']}'" if r["earliest"] else "NULL"
+            vals = (f"{r['family']},{r['user']},{r['fm']},'parent',{r['user']},{r['fm']},"
+                    f"'{esc(r['title'])}','{esc(r['desc'])}',1,'{r['category']}',0,0,'pending',{is_tpl},"
+                    f"'{r['task_type']}','none','{r['freq']}',{earliest_sql},'{r['deadline']}','{r['source']}',"
+                    f"0,0,0,0,0,0,0")
+            dup = (f"SELECT 1 FROM tasks t WHERE t.title='{esc(r['title'])}' AND t.family_id={r['family']} "
+                   f"AND t.executor_id={r['user']} AND t.is_template={is_tpl}")
+            out.append(f"INSERT INTO tasks ({COLS})\nSELECT {vals} FROM DUAL\nWHERE NOT EXISTS ({dup});")
+            out.append("")
+        notif = proto.get("notifications")
+        if notif:
+            for m in proto["members"]:
+                out.append(f"-- {m['name']} 提醒配置({notif['channels']},{notif['timing_mode']} "
+                           f"{notif['offset_minutes']}分钟)")
+                out.append(
+                    "INSERT INTO task_reminder_config (user_id,channels,timing_mode,offset_minutes,enabled)\n"
+                    f"SELECT {m['user_id']},'{notif['channels']}','{notif['timing_mode']}',"
+                    f"{notif['offset_minutes']},{notif['enabled']} FROM DUAL\n"
+                    f"WHERE NOT EXISTS (SELECT 1 FROM task_reminder_config WHERE user_id={m['user_id']});")
+                out.append("")
+    out.append(f"-- 统计:模板 {total_tpl} 条,一次性 {total_once} 条,合计 {total_tpl + total_once} 条")
+    return "\n".join(out), total_tpl, total_once
+
+
+def main():
+    ap = argparse.ArgumentParser(description="由干预方案库生成幂等任务种子 SQL")
+    ap.add_argument("kb_dir", nargs="?", default=DEFAULT_KB, help="知识库目录(默认 %(default)s)")
+    ap.add_argument("--out", help="输出 SQL 文件路径(默认 stdout)")
+    ap.add_argument("--summary", action="store_true", help="仅打印统计摘要")
+    args = ap.parse_args()
+
+    proto_dir = Path(args.kb_dir) / "protocols"
+    files = sorted(proto_dir.glob("*.json"))
+    if not files:
+        print(f"错误:{proto_dir} 下未找到协议 JSON", file=sys.stderr)
+        sys.exit(2)
+    protos = [json.loads(p.read_text(encoding="utf-8")) for p in files]
+    for p in protos:
+        if not re.match(r"^protocol-[a-z0-9-]+$", p.get("id", "")):
+            print(f"错误:协议 id 非法: {p.get('id')!r}", file=sys.stderr)
+            sys.exit(2)
+
+    sql, tpl, once = render_sql(protos)
+    if args.summary:
+        print(f"协议 {len(protos)} 个 | 模板 {tpl} 条 | 一次性 {once} 条 | 合计 {tpl + once} 条")
+        return
+    if args.out:
+        Path(args.out).write_text(sql + "\n", encoding="utf-8")
+        print(f"已写入 {args.out}(模板 {tpl},一次性 {once},合计 {tpl + once})")
+    else:
+        print(sql)
+
+
+if __name__ == "__main__":
+    main()

+ 13 - 9
docs/scripts/render_kb_md.py

@@ -103,22 +103,26 @@ def render_protocols(docs, supplement_names):
             dm = member.get("daily_monitoring", [])
             if dm:
                 out.append("\n#### 每日监测\n")
-                out.append(md_table(["时间", "内容", "类型"],
-                                    [[d["time"], d["name"], d.get("kind", "")] for d in dm]))
+                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["name"], "、".join(w.get("indicator_ids", []))] for w in wm]))
+                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["week"], f["name"], "、".join(f.get("indicator_ids", [])), f.get("note", "")] for f in fu]))
-            ex = member.get("exercises", [])
+                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("、".join(ex) + "\n")
+                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)
 
 

+ 10 - 3
docs/scripts/validate_kb.py

@@ -136,7 +136,7 @@ def validate_protocol(path, doc, errors, refs):
     for i, member in enumerate(root.get("members", [])):
         where = f"{path.name} 成员[{i}]"
         for req in ("family_id", "name", "user_id", "family_member_id", "phone", "phases",
-                    "daily_monitoring", "weekly_monitoring", "followups", "exercises"):
+                    "daily_monitoring", "weekly_monitoring", "followups", "exercise_tasks"):
             if req not in member:
                 errors.append(f"{where} 缺必需字段 {req}")
         for phase in member.get("phases", []):
@@ -162,9 +162,16 @@ def validate_protocol(path, doc, errors, refs):
             for iid in fu.get("indicator_ids", []):
                 if iid not in refs["indicators"]:
                     errors.append(f"{where}.followups 引用不存在的指标: {iid}")
-        for eid in member.get("exercises", []):
+        for eid in member.get("exercise_ids", []):
             if eid not in refs["exercises"]:
-                errors.append(f"{where}.exercises 引用不存在的运动: {eid}")
+                errors.append(f"{where}.exercise_ids 引用不存在的运动: {eid}")
+        for et in member.get("exercise_tasks", []):
+            if not isinstance(et, dict) or "title" not in et or "exercise_ids" not in et:
+                errors.append(f"{where}.exercise_tasks 缺 title/exercise_ids")
+                continue
+            for eid in et.get("exercise_ids", []):
+                if eid not in refs["exercises"]:
+                    errors.append(f"{where}.exercise_tasks 引用不存在的运动: {eid}")
 
 
 def validate_interaction_refs(path, items, errors, refs):

+ 58 - 25
docs/参考资料/知识库/protocols/gut_joint_intervention_v1.json

@@ -1,9 +1,32 @@
 {
-  "version": "1.0",
+  "version": "1.1",
   "updated": "2026-10-13",
   "id": "protocol-gut-joint-intervention-v1",
   "name": "双人肠道菌群联合干预方案",
   "source_doc": "docs/参考资料/方案/双人肠道菌群联合干预方案.md (1).docx",
+  "generation": {
+    "source_type": "health_plan",
+    "category": "health",
+    "executor_type": "parent",
+    "points": 1,
+    "title_rules": {
+      "supplement_slot": "【{cycle_name}】{time} {label}:{dose}",
+      "monitoring": "{name}",
+      "exercise": "{title}"
+    },
+    "period": "min(cycles.start)..max(cycles.end)",
+    "deadline_rules": {
+      "daily": "{cycle_end} {time}:00(监测/运动用整周期 {period_end})",
+      "weekly": "≤周期终点最近的 {weekday} {time}:00",
+      "onetime": "{due} 09:00:00"
+    }
+  },
+  "notifications": {
+    "channels": "wechat",
+    "timing_mode": "before_deadline",
+    "offset_minutes": 5,
+    "enabled": 0
+  },
   "cycles": [
     { "id": "cycle-2", "name": "第二阶段", "start": "2026-10-13", "end": "2026-11-09" },
     { "id": "cycle-3", "name": "第三阶段", "start": "2026-11-10", "end": "2026-12-07" }
@@ -38,23 +61,28 @@
         }
       ],
       "daily_monitoring": [
-        { "time": "19:00", "name": "每日记录:排便情况(次数/性状/颜色)", "kind": "self_report" },
-        { "time": "19:00", "name": "每日记录:腹部症状(腹痛/腹泻/胀气)", "kind": "self_report" },
-        { "time": "19:00", "name": "每日足部护理(温水洗脚+检查足部)", "kind": "physical" }
+        { "time": "19:00", "name": "每日记录:排便情况(次数/性状/颜色)", "description": "关注腹泻、便血、黏液", "kind": "self_report" },
+        { "time": "19:00", "name": "每日记录:腹部症状(腹痛/腹泻/胀气)", "description": "记录腹部不适程度", "kind": "self_report" },
+        { "time": "19:00", "name": "每日足部护理(温水洗脚+检查足部)", "description": "糖尿病足部护理,避免受伤", "kind": "physical" }
       ],
       "weekly_monitoring": [
-        { "weekday": "周一", "name": "每周评估:食欲与食量(与血糖关系)", "indicator_ids": [] },
-        { "weekday": "周一", "name": "每周测体重1次并记录", "indicator_ids": [] },
-        { "weekday": "周三", "name": "本周测血糖2-3次(空腹+餐后2h)", "indicator_ids": ["ind-fpg", "ind-pg2h"] }
+        { "weekday": "周一", "time": "09:00", "name": "每周评估:食欲与食量(与血糖关系)", "description": "每周记录一次食欲与食量变化", "indicator_ids": [] },
+        { "weekday": "周一", "time": "09:00", "name": "每周测体重1次并记录", "description": "关注体重管理", "indicator_ids": [] },
+        { "weekday": "周三", "time": "09:00", "name": "本周测血糖2-3次(空腹+餐后2h)", "description": "糖尿病监测;新增补充剂时加测", "indicator_ids": ["ind-fpg", "ind-pg2h"] }
       ],
-      "followups": [
-        { "week": "4周", "name": "复查血常规(血红蛋白,评估铁缺乏改善)", "indicator_ids": ["ind-hb"], "note": "4周后查血常规" },
-        { "week": "8-12周", "name": "复查血清铁蛋白(评估储存铁恢复)", "indicator_ids": ["ind-ferritin"], "note": "8-12周查血清铁蛋白" },
-        { "week": "12周", "name": "复查糖化血红蛋白 HbA1c", "indicator_ids": ["ind-hba1c"], "note": "每3个月查一次" },
-        { "week": "12周", "name": "复查血脂", "indicator_ids": ["ind-tc", "ind-tg", "ind-ldlc", "ind-hdlc"], "note": "12周查,评估心脑血管风险" },
-        { "week": "12周", "name": "复查肝肾功能", "indicator_ids": ["ind-alt", "ind-ast", "ind-bun", "ind-cre", "ind-egfr", "ind-ua"], "note": "12周查,评估补充剂安全性" }
+      "exercise_ids": ["ex-walk", "ex-toe-stand"],
+      "exercise_tasks": [
+        { "time": "07:00", "title": "早餐后散步15-20分钟(或足尖俯卧撑)", "description": "餐后散步有助于降餐后血糖", "exercise_ids": ["ex-walk", "ex-toe-stand"] },
+        { "time": "12:30", "title": "午餐后散步15-20分钟(或足尖俯卧撑)", "description": "餐后散步有助于降餐后血糖", "exercise_ids": ["ex-walk", "ex-toe-stand"] },
+        { "time": "17:30", "title": "晚餐后散步15-20分钟(或足尖俯卧撑)", "description": "餐后散步助消化、控血糖", "exercise_ids": ["ex-walk", "ex-toe-stand"] }
       ],
-      "exercises": ["ex-walk", "ex-toe-stand"]
+      "followups": [
+        { "due": "2026-10-20", "week": "4周", "name": "复查血常规(血红蛋白,评估铁缺乏改善)", "indicator_ids": ["ind-hb"], "note": "4周后查血常规" },
+        { "due": "2026-11-16", "week": "8-12周", "name": "复查血清铁蛋白(评估储存铁恢复)", "indicator_ids": ["ind-ferritin"], "note": "8-12周查血清铁蛋白" },
+        { "due": "2026-12-03", "week": "12周", "name": "复查糖化血红蛋白 HbA1c", "indicator_ids": ["ind-hba1c"], "note": "每3个月查一次" },
+        { "due": "2026-12-07", "week": "12周", "name": "复查血脂", "indicator_ids": ["ind-tc", "ind-tg", "ind-ldlc", "ind-hdlc"], "note": "12周查,评估心脑血管风险" },
+        { "due": "2026-12-07", "week": "12周", "name": "复查肝肾功能", "indicator_ids": ["ind-alt", "ind-ast", "ind-bun", "ind-cre", "ind-egfr", "ind-ua"], "note": "12周查,评估补充剂安全性" }
+      ]
     },
     {
       "family_id": 90,
@@ -85,20 +113,25 @@
         }
       ],
       "daily_monitoring": [
-        { "time": "19:00", "name": "每日记录:排便情况(次数/性状/颜色)", "kind": "self_report" },
-        { "time": "19:00", "name": "每日记录:腹部症状(腹胀/腹痛/胀气)", "kind": "self_report" }
+        { "time": "19:00", "name": "每日记录:排便情况(次数/性状/颜色)", "description": "关注便秘/腹泻变化", "kind": "self_report" },
+        { "time": "19:00", "name": "每日记录:腹部症状(腹胀/腹痛/胀气)", "description": "记录胀气程度", "kind": "self_report" }
       ],
       "weekly_monitoring": [
-        { "weekday": "周一", "name": "每周评估:食欲与食量", "indicator_ids": [] },
-        { "weekday": "周一", "name": "每周测体重1次并记录", "indicator_ids": [] }
+        { "weekday": "周一", "time": "09:00", "name": "每周评估:食欲与食量", "description": "每周记录一次食欲改善情况", "indicator_ids": [] },
+        { "weekday": "周一", "time": "09:00", "name": "每周测体重1次并记录", "description": "关注体重变化", "indicator_ids": [] }
       ],
-      "followups": [
-        { "week": "4周", "name": "复查血常规(血红蛋白,评估铁缺乏改善)", "indicator_ids": ["ind-hb"], "note": "4周后查血常规" },
-        { "week": "8-12周", "name": "复查血清铁蛋白(评估储存铁恢复)", "indicator_ids": ["ind-ferritin"], "note": "8-12周查血清铁蛋白" },
-        { "week": "12周", "name": "复查血脂", "indicator_ids": ["ind-tc", "ind-tg", "ind-ldlc", "ind-hdlc"], "note": "12周查" },
-        { "week": "12周", "name": "复查肝肾功能", "indicator_ids": ["ind-alt", "ind-ast", "ind-bun", "ind-cre", "ind-egfr", "ind-ua"], "note": "12周查,补充剂安全性评估" }
+      "exercise_ids": ["ex-walk", "ex-toe-stand"],
+      "exercise_tasks": [
+        { "time": "07:00", "title": "早餐后散步15-20分钟(或足尖俯卧撑)", "description": "适度运动,配合八段锦/太极更佳", "exercise_ids": ["ex-walk", "ex-toe-stand"] },
+        { "time": "12:30", "title": "午餐后散步15-20分钟(或足尖俯卧撑)", "description": "适度运动", "exercise_ids": ["ex-walk", "ex-toe-stand"] },
+        { "time": "17:30", "title": "晚餐后散步15-20分钟(或足尖俯卧撑)", "description": "适度运动", "exercise_ids": ["ex-walk", "ex-toe-stand"] }
       ],
-      "exercises": ["ex-walk", "ex-toe-stand"]
+      "followups": [
+        { "due": "2026-10-20", "week": "4周", "name": "复查血常规(血红蛋白,评估铁缺乏改善)", "indicator_ids": ["ind-hb"], "note": "4周后查血常规" },
+        { "due": "2026-11-16", "week": "8-12周", "name": "复查血清铁蛋白(评估储存铁恢复)", "indicator_ids": ["ind-ferritin"], "note": "8-12周查血清铁蛋白" },
+        { "due": "2026-12-07", "week": "12周", "name": "复查血脂", "indicator_ids": ["ind-tc", "ind-tg", "ind-ldlc", "ind-hdlc"], "note": "12周查" },
+        { "due": "2026-12-07", "week": "12周", "name": "复查肝肾功能", "indicator_ids": ["ind-alt", "ind-ast", "ind-bun", "ind-cre", "ind-egfr", "ind-ua"], "note": "12周查,补充剂安全性评估" }
+      ]
     }
   ]
-}
+}

+ 47 - 8
docs/参考资料/知识库/schema/protocol.schema.json

@@ -2,7 +2,7 @@
   "$schema": "http://json-schema.org/draft-07/schema#",
   "$id": "protocol.schema.json",
   "title": "干预方案库 Schema",
-  "description": "分阶段干预方案:周期(阶段)+ 成员 + 时段补剂 + 全程监测 + 运动 + 复查节点。",
+  "description": "分阶段干预方案:周期(阶段)+ 成员 + 时段补剂 + 全程监测 + 运动 + 复查节点。v1.1 起为「可生成规格」:监测含描述、每周监测含执行时间、运动为任务清单、复查含具体日期,可由 gen_tasks_from_protocol.py 忠实生成任务。",
   "type": "object",
   "required": ["version", "updated", "id", "name", "source_doc", "cycles", "members"],
   "properties": {
@@ -11,6 +11,22 @@
     "id": { "type": "string", "pattern": "^protocol-[a-z0-9-]+$" },
     "name": { "type": "string" },
     "source_doc": { "type": "string" },
+    "generation": {
+      "type": "object",
+      "description": "可选的生成约定:任务来源/分类/执行者/标题与截止时间渲染规则。",
+      "additionalProperties": true
+    },
+    "notifications": {
+      "type": "object",
+      "description": "可选的提醒配置:为每个成员生成一条 task_reminder_config。",
+      "properties": {
+        "channels": { "type": "string" },
+        "timing_mode": { "enum": ["before_deadline", "after_timeout"] },
+        "offset_minutes": { "type": "integer", "minimum": 1, "maximum": 1440 },
+        "enabled": { "type": "integer", "enum": [0, 1] }
+      },
+      "additionalProperties": false
+    },
     "cycles": {
       "type": "array",
       "minItems": 1,
@@ -31,7 +47,7 @@
       "minItems": 1,
       "items": {
         "type": "object",
-        "required": ["family_id", "name", "user_id", "family_member_id", "phone", "phases", "daily_monitoring", "weekly_monitoring", "followups", "exercises"],
+        "required": ["family_id", "name", "user_id", "family_member_id", "phone", "phases", "daily_monitoring", "weekly_monitoring", "followups", "exercise_tasks"],
         "properties": {
           "family_id": { "type": "integer" },
           "name": { "type": "string" },
@@ -70,10 +86,11 @@
             "type": "array",
             "items": {
               "type": "object",
-              "required": ["time", "name"],
+              "required": ["time", "name", "description"],
               "properties": {
                 "time": { "type": "string", "pattern": "^\\d{2}:\\d{2}$" },
                 "name": { "type": "string" },
+                "description": { "type": "string" },
                 "kind": { "enum": ["self_report", "physical"] }
               },
               "additionalProperties": false
@@ -83,21 +100,44 @@
             "type": "array",
             "items": {
               "type": "object",
-              "required": ["weekday", "name"],
+              "required": ["weekday", "time", "name", "description"],
               "properties": {
                 "weekday": { "type": "string", "examples": ["周一", "周三"] },
+                "time": { "type": "string", "pattern": "^\\d{2}:\\d{2}$" },
                 "name": { "type": "string" },
+                "description": { "type": "string" },
                 "indicator_ids": { "type": "array", "items": { "type": "string" } }
               },
               "additionalProperties": false
             }
           },
+          "exercise_ids": {
+            "type": "array",
+            "description": "参考引用的运动库 id(与 exercise_tasks 交叉校验)",
+            "items": { "type": "string" }
+          },
+          "exercise_tasks": {
+            "type": "array",
+            "description": "运动任务清单(每日重复,全程)",
+            "items": {
+              "type": "object",
+              "required": ["time", "title", "description", "exercise_ids"],
+              "properties": {
+                "time": { "type": "string", "pattern": "^\\d{2}:\\d{2}$" },
+                "title": { "type": "string" },
+                "description": { "type": "string" },
+                "exercise_ids": { "type": "array", "items": { "type": "string" } }
+              },
+              "additionalProperties": false
+            }
+          },
           "followups": {
             "type": "array",
             "items": {
               "type": "object",
-              "required": ["week", "name"],
+              "required": ["due", "name"],
               "properties": {
+                "due": { "type": "string", "format": "date" },
                 "week": { "type": "string", "examples": ["4周", "8-12周", "12周"] },
                 "name": { "type": "string" },
                 "indicator_ids": { "type": "array", "items": { "type": "string" } },
@@ -105,12 +145,11 @@
               },
               "additionalProperties": false
             }
-          },
-          "exercises": { "type": "array", "items": { "type": "string" } }
+          }
         },
         "additionalProperties": false
       }
     }
   },
   "additionalProperties": false
-}
+}

+ 43 - 37
docs/参考资料/知识库/views/protocols.md

@@ -37,34 +37,37 @@
 
 #### 每日监测
 
-| 时间 | 内容 | 类型 |
-| --- | --- | --- |
-| 19:00 | 每日记录:排便情况(次数/性状/颜色) | self_report |
-| 19:00 | 每日记录:腹部症状(腹痛/腹泻/胀气) | self_report |
-| 19:00 | 每日足部护理(温水洗脚+检查足部) | physical |
+| 时间 | 内容 | 描述 | 类型 |
+| --- | --- | --- | --- |
+| 19:00 | 每日记录:排便情况(次数/性状/颜色) | 关注腹泻、便血、黏液 | self_report |
+| 19:00 | 每日记录:腹部症状(腹痛/腹泻/胀气) | 记录腹部不适程度 | self_report |
+| 19:00 | 每日足部护理(温水洗脚+检查足部) | 糖尿病足部护理,避免受伤 | physical |
 
 #### 每周监测
 
-| 周期 | 内容 | 关联指标 |
-| --- | --- | --- |
-| 周一 | 每周评估:食欲与食量(与血糖关系) |  |
-| 周一 | 每周测体重1次并记录 |  |
-| 周三 | 本周测血糖2-3次(空腹+餐后2h) | ind-fpg、ind-pg2h |
+| 周期 | 时间 | 内容 | 描述 | 关联指标 |
+| --- | --- | --- | --- | --- |
+| 周一 | 09:00 | 每周评估:食欲与食量(与血糖关系) | 每周记录一次食欲与食量变化 |  |
+| 周一 | 09:00 | 每周测体重1次并记录 | 关注体重管理 |  |
+| 周三 | 09:00 | 本周测血糖2-3次(空腹+餐后2h) | 糖尿病监测;新增补充剂时加测 | ind-fpg、ind-pg2h |
 
 #### 复查节点
 
-| 周期 | 项目 | 关联指标 | 说明 |
-| --- | --- | --- | --- |
-| 4周 | 复查血常规(血红蛋白,评估铁缺乏改善) | ind-hb | 4周后查血常规 |
-| 8-12周 | 复查血清铁蛋白(评估储存铁恢复) | ind-ferritin | 8-12周查血清铁蛋白 |
-| 12周 | 复查糖化血红蛋白 HbA1c | ind-hba1c | 每3个月查一次 |
-| 12周 | 复查血脂 | ind-tc、ind-tg、ind-ldlc、ind-hdlc | 12周查,评估心脑血管风险 |
-| 12周 | 复查肝肾功能 | ind-alt、ind-ast、ind-bun、ind-cre、ind-egfr、ind-ua | 12周查,评估补充剂安全性 |
-
-#### 运动
+| 周期 | 日期 | 项目 | 关联指标 | 说明 |
+| --- | --- | --- | --- | --- |
+| 4周 | 2026-10-20 | 复查血常规(血红蛋白,评估铁缺乏改善) | ind-hb | 4周后查血常规 |
+| 8-12周 | 2026-11-16 | 复查血清铁蛋白(评估储存铁恢复) | ind-ferritin | 8-12周查血清铁蛋白 |
+| 12周 | 2026-12-03 | 复查糖化血红蛋白 HbA1c | ind-hba1c | 每3个月查一次 |
+| 12周 | 2026-12-07 | 复查血脂 | ind-tc、ind-tg、ind-ldlc、ind-hdlc | 12周查,评估心脑血管风险 |
+| 12周 | 2026-12-07 | 复查肝肾功能 | ind-alt、ind-ast、ind-bun、ind-cre、ind-egfr、ind-ua | 12周查,评估补充剂安全性 |
 
-ex-walk、ex-toe-stand
+#### 运动任务
 
+| 时间 | 任务 | 描述 | 引用运动 |
+| --- | --- | --- | --- |
+| 07:00 | 早餐后散步15-20分钟(或足尖俯卧撑) | 餐后散步有助于降餐后血糖 | ex-walk、ex-toe-stand |
+| 12:30 | 午餐后散步15-20分钟(或足尖俯卧撑) | 餐后散步有助于降餐后血糖 | ex-walk、ex-toe-stand |
+| 17:30 | 晚餐后散步15-20分钟(或足尖俯卧撑) | 餐后散步助消化、控血糖 | ex-walk、ex-toe-stand |
 
 ### 成员:闫德波(family 90,user 79,手机 13841386977)
 
@@ -91,28 +94,31 @@ ex-walk、ex-toe-stand
 
 #### 每日监测
 
-| 时间 | 内容 | 类型 |
-| --- | --- | --- |
-| 19:00 | 每日记录:排便情况(次数/性状/颜色) | self_report |
-| 19:00 | 每日记录:腹部症状(腹胀/腹痛/胀气) | self_report |
+| 时间 | 内容 | 描述 | 类型 |
+| --- | --- | --- | --- |
+| 19:00 | 每日记录:排便情况(次数/性状/颜色) | 关注便秘/腹泻变化 | self_report |
+| 19:00 | 每日记录:腹部症状(腹胀/腹痛/胀气) | 记录胀气程度 | self_report |
 
 #### 每周监测
 
-| 周期 | 内容 | 关联指标 |
-| --- | --- | --- |
-| 周一 | 每周评估:食欲与食量 |  |
-| 周一 | 每周测体重1次并记录 |  |
+| 周期 | 时间 | 内容 | 描述 | 关联指标 |
+| --- | --- | --- | --- | --- |
+| 周一 | 09:00 | 每周评估:食欲与食量 | 每周记录一次食欲改善情况 |  |
+| 周一 | 09:00 | 每周测体重1次并记录 | 关注体重变化 |  |
 
 #### 复查节点
 
-| 周期 | 项目 | 关联指标 | 说明 |
-| --- | --- | --- | --- |
-| 4周 | 复查血常规(血红蛋白,评估铁缺乏改善) | ind-hb | 4周后查血常规 |
-| 8-12周 | 复查血清铁蛋白(评估储存铁恢复) | ind-ferritin | 8-12周查血清铁蛋白 |
-| 12周 | 复查血脂 | ind-tc、ind-tg、ind-ldlc、ind-hdlc | 12周查 |
-| 12周 | 复查肝肾功能 | ind-alt、ind-ast、ind-bun、ind-cre、ind-egfr、ind-ua | 12周查,补充剂安全性评估 |
-
-#### 运动
+| 周期 | 日期 | 项目 | 关联指标 | 说明 |
+| --- | --- | --- | --- | --- |
+| 4周 | 2026-10-20 | 复查血常规(血红蛋白,评估铁缺乏改善) | ind-hb | 4周后查血常规 |
+| 8-12周 | 2026-11-16 | 复查血清铁蛋白(评估储存铁恢复) | ind-ferritin | 8-12周查血清铁蛋白 |
+| 12周 | 2026-12-07 | 复查血脂 | ind-tc、ind-tg、ind-ldlc、ind-hdlc | 12周查 |
+| 12周 | 2026-12-07 | 复查肝肾功能 | ind-alt、ind-ast、ind-bun、ind-cre、ind-egfr、ind-ua | 12周查,补充剂安全性评估 |
 
-ex-walk、ex-toe-stand
+#### 运动任务
 
+| 时间 | 任务 | 描述 | 引用运动 |
+| --- | --- | --- | --- |
+| 07:00 | 早餐后散步15-20分钟(或足尖俯卧撑) | 适度运动,配合八段锦/太极更佳 | ex-walk、ex-toe-stand |
+| 12:30 | 午餐后散步15-20分钟(或足尖俯卧撑) | 适度运动 | ex-walk、ex-toe-stand |
+| 17:30 | 晚餐后散步15-20分钟(或足尖俯卧撑) | 适度运动 | ex-walk、ex-toe-stand |