feat: add official game audio generation client

This commit is contained in:
2026-09-21 12:17:32 +08:00
parent d222d17500
commit 09e0ce5cf3
28 changed files with 1835 additions and 31 deletions

View File

@@ -48,6 +48,10 @@ import {
import { createDataServicePluginAdapter } from '../coding-plugins/adapters/data-service';
import { createGameResourcePluginAdapter } from '../coding-plugins/adapters/game-resource';
import { GameResourceClient } from '../services/game-resource-client';
import { GameAudioClient } from '../services/game-audio-client';
import { GameAudioDeliveryCoordinator, GameAudioReceiptStore } from '../services/game-audio-delivery';
import { GameAudioPluginAdapter } from '../coding-plugins/adapters/game-audio';
import { MarketplaceHostedAdmissionResolver } from '../coding-plugins/hosted-admission';
import {
GameResourceDeliveryCoordinator,
GameResourceDeliveryReceiptStore,
@@ -339,6 +343,27 @@ export function createCodingComposition(
bundledReleases: CODE_OWNED_OPTIONAL_BUNDLED_RELEASES,
});
const policyClient = options.policyClient ?? new PluginPolicyClient();
const gameAudioClient = new GameAudioClient();
const gameAudioDelivery = new GameAudioDeliveryCoordinator({
client: gameAudioClient,
receipts: new GameAudioReceiptStore(path.join(options.paths.userDataDir, 'coding-runtime', 'game-audio', 'receipts.json')),
leases: projectWriteLeases,
recordTouchedPaths: (conversationId, runId, paths) => productTools.recordTouchedPaths(conversationId, runId, paths),
...(options.acquireBackgroundLease ? { acquireBackgroundLease: options.acquireBackgroundLease } : {}),
});
const gameAudioAdapter = new GameAudioPluginAdapter({
client: gameAudioClient,
delivery: gameAudioDelivery,
admission: new MarketplaceHostedAdmissionResolver({
marketplace: marketplaceClient, packageStore,
makeloreVersion: options.clientVersion ?? '2.0.1',
bundledReleases: CODE_OWNED_OPTIONAL_BUNDLED_RELEASES,
}),
getPricingVersion: () => {
const state = policyClient.getState();
return state.status === 'current' ? state.catalog?.pricing_version?.version_id ?? null : null;
},
});
const knownPluginIds = new Set(pluginDefinitions.map(({ id }) => id));
// Existing user Releases are discovered from the device index at startup;
// unlike the bundled catalog they cannot be enumerated synchronously. Keep
@@ -373,7 +398,7 @@ export function createCodingComposition(
const capabilityRegistry = createCodingCapabilityRegistry({
policyClient,
projectPlugins,
adapters: [dataServiceAdapter, gameResourceAdapter],
adapters: [dataServiceAdapter, gameResourceAdapter, gameAudioAdapter],
definitions: pluginDefinitions,
effectiveResolver,
getDurableProjectId: async (projectPath, localProjectId) => {
@@ -389,7 +414,7 @@ export function createCodingComposition(
projects,
projectPlugins,
policyClient,
adapters: [dataServiceAdapter, gameResourceAdapter],
adapters: [dataServiceAdapter, gameResourceAdapter, gameAudioAdapter],
definitions: pluginDefinitions,
effectiveResolver,
getDefinitions: async () => {
@@ -505,9 +530,11 @@ export function createCodingComposition(
},
});
void gameResourceDelivery.resumePending().catch(() => undefined);
void gameAudioDelivery.resumePending().catch(() => undefined);
const unsubscribeMarketplaceSession = subscribeWorksSquareSession(() => {
void invalidateManagedResources();
void gameResourceDelivery.resumePending().catch(() => undefined);
void gameAudioDelivery.resumePending().catch(() => undefined);
});
const host = createCodingProductHost({
projects,
@@ -526,6 +553,7 @@ export function createCodingComposition(
})
: () => undefined;
return {
gameAudio: gameAudioDelivery,
attachments,
dataService,
devicePackages: devicePackageManager,
@@ -557,6 +585,7 @@ export function createCodingComposition(
async shutdown() {
conversations.dispose();
gameResourceDelivery.dispose();
gameAudioDelivery.dispose();
previewDataSession?.dispose();
if (typeof options.browser.configurePreviewDataSession === 'function') {
options.browser.configurePreviewDataSession(undefined);

View File

@@ -48,6 +48,7 @@ import type {
import type { MarketplaceLibrarySnapshot } from '../coding-plugins/account-plugin-cache';
import type { EffectivePluginResolver } from '../coding-plugins/effective-resolver';
import type { DevicePackageManager } from '../coding-packages/device-package-manager';
import type { GameAudioDeliveryCoordinator } from '../services/game-audio-delivery';
export interface ActiveCodingProject {
id: string;
@@ -65,6 +66,7 @@ export interface CodingProductHost {
}
export interface CodingProductComposition {
gameAudio?: Pick<GameAudioDeliveryCoordinator, 'readSavedOutput'>;
attachments: CodingAttachmentStore;
dataService: DataServiceOperations;
devicePackages: DevicePackageManager;

View File

@@ -4,6 +4,7 @@ import { CodingProjectServiceError } from '../../coding-projects/project-service
import type { HostApiContext } from '../context';
import { parseJsonBody, sendJson } from '../route-utils';
import { CodingProjectPluginServiceError } from '../coding-product-services';
import { GameAudioClientError } from '../../services/game-audio-client';
const PLUGIN_ROUTE = /^\/api\/coding\/plugins\/([^/]+)$/u;
const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9.-]{0,127}$/u;
@@ -96,6 +97,20 @@ export async function handleCodingPluginRoutes(
url: URL,
ctx: HostApiContext,
): Promise<boolean> {
const savedAudio = /^\/api\/coding\/game-audio\/([0-9a-f-]{36})\/outputs\/([0-9])$/u.exec(url.pathname);
if (savedAudio && req.method === 'GET') {
try {
const delivery = ctx.codingProducts?.gameAudio;
if (!delivery) throw new CodingPluginRouteError(503, 'game_audio_unavailable', 'Audio delivery is unavailable');
if (url.search) requestError('Saved audio lookup does not accept query parameters');
sendJson(res, 200, await delivery.readSavedOutput(savedAudio[1], Number(savedAudio[2])));
} catch (error) {
if (error instanceof GameAudioClientError) {
sendJson(res, error.status, { code: error.code, error: error.message });
} else sendError(res, error);
}
return true;
}
if (url.pathname !== '/api/coding/plugins' && !PLUGIN_ROUTE.test(url.pathname)) return false;
const plugins = ctx.codingProducts?.plugins;
if (!plugins) {