diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index cc384e868..512952ac9 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -120,6 +120,7 @@ import { shouldBypassCanvasImagePointsPrecheckForAgentControl, shouldPreserveCanvasAgentSlotOnlyContext, } from "@/lib/canvas/agent-client-intent"; +import { orderItemsBySelectionIds } from "@/lib/canvas/selection-order"; import { getCanvasSkillIconComponent } from "@/components/canvas/canvas-skill-icons"; import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar"; import { VideoParamsBar, type VideoGenerationParams } from "@/components/video-params-bar"; @@ -2190,13 +2191,16 @@ export function CanvasChatPanel({ ); // 获取选中的图片节点 - const selectedImageNodes = useMemo( - () => + const selectedImageNodes = useMemo(() => { + const selectedNodeIdSet = new Set(selectedNodeIds); + return orderItemsBySelectionIds( nodes.filter( - (node) => selectedNodeIds.includes(node.id) && isCanvasImageReadyForAi(node) + (node) => selectedNodeIdSet.has(node.id) && isCanvasImageReadyForAi(node) ), - [nodes, selectedNodeIds] - ); + selectedNodeIds, + (node) => node.id, + ); + }, [nodes, selectedNodeIds]); const selectedImageNodesRef = useRef([]); const generationAnchorNodesRef = useRef([]); @@ -6410,9 +6414,11 @@ export function CanvasChatPanel({ const MAX_CONTEXT_IMAGES = 12; const MAX_AGENT_CANDIDATE_NODES = 30; - const selectedCanvasNodes = nodes - .filter((node) => selectedNodeIdSet.has(node.id)) - .slice(0, 20); + const selectedCanvasNodes = orderItemsBySelectionIds( + nodes.filter((node) => selectedNodeIdSet.has(node.id)), + effectiveSelectedNodeIds, + (node) => node.id, + ).slice(0, 20); const selectedContextNodes = selectedImageNodesForContext.slice(0, 20); generationAnchorNodesRef.current = selectedContextNodes.slice(0, 4); const selectedIds = new Set(selectedContextNodes.map((node) => node.id)); diff --git a/lib/ai/context-memory/media-input.ts b/lib/ai/context-memory/media-input.ts index 955d16aa1..6d06e1697 100644 --- a/lib/ai/context-memory/media-input.ts +++ b/lib/ai/context-memory/media-input.ts @@ -1,6 +1,7 @@ import "server-only"; import { extractImageUrlsFromText, inferOriginalImageUrl } from "@/lib/image-url"; +import { orderItemsBySelectionIds } from "@/lib/canvas/selection-order"; import type { DBMessage } from "@/lib/db/schema"; import type { CanvasImageNodeContext, @@ -228,9 +229,16 @@ export function resolveChatMediaInput( ); const canvasImageRefs = toCanvasImageRefs(params.contextPayload?.canvasImageNodes, maxImages); - const selectedNodeIds = new Set(params.contextPayload?.selectedNodeIds || []); + const selectedNodeIds = (params.contextPayload?.selectedNodeIds || []) + .map((id) => String(id || "").trim()) + .filter(Boolean); + const selectedNodeIdSet = new Set(selectedNodeIds); const selectedImageRefs = dedupeRefs( - canvasImageRefs.filter((ref) => ref.isSelected || (ref.nodeId && selectedNodeIds.has(ref.nodeId))), + orderItemsBySelectionIds( + canvasImageRefs.filter((ref) => ref.isSelected || (ref.nodeId && selectedNodeIdSet.has(ref.nodeId))), + selectedNodeIds, + (ref) => ref.nodeId, + ), maxImages, ); const history = extractHistoryImageRefs({ diff --git a/lib/ai/providers/snapgen-image.ts b/lib/ai/providers/snapgen-image.ts index 15d104cab..f1d91c6b5 100644 --- a/lib/ai/providers/snapgen-image.ts +++ b/lib/ai/providers/snapgen-image.ts @@ -2,6 +2,7 @@ import "server-only"; import sharp from "sharp"; import { uploadFile } from "@/lib/storage/service"; +import { buildSnapgenReferenceOrderPrompt } from "@/lib/ai/providers/snapgen-reference-order"; import { SNAPGEN_GPT_IMAGE_2_REQUEST_ASPECT_RATIOS, SNAPGEN_STANDARD_REQUEST_ASPECT_RATIOS, @@ -876,9 +877,10 @@ export function createSnapgenImage({ apiKey, baseURL }: SnapgenImageConfig) { const outputFormat = normalizeOutputFormat(options.imageParams?.outputFormat); const maxImages = options.imageGenConfig?.maxImages || (apiFormat === "snapgen-grok-image" ? 8 : apiFormat === "snapgen-meta-ai-image" ? 4 : 1); const imageCount = clampImageCount(options.imageParams?.imageCount, maxImages); + const promptForRequest = buildSnapgenReferenceOrderPrompt(prompt, imageUrls); const formData = await buildRequestForm({ apiFormat, - prompt, + prompt: promptForRequest, runtimeModelId, imageUrls, aspectRatio, @@ -908,6 +910,7 @@ export function createSnapgenImage({ apiKey, baseURL }: SnapgenImageConfig) { : apiFormat === "snapgen-generic-image" ? "file_urls" : "files", + referenceOrderPrompt: promptForRequest !== prompt, }); const createPayload = await requestSnapgenMultipart({ diff --git a/lib/ai/providers/snapgen-reference-order.ts b/lib/ai/providers/snapgen-reference-order.ts new file mode 100644 index 000000000..8d7b8e1d3 --- /dev/null +++ b/lib/ai/providers/snapgen-reference-order.ts @@ -0,0 +1,14 @@ +export function buildSnapgenReferenceOrderPrompt(prompt: string, imageUrls: string[]) { + const normalizedPrompt = String(prompt || "").trim(); + const referenceCount = imageUrls.filter((url) => String(url || "").trim()).length; + if (referenceCount < 2) return normalizedPrompt; + + return [ + `参考图顺序说明:本次请求包含 ${referenceCount} 张参考图,顺序与上传/选中顺序一致。`, + "第 1 张参考图优先作为主参考;如果用户提到“第一张图/图1/主图/原图”,对应第 1 张参考图,除非用户明确另行指定。", + "第 2 张及后续参考图仅作为辅助参考,按用户提示使用。", + normalizedPrompt, + ] + .filter(Boolean) + .join("\n"); +} diff --git a/lib/canvas/selection-order.ts b/lib/canvas/selection-order.ts new file mode 100644 index 000000000..e260343fe --- /dev/null +++ b/lib/canvas/selection-order.ts @@ -0,0 +1,40 @@ +export function orderItemsBySelectionIds( + items: T[], + selectedIds: string[], + getItemId: (item: T) => string | null | undefined, +) { + const normalizedSelectedIds = Array.from( + new Set( + selectedIds + .map((id) => String(id || "").trim()) + .filter(Boolean), + ), + ); + if (normalizedSelectedIds.length === 0) return items; + + const itemById = new Map(); + for (const item of items) { + const id = String(getItemId(item) || "").trim(); + if (id && !itemById.has(id)) { + itemById.set(id, item); + } + } + + const orderedItems: T[] = []; + const orderedItemSet = new Set(); + for (const id of normalizedSelectedIds) { + const item = itemById.get(id); + if (item && !orderedItemSet.has(item)) { + orderedItems.push(item); + orderedItemSet.add(item); + } + } + + for (const item of items) { + if (!orderedItemSet.has(item)) { + orderedItems.push(item); + } + } + + return orderedItems; +}