diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..746df51 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Windows 批处理必须是 CRLF:cmd.exe 在纯 LF 下可能解析不了标签(:label)与多行 if 块 +*.bat text eol=crlf +*.cmd text eol=crlf + +# 其余文本统一 LF,避免跨平台 diff 噪声 +*.go text eol=lf +*.ts text eol=lf +*.vue text eol=lf +*.css text eol=lf +*.html text eol=lf +*.json text eol=lf +*.yaml text eol=lf +*.yml text eol=lf +*.md text eol=lf +*.nsh text eol=crlf + +# 二进制资源不做行尾转换 +*.png binary +*.ico binary +*.woff2 binary +*.zip binary +*.exe binary diff --git a/README.md b/README.md index f264248..a9ab97d 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ 3. 浏览器打开终端提示的地址 ``` -大屏 http://<主机IP>:8080/display -管理面板 http://<主机IP>:8080/admin +大屏 http://<主机IP>:19540/display +管理面板 http://<主机IP>:19540/admin ``` > 完全离线运行,不依赖外网,不需要安装任何运行时。 diff --git a/console/main/config.js b/console/main/config.js index 14dc685..6ba7f63 100644 --- a/console/main/config.js +++ b/console/main/config.js @@ -15,7 +15,12 @@ const DEFAULTS = { healthIntervalMs: 5000, restartDelayMs: 3000, // 0 表示无限重启;服务端是现场唯一进程,崩了必须拉起来 - maxRestarts: 0 + maxRestarts: 0, + // 优雅关闭握手:服务端需实现该回环接口,未实现时回退到进程信号 + shutdownPath: '/api/shutdown', + shutdownTimeoutMs: 12000, + // 连续健康这么久后清零崩溃计数,避免历史崩溃耗尽 maxRestarts + healthyResetMs: 60000 }, display: { // all-secondary | all | specified diff --git a/console/main/displays.js b/console/main/displays.js index 7f9954f..1c424e1 100644 --- a/console/main/displays.js +++ b/console/main/displays.js @@ -52,7 +52,7 @@ class DisplayManager { } else if (selection === SELECTION.SPECIFIED) { picked = this.pickSpecified(all) } else { - picked = all.filter((item) => item.id !== primary.id) + picked = this.pickSecondary(all, primary) } // 只有整机就一块屏时才是「单屏测试模式」;笔记本 + 一块扩展屏属于多屏场景, @@ -61,6 +61,15 @@ class DisplayManager { return { single: false, displays: picked } } + // 默认策略是「除内置屏外的每块屏」(doc 06 §6.2)。用 display.internal 判断, + // 而不是拿主屏当替身:现场常把主屏设成外接 HDMI,用主屏排除会把 kiosk 开到笔记本屏。 + // 全部屏都报 internal=false 时(纯台式机)退回「排除主屏」的旧口径。 + pickSecondary(all, primary) { + const external = all.filter((item) => item.internal === false) + if (external.length && external.length < all.length) return external + return all.filter((item) => item.id !== primary.id) + } + // displays 配置里可以写 display.id,也可以写 0 起的序号 pickSpecified(all) { const wanted = Array.isArray(this.display.displays) ? this.display.displays : [] diff --git a/console/main/index.js b/console/main/index.js index 2362062..cad48f2 100644 --- a/console/main/index.js +++ b/console/main/index.js @@ -7,8 +7,7 @@ const { Menu, Tray, ipcMain, - powerSaveBlocker, - shell + powerSaveBlocker } = require('electron') const { loadConfig, resolveExecutable } = require('./config') @@ -242,6 +241,18 @@ function main() { tray.on('double-click', () => showAdmin()) } + // 开机自启:只对打包后的 Windows/macOS 程序有意义,开发态注册的是 electron 可执行文件 + function applyOpenAtLogin() { + if (!app.isPackaged || (process.platform !== 'win32' && process.platform !== 'darwin')) return + const wanted = !cfg.values.startup || cfg.values.startup.openAtLogin !== false + try { + app.setLoginItemSettings({ openAtLogin: wanted }) + log.push('console', wanted ? '开机自启已启用' : '开机自启已关闭') + } catch (err) { + log.push('console', `设置开机自启失败:${err.message}`) + } + } + function registerIpc() { ipcMain.handle('service:get-status', () => statusSnapshot()) ipcMain.handle('service:start', () => { @@ -265,14 +276,6 @@ function main() { if (displays && displaysEnabled) displays.relayout() return statusSnapshot() }) - ipcMain.handle('app:open-external', (_event, url) => { - // 只放行 http/https,避免管理窗口被注入的链接拉去执行本地协议 - if (typeof url === 'string' && /^https?:\/\//i.test(url)) { - shell.openExternal(url) - return true - } - return false - }) ipcMain.handle('app:open-admin-window', () => { showAdmin() return true @@ -308,8 +311,6 @@ function main() { }) } - app.on('second-instance', () => showAdmin()) - app.whenReady().then(() => { cfg = loadConfig(resolveConfigPath()) log = new LogBus({ @@ -367,6 +368,13 @@ function main() { createTray() registerIpc() + // 单实例锁在 whenReady 之前就生效,但回调要用 cfg;注册放在这里, + // 避免第二个实例在配置加载完成前触发 showAdmin() 读到 null + app.on('second-instance', () => showAdmin()) + + // 现场无人值守,Console 需要随系统启动(doc 10 §4.3) + applyOpenAtLogin() + // 防熄屏要在服务端起来之前就生效,否则开机后第一段时间屏幕仍会休眠 powerBlockerId = powerSaveBlocker.start('prevent-display-sleep') log.push('console', `防熄屏已启用(powerSaveBlocker=${powerBlockerId},生效=${powerSaveBlocker.isStarted(powerBlockerId)})`) diff --git a/console/main/log-bus.js b/console/main/log-bus.js index fd5a299..9233bd0 100644 --- a/console/main/log-bus.js +++ b/console/main/log-bus.js @@ -22,7 +22,14 @@ class LogBus { try { fs.mkdirSync(path.dirname(file), { recursive: true }) if (fs.existsSync(file) && fs.statSync(file).size > this.maxFileBytes) { - fs.renameSync(file, `${file}.1`) + // Windows 上 rename 到已存在的目标会失败,先删掉上一份轮转文件 + const rotated = `${file}.1` + try { + fs.rmSync(rotated, { force: true }) + } catch { + // 删不掉就让 rename 失败,落到下面的「仅内存日志」兜底 + } + fs.renameSync(file, rotated) } this.stream = fs.createWriteStream(file, { flags: 'a' }) } catch (err) { diff --git a/console/main/service.js b/console/main/service.js index 3c7e584..fb9a185 100644 --- a/console/main/service.js +++ b/console/main/service.js @@ -14,6 +14,17 @@ const STATE_LABEL = { crashed: '异常退出' } +// 优雅关闭握手:POST server.shutdownPath,让服务端自己走 signal.NotifyContext +// 的关闭流程;请求失败或接口未实现时回退到进程信号。 +const DEFAULT_SHUTDOWN_PATH = '/api/shutdown' +const SHUTDOWN_REQUEST_TIMEOUT_MS = 2000 +// 服务端 srv.Shutdown 的超时是 10s,客户端必须留足余量再强杀 +const DEFAULT_SHUTDOWN_TIMEOUT_MS = 12000 +// 握手不可用时信号已能结束进程,不必再等满超时 +const FALLBACK_FORCE_KILL_MS = 1500 +// 连续健康这么久才认为崩溃计数可以清零 +const HEALTHY_RESET_MS = 60000 + // 服务端守护:拉起 scoreboard.exe、转发日志、健康探测、崩溃自动重启。 // 现场无人值守,所以「崩了自动拉起」比「暴露复杂控制」更重要。 class ServiceSupervisor extends EventEmitter { @@ -33,6 +44,7 @@ class ServiceSupervisor extends EventEmitter { this.lastError = null this.stopRequested = false + this.healthySince = null this.healthTimer = null this.restartTimer = null this.forceKillTimer = null @@ -90,6 +102,7 @@ class ServiceSupervisor extends EventEmitter { const cwd = path.dirname(this.exePath) this.lastError = null + this.healthySince = null this.startedAt = new Date().toISOString() this.setState('starting') this.log.push('console', `启动服务端:${this.exePath} ${args.join(' ')}(cwd=${cwd})`) @@ -140,6 +153,7 @@ class ServiceSupervisor extends EventEmitter { this.child = null this.pid = null this.healthy = false + this.healthySince = null const reason = signal ? `信号 ${signal}` : `退出码 ${code}` if (this.stopRequested) { @@ -187,25 +201,80 @@ class ServiceSupervisor extends EventEmitter { this.log.push('console', '正在停止服务端…') child.once('exit', () => resolve()) - try { - child.kill() - } catch (err) { - this.log.push('console', `发送停止信号失败:${err.message}`) - } - // Go 服务端有 10s 优雅关闭窗口,超过 5s 仍未退出就强杀,避免 Console 退不干净 - this.forceKillTimer = setTimeout(() => { - if (this.child && this.pid) { - this.log.push('console', '服务端未在 5 秒内退出,强制结束') - try { - this.child.kill('SIGKILL') - } catch { - // 进程可能已经没了 - } + + const timeoutMs = Number(this.server.shutdownTimeoutMs) || DEFAULT_SHUTDOWN_TIMEOUT_MS + // 先按最坏情况挂强杀兜底,握手成功后也不撤:服务端可能卡在关闭流程里 + this.armForceKill(timeoutMs) + + this.requestShutdown().then((accepted) => { + if (this.child !== child) return + if (accepted) { + this.log.push('console', `服务端已接受关闭请求,最多等待 ${Math.round(timeoutMs / 1000)} 秒`) + return } - }, 5000) + // 握手不可用:服务端还没实现该接口,或进程已无响应。 + // 退回进程信号;Windows 上 Node 的 kill() 等同强杀,所以这条路只是兜底, + // 真正的优雅关闭依赖服务端补上 shutdownPath 接口。 + this.log.push('console', '关闭接口不可用,退回进程信号') + this.armForceKill(FALLBACK_FORCE_KILL_MS) + try { + child.kill() + } catch (err) { + this.log.push('console', `发送停止信号失败:${err.message}`) + } + }) }) } + // 优雅关闭握手:让服务端自己走 signal.NotifyContext → srv.Shutdown 的流程, + // 10s 内落盘退出。接口按回环地址访问、不鉴权,只在本机有意义。 + // 依赖:服务端实现 POST (2xx 表示已受理)。 + requestShutdown() { + return new Promise((resolve) => { + const body = Buffer.from('{}') + const request = http.request( + { + host: this.healthHost(), + port: this.server.port, + path: this.server.shutdownPath || DEFAULT_SHUTDOWN_PATH, + method: 'POST', + timeout: SHUTDOWN_REQUEST_TIMEOUT_MS, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': body.length, + Connection: 'close' + } + }, + (response) => { + response.resume() + const accepted = response.statusCode >= 200 && response.statusCode < 300 + if (!accepted) this.log.push('console', `关闭接口返回 HTTP ${response.statusCode}`) + resolve(accepted) + } + ) + request.on('timeout', () => request.destroy(new Error('请求超时'))) + request.on('error', (err) => { + this.log.push('console', `关闭接口请求失败:${err.message}`) + resolve(false) + }) + request.end(body) + }) + } + + armForceKill(ms) { + if (this.forceKillTimer) clearTimeout(this.forceKillTimer) + this.forceKillTimer = setTimeout(() => { + this.forceKillTimer = null + if (!this.child || !this.pid) return + this.log.push('console', `服务端未在 ${Math.round(ms / 1000)} 秒内退出,强制结束`) + try { + this.child.kill('SIGKILL') + } catch { + // 进程可能已经没了 + } + }, ms) + } + async restart() { await this.stop() this.restarts = 0 @@ -247,10 +316,30 @@ class ServiceSupervisor extends EventEmitter { } setHealthy(ok) { - if (this.healthy === ok) return + const changed = this.healthy !== ok + if (ok) { + if (!this.healthySince) this.healthySince = Date.now() + } else { + this.healthySince = null + } this.healthy = ok - this.log.push('console', ok ? '服务端健康检查通过' : '服务端健康检查失败(等待重试)') - this.emitStatus() + if (changed) { + this.log.push('console', ok ? '服务端健康检查通过' : '服务端健康检查失败(等待重试)') + } + + // 连续健康一段时间后清零崩溃计数:否则长时间赛事里历史崩溃会耗尽 + // maxRestarts 配额,之后真崩了反而不自动拉起 + let reset = false + if (ok && this.restarts > 0) { + const resetAfter = Number(this.server.healthyResetMs) || HEALTHY_RESET_MS + if (Date.now() - this.healthySince >= resetAfter) { + this.restarts = 0 + reset = true + this.log.push('console', '服务端持续健康,崩溃计数已清零') + } + } + + if (changed || reset) this.emitStatus() } dispose() { diff --git a/console/pnpm-lock.yaml b/console/pnpm-lock.yaml new file mode 100644 index 0000000..7db78a4 --- /dev/null +++ b/console/pnpm-lock.yaml @@ -0,0 +1,2413 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + electron: + specifier: ^38.0.0 + version: 38.8.6(supports-color@7.1.0) + electron-builder: + specifier: ^26.0.12 + version: 26.15.3(electron-builder-squirrel-windows@26.15.3)(supports-color@7.1.0) + +packages: + + '@electron/asar@3.2.1': + resolution: {integrity: sha512-hE2cQMZ5+4o7+6T2lUaVbxIzrOjZZfX7dB02xuapyYFJZEAiWTelq6J3mMoxzd0iONDvYLPVKecB5tyjIoVDVA==} + engines: {node: '>=10.12.0'} + hasBin: true + + '@electron/asar@3.4.1': + resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} + engines: {node: '>=10.12.0'} + hasBin: true + + '@electron/fuses@1.8.0': + resolution: {integrity: sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==} + hasBin: true + + '@electron/get@2.0.0': + resolution: {integrity: sha512-d0XfNGayKLrYRqddW4f2Zdjaj71LfvZlCnUN8ojyNMr7WD8v6B+fqdCV8Etnwn/vUfFWFwMk/HtLEFZy01h4tQ==} + engines: {node: '>=12'} + + '@electron/get@3.0.0': + resolution: {integrity: sha512-hLv4BYFiyrNRI+U0Mm2X7RxCCdJLkDUn8GCEp9QJzbLpZRko+UaLlCjOMkj6TEtirNLPyBA7y1SeGfnpOB21aQ==} + engines: {node: '>=14'} + + '@electron/notarize@2.5.0': + resolution: {integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==} + engines: {node: '>= 10.0.0'} + + '@electron/osx-sign@1.3.3': + resolution: {integrity: sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==} + engines: {node: '>=12.0.0'} + hasBin: true + + '@electron/rebuild@4.2.0': + resolution: {integrity: sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==} + engines: {node: '>=22.12.0'} + hasBin: true + + '@electron/universal@2.0.3': + resolution: {integrity: sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==} + engines: {node: '>=16.4'} + + '@electron/windows-sign@1.2.2': + resolution: {integrity: sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==} + engines: {node: '>=14.14'} + hasBin: true + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@malept/cross-spawn-promise@2.0.0': + resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} + engines: {node: '>= 12.13.0'} + + '@malept/flatpak-bundler@0.4.0': + resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} + engines: {node: '>= 10.0.0'} + + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + + '@noble/hashes@2.4.0': + resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==} + engines: {node: '>= 20.19.0'} + + '@peculiar/asn1-schema@2.9.4': + resolution: {integrity: sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==} + engines: {node: '>=14'} + + '@peculiar/json-schema@1.1.12': + resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} + engines: {node: '>=8.0.0'} + + '@peculiar/utils@2.0.3': + resolution: {integrity: sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==} + + '@peculiar/webcrypto@1.7.1': + resolution: {integrity: sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==} + engines: {node: '>=14.18.0'} + + '@sindresorhus/is@4.0.0': + resolution: {integrity: sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ==} + engines: {node: '>=10'} + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + + '@types/glob@7.2.0': + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/minimatch@6.0.0': + resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} + deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + + '@types/node@26.5.0': + resolution: {integrity: sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@xmldom/xmldom@0.8.15': + resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==} + engines: {node: '>=10.0.0'} + + '@xmldom/xmldom@0.9.12': + resolution: {integrity: sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==} + engines: {node: '>=14.6'} + + abbrev@4.0.0: + resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} + engines: {node: ^20.17.0 || >=22.9.0} + + agent-base@7.0.2: + resolution: {integrity: sha512-k2/tQ1+8Zf50dEUJWklUP80LcE/+Ph+OJ6cf2Ff2fD/c/TtCe6ofnCoNMz9UnyxOQYlaAALZtEWETzn+1JjfHg==} + engines: {node: '>= 14'} + + agent-base@7.1.0: + resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} + engines: {node: '>= 14'} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.0.0: + resolution: {integrity: sha512-8zjUtFJ3db/QoPXuuEMloS2AUf79/yeyttJ7Abr3hteopJu9HK8vsgGviGUMq+zyA6cZZO6gAyZoMTF6TgaEjA==} + engines: {node: '>=8'} + + ansi-styles@4.1.0: + resolution: {integrity: sha512-Qts4KCLKG+waHc9C4m07weIY8qyeixoS0h6RnbsNVD6Fw+pEZGW3vTyObL3WXpE09Mq4Oi7/lBEyLmOiLtlYWQ==} + engines: {node: '>=8'} + + app-builder-lib@26.15.3: + resolution: {integrity: sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==} + engines: {node: '>=14.0.0'} + peerDependencies: + dmg-builder: 26.15.3 + electron-builder-squirrel-windows: 26.15.3 + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + asn1js@3.0.10: + resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==} + engines: {node: '>=12.0.0'} + + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + + balanced-match@1.0.0: + resolution: {integrity: sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + builder-util-runtime@9.7.0: + resolution: {integrity: sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==} + engines: {node: '>=12.0.0'} + + builder-util@26.15.3: + resolution: {integrity: sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==} + engines: {node: '>=14.0.0'} + + bytestreamjs@2.0.1: + resolution: {integrity: sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==} + engines: {node: '>=6.0.0'} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.2: + resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + chalk@4.1.0: + resolution: {integrity: sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==} + engines: {node: '>=10'} + + chalk@4.1.1: + resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==} + engines: {node: '>=10'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + chromium-pickle-js@0.2.0: + resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + + ci-info@4.3.1: + resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} + engines: {node: '>=8'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@5.0.0: + resolution: {integrity: sha512-JrDGPAKjMGSP1G0DUoaceEJ3DZgAfr/q6X7FVk4+U5KxUSKviYGM2k6zWkfyyBHy5rAtzgYJFa1ro2O9PtoxwQ==} + engines: {node: '>= 6'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + compare-version@0.1.2: + resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} + engines: {node: '>=0.10.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cross-dirname@0.1.0: + resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + dir-compare@4.2.0: + resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} + + dmg-builder@26.15.3: + resolution: {integrity: sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==} + + dotenv-expand@11.0.6: + resolution: {integrity: sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==} + engines: {node: '>=12'} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + + ejs@3.1.8: + resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-builder-squirrel-windows@26.15.3: + resolution: {integrity: sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==} + + electron-builder@26.15.3: + resolution: {integrity: sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==} + engines: {node: '>=14.0.0'} + hasBin: true + + electron-publish@26.15.3: + resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==} + + electron-winstaller@5.4.0: + resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} + engines: {node: '>=8.0.0'} + + electron@38.8.6: + resolution: {integrity: sha512-lyBhcVi9QYAZL6FO6r5twAWAjWnYomo3iVDvrb5SJZlq928BGemHOKG0tPIq41NOLaCu9f3XdEEjMkjQPjprRg==} + engines: {node: '>= 12.20.55'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + env-paths@2.2.0: + resolution: {integrity: sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.7: + resolution: {integrity: sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + filelist@1.0.6: + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.1: + resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} + engines: {node: '>=14.14'} + + fs-extra@11.4.0: + resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} + engines: {node: '>=14.14'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.0.0: + resolution: {integrity: sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g==} + engines: {node: '>=10'} + + fs-extra@9.0.1: + resolution: {integrity: sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==} + engines: {node: '>=10'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + glob@7.2.0: + resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + + global-tunnel-ng@2.7.1: + resolution: {integrity: sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==} + engines: {node: '>=0.10'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@11.8.5: + resolution: {integrity: sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==} + engines: {node: '>=10.19.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-proxy-agent@7.0.0: + resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} + engines: {node: '>= 14'} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + + https-proxy-agent@7.0.0: + resolution: {integrity: sha512-0euwPCRyAPSgGdzD1IVN9nJYHtBhJwb6XPfbpQcYbPCwrBidX6GzxmchnaF4sfF/jPb74Ojx5g4yTg3sixlyPw==} + engines: {node: '>= 14'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.4: + resolution: {integrity: sha512-VUA7WAWNCWfm6/8f9kAb8Y6iGBWnmCfgFS5dTrv2C38LLm1KUmpY388mCVCJCsMKQomvOQ1oW8/edXdChd9ZXQ==} + deprecated: Please update to ini >=1.3.6 to avoid a prototype pollution issue + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isbinaryfile@5.0.7: + resolution: {integrity: sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==} + engines: {node: '>= 18.0.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} + + jake@10.8.5: + resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==} + engines: {node: '>=10'} + hasBin: true + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + keyv@4.0.0: + resolution: {integrity: sha512-U7ioE8AimvRVLfw4LffyOIRhL2xVgmE8T22L6i0BucSnBUyv4w+I7VN/zVZwRKHOI6ZRUcdMdWHQ8KSUvGpEog==} + + lazy-val@1.0.5: + resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + node-abi@4.35.0: + resolution: {integrity: sha512-ymk4aIzxdPopw2giv8Fs1Ec6vybGkjmyxUwVqhkI4MCy2tVfXdkOGGWieWVjL0THgH+7a8lRdevyupoYj3Js/Q==} + engines: {node: '>=22.12.0'} + + node-api-version@0.2.1: + resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} + + node-gyp@12.4.0: + resolution: {integrity: sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + nopt@9.0.0: + resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + normalize-url@6.0.1: + resolution: {integrity: sha512-VU4pzAuh7Kip71XEmO9aNREYAdMHFGTVj/i+CaTImS8x0i1d3jUZkXhqluy/PRgjPLMgsLQulYY3PJ/aSbSjpQ==} + engines: {node: '>=10'} + + npm-conf@1.1.3: + resolution: {integrity: sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==} + engines: {node: '>=4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pe-library@0.4.1: + resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} + engines: {node: '>=12', npm: '>=6'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + + pkijs@3.4.0: + resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==} + engines: {node: '>=16.0.0'} + + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + + plist@3.1.1: + resolution: {integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==} + engines: {node: '>=10.4.0'} + + postject@1.0.0-alpha.6: + resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} + engines: {node: '>=14.0.0'} + hasBin: true + + proc-log@6.0.0: + resolution: {integrity: sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA==} + engines: {node: ^20.17.0 || >=22.9.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.2.0: + resolution: {integrity: sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==} + engines: {node: '>=16.0.0'} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + read-binary-file-arch@1.0.6: + resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==} + hasBin: true + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resedit@1.7.0: + resolution: {integrity: sha512-dbsZ0gk5opWPFlKMqvxCrLCuMZUVmsW3yTPT0tT4mYwo5fjQM8c4HMN9ZJt6dRDqDV/78m9SU4rv24PN4NiYaA==} + engines: {node: '>=12', npm: '>=6'} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + rimraf@2.6.2: + resolution: {integrity: sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + roarr@2.15.3: + resolution: {integrity: sha512-AEjYvmAhlyxOeB9OqPUzQCo3kuAkNfuDk/HqWbZdFsqDFpapkTjiw+p4svNEoRLvuqNTxqfL+s+gtD4eDgZ+CA==} + engines: {node: '>=8.0'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + sanitize-filename@1.6.4: + resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver@5.5.0: + resolution: {integrity: sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==} + hasBin: true + + semver@6.2.0: + resolution: {integrity: sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.0: + resolution: {integrity: sha512-mTozplhTX4tLKIHYji92OTZzVyZvi+Z1qRZDeBvQFI2XUB89wrRoj/xXad3c9NZ1GPJXXRvB+k41PQCPTMC+aA==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + stat-mode@1.0.0: + resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} + engines: {node: '>= 6'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + + supports-color@7.1.0: + resolution: {integrity: sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==} + engines: {node: '>=8'} + + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} + engines: {node: '>=18'} + + temp-file@3.4.0: + resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + + temp@0.9.4: + resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} + engines: {node: '>=6.0.0'} + + tiny-async-pool@1.3.0: + resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} + + undici@6.28.1: + resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==} + engines: {node: '>=18.17'} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@1.0.0: + resolution: {integrity: sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==} + engines: {node: '>= 10.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unzipper@0.12.5: + resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==} + + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + webcrypto-core@1.9.2: + resolution: {integrity: sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==} + + which@2.0.1: + resolution: {integrity: sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w==} + engines: {node: '>= 8'} + hasBin: true + + which@5.0.0: + resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@electron/asar@3.2.1': + dependencies: + chromium-pickle-js: 0.2.0 + commander: 5.0.0 + glob: 7.2.0 + minimatch: 3.1.5 + optionalDependencies: + '@types/glob': 7.2.0 + + '@electron/asar@3.4.1': + dependencies: + commander: 5.0.0 + glob: 7.2.0 + minimatch: 3.1.5 + + '@electron/fuses@1.8.0': + dependencies: + chalk: 4.1.1 + fs-extra: 9.0.1 + minimist: 1.2.8 + + '@electron/get@2.0.0(supports-color@7.1.0)': + dependencies: + debug: 4.4.3(supports-color@7.1.0) + env-paths: 2.2.0 + fs-extra: 8.1.0 + got: 11.8.5 + progress: 2.0.3 + semver: 6.2.0 + sumchecker: 3.0.1(supports-color@7.1.0) + optionalDependencies: + global-agent: 3.0.0 + global-tunnel-ng: 2.7.1 + transitivePeerDependencies: + - supports-color + + '@electron/get@3.0.0(supports-color@7.1.0)': + dependencies: + debug: 4.4.3(supports-color@7.1.0) + env-paths: 2.2.0 + fs-extra: 8.1.0 + got: 11.8.5 + progress: 2.0.3 + semver: 6.2.0 + sumchecker: 3.0.1(supports-color@7.1.0) + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@electron/notarize@2.5.0(supports-color@7.1.0)': + dependencies: + debug: 4.4.3(supports-color@7.1.0) + fs-extra: 9.0.1 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@electron/osx-sign@1.3.3(supports-color@7.1.0)': + dependencies: + compare-version: 0.1.2 + debug: 4.4.3(supports-color@7.1.0) + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@electron/rebuild@4.2.0(supports-color@7.1.0)': + dependencies: + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.3(supports-color@7.1.0) + node-abi: 4.35.0 + node-api-version: 0.2.1 + node-gyp: 12.4.0 + read-binary-file-arch: 1.0.6(supports-color@7.1.0) + transitivePeerDependencies: + - supports-color + + '@electron/universal@2.0.3(supports-color@7.1.0)': + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.3(supports-color@7.1.0) + dir-compare: 4.2.0 + fs-extra: 11.4.0 + minimatch: 9.0.9 + plist: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@electron/windows-sign@1.2.2(supports-color@7.1.0)': + dependencies: + cross-dirname: 0.1.0 + debug: 4.4.3(supports-color@7.1.0) + fs-extra: 11.4.0 + minimist: 1.2.8 + postject: 1.0.0-alpha.6 + transitivePeerDependencies: + - supports-color + optional: true + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + + '@malept/cross-spawn-promise@2.0.0': + dependencies: + cross-spawn: 7.0.6 + + '@malept/flatpak-bundler@0.4.0(supports-color@7.1.0)': + dependencies: + debug: 4.4.3(supports-color@7.1.0) + fs-extra: 9.0.0 + lodash: 4.18.1 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - supports-color + + '@noble/hashes@1.4.0': {} + + '@noble/hashes@2.4.0': {} + + '@peculiar/asn1-schema@2.9.4': + dependencies: + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/json-schema@1.1.12': + dependencies: + tslib: 2.8.1 + + '@peculiar/utils@2.0.3': + dependencies: + tslib: 2.8.1 + + '@peculiar/webcrypto@1.7.1': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/json-schema': 1.1.12 + '@peculiar/utils': 2.0.3 + tslib: 2.8.1 + webcrypto-core: 1.9.2 + + '@sindresorhus/is@4.0.0': {} + + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 22.20.1 + '@types/responselike': 1.0.3 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 26.5.0 + + '@types/glob@7.2.0': + dependencies: + '@types/minimatch': 6.0.0 + '@types/node': 26.5.0 + optional: true + + '@types/http-cache-semantics@4.2.0': {} + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 22.20.1 + + '@types/minimatch@6.0.0': + dependencies: + minimatch: 3.1.5 + optional: true + + '@types/ms@2.1.0': {} + + '@types/node@22.20.1': + dependencies: + undici-types: 6.21.0 + + '@types/node@26.5.0': + dependencies: + undici-types: 8.9.0 + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 22.20.1 + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.20.1 + optional: true + + '@xmldom/xmldom@0.8.15': {} + + '@xmldom/xmldom@0.9.12': {} + + abbrev@4.0.0: {} + + agent-base@7.0.2(supports-color@7.1.0): + dependencies: + debug: 4.4.3(supports-color@7.1.0) + transitivePeerDependencies: + - supports-color + + agent-base@7.1.0(supports-color@7.1.0): + dependencies: + debug: 4.4.3(supports-color@7.1.0) + transitivePeerDependencies: + - supports-color + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.7 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.0.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@4.1.0: + dependencies: + color-convert: 2.0.1 + + app-builder-lib@26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3)(supports-color@7.1.0): + dependencies: + '@electron/asar': 3.4.1 + '@electron/fuses': 1.8.0 + '@electron/get': 3.0.0(supports-color@7.1.0) + '@electron/notarize': 2.5.0(supports-color@7.1.0) + '@electron/osx-sign': 1.3.3(supports-color@7.1.0) + '@electron/rebuild': 4.2.0(supports-color@7.1.0) + '@electron/universal': 2.0.3(supports-color@7.1.0) + '@malept/flatpak-bundler': 0.4.0(supports-color@7.1.0) + '@noble/hashes': 2.4.0 + '@peculiar/webcrypto': 1.7.1 + '@types/fs-extra': 9.0.13 + ajv: 8.20.0 + asn1js: 3.0.10 + async-exit-hook: 2.0.1 + builder-util: 26.15.3(supports-color@7.1.0) + builder-util-runtime: 9.7.0(supports-color@7.1.0) + chromium-pickle-js: 0.2.0 + ci-info: 4.3.1 + debug: 4.4.3(supports-color@7.1.0) + dmg-builder: 26.15.3(electron-builder-squirrel-windows@26.15.3)(supports-color@7.1.0) + dotenv: 16.4.5 + dotenv-expand: 11.0.6 + ejs: 3.1.8 + electron-builder-squirrel-windows: 26.15.3(dmg-builder@26.15.3)(supports-color@7.1.0) + electron-publish: 26.15.3(supports-color@7.1.0) + fs-extra: 10.1.0 + hosted-git-info: 4.1.0 + isbinaryfile: 5.0.7 + jiti: 2.7.0 + js-yaml: 4.3.2 + json5: 2.2.3 + lazy-val: 1.0.5 + minimatch: 10.2.6 + pkijs: 3.4.0 + plist: 3.1.0 + proper-lockfile: 4.1.2 + resedit: 1.7.0 + semver: 7.7.4 + tar: 7.5.22 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + unzipper: 0.12.5 + which: 5.0.0 + transitivePeerDependencies: + - supports-color + + argparse@2.0.1: {} + + asn1js@3.0.10: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.2.0 + tslib: 2.8.1 + + async-exit-hook@2.0.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + aws4@1.13.2: {} + + balanced-match@1.0.0: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + bluebird@3.7.2: {} + + boolean@3.2.0: + optional: true + + brace-expansion@1.1.18: + dependencies: + balanced-match: 1.0.0 + concat-map: 0.0.1 + + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.0 + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + buffer-crc32@0.2.13: {} + + buffer-from@1.1.2: {} + + builder-util-runtime@9.7.0(supports-color@7.1.0): + dependencies: + debug: 4.4.3(supports-color@7.1.0) + sax: 1.6.1 + transitivePeerDependencies: + - supports-color + + builder-util@26.15.3(supports-color@7.1.0): + dependencies: + '@types/debug': 4.1.13 + builder-util-runtime: 9.7.0(supports-color@7.1.0) + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@7.1.0) + fs-extra: 10.1.0 + http-proxy-agent: 7.0.0(supports-color@7.1.0) + https-proxy-agent: 7.0.0(supports-color@7.1.0) + js-yaml: 4.3.2 + sanitize-filename: 1.6.4 + source-map-support: 0.5.21 + stat-mode: 1.0.0 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + transitivePeerDependencies: + - supports-color + + bytestreamjs@2.0.1: {} + + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.2: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.0.0 + lowercase-keys: 2.0.0 + normalize-url: 6.0.1 + responselike: 2.0.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + chalk@4.1.0: + dependencies: + ansi-styles: 4.1.0 + supports-color: 7.1.0 + + chalk@4.1.1: + dependencies: + ansi-styles: 4.1.0 + supports-color: 7.1.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.1.0 + supports-color: 7.1.0 + + chownr@3.0.0: {} + + chromium-pickle-js@0.2.0: {} + + ci-info@4.3.1: {} + + ci-info@4.4.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@5.0.0: {} + + commander@9.5.0: + optional: true + + compare-version@0.1.2: {} + + concat-map@0.0.1: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.4 + proto-list: 1.2.4 + optional: true + + core-util-is@1.0.3: {} + + cross-dirname@0.1.0: + optional: true + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.1 + + debug@4.4.3(supports-color@7.1.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 7.1.0 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + defer-to-connect@2.0.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + optional: true + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + optional: true + + delayed-stream@1.0.0: {} + + detect-node@2.1.0: + optional: true + + dir-compare@4.2.0: + dependencies: + minimatch: 3.1.5 + p-limit: 3.1.0 + + dmg-builder@26.15.3(electron-builder-squirrel-windows@26.15.3)(supports-color@7.1.0): + dependencies: + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3)(supports-color@7.1.0) + builder-util: 26.15.3(supports-color@7.1.0) + fs-extra: 10.1.0 + js-yaml: 4.3.2 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + + dotenv-expand@11.0.6: + dependencies: + dotenv: 16.4.5 + + dotenv@16.4.5: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + + ejs@3.1.8: + dependencies: + jake: 10.8.5 + + electron-builder-squirrel-windows@26.15.3(dmg-builder@26.15.3)(supports-color@7.1.0): + dependencies: + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3)(supports-color@7.1.0) + builder-util: 26.15.3(supports-color@7.1.0) + electron-winstaller: 5.4.0(supports-color@7.1.0) + transitivePeerDependencies: + - dmg-builder + - supports-color + + electron-builder@26.15.3(electron-builder-squirrel-windows@26.15.3)(supports-color@7.1.0): + dependencies: + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3)(supports-color@7.1.0) + builder-util: 26.15.3(supports-color@7.1.0) + builder-util-runtime: 9.7.0(supports-color@7.1.0) + chalk: 4.1.2 + ci-info: 4.4.0 + dmg-builder: 26.15.3(electron-builder-squirrel-windows@26.15.3)(supports-color@7.1.0) + fs-extra: 10.1.0 + lazy-val: 1.0.5 + simple-update-notifier: 2.0.0 + yargs: 17.7.3 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + + electron-publish@26.15.3(supports-color@7.1.0): + dependencies: + '@types/fs-extra': 9.0.13 + aws4: 1.13.2 + builder-util: 26.15.3(supports-color@7.1.0) + builder-util-runtime: 9.7.0(supports-color@7.1.0) + chalk: 4.1.2 + form-data: 4.0.6 + fs-extra: 10.1.0 + lazy-val: 1.0.5 + mime: 2.6.0 + transitivePeerDependencies: + - supports-color + + electron-winstaller@5.4.0(supports-color@7.1.0): + dependencies: + '@electron/asar': 3.2.1 + debug: 4.4.3(supports-color@7.1.0) + fs-extra: 7.0.1 + lodash: 4.18.1 + temp: 0.9.4 + optionalDependencies: + '@electron/windows-sign': 1.2.2(supports-color@7.1.0) + transitivePeerDependencies: + - supports-color + + electron@38.8.6(supports-color@7.1.0): + dependencies: + '@electron/get': 2.0.0(supports-color@7.1.0) + '@types/node': 22.20.1 + extract-zip: 2.0.1(supports-color@7.1.0) + transitivePeerDependencies: + - supports-color + + emoji-regex@8.0.0: {} + + encodeurl@1.0.2: + optional: true + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + env-paths@2.2.0: {} + + err-code@2.0.3: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es6-error@4.1.1: + optional: true + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: + optional: true + + exponential-backoff@3.1.3: {} + + extract-zip@2.0.1(supports-color@7.1.0): + dependencies: + debug: 4.4.3(supports-color@7.1.0) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.7: {} + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + filelist@1.0.6: + dependencies: + minimatch: 5.1.9 + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-extra@11.3.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-extra@11.4.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.0.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 1.0.0 + + fs-extra@9.0.1: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 1.0.0 + + fs.realpath@1.0.0: {} + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + glob@7.2.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.3 + semver: 7.8.5 + serialize-error: 7.0.1 + optional: true + + global-tunnel-ng@2.7.1: + dependencies: + encodeurl: 1.0.2 + lodash: 4.18.1 + npm-conf: 1.1.3 + tunnel: 0.0.6 + optional: true + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + optional: true + + gopd@1.2.0: {} + + got@11.8.5: + dependencies: + '@sindresorhus/is': 4.0.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.2 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + optional: true + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + http-cache-semantics@4.2.0: {} + + http-proxy-agent@7.0.0(supports-color@7.1.0): + dependencies: + agent-base: 7.1.0(supports-color@7.1.0) + debug: 4.4.3(supports-color@7.1.0) + transitivePeerDependencies: + - supports-color + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + https-proxy-agent@7.0.0(supports-color@7.1.0): + dependencies: + agent-base: 7.0.2(supports-color@7.1.0) + debug: 4.4.3(supports-color@7.1.0) + transitivePeerDependencies: + - supports-color + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.4: + optional: true + + is-fullwidth-code-point@3.0.0: {} + + isarray@1.0.0: {} + + isbinaryfile@4.0.10: {} + + isbinaryfile@5.0.7: {} + + isexe@2.0.0: {} + + isexe@3.1.5: {} + + isexe@4.0.0: {} + + jake@10.8.5: + dependencies: + async: 3.2.6 + chalk: 4.1.0 + filelist: 1.0.6 + minimatch: 3.1.5 + + jiti@2.7.0: {} + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@1.0.0: {} + + json-stringify-safe@5.0.1: + optional: true + + json5@2.2.3: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.0.0: + dependencies: + json-buffer: 3.0.1 + + lazy-val@1.0.5: {} + + lodash@4.18.1: {} + + lowercase-keys@2.0.0: {} + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@2.6.0: {} + + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.18 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.4 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.4 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + ms@2.1.3: {} + + node-abi@4.35.0: + dependencies: + semver: 7.7.4 + + node-api-version@0.2.1: + dependencies: + semver: 7.7.4 + + node-gyp@12.4.0: + dependencies: + env-paths: 2.2.0 + exponential-backoff: 3.1.3 + graceful-fs: 4.2.11 + nopt: 9.0.0 + proc-log: 6.0.0 + semver: 7.7.4 + tar: 7.5.22 + tinyglobby: 0.2.17 + undici: 6.28.1 + which: 6.0.1 + + node-int64@0.4.0: {} + + nopt@9.0.0: + dependencies: + abbrev: 4.0.0 + + normalize-url@6.0.1: {} + + npm-conf@1.1.3: + dependencies: + config-chain: 1.1.13 + pify: 3.0.0 + optional: true + + object-keys@1.1.1: + optional: true + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + p-cancelable@2.1.1: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + pe-library@0.4.1: {} + + pend@1.2.0: {} + + picomatch@4.0.7: {} + + pify@3.0.0: + optional: true + + pkijs@3.4.0: + dependencies: + '@noble/hashes': 1.4.0 + asn1js: 3.0.10 + bytestreamjs: 2.0.1 + pvtsutils: 1.3.6 + pvutils: 1.2.0 + tslib: 2.8.1 + + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.15 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + plist@3.1.1: + dependencies: + '@xmldom/xmldom': 0.9.12 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + postject@1.0.0-alpha.6: + dependencies: + commander: 9.5.0 + optional: true + + proc-log@6.0.0: {} + + process-nextick-args@2.0.1: {} + + progress@2.0.3: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + proto-list@1.2.4: + optional: true + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.2.0: {} + + quick-lru@5.1.1: {} + + read-binary-file-arch@1.0.6(supports-color@7.1.0): + dependencies: + debug: 4.4.3(supports-color@7.1.0) + transitivePeerDependencies: + - supports-color + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resedit@1.7.0: + dependencies: + pe-library: 0.4.1 + + resolve-alpn@1.2.1: {} + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + retry@0.12.0: {} + + rimraf@2.6.2: + dependencies: + glob: 7.2.0 + + roarr@2.15.3: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + + safe-buffer@5.1.2: {} + + sanitize-filename@1.6.4: + dependencies: + truncate-utf8-bytes: 1.0.2 + + sax@1.6.1: {} + + semver-compare@1.0.0: + optional: true + + semver@5.5.0: {} + + semver@6.2.0: {} + + semver@7.7.4: {} + + semver@7.8.5: {} + + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: {} + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.8.5 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.0 + + source-map@0.6.0: {} + + sprintf-js@1.1.3: + optional: true + + stat-mode@1.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + sumchecker@3.0.1(supports-color@7.1.0): + dependencies: + debug: 4.4.3(supports-color@7.1.0) + transitivePeerDependencies: + - supports-color + + supports-color@7.1.0: + dependencies: + has-flag: 4.0.0 + + tar@7.5.22: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + temp-file@3.4.0: + dependencies: + async-exit-hook: 2.0.1 + fs-extra: 10.1.0 + + temp@0.9.4: + dependencies: + mkdirp: 0.5.6 + rimraf: 2.6.2 + + tiny-async-pool@1.3.0: + dependencies: + semver: 5.5.0 + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.7 + + tmp@0.2.7: {} + + truncate-utf8-bytes@1.0.2: + dependencies: + utf8-byte-length: 1.0.5 + + tslib@2.8.1: {} + + tunnel@0.0.6: + optional: true + + type-fest@0.13.1: + optional: true + + undici-types@6.21.0: {} + + undici-types@8.9.0: {} + + undici@6.28.1: {} + + universalify@0.1.2: {} + + universalify@1.0.0: {} + + universalify@2.0.1: {} + + unzipper@0.12.5: + dependencies: + bluebird: 3.7.2 + duplexer2: 0.1.4 + fs-extra: 11.3.1 + graceful-fs: 4.2.11 + node-int64: 0.4.0 + + utf8-byte-length@1.0.5: {} + + util-deprecate@1.0.2: {} + + webcrypto-core@1.9.2: + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/json-schema': 1.1.12 + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + + which@2.0.1: + dependencies: + isexe: 2.0.0 + + which@5.0.0: + dependencies: + isexe: 3.1.5 + + which@6.0.1: + dependencies: + isexe: 4.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + xmlbuilder@15.1.1: {} + + y18n@5.0.8: {} + + yallist@4.0.0: {} + + yallist@5.0.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yocto-queue@0.1.0: {} diff --git a/console/renderer/preload.js b/console/renderer/preload.js index f23d7de..a286c45 100644 --- a/console/renderer/preload.js +++ b/console/renderer/preload.js @@ -17,7 +17,6 @@ contextBridge.exposeInMainWorld('consoleApi', { getLog: () => ipcRenderer.invoke('log:tail'), clearLog: () => ipcRenderer.invoke('log:clear'), relayoutDisplays: () => ipcRenderer.invoke('display:relayout'), - openExternal: (url) => ipcRenderer.invoke('app:open-external', url), openServerAdmin: () => ipcRenderer.invoke('app:open-server-admin'), minimize: () => ipcRenderer.invoke('window:minimize'), maximize: () => ipcRenderer.invoke('window:maximize'), diff --git a/server/go.mod b/server/go.mod index 731221a..0a9d06e 100644 --- a/server/go.mod +++ b/server/go.mod @@ -3,6 +3,8 @@ module git.yeij.top/AskaEth/Scoreboard/server go 1.26.0 require ( + github.com/xuri/excelize/v2 v2.11.0 + golang.org/x/crypto v0.57.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.58.0 ) @@ -18,9 +20,7 @@ require ( github.com/richardlehane/msoleps v1.0.6 // indirect github.com/tiendc/go-deepcopy v1.7.2 // indirect github.com/xuri/efp v0.0.1 // indirect - github.com/xuri/excelize/v2 v2.11.0 // indirect github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect - golang.org/x/crypto v0.57.0 // indirect golang.org/x/net v0.58.0 // indirect golang.org/x/sys v0.48.0 // indirect golang.org/x/text v0.42.0 // indirect diff --git a/server/internal/domain/ledger.go b/server/internal/domain/ledger.go index 903c130..599d083 100644 --- a/server/internal/domain/ledger.go +++ b/server/internal/domain/ledger.go @@ -162,12 +162,13 @@ func SumByAthlete(entries []LedgerEntry) map[int64]float64 { return out } -// RecomputeEventLedger 重算某个小项的流水:旧流水整项作废,再追加新流水。 +// RecomputeEventLedger 重算某个小项的成绩分与破纪录加分:旧流水整项作废,再追加新流水。 +// 人工调整不参与重算,原样保留——否则每重算一次就把人工扣/加分抹掉。 // 新旧集合一致时原样返回——重复发布同一组必须幂等,不能凭空多出流水。 func RecomputeEventLedger(existing []LedgerEntry, eventID int64, fresh []LedgerEntry, by, reason string) ([]LedgerEntry, error) { active := make([]LedgerEntry, 0, len(existing)) for _, e := range existing { - if e.EventID == eventID && !e.Voided { + if e.EventID == eventID && !e.Voided && recomputableSource(e.SourceType) { active = append(active, e) } } @@ -175,10 +176,24 @@ func RecomputeEventLedger(existing []LedgerEntry, eventID int64, fresh []LedgerE return append([]LedgerEntry(nil), existing...), nil } - out, _ := VoidByEvent(existing, eventID, by, reason) + out := append([]LedgerEntry(nil), existing...) + for i := range out { + if out[i].Voided || out[i].EventID != eventID || !recomputableSource(out[i].SourceType) { + continue + } + out[i].Voided = true + out[i].VoidedBy = by + out[i].VoidReason = reason + } return AppendLedger(out, fresh...) } +// recomputableSource 判断流水来源是否属于「重算范围」:只有引擎产出的成绩分与破纪录加分, +// 人工调整由人负责,重算不得触碰。 +func recomputableSource(t SourceType) bool { + return t == SourceResult || t == SourceRecordBonus +} + // ApplyLedgerOps 按给定顺序应用增量操作。 // 各操作只按来源/事件定位流水,互不依赖顺序,因此乱序到达也能收敛到同一结果。 func ApplyLedgerOps(existing []LedgerEntry, ops []LedgerOp) ([]LedgerEntry, error) { diff --git a/server/internal/domain/records.go b/server/internal/domain/records.go index e536a39..b3750cf 100644 --- a/server/internal/domain/records.go +++ b/server/internal/domain/records.go @@ -84,6 +84,7 @@ func RecordBonusPoints(cfg RecordBonusConfig, breaks []RecordBreak) []RecordBonu // recordBonusLedger 把破纪录加分写成流水。 // 加分与成绩分相互独立:不乘项目权重、也不乘组别权重(case 6 的口径)。 +// 只处理本组别的报名项:上层按组别逐个调用 ScoreEvent,不加组别过滤会让同一笔加分每组各记一次。 func recordBonusLedger(in EventScoreInput) ([]LedgerEntry, error) { if in.RecordBonus == nil || !in.RecordBonus.Enabled || len(in.RecordBreaks) == 0 { return nil, nil @@ -103,6 +104,9 @@ func recordBonusLedger(in EventScoreInput) ([]LedgerEntry, error) { byID := make(map[int64]Entry, len(in.Entries)) for _, e := range in.Entries { + if in.Category.ID != 0 && e.CategoryID != in.Category.ID { + continue + } byID[e.ID] = e } diff --git a/server/internal/domain/scoring.go b/server/internal/domain/scoring.go index cae0fa6..d3b74b4 100644 --- a/server/internal/domain/scoring.go +++ b/server/internal/domain/scoring.go @@ -12,6 +12,7 @@ const rankEpsilon = 1e-9 // ScoreOptions 控制引擎的可选行为;零值即本次运动会的口径。 type ScoreOptions struct { // UnionAttribution 决定联队成绩的归属;空值等同 UnionWhole(整体记给主单位)。 + // 未实现:生产链路没有赋值入口,联队份额也无处加载(见 UnionAttribution 注释)。 UnionAttribution UnionAttribution // IncludeDrafts 为 true 时草稿成绩也参与计分;默认只算已发布(草稿不上屏也不计分)。 IncludeDrafts bool @@ -57,26 +58,44 @@ type RankedResult struct { } // ScoreEvent 按项目的计分通道逐条计算名次与积分,输出流水与名次明细。 -// 项目 scored=false 或没有通道时不产生任何流水(只排名不计分)。 +// 计分与排名解耦:项目 scored=false 或没有通道时照样产出名次(Points=0),只是不写流水。 +// 没有通道时按小项自身的录入方式(result_mode)推导排名口径,见 rankOnlyChannel。 func ScoreEvent(in EventScoreInput) (EventScore, error) { var out EventScore - if !in.Event.Scored { - return out, nil + rankOnly := len(in.Channels) == 0 + channels := sortedChannels(in.Channels) + if rankOnly { + channels = []ScoreChannel{rankOnlyChannel(in)} } - for _, ch := range sortedChannels(in.Channels) { - profile, ok := in.Profiles[ch.ProfileID] - if !ok { - return EventScore{}, fmt.Errorf("%w: 通道 %d 引用模板 %d", ErrProfileNotFound, ch.ID, ch.ProfileID) + for _, ch := range channels { + // 不计分时不需要积分表:既不查模板,也不可能算出非零分。 + profile := ScoringProfile{} + if in.Event.Scored && !rankOnly { + p, ok := in.Profiles[ch.ProfileID] + if !ok { + return EventScore{}, fmt.Errorf("%w: 通道 %d 引用模板 %d", ErrProfileNotFound, ch.ID, ch.ProfileID) + } + profile = p } ranked, err := scoreChannel(in, ch, profile) if err != nil { return EventScore{}, err } + if !in.Event.Scored { + for i := range ranked { + ranked[i].Points = 0 + } + } out.Ranked = append(out.Ranked, ranked...) - out.Ledger = append(out.Ledger, ledgerFromRanked(in, ch, ranked)...) + if in.Event.Scored { + out.Ledger = append(out.Ledger, ledgerFromRanked(in, ch, ranked)...) + } } + if !in.Event.Scored { + return out, nil + } bonus, err := recordBonusLedger(in) if err != nil { return EventScore{}, err @@ -85,6 +104,20 @@ func ScoreEvent(in EventScoreInput) (EventScore, error) { return out, nil } +// rankOnlyChannel 在没有计分通道时,按小项自身的录入方式推导排名口径: +// manual 直接录名次,其余按成绩自动排名;名次记给个人,团体/对抗项目记给单位。 +func rankOnlyChannel(in EventScoreInput) ScoreChannel { + basis := BasisResult + if in.Event.ResultMode == ResultModeManual { + basis = BasisManual + } + scope := ScopeAthlete + if in.Event.EventType == EventTypeTeam || in.Event.EventType == EventTypeMatch { + scope = ScopeTeam + } + return ScoreChannel{EventID: in.Event.ID, Scope: scope, Basis: basis} +} + // sortedChannels 固定通道处理顺序,保证同一份输入每次产出同样的流水顺序。 func sortedChannels(channels []ScoreChannel) []ScoreChannel { out := append([]ScoreChannel(nil), channels...) @@ -325,22 +358,17 @@ func rankedResult(in EventScoreInput, ch ScoreChannel, c candidate, rank int, po return rr } -// scoreByAggregate 先把同一主体的成绩汇总成一个值,再按值排名(预留能力)。 +// scoreByAggregate 先把同一主体的成绩汇总成一个值(求和),再按值排名(预留能力)。 func scoreByAggregate(in EventScoreInput, ch ScoreChannel, profile ScoringProfile) ([]RankedResult, error) { cands, err := collectCandidates(in, ch) if err != nil { return nil, err } - method := ch.Aggregate - if method == "" { - method = AggregateSum - } - type bucket struct { subject int64 entry Entry - values []float64 + total float64 } order := make([]int64, 0) buckets := make(map[int64]*bucket) @@ -358,13 +386,13 @@ func scoreByAggregate(in EventScoreInput, ch ScoreChannel, profile ScoringProfil buckets[subject] = b order = append(order, subject) } - b.values = append(b.values, c.value) + b.total += c.value } agg := make([]candidate, 0, len(order)) for _, id := range order { b := buckets[id] - c := candidate{entry: b.entry, subject: b.subject, value: aggregateValue(method, b.values)} + c := candidate{entry: b.entry, subject: b.subject, value: b.total} c.entry.ID = 0 // 聚合结果不对应单条报名项 agg = append(agg, c) } @@ -408,44 +436,6 @@ func scoreByAggregate(in EventScoreInput, ch ScoreChannel, profile ScoringProfil return out, nil } -func aggregateValue(method AggregateMethod, values []float64) float64 { - if len(values) == 0 { - return 0 - } - switch method { - case AggregateAvg: - var sum float64 - for _, v := range values { - sum += v - } - return sum / float64(len(values)) - case AggregateMax: - best := values[0] - for _, v := range values[1:] { - if v > best { - best = v - } - } - return best - case AggregateMin: - best := values[0] - for _, v := range values[1:] { - if v < best { - best = v - } - } - return best - case AggregateCount: - return float64(len(values)) - default: - var sum float64 - for _, v := range values { - sum += v - } - return sum - } -} - func sortCandidatesByValue(cands []candidate, compare CompareMode) { sort.SliceStable(cands, func(i, j int) bool { if nearlyEqual(cands[i].value, cands[j].value) { diff --git a/server/internal/domain/standings.go b/server/internal/domain/standings.go index 6cecba0..a3c2c64 100644 --- a/server/internal/domain/standings.go +++ b/server/internal/domain/standings.go @@ -2,12 +2,32 @@ package domain import "sort" +// 榜单排序键,取值与规则包 standings.tieBreak 一致。 +// 定义在 domain 是为了让 rules 包复用同一套键名,避免两边各写一份常量。 +const ( + TieBreakPoints = "points" + TieBreakGold = "gold" + TieBreakSilver = "silver" + TieBreakBronze = "bronze" + TieBreakTeamCode = "teamCode" + TieBreakName = "name" +) + +// TieBreak 未配置时的口径,与 devdocs 02 §5.2 一致。 +var ( + defaultTeamTieBreak = []string{TieBreakPoints, TieBreakGold, TieBreakSilver, TieBreakBronze, TieBreakTeamCode} + defaultAthleteTieBreak = []string{TieBreakPoints, TieBreakGold, TieBreakSilver, TieBreakBronze, TieBreakName} +) + // StandingsInput 是排名聚合的输入:流水 + 名次明细 + 单位/运动员基础信息。 type StandingsInput struct { Teams []Team Athletes []Athlete Ledger []LedgerEntry Ranked []RankedResult + // TieBreak 是排序键顺序(如 points/gold/silver/bronze/teamCode/name); + // 空表示用默认口径。配置里的键优先,其余按默认顺序补齐,保证结果全序。 + TieBreak []string // IndividualToTeamWeight 个人积分折算进单位总分的系数;<=0 时按 1 处理。 IndividualToTeamWeight float64 // ExcludeTeamEventMedals 为 true 时团体奖牌榜不计团体项目奖牌;默认计入。 @@ -33,7 +53,7 @@ func rankMedal(rank int) int { } // BuildTeamStandings 生成总积分榜。 -// 总分 = Σ(仅记单位的流水) + 系数 × Σ(记到个人的流水);排序:总分 → 金 → 银 → 铜 → 单位编码。 +// 总分 = Σ(仅记单位的流水) + 系数 × Σ(记到个人的流水);排序按 in.TieBreak,缺省为总分 → 金 → 银 → 铜 → 单位编码。 func BuildTeamStandings(in StandingsInput) []StandingsRow { weight := effectiveWeight(in.IndividualToTeamWeight) sums := SumByTeam(in.Ledger, weight) @@ -62,7 +82,7 @@ func BuildTeamStandings(in StandingsInput) []StandingsRow { } } - sort.SliceStable(rows, func(i, j int) bool { return lessTeamRow(rows[i], rows[j]) }) + sort.SliceStable(rows, func(i, j int) bool { return lessTeamRow(rows[i], rows[j], in.TieBreak) }) for i := range rows { rows[i].Rank = i + 1 } @@ -92,7 +112,7 @@ func BuildMedalStandings(in StandingsInput) []MedalRow { } // BuildAthleteStandings 生成个人积分奖牌榜。 -// 排序:总分 → 金 → 银 → 铜 → 姓名;个人榜的奖牌只统计个人项目(团体项目奖牌归团体榜)。 +// 排序按 in.TieBreak,缺省为总分 → 金 → 银 → 铜 → 姓名;个人榜的奖牌只统计个人项目(团体项目奖牌归团体榜)。 func BuildAthleteStandings(in StandingsInput) []AthleteStandingsRow { points := SumByAthlete(in.Ledger) medals := athleteMedals(in) @@ -154,7 +174,7 @@ func BuildAthleteStandings(in StandingsInput) []AthleteStandingsRow { }) } - sort.SliceStable(rows, func(i, j int) bool { return lessAthleteRow(rows[i], rows[j]) }) + sort.SliceStable(rows, func(i, j int) bool { return lessAthleteRow(rows[i], rows[j], in.TieBreak) }) for i := range rows { rows[i].Rank = i + 1 } @@ -237,43 +257,91 @@ func athleteMedals(in StandingsInput) map[int64]medalTally { return out } -func lessTeamRow(a, b StandingsRow) bool { - if !nearlyEqual(a.Points, b.Points) { - return a.Points > b.Points +// tieBreakKeys 把配置的排序键展开成实际比较顺序:配置优先,缺失的默认键补在后面, +// 保证两行在所有键上都相等时仍能由调用方的 ID 兜底分出先后。 +func tieBreakKeys(cfg, def []string) []string { + if len(cfg) == 0 { + return def } - if a.Gold != b.Gold { - return a.Gold > b.Gold + out := make([]string, 0, len(cfg)+len(def)) + seen := make(map[string]bool, len(cfg)+len(def)) + for _, key := range cfg { + if seen[key] { + continue + } + seen[key] = true + out = append(out, key) } - if a.Silver != b.Silver { - return a.Silver > b.Silver + for _, key := range def { + if seen[key] { + continue + } + seen[key] = true + out = append(out, key) } - if a.Bronze != b.Bronze { - return a.Bronze > b.Bronze - } - if a.Code != b.Code { - return a.Code < b.Code + return out +} + +func lessTeamRow(a, b StandingsRow, tieBreak []string) bool { + for _, key := range tieBreakKeys(tieBreak, defaultTeamTieBreak) { + switch key { + case TieBreakPoints: + if !nearlyEqual(a.Points, b.Points) { + return a.Points > b.Points + } + case TieBreakGold: + if a.Gold != b.Gold { + return a.Gold > b.Gold + } + case TieBreakSilver: + if a.Silver != b.Silver { + return a.Silver > b.Silver + } + case TieBreakBronze: + if a.Bronze != b.Bronze { + return a.Bronze > b.Bronze + } + case TieBreakTeamCode: + if a.Code != b.Code { + return a.Code < b.Code + } + case TieBreakName: + if a.Name != b.Name { + return a.Name < b.Name + } + } } return a.TeamID < b.TeamID } -func lessAthleteRow(a, b AthleteStandingsRow) bool { - if !nearlyEqual(a.Points, b.Points) { - return a.Points > b.Points - } - if a.Gold != b.Gold { - return a.Gold > b.Gold - } - if a.Silver != b.Silver { - return a.Silver > b.Silver - } - if a.Bronze != b.Bronze { - return a.Bronze > b.Bronze - } - if a.Name != b.Name { - return a.Name < b.Name - } - if a.TeamCode != b.TeamCode { - return a.TeamCode < b.TeamCode +func lessAthleteRow(a, b AthleteStandingsRow, tieBreak []string) bool { + for _, key := range tieBreakKeys(tieBreak, defaultAthleteTieBreak) { + switch key { + case TieBreakPoints: + if !nearlyEqual(a.Points, b.Points) { + return a.Points > b.Points + } + case TieBreakGold: + if a.Gold != b.Gold { + return a.Gold > b.Gold + } + case TieBreakSilver: + if a.Silver != b.Silver { + return a.Silver > b.Silver + } + case TieBreakBronze: + if a.Bronze != b.Bronze { + return a.Bronze > b.Bronze + } + case TieBreakTeamCode: + if a.TeamCode != b.TeamCode { + return a.TeamCode < b.TeamCode + } + case TieBreakName: + if a.Name != b.Name { + return a.Name < b.Name + } + } } return a.AthleteID < b.AthleteID } diff --git a/server/internal/domain/types.go b/server/internal/domain/types.go index dc6d522..f903f45 100644 --- a/server/internal/domain/types.go +++ b/server/internal/domain/types.go @@ -98,17 +98,6 @@ const ( BasisAggregate Basis = "aggregate" // 按团体汇总(预留) ) -// AggregateMethod 是 aggregate 通道的汇总口径,默认 sum。 -type AggregateMethod string - -const ( - AggregateSum AggregateMethod = "sum" - AggregateAvg AggregateMethod = "avg" - AggregateMax AggregateMethod = "max" - AggregateMin AggregateMethod = "min" - AggregateCount AggregateMethod = "count" -) - // TieMode 并列名次口径。 type TieMode string @@ -136,11 +125,14 @@ const ( ) // UnionAttribution 联队(一条成绩归属多个单位)的归属口径。 +// 未实现:库中没有保存联队份额的表,加载器也不会填充 Entry.TeamShares, +// ScoreOptions.UnionAttribution 在生产链路里没有任何赋值入口; +// 规则包校验也不接受相关配置。这里只保留数据结构,本期不启用(devdocs 01 §115)。 type UnionAttribution string const ( UnionWhole UnionAttribution = "whole" // 整体记给主单位 - UnionSplit UnionAttribution = "split" // 按 TeamShares 的份额拆分 + UnionSplit UnionAttribution = "split" // 按 TeamShares 的份额拆分(未实现) ) // AdjustmentCategory 人工调整类别。 @@ -230,14 +222,15 @@ type EventCategory struct { // ScoreChannel 计分通道 = 「排谁的名次」+「名次从哪来」+「用哪张积分表」。 // 一个项目可配 0..N 个通道;0 个表示只排名不计分。 +// basis=aggregate 时固定按「同一主体成绩之和」汇总:汇总口径不可配置, +// 库里也没有对应的列,因此不再保留 avg/max/min/count 这类配置项。 type ScoreChannel struct { - ID int64 `json:"id"` - EventID int64 `json:"eventId"` - Scope Scope `json:"scope"` - Basis Basis `json:"basis"` - ProfileID int64 `json:"profileId"` - Aggregate AggregateMethod `json:"aggregate,omitempty"` // 仅 basis=aggregate 时使用 - SortOrder int `json:"sortOrder,omitempty"` + ID int64 `json:"id"` + EventID int64 `json:"eventId"` + Scope Scope `json:"scope"` + Basis Basis `json:"basis"` + ProfileID int64 `json:"profileId"` + SortOrder int `json:"sortOrder,omitempty"` } // ScoringRule 积分模板的一条明细:名次 → 分值。分值一律来自配置。 @@ -259,6 +252,7 @@ type ScoringProfile struct { } // TeamShare 联队中某单位的份额;Weight 为 0..1 的相对份额。 +// 未实现:没有建表、没有 loader 填充、也没有写入入口(见 UnionAttribution 注释)。 type TeamShare struct { TeamID int64 `json:"teamId"` Weight float64 `json:"weight"` @@ -273,7 +267,7 @@ type Entry struct { AthleteID int64 `json:"athleteId,omitempty"` AthleteName string `json:"athleteName,omitempty"` // 冗余姓名,便于引擎直接产出个人榜 MemberNames []string `json:"memberNames,omitempty"` - TeamShares []TeamShare `json:"teamShares,omitempty"` // 跨单位联队;空表示只属 TeamID + TeamShares []TeamShare `json:"teamShares,omitempty"` // 跨单位联队;空表示只属 TeamID(联队未实现,见 UnionAttribution) Bib string `json:"bib,omitempty"` Seed string `json:"seed,omitempty"` Status EntryStatus `json:"status"` diff --git a/server/internal/dto/results.go b/server/internal/dto/results.go new file mode 100644 index 0000000..b623dca --- /dev/null +++ b/server/internal/dto/results.go @@ -0,0 +1,31 @@ +package dto + +// EventResultGroup 是 snapshot.latestResults 的一组「小项 + 组别」成绩, +// 与 web/src/shared/types.ts 的 EventResultGroup 对齐。 +type EventResultGroup struct { + EventID int64 `json:"eventId"` + EventCode string `json:"eventCode"` + EventName string `json:"eventName"` + CategoryID int64 `json:"categoryId"` + CategoryName string `json:"categoryName"` + PublishedAt *string `json:"publishedAt"` + Rows []EventResultRow `json:"rows"` +} + +// EventResultRow 是组内的一条成绩行;字段与 types.ts 的 EventResultRow 一一对应。 +type EventResultRow struct { + EntryID int64 `json:"entryId"` + Rank *int `json:"rank"` + TeamID int64 `json:"teamId"` + TeamCode string `json:"teamCode"` + TeamName string `json:"teamName"` + ShortName *string `json:"shortName"` + AthleteID *int64 `json:"athleteId"` + AthleteName *string `json:"athleteName"` + MemberNames []string `json:"memberNames"` + ResultText string `json:"resultText"` + ResultValue *float64 `json:"resultValue"` + Unit string `json:"unit"` + IsTie bool `json:"isTie"` + Status string `json:"status"` +} diff --git a/server/internal/dto/settings.go b/server/internal/dto/settings.go new file mode 100644 index 0000000..f9beeeb --- /dev/null +++ b/server/internal/dto/settings.go @@ -0,0 +1,132 @@ +// Package dto 存放实时层与 REST 层共用的线上数据结构与纯转换函数。 +// +// 放在这里而不是 httpapi:realtime 与 httpapi 是同层出口,互相 import 会造成分层倒置; +// 本包只依赖 domain 与标准库,不做任何 IO。 +package dto + +import ( + "strconv" + "strings" +) + +// EventSettings 是 GET/PUT /api/settings 与 snapshot.settings 的线上结构, +// 与 web/src/shared/types.ts 的 EventSettings 一一对应。 +type EventSettings struct { + Event EventSettingsEvent `json:"event"` + Display EventSettingsDisplay `json:"display"` + Standings EventSettingsStandings `json:"standings"` + DefaultProfileID *int64 `json:"defaultProfileId"` + RequireApproval bool `json:"requireApproval"` +} + +// EventSettingsEvent 是赛事基本信息。 +type EventSettingsEvent struct { + Name string `json:"name"` + Subtitle *string `json:"subtitle"` + Host *string `json:"host"` + LogoPath *string `json:"logoPath"` + PrimaryColor *string `json:"primaryColor"` +} + +// EventSettingsDisplay 是大屏展示配置。 +type EventSettingsDisplay struct { + Background *string `json:"background"` + Logo *string `json:"logo"` + Title string `json:"title"` + Subtitle *string `json:"subtitle"` + PrimaryColor *string `json:"primaryColor"` + RotateSecond int `json:"rotateSeconds"` +} + +// EventSettingsStandings 是榜单口径配置。 +type EventSettingsStandings struct { + IndividualToTeamWeight float64 `json:"individualToTeamWeight"` + RankingMode string `json:"rankingMode"` + ShowIndividualBoard bool `json:"showIndividualBoard"` +} + +// ParseEventSettings 把 settings 表的点号键聚合成嵌套对象。 +// +// 默认值必须与 REST 的 getSettings 逐条一致:大屏首包走 snapshot、管理端走 /api/settings, +// 两边口径不同会让同一块屏刷新前后显示不一样的标题。 +// defaultProfileId 需要查 scoring_profiles 反查 code,属于 IO,由调用方补填。 +func ParseEventSettings(values map[string]string) EventSettings { + var out EventSettings + + out.Event.Name = orDefault(values["event.name"], "未命名赛事") + out.Event.Subtitle = nilIfEmpty(values["event.subtitle"]) + out.Event.Host = nilIfEmpty(values["event.organizer"]) + out.Event.LogoPath = nilIfEmpty(values["event.logo_path"]) + out.Event.PrimaryColor = nilIfEmpty(values["display.theme_color"]) + + out.Display.Background = nilIfEmpty(values["display.background"]) + out.Display.Logo = nilIfEmpty(values["display.logo"]) + if out.Display.Logo == nil { + out.Display.Logo = out.Event.LogoPath + } + out.Display.Title = orDefault(values["display.title"], out.Event.Name) + out.Display.Subtitle = nilIfEmpty(values["display.subtitle"]) + if out.Display.Subtitle == nil { + out.Display.Subtitle = out.Event.Subtitle + } + out.Display.PrimaryColor = out.Event.PrimaryColor + out.Display.RotateSecond = atoiDefault(values["display.rotate_seconds"], 15) + + out.Standings.IndividualToTeamWeight = atofDefault(values["standings.individual_to_team_weight"], 1) + out.Standings.RankingMode = orDefault(values["standings.mode"], "points") + out.Standings.ShowIndividualBoard = boolDefault( + firstNonEmpty(values["standings.show_individual_board"], values["standings.show_individual_medals"]), true) + + out.RequireApproval = boolDefault(values["results.review_required"], false) + return out +} + +func nilIfEmpty(s string) *string { + if s == "" { + return nil + } + return &s +} + +func orDefault(v, def string) string { + if strings.TrimSpace(v) == "" { + return def + } + return v +} + +func firstNonEmpty(values ...string) string { + for _, v := range values { + if strings.TrimSpace(v) != "" { + return v + } + } + return "" +} + +func atoiDefault(v string, def int) int { + n, err := strconv.Atoi(strings.TrimSpace(v)) + if err != nil { + return def + } + return n +} + +func atofDefault(v string, def float64) float64 { + n, err := strconv.ParseFloat(strings.TrimSpace(v), 64) + if err != nil { + return def + } + return n +} + +func boolDefault(v string, def bool) bool { + switch strings.ToLower(strings.TrimSpace(v)) { + case "true", "1", "yes": + return true + case "false", "0", "no": + return false + default: + return def + } +} diff --git a/server/internal/dto/standings.go b/server/internal/dto/standings.go new file mode 100644 index 0000000..8d41c72 --- /dev/null +++ b/server/internal/dto/standings.go @@ -0,0 +1,65 @@ +package dto + +import "git.yeij.top/AskaEth/Scoreboard/server/internal/domain" + +// StandingRow 是 GET /api/standings 与 snapshot.standings 的线上结构。 +// +// 字段名与 web/src/shared/types.ts 的 StandingRow 对齐:大屏与管理端共用一份解析逻辑, +// 不能再把 domain.StandingsRow 的 points/code/medals 直接下发。 +type StandingRow struct { + TeamID int64 `json:"teamId"` + TeamCode string `json:"teamCode"` + TeamName string `json:"teamName"` + ShortName *string `json:"shortName"` + Color *string `json:"color"` + LogoPath *string `json:"logoPath"` + Rank int `json:"rank"` + Gold int `json:"gold"` + Silver int `json:"silver"` + Bronze int `json:"bronze"` + MedalTotal int `json:"medalTotal"` + TotalPoints float64 `json:"totalPoints"` + RankChange *int `json:"rankChange"` +} + +// StandingRows 把 domain 榜单转换成线上结构。 +// teams 用于补齐颜色/logo 等 domain 榜单没带的展示字段;rankChange 快照没有对比基准,与 REST 一致下发 null。 +func StandingRows(rows []domain.StandingsRow, teams []domain.Team) []StandingRow { + byID := make(map[int64]domain.Team, len(teams)) + for _, t := range teams { + byID[t.ID] = t + } + + out := make([]StandingRow, 0, len(rows)) + for _, r := range rows { + t := byID[r.TeamID] + row := StandingRow{ + TeamID: r.TeamID, + TeamCode: r.Code, + TeamName: r.Name, + ShortName: nilIfEmpty(r.ShortName), + Rank: r.Rank, + Gold: r.Gold, + Silver: r.Silver, + Bronze: r.Bronze, + MedalTotal: r.Medals, + TotalPoints: r.Points, + RankChange: nil, + } + if t.ID != 0 { + row.Color = nilIfEmpty(t.Color) + row.LogoPath = nilIfEmpty(t.LogoPath) + if row.TeamCode == "" { + row.TeamCode = t.Code + } + if row.TeamName == "" { + row.TeamName = t.Name + } + if row.ShortName == nil { + row.ShortName = nilIfEmpty(t.ShortName) + } + } + out = append(out, row) + } + return out +} diff --git a/server/internal/realtime/client.go b/server/internal/realtime/client.go index 8f604ec..381f9cb 100644 --- a/server/internal/realtime/client.go +++ b/server/internal/realtime/client.go @@ -24,24 +24,30 @@ type client struct { closeCode websocket.StatusCode closeReason string - mu sync.Mutex - greeted bool - role string - userID int64 - instanceID string + mu sync.Mutex + greeted bool + // role/userID 是 hello 之后实际生效的身份;sessionRole/sessionUserID 是握手时从会话取到的上限, + // 客户端自报的 role/userId 不能越过它。 + role string + userID int64 + sessionRole string + sessionUserID int64 + instanceID string ackVersion atomic.Int64 // 客户端已确认应用到的版本 lastSent atomic.Int64 // 服务端已发出的最大版本 slow atomic.Bool } -func newClient(h *Hub, conn *websocket.Conn, remoteAddr string) *client { +func newClient(h *Hub, conn *websocket.Conn, remoteAddr, sessionRole string, sessionUserID int64) *client { return &client{ - hub: h, - conn: conn, - remoteAddr: remoteAddr, - send: make(chan []byte, defaultSendBuffer), - done: make(chan struct{}), + hub: h, + conn: conn, + remoteAddr: remoteAddr, + send: make(chan []byte, defaultSendBuffer), + done: make(chan struct{}), + sessionRole: sessionRole, + sessionUserID: sessionUserID, } } diff --git a/server/internal/realtime/hub.go b/server/internal/realtime/hub.go index 2acacb6..d69c477 100644 --- a/server/internal/realtime/hub.go +++ b/server/internal/realtime/hub.go @@ -9,6 +9,9 @@ import ( "time" "github.com/coder/websocket" + + "git.yeij.top/AskaEth/Scoreboard/server/internal/auth" + "git.yeij.top/AskaEth/Scoreboard/server/internal/middleware" ) // 默认参数;现场可通过 Set* 调整。 @@ -210,14 +213,26 @@ func (h *Hub) Handler() http.Handler { h.mu.Unlock() defer h.connWG.Done() - h.serve(conn, r.RemoteAddr) + // 身份只认会话:/ws 必须被 Authenticate 中间件包住,否则一律按 guest 处理。 + role, userID := wsIdentity(r.Context()) + h.serve(conn, r.RemoteAddr, role, userID) }) } -func (h *Hub) serve(conn *websocket.Conn, remoteAddr string) { +// wsIdentity 从请求上下文取会话身份并折算成实时层角色。 +// 未登录(guest)只能进大屏房间;recorder/admin 才有管理端房间的订阅权。 +func wsIdentity(ctx context.Context) (string, int64) { + u := middleware.User(ctx) + if u == nil || !u.Role.Allows(auth.RoleRecorder) { + return RoleDisplay, 0 + } + return RoleAdmin, u.ID +} + +func (h *Hub) serve(conn *websocket.Conn, remoteAddr, sessionRole string, sessionUserID int64) { conn.SetReadLimit(1 << 20) - c := newClient(h, conn, remoteAddr) + c := newClient(h, conn, remoteAddr, sessionRole, sessionUserID) h.addClient(c) ctx, cancel := context.WithCancel(h.ctx) @@ -293,22 +308,42 @@ func (h *Hub) roomClients(room string) []*client { return out } +// handleHello 只信任会话身份:客户端自报的 role/userId 会被忽略, +// 管理端房间必须由登录会话授予,未登录连接请求管理端直接断开。 func (h *Hub) handleHello(ctx context.Context, c *client, p helloPayload) { - if p.Role != RoleDisplay && p.Role != RoleAdmin { + c.mu.Lock() + sessionRole, sessionUserID := c.sessionRole, c.sessionUserID + c.mu.Unlock() + + if p.Role != "" && p.Role != RoleDisplay && p.Role != RoleAdmin { h.log.Debug("hello 角色非法", "role", p.Role, "remote", c.remoteAddr) c.stop(websocket.StatusPolicyViolation, "role 非法") return } + if p.Role == RoleAdmin && sessionRole != RoleAdmin { + h.log.Warn("未登录连接请求管理端房间,已断开", "remote", c.remoteAddr) + c.stop(websocket.StatusPolicyViolation, "无权订阅管理端房间") + return + } + + // 不写 role 时按会话上限走;显式 role=display 允许管理端用户降级成大屏。 + role := sessionRole + if p.Role == RoleDisplay { + role = RoleDisplay + } c.mu.Lock() - c.role = p.Role - c.userID = p.UserID + c.role = role + c.userID = 0 + if role == RoleAdmin { + c.userID = sessionUserID + } c.mu.Unlock() - if p.Role == RoleAdmin { + if role == RoleAdmin { rooms := []string{RoomAdmin} - if p.UserID > 0 { - rooms = append(rooms, AdminRoom(p.UserID)) + if c.userID > 0 { + rooms = append(rooms, AdminRoom(c.userID)) } h.join(c, rooms...) } else { diff --git a/server/internal/realtime/protocol.go b/server/internal/realtime/protocol.go index 69f0d68..f320946 100644 --- a/server/internal/realtime/protocol.go +++ b/server/internal/realtime/protocol.go @@ -84,6 +84,9 @@ func encode(event string, data any) ([]byte, error) { } // helloPayload 是 C→S 的 hello 载荷。 +// +// role 只是「申请」:服务端以会话身份为准,未登录连接申请 admin 会被拒绝, +// 因此这里没有 userId —— 定向房间的 userId 只能来自会话。 // instanceId / page 是协议字段之外的可选扩展:注册表要记「谁在线、在哪一页」, // 老客户端不带这两个字段也能正常工作。 type helloPayload struct { @@ -91,7 +94,6 @@ type helloPayload struct { LastVersion *int64 `json:"lastVersion,omitempty"` DisplayKey string `json:"displayKey,omitempty"` InstanceID string `json:"instanceId,omitempty"` - UserID int64 `json:"userId,omitempty"` Page string `json:"page,omitempty"` } diff --git a/server/internal/realtime/snapshot.go b/server/internal/realtime/snapshot.go index 8adf2ad..6f5cba8 100644 --- a/server/internal/realtime/snapshot.go +++ b/server/internal/realtime/snapshot.go @@ -4,24 +4,31 @@ import ( "context" "database/sql" "encoding/json" + "errors" "fmt" + "sort" + "strconv" + "strings" "git.yeij.top/AskaEth/Scoreboard/server/internal/domain" + "git.yeij.top/AskaEth/Scoreboard/server/internal/dto" ) -// latestResultsLimit 控制快照里「最新成绩」的条数,避免大屏首包过大。 +// latestResultsLimit 控制快照里「最新成绩」的行数上限,避免大屏首包过大。 +// 行会按「小项 + 组别」聚合后再下发,因此组数只会比这个值更少。 const latestResultsLimit = 20 // Snapshot 是 snapshot 事件的 data,也是客户端冷启动的完整状态。 -// 字段名与协议第 3 节一致。 +// 字段名与协议第 3 节一致;settings/standings/latestResults 的形状必须与 REST 同构, +// 否则大屏收到首包就会按错误的键名渲染。 type Snapshot struct { - Version int64 `json:"version"` - Settings map[string]json.RawMessage `json:"settings"` - Teams []domain.Team `json:"teams"` - Events []domain.Event `json:"events"` - Standings []domain.StandingsRow `json:"standings"` - Records []Record `json:"records"` - LatestResults []LatestResult `json:"latestResults"` + Version int64 `json:"version"` + Settings dto.EventSettings `json:"settings"` + Teams []domain.Team `json:"teams"` + Events []domain.Event `json:"events"` + Standings []dto.StandingRow `json:"standings"` + Records []Record `json:"records"` + LatestResults []dto.EventResultGroup `json:"latestResults"` } // Record 是纪录榜的一行。 @@ -38,23 +45,6 @@ type Record struct { Source string `json:"source"` } -// LatestResult 是已发布成绩的展示行,供大屏滚动与详情页使用。 -type LatestResult struct { - ID int64 `json:"id"` - EventID int64 `json:"eventId"` - EntryID int64 `json:"entryId"` - TeamID int64 `json:"teamId"` - AthleteID int64 `json:"athleteId,omitempty"` - Rank int `json:"rank,omitempty"` - ResultText string `json:"resultText,omitempty"` - ResultValue *float64 `json:"resultValue,omitempty"` - Unit string `json:"unit,omitempty"` - IsTie bool `json:"isTie,omitempty"` - Status string `json:"status"` - State string `json:"state"` - UpdatedAt string `json:"updatedAt,omitempty"` -} - // Snapshot 读取一份一致的完整状态。 // // 全部查询放在同一个只读事务里,且版本号取自同一事务, @@ -71,18 +61,21 @@ func (h *Hub) Snapshot(ctx context.Context) (*Snapshot, error) { defer func() { _ = tx.Rollback() }() snap := &Snapshot{ - Settings: map[string]json.RawMessage{}, Teams: []domain.Team{}, Events: []domain.Event{}, - Standings: []domain.StandingsRow{}, + Standings: []dto.StandingRow{}, Records: []Record{}, - LatestResults: []LatestResult{}, + LatestResults: []dto.EventResultGroup{}, } if err := tx.QueryRowContext(ctx, `SELECT COALESCE(MAX(id), 0) FROM change_log`).Scan(&snap.Version); err != nil { return nil, fmt.Errorf("读取快照版本号: %w", err) } - if err := loadSettings(ctx, tx, snap); err != nil { + values, err := loadSettings(ctx, tx) + if err != nil { + return nil, err + } + if err := fillSettings(ctx, tx, values, snap); err != nil { return nil, err } if err := loadTeams(ctx, tx, snap); err != nil { @@ -91,7 +84,7 @@ func (h *Hub) Snapshot(ctx context.Context) (*Snapshot, error) { if err := loadEvents(ctx, tx, snap); err != nil { return nil, err } - if err := loadStandings(ctx, tx, snap); err != nil { + if err := loadStandings(ctx, tx, snap, values); err != nil { return nil, err } if err := loadRecords(ctx, tx, snap); err != nil { @@ -107,21 +100,46 @@ func (h *Hub) Snapshot(ctx context.Context) (*Snapshot, error) { return snap, nil } -func loadSettings(ctx context.Context, tx *sql.Tx, snap *Snapshot) error { - rows, err := tx.QueryContext(ctx, `SELECT key, value FROM settings ORDER BY key`) +// loadSettings 原样读出 settings 表的键值:榜单口径要用原始字符串, +// 而线上结构需要聚合后的嵌套对象,两者由调用方分别消费。 +func loadSettings(ctx context.Context, tx *sql.Tx) (map[string]string, error) { + rows, err := tx.QueryContext(ctx, `SELECT key, value FROM settings`) if err != nil { - return fmt.Errorf("读取赛事配置: %w", err) + return nil, fmt.Errorf("读取赛事配置: %w", err) } defer func() { _ = rows.Close() }() + out := make(map[string]string) for rows.Next() { var key, value string if err := rows.Scan(&key, &value); err != nil { - return fmt.Errorf("解析赛事配置: %w", err) + return nil, fmt.Errorf("解析赛事配置: %w", err) } - snap.Settings[key] = rawJSONOrString(value) + out[key] = value } - return rows.Err() + return out, rows.Err() +} + +// fillSettings 聚合成与 REST /api/settings 相同的嵌套结构。 +// defaultProfileId 在 settings 表里存的是模板 code,需要反查一次主键。 +func fillSettings(ctx context.Context, tx *sql.Tx, values map[string]string, snap *Snapshot) error { + snap.Settings = dto.ParseEventSettings(values) + + code := values["scoring.default_profile"] + if code == "" { + return nil + } + var id int64 + err := tx.QueryRowContext(ctx, `SELECT id FROM scoring_profiles WHERE code = ?`, code).Scan(&id) + switch { + case errors.Is(err, sql.ErrNoRows): + // 配置引用的模板已被删除:保持 null,与 REST 一致,由管理端提示重选。 + case err != nil: + return fmt.Errorf("解析默认积分模板: %w", err) + default: + snap.Settings.DefaultProfileID = &id + } + return nil } func loadTeams(ctx context.Context, tx *sql.Tx, snap *Snapshot) error { @@ -191,9 +209,9 @@ func loadEvents(ctx context.Context, tx *sql.Tx, snap *Snapshot) error { return rows.Err() } -// loadStandings 复用 domain 的排名口径:积分来自流水,奖牌来自已发布名次, -// 保证大屏看到的榜单与 REST /api/standings 一致。 -func loadStandings(ctx context.Context, tx *sql.Tx, snap *Snapshot) error { +// loadStandings 复用 domain 的排名口径,且输入与 REST /api/standings 完全一致: +// 个人流水按系数折算、团体奖牌开关生效、只有配了计分通道的项目才产出名次。 +func loadStandings(ctx context.Context, tx *sql.Tx, snap *Snapshot, values map[string]string) error { ledger, err := loadLedger(ctx, tx) if err != nil { return err @@ -202,11 +220,14 @@ func loadStandings(ctx context.Context, tx *sql.Tx, snap *Snapshot) error { if err != nil { return err } - snap.Standings = domain.BuildTeamStandings(domain.StandingsInput{ - Teams: snap.Teams, - Ledger: ledger, - Ranked: ranked, + rows := domain.BuildTeamStandings(domain.StandingsInput{ + Teams: snap.Teams, + Ledger: ledger, + Ranked: ranked, + IndividualToTeamWeight: atofDefault(values["standings.individual_to_team_weight"], 1), + ExcludeTeamEventMedals: !boolDefault(values["standings.medal_counts_team_events"], true), }) + snap.Standings = dto.StandingRows(rows, snap.Teams) return nil } @@ -235,13 +256,19 @@ func loadLedger(ctx context.Context, tx *sql.Tx) ([]domain.LedgerEntry, error) { return out, rows.Err() } -// loadRanked 只取已发布的前三名,用于统计奖牌;名次明细的完整计算仍由成绩模块负责。 +// loadRanked 与 REST 的 buildRanked 同源:必须 join event_score_channels, +// 否则多通道项目会把一枚奖牌算成多枚,没配通道的项目也会凭空产生奖牌。 func loadRanked(ctx context.Context, tx *sql.Tx) ([]domain.RankedResult, error) { rows, err := tx.QueryContext(ctx, ` - SELECT e.team_id, COALESCE(e.athlete_id, 0), r.rank - FROM results r JOIN entries e ON e.id = r.entry_id - WHERE r.state = 'published' AND r.status = 'ok' AND r.rank BETWEEN 1 AND 3 - ORDER BY r.id`) + SELECT r.id, r.event_id, e.category_id, r.rank, r.result_value, r.status, + e.id, e.team_id, e.athlete_id, COALESCE(a.name, ''), c.scope, c.id + FROM results r + JOIN entries e ON e.id = r.entry_id + JOIN event_score_channels c ON c.event_id = r.event_id + LEFT JOIN athletes a ON a.id = e.athlete_id + WHERE r.state = 'published' AND r.rank IS NOT NULL AND r.rank > 0 AND r.status = 'ok' + AND (c.scope = 'team' OR e.athlete_id IS NOT NULL) + ORDER BY r.id, c.sort_order, c.id`) if err != nil { return nil, fmt.Errorf("读取奖牌名次: %w", err) } @@ -250,19 +277,23 @@ func loadRanked(ctx context.Context, tx *sql.Tx) ([]domain.RankedResult, error) var out []domain.RankedResult for rows.Next() { var ( - teamID, athleteID int64 - rank int + rr domain.RankedResult + value sql.NullFloat64 + athleteID sql.NullInt64 + rank int ) - if err := rows.Scan(&teamID, &athleteID, &rank); err != nil { + if err := rows.Scan(&rr.ResultID, &rr.EventID, &rr.CategoryID, &rank, &value, &rr.Status, + &rr.EntryID, &rr.TeamID, &athleteID, &rr.AthleteName, &rr.Scope, &rr.ChannelID); err != nil { return nil, fmt.Errorf("解析奖牌名次: %w", err) } - out = append(out, domain.RankedResult{ - TeamID: teamID, - AthleteID: athleteID, - Rank: rank, - Scope: domain.ScopeTeam, - Shares: []domain.TeamShare{{TeamID: teamID, Weight: 1}}, - }) + rr.Rank = rank + if value.Valid { + v := value.Float64 + rr.ResultValue = &v + } + rr.AthleteID = athleteID.Int64 + rr.Shares = []domain.TeamShare{{TeamID: rr.TeamID, Weight: 1}} + out = append(out, rr) } return out, rows.Err() } @@ -299,50 +330,149 @@ func loadRecords(ctx context.Context, tx *sql.Tx, snap *Snapshot) error { return rows.Err() } +// loadLatestResults 取最近发布的成绩,按「小项 + 组别」聚合成组后下发。 +// 前端按组渲染(TickerBar 取最新一组、EventResultsPage 取前 N 组),平铺行会让它拿到 undefined。 func loadLatestResults(ctx context.Context, tx *sql.Tx, snap *Snapshot) error { rows, err := tx.QueryContext(ctx, ` - SELECT r.id, r.event_id, r.entry_id, e.team_id, COALESCE(e.athlete_id, 0), - COALESCE(r.rank, 0), COALESCE(r.result_text, ''), r.result_value, - COALESCE(r.unit, ''), r.is_tie, r.status, r.state, - COALESCE(r.verified_at, r.recorded_at, r.updated_at, r.created_at, '') - FROM results r JOIN entries e ON e.id = r.entry_id - WHERE r.state = 'published' - ORDER BY COALESCE(r.verified_at, r.recorded_at, r.updated_at) DESC, r.id DESC - LIMIT ?`, latestResultsLimit) + SELECT r.event_id, COALESCE(ev.code, ''), COALESCE(ev.name, ''), + e.category_id, COALESCE(cat.name, ''), + COALESCE(r.verified_at, r.recorded_at, r.updated_at, r.created_at, ''), + r.entry_id, r.rank, e.team_id, + COALESCE(t.code, ''), COALESCE(t.name, ''), COALESCE(t.short_name, ''), + e.athlete_id, COALESCE(a.name, ''), COALESCE(e.member_names, ''), + COALESCE(r.result_text, ''), r.result_value, COALESCE(r.unit, ''), r.is_tie, r.status + FROM results r + JOIN entries e ON e.id = r.entry_id + JOIN events ev ON ev.id = r.event_id + LEFT JOIN event_categories cat ON cat.id = e.category_id + LEFT JOIN teams t ON t.id = e.team_id + LEFT JOIN athletes a ON a.id = e.athlete_id + WHERE r.state = 'published' + ORDER BY COALESCE(r.verified_at, r.recorded_at, r.updated_at, r.created_at) DESC, r.id DESC + LIMIT ?`, latestResultsLimit) if err != nil { return fmt.Errorf("读取最新成绩: %w", err) } defer func() { _ = rows.Close() }() + type groupKey struct { + eventID int64 + categoryID int64 + } + index := make(map[groupKey]int) + for rows.Next() { var ( - lr LatestResult - value sql.NullFloat64 - isTie int + eventID, categoryID int64 + eventCode, eventName string + categoryName string + publishedAt string + row dto.EventResultRow + rank sql.NullInt64 + value sql.NullFloat64 + athleteID sql.NullInt64 + athleteName string + shortName string + memberNames string + isTie int ) - if err := rows.Scan(&lr.ID, &lr.EventID, &lr.EntryID, &lr.TeamID, &lr.AthleteID, - &lr.Rank, &lr.ResultText, &value, &lr.Unit, &isTie, &lr.Status, &lr.State, &lr.UpdatedAt); err != nil { + if err := rows.Scan(&eventID, &eventCode, &eventName, &categoryID, &categoryName, &publishedAt, + &row.EntryID, &rank, &row.TeamID, &row.TeamCode, &row.TeamName, &shortName, + &athleteID, &athleteName, &memberNames, + &row.ResultText, &value, &row.Unit, &isTie, &row.Status); err != nil { return fmt.Errorf("解析最新成绩: %w", err) } + if rank.Valid { + v := int(rank.Int64) + row.Rank = &v + } if value.Valid { v := value.Float64 - lr.ResultValue = &v + row.ResultValue = &v } - lr.IsTie = isTie != 0 - snap.LatestResults = append(snap.LatestResults, lr) + if athleteID.Valid && athleteID.Int64 != 0 { + v := athleteID.Int64 + row.AthleteID = &v + } + row.AthleteName = nilIfEmpty(athleteName) + row.ShortName = nilIfEmpty(shortName) + row.MemberNames = parseMemberNames(memberNames) + row.IsTie = isTie != 0 + + key := groupKey{eventID: eventID, categoryID: categoryID} + i, ok := index[key] + if !ok { + i = len(snap.LatestResults) + index[key] = i + snap.LatestResults = append(snap.LatestResults, dto.EventResultGroup{ + EventID: eventID, + EventCode: eventCode, + EventName: eventName, + CategoryID: categoryID, + CategoryName: categoryName, + PublishedAt: nilIfEmpty(publishedAt), + Rows: []dto.EventResultRow{}, + }) + } + snap.LatestResults[i].Rows = append(snap.LatestResults[i].Rows, row) } - return rows.Err() + if err := rows.Err(); err != nil { + return err + } + + // 行按发布时间倒序取回,组内需改回名次顺序;无名次的排最后。 + for i := range snap.LatestResults { + sort.SliceStable(snap.LatestResults[i].Rows, func(a, b int) bool { + ra, rb := snap.LatestResults[i].Rows[a], snap.LatestResults[i].Rows[b] + switch { + case ra.Rank == nil && rb.Rank == nil: + return ra.EntryID < rb.EntryID + case ra.Rank == nil: + return false + case rb.Rank == nil: + return true + default: + return *ra.Rank < *rb.Rank + } + }) + } + return nil } -// rawJSONOrString 让 settings 的值保持原样:本来就是 JSON 的直接透传, -// 纯文本则编码成 JSON 字符串,客户端统一按 JSON 解析。 -func rawJSONOrString(value string) json.RawMessage { - if json.Valid([]byte(value)) { - return json.RawMessage(value) +// parseMemberNames 解析 entries.member_names;空值或坏 JSON 一律当作「无成员名单」。 +func parseMemberNames(raw string) []string { + if raw == "" { + return nil } - quoted, err := json.Marshal(value) + var out []string + if err := json.Unmarshal([]byte(raw), &out); err != nil { + return nil + } + return out +} + +func nilIfEmpty(s string) *string { + if s == "" { + return nil + } + return &s +} + +func atofDefault(v string, def float64) float64 { + n, err := strconv.ParseFloat(strings.TrimSpace(v), 64) if err != nil { - return json.RawMessage(`null`) + return def + } + return n +} + +func boolDefault(v string, def bool) bool { + switch strings.ToLower(strings.TrimSpace(v)) { + case "true", "1", "yes": + return true + case "false", "0", "no": + return false + default: + return def } - return quoted } diff --git a/web/src/admin/api/client.ts b/web/src/admin/api/client.ts index 82b868a..e4808d8 100644 --- a/web/src/admin/api/client.ts +++ b/web/src/admin/api/client.ts @@ -69,7 +69,8 @@ async function call(real: () => Promise, mock: () => T | Promise): Prom } export interface Bootstrap { - settings: EventSettings + /** settings 是 admin-only;访客/录入员读不到时为 null,其余赛事数据照常返回。 */ + settings: EventSettings | null teams: Team[] sports: Sport[] events: Event[] @@ -78,11 +79,17 @@ export interface Bootstrap { athleteStandings: AthleteStanding[] } +/** 除 settings 外都视为必需:失败时交给 call 决定降级到示例数据还是抛出业务错误。 */ +function required(result: PromiseSettledResult): T { + if (result.status === 'fulfilled') return result.value + throw result.reason +} + async function bootstrap(): Promise { return call( async () => { const [settings, teams, sports, events, profiles, standings, athleteStandings] = - await Promise.all([ + await Promise.allSettled([ client.settings.get(), client.teams.list(), client.sports.list(), @@ -91,7 +98,17 @@ async function bootstrap(): Promise { client.standings.teams(), client.standings.athletes(), ]) - return { settings, teams, sports, events, profiles, standings, athleteStandings } + // GET /api/settings 是 admin-only,guest/recorder 必然 403。 + // 这是权限边界而非服务不可用,不能让整页数据跟着失败,所以 settings 单独降级为 null。 + return { + settings: settings.status === 'fulfilled' ? settings.value : null, + teams: required(teams), + sports: required(sports), + events: required(events), + profiles: required(profiles), + standings: required(standings), + athleteStandings: required(athleteStandings), + } }, () => mockDb.bootstrap(), ) diff --git a/web/src/admin/pages/DisplayPage.vue b/web/src/admin/pages/DisplayPage.vue index 7ee820a..e43b186 100644 --- a/web/src/admin/pages/DisplayPage.vue +++ b/web/src/admin/pages/DisplayPage.vue @@ -8,48 +8,51 @@ import { useAuthStore } from '../stores/auth' import { useEventStore } from '../stores/event' import { useToastStore } from '../stores/toast' +/** 大屏页面表里的规范值;写错会被大屏静默丢弃,所以用字面量联合锁死。 */ +type DisplayPage = 'standings' | 'team-medals' | 'athlete-standings' | 'event-results' + interface ScreenNode { key: string name: string + /** null = 未知;后端没有实例上报接口,占位节点恒为未知。 */ online: boolean | null - lastSeen: string | null + /** 本机最后一次下发指令的时间,不是大屏心跳。 */ + lastCommandAt: string | null } -const PAGES: Array<{ key: string; label: string }> = [ +const PAGES: Array<{ key: DisplayPage; label: string }> = [ { key: 'standings', label: '总积分榜' }, - { key: 'medals', label: '团体奖牌榜' }, - { key: 'athletes', label: '个人积分榜' }, - { key: 'latest', label: '最新成绩' }, + { key: 'team-medals', label: '团体奖牌榜' }, + { key: 'athlete-standings', label: '个人积分榜' }, + { key: 'event-results', label: '最新成绩' }, ] const data = useEventStore() const auth = useAuthStore() const toast = useToastStore() -const currentPage = ref('standings') +const currentPage = ref('standings') const locked = ref(false) const announceText = ref('') const announceSeconds = ref(15) const pending = ref(null) /** - * 在线看板。 + * 在线看板(占位)。 * * devdocs/05 §7 未定义「大屏在线列表」接口,管理端拿不到每块屏的心跳, - * 因此这里只登记本地已知的屏,并用「最近一次命令是否送达」推断在线状态。 - * 后端补上接口后,把 screens 换成接口数据即可。 + * 所以这里只登记本地已知的屏,在线状态恒为「未知」——指令送达不等于大屏在线。 + * 后端补上实例上报接口后,把 screens 换成接口数据、hasInstanceData 才可能为真。 */ -const screens = ref([{ key: 'main', name: '主屏(本地登记)', online: null, lastSeen: null }]) +const screens = ref([{ key: 'main', name: '主屏(本地登记)', online: null, lastCommandAt: null }]) const canWrite = computed(() => !auth.readOnly && data.canWrite) +/** 只有真实实例上报(online 非 null)才算有在线数据,占位节点一律不谎报在线。 */ +const hasInstanceData = computed(() => screens.value.some((screen) => screen.online !== null)) const onlineCount = computed(() => screens.value.filter((screen) => screen.online === true).length) -function markSeen(): void { - screens.value = screens.value.map((screen) => ({ - ...screen, - online: true, - lastSeen: new Date().toISOString(), - })) +function recordDispatch(): void { + screens.value = screens.value.map((screen) => ({ ...screen, lastCommandAt: new Date().toISOString() })) } async function send(action: 'goto' | 'lock' | 'unlock' | 'reload' | 'announce'): Promise { @@ -57,9 +60,19 @@ async function send(action: 'goto' | 'lock' | 'unlock' | 'reload' | 'announce'): toast.error('当前不可下发指令', auth.readOnly ? '请先登录' : '服务端未连接') return } + if (data.isMock) { + // 示例数据模式下指令不会真正下发,不能提示「已切换」。 + toast.warning('示例数据模式未真正下发', '后端未就绪,指令不会到达大屏') + return + } pending.value = action try { if (action === 'goto') { + // 页面值不在大屏别名表内会被静默丢弃,这里先拦下并明确报错,避免谎报成功。 + if (!PAGES.some((page) => page.key === currentPage.value)) { + toast.error('大屏不认识该页面', `页面值「${currentPage.value}」不在大屏页面表内`) + return + } await adminApi.displayCommand({ action, payload: { page: currentPage.value } }) toast.success(`已切换大屏到「${PAGES.find((page) => page.key === currentPage.value)?.label ?? currentPage.value}」`) } else if (action === 'lock' || action === 'unlock') { @@ -79,7 +92,7 @@ async function send(action: 'goto' | 'lock' | 'unlock' | 'reload' | 'announce'): announceText.value = '' toast.success('公告已推送') } - markSeen() + recordDispatch() } catch (caught) { toast.error('指令下发失败', caught instanceof Error ? caught.message : null) } finally { @@ -195,14 +208,14 @@ async function send(action: 'goto' | 'lock' | 'unlock' | 'reload' | 'announce'):
在线看板 - - {{ onlineCount }} / {{ screens.length }} 在线 + + {{ hasInstanceData ? `${onlineCount} / ${screens.length} 在线` : '在线状态未知(占位)' }}
- 后端未提供大屏在线接口(devdocs/05 §7 未定义),下表为本地登记节点,在线状态按最近一次指令送达推断。 + 后端未提供大屏实例上报接口(devdocs/05 §7 未定义),下表是本地占位节点:「在线」无法判定,也不按指令送达推断;「最近下发」只是本机下发时间,不代表大屏在线。
@@ -210,7 +223,7 @@ async function send(action: 'goto' | 'lock' | 'unlock' | 'reload' | 'announce'): - + @@ -224,14 +237,14 @@ async function send(action: 'goto' | 'lock' | 'unlock' | 'reload' | 'announce'): {{ screen.online === true ? '在线' : screen.online === false ? '掉线' : '未知' }} - +
大屏 状态最后心跳最近下发
{{ screen.lastSeen === null ? '—' : formatDateTime(screen.lastSeen) }}{{ screen.lastCommandAt === null ? '—' : formatDateTime(screen.lastCommandAt) }}
-

当前状态:{{ locked ? '已锁定' : '自动轮播' }} · 页面 {{ currentPage }}

+

当前状态:{{ locked ? '已锁定' : '自动轮播' }} · 页面 {{ PAGES.find((page) => page.key === currentPage)?.label ?? currentPage }}

示例数据模式下指令不会真正下发到大屏。

diff --git a/web/src/admin/pages/EntryPage.vue b/web/src/admin/pages/EntryPage.vue index 81f0845..26cb88e 100644 --- a/web/src/admin/pages/EntryPage.vue +++ b/web/src/admin/pages/EntryPage.vue @@ -35,7 +35,6 @@ interface EditableRow { isTie: boolean note: string resultState: ResultState | null - serverPoints: number | null original: string } @@ -134,11 +133,18 @@ const score = computed(() => { }) }) +/** + * 已发布行产生的分已经计入当前总分,预览时必须先扣掉,差值才是「本次发布」的影响。 + * + * 不能用 result.points:服务端从不返回该字段(恒为 0),否则已发布分会被重复计算。 + * 这里改用与预览同一套算法算出的 rowPoints,口径与服务端计分一致。 + */ const publishedContribution = computed(() => { const map = new Map() + const rowPoints = score.value?.rowPoints for (const row of visibleRows.value) { if (row.resultState !== 'published') continue - const points = row.serverPoints ?? 0 + const points = rowPoints?.get(row.entryId) ?? 0 map.set(row.teamId, (map.get(row.teamId) ?? 0) + (isIndividual.value ? points * weight.value : points)) } return map @@ -250,7 +256,6 @@ function toRow(entry: EntryWithResult): EditableRow { isTie: result?.isTie ?? false, note: result?.note ?? '', resultState: result?.state ?? null, - serverPoints: result?.points ?? null, original: '', } row.original = snapshotOf(entry) @@ -347,6 +352,11 @@ async function saveDraft(silent = false): Promise { function requestPublish(): void { if (selectedEventId.value === null) return + if (data.isMock) { + // 示例数据模式下 publish 只改内存,提示「发布成功」等于谎报落库,直接停用。 + toast.error('示例数据模式不能发布', '后端未就绪,发布不会写入真实赛事') + return + } if (!canWrite.value) { toast.error('当前不可发布', auth.readOnly ? '请先登录' : '服务端未连接') return @@ -515,7 +525,7 @@ watch(