# Daily Widget Reco（任务清单）

> 对应计划：`spec_kit/Daily Widget Reco/plan.md`
>
> 标记规则：
>
> - 执行完成后将对应项从 `- [ ]` 改为 `- [x]`，并把“状态”改为 **已完成**
> - 进行中改为 **进行中**
> - 阻塞写明原因与解除条件

---

## 0. 清单状态说明

- **状态**：未开始 / 进行中 / 已完成 / 阻塞
- **阻塞**：必须写明阻塞点与解除条件

---

## 1. 客户端：补齐 Widget 场景 API（RN）

- [ ] **新增 `fetchRecoWidget`（`POST /v1/reco/widget`）**  
  - **状态**：未开始  
  - **文件**：`client/src/services/recoApi.ts`  
  - **要求**：
    - Header `Accept-Language` 逻辑沿用 `fetchRecoFeed`
    - body 结构沿用 `RecoRequest`
    - timeout 建议 12s
  - **验收**：在 App 内调用可拿到 `items[0].text`（允许为空但不报错）

---

## 2. iOS 原生能力：App Group 共享读写（suiteName 已确认）

> suiteName：`group.com.damer.mindfulness`

- [ ] **新增 App Group 共享存储原生模块（RN Bridge）**  
  - **状态**：未开始  
  - **目标**：RN 可写/读共享 `UserDefaults(suiteName:)`  
  - **要求**：
    - 支持 `setString(key, value)` / `getString(key)`（最小闭环）
    - 以 JSON 字符串形式存储（上层 JS 自行 `JSON.stringify/parse`）
  - **验收**：
    - RN 写入后，Widget 侧能读到同一 key
    - 解析失败不 crash（返回 `null`/空字符串均可，按上层回退）

- [ ] **新增触发 Widget 刷新的原生方法（可选但建议）**  
  - **状态**：未开始  
  - **目标**：App 写入缓存后触发 `WidgetCenter.reloadAllTimelines()`  
  - **验收**：App 调用后，Widget 在合理时间内刷新显示新内容（允许系统延迟）

---

## 3. 客户端：写入共享配置/画像/每日缓存（App ↔ Widget 共通）

- [ ] **写入共享配置 `widget.config.v1`（包含 `apiBaseUrl`）**  
  - **状态**：未开始  
  - **要求**：
    - App 启动后（或 baseURL 计算完成后）写入
    - JSON 字段最少包含：`schema_version`、`apiBaseUrl`、`saved_at`
  - **验收**：Widget 侧可读到正确 baseURL（dev/pro 随 App 切换）

- [ ] **写入共享画像 `widget.userProfile.v1_2`（Onboarding 完成后）**  
  - **状态**：未开始  
  - **依赖**：客户端已能获取/持久化用户画像（本地存储已有 `user.profileScoring`）  
  - **要求**：
    - 结构以现有 `UserProfileV1_2` 输出为准
    - JSON 字段建议包含：`schema_version`、`saved_at`、`user_profile`
  - **验收**：Widget 侧可读到 `user_profile`，且可用于请求后端

- [ ] **实现 App 前台“可选主动拉取”并写入 `widget.dailyReco.v1`**  
  - **状态**：未开始  
  - **要求**：
    - 判断 `day_key`（用户时区 `YYYY-MM-DD`）不是今天时才拉取
    - 调用 `fetchRecoWidget(k=1)`，成功写入缓存
    - 失败不影响 UI，不阻塞主流程
  - **验收**：App 打开后能把今日推荐写入共享缓存，Widget reload 后展示一致

---

## 4. iOS Widget：读取共享缓存并展示（先不接网络也能跑通）

- [ ] **Widget 读取 `widget.dailyReco.v1` 并展示**  
  - **状态**：未开始  
  - **要求**：
    - JSON 解析失败容错：视为无缓存
    - 展示优先级：今日缓存 → 最近缓存 → 兜底文案
  - **验收**：手动写入共享缓存后，Widget 立即能展示对应文案（或在下一次 reload 展示）

---

## 5. iOS Widget：网络拉取每日推荐 + Timeline 每日刷新（核心）

- [ ] **Widget 在缓存过期时请求 `POST /v1/reco/widget` 并写回缓存**  
  - **状态**：未开始  
  - **依赖**：可读到 `widget.config.v1`（apiBaseUrl）与 `widget.userProfile.v1_2`（user_profile）  
  - **要求**：
    - 无鉴权
    - 超时建议 8～12s
    - `Accept-Language` 与 App 保持一致（`en/tc`）
    - `items=[]` 视为失败，走回退
  - **验收**：清空今日缓存后，Widget 能在一次 timeline 请求内拉取并展示今日推荐（允许先兜底再更新）

- [ ] **实现每日刷新调度（TimelinePolicy）**  
  - **状态**：未开始  
  - **要求**：
    - `nextRefreshDate` 设置为“下一天本地时间 00:10～01:00 随机”之一
    - 不追求严格准点，但确保每日范围内能更新
  - **验收**：通过修改系统日期/模拟 day_key 变化，可观察到会进入“过期→重新拉取”逻辑

---

## 6. 联调与验收验证（必须）

- [ ] **联调：添加小组件后，未打开 App 也能每日更新（尽力而为）**  
  - **状态**：未开始  
  - **步骤**：
    - 安装带 Widget 的开发包
    - 在桌面添加 Widget
    - 清空今日缓存并等待 Widget 刷新（或手动触发 reload）
  - **验收**：Widget 能从后端拉取并展示；隔天能更新到新内容（允许系统延迟）

- [ ] **回退验证：断网/后端不可用/返回异常**  
  - **状态**：未开始  
  - **验收**：
    - Widget 不崩溃
    - 展示最近缓存或兜底文案

- [ ] **一致性验证：App 与 Widget 当天展示一致**  
  - **状态**：未开始  
  - **验收**：App 主动拉取并写入缓存后，Widget reload 后展示同一条 `content_id/text`

---

## 7. 文档与总览更新

- [ ] **补充实现说明（可放入客户端 README 或本需求 overflow）**  
  - **状态**：未开始  
  - **要求**：记录 suiteName、共享 key、如何本地验证

- [ ] **更新 `spec_kit/overview.md`**  
  - **状态**：未开始  
  - **要求**：在“Spec Kit Overview”中新增本需求摘要（目标/产物/已完成编码变更）

