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

@@ -82,9 +82,10 @@ async function installCodingFirstChatHost(
hostConnection: HostConnection,
featureComplete = false,
managedCapabilities = false,
audioPreview?: { executionId: string; path: string; dataUrl: string },
): Promise<void> {
await electronApp.evaluate(async (_, payload) => {
const { connection, featureComplete, managedCapabilities } = payload;
const { connection, featureComplete, managedCapabilities, audioPreview } = payload;
const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron');
type MainState = {
captured: CapturedRequest[];
@@ -456,6 +457,9 @@ async function installCodingFirstChatHost(
) => {
const path = request.path ?? '';
const method = request.method ?? 'GET';
if (audioPreview && path === '/api/coding/game-audio/' + audioPreview.executionId + '/outputs/0') {
return respond({ path: audioPreview.path, dataUrl: audioPreview.dataUrl });
}
const body = typeof request.body === 'string' && request.body
? JSON.parse(request.body) as Record<string, unknown>
: undefined;
@@ -737,9 +741,71 @@ async function installCodingFirstChatHost(
if (path === '/api/coding/runtime/diagnostics') return respond({ runtime: { revision: { provider: 1, resources: 1 }, workers: [{ conversationId: conversation.id, generation: 1, state: 'running', stage: 'running' }] } });
return respond({ success: false, error: `Unhandled E2E route: ${method} ${path}` }, 404);
});
}, { connection: hostConnection, featureComplete, managedCapabilities });
}, { connection: hostConnection, featureComplete, managedCapabilities, audioPreview });
}
test('saved Game Audio offers click-only playable local preview in Electron', async ({ launchElectronApp }, testInfo) => {
const app = await launchElectronApp({ skipSetup: true });
let page = await getStableWindow(app);
const connection = await page.evaluate(async () => ({
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
}));
const executionId = '00000000-0000-4000-8000-000000000601';
const relative = 'assets/generated/game-audio/' + executionId + '/sound-1.wav';
// Synthetic quarter-second PCM sample, never a claimed Meowa output.
const wav = Buffer.alloc(4044);
wav.write('RIFF', 0); wav.writeUInt32LE(4036, 4); wav.write('WAVEfmt ', 8);
wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22);
wav.writeUInt32LE(8000, 24); wav.writeUInt32LE(16000, 28);
wav.writeUInt16LE(2, 32); wav.writeUInt16LE(16, 34);
wav.write('data', 36); wav.writeUInt32LE(4000, 40);
await installCodingFirstChatHost(app, connection, true, false, {
executionId, path: relative, dataUrl: 'data:audio/wav;base64,' + wav.toString('base64'),
});
await settleSnapshot(app);
await disableCodingEventSource(page);
await page.reload();
page = await getStableWindow(app);
await page.getByTestId('ai-module-option-programming').click();
await page.evaluate(() => { window.location.hash = '/chat'; });
await expect(page.getByTestId('coding-conversation-header')).toBeVisible();
const snapshot = await page.evaluate(async () => {
const response = await window.electron.ipcRenderer.invoke('hostapi:fetch', {
path: '/api/coding/conversations/conversation-pi-first-chat/snapshot', method: 'GET',
}) as { data: { json: { snapshot: Record<string, unknown> } } };
return response.data.json.snapshot;
});
const details = {
schema: 'makelore-capability.v1', plugin_id: 'makelore.game-audio', plugin_version: '1.0.0',
capability_id: 'game-audio.sound', operation: 'generate', request_id: 'pi:r:t',
success: true, status: 200, code: null, error: null, retryable: false, payload_schema: 'game-audio.v1',
billing: { mode: 'platform_metered', status: 'settled', reserved_points: '0.04',
actual_points: '0.04', usage_amount: 4, unit: 'half_second' },
data: { executionId, logicalOperationId: 'pi:r:t', projectId: 'project-e2e',
providerStatus: 'succeeded', deliveryStatus: 'saved', phase: 'saved', outputCount: 1,
files: [{ index: 0, path: relative, bytes: wav.length, mimeType: 'audio/wav' }] },
};
await emitCodingEvent(page, 'snapshot', {
type: 'snapshot', conversationId: 'conversation-pi-first-chat', workerGeneration: 1, seq: 10,
snapshot: { ...snapshot, cursor: { workerGeneration: 1, seq: 10 }, nodes: [{
kind: 'tool', id: 'audio-preview', toolCallId: 'audio-preview', toolName: 'game_sound_generate',
title: '生成游戏音效', status: 'complete', inputText: '', output: [], details,
}] },
});
await expect(page.getByTestId('tool-progress-preview')).toContainText('游戏音频 · 已保存');
await page.getByTestId('coding-process-group').locator('summary').first().click();
await page.locator('[data-node-id="audio-preview"] > summary').click();
await expect(page.locator('audio')).toHaveCount(0);
await page.getByRole('button', { name: '试听 sound-1.wav' }).click();
const player = page.getByLabel('试听 sound-1.wav', { exact: true });
await expect(player).toHaveAttribute('controls', '');
expect(await player.getAttribute('autoplay')).toBeNull();
await expect.poll(() => player.evaluate((element) => (element as HTMLAudioElement).duration)).toBe(0.25);
expect(await player.evaluate((element) => (element as HTMLAudioElement).paused)).toBe(true);
await page.screenshot({ path: testInfo.outputPath('audio-preview.png') });
});
async function readState(electronApp: ElectronApplication): Promise<{
captured: CapturedRequest[];
snapshotPending: boolean;