Files
Scoreboard/console/main/userpaths.js
T
AskaEth 407abec3b0 fix: 大屏背景图被兜底渐变盖住 + 配置/规则也搬到用户数据目录
1) 大屏背景图不生效:App.vue 里 .bg 与 .bg-fallback 都是 absolute inset:0,
   而兜底渐变是不透明的且在图片之后绘制 → 配了背景图也只看得到渐变。
   改成 v-if/v-else 互斥(用无头 Chromium 真渲染对照验证:修复后取到图片像素,
   回退版本只剩渐变)。新增 background-layer.test.ts 锁住层次关系。

2) 规则/config 仍在安装目录,覆盖安装/卸载会连配置一起删:现在三样都落在
   %LOCALAPPDATA%\ScoreboardConsole\(与数据目录同一基座):
   - console\config.json(Console 读写都用这份)
   - config.yaml(Console 用绝对路径 -config 传给服务端与 passwd)
   - rules\(首次从安装目录复制过去,之后只认用户目录那份)
   首次运行复制迁移、已存在不覆盖、不可写则回落安装目录并记日志(不影响启动);
   开发态行为不变。备份脚本与文档同步。
   注意 resolveExecutable 必须显式传部署包根目录,否则会去用户目录找 scoreboard.exe。

自检:服务端 11 包全绿;console 语法 16 文件 + node:test 20/20;
前端 vue-tsc + build、node --test 69/69。
2026-09-10 10:06:21 +08:00

170 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict'
const fs = require('fs')
const path = require('path')
const { LOCAL_BASE } = require('./datadir')
// 这三样以前随安装包落在安装目录里,但都属于「用户改出来的东西」,必须跟数据目录一样
// 挪到安装目录之外:卸载器结尾是 `RMDir /r $INSTDIR`,覆盖安装又会先静默跑一遍旧卸载器,
// 放在安装目录里等于用户改一次丢一次。
// console\config.json → <用户根>\console\config.json
// config.yaml → <用户根>\config.yaml
// rules\ → <用户根>\rules\
// 用户根与数据目录同一基座:%LOCALAPPDATA%\ScoreboardConsole\
const CONSOLE_SUBDIR = 'console'
const CONSOLE_CONFIG_FILE = 'config.json'
const SERVER_CONFIG_FILE = 'config.yaml'
const RULES_SUBDIR = 'rules'
const ITEM_NAMES = ['consoleConfig', 'serverConfig', 'rulesDir']
const ITEM_LABEL = {
consoleConfig: 'Console 配置文件',
serverConfig: '服务端配置文件',
rulesDir: '规则目录'
}
// 用户根目录;拿不到 %LOCALAPPDATA% 时返回 null,由调用方回落到安装目录
function resolveUserRoot(env) {
const localAppData = env ? env.LOCALAPPDATA : ''
const value = typeof localAppData === 'string' ? localAppData.trim() : ''
return value ? path.join(value, LOCAL_BASE) : null
}
// 解析落点,纯函数不碰磁盘。mode='user' 才接管到用户目录;
// 其余情况(开发态、拿不到 %LOCALAPPDATA%)三项一律用安装目录里随包的那份,
// 行为与以前完全一致 —— 开发态不能动仓库里的文件。
function planUserPaths({ installDir, userRoot, isPackaged }) {
const install = path.resolve(installDir || '.')
const templates = {
consoleConfig: path.join(install, CONSOLE_CONFIG_FILE),
serverConfig: path.join(install, SERVER_CONFIG_FILE),
rulesDir: path.join(install, RULES_SUBDIR)
}
const items = {}
const targets = {
consoleConfig: path.join(install, CONSOLE_CONFIG_FILE),
serverConfig: path.join(install, SERVER_CONFIG_FILE),
rulesDir: path.join(install, RULES_SUBDIR)
}
const reason = !isPackaged ? 'dev' : userRoot ? null : 'no-localappdata'
if (!reason) {
targets.consoleConfig = path.join(userRoot, CONSOLE_SUBDIR, CONSOLE_CONFIG_FILE)
targets.serverConfig = path.join(userRoot, SERVER_CONFIG_FILE)
targets.rulesDir = path.join(userRoot, RULES_SUBDIR)
}
for (const name of ITEM_NAMES) {
items[name] = { target: path.resolve(targets[name]), template: path.resolve(templates[name]) }
}
return {
mode: reason ? 'install' : 'user',
reason,
root: userRoot ? path.resolve(userRoot) : null,
installDir: install,
items
}
}
// 目标目录必须真的能写:mkdir 只保证目录建出来,权限不足或路径被占要在这一步就暴露,
// 好让调用方回落到安装目录,而不是等到写配置文件时才失败
function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true })
fs.accessSync(dir, fs.constants.W_OK)
}
function tryRemove(dir) {
try {
fs.rmSync(dir, { recursive: true, force: true })
} catch {
// 清不掉就留着:只是本次拷贝留下的临时目录,下次会原地重建
}
}
// 规则目录先拷到临时目录再整体改名到位:中途失败不会留下半个 rules\,
// 否则用户目录里一旦存在同名目录,下次启动就按「已存在」跳过、再也不补拷
function copyDirOnce(template, target) {
const tmp = `${target}.tmp-${process.pid}`
try {
tryRemove(tmp)
fs.cpSync(template, tmp, { recursive: true })
fs.renameSync(tmp, target)
} catch (err) {
tryRemove(tmp)
throw err
}
}
// 按 plan 落盘,返回最终生效的路径。判定只有两条:
// 目标不存在 + 安装目录里有 → 复制过去(安装目录那份留作模板,反正卸载器会删);
// 目标已存在 → 一律用用户目录那份,绝不覆盖用户改过的东西。
// 任一目录建不出来 / 拷不动都只记日志,该项继续用安装目录里的那份,绝不删用户文件、
// 也绝不因此启动失败。
function prepareUserPaths({ plan, log }) {
const notify = typeof log === 'function' ? log : () => {}
const result = {
mode: plan.mode,
reason: plan.reason,
root: plan.mode === 'user' ? plan.root : null,
consoleConfig: plan.items.consoleConfig.template,
rulesDir: plan.items.rulesDir.template,
// null = 不接管:拉起服务端与 passwd 时沿用 config.json 里 -config 的原值
serverConfig: null
}
if (plan.mode !== 'user') {
if (plan.reason === 'no-localappdata') {
notify(
'警告:拿不到 %LOCALAPPDATA%,config.json / config.yaml / rules 继续用安装目录里的那份,' +
'覆盖安装会连同它们一起丢失'
)
}
return result
}
for (const name of ITEM_NAMES) {
const { target, template } = plan.items[name]
const label = ITEM_LABEL[name]
if (path.resolve(target) === path.resolve(template)) continue
try {
ensureDir(path.dirname(target))
if (!fs.existsSync(target)) {
if (!fs.existsSync(template)) {
notify(`警告:${label}在用户目录与安装目录里都不存在(${template}),本次跳过复制`)
} else {
if (name === 'rulesDir') copyDirOnce(template, target)
else fs.copyFileSync(template, target)
notify(`已把 ${label} 复制到 ${target}(安装目录里那份留作模板)`)
}
}
} catch (err) {
notify(
`警告:准备 ${label} 失败(${err.message}),继续用安装目录里的 ${template};` +
'覆盖安装会丢失对它的改动'
)
continue
}
if (name === 'serverConfig') result.serverConfig = target
else result[name] = target
}
return result
}
// 服务端读哪份 config.yaml 由 Console 定:config.json 里的 server.args 只声明「有个 -config 参数」,
// 它的值在拉起时换成上面解析出的绝对路径(不传 configPath 时原样返回,开发态就走这条路)
function withConfigArg(args, configPath) {
const list = Array.isArray(args) ? [...args] : []
if (typeof configPath !== 'string' || configPath === '') return list
const index = list.indexOf('-config')
if (index !== -1 && typeof list[index + 1] === 'string') list[index + 1] = configPath
else list.push('-config', configPath)
return list
}
module.exports = { resolveUserRoot, planUserPaths, prepareUserPaths, withConfigArg }