# 安装流程 本文档是 NanoClaw 端到端脚本化安装的契约 (`bash nanoclaw.sh` → `pnpm run setup:auto`)。在添加新步骤、修复回退或更改输出渲染方式之前,请阅读本文档。 ## 三个输出级别 每个安装步骤在**三个不同级别**产生输出。它们面向 不同的受众,输出到不同的位置,并以不同的格式呈现。 不要混淆它们。 | 级别 | 受众 | 目的地 | 格式 | |---|---|---|---| | 1. 面向用户 | 运行安装的操作者 | 终端(通过 clack) | 品牌化的、简洁的、信息性的——"产品内容" | | 2. 进度 | 未来调试者、审查失败运行的 AI agent、发布支持 | `logs/setup.log`(一个文件,仅追加) | 结构化的每个步骤块,线性时间顺序,人类和机器可读 | | 3. 原始 | 正在深度调试特定步骤的人 | `logs/setup-steps/NN-step-name.log`(每个步骤一个文件) | 完整的原始子进程 stdout + stderr,逐字记录 | 可以这样理解:用户看到的是**摘要**,进度日志是 **带关键事实的索引**,原始日志是**证据**。 ### 级别 1:面向用户(clack) 由 `setup/auto.ts` 通过 `@clack/prompts` 渲染。这是我们的*产品 界面*——每行都应读起来像是我们为第一天遇到的陌生人设计的一样。 - 使用 clack spinner 表示进行中的工作。显示经过的时间。 - `p.log.success` / `p.log.step` / `p.log.warn` 用于永久状态标记。 - `p.note` 用于多行信息(配对码、下一步)。 - `p.text` / `p.select` / `p.password` 用于提示。 - 品牌调色板:`setup/auto.ts` 中的 `brand()` / `brandBold()` / `brandChip()` 辅助函数。当终端支持时使用真彩色,否则回退到 16 色青色,管道输出 / `NO_COLOR` 时使用纯文本。 规则: - **无中断。** 每个子步骤属于同一个视觉流程。唯一的例外是 Anthropic 凭据注册(见下文)。 - **无原始子进程输出。** 永远不要对输出内容非我们所编写的子进程使用 `stdio: 'inherit'`。捕获它,仅在失败时显示。 - **无调试样式前缀**(`[add-telegram] …`、`INFO …`、时间戳)。这些属于级别 2 和 3。 - **无 emoji**,除非 clack 图形需要。 ### 级别 2:进度日志 `logs/setup.log`——每次安装运行一个文件,仅追加,跨多次运行安装累积 (如果某次运行中途失败并被重新尝试,新条目会追加)。这是你在操作者报告安装 bug 时 会要求他们粘贴的东西,也是 AI agent 会阅读以了解发生了什么的东西。 条目格式: ``` === [2026-04-22T22:14:12Z] bootstrap [45.1s] → success === platform: linux is_wsl: false node_version: 22.22.2 deps_ok: true native_ok: true raw: logs/setup-steps/01-bootstrap.log === [2026-04-22T22:14:57Z] environment [2.3s] → success === docker: running apple_container: not_found raw: logs/setup-steps/02-environment.log === [2026-04-22T22:15:00Z] container [92.4s] → success === runtime: docker image: nanoclaw-agent:latest build_ok: true raw: logs/setup-steps/03-container.log ``` 设计约束: - 开始行带有起始时间戳(UTC,ISO-8601),使 `grep` 能给出顺序。 - 持续时间以秒为单位,保留一位小数——快速步骤读作 "0.5s",而非 "0ms"。 - 状态为以下之一:`success`、`skipped`、`failed`、`aborted`。 - 字段是步骤特定的,但**必须**是短标量值。无 JSON,无多行。如果值很长,放在原始日志中并引用它。 - 始终输出一个 `raw:` 指针,即使在成功时——使调试第二次失败更容易。 - **用户选择**是其自己的条目,不嵌套在步骤中: ``` === [2026-04-22T22:17:44Z] user-input → display_name === value: gav === [2026-04-22T22:17:51Z] user-input → channel_choice === value: telegram ``` 这些很重要,因为通过安装流程的路径取决于它们。 日志以标识运行的头部块开始,以完成块结束: ``` ## 2026-04-22T22:14:12Z · setup:auto 已启动 user: exedev cwd: /home/exedev/nanoclaw branch: branded-setup commit: 6e0d742 … (步骤条目) … ## 2026-04-22T22:18:54Z · 已完成 (总计 4m42s) ``` 失败时完成块会指出失败的步骤及其错误: ``` ## 2026-04-22T22:16:40Z · 在 container 处中止 (err=cache_miss) ``` ### 级别 3:原始按步骤日志 `logs/setup-steps/NN-step-name.log`——每个步骤一个文件,按执行顺序编号(两位零填充前缀以支持自然排序)。来自子进程的完整逐字 stdout + stderr。每次运行时截断并重写(非追加)。 内容为步骤输出的任何内容:apt 输出、docker 构建层、pnpm install 内容、`curl` 响应体等。这是证据层面——"shell 实际看到了什么?"不过滤任何内容。 ## 新步骤的契约 当你添加一个步骤(无论是 `setup/.ts` 中的 TS 步骤还是从 `auto.ts` 调用的 bash 安装程序),它必须: 1. **从调用方接收一个原始日志路径。** 将所有 stdout + stderr 写入那里。不要直接写入终端。 2. **在结束时输出单个终端状态块**,包含 `STATUS: success|skipped|failed` 和任何步骤特定字段: ``` === NANOCLAW SETUP: STEP_NAME === STATUS: success KEY: value KEY: value === END === ``` 字段名使用 `UPPER_SNAKE_CASE`。值是短标量值。 3. 如果是一个长时间运行的步骤,可选择在中途发出**子状态块**。`auto.ts` 实时解析它们,并可以渲染中间 UI(正如 `pair-telegram` 使用 `PAIR_TELEGRAM_CODE` / `PAIR_TELEGRAM_ATTEMPT` 所做的那样)。 4. **在硬失败时以非零退出**,以便 `auto.ts` 可以区分"步骤运行完成并报告失败"与"步骤崩溃"。 驱动程序处理其余部分:级别 1 中的 spinner,级别 2 中的结构化追加,级别 3 中的原始捕获。 ## Anthropic 例外 Anthropic 凭据注册(`setup/register-claude-token.sh`)是视觉流程中**唯一**允许的中断。原因: - `claude setup-token` 打开浏览器,运行自己的 OAuth 提示,并打印 token。它通过 `script(1)` 拥有 TTY。 - 我们不想自己重新实现 OAuth 设备流程。 - 我们不想拦截/镜像 token(它已经出现在用户终端——镜像它会增加攻击面)。 因此在此步骤期间: - clack 流程显式暂停(一个 `p.log.step` 标记说"这部分是交互式的,你正在移交给 Anthropic")。 - 子进程完全继承 stdio。 - 当控制返回时,clack 在下一行恢复,带有一个成功标记。 级别 2 日志仍然获得一个条目(`auth [interactive] → success` 带有方法——subscription / oauth-token / api-key)。级别 3 的捕获在这里是可选的;镜像 `script -q` 输出很棘手,将 token 泄露到磁盘的风险超过了调试价值。 ## 文件参考 | 文件 | 角色 | |---|---| | `nanoclaw.sh` | 顶层封装。阶段 1(bootstrap)和阶段 2(setup:auto)编排。写入 bootstrap 的原始日志 + 进度条目。 | | `setup.sh` | 阶段 1 bootstrap:Node、pnpm、原生模块验证。发出自己的 `BOOTSTRAP` 状态块(历史上打印到 stdout;现在输出到 bootstrap 原始日志)。 | | `setup/auto.ts` | 阶段 2 驱动程序。编排 clack UI、步骤执行、用户提示,并为其启动的每个步骤写入所有三个日志级别。 | | `setup/logs.ts` | 日志记录原语(`logStep`、`logUserInput`、`logComplete`、`stepRawLog`、`initSetupLog`)。级别 2/3 格式和文件路径的唯一真源。 | | `setup/.ts` | 各个步骤的实现。必须输出一个终端状态块;不能直接写入终端。 | | `setup/register-claude-token.sh` | Anthropic 例外。继承 stdio,打印自己的 UI,向驱动程序返回状态。 | | `setup/add-telegram.sh` | 非交互式适配器安装程序。从 env 读取 `TELEGRAM_BOT_TOKEN`;从不提示。面向用户的部分驻留在 `auto.ts` 中。 | | `setup/pair-telegram.ts` | 发出 `PAIR_TELEGRAM_CODE` / `PAIR_TELEGRAM_ATTEMPT` / `PAIR_TELEGRAM` 状态块。从不打印 UI。驱动程序通过 clack notes 渲染。 | ## 常见陷阱 - **在步骤内部打印调试输出。** 开发时诱人;检入代码时禁止。所有运行时消息都通过状态块(级别 2)或原始日志写入(级别 3)。 - **添加一个"仅此一次"输出到终端的 `console.log`。** 它会破坏 clack 流程——spinner 行会被撕裂。改用 `src/log.ts` 中的 `log.info` / `log.error`(写入原始日志)。 - **对非例外子进程使用 `stdio: 'inherit'`。** 见上文 Anthropic。其他任何情况都需要 `pipe` + 显式捕获。 - **Tee 到 stderr。** Clack 的 spinner 在一个步骤期间拥有终端。即使是 stderr 写入也会撕裂框架。管道传输一切,然后选择要暴露的内容。 - **bash `$VAR…` 位置中的 UTF-8。** Bash 的词法分析器可能将多字节字符的第一个字节拉入变量名,触发 `set -u`。始终加花括号:`${VAR}…`。 ## 未来工作(尚未实现) - **进度日志轮转。** 今天的实现在每次运行时截断。未来:将之前的运行滚动到 `logs/setup.log.1`、`.2` 等。 - **多次运行安装的原始日志轮转。** 目前每次运行会覆盖。目前可以接受;如果支持需要比较连续尝试,则重新审视。 - **`register-claude-token.sh` 的结构化输出。** 交互式步骤目前不发出机器可读状态。未来可以在交互后添加一个状态块,注明使用的方法。