fix: 审查修复波次(P0x4 + P1x30+)

- domain/rules: 计分与排名解耦、破纪录按组别、tieBreak 生效、调整分不丢、移除 aggregate 假能力
- realtime: 新建 internal/dto,快照三项契约对齐,/ws 只认会话身份
- httpapi: 流水幂等、standings 整表载荷、广播接线、强制改密、XFF 防伪造、records 写入、审计、赛事包内容、发布前备份、result.points、实例列表
- web: FormData 上传、YAML 解析、快照归一化、大屏解析层、bootstrap 降级、访客判定、主题切换
- console: 优雅关闭握手、中文化、免验证改管理员账号、选屏与日志轮转修复
- docs: 用户文档三份 + README
- 主代理: 破纪录加分接线(levels 未加载导致恒 0)
This commit is contained in:
AskaEth committed 2026-09-09 21:06:40 +08:00
1 parent 8dae63ee70
commit 7f9701c465
61 files changed
+3844 -333

No files matched your search

+71 -2
View File
@@ -1,5 +1,6 @@
'use strict'
const fs = require('fs')
const path = require('path')
const {
app,
@@ -15,6 +16,7 @@ const { LogBus } = require('./log-bus')
const { ServiceSupervisor } = require('./service')
const { DisplayManager } = require('./displays')
const { createTrayIcon } = require('./tray-icon')
const { validateAccountField, buildPasswdArgs, runPasswd } = require('./account')
// 现场只允许一个实例:两个 Console 会互相抢同一个服务端端口和同一块大屏
if (!app.requestSingleInstanceLock()) {
@@ -36,6 +38,9 @@ function main() {
let displaysEnabled = false
let quitting = false
let shutdownDone = false
let exePath = null
// passwd 子进程同一时刻只允许一个:并发写同一个 sqlite 库没有意义,也会让输出互相串
let accountBusy = false
function resolveConfigPath() {
if (process.env.SCOREBOARD_CONSOLE_CONFIG) {
@@ -87,7 +92,7 @@ function main() {
height,
minWidth: 860,
minHeight: 600,
title: 'Scoreboard Console · 启动管理',
title: '赛事积分榜 · 服务管理',
backgroundColor: '#0f1420',
show: false,
// 标题栏由页面自绘;titleBarStyle 只是兜底,防止某些平台忽略 frame:false
@@ -143,7 +148,7 @@ function main() {
height,
minWidth: 900,
minHeight: 620,
title: 'Scoreboard · 服务端管理面板',
title: '赛事积分榜 · 服务端管理面板',
backgroundColor: '#161616',
show: false,
frame: false,
@@ -253,8 +258,71 @@ function main() {
}
}
// 服务端用什么配置文件,passwd 就得用同一份,否则改的是另一个 data/ 下的账号
function passwdConfigArg() {
const args = Array.isArray(cfg.values.server.args) ? cfg.values.server.args : []
const index = args.indexOf('-config')
if (index !== -1 && typeof args[index + 1] === 'string' && args[index + 1]) {
return args[index + 1]
}
return 'config.yaml'
}
// 账号重置走同目录的 scoreboard.exe passwd 子命令:本机进程、不经 HTTP,
// 所以不需要任何登录态,这就是面板能「免验证」的原因。
async function resetAdminAccount(event, payload) {
if (accountBusy) return { ok: false, error: '上一次重置还没结束,请稍候再试' }
// IPC 入参一律当不可信处理
if (payload === null || typeof payload !== 'object') payload = {}
const currentUsername = typeof payload.currentUsername === 'string' ? payload.currentUsername.trim() : ''
const newUsername = typeof payload.newUsername === 'string' ? payload.newUsername.trim() : ''
const password = typeof payload.password === 'string' ? payload.password : ''
const invalid =
validateAccountField(currentUsername, '当前用户名', true) ||
validateAccountField(newUsername, '新用户名', false) ||
validateAccountField(password, '新密码', false)
if (invalid) return { ok: false, error: invalid }
const exe = exePath || resolveExecutable(cfg.values.server.exe, cfg.path).path
if (!exe || !fs.existsSync(exe)) {
return { ok: false, error: `未找到服务端程序:${exe || cfg.values.server.exe}` }
}
const configArg = passwdConfigArg()
const args = buildPasswdArgs({ configArg, currentUsername, newUsername, password })
const sender = event.sender
accountBusy = true
// 日志里不能带明文密码
log.push(
'console',
`重置管理员账号:${exe} passwd -config ${configArg} -user ${currentUsername}` +
`${newUsername ? ` -new-username ${newUsername}` : ''}(密码已隐藏)`
)
try {
const result = await runPasswd({
exePath: exe,
args,
onOutput: (text) => {
if (!sender.isDestroyed()) sender.send('account:output', text)
}
})
const reason = result.error
? result.error
: `退出码 ${result.exitCode}${result.signal ? `,信号 ${result.signal}` : ''}`
log.push('console', `重置管理员账号${result.ok ? '完成' : '失败'}(${reason})`)
return result
} finally {
accountBusy = false
}
}
function registerIpc() {
ipcMain.handle('service:get-status', () => statusSnapshot())
ipcMain.handle('account:reset', (event, payload) => resetAdminAccount(event, payload))
ipcMain.handle('service:start', () => {
service.start()
return statusSnapshot()
@@ -323,6 +391,7 @@ function main() {
if (cfg.warning) log.push('console', cfg.warning)
const resolved = resolveExecutable(cfg.values.server.exe, cfg.path)
exePath = resolved.path
log.push('console', `服务端程序:${resolved.path}`)
service = new ServiceSupervisor({