feat: integrate automatic game resource delivery

This commit is contained in:
2026-09-06 09:50:49 +08:00
parent f721966f3c
commit 4df4bc9624
20 changed files with 1667 additions and 191 deletions

View File

@@ -1,5 +1,5 @@
import { Buffer } from 'node:buffer';
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
import { readFile, stat } from 'node:fs/promises';
import path from 'node:path';
import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins';
import { PiGameAssetTools } from '../../coding-runtime/pi/extensions/game-assets';
@@ -14,6 +14,11 @@ import {
GameResourceClientError,
type GameResourceGeneration,
} from '../../services/game-resource-client';
import type {
GameResourceDeliveryCoordinator,
GameResourceDeliveryProgress,
GameResourceDeliveryResult,
} from '../../services/game-resource-delivery';
import type {
AdapterInvocationResult,
CodingPluginAdapter,
@@ -30,6 +35,7 @@ type Input = Record<string, unknown>;
export interface GameResourcePluginAdapterOptions {
readonly client: GameResourceClient;
readonly delivery: GameResourceDeliveryCoordinator;
readonly marketplace: MarketplacePackageClientPort;
readonly packageStore: Pick<PluginPackageStore, 'getInstalled' | 'getInstalledRelease'>;
readonly makeloreVersion: string;
@@ -89,6 +95,20 @@ function generationData(value: GameResourceGeneration, includeBilling: boolean)
};
}
function deliveryData(
value: GameResourceDeliveryResult | GameResourceDeliveryProgress,
) {
return {
executionId: value.executionId,
providerStatus: value.providerStatus,
deliveryStatus: value.deliveryStatus,
outputCount: value.outputCount,
files: value.files,
...('phase' in value ? { phase: value.phase } : {}),
...(value.errorCode === undefined ? {} : { errorCode: value.errorCode }),
};
}
function clientFailure(error: unknown): AdapterInvocationResult {
if (error instanceof GameResourceClientError) {
return failure(error.code, error.message, error.status, error.retryable);
@@ -191,6 +211,7 @@ export class GameResourcePluginAdapter implements CodingPluginAdapter {
context: TrustedCodingCapabilityContext,
tool: CodingPluginToolDefinition,
input: unknown,
onProgress?: (result: AdapterInvocationResult) => void,
): Promise<AdapterInvocationResult> {
if (!isRecord(input)) return failure('plugin_input_invalid', 'Game-resource tool input is invalid', 422, false);
try {
@@ -215,24 +236,41 @@ export class GameResourcePluginAdapter implements CodingPluginAdapter {
return failure('plugin_input_invalid', 'Game-resource generation input is invalid', 422, false);
}
const admission = await this.admission(context);
const generated = await this.options.client.generate({
...admission,
projectId: context.durableProjectId,
logicalOperationId: context.requestId,
kind,
templateName: input.templateName,
templateConfig: templateConfig(input.templateConfigJson),
requirement: input.requirement,
...(typeof input.aspectRatio === 'string' ? { aspectRatio: input.aspectRatio } : {}),
...(typeof input.temperature === 'number' ? { temperature: input.temperature } : {}),
...(typeof input.jobName === 'string' ? { jobName: input.jobName } : {}),
...(typeof input.modelName === 'string' ? { modelName: input.modelName } : {}),
...(typeof input.resolution === 'string' ? { resolution: input.resolution } : {}),
...(typeof input.hdRemoveBgMode === 'string' ? { hdRemoveBgMode: input.hdRemoveBgMode } : {}),
...(typeof input.threadId === 'string' ? { threadId: input.threadId } : {}),
referenceFiles: await references(context.projectPath, input.referencePaths),
const generated = await this.options.delivery.generateAndMaterialize({
context: {
conversationId: context.conversationId,
runId: context.runId,
localProjectId: context.localProjectId,
durableProjectId: context.durableProjectId,
projectPath: context.projectPath,
logicalOperationId: context.requestId,
},
request: {
...admission,
projectId: context.durableProjectId,
logicalOperationId: context.requestId,
kind,
templateName: input.templateName,
templateConfig: templateConfig(input.templateConfigJson),
requirement: input.requirement,
...(typeof input.aspectRatio === 'string' ? { aspectRatio: input.aspectRatio } : {}),
...(typeof input.temperature === 'number' ? { temperature: input.temperature } : {}),
...(typeof input.jobName === 'string' ? { jobName: input.jobName } : {}),
...(typeof input.modelName === 'string' ? { modelName: input.modelName } : {}),
...(typeof input.resolution === 'string' ? { resolution: input.resolution } : {}),
...(typeof input.hdRemoveBgMode === 'string' ? { hdRemoveBgMode: input.hdRemoveBgMode } : {}),
...(typeof input.threadId === 'string' ? { threadId: input.threadId } : {}),
referenceFiles: await references(context.projectPath, input.referencePaths),
},
...(onProgress ? {
onProgress: (progress) => onProgress(success(
deliveryData(progress),
202,
progress.billing,
)),
} : {}),
});
if (generated.status === 'failed' || generated.status === 'cancelled') {
if (generated.providerStatus === 'failed' || generated.providerStatus === 'cancelled') {
return failure(
generated.errorCode ?? 'game_resource_generation_failed',
'Hosted game-resource generation was rejected',
@@ -242,41 +280,16 @@ export class GameResourcePluginAdapter implements CodingPluginAdapter {
);
}
return success(
generationData(generated, false),
generated.status === 'succeeded' ? 200 : 202,
deliveryData(generated),
generated.deliveryStatus === 'saved' ? 200 : 202,
generated.billing,
);
}
case 'game_resource_status': {
const executionId = this.executionId(input.executionId);
const generated = await this.options.client.get(executionId);
return success(generationData(generated, true));
}
case 'game_resource_cancel': {
const executionId = this.executionId(input.executionId);
const generated = await this.options.client.cancel(executionId);
return success(generationData(generated, true));
}
case 'game_resource_save_output': {
if (input.confirmed !== true) return failure('confirmation_required', 'Explicit save confirmation is required', 400, false);
const executionId = this.executionId(input.executionId);
const outputIndex = input.outputIndex === undefined ? undefined : input.outputIndex;
if (outputIndex !== undefined && (!Number.isSafeInteger(outputIndex) || (outputIndex as number) < 0 || (outputIndex as number) > 99)) {
return failure('plugin_input_invalid', 'Game-resource output index is invalid', 422, false);
}
const target = relativeProjectPath(context.projectPath, input.relativePath);
const content = await this.options.client.download(executionId, outputIndex as number | undefined);
await mkdir(path.dirname(target.absolute), { recursive: true });
try {
await writeFile(target.absolute, content.bytes, { flag: 'wx' });
} catch (error) {
if (isRecord(error) && error.code === 'EEXIST') {
return failure('game_resource_destination_exists', 'Destination file already exists', 409, false);
}
throw error;
}
return success({ savedPath: target.relative, bytes: content.bytes.byteLength });
}
case 'game_asset_browser': {
const result = await this.gameAssets.browse(context.projectPath, input, context.requestId);
const parsed = JSON.parse(result.content[0]?.text ?? '{}') as unknown;