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
@@ -238,9 +238,9 @@ node /opt/dshs/ensure-role-profile-patch.cjs [--restart] <username>
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 门户 / 用户实例 | `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 在用,勿改) |
|
||||
|
||||
@@ -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**(并行线产物,本档案不重复)。
|
||||
@@ -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技能\...` 一律是**迁移前旧值**,勿照抄执行。
|
||||
@@ -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/[email protected]`**(全局安装于 `/usr/local/lib/node_modules/@deepseek-ai/dsh`,**平台不改其任何文件**) |
|
||||
@@ -23,8 +23,8 @@
|
||||
| `<userRoot> = dataRoot/users/<uuid>` | 每用户根:`ws/`(工作区,用户可见)、`home/`(DSH_HOME:profiles/sessions/storages/skills)、`tmp/`(1777) |
|
||||
| `<profile> = <userRoot>/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) |
|
||||
|
||||
@@ -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),发件人 **`[email protected]`**(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/`)|
|
||||
|
||||
@@ -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. **档案头部格式**:`- 日期:` + `- 状态:`(✅已完成 / 🔄进行中 / ⏸暂缓 / ❌关闭)+ `- 触发:`;正文用「需求 → 改动点 → 验证」三段。
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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-<ts> 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;
|
||||
~^(?<lb301>[^.]+)\.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;
|
||||
}
|
||||
@@ -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`) |
|
||||
| 实例子域 | **`<user>.alotbuy.com`**(一级标签,由门户 vhost 的 `*.alotbuy.com` 承载,无需第二张证书) |
|
||||
| 旧名 301 | `dsh.alotbuy.com.conf`:`dsh.alotbuy.com` / `*.dsh.alotbuy.com` → 301 到 `alotbuy.com` / `<label>.alotbuy.com`,证书 `live/dsh.alotbuy.com` |
|
||||
| 中继兜底 | `relay-direct.conf`:`relay-direct.alotbuy.com`(443),两端点 `/dshs-relay` + `= /dshs-overlay/bootstrap`(Host 改写成 `alotbuy.com`) |
|
||||
| 平台 env | `/etc/dshs.env`:`DSHS_BASE_DOMAIN=alotbuy.com`、`DSHS_COOKIE_DOMAIN=.alotbuy.com`、`DSHS_PORT=3080` |
|
||||
| 中继引导种子 | `lib/config.js` 内置 `['https://alotbuy.com/dshs-relay']`;env 覆盖键 = **`DSHS_OVERLAY_BOOTSTRAP_SEEDS`**(`splitList`,可多值) |
|
||||
| 证书签发 | certbot 1.22.0 + **dns-cloudflare** 插件;凭据 `/etc/cloudflare.ini`(**令牌仅覆盖 alotbuy.com 一个 zone**) |
|
||||
|
||||
**ai1net.com 侧已就绪的部分**:DNS 已在 Cloudflare(`ai1net.com` / `www` / `*` / `<x>.dsh` 均解析到 CF)、CF→源站(47.77.182.89)链路已验证可达(用 80 端口探针文件经 CF 取回原文)、LE **http-01 路径可用**。
|
||||
|
||||
## 1. 🔴 唯一阻塞项(需用户侧提供)
|
||||
|
||||
**`*.ai1net.com` 通配证书必须走 DNS-01**(LE 对通配符只认 DNS-01),而 47 上现存的 CF 令牌**只覆盖 alotbuy.com**(实测 `/zones` 只返回 1 个 zone)⇒ 我无法为 ai1net.com 写 `_acme-challenge` TXT。
|
||||
|
||||
两条可选路径(任选其一):
|
||||
- **A. 给一份 ai1net.com 域的 CF API 令牌**(权限最小化:`Zone → DNS → Edit`,Zone Resources = 仅 `ai1net.com`)
|
||||
→ 落到 `/etc/cloudflare-ai1net.ini`(`dns_cloudflare_api_token = <令牌>`,mode 600)
|
||||
→ 我执行 S1:`certbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/cloudflare-ai1net.ini --dns-cloudflare-propagation-seconds 60 -d ai1net.com -d '*.ai1net.com' --cert-name ai1net.com`
|
||||
- **B. 在 CF 面板生成 Origin CA 证书**(主机名填 `ai1net.com, *.ai1net.com`),把 cert + key 文本给我
|
||||
→ 落到 `/etc/ssl/ai1net/{fullchain.pem,privkey.pem}`,`ai1net.com.conf` 里两处证书路径改指它
|
||||
→ 无需给我任何 API 权限;代价是证书由 CF 侧管理、不参与 certbot 自动续期。
|
||||
|
||||
## 2. cutover 步骤(**证书就绪后**按序执行,每步带验收)
|
||||
|
||||
```bash
|
||||
V=/www/server/panel/vhost/nginx
|
||||
# S1 证书(见 §1;A 路径用 certbot,B 路径跳过)
|
||||
certbot certificates | grep -A3 ai1net.com
|
||||
|
||||
# S2 启用两份 vhost(⛔ 证书不存在时**不要**做这步 ⇒ nginx 起不来 = 门户全挂)
|
||||
cp -a $V/_pending-ai1net/ai1net.com.conf $V/ai1net.com.conf
|
||||
cp -a $V/_pending-ai1net/relay-direct.ai1net.com.conf $V/relay-direct.ai1net.com.conf
|
||||
nginx -t && nginx -s reload
|
||||
# 验收:curl -s --http1.1 -o /dev/null -w '%{http_code}\n' https://ai1net.com/portal.html ⇒ 200
|
||||
|
||||
# S3 平台 env 切换(备份 → 改两项 → 追加种子(加性,保留 alotbuy 作第二种子)→ 重启)
|
||||
cp -a /etc/dshs.env /etc/dshs.env.bak-dom-$(date +%Y%m%d-%H%M%S)
|
||||
sed -i 's/^DSHS_BASE_DOMAIN=.*/DSHS_BASE_DOMAIN=ai1net.com/' /etc/dshs.env
|
||||
sed -i 's/^DSHS_COOKIE_DOMAIN=.*/DSHS_COOKIE_DOMAIN=.ai1net.com/' /etc/dshs.env
|
||||
grep -q '^DSHS_OVERLAY_BOOTSTRAP_SEEDS=' /etc/dshs.env || \
|
||||
echo 'DSHS_OVERLAY_BOOTSTRAP_SEEDS=https://ai1net.com/dshs-relay,https://alotbuy.com/dshs-relay' >> /etc/dshs.env
|
||||
systemctl restart dshs
|
||||
# 验收:实例地址变成 <user>.ai1net.com 且能登录;中继仍在线(探针 29 PASS / 0 FAIL / 0 SKIP)
|
||||
|
||||
# S4 旧名连带项(**必须同批**,否则留半破状态)
|
||||
# S4a relay-direct.conf(alotbuy 版)的 Host 改写也必须指向新基底域:
|
||||
# proxy_set_header Host alotbuy.com; → ai1net.com
|
||||
# S4b dsh.alotbuy.com.conf(旧名 301)的 map 目标改指新域(⚠️ 只改两行,别碰 server_name/正则):
|
||||
# default alotbuy.com; → ai1net.com;
|
||||
# ~^(?<label>[^.]+)\.dsh\.alotbuy\.com$ $label.alotbuy.com; → $label.ai1net.com;
|
||||
nginx -t && nginx -s reload
|
||||
|
||||
# S5 worker 侧种子(可选,加性):/etc/dshs-worker.env 加同名多值 SEEDS,再 restart dshs-worker
|
||||
# ⚠️ 改前 `--scene all` 演练含观察窗口,预算 ≥8 min;⛔ 别套短超时
|
||||
```
|
||||
|
||||
## 3. 回滚(任一步失败都能退回)
|
||||
|
||||
1. 删两份新 vhost ⇒ `nginx -t && nginx -s reload`(门户立即回到 alotbuy 版)。
|
||||
2. `cp -a /etc/dshs.env.bak-dom-<ts> /etc/dshs.env` ⇒ `systemctl restart dshs`(回到 `alotbuy.com` / `.alotbuy.com`)。
|
||||
3. S4 的两处 sed 反向改回(alotbuy 版 vhost 与旧名 301 都可原样恢复)。
|
||||
4. 证书可留(不影响 alotbuy);`certbot delete --cert-name ai1net.com` 可彻底清掉。
|
||||
|
||||
## 4. 已知影响面(提前说明,非阻塞)
|
||||
|
||||
- **切换瞬间所有人需要在新域重登一次**:cookie 域由 `.alotbuy.com` 变 `.ai1net.com` ⇒ 旧 cookie 不再随请求发送(无法双域共存,一个 cookie 只能挂一个 Domain)。
|
||||
- **实例地址由 `<user>.alotbuy.com` 变为 `<user>.ai1net.com`**:旧地址在 alotbuy 版 vhost 仍可用(两套 vhost 并存期间);若要彻底退役旧域,再补一条 `alotbuy.com → ai1net.com` 的 301(本轮**不做**,留待你确认退役时机)。
|
||||
- `work.alotbuy.com`(Gitea · 154.40.35.3)**与本次无关**,不动。
|
||||
- 中继引导种子改成「新域为主、旧域为备」⇒ 两域任一存活都不影响 worker 入网。
|
||||
@@ -9,11 +9,13 @@ import time
|
||||
import urllib.request
|
||||
|
||||
CRED = '/etc/cloudflare.ini'
|
||||
# 2026-09-19 域迁 ai1net.com 后其 zone 走独立凭据(与旧域 token 不通用)
|
||||
CREDS = {'ai1net.com': '/etc/cloudflare-ai1net.ini'}
|
||||
API = 'https://api.cloudflare.com/client/v4'
|
||||
|
||||
|
||||
def token():
|
||||
txt = open(CRED, 'r', encoding='utf-8').read()
|
||||
def token(zone=None):
|
||||
txt = open(CREDS.get(zone or '', CRED), 'r', encoding='utf-8').read()
|
||||
m = re.search(r'dns_cloudflare_api_token\s*=\s*([A-Za-z0-9_\-]+)', txt)
|
||||
if not m:
|
||||
sys.exit('未找到 token')
|
||||
@@ -52,9 +54,9 @@ def show(title, name, typ):
|
||||
|
||||
|
||||
def main():
|
||||
zone = sys.argv[1] if len(sys.argv) > 1 else 'alotbuy.com'
|
||||
zone = sys.argv[1] if len(sys.argv) > 1 else 'ai1net.com'
|
||||
name = sys.argv[2] if len(sys.argv) > 2 else '_acme-challenge.' + zone
|
||||
tok = token()
|
||||
tok = token(zone)
|
||||
|
||||
zid = ''
|
||||
for z in (req('GET', '/zones?name=' + zone, tok).get('result') or []):
|
||||
|
||||
@@ -21,9 +21,9 @@ docs-consistency.py —— 文档库「事实一致性」校验(只读,可
|
||||
- 豁免(历史事实,只增不改):`04-调整方案/**`、`archive/**`、`01-规划与架构.md`、`02-运维手册.md`
|
||||
—— 它们写的时候那个值是对的,回改反而破坏历史。
|
||||
|
||||
⚠️ **刻意不查**「旧域名 dsh.alotbuy.com」「旧配额 512M」:这两者在库里几乎都是
|
||||
⚠️ **刻意不查**「旧域名 `dsh.alotbuy.com` / `alotbuy.com`」「旧配额 512M」:这些在库里几乎都是
|
||||
"旧域已 301" / "512M→384M" 的**合法历史表述**,正则无法可靠区分,误报率过高。
|
||||
改由人工在改域名/改配额时顺手核(档案 22 / 58 是权威)。
|
||||
改由人工在改域名/改配额时顺手核(档案 22 / 58 是权威;2026-09-19 域迁 `ai1net.com` 后旧域仅作过渡装置)。
|
||||
|
||||
用法:
|
||||
python3 scripts/docs-consistency.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: dsh-change-workflow
|
||||
description: dsh 多租户平台(alotbuy.com / dshs,服务器 47.77.182.89)功能改造与优化任务的完整工作流。当用户提出平台需求、改造、优化、缺陷修复、功能调研,或要求"按既有流程执行/沉淀/整理处理机制"时触发。核心:需求识别→调研→规划→开发→验证→归档清理 六阶段 + 红线机制(不自动升级 dsh / 不改官方主程序 / client bundle 禁 exports.default / 不用真实账号测登录 / **R7 禁未经确认的批量·全仓写入** / **R8 会中断在线用户的生产变更须先知会**)+ 备份先行 + append-only 日志 + 表格式交付 + 文档「服务器唯一源 + docs-status 单目录同步」。
|
||||
description: dsh 多租户平台(ai1net.com / dshs,服务器 47.77.182.89)功能改造与优化任务的完整工作流。当用户提出平台需求、改造、优化、缺陷修复、功能调研,或要求"按既有流程执行/沉淀/整理处理机制"时触发。核心:需求识别→调研→规划→开发→验证→归档清理 六阶段 + 红线机制(不自动升级 dsh / 不改官方主程序 / client bundle 禁 exports.default / 不用真实账号测登录 / **R7 禁未经确认的批量·全仓写入** / **R8 会中断在线用户的生产变更须先知会**)+ 备份先行 + append-only 日志 + 表格式交付 + 文档「服务器唯一源 + docs-status 单目录同步」。
|
||||
version: 2.9.2-playwright-banned
|
||||
updated_at: 2026-09-15
|
||||
last_change: 【2026-09-12 新增「三把锁」落地机制 + 更正过期指向】把「多任务并行调度协议」从**设计**补成**可强制执行的判据** —— 全局执行锁 `交接单/.exec-lock`(同一时刻只允许一个执行会话)+ 单级占用锁 `.doing-<单号>` + **服务器侧操作锁 `/opt/dsh/state/.op-lock/`**(管生产态:重启 / drain / 改实例 env·quota / 批量铺插件 / 改 nginx·nft·证书,因「服务器态变更看不出来」故必须显式加锁);工装 `scripts/handoff-guard.sh`(新增【1d】分支)与 `scripts/op-lock.sh`。同时**更正产出闭环里的过期指向**:原文写 `docs-status/文档库状态备注.md` + `bash scripts/docs-status-sync.sh --pull`,**该机制已不存在** → 改为 `INDEX.md §二` 登记 + 收尾四件套(audit → manifest → sync-check → `PUSH=1 handoff-guard`)。出处:当日 3 次并行事故 + `04-调整方案/69`。此前】**R7 禁止未经确认的批量 / 全仓写入**(由来:为"让 scp 出去的文件行尾干净",用脚本把 **147 个文件** CRLF→LF —— 当期只被要求改一个 UI 字符串;被用户指为「容易把服务器搞崩」,且随后 `cp -r` 确实把**未改动的** 2 个文件覆盖到了服务器)+ **R8 会中断在线用户的生产变更须先知会**(由来:档案 58/59 连续两次重启,直接引发用户"会话连接异常"报障)。【2026-09-11 晚,用户当面纠正后大修】① **阶段 0 新增第 0 条「开工前置检查」并标明"本技能最重要的一步"**(装工具/写自动化/查机制前先看:可用技能列表 → 本机/项目已有技能与记忆 → "本机已有的能否满足")+ 红线 **R6「先查已有资产,再动手」**;② **浏览器自动化定型**:用 `browser-harness`(CDP 9223,Windows 调用方式 + helper 清单),**禁再装 agent-browser / Playwright**;⛔ **铁律:browser-harness 附着的是「用户正在用的 Chrome」→ 动手前先 `list_tabs()`,不是全新空浏览器就立刻停手**(本轮真实事故:在其浏览器里登录测试号 → 覆盖用户 `.alotbuy.com` 的 `sid`);dsh composer 是 Lexical(`data-lexical-editor`),`type_text`/`fill_input` 均未生效;③ **更正「空闲回收」认知**:回收**从未生效**(TTL 默认 7 天 / cap 4 / 每请求 touch),但 **"回收没生效"≠"实例不会中断"** —— 中断真凶 = **服务重启(09-11 达 48 次)→ 旧实例变孤儿 → 另起新实例 → 旧页面 401** + **实例崩溃自动重启(近 4 天 69 次;admin 根因 `duplicate loader entry id: permission`)**;附三数核对手法;④ 阶段 1 新增「**用户报障第一步:先读 journal 拿真实失败请求**」(三分钟定位,判据速查 401/502/crash);⑤ 阶段 4 新增「**验证用例必须同时覆盖「有请求体」与「无请求体」**」(档案 51 事后修正实证:只测 POST 会整条漏掉 GET/SSE)+ 「模拟浏览器语义要测到底」(只带最初旧 cookie、显式断言 Set-Cookie 回写);⑥ R4 补**专用测试账号模板**(`role='active'`、`approved_by` 需真实 admin id;`users` 表无 `status` 列)。此前新增「**本环境 TLS 走中间人代理 → Python 脚本必须 SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt**」(两份 CA bundle 的区别 + 零平台改动正解 + 为何不走 R5 注入 env)+「职责边界:技能投放=admin 的事,平台只提供机制与把关」;此前 R5 追加「**安装类操作=扩大,必须先出安装确认清单(七项)**;技能/插件依赖体检只报告+拦截、绝不代装」;另把「复杂引号一律本地写文件→scp(禁 ssh 内 inline node -e/sed)」从建议升为**硬性要求**(本轮同类错误连犯 4 次)。此前补环境事实「正确域名 alotbuy.com(dsh.alotbuy.com 是旧域 301,抓页面只会拿到 301 HTML)」+ 档案 45 存量会话提示落点(login.html);此前新增「**基础运行时版本冻结**」节(档案 44:唯一缝隙=Python user-site 会盖平台包;修法 PYTHONNOUSERSITE + PYTHONUSERBASE 双保险;版本漂移巡检 scripts/runtime-baseline.cjs + cron 05:10);补「3.6 不开放」的正解(引导优先,遮蔽不可行)+ 属主自愈(ws-cleanup --reclaim-only,平台 root 跑 pnpm 污染用户 ws);**R5 已立**。此前新增红线 R5「权限可见面只准收窄,扩大必须先确认」**(含「R5 权限扩大门禁」整节:扩大/收窄方向判定表、权限影响评估四问、触发文件清单、以「实例可访问路径清单」做 diff 验收);阶段 2 红线自查与影响评估同步加入 R5 门禁;此前新增「**无法遮蔽 /usr 内文件**」(ro-bind 目录无法创建挂载点 → 叠加遮蔽会让实例起不来;判据「只有先 tmpfs 再白名单的目录才可自由增删」+ 改 bwrap 参数后必须真启动实例验证)+「**实例Line truncated
|
||||
@@ -13,12 +13,12 @@ dsh 多租户平台(服务器 47.77.182.89,dshs + dsh 0.1.2-rc.1)功能改
|
||||
|
||||
## 平台速查(硬编码事实,勿猜)
|
||||
|
||||
> ⚠️ **正确域名 = `alotbuy.com`**;`dsh.alotbuy.com` 是**旧域名、nginx 301 跳转**
|
||||
> ⚠️ **正确域名 = `ai1net.com`**(2026-09-19 由 `alotbuy.com` 迁入);`alotbuy.com` 是**旧域名、已降为 nginx 301 过渡装置**
|
||||
> ——对它抓页面只会拿到 301 HTML,**别误判为"改动没生效"**(2026-09-11 踩过)。
|
||||
> 平台页由门户直出(`127.0.0.1:3080`),生效副本只有 `/opt/dshs/web/` 一份。
|
||||
> 存量会话提示(档案 45)落在 `web/login.html`:告知「shell 频繁要求审批 → **新开一个会话**即可」。
|
||||
>
|
||||
> ⚠️ **`*.alotbuy.com`(含每个用户子域)都由同一个 vhost 先 `proxy_pass 127.0.0.1:3080`(门户)**,
|
||||
> ⚠️ **`*.ai1net.com`(含每个用户子域)都由同一个 vhost 先 `proxy_pass 127.0.0.1:3080`(门户)**,
|
||||
> 再由门户的 `src/supervisor/proxy.ts` 转发到实例 → **想改"子域访问行为"要改 `proxy.ts`,
|
||||
> nginx 的 `error_page` / `location` 到不了实例层**(档案 49 实测)。
|
||||
> 实例空闲回收后首次访问的链路(档案 49):**导航**(GET+`Accept: text/html`)→ 立刻 302 到
|
||||
@@ -73,7 +73,7 @@ dsh 多租户平台(服务器 47.77.182.89,dshs + dsh 0.1.2-rc.1)功能改
|
||||
> 🔴 **浏览器自动化:只允许两件工具;Playwright 全面禁止**(用户 2026-09-13 明令:「**playwright 禁止使用,加到规则中**」):
|
||||
> ① **`agent-browser`**(WorkBuddy 内置技能,**独立浏览器,不碰用户环境**)—— **本项目默认走这条**(2026-09-13 选定);
|
||||
> ② **`browser-harness`**(CDP **9223**,**附着用户正在用的 Chrome**)—— 只在需要看用户现场时用,
|
||||
> ⛔ **动手前必须先 `list_tabs()`**;旧事故:在其浏览器里登录测试号 → **覆盖了用户的 `.alotbuy.com` 的 `sid`**。
|
||||
> ⛔ **动手前必须先 `list_tabs()`**;旧事故:在其浏览器里登录测试号 → **覆盖了用户的 `.ai1net.com` 的 `sid`**。
|
||||
> ⛔ **禁止 Playwright / `playwright-core`**(含「独立无头」用法)—— 本行**更正**此前把它写成「只读验收默认路径」的错
|
||||
> (那是 2026-09-11 加的,与同页「禁 Playwright」**自相矛盾**;2026-09-13 已按用户明令统一为**禁止**)。
|
||||
> 静态页与 API 取数优先 `curl` / WebFetch,**别动浏览器**。
|
||||
@@ -83,7 +83,7 @@ dsh 多租户平台(服务器 47.77.182.89,dshs + dsh 0.1.2-rc.1)功能改
|
||||
| 服务器 | 47.77.182.89(Alibaba Cloud Linux al8);SSH:用别名 `bt-server`(`~/.ssh/config`:`HostName 47.77.182.89`、`Port 32022`、`User root`、`IdentityFile ~/.ssh/id_ed25519`) |
|
||||
| 门户 | dshs(systemd `dshs`,127.0.0.1:3080);源码 `/opt/dshs`(git master) |
|
||||
| 数据 | `/var/lib/dshs/users/<id>/{home,ws}`;DB `/var/lib/dshs/dshs.db`(root 600) |
|
||||
| 域名 | **alotbuy.com**(门户,CF 代理 + 通配证书,源站 443);用户子域 **`<用户名>.alotbuy.com`** 经 portal proxy 转发实例;旧 `dsh.alotbuy.com` 301 → `alotbuy.com`(档案 22 域名迁移) |
|
||||
| 域名 | **ai1net.com**(门户,CF 代理 + 通配证书,源站 443);用户子域 **`<用户名>.ai1net.com`** 经 portal proxy 转发实例;旧 `alotbuy.com` 301 → `ai1net.com`(档案 22 / 2026-09-19 迁入) |
|
||||
| 实例 | 门户 spawn `node /usr/local/bin/dsh --profile web --host 127.0.0.1 --port <动态>`;ISOLATION account(setpriv,uid∈[100000,199999];admin=114801) |
|
||||
| 文档 | **本机 git 工作树 = 唯一源**:`E://ProgramData//AI技能//aliyun-dsh-server//dsh-server-docs`(仓库 `dsh_shenxian_doc`)→ scp 到服务器 `/opt/dsh/docs`(root 600 / README 644,**无 .git 的部署镜像**)。⛔ **本行 2026-09-13 更正**:原文写「服务器唯一源 / 本地不保留副本 / `dsh-server-docs/` 已废弃」——那是**更早的状态,现已作废** |
|
||||
| 文档同步 | 改完跑**四件套**:`docs-audit.py` → `docs-index-stats.py --write`(INDEX 状态摘要**机器生成**)→ `docs-manifest.py` → `docs-sync-check.sh` → `docs-consistency.py`;**只 scp 自己本次改的文件**、推前复跑对账。⛔ `docs-status/` 机制**已不存在**(原文作废) |
|
||||
@@ -163,7 +163,7 @@ dsh 多租户平台(服务器 47.77.182.89,dshs + dsh 0.1.2-rc.1)功能改
|
||||
4. 本机直连验证(绕 DNS/代理):
|
||||
```bash
|
||||
# SECURE_COOKIES=true → 必须走 TLS;用 --resolve 把域名指到回环
|
||||
curl -sk --resolve alotbuy.com:443:127.0.0.1 https://alotbuy.com/api/...
|
||||
curl -sk --resolve ai1net.com:443:127.0.0.1 https://ai1net.com/api/...
|
||||
# 带登录态:-H "Cookie: sid=<token>"
|
||||
```
|
||||
(明文 `http://127.0.0.1:3080` 在 SECURE_COOKIES 下不通)
|
||||
@@ -205,13 +205,13 @@ dsh 多租户平台(服务器 47.77.182.89,dshs + dsh 0.1.2-rc.1)功能改
|
||||
|
||||
**browser-harness 会附着到本机**正在运行的**用户日常 Chrome**(CDP **9223**)——2026-09-11 实证:
|
||||
`BU_CDP_URL=9223` **不被尊重**(daemon 常驻共享,`BH_RUNTIME_DIR_SHARED=1`),换 `BU_NAME` + 非共享 runtime 重建 daemon 后
|
||||
`list_tabs()` **仍返回用户的 16 个标签页**(`admin.alotbuy.com` / `guest.alotbuy.com` / `portal.html#/plugins/manual`);
|
||||
`list_tabs()` **仍返回用户的 16 个标签页**(`admin.ai1net.com` / `guest.ai1net.com` / `portal.html#/plugins/manual`);
|
||||
`curl --noproxy '*' http://127.0.0.1:9223/json/list` 直接证实 **9223 就是用户浏览器的调试端口**。
|
||||
|
||||
**所以:脚本第一步永远是 `print([t["url"] for t in list_tabs()])`。**
|
||||
- 只要看到的**不是**你自己新开的空浏览器 → **立刻停手**,只做只读检查。
|
||||
- **禁止**在未确认隔离前做任何有副作用的动作(登录 / 输入 / 提交 / 关闭标签)。
|
||||
- 已造成的真实事故:在用户浏览器里 `fetch('/api/auth/login')` → **覆盖其 `.alotbuy.com` 的 `sid`**,用户在该浏览器里变成测试账号身份。
|
||||
- 已造成的真实事故:在用户浏览器里 `fetch('/api/auth/login')` → **覆盖其 `.ai1net.com` 的 `sid`**,用户在该浏览器里变成测试账号身份。
|
||||
补救 = 删掉测试账号(cookie 失效 → 用户重新登录即可)。**收尾必须 `close_tab` 关掉自己开的标签。**
|
||||
|
||||
### 正确调用(Windows,2026-09-11 实测可用)
|
||||
@@ -269,7 +269,7 @@ dsh 多租户平台(服务器 47.77.182.89,dshs + dsh 0.1.2-rc.1)功能改
|
||||
```
|
||||
|
||||
**登录态**:DB 直插临时 session(`user_agent=poc-ui`,用后即删),取 token 后
|
||||
`cdp("Network.setCookie", name="sid", value=TOKEN, domain=".alotbuy.com", path="/", secure=True)` → `Page.reload`。
|
||||
`cdp("Network.setCookie", name="sid", value=TOKEN, domain=".ai1net.com", path="/", secure=True)` → `Page.reload`。
|
||||
用户侧真实浏览器完全不受影响(不覆盖其 sid)。
|
||||
|
||||
**三个必踩坑**:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: dsh-decision-method
|
||||
description: dsh 多租户平台(alotbuy.com)「改造 / 优化功能交互 / UI 界面」的**决策方法论**。当用户提出一个新需求、问「这是不是最优方案 / 还有没有更好的做法」、要在多个方案里选型、要判断某个决策是否该做 / 该不该扩大范围、要**自主给技术实现选取最优解**、或者要复盘「为什么这么定」时使用。⛔ **用户点名「决策方法」/「参考决策方法」/「按你的规划」/「别问我」/「自行决策」/「自主决策」⇒ 必须立即加载本技能,不得凭记忆代替**(机制层由钩子 `dsh-server-docs/scripts/skill-load-guard.py` 强制注入加载提醒;2026-09-16 实证:用户点名后 AI 全程 `Skill` 调用 **0 次**)。核心 = 决策素材库(用户有效决策 **U1-U28** / AI 有效决策 A1-A25 / 反例 X1-X14,**素材源含 180 条用户真实发言**)+ 「如何确认最优解」的十问与判定矩阵 + **技术实现的默认裁决顺序(8 条,AI 自主用、不问用户)** + 决策流程十步 + 交互 UI 改造专项清单 + 决策语言对照表 + **复跑脚本 `extract-user-voice.py`**。**v2.0.0(2026-09-14)结构变更:素材库(U/A/X)已拆到 `references/`,按需读;本文件只留判定核心 + 触发词索引**(索引绑定可识别动作)。**与 dsh-change-workflow 分工:本技能管「怎么想、怎么定」,那个管「怎么落地」。** 与 dsh-feature-first 分工:那个管「谁定什么」,本技能管「怎么定得对」。
|
||||
description: dsh 多租户平台(ai1net.com)「改造 / 优化功能交互 / UI 界面」的**决策方法论**。当用户提出一个新需求、问「这是不是最优方案 / 还有没有更好的做法」、要在多个方案里选型、要判断某个决策是否该做 / 该不该扩大范围、要**自主给技术实现选取最优解**、或者要复盘「为什么这么定」时使用。⛔ **用户点名「决策方法」/「参考决策方法」/「按你的规划」/「别问我」/「自行决策」/「自主决策」⇒ 必须立即加载本技能,不得凭记忆代替**(机制层由钩子 `dsh-server-docs/scripts/skill-load-guard.py` 强制注入加载提醒;2026-09-16 实证:用户点名后 AI 全程 `Skill` 调用 **0 次**)。核心 = 决策素材库(用户有效决策 **U1-U28** / AI 有效决策 A1-A25 / 反例 X1-X14,**素材源含 180 条用户真实发言**)+ 「如何确认最优解」的十问与判定矩阵 + **技术实现的默认裁决顺序(8 条,AI 自主用、不问用户)** + 决策流程十步 + 交互 UI 改造专项清单 + 决策语言对照表 + **复跑脚本 `extract-user-voice.py`**。**v2.0.0(2026-09-14)结构变更:素材库(U/A/X)已拆到 `references/`,按需读;本文件只留判定核心 + 触发词索引**(索引绑定可识别动作)。**与 dsh-change-workflow 分工:本技能管「怎么想、怎么定」,那个管「怎么落地」。** 与 dsh-feature-first 分工:那个管「谁定什么」,本技能管「怎么定得对」。
|
||||
version: 2.8.0
|
||||
updated_at: 2026-09-16
|
||||
created_from: 本工作区 62 份改造档案 + 5 天工作日志(2026-09-08 ~ 09-12)全量提炼
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: dsh-feature-first
|
||||
description: dsh 多租户平台(alotbuy.com)的「功能优先」协作协议 —— **用户只提功能需求,AI 自主完成全部技术决策**。当用户提出任何平台功能/交互需求、报障、或说「我要把精力放在功能搭建上」时使用;也用于 AI 自查「这件事该不该拿去问用户」。核心 = 复盘证据(技术类+报障类占 64%)+ 用户的「功能卡」4 问 + **AI 自主决策的 9 类白名单(永不问)** + **只准上抛的 3 类(功能语义分叉 / 红线门禁 / 超出方法边界)** + **决策方法边界定义(边界外必须问的 8 类)** + 上抛语言转换表 + 报障闭环前置 + 交付回执格式 + 方法本身演进不再上抛 + **§3.6 拆包上抛:红线问题不得与技术方案捆在一起问** · **§3.2 红线按「实际影响面」判、不按「动作名字」判** · **§5.1 结论骨架(答「是否已实现 / 为什么不行」)** + **§5.3 铁律 4–5 / §5.4 硬约束 7–9:待确认项 = 回复的最后一节、逐条编号;每个候选各占一段(竖排、不横排)且必须写「优点 / 缺点」;只有优点或只有缺点 ⇒ 自决、不上抛**(2026-09-15 用户明令)。**配套:`dsh-decision-method`(怎么想)· `dsh-change-workflow`(怎么落地)。**
|
||||
description: dsh 多租户平台(ai1net.com)的「功能优先」协作协议 —— **用户只提功能需求,AI 自主完成全部技术决策**。当用户提出任何平台功能/交互需求、报障、或说「我要把精力放在功能搭建上」时使用;也用于 AI 自查「这件事该不该拿去问用户」。核心 = 复盘证据(技术类+报障类占 64%)+ 用户的「功能卡」4 问 + **AI 自主决策的 9 类白名单(永不问)** + **只准上抛的 3 类(功能语义分叉 / 红线门禁 / 超出方法边界)** + **决策方法边界定义(边界外必须问的 8 类)** + 上抛语言转换表 + 报障闭环前置 + 交付回执格式 + 方法本身演进不再上抛 + **§3.6 拆包上抛:红线问题不得与技术方案捆在一起问** · **§3.2 红线按「实际影响面」判、不按「动作名字」判** · **§5.1 结论骨架(答「是否已实现 / 为什么不行」)** + **§5.3 铁律 4–5 / §5.4 硬约束 7–9:待确认项 = 回复的最后一节、逐条编号;每个候选各占一段(竖排、不横排)且必须写「优点 / 缺点」;只有优点或只有缺点 ⇒ 自决、不上抛**(2026-09-15 用户明令)。**配套:`dsh-decision-method`(怎么想)· `dsh-change-workflow`(怎么落地)。**
|
||||
version: 1.7.1
|
||||
updated_at: 2026-09-16
|
||||
agent_created: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: dsh-instance-diagnose
|
||||
description: DSH 多租户平台(alotbuy.com / 47.77.182.89)单个用户实例的「故障诊断」技能,重点是内存 / OOM / 实例崩溃重启。当出现「实例打不开」「会话突然报错/中断」「跑着跑着断了」「响应慢」「怀疑内存不够」时触发。核心:先做第 0 层「跨机日志取证」(dshlog),再分「服务级 / 实例级 / 会话级」三层定位,再用 cgroup 内存三件套定量归因,最后在隔离 cgroup 里复现——**绝不在生产实例上做压力测试**。
|
||||
description: DSH 多租户平台(ai1net.com / 47.77.182.89)单个用户实例的「故障诊断」技能,重点是内存 / OOM / 实例崩溃重启。当出现「实例打不开」「会话突然报错/中断」「跑着跑着断了」「响应慢」「怀疑内存不够」时触发。核心:先做第 0 层「跨机日志取证」(dshlog),再分「服务级 / 实例级 / 会话级」三层定位,再用 cgroup 内存三件套定量归因,最后在隔离 cgroup 里复现——**绝不在生产实例上做压力测试**。
|
||||
version: 1.1.0
|
||||
updated_at: 2026-09-18
|
||||
last_change: 2026-09-15:死亡路径由 2 条扩到 4 条 —— 新增 C(bwrap 挂载点被嵌套 tmpfs 遮蔽(`Can't chdir to <正常路径>`))与 D(迁移复现不了启动参数(`Can't chdir to :` 空路径)),含分诊判据与「中间目录必须统一前置」的修法。
|
||||
@@ -215,7 +215,7 @@ TOKEN=$(ssh bt-server 'cd /opt/dshs && /usr/local/bin/node mksess-guest.cjs')
|
||||
ssh bt-server "cd /opt/dshs && /usr/local/bin/node -e \"const c=require('crypto');const D=require('/opt/dshs/node_modules/better-sqlite3');const db=new D('/var/lib/dshs/dshs.db');console.log(db.prepare('DELETE FROM sessions WHERE token_hash=?').run(c.createHash('sha256').update('$TOKEN').digest('hex')).changes);db.close();\""
|
||||
```
|
||||
|
||||
**工具**:`playwright-core` + `channel:'chrome'`(装在 `E:\ProgramData\.workbuddy\binaries\node\workspace`;须 `createRequire` 指向该目录,并在该目录内跑)。把 `sid` cookie 写进 context(`domain:'.alotbuy.com'`, `secure:true`, `sameSite:'None'`),再 `goto https://<user>.alotbuy.com/`。
|
||||
**工具**:`playwright-core` + `channel:'chrome'`(装在 `E:\ProgramData\.workbuddy\binaries\node\workspace`;须 `createRequire` 指向该目录,并在该目录内跑)。把 `sid` cookie 写进 context(`domain:'.ai1net.com'`, `secure:true`, `sameSite:'None'`),再 `goto https://<user>.ai1net.com/`。
|
||||
|
||||
**判定用的哨兵与元素(直接 `page.evaluate` 读)**:
|
||||
|
||||
|
||||
@@ -330,7 +330,7 @@ mksess.cjs ensure-role-profile-patch.cjs(临时会话 / profile 补丁)
|
||||
|
||||
| 类别 | 原值 | 替换为 | 命中 |
|
||||
|---|---|---|---|
|
||||
| 平台域名 | `dsh.alotbuy.com` / `*.alotbuy.com` / `alotbuy` | `<baseDomain>` | 3 文件 |
|
||||
| 平台域名 | `dsh.alotbuy.com` / `*.ai1net.com` / `ai1net` | `<baseDomain>` | 3 文件 |
|
||||
| ↳ **特例** | `web/wake.html` 的 `safeNext()` 正则 | 改成**运行时从 `location.hostname` 推导注册域**(`split('.')` 去最左一段),彻底去硬编码 | — |
|
||||
| 服务器公网 IP | `47.77.182.89` | `<SERVER_PUBLIC_IP>` | 1 |
|
||||
| 宿主内网 IP | `172.18.16.212` | `<HOST_LAN_IP>` | 1 |
|
||||
@@ -512,7 +512,7 @@ git rev-parse --short=12 HEAD
|
||||
1. **`text` 被重新赋值导致替换未落盘** —— 脱敏函数里 `text = text.replace(...)` 之后再用 `if text2 != text` 判「是否需要写盘」⇒ 全局替换**永远不落盘**(只落行级改动)。**必须留 `orig = text` 做比较基准。**
|
||||
2. **行级重写的 key 必须写「全局替换之后的文本」** —— 例:原行 `(档案 70 的 anysearch 事故…)`,key 要写 `(档案 70 的 该插件 事故…)`。用替换前的原文当 key ⇒ 静默不命中,留下半吊子句子。
|
||||
3. **行级重写会丢缩进** —— 按 `strip()` 匹配就必须把**原行的 indent 补回去**(多行替换值还要给后续行也加 indent),否则 TS 缩进错乱。
|
||||
4. **全局替换顺序敏感** —— `wake.html` 的专用规则必须在笼统的 `alotbuy → <baseDomain>` **之前**,否则专用规则永远不命中(会被先替换成 `<baseDomain>.com` 这种半成品)。
|
||||
4. **全局替换顺序敏感** —— `wake.html` 的专用规则必须在笼统的 `ai1net → <baseDomain>` **之前**,否则专用规则永远不命中(会被先替换成 `<baseDomain>.com` 这种半成品)。
|
||||
5. **重建会抹掉手写文件** —— 所以有 `OVERLAY`(先快照 `_overlay/` 后恢复)+ `--force` 安全闸。**验证过一次**:重建后 5/5 个 overlay 文件逐字节一致。
|
||||
6. **`.git` 绝不能带过去** —— 历史里含全部内部信息;必须是**全新仓库**。
|
||||
7. **Windows 下 `install.sh` 没有执行位** —— README 一律写 `sudo bash install.sh`;要执行位就 `chmod +x` 后再提交。
|
||||
@@ -555,7 +555,7 @@ cd "/e/ProgramData/AI技能/dsh-ai1net-github"
|
||||
"/e/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" _build_export.py --force
|
||||
|
||||
# 只跑探针/看结构(不重建)
|
||||
grep -rIlF "alotbuy" dsh-web-platform/ ; find dsh-web-platform -type f | wc -l
|
||||
grep -rIlF "ai1net" dsh-web-platform/ ; find dsh-web-platform -type f | wc -l
|
||||
|
||||
# 看某文件与源的差异
|
||||
diff -u "D:/github/dsh_shenxian/<f>" "dsh-web-platform/<f>"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: dsh-plugin-diagnose
|
||||
description: DSH 多租户平台(alotbuy.com / 47.77.182.89)**业务插件**的故障诊断技能 —— 专治「工具能用但浏览器里什么都没有」「原生绑定装不上」「明明改了却没生效」这类**静默失败**。当出现「插件没 UI / 预览不出现 / 卡片不渲染」「导入导出报 GLIBC 版本」「网关崩了但数据其实写进去了」「改了包上传了还是老行为」时触发。核心:先按 **host 半边 / client 半边 / 网关·原生绑定** 三层归属定位,再用「**inject 差集**」「**glibc 直测**」「**产物插探针**」三把尺子取证——**每一次都要有可复现的命令**。
|
||||
description: DSH 多租户平台(ai1net.com / 47.77.182.89)**业务插件**的故障诊断技能 —— 专治「工具能用但浏览器里什么都没有」「原生绑定装不上」「明明改了却没生效」这类**静默失败**。当出现「插件没 UI / 预览不出现 / 卡片不渲染」「导入导出报 GLIBC 版本」「网关崩了但数据其实写进去了」「改了包上传了还是老行为」时触发。核心:先按 **host 半边 / client 半边 / 网关·原生绑定** 三层归属定位,再用「**inject 差集**」「**glibc 直测**」「**产物插探针**」三把尺子取证——**每一次都要有可复现的命令**。
|
||||
version: 1.0.0
|
||||
updated_at: 2026-09-13
|
||||
last_change: 首版。由 2026-09-13 一整天 guest 实例 univer 插件四轮排查沉淀(worker socket 垫片被打包顺序废掉 / 宿主 glibc 2.32 < 绑定要求 2.35 / 网关提交 changeset 也建投影致其崩溃 / client 半边因一条不可满足的 inject 永久挂起),含 4 类真因、3 套取证配方与 8 条实测踩坑。
|
||||
@@ -38,7 +38,7 @@ agent_created: true
|
||||
1. 取实例页面(**不碰用户浏览器**,用临时会话走平台代理):
|
||||
```bash
|
||||
SID=$(node /opt/dshs/mksess-guest.cjs) # 需对应用户;用完必须删会话
|
||||
curl -s -H "Host: <用户名>.alotbuy.com" -H "Cookie: sid=$SID" http://127.0.0.1:3080/ -o page.html
|
||||
curl -s -H "Host: <用户名>.ai1net.com" -H "Cookie: sid=$SID" http://127.0.0.1:3080/ -o page.html
|
||||
```
|
||||
2. 从页面里取**实际下发的客户端插件集合**(权威清单,别用 manifest 正则——会漏行):
|
||||
```python
|
||||
|
||||
Reference in new issue
Block a user