feat: 完善图像工作区与创作工具体验
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-08-16 14:08:02 +08:00
parent bfcb88cfef
commit 26b52d76e3
92 changed files with 16678 additions and 2975 deletions

View File

@@ -0,0 +1,333 @@
# 个人资料会话观察同步:服务端对接说明
> Makelore Code 客户端已完成接入,本文档用于服务端实现与联调。
| 项目 | 内容 |
| --- | --- |
| 对接对象 | 用户服务 / Agent Profile 服务 |
| 客户端状态 | 已接入,等待服务端支持 `session_data` |
| 上游接口 | `PUT /api/user/agent-profile` |
| 数据方向 | 本地客户端 → 云端,只接收观察数据 |
| 适用模块 | Makelore Code 的本地编程会话 |
## 1. 需求结论
客户端会把本地编程会话的问答观察快照,伴随现有个人资料 PUT 请求上传到服务端。
服务端需要满足以下结论:
- 每次上传是某一个会话截至当前时刻的完整问答快照,不是增量消息。
- 一个请求只包含一个 `project_id + session_id` 会话。
- 只接收用户和助手的自然语言文本;代码、路径、日志、工具调用、附件、思考过程和产物不在上传范围内。
- 服务端必须按当前登录账号归属数据,不能信任客户端传入的用户身份字段。
- 同一账号、同一项目、同一会话重复上传时执行幂等更新。
- 本地会话删除不会触发云端删除;客户端没有上传删除事件,也不会请求服务端恢复或下发会话。
- 客户端登录后不会主动遍历并上传所有旧会话;只有本地发生新一轮问答并完成后才会上传该会话的完整快照。
## 2. 请求链路
客户端通过 Electron Main 的 Host API 代理访问服务端,服务端只需要实现上游接口:
```text
Renderer
→ PUT /api/works/user/agent-profile
→ Electron Main 代理
→ PUT /api/user/agent-profile
```
服务端不需要实现 `/api/works/user/agent-profile`。服务端收到的认证方式保持现有个人资料接口不变:
```http
PUT /api/user/agent-profile
Authorization: Bearer <access_token>
Content-Type: application/json
```
用户身份必须从 access token 解析。请求体中没有、也不应新增可指定其他用户的 `user_id``username` 或类似字段。
## 3. PUT 请求体
现有个人资料字段保持兼容,在可选的 `session_data` 字段中增加会话观察快照:
```json
{
"display_name": "小明",
"age": 18,
"gender": "male",
"share_age_with_agents": true,
"share_gender_with_agents": true,
"analysis_enabled": true,
"version": 3,
"session_data": {
"project_id": "project_abc123",
"session_id": "ses_xyz789",
"updated_at": "2026-08-13T10:00:00.000Z",
"messages": [
{
"id": "msg_user_1",
"role": "user",
"created_at": "2026-08-13T09:59:00.000Z",
"text": "请帮我解释这个问题。"
},
{
"id": "msg_assistant_1",
"role": "assistant",
"created_at": "2026-08-13T10:00:00.000Z",
"text": "可以从两个方面理解。"
}
]
}
}
```
### 3.1 `session_data` 字段定义
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `project_id` | string | 是 | 本地项目稳定标识;不是项目路径,也不是项目名称 |
| `session_id` | string | 是 | OpenCode 会话标识;服务端按不透明字符串保存 |
| `updated_at` | ISO 8601 string | 是 | 客户端生成的本次快照时间,统一为 UTC ISO 字符串 |
| `messages` | array | 是 | 按本地会话顺序排列的完整问答消息 |
消息对象:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string | 否 | 本地消息标识;服务端不应依赖其存在 |
| `role` | `user` / `assistant` | 是 | 只会出现这两个值 |
| `created_at` | ISO 8601 string | 否 | 客户端可提供的消息时间 |
| `text` | string | 是 | 客户端清洗后的自然语言文本 |
`session_data` 为可选字段。没有新会话问答时,普通个人资料 PUT 请求可以不带该字段,原有资料保存逻辑必须保持不变。
### 3.2 客户端上传时机
客户端在一次普通用户提问获得助手回答,并且会话运行状态进入 `idle` 后,通过后台队列上传。上传有约 1 秒防抖,同一会话在短时间内只保留最新快照。
以下情况不会触发会话观察上传:
- 项目命令执行;
- 上下文压缩命令;
- 只有用户消息、没有助手回答;
- 助手错误、会话中止或运行失败;
- AI 绘画会话。
## 4. 上传内容边界
客户端在发送前已做过滤,服务端可以把 `messages[].text` 当作普通不可信文本保存,但不能尝试从其他字段恢复原始内容。
### 4.1 允许保留
- 用户自然语言提问;
- 助手自然语言回答;
- 消息顺序、消息 ID、消息时间
- `project_id``session_id` 和快照更新时间。
### 4.2 不在上传范围内
- 源代码、代码块和内联代码;
- 项目路径、文件路径和源文件名;
- 工具调用名称、参数、输入输出和日志;
- 系统消息、隐藏提示词、思考过程和错误消息;
- 附件、图片、音频、视频、文件内容和 Base64 数据;
- 生成的图片、视频、文档、压缩包等产物;
- 产物 URL、`MEDIA:/...` 标记和本地文件引用;
- 本地项目目录、工作区路径、运行时信息和 Provider 信息。
清洗后为空的消息不会出现在 `messages` 中。如果整个会话没有可上传的自然语言消息,客户端不会发送 `session_data`
## 5. 服务端存储与幂等要求
建议新增独立的会话观察表或等价存储,不要把完整会话历史拼接进个人资料主表字段。
### 5.1 唯一键
服务端使用 token 解析出的账号 ID加上请求中的项目和会话标识构成唯一键
```text
(user_id, project_id, session_id)
```
不能只使用 `project_id + session_id`,否则不同账号之间可能发生数据覆盖。
### 5.2 写入语义
`session_data` 是完整快照,服务端收到后应执行 upsert
1. 首次收到时创建记录;
2. 同一唯一键再次收到时,用新的完整 `messages` 替换旧快照;
3. 不要把消息数组按字符串追加,否则重试会产生重复消息;
4. 建议保存服务端 `received_at`,不要用它替代客户端的 `updated_at`
5. 如果收到的 `updated_at` 早于当前已保存版本,应忽略这次旧快照,避免乱序请求覆盖较新的数据,但仍建议返回成功。
推荐字段:
| 字段 | 说明 |
| --- | --- |
| `user_id` | 从 access token 得到 |
| `project_id` | 请求中的项目标识 |
| `session_id` | 请求中的会话标识 |
| `updated_at` | 当前已保存快照的客户端更新时间 |
| `messages` | 完整清洗后问答快照JSON/JSONB 均可 |
| `received_at` | 服务端最近接收时间 |
| `created_at` | 服务端记录创建时间 |
### 5.3 删除与覆盖边界
- 客户端没有会话观察 DELETE 接口。
- 客户端删除本地项目或本地会话时,服务端不会收到删除通知,也不得据此删除云端观察记录。
- 云端观察记录不能反向恢复、修改或删除本地项目与会话。
- 如服务端需要数据保留期限,应作为独立的服务端生命周期策略,不要与本地删除事件绑定。
## 6. 个人资料版本处理
本次请求仍然携带现有个人资料字段和 `version`,服务端需要保持已有 Agent Profile PUT 的兼容行为。
同时建议将 `session_data` 视为独立的观察副作用:
- 不要使用 `session_data` 中的内容修改姓名、年龄、性别、隐私开关等个人资料字段;
- 不要把 `session_data` 返回到个人资料 GET 或 PUT 响应中;
- 成功响应仍返回完整的 Agent Profile 对象;
- 客户端的后台会话上传只使用响应中的 `version``updated_at` 更新本地同步元数据,不会用云端资料字段覆盖本地个人资料;
- 如果个人资料版本已被其他设备更新,建议允许本次观察快照独立 upsert避免仅因资料版本冲突导致观察数据永久重试如必须返回冲突请保持现有 409 错误契约并返回稳定错误码。
如果服务端选择在一次请求中同时更新个人资料和会话观察数据,至少要保证:会话观察数据不会被资料字段覆盖,且重复请求不会重复追加消息。
## 7. 成功响应
上游服务端应沿用现有 Agent Profile 接口,成功时直接返回完整个人资料对象,不要额外包一层 `session_data`
```http
HTTP/1.1 200 OK
Content-Type: application/json
```
```json
{
"display_name": "小明",
"age": 18,
"gender": "male",
"avatar_url": null,
"share_age_with_agents": true,
"share_gender_with_agents": true,
"analysis_enabled": true,
"completed": true,
"version": 4,
"updated_at": "2026-08-13T10:00:01.000Z"
}
```
Electron Main 会将上游成功响应包装为客户端看到的形式:
```json
{
"success": true,
"profile": {
"display_name": "小明",
"age": 18,
"gender": "male",
"share_age_with_agents": true,
"share_gender_with_agents": true,
"analysis_enabled": true,
"completed": true,
"version": 4,
"updated_at": "2026-08-13T10:00:01.000Z"
}
}
```
返回的 `profile` 至少要包含现有客户端所需的完整字段:`display_name``age``gender``share_age_with_agents``share_gender_with_agents``analysis_enabled``completed``version``updated_at`。头像字段按现有个人头像对接文档处理。
## 8. 错误与重试
客户端会在后台处理失败,不阻塞当前对话;失败的完整快照会留在本地并按递增间隔重试,登录后也会继续尝试。因此:
- 服务端遇到暂时不可用、超时、限流或网络错误时,应返回明确的 4xx/5xx 状态和稳定错误信息;
- 不要返回 HTTP 2xx 且声称成功、但实际丢弃 `session_data`
- 不要在失败响应中返回部分成功的 profile避免客户端误判
- 客户端当前不会拆分单个会话快照,也不会接受服务端对消息的静默截断;
- 如果服务端设置请求体大小限制,请返回稳定的错误码,例如 `session_data_too_large`,并提前与客户端确认限制;
- 建议服务端对重复请求和重试保持幂等,不要因为相同快照重复创建记录。
错误响应可以沿用现有 Agent Profile 错误结构,例如:
```json
{
"detail": {
"code": "agent_session_storage_unavailable",
"message": "会话观察数据暂时无法保存"
}
}
```
Electron Main 会把上游失败转换成客户端可识别的 `success: false` 响应;客户端会保留本地待上传数据。
## 9. 安全与隐私要求
- 通过 access token 校验账号身份,禁止使用请求体中的账号字段越权写入。
- `project_id``session_id` 和消息文本都属于用户数据,必须做账号级访问控制。
- 任何面向客户端的个人资料查询接口都不应返回会话观察历史,除非另行设计并获得产品确认。
- 服务端应将消息文本视为不可信输入,展示时进行 HTML/脚本转义,禁止直接拼接为 HTML。
- 日志中不要记录完整 `session_data`、access token、本地路径、文件内容或附件内容。
- 数据库、备份、管理后台和导出接口都应沿用用户数据的访问控制和隐私策略。
## 10. 联调请求示例
下面示例是服务端上游接口实际需要支持的请求。`<access_token>` 替换为真实登录态:
```bash
curl -X PUT 'https://square.nianxx.cn/api/user/agent-profile' \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
--data-raw '{
"display_name": "小明",
"age": 18,
"gender": "male",
"share_age_with_agents": true,
"share_gender_with_agents": true,
"analysis_enabled": true,
"version": 3,
"session_data": {
"project_id": "project_abc123",
"session_id": "ses_xyz789",
"updated_at": "2026-08-13T10:00:00.000Z",
"messages": [
{
"id": "msg_user_1",
"role": "user",
"created_at": "2026-08-13T09:59:00.000Z",
"text": "请帮我解释这个问题。"
},
{
"id": "msg_assistant_1",
"role": "assistant",
"created_at": "2026-08-13T10:00:00.000Z",
"text": "可以从两个方面理解。"
}
]
}
}'
```
## 11. 服务端验收清单
- [ ] `PUT /api/user/agent-profile` 接受可选 `session_data`,不影响不带该字段的旧请求。
- [ ] 服务端从 access token 得到 `user_id`,并使用 `(user_id, project_id, session_id)` 作为唯一键。
- [ ] 首次上传创建记录,重复上传执行完整快照 upsert不追加重复消息。
- [ ] 乱序到达的旧 `updated_at` 不会覆盖较新的快照。
- [ ] 本地删除不会触发云端删除,服务端不会设计反向删除或恢复本地会话的逻辑。
- [ ] `session_data` 不会出现在个人资料 GET/PUT 成功响应中。
- [ ] 成功响应返回完整 Agent Profile包含有效的 `version``updated_at`
- [ ] 暂时失败、限流、校验失败均返回明确状态和稳定错误信息,不返回虚假成功。
- [ ] 服务端日志、普通个人资料接口和非授权账号不会泄露会话文本。
- [ ] 已完成数据库迁移、索引和账号级访问控制,并可以用本文档中的请求完成联调。
## 12. 客户端实现位置
如服务端需要核对客户端字段或测试,可参考:
- `src/lib/agent-session-sync.ts`:快照构建、过滤、本地队列、账号隔离和重试;
- `src/lib/agent-profile.ts`:个人资料 PUT 请求类型;
- `src/stores/user-profile.ts`:将 `session_data` 伴随个人资料上传;
- `src/stores/opencode.ts`:普通问答完成后的后台触发点;
- `tests/unit/agent-session-sync.test.ts`:过滤、上传、失败重试和跨账号保护测试。