Files
Scoreboard/console/main/instances.js
T
AskaEth 2c9ff65fc4 feat: 补齐第一批已知缺口(关闭握手 / 定时备份 / Origin 白名单 / DTO 收敛)
- 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 配置的说明与实现对齐
2026-09-09 21:41:08 +08:00

88 lines
3.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict'
const 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 }