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:
1 parent
8dae63ee70
commit
7f9701c465
61 files changed
+3844
-333
No files matched your search
+71
-2
@@ -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({
|
||||
|
||||
Reference in new issue
Block a user