Files
Scoreboard/console
AskaEth 005b51b2ec fix: 数据目录移出安装目录,修掉「覆盖安装/卸载即丢数据」
根因:数据目录是 <安装目录>\data(config.yaml 的 dataDir 相对服务端工作目录),
而 electron-builder 的卸载器结尾执行 RMDir /r $INSTDIR,且覆盖安装会先跑旧卸载器
(installSection.nsh:52 的 uninstallOldVersion)→ 赛事库/规则/用户/审计/上传图片
连同安装目录被整个删除,再装新版。卸载一次同样如此。

- 服务端新增 -data <dir>(主命令、backup、passwd 三处),非空时覆盖 config.yaml 的
  dataDir;不传时行为完全不变(兼容便携/手工运行)
- Console 把数据指到当前用户目录 %LOCALAPPDATA%\ScoreboardConsole\data(卸载器不会碰
  这里;没有用 app.getPath('userData'),因为 %APPDATA%\<AppName> 正是被删的那个)
- 一次性迁移:目标为空且 <安装目录>\data 有数据时整体改名过去;两边都有数据时不动作
  只记警告;任何失败都保留原地数据、绝不删除
- passwd 也带 -data(否则重置的是安装目录下那个空库里的账号)
- deploy/backup.bat 指向同一数据目录
- 文档:部署指南新增「数据放在哪里(升级/卸载都不会删)」+ 备份/恢复/迁移路径改写,
  上线检查清单与使用手册同步

自检:服务端 12 包全绿;console 语法 14 文件 + datadir 单测 11/11;backup.bat 保持纯 ASCII。
2026-09-10 09:17:56 +08:00
..

Scoreboard Console

赛事积分榜系统的桌面宿主(Electron)。它只做六件事:

  1. 拉起并守护同目录(或配置指定)的 scoreboard.exe,把它的 stdout/stderr 接管到管理窗口与日志文件;
  2. 用 screen.getAllDisplays() 枚举显示器,把大屏页面投到扩展屏;
  3. powerSaveBlocker.start('prevent-display-sleep') 防熄屏;
  4. 提供启动/停止/状态/日志的管理 GUI 和托盘菜单;
  5. 默认注册登录项但不启用(不随系统启动),可在服务管理窗口的「开机自启」开关里开启;
  6. 在服务管理窗口展示服务端在线的大屏实例(GET /api/display/instances)。

规范来源:devdocs/06-大屏端设计.md 第 6 节、devdocs/10-部署与现场方案.md 第 4 节。

目录结构

console/
├── package.json          # main 指向 main/index.js;electron-builder 配置
├── config.json           # 运行配置(端口 / exe 路径 / 选屏策略)
├── main/
│   ├── index.js          # 应用入口:窗口 / 托盘 / IPC / 生命周期
│   ├── config.js         # 配置加载与合并、exe 路径解析
│   ├── datadir.js        # 数据目录解析 + 旧数据一次性迁移
│   ├── datadir.test.js   # 数据目录的单元测试(node --test main/datadir.test.js)
│   ├── service.js        # 服务端子进程守护:spawn / 日志转发 / 健康检查 / 自动重启
│   ├── instances.js      # 已连接大屏实例看板:复用管理面板登录态读 /api/display/instances
│   ├── account.js        # 管理员账号重置:参数校验 + 调 scoreboard.exe passwd
│   ├── displays.js       # 大屏窗口管理:选屏、kiosk、排布、24h 重载
│   ├── log-bus.js        # 日志环形缓冲 + 落文件 + 广播
│   └── tray-icon.js      # 托盘图标(base64 内嵌,无外部资源文件)
├── scripts/
│   └── check-syntax.js   # pnpm check:跨平台递归 node --check
├── extra/                # 打包前由 deploy/build.bat 暂存 使用说明.txt(extraFiles 指向它)
└── renderer/
    ├── index.html        # 管理窗口(含自绘标题栏)
    ├── styles.css
    ├── app.js
    ├── preload.js        # 管理窗口的 contextBridge 白名单
    └── shell-preload.js  # 服务端管理面板窗口的 window.desktopShell 白名单(窗口控制 + 显示器菜单)

运行方式

cd console
pnpm install       # 首次需要下载 Electron
pnpm start         # 等价于 electron .

开发时把 scoreboard.exe 放在以下任一位置都能被找到(config.json 里的相对路径会按顺序探测):

  1. console/
  2. 当前工作目录
  3. 仓库根目录
  4. dist/

现场(部署包)形态:

scoreboard-windows-amd64/
├── Scoreboard_Console.exe
├── resources/app.asar      # 内含 main/ renderer/ config.json
├── config.json             # exe 旁边的可编辑配置
├── scoreboard.exe
├── config.yaml
├── rules/
└── logs/console.log        # 运行时生成

数据不在安装目录里,默认落在 %LOCALAPPDATA%\ScoreboardConsole\data(见下文「数据目录」)。

打包后 config.json 读取顺序:SCOREBOARD_CONSOLE_CONFIG 环境变量 → exe 同目录的 config.json。

数据目录(重要)

electron-builder 生成的卸载器结尾是 RMDir /r $INSTDIR,而覆盖安装会先静默跑一遍旧版卸载器,所以装在安装目录里的 data\ 会被整体删掉;%APPDATA%\Scoreboard Console 卸载时同样会删,因此也不能用 app.getPath('userData')。最终方案:

场景 数据目录
打包态,server.dataDir 留空(默认) %LOCALAPPDATA%\ScoreboardConsole\data(卸载器不碰这个位置)
配置了 server.dataDir 配什么用什么:绝对路径直接用;相对路径以安装目录为基准
开发态(pnpm start),server.dataDir 留空 不接管,沿用服务端默认(工作目录下的 data),仓库里的 data/ 不受影响
  • Console 把最终目录通过服务端的 -data <绝对路径> 传下去,passwd 子命令带同一个参数,保证改的是同一个库里的账号;
  • 迁移只在打包态做一次:目标目录为空(或不存在)且 <安装目录>\data 有数据时,用 fs.renameSync 把旧目录整体改名过去(同盘秒完成)。跨盘、权限不足等任何失败都只记日志,旧数据原地保留;
  • 两边都有数据时不做任何动作,只写一条醒目日志提示人工确认,两份都不会被自动删除;
  • 迁移结果与最终数据目录都写进 logs/console.log(搜「数据目录」「已把旧数据从」);
  • 数据目录若仍在安装目录内,启动时会额外打一条警告。

改完 datadir.js 跑 node --test main/datadir.test.js(Node 内置测试运行器,无额外依赖)。

行为说明

场景 行为
只有一块屏 窗口化(屏幕 80% 大小、居中),不做多屏编排,不挂 display-metrics-changed 等监听(行为冻结);F11 手动切换全屏
多块屏 除内置屏外的每块屏一个 kiosk 窗口,按 display.bounds(DIP)定位,窗口 setIgnoreMouseEvents(true),光标由页面 CSS 隐藏
选屏 运行时枚举,不写死坐标;selection 可选 all-secondary(默认,用 display.internal 排除内置屏)/ all / specified
页面锁定 每块屏各锁一页,URL 形如 http://127.0.0.1:19540/display?page=standings&lock=1&screen=<id>&instance=<host>-<id>,页序按 display.pages 轮转
防熄屏 Console 启动即生效,退出时释放;前台后台都算
服务端就绪 首次 /api/health 通过后开大屏窗口;等待超过 display.waitForServerMs 也先开,避免现场只见桌面
服务端崩溃 按 restartDelayMs 递增退避自动重启(maxRestarts=0 为无限);持续健康 healthyResetMs 后崩溃计数清零;恢复健康后自动重载大屏
数据目录 默认 %LOCALAPPDATA%\ScoreboardConsole\data(安装目录之外,覆盖安装/卸载都不删),启动时按 -data <绝对路径> 交给服务端;打包态首次启动做一次性迁移(见「数据目录」一节),开发态不接管
停止服务 先 POST server.shutdownPath 并带 X-Shutdown-Token(启动时用 crypto.randomBytes(32) 生成,经环境变量 SCOREBOARD_SHUTDOWN_TOKEN 只交给该子进程)请求服务端优雅关闭;受理后最多等 shutdownTimeoutMs,超时或接口不可用(404/连接失败)再强杀
已连接大屏实例 管理窗口打开时每 5 秒读一次服务端 GET /api/display/instances(复用管理面板的 scoreboard_session Cookie,需记录员及以上),显示实例数、页面名与在线状态;窗口隐藏即停表,未登录/服务未启动时显示占位文案
关管理窗口 只隐藏到托盘,服务端继续运行;退出必须走托盘「退出」
开机自启 默认「已注册但未启用」;在服务管理窗口的「开机自启」开关里控制(startup.openAtLogin),仅打包后的 Windows/macOS 生效,开发态不注册
窗口外观 管理窗口与服务端管理面板窗口都是 frame: false 无边框,标题栏与最小化/最大化/关闭三键由页面自绘(拖拽区 -webkit-app-region: drag,按钮 no-drag,双击标题栏切最大化,关闭键 hover 变红)
服务端管理面板 用独立无边框 Electron 窗口加载 http://127.0.0.1:<port>/admin(shell-preload.js 暴露 window.desktopShell),不再调系统浏览器;面板页在浏览器里直接打开时标题栏自动不渲染
显示器菜单 面板页的「大屏控制 → 显示器」卡片经 display:list / display:open / display:close / display:relayout 指挥本机开窗:按屏创建、聚焦、收窗。全屏只在「多于一块屏 + 非主屏」时放行,判定在主进程做,页面传 fullscreen: true 也会被降级为窗口化;面板页只拿到这几条白名单通道,拿不到 ipcRenderer
管理员账号 主进程 spawn 同目录 scoreboard.exe passwd -config <同一份配置> -data <同一个数据目录> -user <当前用户名> -new-username <新用户名> -password <新密码>(留空 = 不改 / 随机生成)。本机进程、不经 HTTP,因此免登录验证;用户名/密码含空格、制表符或换行会被主进程拒绝,密码不进日志。passwd 会作废该用户全部会话,UI 提交前二次确认并提示
日志 内存环形缓冲(log.maxLines)+ 文件 logs/console.log(超 maxFileBytes 轮转一份 .1,轮转前会删掉旧 .1 以兼容 Windows)

配置项(config.json)

键 默认值 说明
server.exe scoreboard.exe 相对路径按上文顺序探测,也可写绝对路径
server.args ["-config","config.yaml"] 传给服务端的参数
server.dataDir "" 数据目录;空串 = 用默认位置(打包态 %LOCALAPPDATA%\ScoreboardConsole\data),填了就按它来(相对路径以安装目录为基准)。Console 会把它作为 -data <绝对路径> 追加到服务端参数后
server.host / server.port 127.0.0.1 / 19540 健康检查、关闭握手与管理面板地址(0.0.0.0 会自动换成回环地址探测)
server.autoStart true Console 启动后是否立刻拉起服务端
server.healthIntervalMs 5000 健康检查间隔
server.restartDelayMs 3000 重启基础退避,每次翻倍,最多 5 倍
server.maxRestarts 0 0 = 无限重启
server.shutdownPath /api/shutdown 优雅关闭握手接口(需服务端实现并校验 X-Shutdown-Token),不可用时回退强杀
server.shutdownTimeoutMs 5000 服务端受理关闭请求后等待其自行退出的上限,超过则强杀
server.healthyResetMs 60000 连续健康多久后清零崩溃计数
display.selection all-secondary all-secondary / all / specified
display.displays [] specified 时生效,填 display id 或 0 起序号
display.pages ["standings","medals","individual","results"] 按屏序轮转锁定;只填大屏支持的页名
display.pageParam page 页面 URL 参数名
display.kiosk true 多屏时是否 kiosk(单屏恒窗口化)
display.ignoreMouse true 多屏窗口是否忽略鼠标
display.reloadEveryHours 24 定时重载兜底,0 = 关闭
display.waitForServerMs 20000 服务端未就绪时开大屏的兜底等待
display.backgroundColor #0b0f1a 窗口底色,减少加载白闪
startup.openAtLogin false 默认注册但不启用(任务管理器可见、已禁用);在服务管理窗口「开机自启」开关里控制(开发态不注册)
admin.width / admin.height 1040 / 760 管理窗口初始尺寸(服务端管理面板窗口按 width 不小于 1100 取用)
log.maxLines / log.file / log.maxFileBytes 2000 / logs/console.log / 4194304 日志缓冲与文件

配置缺项自动用内置默认值,文件不存在或 JSON 语法错误时回落到全默认值,并在管理窗口顶部给出提示。

打包(electron-builder)

package.json 里已写好 build 段:

cd console
pnpm install
pnpm check       # 跨平台语法检查(scripts/check-syntax.js)
pnpm dist        # electron-builder --win --x64 → ../dist/console/
pnpm pack        # 只出目录,不生成安装包,便于现场调试
  • 产物:NSIS 安装包 + zip(Scoreboard Console-0.1.0-x64.*)。
  • extraFiles 会把 ../dist/scoreboard.exe、../config.yaml、config.json(exe 旁的可编辑配置,缺失时 Console 会回落到内置默认值)与 ../rules 拷到安装目录,最终形成第 4 节里的部署包形态。
  • 使用说明.txt 是可选的:deploy/build.bat 在打包前依次找 docs/使用说明.txt、docs/使用手册.md,命中就暂存到 console/extra/使用说明.txt(extraFiles 会把该目录内容平铺到部署包根目录),都没有就跳过并在构建日志里注明。
  • 图标在 resources/icon.ico(package.json 的 win.icon 指向它)。
  • 主程序文件名由 win.executableName 决定,现在是 Scoreboard_Console.exe;productName 仍是 Scoreboard Console(显示名、userData 目录不变)。
  • nsis.include 指向 resources/installer-custom.nsh(构建时由 deploy/prepare-extra.mjs 生成,已 gitignore):许可页顶部文案 + 记住上次安装目录(重装时默认沿用,不退回系统默认路径)。
  • 已在 Windows 构建机上通过 deploy\build.bat 实跑(含 pnpm dist,产出 NSIS 安装包 + zip);Linux 开发机未安装 Electron/electron-builder。

尚未实机验证

以下内容只做了静态语法检查,需要在 Windows + 真实多显示器环境跑一遍:

  • 多屏 kiosk 定位与 display-metrics-changed 重排(改分辨率 / 缩放 / 插拔 HDMI)。
  • 管理面板显示器菜单:真正的 kiosk 全屏 / 窗口化结果、screen.getAllDisplays() 的 label 与主屏判定、display:list 返回的 scaleFactor 是否与现场一致。
  • powerSaveBlocker 在长时间后台下的实际效果。
  • 托盘图标在 Windows 任务栏的显示效果(16px 缩放)。
  • scoreboard.exe 真实拉起、日志转发、健康检查与崩溃重启的端到端链路。
  • 数据目录迁移实机路径:老版本安装目录里的 data\ 是否被整体搬到 %LOCALAPPDATA%\ScoreboardConsole\data、跨盘安装(安装目录在 D 盘、%LOCALAPPDATA% 在 C 盘)时的失败日志与「旧数据原地保留」表现。
  • electron-builder 打包已通过 deploy\build.bat 实跑;NSIS 安装包在真机上的安装 / 卸载流程尚未验证。