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

404 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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` 表。