feat(edit): 编辑上报细节优化
This commit is contained in:
@@ -3,7 +3,7 @@ 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 { classifyAiEditFailure, downloadProviderTaskImage, 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';
|
||||
@@ -57,7 +57,60 @@ test('固定 DNS 回调按 Node 的 all 选项返回地址列表或单个地址'
|
||||
});
|
||||
});
|
||||
|
||||
test('异步供应商任务分两次 Outbox 推进,并把结果登记为独立素材', async () => {
|
||||
test('同源 Pcore 任务取图使用配置地址,不发送密钥且禁止重定向', async () => {
|
||||
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';
|
||||
let requests = 0;
|
||||
globalThis.fetch = async (url, init) => {
|
||||
requests += 1;
|
||||
assert.equal(url, 'https://pcore.ai/v1/images/content/provider-123/0');
|
||||
assert.equal(init.redirect, 'error');
|
||||
assert.equal(init.headers, undefined);
|
||||
assert.ok(init.signal);
|
||||
return new Response('image');
|
||||
};
|
||||
try {
|
||||
assert.deepEqual(await downloadProviderTaskImage('https://pcore.ai/v1/images/content/provider-123/0', 'provider-123'), Buffer.from('image'));
|
||||
for (const url of [
|
||||
'https://pcore.ai/v1/images/content/another-task/0',
|
||||
'https://pcore.ai/v1/images/content/provider-123/1',
|
||||
'https://pcore.ai/v1/images/content/provider-123%2F0/0',
|
||||
'https://pcore.ai/v1/images/content/provider-123/0?url=http://localhost',
|
||||
'https://pcore.ai/v1/images/content/provider-123/0#fragment',
|
||||
'https://user:password@pcore.ai/v1/images/content/provider-123/0',
|
||||
'https://pcore.ai/v1/images/edits/provider-123',
|
||||
'https://127.0.0.1/v1/images/content/provider-123/0',
|
||||
'http://pcore.ai/v1/images/content/provider-123/0',
|
||||
]) await assert.rejects(downloadProviderTaskImage(url, 'provider-123'), /结果地址不安全/);
|
||||
assert.equal(requests, 1, '非当前任务或非安全地址不能进入供应商内容请求');
|
||||
await assert.rejects(downloadProviderTaskImage('https://pcore.ai/v1/images/content/../0', '..'), /结果地址不安全/);
|
||||
globalThis.fetch = async () => new Response(null, { status: 302, headers: { Location: 'https://127.0.0.1/' } });
|
||||
await assert.rejects(downloadProviderTaskImage('https://pcore.ai/v1/images/content/provider-123/0', 'provider-123'), /HTTP 302/);
|
||||
globalThis.fetch = async () => new Response('oversize', { headers: { 'Content-Length': String(26 * 1024 * 1024) } });
|
||||
await assert.rejects(downloadProviderTaskImage('https://pcore.ai/v1/images/content/provider-123/0', 'provider-123'), /超过处理上限/);
|
||||
globalThis.fetch = async () => new Response(new ReadableStream({ start(controller) {
|
||||
controller.enqueue(new Uint8Array(25 * 1024 * 1024));
|
||||
controller.enqueue(new Uint8Array(1));
|
||||
controller.close();
|
||||
} }));
|
||||
await assert.rejects(downloadProviderTaskImage('https://pcore.ai/v1/images/content/provider-123/0', 'provider-123'), /超过处理上限/);
|
||||
globalThis.fetch = async () => { throw new TypeError('fetch failed'); };
|
||||
await assert.rejects(downloadProviderTaskImage('https://pcore.ai/v1/images/content/provider-123/0', 'provider-123'), (error) => {
|
||||
assert.equal(classifyAiEditFailure(error).source, 'pcore');
|
||||
assert.equal(error.message, 'Pcore 结果下载连接失败');
|
||||
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('异步供应商任务登记独立素材,完成结果兼容 base64 和同源图片接口', async () => {
|
||||
const sourceId = '664d0d73-3d39-4a98-ac0a-5c0e521d8998';
|
||||
const referenceId = '6ebfe347-bc77-4270-8213-b2021271c6b2';
|
||||
const outputId = '113d0d73-3d39-4a98-ac0a-5c0e521d8998';
|
||||
@@ -101,6 +154,7 @@ test('异步供应商任务分两次 Outbox 推进,并把结果登记为独立
|
||||
process.env.PCORE_BASE_URL = 'https://pcore.ai/v1';
|
||||
process.env.PCORE_API_KEY = 'test-key';
|
||||
let requests = 0;
|
||||
let useContentUrl = false;
|
||||
globalThis.fetch = async (url, init) => {
|
||||
requests += 1;
|
||||
if (init.method === 'POST') {
|
||||
@@ -119,8 +173,15 @@ test('异步供应商任务分两次 Outbox 推进,并把结果登记为独立
|
||||
assert.equal(body.mask, undefined);
|
||||
return Response.json({ task_id: 'provider-123' });
|
||||
}
|
||||
if (url === 'https://pcore.ai/v1/images/content/provider-123/0') {
|
||||
assert.equal(init.headers, undefined);
|
||||
assert.equal(init.redirect, 'error');
|
||||
return new Response(generated);
|
||||
}
|
||||
assert.equal(url, 'https://pcore.ai/v1/images/edits/provider-123');
|
||||
return Response.json({ status: 'completed', data: [{ b64_json: generated.toString('base64') }] });
|
||||
return Response.json({ status: 'completed', data: [useContentUrl
|
||||
? { b64_json: '', url: 'https://pcore.ai/v1/images/content/provider-123/0' }
|
||||
: { b64_json: generated.toString('base64') }] });
|
||||
};
|
||||
try {
|
||||
const job = { workspaceId: '30cf54b7-06a3-4e5f-a5fc-d74165115094', payload: { edit_id: editId } };
|
||||
@@ -133,6 +194,12 @@ test('异步供应商任务分两次 Outbox 推进,并把结果登记为独立
|
||||
assert.equal(stored.length, 1);
|
||||
assert.equal(stored[0].ContentType, 'image/jpeg');
|
||||
assert.equal(requests, 2);
|
||||
task.status = 'running';
|
||||
useContentUrl = true;
|
||||
await runMediaAiEdit(database, storage, job);
|
||||
assert.equal(task.status, 'succeeded');
|
||||
assert.equal(stored.length, 2);
|
||||
assert.equal(requests, 4, '已保存任务只查询状态和取图,不重新提交生成');
|
||||
} finally {
|
||||
globalThis.fetch = oldFetch;
|
||||
if (oldBase === undefined) delete process.env.PCORE_BASE_URL; else process.env.PCORE_BASE_URL = oldBase;
|
||||
|
||||
@@ -178,7 +178,7 @@ test('任务读取原图、写入独立派生素材,并完成持久化状态',
|
||||
assert.equal(outputAssetId, 'output-asset');
|
||||
assert.equal(writes.length, 1);
|
||||
assert.equal(writes[0].Bucket, 'test-bucket');
|
||||
assert.match(writes[0].Key, /^derivatives\/workspace-id\/three-four\/[a-f0-9]{64}\.jpg$/);
|
||||
assert.match(writes[0].Key, /^derivatives\/workspace-id\/three-four\/[a-f0-9]{64}\/[a-f0-9-]{36}\.jpg$/);
|
||||
assert.deepEqual(await sharp(writes[0].Body).metadata().then(({ width, height }) => [width, height]), [1200, 1600]);
|
||||
await runAspectRatioDerivative(database, storage, job);
|
||||
assert.equal(writes.length, 1);
|
||||
|
||||
@@ -2,6 +2,53 @@ import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { deepSeekJson, makeModel, validateCopy, validatePackage, runProductAiTask } from '../src/product-ai.ts';
|
||||
|
||||
test('SKU 用户要求独立进入高优先级输入,来源冲突和其他 SKU 不覆盖当前任务', async () => {
|
||||
const previousFetch = globalThis.fetch;
|
||||
const previousKey = process.env.DEEPSEEK_API_KEY;
|
||||
process.env.DEEPSEEK_API_KEY = 'test-key';
|
||||
let request;
|
||||
let result;
|
||||
const hint = '这是红色款,25cm。标题不要出现包邮。';
|
||||
globalThis.fetch = async (_url, options) => {
|
||||
request = JSON.parse(options.body);
|
||||
return new Response(JSON.stringify({ choices: [{ finish_reason: 'stop', message: { content: JSON.stringify({
|
||||
title: { ru: 'Красная фигурка 25 см', zh: '红色手办 25cm' },
|
||||
tags: [{ ru: '#красная_фигурка', zh: '红色手办' }],
|
||||
}) } }] }), { status: 200 });
|
||||
};
|
||||
const input = {
|
||||
hint, variant_id: 'current', source_material: { title: '蓝色手办包邮 20cm', description: '蓝色', categories: [], attributes: [] },
|
||||
profile: { title: '旧商品文案', tags: [], description: '旧描述' },
|
||||
variants: [
|
||||
{ id: 'current', display_name: '旧蓝色款', specification_values: [{ color: '蓝色' }] },
|
||||
{ id: 'other', display_name: '其他SKU的专属角色', specification_values: [{ role: '其他角色' }] },
|
||||
],
|
||||
};
|
||||
const tx = { query: async (sql, values) => {
|
||||
if (sql.includes('SELECT kind, status')) return { rows: [{ kind: 'sku_copy', status: 'queued', input_snapshot: input }] };
|
||||
if (sql.includes('result = $3::jsonb')) result = JSON.parse(values[2]);
|
||||
return { rows: [] };
|
||||
} };
|
||||
try {
|
||||
await runProductAiTask({ workspaceTransaction: async (_workspace, work) => work(tx) }, { workspaceId: 'workspace', payload: { task_id: 'task' } });
|
||||
assert.match(request.messages[0].content, /user_requirements 优先于采集资料/);
|
||||
assert.match(request.messages[0].content, /冲突时采用用户修正/);
|
||||
assert.match(request.messages[0].content, /用户要求不改变以下输出格式与语言约束/);
|
||||
const user = request.messages[1].content;
|
||||
assert.ok(user.indexOf('user_requirements') < user.indexOf('辅助商品资料'));
|
||||
assert.ok(user.includes(JSON.stringify({ user_requirements: hint })));
|
||||
assert.ok(user.includes('蓝色手办包邮 20cm'));
|
||||
assert.ok(!user.includes('其他SKU的专属角色'));
|
||||
assert.equal((user.match(/user_requirements/g) ?? []).length, 1);
|
||||
assert.equal(result.variant_id, 'current');
|
||||
assert.deepEqual(result.copy.title, { ru: 'Красная фигурка 25 см', zh: '红色手办 25cm' });
|
||||
} finally {
|
||||
globalThis.fetch = previousFetch;
|
||||
if (previousKey === undefined) delete process.env.DEEPSEEK_API_KEY;
|
||||
else process.env.DEEPSEEK_API_KEY = previousKey;
|
||||
}
|
||||
});
|
||||
|
||||
test('同一商品模型只由类目和稳定商品 ID 确定', () => {
|
||||
const category = { name: '手办', path: '玩具 / 手办' };
|
||||
const first = makeModel(category, 'abcdef12-0000-4000-8000-000000000000');
|
||||
|
||||
Reference in New Issue
Block a user