f96fdcb3b9
Lock health recording behind confirm-only state machines, reject public publishes for the health category, and ship the 14-day experiment Page Data templates with tests. Co-authored-by: Cursor <cursoragent@cursor.com>
1207 lines
51 KiB
Markdown
1207 lines
51 KiB
Markdown
# MeMind Health 架构与通道设计
|
||
|
||
日期:2026-09-02
|
||
状态:架构设计稿。**未改动业务代码、未碰生产、未读写用户数据。**
|
||
适用范围:MeMind H5、微信服务号、MindSpace、Page Data、Memory V2、Agent Gateway
|
||
|
||
---
|
||
|
||
## 0. 一句话定位
|
||
|
||
MeMind Health 不是「另一个健康 App」,而是:
|
||
|
||
> **持续理解本人健康基线,并在长期变化中发现「偏离」的个人健康 Agent。**
|
||
|
||
核心数据链路:
|
||
|
||
```text
|
||
本人 → 录入/设备/报告 → Health Timeline → Personal Baseline
|
||
→ 偏离事件 → Agent 推理 → 行动建议 → 医疗资料沉淀
|
||
```
|
||
|
||
与传统健康产品的根本差异:
|
||
|
||
| 传统健康 App | MeMind Health |
|
||
|---|---|
|
||
| `140/90 → 标红` | 该用户 90 天基线 `119–126/70–77`,近 14 天持续漂移 +8% |
|
||
| 数据看板 | 「最近和以前不一样」的事件叙事 |
|
||
| 用户自己盯曲线 | 只在偏离时介入,无事静默 |
|
||
| 单次录入 | 时间序列 + 关联假设 + 长期学习 |
|
||
|
||
**MVP 验证命题**:
|
||
|
||
> MeMind 能不能比我自己更早发现:我最近和以前不太一样?
|
||
|
||
若成立,血压计只是传感器;**Personal Baseline + Health Timeline + Event Engine** 才是产品。
|
||
|
||
---
|
||
|
||
## 1. 已确认的产品决策(不可随意推翻)
|
||
|
||
以下决策已确认,后续实现必须遵守。变更需重新评审本文档。
|
||
|
||
### D1 单人模型:只监测账号本人
|
||
|
||
- 监测对象 **默认且仅为账号本人**,`HealthProfile` 与 `user_id` 严格 1:1。
|
||
- **不做** `FamilyGroup` / 多 `Subject` / 子女代录代管。
|
||
- 理由:录入标准、测量情境、回顾深度必须统一,否则 Personal Baseline 失真(「参差不齐」)。
|
||
- 未来若做「关注家人」,只能是 **本人主动授权只读分享摘要**(P4),录入权永远在本人。
|
||
|
||
### D2 他人健康数据走辅轨,不进引擎
|
||
|
||
用户说「我妈上周体检血压 150/95」这类内容:
|
||
|
||
- 写入 Memory V2(`fact` / `experience`)或普通备忘,Agent 可对话召回。
|
||
- **禁止**进入 `HealthObservation`、Baseline、Trend、Event Engine。
|
||
- Agent 话术须明确告知:「这不会进入你的健康分析」。
|
||
|
||
### D3 存储走 Page Data(P0 决策)
|
||
|
||
- 结构化健康数值存入 **用户隔离的 PostgreSQL schema**(Page Data 体系,`private_data_execute` 建表 + `private_data_register_dataset` 注册)。
|
||
- 复用现成的用户级 schema 隔离、dataset 授权、操作日志、导出、软删除恢复能力。
|
||
- P1 若基线/趋势聚合出现性能瓶颈,再评估迁移到专用表;迁移方案见 §12.3。
|
||
|
||
### D4 服务号允许规则命中 + 强制确认
|
||
|
||
- 自由文本先走**确定性规则**(正则/关键词);命中即进入对应流程。
|
||
- 规则未命中 → 回交互式菜单,**不让 LLM 硬猜**。
|
||
- 所有由识别/解析得到的数值,**一律经用户确认后才落库**。
|
||
|
||
### D5 健康会话完全独立
|
||
|
||
- 健康会话与普通聊天会话**完全隔离**(独立 session、独立工具白名单)。
|
||
- 普通聊天中识别到健康意图,**只做引导跳转**,给入口链接/按钮。
|
||
- **绝不在普通聊天里直接落库健康数据。**
|
||
|
||
---
|
||
|
||
## 2. 四条不可违背的工程原则
|
||
|
||
1. **健康通道是「锁定态会话」**,不是「聊天里聊健康」。进入后 skill 被 pin,意图分类器不再参与判断。
|
||
2. **拍照/上传不直接落库**,一律「抽取 → 确认 → 提交」三段式。未确认数据不进基线引擎。
|
||
3. **意图不明确时不猜,直接给选项。** 菜单式交互在健康场景比自然语言理解更精准、更快、更可审计。
|
||
4. **健康区数据默认加密,无口令不允许发布。** 服务端硬阻断,不靠前端自觉。
|
||
|
||
配套的分层职责:
|
||
|
||
```text
|
||
规则判断事实 → Rule Engine(确定性、可审计)
|
||
算法发现趋势 → Trend Engine(统计、可复现)
|
||
LLM 负责解释 → Agent(不可作为 primary detector)
|
||
```
|
||
|
||
**禁止**的反模式:`血压 128/85 → LLM → "你觉得危险吗?"`
|
||
|
||
---
|
||
|
||
## 3. 系统总览
|
||
|
||
```text
|
||
MeMind Health(本人)
|
||
│
|
||
┌───────────────────┼───────────────────┐
|
||
│ │ │
|
||
Health Connector Health Timeline Medical Documents
|
||
(P2: Apple/蓝牙) (结构化时序) (报告 OCR + 归档)
|
||
│ │ │
|
||
└───────────────────┼───────────────────┘
|
||
↓
|
||
Personal Baseline Engine
|
||
↓
|
||
┌───────────────────┴───────────────────┐
|
||
↓ ↓
|
||
Rule Engine Trend Engine
|
||
(医学阈值 / 漏测 / 药物变化) (个体偏移 / 变异性 / 联动)
|
||
│ │
|
||
└───────────────────┬───────────────────┘
|
||
↓
|
||
Event Engine
|
||
(Health Event Cluster)
|
||
↓
|
||
MeMind Agent(解释 + 追问 + 建议)
|
||
↓
|
||
提醒 / 复测 / 就医建议 / 就诊摘要 / Memory 沉淀
|
||
```
|
||
|
||
### 3.1 与现有 MeMind 能力的映射
|
||
|
||
| 设计层 | 复用的现有能力 | 关键文件 |
|
||
|---|---|---|
|
||
| 通道锁定不漂移 | `selectedChatSkill` 短路 + `enforceSelectedSkillRuntime` | `chat-intent-router.mjs`、`agent-run-routes.mjs` |
|
||
| 结构化存储 | Page Data 用户隔离 PG schema | `page-data-service.mjs`、`mindspace-userdata-postgres.mjs` |
|
||
| 健康区与发布策略 | `h5_space_categories`(`visibility/ai_access/publish_policy`) | `mindspace.mjs`(`SYSTEM_CATEGORIES`) |
|
||
| 加密发布与口令 | 发布 `access_mode` + `password_hash` | `mindspace-pages.mjs`、`page-data-public-service.mjs` |
|
||
| 私有页不被收录 | `INDEXABLE_ACCESS_MODE = 'public'`,非 public 一律 noindex | `mindspace-index-policy.mjs` |
|
||
| 报告原文件存储 | MindSpace 资产(`h5_assets`)+ 分区归属 | `mindspace.mjs` |
|
||
| 服务号图文/语音接入 | `msgType` 分流 + `eventKey` 菜单事件 | `wechat-mp.mjs` |
|
||
| 图片轮次隔离 | 带图轮次摘掉 `read_image`、污染触发会话轮换 | `chat-image-turn-scope.mjs` |
|
||
| 用药/复诊提醒 | `h5_schedule_items` + `schedule-assistant` | `schedule-service.mjs` |
|
||
| 长期关联假设 | Memory V2 candidate → promote | `memory-v2.mjs`、`memory-v2-lifecycle.mjs` |
|
||
| 灰度 / fail-open | 现有 canary + fail-open 模式 | `memory-v2-runtime.mjs` 等 |
|
||
|
||
### 3.2 需要新增的模块(隔离在健康域内)
|
||
|
||
```text
|
||
health-channel-state.mjs 通道状态机(纯函数,可单测)
|
||
health-observation-service.mjs Observation 读写 + 合理性校验 + 幂等去重
|
||
health-extraction.mjs 图片/报告结构化抽取 + confidence 归一
|
||
health-baseline-engine.mjs 滚动基线计算(P1)
|
||
health-event-engine.mjs Rule + Trend + Event 聚合(P1)
|
||
health-document-service.mjs 报告归档 + 指标映射
|
||
health-share-service.mjs 就诊摘要快照与令牌(P3)
|
||
skills/health-assistant/ skill 定义
|
||
src/pages/health/* H5 健康入口页面
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 入口设计:H5 主入口 + 服务号轻入口
|
||
|
||
两个入口共用同一个 Health Channel 内核,只是壳不同。
|
||
|
||
```text
|
||
┌──────────────────────────┐
|
||
H5 首页 ──────→│ 健康助手入口(卡片 / Tab) │
|
||
└────────────┬─────────────┘
|
||
│
|
||
服务号菜单 / 关键词 ─────────→│
|
||
「健康助手」 │
|
||
↓
|
||
┌────────────────────────────┐
|
||
│ Health Channel 内核 │
|
||
│ (skill pin + 状态机) │
|
||
└────────────┬───────────────┘
|
||
↓
|
||
┌────────────┬────────┴───────┬────────────┐
|
||
↓ ↓ ↓ ↓
|
||
健康录入 健康评估 报告归档 健康档案
|
||
(值/照片) (基线/趋势) (OCR→档案) (MindSpace 健康区)
|
||
```
|
||
|
||
### 4.1 H5 主入口(功能完整,主战场)
|
||
|
||
H5 是主入口,因为需要结构化表单、数值键盘、Timeline 图表、可编辑确认卡、档案浏览。
|
||
|
||
建议做成**独立页面**而非普通聊天里的一个 skill 按钮:
|
||
|
||
```text
|
||
/health 健康首页(今日录入状态 + 最近趋势 + 待确认项)
|
||
/health/chat 健康助手会话(锁定通道)
|
||
/health/records Health Timeline
|
||
/health/documents 报告档案
|
||
/health/profile 基线 / 既往史 / 长期用药
|
||
```
|
||
|
||
`/health/chat` 使用独立会话空间,不与普通聊天会话混用(见 §7)。
|
||
|
||
### 4.2 服务号轻入口(随手录入,主打拍照/语音)
|
||
|
||
服务号的价值是**随手**:量完血压直接拍照发送。
|
||
|
||
进入方式两条,均有现成钩子:
|
||
|
||
- **自定义菜单点击** → `msgType === 'event'` + `eventKey === 'MEMIND_HEALTH'`
|
||
- **关键词命中** → 文本命中「健康助手」「健康」「录血压」等
|
||
|
||
进入后回复固定菜单(服务号仅文本/图文,用数字选择):
|
||
|
||
```text
|
||
已进入【健康助手】专用通道
|
||
本通道只处理健康相关内容,数据自动归入你的健康档案。
|
||
|
||
请回复数字:
|
||
1 健康录入(血压/心率/体重/血氧/体温/睡眠/症状)
|
||
2 上传报告(直接发送照片即可)
|
||
3 健康评估(看看最近和平时比有什么变化)
|
||
4 查看健康档案
|
||
0 退出健康通道
|
||
```
|
||
|
||
关键约束:
|
||
|
||
- 进入后**所有消息(含图片)只走健康链路**,直到用户回复 `0` 或超时。
|
||
- 建议 **30 分钟无交互自动退出**,退出时必须明确告知,避免用户以为还在通道内。
|
||
- 通道状态与现有 canary/stable 会话钉选逻辑并存,**不改动现有分流**。
|
||
|
||
---
|
||
|
||
## 5. 防漂移三层保障
|
||
|
||
这是本设计的核心,对应用户最关心的「不允许漂移,要精准」。
|
||
|
||
### 5.1 第一层:Skill Pin(复用现有短路)
|
||
|
||
现有 `chat-intent-router.mjs` 已具备完整短路能力:
|
||
|
||
- `readSelectedChatSkillId(message)` 读取 `metadata.memindRun.selectedChatSkill`
|
||
- `buildSkillSelectionClassification()` 返回 `route: agent_orchestration`、`confidence: 1`、`reason: '用户已选择 skill'`
|
||
- 结果:**LLM 意图分类器根本不会被调用**
|
||
|
||
健康通道发消息时携带:
|
||
|
||
```json
|
||
{
|
||
"metadata": {
|
||
"memindRun": {
|
||
"selectedChatSkill": "health-assistant",
|
||
"healthChannel": {
|
||
"sessionKind": "health",
|
||
"step": "await_value",
|
||
"pendingDraftId": "drf_...",
|
||
"metricSet": "blood_pressure"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
服务号侧从会话状态读出「当前在健康通道」,注入相同 metadata。
|
||
|
||
`agent-run-routes.mjs` 的 `enforceSelectedSkillRuntime` 进一步固定 `toolMode` / `taskType` / `requiredExecutor`,保证工具集不越界。
|
||
|
||
**这一层解决「会不会被路由到别的 skill」。**
|
||
|
||
### 5.2 第二层:Channel 状态机(解决「这句话是什么意思」)
|
||
|
||
Pin 住 skill 不够 —— 用户说「137」,是血压还是体重?必须有显式状态机,不靠 LLM 猜。
|
||
|
||
```text
|
||
IDLE
|
||
│ 选择「健康录入」
|
||
↓
|
||
AWAIT_METRIC_TYPE ──→ 展示可录入项(血压/心率/体重/血氧/体温/睡眠/症状/用药)
|
||
│ 选择「血压」
|
||
↓
|
||
AWAIT_CONTEXT ──────→ 晨起 / 睡前 / 服药后 / 其他(可跳过取默认)
|
||
│
|
||
↓
|
||
AWAIT_VALUE ────────→ 期望数值;接受手输 / 拍照 / 语音
|
||
│
|
||
↓
|
||
CONFIRM ────────────→ 结构化确认卡(可编辑)
|
||
│ 确认
|
||
↓
|
||
COMMITTED ──────────→ 写入 Observation + 返回 Timeline 即时反馈
|
||
```
|
||
|
||
完整状态转移表:
|
||
|
||
| 当前状态 | 期望输入 | 事件 | 下一状态 |
|
||
|---|---|---|---|
|
||
| `IDLE` | 菜单选择 / 规则命中文本 / 图片 | `select_action` | 对应流程首状态 |
|
||
| `IDLE` | 无法高置信解析 | `ambiguous` | `AWAIT_ACTION_CHOICE` |
|
||
| `AWAIT_ACTION_CHOICE` | 1/2/3/4 | `choose` | 对应流程首状态 |
|
||
| `AWAIT_METRIC_TYPE` | 指标名 | `pick_metric` | `AWAIT_CONTEXT` |
|
||
| `AWAIT_CONTEXT` | 情境 / 跳过 | `pick_context` | `AWAIT_VALUE` |
|
||
| `AWAIT_VALUE` | 数值 / 图片 / 语音 | `submit_value` | `CONFIRM` |
|
||
| `AWAIT_VALUE` | 无关输入 | `unexpected` | `AWAIT_VALUE`(回提示,不猜) |
|
||
| `CONFIRM` | 确认 | `commit` | `COMMITTED` |
|
||
| `CONFIRM` | 修改 | `revise` | `AWAIT_VALUE` |
|
||
| `CONFIRM` | 放弃 | `discard` | `IDLE` |
|
||
| 任意 | `0` / 「退出」 | `exit` | 通道退出 |
|
||
| 任意 | 30 分钟无交互 | `timeout` | 通道退出(告知用户) |
|
||
|
||
状态机每一步都有**明确的期望输入类型**。用户在 `AWAIT_VALUE` 说「137/82」语义完全确定,不存在歧义。
|
||
|
||
若在某状态收到完全无关输入,通道**不猜测**,回:
|
||
|
||
```text
|
||
当前正在录入「血压」。
|
||
- 直接发送数值,如 137/82
|
||
- 或拍照上传血压计读数
|
||
- 回复「取消」放弃本次录入
|
||
- 回复「0」退出健康通道
|
||
```
|
||
|
||
状态机应实现为**纯函数**(`health-channel-state.mjs`),输入 `(state, event)` 输出 `(nextState, actions)`,便于完整单测覆盖。
|
||
|
||
### 5.3 第三层:意图不明时强制选择(D4)
|
||
|
||
`IDLE` 状态自由发言的判定顺序,**确定性优先**:
|
||
|
||
```text
|
||
1. 确定性规则匹配(正则 / 关键词)→ 命中即执行,不调 LLM
|
||
2. 规则未命中 → LLM 做候选动作分类,要求返回 top-2 + confidence
|
||
3. confidence < 0.75 或 top-2 差距 < 0.15 → 不执行,返回选择卡
|
||
4. 用户选择后 → 进入状态机,此后无歧义
|
||
```
|
||
|
||
选择卡内容:
|
||
|
||
```text
|
||
我不太确定你想做什么,请选择:
|
||
1 记录健康数据
|
||
2 查看健康评估
|
||
3 上传 / 归档报告
|
||
4 查看健康档案
|
||
```
|
||
|
||
规则库设计(示例,需持续维护并单测):
|
||
|
||
| 意图 | 规则示例 | 落地动作 |
|
||
|---|---|---|
|
||
| 血压录入 | `(\d{2,3})\s*[//]\s*(\d{2,3})`,或含「血压」+ 两个数 | `metric_set=blood_pressure` → `CONFIRM` |
|
||
| 体重录入 | 含「体重」+ `(\d{2,3}(\.\d)?)` | `metric_set=weight` |
|
||
| 心率录入 | 含「心率/脉搏」+ `(\d{2,3})` | `metric_set=heart_rate` |
|
||
| 血氧 | 含「血氧/SpO2」+ `(\d{2,3})` | `metric_set=spo2` |
|
||
| 体温 | 含「体温/发烧」+ `(3\d(\.\d)?)` | `metric_set=temperature` |
|
||
| 睡眠 | 含「睡了」+ 时长表达 | `metric_set=sleep` |
|
||
| 症状 | 含「头晕/胸闷/乏力/心慌/疼」 | `metric_set=symptom` |
|
||
| 评估 | 「最近怎么样」「帮我看看」「有没有异常」 | 评估流 |
|
||
| 档案 | 「我的档案」「历史记录」「报告」 | 档案流 |
|
||
|
||
**关键原则**:宁可多问一次,也不要错记一条。错误的 Observation 会污染 Personal Baseline,代价远高于多一次交互。
|
||
|
||
歧义值必须强制确认,例如「体重 137」显然异常但格式合法 → 走合理性校验(§6.2)标记 `suspect`。
|
||
|
||
---
|
||
|
||
## 6. 拍照精准记录设计
|
||
|
||
最易漂移的环节,单独设计。
|
||
|
||
### 6.1 先分清两类图片
|
||
|
||
| 类型 | 内容 | 归属 |
|
||
|---|---|---|
|
||
| **设备读数照** | 血压计、体重秤、血氧仪、体温计屏幕 | → `HealthObservation`(数值) |
|
||
| **医疗报告** | 体检报告、化验单、影像报告、处方 | → `HealthDocument`(档案)+ 抽取指标 |
|
||
|
||
判定策略分层,成本递增:
|
||
|
||
1. **用户已在 `AWAIT_VALUE`**(先选「血压」再拍照)→ **类型已确定**,直接按血压解析。**这是最精准路径,产品应主推。**
|
||
2. **`IDLE` 直接发图** → 轻量视觉判断「设备屏幕 vs 文档」,然后**回确认**:「这看起来是血压计读数,要记录为血压吗?」
|
||
3. **判断不确定** → 直接给选项:「这张图是:1 设备读数 2 体检/化验报告 3 其他」
|
||
|
||
产品引导上把「先选类型再拍」做成主路径:
|
||
|
||
```text
|
||
健康录入 → 选「血压」 → 「请输入数值或拍照上传」 → 拍照
|
||
```
|
||
|
||
先验极强,几乎不可能错。
|
||
|
||
### 6.2 设备读数:抽取 → 确认 → 提交
|
||
|
||
**绝不允许「拍照即入库」。**
|
||
|
||
```text
|
||
图片
|
||
↓
|
||
① 图片指纹(内容 hash)去重 ← 防止同一照片重复录入
|
||
↓
|
||
② 结构化抽取(视觉模型 + 强约束 schema)
|
||
↓
|
||
③ 合理性校验(Rule Engine,确定性)
|
||
↓
|
||
④ 生成 ObservationDraft(草稿态,不进基线)
|
||
↓
|
||
⑤ 用户确认卡(H5 可编辑表单;服务号文本 + 回复确认)
|
||
↓
|
||
⑥ 确认后 → HealthObservation(正式)+ 原图存档 + 抽取留痕
|
||
```
|
||
|
||
第 ② 步要求模型输出严格 schema:
|
||
|
||
```json
|
||
{
|
||
"metric_set": "blood_pressure",
|
||
"systolic": 137,
|
||
"diastolic": 82,
|
||
"pulse": 72,
|
||
"device_time": "2026-09-02 07:20",
|
||
"confidence": { "systolic": 0.97, "diastolic": 0.95, "pulse": 0.88 },
|
||
"unreadable": []
|
||
}
|
||
```
|
||
|
||
第 ③ 步校验规则(确定性,不经 LLM):
|
||
|
||
| 校验 | 规则 | 不通过处理 |
|
||
|---|---|---|
|
||
| 量程 | 收缩压 60–260;舒张压 40–160;脉搏 30–200;SpO₂ 50–100;体温 33–43 | 标记 `unreadable`,要求重拍或手输 |
|
||
| 逻辑 | 收缩压 > 舒张压 | 提示可能识别颠倒,要求确认 |
|
||
| 基线偏离 | 与个人基线偏离 > 50% | 标记 `suspect`,**强制**人工确认 |
|
||
| 置信度 | 任一字段 `confidence < 0.9` | 该字段标记待确认,高亮展示 |
|
||
|
||
第 ⑥ 步的**留痕**至关重要:`raw_payload` 保留原始抽取 JSON、图片引用、用户是否修改过。将来审计「这条 137 是怎么来的」可完整回溯,也是 Event Engine evidence chain 的基础。
|
||
|
||
### 6.3 时间戳:隐藏的漂移源
|
||
|
||
「什么时候测的」与「值是多少」同样重要,且极易出错。优先级:
|
||
|
||
```text
|
||
1. 设备屏幕显示时间(OCR 得到) ← 最准
|
||
2. 图片 EXIF 拍摄时间 ← 次准
|
||
3. 用户手动指定 ← 兜底
|
||
4. 消息到达时间 ← 最后手段,必须提示用户
|
||
```
|
||
|
||
若只能用第 4 种,确认卡必须明确写:「测量时间按当前时间记录,如非刚测请修改」。
|
||
|
||
**必须支持补录改时间** —— 否则 Timeline 全错,基线随之失真。
|
||
|
||
### 6.4 报告类:归档与指标抽取分离
|
||
|
||
```text
|
||
报告图片 / PDF
|
||
↓
|
||
① 归档(必做,成功率高)
|
||
→ HealthDocument:类型、机构、报告日期、原文件、OCR 全文
|
||
→ 原文件落入 MindSpace 健康分区资产
|
||
↓
|
||
② 指标抽取(尽力而为,允许失败)
|
||
→ 化验项映射:血脂 / 血糖 / 肝功 / 肾功 / 血常规 ...
|
||
→ 每项带 confidence + 参考范围
|
||
→ 高置信项 → ObservationDraft(待确认)
|
||
→ 低置信项 → 仅存 OCR 文本,不入结构化
|
||
```
|
||
|
||
**归档必须成功,抽取允许失败。** 即使指标识别不出,用户报告也已安全存进档案随时可查 —— 这是产品底线价值。
|
||
|
||
### 6.5 与现有视觉守卫的兼容(硬约束)
|
||
|
||
`docs/regression-guards/vision-turn-read-image-isolation.md` 的守卫**必须继续生效**:
|
||
|
||
- 带图轮次必须摘掉 `read_image`
|
||
- 工具图片污染必须触发会话轮换
|
||
|
||
健康通道的图片处理**必须走同一套** `chat-image-turn-scope.mjs`,**禁止**另开绕过守卫的路径。健康通道只是在其**之上**叠加「结构化 schema 约束 + 确认闸门」,底层图片轮次隔离不变。
|
||
|
||
---
|
||
|
||
## 7. 会话隔离(D5)
|
||
|
||
「不影响现有功能」的关键。
|
||
|
||
```text
|
||
普通会话 健康会话
|
||
───────── ─────────
|
||
sessionKind: 'chat' sessionKind: 'health'
|
||
完整 skill 集 仅 health-assistant + 白名单工具
|
||
Memory V2 全量 resolve 健康上下文 + 有限 profile
|
||
可发布任意分区页面 仅健康区(强制加密)
|
||
```
|
||
|
||
具体隔离点:
|
||
|
||
1. **会话独立**:健康会话使用独立 `session_id` 命名空间。普通聊天历史不注入健康会话;健康数据不会出现在普通聊天上下文。
|
||
2. **工具白名单**:健康会话内由 `enforceSelectedSkillRuntime` 限定工具集 —— 仅允许健康数据读写、文档归档、健康区页面生成。**不允许** `edit_file` 写非健康区路径,**不允许**公开发布。
|
||
3. **Memory 分轨**:Observation 走独立数据面;仅「本人对睡眠敏感」这类**关联假设**晋升 Memory V2,带 `health_correlation` 标记。他人健康数据只进备忘(D2)。
|
||
4. **引导跳转**:普通聊天识别到健康意图时,**只回引导**,不落库:
|
||
|
||
```text
|
||
你想记录健康数据吗?健康数据需要在健康助手通道里录入,
|
||
这样才能保证记录准确并纳入你的健康基线分析。
|
||
|
||
[进入健康助手 →]
|
||
```
|
||
|
||
5. **服务号状态**:健康通道状态存于服务号会话状态,与现有 canary/stable 钉选并存。
|
||
|
||
---
|
||
|
||
## 8. 数据模型(Page Data 落地,D3)
|
||
|
||
### 8.1 分层存储决策
|
||
|
||
| 数据 | 存储 | 理由 |
|
||
|---|---|---|
|
||
| 健康数值(Observation) | Page Data 用户隔离 PG schema | 结构化、可聚合、已有隔离与审计 |
|
||
| 报告原文件(图片/PDF) | MindSpace 健康分区资产(`h5_assets`) | Page Data 不适合大文件 |
|
||
| 报告元数据 + OCR 全文 | Page Data(PG) | 需检索与关联 |
|
||
| 基线 / 事件 | Page Data(PG),P1 | 由引擎写入 |
|
||
| 关联假设 | Memory V2 | 需对话召回 |
|
||
| 提醒 / 复诊 | `h5_schedule_items` | 复用现有提醒链路 |
|
||
|
||
**注意**:报告文件与其元数据分离存储,通过 `asset_id` 关联。
|
||
|
||
### 8.2 表设计(PostgreSQL,用户隔离 schema)
|
||
|
||
```sql
|
||
-- 健康档案(本人,1 行)
|
||
CREATE TABLE IF NOT EXISTS health_profile (
|
||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||
birth_year INT NULL,
|
||
sex TEXT NULL,
|
||
height_cm NUMERIC(5,1) NULL,
|
||
chronic_conditions JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||
long_term_medications JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||
notes TEXT NULL,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
-- 原子观测(所有体征 / 行为 / 主观状态的唯一写入格式)
|
||
CREATE TABLE IF NOT EXISTS health_observations (
|
||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||
observed_at TIMESTAMPTZ NOT NULL, -- 测量时刻,非录入时刻
|
||
metric_type TEXT NOT NULL, -- bp_systolic | bp_diastolic | hr | spo2 | weight | temperature | sleep_minutes | symptom | ...
|
||
value_num NUMERIC(10,2) NULL,
|
||
value_text TEXT NULL, -- 症状等枚举/文本
|
||
unit TEXT NULL,
|
||
context TEXT NULL, -- morning | evening | post_medication | fasting | other
|
||
source TEXT NOT NULL, -- manual | photo | voice | ocr_report | apple_health | ble_device
|
||
source_ref TEXT NULL, -- 图片 hash / asset_id / 设备标识
|
||
time_source TEXT NOT NULL, -- device_screen | exif | user_specified | message_arrival
|
||
confidence NUMERIC(4,3) NULL,
|
||
quality_flag TEXT NULL, -- ok | suspect | user_corrected
|
||
raw_payload JSONB NULL, -- 原始抽取结果 + 审计留痕
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
deleted_at TIMESTAMPTZ NULL
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_health_obs_metric_time
|
||
ON health_observations (metric_type, observed_at DESC);
|
||
|
||
-- 幂等:同一图片 + 同一指标 + 同一测量时刻不重复入库
|
||
CREATE UNIQUE INDEX IF NOT EXISTS uq_health_obs_dedup
|
||
ON health_observations (metric_type, observed_at, source_ref)
|
||
WHERE source_ref IS NOT NULL AND deleted_at IS NULL;
|
||
|
||
-- 待确认草稿(不进基线引擎)
|
||
CREATE TABLE IF NOT EXISTS health_observation_drafts (
|
||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||
draft_key TEXT NOT NULL UNIQUE,
|
||
metric_set TEXT NOT NULL, -- blood_pressure | weight | spo2 | ...
|
||
extracted JSONB NOT NULL, -- 抽取结果 + confidence
|
||
validation JSONB NOT NULL, -- 校验结果 + 标记
|
||
status TEXT NOT NULL DEFAULT 'pending', -- pending | committed | discarded | expired
|
||
channel TEXT NOT NULL, -- h5 | wechat
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
expires_at TIMESTAMPTZ NOT NULL,
|
||
committed_at TIMESTAMPTZ NULL
|
||
);
|
||
|
||
-- 医疗资料档案
|
||
CREATE TABLE IF NOT EXISTS health_documents (
|
||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||
doc_type TEXT NOT NULL, -- checkup | lab | imaging | prescription | discharge | other
|
||
report_date DATE NULL,
|
||
institution TEXT NULL,
|
||
title TEXT NOT NULL,
|
||
asset_id TEXT NULL, -- MindSpace 健康分区资产引用(原文件)
|
||
ocr_text TEXT NULL,
|
||
extracted_metrics JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||
extraction_status TEXT NOT NULL DEFAULT 'pending', -- pending | partial | done | failed
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
deleted_at TIMESTAMPTZ NULL
|
||
);
|
||
|
||
-- 个人基线(P1,引擎写入)
|
||
-- 注意:基线必须按 context 分层(见 §15 O3)。晨间与晚间血压生理上不同,
|
||
-- 混算基线会同时掩盖晨峰异常并制造假偏离。
|
||
CREATE TABLE IF NOT EXISTS health_baselines (
|
||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||
metric_type TEXT NOT NULL,
|
||
context TEXT NOT NULL, -- morning | evening | any(不分层指标用 any)
|
||
window_days INT NOT NULL, -- 7 | 30 | 90
|
||
computed_at TIMESTAMPTZ NOT NULL,
|
||
sample_count INT NOT NULL,
|
||
mean_val NUMERIC(10,2) NULL,
|
||
std_val NUMERIC(10,2) NULL,
|
||
p25_val NUMERIC(10,2) NULL,
|
||
p75_val NUMERIC(10,2) NULL,
|
||
typical_low NUMERIC(10,2) NULL,
|
||
typical_high NUMERIC(10,2) NULL,
|
||
maturity TEXT NOT NULL, -- cold | weak | stable
|
||
UNIQUE (metric_type, context, window_days)
|
||
);
|
||
|
||
-- 健康事件(P1)
|
||
CREATE TABLE IF NOT EXISTS health_events (
|
||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||
event_type TEXT NOT NULL, -- threshold_breach | baseline_drift | missed_measurement | symptom_cluster | correlation_hypothesis
|
||
severity TEXT NOT NULL, -- info | watch | alert
|
||
detected_at TIMESTAMPTZ NOT NULL,
|
||
rule_id TEXT NULL, -- 可审计:哪条规则触发
|
||
metrics_involved JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||
evidence_observation_ids JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||
agent_summary TEXT NULL, -- LLM 生成,不可作为 detection 依据
|
||
status TEXT NOT NULL DEFAULT 'open', -- open | acknowledged | resolved | dismissed
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
-- 关联假设(P1;晋升到 Memory V2 后记录 memory_item_id)
|
||
CREATE TABLE IF NOT EXISTS health_correlations (
|
||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||
factor_a TEXT NOT NULL, -- sleep_deterioration
|
||
factor_b TEXT NOT NULL, -- bp_elevation
|
||
confidence NUMERIC(4,3) NOT NULL,
|
||
occurrence_count INT NOT NULL DEFAULT 1,
|
||
first_seen_at TIMESTAMPTZ NOT NULL,
|
||
last_seen_at TIMESTAMPTZ NOT NULL,
|
||
user_confirmed BOOLEAN NOT NULL DEFAULT FALSE,
|
||
memory_item_id TEXT NULL
|
||
);
|
||
```
|
||
|
||
设计要点:
|
||
|
||
- `health_observations` 是**唯一写入格式**。血压拆成 `bp_systolic` / `bp_diastolic` 两行,便于统一基线计算;确认卡在 UI 层合并展示。
|
||
- 所有含软删除的表都带 `deleted_at`(Page Data `soft_delete` 硬要求)。
|
||
- 唯一索引实现**幂等**:重复发送同一张照片不会产生重复观测。
|
||
- `time_source` 显式记录时间来源,用于后续数据质量分析。
|
||
|
||
### 8.3 Dataset 注册
|
||
|
||
每个表独立注册 dataset,遵守 `page-data-collect` 的「一任务一表一 dataset」原则。
|
||
|
||
```json
|
||
{
|
||
"name": "health_observations",
|
||
"table": "health_observations",
|
||
"description": "MeMind Health 个人健康观测记录",
|
||
"actions": ["read", "insert", "update", "softDelete"],
|
||
"columns": {
|
||
"read": ["id", "observed_at", "metric_type", "value_num", "value_text", "unit", "context", "source", "quality_flag", "created_at"],
|
||
"insert": ["observed_at", "metric_type", "value_num", "value_text", "unit", "context", "source", "source_ref", "time_source", "confidence", "quality_flag", "raw_payload"],
|
||
"update": ["observed_at", "value_num", "value_text", "context", "quality_flag"]
|
||
}
|
||
}
|
||
```
|
||
|
||
**关键安全约束**:`raw_payload`、`source_ref`、`ocr_text` **不得**出现在任何公开页的 `read` 白名单中(见 §9.4)。
|
||
|
||
### 8.4 写入路径(重要澄清)
|
||
|
||
Page Data 有两条 API:公开页 API 与 Owner Admin API。健康数据的写入**不走公开页 API**:
|
||
|
||
```text
|
||
H5 健康录入 / 服务号录入
|
||
→ 平台内登录态请求
|
||
→ health-observation-service.mjs
|
||
→ Owner 侧 Page Data 访问(/api/admin/page-data/* 或服务端直连 dataset 授权)
|
||
→ 用户隔离 PG schema
|
||
|
||
MindSpace 健康区页面(Timeline / 档案展示)
|
||
→ 走公开页 API,但仅 read
|
||
→ 且只读「摘要快照 dataset」,不直连原始 observations(见 §9.4)
|
||
```
|
||
|
||
这样原始健康数据永不通过公开页 API 暴露。
|
||
|
||
---
|
||
|
||
## 9. MindSpace 健康区:强制加密
|
||
|
||
### 9.1 新增系统分区
|
||
|
||
在 `mindspace.mjs` 的 `SYSTEM_CATEGORIES` 增加一项(设计,尚未实现):
|
||
|
||
```js
|
||
{
|
||
code: 'health',
|
||
name: '健康档案区',
|
||
visibilityPolicy: 'private',
|
||
aiAccessPolicy: 'explicit_asset_grant', // AI 需显式授权才能读具体资料
|
||
publishPolicy: 'password_required', // 新增策略值
|
||
sortOrder: 30,
|
||
}
|
||
```
|
||
|
||
`h5_space_categories.publish_policy` 是 `VARCHAR(64)`,新增枚举值**不需要改表结构**。
|
||
|
||
现有系统分区(`oa` / `public` / `draft` / `archive`)策略保持不变。
|
||
|
||
### 9.2 发布闸门(服务端硬阻断)
|
||
|
||
```text
|
||
publish(pageId, accessMode, password)
|
||
↓
|
||
category = resolveCategory(pageId)
|
||
↓
|
||
if (category.publishPolicy === 'password_required') {
|
||
if (accessMode === 'public') → 拒绝:健康区禁止完全公开
|
||
if (!password && !hasExistingPasswordHash) → 拒绝:必须设置口令
|
||
if (accessMode not in ['password', 'login_required']) → 拒绝
|
||
}
|
||
↓
|
||
继续现有发布流程(不改动其他分区行为)
|
||
```
|
||
|
||
必须是**服务端阻断**,不是前端禁用选项。前端仅额外做「不展示完全公开选项」的体验优化。
|
||
|
||
口令须满足现有平台约束:**≥ 6 位**,bind 时必须传入并写入 `password_hash`。禁止只改 `access_mode` 不写 `password_hash`。
|
||
|
||
### 9.3 访问体验:本人免密,外部必密
|
||
|
||
```text
|
||
访问健康页
|
||
↓
|
||
是 owner 且已登录?
|
||
├─ 是 → 直接放行(不要求输口令)
|
||
└─ 否 → 要求口令 → 校验 → 发 page-data token → 放行
|
||
```
|
||
|
||
- 健康区默认 `password` 模式(可分享给医生)。
|
||
- owner 登录态访问**短路**口令校验。
|
||
- 口令可重置、令牌可撤销,复用现有能力:
|
||
- `POST /api/admin/page-data/policies/:pageId/password/reset`
|
||
- `POST /api/admin/page-data/policies/:pageId/tokens/revoke`
|
||
|
||
### 9.4 分享安全:快照而非直连(关键设计)
|
||
|
||
`password` 模式**无法区分访客身份** —— 拿到口令的人能读该页授权的所有行。因此:
|
||
|
||
**禁止**健康区公开页直接 `read` `health_observations` / `health_documents`。
|
||
|
||
**正确做法**:为分享场景生成**只读摘要快照 dataset**:
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS health_share_snapshots (
|
||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||
snapshot_key TEXT NOT NULL UNIQUE,
|
||
scope TEXT NOT NULL, -- clinic_summary | trend_30d | lab_selected
|
||
period_start DATE NOT NULL,
|
||
period_end DATE NOT NULL,
|
||
payload JSONB NOT NULL, -- 已脱敏、已聚合的展示数据
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
expires_at TIMESTAMPTZ NULL,
|
||
revoked_at TIMESTAMPTZ NULL
|
||
);
|
||
```
|
||
|
||
- 分享页只 `read` `health_share_snapshots`,且 `columns.read` 仅含 `payload` 等必要字段。
|
||
- 快照**由用户显式生成**,范围、时间窗、包含项由用户勾选。
|
||
- 快照可设过期时间、可撤销。
|
||
- 原始 `observations` / `ocr_text` / `raw_payload` **永不**进入任何公开页授权。
|
||
|
||
这解决了「分享给医生」与「数据最小暴露」的矛盾。
|
||
|
||
### 9.5 SEO 自动安全(无需改动)
|
||
|
||
`mindspace-index-policy.mjs` 中 `INDEXABLE_ACCESS_MODE = 'public'`,`isPublicationIndexable()` 要求 `accessMode === 'public'`。
|
||
|
||
健康区强制 `password` / `login_required` 后 → **自动 noindex**,无需额外改 SEO 逻辑。
|
||
|
||
此行为落在现有回归守卫 `docs/regression-guards/mindspace-seo-geo.md` 保护范围内,改动健康区时须一并跑 `npm run verify:seo-geo`。
|
||
|
||
### 9.6 健康区内容结构
|
||
|
||
```text
|
||
健康档案区/
|
||
timeline/ 健康时间线页(加密,owner 免密)
|
||
documents/ 报告归档
|
||
2026-08-体检报告/
|
||
2026-09-血脂化验/
|
||
assessments/ 评估摘要
|
||
shares/ 就诊摘要快照页(口令 + 可撤销 + 可过期)
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Health Timeline 与 Personal Baseline
|
||
|
||
### 10.1 Health Timeline(日聚合视图,非存储层)
|
||
|
||
```text
|
||
2026-09-01
|
||
BP 128/76
|
||
HR 68
|
||
Weight 67.2kg
|
||
Sleep 6h32m
|
||
Symptom none
|
||
|
||
2026-09-02
|
||
BP 137/82 ↑ vs 30d baseline +9.8%
|
||
HR 72
|
||
Weight 67.4kg
|
||
Sleep 5h10m ↓ vs 30d baseline -17%
|
||
|
||
7-day trend:
|
||
BP baseline deviation +9.8%
|
||
Sleep baseline deviation -17%
|
||
```
|
||
|
||
实现:`health_observations` 按日 rollup。P0 用查询时聚合;数据量增长后再引入物化视图或定时 job。
|
||
|
||
### 10.2 Personal Baseline(P1)
|
||
|
||
滚动窗口,每日凌晨重算 7 / 30 / 90 天基线。
|
||
|
||
冷启动策略(`maturity` 字段):
|
||
|
||
| 阶段 | 样本量 | maturity | 行为 |
|
||
|---|---|---|---|
|
||
| 冷启动 | < 7 个有效点 | `cold` | 只展示原始值,**不产生偏离事件** |
|
||
| 弱基线 | 7–29 | `weak` | 可产生 `info` 级提示,不发 `alert` |
|
||
| 稳定 | ≥ 30 | `stable` | 全量启用 Trend Engine |
|
||
|
||
这条很重要:**基线未成熟就报警会摧毁用户信任。**
|
||
|
||
有效点定义:`quality_flag != 'suspect'` 且 `deleted_at IS NULL`。`suspect` 只是**待裁决**状态 —— 用户确认后必须落到 `ok` 或 `user_corrected`,不得长期停留,否则真实的恶化会被永久过滤掉。
|
||
|
||
**样本量按 context 分层计算**(O3 决策的直接后果):晨间血压基线只统计 `context='morning'` 的观测。这意味着若用户每天只测一次晨间血压,达到 `stable` 需 30 天;若晨晚各一次,两条基线各自独立计数,**不会因为总数翻倍而提前成熟**。
|
||
|
||
`weight`、`sleep_minutes` 等本身不分晨晚的指标使用 `context='any'`。
|
||
|
||
### 10.3 Rule Engine(确定性)
|
||
|
||
| 规则类 | 示例 | 严重度 |
|
||
|---|---|---|
|
||
| 绝对阈值 | SpO₂ < 90% | `alert` |
|
||
| 绝对阈值 | 收缩压 ≥ 180 或 舒张压 ≥ 110 | `alert` |
|
||
| 漏测 | 连续 3 天无晨间血压 | `watch` |
|
||
| 新症状 | 连续 3 天报告同一症状 | `watch` |
|
||
| 药物变化 | 新增/停用药物后 7 天内加强监测 | `info` |
|
||
|
||
输出 `health_events(event_type='threshold_breach', rule_id=...)`。
|
||
|
||
### 10.4 Trend Engine(统计,不用 LLM)
|
||
|
||
| 检测项 | 方法 |
|
||
|---|---|
|
||
| 基线漂移 | 近 14 天均值 vs 90 天基线,偏离 > 阈值且持续 ≥ N 天 |
|
||
| 变异性增大 | 近 7 天 std > 历史 std × 1.5 |
|
||
| 多指标联动 | 睡眠 ↓ + 静息 HR ↑ + BP ↑ 同期发生 → cluster |
|
||
|
||
输出 `health_events(event_type='baseline_drift' | 'symptom_cluster')`。
|
||
|
||
### 10.5 Event Engine → Agent
|
||
|
||
Event Engine 合并 Rule + Trend 结果,去重、分级、聚合为 Cluster,然后才调用 Agent。
|
||
|
||
**Agent 的输入是结构化 Event,不是原始时序**:
|
||
|
||
```json
|
||
{
|
||
"cluster_id": "...",
|
||
"window": "14d",
|
||
"baseline_maturity": "stable",
|
||
"findings": [
|
||
{ "metric": "bp_systolic", "deviation": "+8%", "baseline": "119-126", "recent": "132-138", "days_above": 5 },
|
||
{ "metric": "sleep_minutes", "deviation": "-17%" }
|
||
],
|
||
"open_questions": ["recent_insomnia", "medication_change", "alcohol", "stress"]
|
||
}
|
||
```
|
||
|
||
Agent 职责:
|
||
|
||
1. 用自然语言解释「发生了什么变化」(**不是诊断**)
|
||
2. 提出 1–2 个高价值追问
|
||
3. 给出行动建议:复测计划 / 记录给医生 / 何时就医
|
||
4. 将用户回答与关联假设写入 `health_correlations` → 满足条件后晋升 Memory V2
|
||
|
||
Agent **不能**反向修改 Event 严重度 —— 那是 Engine 的职责。
|
||
|
||
理想输出示例:
|
||
|
||
> 过去 30 天整体生命体征较稳定。最近 7 天晨间收缩压较过去 90 日个人基线上升约 8 mmHg,其中 5 天高于近期常态;同期睡眠时间有所下降。建议未来 7 天按规范继续早晚测量,如持续偏高,可将趋势记录提供给医生评估。
|
||
|
||
### 10.6 关联假设的长期学习
|
||
|
||
```text
|
||
第一次失眠期 → BP +7%
|
||
第二次失眠期 → BP +9%
|
||
第三次失眠期 → BP +8%
|
||
↓
|
||
occurrence_count = 3, confidence ↑
|
||
↓
|
||
晋升 Memory V2(label: health_correlation)
|
||
↓
|
||
"这个人的血压可能对睡眠变化比较敏感"
|
||
```
|
||
|
||
**术语纪律**:必须称「关联假设」,**禁止**表述为医疗诊断或因果结论。
|
||
|
||
---
|
||
|
||
## 11. 零侵入保证与灰度
|
||
|
||
### 11.1 四条保证
|
||
|
||
1. **全局 Feature Flag**:`MEMIND_HEALTH_ENABLED=false` 时,健康入口不展示、路由不注册、服务号关键词不生效,现有链路**行为不变**。
|
||
2. **只做加法**:意图路由**不改判定逻辑**,健康会话天然携带 `selectedChatSkill`,走的是已存在的短路分支。
|
||
3. **发布闸门只对 `health` 分区生效**:其他分区 `publish_policy` 不变,现有发布行为不受影响。
|
||
4. **fail-open**:抽取/基线服务不可用时,退化为「仅归档原图 + 提示稍后重试」,不阻塞会话、不影响普通聊天。
|
||
|
||
### 11.2 灰度
|
||
|
||
沿用仓库既有 canary 模式:按 `user_id` 灰度,先内部账号 → 小流量 → 全量。健康引擎(P1)应支持 `off / shadow / active` 三态,shadow 模式下只记录会产生哪些事件、不实际通知用户。
|
||
|
||
### 11.3 必须遵守的既有守卫
|
||
|
||
改动触及以下路径时,按 `AGENTS.md` 规则必跑对应 verify:
|
||
|
||
```bash
|
||
npm run verify:mindspace-publish-guards
|
||
npm run verify:mindspace-publish-guards:full
|
||
npm run verify:seo-geo
|
||
npm run verify:seo-discovery
|
||
npm run verify:page-data
|
||
```
|
||
|
||
特别注意:
|
||
|
||
- `chat-image-turn-scope.mjs` — 带图轮次 `read_image` 隔离
|
||
- `mindspace-index-policy.mjs` — 私有页 noindex
|
||
- `mindspace-page-sync-service.mjs` — remote 模式也必须 sync public HTML
|
||
|
||
### 11.4 新增测试
|
||
|
||
```text
|
||
health-channel-state.test.mjs 状态机全转移覆盖 + 无关输入不猜测
|
||
health-extraction.test.mjs schema 约束 / 量程校验 / 置信度门限
|
||
health-observation-service.test.mjs 幂等去重 / 时间来源优先级 / suspect 标记
|
||
health-baseline-engine.test.mjs 冷启动 maturity / 有效点过滤
|
||
health-event-engine.test.mjs 规则触发 + evidence chain 完整性
|
||
health-publish-guard.test.mjs 健康区禁止 public 发布(硬阻断)
|
||
health-share-snapshot.test.mjs 分享页不得授权原始 observations
|
||
```
|
||
|
||
---
|
||
|
||
## 12. 合规与产品边界
|
||
|
||
1. **非医疗器械声明**:MeMind Health 是健康记录与趋势提醒,**不提供诊断**。
|
||
2. **关联假设 ≠ 诊断**:Agent 输出统一附 disclaimer 模板。
|
||
3. **Evidence chain 可追溯**:每条 alert 可展开到 `evidence_observation_ids` + `rule_id`。监管与客诉必备。
|
||
4. **数据分级**:健康观测为敏感数据。分享须用户显式生成快照、可过期、可撤销。
|
||
5. **LLM 边界**:detection 层**零 LLM**;explanation 层 LLM 输入仅为结构化 Event。
|
||
6. **删除权**:用户可删除任意 Observation / Document;删除后基线须重算。
|
||
|
||
### 12.1 隐私默认值
|
||
|
||
- 健康区默认 `private`,`aiAccessPolicy: explicit_asset_grant`。
|
||
- Agent 读取具体报告内容需显式授权,不默认全量注入上下文。
|
||
- 健康数据**不参与** SEO/GEO 收录(§9.5 自动保证)。
|
||
|
||
### 12.2 数据保留
|
||
|
||
复用 Memory V2 生命周期思路:Observation 长期保留(健康趋势需要长历史);但抽取中间产物(原始视觉输出)可设较短保留期。
|
||
|
||
### 12.3 P1 存储迁移预案
|
||
|
||
若 Page Data 聚合性能不足,迁移到专用表的前提:
|
||
|
||
- 保持 `health_observations` 列结构不变,仅换存储位置
|
||
- 提供双写 + 校验期
|
||
- 迁移前后 Timeline 与基线结果必须一致(回归对账脚本)
|
||
|
||
---
|
||
|
||
## 13. 分阶段路线
|
||
|
||
### P0 — 验证入口与录入精度(不做引擎)
|
||
|
||
- H5 `/health` 入口 + 通道状态机
|
||
- 手动录入 + 拍照录入(三段式确认)
|
||
- Health Timeline 展示
|
||
- MindSpace 健康区(强制加密)+ 报告归档
|
||
- 服务号:关键词/菜单进入 + 拍照上传 + 确认落库
|
||
- `health-assistant` skill v0(读 Timeline 摘要回答「最近怎么样」)
|
||
|
||
**不做**:基线、Event Engine、设备接入、分享。
|
||
|
||
**验证指标**:
|
||
|
||
- 拍照识别经确认后准确率 ≥ 98%
|
||
- 通道内漂移事件(被路由到非健康 skill)= 0
|
||
- 健康区无法以 `public` 模式发布(自动化测试保证)
|
||
- 连续 14 天录入完成率
|
||
|
||
### P1 — Personal Baseline + Event Engine(核心命题验证)
|
||
|
||
- Baseline Engine(7/30/90 天 + maturity)
|
||
- Trend Engine + Rule Engine v1
|
||
- Event Engine + Agent 解释
|
||
- 关联假设 v0 → Memory V2 晋升
|
||
|
||
**验证方式**:回溯已有 30 天数据,Event Engine 能否在用户主观感知前 3–7 天产生 `watch` 事件;并访谈「这条提醒有用吗?会不会太吵?」
|
||
|
||
### P2 — 设备接入与提醒
|
||
|
||
- Apple Health / 蓝牙设备 Connector(设备可信 → 免确认路径,但异常值仍标 `suspect`)
|
||
- 分级通知策略(`info` 静默 / `watch` 日摘要 / `alert` 即时)
|
||
|
||
### P3 — 医疗纵深
|
||
|
||
- 用药 Timeline(整合 `schedule-assistant`)
|
||
- 检验指标长期趋势
|
||
- 就诊摘要加密分享页(§9.4 快照机制)
|
||
|
||
### P4 — 授权分享
|
||
|
||
- 本人授权家人/医生**只读摘要**(非代录、非原始数据)
|
||
- 机构侧只读接口(如有需求)
|
||
|
||
---
|
||
|
||
## 14. P0 前置最小实验(先做,成本极低)
|
||
|
||
在写任何平台代码前,用 14 天人工实验验证核心假设。
|
||
|
||
**完整可执行方案见 [memind-health-p0-experiment.md](memind-health-p0-experiment.md)**,此处仅摘要。
|
||
|
||
```text
|
||
参与者:8–10 人(最低 5 人);有意纳入更可能出现波动的人群
|
||
工具: 现有 Page Data 匿名问卷页(方案 A),无需新代码
|
||
|
||
D1–D7 : 基线建设期,每日录入晨间 BP + 睡眠 + 症状 + 扰动因素
|
||
D8 : 手工计算 7 天基线(Excel 即可)
|
||
D8–D14: 继续录入,研究者每日手工标记偏离(不告知参与者)
|
||
D14 : 盲态访谈(必须在揭盲前完成)
|
||
```
|
||
|
||
**主结局 H1(一定可测)**:14 天晨测完成率 ≥ 80% 的参与者 ≥ 3/5。
|
||
|
||
**次结局 H2(可能不可判定)**:手工偏离标记早于主观察觉 ≥ 3 天的参与者 ≥ 3/5。
|
||
|
||
关键设计取舍:
|
||
|
||
- **H1 是主结局,不是 H2。** 若用户坚持不下来,再精妙的基线算法都无意义;此时正确反应是把设备自动同步提到最高优先级,而不是优化算法。
|
||
- **14 天内可能无人偏离**,这是最可能的失败模式,方案中已预设无偏离处理路径,不得因 H2 无结论就否定项目方向。
|
||
- **漏检比误报更值得研究**:用户明确感到变化而基线检测不到,说明指标组合或窗口有问题。
|
||
- **顺带产出照片校验集**:参与者同时提供设备屏幕照片 + 手工录入值,形成 `(图片 → 正确值)` 标注对,直接用作 P0 拍照识别的回归用例。这是很难另外获得的资产。
|
||
|
||
---
|
||
|
||
## 15. 已决事项(O1–O6)
|
||
|
||
以下六项已决策,实现必须遵守。变更需重新评审本章。
|
||
|
||
### O1 症状录入:固定枚举 + 严重度 + 可选备注
|
||
|
||
**决策**:采用固定枚举,附三级严重度,允许自由备注,但备注**不参与**结构化分析。
|
||
|
||
```text
|
||
枚举(可多选;无症状必须显式选「无」):
|
||
无 / 头晕 / 头痛 / 胸闷 / 心慌心悸 / 乏力 / 气短 / 下肢水肿 / 失眠 / 其他
|
||
|
||
严重度(每个勾选项必填):
|
||
轻 = 1 / 中 = 2 / 重 = 3
|
||
|
||
自由备注:可选,存 value_text,仅供 Agent 解释时参考
|
||
```
|
||
|
||
**理由**:`symptom_cluster` 检测需要可比较的离散值。自由文本会产生「有点晕」「头晕晕的」「晕」等语义漂移,无法计数、无法判断「连续 3 天同一症状」。
|
||
|
||
**存储**:每个症状一行 `health_observations`,`metric_type='symptom'`,`value_text=` 枚举码,`value_num=` 严重度。这样症状严重度也可做趋势(如「头晕从轻转中」)。
|
||
|
||
**「无症状」必须显式记录**,不能靠「没有记录」推断 —— 否则无法区分「今天没症状」与「今天忘了记」。
|
||
|
||
### O2 语音录入:必须确认,不得直接落库
|
||
|
||
**决策**:语音识别结果**一律**进入 `CONFIRM` 状态,禁止跳过确认。
|
||
|
||
**理由**:数字类语音识别错误率显著高于文本,且错误形态危险 —— 「一百三十七比八十二」可能识别为「137、82」也可能是「130、782」。血压场景一次错记就污染基线。
|
||
|
||
**附加要求**:语音录入的确认卡必须**同时展示识别原文**,让用户能判断是识别错了还是自己说错了:
|
||
|
||
```text
|
||
听到:「血压一百三十七比八十二」
|
||
识别:收缩压 137,舒张压 82
|
||
确认保存 / 修改 / 重说
|
||
```
|
||
|
||
### O3 血压强制记录情境,且基线按情境分层
|
||
|
||
**决策**:血压录入**强制**记录情境,三选一,按测量时刻自动预选,用户可改。
|
||
|
||
```text
|
||
morning 晨间(默认:测量时刻 < 10:00)
|
||
evening 晚间(默认:测量时刻 ≥ 18:00)
|
||
other 其他(10:00–18:00 默认,或用户手动指定)
|
||
```
|
||
|
||
**这不只是一个字段 —— 基线必须按 context 分层计算**(已反映到 §8.2 `health_baselines` 的 `UNIQUE (metric_type, context, window_days)`)。
|
||
|
||
**理由**(这是本次评审中最重要的修正):晨间血压与晚间血压生理上是不同指标,个体内可差 10–20 mmHg。若混算基线:
|
||
|
||
- 晨峰升高会被晚间的低值稀释 → **漏掉最有临床意义的异常**
|
||
- 用户某天只测了晚间 → 与混合基线比较 → **产生假偏离**
|
||
|
||
两种错误方向相反,但都由同一个原因造成。因此分层是必需的,不是优化项。
|
||
|
||
**代价**:达到 `stable` 所需天数不因多测而减少(见 §10.2)。这个代价可以接受 —— 宁可基线成熟慢,不要基线本身是错的。
|
||
|
||
`other` 情境的观测**记录但不参与**晨/晚基线计算,仅在 Timeline 展示。
|
||
|
||
### O4 报告 OCR:复用现有图片/文件分析链路,不新建 OCR 服务
|
||
|
||
**决策**:健康侧**不实现**独立 OCR。复用仓库现有的图片与文件分析能力(服务号侧 media analysis、Agent 侧图片/文件读取),健康域只在其**之上**叠加两层:
|
||
|
||
```text
|
||
现有图片/文件分析链路(不改动)
|
||
↓
|
||
health-extraction.mjs
|
||
├─ 强约束输出 schema(metric_set + 字段 + confidence)
|
||
└─ 合理性校验(量程 / 逻辑 / 基线偏离 / 置信门限)
|
||
↓
|
||
ObservationDraft(待确认)
|
||
```
|
||
|
||
**理由**:重复实现 OCR 会造成两套图片处理路径,直接威胁 `chat-image-turn-scope.mjs` 的回归守卫(带图轮次 `read_image` 隔离、工具图片污染触发会话轮换)。健康通道必须走同一条底层路径。
|
||
|
||
**实施前置检查**:动工前需确认现有链路对「多页 PDF」「长图化验单」的支持程度。若不支持,应作为独立需求补齐**通用能力**,而不是在健康域内私建分支。
|
||
|
||
### O5 alert 通知:不承诺推送,主机制为「下次进入即见」
|
||
|
||
**决策**:`alert` 触达**不作为产品承诺**。核心机制是进入即见,推送为 best-effort 补充。
|
||
|
||
分级策略:
|
||
|
||
| 严重度 | 触达方式 |
|
||
|---|---|
|
||
| `info` | 静默,仅健康首页与 Timeline 可见 |
|
||
| `watch` | 进入即见 + 可选日摘要(用户须主动开启) |
|
||
| `alert` | 进入即见(置顶未读)+ 48 小时内有交互时发服务号客服消息 + 已订阅者发订阅通知 |
|
||
|
||
**理由**:服务号客服消息受 48 小时交互窗口限制,订阅通知需用户主动订阅且有频次场景限制;H5 无主动推送能力。**恰恰是长期不互动的用户最需要提醒,而这类用户技术上最难触达。**
|
||
|
||
**因此产品措辞必须修正**:
|
||
|
||
```text
|
||
禁止:「异常时我们会通知你」
|
||
应为:「异常会记录在你的健康档案,并在你下次进入时优先展示」
|
||
```
|
||
|
||
**实现要求**:任何入口进入健康通道时(无论何种原因),必须先展示未读 `alert`,且未读状态在用户明确确认前不清除。
|
||
|
||
**同时**:应引导用户主动订阅服务号通知,并在引导文案中说明原因,而不是默认用户已可触达。
|
||
|
||
### O6 偏离阈值:P0 不定死,由实验数据标定
|
||
|
||
**决策**:不在设计阶段固化阈值。P0 采用下列**初始建议值**,并在前置实验与 P0 数据积累阶段完成标定。
|
||
|
||
初始建议值(仅供实验期手工判定与 P1 起点,**非最终值**):
|
||
|
||
| 指标 | 单日标记 | 持续偏离(触发事件) |
|
||
|---|---|---|
|
||
| 晨间收缩压 | `dev% ≥ +5%` 或 `z ≥ +1.5` | 连续 ≥ 2 天同向 → `watch`;≥ 5 天或 `dev% ≥ +10%` → `alert` 候选 |
|
||
| 晨间舒张压 | 同上 | 同上 |
|
||
| 睡眠时长 | `dev% ≤ -15%` 或 `z ≤ -1.5` | 连续 ≥ 3 天 → 参与 cluster 判定 |
|
||
| 静息心率 | `dev% ≥ +10%` 或 `z ≥ +1.5` | 连续 ≥ 3 天 → 参与 cluster 判定 |
|
||
| 体重 | 7 日内变化 ≥ 1.5kg | 直接 `watch` |
|
||
|
||
**双重判定(相对偏差 OR 标准分)的理由**:相对偏差直观但忽略个体变异性 —— 变异大的人频繁误报;z 分数考虑变异性但小样本下 `std` 不稳定。两者取「或」,宁可多标记后人工/规则复核,也不要漏检。
|
||
|
||
**标定流程**:
|
||
|
||
```text
|
||
前置实验(14 天,手工判定)
|
||
→ 记录首次偏离日 vs 主观察觉日
|
||
→ 计算不同阈值下的提前天数与误报数
|
||
→ 选择「提前 ≥ 3 天且误报可接受」的阈值组合
|
||
→ 写回本节,作为 P1 Trend Engine 初始配置
|
||
```
|
||
|
||
**阈值必须可配置**,不得硬编码在检测逻辑中 —— 后续需按真实数据持续调整。
|
||
|
||
---
|
||
|
||
## 16. 相关文档
|
||
|
||
- [memind-health-p0-experiment.md](memind-health-p0-experiment.md) — **P0 前置 14 天实验可执行方案**
|
||
- [ENGINEERING_WORKFLOW_RULES.md](../ENGINEERING_WORKFLOW_RULES.md)
|
||
- [PRODUCTION_RELEASE_RULES.md](../PRODUCTION_RELEASE_RULES.md)
|
||
- [page-data-api-usage.md](page-data-api-usage.md)
|
||
- [schedule-reminder-design.md](schedule-reminder-design.md)
|
||
- [memory-v2/README.md](memory-v2/README.md)
|
||
- [ai-mind-memind-integration-architecture.md](ai-mind-memind-integration-architecture.md)
|
||
- [regression-guards/vision-turn-read-image-isolation.md](regression-guards/vision-turn-read-image-isolation.md)
|
||
- [regression-guards/mindspace-seo-geo.md](regression-guards/mindspace-seo-geo.md)
|
||
- [regression-guards/README.md](regression-guards/README.md)
|