Files
Toolbox/dev_test_scripts/debug/debug_token_lab_emergency_ui.js
T
yangxiangyuan a3e8911deb feat: 新增作息报时器功能并替换短链接前缀为/to/
- 新增作息报时器独立模块,包含完整的日程管理、语音播报和待机功能
- 短链接正式前缀由/l/改为/to/,解决手机小屏下与I、i、1字形混淆问题
- 保留旧前缀/l/、/L/、/I/、/i/兼容已分发的旧短链
- 新增短码大小写兜底匹配,仅唯一匹配时生效避免歧义
- 新增防爆紧急替换功能,可轮换首页鉴权并替换短链接
- 新增多个调试和集成测试脚本,保障数据安全和功能回归
2026-10-09 14:05:36 +08:00

235 lines
12 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.
/**
* 「首页导航防爆紧急替换」页签 · 前端界面验证
*
* 目的:验证页签位置、两个按钮、滚动日志、最终 token / 短链接展示都正常工作。
*
* ⚠️ 本脚本不真实触发换密钥:对 /api/token_lab/nav/emergency/* 做请求拦截,
* 返回预置的假响应,只验证前端渲染与交互,绝不改动生产的密钥与短链库。
*
* 运行:node dev_test_scripts/debug/debug_token_lab_emergency_ui.js
*/
const fs = require('fs')
const puppeteer = require('puppeteer')
const BASE = process.env.TL_BASE || 'http://127.0.0.1:8976'
const CHROME_CANDIDATES = [
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
]
const findChrome = () => CHROME_CANDIDATES.find(p => fs.existsSync(p)) || undefined
const sleep = ms => new Promise(r => setTimeout(r, ms))
let passed = 0
let failed = 0
const check = (name, ok, detail) => {
if (ok) { passed += 1; console.log(` ✔ ${name}`) }
else { failed += 1; console.log(` ✘ ${name}${detail ? ' → ' + detail : ''}`) }
}
const FAKE_TOKEN = 'eyJmYWtlLXRva2VuLWZvci11aS10ZXN0.' + 'A'.repeat(120) + '.signature'
const FAKE_SHORT = 'https://maise.pro/to/UiTest1'
const ROTATE_LOGS = [
'【第 1 步】轮换导航密钥 + 签发新 token',
'目标有效期:30 天',
'已备份原有文件 → config/_nav-key-backup-2026-09-30T16-00-00/(nav-private.jwk.json、nav.jwk.json、short_link.db)',
'已生成新的 RSA-2048 密钥对(公钥指纹 abcdef1234567890)',
'已写入 config/nav-private.jwk.json(含私钥,签发用)',
'已写入 config/nav.jwk.json(仅公钥,信任池用)',
'两处已换成同一把新钥匙 → 旧公钥彻底离开信任池,旧 token 自此刻起失效',
'已用新私钥签发 token(iss=TRAE-NAV,jti=fake-jti)',
'自校验:签名=通过|有效期=正常|iss=通过',
'完成 ✅ 新 token 过期时间:2026/10/30 16:00:00(北京时间)',
]
const RELINK_LOGS = [
'【第 2 步】更换首页短链接',
'正在操作短链接数据库 data/short_link.db',
'token 校验通过(jti=fake-jti,过期 2026/10/30 16:00:00)',
'已停用旧短链 /to/PSq6xv → 立即失效',
'已创建新短链:https://maise.pro/to/UiTest1',
'自校验:新短链返回 302,跳转目标与新 token 一致 ✅',
'完成 ✅ 最终短链接:https://maise.pro/to/UiTest1',
]
;(async () => {
const browser = await puppeteer.launch({
headless: true, executablePath: findChrome(),
args: ['--no-sandbox', '--disable-setuid-sandbox'],
})
const page = await browser.newPage()
await page.setViewport({ width: 1280, height: 1000 })
await page.setCookie(
{ name: 'nav_gate', value: '1', domain: '127.0.0.1', path: '/' },
{ name: 'token_lab_gate', value: '1', domain: '127.0.0.1', path: '/' },
)
const pageErrors = []
page.on('pageerror', e => pageErrors.push(String(e && e.message || e)))
// 拦截两个紧急接口 → 返回预置响应,绝不真实执行
await page.setRequestInterception(true)
let rotateHit = 0
let relinkHit = 0
let rotateBody = null
page.on('request', req => {
const url = req.url()
if (url.includes('/api/token_lab/nav/emergency/rotate')) {
rotateHit += 1
try { rotateBody = JSON.parse(req.postData() || '{}') } catch { rotateBody = null }
return req.respond({
status: 200, contentType: 'application/json',
body: JSON.stringify({
ok: true, token: FAKE_TOKEN, exp: Math.floor(Date.now() / 1000) + 30 * 86400,
months: (rotateBody && rotateBody.months) || 1,
days: ((rotateBody && rotateBody.months) || 1) * 30,
fingerprint: 'abcdef1234567890', backupDir: '_nav-key-backup-2026-09-30T16-00-00', logs: ROTATE_LOGS,
}),
})
}
if (url.includes('/api/token_lab/nav/emergency/relink')) {
relinkHit += 1
return req.respond({
status: 200, contentType: 'application/json',
body: JSON.stringify({
ok: true, code: 'UiTest1', shortUrl: FAKE_SHORT, disabledCodes: ['PSq6xv'], verified: true, logs: RELINK_LOGS,
}),
})
}
return req.continue()
})
console.log('=== 防爆紧急替换页签 · 前端界面验证(接口已拦截,不会真实执行) ===\n')
await page.goto(`${BASE}/tools/token_lab/index.html`, { waitUntil: 'networkidle2', timeout: 30000 })
await page.waitForFunction(() => {
const m = document.getElementById('appMain')
return m && !m.hidden
}, { timeout: 15000 })
// ---------- 1. 页签位置与文案 ----------
console.log('[1] 页签与按钮')
const tabs = await page.$$eval('.tab-btn', els => els.map(e => ({ text: e.textContent.trim(), tab: e.dataset.tab, active: e.classList.contains('active') })))
check('共有 3 个页签', tabs.length === 3, JSON.stringify(tabs.map(t => t.text)))
check('新页签排在第一位', tabs[0] && tabs[0].tab === 'emergency', JSON.stringify(tabs[0] || {}))
check('新页签名称正确', tabs[0] && tabs[0].text === '首页导航防爆紧急替换', tabs[0] && tabs[0].text)
check('原「首页导航生成器」仍在且为默认激活', tabs[1] && tabs[1].tab === 'nav' && tabs[1].active === true, JSON.stringify(tabs[1] || {}))
check('原「Tool 令牌生成器」仍在', tabs[2] && tabs[2].tab === 'tool')
const btnTexts = await page.evaluate(() => ({
b1: document.getElementById('btnEmergencyRotate') ? document.getElementById('btnEmergencyRotate').textContent.trim() : '',
b2: document.getElementById('btnEmergencyRelink') ? document.getElementById('btnEmergencyRelink').textContent.trim() : '',
}))
check('按钮 1 文案为动态月份(默认 1 个月)',
btnTexts.b1 === '自动生成向后 1 个月的鉴权,且当前鉴权立即失效', btnTexts.b1)
check('按钮 2 文案正确', btnTexts.b2 === '将新的首页鉴权token,自动生成新的短链接,且当前短链接立即失效', btnTexts.b2)
// ---------- 1b. 月份下拉 ----------
console.log('\n[1b] 有效期月份下拉(1~12,默认 1)')
const selInfo = await page.evaluate(() => {
const sel = document.getElementById('emergencyMonths')
if (!sel) return null
return {
exists: true,
value: sel.value,
options: Array.from(sel.options).map(o => o.value),
labels: Array.from(sel.options).map(o => o.textContent.trim()),
// 是否紧跟按钮 1 之后
afterButton: (() => {
const btn = document.getElementById('btnEmergencyRotate')
return !!(btn && btn.nextElementSibling === sel)
})(),
}
})
check('下拉存在', !!(selInfo && selInfo.exists))
check('默认选中 1 个月', selInfo && selInfo.value === '1', selInfo && selInfo.value)
check('共 12 个选项', !!(selInfo && selInfo.options.length === 12), selInfo && selInfo.options.length)
check('选项值为 1~12',
!!(selInfo && JSON.stringify(selInfo.options) === JSON.stringify(['1','2','3','4','5','6','7','8','9','10','11','12'])),
selInfo && JSON.stringify(selInfo.options))
check('选项文案形如「N 个月」', !!(selInfo && selInfo.labels[0] === '1 个月' && selInfo.labels[11] === '12 个月'),
selInfo && JSON.stringify(selInfo.labels))
check('下拉位于按钮 1 之后(同一行)', !!(selInfo && selInfo.afterButton))
// 切换下拉 → 按钮文案应跟着变
await page.select('#emergencyMonths', '6')
await sleep(200)
const btnAfterChange = await page.evaluate(() => document.getElementById('btnEmergencyRotate').textContent.trim())
check('切到 6 个月后按钮文案同步更新',
btnAfterChange === '自动生成向后 6 个月的鉴权,且当前鉴权立即失效', btnAfterChange)
await page.select('#emergencyMonths', '12')
await sleep(200)
const btnAfter12 = await page.evaluate(() => document.getElementById('btnEmergencyRotate').textContent.trim())
check('切到 12 个月后按钮文案同步更新',
btnAfter12 === '自动生成向后 12 个月的鉴权,且当前鉴权立即失效', btnAfter12)
// ---------- 2. 切到新页签 ----------
console.log('\n[2] 切换到新页签')
await page.evaluate(() => document.querySelector('.tab-btn[data-tab="emergency"]').click())
await sleep(250)
const panelShown = await page.evaluate(() => {
const p = document.querySelector('.tab-panel[data-panel="emergency"]')
return !!p && p.classList.contains('active') && window.getComputedStyle(p).display !== 'none'
})
check('新页签面板已显示', panelShown)
// ---------- 3. 点按钮 1 ----------
console.log('\n[3] 点击按钮 1(自动生成向后一个月的鉴权…)')
await page.click('#btnEmergencyRotate')
await sleep(2500) // 等日志逐行播放完
const after1 = await page.evaluate(() => ({
log: document.getElementById('emergencyLog').value,
token: document.getElementById('emergencyTokenOut').value,
meta: document.getElementById('emergencyMeta').innerText,
}))
check('确实调用了 rotate 接口', rotateHit === 1, `命中 ${rotateHit} 次`)
check('请求体带上了所选月份 months=12', !!(rotateBody && Number(rotateBody.months) === 12),
JSON.stringify(rotateBody))
check('日志框出现滚动内容', after1.log.length > 100, `长度 ${after1.log.length}`)
check('日志含第 1 步标题', after1.log.includes('【第 1 步】'))
check('日志含"完成"', after1.log.includes('完成 ✅'))
check('日志逐行带时间戳', /\[\d{2}:\d{2}:\d{2}\]/.test(after1.log))
check('最终 token 已展示', after1.token === FAKE_TOKEN, `长度 ${after1.token.length}`)
check('元信息展示公钥指纹', after1.meta.includes('abcdef1234567890'), after1.meta.slice(0, 80))
check('元信息展示有效期 12 个月', after1.meta.includes('12 个月'), after1.meta.slice(0, 120))
check('执行后按钮文案恢复为所选月份',
(await page.evaluate(() => document.getElementById('btnEmergencyRotate').textContent.trim())) === '自动生成向后 12 个月的鉴权,且当前鉴权立即失效')
// ---------- 4. 点按钮 2 ----------
console.log('\n[4] 点击按钮 2(将新 token 绑到新短链…)')
await page.click('#btnEmergencyRelink')
await sleep(2000)
const after2 = await page.evaluate(() => ({
log: document.getElementById('emergencyLog').value,
shortUrl: document.getElementById('emergencyShortUrlOut').value,
meta: document.getElementById('emergencyMeta').innerText,
}))
check('确实调用了 relink 接口', relinkHit === 1, `命中 ${relinkHit} 次`)
check('日志含第 2 步标题', after2.log.includes('【第 2 步】'))
check('日志含停用旧短链步骤', after2.log.includes('已停用旧短链'))
check('最终短链接已展示', after2.shortUrl === FAKE_SHORT, after2.shortUrl)
check('元信息展示已停用的旧短码', after2.meta.includes('PSq6xv'), after2.meta.slice(0, 120))
check('元信息展示自校验结果', after2.meta.includes('通过'), after2.meta.slice(0, 120))
// ---------- 5. 其它页签未受影响 ----------
console.log('\n[5] 原有功能未受影响')
for (const t of ['nav', 'tool']) {
await page.evaluate(tab => document.querySelector(`.tab-btn[data-tab="${tab}"]`).click(), t)
await sleep(150)
const ok = await page.evaluate(tab => {
const p = document.querySelector(`.tab-panel[data-panel="${tab}"]`)
return !!p && p.classList.contains('active')
}, t)
check(`原页签 ${t} 仍可切换`, ok)
}
check('页面无 JS 报错', pageErrors.length === 0, pageErrors.slice(0, 3).join(' | '))
await browser.close()
console.log(`\n=== 结果:通过 ${passed} 项,失败 ${failed} 项 ===`)
process.exit(failed === 0 ? 0 : 1)
})().catch(e => {
console.log('\n✘ 执行异常:' + String((e && e.stack) || e))
process.exit(1)
})