# Codex 工作动态云端同步首期后端交接

> 2026-09-30 后续对接说明：同事已返回 `team-codex-integration-guide.md`，实现以其 `/api/v1/team-codex/sync`、`tasks/list`、`events/list` 及登录 JWT 合同为准，参见 [当前实现](team-codex-local-activity.md)。本文件保留原始需求，其中队列 work_state、Token 等尚未进入已返回协议，不能当成现有字段发送。

> 2026-10-23 队列续接：使用独立 owner-only `queue_snapshot`，详见 [最新交接](Codex排队请求跨设备同步-后端交接-20261023.md)。以下历史 `work_state` 不作为当前实现字段；队列不向其他团队成员开放。

日期：2026-09-30。状态：**按用户确认的精简方向整理，接口待后端实现与确认，尚未上线。**

**本轮以这份文档为准。之前的完整云端方案仅保留作后续参考，不是本轮开发清单。**

2026-09-30 补充核心需求：除了已发生的输入输出，还须同步**同一 Codex 对话中正在执行的内容，以及用户已经加入队列但尚未执行的后续请求**。布局已确认，本机只读队列适配与列表已实现，临时数据回归通过；真实队列全流程和云端仍待验收。以下云端协议为交接提案，不表示接口已经上线。

## 1. 就做这件事

已有「本机 Codex → Arkme 本地团队工作动态」基本跑通。保留现有输入输出采集，补充真实执行/排队状态采集，再增加云端保存与读取：

```text
本地输入输出＋执行/排队状态 → 按账号和指定团队上传 → 云端保存
                                    ├─ 同账号其他电脑查看
                                    └─ 该团队成员按人、项目、任务查看
```

一个接入配置明确绑定一个账号和一个团队；后续任务自动同步，不需要每个任务粘贴一次指令。服务端复用现有账号登录、团队和成员关系。**不要求新建私有云端产品、设备证明系统、上传租约、项目路由服务或历史迁移平台。**

这里 `task` 沿用现有含义，指一个 Codex 对话，不是队列中的每一条要求。层级为「成员 → 项目 → 对话 → 当前执行与排队请求」；多个对话可以分别进行，各自保持自己的队列，不建立跨对话的总执行顺序。不把用户已写好但尚未提交的输入框草稿纳入同步，也不把 AI 自行列出的计划步骤当成用户队列。

## 2. 保留的最小规则

1. **开启一次，选清目标**：第一次启用云端时显示「账号 U → 团队 T，该团队成员可见」，用户确认后开启。原本仅本地的授权不能自动升级成云端公开。
2. **账号以登录身份为准**：后端从认证凭据确定 owner，不允许客户端指定自己代表另一个成员上传。请求中的 expected_owner_ref 仅用于防切号核对。
3. **只同步明确选择的团队**：用户加入多个团队时必须选一个，不自动全发。点击/浏览别的团队页面不改变上传目标。首期同一份本机 Codex 配置只启用一个云端目标。
4. **切号先停**：A 切到 B，先停止原上传、取消请求、清除 A 的页面缓存。A 的待传数据仍标记 A 和原团队；B 不能读或上传这些数据。切回 A 再恢复原队列，不把它换成 B 的数据。
5. **已有任务不换归属**：换团队只对之后的新任务生效，不把已绑定的任务/历史改送新团队。A 的同一个 Codex 会话在 B 登录时继续输出，先保持本地并阻断云端，不能当作 B 的新任务。归属无法确认时先不上传。
6. **后端每次检查权限**：上传者必须是目标团队有效成员；团队读取者必须有该团队访问权限。列表、摘要、筛选和正文都要检查，不能只保护详情页。
7. **重试不重复**：本地保留稳定记录 ID，收到服务端确认后才标记成功；网络失败重试同一个 ID，不删除本地原记录。

原本在多个团队建立的本地接入不代表允许全部上传；启用云端时只选择其中一个配置，其他配置仍保持本地。任务归属沿用本地记录，不根据当前页面或上传时的登录状态重新计算。

暂停只停止后续同步，不代表删除已上传内容。本地删除也不等于云端删除；如要云端删除，复用已有数据管理能力或另补删除操作，首期不伪装为已经删除云端。

**需上线前确认的一条历史规则**：本稿建议已上传内容作为该团队记录保留；离开团队后不能继续团队读写，团队现任成员仍可看已上传历史，本人可从「我的记录」看自己的内容。若团队已有不同保留规则则沿用，但请后端明确回传，不自行猜测。本人查询不额外建一个私有记录库，只是按 owner 筛选同一批已上传记录。

## 3. 数据直接沿用本地结构

| 字段 | 用途/来源 |
| --- | --- |
| owner_ref | 后端根据登录主体写入，展示时关联成员当前昵称头像 |
| team_ref | 用户选择并确认的现有团队引用 |
| source_id | 现有本地 installation.id；区分不同接入来源，首次成功写入后锁定 owner＋team，不得改绑 |
| source_name | 用户可读的电脑/来源别名，非认证凭据；不直接上传真实 hostname |
| task_id | 现有本地任务 UUID，跨重试保持不变 |
| project_key / project_name | 保留现有本地项目归组结果；project_key 为 source 内不透明键，不用于跨电脑/跨成员自动合并 |
| task_title / branch | 已有任务名称与分支展示信息，可缺省 |
| event_id | 稳定事件 ID，可使用现有本地 event.sequence 转字符串；必须与 source_id 一起使用，不能重建数据库后复用旧 source_id |
| turn_id / kind | 现有轮次及 UserPromptSubmit、Stop、Interrupt |
| text / at / truncated | 已有输入/最终回答、时间、是否截断 |
| version | 本地同一记录发生实际更新时递增；重试不递增。用于避免旧重试覆盖新内容 |
| updated_seq | 后端生成的变更序号，用于其他端查询更新，不使用客户端时间当同步游标 |
| work_state | 本次新增：对话的当前执行情况与待处理请求快照，见 3.1；独立于消息事件和任务元数据版本 |

服务端至少保存「来源绑定、任务元数据、事件」，内部可按现有存储规范组织。不需要另建项目服务。不同成员、不同来源的同名项目不自动合并，按成员→来源→项目→任务展示即可。

事件唯一键建议 `(source_id, task_id, event_id)`；source 已锁定 owner/team。还应限制同一 task/turn/kind 的逻辑槽位，避免重新生成 ID 重复插入。已有本地同轮事件可能更新，所以不能仅实现「重复 ID 一律忽略」而丢掉真正更新；采用 version 条件更新即可。

不上传绝对 cwd、worktree 路径、CODEX_HOME、transcript_path、账号密钥或工具日志。只传输入/最终回答/中断、已提交的排队请求及必要展示元数据。普通正文仍可能含敏感信息，沿用脱敏并提示用户；不得自动抓取正文链接或执行其中的内容。云端启用提示应明确包含「尚未执行的排队请求也对所选团队可见」。

### 3.1 执行与排队：复用三个接口，不增加调度系统

**当前能力证据与缺口**：输入/回答继续使用 UserPromptSubmit、Stop、Interrupt；运行状态仍由已收到的 Hook 事件推算，不是实时执行器状态。`src/team-codex-desktop-queue.ts` 现只读新版 `CODEX_HOME/queue_1.sqlite` 的 `queued_items`（包含实时 WAL），并兼容 `.codex-global-state.json` 的 `queued-follow-ups`。2026-09-30 用户真实截图暴露旧版适配遗漏新版数据源，已修正；已从新版来源读到截图中的真实待执行请求。先按 server 的 queue_order，再合并本地暂存队列，以条目 ID / client ID 去重；不同请求即使正文相同也不能合并。`codex_queue_snapshots` 本地快照表不等待 Stop 才采集。原 Hook inbox/待上传缓冲仍是传输队列，**不是 Codex 待执行队列**。

[官方 Hooks 文档](https://learn.chatgpt.com/docs/hooks)没有列出完整排队列表、排队修改/撤回事件；新增适配是当前桌面版的**非公开持久化格式兼容**，不是官方稳定队列 API。[官方 Queue/Steer 说明](https://developers.openai.com/blog/mastering-codex-remote-for-engineering)区分下一轮排队与当前轮补充；本机的 `send-now` 只表示发送意图，不能据此断言是 steer。未修改 Codex、未启用远程控制、未读取历史 transcript。

本机试用边界：

- 仅关联已由真实 Hook 绑定到当前账号/团队的 session；同一 home/session 发现跨账号归属时阻断队列导入。未产生首个接入事件的新对话暂不收录，不能用 cwd/标题猜归属。
- 默认本机 CODEX_HOME 必须与接入时 homeKey 一致；不同目录、远程/云端队列、其他设备和无法识别的格式不保证覆盖。
- 保存队列原 ID/顺序、正文（最多 4,096 字符）、附件数量、发送意图/状态；不复制附件内容、路径或 IDE 上下文。最多 200 项，超限/未知项标为不完整，不报假 0。
- 读取失败保留上次可读内容并标不可读；一部分来源可读只能报告不完整。尤其不能因为旧 JSON 缺少会话就断言新版队列为空。成功读取的旧文件仍是该来源的当前持久化快照，不能仅因文件 mtime 旧就认定队列消失；实际读取时间与来源时间分开保存。
- 当前 `ready` 只表示本机持久化列表格式校验成功，不代表 Codex 进程在线或执行状态已确认。上传映射时需分别声明 queue_source/queue_availability 与 run_state 的可信度。
- 尚未取得 queued request ID 到执行 turn ID 的可靠关联；移出列表不能猜成已执行/已完成。以下 request→turn 关联仍为目标能力，需补来源后联调。

建议每个 task 增加一个独立版本的 `work_state`，以下为**合成协议示例，并非已取得的真实数据**：

```json
{
  "version":8,
  "availability":"complete",
  "observed_at":1790762400000,
  "active_turn":{
    "turn_id":"turn_1","state":"running",
    "input_request_ids":["request_1"],
    "summary":"修复输入框布局"
  },
  "pending":[
    {"request_id":"request_2","delivery":"queue","position":1,"text":"修复后检查窄屏。","enqueued_at":1790762380000},
    {"request_id":"request_3","delivery":"queue","position":2,"text":"然后运行相关测试。","enqueued_at":1790762390000}
  ]
}
```

- `request_id`：请求刚加入队列时就有稳定标识，此时可以还没有 turn_id；编辑、调序、重试保持同一个 ID。同文的两次排队是两个请求，不以文字哈希去重。进入执行时用来源确认的映射关联实际 turn_id，不能用相似文字猜测。一个 turn 可包含多个输入，不强制一对一。
- `active_turn.state`：`running/waiting_input/interrupted/unknown`，以真实执行状态为准；确认空闲才令 active_turn 为 null。Stop 只代表本轮结束，不代表整个需求完成；拿不到运行状态或映射就明确 unknown，不伪造。
- `pending` 按真实待处理顺序返回。`delivery=queue` 表示等下一轮的请求；`delivery=send-now` 为本机来源的即时发送意图，显示「待发送」，不等同 steer。来源明确识别 `delivery=steer` 时才显示「待送入当前轮」。附带 `state=queued/pending/sending/unknown` 和 paused 标记；正在发送、结果待确认不计作下一轮排队数量。位置只在同一对话、同种 delivery 内有意义。
- `availability=complete/partial/unavailable`：只有完整、足够新的队列观察才能显示确切排队数量，包括 0；部分可见显示「至少 N 条，列表不完整」，不可读显示「队列暂不可读」。完整 pending 为空才可确认空队列，省略字段不能清空。
- `version` 为来源端按 task 持久保存的单调版本，覆盖执行状态与队列整体；真正状态变化或新的实际观察才递增，网络重试不递增。不得因重启归零；丢失版本状态时先重新核对，不用新 source 冒充旧任务。
- 已排队请求的修改、调序、移出队列、开始执行都产生新快照，云端原子替换整个 work_state。旧快照不能覆盖新快照；部分/不可读快照不能被解释成取消所有待办，可保留最后完整结果但标为过期。仅知道条目消失时不推断已完成或取消；真正开始/取消需来源证据。
- 已经执行的输入/输出仍进入现有 events，可增加 `request_id` 关联，避免同一请求既在排队区又显示为新请求。队列视图反映现状，首期不要求保存每次编辑的审计历史。
- `observed_at` 是源端实际观察时间；服务端另返回 `received_at`，与 updated_seq 区分。过久未观察应标注「状态可能已过期/最后更新于…」，不能一直声称正在运行或队列为空。无队列读取能力时不得用上传心跳伪造新的观察时间。
- 新入队应立即触发本地保存和上传，不等待 UserPromptSubmit 或 Stop；查看端沿用增量轮询。延迟目标先建议正常联网时 5 秒内可见，需真实端到端验收，不保证实时。账号/团队绑定、项目排除、暂停、退组和脱敏规则同样覆盖未执行请求。
- 首期只同步查看，不在 Arkme 中执行、编辑、调序、取消 Codex 请求，也不自动替团队成员补任务。既有 Codex 排队如何运行保持不变。

验收前必须先证明真实源端的入队、修改、移出、开始执行、重启后恢复均可被可靠观察。只收到历史输入输出不能算「队列同步完成」。无法读取时报告客户端能力缺口，不以手工演示/假数据当作完成。

## 4. 三个接口就能闭环

路径是建议，统一使用现有 Arkme 登录鉴权。复用现有团队列表和成员列表，不计入新接口；它们需要能可靠识别当前登录成员的稳定 user_ref，不能用昵称匹配本人。

所有响应沿用 `{code:200,data:...}`；错误使用对应 HTTP 状态及可识别 error_code。示例 ID 为占位符，真实 ID 沿用平台格式。时间 `at` 使用 Unix 毫秒，和现有本地数据一致。

### A. 批量上传或更新

`POST /api/v1/team-codex/sync`

```json
{
  "expected_owner_ref":"usr_example_A",
  "team_ref":"team_example_alpha",
  "source_id":"11111111-1111-4111-8111-111111111111",
  "source_name":"我的工作电脑",
  "tasks":[{
    "task_id":"22222222-2222-4222-8222-222222222222",
    "version":1,
    "project_key":"project_example",
    "project_name":"示例项目",
    "task_title":"修复输入框布局",
    "branch":"feature/example",
    "events":[{
      "event_id":"101","version":1,"turn_id":"turn_1",
      "kind":"UserPromptSubmit","text":"请检查输入框布局。",
      "at":1790762400000,"truncated":false
    }]
  }]
}
```

```json
{
  "code":200,
  "data":{
    "owner_ref":"usr_example_A",
    "task_results":[{"task_id":"22222222-2222-4222-8222-222222222222","status":"saved","version":1}],
    "event_results":[{"task_id":"22222222-2222-4222-8222-222222222222","event_id":"101","status":"saved","version":1}],
    "updated_seq":1208
  }
}
```

处理要求：

- 当前认证主体不等于 expected_owner_ref，直接拒绝并零写入；team 无权限或 source 原绑定不一致也整批拒绝，不改绑。
- 首次请求在一个事务内登记 source 的 owner/team 并写入；重试不新建来源。task 一旦属于 source，不能借另一个 source 重用 task_id 改归属。
- 相同 ID＋相同 version＋相同内容返回 unchanged；较低 version 返回 stale＋服务端当前 version，不覆盖；相同 version 内容不同返回 conflict；高版本保存更新。任务元数据和事件版本独立比较。
- 每个 task/event 都有结果；只有 saved/unchanged 可以确认本次上传成功。stale 表示本地这版被更新版本取代，不能把它当作正文一致；conflict/rejected 保留本地原因待处理。
- 响应必须在持久化后返回。ACK 丢失重传无副作用。正文较大时拆批，初始建议每批最多 100 条事件、请求 1 MiB、单条正文 256 KiB UTF-8；最终上限由后端确认。
- 旧本地历史也可用同一个接口分批补传，不单独建设导入平台。启用云端时选择「仅之后新增」或「也上传当前账号在这个团队的已有本地记录」，明确条数后确认；不扫描 Codex 原始历史、不跨账号补传。
- 每个 task 可附带 3.1 的 `work_state`；它可以早于该任务的第一条 Hook 事件到达，允许 event_count 为 0 的已排队任务。独立按 work_state.version 比较、原子保存，响应增加逐 task 的 `work_state_results`（task_id/status/version），失败不能被 events 成功的 ACK 掩盖；单独更新 work_state 不改写任务元数据。快照超出后端大小限制必须明确拒绝/报告不完整，不能截断 pending 后仍标 complete。

### B. 查询任务列表与更新

`POST /api/v1/team-codex/tasks/list`

```json
{
  "view":"team","team_ref":"team_example_alpha",
  "member_ref":"usr_example_A","source_id":null,"project_key":null,
  "limit":50,"cursor":null,"since_cursor":null
}
```

- `view=team`：team_ref 必填，后端校验查看者当前团队权限；member_ref 可选，仅用于过滤可见团队成员的数据。
- `view=self`：只查当前登录账号已上传的任务，team_ref 可选；不得指定另一个 owner。这样另一电脑只登录即可读，不需要接入 Codex。
- 返回 items 至少包含 source/task ID、owner_ref、当前昵称头像、team_ref、项目名、任务名、分支、最近时间、状态、event_count 和 task_version；完整正文不在列表返回。
- 同时返回执行/队列摘要：run_state、active_summary、queued_count、steering_count、sending_count、queue_availability、work_state_version、observed_at、received_at。未知发送结果可另计 unknown_count，不当作排队或执行中。数量须与同一完整快照对应；非完整时精确计数字段为 null，可另给已观察到的数量，不以 0 兜底。可提供 run_state 筛选；不要从最后一条输入/输出猜排队数。
- 首次按最近活动分页，返回 `next_cursor/has_more/snapshot_cursor/sync_cursor`；snapshot 与 sync_cursor 边界一致，避免首屏与增量之间漏数据。初始分页固定快照，不能边翻页边混入新排序。
- 后续传 since_cursor，返回 `changed_tasks/removed_task_ids/next_since_cursor/has_more`，只拉新增或变化。正文更新也必须推进任务的 updated_seq/task_version，使展开的详情知道需要刷新。cursor 与 since_cursor 二选一。
- work_state 变化也推进 updated_seq/task_version，即使没有新的回答或正文，另一端也必须能发现新增排队、调序及开始执行。
- 所有游标绑定账号、团队/本人视图及筛选；切换条件后重新取快照。失去团队权限立即拒绝，前端清该团队缓存，不继续显示旧正文。游标过期重新分页，不把全量都当新消息。
- 项目筛选应明确包含 source_id；同名不同项目不能混起来。跨成员分组以 owner_ref 为准，不以昵称为准。

### C. 查询单个任务内容

`POST /api/v1/team-codex/events/list`

```json
{"source_id":"11111111-1111-4111-8111-111111111111","task_id":"22222222-2222-4222-8222-222222222222","view":"team","team_ref":"team_example_alpha","limit":50,"cursor":null}
```

返回 `{code:200,data:{items:[...],next_cursor,has_more,task_version}}`；items 包含 event_id/version/turn_id/kind/text/at/truncated。首次取最近 50 条，页内正序，next_cursor 向更早记录翻页。同轮输入排在回答前；新增轮次排在之前轮次后，不按网络到达先后拼接。缺少输入的回答也能显示，但不得虚构缺失内容。

同一响应增加 `work_state` 与其服务端 received_at，包含当前执行与完整待处理列表；它独立于历史 events 的分页，不会因翻到旧消息而回到旧队列。客户端只接受更新版本的 work_state，旧分页响应不能覆盖新状态。事件有可靠关联时携带 request_id；无关联时留空，不猜。列表摘要与详情须来自一致版本；来源不可读/过期时返回真实可用性。

服务端先验证 source/task/owner/team 的对应关系及当前读取权限；不可访问与不存在均可返回 404，避免探测别人的任务。正文只按纯文本或现有安全渲染展示。

## 5. 错误与客户端最小配合

| 情况 | 后端 | 客户端 |
| --- | --- | --- |
| 未登录/凭据失效 | 401 | 停止上传，恢复原账号认证，不换账号重试 |
| 已退组/无权限 | 403 | 停止该团队上传/读取，清团队缓存；不换团队兜底 |
| owner/source/task 绑定不符 | 409 ACCOUNT_OR_DESTINATION_MISMATCH | 隔离原队列，提示切回原账号或建立正确接入 |
| 相同版本不同内容 | 409 或单项 VERSION_CONFLICT | 不覆盖，保留本地排查 |
| 不存在/不可访问 | 404 | 不展示其他人的标识、摘要或正文 |
| 游标过期 | 410 CURSOR_EXPIRED | 当前有权限时重新读取快照 |
| 超大小 | 413 | 拆批；单条过大明确提示，不假装成功 |
| 限流/临时故障 | 429＋retry_after_ms / 5xx | 原账号原团队的队列退避重试 |

插件需补上传缓冲、ACK/版本记录、云端读适配和开关。队列本地存储和只读列表已补，本机队列需完成真实用户操作验收；运行状态的实时来源、请求与 turn 的可靠关联、未开始过的新对话归属仍待补。保留现有 Hook、项目识别和本地记录。切号时缓冲、请求、缓存一并隔离；来源和目标在采集时确定，不能上传时临时猜。

本机还需保留最小「Codex 会话→原账号/团队」对应关系，不能因重新创建本地接入而把旧会话当作新账号的新任务。这是客户端隔离补充，不要求新建后端设备认证体系。现有本地隔离不等于云端版这些检查已完成。

请求中 task 的 version 是来源端元数据版本；列表响应的 task_version 是服务端整体变化版本（含正文更新），两者不能混作同一个计数器。

同一轮 Stop 已存在时，迟到的该轮输入不能使状态回退到进行中；下一轮输入才重新进行中。结束文案为「本轮结束」，不代表工作完成。离线或部分失败明确显示未同步/同步不完整。

网络恢复重试和主动暂停不同：网络故障可自动补传；用户暂停后停止继续上传，恢复时仍使用原目标。切号后回到原账号再恢复；服务端以权限检查时点裁决已在途请求，不能承诺收回切号前已经合法保存的数据。

云端存储沿用平台现有容量、保留和删除规范，后端需回传真实限制；不默认宣传永久保存。日志不记录正文/凭据，使用 HTTPS；客户端凭据只放 Host，不复制给 Codex。

## 6. 首期验收与后端回传

| 验收 | 结果 |
| --- | --- |
| A 电脑同步，B 电脑登录同账号 | 看到同一份任务与内容；B 不用安装 Hook |
| 两个团队成员分别同步 | 可按成员及项目区分，不混昵称相同的人 |
| 加入多个团队，只选甲 | 只上传甲；打开乙页面不改变去向 |
| A 账号有待传队列时切到 B | B 看不到、传不了 A 的内容；A 旧任务不重贴 B 身份 |
| 换默认团队/旧任务继续输出 | 已有记录与任务不自动转移；无法确定归属则不上云 |
| 重复请求、ACK 丢失、旧版本迟到 | 不重复，不覆盖新版本 |
| 网络中断后恢复 | 原账号原团队补传，成功才 ACK，本地原文保留 |
| 退组/被移除 | 停止新团队上传和读取，清失权缓存；历史按已确认规则处理 |
| 伪造另一个 owner/team/task/cursor | 服务端拒绝，列表和摘要也不能泄露 |
| 选择已有本地记录补传 | 同一个接口分批完成；未选的历史不自动上传 |
| 一段回答尚未结束，用户新增 3 条排队请求 | 同一对话立即显示「执行中＋排队 3 条」及顺序，其他电脑/授权团队成员无需等回答结束 |
| 排队请求编辑、调序或撤回 | 其他端更新为同一版本的新队列，不保留幽灵排队项、不猜成已完成 |
| 排队项开始执行 | 原 request_id 关联实际 turn，从待处理区移至当前执行，不重复出现；结束仅标本轮结束 |
| 同一成员多个对话分别工作 | 每个对话独立显示运行与排队，项目归属清楚，不把所有队列串成一个 |
| 当前轮补充要求与下一轮排队并存 | 分开标识，不把已送入当前轮的内容计作下一轮排队 |
| 队列不可读、来源离线、快照乱序或重启 | 不显示假 0、不伪造运行中；旧快照不覆盖新快照，可见最后观察时间 |

请后端回传：三个接口最终路径、现有登录鉴权方式、稳定本人身份映射、请求响应样例、分页/增量与错误码、大小/保留限制、团队历史规则、测试环境；另确认 work_state 独立版本/ACK、原子快照、摘要计数与变更游标。客户端另回传真实队列数据来源及实测覆盖范围，这不是后端保存接口可以替代的能力。

**本轮不做**：新增独立私有云端模块、跨电脑项目智能合并、复杂授权计划、设备密钥证明、上传租约、实时推送、历史迁移专属接口。先把这三个接口和权限校验跑通，再按实际需要扩展。
