Files
HomeDash/AGENTS.md
HomeDash Bot 30aff4ffd8 docs: 同步「常用服务 / 历史访问」的新规则(不再限制条数、历史含站内服务)
- 常用服务:从"点击次数前 5"改为"所有被点击过的导航项(按次数降序,不限条数)"
- 历史访问:不限条数,且不再排除站内服务(相当于"最近访问");并提示可用 history.json 的 mtime 判断扩展是否还在同步
- README 三处 + AGENTS 一处的过时说明一并更新
2026-09-13 08:39:00 +08:00

329 lines
18 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"
> ⚠️ **两个分组是系统管理的,不要往里手工加条目**:
> - `常用服务`:收录**所有被点击过**的导航项,按点击次数降序(**不置顶任何服务、不设条数上限**),服务端重建;
> 计数来自 `POST /api/history/nav-visit`(导航页点击自动上报,落盘 `nav-visits.json`),**不使用浏览器历史**
> - `历史访问`:按浏览器同步的浏览记录重建,**不限条数、且包含站内服务**(相当于"最近访问",安装入口由它自己放在首位)。
> 数据量取决于浏览器扩展的同步(`history.json` 的 mtime 可判断是否还在同步)
>
> 因此 `navEnabled` 的 `navSection` 必须指向**用户分组**(如 `AI & 工具`、`系统管理`)。
> 实测教训:把新服务直接塞进 `常用服务` 会污染频次榜,且该条目会被下一次重建挤掉。
# 导航条目是**嵌套形状**(实测:扁平 {"name","url"} 会返回 "title and url required"):
# 先查分组下标,再提交 {"sectionIndex":N,"item":{...}}
IDX=$(curl -s -H "Authorization: Bearer $TOK" "$A/sections" | \
python3 -c 'import sys,json;d=json.load(sys.stdin);print([i for i,s in enumerate(d) if s["name"]=="常用服务"][0])')
curl -s -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
-d "{\"sectionIndex\":$IDX,\"item\":{\"title\":\"我的应用\",\"url\":\"https://app.example.com\",\"icon\":\"auto\",\"description\":\"说明\"}}" "$A/item"
# 删除条目:**DELETE + JSON body**(POST 会返回 method not allowed),下标从 /sections 里数
curl -s -X DELETE -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
-d '{"sectionIndex":0,"itemIndex":4}' "$A/item/delete"
> 形状不确定时先取 `GET /api/schema`(§4.1)或读 `admin-server/main.go` 里的处理函数:
> 本手册初版就把 `/api/item` 写成了扁平形状,实测被拒——**文档里的示例必须实测过**。
# 导航项点击计数(「常用服务」排序的唯一依据;前端点击时自动上报,这里用于人工/脚本触发)
curl -s -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
-d '{"url":"https://app.example.com/"}' "$A/history/nav-visit"
# 壁纸 / 门户口令 / 历史
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=<刚改的域名>
PORT=${NAVI_ENTRY_PORT:-3080}; SCHEME=${NAVI_TLS_SCHEME:-https}
for i in $(seq 1 20); do # apply 后 reload 是异步的
code=$(curl -sk -o /dev/null -w '%{http_code}' -m 3 --resolve "$HOST:$PORT:127.0.0.1" "$SCHEME://$HOST:$PORT/")
[ "$code" != "000" ] && break; sleep 0.5
done
echo "$code" # 期望:200(门户)或 302(受保护)
```
---
## 5. 验收清单(部署后 / 改配置后都跑一遍)
把 `<HOST>` 换成路由表里任一已配置域名。**端口必须用变量**:写死 3080 时,非默认端口
部署(例如 `NAVI_ENTRY_PORT=13080`)会全部探测到 `000` —— 这是实测踩过的坑。
```bash
PORT=${NAVI_ENTRY_PORT:-3080} # 入口端口(非 3080 部署必须设)
RT=${NAVI_REDIRECT_PORT:-3081} # http→https 跳转端口
SCHEME=${NAVI_TLS_SCHEME:-https} # NAVI_TLS=0 的部署用 http
T=$(curl -sk -o /dev/null -w '%{http_code}' -m 8 --resolve "$HOST:$PORT:127.0.0.1" "$SCHEME://$HOST:$PORT/")
U=$(curl -sk -o /dev/null -w '%{http_code}' -m 8 --resolve nobody.invalid:$PORT:127.0.0.1 "$SCHEME://nobody.invalid:$PORT/")
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:$RT/)
printf '已配置域名=%s 未知域名=%s healthz=%s schema=%s 跳转=%s\n' "$T" "$U" "$H" "$S" "$R"
```
> `跳转=302` 只在**启用 TLS** 的部署上适用;`NAVI_TLS=0` 的安装没有跳转端口,该项会得到 `000`,属预期。
| 项 | 期望 | 不符合时的含义 |
| --- | --- | --- |
| 已配置域名 | `200` 或 `302` | `404`=不在路由表;`000`=没监听/证书问题;`5xx`=上游或配置坏了 |
| 未知域名 | `404` | 不是 404 说明未知域名兜底配置丢了 |
| healthz | `200` | 容器内服务异常 |
| schema | `200` | 镜像过旧(不含新接口),或前缀打成 `/api/` |
| HTTP→HTTPS 跳转 | `302` | 跳转端口/开关配置问题 |
**交互类改动必须用真实输入验证,不能只查属性**(例如 `/minimal` 的左右滑动切换分组):
用 CDP 注入触摸(`Emulation.setTouchEmulationEnabled` + `Input.dispatchTouchEvent`)、
真实鼠标 `mouse.down/move/up`、`mouse.wheel(deltaX, 0)`;断言"切换真的发生"
(读 `.minimal-headings-row > .minimal-section-heading.headings` 中 `.selected` 的下标变化),
并要求 `<html>` 上 `data-navi-swipe="on"`、`pageerror` 为 0。
只断言"事件到了 / 监听器挂了"**不算通过** —— 本项目实测踩过:事件全都到达,
但自建拦截器把自己的切换 click 吞了,表现为"滑了没反应"。
附加(有数据时):
```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 -nIE '(NAVI_(SYNC|ADMIN)_TOKEN|PORTAL_PASSWORD|PORTAL_SESSION_SECRET)=[0-9A-Za-z_-]{12,}' | grep -v '^AGENTS.md' | wc -l # 真实令牌/口令(按长度+字符集识别,不写具体值)
git grep -nIE '192\.168\.[0-9]+\.[0-9]+' | wc -l # 内网地址
git grep -nIE '[京沪粤豫苏浙川鲁闽湘鄂]ICP备[0-9]{6,}' | grep -v '^AGENTS.md' | wc -l # 备案号(省简称+ICP备+数字)
git grep -nIE '公网安备[0-9]{8,}' | grep -v '^AGENTS.md' | wc -l # 公网安备
git grep -nIE '(^|[^a-z])([a-z0-9-]+\.)+(xyz|top|club|vip)([^a-z]|$)' | grep -v '^AGENTS.md' | wc -l # 真实域名后缀(按需保留白名单)
git ls-files | grep -cE '(^|/)(conf\.yml|gateway\.yml|history\.json|portal-auth\.json|\.env)$' # 数据卷文件必须为 0
git ls-files | grep -ciE '\.(pem|key|crt|p12|pfx)$' # 私钥/证书必须为 0
```
> ⚠️ **不要把真实令牌、口令、备案号、域名当作"示例模式"写进任何文档**(本手册初版就犯过:
> 把真实值写进自检命令,等于把凭据提交进仓库)。检测一律用"长度/字符集/格式"特征来写。
**绝不可提交**:`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-*` |
| `常用服务` 顺序不像"最常点的" | 它只认导航项点击计数,不认浏览器历史 | 用上面的 nav-visit 接口触发/查看;计数文件 `nav-visits.json` |
| 照抄验收命令却全是 000 | 文档里的端口写死 3080,而部署用了别的端口 | 用 `PORT=${NAVI_ENTRY_PORT:-3080}`(本手册已改) |
| 脱敏自检把文档自己判为"命中" | 自检命令里写了真实值当模式 | 用长度/字符集特征检测,并排除文档自身(本手册已改) |
| 部署脚本用 `bash -s` 传参不生效 | 远端 heredoc 是引号形式,不展开本地变量 | 在 ssh 命令行显式传(脚本已处理) |