docs: 更新协议文档,删除废弃计划文档

- 更新 http-error-codes, user-points-chat-data-protocol
- 更新 divination-run-protocol, profile-protocol
- 删除废弃的后端和前端设计计划文档
This commit is contained in:
qzl
2026-04-08 17:23:02 +08:00
parent 49fc9a116f
commit e80a82bef4
57 changed files with 4117 additions and 2269 deletions
@@ -0,0 +1,89 @@
# Bug: 追问1次后无法进入查看历史记录
日期:2026-04-08
状态:已确认(未修复)
## 问题描述
追问1次之后追问入口关闭,但用户也没法点击进去查看追问的历史记录。
## 根因分析
`_loadFollowUpEligibility()` 方法(`divination_result_screen.dart:65-83`)将两个概念混为一谈:
```dart
Future<void> _loadFollowUpEligibility() async {
...
final messages = await widget.divinationApi!.getSessionMessages(
threadId: widget.data.threadId!,
);
final userCount = messages.where((msg) => msg.role == 'user').length;
...
setState(() {
_canFollowUp = userCount < 2; // 同时控制"能追问"和"能进入"
...
});
}
```
按钮逻辑(`divination_result_screen.dart:367`):
```dart
onPressed: (!_canFollowUp || _followUpEligibilityLoading)
? null // _canFollowUp = false 时按钮完全禁用
: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => FollowUpChatScreen(...),
),
);
},
```
**问题**
- `_canFollowUp = userCount < 2` 控制的是"是否还有追问配额"
-`onPressed` 把它和"是否能进入查看历史"混用了
- 导致用户追问1次后(userCount=2),`_canFollowUp=false`,按钮被禁用
- 用户**无法进入追问页面查看历史记录**
**业务逻辑分析**
- 每 session 最多2次追问(首问1次 + 追问1次)
- 追问1次后,用户不能再发送新追问
- 但**应该仍能进入查看历史记录**
### 正确的逻辑应该是
```dart
// 追问配额判断
_canFollowUp = userCount < 2;
// 能否进入查看历史(与追问次数无关,只要有历史消息就能进入)
_canEnterFollowUpChat = messages.isNotEmpty; // 或者只要有 threadId 就能进
// 按钮文案
buttonText = _canFollowUp ? l10n.followUpEntryHint : l10n.followUpViewHistory;
// 按钮是否禁用
onPressed = (_followUpEligibilityLoading)
? null // 只在加载中时禁用
: () { ... } // 始终可点击进入查看
```
## 证据
- 结果页代码:`apps/lib/features/divination/presentation/screens/divination_result_screen.dart`
- `_buildFollowUpBar`: 行 338-394
- `_loadFollowUpEligibility`: 行 65-83
- 按钮 onPress: 行 367
- 追问聊天页:`apps/lib/features/divination/presentation/screens/follow_up_chat_screen.dart`
## 修复方向
1. 分离"能否追问"和"能否进入查看历史"的逻辑
2. 按钮文案根据状态显示不同内容(能追问显示追问提示,已用完显示"查看历史")
3. 只要有 `threadId`,用户就应该能进入查看历史
---
## 相关文档
- 随访工程计划:`docs/plans/2026-04-08-followup-session-history-eng-plan.md`
@@ -0,0 +1,261 @@
# Bug: 追问页面 UI 问题
日期:2026-04-08
状态:已确认(未修复)
## Bug 1: 追问页发送时顶部和消息下方重复加载UI
### 问题描述
在追问页面,用户输入信息发送后,顶部和消息下方同时出现加载UI,但顶部的加载UI是多余的。
### 根因分析
**文件**`apps/lib/features/divination/presentation/screens/follow_up_chat_screen.dart`
**问题代码**
1. 顶部 step 指示器(第 85-124 行):
```dart
if (_sending && _currentStepName != null)
Container(
// 显示 step 进度,如 "解读中..."、"推理中..."
child: Row(
children: [
CircularProgressIndicator(...),
Text(_stepLabel(_currentStepName!)),
],
),
)
```
2. 消息下方 streaming placeholder(第 565-584 行):
```dart
isStreamingPlaceholder
? Row(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(...),
Text(l10n.followUpGenerating), // "生成中..."
],
)
```
**事件触发流程**
| 顺序 | 事件 | 顶部指示器 | 消息下方 |
|------|------|----------|---------|
| 1 | `_submitText()` 调用 | 无 | streaming placeholder 显示 "生成中..." |
| 2 | `STEP_STARTED(stepName='divination')` | 显示 "解读中..." | 继续显示 |
| 3 | `STEP_FINISHED` | 消失 | 继续显示 |
| 4 | `STEP_STARTED(stepName='worker')` | 显示 "推理中..." | **同时显示** "生成中..." |
| 5 | `STEP_FINISHED` | 消失 | 继续显示 |
| 6 | `TEXT_MESSAGE_END` | 无 | placeholder 消失,显示真实内容 |
**问题**:在 worker 阶段(步骤 4),顶部显示 "推理中..." 同时消息下方显示 "生成中...",造成重复反馈。用户只需要一个加载反馈即可。
**修复方向**
1. **方案A**:只在 divination 阶段显示顶部 step 指示器,worker 阶段隐藏(因为 streaming placeholder 已经提供反馈)
```dart
// 第 85 行修改
if (_sending && _currentStepName != null && _currentStepName != 'worker')
```
2. **方案B**:移除顶部 step 指示器,完全依赖 streaming placeholder
3. **方案C**:在 worker 阶段隐藏 streaming placeholder,只显示顶部指示器
推荐 **方案A**,因为:
- divination 阶段没有 streaming placeholder,需要顶部指示器提供反馈
- worker 阶段有 streaming placeholder,不需要顶部指示器
- 改动最小,只加一个条件
---
## Bug 2: 语音模式录制时缺少动画UI
### 问题描述
根据 social-app 的实现,按住说话模式下录音时应该有动画效果,但 eryao 的实现只有文字,没有动画。
### 根因分析
**对比**
| 功能 | social-app | eryao |
|------|------------|-------|
| 录音中动画 | `recordingAnimation` widget(外部传入) | 只有文字 "录音中..." |
| 录音提示文字 | 带动画的 widget | 只有文字 |
| 录音中隐藏输入区域 | `IgnorePointer` + `Opacity` | 未实现 |
**eryao 当前实现**`message_composer.dart:159-172`):
```dart
if (_isRecording) {
if (!showRecordingInlineFeedback) {
return Text(recordingText, ...);
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(width: 16, height: 16), // 空白占位
const SizedBox(height: AppSpacing.xs),
Text(recordingText, ...), // "录音中..."
const SizedBox(height: AppSpacing.xs),
Text(recordingHintText, ...), // "上滑取消"
],
);
}
```
**social-app 实现**`message_composer.dart:222-239`):
```dart
if (_isRecording) {
if (!showRecordingInlineFeedback) {
return Text(resolvedRecordingText, ...);
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
recordingAnimation, // 动画widget(外部传入)
const SizedBox(height: AppSpacing.xs),
Text(resolvedRecordingText, ...),
const SizedBox(height: AppSpacing.xs),
Text(resolvedRecordingHintText, ...),
],
);
}
```
**差异**
1. `recordingAnimation` widget 缺失 - 需要外部传入,eryao 直接用空白占位
2. 录音中图标/按钮没有动画效果
**修复方向**
1. 在 `FollowUpChatScreen` 或其父组件中创建 `recordingAnimation` widget
2. 将 `recordingAnimation` 通过 `MessageComposer` 传入
3. 参考 social-app 的实现,使用脉冲动画或波形动画
**参考实现**(需要添加 `recording_animation.dart`):
```dart
class RecordingAnimation extends StatefulWidget {
final double size;
final Color color;
@override
State<RecordingAnimation> createState() => _RecordingAnimationState();
}
class _RecordingAnimationState extends State<RecordingAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
)..repeat(reverse: true);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.color.withValues(alpha: 0.3),
),
child: Icon(Icons.mic, color: widget.color, size: widget.size * 0.6),
),
);
},
);
}
}
```
---
## Bug 2 & 3: 直接复用 social-app 输入框(不需要 plus 按钮)
### 需求
**直接复用** social-app 的 `MessageComposer`,但**不需要 plus 按钮**。
### 复用清单
| 功能 | social-app | eryao | 处理 |
|------|------------|-------|------|
| `recordingAnimation` | required Widget | 空白占位 | **需要添加** |
| 双层阴影 | 有 | 无 | **需要添加** |
| `AppRadius.xxl` | 用于圆角 | 无此值 | **需要添加** |
| Plus 按钮 | 有 | 无 | **不需要**,保持无 |
| 图标 | `LucideIcons` | Material Icons | 保持 Material Icons |
### 实现步骤
#### 1. 添加 `AppRadius.xxl` 到 `design_tokens.dart`
```dart
class AppRadius {
static const double sm = 8;
static const double md = 12;
static const double lg = 16;
static const double xl = 20;
static const double xxl = 32; // 新增
static const double full = 999;
}
```
#### 2. 创建 `RecordingAnimation` widget
放在 `apps/lib/shared/widgets/` 下,参考 social-app 的脉冲动画效果。
#### 3. 修改 `MessageComposer`
- 添加 `recordingAnimation` 参数(required
- 添加双层阴影
- 使用 `AppRadius.xxl` 替代 `AppRadius.full`
- 移除 plus 按钮(eryao 原有的无 plus 逻辑保持不变)
- 保持 Material Icons
#### 4. 修改 `FollowUpChatScreen`
- 创建 `RecordingAnimation` 实例
- 传入 `MessageComposer`
---
## 相关文件
- `apps/lib/features/divination/presentation/screens/follow_up_chat_screen.dart`
- `apps/lib/shared/widgets/message_composer.dart`
- `apps/lib/shared/widgets/recording_animation.dart`(新建)
- `apps/lib/shared/theme/design_tokens.dart`
- `/home/qzl/Code/social-app/apps/lib/shared/widgets/message_composer.dart`
## 修复优先级
1. **高优先级**Bug 1(重复加载UI
2. **高优先级**Bug 2 & 3(直接复用 social-app 输入框)
@@ -0,0 +1,200 @@
# Bug: 追问时 agent_output 被重新生成,导致 sign_level 被覆盖
日期:2026-04-08
状态:已确认根因(未修复)
## 问题描述
首次解卦完成后,用户继续追问时,agent 会重新生成 `agent_output`,重新计算卦的结论和标签。
**具体表现**
- 首次解卦结论:`中下签`
- 追问后结论:`下下签`(同一个卦,结果被重新生成)
## 根因分析
**问题定位**`backend/src/core/agentscope/runtime/runner.py`
### 根因链条
1. **`runner.py:execute()` 方法(第 70-133 行)**
无论 `runtime_mode``chat` 还是 `follow_up`,都会执行以下逻辑:
```python
# 第 105-119 行:始终推导卦象
derived_divination = self._resolve_derived_divination(run_input=run_input)
await self._emit_step_event(
pipeline=pipeline,
run_input=run_input,
step_name="divination",
event_type="DIVINATION_DERIVED", # <-- 追问时不应发射此事件
...
)
```
2. **`runner.py:_execute_worker_step()` 方法(第 200-245 行)**
始终将 `derived_divination` 传递给 worker
```python
# 第 294-302 行:始终将 divination_derived 放入 worker_output
await emitter.emit_final_text_end(
worker_output={
**worker_payload.model_dump(mode="json", exclude_none=True),
"divination_derived": derived_divination.model_dump(...), # <-- 追问时不应包含
},
...
)
```
3. **`stage_emitter.py:emit_final_text_end()` 方法(第 46-73 行)**
始终将所有字段放入 `TEXT_MESSAGE_END` 事件:
```python
payload = {
"messageId": message_id,
"role": "assistant",
"stage": self._stage,
"status": worker_output.get("status"),
"sign_level": worker_output.get("sign_level"), # <-- 追问时不应有
"conclusion": worker_output.get("conclusion", []), # <-- 追问时不应有
"focus_points": worker_output.get("focus_points", []),
"advice": worker_output.get("advice", []),
"keywords": worker_output.get("keywords", []),
"answer": worker_output.get("answer", ""),
"error": worker_output.get("error"),
"divination_derived": worker_output.get("divination_derived"), # <-- 追问时不应有
...
}
```
4. **`store.py:_persist_text_message()` 方法(第 125-160 行)**
从事件中提取所有字段并完整存储:
```python
worker_output_fields = (
"status",
"sign_level", # <-- 追问时被重新生成并覆盖
"conclusion", # <-- 追问时被重新生成并覆盖
"focus_points",
"advice",
"keywords",
"answer",
"error",
"divination_derived", # <-- 追问时被重新生成
"ui_hints",
)
```
### 问题本质
`runtime_mode=follow_up` 时,系统仍在:
1. 重新推导卦象(调用 `_resolve_derived_divination`
2. 发射完整的 `DIVINATION_DERIVED` 事件
3. 生成包含所有结构化字段的 `worker_output`
4. 将所有字段存储到数据库
但根据工程计划(`docs/plans/2026-04-08-followup-session-history-eng-plan.md:37-40`),追问时的预期行为是:
```
[一次追问]
user -> /agent/runs(runtime_mode=follow_up)
-> assistant(content [+ optional metadata.agent_output.answer])
```
即:追问时只输出 `answer`(内容),不重新生成卦象结构。
## 证据
### 数据库证据
Session `015fe0f9-0500-43ab-911a-4ce8e3160032`
| 时间 | 角色 | sign_level | divination_derived | 问题 |
|------|------|------------|-------------------|------|
| 05:21:19 | user | - | - | 首问问题 |
| 05:21:28 | assistant | 中下签 | 完整 | 首答(正确) |
| 05:22:24 | user | - | - | 追问 |
| 05:22:33 | assistant | **下下签** | 完整 | 追问答(sign_level 被重新生成) |
两个 assistant 消息的 `divination_derived` 完全相同,但 `sign_level` 不同,证明是重新生成的。
## 修复方向
### 1. `runner.py`
在 `execute()` 方法中,根据 `runtime_mode` 决定是否推导卦象:
```python
async def execute(self, ...):
runtime_mode = self._resolve_runtime_mode(run_input=run_input)
if runtime_mode == RuntimeMode.CHAT:
derived_divination = self._resolve_derived_divination(run_input=run_input)
await self._emit_step_event(...) # DIVINATION_DERIVED
else:
derived_divination = None # follow_up 不推导
```
### 2. `stage_emitter.py`
`emit_final_text_end()` 根据 `runtime_mode` 决定发送哪些字段:
```python
async def emit_final_text_end(self, ..., runtime_mode: str):
payload = {"messageId": ..., "role": "assistant", "stage": self._stage}
if runtime_mode == "chat":
payload.update({
"status": worker_output.get("status"),
"sign_level": worker_output.get("sign_level"),
"conclusion": worker_output.get("conclusion", []),
"focus_points": worker_output.get("focus_points", []),
"advice": worker_output.get("advice", []),
"keywords": worker_output.get("keywords", []),
"divination_derived": worker_output.get("divination_derived"),
...
})
else: # follow_up
payload["answer"] = worker_output.get("answer", "")
```
### 3. `store.py`
`_persist_text_message()` 根据 `runtime_mode` 决定提取哪些字段:
```python
async def _persist_text_message(self, ...):
runtime_mode = self._resolve_runtime_mode(event=event)
if runtime_mode == "chat":
worker_output_fields = ("status", "sign_level", "conclusion", ...)
else: # follow_up
worker_output_fields = ("answer",) # 只存储 answer
```
### 4. `runtime_models.py`
`resolve_worker_output_model()` 应根据 `runtime_mode` 返回不同 schema
```python
def resolve_worker_output_model(runtime_mode: RuntimeMode = RuntimeMode.CHAT) -> type[WorkerAgentOutputLite]:
if runtime_mode == RuntimeMode.FOLLOW_UP:
return WorkerAgentOutputLite # 只有 answer
return AgentOutput # 完整结构(继承自 WorkerAgentOutputRich
```
## 相关文件
- `backend/src/core/agentscope/runtime/runner.py` - 主编排逻辑
- `backend/src/core/agentscope/runtime/stage_emitter.py` - 事件发射
- `backend/src/core/agentscope/events/store.py` - 事件持久化
- `backend/src/schemas/agent/runtime_models.py` - 输出 schema 定义
## 相关文档
- 工程计划:`docs/plans/2026-04-08-followup-session-history-eng-plan.md`
- 协议文档:`docs/protocols/divination/divination-run-protocol.md`
@@ -0,0 +1,90 @@
# Bug: 首页历史 Session 只显示4条,"更多"按钮缺失
日期:2026-04-08
状态:功能缺失(未完成)
## 问题描述
前端主页接收后端传来的历史 session,永远只显示四个,"more"按钮不见了。
## 根因分析
### 1. 前端硬编码 `take(4)`
`apps/lib/features/home/presentation/screens/home_screen.dart:287`
```dart
children: historyItems.take(4).map((item) {
```
这是在 commit `6e82053`(重构首页为底部导航栏布局)时**有意添加**的设计选择,用于限制首页展示的历史记录数量。
### 2. 前端未实现"更多"按钮
- `l10n.more` 本地化字符串存在(`'更多'`/`'More'`),定义于 `apps/lib/l10n/app_localizations_zh.dart:115``app_localizations_en.dart:116`
- 但搜索整个 Dart 代码库,`l10n.more` **没有任何地方使用它**
- "更多"功能从未被实现
### 3. 后端 `hasMore` 硬编码为 `False`
`backend/src/v1/agent/service.py:655-659`
```python
return HistorySnapshotResponse(
scope="history_sessions_latest_assistant",
threadId=None,
day=None,
hasMore=False, # 硬编码,未实际计算
messages=messages,
)
```
后端 schema 定义了 `hasMore` 字段(`schemas.py:238`),但 service 层返回时**硬编码为 `False`**,从未实际计算是否还有更多数据。
### 4. 后端 API 行为正确
`backend/src/v1/agent/service.py:641-646`
```python
raw_messages = await self._repository.get_latest_assistant_messages_by_user_sessions(
user_id=str(current_user.id),
visibility_mask=bit_mask(bit=int(SystemVisibilityBit.UI_HISTORY)),
session_limit=50, # 后端返回最多50条
)
```
后端正确返回最多 50 条历史 session,前端只是没有利用这些数据。
## 问题性质
| 层级 | 现象 | 性质 |
|------|------|------|
| 后端 | `hasMore=False` 硬编码 | 缺陷:响应语义不正确 |
| 前端 | `take(4)` 只显示4条 | 设计选择:有意的 UI 限制 |
| 前端 | 无"更多"按钮 | 功能缺失:有 `l10n.more` 但未实现 |
## 证据
- 前端代码:`apps/lib/features/home/presentation/screens/home_screen.dart:287`
- 后端代码:`backend/src/v1/agent/service.py:655`
- 本地化字符串:`apps/lib/l10n/app_localizations_zh.dart:115`
- 提交记录:`6e82053`feat(home): 重构首页为底部导航栏布局)
## 修复方向
### 方案 A:实现完整分页(推荐)
1. **后端**:实现真正的 `hasMore` 计算逻辑
- 添加 `offset` 参数支持分页
- 实际计算 `hasMore = total_count > offset + limit`
2. **前端**:实现"更多"按钮或无限滚动
- 监听滚动位置,滚动到底部时加载更多
- 或添加"更多"按钮手动触发加载
### 方案 B:移除不存在的字符串
如果业务确定只需要显示4条,则:
- 移除 `l10n.more` 本地化字符串,避免误导
## 相关文档
- 工程计划:`docs/plans/2026-04-05-divination-history-profile-eng-plan.md`
- 随访计划:`docs/plans/2026-04-08-followup-session-history-eng-plan.md`
@@ -1,241 +0,0 @@
# Eryao 解卦历史与个人档案后端单一数据源改造计划
日期:2026-04-05
状态:评审中(未开始编码)
## 1. 背景与目标
当前移动端存在两类不符合目标架构的问题:
1. 个人档案(昵称、简介、头像)仍有前端本地状态路径,非后端权威数据源。
2. 首页历史解卦无法稳定由后端快照直接重建结果页,前端被迫做本地兜底。
本计划目标:
- 实现“后端为唯一数据源,前端仅缓存”。
-`DIVINATION_DERIVED` 的完整结构进入消息 `metadata.agent_output` 并持久化。
- 历史接口返回可被前端直接解析的结构化 assistant 输出(不再依赖 `ui_schema`)。
- 个人档案全链路后端化,头像使用 `avatars` bucket。
非目标:
- 本计划不直接提交代码实现。
- 本计划不包含 UI 视觉细节改稿。
## 2. 现状核对(基于仓库代码)
### 2.1 历史接口与消息转换
- 历史接口:`GET /api/v1/agent/history`,定义于 `backend/src/v1/agent/router.py`
- 当前转换逻辑在 `backend/src/v1/agent/utils.py`
- `user` 消息主要输出 `content``attachments`
- `assistant` 消息默认走 `ui_hints -> ui_schema` 编译路径。
- 历史响应结构 `HistoryMessage` 当前包含 `ui_schema`,不直接暴露结构化 `agent_output`
### 2.2 DIVINATION_DERIVED 与落库断点
- 运行时会发出 `DIVINATION_DERIVED`(见 `backend/src/core/agentscope/runtime/runner.py`)。
- 消息落库由 `backend/src/core/agentscope/events/store.py` 负责。
- 当前 `TEXT_MESSAGE_END` 持久化字段包含 `sign_level/summary/.../ui_hints`,未包含 `divination` 结构。
- 结果:历史快照难以完整重建结果页结构。
### 2.3 Profile 与头像
- 后端配置已有 `storage.avatar.bucket`,默认 `avatars``backend/src/core/config/settings.py`)。
- 当前 `v1` 仅挂载 `auth/agent/points` 路由(`backend/src/v1/router.py`),尚无 profile 专用路由。
## 3. 核心设计决策
### 决策 A:把 `divination_derived` 放入 `metadata.agent_output`
-`AgentOutput` 增加字段 `divination_derived`(强类型,禁止裸 `dict`)。
- 事件落库时把 `DIVINATION_DERIVED` 内容并入 assistant 的 `metadata.agent_output.divination_derived`
-`sign_level/summary/advice/...` 同时持久化,形成一条可回放的 assistant 结构化输出。
理由:
- 最小改动复用现有消息表,不新增历史结果表即可满足回放需求。
- 前端可直接从历史响应解析结果页,避免本地拼装。
### 决策 B:历史接口返回 `assistant.agent_output`,移除 `ui_schema`
- `HistoryMessage` 改为:
- `user`: `content + attachments`
- `assistant`: `content + agent_output`
- `ui_schema` 从接口协议中移除(迁移自通用模块的历史遗留,不在本项目范围)。
理由:
- 减少中间编译层,契约更稳定、语义更清晰。
- 前端直接消费业务数据,不依赖通用 UI 编译器。
### 决策 CProfile 全后端化 + 头像对象存储
- 新增 users/profile API,前端只保留缓存层。
- 头像上传走预签名 URL,bucket 固定 `avatars`,路径按用户隔离。
## 4. 协议与接口计划(先文档,后实现)
## 4.1 新增/修改协议文档
按“协议先行”更新以下文档:
1. `docs/protocols/divination/divination-run-protocol.md`
- 增补:历史回放时 assistant `agent_output.divination_derived` 的字段契约。
- 标记:`ui_schema` 已废弃并移除。
2. 新增:`docs/protocols/profile/profile-protocol.md`
- 定义 profile 读写与头像上传签名协议。
3. 如涉及错误码新增,更新:
- `docs/protocols/common/http-error-codes.md`
### 4.2 后端 API 契约(目标)
#### A. 历史快照(改造)
- `GET /api/v1/agent/history`
- 响应中 assistant 消息新增(或替换为)`agent_output`
- `sign_level`
- `summary`
- `conclusion`
- `focus_points`
- `advice`
- `keywords`
- `answer`
- `divination_derived`(完整卦象结构)
#### B. Profile(新增)
- `GET /api/v1/users/me/profile`
- `PATCH /api/v1/users/me/profile`
- `POST /api/v1/users/me/avatar/upload-url`
- (可选)`GET /api/v1/users/me/avatar/signed-url`
#### C. 头像上传约束
- bucket 固定:`config.storage.avatar.bucket`
- 路径前缀建议:`avatars/{user_id}/...`
- 文件类型:`image/png|image/jpeg|image/webp`
- 体积上限:`config.storage.avatar.max_size_mb`
## 5. 数据模型改造计划
### 5.1 Runtime 模型
- 文件:`backend/src/schemas/agent/runtime_models.py`
- 变更:`AgentOutput` 增加 `divination_derived` 字段(类型复用 `schemas/domain/divination.py`)。
- 规则:保持 `extra="forbid"`,禁止无类型漂移。
### 5.2 事件到落库链路
- 文件:`backend/src/core/agentscope/runtime/stage_emitter.py`
- `TEXT_MESSAGE_END` payload 带上 `divination_derived`
- 文件:`backend/src/core/agentscope/events/store.py`
- `worker_output_fields` 纳入 `divination_derived` 并写入 `metadata.agent_output`
### 5.3 历史响应转换
- 文件:`backend/src/v1/agent/utils.py`
- 删除 `ui_hints -> ui_schema` 编译路径。
- assistant 消息改为抽取并返回受控 `agent_output`
- 文件:`backend/src/v1/agent/schemas.py`
- `HistoryMessage` 改字段定义(去 `ui_schema`,加 `agent_output`)。
## 6. 前端消费与缓存策略
### 6.1 历史与结果页
- 历史列表数据源改为后端 `agent/history`
- 点开历史项时:
- 直接解析 `assistant.agent_output.divination_derived` + 解释文本字段。
- 本地仅做缓存,不做真源 fallback。
### 6.2 Profile
- 设置页资料读取改为 `GET /users/me/profile`
- 编辑资料写入 `PATCH /users/me/profile`
- 头像更新走 upload-url + 上传 + profile 更新引用路径。
### 6.3 点数
- 保持后端余额接口作为权威数据源(现有已接)。
- 前端只做短期缓存,解卦完成后强制 refresh。
## 7. 代码清理边界(你关心的“删除通用遗留”)
原则:先去引用,再删定义,最后删文件,避免误删。
分三步:
1. 第一阶段(本次改造内)
- 删除 `agent/history``ui_schema` 的输出与依赖。
- 删除前端对 `ui_schema` 的消费路径(若存在)。
2. 第二阶段(安全清理)
- 搜索 `schemas/domain``schemas/agent/ui_hints` 的实际引用。
- 对“零引用 + 非协议字段”进行清理。
3. 第三阶段(文档与测试补齐)
- 更新协议文档、错误码、回归测试。
备注:
- 不建议在同一 PR 里“功能改造 + 大规模 schema 删除”,建议拆成两个 PR,降低回归风险。
## 8. 测试计划(必须项)
### 8.1 后端单元/集成
1. `TEXT_MESSAGE_END` 持久化:`metadata.agent_output.divination_derived` 落库断言。
2. `GET /api/v1/agent/history`assistant 返回 `agent_output`,且不再返回 `ui_schema`
3. 历史分页与 owner 校验不回退。
4. profile API:读写、权限、字段约束、头像路径安全性。
5. 头像签名 URLbucket/path/mime/size 约束。
### 8.2 前端
1. 历史列表从后端数据渲染。
2. 点击历史项成功进入结果页,字段一致性校验。
3. profile 页面读写闭环(昵称/简介/头像)。
4. 点数刷新与缓存失效策略验证。
## 9. 风险与回滚
主要风险:
- 历史消息中旧数据可能没有 `divination_derived`,前端需兼容空值。
- `ui_schema` 下线后,若有隐藏调用方会断。
回滚策略:
- 协议层采用短期双读兼容窗口(仅过渡期):
- 新字段优先;旧字段仅用于读,不再写。
- 若线上异常,先回滚 history 响应变更,再保持落库新增字段不删。
## 10. 实施顺序(最小风险)
1. 协议文档更新并评审通过。
2. 后端:`AgentOutput` + 事件落库 + history 响应新增 `agent_output`(先加后切)。
3. 前端:改消费到 `agent_output`,移除本地真源。
4. 后端:移除 `ui_schema` 输出。
5. profile API + 前端接入头像上传。
6. 清理无用 schema(独立 PR)。
## 11. 验收标准(DoD
全部满足才算完成:
1. 解卦后写入的 assistant 消息在 DB 中可见 `metadata.agent_output.divination_derived`
2. 首页历史完全来自后端,清空本地缓存后仍可正确展示。
3. 历史详情可完整还原结果页,不依赖 `ui_schema`
4. profile 读写走后端,头像实际落 `avatars` bucket。
5. 前端不再把 profile/history 作为本地权威数据源。
6. 协议文档与实现一致,相关测试通过。
## 12. GSTACK REVIEW REPORT
| Review | Trigger | Why | Runs | Status | Findings |
|--------|---------|-----|------|--------|----------|
| Eng Review | `/plan-eng-review` | 锁定架构、契约、测试闭环 | 1 | Done | 确认后端单一数据源方向;建议分阶段移除 `ui_schema` 并将 schema 清理拆分独立 PR |
| CEO Review | `/plan-ceo-review` | 范围与优先级 | 0 | — | — |
| Design Review | `/plan-design-review` | UI/UX 风险 | 0 | — | — |
| DX Review | `/plan-devex-review` | 开发体验风险 | 0 | — | — |
VERDICT:可以进入实现阶段,但必须先完成协议文档更新并冻结字段契约。
@@ -1,403 +0,0 @@
# 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` 表。
+8 -1
View File
@@ -23,9 +23,16 @@ This document is the source of truth for backend RFC7807 `code` values consumed
| code | status | meaning | frontend handling |
|---|---:|---|---|
| `AGENT_SESSION_RUN_LIMIT_EXCEEDED` | 409 | Session already reached max run count (start + 3 follow-ups) | Show run-limit message and require starting a new session |
| `AGENT_SESSION_RUN_LIMIT_EXCEEDED` | 409 | Session already reached max run count (start + 1 follow-up) | Show run-limit message and require starting a new session |
| `AGENT_RUNTIME_MODE_INVALID` | 422 | Missing or invalid `forwardedProps.runtime_mode` in run request | Show invalid-request message and retry from current page |
| `AGENT_DIVINATION_PAYLOAD_REQUIRED` | 422 | Missing required `forwardedProps.divinationPayload` in run request | Prompt user to restart casting flow and resubmit |
| `AGENT_OUTPUT_DIVINATION_INVALID` | 422 | Worker output contains invalid `divination_derived` payload shape | Show generic history parse error and suggest retrying latest run |
| `AGENT_SESSION_ID_INVALID` | 422 | Invalid session/thread id format | Show invalid-session message and force refresh history |
| `AGENT_SESSION_NOT_FOUND` | 404 | Session does not exist (including follow-up on non-existing thread) | Show session-not-found message and refresh history |
| `AGENT_AUDIO_UNSUPPORTED_FORMAT` | 400 | Audio format is not accepted by transcribe endpoint | Show format hint and ask user to retry with wav audio |
| `AGENT_AUDIO_TOO_LARGE` | 400 | Audio file exceeds transcribe size limit | Show size-limit message and ask user to shorten audio |
| `AGENT_AUDIO_EMPTY` | 400 | Uploaded audio payload is empty | Show retry hint and keep input unchanged |
| `AGENT_ASR_UNAVAILABLE` | 502 | Upstream ASR service unavailable | Show retry message and allow fallback to text input |
## Profile
@@ -28,7 +28,7 @@ Protocol verification status:
- Charge timing: deduct after worker run succeeds (`RUN_FINISHED` path).
- Failure behavior: failed/canceled runs do not deduct points.
- Precheck: before accepting a run, backend must verify `available = balance - frozen_balance >= 20`.
- Session follow-up cap: one session allows at most 4 user runs total (initial divination + 3 follow-ups).
- Session follow-up cap: one session allows at most 2 user runs total (initial divination + 1 follow-up).
- Billing idempotency key for per-run consume: `chat.run.success:{session_id}:{run_id}`.
## Table contract
@@ -10,7 +10,8 @@ Protocol verification status:
## Compatibility strategy
- Current strategy: additive evolution only.
- Run/events contract: `backward-compatible` additive evolution.
- History contract (`GET /agent/history`) currently `requires-migration` (see migration notes in this document).
- Existing required fields cannot be removed or renamed without migration notes.
- Canonical divination terminology values must remain Chinese.
@@ -19,6 +20,8 @@ Protocol verification status:
- Submit run: `POST /api/v1/agent/runs`
- Stream events: `GET /api/v1/agent/runs/{threadId}/events?runId=...`
- History snapshot: `GET /api/v1/agent/history`
- Delete session: `DELETE /api/v1/agent/sessions/{threadId}`
- Audio transcribe: `POST /api/v1/agent/transcribe`
## Run request contract
@@ -85,14 +88,36 @@ Protocol verification status:
- `yaoLines` item enum: `少阳 | 少阴 | 老阳 | 老阴`
- Additional fields are forbidden.
### `runtime_mode` rules
- Allowed values: `chat | follow_up`.
- `chat`: first run for a session.
- `follow_up`: follow-up run in existing session.
- Missing or invalid `runtime_mode` MUST return `422` with code `AGENT_RUNTIME_MODE_INVALID`.
- Current backend behavior still requires `forwardedProps.divinationPayload` in both modes.
### Follow-up request note
- Follow-up submit still uses `POST /api/v1/agent/runs`.
- Required differences from first run:
- `threadId` must be existing session id.
- `forwardedProps.runtime_mode` must be `follow_up`.
- `messages[0].content` is follow-up question text.
- If `threadId` does not exist, backend returns `404` (`AGENT_SESSION_NOT_FOUND`).
## Event output contract
During run streaming, backend emits standard AG-UI lifecycle events and two divination-relevant payload events:
- Lifecycle: `RUN_STARTED`, `RUN_FINISHED` or `RUN_ERROR`.
- Step events: `STEP_STARTED`, `STEP_FINISHED` with `stepName` (for example: `worker`).
- Payload events: `DIVINATION_DERIVED`, `TEXT_MESSAGE_END`.
### 1) `DIVINATION_DERIVED`
- Emitted once after backend derives hexagram data.
- Payload field: `divination` (strict object).
- Emitted only when `runtime_mode=chat`.
`divination` object:
@@ -160,7 +185,9 @@ During run streaming, backend emits standard AG-UI lifecycle events and two divi
### 2) `TEXT_MESSAGE_END`
- Standard final answer event.
- Existing fields remain canonical: `sign_level`, `conclusion`, `focus_points`, `advice`, `keywords`, `answer`.
- `runtime_mode=chat` fields: `status`, `sign_level`, `conclusion`, `focus_points`, `advice`, `keywords`, `answer`, `error`, `divination_derived`.
- `runtime_mode=follow_up` fields: `status`, `answer`, `error`.
- `runtime_mode=follow_up` MUST NOT include `sign_level`, `conclusion`, `focus_points`, `advice`, `keywords`, `divination_derived`.
- Language rule: `conclusion`, `focus_points`, `advice`, `keywords`, `answer` should follow user `ai_language` preference unless user explicitly requests otherwise.
- Canonical six-yao terms remain Chinese in protocol text (for example: 世爻、应爻、动爻、静爻、六亲、六神、伏神、月建、日辰、月破、日冲、空亡、五行旺衰).
@@ -173,17 +200,22 @@ Frontend should combine:
`GET /api/v1/agent/history` is the canonical replay source for frontend history list and result reconstruction.
- When `threadId` is provided, backend returns full session messages ordered by `seq asc`.
- When `threadId` is omitted, backend returns one latest assistant message per session for history list summary.
- `threadId` is the session identifier (same value as AG-UI run/events path parameter).
### Required response shape
```json
{
"scope": "history_day",
"scope": "history_session_full",
"threadId": "uuid|null",
"day": "2026-04-05|null",
"day": null,
"hasMore": false,
"messages": [
{
"id": "uuid",
"threadId": "session-uuid",
"seq": 12,
"role": "assistant",
"content": "...",
@@ -205,6 +237,7 @@ Frontend should combine:
},
{
"id": "uuid",
"threadId": "session-uuid",
"seq": 11,
"role": "user",
"content": "我最近换工作是否合适?",
@@ -222,17 +255,74 @@ Frontend should combine:
Rules:
- `scope=history_session_full` means full-thread replay with `messages` ordered by `seq`.
- `scope=history_sessions_latest_assistant` means cross-session summary list.
- Each `messages[i].threadId` is required and points to the owning session. Frontend must use this value to open detail replay and follow-up.
- `day` and `hasMore` are retained for compatibility with old clients, but in `history_session_full` mode backend currently returns `day=null` and `hasMore=false`.
- In `history_sessions_latest_assistant`, backend computes `hasMore` from `session_limit + 1` query.
- `assistant` message MUST provide `agent_output` when backend has valid worker output metadata.
- `agent_output.divination_derived` uses the same shape as `DIVINATION_DERIVED.divination` payload.
- Frontend reconstructs divination result page from `agent_output` data, not from local mock data.
- `agent_output.sign_level` allowed values: `上上签` / `中上签` / `中下签` / `下下签`.
### Breaking change note
## Migration note (`requires-migration`)
- `ui_schema` is removed from history response and is no longer part of this project protocol.
- This repository currently accepts non-backward-compatible protocol evolution (no production compatibility burden).
### Change set
1. `GET /agent/history?threadId=...` now returns full session replay (`scope=history_session_full`) instead of day-window snapshot semantics.
2. `messages[i].threadId` is now required.
3. `ui_schema` is removed from history payload.
4. `runtime_mode=follow_up` uses minimal `TEXT_MESSAGE_END` schema and no longer emits `DIVINATION_DERIVED`.
5. Added `DELETE /api/v1/agent/sessions/{threadId}` with idempotent `204 No Content` (already deleted or not found also returns 204).
### Frontend migration steps
1. Parse history as ordered message stream by `messages[].seq`.
2. Use `messages[].threadId` as session id for opening follow-up.
3. Do not depend on `ui_schema` in history payload.
4. Keep `day`/`hasMore` optional-read only; do not use them for pagination in thread replay mode.
### Rollback notes
- If backend rolls back to day-window semantics, frontend must switch detail replay back to day-based load logic and stop requiring `messages[].threadId`.
- Keep legacy parser branch behind feature flag during staged rollout.
## Error contract linkage
- All errors use RFC7807 with extension `code` and optional `params`.
- Error code registry source: `docs/protocols/common/http-error-codes.md`.
## Session delete contract
### `DELETE /api/v1/agent/sessions/{threadId}`
- Authorization: current user must own the session.
- Semantics: soft delete session (`deleted_at`), history reads filter deleted sessions by default.
- Success: `204 No Content`.
- Idempotent: already deleted or not found also returns `204 No Content`.
## Transcribe contract
### `POST /api/v1/agent/transcribe`
Request:
- `multipart/form-data`
- field name: `audio`
- allowed content types: `audio/wav`, `audio/x-wav`, `audio/wave`
- max payload bytes: `10MB`
Response:
```json
{
"transcript": "我今天适合出门谈合作吗?"
}
```
Error codes (see common registry):
- `AGENT_AUDIO_UNSUPPORTED_FORMAT`
- `AGENT_AUDIO_TOO_LARGE`
- `AGENT_AUDIO_EMPTY`
- `AGENT_ASR_UNAVAILABLE`
+42 -4
View File
@@ -4,19 +4,23 @@ This document defines the canonical backend contract for user profile read/write
Protocol verification status:
- Backend model source: `backend/src/models/profile.py`
- Backend route source: `backend/src/v1/users/router.py`
- Backend schema source: `backend/src/v1/users/schemas.py`
- Backend service source: `backend/src/v1/users/service.py`
- Frontend mapping source: `apps/lib/features/settings/data/apis/profile_api.dart`
- Storage config source: `backend/src/core/config/settings.py`
- Current status: planned
- Current status: aligned
## Compatibility strategy
- Current strategy: breaking changes allowed during implementation phase (no production compatibility burden).
- Once production compatibility is required, switch to additive-only evolution.
- Current strategy: additive evolution (`backward-compatible`).
- Breaking change requires explicit migration + rollback notes (`requires-migration`).
## Route overview
- Get profile: `GET /api/v1/users/me/profile`
- Update profile: `PATCH /api/v1/users/me/profile`
- Update settings: `PATCH /api/v1/users/me/settings`
- Create avatar upload url: `POST /api/v1/users/me/avatar/upload-url`
- Upload avatar directly: `POST /api/v1/users/me/avatar` (multipart)
@@ -84,6 +88,40 @@ Response:
- Returns the same shape as `GET /users/me/profile`.
## Settings update contract
### `PATCH /api/v1/users/me/settings`
Request:
```json
{
"settings": {
"version": 1,
"preferences": {
"interface_language": "zh-CN",
"ai_language": "zh-CN",
"timezone": "Asia/Shanghai",
"country": "CN"
},
"privacy": {},
"notification": {
"allow_notifications": true,
"allow_vibration": true
}
}
}
```
Rules:
- `settings` must conform to `ProfileSettingsV1`.
- Additional fields are forbidden.
Response:
- Returns the same shape as `GET /users/me/profile`.
## Avatar upload signing contract
### `POST /api/v1/users/me/avatar/upload-url`