334 lines
14 KiB
Markdown
334 lines
14 KiB
Markdown
# 个人资料会话观察同步:服务端对接说明
|
||
|
||
> 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`:过滤、上传、失败重试和跨账号保护测试。
|