From 03339fb3e8cdd0bc633c05c1d81141d38c6dd1b7 Mon Sep 17 00:00:00 2001 From: npc0-hue Date: Wed, 29 Jul 2026 16:26:19 +0800 Subject: [PATCH] refactor(scum): remove orphaned host panels --- .../implementation-blockers.md | 30 ++ .../tasks.md | 1 + platform_web/api/client.ts | 37 -- platform_web/api/types.ts | 29 -- .../components/GameGiftCatalogPanel.test.tsx | 12 - .../components/GameGiftCatalogPanel.tsx | 25 - .../GamePlayerIntelligencePanel.test.tsx | 14 - .../GamePlayerIntelligencePanel.tsx | 33 -- .../components/ScumFileConfigWorkbench.tsx | 46 -- .../components/ScumMapTrajectoryPanel.test.ts | 9 - .../components/ScumMapTrajectoryPanel.tsx | 22 - .../components/ScumOperationsPanel.test.tsx | 101 ---- .../components/ScumOperationsPanel.tsx | 436 ------------------ platform_web/contracts/scumOperations.test.ts | 70 --- platform_web/contracts/scumOperations.ts | 146 ------ platform_web/schemas/scumOperations.test.ts | 56 --- platform_web/schemas/scumOperations.ts | 148 ------ platform_web/theme/base.css | 1 - 18 files changed, 31 insertions(+), 1185 deletions(-) delete mode 100644 platform_web/components/GameGiftCatalogPanel.test.tsx delete mode 100644 platform_web/components/GameGiftCatalogPanel.tsx delete mode 100644 platform_web/components/GamePlayerIntelligencePanel.test.tsx delete mode 100644 platform_web/components/GamePlayerIntelligencePanel.tsx delete mode 100644 platform_web/components/ScumFileConfigWorkbench.tsx delete mode 100644 platform_web/components/ScumMapTrajectoryPanel.test.ts delete mode 100644 platform_web/components/ScumMapTrajectoryPanel.tsx delete mode 100644 platform_web/components/ScumOperationsPanel.test.tsx delete mode 100644 platform_web/components/ScumOperationsPanel.tsx delete mode 100644 platform_web/contracts/scumOperations.test.ts delete mode 100644 platform_web/contracts/scumOperations.ts delete mode 100644 platform_web/schemas/scumOperations.test.ts delete mode 100644 platform_web/schemas/scumOperations.ts diff --git a/openspec/changes/move-scum-feature-ownership-to-plugin/implementation-blockers.md b/openspec/changes/move-scum-feature-ownership-to-plugin/implementation-blockers.md index 12c4b24..c1223d7 100644 --- a/openspec/changes/move-scum-feature-ownership-to-plugin/implementation-blockers.md +++ b/openspec/changes/move-scum-feature-ownership-to-plugin/implementation-blockers.md @@ -41,3 +41,33 @@ Consequently these tasks remain open and blocked rather than simulated: No fallback to raw RCON, credentials, SQL, direct game-database access, OCR, screenshots, keyboard/mouse injection, or desktop automation is permitted. + +## 6.3 frontend cleanup and rollback evidence (2026-07-29) + +The generic `PluginPageHostPage` has no hard-coded SCUM import or `game.scum` +branch. Static `rg` call-graph audit found zero non-test callers for the +following former `platform_web` implementation, so this batch removed it with +its private tests, contracts, schemas, API-client methods/types, and map-only +styles: + +- `ScumOperationsPanel`, `ScumFileConfigWorkbench`, and the private + `scumOperations` contract/schema; +- `GamePlayerIntelligencePanel` and `GameGiftCatalogPanel`; +- `ScumMapTrajectoryPanel`. + +Their plugin-owned replacement is `features/page.ts`, with typed bridge reads +and commands in `features/api.ts`, and the read-only provenance adapters in +`features/migration.ts`. The replacement remains capability-gated: missing +login/position event producers and missing state-patch/reward-delivery +handlers render unavailable controls and never synthesize records or enable a +fallback execution path. + +The platform-side `/game-players`, `/game-gifts`, and `/game-map-trajectories` +routes, DTO/domain/model/service/repository implementations, and their +fixtures remain intentionally. `plugins/tests/fixtures/scum-migration-parity.ts` +continues to consume their historical record shapes through allowlisted, +`transitional-read-only` migration records. Rollback therefore consists of +disabling the exact server/version plugin authority flag, leaving those prior +records visible and read-only; it does not restore any deleted host panel or +re-enable an unsafe execution route. Backend removal remains blocked until 6.2 +parity and the required versioned event/operation protocols exist. diff --git a/openspec/changes/move-scum-feature-ownership-to-plugin/tasks.md b/openspec/changes/move-scum-feature-ownership-to-plugin/tasks.md index b7b0c98..f607ffd 100644 --- a/openspec/changes/move-scum-feature-ownership-to-plugin/tasks.md +++ b/openspec/changes/move-scum-feature-ownership-to-plugin/tasks.md @@ -45,3 +45,4 @@ ## Verification evidence - 5.6: `cd plugins/examples/scum-server-plugin/companion && go test ./...` passed on 2026-07-29. The isolated typed-port fixture covers safe configuration read/patch, `player.notify`, and version-bound `vehicle.spawn` through claim/ack/registry/complete; it checks approval, binding, version/capability gating, idempotency, bounded outcomes, and redaction. A production-source test rejects raw SQL/direct database access, unrestricted command/RCON execution, OCR/screenshot/input automation, and direct socket paths. +- 6.3 preparation: `platform_web` static call-graph audit on 2026-07-29 found that the legacy SCUM operation/config/player/gift/trajectory panels, their private contracts/schemas, and their game-player/gift/map client calls had no non-test caller after generic plugin-page hosting. The orphan frontend implementation was removed; the backend historical routes/models/services/repositories remain read-only migration input as documented in `implementation-blockers.md`. `cd platform_web && pnpm test && pnpm build`, `cd plugins && pnpm test && pnpm typecheck && pnpm validate:manifest`, `openspec validate move-scum-feature-ownership-to-plugin --strict`, and `scripts/check-structure.sh` passed. diff --git a/platform_web/api/client.ts b/platform_web/api/client.ts index e001e66..83a1620 100644 --- a/platform_web/api/client.ts +++ b/platform_web/api/client.ts @@ -48,21 +48,6 @@ import type { GameClientBridgeSnapshotQuery, GameClientBridgeStatusResponse, GamePluginListResponse, - GamePlayerListResponse, - GamePlayerProfileResponse, - GamePlayerStatePatchListResponse, - GamePlayerStatePatchRequest, - GamePlayerStatePatchResponse, - GamePlayerStateResponse, - GameMapTrajectoryResponse, - GameGiftCatalogListResponse, - GameGiftCatalogRequest, - GameGiftCatalogResponse, - GameGiftRevisionListResponse, - GameGiftRevisionResponse, - GameGiftGrantListResponse, - GameGiftGrantRequest, - GameGiftGrantResponse, HealthResponse, JobCreateRequest, JobListResponse, @@ -589,28 +574,6 @@ export class PlatformApiClient { return this.request(`/backups?serverInstanceId=${encodeURIComponent(serverInstanceId)}`); } - async listGamePlayers(serverInstanceId: string, search = ""): Promise { - const query = search ? `?search=${encodeURIComponent(search)}` : ""; - return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-players${query}`); - } - - async getGamePlayerProfile(serverInstanceId: string, playerId: string): Promise { - return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-players/${encodeURIComponent(playerId)}`); - } - async getGameMapTrajectories(serverInstanceId: string, filter: { from?: string; to?: string; playerIds?: string[]; vehicleIds?: string[] } = {}): Promise { const params = new URLSearchParams(); if (filter.from) params.set("from", filter.from); if (filter.to) params.set("to", filter.to); if (filter.playerIds?.length) params.set("playerId", filter.playerIds.join(",")); if (filter.vehicleIds?.length) params.set("vehicleId", filter.vehicleIds.join(",")); const query = params.toString(); return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-map-trajectories${query ? `?${query}` : ""}`); } - - async getGamePlayerState(serverInstanceId: string, playerId: string): Promise { return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-players/${encodeURIComponent(playerId)}/state`); } - async listGamePlayerStatePatches(serverInstanceId: string, playerId: string): Promise { return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-players/${encodeURIComponent(playerId)}/state-patches`); } - async requestGamePlayerStatePatch(serverInstanceId: string, playerId: string, request: GamePlayerStatePatchRequest): Promise { return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-players/${encodeURIComponent(playerId)}/state-patches`, { method: "POST", body: request }); } - async approveGamePlayerStatePatch(serverInstanceId: string, playerId: string, patchId: string): Promise { return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-players/${encodeURIComponent(playerId)}/state-patches/${encodeURIComponent(patchId)}/approve`, { method: "POST" }); } - async listGameGiftCatalogs(serverInstanceId: string): Promise { return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-gifts`); } - async saveGameGiftCatalog(serverInstanceId: string, request: GameGiftCatalogRequest): Promise { return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-gifts`, { method: "POST", body: request }); } - async publishGameGiftCatalog(serverInstanceId: string, catalogId: string): Promise { return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-gifts/${encodeURIComponent(catalogId)}/publish`, { method: "POST" }); } - async listGameGiftRevisions(serverInstanceId: string, catalogId: string): Promise { return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-gifts/${encodeURIComponent(catalogId)}/revisions`); } - async listGameGiftGrants(serverInstanceId: string): Promise { return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-gift-grants`); } - async requestGameGiftGrant(serverInstanceId: string, request: GameGiftGrantRequest): Promise { return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-gift-grants`, { method: "POST", body: request }); } - async approveGameGiftGrant(serverInstanceId: string, grantId: string): Promise { return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-gift-grants/${encodeURIComponent(grantId)}/approve`, { method: "POST" }); } - async getBackup(id: string): Promise { return this.request(`/backups/${encodeURIComponent(id)}`); } diff --git a/platform_web/api/types.ts b/platform_web/api/types.ts index 519adc0..7b4fd6a 100644 --- a/platform_web/api/types.ts +++ b/platform_web/api/types.ts @@ -310,35 +310,6 @@ export interface RuntimeLogEventResponse { retentionDays: number; severity: "info" | "notice" | "warning" | "critical"; } -export interface GamePlayerResponse { id: string; serverInstanceId: string; gamePlayerId: string; displayName: string; firstSeenAt: string; lastSeenAt: string; } -export interface GamePlayerAliasResponse { alias: string; firstSeenAt: string; lastSeenAt: string; } -export interface GamePlayerSessionResponse { id: string; sourceSessionId: string; startedAt: string; endedAt?: string; endReason?: string; } -export interface GameAccessAttemptResponse { id: string; occurredAt: string; outcome: string; reason: string; } -export interface GameSecuritySignalResponse { id: string; ruleKey: string; status: "open" | "review-required" | "expired"; evidenceCount: number; summary: string; firstObservedAt: string; lastObservedAt: string; } -export interface GamePlayerProfileResponse { player: GamePlayerResponse; aliases: GamePlayerAliasResponse[]; sessions: GamePlayerSessionResponse[]; accessAttempts: GameAccessAttemptResponse[]; securitySignals: GameSecuritySignalResponse[]; } -export interface GamePlayerListResponse { items: GamePlayerResponse[]; } -export interface GamePlayerStateFieldResponse { key: string; label: string; kind: "skill" | "attribute"; minimum: number; maximum: number; value: number; } -export interface GamePlayerStateResponse { gameVersion: string; stateVersion: string; safetyWindow?: string; maintenanceVerified: boolean; playerOnline: boolean; supported: boolean; fields: GamePlayerStateFieldResponse[]; observedAt: string; } -export interface GamePlayerStatePatchChangeRequest { fieldKey: string; before: number; after: number; } -export interface GamePlayerStatePatchRequest { gameVersion: string; expectedStateVersion: string; safetyWindow: string; changes: GamePlayerStatePatchChangeRequest[]; reason: string; } -export interface GamePlayerStatePatchResponse { id: string; gameVersion: string; expectedStateVersion: string; changes: GamePlayerStatePatchChangeRequest[]; reason: string; requesterId: string; approverId?: string; status: "pending-approval" | "queued" | "execution-failed" | "execution-unknown" | "confirmation-failed" | "confirmed"; bridgeCommandId?: string; executionSummary?: string; confirmedStateVersion?: string; createdAt: string; approvedAt?: string; completedAt?: string; } -export interface GamePlayerStatePatchListResponse { items: GamePlayerStatePatchResponse[]; } -export interface GameMapTrajectoryPointResponse { mapX: number; mapY: number; occurredAt: string; collectedAt: string; source: "companion" | "log-projection"; } -export interface GameMapTrajectoryEntityResponse { kind: "player" | "vehicle"; entityId: string; gamePlayerRecordId?: string; label: string; points: GameMapTrajectoryPointResponse[]; collectedAt?: string; sources: string[]; } -export interface GameMapTrajectorySegmentResponse { gamePlayerRecordId: string; vehicleId: string; startedAt: string; endedAt?: string; } -export interface GameMapTrajectoryMapResponse { mapId: string; mapVersion: string; imageWidth: number; imageHeight: number; precision: number; } -export interface GameMapTrajectoryResponse { status: "ready" | "empty" | "missing-map"; reason?: string; map?: GameMapTrajectoryMapResponse; from?: string; to?: string; players: GameMapTrajectoryEntityResponse[]; vehicles: GameMapTrajectoryEntityResponse[]; rideSegments: GameMapTrajectorySegmentResponse[]; } -export interface GameGiftItemRequest { catalogItemKey: string; quantity: number; } -export interface GameGiftItemResponse extends GameGiftItemRequest { label: string; } -export interface GameGiftCatalogRequest { id?: string; name: string; gameVersion: string; items: GameGiftItemRequest[]; } -export interface GameGiftCatalogResponse { id: string; name: string; gameVersion: string; draftItems: GameGiftItemResponse[]; latestRevisionId?: string; updatedAt: string; } -export interface GameGiftCatalogListResponse { items: GameGiftCatalogResponse[]; } -export interface GameGiftRevisionResponse { id: string; catalogId: string; revision: number; gameVersion: string; items: GameGiftItemResponse[]; publishedBy: string; publishedAt: string; } -export interface GameGiftRevisionListResponse { items: GameGiftRevisionResponse[]; } -export interface GameGiftGrantRequest { revisionId: string; gamePlayerRecordId: string; notice: string; idempotencyKey: string; } -export interface GameGiftGrantResponse { id: string; revisionId: string; revisionNumber: number; gameVersion: string; items: GameGiftItemResponse[]; gamePlayerRecordId: string; playerDisplayName: string; notice: string; requesterId: string; approverId?: string; status: "pending-approval" | "queued" | "delivered" | "notification_failed" | "failed" | "unknown"; deliverySummary?: string; notificationSummary?: string; createdAt: string; approvedAt?: string; completedAt?: string; } -export interface GameGiftGrantListResponse { items: GameGiftGrantResponse[]; } - export interface RuntimeTransportProfileResponse { key: string; kind: string; diff --git a/platform_web/components/GameGiftCatalogPanel.test.tsx b/platform_web/components/GameGiftCatalogPanel.test.tsx deleted file mode 100644 index 5ce55f4..0000000 --- a/platform_web/components/GameGiftCatalogPanel.test.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, expect, it } from "vitest"; -import source from "./GameGiftCatalogPanel.tsx?raw"; - -describe("GameGiftCatalogPanel", () => { - it("keeps lifecycle state readable and avoids raw game command surfaces", () => { - expect(source).toContain("版本化礼包目录与定向发放"); - expect(source).toContain("通知失败"); - expect(source).toContain("绝不自动重试"); - expect(source).not.toContain("rawCommand"); - expect(source).not.toContain("sendSourceRCONCommand"); - }); -}); diff --git a/platform_web/components/GameGiftCatalogPanel.tsx b/platform_web/components/GameGiftCatalogPanel.tsx deleted file mode 100644 index 27c5987..0000000 --- a/platform_web/components/GameGiftCatalogPanel.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { CheckCircle2, Gift, RefreshCw, Send } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; -import { platformApiClient } from "../api/client"; -import type { GameGiftCatalogResponse, GameGiftGrantResponse, GameGiftRevisionResponse, GamePlayerResponse } from "../api/types"; -import { ErrorState, LoadingState } from "./StateViews"; - -type Ready = { catalogs: GameGiftCatalogResponse[]; revisions: GameGiftRevisionResponse[]; grants: GameGiftGrantResponse[]; players: GamePlayerResponse[]; selectedCatalog?: string; selectedRevision?: string; selectedPlayer?: string; name: string; notice: string; item: "bandage" | "water-bottle" | "improvised-spear"; quantity: number; note?: string }; -type State = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; value: Ready }; -const itemLabels = { bandage: "绷带", "water-bottle": "饮用水", "improvised-spear": "简易长矛" } as const; - -export function GameGiftCatalogPanel({ serverInstanceId }: { serverInstanceId: string }) { - const [state, setState] = useState({ status: "loading" }); - const load = useCallback(async () => { setState({ status: "loading" }); try { const [catalogs, grants, players] = await Promise.all([platformApiClient.listGameGiftCatalogs(serverInstanceId), platformApiClient.listGameGiftGrants(serverInstanceId), platformApiClient.listGamePlayers(serverInstanceId)]); const selectedCatalog = catalogs.items[0]?.id; const revisions = selectedCatalog ? await platformApiClient.listGameGiftRevisions(serverInstanceId, selectedCatalog) : { items: [] }; setState({ status: "ready", value: { catalogs: catalogs.items, revisions: revisions.items, grants: grants.items, players: players.items, selectedCatalog, selectedRevision: revisions.items[0]?.id, selectedPlayer: players.items[0]?.id, name: "新手补给", notice: "已向你发放运营礼包,请查收。", item: "bandage", quantity: 2 } }); } catch (error) { setState({ status: "error", reason: error instanceof Error ? error.message : "礼包目录读取失败" }); } }, [serverInstanceId]); - useEffect(() => { void load(); }, [load]); - const edit = (patch: Partial) => setState((current) => current.status === "ready" ? { ...current, value: { ...current.value, ...patch } } : current); - async function createDraft() { if (state.status !== "ready") return; const v = state.value; try { const saved = await platformApiClient.saveGameGiftCatalog(serverInstanceId, { id: v.selectedCatalog, name: v.name, gameVersion: "0.9.700.90357", items: [{ catalogItemKey: v.item, quantity: v.quantity }] }); edit({ catalogs: [saved, ...v.catalogs.filter((catalog) => catalog.id !== saved.id)], selectedCatalog: saved.id, revisions: [], selectedRevision: undefined, note: "草稿已保存;发布后才可定向发放。" }); } catch (error) { edit({ note: error instanceof Error ? error.message : "草稿保存失败" }); } } - async function publish() { if (state.status !== "ready" || !state.value.selectedCatalog) return; try { const revision = await platformApiClient.publishGameGiftCatalog(serverInstanceId, state.value.selectedCatalog); edit({ revisions: [revision, ...state.value.revisions], selectedRevision: revision.id, note: "已发布不可变版本,可预览并选择目标玩家。" }); } catch (error) { edit({ note: error instanceof Error ? error.message : "发布失败" }); } } - async function requestGrant() { if (state.status !== "ready") return; const v=state.value; if (!v.selectedRevision || !v.selectedPlayer) return; try { const grant=await platformApiClient.requestGameGiftGrant(serverInstanceId,{revisionId:v.selectedRevision,gamePlayerRecordId:v.selectedPlayer,notice:v.notice,idempotencyKey:`gift-${v.selectedRevision}-${v.selectedPlayer}`}); edit({grants:[grant,...v.grants],note:"发放申请已冻结礼包版本和目标身份,等待平台管理员审批。"}); } catch(error){edit({note:error instanceof Error?error.message:"发放申请失败"});} } - async function approve(grant: GameGiftGrantResponse) { if(state.status!=="ready")return;try{const next=await platformApiClient.approveGameGiftGrant(serverInstanceId,grant.id);edit({grants:state.value.grants.map((item)=>item.id===next.id?next:item),note:"已通过受控游戏通道排队发放。"});}catch(error){edit({note:error instanceof Error?error.message:"审批失败"});} } - if(state.status==="loading")return ; - if(state.status==="error")return void load()}/>; - const v=state.value; const selectedRevision=v.revisions.find((item)=>item.id===v.selectedRevision); - return

版本化礼包目录与定向发放

仅可选择此 SCUM 版本已验证的目录项;网页不提交游戏生成指令、物品代码或 RCON 文本。

礼包草稿与版本

预览、玩家选择与发放确认

{selectedRevision ? `版本 #${selectedRevision.revision}:${selectedRevision.items.map((item)=>`${item.label} ×${item.quantity}`).join("、")}` : "请先发布一个礼包版本。"}

{v.note&&

{v.note}

}
{v.grants.map((grant)=>
{grantStatus(grant.status)}{grant.playerDisplayName} · {grant.items.map((item)=>`${item.label} ×${item.quantity}`).join("、")}版本 #{grant.revisionNumber} · {grant.deliverySummary||"等待投递结果"}{grant.notificationSummary?` · 通知:${grant.notificationSummary}`:""}{grant.status==="pending-approval"&&}
)}
; -} -function grantStatus(status: GameGiftGrantResponse["status"]) { return ({"pending-approval":"等待平台管理员审批",queued:"已排队投递",delivered:"物品已投递,通知处理中",notification_failed:"物品已投递,定向通知失败",failed:"投递失败",unknown:"投递结果未知,绝不自动重试"})[status]; } diff --git a/platform_web/components/GamePlayerIntelligencePanel.test.tsx b/platform_web/components/GamePlayerIntelligencePanel.test.tsx deleted file mode 100644 index 0fe5dce..0000000 --- a/platform_web/components/GamePlayerIntelligencePanel.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -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).toContain("受控技能与角色属性修改"); - expect(source).toContain("等待平台管理员审批"); - expect(source).toContain("写后读取确认失败"); - expect(source).not.toContain("networkFingerprint"); - expect(source).not.toMatch(/\bban\b|\bkick\b/i); - }); -}); diff --git a/platform_web/components/GamePlayerIntelligencePanel.tsx b/platform_web/components/GamePlayerIntelligencePanel.tsx deleted file mode 100644 index b8af495..0000000 --- a/platform_web/components/GamePlayerIntelligencePanel.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { CheckCircle2, RefreshCw, ShieldAlert, SlidersHorizontal, UsersRound } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; - -import { platformApiClient } from "../api/client"; -import type { GamePlayerProfileResponse, GamePlayerResponse, GamePlayerStatePatchResponse, GamePlayerStateResponse } from "../api/types"; -import { ErrorState, LoadingState } from "./StateViews"; -import { GameGiftCatalogPanel } from "./GameGiftCatalogPanel"; - -type Selected = { profile: GamePlayerProfileResponse; state?: GamePlayerStateResponse; patches: GamePlayerStatePatchResponse[]; reason: string; after: Record; note?: string }; -type State = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; players: GamePlayerResponse[]; selected?: Selected }; - -export function GamePlayerIntelligencePanel({ serverInstanceId }: { serverInstanceId: string }) { - const [state, setState] = useState({ 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: message(error, "玩家档案读取失败。") }); } }, [serverInstanceId]); - useEffect(() => { void load(); }, [load]); - async function select(player: GamePlayerResponse) { try { const [profile, stateSnapshot, patchList] = await Promise.all([platformApiClient.getGamePlayerProfile(serverInstanceId, player.id), platformApiClient.getGamePlayerState(serverInstanceId, player.id), platformApiClient.listGamePlayerStatePatches(serverInstanceId, player.id)]); const after = Object.fromEntries(stateSnapshot.fields.map((field) => [field.key, String(field.value)])); setState((current) => current.status === "ready" ? { ...current, selected: { profile, state: stateSnapshot, patches: patchList.items, reason: "", after } } : current); } catch (error) { setState((current) => current.status === "ready" ? { ...current, selected: { profile: { player, aliases: [], sessions: [], accessAttempts: [], securitySignals: [] }, patches: [], reason: "", after: {}, note: message(error, "玩家状态快照不可用,已禁用修改。") } } : { status: "error", reason: message(error, "玩家档案读取失败。") }); } } - async function submitPatch() { if (state.status !== "ready") return; const selected = state.selected; if (!selected || !selected.state) return; const snapshot = selected.state; const changes = snapshot.fields.filter((field) => selected.after[field.key] !== String(field.value)).map((field) => ({ fieldKey: field.key, before: field.value, after: Number(selected.after[field.key]) })); try { const patch = await platformApiClient.requestGamePlayerStatePatch(serverInstanceId, selected.profile.player.id, { gameVersion: snapshot.gameVersion, expectedStateVersion: snapshot.stateVersion, safetyWindow: snapshot.safetyWindow || "", reason: selected.reason, changes }); setState((current) => current.status === "ready" && current.selected ? { ...current, selected: { ...current.selected, patches: [patch, ...current.selected.patches], note: "修改申请已保存,等待平台管理员审批。" } } : current); } catch (error) { setState((current) => current.status === "ready" && current.selected ? { ...current, selected: { ...current.selected, note: message(error, "修改申请未被接受。") } } : current); } } - async function approvePatch(patch: GamePlayerStatePatchResponse) { if (state.status !== "ready" || !state.selected) return; try { const approved = await platformApiClient.approveGamePlayerStatePatch(serverInstanceId, state.selected.profile.player.id, patch.id); setState((current) => current.status === "ready" && current.selected ? { ...current, selected: { ...current.selected, patches: current.selected.patches.map((item) => item.id === approved.id ? approved : item), note: "已批准并通过受控游戏通道排队执行。" } } : current); } catch (error) { setState((current) => current.status === "ready" && current.selected ? { ...current, selected: { ...current.selected, note: message(error, "审批失败。") } } : current); } } - if (state.status === "loading") return ; - if (state.status === "error") return void load()} />; - const selected = state.selected; - return <>

本地玩家档案与登录轨迹

游戏身份独立于平台账号;网络数据仅用于服务器内不可逆关联,绝不展示或持久化原始值。

{state.players.length ? state.players.map((player) => ) :

暂无已投影的成功登录记录。

}
{selected && setSelected(setState, reason)} onAfter={(key, value) => setAfter(setState, key, value)} onSubmit={() => void submitPatch()} onApprove={(patch) => void approvePatch(patch)} />}
; -} - -function PlayerDetail({ selected, onReason, onAfter, onSubmit, onApprove }: { selected: Selected; onReason: (value: string) => void; onAfter: (key: string, value: string) => void; onSubmit: () => void; onApprove: (patch: GamePlayerStatePatchResponse) => void }) { - const writable = Boolean(selected.state?.supported && selected.state.maintenanceVerified && !selected.state.playerOnline && selected.state.safetyWindow); - return

{selected.profile.player.displayName} 的审阅记录

别名 {selected.profile.aliases.length} · 会话 {selected.profile.sessions.length}
别名{selected.profile.aliases.map((alias) => alias.alias).join(" / ") || "无"}
{selected.profile.sessions.slice(0, 10).map((session) =>
会话{formatTime(session.startedAt)} → {session.endedAt ? formatTime(session.endedAt) : "仍在线"} {session.endReason ? `(${session.endReason})` : ""}
)}
{selected.profile.securitySignals.length ? selected.profile.securitySignals.map((signal) =>
) :

未发现需要人工审核的风险信号。

}
{selected.profile.accessAttempts.length > 0 &&

失败尝试:{selected.profile.accessAttempts.length} 条(仅显示结果与时间,不显示网络标识)。

}

受控技能与角色属性修改

{writable ? "已验证维护/离线安全窗口" : "未验证版本或安全窗口,已禁用修改"}
{selected.state && <>

版本:{selected.state.gameVersion} · 状态版本:{selected.state.stateVersion}。仅显示此服务器版本明确支持的字段。

{selected.state.fields.map((field) => )}