# 「发给自己」多角色对话：跨端同步需求与接口交接

状态：2026-09-27 插件已有本机试验版；本文是**待后端实现的需求与接口建议**，不代表接口已上线。新接口路径和字段名称均为语义示例，优先沿用现有消息、文件和实时通道。**历史本机角色消息的跨端回填是本需求的上线条件，不是可选的后续优化。**

## 1. 核心需求

用户在「发给自己」中可以创建多个代表自己的发言角色（名称、可选头像），发送时选择「我」或某个角色。普通「我」消息仍在右侧；角色消息在左侧展示角色发送当时的名称和头像，形成自己与自己对话的体验。消息详情的每条延展也应展示**该条延展自己的**发送时角色，而非父消息的角色。

跨端目标是：在 Web、桌面、iOS、Android 等任一端创建/修改角色或发送角色消息后，**同一账号的其他在线端及时看到相同的角色列表与消息展示**；离线端重新进入后能够补齐。**接口上线前已经用本机试验版发出的角色消息，也必须在该本机完成回填后同步到其他端，不能变回普通「我」。**这里的角色只是展示身份，消息的真实作者、所有权、权限和审计身份始终是当前账号，不能伪造第二个用户，也不能给自己制造未读或通知。

角色选择属于当前端的输入草稿状态：在「发给自己」会话内保留，离开后恢复「我」。不建议将“当前选中哪个角色”跨设备同步，否则两端同时输入会互相改变发送身份。

## 2. 当前实现与差距

| 项目 | 当前插件试验版 | 跨端所缺 |
| --- | --- | --- |
| 角色名称、角色 ID | 本机、按账号保存在插件 SQLite | 云端账号级角色资料和增量同步 |
| 自定义头像 | 新创建的角色头像只写入按账号隔离的插件本机目录；旧试验版曾得到 `file_asset://` 引用，现尽力迁入本机 | 跨端版可复用经鉴权的云端文件能力及长期引用机制 |
| 消息角色归属 | 本机用消息 `recordUid` 绑定角色，并保存发送时名称/头像快照；详情延展也按各自 `recordUid` 补快照 | 新消息写入时由服务端原子保存角色快照；旧本机快照可验证、幂等回填，所有端回读一致 |
| 消息正文 | 仍走现有「发给自己」消息接口，并非离线消息 | 不需重做正文服务；需扩展角色字段与返回值 |
| 变更通知 | 只有当前插件进程可见；不同设备和隔离的预览端口不共享 | 复用现有账号级 `projection.invalidated.v1` 通道，补角色投影及可靠回读 |

本机 SQLite 表为 `self_role` 与 `self_role_record`，头像图片在账号隔离的本机目录。`self_role_record` 即使角色已经删除仍保留发送时快照。因此当前看到的角色左/右布局只在存有这份本机数据的环境中成立；另一设备即使读取到同一条正文，也没有角色快照。

同一台电脑上的两个窗口**只有在明确共用同一插件数据目录时**，才有机会重读这份 SQLite；不同端口对应的独立预览目录并不会自动共用，而且共用文件也没有跨进程即时变更通知。这种本机共享不能替代 Web、桌面和手机之间的账号级同步。

## 3. 后端最少需要提供的能力（优先级顺序）

### P0-A：账号级角色资料读写

- 列表读取、创建、修改名称/头像、删除角色；均由登录态确定账号，不信任客户端提交的 `ownerUserId`。
- 建议字段：`roleId`（稳定 UUID）、`name`（1–20 字）、`avatarAssetRef?`、`revision`、`createdAtMillis`、`updatedAtMillis`、`deletedAtMillis?`。
- 同账号多端读到相同结果；写入返回更新后的角色与单调递增的账号级 `revision`，用于断线补偿和冲突检查。
- 删除只影响以后能否选择该角色；**不能改写旧消息的角色名称/头像快照**。已删除角色也不能再用于新消息。
- 当前试验版最多 20 个角色；请确认后端是否沿用这一限制。

### P0-B：现有「发给自己」写消息时携带角色 ID，并保存不可变快照

- 扩展现有文本、富文本/附件、主题内消息及延展/回复写入路径：`displayRoleId?: string`；省略表示普通「我」。不用为了角色复制一条私聊消息。
- 现有插件写入入口包括 `/api/v1/records/create`、`/api/v1/topics/records/create`、`/api/v1/records/extensions/create`；建议扩展这些已有请求，而不是另起“角色聊天”消息接口。其他附件发送路径也要核对是否最终进入同一记录写入链。
- 服务端在**同一消息提交事务**中校验该角色属于当前账号、读取当时的名称/头像，并写入 `displayRoleSnapshot = { roleId, name, avatarAssetRef? }`。真实 `senderUserId` 和已有 `sender_snapshot` 保持当前账号；`sender_snapshot` 是真实作者的历史资料，不能拿来存角色，否则权限与历史作者语义混乱。
- 继续沿用现有 `recordUid` 幂等键。相同 `recordUid` 的重试必须得到同一消息及同一快照；若重试要求改成另一角色，明确返回冲突，不能悄悄改写已发送消息。
- 返回结果须包含最终 `recordUid` 与角色快照，避免客户端先乐观展示、刷新后跳回右侧。

### P0-C：所有相关读取入口返回同一消息快照

- 「发给自己」总流、主题内时间线、消息详情、搜索结果、分页前后定位、延展/回复树都返回**每条记录自己的** `displayRoleSnapshot?`；延展树不能把父消息角色复制到子延展。
- `displayRoleSnapshot` 缺失的历史消息按普通「我」显示；不能根据当前角色名称反推历史身份。
- 角色消息的归属、删除、编辑、转发、审计等权限仍按真实账号判断；展示在左侧不等于收到了别人的消息。

### P0-D：其他在线端及时获知变化，断线可补齐

- 已有账号级 SSE `/api/v1/sse/chat/noty` 和 `projection.invalidated.v1`，适合沿用“收到失效通知后回读”模式；角色资料可增加 `self_role` 投影，记录变更可复用 `record` 投影。请后端确认目前自己发送的记录是否也向该账号的其他在线端发出 `record` 失效通知，以及从通知到回读是否已保证读到新快照。
- 实时通知不能代替持久化同步。角色目录须有 `revision`/增量查询；记录可按现有游标或更新时间回查。若 SSE 不能提供断线回放，客户端重连时须全量/增量回查，覆盖首屏查询到订阅建立之间的空隙。不能只依赖一次通知。
- 在自己的另一端同步时，不增加对方未读，也不向自己推送“有人给你发消息”的通知。

### P0-E：跨端头像引用

- 通用文件上传能力已经存在，原则上不用为头像重新造一套上传接口；需要确认现有文件资产能作为长期、仅同账号可读的角色头像，且角色删除后旧消息快照引用的头像仍可读取。若现有 `file_asset://` 会因未被消息附件引用而清理，则需补角色头像持久引用/专用生命周期。

### P0-F：现有本机试验数据迁移（跨端上线条件）

- 必须提供一次性、可重试、幂等的**本机角色与历史快照导入**能力：按账号导入当前角色资料和历史消息 `recordUid → displayRoleSnapshot`，包括主题及延展中的记录。后端逐条核验消息真实作者属于当前账号、来源是「发给自己」，不复制消息正文、不新增消息、不改变真实作者。
- **已删除角色的历史消息也要迁移**：本机仍有快照但当前角色列表中没有该角色。服务端须接受“只有历史快照、没有可选角色”的回填，不能要求先重新创建角色，更不能让旧消息换成角色当前昵称/头像。
- 本机头像引用不能直接提供给其他端。客户端先通过现有鉴权文件能力上传仍可读取的图片，再把云端长期有效的头像引用写入角色资料和历史快照；旧云端 `file_asset://` 可经验证后复用。若图片已不可用，仍导入角色名称与绑定，头像缺失要有明确结果，不得因此丢掉整条角色归属。
- 多台旧设备或独立预览端口可以各自有本机数据；每个数据目录都要在升级后各自提交其本机增量，服务端按稳定 `roleId` 与 `recordUid` 幂等合并。冲突必须逐条返回 `accepted` / `existing` / `conflict` / `rejected` 与原因，不可静默覆盖云端已有快照。
- 客户端须分页读取本机绑定、分批上传并保存逐条回执；断网、切账号、进程中断后可继续。云端回读验收前**不得删除本机记录或头像**。过渡期先取云端快照，云端尚未回填的消息继续以当前端本机快照显示。
- 本机分页游标按 `recordUid` 排序，只保证单轮遍历；UUID 不代表创建时间，新写入的记录可能落在已扫过的游标之前。迁移端要按逐条回执判定完成，并从头复扫未确认记录，不能把最后一个游标当作永久同步水位。
- 若保存本机 SQLite/头像的安装或预览目录在迁移前被删除，后端无法凭消息正文推断原角色；须保留或备份原数据目录，直到该目录完成回填。

## 4. 建议的接口形状（语义示例，不要求照抄路径）

```text
GET    /me/self-roles?sinceRevision=<n>       -> { roles, deletedRoleIds, revision }
POST   /me/self-roles                         -> { roleId, name, avatarAssetRef? }
PATCH  /me/self-roles/{roleId}                -> { name?, avatarAssetRef?, expectedRevision? }
DELETE /me/self-roles/{roleId}                -> { revision }

现有「发给自己」发送接口新增请求字段：displayRoleId?
消息返回、时间线、详情、搜索新增字段：
  senderUserId: <真实账号 ID>
  displayRoleSnapshot?: { roleId, name, avatarAssetRef? }

沿用账号级 projection.invalidated.v1 通知（示意）：
  projection=self_role -> { revision }，客户端增量读取角色
  projection=record    -> { recordUid? }，客户端回读对应记录
  重连/漏通知时按 revision 或现有记录游标补读

迁移接口（可批量分片，路径仅示意）：
  POST /me/self-roles/import -> { roles[], recordSnapshots[], idempotencyKey }
  recordSnapshots[] = { recordUid, displayRoleSnapshot: { roleId, name, avatarAssetRef? } }
  返回逐条 accepted / existing / conflict / rejected、原因与最新 revision
```

如果现有消息推送已覆盖「发给自己」，无需再建第二条消息通道；只需保证事件触发后能读到最新角色快照。接口命名、分页、错误码可遵循现有后端规范，但上述语义不能缺。

## 5. 插件当前阶段：不新增后端能力的本机试验版

本阶段只让**新建及后续角色功能数据**本机化：角色资料、自定义头像图片、消息角色绑定及历史快照留在当前插件的账号隔离本机存储；发给自己的消息正文继续使用已有服务。创建/选择角色不新增角色相关云端写入。旧头像若暂时无法迁入，仍可能保留原云端图片引用。不同设备、不同独立预览配置不保证角色同步；另一端没有本机快照时会按普通「我」展示，不得假装已同步。本机现提供按账号、稳定游标分页读取历史绑定的内部迁移清单；后端接口未上线，清单**尚未上传**。

自定义角色头像已改为专用本机保存入口、按账号隔离的持久文件和本机鉴权读取。插件打开角色列表/相关消息时，会尽力把旧试验版 `file_asset://` 图片复制到本机，同时更新本机角色与历史消息快照引用；图片暂时读不到时保留旧引用，不妨碍打开列表。**已上传过的历史头像不会因为本次改动而从云端删除**；清理云端原件须另行确认文件所有权与删除策略，不能盲删。

本机角色资料的备份/迁移要与消息正文区分：现有正文在云端，本机角色展示元数据若卸载或清空对应本机数据可能丢失。跨端接口落地前不要将“登录同账号”宣传成能恢复角色资料。

### 5.1 角色尚未同步时的头像兼容规则（2026-09-28）

- 当前端有该消息的本机角色快照：继续显示该角色头像（未设图片时使用角色文字头像），保留左侧角色排布。
- 普通「我」，或另一台设备尚未获得该消息的角色信息：优先使用**当前账号头像**，保留右侧排布。即使消息携带旧头像资产、失效图片地址或其他设备的本机头像引用，也不能让这些引用优先于可用的当前账号头像。
- 上述规则统一覆盖「发给自己」、个人主题内的消息列表、消息详情及本人延展回复；不替换其他作者或私聊群聊的头像。不增加“当前资料”等提示文案。
- 这是显示层回退，不是角色同步。不得把回退得到的账号头像写成角色历史快照，不得清除本机角色绑定。未来云端角色快照回填成功后，其他端再按真实角色信息展示。
- 当前账号资料尚未加载时可暂用记录已有头像；资料到达后自动切到账号头像。账号本身没有设置头像或网络无法读取头像时，仍使用既有默认显示，不能伪造头像。

## 6. 联调验收

1. A 端建角色并设头像，B 端在线时及时出现；B 端断网后重连也能补齐，账号 C 绝不可读。
2. A 端用该角色发文本、图片/附件、主题消息及延展，B 端均在左侧看到发送时名称/头像；普通「我」仍在右侧。
3. 修改或删除角色后，旧消息快照不变，新消息使用新资料或禁止使用已删除角色。
4. 相同 `recordUid` 断线重试不产生重复消息、不换角色；切账号后不串角色数据。
5. A/B 同时编辑不同草稿，不同步彼此“当前选择”；角色资料和已发送消息仍同步。
6. 列表、详情、搜索、分页、刷新和重新登录的展示一致；角色消息仍由真实账号拥有，不生成自发未读/通知。
7. 本机旧数据迁移后的消息条数不变，逐条核对快照及头像；冲突有可审计结果。
8. 迁移前已删除的角色、直接在详情页发送的延展、多个独立本机目录分别导入后，其他端均按各条消息发送时的角色显示；断网重试和重复导入不改写快照。
9. 同步接口上线前：A 的本我消息显示当前账号头像，A 的角色消息保留角色头像；B 无对应角色信息时统一显示当前账号头像。列表、详情、延展回复一致；回退前后本机角色绑定及历史快照保持不变。

## 7. 请后端同事优先确认

- 现有「发给自己」各发送路径能否统一接收 `displayRoleId` 并在写消息事务中存快照？是否有路径会改写/重新分配 `recordUid`？
- 现有账号级实时通道是否已覆盖自身消息与角色变更？事件游标和断线补偿怎么做？
- 现有文件资产能否作为长期、仅本人可读的角色头像；若不能，最小补充能力是什么？
- 历史 `recordUid` 批量关联角色快照的安全迁移接口是否可作为 P0 提供？是否允许“角色已删除，仅保留历史快照”的导入？

建议先返回这四项的现有能力与缺口，再定最终接口路径和字段名。
