三条线合并入库 —— 均已完成并上线(源码与生产一致,此前只部署未入仓)。 ⚠️ 其中域名迁移线为**另一会话**产出,本会话只做入库、**未复验其正确性**(它自报零回归)。 【档案 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。
94 lines
3.8 KiB
JavaScript
94 lines
3.8 KiB
JavaScript
/**
|
||
* `web/i18n.js` 的品牌文案渲染回归(2026-09-19,档案 134 配套)。
|
||
*
|
||
* 为什么单独钉这一条:品牌标识从 DeepSeek 图形换成了**文字**(中文「能力枢纽」/
|
||
* 英文及其他语言 `CapabilityNet`)。词条漏配或 key 写错时,i18n 会**静默回退英文**
|
||
* —— 页面上看不出报错,只有中文用户看到英文名。这类"静默失败"必须靠测试钉住。
|
||
*
|
||
* 做法:用 `node:vm` 跑**仓库里那份真实的 `web/i18n.js`**(不是复制一遍逻辑),
|
||
* 配一个够用的最小 DOM 桩,断言六种语言解析路径下的**实际渲染结果**。
|
||
*
|
||
* ⚠️ 桩里必须注入 `URLSearchParams` / `URL`:`vm` 上下文只含 ECMAScript 内置、
|
||
* 不含 Web API;少了它们 `new URLSearchParams(location.search)` 会抛 ReferenceError,
|
||
* 被 i18n.js 的 try/catch 吞掉 ⇒ 表现为"`?lang=` 不生效"的**假失败**(第一版踩过)。
|
||
*/
|
||
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { readFileSync } from 'node:fs'
|
||
import vm from 'node:vm'
|
||
|
||
const SRC = readFileSync(new URL('../web/i18n.js', import.meta.url), 'utf8')
|
||
|
||
/** 造一个够 i18n.js 用的最小元素桩(只需要 getAttribute / textContent)。 */
|
||
function makeEl(attrs = {}) {
|
||
return {
|
||
_attrs: { ...attrs },
|
||
textContent: attrs.__text ?? '',
|
||
className: '',
|
||
innerHTML: '',
|
||
style: {},
|
||
getAttribute(name) {
|
||
return Object.prototype.hasOwnProperty.call(this._attrs, name) ? this._attrs[name] : null
|
||
},
|
||
setAttribute(name, value) {
|
||
this._attrs[name] = value
|
||
},
|
||
appendChild() {},
|
||
addEventListener() {},
|
||
querySelectorAll() {
|
||
return []
|
||
},
|
||
}
|
||
}
|
||
|
||
/** 在最小 DOM 桩里跑一次 i18n.js,返回品牌节点的实际渲染结果与解析出的语言。 */
|
||
function renderBrand({ search = '', cookie = '', language = 'en-US' } = {}) {
|
||
const brandNode = makeEl({ 'data-i18n': 'brand.name', __text: 'CapabilityNet' })
|
||
const sandbox = {
|
||
console,
|
||
setTimeout: () => {},
|
||
URLSearchParams,
|
||
URL,
|
||
document: {
|
||
readyState: 'complete',
|
||
cookie,
|
||
documentElement: makeEl(),
|
||
querySelectorAll(selector) {
|
||
return selector === '[data-i18n]' ? [brandNode] : []
|
||
},
|
||
createElement: () => makeEl(),
|
||
addEventListener: () => {},
|
||
},
|
||
navigator: { language, languages: [language] },
|
||
location: { search, href: `https://ai1net.com/login.html${search}`, reload: () => {} },
|
||
window: {},
|
||
}
|
||
sandbox.window = sandbox
|
||
vm.createContext(sandbox)
|
||
vm.runInContext(SRC, sandbox, { filename: 'web/i18n.js' })
|
||
return { brand: brandNode.textContent, lang: sandbox.window.I18N.lang }
|
||
}
|
||
|
||
test('品牌文案:中文显示「能力枢纽」,英文及其他语言显示 CapabilityNet', () => {
|
||
const cases = [
|
||
['默认(浏览器 en-US)', { language: 'en-US' }, 'CapabilityNet'],
|
||
['中文(浏览器 zh-CN)', { language: 'zh-CN' }, '能力枢纽'],
|
||
['中文(?lang=zh)', { search: '?lang=zh', language: 'en-US' }, '能力枢纽'],
|
||
['英文(?lang=en 覆盖中文浏览器)', { search: '?lang=en', language: 'zh-CN' }, 'CapabilityNet'],
|
||
['cookie dsh_lang=zh', { cookie: 'dsh_lang=zh', language: 'en-US' }, '能力枢纽'],
|
||
['未支持语言(ja)⇒ 回退英文', { language: 'ja-JP' }, 'CapabilityNet'],
|
||
]
|
||
for (const [label, opts, expected] of cases) {
|
||
const got = renderBrand(opts)
|
||
assert.equal(got.brand, expected, label)
|
||
}
|
||
})
|
||
|
||
test('品牌文案:不会退化成裸 key、不留空', () => {
|
||
for (const language of ['en-US', 'zh-CN', 'ja-JP', 'de-DE']) {
|
||
const { brand } = renderBrand({ language })
|
||
assert.notEqual(brand, 'brand.name', `裸 key 泄露(${language})`)
|
||
assert.ok(brand.trim() !== '', `空文案(${language})`)
|
||
}
|
||
})
|