import { createLogger } from "@/lib/observability/logger"; import { geolocation } from "@vercel/functions"; import { convertToModelMessages, createUIMessageStream, generateText, smoothStream, stepCountIs, streamText, } from "ai"; import { eq, sql } from "drizzle-orm"; import { unstable_cache as cache } from "next/cache"; import { after, NextResponse } from "next/server"; import { createResumableStreamContext, type ResumableStreamContext, } from "resumable-stream"; import type { ModelCatalog } from "tokenlens/core"; import { fetchModels } from "tokenlens/fetch"; import { getUsage } from "tokenlens/helpers"; import { auth, type UserType } from "@/app/(auth)/auth"; import type { VisibilityType } from "@/components/visibility-selector"; import { entitlementsByUserType } from "@/lib/ai/entitlements"; import type { ChatModel } from "@/lib/ai/models"; import { type RequestHints, systemPrompt } from "@/lib/ai/prompts"; import { myProvider } from "@/lib/ai/providers"; import { isFalGptImage2EditModelId, submitFalImageTask, } from "@/lib/ai/providers/fal-image"; import { getLanguageModelById } from "@/lib/ai/dynamic-provider"; import { shouldForceInlineVisionInputs } from "@/lib/ai/model-compat"; import { createDocument } from "@/lib/ai/tools/create-document"; import { getWeather } from "@/lib/ai/tools/get-weather"; import { requestSuggestions } from "@/lib/ai/tools/request-suggestions"; import { updateDocument } from "@/lib/ai/tools/update-document"; import { executeDirectVideoGeneration, generateVideo, } from "@/lib/ai/tools/generate-video"; import { inferAgentImageCountFromText } from "@/lib/ai/runtime/image-count-intent"; import { getAgentRequiredUploadCount, normalizeAgentPresetModeConfig, } from "@/lib/agents/preset-mode-config"; import { buildAgentSceneImageRolePrompt } from "@/lib/agents/scene-image-role-prompt"; import { optimizeAgentSceneWorkflowPrompt } from "@/lib/agents/scene-workflow-prompt-optimizer"; import { getEnabledTools } from "@/lib/ai/tools-registry"; import { buildCanvasAgentAnnotationEditPrompt, hasCanvasAgentAnnotationEditIntent, } from "@/lib/canvas/agent-annotation-prompt"; import { getBestAnchorNode, getSelectedMediaNodes, getSelectedSlots, normalizeCanvasAgentContext, } from "@/lib/canvas/agent-context"; import { canApplyCanvasAgentMutations, shouldStreamCanvasAgentShadowOperations, } from "@/lib/canvas/agent-feature-flags"; import { getCanvasAgentFeatureFlagsFromSystemSettings } from "@/lib/canvas/agent-feature-settings"; import { buildPendingCanvasOperation, extractPendingCanvasOperationFromWorkingState, isCanvasOperationCancelText, isCanvasOperationConfirmText, withConfirmedCanvasOperationPlan, } from "@/lib/canvas/agent-confirmation"; import { buildCanvasAgentSlotCreationPlan, planCanvasAgentOperationWithModel, resolveCanvasAgentSlotCreationIntent, type CanvasAgentModelPlannerOutcome, } from "@/lib/canvas/agent-model-planner"; import { buildCanvasAgentExplicitOperationPlan, type CanvasAgentExplicitOperationPlan, } from "@/lib/canvas/agent-explicit-operations"; import { buildCanvasAgentResultViewOperation } from "@/lib/canvas/agent-result-view"; import { buildCanvasAgentComparisonViewPlan, buildCanvasAgentLatestResultViewPlan, hasCanvasAgentComparisonViewIntent, hasCanvasAgentLatestResultViewIntent, } from "@/lib/canvas/agent-view-intent"; import { buildCanvasAgentPanViewPlan, hasCanvasAgentPanViewIntent, } from "@/lib/canvas/agent-view-pan"; import { buildCanvasAgentDirectWritebackPlan } from "@/lib/canvas/agent-direct-writeback"; import { createCanvasCapabilityTools, getCanvasCapabilityToolNames, } from "@/lib/canvas/capabilities/ai-sdk-tools"; import { getCanvasAgentOperationErrorMessage, normalizeCanvasAgentOperationErrorCode, } from "@/lib/canvas/agent-operation-errors"; import { shouldBypassCanvasImagePointsPrecheckForAgentControl } from "@/lib/canvas/agent-client-intent"; import { applyCanvasOperationPlan } from "@/lib/canvas/agent-mutations"; import { isProductionEnvironment } from "@/lib/constants"; import type { CanvasAgentContext, CanvasOperationPlan, CanvasViewOperationPlan, } from "@/types/canvas/agent-protocol"; import { createStreamId, deleteChatById, getChatById, getPointsSystemConfig, getUserById, getMessageCountByUserId, getMessagesByChatId, saveChat, saveMessages, updateMessageById, updateChatLastContextById, getAgentAiModelWithProvider, getAiModelById, getEnabledAiModelsWithProvider, getSubModelsByParentModelId, getAiProviderById, checkUserImageQuota, consumeUserPoints, createAgentPresetExecutionSnapshot, createMessageTargetResolution, createMediaAssetRelation, getAccessibleAgentPresetById, getConversationWorkingStateByChatId, getMediaAssetsByUserAndUrls, getSiteSettings, upsertConversationWorkingState, upsertMediaAsset, } from "@/lib/db/queries"; import { db } from "@/lib/db"; import type { DBMessage } from "@/lib/db/schema"; import { agentPreset as agentPresetTable, generatedImage, message as messageTable, } from "@/lib/db/schema"; import { generateCanvasImage } from "@/lib/canvas/generate-image"; import { detectRequestedStoryboardGrid, isGridConfirmationReply, type PendingGridConfirmation, } from "@/lib/ai/conversation-action-resolver"; import { bootstrapConversationStateProtocol, composeConversationStateProtocol, persistAgentPresetTurn, persistClarificationTurn, setConversationStateProtocolEventObserver, persistResolvedTargetTurn, persistImageWorkflowTurn, persistPromptOnlyTurn, resolveGridConfirmationRequest, resolveTargetExecutionOverride, persistStoryboardConfirmedTurn, persistStoryboardExecutionCompletedTurn, persistStoryboardExecutionStartedTurn, persistStoryboardRevisionTurn, persistStoryboardAssistantTurn, persistVideoGenerationCompletedTurn, persistVideoGenerationFailedTurn, persistVideoGenerationStartedTurn, persistWorkflowCancelledTurn, } from "@/lib/ai/conversation-state-protocol"; import { resolveChatMediaInput } from "@/lib/ai/context-memory/media-input"; import { resolveTargetAssets } from "@/lib/ai/context-memory/target-resolution"; import type { ResolvedChatMediaInput, ResolvedTargetAssets, } from "@/lib/ai/context-memory/types"; import { ChatSDKError } from "@/lib/errors"; import { getFriendlyRateLimitMessage, getRequestErrorMessage, isRateLimitError, } from "@/lib/request-error"; import { resolveClarificationBypass, resolveClarificationMessage, } from "@/lib/ai/runtime/clarification-interruption"; import { shouldBypassCanvasAgentMissingTargetClarification } from "@/lib/ai/runtime/canvas-agent-clarification"; import { buildAgentToolImagesMarkdown, buildAgentToolVideoMarkdown, prependImageModelBadge, } from "@/lib/ai/runtime/agent-media-markdown"; import { extractImageUrlsFromText } from "@/lib/image-url"; import { saveGeneratedImages } from "@/lib/ai/runtime/generated-image-persistence"; import { recordAiModelHealthFailure, recordAiModelHealthSuccess, shouldFailoverAiModelError, shouldFailoverVip98kRequestRejection, } from "@/lib/ai/runtime/model-health"; import type { FalImageEditTaskMetadata } from "@/lib/ai/runtime/fal-image-edit-task"; import { saveAssistantTextOnlyMessage } from "@/lib/ai/runtime/assistant-message-persistence"; import { resolveCanvasAgentClarificationGate } from "@/lib/ai/runtime/middlewares/canvas-agent-clarification-gate"; import { executeDirectAgentBatchImageEditTool, executeDirectAgentImageEditTool, isDirectAgentImageEditServiceError, } from "@/lib/ai/runtime/direct-agent-image-edit-executor"; import { executeDirectAgentImageAnalysis, executeDirectAgentImageGeneration, executeDirectAgentVideoGeneration, } from "@/lib/ai/runtime/direct-agent-media-executors"; import { buildDirectAgentAnalysisAssistantText, buildDirectAgentBatchImageEditAssistantText, buildDirectAgentImageEditAssistantText, buildDirectAgentImageGenerationAssistantText, buildDirectAgentVideoGenerationAssistantText, } from "@/lib/ai/runtime/direct-agent-response-builders"; import { buildDirectAgentAnalyzeCompletedProgress, buildDirectAgentAnalyzeFailedProgress, buildDirectAgentAnalyzePlanningProgress, buildDirectAgentImageContextProgress, buildDirectAgentImageEditCompletedProgress, buildDirectAgentImageEditExecutingProgress, buildDirectAgentImageEditFailedProgress, buildDirectAgentImageEditSelectProgress, buildDirectAgentImageGenerateCompletedProgress, buildDirectAgentImageGenerateExecutingProgress, buildDirectAgentImageGenerateFailedProgress, buildDirectAgentImageGenerateSelectProgress, buildDirectAgentVideoGenerateCompletedProgress, buildDirectAgentVideoGenerateExecutingProgress, buildDirectAgentVideoGenerateFailedProgress, buildDirectAgentVideoGenerateSelectProgress, } from "@/lib/ai/runtime/direct-agent-progress"; import { safePersistDirectAgentAnalyzeTurn, safePersistDirectAgentImageEditTurn, safePersistDirectAgentVideoCompletedTurn, safePersistDirectAgentVideoFailedTurn, } from "@/lib/ai/runtime/direct-agent-working-state"; import { getDirectAgentToolDisplayName, getDirectAgentToolPrefaceFallback, getDirectAgentToolErrorMessage, getDirectAgentToolLabel, getDirectAgentToolTargetMissingMessage, isDirectAgentImageEditToolName, resolveDirectAgentToolContext, shouldRunDirectAgentBatchImageEdit, } from "@/lib/ai/runtime/direct-agent-execution"; import { pruneTaskBoundaryContext, pruneTaskBoundaryHistoryMessages, } from "@/lib/ai/runtime/task-boundary-context-pruner"; import { buildRecentDialogueMemory } from "@/lib/ai/runtime/recent-dialogue-memory"; import { resolveAgentModelSlotDescriptor, upsertAgentModelSlot, type AgentModelSlots, } from "@/lib/ai/runtime/agent-model-slots"; import { isPureGreetingTurnRequest } from "@/lib/ai/runtime/task-boundary-classifier"; import { isCapabilityQuestionTurnRequest } from "@/lib/ai/runtime/task-boundary-text"; import { getProviderHealthSnapshot, recordProviderHealthFailure, recordProviderHealthSuccess, shouldAvoidProvider, } from "@/lib/ai/runtime/provider-health"; import { resolveCanvasAgentRequestPolicy } from "@/lib/ai/runtime/canvas-agent-kernel"; import { arbitrateAgentIntent } from "@/lib/ai/runtime/intent-arbiter"; import { buildAgentRoleIdentityInstruction } from "@/lib/ai/runtime/agent-role-profile"; import { getVideoStoryboardConfig } from "@/lib/video-agent/config"; import { runRuxaNineGridPipeline } from "@/lib/video-agent/pipeline/ruxa-nine-grid-pipeline"; import { buildStoryboardConfirmedAck, executeStoryboardConfirmedWorkflow, handleStoryboardWorkflowFollowup, } from "@/lib/video-agent/storyboard-chat-followup"; import { buildStoryboardPreferenceMemoryUpdate, } from "@/lib/video-agent/storyboard-preference-memory"; import { attachVideoStoryboardTaskMessage, getLatestActiveVideoStoryboardTaskByChatId, markVideoStoryboardAwaitingConfirmation, markVideoStoryboardConfirmed, markVideoStoryboardDraftForRevision, cancelVideoStoryboardTaskByUser, } from "@/lib/video-agent/task-store"; import type { StoryboardModelCandidate } from "@/lib/video-agent/types"; import type { ChatMessage } from "@/lib/types"; import { collectMessageImageAttachments } from "@/lib/chat/message-image-attachments"; import type { AppUsage } from "@/lib/usage"; import { convertToUIMessages, generateUUID } from "@/lib/utils"; import { isAiModelAgentAuto } from "@/lib/db/ai-model-flags"; import type { AgentExecutionPlan, AgentPreparedExecution, } from "@/lib/ai/auto-agent-graph"; import { generateTitleFromUserMessage } from "../../actions"; import { type PostRequestBody, postRequestBodySchema } from "./schema"; import { isAiModelMultimodal } from "@/lib/db/ai-model-flags"; import { createChatApiFailureResponse, getBudgetedTimeout, getChatStreamErrorMessage, OperationTimeoutError, withOperationTimeout, } from "./_lib/errors"; import { applyResolvedTargetImagesToLastUserMessage, buildAutoModeFallbackMessage, buildPromptOnlyOptimizationSeed, buildPromptOnlyReply, collectImageInputsFromMessageParts, convertMessagesWithImages, ensureImageAnalysisSummarySection, ensureStructuredAgentFriendlyMessage, extractMessageImageAssetIds, extractMessageImageUrls, isRemoteImageAccessible, injectHistoryImagesToLastUserMessage, normalizeAssistantImageMarkdown, normalizeResolvedTargetImageUrls, shouldEnrichImageAnalysisText, summarizeTargetImageUrl, } from "./_lib/image-media"; import { buildImageFallbackCandidates, generateImageWithFailoverCandidate, getImageCrossProviderCandidates, IMAGE_FAILOVER_PROVIDER_ALLOWLIST, resolveImageModelRuntimeId, normalizeFailoverErrorMessage, setImageFallbackModelCooldown, shouldCooldownImageFallbackModel, } from "./_lib/image-failover"; import { buildAgentSceneWorkflowPrompt, buildAgentSlotPrompt, buildOptimizedAgentSceneWorkflowPrompt, dedupeAgentSlotPromptAgainstWorkflow, extractCurrentMessageImageParts, getSceneUploadedImageBreakdown, normalizeAgentSupplementPrompt, resolveScenePromptOptimizationImageUrls, type AgentPresetMessageImagePart, } from "./_lib/agent-scene-workflow"; import { analyzeReplacementExecutionPlan, buildAgentReplacementPrompt, isMarketplaceProductDetailReferenceId, normalizeAgentModeDetailReplacementStrategy, normalizeAgentModeReferences, normalizeAgentModeSourceImages, normalizeReplacementResultImage, resolveDirectReplacementModelRef, resolveImagePartsByReferenceExclusion, resolveReplacementReferenceGeometry, resolveReplacementScenePlanningRuntime, } from "./_lib/detail-replacement"; import { selectDetailReplacementGenerationInputs } from "@/lib/ai/detail-replacement-inputs"; import { normalizeAgentReportForDisplay, sanitizeAgentOutputSettingsSection, } from "./_lib/agent-output"; import { consumeGeneratedVideoPoints, getModelCreditCost, } from "./_lib/billing"; import { ANNOTATION_SYSTEM_PROMPT, applyHiddenPromptToMessages, applyOptimizedPromptToLatestUserMessage, convertImageUrlToBase64, hasAnnotatedImage, injectOptimizedPromptSection, } from "./_lib/message-preparation"; import { resolveModelDisplayNameByModelId, resolveModelEntryByRuntimeModelId, resolveModelRefByRuntimeModelId, } from "./_lib/model-resolver"; import { normalizeAssistantPartsForPersistence } from "./_lib/persistence"; import { buildAgentTargetLockPayload, createSseResponse, runBoundedParallel, safeWriteStreamChunk, startStreamKeepalive, type StreamWriter, writeAgentProgressChunk, writeAgentTargetLockChunk, writeAssistantTextChunk, writeCanvasMediaChunk, writeCanvasCapabilityCommandChunk, writeCanvasOperationChunk, writeCanvasViewOperationChunk, writeImageGenerationStatusChunk, writeProtocolDebugChunk, } from "./_lib/stream"; import { extractVideoUrlFromText, getSupportedVideoModesForModel, isImageGenerationModelType, isVideoProviderType, resolveStoryboardModelCandidates, scheduleVideoArchive, selectBestVideoSubModelForMode, type VideoModeType, } from "./_lib/video-models"; const chatApiLogger = createLogger("chat.route"); const INTERNAL_IMAGE_GENERATION_FAILURE_PATTERN = /生成图片失败|图片生成失败|未找到图片数据|未解析到图片|模型未返回生成图片|图片数据不可用|未返回可用图片/i; const DETAIL_REPLACEMENT_MODEL_TEXT_HINT_PATTERN = /自拍|合照|聚会|聚餐|围坐|用餐|试坐|承重|受力|拿手机|手持|人物场景|人物关系|真人演示|人物展示|人像展示|固定模特|用户模特|same\s+model|fixed\s+model|person|people|human/i; function assertGeneratedImageMarkdownHasImage(markdown: string, modelLabel: string) { const normalizedMarkdown = String(markdown || "").trim(); const imageUrls = extractImageUrlsFromText(normalizedMarkdown); if (imageUrls.length > 0) { return normalizedMarkdown; } const reason = INTERNAL_IMAGE_GENERATION_FAILURE_PATTERN.test(normalizedMarkdown) ? normalizedMarkdown : "模型响应未包含可用图片"; throw new Error(`${modelLabel} 未返回可用图片:${reason.slice(0, 160)}`); } function getPublicImageGenerationErrorText(error: unknown) { const message = error instanceof Error ? error.message : String(error || ""); if (!message || INTERNAL_IMAGE_GENERATION_FAILURE_PATTERN.test(message)) { return "图片生成失败,请稍后重试"; } return message; } function replacementScenePlanShouldIncludeModelImage(scenePlan: any) { if (!scenePlan) return false; if (scenePlan.useModelImage === true) return true; if ( scenePlan.peoplePolicy && scenePlan.peoplePolicy !== "none" ) { return true; } if (scenePlan.generationRoute === "people_relation_locked") return true; if (scenePlan.pageKind === "people_scene" || scenePlan.pageKind === "action_poster") { return true; } const text = [ scenePlan.modelImageReason, scenePlan.routeReason, scenePlan.sceneSummary, scenePlan.cameraSummary, scenePlan.placementSummary, scenePlan.preserveSummary, scenePlan.styleSummary, scenePlan.avoidSummary, scenePlan.generationPrompt, ] .filter(Boolean) .join(" "); return DETAIL_REPLACEMENT_MODEL_TEXT_HINT_PATTERN.test(text); } const AGENT_REPLACEMENT_PARALLEL_WORKER_COUNT = Math.max( 1, Math.min(8, Number(process.env.AGENT_REPLACEMENT_PARALLEL_WORKER_COUNT || 6) || 6), ); function writeCanvasOperationFailureChunk(params: { writer: StreamWriter; label: string; projectId: string; operationId: string; operation: string; error: unknown; anchorNodeId?: string | null; mediaItemIds?: string[]; fallbackToCanvasMedia?: boolean; }) { const errorCode = normalizeCanvasAgentOperationErrorCode( (params.error as { code?: unknown } | null)?.code ); const errorMessage = getCanvasAgentOperationErrorMessage({ code: errorCode, fallbackMessage: params.error instanceof Error ? params.error.message : undefined, }); writeCanvasOperationChunk( params.writer, { projectId: params.projectId, operationId: params.operationId, operation: params.operation, status: "failed", createdNodeIds: [], updatedNodeIds: [], createdEdgeIds: [], anchorNodeId: params.anchorNodeId || null, handledCanvasMediaItemIds: [], mediaItemIds: params.mediaItemIds || [], errorCode, errorMessage, fallbackToCanvasMedia: params.fallbackToCanvasMedia, message: errorMessage, }, params.label ); return { errorCode, errorMessage, }; } function writeCanvasOperationShadowChunk(params: { writer: StreamWriter; label: string; projectId: string; operationId: string; operation: string; anchorNodeId?: string | null; mediaItemIds?: string[]; message?: string; }) { writeCanvasOperationChunk( params.writer, { projectId: params.projectId, operationId: params.operationId, operation: params.operation, status: "shadowed", createdNodeIds: [], updatedNodeIds: [], createdEdgeIds: [], anchorNodeId: params.anchorNodeId || null, handledCanvasMediaItemIds: [], mediaItemIds: params.mediaItemIds || [], fallbackToCanvasMedia: true, message: params.message || "已记录画布操作计划(灰度模式,未修改画布)", }, params.label ); } const AGENT_EXECUTION_PREFACE_INTERNAL_PATTERN = /(?:^|\n)\s*(?:#{1,6}\s*)?(?:意图分析|任务规划|需求理解|图片分析|执行计划|输出设置|优化后提示词|使用建议)(?:\s|[::]|$)|【\s*(?:意图分析|任务规划|需求理解|图片分析|执行计划|输出设置|优化后提示词|使用建议)\s*】|提示词|自动优化|模型|提交生成|回写|流程|步骤/; function buildAgentExecutionPrefaceText(params: { friendlyMessage?: string | null; fallback: string; }) { const fallback = String(params.fallback || "").replace(/\s+/g, " ").trim(); const rawMessage = String(params.friendlyMessage || "") .replace(//g, "") .trim(); if (!rawMessage || AGENT_EXECUTION_PREFACE_INTERNAL_PATTERN.test(rawMessage)) { return fallback; } const candidate = rawMessage .split(/\r?\n/) .map((line) => line.replace(/^[-*]\s*/, "").trim()) .find(Boolean); if (!candidate || AGENT_EXECUTION_PREFACE_INTERNAL_PATTERN.test(candidate)) { return fallback; } const firstSentence = candidate.match(/^(.{1,80}?[。!?!?~~])/u)?.[1] || candidate; const compact = firstSentence.replace(/\s+/g, " ").trim().slice(0, 80); return /[。!?!?~~]$/.test(compact) ? compact : `${compact}。`; } async function writeAgentExecutionPrefaceMessage(params: { writer: StreamWriter; chatId: string; label: string; friendlyMessage?: string | null; fallback: string; }) { const text = buildAgentExecutionPrefaceText({ friendlyMessage: params.friendlyMessage, fallback: params.fallback, }); if (!text) return null; const messageId = generateUUID(); writeAssistantTextChunk(params.writer, text, params.label, messageId); try { await saveAssistantTextOnlyMessage({ chatId: params.chatId, messageId, text, }); } catch (error) { chatApiLogger.warn("[Chat API] Failed to persist agent execution preface:", error); } return messageId; } async function replayAssistantMessagesForDuplicateRequest({ chatId, userMessageId, writer, source, waitMs = DUPLICATE_REQUEST_WAIT_MS, }: { chatId: string; userMessageId: string; writer: StreamWriter; source: string; waitMs?: number; }) { const startedAt = Date.now(); const seenAssistantMessageIds = new Set(); while (Date.now() - startedAt < waitMs) { try { const dbMessages = await getMessagesByChatId({ id: chatId }); const userMessageIndex = dbMessages.findIndex((msg) => msg.id === userMessageId); if (userMessageIndex >= 0) { const assistantMessages = dbMessages .slice(userMessageIndex + 1) .filter((msg) => msg.role === "assistant"); if (assistantMessages.length > 0) { for (const assistantMessage of assistantMessages) { if (seenAssistantMessageIds.has(assistantMessage.id)) continue; seenAssistantMessageIds.add(assistantMessage.id); safeWriteStreamChunk( writer, { type: "data-appendMessage", data: JSON.stringify({ id: assistantMessage.id, role: "assistant", parts: assistantMessage.parts, }), }, `${source}:replay`, ); } return; } } } catch (error) { chatApiLogger.warn(`[Chat API] Failed duplicate replay fetch (${source})`, error); } safeWriteStreamChunk( writer, { type: "data-keepalive", data: JSON.stringify({ ts: Date.now(), source }), }, `${source}:keepalive`, ); await sleep(1000); } } type ChatImageParams = NonNullable; type ChatImageSize = NonNullable; type AgentRecommendedImageParams = { aspectRatio?: string; imageSize?: string; imageCount?: number; negativePrompt?: string; promptExtend?: boolean; }; type ChatVideoParams = NonNullable; type ChatVideoParamsDraft = Partial; type AgentRecommendedVideoParams = { mode?: "text-to-video" | "first-frame" | "first-last-frame" | "reference-images"; resolution?: "480p" | "720p" | "1080p"; ratio?: string; duration?: number; generateAudio?: boolean; cameraMove?: string; }; type AgentVideoPlan = { script: string; storyboard: Array<{ shot: number; title: string; duration: number; description: string; prompt: string; camera?: string; }>; finalPrompt: string; negativePrompt?: string; recommendedVideoParams?: AgentRecommendedVideoParams; }; function normalizeChatImageSize(value?: string | null): ChatImageSize | undefined { const normalized = String(value || "").trim().toUpperCase(); const kMatch = normalized.match(/^(\d+(?:\.\d+)?)K$/); if (!kMatch?.[1]) return undefined; const normalizedK = `${Number(kMatch[1]).toString()}K`; if (["0.5K", "1K", "2K", "4K", "8K", "10K", "12K"].includes(normalizedK)) { return normalizedK as ChatImageSize; } return undefined; } function hasUserCustomizedImageParams(params?: PostRequestBody["imageParams"]): boolean { if (!params) return false; const customizedAspectRatio = typeof params.aspectRatio === "string" && params.aspectRatio.trim() && params.aspectRatio !== "auto"; const customizedImageSize = typeof params.imageSize === "string" && params.imageSize.trim() && params.imageSize.toUpperCase() !== "2K"; const customizedImageCount = typeof params.imageCount === "number" && params.imageCount !== 1; const customizedPromptExtend = typeof params.promptExtend === "boolean"; const customizedAdvanced = Boolean( params.size || params.seed !== undefined || params.n || params.negativePrompt || customizedPromptExtend ); return customizedAspectRatio || customizedImageSize || customizedImageCount || customizedAdvanced; } function mergeImageParamsWithAgentRecommendation(params: { incoming?: PostRequestBody["imageParams"]; recommended?: AgentRecommendedImageParams; intent?: "image_edit" | "image_gen" | "vision" | "video_gen" | "chat"; hasUserCustomSettings: boolean; }): PostRequestBody["imageParams"] { const { incoming, recommended, intent, hasUserCustomSettings } = params; const base: ChatImageParams = { ...(incoming || {}) }; if (!recommended) { return Object.keys(base).length > 0 ? base : undefined; } const merged: ChatImageParams = { ...base, negativePrompt: typeof recommended.negativePrompt === "string" && recommended.negativePrompt.trim() ? recommended.negativePrompt.trim() : base.negativePrompt, promptExtend: typeof recommended.promptExtend === "boolean" ? recommended.promptExtend : base.promptExtend, }; if (!hasUserCustomSettings) { if (recommended.aspectRatio) merged.aspectRatio = recommended.aspectRatio; const recommendedImageSize = normalizeChatImageSize(recommended.imageSize); if (recommendedImageSize) merged.imageSize = recommendedImageSize; if (typeof recommended.imageCount === "number") merged.imageCount = recommended.imageCount; } if (intent === "image_edit") { merged.imageCount = 1; if (typeof base.promptExtend !== "boolean") { merged.promptExtend = false; } } return Object.keys(merged).length > 0 ? merged : undefined; } function hasUserCustomizedVideoParams(params?: PostRequestBody["videoParams"]): boolean { if (!params) return false; const defaults = { mode: "text-to-video", resolution: "720p", ratio: "16:9", duration: 5, generateAudio: true, cameraMove: "none", } as const; if (params.mode && params.mode !== defaults.mode) return true; if (params.resolution && params.resolution !== defaults.resolution) return true; if (params.ratio && params.ratio !== defaults.ratio) return true; if (typeof params.duration === "number" && params.duration !== defaults.duration) return true; if (typeof params.generateAudio === "boolean" && params.generateAudio !== defaults.generateAudio) return true; if (typeof params.cameraMove === "string" && params.cameraMove !== defaults.cameraMove) return true; if (typeof params.seed === "number") return true; return false; } function mergeVideoParamsWithAgentRecommendation(params: { incoming?: PostRequestBody["videoParams"]; recommended?: AgentRecommendedVideoParams; hasUserCustomSettings: boolean; }): PostRequestBody["videoParams"] { const { incoming, recommended, hasUserCustomSettings } = params; const base: ChatVideoParamsDraft = { ...(incoming || {}) }; if (!recommended) { return Object.keys(base).length > 0 ? (base as ChatVideoParams) : undefined; } const merged: ChatVideoParamsDraft = { ...base }; if (!hasUserCustomSettings) { if (recommended.mode) merged.mode = recommended.mode; if (recommended.resolution) merged.resolution = recommended.resolution; if (recommended.ratio) merged.ratio = recommended.ratio; if (typeof recommended.duration === "number") merged.duration = recommended.duration; if (typeof recommended.generateAudio === "boolean") { merged.generateAudio = recommended.generateAudio; } if (recommended.cameraMove) merged.cameraMove = recommended.cameraMove; } if (!merged.mode) { merged.mode = recommended.mode || (incoming?.mode as ChatVideoParams["mode"]) || "text-to-video"; } if (!merged.resolution) { merged.resolution = recommended.resolution || (incoming?.resolution as ChatVideoParams["resolution"]) || "720p"; } if (!merged.ratio) { merged.ratio = recommended.ratio || incoming?.ratio || "16:9"; } if (typeof merged.duration !== "number") { merged.duration = recommended.duration || incoming?.duration || 5; } return Object.keys(merged).length > 0 ? (merged as ChatVideoParams) : undefined; } export const maxDuration = 1200; // 20 分钟,支持单模型 10 分钟 + 故障迁移备用模型等待 let globalStreamContext: ResumableStreamContext | null = null; const inFlightChatRequests = new Map(); const IN_FLIGHT_REQUEST_TTL_MS = 5 * 60 * 1000; const DUPLICATE_REQUEST_WAIT_MS = 2 * 60 * 1000; const CHAT_PRE_RESPONSE_SOFT_TIMEOUT_MS = 55_000; const SMART_AUTO_GRAPH_TIMEOUT_MS = 28_000; const SMART_AUTO_FALLBACK_TIMEOUT_MS = 20_000; const SMART_AUTO_IMAGE_SUMMARY_TIMEOUT_MS = 18_000; const DEFAULT_STREAM_KEEPALIVE_INTERVAL_MS = 12_000; const CHAT_CREATION_LOOKUP_RETRY_DELAYS_MS = [60, 180, 360] as const; const DETAIL_REPLACEMENT_PROMPT_PACK_REQUIRED_MESSAGE = "请先在「风格化提示词配置」里选择提示词组合,再执行详情场景替换;缺少提示词组时系统不会处理。"; const CANVAS_AGENT_GREETING_MESSAGE = "你好,我在。你可以直接告诉我想做什么:生图、修图、生成视频、分析图片都可以。"; const CANVAS_AGENT_CAPABILITY_MESSAGE = [ "我主要帮你完成画布里的视觉创作任务:", "", "- 生图:按文字、参考图或画布选中内容生成图片。", "- 修图:换背景、去元素、局部重绘、扩图、增强、抠图、改尺寸。", "- 视频:文生视频、图生视频、分镜/镜头视频需求整理与生成。", "- 图片分析:识别主体、风格、构图、材质、光线,并整理可执行提示词。", "- 画布协作:读取当前画布选择、槽位、最近结果,继续生成、修改或写回画布。", "", "你可以直接说:把这张图换成白底、参考左边图片生成 3 张主图、让这张图动起来。", ].join("\n"); const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function findChatWithRetry(chatId: string) { let currentChat = await getChatById({ id: chatId }); if (currentChat) { return currentChat; } for (const delayMs of CHAT_CREATION_LOOKUP_RETRY_DELAYS_MS) { await sleep(delayMs); currentChat = await getChatById({ id: chatId }); if (currentChat) { return currentChat; } } return null; } const getTokenlensCatalog = cache( async (): Promise => { try { return await fetchModels(); } catch (err) { chatApiLogger.warn("TokenLens: catalog fetch failed, using default catalog", err); return; // tokenlens helpers will fall back to defaultCatalog } }, ["tokenlens-catalog"], { revalidate: 24 * 60 * 60 } // 24 hours ); export function getStreamContext() { if (!globalStreamContext) { try { globalStreamContext = createResumableStreamContext({ waitUntil: after, }); } catch (error: any) { if (error.message.includes("REDIS_URL")) { chatApiLogger.info(" > Resumable streams are disabled due to missing REDIS_URL"); } else { chatApiLogger.error("console_error", error); } } } return globalStreamContext; } export async function POST(request: Request) { let requestBody: PostRequestBody; let requestDedupKey: string | null = null; let acquiredInFlightLock = false; try { const json = await request.json(); chatApiLogger.info("[Chat API] Received request body:", JSON.stringify(json, null, 2)); requestBody = postRequestBodySchema.parse(json); } catch (error) { chatApiLogger.error("[Chat API] Schema validation failed:", error); return new ChatSDKError("bad_request:api").toResponse(); } try { const { id, message, agentPresetId, selectedChatModel, selectedVisibilityType, composerMode, hiddenPrompt, workflowPromptOptimizationEnabled, autoContext, contextPayload, agentModePayload, deepThinkingEnabled, debugProtocol, }: { id: string; message: ChatMessage; agentPresetId?: string; selectedChatModel: ChatModel["id"]; selectedVisibilityType: VisibilityType; composerMode?: "agent" | "image" | "video" | "detail"; hiddenPrompt?: string; workflowPromptOptimizationEnabled?: boolean; autoContext?: string; contextPayload?: PostRequestBody["contextPayload"]; agentModePayload?: PostRequestBody["agentModePayload"]; deepThinkingEnabled?: boolean; debugProtocol?: boolean; } = requestBody; const requestStartedAt = Date.now(); const requestOrigin = (() => { try { return new URL(request.url).origin; } catch { return ""; } })(); const canvasPageRequest = (() => { const referer = request.headers.get("referer"); if (!referer) return { isCanvasPage: false, projectId: "" }; try { const pathname = new URL(referer).pathname; const match = pathname.match(/^\/canvas\/([^/?#]+)/); return { isCanvasPage: pathname === "/canvas" || pathname.startsWith("/canvas/"), projectId: match?.[1] || "", }; } catch { return { isCanvasPage: false, projectId: "" }; } })(); const isCanvasPageRequest = canvasPageRequest.isCanvasPage; const requestTextContent = message.parts .filter((part): part is { type: "text"; text: string } => part.type === "text") .map((part) => part.text) .join(" ") .trim(); const autoContextText = typeof autoContext === "string" ? autoContext.trim() : ""; const initialRequestCanvasAgentContext = normalizeCanvasAgentContext( contextPayload || undefined ); const canvasAgentFeatures = await getCanvasAgentFeatureFlagsFromSystemSettings(); const canApplyCanvasAgentMutation = canApplyCanvasAgentMutations(canvasAgentFeatures); const currentMessageAttachmentCount = message.parts.filter( (part) => part.type === "file" ).length; const hasCanvasControlIntentFastPath = composerMode !== "agent" && (contextPayload?.source === "canvas" || contextPayload?.source === "realtime-canvas" || Boolean(initialRequestCanvasAgentContext) || isCanvasPageRequest) && shouldBypassCanvasImagePointsPrecheckForAgentControl({ text: requestTextContent, activeComposerMode: composerMode, attachmentCount: currentMessageAttachmentCount, hasSkillTag: Boolean(agentPresetId || hiddenPrompt), hasWorkflowContext: Boolean(agentModePayload), agentEnabled: true, }); const inferredCanvasProjectId = contextPayload?.projectId || initialRequestCanvasAgentContext?.projectId || canvasPageRequest.projectId || (isCanvasPageRequest ? id : ""); const requestCanvasAgentContext: CanvasAgentContext | null = initialRequestCanvasAgentContext || (hasCanvasControlIntentFastPath && inferredCanvasProjectId ? { version: 1, source: "canvas", projectId: inferredCanvasProjectId, viewport: { x: 0, y: 0, zoom: 1 }, selectedNodeIds: [], selectedNodes: [], candidateNodes: [], slots: [], annotations: [], focusPolicy: "auto", } : null); const canvasAgentProjectId = contextPayload?.projectId || requestCanvasAgentContext?.projectId || inferredCanvasProjectId || ""; const isCanvasAgentCanvasRequest = contextPayload?.source === "canvas" || (hasCanvasControlIntentFastPath && Boolean(canvasAgentProjectId)); if (requestCanvasAgentContext) { chatApiLogger.info("[CanvasAgentContext] Normalized request context:", { source: requestCanvasAgentContext.source, projectId: requestCanvasAgentContext.projectId || null, selectedNodeCount: requestCanvasAgentContext.selectedNodes.length, candidateNodeCount: requestCanvasAgentContext.candidateNodes.length, slotCount: requestCanvasAgentContext.slots.length, annotationCount: requestCanvasAgentContext.annotations.length, focusPolicy: requestCanvasAgentContext.focusPolicy, writebackMode: canvasAgentFeatures.writebackMode, }); } const now = Date.now(); for (const [key, startedAt] of inFlightChatRequests.entries()) { if (now - startedAt > IN_FLIGHT_REQUEST_TTL_MS) { inFlightChatRequests.delete(key); } } requestDedupKey = `${id}:${message.id}`; if (inFlightChatRequests.has(requestDedupKey)) { chatApiLogger.warn(`[Chat API] Duplicate in-flight request ignored (chatId=${id}, messageId=${message.id})`); const duplicateStream = createUIMessageStream({ execute: async ({ writer }) => { await replayAssistantMessagesForDuplicateRequest({ chatId: id, userMessageId: message.id, writer, source: "duplicate-in-flight", }); }, }); return createSseResponse(duplicateStream); } inFlightChatRequests.set(requestDedupKey, now); acquiredInFlightLock = true; let finalModelId = selectedChatModel; // 🆕 存储智能模式的相关信息 let smartAutoFriendlyMessage: string | undefined; // AI的自然对话回复 let smartAutoOptimizedPrompt: string | undefined; // 优化后的提示词(不修改用户消息,只用于API调用) let smartAutoPromptFromVideoPlan = false; let smartAutoRecommendedImageParams: AgentRecommendedImageParams | undefined; let smartAutoVideoPlan: AgentVideoPlan | undefined; let smartAutoRecommendedVideoParams: AgentRecommendedVideoParams | undefined; let smartAutoPlan: AgentExecutionPlan | undefined; let smartAutoExecution: AgentPreparedExecution | undefined; let smartAutoDetectedIntent: "image_edit" | "vision" | "image_gen" | "video_gen" | "chat" | undefined; let smartAutoHistoryImages: string[] = []; // 从历史对话中提取的图片URL(支持多张) let smartAutoNeedsClarification = false; // 是否需要先追问用户 let smartAutoClarificationQuestion: string | undefined; let requestResolvedMediaInput: ResolvedChatMediaInput | null = null; let requestTargetResolution: ResolvedTargetAssets | null = null; let requestWorkingState: | Awaited> | null = null; let storyboardPreferenceMemory = null; let imageWorkflowMemory = null; let reusablePromptMemory = null; let confirmedPromptForConversationState: string | undefined; let smartAutoHistoryMessagesFromDb: DBMessage[] = []; let smartAutoMediaHistoryMessagesFromDb: DBMessage[] = []; let recentDialogueMemory = buildRecentDialogueMemory({ messages: [], currentText: requestTextContent, }); let preferredAgentModelId: string | undefined; let configuredAgentConversationModelId: string | undefined; let agentModelSlots: AgentModelSlots = {}; let pendingGridConfirmation: | PendingGridConfirmation | null = null; let shouldAskGridConfirmation = false; const deepThinkingOn = deepThinkingEnabled !== false; const shouldRespectDirectComposerMode = composerMode === "image" || composerMode === "video" || composerMode === "detail"; const shouldIsolateDirectComposerContext = shouldRespectDirectComposerMode; const requestPolicy = resolveCanvasAgentRequestPolicy({ composerMode, agentEntryMode: agentPresetId ? "preset" : "auto", selectedChatModel: finalModelId, hasCanvasControlIntentFastPath, }); finalModelId = requestPolicy.effectiveSelectedChatModel; let shouldRunAgentConversation = finalModelId === "auto" || requestPolicy.shouldForceToolOrchestration; let shouldKeepSelectedModel = false; if (requestPolicy.reason === "agent_auto_composer") { chatApiLogger.info("[Smart Auto Mode] 🧭 Agent composer forces tool orchestration, ignoring stale selected model"); } if (shouldRespectDirectComposerMode && shouldRunAgentConversation) { shouldRunAgentConversation = false; chatApiLogger.info(`[Smart Auto Mode] ⛳ Direct composer mode (${composerMode}) disables initial auto-agent routing`); } const pendingProtocolDebugEvents: Array<{ phase: string; payload: Record }> = []; const requestedModelId = selectedChatModel; let requestedModelProviderType: string | undefined; let requestedModelIsFixedVisual = false; let requestedModelIsConversationMultimodal = false; let requestedModelConfig: Awaited> | null = null; if (!shouldRunAgentConversation && requestedModelId !== "auto") { const selectedModelIsUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(requestedModelId); let selectedModelConfig = selectedModelIsUUID ? await getAiModelById({ id: requestedModelId }) : null; let selectedModelProviderType: string | undefined; if (!selectedModelConfig) { const enabledModels = await getEnabledAiModelsWithProvider(false); const matchedModel = enabledModels.find( ({ model }) => model.id === requestedModelId || model.modelId === requestedModelId ); if (matchedModel) { selectedModelConfig = matchedModel.model; selectedModelProviderType = matchedModel.provider.type as string; } } if (selectedModelConfig && !selectedModelProviderType) { const selectedProvider = await getAiProviderById({ id: selectedModelConfig.providerId, }); selectedModelProviderType = selectedProvider?.type as string | undefined; } requestedModelConfig = selectedModelConfig; requestedModelProviderType = selectedModelProviderType; if (selectedModelConfig) { const isFixedVisualModel = !selectedModelConfig.isSubModel && ( isImageGenerationModelType(selectedModelProviderType, selectedModelConfig) || isVideoProviderType(selectedModelProviderType, selectedModelConfig) ); const isConversationMultimodalModel = !selectedModelConfig.isSubModel && isAiModelMultimodal(selectedModelConfig) && !isImageGenerationModelType(selectedModelProviderType, selectedModelConfig) && !isVideoProviderType(selectedModelProviderType, selectedModelConfig); requestedModelIsFixedVisual = isFixedVisualModel; requestedModelIsConversationMultimodal = isConversationMultimodalModel; if (deepThinkingOn) { if (isAiModelAgentAuto(selectedModelConfig)) { if (shouldRespectDirectComposerMode) { chatApiLogger.info(`[Smart Auto Mode] ⛳ Respect direct composer mode (${composerMode}), skip forcing agent-only routing for: ${selectedModelConfig.displayName}`); } else { shouldRunAgentConversation = true; shouldKeepSelectedModel = false; preferredAgentModelId = selectedModelConfig.id; configuredAgentConversationModelId = selectedModelConfig.id; chatApiLogger.info(`[Smart Auto Mode] 🧠 Agent-only model detected, force auto routing: ${selectedModelConfig.displayName}`); } } else if (isFixedVisualModel || isConversationMultimodalModel) { if (shouldRespectDirectComposerMode) { chatApiLogger.info(`[Smart Auto Mode] ⛳ Respect direct composer mode (${composerMode}), keep direct execution model: ${selectedModelConfig.displayName}`); } else { shouldRunAgentConversation = true; shouldKeepSelectedModel = true; preferredAgentModelId = selectedModelConfig.id; chatApiLogger.info(`[Smart Auto Mode] 🎯 Agent conversation enabled for selected model: ${selectedModelConfig.displayName} (${selectedModelProviderType || "unknown"}), preferredAgentModelId=${preferredAgentModelId}`); } } } else if ( isFixedVisualModel || isConversationMultimodalModel || isAiModelAgentAuto(selectedModelConfig) ) { chatApiLogger.info(`[Smart Auto Mode] ⏭️ Deep thinking disabled, skip agent analysis for: ${selectedModelConfig.displayName}`); } } } if (shouldRunAgentConversation && !preferredAgentModelId) { const configuredAgentModel = await getAgentAiModelWithProvider().catch((agentModelError) => { chatApiLogger.warn("[Chat API] Failed to load configured agent model:", agentModelError); return null; }); if (configuredAgentModel?.model?.id) { preferredAgentModelId = configuredAgentModel.model.id; configuredAgentConversationModelId = configuredAgentModel.model.id; agentModelSlots = upsertAgentModelSlot(agentModelSlots, "planner", { modelId: configuredAgentModel.model.id, displayName: configuredAgentModel.model.displayName || configuredAgentModel.model.modelId, providerId: configuredAgentModel.model.providerId, providerType: configuredAgentModel.provider.type, providerKey: configuredAgentModel.model.providerId, source: "configured_agent_default", reason: "agent_default_model", }); agentModelSlots = upsertAgentModelSlot(agentModelSlots, "analyzer", { modelId: configuredAgentModel.model.id, displayName: configuredAgentModel.model.displayName || configuredAgentModel.model.modelId, providerId: configuredAgentModel.model.providerId, providerType: configuredAgentModel.provider.type, providerKey: configuredAgentModel.model.providerId, source: "configured_agent_default", reason: "agent_default_model", }); chatApiLogger.info(`[Smart Auto Mode] 🧠 Loaded configured agent conversation model: ${configuredAgentModel.model.displayName}`); } } const hasCurrentMessageMediaInput = message.parts.some((part: any) => part?.type === "file" || part?.type === "image" || part?.type === "image_url" ); const isGreetingTurnRequest = isPureGreetingTurnRequest(requestTextContent); const isCapabilityQuestionRequest = isCapabilityQuestionTurnRequest(requestTextContent); const canvasFastPathMessage = isCapabilityQuestionRequest ? CANVAS_AGENT_CAPABILITY_MESSAGE : isGreetingTurnRequest ? CANVAS_AGENT_GREETING_MESSAGE : ""; if ( (shouldRunAgentConversation || isCanvasPageRequest || contextPayload?.source === "canvas" || Boolean(requestCanvasAgentContext)) && canvasFastPathMessage && !hasCurrentMessageMediaInput ) { const session = await auth(); if (!session?.user) { return new ChatSDKError("unauthorized:chat").toResponse(); } const fastPathTitle = requestTextContent.trim().slice(0, 80) || "新对话"; await saveChat({ id, userId: session.user.id, title: fastPathTitle, visibility: selectedVisibilityType, }); const greetingChat = await findChatWithRetry(id); if (!greetingChat) { throw new ChatSDKError("bad_request:database", "Failed to save chat"); } if (greetingChat.userId !== session.user.id) { return new ChatSDKError("forbidden:chat").toResponse(); } const greetingStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); writeAssistantTextChunk( writer, canvasFastPathMessage, isCapabilityQuestionRequest ? "capability-fast-path" : "greeting-fast-path", messageId ); await saveMessages({ messages: [ { id: message.id, chatId: id, role: "user", parts: message.parts as any, attachments: collectMessageImageAttachments({ parts: message.parts }), createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: canvasFastPathMessage }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); }, }); return createSseResponse(greetingStream); } const allowAgentVideoGeneration = Boolean(requestBody.videoParams) || isVideoProviderType(requestedModelProviderType, requestedModelConfig || undefined); let smartAutoWorkflowDecision: | { action?: string; workflowId?: string; workflowType?: string; allowWorkflowExecution?: boolean; } | undefined; const activeStoryboardTaskForChat = shouldIsolateDirectComposerContext ? null : await getLatestActiveVideoStoryboardTaskByChatId({ chatId: id, }).catch((storyboardLookupError) => { chatApiLogger.warn("[Chat API] Failed to preload active storyboard task before smart auto mode:", storyboardLookupError); return null; }); requestWorkingState = shouldIsolateDirectComposerContext ? null : await getConversationWorkingStateByChatId({ chatId: id, }).catch((workingStatePreloadError) => { chatApiLogger.warn("[Chat API] Failed to preload conversation working state before smart auto mode:", workingStatePreloadError); return null; }); const hasImageInputForConversationAction = message.parts.some((part: any) => part?.type === "file" || part?.type === "image" || part?.type === "image_url" ); const conversationStateBootstrap = bootstrapConversationStateProtocol({ text: requestTextContent, workingState: requestWorkingState, activeStoryboardTask: activeStoryboardTaskForChat, hasImageInputForPromptCarryForward: hasImageInputForConversationAction, hasCanvasSelection: Boolean(contextPayload?.selectedNodeIds?.length), }); chatApiLogger.info("[ConversationStateProtocol] Bootstrap:", { action: conversationStateBootstrap.actionResolution.kind, taskType: conversationStateBootstrap.taskBoundaryDecision.taskType, taskMode: conversationStateBootstrap.taskBoundaryDecision.taskMode, hasPromptMemory: Boolean(conversationStateBootstrap.promptMemory?.optimizedPrompt), hasImageWorkflowMemory: Boolean(conversationStateBootstrap.imageWorkflowMemory), hasStoryboardPreferenceMemory: Boolean(conversationStateBootstrap.storyboardPreferenceMemory), hasPendingGridConfirmation: Boolean(conversationStateBootstrap.pendingGridConfirmation), hasActiveStoryboardTask: Boolean(conversationStateBootstrap.activeStoryboardTask?.task?.id), }); pendingProtocolDebugEvents.push({ phase: "bootstrap", payload: { action: conversationStateBootstrap.actionResolution.kind, taskBoundary: conversationStateBootstrap.taskBoundaryDecision, pendingGridConfirmation: conversationStateBootstrap.pendingGridConfirmation, hasPromptMemory: Boolean(conversationStateBootstrap.promptMemory?.optimizedPrompt), hasImageWorkflowMemory: Boolean(conversationStateBootstrap.imageWorkflowMemory), hasStoryboardPreferenceMemory: Boolean(conversationStateBootstrap.storyboardPreferenceMemory), hasActiveStoryboardTask: Boolean(conversationStateBootstrap.activeStoryboardTask?.task?.id), }, }); storyboardPreferenceMemory = conversationStateBootstrap.storyboardPreferenceMemory; imageWorkflowMemory = conversationStateBootstrap.imageWorkflowMemory; reusablePromptMemory = conversationStateBootstrap.promptMemory; pendingGridConfirmation = conversationStateBootstrap.pendingGridConfirmation; confirmedPromptForConversationState = conversationStateBootstrap.confirmedPrompt; const resolvedConversationAction = conversationStateBootstrap.actionResolution; const taskBoundaryDecision = conversationStateBootstrap.taskBoundaryDecision; const taskBoundaryRequiresAgentConversation = taskBoundaryDecision.taskType === "prompt_only" || taskBoundaryDecision.taskType === "analysis_only" || taskBoundaryDecision.taskType === "execution_image" || taskBoundaryDecision.taskType === "execution_video" || taskBoundaryDecision.taskType === "workflow_followup"; const taskBoundaryOverridesLegacyWorkflow = taskBoundaryDecision.taskType === "greeting" || taskBoundaryDecision.taskType === "chat" || taskBoundaryDecision.taskType === "analysis_only" || taskBoundaryDecision.taskType === "prompt_only"; const prunedTaskBoundaryContext = pruneTaskBoundaryContext({ decision: taskBoundaryDecision, contextPayload: contextPayload || undefined, autoContextText: autoContextText || undefined, workingState: requestWorkingState || undefined, hasCurrentImageInput: hasImageInputForConversationAction, }); const effectiveContextPayload = prunedTaskBoundaryContext.contextPayload; const effectiveAutoContextText = prunedTaskBoundaryContext.autoContextText || ""; const effectiveWorkingState = prunedTaskBoundaryContext.workingState; if (taskBoundaryRequiresAgentConversation && !shouldRunAgentConversation) { if (shouldRespectDirectComposerMode) { chatApiLogger.info(`[Smart Auto Mode] ⛳ Respect direct composer mode (${composerMode}), skip task-boundary agent force: ${taskBoundaryDecision.taskType}/${taskBoundaryDecision.taskMode}`); } else { shouldRunAgentConversation = true; shouldKeepSelectedModel = false; chatApiLogger.info(`[Smart Auto Mode] 🧭 Force agent conversation due to task boundary: ${taskBoundaryDecision.taskType}/${taskBoundaryDecision.taskMode}`); } } if (taskBoundaryOverridesLegacyWorkflow && shouldKeepSelectedModel) { shouldKeepSelectedModel = false; chatApiLogger.info(`[Smart Auto Mode] 🧼 Release selected-model lock due to task boundary override: ${taskBoundaryDecision.taskType}`); } if (resolvedConversationAction.forceAgentConversation && !shouldRunAgentConversation) { if (shouldRespectDirectComposerMode) { chatApiLogger.info(`[Smart Auto Mode] ⛳ Respect direct composer mode (${composerMode}), skip legacy workflow force: ${resolvedConversationAction.kind}`); } else if (taskBoundaryOverridesLegacyWorkflow) { chatApiLogger.info(`[Smart Auto Mode] 🪫 Ignore legacy workflow follow-up due to task boundary override: ${resolvedConversationAction.kind} -> ${taskBoundaryDecision.taskType}`); } else { shouldRunAgentConversation = true; shouldKeepSelectedModel = resolvedConversationAction.shouldKeepSelectedModel; if (resolvedConversationAction.kind === "grid_confirmation_followup" && pendingGridConfirmation) { chatApiLogger.info(`[Smart Auto Mode] 🧩 Force agent conversation due to pending grid confirmation: ${pendingGridConfirmation.label}`); } else if ( resolvedConversationAction.kind === "active_storyboard_followup" && activeStoryboardTaskForChat?.task?.id ) { chatApiLogger.info(`[Smart Auto Mode] 🔐 Force agent conversation due to active storyboard task: ${activeStoryboardTaskForChat.task.id}`); } else if (resolvedConversationAction.kind === "image_generation_followup") { chatApiLogger.info("[Smart Auto Mode] 🪄 Force agent conversation due to image generation follow-up workflow"); } else if (resolvedConversationAction.kind === "image_edit_followup") { chatApiLogger.info("[Smart Auto Mode] 🖼️ Force agent conversation due to image edit follow-up workflow"); } } } const shouldFastTrackPromptOnly = shouldRunAgentConversation && taskBoundaryDecision.taskType === "prompt_only"; if (shouldFastTrackPromptOnly) { if (!requestResolvedMediaInput) { requestResolvedMediaInput = resolveChatMediaInput({ messageParts: message.parts as any[], historyMessages: [], contextPayload: effectiveContextPayload, requestOrigin, maxImages: 4, }); } if (!requestTargetResolution) { requestTargetResolution = resolveTargetAssets({ text: requestTextContent, mediaInput: requestResolvedMediaInput, contextPayload: effectiveContextPayload, workingState: effectiveWorkingState || undefined, }); } smartAutoDetectedIntent = "vision"; smartAutoNeedsClarification = false; smartAutoFriendlyMessage = "收到,我先分析这张图并整理可直接使用的提示词,不会直接生图。"; chatApiLogger.info("[Smart Auto Mode] ✍️ Fast-path prompt-only request, skip heavy auto-agent workflow"); } // ========== Smart Auto Mode Logic ========== if (shouldRunAgentConversation && !shouldFastTrackPromptOnly) { chatApiLogger.info("[Smart Auto Mode] 🚀 Starting intelligent analysis..."); let autoTextForFallback = ""; let autoFromHistoryForFallback = false; let autoCurrentImageUrlsForFallback: string[] = []; let confirmedPromptForGeneration: string | undefined; let forcePromptDrivenImageGeneration = false; let shouldPreferArbitratedFriendlyMessage = false; const smartAutoInputText = resolvedConversationAction.effectiveInputText || requestTextContent; try { // 🆕 首先获取历史消息(在 Smart Auto Mode 内部单独获取,避免依赖后面的 messagesFromDb) const loadedHistoryMessages = await getMessagesByChatId({ id }); smartAutoHistoryMessagesFromDb = pruneTaskBoundaryHistoryMessages({ decision: taskBoundaryDecision, messages: loadedHistoryMessages, purpose: "model", hasCurrentImageInput: hasImageInputForConversationAction, }); smartAutoMediaHistoryMessagesFromDb = pruneTaskBoundaryHistoryMessages({ decision: taskBoundaryDecision, messages: loadedHistoryMessages, purpose: "media", hasCurrentImageInput: hasImageInputForConversationAction, }); chatApiLogger.info("[Smart Auto Mode] Loaded", loadedHistoryMessages.length, "messages from DB"); chatApiLogger.info("[Smart Auto Mode] Pruned history:", { modelMessages: smartAutoHistoryMessagesFromDb.length, mediaMessages: smartAutoMediaHistoryMessagesFromDb.length, taskType: taskBoundaryDecision.taskType, taskMode: taskBoundaryDecision.taskMode, }); recentDialogueMemory = buildRecentDialogueMemory({ messages: loadedHistoryMessages, currentText: smartAutoInputText, maxEntries: taskBoundaryDecision.taskType === "chat" ? 6 : 4, maxTotalChars: taskBoundaryDecision.taskType === "chat" ? 1000 : 700, }); chatApiLogger.info("[RecentDialogueMemory] Smart auto recall:", { entries: recentDialogueMemory.entries.length, isRecallRequest: recentDialogueMemory.isRecallRequest, taskType: taskBoundaryDecision.taskType, }); autoTextForFallback = smartAutoInputText; if (!requestWorkingState && !shouldIsolateDirectComposerContext) { requestWorkingState = await getConversationWorkingStateByChatId({ chatId: id, }); } confirmedPromptForGeneration = confirmedPromptForConversationState; const isPromptCarryForwardExecution = resolvedConversationAction.kind === "prompt_carry_forward"; forcePromptDrivenImageGeneration = resolvedConversationAction.usePromptCarryForward && Boolean(confirmedPromptForGeneration); if (!requestResolvedMediaInput) { requestResolvedMediaInput = resolveChatMediaInput({ messageParts: message.parts as any[], historyMessages: smartAutoMediaHistoryMessagesFromDb, contextPayload: effectiveContextPayload, requestOrigin, maxImages: 4, }); } if (!requestTargetResolution) { requestTargetResolution = resolveTargetAssets({ text: requestTextContent, mediaInput: requestResolvedMediaInput, contextPayload: effectiveContextPayload, workingState: effectiveWorkingState || undefined, }); } const hasSelectedImageHint = Boolean(effectiveContextPayload?.selectedNodeIds?.length) || /用户已选中\s*\d+\s*张图片/.test(effectiveAutoContextText); const hasExplicitTargetHint = requestTargetResolution.resolutionMode === "text_reference" || /第[一二三四五六七八九十\d]+张|左边|左侧|左上|左下|右边|右侧|右上|右下|上面|上方|下面|下方|中间|居中|中央|选中/.test(requestTextContent); const hasActionableEditIntent = /改|调整|优化|换|替换|去掉|抠|重绘|增强|修|处理|按照|按|保留|统一|色差|背景|清晰|风格/.test( requestTextContent ); const currentMessageImageInputs = requestResolvedMediaInput.currentMessageImageUrls.slice(0, 4); let imageUrls = requestTargetResolution.targetImageUrls.length > 0 ? requestTargetResolution.targetImageUrls.slice(0, 4) : requestResolvedMediaInput.resolvedImageUrls.slice(0, 4); let hasImage = imageUrls.length > 0; autoCurrentImageUrlsForFallback = currentMessageImageInputs.length > 0 ? currentMessageImageInputs.slice() : imageUrls.slice(); smartAutoHistoryImages = currentMessageImageInputs.length === 0 ? imageUrls.slice(0, 4) : []; autoFromHistoryForFallback = currentMessageImageInputs.length === 0 && imageUrls.length > 0; if (forcePromptDrivenImageGeneration) { imageUrls = []; hasImage = false; autoCurrentImageUrlsForFallback = []; smartAutoHistoryImages = []; autoFromHistoryForFallback = false; autoTextForFallback = confirmedPromptForGeneration || requestTextContent; smartAutoOptimizedPrompt = confirmedPromptForGeneration; chatApiLogger.info("[Smart Auto Mode] 🎯 Using confirmed prompt-only result for image generation"); } chatApiLogger.info("[Smart Auto Mode] Input:", { textContent: requestTextContent.substring(0, 100), hasImage, imageCount: imageUrls.length, fromHistory: smartAutoHistoryImages.length > 0, resolutionMode: requestTargetResolution.resolutionMode, resolutionConfidence: requestTargetResolution.confidence, }); const historyMessages: { role: "user" | "assistant"; content: string }[] = recentDialogueMemory.entries.map((entry) => ({ role: entry.role, content: entry.content, })); chatApiLogger.info("[Smart Auto Mode] History context:", historyMessages.length, "memory entries"); // 2. 使用 LangGraph 驱动的自动模式工作流 const { runAutoAgentGraph } = await import("@/lib/ai/auto-agent-graph"); const autoGraphTimeoutMs = getBudgetedTimeout({ startedAt: requestStartedAt, preferredMs: SMART_AUTO_GRAPH_TIMEOUT_MS, minimumMs: 8_000, }); if (!autoGraphTimeoutMs) { throw new OperationTimeoutError( "runAutoAgentGraph.preResponseBudget", CHAT_PRE_RESPONSE_SOFT_TIMEOUT_MS ); } const composedConversationState = composeConversationStateProtocol({ bootstrap: conversationStateBootstrap, autoContextText: effectiveAutoContextText || undefined, resolvedMediaInput: requestResolvedMediaInput, targetResolution: requestTargetResolution, }); chatApiLogger.info("[ConversationStateProtocol] Composed:", { workflowType: composedConversationState.threadState.workflowState?.type || null, workflowStatus: composedConversationState.threadState.workflowState?.status || null, memoryHintsCount: composedConversationState.threadState.memoryHints.length, contextSummaryPreview: composedConversationState.threadState.contextSummary?.slice(0, 180), }); pendingProtocolDebugEvents.push({ phase: "composed", payload: { workflowState: composedConversationState.threadState.workflowState, memoryHints: composedConversationState.threadState.memoryHints, contextSummary: composedConversationState.threadState.contextSummary, recentDialogueMemory: recentDialogueMemory.contextSummary, }, }); const agentGraphContext = [ composedConversationState.composedConversationMemory.contextSummary, recentDialogueMemory.contextSummary, ] .filter(Boolean) .join("\n"); const autoGraphResult = await withOperationTimeout( runAutoAgentGraph({ text: smartAutoInputText, hasImage, imageUrls, assetIds: Array.from( new Set([ ...extractMessageImageAssetIds(message.parts as any[]), ...((effectiveContextPayload?.selectedAssetIds || []).filter(Boolean) as string[]), ]), ), imageFromHistory: smartAutoHistoryImages.length > 0, targetResolutionMode: requestTargetResolution.resolutionMode, mediaSource: requestResolvedMediaInput.source, hasCanvasSelection: Boolean(effectiveContextPayload?.selectedNodeIds?.length) || requestResolvedMediaInput.selectedImageUrls.length > 0 || getSelectedSlots(requestCanvasAgentContext).length > 0, selectedImageCount: requestResolvedMediaInput.selectedImageUrls.length, canvasAgentContext: requestCanvasAgentContext, selectedSlotCount: requestCanvasAgentContext?.slots.length || 0, hasSelectedAgentSlot: getSelectedSlots(requestCanvasAgentContext).length > 0, historyImageCount: requestResolvedMediaInput.historyAssistantImageUrls.length + requestResolvedMediaInput.historyUserImageUrls.length, currentUploadImageCount: requestResolvedMediaInput.currentMessageImageUrls.length, historyMessages, context: agentGraphContext || effectiveAutoContextText || undefined, preferredModelId: preferredAgentModelId, threadState: composedConversationState.threadState, }), "runAutoAgentGraph", autoGraphTimeoutMs, ); const requestedStoryboardGrid = detectRequestedStoryboardGrid( pendingGridConfirmation && isGridConfirmationReply(requestTextContent) ? pendingGridConfirmation.originalPrompt : requestTextContent ); if (requestedStoryboardGrid) { storyboardPreferenceMemory = buildStoryboardPreferenceMemoryUpdate({ existing: storyboardPreferenceMemory, preferredGridType: requestedStoryboardGrid.gridType, preferredShotCount: requestedStoryboardGrid.shotCount, prefersConfirmation: true, lastUsedGridType: requestedStoryboardGrid.gridType, lastUsedShotCount: requestedStoryboardGrid.shotCount, }); } smartAutoPlan = autoGraphResult.plan; smartAutoExecution = autoGraphResult.execution; if (autoGraphResult.workflowDecision) { smartAutoWorkflowDecision = autoGraphResult.workflowDecision; chatApiLogger.info("[Smart Auto Mode] Workflow decision:", { action: autoGraphResult.workflowDecision.action, intent: autoGraphResult.workflowDecision.confirmationIntent, workflowId: autoGraphResult.workflowDecision.workflowId, workflowType: autoGraphResult.workflowDecision.workflowType, nextWorkflowStatus: autoGraphResult.workflowDecision.nextWorkflowStatus, allowWorkflowExecution: autoGraphResult.workflowDecision.allowWorkflowExecution, }); if ( autoGraphResult.workflowDecision.workflowType === "storyboard_video" && autoGraphResult.workflowDecision.workflowId ) { const storyboardTaskId = autoGraphResult.workflowDecision.workflowId; try { switch (autoGraphResult.workflowDecision.action) { case "awaiting_confirmation": await markVideoStoryboardAwaitingConfirmation({ taskId: storyboardTaskId, }); break; case "confirmed": await markVideoStoryboardConfirmed({ taskId: storyboardTaskId, messageId: message.id, }); break; case "revision_requested": await markVideoStoryboardDraftForRevision({ taskId: storyboardTaskId, }); break; case "cancel_requested": await cancelVideoStoryboardTaskByUser({ taskId: storyboardTaskId, error: "user_cancelled_via_chat", }); break; case "pass": default: break; } } catch (workflowPersistError) { chatApiLogger.warn("[Smart Auto Mode] Failed to persist storyboard workflow decision:", workflowPersistError); } } } if (forcePromptDrivenImageGeneration && confirmedPromptForGeneration) { smartAutoDetectedIntent = "image_gen"; smartAutoOptimizedPrompt = confirmedPromptForGeneration; smartAutoExecution = { mode: "generate_image", routeIntent: "image_gen", routeSubIntent: undefined, targetSource: "none", preferredCapability: "generate_image", shouldUseStructuredReply: true, shouldOptimizePrompt: true, shouldApplyImagePipeline: true, shouldApplyVideoPipeline: false, shouldStayConversational: false, shouldPublishToCanvas: false, toolCalls: [], availableTools: [], }; smartAutoFriendlyMessage = smartAutoFriendlyMessage || (isPromptCarryForwardExecution ? "收到,我会使用上一轮优化后的提示词,结合你这轮图片继续执行。" : "收到,我会按你刚确认的提示词直接生图。"); } if (smartAutoPlan) { chatApiLogger.info("[Smart Auto Mode] Plan result:", { goal: smartAutoPlan.goal, nextAction: smartAutoPlan.nextAction, intent: smartAutoPlan.intent, targetSource: smartAutoPlan.targetSource, preferredCapability: smartAutoPlan.preferredCapability, confidence: smartAutoPlan.confidence, toolCalls: smartAutoPlan.toolCalls?.map((tool) => tool.name) || [], }); } if (autoGraphResult.analysis) { chatApiLogger.info("[Smart Auto Mode] Analysis result:", { intent: autoGraphResult.analysis.intent, subIntent: autoGraphResult.analysis.subIntent, confidence: autoGraphResult.analysis.confidence, needsClarification: autoGraphResult.analysis.needsClarification, }); const arbitratedIntent = arbitrateAgentIntent({ text: requestTextContent, hasImage, hasHistoryImage: smartAutoHistoryImages.length > 0, actionResolution: resolvedConversationAction, taskBoundaryDecision, analysis: autoGraphResult.analysis, execution: smartAutoExecution, targetSource: requestTargetResolution.resolutionMode, }); if (arbitratedIntent.overridden) { chatApiLogger.info("[IntentArbiter] Override auto-agent intent:", { fromIntent: autoGraphResult.analysis.intent, fromMode: autoGraphResult.execution?.mode, toIntent: arbitratedIntent.analysis.intent, toMode: arbitratedIntent.execution.mode, reason: arbitratedIntent.reason, }); } smartAutoExecution = arbitratedIntent.execution as AgentPreparedExecution; smartAutoDetectedIntent = arbitratedIntent.analysis.intent; smartAutoNeedsClarification = Boolean(arbitratedIntent.analysis.needsClarification) || (!arbitratedIntent.overridden && smartAutoPlan?.nextAction === "clarify"); smartAutoClarificationQuestion = arbitratedIntent.analysis.clarifyingQuestions?.[0]; if (arbitratedIntent.friendlyMessage) { smartAutoFriendlyMessage = arbitratedIntent.friendlyMessage; shouldPreferArbitratedFriendlyMessage = true; } const targetExecutionOverride = resolveTargetExecutionOverride({ hasImage, hasSelectedImageHint, hasExplicitTargetHint, hasActionableEditIntent, executionMode: smartAutoExecution?.mode, friendlyMessage: smartAutoFriendlyMessage, optimizedPrompt: smartAutoOptimizedPrompt, requestText: requestTextContent, }); if ( smartAutoNeedsClarification && targetExecutionOverride.shouldBypassClarification ) { smartAutoNeedsClarification = false; smartAutoClarificationQuestion = undefined; smartAutoOptimizedPrompt = targetExecutionOverride.nextOptimizedPrompt; smartAutoFriendlyMessage = targetExecutionOverride.nextFriendlyMessage; chatApiLogger.info("[Smart Auto Mode] ✅ Skip clarification due to explicit target/selection context"); } } const requestedGrid = !pendingGridConfirmation || smartAutoInputText === requestTextContent ? detectRequestedStoryboardGrid(requestTextContent) : pendingGridConfirmation; const gridConfirmationResolution = resolveGridConfirmationRequest({ requestedGrid, hasActiveStoryboardTask: Boolean(activeStoryboardTaskForChat?.task?.id), executionMode: smartAutoExecution?.mode, detectedIntent: smartAutoDetectedIntent, requestText: requestTextContent, effectiveInputText: smartAutoInputText, }); if (gridConfirmationResolution.shouldRequestConfirmation) { shouldAskGridConfirmation = true; smartAutoNeedsClarification = true; smartAutoClarificationQuestion = gridConfirmationResolution.clarificationMessage; smartAutoFriendlyMessage = gridConfirmationResolution.clarificationMessage; pendingGridConfirmation = gridConfirmationResolution.pendingGridConfirmation; chatApiLogger.info("[Smart Auto Mode] 🧱 Require grid confirmation before generation:", { gridType: gridConfirmationResolution.pendingGridConfirmation?.gridType, shotCount: gridConfirmationResolution.pendingGridConfirmation?.shotCount, }); } if (autoGraphResult.friendlyMessage && !shouldPreferArbitratedFriendlyMessage) { smartAutoFriendlyMessage = autoGraphResult.friendlyMessage; chatApiLogger.info("[Smart Auto Mode] 💬 Conversational reply:", smartAutoFriendlyMessage); } if (shouldAskGridConfirmation && smartAutoClarificationQuestion) { smartAutoFriendlyMessage = smartAutoClarificationQuestion; } if ( !smartAutoNeedsClarification && hasImage && hasActionableEditIntent && (hasSelectedImageHint || hasExplicitTargetHint) && smartAutoFriendlyMessage && (/[??]$/.test(smartAutoFriendlyMessage.trim()) || /哪一张|哪个|请先确认/.test(smartAutoFriendlyMessage)) ) { smartAutoFriendlyMessage = "收到,我将按你指定的目标图直接执行。"; chatApiLogger.info("[Smart Auto Mode] ✅ Replace clarification-like friendly reply"); } const autoIntent = smartAutoDetectedIntent || autoGraphResult.analysis?.intent; const autoExecutionMode = smartAutoExecution?.mode; const isImageTask = autoExecutionMode === "generate_image" || autoExecutionMode === "edit_image"; const needsRicherAutoReply = !smartAutoNeedsClarification && !smartAutoFriendlyMessage; if (needsRicherAutoReply) { if (isImageTask) { smartAutoFriendlyMessage = autoGraphResult.analysis?.conversationalReply || "我先分析你这轮图片里的主体、风格和构图,再给你执行方案。"; } else if (autoIntent === "vision") { smartAutoFriendlyMessage = autoGraphResult.analysis?.conversationalReply || "收到,我先看图分析,再直接回答你。"; } else if (autoIntent === "chat") { smartAutoFriendlyMessage = autoGraphResult.analysis?.conversationalReply || "收到,我们先继续聊你的这个需求。"; } else { smartAutoFriendlyMessage = smartAutoExecution?.shouldUseStructuredReply ? buildAutoModeFallbackMessage({ text: autoTextForFallback, hasImage, fromHistory: smartAutoHistoryImages.length > 0, }) : "收到,我先理解你的当前目标,再继续处理。"; } chatApiLogger.info("[Smart Auto Mode] 🧩 Filled missing conversational reply"); } if (autoGraphResult.optimizedPrompt) { smartAutoOptimizedPrompt = autoGraphResult.optimizedPrompt; chatApiLogger.info("[Smart Auto Mode] ✨ Prompt optimized (LLM):", { original: requestTextContent.substring(0, 50), optimized: autoGraphResult.optimizedPrompt.substring(0, 100), }); } if (autoGraphResult.videoPlan) { smartAutoVideoPlan = autoGraphResult.videoPlan; smartAutoRecommendedVideoParams = autoGraphResult.videoPlan.recommendedVideoParams; if (!smartAutoOptimizedPrompt && autoGraphResult.videoPlan.finalPrompt?.trim()) { smartAutoOptimizedPrompt = autoGraphResult.videoPlan.finalPrompt.trim(); smartAutoPromptFromVideoPlan = true; } chatApiLogger.info("[Smart Auto Mode] 🎬 Agent video plan ready:", { hasScript: Boolean(autoGraphResult.videoPlan.script), storyboardCount: autoGraphResult.videoPlan.storyboard?.length || 0, hasFinalPrompt: Boolean(autoGraphResult.videoPlan.finalPrompt), recommendedVideoParams: smartAutoRecommendedVideoParams, }); } if (autoGraphResult.recommendedImageParams) { smartAutoRecommendedImageParams = autoGraphResult.recommendedImageParams; chatApiLogger.info("[Smart Auto Mode] 🎛️ Agent recommended image params:", smartAutoRecommendedImageParams); } const shouldForceConversationalRouteForVisualModel = shouldKeepSelectedModel && requestedModelIsFixedVisual && !requestedModelIsConversationMultimodal && Boolean(smartAutoExecution?.shouldStayConversational); if (autoGraphResult.route && (!shouldKeepSelectedModel || shouldForceConversationalRouteForVisualModel)) { const routeIsVideo = isVideoProviderType(autoGraphResult.route.providerType, { modelId: autoGraphResult.route.modelId, }); if (routeIsVideo && !allowAgentVideoGeneration) { const fallbackIntent: "image_edit" | "image_gen" = hasImage ? "image_edit" : "image_gen"; smartAutoDetectedIntent = fallbackIntent; smartAutoExecution = smartAutoExecution ? { ...smartAutoExecution, mode: hasImage ? "edit_image" : "generate_image", routeIntent: fallbackIntent, shouldApplyImagePipeline: true, shouldApplyVideoPipeline: false, shouldOptimizePrompt: true, shouldStayConversational: false, shouldUseStructuredReply: true, } : smartAutoExecution; smartAutoVideoPlan = undefined; smartAutoRecommendedVideoParams = undefined; if (smartAutoPromptFromVideoPlan) { smartAutoOptimizedPrompt = requestTextContent.trim() || undefined; smartAutoPromptFromVideoPlan = false; } smartAutoFriendlyMessage = smartAutoFriendlyMessage && !/视频/.test(smartAutoFriendlyMessage) ? smartAutoFriendlyMessage : "当前 Agent 默认按图片任务执行;如需生成视频,请先手动切换到“视频”模式。"; chatApiLogger.info(`[Smart Auto Mode] 🚫 Blocked auto video route (${autoGraphResult.route.displayName}), fallback to ${fallbackIntent}`); try { const { routeToModel } = await import("@/lib/ai/model-router"); const fallbackRoute = await routeToModel({ intent: fallbackIntent, subIntent: autoGraphResult.analysis?.subIntent, hasImage, }); if (!isVideoProviderType(fallbackRoute.providerType, { modelId: fallbackRoute.modelId })) { finalModelId = fallbackRoute.id; chatApiLogger.info(`[Smart Auto Mode] ✅ Fallback routed to: ${fallbackRoute.displayName} (${fallbackRoute.reason})`); } else { chatApiLogger.warn(`[Smart Auto Mode] ⚠️ Fallback route still video (${fallbackRoute.displayName}), keep previous model`); } } catch (fallbackRouteError) { chatApiLogger.error("[Smart Auto Mode] Failed to resolve non-video fallback route:", fallbackRouteError); } } else { finalModelId = autoGraphResult.route.id; chatApiLogger.info("console_log", shouldForceConversationalRouteForVisualModel ? `[Smart Auto Mode] ✅ Switched visual model to conversational route: ${autoGraphResult.route.displayName} (${autoGraphResult.route.reason})` : `[Smart Auto Mode] ✅ Routed to: ${autoGraphResult.route.displayName} (${autoGraphResult.route.reason})`); } } else if (autoGraphResult.route && shouldKeepSelectedModel) { finalModelId = requestedModelId; chatApiLogger.info("[Smart Auto Mode] 🔒 Keep user-selected model, skip routing override"); } } catch (autoError) { // 智能分析失败时,回退到简单逻辑 chatApiLogger.error("[Smart Auto Mode] ❌ Error, falling back:", autoError); const isAutoTimeout = autoError instanceof OperationTimeoutError; if (isAutoTimeout) { chatApiLogger.warn(`[Smart Auto Mode] ⏱️ Auto-agent timed out or exceeded pre-response budget (${CHAT_PRE_RESPONSE_SOFT_TIMEOUT_MS}ms), using local fallback`); } try { const { analyzeIntent } = await import("@/lib/ai/smart-analyzer"); const rescueImageUrls = autoCurrentImageUrlsForFallback.length > 0 ? autoCurrentImageUrlsForFallback : smartAutoHistoryImages; const rescueHasImage = rescueImageUrls.length > 0; const rescueFromHistory = rescueHasImage && autoCurrentImageUrlsForFallback.length === 0 && smartAutoHistoryImages.length > 0; const rescueAssetIds = Array.from( new Set([ ...extractMessageImageAssetIds(message.parts as any[]), ...((effectiveContextPayload?.selectedAssetIds || []).filter(Boolean) as string[]), ]), ); const rescueTimeoutMs = getBudgetedTimeout({ startedAt: requestStartedAt, preferredMs: SMART_AUTO_FALLBACK_TIMEOUT_MS, minimumMs: 5_000, }); if (rescueTimeoutMs) { const rescueAnalysis = await withOperationTimeout( analyzeIntent({ text: autoTextForFallback, hasImage: rescueHasImage, imageUrls: rescueImageUrls, assetIds: rescueAssetIds, imageFromHistory: rescueFromHistory || autoFromHistoryForFallback, historyMessages: [], context: undefined, preferredModelId: preferredAgentModelId, }), "fallbackAnalyzeIntent", rescueTimeoutMs, ); smartAutoDetectedIntent = rescueAnalysis.intent; const arbitratedIntent = arbitrateAgentIntent({ text: autoTextForFallback, hasImage: rescueHasImage, hasHistoryImage: rescueFromHistory || autoFromHistoryForFallback, actionResolution: resolvedConversationAction, taskBoundaryDecision, analysis: rescueAnalysis, execution: undefined, targetSource: requestTargetResolution?.resolutionMode || "none", }); if (arbitratedIntent.overridden) { chatApiLogger.info("[IntentArbiter] Override rescue intent:", { fromIntent: rescueAnalysis.intent, toIntent: arbitratedIntent.analysis.intent, toMode: arbitratedIntent.execution.mode, reason: arbitratedIntent.reason, }); } smartAutoDetectedIntent = arbitratedIntent.analysis.intent; smartAutoNeedsClarification = Boolean(arbitratedIntent.analysis.needsClarification); smartAutoClarificationQuestion = arbitratedIntent.analysis.clarifyingQuestions?.[0]; smartAutoExecution = arbitratedIntent.execution as AgentPreparedExecution; if (arbitratedIntent.friendlyMessage || rescueAnalysis.conversationalReply?.trim()) { smartAutoFriendlyMessage = ( arbitratedIntent.friendlyMessage || rescueAnalysis.conversationalReply || "" ).trim(); } } else { chatApiLogger.warn("[Smart Auto Mode] Skip rescue analysis due to pre-response budget"); } } catch (rescueError) { chatApiLogger.error("[Smart Auto Mode] Rescue analysis failed:", rescueError); try { const { fallbackKeywordAnalysis } = await import("@/lib/ai/smart-analyzer"); const keywordFallback = fallbackKeywordAnalysis( autoTextForFallback, autoCurrentImageUrlsForFallback.length > 0 || smartAutoHistoryImages.length > 0, ); const arbitratedIntent = arbitrateAgentIntent({ text: autoTextForFallback, hasImage: autoCurrentImageUrlsForFallback.length > 0 || smartAutoHistoryImages.length > 0, hasHistoryImage: smartAutoHistoryImages.length > 0 || autoFromHistoryForFallback, actionResolution: resolvedConversationAction, taskBoundaryDecision, analysis: keywordFallback, execution: undefined, targetSource: requestTargetResolution?.resolutionMode || "none", }); if (arbitratedIntent.overridden) { chatApiLogger.info("[IntentArbiter] Override keyword-fallback intent:", { fromIntent: keywordFallback.intent, toIntent: arbitratedIntent.analysis.intent, toMode: arbitratedIntent.execution.mode, reason: arbitratedIntent.reason, }); } smartAutoDetectedIntent = arbitratedIntent.analysis.intent; smartAutoNeedsClarification = Boolean(arbitratedIntent.analysis.needsClarification); smartAutoClarificationQuestion = arbitratedIntent.analysis.clarifyingQuestions?.[0]; smartAutoExecution = arbitratedIntent.execution as AgentPreparedExecution; if (arbitratedIntent.friendlyMessage || keywordFallback.conversationalReply?.trim()) { smartAutoFriendlyMessage = ( arbitratedIntent.friendlyMessage || keywordFallback.conversationalReply || "" ).trim(); chatApiLogger.warn("[Smart Auto Mode] Applied keyword fallback after rescue failure"); } } catch (keywordFallbackError) { chatApiLogger.warn("[Smart Auto Mode] Keyword fallback after rescue failure also failed:", keywordFallbackError); } } if ( !smartAutoOptimizedPrompt && smartAutoExecution?.shouldOptimizePrompt && (smartAutoDetectedIntent === "image_edit" || smartAutoDetectedIntent === "image_gen") ) { try { const { optimizePrompt } = await import("@/lib/ai/prompt-optimizer"); const optimizedFallback = optimizePrompt({ originalPrompt: autoTextForFallback, intent: smartAutoDetectedIntent, subIntent: smartAutoExecution.routeSubIntent, }); if (optimizedFallback.optimizedPrompt?.trim()) { smartAutoOptimizedPrompt = optimizedFallback.optimizedPrompt.trim(); chatApiLogger.info("[Smart Auto Mode] ✨ Prompt optimized (fallback heuristic):", { original: autoTextForFallback.substring(0, 80), optimized: smartAutoOptimizedPrompt.substring(0, 140), intent: smartAutoDetectedIntent, subIntent: smartAutoExecution.routeSubIntent || null, }); } if (!smartAutoRecommendedImageParams && optimizedFallback.recommendedImageParams) { smartAutoRecommendedImageParams = optimizedFallback.recommendedImageParams; } } catch (fallbackOptimizeError) { chatApiLogger.warn("[Smart Auto Mode] Fallback prompt optimization failed:", fallbackOptimizeError); } } const allModels = await getEnabledAiModelsWithProvider(); const hasImage = autoCurrentImageUrlsForFallback.length > 0 || smartAutoHistoryImages.length > 0; let fallbackVisualSummary: string | undefined; if (isAutoTimeout && hasImage) { try { const currentImageUrls = autoCurrentImageUrlsForFallback.length > 0 ? autoCurrentImageUrlsForFallback : collectImageInputsFromMessageParts(message.parts as any[], requestOrigin); const summaryImageUrls = currentImageUrls.length > 0 ? currentImageUrls : smartAutoHistoryImages; if (summaryImageUrls.length > 0) { const { summarizeImageForExecutionReport } = await import("@/lib/ai/smart-analyzer"); const summaryAssetIds = Array.from( new Set([ ...extractMessageImageAssetIds(message.parts as any[]), ...((effectiveContextPayload?.selectedAssetIds || []).filter(Boolean) as string[]), ]), ); const fallbackSummaryTimeoutMs = getBudgetedTimeout({ startedAt: requestStartedAt, preferredMs: SMART_AUTO_IMAGE_SUMMARY_TIMEOUT_MS, minimumMs: 5_000, }); if (fallbackSummaryTimeoutMs) { const summary = await withOperationTimeout( summarizeImageForExecutionReport({ text: autoTextForFallback, imageUrls: summaryImageUrls, context: effectiveAutoContextText || undefined, preferredModelId: preferredAgentModelId, assetIds: summaryAssetIds, timeoutMs: fallbackSummaryTimeoutMs, }), "fallbackVisualSummary", fallbackSummaryTimeoutMs, ); if (summary?.trim()) { fallbackVisualSummary = summary.trim(); } } else { chatApiLogger.warn("[Smart Auto Mode] Skip fallback visual summary due to pre-response budget"); } } } catch (summaryError) { chatApiLogger.warn("[Smart Auto Mode] Fallback visual summary failed:", summaryError); } } // 回退路由:优先按分析出的意图选模型,避免“有图就生图” let bestModel = null; try { const fallbackIntent = smartAutoDetectedIntent || (hasImage ? "vision" : "chat"); const { routeToModel } = await import("@/lib/ai/model-router"); const fallbackRoute = await routeToModel({ intent: fallbackIntent, subIntent: smartAutoExecution?.routeSubIntent, hasImage, }); bestModel = allModels.find( (item) => item.model.id === fallbackRoute.id || item.model.modelId === fallbackRoute.modelId, ) || null; if (bestModel) { chatApiLogger.info(`[Smart Auto Mode] Fallback routed by intent: ${fallbackRoute.displayName} (${fallbackRoute.reason})`); } } catch (fallbackRouteError) { chatApiLogger.warn("[Smart Auto Mode] Fallback routeByIntent failed, use local heuristic:", fallbackRouteError); } if (!bestModel && hasImage) { bestModel = allModels.find( (m) => m.model.supportsImageGen && !m.model.isSubModel && !isVideoProviderType(m.provider?.type, m.model), ); if (isAutoTimeout) { smartAutoFriendlyMessage = buildAutoModeFallbackMessage({ text: autoTextForFallback, hasImage: true, fromHistory: autoFromHistoryForFallback || smartAutoHistoryImages.length > 0, imageSummary: fallbackVisualSummary, }); } else { smartAutoFriendlyMessage = smartAutoFriendlyMessage || buildAutoModeFallbackMessage({ text: autoTextForFallback, hasImage: true, fromHistory: autoFromHistoryForFallback || smartAutoHistoryImages.length > 0, imageSummary: fallbackVisualSummary, }); } } else { smartAutoFriendlyMessage = smartAutoFriendlyMessage || buildAutoModeFallbackMessage({ text: autoTextForFallback, hasImage: false, fromHistory: false, }); } if (!bestModel) { bestModel = allModels.find( (m) => m.model.isDefault && !m.model.isSubModel && !isVideoProviderType(m.provider?.type, m.model), ); } if (!bestModel && allModels.length > 0) { bestModel = allModels.find( (m) => !m.model.isSubModel && !isVideoProviderType(m.provider?.type, m.model), ) || allModels.find((m) => !m.model.isSubModel) || allModels[0]; } if (shouldKeepSelectedModel) { finalModelId = requestedModelId; chatApiLogger.info("[Smart Auto Mode] 🔒 Keep user-selected model in fallback mode"); } else if (bestModel) { finalModelId = bestModel.model.id; chatApiLogger.info(`[Smart Auto Mode] Fallback selected: ${bestModel.model.displayName}`); } else { return new ChatSDKError("bad_request:api", "No available models for auto mode").toResponse(); } } } // ========== End Smart Auto Mode Logic ========== if (shouldRunAgentConversation && !allowAgentVideoGeneration) { const finalModelIsUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(finalModelId); let finalProviderType: string | undefined; let finalModelConfig: Awaited> | null = null; if (finalModelIsUuid) { finalModelConfig = await getAiModelById({ id: finalModelId }).catch(() => null); if (finalModelConfig) { finalProviderType = ( await getAiProviderById({ id: finalModelConfig.providerId }).catch(() => null) )?.type as string | undefined; } } if (isVideoProviderType(finalProviderType, finalModelConfig || undefined)) { const allModels = await getEnabledAiModelsWithProvider(false); const fallbackModel = allModels.find( (m) => !m.model.isSubModel && m.model.supportsImageGen && !isVideoProviderType(m.provider?.type, m.model), ) || allModels.find( (m) => !m.model.isSubModel && !isVideoProviderType(m.provider?.type, m.model), ); if (fallbackModel) { const hasImageInput = message.parts.some((part: any) => part?.type === "file" || part?.type === "image" || part?.type === "image_url" ); smartAutoDetectedIntent = hasImageInput ? "image_edit" : "image_gen"; smartAutoExecution = smartAutoExecution ? { ...smartAutoExecution, mode: hasImageInput ? "edit_image" : "generate_image", routeIntent: hasImageInput ? "image_edit" : "image_gen", shouldApplyImagePipeline: true, shouldApplyVideoPipeline: false, shouldOptimizePrompt: true, shouldStayConversational: false, shouldUseStructuredReply: true, } : smartAutoExecution; finalModelId = fallbackModel.model.id; smartAutoVideoPlan = undefined; smartAutoRecommendedVideoParams = undefined; chatApiLogger.info(`[Smart Auto Mode] 🚫 Safety override: replace video model (${finalProviderType}) with ${fallbackModel.model.displayName}`); } } } if ( shouldRunAgentConversation && !smartAutoNeedsClarification && !smartAutoFriendlyMessage?.trim() ) { const latestUserText = message.parts .filter((part): part is { type: "text"; text: string } => part.type === "text") .map((part) => part.text) .join(" "); const hasCurrentImageInput = message.parts.some((part: any) => part?.type === "file" || part?.type === "image" || part?.type === "image_url" ); const hasHistoryImageInput = smartAutoHistoryImages.length > 0; smartAutoFriendlyMessage = smartAutoExecution?.shouldUseStructuredReply ? buildAutoModeFallbackMessage({ text: latestUserText, hasImage: hasCurrentImageInput || hasHistoryImageInput, fromHistory: !hasCurrentImageInput && hasHistoryImageInput, }) : "收到,我先理解你的当前目标,再继续处理。"; chatApiLogger.info("[Smart Auto Mode] 🧩 Backfilled conversational reply with fallback"); } if ( shouldRunAgentConversation && !smartAutoNeedsClarification && smartAutoExecution?.shouldUseStructuredReply ) { const latestUserText = message.parts .filter((part): part is { type: "text"; text: string } => part.type === "text") .map((part) => part.text) .join(" "); const hasCurrentImageInput = message.parts.some((part: any) => part?.type === "file" || part?.type === "image" || part?.type === "image_url" ); const hasHistoryImageInput = smartAutoHistoryImages.length > 0; smartAutoFriendlyMessage = ensureStructuredAgentFriendlyMessage({ message: smartAutoFriendlyMessage, text: latestUserText, hasImage: hasCurrentImageInput || hasHistoryImageInput, fromHistory: !hasCurrentImageInput && hasHistoryImageInput, }); } const hasUserCustomImageParams = hasUserCustomizedImageParams(requestBody.imageParams); const hasUserCustomVideoParams = hasUserCustomizedVideoParams(requestBody.videoParams); const shouldApplyAgentImageParams = shouldRunAgentConversation && !smartAutoNeedsClarification && Boolean(smartAutoExecution?.shouldApplyImagePipeline); const shouldApplyAgentVideoParams = shouldRunAgentConversation && !smartAutoNeedsClarification && allowAgentVideoGeneration && Boolean(smartAutoExecution?.shouldApplyVideoPipeline); const mergedEffectiveImageParams = shouldApplyAgentImageParams ? mergeImageParamsWithAgentRecommendation({ incoming: requestBody.imageParams, recommended: smartAutoRecommendedImageParams, intent: smartAutoDetectedIntent, hasUserCustomSettings: hasUserCustomImageParams, }) : requestBody.imageParams; const inferredAgentImageCount = shouldApplyAgentImageParams && smartAutoDetectedIntent === "image_gen" ? inferAgentImageCountFromText(requestTextContent) : null; const effectiveImageParams = inferredAgentImageCount !== null ? { ...(mergedEffectiveImageParams || {}), imageCount: inferredAgentImageCount, } : mergedEffectiveImageParams; const effectiveVideoParams = shouldApplyAgentVideoParams ? mergeVideoParamsWithAgentRecommendation({ incoming: requestBody.videoParams, recommended: smartAutoRecommendedVideoParams, hasUserCustomSettings: hasUserCustomVideoParams, }) : requestBody.videoParams; const defaultImageQualityParams = { aspectRatio: "auto", imageSize: smartAutoDetectedIntent === "image_edit" ? "4K" : "2K", imageCount: 1, } as const; const normalizedEffectiveImageSize = normalizeChatImageSize(effectiveImageParams?.imageSize); const effectiveImageGenerationParams = { ...(effectiveImageParams || {}), aspectRatio: effectiveImageParams?.aspectRatio || defaultImageQualityParams.aspectRatio, imageSize: normalizedEffectiveImageSize || defaultImageQualityParams.imageSize, imageCount: effectiveImageParams?.imageCount || defaultImageQualityParams.imageCount, }; if (shouldApplyAgentImageParams) { chatApiLogger.info("[Smart Auto Mode] 🎯 Effective image params:", { incoming: requestBody.imageParams, recommended: smartAutoRecommendedImageParams, inferredImageCount: inferredAgentImageCount, merged: effectiveImageGenerationParams, hasUserCustomImageParams, }); } if (shouldApplyAgentVideoParams) { chatApiLogger.info("[Smart Auto Mode] 🎯 Effective video params:", { incoming: requestBody.videoParams, recommended: smartAutoRecommendedVideoParams, merged: effectiveVideoParams, hasUserCustomVideoParams, hasVideoPlan: Boolean(smartAutoVideoPlan), }); } if ( shouldRunAgentConversation && smartAutoFriendlyMessage && smartAutoExecution?.shouldUseStructuredReply ) { smartAutoFriendlyMessage = sanitizeAgentOutputSettingsSection({ message: smartAutoFriendlyMessage, userImageParams: requestBody.imageParams, hasUserCustomImageParams, }); smartAutoFriendlyMessage = injectOptimizedPromptSection({ message: smartAutoFriendlyMessage, originalText: requestTextContent, optimizedPrompt: smartAutoOptimizedPrompt, }); } if (shouldRunAgentConversation) { const currentImageUrls = extractMessageImageUrls(message.parts as any[]); const summaryImageUrls = currentImageUrls.length > 0 ? currentImageUrls : smartAutoHistoryImages; const shouldRequireImageAnalysis = summaryImageUrls.length > 0; if (shouldRequireImageAnalysis) { try { const { summarizeImageForExecutionReport } = await import("@/lib/ai/smart-analyzer"); const latestUserText = message.parts .filter((part): part is { type: "text"; text: string } => part.type === "text") .map((part) => part.text) .join(" "); const summaryAssetIds = Array.from( new Set([ ...extractMessageImageAssetIds(message.parts as any[]), ...((effectiveContextPayload?.selectedAssetIds || []).filter(Boolean) as string[]), ]), ); let summary: string | null = null; const summaryTimeoutMs = getBudgetedTimeout({ startedAt: requestStartedAt, preferredMs: SMART_AUTO_IMAGE_SUMMARY_TIMEOUT_MS, minimumMs: 6_000, }); if (summaryTimeoutMs) { summary = await withOperationTimeout( summarizeImageForExecutionReport({ text: latestUserText, imageUrls: summaryImageUrls, context: effectiveAutoContextText || undefined, preferredModelId: preferredAgentModelId, assetIds: summaryAssetIds, timeoutMs: summaryTimeoutMs, }), "enrichImageAnalysisSummary", summaryTimeoutMs, ); } else { chatApiLogger.warn("[Smart Auto Mode] Skip image summary enrichment due to pre-response budget"); } if (summary?.trim() && smartAutoExecution?.shouldUseStructuredReply) { const baseMessage = ensureStructuredAgentFriendlyMessage({ message: smartAutoFriendlyMessage, text: latestUserText, hasImage: true, fromHistory: currentImageUrls.length === 0 && summaryImageUrls.length > 0, }); smartAutoFriendlyMessage = ensureImageAnalysisSummarySection( baseMessage || buildAutoModeFallbackMessage({ text: latestUserText, hasImage: true, fromHistory: currentImageUrls.length === 0 && summaryImageUrls.length > 0, imageSummary: summary.trim(), }), summary.trim(), ); chatApiLogger.info("[Smart Auto Mode] ✅ Image analysis summary ready:", summary.trim().slice(0, 160)); } else if ( summary?.trim() && !smartAutoExecution?.shouldUseStructuredReply && !smartAutoFriendlyMessage ) { smartAutoFriendlyMessage = "收到,我会结合这张图的内容直接回答你。"; } else if (smartAutoFriendlyMessage && shouldEnrichImageAnalysisText(smartAutoFriendlyMessage)) { chatApiLogger.warn("[Smart Auto Mode] ⚠️ Image summary unavailable, keep original analysis text"); } } catch (enrichError) { chatApiLogger.warn("[Smart Auto Mode] Skip image analysis enrichment:", enrichError); } } } if ( shouldRunAgentConversation && smartAutoFriendlyMessage && smartAutoExecution?.shouldUseStructuredReply ) { smartAutoFriendlyMessage = normalizeAgentReportForDisplay(smartAutoFriendlyMessage); } if ( shouldRunAgentConversation && !shouldKeepSelectedModel && smartAutoExecution?.shouldStayConversational && preferredAgentModelId ) { if (finalModelId !== preferredAgentModelId) { finalModelId = preferredAgentModelId; chatApiLogger.info(`[Smart Auto Mode] 🧠 Conversational reply follows agent default model: ${configuredAgentConversationModelId || preferredAgentModelId}`); } } if (shouldRunAgentConversation && smartAutoExecution?.shouldStayConversational) { const candidateReplyDescriptor = await resolveAgentModelSlotDescriptor(finalModelId).catch( () => null, ); if (candidateReplyDescriptor) { agentModelSlots = upsertAgentModelSlot(agentModelSlots, "reply", { ...candidateReplyDescriptor, source: "candidate_reply_model", reason: "post_agent_routing", locked: shouldKeepSelectedModel, }); const providerHealth = getProviderHealthSnapshot(candidateReplyDescriptor.providerKey); if ( !shouldKeepSelectedModel && shouldAvoidProvider(providerHealth) ) { let fallbackReplyModelId: string | undefined; if ( preferredAgentModelId && preferredAgentModelId !== finalModelId ) { const preferredDescriptor = await resolveAgentModelSlotDescriptor( preferredAgentModelId, ).catch(() => null); if ( preferredDescriptor && !shouldAvoidProvider(getProviderHealthSnapshot(preferredDescriptor.providerKey)) ) { fallbackReplyModelId = preferredAgentModelId; } } if (!fallbackReplyModelId) { try { const { routeToModel } = await import("@/lib/ai/model-router"); const replyFallbackRoute = await routeToModel({ intent: "chat", subIntent: undefined, hasImage: false, }); const fallbackDescriptor = await resolveAgentModelSlotDescriptor( replyFallbackRoute.id, ).catch(() => null); if ( fallbackDescriptor && !shouldAvoidProvider(getProviderHealthSnapshot(fallbackDescriptor.providerKey)) ) { fallbackReplyModelId = replyFallbackRoute.id; } } catch (replyRouteError) { chatApiLogger.warn("[Smart Auto Mode] Failed to resolve reply fallback route after provider degradation:", replyRouteError); } } if (fallbackReplyModelId && fallbackReplyModelId !== finalModelId) { finalModelId = fallbackReplyModelId; const fallbackDescriptor = await resolveAgentModelSlotDescriptor(finalModelId).catch( () => null, ); if (fallbackDescriptor) { agentModelSlots = upsertAgentModelSlot(agentModelSlots, "reply", { ...fallbackDescriptor, source: "provider_health_fallback", reason: providerHealth?.lastEventType || "degraded_provider", locked: false, }); } chatApiLogger.warn(`[Smart Auto Mode] ♻️ Switched reply model due provider health: ${candidateReplyDescriptor.displayName || candidateReplyDescriptor.modelId} -> ${fallbackDescriptor?.displayName || fallbackReplyModelId}`); } } } } const session = await auth(); if (!session?.user) { return new ChatSDKError("unauthorized:chat").toResponse(); } const userType: UserType = session.user.type; const messageCount = await getMessageCountByUserId({ id: session.user.id, differenceInHours: 24, }); if (messageCount > entitlementsByUserType[userType].maxMessagesPerDay) { return new ChatSDKError("rate_limit:chat").toResponse(); } // ========== 使用时间限制验证(后端安全检查) ========== try { // 从数据库获取网站设置 const { siteSettings } = await import("@/lib/db/schema"); const [settings] = await db.select().from(siteSettings).limit(1); if (settings?.usageTimeEnabled) { const now = new Date(); const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`; const startTime = settings.usageTimeStart || "08:00"; const endTime = settings.usageTimeEnd || "20:00"; const isWithinUsageTime = currentTime >= startTime && currentTime <= endTime; if (!isWithinUsageTime) { chatApiLogger.info(`[Chat API] ⛔ 访问被拒绝:当前时间 ${currentTime} 不在允许范围 ${startTime}-${endTime}`); return NextResponse.json( { error: "usage_time_restricted", message: settings.usageTimeMessage || "当前不在系统使用时间段内", currentTime, allowedTime: `${startTime} - ${endTime}` }, { status: 403 } ); } chatApiLogger.info(`[Chat API] ✅ 时间验证通过:${currentTime} 在允许范围 ${startTime}-${endTime}`); } } catch (timeCheckError) { chatApiLogger.error("[Chat API] ⚠️ 时间验证失败(继续执行):", timeCheckError); // 如果时间检查失败,继续执行(避免因检查逻辑错误导致系统不可用) } let chat = await getChatById({ id }); let messagesFromDb: DBMessage[] = []; let messagesForMediaInput: DBMessage[] = []; if (chat) { if (chat.userId !== session.user.id) { return new ChatSDKError("forbidden:chat").toResponse(); } // Only fetch messages if chat already exists messagesFromDb = await getMessagesByChatId({ id }); } else { const title = await generateTitleFromUserMessage({ message, }); try { await saveChat({ id, userId: session.user.id, title, visibility: selectedVisibilityType, }); } catch (error) { const racedChat = await findChatWithRetry(id); if (!racedChat) { throw error; } if (racedChat.userId !== session.user.id) { return new ChatSDKError("forbidden:chat").toResponse(); } chatApiLogger.warn("[Chat API] saveChat conflicted, reused existing chat:", { chatId: id, userId: session.user.id, }); } chat = await findChatWithRetry(id); if (!chat) { throw new ChatSDKError("bad_request:database", "Failed to save chat"); } if (chat.userId !== session.user.id) { return new ChatSDKError("forbidden:chat").toResponse(); } // In a race scenario another request may have already written messages. messagesFromDb = await getMessagesByChatId({ id }); } const loadedMessagesForBoundary = shouldIsolateDirectComposerContext ? [] : messagesFromDb; messagesFromDb = shouldIsolateDirectComposerContext ? [] : pruneTaskBoundaryHistoryMessages({ decision: taskBoundaryDecision, messages: loadedMessagesForBoundary, purpose: "model", hasCurrentImageInput: hasImageInputForConversationAction, }); messagesForMediaInput = shouldIsolateDirectComposerContext ? [] : pruneTaskBoundaryHistoryMessages({ decision: taskBoundaryDecision, messages: loadedMessagesForBoundary, purpose: "media", hasCurrentImageInput: hasImageInputForConversationAction, }); recentDialogueMemory = shouldIsolateDirectComposerContext ? recentDialogueMemory : buildRecentDialogueMemory({ messages: loadedMessagesForBoundary, currentText: requestTextContent, maxEntries: taskBoundaryDecision.taskType === "chat" ? 6 : 4, maxTotalChars: taskBoundaryDecision.taskType === "chat" ? 1000 : 700, }); if (recentDialogueMemory.entries.length > 0) { chatApiLogger.info("[RecentDialogueMemory] Request recall:", { entries: recentDialogueMemory.entries.length, isRecallRequest: recentDialogueMemory.isRecallRequest, taskType: taskBoundaryDecision.taskType, }); } if (!requestWorkingState && !shouldIsolateDirectComposerContext) { requestWorkingState = await getConversationWorkingStateByChatId({ chatId: id, }); } if (!requestResolvedMediaInput) { requestResolvedMediaInput = resolveChatMediaInput({ messageParts: message.parts as any[], historyMessages: messagesForMediaInput, contextPayload: effectiveContextPayload, requestOrigin, maxImages: 4, }); } if (!requestTargetResolution) { requestTargetResolution = resolveTargetAssets({ text: requestTextContent, mediaInput: requestResolvedMediaInput, contextPayload: effectiveContextPayload, workingState: effectiveWorkingState || undefined, }); } if ( requestResolvedMediaInput.currentMessageImageUrls.length === 0 && smartAutoHistoryImages.length === 0 && requestTargetResolution.targetImageUrls.length > 0 ) { smartAutoHistoryImages = requestTargetResolution.targetImageUrls.slice(0, 4); } const uiMessages = [...convertToUIMessages(messagesFromDb), message]; const hiddenPromptText = typeof hiddenPrompt === "string" ? hiddenPrompt.trim() : ""; const canvasAnnotationHiddenPrompt = canvasAgentFeatures.annotationEditEnabled ? buildCanvasAgentAnnotationEditPrompt({ context: requestCanvasAgentContext, targetNodeIds: (requestTargetResolution?.targetRefs || []) .map((ref) => ref.nodeId) .filter((nodeId): nodeId is string => Boolean(nodeId)), userText: requestTextContent, }) : ""; const hiddenPromptPrefixText = [ hiddenPromptText, canvasAnnotationHiddenPrompt, ].filter(Boolean).join("\n\n"); const hiddenPromptPrefix = hiddenPromptPrefixText ? `${hiddenPromptPrefixText}\n\n` : ""; const uiMessagesForModel = hiddenPromptPrefixText ? applyHiddenPromptToMessages(uiMessages, hiddenPromptPrefixText) : uiMessages; if (hiddenPromptText) { const hiddenPromptLines = hiddenPromptText .split("\n") .map((line) => line.trim()) .filter(Boolean); chatApiLogger.info("[Chat API] Hidden workflow prompt summary:", { lineCount: hiddenPromptLines.length, preview: hiddenPromptLines.slice(0, 12), }); } if (canvasAnnotationHiddenPrompt) { chatApiLogger.info("[Chat API] Canvas annotation edit prompt enabled:", { targetNodeCount: (requestTargetResolution?.targetRefs || []).filter((ref) => Boolean(ref.nodeId) ).length, hasAnnotationIntent: hasCanvasAgentAnnotationEditIntent(requestTextContent), preview: canvasAnnotationHiddenPrompt.split("\n").slice(0, 6), }); } const { longitude, latitude, city, country } = geolocation(request); const requestHints: RequestHints = { longitude, latitude, city, country, }; const insertedUserMessage = await db .insert(messageTable) .values({ chatId: id, id: message.id, role: "user", parts: message.parts as any, attachments: collectMessageImageAttachments({ parts: message.parts }), createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }) .onConflictDoNothing({ target: [messageTable.id], }) .returning({ id: messageTable.id }); if (insertedUserMessage.length === 0) { chatApiLogger.warn(`[Chat API] Duplicate user message ignored (chatId=${id}, messageId=${message.id})`); const duplicateStream = createUIMessageStream({ execute: async ({ writer }) => { await replayAssistantMessagesForDuplicateRequest({ chatId: id, userMessageId: message.id, writer, source: "duplicate-user-message", }); }, }); return createSseResponse(duplicateStream); } if (requestResolvedMediaInput && requestTargetResolution) { try { await createMessageTargetResolution({ userId: session.user.id, chatId: id, messageId: message.id, projectId: contextPayload?.projectId || null, rawText: requestTextContent, uploadedAssetIds: requestResolvedMediaInput.uploadedAssetIds, uploadedImageUrls: requestResolvedMediaInput.currentMessageImageUrls, selectedAssetIds: requestResolvedMediaInput.selectedAssetIds, selectedImageUrls: requestResolvedMediaInput.selectedImageUrls, candidateAssetIds: requestResolvedMediaInput.candidateAssetIds, candidateImageUrls: requestResolvedMediaInput.imageRefs.map((ref) => ref.url), resolvedAssetIds: requestTargetResolution.targetAssetIds, resolvedImageUrls: requestTargetResolution.targetImageUrls, resolutionMode: requestTargetResolution.resolutionMode, confidence: requestTargetResolution.confidence, needsClarification: requestTargetResolution.needsClarification, reasoning: { reason: requestTargetResolution.reason, fromHistory: requestTargetResolution.fromHistory, focusPolicy: contextPayload?.focusPolicy || null, contextSource: contextPayload?.source || null, }, }); await persistResolvedTargetTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, currentIntent: smartAutoExecution?.routeIntent || smartAutoDetectedIntent || (requestResolvedMediaInput.hasImage ? "vision" : requestBody.videoParams ? "video_gen" : null), currentFocusAssetIds: requestTargetResolution.targetAssetIds, currentFocusImageUrls: requestTargetResolution.targetImageUrls, selectedAssetIds: requestResolvedMediaInput.selectedAssetIds, selectedImageUrls: requestResolvedMediaInput.selectedImageUrls, lastUploadedAssetIds: requestResolvedMediaInput.uploadedAssetIds, lastUploadedImageUrls: requestResolvedMediaInput.currentMessageImageUrls, lastReferencedAssetIds: requestTargetResolution.candidateAssetIds, lastReferencedImageUrls: requestTargetResolution.candidateImageUrls, lastResolvedTargetAssetIds: requestTargetResolution.targetAssetIds, lastResolvedTargetImageUrls: requestTargetResolution.targetImageUrls, activeCanvasNodeIds: contextPayload?.selectedNodeIds || [], lastCanvasSnapshot: Array.isArray(contextPayload?.canvasImageNodes) && contextPayload?.canvasImageNodes.length > 0 ? { source: contextPayload?.source || "canvas", selectedNodeIds: contextPayload?.selectedNodeIds || [], canvasImageNodes: contextPayload?.canvasImageNodes, } : null, lastMessageId: message.id, text: requestTextContent, resolutionMode: requestTargetResolution.resolutionMode, confidence: requestTargetResolution.confidence, }); } catch (memoryError) { chatApiLogger.warn("[Chat API] Failed to persist context-memory state:", memoryError); } } const buildCurrentAgentTargetLockPayload = () => { if (!shouldRunAgentConversation) return null; if (composerMode !== "agent") return null; if (effectiveContextPayload?.source !== "canvas") return null; if (!requestResolvedMediaInput || !requestTargetResolution) return null; return buildAgentTargetLockPayload({ requestTargetResolution, requestResolvedMediaInput, needsClarification: smartAutoNeedsClarification, clarificationMessage: smartAutoClarificationQuestion || smartAutoFriendlyMessage || undefined, }); }; const writeCurrentAgentTargetLockChunk = ( writer: StreamWriter, label: string, ) => { const payload = buildCurrentAgentTargetLockPayload(); if (!payload) return; writeAgentTargetLockChunk(writer, payload, label); }; const getConversationWorkingSummary = () => requestWorkingState?.lastTurnSummary && typeof requestWorkingState.lastTurnSummary === "object" && !Array.isArray(requestWorkingState.lastTurnSummary) ? (requestWorkingState.lastTurnSummary as Record) : {}; const persistPendingCanvasOperation = async ( pendingCanvasOperation: ReturnType | null ) => { await upsertConversationWorkingState({ userId: session.user.id, chatId: id, projectId: pendingCanvasOperation?.projectId || contextPayload?.projectId || null, currentIntent: pendingCanvasOperation ? "canvas_operation" : undefined, lastMessageId: message.id, lastTurnSummary: { ...getConversationWorkingSummary(), pendingCanvasOperation, }, }).catch((error) => { chatApiLogger.warn("[CanvasAgentConfirmation] Failed to persist pending operation:", error); return null; }); }; const applyDirectAgentCanvasWriteback = async (params: { writer: StreamWriter; assistantMessageId: string; operationLabel: string; resultType: "image" | "video"; resultUrls: string[]; prompt: string; mode: "generate" | "edit" | "video"; sourceImageUrls?: string[]; assetIdsByUrl?: Record; model?: string | null; targetSlotId?: string | null; }): Promise<{ mediaItemIds: string[]; handledCanvasMediaItemIds: string[]; }> => { const projectId = contextPayload?.projectId; if ( contextPayload?.source !== "canvas" || !projectId || !requestCanvasAgentContext ) { return { mediaItemIds: [], handledCanvasMediaItemIds: [] }; } const writebackPlan = buildCanvasAgentDirectWritebackPlan({ projectId, chatId: id, assistantMessageId: params.assistantMessageId, operationLabel: params.operationLabel, resultType: params.resultType, resultUrls: params.resultUrls, prompt: params.prompt, mode: params.mode, context: requestCanvasAgentContext, requestTextContent, targetRefs: requestTargetResolution?.targetRefs || [], sourceImageUrls: params.sourceImageUrls, assetIdsByUrl: params.assetIdsByUrl, model: params.model, targetSlotId: params.targetSlotId, }); if (!writebackPlan) { return { mediaItemIds: [], handledCanvasMediaItemIds: [] }; } const { mediaItemIds, mediaItems, operation, operationId, plan, selectedMediaNodes, shouldFillSlot, } = writebackPlan; if (!canApplyCanvasAgentMutation) { if (shouldStreamCanvasAgentShadowOperations(canvasAgentFeatures)) { writeCanvasOperationShadowChunk({ writer: params.writer, label: `${params.operationLabel}:canvas-operation-shadow`, projectId, operationId, operation, anchorNodeId: plan.anchorNodeId || plan.targetSlotId || null, mediaItemIds, }); } chatApiLogger.info("[CanvasAgentOperation] Skipped direct canvas mutation:", { operationId, operation, writebackMode: canvasAgentFeatures.writebackMode, operationProtocolEnabled: canvasAgentFeatures.operationProtocolEnabled, mediaItemCount: mediaItems.length, }); return { mediaItemIds, handledCanvasMediaItemIds: [], }; } const wantsReplaceExistingMedia = params.mode === "edit" && params.resultType === "image" && /覆盖|替换原图|替换当前|直接替换|替换选中|替换这张|覆盖原图/.test( requestTextContent ); const replaceTargetNodeIds = selectedMediaNodes .filter((node) => node.type === params.resultType) .map((node) => node.nodeId) .filter(Boolean) .slice(0, mediaItems.length); if ( wantsReplaceExistingMedia && replaceTargetNodeIds.length === mediaItems.length && replaceTargetNodeIds.length > 0 ) { const replaceOperationId = `${operationId}:replace`; const replacePlan: CanvasOperationPlan = { ...plan, operationId: replaceOperationId, operation: "replace_node_media", sourceNodeIds: replaceTargetNodeIds, targetNodeIds: replaceTargetNodeIds, anchorNodeId: undefined, targetSlotId: undefined, confirmed: false, mediaItems: mediaItems.map((item, index) => ({ ...item, sourceNodeIds: [replaceTargetNodeIds[index]], })), metadata: { ...(plan.metadata || {}), pendingConfirmation: true, riskOperation: "replace_node_media", }, }; const pendingReply = "新结果已生成。替换原图会覆盖现有节点,请回复“确认执行”后我再替换。"; await persistPendingCanvasOperation( buildPendingCanvasOperation({ projectId, messageId: message.id, userText: requestTextContent, riskOperation: "replace_node_media", targetNodeIds: replaceTargetNodeIds, reply: pendingReply, operationPlan: replacePlan, viewPlan: { version: 1, viewOperationId: `${replaceOperationId}:view`, operation: replaceTargetNodeIds.length > 1 ? "focus_nodes" : "focus_selection", projectId, nodeIds: replaceTargetNodeIds, anchorNodeId: replaceTargetNodeIds[0], padding: 120, persist: false, reason: "focus_pending_replace_node_media", }, }) ); writeAgentProgressChunk( params.writer, { stage: "clarify", currentStep: "finalize", title: "替换原图需要确认", message: pendingReply, toolName: "canvasOperation", capability: "replace_node_media", }, `${params.operationLabel}:replace-confirmation`, ); return { mediaItemIds, handledCanvasMediaItemIds: [], }; } try { const result = await applyCanvasOperationPlan({ userId: session.user.id, projectId, plan, }); const handledCanvasMediaItemIds = mediaItems.map((item) => item.mediaItemId); writeCanvasOperationChunk( params.writer, { projectId, operationId, operation, createdNodeIds: result.createdNodeIds, createdNodeIdsByMediaItemId: result.createdNodeIdsByMediaItemId, updatedNodeIds: result.updatedNodeIds, createdEdgeIds: result.createdEdgeIds, canvasRevision: result.canvasRevision, anchorNodeId: plan.anchorNodeId || plan.targetSlotId || null, handledCanvasMediaItemIds, message: shouldFillSlot ? "已填充画布槽位" : "已写回画布", }, `${params.operationLabel}:canvas-operation`, ); if (canvasAgentFeatures.viewOperationEnabled) { const viewOperation = buildCanvasAgentResultViewOperation({ projectId, operationId, mutationOperation: operation, sourceNodeIds: plan.sourceNodeIds, createdNodeIds: result.createdNodeIds, updatedNodeIds: result.updatedNodeIds, anchorNodeId: result.createdNodeIds[0] || result.updatedNodeIds[0] || null, }); if (viewOperation) { writeCanvasViewOperationChunk( params.writer, viewOperation, `${params.operationLabel}:canvas-view-operation`, ); } } return { mediaItemIds: handledCanvasMediaItemIds, handledCanvasMediaItemIds, }; } catch (error) { const failure = writeCanvasOperationFailureChunk({ writer: params.writer, label: `${params.operationLabel}:canvas-operation-failed`, projectId, operationId, operation, error, anchorNodeId: plan.anchorNodeId || plan.targetSlotId || null, mediaItemIds, fallbackToCanvasMedia: true, }); chatApiLogger.warn("[CanvasAgentOperation] Writeback failed, fallback to canvasMedia:", { operationId, errorCode: failure.errorCode, error, }); return { mediaItemIds, handledCanvasMediaItemIds: [], }; } }; const createDirectAgentTargetSlotIfNeeded = async (params: { writer: StreamWriter; assistantMessageId: string; operationLabel: string; resultType: "image" | "video"; }) => { const projectId = contextPayload?.projectId; if ( contextPayload?.source !== "canvas" || !projectId || !requestCanvasAgentContext || !canvasAgentFeatures.slotEnabled ) { return null; } if (getSelectedSlots(requestCanvasAgentContext).length > 0) { return null; } const slotIntent = resolveCanvasAgentSlotCreationIntent({ userText: requestTextContent, }); if (!slotIntent.shouldCreate || !slotIntent.combinedWithMediaExecution) { return null; } const operationId = `${params.assistantMessageId}:${params.operationLabel}:create-slot`; const plan = buildCanvasAgentSlotCreationPlan({ context: requestCanvasAgentContext, projectId, messageId: params.assistantMessageId, chatId: id, userText: requestTextContent, operationId, slotType: slotIntent.slotType, slotLabel: slotIntent.label, aspectRatio: slotIntent.aspectRatio, }); if (!plan) return null; if (!canApplyCanvasAgentMutation) { if (shouldStreamCanvasAgentShadowOperations(canvasAgentFeatures)) { writeCanvasOperationShadowChunk({ writer: params.writer, label: `${params.operationLabel}:create-slot-operation-shadow`, projectId, operationId, operation: plan.operation, anchorNodeId: plan.anchorNodeId || null, message: "已记录创建画布槽位计划(灰度模式,未修改画布)", }); } chatApiLogger.info( "[CanvasAgentOperation] Skipped target slot creation mutation:", { operationId, resultType: params.resultType, writebackMode: canvasAgentFeatures.writebackMode, operationProtocolEnabled: canvasAgentFeatures.operationProtocolEnabled, } ); return null; } try { writeAgentProgressChunk( params.writer, { stage: "executing", currentStep: "execute", title: "正在创建画布槽位", message: "我会先在当前视口创建一个空槽位,再把本次结果填进去。", toolName: "canvasOperation", capability: "create_agent_slot", }, `${params.operationLabel}:create-slot-progress`, ); const result = await applyCanvasOperationPlan({ userId: session.user.id, projectId, plan, }); const createdSlotId = result.createdNodeIds[0] || null; writeCanvasOperationChunk( params.writer, { projectId, operationId: plan.operationId, operation: plan.operation, createdNodeIds: result.createdNodeIds, createdNodeIdsByMediaItemId: result.createdNodeIdsByMediaItemId, updatedNodeIds: result.updatedNodeIds, createdEdgeIds: result.createdEdgeIds, canvasRevision: result.canvasRevision, anchorNodeId: createdSlotId, handledCanvasMediaItemIds: [], message: "已创建画布槽位", }, `${params.operationLabel}:create-slot-operation`, ); return createdSlotId; } catch (error) { const failure = writeCanvasOperationFailureChunk({ writer: params.writer, label: `${params.operationLabel}:create-slot-operation-failed`, projectId, operationId, operation: plan.operation, error, fallbackToCanvasMedia: false, }); chatApiLogger.warn( "[CanvasAgentOperation] Failed to create target slot before media writeback:", { operationId, resultType: params.resultType, errorCode: failure.errorCode, error, } ); return null; } }; const resolveCanvasViewOperationPlan = (): CanvasViewOperationPlan | null => { if (!canvasAgentFeatures.viewOperationEnabled) return null; if (composerMode !== "agent" && !hasCanvasControlIntentFastPath) return null; const projectId = contextPayload?.projectId || requestCanvasAgentContext?.projectId || (hasCanvasControlIntentFastPath ? id : undefined); if (!projectId) return null; const text = requestTextContent.trim(); if (!text) return null; const lowerText = text.toLowerCase(); const hasComparisonViewIntent = hasCanvasAgentComparisonViewIntent(text); const hasLatestResultViewIntent = hasCanvasAgentLatestResultViewIntent(text); const hasPanViewIntent = hasCanvasAgentPanViewIntent(text); const hasViewKeyword = hasComparisonViewIntent || hasLatestResultViewIntent || hasPanViewIntent || /视角|视野|画布|聚焦|定位|居中|看全局|看整体|看全部|缩小|放大|拉近|拉远|zoom/.test( lowerText ); if (!hasViewKeyword) return null; const viewOperationId = `${message.id}:canvas-view:${Date.now()}`; if (!requestCanvasAgentContext) { if (/看全局|看整体|看全部|全局|整体/.test(lowerText)) { return { version: 1, viewOperationId, operation: "fit_all", projectId, padding: 140, persist: false, reason: "fit_all_canvas", }; } if (/缩小|拉远|zoom\s*out/.test(lowerText)) { return { version: 1, viewOperationId, operation: "zoom_out", projectId, persist: false, reason: "zoom_out_canvas", }; } if (/放大|拉近|zoom\s*in/.test(lowerText)) { return { version: 1, viewOperationId, operation: "zoom_in", projectId, persist: false, reason: "zoom_in_canvas", }; } return null; } if (hasLatestResultViewIntent) { const latestResultViewPlan = buildCanvasAgentLatestResultViewPlan({ userText: text, projectId, operationId: viewOperationId, }); if (latestResultViewPlan) return latestResultViewPlan; } const hasMediaMutationKeyword = /生成|出图|做一版|再出|换|改|编辑|抠图|去水印|高清|清晰|扩图|裁剪|删除|替换|背景|视频|模型|洗图|重绘|尺寸|像素|分辨率/.test( lowerText ); if (hasMediaMutationKeyword) return null; const explicitTargetNodeIds = Array.from( new Set( [ ...(requestTargetResolution?.targetRefs || []) .map((ref) => ref.nodeId) .filter(Boolean), ...(requestCanvasAgentContext.selectedNodeIds || []), ].filter((value): value is string => Boolean(value)) ) ).slice(0, 20); const anchorNode = getBestAnchorNode(requestCanvasAgentContext) || getSelectedMediaNodes(requestCanvasAgentContext)[0] || requestCanvasAgentContext.selectedNodes[0] || null; const hasSpecificFocusTarget = explicitTargetNodeIds.length > 0 || Boolean(anchorNode?.nodeId); if (hasComparisonViewIntent) { const comparisonViewPlan = buildCanvasAgentComparisonViewPlan({ context: requestCanvasAgentContext, projectId, operationId: viewOperationId, }); if (comparisonViewPlan) return comparisonViewPlan; } if (/看全局|看整体|看全部|全局|整体/.test(lowerText)) { return { version: 1, viewOperationId, operation: "fit_all", projectId, padding: 140, persist: false, reason: "fit_all_canvas", }; } if (/缩小|拉远|zoom\s*out/.test(lowerText)) { return { version: 1, viewOperationId, operation: "zoom_out", projectId, persist: false, reason: "zoom_out_canvas", }; } if ( /聚焦|定位|居中|选中|这张|当前|主体|编辑对象|放大.*看|看清/.test(lowerText) && hasSpecificFocusTarget ) { const nodeIds = explicitTargetNodeIds.length > 0 ? explicitTargetNodeIds : anchorNode?.nodeId ? [anchorNode.nodeId] : []; return { version: 1, viewOperationId, operation: nodeIds.length > 1 ? "focus_nodes" : "focus_selection", projectId, nodeIds, anchorNodeId: anchorNode?.nodeId, padding: 120, persist: false, reason: "focus_canvas_target", }; } if (hasPanViewIntent) { const panViewPlan = buildCanvasAgentPanViewPlan({ userText: text, context: requestCanvasAgentContext, projectId, operationId: viewOperationId, }); if (panViewPlan) return panViewPlan; } if (/放大|拉近|zoom\s*in/.test(lowerText)) { return { version: 1, viewOperationId, operation: "zoom_in", projectId, persist: false, reason: "zoom_in_canvas", }; } return null; }; const resolveCanvasLinkOperationPlan = (): CanvasOperationPlan | null => { if (!canApplyCanvasAgentMutation) return null; if (composerMode !== "agent" && !hasCanvasControlIntentFastPath) return null; if (!isCanvasAgentCanvasRequest || !canvasAgentProjectId) return null; if (!requestCanvasAgentContext) return null; const text = requestTextContent.trim().toLowerCase(); if (!/连起来|连线|连接|关联|参考关系|版本关系/.test(text)) { return null; } if (/生成|出图|换|编辑|抠图|删除|替换|背景|视频|洗图|重绘/.test(text)) { return null; } const nodeIds = Array.from( new Set( [ ...(requestTargetResolution?.targetRefs || []) .map((ref) => ref.nodeId) .filter(Boolean), ...(requestCanvasAgentContext.selectedNodeIds || []), ].filter((value): value is string => Boolean(value)) ) ).slice(0, 20); if (nodeIds.length < 2) return null; return { version: 1, operationId: `${message.id}:canvas-link`, operation: "link_nodes", projectId: canvasAgentProjectId, baseCanvasRevision: requestCanvasAgentContext.canvasRevision, baseCanvasUpdatedAt: requestCanvasAgentContext.canvasUpdatedAt, sourceNodeIds: nodeIds, edgeType: "reference", preserveOriginal: true, metadata: { messageId: message.id, chatId: id, prompt: requestTextContent, }, }; }; const extractCanvasTextUpdateContent = (rawText: string) => { const text = rawText.trim(); const hasExplicitFinalTextDelimiter = /[::"“'‘]/.test(text); const patterns = [ /(?:改成|改为|改到|替换为|设置为|设为|写成|变成)\s*[::]?\s*([\s\S]+)$/i, /[::]\s*([\s\S]+)$/i, ]; for (const pattern of patterns) { const match = text.match(pattern); const content = String(match?.[1] || "") .trim() .replace(/^["'“”‘’]+|["'“”‘’。;;]+$/g, "") .trim(); if (!content) continue; const isVagueRewriteInstruction = !hasExplicitFinalTextDelimiter && content.length <= 24 && /更|高级|高级感|优化|润色|简洁|专业|吸引|好看|一点|一些|风格|语气/.test( content ); if (isVagueRewriteInstruction) return ""; return content.slice(0, 4000); } return ""; }; const resolveCanvasTextOperationPlan = (): CanvasOperationPlan | null => { if (!canApplyCanvasAgentMutation) return null; if (composerMode !== "agent" && !hasCanvasControlIntentFastPath) return null; if (!isCanvasAgentCanvasRequest || !canvasAgentProjectId) return null; if (!requestCanvasAgentContext) return null; const text = requestTextContent.trim(); if (!text) return null; const hasTextIntent = /文本|文字|文案|标题|这段|选中/.test(text) && /改成|改为|改到|替换为|设置为|设为|写成|变成/.test(text); if (!hasTextIntent) return null; if (/图片|背景|抠图|视频|生成|出图|高清|去水印|扩图|重绘/.test(text)) { return null; } const selectedTextNodeIds = requestCanvasAgentContext.selectedNodes .filter((node) => node.type === "text") .map((node) => node.nodeId) .filter(Boolean) .slice(0, 10); if (selectedTextNodeIds.length === 0) return null; const textContent = extractCanvasTextUpdateContent(text); if (!textContent) return null; return { version: 1, operationId: `${message.id}:canvas-update-text`, operation: "update_text", projectId: canvasAgentProjectId, baseCanvasRevision: requestCanvasAgentContext.canvasRevision, baseCanvasUpdatedAt: requestCanvasAgentContext.canvasUpdatedAt, targetNodeIds: selectedTextNodeIds, textContent, preserveOriginal: false, metadata: { messageId: message.id, chatId: id, prompt: requestTextContent, }, }; }; const resolveCanvasExplicitOperationPlan = (): CanvasAgentExplicitOperationPlan | null => { if (!canApplyCanvasAgentMutation) return null; if (composerMode !== "agent" && !hasCanvasControlIntentFastPath) return null; if (!isCanvasAgentCanvasRequest || !canvasAgentProjectId) return null; if (!requestCanvasAgentContext) return null; return buildCanvasAgentExplicitOperationPlan({ context: requestCanvasAgentContext, projectId: canvasAgentProjectId, messageId: message.id, chatId: id, userText: requestTextContent, requestedNodeIds: (requestTargetResolution?.targetRefs || []) .map((ref) => ref.nodeId) .filter((nodeId): nodeId is string => Boolean(nodeId)), }); }; const resolveCanvasModelPlannerOutcome = async (): Promise => { if (composerMode !== "agent" && !hasCanvasControlIntentFastPath) return null; if (!isCanvasAgentCanvasRequest || !canvasAgentProjectId) return null; if (!requestCanvasAgentContext) return null; if ( !canvasAgentFeatures.viewOperationEnabled && !canApplyCanvasAgentMutation ) { return null; } const plannerModelId = configuredAgentConversationModelId || (requestedModelIsFixedVisual ? undefined : preferredAgentModelId); const plannerModel = plannerModelId ? await getLanguageModelById(plannerModelId).catch((error) => { chatApiLogger.warn( "[CanvasAgentModelPlanner] Failed to resolve preferred planner model:", error ); return null; }) : null; try { return await planCanvasAgentOperationWithModel({ model: plannerModel || myProvider.languageModel("chat-model"), userText: requestTextContent, context: requestCanvasAgentContext, projectId: canvasAgentProjectId, messageId: message.id, chatId: id, }); } catch (error) { chatApiLogger.warn("[CanvasAgentModelPlanner] Planning failed:", error); return null; } }; const streamId = generateUUID(); await createStreamId({ streamId, chatId: id }); const pendingCanvasOperation = extractPendingCanvasOperationFromWorkingState(requestWorkingState); if ( pendingCanvasOperation && (isCanvasOperationConfirmText(requestTextContent) || isCanvasOperationCancelText(requestTextContent)) ) { const isCancelPendingCanvasOperation = isCanvasOperationCancelText(requestTextContent); const isConfirm = !isCancelPendingCanvasOperation; const pendingCanvasOperationStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); if (!isConfirm) { const reply = "已取消这次待确认的画布操作。"; await persistPendingCanvasOperation(null); writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: "已取消画布操作", message: reply, toolName: "canvasOperation", capability: pendingCanvasOperation.riskOperation, }, "canvas-pending-operation:cancelled", ); writeAssistantTextChunk( writer, reply, "canvas-pending-operation:cancelled-reply", messageId, ); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: reply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); return; } const confirmedPlan = withConfirmedCanvasOperationPlan( pendingCanvasOperation, message.id ); if (!confirmedPlan) { const reply = "已收到确认,但这个高风险画布操作当前还没有开放自动执行。"; await persistPendingCanvasOperation(null); writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: "画布操作未执行", message: reply, toolName: "canvasOperation", capability: pendingCanvasOperation.riskOperation, }, "canvas-pending-operation:unsupported", ); writeAssistantTextChunk( writer, reply, "canvas-pending-operation:unsupported-reply", messageId, ); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: reply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); return; } if (!canApplyCanvasAgentMutation) { const reply = "已收到确认,但当前 Agent 画布写回处于灰度模式,未修改画布。"; await persistPendingCanvasOperation(null); chatApiLogger.info( "[CanvasAgentOperation] Skipped confirmed pending operation in non-authoritative mode:", { writebackMode: canvasAgentFeatures.writebackMode, operationId: confirmedPlan.operationId, operation: confirmedPlan.operation, } ); writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: "画布操作已进入灰度保护", message: reply, toolName: "canvasOperation", capability: confirmedPlan.operation, }, "canvas-pending-operation:shadow", ); writeAssistantTextChunk( writer, reply, "canvas-pending-operation:shadow-reply", messageId, ); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: reply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); return; } const reply = "已确认,画布操作已执行。"; writeAgentProgressChunk( writer, { stage: "executing", currentStep: "execute", title: "正在执行已确认的画布操作", message: reply, toolName: "canvasOperation", capability: confirmedPlan.operation, }, "canvas-pending-operation:executing", ); try { const result = await applyCanvasOperationPlan({ userId: session.user.id, projectId: confirmedPlan.projectId, plan: confirmedPlan, }); await persistPendingCanvasOperation(null); writeCanvasOperationChunk( writer, { projectId: confirmedPlan.projectId, operationId: confirmedPlan.operationId, operation: confirmedPlan.operation, createdNodeIds: result.createdNodeIds, createdNodeIdsByMediaItemId: result.createdNodeIdsByMediaItemId, updatedNodeIds: result.updatedNodeIds, createdEdgeIds: result.createdEdgeIds, canvasRevision: result.canvasRevision, anchorNodeId: confirmedPlan.targetNodeIds?.[0] || confirmedPlan.sourceNodeIds?.[0] || confirmedPlan.anchorNodeId || null, handledCanvasMediaItemIds: [], message: reply, }, "canvas-pending-operation:operation", ); if ( pendingCanvasOperation.viewPlan && canvasAgentFeatures.viewOperationEnabled ) { writeCanvasViewOperationChunk( writer, pendingCanvasOperation.viewPlan, "canvas-pending-operation:view-operation", ); } writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: "画布操作完成", message: reply, toolName: "canvasOperation", capability: confirmedPlan.operation, }, "canvas-pending-operation:completed", ); writeAssistantTextChunk( writer, reply, "canvas-pending-operation:reply", messageId, ); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: reply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); } catch (error) { const failure = writeCanvasOperationFailureChunk({ writer, label: "canvas-pending-operation:operation-failed", projectId: confirmedPlan.projectId, operationId: confirmedPlan.operationId, operation: confirmedPlan.operation, error, anchorNodeId: confirmedPlan.targetNodeIds?.[0] || confirmedPlan.sourceNodeIds?.[0] || confirmedPlan.anchorNodeId || null, }); writeAgentProgressChunk( writer, { stage: "failed", currentStep: "finalize", title: "画布操作失败", message: failure.errorMessage, toolName: "canvasOperation", capability: confirmedPlan.operation, }, "canvas-pending-operation:failed", ); writeAssistantTextChunk( writer, failure.errorMessage, "canvas-pending-operation:error", messageId, ); } }, }); return createSseResponse(pendingCanvasOperationStream); } const canvasViewOperationPlan = resolveCanvasViewOperationPlan(); if (canvasViewOperationPlan) { let canvasViewReply = "已聚焦到当前编辑对象。"; if (canvasViewOperationPlan.operation === "fit_all") { canvasViewReply = "已缩小到画布全局视图。"; } else if (canvasViewOperationPlan.operation === "zoom_in") { canvasViewReply = "已放大画布视角。"; } else if (canvasViewOperationPlan.operation === "zoom_out") { canvasViewReply = "已缩小画布视角。"; } else if (canvasViewOperationPlan.operation === "pan_to") { canvasViewReply = "已平移画布视角。"; } const canvasViewStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: "正在调整画布视角", message: canvasViewReply, toolName: "canvasView", capability: canvasViewOperationPlan.operation, }, "canvas-view:progress", ); writeCurrentAgentTargetLockChunk(writer, "canvas-view:target-lock"); writeCanvasViewOperationChunk( writer, canvasViewOperationPlan, "canvas-view:operation", ); writeAssistantTextChunk(writer, canvasViewReply, "canvas-view:reply", messageId); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: canvasViewReply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); }, }); return createSseResponse(canvasViewStream); } const canvasLinkOperationPlan = resolveCanvasLinkOperationPlan(); if (canvasLinkOperationPlan) { const canvasLinkReply = "已为选中的画布节点建立参考连线。"; const canvasLinkStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); writeAgentProgressChunk( writer, { stage: "executing", currentStep: "execute", title: "正在连接画布节点", message: "我会保留原节点,只新增参考连线。", toolName: "canvasOperation", capability: "link_nodes", }, "canvas-link:progress-executing", ); try { const result = await applyCanvasOperationPlan({ userId: session.user.id, projectId: canvasLinkOperationPlan.projectId, plan: canvasLinkOperationPlan, }); writeCanvasOperationChunk( writer, { projectId: canvasLinkOperationPlan.projectId, operationId: canvasLinkOperationPlan.operationId, operation: canvasLinkOperationPlan.operation, createdNodeIds: result.createdNodeIds, createdNodeIdsByMediaItemId: result.createdNodeIdsByMediaItemId, updatedNodeIds: result.updatedNodeIds, createdEdgeIds: result.createdEdgeIds, canvasRevision: result.canvasRevision, anchorNodeId: canvasLinkOperationPlan.sourceNodeIds?.[0] || null, handledCanvasMediaItemIds: [], message: canvasLinkReply, }, "canvas-link:operation", ); if (canvasAgentFeatures.viewOperationEnabled) { writeCanvasViewOperationChunk( writer, { version: 1, viewOperationId: `${canvasLinkOperationPlan.operationId}:view`, operation: "focus_nodes", projectId: canvasLinkOperationPlan.projectId, nodeIds: canvasLinkOperationPlan.sourceNodeIds, padding: 140, persist: false, reason: "focus_linked_nodes", }, "canvas-link:view-operation", ); } writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: "画布连线完成", message: canvasLinkReply, toolName: "canvasOperation", capability: "link_nodes", }, "canvas-link:progress-completed", ); writeAssistantTextChunk(writer, canvasLinkReply, "canvas-link:reply", messageId); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: canvasLinkReply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); } catch (error) { const failure = writeCanvasOperationFailureChunk({ writer, label: "canvas-link:operation-failed", projectId: canvasLinkOperationPlan.projectId, operationId: canvasLinkOperationPlan.operationId, operation: canvasLinkOperationPlan.operation, error, anchorNodeId: canvasLinkOperationPlan.sourceNodeIds?.[0] || null, }); writeAgentProgressChunk( writer, { stage: "failed", currentStep: "finalize", title: "画布连线失败", message: failure.errorMessage, toolName: "canvasOperation", capability: "link_nodes", }, "canvas-link:progress-failed", ); writeAssistantTextChunk(writer, failure.errorMessage, "canvas-link:error", messageId); } }, }); return createSseResponse(canvasLinkStream); } const canvasTextOperationPlan = resolveCanvasTextOperationPlan(); if (canvasTextOperationPlan) { const canvasTextReply = "已更新选中的文本节点。"; const canvasTextStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); writeAgentProgressChunk( writer, { stage: "executing", currentStep: "execute", title: "正在更新画布文本", message: "我会只修改选中的文本节点内容。", toolName: "canvasOperation", capability: "update_text", }, "canvas-text:progress-executing", ); try { const result = await applyCanvasOperationPlan({ userId: session.user.id, projectId: canvasTextOperationPlan.projectId, plan: canvasTextOperationPlan, }); writeCanvasOperationChunk( writer, { projectId: canvasTextOperationPlan.projectId, operationId: canvasTextOperationPlan.operationId, operation: canvasTextOperationPlan.operation, createdNodeIds: result.createdNodeIds, createdNodeIdsByMediaItemId: result.createdNodeIdsByMediaItemId, updatedNodeIds: result.updatedNodeIds, createdEdgeIds: result.createdEdgeIds, canvasRevision: result.canvasRevision, anchorNodeId: canvasTextOperationPlan.targetNodeIds?.[0] || null, handledCanvasMediaItemIds: [], message: canvasTextReply, }, "canvas-text:operation", ); if (canvasAgentFeatures.viewOperationEnabled) { writeCanvasViewOperationChunk( writer, { version: 1, viewOperationId: `${canvasTextOperationPlan.operationId}:view`, operation: "focus_nodes", projectId: canvasTextOperationPlan.projectId, nodeIds: canvasTextOperationPlan.targetNodeIds, padding: 120, persist: false, reason: "focus_updated_text_nodes", }, "canvas-text:view-operation", ); } writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: "画布文本已更新", message: canvasTextReply, toolName: "canvasOperation", capability: "update_text", }, "canvas-text:progress-completed", ); writeAssistantTextChunk(writer, canvasTextReply, "canvas-text:reply", messageId); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: canvasTextReply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); } catch (error) { const failure = writeCanvasOperationFailureChunk({ writer, label: "canvas-text:operation-failed", projectId: canvasTextOperationPlan.projectId, operationId: canvasTextOperationPlan.operationId, operation: canvasTextOperationPlan.operation, error, anchorNodeId: canvasTextOperationPlan.targetNodeIds?.[0] || null, }); writeAgentProgressChunk( writer, { stage: "failed", currentStep: "finalize", title: "画布文本更新失败", message: failure.errorMessage, toolName: "canvasOperation", capability: "update_text", }, "canvas-text:progress-failed", ); writeAssistantTextChunk(writer, failure.errorMessage, "canvas-text:error", messageId); } }, }); return createSseResponse(canvasTextStream); } const canvasExplicitOperationPlan = resolveCanvasExplicitOperationPlan(); if (canvasExplicitOperationPlan) { const { operationPlan } = canvasExplicitOperationPlan; if ( operationPlan.operation === "create_agent_slot" && !canvasAgentFeatures.slotEnabled ) { const canvasSlotDisabledStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); const slotDisabledReply = "当前画布 Agent Slot 能力暂未开启。"; writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: "画布槽位未创建", message: slotDisabledReply, toolName: "canvasOperation", capability: operationPlan.operation, }, "canvas-explicit:slot-disabled", ); writeAssistantTextChunk( writer, slotDisabledReply, "canvas-explicit:slot-disabled-reply", messageId, ); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: slotDisabledReply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); }, }); return createSseResponse(canvasSlotDisabledStream); } const canvasExplicitStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); writeAgentProgressChunk( writer, { stage: "executing", currentStep: "execute", title: canvasExplicitOperationPlan.progressTitle, message: canvasExplicitOperationPlan.progressMessage, toolName: "canvasOperation", capability: operationPlan.operation, }, "canvas-explicit:progress-executing", ); try { const result = await applyCanvasOperationPlan({ userId: session.user.id, projectId: operationPlan.projectId, plan: operationPlan, }); const anchorNodeId = result.createdNodeIds[0] || operationPlan.targetNodeIds?.[0] || operationPlan.anchorNodeId || null; writeCanvasOperationChunk( writer, { projectId: operationPlan.projectId, operationId: operationPlan.operationId, operation: operationPlan.operation, createdNodeIds: result.createdNodeIds, createdNodeIdsByMediaItemId: result.createdNodeIdsByMediaItemId, updatedNodeIds: result.updatedNodeIds, createdEdgeIds: result.createdEdgeIds, canvasRevision: result.canvasRevision, anchorNodeId, handledCanvasMediaItemIds: [], message: canvasExplicitOperationPlan.reply, }, "canvas-explicit:operation", ); const focusNodeIds = Array.from( new Set([ ...result.createdNodeIds, ...(operationPlan.targetNodeIds || []), ]) ) .filter(Boolean) .slice(0, 20); if (canvasAgentFeatures.viewOperationEnabled && focusNodeIds.length > 0) { writeCanvasViewOperationChunk( writer, { version: 1, viewOperationId: `${operationPlan.operationId}:view`, operation: focusNodeIds.length > 1 ? "focus_nodes" : "focus_selection", projectId: operationPlan.projectId, nodeIds: focusNodeIds, anchorNodeId: focusNodeIds[0], padding: operationPlan.operation === "create_text_node" ? 160 : 120, persist: false, reason: "focus_explicit_canvas_operation", }, "canvas-explicit:view-operation", ); } writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: canvasExplicitOperationPlan.completedTitle, message: canvasExplicitOperationPlan.reply, toolName: "canvasOperation", capability: operationPlan.operation, }, "canvas-explicit:progress-completed", ); writeAssistantTextChunk( writer, canvasExplicitOperationPlan.reply, "canvas-explicit:reply", messageId, ); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: canvasExplicitOperationPlan.reply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); } catch (error) { const failure = writeCanvasOperationFailureChunk({ writer, label: "canvas-explicit:operation-failed", projectId: operationPlan.projectId, operationId: operationPlan.operationId, operation: operationPlan.operation, error, anchorNodeId: operationPlan.targetNodeIds?.[0] || operationPlan.anchorNodeId || null, }); writeAgentProgressChunk( writer, { stage: "failed", currentStep: "finalize", title: canvasExplicitOperationPlan.failedTitle, message: failure.errorMessage, toolName: "canvasOperation", capability: operationPlan.operation, }, "canvas-explicit:progress-failed", ); writeAssistantTextChunk( writer, failure.errorMessage, "canvas-explicit:error", messageId, ); } }, }); return createSseResponse(canvasExplicitStream); } const canvasModelPlannerOutcome = await resolveCanvasModelPlannerOutcome(); if (canvasModelPlannerOutcome && canvasModelPlannerOutcome.kind !== "none") { const canvasModelPlannerStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); if (canvasModelPlannerOutcome.kind === "confirmation_required") { if (!canApplyCanvasAgentMutation) { const shadowReply = "我已识别到这是高风险画布操作,但当前 Agent 画布写回处于灰度模式,未修改画布。"; chatApiLogger.info( "[CanvasAgentOperation] Skipped model-planned confirmation in non-authoritative mode:", { writebackMode: canvasAgentFeatures.writebackMode, riskOperation: canvasModelPlannerOutcome.riskOperation, targetNodeIds: canvasModelPlannerOutcome.targetNodeIds, } ); writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: "画布操作已进入灰度保护", message: shadowReply, toolName: "canvasOperation", capability: canvasModelPlannerOutcome.riskOperation, }, "canvas-model-planner:confirmation-shadow", ); writeAssistantTextChunk( writer, shadowReply, "canvas-model-planner:shadow-reply", messageId, ); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: shadowReply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); return; } const pendingOperation = buildPendingCanvasOperation({ projectId: contextPayload?.projectId || canvasModelPlannerOutcome.operationPlan?.projectId || "", messageId: message.id, userText: requestTextContent, riskOperation: canvasModelPlannerOutcome.riskOperation, targetNodeIds: canvasModelPlannerOutcome.targetNodeIds, reply: canvasModelPlannerOutcome.reply, operationPlan: canvasModelPlannerOutcome.operationPlan, viewPlan: canvasModelPlannerOutcome.viewPlan, }); await persistPendingCanvasOperation(pendingOperation); writeAgentProgressChunk( writer, { stage: "clarify", currentStep: "finalize", title: "画布操作需要确认", message: canvasModelPlannerOutcome.reply, toolName: "canvasOperation", capability: canvasModelPlannerOutcome.riskOperation, }, "canvas-model-planner:confirmation", ); writeCurrentAgentTargetLockChunk(writer, "canvas-model-planner:target-lock"); writeAssistantTextChunk( writer, canvasModelPlannerOutcome.reply, "canvas-model-planner:reply", messageId, ); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: canvasModelPlannerOutcome.reply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); return; } if (canvasModelPlannerOutcome.kind === "view") { if (!canvasAgentFeatures.viewOperationEnabled) { const disabledReply = "当前画布视角控制暂未开启。"; writeAssistantTextChunk( writer, disabledReply, "canvas-model-planner:view-disabled", messageId, ); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: disabledReply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); return; } writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: "正在调整画布视角", message: canvasModelPlannerOutcome.reply, toolName: "canvasView", capability: canvasModelPlannerOutcome.viewPlan.operation, }, "canvas-model-planner:view-progress", ); writeCurrentAgentTargetLockChunk(writer, "canvas-model-planner:target-lock"); writeCanvasViewOperationChunk( writer, canvasModelPlannerOutcome.viewPlan, "canvas-model-planner:view-operation", ); writeAssistantTextChunk( writer, canvasModelPlannerOutcome.reply, "canvas-model-planner:reply", messageId, ); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: canvasModelPlannerOutcome.reply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); return; } const operationPlan = canvasModelPlannerOutcome.operationPlan; if ( operationPlan.operation === "create_agent_slot" && !canvasAgentFeatures.slotEnabled ) { const slotDisabledReply = "当前画布 Agent Slot 能力暂未开启。"; writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: "画布槽位未创建", message: slotDisabledReply, toolName: "canvasOperation", capability: operationPlan.operation, }, "canvas-model-planner:slot-disabled", ); writeAssistantTextChunk( writer, slotDisabledReply, "canvas-model-planner:slot-disabled-reply", messageId, ); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: slotDisabledReply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); return; } if (!canApplyCanvasAgentMutation) { const shadowReply = "我已生成画布操作计划,但当前 Agent 画布写回处于灰度模式,未修改画布。"; if (shouldStreamCanvasAgentShadowOperations(canvasAgentFeatures)) { writeCanvasOperationShadowChunk({ writer, label: "canvas-model-planner:operation-shadow-event", projectId: operationPlan.projectId, operationId: operationPlan.operationId, operation: operationPlan.operation, anchorNodeId: operationPlan.targetNodeIds?.[0] || operationPlan.sourceNodeIds?.[0] || operationPlan.anchorNodeId || operationPlan.targetSlotId || null, mediaItemIds: (operationPlan.mediaItems || []) .map((item) => item.mediaItemId) .filter(Boolean), message: shadowReply, }); } chatApiLogger.info( "[CanvasAgentOperation] Skipped model-planned mutation in non-authoritative mode:", { writebackMode: canvasAgentFeatures.writebackMode, operationId: operationPlan.operationId, operation: operationPlan.operation, projectId: operationPlan.projectId, } ); writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: "画布操作已进入灰度保护", message: shadowReply, toolName: "canvasOperation", capability: operationPlan.operation, }, "canvas-model-planner:operation-shadow", ); writeAssistantTextChunk( writer, shadowReply, "canvas-model-planner:shadow-reply", messageId, ); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: shadowReply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); return; } writeAgentProgressChunk( writer, { stage: "executing", currentStep: "execute", title: "正在执行模型规划的画布操作", message: canvasModelPlannerOutcome.reply, toolName: "canvasOperation", capability: operationPlan.operation, }, "canvas-model-planner:operation-progress", ); writeCurrentAgentTargetLockChunk(writer, "canvas-model-planner:target-lock"); try { const result = await applyCanvasOperationPlan({ userId: session.user.id, projectId: operationPlan.projectId, plan: operationPlan, }); writeCanvasOperationChunk( writer, { projectId: operationPlan.projectId, operationId: operationPlan.operationId, operation: operationPlan.operation, createdNodeIds: result.createdNodeIds, createdNodeIdsByMediaItemId: result.createdNodeIdsByMediaItemId, updatedNodeIds: result.updatedNodeIds, createdEdgeIds: result.createdEdgeIds, canvasRevision: result.canvasRevision, anchorNodeId: operationPlan.targetNodeIds?.[0] || operationPlan.sourceNodeIds?.[0] || operationPlan.anchorNodeId || null, handledCanvasMediaItemIds: [], message: canvasModelPlannerOutcome.reply, }, "canvas-model-planner:operation", ); if ( canvasModelPlannerOutcome.viewPlan && canvasAgentFeatures.viewOperationEnabled ) { writeCanvasViewOperationChunk( writer, canvasModelPlannerOutcome.viewPlan, "canvas-model-planner:view-operation", ); } writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: "画布操作完成", message: canvasModelPlannerOutcome.reply, toolName: "canvasOperation", capability: operationPlan.operation, }, "canvas-model-planner:progress-completed", ); writeAssistantTextChunk( writer, canvasModelPlannerOutcome.reply, "canvas-model-planner:reply", messageId, ); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: canvasModelPlannerOutcome.reply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); } catch (error) { const failure = writeCanvasOperationFailureChunk({ writer, label: "canvas-model-planner:operation-failed", projectId: operationPlan.projectId, operationId: operationPlan.operationId, operation: operationPlan.operation, error, anchorNodeId: operationPlan.targetNodeIds?.[0] || operationPlan.sourceNodeIds?.[0] || operationPlan.anchorNodeId || null, }); writeAgentProgressChunk( writer, { stage: "failed", currentStep: "finalize", title: "画布操作失败", message: failure.errorMessage, toolName: "canvasOperation", capability: operationPlan.operation, }, "canvas-model-planner:progress-failed", ); writeAssistantTextChunk( writer, failure.errorMessage, "canvas-model-planner:error", messageId, ); } }, }); return createSseResponse(canvasModelPlannerStream); } if (hasCanvasControlIntentFastPath) { const canvasControlFallbackReply = "我识别到这是画布控制指令,但当前没有找到可执行的画布目标。请先选中画布对象,或换成“放大画布 / 缩小看全局 / 聚焦刚生成结果”这类明确视角指令。"; const canvasControlFallbackStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); writeAgentProgressChunk( writer, { stage: "clarify", currentStep: "finalize", title: "需要明确画布目标", message: canvasControlFallbackReply, toolName: "canvasOperation", capability: "clarify_canvas_control", }, "canvas-control-fast-path:clarify", ); writeAssistantTextChunk( writer, canvasControlFallbackReply, "canvas-control-fast-path:reply", messageId, ); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: canvasControlFallbackReply }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); }, }); return createSseResponse(canvasControlFallbackStream); } const promptOnlySourceImageUrls = requestResolvedMediaInput.currentMessageImageUrls.length > 0 ? requestResolvedMediaInput.currentMessageImageUrls.slice(0, 4) : requestTargetResolution?.targetImageUrls.slice(0, 4) || smartAutoHistoryImages.slice(0, 4); const promptOnlySourceAssetIds = requestTargetResolution?.targetAssetIds?.length ? requestTargetResolution.targetAssetIds : requestResolvedMediaInput.uploadedAssetIds; const wantsPromptOnlyFromImageContext = shouldRunAgentConversation && !smartAutoNeedsClarification && promptOnlySourceImageUrls.length > 0 && taskBoundaryDecision.taskType === "prompt_only"; if (wantsPromptOnlyFromImageContext) { let promptOnlyImageSummary = ""; try { const { summarizeImageForExecutionReport } = await import("@/lib/ai/smart-analyzer"); const summaryTimeoutMs = getBudgetedTimeout({ startedAt: requestStartedAt, preferredMs: SMART_AUTO_IMAGE_SUMMARY_TIMEOUT_MS, minimumMs: 6_000, }); if (summaryTimeoutMs) { const summaryAssetIds = Array.from( new Set([ ...extractMessageImageAssetIds(message.parts as any[]), ...((effectiveContextPayload?.selectedAssetIds || []).filter(Boolean) as string[]), ]), ); const summary = await withOperationTimeout( summarizeImageForExecutionReport({ text: requestTextContent, imageUrls: promptOnlySourceImageUrls, context: effectiveAutoContextText || undefined, preferredModelId: preferredAgentModelId, assetIds: promptOnlySourceAssetIds.length > 0 ? promptOnlySourceAssetIds : summaryAssetIds, timeoutMs: summaryTimeoutMs, }), "promptOnlyImageSummary", summaryTimeoutMs, ); if (summary?.trim()) { promptOnlyImageSummary = summary.trim(); } } } catch (promptOnlySummaryError) { chatApiLogger.warn("[Chat API] Prompt-only image summary failed:", promptOnlySummaryError); } let promptOnlyOptimizedPrompt = smartAutoOptimizedPrompt?.trim() || ""; if (!promptOnlyOptimizedPrompt) { try { const { optimizePromptWithLLM, optimizePrompt } = await import("@/lib/ai/prompt-optimizer"); const promptSeed = buildPromptOnlyOptimizationSeed({ text: requestTextContent, imageSummary: promptOnlyImageSummary, }); const optimizedResult = await optimizePromptWithLLM({ originalPrompt: promptSeed, intent: "image_gen", historyMessages: smartAutoHistoryMessagesFromDb .map((msg) => { const parts = Array.isArray(msg.parts) ? (msg.parts as any[]) : []; const content = parts .filter((part: any) => part?.type === "text") .map((part: any) => String(part?.text || "")) .join(" ") .trim(); if (!content) return null; return { role: msg.role as "user" | "assistant", content: content.slice(0, 300), }; }) .filter(Boolean) as Array<{ role: "user" | "assistant"; content: string }>, context: effectiveAutoContextText || undefined, preferredModelId: preferredAgentModelId, }).catch(() => optimizePrompt({ originalPrompt: promptSeed, intent: "image_gen", }), ); promptOnlyOptimizedPrompt = String(optimizedResult?.optimizedPrompt || "").trim(); } catch (promptOnlyOptimizeError) { chatApiLogger.warn("[Chat API] Prompt-only optimization failed:", promptOnlyOptimizeError); } } if (!promptOnlyOptimizedPrompt) { promptOnlyOptimizedPrompt = buildPromptOnlyOptimizationSeed({ text: requestTextContent, imageSummary: promptOnlyImageSummary, }); } const promptOnlyMessage = buildPromptOnlyReply({ userText: requestTextContent, imageSummary: promptOnlyImageSummary, optimizedPrompt: promptOnlyOptimizedPrompt, }); const promptOnlyStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); writeAgentProgressChunk( writer, { stage: "planning", currentStep: "analyze", title: "正在提炼可执行提示词", message: "我在结合当前目标图和上下文整理更稳定的提示词。", toolName: "promptOnly", capability: "prompt_only", }, "prompt-only:progress-planning", ); writeCurrentAgentTargetLockChunk(writer, "prompt-only:target-lock"); writeAssistantTextChunk(writer, promptOnlyMessage, "prompt-only:reply", messageId); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: promptOnlyMessage }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); try { await persistPromptOnlyTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId, text: requestTextContent, optimizedPrompt: promptOnlyOptimizedPrompt, imageSummary: promptOnlyImageSummary, sourceImageUrls: promptOnlySourceImageUrls, }); } catch (promptOnlyMemoryError) { chatApiLogger.warn("[Chat API] Failed to persist prompt-only response state:", promptOnlyMemoryError); } writeAgentProgressChunk( writer, { stage: "completed", currentStep: "finalize", title: "提示词整理完成", message: "已经给出可直接复用的优化提示词。", toolName: "promptOnly", capability: "prompt_only", }, "prompt-only:progress-completed", ); }, }); return createSseResponse(promptOnlyStream); } if (shouldRunAgentConversation && smartAutoNeedsClarification) { const canvasAgentSlotClarificationBypass = shouldBypassCanvasAgentMissingTargetClarification({ text: requestTextContent, contextPayload: effectiveContextPayload, executionMode: smartAutoExecution?.mode, }); if (canvasAgentSlotClarificationBypass.shouldBypass) { smartAutoNeedsClarification = false; smartAutoClarificationQuestion = undefined; smartAutoFriendlyMessage = "收到,我会把结果填入当前选中的画布槽位。"; chatApiLogger.info( "[Smart Auto Mode] ✅ Skip clarification for selected canvas Agent slot", { reason: canvasAgentSlotClarificationBypass.reason, executionMode: smartAutoExecution?.mode, } ); } } if (shouldRunAgentConversation && smartAutoNeedsClarification) { const clarificationBypass = resolveClarificationBypass({ text: message.parts .filter((part): part is { type: "text"; text: string } => part.type === "text") .map((part) => part.text) .join(" ") .trim(), hasCurrentImageInput: message.parts.some((part: any) => part?.type === "file" || part?.type === "image" || part?.type === "image_url" ), hasHistoryImageInput: smartAutoHistoryImages.length > 0, hasCanvasSelection: Boolean(effectiveContextPayload?.selectedNodeIds?.length), targetResolutionMode: requestTargetResolution?.resolutionMode, executionMode: smartAutoExecution?.mode, friendlyMessage: smartAutoFriendlyMessage, }); if (clarificationBypass.shouldBypass) { smartAutoNeedsClarification = false; smartAutoClarificationQuestion = undefined; smartAutoFriendlyMessage = clarificationBypass.nextFriendlyMessage; chatApiLogger.info("[Smart Auto Mode] ✅ Force direct execution (skip clarification)"); } } if (shouldRunAgentConversation) { const canvasClarificationGate = resolveCanvasAgentClarificationGate({ text: requestTextContent, composerMode, contextPayload: effectiveContextPayload, mediaInput: requestResolvedMediaInput, targetResolution: requestTargetResolution, taskBoundaryDecision, executionMode: smartAutoExecution?.mode, actionKind: resolvedConversationAction.kind, }); if (canvasClarificationGate.shouldClarify) { smartAutoNeedsClarification = true; smartAutoClarificationQuestion = canvasClarificationGate.clarificationMessage || smartAutoClarificationQuestion; smartAutoFriendlyMessage = canvasClarificationGate.clarificationMessage || smartAutoFriendlyMessage; chatApiLogger.info("[Smart Auto Mode] 🧭 Canvas clarification gate triggered:", { reason: canvasClarificationGate.reason, executionMode: smartAutoExecution?.mode, resolutionMode: requestTargetResolution?.resolutionMode, selectedCount: requestResolvedMediaInput?.selectedImageRefs.length || 0, candidateCount: requestTargetResolution?.candidateRefs.length || 0, }); } } if (shouldRunAgentConversation && smartAutoNeedsClarification) { const clarificationMessage = resolveClarificationMessage({ clarificationQuestion: smartAutoClarificationQuestion, friendlyMessage: smartAutoFriendlyMessage, }); const clarificationStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); writeAgentProgressChunk( writer, { stage: "clarify", currentStep: "lock_target", title: "等待你确认关键信息", message: smartAutoClarificationQuestion || smartAutoFriendlyMessage || "我需要先确认目标图或执行范围,避免误处理。", toolName: "clarification", capability: smartAutoExecution?.preferredCapability || undefined, }, "clarification:progress", ); writeCurrentAgentTargetLockChunk(writer, "clarification:target-lock"); writer.write({ type: "data-appendMessage", data: JSON.stringify({ id: messageId, role: "assistant", parts: [{ type: "text", text: clarificationMessage }], }), }); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: clarificationMessage }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); try { await persistClarificationTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId, text: requestTextContent, clarificationMessage, currentIntent: smartAutoDetectedIntent || null, pendingGridConfirmation: shouldAskGridConfirmation && pendingGridConfirmation ? pendingGridConfirmation : null, }); } catch (clarificationPersistError) { chatApiLogger.warn("[Chat API] Failed to persist clarification state:", clarificationPersistError); } }, }); return createSseResponse(clarificationStream); } if ( shouldRunAgentConversation && smartAutoWorkflowDecision?.workflowType === "storyboard_video" && (smartAutoWorkflowDecision?.action === "revision_requested" || smartAutoWorkflowDecision?.action === "cancel_requested") ) { const workflowReplyStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); const workflowFollowupResult = await handleStoryboardWorkflowFollowup({ action: smartAutoWorkflowDecision.action as "revision_requested" | "cancel_requested", workflowId: smartAutoWorkflowDecision.workflowId, text: requestTextContent, friendlyMessage: smartAutoFriendlyMessage, onProgress: (event) => { safeWriteStreamChunk( writer, { type: "data-videoStoryboard", data: JSON.stringify(event), }, `storyboard-revision:${event.stage}`, ); }, }); const replyText = workflowFollowupResult.replyText; writer.write({ type: "data-appendMessage", data: JSON.stringify({ id: messageId, role: "assistant", parts: [{ type: "text", text: replyText }], attachments: [], }), }); await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: replyText }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: workflowFollowupResult.videoMetadata, }, ], }); try { if ( smartAutoWorkflowDecision?.action === "revision_requested" && smartAutoWorkflowDecision.workflowId ) { await persistStoryboardRevisionTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId, text: requestTextContent, taskId: smartAutoWorkflowDecision.workflowId, status: workflowFollowupResult.nextWorkflowStatus, }); } else if (smartAutoWorkflowDecision?.action === "cancel_requested") { await persistWorkflowCancelledTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId, text: requestTextContent, workflowType: "storyboard_video", workflowId: smartAutoWorkflowDecision?.workflowId, status: workflowFollowupResult.nextWorkflowStatus, }); } } catch (workflowPersistError) { chatApiLogger.warn("[Chat API] Failed to persist storyboard workflow interruption state:", workflowPersistError); } }, }); return createSseResponse(workflowReplyStream); } if ( shouldRunAgentConversation && smartAutoWorkflowDecision?.workflowType === "storyboard_video" && smartAutoWorkflowDecision?.action === "confirmed" && smartAutoWorkflowDecision.workflowId ) { const confirmedTaskId = smartAutoWorkflowDecision.workflowId; const confirmedStoryboardStream = createUIMessageStream({ execute: async ({ writer }) => { const acknowledgeMessageId = generateUUID(); const { acknowledgeMessage, acknowledgeMetadata } = buildStoryboardConfirmedAck(confirmedTaskId); writer.write({ type: "data-appendMessage", data: JSON.stringify({ id: acknowledgeMessageId, role: "assistant", parts: [{ type: "text", text: acknowledgeMessage }], attachments: [], }), }); await saveMessages({ messages: [ { id: acknowledgeMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: acknowledgeMessage }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: acknowledgeMetadata, }, ], }); try { await persistStoryboardConfirmedTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId: acknowledgeMessageId, text: requestTextContent, taskId: confirmedTaskId, status: "confirmed", }); } catch (storyboardConfirmMemoryError) { chatApiLogger.warn("[Chat API] Failed to persist storyboard confirmation memory:", storyboardConfirmMemoryError); } try { await persistStoryboardExecutionStartedTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId: acknowledgeMessageId, text: requestTextContent, taskId: confirmedTaskId, status: "running", }); } catch (storyboardExecutionStartError) { chatApiLogger.warn("[Chat API] Failed to persist storyboard execution start state:", storyboardExecutionStartError); } const { executionResult, firstVideoUrl, finalVideoMetadata } = await executeStoryboardConfirmedWorkflow({ taskId: confirmedTaskId, onProgress: (event) => { safeWriteStreamChunk( writer, { type: "data-videoStoryboard", data: JSON.stringify(event), }, `storyboard-confirmed:${event.stage}`, ); }, }); const finalMessageId = generateUUID(); writer.write({ type: "data-appendMessage", data: JSON.stringify({ id: finalMessageId, role: "assistant", parts: [{ type: "text", text: executionResult.markdown }], attachments: [], }), }); await saveMessages({ messages: [ { id: finalMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: executionResult.markdown }], attachments: [], createdAt: new Date(), videoStorageStatus: firstVideoUrl ? "temporary" : null, temporaryVideoUrl: firstVideoUrl || null, permanentVideoUrl: null, archivedAt: null, videoMetadata: finalVideoMetadata, }, ], }); await attachVideoStoryboardTaskMessage({ taskId: executionResult.taskId, messageId: finalMessageId, }).catch((taskAttachError) => { chatApiLogger.warn("[Chat API] Failed to attach confirmed storyboard task with message:", taskAttachError); }); try { await persistStoryboardExecutionCompletedTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId: finalMessageId, text: requestTextContent, taskId: executionResult.taskId, status: executionResult.status, successCount: executionResult.successCount, failedCount: executionResult.failedCount, firstVideoUrl: firstVideoUrl || undefined, }); } catch (storyboardExecutionCompletedError) { chatApiLogger.warn("[Chat API] Failed to persist storyboard execution completion state:", storyboardExecutionCompletedError); } if (firstVideoUrl) { await consumeGeneratedVideoPoints({ userId: session.user.id, chatId: id, messageId: finalMessageId, modelRefs: executionResult.modelRefs, duration: Number((finalVideoMetadata as any)?.duration) || undefined, resolution: String((finalVideoMetadata as any)?.resolution || "").trim() || undefined, }); scheduleVideoArchive({ chatId: id, messageId: finalMessageId, text: executionResult.markdown, temporaryVideoUrl: firstVideoUrl, taskId: finalMessageId, metadata: { storyboard: { enabled: true, taskId: executionResult.taskId, successCount: executionResult.successCount, failedCount: executionResult.failedCount, }, status: executionResult.status, updatedAt: Date.now(), }, }); } }, }); return createSseResponse(confirmedStoryboardStream); } if (!agentPresetId && agentModePayload?.modeType === "detail_replacement") { const replacementReferences = normalizeAgentModeReferences(agentModePayload); const detailReplacementStrategy = normalizeAgentModeDetailReplacementStrategy(agentModePayload); if (detailReplacementStrategy !== "product_only" && !hiddenPromptText) { return NextResponse.json( { code: "bad_request:api", message: DETAIL_REPLACEMENT_PROMPT_PACK_REQUIRED_MESSAGE, }, { status: 400 }, ); } if (replacementReferences.length === 0) { return NextResponse.json( { code: "bad_request:api", message: "详情参考图替换至少需要 1 张详情参考图", }, { status: 400 }, ); } const uploadedImageParts = extractCurrentMessageImageParts(message.parts as any[]); const sourceImages = normalizeAgentModeSourceImages(agentModePayload); const inferredImageParts = resolveImagePartsByReferenceExclusion({ uploadedImageParts, referenceImageUrls: replacementReferences.map((item) => item.imageUrl), }); const productSourceImages = sourceImages.productImages.length > 0 ? sourceImages.productImages : inferredImageParts.productParts.map((part) => ({ url: part.url, label: part.name || "产品图", name: part.name || "产品图", assetId: part.assetId || null, })); const modelSourceImage = sourceImages.modelImage || (inferredImageParts.modelPart ? { url: inferredImageParts.modelPart.url, label: inferredImageParts.modelPart.name || "模特图", name: inferredImageParts.modelPart.name || "模特图", assetId: inferredImageParts.modelPart.assetId || null, } : null); const replacementReferenceUrlSet = new Set( replacementReferences.map((item) => item.imageUrl), ); const productInputImageUrls = Array.from( new Set( productSourceImages .map((item) => item.url) .filter((url) => /^https?:\/\//i.test(url) && !replacementReferenceUrlSet.has(url)), ), ).slice(0, 16); const modelInputImageUrl = modelSourceImage?.url && !replacementReferenceUrlSet.has(modelSourceImage.url) ? modelSourceImage.url : null; const directReplacementModels = await getEnabledAiModelsWithProvider().catch((error) => { chatApiLogger.warn("[Chat API] Failed to load models for direct detail replacement:", error); return undefined; }); const resolvedModelRef = await resolveDirectReplacementModelRef({ selectedChatModel, finalModelId, models: directReplacementModels, }); if (!resolvedModelRef || resolvedModelRef === "auto") { return NextResponse.json( { code: "bad_request:api", message: "当前风格化未配置可用的生图模型", }, { status: 400 }, ); } const sanitizedUserSupplementPrompt = normalizeAgentSupplementPrompt( requestTextContent, "详情参考图替换", ); const baseReplacementPrompt = detailReplacementStrategy === "product_only" ? "仅替换参考详情图中的产品主体" : buildAgentSlotPrompt({ workflowPrompt: hiddenPromptText || null, slotPrompt: "", userSupplementPrompt: sanitizedUserSupplementPrompt || null, includeSlotTitle: false, modeType: "detail_replacement", }) || hiddenPromptText || sanitizedUserSupplementPrompt || "执行详情参考图背景替换"; const resolvedAspectRatio = "auto"; const resolvedImageSize = effectiveImageParams?.imageSize || defaultImageQualityParams.imageSize; const sourceAssetIds = Array.from( new Set( [ ...productSourceImages.map((item) => item.assetId), modelSourceImage?.assetId, ...uploadedImageParts.map((part) => part.assetId), ].filter((assetId): assetId is string => Boolean(assetId)), ), ); const directDetailReplacementStream = createUIMessageStream({ execute: async ({ writer }) => { writeAssistantTextChunk( writer, "", "agent-mode:detail-replacement:start", ); if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( writer, { phase: "processing", modelName: resolvedModelRef, imageCount: replacementReferences.length, }, "agent-mode:detail-replacement:status-processing", ); } let successCount = 0; let resolvedModelDisplayName = resolvedModelRef; const replacementVisionModels = directReplacementModels || (await getEnabledAiModelsWithProvider().catch((error) => { chatApiLogger.warn("[Chat API] Failed to load configured vision models for direct detail replacement:", error); return undefined; })); const replacementScenePlanningRuntime = await resolveReplacementScenePlanningRuntime({ models: replacementVisionModels, }); const stopKeepalive = startStreamKeepalive( writer, "agent-mode:detail-replacement", 12_000, ); const emitReplacementResultMessage = async (params: { reference: (typeof replacementReferences)[number]; bodyText?: string | null; image: { originalUrl: string; thumbnailUrl: string; }; imageSizeLabel?: string | null; modelDisplayName?: string | null; sourceImageUrls: string[]; streamLabel: string; promptForPersistence?: string | null; isFallback?: boolean; }) => { const persistedImageSize = String(params.imageSizeLabel || "").trim() || resolvedImageSize; const assistantText = prependImageModelBadge( [ ``, ``, ``, `### ${params.reference.label}`, String(params.bodyText || "").trim(), buildAgentToolImagesMarkdown([ { originalUrl: params.image.originalUrl, thumbnailUrl: params.image.thumbnailUrl, imageSize: persistedImageSize, }, ]), ] .filter(Boolean) .join("\n\n"), params.modelDisplayName || null, ); const assistantMessageId = generateUUID(); try { await saveMessages({ messages: [ { id: assistantMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: assistantText }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); if (!params.isFallback) { await saveGeneratedImages( assistantText, session.user.id, id, assistantMessageId, params.promptForPersistence || baseReplacementPrompt, resolvedModelRef, { sourceAssetIds, sourceImageUrls: params.sourceImageUrls, relationType: "edited_from", sourceType: "detail_replacement", projectId: contextPayload?.projectId || null, userPrompt: requestTextContent, optimizedPrompt: params.promptForPersistence || baseReplacementPrompt, requestedImageCount: 1, aspectRatio: resolvedAspectRatio, imageSize: persistedImageSize, }, ); } } catch (persistError) { chatApiLogger.warn("[Chat API] Failed to persist direct detail replacement result; streaming response anyway:", { chatId: id, referenceId: params.reference.id, referenceLabel: params.reference.label, error: persistError, }); } writeAssistantTextChunk( writer, assistantText, params.streamLabel, assistantMessageId, ); if (contextPayload?.source === "canvas") { writeCanvasMediaChunk( writer, { messageId: assistantMessageId, imageUrls: [params.image.originalUrl], prompt: params.promptForPersistence || baseReplacementPrompt, sourceImageUrls: params.sourceImageUrls, }, `${params.streamLabel}:canvas-media`, ); } }; try { await runBoundedParallel( Array.from(replacementReferences.entries()), AGENT_REPLACEMENT_PARALLEL_WORKER_COUNT, async ([referenceIndex, reference]) => { const itemStartedAt = Date.now(); if (reference.passthrough === true) { const referenceSourceImageUrls = Array.from( new Set([ ...productInputImageUrls, ...(modelInputImageUrl ? [modelInputImageUrl] : []), reference.imageUrl, ]), ); await emitReplacementResultMessage({ reference, bodyText: "已按人工标记沿用参考图,直接应用当前切片,不参与继续生成。", image: { originalUrl: reference.imageUrl, thumbnailUrl: reference.imageUrl, }, modelDisplayName: null, sourceImageUrls: referenceSourceImageUrls, streamLabel: "agent-mode:image:detail-replacement:item-passthrough", }); successCount += 1; chatApiLogger.info("[Chat API] Direct detail replacement item passthrough completed:", { referenceIndex: referenceIndex + 1, totalReferences: replacementReferences.length, referenceLabel: reference.label, elapsedMs: Date.now() - itemStartedAt, }); return; } const fallbackSourceImageUrls = Array.from( new Set([ ...productInputImageUrls, ...(modelInputImageUrl ? [modelInputImageUrl] : []), ...(reference.rerunReferenceImages || []) .map((image) => String(image.url || "").trim()) .filter((url) => /^https?:\/\//i.test(url)) .slice(0, 6), reference.imageUrl, ]), ); try { const planningStartedAt = Date.now(); const replacementExecutionPlan = await analyzeReplacementExecutionPlan({ modeType: "detail_replacement", replacementStrategy: detailReplacementStrategy, referenceImageUrl: reference.imageUrl, referenceLabel: reference.label, productImageUrls: productInputImageUrls, modelImageUrl: modelInputImageUrl, workflowPrompt: detailReplacementStrategy === "product_only" ? null : baseReplacementPrompt || hiddenPromptText || null, planningRuntime: replacementScenePlanningRuntime, models: replacementVisionModels, }); const planningElapsedMs = Date.now() - planningStartedAt; chatApiLogger.info("[Chat API] Direct detail replacement reference decision:", { referenceIndex: referenceIndex + 1, totalReferences: replacementReferences.length, referenceLabel: reference.label, planningElapsedMs, shouldReplace: replacementExecutionPlan.shouldReplace, reason: replacementExecutionPlan.reason, detectionSource: replacementExecutionPlan.detectionSource, scenePlanSource: replacementExecutionPlan.scenePlan.analysisSource, pageKind: replacementExecutionPlan.scenePlan.pageKind, generationRoute: replacementExecutionPlan.scenePlan.generationRoute, promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, packPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, textPolicy: replacementExecutionPlan.scenePlan.textPolicy, peoplePolicy: replacementExecutionPlan.scenePlan.peoplePolicy, whiteLayout: replacementExecutionPlan.scenePlan.whiteBackgroundLikely, copyCoupled: replacementExecutionPlan.scenePlan.copyImageCoupled, useModelImage: replacementExecutionPlan.scenePlan.useModelImage, sourceProductImages: productInputImageUrls.length, sourceModelImage: Boolean(modelInputImageUrl), }); const rerunReferenceImageUrls = (reference.rerunReferenceImages || []) .map((image) => String(image.url || "").trim()) .filter((url) => /^https?:\/\//i.test(url)) .slice(0, 6); const referenceSourceImageUrls = Array.from( new Set([ ...productInputImageUrls, ...(modelInputImageUrl ? [modelInputImageUrl] : []), ...rerunReferenceImageUrls, reference.imageUrl, ]), ); if (!replacementExecutionPlan.shouldReplace) { chatApiLogger.warn("[Chat API] Direct detail replacement plan returned shouldReplace=false; forcing generation:", { referenceIndex: referenceIndex + 1, totalReferences: replacementReferences.length, referenceLabel: reference.label, reason: replacementExecutionPlan.reason, }); } const referenceGeometry = await resolveReplacementReferenceGeometry(reference.imageUrl); const generationAspectRatio = referenceGeometry?.aspectRatio || resolvedAspectRatio; const selectedProductIndexes = replacementExecutionPlan.scenePlan .selectedProductImageIndexes.map((index) => index - 1) .filter((index) => index >= 0 && index < productInputImageUrls.length); const selectedProductContextImageUrls = selectedProductIndexes.length > 0 ? selectedProductIndexes.map((index) => productInputImageUrls[index]!) : productInputImageUrls.slice(0, Math.min(productInputImageUrls.length, 2)); const shouldIncludeModelImage = detailReplacementStrategy !== "product_only" && Boolean(modelInputImageUrl) && (replacementScenePlanShouldIncludeModelImage( replacementExecutionPlan.scenePlan, ) || selectedProductContextImageUrls.length === 0); const generationInputs = selectDetailReplacementGenerationInputs({ productImageUrls: selectedProductContextImageUrls, modelImageUrl: modelInputImageUrl, includeModelImage: shouldIncludeModelImage, rerunReferenceImageUrls, referenceImageUrl: reference.imageUrl, }); const sourceImageUrls = generationInputs.imageUrls; chatApiLogger.info("[Chat API] Direct detail replacement image generation started:", { referenceIndex: referenceIndex + 1, totalReferences: replacementReferences.length, referenceLabel: reference.label, pageKind: replacementExecutionPlan.scenePlan.pageKind, generationRoute: replacementExecutionPlan.scenePlan.generationRoute, packPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, selectedProductImages: generationInputs.productImageUrls.length, sourceProductImages: productInputImageUrls.length, includeModelImage: Boolean(generationInputs.modelImageUrl), rerunReferenceImages: generationInputs.rerunImageUrls.length, sourceImageCount: sourceImageUrls.length, whiteLayout: replacementExecutionPlan.scenePlan.whiteBackgroundLikely, copyCoupled: replacementExecutionPlan.scenePlan.copyImageCoupled, }); const replacementPrompt = buildAgentReplacementPrompt({ basePrompt: baseReplacementPrompt, modeType: "detail_replacement", replacementStrategy: detailReplacementStrategy, marketplaceProductDetailReference: isMarketplaceProductDetailReferenceId(reference.id) || isMarketplaceProductDetailReferenceId(reference.candidateId), referenceLabel: reference.label, referenceIndex, totalReferences: replacementReferences.length, hasModelImage: Boolean(generationInputs.modelImageUrl), productImageCount: generationInputs.productImageUrls.length, originalProductImageCount: productInputImageUrls.length, rerunReferenceCount: generationInputs.rerunImageUrls.length, templateReferenceCount: 0, scenePlan: replacementExecutionPlan.scenePlan, referenceGeometry, }); chatApiLogger.info("[Chat API] Direct detail replacement prompt assembled:", { referenceIndex: referenceIndex + 1, totalReferences: replacementReferences.length, referenceLabel: reference.label, pageKind: replacementExecutionPlan.scenePlan.pageKind, generationRoute: replacementExecutionPlan.scenePlan.generationRoute, promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, basePromptChars: baseReplacementPrompt.length, promptChars: replacementPrompt.length, hasPromptPackBlock: replacementPrompt.includes("【必须生效的前置场景词包】"), }); let generationStartedAt = 0; let generationElapsedMs: number | null = null; let normalizeStartedAt = 0; let normalizeElapsedMs: number | null = null; try { generationStartedAt = Date.now(); const result = await generateCanvasImage({ prompt: replacementPrompt, negativePrompt: effectiveImageParams?.negativePrompt, model: resolvedModelRef, ownerKey: session.user.id, aspectRatio: generationAspectRatio, imageSize: resolvedImageSize, imageCount: 1, referenceImages: sourceImageUrls, promptExtend: false, providerImageParams: { inputImageRoleByUrl: generationInputs.inputImageRoleByUrl, }, debugLabel: "agent-detail-replacement", debugMeta: { referenceLabel: reference.label, referenceWidth: referenceGeometry?.width, referenceHeight: referenceGeometry?.height, referenceAspectRatio: referenceGeometry?.aspectRatio, }, onQueueStatusChange: contextPayload?.source === "canvas" ? async (queueStatus) => { writeImageGenerationStatusChunk( writer, { phase: queueStatus.status === "running" ? "processing" : "queued", modelName: queueStatus.modelName || resolvedModelDisplayName || resolvedModelRef, imageCount: 1, queuePosition: queueStatus.queuePosition, sameModelRunningCount: queueStatus.sameModelRunningCount, concurrencyLimit: queueStatus.concurrencyLimit, estimatedWaitMs: queueStatus.estimatedWaitMs, }, `agent-mode:detail-replacement:queue:${reference.id}`, ); } : undefined, }); generationElapsedMs = Date.now() - generationStartedAt; resolvedModelDisplayName = result.model || resolvedModelDisplayName; const firstImage = result.images[0]; if (!firstImage) { throw new Error("未返回结果图片"); } let finalImage = firstImage; let finalImageSizeLabel: string = resolvedImageSize; try { normalizeStartedAt = Date.now(); const normalizedImage = await normalizeReplacementResultImage({ generatedImageUrl: firstImage.originalUrl, generatedThumbnailUrl: firstImage.thumbnailUrl, referenceImageUrl: reference.imageUrl, referenceGeometry, scenePlan: replacementExecutionPlan.scenePlan, preserveReferenceOutsideSubject: detailReplacementStrategy === "product_only", preserveReferenceText: true, uploadPrefix: "agent-detail-replacement", }); finalImage = { originalUrl: normalizedImage.originalUrl, thumbnailUrl: normalizedImage.thumbnailUrl || normalizedImage.originalUrl, }; finalImageSizeLabel = normalizedImage.imageSize; normalizeElapsedMs = Date.now() - normalizeStartedAt; } catch (normalizeError) { if (normalizeStartedAt > 0 && normalizeElapsedMs === null) { normalizeElapsedMs = Date.now() - normalizeStartedAt; } chatApiLogger.warn("[Chat API] Failed to normalize direct detail replacement output:", { referenceLabel: reference.label, referenceImageUrl: reference.imageUrl, resultImageUrl: firstImage.originalUrl, error: normalizeError instanceof Error ? normalizeError.message : "unknown", }); const rawResultAccessible = await isRemoteImageAccessible( firstImage.originalUrl ).catch(() => false); if (!rawResultAccessible) { throw normalizeError; } } chatApiLogger.info("[Chat API] Direct detail replacement image generation completed:", { referenceIndex: referenceIndex + 1, totalReferences: replacementReferences.length, referenceLabel: reference.label, modelDisplayName: resolvedModelDisplayName, resultImageSize: finalImageSizeLabel, sourceImageCount: sourceImageUrls.length, generationAspectRatio, pageKind: replacementExecutionPlan.scenePlan.pageKind, generationRoute: replacementExecutionPlan.scenePlan.generationRoute, promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, elapsedMs: Date.now() - itemStartedAt, planningElapsedMs, generationElapsedMs, normalizeElapsedMs, }); await emitReplacementResultMessage({ reference, image: finalImage, imageSizeLabel: finalImageSizeLabel, modelDisplayName: resolvedModelDisplayName, sourceImageUrls, streamLabel: "agent-mode:image:detail-replacement:item", promptForPersistence: replacementPrompt, }); successCount += 1; } catch (replacementItemError) { if (generationStartedAt > 0 && generationElapsedMs === null) { generationElapsedMs = Date.now() - generationStartedAt; } chatApiLogger.error("[Chat API] Direct detail replacement item failed:", { referenceIndex, referenceLabel: reference.label, elapsedMs: Date.now() - itemStartedAt, planningElapsedMs, generationElapsedMs, normalizeElapsedMs, error: replacementItemError, }); await emitReplacementResultMessage({ reference, bodyText: `生成失败,当前先沿用参考图:${ replacementItemError instanceof Error ? replacementItemError.message : "请稍后重试" }`, image: { originalUrl: reference.imageUrl, thumbnailUrl: reference.imageUrl, }, modelDisplayName: null, sourceImageUrls: referenceSourceImageUrls, streamLabel: "agent-mode:image:detail-replacement:item-fallback", isFallback: true, }); } } catch (replacementPlanError) { chatApiLogger.error("[Chat API] Direct detail replacement item failed before generation:", { referenceIndex, referenceLabel: reference.label, elapsedMs: Date.now() - itemStartedAt, error: replacementPlanError, }); await emitReplacementResultMessage({ reference, bodyText: `生成失败,当前先沿用参考图:${ replacementPlanError instanceof Error ? replacementPlanError.message : "请稍后重试" }`, image: { originalUrl: reference.imageUrl, thumbnailUrl: reference.imageUrl, }, modelDisplayName: null, sourceImageUrls: fallbackSourceImageUrls, streamLabel: "agent-mode:image:detail-replacement:item-fallback", isFallback: true, }); } }); } finally { stopKeepalive(); } if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( writer, { phase: successCount > 0 ? "completed" : "failed", modelName: resolvedModelDisplayName, imageCount: successCount, }, "agent-mode:detail-replacement:status-finished", ); } writeAssistantTextChunk( writer, "", "agent-mode:detail-replacement:end", ); }, }); return createSseResponse(directDetailReplacementStream); } if (agentPresetId) { const selectedAgentPreset = await getAccessibleAgentPresetById({ id: agentPresetId, userId: session.user.id, }); if (!selectedAgentPreset) { return NextResponse.json( { code: "not_found:chat", message: "风格化不存在或无权使用", }, { status: 404 }, ); } if (selectedAgentPreset.type === "text") { // 生文风格化走常规多模态对话链路: // - 模型已由前端切到风格化预设模型 // - 提示词通过 hiddenPrompt 注入 // - 图片输入沿用普通多模态附件 } else { const orderedSlots = [...(selectedAgentPreset.slots || [])].sort( (left, right) => left.slotIndex - right.slotIndex, ); const presetModeConfig = normalizeAgentPresetModeConfig(selectedAgentPreset.defaultParams); const isSceneGenerationPreset = selectedAgentPreset.type === "image" && presetModeConfig.modeType === "scene_generation"; const isDetailReplacementPreset = selectedAgentPreset.type === "image" && presetModeConfig.modeType === "detail_replacement"; const isSkuReplacementPreset = selectedAgentPreset.type === "image" && presetModeConfig.modeType === "sku_replacement"; const sceneProductSlotCount = isSceneGenerationPreset ? Math.max(1, presetModeConfig.scene.productImageSlotCount || 1) : 0; const sceneModelSlotEnabled = isSceneGenerationPreset ? presetModeConfig.scene.includeModelSlot : false; const replacementProductSlotCount = isDetailReplacementPreset ? presetModeConfig.detail.productImageSlotCount : isSkuReplacementPreset ? presetModeConfig.sku.productImageSlotCount : 0; const replacementModelSlotEnabled = isDetailReplacementPreset ? presetModeConfig.detail.includeModelSlot : isSkuReplacementPreset ? presetModeConfig.sku.includeModelSlot : false; const replacementReferences = normalizeAgentModeReferences(agentModePayload); const detailReplacementStrategy = normalizeAgentModeDetailReplacementStrategy(agentModePayload); const isMatchingReplacementPayload = (isDetailReplacementPreset && agentModePayload?.modeType === "detail_replacement") || (isSkuReplacementPreset && agentModePayload?.modeType === "sku_replacement"); if (orderedSlots.length === 0) { return NextResponse.json( { code: "bad_request:api", message: "当前风格化未配置可执行槽位", }, { status: 400 }, ); } if ((isDetailReplacementPreset || isSkuReplacementPreset) && !isMatchingReplacementPayload) { return NextResponse.json( { code: "bad_request:api", message: "当前模式缺少执行上下文,请返回第二步重新选择参考图后再试", }, { status: 400 }, ); } if ((isDetailReplacementPreset || isSkuReplacementPreset) && replacementReferences.length === 0) { return NextResponse.json( { code: "bad_request:api", message: isDetailReplacementPreset ? "详情替换模式至少需要 1 张详情参考图" : "SKU替换模式至少需要 1 张 SKU 参考图", }, { status: 400 }, ); } if ( isDetailReplacementPreset && detailReplacementStrategy !== "product_only" && !hiddenPromptText ) { return NextResponse.json( { code: "bad_request:api", message: DETAIL_REPLACEMENT_PROMPT_PACK_REQUIRED_MESSAGE, }, { status: 400 }, ); } const uploadedImageParts = extractCurrentMessageImageParts(message.parts as any[]); const requiredUploadCount = getAgentRequiredUploadCount({ presetType: selectedAgentPreset.type, defaultParams: selectedAgentPreset.defaultParams || null, fallback: 3, }); if (uploadedImageParts.length < requiredUploadCount) { return NextResponse.json( { code: "bad_request:api", message: `当前风格化需要上传 ${requiredUploadCount} 张图片`, }, { status: 400 }, ); } const presetModelId = String(selectedAgentPreset.defaultModelId || "").trim(); if (!presetModelId) { return NextResponse.json( { code: "bad_request:api", message: "当前风格化未配置默认模型", }, { status: 400 }, ); } const sanitizedUserSupplementPrompt = normalizeAgentSupplementPrompt( requestTextContent, selectedAgentPreset.name, ); const presetSnapshot = await createAgentPresetExecutionSnapshot({ presetId: selectedAgentPreset.id, userId: session.user.id, projectId: contextPayload?.projectId || null, chatId: id, messageId: message.id, presetName: selectedAgentPreset.name, presetType: selectedAgentPreset.type, visibility: selectedAgentPreset.visibility, categoryId: selectedAgentPreset.category?.id || selectedAgentPreset.categoryId || null, categoryName: selectedAgentPreset.category?.name || null, promptTemplate: selectedAgentPreset.promptTemplate, defaultModelId: selectedAgentPreset.defaultModelId || null, resolvedModelId: presetModelId, defaultParams: selectedAgentPreset.defaultParams || null, slotCount: orderedSlots.length, requiredUploadCount, slots: orderedSlots.map((slot) => ({ slotIndex: slot.slotIndex, title: slot.title || null, promptTemplate: slot.promptTemplate, defaultParams: slot.defaultParams || null, requireUpload: slot.requireUpload !== false, uploadLabel: slot.uploadLabel || null, referenceAssetIds: slot.referenceAssetIds || [], referenceImageUrls: (slot.referenceAssets || []) .map((asset) => String(asset.originalUrl || "").trim()) .filter(Boolean), })), referenceAssetIds: orderedSlots.flatMap((slot) => slot.referenceAssetIds || []), referenceImageUrls: orderedSlots.flatMap((slot) => (slot.referenceAssets || []) .map((asset) => String(asset.originalUrl || "").trim()) .filter(Boolean), ), userInputText: sanitizedUserSupplementPrompt || null, requestPayload: { selectedChatModel, agentPresetId, requiredUploadCount, uploadedImageUrls: uploadedImageParts.map((part) => part.url), agentModePayload: isDetailReplacementPreset || isSkuReplacementPreset ? { modeType: agentModePayload?.modeType || null, replacementStrategy: agentModePayload?.replacementStrategy || null, query: agentModePayload?.query || null, queryType: agentModePayload?.queryType || null, selectedCandidateId: agentModePayload?.selectedCandidateId || null, referenceCount: replacementReferences.length, } : null, }, }); try { await db .update(agentPresetTable) .set({ usageCount: sql`${agentPresetTable.usageCount} + 1`, lastUsedAt: new Date(), updatedAt: new Date(), }) .where(eq(agentPresetTable.id, selectedAgentPreset.id)); } catch (agentUsageError) { chatApiLogger.warn("[Chat API] Failed to update agent preset usage stats:", agentUsageError); } const agentSequenceStream = createUIMessageStream({ execute: async ({ writer }) => { if (selectedAgentPreset.type === "image") { writeAssistantTextChunk( writer, "", "agent-preset:image:start", ); if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( writer, { phase: "processing", modelName: selectedAgentPreset.defaultModel?.displayName || selectedAgentPreset.name, imageCount: isSceneGenerationPreset ? Math.max(1, effectiveImageParams?.imageCount || 1) : isDetailReplacementPreset || isSkuReplacementPreset ? replacementReferences.length : orderedSlots.length, }, "agent-preset:image:status-processing", ); } } let successCount = 0; const sharedUploadedImageUrls = uploadedImageParts .map((part) => part.url) .filter((url) => /^https?:\/\//i.test(url)); const sharedUploadedAssetIds = uploadedImageParts .map((part) => part.assetId) .filter((assetId): assetId is string => Boolean(assetId)); if ( selectedAgentPreset.type === "image" && (isDetailReplacementPreset || isSkuReplacementPreset) ) { const replacementSlot = orderedSlots[0]; const replacementTemplateReferenceImageUrls = Array.from( new Set( (replacementSlot?.referenceAssets || []) .map((asset) => String(asset.originalUrl || "").trim()) .filter(Boolean), ), ); const modelInputImageUrl = replacementModelSlotEnabled ? sharedUploadedImageUrls[Math.max(0, replacementProductSlotCount)] || null : null; const productInputImageUrls = isDetailReplacementPreset ? sharedUploadedImageUrls.filter((url) => url !== modelInputImageUrl) : sharedUploadedImageUrls.slice( 0, Math.max(1, replacementProductSlotCount) ); const sourceAssetIds = Array.from( new Set( [ ...sharedUploadedAssetIds, ...(replacementSlot?.referenceAssetIds || []), ].filter(Boolean), ), ); const baseReplacementPrompt = buildAgentSlotPrompt({ workflowPrompt: isDetailReplacementPreset && detailReplacementStrategy === "product_only" ? null : hiddenPromptText || null, slotPrompt: isDetailReplacementPreset && detailReplacementStrategy === "product_only" ? "" : replacementSlot?.promptTemplate || selectedAgentPreset.promptTemplate, slotTitle: selectedAgentPreset.name, userSupplementPrompt: isDetailReplacementPreset && detailReplacementStrategy === "product_only" ? null : sanitizedUserSupplementPrompt || null, includeSlotTitle: false, modeType: isDetailReplacementPreset ? "detail_replacement" : isSkuReplacementPreset ? "sku_replacement" : "generic", }); if (!isDetailReplacementPreset && productInputImageUrls.length === 0) { throw new Error("替换模式至少需要 1 张输入商品图"); } const mergedImageParams = { ...(selectedAgentPreset.defaultParams || {}), ...(replacementSlot?.defaultParams || {}), } as Record; const replacementVisionModels = await getEnabledAiModelsWithProvider().catch( (error) => { chatApiLogger.warn("[Chat API] Failed to load configured vision models for replacement analysis:", error); return undefined; }, ); const resolvedModelRef = isDetailReplacementPreset ? await resolveDirectReplacementModelRef({ selectedChatModel, finalModelId: presetModelId, models: replacementVisionModels, }) : String(selectedChatModel || "").trim() && String(selectedChatModel || "").trim() !== "auto" ? String(selectedChatModel || "").trim() : presetModelId; const configuredReplacementAspectRatio = typeof mergedImageParams.aspectRatio === "string" ? mergedImageParams.aspectRatio : effectiveImageParams?.aspectRatio || defaultImageQualityParams.aspectRatio; const resolvedAspectRatio = "auto"; const resolvedImageSize = typeof mergedImageParams.imageSize === "string" ? mergedImageParams.imageSize : effectiveImageParams?.imageSize || defaultImageQualityParams.imageSize; let resolvedModelDisplayName = selectedAgentPreset.defaultModel?.displayName || selectedAgentPreset.name; if ( configuredReplacementAspectRatio && configuredReplacementAspectRatio !== "auto" ) { chatApiLogger.info("[Chat API] Replacement mode forced aspect ratio to auto to preserve reference canvas:", { configuredAspectRatio: configuredReplacementAspectRatio, }); } const replacementScenePlanningRuntime = await resolveReplacementScenePlanningRuntime({ models: replacementVisionModels, }); const replacementScenePlanningTimeoutMs = replacementScenePlanningRuntime?.kind === "local_moonshot" ? 90_000 : 45_000; chatApiLogger.info("[Chat API] Replacement scene planning runtime:", { kind: replacementScenePlanningRuntime?.kind || "none", modelName: replacementScenePlanningRuntime?.modelName || null, timeoutMs: replacementScenePlanningTimeoutMs, }); const stopKeepalive = startStreamKeepalive( writer, "agent-preset:replacement", 12_000, ); const emitReplacementResultMessage = async (params: { reference: (typeof replacementReferences)[number]; bodyText?: string | null; image: { originalUrl: string; thumbnailUrl?: string | null; }; imageSizeLabel?: string | null; modelDisplayName?: string | null; sourceImageUrls: string[]; sourceType: "detail_replacement" | "sku_replacement"; streamLabel: string; }) => { const persistedImageSize = String(params.imageSizeLabel || "").trim() || resolvedImageSize; const assistantText = prependImageModelBadge( [ ``, `### ${params.reference.label}`, String(params.bodyText || "").trim(), buildAgentToolImagesMarkdown([ { originalUrl: params.image.originalUrl, thumbnailUrl: params.image.thumbnailUrl, imageSize: persistedImageSize, }, ]), ] .filter(Boolean) .join("\n\n"), params.modelDisplayName || null, ); const assistantMessageId = generateUUID(); await saveMessages({ messages: [ { id: assistantMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: assistantText }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); await saveGeneratedImages( assistantText, session.user.id, id, assistantMessageId, baseReplacementPrompt || selectedAgentPreset.name, resolvedModelRef, { sourceAssetIds, sourceImageUrls: params.sourceImageUrls, relationType: "edited_from", sourceType: params.sourceType, projectId: contextPayload?.projectId || null, userPrompt: requestTextContent, optimizedPrompt: baseReplacementPrompt || undefined, requestedImageCount: 1, aspectRatio: resolvedAspectRatio, imageSize: persistedImageSize, }, ); writeAssistantTextChunk( writer, assistantText, params.streamLabel, assistantMessageId, ); if (contextPayload?.source === "canvas") { writeCanvasMediaChunk( writer, { messageId: assistantMessageId, imageUrls: [params.image.originalUrl], prompt: baseReplacementPrompt || selectedAgentPreset.name, sourceImageUrls: params.sourceImageUrls, agentPresetId: selectedAgentPreset.id, agentPresetName: selectedAgentPreset.name, agentCategoryId: selectedAgentPreset.category?.id || selectedAgentPreset.categoryId || null, agentCategoryName: selectedAgentPreset.category?.name || null, agentExecutionSnapshotId: presetSnapshot.id, }, `${params.streamLabel}:canvas-media`, ); } }; try { await runBoundedParallel( Array.from(replacementReferences.entries()), AGENT_REPLACEMENT_PARALLEL_WORKER_COUNT, async ([referenceIndex, reference]) => { if (reference.passthrough === true) { const replacementSourceType = isDetailReplacementPreset ? "detail_replacement" : "sku_replacement"; const referenceSourceImageUrls = Array.from( new Set([ ...sharedUploadedImageUrls, ...replacementTemplateReferenceImageUrls, reference.imageUrl, ]), ); await emitReplacementResultMessage({ reference, bodyText: "已按人工标记沿用参考图,直接应用当前切片,不参与继续生成。", image: { originalUrl: reference.imageUrl, thumbnailUrl: reference.imageUrl, }, modelDisplayName: null, sourceImageUrls: referenceSourceImageUrls, sourceType: replacementSourceType, streamLabel: isDetailReplacementPreset ? "agent-preset:image:detail-replacement:item-passthrough" : "agent-preset:image:sku-replacement:item-passthrough", }); successCount += 1; return; } const pairedProductIndex = isSkuReplacementPreset && productInputImageUrls.length > 0 ? Math.min( typeof reference.assignedProductIndex === "number" ? reference.assignedProductIndex : referenceIndex, productInputImageUrls.length - 1 ) : undefined; const orderedProductContextImageUrls = pairedProductIndex !== undefined ? [ productInputImageUrls[pairedProductIndex]!, ...productInputImageUrls.filter( (_, productIndex) => productIndex !== pairedProductIndex ), ] : productInputImageUrls; const replacementExecutionPlan = await analyzeReplacementExecutionPlan({ modeType: isDetailReplacementPreset ? "detail_replacement" : "sku_replacement", replacementStrategy: isDetailReplacementPreset ? detailReplacementStrategy : undefined, referenceImageUrl: reference.imageUrl, referenceLabel: reference.label, productImageUrls: orderedProductContextImageUrls, modelImageUrl: modelInputImageUrl, workflowPrompt: isDetailReplacementPreset && detailReplacementStrategy === "product_only" ? null : baseReplacementPrompt || hiddenPromptText || null, planningRuntime: replacementScenePlanningRuntime, models: replacementVisionModels, }); const replacementSourceType = isDetailReplacementPreset ? "detail_replacement" : "sku_replacement"; chatApiLogger.info("[Chat API] Replacement reference decision:", { mode: replacementSourceType, referenceIndex: referenceIndex + 1, totalReferences: replacementReferences.length, referenceLabel: reference.label, referenceImageUrl: reference.imageUrl, pairedProductIndex: typeof pairedProductIndex === "number" ? pairedProductIndex + 1 : null, shouldReplace: replacementExecutionPlan.shouldReplace, reason: replacementExecutionPlan.reason, detectionModelName: replacementExecutionPlan.detectionModelName, detectionSource: replacementExecutionPlan.detectionSource, scenePlanSource: replacementExecutionPlan.scenePlan.analysisSource, generationRoute: replacementExecutionPlan.scenePlan.generationRoute, promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, textPolicy: replacementExecutionPlan.scenePlan.textPolicy, peoplePolicy: replacementExecutionPlan.scenePlan.peoplePolicy, selectedProductImageIndexes: replacementExecutionPlan.scenePlan.selectedProductImageIndexes, useModelImage: replacementExecutionPlan.scenePlan.useModelImage, }); chatApiLogger.info("[Chat API] Replacement scene plan summary:", { mode: replacementSourceType, referenceIndex: referenceIndex + 1, referenceLabel: reference.label, analysisModelName: replacementExecutionPlan.scenePlan.analysisModelName, analysisSource: replacementExecutionPlan.scenePlan.analysisSource, selectedProductImageIndexes: replacementExecutionPlan.scenePlan.selectedProductImageIndexes, productSelectionReason: replacementExecutionPlan.scenePlan.productSelectionReason, useModelImage: replacementExecutionPlan.scenePlan.useModelImage, modelImageReason: replacementExecutionPlan.scenePlan.modelImageReason, pageKind: replacementExecutionPlan.scenePlan.pageKind, generationRoute: replacementExecutionPlan.scenePlan.generationRoute, promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, routeReason: replacementExecutionPlan.scenePlan.routeReason, }); const rerunReferenceImageUrls = (reference.rerunReferenceImages || []) .map((image) => String(image.url || "").trim()) .filter((url) => /^https?:\/\//i.test(url)) .slice(0, 6); const referenceSourceImageUrls = Array.from( new Set([ ...sharedUploadedImageUrls, ...replacementTemplateReferenceImageUrls, ...rerunReferenceImageUrls, reference.imageUrl, ]), ); if (!isDetailReplacementPreset && !replacementExecutionPlan.shouldReplace) { chatApiLogger.info("[Chat API] Replacement reference skipped and reused:", { mode: replacementSourceType, referenceIndex: referenceIndex + 1, totalReferences: replacementReferences.length, referenceLabel: reference.label, reason: replacementExecutionPlan.reason, }); await emitReplacementResultMessage({ reference, bodyText: replacementExecutionPlan.reason, image: { originalUrl: reference.imageUrl, thumbnailUrl: reference.imageUrl, }, modelDisplayName: null, sourceImageUrls: referenceSourceImageUrls, sourceType: replacementSourceType, streamLabel: isDetailReplacementPreset ? "agent-preset:image:detail-replacement:item" : "agent-preset:image:sku-replacement:item", }); successCount += 1; return; } if (isDetailReplacementPreset && !replacementExecutionPlan.shouldReplace) { chatApiLogger.warn("[Chat API] Detail replacement preset plan returned shouldReplace=false; forcing generation:", { referenceIndex: referenceIndex + 1, totalReferences: replacementReferences.length, referenceLabel: reference.label, reason: replacementExecutionPlan.reason, }); } const referenceGeometry = await resolveReplacementReferenceGeometry(reference.imageUrl); const generationAspectRatio = referenceGeometry?.aspectRatio || resolvedAspectRatio; const selectedProductIndexes = replacementExecutionPlan.scenePlan .selectedProductImageIndexes.map((index) => index - 1) .filter( (index) => index >= 0 && index < orderedProductContextImageUrls.length, ); const selectedProductContextImageUrls = selectedProductIndexes.length > 0 ? selectedProductIndexes.map( (index) => orderedProductContextImageUrls[index]!, ) : orderedProductContextImageUrls.slice( 0, Math.min(orderedProductContextImageUrls.length, 2), ); const shouldIncludeModelImage = !(isDetailReplacementPreset && detailReplacementStrategy === "product_only") && Boolean(modelInputImageUrl) && (replacementScenePlanShouldIncludeModelImage( replacementExecutionPlan.scenePlan, ) || (isDetailReplacementPreset && selectedProductContextImageUrls.length === 0)); const detailGenerationInputs = isDetailReplacementPreset ? selectDetailReplacementGenerationInputs({ productImageUrls: selectedProductContextImageUrls, modelImageUrl: modelInputImageUrl, includeModelImage: shouldIncludeModelImage, templateReferenceImageUrls: replacementTemplateReferenceImageUrls, rerunReferenceImageUrls, referenceImageUrl: reference.imageUrl, }) : null; const sourceImageUrls = detailGenerationInputs ? detailGenerationInputs.imageUrls : Array.from( new Set( [ ...selectedProductContextImageUrls, ...(shouldIncludeModelImage && modelInputImageUrl ? [modelInputImageUrl] : []), ...replacementTemplateReferenceImageUrls, ...rerunReferenceImageUrls, reference.imageUrl, ].filter(Boolean), ), ); const replacementPrompt = buildAgentReplacementPrompt({ basePrompt: baseReplacementPrompt || (isDetailReplacementPreset ? "执行详情替换" : "执行 SKU 替换"), modeType: isDetailReplacementPreset ? "detail_replacement" : "sku_replacement", replacementStrategy: isDetailReplacementPreset ? detailReplacementStrategy : undefined, marketplaceProductDetailReference: isDetailReplacementPreset && (isMarketplaceProductDetailReferenceId(reference.id) || isMarketplaceProductDetailReferenceId(reference.candidateId)), referenceLabel: reference.label, referenceIndex, totalReferences: replacementReferences.length, hasModelImage: detailGenerationInputs ? Boolean(detailGenerationInputs.modelImageUrl) : shouldIncludeModelImage, productImageCount: detailGenerationInputs ? detailGenerationInputs.productImageUrls.length : selectedProductContextImageUrls.length, originalProductImageCount: productInputImageUrls.length, rerunReferenceCount: detailGenerationInputs ? detailGenerationInputs.rerunImageUrls.length : rerunReferenceImageUrls.length, templateReferenceCount: detailGenerationInputs ? detailGenerationInputs.templateImageUrls.length : replacementTemplateReferenceImageUrls.length, pairedProductIndex, scenePlan: replacementExecutionPlan.scenePlan, referenceGeometry, }); chatApiLogger.info("[Chat API] Replacement image generation started:", { mode: replacementSourceType, referenceIndex: referenceIndex + 1, totalReferences: replacementReferences.length, referenceLabel: reference.label, pairedProductIndex: typeof pairedProductIndex === "number" ? pairedProductIndex + 1 : null, pageKind: replacementExecutionPlan.scenePlan.pageKind, generationRoute: replacementExecutionPlan.scenePlan.generationRoute, promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, basePromptChars: baseReplacementPrompt.length, promptChars: replacementPrompt.length, hasPromptPackBlock: replacementPrompt.includes("【必须生效的前置场景词包】"), sourceImageCount: sourceImageUrls.length, generationAspectRatio, imageOrder: { productImages: detailGenerationInputs !== null ? detailGenerationInputs.productImageUrls.length : selectedProductContextImageUrls.length, originalProductImages: productInputImageUrls.length, hasModelImage: detailGenerationInputs ? Boolean(detailGenerationInputs.modelImageUrl) : shouldIncludeModelImage, templateReferenceImages: detailGenerationInputs ? detailGenerationInputs.templateImageUrls.length : replacementTemplateReferenceImageUrls.length, rerunReferenceImages: detailGenerationInputs ? detailGenerationInputs.rerunImageUrls.length : rerunReferenceImageUrls.length, finalReferenceImageIndex: sourceImageUrls.length, }, }); try { const result = await generateCanvasImage({ prompt: replacementPrompt, negativePrompt: typeof mergedImageParams.negativePrompt === "string" ? mergedImageParams.negativePrompt : undefined, model: resolvedModelRef, ownerKey: session.user.id, aspectRatio: generationAspectRatio, imageSize: resolvedImageSize, imageCount: 1, referenceImages: sourceImageUrls, promptExtend: isDetailReplacementPreset ? false : typeof mergedImageParams.promptExtend === "boolean" ? mergedImageParams.promptExtend : undefined, providerImageParams: detailGenerationInputs ? { inputImageRoleByUrl: detailGenerationInputs.inputImageRoleByUrl, } : undefined, debugLabel: `agent-${replacementSourceType}`, debugMeta: { referenceLabel: reference.label, referenceWidth: referenceGeometry?.width, referenceHeight: referenceGeometry?.height, referenceAspectRatio: referenceGeometry?.aspectRatio, }, onQueueStatusChange: contextPayload?.source === "canvas" ? async (queueStatus) => { writeImageGenerationStatusChunk( writer, { phase: queueStatus.status === "running" ? "processing" : "queued", modelName: queueStatus.modelName || resolvedModelDisplayName || resolvedModelRef, imageCount: 1, queuePosition: queueStatus.queuePosition, sameModelRunningCount: queueStatus.sameModelRunningCount, concurrencyLimit: queueStatus.concurrencyLimit, estimatedWaitMs: queueStatus.estimatedWaitMs, }, `agent-preset:replacement:queue:${reference.id}`, ); } : undefined, }); resolvedModelDisplayName = result.model || resolvedModelDisplayName; const firstImage = result.images[0]; if (!firstImage) { throw new Error("未返回结果图片"); } let finalImage = firstImage; let finalImageSizeLabel: string = resolvedImageSize; try { const normalizedImage = await normalizeReplacementResultImage({ generatedImageUrl: firstImage.originalUrl, generatedThumbnailUrl: firstImage.thumbnailUrl, referenceImageUrl: reference.imageUrl, referenceGeometry, scenePlan: replacementExecutionPlan.scenePlan, preserveReferenceOutsideSubject: isDetailReplacementPreset && detailReplacementStrategy === "product_only", preserveReferenceText: isDetailReplacementPreset, uploadPrefix: isDetailReplacementPreset ? "agent-detail-replacement" : "agent-sku-replacement", }); finalImage = { originalUrl: normalizedImage.originalUrl, thumbnailUrl: normalizedImage.thumbnailUrl, }; finalImageSizeLabel = normalizedImage.imageSize; } catch (normalizeError) { chatApiLogger.warn("[Chat API] Failed to normalize replacement output to reference size, using original model output:", { referenceLabel: reference.label, referenceImageUrl: reference.imageUrl, resultImageUrl: firstImage.originalUrl, error: normalizeError instanceof Error ? normalizeError.message : "unknown", }); const rawResultAccessible = await isRemoteImageAccessible( firstImage.originalUrl ).catch(() => false); if (!rawResultAccessible) { throw normalizeError; } } chatApiLogger.info("[Chat API] Replacement image generation completed:", { mode: replacementSourceType, referenceIndex: referenceIndex + 1, totalReferences: replacementReferences.length, referenceLabel: reference.label, modelDisplayName: resolvedModelDisplayName, resultImageUrl: finalImage.originalUrl, resultImageSize: finalImageSizeLabel, }); await emitReplacementResultMessage({ reference, image: finalImage, imageSizeLabel: finalImageSizeLabel, modelDisplayName: resolvedModelDisplayName, sourceImageUrls: Array.from( new Set([ ...sharedUploadedImageUrls, ...replacementTemplateReferenceImageUrls, ...replacementReferences.flatMap((item) => (item.rerunReferenceImages || []).map((image) => image.url) ), ...replacementReferences.map((item) => item.imageUrl), ]), ), sourceType: replacementSourceType, streamLabel: isDetailReplacementPreset ? "agent-preset:image:detail-replacement:item" : "agent-preset:image:sku-replacement:item", }); successCount += 1; } catch (replacementItemError) { chatApiLogger.error("[Chat API] Agent replacement item execution failed:", { referenceIndex, referenceLabel: reference.label, error: replacementItemError, }); await emitReplacementResultMessage({ reference, bodyText: `生成失败,当前先沿用参考图:${ replacementItemError instanceof Error ? replacementItemError.message : "请稍后重试" }`, image: { originalUrl: reference.imageUrl, thumbnailUrl: reference.imageUrl, }, modelDisplayName: null, sourceImageUrls: referenceSourceImageUrls, sourceType: replacementSourceType, streamLabel: isDetailReplacementPreset ? "agent-preset:image:detail-replacement:item-fallback" : "agent-preset:image:sku-replacement:item-fallback", }); successCount += 1; } }); } finally { stopKeepalive(); } } else if (selectedAgentPreset.type === "image" && isSceneGenerationPreset) { const sceneSlot = orderedSlots[0]; const sceneAssistantMessageId = generateUUID(); const sceneReferenceImageUrls = Array.from( new Set( (sceneSlot?.referenceAssets || []) .map((asset) => String(asset.originalUrl || "").trim()) .filter(Boolean), ), ); const sourceImageUrls = Array.from( new Set([...sharedUploadedImageUrls, ...sceneReferenceImageUrls].filter(Boolean)), ); const sourceAssetIds = Array.from( new Set( [...sharedUploadedAssetIds, ...(sceneSlot?.referenceAssetIds || [])].filter(Boolean), ), ); const sceneUploadedImageBreakdown = getSceneUploadedImageBreakdown({ uploadedImageParts, fallbackPrimaryCount: sceneProductSlotCount, fallbackIncludeModelImage: sceneModelSlotEnabled, }); const sceneImageRolePrompt = buildAgentSceneImageRolePrompt({ primaryProductImageCount: sceneUploadedImageBreakdown.primaryProductImageCount, bundleProductImageCount: sceneUploadedImageBreakdown.bundleProductImageCount, includeModelImage: sceneUploadedImageBreakdown.includeModelImage, }); const sceneOptimizationImageUrls = resolveScenePromptOptimizationImageUrls({ uploadedImageParts, primaryProductImageCount: sceneUploadedImageBreakdown.primaryProductImageCount, bundleProductImageCount: sceneUploadedImageBreakdown.bundleProductImageCount, includeModelImage: sceneUploadedImageBreakdown.includeModelImage, }); const shouldAppendSceneSlotPrompt = !hiddenPromptText; const rawSceneSlotPrompt = shouldAppendSceneSlotPrompt ? sceneSlot?.promptTemplate || selectedAgentPreset.promptTemplate || "" : ""; const dedupedSceneSlotPrompt = rawSceneSlotPrompt ? dedupeAgentSlotPromptAgainstWorkflow({ workflowPrompt: hiddenPromptText || null, slotPrompt: rawSceneSlotPrompt, modeType: "scene_generation", }) : ""; const optimizedSceneWorkflowResult = workflowPromptOptimizationEnabled !== false && hiddenPromptText && sceneOptimizationImageUrls.primaryImageUrl ? await optimizeAgentSceneWorkflowPrompt({ workflowPrompt: hiddenPromptText, slotPrompt: dedupedSceneSlotPrompt || null, primaryImageUrl: sceneOptimizationImageUrls.primaryImageUrl, modelImageUrl: sceneOptimizationImageUrls.modelImageUrl, }).catch((error) => { chatApiLogger.warn("[Chat API] Agent scene workflow prompt optimization failed:", error); return null; }) : null; const optimizedSceneWorkflowPrompt = hiddenPromptText ? buildOptimizedAgentSceneWorkflowPrompt({ workflowPrompt: hiddenPromptText, optimizationResult: optimizedSceneWorkflowResult, }) : ""; if (optimizedSceneWorkflowResult) { chatApiLogger.info("[Chat API] Agent scene workflow prompt optimized:", { chatId: id, agentPresetId: selectedAgentPreset.id, optimizationModelName: optimizedSceneWorkflowResult.optimizationModelName || null, optimizationSource: optimizedSceneWorkflowResult.optimizationSource || null, sceneGoal: optimizedSceneWorkflowResult.sceneGoal, environmentSummary: optimizedSceneWorkflowResult.environmentSummary, cameraLightingSummary: optimizedSceneWorkflowResult.cameraLightingSummary, characterSummary: optimizedSceneWorkflowResult.characterSummary, negativeConstraints: optimizedSceneWorkflowResult.negativeConstraints, conflicts: optimizedSceneWorkflowResult.conflicts, }); } else if (hiddenPromptText && workflowPromptOptimizationEnabled === false) { chatApiLogger.info("[Chat API] Agent scene workflow prompt optimization skipped:", { chatId: id, agentPresetId: selectedAgentPreset.id, reason: "workflow_prompt_optimization_disabled", }); } const scenePrompt = buildAgentSceneWorkflowPrompt({ workflowPrompt: optimizedSceneWorkflowPrompt || hiddenPromptText || null, slotPrompt: optimizedSceneWorkflowPrompt ? null : dedupedSceneSlotPrompt || null, imageRolePrompt: sceneImageRolePrompt, userSupplementPrompt: sanitizedUserSupplementPrompt || null, }) || buildAgentSlotPrompt({ workflowPrompt: hiddenPromptText || null, slotPrompt: sceneSlot?.promptTemplate || selectedAgentPreset.promptTemplate, userSupplementPrompt: sanitizedUserSupplementPrompt || null, includeSlotTitle: false, modeType: "scene_generation", }); const sceneGenerationStartedAt = Date.now(); chatApiLogger.info("[Chat API] Agent scene prompt guardrails:", { chatId: id, agentPresetId: selectedAgentPreset.id, agentPresetName: selectedAgentPreset.name, agentExecutionSnapshotId: presetSnapshot.id, includesWorkflowPrompt: Boolean(hiddenPromptText), includesUserSupplementPrompt: Boolean(sanitizedUserSupplementPrompt), includesModelImage: sceneUploadedImageBreakdown.includeModelImage, shouldAppendSceneSlotPrompt, includesSceneSlotPrompt: Boolean(dedupedSceneSlotPrompt), productImageCount: sceneUploadedImageBreakdown.primaryProductImageCount, bundleImageCount: sceneUploadedImageBreakdown.bundleProductImageCount, uploadedImageCount: sharedUploadedImageUrls.length, hasProductIdentityLock: sceneImageRolePrompt.includes("【产品锁定约束】"), hasModelIdentityLock: sceneImageRolePrompt.includes("【模特锁定约束】"), hasModelClothingOverrideRule: sceneImageRolePrompt.includes( "只有当工作流设定或补充要求里明确要求修改模特衣服" ), sceneSlotPromptPreview: dedupedSceneSlotPrompt .split("\n") .map((line) => line.trim()) .filter(Boolean) .slice(0, 10), imageRolePreview: sceneImageRolePrompt .split("\n") .map((line) => line.trim()) .filter(Boolean) .slice(0, 12), }); try { const mergedImageParams = { ...(selectedAgentPreset.defaultParams || {}), ...(sceneSlot?.defaultParams || {}), } as Record; const resolvedModelRef = presetModeConfig.scene.allowModelOverride && String(selectedChatModel || "").trim() && String(selectedChatModel || "").trim() !== "auto" ? String(selectedChatModel || "").trim() : presetModelId; const resolvedImageCount = Math.max(1, effectiveImageParams?.imageCount || 1); chatApiLogger.info("[Chat API] Agent scene generation started:", { chatId: id, agentPresetId: selectedAgentPreset.id, agentPresetName: selectedAgentPreset.name, agentExecutionSnapshotId: presetSnapshot.id, projectId: contextPayload?.projectId || null, selectedChatModel: String(selectedChatModel || "").trim() || "auto", resolvedModelRef, resolvedImageCount, sourceImageCount: sourceImageUrls.length, sourceAssetCount: sourceAssetIds.length, promptStrategy: hiddenPromptText ? "workflow_plus_image_lock" : rawSceneSlotPrompt ? "legacy_template_fallback" : sanitizedUserSupplementPrompt ? "workflow_plus_user_supplement" : "workflow_only", promptLength: String(scenePrompt || `生成${selectedAgentPreset.name}`).length, aspectRatio: typeof mergedImageParams.aspectRatio === "string" ? mergedImageParams.aspectRatio : effectiveImageParams?.aspectRatio || defaultImageQualityParams.aspectRatio, imageSize: typeof mergedImageParams.imageSize === "string" ? mergedImageParams.imageSize : effectiveImageParams?.imageSize || defaultImageQualityParams.imageSize, }); const result = await generateCanvasImage({ prompt: scenePrompt || `生成${selectedAgentPreset.name}`, negativePrompt: typeof mergedImageParams.negativePrompt === "string" ? mergedImageParams.negativePrompt : undefined, model: resolvedModelRef, ownerKey: session.user.id, aspectRatio: typeof mergedImageParams.aspectRatio === "string" ? mergedImageParams.aspectRatio : effectiveImageParams?.aspectRatio || defaultImageQualityParams.aspectRatio, imageSize: typeof mergedImageParams.imageSize === "string" ? mergedImageParams.imageSize : effectiveImageParams?.imageSize || defaultImageQualityParams.imageSize, imageCount: resolvedImageCount, referenceImages: sourceImageUrls, debugLabel: "agent-scene-generation", debugMeta: { chatId: id, agentPresetId: selectedAgentPreset.id, agentPresetName: selectedAgentPreset.name, agentExecutionSnapshotId: presetSnapshot.id, projectId: contextPayload?.projectId || null, }, onQueueStatusChange: contextPayload?.source === "canvas" ? async (queueStatus) => { writeImageGenerationStatusChunk( writer, { phase: queueStatus.status === "running" ? "processing" : "queued", modelName: queueStatus.modelName || selectedAgentPreset.name, imageCount: resolvedImageCount, queuePosition: queueStatus.queuePosition, sameModelRunningCount: queueStatus.sameModelRunningCount, concurrencyLimit: queueStatus.concurrencyLimit, estimatedWaitMs: queueStatus.estimatedWaitMs, }, "agent-preset:image:scene-generation:queue", ); } : undefined, }); chatApiLogger.info("[Chat API] Agent scene generation model completed:", { chatId: id, agentPresetId: selectedAgentPreset.id, agentPresetName: selectedAgentPreset.name, agentExecutionSnapshotId: presetSnapshot.id, resultImageCount: result.images.length, resultModelName: result.model, resultModelRef: result.modelRef, elapsedMs: Date.now() - sceneGenerationStartedAt, }); const imageMarkdown = buildAgentToolImagesMarkdown( result.images.map((image) => ({ originalUrl: image.originalUrl, thumbnailUrl: image.thumbnailUrl, imageSize: typeof mergedImageParams.imageSize === "string" ? mergedImageParams.imageSize : effectiveImageParams?.imageSize || defaultImageQualityParams.imageSize, })), ); const assistantText = prependImageModelBadge( [`### ${selectedAgentPreset.name}`, imageMarkdown].filter(Boolean).join("\n\n"), result.model, ); await saveMessages({ messages: [ { id: sceneAssistantMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: assistantText }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); await saveGeneratedImages( assistantText, session.user.id, id, sceneAssistantMessageId, scenePrompt || selectedAgentPreset.name, result.modelRef || resolvedModelRef, { sourceAssetIds, sourceImageUrls, relationType: "generated_from", sourceType: sourceImageUrls.length > 0 ? "reference_generate" : "generated", projectId: contextPayload?.projectId || null, userPrompt: requestTextContent, optimizedPrompt: scenePrompt || undefined, requestedImageCount: resolvedImageCount, }, ); chatApiLogger.info("[Chat API] Agent scene generation persisted:", { chatId: id, agentPresetId: selectedAgentPreset.id, agentPresetName: selectedAgentPreset.name, agentExecutionSnapshotId: presetSnapshot.id, savedImageCount: result.images.length, elapsedMs: Date.now() - sceneGenerationStartedAt, }); writeAssistantTextChunk( writer, assistantText, "agent-preset:image:scene-generation", sceneAssistantMessageId, ); if (contextPayload?.source === "canvas") { writeCanvasMediaChunk( writer, { messageId: sceneAssistantMessageId, imageUrls: result.images.map((image) => image.originalUrl), prompt: scenePrompt || selectedAgentPreset.name, sourceImageUrls, agentPresetId: selectedAgentPreset.id, agentPresetName: selectedAgentPreset.name, agentCategoryId: selectedAgentPreset.category?.id || selectedAgentPreset.categoryId || null, agentCategoryName: selectedAgentPreset.category?.name || null, agentExecutionSnapshotId: presetSnapshot.id, }, "agent-preset:image:scene-generation:canvas-media", ); } chatApiLogger.info("[Chat API] Agent scene generation response streamed:", { chatId: id, agentPresetId: selectedAgentPreset.id, agentPresetName: selectedAgentPreset.name, agentExecutionSnapshotId: presetSnapshot.id, resultImageCount: result.images.length, elapsedMs: Date.now() - sceneGenerationStartedAt, }); successCount = result.images.length; } catch (sceneError) { chatApiLogger.error("[Chat API] Agent preset scene generation failed:", sceneError); chatApiLogger.error("[Chat API] Agent scene generation failed detail:", { chatId: id, agentPresetId: selectedAgentPreset.id, agentPresetName: selectedAgentPreset.name, agentExecutionSnapshotId: presetSnapshot.id, elapsedMs: Date.now() - sceneGenerationStartedAt, error: sceneError instanceof Error ? sceneError.message : String(sceneError), }); const errorMessage = `### ${selectedAgentPreset.name}\n\n执行失败:${ sceneError instanceof Error ? sceneError.message : "请稍后重试" }`; await saveMessages({ messages: [ { id: sceneAssistantMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: errorMessage }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); writeAssistantTextChunk( writer, errorMessage, "agent-preset:error:scene-generation", sceneAssistantMessageId, ); } } else { for (const [slotOrder, slot] of orderedSlots.entries()) { const slotLabel = String(slot.title || slot.uploadLabel || "").trim() || `第${slotOrder + 1}张`; const slotReferenceImageUrls = Array.from( new Set( (slot.referenceAssets || []) .map((asset) => String(asset.originalUrl || "").trim()) .filter(Boolean), ), ); const sourceImageUrls = Array.from( new Set( [ ...sharedUploadedImageUrls, ...slotReferenceImageUrls, ].filter(Boolean), ), ); const sourceAssetIds = Array.from( new Set( [ ...sharedUploadedAssetIds, ...(slot.referenceAssetIds || []), ].filter(Boolean), ), ); const slotPrompt = buildAgentSlotPrompt({ workflowPrompt: hiddenPromptText || null, slotPrompt: slot.promptTemplate, slotTitle: slot.title || slot.uploadLabel || null, userSupplementPrompt: sanitizedUserSupplementPrompt || null, modeType: "generic", }); const assistantMessageId = generateUUID(); try { if (selectedAgentPreset.type === "image") { const mergedImageParams = { ...(selectedAgentPreset.defaultParams || {}), ...(slot.defaultParams || {}), } as Record; const result = await generateCanvasImage({ prompt: slotPrompt || `生成${slotLabel}`, negativePrompt: typeof mergedImageParams.negativePrompt === "string" ? mergedImageParams.negativePrompt : undefined, model: presetModelId, ownerKey: session.user.id, aspectRatio: typeof mergedImageParams.aspectRatio === "string" ? mergedImageParams.aspectRatio : effectiveImageParams?.aspectRatio || defaultImageQualityParams.aspectRatio, imageSize: typeof mergedImageParams.imageSize === "string" ? mergedImageParams.imageSize : effectiveImageParams?.imageSize || defaultImageQualityParams.imageSize, imageCount: 1, referenceImages: sourceImageUrls, onQueueStatusChange: contextPayload?.source === "canvas" ? async (queueStatus) => { writeImageGenerationStatusChunk( writer, { phase: queueStatus.status === "running" ? "processing" : "queued", modelName: queueStatus.modelName || slotLabel, imageCount: 1, queuePosition: queueStatus.queuePosition, sameModelRunningCount: queueStatus.sameModelRunningCount, concurrencyLimit: queueStatus.concurrencyLimit, estimatedWaitMs: queueStatus.estimatedWaitMs, }, `agent-preset:image:slot-queue:${slot.slotIndex}`, ); } : undefined, }); const imageMarkdown = buildAgentToolImagesMarkdown( result.images.map((image) => ({ originalUrl: image.originalUrl, thumbnailUrl: image.thumbnailUrl, imageSize: typeof mergedImageParams.imageSize === "string" ? mergedImageParams.imageSize : effectiveImageParams?.imageSize || defaultImageQualityParams.imageSize, })), ); const assistantText = prependImageModelBadge( [`### ${slotLabel}`, imageMarkdown].filter(Boolean).join("\n\n"), result.model, ); await saveMessages({ messages: [ { id: assistantMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: assistantText }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); await saveGeneratedImages( assistantText, session.user.id, id, assistantMessageId, slotPrompt || slotLabel, result.modelRef || presetModelId, { sourceAssetIds, sourceImageUrls, relationType: "generated_from", sourceType: sourceImageUrls.length > 0 ? "reference_generate" : "generated", projectId: contextPayload?.projectId || null, userPrompt: requestTextContent, optimizedPrompt: slotPrompt || undefined, requestedImageCount: 1, }, ); writeAssistantTextChunk( writer, assistantText, `agent-preset:image:slot-${slotOrder + 1}`, assistantMessageId, ); if (contextPayload?.source === "canvas") { writeCanvasMediaChunk( writer, { messageId: assistantMessageId, imageUrls: result.images.map((image) => image.originalUrl), prompt: slotPrompt || slotLabel, sourceImageUrls, agentPresetId: selectedAgentPreset.id, agentPresetName: selectedAgentPreset.name, agentCategoryId: selectedAgentPreset.category?.id || selectedAgentPreset.categoryId || null, agentCategoryName: selectedAgentPreset.category?.name || null, agentExecutionSnapshotId: presetSnapshot.id, }, `agent-preset:image:slot-${slotOrder + 1}:canvas-media`, ); } } else { const mergedVideoParams = { ...(selectedAgentPreset.defaultParams || {}), ...(slot.defaultParams || {}), } as Record; const requestedMode = String(mergedVideoParams.mode || "").trim() as | "text-to-video" | "first-frame" | "first-last-frame" | "reference-images"; const mode = requestedMode || (sourceImageUrls.length >= 2 ? "first-last-frame" : sourceImageUrls.length >= 1 ? "first-frame" : "text-to-video"); const directVideoRatio = mergedVideoParams.ratio === "16:9" || mergedVideoParams.ratio === "4:3" || mergedVideoParams.ratio === "1:1" || mergedVideoParams.ratio === "3:4" || mergedVideoParams.ratio === "9:16" || mergedVideoParams.ratio === "21:9" || mergedVideoParams.ratio === "adaptive" ? mergedVideoParams.ratio : undefined; const images = mode === "first-last-frame" ? sourceImageUrls.slice(0, 2) : mode === "first-frame" ? sourceImageUrls.slice(0, 1) : mode === "reference-images" ? sourceImageUrls.slice(0, 4) : []; const result = await executeDirectVideoGeneration({ prompt: slotPrompt || `生成${slotLabel}`, mode, images, resolution: mergedVideoParams.resolution, ratio: directVideoRatio, duration: mergedVideoParams.duration, preferredModelRef: presetModelId, }); const videoUrl = String(result.videoUrl || "").trim(); if (!videoUrl) { throw new Error("视频生成完成但未返回视频地址"); } const assistantText = prependImageModelBadge( [ `### ${slotLabel}`, buildAgentToolVideoMarkdown(videoUrl), ] .filter(Boolean) .join("\n\n"), result.modelDisplayName, ); await saveMessages({ messages: [ { id: assistantMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: assistantText }], attachments: [], createdAt: new Date(), videoStorageStatus: "temporary", temporaryVideoUrl: videoUrl, permanentVideoUrl: null, archivedAt: null, videoMetadata: (result.metadata || {}) as any, }, ], }); try { await persistVideoGenerationCompletedTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId: assistantMessageId, text: requestTextContent, source: "agent_preset_direct_video", providerType: "direct-video", modelRef: result.modelRef || presetModelId, mode, status: "completed", temporaryVideoUrl: videoUrl, }); } catch (memoryError) { chatApiLogger.warn("[Chat API] Failed to update working state after agent preset video generation:", memoryError); } await consumeGeneratedVideoPoints({ userId: session.user.id, chatId: id, messageId: assistantMessageId, modelRefs: [result.modelRef || presetModelId], duration: Number((result.metadata as any)?.duration) || undefined, resolution: String((result.metadata as any)?.resolution || "").trim() || undefined, }); scheduleVideoArchive({ chatId: id, messageId: assistantMessageId, text: assistantText, temporaryVideoUrl: videoUrl, taskId: assistantMessageId, metadata: result.metadata, }); writeAssistantTextChunk( writer, assistantText, `agent-preset:video:slot-${slotOrder + 1}`, assistantMessageId, ); if (contextPayload?.source === "canvas") { writeCanvasMediaChunk( writer, { messageId: assistantMessageId, videoUrls: [videoUrl], prompt: slotPrompt || slotLabel, sourceImageUrls, agentPresetId: selectedAgentPreset.id, agentPresetName: selectedAgentPreset.name, agentCategoryId: selectedAgentPreset.category?.id || selectedAgentPreset.categoryId || null, agentCategoryName: selectedAgentPreset.category?.name || null, agentExecutionSnapshotId: presetSnapshot.id, }, `agent-preset:video:slot-${slotOrder + 1}:canvas-media`, ); } } successCount += 1; } catch (slotError) { chatApiLogger.error("[Chat API] Agent preset slot execution failed:", { slotIndex: slot.slotIndex, slotLabel, error: slotError, }); const errorMessage = `### ${slotLabel}\n\n执行失败:${ slotError instanceof Error ? slotError.message : "请稍后重试" }`; await saveMessages({ messages: [ { id: assistantMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: errorMessage }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); if (selectedAgentPreset.type === "video") { try { await persistVideoGenerationFailedTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId: assistantMessageId, text: requestTextContent, source: "agent_preset_direct_video", providerType: "direct-video", modelRef: presetModelId, status: "failed", error: slotError instanceof Error ? slotError.message : String(slotError), }); } catch (videoMemoryError) { chatApiLogger.warn("[Chat API] Failed to persist agent preset video failure state:", videoMemoryError); } } writeAssistantTextChunk( writer, errorMessage, `agent-preset:error:slot-${slotOrder + 1}`, assistantMessageId, ); } } } try { await persistAgentPresetTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, currentIntent: selectedAgentPreset.type === "video" ? "video_gen" : "image_gen", lastMessageId: message.id, text: requestTextContent, agentPresetId: selectedAgentPreset.id, agentPresetName: selectedAgentPreset.name, slotCount: orderedSlots.length, successCount, }); } catch (agentPresetMemoryError) { chatApiLogger.warn("[Chat API] Failed to persist agent preset working state:", agentPresetMemoryError); } if (selectedAgentPreset.type === "image") { if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( writer, { phase: successCount > 0 ? "completed" : "failed", modelName: selectedAgentPreset.defaultModel?.displayName || selectedAgentPreset.name, imageCount: successCount, }, "agent-preset:image:status-finished", ); } writeAssistantTextChunk( writer, "", "agent-preset:image:end", ); } }, }); return createSseResponse(agentSequenceStream); } } const directAgentToolContext = shouldRunAgentConversation ? resolveDirectAgentToolContext({ execution: smartAutoExecution, plan: smartAutoPlan, requestResolvedMediaInput, requestTargetResolution, requestWorkingState, }) : null; const directAgentExecution = directAgentToolContext?.execution || null; const directAgentToolTargetImageUrls = directAgentToolContext?.targetImageUrls || []; const directAgentToolTargetImageUrl = directAgentToolContext?.targetImageUrl || null; const selectedCanvasImageNodeIds = ( requestCanvasAgentContext?.selectedNodes || [] ) .filter((node) => node.type === "image") .map((node) => node.nodeId) .filter(Boolean); const shouldUseCanvasCapabilityToolLoop = Boolean( contextPayload?.source === "canvas" && contextPayload.projectId && selectedCanvasImageNodeIds.length > 0 ); if ( shouldRunAgentConversation && !smartAutoNeedsClarification && directAgentExecution?.toolName === "analyzeImage" ) { if (directAgentToolTargetImageUrls.length <= 0) { const missingTargetMessage = getDirectAgentToolTargetMissingMessage("analyze"); const missingTargetStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); writeCurrentAgentTargetLockChunk(writer, "agent:analyze:missing-target-lock"); writeAssistantTextChunk( writer, missingTargetMessage, "agent:analyze:missing-target", messageId, ); await saveAssistantTextOnlyMessage({ chatId: id, messageId, text: missingTargetMessage, }); }, }); return createSseResponse(missingTargetStream); } const analysisAssistantMessageId = generateUUID(); const analysisSummaryAssetIds = Array.from( new Set([ ...(requestTargetResolution?.targetAssetIds || []), ...(requestResolvedMediaInput.selectedAssetIds || []), ...(requestResolvedMediaInput.uploadedAssetIds || []), ...extractMessageImageAssetIds(message.parts as any[]), ]), ); const directAnalyzeStream = createUIMessageStream({ execute: async ({ writer }) => { writeAgentProgressChunk( writer, buildDirectAgentAnalyzePlanningProgress(), "agent:analyze:progress-planning", ); writeCurrentAgentTargetLockChunk(writer, "agent:analyze:target-lock"); try { const analysisResult = await executeDirectAgentImageAnalysis({ requestTextContent, imageUrls: directAgentToolTargetImageUrls, context: effectiveAutoContextText || undefined, preferredModelId: preferredAgentModelId || finalModelId, answerModelId: finalModelId || preferredAgentModelId, assetIds: analysisSummaryAssetIds, }); const assistantText = buildDirectAgentAnalysisAssistantText({ summary: analysisResult.summary, answer: analysisResult.answer, }); await saveAssistantTextOnlyMessage({ chatId: id, messageId: analysisAssistantMessageId, text: assistantText, }); await safePersistDirectAgentAnalyzeTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, currentSubIntent: smartAutoExecution?.routeSubIntent || "analyze", targetAssetIds: requestTargetResolution?.targetAssetIds || null, targetImageUrls: directAgentToolTargetImageUrls, messageId: analysisAssistantMessageId, requestTextContent, }); writeAssistantTextChunk( writer, assistantText, "agent:analyze:success", analysisAssistantMessageId, ); writeAgentProgressChunk( writer, buildDirectAgentAnalyzeCompletedProgress(), "agent:analyze:progress-completed", ); } catch (directAnalyzeError) { chatApiLogger.error("[Chat API] Direct agent image analysis failed:", directAnalyzeError); const errorMessage = getDirectAgentToolErrorMessage("analyze", directAnalyzeError); await saveAssistantTextOnlyMessage({ chatId: id, messageId: analysisAssistantMessageId, text: errorMessage, }); writeAssistantTextChunk( writer, errorMessage, "agent:analyze:error", analysisAssistantMessageId, ); writeAgentProgressChunk( writer, buildDirectAgentAnalyzeFailedProgress(errorMessage), "agent:analyze:progress-failed", ); } }, }); return createSseResponse(directAnalyzeStream); } if ( shouldRunAgentConversation && !smartAutoNeedsClarification && directAgentExecution?.toolName === "generateImage" ) { const directImageStream = createUIMessageStream({ execute: async ({ writer }) => { await writeAgentExecutionPrefaceMessage({ writer, chatId: id, label: "agent:generate_image:preface", friendlyMessage: smartAutoFriendlyMessage, fallback: directAgentToolTargetImageUrls.length > 0 ? "收到,我会参考你选的图片来生成新画面,稍等一下。" : "好的,我按你的描述来做一版画面,稍等我处理一下。", }); writeAgentProgressChunk( writer, buildDirectAgentImageContextProgress({ imageCount: directAgentToolTargetImageUrls.length, mode: "generate", }), "agent:generate_image:progress-image-context", ); writeAgentProgressChunk( writer, buildDirectAgentImageGenerateSelectProgress(), "agent:generate_image:progress-select-tool", ); writeCurrentAgentTargetLockChunk(writer, "agent:generate_image:target-lock"); writeAssistantTextChunk( writer, "", "agent:generate_image:start", ); writeAgentProgressChunk( writer, buildDirectAgentImageGenerateExecutingProgress(), "agent:generate_image:progress-executing", ); if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( writer, { phase: "processing", providerType: requestedModelProviderType, modelName: finalModelId, imageCount: effectiveImageParams?.imageCount || defaultImageQualityParams.imageCount || 1, }, "agent:generate_image:status-processing", ); } const assistantMessageId = generateUUID(); const createdTargetSlotId = await createDirectAgentTargetSlotIfNeeded({ writer, assistantMessageId, operationLabel: "agent-generate-image", resultType: "image", }); const directImageStatusPayload = { providerType: requestedModelProviderType, modelName: finalModelId, imageCount: effectiveImageParams?.imageCount || defaultImageQualityParams.imageCount || 1, }; const stopDirectImageKeepalive = startStreamKeepalive( writer, "agent:generate_image", DEFAULT_STREAM_KEEPALIVE_INTERVAL_MS, ); const stopDirectImageStatus = contextPayload?.source === "canvas" ? setInterval(() => { writeImageGenerationStatusChunk( writer, { phase: "processing", ...directImageStatusPayload, }, "agent:generate_image:status-processing-keepalive", ); }, DEFAULT_STREAM_KEEPALIVE_INTERVAL_MS) : null; try { const directImageResult = await executeDirectAgentImageGeneration({ requestTextContent, optimizedPrompt: smartAutoOptimizedPrompt, userId: session.user.id, finalModelId, imageParams: effectiveImageParams, defaultImageQualityParams, referenceImageUrls: directAgentToolTargetImageUrls, }); const assistantText = buildDirectAgentImageGenerationAssistantText({ imageMarkdown: directImageResult.imageMarkdown, modelDisplayName: directImageResult.model, }); await saveAssistantTextOnlyMessage({ chatId: id, messageId: assistantMessageId, text: assistantText, }); await saveGeneratedImages( assistantText, session.user.id, id, assistantMessageId, directImageResult.prompt, directImageResult.modelRef || finalModelId, { sourceImageUrls: directAgentToolTargetImageUrls, relationType: "generated_from", sourceType: directAgentToolTargetImageUrls.length > 0 ? "reference_generate" : "generated", projectId: contextPayload?.projectId || null, userPrompt: requestTextContent, optimizedPrompt: directImageResult.prompt, requestedImageCount: effectiveImageParams?.imageCount || defaultImageQualityParams.imageCount || 1, aspectRatio: effectiveImageParams?.aspectRatio || defaultImageQualityParams.aspectRatio, imageSize: effectiveImageParams?.imageSize || defaultImageQualityParams.imageSize, }, ); writeAssistantTextChunk( writer, assistantText, "agent:generate_image:result", assistantMessageId, ); if (contextPayload?.source === "canvas") { const canvasWriteback = await applyDirectAgentCanvasWriteback({ writer, assistantMessageId, operationLabel: "agent-generate-image", resultType: "image", resultUrls: directImageResult.images.map((image) => image.originalUrl), prompt: directImageResult.prompt, mode: "generate", sourceImageUrls: directAgentToolTargetImageUrls, model: directImageResult.modelRef || finalModelId, targetSlotId: createdTargetSlotId, }); writeCanvasMediaChunk( writer, { messageId: assistantMessageId, imageUrls: directImageResult.images.map((image) => image.originalUrl), prompt: directImageResult.prompt, sourceImageUrls: directAgentToolTargetImageUrls, mediaItemIds: canvasWriteback.mediaItemIds, handledCanvasMediaItemIds: canvasWriteback.handledCanvasMediaItemIds, }, "agent:generate_image:canvas-media", ); writeImageGenerationStatusChunk( writer, { phase: "completed", providerType: requestedModelProviderType, modelName: directImageResult.model, imageCount: directImageResult.imageCount, }, "agent:generate_image:status-completed", ); } writeAgentProgressChunk( writer, buildDirectAgentImageGenerateCompletedProgress(), "agent:generate_image:progress-completed", ); writeAssistantTextChunk( writer, "", "agent:generate_image:end", ); } catch (directToolError) { chatApiLogger.error("[Chat API] Direct agent image generation failed:", directToolError); const errorMessage = getDirectAgentToolErrorMessage( "generate_image", directToolError, ); await saveAssistantTextOnlyMessage({ chatId: id, messageId: assistantMessageId, text: errorMessage, }); writeAssistantTextChunk( writer, errorMessage, "agent:generate_image:error", assistantMessageId, ); if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( writer, { phase: "failed", providerType: requestedModelProviderType, modelName: finalModelId, }, "agent:generate_image:status-failed", ); } writeAgentProgressChunk( writer, buildDirectAgentImageGenerateFailedProgress(errorMessage), "agent:generate_image:progress-failed", ); writeAssistantTextChunk( writer, "", "agent:generate_image:end-error", ); } finally { stopDirectImageKeepalive(); if (stopDirectImageStatus) { clearInterval(stopDirectImageStatus); } } }, }); return createSseResponse(directImageStream); } if ( shouldRunAgentConversation && !smartAutoNeedsClarification && directAgentExecution?.toolName === "generateVideo" ) { const directVideoStream = createUIMessageStream({ execute: async ({ writer }) => { await writeAgentExecutionPrefaceMessage({ writer, chatId: id, label: "agent:generate_video:preface", friendlyMessage: smartAutoFriendlyMessage, fallback: directAgentToolTargetImageUrls.length > 0 ? "收到,我会参考你选的画面来生成视频,稍等一下。" : "好的,我按你的需求来做这段视频,稍等我处理一下。", }); const assistantMessageId = generateUUID(); writeAgentProgressChunk( writer, buildDirectAgentImageContextProgress({ imageCount: directAgentToolTargetImageUrls.length, mode: "video", }), "agent:generate_video:progress-image-context", ); writeAgentProgressChunk( writer, buildDirectAgentVideoGenerateSelectProgress(), "agent:generate_video:progress-select-tool", ); writeCurrentAgentTargetLockChunk(writer, "agent:generate_video:target-lock"); const createdTargetSlotId = await createDirectAgentTargetSlotIfNeeded({ writer, assistantMessageId, operationLabel: "agent-generate-video", resultType: "video", }); try { writeAgentProgressChunk( writer, buildDirectAgentVideoGenerateExecutingProgress(), "agent:generate_video:progress-executing", ); const directVideoResult = await executeDirectAgentVideoGeneration({ requestTextContent, optimizedPrompt: smartAutoOptimizedPrompt, finalModelId, sourceImageUrls: directAgentToolTargetImageUrls, videoParams: effectiveVideoParams, smartAutoExecution, smartAutoPlan, }); const { mode, result, videoUrl } = directVideoResult; const assistantText = buildDirectAgentVideoGenerationAssistantText({ videoUrl, modelDisplayName: result.modelDisplayName, }); await saveAssistantTextOnlyMessage({ chatId: id, messageId: assistantMessageId, text: assistantText, videoStorageStatus: "temporary", temporaryVideoUrl: videoUrl, videoMetadata: (result.metadata || {}) as any, }); await safePersistDirectAgentVideoCompletedTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId: assistantMessageId, requestTextContent, modelRef: result.modelRef || finalModelId, mode, temporaryVideoUrl: videoUrl, }); await consumeGeneratedVideoPoints({ userId: session.user.id, chatId: id, messageId: assistantMessageId, modelRefs: [result.modelRef || finalModelId], duration: Number((result.metadata as any)?.duration) || Number(effectiveVideoParams?.duration) || undefined, resolution: String( (result.metadata as any)?.resolution || effectiveVideoParams?.resolution || "" ).trim() || undefined, }); if (videoUrl) { scheduleVideoArchive({ chatId: id, messageId: assistantMessageId, text: assistantText, temporaryVideoUrl: videoUrl, taskId: assistantMessageId, metadata: result.metadata, }); } writeAssistantTextChunk( writer, assistantText, "agent:generate_video:result", assistantMessageId, ); if (contextPayload?.source === "canvas") { const canvasWriteback = await applyDirectAgentCanvasWriteback({ writer, assistantMessageId, operationLabel: "agent-generate-video", resultType: "video", resultUrls: [videoUrl], prompt: directVideoResult.prompt, mode: "video", sourceImageUrls: directAgentToolTargetImageUrls, model: result.modelRef || finalModelId, targetSlotId: createdTargetSlotId, }); writeCanvasMediaChunk( writer, { messageId: assistantMessageId, videoUrls: [videoUrl], prompt: directVideoResult.prompt, sourceImageUrls: directAgentToolTargetImageUrls, mediaItemIds: canvasWriteback.mediaItemIds, handledCanvasMediaItemIds: canvasWriteback.handledCanvasMediaItemIds, }, "agent:generate_video:canvas-media", ); } writeAgentProgressChunk( writer, buildDirectAgentVideoGenerateCompletedProgress(), "agent:generate_video:progress-completed", ); } catch (directToolError) { chatApiLogger.error("[Chat API] Direct agent video generation failed:", directToolError); const errorMessage = getDirectAgentToolErrorMessage( "generate_video", directToolError, ); await saveAssistantTextOnlyMessage({ chatId: id, messageId: assistantMessageId, text: errorMessage, }); await safePersistDirectAgentVideoFailedTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId: assistantMessageId, requestTextContent, modelRef: finalModelId, error: directToolError, }); writeAssistantTextChunk( writer, errorMessage, "agent:generate_video:error", assistantMessageId, ); writeAgentProgressChunk( writer, buildDirectAgentVideoGenerateFailedProgress(errorMessage), "agent:generate_video:progress-failed", ); } }, }); return createSseResponse(directVideoStream); } if ( shouldRunAgentConversation && !smartAutoNeedsClarification && directAgentExecution && !shouldUseCanvasCapabilityToolLoop && isDirectAgentImageEditToolName(directAgentExecution.toolName) ) { const directAgentToolCapability = directAgentExecution.capability; if (!directAgentToolTargetImageUrl) { const missingTargetMessage = getDirectAgentToolTargetMissingMessage( directAgentToolCapability, ); const missingTargetStream = createUIMessageStream({ execute: async ({ writer }) => { const messageId = generateUUID(); writeCurrentAgentTargetLockChunk( writer, `agent:${directAgentToolCapability}:missing-target-lock`, ); writeAssistantTextChunk( writer, missingTargetMessage, `agent:${directAgentToolCapability}:missing-target`, messageId, ); await saveAssistantTextOnlyMessage({ chatId: id, messageId, text: missingTargetMessage, }); }, }); return createSseResponse(missingTargetStream); } const assistantMessageId = generateUUID(); const directToolLabel = getDirectAgentToolLabel(directAgentToolCapability); const directToolDisplayName = getDirectAgentToolDisplayName( directAgentToolCapability, ); const directToolStream = createUIMessageStream({ execute: async ({ writer }) => { await writeAgentExecutionPrefaceMessage({ writer, chatId: id, label: `agent:${directAgentToolCapability}:preface`, friendlyMessage: smartAutoFriendlyMessage, fallback: getDirectAgentToolPrefaceFallback(directAgentToolCapability), }); writeAgentProgressChunk( writer, buildDirectAgentImageContextProgress({ imageCount: directAgentToolTargetImageUrls.length, mode: "edit", }), `agent:${directAgentToolCapability}:progress-image-context`, ); writeAgentProgressChunk( writer, buildDirectAgentImageEditSelectProgress({ toolName: directAgentExecution.toolName, capability: directAgentToolCapability, label: directToolDisplayName, }), `agent:${directAgentToolCapability}:progress-select-tool`, ); writeCurrentAgentTargetLockChunk( writer, `agent:${directAgentToolCapability}:target-lock`, ); writeAgentProgressChunk( writer, buildDirectAgentImageEditExecutingProgress({ toolName: directAgentExecution.toolName, capability: directAgentToolCapability, label: directToolDisplayName, }), `agent:${directAgentToolCapability}:progress-executing`, ); try { const createdTargetSlotId = await createDirectAgentTargetSlotIfNeeded({ writer, assistantMessageId, operationLabel: `agent-${directAgentToolCapability}`, resultType: "image", }); const shouldBatchDirectImageEdit = shouldRunDirectAgentBatchImageEdit({ capability: directAgentToolCapability, sourceImageUrls: directAgentToolTargetImageUrls, userText: requestTextContent, }); const directImageEditParams = { capability: directAgentToolCapability, requestUrl: request.url, requestHeaders: request.headers, userId: session.user.id, chatId: id, messageId: assistantMessageId, projectId: contextPayload?.projectId || null, targetAssetIds: requestTargetResolution?.targetAssetIds || null, sourceImageUrl: directAgentToolTargetImageUrl, sourceImageUrls: directAgentToolTargetImageUrls, requestTextContent, optimizedPrompt: smartAutoOptimizedPrompt, hiddenPromptPrefix, finalModelId, imageParams: effectiveImageParams, defaultImageQualityParams, smartAutoExecution, smartAutoPlan, canvasAgentContext: requestCanvasAgentContext, targetNodeIds: (requestTargetResolution?.targetRefs || []) .map((ref) => ref.nodeId) .filter((nodeId): nodeId is string => Boolean(nodeId)), saveGeneratedImages, }; const directImageEditResults = shouldBatchDirectImageEdit ? await executeDirectAgentBatchImageEditTool(directImageEditParams) : [ { ...(await executeDirectAgentImageEditTool(directImageEditParams)), sourceImageUrl: directAgentToolTargetImageUrl, sourceIndex: 0, }, ]; const firstImageEditResult = directImageEditResults[0]; const resultUrls = directImageEditResults .map((result) => result.originalUrl) .filter((url): url is string => Boolean(url)); const resultAssetIds = directImageEditResults .map((result) => result.mediaAssetId) .filter((assetId): assetId is string => Boolean(assetId)); const resultOriginalUrl = firstImageEditResult?.originalUrl || ""; const resultThumbnailUrl = firstImageEditResult?.thumbnailUrl || null; const resultImageSize = firstImageEditResult?.imageSize || "2K"; const resultAssetId = firstImageEditResult?.mediaAssetId || null; const assetIdsByUrl = directImageEditResults.reduce< Record >((acc, result) => { if (result.originalUrl) { acc[result.originalUrl] = result.mediaAssetId || undefined; } return acc; }, {}); const assistantText = directImageEditResults.length > 1 ? buildDirectAgentBatchImageEditAssistantText({ capability: directAgentToolCapability, results: directImageEditResults, }) : buildDirectAgentImageEditAssistantText({ capability: directAgentToolCapability, originalUrl: resultOriginalUrl, thumbnailUrl: resultThumbnailUrl, imageSize: resultImageSize, }); await saveAssistantTextOnlyMessage({ chatId: id, messageId: assistantMessageId, text: assistantText, }); await safePersistDirectAgentImageEditTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, capability: directAgentToolCapability, currentSubIntent: smartAutoExecution?.routeSubIntent || directAgentToolCapability, resultAssetId, resultAssetIds: resultAssetIds.length > 0 ? resultAssetIds : null, resultOriginalUrl, resultOriginalUrls: resultUrls.length > 0 ? resultUrls : null, resultImageSize, sourceImageUrls: directAgentToolTargetImageUrls, messageId: assistantMessageId, requestTextContent, optimizedPrompt: smartAutoOptimizedPrompt || undefined, }); if (directAgentExecution.publishToCanvas && contextPayload?.source === "canvas") { chatApiLogger.info(`[Chat API] Agent publishToCanvas satisfied by assistant media auto-sync (${directAgentToolCapability})`); } writeAssistantTextChunk( writer, assistantText, `agent:${directAgentToolCapability}:success`, assistantMessageId, ); if (contextPayload?.source === "canvas") { const canvasWriteback = await applyDirectAgentCanvasWriteback({ writer, assistantMessageId, operationLabel: `agent-${directAgentToolCapability}`, resultType: "image", resultUrls, prompt: requestTextContent || directToolLabel, mode: "edit", sourceImageUrls: directAgentToolTargetImageUrls, assetIdsByUrl: resultUrls.length > 0 ? assetIdsByUrl : undefined, model: finalModelId, targetSlotId: createdTargetSlotId, }); writeCanvasMediaChunk( writer, { messageId: assistantMessageId, imageUrls: resultUrls, prompt: requestTextContent || directToolLabel, sourceImageUrls: directAgentToolTargetImageUrls, mediaItemIds: canvasWriteback.mediaItemIds, handledCanvasMediaItemIds: canvasWriteback.handledCanvasMediaItemIds, }, `agent:${directAgentToolCapability}:canvas-media`, ); } writeAgentProgressChunk( writer, buildDirectAgentImageEditCompletedProgress({ toolName: directAgentExecution.toolName, capability: directAgentToolCapability, label: directToolDisplayName, }), `agent:${directAgentToolCapability}:progress-completed`, ); } catch (directToolError) { chatApiLogger.error(`[Chat API] Direct agent tool ${directAgentToolCapability} failed:`, directToolError); if (!isDirectAgentImageEditServiceError(directToolError)) { chatApiLogger.warn(`[Chat API] Direct agent tool ${directAgentToolCapability} threw unexpected error type`); } const errorMessage = getDirectAgentToolErrorMessage( directAgentToolCapability, directToolError, ); await saveAssistantTextOnlyMessage({ chatId: id, messageId: assistantMessageId, text: errorMessage, }); writeAssistantTextChunk( writer, errorMessage, `agent:${directAgentToolCapability}:error`, assistantMessageId, ); writeAgentProgressChunk( writer, buildDirectAgentImageEditFailedProgress({ toolName: directAgentExecution.toolName, capability: directAgentToolCapability, label: directToolLabel, errorMessage, }), `agent:${directAgentToolCapability}:progress-failed`, ); } }, }); return createSseResponse(directToolStream); } let finalMergedUsage: AppUsage | undefined; // 获取启用的工具列表 const enabledTools = await getEnabledTools(); if (String(finalModelId || "").trim() === "auto") { const hasImageInputForAutoFallback = message.parts.some((part: any) => part?.type === "file" || part?.type === "image" || part?.type === "image_url" ) || smartAutoHistoryImages.length > 0; const autoFallbackIntent = smartAutoExecution?.shouldApplyVideoPipeline || composerMode === "video" ? "video_gen" : smartAutoExecution?.shouldApplyImagePipeline || composerMode === "image" || composerMode === "detail" ? hasImageInputForAutoFallback ? "image_edit" : "image_gen" : hasImageInputForAutoFallback ? "vision" : "chat"; try { const { routeToModel } = await import("@/lib/ai/model-router"); const fallbackRoute = await routeToModel({ intent: autoFallbackIntent, hasImage: hasImageInputForAutoFallback, }); finalModelId = fallbackRoute.id; chatApiLogger.warn("[Chat API] Auto model was unresolved, routed to fallback model:", { intent: autoFallbackIntent, modelId: fallbackRoute.id, displayName: fallbackRoute.displayName, reason: fallbackRoute.reason, }); } catch (autoModelRouteError) { chatApiLogger.error("[Chat API] Failed to route unresolved auto model:", autoModelRouteError); const enabledModels = await getEnabledAiModelsWithProvider(false); const nonAgentModels = enabledModels.filter(({ model }) => !isAiModelAgentAuto(model)); const candidates = nonAgentModels.length > 0 ? nonAgentModels : enabledModels; const fallbackModel = candidates.find(({ model, provider }) => { if (!model.isEnabled || model.isSubModel) return false; const providerType = String(provider?.type || ""); if (autoFallbackIntent === "video_gen") { return isVideoProviderType(providerType, model); } if (autoFallbackIntent === "image_edit" || autoFallbackIntent === "image_gen") { return isImageGenerationModelType(providerType, model); } return !isVideoProviderType(providerType, model) && !model.supportsImageGen; }) || candidates.find( ({ model, provider }) => model.isEnabled && !model.isSubModel && !isVideoProviderType(String(provider?.type || ""), model) ) || candidates.find(({ model }) => model.isEnabled && !model.isSubModel); if (!fallbackModel) { return new ChatSDKError("bad_request:api", "No available models for auto mode").toResponse(); } finalModelId = fallbackModel.model.id; chatApiLogger.warn("[Chat API] Auto model fallback used first available model:", { intent: autoFallbackIntent, modelId: fallbackModel.model.id, displayName: fallbackModel.model.displayName, providerType: fallbackModel.provider?.type, }); } } // 检查是否是 UUID 格式(数据库中的模型 ID) const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( finalModelId ); // 获取语言模型实例 let languageModel; let actualModelId: string; let resolvedModelSupportsImageGen = false; let resolvedProviderType: string | undefined; let resolvedRuntimeModelLookupId = finalModelId; if (isUUID) { // 从数据库获取动态配置的模型 let targetModelId = finalModelId; // 🆕 检查是否是视频主模型,如果是,根据 videoParams.mode 选择子模型 const aiModelData = await getAiModelById({ id: finalModelId }); const aiModelProvider = aiModelData ? await getAiProviderById({ id: aiModelData.providerId }).catch(() => null) : null; chatApiLogger.info("[Chat API] Model data:", { id: finalModelId, isSubModel: aiModelData?.isSubModel }); if (aiModelData && !aiModelData.isSubModel && aiModelProvider?.type === "volcano-video") { // 检查是否有子模型 const subModels = await getSubModelsByParentModelId(finalModelId); chatApiLogger.info("[Chat API] Found sub-models:", subModels.length); if (subModels.length > 0) { // 这是一个主模型,需要根据 videoParams 选择子模型 const videoParams = effectiveVideoParams; chatApiLogger.info("[Chat API] Video params:", videoParams); // 如果没有 videoParams 或没有 mode,使用默认模式 const mode = (videoParams?.mode || "text-to-video") as VideoModeType; chatApiLogger.info("[Chat API] Using mode:", mode); const matchedSubModel = selectBestVideoSubModelForMode(subModels as any, mode); if (matchedSubModel) { chatApiLogger.info(`[Chat API] 视频模式 ${mode} → 选择子模型: ${matchedSubModel.model.displayName} (${matchedSubModel.model.modelId})`); targetModelId = matchedSubModel.model.id; } else { chatApiLogger.warn(`[Chat API] 未找到可用子模型,继续使用主模型`); } } } const dynamicModel = await getLanguageModelById(targetModelId); if (!dynamicModel) { return new ChatSDKError( "bad_request:api", "Model not found or disabled" ).toResponse(); } languageModel = dynamicModel; resolvedRuntimeModelLookupId = targetModelId; // 对于动态模型,我们需要从数据库获取 modelId const finalModelData = await getAiModelById({ id: targetModelId }); actualModelId = finalModelData?.modelId ?? targetModelId; resolvedModelSupportsImageGen = Boolean(finalModelData?.supportsImageGen); if (finalModelData) { resolvedProviderType = ( await getAiProviderById({ id: finalModelData.providerId }).catch(() => null) )?.type; } } else { // 使用预定义的模型 languageModel = myProvider.languageModel(finalModelId); actualModelId = languageModel.modelId; resolvedProviderType = (languageModel as any)?.provider; resolvedRuntimeModelLookupId = actualModelId; } const recentDialogueSystemContext = recentDialogueMemory.contextSummary ? [ recentDialogueMemory.contextSummary, ...recentDialogueMemory.memoryHints, ].join("\n") : ""; const baseConversationSystemPrompt = systemPrompt({ selectedChatModel: finalModelId, requestHints, }); const shouldUseProductIdentityInstruction = shouldRunAgentConversation || isCanvasPageRequest || contextPayload?.source === "canvas" || Boolean(requestCanvasAgentContext); const agentIdentityInstruction = shouldUseProductIdentityInstruction ? await buildAgentRoleIdentityInstruction({ allowVideoGeneration: allowAgentVideoGeneration, hasCanvasContext: Boolean( isCanvasPageRequest || effectiveContextPayload?.source === "canvas" || effectiveContextPayload?.selectedNodeIds?.length || effectiveContextPayload?.canvasImageNodes?.length, ), }) : ""; const effectiveConversationSystemPrompt = [ agentIdentityInstruction, recentDialogueSystemContext, baseConversationSystemPrompt, shouldUseCanvasCapabilityToolLoop ? [ "当前请求来自无限画布,且用户已明确选中图片节点。", "涉及选中图片的识别、编辑、生成、3D、改名、批注或删除时,必须调用已提供的画布能力工具,不得只用文字声称完成。", "可以连续调用多个工具;下一步要处理上一步结果时,把上一步返回的 createdNodeIds 作为 nodeIds。", "只有工具返回成功后才能告诉用户已完成;若工具返回 interactive,说明已在画布打开对应交互界面并等待用户操作。", ].join("\n") : "", ] .filter(Boolean) .join("\n\n"); const activeRuntimeModelDescriptor = await resolveAgentModelSlotDescriptor( resolvedRuntimeModelLookupId, ).catch(() => null); if (shouldRunAgentConversation && activeRuntimeModelDescriptor) { const slotName = smartAutoExecution?.shouldApplyImagePipeline || smartAutoExecution?.shouldApplyVideoPipeline ? "execution" : "reply"; agentModelSlots = upsertAgentModelSlot(agentModelSlots, slotName, { ...activeRuntimeModelDescriptor, source: "runtime_selected_model", reason: slotName === "reply" ? smartAutoExecution?.shouldStayConversational ? "conversational_reply_runtime" : "default_reply_runtime" : "execution_runtime_model", locked: slotName === "reply" ? shouldKeepSelectedModel : false, }); pendingProtocolDebugEvents.push({ phase: "modelSlots", payload: { modelSlots: agentModelSlots, providerHealth: activeRuntimeModelDescriptor.providerKey ? getProviderHealthSnapshot(activeRuntimeModelDescriptor.providerKey) : null, }, }); } // ========== 图片生成配额检查 ========== const isUuidModelId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( finalModelId ); const currentModelConfig = isUuidModelId ? await getAiModelById({ id: finalModelId }).catch(() => undefined) : undefined; const currentProviderConfig = currentModelConfig?.providerId ? await getAiProviderById({ id: currentModelConfig.providerId }).catch(() => null) : null; const runtimeProviderType = String( resolvedProviderType || (languageModel as any).provider || "" ); const normalizedRuntimeProviderType = runtimeProviderType === "gemini-image" ? "gemini" : runtimeProviderType === "huanke-gemini" ? "huanke-gemini" : runtimeProviderType; const isImageGenerationModel = isImageGenerationModelType( normalizedRuntimeProviderType, currentModelConfig ? { modelId: currentModelConfig.modelId, capabilities: currentModelConfig.capabilities, supportsImageGen: currentModelConfig.supportsImageGen, } : undefined ); const modelCreditCostPerImage = getModelCreditCost(currentModelConfig, "image", { imageSize: effectiveImageGenerationParams.imageSize, }); const modelCreditCostPerVideo = getModelCreditCost(currentModelConfig, "video", { duration: Number(effectiveVideoParams?.duration) || undefined, resolution: String(effectiveVideoParams?.resolution || "").trim() || undefined, }); if (isImageGenerationModel && !hasCanvasControlIntentFastPath) { const quota = await checkUserImageQuota(session.user.id); if (!quota.allowed) { chatApiLogger.info(`[Chat API] ⛔ 图片配额已用完: ${quota.used}/${quota.limit}`); return NextResponse.json( { error: "quota_exceeded", message: `本月图片生成配额已用完(已使用 ${quota.used}/${quota.limit} 张),配额将于下月1日重置,如需更多配额请联系管理员。`, used: quota.used, limit: quota.limit, }, { status: 429 } ); } chatApiLogger.info(`[Chat API] ✅ 图片配额检查通过: ${quota.used}/${quota.limit ?? '无限制'}`); const pointsConfig = await getPointsSystemConfig(); const normalizedUnitCost = Number.isFinite(modelCreditCostPerImage) ? Math.max(0, Math.round(modelCreditCostPerImage)) : 0; if (pointsConfig.enabled && normalizedUnitCost > 0 && pointsConfig.blockOnInsufficient !== false) { const requestedImageCount = Math.max( 1, Math.round( Number( effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1 ) ) ); const expectedCost = normalizedUnitCost * requestedImageCount; const userData = await getUserById({ id: session.user.id }); if (!userData?.isHighVip) { const currentBalance = Math.max(0, Number(userData?.pointsBalance || 0)); if (currentBalance < expectedCost) { return NextResponse.json( { error: "insufficient_points", message: `积分不足,当前余额 ${currentBalance},本次预计消耗 ${expectedCost}(${normalizedUnitCost} x ${requestedImageCount} 张)。`, balance: currentBalance, expectedCost, unitCost: normalizedUnitCost, }, { status: 402 } ); } } } } const isVideoGenerationModel = isVideoProviderType( normalizedRuntimeProviderType, currentModelConfig ? { modelId: currentModelConfig.modelId, capabilities: currentModelConfig.capabilities, supportsImageGen: currentModelConfig.supportsImageGen, } : undefined ); if (isVideoGenerationModel && !hasCanvasControlIntentFastPath) { const pointsConfig = await getPointsSystemConfig(); const normalizedVideoUnitCost = Number.isFinite(modelCreditCostPerVideo) ? Math.max(0, Math.round(modelCreditCostPerVideo)) : 0; if ( pointsConfig.enabled && normalizedVideoUnitCost > 0 && pointsConfig.blockOnInsufficient !== false ) { const requestedVideoDuration = Math.max( 1, Math.round(Number(effectiveVideoParams?.duration || 0)) || 0 ); const expectedCost = normalizedVideoUnitCost; const userData = await getUserById({ id: session.user.id }); if (!userData?.isHighVip) { const currentBalance = Math.max(0, Number(userData?.pointsBalance || 0)); if (currentBalance < expectedCost) { return NextResponse.json( { error: "insufficient_points", message: requestedVideoDuration ? `积分不足,当前余额 ${currentBalance},本次预计消耗 ${expectedCost}(${requestedVideoDuration} 秒视频)。` : `积分不足,当前余额 ${currentBalance},本次预计消耗 ${expectedCost}(1 条视频)。`, balance: currentBalance, expectedCost, unitCost: normalizedVideoUnitCost, duration: requestedVideoDuration || undefined, }, { status: 402 } ); } } } } // ========== Gemini 图片生成独立处理 ========== // 支持 gemini-image 和 huanke-gemini 两种图片生成 provider const isGeminiImageProvider = ["gemini-image", "huanke-gemini"].includes((languageModel as any).provider); if (isGeminiImageProvider && typeof (languageModel as any).doGenerate === "function") { chatApiLogger.info("[Chat API] Using Gemini image generation, provider:", (languageModel as any).provider); chatApiLogger.info("[Chat API] Image params:", effectiveImageGenerationParams); // 🆕 获取备用模型配置 const currentModelConfig = await getAiModelById({ id: finalModelId }); const configuredFallbacks = [ currentModelConfig?.fallbackModelId1, currentModelConfig?.fallbackModelId2, ].filter(Boolean) as string[]; const allModels = await getEnabledAiModelsWithProvider(); const currentModelEntry = resolveModelEntryByRuntimeModelId( allModels, actualModelId, finalModelId, ); const fallbackCandidates = buildImageFallbackCandidates(allModels, { currentProviderId: currentModelConfig?.providerId, currentModelId: currentModelConfig?.id, currentModelKey: actualModelId, configuredFallbackIds: configuredFallbacks, }); chatApiLogger.info("[Chat API] Gemini fallback candidates:", fallbackCandidates.map((candidate) => `${candidate.model.displayName}(${candidate.provider.type})`)); const geminiStream = createUIMessageStream({ execute: async ({ writer }) => { // 🆕 如果是智能模式且有对话回复,先发送对话消息让用户看到 // 在消息末尾加上特殊标记,告诉前端"图片正在生成中" const conversationMessage = smartAutoFriendlyMessage || null; if (conversationMessage) { const chatMessageId = generateUUID(); chatApiLogger.info("[Chat API] 💬 Gemini: Sending conversational reply first:", smartAutoFriendlyMessage); // 发送带标记的对话消息 writeAssistantTextChunk( writer, conversationMessage, "gemini:conversation", chatMessageId, ); // 保存对话消息到数据库(不带标记,纯对话) await saveMessages({ messages: [ { id: chatMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: smartAutoFriendlyMessage }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); } const buildMessagesForGemini = () => { const lastUserMessage = uiMessagesForModel.filter((m) => m.role === "user").pop(); let messagesForGemini = lastUserMessage ? [lastUserMessage] : uiMessagesForModel; messagesForGemini = applyOptimizedPromptToLatestUserMessage( messagesForGemini, smartAutoOptimizedPrompt, hiddenPromptPrefix, ); return injectHistoryImagesToLastUserMessage(messagesForGemini, smartAutoHistoryImages); }; // 🆕 尝试生成图片的函数(支持备用模型切换) const tryGenerateImage = async (modelIdToUse: string, isRetry = false): Promise => { chatApiLogger.info(`[Chat API] ${isRetry ? "🔄 Retry with fallback" : "🚀 Trying"} model: ${modelIdToUse}`); // 只传递最后一条用户消息,避免上下文历史干扰生图效果 const messagesForGemini = buildMessagesForGemini(); chatApiLogger.info("[Chat API] Gemini: Using only last user message (no history context)"); // 转换消息:gemini-image 需要 base64 格式,huanke-gemini 需要 URL 格式 const providerForConvert = (languageModel as any).provider === "huanke-gemini" ? "huanke-gemini" : "gemini"; const modelMessages = await convertMessagesWithImages(messagesForGemini, providerForConvert); chatApiLogger.info("[Chat API] Message conversion provider:", providerForConvert); // 调用 doGenerate 获取图片(如果是备用模型,传入 overrideModelId) const result = await (languageModel as any).doGenerate({ messages: modelMessages, imageParams: effectiveImageGenerationParams, overrideModelId: isRetry ? modelIdToUse : undefined, // 🆕 使用备用模型 ID }); return assertGeneratedImageMarkdownHasImage( result.text || "", isRetry ? `备用模型 ${modelIdToUse}` : `主模型 ${modelIdToUse}`, ); }; let stopKeepalive: (() => void) | null = null; try { let imageMarkdown = ""; // 初始化为空字符串 let usedModelId = actualModelId; const generationStartedAt = Date.now(); // 🆕 图片生成真正开始时发出标记(用于前端显示占位图) writeAssistantTextChunk( writer, "", "gemini:image-start", ); if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( writer, { phase: "processing", providerType: (languageModel as any).provider, modelName: currentModelConfig?.displayName || actualModelId, imageCount: effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1, }, "gemini:image-status-start", ); } stopKeepalive = startStreamKeepalive(writer, "gemini-image"); try { // 首先尝试主模型 imageMarkdown = await tryGenerateImage(actualModelId, false); } catch (primaryError) { chatApiLogger.error("[Chat API] Primary model failed:", primaryError); await recordAiModelHealthFailure({ model: currentModelEntry?.model || currentModelConfig, provider: currentModelEntry?.provider, error: primaryError, context: "chat_gemini_primary", elapsedMs: Date.now() - generationStartedAt, metadata: { runtimeModelId: actualModelId, finalModelId, providerType: (languageModel as any).provider, aspectRatio: effectiveImageGenerationParams.aspectRatio, imageSize: effectiveImageGenerationParams.imageSize, imageCount: effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1, }, }).catch((healthError) => { chatApiLogger.warn("[Chat API] Record Gemini primary health failure failed:", healthError); }); if (!shouldFailoverAiModelError(primaryError)) { chatApiLogger.info("[Chat API] Gemini primary error is not eligible for image failover; returning original error."); throw primaryError; } // 🆕 尝试备用模型(支持跨服务商切换) let fallbackSuccess = false; const triedFallbackModelIds = new Set(); for (const candidate of fallbackCandidates) { try { triedFallbackModelIds.add(String(candidate.model.id || "")); chatApiLogger.info(`[Chat API] 🔄 Trying fallback model: ${candidate.model.displayName} (${candidate.provider.type})`); const fallbackResult = await generateImageWithFailoverCandidate({ candidate, messages: buildMessagesForGemini(), imageParams: effectiveImageGenerationParams, fallbackImageParams: { aspectRatio: defaultImageQualityParams.aspectRatio, imageSize: defaultImageQualityParams.imageSize, }, }); imageMarkdown = assertGeneratedImageMarkdownHasImage( fallbackResult.text, `备用模型 ${fallbackResult.displayName}`, ); usedModelId = fallbackResult.usedModelId; fallbackSuccess = true; chatApiLogger.info(`[Chat API] ✅ Fallback model ${fallbackResult.displayName} succeeded!`); break; } catch (fallbackError) { const fallbackErrorMessage = normalizeFailoverErrorMessage(fallbackError); if (shouldCooldownImageFallbackModel(fallbackErrorMessage)) { setImageFallbackModelCooldown(candidate.model.id, fallbackErrorMessage); } await recordAiModelHealthFailure({ model: candidate.model, provider: candidate.provider, error: fallbackError, context: "chat_gemini_fallback", elapsedMs: Date.now() - generationStartedAt, metadata: { primaryModelId: actualModelId, fallbackModelId: candidate.model.modelId, providerType: candidate.provider.type, aspectRatio: effectiveImageGenerationParams.aspectRatio, imageSize: effectiveImageGenerationParams.imageSize, }, }).catch((healthError) => { chatApiLogger.warn("[Chat API] Record Gemini fallback health failure failed:", healthError); }); chatApiLogger.error(`[Chat API] ❌ Fallback model ${candidate.model.displayName} (${candidate.provider.type}) failed:`, fallbackError); } } if (!fallbackSuccess) { const providerPriority: Record = { "volcano-vision": 0, "alibaba-image": 1, "bfl-image": 2, "ruxa-image": 3, "fal-image": 4, "snapgen-image": 5, "openai-image": 6, bananaimage: 7, visionary: 7, atlascloud: 7, vip98k: 7, "stepfun-image": 7, "grsai-gpt-image": 8, runway: 9, krea: 9, gemini: 10, "huanke-gemini": 11, }; const crossProviderCandidates = getImageCrossProviderCandidates(allModels, { currentModelId: finalModelId, currentModelKey: actualModelId, currentProviderId: currentModelConfig?.providerId, providerPriority, }).filter((candidate) => !triedFallbackModelIds.has(String(candidate.model.id || ""))); for (const candidate of crossProviderCandidates) { try { triedFallbackModelIds.add(String(candidate.model.id || "")); chatApiLogger.info(`[Chat API] 🔁 Gemini cross-provider failover: ${candidate.model.displayName} (${candidate.provider.type})`); const fallbackResult = await generateImageWithFailoverCandidate({ candidate, messages: buildMessagesForGemini(), imageParams: effectiveImageGenerationParams, fallbackImageParams: { aspectRatio: defaultImageQualityParams.aspectRatio, imageSize: defaultImageQualityParams.imageSize, imageCount: defaultImageQualityParams.imageCount, }, }); imageMarkdown = assertGeneratedImageMarkdownHasImage( fallbackResult.text, `跨服务备用模型 ${fallbackResult.displayName}`, ); usedModelId = fallbackResult.usedModelId; fallbackSuccess = true; chatApiLogger.info(`[Chat API] ✅ Gemini cross-provider fallback model ${fallbackResult.displayName} succeeded!`); break; } catch (fallbackError) { const fallbackErrorMessage = normalizeFailoverErrorMessage(fallbackError); if (shouldCooldownImageFallbackModel(fallbackErrorMessage)) { setImageFallbackModelCooldown(candidate.model.id, fallbackErrorMessage); } await recordAiModelHealthFailure({ model: candidate.model, provider: candidate.provider, error: fallbackError, context: "chat_gemini_cross_fallback", elapsedMs: Date.now() - generationStartedAt, metadata: { primaryModelId: actualModelId, fallbackModelId: candidate.model.modelId, providerType: candidate.provider.type, aspectRatio: effectiveImageParams?.aspectRatio, imageSize: effectiveImageParams?.imageSize, }, }).catch((healthError) => { chatApiLogger.warn("[Chat API] Record Gemini cross-provider health failure failed:", healthError); }); chatApiLogger.error(`[Chat API] ❌ Gemini cross-provider fallback model ${candidate.model.displayName} (${candidate.provider.type}) failed:`, fallbackError); } } } // 如果所有备用模型都失败,抛出原始错误触发火山引擎故障转移 if (!fallbackSuccess) { throw primaryError; } } chatApiLogger.info("[Chat API] Gemini generated, length:", imageMarkdown.length); chatApiLogger.info("[Chat API] Gemini image markdown:", imageMarkdown.substring(0, 200)); chatApiLogger.info("[Chat API] Used model ID:", usedModelId); const usedModelEntry = resolveModelEntryByRuntimeModelId( allModels, usedModelId, usedModelId === actualModelId ? finalModelId : undefined, ); await recordAiModelHealthSuccess({ model: usedModelEntry?.model || (usedModelId === actualModelId ? currentModelConfig : null), }).catch((healthError) => { chatApiLogger.warn("[Chat API] Record Gemini health success failed:", healthError); }); const usedModelDisplayName = resolveModelDisplayNameByModelId(allModels, usedModelId) || currentModelConfig?.displayName || actualModelId; const imageMarkdownWithModel = prependImageModelBadge( imageMarkdown, usedModelDisplayName, ); // 生成一个统一的消息 ID const messageId = generateUUID(); // 写入消息到流 - 使用正确的 parts 格式 writeAssistantTextChunk( writer, imageMarkdownWithModel, "gemini:image-result", messageId, ); if (contextPayload?.source === "canvas") { writeCanvasMediaChunk( writer, { messageId, text: imageMarkdownWithModel, prompt: requestTextContent, sourceImageUrls: requestTargetResolution?.targetImageUrls || [], }, "gemini:image-canvas-media", ); } // 🆕 图片生成结束标记(成功) writeAssistantTextChunk( writer, "", "gemini:image-end-success", ); if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( writer, { phase: "completed", providerType: (languageModel as any).provider, modelName: usedModelDisplayName, imageCount: effectiveImageParams?.imageCount || defaultImageQualityParams.imageCount || 1, }, "gemini:image-status-completed", ); } // 保存消息到数据库(现在是 S3 URL,可以完整保存) try { await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: imageMarkdownWithModel }], attachments: [], // 必须提供 attachments 字段(数据库 notNull) createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); chatApiLogger.info("[Chat API] ✅ Gemini message saved successfully!"); // 提取用户 prompt(最后一条用户消息的文本) const lastUserMessage = uiMessages.filter((m) => m.role === "user").pop(); const userPrompt = lastUserMessage?.parts?.find((p): p is { type: "text"; text: string } => p.type === "text")?.text || ""; // 保存图片记录到 GeneratedImage 表 await saveGeneratedImages( imageMarkdownWithModel, session.user.id, id, messageId, userPrompt, resolveModelRefByRuntimeModelId( allModels, usedModelId, usedModelId === actualModelId ? finalModelId : undefined, ), { sourceAssetIds: requestTargetResolution?.targetAssetIds, sourceImageUrls: requestTargetResolution?.targetImageUrls, relationType: requestResolvedMediaInput?.hasImage ? "edited_from" : "generated_from", sourceType: requestResolvedMediaInput?.hasImage ? "generated" : "generated", projectId: contextPayload?.projectId || null, optimizedPrompt: smartAutoOptimizedPrompt || userPrompt, requestedImageCount: effectiveImageParams?.imageCount, aspectRatio: effectiveImageParams?.aspectRatio, imageSize: effectiveImageParams?.imageSize, }, ); } catch (saveError) { chatApiLogger.error("[Chat API] ❌ Failed to save Gemini message to database:", saveError); chatApiLogger.error("[Chat API] Message content that failed:", imageMarkdown.substring(0, 300)); } } catch (error) { chatApiLogger.error("[Chat API] Gemini generation error:", error); chatApiLogger.info("[Chat API] Attempting failover to Volcano Vision..."); // 故障转移:尝试使用火山引擎模型 try { // 查找火山引擎模型 const allModels = await getEnabledAiModelsWithProvider(); chatApiLogger.info("[Chat API] Total enabled models:", allModels.length); const volcanoModel = allModels.find( (m: any) => m.provider.type === "volcano-vision" && m.model.isEnabled ); if (volcanoModel) { chatApiLogger.info("[Chat API] Found Volcano Vision model for failover:", volcanoModel.model.id); // 获取火山引擎模型实例 const volcanoLanguageModel = await getLanguageModelById(volcanoModel.model.id); if (volcanoLanguageModel && typeof (volcanoLanguageModel as any).doGenerate === "function") { chatApiLogger.info("[Chat API] Executing Volcano Vision failover..."); // 转换消息(火山引擎会自动处理 Base64) const volcanoMessages = await convertMessagesWithImages( injectHistoryImagesToLastUserMessage(uiMessagesForModel, smartAutoHistoryImages), "volcano" ); // 调用火山引擎生成(故障转移时只生成 1 张,保持与 Gemini 一致) const volcanoResult = await (volcanoLanguageModel as any).doGenerate({ messages: volcanoMessages, maxImages: 1, // 故障转移时只生成 1 张 imageParams: effectiveImageGenerationParams, }); const imageMarkdown = assertGeneratedImageMarkdownHasImage( volcanoResult.text || "", volcanoModel.model.displayName || volcanoModel.model.modelId, ); const imageMarkdownWithModel = prependImageModelBadge( imageMarkdown, volcanoModel.model.displayName || volcanoModel.model.modelId, ); chatApiLogger.info("[Chat API] Volcano Vision failover success, length:", imageMarkdown.length); // 生成消息 ID const messageId = generateUUID(); // 写入消息到流 writeAssistantTextChunk( writer, imageMarkdownWithModel, "gemini:volcano-failover-result", messageId, ); if (contextPayload?.source === "canvas") { writeCanvasMediaChunk( writer, { messageId, text: imageMarkdownWithModel, prompt: requestTextContent, sourceImageUrls: requestTargetResolution?.targetImageUrls || [], }, "gemini:volcano-failover-canvas-media", ); } // 🆕 图片生成结束标记(故障转移成功) writeAssistantTextChunk( writer, "", "gemini:volcano-failover-end", ); if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( writer, { phase: "completed", providerType: "volcano-vision", modelName: volcanoModel.model.displayName || volcanoModel.model.modelId, imageCount: 1, }, "gemini:volcano-failover-status-completed", ); } // 保存消息到数据库 try { await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: imageMarkdownWithModel }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); chatApiLogger.info("[Chat API] ✅ Volcano Vision failover message saved!"); // 提取用户 prompt const lastUserMessage = uiMessages.filter((m) => m.role === "user").pop(); const userPrompt = lastUserMessage?.parts?.find((p): p is { type: "text"; text: string } => p.type === "text")?.text || ""; // 保存图片记录到 GeneratedImage 表 await saveGeneratedImages( imageMarkdownWithModel, session.user.id, id, messageId, userPrompt, volcanoModel.model.id, { sourceAssetIds: requestTargetResolution?.targetAssetIds, sourceImageUrls: requestTargetResolution?.targetImageUrls, relationType: requestResolvedMediaInput?.hasImage ? "edited_from" : "generated_from", sourceType: "generated", projectId: contextPayload?.projectId || null, optimizedPrompt: smartAutoOptimizedPrompt || userPrompt, requestedImageCount: effectiveImageParams?.imageCount, aspectRatio: effectiveImageParams?.aspectRatio, imageSize: effectiveImageParams?.imageSize, }, ); } catch (saveError) { chatApiLogger.error("[Chat API] Failed to save failover message:", saveError); } return; // 成功,直接返回 } } chatApiLogger.info("[Chat API] No Volcano Vision model available for failover"); } catch (failoverError) { chatApiLogger.error("[Chat API] Failover to Volcano Vision also failed:", failoverError); } // 如果故障转移也失败,返回错误消息 const errorMessageId = generateUUID(); writeAssistantTextChunk( writer, "图片生成失败,请稍后重试", "gemini:error", errorMessageId, ); // 🆕 图片生成结束标记(失败) writeAssistantTextChunk( writer, "", "gemini:image-end-failed", ); // 尝试保存错误消息 try { await saveMessages({ messages: [ { id: errorMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: "图片生成失败,请稍后重试" }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); } catch (saveError) { chatApiLogger.error("[Chat API] Failed to save error message:", saveError); } } finally { stopKeepalive?.(); } }, }); return createSseResponse(geminiStream); } // ========== Gemini 处理结束 ========== // ========== 阿里云千问图片生成独立处理 ========== const isAlibabaLikeImageProvider = isImageGenerationModel && [ "alibaba-image", "bfl-image", "ruxa-image", "fal-image", "snapgen-image", "openai-image", "bananaimage", "visionary", "atlascloud", "vip98k", "stepfun-image", "midjourney", "runway", "krea", ].includes((languageModel as any).provider); if (isAlibabaLikeImageProvider && typeof (languageModel as any).doGenerate === "function") { chatApiLogger.info("[Chat API] Using image generation provider:", (languageModel as any).provider); chatApiLogger.info("[Chat API] Image params:", effectiveImageGenerationParams); // 🆕 获取模型显示名称,用于错误提示 const alibabaModelConfig = await getAiModelById({ id: finalModelId }); const modelDisplayName = alibabaModelConfig?.displayName || actualModelId; const configuredFallbacks = [ alibabaModelConfig?.fallbackModelId1, alibabaModelConfig?.fallbackModelId2, ].filter(Boolean) as string[]; const allModels = await getEnabledAiModelsWithProvider(); const currentModelEntry = resolveModelEntryByRuntimeModelId( allModels, actualModelId, finalModelId, ); const fallbackCandidates = buildImageFallbackCandidates(allModels, { currentProviderId: alibabaModelConfig?.providerId, currentModelId: alibabaModelConfig?.id, currentModelKey: actualModelId, configuredFallbackIds: configuredFallbacks, }); chatApiLogger.info("[Chat API] Alibaba fallback candidates:", fallbackCandidates.map((candidate) => `${candidate.model.displayName}(${candidate.provider.type})`)); const alibabaStream = createUIMessageStream({ execute: async ({ writer }) => { let stopKeepalive: (() => void) | null = null; try { // 🆕 如果是智能模式且有对话回复,先发送对话消息让用户看到 const conversationMessage = smartAutoFriendlyMessage || null; if (conversationMessage) { const chatMessageId = generateUUID(); chatApiLogger.info("[Chat API] 💬 Sending conversational reply first:", smartAutoFriendlyMessage); // 发送带标记的对话消息 writeAssistantTextChunk( writer, conversationMessage, "alibaba:conversation", chatMessageId, ); // 保存对话消息到数据库(不带标记,纯对话) await saveMessages({ messages: [ { id: chatMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: smartAutoFriendlyMessage }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); } // 只传递最后一条用户消息 const lastUserMessage = uiMessagesForModel.filter((m) => m.role === "user").pop(); let messagesForAlibaba = lastUserMessage ? [lastUserMessage] : uiMessagesForModel; chatApiLogger.info("[Chat API] Alibaba: Using only last user message"); // 🆕 如果有智能模式优化后的提示词,替换消息中的文本(仅用于API调用) if (smartAutoOptimizedPrompt) { chatApiLogger.info("[Chat API] Using optimized prompt for API call"); messagesForAlibaba = applyOptimizedPromptToLatestUserMessage( messagesForAlibaba, smartAutoOptimizedPrompt, hiddenPromptPrefix, ); } const resolvedTargetImageUrlsForProvider = normalizeResolvedTargetImageUrls( requestTargetResolution?.targetImageUrls ); if (resolvedTargetImageUrlsForProvider.length > 0) { messagesForAlibaba = applyResolvedTargetImagesToLastUserMessage( messagesForAlibaba, resolvedTargetImageUrlsForProvider ); chatApiLogger.info("[Chat API] Image provider target images:", { resolutionMode: requestTargetResolution?.resolutionMode || null, targetImageCount: resolvedTargetImageUrlsForProvider.length, imageCount: resolvedTargetImageUrlsForProvider.length, multiImageTarget: resolvedTargetImageUrlsForProvider.length > 1, firstImageUrl: summarizeTargetImageUrl(resolvedTargetImageUrlsForProvider[0]), }); } messagesForAlibaba = injectHistoryImagesToLastUserMessage(messagesForAlibaba, smartAutoHistoryImages); const isFalAsyncImageEdit = (languageModel as any).provider === "fal-image" && isFalGptImage2EditModelId(actualModelId); if (isFalAsyncImageEdit) { const falProvider = alibabaModelConfig?.providerId ? await getAiProviderById({ id: alibabaModelConfig.providerId }).catch(() => undefined) : undefined; const falApiKey = String(falProvider?.apiKey || "").trim(); if (!falApiKey) { throw new Error("FAL 图片编辑模型未配置可用的 API Key"); } const falMessages = await convertMessagesWithImages(messagesForAlibaba, "fal-image"); const queuedTask = await submitFalImageTask({ apiKey: falApiKey, modelId: actualModelId, messages: falMessages, imageParams: effectiveImageGenerationParams, }); const placeholderMessageId = generateUUID(); const userPrompt = lastUserMessage?.parts?.find( (p): p is { type: "text"; text: string } => p.type === "text", )?.text || ""; const modelRef = resolveModelRefByRuntimeModelId( allModels, queuedTask.modelId, finalModelId, ); const placeholderText = `已提交 ${modelDisplayName} 编辑任务,预计 2-4 分钟完成,结果会自动回填到这里。`; const queuedMetadata: FalImageEditTaskMetadata = { taskKind: "fal_gpt_image_2_edit", provider: "fal-image", providerId: String(alibabaModelConfig?.providerId || ""), endpointId: queuedTask.endpointId, requestId: queuedTask.requestId, modelId: queuedTask.modelId, modelRef, modelDisplayName, prompt: queuedTask.prompt, optimizedPrompt: smartAutoOptimizedPrompt || userPrompt || queuedTask.prompt, userPrompt: userPrompt || queuedTask.prompt, requestedImageCount: effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1, aspectRatio: effectiveImageGenerationParams.aspectRatio, imageSize: effectiveImageGenerationParams.imageSize, outputSizeLabel: queuedTask.outputSizeLabel, sourceImageUrls: requestTargetResolution?.targetImageUrls || queuedTask.sourceImageUrls, sourceAssetIds: requestTargetResolution?.targetAssetIds || [], relationType: requestResolvedMediaInput?.hasImage ? "edited_from" : "generated_from", sourceType: "generated", projectId: contextPayload?.projectId || null, contextSource: contextPayload?.source || null, status: "queued", startedAt: Date.now(), updatedAt: Date.now(), queuePosition: null, lastLogs: [], error: null, }; writeAssistantTextChunk( writer, "", "alibaba:image-start", ); writeImageGenerationStatusChunk( writer, { phase: "queued", providerType: "fal-image", modelName: modelDisplayName, imageCount: effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1, }, "alibaba:image-status-queued", ); writeAssistantTextChunk( writer, placeholderText, "alibaba:image-queued", placeholderMessageId, ); await saveMessages({ messages: [ { id: placeholderMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: placeholderText }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: queuedMetadata as any, }, ], }); chatApiLogger.info("[Chat API] ✅ Queued FAL GPT Image 2 Edit task", { requestId: queuedTask.requestId, endpointId: queuedTask.endpointId, messageId: placeholderMessageId, }); return; } const tryGenerateImage = async (modelIdToUse: string, isRetry: boolean) => { // 转换消息(图片编辑模型统一使用 URL 输入) const providerForConvert = (languageModel as any).provider === "bfl-image" ? "bfl-image" : (languageModel as any).provider === "ruxa-image" ? "ruxa-image" : (languageModel as any).provider === "fal-image" ? "fal-image" : (languageModel as any).provider === "snapgen-image" ? "snapgen-image" : (languageModel as any).provider === "openai-image" ? "openai-image" : (languageModel as any).provider === "bananaimage" ? "bananaimage" : (languageModel as any).provider === "visionary" ? "visionary" : (languageModel as any).provider === "atlascloud" ? "atlascloud" : (languageModel as any).provider === "vip98k" ? "vip98k" : (languageModel as any).provider === "stepfun-image" ? "stepfun-image" : (languageModel as any).provider === "midjourney" ? "midjourney" : (languageModel as any).provider === "runway" ? "runway" : (languageModel as any).provider === "krea" ? "krea" : "alibaba-image"; const modelMessages = await convertMessagesWithImages(messagesForAlibaba, providerForConvert); // 调用 doGenerate 获取图片,传递模型显示名称 const result = await (languageModel as any).doGenerate({ messages: modelMessages, imageParams: effectiveImageGenerationParams, imageGenConfig: alibabaModelConfig?.imageGenConfig || null, modelDisplayName, // 🆕 传递模型显示名称 overrideModelId: isRetry ? modelIdToUse : undefined, }); if (result?.finishReason === "error") { throw new Error(result.text || "图片生成失败"); } return assertGeneratedImageMarkdownHasImage( result.text || "", isRetry ? `备用模型 ${modelIdToUse}` : `主模型 ${modelIdToUse}`, ); }; let imageMarkdown = ""; let usedModelId = actualModelId; const generationStartedAt = Date.now(); // 🆕 图片生成真正开始时发出标记(用于前端显示占位图) writeAssistantTextChunk( writer, "", "alibaba:image-start", ); if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( writer, { phase: "processing", providerType: (languageModel as any).provider, modelName: alibabaModelConfig?.displayName || actualModelId, imageCount: effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1, }, "alibaba:image-status-start", ); } stopKeepalive = startStreamKeepalive(writer, "alibaba-image"); try { imageMarkdown = await tryGenerateImage(actualModelId, false); } catch (primaryError) { chatApiLogger.error("[Chat API] Alibaba primary model failed:", primaryError); await recordAiModelHealthFailure({ model: currentModelEntry?.model || alibabaModelConfig, provider: currentModelEntry?.provider, error: primaryError, context: "chat_image_primary", elapsedMs: Date.now() - generationStartedAt, metadata: { runtimeModelId: actualModelId, finalModelId, providerType: (languageModel as any).provider, aspectRatio: effectiveImageGenerationParams.aspectRatio, imageSize: effectiveImageGenerationParams.imageSize, imageCount: effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1, }, }).catch((healthError) => { chatApiLogger.warn("[Chat API] Record image primary health failure failed:", healthError); }); if ( !shouldFailoverAiModelError(primaryError) && !shouldFailoverVip98kRequestRejection( primaryError, (languageModel as any).provider, ) ) { chatApiLogger.info("[Chat API] Alibaba primary error is not eligible for image failover; returning original error."); throw primaryError; } let fallbackSuccess = false; const triedFallbackModelIds = new Set(); for (const candidate of fallbackCandidates) { try { triedFallbackModelIds.add(String(candidate.model.id || "")); chatApiLogger.info(`[Chat API] 🔄 Alibaba fallback model: ${candidate.model.displayName} (${candidate.provider.type})`); const fallbackResult = await generateImageWithFailoverCandidate({ candidate, messages: messagesForAlibaba, imageParams: effectiveImageGenerationParams, fallbackImageParams: { aspectRatio: defaultImageQualityParams.aspectRatio, imageSize: defaultImageQualityParams.imageSize, imageCount: defaultImageQualityParams.imageCount, }, }); imageMarkdown = assertGeneratedImageMarkdownHasImage( fallbackResult.text, `备用模型 ${fallbackResult.displayName}`, ); usedModelId = fallbackResult.usedModelId; fallbackSuccess = true; chatApiLogger.info(`[Chat API] ✅ Alibaba fallback model ${fallbackResult.displayName} succeeded!`); break; } catch (fallbackError) { const fallbackErrorMessage = normalizeFailoverErrorMessage(fallbackError); if (shouldCooldownImageFallbackModel(fallbackErrorMessage)) { setImageFallbackModelCooldown(candidate.model.id, fallbackErrorMessage); } await recordAiModelHealthFailure({ model: candidate.model, provider: candidate.provider, error: fallbackError, context: "chat_image_fallback", elapsedMs: Date.now() - generationStartedAt, metadata: { primaryModelId: actualModelId, fallbackModelId: candidate.model.modelId, providerType: candidate.provider.type, aspectRatio: effectiveImageGenerationParams.aspectRatio, imageSize: effectiveImageGenerationParams.imageSize, }, }).catch((healthError) => { chatApiLogger.warn("[Chat API] Record image fallback health failure failed:", healthError); }); chatApiLogger.error(`[Chat API] ❌ Alibaba fallback model ${candidate.model.displayName} (${candidate.provider.type}) failed:`, fallbackError); } } if (!fallbackSuccess) { const providerPriority: Record = { "volcano-vision": 0, "alibaba-image": 1, "bfl-image": 2, "ruxa-image": 3, "fal-image": 4, "snapgen-image": 5, "openai-image": 6, bananaimage: 7, visionary: 7, atlascloud: 7, vip98k: 7, "stepfun-image": 7, runway: 8, krea: 9, gemini: 9, "huanke-gemini": 10, }; const crossProviderCandidates = getImageCrossProviderCandidates(allModels, { currentModelId: finalModelId, currentModelKey: actualModelId, currentProviderId: alibabaModelConfig?.providerId, providerPriority, }).filter((candidate) => !triedFallbackModelIds.has(String(candidate.model.id || ""))); for (const candidate of crossProviderCandidates) { try { triedFallbackModelIds.add(String(candidate.model.id || "")); chatApiLogger.info(`[Chat API] 🔁 Cross-provider failover attempt: ${candidate.model.displayName} (${candidate.provider.type})`); if ( candidate.provider.type === "fal-image" && isFalGptImage2EditModelId( String(candidate.model?.modelId || candidate.model?.id || "") ) ) { chatApiLogger.info(`[Chat API] Skip async-only failover candidate: ${candidate.model.displayName}`); continue; } const candidateLanguageModel = await getLanguageModelById(candidate.model.id); if (!candidateLanguageModel || typeof (candidateLanguageModel as any).doGenerate !== "function") { chatApiLogger.info(`[Chat API] Skip candidate without doGenerate: ${candidate.model.id}`); continue; } const convertProvider = candidate.provider.type === "bfl-image" ? "bfl-image" : candidate.provider.type === "ruxa-image" ? "ruxa-image" : candidate.provider.type === "fal-image" ? "fal-image" : candidate.provider.type === "snapgen-image" ? "snapgen-image" : candidate.provider.type === "openai-image" ? "openai-image" : candidate.provider.type === "bananaimage" ? "bananaimage" : candidate.provider.type === "visionary" ? "visionary" : candidate.provider.type === "atlascloud" ? "atlascloud" : candidate.provider.type === "vip98k" ? "vip98k" : candidate.provider.type === "stepfun-image" ? "stepfun-image" : candidate.provider.type === "midjourney" ? "midjourney" : candidate.provider.type === "runway" ? "runway" : candidate.provider.type === "krea" ? "krea" : candidate.provider.type === "gemini" ? "gemini" : candidate.provider.type === "huanke-gemini" ? "huanke-gemini" : candidate.provider.type === "volcano-vision" ? "volcano" : "alibaba-image"; const candidateMessages = await convertMessagesWithImages( messagesForAlibaba, convertProvider ); const candidateRuntimeModelId = resolveImageModelRuntimeId(candidate.model); const candidateResult = await (candidateLanguageModel as any).doGenerate({ messages: candidateMessages, imageParams: effectiveImageGenerationParams, imageGenConfig: candidate.model.imageGenConfig || null, modelDisplayName: candidate.model.displayName, overrideModelId: candidateRuntimeModelId || candidate.model.modelId, }); if (candidateResult?.finishReason === "error") { throw new Error(candidateResult.text || "图片生成失败"); } imageMarkdown = assertGeneratedImageMarkdownHasImage( candidateResult.text || "", `跨服务备用模型 ${candidate.model.displayName}`, ); usedModelId = candidate.model.modelId; fallbackSuccess = true; chatApiLogger.info(`[Chat API] ✅ Cross-provider failover succeeded: ${candidate.model.displayName} (${candidate.provider.type})`); break; } catch (crossError) { const crossErrorMessage = normalizeFailoverErrorMessage(crossError); if (shouldCooldownImageFallbackModel(crossErrorMessage)) { setImageFallbackModelCooldown(candidate.model.id, crossErrorMessage); } await recordAiModelHealthFailure({ model: candidate.model, provider: candidate.provider, error: crossError, context: "chat_image_cross_fallback", elapsedMs: Date.now() - generationStartedAt, metadata: { primaryModelId: actualModelId, fallbackModelId: candidate.model.modelId, providerType: candidate.provider.type, aspectRatio: effectiveImageGenerationParams.aspectRatio, imageSize: effectiveImageGenerationParams.imageSize, }, }).catch((healthError) => { chatApiLogger.warn("[Chat API] Record image cross-provider health failure failed:", healthError); }); chatApiLogger.error(`[Chat API] ❌ Cross-provider failover failed: ${candidate.model.displayName} (${candidate.provider.type})`, crossError); } } } if (!fallbackSuccess) { throw primaryError; } } chatApiLogger.info("[Chat API] Alibaba generated, length:", imageMarkdown.length); chatApiLogger.info("[Chat API] Alibaba used model:", usedModelId); const usedModelEntry = resolveModelEntryByRuntimeModelId( allModels, usedModelId, usedModelId === actualModelId ? finalModelId : undefined, ); await recordAiModelHealthSuccess({ model: usedModelEntry?.model || (usedModelId === actualModelId ? alibabaModelConfig : null), }).catch((healthError) => { chatApiLogger.warn("[Chat API] Record image health success failed:", healthError); }); const usedModelDisplayName = resolveModelDisplayNameByModelId(allModels, usedModelId) || alibabaModelConfig?.displayName || actualModelId; const imageMarkdownWithModel = prependImageModelBadge( imageMarkdown, usedModelDisplayName, ); // 生成消息 ID(图片消息) const messageId = generateUUID(); // 写入图片消息到流 writeAssistantTextChunk( writer, imageMarkdownWithModel, "alibaba:image-result", messageId, ); if (contextPayload?.source === "canvas") { writeCanvasMediaChunk( writer, { messageId, text: imageMarkdownWithModel, prompt: requestTextContent, sourceImageUrls: requestTargetResolution?.targetImageUrls || [], }, "alibaba:image-canvas-media", ); } // 🆕 图片生成结束标记(成功) writeAssistantTextChunk( writer, "", "alibaba:image-end-success", ); if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( writer, { phase: "completed", providerType: (languageModel as any).provider, modelName: usedModelDisplayName, imageCount: effectiveImageParams?.imageCount || defaultImageQualityParams.imageCount || 1, }, "alibaba:image-status-completed", ); } // 保存图片消息到数据库 try { await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: imageMarkdownWithModel }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); chatApiLogger.info("[Chat API] ✅ Alibaba message saved successfully!"); // 提取用户 prompt const userPrompt = lastUserMessage?.parts?.find((p): p is { type: "text"; text: string } => p.type === "text")?.text || ""; // 保存图片记录到 GeneratedImage 表 await saveGeneratedImages( imageMarkdownWithModel, session.user.id, id, messageId, userPrompt, resolveModelRefByRuntimeModelId( allModels, usedModelId, usedModelId === actualModelId ? finalModelId : undefined, ), { sourceAssetIds: requestTargetResolution?.targetAssetIds, sourceImageUrls: requestTargetResolution?.targetImageUrls, relationType: requestResolvedMediaInput?.hasImage ? "edited_from" : "generated_from", sourceType: "generated", projectId: contextPayload?.projectId || null, optimizedPrompt: smartAutoOptimizedPrompt || userPrompt, requestedImageCount: effectiveImageParams?.imageCount, aspectRatio: effectiveImageParams?.aspectRatio, imageSize: effectiveImageParams?.imageSize, }, ); } catch (saveError) { chatApiLogger.error("[Chat API] ❌ Failed to save Alibaba message:", saveError); } } catch (error) { chatApiLogger.error("[Chat API] Alibaba generation error:", error); // 返回错误消息 const errorMessageId = generateUUID(); const errorText = getPublicImageGenerationErrorText(error); writeAssistantTextChunk( writer, errorText, "alibaba:error", errorMessageId, ); // 🆕 图片生成结束标记(失败) writeAssistantTextChunk( writer, "", "alibaba:image-end-failed", ); try { await saveMessages({ messages: [ { id: errorMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: errorText }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); } catch (saveError) { chatApiLogger.error("[Chat API] Failed to save error message:", saveError); } } finally { stopKeepalive?.(); } }, }); return createSseResponse(alibabaStream); } // ========== 阿里云图片生成处理结束 ========== // ========== 火山引擎视觉图片生成独立处理 ========== if ((languageModel as any).provider === "volcano-vision" && typeof (languageModel as any).doGenerate === "function") { chatApiLogger.info("[Chat API] Using Volcano Vision image generation"); // 🆕 检测是否有标注图片,如果有则在消息中注入系统提示 const hasAnnotation = hasAnnotatedImage(uiMessages); let messagesForVolcano = applyOptimizedPromptToLatestUserMessage( uiMessagesForModel, smartAutoOptimizedPrompt, hiddenPromptPrefix, ); const volcanoModelConfig = await getAiModelById({ id: finalModelId }); const configuredFallbacks = [ volcanoModelConfig?.fallbackModelId1, volcanoModelConfig?.fallbackModelId2, ].filter(Boolean) as string[]; const allModels = await getEnabledAiModelsWithProvider(); const fallbackCandidates = buildImageFallbackCandidates(allModels, { currentProviderId: volcanoModelConfig?.providerId, currentModelId: volcanoModelConfig?.id, currentModelKey: actualModelId, configuredFallbackIds: configuredFallbacks, }); chatApiLogger.info("[Chat API] Volcano fallback candidates:", fallbackCandidates.map((candidate) => `${candidate.model.displayName}(${candidate.provider.type})`)); if (hasAnnotation) { chatApiLogger.info("[Chat API] 🎨 Volcano Vision: 检测到标注图片,将注入系统提示"); // 创建消息副本,修改最后一条用户消息的文本 messagesForVolcano = messagesForVolcano.map((msg, idx) => { if (msg.role === "user" && idx === messagesForVolcano.length - 1) { // 修改最后一条用户消息 const modifiedParts = msg.parts.map((part) => { if (part.type === "text" && part.text) { return { ...part, text: ANNOTATION_SYSTEM_PROMPT + "\n\n用户请求:" + part.text, }; } return part; }); return { ...msg, parts: modifiedParts }; } return msg; }); } messagesForVolcano = injectHistoryImagesToLastUserMessage(messagesForVolcano, smartAutoHistoryImages); const volcanoStream = createUIMessageStream({ execute: async ({ writer }) => { let stopKeepalive: (() => void) | null = null; try { // 🆕 如果是智能模式且有对话回复,先发送对话消息让用户看到 const conversationMessage = smartAutoFriendlyMessage || null; if (conversationMessage) { const chatMessageId = generateUUID(); chatApiLogger.info("[Chat API] 💬 Volcano: Sending conversational reply first:", smartAutoFriendlyMessage); // 发送带标记的对话消息 writeAssistantTextChunk( writer, conversationMessage, "volcano:conversation", chatMessageId, ); // 保存对话消息到数据库(不带标记,纯对话) await saveMessages({ messages: [ { id: chatMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: smartAutoFriendlyMessage }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); } // 转换消息,火山引擎支持 URL 和 base64 const modelMessages = await convertMessagesWithImages(messagesForVolcano, "volcano"); const tryGenerateImage = async (modelIdToUse: string, isRetry: boolean) => { const result = await (languageModel as any).doGenerate({ messages: modelMessages, imageParams: effectiveImageGenerationParams, overrideModelId: isRetry ? modelIdToUse : undefined, }); if (result?.finishReason === "error") { throw new Error(result.text || "图片生成失败"); } return assertGeneratedImageMarkdownHasImage( result.text || "", isRetry ? `备用模型 ${modelIdToUse}` : `主模型 ${modelIdToUse}`, ); }; let imageMarkdown = ""; let usedModelId = actualModelId; // 🆕 图片生成真正开始时发出标记(用于前端显示占位图) writeAssistantTextChunk( writer, "", "volcano:image-start", ); if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( writer, { phase: "processing", providerType: "volcano-vision", modelName: volcanoModelConfig?.displayName || actualModelId, imageCount: effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1, }, "volcano:image-status-start", ); } stopKeepalive = startStreamKeepalive(writer, "volcano-image"); try { imageMarkdown = await tryGenerateImage(actualModelId, false); } catch (primaryError) { chatApiLogger.error("[Chat API] Volcano primary model failed:", primaryError); if (!shouldFailoverAiModelError(primaryError)) { chatApiLogger.info("[Chat API] Volcano primary error is not eligible for image failover; returning original error."); throw primaryError; } let fallbackSuccess = false; for (const candidate of fallbackCandidates) { try { chatApiLogger.info(`[Chat API] 🔄 Volcano fallback model: ${candidate.model.displayName} (${candidate.provider.type})`); const fallbackResult = await generateImageWithFailoverCandidate({ candidate, messages: messagesForVolcano, imageParams: effectiveImageGenerationParams, fallbackImageParams: { aspectRatio: defaultImageQualityParams.aspectRatio, imageSize: defaultImageQualityParams.imageSize, imageCount: defaultImageQualityParams.imageCount, }, }); imageMarkdown = assertGeneratedImageMarkdownHasImage( fallbackResult.text, `备用模型 ${fallbackResult.displayName}`, ); usedModelId = fallbackResult.usedModelId; fallbackSuccess = true; chatApiLogger.info(`[Chat API] ✅ Volcano fallback model ${fallbackResult.displayName} succeeded!`); break; } catch (fallbackError) { const fallbackErrorMessage = normalizeFailoverErrorMessage(fallbackError); if (shouldCooldownImageFallbackModel(fallbackErrorMessage)) { setImageFallbackModelCooldown(candidate.model.id, fallbackErrorMessage); } chatApiLogger.error(`[Chat API] ❌ Volcano fallback model ${candidate.model.displayName} (${candidate.provider.type}) failed:`, fallbackError); } } if (!fallbackSuccess) { const providerPriority: Record = { "alibaba-image": 0, "bfl-image": 1, "ruxa-image": 2, "fal-image": 3, "snapgen-image": 4, "openai-image": 5, bananaimage: 6, visionary: 6, atlascloud: 6, vip98k: 6, "stepfun-image": 6, runway: 7, krea: 8, gemini: 8, "huanke-gemini": 9, "volcano-vision": 9, }; const crossProviderCandidates = getImageCrossProviderCandidates(allModels, { currentModelId: finalModelId, currentModelKey: actualModelId, currentProviderId: volcanoModelConfig?.providerId, providerPriority, }); for (const candidate of crossProviderCandidates) { try { chatApiLogger.info(`[Chat API] 🔁 Volcano cross-provider failover attempt: ${candidate.model.displayName} (${candidate.provider.type})`); if ( candidate.provider.type === "fal-image" && isFalGptImage2EditModelId( String(candidate.model?.modelId || candidate.model?.id || "") ) ) { chatApiLogger.info(`[Chat API] Skip async-only failover candidate: ${candidate.model.displayName}`); continue; } const candidateLanguageModel = await getLanguageModelById(candidate.model.id); if (!candidateLanguageModel || typeof (candidateLanguageModel as any).doGenerate !== "function") { chatApiLogger.info(`[Chat API] Skip candidate without doGenerate: ${candidate.model.id}`); continue; } const convertProvider = candidate.provider.type === "bfl-image" ? "bfl-image" : candidate.provider.type === "ruxa-image" ? "ruxa-image" : candidate.provider.type === "fal-image" ? "fal-image" : candidate.provider.type === "snapgen-image" ? "snapgen-image" : candidate.provider.type === "openai-image" ? "openai-image" : candidate.provider.type === "bananaimage" ? "bananaimage" : candidate.provider.type === "visionary" ? "visionary" : candidate.provider.type === "atlascloud" ? "atlascloud" : candidate.provider.type === "vip98k" ? "vip98k" : candidate.provider.type === "stepfun-image" ? "stepfun-image" : candidate.provider.type === "midjourney" ? "midjourney" : candidate.provider.type === "runway" ? "runway" : candidate.provider.type === "krea" ? "krea" : candidate.provider.type === "gemini" ? "gemini" : candidate.provider.type === "huanke-gemini" ? "huanke-gemini" : candidate.provider.type === "volcano-vision" ? "volcano" : "alibaba-image"; const candidateMessages = await convertMessagesWithImages( messagesForVolcano, convertProvider ); const candidateRuntimeModelId = resolveImageModelRuntimeId(candidate.model); const candidateResult = await (candidateLanguageModel as any).doGenerate({ messages: candidateMessages, imageParams: effectiveImageGenerationParams, imageGenConfig: candidate.model.imageGenConfig || null, modelDisplayName: candidate.model.displayName, overrideModelId: candidateRuntimeModelId || candidate.model.modelId, }); if (candidateResult?.finishReason === "error") { throw new Error(candidateResult.text || "图片生成失败"); } imageMarkdown = assertGeneratedImageMarkdownHasImage( candidateResult.text || "", `跨服务备用模型 ${candidate.model.displayName}`, ); usedModelId = candidate.model.modelId; fallbackSuccess = true; chatApiLogger.info(`[Chat API] ✅ Volcano cross-provider failover succeeded: ${candidate.model.displayName} (${candidate.provider.type})`); break; } catch (crossError) { const crossErrorMessage = normalizeFailoverErrorMessage(crossError); if (shouldCooldownImageFallbackModel(crossErrorMessage)) { setImageFallbackModelCooldown(candidate.model.id, crossErrorMessage); } chatApiLogger.error(`[Chat API] ❌ Volcano cross-provider failover failed: ${candidate.model.displayName} (${candidate.provider.type})`, crossError); } } } if (!fallbackSuccess) { throw primaryError; } } chatApiLogger.info("[Chat API] Volcano Vision generated, length:", imageMarkdown.length); chatApiLogger.info("[Chat API] Volcano Vision image markdown:", imageMarkdown.substring(0, 200)); const usedModelDisplayName = resolveModelDisplayNameByModelId(allModels, usedModelId) || volcanoModelConfig?.displayName || actualModelId; const imageMarkdownWithModel = prependImageModelBadge( imageMarkdown, usedModelDisplayName, ); // 生成消息 ID(图片消息) const messageId = generateUUID(); // 写入图片消息到流 writeAssistantTextChunk( writer, imageMarkdownWithModel, "volcano:image-result", messageId, ); if (contextPayload?.source === "canvas") { writeCanvasMediaChunk( writer, { messageId, text: imageMarkdownWithModel, prompt: requestTextContent, sourceImageUrls: requestTargetResolution?.targetImageUrls || [], }, "volcano:image-canvas-media", ); } // 🆕 图片生成结束标记(成功) writeAssistantTextChunk( writer, "", "volcano:image-end-success", ); if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( writer, { phase: "completed", providerType: "volcano-vision", modelName: usedModelDisplayName, imageCount: effectiveImageParams?.imageCount || defaultImageQualityParams.imageCount || 1, }, "volcano:image-status-completed", ); } // 保存图片消息到数据库 try { await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: imageMarkdownWithModel }], attachments: [], // 必须提供 attachments 字段(数据库 notNull) createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); chatApiLogger.info("[Chat API] ✅ Volcano Vision message saved successfully!"); // 提取用户 prompt const lastUserMessage = uiMessages.filter((m) => m.role === "user").pop(); const userPrompt = lastUserMessage?.parts?.find((p): p is { type: "text"; text: string } => p.type === "text")?.text || ""; // 保存图片记录到 GeneratedImage 表 await saveGeneratedImages( imageMarkdownWithModel, session.user.id, id, messageId, userPrompt, resolveModelRefByRuntimeModelId( allModels, usedModelId, usedModelId === actualModelId ? finalModelId : undefined, ), { sourceAssetIds: requestTargetResolution?.targetAssetIds, sourceImageUrls: requestTargetResolution?.targetImageUrls, relationType: requestResolvedMediaInput?.hasImage ? "edited_from" : "generated_from", sourceType: "generated", projectId: contextPayload?.projectId || null, optimizedPrompt: smartAutoOptimizedPrompt || userPrompt, requestedImageCount: effectiveImageParams?.imageCount, aspectRatio: effectiveImageParams?.aspectRatio, imageSize: effectiveImageParams?.imageSize, }, ); } catch (saveError) { chatApiLogger.error("[Chat API] ❌ Failed to save Volcano Vision message to database:", saveError); chatApiLogger.error("[Chat API] Message content that failed:", imageMarkdown.substring(0, 300)); } } catch (error) { chatApiLogger.error("[Chat API] Volcano Vision generation error:", error); const errorMessageId = generateUUID(); writeAssistantTextChunk( writer, "图片生成失败,请稍后重试", "volcano:error", errorMessageId, ); // 🆕 图片生成结束标记(失败) writeAssistantTextChunk( writer, "", "volcano:image-end-failed", ); // 尝试保存错误消息 try { await saveMessages({ messages: [ { id: errorMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: "图片生成失败,请稍后重试" }], attachments: [], // 必须提供 attachments 字段 createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); } catch (saveError) { chatApiLogger.error("[Chat API] Failed to save error message:", saveError); } } finally { stopKeepalive?.(); } }, }); return createSseResponse(volcanoStream); } // ========== 火山引擎视觉处理结束 ========== // ========== 千问/FAL/RUXA 视频生成独立处理 ========== if ( ["qianwen-video", "fal-video", "ruxa-video", "runway", "krea", "bltcy-video", "vip98k"].includes((languageModel as any).provider) && typeof (languageModel as any).doGenerate === "function" ) { const currentVideoProvider = (languageModel as any).provider as | "qianwen-video" | "fal-video" | "ruxa-video" | "runway" | "krea" | "bltcy-video" | "vip98k"; chatApiLogger.info(`[Chat API] Using ${currentVideoProvider} video generation`); const videoGenerationStream = createUIMessageStream({ execute: async ({ writer }) => { try { const conversationMessage = smartAutoFriendlyMessage || null; if (conversationMessage) { const chatMessageId = generateUUID(); writer.write({ type: "data-appendMessage", data: JSON.stringify({ id: chatMessageId, role: "assistant", parts: [{ type: "text", text: conversationMessage }], }), }); await saveMessages({ messages: [ { id: chatMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: conversationMessage }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); } const messagesForVideo = injectHistoryImagesToLastUserMessage( applyOptimizedPromptToLatestUserMessage( uiMessagesForModel, smartAutoOptimizedPrompt, hiddenPromptPrefix, ), smartAutoHistoryImages, ); // 千问/FAL 视频直接使用原始 URL,不转换为 base64 const modelMessages = convertToModelMessages(messagesForVideo); chatApiLogger.info(`[Chat API] ${currentVideoProvider} messages prepared, count:`, modelMessages.length); let videoMarkdown = "视频生成失败"; let tempVideoUrl = ""; let billedModelRefs: string[] = [finalModelId]; let videoMetadata: Record = { provider: currentVideoProvider, model: finalModelId, mode: effectiveVideoParams?.mode, resolution: effectiveVideoParams?.resolution, ratio: effectiveVideoParams?.ratio, duration: effectiveVideoParams?.duration, status: "succeeded", updatedAt: Date.now(), }; let usedStoryboardPipeline = false; if (currentVideoProvider === "ruxa-video") { const storyboardConfig = await getVideoStoryboardConfig().catch(() => null); if (storyboardConfig?.enabled) { const storyboardModelCandidates = await resolveStoryboardModelCandidates(finalModelId); const runtimeModelId = String((languageModel as any).modelId || "").trim(); const selectedModelId = runtimeModelId || storyboardModelCandidates.find((item) => item.id === finalModelId)?.modelId || ""; const storyboardResult = await runRuxaNineGridPipeline({ userId: session.user.id, chatId: id, modelMessages: modelMessages as any, selectedModelId, selectedModelRef: finalModelId, modelCandidates: storyboardModelCandidates, videoParams: effectiveVideoParams as any, config: storyboardConfig, doGenerate: (input) => (languageModel as any).doGenerate({ messages: input.messages, videoParams: input.videoParams, overrideModelId: input.overrideModelId, }), onProgress: (event) => { safeWriteStreamChunk( writer, { type: "data-videoStoryboard", data: JSON.stringify(event), }, `ruxa-video:storyboard:${event.stage}`, ); }, }); if (storyboardResult.triggered) { usedStoryboardPipeline = true; videoMarkdown = storyboardResult.markdown || "视频生成失败,请稍后重试"; tempVideoUrl = storyboardResult.videoUrls?.[0] || ""; const isAwaitingConfirmation = storyboardResult.reason === "awaiting_user_confirmation"; billedModelRefs = storyboardResult.modelRefs && storyboardResult.modelRefs.length > 0 ? storyboardResult.modelRefs : [finalModelId]; const successCount = storyboardResult.videoUrls?.length || 0; videoMetadata = { ...videoMetadata, ...(storyboardResult.metadata || {}), status: isAwaitingConfirmation ? "awaiting_user_confirmation" : successCount > 0 ? "succeeded" : "failed", storyboard: { enabled: true, taskId: storyboardResult.taskId, confidence: storyboardResult.confidence, successCount, awaitingConfirmation: isAwaitingConfirmation, }, updatedAt: Date.now(), }; } } } if (!usedStoryboardPipeline) { const result = await (languageModel as any).doGenerate({ messages: modelMessages, videoParams: effectiveVideoParams, }); if (result?.finishReason === "error") { throw new Error(result.text || "视频生成失败"); } videoMarkdown = result.text || "视频生成失败"; tempVideoUrl = extractVideoUrlFromText(videoMarkdown) || ""; } chatApiLogger.info(`[Chat API] ${currentVideoProvider} video generated, length:`, videoMarkdown.length); // 生成一个统一的消息 ID const messageId = generateUUID(); // 写入消息到流 writer.write({ type: "data-appendMessage", data: JSON.stringify({ id: messageId, role: "assistant", parts: [{ type: "text", text: videoMarkdown }], attachments: [], }), }); // 保存消息到数据库 try { await saveMessages({ messages: [ { id: messageId, chatId: id, role: "assistant", parts: [{ type: "text", text: videoMarkdown }], attachments: [], createdAt: new Date(), videoStorageStatus: tempVideoUrl ? "temporary" : null, temporaryVideoUrl: tempVideoUrl || null, permanentVideoUrl: null, archivedAt: null, videoMetadata: videoMetadata as any, }, ], }); const storyboardTaskId = String((videoMetadata as any)?.taskId || "").trim(); if (storyboardTaskId) { await attachVideoStoryboardTaskMessage({ taskId: storyboardTaskId, messageId, }).catch((taskAttachError) => { chatApiLogger.warn("[Chat API] Failed to attach storyboard task with message:", taskAttachError); }); } chatApiLogger.info(`[Chat API] ✅ ${currentVideoProvider} video message saved successfully!`); if ((videoMetadata as any)?.storyboard?.enabled) { try { const storyboardGridType = String((videoMetadata as any)?.gridType || "").trim(); const storyboardShotCount = Number((videoMetadata as any)?.shotCount || 0); await persistStoryboardAssistantTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, lastMessageId: messageId, text: requestTextContent, storyboardGridType: storyboardGridType === "grid_2x2" || storyboardGridType === "grid_3x2" || storyboardGridType === "grid_3x3" ? (storyboardGridType as "grid_2x2" | "grid_3x2" | "grid_3x3") : undefined, storyboardShotCount: storyboardShotCount === 4 || storyboardShotCount === 6 || storyboardShotCount === 9 ? storyboardShotCount : undefined, }); } catch (storyboardMemoryPersistError) { chatApiLogger.warn("[Chat API] Failed to persist storyboard memory after assistant response:", storyboardMemoryPersistError); } } else { try { await persistVideoGenerationCompletedTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId, text: requestTextContent, source: "native_video_provider", providerType: currentVideoProvider, modelRef: finalModelId, mode: typeof effectiveVideoParams?.mode === "string" ? effectiveVideoParams.mode : undefined, taskId: storyboardTaskId || undefined, status: typeof (videoMetadata as any)?.status === "string" ? (videoMetadata as any).status : tempVideoUrl ? "completed" : undefined, temporaryVideoUrl: tempVideoUrl || undefined, }); } catch (videoWorkflowPersistError) { chatApiLogger.warn("[Chat API] Failed to persist native video workflow state:", videoWorkflowPersistError); } } const shouldConsumeVideoPoints = tempVideoUrl && (videoMetadata as any)?.status !== "awaiting_user_confirmation"; if (shouldConsumeVideoPoints) { await consumeGeneratedVideoPoints({ userId: session.user.id, chatId: id, messageId, modelRefs: billedModelRefs, duration: Number(effectiveVideoParams?.duration) || undefined, resolution: String(effectiveVideoParams?.resolution || "").trim() || undefined, }); } if (tempVideoUrl) { scheduleVideoArchive({ chatId: id, messageId, text: videoMarkdown, temporaryVideoUrl: tempVideoUrl, taskId: messageId, metadata: videoMetadata, }); } } catch (saveError) { chatApiLogger.error(`[Chat API] ❌ Failed to save ${currentVideoProvider} video message:`, saveError); } } catch (error) { chatApiLogger.error(`[Chat API] ${currentVideoProvider} video generation error:`, error); const errorMessageId = generateUUID(); const errorText = "视频生成失败,请稍后重试"; writer.write({ type: "data-appendMessage", data: JSON.stringify({ id: errorMessageId, role: "assistant", parts: [{ type: "text", text: errorText }], attachments: [], }), }); try { await saveMessages({ messages: [ { id: errorMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: errorText }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); await persistVideoGenerationFailedTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId: errorMessageId, text: requestTextContent, source: "native_video_provider", providerType: currentVideoProvider, modelRef: finalModelId, mode: typeof effectiveVideoParams?.mode === "string" ? effectiveVideoParams.mode : undefined, status: "failed", error: error instanceof Error ? error.message : String(error), }); } catch (videoFailurePersistError) { chatApiLogger.warn("[Chat API] Failed to persist native video failure state:", videoFailurePersistError); } } }, }); return createSseResponse(videoGenerationStream); } // ========== 千问/FAL/RUXA 视频处理结束 ========== let stopStreamKeepalive: (() => void) | null = null; const stream = createUIMessageStream({ execute: async ({ writer: dataStream }) => { const stopDefaultKeepalive = startStreamKeepalive( dataStream, "chat-default", DEFAULT_STREAM_KEEPALIVE_INTERVAL_MS ); stopStreamKeepalive = stopDefaultKeepalive; setConversationStateProtocolEventObserver((event) => { writeProtocolDebugChunk( dataStream, Boolean(debugProtocol), { phase: "event", type: event.type, payload: event.payload, }, `protocol:event:${event.type}` ); }); safeWriteStreamChunk( dataStream, { type: "data-keepalive", data: JSON.stringify({ ts: Date.now(), label: "chat-default:init" }), }, "chat-default:init" ); writeProtocolDebugChunk( dataStream, Boolean(debugProtocol), { phase: "request", chatId: id, selectedChatModel, hasHiddenPrompt: Boolean(hiddenPrompt), hasAutoContext: Boolean(autoContext), }, "protocol:request" ); writeCurrentAgentTargetLockChunk(dataStream, "chat-default:target-lock"); for (const event of pendingProtocolDebugEvents) { writeProtocolDebugChunk( dataStream, Boolean(debugProtocol), { phase: event.phase, ...event.payload, }, `protocol:${event.phase}` ); } try { // 构建工具对象,只包含启用的工具 const tools: any = {}; const activeTools: string[] = []; if (enabledTools.includes("getWeather")) { tools.getWeather = getWeather; activeTools.push("getWeather"); } if (enabledTools.includes("createDocument")) { tools.createDocument = createDocument({ session, dataStream }); activeTools.push("createDocument"); } if (enabledTools.includes("updateDocument")) { tools.updateDocument = updateDocument({ session, dataStream }); activeTools.push("updateDocument"); } if (enabledTools.includes("requestSuggestions")) { tools.requestSuggestions = requestSuggestions({ session, dataStream, }); activeTools.push("requestSuggestions"); } const shouldEnableGenerateVideoTool = enabledTools.includes("generateVideo") && (!shouldRunAgentConversation || allowAgentVideoGeneration); if (shouldEnableGenerateVideoTool) { tools.generateVideo = generateVideo({ dataStream }); activeTools.push("generateVideo"); } else if (enabledTools.includes("generateVideo") && shouldRunAgentConversation) { chatApiLogger.info("[Smart Auto Mode] 🚫 generateVideo tool disabled (manual video mode not selected)"); } if ( contextPayload?.source === "canvas" && contextPayload.projectId && selectedCanvasImageNodeIds.length > 0 ) { const canvasCapabilityTools = await createCanvasCapabilityTools({ projectId: contextPayload.projectId, selectedNodeIds: selectedCanvasImageNodeIds, userId: session.user.id, requestHeaders: request.headers, requestUrl: request.url, defaultModelId: finalModelId, destructiveConfirmed: isCanvasOperationConfirmText(requestTextContent), callbacks: { onClientCommand: (command, capability) => { writeCanvasCapabilityCommandChunk( dataStream as unknown as StreamWriter, command, `canvas-capability:${capability.id}:client-command` ); }, onResult: (runtimeResult, capability) => { if (!runtimeResult.operation) return; writeCanvasOperationChunk( dataStream as unknown as StreamWriter, { projectId: contextPayload.projectId!, operationId: runtimeResult.task.requestId, operation: capability.id, status: "applied", createdNodeIds: runtimeResult.operation.createdNodeIds, createdNodeIdsByMediaItemId: runtimeResult.operation.createdNodeIdsByMediaItemId, updatedNodeIds: runtimeResult.operation.updatedNodeIds, canvasRevision: runtimeResult.operation.canvasRevision, message: `${capability.title}已完成`, }, `canvas-capability:${capability.id}:operation` ); }, }, }); Object.assign(tools, canvasCapabilityTools); activeTools.push(...getCanvasCapabilityToolNames(canvasCapabilityTools)); } chatApiLogger.info("[Chat API] Calling streamText with model:", (languageModel as any).modelId, (languageModel as any).provider); // 调试日志 chatApiLogger.info("[Chat API] Provider type:", (languageModel as any).provider); // 调试日志 // 转换消息,对于 Gemini 和千问视频将图片 URL 转换为 base64 const convertedMessages = await convertMessagesWithImages( injectHistoryImagesToLastUserMessage(uiMessagesForModel, smartAutoHistoryImages), { providerType: (languageModel as any).provider || "", providerBaseUrl: currentProviderConfig?.baseUrl || null, modelId: currentModelConfig?.modelId || actualModelId, displayName: currentModelConfig?.displayName || actualModelId, forceInlineImageUrls: shouldForceInlineVisionInputs({ providerType: resolvedProviderType || currentProviderConfig?.type || runtimeProviderType, providerBaseUrl: currentProviderConfig?.baseUrl || null, modelId: currentModelConfig?.modelId || actualModelId, displayName: currentModelConfig?.displayName || actualModelId, }), loggerPrefix: "[Chat API] Stream text vision inline", } ); chatApiLogger.info("[Chat API] Messages converted, count:", convertedMessages.length); // 调试日志 // 🆕 检测是否是视频模型 const providerType = (languageModel as any).provider; const effectiveProviderType = resolvedProviderType || providerType; chatApiLogger.info("[Chat API] Provider type for tool detection:", { runtime: providerType, resolved: resolvedProviderType, effective: effectiveProviderType, }); const isVolcanoVideoModel = effectiveProviderType === "volcano-video" || effectiveProviderType === "doubao-video" || actualModelId.includes("seedance"); const isOpenAICompatImageModel = resolvedModelSupportsImageGen && providerType === "openai.chat" && !isVolcanoVideoModel; chatApiLogger.info("[Chat API] Is volcano video model:", isVolcanoVideoModel); // 🆕 处理 OpenAI 兼容的生图模型输出(防止返回 data:image;base64 原文) if (isOpenAICompatImageModel) { chatApiLogger.info("[Chat API] Using generateText for OpenAI-compatible image model"); const configuredFallbacks = [ currentModelConfig?.fallbackModelId1, currentModelConfig?.fallbackModelId2, ].filter(Boolean) as string[]; const allModels = await getEnabledAiModelsWithProvider(); const fallbackCandidates = buildImageFallbackCandidates(allModels, { currentProviderId: currentModelConfig?.providerId, currentModelId: currentModelConfig?.id, currentModelKey: actualModelId, configuredFallbackIds: configuredFallbacks, }); chatApiLogger.info("[Chat API] OpenAI-compatible fallback candidates:", fallbackCandidates.map((candidate) => `${candidate.model.displayName}(${candidate.provider.type})`)); const lastUserMessage = uiMessagesForModel.filter((m) => m.role === "user").pop(); let messagesForCrossProvider = lastUserMessage ? [lastUserMessage] : uiMessagesForModel; messagesForCrossProvider = applyOptimizedPromptToLatestUserMessage( messagesForCrossProvider, smartAutoOptimizedPrompt, hiddenPromptPrefix, ); messagesForCrossProvider = injectHistoryImagesToLastUserMessage( messagesForCrossProvider, smartAutoHistoryImages ); // 生成开始标记 dataStream.write({ type: "data-appendMessage", data: JSON.stringify({ id: generateUUID(), role: "assistant", parts: [{ type: "text", text: "" }], }), }); if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( dataStream as unknown as StreamWriter, { phase: "processing", providerType: currentModelConfig?.providerId ? resolvedProviderType || providerType : providerType, modelName: currentModelConfig?.displayName || actualModelId, imageCount: effectiveImageParams?.imageCount || 1, }, "openai-compat:image-status-start", ); } try { const tryGenerateWithOpenAICompatModel = async (modelToUse: any) => { const imageRequestTimeoutMs = 600_000; const generated = await generateText({ model: modelToUse, system: systemPrompt({ selectedChatModel: finalModelId, requestHints }), messages: convertedMessages, abortSignal: AbortSignal.timeout(imageRequestTimeoutMs), }); const rawText = generated.text?.trim() || ""; const normalizedMarkdown = await normalizeAssistantImageMarkdown( rawText, effectiveImageParams?.imageSize || "2K" ); return assertGeneratedImageMarkdownHasImage( normalizedMarkdown, currentModelConfig?.displayName || actualModelId, ); }; let normalizedText = ""; let usedModelId = actualModelId; try { normalizedText = await tryGenerateWithOpenAICompatModel(languageModel); } catch (primaryError) { chatApiLogger.error("[Chat API] OpenAI-compatible primary model failed:", primaryError); if (!shouldFailoverAiModelError(primaryError)) { chatApiLogger.info("[Chat API] OpenAI-compatible primary error is not eligible for image failover; returning original error."); throw primaryError; } let fallbackSuccess = false; for (const candidate of fallbackCandidates) { try { chatApiLogger.info(`[Chat API] 🔄 OpenAI-compatible fallback model: ${candidate.model.displayName} (${candidate.provider.type})`); const shouldUseOpenAICompatFallback = candidate.sameProvider && !IMAGE_FAILOVER_PROVIDER_ALLOWLIST.has(String(candidate.provider?.type || "")); if (shouldUseOpenAICompatFallback) { const fallbackRuntimeModelId = String( candidate.model?.modelId || candidate.model?.id || "" ); const fallbackLanguageModel = await getLanguageModelById( finalModelId, fallbackRuntimeModelId ); if (!fallbackLanguageModel) { chatApiLogger.info(`[Chat API] Skip unavailable OpenAI-compatible fallback model: ${fallbackRuntimeModelId}`); continue; } normalizedText = await tryGenerateWithOpenAICompatModel(fallbackLanguageModel); usedModelId = fallbackRuntimeModelId; } else { const fallbackResult = await generateImageWithFailoverCandidate({ candidate, messages: messagesForCrossProvider, imageParams: effectiveImageGenerationParams, fallbackImageParams: { aspectRatio: defaultImageQualityParams.aspectRatio, imageSize: defaultImageQualityParams.imageSize, imageCount: defaultImageQualityParams.imageCount, }, }); normalizedText = assertGeneratedImageMarkdownHasImage( fallbackResult.text, `备用模型 ${fallbackResult.displayName}`, ); usedModelId = fallbackResult.usedModelId; } fallbackSuccess = true; chatApiLogger.info(`[Chat API] ✅ OpenAI-compatible fallback model ${candidate.model.displayName} succeeded!`); break; } catch (fallbackError) { const fallbackErrorMessage = normalizeFailoverErrorMessage(fallbackError); if (shouldCooldownImageFallbackModel(fallbackErrorMessage)) { setImageFallbackModelCooldown(candidate.model.id, fallbackErrorMessage); } chatApiLogger.error(`[Chat API] ❌ OpenAI-compatible fallback model ${candidate.model.displayName} (${candidate.provider.type}) failed:`, fallbackError); } } if (!fallbackSuccess) { const providerPriority: Record = { "volcano-vision": 0, "alibaba-image": 1, "bfl-image": 2, "ruxa-image": 3, "fal-image": 4, "snapgen-image": 5, "openai-image": 6, bananaimage: 7, visionary: 7, atlascloud: 7, vip98k: 7, "stepfun-image": 7, runway: 8, krea: 9, gemini: 9, "huanke-gemini": 10, }; const crossProviderCandidates = getImageCrossProviderCandidates(allModels, { currentModelId: finalModelId, currentModelKey: actualModelId, currentProviderId: currentModelConfig?.providerId, providerPriority, }); for (const candidate of crossProviderCandidates) { try { chatApiLogger.info(`[Chat API] 🔁 OpenAI-compatible cross-provider failover: ${candidate.model.displayName} (${candidate.provider.type})`); if ( candidate.provider.type === "fal-image" && isFalGptImage2EditModelId( String(candidate.model?.modelId || candidate.model?.id || "") ) ) { chatApiLogger.info(`[Chat API] Skip async-only failover candidate: ${candidate.model.displayName}`); continue; } const candidateLanguageModel = await getLanguageModelById(candidate.model.id); if ( !candidateLanguageModel || typeof (candidateLanguageModel as any).doGenerate !== "function" ) { chatApiLogger.info(`[Chat API] Skip cross-provider candidate without doGenerate: ${candidate.model.id}`); continue; } const convertProvider = candidate.provider.type === "bfl-image" ? "bfl-image" : candidate.provider.type === "ruxa-image" ? "ruxa-image" : candidate.provider.type === "fal-image" ? "fal-image" : candidate.provider.type === "snapgen-image" ? "snapgen-image" : candidate.provider.type === "openai-image" ? "openai-image" : candidate.provider.type === "bananaimage" ? "bananaimage" : candidate.provider.type === "visionary" ? "visionary" : candidate.provider.type === "atlascloud" ? "atlascloud" : candidate.provider.type === "vip98k" ? "vip98k" : candidate.provider.type === "stepfun-image" ? "stepfun-image" : candidate.provider.type === "midjourney" ? "midjourney" : candidate.provider.type === "runway" ? "runway" : candidate.provider.type === "krea" ? "krea" : candidate.provider.type === "gemini" ? "gemini" : candidate.provider.type === "huanke-gemini" ? "huanke-gemini" : candidate.provider.type === "volcano-vision" ? "volcano" : "alibaba-image"; const candidateMessages = await convertMessagesWithImages( messagesForCrossProvider, convertProvider ); const candidateRuntimeModelId = resolveImageModelRuntimeId(candidate.model); const candidateResult = await (candidateLanguageModel as any).doGenerate({ messages: candidateMessages, imageParams: effectiveImageGenerationParams, imageGenConfig: candidate.model.imageGenConfig || null, modelDisplayName: candidate.model.displayName, overrideModelId: candidateRuntimeModelId || candidate.model.modelId, }); if (candidateResult?.finishReason === "error") { throw new Error(candidateResult.text || "图片生成失败"); } normalizedText = assertGeneratedImageMarkdownHasImage( candidateResult.text || "", `跨服务备用模型 ${candidate.model.displayName}`, ); usedModelId = candidate.model.modelId; fallbackSuccess = true; chatApiLogger.info(`[Chat API] ✅ OpenAI-compatible cross-provider failover succeeded: ${candidate.model.displayName} (${candidate.provider.type})`); break; } catch (crossError) { const crossErrorMessage = normalizeFailoverErrorMessage(crossError); if (shouldCooldownImageFallbackModel(crossErrorMessage)) { setImageFallbackModelCooldown(candidate.model.id, crossErrorMessage); } chatApiLogger.error(`[Chat API] ❌ OpenAI-compatible cross-provider failover failed: ${candidate.model.displayName} (${candidate.provider.type})`, crossError); } } } if (!fallbackSuccess) { throw primaryError; } } const usedModelDisplayName = resolveModelDisplayNameByModelId(allModels, usedModelId) || currentModelConfig?.displayName || actualModelId; const normalizedTextWithModel = prependImageModelBadge( normalizedText, usedModelDisplayName, ); const messageId = generateUUID(); dataStream.write({ type: "data-appendMessage", data: JSON.stringify({ id: messageId, role: "assistant", parts: [{ type: "text", text: normalizedTextWithModel }], }), }); if (contextPayload?.source === "canvas") { writeCanvasMediaChunk( dataStream as unknown as StreamWriter, { messageId, text: normalizedTextWithModel, prompt: requestTextContent, sourceImageUrls: requestTargetResolution?.targetImageUrls || [], }, "openai-compat:image-canvas-media", ); } // 生成结束标记 dataStream.write({ type: "data-appendMessage", data: JSON.stringify({ id: generateUUID(), role: "assistant", parts: [{ type: "text", text: "" }], }), }); if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( dataStream as unknown as StreamWriter, { phase: "completed", providerType: resolvedProviderType || providerType, modelName: usedModelDisplayName, imageCount: effectiveImageParams?.imageCount || 1, }, "openai-compat:image-status-completed", ); } await saveMessages({ messages: [ { id: messageId, role: "assistant", parts: [{ type: "text", text: normalizedTextWithModel }], createdAt: new Date(), attachments: [], chatId: id, videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); const latestUserMessage = uiMessages.filter((m) => m.role === "user").pop(); const userPrompt = latestUserMessage?.parts?.find( (p): p is { type: "text"; text: string } => p.type === "text" )?.text || ""; await saveGeneratedImages( normalizedTextWithModel, session.user.id, id, messageId, userPrompt, resolveModelRefByRuntimeModelId( allModels, usedModelId, usedModelId === actualModelId ? finalModelId : undefined, ), { sourceAssetIds: requestTargetResolution?.targetAssetIds, sourceImageUrls: requestTargetResolution?.targetImageUrls, relationType: requestResolvedMediaInput?.hasImage ? "edited_from" : "generated_from", sourceType: "generated", projectId: contextPayload?.projectId || null, optimizedPrompt: smartAutoOptimizedPrompt || userPrompt, requestedImageCount: effectiveImageParams?.imageCount, aspectRatio: effectiveImageParams?.aspectRatio, imageSize: effectiveImageParams?.imageSize, }, ); return; } catch (error) { chatApiLogger.error("[Chat API] OpenAI-compatible image model generation error:", error); const errorMessageId = generateUUID(); const errorText = getPublicImageGenerationErrorText(error); dataStream.write({ type: "data-appendMessage", data: JSON.stringify({ id: errorMessageId, role: "assistant", parts: [{ type: "text", text: errorText }], }), }); dataStream.write({ type: "data-appendMessage", data: JSON.stringify({ id: generateUUID(), role: "assistant", parts: [{ type: "text", text: "" }], }), }); if (contextPayload?.source === "canvas") { writeImageGenerationStatusChunk( dataStream as unknown as StreamWriter, { phase: "failed", providerType: resolvedProviderType || providerType, modelName: currentModelConfig?.displayName || actualModelId, imageCount: effectiveImageParams?.imageCount || 1, }, "openai-compat:image-status-failed", ); } await saveMessages({ messages: [ { id: errorMessageId, role: "assistant", parts: [{ type: "text", text: errorText }], createdAt: new Date(), attachments: [], chatId: id, videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); return; } } // 🎬 视频模型使用专门的生成流程 if (isVolcanoVideoModel) { chatApiLogger.info("[Chat API] Using video generation API"); // 导入视频生成函数 const { doGenerateStream } = await import("@/lib/ai/providers/volcano-video"); const conversationMessage = smartAutoFriendlyMessage || null; if (conversationMessage) { const chatMessageId = generateUUID(); dataStream.write({ type: "data-appendMessage", data: JSON.stringify({ id: chatMessageId, role: "assistant", parts: [{ type: "text", text: conversationMessage }], attachments: [], }), }); await saveMessages({ messages: [ { id: chatMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: conversationMessage }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }, ], }); } const messagesForVideo = injectHistoryImagesToLastUserMessage( applyOptimizedPromptToLatestUserMessage( uiMessagesForModel, smartAutoOptimizedPrompt, hiddenPromptPrefix, ), smartAutoHistoryImages, ); // 从最后一条用户消息中提取提示词和图片 const lastUserMessage = messagesForVideo.filter(m => m.role === "user").pop(); let prompt = ""; const imageUrls: string[] = []; const summarizeImageRef = (rawUrl: string) => { const trimmed = String(rawUrl || "").trim(); if (!trimmed) return ""; if (trimmed.startsWith("data:image/")) { return `data:image(base64,${trimmed.length} chars)`; } try { const parsed = new URL(trimmed); return `${parsed.origin}${parsed.pathname}`; } catch { return trimmed.slice(0, 120); } }; if (lastUserMessage) { // 🆕 处理 parts 数组(AI SDK v4 格式) if ((lastUserMessage as any).parts && Array.isArray((lastUserMessage as any).parts)) { chatApiLogger.info("[Chat API] Video input parts:", (lastUserMessage as any).parts.length); for (const part of (lastUserMessage as any).parts) { if (part.type === "text" && part.text) { // 提取文本 prompt += part.text; } else if (part.type === "file" && part.url && part.mediaType?.startsWith("image/")) { // 提取图片 imageUrls.push(part.url); } } } // 备用方案1:content 字符串/数组 const legacyContent = (lastUserMessage as any).content; if (typeof legacyContent === "string") { prompt = legacyContent; } else if (Array.isArray(legacyContent)) { for (const part of legacyContent) { if (typeof part === "string") { prompt += part; } else if (part.type === "text") { prompt += part.text; } else if (part.type === "image" && (part as any).image) { imageUrls.push((part as any).image); } } } else if ((lastUserMessage as any).experimental_attachments) { // 备用方案2:experimental_attachments for (const attachment of (lastUserMessage as any).experimental_attachments) { if (attachment.url && attachment.contentType?.startsWith("image/")) { imageUrls.push(attachment.url); } } } } chatApiLogger.info("[Chat API] Video input extracted:", { promptLength: prompt.trim().length, imageCount: imageUrls.length, imageRefs: imageUrls.slice(0, 4).map(summarizeImageRef), }); const videoMessageId = generateUUID(); const videoStartAt = Date.now(); const baseVideoMetadata: Record = { provider: effectiveProviderType || "volcano-video", mode: effectiveVideoParams?.mode || "text-to-video", resolution: effectiveVideoParams?.resolution || "720p", ratio: effectiveVideoParams?.ratio || "16:9", duration: effectiveVideoParams?.duration || 5, status: "queued", startedAt: videoStartAt, updatedAt: videoStartAt, }; try { chatApiLogger.info("[Chat API] Video generation params:", { promptLength: prompt.trim().length, imageCount: imageUrls.length, videoParams: effectiveVideoParams, }); const videoMode = (effectiveVideoParams?.mode || "text-to-video") as VideoModeType; // 🆕 重新执行子模型选择逻辑,确定要使用的模型 UUID let videoModelId = finalModelId; // 默认使用选择的模型 ID // 检查是否是主模型,需要选择子模型(仅 volcano-video 支持子模型) const selectedModelData = await getAiModelById({ id: finalModelId }); const selectedModelProvider = selectedModelData ? await getAiProviderById({ id: selectedModelData.providerId }).catch(() => null) : null; if ( selectedModelData && !selectedModelData.isSubModel && selectedModelProvider?.type === "volcano-video" ) { const videoSubModels = await getSubModelsByParentModelId(finalModelId); chatApiLogger.info(`[Chat API] Video: Found ${videoSubModels.length} sub-models for master model`); if (videoSubModels.length > 0) { const matchedModel = selectBestVideoSubModelForMode(videoSubModels as any, videoMode); if (matchedModel) { videoModelId = matchedModel.model.id; chatApiLogger.info(`[Chat API] Video: Selected sub-model ${matchedModel.model.displayName} (${matchedModel.model.modelId}) for mode ${videoMode}`); } else { chatApiLogger.info(`[Chat API] Video: No compatible sub-model found for mode ${videoMode}, keep current model`); } } } chatApiLogger.info("[Chat API] Getting model config for:", videoModelId); const aiModelData = await getAiModelById({ id: videoModelId }); if (!aiModelData) { throw new Error("视频模型配置未找到"); } // 若用户手动选了不兼容模式的子模型,自动回退到同父模型兼容子模型 const selectedSupportsMode = getSupportedVideoModesForModel(aiModelData as any).includes(videoMode); if (!selectedSupportsMode) { let fallbackModel: { model: any } | null = null; if (aiModelData.parentModelId) { const siblingSubModels = await getSubModelsByParentModelId(aiModelData.parentModelId); fallbackModel = selectBestVideoSubModelForMode(siblingSubModels as any, videoMode); } else if (selectedModelProvider?.type === "volcano-video") { const enabledModels = await getEnabledAiModelsWithProvider(); const sameProviderModels = enabledModels .filter( ({ model, provider }) => provider.type === "volcano-video" && model.isEnabled && model.id !== aiModelData.id, ) .map(({ model }) => ({ model })); fallbackModel = selectBestVideoSubModelForMode(sameProviderModels as any, videoMode); } if (fallbackModel) { chatApiLogger.warn(`[Chat API] Video: Model ${aiModelData.modelId} does not support ${videoMode}, fallback to ${fallbackModel.model.modelId}`); videoModelId = fallbackModel.model.id; } } const finalVideoModelData = videoModelId === aiModelData.id ? aiModelData : await getAiModelById({ id: videoModelId }); if (!finalVideoModelData) { throw new Error("视频模型配置未找到"); } // 如果是子模型,获取父模型的 Provider 配置 let providerModelData = finalVideoModelData; if (finalVideoModelData.isSubModel && finalVideoModelData.parentModelId) { chatApiLogger.info("[Chat API] This is a sub-model, getting parent model config:", finalVideoModelData.parentModelId); const parentModel = await getAiModelById({ id: finalVideoModelData.parentModelId }); if (parentModel) { providerModelData = parentModel; } } chatApiLogger.info("[Chat API] Getting provider config for:", providerModelData.providerId); const providerData = await getAiProviderById({ id: providerModelData.providerId }); if (!providerData) { throw new Error("Provider 配置未找到"); } const actualVideoModelId = finalVideoModelData.modelId; // 构建视频生成选项 const videoOptions: any = { model: actualVideoModelId, prompt: prompt || "生成视频", mode: videoMode, resolution: effectiveVideoParams?.resolution || "720p", ratio: effectiveVideoParams?.ratio || "16:9", duration: effectiveVideoParams?.duration || 5, fps: effectiveVideoParams?.fps || 24, cameraMove: effectiveVideoParams?.cameraMove || "none", cameraFixed: effectiveVideoParams?.cameraFixed || false, watermark: effectiveVideoParams?.watermark || false, seed: effectiveVideoParams?.seed, }; // 根据模式添加图片 if (videoOptions.mode === "first-frame" && imageUrls[0]) { videoOptions.firstFrameImage = imageUrls[0]; } else if (videoOptions.mode === "first-last-frame" && imageUrls.length >= 2) { videoOptions.firstFrameImage = imageUrls[0]; videoOptions.lastFrameImage = imageUrls[1]; } else if (videoOptions.mode === "reference-images" && imageUrls.length > 0) { videoOptions.referenceImages = imageUrls; } chatApiLogger.info("[Chat API] Video options constructed:", { model: videoOptions.model, mode: videoOptions.mode, resolution: videoOptions.resolution, ratio: videoOptions.ratio, duration: videoOptions.duration, imageCount: videoOptions.referenceImages?.length || Number(Boolean(videoOptions.firstFrameImage)) + Number(Boolean(videoOptions.lastFrameImage)), }); // 先写入占位消息,避免刷新后消息丢失 const pendingText = "🎬 视频任务已创建,正在处理中,请稍候..."; await saveMessages({ messages: [ { id: videoMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: pendingText }], attachments: [], createdAt: new Date(), videoStorageStatus: "temporary", temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: { ...baseVideoMetadata, model: actualVideoModelId, status: "running", updatedAt: Date.now(), } as any, }, ], }); try { await persistVideoGenerationStartedTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId: videoMessageId, text: requestTextContent, source: "stream_video_provider", providerType: String(providerData.type || "stream"), modelRef: finalVideoModelData.id || finalModelId, mode: videoOptions.mode, status: "running", }); } catch (videoWorkflowStartError) { chatApiLogger.warn("[Chat API] Failed to persist stream video start state:", videoWorkflowStartError); } dataStream.write({ type: "data-appendMessage", data: JSON.stringify({ id: videoMessageId, role: "assistant", parts: [{ type: "text", text: pendingText }], attachments: [], }), }); const apiKey = providerData.apiKey; const baseURL = providerData.baseUrl || "https://ark.cn-beijing.volces.com/api/v3"; chatApiLogger.info("[Chat API] Provider configured:", { baseURL, hasApiKey: !!apiKey }); // 调用视频生成流 chatApiLogger.info("[Chat API] Starting video generation stream..."); const videoStream = doGenerateStream(apiKey, baseURL, videoOptions); // 收集所有流输出 chatApiLogger.info("[Chat API] Processing video stream..."); let fullText = ""; let chunkCount = 0; for await (const chunk of videoStream) { chunkCount++; fullText += chunk; chatApiLogger.info(`[Chat API] Received chunk ${chunkCount}, length: ${chunk.length}`); } chatApiLogger.info(`[Chat API] Video stream completed. Total chunks: ${chunkCount}, total text length: ${fullText.length}`); // 提取流中的元数据并清理输出文案 const metadataMatch = fullText.match(//s); let streamMetadata: Record = {}; if (metadataMatch?.[1]) { try { streamMetadata = JSON.parse(metadataMatch[1]); } catch (parseError) { chatApiLogger.warn("[Chat API] Failed to parse VIDEO_METADATA from stream", parseError); } } const cleanedText = fullText .replace(//gs, "") .trim(); const videoUrlMatch = cleanedText.match(/]+src="([^"]+)"/); const temporaryVideoUrl = (typeof streamMetadata.temporaryVideoUrl === "string" && streamMetadata.temporaryVideoUrl) || (videoUrlMatch ? videoUrlMatch[1] : null); const successMetadata = { ...baseVideoMetadata, ...streamMetadata, model: actualVideoModelId, taskId: typeof streamMetadata.taskId === "string" && streamMetadata.taskId ? streamMetadata.taskId : undefined, status: "succeeded", completedAt: Date.now(), updatedAt: Date.now(), }; await updateMessageById({ messageId: videoMessageId, chatId: id, parts: [{ type: "text", text: cleanedText }], videoStorageStatus: temporaryVideoUrl ? "temporary" : null, temporaryVideoUrl: temporaryVideoUrl, permanentVideoUrl: null, archivedAt: null, videoMetadata: successMetadata as any, }); try { await persistVideoGenerationCompletedTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId: videoMessageId, text: requestTextContent, source: "stream_video_provider", providerType: String(providerData.type || "stream"), modelRef: finalVideoModelData.id || finalModelId, mode: videoOptions.mode, taskId: typeof successMetadata.taskId === "string" && successMetadata.taskId ? successMetadata.taskId : undefined, status: "completed", temporaryVideoUrl: temporaryVideoUrl || undefined, }); } catch (videoWorkflowCompletedError) { chatApiLogger.warn("[Chat API] Failed to persist stream video completion state:", videoWorkflowCompletedError); } await consumeGeneratedVideoPoints({ userId: session.user.id, chatId: id, messageId: videoMessageId, modelRefs: [finalVideoModelData.id, providerModelData.id, finalModelId], duration: Number(videoOptions.duration) || undefined, resolution: String(videoOptions.resolution || "").trim() || undefined, }); dataStream.write({ type: "data-appendMessage", data: JSON.stringify({ id: videoMessageId, role: "assistant", parts: [{ type: "text", text: cleanedText }], attachments: [], }), }); chatApiLogger.info("[Chat API] ✅ Video message updated successfully"); if (temporaryVideoUrl) { chatApiLogger.info("[Chat API] 📹 临时视频URL已保存,开始异步归档到S3"); scheduleVideoArchive({ chatId: id, messageId: videoMessageId, text: cleanedText, temporaryVideoUrl, taskId: typeof successMetadata.taskId === "string" && successMetadata.taskId ? successMetadata.taskId : videoMessageId, metadata: successMetadata, }); } chatApiLogger.info("[Chat API] Video generation finished successfully"); return; } catch (error: any) { chatApiLogger.error("[Chat API] ❌ Video generation error:", error); chatApiLogger.error("[Chat API] Error stack:", error.stack); const errorMessage = `❌ 视频生成失败:${error.message}\n\n请检查配置或稍后重试。`; const failedMetadata = { ...baseVideoMetadata, status: "failed", error: error?.message || "未知错误", updatedAt: Date.now(), }; try { await updateMessageById({ messageId: videoMessageId, chatId: id, parts: [{ type: "text", text: errorMessage }], videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: failedMetadata as any, }); try { await persistVideoGenerationFailedTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId: videoMessageId, text: requestTextContent, source: "stream_video_provider", providerType: "stream", modelRef: finalModelId, mode: typeof effectiveVideoParams?.mode === "string" ? effectiveVideoParams.mode : undefined, status: "failed", error: error?.message || "未知错误", }); } catch (videoWorkflowFailedError) { chatApiLogger.warn("[Chat API] Failed to persist stream video failure state:", videoWorkflowFailedError); } dataStream.write({ type: "data-appendMessage", data: JSON.stringify({ id: videoMessageId, role: "assistant", parts: [{ type: "text", text: errorMessage }], attachments: [], }), }); } catch (updateError) { chatApiLogger.error("[Chat API] Failed to update placeholder video message:", updateError); const fallbackMessageId = generateUUID(); dataStream.write({ type: "data-appendMessage", data: JSON.stringify({ id: fallbackMessageId, role: "assistant", parts: [{ type: "text", text: errorMessage }], attachments: [], }), }); await saveMessages({ messages: [ { id: fallbackMessageId, chatId: id, role: "assistant", parts: [{ type: "text", text: errorMessage }], attachments: [], createdAt: new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: failedMetadata as any, }, ], }); try { await persistVideoGenerationFailedTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId: fallbackMessageId, text: requestTextContent, source: "stream_video_provider", providerType: "stream", modelRef: finalModelId, mode: typeof effectiveVideoParams?.mode === "string" ? effectiveVideoParams.mode : undefined, status: "failed", error: error?.message || "未知错误", }); } catch (videoWorkflowFailedError) { chatApiLogger.warn("[Chat API] Failed to persist fallback stream video failure state:", videoWorkflowFailedError); } } return; } } // 常规文本模型使用 streamText const streamRequestTimeoutMs = resolvedModelSupportsImageGen ? 600_000 : 90_000; const shouldDisableSmoothStream = resolvedModelSupportsImageGen || isVolcanoVideoModel; const result = streamText({ model: languageModel, system: effectiveConversationSystemPrompt, messages: convertedMessages, stopWhen: stepCountIs(shouldUseCanvasCapabilityToolLoop ? 8 : 5), experimental_activeTools: (finalModelId === "chat-model-reasoning" && !shouldUseCanvasCapabilityToolLoop) || isVolcanoVideoModel ? [] : activeTools, experimental_transform: shouldDisableSmoothStream ? undefined : smoothStream({ chunking: "word" }), tools: isVolcanoVideoModel ? {} : tools, // 🆕 视频模型禁用工具 abortSignal: AbortSignal.timeout(streamRequestTimeoutMs), experimental_telemetry: { isEnabled: isProductionEnvironment, functionId: "stream-text", }, onFinish: async ({ usage }) => { if (activeRuntimeModelDescriptor?.providerKey) { recordProviderHealthSuccess({ providerKey: activeRuntimeModelDescriptor.providerKey, providerType: activeRuntimeModelDescriptor.providerType, }); } try { const providers = await getTokenlensCatalog(); if (!actualModelId) { finalMergedUsage = usage; dataStream.write({ type: "data-usage", data: finalMergedUsage, }); return; } if (!providers) { finalMergedUsage = usage; dataStream.write({ type: "data-usage", data: finalMergedUsage, }); return; } const summary = getUsage({ modelId: actualModelId, usage, providers, }); finalMergedUsage = { ...usage, ...summary, modelId: actualModelId, } as AppUsage; dataStream.write({ type: "data-usage", data: finalMergedUsage }); } catch (err) { chatApiLogger.warn("TokenLens enrichment failed", err); finalMergedUsage = usage; dataStream.write({ type: "data-usage", data: finalMergedUsage }); } }, }); result.consumeStream({ onError: (error) => { chatApiLogger.error("[Chat API] streamText consume error:", error); if (activeRuntimeModelDescriptor?.providerKey) { recordProviderHealthFailure({ providerKey: activeRuntimeModelDescriptor.providerKey, providerType: activeRuntimeModelDescriptor.providerType, error, }); } }, }); dataStream.merge( result.toUIMessageStream({ sendReasoning: true, onError: (error) => { chatApiLogger.error("[Chat API] streamText ui stream error:", error); if (activeRuntimeModelDescriptor?.providerKey) { recordProviderHealthFailure({ providerKey: activeRuntimeModelDescriptor.providerKey, providerType: activeRuntimeModelDescriptor.providerType, error, }); } return getChatStreamErrorMessage(error); }, }) ); } finally { setConversationStateProtocolEventObserver(null); stopStreamKeepalive = stopDefaultKeepalive; } }, generateId: generateUUID, onFinish: async ({ messages }) => { stopStreamKeepalive?.(); stopStreamKeepalive = null; const messagesToPersist: Array<{ id: string; role: string; parts: unknown; createdAt: Date; attachments: unknown; chatId: string; videoStorageStatus: null; temporaryVideoUrl: null; permanentVideoUrl: null; archivedAt: null; videoMetadata: null; }> = []; for (const currentMessage of messages) { const normalizedParts = currentMessage.role === "assistant" ? normalizeAssistantPartsForPersistence( currentMessage.parts as Array> | undefined, ) : currentMessage.parts; if ( currentMessage.role === "assistant" && (!Array.isArray(normalizedParts) || normalizedParts.length === 0) ) { continue; } messagesToPersist.push({ id: currentMessage.id, role: currentMessage.role, parts: normalizedParts, createdAt: new Date(), attachments: [], chatId: id, videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }); } if (messagesToPersist.length > 0) { await saveMessages({ messages: messagesToPersist, }); } // 检测助手消息中是否包含视频,如果有则立即标记并归档到永久存储 for (const msg of messages) { if (msg.role === "assistant" && msg.parts) { const textContent = msg.parts .filter((part: any) => part && part.type === "text" && part.text) .map((part: any) => part.text) .join(""); const tempVideoUrl = extractVideoUrlFromText(textContent); if (tempVideoUrl) { const genericVideoMetadata = { provider: "stream", model: actualModelId, status: "succeeded", updatedAt: Date.now(), }; await updateMessageById({ messageId: msg.id, chatId: id, videoStorageStatus: "temporary", temporaryVideoUrl: tempVideoUrl, permanentVideoUrl: null, archivedAt: null, videoMetadata: genericVideoMetadata as any, }); try { await persistVideoGenerationCompletedTurn({ userId: session.user.id, chatId: id, projectId: contextPayload?.projectId || null, messageId: msg.id, text: requestTextContent, source: "chat_stream_output", providerType: "stream", modelRef: actualModelId, status: "completed", temporaryVideoUrl: tempVideoUrl, }); } catch (videoWorkflowCompletedError) { chatApiLogger.warn("[Chat API] Failed to persist streamed assistant video state:", videoWorkflowCompletedError); } scheduleVideoArchive({ chatId: id, messageId: msg.id, text: textContent, temporaryVideoUrl: tempVideoUrl, taskId: msg.id, metadata: genericVideoMetadata, }); } } } if (finalMergedUsage) { try { await updateChatLastContextById({ chatId: id, context: finalMergedUsage, }); } catch (err) { chatApiLogger.warn("Unable to persist last usage for chat", id, err); } } }, onError: () => { stopStreamKeepalive?.(); stopStreamKeepalive = null; return "Oops, an error occurred!"; }, }); // const streamContext = getStreamContext(); // if (streamContext) { // return new Response( // await streamContext.resumableStream(streamId, () => // stream.pipeThrough(new JsonToSseTransformStream()) // ) // ); // } return createSseResponse(stream); } catch (error) { const vercelId = request.headers.get("x-vercel-id"); if (error instanceof ChatSDKError) { return error.toResponse(); } if (isRateLimitError(error)) { const detailMessage = getRequestErrorMessage(error); chatApiLogger.warn("[Chat API] Upstream rate limited request:", { vercelId, error: detailMessage, }); return NextResponse.json( { code: "rate_limit:chat", message: getFriendlyRateLimitMessage(), cause: detailMessage || "upstream_rate_limited", retryAfter: 10, }, { status: 429, headers: { "Retry-After": "10", }, } ); } // Check for Vercel AI Gateway credit card error if ( error instanceof Error && error.message?.includes( "AI Gateway requires a valid credit card on file to service requests" ) ) { return new ChatSDKError("bad_request:activate_gateway").toResponse(); } return createChatApiFailureResponse(error, vercelId); } finally { if (requestDedupKey && acquiredInFlightLock) { inFlightChatRequests.delete(requestDedupKey); } } } export async function DELETE(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get("id"); if (!id) { return new ChatSDKError("bad_request:api").toResponse(); } const session = await auth(); if (!session?.user) { return new ChatSDKError("unauthorized:chat").toResponse(); } const chat = await getChatById({ id }); if (chat?.userId !== session.user.id) { return new ChatSDKError("forbidden:chat").toResponse(); } const deletedChat = await deleteChatById({ id }); return Response.json(deletedChat, { status: 200 }); }