feat(plugin): gate pages on companion features

This commit is contained in:
npc0-hue
2026-07-29 09:53:39 +08:00
parent 271e1e684f
commit c9494abc48
16 changed files with 300 additions and 61 deletions
+10
View File
@@ -60,8 +60,11 @@ export interface GameClientBridgePageContractResponse {
commandTypes?: string[];
snapshotTypes?: string[];
queryTemplateKeys?: string[];
featureKeys?: string[];
}
export interface GameClientBridgeFeatureDeclarationResponse { key: string; title: string; permission: string; requiredHandlers?: string[]; requiredEventProducers?: string[]; }
export interface GameClientBridgeCompanionDeclarationResponse {
profileKey: string;
configTemplateKey: string;
@@ -85,6 +88,7 @@ export interface GameClientBridgeManifestResponse {
commandRetentionSeconds: number;
maxCommands: number;
pages?: GameClientBridgePageContractResponse[];
features?: GameClientBridgeFeatureDeclarationResponse[];
companion?: GameClientBridgeCompanionDeclarationResponse;
}
@@ -96,14 +100,19 @@ export interface GameClientBridgeProfileDeclarationResponse {
commandTypes: string[];
snapshotTypes: string[];
queryTemplateKeys: string[];
handlerTypes?: string[];
eventProducerTypes?: string[];
}
export interface GameClientBridgeFeatureAvailabilityResponse { key: string; available: boolean; reason?: string; }
export interface GameClientBridgeStatusResponse {
serverInstanceId: string;
pluginId: string;
available: boolean;
reason?: string;
profiles: GameClientBridgeProfileDeclarationResponse[];
features?: GameClientBridgeFeatureAvailabilityResponse[];
}
export interface GameClientBridgeCommandResultResponse {
@@ -224,6 +233,7 @@ export interface GamePluginPageResponse {
bundleIntegritySha256?: string;
permissions: string[];
bridgeActions?: string[];
featureKeys?: string[];
}
export interface RuntimeDiscoveryProbeResponse {
+3 -1
View File
@@ -83,6 +83,7 @@ export interface PluginPageContract {
bundleIntegritySha256?: string;
permissions?: PluginPermission[];
bridgeActions?: PluginBridgeAction[];
featureKeys?: string[];
}
export interface PluginBridgeManifestContract {
@@ -108,7 +109,8 @@ export function pluginBridgeManifestContractFromResponse(
bundleVersion: page.bundleVersion,
bundleIntegritySha256: page.bundleIntegritySha256,
permissions: page.permissions.filter(isPluginPermission),
bridgeActions: page.bridgeActions?.filter(isPluginBridgeAction)
bridgeActions: page.bridgeActions?.filter(isPluginBridgeAction),
featureKeys: page.featureKeys ? [...page.featureKeys] : []
})),
aiPurposes: [...plugin.aiPurposes]
};
+5 -1
View File
@@ -60,7 +60,11 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin }: Plugin
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 可用性。" }); });
void platformApiClient.getGameClientBridgeStatus(serverId).then((status) => {
const requiredFeatures = declaredBundlePage.featureKeys ?? [];
const unavailable = requiredFeatures.map((key) => status.features?.find((feature) => feature.key === key)).find((feature) => !feature?.available);
if (active) setAvailability(unavailable ? { available: false, reason: unavailable.reason || `功能 ${unavailable.key} 没有兼容的 Companion 实现。` } : { 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]);
+10 -2
View File
@@ -69,7 +69,8 @@ export function parseSafeGameClientBridgeStatus(value: unknown): GameClientBridg
pluginId: string(record.pluginId, "pluginId"),
available: boolean(record.available, "available"),
reason: optionalString(record.reason, "reason"),
profiles: array(record.profiles, "profiles").map(parseProfile)
profiles: array(record.profiles, "profiles").map(parseProfile),
features: array(record.features ?? [], "features").map(parseFeature)
};
}
@@ -136,10 +137,17 @@ function parseProfile(value: unknown): GameClientBridgeProfileDeclarationRespons
reason: optionalString(record.reason, "profile.reason"),
commandTypes: stringArray(record.commandTypes, "profile.commandTypes"),
snapshotTypes: stringArray(record.snapshotTypes, "profile.snapshotTypes"),
queryTemplateKeys: stringArray(record.queryTemplateKeys, "profile.queryTemplateKeys")
queryTemplateKeys: stringArray(record.queryTemplateKeys, "profile.queryTemplateKeys"),
handlerTypes: stringArray(record.handlerTypes ?? [], "profile.handlerTypes"),
eventProducerTypes: stringArray(record.eventProducerTypes ?? [], "profile.eventProducerTypes")
};
}
function parseFeature(value: unknown): { key: string; available: boolean; reason?: string } {
const record = safeObject(value, "Game Client Bridge feature");
return { key: string(record.key, "feature.key"), available: boolean(record.available, "feature.available"), reason: optionalString(record.reason, "feature.reason") };
}
function parseResult(value: unknown): GameClientBridgeCommandResultResponse {
const record = safeObject(value, "Game Client Bridge command result");
const result: GameClientBridgeCommandResultResponse = {