feat(scum): add local game player intelligence
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import source from "./GamePlayerIntelligencePanel.tsx?raw";
|
||||
|
||||
describe("GamePlayerIntelligencePanel", () => {
|
||||
it("renders reviewable labels without raw network material or enforcement controls", () => {
|
||||
expect(source).toContain("需要人工审核");
|
||||
expect(source).toContain("失败尝试");
|
||||
expect(source).not.toContain("networkFingerprint");
|
||||
expect(source).not.toMatch(/\bban\b|\bkick\b/i);
|
||||
});
|
||||
});
|
||||
@@ -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 }); }
|
||||
Reference in New Issue
Block a user