fix canvas generation chat and failover behavior
This commit is contained in:
parent
cf21c80d24
commit
e5838f480a
@ -438,6 +438,30 @@ const CANVAS_IMAGE_TASK_POLL_INTERVAL_MS = 1600;
|
||||
const CANVAS_IMAGE_TASK_TIMEOUT_MS = 4 * 60 * 1000;
|
||||
const CANVAS_GENERATION_NODE_GAP = 56;
|
||||
const CANVAS_GENERATION_STACK_GAP = 28;
|
||||
const CANVAS_GENERATION_COLLISION_GAP = 32;
|
||||
|
||||
function hashCanvasChatValue(value: string) {
|
||||
let hash = 0;
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
hash = (hash * 31 + value.charCodeAt(index)) | 0;
|
||||
}
|
||||
return Math.abs(hash).toString(36);
|
||||
}
|
||||
|
||||
function buildCanvasMediaFallbackMessageId(chatId: string, urls: string[]) {
|
||||
const key = urls.map((url) => normalizeMediaUrl(url)).filter(Boolean).join("|");
|
||||
return `canvas-media-${chatId}-${hashCanvasChatValue(key || String(Date.now()))}`;
|
||||
}
|
||||
|
||||
function buildCanvasMediaMarkdown(params: {
|
||||
imageUrls: string[];
|
||||
videoUrls: string[];
|
||||
}) {
|
||||
return [
|
||||
...params.imageUrls.map((url) => ``),
|
||||
...params.videoUrls.map((url) => `[生成视频](${url})`),
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
function findCanvasImageAnchorNode(
|
||||
nodes: CanvasNode[],
|
||||
@ -466,6 +490,8 @@ function findCanvasImageAnchorNode(
|
||||
|
||||
function getAnchoredGenerationPosition(params: {
|
||||
anchorNode?: CanvasNode | null;
|
||||
existingNodes?: CanvasNode[];
|
||||
excludeNodeIds?: string[];
|
||||
size: { width: number; height: number };
|
||||
index?: number;
|
||||
fallbackNodeCount?: number;
|
||||
@ -473,34 +499,140 @@ function getAnchoredGenerationPosition(params: {
|
||||
}) {
|
||||
const index = params.index || 0;
|
||||
const anchorNode = params.anchorNode || null;
|
||||
const size = {
|
||||
width: Math.max(1, Number(params.size?.width || 300)),
|
||||
height: Math.max(1, Number(params.size?.height || 300)),
|
||||
};
|
||||
const excludedIds = new Set(params.excludeNodeIds || []);
|
||||
|
||||
const overlapsExistingNode = (position: { x: number; y: number }) => {
|
||||
const padding = CANVAS_GENERATION_COLLISION_GAP;
|
||||
const left = position.x - padding;
|
||||
const top = position.y - padding;
|
||||
const right = position.x + size.width + padding;
|
||||
const bottom = position.y + size.height + padding;
|
||||
|
||||
return (params.existingNodes || []).some((existingNode) => {
|
||||
if (!existingNode || excludedIds.has(existingNode.id)) return false;
|
||||
const nodeWidth = Math.max(1, Number(existingNode.size?.width || 300));
|
||||
const nodeHeight = Math.max(1, Number(existingNode.size?.height || 300));
|
||||
const nodeLeft = Number(existingNode.position?.x || 0);
|
||||
const nodeTop = Number(existingNode.position?.y || 0);
|
||||
const nodeRight = nodeLeft + nodeWidth;
|
||||
const nodeBottom = nodeTop + nodeHeight;
|
||||
|
||||
return !(
|
||||
right <= nodeLeft ||
|
||||
left >= nodeRight ||
|
||||
bottom <= nodeTop ||
|
||||
top >= nodeBottom
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const firstFreePosition = (candidates: Array<{ x: number; y: number }>) => {
|
||||
return candidates.find((candidate) => !overlapsExistingNode(candidate)) || null;
|
||||
};
|
||||
|
||||
if (anchorNode) {
|
||||
const anchorWidth = Number(anchorNode.size?.width || 300);
|
||||
const anchorHeight = Number(anchorNode.size?.height || 300);
|
||||
const stackStep = Math.max(
|
||||
96,
|
||||
Math.min(params.size.height, anchorHeight) + CANVAS_GENERATION_STACK_GAP
|
||||
Math.min(size.height, anchorHeight) + CANVAS_GENERATION_STACK_GAP
|
||||
);
|
||||
return {
|
||||
x: anchorNode.position.x + anchorWidth + CANVAS_GENERATION_NODE_GAP,
|
||||
y: anchorNode.position.y + index * stackStep,
|
||||
};
|
||||
const anchorLeft = Number(anchorNode.position?.x || 0);
|
||||
const anchorTop = Number(anchorNode.position?.y || 0);
|
||||
const anchorRight = anchorLeft + anchorWidth;
|
||||
const anchorBottom = anchorTop + anchorHeight;
|
||||
const gap = CANVAS_GENERATION_NODE_GAP;
|
||||
const preferred = [
|
||||
{
|
||||
x: anchorRight + gap,
|
||||
y: anchorTop + index * stackStep,
|
||||
},
|
||||
{
|
||||
x: anchorLeft - size.width - gap,
|
||||
y: anchorTop + index * stackStep,
|
||||
},
|
||||
{
|
||||
x: anchorLeft + index * (size.width + CANVAS_GENERATION_STACK_GAP),
|
||||
y: anchorBottom + gap,
|
||||
},
|
||||
{
|
||||
x: anchorLeft + index * (size.width + CANVAS_GENERATION_STACK_GAP),
|
||||
y: anchorTop - size.height - gap,
|
||||
},
|
||||
];
|
||||
const preferredFree = firstFreePosition(preferred);
|
||||
if (preferredFree) return preferredFree;
|
||||
|
||||
const horizontalStep = size.width + gap;
|
||||
const verticalStep = size.height + gap;
|
||||
const spiralCandidates: Array<{ x: number; y: number }> = [];
|
||||
for (let ring = 1; ring <= 10; ring += 1) {
|
||||
const rightX = anchorRight + gap + (ring - 1) * horizontalStep;
|
||||
const leftX = anchorLeft - size.width - gap - (ring - 1) * horizontalStep;
|
||||
const downY = anchorBottom + gap + (ring - 1) * verticalStep;
|
||||
const upY = anchorTop - size.height - gap - (ring - 1) * verticalStep;
|
||||
|
||||
spiralCandidates.push(
|
||||
{ x: rightX, y: anchorTop },
|
||||
{ x: leftX, y: anchorTop },
|
||||
{ x: anchorLeft, y: downY },
|
||||
{ x: anchorLeft, y: upY },
|
||||
{ x: rightX, y: downY },
|
||||
{ x: rightX, y: upY },
|
||||
{ x: leftX, y: downY },
|
||||
{ x: leftX, y: upY }
|
||||
);
|
||||
}
|
||||
const spiralFree = firstFreePosition(spiralCandidates);
|
||||
if (spiralFree) return spiralFree;
|
||||
|
||||
return preferred[0]!;
|
||||
}
|
||||
|
||||
const viewport = params.viewport || { x: 0, y: 0, zoom: 1 };
|
||||
const zoom = Math.max(0.1, viewport.zoom || 1);
|
||||
if (typeof window !== "undefined") {
|
||||
return {
|
||||
x: (-viewport.x + window.innerWidth * 0.52) / zoom - params.size.width / 2 + index * 28,
|
||||
y: (-viewport.y + window.innerHeight * 0.46) / zoom - params.size.height / 2 + index * 28,
|
||||
const centerPosition = {
|
||||
x:
|
||||
(-viewport.x + window.innerWidth * 0.52) / zoom -
|
||||
size.width / 2 +
|
||||
index * 28,
|
||||
y:
|
||||
(-viewport.y + window.innerHeight * 0.46) / zoom -
|
||||
size.height / 2 +
|
||||
index * 28,
|
||||
};
|
||||
if (!overlapsExistingNode(centerPosition)) return centerPosition;
|
||||
}
|
||||
|
||||
const fallbackNodeCount = params.fallbackNodeCount || 0;
|
||||
return {
|
||||
x: 100 + (fallbackNodeCount % 3) * (params.size.width + 20),
|
||||
y: 100 + Math.floor(fallbackNodeCount / 3) * (params.size.height + 20),
|
||||
const fallbackBase = {
|
||||
x:
|
||||
100 +
|
||||
(fallbackNodeCount % 3) *
|
||||
(size.width + CANVAS_GENERATION_COLLISION_GAP),
|
||||
y:
|
||||
100 +
|
||||
Math.floor(fallbackNodeCount / 3) *
|
||||
(size.height + CANVAS_GENERATION_COLLISION_GAP),
|
||||
};
|
||||
if (!overlapsExistingNode(fallbackBase)) return fallbackBase;
|
||||
|
||||
for (let row = 0; row < 20; row += 1) {
|
||||
for (let col = 0; col < 6; col += 1) {
|
||||
const candidate = {
|
||||
x: 100 + col * (size.width + CANVAS_GENERATION_COLLISION_GAP),
|
||||
y: 100 + row * (size.height + CANVAS_GENERATION_COLLISION_GAP),
|
||||
};
|
||||
if (!overlapsExistingNode(candidate)) return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackBase;
|
||||
}
|
||||
|
||||
type CanvasImageGenerationRequest = {
|
||||
@ -3351,7 +3483,13 @@ export function CanvasChatPanel({
|
||||
}, [videoFrameSlots.length]);
|
||||
|
||||
// 合并 initialMessages 和历史消息
|
||||
const mergedInitialMessages = historyData?.messages ?? (activeChatId === chatId ? initialMessages : []);
|
||||
const mergedInitialMessages = useMemo(
|
||||
() =>
|
||||
sortCanvasMessagesByCreatedAt(
|
||||
historyData?.messages ?? (activeChatId === chatId ? initialMessages : [])
|
||||
),
|
||||
[activeChatId, chatId, historyData?.messages, initialMessages]
|
||||
);
|
||||
|
||||
const removeCanvasGenerationPlaceholders = useCallback(
|
||||
(chatSessionId?: string) => {
|
||||
@ -3496,9 +3634,23 @@ export function CanvasChatPanel({
|
||||
: selectedImageNodesRef.current,
|
||||
sourceImageUrls,
|
||||
});
|
||||
const size = { width: dimensions.width, height: dimensions.height };
|
||||
const placeholderNode = placeholderNodes[index];
|
||||
const size = anchorNode
|
||||
? {
|
||||
width: Math.max(
|
||||
1,
|
||||
Math.round(Number(anchorNode.size?.width || dimensions.width))
|
||||
),
|
||||
height: Math.max(
|
||||
1,
|
||||
Math.round(Number(anchorNode.size?.height || dimensions.height))
|
||||
),
|
||||
}
|
||||
: { width: dimensions.width, height: dimensions.height };
|
||||
const position = getAnchoredGenerationPosition({
|
||||
anchorNode,
|
||||
existingNodes: currentNodes,
|
||||
excludeNodeIds: placeholderNode ? [placeholderNode.id] : [],
|
||||
fallbackNodeCount: currentNodes.length + addedImageCount + addedVideoCount,
|
||||
index,
|
||||
size,
|
||||
@ -3524,11 +3676,9 @@ export function CanvasChatPanel({
|
||||
agentExecutionSnapshotId,
|
||||
sourceNodeId: sourceNodeId || anchorNode?.id || undefined,
|
||||
placementMode: "anchored-generation",
|
||||
allowOverlap: true,
|
||||
recentlyGeneratedAt: completedAt,
|
||||
},
|
||||
};
|
||||
const placeholderNode = placeholderNodes[index];
|
||||
const nodeId = placeholderNode
|
||||
? placeholderNode.id
|
||||
: addNode({
|
||||
@ -3733,6 +3883,77 @@ export function CanvasChatPanel({
|
||||
const payload: CanvasMediaStreamPayload =
|
||||
typeof dataPart.data === "string" ? JSON.parse(dataPart.data) : dataPart.data;
|
||||
if (!payload || typeof payload !== "object") return;
|
||||
const imageUrls = Array.isArray(payload.imageUrls)
|
||||
? payload.imageUrls.filter(
|
||||
(url): url is string =>
|
||||
typeof url === "string" && /^https?:\/\//i.test(url)
|
||||
)
|
||||
: [];
|
||||
const videoUrls = Array.isArray(payload.videoUrls)
|
||||
? payload.videoUrls.filter(
|
||||
(url): url is string =>
|
||||
typeof url === "string" && /^https?:\/\//i.test(url)
|
||||
)
|
||||
: [];
|
||||
if (isEventForActiveChat && (imageUrls.length > 0 || videoUrls.length > 0)) {
|
||||
const allUrls = [...imageUrls, ...videoUrls];
|
||||
const fallbackMessageId =
|
||||
String(payload.messageId || "").trim() ||
|
||||
buildCanvasMediaFallbackMessageId(eventChatId, allUrls);
|
||||
const mediaMarkdown = buildCanvasMediaMarkdown({ imageUrls, videoUrls });
|
||||
const mediaUrlKeys = new Set(
|
||||
allUrls.map((url) => normalizeMediaUrl(url)).filter(Boolean)
|
||||
);
|
||||
|
||||
setMessages((prevMessages) => {
|
||||
const existingIndex = prevMessages.findIndex(
|
||||
(message) => message.id === fallbackMessageId
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
const existingMessage = prevMessages[existingIndex] as any;
|
||||
const existingText = ((existingMessage.parts || []) as any[])
|
||||
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
||||
.map((part) => String(part.text))
|
||||
.join("\n");
|
||||
const existingUrlKeys = new Set(
|
||||
[
|
||||
...extractAssistantImageUrls(existingText),
|
||||
...extractVideoUrls(existingText),
|
||||
]
|
||||
.map((url) => normalizeMediaUrl(url))
|
||||
.filter(Boolean)
|
||||
);
|
||||
const hasAllMedia = Array.from(mediaUrlKeys).every((key) =>
|
||||
existingUrlKeys.has(key)
|
||||
);
|
||||
if (hasAllMedia) return prevMessages;
|
||||
|
||||
const nextText = [existingText, mediaMarkdown]
|
||||
.filter((text) => String(text || "").trim())
|
||||
.join("\n\n");
|
||||
const nextMessages = [...prevMessages];
|
||||
nextMessages[existingIndex] = {
|
||||
...existingMessage,
|
||||
role: "assistant",
|
||||
content: nextText,
|
||||
parts: [{ type: "text", text: nextText }],
|
||||
};
|
||||
return sortCanvasMessagesByCreatedAt(nextMessages);
|
||||
}
|
||||
|
||||
const createdAt = new Date();
|
||||
return sortCanvasMessagesByCreatedAt([
|
||||
...prevMessages,
|
||||
{
|
||||
id: fallbackMessageId,
|
||||
role: "assistant",
|
||||
content: mediaMarkdown,
|
||||
createdAt,
|
||||
parts: [{ type: "text", text: mediaMarkdown }],
|
||||
} as any,
|
||||
]);
|
||||
});
|
||||
}
|
||||
void appendStreamMediaToCanvas(payload);
|
||||
clearStreamImageGenerationStateForChat(eventChatId);
|
||||
} catch (e) {
|
||||
@ -3875,19 +4096,24 @@ export function CanvasChatPanel({
|
||||
|
||||
const nextMessages = [...prevMessages];
|
||||
nextMessages[existingIndex] = updatedMessage;
|
||||
return nextMessages;
|
||||
return sortCanvasMessagesByCreatedAt(nextMessages);
|
||||
}
|
||||
|
||||
const createdAtValue =
|
||||
messageData.createdAt ||
|
||||
messageData.created_at ||
|
||||
messageData.timestamp ||
|
||||
new Date();
|
||||
const newMessage: any = {
|
||||
id: messageData.id,
|
||||
role: messageData.role,
|
||||
content: normalizedParts.find((part: any) => part?.type === "text")?.text || "",
|
||||
createdAt: new Date(),
|
||||
createdAt: createdAtValue,
|
||||
parts: normalizedParts,
|
||||
attachments: normalizedAttachments,
|
||||
};
|
||||
|
||||
return [...prevMessages, newMessage];
|
||||
return sortCanvasMessagesByCreatedAt([...prevMessages, newMessage]);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[CanvasChat] Failed to parse data-appendMessage:", e);
|
||||
@ -4342,7 +4568,7 @@ export function CanvasChatPanel({
|
||||
processedMessageIds.current = new Set();
|
||||
hasHydratedHistoryRef.current = true;
|
||||
forceHistoryHydrationRef.current = null;
|
||||
setMessages(historyMessages);
|
||||
setMessages(sortCanvasMessagesByCreatedAt(historyMessages));
|
||||
setMessageViewportKey((prev) => prev + 1);
|
||||
console.log("[CanvasChat] 已同步历史消息:", historyMessages.length, signature);
|
||||
|
||||
@ -4697,9 +4923,21 @@ export function CanvasChatPanel({
|
||||
: selectedImageNodesRef.current,
|
||||
sourceImageUrls: nearestUserImageUrls,
|
||||
});
|
||||
const size = { width: dimensions.width, height: dimensions.height };
|
||||
const size = anchorNode
|
||||
? {
|
||||
width: Math.max(
|
||||
1,
|
||||
Math.round(Number(anchorNode.size?.width || dimensions.width))
|
||||
),
|
||||
height: Math.max(
|
||||
1,
|
||||
Math.round(Number(anchorNode.size?.height || dimensions.height))
|
||||
),
|
||||
}
|
||||
: { width: dimensions.width, height: dimensions.height };
|
||||
const position = getAnchoredGenerationPosition({
|
||||
anchorNode,
|
||||
existingNodes: currentNodes,
|
||||
fallbackNodeCount: currentNodes.length + addedImageCount + addedVideoCount,
|
||||
index,
|
||||
size,
|
||||
@ -4725,7 +4963,6 @@ export function CanvasChatPanel({
|
||||
compareSourceUrl: canUseCompareSource ? compareSourceUrl : undefined,
|
||||
sourceNodeId: sourceNodeId || anchorNode?.id || undefined,
|
||||
placementMode: "anchored-generation",
|
||||
allowOverlap: true,
|
||||
recentlyGeneratedAt: completedAt,
|
||||
},
|
||||
},
|
||||
@ -8000,7 +8237,12 @@ export function CanvasChatPanel({
|
||||
}, [isDetailGenerationMode, messages.length]);
|
||||
|
||||
const visibleMessages = useMemo(
|
||||
() => (isDetailGenerationMode ? messages.slice(detailModeMessageStartIndex) : messages),
|
||||
() => {
|
||||
const scopedMessages = isDetailGenerationMode
|
||||
? messages.slice(detailModeMessageStartIndex)
|
||||
: messages;
|
||||
return sortCanvasMessagesByCreatedAt(scopedMessages);
|
||||
},
|
||||
[detailModeMessageStartIndex, isDetailGenerationMode, messages]
|
||||
);
|
||||
|
||||
@ -8141,12 +8383,20 @@ export function CanvasChatPanel({
|
||||
};
|
||||
const placeholderSize = anchorNode
|
||||
? {
|
||||
width: Math.max(180, Math.round(Number(anchorNode.size?.width || ratioPlaceholderSize.width))),
|
||||
height: Math.max(180, Math.round(Number(anchorNode.size?.height || ratioPlaceholderSize.height))),
|
||||
width: Math.max(
|
||||
180,
|
||||
Math.round(Number(anchorNode.size?.width || ratioPlaceholderSize.width))
|
||||
),
|
||||
height: Math.max(
|
||||
180,
|
||||
Math.round(Number(anchorNode.size?.height || ratioPlaceholderSize.height))
|
||||
),
|
||||
}
|
||||
: ratioPlaceholderSize;
|
||||
const position = getAnchoredGenerationPosition({
|
||||
anchorNode,
|
||||
existingNodes: currentNodes,
|
||||
excludeNodeIds: placeholderId ? [placeholderId] : [],
|
||||
fallbackNodeCount: currentNodes.length,
|
||||
size: placeholderSize,
|
||||
viewport: useCanvasStore.getState().viewport,
|
||||
@ -8156,11 +8406,16 @@ export function CanvasChatPanel({
|
||||
chatId: activeChatId,
|
||||
detailProgressLabel: label,
|
||||
generationProgress:
|
||||
phase === "queued" ? 12 : phase === "finalizing" ? 96 : phase === "failed" ? 100 : undefined,
|
||||
phase === "queued"
|
||||
? 12
|
||||
: phase === "finalizing"
|
||||
? 96
|
||||
: phase === "failed"
|
||||
? 100
|
||||
: undefined,
|
||||
modelName: streamState.modelName || currentModel?.displayName || undefined,
|
||||
providerType: streamState.providerType || currentModel?.providerType || undefined,
|
||||
placementMode: "anchored-generation",
|
||||
allowOverlap: true,
|
||||
};
|
||||
|
||||
if (placeholderId) {
|
||||
|
||||
@ -369,11 +369,23 @@ export function formatMessageDateLabel(timestamp: number): string {
|
||||
}
|
||||
|
||||
export function sortCanvasMessagesByCreatedAt(messages: ChatMessage[]): ChatMessage[] {
|
||||
return [...messages].sort((a, b) => {
|
||||
const timeA = getMessageTimestamp(a) ?? 0;
|
||||
const timeB = getMessageTimestamp(b) ?? 0;
|
||||
return timeA - timeB;
|
||||
});
|
||||
return messages
|
||||
.map((message, index) => ({ message, index }))
|
||||
.sort((a, b) => {
|
||||
const messageA = a.message;
|
||||
const messageB = b.message;
|
||||
const timeA = getMessageTimestamp(messageA) ?? 0;
|
||||
const timeB = getMessageTimestamp(messageB) ?? 0;
|
||||
if (timeA !== timeB) return timeA - timeB;
|
||||
|
||||
if (messageA.role !== messageB.role) {
|
||||
if (messageA.role === "user") return -1;
|
||||
if (messageB.role === "user") return 1;
|
||||
}
|
||||
|
||||
return a.index - b.index;
|
||||
})
|
||||
.map((item) => item.message);
|
||||
}
|
||||
|
||||
export function buildCanvasMessageFingerprint(message: ChatMessage): string {
|
||||
|
||||
@ -716,15 +716,22 @@ export function LovartHome({
|
||||
.filter((item) => item !== "精选");
|
||||
|
||||
const uniqueConfigured = Array.from(new Set(configured));
|
||||
const merged = [
|
||||
"全部",
|
||||
...uniqueConfigured.filter((item) => item !== "全部"),
|
||||
...DISCOVERY_DEFAULT_TABS,
|
||||
];
|
||||
if (uniqueConfigured.length > 0) {
|
||||
const configuredTabs = [
|
||||
"全部",
|
||||
...uniqueConfigured.filter((item) => item !== "全部"),
|
||||
];
|
||||
return Array.from(new Set(configuredTabs)).slice(0, 12);
|
||||
}
|
||||
|
||||
return Array.from(new Set(merged)).slice(0, 12);
|
||||
return ["全部", ...DISCOVERY_DEFAULT_TABS].slice(0, 12);
|
||||
}, [surpriseCategories]);
|
||||
|
||||
useEffect(() => {
|
||||
if (discoveryTabs.includes(activeCategory)) return;
|
||||
setActiveCategory(discoveryTabs[0] || "全部");
|
||||
}, [activeCategory, discoveryTabs]);
|
||||
|
||||
const composerPreviewTexts = useMemo(() => {
|
||||
const configured = (siteSettingsData?.lovartComposerPreviewTexts || [])
|
||||
.map((item) => String(item || "").trim())
|
||||
|
||||
@ -311,6 +311,7 @@ type CanvasModelWithProvider = Awaited<
|
||||
>[number];
|
||||
|
||||
type CanvasImageSizeValue = "1K" | "2K" | "4K";
|
||||
const CANVAS_FAILOVER_SAME_NAME_THRESHOLD = 0.5;
|
||||
|
||||
interface CanvasImageGenConfig {
|
||||
maxImages?: number;
|
||||
@ -389,6 +390,67 @@ function getCanvasModelRefKeys(item: CanvasModelWithProvider) {
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeModelNameForSimilarity(value: unknown) {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[「」『』【】[\]()(){}]/g, " ")
|
||||
.replace(/[_/|::,,.。+]+/g, "-")
|
||||
.replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
function tokenizeModelNameForSimilarity(value: unknown) {
|
||||
const normalized = normalizeModelNameForSimilarity(value);
|
||||
const compact = normalized.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "");
|
||||
const tokens = new Set(
|
||||
normalized
|
||||
.split(/[^a-z0-9\u4e00-\u9fa5]+/g)
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length >= 2)
|
||||
);
|
||||
|
||||
if (compact.length >= 2) {
|
||||
tokens.add(compact);
|
||||
for (let index = 0; index < compact.length - 1; index += 1) {
|
||||
tokens.add(compact.slice(index, index + 2));
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function scoreModelNameSimilarity(
|
||||
source: CanvasModelWithProvider,
|
||||
candidate: CanvasModelWithProvider
|
||||
) {
|
||||
const sourceNames = [
|
||||
source.model.displayName,
|
||||
source.model.modelId,
|
||||
].filter(Boolean);
|
||||
const candidateNames = [
|
||||
candidate.model.displayName,
|
||||
candidate.model.modelId,
|
||||
].filter(Boolean);
|
||||
|
||||
let bestScore = 0;
|
||||
for (const sourceName of sourceNames) {
|
||||
const sourceTokens = tokenizeModelNameForSimilarity(sourceName);
|
||||
if (sourceTokens.size === 0) continue;
|
||||
for (const candidateName of candidateNames) {
|
||||
const candidateTokens = tokenizeModelNameForSimilarity(candidateName);
|
||||
if (candidateTokens.size === 0) continue;
|
||||
let overlap = 0;
|
||||
sourceTokens.forEach((token) => {
|
||||
if (candidateTokens.has(token)) overlap += 1;
|
||||
});
|
||||
const score = overlap / Math.max(sourceTokens.size, candidateTokens.size);
|
||||
bestScore = Math.max(bestScore, score);
|
||||
}
|
||||
}
|
||||
|
||||
return bestScore;
|
||||
}
|
||||
|
||||
function findCanvasFailoverModel(params: {
|
||||
selectedModel: CanvasModelWithProvider;
|
||||
imageModels: CanvasModelWithProvider[];
|
||||
@ -422,14 +484,30 @@ function findCanvasFailoverModel(params: {
|
||||
if (configured) return configured;
|
||||
}
|
||||
|
||||
const sameProviderType = params.imageModels.find(
|
||||
(item) =>
|
||||
isUsableFallback(item) &&
|
||||
item.provider.type === params.selectedModel.provider.type
|
||||
);
|
||||
if (sameProviderType) return sameProviderType;
|
||||
const usableModels = params.imageModels.filter(isUsableFallback);
|
||||
const scoredModels = usableModels
|
||||
.map((item, index) => ({
|
||||
item,
|
||||
index,
|
||||
nameSimilarity: scoreModelNameSimilarity(params.selectedModel, item),
|
||||
sameProviderType: item.provider.type === params.selectedModel.provider.type,
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
const leftSameName =
|
||||
left.nameSimilarity >= CANVAS_FAILOVER_SAME_NAME_THRESHOLD;
|
||||
const rightSameName =
|
||||
right.nameSimilarity >= CANVAS_FAILOVER_SAME_NAME_THRESHOLD;
|
||||
if (leftSameName !== rightSameName) return leftSameName ? -1 : 1;
|
||||
if (left.sameProviderType !== right.sameProviderType) {
|
||||
return left.sameProviderType ? -1 : 1;
|
||||
}
|
||||
if (right.nameSimilarity !== left.nameSimilarity) {
|
||||
return right.nameSimilarity - left.nameSimilarity;
|
||||
}
|
||||
return left.index - right.index;
|
||||
});
|
||||
|
||||
return params.imageModels.find(isUsableFallback) || null;
|
||||
return scoredModels[0]?.item || null;
|
||||
}
|
||||
|
||||
export async function generateCanvasImage(
|
||||
|
||||
@ -250,7 +250,6 @@ export const useCanvasStore = create<CanvasStore>()(
|
||||
const stepY = height + padding;
|
||||
|
||||
const overlaps = (pos: Position) => nodes.some(n => {
|
||||
if (n.type !== 'image') return false;
|
||||
const nSize = n.size || { width: 300, height: 300 };
|
||||
return !(
|
||||
pos.x + width + padding <= n.position.x ||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user