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

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

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

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

## 1. 安装桥接程序

本文供安装 Agent 按顺序执行。正式服务固定为 `https://nookme.yunniao.hk`；不让用户填写其他地址。所有前置检查通过后，才认领配对码并安装常驻服务。已有 Node / Codex 可用时直接复用，不重复安装或无条件升级。

这些步骤由安装 Agent 执行；当前 `node Bridge/main.mjs --install` 不负责安装 Node / Codex，也不能替代下面的联网和环境检查。没有通过检查时，说明具体失败项并修复，不把“已下载”报告成“已连接”。

### 1. 确认安装位置和现有环境

| 项目 | 要求与检查方式 | 未满足时 |
|---|---|---|
| 主机 | 可持续运行的 macOS 或 Linux；`uname -s`、`uname -m` | 当前常驻安装不支持原生 Windows；不能假装安装成功 |
| 基础工具 | `command -v curl`、`command -v tar`；解压和 HTTPS 下载可用 | 使用主机已有包管理器补齐；不假定 Linux 自带这些工具 |
| Node.js | `command -v node`、`node --version`；主版本至少 22 | 按第 3 节安装，或给当前安装进程提供合适版本的 PATH |
| Codex | 优先复用安装 Agent 已在使用的 Codex 可执行文件；其次 `command -v codex` | 按第 4 节安装或设置绝对路径 |
| 后台服务 | macOS 使用当前登录用户的 launchd；Linux 使用当前用户的 systemd | 先解决用户服务不可用的问题，不临时换成前台进程冒充常驻 |
| 存储 | 固定安装目录、私有状态目录及工作目录可写，磁盘有足够空间 | 不使用会自动删除的临时目录保存最终程序或状态 |
| 现有绑定 | 检查已有服务及状态文件是否存在，只检查必要字段 | 已安装则走维护/恢复流程，禁止覆盖已有绑定、重复认领或并行启动 |

检查命令不包含账号查询，不读取登录凭据，不发送模型测试请求。`codex --version` 和 `codex app-server --help` 仅用于检查程序和子命令存在；它们不能证明模型调用可用。真实握手、会话恢复和错误报告由正式常驻进程完成。

macOS 检查 `launchctl print "gui/$(id -u)"` 是否成功；仅在 SSH 会话且没有对应 GUI 用户域时，不要继续按桌面安装方式操作。Linux 检查 `systemctl --user is-system-running`；`running` 可继续，`degraded` 需检查是否影响 NookMe，无法连接用户总线则停止安装。普通用户服务不自动保证注销后继续运行；若确需注销后常驻，应根据用户的实际运行需求配置 linger，而不是默认改动系统策略。

安装目录和私有目录示例（只在不存在时创建，不移动或覆盖现有安装）：

```sh
umask 077
NOOK_INSTALL_ROOT="$HOME/.local/share/nook-bridge"
mkdir -p "$NOOK_INSTALL_ROOT/releases" "$NOOK_INSTALL_ROOT/private" "$NOOK_INSTALL_ROOT/workspace"
test -w "$NOOK_INSTALL_ROOT/private"
df -h "$NOOK_INSTALL_ROOT"
```

### 2. 安装前联网检查：必须执行

先测试实际安装主机的出站网络。不能以 Agent 自己的网页搜索工具能打开页面，代替目标主机的网络检查；也不能仅凭 ping 成功判断 HTTPS 可用。

```sh
curl --fail --silent --show-error --proto '=https' \
  --connect-timeout 10 --max-time 30 \
  https://nookme.yunniao.hk/health
```

合格结果是 HTTPS 证书校验通过、HTTP 200、JSON 中 `ok` 为 `true`。这一步覆盖本次请求的解析、连接、TLS 和 NookMe 健康检查；不能据此声称所有外部服务或 Codex 模型都可用。然后检查说明页确实返回 Markdown，而非代理登录页或 HTML 错误页：

```sh
curl --fail --silent --show-error --proto '=https' \
  --connect-timeout 10 --max-time 30 \
  https://nookme.yunniao.hk/connect
```

仅缺少依赖、确需下载时，再检查对应来源：Node 官方下载页及选定版本的实际下载 URL；Codex 官方安装脚本及其使用的下载源。不要为了已有可用依赖强制联网安装或升级。下载阶段还必须实际成功取得完整 Bridge 压缩包；HEAD 请求或主页可达不能代替包下载验证。

按失败类型处理：

| 现象 | 处理 |
|---|---|
| DNS 解析失败 | 检查主机 DNS、网络连接及代理配置；不要永久写入临时源站 IP |
| 连接超时/拒绝 | 检查主机出站 HTTPS、代理或防火墙、服务可用性；重试应有次数上限 |
| TLS/证书错误 | 检查系统时间、受信任根证书和代理证书；不使用 `curl -k` 或关闭 Node TLS 校验 |
| 403/代理登录页 | 检查企业代理、网关或服务访问限制；不将其误判为配对失败 |
| 502/503/504 | 保留现有状态，有限重试；仍失败则报告服务暂不可用 |
| 下载失败/解压失败 | 不执行不完整文件，不继续 claim；重新取得完整包后再继续 |

保留用户已有有效代理；不要批量清空 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 等变量。检查代理时只报告是否设置，不能打印含用户名、密码的代理 URL。curl 与 Node 的网络行为可能不同：Node 准备好后，必须用将运行 Bridge 的同一个 Node 和同一个环境再检查一次：

```sh
node --input-type=module <<'JS'
try {
  const r = await fetch('https://nookme.yunniao.hk/health', {
    signal: AbortSignal.timeout(15000), redirect: 'error'
  });
  if (r.status !== 200 || (await r.json()).ok !== true) throw new Error();
  console.log('NookMe HTTPS health: OK');
} catch {
  console.error('NookMe HTTPS health failed in the Bridge Node environment');
  process.exitCode = 1;
}
JS
```

若 curl 成功而 Node 失败，停止并修复 Node 实际使用的代理/证书配置，然后重测。不要把代理秘密写进回复或日志。当前服务安装器只显式写入 PATH 和 NOOK 配置，不会把安装终端的全部代理、证书环境自动复制到后台服务；依赖这些环境的主机必须为用户服务配置所需变量，并核对后台运行结果。开通公网入站端口不是安装要求。

### 3. 缺少 Node.js 时安装

Bridge 要求 **Node.js 22+**，仅使用 Node 内置模块，无第三方 npm 依赖。不要对 Bridge 运行 `npm install`，不需要安装 Go、数据库或编译整个 iOS 项目。

1. 已有版本至少为 22 且可执行时复用；存在多个版本时确认实际路径，不只看交互终端的别名。
2. 缺少合适版本时，从 [Node.js 官方下载页](https://nodejs.org/en/download) 选择仍受支持、主版本至少 22 的 LTS 版本，匹配 `uname -s` 和 `uname -m`。macOS 的 `arm64` 是 Apple Silicon、`x86_64` 对应 x64；Linux 的 `aarch64` 对应 arm64、`x86_64` 对应 x64。其他平台需核对官方支持，不猜测架构。
3. macOS 可使用官方 `.pkg` 安装器；需要系统安装权限时使用正常权限流程。也可使用官方 macOS 二进制压缩包安装到用户目录。若主机已有版本管理器，可沿用，不为 NookMe 强行替换全局 Node。
4. Linux 可使用官方对应平台的二进制包，校验同版本发布目录的 SHA-256 后解压到用户目录，再将其 `bin` 加入本次安装的 PATH。检查发行版/libc 与该官方包是否兼容；不能把 glibc 包直接当作 Alpine/musl 包使用。
5. 在选定环境下重新执行下方检查。目录必须持久保留，因为常驻服务会记录实际 Node 可执行文件路径。

```sh
node --version
node -e 'if (Number(process.versions.node.split(".")[0]) < 22) process.exit(1); console.log(process.execPath)'
```

手动校验官方压缩包：先从选定版本的同一官方发布目录下载压缩包与 `SHASUMS256.txt`，按精确文件名找到期望 SHA-256，再与 `shasum -a 256 文件名`（macOS）或 `sha256sum 文件名`（Linux）比较；不匹配则停止。具体版本、平台和文件名以当次官方页面为准，不复制过时的固定版本号。[Node 下载与校验入口](https://nodejs.org/en/download)。

### 4. 缺少 Codex CLI 时安装

优先复用本机现有 Codex，包括安装 Agent 已在使用且支持 App Server 的二进制。如果只是 PATH 中找不到它，设置 `NOOK_CODEX_BIN` 为已确认的绝对路径，不必重新安装。

确实不存在时，按 [OpenAI 官方 Codex CLI 安装说明](https://learn.chatgpt.com/docs/codex/cli) 安装。官方 macOS / Linux 独立安装脚本可先下载、检查，再执行：

```sh
NOOK_CODEX_INSTALLER=$(mktemp)
curl --fail --silent --show-error --location \
  --proto '=https' --proto-redir '=https' \
  --connect-timeout 10 --max-time 120 \
  https://chatgpt.com/codex/install.sh -o "$NOOK_CODEX_INSTALLER"
## 安装 Agent 先检查下载文件和来源，下载成功后才执行下面这一行。
sh "$NOOK_CODEX_INSTALLER"
rm -f "$NOOK_CODEX_INSTALLER"
```

下载失败时不要执行后面的 `sh`。根据安装器实际返回的路径更新本次安装 PATH；不要假设所有主机都装在同一目录。随后检查：

```sh
command -v codex
codex --version
codex app-server --help
export NOOK_CODEX_BIN="$(command -v codex)"
test -x "$NOOK_CODEX_BIN"
```

若使用不在 PATH 中的既有二进制，以上命令用其绝对路径执行。沿用已有 Codex 账号、配置和模型，不执行 `codex login status`，不要求用户重复登录，不用 `codex exec` 发送“测试一下”等模型请求。如果正式启动时确实返回认证或权限错误，再按实际错误处理；不能把安装依赖成功当作账号已可用。

### 5. 上报准备进度，下载并检查 Bridge

先完成前面的系统和联网检查。得到有效配对码且能访问 NookMe 后，使用安装 Agent 的结构化 HTTP 请求向 `POST https://nookme.yunniao.hk/v2/agent/setup` 发送：

```json
{"code":"用户单独提供的一次性配对码","stage":"download","status":"running"}
```

这一步不消费配对码；不要手动调用 claim。若已有有效配对码且即将补装依赖，可在网络检查成功后提前上报准备进度，再继续环境准备。配对码有效期为 15 分钟，长时间安装依赖后可能需要用户从 App 重新获取；不要使用过期或已消费的码反复安装。若 NookMe 本身不可达，无法上报进度，应在原对话说明实际检查失败。

从固定服务下载到临时目录检查，再解压到新的持久版本目录。以下各条需确认前一步成功后再执行：

```sh
NOOK_DOWNLOAD_DIR=$(mktemp -d)
curl --fail --silent --show-error --proto '=https' \
  --connect-timeout 10 --max-time 120 \
  https://nookme.yunniao.hk/bridge/nook-bridge.tar.gz \
  -o "$NOOK_DOWNLOAD_DIR/nook-bridge.tar.gz"
tar -tzf "$NOOK_DOWNLOAD_DIR/nook-bridge.tar.gz"
```

检查归档能完整读取，路径没有绝对路径或 `..` 越界，内容是预期的 Bridge / Gateway / Schema 源文件；不应包含私有状态或凭据。然后选择尚未使用的版本目录，例如 `$NOOK_INSTALL_ROOT/releases/安装日期时间`，创建后以 `tar -xzf 压缩包 -C 新目录` 解压。不得直接覆盖正在运行的版本目录。确认至少包含 `Bridge/main.mjs`、`Bridge/service.mjs`、`Bridge/package.json`、`Gateway/crypto.mjs`，并阅读包内 `Bridge/README.md`。

下载或检查失败时，用同一 setup 接口上报 `status: "failed", errorCode: "download_failed"`，保留失败步骤并停止。配对码、密钥不得写入日志、命令历史、源码或回复。公开包当前没有独立签名清单；不能把可解压等同于签名验证。

### 6. 所有检查通过后才配对和安装

在已解压的版本根目录执行；命令中的占位符由安装 Agent 在受保护的执行环境中注入，不能原样使用，也不要把真实秘密拼进可见命令或 shell history。关闭 shell tracing，不启用 `set -x`。

```sh
NOOK_SERVER='https://nookme.yunniao.hk' \
NOOK_BRIDGE_STATE='<私有目录内的绝对路径>/state.json' \
NOOK_BRIDGE_WORKSPACE='<工作目录的绝对路径>' \
NOOK_CODEX_BIN='<已确认的 Codex 可执行文件绝对路径>' \
NOOK_PAIRING_CODE='<App 单独分享的一次性码>' \
node Bridge/main.mjs --install
```

启用端到端加密时，额外注入 `NOOK_CONTENT_KEY`，其值必须是 Base64 编码的 32 字节密钥。它只交给手机和本机 Bridge，不提交给中转或模型。不要重复 claim；`--install` 会认领一次、私密保存连接，然后安装并启动最终后台服务。默认状态文件权限为 0600。

如果安装时环境检查通过但服务启动失败，已保存的配对可能已被消费。保留状态，移除首次配对码，按日志修复服务并恢复原绑定，不重新认领同一个码，不清空状态文件。启动前检查 `.lock` 对应的旧进程；只有确认进程已退出才能删除陈旧锁。

### 7. 确认后台服务和手机就绪

macOS：

```sh
launchctl print "gui/$(id -u)/com.yunniao.nook.bridge"
```

Linux：

```sh
systemctl --user status nook-bridge.service --no-pager
journalctl --user -u nook-bridge.service -n 30 --no-pager
```

macOS 服务日志是状态文件旁的 `service.log`。只读取定位错误所需的日志，分享诊断前隐藏秘密。核对服务使用的 Node / Codex 路径和必要网络环境；本地进程存在不代表已经就绪。

必须等最终常驻进程依次完成启动、Codex 握手与会话创建/恢复、即时收发循环，再由它发送 `/v2/agent/ready`。手机同时显示就绪且接收器在线，才算安装完成。禁止手动调用 ready、伪造进度或先启动前台实例报告成功再切换后台。此后不再把安装工作留到用户发送第一条消息时处理。

后续重启不再提供配对码，恢复原状态；不能启动第二个前台 Bridge。电脑休眠、断网或用户服务停止期间无法即时处理消息，恢复后会重试。安装完成不自动创建业务监视、定时任务、子助手或测试通知。

### 8. 安装结果应怎样报告

报告系统/架构、复用或安装的 Node 与 Codex 版本、实际执行路径、curl 与 Node 联网检查结果、安装和状态目录、后台服务状态、手机是否真正就绪。只报告做过且有结果的检查；未验证的部分明确写出。不要报告配对码、令牌、内容密钥或含凭据的代理地址。

依赖安装、程序下载、服务启动、手机就绪是不同结果。遇到失败说明卡在哪一步及下一项修复动作；不要仅说“完成了安装”掩盖未连通。

## 初始化阶段

手机按服务端真实状态逐步切换 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://nookme.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 执行，也不阻塞初始化。正常使用时，真实请求的执行错误按处理状态回传。
