feat: integrate automatic game resource delivery
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user