# Eryao 工程计划:历史解卦与个人档案后端化(单一数据源) 日期:2026-04-05 状态:规划中(Planning Only) ## 0. 约束与决策前提 本计划基于已确认前提: 1. 当前无生产兼容压力,旧字段可直接不兼容。 2. 前端只做缓存层,不做权威数据源。 3. `ui_schema` 属于通用迁移遗留,不在本项目范围,目标是移除。 4. 头像存储必须使用 `avatars` bucket(`config.storage.avatar.bucket`)。 --- ## 1. 目标 在不引入额外业务表的前提下,完成以下工程目标: 1. assistant 消息落库时,`metadata.agent_output` 持久化完整 `divination_derived`。 2. `GET /api/v1/agent/history` 返回前端可直接消费的 `assistant.agent_output`(移除 `ui_schema`)。 3. 新增 profile 后端 API,前端设置页改为后端读写。 4. 头像上传改为预签名 + `avatars` bucket,后端校验路径和类型。 --- ## 2. 系统边界与职责 ### 2.1 边界图 ```text [Flutter App] | Auth Token v [API Router v1] |---- /agent/runs + /agent/history |---- /users/me/profile + /users/me/avatar/upload-url v [Service Layer] |---- AgentService: 会话、历史、消息转换 |---- UserProfileService: 档案读写、头像签名 v [Repository Layer] |---- sessions/messages/profiles CRUD v [Postgres + Supabase Storage] |---- messages.metadata_json |---- profiles |---- bucket: avatars ``` ### 2.2 分层职责 - Router:参数校验、鉴权入口、RFC7807 错误转换。 - Service:业务规则与信任边界控制。 - Repository:纯查询和写入,不做鉴权决策。 - Schema:协议强类型、禁止松散 dict 漂移。 --- ## 3. 数据流设计 ## 3.1 解卦写入链路(新增 `divination_derived`) ```text POST /agent/runs -> Runner emit DIVINATION_DERIVED(divination) -> StageEmitter merge into TEXT_MESSAGE_END payload -> EventStore picks worker_output_fields -> metadata.agent_output.divination_derived persisted -> messages.metadata_json ``` ### 关键点 1. `AgentOutput` 增加 `divination_derived` 强类型字段。 2. `EventStore` 字段白名单纳入 `divination_derived`。 3. `extra="forbid"` 保留,防止脏字段入库。 ## 3.2 历史读取链路(移除 `ui_schema`) ```text GET /agent/history -> AgentService.get_history_snapshot -> convert_message_to_history user -> content + attachments assistant -> content + agent_output -> HistoryMessage response ``` ### 关键点 1. 停止 `ui_hints -> ui_schema` 编译。 2. assistant 返回受控 `agent_output` 子集,不透传任意 metadata。 3. 前端结果页以 `agent_output.divination_derived` 为主数据源。 ## 3.3 Profile 与头像链路 ```text GET /users/me/profile -> read profiles PATCH /users/me/profile -> validate payload -> update profiles POST /users/me/avatar/upload-url -> validate mime/size/path -> create signed upload url (bucket=avatars) ``` --- ## 4. API 契约(冻结版) ## 4.1 History 响应(目标结构) ```json { "scope": "history_day", "threadId": "uuid", "day": "2026-04-05", "hasMore": false, "messages": [ { "id": "uuid", "seq": 12, "role": "assistant", "content": "...", "timestamp": "2026-04-05T12:34:56Z", "agent_output": { "sign_level": "中上签", "summary": "...", "conclusion": ["..."], "focus_points": ["..."], "advice": ["..."], "keywords": ["..."], "answer": "...", "divination_derived": { "binaryCode": "101001", "changedBinaryCode": "100001", "guaName": "...", "targetGuaName": "...", "ganzhi": {}, "yaoInfoList": [] } } } ] } ``` 说明: - 本接口不再返回 `ui_schema`。 - user 消息仍可返回 `attachments`。 ## 4.2 Profile API ### `GET /api/v1/users/me/profile` ```json { "user_id": "uuid", "display_name": "string", "bio": "string", "avatar_path": "avatars/{user_id}/...", "avatar_url": "https://...", "updated_at": "..." } ``` ### `PATCH /api/v1/users/me/profile` 请求: ```json { "display_name": "string<=30", "bio": "string<=200", "avatar_path": "avatars/{user_id}/..." } ``` ### `POST /api/v1/users/me/avatar/upload-url` 请求: ```json { "mime_type": "image/png", "file_size": 123456, "ext": "png" } ``` 响应: ```json { "bucket": "avatars", "path": "avatars/{user_id}/{uuid}.png", "upload_url": "https://...", "expires_in": 600 } ``` --- ## 5. 信任边界与安全规则 1. `user_id` 只能取 JWT `sub`,禁止客户端传 owner。 2. 头像 path 必须前缀匹配:`avatars/{current_user.id}/`。 3. bucket 必须等于 `config.storage.avatar.bucket`。 4. mime 白名单:`image/png|image/jpeg|image/webp`。 5. size 上限:`config.storage.avatar.max_size_mb`。 6. history 读取严格校验 session owner。 7. 错误统一 RFC7807 + `code`。 --- ## 6. 失败模式与处理 ## 6.1 消息落库阶段 1. `divination_derived` 校验失败 - 行为:拒绝写入该字段并记录结构化日志。 - 错误码:`AGENT_OUTPUT_DIVINATION_INVALID`(新)。 2. TEXT_MESSAGE_END 缺失关键字段 - 行为:整条 assistant 消息按失败路径处理,不写半残对象。 ## 6.2 history 读取阶段 1. `agent_output` 缺失或损坏 - 行为:assistant 消息返回 `content`,并标记 `agent_output=null`。 - 前端:展示“历史记录不完整”提示,不崩溃。 2. 非 owner 访问 - 行为:403,`code=AGENT_SESSION_FORBIDDEN`。 ## 6.3 头像上传阶段 1. bucket/path 越权 - 422,`AVATAR_PATH_SCOPE_INVALID`。 2. mime/size 非法 - 422,`AVATAR_FILE_INVALID`。 3. storage 签名失败 - 502,`AVATAR_SIGNED_URL_FAILED`。 --- ## 7. 关键边缘场景 1. 用户连续点击“保存资料”两次: - 以后端最后一次写入为准,前端按钮防抖。 2. 上传头像成功但 profile 更新失败: - 前端重试 profile PATCH,不重复上传。 3. history 返回空列表: - 前端展示空态,不触发本地假数据。 4. 助手消息存在但缺 `divination_derived`: - 卡片可展示摘要,不允许进入完整结果页。 5. 解卦完成后 history 立即读取: - 允许短暂读到旧快照,前端做一次重拉。 --- ## 8. 技术取舍 ### 方案 A(推荐):在现有 messages.metadata 扩展 - 优点: - 最小变更,不新增表。 - 复用当前会话与历史体系。 - 缺点: - metadata 体积增大,需要关注单条消息大小。 ### 方案 B:新增 `divination_results` 独立表 - 优点: - 结构更纯,查询更明确。 - 缺点: - 迁移、回写、关联复杂度明显增加。 结论: - 当前阶段选 A,满足速度与复杂度平衡。 --- ## 9. 实施切片(按风险顺序) ### Slice 1:协议与 schema 1. 更新协议文档:history + profile + 错误码。 2. 更新 `AgentOutput` 模型字段。 ### Slice 2:写链路改造 1. runner/emitter/store 打通 `divination_derived` 落库。 2. 增加单元测试与集成测试。 ### Slice 3:读链路改造 1. history 转换改为返回 `agent_output`。 2. 移除 `ui_schema` 响应字段。 ### Slice 4:profile API + 头像 1. users 路由、service、schema。 2. 头像 upload-url 接口。 ### Slice 5:前端切换 1. 历史列表/详情改消费后端 `agent_output`。 2. 设置页改 profile 接口。 3. 清理本地真源。 --- ## 10. 测试覆盖计划 ## 10.1 后端测试矩阵 ### A. AgentOutput 落库 1. `divination_derived` 正常写入。 2. `divination_derived` 非法结构拒绝写入。 ### B. history 接口 1. assistant 返回 `agent_output`。 2. 响应不含 `ui_schema`。 3. 非 owner 403。 4. 空历史返回空数组。 ### C. profile 接口 1. GET 返回当前用户档案。 2. PATCH 字段边界(空、超长、非法字符)。 3. 并发 PATCH 最终一致性。 ### D. avatar upload-url 1. 合法 mime/size/path 成功签名。 2. bucket/path 越权失败。 3. mime/size 超限失败。 4. storage 异常返回 502 问题体。 ## 10.2 前端测试矩阵 1. history 列表从接口渲染。 2. 点击历史项进入结果页并解析 `divination_derived`。 3. profile 读写回显。 4. 头像上传后刷新显示。 5. 异常提示(网络失败、数据缺失)不崩溃。 --- ## 11. 可观测性 新增日志字段建议: 1. history 响应统计:`thread_id`, `message_count`, `assistant_with_agent_output_count`。 2. profile 更新:`user_id`, `updated_fields`。 3. avatar 签名:`user_id`, `mime_type`, `file_size`, `success/failure_code`。 指标建议: 1. `history_agent_output_missing_rate`。 2. `avatar_upload_url_failure_rate`。 3. `profile_patch_error_rate`。 --- ## 12. 风险与回滚 ### 风险 1. 单条 metadata 变大,可能影响查询性能。 2. 前端解析新结构时存在字段名误配风险。 ### 回滚 1. 若读链路异常,先回滚 history 输出层(保持落库不回滚)。 2. profile 接口异常时,可临时只读禁写,保护账户信息。 --- ## 13. 验收标准(Done) 1. 新产生 assistant 消息均含 `metadata.agent_output.divination_derived`。 2. history 接口返回 `agent_output`,且不再返回 `ui_schema`。 3. 前端历史页与结果页不依赖本地真源。 4. profile 读写和头像上传全走后端。 5. 测试矩阵项全部落地并通过。 --- ## 14. NOT in Scope 1. 大规模清理 `backend/src/schemas/domain/**`。 2. 历史数据回填脚本。 3. 新增独立 `divination_results` 表。