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,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { autoMentionCanvasResourceReferences, findCanvasResourceAutoLinkMatch, type CanvasResourceReference, applyCanvasConnectionPromptSync, buildAssetMentionReferences, buildCanvasNodeMentionReferenceMap, buildNodeMentionReferences, buildOrderedCanvasResourceReferences, canvasResourceMentionToken, collectUpstreamVideoNodes, imageGenerationReferenceConnections } from "../src/lib/canvas/canvas-resource-references";
import { autoMentionCanvasResourceReferences, findCanvasResourceAutoLinkMatch, type CanvasResourceReference, applyCanvasConnectionPromptSync, buildAssetMentionReferences, buildCanvasNodeMentionReferenceMap, buildNodeMentionReferences, buildOrderedCanvasResourceReferences, canvasResourceMentionToken, collectUpstreamVideoNodes, imageGenerationReferenceConnections, reorderCanvasResourceConnections } from "../src/lib/canvas/canvas-resource-references";
import { canvasNodeToAsset } from "../src/lib/canvas/canvas-node-asset";
import { buildNodeGenerationInputs } from "../src/components/canvas/canvas-node-generation";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "../src/types/canvas";
@@ -227,6 +227,21 @@ describe("canvas resource mention slots", () => {
expect(uploadedReference?.storageKey).toBe("video:user:source");
});
test("绘图引用保留本地预览定位信息", () => {
const drawing: CanvasNodeData = {
id: "drawing-a",
type: CanvasNodeType.Drawing,
title: "草图 A",
position: { x: 0, y: 0 },
width: 100,
height: 100,
metadata: { drawingId: "drawing-document-a", drawingRevision: 3 },
};
const target = videoNode("target");
const [reference] = buildNodeMentionReferences(target, [drawing, target], [connection(drawing.id, target.id)]);
expect(reference).toMatchObject({ kind: "image", label: "绘图1", drawingId: "drawing-document-a", drawingRevision: 3 });
});
test("画布节点引用只保存类型位置,不保存节点 ID", () => {
const target = videoNode("target");
const image = imageNode("image-a");
@@ -339,3 +354,32 @@ describe("image generation reference connections", () => {
expect(copied.every((item) => item.toNodeId === "result")).toBe(true);
});
});
describe("reorder canvas resource connections", () => {
test("直接引用按指定顺序换位并同步编号提示词", () => {
const imageA = imageNode("image-a");
const imageB = imageNode("image-b");
const target = { ...videoNode("target"), metadata: { composerContent: "比较 @图片1 和 @图片2" } };
const nodes = [imageA, imageB, target];
const previousConnections = [connection(imageA.id, target.id), connection(imageB.id, target.id)];
const nextConnections = reorderCanvasResourceConnections(target.id, [imageB.id, imageA.id], nodes, previousConnections);
expect(nextConnections.map((item) => item.fromNodeId)).toEqual([imageB.id, imageA.id]);
const nextNodes = applyCanvasConnectionPromptSync(nodes, previousConnections, nodes, nextConnections);
expect(nextNodes.find((node) => node.id === target.id)?.metadata?.composerContent).toBe("比较 @图片2 和 @图片1");
});
test("配置节点承接的引用可排序且不改变主链和无关连线", () => {
const imageA = imageNode("image-a");
const imageB = imageNode("image-b");
const target = videoNode("target");
const config: CanvasNodeData = { id: "config", type: CanvasNodeType.Config, title: "config", position: { x: 0, y: 0 }, width: 100, height: 100, metadata: {} };
const other = videoNode("other");
const main = connection(target.id, config.id);
const unrelated = connection(imageA.id, other.id);
const previousConnections = [main, connection(imageA.id, config.id), unrelated, connection(imageB.id, config.id)];
const nextConnections = reorderCanvasResourceConnections(target.id, [imageB.id, imageA.id], [imageA, imageB, target, config, other], previousConnections);
expect(nextConnections).toEqual([main, connection(imageB.id, config.id), unrelated, connection(imageA.id, config.id)]);
expect(nextConnections[0]).toBe(main);
expect(nextConnections[2]).toBe(unrelated);
});
});

View File

@@ -8,7 +8,7 @@ import { LocalDreaminaGenerationClientError, runLocalDreaminaGenerationTask, typ
import { createGenerationBatchRetryContexts, createGenerationRetryContext, generationTaskMetadata, runBackendCanvasGenerationTask, runCanvasGenerationTaskToConsumer } from "../src/lib/canvas/canvas-project-generation";
import { runCanvasAgentGenerationOps } from "../src/pages/canvas/use-canvas-agent-operations";
import { CanvasNodeType, type CanvasNodeData } from "../src/types/canvas";
import { onlineToolToOps } from "../src/components/canvas/canvas-assistant-online-tools";
import { buildToolAgentMessages, onlineToolToOps } from "../src/components/canvas/canvas-assistant-online-tools";
import { generationTaskShowsProgress, generationTaskStageLabel, generationTaskStatusLabel } from "../src/lib/generation-task-display";
import { generationErrorMessage } from "../src/lib/generation-error";
import { generationTaskNodeId, syncGenerationTaskToCanvasStore } from "../src/lib/canvas/canvas-generation-task-sync";
@@ -339,6 +339,58 @@ test("backend text generation forwards streaming callbacks and thinking options"
expect(result).toEqual({ mode: "text", text: "完整正文", reasoning: "思考摘要" });
});
test("online node retry creates a backend task and preserves retry identity in task metadata", async () => {
let createdInput: Parameters<NonNullable<Parameters<typeof runBackendGenerationTask>[1]>["createTask"]>[0] | undefined;
const running: GenerationTask = { id: "retry-task-new", type: "canvas_image", status: "running", prompt: "retry", attempts: 1, createdAt: "", updatedAt: "" };
await runBackendGenerationTask(
{
projectId: "canvas-retry",
mode: "image",
prompt: "retry",
config: backendModelConfig("retry-image-model"),
clientOperationId: "retry:operation-0000000000000001",
retryOf: "retry-task-old",
attemptGroupId: "retry-group",
},
{
createTask: async (input) => {
createdInput = input;
return running;
},
waitTask: async () => ({ ...running, status: "succeeded", resultJson: JSON.stringify({ mode: "image", images: [] }) }),
runLocal: async () => { throw new Error("must not use local Runtime"); },
createId: () => "unused-retry-id",
now: () => "2026-09-10T00:00:00.000Z",
},
);
expect(createdInput?.input?.metadata).toMatchObject({
clientOperationId: "retry:operation-0000000000000001",
retryOf: "retry-task-old",
attemptGroupId: "retry-group",
});
});
test("online canvas Agent sends resource references instead of inline image bodies", async () => {
const snapshot = { projectId: "canvas-agent", title: "Agent", nodes: [], connections: [], selectedNodeIds: [], viewport: { x: 0, y: 0, zoom: 1 } };
const messages = await buildToolAgentMessages(snapshot, [], {
id: "message",
role: "user",
text: "分析图片",
references: [{ id: "image", type: CanvasNodeType.Image, title: "参考图", dataUrl: `data:image/png;base64,${"A".repeat(1024)}`, storageKey: "resource:image-remote" }],
});
const serialized = JSON.stringify(messages);
expect(serialized).toContain("resource:image-remote");
expect(serialized).not.toContain("data:image/png");
expect(serialized.length).toBeLessThan(20_000);
await expect(buildToolAgentMessages(snapshot, [], {
id: "local-message",
role: "user",
text: "分析本地图片",
references: [{ id: "local-image", type: CanvasNodeType.Image, title: "未上传图片", dataUrl: "data:image/png;base64,AAAA" }],
})).rejects.toThrow("引用图片尚未同步到服务器");
});
test("the shared local generation entry projects pre-receipt work as submitting without fake progress", async () => {
const updates: GenerationTask[] = [];
let release!: () => void;

View File

@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import { ImageSizePicker } from "../src/components/image-size-picker";
import { applyImageSizeSelection } from "../src/components/image-settings-panel";
import { ImageSizePresetsEditor } from "../src/components/image-size-presets-editor";
import { ModelCapabilityEditor } from "../src/components/model-capability-editor";
import { defaultImageCapabilityConfig, normalizeModelCapabilityConfig } from "../src/lib/model-capabilities";
@@ -10,6 +11,12 @@ import { resolveImageRequestSize, validateImageSize } from "../src/services/api/
import { buildGeminiImageGenerationConfig } from "../src/lib/gemini-image";
describe("统一图片分辨率与宽高比", () => {
test("比例协议切换 4K 时同时提交尺寸和真实质量档位", () => {
const changes: Array<[string, string]> = [];
applyImageSizeSelection((key, value) => changes.push([key, value]), "16:9", "high");
expect(changes).toEqual([["size", "16:9"], ["quality", "high"]]);
});
test("像素协议从旧比例配置切换 4K 后发送真实像素,不回退到 1K", () => {
const profile = defaultImageCapabilityConfig();
profile.size = { parameter: "size", values: ["16:9"], default: "16:9", allowCustom: true };