From 72ed4dde839a221251f3c0e4a308d721ec446023 Mon Sep 17 00:00:00 2001 From: john Date: Sun, 19 Jul 2026 19:09:26 +0800 Subject: [PATCH] fix: complete image_make asset delivery --- db.mjs | 2 +- image-make-client.mjs | 3 ++ image-make-client.test.mjs | 45 ++++++++++++++++++++++ mindspace-image-generation-routes.mjs | 2 +- mindspace-image-generation-routes.test.mjs | 10 +++-- mindspace-image-generation.test.mjs | 8 ++++ schema.sql | 2 +- 7 files changed, 66 insertions(+), 6 deletions(-) diff --git a/db.mjs b/db.mjs index e4ff41c..c060316 100644 --- a/db.mjs +++ b/db.mjs @@ -237,7 +237,7 @@ export async function migrateSchema(pool) { await pool.query( `ALTER TABLE h5_assets - MODIFY source_type ENUM('upload', 'chat', 'agent', 'template', 'generated', 'workspace') + MODIFY source_type ENUM('upload', 'chat', 'agent', 'template', 'generated', 'workspace', 'image_make') NOT NULL DEFAULT 'upload'`, ); diff --git a/image-make-client.mjs b/image-make-client.mjs index 81fcda5..318c659 100644 --- a/image-make-client.mjs +++ b/image-make-client.mjs @@ -123,6 +123,9 @@ export function createImageMakeClient({ const deadline = Date.now() + overallTimeout; let job = created; + if (!ACTIVE_STATUSES.has(String(job?.status ?? '')) && !job?.result && created?.status_url) { + job = await safeJson(await request(created.status_url)); + } while (ACTIVE_STATUSES.has(String(job?.status ?? ''))) { if (Date.now() >= deadline) { throw createClientError('IMAGE_MAKE_TIMEOUT', '图片生成超时', { jobId }); diff --git a/image-make-client.test.mjs b/image-make-client.test.mjs index a85755f..7a78590 100644 --- a/image-make-client.test.mjs +++ b/image-make-client.test.mjs @@ -80,6 +80,51 @@ test('image_make client fails closed on checksum mismatch', async () => { ); }); +test('image_make client resolves a completed idempotent reuse through status_url', async () => { + const image = Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WEBPreused')]); + const sha256 = crypto.createHash('sha256').update(image).digest('hex'); + const calls = []; + const client = createImageMakeClient({ + baseUrl: 'http://127.0.0.1:8080', token: 'test-token', sleep: async () => {}, + fetchImpl: async (url) => { + const path = new URL(url).pathname; + calls.push(path); + if (path === '/v1/generation-jobs') { + return Response.json({ + job_id: 'job-reused', + status: 'succeeded', + status_url: '/v1/generation-jobs/job-reused', + }); + } + if (path === '/v1/generation-jobs/job-reused') { + return Response.json({ + job_id: 'job-reused', + status: 'succeeded', + result: { + download_url: '/v1/generation-jobs/job-reused/result', + sha256, + }, + }); + } + if (path.endsWith('/result')) { + return new Response(image, { headers: { 'content-type': 'image/webp' } }); + } + throw new Error(`Unexpected request ${path}`); + }, + }); + const result = await client.generateImage({ + prompt: 'reuse me', + presetId: 'memind_dark_hero', + idempotencyKey: 'imgreq-reused', + }); + assert.equal(result.jobId, 'job-reused'); + assert.deepEqual(calls, [ + '/v1/generation-jobs', + '/v1/generation-jobs/job-reused', + '/v1/generation-jobs/job-reused/result', + ]); +}); + test('image_make env client stays disabled until URL and token are both configured', () => { assert.equal(createImageMakeClientFromEnv({}), null); assert.equal(createImageMakeClientFromEnv({ IMAGE_MAKE_BASE_URL: 'http://127.0.0.1:8080' }), null); diff --git a/mindspace-image-generation-routes.mjs b/mindspace-image-generation-routes.mjs index f25a5fe..51d2af4 100644 --- a/mindspace-image-generation-routes.mjs +++ b/mindspace-image-generation-routes.mjs @@ -28,7 +28,7 @@ async function handleGenerate(req, res, { service, userId, consumerPrefix }) { purpose, prompt, negativePrompt: req.body?.negative_prompt, - consumerRef: `${consumerPrefix}:${req.requestId ?? 'request'}:${purpose}`, + consumerRef: `${consumerPrefix}:${idempotencyKey || req.requestId || 'request'}:${purpose}`, idempotencyKey, }); if (!result.ok) { diff --git a/mindspace-image-generation-routes.test.mjs b/mindspace-image-generation-routes.test.mjs index 83271c9..6003159 100644 --- a/mindspace-image-generation-routes.test.mjs +++ b/mindspace-image-generation-routes.test.mjs @@ -42,7 +42,7 @@ test('image generation route passes authenticated user and idempotency key to th purpose: 'hero', prompt: 'calm library', negativePrompt: undefined, - consumerRef: 'mindspace:req-1:hero', + consumerRef: 'mindspace:imgreq-1:hero', idempotencyKey: 'imgreq-1', }]); } finally { await fixture.close(); } @@ -84,11 +84,15 @@ test('internal image generation route requires its injected secret guard and exp assert.equal(denied.status, 401); const allowed = await fetch(`${fixture.url}/agent/mindspace_image_generate`, { method: 'POST', - headers: { 'content-type': 'application/json', authorization: 'Bearer internal-secret' }, + headers: { + 'content-type': 'application/json', + authorization: 'Bearer internal-secret', + 'idempotency-key': 'page-hero-1', + }, body: JSON.stringify({ user_id: 'user-2', purpose: 'hero', prompt: 'x' }), }); assert.equal(allowed.status, 201); assert.equal(calls[0].userId, 'user-2'); - assert.equal(calls[0].consumerRef, 'mindspace-agent:req-1:hero'); + assert.equal(calls[0].consumerRef, 'mindspace-agent:page-hero-1:hero'); } finally { await fixture.close(); } }); diff --git a/mindspace-image-generation.test.mjs b/mindspace-image-generation.test.mjs index 0ddd621..db6864a 100644 --- a/mindspace-image-generation.test.mjs +++ b/mindspace-image-generation.test.mjs @@ -1,7 +1,15 @@ import assert from 'node:assert/strict'; +import fs from 'node:fs'; import test from 'node:test'; import { createMindSpaceImageGenerationService } from './mindspace-image-generation.mjs'; +test('MindSpace asset schema accepts image_make as a source type', () => { + const schemaSql = fs.readFileSync(new URL('./schema.sql', import.meta.url), 'utf8'); + const dbSource = fs.readFileSync(new URL('./db.mjs', import.meta.url), 'utf8'); + assert.match(schemaSql, /source_type ENUM\([^\n]*'image_make'/); + assert.match(dbSource, /MODIFY source_type ENUM\([^\n]*'image_make'/); +}); + test('MindSpace image generation stays inert while the admin purpose is disabled', async () => { const service = createMindSpaceImageGenerationService({ configService: { diff --git a/schema.sql b/schema.sql index 7144cf7..22e7d5b 100644 --- a/schema.sql +++ b/schema.sql @@ -79,7 +79,7 @@ CREATE TABLE IF NOT EXISTS h5_assets ( risk_level ENUM('none', 'low', 'medium', 'high', 'critical') NOT NULL DEFAULT 'none', visibility ENUM('private', 'internal', 'public_candidate') NOT NULL DEFAULT 'private', status ENUM('uploaded', 'processing', 'ready', 'quarantined', 'archived', 'deleted') NOT NULL DEFAULT 'ready', - source_type ENUM('upload', 'chat', 'agent', 'template', 'generated', 'workspace') NOT NULL DEFAULT 'upload', + source_type ENUM('upload', 'chat', 'agent', 'template', 'generated', 'workspace', 'image_make') NOT NULL DEFAULT 'upload', created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, deleted_at BIGINT NULL,