// External v1 场景生成路由的本地真实栈受理 smoke。 // // 用法: // E2E_API_BASE 默认 http://127.0.0.1:4198 // E2E_EXTERNAL_API_KEY 可选;不给时脚本在 dev 环境注册一个账号并通过 // /api/profile/api-keys 现建一个 Key(dev 密码登录会自动注册)。 // // 覆盖:非法 Key 401 → 缺幂等键 400 → 空 sceneContent 400 → custom 缺 customStyle 400 → // 合法请求 202 且返回 operationId → 同键重放返回同一 operationId → 同键换请求体 409 → // 按 operationId 查一次生成状态(Provider 出图按环境可用性如实记录,不作为受理判据)。 const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:4198'; const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' }; const DEV_PASSWORD = 'GenE2e123!'; let failures = 0; function check(name, ok, detail = '') { if (!ok) failures += 1; console.log( `${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` :: ${detail}` : ''}`, ); } async function api(pathname, options = {}) { const { method = 'GET', token, body, headers = {}, externalKey } = options; const finalHeaders = { ...ENVELOPE, ...headers }; if (token) finalHeaders.Authorization = `Bearer ${token}`; if (externalKey) finalHeaders.Authorization = `Bearer ${externalKey}`; let finalBody; if (body !== undefined) { finalHeaders['Content-Type'] = 'application/json'; finalBody = JSON.stringify(body); } const response = await fetch(`${API}${pathname}`, { method, headers: finalHeaders, body: finalBody, }); const text = await response.text(); let json = null; try { json = JSON.parse(text); } catch { json = null; } return { status: response.status, text, json, data: json?.data, error: json?.error, }; } async function resolveExternalKey() { const provided = (process.env.E2E_EXTERNAL_API_KEY ?? '').trim(); if (provided) return provided; const phone = `133${String(Date.now()).slice(-8)}`; const registered = await api('/api/auth/entry', { method: 'POST', body: { purePhoneNumber: phone, password: DEV_PASSWORD }, }); const token = registered.data?.token; check( 'dev 账号注册/登录成功', registered.status === 200 && Boolean(token), `status=${registered.status}`, ); if (!token) process.exit(1); const created = await api('/api/profile/api-keys', { method: 'POST', token, body: { name: 'E2E 场景路由 smoke' }, }); const apiKey = created.data?.apiKey; check( '创建外部 API Key 成功', created.status === 200 && Boolean(apiKey), `status=${created.status}`, ); if (!apiKey) process.exit(1); return apiKey; } const SCENE_PATH = '/api/external/v1/editor/scenes/generations'; async function main() { const apiKey = await resolveExternalKey(); const unauthorized = await api(SCENE_PATH, { method: 'POST', externalKey: 'tnr_sk_invalid_probe', headers: { 'Idempotency-Key': `scene-smoke-unauthorized-${Date.now()}` }, body: { sceneContent: '夕阳下的海边小镇', stylePreset: 'flat' }, }); check( '非法 API Key 被拒(401)', unauthorized.status === 401, `status=${unauthorized.status}`, ); const stamp = Date.now(); const missingKey = await api(SCENE_PATH, { method: 'POST', externalKey: apiKey, body: { sceneContent: '夕阳下的海边小镇', stylePreset: 'flat' }, }); check( '缺 Idempotency-Key 被拒(400)', missingKey.status === 400, `status=${missingKey.status}`, ); const blankScene = await api(SCENE_PATH, { method: 'POST', externalKey: apiKey, headers: { 'Idempotency-Key': `scene-smoke-blank-${stamp}` }, body: { sceneContent: ' ', stylePreset: 'flat' }, }); check( '空 sceneContent 被拒(400)', blankScene.status === 400, `status=${blankScene.status}`, ); const customWithoutStyle = await api(SCENE_PATH, { method: 'POST', externalKey: apiKey, headers: { 'Idempotency-Key': `scene-smoke-custom-${stamp}` }, body: { sceneContent: '夕阳下的海边小镇', stylePreset: 'custom' }, }); check( 'custom 风格缺 customStyle 被拒(400)', customWithoutStyle.status === 400, `status=${customWithoutStyle.status}`, ); const idempotencyKey = `scene-smoke-${stamp}`; const body = { sceneContent: '夕阳下的海边小镇,远处有灯塔', stylePreset: 'flat', aspectRatio: '16:9', imageSize: '1K', generationInputs: { source: 'ai-game-creator-client' }, }; const accepted = await api(SCENE_PATH, { method: 'POST', externalKey: apiKey, headers: { 'Idempotency-Key': idempotencyKey }, body, }); const operationId = accepted.data?.operationId ?? accepted.data?.operation_id; check( '合法场景请求被受理(202 + operationId)', accepted.status === 202 && Boolean(operationId), `status=${accepted.status} operationId=${operationId ?? ''} msg=${accepted.error?.message ?? ''}`, ); if (!operationId) process.exit(1); const replayed = await api(SCENE_PATH, { method: 'POST', externalKey: apiKey, headers: { 'Idempotency-Key': idempotencyKey }, body, }); check( '同键同请求重放返回同一 operationId', replayed.status === 202 && (replayed.data?.operationId ?? replayed.data?.operation_id) === operationId, `status=${replayed.status} operationId=${replayed.data?.operationId ?? ''}`, ); const conflicting = await api(SCENE_PATH, { method: 'POST', externalKey: apiKey, headers: { 'Idempotency-Key': idempotencyKey }, body: { ...body, sceneContent: '同一个幂等键换了画面内容' }, }); check( '同键换请求体被拒(409)', conflicting.status === 409, `status=${conflicting.status} code=${conflicting.error?.code ?? ''}`, ); let generation = null; for (let attempt = 0; attempt < 12; attempt += 1) { generation = await api(`/api/external/v1/generations/${operationId}`, { externalKey: apiKey, }); const status = generation.data?.status ?? ''; if ( status === 'completed' || status === 'succeeded' || status === 'failed' || status === 'cancelled' ) { break; } await new Promise((resolve) => setTimeout(resolve, 5_000)); } const status = generation?.data?.status ?? 'unknown'; check( '按 operationId 能查到生成状态', generation?.status === 200 && status !== 'unknown', `status=${generation?.status} generationStatus=${status}`, ); const result = generation?.data?.result ?? {}; const assetObjectId = result.assetObjectId ?? result.asset?.assetObjectId; const objectKey = result.asset?.objectKey ?? result.objectKey ?? ''; const providerProducedImage = status === 'completed' && Boolean(assetObjectId); check( 'Provider 出图成功并返回素材引用(completed + assetObjectId)', providerProducedImage, `generationStatus=${status} assetObjectId=${assetObjectId ?? ''}`, ); if (providerProducedImage && objectKey) { const readUrl = await api( `/api/external/v1/assets/read-url?objectKey=${encodeURIComponent(objectKey)}`, { externalKey: apiKey }, ); const signedUrl = readUrl.data?.read?.signedUrl ?? readUrl.data?.signedUrl; const imageResponse = signedUrl ? await fetch(signedUrl) : null; const contentType = imageResponse?.headers.get('content-type') ?? ''; check( '出图素材能通过 external read-url 换签名地址下载', readUrl.status === 200 && Boolean(signedUrl) && imageResponse?.status === 200 && contentType.startsWith('image/'), `readUrl=${readUrl.status} objectKey=${objectKey} download=${imageResponse?.status ?? 'n/a'} type=${contentType}`, ); } console.log( `\nProvider 出图:${ providerProducedImage ? `已验证(assetObjectId=${assetObjectId}${objectKey ? ` objectKey=${objectKey}` : ''})` : `未验证(当前 generationStatus=${status},本机图像 Provider 不可用时属环境限制)` }`, ); console.log(`生成状态原始响应:${generation?.text?.slice(0, 400) ?? ''}`); console.log(`\n${failures === 0 ? '全部通过' : `${failures} 项失败`}`); process.exit(failures === 0 ? 0 : 1); } main().catch((error) => { console.error( `[check-external-v1-scene-generation-smoke] 运行失败:${error}`, ); process.exit(1); });