feat(scum): add local game player intelligence

This commit is contained in:
npc0-hue
2026-07-28 15:23:09 +08:00
parent c5806cc6b7
commit 73947edcee
26 changed files with 1209 additions and 65 deletions
@@ -0,0 +1,20 @@
import { RefreshCw, ShieldAlert, UsersRound } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { platformApiClient } from "../api/client";
import type { GamePlayerProfileResponse, GamePlayerResponse } from "../api/types";
import { ErrorState, LoadingState } from "./StateViews";
type State = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; players: GamePlayerResponse[]; selected?: GamePlayerProfileResponse };
export function GamePlayerIntelligencePanel({ serverInstanceId }: { serverInstanceId: string }) {
const [state, setState] = useState<State>({ status: "loading" });
const load = useCallback(async () => { setState({ status: "loading" }); try { const result = await platformApiClient.listGamePlayers(serverInstanceId); setState({ status: "ready", players: result.items }); } catch (error) { setState({ status: "error", reason: error instanceof Error ? error.message : "玩家档案读取失败。" }); } }, [serverInstanceId]);
useEffect(() => { void load(); }, [load]);
async function select(player: GamePlayerResponse) { try { const selected = await platformApiClient.getGamePlayerProfile(serverInstanceId, player.id); setState((current) => current.status === "ready" ? { ...current, selected } : current); } catch (error) { setState({ status: "error", reason: error instanceof Error ? error.message : "玩家档案读取失败。" }); } }
if (state.status === "loading") return <LoadingState label="正在加载本地游戏玩家档案…" />;
if (state.status === "error") return <ErrorState title="玩家档案不可用" reason={state.reason} onRetry={() => void load()} />;
const profile = state.selected;
return <section className="console-panel" aria-label="SCUM 本地玩家档案与风险信号"><div className="panel-header"><div><h2><UsersRound size={16} /> </h2><p className="provider-id"></p></div><button type="button" className="icon-command" onClick={() => void load()}><RefreshCw size={14} /><span></span></button></div><div className="console-record-list">{state.players.length ? state.players.map((player) => <button type="button" className="console-record" key={player.id} onClick={() => void select(player)}><strong>{player.displayName}</strong><span> ID{player.gamePlayerId}</span><small>{formatTime(player.lastSeenAt)}</small></button>) : <p className="page-status"></p>}</div>{profile && <div className="console-module"><div className="panel-header"><h3>{profile.player.displayName} </h3><span className="page-status"> {profile.aliases.length} · {profile.sessions.length}</span></div><div className="console-row-list"><div className="console-row"><strong></strong><span>{profile.aliases.map((alias) => alias.alias).join(" / ") || "无"}</span></div>{profile.sessions.slice(0, 10).map((session) => <div className="console-row" key={session.id}><strong></strong><span>{formatTime(session.startedAt)} {session.endedAt ? formatTime(session.endedAt) : "仍在线"} {session.endReason ? `(${session.endReason})` : ""}</span></div>)}</div><div className="console-record-list">{profile.securitySignals.length ? profile.securitySignals.map((signal) => <div className="console-record" key={signal.id}><ShieldAlert size={15} aria-hidden="true" /><strong>{signal.ruleKey}</strong><span>{signal.status === "review-required" ? "需要人工审核" : signal.status}</span><small>{signal.summary} · {signal.evidenceCount} </small></div>) : <p className="page-status"></p>}</div>{profile.accessAttempts.length > 0 && <p className="page-status">{profile.accessAttempts.length} </p>}</div>}</section>;
}
function formatTime(value: string) { const date = new Date(value); return Number.isNaN(date.valueOf()) ? "--" : date.toLocaleString("zh-CN", { hour12: false }); }