- httpapi:新增 POST /api/shutdown(admin 会话或 X-Shutdown-Token 常量时间比对) 与 GET /api/rules/switches(recorder 可读,只回布尔开关);settings/standings 的 重复 DTO 收敛到 internal/dto;standings.showIndividualMedals 贯通到设置读写与前端 - export:抽出 export.Service,新增自动备份定时任务(backup.intervalMinutes/keep), 自动、手动、发布前三类备份共用同一把互斥锁与滚动清理 - realtime:/ws 去掉 InsecureSkipVerify,改用 OriginPatterns(同源与无 Origin 放行, 跨站需命中 server.allowedOrigins) - console:停止服务改走 POST /api/shutdown 优雅关闭(5 秒宽限后回退强杀), 服务管理器新增「已连接的大屏实例」卡片 - deploy:build.bat 改用 for /f 读内部署名——set /p 在 chcp 65001 下返回空, 导致最近这次构建的 exe 里根本没注入署名 - docs:备份策略、关闭超时、Origin 配置的说明与实现对齐
88 lines
3.3 KiB
JavaScript
88 lines
3.3 KiB
JavaScript
'use strict'
|
||
|
||
const http = require('http')
|
||
const { session } = require('electron')
|
||
|
||
// 大屏实例看板:复用服务端管理面板的登录态,不另造一套登录逻辑。
|
||
// 管理面板窗口与 Console 同属默认 session,登录后 scoreboard_session 就存在这里,
|
||
// 主进程取同一张 Cookie 访问 GET /api/display/instances(记录员及以上可读)。
|
||
// Cookie 名与服务端 server/internal/auth/auth.go 的 CookieSession 一致。
|
||
const SESSION_COOKIE = 'scoreboard_session'
|
||
const REQUEST_TIMEOUT_MS = 3000
|
||
|
||
function requestJSON({ host, port, path, cookie }) {
|
||
return new Promise((resolve) => {
|
||
const request = http.get(
|
||
{
|
||
host,
|
||
port,
|
||
path,
|
||
timeout: REQUEST_TIMEOUT_MS,
|
||
headers: {
|
||
Cookie: cookie,
|
||
Accept: 'application/json',
|
||
Connection: 'close'
|
||
}
|
||
},
|
||
(response) => {
|
||
const chunks = []
|
||
response.on('data', (chunk) => chunks.push(chunk))
|
||
response.on('end', () => {
|
||
resolve({ status: response.statusCode, body: Buffer.concat(chunks).toString('utf8') })
|
||
})
|
||
}
|
||
)
|
||
request.on('timeout', () => request.destroy(new Error('请求超时')))
|
||
request.on('error', (err) => resolve({ error: err }))
|
||
})
|
||
}
|
||
|
||
async function readSessionCookie(url) {
|
||
try {
|
||
const cookies = await session.defaultSession.cookies.get({ url })
|
||
const hit = cookies.find((item) => item.name === SESSION_COOKIE)
|
||
return hit ? `${hit.name}=${hit.value}` : null
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
// 成功返回 { ok: true, instances };失败返回 { ok: false, reason, message },
|
||
// message 直接作为界面占位文案,reason 供调用方决定要不要记日志(避免失败时刷屏)。
|
||
async function fetchDisplayInstances({ host, port }) {
|
||
const cookie = await readSessionCookie(`http://${host}:${port}/`)
|
||
if (!cookie) {
|
||
return { ok: false, reason: 'unauthenticated', message: '未登录,请先在服务端管理面板登录(记录员及以上)' }
|
||
}
|
||
|
||
const result = await requestJSON({ host, port, path: '/api/display/instances', cookie })
|
||
|
||
if (result.error) {
|
||
if (result.error.code === 'ECONNREFUSED' || result.error.code === 'ECONNRESET') {
|
||
return { ok: false, reason: 'offline', message: '服务端未运行,暂时无法读取大屏实例' }
|
||
}
|
||
return { ok: false, reason: 'error', message: `读取大屏实例失败:${result.error.message}` }
|
||
}
|
||
if (result.status === 401) {
|
||
return { ok: false, reason: 'unauthenticated', message: '登录已失效,请重新登录服务端管理面板' }
|
||
}
|
||
if (result.status === 403) {
|
||
return { ok: false, reason: 'forbidden', message: '当前账号权限不足(需要记录员及以上)' }
|
||
}
|
||
if (result.status === 404) {
|
||
return { ok: false, reason: 'unsupported', message: '服务端版本较旧,未提供大屏实例接口' }
|
||
}
|
||
if (result.status !== 200) {
|
||
return { ok: false, reason: 'error', message: `读取大屏实例失败:HTTP ${result.status}` }
|
||
}
|
||
|
||
try {
|
||
const payload = JSON.parse(result.body)
|
||
return { ok: true, instances: Array.isArray(payload.data) ? payload.data : [] }
|
||
} catch (err) {
|
||
return { ok: false, reason: 'error', message: `大屏实例响应无法解析:${err.message}` }
|
||
}
|
||
}
|
||
|
||
module.exports = { fetchDisplayInstances }
|