Your Name b6db6a671c
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: upgrade agent workflow generation modes
2026-03-18 22:17:11 +08:00

382 lines
12 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/(auth)/auth";
import { getSystemSetting, upsertMediaAsset } from "@/lib/db/queries";
import { uploadFile } from "@/lib/storage/service";
import {
AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY,
normalizeAgentWorkflowMaterialApiConfig,
} from "@/lib/agents/workflow-material-config";
export const maxDuration = 180;
const querySchema = z.object({
keyword: z.string().trim().min(1).max(64),
source: z.enum(["primary", "secondary"]).optional(),
queryType: z.enum(["sku", "spu"]).optional(),
});
type ExternalImageRecord = {
key?: string;
label?: string;
fileName?: string;
fileSize?: number;
downloadUrl?: string;
};
type ExternalSearchResponse = {
success?: boolean;
data?: {
sku?: string;
spu?: string;
materialId?: string;
productModel?: string;
skuCount?: number;
skuList?: Array<{
skuName?: string;
skuId?: string;
hasWhiteBackground?: boolean;
whiteBackgroundCount?: number;
previewImage?: {
downloadUrl?: string;
fileName?: string;
} | null;
}>;
whiteBackground?: {
fixed?: ExternalImageRecord[];
other?: ExternalImageRecord[];
};
downloadUrls?: {
zip?: string;
};
};
error?: {
code?: string;
message?: string;
};
};
function normalizeBaseUrl(value: string) {
return String(value || "").trim().replace(/\/+$/, "");
}
function sanitizeFilenamePart(value: string) {
return String(value || "")
.trim()
.replace(/[\\/:*?"<>|]+/g, "-")
.replace(/\s+/g, "-")
.slice(0, 80);
}
function getExtensionFromMimeType(mimeType: string) {
const normalized = String(mimeType || "").toLowerCase();
if (normalized.includes("png")) return "png";
if (normalized.includes("webp")) return "webp";
if (normalized.includes("jpeg") || normalized.includes("jpg")) return "jpg";
if (normalized.includes("gif")) return "gif";
return "png";
}
function ensureFilenameWithExtension(filename: string, mimeType: string) {
const trimmed = sanitizeFilenamePart(filename) || `workflow-material-${Date.now()}`;
if (/\.[a-z0-9]+$/i.test(trimmed)) {
return trimmed;
}
return `${trimmed}.${getExtensionFromMimeType(mimeType)}`;
}
async function importExternalImage(params: {
userId: string;
keyword: string;
source: "primary" | "secondary";
matchedSku: string;
productModel?: string | null;
group: "fixed" | "other";
sortOrder: number;
image: ExternalImageRecord;
}) {
const downloadUrl = String(params.image.downloadUrl || "").trim();
if (!downloadUrl) return null;
const imageResponse = await fetch(downloadUrl, {
cache: "no-store",
redirect: "follow",
});
if (!imageResponse.ok) {
throw new Error(`下载图片失败(${imageResponse.status})`);
}
const imageBuffer = await imageResponse.arrayBuffer();
const mimeType =
String(imageResponse.headers.get("content-type") || "").split(";")[0].trim() || "image/png";
const filename = ensureFilenameWithExtension(
params.image.fileName ||
params.image.label ||
`${params.matchedSku || params.keyword}-${params.group}-${params.sortOrder + 1}`,
mimeType
);
const uploaded = await uploadFile(filename, imageBuffer);
let assetId: string | null = null;
try {
const asset = await upsertMediaAsset({
userId: params.userId,
mediaType: "image",
sourceType: "agent_workflow_material_search",
status: "ready",
originalUrl: uploaded.url,
previewUrl: uploaded.url,
thumbnailUrl: uploaded.url,
mimeType: uploaded.contentType || mimeType,
displayName: filename,
sourceTable: "agent_workflow_material_search",
metadata: {
keyword: params.keyword,
source: params.source,
matchedSku: params.matchedSku,
productModel: params.productModel || null,
group: params.group,
externalKey: String(params.image.key || "").trim() || null,
fileSize: Number(params.image.fileSize || 0) || null,
pathname: uploaded.pathname,
},
});
assetId = asset?.id ?? null;
} catch (error) {
console.warn("[Agent Workflow Material Search] Failed to register media asset:", error);
}
return {
id: assetId || `${params.group}-${params.sortOrder + 1}`,
key: String(params.image.key || "").trim() || `${params.group}-${params.sortOrder + 1}`,
label: String(params.image.label || "").trim() || `图片${params.sortOrder + 1}`,
fileName: filename,
imageUrl: uploaded.url,
assetId,
mimeType: uploaded.contentType || mimeType,
group: params.group,
sortOrder: params.sortOrder,
};
}
function mapUpstreamErrorStatus(code: string) {
if (
code === "SKU_NOT_FOUND" ||
code === "SPU_NOT_FOUND" ||
code === "NO_SKUS" ||
code === "WHITE_BACKGROUND_NOT_FOUND"
) {
return 404;
}
if (code === "INVALID_API_KEY") {
return 500;
}
if (code === "SKU_NOT_UNIQUE") {
return 409;
}
return 502;
}
export async function GET(request: NextRequest) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const parsed = querySchema.parse({
keyword: request.nextUrl.searchParams.get("keyword"),
source: request.nextUrl.searchParams.get("source"),
queryType: request.nextUrl.searchParams.get("queryType"),
});
const setting = await getSystemSetting({ key: AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY });
const config = normalizeAgentWorkflowMaterialApiConfig(setting?.value);
if (!config.enabled) {
return NextResponse.json(
{ error: "素材搜索功能未开启,请先在系统设置中启用" },
{ status: 400 }
);
}
if (!config.apiKey) {
return NextResponse.json(
{ error: "素材搜索 API Key 未配置,请先在系统设置中保存" },
{ status: 500 }
);
}
const normalizedKeyword = parsed.keyword.trim().toUpperCase();
const queryType = parsed.queryType === "spu" ? "spu" : "sku";
const upstreamUrl = `${normalizeBaseUrl(
config.baseUrl
)}/standard-model-white-background?resourceType=white-background&${queryType}=${encodeURIComponent(normalizedKeyword)}`;
const upstreamResponse = await fetch(upstreamUrl, {
method: "GET",
headers: {
"X-API-Key": config.apiKey,
},
cache: "no-store",
});
const upstreamResult = (await upstreamResponse.json().catch(() => ({}))) as ExternalSearchResponse;
if (!upstreamResponse.ok || upstreamResult.success !== true || !upstreamResult.data) {
const errorCode = String(upstreamResult.error?.code || "UPSTREAM_ERROR").trim();
const errorMessage =
String(upstreamResult.error?.message || "").trim() ||
`素材搜索失败(${upstreamResponse.status})`;
return NextResponse.json(
{
success: false,
error: {
code: errorCode,
message: errorMessage,
},
},
{ status: mapUpstreamErrorStatus(errorCode) }
);
}
if (queryType === "spu") {
const skuOptions = Array.isArray(upstreamResult.data.skuList)
? upstreamResult.data.skuList
.map((item) => {
const skuName = String(item?.skuName || "").trim();
if (!skuName) return null;
return {
skuName,
skuId: String(item?.skuId || "").trim() || null,
hasWhiteBackground: item?.hasWhiteBackground === true,
whiteBackgroundCount: Number.isFinite(Number(item?.whiteBackgroundCount))
? Math.max(0, Math.min(999, Math.round(Number(item?.whiteBackgroundCount))))
: 0,
previewImageUrl: String(item?.previewImage?.downloadUrl || "").trim() || null,
previewFileName: String(item?.previewImage?.fileName || "").trim() || null,
};
})
.filter(Boolean)
: [];
return NextResponse.json({
success: true,
data: {
query: normalizedKeyword,
queryType: "spu",
matchedSku: "",
matchedSpu: String(upstreamResult.data.spu || normalizedKeyword).trim() || normalizedKeyword,
materialId: String(upstreamResult.data.materialId || "").trim() || null,
productModel: String(upstreamResult.data.spu || normalizedKeyword).trim() || null,
zipUrl: null,
fixedCount: 0,
otherCount: 0,
totalCount: skuOptions.length,
images: [],
skuOptions,
},
});
}
const matchedSku = String(upstreamResult.data.sku || normalizedKeyword).trim() || normalizedKeyword;
const productModel = String(upstreamResult.data.productModel || "").trim() || null;
const fixedImages = Array.isArray(upstreamResult.data.whiteBackground?.fixed)
? upstreamResult.data.whiteBackground?.fixed || []
: [];
const otherImages = Array.isArray(upstreamResult.data.whiteBackground?.other)
? upstreamResult.data.whiteBackground?.other || []
: [];
const importedImages = (
await Promise.all([
...fixedImages.map((image, index) =>
importExternalImage({
userId: session.user.id,
keyword: normalizedKeyword,
source: parsed.source || "primary",
matchedSku,
productModel,
group: "fixed",
sortOrder: index,
image,
}).catch((error) => {
console.warn("[Agent Workflow Material Search] Fixed image import failed:", error);
return null;
})
),
...otherImages.map((image, index) =>
importExternalImage({
userId: session.user.id,
keyword: normalizedKeyword,
source: parsed.source || "primary",
matchedSku,
productModel,
group: "other",
sortOrder: fixedImages.length + index,
image,
}).catch((error) => {
console.warn("[Agent Workflow Material Search] Other image import failed:", error);
return null;
})
),
])
).filter(
(
item
): item is {
id: string;
key: string;
label: string;
fileName: string;
imageUrl: string;
assetId: string | null;
mimeType: string;
group: "fixed" | "other";
sortOrder: number;
} => Boolean(item)
);
if (importedImages.length === 0) {
return NextResponse.json(
{
success: false,
error: {
code: "IMAGE_IMPORT_FAILED",
message: "已获取到素材记录,但图片导入失败,请稍后重试",
},
},
{ status: 502 }
);
}
return NextResponse.json({
success: true,
data: {
query: normalizedKeyword,
queryType: "sku",
matchedSku,
matchedSpu: productModel,
materialId: String(upstreamResult.data.materialId || "").trim() || null,
productModel,
zipUrl: String(upstreamResult.data.downloadUrls?.zip || "").trim() || null,
fixedCount: importedImages.filter((item) => item.group === "fixed").length,
otherCount: importedImages.filter((item) => item.group === "other").length,
totalCount: importedImages.length,
images: importedImages,
skuOptions: [],
},
});
} catch (error) {
console.error("[Agent Workflow Material Search] Error:", error);
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "搜索参数无效", details: error.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: "素材搜索失败,请稍后重试" },
{ status: 500 }
);
}
}