chore(release): 版本发布 - 发布 v1.2.8.rc3 候选版本
修复生产前端 Rolldown 分包循环依赖白屏,补齐画布引用排序与素材 payload 合同,并同步工作流生成素材字段。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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! } })),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user