Integrate SCUM real ops workflows
This commit is contained in:
@@ -9,7 +9,7 @@ import { EmptyState, ErrorState, LoadingState } from "../components/StateViews";
|
||||
import type { PageComponentProps } from "../contracts/page";
|
||||
import { pluginBridgeManifestContractFromResponse } from "../contracts/pluginBridge";
|
||||
import type { PluginPageWorkspaceActions } from "../contracts/pluginPageHost";
|
||||
import { createPluginBridgeDispatcher, createPluginBridgeHostContext } from "../utils/pluginBridgeHost";
|
||||
import { createPluginBridgeHostContext } from "../utils/pluginBridgeHost";
|
||||
import { loadPluginPageBundle, type PluginPageAvailability } from "../utils/pluginPageBundles";
|
||||
|
||||
type PluginPageState =
|
||||
@@ -27,7 +27,7 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
|
||||
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; workspaceActions?: PluginPageWorkspaceActions; availability: PluginPageAvailability }> | null>(null);
|
||||
const [bundle, setBundle] = useState<ComponentType<{ context: ReturnType<typeof createPluginBridgeHostContext>; 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);
|
||||
@@ -69,97 +69,18 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
|
||||
const workspaceActions = useMemo<PluginPageWorkspaceActions | undefined>(() => {
|
||||
if (!pluginId) return undefined;
|
||||
return {
|
||||
refreshWorkspace: async () => {
|
||||
const response = await platformApiClient.listGamePlugins();
|
||||
const plugin = response.items.find((candidate) => candidate.id === pluginId);
|
||||
if (!plugin) {
|
||||
throw new Error("未找到已注册的插件声明。");
|
||||
}
|
||||
readyPluginRef.current = plugin;
|
||||
setState({ status: "ready", plugin });
|
||||
return plugin.fileWorkspace;
|
||||
},
|
||||
requestFile: async (fileKey) => {
|
||||
const plugin = readyPluginRef.current;
|
||||
const context = hostContextRef.current;
|
||||
if (!serverId) {
|
||||
return { status: "denied", message: "插件页面没有绑定服务器,无法读取文件。" };
|
||||
}
|
||||
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 (!serverId) {
|
||||
return { serverInstanceId: "", pluginId, key: fileKey, state: "unavailable", reason: "插件页面没有绑定服务器,无法读取文件快照。" };
|
||||
}
|
||||
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 (!serverId) {
|
||||
return { status: "denied", message: "插件页面没有绑定服务器,无法写入文件。" };
|
||||
}
|
||||
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 : "文件写入请求未能提交。" };
|
||||
}
|
||||
}
|
||||
listSCUMPlayers: () => platformApiClient.listSCUMPlayers(serverId),
|
||||
listSCUMSquads: () => platformApiClient.listSCUMSquads(serverId),
|
||||
listSCUMSquadMembers: () => platformApiClient.listSCUMSquadMembers(serverId),
|
||||
listSCUMVehicles: () => platformApiClient.listSCUMVehicles(serverId),
|
||||
listSCUMFlags: () => platformApiClient.listSCUMFlags(serverId),
|
||||
listSCUMPositions: () => platformApiClient.listSCUMPositions(serverId),
|
||||
listSCUMOperations: () => platformApiClient.listSCUMOperations(serverId),
|
||||
createSCUMOperation: (request) => platformApiClient.createSCUMOperation(serverId, request as never),
|
||||
approveSCUMOperation: (operationId) => platformApiClient.approveSCUMOperation(serverId, operationId),
|
||||
listSCUMWorkflows: () => platformApiClient.listSCUMWorkflows(serverId),
|
||||
createSCUMWorkflow: (request) => platformApiClient.createSCUMWorkflow(serverId, request as never),
|
||||
listSCUMWorkflowSteps: (workflowId) => platformApiClient.listSCUMWorkflowSteps(serverId, workflowId)
|
||||
};
|
||||
}, [pluginId, serverId]);
|
||||
const bundleLoadKey = declaredBundlePage ? [declaredBundlePage.bundleKey, declaredBundlePage.bundleVersion, declaredBundlePage.bundleIntegritySha256, declaredBundlePage.path].join(":") : "";
|
||||
@@ -196,7 +117,7 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
|
||||
<>
|
||||
{bundleError && <ErrorState title="插件页面不可用" reason={bundleError} />}
|
||||
{!bundle && !bundleError && <LoadingState label="正在校验并加载插件页面 bundle…" compact />}
|
||||
{bundle && React.createElement(bundle, { context: hostContext, workspace: state.plugin.fileWorkspace, workspaceActions, availability })}
|
||||
{bundle && React.createElement(bundle, { context: hostContext, workspaceActions, availability })}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -226,7 +147,7 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
|
||||
</section>
|
||||
{bundleError && <ErrorState title="插件页面不可用" reason={bundleError} />}
|
||||
{!bundle && !bundleError && <LoadingState label="正在校验并加载插件页面 bundle…" />}
|
||||
{bundle && React.createElement(bundle, { context: hostContext, workspace: state.plugin.fileWorkspace, workspaceActions, availability })}
|
||||
{bundle && React.createElement(bundle, { context: hostContext, workspaceActions, availability })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user