fix: preserve SnapGen reference image order

This commit is contained in:
134 2026-06-24 17:05:28 +08:00
parent 91a855c041
commit 560491aabb
5 changed files with 82 additions and 11 deletions

View File

@ -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<CanvasNode[]>([]);
const generationAnchorNodesRef = useRef<CanvasNode[]>([]);
@ -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));

View File

@ -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({

View File

@ -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({

View File

@ -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");
}

View File

@ -0,0 +1,40 @@
export function orderItemsBySelectionIds<T>(
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<string, T>();
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<T>();
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;
}