Add SCUM Source RCON transport

This commit is contained in:
npc0-hue
2026-07-23 10:55:31 +08:00
parent ec96c22e4c
commit 742f96ea02
33 changed files with 1297 additions and 19 deletions
@@ -0,0 +1,107 @@
import { type FormEvent, useState } from "react";
import { platformApiClient } from "../api/client";
import { sourceRCONChatRequest, sourceRCONRawCommandRequest } from "../schemas/sourceRcon";
import { ResultBadge } from "./StateViews";
interface SourceRCONCommandPanelProps {
serverId: string;
pluginId: string;
}
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 [rawCommand, setRawCommand] = useState("");
const [pending, setPending] = useState<"chat" | "command" | null>(null);
const [dispatch, setDispatch] = useState<DispatchState>(null);
if (pluginId !== "game.scum") {
return null;
}
async function sendChat(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setPending("chat");
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) });
} catch (error) {
setDispatch({ status: "failed", label: error instanceof Error ? error.message : "聊天消息提交失败" });
} finally {
setPending(null);
}
}
async function sendRawCommand(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setPending("command");
setDispatch({ status: "pending", label: "正在提交原始管理员指令" });
try {
const submitted = await platformApiClient.sendSourceRCONCommand(serverId, sourceRCONRawCommandRequest(serverId, rawCommand));
setRawCommand("");
setDispatch({ status: "succeeded", label: sourceRCONDispatchLabel(submitted.jobId, submitted.status) });
} catch (error) {
setDispatch({ status: "failed", label: error instanceof Error ? error.message : "原始管理员指令提交失败" });
} finally {
setPending(null);
}
}
return (
<article className="console-panel" aria-label="SCUM Source RCON controls">
<div className="panel-header">
<div>
<h2>SCUM </h2>
<p className="page-status"></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>
<label>
<textarea value={chatMessage} maxLength={1024} rows={3} onChange={(event) => setChatMessage(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>
</form>
</section>
<section className="console-module" aria-label="SCUM raw administrator command">
<div className="panel-header"><h2></h2></div>
<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" />
</label>
<p className="page-status"> SCUM</p>
<div className="action-strip"><button type="submit" className="icon-command" disabled={pending !== null || !rawCommand.trim()}>{pending === "command" ? "提交中…" : "发送指令"}</button></div>
</form>
</section>
</div>
</article>
);
}
function sourceRCONDispatchLabel(jobId: string, status: string): string {
return `${status === "queued" ? "排队" : "提交"} · 任务 ${jobId}`;
}