Files
dsh_shenxian/dsh-server-docs/ops/域名迁移_ai1net_20260919/RUNBOOK-cutover.md
T
admin 971ccc3703 feat(auth): 注册页人机验证 + 邮箱验证码;品牌标识去 DeepSeek(附域名迁移线 序㊿ 补提交)
三条线合并入库 —— 均已完成并上线(源码与生产一致,此前只部署未入仓)。
⚠️ 其中域名迁移线为**另一会话**产出,本会话只做入库、**未复验其正确性**(它自报零回归)。

【档案 134 · 注册页人机验证 + 邮箱验证码】
- DB 迁移 v10:users.email(唯一索引 LOWER(email))+ email_codes 事件表(2 索引)
- 新增模块 src/web/{register-guard,mail,turnstile,email-code}.ts
- routes/auth.ts:新增 GET /api/auth/register/config、POST /api/auth/register/email-code;
  注册接口加人机验证与验证码校验;config.ts 新增 12 项配置(默认空 ⇒ 不配 = 老行为)
- 邮件走**可插拔驱动**(brevo/http/log),发件人 [email protected](Brevo 域名已认证 + DKIM + SPF)
- 防爆破:三层配额(邮箱 6/h、8/天;IP 20/h;全局 200/h)+ 递增冷却阶梯
  (60→60→180→300→900→1800s)+ 试错 5 次作废 + 码只存哈希 + 单次使用 + 与用户名绑定
- Turnstile 服务端校 **success + action + hostname 三项**:sitekey 是公开的,
  只校 success 时"拿我们的 sitekey 在自己站点替真人取合法 token 再打我们接口"这条路是通的
- 新增 test/register-guard.test.mjs(19 用例)

【档案 137 · 品牌标识改造 — 去 DeepSeek 图形】
- login/register/admin 页头:删 DeepSeek 鲸鱼图标 + 「DeepSeek」文字图形
  → 平台标识(中文「能力枢纽」/英语及其他语言「CapabilityNet」,走 i18n 词条 brand.name)
- portal 顶栏换图标(页面名「管理门户」保留)
- 新建 web/favicon.svg(平台自有 hub 图标,避开 DeepSeek 蓝)+ 四页 favicon 指向它
- 新增 test/i18n-brand.test.mjs(node:vm 跑真实 i18n.js,六条语言路径断言渲染结果)
- scripts/verify-static.mjs 新增 SVG 段:XML 注释不得含 ASCII 双连字符(否则整份 SVG
  解析失败、图标静默不显示 —— 实际踩到过)
- 🔴 会话页面(实例内官方 dsh 界面)的标识**按用户要求未动**(也受 R2 约束)

【档案 135/136 · 域名迁移线(另一会话产出)】
- 域名收敛为 ai1net.com;旧域 alotbuy.com 降级为 301 过渡装置
- src/net/relay/{addr-override,directory,rendezvous,switcher}.ts 种子与候选链更新;
  src/web/server.ts、src/worker/relay-tunnel.ts、scripts/verify-cluster-domain.mjs
- 档案 136 = 控制面按两台中继取并集(**已立项、未落地**)

验证(本会话两条线):新增单测 21 条全通过|全量 221 pass / 0 fail / 1 skipped|
verify-static 全合格|其余 10 个 verify 脚本全 OK|线上实测:Turnstile 假 token 403、
发码 delivered、四页 deepseek 命中 0、favicon 200。
2026-09-19 09:11:24 +08:00

99 lines
7.9 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.
> ✅ **2026-09-19 07:3x 状态更新:本 RUNBOOK 已全部执行完,且「旧域彻底退役」那一步也一并做了。**
> 原件原位于 47 的 `/www/server/panel/vhost/nginx/_pending-ai1net/`(该目录已于本日归档报废)
> ⇒ **本文件是该 RUNBOOK 的唯一在册副本**。
>
> - **§1 阻塞项已解决**:用户提供 ai1net.com 的 CF 令牌 ⇒ 落 `/etc/cloudflare-ai1net.ini`(600)
> ⇒ `certbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/cloudflare-ai1net.ini
> --dns-cloudflare-propagation-seconds 60 -d ai1net.com -d '*.ai1net.com' --cert-name ai1net.com` 签发成功(DNS-01)。
> - **§2 的 S1–S5 全部执行**(06:0x–07:3x):两份新 vhost 已启用;`/etc/dshs.env` 已切到 `ai1net.com` / `.ai1net.com`;
> `relay-direct.conf` 与 `dsh.alotbuy.com.conf` 已改指新域;106 `/etc/dshs-worker.env` 已补同份种子。
> - 🔴 **§4 末尾那条「留待你确认退役时机」已被确认并落地**(用户 2026-09-19 明令「旧域从项目中移除,后续全部使用 ai1net.com」):
> `alotbuy.com.conf` 由**独立门户**降级为 **301 过渡装置**;引导种子里的 2 条旧域项已撤。
> ⇒ 完整判据 / 步骤 / 验收 / 回滚见 **`04-调整方案/135-旧域alotbuy.com从项目移除.md`**。
> ⚠️ 故本文件 §4 末条「本轮**不做**」的表述**已被本次取代**(⛔ §4 原文保留不改,仅在此更正)。
> - **回滚点**:47 `/root/_dompurge/`(被替换的两份新配置、目录缓存备份、归档的 `_pending-ai1net-*`);
> 生产侧 `/www/server/panel/vhost/nginx/alotbuy.com.conf.bak-dompurge-20260919-073004`、
> `/etc/systemd/system/dshs.service.d/overlay-443fb.conf.bak-dompurge-20260919-073101`、
> 106 `/etc/dshs-worker.env.bak-dompurge-20260919-073149`。
---
# 域名迁移 runbook —— `alotbuy.com` → `ai1net.com`(47 · 2026-09-19 建立)
> 本文件与同目录两个 `*.conf` 一起放在 47 的 `/www/server/panel/vhost/nginx/_pending-ai1net/`(**待启用**,nginx 只 include 该目录的 `*.conf` 一层,子目录天然不生效)。
> 目标:把线上部署的 DSH 服务域名由 `alotbuy.com` 切到 `ai1net.com`(门户 + 实例子域 + 中继兜底入口 + 平台 env)。
## 0. 现状(2026-09-19 取证)
| 项 | 现状 |
|---|---|
| 门户 vhost | `/www/server/panel/vhost/nginx/alotbuy.com.conf`:`alotbuy.com www.alotbuy.com *.alotbuy.com` → 3080,`/dshs-relay` → 20080,证书 `live/alotbuy.com`(SAN `alotbuy.com` + `*.alotbuy.com`) |
| 实例子域 | **`<user>.alotbuy.com`**(一级标签,由门户 vhost 的 `*.alotbuy.com` 承载,无需第二张证书) |
| 旧名 301 | `dsh.alotbuy.com.conf`:`dsh.alotbuy.com` / `*.dsh.alotbuy.com` → 301 到 `alotbuy.com` / `<label>.alotbuy.com`,证书 `live/dsh.alotbuy.com` |
| 中继兜底 | `relay-direct.conf`:`relay-direct.alotbuy.com`(443),两端点 `/dshs-relay` + `= /dshs-overlay/bootstrap`(Host 改写成 `alotbuy.com`) |
| 平台 env | `/etc/dshs.env`:`DSHS_BASE_DOMAIN=alotbuy.com`、`DSHS_COOKIE_DOMAIN=.alotbuy.com`、`DSHS_PORT=3080` |
| 中继引导种子 | `lib/config.js` 内置 `['https://alotbuy.com/dshs-relay']`;env 覆盖键 = **`DSHS_OVERLAY_BOOTSTRAP_SEEDS`**(`splitList`,可多值) |
| 证书签发 | certbot 1.22.0 + **dns-cloudflare** 插件;凭据 `/etc/cloudflare.ini`(**令牌仅覆盖 alotbuy.com 一个 zone**) |
**ai1net.com 侧已就绪的部分**:DNS 已在 Cloudflare(`ai1net.com` / `www` / `*` / `<x>.dsh` 均解析到 CF)、CF→源站(47.77.182.89)链路已验证可达(用 80 端口探针文件经 CF 取回原文)、LE **http-01 路径可用**。
## 1. 🔴 唯一阻塞项(需用户侧提供)
**`*.ai1net.com` 通配证书必须走 DNS-01**(LE 对通配符只认 DNS-01),而 47 上现存的 CF 令牌**只覆盖 alotbuy.com**(实测 `/zones` 只返回 1 个 zone)⇒ 我无法为 ai1net.com 写 `_acme-challenge` TXT。
两条可选路径(任选其一):
- **A. 给一份 ai1net.com 域的 CF API 令牌**(权限最小化:`Zone → DNS → Edit`,Zone Resources = 仅 `ai1net.com`)
→ 落到 `/etc/cloudflare-ai1net.ini`(`dns_cloudflare_api_token = <令牌>`,mode 600)
→ 我执行 S1:`certbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/cloudflare-ai1net.ini --dns-cloudflare-propagation-seconds 60 -d ai1net.com -d '*.ai1net.com' --cert-name ai1net.com`
- **B. 在 CF 面板生成 Origin CA 证书**(主机名填 `ai1net.com, *.ai1net.com`),把 cert + key 文本给我
→ 落到 `/etc/ssl/ai1net/{fullchain.pem,privkey.pem}`,`ai1net.com.conf` 里两处证书路径改指它
→ 无需给我任何 API 权限;代价是证书由 CF 侧管理、不参与 certbot 自动续期。
## 2. cutover 步骤(**证书就绪后**按序执行,每步带验收)
```bash
V=/www/server/panel/vhost/nginx
# S1 证书(见 §1;A 路径用 certbot,B 路径跳过)
certbot certificates | grep -A3 ai1net.com
# S2 启用两份 vhost(⛔ 证书不存在时**不要**做这步 ⇒ nginx 起不来 = 门户全挂)
cp -a $V/_pending-ai1net/ai1net.com.conf $V/ai1net.com.conf
cp -a $V/_pending-ai1net/relay-direct.ai1net.com.conf $V/relay-direct.ai1net.com.conf
nginx -t && nginx -s reload
# 验收:curl -s --http1.1 -o /dev/null -w '%{http_code}\n' https://ai1net.com/portal.html ⇒ 200
# S3 平台 env 切换(备份 → 改两项 → 追加种子(加性,保留 alotbuy 作第二种子)→ 重启)
cp -a /etc/dshs.env /etc/dshs.env.bak-dom-$(date +%Y%m%d-%H%M%S)
sed -i 's/^DSHS_BASE_DOMAIN=.*/DSHS_BASE_DOMAIN=ai1net.com/' /etc/dshs.env
sed -i 's/^DSHS_COOKIE_DOMAIN=.*/DSHS_COOKIE_DOMAIN=.ai1net.com/' /etc/dshs.env
grep -q '^DSHS_OVERLAY_BOOTSTRAP_SEEDS=' /etc/dshs.env || \
echo 'DSHS_OVERLAY_BOOTSTRAP_SEEDS=https://ai1net.com/dshs-relay,https://alotbuy.com/dshs-relay' >> /etc/dshs.env
systemctl restart dshs
# 验收:实例地址变成 <user>.ai1net.com 且能登录;中继仍在线(探针 29 PASS / 0 FAIL / 0 SKIP)
# S4 旧名连带项(**必须同批**,否则留半破状态)
# S4a relay-direct.conf(alotbuy 版)的 Host 改写也必须指向新基底域:
# proxy_set_header Host alotbuy.com; → ai1net.com
# S4b dsh.alotbuy.com.conf(旧名 301)的 map 目标改指新域(⚠️ 只改两行,别碰 server_name/正则):
# default alotbuy.com; → ai1net.com;
# ~^(?<label>[^.]+)\.dsh\.alotbuy\.com$ $label.alotbuy.com; → $label.ai1net.com;
nginx -t && nginx -s reload
# S5 worker 侧种子(可选,加性):/etc/dshs-worker.env 加同名多值 SEEDS,再 restart dshs-worker
# ⚠️ 改前 `--scene all` 演练含观察窗口,预算 ≥8 min;⛔ 别套短超时
```
## 3. 回滚(任一步失败都能退回)
1. 删两份新 vhost ⇒ `nginx -t && nginx -s reload`(门户立即回到 alotbuy 版)。
2. `cp -a /etc/dshs.env.bak-dom-<ts> /etc/dshs.env` ⇒ `systemctl restart dshs`(回到 `alotbuy.com` / `.alotbuy.com`)。
3. S4 的两处 sed 反向改回(alotbuy 版 vhost 与旧名 301 都可原样恢复)。
4. 证书可留(不影响 alotbuy);`certbot delete --cert-name ai1net.com` 可彻底清掉。
## 4. 已知影响面(提前说明,非阻塞)
- **切换瞬间所有人需要在新域重登一次**:cookie 域由 `.alotbuy.com` 变 `.ai1net.com` ⇒ 旧 cookie 不再随请求发送(无法双域共存,一个 cookie 只能挂一个 Domain)。
- **实例地址由 `<user>.alotbuy.com` 变为 `<user>.ai1net.com`**:旧地址在 alotbuy 版 vhost 仍可用(两套 vhost 并存期间);若要彻底退役旧域,再补一条 `alotbuy.com → ai1net.com` 的 301(本轮**不做**,留待你确认退役时机)。
- `work.alotbuy.com`(Gitea · 154.40.35.3)**与本次无关**,不动。
- 中继引导种子改成「新域为主、旧域为备」⇒ 两域任一存活都不影响 worker 入网。