feat(ai-image): AI生图开发

This commit is contained in:
2026-09-27 22:40:52 +08:00
parent e670d06ca6
commit a29dd92676
105 changed files with 9943 additions and 361 deletions
+350
View File
@@ -0,0 +1,350 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import sharp from 'sharp';
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import { createMediaAiEditRequestSchema } from '@atlas/contracts';
import { classifyAiEditFailure, normalizeAiEditOutput, pinnedLookup, providerImageResult, runMediaAiEdit } from '../src/media-ai-edit.ts';
import { buildMediaAiPrompt } from '../src/media-ai-prompt.ts';
const productId = '733d0d73-3d39-4a98-ac0a-5c0e521d8998';
test('仅扩图在供应商改写中心后仍把原图区域覆盖回结果', async () => {
const source = await sharp({ create: { width: 600, height: 600, channels: 3, background: '#e1252a' } }).png().toBuffer();
const generated = await sharp({ create: { width: 1152, height: 1536, channels: 3, background: '#2055d8' } }).png().toBuffer();
const settings = createMediaAiEditRequestSchema.parse({ product_id: productId, mode: 'outpaint' });
const output = await normalizeAiEditOutput(generated, settings, source);
const { data, info } = await sharp(output).raw().toBuffer({ resolveWithObject: true });
assert.equal(info.width, 1152);
assert.equal(info.height, 1536);
const pixel = (x, y) => [...data.subarray((y * info.width + x) * 3, (y * info.width + x) * 3 + 3)];
assert.ok(pixel(576, 768)[0] > 200, '原图中心仍应为红色');
assert.ok(pixel(576, 20)[2] > 170, '扩充边缘仍应为蓝色');
});
test('提示词明确区分主图、辅助图和尺寸数值', () => {
const settings = createMediaAiEditRequestSchema.parse({
product_id: productId,
reference_asset_ids: ['50ea00b7-6675-4cc7-b7a2-d45f05c07100'],
dimensions: { length: 12, unit: 'cm' }, generate_size_chart: true,
});
const prompt = buildMediaAiPrompt(settings);
assert.match(prompt, /Image 1 is the fixed main product image/);
assert.match(prompt, /Images 2 through 2 are auxiliary references only/);
assert.match(prompt, /length: 12 cm/);
assert.match(prompt, /Do not infer other measurements/);
});
test('Pcore 完成结果的空 b64_json 回退到可下载 URL', () => {
const url = 'https://pcore.ai/v1/images/content/task-example/0';
assert.equal(providerImageResult({ data: [{ b64_json: '', url }] }), url);
assert.equal(providerImageResult({ data: [{ b64_json: ' ', url }] }), url);
assert.deepEqual(providerImageResult({ data: [{ b64_json: '', b64_image: Buffer.from('image').toString('base64'), url }] }),
Buffer.from('image'));
assert.deepEqual(providerImageResult({ data: [{ b64_json: Buffer.from('image').toString('base64'), url }] }),
Buffer.from('image'));
});
test('固定 DNS 回调按 Node 的 all 选项返回地址列表或单个地址', () => {
const lookup = pinnedLookup('8.8.8.8');
lookup('pcore.ai', { all: true }, (error, addresses) => {
assert.equal(error, null);
assert.deepEqual(addresses, [{ address: '8.8.8.8', family: 4 }]);
});
lookup('pcore.ai', { all: false }, (error, address, family) => {
assert.equal(error, null);
assert.equal(address, '8.8.8.8');
assert.equal(family, 4);
});
});
test('异步供应商任务分两次 Outbox 推进,并把结果登记为独立素材', async () => {
const sourceId = '664d0d73-3d39-4a98-ac0a-5c0e521d8998';
const referenceId = '6ebfe347-bc77-4270-8213-b2021271c6b2';
const outputId = '113d0d73-3d39-4a98-ac0a-5c0e521d8998';
const editId = '68db226f-3fcb-4579-ab9e-4932b1d84067';
const source = await sharp({ create: { width: 600, height: 800, channels: 3, background: '#ad3222' } }).jpeg().toBuffer();
const reference = await sharp({ create: { width: 400, height: 400, channels: 4, background: '#2452af80' } }).png().toBuffer();
const generated = await sharp({ create: { width: 1152, height: 1536, channels: 3, background: '#2f72b7' } }).png().toBuffer();
const settings = createMediaAiEditRequestSchema.parse({ product_id: productId,
reference_asset_ids: [referenceId], prompt: '保留商品,使用明亮背景' });
const task = { status: 'queued', source_asset_id: sourceId, reference_asset_ids: [referenceId], settings,
provider_task_id: null, created_at: new Date() };
const outbox = [];
const stored = [];
const database = { workspaceTransaction: async (_workspaceId, work) => work({
query: async (sql, params = []) => {
if (sql.includes('SELECT status, source_asset_id, reference_asset_ids')) return { rows: [{ ...task }] };
if (sql.includes('SELECT id, storage_provider, bucket, object_key')) return { rows: [
{ id: sourceId, storage_provider: 'local_minio', bucket: 'atlas-assets', object_key: 'main.jpg' },
{ id: referenceId, storage_provider: 'local_minio', bucket: 'atlas-assets', object_key: 'reference.jpg' },
] };
if (sql.includes('SELECT status FROM atlas.media_ai_edits')) return { rows: [{ status: task.status }] };
if (sql.includes('SELECT id FROM atlas.media_assets WHERE checksum_sha256')) return { rows: [] };
if (sql.includes('INSERT INTO atlas.media_assets')) return { rows: [{ id: outputId }] };
if (sql.includes('INSERT INTO atlas.outbox_events')) outbox.push(sql);
if (sql.includes("SET status = 'running'")) task.status = 'running';
if (sql.includes('SET provider_task_id =')) task.provider_task_id = params[1];
if (sql.includes("SET status = 'succeeded'")) task.status = 'succeeded';
return { rows: [] };
},
}) };
const storage = { send: async (command) => {
if (command instanceof GetObjectCommand) return { Body: (async function* () {
yield command.input.Key === 'main.jpg' ? source : reference;
})() };
if (command instanceof PutObjectCommand) { stored.push(command.input); return {}; }
throw new Error('unexpected object storage command');
} };
const oldFetch = globalThis.fetch;
const oldBase = process.env.PCORE_BASE_URL;
const oldKey = process.env.PCORE_API_KEY;
process.env.PCORE_BASE_URL = 'https://pcore.ai/v1';
process.env.PCORE_API_KEY = 'test-key';
let requests = 0;
globalThis.fetch = async (url, init) => {
requests += 1;
if (init.method === 'POST') {
assert.equal(url, 'https://pcore.ai/v1/images/edits');
assert.equal(init.headers['Content-Type'], 'application/json');
const body = JSON.parse(init.body);
assert.equal(body.model, 'gpt-image-2');
assert.equal(body.size, '1152x1536');
assert.equal(body.n, 1);
assert.equal(body.async, true);
assert.equal(body.images.length, 2);
assert.deepEqual(body.images, [
{ image_url: `data:image/jpeg;base64,${source.toString('base64')}` },
{ image_url: `data:image/png;base64,${reference.toString('base64')}` },
]);
assert.equal(body.mask, undefined);
return Response.json({ task_id: 'provider-123' });
}
assert.equal(url, 'https://pcore.ai/v1/images/edits/provider-123');
return Response.json({ status: 'completed', data: [{ b64_json: generated.toString('base64') }] });
};
try {
const job = { workspaceId: '30cf54b7-06a3-4e5f-a5fc-d74165115094', payload: { edit_id: editId } };
await runMediaAiEdit(database, storage, job);
assert.equal(task.status, 'running');
assert.equal(task.provider_task_id, 'provider-123');
assert.equal(outbox.length, 1);
await runMediaAiEdit(database, storage, job);
assert.equal(task.status, 'succeeded');
assert.equal(stored.length, 1);
assert.equal(stored[0].ContentType, 'image/jpeg');
assert.equal(requests, 2);
} finally {
globalThis.fetch = oldFetch;
if (oldBase === undefined) delete process.env.PCORE_BASE_URL; else process.env.PCORE_BASE_URL = oldBase;
if (oldKey === undefined) delete process.env.PCORE_API_KEY; else process.env.PCORE_API_KEY = oldKey;
}
});
test('仅扩图以 JSON 图片和蒙版对象提交 Pcore', async () => {
const sourceId = '664d0d73-3d39-4a98-ac0a-5c0e521d8998';
const editId = '68db226f-3fcb-4579-ab9e-4932b1d84067';
const source = await sharp({ create: { width: 600, height: 600, channels: 3, background: '#ad3222' } }).jpeg().toBuffer();
const settings = createMediaAiEditRequestSchema.parse({ product_id: productId, mode: 'outpaint' });
const database = { workspaceTransaction: async (_workspaceId, work) => work({
query: async (sql) => {
if (sql.includes('SELECT status, source_asset_id, reference_asset_ids')) return { rows: [{
status: 'queued', source_asset_id: sourceId, reference_asset_ids: [], settings,
provider_task_id: null, created_at: new Date(),
}] };
if (sql.includes('SELECT id, storage_provider, bucket, object_key')) return { rows: [{
id: sourceId, storage_provider: 'local_minio', bucket: 'atlas-assets', object_key: 'main.jpg',
}] };
return { rows: [] };
},
}) };
const storage = { send: async () => ({ Body: (async function* () { yield source; })() }) };
const oldFetch = globalThis.fetch;
const oldBase = process.env.PCORE_BASE_URL;
const oldKey = process.env.PCORE_API_KEY;
process.env.PCORE_BASE_URL = 'https://pcore.ai';
process.env.PCORE_API_KEY = 'test-key';
globalThis.fetch = async (_url, init) => {
const body = JSON.parse(init.body);
assert.equal(init.headers['Content-Type'], 'application/json');
assert.equal(body.images.length, 1);
assert.match(body.images[0].image_url, /^data:image\/png;base64,/);
assert.match(body.mask.image_url, /^data:image\/png;base64,/);
const image = Buffer.from(body.images[0].image_url.split(',')[1], 'base64');
const mask = Buffer.from(body.mask.image_url.split(',')[1], 'base64');
assert.equal((await sharp(image).metadata()).width, 1152);
assert.equal((await sharp(mask).metadata()).height, 1536);
return Response.json({ task_id: 'provider-outpaint' });
};
try {
await runMediaAiEdit(database, storage, {
workspaceId: '30cf54b7-06a3-4e5f-a5fc-d74165115094', payload: { edit_id: editId },
});
} finally {
globalThis.fetch = oldFetch;
if (oldBase === undefined) delete process.env.PCORE_BASE_URL; else process.env.PCORE_BASE_URL = oldBase;
if (oldKey === undefined) delete process.env.PCORE_API_KEY; else process.env.PCORE_API_KEY = oldKey;
}
});
test('Pcore 终态失败不重复轮询,泛化错误不会误判成内容拒绝', async () => {
const sourceId = '664d0d73-3d39-4a98-ac0a-5c0e521d8998';
const editId = '68db226f-3fcb-4579-ab9e-4932b1d84067';
const settings = createMediaAiEditRequestSchema.parse({ product_id: productId });
let status = 'running';
let failureCode = null;
let providerErrorRaw = null;
let polls = 0;
let providerError = {
code: 'failed', message: '您的请求无法用于生成图像。该请求可能因安全政策被拦截。',
};
const database = { workspaceTransaction: async (_workspaceId, work) => work({
query: async (sql, params = []) => {
if (sql.includes('SELECT status, source_asset_id, reference_asset_ids')) return { rows: [{
status, source_asset_id: sourceId, reference_asset_ids: [], settings,
provider_task_id: 'provider-123', created_at: new Date(),
}] };
if (sql.includes('SELECT id, storage_provider, bucket, object_key')) return { rows: [{
id: sourceId, storage_provider: 'local_minio', bucket: 'atlas-assets', object_key: 'main.jpg',
}] };
if (sql.includes("SET status = 'failed'")) {
status = 'failed';
failureCode = params[1];
if (sql.includes('atlas.operations')) {
providerErrorRaw = params[6];
assert.equal(params[3], 'pcore');
assert.equal(params[4], 'provider_polling');
}
}
return { rows: [] };
},
}) };
const oldFetch = globalThis.fetch;
const oldBase = process.env.PCORE_BASE_URL;
const oldKey = process.env.PCORE_API_KEY;
process.env.PCORE_BASE_URL = 'https://pcore.ai';
process.env.PCORE_API_KEY = 'test-key';
globalThis.fetch = async (url) => {
polls += 1;
assert.equal(url, 'https://pcore.ai/v1/images/edits/provider-123');
return Response.json({ status: 'failed', error: providerError });
};
try {
const job = { workspaceId: '30cf54b7-06a3-4e5f-a5fc-d74165115094', payload: { edit_id: editId } };
await runMediaAiEdit(database, {}, job);
assert.equal(status, 'failed');
assert.equal(failureCode, 'AI_IMAGE_PROVIDER_FAILED');
assert.equal(providerErrorRaw, JSON.stringify(providerError));
assert.equal(polls, 1);
status = 'running';
providerError = { code: 'content_policy_violation', message: 'The image was blocked.' };
await runMediaAiEdit(database, {}, job);
assert.equal(failureCode, 'AI_IMAGE_CONTENT_REJECTED');
assert.equal(providerErrorRaw, JSON.stringify(providerError));
assert.equal(polls, 2);
} finally {
globalThis.fetch = oldFetch;
if (oldBase === undefined) delete process.env.PCORE_BASE_URL; else process.env.PCORE_BASE_URL = oldBase;
if (oldKey === undefined) delete process.env.PCORE_API_KEY; else process.env.PCORE_API_KEY = oldKey;
}
});
test('Pcore 提交时返回错误体会保留原始内容', async () => {
const sourceId = '664d0d73-3d39-4a98-ac0a-5c0e521d8998';
const editId = '68db226f-3fcb-4579-ab9e-4932b1d84067';
const source = await sharp({ create: { width: 600, height: 800, channels: 3, background: '#ad3222' } }).jpeg().toBuffer();
const settings = createMediaAiEditRequestSchema.parse({ product_id: productId });
const database = { workspaceTransaction: async (_workspaceId, work) => work({
query: async (sql) => {
if (sql.includes('SELECT status, source_asset_id, reference_asset_ids')) return { rows: [{
status: 'queued', source_asset_id: sourceId, reference_asset_ids: [], settings,
provider_task_id: null, created_at: new Date(),
}] };
if (sql.includes('SELECT id, storage_provider, bucket, object_key')) return { rows: [{
id: sourceId, storage_provider: 'local_minio', bucket: 'atlas-assets', object_key: 'main.jpg',
}] };
return { rows: [] };
},
}) };
const storage = { send: async () => ({ Body: (async function* () { yield source; })() }) };
const oldFetch = globalThis.fetch;
const oldBase = process.env.PCORE_BASE_URL;
const oldKey = process.env.PCORE_API_KEY;
process.env.PCORE_BASE_URL = 'https://pcore.ai';
process.env.PCORE_API_KEY = 'test-key';
const providerError = { code: 'invalid_request_error', message: '图像文件无效' };
globalThis.fetch = async () => Response.json({ status: 'failed', error: providerError });
try {
const job = { workspaceId: '30cf54b7-06a3-4e5f-a5fc-d74165115094', payload: { edit_id: editId } };
await assert.rejects(runMediaAiEdit(database, storage, job), (error) => {
assert.deepEqual(classifyAiEditFailure(error), {
code: 'AI_IMAGE_GENERATION_FAILED', source: 'pcore', stage: 'provider_submission',
detail: 'Pcore 图片任务请求被拒绝(invalid_request_error 图像文件无效)',
providerErrorRaw: JSON.stringify(providerError),
});
return true;
});
} finally {
globalThis.fetch = oldFetch;
if (oldBase === undefined) delete process.env.PCORE_BASE_URL; else process.env.PCORE_BASE_URL = oldBase;
if (oldKey === undefined) delete process.env.PCORE_API_KEY; else process.env.PCORE_API_KEY = oldKey;
}
});
test('失败诊断区分 Pcore 查询中断、Atlas 图片处理与保存失败', async () => {
const sourceId = '664d0d73-3d39-4a98-ac0a-5c0e521d8998';
const settings = createMediaAiEditRequestSchema.parse({ product_id: productId });
const database = { workspaceTransaction: async (_workspaceId, work) => work({
query: async (sql) => {
if (sql.includes('SELECT status, source_asset_id, reference_asset_ids')) return { rows: [{
status: 'running', source_asset_id: sourceId, reference_asset_ids: [], settings,
provider_task_id: 'provider-123', created_at: new Date(),
}] };
if (sql.includes('SELECT id, storage_provider, bucket, object_key')) return { rows: [{
id: sourceId, storage_provider: 'local_minio', bucket: 'atlas-assets', object_key: 'main.jpg',
}] };
return { rows: [] };
},
}) };
const job = { workspaceId: '30cf54b7-06a3-4e5f-a5fc-d74165115094',
payload: { edit_id: '68db226f-3fcb-4579-ab9e-4932b1d84067' } };
const oldFetch = globalThis.fetch;
const oldBase = process.env.PCORE_BASE_URL;
const oldKey = process.env.PCORE_API_KEY;
process.env.PCORE_BASE_URL = 'https://pcore.ai/v1';
process.env.PCORE_API_KEY = 'test-key';
try {
globalThis.fetch = async () => { throw new TypeError('fetch failed'); };
await assert.rejects(runMediaAiEdit(database, {}, job), (error) => {
assert.deepEqual(classifyAiEditFailure(error), {
code: 'AI_IMAGE_GENERATION_FAILED', source: 'pcore', stage: 'provider_polling',
detail: 'fetch failed', providerErrorRaw: null,
});
return true;
});
globalThis.fetch = async () => Response.json({ status: 'completed',
data: [{ b64_json: Buffer.from('invalid image').toString('base64') }] });
await assert.rejects(runMediaAiEdit(database, {}, job), (error) => {
const failure = classifyAiEditFailure(error);
assert.equal(failure.source, 'atlas');
assert.equal(failure.stage, 'result_processing');
assert.match(failure.detail, /image|format|buffer/i);
return true;
});
const generated = await sharp({ create: { width: 100, height: 100, channels: 3,
background: '#445566' } }).png().toBuffer();
globalThis.fetch = async () => Response.json({ status: 'completed',
data: [{ b64_json: generated.toString('base64') }] });
const storage = { send: async () => { throw new Error('private/object-key.jpg'); } };
await assert.rejects(runMediaAiEdit(database, storage, job), (error) => {
const failure = classifyAiEditFailure(error);
assert.equal(failure.source, 'atlas');
assert.equal(failure.stage, 'result_storage');
assert.equal(failure.detail, 'Error');
return true;
});
} finally {
globalThis.fetch = oldFetch;
if (oldBase === undefined) delete process.env.PCORE_BASE_URL; else process.env.PCORE_BASE_URL = oldBase;
if (oldKey === undefined) delete process.env.PCORE_API_KEY; else process.env.PCORE_API_KEY = oldKey;
}
});