fix: complete image_make asset delivery

This commit is contained in:
john
2026-07-19 19:09:26 +08:00
parent 32e697b698
commit a0043e95c1
7 changed files with 66 additions and 6 deletions
+1 -1
View File
@@ -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'`,
);
+3
View File
@@ -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 });
+45
View File
@@ -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);
+1 -1
View File
@@ -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) {
+7 -3
View File
@@ -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(); }
});
+8
View File
@@ -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: {
+1 -1
View File
@@ -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,