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; export interface AgentSceneWorkflowOptimizationResult { sceneGoal: string; environmentSummary: string; propSummary: string; cameraLightingSummary: string; characterSummary: string; negativeConstraints: string[]; conflicts: string[]; optimizationModelName?: string | null; optimizationSource?: "configured_qwen3_vl" | "fallback_qwen3_vl" | "disabled"; } function sanitizeShortText(value: unknown, maxLength: number) { return String(value || "").replace(/\s+/g, " ").trim().slice(0, maxLength); } function sanitizeStringList(value: unknown, maxItems: number, maxLength: number) { const items = Array.isArray(value) ? value : []; return Array.from( new Set( items .map((item) => sanitizeShortText(item, maxLength)) .filter(Boolean) .slice(0, maxItems) ) ); } 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 parseOptimizationResult( text: string ): Omit | null { const jsonText = extractJsonObjectText(text); if (!jsonText) { return null; } try { const parsed = JSON.parse(jsonText) as Record; const result = { sceneGoal: sanitizeShortText(parsed.sceneGoal, 60), environmentSummary: sanitizeShortText(parsed.environmentSummary, 120), propSummary: sanitizeShortText(parsed.propSummary, 120), cameraLightingSummary: sanitizeShortText(parsed.cameraLightingSummary, 80), characterSummary: sanitizeShortText(parsed.characterSummary, 80), negativeConstraints: sanitizeStringList(parsed.negativeConstraints, 6, 26), conflicts: sanitizeStringList(parsed.conflicts, 4, 50), }; if ( !result.sceneGoal && !result.environmentSummary && !result.propSummary && !result.cameraLightingSummary && !result.characterSummary && result.negativeConstraints.length === 0 && result.conflicts.length === 0 ) { return null; } return result; } catch (error) { console.warn( "[Agent Scene Workflow Prompt Optimizer] Failed to parse JSON response:", error ); return null; } } function buildOptimizerPrompt(params: { workflowPrompt: string; slotPrompt?: string | null; }) { return [ "你是电商家具场景生图提示词整理器。", "你的任务是把冗长的工作流选项提示词,压缩成更适合 Gemini / Imagen 生图模型执行的结构化摘要。", "你只能做压缩、去重、冲突发现,不能改写主SKU产品身份,不能新增工作流里没有的产品结构、颜色、材质或空间元素。", "不要重复角色、技能、流程、请按选项执行这类管理话术。", "如果有人物要求,只总结人物身份、站位和作用;人物不能压过产品主体。如果没有明确人物要求,characterSummary 返回空字符串。", "negativeConstraints 只保留真正影响出图稳定性的禁止项,例如变形、换色、喧宾夺主、额外主体、结构错误。", "conflicts 只写真正的提示冲突;没有冲突返回空数组。", '只输出 JSON:{"sceneGoal":"","environmentSummary":"","propSummary":"","cameraLightingSummary":"","characterSummary":"","negativeConstraints":[],"conflicts":[]}', "字段要求:sceneGoal 不超过60字;environmentSummary 不超过120字;propSummary 不超过120字;cameraLightingSummary 不超过80字;characterSummary 不超过80字;negativeConstraints 最多6条,每条不超过26字;conflicts 最多4条,每条不超过50字。", `工作流设定:\n${params.workflowPrompt}`, params.slotPrompt ? `模式模板补充:\n${params.slotPrompt}` : "", ] .filter(Boolean) .join("\n\n"); } async function optimizeWithConfiguredQwenVisionModel(params: { prompt: string; primaryImageUrl: string; modelImageUrl?: string | null; }) { const configuredModels = await getEnabledAiModelsWithProvider().catch((error) => { console.warn( "[Agent Scene Workflow Prompt Optimizer] 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 content: Array<{ type: "text" | "image"; text?: string; image?: string }> = [ { type: "text", text: params.prompt, }, { type: "image", image: params.primaryImageUrl, }, ]; if (params.modelImageUrl) { content.push({ type: "image", image: params.modelImageUrl, }); } const { text } = await generateText({ model: languageModel, messages: [ { role: "user", content, }, ], temperature: 0, maxTokens: 320, abortSignal: AbortSignal.timeout(30_000), } as any); const parsed = parseOptimizationResult(text); if (!parsed) { return null; } return { ...parsed, optimizationModelName: configuredQwenVisionModel.model.displayName || configuredQwenVisionModel.model.modelId, optimizationSource: "configured_qwen3_vl" as const, }; } catch (error) { console.warn( "[Agent Scene Workflow Prompt Optimizer] Configured Qwen-VL optimization failed:", error ); return null; } } export async function optimizeAgentSceneWorkflowPrompt(params: { workflowPrompt: string; slotPrompt?: string | null; primaryImageUrl: string; modelImageUrl?: string | null; }): Promise { const workflowPrompt = String(params.workflowPrompt || "").trim(); const primaryImageUrl = String(params.primaryImageUrl || "").trim(); if (!workflowPrompt || !/^https?:\/\//i.test(primaryImageUrl)) { return null; } const prompt = buildOptimizerPrompt({ workflowPrompt, slotPrompt: params.slotPrompt || null, }); const configuredResult = await optimizeWithConfiguredQwenVisionModel({ prompt, primaryImageUrl, modelImageUrl: params.modelImageUrl || null, }); if (configuredResult) { return configuredResult; } try { const fallbackResult = await callQwen3Vl({ imageUrl: primaryImageUrl, prompt, temperature: 0, maxTokens: 320, timeoutMs: 30_000, retries: 1, }); const parsed = parseOptimizationResult(fallbackResult.text); if (!parsed) { return null; } return { ...parsed, optimizationModelName: fallbackResult.model, optimizationSource: "fallback_qwen3_vl", }; } catch (error) { console.warn( "[Agent Scene Workflow Prompt Optimizer] Fallback Qwen3-VL optimization failed:", error ); return null; } }