Stream live server logs over SSE

This commit is contained in:
npc0-hue
2026-08-03 22:28:54 +08:00
parent 5d4fca14f9
commit 7eac1926dd
48 changed files with 1526 additions and 263 deletions
@@ -1,7 +1,7 @@
import { type FormEvent, useState } from "react";
import { platformApiClient } from "../api/client";
import { sourceRCONChatRequest, sourceRCONRawCommandRequest } from "../schemas/sourceRcon";
import { scumAnnouncementCommand, scumManagementRCONCommandRequest } from "../schemas/scumManagementRcon";
import { ResultBadge } from "./StateViews";
interface SourceRCONCommandPanelProps {
@@ -12,28 +12,25 @@ interface SourceRCONCommandPanelProps {
type DispatchState = { status: "pending" | "succeeded" | "failed"; label: string } | null;
export function SourceRCONCommandPanel({ serverId, pluginId }: SourceRCONCommandPanelProps) {
const [chatType, setChatType] = useState(4);
const [chatMessage, setChatMessage] = useState("");
const [targetSteamId, setTargetSteamId] = useState("");
const [announcement, setAnnouncement] = useState("");
const [rawCommand, setRawCommand] = useState("");
const [pending, setPending] = useState<"chat" | "command" | null>(null);
const [pending, setPending] = useState<"announcement" | "command" | null>(null);
const [dispatch, setDispatch] = useState<DispatchState>(null);
if (pluginId !== "game.scum") {
return null;
}
async function sendChat(event: FormEvent<HTMLFormElement>) {
async function sendAnnouncement(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setPending("chat");
setDispatch({ status: "pending", label: "正在提交聊天消息" });
setPending("announcement");
setDispatch({ status: "pending", label: "正在提交服务器公告" });
try {
const submitted = await platformApiClient.sendSourceRCONCommand(serverId, sourceRCONChatRequest(serverId, { chatType, message: chatMessage, targetSteamId }));
setChatMessage("");
setTargetSteamId("");
setDispatch({ status: "succeeded", label: sourceRCONDispatchLabel(submitted.jobId, submitted.status) });
const submitted = await platformApiClient.queueGameClientBridgeCommand(serverId, scumManagementRCONCommandRequest(serverId, scumAnnouncementCommand(announcement)));
setAnnouncement("");
setDispatch({ status: "succeeded", label: protectedRCONDispatchLabel(submitted.id, submitted.state) });
} catch (error) {
setDispatch({ status: "failed", label: error instanceof Error ? error.message : "聊天消息提交失败" });
setDispatch({ status: "failed", label: error instanceof Error ? error.message : "服务器公告提交失败" });
} finally {
setPending(null);
}
@@ -44,9 +41,9 @@ export function SourceRCONCommandPanel({ serverId, pluginId }: SourceRCONCommand
setPending("command");
setDispatch({ status: "pending", label: "正在提交原始管理员指令" });
try {
const submitted = await platformApiClient.sendSourceRCONCommand(serverId, sourceRCONRawCommandRequest(serverId, rawCommand));
const submitted = await platformApiClient.queueGameClientBridgeCommand(serverId, scumManagementRCONCommandRequest(serverId, rawCommand));
setRawCommand("");
setDispatch({ status: "succeeded", label: sourceRCONDispatchLabel(submitted.jobId, submitted.status) });
setDispatch({ status: "succeeded", label: protectedRCONDispatchLabel(submitted.id, submitted.state) });
} catch (error) {
setDispatch({ status: "failed", label: error instanceof Error ? error.message : "原始管理员指令提交失败" });
} finally {
@@ -55,35 +52,24 @@ export function SourceRCONCommandPanel({ serverId, pluginId }: SourceRCONCommand
}
return (
<article className="console-panel" aria-label="SCUM Source RCON controls">
<article className="console-panel" aria-label="SCUM protected RCON controls">
<div className="panel-header">
<div>
<h2>SCUM </h2>
<p className="page-status"></p>
<h2>SCUM </h2>
<p className="page-status"> protected RCON </p>
</div>
{dispatch && <ResultBadge status={dispatch.status} label={dispatch.label} />}
</div>
<div className="operations-command-grid">
<section className="console-module" aria-label="SCUM chat command">
<div className="panel-header"><h2></h2></div>
<form className="provider-form" onSubmit={(event) => void sendChat(event)}>
<div className="form-grid">
<label>
<select value={chatType} onChange={(event) => setChatType(Number(event.target.value))} disabled={pending !== null}>
{[0, 1, 2, 3, 4, 5, 6, 7].map((value) => <option key={value} value={value}> {value}</option>)}
</select>
</label>
<label>
SteamID64
<input value={targetSteamId} inputMode="numeric" maxLength={17} onChange={(event) => setTargetSteamId(event.target.value)} disabled={pending !== null} placeholder="留空为广播" />
</label>
</div>
<section className="console-module" aria-label="SCUM announcement command">
<div className="panel-header"><h2></h2></div>
<form className="provider-form" onSubmit={(event) => void sendAnnouncement(event)}>
<label>
<textarea value={chatMessage} maxLength={1024} rows={3} onChange={(event) => setChatMessage(event.target.value)} disabled={pending !== null} placeholder="输入单行聊天内容" />
<textarea value={announcement} maxLength={1024} rows={3} onChange={(event) => setAnnouncement(event.target.value)} disabled={pending !== null} placeholder="输入单行公告内容" />
</label>
<div className="action-strip"><button type="submit" className="primary-command" disabled={pending !== null || !chatMessage.trim()}>{pending === "chat" ? "提交中…" : "发送聊天"}</button></div>
<p className="page-status"> SCUM #Announce </p>
<div className="action-strip"><button type="submit" className="primary-command" disabled={pending !== null || !announcement.trim()}>{pending === "announcement" ? "提交中…" : "发送公告"}</button></div>
</form>
</section>
<section className="console-module" aria-label="SCUM raw administrator command">
@@ -91,9 +77,9 @@ export function SourceRCONCommandPanel({ serverId, pluginId }: SourceRCONCommand
<form className="provider-form" onSubmit={(event) => void sendRawCommand(event)}>
<label>
<textarea value={rawCommand} maxLength={4000} rows={5} onChange={(event) => setRawCommand(event.target.value)} disabled={pending !== null} placeholder="例如 SetTime 12" />
<textarea value={rawCommand} maxLength={8192} rows={5} onChange={(event) => setRawCommand(event.target.value)} disabled={pending !== null} placeholder="例如 #ListPlayers 或 #SetTime 12" />
</label>
<p className="page-status"> SCUM</p>
<p className="page-status"> SCUM Run </p>
<div className="action-strip"><button type="submit" className="icon-command" disabled={pending !== null || !rawCommand.trim()}>{pending === "command" ? "提交中…" : "发送指令"}</button></div>
</form>
</section>
@@ -102,6 +88,6 @@ export function SourceRCONCommandPanel({ serverId, pluginId }: SourceRCONCommand
);
}
function sourceRCONDispatchLabel(jobId: string, status: string): string {
return `${status === "queued" ? "排队" : "提交"} · 任务 ${jobId}`;
function protectedRCONDispatchLabel(commandId: string, state: string): string {
return `${state === "pending" ? "排队" : "提交"} · 桥接命令 ${commandId}`;
}