feat: sync canvas resize flow and latest local updates
This commit is contained in:
parent
dc6162c93d
commit
5f849bf0b7
@ -49,10 +49,19 @@ import {
|
||||
getAiProviderById,
|
||||
checkUserImageQuota,
|
||||
consumeUserPoints,
|
||||
createMessageTargetResolution,
|
||||
getConversationWorkingStateByChatId,
|
||||
upsertConversationWorkingState,
|
||||
} from "@/lib/db/queries";
|
||||
import { db } from "@/lib/db";
|
||||
import type { DBMessage } from "@/lib/db/schema";
|
||||
import { generatedImage, message as messageTable } from "@/lib/db/schema";
|
||||
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 { extractImageUrlsFromText } from "@/lib/image-url";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
@ -1626,6 +1635,7 @@ async function saveGeneratedImages(
|
||||
const matches = [...imageMarkdown.matchAll(imageRegex)];
|
||||
|
||||
let savedImageCount = 0;
|
||||
const savedOriginalUrls: string[] = [];
|
||||
|
||||
for (const match of matches) {
|
||||
const altText = match[1] || "";
|
||||
@ -1644,12 +1654,30 @@ async function saveGeneratedImages(
|
||||
modelId,
|
||||
});
|
||||
savedImageCount += 1;
|
||||
if (!savedOriginalUrls.includes(originalUrl)) {
|
||||
savedOriginalUrls.push(originalUrl);
|
||||
}
|
||||
console.log(`[Gallery] Saved image: ${originalUrl.substring(0, 80)}...`);
|
||||
} catch (error) {
|
||||
console.error("[Gallery] Failed to save image record:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (savedOriginalUrls.length > 0) {
|
||||
try {
|
||||
await upsertConversationWorkingState({
|
||||
userId,
|
||||
chatId,
|
||||
currentFocusImageUrls: savedOriginalUrls,
|
||||
lastGeneratedImageUrls: savedOriginalUrls,
|
||||
lastResolvedTargetImageUrls: savedOriginalUrls,
|
||||
lastMessageId: messageId,
|
||||
});
|
||||
} catch (memoryError) {
|
||||
console.warn("[Gallery] Failed to update working state with generated images:", memoryError);
|
||||
}
|
||||
}
|
||||
|
||||
if (savedImageCount <= 0) return;
|
||||
|
||||
try {
|
||||
@ -1882,6 +1910,7 @@ export async function POST(request: Request) {
|
||||
selectedVisibilityType,
|
||||
hiddenPrompt,
|
||||
autoContext,
|
||||
contextPayload,
|
||||
deepThinkingEnabled,
|
||||
}: {
|
||||
id: string;
|
||||
@ -1890,6 +1919,7 @@ export async function POST(request: Request) {
|
||||
selectedVisibilityType: VisibilityType;
|
||||
hiddenPrompt?: string;
|
||||
autoContext?: string;
|
||||
contextPayload?: PostRequestBody["contextPayload"];
|
||||
deepThinkingEnabled?: boolean;
|
||||
} = requestBody;
|
||||
const requestStartedAt = Date.now();
|
||||
@ -1900,6 +1930,12 @@ export async function POST(request: Request) {
|
||||
return "";
|
||||
}
|
||||
})();
|
||||
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 now = Date.now();
|
||||
for (const [key, startedAt] of inFlightChatRequests.entries()) {
|
||||
@ -1941,6 +1977,11 @@ export async function POST(request: Request) {
|
||||
let smartAutoHistoryImages: string[] = []; // 从历史对话中提取的图片URL(支持多张)
|
||||
let smartAutoNeedsClarification = false; // 是否需要先追问用户
|
||||
let smartAutoClarificationQuestion: string | undefined;
|
||||
let requestResolvedMediaInput: ResolvedChatMediaInput | null = null;
|
||||
let requestTargetResolution: ResolvedTargetAssets | null = null;
|
||||
let requestWorkingState:
|
||||
| Awaited<ReturnType<typeof getConversationWorkingStateByChatId>>
|
||||
| null = null;
|
||||
let preferredAgentModelId: string | undefined;
|
||||
const deepThinkingOn = deepThinkingEnabled !== false;
|
||||
let shouldRunAgentConversation = finalModelId === "auto";
|
||||
@ -2032,126 +2073,62 @@ export async function POST(request: Request) {
|
||||
// 🆕 首先获取历史消息(在 Smart Auto Mode 内部单独获取,避免依赖后面的 messagesFromDb)
|
||||
const smartAutoHistoryMessagesFromDb = await getMessagesByChatId({ id });
|
||||
console.log("[Smart Auto Mode] Loaded", smartAutoHistoryMessagesFromDb.length, "messages from DB");
|
||||
|
||||
// 1. 提取用户消息内容
|
||||
const lastUserMessage = message;
|
||||
const textContent = lastUserMessage.parts
|
||||
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join(" ");
|
||||
autoTextForFallback = textContent;
|
||||
const autoContextText = typeof autoContext === "string" ? autoContext.trim() : "";
|
||||
const hasSelectedImageHint = /用户已选中\s*\d+\s*张图片/.test(autoContextText);
|
||||
const hasExplicitTargetHint = /第[一二三四五六七八九十\d]+张|左|右|上|下|中间|选中/.test(textContent);
|
||||
const hasActionableEditIntent = /改|调整|优化|换|替换|去掉|抠|重绘|增强|修|处理|按照|按|保留|统一|色差|背景|清晰|风格/.test(
|
||||
textContent
|
||||
);
|
||||
|
||||
const currentMessageImageInputs = collectImageInputsFromMessageParts(
|
||||
lastUserMessage.parts as any[],
|
||||
requestOrigin,
|
||||
).slice(0, 4);
|
||||
let imageUrls = currentMessageImageInputs;
|
||||
let hasImage = imageUrls.length > 0;
|
||||
autoCurrentImageUrlsForFallback = imageUrls.slice();
|
||||
|
||||
// 🆕 历史图片上下文感知:如果用户没有发送新图片,自动使用历史中最近的图片(支持多张)
|
||||
// 逻辑:只要用户在对话中没有附上新图片,就默认继续处理之前的图片
|
||||
if (imageUrls.length === 0 && smartAutoHistoryMessagesFromDb.length > 0) {
|
||||
console.log("[Smart Auto Mode] 🔍 No image in current message, auto-searching history...");
|
||||
|
||||
const explicitFollowLatestResult = /(上一张|上一轮|刚生成|最新生成|这个结果|该结果|按这个结果|继续在这张|基于这张结果|延续上一次结果)/i.test(
|
||||
textContent
|
||||
);
|
||||
const explicitUseOriginalUpload = /(原图|上传图|参考图|最初那张|第一张图|素材图)/i.test(
|
||||
textContent
|
||||
);
|
||||
|
||||
const latestAssistantImages: string[] = [];
|
||||
const latestUserImages: string[] = [];
|
||||
const pushUniqueUrl = (bucket: string[], maybeUrl?: string) => {
|
||||
if (!maybeUrl || typeof maybeUrl !== "string") return;
|
||||
const normalized = maybeUrl.trim();
|
||||
if (!/^https?:\/\//i.test(normalized)) return;
|
||||
if (!bucket.includes(normalized)) {
|
||||
bucket.push(normalized);
|
||||
}
|
||||
};
|
||||
|
||||
// 从历史消息中倒序查找最近的“AI生成图”和“用户上传图”
|
||||
for (let i = smartAutoHistoryMessagesFromDb.length - 1; i >= 0; i--) {
|
||||
const msg = smartAutoHistoryMessagesFromDb[i];
|
||||
const parts = msg.parts as any[];
|
||||
|
||||
if (!parts) continue;
|
||||
|
||||
if (msg.role === "assistant" && latestAssistantImages.length === 0) {
|
||||
for (const part of parts) {
|
||||
if (part.type === "text" && part.text) {
|
||||
const parsedImageUrls = extractOriginalImageUrlsFromText(part.text);
|
||||
for (const parsedUrl of parsedImageUrls) {
|
||||
pushUniqueUrl(latestAssistantImages, parsedUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.role === "user" && latestUserImages.length === 0) {
|
||||
for (const part of parts) {
|
||||
if ((part.type === "file" || part.type === "image") && part.url) {
|
||||
pushUniqueUrl(latestUserImages, part.url);
|
||||
}
|
||||
if (part.type === "image_url" && part.image_url?.url) {
|
||||
pushUniqueUrl(latestUserImages, part.image_url.url);
|
||||
}
|
||||
if (part.type === "text" && part.text) {
|
||||
const parsedImageUrls = extractOriginalImageUrlsFromText(part.text);
|
||||
for (const parsedUrl of parsedImageUrls) {
|
||||
pushUniqueUrl(latestUserImages, parsedUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (latestAssistantImages.length > 0 && latestUserImages.length > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (explicitUseOriginalUpload && latestUserImages.length > 0) {
|
||||
smartAutoHistoryImages = latestUserImages.slice(0, 4);
|
||||
console.log(
|
||||
"[Smart Auto Mode] 🖼️ Using latest user-uploaded image(s) by explicit instruction:",
|
||||
smartAutoHistoryImages.length
|
||||
);
|
||||
} else if (latestAssistantImages.length > 0) {
|
||||
smartAutoHistoryImages = latestAssistantImages.slice(0, 4);
|
||||
console.log(
|
||||
explicitFollowLatestResult
|
||||
? "[Smart Auto Mode] 🖼️ Using latest generated image(s) by explicit instruction:"
|
||||
: "[Smart Auto Mode] 🖼️ Using latest generated image(s) for continuous edit:",
|
||||
smartAutoHistoryImages.length
|
||||
);
|
||||
} else if (latestUserImages.length > 0) {
|
||||
smartAutoHistoryImages = latestUserImages.slice(0, 4);
|
||||
console.log(
|
||||
"[Smart Auto Mode] 🖼️ Fallback to latest user-uploaded image(s):",
|
||||
smartAutoHistoryImages.length
|
||||
);
|
||||
}
|
||||
|
||||
if (smartAutoHistoryImages.length > 0) {
|
||||
hasImage = true;
|
||||
imageUrls = smartAutoHistoryImages;
|
||||
autoFromHistoryForFallback = true;
|
||||
autoCurrentImageUrlsForFallback = imageUrls.slice();
|
||||
console.log("[Smart Auto Mode] ✅ Auto-using", smartAutoHistoryImages.length, "history image(s) for context continuity");
|
||||
} else {
|
||||
console.log("[Smart Auto Mode] ℹ️ No image found in history, proceeding as pure text");
|
||||
}
|
||||
autoTextForFallback = requestTextContent;
|
||||
if (!requestWorkingState) {
|
||||
requestWorkingState = await getConversationWorkingStateByChatId({
|
||||
chatId: id,
|
||||
});
|
||||
}
|
||||
if (!requestResolvedMediaInput) {
|
||||
requestResolvedMediaInput = resolveChatMediaInput({
|
||||
messageParts: message.parts as any[],
|
||||
historyMessages: smartAutoHistoryMessagesFromDb,
|
||||
contextPayload: contextPayload || undefined,
|
||||
requestOrigin,
|
||||
maxImages: 4,
|
||||
});
|
||||
}
|
||||
if (!requestTargetResolution) {
|
||||
requestTargetResolution = resolveTargetAssets({
|
||||
text: requestTextContent,
|
||||
mediaInput: requestResolvedMediaInput,
|
||||
contextPayload: contextPayload || undefined,
|
||||
workingState: requestWorkingState || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("[Smart Auto Mode] Input:", { textContent: textContent.substring(0, 100), hasImage, imageCount: imageUrls.length, fromHistory: smartAutoHistoryImages.length > 0 });
|
||||
const hasSelectedImageHint =
|
||||
Boolean(contextPayload?.selectedNodeIds?.length) ||
|
||||
/用户已选中\s*\d+\s*张图片/.test(autoContextText);
|
||||
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;
|
||||
|
||||
console.log("[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 }[] = [];
|
||||
@ -2186,7 +2163,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
const autoGraphResult = await withOperationTimeout(
|
||||
runAutoAgentGraph({
|
||||
text: textContent,
|
||||
text: requestTextContent,
|
||||
hasImage,
|
||||
imageUrls,
|
||||
imageFromHistory: smartAutoHistoryImages.length > 0,
|
||||
@ -2218,8 +2195,8 @@ export async function POST(request: Request) {
|
||||
) {
|
||||
smartAutoNeedsClarification = false;
|
||||
smartAutoClarificationQuestion = undefined;
|
||||
if (!smartAutoOptimizedPrompt && textContent.trim()) {
|
||||
smartAutoOptimizedPrompt = textContent.trim();
|
||||
if (!smartAutoOptimizedPrompt && requestTextContent.trim()) {
|
||||
smartAutoOptimizedPrompt = requestTextContent.trim();
|
||||
}
|
||||
if (
|
||||
!smartAutoFriendlyMessage ||
|
||||
@ -2280,7 +2257,7 @@ export async function POST(request: Request) {
|
||||
if (autoGraphResult.optimizedPrompt) {
|
||||
smartAutoOptimizedPrompt = autoGraphResult.optimizedPrompt;
|
||||
console.log("[Smart Auto Mode] ✨ Prompt optimized (LLM):", {
|
||||
original: textContent.substring(0, 50),
|
||||
original: requestTextContent.substring(0, 50),
|
||||
optimized: autoGraphResult.optimizedPrompt.substring(0, 100),
|
||||
});
|
||||
}
|
||||
@ -2311,7 +2288,7 @@ export async function POST(request: Request) {
|
||||
smartAutoVideoPlan = undefined;
|
||||
smartAutoRecommendedVideoParams = undefined;
|
||||
if (smartAutoPromptFromVideoPlan) {
|
||||
smartAutoOptimizedPrompt = textContent.trim() || undefined;
|
||||
smartAutoOptimizedPrompt = requestTextContent.trim() || undefined;
|
||||
smartAutoPromptFromVideoPlan = false;
|
||||
}
|
||||
smartAutoFriendlyMessage =
|
||||
@ -2821,6 +2798,36 @@ export async function POST(request: Request) {
|
||||
messagesFromDb = await getMessagesByChatId({ id });
|
||||
}
|
||||
|
||||
if (!requestWorkingState) {
|
||||
requestWorkingState = await getConversationWorkingStateByChatId({
|
||||
chatId: id,
|
||||
});
|
||||
}
|
||||
if (!requestResolvedMediaInput) {
|
||||
requestResolvedMediaInput = resolveChatMediaInput({
|
||||
messageParts: message.parts as any[],
|
||||
historyMessages: messagesFromDb,
|
||||
contextPayload: contextPayload || undefined,
|
||||
requestOrigin,
|
||||
maxImages: 4,
|
||||
});
|
||||
}
|
||||
if (!requestTargetResolution) {
|
||||
requestTargetResolution = resolveTargetAssets({
|
||||
text: requestTextContent,
|
||||
mediaInput: requestResolvedMediaInput,
|
||||
contextPayload: contextPayload || undefined,
|
||||
workingState: requestWorkingState || 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 hiddenPromptPrefix = hiddenPromptText ? `${hiddenPromptText}\n\n` : "";
|
||||
@ -2874,6 +2881,76 @@ export async function POST(request: Request) {
|
||||
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 upsertConversationWorkingState({
|
||||
userId: session.user.id,
|
||||
chatId: id,
|
||||
projectId: contextPayload?.projectId || null,
|
||||
currentIntent:
|
||||
smartAutoDetectedIntent ||
|
||||
(requestResolvedMediaInput.hasImage
|
||||
? "image_edit"
|
||||
: 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,
|
||||
lastTurnSummary: {
|
||||
text: requestTextContent.slice(0, 500),
|
||||
resolutionMode: requestTargetResolution.resolutionMode,
|
||||
confidence: requestTargetResolution.confidence,
|
||||
},
|
||||
lastMessageId: message.id,
|
||||
incrementTurnCount: true,
|
||||
});
|
||||
} catch (memoryError) {
|
||||
console.warn("[Chat API] Failed to persist context-memory state:", memoryError);
|
||||
}
|
||||
}
|
||||
|
||||
const streamId = generateUUID();
|
||||
await createStreamId({ streamId, chatId: id });
|
||||
|
||||
|
||||
@ -43,6 +43,33 @@ const imageParamsSchema = z.object({
|
||||
n: z.number().min(1).max(10).optional(), // 兼容 n 参数
|
||||
}).optional();
|
||||
|
||||
const contextPayloadSchema = z
|
||||
.object({
|
||||
source: z.enum(["chat", "canvas", "realtime-canvas"]).optional(),
|
||||
projectId: z.string().uuid().optional(),
|
||||
selectedNodeIds: z.array(z.string().min(1).max(128)).max(20).optional(),
|
||||
selectedAssetIds: z.array(z.string().uuid()).max(20).optional(),
|
||||
focusPolicy: z.enum(["auto", "selected", "latest_result", "original"]).optional(),
|
||||
canvasImageNodes: z
|
||||
.array(
|
||||
z.object({
|
||||
nodeId: z.string().min(1).max(128),
|
||||
assetId: z.string().uuid().optional(),
|
||||
url: z.string().url(),
|
||||
title: z.string().max(256).optional(),
|
||||
prompt: z.string().max(2000).optional(),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
width: z.number().optional(),
|
||||
height: z.number().optional(),
|
||||
isSelected: z.boolean().optional(),
|
||||
}),
|
||||
)
|
||||
.max(20)
|
||||
.optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
export const postRequestBodySchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
message: z.object({
|
||||
@ -58,6 +85,7 @@ export const postRequestBodySchema = z.object({
|
||||
deepThinkingEnabled: z.boolean().optional(),
|
||||
videoParams: videoParamsSchema, // 视频参数
|
||||
imageParams: imageParamsSchema, // 🆕 图片生成参数
|
||||
contextPayload: contextPayloadSchema,
|
||||
});
|
||||
|
||||
export type PostRequestBody = z.infer<typeof postRequestBodySchema>;
|
||||
|
||||
@ -3177,10 +3177,12 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
id: rawId,
|
||||
modelId: currentModelIdRef.current,
|
||||
imageParams: imageParamsRef.current,
|
||||
contextPayload: buildContextPayload(),
|
||||
lastMessage: request.messages.at(-1),
|
||||
});
|
||||
const hiddenPrompt = hiddenPromptRef.current || undefined;
|
||||
const autoContext = autoContextRef.current || undefined;
|
||||
const contextPayload = buildContextPayload();
|
||||
hiddenPromptRef.current = null;
|
||||
autoContextRef.current = null;
|
||||
return {
|
||||
@ -3197,6 +3199,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
imageParams: isVideoModelRef.current ? undefined : imageParamsRef.current || undefined,
|
||||
hiddenPrompt,
|
||||
autoContext,
|
||||
contextPayload,
|
||||
...request.body,
|
||||
},
|
||||
};
|
||||
@ -3951,6 +3954,67 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
return lines.join("\n");
|
||||
}, [nodes, selectedImageNodes]);
|
||||
|
||||
const buildContextPayload = useCallback(() => {
|
||||
const imageNodes = nodes.filter(
|
||||
(node) =>
|
||||
node.type === "image" &&
|
||||
typeof (node.data as any)?.src === "string" &&
|
||||
String((node.data as any).src).trim().length > 0,
|
||||
);
|
||||
|
||||
if (imageNodes.length === 0) return undefined;
|
||||
|
||||
const MAX_CONTEXT_IMAGES = 12;
|
||||
const selectedIds = new Set(selectedImageNodes.map((node) => node.id));
|
||||
const prioritizedNodes =
|
||||
selectedImageNodes.length > 0
|
||||
? [
|
||||
...selectedImageNodes,
|
||||
...imageNodes.filter((node) => !selectedIds.has(node.id)),
|
||||
]
|
||||
: imageNodes;
|
||||
const contextNodes = prioritizedNodes.slice(0, MAX_CONTEXT_IMAGES);
|
||||
const selectedAssetIds = Array.from(
|
||||
new Set(
|
||||
selectedImageNodes
|
||||
.map((node) => {
|
||||
const assetId = (node.data as any)?.assetId;
|
||||
return typeof assetId === "string" && assetId.trim() ? assetId.trim() : null;
|
||||
})
|
||||
.filter((value): value is string => Boolean(value)),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
source: "canvas" as const,
|
||||
projectId: rawChatId || undefined,
|
||||
selectedNodeIds: selectedImageNodes.map((node) => node.id),
|
||||
selectedAssetIds,
|
||||
focusPolicy: selectedImageNodes.length > 0 ? ("selected" as const) : ("auto" as const),
|
||||
canvasImageNodes: contextNodes.map((node) => ({
|
||||
nodeId: node.id,
|
||||
assetId:
|
||||
typeof (node.data as any)?.assetId === "string" && (node.data as any).assetId.trim()
|
||||
? String((node.data as any).assetId).trim()
|
||||
: undefined,
|
||||
url: String((node.data as any)?.src || ""),
|
||||
title:
|
||||
typeof (node.data as any)?.title === "string"
|
||||
? String((node.data as any).title).slice(0, 256)
|
||||
: undefined,
|
||||
prompt:
|
||||
typeof (node.data as any)?.prompt === "string"
|
||||
? String((node.data as any).prompt).slice(0, 2000)
|
||||
: undefined,
|
||||
x: Number(node.position.x || 0),
|
||||
y: Number(node.position.y || 0),
|
||||
width: Number(node.size?.width || 0) || undefined,
|
||||
height: Number(node.size?.height || 0) || undefined,
|
||||
isSelected: selectedIds.has(node.id),
|
||||
})),
|
||||
};
|
||||
}, [nodes, rawChatId, selectedImageNodes]);
|
||||
|
||||
const updateAttachmentAtVideoSlot = useCallback(
|
||||
(slotIndex: number, attachment: PickedObjectAttachment | null) => {
|
||||
const slotCount = Math.max(videoFrameSlots.length, 2);
|
||||
@ -6012,6 +6076,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
sendMessage,
|
||||
clearSelection,
|
||||
buildAutoContext,
|
||||
buildContextPayload,
|
||||
isVideoModel,
|
||||
videoParams,
|
||||
videoFrameSlots.length,
|
||||
|
||||
@ -2498,6 +2498,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
|
||||
{resizeAnnotatorOpen && data.src && (
|
||||
<ImageAnnotator
|
||||
key={`resize-${node.id}-${data.src}`}
|
||||
imageUrl={data.src}
|
||||
initialResizeMode
|
||||
confirmButtonText="发送到画布"
|
||||
|
||||
@ -108,10 +108,80 @@ export function ImageAnnotator({
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const objectUrlRef = useRef<string | null>(null);
|
||||
const toCanvasLoadUrl = useCallback((url: string) => {
|
||||
return buildImageProxyUrl(url);
|
||||
}, []);
|
||||
|
||||
const revokeObjectUrl = useCallback(() => {
|
||||
if (objectUrlRef.current) {
|
||||
URL.revokeObjectURL(objectUrlRef.current);
|
||||
objectUrlRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const createImageElement = useCallback((src: string, useCrossOrigin = true) => {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const img = new window.Image();
|
||||
if (useCrossOrigin) {
|
||||
img.crossOrigin = "anonymous";
|
||||
}
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => reject(new Error(`图片加载失败: ${src}`));
|
||||
img.src = src;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadImageFromSource = useCallback(async (sourceUrl: string) => {
|
||||
const normalizedSource = String(sourceUrl || "").trim();
|
||||
if (!normalizedSource) {
|
||||
throw new Error("图片地址为空");
|
||||
}
|
||||
|
||||
const directUrl = normalizedSource;
|
||||
const proxiedUrl = toCanvasLoadUrl(normalizedSource);
|
||||
const candidateUrls = Array.from(
|
||||
new Set([proxiedUrl, directUrl].filter((item): item is string => Boolean(item)))
|
||||
);
|
||||
|
||||
for (const candidateUrl of candidateUrls) {
|
||||
try {
|
||||
const shouldUseCrossOrigin = !candidateUrl.startsWith("data:") && !candidateUrl.startsWith("blob:");
|
||||
const imageElement = await createImageElement(candidateUrl, shouldUseCrossOrigin);
|
||||
revokeObjectUrl();
|
||||
return imageElement;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (const candidateUrl of candidateUrls) {
|
||||
try {
|
||||
const response = await fetch(candidateUrl, { cache: "force-cache" });
|
||||
if (!response.ok) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const imageElement = await createImageElement(objectUrl, false);
|
||||
revokeObjectUrl();
|
||||
objectUrlRef.current = objectUrl;
|
||||
return imageElement;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("图片加载失败");
|
||||
}, [createImageElement, revokeObjectUrl, toCanvasLoadUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
revokeObjectUrl();
|
||||
};
|
||||
}, [revokeObjectUrl]);
|
||||
|
||||
// Handle open change
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
onOpenChange?.(newOpen);
|
||||
@ -160,18 +230,10 @@ export function ImageAnnotator({
|
||||
setProcessType(null);
|
||||
setActions([]);
|
||||
setCurrentAction(null);
|
||||
|
||||
const img = new window.Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = () => {
|
||||
setImage(img);
|
||||
setIsLoading(false);
|
||||
};
|
||||
img.onerror = () => {
|
||||
setIsLoading(false);
|
||||
toast.error("图片加载失败");
|
||||
};
|
||||
img.src = toCanvasLoadUrl(imageUrl);
|
||||
|
||||
const img = await loadImageFromSource(imageUrl);
|
||||
setImage(img);
|
||||
setIsLoading(false);
|
||||
toast.success("图片上传成功");
|
||||
} catch (error) {
|
||||
console.error("上传图片失败:", error);
|
||||
@ -179,7 +241,7 @@ export function ImageAnnotator({
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}, [loadImageFromSource]);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
@ -215,18 +277,10 @@ export function ImageAnnotator({
|
||||
setProcessType(null);
|
||||
setActions([]);
|
||||
setCurrentAction(null);
|
||||
|
||||
const img = new window.Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = () => {
|
||||
setImage(img);
|
||||
setIsLoading(false);
|
||||
};
|
||||
img.onerror = () => {
|
||||
setIsLoading(false);
|
||||
toast.error("图片加载失败");
|
||||
};
|
||||
img.src = toCanvasLoadUrl(imageUrl);
|
||||
|
||||
const img = await loadImageFromSource(imageUrl);
|
||||
setImage(img);
|
||||
setIsLoading(false);
|
||||
toast.success("图片上传成功");
|
||||
} catch (error) {
|
||||
console.error("上传图片失败:", error);
|
||||
@ -240,7 +294,7 @@ export function ImageAnnotator({
|
||||
|
||||
document.addEventListener('paste', handlePaste);
|
||||
return () => document.removeEventListener('paste', handlePaste);
|
||||
}, [mounted, originalImageUrl]);
|
||||
}, [loadImageFromSource, mounted, originalImageUrl]);
|
||||
|
||||
// 尺寸调整相关状态
|
||||
const [isResizeMode, setIsResizeMode] = useState(initialResizeMode);
|
||||
@ -362,19 +416,10 @@ export function ImageAnnotator({
|
||||
setProcessType(null);
|
||||
setActions([]);
|
||||
setCurrentAction(null);
|
||||
|
||||
// 触发图片加载逻辑
|
||||
const img = new window.Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = () => {
|
||||
setImage(img);
|
||||
setIsLoading(false);
|
||||
};
|
||||
img.onerror = () => {
|
||||
setIsLoading(false);
|
||||
toast.error("图片加载失败");
|
||||
};
|
||||
img.src = toCanvasLoadUrl(imageUrl);
|
||||
|
||||
const img = await loadImageFromSource(imageUrl);
|
||||
setImage(img);
|
||||
setIsLoading(false);
|
||||
toast.success("图片上传成功");
|
||||
} catch (error) {
|
||||
console.error("上传图片失败:", error);
|
||||
@ -390,29 +435,41 @@ export function ImageAnnotator({
|
||||
useEffect(() => {
|
||||
if (!imageUrl) return; // 如果没有初始图片,不执行加载逻辑
|
||||
|
||||
setIsLoading(true);
|
||||
setIsResizeMode(initialResizeMode);
|
||||
setOriginalImageUrl(imageUrl);
|
||||
setProcessedImageUrl(null);
|
||||
setProcessedImageBase64(null);
|
||||
setProcessType(null);
|
||||
const img = new window.Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = () => {
|
||||
setImage(img);
|
||||
setIsLoading(false);
|
||||
if (img) {
|
||||
let cancelled = false;
|
||||
|
||||
const initializeImage = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setIsResizeMode(initialResizeMode);
|
||||
setOriginalImageUrl(imageUrl);
|
||||
setProcessedImageUrl(null);
|
||||
setProcessedImageBase64(null);
|
||||
setProcessType(null);
|
||||
|
||||
const img = await loadImageFromSource(imageUrl);
|
||||
if (cancelled) return;
|
||||
|
||||
setImage(img);
|
||||
setIsLoading(false);
|
||||
setIsResizeMode(initialResizeMode);
|
||||
setResizeCanvasWidth(img.width);
|
||||
setResizeCanvasHeight(img.height);
|
||||
setSelectedAspectRatio(null);
|
||||
} catch (error) {
|
||||
if (cancelled) return;
|
||||
console.error("初始化图片加载失败:", error);
|
||||
setImage(null);
|
||||
setIsLoading(false);
|
||||
toast.error("当前图片加载失败,请重试");
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
initializeImage();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
img.src = toCanvasLoadUrl(imageUrl);
|
||||
}, [imageUrl, initialResizeMode, toCanvasLoadUrl]);
|
||||
}, [imageUrl, initialResizeMode, loadImageFromSource]);
|
||||
|
||||
// 初始化 canvas - 适配弹窗大小,固定编辑器大小,图片缩小显示
|
||||
useEffect(() => {
|
||||
|
||||
309
lib/ai/context-memory/media-input.ts
Normal file
309
lib/ai/context-memory/media-input.ts
Normal file
@ -0,0 +1,309 @@
|
||||
import "server-only";
|
||||
|
||||
import { extractImageUrlsFromText, inferOriginalImageUrl } from "@/lib/image-url";
|
||||
import type { DBMessage } from "@/lib/db/schema";
|
||||
import type {
|
||||
CanvasImageNodeContext,
|
||||
ResolveChatMediaInputParams,
|
||||
ResolvedChatMediaInput,
|
||||
ResolvedImageRef,
|
||||
} from "@/lib/ai/context-memory/types";
|
||||
|
||||
function normalizeMediaUrl(url: string): string {
|
||||
const raw = String(url || "").trim();
|
||||
if (!raw) return "";
|
||||
|
||||
if (/^data:image\/[a-z0-9+.-]+;base64,/i.test(raw)) {
|
||||
return raw.replace(/\s+/g, "");
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(raw);
|
||||
return `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "").toLowerCase();
|
||||
} catch {
|
||||
return raw
|
||||
.split("#")[0]
|
||||
.split("?")[0]
|
||||
.replace(/\/+$/, "")
|
||||
.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeImageInputRef(rawValue: unknown, requestOrigin?: string): string | null {
|
||||
if (typeof rawValue !== "string") return null;
|
||||
const trimmed = rawValue.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
if (/^data:image\/[a-z0-9+.-]+;base64,/i.test(trimmed)) {
|
||||
return trimmed.replace(/\s+/g, "");
|
||||
}
|
||||
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
return inferOriginalImageUrl(trimmed);
|
||||
}
|
||||
|
||||
if (requestOrigin && trimmed.startsWith("/")) {
|
||||
try {
|
||||
return new URL(trimmed, requestOrigin).toString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectImageInputsFromMessageParts(
|
||||
parts: Array<Record<string, any>> | undefined,
|
||||
requestOrigin?: string,
|
||||
): string[] {
|
||||
if (!Array.isArray(parts) || parts.length === 0) return [];
|
||||
|
||||
const result: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const pushInput = (rawValue: unknown) => {
|
||||
const normalized = normalizeImageInputRef(rawValue, requestOrigin);
|
||||
if (!normalized) return;
|
||||
const key = normalizeMediaUrl(normalized);
|
||||
if (!key || seen.has(key)) return;
|
||||
seen.add(key);
|
||||
result.push(normalized);
|
||||
};
|
||||
|
||||
for (const part of parts) {
|
||||
if (!part) continue;
|
||||
|
||||
if (part.type === "file") {
|
||||
pushInput(part.url);
|
||||
pushInput(part.data);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.type === "image") {
|
||||
pushInput(part.url);
|
||||
pushInput(part.image);
|
||||
pushInput(part.data);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.type === "image_url") {
|
||||
pushInput(part.image_url?.url);
|
||||
pushInput(part.url);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.type === "text" && typeof part.text === "string") {
|
||||
for (const parsedUrl of extractImageUrlsFromText(part.text)) {
|
||||
pushInput(parsedUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function dedupeRefs(refs: ResolvedImageRef[], maxImages: number): ResolvedImageRef[] {
|
||||
const seen = new Set<string>();
|
||||
const result: ResolvedImageRef[] = [];
|
||||
|
||||
for (const ref of refs) {
|
||||
const key = ref.normalizedUrl || normalizeMediaUrl(ref.url);
|
||||
if (!key || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
result.push({ ...ref, normalizedUrl: key });
|
||||
if (result.length >= maxImages) break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function toCanvasImageRefs(nodes: CanvasImageNodeContext[] | undefined, maxImages: number) {
|
||||
if (!Array.isArray(nodes) || nodes.length === 0) return [] as ResolvedImageRef[];
|
||||
|
||||
return dedupeRefs(
|
||||
nodes
|
||||
.filter((node) => typeof node?.url === "string" && node.url.trim())
|
||||
.map((node) => ({
|
||||
url: inferOriginalImageUrl(node.url),
|
||||
normalizedUrl: normalizeMediaUrl(inferOriginalImageUrl(node.url)),
|
||||
assetId: typeof node.assetId === "string" ? node.assetId.trim() || undefined : undefined,
|
||||
nodeId: node.nodeId,
|
||||
title: node.title,
|
||||
prompt: node.prompt,
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
isSelected: Boolean(node.isSelected),
|
||||
source: node.isSelected ? "canvas_selection" : "canvas_context",
|
||||
role: node.isSelected ? "selected" : "reference",
|
||||
})),
|
||||
maxImages,
|
||||
);
|
||||
}
|
||||
|
||||
function extractHistoryImageRefs(params: {
|
||||
historyMessages: DBMessage[];
|
||||
requestOrigin?: string;
|
||||
maxImages: number;
|
||||
}) {
|
||||
const latestAssistant: ResolvedImageRef[] = [];
|
||||
const latestUser: ResolvedImageRef[] = [];
|
||||
const seenAssistant = new Set<string>();
|
||||
const seenUser = new Set<string>();
|
||||
|
||||
const pushRef = (
|
||||
bucket: ResolvedImageRef[],
|
||||
seen: Set<string>,
|
||||
url: string,
|
||||
options: Pick<ResolvedImageRef, "source" | "role" | "messageId">,
|
||||
) => {
|
||||
const normalizedUrl = normalizeMediaUrl(url);
|
||||
if (!normalizedUrl || seen.has(normalizedUrl) || bucket.length >= params.maxImages) return;
|
||||
seen.add(normalizedUrl);
|
||||
bucket.push({
|
||||
url,
|
||||
normalizedUrl,
|
||||
source: options.source,
|
||||
role: options.role,
|
||||
messageId: options.messageId,
|
||||
});
|
||||
};
|
||||
|
||||
for (let index = params.historyMessages.length - 1; index >= 0; index -= 1) {
|
||||
const historyMessage = params.historyMessages[index];
|
||||
const parts = historyMessage.parts as Array<Record<string, any>> | undefined;
|
||||
if (!Array.isArray(parts) || parts.length === 0) continue;
|
||||
|
||||
const urls = collectImageInputsFromMessageParts(parts, params.requestOrigin);
|
||||
if (!urls.length) continue;
|
||||
|
||||
if (historyMessage.role === "assistant" && latestAssistant.length < params.maxImages) {
|
||||
for (const url of urls) {
|
||||
pushRef(latestAssistant, seenAssistant, url, {
|
||||
source: "history_assistant_result",
|
||||
role: "history_result",
|
||||
messageId: historyMessage.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (historyMessage.role === "user" && latestUser.length < params.maxImages) {
|
||||
for (const url of urls) {
|
||||
pushRef(latestUser, seenUser, url, {
|
||||
source: "history_user_upload",
|
||||
role: "reference",
|
||||
messageId: historyMessage.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (latestAssistant.length >= params.maxImages && latestUser.length >= params.maxImages) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
latestAssistant,
|
||||
latestUser,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveChatMediaInput(
|
||||
params: ResolveChatMediaInputParams,
|
||||
): ResolvedChatMediaInput {
|
||||
const maxImages = Math.max(1, Math.min(12, Math.round(params.maxImages || 4)));
|
||||
const currentMessageImageRefs = dedupeRefs(
|
||||
collectImageInputsFromMessageParts(params.messageParts, params.requestOrigin).map((url) => ({
|
||||
url,
|
||||
normalizedUrl: normalizeMediaUrl(url),
|
||||
source: "current_message",
|
||||
role: "upload",
|
||||
})),
|
||||
maxImages,
|
||||
);
|
||||
|
||||
const canvasImageRefs = toCanvasImageRefs(params.contextPayload?.canvasImageNodes, maxImages);
|
||||
const selectedNodeIds = new Set(params.contextPayload?.selectedNodeIds || []);
|
||||
const selectedImageRefs = dedupeRefs(
|
||||
canvasImageRefs.filter((ref) => ref.isSelected || (ref.nodeId && selectedNodeIds.has(ref.nodeId))),
|
||||
maxImages,
|
||||
);
|
||||
const history = extractHistoryImageRefs({
|
||||
historyMessages: params.historyMessages,
|
||||
requestOrigin: params.requestOrigin,
|
||||
maxImages,
|
||||
});
|
||||
|
||||
const resolvedImageRefs =
|
||||
currentMessageImageRefs.length > 0
|
||||
? currentMessageImageRefs
|
||||
: selectedImageRefs.length > 0
|
||||
? selectedImageRefs
|
||||
: history.latestAssistant.length > 0
|
||||
? history.latestAssistant
|
||||
: history.latestUser;
|
||||
|
||||
const imageRefs = dedupeRefs(
|
||||
[
|
||||
...currentMessageImageRefs,
|
||||
...selectedImageRefs,
|
||||
...canvasImageRefs,
|
||||
...history.latestAssistant,
|
||||
...history.latestUser,
|
||||
],
|
||||
maxImages * 4,
|
||||
);
|
||||
|
||||
const selectedAssetIds = Array.from(
|
||||
new Set(
|
||||
selectedImageRefs
|
||||
.map((ref) => ref.assetId)
|
||||
.filter((value): value is string => typeof value === "string" && value.trim().length > 0),
|
||||
),
|
||||
);
|
||||
const uploadedAssetIds = Array.from(
|
||||
new Set(
|
||||
currentMessageImageRefs
|
||||
.map((ref) => ref.assetId)
|
||||
.filter((value): value is string => typeof value === "string" && value.trim().length > 0),
|
||||
),
|
||||
);
|
||||
const candidateAssetIds = Array.from(
|
||||
new Set(
|
||||
imageRefs
|
||||
.map((ref) => ref.assetId)
|
||||
.filter((value): value is string => typeof value === "string" && value.trim().length > 0),
|
||||
),
|
||||
);
|
||||
|
||||
const source =
|
||||
currentMessageImageRefs.length > 0
|
||||
? "current_message"
|
||||
: selectedImageRefs.length > 0
|
||||
? "canvas_selection"
|
||||
: resolvedImageRefs.length > 0
|
||||
? "history"
|
||||
: "none";
|
||||
|
||||
return {
|
||||
imageRefs,
|
||||
currentMessageImageRefs,
|
||||
selectedImageRefs,
|
||||
canvasImageRefs,
|
||||
historyUserImageRefs: history.latestUser,
|
||||
historyAssistantImageRefs: history.latestAssistant,
|
||||
resolvedImageRefs,
|
||||
currentMessageImageUrls: currentMessageImageRefs.map((ref) => ref.url),
|
||||
selectedImageUrls: selectedImageRefs.map((ref) => ref.url),
|
||||
historyUserImageUrls: history.latestUser.map((ref) => ref.url),
|
||||
historyAssistantImageUrls: history.latestAssistant.map((ref) => ref.url),
|
||||
resolvedImageUrls: resolvedImageRefs.map((ref) => ref.url),
|
||||
selectedAssetIds,
|
||||
candidateAssetIds,
|
||||
uploadedAssetIds,
|
||||
hasImage: resolvedImageRefs.length > 0,
|
||||
source,
|
||||
fromHistory: source === "history" || source === "canvas_selection",
|
||||
};
|
||||
}
|
||||
378
lib/ai/context-memory/target-resolution.ts
Normal file
378
lib/ai/context-memory/target-resolution.ts
Normal file
@ -0,0 +1,378 @@
|
||||
import "server-only";
|
||||
|
||||
import type {
|
||||
ResolveTargetAssetsParams,
|
||||
ResolvedImageRef,
|
||||
ResolvedTargetAssets,
|
||||
} from "@/lib/ai/context-memory/types";
|
||||
|
||||
const ORIGINAL_IMAGE_REGEX =
|
||||
/(原图|上传图|参考图|最初那张|第一张图|素材图|源图|原始图片|原始那张)/i;
|
||||
const LATEST_RESULT_REGEX =
|
||||
/(上一张|上一轮|刚生成|最新生成|这个结果|该结果|按这个结果|继续在这张|基于这张结果|延续上一次结果|上一版|刚才那张结果)/i;
|
||||
const SELECTION_REGEX = /(选中的|当前这张|我选的|这几张|这些图|选中图)/i;
|
||||
const ACTIONABLE_EDIT_REGEX =
|
||||
/(改|调整|优化|换|替换|去掉|抠|重绘|增强|修|处理|按照|按|保留|统一|色差|背景|清晰|风格|亮一点|暗一点|白底|透明底)/i;
|
||||
|
||||
function normalizeStringArray(values?: string[] | null): string[] {
|
||||
if (!Array.isArray(values)) return [];
|
||||
return Array.from(
|
||||
new Set(
|
||||
values
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function chineseNumberToInt(value: string): number | null {
|
||||
const normalized = value.trim();
|
||||
if (!normalized) return null;
|
||||
if (/^\d+$/.test(normalized)) return Number.parseInt(normalized, 10);
|
||||
|
||||
const map: Record<string, number> = {
|
||||
一: 1,
|
||||
二: 2,
|
||||
两: 2,
|
||||
三: 3,
|
||||
四: 4,
|
||||
五: 5,
|
||||
六: 6,
|
||||
七: 7,
|
||||
八: 8,
|
||||
九: 9,
|
||||
十: 10,
|
||||
};
|
||||
|
||||
if (normalized === "十") return 10;
|
||||
if (normalized.length === 1) return map[normalized] ?? null;
|
||||
if (normalized.startsWith("十")) {
|
||||
return 10 + (map[normalized.slice(1)] ?? 0);
|
||||
}
|
||||
if (normalized.endsWith("十")) {
|
||||
return (map[normalized.slice(0, 1)] ?? 0) * 10;
|
||||
}
|
||||
if (normalized.includes("十")) {
|
||||
const [head, tail] = normalized.split("十");
|
||||
return (map[head] ?? 0) * 10 + (map[tail] ?? 0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function sortRefsForCanvasOrdering(refs: ResolvedImageRef[]) {
|
||||
return [...refs].sort((left, right) => {
|
||||
const leftY = Number(left.y ?? 0);
|
||||
const rightY = Number(right.y ?? 0);
|
||||
if (Math.abs(leftY - rightY) > 12) {
|
||||
return leftY - rightY;
|
||||
}
|
||||
return Number(left.x ?? 0) - Number(right.x ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
function getPositionLabel(ref: ResolvedImageRef, refs: ResolvedImageRef[]): string {
|
||||
if (refs.length <= 1) return "中间";
|
||||
const xs = refs.map((item) => Number(item.x ?? 0));
|
||||
const ys = refs.map((item) => Number(item.y ?? 0));
|
||||
const minX = Math.min(...xs);
|
||||
const maxX = Math.max(...xs);
|
||||
const minY = Math.min(...ys);
|
||||
const maxY = Math.max(...ys);
|
||||
const spanX = Math.max(1, maxX - minX);
|
||||
const spanY = Math.max(1, maxY - minY);
|
||||
const relX = (Number(ref.x ?? 0) - minX) / spanX;
|
||||
const relY = (Number(ref.y ?? 0) - minY) / spanY;
|
||||
|
||||
const horizontal = relX <= 0.33 ? "左" : relX >= 0.66 ? "右" : "中";
|
||||
const vertical = relY <= 0.33 ? "上" : relY >= 0.66 ? "下" : "中";
|
||||
|
||||
if (horizontal === "中" && vertical === "中") return "中间";
|
||||
return `${vertical}${horizontal}`;
|
||||
}
|
||||
|
||||
function pickByOrdinal(text: string, refs: ResolvedImageRef[]): ResolvedImageRef[] {
|
||||
const matched = text.match(/第\s*([一二三四五六七八九十两\d]+)\s*(?:张|个|幅|图)/i);
|
||||
const numeric = matched ? chineseNumberToInt(matched[1]) : null;
|
||||
if (!numeric || numeric <= 0) return [];
|
||||
const ordered = sortRefsForCanvasOrdering(refs);
|
||||
return ordered[numeric - 1] ? [ordered[numeric - 1]] : [];
|
||||
}
|
||||
|
||||
function pickBySpatialHint(text: string, refs: ResolvedImageRef[]): ResolvedImageRef[] {
|
||||
if (!refs.length) return [];
|
||||
|
||||
const normalizedText = text.replace(/\s+/g, "");
|
||||
const hasLeft = normalizedText.includes("左");
|
||||
const hasRight = normalizedText.includes("右");
|
||||
const hasTop = normalizedText.includes("上");
|
||||
const hasBottom = normalizedText.includes("下");
|
||||
const hasCenter = normalizedText.includes("中间") || normalizedText.includes("中");
|
||||
|
||||
if (!(hasLeft || hasRight || hasTop || hasBottom || hasCenter)) return [];
|
||||
|
||||
const withLabels = refs.map((ref) => ({
|
||||
ref,
|
||||
label: getPositionLabel(ref, refs),
|
||||
}));
|
||||
|
||||
return withLabels
|
||||
.filter(({ label }) => {
|
||||
if (hasLeft && !label.includes("左")) return false;
|
||||
if (hasRight && !label.includes("右")) return false;
|
||||
if (hasTop && !label.includes("上")) return false;
|
||||
if (hasBottom && !label.includes("下")) return false;
|
||||
if (hasCenter && !(label.includes("中") || label === "中间")) return false;
|
||||
return true;
|
||||
})
|
||||
.map(({ ref }) => ref);
|
||||
}
|
||||
|
||||
function buildResolvedTargetAssets(params: {
|
||||
targetRefs: ResolvedImageRef[];
|
||||
candidateRefs: ResolvedImageRef[];
|
||||
resolutionMode: ResolvedTargetAssets["resolutionMode"];
|
||||
confidence: number;
|
||||
needsClarification: boolean;
|
||||
reason: string;
|
||||
fromHistory: boolean;
|
||||
}): ResolvedTargetAssets {
|
||||
const targetRefs = params.targetRefs;
|
||||
const candidateRefs = params.candidateRefs;
|
||||
return {
|
||||
targetRefs,
|
||||
candidateRefs,
|
||||
targetImageUrls: targetRefs.map((ref) => ref.url),
|
||||
targetAssetIds: normalizeStringArray(targetRefs.map((ref) => ref.assetId || "")),
|
||||
candidateImageUrls: candidateRefs.map((ref) => ref.url),
|
||||
candidateAssetIds: normalizeStringArray(candidateRefs.map((ref) => ref.assetId || "")),
|
||||
resolutionMode: params.resolutionMode,
|
||||
confidence: Math.max(0, Math.min(100, Math.round(params.confidence))),
|
||||
needsClarification: params.needsClarification,
|
||||
reason: params.reason,
|
||||
fromHistory: params.fromHistory,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveTargetAssets(
|
||||
params: ResolveTargetAssetsParams,
|
||||
): ResolvedTargetAssets {
|
||||
const text = String(params.text || "").trim();
|
||||
const mediaInput = params.mediaInput;
|
||||
const canvasRefs = mediaInput.canvasImageRefs;
|
||||
const selectedRefs = mediaInput.selectedImageRefs;
|
||||
const currentRefs = mediaInput.currentMessageImageRefs;
|
||||
const assistantRefs = mediaInput.historyAssistantImageRefs;
|
||||
const userRefs = mediaInput.historyUserImageRefs;
|
||||
const candidateRefs = mediaInput.imageRefs;
|
||||
const workingState = params.workingState;
|
||||
const focusPolicy = params.contextPayload?.focusPolicy || "auto";
|
||||
|
||||
if (focusPolicy === "original" && userRefs.length > 0) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: userRefs,
|
||||
candidateRefs,
|
||||
resolutionMode: "text_reference",
|
||||
confidence: 96,
|
||||
needsClarification: false,
|
||||
reason: "前端上下文指定优先使用原图",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (focusPolicy === "latest_result" && assistantRefs.length > 0) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: assistantRefs,
|
||||
candidateRefs,
|
||||
resolutionMode: "latest_result",
|
||||
confidence: 96,
|
||||
needsClarification: false,
|
||||
reason: "前端上下文指定优先使用上一轮结果",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (focusPolicy === "selected" && selectedRefs.length > 0) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: selectedRefs,
|
||||
candidateRefs,
|
||||
resolutionMode: "explicit_selection",
|
||||
confidence: 97,
|
||||
needsClarification: false,
|
||||
reason: "前端已明确选中目标图片",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (ORIGINAL_IMAGE_REGEX.test(text) && userRefs.length > 0) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: userRefs,
|
||||
candidateRefs,
|
||||
resolutionMode: "text_reference",
|
||||
confidence: 94,
|
||||
needsClarification: false,
|
||||
reason: "用户文本明确指向原图/上传图",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (LATEST_RESULT_REGEX.test(text) && assistantRefs.length > 0) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: assistantRefs,
|
||||
candidateRefs,
|
||||
resolutionMode: "latest_result",
|
||||
confidence: 94,
|
||||
needsClarification: false,
|
||||
reason: "用户文本明确指向上一轮结果",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
const ordinalRefs = pickByOrdinal(text, canvasRefs);
|
||||
if (ordinalRefs.length > 0) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: ordinalRefs,
|
||||
candidateRefs,
|
||||
resolutionMode: "text_reference",
|
||||
confidence: 88,
|
||||
needsClarification: false,
|
||||
reason: "根据用户文本中的序号定位到画布图片",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
const spatialRefs = pickBySpatialHint(text, canvasRefs);
|
||||
if (spatialRefs.length > 0) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: spatialRefs,
|
||||
candidateRefs,
|
||||
resolutionMode: "text_reference",
|
||||
confidence: spatialRefs.length === 1 ? 90 : 76,
|
||||
needsClarification: false,
|
||||
reason: "根据用户文本中的方位指向定位到画布图片",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (currentRefs.length > 0) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: currentRefs,
|
||||
candidateRefs,
|
||||
resolutionMode: "current_upload",
|
||||
confidence: 93,
|
||||
needsClarification: false,
|
||||
reason: "本轮消息包含图片输入,默认优先处理当前上传图片",
|
||||
fromHistory: false,
|
||||
});
|
||||
}
|
||||
|
||||
if ((SELECTION_REGEX.test(text) || focusPolicy === "selected") && selectedRefs.length > 0) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: selectedRefs,
|
||||
candidateRefs,
|
||||
resolutionMode: "explicit_selection",
|
||||
confidence: 92,
|
||||
needsClarification: false,
|
||||
reason: "用户文本或上下文明确要求使用当前选中的图片",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedRefs.length > 0) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: selectedRefs,
|
||||
candidateRefs,
|
||||
resolutionMode: "explicit_selection",
|
||||
confidence: 86,
|
||||
needsClarification: false,
|
||||
reason: "当前存在画布选中图片,默认优先继续处理选中目标",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
const workingStateResolvedUrls = normalizeStringArray(
|
||||
workingState?.lastResolvedTargetImageUrls,
|
||||
);
|
||||
if (workingStateResolvedUrls.length > 0) {
|
||||
const refs = workingStateResolvedUrls.map((url) => ({
|
||||
url,
|
||||
normalizedUrl: url,
|
||||
source: "working_memory" as const,
|
||||
role: "working_memory" as const,
|
||||
}));
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: refs,
|
||||
candidateRefs,
|
||||
resolutionMode: "working_memory",
|
||||
confidence: 82,
|
||||
needsClarification: false,
|
||||
reason: "使用会话工作记忆中的上一轮已解析目标图",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
const workingStateGeneratedUrls = normalizeStringArray(workingState?.lastGeneratedImageUrls);
|
||||
if (workingStateGeneratedUrls.length > 0) {
|
||||
const refs = workingStateGeneratedUrls.map((url) => ({
|
||||
url,
|
||||
normalizedUrl: url,
|
||||
source: "working_memory" as const,
|
||||
role: "working_memory" as const,
|
||||
}));
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: refs,
|
||||
candidateRefs,
|
||||
resolutionMode: "working_memory",
|
||||
confidence: 78,
|
||||
needsClarification: false,
|
||||
reason: "使用会话工作记忆中的最近生成结果",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (assistantRefs.length > 0) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: assistantRefs,
|
||||
candidateRefs,
|
||||
resolutionMode: "latest_result",
|
||||
confidence: 74,
|
||||
needsClarification: false,
|
||||
reason: "未显式指定时,默认续接最近一轮生成结果",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (userRefs.length > 0) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: userRefs,
|
||||
candidateRefs,
|
||||
resolutionMode: "working_memory",
|
||||
confidence: 68,
|
||||
needsClarification: false,
|
||||
reason: "回退使用最近一次用户上传图片",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (ACTIONABLE_EDIT_REGEX.test(text)) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: [],
|
||||
candidateRefs,
|
||||
resolutionMode: "clarification",
|
||||
confidence: 0,
|
||||
needsClarification: true,
|
||||
reason: "用户表达了编辑意图,但当前没有可用图片目标",
|
||||
fromHistory: false,
|
||||
});
|
||||
}
|
||||
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: [],
|
||||
candidateRefs,
|
||||
resolutionMode: "none",
|
||||
confidence: 0,
|
||||
needsClarification: false,
|
||||
reason: "当前请求没有可定位的图片目标",
|
||||
fromHistory: false,
|
||||
});
|
||||
}
|
||||
119
lib/ai/context-memory/types.ts
Normal file
119
lib/ai/context-memory/types.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import type { DBMessage } from "@/lib/db/schema";
|
||||
|
||||
export interface CanvasImageNodeContext {
|
||||
nodeId: string;
|
||||
assetId?: string;
|
||||
url: string;
|
||||
title?: string;
|
||||
prompt?: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export interface ContextPayload {
|
||||
source?: "chat" | "canvas" | "realtime-canvas";
|
||||
projectId?: string;
|
||||
selectedNodeIds?: string[];
|
||||
selectedAssetIds?: string[];
|
||||
focusPolicy?: "auto" | "selected" | "latest_result" | "original";
|
||||
canvasImageNodes?: CanvasImageNodeContext[];
|
||||
}
|
||||
|
||||
export interface ResolvedImageRef {
|
||||
url: string;
|
||||
normalizedUrl: string;
|
||||
assetId?: string;
|
||||
nodeId?: string;
|
||||
title?: string;
|
||||
prompt?: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
isSelected?: boolean;
|
||||
source:
|
||||
| "current_message"
|
||||
| "canvas_selection"
|
||||
| "canvas_context"
|
||||
| "history_user_upload"
|
||||
| "history_assistant_result"
|
||||
| "working_memory";
|
||||
role: "upload" | "selected" | "reference" | "history_result" | "working_memory";
|
||||
messageId?: string;
|
||||
}
|
||||
|
||||
export interface ResolvedChatMediaInput {
|
||||
imageRefs: ResolvedImageRef[];
|
||||
currentMessageImageRefs: ResolvedImageRef[];
|
||||
selectedImageRefs: ResolvedImageRef[];
|
||||
canvasImageRefs: ResolvedImageRef[];
|
||||
historyUserImageRefs: ResolvedImageRef[];
|
||||
historyAssistantImageRefs: ResolvedImageRef[];
|
||||
resolvedImageRefs: ResolvedImageRef[];
|
||||
currentMessageImageUrls: string[];
|
||||
selectedImageUrls: string[];
|
||||
historyUserImageUrls: string[];
|
||||
historyAssistantImageUrls: string[];
|
||||
resolvedImageUrls: string[];
|
||||
selectedAssetIds: string[];
|
||||
candidateAssetIds: string[];
|
||||
uploadedAssetIds: string[];
|
||||
hasImage: boolean;
|
||||
source: "current_message" | "canvas_selection" | "history" | "none";
|
||||
fromHistory: boolean;
|
||||
}
|
||||
|
||||
export interface WorkingStateImageMemory {
|
||||
currentFocusAssetIds?: string[] | null;
|
||||
currentFocusImageUrls?: string[] | null;
|
||||
selectedAssetIds?: string[] | null;
|
||||
selectedImageUrls?: string[] | null;
|
||||
lastUploadedAssetIds?: string[] | null;
|
||||
lastUploadedImageUrls?: string[] | null;
|
||||
lastGeneratedAssetIds?: string[] | null;
|
||||
lastGeneratedImageUrls?: string[] | null;
|
||||
lastReferencedAssetIds?: string[] | null;
|
||||
lastReferencedImageUrls?: string[] | null;
|
||||
lastResolvedTargetAssetIds?: string[] | null;
|
||||
lastResolvedTargetImageUrls?: string[] | null;
|
||||
activeCanvasNodeIds?: string[] | null;
|
||||
}
|
||||
|
||||
export interface ResolvedTargetAssets {
|
||||
targetRefs: ResolvedImageRef[];
|
||||
candidateRefs: ResolvedImageRef[];
|
||||
targetImageUrls: string[];
|
||||
targetAssetIds: string[];
|
||||
candidateImageUrls: string[];
|
||||
candidateAssetIds: string[];
|
||||
resolutionMode:
|
||||
| "explicit_selection"
|
||||
| "current_upload"
|
||||
| "text_reference"
|
||||
| "working_memory"
|
||||
| "latest_result"
|
||||
| "clarification"
|
||||
| "none";
|
||||
confidence: number;
|
||||
needsClarification: boolean;
|
||||
reason: string;
|
||||
fromHistory: boolean;
|
||||
}
|
||||
|
||||
export interface ResolveChatMediaInputParams {
|
||||
messageParts: Array<Record<string, any>> | undefined;
|
||||
historyMessages: DBMessage[];
|
||||
contextPayload?: ContextPayload;
|
||||
requestOrigin?: string;
|
||||
maxImages?: number;
|
||||
}
|
||||
|
||||
export interface ResolveTargetAssetsParams {
|
||||
text: string;
|
||||
mediaInput: ResolvedChatMediaInput;
|
||||
contextPayload?: ContextPayload;
|
||||
workingState?: WorkingStateImageMemory | null;
|
||||
}
|
||||
101
lib/db/migrations/0050_add_context_memory_tables.sql
Normal file
101
lib/db/migrations/0050_add_context_memory_tables.sql
Normal file
@ -0,0 +1,101 @@
|
||||
CREATE TABLE IF NOT EXISTS "ConversationWorkingState" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"userId" uuid NOT NULL,
|
||||
"chatId" uuid NOT NULL,
|
||||
"projectId" uuid,
|
||||
"currentIntent" varchar(64),
|
||||
"currentSubIntent" varchar(64),
|
||||
"currentFocusAssetIds" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"currentFocusImageUrls" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"selectedAssetIds" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"selectedImageUrls" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"lastUploadedAssetIds" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"lastUploadedImageUrls" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"lastGeneratedAssetIds" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"lastGeneratedImageUrls" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"lastReferencedAssetIds" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"lastReferencedImageUrls" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"lastResolvedTargetAssetIds" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"lastResolvedTargetImageUrls" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"activeCanvasNodeIds" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"lastCanvasSnapshot" jsonb,
|
||||
"lastTurnSummary" jsonb,
|
||||
"lastMessageId" uuid,
|
||||
"turnCount" integer DEFAULT 0 NOT NULL,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "ConversationWorkingState_chatId_unique" UNIQUE("chatId")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "MessageTargetResolution" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"userId" uuid NOT NULL,
|
||||
"chatId" uuid NOT NULL,
|
||||
"messageId" uuid,
|
||||
"projectId" uuid,
|
||||
"rawText" text NOT NULL,
|
||||
"uploadedAssetIds" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"uploadedImageUrls" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"selectedAssetIds" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"selectedImageUrls" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"candidateAssetIds" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"candidateImageUrls" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"resolvedAssetIds" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"resolvedImageUrls" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"resolutionMode" varchar(64) NOT NULL,
|
||||
"confidence" integer DEFAULT 0 NOT NULL,
|
||||
"needsClarification" boolean DEFAULT false NOT NULL,
|
||||
"reasoning" jsonb,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ConversationWorkingState" ADD CONSTRAINT "ConversationWorkingState_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ConversationWorkingState" ADD CONSTRAINT "ConversationWorkingState_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ConversationWorkingState" ADD CONSTRAINT "ConversationWorkingState_projectId_CanvasProject_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."CanvasProject"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "MessageTargetResolution" ADD CONSTRAINT "MessageTargetResolution_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "MessageTargetResolution" ADD CONSTRAINT "MessageTargetResolution_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "MessageTargetResolution" ADD CONSTRAINT "MessageTargetResolution_messageId_Message_v2_id_fk" FOREIGN KEY ("messageId") REFERENCES "public"."Message_v2"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "MessageTargetResolution" ADD CONSTRAINT "MessageTargetResolution_projectId_CanvasProject_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."CanvasProject"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "ConversationWorkingState_user_updated_idx"
|
||||
ON "ConversationWorkingState"("userId", "updatedAt");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "MessageTargetResolution_chat_created_idx"
|
||||
ON "MessageTargetResolution"("chatId", "createdAt");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "MessageTargetResolution_message_idx"
|
||||
ON "MessageTargetResolution"("messageId");
|
||||
@ -253,6 +253,13 @@
|
||||
"when": 1772766180000,
|
||||
"tag": "0049_add_model3d_assets",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 36,
|
||||
"version": "7",
|
||||
"when": 1772788800000,
|
||||
"tag": "0050_add_context_memory_tables",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -52,6 +52,10 @@ import {
|
||||
pointsLedger,
|
||||
tutorialVideos,
|
||||
messageDeprecated,
|
||||
conversationWorkingState,
|
||||
type ConversationWorkingState,
|
||||
messageTargetResolution,
|
||||
type MessageTargetResolution,
|
||||
} from "./schema";
|
||||
import { generateHashedPassword } from "./utils";
|
||||
import {
|
||||
@ -666,6 +670,245 @@ export async function updateChatLastContextById({
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStringArray(value?: string[] | null): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const deduped = new Set<string>();
|
||||
for (const item of value) {
|
||||
if (typeof item !== "string") continue;
|
||||
const trimmed = item.trim();
|
||||
if (!trimmed) continue;
|
||||
deduped.add(trimmed);
|
||||
}
|
||||
return Array.from(deduped);
|
||||
}
|
||||
|
||||
export async function getConversationWorkingStateByChatId({
|
||||
chatId,
|
||||
}: {
|
||||
chatId: string;
|
||||
}): Promise<ConversationWorkingState | null> {
|
||||
try {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(conversationWorkingState)
|
||||
.where(eq(conversationWorkingState.chatId, chatId))
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
} catch (_error) {
|
||||
throw new ChatSDKError(
|
||||
"bad_request:database",
|
||||
"Failed to get conversation working state"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function upsertConversationWorkingState(params: {
|
||||
userId: string;
|
||||
chatId: string;
|
||||
projectId?: string | null;
|
||||
currentIntent?: string | null;
|
||||
currentSubIntent?: string | null;
|
||||
currentFocusAssetIds?: string[] | null;
|
||||
currentFocusImageUrls?: string[] | null;
|
||||
selectedAssetIds?: string[] | null;
|
||||
selectedImageUrls?: string[] | null;
|
||||
lastUploadedAssetIds?: string[] | null;
|
||||
lastUploadedImageUrls?: string[] | null;
|
||||
lastGeneratedAssetIds?: string[] | null;
|
||||
lastGeneratedImageUrls?: string[] | null;
|
||||
lastReferencedAssetIds?: string[] | null;
|
||||
lastReferencedImageUrls?: string[] | null;
|
||||
lastResolvedTargetAssetIds?: string[] | null;
|
||||
lastResolvedTargetImageUrls?: string[] | null;
|
||||
activeCanvasNodeIds?: string[] | null;
|
||||
lastCanvasSnapshot?: any;
|
||||
lastTurnSummary?: any;
|
||||
lastMessageId?: string | null;
|
||||
incrementTurnCount?: boolean;
|
||||
}): Promise<ConversationWorkingState | null> {
|
||||
try {
|
||||
const existing = await getConversationWorkingStateByChatId({
|
||||
chatId: params.chatId,
|
||||
});
|
||||
const now = new Date();
|
||||
|
||||
const values = {
|
||||
userId: params.userId,
|
||||
chatId: params.chatId,
|
||||
projectId:
|
||||
params.projectId !== undefined ? params.projectId : existing?.projectId ?? null,
|
||||
currentIntent:
|
||||
params.currentIntent !== undefined
|
||||
? params.currentIntent
|
||||
: existing?.currentIntent ?? null,
|
||||
currentSubIntent:
|
||||
params.currentSubIntent !== undefined
|
||||
? params.currentSubIntent
|
||||
: existing?.currentSubIntent ?? null,
|
||||
currentFocusAssetIds:
|
||||
params.currentFocusAssetIds !== undefined
|
||||
? normalizeStringArray(params.currentFocusAssetIds)
|
||||
: normalizeStringArray(existing?.currentFocusAssetIds),
|
||||
currentFocusImageUrls:
|
||||
params.currentFocusImageUrls !== undefined
|
||||
? normalizeStringArray(params.currentFocusImageUrls)
|
||||
: normalizeStringArray(existing?.currentFocusImageUrls),
|
||||
selectedAssetIds:
|
||||
params.selectedAssetIds !== undefined
|
||||
? normalizeStringArray(params.selectedAssetIds)
|
||||
: normalizeStringArray(existing?.selectedAssetIds),
|
||||
selectedImageUrls:
|
||||
params.selectedImageUrls !== undefined
|
||||
? normalizeStringArray(params.selectedImageUrls)
|
||||
: normalizeStringArray(existing?.selectedImageUrls),
|
||||
lastUploadedAssetIds:
|
||||
params.lastUploadedAssetIds !== undefined
|
||||
? normalizeStringArray(params.lastUploadedAssetIds)
|
||||
: normalizeStringArray(existing?.lastUploadedAssetIds),
|
||||
lastUploadedImageUrls:
|
||||
params.lastUploadedImageUrls !== undefined
|
||||
? normalizeStringArray(params.lastUploadedImageUrls)
|
||||
: normalizeStringArray(existing?.lastUploadedImageUrls),
|
||||
lastGeneratedAssetIds:
|
||||
params.lastGeneratedAssetIds !== undefined
|
||||
? normalizeStringArray(params.lastGeneratedAssetIds)
|
||||
: normalizeStringArray(existing?.lastGeneratedAssetIds),
|
||||
lastGeneratedImageUrls:
|
||||
params.lastGeneratedImageUrls !== undefined
|
||||
? normalizeStringArray(params.lastGeneratedImageUrls)
|
||||
: normalizeStringArray(existing?.lastGeneratedImageUrls),
|
||||
lastReferencedAssetIds:
|
||||
params.lastReferencedAssetIds !== undefined
|
||||
? normalizeStringArray(params.lastReferencedAssetIds)
|
||||
: normalizeStringArray(existing?.lastReferencedAssetIds),
|
||||
lastReferencedImageUrls:
|
||||
params.lastReferencedImageUrls !== undefined
|
||||
? normalizeStringArray(params.lastReferencedImageUrls)
|
||||
: normalizeStringArray(existing?.lastReferencedImageUrls),
|
||||
lastResolvedTargetAssetIds:
|
||||
params.lastResolvedTargetAssetIds !== undefined
|
||||
? normalizeStringArray(params.lastResolvedTargetAssetIds)
|
||||
: normalizeStringArray(existing?.lastResolvedTargetAssetIds),
|
||||
lastResolvedTargetImageUrls:
|
||||
params.lastResolvedTargetImageUrls !== undefined
|
||||
? normalizeStringArray(params.lastResolvedTargetImageUrls)
|
||||
: normalizeStringArray(existing?.lastResolvedTargetImageUrls),
|
||||
activeCanvasNodeIds:
|
||||
params.activeCanvasNodeIds !== undefined
|
||||
? normalizeStringArray(params.activeCanvasNodeIds)
|
||||
: normalizeStringArray(existing?.activeCanvasNodeIds),
|
||||
lastCanvasSnapshot:
|
||||
params.lastCanvasSnapshot !== undefined
|
||||
? params.lastCanvasSnapshot
|
||||
: existing?.lastCanvasSnapshot ?? null,
|
||||
lastTurnSummary:
|
||||
params.lastTurnSummary !== undefined
|
||||
? params.lastTurnSummary
|
||||
: existing?.lastTurnSummary ?? null,
|
||||
lastMessageId:
|
||||
params.lastMessageId !== undefined
|
||||
? params.lastMessageId
|
||||
: existing?.lastMessageId ?? null,
|
||||
turnCount:
|
||||
(existing?.turnCount ?? 0) + (params.incrementTurnCount ? 1 : 0),
|
||||
createdAt: existing?.createdAt ?? now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const [result] = await db
|
||||
.insert(conversationWorkingState)
|
||||
.values(values)
|
||||
.onConflictDoUpdate({
|
||||
target: conversationWorkingState.chatId,
|
||||
set: {
|
||||
userId: values.userId,
|
||||
projectId: values.projectId,
|
||||
currentIntent: values.currentIntent,
|
||||
currentSubIntent: values.currentSubIntent,
|
||||
currentFocusAssetIds: values.currentFocusAssetIds,
|
||||
currentFocusImageUrls: values.currentFocusImageUrls,
|
||||
selectedAssetIds: values.selectedAssetIds,
|
||||
selectedImageUrls: values.selectedImageUrls,
|
||||
lastUploadedAssetIds: values.lastUploadedAssetIds,
|
||||
lastUploadedImageUrls: values.lastUploadedImageUrls,
|
||||
lastGeneratedAssetIds: values.lastGeneratedAssetIds,
|
||||
lastGeneratedImageUrls: values.lastGeneratedImageUrls,
|
||||
lastReferencedAssetIds: values.lastReferencedAssetIds,
|
||||
lastReferencedImageUrls: values.lastReferencedImageUrls,
|
||||
lastResolvedTargetAssetIds: values.lastResolvedTargetAssetIds,
|
||||
lastResolvedTargetImageUrls: values.lastResolvedTargetImageUrls,
|
||||
activeCanvasNodeIds: values.activeCanvasNodeIds,
|
||||
lastCanvasSnapshot: values.lastCanvasSnapshot,
|
||||
lastTurnSummary: values.lastTurnSummary,
|
||||
lastMessageId: values.lastMessageId,
|
||||
turnCount: values.turnCount,
|
||||
updatedAt: values.updatedAt,
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
|
||||
return result ?? null;
|
||||
} catch (_error) {
|
||||
throw new ChatSDKError(
|
||||
"bad_request:database",
|
||||
"Failed to upsert conversation working state"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createMessageTargetResolution(params: {
|
||||
userId: string;
|
||||
chatId: string;
|
||||
messageId?: string | null;
|
||||
projectId?: string | null;
|
||||
rawText: string;
|
||||
uploadedAssetIds?: string[] | null;
|
||||
uploadedImageUrls?: string[] | null;
|
||||
selectedAssetIds?: string[] | null;
|
||||
selectedImageUrls?: string[] | null;
|
||||
candidateAssetIds?: string[] | null;
|
||||
candidateImageUrls?: string[] | null;
|
||||
resolvedAssetIds?: string[] | null;
|
||||
resolvedImageUrls?: string[] | null;
|
||||
resolutionMode: string;
|
||||
confidence?: number | null;
|
||||
needsClarification?: boolean;
|
||||
reasoning?: any;
|
||||
}): Promise<MessageTargetResolution | null> {
|
||||
try {
|
||||
const [result] = await db
|
||||
.insert(messageTargetResolution)
|
||||
.values({
|
||||
userId: params.userId,
|
||||
chatId: params.chatId,
|
||||
messageId: params.messageId ?? null,
|
||||
projectId: params.projectId ?? null,
|
||||
rawText: params.rawText,
|
||||
uploadedAssetIds: normalizeStringArray(params.uploadedAssetIds),
|
||||
uploadedImageUrls: normalizeStringArray(params.uploadedImageUrls),
|
||||
selectedAssetIds: normalizeStringArray(params.selectedAssetIds),
|
||||
selectedImageUrls: normalizeStringArray(params.selectedImageUrls),
|
||||
candidateAssetIds: normalizeStringArray(params.candidateAssetIds),
|
||||
candidateImageUrls: normalizeStringArray(params.candidateImageUrls),
|
||||
resolvedAssetIds: normalizeStringArray(params.resolvedAssetIds),
|
||||
resolvedImageUrls: normalizeStringArray(params.resolvedImageUrls),
|
||||
resolutionMode: params.resolutionMode,
|
||||
confidence: Math.max(0, Math.min(100, Math.round(params.confidence ?? 0))),
|
||||
needsClarification: Boolean(params.needsClarification),
|
||||
reasoning: params.reasoning ?? null,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
.returning();
|
||||
|
||||
return result ?? null;
|
||||
} catch (_error) {
|
||||
throw new ChatSDKError(
|
||||
"bad_request:database",
|
||||
"Failed to create message target resolution"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMessageCountByUserId({
|
||||
id,
|
||||
differenceInHours,
|
||||
|
||||
@ -665,3 +665,80 @@ export const videoStoryboardShotTask = pgTable("VideoStoryboardShotTask", {
|
||||
});
|
||||
|
||||
export type VideoStoryboardShotTask = InferSelectModel<typeof videoStoryboardShotTask>;
|
||||
|
||||
// 会话工作记忆表(用于连续对话中的图片/资产目标定位)
|
||||
export const conversationWorkingState = pgTable("ConversationWorkingState", {
|
||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
userId: uuid("userId")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
chatId: uuid("chatId")
|
||||
.notNull()
|
||||
.references(() => chat.id, { onDelete: "cascade" })
|
||||
.unique(),
|
||||
projectId: uuid("projectId").references(() => canvasProject.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
currentIntent: varchar("currentIntent", { length: 64 }),
|
||||
currentSubIntent: varchar("currentSubIntent", { length: 64 }),
|
||||
currentFocusAssetIds: jsonb("currentFocusAssetIds").$type<string[]>().notNull().default([]),
|
||||
currentFocusImageUrls: jsonb("currentFocusImageUrls").$type<string[]>().notNull().default([]),
|
||||
selectedAssetIds: jsonb("selectedAssetIds").$type<string[]>().notNull().default([]),
|
||||
selectedImageUrls: jsonb("selectedImageUrls").$type<string[]>().notNull().default([]),
|
||||
lastUploadedAssetIds: jsonb("lastUploadedAssetIds").$type<string[]>().notNull().default([]),
|
||||
lastUploadedImageUrls: jsonb("lastUploadedImageUrls").$type<string[]>().notNull().default([]),
|
||||
lastGeneratedAssetIds: jsonb("lastGeneratedAssetIds").$type<string[]>().notNull().default([]),
|
||||
lastGeneratedImageUrls: jsonb("lastGeneratedImageUrls").$type<string[]>().notNull().default([]),
|
||||
lastReferencedAssetIds: jsonb("lastReferencedAssetIds").$type<string[]>().notNull().default([]),
|
||||
lastReferencedImageUrls: jsonb("lastReferencedImageUrls").$type<string[]>().notNull().default([]),
|
||||
lastResolvedTargetAssetIds: jsonb("lastResolvedTargetAssetIds")
|
||||
.$type<string[]>()
|
||||
.notNull()
|
||||
.default([]),
|
||||
lastResolvedTargetImageUrls: jsonb("lastResolvedTargetImageUrls")
|
||||
.$type<string[]>()
|
||||
.notNull()
|
||||
.default([]),
|
||||
activeCanvasNodeIds: jsonb("activeCanvasNodeIds").$type<string[]>().notNull().default([]),
|
||||
lastCanvasSnapshot: jsonb("lastCanvasSnapshot"),
|
||||
lastTurnSummary: jsonb("lastTurnSummary"),
|
||||
lastMessageId: uuid("lastMessageId"),
|
||||
turnCount: integer("turnCount").notNull().default(0),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type ConversationWorkingState = InferSelectModel<typeof conversationWorkingState>;
|
||||
|
||||
// 每轮消息的目标资产/图片定位审计表
|
||||
export const messageTargetResolution = pgTable("MessageTargetResolution", {
|
||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
userId: uuid("userId")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
chatId: uuid("chatId")
|
||||
.notNull()
|
||||
.references(() => chat.id, { onDelete: "cascade" }),
|
||||
messageId: uuid("messageId").references(() => message.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
projectId: uuid("projectId").references(() => canvasProject.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
rawText: text("rawText").notNull(),
|
||||
uploadedAssetIds: jsonb("uploadedAssetIds").$type<string[]>().notNull().default([]),
|
||||
uploadedImageUrls: jsonb("uploadedImageUrls").$type<string[]>().notNull().default([]),
|
||||
selectedAssetIds: jsonb("selectedAssetIds").$type<string[]>().notNull().default([]),
|
||||
selectedImageUrls: jsonb("selectedImageUrls").$type<string[]>().notNull().default([]),
|
||||
candidateAssetIds: jsonb("candidateAssetIds").$type<string[]>().notNull().default([]),
|
||||
candidateImageUrls: jsonb("candidateImageUrls").$type<string[]>().notNull().default([]),
|
||||
resolvedAssetIds: jsonb("resolvedAssetIds").$type<string[]>().notNull().default([]),
|
||||
resolvedImageUrls: jsonb("resolvedImageUrls").$type<string[]>().notNull().default([]),
|
||||
resolutionMode: varchar("resolutionMode", { length: 64 }).notNull(),
|
||||
confidence: integer("confidence").notNull().default(0),
|
||||
needsClarification: boolean("needsClarification").notNull().default(false),
|
||||
reasoning: jsonb("reasoning"),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type MessageTargetResolution = InferSelectModel<typeof messageTargetResolution>;
|
||||
|
||||
@ -41,6 +41,7 @@ export interface ImageTextEditItem {
|
||||
// 图片节点数据
|
||||
export interface ImageNodeData {
|
||||
src: string;
|
||||
assetId?: string;
|
||||
title?: string;
|
||||
note?: string;
|
||||
prompt?: string;
|
||||
@ -59,6 +60,7 @@ export interface ImageNodeData {
|
||||
// 视频节点数据
|
||||
export interface VideoNodeData {
|
||||
src: string;
|
||||
assetId?: string;
|
||||
thumbnail?: string;
|
||||
prompt?: string;
|
||||
model?: string;
|
||||
@ -91,6 +93,7 @@ export interface SceneFusionNodeData {
|
||||
// 3D 模型节点数据
|
||||
export interface Model3DNodeData {
|
||||
src: string;
|
||||
assetId?: string;
|
||||
previewImage?: string;
|
||||
prompt?: string;
|
||||
modelVersion?: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user