diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..50043ad --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,296 @@ +# 康记 / 体己 —— 工程前提 + +> 这是一个 6 周决赛 demo 项目。今天是 2026-05-25,处于 W1末/W2初。 +> 任何 IDE/Claude 会话开始干活前,先读这份文件。 + +--- + +## 1. 产品定位 + +- **名字**:康记(对内代号 体己 / Tiji) +- **形态**: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 (MLX 4bit 量化) | ~1.0GB,负责文本生成、关键词抽取、趋势解读 | +| VL | Qwen2.5-VL-3B (MLX 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) + +现有 3 个 `@Model`,要新增 2 个: + +```swift +// 已有(在 Models/Models.swift) +@Model class Indicator { name, value, unit, range, statusRaw, note, capturedAt } +@Model class Report { title, typeRaw, reportDate, institution, note, summary, pageCount, createdAt } +@Model class DiaryEntry { content, createdAt } + +// 待加字段 +// Indicator + report: Report? 反向关系 +// Indicator + asset: Asset? 关联原图 +// Indicator + pinned: Bool C2 "关联到趋势" 后置 true,Trends 默认展示 pinned 指标 +// Report + indicators: [Indicator] @Relationship cascade +// Report + assets: [Asset] @Relationship cascade +// DiaryEntry + tags: [String] VL/LLM 抽取的标签 + +// 待加 @Model +@Model class Asset { + var relativePath: String // 相对 Vault/ 的路径 + var mimeType: String + var bytes: Int + var createdAt: Date +} + +@Model class ChatTurn { + var question: String + var answer: String + var referencedIndicatorIDs: [String] + var referencedReportIDs: [String] + var createdAt: Date + var decodeRate: Double // 该轮问答推理速度,Me 页性能展示 +} +``` + +**原图存储**: `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 一句话解读 + │ └─ Modal: 选择 拍一张 / 写日记 / 问问看 + └─ 问候 + 今日摘要 + 时间线 + 影像档案入口 +``` + +- **3 Tab 不变**,中间 + 号是 Sheet +- AI 问答以 Modal Sheet 形式出现,**不占 Tab** +- "问问看"入口除了在 RecordSheet 里,首页摘要卡片下方也有一个常驻入口 +- 历史时间线在首页下半部分,不单独开 Tab + +### 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/TijiApp.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, VLSession, Prompts/ +├── 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 +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 问答(打字机效果) | +| 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 — 现场记忆点 + +每写一个功能,问自己:这条提升了上面哪一项?如果都没有,就别做。 diff --git a/docs/superpowers/specs/2026-05-25-kangji-features-design.md b/docs/superpowers/specs/2026-05-25-kangji-features-design.md new file mode 100644 index 0000000..c2764d6 --- /dev/null +++ b/docs/superpowers/specs/2026-05-25-kangji-features-design.md @@ -0,0 +1,641 @@ +# 康记 / 体己 —— 功能设计 Spec(v1.0) + +**日期**:2026-05-25 +**状态**:Draft, 已与产品方对齐 §1-§6 +**关联**:[CLAUDE.md](../../../CLAUDE.md) 工程前提 + +--- + +## 0. 概要 + +康记是一个 iOS 原生健康影像档案 App,**100% 端侧 AI 推理**,基于 SwiftUI + SwiftData + MLX Swift,目标 6 周交付决赛 demo。本 spec 把原始功能清单收敛为 **方案 B**:核心 5 模块 + Live Activity + 分享摘要,其余 P2/P3 全部 deferred。 + +**5 大核心模块** + +1. 统一拍照(合并原 1.x 异常项快拍 + 2.x 关键报告归档) +2. 文字日记 +3. 本地 RAG 问答(结构化检索) +4. 趋势分析(Swift Charts + AI 一句话解读) +5. 影像档案库 C1 列表 + C2 详情(看的一半) + +**附加 P1**:Live Activity(demo 杀手锏)、分享文字摘要、首启动 Onboarding、Face ID + 永久删除 + +--- + +## 1. 整体架构 + +### 1.1 模块分层 + +``` +UI 层 (SwiftUI) + │ + ├─ Tab: Home / Trend / Me + ├─ Modal Sheet: RecordSheet → UnifiedCaptureFlow / DiaryComposer / AskSheet + └─ Navigation: ArchiveListView → ReportDetailView + │ + ▼ +*Service 层 + ├─ CaptureService // 拍照 → VL → draft → commit + ├─ AskService // 两段式 RAG + ├─ TrendService // 聚合 + AI 解读 + └─ ReportCompareService // 同类型上一份报告 diff(纯模板,不调 LLM) + │ + ▼ +AIRuntime (actor 单例) + ├─ LLMSession (Qwen3-1.7B, 流式) + └─ VLSession (Qwen2.5-VL-3B, 单次) + │ + ▼ +Persistence + ├─ SwiftData (Indicator / Report / DiaryEntry / Asset / ChatTurn) + └─ FileVault (Application Support/Vault/, .completeFileProtection) +``` + +### 1.2 模块职责与边界 + +| 模块 | 干什么 | 不干什么 | +|---|---|---| +| **UI** | View + ViewModel,组织展示 | 不直接调 MLX,不读写文件 | +| **CaptureService** | 拍一张全流程:`UIImage → VL prompt → 结构化 JSON → 存盘 + Indicator/Report` | 不管 UI 状态机 | +| **AskService** | RAG:`query → SwiftData 关键词检索 → 拼 prompt → 流式输出 → 引用回链` | 不管对话历史展示样式 | +| **TrendService** | 按指标名/时间范围聚合数据,生成 Chart 数据点 + LLM 一句话解读 | 不画图表 | +| **ReportCompareService** | 找上一份同类型 Report,逐指标 diff,模板拼装 | 不调 LLM | +| **AIRuntime** | MLX 模型加载/卸载/推理,actor 单例,串行化推理 | 不懂业务 | +| **Persistence** | SwiftData 持久化 + FileVault 原图加密目录 + 永久删除 | 不懂 AI | + +### 1.3 强制规则(违反即反 spec) + +- **UI 永远不直接调 `AIRuntime`**——必须经过 Service 中转,这样 UI 可注入 mock、可预览 +- **`AIRuntime` 必须 actor**——同一时刻只允许一个推理任务,MLX 共享显存,并发会 OOM +- **`*Service` 不直接读写 SwiftData 主上下文**——传入 `ModelContext` 或走 ServiceLocator +- **B3 写,C2 读,不要合并主框架**——B3 是 draft 编辑(写入路径),C2 是 detail 浏览(读取路径) + +--- + +## 2. AI 链路 + +### 2.1 `AIRuntime` 接口 + +``` +体己/AI/ +├── AIRuntime.swift // actor 单例,推理串行化 +├── ModelStore.swift // 模型路径管理 + 下载 + bundle 旁路 +├── LLMSession.swift // Qwen3-1.7B 文本生成,流式 +├── VLSession.swift // Qwen2.5-VL-3B 图像理解,单次 +└── Prompts/ + ├── VLExtraction.swift // 拍照解析(含 few-shot) + ├── KeywordExtraction.swift // RAG 第一步 + ├── ChatRAG.swift // RAG 第二步 + └── TrendNarrative.swift // 趋势一句话解读 +``` + +```swift +actor AIRuntime { + static let shared = AIRuntime() + + enum Status { case notReady, downloading(Double), loading, ready, error(String) } + private(set) var status: Status = .notReady + + func prepare() async throws + func generate(prompt: String, maxTokens: Int) -> AsyncThrowingStream + func analyze(image: UIImage, instruction: String) async throws -> String + var lastDecodeRate: Double { get } +} + +struct TokenChunk { + let text: String + let decodeRate: Double +} +``` + +### 2.2 模型分发 + +| 项 | 决策 | +|---|---| +| 模型来源 | HuggingFace MLX 社区版 Qwen3-1.7B-MLX-4bit + Qwen2.5-VL-3B-MLX-4bit | +| 体积 | LLM ~1.0GB + VL ~2.0GB ≈ 3GB | +| 存储 | `Application Support/Models/`,`URLSession.downloadTask` + 断点续传 | +| 首启动 | 启动屏 → 隐私承诺 → "下载模型"页(进度 + WiFi 提示) → 主界面 | +| 旁路 | `ModelStore.seedModelsFromBundle()`,demo 现场预装机用 | +| 模型缺失 | App 可启动,AI 入口显示"模型未就绪,前往下载" | + +### 2.3 VL Pipeline:拍一张统一流程 + +``` +拍照 (UIImage) + │ + ▼ Step 1 预处理 (CaptureService) + - 缩放到 ≤ 1024 长边 + - 写 Vault 作为 Asset(加密) + │ + ▼ Step 2 VL 解析 (AIRuntime.analyze) + - VLExtraction prompt 要求输出 JSON + - 期望 { kind: "single"|"report", items: [...], summary: "..." } + │ + ▼ Step 3 解析容错 + - JSONDecoder + 宽松正则兜底 + - 失败 → A2/B3 字段为空,提示"识别不完整,请手动补充" + │ + ▼ Step 4 确认页 + - kind=single → A2ConfirmView(单项 + 一句话) + - kind=report → B3MetaView(指标列表 + 整体摘要,异常优先) + - 所有字段可编辑 + │ + ▼ Step 5 保存 + - draft → commit,事务写入 Indicator + Report + Asset +``` + +**VL prompt 要点** +- 明确"只输出 JSON,不要解释" +- 带 2 个 few-shot 示例(单项 + 多项) +- 异常状态由 VL 基于参考范围直接判断 +- **不能让用户卡在错误屏**——失败回退到手动录入 + +### 2.4 RAG:结构化检索 + LLM 生成(不做 embedding) + +``` +用户问句 + │ + ▼ Step 1 关键词抽取 (Qwen3-1.7B, ~50 token, <1s) + { indicators: [...], time_range: {days: 90}, intent: "trend"|"value"|"diary" } + │ + ▼ Step 2 SwiftData 检索 (AskService) + - 按 indicators 模糊匹配 Indicator.name + - 按 time_range 过滤 capturedAt + - intent=diary 时检索 DiaryEntry.content + - 返回 ≤ 10 条 + 引用 ID + │ + ▼ Step 3 拼 ChatRAG prompt + - System: 不诊断、不建议就医、只描述数据 + - Context: 检索结果, 每条编号 [1]-[n] + - User: 原始问句 + │ + ▼ Step 4 流式生成 (LLMSession.generate) + - UI 打字机效果 + - 顶部小字: "本机推理中 · 24.3 tok/s" + │ + ▼ Step 5 引用回链 + - [1][2] 后处理为可点击 Pill + - 点击 → 跳 Indicator/Report C2 详情 + - 整轮存 ChatTurn(referencedIDs + decodeRate) +``` + +**Step 1 失败回退**:近 30 天全表扫描,不卡死。 +**Step 5 跨页跳转**:Report 引用走 ReportDetailView,Indicator 引用走 Trends 高亮该点。 + +### 2.5 Live Activity + +| 项 | 决策 | +|---|---| +| 触发 | VL 推理 / RAG 生成开始 | +| 显示 | "Qwen2.5-VL · 24.3 tok/s" / "本机问答中 · 22.1 tok/s" | +| 实现 | ActivityKit + WidgetExtension target,`AIRuntime.lastDecodeRate` 每 0.5s `Activity.update` | +| 结束 | 完成后保留 2s "已完成 · 0.8s" 再 dismiss | +| 真机限定 | 模拟器不支持,W5 末预留 1d 真机调试 | + +--- + +## 3. 数据流与数据模型 + +### 3.1 SwiftData @Model 全集 + +```swift +@Model final class Indicator { + var name: String + var value: String // 字符串,允许 ">10" "阴性" 等非数值 + var unit: String + var range: String + var statusRaw: String // "high" | "low" | "normal" + var note: String? + var capturedAt: Date + + var report: Report? // 反向关系,nil 表示单项快拍 + var asset: Asset? + var pinned: Bool = false // C2 "关联到趋势" 置 true +} + +@Model final class Report { + var title: String + var typeRaw: String + var reportDate: Date + var institution: String? + var note: String? + var summary: String? // VL 100 字摘要 + var pageCount: Int + var createdAt: Date + + @Relationship(deleteRule: .cascade, inverse: \Indicator.report) + var indicators: [Indicator] = [] + @Relationship(deleteRule: .cascade) + var assets: [Asset] = [] +} + +@Model final class DiaryEntry { + var content: String + var createdAt: Date + var tags: [String] = [] // VL/LLM 抽取 +} + +@Model final class Asset { + var relativePath: String + var mimeType: String + var bytes: Int + var createdAt: Date +} + +@Model final class ChatTurn { + var question: String + var answer: String + var referencedIndicatorIDs: [String] + var referencedReportIDs: [String] + var createdAt: Date + var decodeRate: Double +} +``` + +### 3.2 SwiftData migration + +- **W2-W4**:破坏性迁移,改 schema 就删模拟器沙盒。不要写 `VersionedSchema` +- **W5 起**:冻结 schema。需要改 → 写 `SchemaMigrationPlan`,只允许加字段 +- **demo 翻车**:启动检测不兼容 → 弹"数据格式更新,需重建" → 删库重启。**绝不静默丢数据** + +### 3.3 FileVault + +```swift +final class FileVault { + static let shared = FileVault() + private let vaultURL: URL // Application Support/Vault/ + + func writeJPEG(_ image: UIImage, quality: CGFloat = 0.85) throws -> (relativePath: String, bytes: Int) + func loadImage(relativePath: String) throws -> UIImage + func remove(relativePath: String) throws + func wipe() throws +} +``` + +- 目录 `.completeFileProtection`,iOS 硬件级加密 +- 不做版本管理、不做去重、不做缩略图缓存(YAGNI) +- 屏幕锁定时文件不可读 → AI 入口本来就由 Face ID 拦,业务上不冲突 + +### 3.4 拍照→保存时序 + +``` +User → UI(B2Scan) → CaptureService → AIRuntime → Persistence + │ openCamera + │ ◄── images[] + │ analyze(images) + │ writeJPEG × N + │ ─────────────────────────► Assets stored + │ analyze(image1) ──► MLX VL + │ ◄── JSON + │ (repeat for img2…) + │ ◄── draft (Report + Indicators in-memory) + │ edit + save + │ commit ──────────────────► Report + Indicators + │ ◄── reportID +``` + +- N 页 VL 推理**串行**(AIRuntime 串行化),UI 显示 "第 k/N 页解析中" + 取消按钮 +- draft 阶段 SwiftData 未提交,用户取消则 Asset 一并回滚 + +--- + +## 4. UI 改造与新建清单 + +### 4.1 现有视图改造 + +| 文件 | 改造 | 工作量 | +|---|---|---| +| `RootView` | RecordSheet 加"问问看";首页常驻"问问看"入口 | 0.5d | +| `HomeView` | `@Query` 接真数据,时间线 5 条,"今日摘要"接 `TrendService.dailyDigest()`,影像档案入口数量 | 1.5d | +| `Quick/A1Viewfinder` | 改用 `VNDocumentCameraView` 单张模式 | 0.5d | +| `Quick/A2Confirm` | 接 Indicator draft,字段可编辑,显示一句话解读 | 1d | +| `Quick/A3Batch` | **复用为整份报告指标列表**,异常优先 | 1d | +| `Quick/QuickCaptureFlow` | 改为 `UnifiedCaptureFlow`,kind 分叉 | 1d | +| `Archive/B1Guide` | **砍** | -0.5d | +| `Archive/B2Scan` | 改用 VNDocumentCameraView 多页 | 0.5d | +| `Archive/B3Meta` | 接 Report draft + AI 摘要 + 共用 A3 指标列表 | 1d | +| `Archive/B4Progress` | "第 k/N 页"+ 取消,绑 AIRuntime 队列 | 1d | +| `Trends/TrendsView` | 全新(见 4.2) | 3d | +| `Me/MeView` | 模型管理 + Face ID + 永久删除 + 关于 | 2d | +| `Record/RecordSheet` | 四入口 | 0.5d | +| `Models/Models.swift` | 加 Asset / ChatTurn + 字段 | 0.5d | + +### 4.2 新建视图 + +#### `Features/Capture/UnifiedCaptureFlow.swift`(P0,1d) +状态机 View,根据 `CaptureService` 状态切换 Camera → Progress → A2 / B3。 + +#### `Features/Ask/AskSheet.swift`(P0,2d) +- Modal sheet, fraction(0.9) +- 顶部:3 个动态推荐问题 chip +- 中部:对话流(`ChatTurn` 气泡) +- AI 流式打字机 + 顶部小字 tok/s +- 引用 `[1][2]` → Pill → 跳源 +- 底部输入框 + 发送 +- **不做**:多轮上下文连续追问(每问独立 RAG) + +#### `Features/Trends/TrendsView.swift`(P0,3d) +- 顶部横向 chip:指标(从 pinned + 高频 Indicator.name 去重) +- 时间范围 segmented:3M / 6M / 1Y / 全部 +- Swift `Chart`:折线 + 参考范围 `RuleMark` 条带 + 异常点高亮 +- 点 tap → C2 详情 +- 下方 AI 解读卡片(`TrendNarrative`) + +#### `Features/Archive/ArchiveListView.swift`(P0,2d)—— C1 +- `@Query` 全部 Report,按 `reportDate` 年份分组 +- 顶部分类 chip:全部 / 体检报告 / 化验单 / 影像报告 / 处方 +- 卡片:左缩略图 + 右(标题 + 异常 chip + 日期 + 机构) +- 入口:HomeView "我的报告档案" 卡 → push + +#### `Features/Archive/ReportDetailView.swift`(P0,2d)—— C2 +- 三 Tab Picker:原图 / 解读 / 指标 +- **原图 Tab**:`TabView(.page)` 翻页,点击放大,长按保存到相册(需相册权限) +- **解读 Tab**:数字摘要(`indicators.count` / 偏高 / 偏低 / 正常)+ `Report.summary` + **对比上次区块** +- **指标 Tab**:`Report.indicators`,异常优先 +- 底部两按钮: + - **关联到趋势** → 本报告 `Indicator.pinned = true` 批量更新 + - **重新解读** → `CaptureService.reanalyze(report:)` 重跑 VL +- 其他入口:ChatTurn 引用 / Trends 数据点 / Home 时间线 + +#### `Features/Me/MeView.swift`(P1,2d) +1. 模型状态:Qwen3-1.7B / Qwen2.5-VL-3B 状态、占用空间、上次速度 +2. 隐私:Face ID Toggle(`@AppStorage`)、永久删除(二次确认) +3. 数据:导出文字摘要(`UIActivityViewController`) +4. 关于:版本 + 模型许可证 + +#### `Features/Onboarding/OnboardingFlow.swift`(P1,2d) +3 页:隐私承诺 → 模型下载(进度 + WiFi 提示) → 完成。`@AppStorage("onboarded")` 记录。 + +#### `LiveActivity/CaptureActivity.swift` + WidgetExtension target(P1,2d) +- `CaptureActivityAttributes`: modelName / status / decodeRate / elapsedMs +- 锁屏 + 灵动岛 compact/expanded +- AIRuntime 推理时 start/update/end +- 真机限定 + +### 4.3 服务层文件 + +``` +体己/AI/ [7.5d] +├── AIRuntime.swift 2d +├── ModelStore.swift 1d +├── LLMSession.swift 1d +├── VLSession.swift 1d +└── Prompts/ 2.5d + ├── VLExtraction.swift + ├── ChatRAG.swift + ├── KeywordExtraction.swift + └── TrendNarrative.swift + +体己/Services/ [4.5d] +├── CaptureService.swift 1.5d +├── AskService.swift 1.5d +├── TrendService.swift 1d +└── ReportCompareService.swift 0.5d + +体己/Persistence/ [1d] +├── FileVault.swift 0.5d +└── PermanentDelete.swift 0.5d + +体己/Security/ [0.5d] +└── AppLock.swift 0.5d +``` + +### 4.4 工作量 + +| 类别 | 估算 | +|---|---| +| 现有改造 | ~13d | +| 新建视图(含 C1/C2 +4d) | ~16d | +| AI 层 | ~7.5d | +| Services 层(含 ReportCompare +0.5d) | ~4.5d | +| Persistence / Security | ~1.5d | +| **纯开发** | **~42.5d** | +| 联调 / Bug / polish | ~7.5d 缓冲 | +| **总计** | **~50d ≈ 6 周** | + +任何一项延期 > 1d,按 §6.R7 砍 P1 不动 P0。 + +--- + +## 5. 6 周时间表 + +### W2(本周,5/25-5/31)—— AI 跑通 + Schema 重建 +- MLX SPM 引入 + Xcode target +- `AIRuntime` actor + `prepare()` + `generate()` +- `LLMSession` 跑通 Qwen3-1.7B 加载 + 文本生成 +- `ModelStore` 路径管理 + bundle 旁路(下载延后到 W6) +- `Models/Models.swift` 新增字段 + Asset / ChatTurn +- 删模拟器沙盒确认 Schema 重建 +- `FileVault` 写/读/删测试图 + +**里程碑**:debug 按钮点击控制台打印 LLM 流式输出 + 速度 +**验收**:decode ≥ 15 tok/s,无 OOM +**红线**:跑不通周五前换 llama.cpp + +### W3(6/1-6/7)—— 日记 + 基础 RAG +- `DiaryComposer` 接 RecordSheet +- `AskSheet` 打字机 UI + 流式 +- `AskService` 两段式 RAG:`KeywordExtraction` → SwiftData → `ChatRAG` +- 引用 `[1][2]` → Pill(目标视图可 stub) +- `ChatTurn` 持久化 + +**里程碑**:写 3 条日记 → 问"我最近写了什么" → 带引用回答 +**验收**:首 token < 2s,引用 ID 匹配 +**红线**:JSON 抽取失败率 > 30% → 加 few-shot 或换 prompt + +### W4(6/8-6/14)—— VL + 统一拍照 + C1 +- `VLSession` 跑通 Qwen2.5-VL 加载 + 单图推理 +- `VLExtraction` prompt(few-shot + JSON) +- `CaptureService.analyze` → draft → commit +- `UnifiedCaptureFlow` 状态机 +- A1/B2 改 `VNDocumentCameraView` +- A2/A3/B3/B4 接真数据 +- **W4 末:`ArchiveListView` C1**(分类 + 年份分组) + +**里程碑**:三张真化验单 → 70% 字段自动填好;档案列表可见所有报告 +**验收**:VL 单页 < 8s,JSON 失败有可见兜底 +**红线**:VL > 15s 或失败率 > 40% → 降级 Vision OCR + Qwen3 文本后处理 + +### W5(6/15-6/21)—— C2 + Trends + 隐私 + Live Activity +- **`ReportDetailView` C2** 三 Tab + 关联到趋势 + 重新解读 +- **`ReportCompareService`** + C2 解读 Tab "对比上次" +- `TrendService` + `TrendsView`(Swift Charts + AI 解读) +- `HomeView` 接 `@Query` 真数据 + 时间线 5 条 +- `TrendService.dailyDigest()` 接首页摘要卡 +- `AppLock` Face ID 启动锁 +- `PermanentDelete` 接 Me 页 +- WidgetExtension target + `CaptureActivity` +- **真机调试 Live Activity** + +**里程碑**:C1 → C2 三 Tab → 对比上次 → 关联到趋势 → Trends 上看到新指标;拍照灵动岛滚 tok/s +**验收**:Charts ≥ 3 个指标各 ≥ 6 点;Live Activity 锁屏可见 +**红线**:Live Activity 周三前调不通 → 降级 App 内顶部条 + +### W6(6/22-6/28)—— 首启动 + Me + Polish + Demo +- `OnboardingFlow`(隐私承诺 + 模型下载 + 完成) +- `ModelStore` 真实 URLSession 下载 + 续传 + 进度 +- `MeView`(模型状态 + 隐私 + 关于) +- 9.4 分享文字摘要 +- 所有空状态插画 / 文案 +- `#if DEBUG` 种子数据(12 份报告) +- 真机录 3 分钟 demo 视频(含 Live Activity) +- PPT 5 页核心(按卖点排序) + +**里程碑**:零安装到首问答 < 5 分钟;demo 视频成片 +**验收**:评委 iPhone 跑得动(提前预拷模型) +**红线**:W6 不再加新功能 + +### 每周日 retro 决策树 + +``` +本周 P0 全完成? +├─ 是 → 进下周 +└─ 否,延期 > 2d + → 砍下周 1 项 P1 + 砍顺序:Live Activity → Onboarding 简化 → 分享摘要 → Me polish + 绝不动:C1 / C2 / 对比上次 / 统一拍照 / AskSheet / Trends / Face ID +``` + +--- + +## 6. 风险与回退预案 + +### R1 · MLX 跑不通 / 速度太慢 🔴 致命 +**信号**:W2 周五,decode < 10 tok/s 或首 token > 5s,OOM +**回退**:① 更小量化 → ② llama.cpp + GGUF(失 SME2 卖点)→ ③ Qwen2.5-0.5B +**预防**:W2 第一天就跑通;iPhone 15 Pro+ 基线 + +### R2 · VL 准确率不够 🔴 致命 +**信号**:W4 中,5 张真化验单 < 60% 字段正确;频繁残缺 JSON +**回退**:① 加强 few-shot 到 4-5 个 → ② 降级 Vision OCR + Qwen3 文本后处理(失"VL"故事但保识别)→ ③ Demo 只用已知能解析的图 +**预防**:W3 末准备 5-10 张真单做回归集;A2/B3 必备兜底文案 + +### R3 · Live Activity 真机调不通 🟠 高 +**信号**:W5 末,Capability/证书问题,模拟器没法测 +**回退**:① 只保锁屏不要 App 内顶部条 → ② 整个砍,改 App 内顶部 `safeAreaInset` 粘性条 → ③ Demo 视频后期加字幕 +**预防**:W5 周一建好 target + +### R4 · SwiftData migration 翻车 🟠 高 +**信号**:Schema 编过但启动崩 / cascade 误删 +**回退**:W5 前删沙盒;W5 后 `VersionedSchema` 只加字段;demo 翻车显式弹窗"重建",绝不静默丢数据 +**预防**:每次改 @Model 重启验证;Schema 改动 W2-W4 集中,W5 冻结 + +### R5 · 3GB 下载体验灾难 🟠 高 +**信号**:demo 现场装包后下载 30 分钟还没好,中断不能续 +**回退**:① demo 用预拷模型设备 + `seedModelsFromBundle()` → ② 分两步下(先 LLM 后 VL)→ ③ 视频备份 +**预防**:W6 在 2 台 demo 机断网测试 + +### R6 · 健康话术翻车 🟡 中 +**信号**:LLM 输出"你应该……""建议就医" +**回退**:System prompt 末尾追加禁令;AskService 后处理过滤诊断关键词;启动屏永久免责小字 +**预防**:W3 RAG 跑通后立刻做 20 条危险问题安全测试,写进单元测试 + +### R7 · 时间不够 🟡 中 +**信号**:某周日 P0 还有 > 1d 没完成 +**回退顺序**:Live Activity → Onboarding 简化 → 分享摘要 → Me polish +**绝不砍**:C1/C2、对比上次、统一拍照、AskSheet、Trends、Face ID +**预防**:每周日 retro + +### R8 · 评委 iPhone 不支持 SME2 🟡 中 +**信号**:iPhone 14 非 Pro 装包,速度比预期慢 +**回退**:不依赖评委设备,自带 iPhone 15 Pro / 16 Pro 演示 +**预防**:W6 准备 2 台 A17/A18 demo 机 + +### 风险红绿灯仪表盘 + +| 风险 | W2 | W3 | W4 | W5 | W6 | +|---|---|---|---|---|---| +| R1 MLX | 🔴 | 🟡 | 🟢 | 🟢 | 🟢 | +| R2 VL | — | — | 🔴 | 🟡 | 🟢 | +| R3 Live Activity | — | — | — | 🔴 | 🟡 | +| R4 Migration | 🟡 | 🟡 | 🟡 | 🟠 冻结 | 🟢 | +| R5 模型下载 | — | — | — | 🟡 | 🔴 | +| R6 医疗话术 | — | 🟡 | 🟡 | 🟡 | 🟢 | +| R7 P0 进度 | 🟡 | 🟡 | 🟠 | 🔴 | 🟡 | +| R8 demo 设备 | — | — | — | — | 🔴 | + +每周日 retro 显式确认当周"关键"项:**通过 / 需补救 / 触发回退**。 + +--- + +## 7. 非目标(明确不做) + +写代码时如果想加,先回这里看一眼: + +- ❌ 医疗诊断、剂量推荐、急诊判断、医生预约 +- ❌ 连拍模式(1.6) +- ❌ AES 自实现 / 截屏黑屏防护(走 iOS 系统级) +- ❌ 加密 ZIP 导出(9.1-9.3)——只留 9.4 文字摘要 +- ❌ 多页 PDF 自写透视校正(VisionKit 白送) +- ❌ 用药提醒、复检提醒、体检周年提醒(11.x) +- ❌ 语音输入(12.x) +- ❌ 多指标相关性、聚类(13.x) +- ❌ 暗黑模式、主题色切换(14.x) +- ❌ 多 profile / 家庭成员(15.x) +- ❌ AI 模型升级 in-app(17.x) +- ❌ iCloud 同步(18.x,与"100% 本地"冲突) +- ❌ Widget / Home Screen 小组件(19.x;Live Activity 不算) +- ❌ 社交、广告、内购、账号系统 + +**例外加回**:报告对比 16.1(P0,已加回,见 §4.2 C2 + §5 W5) + +--- + +## 8. 评委 PPT 卖点排序 + +写每个功能时记住为什么这么做: + +1. **影像档案系统**(统一 VL 拍照 + C1/C2 归档库)— 核心创意 +2. **100% 本地 + SME2 加速** — 技术亮点 +3. **本地 RAG 长期记忆** — 端侧不可替代性 +4. **隐私三件套**(系统级 file protection + Face ID + 永久删除)— 信任建立 +5. **AI 趋势解读 + 对比上次** — 长期价值 +6. **Live Activity 实时 tok/s** — 现场记忆点 + +每写一个功能,问自己:这条提升了上面哪一项?都没有就别做。 + +--- + +## 附录 · 与原始功能清单的映射 + +| 原编号 | 状态 | 落地位置 | +|---|---|---| +| 1.x 异常项快拍 | **合并** | UnifiedCaptureFlow(kind=single) | +| 1.2 智能取景框 | **砍** | VNDocumentCameraView 取代 | +| 1.6 连拍模式 | **砍** | YAGNI | +| 2.x 关键报告归档 | **合并** | UnifiedCaptureFlow(kind=report) | +| 2.1 多页扫描 | 改造 | VNDocumentCameraView 多页 | +| 2.6 原图加密 | 改造 | FileVault `.completeFileProtection` | +| 3.x 自然语言日记 | P0 | DiaryComposer + DiaryEntry | +| 4.x AI 问答 | P0 | AskSheet + AskService(结构化 RAG) | +| 4.4 流式输出 | P0 | LLMSession + AskSheet 打字机 | +| 5.x 趋势分析 | P0 | TrendsView + TrendService + Swift Charts | +| 6.1-6.2 本地 + 离线 | 自然结果 | MLX + 无网络依赖 | +| 6.3 数据加密 | 改造 | `.completeFileProtection`(系统级) | +| 6.4 Face ID | P0 | AppLock | +| 6.5 截屏防护 | **砍** | iOS 无官方 API | +| 6.6 永久删除 | P0 | PermanentDelete | +| 7.x 首页 | P0(已有骨架) | HomeView 接真数据 | +| 8.x 模型管理 | P1 | MeView | +| 9.4 分享文字摘要 | P1 | MeView "导出" | +| 9.1-9.3 加密 ZIP 导出 | **砍** | | +| 10.x 引导 | P1(简化) | OnboardingFlow 3 页 | +| 11.x 提醒 | **砍** | | +| 12.x 语音 | **砍** | | +| 13.x 进阶趋势 | **砍** | | +| 14.x 暗黑/主题 | **砍** | | +| 15.x 多 profile | **砍** | 数据模型成本太大 | +| **16.1 报告对比** | **加回 P0** | C2 解读 Tab + ReportCompareService | +| 17.x 模型升级 | **砍** | | +| 18.x iCloud 同步 | **砍** | 与本地卖点冲突 | +| 19.2 灵动岛 | P1 加分 | CaptureActivity LiveActivity | +| 19.1 Widget | **砍** | | +| **C1 档案列表** | **新增 P0** | ArchiveListView | +| **C2 报告详情三 Tab** | **新增 P0** | ReportDetailView | +| **C2 关联到趋势** | **新增 P0** | Indicator.pinned 批量更新 | +| **C2 重新解读** | **新增 P0** | CaptureService.reanalyze | + +--- + +**END OF SPEC v1.0**