三条线合并入库 —— 均已完成并上线(源码与生产一致,此前只部署未入仓)。 ⚠️ 其中域名迁移线为**另一会话**产出,本会话只做入库、**未复验其正确性**(它自报零回归)。 【档案 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。
107 lines
5.7 KiB
JavaScript
107 lines
5.7 KiB
JavaScript
#!/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)
|