const page = document.body.dataset.page
const $ = (selector) => document.querySelector(selector)
function escapeHtml(value) {
const node = document.createElement('span')
node.textContent = String(value ?? '')
return node.innerHTML
}
function number(value, digits = 0) {
if (value === null || value === undefined || Number.isNaN(Number(value))) return '—'
return Number(value).toLocaleString('zh-CN', { maximumFractionDigits: digits, minimumFractionDigits: digits })
}
function usd(value, digits = 3) {
const numeric = Number(value)
if (value === null || value === undefined || !Number.isFinite(numeric)) return '—'
const formatter = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: digits, minimumFractionDigits: digits })
const parts = formatter.formatToParts(numeric)
const whole = parts.filter(({ type }) => ['minusSign', 'plusSign', 'integer', 'group'].includes(type)).map(({ value: part }) => part).join('')
const point = parts.find(({ type }) => type === 'decimal')?.value ?? '.'
const fraction = parts.find(({ type }) => type === 'fraction')?.value ?? ''.padEnd(digits, '0')
const label = `$${formatter.format(numeric)}`
return `$${escapeHtml(whole)}${escapeHtml(point)}${escapeHtml(fraction)}`
}
function compact(value) {
if (value === null || value === undefined) return '—'
return new Intl.NumberFormat('zh-CN', { notation: 'compact', maximumFractionDigits: 2 }).format(Number(value))
}
function percent(value) {
return value === null || value === undefined ? '—' : `${number(Number(value) * 100, 1)}%`
}
function time(value) {
if (!value) return '—'
return new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
}).format(new Date(value))
}
async function requestJson(url, options = {}, timeoutMs = 20000) {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs)
try {
const response = await fetch(url, { ...options, signal: controller.signal, headers: { 'content-type': 'application/json', ...(options.headers ?? {}) } })
const data = await response.json().catch(() => null)
if (response.status === 401 && page !== 'login') {
location.assign('/login')
throw new Error('登录状态已失效')
}
if (!response.ok || !data?.ok) throw new Error(data?.error ?? `HTTP ${response.status}`)
return data
} finally {
clearTimeout(timer)
}
}
function shell() {
const mount = $('[data-shell]')
if (!mount) return
const links = [
['scores', '/scores', '账号评分'],
['ranking', '/ranking', '用户用量'],
['lottery', '/lottery', '额度抽奖'],
]
mount.innerHTML = ``
$('#logout').addEventListener('click', async () => {
await requestJson('/api/logout', { method: 'POST', body: '{}' }).catch(() => null)
location.assign('/login')
})
}
async function loginPage() {
const form = $('#login-form')
form.addEventListener('submit', async (event) => {
event.preventDefault()
const button = form.querySelector('button')
const error = $('#login-error')
button.disabled = true
error.textContent = ''
try {
await requestJson('/api/login', {
method: 'POST',
body: JSON.stringify({ username: $('#username').value, password: $('#password').value }),
})
location.assign('/scores')
} catch (cause) {
error.textContent = cause instanceof Error ? cause.message : String(cause)
} finally {
button.disabled = false
}
})
}
let scoreRows = []
let scoreRefreshedAt = null
let scoreNextRefreshAt = null
function gradeClass(value) {
const grade = String(value ?? '').toLowerCase()
if (grade === 'a' || grade === 'b' || grade === 'excellent' || grade === 'good') return 'grade-good'
if (grade === 'd' || grade === 'e' || grade === 'poor' || grade === 'critical' || grade === 'insufficient') return 'grade-risk'
return 'grade-mid'
}
function groupLabels(row) {
const groups = Array.isArray(row.groupNames) && row.groupNames.length ? row.groupNames : [row.groupName].filter(Boolean)
return `
${groups.map((group) => `${escapeHtml(group)}`).join('')}
`
}
function countdown(value) {
if (!value) return '--:--'
const remaining = Math.max(0, new Date(value).getTime() - Date.now())
if (remaining === 0) return '等待刷新'
const totalSeconds = Math.ceil(remaining / 1000)
const minutes = Math.floor(totalSeconds / 60)
const seconds = totalSeconds % 60
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
}
function renderRefreshClock() {
$('#score-updated-time').textContent = scoreRefreshedAt ? `北京时间 ${time(scoreRefreshedAt)}` : '尚无成功快照'
$('#score-countdown').textContent = countdown(scoreNextRefreshAt)
}
function renderScoreRows() {
const term = ($('#score-filter')?.value ?? '').trim().toLowerCase()
const rows = scoreRows.filter((row) => `${row.accountName} ${row.groupName}`.toLowerCase().includes(term))
$('#score-body').innerHTML = rows.length ? rows.map((row) => {
const usage = row.usage ?? {}
return `
| ${escapeHtml(row.accountName)}#${escapeHtml(row.accountId)} |
${groupLabels(row)} |
${number(row.priority)} |
${number(row.score, 1)} |
${escapeHtml(row.grade ?? '—')} |
${escapeHtml(row.confidence ?? '—')} |
${number(row.observedAttempts)} |
${percent(row.failureRate)} |
${row.ttftP95Ms == null ? '—' : `${number(row.ttftP95Ms)} ms`} |
${compact(usage.requestCount)} |
${compact(usage.tokenCount)} |
${usd(usage.apiAmountUsd)} |
${number(row.failoverRecovered)} / ${number(row.failoverRequests)} |
${row.currentlyAvailable ? '可用' : '不可用'} |
`
}).join('') : '| 没有匹配的账号 |
'
}
function renderScores(data) {
scoreRows = data.accounts ?? []
$('#metric-accounts').textContent = number(scoreRows.length)
$('#metric-groups').textContent = number((data.groups ?? []).length)
$('#metric-good').textContent = number(scoreRows.filter((row) => Number(row.score) >= 80).length)
$('#metric-risk').textContent = number(scoreRows.filter((row) => Number(row.score) < 60).length)
$('#metric-window').textContent = data.window ?? '—'
$('#score-state').textContent = ({ ready: '已更新', refreshing: '刷新中', stale: '使用旧快照', unavailable: '暂无快照' })[data.status] ?? data.status
$('#score-state').dataset.state = data.status
scoreRefreshedAt = data.refreshedAt ?? scoreRefreshedAt
scoreNextRefreshAt = data.nextRefreshAt ?? scoreNextRefreshAt
renderRefreshClock()
renderScoreRows()
}
async function scoresPage() {
$('#score-filter').addEventListener('input', renderScoreRows)
$('#refresh-scores').addEventListener('click', async () => {
const button = $('#refresh-scores')
button.disabled = true
$('#score-state').textContent = '刷新中'
try { renderScores(await requestJson('/api/scores/refresh', { method: 'POST', body: '{}' }, 200000)) }
catch (error) { $('#score-updated-time').textContent = error instanceof Error ? error.message : String(error) }
finally { button.disabled = false }
})
renderScores(await requestJson('/api/scores'))
setInterval(renderRefreshClock, 1000)
setInterval(async () => {
if (!document.hidden) {
const data = await requestJson('/api/scores').catch(() => null)
if (data) renderScores(data)
}
}, 30000)
}
async function rankingPage() {
const data = await requestJson('/api/ranking')
const ranking = data.ranking
$('#ranking-range').textContent = `${ranking.startDate} 至 ${ranking.endDate}`
$('#ranking-cost').innerHTML = usd(ranking.totals.actualCost)
$('#ranking-requests').textContent = compact(ranking.totals.requests)
$('#ranking-tokens').textContent = compact(ranking.totals.tokens)
$('#ranking-body').innerHTML = ranking.rows.length ? ranking.rows.map((row) => `| ${String(row.rank).padStart(2, '0')} | ${escapeHtml(row.displayName)} | ${usd(row.actualCost)} | ${compact(row.requests)} | ${compact(row.tokens)} |
`).join('') : '| 当前窗口暂无用量 |
'
}
function creditLabel(status) {
return ({ succeeded: '已充值', dry_run: '模拟充值', disabled: '充值未开启', pending: '充值待确认', failed: '充值失败' })[status] ?? status
}
function renderLottery(data) {
$('#lottery-prize').textContent = number(data.prizeAmountUsd, 0)
$('#lottery-remaining').textContent = number(data.remainingDraws)
$('#lottery-eligible').textContent = number(data.eligibleUserCount)
$('#lottery-next').textContent = time(data.nextGrantAt)
$('#lottery-mode').textContent = data.automaticCredit?.enabled ? creditLabel(data.automaticCredit.mode) : '自动充值关闭'
$('#draw-button').disabled = Number(data.remainingDraws) < 1 || Number(data.eligibleUserCount) < 1
$('#draw-status').textContent = Number(data.remainingDraws) < 1 ? '今天的机会已用完' : `${data.eligibleUserCount} 名候选用户已就绪`
$('#record-list').innerHTML = data.records?.length ? data.records.map((record) => `${escapeHtml(record.winnerDisplayName)}$${number(record.prizeAmountUsd, 0)} · ${creditLabel(record.creditStatus)}`).join('') : '暂无开奖记录'
}
async function lotteryPage() {
let state = await requestJson('/api/lottery')
renderLottery(state)
$('#draw-button').addEventListener('click', async () => {
const button = $('#draw-button')
button.disabled = true
$('#draw-status').textContent = '正在抽取活跃用户...'
try {
const data = await requestJson('/api/lottery/draw', { method: 'POST', body: '{}' })
$('#winner-name').textContent = data.record.winnerDisplayName
$('#winner-prize').textContent = number(data.record.prizeAmountUsd, 0)
$('#winner-meta').textContent = `${data.record.eligibleCount} 人等概率 · ${creditLabel(data.record.creditStatus)}`
$('#winner-dialog').showModal()
state = await requestJson('/api/lottery')
renderLottery(state)
} catch (error) {
$('#draw-status').textContent = error instanceof Error ? error.message : String(error)
button.disabled = false
}
})
$('#winner-close').addEventListener('click', () => $('#winner-dialog').close())
}
async function boot() {
if (page === 'login') return await loginPage()
shell()
if (page === 'scores') return await scoresPage()
if (page === 'ranking') return await rankingPage()
if (page === 'lottery') return await lotteryPage()
}
boot().catch((error) => {
const target = $('.workspace') ?? $('main')
if (target) target.insertAdjacentHTML('afterbegin', `${escapeHtml(error instanceof Error ? error.message : String(error))}
`)
})