# 个人资料会话观察同步:服务端对接说明 > 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 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. 联调请求示例 下面示例是服务端上游接口实际需要支持的请求。`` 替换为真实登录态: ```bash curl -X PUT 'https://square.nianxx.cn/api/user/agent-profile' \ -H 'Authorization: Bearer ' \ -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`:过滤、上传、失败重试和跨账号保护测试。