145 lines
5.5 KiB
Markdown
145 lines
5.5 KiB
Markdown
# 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”中新增本需求摘要(目标/产物/已完成编码变更)
|
||
|