# Nook 常驻桥接接入说明（v2，即时传输）

服务地址：https://nook.yunniao.hk

目标链路：手机 → Nook 服务 → 即时长轮询 → 常驻 Nook Bridge → Codex App Server turn/start → Nook 服务 → 手机。

不要另装用于接收手机消息的定时轮询任务，也不要让模型每隔一分钟自行取件。此限制仅针对消息传输，不限制用户请求的提醒、定时业务和卡片自动更新；这些由 Bridge 的 scheduleActions 持久化调度入口处理。安装或升级能力本身不创建、恢复任何具体业务任务。接收工作由持续运行的普通程序完成；长轮询有消息立即返回，25 秒是无消息时的连接续期上限，不是收到消息后的等待时间。

## 1. 安装桥接程序

需要一台可持续运行、能够访问此服务的 Mac 或 Linux 主机，Node.js 22+，以及本机现有的 Codex CLI。沿用当前 Codex 环境，不查询账号或登录状态，不要求用户重新登录，也不以模型测试作为安装前置条件。若当前服务使用 Tailscale，手机与桥接主机都必须能访问同一 Tailnet 的服务地址。

读取本说明后，先向 `POST https://nook.yunniao.hk/v2/agent/setup` 发送 JSON `{ "code": "用户提供的一次性配对码", "stage": "download", "status": "running" }`，让手机显示“准备接收程序”，再下载并解压。此请求不会消费配对码，不要提前 claim。若下载或解压失败，用同一接口上报 `status: "failed", errorCode: "download_failed"`，并在原对话说明本机错误。不要把配对码写进文件、日志或回复。进度请求遇到临时网络故障可重试，不能用虚假进度代替实际安装。

桥接程序源码包：https://nook.yunniao.hk/bridge/nook-bridge.tar.gz

源码中的 Bridge/README.md 包含启动和常驻服务配置。下载、解压到固定的本地目录后，在该目录运行：

```sh
NOOK_SERVER='https://nook.yunniao.hk' NOOK_PAIRING_CODE='<用户单独提供的一次性配对码>' node Bridge/main.mjs --install
```

配对码只能交给桥接程序认领一次。不要先手动调用 claim 消费配对码，再让程序用同一个码启动。启用加密时，首次启动还需通过 NOOK_CONTENT_KEY 传入用户单独分享的 base64 32 字节密钥；不启用时省略。

桥接程序将凭据、内容密钥、待处理请求、待发送事件和自己创建的 Codex thread ID 保存到权限 0600 的本地状态文件。默认是 work/bridge/state.json；可通过 NOOK_BRIDGE_STATE 指定。不要放进代码仓库、回复、日志或模型输入。后续重启不再传 NOOK_PAIRING_CODE；程序直接恢复该文件。

`--install` 保存配对后直接安装并启动 launchd / systemd 用户服务，安装进程不报告就绪。只有最终常驻进程完成初始化后才向手机报告就绪。禁止先用前台进程报告成功，再停机切换后台；禁止把会话创建、状态恢复或安装工作留到用户点 OK 或发首条消息之后。直接安装并启动；仅在实际启动或处理消息发生错误时报告对应失败，不添加账号、登录或模型可用性预检。

## 初始化阶段

手机按服务端真实状态逐步切换 SVG 插画与当前说明，不罗列全部步骤；百分比按已完成阶段计算，最后就绪才显示 100%。阶段顺序为：准备接收程序 → 配对手机 → 安装常驻服务 → 启动本机助手 → 准备对话与本地状态 → 开启即时收发 → 可以开始对话。快步骤可能直接变为已完成；不要人为 sleep 展示动画。

下载阶段由安装 Agent 上报；claim 自动更新配对阶段；后续由 Bridge 自动调用 `POST /v2/agent/setup`，使用 Agent Bearer token，发送 `{ "stage": "installing|starting|session|receiver", "status": "running" }`。失败发送对应阶段和 `status: "failed", errorCode: "阶段名_failed"`。仅固定阶段和错误码会保存；禁止发送错误原文、消息正文、令牌、文件路径或内容密钥。`GET /v2/app/status` 的 setup 字段包含 stage、status、updatedAt（Unix 毫秒）、version 和可选 errorCode。错误必须停留在发生的步骤，重启从 starting 重新推进。

只有正式 `/v2/agent/ready` 能把最后一步置为完成。手机初始化页面在前台时保持亮屏；完成、失败、离开页面或切到后台后恢复原先的自动息屏设置。用户仍可主动锁屏。

## 2. Codex 会话与权限

桥接程序通过本机 stdio 启动 `codex app-server --listen stdio://`，完成 initialize / initialized 后创建自己的专属会话，以后恢复同一 thread ID。模型选择沿用本机 Codex 配置。

不要自动复用当前桌面对话，不要仅因为 thread/read 能读取某个 ID 就认为能安全共用。独立 App Server 对桌面进程中的活跃状态没有足够的验证保证；并发写入会混入手机与桌面的消息。当前桥接程序不接受任意桌面 thread ID。

收到一个新请求后，程序先落盘并发送 receipt，再直接发起 turn/start。只有 turn/start 被接受才发送 processing；只有 turn/completed 的状态为 completed 且输出通过校验，才发送最终回复和 completed。工作串行处理，新到消息仍可并行接收和落盘。权限与交互请求不会被程序自动批准；需用户进一步处理时，向手机明确报告未完成。

模型指令源码：Bridge/prompts/codex-turn.md。桥接程序不给模型发送配对码、访问令牌或内容密钥；模型只接收用户请求和当前业务快照，不负责传输层。

## 3. 接收、就绪与确认 API

1. `POST /v2/agent/claim`，JSON `{ "code": "一次性码" }`，返回 connectionID、agentToken、transport=long-poll、maxWait=25。认领与保存由桥接程序完成。
2. 后续请求使用 `Authorization: Bearer <agentToken>`。
3. `GET /v2/agent/requests?wait=25` 等待消息。响应为 `{ "packets": [...] }`；无消息最多等待 25 秒。收到消息或空响应后立即建立下一次等待，不额外 sleep。网络故障才使用指数退避重连。
4. 启动时先 `POST /v2/agent/not-ready` 清除旧就绪。最终常驻进程完成 Codex 握手和会话创建或恢复、持久化恢复，处理完旧积压，并启动接收、处理、回传循环后，`POST /v2/agent/ready`，JSON `{ "receiverInstalled": true, "transport": "long-poll", "codexReady": true }`。这是可以立即接收并处理首条消息的就绪声明，不代表某项业务操作已成功；此后不再执行安装或切换进程。正常关闭时调用 `/v2/agent/not-ready` 撤销就绪。
5. 先原子保存每个 request ID 和请求正文，再生成 `type=receipt, requestID=原请求ID` 事件。随后 `POST /v2/agent/requests/<ID>/ack` 清理中转队列。崩溃前未落盘不得 ack。
6. Agent 事件使用 `POST /v2/agent/send` 发送。HTTP 202 只表示服务器暂存，不等于手机收到。程序保留原始事件 ID 并重试，查询 `GET /v2/agent/deliveries`，手机明确确认后才删除待发事件。
7. 手机可用 `GET /v2/app/events?wait=25` 即时等待回执与回复，保存后调用 app events ack。旧的无 wait GET 接口仍可用于兼容和诊断。

## 4. 数据与处理状态

明文包：`{ "id":"事件ID", "envelope":{ "id":"同一事件ID", "type":"..." } }`。

- 手机请求类型：message、answer、shortcut、state_request。包含 language、assistantID，以及相应的 text、questionID、responseID 或 template。message 可附带 attachments 数组（最多 6 个、原始内容合计 8 MiB），每项包含 id、name、mimeType、byteCount 和 Base64 data；支持该功能的 Bridge 在 state.capabilities 中返回 attachments-v1。附件与消息一起加密，中转单包上限 16 MiB。
- `receipt`：requestID。表示桥接程序已经可靠接收，不表示 Codex 已开始或业务完成。
- `status`：requestID、status（processing / completed / failed）、statusVersion（同一请求严格递增的正整数）。手机忽略过期状态，避免重连后从完成退回处理中。
- `message`：message 对象包含 id、assistantID、role="assistant"、text、createdAt、delivery="received"。createdAt 为从 2001-01-01 UTC 起的秒数（Swift Codable Date）。可同时带 requestID。
- `state`：snapshot 为 Nook/state/v1，包含 revision、assistants、pending_questions、operations、cards。空数组也必须保留。普通重开 App 的 state_request 由桥接程序从本地快照响应，无需额外调用模型。
- `result`：requestID、result（JSON 对象字符串）。shortcut 的 template 也是 JSON 对象字符串；保留其键名与值类型。

Assistant 字段：id、name、symbol（SF Symbol）、preview、version。
Question 字段：id、assistantID、title、detail、options:[{value,label}]、allowText、version。
Operation 字段：id、assistantID、title、status、detail、version。
卡面 JSON Schema：https://nook.yunniao.hk/schemas/card.schema.json。卡面不包含可执行 HTML。

同一张卡片轮播多组数据时，保留原 ID、模板和所属助手，添加 `carousel: {"intervalSeconds": 5, "pages": [...]}`。顶层内容是第 1 页，`pages` 是额外的 1–9 页，每页必须包含 `primaryInformation`、`primaryDescription`、`first`、`second`，可选 `dataSource`。App 支持 3–60 秒间隔，默认 5 秒；桌面小组件使用约 5 分钟间隔的系统时间线，实际时间由系统决定；实时活动与灵动岛显示第 1 页。轮播只是展示切换，不刷新来源数据、不增加版本或触发计划任务。数据更新仍修改同一张卡片并增加版本，保留来源和演示声明。

- 新建动态卡片：仅在用户要求新建或没有相关原卡片时，创建新 ID、`version: 1`，填写完整卡面字段和 `carousel`，追加到完整快照；每个分页不创建独立卡片。
- 静态转动态：在当前快照中定位原卡片，保留 ID、所属助手、原第 1 页内容、来源和未要求修改的外观字段，添加 `carousel`，递增该卡片 `version`，在原位置替换；保留其余卡片、助手、未解决问题与操作。Bridge 分配新快照 revision。用户已明确要求转换时直接执行，不重复确认；目标不明确或分页数据缺失时才澄清，不编造数据。
- 修改分页数据或轮播间隔同样递增版本；仅展示翻页不递增版本。用户要求恢复静态卡片时移除 `carousel` 并递增版本，默认保留第 1 页，也可保留用户指定页。

桥接程序在 model turn 完成后保存新快照并递增 revision。保持业务条目的稳定 ID；对过期问题的回答明确报告冲突。普通收悉与业务成功相互独立。

## 5. 重试、重启与恢复

中转正文只在内存保存：App → Agent 当前 45 秒；Agent → App 5 分钟。服务器重启不会保留正文、在线状态、待认领码或短期送达记录。手机和桥接程序都必须保留原 ID 的本地重试状态。

桥接程序去重后只启动一次 Codex turn。已排队但未启动的请求可在桥接重启后处理；崩溃时已处于 starting / processing 的请求会被标记为结果不确定并告知用户，不自动再启动一次可能重复产生副作用的操作。用户检查后可发送一个新的请求。

重装手机后，恢复业务内容需要保留或导入桥接主机保存的快照。不要声称中转服务器保存了历史聊天或业务数据。新的配对默认使用新的桥接状态文件；恢复旧业务状态请按 Bridge/README.md 操作，不能将旧连接的待发消息直接转给新手机。

## 6. 可选内容加密

密钥由用户手动交给手机和桥接程序，绝不提交中转服务器，也不放入 Codex 的模型上下文。使用 AES-256-GCM，每次发送使用随机 12 字节 nonce，编码为 `base64(nonce || ciphertext || 16-byte tag)`。包为 `{id,sealed}`，加密内容是完整 envelope JSON，内部 id 必须与外部一致。

拒绝认证失败、ID 不一致和明文降级。此方案是手动配置的共享密钥加密，不提供前向保密。重装或更换绑定时按新连接重新交换密钥。用于交接密钥的助手平台本身可能接触分享文本，不宣称密钥对该平台不可见。

## 开发验证（不属于安装步骤）

开发时可用手机消息检查：桥接落盘 → 已收到 → Codex turn/start → 正在处理 → 真实回复，并验证重复请求、断线重连与进程重启。这些是开发验证，不要求安装 Agent 执行，也不阻塞初始化。正常使用时，真实请求的执行错误按处理状态回传。
