9.5 KiB
9.5 KiB
Eryao 工程计划:历史解卦与个人档案后端化(单一数据源)
日期:2026-04-05
状态:规划中(Planning Only)
0. 约束与决策前提
本计划基于已确认前提:
- 当前无生产兼容压力,旧字段可直接不兼容。
- 前端只做缓存层,不做权威数据源。
ui_schema属于通用迁移遗留,不在本项目范围,目标是移除。- 头像存储必须使用
avatarsbucket(config.storage.avatar.bucket)。
1. 目标
在不引入额外业务表的前提下,完成以下工程目标:
- assistant 消息落库时,
metadata.agent_output持久化完整divination_derived。 GET /api/v1/agent/history返回前端可直接消费的assistant.agent_output(移除ui_schema)。- 新增 profile 后端 API,前端设置页改为后端读写。
- 头像上传改为预签名 +
avatarsbucket,后端校验路径和类型。
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
关键点
AgentOutput增加divination_derived强类型字段。EventStore字段白名单纳入divination_derived。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
关键点
- 停止
ui_hints -> ui_schema编译。 - assistant 返回受控
agent_output子集,不透传任意 metadata。 - 前端结果页以
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. 信任边界与安全规则
user_id只能取 JWTsub,禁止客户端传 owner。- 头像 path 必须前缀匹配:
avatars/{current_user.id}/。 - bucket 必须等于
config.storage.avatar.bucket。 - mime 白名单:
image/png|image/jpeg|image/webp。 - size 上限:
config.storage.avatar.max_size_mb。 - history 读取严格校验 session owner。
- 错误统一 RFC7807 +
code。
6. 失败模式与处理
6.1 消息落库阶段
divination_derived校验失败- 行为:拒绝写入该字段并记录结构化日志。
- 错误码:
AGENT_OUTPUT_DIVINATION_INVALID(新)。
- TEXT_MESSAGE_END 缺失关键字段
- 行为:整条 assistant 消息按失败路径处理,不写半残对象。
6.2 history 读取阶段
agent_output缺失或损坏- 行为:assistant 消息返回
content,并标记agent_output=null。 - 前端:展示“历史记录不完整”提示,不崩溃。
- 行为:assistant 消息返回
- 非 owner 访问
- 行为:403,
code=AGENT_SESSION_FORBIDDEN。
- 行为:403,
6.3 头像上传阶段
- bucket/path 越权
- 422,
AVATAR_PATH_SCOPE_INVALID。
- 422,
- mime/size 非法
- 422,
AVATAR_FILE_INVALID。
- 422,
- storage 签名失败
- 502,
AVATAR_SIGNED_URL_FAILED。
- 502,
7. 关键边缘场景
- 用户连续点击“保存资料”两次:
- 以后端最后一次写入为准,前端按钮防抖。
- 上传头像成功但 profile 更新失败:
- 前端重试 profile PATCH,不重复上传。
- history 返回空列表:
- 前端展示空态,不触发本地假数据。
- 助手消息存在但缺
divination_derived:- 卡片可展示摘要,不允许进入完整结果页。
- 解卦完成后 history 立即读取:
- 允许短暂读到旧快照,前端做一次重拉。
8. 技术取舍
方案 A(推荐):在现有 messages.metadata 扩展
- 优点:
- 最小变更,不新增表。
- 复用当前会话与历史体系。
- 缺点:
- metadata 体积增大,需要关注单条消息大小。
方案 B:新增 divination_results 独立表
- 优点:
- 结构更纯,查询更明确。
- 缺点:
- 迁移、回写、关联复杂度明显增加。
结论:
- 当前阶段选 A,满足速度与复杂度平衡。
9. 实施切片(按风险顺序)
Slice 1:协议与 schema
- 更新协议文档:history + profile + 错误码。
- 更新
AgentOutput模型字段。
Slice 2:写链路改造
- runner/emitter/store 打通
divination_derived落库。 - 增加单元测试与集成测试。
Slice 3:读链路改造
- history 转换改为返回
agent_output。 - 移除
ui_schema响应字段。
Slice 4:profile API + 头像
- users 路由、service、schema。
- 头像 upload-url 接口。
Slice 5:前端切换
- 历史列表/详情改消费后端
agent_output。 - 设置页改 profile 接口。
- 清理本地真源。
10. 测试覆盖计划
10.1 后端测试矩阵
A. AgentOutput 落库
divination_derived正常写入。divination_derived非法结构拒绝写入。
B. history 接口
- assistant 返回
agent_output。 - 响应不含
ui_schema。 - 非 owner 403。
- 空历史返回空数组。
C. profile 接口
- GET 返回当前用户档案。
- PATCH 字段边界(空、超长、非法字符)。
- 并发 PATCH 最终一致性。
D. avatar upload-url
- 合法 mime/size/path 成功签名。
- bucket/path 越权失败。
- mime/size 超限失败。
- storage 异常返回 502 问题体。
10.2 前端测试矩阵
- history 列表从接口渲染。
- 点击历史项进入结果页并解析
divination_derived。 - profile 读写回显。
- 头像上传后刷新显示。
- 异常提示(网络失败、数据缺失)不崩溃。
11. 可观测性
新增日志字段建议:
- history 响应统计:
thread_id,message_count,assistant_with_agent_output_count。 - profile 更新:
user_id,updated_fields。 - avatar 签名:
user_id,mime_type,file_size,success/failure_code。
指标建议:
history_agent_output_missing_rate。avatar_upload_url_failure_rate。profile_patch_error_rate。
12. 风险与回滚
风险
- 单条 metadata 变大,可能影响查询性能。
- 前端解析新结构时存在字段名误配风险。
回滚
- 若读链路异常,先回滚 history 输出层(保持落库不回滚)。
- profile 接口异常时,可临时只读禁写,保护账户信息。
13. 验收标准(Done)
- 新产生 assistant 消息均含
metadata.agent_output.divination_derived。 - history 接口返回
agent_output,且不再返回ui_schema。 - 前端历史页与结果页不依赖本地真源。
- profile 读写和头像上传全走后端。
- 测试矩阵项全部落地并通过。
14. NOT in Scope
- 大规模清理
backend/src/schemas/domain/**。 - 历史数据回填脚本。
- 新增独立
divination_results表。