chore(release): 版本发布 - 发布 v1.2.8.rc3 候选版本

修复生产前端 Rolldown 分包循环依赖白屏,补齐画布引用排序与素材 payload 合同,并同步工作流生成素材字段。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCat
2026-09-10 13:48:31 +08:00
parent 01084c9a0f
commit 916043fb5a
21 changed files with 535 additions and 37 deletions

View File

@@ -1,7 +1,7 @@
import { nanoid } from "nanoid";
import { type ResponseInputMessage, type ResponseToolCall } from "@/services/api/image";
import { runBackendToolGenerationTask } from "@/services/api/generation-task";
import { imageToDataUrl } from "@/services/image-storage";
import { resourceIdFromStorageKey } from "@/services/api/resources";
import { normalizeModelOptionValue, resolveModelRequestConfig, selectableModelsByCapability, type AiConfig } from "@/stores/use-config-store";
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
import { CanvasNodeType, type CanvasAssistantMessage, type CanvasAssistantReference, type CanvasAssistantSession, type CanvasNodeData } from "@/types/canvas";
@@ -476,6 +476,12 @@ export function buildAssistantReferences(nodes: CanvasNodeData[], selectedNodeId
export async function buildToolAgentMessages(snapshot: CanvasAgentSnapshot, history: CanvasAssistantMessage[], userMessage: CanvasAssistantMessage, skills: Skill[] = []): Promise<ResponseInputMessage[]> {
const refs = userMessage.references || [];
const imageRefs = refs.filter((item) => item.type === CanvasNodeType.Image);
const unavailableImageRefs = imageRefs.filter((item) => !resourceIdFromStorageKey(item.storageKey));
if (unavailableImageRefs.length) {
const names = unavailableImageRefs.map((item) => item.title || item.id).join("、");
throw new Error(`引用图片尚未同步到服务器:${names}。请等待上传完成后重试`);
}
const skillCatalog = skills
.filter((skill) => skill.isAdded)
.slice(0, 40)
@@ -493,7 +499,7 @@ export async function buildToolAgentMessages(snapshot: CanvasAgentSnapshot, hist
content: [
...refs.flatMap((item) => (item.text ? [{ type: "text" as const, text: `选中节点 ${item.title}:${item.text}` }] : [])),
{ type: "text", text: `当前画布:${JSON.stringify(compactSnapshot(snapshot))}\n\n用户需求:${userMessage.text}` },
...(await Promise.all(refs.filter((item) => item.dataUrl).map(async (item) => ({ type: "image_url" as const, image_url: { url: await imageToDataUrl(item) } })))),
...imageRefs.map((item) => ({ type: "image_url" as const, image_url: { url: item.storageKey! } })),
],
},
];

View File

@@ -1,7 +1,7 @@
import { Button, Image as AntImage, InputNumber, Modal, Popover } from "antd";
import { Tooltip } from "@/components/ui/base/tooltip";
import { useEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
import { ArrowUp, AtSign, Boxes, Camera, ChevronDown, FileText, ImageIcon, ImagePlus, Link2, LoaderCircle, Maximize2, Music2, Pencil, SlidersHorizontal, UserRound, Video, WandSparkles, X } from "lucide-react";
import { ArrowUp, AtSign, Boxes, Camera, ChevronDown, FileText, GripVertical, ImageIcon, ImagePlus, Link2, LoaderCircle, Maximize2, Music2, Pencil, SlidersHorizontal, UserRound, Video, WandSparkles, X } from "lucide-react";
import { ModelPicker } from "@/components/model-picker";
import { defaultConfig, modelOptionName, resolveModelChannel, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store";
@@ -36,6 +36,7 @@ import { quoteLogicalModel } from "@/services/api/logical-models";
export type CanvasNodeGenerationMode = CanvasGenerationMode;
type CanvasNodePromptPanelProps = {
projectId: string;
node: CanvasNodeData;
isRunning: boolean;
onPromptChange: (nodeId: string, prompt: string) => void;
@@ -43,6 +44,7 @@ type CanvasNodePromptPanelProps = {
onGenerate: (nodeId: string, mode: CanvasNodeGenerationMode, prompt: string) => void;
mentionReferences?: CanvasResourceReference[];
onRemoveReference?: (nodeId: string, reference: CanvasResourceReference) => void;
onReorderReferences?: (nodeId: string, orderedNodeIds: string[]) => void;
onClose?: () => void;
onNodeMouseDown?: (event: ReactPointerEvent, nodeId: string) => void;
onImageSettingsOpenChange?: (open: boolean) => void;
@@ -60,7 +62,7 @@ const PROMPT_EDITOR_VERTICAL_PADDING = 12;
const PROMPT_EDITOR_EXPANDED_VERTICAL_PADDING = 20;
const PROMPT_EDITOR_MAX_LINES = 8;
export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfigChange, onGenerate, mentionReferences = [], onRemoveReference, onClose, onNodeMouseDown, onImageSettingsOpenChange, workspaceMode = "professional" }: CanvasNodePromptPanelProps) {
export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChange, onConfigChange, onGenerate, mentionReferences = [], onRemoveReference, onReorderReferences, onClose, onNodeMouseDown, onImageSettingsOpenChange, workspaceMode = "professional" }: CanvasNodePromptPanelProps) {
const globalConfig = useEffectiveConfig();
const themeName = useThemeStore((state) => state.theme);
const theme = canvasThemes[themeName];
@@ -83,7 +85,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
const [paramsExpanded, setParamsExpanded] = useState(false); // #98 决策2:B区参数区折叠状态(手风琴)
const [promptOptimizerOpen, setPromptOptimizerOpen] = useState(false);
const [autoLinkEnabled, setAutoLinkEnabled] = useState(true);
const resolvedMentionReferences = useResolvedCanvasResourceReferences(mentionReferences);
const resolvedMentionReferences = useResolvedCanvasResourceReferences(mentionReferences, { projectId });
const normalizedSavedPrompt = useMemo(() => normalizeCanvasNodeMentionTokens(savedPrompt, mentionReferences), [mentionReferences, savedPrompt]);
const activeReferences = resolvedMentionReferences.filter((item) => item.active && item.kind !== "skill");
const requirements: ModelRequirements = {
@@ -436,7 +438,13 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
return (
<>
<div className="canvas-node-composer-editor" style={{ height }}>
<ConnectedReferenceShelf references={resolvedMentionReferences} theme={theme} onInsert={insertPromptReference} onRemove={(reference) => onRemoveReference?.(node.id, reference)} />
<ConnectedReferenceShelf
references={resolvedMentionReferences}
theme={theme}
onInsert={insertPromptReference}
onRemove={(reference) => onRemoveReference?.(node.id, reference)}
onReorder={onReorderReferences ? (orderedNodeIds) => onReorderReferences(node.id, orderedNodeIds) : undefined}
/>
<CanvasResourceMentionTextarea
value={prompt}
references={resolvedMentionReferences}
@@ -655,11 +663,30 @@ function referenceShelfHeading(references: CanvasResourceReference[]) {
return `${label} · ${references.length}`;
}
function ConnectedReferenceShelf({ references, theme, onInsert, onRemove }: { references: CanvasResourceReference[]; theme: CanvasTheme; onInsert: (reference: CanvasResourceReference) => void; onRemove?: (reference: CanvasResourceReference) => void }) {
function ConnectedReferenceShelf({ references, theme, onInsert, onRemove, onReorder }: { references: CanvasResourceReference[]; theme: CanvasTheme; onInsert: (reference: CanvasResourceReference) => void; onRemove?: (reference: CanvasResourceReference) => void; onReorder?: (orderedNodeIds: string[]) => void }) {
const activeReferences = references.filter((item) => item.active && item.kind !== "skill");
const [imagePreview, setImagePreview] = useState<CanvasResourceReference | null>(null);
const [draggedReferenceId, setDraggedReferenceId] = useState<string | null>(null);
if (!activeReferences.length) return null;
const moveReference = (sourceId: string, targetId: string) => {
if (!onReorder || sourceId === targetId) return;
const sourceIndex = activeReferences.findIndex((reference) => reference.nodeId === sourceId);
const targetIndex = activeReferences.findIndex((reference) => reference.nodeId === targetId);
if (sourceIndex < 0 || targetIndex < 0) return;
const ordered = [...activeReferences];
const [moved] = ordered.splice(sourceIndex, 1);
ordered.splice(targetIndex, 0, moved);
onReorder(ordered.map((reference) => reference.nodeId));
};
const moveReferenceByOffset = (sourceId: string, offset: -1 | 1) => {
const sourceIndex = activeReferences.findIndex((reference) => reference.nodeId === sourceId);
const target = activeReferences[sourceIndex + offset];
if (!target) return;
moveReference(sourceId, target.nodeId);
};
return (
<>
<div className="canvas-node-composer-references" role="group" aria-label="已连接素材">
@@ -667,7 +694,45 @@ function ConnectedReferenceShelf({ references, theme, onInsert, onRemove }: { re
{activeReferences.map((reference, index) => {
const canPreview = Boolean(reference.previewUrl) && (reference.kind === "image" || reference.kind === "character" || reference.kind === "video");
return (
<span key={reference.id} className="canvas-node-reference-chip">
<span
key={reference.id}
className="canvas-node-reference-chip"
data-dragging={draggedReferenceId === reference.nodeId || undefined}
onDragOver={(event) => {
if (!onReorder || !draggedReferenceId) return;
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}}
onDrop={(event) => {
event.preventDefault();
const sourceId = draggedReferenceId || event.dataTransfer.getData("text/plain");
setDraggedReferenceId(null);
moveReference(sourceId, reference.nodeId);
}}
>
{onReorder ? (
<button
type="button"
className="canvas-node-reference-drag-handle"
draggable
title={`拖动调整 ${reference.label} 的顺序`}
aria-label={`调整 ${reference.label} 的顺序;使用左右方向键也可移动`}
onDragStart={(event) => {
setDraggedReferenceId(reference.nodeId);
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", reference.nodeId);
}}
onDragEnd={() => setDraggedReferenceId(null)}
onKeyDown={(event) => {
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
event.preventDefault();
moveReferenceByOffset(reference.nodeId, event.key === "ArrowLeft" ? -1 : 1);
}}
onPointerDown={(event) => event.stopPropagation()}
>
<GripVertical className="size-3" />
</button>
) : null}
<span className="canvas-node-reference-order" aria-hidden>{index + 1}</span>
<button
type="button"

View File

@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
import { loadCanvasDrawingPreview } from "@/lib/canvas/canvas-drawing-storage";
import { resolveImageUrl } from "@/services/image-storage";
type ResolvedPreview = {
@@ -10,13 +11,14 @@ type ResolvedPreview = {
const previewPromiseCache = new Map<string, Promise<string>>();
export function useResolvedCanvasResourceReferences(references: CanvasResourceReference[]) {
export function useResolvedCanvasResourceReferences(references: CanvasResourceReference[], options?: { projectId?: string }) {
const projectId = options?.projectId;
const requests = useMemo(
() => references.flatMap((reference) => {
const identity = previewIdentity(reference);
const identity = previewIdentity(reference, projectId);
return identity ? [{ reference, identity }] : [];
}),
[references],
[projectId, references],
);
const [resolvedById, setResolvedById] = useState<Record<string, ResolvedPreview>>({});
@@ -27,7 +29,7 @@ export function useResolvedCanvasResourceReferences(references: CanvasResourceRe
requests.map(async ({ reference, identity }) => ({
id: reference.id,
identity,
url: await resolveReferencePreview(reference, identity),
url: await resolveReferencePreview(reference, identity, projectId),
})),
).then((resolved) => {
if (cancelled) return;
@@ -45,27 +47,39 @@ export function useResolvedCanvasResourceReferences(references: CanvasResourceRe
return () => {
cancelled = true;
};
}, [requests]);
}, [projectId, requests]);
return useMemo(
() => references.map((reference) => {
const identity = previewIdentity(reference);
const identity = previewIdentity(reference, projectId);
const resolved = identity ? resolvedById[reference.id] : undefined;
return resolved?.identity === identity && resolved.url !== reference.previewUrl ? { ...reference, previewUrl: resolved.url } : reference;
}),
[references, resolvedById],
[projectId, references, resolvedById],
);
}
function previewIdentity(reference: CanvasResourceReference) {
function previewIdentity(reference: CanvasResourceReference, projectId?: string) {
if (reference.drawingId && projectId) return `drawing:${projectId}:${reference.drawingId}:${reference.drawingRevision || 0}`;
const storageKey = reference.kind === "video" ? reference.previewStorageKey : reference.storageKey;
if (!storageKey || !["image", "video", "character"].includes(reference.kind)) return "";
return `${reference.kind}:${storageKey}`;
}
function resolveReferencePreview(reference: CanvasResourceReference, identity: string) {
function resolveReferencePreview(reference: CanvasResourceReference, identity: string, projectId?: string) {
const cached = previewPromiseCache.get(identity);
if (cached) return cached;
if (reference.drawingId && projectId) {
const pending = loadCanvasDrawingPreview(projectId, reference.drawingId)
.then((preview) => preview ? blobToDataUrl(preview) : reference.previewUrl || "")
.catch(() => reference.previewUrl || "")
.then((url) => {
if (!url) previewPromiseCache.delete(identity);
return url;
});
previewPromiseCache.set(identity, pending);
return pending;
}
const storageKey = reference.kind === "video" ? reference.previewStorageKey : reference.storageKey;
const pending = resolveImageUrl(storageKey, reference.previewUrl || "", { cacheMiss: true })
.catch(() => reference.previewUrl || "")
@@ -76,3 +90,12 @@ function resolveReferencePreview(reference: CanvasResourceReference, identity: s
previewPromiseCache.set(identity, pending);
return pending;
}
function blobToDataUrl(blob: Blob) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(reader.error || new Error("读取绘图预览失败"));
reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : "");
reader.readAsDataURL(blob);
});
}

View File

@@ -105,7 +105,7 @@ export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle =
/>
</span>
</div> : null}
{showSize ? <ImageSizePicker profile={profile} size={activeSize} quality={quality} onChange={(size) => onConfigChange("size", size)} /> : null}
{showSize ? <ImageSizePicker profile={profile} size={activeSize} quality={quality} onChange={(size, nextQuality) => applyImageSizeSelection(onConfigChange, size, nextQuality)} /> : null}
{showCount && effectiveMaxCount > 1 ? (
<div className="space-y-2">
<SettingTitle color={theme.node.muted}>生成张数</SettingTitle>
@@ -124,6 +124,11 @@ export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle =
);
}
export function applyImageSizeSelection(onConfigChange: ImageSettingsPanelProps["onConfigChange"], size: string, quality?: string) {
onConfigChange("size", size);
if (quality) onConfigChange("quality", quality);
}
export function ImageSettingsTheme({ theme, children }: { theme: CanvasTheme; children: ReactNode }) {
return (
<ConfigProvider