Files
kangkang/CLAUDE.md
link2026 37b47b2076 docs(claude): sync §5/§7/§10 with Monitor+Profile; fix SeriesBucket SwiftData import
- §5 schema 重写为 7 @Model 完整列表(含 UserProfile + Indicator.seriesKey)
- §7 IA 改成 5 槽 TabBar(2 内容 + 中间 + + 2 设置),记录入口 5 个 kind
- §10.6 红线例外清单加 Monitor + Profile(Symptom 也补上)
- SeriesBucket.swift 缺 import SwiftData(persistentModelID 报错)

全套测试 50 case pass / 0 fail / 0 warning。
2026-05-26 07:53:16 +08:00

291 lines
14 KiB
Markdown

# 康康 —— 工程前提
> 这是一个 6 周决赛 demo 项目。今天是 2026-05-25,处于 W1末/W2初。
> 任何 IDE/Claude 会话开始干活前,先读这份文件。
---
## 1. 产品定位
- **名字**:康康(对内代号 Kangkang)
- **形态**:iOS 原生 App,SwiftUI + SwiftData
- **核心卖点**:**100% 本地推理**的个人健康影像档案 + 大白话解读 + 本地 RAG 问答
- **目标用户**:不愿把体检/化验报告交给云端的普通人
- **明确不做**:医疗诊断、剂量推荐、急诊判断、医生预约、社交、广告、内购、数据上云、账号系统
---
## 2. 技术栈 / 选型(已锁定,不要再讨论)
| 项 | 选型 | 备注 |
|---|---|---|
| UI | SwiftUI | iOS 17+,用 `@Observable` / `@Model` |
| 持久化 | SwiftData | 见 §5 数据模型 |
| 图表 | Swift Charts | iOS 16+ 原生 |
| **AI 运行时** | **MLX Swift (Apple 官方)** | 不要建议 Core ML / llama.cpp / Ollama |
| LLM | Qwen3-1.7B 4bit (HF: `mlx-community/Qwen3-1.7B-4bit`) | ~1.0GB,负责文本生成、关键词抽取、趋势解读 |
| VL | Qwen2.5-VL-3B-Instruct 4bit (HF: `mlx-community/Qwen2.5-VL-3B-Instruct-4bit`) | ~2.0GB,负责拍照→结构化指标 |
| 文档扫描 | VisionKit `VNDocumentCameraView` | 不要自己写透视校正 |
| Face ID | LocalAuthentication | |
| Live Activity | ActivityKit + WidgetExtension | demo 杀手锏,真机才能测 |
**不引入**:任何云服务 SDK、任何 embedding 模型(RAG 用结构化检索,不用语义)、任何账号系统、任何分析 SDK。
---
## 3. AI 链路核心规则
### 3.1 模块边界(强制)
```
UI → CaptureService / AskService / TrendService → AIRuntime → MLX
Persistence
```
- **UI 永远不直接调 `AIRuntime`**。所有 AI 调用必须经过 `*Service` 层,这样 UI 可以注入 mock、可以预览。
- **`AIRuntime``actor` 单例,串行化**。同一时刻只允许一个推理任务,MLX 共享显存,并发会 OOM。CaptureService 拍照时如果 AskService 正在流式生成,要在队列里排队。
- **`*Service` 不直接读写 SwiftData 主上下文**。要么传入 `ModelContext`,要么走 ServiceLocator,方便测试。
### 3.2 VL pipeline(拍一张 = 一条流程)
**重要**:快拍(1.x) 和 报告归档(2.x) 已经合并成统一 `CaptureService`,UI 不再有 A1-A3 和 B1-B4 两条独立路径。流程:
```
拍照 → 写 Vault(加密目录) → VL 推理(要求输出 JSON,含 kind=single|report)
→ 解析容错(失败回退到手动录入,不卡死)
→ 单项走 A2ConfirmView,整份走 B3MetaView
→ 保存到 Indicator/Report + 关联 Asset
```
VL prompt 必须:
- 明确要求"只输出 JSON,不要解释"
- 带 2 个 few-shot 示例(单项 + 多项)
- 异常状态由 VL 模型基于参考范围直接判断,不要再二次调用 LLM
### 3.3 RAG(结构化检索,不做 embedding)
**两段式调用**:
1. 用 Qwen3-1.7B 抽取意图 + 关键词,输出 JSON `{indicators, time_range, intent}`,~50 token,<1s
2. SwiftData 按关键词检索 ≤ 10 条记录,拼 `ChatRAG` prompt,流式生成回答
**第 1 步失败时**回退到"近 30 天全表扫描",不卡死。
**引用回链**:回答中 `[1][2]` 后处理为可点击 Pill,点击跳源记录详情。
### 3.4 Live Activity
- VL 推理 / RAG 生成开始时启动 Activity
- 每 0.5s 通过 `AIRuntime.lastDecodeRate` 推送 tok/s
- 推理完成保留 2s 显示"已完成 · 0.8s"再 dismiss
- **只能真机测**,模拟器不显示。W5 末预留时间。
---
## 4. 模型分发
- 模型放 `Application Support/Models/`,首启动用 `URLSession.downloadTask` 拉,带断点续传 + 进度条
- 总体积 ~3GB,WiFi 提示必须有
- App 在模型未就绪时**仍可启动**,但所有 AI 入口显示"模型未就绪,前往下载"
- `ModelStore` 必须提供**旁路接口**:允许把模型预拷进沙盒(demo 现场重装时用)
---
## 5. 数据模型(SwiftData)
**当前 schema(2026-05-26)**:7 个 @Model
```swift
@Model class Indicator {
name, value, unit, range, statusRaw, note, capturedAt,
report: Report?, asset: Asset?,
pinned: Bool, // true,Trends
seriesKey: String? // "bp.systolic" / "glucose.fasting" / ... key
}
@Model class Report { title, typeRaw, reportDate, institution, note, summary, pageCount, createdAt,
indicators: [Indicator] cascade,
assets: [Asset] cascade }
@Model class DiaryEntry { content, createdAt, tags: [String] }
@Model class Symptom { name, startedAt, endedAt?, note?, severity 1-5, tags, createdAt }
@Model class Asset { relativePath, mimeType, bytes, createdAt }
@Model class ChatTurn { question, answer, referencedIndicatorIDs, referencedReportIDs, createdAt, decodeRate }
@Model class UserProfile { // App (UserProfileStore.loadOrCreate)
birthYear?, biologicalSexRaw, heightCM?, bloodTypeRaw,
allergies, chronicConditions, familyHistory, currentMedications,
updatedAt
}
```
**原图存储**: `Asset` 只存元数据 + 相对路径,真实 JPEG 落在 `Application Support/Vault/`,目录用 `.completeFileProtection`(iOS 硬件级加密,不要自己造 AES 轮子)。
---
## 6. 安全 / 隐私(已收敛 — 不要扩展)
| 做 | 不做 |
|---|---|
| `Application Support/Vault/` 全目录 `.completeFileProtection` | 自实现 AES 加密 |
| SwiftData store 文件 `.completeFileProtection` | |
| Face ID 启动锁(可选开关,默认关) | |
| 永久删除(SwiftData 硬删 + Asset 文件 unlink) | |
| 离线运行(自然结果,不用单独做) | |
| | 截屏黑屏防护(iOS 没有官方 API,不做) |
| | 加密 ZIP 导出 |
唯一的"导出"是 **9.4 分享文字摘要**(只分享解读文本,不带原图)。
---
## 7. 信息架构
```
TabBar: [主页] [记录] [+ 新建] [趋势] [我的]
│ │ │ │ │
│ │ │ │ └─ 个人资料 / 模型管理 / Face ID / 关于
│ │ │ └─ 折线图 + AI 一句话解读
│ │ └─ Sheet: 拍一张 / 指标记录 / 报告归档 / 写日记 / 症状
│ └─ ArchiveListView(时间线 + 分类 chip + 年/月分组)
└─ 问候 + 今日摘要 + 进行中症状 + 最近时间线
```
- TabBar **5 槽**:左 2 个内容 Tab + 中间 + 号 + 右 2 个 Tab
- "+ 新建" 是 sheet 不是 Tab
- AI 问答以 Modal Sheet 形式出现,**不占 Tab**
- 「指标记录」sheet 顶部 LazyVGrid 是 8 个 MonitorMetric 长期监测预设(进趋势),
下方 horizontal scroll 是化验项快捷预设(不进趋势),不选预设走自由输入
- 「我的 · 个人资料」是 NavigationLink push 的 Form 编辑页
### 7.1 档案库 C1 / C2 导航(看的一半)
录入流程(拍照→VL→编辑→存)只是"录的一半"。**"看的一半"由 C1 列表 + C2 详情承担**——这是 demo 的核心看点之一,不能砍。
```
首页 "我的报告档案" 卡 ──push──► C1 ArchiveListView
│ 分类 chip:全部/体检/化验/影像/处方
│ 按 reportDate 年份分组,卡片显示异常 chip
└──push──► C2 ReportDetailView
├─ Tab "原图":TabView(.page) 翻页 + 长按保存
├─ Tab "解读":数字摘要(总/高/低/正常)
│ + AI 整体摘要
│ + 对比上次(同类型上一份 Report diff)
└─ Tab "指标":Indicator 列表,异常优先
C2 底部两个动作:
├─ "关联到趋势" ──► 把本报告内未 pinned 的 Indicator 批量 pinned = true,Trends 默认展示
└─ "重新解读" ──► CaptureService.reanalyze(report:),重跑 VL 覆盖 summary/indicators
其他进入 C2 的入口:
• ChatTurn 引用 Pill 点击(referencedReportIDs)
• 趋势页数据点 tap → 跳到该点来源 Report 的 C2
• HomeView 时间线点报告类条目
```
### 7.2 对比上次("对比上次"=报告对比,已加回)
C2 解读 Tab 底部显示一段 diff 文本,**由 `ReportCompareService` 计算,不再调 LLM**:
- 找出"同 `typeRaw` 的上一份 Report"(`reportDate < current AND ORDER BY DESC LIMIT 1`)
- 同名 `Indicator` 配对,数值 diff:`Δ` 绝对值 + 百分比 + 升/降箭头
- 标红:跨越参考范围边界(原本正常→偏高,或反过来)
- 文案模板拼装,不走 LLM,响应即时
- 若无上一份,该区块隐藏
---
## 8. 现有代码状态(2026-05-25)
```
康康/
├── App/KangkangApp.swift ✅ SwiftData container 已建
├── RootView.swift ✅ 3 Tab + RecordSheet 已建
├── Models/Models.swift ✅ Indicator / Report / DiaryEntry,缺 Asset / ChatTurn
├── DesignSystem/ ✅ Tokens + Components,沿用
└── Features/
├── Home/ ✅ HomeView 静态 UI,数据未接
├── Quick/A1-A3 🔧 待合并进 UnifiedCaptureFlow
├── Archive/B1-B4 🔧 B1 砍,B2 改用 VisionKit DocumentCamera
├── Record/RecordSheet ✅ 入口选择 UI
├── Trends/ ❌ 只有 placeholder
└── Me/ ❌ 只有 placeholder
待建:
├── AI/ ⚠️ AIRuntime + LLMSession + ModelStore + TokenChunk ✅;VLSession + Prompts/ ❌
├── Debug/DebugAIRunner.swift ✅ DEBUG-only AI 自检入口
├── Services/ ❌ CaptureService, AskService, TrendService, ReportCompareService
├── Persistence/FileVault.swift ✅ 原图加密目录管理
├── Security/AppLock.swift ❌ Face ID 启动锁
├── Features/Ask/ ❌ AskSheet (RAG 问答 UI)
├── Features/Archive/
│ ├── ArchiveListView ❌ C1 档案列表(分类 chip + 年份分组)
│ └── ReportDetailView ❌ C2 报告详情(三 Tab:原图/解读/指标 + 对比上次)
├── Features/Capture/
│ └── UnifiedCaptureFlow ❌ 替代 QuickCaptureFlow,状态机驱动 A1→VL→A2/B3
├── Features/Onboarding/ ❌ 首启动隐私承诺 + 模型下载
└── LiveActivity/ ❌ WidgetExtension target
```
---
## 9. 设计系统约束
- **不要新增颜色 token**。所有颜色走 `Tj.Palette.*` (sand / paper / ink / brick / leaf / line / text / text3)
- **不要新增字体大小**。走 `Font.tjTitle()` / `tjH2()` / `tjSerifBody()` / 系统 size
- **圆角走 `Tj.Radius.*`**,卡片走 `.tjCard()` modifier
- 按钮走 `TjPrimaryButton` / `TjGhostButton`
新加 View 时先看 `DesignSystem/Components.swift`,有现成的不要复刻。
---
## 10. 不能跨越的红线
写代码前必读:
1. **不引入云服务**——任何 SDK 都不行,包括崩溃上报、分析、灰度
2. **不自己实现密码学**——`.completeFileProtection` 已经够
3. **UI 不直接调 AIRuntime**——必须经过 Service
4. **AIRuntime 必须 actor 化**——禁止 class + lock
5. **VL/LLM prompt 必须有 few-shot + 失败回退**——不能让用户卡在 AI 错误屏
6. **新功能必须问"清单里有吗"**——清单外的功能(用药提醒、多 profile、暗黑模式、iCloud 同步……)默认不做,要做必须先讨论。**已加回的例外**:报告对比(16.1,§7.2)、症状追踪(Symptom @Model)、长期监测指标(MonitorMetric / IndicatorQuickSheet,W2)、个人资料(UserProfile,W2)
7. **不要在 6 周里重构现有 Tab/RecordSheet 骨架**——增量加东西,不要推倒重来
8. **报告详情(C2)与归档元信息编辑(B3)是两个 View**——B3 是 draft 编辑(写),C2 是 detail 浏览(读),不要合并复用主框架
---
## 11. 6 周时间表
| 周次 | 必交付 |
|---|---|
| W1 末 / W2 当前 | 项目结构、MLX 跑通 Qwen3-1.7B、首个 token 在设备吐出 |
| W2-W3 | AIRuntime + LLMSession,文字日记 + 基础 RAG 问答(打字机效果)(W2 进行中) |
| W3-W4 | VLSession + 统一拍照流程(单项 + 整份)、Asset / FileVault |
| W4 末 | **C1 ArchiveListView**(分类 chip + 年份分组,接 @Query) |
| W4-W5 | 趋势(Swift Charts + AI 解读)、**C2 ReportDetailView**(三 Tab + 重新解读) |
| W5 中 | **ReportCompareService** + C2 解读 Tab "对比上次" 区块 |
| W5 末 | Face ID、永久删除、首页时间线接入真数据、Live Activity(真机) |
| W6 | 模型管理页、首启动下载流程、UI polish、demo 视频、PPT |
**P0 新增项**(从 C 区视觉稿补回):C1 档案列表、C2 报告详情三 Tab、对比上次、关联到趋势、重新解读
**P1**(必须做完):Live Activity、分享文字摘要(9.4)、首启动隐私承诺页
**P2**(余力做):—— 任何 P2/P3 都暂时不做,清单里 11/12/13/14/15/17/18/19 全部 deferred(注意:16.1 报告对比已升 P0)
**砍 P1 决策顺序**(任何一周延期触发):Live Activity → Onboarding 简化 → 分享摘要 → 模型管理页 polish。**绝不动 C1/C2/对比上次**——视觉稿都做了,demo PPT 也要展示,这是核心卖点之一。
---
## 12. 评委 PPT 卖点排序(写代码时记住为什么这么做)
1. 影像档案系统(统一 VL 拍照 + 归档) — 核心创意
2. 100% 本地 + SME2 加速 — 技术亮点
3. 本地 RAG 长期记忆 — 端侧不可替代性
4. 隐私三件套(系统级加密 + Face ID + 永久删除) — 信任建立
5. AI 趋势解读 — 长期价值
6. Live Activity 实时 tok/s — 现场记忆点
每写一个功能,问自己:这条提升了上面哪一项?如果都没有,就别做。