ai-chatbot/lib/agents/workflow-material-appearance.ts
Your Name 90458511c1
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add workflow appearance recognition controls
2026-04-03 17:36:13 +08:00

174 lines
4.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "server-only";
import { generateText } from "ai";
import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
import { callQwen3Vl } from "@/lib/ai/qwen3-vl";
import { getEnabledAiModelsWithProvider } from "@/lib/db/queries";
const QWEN_VL_MODEL_MATCHER =
/(qwen|千问|通义).*(vl|vision)|qwen3-vl|qwen-vl/i;
function sanitizeAppearanceSummary(value: unknown, maxLength = 220) {
return String(value || "").replace(/\s+/g, " ").trim().slice(0, maxLength);
}
function extractJsonObjectText(text: string): string | null {
const normalized = String(text || "").trim();
if (!normalized) {
return null;
}
const fencedMatch = normalized.match(/```(?:json)?\s*([\s\S]*?)```/i);
if (fencedMatch?.[1]) {
const fencedText = fencedMatch[1].trim();
if (fencedText.startsWith("{") && fencedText.endsWith("}")) {
return fencedText;
}
}
if (normalized.startsWith("{") && normalized.endsWith("}")) {
return normalized;
}
const firstBraceIndex = normalized.indexOf("{");
const lastBraceIndex = normalized.lastIndexOf("}");
if (firstBraceIndex >= 0 && lastBraceIndex > firstBraceIndex) {
return normalized.slice(firstBraceIndex, lastBraceIndex + 1);
}
return null;
}
function parseAppearanceSummary(text: string) {
const jsonText = extractJsonObjectText(text);
if (jsonText) {
try {
const parsed = JSON.parse(jsonText) as Record<string, unknown>;
const summary = sanitizeAppearanceSummary(
parsed.appearanceSummary || parsed.summary || parsed.description
);
if (summary) {
return summary;
}
} catch (error) {
console.warn(
"[Workflow Material Appearance] Failed to parse JSON response:",
error
);
}
}
return sanitizeAppearanceSummary(
String(text || "")
.replace(/```(?:json)?/gi, "")
.replace(/```/g, "")
);
}
function buildAppearancePrompt() {
return [
"你是电商商品白底图识别器。",
"请只基于商品本体,识别并描述产品的外观结构和颜色,用于后续生图时保持产品结构不要改变。",
"描述重点:1. 产品品类与整体轮廓;2. 关键结构件及数量关系,例如层板、格口、门板、抽屉、腿、扶手、靠背、边框、台面、开孔、开口方向;3. 主色、副色与颜色分布。",
"不要分析或描述材质、纹理、表面工艺、木纹、金属感、布艺感,也不要描述背景、光影、拍摄角度、水印、文案,更不要输出营销形容词。",
'只输出 JSON:{"appearanceSummary":"..."}',
"appearanceSummary 必须是中文,长度 60 到 120 字,具体、克制、可直接作为生图约束。",
].join("\n");
}
async function analyzeWithConfiguredQwenVisionModel(params: {
imageUrl: string;
prompt: string;
}) {
const configuredModels = await getEnabledAiModelsWithProvider().catch((error) => {
console.warn(
"[Workflow Material Appearance] Failed to load configured models:",
error
);
return [];
});
const configuredQwenVisionModel = configuredModels.find(
({ model }) =>
model.supportsVision &&
!model.isSubModel &&
QWEN_VL_MODEL_MATCHER.test(`${model.displayName} ${model.modelId}`)
);
if (!configuredQwenVisionModel) {
return null;
}
try {
const languageModel = await getLanguageModelById(
configuredQwenVisionModel.model.id
);
if (!languageModel) {
return null;
}
const { text } = await generateText({
model: languageModel,
messages: [
{
role: "user",
content: [
{
type: "text",
text: params.prompt,
},
{
type: "image",
image: params.imageUrl,
},
],
},
],
temperature: 0,
maxTokens: 220,
abortSignal: AbortSignal.timeout(25_000),
} as any);
return parseAppearanceSummary(text);
} catch (error) {
console.warn(
"[Workflow Material Appearance] Configured Qwen-VL analysis failed:",
error
);
return null;
}
}
export async function analyzeWorkflowMaterialAppearanceFromImage(imageUrl: string) {
const normalizedUrl = String(imageUrl || "").trim();
if (!/^https?:\/\//i.test(normalizedUrl)) {
return null;
}
const prompt = buildAppearancePrompt();
const configuredSummary = await analyzeWithConfiguredQwenVisionModel({
imageUrl: normalizedUrl,
prompt,
});
if (configuredSummary) {
return configuredSummary;
}
try {
const result = await callQwen3Vl({
imageUrl: normalizedUrl,
prompt,
temperature: 0,
maxTokens: 220,
timeoutMs: 25_000,
retries: 1,
});
return parseAppearanceSummary(result.text);
} catch (error) {
console.warn(
"[Workflow Material Appearance] Fallback Qwen3-VL analysis failed:",
error
);
return null;
}
}