把本地静态站点(个人主页、落地页、单文件 HTML)一键完成「备案合规扫描 → SCP 上传 → 真实外网验证」,并支持服务器 nginx 诊断。内置广告法违禁词、境外 CDN、清除词残留、备案要素四道检查。当用户说"改完主页发布一下""把页面传到服务器""发布前检查下备案/合规""看看线上生效没有""主页改了重新发布""去掉某内容再发"时使用。适用于腾讯云/宝塔 nginx 托管的静态站点。
---
name: 静态主页发布与合规检查
description: 把本地静态站点(个人主页、落地页、单文件 HTML)一键完成「备案合规扫描 → SCP 上传 → 真实外网验证」,并支持服务器 nginx 诊断。内置广告法违禁词、境外 CDN、清除词残留、备案要素四道检查。当用户说"改完主页发布一下""把页面传到服务器""发布前检查下备案/合规""看看线上生效没有""主页改了重新发布""去掉某内容再发"时使用。适用于腾讯云/宝塔 nginx 托管的静态站点。
agent_created: true
---
# 静态主页发布与合规检查
> ## 🔒 隐私红线(最高优先级,任何修改都不得违反)
>
> **本 skill 内禁止出现任何:服务器 IP / 域名、服务器目录路径、SSH 账号、密钥路径、内网端口。**
> 原因:skill 会被打包成 zip 公开发布到网站供人下载,夹带真实服务器信息等于把钥匙交给外人。
>
> 落地规则:
> 1. 所有服务器相关值只能来自 `publish.config.json`(**放在项目目录,不在 skill 内**)或命令行参数;
> 2. 文档示例一律用 `<服务器IP或域名>` / `<站点根目录>` / `$VHOST_DIR` 等占位符;
> 3. 缺配置时脚本**直接报错退出**,绝不猜默认值、绝不回落到某个具体地址;
> 4. 打 zip 前必须跑一次敏感扫描(见文末「发布前自检」)。
把"改主页 → 合规扫描 → 上传 → 验证"这条高频重复劳动压成**一条命令**,同时把踩过的坑固化成硬约束,避免每次从头踩。
## 核心认知(血泪坑,先看这个能省半小时)
**坑1:验证绝对不能用 `curl 127.0.0.1`。**
nginx 80 端口往往有多个 server block,`127.0.0.1` 会命中**别的** server(典型:宝塔 `phpfpm_status.conf`),它没有 `location /`,于是走到 nginx 兜底页(138 字节欢迎页/404)。你会误判成"我的配置没生效/主页没传上去",然后往错误方向查半天。
✅ 正确做法:**永远用真实外网地址或域名** curl(如 `http://<你的域名>/`),才会按 `server_name` 精确命中目标 server。
**坑2:本机 curl 必须加 `--noproxy '*'`。**
Windows 系统代理会拦掉脚本发出的请求,报 `502 Bad Gateway`。浏览器不受影响,只有脚本中招。脚本内部已内置,手写 curl 时别漏。
**坑3:宝塔 vhost 文件默认 600 权限,nginx worker (www) 读不到 → 静默失效。**
表现是 `nginx -t` 通过、`reload` 无报错,但配置就是不生效。
✅ 改完 vhost 必须 `chmod 644`。
**坑4:`nginx -s reload` 输出空 ≠ 成功。**
必须 curl 看响应头 `Content-Length` 跟本地文件字节数对不对得上,才算真生效。
**坑5:改 vhost 前先 backup。**
`cp xxx.conf xxx.conf.bak.$(date +%Y%m%d_%H%M%S)`,回滚就一句 `cp`。
**坑6(红线):ICP 备案号绝不可编造。**
虚假备案号比不挂严重得多(工信部可查,罚款/关站)。纯 IP 访问不强制备案(备案主体是域名),**绑域名才必须备案**。未拿到真实备案号时,脚本只 WARN 不阻塞。
**坑7:`data-page-node-id` 会让 HTML 体积膨胀。**
编辑时自动注入每个元素一个 ID,21.7KB → 27KB 属正常,**不影响功能和显示**,别当 bug 查。
**坑8(2026-09-08 真事故):扫描器报"0 命中" ≠ 真的干净。**
老版 `scan_secret.py` 只认 Linux 路径(`/www`、`/opt`)和 "root 加 @" 的写法,**对 Windows 的 `C:/Users/<你的用户名>/.workbuddy/...` 完全不检测**——一路绿灯把本机用户名传上了公网。
✅ 已补规则 `Windows 用户目录`;文档里的占位写法用 `<你的用户名>` 或 `Users/x`,已进白名单。
✅ 教训:**"已上架"的老 skill 也要用新规则回扫**,别以为上次过了就永远过了。
**坑9:打包是整目录 `make_archive`,会把本机痕迹一起传走。**
`_skillhub_meta.json` 里躺着 `C:\Users\<你的用户名>\...` 的绝对路径,`.local.env` 里躺着真实 IP / 授权 key / 私钥路径。
✅ `sync_homepage.py` 的 `_pack_skill()` 现在会跳过:`_` 和 `.` 开头的文件、`.pyc`、`.local.env`、`.env`、`__pycache__` 等。
✅ 需要留本机的私有配置,就放进 skill 根目录的 `.local.env`,脚本读它(模式:环境变量 > `.local.env` > 占位默认值),本机用法不变、公开包干净。
## 快速上手
脚本:`scripts/site_publish.py`(纯标准库,无依赖,跨平台)。
```bash
PY="C:/Users/<你的用户名>/.workbuddy/binaries/python/versions/3.13.12/python.exe"
S="C:/Users/<你的用户名>/.workbuddy/skills/static-site-publish/scripts/site_publish.py"
```
```bash
# 1) 只扫描不发布(改完先过一遍)
"$PY" "$S" scan ./index.html --must-clear "rustdesk,hbbs"
# 2) 全流程:扫描 → SCP 上传 → 外网验证(推荐日常用这条)
"$PY" "$S" deploy ./index.html --must-clear "rustdesk" \
--verify /aqh-learn/home --present "AI 学习助手"
# 3) 只做线上验证(改了服务器配置后复查)
"$PY" "$S" verify http://<你的域名>/ --expect-file ./index.html \
--absent "RustDesk" --present "AI 学习助手"
# 4) 服务器侧诊断(nginx -t / vhost 权限 / 外网自测)
"$PY" "$S" remote
```
脚本**不含任何默认目标**。服务器信息一律从 `publish.config.json` 或命令行传入(见下方「配置方式」),
换机器只需换一份配置文件,脚本本身通用。
## 六道合规扫描(`scan` 自动跑)
| # | 检查项 | 命中处理 |
|---|---|---|
| ① | 广告法/平台违禁词(最/第一/顶级/国家级/百分百/根治/暴富…) | FAIL 阻塞 |
| ② | 境外 CDN(Google Fonts / jsdelivr / unpkg / bootcdn…) | FAIL 阻塞 |
| ③ | 外链归属(是否全部指向自有域名) | 第三方 WARN |
| ④ | 必清词残留(`--must-clear`,如要删的产品名) | FAIL 阻塞 |
| ⑤ | 备案要素(ICP 号 / 工信部链接 / 主办单位 / 联系方式) | WARN 不阻塞 |
| ⑥ | 外部 script 引用 | WARN |
退出码:`0` 通过 / `1` 有问题 / `2` 文件不存在。`deploy` 遇阻塞会中止上传,确认无碍可加 `--force`。
> 注:`xmlns="http://www.w3.org/2000/svg"` 这类**命名空间声明**已自动排除,不会误报成外链。
## 标准流程(改主页的完整 SOP)
1. **改本地文件** —— 只改明确要求的内容,不擅自扩大范围。
2. **`scan` 扫描** —— 必清词用 `--must-clear` 传入,确认全部 PASS。
3. **`deploy` 发布** —— 自动扫描 + SCP + 三重验证(HTTP 200 / 字节数吻合 / 清除词线上 0 命中)。
4. **`present_files` 交付** —— 把源文件给用户看线上对照。
5. **写 memory** —— 记下改了什么、URL、踩的坑。
## 需要动 nginx vhost 时(如加新路径/新站点)
严格按序,**一步都不能省**:
```bash
# 1. backup
cp $VHOST_DIR/xxx.conf $VHOST_DIR/xxx.conf.bak.$(date +%Y%m%d_%H%M%S)
# 2. 改配置(注意:location 精确路径优先级 > 前缀,反代不会被 location / 吃掉)
# 3. 权限(不改 600 会静默失效)
chmod 644 $VHOST_DIR/*.conf
# 4. 校验 + 重载
$(command -v nginx) -t && $(command -v nginx) -s reload
# 5. 用外网地址验证(不是 127.0.0.1!)
curl -sI http://<你的域名>/ | head -5
```
## 配置方式(skill 内不含任何真实服务器信息)
| 配置项 | 含义 | 传入方式(优先级从高到低) |
|---|---|---|
| `host` | 服务器 IP / 域名 | 命令行 `--host` |
| `user` | SSH 用户名 | 命令行 `--user` |
| `key` | SSH 私钥路径 | 命令行 `--key` |
| `remote` | 远程目标文件绝对路径 | 命令行 `--remote` |
| `base_url` / `url` | 站点外网根地址(**验证必须用这个,不是 127.0.0.1**) | 命令行 `--url` |
| `self_host` | 视为"自有域名"的 host 列表 | 命令行 `--self-host` |
1. 命令行参数(最高优先级)
2. `./publish.config.json`(**项目目录内,不随 skill 分发**)
3. `~/.workbuddy/site-publish.config.json`(用户级,不在 skill 目录内)
4. 环境变量 `SITE_HOST` / `SITE_USER` / `SITE_KEY` / `SITE_REMOTE` / `SITE_BASE_URL`
模板见 `publish.config.example.json`(**只有占位符,无真实值**)。
缺配置时脚本会直接报错退出,**绝不猜、绝不填默认值**。
## 增量同步(让主页板块自己长)
新增 skill / 应用不用手改 `index.html`,走 `scripts/sync_homepage.py`(靠项目目录的 `registry.json` 驱动)。
主页内需有锚点:`<!-- CARDS:SKILLS:BEGIN/END -->`、`<!-- CARDS:APPS:BEGIN/END -->`。
```bash
cd <项目目录> # 脚本以当前工作目录作为项目根目录
"$PY" sync_homepage.py status # 概览 + 发现未登记 skill + 应用探活
"$PY" sync_homepage.py add-skill "<skill目录>" # 登记一个 skill(描述取自 SKILL.md)
"$PY" sync_homepage.py add-app --name X --url U ... # 登记一个线上应用
"$PY" sync_homepage.py build # 按 registry 重建卡片(自动 backup)
"$PY" sync_homepage.py pack --all # 重新打包 zip
"$PY" sync_homepage.py sync # 全流程:打包 → 重建 → 上传 → 发布
```
> ### ⚠️ 安全默认:发现新 skill **只报告,不自动上架**
> `sync` 扫到未登记 skill 时默认跳过并告警,必须人工 `add-skill` 确认,或显式加 `--auto-add`。
>
> **事故教训(2026-09-06)**:初版 `sync` 会自动登记并上传所有扫到的 skill,
> 结果把 `rustdesk-hbbr-relay-stress`、`flask-deploy-tencent-lighthouse` 等 **4 个含私有服务器信息的 skill
> 直接传到了公开服务器**,主页上也多出 4 张不该出现的卡片(含刚被要求清除的 RustDesk)。
> 事后删服务器文件 + 回滚 registry + 重建发布才止损。
> **结论:公开上架必须是白名单动作,绝不能"扫到就传"。**