14 KiB
个人资料会话观察同步:服务端对接说明
Makelore Code 客户端已完成接入,本文档用于服务端实现与联调。
| 项目 | 内容 |
|---|---|
| 对接对象 | 用户服务 / Agent Profile 服务 |
| 客户端状态 | 已接入,等待服务端支持 session_data |
| 上游接口 | PUT /api/user/agent-profile |
| 数据方向 | 本地客户端 → 云端,只接收观察数据 |
| 适用模块 | Makelore Code 的本地编程会话 |
1. 需求结论
客户端会把本地编程会话的问答观察快照,伴随现有个人资料 PUT 请求上传到服务端。
服务端需要满足以下结论:
- 每次上传是某一个会话截至当前时刻的完整问答快照,不是增量消息。
- 一个请求只包含一个
project_id + session_id会话。 - 只接收用户和助手的自然语言文本;代码、路径、日志、工具调用、附件、思考过程和产物不在上传范围内。
- 服务端必须按当前登录账号归属数据,不能信任客户端传入的用户身份字段。
- 同一账号、同一项目、同一会话重复上传时执行幂等更新。
- 本地会话删除不会触发云端删除;客户端没有上传删除事件,也不会请求服务端恢复或下发会话。
- 客户端登录后不会主动遍历并上传所有旧会话;只有本地发生新一轮问答并完成后才会上传该会话的完整快照。
2. 请求链路
客户端通过 Electron Main 的 Host API 代理访问服务端,服务端只需要实现上游接口:
Renderer
→ PUT /api/works/user/agent-profile
→ Electron Main 代理
→ PUT /api/user/agent-profile
服务端不需要实现 /api/works/user/agent-profile。服务端收到的认证方式保持现有个人资料接口不变:
PUT /api/user/agent-profile
Authorization: Bearer <access_token>
Content-Type: application/json
用户身份必须从 access token 解析。请求体中没有、也不应新增可指定其他用户的 user_id、username 或类似字段。
3. PUT 请求体
现有个人资料字段保持兼容,在可选的 session_data 字段中增加会话观察快照:
{
"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,加上请求中的项目和会话标识构成唯一键:
(user_id, project_id, session_id)
不能只使用 project_id + session_id,否则不同账号之间可能发生数据覆盖。
5.2 写入语义
session_data 是完整快照,服务端收到后应执行 upsert:
- 首次收到时创建记录;
- 同一唯一键再次收到时,用新的完整
messages替换旧快照; - 不要把消息数组按字符串追加,否则重试会产生重复消息;
- 建议保存服务端
received_at,不要用它替代客户端的updated_at; - 如果收到的
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/1.1 200 OK
Content-Type: application/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 会将上游成功响应包装为客户端看到的形式:
{
"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 错误结构,例如:
{
"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> 替换为真实登录态:
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:过滤、上传、失败重试和跨账号保护测试。