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
+2 -2
View File
@@ -15,9 +15,9 @@ import type {
GameClientBridgeStatusResponse
} from "../api/types";
const commandStates = new Set<GameClientBridgeCommandState>(["pending", "claimed", "succeeded", "failed", "cancelled", "expired"]);
const commandStates = new Set<GameClientBridgeCommandState>(["pending", "claimed", "succeeded", "failed", "cancelled", "expired", "unknown"]);
const approvalStates = new Set<GameClientBridgeApprovalState>(["not_required", "pending", "approved", "rejected"]);
const resultStatuses = new Set<GameClientBridgeResultStatus>(["succeeded", "failed", "cancelled"]);
const resultStatuses = new Set<GameClientBridgeResultStatus>(["succeeded", "failed", "cancelled", "unknown"]);
const forbiddenKeys = new Set([
"apikey",
"accesskey",
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { scumAnnouncementCommand, scumManagementRCONCommandRequest } from "./scumManagementRcon";
describe("SCUM management RCON bridge schema", () => {
it("builds a protected bridge request without connection material", () => {
const stamp = Date.UTC(2026, 7, 3, 8, 0, 0);
expect(scumManagementRCONCommandRequest("server/unsafe", " ListPlayers ", stamp)).toEqual({
profileKey: "scum-client-manager",
commandType: "management.rcon.request",
payload: { requestText: "#ListPlayers" },
idempotencyKey: `web:scum-rcon:server-unsafe:${stamp}`,
priority: 20,
expiresAt: "2026-08-03T08:02:00.000Z"
});
});
it("formats announcements and rejects framed command text", () => {
expect(scumAnnouncementCommand("Restart in ten minutes")).toBe("#Announce Restart in ten minutes");
expect(scumManagementRCONCommandRequest("server-1", "#SetTime 12").payload).toEqual({ requestText: "#SetTime 12" });
expect(() => scumManagementRCONCommandRequest("server-1", "ListPlayers\nSetTime 12")).toThrow("管理指令必须是受限的单行文本");
});
});
@@ -0,0 +1,41 @@
import type { GameClientBridgeQueueRequest } from "../api/types";
export const scumManagementRCONProfileKey = "scum-client-manager";
export const scumManagementRCONCommandType = "management.rcon.request";
const maxManagementCommandBytes = 8192;
const managementCommandTtlMs = 120_000;
export function scumManagementRCONCommandRequest(serverInstanceId: string, command: string, sequence = Date.now()): GameClientBridgeQueueRequest {
const stamp = Math.max(0, Math.floor(sequence));
return {
profileKey: scumManagementRCONProfileKey,
commandType: scumManagementRCONCommandType,
payload: { requestText: normalizeSCUMManagementCommand(command, "管理指令") },
idempotencyKey: `web:scum-rcon:${safeBridgeIdentifierPart(serverInstanceId)}:${stamp}`,
priority: 20,
expiresAt: new Date(stamp + managementCommandTtlMs).toISOString()
};
}
export function scumAnnouncementCommand(message: string): string {
return `#Announce ${validateSCUMManagementRCONText(message, "公告内容")}`;
}
export function validateSCUMManagementRCONText(value: string, label: string): string {
const normalized = value.trim();
if (!normalized || new TextEncoder().encode(normalized).byteLength > maxManagementCommandBytes || /[\u0000\r\n]/.test(normalized)) {
throw new Error(`${label}必须是受限的单行文本。`);
}
return normalized;
}
function normalizeSCUMManagementCommand(value: string, label: string): string {
const normalized = validateSCUMManagementRCONText(value, label);
return normalized.startsWith("#") ? normalized : `#${normalized}`;
}
function safeBridgeIdentifierPart(value: string): string {
const normalized = value.trim().replace(/[^A-Za-z0-9._:-]+/g, "-").replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, "").slice(0, 96);
return normalized || "server";
}