feat(plugin): add SCUM ownership migration foundation
This commit is contained in:
@@ -1,17 +1,15 @@
|
||||
import { ArrowLeft, PlugZap } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import * as React from "react";
|
||||
import { useCallback, useEffect, useState, type ComponentType } from "react";
|
||||
|
||||
import { platformApiClient } from "../api/client";
|
||||
import type { GamePluginResponse } from "../api/types";
|
||||
import { PageFrame } from "../components/PageFrame";
|
||||
import { ScumFileConfigWorkbench } from "../components/ScumFileConfigWorkbench";
|
||||
import { GamePlayerIntelligencePanel } from "../components/GamePlayerIntelligencePanel";
|
||||
import { ScumMapTrajectoryPanel } from "../components/ScumMapTrajectoryPanel";
|
||||
import { EmptyState, ErrorState, LoadingState } from "../components/StateViews";
|
||||
import type { PageComponentProps } from "../contracts/page";
|
||||
import { pluginBridgeManifestContractFromResponse } from "../contracts/pluginBridge";
|
||||
import { normalizeScumRouteKey, resolveScumOperationsPageContract, scumOperationsPluginId, scumOperationsRouteKey } from "../contracts/scumOperations";
|
||||
import { createPluginBridgeHostContext } from "../utils/pluginBridgeHost";
|
||||
import { loadPluginPageBundle } from "../utils/pluginPageBundles";
|
||||
|
||||
type PluginPageState =
|
||||
| { status: "loading" }
|
||||
@@ -24,9 +22,12 @@ interface PluginPageHostPageProps extends PageComponentProps {
|
||||
|
||||
export function PluginPageHostPage({ params, onNavigate, initialPlugin }: PluginPageHostPageProps) {
|
||||
const pluginId = params.pluginId ?? "";
|
||||
const routeKey = normalizeScumRouteKey(pluginId, params.routeKey ?? "");
|
||||
const routeKey = params.routeKey ?? "";
|
||||
const serverId = params.serverId ?? "";
|
||||
const [state, setState] = useState<PluginPageState>(() => initialPlugin ? { status: "ready", plugin: initialPlugin } : { status: "loading" });
|
||||
const [bundle, setBundle] = useState<ComponentType<{ context: ReturnType<typeof createPluginBridgeHostContext>; workspace?: unknown; availability: { available: boolean; reason?: string } }> | null>(null);
|
||||
const [bundleError, setBundleError] = useState("");
|
||||
const [availability, setAvailability] = useState<{ available: boolean; reason?: string }>({ available: false, reason: "正在验证 Companion 可用性。" });
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!pluginId || !routeKey) {
|
||||
@@ -49,6 +50,20 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin }: Plugin
|
||||
}
|
||||
}, [initialPlugin, load]);
|
||||
|
||||
const readyPlugin = state.status === "ready" ? state.plugin : undefined;
|
||||
const declaredPage = readyPlugin?.pages.find((candidate) => candidate.key === routeKey);
|
||||
const declaredBundlePage = readyPlugin ? pluginBridgeManifestContractFromResponse(readyPlugin).pages.find((candidate) => candidate.key === routeKey) : undefined;
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
if (!declaredBundlePage) return () => { active = false; };
|
||||
globalThis.__PLUGIN_PAGE_REACT__ = React;
|
||||
setBundle(null); setBundleError("");
|
||||
void loadPluginPageBundle(declaredBundlePage).then((loaded) => { if (active) setBundle(() => loaded); }).catch((error) => { if (active) setBundleError(error instanceof Error ? error.message : "插件页面 bundle 加载失败。"); });
|
||||
if (!serverId) { setAvailability({ available: false, reason: "插件页面没有绑定服务器。" }); return () => { active = false; }; }
|
||||
void platformApiClient.getGameClientBridgeStatus(serverId).then((status) => { if (active) setAvailability({ available: status.available, reason: status.reason }); }).catch((error) => { if (active) setAvailability({ available: false, reason: error instanceof Error ? error.message : "无法验证 Companion 可用性。" }); });
|
||||
return () => { active = false; };
|
||||
}, [declaredBundlePage, serverId]);
|
||||
|
||||
if (state.status === "loading") {
|
||||
return <LoadingState label="正在加载插件页面声明…" />;
|
||||
}
|
||||
@@ -56,7 +71,7 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin }: Plugin
|
||||
return <ErrorState title="插件页面不可用" reason={state.reason} onRetry={() => void load()} />;
|
||||
}
|
||||
|
||||
const page = state.plugin.pages.find((candidate) => candidate.key === routeKey);
|
||||
const page = declaredPage;
|
||||
if (!page) {
|
||||
return <ErrorState title="插件页面不可用" reason="当前插件没有声明该 routeKey。" />;
|
||||
}
|
||||
@@ -67,9 +82,6 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin }: Plugin
|
||||
serverInstanceId: serverId || undefined,
|
||||
themeTokens: { colorScheme: "dark", accentColor: "#7dd3fc" }
|
||||
});
|
||||
const isScumOperations = state.plugin.id === scumOperationsPluginId && routeKey === scumOperationsRouteKey;
|
||||
const scumResolution = isScumOperations ? resolveScumOperationsPageContract(state.plugin, serverId) : null;
|
||||
|
||||
return (
|
||||
<div className="console-page">
|
||||
<PageFrame
|
||||
@@ -92,24 +104,11 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin }: Plugin
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{scumResolution && !scumResolution.available && <ErrorState title="SCUM 运维声明不可用" reason={scumResolution.reason} compact />}
|
||||
{scumResolution?.available && (
|
||||
<div className="action-list" aria-label="SCUM operations declarations">
|
||||
<span><strong>命令目录</strong> {scumResolution.contract.commands.length} 项,均来自插件 operations 页面声明</span>
|
||||
<span><strong>快照目录</strong> {scumResolution.contract.snapshots.length} 项,按插件 schemaVersion 投影</span>
|
||||
<span><strong>查询模板</strong> {scumResolution.contract.queryTemplates.length} 项,仅保留声明式 SQLite 模板引用</span>
|
||||
</div>
|
||||
)}
|
||||
{!scumResolution && (
|
||||
<EmptyState
|
||||
title="插件页面已接入 Host Bridge"
|
||||
description="当前页面只接收 manifest 声明与平台安全上下文;具体操作由对应插件页面实现。"
|
||||
/>
|
||||
)}
|
||||
<div className="action-list" aria-label="plugin page declarations"><span><strong>Bundle</strong> {page.bundleKey ? `${page.bundleKey}@${page.bundleVersion}` : "未声明"}</span><span><strong>完整性</strong> {page.bundleIntegritySha256 ? `${page.bundleIntegritySha256.slice(0, 18)}…` : "未声明"}</span><span><strong>Companion</strong> {availability.available ? "可用" : "不可用"}</span></div>
|
||||
</section>
|
||||
{scumResolution?.available && state.plugin.fileWorkspace && <ScumFileConfigWorkbench contract={scumResolution.contract} workspace={state.plugin.fileWorkspace} />}
|
||||
{scumResolution?.available && <GamePlayerIntelligencePanel serverInstanceId={serverId} />}
|
||||
{scumResolution?.available && <ScumMapTrajectoryPanel serverInstanceId={serverId} />}
|
||||
{bundleError && <ErrorState title="插件页面不可用" reason={bundleError} />}
|
||||
{!bundle && !bundleError && <LoadingState label="正在校验并加载插件页面 bundle…" />}
|
||||
{bundle && React.createElement(bundle, { context: hostContext, workspace: state.plugin.fileWorkspace, availability })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user