docs(spec): 导出身体档案功能设计(2026-05-27)
记录 Tab 顶部入口 + 全屏 sheet,两段式本地 RAG(意图抽取 → 结构化检索 → Markdown 生成), 新增 HealthExport @Model 持久化历史。给 W3 AskService 铺路。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,415 @@
|
|||||||
|
# 导出身体档案 — 设计文档
|
||||||
|
|
||||||
|
**日期**:2026-05-27 (W2)
|
||||||
|
**作者**:link2026 + Claude
|
||||||
|
**关联卖点**:#1 影像档案系统、#2 100% 本地、#3 本地 RAG 长期记忆、#4 隐私三件套、#6 Live Activity tok/s
|
||||||
|
**优先级**:P0(打通 RAG 链路 + demo 主要演示场景)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 一句话定位
|
||||||
|
|
||||||
|
在「记录」Tab 顶部增加「导出身体档案」入口,用户输入自然语言主诉(如「我感冒 3 天,把最近一个月给医生看」),完全本地的两段式 RAG 把 SwiftData 里相关的指标 / 报告 / 症状 / 日记 / 个人资料检索并生成给医生看的 Markdown 摘要,可复制、分享、查看历史。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 用户故事
|
||||||
|
|
||||||
|
> 周日晚上,我感冒第 3 天还没好。明早要去社区医院,医生只有 5 分钟问诊,我想把过去一个月的体温记录、上次体检的关键异常项、在服的降压药、家族过敏史一次性整理出来给医生。我不想把这些数据上传到任何云。
|
||||||
|
|
||||||
|
成功标准:
|
||||||
|
|
||||||
|
- 输入 prompt → 30 秒内出现首字 → 90 秒内完整生成
|
||||||
|
- 输出 Markdown 包含主诉 / 患者背景 / 近期症状 / 关键指标 / 在服药与过敏 / 患者疑问
|
||||||
|
- 一键复制到微信发给医生,或直接 AirDrop / 邮件分享
|
||||||
|
- 重启 App 后能看到历史导出
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 范围
|
||||||
|
|
||||||
|
**做**:
|
||||||
|
|
||||||
|
- 记录 Tab 右上角 toolbar「导出」按钮
|
||||||
|
- ArchiveListView 顶部「我的导出」横向卡区(有历史时显示,前 3 条 + 查看全部)
|
||||||
|
- 全屏 sheet:prompt 输入 / Phase 指示 / 流式 Markdown / 完成后复制+分享+重新生成
|
||||||
|
- 历史列表页 + 详情页
|
||||||
|
- 两段式 RAG 链路:Qwen3-1.7B 抽意图 → SwiftData 结构化检索 → Qwen3-1.7B 生成 Markdown
|
||||||
|
- 新 `HealthExport` @Model + Schema 注册
|
||||||
|
- 引用回链(referencedXxxIDs,W3 再做点击跳转)
|
||||||
|
|
||||||
|
**不做**:
|
||||||
|
|
||||||
|
- embedding / 向量检索
|
||||||
|
- 跨设备同步、云端备份
|
||||||
|
- PDF 导出(W6 余力再说)
|
||||||
|
- 给医生的诊断建议 / 用药建议(红线 §10.1)
|
||||||
|
- 自动定期导出(此版无 schedule)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ UI ─────────────────────────────────────────────────────┐
|
||||||
|
│ ArchiveListView │
|
||||||
|
│ ├─ .toolbar trailing: "导出" 图标按钮 │
|
||||||
|
│ └─ 顶部横向卡区 HealthExportRecentStrip(有历史时显示)│
|
||||||
|
│ │ │
|
||||||
|
│ └─→ HealthExportSheet (full-screen cover) │
|
||||||
|
│ ├─ prompt TextEditor │
|
||||||
|
│ ├─ Phase 状态条 │
|
||||||
|
│ ├─ Markdown 流式渲染 │
|
||||||
|
│ └─ Actions: 复制 / 分享 / 重新生成 │
|
||||||
|
│ │
|
||||||
|
│ HealthExportListView (NavigationLink "查看全部") │
|
||||||
|
│ └─ 全部历史(@Query DESC)→ HealthExportDetailView │
|
||||||
|
└────────────────────────────────────────────────────────┘
|
||||||
|
↑ Event 流
|
||||||
|
┌─ Service ────────────────────────────────────────────────┐
|
||||||
|
│ HealthExportService (struct, DI ModelContext + Runtime) │
|
||||||
|
│ func export(prompt:) -> AsyncThrowingStream<Event> │
|
||||||
|
│ Event = .phaseChanged(Phase) | .token(TokenChunk) │
|
||||||
|
│ | .completed(HealthExport) | .failed(Error) │
|
||||||
|
└────────────────────────────────────────────────────────┘
|
||||||
|
↑ 串行排队
|
||||||
|
┌─ AI 层 (已存在) ──────────────────────────────────────────┐
|
||||||
|
│ AIRuntime(actor 单例)→ LLMSession 串行两次调用 │
|
||||||
|
└────────────────────────────────────────────────────────┘
|
||||||
|
↑ 检索
|
||||||
|
┌─ Persistence (SwiftData) ────────────────────────────────┐
|
||||||
|
│ Indicator / Report / Symptom / DiaryEntry / │
|
||||||
|
│ UserProfile / HealthExport(新增) │
|
||||||
|
└────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**红线对齐**(CLAUDE.md §10):
|
||||||
|
|
||||||
|
- UI 不直接调 AIRuntime,只与 HealthExportService 通讯 ✅
|
||||||
|
- AIRuntime 仍是 actor 单例,两段调用在它的队列内串行,与 CaptureService / 未来的 AskService 互不抢占 GPU ✅
|
||||||
|
- 两个 prompt 都带 few-shot + 失败回退 ✅
|
||||||
|
- 不引入云服务、不自实现密码学、不重构现有 Tab/RecordSheet ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 数据模型
|
||||||
|
|
||||||
|
新增 `Models/HealthExport.swift`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import Foundation
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
@Model final class HealthExport {
|
||||||
|
var id: UUID = UUID()
|
||||||
|
var prompt: String = "" // 用户原始输入
|
||||||
|
var content: String = "" // 生成的 Markdown 全文
|
||||||
|
var createdAt: Date = .now
|
||||||
|
|
||||||
|
// 引用回链(对齐 §3.3)
|
||||||
|
var referencedIndicatorIDs: [UUID] = []
|
||||||
|
var referencedReportIDs: [UUID] = []
|
||||||
|
var referencedSymptomIDs: [UUID] = []
|
||||||
|
var referencedDiaryIDs: [UUID] = []
|
||||||
|
|
||||||
|
// 意图抽取快照(供"重新生成"复用,不再调一次 LLM)
|
||||||
|
var inferredTimeFromDate: Date?
|
||||||
|
var inferredTimeToDate: Date?
|
||||||
|
var inferredIntent: String?
|
||||||
|
|
||||||
|
// demo 卖点凭证
|
||||||
|
var modelTag: String = "Qwen3-1.7B-4bit"
|
||||||
|
var decodeRate: Double = 0 // 末次 tok/s
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schema 注册**:`App/KangkangApp.swift` 的 `ModelContainer(for:)` 加入 `HealthExport.self`(增表是 SwiftData 兼容变更,无需手写迁移)。
|
||||||
|
|
||||||
|
**为什么 `referenced*IDs` 用 `[UUID]` 而不是 SwiftData 关系**:
|
||||||
|
导出是历史快照,源 Indicator / Report 可能后续被用户永久删除(§10.4);弱关联避免 cascade 影响历史导出本身。点击跳转时,源记录若已不存在,UI 显示「记录已删除」灰态。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 状态机 + 数据流
|
||||||
|
|
||||||
|
`HealthExportService.export(prompt:)` 是 `AsyncThrowingStream<Event, Error>`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
enum Phase: String {
|
||||||
|
case extractingIntent // 理解意图
|
||||||
|
case retrieving // 检索数据
|
||||||
|
case generating // 撰写报告
|
||||||
|
case completed
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Event {
|
||||||
|
case phaseChanged(Phase)
|
||||||
|
case token(TokenChunk)
|
||||||
|
case completed(HealthExport)
|
||||||
|
case failed(Error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**流程**:
|
||||||
|
|
||||||
|
```
|
||||||
|
.idle
|
||||||
|
│ user tap 生成
|
||||||
|
▼
|
||||||
|
phaseChanged(.extractingIntent)
|
||||||
|
│ LLMSession.generate(prompt: INTENT_PROMPT, maxTokens: 120)
|
||||||
|
│ 失败 → 回退到默认 {time_range_days: 30, keywords: [], symptom_keywords: []}
|
||||||
|
▼
|
||||||
|
phaseChanged(.retrieving)
|
||||||
|
│ 同步 SwiftData 查询:
|
||||||
|
│ - Indicator where capturedAt ∈ [from, to], 可选按 keyword 过滤 name/seriesKey
|
||||||
|
│ - Report where reportDate ∈ [from, to]
|
||||||
|
│ - Symptom where startedAt <= to AND (endedAt == nil OR endedAt >= from)
|
||||||
|
│ - DiaryEntry where createdAt ∈ [from, to] AND content contains any symptom_keyword
|
||||||
|
│ (privacy 过滤:无主诉相关关键词的日记不入 prompt;
|
||||||
|
│ 若 symptom_keywords 为空,则一律不包含日记 —— 安全默认)
|
||||||
|
│ - UserProfile 单例,无条件包含
|
||||||
|
▼
|
||||||
|
phaseChanged(.generating)
|
||||||
|
│ 拼 GENERATION_PROMPT(把上一步结果序列化为简短结构)
|
||||||
|
│ LLMSession.generate(prompt:, maxTokens: 1024)
|
||||||
|
│ for token in stream: yield .token(chunk)
|
||||||
|
▼
|
||||||
|
phaseChanged(.completed)
|
||||||
|
│ build HealthExport(prompt, content, referencedIDs, inferred*, decodeRate)
|
||||||
|
│ modelContext.insert + try modelContext.save()
|
||||||
|
▼
|
||||||
|
.completed(healthExport)
|
||||||
|
```
|
||||||
|
|
||||||
|
**取消语义**:UI 关闭 sheet → stream 被取消 → 中间态不入库。
|
||||||
|
|
||||||
|
**与 AIRuntime 互斥**:HealthExportService 在 `AIRuntime` 的 actor 函数里调度两次 LLM 调用;若此时 CaptureService 正在跑 VL,自然在 actor 队列里等待。Phase indicator 在 UI 上显示「排队中」(可选,W3 polish)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Prompt 设计
|
||||||
|
|
||||||
|
两个 prompt 都放在 `AI/Prompts/HealthExportPrompts.swift`,带 2 个 few-shot。
|
||||||
|
|
||||||
|
### 7.1 意图抽取(Qwen3-1.7B,~120 token 输出)
|
||||||
|
|
||||||
|
```text
|
||||||
|
你是健康数据助手。读用户的请求,只输出严格 JSON,不要任何解释或 Markdown。
|
||||||
|
|
||||||
|
字段:
|
||||||
|
{
|
||||||
|
"time_range_days": int, // 时间窗,默认 30
|
||||||
|
"keywords": [string], // 指标关键词(中文,如"血压"/"血糖"/"体温")
|
||||||
|
"symptom_keywords": [string], // 症状关键词
|
||||||
|
"intent": string // 简短意图标签
|
||||||
|
}
|
||||||
|
|
||||||
|
示例 1:
|
||||||
|
User: 我感冒3天了,要把最近一个月的健康情况给医生看
|
||||||
|
Output: {"time_range_days":30,"keywords":["体温","血压","脉搏"],"symptom_keywords":["感冒","咳嗽","咽喉痛","发烧"],"intent":"cold_consult"}
|
||||||
|
|
||||||
|
示例 2:
|
||||||
|
User: 我最近血糖好像不稳,把上次体检前后的化验单整理一下
|
||||||
|
Output: {"time_range_days":90,"keywords":["血糖","糖化血红蛋白","胰岛素"],"symptom_keywords":[],"intent":"glucose_review"}
|
||||||
|
|
||||||
|
User: {{USER_PROMPT}}
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
|
||||||
|
**解析容错**:
|
||||||
|
- 非 JSON → 抓 `{…}` 之间的子串再试一次
|
||||||
|
- 仍失败 → 用默认 `{30, [], []}`,继续流程,不报错给用户
|
||||||
|
|
||||||
|
### 7.2 报告生成(Qwen3-1.7B,maxTokens 1024)
|
||||||
|
|
||||||
|
```text
|
||||||
|
你正在帮患者撰写一份给社区医生看的就诊摘要。
|
||||||
|
要求:
|
||||||
|
- 输出 Markdown,严格按下方结构
|
||||||
|
- 只用「数据」中提供的信息,数据缺失就写"无记录"
|
||||||
|
- 不要给诊断意见、不要给用药建议、不要写"建议就医"
|
||||||
|
- 引用具体数值时保留单位和参考范围
|
||||||
|
- 全文中文,简洁,医生 30 秒能扫完
|
||||||
|
|
||||||
|
结构:
|
||||||
|
# 就诊摘要 — {{INTENT_LABEL_CN}}
|
||||||
|
## 主诉
|
||||||
|
## 患者背景
|
||||||
|
## 近期症状(按时间倒序)
|
||||||
|
## 关键指标(异常项优先)
|
||||||
|
## 在服药与过敏
|
||||||
|
## 患者疑问
|
||||||
|
|
||||||
|
数据:
|
||||||
|
{{SERIALIZED_DATA_JSON}}
|
||||||
|
|
||||||
|
患者原话:{{USER_PROMPT}}
|
||||||
|
|
||||||
|
现在生成:
|
||||||
|
```
|
||||||
|
|
||||||
|
**SERIALIZED_DATA_JSON 结构**(给 LLM 看的精简结构):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"profile": {
|
||||||
|
"age": 38, "sex": "男", "height_cm": 172,
|
||||||
|
"allergies": ["青霉素"],
|
||||||
|
"chronic": ["高血压(2 年)"],
|
||||||
|
"family_history": ["父亲冠心病"],
|
||||||
|
"current_meds": ["缬沙坦 80mg qd"]
|
||||||
|
},
|
||||||
|
"symptoms": [
|
||||||
|
{"name": "感冒", "started": "2026-05-24", "severity": 2,
|
||||||
|
"ongoing": true, "note": "鼻塞、低烧"}
|
||||||
|
],
|
||||||
|
"indicators": [
|
||||||
|
{"name": "收缩压", "value": 142, "unit": "mmHg", "range": "<140",
|
||||||
|
"status": "high", "date": "2026-05-26"}
|
||||||
|
],
|
||||||
|
"reports": [
|
||||||
|
{"title": "年度体检", "type": "physical", "date": "2026-04-12",
|
||||||
|
"institution": "瑞金医院"}
|
||||||
|
],
|
||||||
|
"diaries": [
|
||||||
|
{"date": "2026-05-25", "excerpt": "夜里两点醒了一次,头痛 7/10"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. UI 详细设计
|
||||||
|
|
||||||
|
### 8.1 ArchiveListView 改动
|
||||||
|
|
||||||
|
- toolbar trailing 加按钮:`Image(systemName: "doc.text.below.ecg") "导出"`
|
||||||
|
- 在 `List` 顶部插入 `HealthExportRecentStrip()`(若 `@Query HealthExport` 非空)
|
||||||
|
- 横向卡区,3 条最近导出 + 末尾「查看全部 →」卡,点击进入 `HealthExportListView`
|
||||||
|
|
||||||
|
### 8.2 HealthExportSheet (full-screen cover)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ ✕ 导出身体档案 本地·永不上传 │ Header
|
||||||
|
├──────────────────────────────────────────────┤
|
||||||
|
│ 例:我感冒3天了,把最近一个月给医生看 │ Hint
|
||||||
|
│ ┌──────────────────────────────────────────┐ │
|
||||||
|
│ │ (多行 TextEditor,~6 行) │ │
|
||||||
|
│ └──────────────────────────────────────────┘ │
|
||||||
|
│ [ 生成报告 ] │ TjPrimaryButton
|
||||||
|
├──────────────────────────────────────────────┤
|
||||||
|
│ ●─○─○ 理解意图 │ Phase pill,
|
||||||
|
│ │ 生成时显示
|
||||||
|
│ 本地推理 · Qwen3 · 24.3 tok/s │
|
||||||
|
├──────────────────────────────────────────────┤
|
||||||
|
│ # 就诊摘要 — 感冒就诊 │
|
||||||
|
│ ## 主诉 │ Markdown 流式
|
||||||
|
│ 患者男,38 岁…… │ 渲染(原生
|
||||||
|
│ …(打字机效果)… │ Text(LocalizedStringKey))
|
||||||
|
│ │
|
||||||
|
├──────────────────────────────────────────────┤
|
||||||
|
│ [ 复制 ] [ 分享 ] [ 重新生成 ] │ 完成后才显示
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- 「分享」用系统 `ShareLink(item: content)`,导出纯文本
|
||||||
|
- 「重新生成」复用同一 `prompt` + `inferred*` 字段,跳过意图抽取,直接走 retrieving + generating
|
||||||
|
- 持久化时机:`.completed` 事件触发时由 Service 立即 `insert + save`;sheet 关闭只是 dismiss 视图,不再写库
|
||||||
|
- 生成中按 ✕ → 取消 stream → 不入库;已生成完成后按 ✕ → 仅 dismiss(数据已在库中)
|
||||||
|
|
||||||
|
### 8.3 HealthExportListView
|
||||||
|
|
||||||
|
简单的 `List` + `@Query(sort: \.createdAt, order: .reverse)`,每条显示:
|
||||||
|
- 标题:`HealthExport.prompt` 截断到 60 字
|
||||||
|
- 副标题:`relativeDate(createdAt)` + `tok/s` 标签
|
||||||
|
- 滑动删除
|
||||||
|
|
||||||
|
### 8.4 HealthExportDetailView
|
||||||
|
|
||||||
|
- 只读 Markdown(复用 sheet 的渲染组件)
|
||||||
|
- 顶部信息条:生成时间 / 模型 tag / tok/s
|
||||||
|
- toolbar:复制 / 分享 / 删除
|
||||||
|
- W3 再补:`referenced*IDs` 转 Pill,点击跳源记录(此 spec 不阻塞)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 错误处理
|
||||||
|
|
||||||
|
| 情况 | 行为 |
|
||||||
|
|---|---|
|
||||||
|
| 模型未就绪 | toolbar 按钮置灰 + 副标题「模型未就绪,前往下载」(对齐 §4) |
|
||||||
|
| 意图抽取 JSON 解析失败 | 默认 `{30 days, [], []}` 兜底,流程继续,不报错给用户 |
|
||||||
|
| SwiftData 查询为空 | 数据段填 `"无记录"`,LLM 仍生成结构化"无明显异常"摘要 |
|
||||||
|
| 生成 stream 中途取消 | Service 抛 `CancellationError`,UI 显示「已取消」,不入库 |
|
||||||
|
| 生成超时 (>120s) | `Task.withTimeout` 超时取消,UI 同取消逻辑 |
|
||||||
|
| LLM 抛错(显存等) | UI 显示「生成失败:{msg}」+ 重试按钮 |
|
||||||
|
| `modelContext.save` 失败 | 仅日志,UI 仍展示文本,提示「保存失败,请重试」 |
|
||||||
|
|
||||||
|
**安全:** 全程不调用任何网络;`HealthExport` 持久化继承 §6 的 `.completeFileProtection`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 测试策略
|
||||||
|
|
||||||
|
**单元(`HealthExportServiceTests`)**:
|
||||||
|
|
||||||
|
- mock `AIRuntime` 协议(新增 `protocol AIRuntimeProtocol`,actor 单例符合该协议)
|
||||||
|
- 给定固定 SwiftData in-memory + 已知 Indicator/Symptom → 验证 referencedIDs 正确
|
||||||
|
- 意图抽取返回非 JSON → 验证回退到默认 30 天
|
||||||
|
- 验证 Phase 转换顺序:`.extractingIntent → .retrieving → .generating → .completed`
|
||||||
|
- 取消语义:在 `.generating` 阶段取消 → 不入库
|
||||||
|
|
||||||
|
**Preview**:
|
||||||
|
|
||||||
|
- `HealthExportSheet` 用 mock service 吐预设 Markdown(打字机视效在 Preview 即可看到)
|
||||||
|
- `HealthExportListView` 用 3 条 fake `HealthExport`
|
||||||
|
|
||||||
|
**真机验收**(W3 末):
|
||||||
|
|
||||||
|
- 在 16 inch M3 Max 模拟器上跑通(simulator 走 CPU,慢但能跑通流程)
|
||||||
|
- 真机 iPhone 15 Pro:首字 ≤ 10s,完整生成 ≤ 60s,tok/s ≥ 20
|
||||||
|
- 关 WiFi + 飞行模式仍能正常生成(隐私三件套 demo 关键)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 与现有/未来代码的关系
|
||||||
|
|
||||||
|
- **复用**:`AIRuntime` / `LLMSession` / `TokenChunk` / `Tj.*` Design System
|
||||||
|
- **铺路**:`HealthExportService` 的两段式 RAG 工程模式直接复用给 W3 的 `AskService`(只需替换 generation prompt + 输出形态)
|
||||||
|
- **不冲突**:`CaptureService` 在 AIRuntime 队列里和本服务串行;两者不会同时占 GPU
|
||||||
|
- **不影响**:`ArchiveListView` / `RecordSheet` / 现有 7 个 @Model 都不需要重构
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 取舍记录
|
||||||
|
|
||||||
|
| 决策 | 选择 | 拒绝的方案 | 理由 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 入口位置 | 「记录」Tab toolbar | RecordSheet 加一项 | 语义:RecordSheet 是「写入」,导出是「读出」 |
|
||||||
|
| 数据范围 | Indicator+Report+Symptom+Profile+Diary | 仅 Indicator+Report | 「感冒 3 天」需要 Symptom;医生需要 Profile;Diary 由 LLM 关键词过滤后入 prompt,降低隐私风险 |
|
||||||
|
| 历史位置 | ArchiveListView 顶部横向卡区 + 查看全部 | 「我的」Tab 加历史入口 | 路径更短;符合「记录 Tab=身体档案」语义 |
|
||||||
|
| Pipeline | 严格两段式 RAG | 单段 LLM / 模板化 | 准确性 + 复用给 AskService + demo 卖点 #3 |
|
||||||
|
| Markdown 渲染 | SwiftUI 原生 `Text(LocalizedStringKey)` | 第三方 Markdown 库 | YAGNI;W6 polish 时再评估 |
|
||||||
|
| referenced 关联 | `[UUID]` 弱关联 | SwiftData 关系 | 历史快照 vs 源记录可被永久删除 |
|
||||||
|
| Live Activity | 此版只在 Service 暴露 decodeRate,UI 显示数字 | 此版直接接 ActivityKit | W5 真机阶段统一接,与 AskService 共用一套 Activity |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 排期估算(放在 W2 末 ~ W3 初)
|
||||||
|
|
||||||
|
| 步骤 | 工作量 |
|
||||||
|
|---|---|
|
||||||
|
| HealthExport @Model + Schema 注册 | 0.5h |
|
||||||
|
| HealthExportPrompts(两个 prompt + few-shot 调试) | 2h |
|
||||||
|
| HealthExportService(状态机 + 两段调用 + 检索) | 4h |
|
||||||
|
| HealthExportSheet(输入 + Phase + 流式渲染 + 三按钮) | 3h |
|
||||||
|
| ArchiveListView toolbar + RecentStrip | 1.5h |
|
||||||
|
| HealthExportListView + DetailView | 1.5h |
|
||||||
|
| 单元测试 + 真机验收 | 2h |
|
||||||
|
| **合计** | **~14h ≈ 2 个工作日** |
|
||||||
|
|
||||||
|
也是 W3「AskService 基础 RAG」的前置铺路工作,工程上一举两得。
|
||||||
Reference in New Issue
Block a user