Add SCUM file management workbench
This commit is contained in:
@@ -326,6 +326,7 @@ describe("first-party console pages", () => {
|
||||
expect(serverLiveOperationsSource).toContain("mergeTerminalLines");
|
||||
expect(serverLiveOperationsSource).not.toContain("terminalLogPollMs = 1000");
|
||||
expect(serverLiveOperationsSource).not.toContain("logStreamPollMs = 5000");
|
||||
expect(serverLiveOperationsSource).not.toContain("queryLogStream(");
|
||||
expect(serverLiveOperationsSource).not.toContain("SaveWorld");
|
||||
});
|
||||
|
||||
|
||||
@@ -95,4 +95,11 @@ describe("PluginPageHostPage", () => {
|
||||
expect(hostSource).toContain("loadPluginPageBundle");
|
||||
expect(hostSource).not.toMatch(/ScumFileConfigWorkbench|GamePlayerIntelligencePanel|GameGiftCatalogPanel|ScumMapTrajectoryPanel|game\.scum/);
|
||||
});
|
||||
|
||||
it("keeps file workspace callbacks stable across parent operational refreshes", () => {
|
||||
expect(hostSource).toContain("readyPluginRef.current = readyPlugin");
|
||||
expect(hostSource).toContain("hostContextRef.current = hostContext");
|
||||
expect(hostSource).toContain("}, [pluginId, serverId]);");
|
||||
expect(hostSource).not.toContain("}, [hostContext, readyPlugin, serverId]);");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ArrowLeft, PlugZap } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useCallback, useEffect, useState, type ComponentType } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ComponentType } from "react";
|
||||
|
||||
import { platformApiClient } from "../api/client";
|
||||
import type { GamePluginResponse } from "../api/types";
|
||||
@@ -8,7 +8,8 @@ import { PageFrame } from "../components/PageFrame";
|
||||
import { EmptyState, ErrorState, LoadingState } from "../components/StateViews";
|
||||
import type { PageComponentProps } from "../contracts/page";
|
||||
import { pluginBridgeManifestContractFromResponse } from "../contracts/pluginBridge";
|
||||
import { createPluginBridgeHostContext } from "../utils/pluginBridgeHost";
|
||||
import type { PluginPageWorkspaceActions } from "../contracts/pluginPageHost";
|
||||
import { createPluginBridgeDispatcher, createPluginBridgeHostContext } from "../utils/pluginBridgeHost";
|
||||
import { loadPluginPageBundle, type PluginPageAvailability } from "../utils/pluginPageBundles";
|
||||
|
||||
type PluginPageState =
|
||||
@@ -18,16 +19,19 @@ type PluginPageState =
|
||||
|
||||
interface PluginPageHostPageProps extends PageComponentProps {
|
||||
initialPlugin?: GamePluginResponse;
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
export function PluginPageHostPage({ params, onNavigate, initialPlugin }: PluginPageHostPageProps) {
|
||||
export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded = false }: PluginPageHostPageProps) {
|
||||
const pluginId = params.pluginId ?? "";
|
||||
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: PluginPageAvailability }> | null>(null);
|
||||
const [bundle, setBundle] = useState<ComponentType<{ context: ReturnType<typeof createPluginBridgeHostContext>; workspace?: unknown; workspaceActions?: PluginPageWorkspaceActions; availability: PluginPageAvailability }> | null>(null);
|
||||
const [bundleError, setBundleError] = useState("");
|
||||
const [availability, setAvailability] = useState<PluginPageAvailability>({ available: false, reason: "正在验证 Companion 可用性。" });
|
||||
const readyPluginRef = useRef<GamePluginResponse | undefined>(undefined);
|
||||
const hostContextRef = useRef<ReturnType<typeof createPluginBridgeHostContext> | undefined>(undefined);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!pluginId || !routeKey) {
|
||||
@@ -52,19 +56,107 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin }: Plugin
|
||||
|
||||
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;
|
||||
const manifestContract = useMemo(() => (readyPlugin ? pluginBridgeManifestContractFromResponse(readyPlugin) : undefined), [readyPlugin]);
|
||||
const declaredBundlePage = manifestContract?.pages.find((candidate) => candidate.key === routeKey);
|
||||
const hostContext = useMemo(() => manifestContract ? createPluginBridgeHostContext({
|
||||
plugin: manifestContract,
|
||||
routeKey,
|
||||
serverInstanceId: serverId || undefined,
|
||||
themeTokens: { colorScheme: "dark", accentColor: "#7dd3fc" }
|
||||
}) : undefined, [manifestContract, routeKey, serverId]);
|
||||
readyPluginRef.current = readyPlugin;
|
||||
hostContextRef.current = hostContext;
|
||||
const workspaceActions = useMemo<PluginPageWorkspaceActions | undefined>(() => {
|
||||
if (!pluginId || !serverId) return undefined;
|
||||
return {
|
||||
requestFile: async (fileKey) => {
|
||||
const plugin = readyPluginRef.current;
|
||||
const context = hostContextRef.current;
|
||||
if (!plugin || !context) {
|
||||
return { status: "denied", message: "插件页面上下文尚未就绪。" };
|
||||
}
|
||||
if (!plugin.fileWorkspace?.files.some((file) => file.key === fileKey)) {
|
||||
return { status: "denied", message: "该文件不在当前插件声明的工作区内。" };
|
||||
}
|
||||
const dispatch = createPluginBridgeDispatcher(context, platformApiClient);
|
||||
const response = await dispatch({
|
||||
requestId: `web:plugin-file-read:${serverId}:${fileKey}:${Date.now()}`,
|
||||
action: "files.request",
|
||||
payload: { operation: "read", key: fileKey }
|
||||
});
|
||||
const jobId = response.result?.jobId;
|
||||
if (response.status === "queued" || response.status === "ok") {
|
||||
return { status: response.status, jobId, message: jobId ? `读取任务 ${jobId} 已提交。` : "已提交文件读取请求。" };
|
||||
}
|
||||
return { status: response.status, message: response.error?.message ?? "文件读取请求未能提交。" };
|
||||
},
|
||||
getFileSnapshot: async (fileKey) => {
|
||||
const plugin = readyPluginRef.current;
|
||||
if (!plugin) {
|
||||
return { serverInstanceId: serverId, pluginId, key: fileKey, state: "unavailable", reason: "插件页面上下文尚未就绪。" };
|
||||
}
|
||||
if (!plugin.fileWorkspace?.files.some((file) => file.key === fileKey)) {
|
||||
return { serverInstanceId: serverId, pluginId: plugin.id, key: fileKey, state: "unavailable", reason: "该文件不在当前插件声明的工作区内。" };
|
||||
}
|
||||
try {
|
||||
return await platformApiClient.getDeclaredFileReadSnapshot(serverId, fileKey);
|
||||
} catch (error) {
|
||||
return {
|
||||
serverInstanceId: serverId,
|
||||
pluginId: plugin.id,
|
||||
key: fileKey,
|
||||
state: "unavailable",
|
||||
reason: error instanceof Error ? error.message : "无法读取文件快照。"
|
||||
};
|
||||
}
|
||||
},
|
||||
writeFile: async (fileKey, content, options) => {
|
||||
const plugin = readyPluginRef.current;
|
||||
const context = hostContextRef.current;
|
||||
if (!plugin || !context) {
|
||||
return { status: "denied", message: "插件页面上下文尚未就绪。" };
|
||||
}
|
||||
const file = plugin.fileWorkspace?.files.find((candidate) => candidate.key === fileKey);
|
||||
if (!file) {
|
||||
return { status: "denied", message: "该文件不在当前插件声明的工作区内。" };
|
||||
}
|
||||
if (file.kind !== "config" || !file.editable) {
|
||||
return { status: "denied", message: "该声明文件不允许通过配置工作台写入。" };
|
||||
}
|
||||
if (!context.permissions.includes("server.files.write")) {
|
||||
return { status: "denied", message: "当前页面没有声明文件写入权限。" };
|
||||
}
|
||||
try {
|
||||
const response = await platformApiClient.dispatchFileOperation({
|
||||
serverInstanceId: serverId,
|
||||
pluginId: plugin.id,
|
||||
operation: "write",
|
||||
key: fileKey,
|
||||
content,
|
||||
expectedChecksum: options?.expectedChecksum,
|
||||
idempotencyKey: `web:plugin-file-write:${serverId}:${fileKey}:${Date.now()}`
|
||||
});
|
||||
return { status: response.status, jobId: response.job.id, message: `写入任务 ${response.job.id} 已提交。` };
|
||||
} catch (error) {
|
||||
return { status: "error", message: error instanceof Error ? error.message : "文件写入请求未能提交。" };
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [pluginId, serverId]);
|
||||
const bundleLoadKey = declaredBundlePage ? [declaredBundlePage.bundleKey, declaredBundlePage.bundleVersion, declaredBundlePage.bundleIntegritySha256, declaredBundlePage.path].join(":") : "";
|
||||
const loadableBundlePage = useMemo(() => declaredBundlePage, [bundleLoadKey]);
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
if (!declaredBundlePage) return () => { active = false; };
|
||||
if (!loadableBundlePage) 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 加载失败。"); });
|
||||
void loadPluginPageBundle(loadableBundlePage).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, features: status.features });
|
||||
}).catch((error) => { if (active) setAvailability({ available: false, reason: error instanceof Error ? error.message : "无法验证 Companion 可用性。" }); });
|
||||
return () => { active = false; };
|
||||
}, [declaredBundlePage, serverId]);
|
||||
}, [bundleLoadKey, loadableBundlePage, serverId]);
|
||||
|
||||
if (state.status === "loading") {
|
||||
return <LoadingState label="正在加载插件页面声明…" />;
|
||||
@@ -77,13 +169,18 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin }: Plugin
|
||||
if (!page) {
|
||||
return <ErrorState title="插件页面不可用" reason="当前插件没有声明该 routeKey。" />;
|
||||
}
|
||||
const manifestContract = pluginBridgeManifestContractFromResponse(state.plugin);
|
||||
const hostContext = createPluginBridgeHostContext({
|
||||
plugin: manifestContract,
|
||||
routeKey,
|
||||
serverInstanceId: serverId || undefined,
|
||||
themeTokens: { colorScheme: "dark", accentColor: "#7dd3fc" }
|
||||
});
|
||||
if (!hostContext) {
|
||||
return <ErrorState title="插件页面不可用" reason="插件页面上下文初始化失败。" />;
|
||||
}
|
||||
if (embedded) {
|
||||
return (
|
||||
<>
|
||||
{bundleError && <ErrorState title="插件页面不可用" reason={bundleError} />}
|
||||
{!bundle && !bundleError && <LoadingState label="正在校验并加载插件页面 bundle…" compact />}
|
||||
{bundle && React.createElement(bundle, { context: hostContext, workspace: state.plugin.fileWorkspace, workspaceActions, availability })}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="console-page">
|
||||
<PageFrame
|
||||
@@ -110,7 +207,7 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin }: Plugin
|
||||
</section>
|
||||
{bundleError && <ErrorState title="插件页面不可用" reason={bundleError} />}
|
||||
{!bundle && !bundleError && <LoadingState label="正在校验并加载插件页面 bundle…" />}
|
||||
{bundle && React.createElement(bundle, { context: hostContext, workspace: state.plugin.fileWorkspace, availability })}
|
||||
{bundle && React.createElement(bundle, { context: hostContext, workspace: state.plugin.fileWorkspace, workspaceActions, availability })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -170,6 +170,15 @@ describe("ServerDetailPage config write approval", () => {
|
||||
expect(serverDetailPageSource).not.toContain('capability: "process.install"');
|
||||
});
|
||||
|
||||
it("routes the SCUM logs section to the declared file management workbench", () => {
|
||||
expect(serverDetailPageSource).toContain("serverDetailSectionLabel(entry, instance.data.pluginId)");
|
||||
expect(serverDetailPageSource).toContain('"文件管理"');
|
||||
expect(serverDetailPageSource).toContain("ScumFileManagementSection");
|
||||
expect(serverDetailPageSource).toContain("PluginPageHostPage");
|
||||
expect(serverDetailPageSource).toContain('routeKey: "files-config"');
|
||||
expect(serverDetailPageSource).toContain('file.kind === "config"');
|
||||
});
|
||||
|
||||
it("keeps plugin lifecycle and bridge-visible output on platform-owned logical references", () => {
|
||||
expect(serverDetailPageSource).toContain("parsePluginArtifactReference(result)");
|
||||
expect(serverDetailPageSource).toContain("platformApiClient.openArtifactDownload(artifact.id)");
|
||||
|
||||
@@ -20,7 +20,7 @@ import type {
|
||||
ServerMemberResponse,
|
||||
ServerMetricsResponse,
|
||||
RuntimeBindingResponse,
|
||||
ServerDeploymentResponse,
|
||||
ServerDeploymentResponse,
|
||||
ServerRuntimeActionsResponse,
|
||||
MetricSampleResponse,
|
||||
RemoteAdapterDeclarationResponse
|
||||
@@ -73,6 +73,7 @@ import { createPluginBridgeDispatcher, createPluginBridgeHostContext, parsePlugi
|
||||
import { downloadArtifactReference, safeArtifactError, safeArtifactFilename } from "../utils/artifactTransfer";
|
||||
import { cx } from "../utils/classes";
|
||||
import { stateLabel, statusClass } from "./ServersPage";
|
||||
import { PluginPageHostPage } from "./PluginPageHostPage";
|
||||
import { appendLiveLogEntries, entryFromServerLogEvent, mergeLogStreams, parseLogStreamEvent, parseServerLogEvent, streamFromServerLogEvent, type LiveLogEntry } from "../utils/logEvents";
|
||||
|
||||
type LoadState<T> = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: T };
|
||||
@@ -81,7 +82,8 @@ const defaultConfigKey = "server.properties";
|
||||
const serverDetailRefreshMs = 5000;
|
||||
const serverMetricFreshMs = 30000;
|
||||
|
||||
export function ServerDetailPage({ session, params, operations, onNavigate }: PageComponentProps) {
|
||||
export function ServerDetailPage(props: PageComponentProps) {
|
||||
const { session, params, operations, onNavigate } = props;
|
||||
const serverId = params.serverId ?? "";
|
||||
const [section, setSection] = useState<ServerDetailSection>("logs");
|
||||
const [instance, setInstance] = useState<LoadState<ServerInstanceResponse>>({ status: "loading" });
|
||||
@@ -94,7 +96,7 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
|
||||
const [remoteAdapters, setRemoteAdapters] = useState<RemoteAdapterDeclarationResponse[]>([]);
|
||||
const [runtimeActions, setRuntimeActions] = useState<LoadState<ServerRuntimeActionsResponse>>({ status: "loading" });
|
||||
const [runtimeBinding, setRuntimeBinding] = useState<LoadState<RuntimeBindingResponse>>({ status: "loading" });
|
||||
const [deployment, setDeployment] = useState<LoadState<ServerDeploymentResponse>>({ status: "loading" });
|
||||
const [deployment, setDeployment] = useState<LoadState<ServerDeploymentResponse>>({ status: "loading" });
|
||||
const [liveLogOpen, setLiveLogOpen] = useState(false);
|
||||
const [terminalOpen, setTerminalOpen] = useState(false);
|
||||
const [confirm, setConfirm] = useState<null | { title: string; description: string; danger?: boolean; run: () => Promise<void> }>(null);
|
||||
@@ -107,7 +109,7 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
|
||||
}
|
||||
setInstance({ status: "loading" });
|
||||
try {
|
||||
const [detail, pluginResponse, jobResponse, runtimeResponse, bindingResponse, deploymentResponse, metricHistoryResponse, backupResponse, adapterResponse] = await Promise.all([
|
||||
const [detail, pluginResponse, jobResponse, runtimeResponse, bindingResponse, deploymentResponse, metricHistoryResponse, backupResponse, adapterResponse] = await Promise.all([
|
||||
platformApiClient.getServerInstance(serverId),
|
||||
platformApiClient.listGamePlugins(),
|
||||
platformApiClient.listJobs(serverId),
|
||||
@@ -119,10 +121,10 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
|
||||
.getServerRuntimeBinding(serverId)
|
||||
.then((data): LoadState<RuntimeBindingResponse> => ({ status: "ready", data }))
|
||||
.catch((error): LoadState<RuntimeBindingResponse> => ({ status: "error", reason: error instanceof Error ? error.message : "运行配置加载失败" })),
|
||||
platformApiClient
|
||||
.getServerDeployment(serverId)
|
||||
.then((data): LoadState<ServerDeploymentResponse> => ({ status: "ready", data }))
|
||||
.catch((error): LoadState<ServerDeploymentResponse> => ({ status: "error", reason: error instanceof Error ? error.message : "部署定义加载失败" })),
|
||||
platformApiClient
|
||||
.getServerDeployment(serverId)
|
||||
.then((data): LoadState<ServerDeploymentResponse> => ({ status: "ready", data }))
|
||||
.catch((error): LoadState<ServerDeploymentResponse> => ({ status: "error", reason: error instanceof Error ? error.message : "部署定义加载失败" })),
|
||||
platformApiClient.listMetricHistory(serverId).catch(() => ({ items: [], count: 0 })),
|
||||
platformApiClient.listBackups(serverId).catch(() => ({ items: [], count: 0 })),
|
||||
platformApiClient.listRemoteAdapters(serverId).catch(() => ({ items: [], count: 0 }))
|
||||
@@ -132,7 +134,7 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
|
||||
setJobs(jobResponse.items);
|
||||
setRuntimeActions(runtimeResponse);
|
||||
setRuntimeBinding(bindingResponse);
|
||||
setDeployment(deploymentResponse);
|
||||
setDeployment(deploymentResponse);
|
||||
setMetricHistory(metricHistoryResponse.items);
|
||||
setBackups(backupResponse.items);
|
||||
setRemoteAdapters(adapterResponse.items);
|
||||
@@ -150,7 +152,7 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
|
||||
setArtifacts([]);
|
||||
setRuntimeActions({ status: "error", reason: "运行分发状态加载失败" });
|
||||
setRuntimeBinding({ status: "error", reason: "运行配置加载失败" });
|
||||
setDeployment({ status: "error", reason: "部署定义加载失败" });
|
||||
setDeployment({ status: "error", reason: "部署定义加载失败" });
|
||||
setMetricHistory([]);
|
||||
setBackups([]);
|
||||
setRemoteAdapters([]);
|
||||
@@ -267,7 +269,7 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
|
||||
aria-current={section === entry.id ? "page" : undefined}
|
||||
onClick={() => setSection(entry.id)}
|
||||
>
|
||||
{entry.label}
|
||||
{serverDetailSectionLabel(entry)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
@@ -330,12 +332,12 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
|
||||
aria-current={section === entry.id ? "page" : undefined}
|
||||
onClick={() => setSection(entry.id)}
|
||||
>
|
||||
{entry.label}
|
||||
{serverDetailSectionLabel(entry, instance.data.pluginId)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{section === "logs" && <LogsSection serverId={serverId} />}
|
||||
{section === "logs" && (instance.data.pluginId === "game.scum" ? <ScumFileManagementSection pageProps={props} serverId={serverId} plugin={plugins.find((plugin) => plugin.id === instance.data.pluginId)} /> : <LogsSection serverId={serverId} />)}
|
||||
{section === "terminal" && <SourceRCONCommandPanel serverId={instance.data.id} pluginId={instance.data.pluginId} />}
|
||||
{section === "runtime" && (
|
||||
<RuntimeBindingSection
|
||||
@@ -409,6 +411,24 @@ function uniqueArtifacts(artifacts: ArtifactResponse[]): ArtifactResponse[] {
|
||||
return [...byID.values()];
|
||||
}
|
||||
|
||||
function serverDetailSectionLabel(entry: { id: ServerDetailSection; label: string }, pluginId?: string): string {
|
||||
return pluginId === "game.scum" && entry.id === "logs" ? "文件管理" : entry.label;
|
||||
}
|
||||
|
||||
interface ScumFileManagementSectionProps {
|
||||
pageProps: PageComponentProps;
|
||||
serverId: string;
|
||||
plugin?: GamePluginResponse;
|
||||
}
|
||||
|
||||
function ScumFileManagementSection({ pageProps, serverId, plugin }: ScumFileManagementSectionProps) {
|
||||
const params = useMemo(() => ({ ...pageProps.params, pluginId: plugin?.id ?? "", routeKey: "files-config", serverId }), [pageProps.params.pluginId, pageProps.params.routeKey, pageProps.params.serverId, plugin?.id, serverId]);
|
||||
if (!plugin) {
|
||||
return <LoadingState label="正在加载 SCUM 文件工作台…" compact />;
|
||||
}
|
||||
return <PluginPageHostPage {...pageProps} params={params} initialPlugin={plugin} embedded />;
|
||||
}
|
||||
|
||||
interface ServerMetadataSectionProps {
|
||||
instance: ServerInstanceResponse;
|
||||
session: PageComponentProps["session"];
|
||||
@@ -1999,7 +2019,7 @@ function PluginBridgeExecutionPanel({ plugin, serverId, serverInstance, artifact
|
||||
requestId: `web:bridge:${serverId}:${plugin.id}:${action}:${Date.now()}`,
|
||||
action,
|
||||
aiPurpose: action === "ai.invoke" ? plugin.aiPurposes[0] : undefined,
|
||||
payload: bridgePayloadForAction(action, serverInstance, artifacts[0])
|
||||
payload: bridgePayloadForAction(action, plugin, serverInstance, artifacts[0])
|
||||
});
|
||||
setPendingAction(null);
|
||||
if (response.status === "ok" || response.status === "queued") {
|
||||
@@ -2044,9 +2064,10 @@ function PluginBridgeExecutionPanel({ plugin, serverId, serverInstance, artifact
|
||||
);
|
||||
}
|
||||
|
||||
function bridgePayloadForAction(action: PluginBridgeAction, serverInstance: ServerInstanceResponse, artifact?: ArtifactResponse): Record<string, string> | undefined {
|
||||
function bridgePayloadForAction(action: PluginBridgeAction, plugin: GamePluginResponse, serverInstance: ServerInstanceResponse, artifact?: ArtifactResponse): Record<string, string> | undefined {
|
||||
if (action === "files.request") {
|
||||
return { operation: "read", key: "logs/latest.log", expectedConfigVersion: String(serverInstance.configVersion) };
|
||||
const declaredFileKey = plugin.fileWorkspace?.files.find((file) => file.kind === "config")?.key ?? plugin.fileWorkspace?.files[0]?.key ?? "logs/latest.log";
|
||||
return { operation: "read", key: declaredFileKey, expectedConfigVersion: String(serverInstance.configVersion) };
|
||||
}
|
||||
if (action === "artifacts.open" && artifact) {
|
||||
return { artifactId: artifact.id };
|
||||
|
||||
Reference in New Issue
Block a user