diff --git a/docs/README.md b/docs/README.md index 2eb2569..8185a9e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ > 文档状态:方案基线 > 最后核验:2026-08-04 -> 当前阶段:P-1 高保真交互原型已产出、待视觉确认;Android 工程尚未初始化 +> 当前阶段:P-1 核心原型已确认;首页、问卦簿与默认本机保存 v0.3 已实现并待视觉复核;Android 工程尚未初始化 本目录是 Brainwave 的项目知识事实源。产品决策、领域算法、架构边界、验收标准和已知失败模式必须写入仓库;聊天记录、口头约定和临时提示不构成项目规范。 @@ -73,6 +73,8 @@ Brainwave 是一个以《易经》三枚铜币法为文化背景的 Android 个 - 用户真实投币并录入;MVP 不提供随机起卦按钮。 - 东方文化表达采用“纸、墨、朱砂、留白”的内容优先风格。 - AI 仅在用户主动点击「解」之后调用。 +- 完整起卦、问题、解读与行动记录默认保存在 App 私有本机存储,首页明确告知,用户可在设置全局关闭或对当次退出。 +- 本地历史默认排除系统/设备迁移/云备份;自动保存与 AI 数据发送授权完全独立。 仍需产品确认的事项记录在[决策记录](decisions.md#未决问题)。任何代理不得把 `TBD` 悄悄变成产品事实。 diff --git a/docs/ai-safety.md b/docs/ai-safety.md index d285b9c..765f86e 100644 --- a/docs/ai-safety.md +++ b/docs/ai-safety.md @@ -15,6 +15,8 @@ 禁止后台预取、自动重试到另一模型、首次进入结果页即调用、以及把 AI 回复用于改写 `CastResult`。 +本地历史自动保存、读取问卦簿、切换保存设置或已有 AI 解读记录,都不构成网络调用授权。`autoSaveHistory` 与 AI 同意版本必须是独立状态;任何保存路径均不得顺带调用 AI。 + ## 2. AI 输入契约 客户端提交给后端的业务载荷应是明确 DTO,不发送整个数据库实体或 UI 状态: @@ -143,6 +145,7 @@ Android 端密钥管理参考:[Android 安全清单](https://developer.android - 同一 `CastResult` 在 AI 请求前后字节级确定字段不变。 - 没有 `CastResult`、未点击、未同意或结果尚未显示时,网络 mock 收到 0 次调用。 +- 自动保存完成、历史详情打开、保存设置切换和“保存本次”执行时,网络 mock 均收到 0 次调用;这些操作不得改变 AI 同意状态。 - 提示注入样例不能让模型重算卦、泄露系统提示或执行用户文本中的指令。 - 高风险测试集不产生确定的医疗、法律、投资或人生决定。 - 所有合法输出都有可撤销行动;所有非法输出都被客户端拒绝且可改用本地解释。 diff --git a/docs/architecture.md b/docs/architecture.md index 193a34d..8330aac 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -58,6 +58,7 @@ app/src/main/java// │ └── ai/ # 网络 DTO、gateway、响应校验 └── feature/ ├── onboarding/ + ├── home/ # 回访首页、最近一次、已上线次级入口 ├── question/ ├── casting/ ├── result/ @@ -107,7 +108,13 @@ core/designsystem → Compose/Material + core model(仅绘制需要) ### `HistoryRepository` -在用户明确保存后持久化会话。Room 模型不得泄露到 UI;数据库迁移必须有测试。 +接收第六爻锁定事件与当次 `HistorySavePolicy` 快照,在 `autoSaveHistory=true` 时持久化会话;总开关关闭时仍支持结果页明确发出的“保存本次”命令。策略包含 `saveQuestion`、`saveExplanation` 和 `saveActionNote`,生产默认值均为 `true`,由 DataStore 提供但不得泄露到领域起卦逻辑。 + +Repository 提供倒序历史与最新一条的 `Flow` 查询,并把随后产生的本地或 AI 解读事务性关联到同一 session。重新解读新增版本或执行经过明确确认的替换命令,禁止普通 upsert 静默覆盖。DAO 事务负责 session、explanation 和 action 的一致写入与级联删除;首页直接订阅最新记录的投影,不维护第二份敏感正文缓存。Room 模型不得泄露到 UI;数据库迁移必须有测试。 + +### `HomeViewModel` / `HistoryViewModel` + +`HomeViewModel` 组合 onboarding 状态、最新历史投影、保存策略摘要和已启用功能入口,只输出真实可进入的入口;不为未来功能生成禁用占位。首页固定输出本机保存/AI 发送边界文案和设置入口。`HistoryViewModel` 维护列表、空态、详情、单条删除和清空全部确认状态,通过 `HistoryRepository` 完成操作。导航使用一次性 UI 事件或 NavHost 回调,返回后恢复列表位置。 ### `ExplanationRepository` @@ -136,10 +143,12 @@ ResultUiState( ## 7. 数据和存储选择 - **assets 或预置 Room**:版本化的 64 卦及爻辞。首版如果仅按编号读取,JSON assets 更简单;需要全文检索或内容迁移时再采用预置 Room。 -- **Room**:用户主动保存的会话、解释和行动记录。Android 官方推荐 Room 管理非平凡结构化数据。[Room 文档](https://developer.android.com/training/data-storage/room) -- **DataStore**:方法说明是否已读、主题、AI 同意版本等少量设置;不保存大型历史或完整卦库。[DataStore 文档](https://developer.android.com/topic/libraries/architecture/datastore) +- **Room**:按保存策略自动写入或由用户当次明确保存的会话、解释和行动记录。Android 官方推荐 Room 管理非平凡结构化数据。[Room 文档](https://developer.android.com/training/data-storage/room) +- **DataStore**:方法说明是否已读、主题、AI 同意版本和历史保存策略等少量设置;不保存大型历史或完整卦库。[DataStore 文档](https://developer.android.com/topic/libraries/architecture/datastore) - **SavedStateHandle**:恢复当前未完成流程;不是长期历史数据库。 +历史 Room 数据库及其辅助文件必须通过 Android backup/data-extraction 配置排除系统自动备份和设备迁移。此边界由构建配置测试机械验证,不能只依赖界面文案;引入导出、账号或同步前必须新增 ADR。 + 详细契约见[数据与内容](data-content.md)。 ## 8. 网络与 AI 边界 diff --git a/docs/data-content.md b/docs/data-content.md index 3d8fea7..9882e45 100644 --- a/docs/data-content.md +++ b/docs/data-content.md @@ -7,11 +7,11 @@ | 数据 | 来源 | 默认位置 | 敏感性 | 是否参与起卦 | |---|---|---|---|---| -| 三轮币面/六爻结果 | 用户输入/本地计算 | 会话状态,可选 Room | 低至中 | 是 | -| 用户问题 | 用户输入 | 会话状态,可选 Room | 高 | 否 | +| 十八枚币面/六爻结果 | 用户输入/本地计算 | 会话状态;结果锁定后默认 Room | 低至中 | 是 | +| 用户问题 | 用户输入 | 会话状态;按默认开启的设置进入 Room | 高 | 否 | | 64 卦与爻辞 | 经审核的内容包 | assets 或预置 Room | 低,关注版权 | 仅用于查表 | | 本地解释 | 编辑内容 | assets | 低,关注版权 | 否 | -| AI 请求与回复 | 用户主动请求 | 内存,可选 Room | 高 | 否 | +| AI 请求与回复 | 用户主动请求 | 请求在内存;回复按默认开启的设置进入 Room | 高 | 否 | | 偏好与同意版本 | 用户设置 | DataStore | 中 | 否 | 任何“否”的数据都不能成为 `CastEngine` 输入。 @@ -106,6 +106,7 @@ ExplanationEntity - contractVersion: string - content: validated structured JSON - createdAt +- supersedesExplanationId: nullable foreign key ActionNoteEntity - sessionId: foreign key @@ -118,6 +119,9 @@ ActionNoteEntity - 数据库存储原始 18 枚币面和确定结果,读取时可重新计算并进行一致性检查。 - `questionText` 默认可为空;用户不保存问题时不能偷偷复制到其他表。 +- 第六爻锁定时读取一次 `HistorySavePolicy` 快照;总开关开启时,在单个事务中创建 `CastingSessionEntity`,并按默认开启的内容开关写入 `questionText`。关闭总开关或选择“本次不保存”时不得产生长期记录。 +- 同一流程随后生成的本地或 AI 解读按策略写入关联的 `ExplanationEntity`;重新解读新增实体并通过 `supersedesExplanationId` 保留版本关系,不得静默覆写旧正文。 +- 行动记录按策略关联到同一 session;没有实际行动内容时不创建空 `ActionNoteEntity`。 - 删除 session 应级联删除解释和行动记录。 - enum 使用稳定字符串或显式转换,不依赖 Kotlin ordinal。 - 每次 schema 迁移必须有 migration test;禁止发布构建使用 destructive migration。 @@ -131,7 +135,12 @@ DataStore 仅保存少量偏好: - 减少应用内非必要动画; - AI 同意文本版本和同意时间; - 默认解释方式; -- 是否允许保存问题原文。 +- `autoSaveHistory`:默认 `true`; +- `saveQuestionText`:默认 `true`; +- `saveExplanationContent`:默认 `true`; +- `saveActionNote`:默认 `true`。 + +总开关关闭时,后三项不参与自动写入,但保留用户上次选择;再次开启后恢复。DataStore 中的本地保存偏好与 AI 同意版本是独立字段,不得相互推导。 不要把六爻历史、完整内容包或 AI 长文本塞入 DataStore。 @@ -143,22 +152,26 @@ DataStore 仅保存少量偏好: - AI 请求前展示准确的数据清单和服务方类别。 - 日志、分析、崩溃报告、截图测试夹具不得使用真实用户问题。 - 调试日志使用固定脱敏样例;发布构建关闭网络 body logging。 -- 历史导出、云备份和 Android Auto Backup 策略在实现前必须单独决策。 +- `CastingSessionEntity`、`ExplanationEntity`、`ActionNoteEntity` 所在的 Room 数据库及其 `-wal`、`-shm` 等辅助文件必须通过 Android backup/data-extraction 规则排除,不参与 Auto Backup、设备到设备迁移或厂商云备份。 +- MVP 不提供历史导出、账号或云同步。未来任何导出、备份或同步都必须新增决策、展示准确范围并取得单独的明确选择。 ## 8. 数据生命周期 ```text 草稿问题/未完成投币:SavedStateHandle + 内存 - ↓ 用户完成 + ↓ 第六爻确认并锁定 不可变 CastResult:结果页内存 - ↓ 用户明确保存 -Room 历史快照 + ├─ autoSaveHistory=true → Room 会话快照 + 按设置保存问题 + └─ autoSaveHistory=false → 不写 Room,可由用户选择“保存本次” AI 请求:请求期间内存 → 服务端按已披露策略处理 -AI 回复:结果页内存 → 用户保存后可进入 Room +本地/AI 回复:结果页内存 → 按设置关联到同一 Room 会话 + └─ 会话不存在或解释保存关闭 → 不长期保存,可由用户选择保存本次 ``` -会话中止后是否保留草稿由产品设置决定;无论如何都不能把草稿作为遥测发送。 +“最近一次”不是独立复制的数据,而是按 `createdAt` 从 Room 查询得到的最新已保存会话。首页不得缓存另一份问题或解释正文;删除该会话后,最近记录区域必须随查询结果一起更新为空态或下一条记录。 + +中止的未完成草稿只按 SavedState 恢复策略存在,不进入问卦簿;无论如何都不能把草稿作为遥测发送。用户选择“本次不保存/删除本次记录”后,应删除已经自动创建的 session 及其关联内容,但不改写全局设置。 ## 9. 内容变更规则 diff --git a/docs/decisions.md b/docs/decisions.md index 69bb8c1..5893d4d 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -76,11 +76,13 @@ ## ADR-009:历史记录采用明确保存、本机优先 -- 状态:`Proposed` +- 状态:`Superseded by ADR-012` - 日期:2026-08-04 -- 决定:用户完成后明确保存才进入 Room;保存时可不保留问题原文;默认无账号和云同步。 +- 关联:FR-H-001~006、原 TBD-007、2026-08-04 用户确认 +- 决定:用户完成后明确点击保存才进入 Room;固定保存可复核的起卦快照,问题原文和当前解释分别使用默认关闭的独立选项;默认无账号和云同步。 - 原因:问题可能高度敏感,自动永久保存不是安全默认值。 -- 后果:历史功能不能成为完成起卦的前置条件。 +- 后果:首页在存在记录时显示“最近一次 / 问卦簿”次级入口,但“开始一问”仍是唯一主操作;没有记录时提供说明和开始入口,不显示空白列表。历史功能不能成为完成起卦的前置条件。 +- 取代原因:用户进一步确认“用户写的内容与解读默认完整保存在 App 内”,并要求在首页显式说明、在设置中可关闭;现行规则见 ADR-012。 ## ADR-010:多动爻全部透明展示 @@ -101,6 +103,24 @@ - 后果:原型是体验契约与评审证据,但不是生产领域代码;P1 必须独立实现和穷举验证算法,P3 复用已确认的设计令牌与状态关系。 - 复审条件:用户否定当前流程或目标平台发生变化。 +## ADR-012:完整记录默认在本机自动保存,可配置关闭 + +- 状态:`Accepted` +- 日期:2026-08-04 +- 关联:FR-H-001~008、取代 ADR-009、解决原 TBD-008、2026-08-04 用户确认 +- 决定: + 1. 第六爻确认并锁定 `CastResult` 后,只要“自动保存完整记录”开启,就立即在 Room 创建可复核的会话快照;未完成的问题与投币草稿仍只用于会话恢复,不进入长期历史。 + 2. `autoSaveHistory`、`saveQuestionText`、`saveExplanationContent` 和 `saveActionNote` 默认均为开启。关闭总开关后不再自动写入历史,三个内容开关同步停用;用户仍可在当次结果页选择“保存本次”。 + 3. 当次随后生成的本地或 AI 解读,在对应设置开启时关联到同一会话;重新解读不得静默覆盖已保存内容,必须新增版本或由用户明确确认替换。 + 4. 首页固定显示“起卦与历史默认保存在本机,不主动上传。只有你选择 AI 解读时,本次所需内容才会发送。”,并提供设置入口。结果页提供明确的自动保存状态和“本次不保存/删除本次记录”。 + 5. 问卦簿支持单条删除和清空全部;删除会级联移除会话、解读和行动记录。 + 6. 历史数据库及其问题、解读和行动数据默认排除 Android Auto Backup、设备到设备迁移的数据提取规则和任何云备份;MVP 不提供账号或云同步。未来备份、导出或同步必须另立决策并取得单独、明确的用户选择。 + 7. 本地自动保存与 AI 数据发送授权完全独立。自动保存开启、历史中已有内容或用户曾同意旧请求,都不能触发网络调用;每次 AI 调用仍须满足 AI 调用门。 +- 原因:完整的起卦、问题、解读与行动记录共同构成可回顾的个人反思日志;默认保存能避免用户遗漏,但必须通过首页透明告知、设置和单次退出能力保留控制权。 +- 未采用:继续要求每次手动保存会频繁丢失预期记录;默认只保存卦象快照会让问卦簿缺少上下文,削弱回顾价值。 +- 后果:P4 必须实现保存策略、事务关联、设置、单次退出、删除、迁移和备份排除测试;ADR-012 是生产事实源,决策时尚未同步的 v0.2 原型只能作为旧流程评审材料,现行 v0.3 已完成同步。 +- 复审触发:引入账号、导出、云同步、系统备份、跨设备迁移或新的隐私/合规要求。 + ## 未决问题 | ID | 问题 | 推荐默认 | 阻塞阶段 | @@ -111,8 +131,6 @@ | TBD-004 | 问题是否允许留空 | 允许选择“不写具体内容”,但需显式操作 | P3 | | TBD-005 | 经典原文、现代白话的版本与授权 | 自有白话 + 可核验公版原文 | P2,发布阻塞 | | TBD-006 | 乾用九、坤用六是否纳入 MVP | 内容具备时展示 | P2/P3 | -| TBD-007 | 历史是否默认保存卦象 | 默认不保存,完成后询问 | P4 | -| TBD-008 | Android Auto Backup 是否包含历史 | 默认排除敏感历史,待隐私评审 | P4 | | TBD-009 | AI 模型供应商与自有后端 | 供应商无关接口;先交付本地版 | P5 | | TBD-010 | 服务端问题/回复保留期 | 最小化且明确披露,优先不持久化正文 | P5,发布阻塞 | | TBD-011 | 高风险本地资源表覆盖地区 | 首发市场确认后维护,不让模型编号码 | P5 | diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index 603c85e..df2eb0a 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -1,6 +1,6 @@ # 分阶段实施计划 -> 状态:P-1 高保真交互原型已完成,待用户视觉确认;P0 尚未开始 +> 状态:P-1 核心原型已确认,首页/问卦簿与默认本机保存 v0.3 已完成待视觉复核;P0 尚未开始 > 计划原则:先用原型确认高返工成本体验,再锁定确定性领域核心,随后接内容和 UI,最后接网络 AI ## 1. 依赖图 @@ -25,7 +25,8 @@ P1 与 P2 可并行,但 P3 不能在领域与内容契约未稳定时复制原 任务: - 建立 360 × 792 基准视口的高保真 HTML 原型和设计令牌。 -- 覆盖欢迎、起念、六轮录入、结果、本地/AI 选择、同意、成功与失败状态。 +- 覆盖欢迎、回访首页、起念、六轮录入、结果、本地/AI 选择、同意、成功与失败状态。 +- 覆盖默认本机自动保存、首页告知、设置开关、当次退出、解读关联、清空全部、问卦簿列表/空态/详情和删除确认,并保持首页单一主操作。 - 使用已知夹具核对本卦、之卦、动爻和无动爻布局。 - 导出关键状态截图,并把评审入口与 Android 映射写入[原型说明](prototype.md)。 - 收集用户对视觉、录入控件、结果层级与解释语气的明确反馈。 @@ -37,7 +38,7 @@ P1 与 P2 可并行,但 P3 不能在领域与内容契约未稳定时复制原 - 原型不发起外部请求,不伪装真实 AI,不把工作名当作正式决定。 - 用户确认或提出一轮可执行的修改意见;确认前不把视觉固化为生产 Compose 页面。 -当前交付物已完成,视觉确认仍待用户评审。 +核心 v0.1 已由用户确认;v0.3 已同步 ADR-012 并通过十八状态浏览器审计,首页、问卦簿和保存设置待用户视觉复核。 ## 3. P0:仓库与 Android 骨架 @@ -129,15 +130,20 @@ P1 与 P2 可并行,但 P3 不能在领域与内容契约未稳定时复制原 - 实现版本化本地解释模板。 - 实现“可以试的一小步”的非裁决式结构。 -- 实现 Room 历史、可选保存问题、详情和删除。 -- 实现 DataStore 设置与同意版本基础设施。 -- 完成 migration、删除、隐私和离线测试。 +- 实现 Room 历史:第六爻锁定后按策略自动保存快照,问题原文、当次解读和行动记录默认保存到同一会话。 +- 实现 `autoSaveHistory` 总开关及三个默认开启的内容开关;支持总开关关闭后的“保存本次”和全局设置不变的“本次不保存/删除本次记录”。 +- 实现按时间倒序的问卦簿、派生“最近一次”、空态、详情、解释版本、单条删除和清空全部确认。 +- 在首页实现固定本机保存/AI 发送边界说明和设置入口;本地保存设置与 AI 同意版本保持独立。 +- 配置 backup/data-extraction 规则,排除历史 Room 数据库及辅助文件。 +- 完成 migration、策略组合、事务关联、删除、备份排除、隐私和离线测试。 退出条件: -- 用户可选择不保存问题而保存卦象结果。 +- 默认设置下,完成起卦会写入一条包含问题的 Room 会话,随后生成的解读会关联到同一会话且不静默覆盖旧版本。 +- 总开关关闭、单个内容开关关闭和“本次不保存”均按契约生效;未完成草稿不进入 Room。 +- 首页准确告知本机保存与 AI 发送边界,不存在未上线功能的空占位。 - 本地解释在无网络时可用。 -- 删除行为与备份策略一致且经过验证。 +- 单条删除、清空全部与备份排除策略一致且经过验证。 - 此阶段已经是可发布的本地版候选。 ## 8. P5:AI 解读 diff --git a/docs/product-spec.md b/docs/product-spec.md index 7055162..23adea7 100644 --- a/docs/product-spec.md +++ b/docs/product-spec.md @@ -71,7 +71,7 @@ Brainwave 帮助用户把一件正在纠结的事放慢来看。应用保留三 - **FR-Q-001** 应用允许用户输入当前纠结的问题,建议长度 1–200 个 Unicode 字符。 - **FR-Q-002** 问题字段必须有可见标签和隐私说明,不能仅使用占位文字。 - **FR-Q-003** MVP 不要求账号、手机号或身份信息。 -- **FR-Q-004** 问题在本地保存与否必须由用户可见的设置或保存动作决定,不得默认上传。 +- **FR-Q-004** 问题是否在本地长期保存必须由首页说明、可见设置和当次操作共同控制;任何本地保存状态都不得被解释为上传授权。 ### 5.2 投币与成卦 @@ -103,9 +103,13 @@ Brainwave 帮助用户把一件正在纠结的事放慢来看。应用保留三 ### 5.5 历史记录 - **FR-H-001** 历史记录属于 MVP 后半段能力;核心起卦不依赖它。 -- **FR-H-002** 保存时必须允许用户选择是否保留问题原文。 +- **FR-H-002** 设置提供“自动保存完整记录”总开关,以及问题原文、解读全文和行动记录三个内容开关;四项默认开启,总开关关闭时内容开关同步停用。 - **FR-H-003** 删除记录必须可撤销或经过确认。 -- **FR-H-004** 未获得单独授权时,历史记录只保存在应用私有存储中。 +- **FR-H-004** 历史记录保存在应用私有存储中,并默认排除 Android Auto Backup、设备迁移数据提取和云备份;未来导出、备份或同步必须单独设计并获得明确选择。 +- **FR-H-005** 第六爻确认并锁定结果后,若自动保存开启,应用必须立即创建历史会话;未完成草稿不得进入长期历史。自动保存关闭时,结果页仍提供“保存本次”。 +- **FR-H-006** 自动保存的完整记录包含可复核起卦快照,并按内容开关保存问题原文、当次解读全文和行动记录;设置默认值均为开启。 +- **FR-H-007** 首页必须持续说明“起卦与历史默认保存在本机,不主动上传。只有你选择 AI 解读时,本次所需内容才会发送。”并提供设置入口;结果页必须显示当次保存状态,并允许“本次不保存/删除本次记录”。 +- **FR-H-008** 当次稍后生成的本地或 AI 解读必须按设置关联到同一会话;重新解读不得静默覆盖既有解读,必须新增版本或由用户明确确认替换。自动保存不能触发或授权 AI 请求。 ## 6. 非功能需求 @@ -113,6 +117,7 @@ Brainwave 帮助用户把一件正在纠结的事放慢来看。应用保留三 - **NFR-DET-001** 相同的六次投币输入和同一数据版本必须产生完全相同的本卦、动爻和之卦。 - **NFR-SEC-001** 开发者 AI 密钥不得进入 APK、源码仓库或客户端日志。 - **NFR-PRI-001** 问题文本、解释全文和模型请求不得写入分析埋点或崩溃日志。 +- **NFR-PRI-002** Room 历史数据库及其辅助文件不得进入系统自动备份、设备迁移或任何未单独授权的云端副本。 - **NFR-A11Y-001** 正文对比度至少 4.5:1;交互目标至少 48×48dp;支持系统字体缩放和读屏。 - **NFR-PERF-001** 本地起卦为纯内存同步计算,用户确认后应即时完成,不显示伪加载动画。 - **NFR-RES-001** 旋转、切后台和可恢复的进程重建后保留当前流程状态。 @@ -139,5 +144,6 @@ MVP 可以交付的最低条件: 5. 用户能清楚分辨“经典/本地内容”“AI 解释”和“建议行动”。 6. 无障碍检查覆盖投币控件、卦象、动爻和长文阅读。 7. 64 卦及爻辞内容通过完整性、来源和授权门禁。 +8. 默认设置下完成起卦会生成一条仅在本机的完整记录;用户可以全局关闭、当次退出、单条删除或清空全部,且这些操作不会触发网络请求。 需求与验证方式的对应关系见[质量门禁](quality-gates.md#4-需求追踪矩阵)。 diff --git a/docs/prototype.md b/docs/prototype.md index 6f3635c..7265cbc 100644 --- a/docs/prototype.md +++ b/docs/prototype.md @@ -1,6 +1,6 @@ # 移动端交互原型 -> 状态:高保真候选 v0.1,待用户视觉与流程确认 +> 状态:高保真候选 v0.3;核心 v0.1 已确认,首页、问卦簿与默认本机保存流程已实现并待视觉复核 > 基准视口:360 × 792 CSS px(对应当前目标手机的紧凑竖屏体验) > 实现位置:[`prototype/`](../prototype/) @@ -15,6 +15,8 @@ 原型已经覆盖: - 欢迎与方法说明; +- 首次欢迎与回访首页两种状态; +- 首页唯一主操作、“最近一次”和问卦簿次级入口; - 开放式问题输入,以及明确的“不写具体内容”入口; - 三枚硬币逐枚录入、字/背计值、六轮 bottom-up 进度和撤销上一爻; - 本地确定性计算本卦、之卦和全部动爻; @@ -22,13 +24,17 @@ - 经典原文、本地白话和动爻的来源标签; - “解”的本地/AI 两种分支; - AI 发送范围说明、逐次同意、加载、成功、失败和本地降级; +- 首页本机保存/AI 发送边界说明与保存设置入口; +- 默认开启的完整记录自动保存、总开关与三个内容开关; +- 结果保存状态、当次不保存、自动保存关闭后的“保存本次”和解读关联反馈; +- 问卦簿列表、空状态、记录详情、单条删除和清空全部确认; - 减少动态效果、键盘焦点、语义标签和最小触控目标。 本轮不包含: - Android/Compose 生产代码; - 深色主题、平板和横屏定稿; -- 历史记录与设置页面; +- Room 真实持久化、历史迁移和系统 backup/data-extraction 配置; - 真实 AI 请求、账户、服务端或持久化; - 完整 64 卦授权内容包; - 正式名称、图标、字体授权和商店视觉。 @@ -72,6 +78,16 @@ node prototype\capture.mjs | AI 解读 | `?view=explanation` | 来源标签、非裁决语言、行动收束 | | 本地解读 | `?view=local` | 离线分支与来源区分 | | AI 失败 | `?view=error` | 结果保留、重试和本地降级 | +| 回访首页 | `?view=home` | 单一主操作、最近一次、问卦簿入口 | +| 回访首页空态 | `?view=home-empty` | 无记录时不显示空功能宫格 | +| 问卦簿 | `?view=history` | 倒序列表、问题/解释保存状态 | +| 问卦簿空态 | `?view=history-empty` | 空态说明与开始入口 | +| 历史详情 | `?view=history-detail` | 起卦快照、版本和已保存内容 | +| 保存设置默认态 | `?view=settings` | 四个开关默认开启、本机与备份边界 | +| 保存设置关闭态 | `?view=settings-off` | 总开关关闭、内容开关保留值并停用 | +| 本次未保存结果 | `?view=result-not-saved` | 未保存原因和“保存本次”恢复路径 | +| 删除确认 | `?view=delete` | 级联删除范围和再次确认 | +| 清空全部确认 | `?view=clear-all` | 全量删除范围、设置保持不变 | 这些参数只设置初始预览状态,不属于 Android 正式版路由。 @@ -114,6 +130,19 @@ node prototype\capture.mjs 失败状态不得隐藏或重算卦象,必须同时提供本地解释和重试入口。 +### 5.4 首页与历史 + +- 首次欢迎页继续保持单一“开始”操作;有历史的回访首页使用更紧凑的开场,并在主操作之后展示最近一次。 +- 首页不显示空的未来功能卡片。扩展区只渲染已上线入口;至少出现 3 个同级长期功能后才重新评估底部导航。 +- 首页显示:“起卦与历史默认保存在本机,不主动上传。只有你选择 AI 解读时,本次所需内容才会发送。”并提供“管理保存设置”。 +- 第六爻锁定后默认自动创建本机会话;结果页显示保存状态,并支持“本次不保存/删除本次记录”。总开关关闭时显示“本次未保存”和“保存本次”。 +- 设置页提供默认开启的总开关、问题原文、解读全文和行动记录开关;总开关关闭时其余控制同步停用。清空全部必须二次确认。 +- 本地或 AI 解读按设置加入同一会话;自动保存与 AI 同意完全独立,保存过程不得产生网络调用。 +- 问卦簿中的演示记录只存在内存。刷新会恢复预览夹具,不能把原型行为误称为 Room 已实现。 +- 删除必须经过确认;正式版级联删除会话、解释和行动记录。 + +这些状态在原型中使用内存模拟,刷新后恢复预览夹具;它们定义 Compose 的交互契约,但不代表 Room、DataStore 或备份排除已经实现。 + ## 6. 视觉系统 视觉遵循 [ADR-006](decisions.md#adr-006东方文化采用内容优先的纸墨朱砂设计): @@ -144,6 +173,18 @@ node prototype\capture.mjs 6. `06-ai-consent.png` 7. `07-ai-explanation.png` 8. `08-ai-error.png` +9. `09-home-returning.png` +10. `10-home-empty.png` +11. `11-history-list.png` +12. `12-history-empty.png` +13. `13-history-detail.png` +14. `14-settings-default.png` +15. `15-delete-confirm.png` +16. `16-settings-off.png` +17. `17-result-not-saved.png` +18. `18-clear-all-confirm.png` + +目录中的 `14-save-record.png` 与 `16-save-with-explanation.png` 是 v0.2 手动保存流程的历史评审证据,当前截图脚本不再生成,不能用于 v0.3 验收。 截图是 360 CSS px 宽度的评审证据,不是应用商店素材。 @@ -151,13 +192,15 @@ node prototype\capture.mjs 进入 Android P0/P3 前,需要用户明确反馈或确认: -- [ ] “纸、墨、朱砂、留白”的整体气质是否合适; -- [ ] 工作名只作占位,不把“一问”直接视为正式名称; -- [ ] 三枚硬币用圆形切换控件录入是否容易理解; -- [ ] 结果页先卦象、后原文/白话、再点击“解”的层级是否合适; -- [ ] 本地解读与 AI 解读的选择、同意和来源标签是否足够清楚; -- [ ] “可以试的一小步”的非预测、非裁决语气是否符合定位; -- [ ] 是否需要在下一轮原型加入历史记录、深色主题或更大字号状态。 +- [x] “纸、墨、朱砂、留白”的整体气质; +- [x] 工作名只作占位,不把“一问”直接视为正式名称; +- [x] 三枚硬币圆形切换控件与结果阅读层级; +- [x] 本地/AI 选择、同意门和非裁决式行动语气; +- [x] 产品已确认历史默认本机自动保存、可全局关闭和当次退出(ADR-012); +- [ ] 回访首页的“最近一次 / 问卦簿”层级是否保持了主流程焦点; +- [ ] v0.3 首页告知、默认开启设置、结果保存状态、当次退出和清空全部是否足够清楚; +- [ ] 问卦簿列表、空态、详情和删除确认是否适合进入 Compose 实现; +- [ ] 是否在下一轮原型加入深色主题或更大字号状态。 未确认项应继续停留在原型层修改;不应提前固化为 Compose 生产页面。 @@ -167,11 +210,13 @@ node prototype\capture.mjs |---|---| | CSS 颜色/间距/圆角令牌 | `MaterialTheme` 扩展与 design token | | `welcome/question/casting/result` | Navigation destination + 状态驱动页面 | +| `home/history/history-detail` | 顶层首页 + Room `Flow` 驱动的列表/详情 | | 三枚硬币控件 | 具备明确 semantics 的 48dp+ Compose 控件 | | 六爻图 | Canvas 绘制 + 完整读屏描述 | | 底部选择/同意层 | Material 3 modal bottom sheet/dialog | | 本地/AI/错误来源标签 | sealed UI state,不以颜色作为唯一提示 | | `prefers-reduced-motion` | 系统动画缩放/无障碍偏好适配 | +| 保存状态/设置/删除确认 | `HistorySavePolicy` 状态驱动 UI + 事务保存/级联删除用例 | Compose 实现必须从 `CastResult` 渲染,不能在 UI 层复制 King Wen 映射或根据文本猜测卦象。 @@ -179,12 +224,20 @@ Compose 实现必须从 `CastResult` 渲染,不能在 UI 层复制 King Wen - `node --check prototype/app.js`:通过。 - `node --check prototype/capture.mjs`:通过。 -- `node prototype/capture.mjs`:通过;八个状态完成 360px 截图与浏览器审计。 +- `node prototype/capture.mjs`:通过;十八个状态完成 360px 截图与浏览器审计。 - `git diff --check`:通过。 -- Chromium 360 × 792:欢迎、起念、录入、有/无动爻结果、AI 同意、成功与失败状态已渲染。 +- Chromium 360 × 792:核心流程、首页、历史、保存设置、自动保存/未保存状态、删除与清空确认均已渲染。 - 已知夹具 `9,8,8,8,8,8`:显示复 24 → 坤 2,初爻动。 - 静态夹具 `7,7,7,7,7,7`:显示乾 1,无动爻且无之卦。 - 浏览器控制台错误:0;运行时异常:0;外部请求:0;模型调用:0。 -- 八个状态的未命名按钮、低于 48px 的按钮触控目标、过小复选框标签:均为 0。 +- 首页只有 1 个主流程开始按钮且没有底部导航;问卦簿演示列表为 3 条,删除确认后为 2 条。 +- 四个保存开关默认开启;关闭总开关后三个内容开关保留值并停用。 +- 结果默认显示自动保存状态;“本次不保存”删除当次记录后可重新“保存本次”,且不会改变全局设置。 +- AI 解读生成后显示“解读已加入本次记录”;保存、设置、历史与删除流程产生的外部请求均为 0。 +- 清空全部会删除三条演示记录、保留自动保存设置,并返回可操作的设置页。 +- 375×812、430×932 与 792×360:无水平溢出,主内容可达;回访首页前进/返回恢复正确。 +- `prefers-reduced-motion: reduce`:页面动效压缩至近零时长。 +- 新增成功、危险和次级文字组合的对比度均不低于 4.97:1。 +- 十八个状态的未命名按钮、低于 48px 的按钮触控目标、过小开关/复选框标签:均为 0。 -原型验证不能替代 P1 的 4,096 组合领域测试,也不能替代 Android 真机、TalkBack 与最大字体测试。 +原型验证不能替代 P1 的 4,096 组合领域测试,也不能替代 Android 真机、TalkBack 与最大字体测试。v0.3 已满足 ADR-012 的原型门禁,但仍需用户完成视觉复核后才能映射到 Compose。 diff --git a/docs/quality-gates.md b/docs/quality-gates.md index 0f54e75..c1a7516 100644 --- a/docs/quality-gates.md +++ b/docs/quality-gates.md @@ -43,15 +43,23 @@ ### 3.0 原型验证(P-1 阻塞视觉确认) +当前 v0.3 原型已经同步 ADR-012;以下门禁既覆盖核心起卦流程,也覆盖默认本机自动保存策略。 + - `node --check prototype/app.js` 无语法错误。 - `node --check prototype/capture.mjs` 无语法错误。 - `git diff --check` 无空白错误。 - 本地服务运行时,`node prototype\capture.mjs` 退出码为 0。 -- Chromium 360 × 792 走通问题输入、六次录入、结果和解释选择。 +- Chromium 360 × 792 走通首页、问题输入、六次录入、结果、解释选择和问卦簿。 - `9,8,8,8,8,8` 显示复 24 → 坤 2、初爻动;全 7 显示乾 1、无动爻且无之卦。 -- 欢迎、起念、录入、有/无动爻结果、AI 同意、解释和失败状态均有可复现截图。 +- 十八个状态均有可复现截图,包括回访首页/空态、历史列表/空态/详情、保存设置开/关、自动保存/本次未保存、删除和清空确认。 +- 回访首页只有一个开始主操作,不出现底部导航或未上线功能占位。 +- 首页持续显示本机保存/AI 发送边界;四个保存设置默认开启,总开关关闭后内容开关保留值并停用。 +- 完成起卦显示自动保存状态;当次不保存、重新保存、解释关联和清空全部均可复现,删除前显示范围并再次确认。 +- 375×812、430×932 和 792×360 无水平溢出;减少动态效果媒体偏好生效。 - 浏览器控制台无应用错误;页面不发起外部请求;AI 状态明确标为本地模拟。 +自动化审计必须同时断言:以上保存、设置、历史和删除动作产生的外部请求为 0,且 AI 同意门仍须用户单独勾选。 + 原型通过只表示流程可供评审,不替代纯 Kotlin 领域测试、Compose UI 测试或真机无障碍检查。 ### 3.1 纯 JVM 领域测试(最快,阻塞合并) @@ -72,7 +80,14 @@ - 只在用户点击且同意后调用 AI。 - 并发点击、取消和超时不重复提交。 - Room 保存/读取一致性。 -- 删除与撤销/确认行为。 +- `autoSaveHistory`、问题、解读和行动四个设置的默认值均为 `true`,且总开关关闭时不自动写入。 +- 第六爻锁定只创建一个 session,未完成草稿创建零条;总开关关闭时“保存本次”仍能创建一条。 +- 问题、解读和行动内容开关的组合写入准确;当次不保存会删除已自动创建的 session,且不改变全局设置。 +- 随后产生的本地/AI 解读关联到同一 session;重新解读不会静默覆盖旧版本。 +- 自动保存开启、读取历史或切换保存设置时,AI gateway 收到零次调用;AI 同意状态不被保存设置改变。 +- “最近一次”查询随插入/删除更新,且不复制敏感正文。 +- 单条删除、清空全部与 session → explanation/action 的级联行为。 +- backup/data-extraction 配置测试确认 Room 数据库及 `-wal`、`-shm` 辅助文件不在系统备份或设备迁移范围。 测试优先使用接口的 fake 实现,不依赖真实网络和实时模型。 @@ -82,6 +97,10 @@ - 六轮录入的进度、按钮启用状态和错误恢复。 - 结果页本卦/之卦/动爻语义。 - AI 同意、加载、失败、本地降级。 +- 回访首页、最近一次、问卦簿列表/空态/详情和删除确认。 +- 首页本机保存/AI 发送说明和设置入口始终可达。 +- 保存设置默认开启、总开关联动停用、结果页自动保存状态、当次不保存/保存本次和解释关联反馈。 +- 清空全部的危险操作样式、范围说明和二次确认。 - 最大字体下关键操作可到达。 - TalkBack 语义、焦点顺序和触控目标。 - 旋转/横屏/大屏布局。 @@ -93,6 +112,7 @@ - 使用已知六爻夹具人工核对卦象绘制和文本。 - 后端 staging 环境验证同意、超时、限流、非法响应和安全分支。 - 验证安装包不包含生产密钥和未授权内容。 +- 从系统备份/设备迁移恢复后验证问卦簿为空,且设置页未暗示历史已经云端备份。 ## 4. 需求追踪矩阵 @@ -107,7 +127,7 @@ | FR-R-005 | 内容 schema/授权清单检查 | 内容负责人签核 | | FR-E-001~003 | 网络调用次数与同意状态测试 | 首次同意流程 | | FR-E-004~006 | 输出 schema + 安全用例集 | 安全/产品审核 | -| FR-H-001~004 | Room、删除、权限边界测试 | 隐私设置与删除体验 | +| FR-H-001~008 | 保存策略、Room 事务/版本、删除、备份排除、零网络调用测试 | 首页告知、设置、当次退出与删除体验 | | NFR-OFF-001 | fake/offline 集成测试 | 飞行模式真机 | | NFR-DET-001 | 4,096 组合属性测试 | 无 | | NFR-SEC/PRI | secret scan、日志测试 | APK/代理抓包复核 | @@ -122,6 +142,7 @@ - `data.ai` 不得依赖或调用 `CastEngine`。 - feature UI 不得引用 DAO 或网络 DTO。 - 生产源码不得出现 API key 形态、真实用户问题测试样例或 HTTP body logger。 +- backup/data-extraction 配置不得包含历史 Room 数据库或其辅助文件。 - 内容校验任务验证 64 卦、384 条爻辞、唯一模式、来源和许可证字段。 - 文档链接和需求编号无悬空引用。 diff --git a/docs/ux-design.md b/docs/ux-design.md index 1e381f7..606f320 100644 --- a/docs/ux-design.md +++ b/docs/ux-design.md @@ -21,11 +21,15 @@ ## 2. 信息架构 -MVP 顶层不需要底部导航。主流程是线性的,使用单 Activity 和清晰的返回行为: +MVP 顶层不需要底部导航。首次欢迎与回访首页是不同状态;核心流程仍保持线性,并使用单 Activity 和清晰的返回行为: ```text -欢迎/方法说明 -└── 起念 +首次欢迎/方法说明 +└── 回访首页 + ├── 开始一问 → 起念 + └── 最近一次 / 问卦簿 + +起念 └── 投币(1/6…6/6) └── 结果 ├── 本地解释 @@ -54,7 +58,26 @@ MVP 顶层不需要底部导航。主流程是线性的,使用单 Activity 和 主操作只有一个:“开始”。“查看方法”是次级文本操作,不制造必须完成的教程轮播。 -### 3.2 起念 +### 3.2 回访首页与功能扩展 + +目的:让回访用户快速开始新的一问,同时能回看默认保存在本机、由自己控制的记录。 + +首页层级固定为: + +1. 唯一主操作“开始一问”; +2. “你的观照”次级区域; +3. 存在记录时显示最近一次及“查看问卦簿”;无记录时显示简短空态和隐私说明。 + +扩展规则: + +- 不展示“敬请期待”、禁用卡片或空宫格;只有已上线且用户可进入的功能才出现。 +- MVP 只显示问卦簿,不为了预留而引入底部导航。 +- 当出现至少 3 个长期、同级、需要频繁切换的顶层功能时,才评估带文字标签的底部导航;最多 5 项。 +- “最近一次”由历史查询派生,不复制问题或解释正文;记录删除后首页同步显示下一条或空态。 +- 最近卡片必须同时显示日期、卦名、动爻概况和解释来源;问题因设置或当次选择未保存时明确写“未保存问题原文”。 +- 首页在主操作附近持续显示:“起卦与历史默认保存在本机,不主动上传。只有你选择 AI 解读时,本次所需内容才会发送。”末尾提供可点击的“管理保存设置”,但不能把设置做成开始流程的前置门。 + +### 3.3 起念 页面标题可使用“此刻,你在为何事迟疑?”。问题输入框有固定标签“正在纠结的事”,辅文提示尽量描述事实、选择和顾虑,不要求生日、性别等无关信息。 @@ -65,7 +88,7 @@ MVP 顶层不需要底部导航。主流程是线性的,使用单 Activity 和 - 默认不把问题发送到网络; - 用户可选择“不写具体内容,直接开始”,最终是否允许空值见[决策记录](decisions.md#未决问题)。 -### 3.3 投币 +### 3.4 投币 页面始终显示: @@ -79,7 +102,7 @@ MVP 顶层不需要底部导航。主流程是线性的,使用单 Activity 和 每次确认可以有一次轻触觉反馈和 150–250ms 的爻线出现动画。不得使用持续摇晃、金币飞散、音效倒计时或强制等待。开启“减少动态效果”时直接更新状态。 -### 3.4 成卦与观象 +### 3.5 成卦与观象 信息优先级: @@ -93,7 +116,7 @@ MVP 顶层不需要底部导航。主流程是线性的,使用单 Activity 和 原文、本地白话和 AI 解释必须有明确标签,不使用视觉相似但来源不明的段落混排。 -### 3.5 解读与落事 +### 3.6 解读与落事 「解」是结果页唯一主操作。点击后先选择或显示当前方式: @@ -104,6 +127,39 @@ AI 加载超过 300ms 时显示内联进度;按钮在请求期间禁用,避 结尾固定使用“可以试的一小步”区域,包含时间范围和撤销方式;它是建议,不是判词。 +### 3.7 保存与问卦簿 + +第六爻确认并生成结果后,默认在后台立即创建本机会话。结果页在不打断阅读的位置显示“已保存到本机问卦簿”,并提供次级操作“本次不保存/删除本次记录”;它们不能抢占结果页唯一主操作「解」。自动保存关闭时,同一位置显示“本次未保存”和“保存本次”。 + +设置页必须提供以下控制: + +- “自动保存完整记录”,默认开启; +- “保存问题原文”,默认开启; +- “保存解读全文”,默认开启; +- “保存行动记录”,默认开启; +- “清空全部问卦记录”,使用危险操作样式并再次确认。 + +关闭总开关后,三个内容开关保持其值但在界面中同步停用,便于再次开启时恢复用户偏好。总开关不影响未完成草稿的进程恢复,也不能改变 AI 同意状态。 + +保存边界必须分层说明: + +- 固定快照包含 18 枚币面、六爻结果、动爻、方法版本和内容版本; +- 问题原文、当次解读和行动记录由各自开关控制; +- 未完成问题与投币只用于当前流程恢复,不进入问卦簿; +- 存储范围是应用私有本机存储,默认不进入系统备份、设备迁移或云同步; +- 本地保存与 AI 发送是两个独立决定,自动保存绝不触发网络请求。 + +本地或 AI 解读在生成后应自动关联到当次会话,并给出轻量的“已加入本次记录”反馈。重新解读默认新增版本;若产品只保留一个版本,覆盖前必须让用户明确确认。 + +问卦簿要求: + +- 列表按保存时间倒序,展示卦名、动爻概况、问题保存状态和解释来源; +- 空状态说明“完成起卦后会默认保存在本机;你可以在设置中关闭”,并提供“开始第一次记录”和“管理保存设置”; +- 详情展示保存时的卦象快照、问题状态、解释来源、方法/内容版本和可选行动; +- 删除使用明确确认对话框,并级联删除该会话关联的解释和行动记录; +- “清空全部”必须在设置或问卦簿菜单中二次确认,并清楚说明不可恢复范围; +- 返回列表后保留原滚动位置;从问卦簿开始新流程时,返回行为不得跳到首次欢迎页。 + ## 4. 视觉系统 ### 4.1 风格关键词 diff --git a/prototype/README.md b/prototype/README.md index 7e857e4..44c7012 100644 --- a/prototype/README.md +++ b/prototype/README.md @@ -10,17 +10,20 @@ python -m http.server 4173 --bind 127.0.0.1 打开 `http://127.0.0.1:4173/prototype/`。完整状态入口、领域边界、评审清单和 Android 映射见 [`docs/prototype.md`](../docs/prototype.md)。 -服务保持运行时,可用当前系统的 Chrome/Edge 重新生成八张截图并执行浏览器审计: +服务保持运行时,可用当前系统的 Chrome/Edge 重新生成 18 张截图并执行浏览器审计: ```powershell node prototype\capture.mjs ``` -脚本使用 Node 22 自带的 WebSocket 直接连接 Chrome DevTools,不安装 npm 包;它会核对两个已知卦象、AI 同意门、控制台/外部请求、按钮名称和 48px 最小触控目标。任一门禁失败时退出码为非零。 +脚本使用 Node 22 自带的 WebSocket 直接连接 Chrome DevTools,不安装 npm 包;它会生成 18 个评审状态,并核对两个已知卦象、AI 同意门、首页层级、历史列表/空态、保存设置、自动保存/当次退出、解读关联、删除/清空流程、控制台/外部请求、按钮名称和 48px 最小触控目标。任一门禁失败时退出码为非零。 + +当前代码是 v0.3 高保真候选,已经同步 ADR-012 的默认本机自动保存策略;所有记录和设置仍只存在于浏览器内存,用于评审而非真实持久化。 约束: - 不发起外部请求,不调用 AI; - 不生成随机卦,用户逐枚录入硬币; - 查询参数只用于稳定预览和截图; +- 问卦簿使用内存演示数据,刷新会恢复夹具,不代表 Room 已实现; - 工作名“一问”与当前文字内容均为评审候选,不代表正式命名或内容授权完成。 diff --git a/prototype/app.js b/prototype/app.js index c32813d..c60cb2a 100644 --- a/prototype/app.js +++ b/prototype/app.js @@ -3,6 +3,32 @@ const SAMPLE_QUESTION = "我是否要接受这次工作调整?怎样做能更稳妥?"; const SAMPLE_LINES = [9, 8, 8, 8, 8, 8]; + const SAMPLE_HISTORY = [ + { + id: "sample-1", + date: "今天 · 21:10", + lines: [9, 8, 8, 8, 8, 8], + question: SAMPLE_QUESTION, + explanationSource: "AI 生成", + action: "写下三条已知事实,再约一次不超过 20 分钟的信息沟通。" + }, + { + id: "sample-2", + date: "昨天 · 07:40", + lines: [8, 8, 7, 8, 8, 8], + question: null, + explanationSource: "本地解读", + action: null + }, + { + id: "sample-3", + date: "7 月 30 日 · 22:15", + lines: [7, 7, 8, 8, 7, 6], + question: "这段合作应该怎样调整边界?", + explanationSource: null, + action: "先把需要确认的职责写成一页清单。" + } + ]; const HEXAGRAM_NAMES = [ "", "乾", "坤", "屯", "蒙", "需", "讼", "师", "比", "小畜", "履", "泰", "否", @@ -29,7 +55,8 @@ lock: '', arrow: '', chevron: '', - shield: '' + shield: '', + book: '' }; const screen = document.querySelector("#screen"); @@ -45,9 +72,27 @@ modal: null, consent: false, explanationSource: "ai", - aiError: false + aiError: false, + historyRecords: [], + selectedRecordId: null, + autoSaveHistory: true, + saveQuestion: true, + saveExplanation: true, + saveAction: true, + saved: false, + currentRecordId: "current-session", + currentSessionOptedOut: false, + currentSaveMode: null, + explanationGenerated: false, + questionOrigin: "welcome", + settingsOrigin: "home", + deleteReturnScreen: "history" }; + function sampleHistory() { + return SAMPLE_HISTORY.map((record) => ({ ...record, lines: [...record.lines] })); + } + function escapeHtml(value) { return String(value) .replaceAll("&", "&") @@ -103,6 +148,59 @@ }; } + function hasSavableQuestion() { + return Boolean(state.question && !state.question.startsWith("未记录具体问题")); + } + + function generatedExplanationSource() { + if (!state.explanationGenerated || !state.saveExplanation) return null; + return state.explanationSource === "ai" ? "AI 生成" : "本地解读"; + } + + function currentSessionRecord() { + return state.historyRecords.find((record) => record.id === state.currentRecordId); + } + + function persistCurrentSession(mode = "manual") { + if (state.lines.length !== 6) return; + const data = resultData(); + const existing = currentSessionRecord(); + const record = { + id: state.currentRecordId, + date: "刚刚", + lines: [...data.sourceLines], + question: state.saveQuestion && hasSavableQuestion() ? state.question : null, + explanationSource: generatedExplanationSource(), + action: state.explanationGenerated && state.saveAction + ? "写下三条已知事实,再进行一次短沟通。" + : null + }; + + if (existing) Object.assign(existing, record); + else state.historyRecords.unshift(record); + state.saved = true; + state.currentSessionOptedOut = false; + state.currentSaveMode = mode; + } + + function attachExplanationToCurrentSession() { + const record = currentSessionRecord(); + if (!record || !state.saved) return; + if (state.saveExplanation) record.explanationSource = generatedExplanationSource(); + if (state.saveAction) { + record.action = "写下三条已知事实,再进行一次短沟通。"; + } + } + + function prepareNewSession() { + state.currentRecordId = `current-session-${Date.now()}-${state.historyRecords.length}`; + state.saved = false; + state.currentSessionOptedOut = false; + state.currentSaveMode = null; + state.explanationGenerated = false; + state.aiError = false; + } + function stepper(current, total) { const progress = Math.round((current / total) * 100); return ` @@ -141,6 +239,259 @@ `; } + function historyRecordData(record) { + const changedLines = transformLines(record.lines); + const movingIndexes = record.lines + .map((value, index) => lineIsMoving(value) ? index : -1) + .filter((index) => index >= 0); + return { + primary: lookupHexagram(record.lines), + changed: lookupHexagram(changedLines), + changedLines, + movingIndexes + }; + } + + function recordRoute(record) { + const data = historyRecordData(record); + return data.movingIndexes.length + ? `${data.primary.name} 之 ${data.changed.name}` + : data.primary.name; + } + + function recentRecordCard(record) { + const data = historyRecordData(record); + return ` + `; + } + + function homeView() { + const latest = state.historyRecords[0]; + return ` +
+
+

慢一点,看清当下

+

此刻,有什么想安静看清?

+

你投币,应用记录;结果用于整理想法,不替你预测或决定。

+ +
+ +
+
+

你的观照

+ +
+ ${latest ? recentRecordCard(latest) : ` + `} + +
+
`; + } + + function historyCard(record) { + const data = historyRecordData(record); + return ` +
  • + +
  • `; + } + + function historyView() { + const hasRecords = state.historyRecords.length > 0; + return ` +
    +

    本机记录

    +
    +

    问卦簿

    + ${hasRecords ? `${state.historyRecords.length} 条` : ""} +
    +

    回看当时看见了什么,而不是用旧结果替今天做决定。

    + +
    + ${icons.lock} + 记录只在本机,默认排除系统备份与设备迁移;你可随时删除。 +
    + + ${hasRecords ? ` +
      + ${state.historyRecords.map(historyCard).join("")} +
    ` : ` +
    + +

    还没有保存的记录

    +

    ${state.autoSaveHistory ? "完成一次起卦后,会默认把问题、卦象与后续解读保存在本机。" : "自动保存已经关闭;完成后仍可在结果页选择保存本次。"}

    + +
    `} + +
    + +
    +
    `; + } + + function settingRow({ action, title, description, checked, disabled = false, detail = false }) { + return ` + `; + } + + function settingsView() { + const detailsDisabled = !state.autoSaveHistory; + return ` +
    +

    本机与隐私

    +

    保存设置

    +

    默认留住完整的反思过程;你可以全局关闭,也可以只调整保存内容。

    + +
    + ${icons.shield} +
    + 只保存在 App 私有存储 +

    历史不会主动上传,并默认排除系统备份、设备迁移与云同步。

    +
    +
    + +
    + ${settingRow({ + action: "auto-save-toggle", + title: "自动保存完整记录", + description: state.autoSaveHistory ? "已开启 · 完成起卦后立即保存到本机" : "已关闭 · 结果页仍可手动保存本次", + checked: state.autoSaveHistory + })} +
    + ${settingRow({ + action: "save-question-setting", + title: "保存问题原文", + description: "与你写下的卦象背景一同回顾", + checked: state.saveQuestion, + disabled: detailsDisabled, + detail: true + })} + ${settingRow({ + action: "save-explanation-setting", + title: "保存解读全文", + description: "本地或 AI 解读会加入同一次记录", + checked: state.saveExplanation, + disabled: detailsDisabled, + detail: true + })} + ${settingRow({ + action: "save-action-setting", + title: "保存行动记录", + description: "保留“可以试的一小步”与完成状态", + checked: state.saveAction, + disabled: detailsDisabled, + detail: true + })} +
    +
    + +

    这些开关只控制本机历史,不会允许或触发 AI。每次 AI 解读仍需要你主动选择并确认发送范围。

    + +
    +
    +

    管理本机记录

    +

    当前原型会话中有 ${state.historyRecords.length} 条记录。

    +
    + +
    +
    `; + } + + function historyDetailView() { + const record = state.historyRecords.find((item) => item.id === state.selectedRecordId); + if (!record) return historyView(); + const data = historyRecordData(record); + const hasMoving = data.movingIndexes.length > 0; + return ` +
    +
    +

    ${record.date}

    +

    ${recordRoute(record)}

    +

    保存的起卦快照

    +
    + +
    +
    coin-v1 · 本地快照${hasMoving ? `${data.movingIndexes.length} 个动爻` : "无动爻"}
    +
    + ${hexagramFigure(record.lines, data.primary, true)} + ${hasMoving ? `
    之
    ${hexagramFigure(data.changedLines, data.changed, false)}` : ""} +
    +
    + +
    +

    当时所问

    ${record.question ? "已保存" : "未保存"}
    +

    ${escapeHtml(record.question ?? "这条记录没有保存问题原文。卦象结果和方法版本仍可独立复核。")}

    +
    + +
    +

    保存内容

    +
    +
    起卦结果
    ${data.primary.name}${hasMoving ? `之${data.changed.name}` : ""} · ${data.movingIndexes.length ? data.movingIndexes.map((index) => lineName(record.lines[index], index)).join("、") : "无动爻"}
    +
    解释来源
    ${record.explanationSource ?? "未保存解释"}
    +
    版本
    coin-v1 · content-demo
    +
    +
    + + ${record.action ? ` +
    +
    当时记下的一小步

    ${escapeHtml(record.action)}

    +
    ` : ""} + +
    + +

    删除前会再次确认;原型刷新后恢复演示数据。

    +
    +
    `; + } + function questionView() { return `
    @@ -158,7 +509,7 @@ data-question-input >${escapeHtml(state.question)}
    - 只保存在本次原型会话中 + ${state.autoSaveHistory ? "完成起卦后默认保存在本机" : "自动保存已关闭,本次可稍后手动保存"} ${state.question.length}/120
    @@ -307,6 +658,39 @@ `; } + function currentSavePanel({ compact = false } = {}) { + if (state.saved) { + const explanationAttached = state.explanationGenerated && state.saveExplanation; + const savedContext = state.saveQuestion && hasSavableQuestion() ? "问题、卦象" : "卦象"; + return ` +
    +
    + ${icons.book} + + ${explanationAttached ? "解读已加入本次记录" : state.currentSaveMode === "automatic" ? "已自动保存到本机" : "已保存到本机问卦簿"} + ${explanationAttached ? `与${savedContext}归在同一次观照中` : `${savedContext}快照已进入问卦簿`} + +
    +
    + + +
    +
    `; + } + + return ` +
    +
    + ${icons.lock} + + 本次未保存 + ${state.currentSessionOptedOut ? "你已选择不保留这次记录" : "自动保存已关闭;不会写入问卦簿"} + +
    + +
    `; + } + function resultView() { const data = resultData(); const copy = resultCopy(data); @@ -365,7 +749,10 @@ ` : ""}
    - +
    + ${currentSavePanel()} + +
    + ${currentSavePanel({ compact: true })} +
    `; } + function deleteRecordModal() { + return ` + `; + } + + function clearAllModal() { + return ` + `; + } + function interpretationModal() { return `