Integrate SCUM real ops workflows

This commit is contained in:
npc0-hue
2026-08-10 21:12:53 +08:00
parent 1063330710
commit a770bc6250
88 changed files with 6375 additions and 2719 deletions
@@ -1,26 +1,11 @@
import type { SCUMConfigField, SCUMFeatureWorkspace, SCUMLogicalDirectory, SCUMLogicalFile } from "./contracts.js";
type StateSetter<T> = (next: T | ((previous: T) => T)) => void;
export type ReactLike = {
createElement: (...args: any[]) => any;
useEffect?: (effect: () => void | (() => void), deps: readonly unknown[]) => void;
useState?: <T>(initialState: T | (() => T)) => [T, StateSetter<T>];
};
export type SCUMFileReadSnapshot = {
serverInstanceId: string;
pluginId: string;
key: string;
state: "ready" | "pending" | "not-read" | "unavailable" | string;
content?: string;
version?: number;
checksum?: string;
sizeBytes?: number;
jobId?: string;
readAt?: string;
reason?: string;
};
export type SCUMPageContext = {
pageKey?: string;
pageTitle?: string;
@@ -28,499 +13,186 @@ export type SCUMPageContext = {
permissions: string[];
availability: { available: boolean; reason?: string };
featureAvailability?: Array<{ key: string; available: boolean; reason?: string }>;
workspace?: SCUMFeatureWorkspace;
workspaceActions?: {
refreshWorkspace?: () => Promise<SCUMFeatureWorkspace | undefined>;
requestFile?: (fileKey: string) => Promise<{ status: string; message: string; jobId?: string }>;
getFileSnapshot?: (fileKey: string) => Promise<SCUMFileReadSnapshot>;
writeFile?: (fileKey: string, content: string, options?: { expectedChecksum?: string }) => Promise<{ status: string; message: string; jobId?: string }>;
};
workspaceActions?: SCUMWorkspaceActions;
};
type NormalizedWorkspace = { defaultDirectoryKey: string; directories: readonly SCUMLogicalDirectory[]; files: readonly SCUMLogicalFile[]; configFields: readonly SCUMConfigField[] };
type ConfigMode = "fields" | "pairs" | "source";
type RawEncoding = "utf-8" | "utf-16le";
type FileRequestState = { fileKey: string; status: string; message: string; jobId?: string } | null;
type WorkspaceRefreshState = { status: string; message: string } | null;
type PreviewState = { fileKey: string; mode: ConfigMode; summary: string; proposedContent: string; lines: readonly DiffLine[] } | null;
type DiffLine = { kind: "same" | "added" | "removed"; text: string };
type SCUMWorkspaceActions = {
listSCUMPlayers?: () => Promise<unknown>;
listSCUMSquads?: () => Promise<unknown>;
listSCUMSquadMembers?: () => Promise<unknown>;
listSCUMVehicles?: () => Promise<unknown>;
listSCUMFlags?: () => Promise<unknown>;
listSCUMPositions?: () => Promise<unknown>;
listSCUMOperations?: () => Promise<unknown>;
createSCUMOperation?: (request: unknown) => Promise<unknown>;
listSCUMWorkflows?: () => Promise<unknown>;
createSCUMWorkflow?: (request: unknown) => Promise<unknown>;
listSCUMWorkflowSteps?: (workflowId?: string) => Promise<unknown>;
};
type RecordMap = Record<string, unknown>;
type DataState = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: SCUMSurfaceData };
type ActionState = { status: "idle" | "pending" | "ok" | "error"; message?: string };
type SCUMSurfaceData = { players: RecordMap[]; squads: RecordMap[]; members: RecordMap[]; vehicles: RecordMap[]; flags: RecordMap[]; positions: RecordMap[]; operations: RecordMap[]; workflows: RecordMap[]; steps: RecordMap[] };
const emptyData: SCUMSurfaceData = { players: [], squads: [], members: [], vehicles: [], flags: [], positions: [], operations: [], workflows: [], steps: [] };
export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext) {
const e = react.createElement;
if (input.pageKey && input.pageKey !== "files-config") return renderSCUMFeatureSurface(e, input);
const workspace = normalizeWorkspace(input.workspace);
const [selectedDirectoryKey, setSelectedDirectoryKey] = usePluginState(react, workspace.defaultDirectoryKey);
const effectiveDirectoryKey = workspace.directories.some((directory) => directory.key === selectedDirectoryKey) ? selectedDirectoryKey : workspace.defaultDirectoryKey;
const directoryFiles = workspace.files.filter((file) => file.directoryKey === effectiveDirectoryKey);
const defaultFileKey = initialFileKey(workspace, effectiveDirectoryKey);
const [selectedFileKey, setSelectedFileKey] = usePluginState(react, defaultFileKey);
const selectedFile = directoryFiles.find((file) => file.key === selectedFileKey) ?? directoryFiles[0] ?? workspace.files[0];
const selectedFields = selectedFile ? workspace.configFields.filter((field) => field.fileKey === selectedFile.key) : [];
const [configMode, setConfigMode] = usePluginState<ConfigMode>(react, "fields");
const [fieldDraft, setFieldDraft] = usePluginState<Record<string, string>>(react, {});
const [pairDraft, setPairDraft] = usePluginState<Record<string, string>>(react, {});
const [rawDraft, setRawDraft] = usePluginState<Record<string, string>>(react, {});
const [rawEncoding, setRawEncoding] = usePluginState<RawEncoding>(react, "utf-8");
const [snapshot, setSnapshot] = usePluginState<SCUMFileReadSnapshot | null>(react, null);
const [requestState, setRequestState] = usePluginState<FileRequestState>(react, null);
const [workspaceRefreshState, setWorkspaceRefreshState] = usePluginState<WorkspaceRefreshState>(react, null);
const [writeState, setWriteState] = usePluginState<FileRequestState>(react, null);
const [preview, setPreview] = usePluginState<PreviewState>(react, null);
const scoped = Boolean(input.serverInstanceId);
const canFilesRead = scoped && input.permissions.includes("server.files.read");
const canFilesWrite = scoped && input.permissions.includes("server.files.write");
const [state, setState] = usePluginState<DataState>(react, { status: "loading" });
const [action, setAction] = usePluginState<ActionState>(react, { status: "idle" });
const pageKey = input.pageKey ?? "players";
if (react.useEffect) {
react.useEffect(() => {
let active = true;
setPreview(null);
setWriteState(null);
if (!selectedFile || !input.workspaceActions?.getFileSnapshot) {
setSnapshot(null);
return () => { active = false; };
}
void input.workspaceActions.getFileSnapshot(selectedFile.key).then((next) => {
if (active) setSnapshot(next);
}).catch((error) => {
if (active) setSnapshot({ serverInstanceId: input.serverInstanceId ?? "", pluginId: "game.scum", key: selectedFile.key, state: "unavailable", reason: error instanceof Error ? error.message : "无法读取文件快照。" });
});
return () => { active = false; };
}, [input.serverInstanceId, input.workspaceActions?.getFileSnapshot, selectedFile?.key]);
}
const refresh = () => {
const actions = input.workspaceActions;
if (!input.serverInstanceId || !actions) {
setState({ status: "error", reason: "插件页面没有绑定服务器,无法读取 SCUM 投影。" });
return;
}
setState({ status: "loading" });
void Promise.all([
safeList(actions.listSCUMPlayers), safeList(actions.listSCUMSquads), safeList(actions.listSCUMSquadMembers), safeList(actions.listSCUMVehicles),
safeList(actions.listSCUMFlags), safeList(actions.listSCUMPositions), safeList(actions.listSCUMOperations), safeList(actions.listSCUMWorkflows), safeList(actions.listSCUMWorkflowSteps)
]).then(([players, squads, members, vehicles, flags, positions, operations, workflows, steps]) => setState({ status: "ready", data: { players, squads, members, vehicles, flags, positions, operations, workflows, steps } }))
.catch((error) => setState({ status: "error", reason: error instanceof Error ? error.message : "SCUM 投影读取失败。" }));
};
function refreshWorkspaceCatalog() {
if (!input.workspaceActions?.refreshWorkspace) return;
setWorkspaceRefreshState({ status: "pending", message: "正在从平台刷新声明文件列表..." });
void input.workspaceActions.refreshWorkspace().then((next) => {
const count = next?.files?.length ?? 0;
setWorkspaceRefreshState({ status: "ok", message: `文件列表已刷新:${count} 个声明文件。` });
}).catch((error) => {
setWorkspaceRefreshState({ status: "error", message: error instanceof Error ? error.message : "文件列表刷新失败。" });
});
}
if (react.useEffect) react.useEffect(() => { refresh(); return undefined; }, [input.serverInstanceId, pageKey, input.workspaceActions]);
function loadFileSnapshot(fileKey: string, poll = false, attempt = 0) {
if (!input.workspaceActions?.getFileSnapshot) return;
void input.workspaceActions.getFileSnapshot(fileKey).then((next) => {
setSnapshot(next);
if (poll && next.state !== "ready" && attempt < 8) {
globalThis.setTimeout(() => loadFileSnapshot(fileKey, true, attempt + 1), Math.min(3200, 600 + attempt * 350));
}
}).catch((error) => {
setSnapshot({ serverInstanceId: input.serverInstanceId ?? "", pluginId: "game.scum", key: fileKey, state: "unavailable", reason: error instanceof Error ? error.message : "无法读取文件快照。" });
});
}
function selectDirectory(directoryKey: string) {
const nextFiles = workspace.files.filter((file) => file.directoryKey === directoryKey);
const nextFile = nextFiles.find((file) => file.kind === "config") ?? nextFiles[0];
setSelectedDirectoryKey(directoryKey);
if (nextFile) selectFile(nextFile, directoryKey);
}
function selectFile(file: SCUMLogicalFile, directoryKey = file.directoryKey) {
setSelectedDirectoryKey(directoryKey);
setSelectedFileKey(file.key);
setConfigMode(file.kind === "config" && workspace.configFields.some((field) => field.fileKey === file.key) ? "fields" : "source");
setRequestState(null);
setWriteState(null);
setPreview(null);
setRawEncoding("utf-8");
}
function requestSelectedFile() {
if (!selectedFile || !canFilesRead || !input.workspaceActions?.requestFile) return;
setRequestState({ fileKey: selectedFile.key, status: "pending", message: "正在提交文件读取请求..." });
setPreview(null);
void input.workspaceActions.requestFile(selectedFile.key).then((result) => {
setRequestState({ fileKey: selectedFile.key, status: result.status, message: result.message, jobId: result.jobId });
if (result.status === "queued" || result.status === "ok") {
setSnapshot({ serverInstanceId: input.serverInstanceId ?? "", pluginId: "game.scum", key: selectedFile.key, state: "pending", jobId: result.jobId, reason: "等待运行端完成文件读取。" });
loadFileSnapshot(selectedFile.key, true);
}
}).catch((error) => setRequestState({ fileKey: selectedFile.key, status: "error", message: error instanceof Error ? error.message : "文件读取请求失败。" }));
}
function refreshSelectedSnapshot() {
if (!selectedFile || !input.workspaceActions?.getFileSnapshot) return;
setPreview(null);
loadFileSnapshot(selectedFile.key);
}
function previewModeChange(mode: ConfigMode, content: string, current: string) {
if (!selectedFile) return;
const diff = buildSimpleDiff(current, content);
setPreview({ fileKey: selectedFile.key, mode, proposedContent: content, summary: diff.summary, lines: diff.lines });
}
function writePreviewedContent() {
if (!selectedFile || !preview || preview.fileKey !== selectedFile.key || !input.workspaceActions?.writeFile) return;
setWriteState({ fileKey: selectedFile.key, status: "pending", message: "正在提交声明文件写入..." });
void input.workspaceActions.writeFile(selectedFile.key, preview.proposedContent, { expectedChecksum: snapshot?.checksum }).then((result) => {
setWriteState({ fileKey: selectedFile.key, status: result.status, message: result.message, jobId: result.jobId });
setPreview(null);
}).catch((error) => setWriteState({ fileKey: selectedFile.key, status: "error", message: error instanceof Error ? error.message : "文件写入请求失败。" }));
}
return e("section", { className: "console-panel", "aria-label": "SCUM 文件管理" },
const data = state.status === "ready" ? state.data : emptyData;
return e("section", { className: "console-panel", "aria-label": input.pageTitle ?? surfaceTitle(pageKey) },
e("div", { className: "panel-header" },
e("div", null, e("h2", null, "文件管理"), e("p", { className: "provider-id" }, "第一级选择目录,第二级选择目录内文件;配置默认表单,日志只读原文。")),
e("span", { className: "page-status" }, scoped ? "声明文件工作区" : "插件页面未绑定服务器")
),
e("div", { className: "file-workbench" },
navigationPane(e, workspace, effectiveDirectoryKey, directoryFiles, selectedFile?.key, workspaceRefreshState, Boolean(input.workspaceActions?.refreshWorkspace), selectDirectory, selectFile, refreshWorkspaceCatalog),
selectedFile
? fileDetail(e, {
file: selectedFile,
fields: selectedFields,
mode: selectedFile.kind === "log" ? "source" : configMode,
setMode: setConfigMode,
fieldDraft,
setFieldDraft,
pairDraft,
setPairDraft,
rawDraft,
setRawDraft,
rawEncoding,
setRawEncoding,
snapshot: snapshot?.key === selectedFile.key ? snapshot : null,
requestState: requestState?.fileKey === selectedFile.key ? requestState : null,
writeState: writeState?.fileKey === selectedFile.key ? writeState : null,
preview: preview?.fileKey === selectedFile.key ? preview : null,
canFilesRead,
canFilesWrite,
canRequestFile: Boolean(input.workspaceActions?.requestFile),
canRefreshSnapshot: Boolean(input.workspaceActions?.getFileSnapshot),
canWriteFile: Boolean(input.workspaceActions?.writeFile),
onRequestFile: requestSelectedFile,
onRefreshSnapshot: refreshSelectedSnapshot,
onPreview: previewModeChange,
onWrite: writePreviewedContent
})
: e("div", { className: "file-workbench-detail" }, e("p", { className: "page-status" }, "当前插件没有可展示的声明文件。"))
)
);
}
function renderSCUMFeatureSurface(e: ReactLike["createElement"], input: SCUMPageContext) {
const meta = scumSurfaceMeta(input.pageKey ?? "");
const featureRows = (input.featureAvailability ?? [])
.filter((feature) => meta.features.includes(feature.key))
.map((feature) => e("div", { key: feature.key, className: "console-row" }, e("span", null, feature.key), e("strong", null, feature.available ? "可用" : feature.reason ?? "等待 Companion")));
return e("section", { className: "console-panel", "aria-label": input.pageTitle ?? meta.title },
e("div", { className: "panel-header" },
e("div", null, e("h2", null, input.pageTitle ?? meta.title), e("p", { className: "provider-id" }, meta.summary)),
e("span", { className: "page-status" }, input.availability.available ? "Companion 在线" : input.availability.reason ?? "等待 Companion")
),
e("div", { className: "console-row-list" },
e("div", { className: "console-row" }, e("span", null, "服务器"), e("strong", null, input.serverInstanceId ?? "未绑定")),
e("div", { className: "console-row" }, e("span", null, "权限"), e("strong", null, input.permissions.join(" / ") || "未声明")),
featureRows.length ? featureRows : e("div", { className: "console-row" }, e("span", null, "插件能力"), e("strong", null, meta.features.join(" / ") || "由插件声明"))
)
);
}
function scumSurfaceMeta(pageKey: string): { title: string; summary: string; features: string[] } {
switch (pageKey) {
case "players": return { title: "用户管理", summary: "玩家查询、在线状态、维护窗口和状态修正由 SCUM 插件 Companion 提供。", features: ["player.intelligence", "state.patch"] };
case "squads": return { title: "队伍管理", summary: "队伍列表、成员关系和风险上下文来自插件声明的 squads 快照。", features: ["player.intelligence"] };
case "live-map": return { title: "实时地图", summary: "玩家、载具和轨迹采样由插件事件流驱动。", features: ["trajectory.collect", "vehicle.spawn"] };
case "gifts": return { title: "礼包管理", summary: "礼包目录、发放和玩家通知通过受保护插件命令执行。", features: ["reward.delivery"] };
default: return { title: "SCUM 插件页面", summary: "该页面由 SCUM 插件声明。", features: [] };
}
}
function navigationPane(e: ReactLike["createElement"], workspace: NormalizedWorkspace, directoryKey: string, directoryFiles: readonly SCUMLogicalFile[], selectedFileKey: string | undefined, refreshState: WorkspaceRefreshState, canRefreshWorkspace: boolean, onDirectoryChange: (directoryKey: string) => void, onFileSelect: (file: SCUMLogicalFile) => void, onRefreshWorkspace: () => void) {
const activeDirectory = workspace.directories.find((directory) => directory.key === directoryKey);
const selectedFile = directoryFiles.find((file) => file.key === selectedFileKey) ?? directoryFiles[0];
return e("aside", { className: "file-workbench-nav", "aria-label": "SCUM 文件两级菜单" },
e("div", { className: "console-row-actions file-workbench-nav-actions" },
e("button", { type: "button", className: "icon-command", disabled: !canRefreshWorkspace, onClick: onRefreshWorkspace }, "刷新文件列表")
),
refreshState ? e("p", { className: "page-status", "data-state": refreshState.status }, refreshState.message) : null,
e("label", { className: "file-workbench-picker" },
e("span", null, "目录"),
e("select", { value: directoryKey, onChange: (event: { target: { value: string } }) => onDirectoryChange(event.target.value), "aria-label": "选择目录" },
workspace.directories.length ? workspace.directories.map((directory) => e("option", { key: directory.key, value: directory.key }, `${directory.label} · ${directoryScopeLabel(directory.scope)}`)) : e("option", { value: "" }, "没有已声明目录")
)
),
e("div", { className: "file-workbench-directory-heading" },
e("strong", null, activeDirectory?.label ?? "插件声明目录"),
e("span", null, activeDirectory ? `${activeDirectory.key} · ${directoryScopeLabel(activeDirectory.scope)}` : "等待平台声明"),
e("small", null, `${directoryFiles.length} 个声明文件`)
),
e("div", { className: "file-workbench-directory", role: "listbox", "aria-label": "声明文件列表" },
directoryFiles.length
? directoryFiles.map((file) => e("button", { key: file.key, type: "button", className: "file-workbench-file", role: "option", "aria-selected": file.key === selectedFile?.key, "aria-current": file.key === selectedFile?.key ? "page" : undefined, onClick: () => onFileSelect(file) },
e("strong", null, file.label),
e("span", null, file.kind === "config" ? file.editable === false ? "配置只读" : "配置可写" : `日志${file.streamKey ? ` · ${file.streamKey}` : ""}`)
))
: e("p", { className: "page-status" }, "当前目录没有平台声明文件。")
)
);
}
function fileDetail(e: ReactLike["createElement"], props: {
file: SCUMLogicalFile; fields: readonly SCUMConfigField[]; mode: ConfigMode; setMode: StateSetter<ConfigMode>; fieldDraft: Record<string, string>; setFieldDraft: StateSetter<Record<string, string>>; pairDraft: Record<string, string>; setPairDraft: StateSetter<Record<string, string>>;
rawDraft: Record<string, string>; setRawDraft: StateSetter<Record<string, string>>; rawEncoding: RawEncoding; setRawEncoding: StateSetter<RawEncoding>; snapshot: SCUMFileReadSnapshot | null;
requestState: FileRequestState; writeState: FileRequestState; preview: PreviewState; canFilesRead: boolean; canFilesWrite: boolean; canRequestFile: boolean; canRefreshSnapshot: boolean; canWriteFile: boolean;
onRequestFile: () => void; onRefreshSnapshot: () => void; onPreview: (mode: ConfigMode, content: string, current: string) => void; onWrite: () => void;
}) {
const canShowFields = props.file.kind === "config" && props.fields.length > 0;
const currentContent = props.snapshot?.state === "ready" ? decodeRawContent(props.snapshot.content ?? "", props.rawEncoding) : "";
const snapshotValues = props.snapshot?.state === "ready" ? parseIniAssignments(currentContent) : {};
return e("article", { className: "file-workbench-detail", "aria-label": `文件 ${props.file.label}` },
e("div", { className: "panel-header" },
e("div", null, e("h2", null, props.file.label), e("p", { className: "provider-id" }, `${props.file.directoryKey} · ${props.file.kind === "config" ? "配置文件" : "日志文件"}${props.file.streamKey ? ` · ${props.file.streamKey}` : ""}`)),
e("div", null, e("h2", null, input.pageTitle ?? surfaceTitle(pageKey)), e("p", { className: "provider-id" }, surfaceSummary(pageKey))),
e("div", { className: "console-row-actions" },
e("button", { type: "button", className: "icon-command", disabled: !props.canFilesRead || !props.canRequestFile, onClick: props.onRequestFile }, "读取文件"),
e("button", { type: "button", className: "icon-command", disabled: !props.canRefreshSnapshot, onClick: props.onRefreshSnapshot }, "刷新结果")
e("span", { className: "page-status" }, input.availability.available ? "投影/Companion 可用" : input.availability.reason ?? "等待 Run/Companion"),
e("button", { type: "button", className: "icon-command", onClick: refresh }, "刷新投影"),
workflowButton(e, input, setAction, refresh, pageWorkflow(pageKey))
)
),
props.requestState ? e("p", { className: "page-status", "data-state": props.requestState.status }, props.requestState.message) : null,
props.writeState ? e("p", { className: "page-status", "data-state": props.writeState.status }, props.writeState.message) : null,
props.file.kind === "log" || props.snapshot?.state === "ready" ? encodingSwitcher(e, props.rawEncoding, props.setRawEncoding) : null,
canShowFields
? e("div", { className: "file-workbench-mode", role: "tablist", "aria-label": "表单配置与原文模式" },
e("span", null, "编辑视图"),
e("button", { type: "button", role: "tab", "aria-selected": props.mode === "fields", className: props.mode === "fields" ? "file-workbench-mode-active" : undefined, onClick: () => props.setMode("fields") }, "配置表单"),
e("button", { type: "button", role: "tab", "aria-selected": props.mode === "pairs", className: props.mode === "pairs" ? "file-workbench-mode-active" : undefined, onClick: () => props.setMode("pairs") }, "键值视图"),
e("button", { type: "button", role: "tab", "aria-selected": props.mode === "source", className: props.mode === "source" ? "file-workbench-mode-active" : undefined, onClick: () => props.setMode("source") }, "原文模式")
)
: null,
canShowFields && props.mode === "fields"
? modeledConfigurationFields(e, props.file, props.fields, props.fieldDraft, props.setFieldDraft, snapshotValues, currentContent, props.snapshot?.state === "ready", props.canFilesWrite, props.canWriteFile, props.preview, props.onPreview, props.onWrite)
: canShowFields && props.mode === "pairs"
? modeledKeyValueEditor(e, props.file, props.fields, props.pairDraft, props.setPairDraft, snapshotValues, currentContent, props.snapshot?.state === "ready", props.canFilesWrite, props.canWriteFile, props.preview, props.onPreview, props.onWrite)
: rawFileView(e, props.file, currentContent, props.rawDraft, props.setRawDraft, props.snapshot, props.canFilesWrite, props.canWriteFile, props.preview, props.onPreview, props.onWrite)
action.status !== "idle" ? e("p", { className: "page-status", "data-state": action.status }, action.message) : null,
state.status === "loading" ? e("p", { className: "page-status" }, "正在读取平台本地 SCUM 投影…") : null,
state.status === "error" ? e("p", { className: "page-status", "data-state": "error" }, state.reason) : null,
state.status === "ready" ? renderSurfaceBody(e, pageKey, data, input, setAction, refresh) : null
);
}
function encodingSwitcher(e: ReactLike["createElement"], encoding: RawEncoding, setEncoding: StateSetter<RawEncoding>) {
return e("div", { className: "file-workbench-mode file-workbench-encoding", role: "tablist", "aria-label": "原文编码" },
e("span", null, "文本编码"),
e("button", { type: "button", role: "tab", "aria-selected": encoding === "utf-8", className: encoding === "utf-8" ? "file-workbench-mode-active" : undefined, onClick: () => setEncoding("utf-8") }, "UTF-8"),
e("button", { type: "button", role: "tab", "aria-selected": encoding === "utf-16le", className: encoding === "utf-16le" ? "file-workbench-mode-active" : undefined, onClick: () => setEncoding("utf-16le") }, "UTF-16 LE")
function renderSurfaceBody(e: ReactLike["createElement"], pageKey: string, data: SCUMSurfaceData, input: SCUMPageContext, setAction: StateSetter<ActionState>, refresh: () => void) {
switch (pageKey) {
case "players": return playersSurface(e, data, input, setAction, refresh);
case "squads": return squadsSurface(e, data);
case "live-map": return mapSurface(e, data);
case "gifts": return giftsSurface(e, data, input, setAction, refresh);
case "workflows": return workflowsSurface(e, data);
default: return playersSurface(e, data, input, setAction, refresh);
}
}
function playersSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input: SCUMPageContext, setAction: StateSetter<ActionState>, refresh: () => void) {
return e("div", { className: "console-record-list" },
statsStrip(e, [["玩家投影", data.players.length], ["在线", data.players.filter((p) => boolField(p, "Online", "online")).length], ["坐标", data.positions.length], ["待审操作", data.operations.filter((op) => field(op, "Status", "status") === "waiting").length]]),
data.players.length ? data.players.slice(0, 80).map((player) => e("article", { key: idOf(player), className: "console-record" },
e("div", { className: "console-record-head" }, e("strong", null, textField(player, "DisplayName", "displayName") || textField(player, "GamePlayerID", "gamePlayerId") || "未知玩家"), e("span", { className: `status-pill ${boolField(player, "Online", "online") ? "status-active" : "status-disabled"}` }, boolField(player, "Online", "online") ? "在线" : "离线/未知")),
e("div", { className: "console-record-meta" }, e("span", null, `Steam ${textField(player, "SteamID", "steamId") || "unknown"}`), e("span", null, `Profile ${textField(player, "UserProfileID", "userProfileId") || "unknown"}`), e("span", null, `队伍 ${textField(player, "SquadName", "squadName") || textField(player, "SquadID", "squadId") || "unknown"}`), e("span", null, freshness(player))),
e("span", { className: "provider-id" }, `Fame ${numField(player, "FamePoints", "famePoints")} · Cash ${numField(player, "NormalBalance", "normalBalance")} · Gold ${numField(player, "GoldBalance", "goldBalance")} · ${coords(field(player, "Position", "position") as RecordMap | undefined)}`),
e("div", { className: "console-row-actions" },
operationButton(e, input, setAction, refresh, player, "player.fame.set", "fame", "Fame +100", 100),
operationButton(e, input, setAction, refresh, player, "player.currency.normal.set", "amount", "现金 +1000", 1000),
operationButton(e, input, setAction, refresh, player, "player.attribute.855.set", "after", "855 审批", Number(numField(player, "855", "855")) || 1, true)
)
)) : e("p", { className: "page-status" }, "暂无玩家投影。先运行 player/world refresh workflow;不会显示假玩家。")
);
}
function modeledConfigurationFields(e: ReactLike["createElement"], file: SCUMLogicalFile, fields: readonly SCUMConfigField[], draft: Record<string, string>, setDraft: StateSetter<Record<string, string>>, values: Record<string, string>, currentContent: string, hasReadSnapshot: boolean, canFilesWrite: boolean, canWriteFile: boolean, preview: PreviewState, onPreview: (mode: ConfigMode, content: string, current: string) => void, onWrite: () => void) {
const editable = hasReadSnapshot && canFilesWrite && file.editable !== false;
const fieldKeys = new Set(fields.map((field) => field.key));
const selectedDraft = Object.fromEntries(Object.entries(draft).filter(([key]) => fieldKeys.has(key)));
const hasChanges = Object.keys(selectedDraft).length > 0;
const unknown = unknownIniAssignments(currentContent, fields);
return e("div", { className: "file-workbench-fields" },
fields.map((field) => {
const value = draft[field.key] ?? values[field.configKey] ?? field.defaultValue;
return e("label", { className: "file-workbench-field", key: field.key },
e("span", null, e("strong", null, field.label), e("small", null, `${field.configKey} · ${field.description}`)),
fieldControl(e, field, value, editable, (next) => setDraft((current) => ({ ...current, [field.key]: next }))),
e("small", null, `${controlLabel(field)} · ${field.restartImpact === "restart-required" ? "修改后需要受控重启" : "可在安全窗口内生效"}`)
);
}),
unknown.length ? e("section", { className: "file-workbench-unknown", "aria-label": "未建模配置项" }, e("h3", null, "未建模配置项"), unknown.map((item) => e("div", { className: "console-row", key: `${item.key}:${item.index}` }, e("span", null, item.key), e("strong", null, item.value || "空值")))) : null,
e("div", { className: "console-row-actions file-workbench-actions" },
e("button", { type: "button", className: "icon-command", disabled: !editable || !hasChanges, onClick: () => onPreview("fields", composeIniContent(currentContent, fields, selectedDraft), currentContent) }, "预览改动"),
e("button", { type: "button", className: "primary-command", disabled: !editable || !canWriteFile || !preview || preview.mode !== "fields", onClick: onWrite }, "提交写入")
),
preview && preview.mode === "fields" ? diffPreview(e, preview) : null,
e("p", { className: "page-status" }, hasReadSnapshot ? editable ? "配置表单会保留原文中的未知行;提交前先预览差异。" : "当前账号仅有读取权限,配置项为只读。" : "先读取文件后才显示服务器当前值;未读取时不会把默认值伪装成原文。")
function squadsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData) {
return e("div", { className: "overview-two-col" },
tablePanel(e, "队伍", data.squads, (squad) => [textField(squad, "Name", "name") || textField(squad, "SquadID", "squadId"), `成员 ${numField(squad, "MemberCount", "memberCount")}`, `队长 ${textField(squad, "LeaderProfileID", "leaderProfileId") || "unknown"}`, freshness(squad)]),
tablePanel(e, "成员 / 旗帜", [...data.members.slice(0, 40), ...data.flags.slice(0, 40)], (item) => [textField(item, "DisplayName", "displayName") || textField(item, "FlagID", "flagId") || "unknown", textField(item, "Rank", "rank") || textField(item, "OwnershipConfidence", "ownershipConfidence") || "unknown", textField(item, "SquadID", "squadId") || textField(item, "OwnerSquadID", "ownerSquadId") || "unknown", freshness(item)])
);
}
function modeledKeyValueEditor(e: ReactLike["createElement"], file: SCUMLogicalFile, fields: readonly SCUMConfigField[], draft: Record<string, string>, setDraft: StateSetter<Record<string, string>>, values: Record<string, string>, currentContent: string, hasReadSnapshot: boolean, canFilesWrite: boolean, canWriteFile: boolean, preview: PreviewState, onPreview: (mode: ConfigMode, content: string, current: string) => void, onWrite: () => void) {
const editable = hasReadSnapshot && canFilesWrite && file.editable !== false;
const currentPairs = modeledPairText(fields, values);
const rawValue = draft[file.key] ?? currentPairs;
const parsed = parseModeledPairDraft(rawValue, fields);
const proposedContent = composeIniContent(currentContent, fields, parsed.changes);
const hasChanges = proposedContent !== currentContent && Object.keys(parsed.changes).length > 0;
return e("div", { className: "file-workbench-raw file-workbench-pairs" },
e("textarea", { className: "file-workbench-raw-editor file-workbench-keyvalue-editor", value: rawValue, readOnly: !editable, spellCheck: false, "aria-label": `${file.label} 键值视图编辑`, onChange: (event: { target: { value: string } }) => setDraft((current) => ({ ...current, [file.key]: event.target.value })) }),
parsed.unknown.length ? e("section", { className: "file-workbench-unknown", "aria-label": "不可写键值" }, e("h3", null, "不可写键值"), parsed.unknown.map((key) => e("div", { className: "console-row", key }, e("span", null, key), e("strong", null, "未在声明字段中")))) : null,
e("div", { className: "console-row-actions file-workbench-actions" },
e("button", { type: "button", className: "icon-command", disabled: !editable || parsed.unknown.length > 0 || !hasChanges, onClick: () => onPreview("pairs", proposedContent, currentContent) }, "预览改动"),
e("button", { type: "button", className: "primary-command", disabled: !editable || !canWriteFile || !preview || preview.mode !== "pairs", onClick: onWrite }, "提交写入")
),
preview && preview.mode === "pairs" ? diffPreview(e, preview) : null,
e("p", { className: "page-status" }, hasReadSnapshot ? "键值视图适合批量调整已声明字段;未知键只展示,不会被作为安全写入目标。" : "先读取文件后才启用键值编辑。")
function mapSurface(e: ReactLike["createElement"], data: SCUMSurfaceData) {
const overlays = [...data.positions, ...data.vehicles.map((v) => field(v, "Position", "position") as RecordMap).filter(Boolean), ...data.flags.map((f) => field(f, "Position", "position") as RecordMap).filter(Boolean)];
return e("div", { className: "console-record-list" },
statsStrip(e, [["玩家", data.players.length], ["载具", data.vehicles.length], ["旗帜", data.flags.length], ["坐标点", overlays.length]]),
e("div", { className: "map-projection-board" }, overlays.slice(0, 120).map((point, index) => e("span", { key: `${idOf(point)}:${index}`, className: "map-projection-dot", title: `${textField(point, "SubjectType", "subjectType") || "point"} ${coords(point)}`, style: dotStyle(point) }, ""))),
tablePanel(e, "地图覆盖物", overlays, (point) => [textField(point, "SubjectType", "subjectType") || "unknown", textField(point, "SubjectID", "subjectId") || textField(point, "GamePlayerID", "gamePlayerId") || textField(point, "VehicleID", "vehicleId") || "unknown", coords(point), freshness(point)])
);
}
function fieldControl(e: ReactLike["createElement"], field: SCUMConfigField, value: string, editable: boolean, onChange: (next: string) => void) {
if (field.control === "boolean") {
return e("select", { value: normalizeBoolean(value) ? "true" : "false", disabled: !editable, "aria-label": field.label, onChange: (event: { target: { value: string } }) => onChange(event.target.value) },
e("option", { value: "true" }, ""),
e("option", { value: "false" }, "否")
);
}
if (field.control === "number" || field.control === "port") {
return e("div", { className: "file-workbench-number-control" },
e("input", { type: "range", value, min: field.minimum, max: field.maximum, disabled: !editable, "aria-label": `${field.label}滑动输入`, onChange: (event: { target: { value: string } }) => onChange(event.target.value) }),
e("input", { type: "number", value, min: field.minimum, max: field.maximum, readOnly: !editable, "aria-label": field.label, onChange: (event: { target: { value: string } }) => onChange(event.target.value) })
);
}
return e("input", { type: "text", value, readOnly: !editable, "aria-label": field.label, onChange: (event: { target: { value: string } }) => onChange(event.target.value) });
}
function rawFileView(e: ReactLike["createElement"], file: SCUMLogicalFile, currentContent: string, rawDraft: Record<string, string>, setRawDraft: StateSetter<Record<string, string>>, snapshot: SCUMFileReadSnapshot | null, canFilesWrite: boolean, canWriteFile: boolean, preview: PreviewState, onPreview: (mode: ConfigMode, content: string, current: string) => void, onWrite: () => void) {
if (snapshot?.state !== "ready") {
const message = snapshot?.reason ?? (file.kind === "config" ? "尚未读取此配置文件的受控原文。" : "尚未读取此日志文件的受控内容。");
return e("div", { className: "file-workbench-raw" }, e("p", { className: "page-status" }, message), snapshot?.jobId ? e("small", null, `读取任务 ${snapshot.jobId}`) : null);
}
const rawValue = rawDraft[file.key] ?? currentContent;
const editable = file.kind === "config" && canFilesWrite && file.editable !== false;
if (file.kind === "log") {
return e("div", { className: "file-workbench-raw" }, e("pre", { className: "runtime-task-log" }, rawValue), e("p", { className: "page-status" }, `日志原文只读 · ${contentLineCount(rawValue)} 行 · ${snapshot.sizeBytes ?? 0} B`));
}
return e("div", { className: "file-workbench-raw" },
e("textarea", { className: "file-workbench-raw-editor", value: rawValue, readOnly: !editable, spellCheck: false, "aria-label": `${file.label} 原文模式编辑`, onChange: (event: { target: { value: string } }) => setRawDraft((current) => ({ ...current, [file.key]: event.target.value })) }),
e("div", { className: "console-row-actions file-workbench-actions" },
e("button", { type: "button", className: "icon-command", disabled: !editable || rawValue === currentContent, onClick: () => onPreview("source", rawValue, currentContent) }, "预览改动"),
e("button", { type: "button", className: "primary-command", disabled: !editable || !canWriteFile || !preview || preview.mode !== "source", onClick: onWrite }, "提交写入")
),
preview && preview.mode === "source" ? diffPreview(e, preview) : null,
e("p", { className: "page-status" }, editable ? "原文模式会整文件写入声明 file key;提交前请先预览差异。" : "原文配置当前只读。")
function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input: SCUMPageContext, setAction: StateSetter<ActionState>, refresh: () => void) {
return e("div", { className: "console-record-list" },
statsStrip(e, [["可选玩家", data.players.length], ["发放操作", data.operations.filter((op) => textField(op, "TemplateKey", "templateKey") === "reward.deliver").length], ["未知态", data.operations.filter((op) => field(op, "Status", "status") === "unknown").length]]),
e("p", { className: "page-status" }, "礼包只创建 typed delivery workflow;确认结果未知时不会重复发放。"),
data.players.slice(0, 40).map((player) => e("article", { key: idOf(player), className: "console-record" },
e("div", { className: "console-record-head" }, e("strong", null, textField(player, "DisplayName", "displayName") || idOf(player)), e("span", { className: "status-pill status-disabled" }, freshness(player))),
e("div", { className: "console-row-actions" }, operationButton(e, input, setAction, refresh, player, "reward.deliver", "rewardKey", "创建礼包发放", "starter-pack"), operationButton(e, input, setAction, refresh, player, "player.notify", "message", "发送通知", "你的礼包正在审核发放。"))
))
);
}
function diffPreview(e: ReactLike["createElement"], preview: Exclude<PreviewState, null>) {
return e("section", { className: "file-workbench-diff", "aria-label": "文件改动预览" },
e("div", { className: "console-row" }, e("span", null, "差异预览"), e("strong", null, preview.summary)),
e("pre", { className: "runtime-task-log" }, preview.lines.map((line) => `${line.kind === "added" ? "+" : line.kind === "removed" ? "-" : " "} ${line.text}`).join("\n"))
function workflowsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData) {
return e("div", { className: "console-record-list" },
data.workflows.length ? data.workflows.map((wf) => e("article", { key: idOf(wf), className: "console-record" },
e("div", { className: "console-record-head" }, e("strong", null, textField(wf, "TemplateKey", "templateKey") || idOf(wf)), e("span", { className: "status-pill status-active" }, textField(wf, "Status", "status") || "queued")),
e("div", { className: "console-record-meta" }, e("span", null, `当前步骤 ${textField(wf, "CurrentStepKey", "currentStepKey") || "等待调度"}`), e("span", null, `创建 ${dateField(wf, "CreatedAt", "createdAt")}`)),
e("span", { className: "provider-id" }, summaryText(wf))
)) : e("p", { className: "page-status" }, "暂无 workflow。可以从各页面发起 refresh/audit/correction/gift workflow。"),
tablePanel(e, "步骤", data.steps, (step) => [textField(step, "StepKey", "stepKey"), textField(step, "Status", "status"), textField(step, "Capability", "capability") || textField(step, "QueryTemplateKey", "queryTemplateKey") || textField(step, "OperationKey", "operationKey"), summaryText(step)])
);
}
function usePluginState<T>(react: ReactLike, initialState: T): [T, StateSetter<T>] {
return react.useState ? react.useState(initialState) : [initialState, () => undefined];
function workflowButton(e: ReactLike["createElement"], input: SCUMPageContext, setAction: StateSetter<ActionState>, refresh: () => void, templateKey: string) {
if (!templateKey) return null;
return e("button", { type: "button", className: "primary-command", disabled: !input.workspaceActions?.createSCUMWorkflow, onClick: () => createWorkflow(input, setAction, refresh, templateKey) }, workflowLabel(templateKey));
}
function normalizeWorkspace(workspace?: SCUMFeatureWorkspace): NormalizedWorkspace {
const directories = workspace?.directories ?? [];
const directoryKeys = new Set(directories.map((directory) => directory.key));
const files = (workspace?.files ?? []).filter((file) => directoryKeys.has(file.directoryKey));
const fileKeys = new Set(files.map((file) => file.key));
const configFields = (workspace?.configFields ?? []).filter((field) => fileKeys.has(field.fileKey));
return { defaultDirectoryKey: workspace?.defaultDirectoryKey && directories.some((directory) => directory.key === workspace.defaultDirectoryKey) ? workspace.defaultDirectoryKey : directories[0]?.key ?? "", directories, files, configFields };
function operationButton(e: ReactLike["createElement"], input: SCUMPageContext, setAction: StateSetter<ActionState>, refresh: () => void, player: RecordMap, templateKey: string, valueKey: string, label: string, value: unknown, guarded = false) {
return e("button", { type: "button", className: "icon-command", disabled: !input.workspaceActions?.createSCUMOperation, onClick: () => createOperation(input, setAction, refresh, player, templateKey, valueKey, value, guarded) }, label);
}
function initialFileKey(workspace: NormalizedWorkspace, directoryKey: string): string {
return workspace.files.find((file) => file.directoryKey === directoryKey && file.kind === "config")?.key ?? workspace.files.find((file) => file.directoryKey === directoryKey)?.key ?? workspace.files[0]?.key ?? "";
function createWorkflow(input: SCUMPageContext, setAction: StateSetter<ActionState>, refresh: () => void, templateKey: string) {
setAction({ status: "pending", message: `正在创建 ${templateKey} workflow…` });
void input.workspaceActions?.createSCUMWorkflow?.({ templateKey, idempotencyKey: `plugin:${templateKey}:${input.serverInstanceId}:${Date.now()}` }).then((result) => {
setAction({ status: "ok", message: `Workflow 已创建:${textField(result as RecordMap, "id") || templateKey}` }); refresh();
}).catch((error) => setAction({ status: "error", message: error instanceof Error ? error.message : "Workflow 创建失败。" }));
}
function directoryScopeLabel(scope: SCUMLogicalDirectory["scope"]): string {
return scope === "logs" ? "日志声明" : "配置声明";
function createOperation(input: SCUMPageContext, setAction: StateSetter<ActionState>, refresh: () => void, player: RecordMap, templateKey: string, valueKey: string, value: unknown, guarded: boolean) {
const playerId = textField(player, "GamePlayerID", "gamePlayerId") || textField(player, "SteamID", "steamId");
const before = guarded ? field(field(player, "UnknownFields", "unknownFields") as RecordMap | undefined, "855") ?? 0 : undefined;
const payload: RecordMap = guarded ? { fieldKey: "855", before, after: value, safetyWindow: `plugin-maintenance-${Date.now()}`, backupRef: `backup-required:${Date.now()}` } : { [valueKey]: value };
setAction({ status: "pending", message: `正在创建 ${templateKey} typed operation…` });
void input.workspaceActions?.createSCUMOperation?.({ templateKey, playerId, payload, reason: "SCUM plugin projection surface request", idempotencyKey: `plugin:${templateKey}:${playerId}:${Date.now()}` }).then((result) => {
setAction({ status: "ok", message: `操作已进入审批/确认队列:${textField(result as RecordMap, "id") || templateKey}` }); refresh();
}).catch((error) => setAction({ status: "error", message: error instanceof Error ? error.message : "操作创建失败。" }));
}
function modeledPairText(fields: readonly SCUMConfigField[], values: Record<string, string>): string {
return fields.map((field) => `${field.configKey}=${values[field.configKey] ?? field.defaultValue}`).join("\n");
}
function parseModeledPairDraft(content: string, fields: readonly SCUMConfigField[]): { changes: Record<string, string>; unknown: string[] } {
const byConfigKey = new Map(fields.map((field) => [field.configKey, field]));
const changes: Record<string, string> = {};
const unknown: string[] = [];
for (const line of content.split("\n")) {
const parsed = parseIniAssignment(line);
if (!parsed) continue;
const field = byConfigKey.get(parsed.key);
if (!field) {
unknown.push(parsed.key);
continue;
}
changes[field.key] = parsed.value;
function pageWorkflow(pageKey: string): string {
switch (pageKey) {
case "players": return "scum.player-refresh";
case "squads": return "scum.territory-audit";
case "live-map": return "scum.world-refresh";
case "gifts": return "scum.gift-delivery";
case "workflows": return "scum.product-cleanup";
default: return "scum.bootstrap-real-data";
}
return { changes, unknown: [...new Set(unknown)] };
}
function controlLabel(field: SCUMConfigField): string {
const range = field.minimum !== undefined || field.maximum !== undefined ? `范围 ${field.minimum ?? "不限"}-${field.maximum ?? "不限"}` : "";
const label = field.control === "boolean" ? "是/否选择" : field.control === "number" || field.control === "port" ? "滑动输入" : "文本填空";
return [label, range].filter(Boolean).join(" · ");
}
function surfaceTitle(pageKey: string): string { return pageKey === "squads" ? "队伍/旗帜管理" : pageKey === "live-map" ? "实时地图" : pageKey === "gifts" ? "礼包管理" : pageKey === "workflows" ? "Workflow 状态" : "用户管理"; }
function surfaceSummary(pageKey: string): string { return pageKey === "live-map" ? "玩家、载具、旗帜坐标来自平台本地投影;缺失时显示 stale/unknown。" : pageKey === "gifts" ? "礼包发放、通知和确认都通过 typed workflow,不直接改投影。" : pageKey === "squads" ? "队伍、成员、旗帜所有权来自 SCUM.db typed observations。" : "玩家列表由登录日志和 SCUM.db typed observations 创建,不显示样例数据。"; }
function workflowLabel(templateKey: string): string { return templateKey.includes("audit") ? "发起审计" : templateKey.includes("gift") ? "创建发放 workflow" : templateKey.includes("world") ? "刷新世界投影" : templateKey.includes("cleanup") ? "清理旧入口" : "刷新真实数据"; }
function normalizeBoolean(value: string): boolean {
return /^(true|1|yes|on)$/i.test(String(value).trim());
}
function parseIniAssignments(content: string): Record<string, string> {
const values: Record<string, string> = {};
for (const line of content.split("\n")) {
const parsed = parseIniAssignment(line);
if (parsed) values[parsed.key] = parsed.value;
}
return values;
}
function unknownIniAssignments(content: string, fields: readonly SCUMConfigField[]): Array<{ key: string; value: string; index: number }> {
const known = new Set(fields.map((field) => field.configKey));
const unknown: Array<{ key: string; value: string; index: number }> = [];
content.split("\n").forEach((line, index) => {
const parsed = parseIniAssignment(line);
if (parsed && !known.has(parsed.key)) unknown.push({ ...parsed, index });
});
return unknown;
}
function parseIniAssignment(line: string): { key: string; value: string } | null {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";") || trimmed.startsWith("[")) return null;
const separator = line.indexOf("=");
if (separator < 1) return null;
return { key: line.slice(0, separator).trim(), value: line.slice(separator + 1).trim() };
}
function composeIniContent(content: string, fields: readonly SCUMConfigField[], draft: Record<string, string>): string {
const byField = new Map(fields.map((field) => [field.key, field]));
const changes = new Map<string, string>();
for (const [fieldKey, value] of Object.entries(draft)) {
const field = byField.get(fieldKey);
if (field) changes.set(field.configKey, value);
}
if (changes.size === 0) return content;
const applied = new Set<string>();
const lines = content.split("\n").map((line) => {
const parsed = parseIniAssignment(line);
if (!parsed || !changes.has(parsed.key)) return line;
applied.add(parsed.key);
return `${line.slice(0, line.indexOf("=") + 1)}${changes.get(parsed.key) ?? ""}`;
});
for (const [key, value] of changes) {
if (!applied.has(key)) lines.push(`${key}=${value}`);
}
return content.endsWith("\n") ? lines.join("\n") : lines.join("\n").replace(/\n$/, "");
}
function decodeRawContent(content: string, encoding: RawEncoding): string {
if (encoding === "utf-8") return content;
const bytes = Uint8Array.from(Array.from(content), (char) => char.charCodeAt(0) & 0xff);
if (typeof TextDecoder !== "undefined") return new TextDecoder("utf-16le").decode(bytes);
let decoded = "";
for (let index = 0; index < bytes.length; index += 2) decoded += String.fromCharCode(bytes[index] | ((bytes[index + 1] ?? 0) << 8));
return decoded;
}
function buildSimpleDiff(current: string, proposed: string): { summary: string; lines: readonly DiffLine[] } {
const currentLines = current.split("\n");
const proposedLines = proposed.split("\n");
const max = Math.max(currentLines.length, proposedLines.length);
const lines: DiffLine[] = [];
let added = 0;
let removed = 0;
for (let index = 0; index < max; index += 1) {
const before = currentLines[index];
const after = proposedLines[index];
if (before === after) {
if (before !== undefined) lines.push({ kind: "same", text: before });
continue;
}
if (before !== undefined) { removed += 1; lines.push({ kind: "removed", text: before }); }
if (after !== undefined) { added += 1; lines.push({ kind: "added", text: after }); }
}
return { summary: `+${added} / -${removed} 行变更`, lines };
}
function contentLineCount(content: string): number {
return content ? content.split("\n").length : 0;
}
function statsStrip(e: ReactLike["createElement"], items: Array<[string, number]>) { return e("div", { className: "console-stat-strip" }, items.map(([label, value]) => e("span", { key: label, className: "server-card-stat" }, e("span", null, label), e("strong", null, String(value))))); }
function tablePanel(e: ReactLike["createElement"], title: string, rows: RecordMap[], render: (row: RecordMap) => unknown[]) { return e("article", { className: "console-module" }, e("div", { className: "panel-header" }, e("h2", null, title), e("span", { className: "page-status" }, `${rows.length}`)), e("div", { className: "console-row-list" }, rows.length ? rows.slice(0, 100).map((row) => e("div", { key: idOf(row), className: "console-row" }, render(row).map((part, i) => i === 0 ? e("span", { key: i }, String(part ?? "unknown")) : e("strong", { key: i }, String(part ?? "unknown"))))) : e("p", { className: "page-status" }, "暂无真实投影数据。"))); }
function dotStyle(point: RecordMap): Record<string, string> { const x = Number(field(point, "X", "x") ?? 0); const y = Number(field(point, "Y", "y") ?? 0); return { left: `${Math.max(2, Math.min(98, 50 + x / 10000))}%`, top: `${Math.max(2, Math.min(98, 50 - y / 10000))}%` }; }
function safeList(fn?: () => Promise<unknown>): Promise<RecordMap[]> { return fn ? fn().then((value) => Array.isArray((value as RecordMap)?.items) ? (value as { items: RecordMap[] }).items : []) : Promise.resolve([]); }
function usePluginState<T>(react: ReactLike, initial: T): [T, StateSetter<T>] { return react.useState ? react.useState<T>(initial) : [initial, () => undefined]; }
function field(row: RecordMap | undefined, ...keys: string[]): unknown { if (!row) return undefined; for (const key of keys) if (row[key] !== undefined) return row[key]; return undefined; }
function textField(row: RecordMap | unknown, ...keys: string[]): string { const value = field(row as RecordMap, ...keys); return value === undefined || value === null ? "" : String(value); }
function boolField(row: RecordMap, ...keys: string[]): boolean { const value = field(row, ...keys); return value === true || value === "true"; }
function numField(row: RecordMap, ...keys: string[]): string { const value = field(row, ...keys); return value === undefined || value === null || value === "" ? "--" : String(value); }
function idOf(row: RecordMap): string { return textField(row, "ID", "id", "GamePlayerID", "gamePlayerId", "SquadID", "squadId", "VehicleID", "vehicleId", "FlagID", "flagId", "StepKey", "stepKey") || Math.random().toString(36).slice(2); }
function freshness(row: RecordMap): string { const fresh = field(row, "Freshness", "freshness") as RecordMap | undefined; return textField(fresh, "Status", "status") || "unknown"; }
function coords(row?: RecordMap): string { if (!row) return "坐标 unknown"; const ok = field(row, "HasCoordinates", "hasCoordinates"); return ok === false ? "坐标 unknown" : `X ${numField(row, "X", "x")} / Y ${numField(row, "Y", "y")} / Z ${numField(row, "Z", "z")}`; }
function summaryText(row: RecordMap): string { const summary = field(row, "SafeSummary", "safeSummary") as RecordMap | undefined; return textField(summary, "Message", "message") || textField(row, "BlockerReason", "blockerReason") || "safe summary pending"; }
function dateField(row: RecordMap, ...keys: string[]): string { const value = textField(row, ...keys); return value ? new Date(value).toLocaleString() : "unknown"; }