fix:小组件- PUSH
This commit is contained in:
24
spec_kit/Daily Widget Reco/overflow.md
Normal file
24
spec_kit/Daily Widget Reco/overflow.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Daily Widget Reco(补充说明 / Overflow)
|
||||
|
||||
## 1. 关键配置
|
||||
|
||||
- **App Group suiteName**:`group.com.damer.mindfulness`
|
||||
- **后端鉴权**:无
|
||||
- **多语言**:仅 `en / tc`
|
||||
|
||||
## 2. 共享存储 Key(App ↔ Widget)
|
||||
|
||||
- `widget.config.v1`
|
||||
- 字段:`schema_version=1`、`saved_at`、`apiBaseUrl`
|
||||
- `widget.userProfile.v1_2`
|
||||
- 字段:`schema_version=1`、`saved_at`、`user_profile`(结构对齐 `UserProfileV1_2`)
|
||||
- `widget.dailyReco.v1`
|
||||
- 字段:`schema_version=1`、`saved_at`、`day_key`(本地日 `YYYY-MM-DD`)、`lang`、`item{content_id,text}`、`source`
|
||||
|
||||
## 3. 更新策略(双通道)
|
||||
|
||||
- **Widget 主动拉取**:缓存过期时请求 `POST /v1/reco/widget`,写回 `widget.dailyReco.v1`,并使用 `TimelinePolicy.after` 设定次日刷新
|
||||
- **App 辅助刷新**:
|
||||
- 启动与回到前台时调用 `ensureDailyWidgetRecoUpToDate`
|
||||
- 成功写入缓存后调用 `WidgetCenter.reloadAllTimelines()`(系统仍可能延迟)
|
||||
|
||||
176
spec_kit/Daily Widget Reco/plan.md
Normal file
176
spec_kit/Daily Widget Reco/plan.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# Daily Widget Reco(技术计划)
|
||||
|
||||
> 对应规范:`spec_kit/Daily Widget Reco/spec.md`
|
||||
>
|
||||
> 已确认输入(来自澄清):
|
||||
>
|
||||
> - 更新责任:**App 与 Widget 都需要**(双通道:Widget 主动拉取 + App 辅助刷新)
|
||||
> - App Group suiteName:`group.com.damer.mindfulness`
|
||||
> - 后端鉴权:不需要
|
||||
> - baseURL:由 **App 写入共享区** 提供给 Widget
|
||||
> - “每日”口径:用户本地时区
|
||||
> - 画像来源:App 写入共享区(Onboarding 完成后写入)
|
||||
|
||||
---
|
||||
|
||||
## 1. 计划目标
|
||||
|
||||
- 打通 `POST /v1/reco/widget`,让客户端与 Widget 都能获取每日推荐(Top1)。
|
||||
- 通过 App Group 共享存储,保证 **App 与 Widget 当天展示一致**。
|
||||
- 实现“每日更新(尽力而为)”:用户添加小组件后,即使不打开 App,也能依赖 WidgetKit Timeline 在每日范围内刷新。
|
||||
- 完整回退链路:今日缓存 → 最近缓存 → 兜底文案;任何失败不 crash、不阻塞主流程。
|
||||
|
||||
---
|
||||
|
||||
## 2. 总体方案(双通道更新)
|
||||
|
||||
### 2.1 核心结论
|
||||
|
||||
- **Widget 主动拉取**是满足“添加小组件后每日更新”的必要条件(仅靠 App 前台触发无法覆盖用户不打开 App 的情况)。
|
||||
- 同时保留 **App 辅助拉取与 reload**:
|
||||
- App 启动/进入前台/画像更新后可主动拉取并写缓存,提升即时性与一致性。
|
||||
- App 在写入共享缓存后调用 `WidgetCenter.reloadAllTimelines()`,加速 Widget 读取到新数据(系统仍可能延迟)。
|
||||
|
||||
### 2.2 数据流(高层)
|
||||
|
||||
- App(RN):
|
||||
- 写入共享配置(`apiBaseUrl` 等)→ 写入共享画像 →(可选)拉取 `widget` 推荐 → 写入共享每日缓存 → reload Widget
|
||||
- Widget(Swift):
|
||||
- 读取共享每日缓存,若过期则读取共享配置+画像并请求后端 → 写入共享缓存 → 输出 timeline
|
||||
|
||||
---
|
||||
|
||||
## 3. 客户端(React Native / Expo)改动点
|
||||
|
||||
### 3.1 新增 Widget 场景 API 封装
|
||||
|
||||
- 在 `client/src/services/recoApi.ts` 新增:
|
||||
- `fetchRecoWidget(req: RecoRequest): Promise<RecoEngineResult>`
|
||||
- Header `Accept-Language` 逻辑沿用 Feed(`zh* -> tc` else `en`)
|
||||
- path:`/v1/reco/widget`
|
||||
|
||||
### 3.2 共享存储:新增 App Group 写入能力
|
||||
|
||||
> 目标:RN 侧将必要数据写入 `UserDefaults(suiteName: "group.com.damer.mindfulness")`,供 Widget 读取。
|
||||
|
||||
建议实现路径(两种任选其一,按当前工程实际选型落地):
|
||||
|
||||
- 方案 A(推荐):引入一个“App Group UserDefaults 读写”的 RN 原生桥/插件(Swift/ObjC 模块 + JS 封装)。
|
||||
- 方案 B:若项目已存在可用能力(例如已接入 `react-native-shared-group-preferences` 或自研模块),直接复用并统一 key。
|
||||
|
||||
需要写入的共享 key(与 spec 对齐):
|
||||
|
||||
- `widget.config.v1`:包含 `apiBaseUrl`(以及未来灰度字段)
|
||||
- `widget.userProfile.v1_2`:用户画像快照(JSON)
|
||||
- `widget.dailyReco.v1`:每日推荐缓存(JSON)
|
||||
|
||||
### 3.3 App 侧写入与刷新触发时机
|
||||
|
||||
- **画像写入**:
|
||||
- Onboarding 完成后(或画像更新后)写入 `widget.userProfile.v1_2`
|
||||
- **配置写入**:
|
||||
- App 启动时(或 `API_BASE_URL` 计算完成后)写入 `widget.config.v1`
|
||||
- **主动拉取每日推荐(可选但建议)**:
|
||||
- App 启动或进入前台时:
|
||||
- 读取共享 `widget.dailyReco.v1`,若 `day_key` 不是今天则调用 `fetchRecoWidget` 拉取 Top1
|
||||
- 成功写入共享缓存,失败不影响 UI
|
||||
- **触发 Widget 刷新**:
|
||||
- 当 App 写入 `widget.dailyReco.v1` 成功后,调用 `WidgetCenter.reloadAllTimelines()`(通过原生桥触发)
|
||||
|
||||
> day_key 计算:以用户本地时区生成 `YYYY-MM-DD`(例如 `2026-02-03`)。
|
||||
|
||||
---
|
||||
|
||||
## 4. iOS Widget(Swift / WidgetKit)改动点
|
||||
|
||||
### 4.1 共享存储读取
|
||||
|
||||
- 统一使用:
|
||||
- `UserDefaults(suiteName: "group.com.damer.mindfulness")`
|
||||
- 读取并解析:
|
||||
- `widget.dailyReco.v1`
|
||||
- `widget.userProfile.v1_2`
|
||||
- `widget.config.v1`
|
||||
|
||||
解析失败必须容错:视为缺失,走回退策略。
|
||||
|
||||
### 4.2 Timeline 刷新策略(每日)
|
||||
|
||||
- `getTimeline` 流程:
|
||||
1. 读 `widget.dailyReco.v1`
|
||||
2. 若 `day_key` 为今天且文案存在 → 直接出 timeline
|
||||
3. 否则尝试网络拉取(需要 config + userProfile 均存在):
|
||||
- 请求 `POST {apiBaseUrl}/v1/reco/widget`
|
||||
- `Accept-Language`:从缓存 lang 或系统语言映射到 `en/tc`(优先与 App 一致)
|
||||
- `k=1`
|
||||
4. 成功:写入共享缓存并出 timeline
|
||||
5. 失败:用“最近缓存/兜底文案”出 timeline
|
||||
|
||||
- `policy`:
|
||||
- `TimelinePolicy.after(nextRefreshDate)`
|
||||
- `nextRefreshDate`:下一天本地时间的一个随机刷新点(例如 00:10~01:00 随机),减少集中刷新与被系统限流概率
|
||||
|
||||
### 4.3 网络实现注意事项
|
||||
|
||||
- 使用 `URLSession`,超时建议 8~12 秒(与 RN 对齐即可)
|
||||
- 无鉴权
|
||||
- 失败不抛出 crash:打印有限日志(或仅在 Debug)
|
||||
|
||||
---
|
||||
|
||||
## 5. 共享数据结构与一致性规则
|
||||
|
||||
### 5.1 `widget.dailyReco.v1`(v1)
|
||||
|
||||
按 `spec.md` 定义字段,重点:
|
||||
|
||||
- `day_key`:本地日 key(用户时区)
|
||||
- `lang`:`en|tc`
|
||||
- `item.content_id + item.text`:Widget 展示必须字段
|
||||
- `source`:标记由 `app` 还是 `widget` 写入(便于排查)
|
||||
|
||||
### 5.2 一致性策略(同一天同一条)
|
||||
|
||||
规则:
|
||||
|
||||
- Widget 展示以 `widget.dailyReco.v1` 为准(共享缓存是单一事实来源)。
|
||||
- App 若拉取到新结果,应覆盖写入共享缓存,并触发 reload,保证 Widget 同步。
|
||||
- 若当天出现“不同条”风险(并发写入):
|
||||
- 以最后写入者为准(last-write-wins)
|
||||
- 通过 `saved_at` + `source` 辅助排查
|
||||
|
||||
---
|
||||
|
||||
## 6. 回退策略(必须实现)
|
||||
|
||||
- 今日缓存存在且有效 → 展示
|
||||
- 今日缓存无效但有最近缓存 → 展示最近缓存
|
||||
- 无任何缓存 → 展示兜底安全文案(写死)
|
||||
|
||||
同时:
|
||||
|
||||
- 后端返回 `items=[]` 视为失败
|
||||
- JSON 解析失败视为无缓存
|
||||
|
||||
---
|
||||
|
||||
## 7. 实施顺序(建议)
|
||||
|
||||
1. **补齐客户端 API**:新增 `fetchRecoWidget`,并在本地可通过 Postman/或 RN 调用验证返回。
|
||||
2. **落地 App Group 共享读写(RN → iOS)**:先能写入/读取一个测试 key,确保 suiteName 正确。
|
||||
3. **写入共享配置与画像**:保证 Widget 侧具备发起请求所需输入。
|
||||
4. **Widget 侧读取缓存并展示**:先用共享缓存驱动 UI(不接网络也能跑通)。
|
||||
5. **Widget 侧网络拉取 + 写回缓存**:实现每日更新主链路。
|
||||
6. **App 辅助刷新**:App 前台拉取(可选)+ 写缓存 + reloadAllTimelines。
|
||||
|
||||
---
|
||||
|
||||
## 8. 验收与测试建议
|
||||
|
||||
- **联调验收**:
|
||||
- 手动清空共享缓存 → 添加 Widget → 观察首次拉取并展示(或先展示兜底再更新)
|
||||
- 修改设备日期到次日(或模拟 day_key 变化)→ 触发 `getTimeline`,确认会重新拉取
|
||||
- 断网 → 应展示最近缓存/兜底文案,不崩溃
|
||||
- **一致性验收**:
|
||||
- App 拉取后写入共享缓存,Widget 在 reload 后显示同一条
|
||||
|
||||
197
spec_kit/Daily Widget Reco/spec.md
Normal file
197
spec_kit/Daily Widget Reco/spec.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Daily Widget Reco(高层规范)
|
||||
|
||||
> 阶段:高层规范(spec)
|
||||
>
|
||||
> 背景引用:
|
||||
>
|
||||
> - `spec_kit/Personalized Reco/overview.md`:后端已实现可复用推荐流程(Feed / Push / Widget)
|
||||
> - `spec_kit/iOS Widget/spec.md`:已完成 WidgetKit Extension V1(写死文案)
|
||||
>
|
||||
> 本需求聚焦:**iOS 小组件“每日推荐”与 App 数据共通**,以及**客户端与后端推荐接口的串联**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与动机
|
||||
|
||||
后端已实现推荐接口(含 `widget` 场景),但客户端目前只接入了 Feed 推荐(`POST /v1/reco/feed`),iOS 小组件仍是 V1 写死文案。
|
||||
|
||||
现在需要实现:
|
||||
|
||||
- iOS 小组件展示“每日推荐”(每天一条),从后端获取。
|
||||
- App 与 Widget 的推荐内容数据共通(同一天展示同一条),并支持在用户添加小组件后开始每日更新。
|
||||
- 推荐链路具备可回退能力:网络失败、画像缺失、接口异常时不崩溃、仍有可展示内容。
|
||||
|
||||
---
|
||||
|
||||
## 2. 目标(Goals)
|
||||
|
||||
- **后端串联**:客户端与 iOS WidgetKit 能稳定调用后端 `widget` 场景推荐接口获取每日文案。
|
||||
- **数据共通**:App 与 Widget 通过 **App Group 共享存储**读写同一份“每日推荐缓存”,保证展示一致。
|
||||
- **每日更新(尽力而为)**:
|
||||
- 小组件添加到桌面后,系统开始调度 Widget Timeline;Widget 能在**每天**刷新一次内容(受系统限制,允许延迟)。
|
||||
- App 在合适时机触发更新与 `WidgetCenter.reload`,提高准时性(但不承诺“严格准点”)。
|
||||
- **安全与回退**:出现失败时按回退梯度展示(今日缓存 → 最近一次缓存 → 兜底安全文案),且不阻塞 App 主流程。
|
||||
|
||||
---
|
||||
|
||||
## 3. 非目标(Non-goals)
|
||||
|
||||
- 不做 Android Widget。
|
||||
- 不在本需求内改动后端推荐算法逻辑与 DB(后端已实现接口)。
|
||||
- 不承诺高频刷新(例如分钟级),遵循 iOS WidgetKit 刷新限制。
|
||||
- 不在本需求内实现复杂的“用户自定义小组件配置”(例如选择主题/类型);仅做“每日一句”。
|
||||
|
||||
---
|
||||
|
||||
## 4. 范围与交付物
|
||||
|
||||
### 4.1 范围
|
||||
|
||||
- 客户端(React Native / Expo):
|
||||
- 增加 `POST /v1/reco/widget` 的请求封装(与 Feed 形态一致)。
|
||||
- 负责把“用户画像快照/必要上下文”写入 App Group(供 Widget 拉取/请求)。
|
||||
- 在合适时机触发“写缓存 + 通知 Widget 刷新”。
|
||||
- iOS Widget(Swift / WidgetKit Extension):
|
||||
- 从 App Group 读取“每日推荐缓存”并展示。
|
||||
- 在需要时(缓存过期/缺失)发起网络请求从后端拉取,并写回缓存(或由 App 拉取并写回,见第 8 节决策)。
|
||||
- 共享数据契约:
|
||||
- 定义 App 与 Widget 共用的数据结构(版本化、可演进)。
|
||||
|
||||
### 4.2 交付物
|
||||
|
||||
- 客户端:推荐接口封装 + 共享存储写入 + 刷新触发。
|
||||
- iOS:Widget Timeline Provider 支持每日推荐(读取共享缓存/必要时拉取)。
|
||||
- 文档:本 `spec.md`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 术语与关键约束
|
||||
|
||||
- **Daily Reco(每日推荐)**:Widget 场景 Top1 文案(每天一条)。
|
||||
- **App Group**:iOS 用于 App 与 Extension 共享数据的机制(建议使用共享 `UserDefaults(suiteName:)`)。
|
||||
- **Widget 刷新限制**:刷新由系统调度,**不可保证准点**;需要设计缓存与兜底展示。
|
||||
|
||||
---
|
||||
|
||||
## 6. 后端接口契约(Client ↔ Server)
|
||||
|
||||
### 6.1 接口
|
||||
|
||||
- `POST /v1/reco/widget`
|
||||
|
||||
### 6.2 Header
|
||||
|
||||
- `Accept-Language`:
|
||||
- 客户端沿用既有策略:当前仅区分 `en/tc`
|
||||
- 规则:`i18n.language` 以 `zh` 开头 → `tc`,否则 `en`
|
||||
|
||||
### 6.3 Request Body(与 Feed 一致)
|
||||
|
||||
- `k?: number`(Widget 默认 1;建议不传或显式传 `1`)
|
||||
- `user_profile: UserProfileV1_2`(必填)
|
||||
- `already_recommended_ids?: (string|number)[]`(可选,默认 `[]`)
|
||||
- `touched_or_viewed_ids?: (string|number)[]`(可选,默认 `[]`)
|
||||
- `now?: string`(ISO8601,可选,用于测试/确定性)
|
||||
|
||||
### 6.4 Response Body
|
||||
|
||||
- `items: RecommendedItem[]`(Widget 预期取 `items[0]`;允许为空)
|
||||
- `meta: Record<string, unknown>`(用于可观测/调参;客户端可选择性落地)
|
||||
|
||||
---
|
||||
|
||||
## 7. 共享数据契约(App ↔ Widget)
|
||||
|
||||
> 原则:结构**版本化**、字段可缺省、读取端容错;避免把整个业务状态塞进共享区。
|
||||
|
||||
### 7.1 共享 Key(建议)
|
||||
|
||||
- `widget.dailyReco.v1`:每日推荐缓存(Widget 展示的主数据)
|
||||
- `widget.userProfile.v1_2`:用户画像快照(供 Widget 发起请求使用)
|
||||
- `widget.recoHistory.v1`:去重/频控所需的最小历史(可选,控制大小)
|
||||
- `widget.config.v1`:Widget 端需要的配置(例如 `apiBaseUrl`、灰度开关等)
|
||||
|
||||
### 7.2 `widget.dailyReco.v1` 数据结构(JSON)
|
||||
|
||||
- `schema_version: 1`
|
||||
- `saved_at: string`(ISO8601)
|
||||
- `day_key: string`(本地日维度 key,例如 `2026-02-03`;用于“今天是否已刷新”的判断)
|
||||
- `lang: "en" | "tc"`
|
||||
- `item`(可为空):
|
||||
- `content_id: number`
|
||||
- `text: string`
|
||||
- `final_score?: number`
|
||||
- `fallback_level_final?: number`
|
||||
- `meta?: Record<string, unknown>`(可选,用于排查;注意体积)
|
||||
- `source: "app" | "widget"`(可选:谁写入的)
|
||||
|
||||
### 7.3 读写规则
|
||||
|
||||
- Widget 展示优先级:
|
||||
1. `day_key` 为今天且 `item.text` 非空 → 直接展示
|
||||
2. 否则展示最近一次缓存(若存在)
|
||||
3. 否则展示兜底安全文案(写死)
|
||||
- 写入要求:
|
||||
- 写入必须原子化(一次性写完整 JSON),避免部分字段缺失导致解析失败
|
||||
- 解析失败时当作“无缓存”,走兜底策略
|
||||
|
||||
---
|
||||
|
||||
## 8. 每日更新策略(刷新触发与责任划分)
|
||||
|
||||
### 8.1 关键结论
|
||||
|
||||
为满足“用户添加小组件后可每日更新”,需要依赖 WidgetKit 自身的 Timeline 调度。仅靠 App 在前台触发更新,无法保证用户不打开 App 时也能更新。
|
||||
|
||||
### 8.2 推荐实现(V1.5:Widget 主动拉取)
|
||||
|
||||
- **Widget Timeline Provider**:
|
||||
- 每次 `getTimeline`:
|
||||
- 先读 `widget.dailyReco.v1`
|
||||
- 若 `day_key` 不是今天 → 尝试从后端请求 `POST /v1/reco/widget` 获取今日内容
|
||||
- 成功后写入 `widget.dailyReco.v1` 并返回 timeline
|
||||
- 失败则返回“最近一次缓存/兜底文案”的 timeline
|
||||
- Timeline 刷新策略:
|
||||
- `policy = .after(nextRefreshDate)`,`nextRefreshDate` 设为“下一天的本地 00:10~01:00 之间随机一个时间”(减少集中刷新)
|
||||
|
||||
- **App(RN)**:
|
||||
- 在 Onboarding 完成或画像更新时,把 `widget.userProfile.v1_2` 写入共享区
|
||||
- 当 App 成功拉取到推荐(可选)时,也可写入 `widget.dailyReco.v1` 并触发 `WidgetCenter.reloadAllTimelines()`,提升即时性
|
||||
|
||||
### 8.3 备选实现(App 主动拉取 + Widget 仅展示)
|
||||
|
||||
若团队希望 Widget 端完全不发网络请求,则:
|
||||
|
||||
- App 负责在“启动/前台/每天首次打开”等时机拉取 `POST /v1/reco/widget` 并写入共享缓存
|
||||
- Widget 仅读取缓存展示
|
||||
|
||||
缺点:用户不打开 App 时,小组件可能长期不更新(不满足“每日更新”的强诉求)
|
||||
|
||||
---
|
||||
|
||||
## 9. 失败回退与稳定性
|
||||
|
||||
- **网络失败**:使用最近一次缓存;若无缓存,展示兜底安全文案。
|
||||
- **接口返回 items 为空**:视为失败,走同样回退。
|
||||
- **画像缺失**:
|
||||
- App 侧应尽量在首次进入完成问卷后写入 `widget.userProfile.v1_2`
|
||||
- 若仍缺失,Widget 端不崩溃,直接展示缓存/兜底文案;并在下一次 timeline 继续尝试
|
||||
- **数据损坏/解析失败**:清空本次读取结果,走兜底策略(不可 crash)。
|
||||
|
||||
---
|
||||
|
||||
## 10. 验收标准(Acceptance Criteria)
|
||||
|
||||
- **接口串联**:客户端/Widget 能成功请求 `POST /v1/reco/widget` 并解析返回。
|
||||
- **数据共通**:同一天内,App 与 Widget 展示的“每日推荐”一致(以共享缓存为准)。
|
||||
- **每日更新**:用户把小组件添加到桌面后,小组件在后续每天能更新到新的推荐(允许系统延迟,但应在一天内更新)。
|
||||
- **可用性**:断网/后端不可用/返回异常时,Widget 仍能展示最近缓存或兜底文案,App 不崩溃。
|
||||
|
||||
---
|
||||
|
||||
## 11. 风险与注意事项
|
||||
|
||||
- iOS Widget 刷新由系统调度,无法承诺“严格 00:00 更新”;需接受“尽力而为 + 缓存兜底”。
|
||||
- App Group 的 suiteName / entitlements 配置错误会导致 Widget 读不到共享数据;必须在实现阶段统一校验。
|
||||
- Widget 端直连后端需要维护 baseURL 与环境切换策略(dev/pro);建议通过 `widget.config.v1` 注入,避免写死。
|
||||
|
||||
144
spec_kit/Daily Widget Reco/tasks.md
Normal file
144
spec_kit/Daily Widget Reco/tasks.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# 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”中新增本需求摘要(目标/产物/已完成编码变更)
|
||||
|
||||
Reference in New Issue
Block a user