Align SCUM operations with reference tools

This commit is contained in:
npc0-hue
2026-08-13 18:21:08 +08:00
parent 8b236d7c15
commit da01d39866
24 changed files with 375 additions and 105 deletions
+36
View File
@@ -47,6 +47,14 @@ import type {
GameClientBridgeSnapshotListResponse,
GameClientBridgeSnapshotQuery,
GameClientBridgeStatusResponse,
GameGiftCatalogListResponse,
GameGiftCatalogRequest,
GameGiftCatalogResponse,
GameGiftGrantListResponse,
GameGiftGrantRequest,
GameGiftGrantResponse,
GameGiftRevisionListResponse,
GameGiftRevisionResponse,
GamePluginListResponse,
HealthResponse,
JobCreateRequest,
@@ -628,6 +636,34 @@ export class PlatformApiClient {
return this.request<SCUMListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/positions`);
}
async listGameGiftCatalogs(serverInstanceId: string): Promise<GameGiftCatalogListResponse> {
return this.request<GameGiftCatalogListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-gifts`);
}
async saveGameGiftCatalog(serverInstanceId: string, request: GameGiftCatalogRequest): Promise<GameGiftCatalogResponse> {
return this.request<GameGiftCatalogResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-gifts`, { method: "POST", body: request });
}
async publishGameGiftCatalog(serverInstanceId: string, catalogId: string): Promise<GameGiftRevisionResponse> {
return this.request<GameGiftRevisionResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-gifts/${encodeURIComponent(catalogId)}/publish`, { method: "POST", body: {} });
}
async listGameGiftRevisions(serverInstanceId: string, catalogId: string): Promise<GameGiftRevisionListResponse> {
return this.request<GameGiftRevisionListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-gifts/${encodeURIComponent(catalogId)}/revisions`);
}
async listGameGiftGrants(serverInstanceId: string): Promise<GameGiftGrantListResponse> {
return this.request<GameGiftGrantListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-gift-grants`);
}
async requestGameGiftGrant(serverInstanceId: string, request: GameGiftGrantRequest): Promise<GameGiftGrantResponse> {
return this.request<GameGiftGrantResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-gift-grants`, { method: "POST", body: request });
}
async approveGameGiftGrant(serverInstanceId: string, grantId: string): Promise<GameGiftGrantResponse> {
return this.request<GameGiftGrantResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/game-gift-grants/${encodeURIComponent(grantId)}/approve`, { method: "POST", body: {} });
}
async listSCUMOperations(serverInstanceId: string): Promise<SCUMOperationListResponse> {
return this.request<SCUMOperationListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/operations`);
}
+12
View File
@@ -388,9 +388,12 @@ export interface GamePluginResponse {
validationViolations?: string[];
runtimeProfiles?: GamePluginRuntimeProfilesResponse;
gameClientBridge?: GameClientBridgeManifestResponse;
mapTrajectories?: GameMapGeometryResponse;
status: GamePluginStatus;
}
export interface GameMapGeometryResponse { mapId: string; mapVersion: string; worldMinX: number; worldMinY: number; worldMaxX: number; worldMaxY: number; imageWidth: number; imageHeight: number; precision: number; sampleDistance: number; sampleIntervalSeconds: number; retentionSeconds: number; }
export type PluginCreateFieldType = "text" | "number" | "boolean" | "select" | "port";
export interface PluginCreateFieldResponse {
@@ -1386,6 +1389,15 @@ export type SCUMSquadsListResponse = SCUMListResponse<SCUMSquadRecord>;
export type SCUMActivityListResponse = SCUMListResponse<SCUMActivityEventRecord>;
export type SCUMGiftsListResponse = SCUMListResponse<SCUMGiftRecord>;
export type SCUMMapPointsListResponse = SCUMListResponse<SCUMMapPointRecord>;
export interface GameGiftItemResponse { catalogItemKey: string; label: string; quantity: number; }
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 GameGiftGrantResponse { id: string; revisionId: string; revisionNumber: number; gameVersion: string; items: GameGiftItemResponse[]; gamePlayerRecordId: string; playerDisplayName: string; notice: string; requesterId: string; approverId?: string; status: string; deliverySummary?: string; notificationSummary?: string; createdAt: string; approvedAt?: string; completedAt?: string; }
export interface GameGiftGrantListResponse { items: GameGiftGrantResponse[]; }
export interface GameGiftCatalogRequest { id?: string; name: string; gameVersion: string; items: Array<{ catalogItemKey: string; quantity: number }>; }
export interface GameGiftGrantRequest { revisionId: string; gamePlayerRecordId: string; notice: string; idempotencyKey: string; }
export interface SCUMWorkflowCreateRequest { templateKey: string; idempotencyKey: string; input?: SCUMJsonRecord; }
export interface SCUMOperationRequest { templateKey: string; playerId?: string; payload?: SCUMJsonRecord; guard?: SCUMJsonRecord; reason: string; idempotencyKey: string; }
export interface SCUMWorkflowResponse { id: string; serverInstanceId: string; pluginId: string; templateKey: string; requestedBy?: string; idempotencyKey?: string; status: string; currentStepKey?: string; input?: SCUMJsonRecord; safeSummary?: SCUMJsonRecord; blockerReason?: string; auditReferences?: string[]; createdAt: string; updatedAt: string; completedAt?: string; }
+7
View File
@@ -5,6 +5,7 @@ export interface PluginPageWorkspaceActions {
listSCUMActivity?: () => Promise<unknown>;
listSCUMGifts?: () => Promise<unknown>;
listSCUMMapPoints?: () => Promise<unknown>;
getSCUMMapGeometry?: () => unknown;
listSCUMSquadMembers?: () => Promise<unknown>;
listSCUMVehicles?: () => Promise<unknown>;
listSCUMFlags?: () => Promise<unknown>;
@@ -15,4 +16,10 @@ export interface PluginPageWorkspaceActions {
listSCUMWorkflows?: () => Promise<unknown>;
createSCUMWorkflow?: (request: unknown) => Promise<unknown>;
listSCUMWorkflowSteps?: (workflowId?: string) => Promise<unknown>;
listGameGiftCatalogs?: () => Promise<unknown>;
saveGameGiftCatalog?: (request: unknown) => Promise<unknown>;
publishGameGiftCatalog?: (catalogId: string) => Promise<unknown>;
listGameGiftGrants?: () => Promise<unknown>;
requestGameGiftGrant?: (request: unknown) => Promise<unknown>;
approveGameGiftGrant?: (grantId: string) => Promise<unknown>;
}
@@ -85,10 +85,10 @@ describe("PluginPageHostPage", () => {
expect(html).not.toMatch(/sessionToken|componentKey|hostPath|dsn|runSocket|credential/i);
});
it("renders the persisted SCUM dataset surface without a browser-side bundle dependency", () => {
it("loads the declared SCUM bundle without a browser-side game branch", () => {
const html = renderToStaticMarkup(<PluginPageHostPage {...props("")} initialPlugin={plugin} />);
expect(html).toContain("未绑定服务器");
expect(html).toContain("正在读取用户同步数据");
expect(html).toContain("正在校验并加载插件页面 bundle");
});
it("remains a manifest-driven host without SCUM component imports or game branches", () => {
@@ -104,7 +104,7 @@ describe("PluginPageHostPage", () => {
expect(hostSource).toContain("listSCUMActivity: () => platformApiClient.listSCUMActivity(serverId)");
expect(hostSource).toContain("listSCUMGifts: () => platformApiClient.listSCUMGifts(serverId)");
expect(hostSource).toContain("listSCUMMapPoints: () => platformApiClient.listSCUMMapPoints(serverId)");
expect(hostSource).toContain("SCUMPersistedDataView");
expect(hostSource).not.toContain("SCUMPersistedDataView");
expect(hostSource).toContain("createSCUMWorkflow: (request) => platformApiClient.createSCUMWorkflow(serverId, request as never)");
expect(hostSource).toContain("createSCUMOperation: (request) => platformApiClient.createSCUMOperation(serverId, request as never)");
expect(hostSource).toContain("listSCUMWorkflowSteps: (workflowId) => platformApiClient.listSCUMWorkflowSteps(serverId, workflowId)");
+14 -11
View File
@@ -5,7 +5,6 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ComponentType }
import { platformApiClient } from "../api/client";
import type { GamePluginResponse } from "../api/types";
import { PageFrame } from "../components/PageFrame";
import { SCUMPersistedDataView, scumDatasetForPluginRoute } from "../components/SCUMPersistedDataView";
import { EmptyState, ErrorState, LoadingState } from "../components/StateViews";
import type { PageComponentProps } from "../contracts/page";
import { pluginBridgeManifestContractFromResponse } from "../contracts/pluginBridge";
@@ -76,6 +75,7 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
listSCUMActivity: () => platformApiClient.listSCUMActivity(serverId),
listSCUMGifts: () => platformApiClient.listSCUMGifts(serverId),
listSCUMMapPoints: () => platformApiClient.listSCUMMapPoints(serverId),
getSCUMMapGeometry: () => readyPluginRef.current?.mapTrajectories,
listSCUMSquadMembers: () => platformApiClient.listSCUMSquadMembers(serverId),
listSCUMVehicles: () => platformApiClient.listSCUMVehicles(serverId),
listSCUMFlags: () => platformApiClient.listSCUMFlags(serverId),
@@ -85,7 +85,13 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
approveSCUMOperation: (operationId) => platformApiClient.approveSCUMOperation(serverId, operationId),
listSCUMWorkflows: () => platformApiClient.listSCUMWorkflows(serverId),
createSCUMWorkflow: (request) => platformApiClient.createSCUMWorkflow(serverId, request as never),
listSCUMWorkflowSteps: (workflowId) => platformApiClient.listSCUMWorkflowSteps(serverId, workflowId)
listSCUMWorkflowSteps: (workflowId) => platformApiClient.listSCUMWorkflowSteps(serverId, workflowId),
listGameGiftCatalogs: () => platformApiClient.listGameGiftCatalogs(serverId),
saveGameGiftCatalog: (request) => platformApiClient.saveGameGiftCatalog(serverId, request as never),
publishGameGiftCatalog: (catalogId) => platformApiClient.publishGameGiftCatalog(serverId, catalogId),
listGameGiftGrants: () => platformApiClient.listGameGiftGrants(serverId),
requestGameGiftGrant: (request) => platformApiClient.requestGameGiftGrant(serverId, request as never),
approveGameGiftGrant: (grantId) => platformApiClient.approveGameGiftGrant(serverId, grantId)
};
}, [pluginId, serverId]);
const bundleLoadKey = declaredBundlePage ? [declaredBundlePage.bundleKey, declaredBundlePage.bundleVersion, declaredBundlePage.bundleIntegritySha256, declaredBundlePage.path].join(":") : "";
@@ -117,14 +123,12 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
if (!hostContext) {
return <ErrorState title="插件页面不可用" reason="插件页面上下文初始化失败。" />;
}
const scumDataset = state.plugin.serverType === "scum" ? scumDatasetForPluginRoute(routeKey) : null;
if (embedded) {
return (
<>
{scumDataset && <SCUMPersistedDataView serverInstanceId={serverId} dataset={scumDataset} />}
{!scumDataset && bundleError && <ErrorState title="插件页面不可用" reason={bundleError} />}
{!scumDataset && !bundle && !bundleError && <LoadingState label="正在校验并加载插件页面 bundle…" compact />}
{!scumDataset && bundle && React.createElement(bundle, { context: hostContext, workspaceActions, availability })}
{bundleError && <ErrorState title="插件页面不可用" reason={bundleError} />}
{!bundle && !bundleError && <LoadingState label="正在校验并加载插件页面 bundle…" compact />}
{bundle && React.createElement(bundle, { context: hostContext, workspaceActions, availability })}
</>
);
}
@@ -152,10 +156,9 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
<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>
{scumDataset && <SCUMPersistedDataView serverInstanceId={serverId} dataset={scumDataset} />}
{!scumDataset && bundleError && <ErrorState title="插件页面不可用" reason={bundleError} />}
{!scumDataset && !bundle && !bundleError && <LoadingState label="正在校验并加载插件页面 bundle…" />}
{!scumDataset && bundle && React.createElement(bundle, { context: hostContext, workspaceActions, availability })}
{bundleError && <ErrorState title="插件页面不可用" reason={bundleError} />}
{!bundle && !bundleError && <LoadingState label="正在校验并加载插件页面 bundle…" />}
{bundle && React.createElement(bundle, { context: hostContext, workspaceActions, availability })}
</div>
);
}