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

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

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

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

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

107 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* verify-static.mjs —— 静态页不变量校验(2026-09-13 新增)
*
* 为什么需要:平台有 9 个静态页,其中 wake.html 承担"启动过渡页"(有 5 个 id 被内联 JS 依赖),
* 而"去平台痕迹"(用户可见面不得出现 dshs)是个**容易回归**的约束 ——
* 新人加个页面忘了改名就会漏出去。把它变成 CI 可跑的判据。
*
* 用法:node scripts/verify-static.mjs 退出码 0=全绿 / 1=有违规
*/
import { readFileSync, readdirSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
const WEB = join(ROOT, 'web')
const BANNED = 'dshs' // 用户可见面不得出现的平台内部名
const WAKE_IDS = ['spin', 'step', 'acts', 'retry', 'note'] // wake.html 内联 JS 依赖的 id
// 档案 81 · R5:已完成多语言迁移的页面(**迁移一页加一个**)。这些页不得再出现裸中文文案。
const I18N_PAGES = ['login.html', 'register.html', 'wake.html', 'index.html']
let bad = 0
const pages = readdirSync(WEB).filter((f) => f.endsWith('.html'))
console.log('=== 静态页不变量(' + pages.length + ' 页)===')
for (const f of pages) {
const s = readFileSync(join(WEB, f), 'utf8')
const problems = []
const title = /<title[^>]*>([^<]*)<\/title>/.exec(s)
if (!title || title[1].trim() === '') problems.push('缺 <title> 或为空')
else if (title[1].includes(BANNED)) problems.push('title 含内部平台名: ' + title[1])
if (s.includes(BANNED)) problems.push('页面正文含内部平台名(去痕迹约束)')
// 档案 81 · R5:**已迁移多语言的页面不得再出现裸中文文案** —— 用户可见文案必须走词条
// (`data-i18n` / `I18N.t`)。注释不算文案,故先剥掉 HTML 与 JS 注释再判。
// 只对白名单页生效 ⇒ 未迁移页仍允许中文,不会误拦(迁移一页就往 I18N_PAGES 加一个)。
if (I18N_PAGES.includes(f)) {
const stripped = s
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/^[ \t]*\/\/.*$/gm, '')
.replace(/[ \t]\/\/[^\n]*/g, '') // 行尾注释(`code // 说明`)也要剥(`https://` 不会被误伤:左邻是 `:` 不是空格)
const cjk = stripped.split('\n').map((l, i) => [i + 1, l]).filter(([, l]) => /[\u4e00-\u9fa5]/.test(l))
if (cjk.length) {
problems.push('已迁移多语言但仍有裸中文文案(行 ' + cjk.slice(0, 3).map(([n]) => n).join('/') + '…)')
}
if (!s.includes('/i18n.js')) problems.push('已迁移多语言但未引入 /i18n.js')
}
if (f === 'wake.html') {
for (const id of WAKE_IDS) {
if (!new RegExp('id="' + id + '"').test(s)) problems.push('缺 id="' + id + '"(内联 JS 依赖)')
}
if (!s.includes('instance_circuit_open')) problems.push('缺档案 78 的熔断提示分支')
}
if (problems.length) { bad++; console.log(' ✗ ' + f + ':' + problems.join(';')) }
else console.log(' ✓ ' + f + (title ? '(title=' + title[1] + ')' : ''))
}
// 内联 <script> 必须能被 JS 解析(2026-09-13 事故:脚本语法错误 = 静默失效)
import { writeFileSync, mkdtempSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { execFileSync } from 'node:child_process'
const tmp = mkdtempSync(join(tmpdir(), 'vstatic-'))
for (const f of pages) {
const s = readFileSync(join(WEB, f), 'utf8')
const blocks = [...s.matchAll(/<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/g)].map((m) => m[1])
blocks.forEach((code, i) => {
const file = join(tmp, f.replace(/\W/g, '_') + '.' + i + '.js')
writeFileSync(file, code)
try { execFileSync(process.execPath, ['--check', file], { stdio: 'pipe' }); console.log(' ✓ ' + f + ' 内联脚本#' + i + ' 语法通过') }
catch (e) { bad++; console.log(' ✗ ' + f + ' 内联脚本#' + i + ' **语法失败**:' + String(e.stderr || e).split('\n').slice(0, 2).join(' ')) }
})
}
// CSS/JS 资源也要过一遍去痕迹约束
for (const f of readdirSync(WEB).filter((f) => /\.(css|js)$/.test(f))) {
const s = readFileSync(join(WEB, f), 'utf8')
if (s.includes(BANNED)) { bad++; console.log(' ✗ ' + f + ':含内部平台名') }
else console.log(' ✓ ' + f)
}
// SVG 资产单独过一遍(2026-09-19 档案 137 新增)。
// 为什么必须查:**XML 注释里不允许出现两个连续 ASCII 连字符**,而 SVG 文件里写 CSS 变量名
// (双连字符加名字)是最自然的写法 ⇒ 一旦踩中,**整份 SVG 解析失败、图标静默不显示**,
// 页面与接口都不报任何错。第一版 favicon 就栽在这里(注释里写了 `--ink`),
// 是靠"用真解析器验一遍"才抓到的 ⇒ 把它做成可重跑的判据,而不是靠人记得。
for (const f of readdirSync(WEB).filter((f) => f.endsWith('.svg'))) {
const s = readFileSync(join(WEB, f), 'utf8')
const problems = []
if (s.includes(BANNED)) problems.push('含内部平台名')
if (!/^\s*<svg[\s>]/.test(s)) problems.push('未以 <svg 开头')
if (!s.trimEnd().endsWith('</svg>')) problems.push('未以 </svg> 结尾')
if (!/viewBox="0 0 \d+ \d+"/.test(s)) problems.push('缺 viewBox(缩放不可控)')
for (const m of s.matchAll(/<!--([\s\S]*?)-->/g)) {
if (m[1].includes('--')) {
problems.push('XML 注释含双连字符(XML 语法不允许 ⇒ 整份 SVG 会解析失败、图标静默不显示)')
}
}
if (problems.length) { bad++; console.log(' ✗ ' + f + ':' + problems.join(';')) }
else console.log(' ✓ ' + f)
}
console.log(bad ? '结论:' + bad + ' 项不合格 ❌' : '结论:全部合格 ✅')
process.exit(bad ? 1 : 0)