Parcourir la source

docs: 生成方案标准内容格式设计(planJson tasks 单一事实源 + Pydantic 校验 + 懒加载回填 + 规划师任务编辑UI)

iwt il y a 3 semaines
Parent
commit
9568dc7f0d

+ 1 - 0
docs/superpowers/PROJECT-OVERVIEW.md

@@ -359,6 +359,7 @@
 | `2026-08-13-health-status-survey-design.md` | 🔄 v2 设计已定稿(2026-08-18;v1 已实施;v2 疾病史两源预置清单+确诊时间+服药时长) | 当前状态调研 — 健康现状档案 |
 | `2026-08-13-health-status-survey-design.md` | 🔄 v2 设计已定稿(2026-08-18;v1 已实施;v2 疾病史两源预置清单+确诊时间+服药时长) | 当前状态调研 — 健康现状档案 |
 | `2026-08-18-health-status-survey-v2.md` | 🟢 已实施(单文件改 health-status-form.vue;8 组 54 项预置清单分类多选+确诊时间;用药年月+时长自动计算;后端零变更;v1 旧数据兼容) | 当前状态调研 — 健康现状档案 v2 |
 | `2026-08-18-health-status-survey-v2.md` | 🟢 已实施(单文件改 health-status-form.vue;8 组 54 项预置清单分类多选+确诊时间;用药年月+时长自动计算;后端零变更;v1 旧数据兼容) | 当前状态调研 — 健康现状档案 v2 |
 | `2026-08-23-coach-butler-design.md` | ✅ 已实施 | AI健康教练人格分化(浠宝/福宝话术路由)与 L2 家庭管家自助选择设计 |
 | `2026-08-23-coach-butler-design.md` | ✅ 已实施 | AI健康教练人格分化(浠宝/福宝话术路由)与 L2 家庭管家自助选择设计 |
+| `2026-08-28-plan-standard-content-design.md` | 🟡 设计已确认 | 生成方案标准内容格式(plan_json.sections[].tasks 唯一事实源 + LangGraph prompt tasks 输出 + Pydantic 校验 + 正则兜底 + 存量懒加载回填 + 规划师端结构化任务编辑 UI) |
 | `api/API_REFERENCE.md` | 🟢 已建立(2026-08-18;200+ 接口清单;废弃接口标注;新增接口检查流程) | 后台接口参考文档 |
 | `api/API_REFERENCE.md` | 🟢 已建立(2026-08-18;200+ 接口清单;废弃接口标注;新增接口检查流程) | 后台接口参考文档 |
 
 
 ### 实施计划(plans/)
 ### 实施计划(plans/)

+ 172 - 0
docs/superpowers/specs/2026-08-28-plan-standard-content-design.md

@@ -0,0 +1,172 @@
+# 生成方案标准内容格式设计
+
+**日期:** 2026-08-28
+**状态:** 待审查
+**优先级:** P1
+
+## 一、背景与问题
+
+现有健康方案生成链路存在三处脱节:
+
+1. **AI 返回侧**:`cfc-langgraph/app/api/adapter.py` 的 `PLAN_SYSTEM_PROMPT` 已要求 LLM 输出结构化 JSON(`overview` + `sections[].content/items` + `abnormal_indicators`),但 JSON 解析靠 `find("{")`/`rfind("}")` 手动截取,无 Pydantic 校验,失败时静默降级为 raw 文本——前端需同时兼容两种形态。
+
+2. **呈现/编辑侧**:`cfc-frontend/pages/health/health-plan-summary.vue` 已支持分段确认 + 反馈式重生成,但 `submitPlan` 组装 `planJson` 时**漏写 `sections[].tasks`**(当前 schema 根本没有该字段),导致保存时丢失任务意图。规划师端 `HealthPlanReview.vue` 仅提供 `planContent` 纯文本编辑,无结构化任务条目 UI。
+
+3. **任务转化侧**:`HealthPlanServiceImpl.generateDailyTasksFromPlan` 用正则(`TASK_PATTERN` + 动作词分类)从 `plan_content` 纯文本猜测任务行,可靠性建立在"LLM 输出恰好符合正则预期"上。结构化的 `plan_json` 在此链路完全未被消费。前后端各维护一份重复正则(`TASK_PATTERN` 与前端 `parseTasksFromPlan`)。
+
+**目标:** 让 `plan_json.sections[].tasks` 成为任务生成的唯一事实源,AI 有标准格式返回、用户看到标准格式可编辑、结果确定性地转化为任务。
+
+## 二、方案选型(用户已确认)
+
+采用**方案 A**:
+
+- LangGraph prompt 输出 `tasks` 数组 + Pydantic 校验
+- 校验失败时用现有正则从 content 兜底提取,仍写入 `tasks`
+- 任务生成优先读 `planJson.sections[].tasks`,缺失才回落正则
+- 存量方案用**懒加载回填**统一(每次生成任务时若发现无 tasks 则即时用正则回填并落库)
+
+不做方案 B(完全移除正则,容错低)或方案 C(仅前端展示,未解决可靠性)。
+
+## 三、数据契约
+
+在现有 `sections[].items` 基础上,为每个 section 增加 `tasks` 数组(机器可读任务意图),与 `content`(人类可读 Markdown)分离:
+
+```json
+{
+  "overview": "总体概述",
+  "sections": [
+    {
+      "key": "nutrition",
+      "title": "营养补充建议",
+      "content": "## 建议 1...(Markdown 展示文本)",
+      "items": [{"name","dosage","timing","reason"}],
+      "tasks": [
+        {
+          "action_type": "buy",
+          "title": "购买维生素D3补充剂",
+          "dimension": "wealth",
+          "frequency": "once",
+          "notes": "每日一粒,随餐服用"
+        }
+      ]
+    }
+  ],
+  "abnormal_indicators": []
+}
+```
+
+### 字段约定
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| `action_type` | string 枚举 | `buy`/`read`/`exercise`/`checkin`/`diet`/`activity`,决定任务类别与后处理 |
+| `title` | string | 任务标题(最终写入 Task.title) |
+| `dimension` | string 枚举 | 五维维度 `body`/`mind`/`wisdom`/`action`/`wealth` |
+| `frequency` | string | `once`=一次性 / `daily`=每日重复 |
+| `notes` | string? | 补充说明(可选) |
+
+### action_type 与现有分类映射
+
+| action_type | 现有 TaskDraft 分类 | 维度 | 特殊后处理 |
+|---|---|---|---|
+| `buy` | 购买任务 | wealth | 派生"每日使用/服用"子任务(parentTaskId) |
+| `read` | 阅读任务 | wisdom | — |
+| `exercise` | 运动任务 | body | — |
+| `checkin` | 打卡任务 | mind | — |
+| `diet` | 饮食任务 | body | — |
+| `activity` | 活动任务 | action | — |
+
+## 四、LangGraph 层改造
+
+**文件:** `cfc-langgraph/app/api/adapter.py`、`cfc-langgraph/app/schemas.py`
+
+1. **`PLAN_SYSTEM_PROMPT` 追加 tasks 输出契约**:在每个 section 内强制输出 `tasks` 数组,明确枚举取值与 frequency 语义。
+
+2. **新增 Pydantic 模型**(`app/schemas.py`):
+
+```python
+from typing import List, Optional, Literal
+from pydantic import BaseModel
+
+class PlanTask(BaseModel):
+    action_type: Literal["buy","read","exercise","checkin","diet","activity"]
+    title: str
+    dimension: Literal["body","mind","wisdom","action","wealth"]
+    frequency: Literal["once","daily"] = "daily"
+    notes: Optional[str] = None
+
+class PlanSection(BaseModel):
+    key: str
+    title: str = ""
+    content: str = ""
+    items: List[dict] = []
+    tasks: List[PlanTask] = []
+
+class PlanResponse(BaseModel):
+    overview: str = ""
+    sections: List[PlanSection] = []
+    abnormal_indicators: List[dict] = []
+```
+
+3. **生成逻辑**:LLM 调用用结构化输出(`response_format={"type":"json_object"}`),随后 `PlanResponse.model_validate_json()` 校验;**校验失败 → 触发一次正则兜底重新提取 + 二次校验**,仍失败则降级(raw 文本 + 前端兼容)。
+
+4. **`regenerate-section`**:反馈重生成改为返回 `{content, tasks}`,前端同步更新,不再只回传纯字符串。
+
+## 五、Java 层改造
+
+**文件:** `HealthPlanServiceImpl.java`、`DatabaseInitializer.java`、`schema.sql`
+
+### 5.1 任务生成优先读 tasks
+
+`generateDailyTasksFromPlan` 新逻辑:
+1. **优先**:遍历 `plan_json.sections[].tasks[]`,按 `action_type` 直接映射现有 `TaskDraft` 分类。
+2. **兜底**:该 section 无 `tasks` 数组(老数据/LLM 漏输出)→ 回落现有 `TASK_PATTERN` 正则从 `content` 提取。
+3. `TaskDraft` 的 dimension/frequency 直接取自 `tasks[].dimension`/`tasks[].frequency`,不再依赖正则猜维度。
+
+### 5.2 存量方案懒加载回填(用户确认)
+
+在 `generateDailyTasksFromPlan` 开头:
+- 若 `plan_json` 中该 section 无 `tasks` 数组 → 用现有正则(`TASK_PATTERN` + 动作分类)从 `content` 提取任务,写入 `plan_json.sections[].tasks`,**落库**(`updateById`)。
+- 幂等:已回填的 section 跳过;重复调用不产生重复任务。
+
+无需预跑全量脚本,零运维成本;存量方案在首次发布/确认时自动统一。
+
+## 六、前端改造
+
+**文件:** `cfc-frontend/pages/health/health-plan-summary.vue`、`cfc-web/src/views/teacher/HealthPlanReview.vue`
+
+### 6.1 家长端(只读展示 + 反馈式重生成)
+
+- **修复 bug**:`submitPlan` 组装 `planJson` 时补上 `sections[].tasks`(当前只写 overview+sections,漏 tasks)。
+- 展示无 tasks 时降级 review 模式(兼容)。
+- 反馈式重生成保留;regenerate 返回 tasks 时前端同步更新。
+
+### 6.2 规划师端(结构化任务条目编辑)
+
+- `HealthPlanReview.vue` 当前只有 `planContent` 纯文本编辑,无 tasks UI → **新增任务条目级增删改**(action_type/title/dimension/frequency/notes)。
+- 任务编辑直接改 `plan_json.sections[].tasks`,保存走现有 `pending-review/update` 接口(已是透传 planJson)。
+
+## 七、测试与验证
+
+### 单元测试
+- **LangGraph**:`PlanResponse` 校验(合法/非法 action_type、frequency 越界)、正则兜底回填。
+- **Java**:`generateDailyTasksFromPlan` 覆盖"有 tasks(优先走 tasks)"与"无 tasks(回落正则)"两分支;`TaskDraft` 映射正确性(buy→wealth+派生使用子任务等);懒加载回填幂等性。
+
+### 验证命令
+| 模块 | 命令 |
+|---|---|
+| 后端 | `cd cfc-backend && mvn clean compile` + 针对性单测 |
+| LangGraph | `cd cfc-langgraph && pytest tests/ -v` |
+| cfc-web | `cd cfc-web && npm run build` |
+
+### 手工验收
+1. 新方案 AI 生成含 tasks → 发布 → 任务正确生成且维度匹配。
+2. 存量无 tasks 老 planJson → 首次发布/确认自动回填 → 任务正常。
+3. 规划师 cfc-web 编辑任务条目 → 保存 → 重新发布反映改动。
+4. LLM 漏输出 tasks → 正则兜底仍能生成任务。
+
+## 八、边界与注意事项
+
+- 正则 `TASK_PATTERN` 短期保留作兜底,不退役;等所有活跃方案走新格式后再评估退役。
+- 发现 `getPlanList` 中残留 `SortUtil.applySort` 调用(之前 revert 掉的分页+排序残留)——**不在本设计范围内**,仅提示后续清理。
+- 前端小程序需在 HBuilderX 重新打包验证(禁止 Agent 自行 `npm run build:mp-weixin`)。