feat: 重构定投评估工具并优化Cloud工具体验
- 重构市场定投评估逻辑:优先读取本地估值快照,必要时通过探针补抓海外数据,后端接口新增force参数支持强制刷新,更新前后端注释与交互文案 - 优化Cloud工具全链路: - 新增运营商登录页地址字段,支持一键复制账号密码并打开对应登录页 - 缓存密钥派生结果,避免重复阻塞事件循环,提升列表加载性能 - 完善接口错误处理,新增面板级加载失败提示与自动重试功能 - 修复云资源代理转发时的Cookie缺失问题 - 并行加载所有面板数据,优化首屏渲染速度 - 新增登录按钮样式与错误提示样式 - 新增4个Cloud工具调试脚本,用于性能测试、解密审计、回归验证与加载耗时统计 - 启动定投评估工具的定时调度任务
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
// 调试脚本:逐行审计 cloud ecs/vas 加密字段,区分"明文原本为空"与"解密失败"
|
||||
// 用法:node dev_test_scripts/debug/debug_cloud_decrypt_audit.js
|
||||
const crypto = require('crypto')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
|
||||
// 与 index.js 相同的凭据加载逻辑
|
||||
const userProfile = process.env.USERPROFILE || process.env.HOME || os.homedir()
|
||||
const credsPath = path.join(userProfile, 'Toolbox_local_creds.env.local')
|
||||
if (fs.existsSync(credsPath)) {
|
||||
fs.readFileSync(credsPath, 'utf-8').split(/\r?\n/).forEach(line => {
|
||||
const t = line.trim()
|
||||
if (!t || t.startsWith('#')) return
|
||||
const i = t.indexOf('=')
|
||||
if (i <= 0) return
|
||||
const k = t.slice(0, i).trim()
|
||||
const v = t.slice(i + 1).trim()
|
||||
if (k && !Object.prototype.hasOwnProperty.call(process.env, k)) process.env[k] = v
|
||||
})
|
||||
console.log('creds loaded from', credsPath)
|
||||
} else {
|
||||
console.log('WARN: creds file not found')
|
||||
}
|
||||
|
||||
const { db } = require('../../src/server/db')
|
||||
const cfg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'config', 'cloud.json'), 'utf-8'))
|
||||
const saltStr = String(cfg.machine_salt || '')
|
||||
const passphrase = process.env.CLOUD_PASSPHRASE || process.env.SYSTEM_AUTH_PASS_93220 || ''
|
||||
console.log('passphrase present:', !!passphrase)
|
||||
const key = crypto.scryptSync(passphrase + saltStr, Buffer.from(saltStr || 'default', 'utf-8'), 32)
|
||||
|
||||
const audit = (table, ec, ivc, tagc) => {
|
||||
const rows = db.prepare(`SELECT id, ${ec}, ${ivc}, ${tagc} FROM ${table}`).all()
|
||||
let empty = 0
|
||||
let fail = 0
|
||||
let ok = 0
|
||||
for (const r of rows) {
|
||||
if (!r[ec] || !r[ivc] || !r[tagc]) continue
|
||||
try {
|
||||
const d = crypto.createDecipheriv('aes-256-gcm', key, r[ivc])
|
||||
d.setAuthTag(r[tagc])
|
||||
const pt = Buffer.concat([d.update(r[ec]), d.final()]).toString('utf-8')
|
||||
if (pt === '') { empty++; console.log(`${table} id=${r.id}: 解密成功但明文为空(原本就没填)`) } else ok++
|
||||
} catch (e) { fail++; console.log(`${table} id=${r.id}: 解密失败 ${String(e.message)}`) }
|
||||
}
|
||||
console.log(`${table}: ok_non_empty=${ok} empty_plaintext=${empty} auth_fail=${fail}`)
|
||||
}
|
||||
|
||||
audit('cloud_operators', 'login_password_enc', 'login_password_iv', 'login_password_tag')
|
||||
audit('cloud_ecs', 'rdp_password_enc', 'rdp_password_iv', 'rdp_password_tag')
|
||||
audit('cloud_vas', 'access_text_enc', 'access_text_iv', 'access_text_tag')
|
||||
console.log('audit done')
|
||||
@@ -0,0 +1,80 @@
|
||||
// 调试脚本:测量 cloud 工具初始化各接口耗时 + 拉取前端加载日志,定位刷新"顿一下"的卡点
|
||||
// 用法:node dev_test_scripts/debug/debug_cloud_load_timing.js
|
||||
const BASE = process.env.CLOUD_TIMING_BASE || 'http://localhost:8976'
|
||||
|
||||
const getCookieFromGo = async () => {
|
||||
const r1 = await fetch(`${BASE}/go?systemId=Tools-cloud`, { redirect: 'manual' })
|
||||
const loc = r1.headers.get('location')
|
||||
if (!loc) throw new Error('no location from /go, status=' + r1.status)
|
||||
const r2 = await fetch(new URL(loc, BASE).toString(), { redirect: 'manual' })
|
||||
const setc = String(r2.headers.get('set-cookie') || '')
|
||||
if (!/cloud_gate=1/.test(setc)) throw new Error('no cloud_gate cookie, status=' + r2.status + ' set-cookie=' + setc)
|
||||
return 'cloud_gate=1'
|
||||
}
|
||||
|
||||
const timeIt = async (name, fn) => {
|
||||
const t0 = Date.now()
|
||||
try {
|
||||
const r = await fn()
|
||||
await r.text()
|
||||
return { name, ms: Date.now() - t0, status: r.status }
|
||||
} catch (e) {
|
||||
return { name, ms: Date.now() - t0, error: String((e && e.message) || e) }
|
||||
}
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
const cookie = await getCookieFromGo()
|
||||
const get = url => fetch(`${BASE}${url}`, { headers: { Cookie: cookie }, cache: 'no-store' })
|
||||
const post = url => fetch(`${BASE}${url}`, { method: 'POST', headers: { Cookie: cookie, 'Content-Type': 'application/json' }, body: JSON.stringify({ source: 'cloud', action: 'timing_probe' }) })
|
||||
|
||||
// 1) 拉取前端真实加载日志(用户刷新时上报的 start/done/error 序列)
|
||||
try {
|
||||
const logs = await (await get('/api/debug/logs?limit=150&channel=cloud')).json()
|
||||
console.log('=== 最近 cloud 前端事件(旧→新) ===')
|
||||
;(logs.lines || []).forEach(l => {
|
||||
const o = typeof l === 'string' ? (() => { try { return JSON.parse(l) } catch { return { raw: l } } })() : l
|
||||
console.log(`${o.ts || o.time || o.t || ''} | ${o.action || o.type || o.event || ''} | count=${o.count != null ? o.count : ''} ${o.error ? 'err=' + String(o.error).slice(0, 80) : ''}`)
|
||||
})
|
||||
} catch (e) { console.log('读取 debug logs 失败:', e.message) }
|
||||
|
||||
// 2) 顺序测量各接口(2 轮)
|
||||
const endpoints = [
|
||||
['GET /api/cloud/operators', () => get('/api/cloud/operators')],
|
||||
['GET /api/cloud/ecs', () => get('/api/cloud/ecs')],
|
||||
['GET /api/cloud/vas', () => get('/api/cloud/vas')],
|
||||
['GET /api/cloud/renew', () => get('/api/cloud/renew')],
|
||||
['GET /api/cloud/notes', () => get('/api/cloud/notes')],
|
||||
['GET /api/cloud/balance_alerts/status', () => get('/api/cloud/balance_alerts/status')],
|
||||
['POST /api/debug/event', () => post('/api/debug/event')],
|
||||
['GET /api/flags', () => get('/api/flags')]
|
||||
]
|
||||
for (let round = 1; round <= 2; round++) {
|
||||
console.log(`=== 顺序测量 第${round}轮 ===`)
|
||||
for (const [name, fn] of endpoints) {
|
||||
const r = await timeIt(name, fn)
|
||||
console.log(`${String(r.ms).padStart(5)}ms ${r.status || ''} ${r.error || ''} ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 模拟浏览器初始化并发突发(2 轮)
|
||||
for (let round = 1; round <= 2; round++) {
|
||||
console.log(`=== 并发突发 第${round}轮 ===`)
|
||||
const t0 = Date.now()
|
||||
const results = await Promise.all([
|
||||
timeIt('POST boot', () => post('/api/debug/event')),
|
||||
timeIt('POST ops_start', () => post('/api/debug/event')),
|
||||
timeIt('GET operators', () => get('/api/cloud/operators')),
|
||||
timeIt('GET ecs', () => get('/api/cloud/ecs')),
|
||||
timeIt('GET vas', () => get('/api/cloud/vas')),
|
||||
timeIt('GET renew', () => get('/api/cloud/renew')),
|
||||
timeIt('GET notes', () => get('/api/cloud/notes')),
|
||||
timeIt('GET balance_status', () => get('/api/cloud/balance_alerts/status')),
|
||||
timeIt('GET flags', () => get('/api/flags'))
|
||||
])
|
||||
results.sort((a, b) => a.ms - b.ms).forEach(r => console.log(`${String(r.ms).padStart(5)}ms ${r.status || ''} ${r.error || ''} ${r.name}`))
|
||||
console.log(`突发总耗时: ${Date.now() - t0}ms`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('FATAL', e); process.exit(1) })
|
||||
@@ -0,0 +1,46 @@
|
||||
// 调试脚本:cloud 重启后鉴权链路 + 解密数据完整性回归
|
||||
// 用法:node dev_test_scripts/debug/debug_cloud_regression.js
|
||||
const BASE = 'http://localhost:8976'
|
||||
|
||||
const main = async () => {
|
||||
const r1 = await fetch(BASE + '/go?systemId=Tools-cloud', { redirect: 'manual' })
|
||||
console.log('entry:', r1.status, r1.headers.get('location') ? '(redirect ok)' : '(NO LOCATION)')
|
||||
const r2 = await fetch(new URL(r1.headers.get('location'), BASE), { redirect: 'manual' })
|
||||
const setc = String(r2.headers.get('set-cookie') || '')
|
||||
console.log('index hop:', r2.status, /cloud_gate=1/.test(setc) ? '(cookie ok)' : '(NO COOKIE)')
|
||||
const r3 = await fetch(BASE + '/tools/cloud/index.html', { headers: { Cookie: 'cloud_gate=1' } })
|
||||
console.log('index.html:', r3.status)
|
||||
const r4 = await fetch(BASE + '/tools/cloud/cloud.js', { headers: { Cookie: 'cloud_gate=1' } })
|
||||
console.log('cloud.js:', r4.status)
|
||||
|
||||
const H = { Cookie: 'cloud_gate=1' }
|
||||
const j = async u => (await fetch(BASE + u, { headers: H })).json()
|
||||
|
||||
const ops = await j('/api/cloud/operators')
|
||||
const opsEnc = (ops.rows || []).filter(r => r.login_password_enc)
|
||||
const opsDecOk = opsEnc.filter(r => String(r.login_password || '') !== '').length
|
||||
console.log(`operators: ok=${ops.ok} rows=${(ops.rows || []).length} enc=${opsEnc.length} decrypted_non_empty=${opsDecOk}`)
|
||||
|
||||
const ecs = await j('/api/cloud/ecs')
|
||||
const ecsEnc = (ecs.rows || []).filter(r => r.rdp_password_enc)
|
||||
const ecsDecOk = ecsEnc.filter(r => String(r.rdp_password || '') !== '').length
|
||||
console.log(`ecs: ok=${ecs.ok} rows=${(ecs.rows || []).length} enc=${ecsEnc.length} decrypted_non_empty=${ecsDecOk}`)
|
||||
|
||||
const vas = await j('/api/cloud/vas')
|
||||
const vasEnc = (vas.rows || []).filter(r => r.access_text_enc)
|
||||
const vasDecOk = vasEnc.filter(r => String(r.access_text || '') !== '').length
|
||||
console.log(`vas: ok=${vas.ok} rows=${(vas.rows || []).length} enc=${vasEnc.length} decrypted_non_empty=${vasDecOk}`)
|
||||
|
||||
const rn = await j('/api/cloud/renew')
|
||||
console.log(`renew: ok=${rn.ok} rows=${(rn.rows || []).length}`)
|
||||
const nt = await j('/api/cloud/notes')
|
||||
console.log(`notes: ok=${nt.ok} rows=${(nt.rows || []).length}`)
|
||||
const bs = await j('/api/cloud/balance_alerts/status')
|
||||
console.log(`balance status: ok=${bs.ok} proxied=${!!bs.proxied_to_automation} listener=${bs.data && bs.data.listener ? 'present' : 'n/a'}`)
|
||||
|
||||
const bad = (opsEnc.length !== opsDecOk) || (ecsEnc.length !== ecsDecOk) || (vasEnc.length !== vasDecOk)
|
||||
console.log(bad ? 'REGRESSION FAIL: 存在解密为空的行' : 'REGRESSION PASS')
|
||||
process.exit(bad ? 1 : 0)
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('ERR', e.message); process.exit(1) })
|
||||
@@ -0,0 +1,29 @@
|
||||
// 调试脚本:对指定 BASE 做 cloud 接口顺序+并发耗时复测
|
||||
// 用法:node dev_test_scripts/debug/debug_cloud_timing_probe.js [base_url]
|
||||
const BASE = process.argv[2] || 'http://localhost:8976'
|
||||
|
||||
const main = async () => {
|
||||
const r1 = await fetch(BASE + '/go?systemId=Tools-cloud', { redirect: 'manual' })
|
||||
const loc = r1.headers.get('location')
|
||||
if (!loc) throw new Error('no location, status=' + r1.status)
|
||||
await fetch(new URL(loc, BASE), { redirect: 'manual' })
|
||||
const H = { Cookie: 'cloud_gate=1' }
|
||||
const t = async n => {
|
||||
const t0 = Date.now()
|
||||
const r = await fetch(BASE + n, { headers: H })
|
||||
await r.text()
|
||||
return `${String(Date.now() - t0).padStart(5)}ms ${r.status} ${n}`
|
||||
}
|
||||
console.log('-- sequential --')
|
||||
for (const u of ['/api/cloud/operators', '/api/cloud/ecs', '/api/cloud/vas', '/api/cloud/renew', '/api/cloud/notes', '/api/cloud/balance_alerts/status']) {
|
||||
console.log(await t(u))
|
||||
}
|
||||
console.log('-- burst --')
|
||||
const t0 = Date.now()
|
||||
const urls = ['/api/cloud/operators', '/api/cloud/ecs', '/api/cloud/vas', '/api/cloud/renew', '/api/cloud/notes', '/api/cloud/balance_alerts/status', '/api/flags']
|
||||
const rs = await Promise.all(urls.map(u => fetch(BASE + u, { headers: H }).then(async r => { await r.text(); return `${String(Date.now() - t0).padStart(5)}ms ${r.status} ${u}` })))
|
||||
rs.forEach(x => console.log(x))
|
||||
console.log('burst total', Date.now() - t0, 'ms')
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('ERR', e.message); process.exit(1) })
|
||||
Reference in New Issue
Block a user