用户要求:教程要能让 agent 跟着做,且区分 agent 版与人类版。 新增 AGENTS.md(可机械执行,不做叙述): - §0 铁律 8 条(首装/升级不可混用、绝不重置门户口令、apply 后异步 reload、 /admin/api 前缀、路由表整表替换、脱敏清单、不 sed -i 单文件挂载、部署串行) - §1 场景判定(一条命令区分首装/升级) - §2 首装:前置检查、三组命令示例、**逐步期望输出**、5 条失败处置表 - §3 升级:硬性前提(旧容器无 cookie 域就必须传 NAVI_COOKIE_DOMAIN)、命令、 期望输出、收尾断言(含 navi-next 残留的处置) - §4 配置:先取 /api/schema 清单,再给可直接照抄的六类写入命令 + **写完必须验证** - §5 验收清单:五项探测及各自不符合时的含义 - §6 提交前脱敏清单(可执行命令 + 绝不可提交项 + "可配置项不得写死") - §7 回滚 §8 已知坑(8 条,全部是本次实测踩过的) README:顶部加"两种读者两个入口"导航,并在首装/部署两节加指向 AGENTS.md 的指引。
283 lines
14 KiB
Markdown
283 lines
14 KiB
Markdown
# AGENTS.md — 部署与配置执行手册(写给 AI agent / 自动化脚本)
|
||
|
||
本文件是**可机械执行**的版本:每条命令都给出**期望输出**,判断标准明确,不做叙述性解释。
|
||
人类读者请看 [README.md](README.md)。
|
||
|
||
---
|
||
|
||
## 0. 铁律(违反会造成线上故障,先读完再动手)
|
||
|
||
| # | 铁律 | 原因 |
|
||
| --- | --- | --- |
|
||
| 1 | **首装与升级脚本不可混用**:新机器用 `deploy/install-host.sh`;已有部署用 `deploy/zd-host.sh <版本>` | `zd-host.sh` 要求已存在 host 网络的 `navi` 容器,否则拒绝运行 |
|
||
| 2 | **绝不要重置门户口令**,除非用户明确要求 | 口令是所有受保护域名(含用户访问 agent 界面的入口)的唯一凭据;重置后没拿到新口令 = 把用户锁在门外。默认只清理明文 env,不动口令 |
|
||
| 3 | **改完路由表必须 `apply`**,且 `apply` 后是**异步 reload**:要轮询等入口就绪再探测 | 立即探测会拿到旧结果(表现为"apply 了却 404") |
|
||
| 4 | **管理接口前缀是 `/admin/api`**;根路径 `/api/` 会返回 200 + Dashy 的 HTML | 只看状态码会把"打到错误服务"误判成成功,判断命中要看返回体是否是 JSON |
|
||
| 5 | **路由表接口是整表替换**:必须提交完整 `{"routes":[…]}`;清空非空表要 `force:true` | 只提交单条会把整张表换掉 |
|
||
| 6 | **不要提交任何凭据/域名/内网地址/数据卷文件**(见 §6 脱敏清单) | 仓库会被发布 |
|
||
| 7 | **不要 `sed -i` 修改单文件 bind mount**(如 `portal/nginx/nginx.conf`) | inode 替换会导致全站 502;要用同 inode 写入(`cat >`)或重启容器 |
|
||
| 8 | **部署串行执行**,不要并发两次部署 | 并发会算错目标端口/镜像,导致半成品状态 |
|
||
|
||
---
|
||
|
||
## 1. 场景判定(先跑这段)
|
||
|
||
```bash
|
||
docker ps -a --filter name=^navi$ --format '{{.Names}} {{.Image}} {{.Status}}'
|
||
```
|
||
|
||
| 输出 | 场景 | 跳到 |
|
||
| --- | --- | --- |
|
||
| 空 | 全新机器 | §2 首装 |
|
||
| `navi` 存在且 `NetworkMode=host` | 已有部署 | §3 升级 |
|
||
| `navi` 存在但不是 host 网络 | 旧的桥接形态 | 先按 §3 的注意事项确认端口/网络,再升级 |
|
||
|
||
---
|
||
|
||
## 2. 首装(干净机器)
|
||
|
||
### 2.1 前置检查
|
||
|
||
```bash
|
||
docker info >/dev/null && echo DOCKER_OK # 期望:DOCKER_OK
|
||
ss -H -lnt | awk '{print $4}' | grep -E ':(3080|3081|8443)$' # 期望:无输出(端口空闲)
|
||
ls /vol1/docker/cert/fullchain.pem /vol1/docker/cert/privkey.pem # 有证书则列出;没有则用 NAVI_TLS=0
|
||
df -h /tmp | awk 'NR==2{print $4}' # 期望:≥2G(构建需要约 250M)
|
||
```
|
||
|
||
### 2.2 执行
|
||
|
||
```bash
|
||
# 有证书:
|
||
NAVI_DOMAIN=<你的域名> NAVI_SYNC_TOKEN=<随机> NAVI_ADMIN_TOKEN=<随机> \
|
||
bash deploy/install-host.sh
|
||
|
||
# 无证书(内网先跑通):
|
||
NAVI_DOMAIN=<你的域名> NAVI_TLS=0 NAVI_SYNC_TOKEN=<随机> NAVI_ADMIN_TOKEN=<随机> \
|
||
bash deploy/install-host.sh
|
||
|
||
# 非默认端口/目录:
|
||
NAVI_ENTRY_PORT=13080 NAVI_TLS_PORT=18443 NAVI_REDIRECT_PORT=13081 \
|
||
NAVI_DATA_DIR=/srv/navi-data NAVI_CERT_DIR=/srv/cert bash deploy/install-host.sh
|
||
```
|
||
|
||
### 2.3 期望输出(逐项核对,任一 ✗ 即失败)
|
||
|
||
```text
|
||
== 1/6 前置检查 == 数据目录 / 证书目录 / 端口回显
|
||
== 2/6 令牌 == 只打印一次,自行保存
|
||
== 3/6 镜像 == Successfully tagged homedash:ui-install
|
||
== 4/6 初始网关配置 == 已生成 <数据目录>/gateway.yml(含门户路由 …)
|
||
== 5/6 启动容器 == healthz 就绪(≤60s)
|
||
== 6/6 应用配置与自检 == apply: {"routes":2,"status":"applied"}
|
||
✓ 后台 /admin → 200 ✓ 内部配置 /conf.yml → 200
|
||
✓ 未知域名 → 自定义 404 ✓ 门户 <域名> → 200
|
||
✓ TLS 已生效(配置含 ssl_certificate) ← 仅 NAVI_TLS=1 时出现
|
||
✓ http→https 跳转 → 302 ← 仅 NAVI_TLS=1 时出现
|
||
首装完成。 ← 出现这行才算成功;退出码必须为 0
|
||
```
|
||
|
||
**失败处置**(脚本任一步失败即停,容器保留便于排查):
|
||
|
||
| 报错 | 原因 | 处理 |
|
||
| --- | --- | --- |
|
||
| `已存在名为 navi 的容器` | 该机器已有部署 | 改走 §3 升级,或先 `docker rm -f navi` |
|
||
| `端口 N 已被占用` | 端口冲突 | 用 `NAVI_*_PORT` 换端口 |
|
||
| `缺少证书 …/fullchain.pem` | 证书缺失/命名不符 | 放置 `fullchain.pem`+`privkey.pem`,或用 `NAVI_TLS=0` |
|
||
| `生成的配置里没有 ssl_certificate` | `gateway.yml` 字段名被改坏 | 检查 `listenHttps` / `listenTlsPort`(**大小写敏感**,写错会被 YAML 静默忽略) |
|
||
| `配置未通过 nginx 校验` | 生成的配置非法 | 界面/接口返回里带 nginx 报错原文;**旧配置会被保留**,入口不会中断 |
|
||
|
||
---
|
||
|
||
## 3. 升级(已有部署)
|
||
|
||
### 3.1 硬性前提
|
||
|
||
```bash
|
||
# ① 旧容器是否已有 cookie 域?没有就必须在命令里传 NAVI_COOKIE_DOMAIN
|
||
docker inspect navi --format '{{range .Config.Env}}{{println .}}{{end}}' | grep '^PORTAL_COOKIE_DOMAIN='
|
||
```
|
||
|
||
若上面**无输出**,则升级命令**必须**带 `NAVI_COOKIE_DOMAIN`,否则脚本会拒绝执行(这是刻意的:
|
||
新版已删除代码里写死的默认域名,不带就会让登录 cookie 退化为 host-only → 跨子域登录失效)。
|
||
|
||
### 3.2 执行(从开发机推送镜像)
|
||
|
||
```bash
|
||
NAVI_COOKIE_DOMAIN=.example.com NAVI_HOME_URL=https://example.com/ \
|
||
NAVI_HOST=user@host NAVI_SSH_PASS='<密码>' \
|
||
bash deploy/zd-host.sh <版本号>
|
||
```
|
||
|
||
- 开发机需 `sshpass`(或用密钥并自行去掉 `-p`)。
|
||
- 升级会有**数秒中断**(入口即应用容器),失败自动回滚。
|
||
- **不要**传 `NAVI_ROTATE_PORTAL_PASSWORD=1`,除非用户明确要求改门户口令。
|
||
- 需要指定自检域名时传 `NAVI_CHECK_HOST=<已配置的域名>`;默认自动取数据卷 `gateway.yml` 路由表首条。
|
||
|
||
### 3.3 期望输出
|
||
|
||
```text
|
||
== 1/3 本地构建 homedash:ui-<版本> == 镜像与包就绪: 222M
|
||
== 2/3 上传 ==
|
||
== 3/3 远端切换 ==
|
||
ready http://127.0.0.1:18080/healthz
|
||
ready http://127.0.0.1:18080/conf.yml
|
||
ready http://127.0.0.1:18080/admin
|
||
自检探测域名: <域名>
|
||
已配置域名 <域名> → 200|302
|
||
入口自检通过(3080/3081)
|
||
已重新生成网关配置: {"file":"…","routes":N,"status":"applied"}
|
||
DEPLOY_OK tag=homedash:ui-<版本> mode=host health=healthy
|
||
```
|
||
|
||
### 3.4 收尾断言(必做)
|
||
|
||
```bash
|
||
docker inspect navi --format '{{.Config.Image}} {{.State.Status}}' # 期望:homedash:ui-<版本> running
|
||
docker ps -a --filter name=navi-next --format '{{.Names}}' # 期望:无输出
|
||
```
|
||
|
||
若容器停在 `navi-next`(切换中途中断的残留,服务通常仍正常):
|
||
|
||
```bash
|
||
# 先用 navi-next 跑完 §5 验收,全绿后再改名
|
||
docker rename navi-next navi
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 配置(agent 直接用接口改,不必读代码)
|
||
|
||
### 4.1 先取清单
|
||
|
||
```bash
|
||
A=http://127.0.0.1:18080/admin/api # 经入口;容器内可直连 http://127.0.0.1:3005/api
|
||
TOK=<NAVI_ADMIN_TOKEN>
|
||
curl -s "$A/schema" | head -c 400
|
||
```
|
||
|
||
返回 `items`(单值配置项 + 当前值 + 写入口 + 请求体示例)与 `resources`(结构化资源及其写入口)。
|
||
**当前为空的键也会列出**,所以不要凭猜测字段名。
|
||
|
||
### 4.2 常用写入(照抄即可)
|
||
|
||
```bash
|
||
# 站点标题 / 各页文案 / 备案号(空串 = 回落内置默认)
|
||
curl -s -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
|
||
-d '{"title":"我的导航","portalTitle":"我的门户","heroTitle":"欢迎","icp":"京ICP备00000000号-1","authLogo":"JF"}' \
|
||
"$A/pageinfo"
|
||
|
||
# 门户页服务卡片(空串 = 恢复内置卡片)
|
||
curl -s -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
|
||
-d '{"html":"<a class=\"card\" href=\"https://example.com\">示例</a>"}' "$A/portal-cards"
|
||
|
||
# 路由表(整表替换)→ 必须 apply
|
||
curl -s -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
|
||
-d '{"routes":[{"host":"app.example.com","upstream":"http://127.0.0.1:3000","requireLogin":true,"websocket":false,"kind":"proxy"}]}' \
|
||
"$A/gateway/routes"
|
||
curl -s -X POST -H "Authorization: Bearer $TOK" "$A/gateway/apply"
|
||
|
||
# 导航分组 / 条目
|
||
curl -s -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
|
||
-d '{"name":"常用服务"}' "$A/section"
|
||
curl -s -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
|
||
-d '{"name":"我的应用","url":"https://app.example.com","section":"常用服务","icon":"auto"}' "$A/item"
|
||
|
||
# 壁纸 / 门户口令 / 历史
|
||
curl -s -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"url":"https://x/bg.jpg"}' "$A/wallpaper"
|
||
curl -s -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"username":"admin","password":"<新口令>"}' "$A/portal-auth"
|
||
curl -s -X DELETE -H "Authorization: Bearer $TOK" "$A/history/item?url=<编码后的URL>"
|
||
```
|
||
|
||
### 4.3 写完必须验证(不要以"接口返回 200"当成功)
|
||
|
||
```bash
|
||
HOST=<刚改的域名>
|
||
for i in $(seq 1 20); do # apply 后 reload 是异步的
|
||
code=$(curl -sk -o /dev/null -w '%{http_code}' -m 3 --resolve "$HOST:3080:127.0.0.1" "https://$HOST:3080/")
|
||
[ "$code" != "000" ] && break; sleep 0.5
|
||
done
|
||
echo "$code" # 期望:200(门户)或 302(受保护)
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 验收清单(部署后 / 改配置后都跑一遍)
|
||
|
||
把 `<HOST>` 换成路由表里任一已配置域名。**每一项都要看实际输出,不要假定通过。**
|
||
|
||
```bash
|
||
T=$(curl -sk -o /dev/null -w '%{http_code}' -m 8 --resolve "$HOST:3080:127.0.0.1" "https://$HOST:3080/")
|
||
U=$(curl -sk -o /dev/null -w '%{http_code}' -m 8 --resolve nobody.invalid:3080:127.0.0.1 https://nobody.invalid:3080/)
|
||
H=$(curl -s -o /dev/null -w '%{http_code}' -m 8 http://127.0.0.1:18080/healthz)
|
||
S=$(curl -s -o /dev/null -w '%{http_code}' -m 8 http://127.0.0.1:18080/admin/api/schema)
|
||
R=$(curl -s -o /dev/null -w '%{http_code}' -m 8 -H "Host: $HOST" http://127.0.0.1:3081/)
|
||
printf '已配置域名=%s 未知域名=%s healthz=%s schema=%s 跳转=%s\n' "$T" "$U" "$H" "$S" "$R"
|
||
```
|
||
|
||
| 项 | 期望 | 不符合时的含义 |
|
||
| --- | --- | --- |
|
||
| 已配置域名 | `200` 或 `302` | `404`=不在路由表;`000`=没监听/证书问题;`5xx`=上游或配置坏了 |
|
||
| 未知域名 | `404` | 不是 404 说明未知域名兜底配置丢了 |
|
||
| healthz | `200` | 容器内服务异常 |
|
||
| schema | `200` | 镜像过旧(不含新接口),或前缀打成 `/api/` |
|
||
| HTTP→HTTPS 跳转 | `302` | 跳转端口/开关配置问题 |
|
||
|
||
附加(有数据时):
|
||
|
||
```bash
|
||
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:3005/api/gateway/routes | grep -o '"host"' | wc -l # 路由条数应与升级前一致
|
||
docker exec navi sh -c 'ls /app/data/portal-auth.json' # 存在 → 登录凭据仍在数据卷
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 提交前脱敏清单(把仓库发布出去之前必跑)
|
||
|
||
```bash
|
||
# 期望:全部为 0
|
||
git grep -cIE 'REDACTED_TOKEN|REDACTED_TOKEN|PORTAL_PASSWORD_REDACTED' | wc -l # 真实令牌/口令
|
||
git grep -cIE '10.0.0.[0-9]+' | wc -l # 真实内网地址
|
||
git grep -cIE 'example.com' | wc -l # 真实域名
|
||
git grep -cIE '豫ICP备00000000|豫公网安备00000000000000' | wc -l # 真实备案号
|
||
git ls-files | grep -cE '(^|/)(conf\.yml|gateway\.yml|history\.json|portal-auth\.json|\.env)$' # 数据卷文件
|
||
git ls-files | grep -ciE '\.(pem|key|crt|p12|pfx)$' # 私钥/证书
|
||
```
|
||
|
||
**绝不可提交**:`conf.yml`、`gateway.yml`、`history.json`、`portal-auth.json`、`.env`、
|
||
`*.pem|key|crt`、任何真实令牌与口令、真实域名/内网地址/备案号。
|
||
|
||
**可配置项不要写死在代码里**:文案走 `pageInfo`、备案号走 `pageInfo.icp/gongan`、
|
||
域名走环境变量(`PORTAL_COOKIE_DOMAIN` / `PORTAL_HOME_URL` / `gateway.yml` 的 `loginUrl`)、
|
||
门户卡片走数据卷 `portal/cards.html`。
|
||
|
||
---
|
||
|
||
## 7. 回滚
|
||
|
||
```bash
|
||
# 升级失败会自动回滚(脚本内建);手动回滚到上一个版本:
|
||
docker ps -a --filter name=navi-prev --format '{{.Names}} {{.Image}}' | tail -3
|
||
docker rm -f navi # 或 docker rename navi navi-broken
|
||
docker rename navi-prev-<时间戳> navi
|
||
docker start navi
|
||
# 验证:跑 §5 清单
|
||
```
|
||
|
||
保留几天再清理历史回滚点;`/app/data` 数据卷与 `gateway.yml.bak-*`、`conf.yml.bak-*` 是配置级回滚点。
|
||
|
||
---
|
||
|
||
## 8. 已知坑(都是实测踩过的,别重复踩)
|
||
|
||
| 现象 | 真因 | 处理 |
|
||
| --- | --- | --- |
|
||
| 改配置后探测 404,其实成功了 | `apply` 的 reload 是异步的 | 轮询等待入口就绪(§4.3) |
|
||
| 接口返回 200 但什么都没改 | 打到了根 `/api/`(Dashy) | 用 `/admin/api`,看返回体是否 JSON |
|
||
| "HTTPS 端口"其实是明文 | `gateway.yml` 字段名大小写不符(`listenHttps`),YAML 静默忽略 | 对齐字段名;脚本会断言配置含 `ssl_certificate` |
|
||
| 提交一条路由后端口/其它路由全变了 | 旧接口整份配置重置(已修) | 升级到含修复的版本;提交完整 `{"routes":[…]}` |
|
||
| 脚本报 `HOST: unbound variable` | 必填校验写在赋值之前 + `set -u` | 升级脚本(已修);或先给变量赋值再校验 |
|
||
| 容器名叫 `navi-next` | 切换中途中断 | 验收通过后 `docker rename navi-next navi` |
|
||
| `no space left on device` | `/tmp` 是 tmpfs,构建包 ~222M | 构建前 `rm -rf /tmp/homedash-*` |
|
||
| 部署脚本用 `bash -s` 传参不生效 | 远端 heredoc 是引号形式,不展开本地变量 | 在 ssh 命令行显式传(脚本已处理) |
|