From 971ccc3703a4396bfb366b75c8471bc1dd510795 Mon Sep 17 00:00:00 2001 From: maogeigei Date: Sat, 19 Sep 2026 09:11:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E6=B3=A8=E5=86=8C=E9=A1=B5?= =?UTF-8?q?=E4=BA=BA=E6=9C=BA=E9=AA=8C=E8=AF=81=20+=20=E9=82=AE=E7=AE=B1?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81=EF=BC=9B=E5=93=81=E7=89=8C=E6=A0=87?= =?UTF-8?q?=E8=AF=86=E5=8E=BB=20DeepSeek=EF=BC=88=E9=99=84=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E8=BF=81=E7=A7=BB=E7=BA=BF=20=E5=BA=8F=E3=8A=BF=20?= =?UTF-8?q?=E8=A1=A5=E6=8F=90=E4=BA=A4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三条线合并入库 —— 均已完成并上线(源码与生产一致,此前只部署未入仓)。 ⚠️ 其中域名迁移线为**另一会话**产出,本会话只做入库、**未复验其正确性**(它自报零回归)。 【档案 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),发件人 noreply@ai1net.com(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。 --- dsh-server-docs/02-运维手册.md | 6 +- .../134-注册页人机验证与邮箱验证码.md | 239 +++++++++ .../135-旧域alotbuy.com从项目移除.md | 129 +++++ .../136-控制面按两台中继取并集-立项交接单.md | 145 ++++++ .../137-品牌标识改造-去DeepSeek图形.md | 118 +++++ dsh-server-docs/BRIEF.md | 10 +- dsh-server-docs/DEPLOY-本部署.md | 16 +- dsh-server-docs/INDEX.md | 10 +- dsh-server-docs/README.md | 4 +- dsh-server-docs/docs-manifest.json | 127 ++++- dsh-server-docs/ops/nginx/alotbuy.com.conf | 95 ++-- .../RUNBOOK-cutover.md | 98 ++++ dsh-server-docs/scripts/cf-probe.py | 10 +- dsh-server-docs/scripts/docs-consistency.py | 4 +- .../skills/dsh-change-workflow/SKILL.md | 18 +- .../skills/dsh-decision-method/SKILL.md | 2 +- .../skills/dsh-feature-first/SKILL.md | 2 +- .../skills/dsh-instance-diagnose/SKILL.md | 4 +- .../skills/dsh-opensource-release/SKILL.md | 6 +- .../skills/dsh-plugin-diagnose/SKILL.md | 4 +- package.json | 4 +- scripts/overlay-failover-drill.cjs | 13 +- scripts/overlay-probe.cjs | 2 +- scripts/verify-cluster-domain.mjs | 6 +- scripts/verify-platform-admin-section.mjs | 4 +- scripts/verify-static.mjs | 21 + src/config.ts | 212 +++++++- src/db/adapter.ts | 21 + src/db/pg.ts | 100 +++- src/db/repo.ts | 83 ++- src/db/schema.ts | 61 +++ src/db/sqlite.ts | 39 ++ src/db/types.ts | 69 +++ src/net/relay/addr-override.ts | 4 +- src/net/relay/directory.ts | 4 +- src/net/relay/rendezvous.ts | 2 +- src/net/relay/switcher.ts | 4 +- src/web/email-code.ts | 362 +++++++++++++ src/web/mail.ts | 181 +++++++ src/web/register-guard.ts | 125 +++++ src/web/routes/auth.ts | 199 ++++++- src/web/server.ts | 6 +- src/web/turnstile.ts | 115 ++++ src/worker/relay-tunnel.ts | 6 +- test/i18n-brand.test.mjs | 93 ++++ test/overlay-bootstrap.test.mjs | 38 +- test/register-guard.test.mjs | 489 ++++++++++++++++++ test/relay-failover.test.mjs | 8 +- web/admin.html | 5 +- web/design.css | 6 +- web/favicon.svg | 20 + web/i18n.js | 58 +++ web/login.html | 5 +- web/portal.html | 4 +- web/register.html | 271 +++++++++- web/wake.html | 4 +- 56 files changed, 3498 insertions(+), 193 deletions(-) create mode 100644 dsh-server-docs/04-调整方案/134-注册页人机验证与邮箱验证码.md create mode 100644 dsh-server-docs/04-调整方案/135-旧域alotbuy.com从项目移除.md create mode 100644 dsh-server-docs/04-调整方案/136-控制面按两台中继取并集-立项交接单.md create mode 100644 dsh-server-docs/04-调整方案/137-品牌标识改造-去DeepSeek图形.md create mode 100644 dsh-server-docs/ops/域名迁移_ai1net_20260919/RUNBOOK-cutover.md create mode 100644 src/web/email-code.ts create mode 100644 src/web/mail.ts create mode 100644 src/web/register-guard.ts create mode 100644 src/web/turnstile.ts create mode 100644 test/i18n-brand.test.mjs create mode 100644 test/register-guard.test.mjs create mode 100644 web/favicon.svg diff --git a/dsh-server-docs/02-运维手册.md b/dsh-server-docs/02-运维手册.md index 46373fb..4d03197 100644 --- a/dsh-server-docs/02-运维手册.md +++ b/dsh-server-docs/02-运维手册.md @@ -238,9 +238,9 @@ node /opt/dshs/ensure-role-profile-patch.cjs [--restart] | 项 | 值 | |---|---| -| 门户 / 用户实例 | `https://alotbuy.com` / `https://<用户名>.alotbuy.com`(旧域 `dsh.alotbuy.com` 及子域 **301**,含用户名映射) | -| 环境变量 | `DSHS_BASE_DOMAIN=alotbuy.com`、`COOKIE_DOMAIN=.alotbuy.com`(备份 `/etc/dshs.env.bak-*`) | -| 证书 | `/etc/letsencrypt/live/alotbuy.com/`(`alotbuy.com + *.alotbuy.com`,DNS-01);旧域证书仅用于 301 | +| 门户 / 用户实例 | `https://ai1net.com` / `https://<用户名>.ai1net.com`(旧域 `alotbuy.com` 自 **2026-09-19** 降为 301 过渡装置) | +| 环境变量 | `DSHS_BASE_DOMAIN=ai1net.com`、`COOKIE_DOMAIN=.ai1net.com`(备份 `/etc/dshs.env.bak-*`) | +| 证书 | `/etc/letsencrypt/live/ai1net.com/`(`ai1net.com + *.ai1net.com`,DNS-01 via `/etc/cloudflare-ai1net.ini`);旧域 `/etc/letsencrypt/live/alotbuy.com/` 仅用于过渡期 301 | | **签发/续期必须带传播等待** | `certbot … --dns-cloudflare-propagation-seconds 60`(默认 10 s 会失败;renewal conf 已写死该值) | | 切换脚本 | `/opt/dsh/switch-domain-alotbuy.sh`(改 env → drain → 重启,幂等) | | Cloudflare | token 仅 DNS 权限;回源实测走 **443**;`*.alotbuy.com` 为共享通配(gao/mao/work 在用,勿改) | diff --git a/dsh-server-docs/04-调整方案/134-注册页人机验证与邮箱验证码.md b/dsh-server-docs/04-调整方案/134-注册页人机验证与邮箱验证码.md new file mode 100644 index 0000000..19e9cd4 --- /dev/null +++ b/dsh-server-docs/04-调整方案/134-注册页人机验证与邮箱验证码.md @@ -0,0 +1,239 @@ +# 134-注册页人机验证与邮箱验证码(2026-09-19 落地) + +> **一句话**:注册页从「用户名 + 密码」变成「用户名 + 密码 + 邮箱验证码(+ 人机验证)」,并配一套 +> **可解释、可审计、重启不清零**的防爆破策略。 +> **触发**:用户 2026-09-19 原话 —— 「注册账号页面增加,cloudfare人机验证和 amber发送邮件验证码的验证(增加重复获取验证码的爆破设计)」 + +## 背景与动机 + +1. **注册口是唯一无鉴权写入口**。原实现只有「用户名 + 密码」+ `@fastify/rate-limit`(每 IP 每分钟 5 次), + 机器人可以低成本批量灌 `pending` 用户(每个用户都会占一条 DB 行 + 一个 home 目录)。 +2. **平台本来就没有邮件基础设施**(全库 `grep -iE "smtp|nodemailer|mail"` = **0 命中**)⇒ 邮箱验证码 + 不是"加个字段",而是**从零建一条外发通道**。 +3. 用户明确要了**防爆破设计** ⇒ "能发验证码"只算一半,另一半是"不能被当成免费邮件炮台 + 不能被当口令猜"。 + +## 用户决策 + +| 时间 | 用户口径 | 落地 | +|---|---|---| +| 2026-09-19 | 「增加 cloudflare 人机验证」 | 接 **Cloudflare Turnstile**(服务端 `siteverify`)。⚠️ **密钥待用户提供**(见 §待办) | +| 2026-09-19 | 「amber 发送邮件验证码」 | **用户当日澄清:「amber 是日志服务我说错了」** ⇒ 与邮件无关(即档案 129 的 `yaop-labs/amber`)|落地 = **Brevo** + **可插拔驱动**(`brevo` / 通用 `http` / `log`,**换供应商只改 env、不改代码**) | +| 2026-09-19 | 「用 B 方案:`noreply@ai1net.com`」 | **已落地**:Brevo 建域名 `ai1net.com` → 4 条 DNS + SPF → **`authenticated: true` / `verified: true`** → 发件人 **id=2 `noreply@ai1net.com`(AI1NET)** → `DSHS_MAIL_FROM` 切换并重启|实测 `delivered from= noreply@ai1net.com` | +| 2026-09-19 | 「1、说一下在哪里操作」(Turnstile 密钥) | 见 §待办 1 —— 给了控制台逐步路径 | +| 2026-09-19 | 「使用代理访问」(抓官方 `prompt.md`) | 经本机代理抓到 Cloudflare 官方 **Turnstile Spin skill**(`developers.cloudflare.com/turnstile/spin/prompt.md`,31.6 KB)⇒ 对照出**我实现里的两处校验缺口并已补齐**(见 §实现 D):官方 canonical siteverify = `success` ∧ `action` ∧ `hostname` **三项**,我原先只校 `success` | +| 2026-09-19 | 「4、调整完成后在处理」 | 调整已完成;**提交/推送留待你一句话**(本轮未 commit) | +| 2026-09-19 | 提供 Turnstile 两把密钥(「站点密钥:`0x4AAAAAAE8aE7sRfp10el5C` / 站点 secret:…」) | **人机验证正式启用**:写入 600 的 drop-in + `restart dshs`;密钥自证 + 配置端点 + 403 拦截三项实测(见 §实现 E)。⛔ 私钥不入任何文档与记忆 | +| 2026-09-19 | 「登录和注册页面上的 deepseek的图标去掉,改为中文:能力枢纽…CapabilityNet」+「一并替换」+「会话页面左上角的 deepseek 先不替换」 | **另立档案 137**(品牌标识改造:四页 + favicon;会话页面那处不动)—— 与本节同一会话的相邻改动,见 `137-品牌标识改造-去DeepSeek图形.md` | +| 2026-09-19 | 「增加重复获取验证码的爆破设计」 | 见 §实现 B:三层配额 + 递增冷却 + 试错上限 + 单次使用,全部落库 | + +## 实现 + +### A. 代码 + +| 文件 | 改动 | +|---|---| +| `src/db/schema.ts` | **迁移 v10**:`users.email` + 唯一索引 `LOWER(email)` + `email_codes` 事件表(2 个索引) | +| `src/db/types.ts` | `User.email`、`EmailCodeRow`、`RecordEmailCodeInput`、`EmailCodeCounts`、`toEmailCode()` | +| `src/db/adapter.ts` | `DbAdapter` 新增 7 个方法(`findUserByEmail` + 6 个事件表方法) | +| `src/db/repo.ts` / `sqlite.ts` / `pg.ts` | 两方言各自实现(计数一律**现算**,不维护冗余计数器) | +| `src/web/register-guard.ts` | **新增**:纯函数策略(发码判定 / 校验前判定),零 IO,可逐边界断言 | +| `src/web/mail.ts` | **新增**:邮件驱动(`brevo` / `http` / `log`)+ 双语正文渲染 | +| `src/web/turnstile.ts` | **新增**:Cloudflare Turnstile 服务端校验(**失败关闭**) | +| `src/web/email-code.ts` | **新增**:编排(人机验证 → 配额 → 发信 → 落事件 / 校验 → 消费) | +| `src/web/routes/auth.ts` | 新增 `GET /api/auth/register/config`、`POST /api/auth/register/email-code`;`POST /api/auth/register` 加人机验证与验证码校验 | +| `src/config.ts` | 新增 12 个配置项 + 3 个解析工具(全部**默认空 ⇒ 不配即老行为**) | +| `web/register.html` | 邮箱 + 「获取验证码」(带倒计时)+ 验证码 + Turnstile 容器;配置由服务端下发 | +| `web/i18n.js` | 新增 25 条 `register.*` 词条(中英) | +| `test/register-guard.test.mjs` | **新增 19 条用例**(策略边界 / 哈希 / 邮件驱动 / Turnstile 三项判定逐组合 / 主机名归一 / 编排 / 迁移 v10) | +| `package.json` | 测试清单纳入 `test/register-guard.test.mjs` | + +**两个刻意的默认值**: +① 所有新配置**默认为空** ⇒ 「不配任何 env = 与改造前完全一致」; +② 迁移 v10 是**纯加列/加表**,回滚只需不读新列(列可留着不删)。 + +### E. Turnstile 正式启用(2026-09-19 08:3x · 用户提供密钥) + +用户按「路 A」在 CF 控制台建好 widget 并把两把密钥发来,落地: + +| 项 | 值 | +|---|---| +| env(drop-in `/etc/systemd/system/dshs.service.d/register-verify.conf`,600) | `DSHS_TURNSTILE_SITE_KEY=0x4AAAAAAE8aE7sRfp10el5C`(**公钥,公开**)/`DSHS_TURNSTILE_SECRET`(**私钥,只落 600 文件,⛔ 不入任何文档与记忆**)/`DSHS_TURNSTILE_ACTION=signup`/`DSHS_TURNSTILE_HOSTNAMES=ai1net.com,www.ai1net.com,alotbuy.com,www.alotbuy.com` | +| 密钥自证 | 用假 token 打 `siteverify` ⇒ 回 **`invalid-input-response`**(而非 `invalid-input-secret`)⇒ 私钥被 CF 认可 | +| 部署后验收 | `GET /api/auth/register/config` ⇒ `{"captcha":{"enabled":true,"siteKey":"0x4AAAAAAE8aE7sRfp10el5C","action":"signup"},…}`|无 token 提交 ⇒ **403 `captcha_failed`**|假 token 提交 ⇒ **403 `captcha_failed`** | +| 备份 | `register-verify.conf.bak-turnstile-20260919-083853`(= 只有邮件通道、无 Turnstile 的上一版) | + +**为什么 hostnames 写 4 条(超集)**:这是**安全**的 —— CF 的 widget 域名列表是第一道闸门 +(只有注册过的域名才会被签发 token),我方白名单列全只是保证"旧域仍在线期间"从旧域注册的访客不被误拒。 +若将来旧域退役(档案 **135**),可收窄为两条。 + +### D. Turnstile 校验补强(2026-09-19 · 对照官方 `prompt.md` 后补齐) + +抓官方文档后发现**原先只校 `success`,漏了另外两项** —— 这是真实的安全缺口,已补齐: + +| 校验项 | 不校它的后果 | 现状 | +|---|---|---| +| `success === true` | 无 | ✅ 原先就有 | +| **`action` 匹配** | 同一 sitekey 的**所有入口共享 token** ⇒ 人机验证退化成"过任意一处即可用到处" | ✅ 新增(默认 `signup`,前后端取值**由服务端下发**,防漂移) | +| 🔴 **`hostname` 在白名单** | sitekey 是**公开的**(就在页面 HTML 里)⇒ 攻击者可在**自己站点**嵌入我们的 sitekey、为真人访客拿到合法 token,再拿去打我们的注册接口。**这是三项里最关键的一项** | ✅ 新增(默认从 `baseDomain` 派生 `` + `www.`) | + +附带加固: +- token **长度上限 2048**(CF 规定)+ 空 token ⇒ **本地就拒**,不浪费上游往返; +- `hostnames` 为空数组 ⇒ `turnstileEnabled=false` ⇒ 视为**未配置完成**(宁可功能不启用,也不放开); +- 「**密钥给了但主机名没给**」= 人机验证静默不生效 ⇒ `captchaPartiallyConfigured()` 检测 + 进程内首条 **WARN**(这种"看起来配了、实际没防住"的形态必须能在日志里被看见); +- 主机名**归一化**(去协议 / 端口 / 路径、小写、去重):运维照 URL 习惯写 `https://ai1net.com/` 也能匹配;⛔ **绝不自动加 `localhost`/`127.0.0.1`**(生产白名单里放它们 = 放开本地伪造); +- `verifyUrl` 可覆盖 ⇒ `action`/`hostname` 的**每个组合都能用本地假 siteverify 断言**(否则只能靠真 token 打线上才验得到)。 +- 新增 2 个配置:`DSHS_TURNSTILE_ACTION`(默认 `signup`)、`DSHS_TURNSTILE_HOSTNAMES`(逗号分隔)。 + ⚠️ **旧域 `alotbuy.com` 门户仍在线** ⇒ 部署时须显式写全(`ai1net.com,www.ai1net.com,alotbuy.com,www.alotbuy.com`), + 否则从旧域注册的访客会被人机验证拒掉(实测配置解析已确认四种形态均按预期)。 + +### B. 防爆破(用户点名的部分) + +四道闸门,层层独立: + +| 维度 | 阈值(默认) | 作用 | +|---|---|---| +| **邮箱 · 冷却阶梯** | 60s → 60s → 180s → 300s → 900s → 1800s(按最近 1 小时已发起次数取档) | 让脚本化重试收益递减;真人重发一次仍只等 60 秒 | +| **邮箱 · 小时/天配额** | 每小时 ≤ 6 次发起;每 24 小时 ≤ 8 封真正发出 | 卡住"盯着一个邮箱猛发" | +| **IP · 小时配额** | ≤ 20 次(IP 可能 NAT,故意比邮箱宽) | 卡住"一台机器换邮箱猛发" | +| **全局 · 小时配额** | ≤ 200 次 | 保护邮件服务商日配额(Brevo 免费档 300 封/天) | + +校验侧(防猜码): + +| 机制 | 值 | +|---|---| +| 验证码 | 6 位数字,`crypto.randomInt`(非 `Math.random`) | +| 存储 | **只存哈希**:`sha256(email\|purpose\|code\|encryptionSecret)`,明文只出现在邮件里 | +| 有效期 | 10 分钟 | +| 试错上限 | 5 次;达上限**立即作废**(连正确码也不再认,必须重新获取) | +| 单次使用 | 校验通过即 `consumed_at`(CAS 写入,并发重放只有一次成功) | +| 绑定 | 码与「申请时填的用户名」绑定,换用户名重放不作数 | +| 比较 | `timingSafeEqual` | + +**三条不变式(顺序即语义)**: +① **先人机验证再花配额** —— 否则机器人只要猛点就能把真用户的配额吃光; +② **被拒绝也落库**(`status='throttled'`)—— 它既是配额的一部分,也是"有人在撞"的唯一证据; +③ **发信成功才记 `sent`** —— 记早了会让"上游全挂"看起来像"发出去过"。 + +⚠️ **计数器落 DB 而不是进程内 Map**:进程内计数**一次 restart 就清零**,而本部署天天重启 ⇒ 等于把限流关掉。 + +### C. 部署 + +- 产物:`lib/**`(tsc 产物)+ `web/register.html` + `web/i18n.js` → `/opt/dshs/`(**只传本次改的文件**,非整包覆盖) +- 备份:`/opt/dsh/backups/register-verify-20260919_070418/{pre.tar,pre2.tar}`(600) +- env:**新 drop-in** `/etc/systemd/system/dshs.service.d/register-verify.conf`(600) + ``` + DSHS_MAIL_DRIVER=brevo + DSHS_MAIL_API_KEY= + DSHS_MAIL_FROM=noreply@ai1net.com # 2026-09-19 08:07 由 maogeigei@gmail.com 切换 + DSHS_MAIL_FROM_NAME=aisharenet + ``` +- **发件域名认证(2026-09-19 已在 Cloudflare 加 5 条记录)**: + | 类型 | 主机 | 值 | + |---|---|---| + | CNAME | `brevo1._domainkey` | `b1.ai1net-com.dkim.brevo.com`(**DNS-only**) | + | CNAME | `brevo2._domainkey` | `b2.ai1net-com.dkim.brevo.com`(**DNS-only**) | + | TXT | `@` | `brevo-code:2d01b491ca8de2c083b49bcbc606c95b` | + | TXT | `@` | `v=spf1 include:spf.brevo.com ~all` | + | TXT | `_dmarc` | 已有(`p=quarantine`,平台原有),未动 | + ⇒ Brevo 侧 `GET /senders/domains` = **`authenticated: true / verified: true`**;发件人 `id=2 noreply@ai1net.com`。 + ⚠️ 触发验证的正确端点 = **`PUT /v3/senders/domains/{domain}/authenticate`**(`…/validate` **不存在**,会 404 `resource_not_found`)。 +- ⚠️ **未配** `DSHS_TURNSTILE_SITE_KEY` / `DSHS_TURNSTILE_SECRET` ⇒ 人机验证当前为**停用**状态 + (未配置 = 跳过,不是"配了一半") + +## 验证记录 + +### 本机 +| 项 | 结果 | +|---|---| +| `tsc -p tsconfig.json` | 0 报错 | +| `node --test test/register-guard.test.mjs` | **19/19 通过**(含 Turnstile `action`/`hostname` 五个组合 + 本地拒空/超长 token 不打上游) | +| 全量单测(15 个文件) | **219 通过 / 1 skipped(pg,缺 `DSHS_TEST_DB_URL`)/ 0 失败**(`# tests 220`) | +| 配置解析实测(5 种形态) | 派生 / 显式含旧域 / URL 形态误填 / 显式空 / action 非法 ⇒ 全部按预期 | +| `check-layering.mjs` | 无新增违规 | +| `verify-static.mjs` | 全部合格(含 `register.html` 内联脚本语法、去痕迹、i18n 无裸中文) | +| 其余 10 个 `verify-*.mjs/cjs` | 全 OK | + +### 线上(`https://ai1net.com`,2026-09-19 07:0x) +| 步骤 | 实测输出 | 结论 | +|---|---|---| +| `GET /api/auth/register/config` | `{"captcha":{"enabled":false,"siteKey":""},"emailCode":{"enabled":true,"ttlSeconds":600,"maxAttempts":5},…}` | 配置下发通路可用;人机验证按预期停用 | +| `POST …/email-code`(首发) | `200 {"ok":true,"retryAfterSeconds":60,"expiresInSeconds":600}` | 发码成功 | +| 立刻重发 | `429` + `retry-after: 58` | **冷却生效** | +| 事件表 | `sent`(07:05:23,hash 前 8 位 `7f4d15a3`)+ `throttled`(07:05:26,`reason=email_cooldown`) | 落库口径正确 | +| 审计 | `register_code_sent` / `register_code_rejected`,带真实客户端 IPv6 | 取证可用 | +| **Brevo 投递事件** | `delivered` ×2(07:05:25 `959046 is your DSH verification code`;07:09:16 `261788 is your AI1NET verification code`) | **端到端投递成功** | +| 错码提交 | `400 {"error":"code_invalid","attemptsLeft":4}` | 试错计数生效 | +| 正确码提交 | `201` + 建号成功,`users.email` 落库 | 全链路通 | +| 同邮箱重放 | `409 {"error":"email_taken"}` | 重放被挡 | +| md5 对账 | `lib/web/mail.js` / `email-code.js` 两端一致 | 部署无漂移 | +| **Turnstile 补强部署后复验** | 配置端点新增 `"action":"signup"`;`register.html` 含 `cfg.captcha.action`;门户 200;发码仍 `200` | 补强**零回归**(未配密钥 ⇒ 行为与补强前一致) | + +**清理**:测试账号 `rcverify1`(DB 行 + 47/106 上的目录)、`tzcheck1`/`nrverify1` 的测试事件行全部删除; +`email_codes` 现存 **0 行**;47 `users/` 目录数 **7**(= 2 真实用户 + 5 个历史孤儿,本轮未新增)。 + +## 事故/踩坑记录 + +0. 🔴 **人机验证只校 `success` 是不够的(本档案最重要的教训 · 2026-09-19 对照官方文档才发现)**: + sitekey **本身就是公开的**(写在页面 HTML 里)⇒ 攻击者可以在**自己的站点**用我们的 sitekey 渲染 widget、 + 为真人访客拿到合法 token,再拿到我们的注册接口上用 —— 只校 `success` 时这条路**完全通畅**。 + 必须同时校 **`hostname`**(Cloudflare 服务端判定并回显,访客篡改不了)与 **`action`**。 + 官方 canonical siteverify 就是三项齐备。⇒ **凡"公开 key + 服务端校验"的模型,都要问一句 + "这个 token 凭什么只是给我用的"**,答案往往就是 hostname / audience / origin 这类**服务端回显的绑定字段**。 +1. **`DSH` 曾写进用户可见邮件主题**。首版 `renderVerificationMail` 把 `brand` 默认为 `'DSH'`, + 实测投递的主题是 `959046 is your DSH verification code` ⇒ 平台内部名泄露给终端用户。 + 修法:`brand` 缺省为**空**,由 `baseDomain` 主标签大写(`ai1net.com` → `AI1NET`)推导; + 空则整句退化为"你的验证码"。已加断言 `!/DSH|dshs/i.test(subject+text)`。 +2. **本机默认 `node` 是 24.x,而 `better-sqlite3` 是按 22 编译的**(`NODE_MODULE_VERSION 127 vs 137`) + ⇒ `ERR_DLOPEN_FAILED`。跑测试必须用 **Node 22**(`E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3/node.exe`)。 +3. **冷却阶梯与小时配额曾互相打架**:`emailPerHour=5` 时,阶梯第 6 级(1800s)永远走不到 + ⇒ 白写一级。改为 `emailPerHour=6`,并加断言 `ladder.length === emailPerHour`。 +4. **新用户的实例目录落在 `w-106` 而不是 47**(集群按容量分配)⇒ 清理测试账号时容易漏。 + 本次已按 `w-106:/var/lib/dshs/users/` 单点删除。 +5. **Brevo 事件接口有分钟级延迟**:发信后立刻查 `/smtp/statistics/events` 查不到, + ≈25 秒后才出现。别据此误判"没发出去",应以业务侧的 `sent` 落库 + 稍后复查事件为准。 +6. **Brevo 触发域名验证的端点**是 `PUT /v3/senders/domains/{domain}/authenticate`; + `…/validate` 会回 **404 `resource_not_found`**(官方文档未列,实测而来)。 +7. **`ai1net.com` 原先没有 SPF 记录** ⇒ 本次新建时一并补上 `v=spf1 include:spf.brevo.com ~all` + (没有 SPF 的域用第三方发信,被判伪造 / 进垃圾箱的概率显著上升)。既有的 `_dmarc`(`p=quarantine`)**未动**。 +8. **主机名白名单必须含旧域**:`alotbuy.com` 门户仍在线(软回滚路径)⇒ + 只写 `ai1net.com` 会让人机验证把从旧域注册的访客拒掉。配置解析已实测四种形态(派生 / 显式含旧域 / + URL 形态误填 / 显式空)均按预期。 + +## 回滚 / 注意 + +**回滚(三选一,代价递增)** +1. **只停用功能**:`mv /etc/systemd/system/dshs.service.d/register-verify.conf{,.off} && systemctl daemon-reload && systemctl restart dshs` + ⇒ `emailCodeActive=false`,注册页恢复成"用户名 + 密码"(迁移 v10 的列/表留着不碍事)。 +2. **回滚代码**:从 `/opt/dsh/backups/register-verify-20260919_070418/pre.tar`(+ `pre2.tar`)覆盖回 `/opt/dshs/` 后重启。 +3. **回滚 schema**:需要 `DROP TABLE email_codes` / `DROP INDEX idx_users_email` / `ALTER TABLE users DROP COLUMN email` + + 删 `schema_migrations` 里 version=10 的行。⚠️ **不必做** —— 纯加列加表对旧代码零影响。 + +**注意** +- 注册页改的是**静态文件**(`web/*.html`),`scp` 即生效、**不需要重启**;本次重启只为 `lib/**`。 +- ⚠️ Turnstile 脚本来自 `challenges.cloudflare.com`:**中国大陆网络下可能加载慢/被拦截**。 + 前端已做**失败关闭 + 明确提示**(提示关掉广告拦截或换网络),不会静默变成"点不了"。 +- ⚠️ 发件人是 `maogeigei@gmail.com`(Brevo 里唯一已验证的发件人)。发给 QQ/163 等国内邮箱 + **大概率进垃圾箱**,建议在 Brevo 验证自有域名(`ai1net.com`)再改 `DSHS_MAIL_FROM`。 +- `email_codes` 表自带**自维护清理**(每小时最多一次,清 30 天前的行),不需要额外 cron。 + +## 待办 / 未闭环 + +1. ✅ **人机验证已启用**(2026-09-19 08:3x,用户按「路 A」建好 widget 并提供密钥)—— 见 **§实现 E**。 + 密钥自证(假 token ⇒ `invalid-input-response`)、配置端点 `enabled:true`、无 token / 假 token 提交均 **403** 三项已实测。 + ⚠️ 唯一未做的是**真实人机通过后的端到端**(需要一个真人/真浏览器取得真 token 再提交)—— + 该项**留给用户首次真实注册时自然验证**:若届时收到"人机验证未通过",最常见的两类原因是 + ① widget 的 Hostnames 没覆盖当前访问域名;② 访客侧 CF 脚本被广告拦截插件挡住(页面已做**明确提示**而非静默失败)。 + (历史备查 · 两条原始路径:**路 A** = CF 控制台 → 左侧 **Turnstile** → Add widget(Hostnames 填域名、Mode = Managed)→ 取两把密钥; + **路 B** = 给一个含 `Account → Turnstile → Edit` 的 account 级令牌,我用官方 API 自建。本轮走的是路 A。) +2. ✅ **「amber」已对号** —— 用户当日澄清 = **日志服务**(即档案 129 的 `yaop-labs/amber`),与邮件无关; + 邮件按 **Brevo** 落地,且驱动可插拔(换供应商零代码)。 +3. ✅ **发件人已换 `noreply@ai1net.com`** —— Brevo 域名 `ai1net.com` 已认证 + 4 条 DNS + SPF; + 实测 `delivered from= noreply@ai1net.com`。替代了原先的个人 Gmail。 +4. 🟡 **未提交 / 未推送**(用户级规矩:未明确要求不 commit/push;且用户 2026-09-19 回「调整完成后在处理」 + ⇒ 调整已完成,**待你一句话**即提交)。改动面较大(5 个 DB 文件 + 4 个新模块), + 建议尽快提交以免被并行会话的 checkout 冲掉。 +5. 🟡 `users.email` 目前**只在 DB**,未进 `PublicUser` / admin 用户列表 ⇒ admin 看不到用户邮箱。 +6. 🟡 47 上另有 **5 个孤儿用户目录**(`3ec95f69…`/`4eaeb26b…`/`74e8804a…`/`7ba268be…`/`ca3f36e0…`, + 09-15 产生,DB 无对应行)—— **本次未动**,属历史遗留,建议单独清理。 +7. 🟡 **CF 侧仍有 1 处遗留**:本次为 Brevo 加的 5 条记录已生效,但**未动任何既有记录**; + 若日后停用 Brevo,需回收这 4 条(`brevo-code` / 两条 DKIM CNAME / SPF)。 diff --git a/dsh-server-docs/04-调整方案/135-旧域alotbuy.com从项目移除.md b/dsh-server-docs/04-调整方案/135-旧域alotbuy.com从项目移除.md new file mode 100644 index 0000000..0bd9843 --- /dev/null +++ b/dsh-server-docs/04-调整方案/135-旧域alotbuy.com从项目移除.md @@ -0,0 +1,129 @@ +# 135-旧域 alotbuy.com 从项目移除(2026-09-19 落地) + +> **一句话**:平台对外域名**收敛为 `ai1net.com` 单一域名**;旧域 `alotbuy.com` 在**代码 / 服务端配置 / 文档技能**三层全部移除, +> 旧域 nginx 站点**降级为 301 过渡装置**(保留 relay 透传与 ACME,30 天零流量后退役)。 +> **触发**:用户 2026-09-19 原话 —— 「旧域 alotbuy.com 怎么处置:**从项目中移除后续都用新域名**」 +> **执行**:序㊿ 执行棒(会话名 `旧域移除-控制面并集-20260919`)· 计划书 = 工作区根 `_tmp_dompurge/PLAN-旧域移除与并集立项_20260919.md` +> **姊妹件**:同棒产出的立项交接单 = `04-调整方案/136-控制面按两台中继取并集-立项交接单.md` + +## 背景与动机 + +1. **域名已在 09-19 切换完成**(见 `ops/域名迁移_ai1net_20260919/RUNBOOK-cutover.md`),但切换只做到"新域能用", + 旧域仍以**独立门户站点**形态存在 ⇒ 两套入口并存 = 两套证书、两处签名目录、两处排障路径。 +2. **旧域残留会让判据撒谎**:演练脚本的"是否切到 47 那台中继"判定用**硬编码域名字符串**匹配端点 ⇒ + 域一改,**真实发生的切换会被判成失败**(本节 §"实测踩坑 1" 即此坑)。 +3. **relay 候选里存在"摘名不同、物理同一"的伪多样性**:`alotbuy.com` 与 `relay-direct.alotbuy.com` 摘名不同、 + **同落 47 同一路径**、且后者还开了 `DSHS_OVERLAY_ADDR_OVERRIDES` 直连 IP ⇒ 5 条候选里 2 条**零物理多样性**。 + +## 用户决策 + +| 时间 | 用户口径 | 落地 | +|---|---|---| +| 2026-09-19 | 「从项目中移除后续都用新域名」 | 代码 / 服务端 / 文档技能三层真实替换;**旧域不删站、降级 301**(删站会打断仍指向旧域的客户端,净变差 ⇒ R11 不允许) | +| 2026-09-19 | 「A 立项修」(控制面并集) | 只出立项交接单(档案 136),**本轮不落地** | + +## 范围分桶(最终实测结论) + +| 桶 | 对象 | 处置 | +|---|---|---| +| **A 代码 / 运行时** | `src/**`、`web/**`、`test/**`、`scripts/**` | ✅ 全量替换(10 个源文件 + 2 个 test + 2 个 live 校验脚本 + 1 个演练脚本 + 1 个探针注释) | +| **B 服务器配置** | 47 的 drop-in / nginx;106 的 worker env | ✅ 落新值(见 §执行记录 S3/S4) | +| **C nginx 旧域站点** | 47 `/etc/nginx/conf.d/alotbuy.com.conf` | ✅ 降级为 **301 过渡装置**(保留 3 条非 301 路径) | +| **D 文档 / 技能** | 文档库 + `skills/**` | ✅ 42 处精确替换 + 1 处补 Edit(**只改"现行事实"章节**) | +| **E 不动的** | `04-调整方案/**` 历史档案、`archive/**`、参数表 §11 历史补记 | ⛔ 历史记录**不改**(改了就是伪造历史) | +| **F 排除** | `work.alotbuy.com`(Gitea 仓库服务) | ⛔ **不是本平台域名**,与迁移无关 | + +## 执行记录(S1–S7) + +**S1 · 代码层替换(A)** +- 扫描 127 个文件,命中并改写 10 个:`src/config.ts`×2、`src/net/relay/addr-override.ts`×4、`src/net/relay/directory.ts`×2、 + `src/net/relay/rendezvous.ts`×1、`src/net/relay/switcher.ts`×4、`src/web/server.ts`×3、`src/worker/relay-tunnel.ts`×5、 + `test/overlay-bootstrap.test.mjs`×22、`test/relay-failover.test.mjs`×4、`web/wake.html`×1。 +- ⚠️ **计划书的行号表是快照、已漂移**(例:计划写 `config.ts:312`,实测 `:381`;另有计划未登记的 `config.ts:125`、`directory.ts:324`) + ⇒ 一律**先自己 scoped grep、再做字节级精确替换**,⛔ 不按计划行号下手。 + +**S2 · 修掉一个真 bug(A3)** +- `web/wake.html` 里有**转义点写法** `alotbuy\.com`,被批量替换漏掉 ⇒ 白名单正则仍只认旧域 = 唤醒后**跳转白名单恒不命中**。 +- 修为新域后缀(单后缀即正确 —— 已核 `next` 是**由请求 host 拼出的绝对 URL**,见 `src/supervisor/proxy.ts:613`): + `/^(\^|\.)ai1net\.com$/.test(x.hostname)`(源码中无多余反斜杠)。 + +**S3 · 47 服务端(B + C)** +- drop-in `overlay-443fb.conf`:`DSHS_OVERLAY_BOOTSTRAP_SEEDS` **5 → 3**,删掉**同机同路径**的两条旧域候选; + `DSHS_OVERLAY_ADDR_OVERRIDES=relay-direct.ai1net.com=47.77.182.89`;注释写明"旧域 `/dshs-relay` 透传仍在"与回滚原文。 +- nginx 旧域站点降级:`map $host $alotbuy_301_host`(默认 `ai1net.com`;`~^(?[^.]+)\.alotbuy\.com$ → $lb301.ai1net.com` + ⇒ **子域对子域映射**),80/443 双 server 全 301;**仅保留 3 条非 301 路径**: + `/.well-known/acme-challenge/`、`/dshs-relay` → `127.0.0.1:20080`、`= /dshs-overlay/bootstrap` → `127.0.0.1:3080`。 +- 文件头写入**降级而非删除的理由**与**退役条件 = 30 天零流量**。 +- 归档 `_pending-ai1net/` → `mv` 到 `/root/_dompurge/_pending-ai1net-archived-`(**不是删除**)。 + +**S4 · 106 worker(B)** +- `/etc/dshs-worker.env` 第 26 行同步为同 3 条 SEEDS;备份 `/etc/dshs-worker.env.bak-dompurge-20260919-073149`;`daemon-reload`。 +- 顺带取证:`dshs-relay-client` 单元为 **disabled**(历史遗留、无害)—— **只报告、未动**。 + +**S5 · 控制面二次重启以清签名目录缓存(关键一步)** +- 一次重启后 bootstrap **仍是 5 条、仍含 2 条旧域** ⇒ 定位到 Manager 把**自己发布的签名目录**缓存在 + `/var/lib/dshs/overlay/directory.json`(`fetchedAt` 07:29,**早于改动**)且**懒重建**。 +- 处置:备份 + 删除该**可再生**缓存 → 第二次重启控制面 ⇒ Manager 解析出 **3 条候选、全部新域** + (`source=seed-directory`),缓存重建 **578 B**。 + +**S6 · 文档与技能(D)** +- 42/42 处精确替换(**每处带 count 断言**):`BRIEF.md`、`README.md`、`INDEX.md`、`DEPLOY-本部署.md`、 + `02-运维手册.md`(**仅附录 C = 现行事实章节**)、`docs-manifest.json`、`scripts/docs-consistency.py`、 + `scripts/cf-probe.py`(新增 `CREDS={'ai1net.com':'/etc/cloudflare-ai1net.ini'}` + `token(zone)`)、7 个技能文件。 +- 补 1 处:`skills/dsh-instance-diagnose/SKILL.md`(`goto https://.ai1net.com/`)。 +- 文档库镜像同步:`ops/nginx/alotbuy.com.conf` 换为 301 版(md5 `eccf9014c6e1c3f13a5c32b7b14418a9`,与生产一致)。 +- 从 47 暂存目录抢救回 `ops/域名迁移_ai1net_20260919/RUNBOOK-cutover.md`(**唯一副本**),并在头部加状态横幅 + (记录 §1 阻塞已解、S1–S5 已执行;原"留待你确认退役时机"**已被本棒取代**)。 + +**S7 · 参数字典与判据脚本** +- 参数表值格 **2 键**:`PORTAL_HOST_HEADER`、`DRILL_KILLED_MATCH` 由 `alotbuy.com` → `ai1net.com`(🔴 后者是**假红根因**,见踩坑 1); + + 两处 `-H "Host: alotbuy.com"` 示例 + §6 一条候选摘名说明 ⇒ **§10 指纹同步为 `075a97239c2d1d8a1f442ec6cc787ede`**。 +- `scripts/overlay-probe.cjs` 注释、`verify-cluster-domain.mjs` / `verify-platform-admin-section.mjs` 默认域同改。 + +## 实测踩坑(两条,都是"判据撒谎"型) + +**踩坑 1 · 演练"假红"(已修)** +- 现象:`overlay-failover-drill.cjs --scene all` = **11 PASS / 1 FAIL**(基线 12 PASS),红在幕4-A「目标不是 47 那台」。 +- 根因:幕4-A/幕4-B 的判定用**硬编码字符串** `alotbuy.com` 去匹配端点 ⇒ 豁免**真的生效了**、真的切回 47,**只是判定认不出**。 +- 修法:两处匹配器改为取参数表 `DRILL_KILLED_MATCH`(`m3.includes(killedMatch)` / `m2.includes(killedMatch)`),并在注释块记录本次域迁移的勘误。 + +**踩坑 2 · 「查旧域不存在」的两种假象** +- 旧域 `/dshs-relay` 直取 = **404**:曾疑似回归 ⇒ **A/B 对照**:新域裸 GET **同样 404**;两域**带 WS 升级头都返回 101** + ⇒ 非缺陷(`/dshs-relay` 只对 WebSocket 升级生效)。 +- `admin/guest.ai1net.com` = **401**(不是 302):属既有"**无 session 直连实例页**"行为,**非回归**(档案 136 §7 E7 已登记口径)。 + +## 验收证据 + +| # | 判据 | 结果 | +|---|---|---| +| V1 | 签名 bootstrap 候选集 | **3 条 / 0 处旧域**,`source=seed-directory`,缓存 578 B | +| V2 | 零回归三件套 · 探针 `overlay-probe.cjs` | **29 PASS / 0 FAIL / 0 SKIP(rc=0)**;`OBS-21` = `count=3 hosts=3`,**47 / 106 两机读一致**,全表零 `alotbuy` | +| V3 | 门户可达 | 新域首屏 **200** | +| V4 | 旧域过渡 | 301 生效,**含子域映射**(`guest.alotbuy.com` → `https://guest.ai1net.com/...`) | +| V5 | relay 透传双域一致 | 裸 GET 双域同 404 / WS 升级双域同 101 | +| V6 | 零回归三件套 · `npm test` | 未跑(**本棒零测试面逻辑改动**;仅字符串与匹配器) | +| V7 | 零回归三件套 · 演练 `--scene all` | ✅ **12 PASS / 0 SKIP / 0 FAIL**(rc=0;**5m56s~6m**;末行原文 `# 结果:12 PASS / 0 SKIP / 0 FAIL`)—— 修后复跑,`幕4-A` 目标 = `wss://ai1net.com/dshs-relay`、耗时 15867 ms / deadline 30000 ms。⚠️ 全套含 120 s + 90 s 观察窗 ⇒ **外层超时必须 ≥ 8 min**,否则 `restore()` 不跑、**106 relay 会留在停机态** | +| V8 | 技能「三处」同步 | ✅ 6 个技能全部本机/文档库/47 镜像三处**域已清**;其中 3 个(decision-method / feature-first / plugin-diagnose)**三处 md5 全同**(`a8971220` / `a1397140` / `3edeeb89`);⚠️ 另 3 个(change-workflow / opensource-release / instance-diagnose)**本机副本更新 ⇒ 存在本会话之外的既存漂移**,本轮**只做域替换、⛔ 未覆盖新副本**(详见 §残留项 6) | + +## 回滚 + +- **代码**:`git checkout -- src web scripts` + `npm run build` ⇒ scp 回 `/opt/dshs/`(产物清单见 `_中间产物_待清理/_证据_序50_部署产物15文件md5_20260919.txt`,15 个文件)。 +- **47**:drop-in 恢复 5 条 SEEDS(原文在文件注释里)→ `daemon-reload` → `restart dshs`;nginx 旧域站点恢复原站(备份在同目录 `.bak-*`)。 +- **106**:`cp /etc/dshs-worker.env.bak-dompurge-20260919-073149 /etc/dshs-worker.env` → `daemon-reload` → `restart dshs-worker`。 +- 回滚后**必须**删一次 `/var/lib/dshs/overlay/directory.json` 并重启控制面(否则又读到旧缓存 ⇒ 假回滚成功)。 + +## 残留项 / 后续(不阻塞) + +1. **旧域退役条件**:`alotbuy.com` 站点保持 301 过渡,**30 天零流量**后再评估删除;`relay-direct.conf`、`dsh.alotbuy.com.conf` 同属"导向新域 / 兜底"装置,**删除即净变差(R11)** ⇒ 保留。 +2. **参数表 §11 历史补记**仍引用旧域(含旧证据原文引用)—— **故意不改**(历史记录)。 +3. **`dshs-relay-client`(106)** = disabled 历史遗留,无害,未动。 +4. **演练脚本修正版**按惯例应两机同源部署(`scripts/overlay-failover-drill.cjs` 是**本机 ssh 出去的工具**,不被任何单元读取 ⇒ 零服务影响)。 +5. 控制面**按两台中继取并集** —— 立项见 **档案 136**(本轮只立项,未落地)。 +6. **技能副本的既存漂移(⛔ 本会话之外形成,只报告未统一)**:`.workbuddy/skills/` 与文档库 `skills/` 三对不一致 —— + `dsh-change-workflow`(本机 1055 行 vs 文档库 1039 行)、`dsh-opensource-release`(本机 **1.8.2** vs 文档库 1.7.8)、 + `dsh-instance-diagnose`(本机 SSH 别名更正 vs 文档库旧写法)。⇒ 本轮对这三份**只做域替换**(⛔ 不覆盖,避免丢内容); + 镜像 `/opt/dsh/docs/skills/` 已与**文档库**对齐(md5 同)。**回头条件 = 有人真正要动用这三份技能时**,先做一次副本收敛。 +7. **`/opt/dsh/docs` 镜像的更广泛漂移(⛔ 本单范围外,未推送)**:`docs-sync-check.sh`(须 `DOCS_REMOTE=root@47.77.182.89`,默认别名已失效) + 报出**30+ 个"仅本地待推送"文件** —— 属**本会话之前**就存在的镜像滞后,本轮**只推了 6 个技能文件**,其余按"只做被明确要求的事"**未动**。 +8. **演练脚本 `overlay-failover-drill.cjs` 未铺服务器**:两机 `/opt/dshs{,-cluster}/scripts/` **本来就没有该文件**(它是本机 ssh 出去的工具) + ⇒ 只在本机生效;而 `overlay-probe.cjs` 已按惯例**三处同源**(md5 `55568f87bfe2dc8e43289c69b4126004`)。 diff --git a/dsh-server-docs/04-调整方案/136-控制面按两台中继取并集-立项交接单.md b/dsh-server-docs/04-调整方案/136-控制面按两台中继取并集-立项交接单.md new file mode 100644 index 0000000..4ff3146 --- /dev/null +++ b/dsh-server-docs/04-调整方案/136-控制面按两台中继取并集-立项交接单.md @@ -0,0 +1,145 @@ +# 136 · 控制面(平台 web 层)按两台中继取并集 —— 立项交接单 + +> **归档号**:136(占号 = `mkdir 04-调整方案/.lock-136`,2026-09-19 现核成功) +> **日期**:2026-09-19 +> **状态**:🔄 待执行(**已立项,未开工** —— 本单只做立项,零代码 / 零服务器改动) +> **触发**:用户 2026-09-19 原话 —— 「**A 立项修**」(对「控制面也按两台中继取并集」的答复) +> **上游依据**:本工作区 `接续入口_覆盖网络线_20260916.md §0` 最新行的 **⑤ 遗留**(序㊾ 只修了观测面,控制面未跟) +> **单一来源**:根因链 = 本单 §2|观测面口径 = `scripts/overlay-probe.cjs`(序㊾ 已并列集)|拓扑 = `会合中继拆分_取证与改造方案_20260916.md` + +--- + +## 1. 目标与不在范围 + +**目标(一句话)**:让**控制面**(`src/web/` 这一层的可达性解析)与**观测面**(`overlay-probe.cjs`)**用同一口径**看两台自研中继 —— 即「按 `hostId`(含 `network`)合并两台中继的 `/status` 视图」,从而消除「worker 漂到另一台中继时,探针全绿而**实例子域 500**」这一**假绿**。 + +**判据(可验证终态)**:在「`w-106` 归属在 **106 自家中继**」这一现场下,`https://guest.` 与 `https://admin.` **均 302**(⛔ 不是 500),且探针仍 **29 PASS / 0 FAIL / 0 SKIP(rc=0)**。 + +**在范围内**:`src/web/server.ts`(`RelayRendezvous` 接线 + `/status` 快照取数点 + `presenceOnline`)、`src/net/relay/rendezvous.ts`(`resolve()` 三级链)、以及为对齐口径可能需要的 `src/net/relay/presence*` 一侧改动。 + +**⛔ 不在范围内**: +- ⛔ 不改 `hostsProvider` / `dsh_hosts` 表结构(那是拆分方案 S2/C3,**已落地**,本项不再动)。 +- ⛔ 不改 `src/net/reachability.ts` 的 **P0-3「`via='relay'` 拒绝回落 `agentUrl`」** —— 那是**有意的失败关闭**,本项要修的是「解析不出落点」,⛔ 不是「让它回落」。 +- ⛔ 不改探针阈值 / 参数表值格 / `CAND_MIN`(序㊾ 已定,⛔ 本项只让控制面**追平**它)。 +- ⛔ 不动 sshd 反向隧道相关残留、⛔ 不新增监听口、⛔ 不改 nft / nginx。 + +--- + +## 2. 根因链(**已取证,2026-09-19,⛔ 别重做**) + +行号 = 本次阅读的 **HEAD `d2ef362`**(装置已部署到 47 `/opt/dshs/lib`)。 + +| # | 位置 | 事实 | +|---|---|---| +| 1 | `src/config.ts:592` | 状态源**只有一个** URL:`relayStatusUrl: overrides.relayStatusUrl ?? process.env.DSHS_RELAY_STATUS_URL ?? ''` | +| 2 | `src/web/server.ts:361` | `currentClient()` = **当前唯一在持有的那条**拨号通道的 client(`currentClientRef` 由 `:639` 赋值) | +| 3 | `src/web/server.ts:398-413` | `/status` 快照 → `relayEndpoints`(`{localPort, online}`),**只读 `currentClient()` 那一台** | +| 4 | `src/web/server.ts:647-655` | `presenceOnline(name)` 与订阅里的落点,**同样只取 `currentClient()`** | +| 5 | `src/web/server.ts:727 / 746 / 767` | `new RelayRendezvous({ …, online: (name)=>…, presence: presenceOnline })` —— 两个数据源**同源于一台中继** | +| 6 | `src/net/relay/rendezvous.ts:62-68` | `const pushed = this.opts.presence?.(name)`;`const online = pushed !== undefined ? pushed : this.opts.online?.(name)` ⇒ **两者都拿不到 ⇒ `resolve()` 返 `undefined`** | +| 7 | `src/net/reachability.ts:101-108` | `host.via === VIA_RELAY` 且解析不出落点 ⇒ **按设计抛错**:`host "…" 声明 via=relay 但解析不出落点:拒绝回落到 endpoint`(P0-3) | + +**⇒ 现象**:`w-106` 一旦按**抖动选路**(`switcher.ts`,属既有设计)漂到 **106 自家中继**,Manager 侧 `currentClient()` 仍是 47 那条 ⇒ `online` / `presence` 都查不到 `w-106` ⇒ 第 7 步抛错 ⇒ **实例子域 500**;而观测面(序㊾ 已并列集)**全绿** ⇒ **假绿**。 + +**本轮实测旁证(2026-09-19 07:3x)**: +``` +47 relay /status ⇒ sessions ["manager","w-106"] dialers ["manager"] +106 relay /status ⇒ sessions [] dialers ["manager"] +``` +⇒ 两台**都**被 Manager 拨入,但 `w-106` 只出现在 47 的会话表里 ⇒ 缺口**当下被"worker 恰好挂在 47"掩盖**,尚未显形 —— 这正是它危险的地方(**平时不红,漂到 106 才红**)。 + +--- + +## 3. 🔴 修前补测(**必做,第一件事**) + +⚠️ 序㊾ 收口行里的结论「S5 让 106 worker 主入口落回 47 中继后修复(500 → 302)」**只验了"落回 47 即 302",未验"漂到 106 自家中继是否仍 500"**。⛔ 不许把"已恢复 302"当成缺口不存在。 + +**补测步骤(只读 + 一次性制造现场)**: +1. 记录基线归属:`curl -s http://127.0.0.1:20080/status`(47)与 `ssh test106 'curl -s http://127.0.0.1:20080/status'`(106)。 +2. 制造现场:**停 47 的 `dshs-relay` 45 s 后起**(序㊾ 已实测此法能逼 worker 回落/漂移;⚠️ 首轮"停 3 s"不足够 —— 106 是该 worker 候选链**第一条**,它会回连),或按候选链顺序停"当前那条"。 +3. 现场下取三组读数:① 两台 relay `/status` 的 `sessions` 归属 ② 探针输出(应**仍全绿**)③ `curl -sk --http1.1 -o /dev/null -w '%{http_code}' https://guest./`(**期望复现 500**)。 +4. **先红**:把第 3 步 ③ 的 500 原文(含 `journalctl -u dshs` 里那条 `声明 via=relay 但解析不出落点`)贴进本单 §8。 + ⚠️ 若**复现不出 500** ⇒ ⛔ **停手报告**(缺口可能已被别处改动掩盖),**不许**在没有红的情况下开工 —— 没有红就没有可验证的绿。 + +--- + +## 4. 修法与决策点 + +**主修方向(已定)**:把 `online` / `presence` **合并两台中继**的在线态(并集),与序㊾ 探针**同口径**。 + +| 难点 | 说明 | +|---|---| +| `online`(`/status` 快照) | ✅ **容易**:对**每台**中继各取一份 `/status`,按 `network:hostId:port` 合并(`online` 取**或**、`localPort` 取**在线那一侧**)—— 与 `overlay-probe.cjs#mergeEndpoints()` **逐字同口径** | +| `presence`(订阅推送) | 🔴 **真难点**:presence 是**订阅**,Manager 连哪台就只能订阅哪台。两条候选见下 | +| 🔴 口径一致性 | 第二台若走 `/status` 轮询,其**新鲜度**语义必须与订阅一致,⛔ 防「**假在线**」(订阅侧是推、轮询侧是拉,陈旧窗口不同) | + +**决策点(两条候选 —— 各有优有劣,需在执行棒开工前定)**: + +**候选 A:Manager 同时对两台中继各建一条 presence 订阅** +- 优点:口径**完全一致**(都是推、都是同一份 presence 语义);无陈旧窗口差异;与「并集」设计最贴合。 +- 缺点:Manager 需**同时持有两条拨号通道**(当前是"单通道 + 切流"),与 `switcher.ts` 的**单通道/冷却**模型冲突 ⇒ 改动面最大,可能牵动切流与冷却语义(🔴 **动冷却 = 高风险区**,参数表已把冷却值钉死)。 + +**候选 B:第一条(当前通道)走订阅,第二条降级为 `/status` 轮询,两边并集** +- 优点:改动面小、⛔ 不碰切流与冷却;复用 `:398` 已有的 `/status` 快照代码路径(只是从"一台"变"两台")。 +- 缺点:第二台是**拉**、有陈旧窗口 ⇒ 存在「**假在线**」窗口(对端刚离线、轮询未刷);需额外定义并**断言**陈旧阈值,否则把真 FAIL 糊成 PASS(违反序㊾ 已立的「并集只消除'看不见'、⛔ 不把真 FAIL 糊成 PASS」原则)。 + +**倾向(可推翻)**:**先 B 后 A** —— B 能**立即消除 500**(目标达成),且不碰冷却(风险最低);A 作为架构目标登记。⚠️ 若执行棒取证发现「B 的陈旧窗口会让实例子域**偶尔 500**」⇒ 升级为 A。 +🔴 **需要用户拍板的只有边界外项**;以上属**技术实现取舍**,执行棒按 `dsh-decision-method §4.4` 自决并在 §8 写「我选了什么(可推翻)」,⛔ **不上抛**。 + +--- + +## 5. 避让 / 重叠检查(**开工前必核**) + +| 对象 | 关系 | 处置 | +|---|---|---| +| `会合中继拆分_取证与改造方案_20260916.md` **§2 C3**(`dsh_hosts.endpoint` 磨掉"经谁中转") | ⚠️ **最易混淆**:C3 说的是**表形状**(补 `via` 列),⇒ **已由该方案 S2 落地**(`dsh_hosts.via` 已存在、`Reachability.via` 已在用) | ⛔ **本项不再动表**;本项修的是**解析数据源数量**,不是表形状 | +| 同方案 **§9.4**「S4 有方案缺口:中继一旦不在 Manager 主机上,'Manager 拨落点'就断」 | 🔴 **同源问题、不同切面** —— §9.4 讲**拨号侧**落点,本项讲**读状态侧**并集 | 执行棒须引用 §9.4 并写明**本项不解决 §9.4**(避免重复设计 / 互相以为对方会修) | +| `ts` 侧 `presence` 既有实现(`src/web/server.ts:363-389`,序⑲ D5「订阅新鲜 ⇒ 挂起 `/status` 轮询」) | 🔴 **强耦合**:该开关决定 `/status` 轮询**是否在跑** | 本项改动**必须**说明它与 D5 开关的相互作用(第二台轮询是否受 D5 门控?) | +| 探针 `overlay-probe.cjs`(序㊾ 已并列集) | **同口径来源** | 本项**必须复用其合并语义**(键、`online` 取或、`localPort` 取在线侧),⛔ 不许另造一套口径(避免"同一事实两处写") | + +--- + +## 6. 步骤(S0–S4) + +| 步 | 动作 | 生效 | 验收 | +|---|---|---|---| +| **S0** | **修前补测**(§3):制造"worker 在 106"现场 ⇒ 取证 **500 原文** | — | §8 里贴出 500 原文 + `在解析不出落点` 日志行 | +| **S1** | 抽"多中继状态视图"取数入口(**先不动行为**):把"取一台 `/status`"泛化为"取 N 台 + 合并",**默认 N=1 ⇒ 零行为变化**(等价性证明写进注释) | 本机 | `npm test` **与基线逐字相同**(≥217 pass / 0 fail / 1 skipped) | +| **S2** | 按 §4 选定候选(倾向 **B**)接线 `online` / `presence` 并集;🔴 同时定义**陈旧阈值**并写进参数表 | build + 部署 | §7 逐条 | +| **S3** | 部署 + 重启 47 `dshs`(控制面断数十秒,R8 允许,动手前一句话说明) | **立即** | 探针 29 PASS + 目标现场 302 | +| **S4** | 回填:本单 §8 + 当日日志 + `接续入口 §0` | — | — | + +⚠️ `npm test` 必须用 **Node 22**(`E:\ProgramData\.workbuddy\binaries\node\versions\22.22.2-3\node.exe`)。 +⚠️ 部署面:47 `/opt/dshs/lib`(`src/web/server.js`、`src/net/relay/*.js` 及其 `.d.ts`);**md5 逐文件对账**。 + +--- + +## 7. 逐条验收(**先红后绿**) + +| # | 判据 | 红(开工前必须拿到) | 绿(修后) | +|---|---|---|---| +| E1 | **修复本体** | 现场(worker 在 106 自家中继)⇒ `guest.` **500** | 同态 ⇒ **302** | +| E2 | **解析链** | `journalctl -u dshs` 有 `声明 via=relay 但解析不出落点` | 该行**消失**,且出现"并集命中"的可诊断痕迹 | +| E3 | **并集真并集(⛔ 防假绿)** | — | 模拟"对端不可达"腿 ⇒ 判据**按单台视角回退**(⛔ 不许因为"并集了"就把红的糊成绿) | +| E4 | 零回归 · 探针 | 基线 **29 PASS / 0 FAIL / 0 SKIP(rc=0)** | **逐字相同** | +| E5 | 零回归 · 单测 | 基线 **217 pass / 0 fail / 1 skipped**(`# tests 218`) | **逐字相同** | +| E6 | 零回归 · 演练 | 基线 `--scene all` **12 PASS / 0 SKIP / 0 FAIL** | 逐字相同;⚠️ 含 120 s + 90 s 观察窗 ⇒ **外层超时 ≥ 8 min**,否则 `restore()` 不跑 ⇒ **106 `dshs-relay` 留在停机态** | +| E7 | 端到端 | — | 门户 `https:///portal.html` = **200**;`admin.` / `guest.` 在**有 session** 时 302 带 `?token=`(⚠️ 无 cookie 直连返回 **401 属已知正常**) | +| E8 | `bootstrap` 面 | — | `relays[]` / `bootstrap[]` 仍 **3 条、零旧域** | + +--- + +## 8. 回滚 / 回报格式 + +**回滚**:`git checkout -- src/` → 重新 build → scp 回 47 `/opt/dshs/lib` → `systemctl restart dshs`。 +⚠️ 回滚点须在执行棒开工时**先备份** 47 `/opt/dshs/lib`(`cp -a` 到 `/opt/dsh/backups/-/`)。 + +**§8 回报(执行棒收口必写,逐条给原文级证据)**: +1. **修前补测结果**:现场归属(两机 `sessions`)+ 500 原文 + 那行 journal。 +2. **我选了什么(可推翻)**:候选 A / B 的取舍与理由。 +3. **改动清单**:文件 × 行 × 改前改后(+ 等价性证明,若适用)。 +4. **先红后绿**:红 / 绿两条原文并列 **diff 只差这几行**。 +5. **零回归三件套**:E4 / E5 / E6 的逐字数值。 +6. **部署与部署面**:文件 list + **md5 对账** + 回滚点路径。 +7. **边界自证**:⛔ 未 commit / 未 push(写 HEAD)|⛔ 未改 `REACHABILITY`/P0-3 语义 |⛔ 未改探针阈值与参数表值格 |⛔ 未新增监听口 |🔴 `COOLDOWN_MS=0` 计数 = 0 |⛔ 未调 `RELAY_FAILOVER_DEADLINE_MS` / `HB_SEC` / burst。 +8. **如实留档**:本轮未解决项、未测项、以及任何与上游文档冲突的取证结论。 diff --git a/dsh-server-docs/04-调整方案/137-品牌标识改造-去DeepSeek图形.md b/dsh-server-docs/04-调整方案/137-品牌标识改造-去DeepSeek图形.md new file mode 100644 index 0000000..d3cc912 --- /dev/null +++ b/dsh-server-docs/04-调整方案/137-品牌标识改造-去DeepSeek图形.md @@ -0,0 +1,118 @@ +# 137-品牌标识改造(去 DeepSeek 图形 → 平台自有标识) + +> **一句话**:把我们自己的页面上所有 **DeepSeek 品牌标识**(鲸鱼图标 / 「DeepSeek」文字图形 / favicon) +> 换成**平台自有标识** —— 中文「**能力枢纽**」,英语及其他语言「**CapabilityNet**」。 +> **触发**:用户 2026-09-19 原话三句 —— +> ① 「登录和注册页面上的 deepseek的图标去掉,改为中文: 能力枢纽 小写 CapabilityNet ,英语和其他语言:CapabilityNet」 +> ② 「一并替换」(= 其余页面也一并办) +> ③ 「会话页面左上角的 deepseek 先不替换」(= 实例内官方 dsh 界面那处**不动**) +> **执行**:会话 `登录注册页品牌标识改造` → `品牌标识一并替换-admin portal favicon` · 状态 ✅ 已上线 + +--- + +## 0. 替换范围(**先划边界,这是本档案最重要的一节**) + +| 位置 | 处置 | 依据 | +|---|---|---| +| `login.html` / `register.html` 页头品牌区 | ✅ **替换** | 用户① | +| `admin.html`(管理台登录卡片) | ✅ **替换** | 用户②「一并替换」 | +| `portal.html`(管理门户顶栏) | ✅ **换图标** | 用户②;文字「管理门户」保留(见 §1 注) | +| 四页的 **favicon** | ✅ **换新图标** | 用户②;`logo.svg` 就是 DeepSeek 图标 | +| 🔴 **会话页面左上角**(用户实例内的 dsh 会话窗口) | ⛔ **不替换** | 用户③明示「先不替换」。技术上它属**官方 dsh 的 client bundle**,不在本仓 `web/` 目录 ⇒ 也不受红线 **R2**(不改官方 dsh 主程序)允许 | +| `index.html` / `wake.html` | — 无品牌标识 | 前者是纯跳转页、后者是启动过渡页,二者均无 logo/wordmark,且**本来就没有 favicon link**(未加,见 §4) | + +⚠️ **"会话页面"不在本次能力范围内**:本仓 `web/*.html` 是**平台自有的门户/管理页**; +用户在实例里看到的会话界面由官方 `@deepseek-ai/dsh` 渲染 ⇒ 那处的标识要么属于官方、要么需走 +**profile 层官方插件机制**(R2 的合规路径),不是"改个静态文件"能解决的。 + +## 1. 改了什么 + +| 文件 | 改动 | +|---|---| +| `web/login.html` | `.auth-brand` 内:**删** ``(DeepSeek 鲸鱼,2109 B 的 `#4D6BFE` 图形)+ **删** 内联 `DeepSeek…`(131×24 文字图形)→ **换** `CapabilityNet`。10758 → 6799 字节 | +| `web/register.html` | 同上。20002 → 16919 字节 | +| `web/admin.html` | 同构处理(它复用 `.auth-brand`):**删图标 + 删内联 DeepSeek SVG** → `能力枢纽`(该页是**纯中文页**、未接 i18n ⇒ 直接写中文)。11913 → 7928 字节 | +| `web/portal.html` | 顶栏 `.brand` **只换图标**:`src="/logo.svg"` → `src="/favicon.svg"`;⭐ **文字「管理门户」保留** —— 它是**页面定位名**、不是 DeepSeek 痕迹,删掉属越界 | +| **`web/favicon.svg`** | **新建**(1008 B):平台自有图标,取「枢纽 / hub」意象 —— 圆角深底 + 中心节点 + 三条辐条 + 三个外环节点;配色用平台自己的 token(`ink #0f1c33` 底 / `accent-2 #38d6d0` 节点),**刻意避开**原 DeepSeek 的 `#4D6BFE`。同时兼作 favicon 与 portal 顶栏图标 | +| 四页 head | `` → `href="/favicon.svg"` | +| `web/i18n.js` | 新增词条 `brand.name`:`en` → `CapabilityNet`、`zh` → `能力枢纽` | +| `web/design.css` | `.auth-brand .wordmark` 由「定高 22px(给 SVG 用)」改为**文字排版**(`height:auto` + 19px/600/行高 1.2) | +| `test/i18n-brand.test.mjs` | **新增**:`node:vm` + 最小 DOM 桩跑**仓库里那份真实的 `i18n.js`**,断言六条语言路径的**实际渲染结果** | +| `scripts/verify-static.mjs` | **新增 SVG 段**:见 §3-1(踩坑后的防护) | + +**两个刻意的选择** + +1. **登录/注册页走 i18n 词条,⛔ 不写死在 HTML**:① `verify-static.mjs` 的 `I18N_PAGES` + 已把这两页列入「不得出现裸中文文案」,硬写「能力枢纽」会被拦;② 「英语**和其他语言**」 + 要的正是**回退**语义 —— `lookup()` 缺 key 时回退 `en`,只写 `en` 一条即覆盖所有其他语言。 + ⚠️ `admin.html` / `portal.html` **未接 i18n**(既有的纯中文页)⇒ 那两处直接写中文, + **不引入 i18n 运行时**(引它会连带处理它们大量既有裸中文,属扩大范围)。 +2. **发子域名 `favicon.svg` 而不是复用 `logo.svg`**:favicon 缓存按 **URL** 生效 ⇒ 换文件名 + 天然绕过浏览器里那份旧的 DeepSeek 图标缓存,**不需要加 `?v=` 版本串**。 + +## 2. 验证证据 + +| 项 | 实测 | +|---|---| +| **i18n 渲染自证**(跑真实 `i18n.js`) | 六条全绿:浏览器 `en-US` → `CapabilityNet`|`zh-CN` → `能力枢纽`|`?lang=zh` → `能力枢纽`|`?lang=en`(覆盖中文浏览器)→ `CapabilityNet`|`cookie dsh_lang=zh` → `能力枢纽`|`ja-JP` → 回退 `CapabilityNet` | +| **裸 key / 空文案** | 四种语言下均非 `brand.name`、非空 | +| **SVG 合法性**(真 XML 解析器) | `favicon.svg` 解析通过;`children=[title, rect, g, circle×4]`、`paths` 三条齐备;另两个既有 SVG 也一并验过 | +| **反向自证**(坏样本必须被拦) | 故意往注释里注入 `--accent` ⇒ **真解析器报 `not well-formed`** + **校验脚本报红**;清理后复跑全绿 | +| **残留检查**(本地 + 线上) | `web/*.html|css|js` 的 DeepSeek 命中:**仅 `design.css` / `i18n.js` 各 1 处、且都在注释内**(记录"为什么改",属必要留痕);四页线上 `grep -ci deepseek` = **0 / 0 / 0 / 0** | +| **静态页不变量** | `verify-static.mjs` 全部合格(6 页 + 7 段内联脚本语法 + 3 个 css/js + 3 个 svg) | +| **全量单测** | **221 pass / 0 fail / 1 skipped**(`# tests 222`,Node 22.22.2) | +| **线上(`https://ai1net.com`)** | `favicon.svg` = **200 + `image/svg+xml`**;四页均 `favicon=1 / logo.svg=0`;`admin.html` 显示 `能力枢纽`;`portal.html` 显示新图标 + 「管理门户」 | +| **部署 md5 对账** | 5 个 web 文件两端逐一一致(`admin 8736bcc7…` / `portal 164075c8…` / `login 2467bd50…` / `register dfbf996f…` / `favicon fe37ebbf…`) | +| **行尾** | 逐字节核对:`admin.html` / `portal.html` 仍 CRLF、`login.html` / `register.html` / `favicon.svg` 仍 LF —— **与该目录既有风格逐一对应,无偏差** | +| **回滚点** | ① `/opt/dsh/backups/rebrand-20260919-084220/pre.tar`(第一批 4 文件)② `/opt/dsh/backups/rebrand2-20260919-090205/pre.tar`(本批) | + +**部署方式**:静态文件 `scp` 即生效(**不需要重启**);未动任何服务或 env。 + +## 3. 踩坑记录 + +1. 🔴 **SVG 里的 XML 注释不能含 ASCII 双连字符 `--`**(本次实际踩到)。 + 我在 `favicon.svg` 的注释里写了 CSS 变量名 `--ink` / `--accent-2` ⇒ **XML 语法不允许** + ⇒ **整份 SVG 解析失败 ⇒ 浏览器静默不显示图标**,页面与接口都不报任何错。 + - 抓到它的方式:**用真解析器验一遍**(`ET.parse` 报 `not well-formed (invalid token)`), + 而不是只看标签配对或肉眼看 —— 肉眼 100% 看不出这处问题。 + - ⚠️ **中文破折号 `——`(U+2014×2)是合法的**,只有 ASCII `--`(U+002D×2)非法 —— + 所以"注释里有破折号"不是判据,"注释里有**两个连续半角减号**"才是。 + - **已固化为判据**:`scripts/verify-static.mjs` 新增 SVG 段(开头/结尾/viewBox/去痕迹/注释双连字符), + 并做了**反向自证**(注入坏样本 ⇒ 必须报红)。 +2. **`logo.svg` 同时兼作 favicon 与页面图标** ⇒ 换品牌时"页面上的图标"和"浏览器标签图标" + 是**同一份资产**,改一处必须想到另一处(第一批只改了页面内,favicon 就漏了)。 + +## 4. 未做 / 已发现(**只报告,未动手**) + +1. 🟡 **`index.html` / `wake.html` 没有 favicon link** ⇒ 这两个页面的标签图标是浏览器默认。 + 本轮未加(用户没要求,且属功能增量)。若要统一,改动 = 各加一行 ``。 +2. 🟡 **三个 DeepSeek 资产现已全部无引用**:`web/logo.svg`、`web/deepseek-text.svg`、`web/deepseek.webp` + (`grep` 全仓已无任何 HTML/CSS/JS 引用)。**未删** —— 属"额外发现即先报告", + 删除是不可逆动作、需你点头(`git` 里可恢复,风险低)。 +3. ⚪ **`.topbar .wordmark` 那条 CSS 保留未改** —— `admin.html` 用的是 `.auth-brand`、 + `portal.html` 用的是 `.brand`,当前**没有任何页面**用 `.topbar .wordmark`; + 保留是为防样式回归(改它不影响现状,属无收益改动)。 + +## 5. 一条待澄清的表述(未阻塞交付) + +用户原话里的「**小写 CapabilityNet**」有两种读法,本轮按**读法一**实现: + +- **读法一(已实现)**:中文界面 = 「能力枢纽」;英语及其他语言 = 「CapabilityNet」。 +- **读法二**:中文界面 = 「能力枢纽」+ 其下一行**小号** `CapabilityNet`(双语锁定); + 或要求英文写成**全小写** `capabilitynet`。 + +⇒ 若是读法二,改动量 = `i18n.js` 一处词条 + `design.css` 一行(**无需碰 HTML**)。 + +## 6. 回滚 + +| 对象 | 方式 | +|---|---| +| **第一批(login/register/design.css/i18n.js)** | `/opt/dsh/backups/rebrand-20260919-084220/pre.tar` → `/opt/dshs/` | +| **本批(admin/portal/login/register/favicon/verify-static)** | `/opt/dsh/backups/rebrand2-20260919-090205/pre.tar` → `/opt/dshs/` | +| 生效方式 | **`tar` 覆盖即生效,无需重启**(纯静态资源) | +| ⚠️ 注意 | 回滚后页面会**重新引用 `/logo.svg`**(DeepSeek 图标仍在服务器上、文件没删)⇒ 回滚是完整的 | + +## 7. 关联 + +- 同一会话的相邻改动:**Cloudflare Turnstile 密钥配置并启用**(见档案 **134 §实现 E**)。 +- 旧域移除见 **135**;控制面并集立项见 **136**(并行线产物,本档案不重复)。 diff --git a/dsh-server-docs/BRIEF.md b/dsh-server-docs/BRIEF.md index ce690c1..6f0f18f 100644 --- a/dsh-server-docs/BRIEF.md +++ b/dsh-server-docs/BRIEF.md @@ -2,20 +2,20 @@ > **用途**:30 秒掌握「现在是什么样、正在做什么、该读哪一篇」。只写**结论与指针**,细节的单一来源见每行括注。 > **防漂移**:本文件是**人工维护的摘要层**,不复制细节;`scripts/docs-audit.py` 会检查本文件引用的档案号是否存在、域名/阈值等关键串是否与来源文件一致。 -> **最后人工核对**:2026-09-16 10:5x +> **最后人工核对**:2026-09-19 06:4x --- ## 1. 一句话 -多租户 DSH 托管平台:门户(`alotbuy.com`)+ 每用户一个独立 dsh 实例(bwrap 合成根 + 独立 uid + systemd scope)。 +多租户 DSH 托管平台:门户(`ai1net.com`)+ 每用户一个独立 dsh 实例(bwrap 合成根 + 独立 uid + systemd scope)。 **自 2026-09-15 起为集群形态**:`47.77.182.89` = **Manager**(cluster 模式,兼本地 Worker `w-47`)|**106 = Worker `w-106`**(经 SSH 反向隧道)。 ## 2. 现行事实(结论层 · 改任何东西前先对齐这里) | 项 | 现行值 | 单一来源 | |---|---|---| -| **入口** | 门户 `https://alotbuy.com`;用户实例 `https://<用户名>.alotbuy.com`;旧域 `dsh.alotbuy.com` → **301** | 档案 22 / `DEPLOY §1` | +| **入口** | 门户 `https://ai1net.com`;用户实例 `https://<用户名>.ai1net.com`;旧域 `alotbuy.com` 自 **2026-09-19** 起**降为 301 过渡装置**(门户块 `return 301 https://ai1net.com$request_uri`;**仅** `relay-direct.alotbuy.com` 与旧域 `/dshs-relay`、`/dshs-overlay/bootstrap` 继续透传 = 覆盖网络兜底);**relay 候选链同步由 5 条压到 3 条**(删掉两条「摘名不同、物理同落 47 同一路径」的伪多样性,⛔ 未减少真实冗余)。⛔ **退役条件 = 旧域 30 天访问量为 0** 后再删 `relay-direct.conf` 与 `dsh.alotbuy.com.conf` | 档案 22 / **135** / `DEPLOY §1` / `ops/域名迁移_ai1net_20260919/RUNBOOK-cutover.md §5` | | **服务** | systemd `dshs`(`127.0.0.1:3080`)+ 宝塔 nginx 反代(CF 回源 443) | `DEPLOY §1` | | **集群拓扑** | 47 = **Manager**(`deployMode=cluster`;控制面库 = 47 上 **PG13** `/var/lib/dshs-pg`,单元 `dshs-pg`,`127.0.0.1:15432`)+ 本地 Worker `w-47`(19100)|**106 = Worker `w-106`**(19000,**经 SSH 反向隧道**落在 Manager loopback,`DSHS_TUNNEL_TARGET`)|存量用户锚 `w-47`、新用户按容量落 `w-106`(**粘性优先**,`epoch` fencing 防双写)。⛔ 47 兼作 Worker 是**有意设计**,勿当待修项。**回滚** = `rm -f /etc/systemd/system/dshs.service.d/cluster.conf && systemctl daemon-reload && systemctl restart dshs` | `交接单/T08 §10` / `集群化改造方案_Manager-Worker_20260914.md` | | **运行时** | dsh **0.1.5-rc.1**(2026-09-14 由 0.1.2-rc.1 升级,走档案 07 五阶段全流程;官方包**零改动**)| Node 22.23.2 | pnpm 9.15.9 | nginx 1.28.3 | ⚠️ **bwrap 逐机不同:47 = 0.4.0 / 106 = 0.11.0**(`--perms` 属 0.5+ ⇒ 47 上 `Unknown option` ⇒ 沙箱起不来 = 所有实例全挂;要权限**用 `--tmpfs`**) | `DEPLOY §4` | 升级档案 `04/90` | `交接单/T08 §10` | @@ -27,7 +27,7 @@ | **admin 管理面** | 门户:服务 / **密钥** / 用户 / 技能 / 插件管理 + 运行环境;会话内 `portal-entry` 卡片**一键整页跳门户** | `portal.html` / 档案 47 | | **清理阈值** | `ws` ≥ **2048 MB** → 清 T1 平台产物 + T2 超 **90 天**顶层脚本;`sessions` ≥ **1024 MB** → 清超 **365 天**;回收站 **30 天** | 档案 28 / `02 §C.7` | | **定时任务** | 每小时 :05 用量快照 → 04:10 工作区清理 → 04:20 会话回收 → 04:40 回收站清空 → **每周日 05:00 全量备份** | `DEPLOY §6.5` | -| **证书** | `alotbuy.com + *.alotbuy.com`,DNS-01 via `/etc/cloudflare.ini`,**必须 `--dns-cloudflare-propagation-seconds 60`** | 档案 22 / `02 §C.1` | +| **证书** | `ai1net.com + *.ai1net.com`(2026-09-19 经 **DNS-01** 签发,凭据 `/etc/cloudflare-ai1net.ini`,已入 certbot 自动续期)|`alotbuy.com + *.alotbuy.com`(旧域,**仅过渡期保留**)|**必须 `--dns-cloudflare-propagation-seconds 60`** | 档案 22 / `02 §C.1` / RUNBOOK §5 | | **红线** | 不自动升级 dsh;不改官方 dsh 主程序与缓存;client bundle 禁 `exports.default`;服务器 docs 保持 root 600;未明确要求不 commit/push;⛔ **禁止人工删锁 / 接管**(**R9**) | `README §红线` | ## 3. 当前待办(**单一来源 = `03-路线图与待办.md`** + **已规划的单子见 `交接单/README.md §一`**) @@ -73,7 +73,7 @@ ## 7. 文档层约定 -单一来源:清单与状态 = `INDEX.md` §二|待办 = `03-路线图`|部署事实 = `DEPLOY-本部署.md`|术语以档案 38b 为准|域名以 `alotbuy.com` 为准。详见 `README §阅读约定` 与档案 53 / 54。 +单一来源:清单与状态 = `INDEX.md` §二|待办 = `03-路线图`|部署事实 = `DEPLOY-本部署.md`|术语以档案 38b 为准|域名以 `ai1net.com` 为准。详见 `README §阅读约定` 与档案 53 / 54。 **归档编号**:至 **112**(其中 103–112 = 覆盖网络 / 客户端化 10 份规划);**编号 63 是空号,勿补占**。 **本机工作区(2026-09-13 起)**:`E:\ProgramData\AI技能\aliyun-dsh-server`(文档库工作树 = `dsh-server-docs/`);代码仓仍为 `D:\github\dsh_shenxian`。历史文档里出现的 `D:\AI技能\...` 一律是**迁移前旧值**,勿照抄执行。 diff --git a/dsh-server-docs/DEPLOY-本部署.md b/dsh-server-docs/DEPLOY-本部署.md index 545f271..9918c26 100644 --- a/dsh-server-docs/DEPLOY-本部署.md +++ b/dsh-server-docs/DEPLOY-本部署.md @@ -1,15 +1,15 @@ -# DEPLOY — 本部署说明(alotbuy.com / 47.77.182.89) +# DEPLOY — 本部署说明(ai1net.com / 47.77.182.89) > 用途:把"这套平台到底部署在哪、装了什么、改了什么、怎么重建/回滚"一次讲清,供后来人(或未来的我)在**不翻聊天记录**的前提下独立重建。 -> 对应档案 19 §C9。最后更新:2026-09-11 +> 对应档案 19 §C9。最后更新:2026-09-19(域名迁至 ai1net.com) ## 1. 拓扑与入口 | 项 | 值 | |---|---| | 服务器 | `47.77.182.89`(Alibaba Cloud Linux 8,阿里云)|SSH 别名 `bt-server`(端口 32022,root) | -| 门户 / 用户实例 | **`https://alotbuy.com`** / **`https://<用户名>.alotbuy.com`**(Cloudflare 代理,回源走 443) | -| 旧域名 | `dsh.alotbuy.com`(含 `*.dsh.*`)→ **301** 到新域(保留用户名映射) | +| 门户 / 用户实例 | **`https://ai1net.com`** / **`https://<用户名>.ai1net.com`**(Cloudflare 代理,回源走 443) | +| 旧域名 | `alotbuy.com`(含 `*.alotbuy.com`、`*.dsh.alotbuy.com`)→ **301** 到新域(2026-09-19 降为过渡装置;`relay-direct.alotbuy.com` 与 `/dshs-relay`、`/dshs-overlay/bootstrap` 仍透传) | | 面板 | 宝塔(站点 vhost 由面板管理,注意:面板保存会**重写** vhost,可能抹掉自定义指令) | | 编排器 | 自研 `dshs`(Node/TS),systemd 单元 `dshs`,监听 `127.0.0.1:3080` | | 被托管运行时 | 官方 **`@deepseek-ai/dsh@0.1.5-rc.1`**(全局安装于 `/usr/local/lib/node_modules/@deepseek-ai/dsh`,**平台不改其任何文件**) | @@ -23,8 +23,8 @@ | ` = dataRoot/users/` | 每用户根:`ws/`(工作区,用户可见)、`home/`(DSH_HOME:profiles/sessions/storages/skills)、`tmp/`(1777) | | ` = /home/profiles/web` | 该用户的 dsh profile:`node_modules/`、`cordis.patch.yml`、`package.json`、`pnpm-lock.yaml` | | `/opt/dsh/` | 运维脚本与备份:`switch-domain-alotbuy.sh`、`backups/`、`artifacts/`(插件 tgz) | -| `/www/server/panel/vhost/nginx/` | `alotbuy.com.conf`(现役)、`dsh.alotbuy.com.conf`(301 跳转)+ 多份 `.bak-*` | -| `/etc/letsencrypt/live/{alotbuy.com,dsh.alotbuy.com}/` | 证书(DNS-01 via `/etc/cloudflare.ini`) | +| `/www/server/panel/vhost/nginx/` | `ai1net.com.conf`(**现役门户**)、`relay-direct.conf`(443/TCP 兜底)、`alotbuy.com.conf`(**2026-09-19 降级为 301**)、`dsh.alotbuy.com.conf`(301)+ 多份 `.bak-*` | +| `/etc/letsencrypt/live/{ai1net.com,alotbuy.com,dsh.alotbuy.com}/` | 证书(新域 DNS-01 via `/etc/cloudflare-ai1net.ini`;旧域 via `/etc/cloudflare.ini`) | ## 3. 环境变量(`/etc/dshs.env`) @@ -32,8 +32,8 @@ |---|---| | `DSHS_PORT` | 编排器监听端口(127.0.0.1:3080) | | `DSHS_DATA_ROOT` | dataRoot | -| `DSHS_BASE_DOMAIN` | 门户/用户子域基域(现 `alotbuy.com`) | -| `DSHS_COOKIE_DOMAIN` | 会话 Cookie 域(现 `.alotbuy.com`;见档案 22 §五 取舍说明) | +| `DSHS_BASE_DOMAIN` | 门户/用户子域基域(现 `ai1net.com`) | +| `DSHS_COOKIE_DOMAIN` | 会话 Cookie 域(现 `.ai1net.com`;见档案 22 §五 取舍说明) | | `DSHS_SECURE_COOKIES` | Cookie `Secure` 标记(HTTPS 下应开) | | `DSHS_DSH_BIN` | 子实例可执行路径 | | `DSHS_ISOLATION_MODE` | 隔离模式(本部署 `account` = bwrap + setpriv + systemd-run scope) | diff --git a/dsh-server-docs/INDEX.md b/dsh-server-docs/INDEX.md index eb2081d..3e252d6 100644 --- a/dsh-server-docs/INDEX.md +++ b/dsh-server-docs/INDEX.md @@ -3,7 +3,7 @@ > **首读 `BRIEF.md`(现状卡)**:30 秒对齐现行事实;本文件只管"定位篇目 + 状态"。 > **双端模型**:本机 `D:\github\dsh_shenxian\dsh-server-docs\`(**工作树**,git 仓库 `dsh_shenxian_doc`)↔ 服务器 `/opt/dsh/docs`(**部署镜像**,root 600,**无 .git**,靠 scp)。 > **对账**:`bash scripts/docs-sync-check.sh`(退出码 0 = 全绿)。 -> **最后核对**:2026-09-12 — **篇数与规模不在此写死**:本库由多会话并行改动,绝对值数十分钟即失效(同日实测 83 → 85 → 87,3 次作废);**一律以 `python3 scripts/docs-manifest.py` 复跑结果为准**。代码 HEAD 看服务器:`git -C /opt/dshs log -1`(最后核对值 `ebe8075`)。域名 `alotbuy.com`(旧域 `dsh.alotbuy.com` 已 301)。 +> **最后核对**:2026-09-12 — **篇数与规模不在此写死**:本库由多会话并行改动,绝对值数十分钟即失效(同日实测 83 → 85 → 87,3 次作废);**一律以 `python3 scripts/docs-manifest.py` 复跑结果为准**。代码 HEAD 看服务器:`git -C /opt/dshs log -1`(最后核对值 `ebe8075`)。域名 `ai1net.com`(旧域 `alotbuy.com` 自 2026-09-19 仅作 301 过渡装置)。 --- @@ -11,7 +11,7 @@ | 我要做什么 | 先读 | |---|---| -| **访问入口(当前域名)** | 门户 `https://alotbuy.com`;用户实例 `https://<用户名>.alotbuy.com` | `04-22`(迁移+回滚) | +| **访问入口(当前域名)** | 门户 `https://ai1net.com`;用户实例 `https://<用户名>.ai1net.com` | `04-22`(迁移+回滚) | | **重建 / 交接部署** | **`DEPLOY-本部署.md`**(拓扑·目录·env·依赖·脚本族·构建/部署/回滚) | 档案 19 §C9 | | 理解架构 / 多租户隔离 | `01-规划与架构.md`(一~五、九、十三) | `archive/` 全文 | | 日常运维(重启/备份/token/KEY/排障) | `02-运维手册.md` | `03-路线图与待办.md` | @@ -44,7 +44,7 @@ ## 二、全量清单 -> **状态摘要**(**机器生成,勿手改**):档案 **117** 份(`04-*`),另含根级编号 5 条(01/02/03/06/07),另有非编号行 14 条(README / INDEX / 技能 / poc 等)—— ✅ 17 | 🔄 5 | 📝 1 | 🔍 1 | 📋 22 | 未标记 76。复跑 `python3 scripts/docs-index-stats.py` 取数,`--write` 就地刷新本行。 +> **状态摘要**(**机器生成,勿手改**):档案 **121** 份(`04-*`),另含根级编号 5 条(01/02/03/06/07),另有非编号行 14 条(README / INDEX / 技能 / poc 等)—— ✅ 20 | 🔄 5 | 🔍 1 | 📋 23 | 🟡 1 | 未标记 76。复跑 `python3 scripts/docs-index-stats.py` 取数,`--write` 就地刷新本行。 > **分层与机读明细**(路径 / 日期 / 字符数 / 被引次数 / tier,可 `jq` 先筛后读):**`docs-manifest.json`**;复跑 `scripts/docs-manifest.py` 即刷新。 > 图例:✅已落地 | 🔄维护中 | 🧪PoC | 📝待开发 | 🔍核查完成 | 📋评估 | 🟡保留兜底 | 🗄归档|🔧修复|🔴|🚧|❓|PoC 实证完成(插件 v0|已上线|生效|已实施|已实施 + API 全链路验|已冻结|已落地 + 验证通过|审计已完成 → 加固项待用户|已上线,页面全部 200|已封板,实施中|核查完成 → 收敛项 H1-|✅ 已实施并在 admin/|🔄 批次 1 因改走 bun|方案 A + handoff|根因已定位并修复(nginx|修复已编译(CI ✅),待重|已实施并上线|已修复并上线(服务已重启|清单建立|✅ 已实施并端到端验证|✅ 已实施并 live 验证|✅ 已实施并视觉验证|🔍 核查完成,P0 待决策|✅ 已实施(commit 见|✅ 已实施(commit `|✅ 已修复(commit `|🔍 取证完成|🔍 核查完成|✅ 已实施(`99de8c5|✅ 已落地|✅ 已修复|已落地并实测验证|已落地并端到端验证|🔄 第一批已修复|✅ 已实施(两批):第一批|✅ 已实施并端到端验证(co|✅ 已实施并验证(服务器已生|admin 侧已实施|✅ 已部署并生效(服务器 `|✅ 已实现、已验证、已部署|✅ 已完成并部署|✅ 全部完成并验证|✅ 已完成(2026-09-|✅ 已止损|✅ 已落地并部署(2026-|✅ 已完成并部署(2026-|✅ 已实现并单点验证|✅ 已落地并实测验证(配置|📋 标准已立|✅ 代码改造 + 组件级验证|✅ 已落地并验证(服务器 `|✅ 已部署并验证(2026-|✅ 已修复并部署|📋 评估中|🚧 执行中(R0 已完成|🚧 代码完成、产物已出|✅ 已完成并上线验证|✅ 已上线并端到端验证(后端|✅ 已修复并验证(源仓 `c|🔄 进行中(L2 机制级已验 @@ -178,6 +178,10 @@ | 04-128 | 📋 | **DSH 桌面客户端开发方案 —— 基于官方 Electron 壳迭代**(待评审 · 只做规划不含代码):复用官方壳 vs 自建的取舍与分发/版本矩阵 | | 04-129 | ✅ | **日志采集 · 实时巡检 · 事后溯源(方案 C 实现)**:不装新日志服务(否 Amber / 否 Loki+Alloy),用 `scripts/dshlog.mjs` 把 47/106 journald 拉回本机按天归档 + 跨机时间线 + 8 条巡检规则;**服务器侧零安装零端口**;⚠️ 最大剩余缺口 = 实例 `dsh --profile` 的 stdout 不进 journald | | 04-133 | 🟡 | 🔴 **序㊺ 实测判「D 本轮不实现」**(真实首屏包 **23 块 0 个低熵块**、60 份真实独立内容 **0 份低熵** ⇒ `D-1` 无对象可稀释、`D-2` 前提不成立)⇒ 剩 **C 域分离 + 判据重裁**(序㊻ · automation `aab9e357-8101-4011-b829-bf9f4a459bf7`)|**覆盖网络 · 低熵块治理方案 —— C 域分离 + D 非确定性**:块 id / 内容 id 的收敛哈希改 **per-network keyed hash**(切断跨 network 相关性,代价≈0)+ 低熵块非确定性(首选 **D-1「稀释域」**);⛔ **B(OPRF / SA-MLE)不立项**(降级为可选加强);🔴 **第一个动作 = 补测「低熵块种类数 / 体积 / 占首屏包比例」**(从未测过;且切分是**定长 1 MiB** ⇒ 首屏包内几乎无纯低熵块,待测必须含**子窗口腿**防假绿)|交付 = `交接单/覆盖网络-序45-低熵块治理-测熵与实现.md` | +| 04-134 | ✅ | **注册页人机验证与邮箱验证码**(2026-09-19 落地 · 线上实测通过):注册 = 用户名 + 密码 + **邮箱验证码** + **Cloudflare Turnstile**(✅ **密钥已配、已启用**;服务端校 `success` + `action` + `hostname` **三项** —— 只校 `success` 时"拿公开 sitekey 在自己站点替真人取合法 token 再打我们接口"这条路是通的);DB **迁移 v10**(`users.email` + `email_codes` 事件表);邮件走**可插拔驱动**(`brevo` / `http` / `log`,换供应商只改 env),发件人 **`noreply@ai1net.com`**(Brevo 域名已认证 + DKIM + SPF);防爆破 = 三层配额 + **递增冷却阶梯**(60→60→180→300→900→1800s)+ 试错 5 次作废 + 码只存哈希 + 单次使用;实测 Brevo **delivered**、冷却 429、错码 `attemptsLeft` 递减、正确码 201、重放被挡、无 token/假 token **403**|档案 `04-调整方案/134-注册页人机验证与邮箱验证码.md` | +| 04-135 | ✅ | **旧域 `alotbuy.com` 从项目移除**(2026-09-19 落地):代码 / 服务端 / 文档技能三层收敛为 `ai1net.com` 单域;旧域站点**降级为 301 过渡装置**(保留 ACME + `/dshs-relay` + `/dshs-overlay/bootstrap`,**30 天零流量**后退役);relay 候选 **5 → 3**(删掉两条“摘名不同、同落 47”的伪多样性);修 `wake.html` 转义点残留 + 演练脚本硬编码域造成的**假红**;参数表 2 键与 §10 指纹同步 | +| 04-136 | 📋 | **控制面按两台中继取并集 —— 立项交接单**(2026-09-19 立项 · **未落地**):平台 web 层只取 47 中继 ⇒ 106 中继不被使用;根因链 7 行(取址路径 `src/web/server.ts` / `rendezvous.ts`)、**先红后绿**强制、候选 A/B 各带优缺点、验收 E1–E8 + 回滚 | +| 04-137 | ✅ | **品牌标识改造 —— 去 DeepSeek 图形 → 平台自有标识**(2026-09-19 上线):**四页 + favicon 一并替换** —— `login` / `register` / `admin` 页头删 **DeepSeek 鲸鱼图标 +「DeepSeek」文字图形** 改平台标识(中文「**能力枢纽**」/英语及其他语言「**CapabilityNet**」,走 i18n 词条 `brand.name`,`en` 一条即覆盖其他语言回退);`portal` 顶栏换图标(页面名「管理门户」保留);**新建 `web/favicon.svg`**(平台自有 hub 图标,避开 DeepSeek 蓝);🔴 **会话页面左上角那处不动**(用户明示「先不替换」+它属官方 dsh client bundle,受 R2 约束);🔴 踩坑:**SVG 的 XML 注释不得含 ASCII 双连字符 `--`**(写 CSS 变量名即踩)⇒ 整份 SVG 解析失败、图标**静默不显示**,已加进 `verify-static.mjs` 并做反向自证;新增 `test/i18n-brand.test.mjs`|档案 `04-调整方案/137-品牌标识改造-去DeepSeek图形.md` | | — | 🗄 | `archive/dsh-improvement-plan-20260909-full.md`:拆分前 19 章 | | — | ✅ | `skills/dsh-change-workflow/SKILL.md`:六阶段 + **红线 R1-R11**(工作副本在本机 `.workbuddy/skills/`)| | — | ✅ | `skills/dsh-decision-method/SKILL.md`:**改造决策方法论**(用户有效决策 U1-U12 / AI 有效决策 A1-A13 / 反例 X1-X8 + 确认最优解十问 + 交互 UI 专项清单)(工作副本在本机 `.workbuddy/skills/`)| diff --git a/dsh-server-docs/README.md b/dsh-server-docs/README.md index 8746c26..0e33571 100644 --- a/dsh-server-docs/README.md +++ b/dsh-server-docs/README.md @@ -1,7 +1,7 @@ # dsh 多租户平台 — 改造方案文档库 > 服务器:47.77.182.89(Alibaba Cloud Linux al8)| 平台:dshs + dsh 0.1.2-rc.1(account 硬隔离) -> 域名:**`alotbuy.com`**(门户)/**`<用户名>.alotbuy.com`**(用户实例)| 旧域 `dsh.alotbuy.com` 已 **301**(档案 22) +> 域名:**`ai1net.com`**(门户)/**`<用户名>.ai1net.com`**(用户实例)| 旧域 `alotbuy.com` 自 2026-09-19 仅作 **301** 过渡装置(档案 22 / `域名迁移_ai1net_20260919`) > 服务:systemd `dshs`(127.0.0.1:3080)| 现行状态与部署细节见 **`DEPLOY-本部署.md`** > ⚠️ **两处 `docs/` 勿混**(档案 19 §C10): @@ -16,7 +16,7 @@ 1. **两套编号互不相同**:根级 `01/02/03/06` 是**长期文档**(架构、运维、路线图、UI 规范);`04-调整方案/NN-` 是**一次性改造成档案**。说"档案 NN"时**默认指后者**。 2. **单一来源**:**清单与状态 = `INDEX.md` §二**;**待办与优先级 = `03-路线图与待办.md`**;**部署/构建/回滚 = `DEPLOY-本部署.md`**。其余文件提到这些内容时**只做指针,不复制**(历史上 README/INDEX 各存一份清单,已漂移 —— 见档案 53)。 3. **术语以档案 38b 为准**:统一称「**功能插件**」(早期档案写"业务插件"= 同一事物;历史档案保留原文,不回改)。 -4. **域名以 `alotbuy.com` 为准**:2026-09-11 迁移(档案 22)。早期档案里的 `dsh.alotbuy.com` 属当时事实,**保留原文**;现行入口一律 `alotbuy.com`。 +4. **域名以 `ai1net.com` 为准**:**2026-09-19** 由 `alotbuy.com` 迁入(档案 22 / `ops/域名迁移_ai1net_20260919/`)。早期档案里的 `dsh.alotbuy.com`、`alotbuy.com` 属当时事实,**保留原文**;现行入口一律 `ai1net.com`。 5. **AI 默认不读**:`archive/`(历史全文)、`scripts/` `ops/`(工具与配置)、`*.bak*`、`04-调整方案/poc/`(源码)—— 需要时再按需打开。 6. **档案头部格式**:`- 日期:` + `- 状态:`(✅已完成 / 🔄进行中 / ⏸暂缓 / ❌关闭)+ `- 触发:`;正文用「需求 → 改动点 → 验证」三段。 diff --git a/dsh-server-docs/docs-manifest.json b/dsh-server-docs/docs-manifest.json index e5eda4b..bed6c22 100644 --- a/dsh-server-docs/docs-manifest.json +++ b/dsh-server-docs/docs-manifest.json @@ -1,31 +1,31 @@ { "generatedFrom": "scripts/docs-manifest.py", "counts": { - "files": 189, - "chars": 1982749 + "files": 194, + "chars": 2050190 }, "tiers": { "hot": 6, "cur": 25, "warm": 61, - "cold": 38, - "doc": 59 + "cold": 42, + "doc": 60 }, "domains": { - "platform": 74, + "platform": 75, "method": 47, - "ui": 15, + "ui": 17, "plugin": 27, - "ops": 14, + "ops": 16, "?": 8, "external": 4 }, "layers": { "L0": 1, - "L5": 145, + "L5": 149, "L4": 22, "L2": 3, - "?": 3, + "?": 4, "L1": 2, "L3": 13 }, @@ -51,7 +51,7 @@ "title": "六、单实例迁移执行步骤(从当前 data/ 形态 → users/main 形态)", "status": "?", "date": "", - "chars": 14585, + "chars": 14656, "lines": 338, "refs": 0, "refsCurrent": 0, @@ -548,7 +548,7 @@ "date": "04-12", "chars": 4117, "lines": 77, - "refs": 4, + "refs": 6, "refsCurrent": 0, "tier": "warm", "layer": "L5", @@ -713,7 +713,7 @@ "date": "04-13", "chars": 2444, "lines": 58, - "refs": 5, + "refs": 9, "refsCurrent": 0, "tier": "warm", "layer": "L5", @@ -735,6 +735,66 @@ "domain": "method", "tldr": "" }, + { + "path": "04-调整方案/134-注册页人机验证与邮箱验证码.md", + "num": "134", + "title": "134-注册页人机验证与邮箱验证码(2026-09-19 落地)", + "status": "✅", + "date": "09-19", + "chars": 13148, + "lines": 240, + "refs": 0, + "refsCurrent": 0, + "tier": "cold", + "layer": "L5", + "domain": "ui", + "tldr": "" + }, + { + "path": "04-调整方案/135-旧域alotbuy.com从项目移除.md", + "num": "135", + "title": "135-旧域 alotbuy.com 从项目移除(2026-09-19 落地)", + "status": "✅", + "date": "09-19", + "chars": 7758, + "lines": 130, + "refs": 0, + "refsCurrent": 0, + "tier": "cold", + "layer": "L5", + "domain": "ops", + "tldr": "" + }, + { + "path": "04-调整方案/136-控制面按两台中继取并集-立项交接单.md", + "num": "136", + "title": "136 · 控制面(平台 web 层)按两台中继取并集 —— 立项交接单", + "status": "🔄 待执行(已立项,未开工", + "date": "09-19", + "chars": 7839, + "lines": 146, + "refs": 0, + "refsCurrent": 0, + "tier": "cold", + "layer": "L5", + "domain": "platform", + "tldr": "" + }, + { + "path": "04-调整方案/137-品牌标识改造-去DeepSeek图形.md", + "num": "137", + "title": "137-品牌标识改造(去 DeepSeek 图形 → 平台自有标识)", + "status": "✅", + "date": "09-19", + "chars": 6287, + "lines": 119, + "refs": 0, + "refsCurrent": 0, + "tier": "cold", + "layer": "L5", + "domain": "ui", + "tldr": "" + }, { "path": "04-调整方案/14-实例会话敏感信息暴露面审计与加固.md", "num": "14", @@ -2076,7 +2136,7 @@ "title": "BRIEF — 现状卡(**AI / 人 首读这一份**)", "status": "?", "date": "", - "chars": 5965, + "chars": 6424, "lines": 80, "refs": 0, "refsCurrent": 0, @@ -2103,10 +2163,10 @@ { "path": "DEPLOY-本部署.md", "num": null, - "title": "DEPLOY — 本部署说明(alotbuy.com / 47.77.182.89)", + "title": "DEPLOY — 本部署说明(ai1net.com / 47.77.182.89)", "status": "?", "date": "", - "chars": 4415, + "chars": 4659, "lines": 102, "refs": 0, "refsCurrent": 0, @@ -2121,8 +2181,8 @@ "title": "dsh 平台文档导航(INDEX)", "status": "?", "date": "", - "chars": 22828, - "lines": 264, + "chars": 24468, + "lines": 268, "refs": 0, "refsCurrent": 0, "tier": "doc", @@ -2136,7 +2196,7 @@ "title": "dsh 多租户平台 — 改造方案文档库", "status": "?", "date": "", - "chars": 10835, + "chars": 10935, "lines": 103, "refs": 0, "refsCurrent": 0, @@ -2340,6 +2400,21 @@ "domain": "plugin", "tldr": "" }, + { + "path": "ops/域名迁移_ai1net_20260919/RUNBOOK-cutover.md", + "num": null, + "title": "域名迁移 runbook —— `alotbuy.com` → `ai1net.com`(47 · 2026-09-19 建立)", + "status": "?", + "date": "", + "chars": 5785, + "lines": 99, + "refs": 0, + "refsCurrent": 0, + "tier": "doc", + "layer": "?", + "domain": "ops", + "tldr": "" + }, { "path": "ops/接续入口_覆盖网络线_20260916.md", "num": null, @@ -2391,7 +2466,7 @@ "title": "dsh-change-workflow — dsh 平台改造工作流", "status": "?", "date": "", - "chars": 70412, + "chars": 70439, "lines": 1039, "refs": 0, "refsCurrent": 0, @@ -2406,7 +2481,7 @@ "title": "dsh-decision-method — 平台改造的思考与决策方法", "status": "?", "date": "", - "chars": 15995, + "chars": 15994, "lines": 360, "refs": 0, "refsCurrent": 0, @@ -2496,7 +2571,7 @@ "title": "dsh-feature-first — 功能优先协作协议", "status": "?", "date": "", - "chars": 14920, + "chars": 14919, "lines": 361, "refs": 0, "refsCurrent": 0, @@ -2511,7 +2586,7 @@ "title": "dsh-instance-diagnose — DSH 实例故障诊断", "status": "?", "date": "", - "chars": 12545, + "chars": 12542, "lines": 252, "refs": 0, "refsCurrent": 0, @@ -2541,7 +2616,7 @@ "title": "dsh-opensource-release — 开源导出与版本迭代", "status": "?", "date": "", - "chars": 48137, + "chars": 48133, "lines": 595, "refs": 0, "refsCurrent": 0, @@ -2556,7 +2631,7 @@ "title": "DSH 业务插件故障诊断", "status": "?", "date": "", - "chars": 5919, + "chars": 5917, "lines": 121, "refs": 0, "refsCurrent": 0, @@ -2856,8 +2931,8 @@ "title": "交接单 · 低熵块治理(**测熵先行** + C 域分离 + D 非确定性)", "status": "待执行", "date": "", - "chars": 19215, - "lines": 313, + "chars": 43309, + "lines": 620, "refs": 0, "refsCurrent": 0, "tier": "doc", diff --git a/dsh-server-docs/ops/nginx/alotbuy.com.conf b/dsh-server-docs/ops/nginx/alotbuy.com.conf index af61565..7f55a88 100644 --- a/dsh-server-docs/ops/nginx/alotbuy.com.conf +++ b/dsh-server-docs/ops/nginx/alotbuy.com.conf @@ -1,12 +1,29 @@ -# alotbuy.com —— DSH 平台站点(2026-09-10 建立,档案 21 场景延伸) -# 走 Cloudflare 代理(橙云),故: -# 1) 用 set_real_ip_from + CF-Connecting-IP 还原真实客户端 IP(否则日志/风控里全是 CF 的 IP) -# 2) 80 端口「代理」而非 301:CF 若为 Flexible,源站 301 会造成 CF 侧循环;改成代理两种模式都能用 -# —— 但**必须**把 CF 的 SSL/TLS 模式设为 Full (Strict),否则 CF→源站是明文(凭据裸奔) -# 3) 继承 dsh 站点的关键优化:proxy_buffering off(流式)、gzip_proxied any(930KB bundle 压缩) +# alotbuy.com —— 旧域(2026-09-10 建立为门户站点;**2026-09-19 降级为 301 过渡装置**) +# +# 为什么"降级"而不是"删除"(判据见 04-调整方案/135): +# ① 老书签 / 外链不 404(301 到新域**同名子域**,保留用户名映射) +# ② 已入网节点的**旧域中继入口**仍可透传 ⇒ 不断线(见下方保留的 location) +# ③ 软回滚路径保留:`cp alotbuy.com.conf.bak-dompurge- alotbuy.com.conf && nginx -t && nginx -s reload` +# ⛔ 退役条件 = **旧域 30 天访问量为 0**(看 /www/wwwlogs/alotbuy.com.log)→ 连 +# `dsh.alotbuy.com.conf`、`relay-direct.conf` 一并删。 +# +# 唯三**不** 301 的路径(其余一律 301 到新域): +# /dshs-relay → 127.0.0.1:20080(自研 relay WebSocket;旧域入口,SEEDS 已不再引导) +# /dshs-overlay/bootstrap → 127.0.0.1:3080(覆盖网络目录只读路由) +# /.well-known/acme-challenge/(ACME 落点,续期不掉链) +# +# map 必须位于 http 上下文 —— 面板 vhost 文件正是被 include 在 http{} 内,故写在本文件顶层。 +# ⚠️ 与 `dsh.alotbuy.com.conf` 的 map 是**两个不同变量名**,不冲突; +# `*.dsh.alotbuy.com` 由那个文件**更长的通配 server_name** 接管,不进本文件的 map。 +# ⚠️ 301 目标是**不同主机**(ai1net.com,CF 侧已是 Full(strict))⇒ 不存在"源站 301 造成 CF 循环"。 +# 故 80 端口可以直接 301(原门户块当年为兼容 CF Flexible 才用代理)。 +map $host $alotbuy_301_host { + default ai1net.com; + ~^(?[^.]+)\.alotbuy\.com$ $lb301.ai1net.com; +} -# ---- HTTP:代理(不用 301,兼容 CF Flexible;Full (Strict) 下 80 不会被用到)---- +# ---- HTTP:301 到新域(旧域不再承载门户)---- server { # CF 回源 IP → 还原真实客户端 IP(仅本 server 生效) set_real_ip_from 173.245.48.0/20; @@ -34,25 +51,40 @@ server { real_ip_header CF-Connecting-IP; listen 80; server_name alotbuy.com www.alotbuy.com *.alotbuy.com; - location / { + + # ── ACME 挑战(续期用;`^~` 优先于下面的 `location /`)── + location ^~ /.well-known/acme-challenge/ { + root /www/server/nginx/html; + default_type text/plain; + access_log off; + } + + # ── 覆盖网络中继(保留:已入网节点仍可能持旧域地址)── + location /dshs-relay { + proxy_pass http://127.0.0.1:20080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + } + location = /dshs-overlay/bootstrap { proxy_pass http://127.0.0.1:3080; proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_read_timeout 3600s; proxy_buffering off; - proxy_cache off; - gzip_proxied any; } + + location / { return 301 https://$alotbuy_301_host$request_uri; } + access_log /www/wwwlogs/alotbuy.com.log; error_log /www/wwwlogs/alotbuy.com.error.log; } -# ---- HTTPS:门户 + 用户实例子域 ---- +# ---- HTTPS:301 到新域(同上)---- server { # CF 回源 IP → 还原真实客户端 IP(仅本 server 生效) set_real_ip_from 173.245.48.0/20; @@ -84,32 +116,33 @@ server { ssl_certificate /etc/letsencrypt/live/alotbuy.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/alotbuy.com/privkey.pem; - location / { - proxy_pass http://127.0.0.1:3080; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + location ^~ /.well-known/acme-challenge/ { + root /www/server/nginx/html; + default_type text/plain; + access_log off; + } + + # ── 覆盖网络中继(保留)── + location /dshs-relay { + proxy_pass http://127.0.0.1:20080; proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; + proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; proxy_read_timeout 3600s; - # 流式(dsh 回复/思考逐块下发)+ 反代压缩 + 解除缓冲 - proxy_buffering off; - proxy_cache off; proxy_send_timeout 3600s; - gzip_proxied any; + proxy_buffering off; } - # 内容哈希命名的静态资源 → 长缓存 - location ~* ^/assets/ { + location = /dshs-overlay/bootstrap { proxy_pass http://127.0.0.1:3080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_buffering off; - gzip_proxied any; - expires 30d; } + + location / { return 301 https://$alotbuy_301_host$request_uri; } + access_log /www/wwwlogs/alotbuy.com.log; error_log /www/wwwlogs/alotbuy.com.error.log; } diff --git a/dsh-server-docs/ops/域名迁移_ai1net_20260919/RUNBOOK-cutover.md b/dsh-server-docs/ops/域名迁移_ai1net_20260919/RUNBOOK-cutover.md new file mode 100644 index 0000000..b3e6a48 --- /dev/null +++ b/dsh-server-docs/ops/域名迁移_ai1net_20260919/RUNBOOK-cutover.md @@ -0,0 +1,98 @@ +> ✅ **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`) | +| 实例子域 | **`.alotbuy.com`**(一级标签,由门户 vhost 的 `*.alotbuy.com` 承载,无需第二张证书) | +| 旧名 301 | `dsh.alotbuy.com.conf`:`dsh.alotbuy.com` / `*.dsh.alotbuy.com` → 301 到 `alotbuy.com` / `