Files
HomeDash/AGENTS.md
HomeDash Bot 02b321ab51 docs: 拆分双版本教程 —— AGENTS.md(agent 照着做)+ README.md(人类)
用户要求:教程要能让 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 的指引。
2026-09-12 10:50:02 +08:00

283 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 命令行显式传(脚本已处理) |