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。
This commit is contained in:
1 parent
d2ef362a98
commit
971ccc3703
56 files changed
+3498
-193
No files matched your search
@@ -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 方案:`[email protected]`」 | **已落地**:Brevo 建域名 `ai1net.com` → 4 条 DNS + SPF → **`authenticated: true` / `verified: true`** → 发件人 **id=2 `[email protected]`(AI1NET)** → `DSHS_MAIL_FROM` 切换并重启|实测 `delivered from= [email protected]` |
|
||||
| 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` 派生 `<domain>` + `www.<domain>`) |
|
||||
|
||||
附带加固:
|
||||
- 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=<Brevo 密钥>
|
||||
[email protected] # 2026-09-19 08:07 由 [email protected] 切换
|
||||
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/<id>` 单点删除。
|
||||
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)。
|
||||
@@ -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`;`~^(?<lb301>[^.]+)\.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-<ts>`(**不是删除**)。
|
||||
|
||||
**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://<user>.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`(须 `[email protected]`,默认别名已失效)
|
||||
报出**30+ 个"仅本地待推送"文件** —— 属**本会话之前**就存在的镜像滞后,本轮**只推了 6 个技能文件**,其余按"只做被明确要求的事"**未动**。
|
||||
8. **演练脚本 `overlay-failover-drill.cjs` 未铺服务器**:两机 `/opt/dshs{,-cluster}/scripts/` **本来就没有该文件**(它是本机 ssh 出去的工具)
|
||||
⇒ 只在本机生效;而 `overlay-probe.cjs` 已按惯例**三处同源**(md5 `55568f87bfe2dc8e43289c69b4126004`)。
|
||||
@@ -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.<domain>` 与 `https://admin.<domain>` **均 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.<domain>/`(**期望复现 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.<domain>` **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://<domain>/portal.html` = **200**;`admin.<domain>` / `guest.<domain>` 在**有 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/<seq>-<ts>/`)。
|
||||
|
||||
**§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. **如实留档**:本轮未解决项、未测项、以及任何与上游文档冲突的取证结论。
|
||||
@@ -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` 内:**删** `<img class="logo" src="/logo.svg">`(DeepSeek 鲸鱼,2109 B 的 `#4D6BFE` 图形)+ **删** 内联 `<svg><title>DeepSeek</title>…</svg>`(131×24 文字图形)→ **换** `<span class="wordmark" data-i18n="brand.name">CapabilityNet</span>`。10758 → 6799 字节 |
|
||||
| `web/register.html` | 同上。20002 → 16919 字节 |
|
||||
| `web/admin.html` | 同构处理(它复用 `.auth-brand`):**删图标 + 删内联 DeepSeek SVG** → `<span class="wordmark">能力枢纽</span>`(该页是**纯中文页**、未接 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 | `<link rel="icon" href="/logo.svg">` → `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` 显示 `<span class="wordmark">能力枢纽</span>`;`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** ⇒ 这两个页面的标签图标是浏览器默认。
|
||||
本轮未加(用户没要求,且属功能增量)。若要统一,改动 = 各加一行 `<link rel="icon" href="/favicon.svg">`。
|
||||
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**(并行线产物,本档案不重复)。
|
||||
Reference in new issue
Block a user