Files
eryao/docs/plans/2026-04-05-divination-history-profile-eng-plan.md
T

9.5 KiB
Raw Blame History

Eryao 工程计划:历史解卦与个人档案后端化(单一数据源)

日期:2026-04-05
状态:规划中(Planning Only

0. 约束与决策前提

本计划基于已确认前提:

  1. 当前无生产兼容压力,旧字段可直接不兼容。
  2. 前端只做缓存层,不做权威数据源。
  3. ui_schema 属于通用迁移遗留,不在本项目范围,目标是移除。
  4. 头像存储必须使用 avatars bucketconfig.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 边界图

[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

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

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 与头像链路

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 响应(目标结构)

{
  "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

{
  "user_id": "uuid",
  "display_name": "string",
  "bio": "string",
  "avatar_path": "avatars/{user_id}/...",
  "avatar_url": "https://...",
  "updated_at": "..."
}

PATCH /api/v1/users/me/profile

请求:

{
  "display_name": "string<=30",
  "bio": "string<=200",
  "avatar_path": "avatars/{user_id}/..."
}

POST /api/v1/users/me/avatar/upload-url

请求:

{
  "mime_type": "image/png",
  "file_size": 123456,
  "ext": "png"
}

响应:

{
  "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 访问
    • 行为:403code=AGENT_SESSION_FORBIDDEN

6.3 头像上传阶段

  1. bucket/path 越权
    • 422AVATAR_PATH_SCOPE_INVALID
  2. mime/size 非法
    • 422AVATAR_FILE_INVALID
  3. storage 签名失败
    • 502AVATAR_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 4profile 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 表。