> ## Content Index
> Fetch the complete content index at: https://isoziyuan.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# agent2api 一键安装器：一条命令装起带 HTTPS 的本地 AI 网关
- URL: https://isoziyuan.com/p/100171/
- Published: 2026-09-28T05:44:15.000Z
- Description: 一条命令把 agent2api 装到服务器上：自动挑空闲端口、自动签 HTTPS 证书、自动认已有 Caddy、缺 docker 自己装。默认只问 2 个问题、中文回答也认、报错都给可粘的命令。附五轮压测与 37 项回归验证。
- Author: Isoziyuan
- Tags: Docker, Caddy, 自托管, 自动化部署, 新手指南

## 一句话

把 agent2api 装到你的服务器上，一条命令。它是本地网关——把 WorkBuddy、Qoder、Cline 这类客户端的登录态转成 OpenAI 兼容接口，自己管多账号、模型路由和出网。脚本会**自动挑空闲端口**、想绑域名就**自动申请 Let's Encrypt 证书**、自动认你机器上已有的 Caddy 并接好反代。

```bash
# 在 VPS 上粘这一行就装完了 —— 不用先在本地下载再 scp 上传
curl -fsSL "https://pan.ailxw.com/api/pickup-download?code=20818" -o ~/a2a.sh && bash ~/a2a.sh
```

> 提示符是 `#`（登录就是 root，多数 VPS 默认如此）就用上面这条；是 `$`（普通用户）就把最后的 `bash` 换成 `sudo bash`。  
> **别习惯性加 `sudo`** —— 很多精简镜像根本没装 sudo，加了会报 `sudo: command not found`，后面还跟一个  
> `curl: (23) Failed writing body`，看着像网络问题，其实是 sudo 不存在。  
> 它只动三处：起一个 docker 容器、往 Caddyfile 里加一段**带标记的**站点块（`--no-domain` 可一键摘掉）、在安装目录写几个文件。改反代配置前先备份、改完先校验、失败自动回滚；原有站点全程不受影响。  
> 注意是**在 VPS 上执行**，不是在你自己的电脑上。粘上去如果没反应，说明这个下载源不通，文末「下载」一节有两个备用源。

## 装完之后会得到什么

| 项目 | 说明                                                                  |
| -- | ------------------------------------------------------------------- |
| 面板 | https://你的域名/（不绑域名时走 SSH 隧道，见下文「不绑域名怎么用」）                           |
| 网关 | https://你的域名/v1，客户端 base\_url 填它（不绑域名时是 http://127.0.0.1:<网关端口>/v1） |
| 证书 | Let's Encrypt 自动签发 + 自动续期（Caddy 负责）                                 |
| 容器 | 单容器 + 可选一个自建 Caddy 容器；内存上限默认 384m                                   |
| 数据 | 全在安装目录（data/），删目录即彻底清理                                              |

## 命令行

最常用的四条：

| 命令                                                            | 干什么             |
| ------------------------------------------------------------- | --------------- |
| sudo bash install-agent2api.sh                                | 交互式安装（第一次用这个）   |
| sudo bash install-agent2api.sh --dry-run                      | 只打印它打算做什么，不动手   |
| sudo bash install-agent2api.sh --yes --domain a2a.example.com | 全自动 + 绑域名签证书    |
| sudo bash install-agent2api.sh --uninstall                    | 卸载（停容器 + 摘掉站点块） |

装完之后的运维：

| 命令              | 干什么                   |
| --------------- | --------------------- |
| \--status       | 容器/端口健康、域名可达、证书到期、日志尾 |
| \--check-update | 查 Docker Hub 上有没有新版本  |
| \--upgrade      | 升级；**健康复检不过自动回滚**     |

域名与反代相关：

| 参数                    | 说明                     |                                  |                                 |
| --------------------- | ---------------------- | -------------------------------- | ------------------------------- |
| \--domain <域名>        | 绑域名 + 签证书；不带则**沿用上次的** |                                  |                                 |
| \--no-domain          | 明确不要域名，移除上次写入的站点块      |                                  |                                 |
| \`--expose both\\     | panel\\                | gateway\`                        | 域名下暴露什么（默认 both，/v1\* 走网关其余走面板） |
| \`--caddy-mode host\\ | docker\\               | self\`                           | 强制反代形态；默认自动探测                   |
| \`--cf y\\            | n\`                    | 域名是否走 Cloudflare 橙云（自动加真实 IP 还原） |                                 |

其它：`--dir` 安装目录、`--tag` 镜像版本、`--panel-port` / `--gateway-port` 指定端口、`--mem` 内存上限、`--lock-register` / `--open-register` 注册开关、`--with-manager` 登记为 workbuddy-manager 的上游。

**关于升级，有个坑值得先说**：agent2api 面板里那个「立即更新」按钮在 Docker 部署下是**用不了的**——它去 GitHub Releases 下载，而那个仓库的 Releases 里只有桌面端安装包（`.dmg` 和 `setup.exe`），没有任何 Linux 产物。面板自己也会提示「请通过 Docker 镜像更新」。所以升级只能换镜像，`--upgrade` 干的就是这件事：改 compose 里的镜像 tag → 拉取 → 重建 → 等 healthy → 从容器内部复核两个端口，任何一步不过就**自动回滚旧版本**并把状态文件也一起还原。

## 改了什么（实测 + 用户反馈挖到的 9 个真问题）

脚本不是一次写成的。下面每一条都是实测撞出来的，不是"理论上可能"。

**1\. 端口校验会"假通过"，然后访问 502。** 原来只查宿主机 `ss` 看端口有没有被占。实测发现：宿主端口被 docker-proxy 占着，但容器里根本没有进程在监听——校验通过，一访问就 502。现在改成**从容器内部**验证两个端口真的在服务。

**2\. 交互式菜单的选择，从来没生效过。** `choose()` 函数把选项菜单用 `printf` 打到了 stdout，而调用方是 `$(choose ...)`——菜单文本被一起捕获，`case` 永远匹配不上，**而且一个错都不报**。它潜伏了很久，原因很尴尬：我所有手工测试都显式传了参数，从没走过交互分支。现在提示与菜单一律写 stderr，stdout 只留结果。

**3\. `--expose` 拼错一个字母，会静默生成一个空路由。** `--expose foo` 原样传下去，生成的站点块里没有任何 `handle`，域名整体不通——而 Caddy 的 `validate` 还能通过。现在枚举校验拦下来。

**4\. `--yes` 重跑一次，域名和端口就丢了。** 状态文件没被当默认值用，重跑时回落到默认值；而磁盘上的反代块还指着旧端口——静默得到一个 502 的域名。现在改成**状态文件提供默认值、命令行显式项优先**；确实想去掉域名就显式 `--no-domain`，它会把站点块一起摘掉。

**5\. 改 Caddyfile 的过程中被打断，会留下半截配置。** 这种配置当下不发作（运行中的 Caddy 还用着旧配置），等下次 reload 或重启才炸，是最难查的那类故障。现在注册了信号处理，收到 SIGTERM 就把备份还原回去并重载。

**6\. 裸机装不了。** 原来要求机器上已有 Caddy，而实际上很多机器 80/443 上什么都没有。现在多了一种形态：**自己起一个 `caddy:2-alpine` 容器**，配置和证书都放在安装目录里，跟别人的反代完全隔离。实测在干净机器上从零到签出证书是全通的。这里也踩过一个：**bind mount 一个还不存在的文件，docker 会创建同名"目录"**，于是 Caddy 起不来且报错极难看懂——所以必须先把配置文件落盘，再 `compose up`。

**7\. 交互问得太多了。** 最初默认路径要回答 9 个问题：容器名、时区、镜像版本、面板端口、网关端口、内存上限、域名、要不要登记 manager、确认安装。其中一半小白根本不该被问 —— 端口是自动挑空闲的、时区默认就行、容器名更是无所谓。现在的默认路径**只问 2 个**：先问域名（它决定你后面怎么访问，是最关键的决策，所以放最前），再问一句"高级选项需要改吗"，默认 `n` 就直接开装。命令行上给过 `--dir`/`--mem` 这类参数的话，脚本认为你是老手，第 2 步不问了，但仍会把没给的那几项问一遍。

**8\. 不绑域名时，SSH 隧道漏了网关端口。** 原来只让你转发面板端口（`ssh -N -L 3066:...`）。你能打开面板、能加账号、能建 Key —— 然后客户端连不上。因为客户端要连的是**网关**（`3065`），而隧道里根本没有它。症状是"面板一切正常但 API 就是不通"，会让人往别处怀疑很久。现在两个端口一起转，而且脚本会**按你的实际端口和公网 IP 把整条命令打印出来**，直接复制即可（原来写的是 `<本机>` 占位符，还配一句"base\_url = 上面的网关地址"——上面压根没有网关地址）。

**9\. 我把「封掉自助注册」设成了默认，挡了正当用法。** 这条是用户直接反馈的：装完发现自助注册被 403 挡了，而他就是想在浏览器里直接注册。 我当时的理由很充分 —— 域名进 CT 日志会被公开索引，不封等于把管理台交给陌生人。 但**理由充分不等于应该替用户做这个决定**：对一个自用工具来说，「能注册」是刚需，「防抢注」是加分项， 我把优先级搞反了。现在默认**不封**，注册端点正常可用；同时把安全提醒做在它该在的地方 —— 装完检测管理员注册了没，没注册就醒目提醒「现在任何人打开面板都能抢注，请立刻去注册」。 想封的仍然可以 `--lock-register`，两条路都留着了。

### 再 5 轮：这次按"用户实际怎么操作"来跑

前几轮我都是按**功能/入口**测的（`--uninstall` 通不通、`--domain` 对不对）。这一轮换成**按真实操作路径**跑， 结果又抓到 5 个问题 —— 而且都是"按功能测永远测不出来"的那类。

| 轮次 | 模拟的真实场景                                           | 抓到                                                              |
| -- | ------------------------------------------------- | --------------------------------------------------------------- |
| 1  | **全新 VPS 的第一天**：一行命令装 → 只按回车 → 照它打印的提示操作 → 调通 API | 提示里"加上游账号"太笼统（小白不知道指什么）                                         |
| 2  | **糊涂用户**：全选 y、域名带 https:// 和斜杠、答非所问、菜单输 9         | **中文回答「是/好/对」被当成"否"**；听不懂的回答默默当否；菜单越界静默回落                       |
| 3  | **中途放弃**：下载阶段 Ctrl+C / kill -9，然后重跑               | 中断提示说"配置已还原"，但容器其实已经建了；中断后 \--status 报"用 --dir 指定安装目录"（用户明明指定了） |
| 4  | **反复折腾**：连跑 5 次、来回改端口/内存/域名                       | **Caddy 备份文件无限累积**（连跑 4 次 = 4 个，堆在 /etc/caddy/）                 |
| 5  | **卸载重装**：卸载（留目录）→ 重装 → 数据还在吗                      | 数据保留完全正确（注册和 Key 都在）✓                                           |

**最严重的是第 2 轮那个**：脚本是中文界面，但 `ask_yn` 只认英文 `y/yes`。 用户看到「要不要禁止别人从网上自己注册账号？」回答「是」—— 落进 `*)` 分支被当成**否**， 也就是**执行了和他意思相反的操作**。现在中文回答全部识别（是/好/对/要/嗯）， 而且**听不懂的回答会重问并解释**，不再默默选一个。

第 3 轮那条也值得记：中断时我原本打印"配置已还原到你运行前的状态，没有改坏任何东西"—— 听起来很安心，但实测**容器其实已经建起来了**（只是状态文件还没写）。说"什么都没发生"是不诚实的， 用户会以为环境是干净的。现在会如实说明"容器可能已经建起来了，重跑一次就能接上"。

第 4 轮的备份累积是"用的越久越脏"那类问题：每次改共享反代配置都会存一份备份， 用户来回折腾几次就在 `/etc/caddy/` 堆一排。现在只保留最近 3 份 —— 而且**只删本脚本自己建的**（严格匹配文件名模式，绝不碰用户自己的备份，实测验证过）。

**第 1 轮还顺手补了一个信息**：脚本原来只说"在「账号」里加上游账号"，小白根本不知道指什么。 我扒了面板接口，实际支持 14 种（WorkBuddy / 小浣熊 / Qoder / Trae / Cline / Accio / ZCode / CatPaw / CodeArts…）， 现在直接列在提示里。

### 被用户骂醒的一次：「一键脚本」不该让用户自己去装依赖

用户在新机器上跑，撞到这句：

```
× 这台机器上还没装 docker（跑这个服务必须用它）
装它很简单，把下面这行粘进去回车就行（官方一键脚本）：
      curl -fsSL https://get.docker.com | sh
装完再重新跑本脚本即可。
```

他的回复很直接：**「你必须要把我的一键脚本支持自动检测环境自动安装需要的依赖，而不是让用户自己去安装」**。

他说得对。**"给你一条命令让你自己粘"根本不叫一键。** 叫"一键加一步"。凡是这种"你缺 X，去装一下 X 再回来"的设计，都是把麻烦推给用户。

现在改成：**缺什么自己装，四种方式依次降级**——

| 方式 | 做什么                            | 什么时候能救          |
| -- | ------------------------------ | --------------- |
| 1  | docker 官方脚本                    | 正常情况，最快         |
| 2  | 官方脚本 + 阿里云镜像                   | 国内直连官方源慢/不通     |
| 3  | 发行版自带仓库（apt install docker.io） | 官方源整体不可达        |
| 4  | **重装包**（apt --reinstall）       | 包显示已装、二进制却缺失/损坏 |

实测在一台被卸干净的 Debian 12 上：**从"没有 docker"到 agent2api 装好可用，总共 60 秒**。

**这里踩到一个值得记的坑**：我最初用"脚本退出码"判断装成功没装成功。结果实测发现 —— **`get.docker.com` 会退出 0 却什么都没装**：当 docker 的包显示"已安装"、但二进制文件被删掉时， `apt install` 认为"已是最新版"直接跳过，脚本照样报成功。

所以判定标准必须换成**「docker 命令是否真的可用」**：

```bash
docker_ok() { command -v docker >/dev/null 2>&1; }
# 每个方式跑完都验一次，而不是看它的退出码
sh "$script" >"$log" 2>&1 || true
docker_ok && ok=1
```

**这条修正本身就是"方式 4"存在的原因** —— 如果我只信退出码，就永远不会走到重装那一步， 用户会卡在一个"脚本说装好了、但 docker 根本不在"的状态里。

**还顺手修了一个反直觉的行为**：`check_docker` 在命令分发之前就跑，所以 `--status` / `--uninstall` 也会触发自动安装 —— 用户说"我要卸载"，脚本却给他装了个 docker 出来。（这是我测试时踩到的： 我的一条测试命令里先跑 `--uninstall`，结果它把 docker 装回来了，害我排查了半天。） 现在只有**安装和升级**才会自动装依赖。

**另外给不想被动的用户留了后门**：`--no-deps` —— 不碰你的系统，缺什么只告诉你命令。

## 一个值得记的坑：宿主机端口"看起来被占了"，其实没占

这是第 1 条的完整版，也是我认为最值得单独讲的一条。

`docker run -p 127.0.0.1:3065:3065` 之后，宿主机的 3065 端口会被 **docker-proxy** 绑上——哪怕容器里的进程早就退出了、或者压根没监听这个端口。于是：

- `ss -ltn` 看宿主机：3065 是"被占用"的
- 从容器外 `curl 127.0.0.1:3065`：502
- 从容器内 `curl 127.0.0.1:3065`：Connection refused

**只查宿主端口的校验，在这件事上是不可信的。** 判断"服务真的起来了没"，必须进容器里去看，或者从外部发一个真实请求看响应码。脚本现在的自检是后者：`docker exec <容器> curl -sf http://127.0.0.1:<端口>/`，两个端口都过才算就绪。

同一类问题的另一半是：**改自定义端口时，光改宿主映射没用**。agent2api 容器内部监听哪个端口，是由 `AGENT2API_PANEL_PORT` / `AGENT2API_PROXY_PORT` 两个环境变量决定的。只改 compose 的 `ports:` 而不改环境变量，症状就是"面板能开、`/v1` 一直 502"。

## 验证矩阵

脚本自带一个回归套件 `test-install-agent2api.sh`，一条命令跑完 37 个用例，退出码就是失败数。最近一次在 Debian 12 + Docker 29.8.1 上：

| 分组                                                                      | 用例数    | 结果          |
| ----------------------------------------------------------------------- | ------ | ----------- |
| 参数校验（端口非数字/越界、枚举拼错、路径、内存格式、容器名）                                         | 10     | 全过          |
| 默认值路径与幂等（无域名安装、重跑不漂移、不传域名不该有域名）                                         | 3      | 全过          |
| 状态与版本管理（status / check-update / 升级同版本 / **升级失败回滚**）                     | 4      | 全过          |
| 端口被占自动换端口、容器名冲突定向报错                                                     | 2      | 全过          |
| 域名与 TLS（签证书、**注册开关 lock/open**、托管块幂等、状态复用、**冲突在动手前失败**、\--no-domain 摘除） | 6      | 全过          |
| **流式 SSE 未被反代缓冲**                                                       | 1      | 全过          |
| 卸载（含"从未安装过"时也不报错）                                                       | 2      | 全过          |
| 原有生产站点未被影响                                                              | 1      | 全过          |
| **合计**                                                                  | **37** | **37 / 37** |

流式那条值得单独说：判据不是"能返回内容"，而是**首字节时间明显小于总耗时**。实测经 manager 网关首字节 0.58s、总耗时 3.28s，37 个 `data:` 帧加一个 `[DONE]`——说明是逐帧下发而不是整段缓冲。Caddy 默认就透传，但如果你前面挂的是 Nginx，**必须显式 `proxy_buffering off`**，否则首字延迟会等于整段生成时间。脚本给 Nginx 用户打印的配置片段里带了这行。

**哪些没进自动套件，也说清楚**：裸机自建 Caddy 那套（需要腾出 80/443，在有反代的机器上跑会打断现有服务）、SIGTERM 中断还原（需要可控的中断时机）、`--with-manager` 的端到端登记（会改动生产 manager 的上游列表）。这三项都是手工验证过的，但没做成自动化——所以别把"37/37 全绿"理解成"什么都验过了"。

### 又跑了 5 轮专项压测（奔着找 bug 去，不是走一遍）

| 轮次 | 打法                                                                  | 结果      |
| -- | ------------------------------------------------------------------- | ------- |
| 1  | 完整回归套件（域名 + 证书 + 真实流式）                                              | 29 / 29 |
| 2  | **交互流程**（pty 驱动：默认两回车、高级选项、绑域名全链、已安装菜单、非法输入、Ctrl+C 中断）              | 抓 3 个   |
| 3  | **边界与异常**（参数冲突、越界值、docker 缺失、状态文件损坏、端口占满）                           | 抓 6 个   |
| 4  | **真实使用场景**（装 → 真的走 API 注册管理员 → 建 Key → 调 /v1/models → 升级 → 回滚 → 卸载） | 全通      |
| 5  | 输出可读性与文档一致性                                                         | 抓 1 个   |

**10 个真问题里，3 个是"静默型"—— 我认为这一类最值得记：**

1\. **`--expose foo` 不带域名时被静默吞掉。** 没域名时脚本会把暴露模式强制设成 `none`，于是这个非法值被无声覆盖，一个错都不报。**枚举值对不对，跟有没有域名无关** —— 现在在解析参数时就拦。 2\. **`--caddy-mode bogus` 被静默忽略。** 内部是个 `case` 匹配，匹配不上就沿用自动探测结果。用户明确指定的形态没生效，也没有任何提示。 3\. **在"确认开始安装"处输入中断（EOF）会直接开装。** `read` 失败时我原来写的是 `ans="${ans:-$def}"`，而这个问题的默认值正好是 `y`。也就是说**终端一断、或者输入被别的东西耗尽，它就自己开装了**。现在读不到输入就按"否"处理，并明确说一句。

还有一条不算静默、但更严重：**状态文件以前是 `source` 执行的**。它能被写坏，也能被写进别的东西 —— 而脚本以 root 运行。现在改成自己解析 + 键白名单 + `printf -v` 赋值，**绝不执行文件里的内容**。

其余 6 条：`--no-domain` 与 `--domain` 同时给会自相矛盾（既设了域名又按不绑域名处理）；`--mem 0m` / `1k` 被放行（docker 必然启动失败，它最低要 6m）；状态文件坏了会静默按默认值装（可能装了另一个容器）；只传一个 `--dir` 会被连问 6 个高级问题；重跑选"重新配置"时那个高级选项开关永不出现。

**每修一个，我就补一条回归用例。** 套件从 29 涨到 34 —— 不是为了数字好看，是为了这些 bug **不可能再回来**。

### 又跑了 5 轮：这次专门盯"新手能不能自己走通"

前一轮是找 bug，这一轮的标准换成了**"一个不懂技术的人，能不能只靠脚本自己打印的东西走通"**。又抓到 6 个问题：

| 轮次 | 打法              | 发现                                                                               |
| -- | --------------- | -------------------------------------------------------------------------------- |
| 1  | 首屏审计（用新手眼睛看第一屏） | 开头没说"在装什么、要多久"；术语密集（宿主 Caddy / systemd / 反代后端）；下载那步没给预期；\--yes 还打印提问说明，看着像要问其实没问 |
| 2  | 逐句文案审计          | 11 处术语/黑话（"镜像 tag"→"版本"、"容器名"→"服务名字"、"上游"→"接到那个面板上"）                             |
| 3  | 异常场景引导          | **docker 缺失只说"请先安装 Docker"** —— 新手根本不知道怎么做                                       |
| 4  | 完成后「下一步」引导      | **漏了最关键的一步**：没说要"加上游账号" —— 少了这步客户端拿不到任何模型                                        |
| 5  | 只靠脚本输出走通        | **脚本打印的隧道命令会绕过你已有的 ssh 别名/密钥**，报 Permission denied                               |

**第 5 条是我自己踩的**：我照着脚本打印的命令执行，被拒了 —— 因为脚本给的是 `root@IP`，而服务器上我平时是用 `ssh se`（配了密钥）登录的。**它绕过了我已经配好的东西。** 现在会补一句："如果报 Permission denied，就把你平时登录它的那条 ssh 命令拿出来，在后面加上这两个转发参数。"

**还抓到一个隐蔽的源码 bug**：`printf` 的格式符比参数少一个时，**printf 会重复整个格式串** —— 输出里"第 2 步：另开浏览器，打开 " 打了两遍、第二遍地址是空的。修完顺手写了个扫描脚本，把全项目 4 个 shell 文件都过了一遍（现在 0 处）。

**改动就一句话：把用户当成不懂技术的人。** 开头先说人话；术语换成人话；报错一律给**能直接粘的命令**；装完只强调一件事，技术细节降级到后面；补上"进面板后按顺序做三件事"。

### 上线后被用户抓到一个 bug：选「卸载」却继续安装

**用户报的**：重跑脚本时会先弹一个菜单（重新配置 / 升级 / 卸载 / 退出），他选了「3) 卸载」，脚本却继续往下走，还问他"确认开始安装？"。

原因很典型：菜单里选「卸载」只是把 `DO_UNINSTALL=1`，而**那个标记是在 `main()` 开头就检查过的** —— 现在再设等于没人看。**"设个标记让别处去处理"这种间接写法，一定要确认"处理方"还没跑过。**

修法就是别绕这一圈：菜单里**直接调用 `do_uninstall` / `do_upgrade`**。顺带把全部同类标记（`DO_STATUS`/`DO_UPGRADE`/`DO_CHECK_UPDATE`）审了一遍，确认只有这一处。

**更值得说的是我为什么没测出来**：我测了 `--uninstall` 和 `--upgrade` 两个命令行入口，就默认"菜单选 3 等于 `--uninstall`"——**没测。** 这属于典型的"看起来等价所以跳过"。现在补了 2 个用 `script(1)` 造 pty 驱动菜单的用例，套件从 34 涨到 36。

## 跑一次看看

```bash
# 1. 在 VPS 上一行装完（推荐；不用先在本地下载再上传）
#    提示符是 # 用这条；是 $ 就把最后的 bash 换成 sudo bash
curl -fsSL "https://pan.ailxw.com/api/pickup-download?code=20818" -o ~/a2a.sh && bash ~/a2a.sh

# 2. 想带参数就接在后面
curl -fsSL "https://pan.ailxw.com/api/pickup-download?code=20818" -o ~/a2a.sh \
  && bash ~/a2a.sh --domain a2a.example.com --expose both

# 3. 脚本已经在机器上了，就直接跑（全交互，推荐第一次）
sudo bash install-agent2api.sh

# 4. 想先看清楚它要干什么
sudo bash install-agent2api.sh --dry-run

# 5. 装完看状态
sudo bash install-agent2api.sh --status --dir /opt/agent2api

# 6. 以后升级（面板里的"立即更新"在 Docker 部署下用不了，用这个）
sudo bash install-agent2api.sh --check-update --dir /opt/agent2api
sudo bash install-agent2api.sh --upgrade      --dir /opt/agent2api

# 7. 不想要了
sudo bash install-agent2api.sh --uninstall
```

**为什么写成 `-o 文件 && bash 文件`，而不是顺手就来的 `curl … | bash`** —— 这两种写法我都试过，各有一个坑，而且都是"看起来在跑、其实没跑对"：

**坑一：管道会把 stdin 占掉。** 安装器默认是交互式的，`curl | bash` 时它读不到你的键盘。我第一次在测试机上验证时就撞上了：`bash install.sh < /dev/null` 跑出来，它一个都不问、**静默全部采用默认值**往下装 —— 你以为在交互，其实配置项你一个都没选过。

**坑二：管道失败是无声的。** `curl -fsSL … | bash` 里如果源不通，`-f` 让 curl 不输出错误体、`-s` 让它不输出进度，于是 **curl 什么都不打印、bash 收到空输入**，你屏幕上什么都不会出现。国内访问 `cdn.jsdelivr.net` 经常不通，症状就是"粘上去没反应"——我就是这么收到的反馈。

改成 `-o 文件 && bash 文件`：出错会打印原因、`&&` 会拦住后续、stdin 还是你的终端，交互正常。

（顺带说个细节：即使管道那两种坑都绕开，`curl … | bash` 还有个副作用 —— `$0` 会变成 `bash`。实测 `echo 'echo $0' | bash` 输出 `bash`，存成文件执行输出的才是文件路径。安装器要用自身路径打印"以后怎么重跑 / 怎么卸载"，管道执行会让它打出 `bash bash --uninstall` 这种没法用的东西。所以仓库里的引导脚本 `deploy.sh` 也是**先落盘再执行**，并且在管道场景下把 stdin 接回终端。）

首次使用：打开面板注册管理员 → 「账号」里加上游账号 → 「网关 Key」建一把 Key → 客户端 `base_url` 填 `https://你的域名/v1`、`api_key` 填那把 Key。

**自助注册默认是开着的** —— 直接在浏览器里打开面板就能注册，不用绕隧道。

但这里有个必须知道的背景：agent2api 的规则是「**第一个打开面板的人注册成管理员**」，而域名签了证书就会进 Certificate Transparency 日志、被公开索引。也就是说，从面板上线到你注册完成之间，谁先打开谁就是管理员。所以脚本装完会**检测管理员注册了没**，没注册就醒目提醒：

```
× 管理员还没注册 —— 现在任何人打开面板都能抢注成管理员，请立刻去注册！
    立刻打开：https://你的域名/
```

看到这行就去注册，注册完再干别的。**想更稳妥就封掉**：重跑加 `--lock-register`，注册端点返回 403，注册改走 SSH 隧道（隧道直连容器、不经过 Caddy，不受该规则影响）。注意**两个端口都要转发**，只转面板的话面板能开、客户端连不上网关：

```bash
ssh -N -L 3066:127.0.0.1:3066 -L 3065:127.0.0.1:3065 root@<你的服务器IP>
# 端口改成你自己的。这条命令要一直开着；它不输出任何东西、看着像卡住 —— 那是在转发，正常
```

想再开回来：重跑加 `--open-register`。

## 不绑域名怎么用（SSH 隧道）

不绑域名是完全可行的，但有个细节第一次写时我漏了：**隧道必须把网关端口一起转**。

只转面板（`3066`）的话，你能打开管理面板、能加账号、能建 Key —— 然后客户端连不上，因为客户端要连的是**网关**（`3065`）。症状是"面板一切正常，但 API 就是不通"，很容易怀疑到别的地方去。

装完之后脚本会把这条命令**按你的实际端口和 IP 打印出来**，直接复制就行：

```bash
ssh -N -L 3066:127.0.0.1:3066 -L 3065:127.0.0.1:3065 root@<你的服务器IP>
```

- 这条命令**要一直开着**（另开一个终端窗口跑）。它不输出任何东西、看着像卡住 —— 那是在转发，正常。要停就 `Ctrl+C`。
- 隧道只对**你自己这台电脑**有效，别人访问不到（这也是它比把端口直接暴露到公网安全的地方）。
- 然后：面板 → 浏览器开 `http://127.0.0.1:3066`；客户端 `base_url` → `http://127.0.0.1:3065/v1`。

嫌麻烦就绑个域名：带 `--domain 你的域名` 重跑，自动签 HTTPS 证书，之后就不用隧道了。

## 常见报错（都是真实遇到过的）

| 你看到的                                                       | 真正的原因                           | 怎么办                                                                                    |                                  |
| ---------------------------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------- |
| sudo: command not found，后面跟 curl: (23) Failed writing body | 你**登录就是 root**，而这台机器**没装 sudo** | 去掉 sudo，直接 bash \~/a2a.sh。先看提示符是 # 还是 $                                                |                                  |
| 粘上去**一点输出都没有**，光标直接回来                                      | 下载源不通（\`curl -fsSL … \\         | bash\` 的失败是**无声的**）                                                                    | 换源（见下面三个源），或改用 \-o 文件 && bash 文件 |
| docker: command not found                                  | 机器上还没装 Docker                   | 先装：\`curl -fsSL [https://get.docker.com](https://get.docker.com/?ref=isoziyuan.com) \\ | sh\`                             |
| 需要 root 权限运行…                                              | 你是普通用户                          | 按提示把 bash 换成 sudo bash（脚本会先看这台机器有没有 sudo，再给对应的建议）                                      |                                  |
| 80/443 被 xxx 占用                                            | 机器上已有反代（Nginx 等）在跑              | 脚本会打印可直接粘贴的 Nginx 片段；或腾出 80/443 后用 \--caddy-mode self                                  |                                  |
| 打开面板提示注册被拒 / setup 返回 403                                  | 这台装的时候用了 \--lock-register       | 重跑加 \--open-register 开回来；或按隧道方式注册                                                      |                                  |

## 下载

**取件码 20818** —— 永久有效、不限次数：

[https://pan.ailxw.com/pickup/20818](https://pan.ailxw.com/pickup/20818?ref=isoziyuan.com)

**三个下载源，哪个通用哪个**（脚本内容完全一样，取回后可核对下面的 SHA256）：

```bash
# 源 1：网盘（推荐，国内可达）
curl -fsSL "https://pan.ailxw.com/api/pickup-download?code=20818" -o ~/a2a.sh && sudo bash ~/a2a.sh

# 源 2：jsDelivr CDN
curl -fsSL "https://cdn.jsdelivr.net/gh/yys9253462-gif/agent2api-installer@main/install-agent2api.sh" -o ~/a2a.sh && sudo bash ~/a2a.sh

# 源 3：GitHub 直连
curl -fsSL "https://raw.githubusercontent.com/yys9253462-gif/agent2api-installer/main/install-agent2api.sh" -o ~/a2a.sh && sudo bash ~/a2a.sh
```

也可以让引导脚本自己挨个试（它会依次尝试上面三个源，落盘后执行）：

```bash
curl -fsSL https://cdn.jsdelivr.net/gh/yys9253462-gif/agent2api-installer@main/deploy.sh | sudo bash
```

| 项目     | 值                                                                |
| ------ | ---------------------------------------------------------------- |
| 文件名    | install-agent2api.sh                                             |
| 大小     | 95874 字节                                                         |
| SHA256 | 8131f3301acbc81315625e428e37371b4bfd81b97544d7d3bdb41765b56dedf7 |
| 版本     | v1.7.0                                                           |
| 依赖     | 只要 docker（Debian/Ubuntu 系实测；脚本自身无其它依赖）                           |

源码与回归套件在这里，欢迎提 issue：

[https://github.com/yys9253462-gif/agent2api-installer](https://github.com/yys9253462-gif/agent2api-installer?ref=isoziyuan.com)

```bash
# 下载后建议先核一下 SHA256 再跑
sha256sum install-agent2api.sh
```