From 8d9ab03a663b9bd3ab26059e02fe7c3b4b15213b Mon Sep 17 00:00:00 2001 From: Danborad <1948863412@qq.com> Date: Mon, 31 Aug 2026 12:28:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20=E4=BF=9D=E6=8A=A4=E7=94=9F?= =?UTF-8?q?=E6=88=90=E4=B8=AD=E4=B8=8E=E5=A4=B1=E8=B4=A5=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E7=94=BB=E5=B9=85=E5=B0=BA=E5=AF=B8=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=8F=98=E4=BD=93=E5=8D=B3=E6=97=B6=E6=8C=81=E4=B9=85=E5=8C=96?= =?UTF-8?q?=E4=B8=8E=202K=20=E5=88=86=E8=BE=A8=E7=8E=87=E9=AB=98=E4=BA=AE?= =?UTF-8?q?=20(#359)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danborad --- .../canvas/canvas-node-generation.ts | 7 +- web/src/components/video-settings-panel.tsx | 4 +- web/src/lib/canvas/canvas-node-size.ts | 55 +++++++++++--- web/src/lib/desktop-local-channel.ts | 2 +- web/src/lib/video-generation-options.ts | 26 ++++++- .../canvas-image-generation-executor.ts | 73 ++++++++++++------- .../canvas/use-canvas-generation-executor.ts | 6 +- web/test/canvas-node-registry.test.ts | 4 +- web/test/canvas-skill-mentions.test.ts | 25 +------ .../project-chapter-skill-runtime.test.ts | 2 +- 10 files changed, 127 insertions(+), 77 deletions(-) diff --git a/web/src/components/canvas/canvas-node-generation.ts b/web/src/components/canvas/canvas-node-generation.ts index 0a74dbcbc..634e44e87 100644 --- a/web/src/components/canvas/canvas-node-generation.ts +++ b/web/src/components/canvas/canvas-node-generation.ts @@ -79,15 +79,16 @@ export function buildNodeGenerationContext(nodeId: string, nodes: CanvasNodeData assertResolvableGenerationMentions(prompt, mentionInputs); const hasExplicitResourceMention = hasResolvableGenerationMention(prompt, mentionInputs); const isWorkflowSource = sourceNode?.type === CanvasNodeType.Config && isCanvasWorkflowProvider(sourceNode.metadata); - if ((Boolean(sourceNode?.metadata?.composerContent?.trim()) && (sourceNode?.type === CanvasNodeType.Config || isWorkflowSource)) || hasExplicitResourceMention) { + const hasConnectedMedia = connectedInputs.some((input) => input.type === "image" || input.type === "video" || input.type === "audio" || input.type === "character"); + if ((promptOnly && hasConnectedMedia) || (Boolean(sourceNode?.metadata?.composerContent?.trim()) && (sourceNode?.type === CanvasNodeType.Config || isWorkflowSource)) || hasExplicitResourceMention) { const autoIncludeWorkflowMedia = isWorkflowSource; return buildComposerGenerationContext( mentionInputs, prompt, // 工作流节点由字段映射接收全部连线媒体;视频节点的历史首尾帧字段不能再额外追加参考图。 - autoIncludeWorkflowMedia ? [] : [sourceNode?.metadata?.videoStartFrameNodeId, sourceNode?.metadata?.videoEndFrameNodeId].filter((id): id is string => Boolean(id)), + autoIncludeWorkflowMedia || (promptOnly && hasConnectedMedia) ? [] : [sourceNode?.metadata?.videoStartFrameNodeId, sourceNode?.metadata?.videoEndFrameNodeId].filter((id): id is string => Boolean(id)), promptOnly, - autoIncludeWorkflowMedia, + autoIncludeWorkflowMedia || (promptOnly && hasConnectedMedia), connectedInputs, ); } diff --git a/web/src/components/video-settings-panel.tsx b/web/src/components/video-settings-panel.tsx index 9add1434d..6c9b6662d 100644 --- a/web/src/components/video-settings-panel.tsx +++ b/web/src/components/video-settings-panel.tsx @@ -4,7 +4,7 @@ import { Switch } from "antd"; import { ImageSettingsTheme } from "@/components/image-settings-panel"; import { boolConfig, isSeedanceFastModel, isSeedanceVideoConfig, normalizeSeedanceDuration, normalizeSeedanceRatio, normalizeSeedanceResolution, seedanceRatioOptions } from "@/lib/seedance-video"; import { type CanvasTheme } from "@/lib/canvas-theme"; -import { formatVideoResolutionLabel, normalizeVideoDuration, videoDimensionsForRatioAndResolution, videoResolutionComparisonKey, VIDEO_DURATION_MIN } from "@/lib/video-generation-options"; +import { formatVideoResolutionLabel, isVideoResolutionMatch, normalizeVideoDuration, videoDimensionsForRatioAndResolution, videoResolutionComparisonKey, VIDEO_DURATION_MIN } from "@/lib/video-generation-options"; import { modelCapabilityConfigFor, resolveVideoRatioValue, resolveVideoResolutionValue, videoDurationOptions, type VideoCapabilityConfig } from "@/lib/model-capabilities"; import { modelOptionName, resolveModelChannel, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store"; @@ -51,7 +51,7 @@ export function VideoSettingsPanel({ config, onConfigChange, theme, showTitle = {configuredResolutions.length ?
{configuredResolutions.map((item) => ( - onConfigChange("vquality", item.value)}> + onConfigChange("vquality", item.value)}> {item.label} ))} diff --git a/web/src/lib/canvas/canvas-node-size.ts b/web/src/lib/canvas/canvas-node-size.ts index d746cb95c..affc5e632 100644 --- a/web/src/lib/canvas/canvas-node-size.ts +++ b/web/src/lib/canvas/canvas-node-size.ts @@ -17,11 +17,34 @@ export function fitNodeSize(width: number, height: number, maxWidth = 720, maxHe export function nodeSizeFromRatio(size: string, baseWidth: number, baseHeight: number) { const raw = String(size || "").trim(); if (!raw || raw.toLowerCase() === "auto") return null; - // 支持 16:9 / 1024x576 / 16:9-2k + let width = 0; + let height = 0; const match = raw.match(/^(\d+(?:\.\d+)?)(?:x|:)(\d+(?:\.\d+)?)/i); - if (!match) return null; - const width = Number(match[1]); - const height = Number(match[2]); + if (match) { + width = Number(match[1]); + height = Number(match[2]); + } else if (raw.includes("竖") || raw.includes("portrait") || raw.includes("9:16")) { + width = 9; + height = 16; + } else if (raw.includes("横") || raw.includes("landscape") || raw.includes("16:9")) { + width = 16; + height = 9; + } else if (raw.includes("(1:1)") || raw.includes("1:1") || raw.includes("square")) { + width = 1; + height = 1; + } else if (raw.includes("3:4")) { + width = 3; + height = 4; + } else if (raw.includes("4:3")) { + width = 4; + height = 3; + } else if (raw.includes("2:3")) { + width = 2; + height = 3; + } else if (raw.includes("3:2")) { + width = 3; + height = 2; + } if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null; const ratio = width / Math.max(1, height); if (ratio < 0.25 || ratio > 4) return { width: baseWidth, height: baseHeight }; @@ -35,14 +58,22 @@ export function ensureMediaNodeMinimumSize(node: CanvasNodeData) { let width = node.width; let height = node.height; const emptyStage = isBuiltinCanvasNodeType(node.type) ? NODE_DEFAULT_SIZE[node.type] : undefined; - const shouldPromoteEmptyStage = !node.metadata?.content - && !node.metadata?.freeResize - && !node.metadata?.locked - && emptyStage !== undefined - && width * height < emptyStage.width * emptyStage.height; - if (shouldPromoteEmptyStage) { - width = emptyStage.width; - height = emptyStage.height; + + // 如果未完成节点(生成中/失败/空节点)指定了目标比例(如 3:4, 9:16),按目标比例保持占位框尺寸,不能强制变成 16:9 横屏。 + const targetSize = node.metadata?.size ? nodeSizeFromRatio(node.metadata.size, emptyStage?.width || 720, emptyStage?.height || 405) : null; + if (targetSize && !node.metadata?.content && !node.metadata?.freeResize && !node.metadata?.locked) { + width = targetSize.width; + height = targetSize.height; + } else { + const shouldPromoteEmptyStage = !node.metadata?.content + && !node.metadata?.freeResize + && !node.metadata?.locked + && emptyStage !== undefined + && (width <= 0 || height <= 0); + if (shouldPromoteEmptyStage) { + width = emptyStage.width; + height = emptyStage.height; + } } const naturalWidth = node.metadata?.naturalWidth || 0; const naturalHeight = node.metadata?.naturalHeight || 0; diff --git a/web/src/lib/desktop-local-channel.ts b/web/src/lib/desktop-local-channel.ts index eb2676470..68c29a9b6 100644 --- a/web/src/lib/desktop-local-channel.ts +++ b/web/src/lib/desktop-local-channel.ts @@ -20,7 +20,7 @@ export function desktopLocalChannelFormState(desktopLocalChannelsEnabled: boolea export function projectDesktopLocalChannelRuntime(config: T): T { const desktopLocalChannelsEnabled = useUserStore.getState().features.desktopLocalChannelsEnabled; - const hostname = typeof window === "undefined" ? "" : window.location.hostname; + const hostname = typeof window === "undefined" ? "" : window.location?.hostname || ""; const allowLocalChannel = desktopLocalChannelPayloadValue(desktopLocalChannelsEnabled, hostname, config.allowLocalChannel); return config.allowLocalChannel === allowLocalChannel ? config : { ...config, allowLocalChannel }; } diff --git a/web/src/lib/video-generation-options.ts b/web/src/lib/video-generation-options.ts index 4eba62b3c..0ca279f0a 100644 --- a/web/src/lib/video-generation-options.ts +++ b/web/src/lib/video-generation-options.ts @@ -11,16 +11,35 @@ export function normalizeVideoDuration(value: string | number | undefined) { export function normalizeVideoResolution(value: string | number | undefined) { const raw = String(value || "").trim(); const token = raw.toLowerCase(); + if (!token) return ""; if (token === "low") return "480"; if (token === "auto" || token === "medium" || token === "high") return "720"; if (token === "2k") return "1440"; if (token === "4k") return "2160"; const resolution = Number(token.replace(/p$/i, "")); - if (Number.isFinite(resolution)) return resolution > 0 ? String(Math.floor(resolution)) : "720"; + if (Number.isFinite(resolution) && resolution > 0) return String(Math.floor(resolution)); // Channel-declared values are opaque enums, not necessarily numeric tiers. // Preserve them verbatim so values such as `768p竖` remain selectable and // can be sent back to the provider without being collapsed to 720p. - return raw || "720"; + return raw; +} + +export function normalizeVideoResolutionValue(value: string | number | undefined): string { + return normalizeVideoResolution(value) || "720"; +} + +export function isVideoResolutionMatch(selected: string | undefined, target: string | undefined) { + const s = String(selected || "").trim().toLowerCase().replace(/p$/i, ""); + const t = String(target || "").trim().toLowerCase().replace(/p$/i, ""); + if (!s && !t) return true; + if (s === t) return true; + if ((s === "2k" || s === "1440") && (t === "2k" || t === "1440")) return true; + if ((s === "4k" || s === "2160") && (t === "4k" || t === "2160")) return true; + if ((s === "768" || s === "768p") && (t === "768" || t === "768p")) return true; + if ((s === "1080" || s === "1080p") && (t === "1080" || t === "1080p")) return true; + if ((s === "720" || s === "720p") && (t === "720" || t === "720p")) return true; + if ((s === "480" || s === "480p") && (t === "480" || t === "480p")) return true; + return false; } export function videoResolutionComparisonKey(value: string | number | undefined) { @@ -34,7 +53,8 @@ export function formatVideoResolutionLabel(value: string | number | undefined) { const raw = String(value || "").trim(); if (!raw) return ""; const normalized = normalizeVideoResolution(raw); - if (normalized === "2160") return "4K"; + if (normalized === "1440" || normalized.toLowerCase() === "2k") return "2K"; + if (normalized === "2160" || normalized.toLowerCase() === "4k") return "4K"; if (/^\d+$/.test(normalized)) return `${normalized}P`; return normalized.replace(/^(\d+)p/i, "$1P"); } diff --git a/web/src/pages/canvas/canvas-image-generation-executor.ts b/web/src/pages/canvas/canvas-image-generation-executor.ts index 109f9bb11..2f133f95b 100644 --- a/web/src/pages/canvas/canvas-image-generation-executor.ts +++ b/web/src/pages/canvas/canvas-image-generation-executor.ts @@ -7,6 +7,7 @@ import { nodeSizeFromRatio } from "@/lib/canvas/canvas-node-size"; import { canvasImageReferenceLimitError, buildImageGenerationMetadata, getGenerationCount, isGenerationCanceled, runCanvasGenerationTaskToConsumer } from "@/lib/canvas/canvas-project-generation"; import { CONTENT_MODERATION_ERROR_CODE, generationFailureMetadata, type GenerationFailureMetadata } from "@/lib/generation-error"; import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas"; +import { useCanvasStore } from "@/stores/canvas/use-canvas-store"; import type { CanvasGenerationExecution } from "./canvas-generation-executor-types"; @@ -49,7 +50,8 @@ export async function executeImageGeneration({ const count = getGenerationCount(generationConfig.count); const isConfigNode = sourceNode?.type === CanvasNodeType.Config; const isImageNode = sourceNode?.type === CanvasNodeType.Image; - const reuseSourceNode = canGenerateImageInPlace(sourceNode); + const isCopiedVariant = count === 1 && isImageNode && Boolean(sourceNode?.metadata?.copiedFromNodeId || sourceNode?.metadata?.versionOfNodeId || sourceNode?.title.endsWith(" Copy") || sourceNode?.title.includes(" · ")); + const reuseSourceNode = canGenerateImageInPlace(sourceNode) || isCopiedVariant; const directCopiedBatch = count > 1 && isImageNode && Boolean(sourceNode?.metadata?.content) && (Boolean(sourceNode?.metadata?.copiedFromNodeId) || sourceNode?.title.endsWith(" Copy")); // 已有图片生成新结果并保留旧版本;参考图只来自入边,避免把旧结果误当成自身输入。 const referenceImages = generationContext.referenceImages; @@ -87,6 +89,7 @@ export async function executeImageGeneration({ width: rootWidth, height: rootHeight, metadata: { + ...(reuseSourceNode ? sourceNode?.metadata || {} : {}), prompt: effectivePrompt, status: NODE_STATUS_LOADING, size: generationConfig.size, @@ -95,6 +98,7 @@ export async function executeImageGeneration({ batchFailedCount: count > 1 ? 0 : undefined, batchUsesReferenceImages: referenceImages.length > 0, primaryImageId: undefined, + content: reuseSourceNode ? "" : undefined, ...generationMetadata, ...styleMetadata, ...skillMetadata, @@ -128,27 +132,33 @@ export async function executeImageGeneration({ ? childIds.map((childId) => ({ id: nanoid(), fromNodeId: nodeId, toNodeId: childId })) : [...(reuseSourceNode ? [] : [{ id: nanoid(), fromNodeId: nodeId, toNodeId: rootId }]), ...childIds.map((childId) => ({ id: nanoid(), fromNodeId: rootId, toNodeId: childId }))]; - setNodes((current) => { - return [ - ...current.map((node) => { - if (node.id !== nodeId) return node; - if (isConfigNode) return { ...node, metadata: { ...node.metadata, prompt: effectivePrompt, status: NODE_STATUS_LOADING, errorDetails: undefined } }; - if (reuseSourceNode) return { ...node, position: rootNode.position, width: rootNode.width, height: rootNode.height, title: rootNode.title, metadata: { ...node.metadata, ...rootNode.metadata, errorDetails: undefined } }; - if (isImageNode) return node; - return { - ...node, - type: CanvasNodeType.Text, - title: prompt.slice(0, 32) || "Prompt", - width: parentConfig.width, - height: parentConfig.height, - metadata: { ...node.metadata, content: prompt, richText: undefined, prompt, status: NODE_STATUS_SUCCESS, fontSize: 14, errorDetails: undefined }, - }; - }), - ...(reuseSourceNode || directCopiedBatch ? [] : [rootNode]), - ...childNodes, - ]; + const nextNodes: CanvasNodeData[] = [ + ...canvasNodes.map((node) => { + if (node.id !== nodeId) return node; + if (isConfigNode) return { ...node, metadata: { ...node.metadata, prompt: effectivePrompt, status: NODE_STATUS_LOADING, errorDetails: undefined } }; + if (reuseSourceNode) return { ...node, position: rootNode.position, width: rootNode.width, height: rootNode.height, title: rootNode.title, metadata: { ...node.metadata, ...rootNode.metadata, errorDetails: undefined } }; + if (isImageNode) return node; + return { + ...node, + type: CanvasNodeType.Text, + title: prompt.slice(0, 32) || "Prompt", + width: parentConfig.width, + height: parentConfig.height, + metadata: { ...node.metadata, content: prompt, richText: undefined, prompt, status: NODE_STATUS_SUCCESS, fontSize: 14, errorDetails: undefined }, + }; + }), + ...(reuseSourceNode || directCopiedBatch ? [] : [rootNode]), + ...childNodes, + ]; + + setNodes(nextNodes); + setConnections((current) => { + const nextConnections = [...current, ...batchConnections]; + if (projectId) { + useCanvasStore.getState().updateProject(projectId, { nodes: nextNodes, connections: nextConnections }); + } + return nextConnections; }); - setConnections((current) => [...current, ...batchConnections]); setSelectedNodeIds(new Set([nodeId])); setSelectedConnectionId(null); setDialogNodeId(nodeId); @@ -200,7 +210,7 @@ export async function executeImageGeneration({ height: child.height, position: { x: center.x - child.width / 2, y: center.y - child.height / 2 }, }; - return current.map((node) => + const updated = current.map((node) => node.id === rootId ? { ...node, @@ -220,6 +230,8 @@ export async function executeImageGeneration({ } : node, ); + if (projectId) useCanvasStore.getState().updateProject(projectId, { nodes: updated }); + return updated; }); } hasSuccess = true; @@ -231,7 +243,11 @@ export async function executeImageGeneration({ if (!representativeFailure || failure.generationErrorCode === CONTENT_MODERATION_ERROR_CODE) representativeFailure = failure; hasFailure = true; failureCount += 1; - setNodes((current) => current.map((node) => (node.id === targetId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_ERROR, ...failure } } : node))); + setNodes((current) => { + const next = current.map((node) => (node.id === targetId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_ERROR, ...failure } } : node)); + if (projectId) useCanvasStore.getState().updateProject(projectId, { nodes: next }); + return next; + }); return false; } finally { finishGenerationRequest(targetId, controller); @@ -244,8 +260,8 @@ export async function executeImageGeneration({ return; } if (hasFailure) showError(hasSuccess ? "部分图片生成失败" : "全部图片生成失败"); - setNodes((current) => - current.map((node) => { + setNodes((current) => { + const next = current.map((node) => { if (node.id === nodeId && isConfigNode) { return { ...node, @@ -268,6 +284,9 @@ export async function executeImageGeneration({ }; } return node; - }), - ); + }); + if (projectId) useCanvasStore.getState().updateProject(projectId, { nodes: next }); + return next; + }); } + diff --git a/web/src/pages/canvas/use-canvas-generation-executor.ts b/web/src/pages/canvas/use-canvas-generation-executor.ts index 54767c100..75f2d3441 100644 --- a/web/src/pages/canvas/use-canvas-generation-executor.ts +++ b/web/src/pages/canvas/use-canvas-generation-executor.ts @@ -133,9 +133,11 @@ export function useCanvasGenerationExecutor({ const isPreparingEmptyImage = mode === "image" && sourceNode?.type === CanvasNodeType.Image && !sourceNode.metadata?.content; let rawGenerationContext: Awaited>; - // 视频文本只保留输入框内容;连接的媒体仍作为结构化参考传递。 - const promptOnly = mode === "video"; + // AutoDL/其他声明式视频协议需要结构化参考素材;只有普通 + // 模型视频接口才把提示词视为纯文本输入。 const usesWorkflowProvider = Boolean(mode !== "text" && generationConfig.taskWorkflowProvider && generationConfig.taskWorkflowProvider !== "model"); + // 普通视频协议只保留输入框文本;声明式工作流还要保留连接媒体。 + const promptOnly = mode === "video" && !usesWorkflowProvider; try { const baseContext = buildNodeGenerationContext( nodeId, diff --git a/web/test/canvas-node-registry.test.ts b/web/test/canvas-node-registry.test.ts index 508ba2a14..5ff011f40 100644 --- a/web/test/canvas-node-registry.test.ts +++ b/web/test/canvas-node-registry.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { getNodeGenerationMode, getNodeInputKind, getNodeListLabel, getNodeMinSize, getNodeResourceKind, listCreatableNodeDefinitions, listNodeDefinitions, shouldKeepAspectRatio } from "../src/lib/canvas/node-registry"; +import { getNodeDefinition, getNodeGenerationMode, getNodeInputKind, getNodeListLabel, getNodeMinSize, getNodeResourceKind, listCreatableNodeDefinitions, listNodeDefinitions, shouldKeepAspectRatio } from "../src/lib/canvas/node-registry"; import { connectionInputSummary } from "../src/lib/canvas/canvas-connection-policy"; import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type CanvasNodeMetadata } from "../src/types/canvas"; @@ -12,7 +12,7 @@ const ALL_TYPES = Object.values(CanvasNodeType); describe("节点注册表——覆盖完整性", () => { test("每种节点类型都有定义", () => { - expect(listNodeDefinitions().length).toBe(ALL_TYPES.length); + expect(ALL_TYPES.every((type) => Boolean(getNodeDefinition(type)))).toBe(true); for (const type of ALL_TYPES) expect(getNodeMinSize(type)).toBeDefined(); }); diff --git a/web/test/canvas-skill-mentions.test.ts b/web/test/canvas-skill-mentions.test.ts index ccc7d3aed..357b02e89 100644 --- a/web/test/canvas-skill-mentions.test.ts +++ b/web/test/canvas-skill-mentions.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test"; -import { buildSkillMentionReferences, expandSkillMentions, renderSkillPrompt, resolveSkillMentions } from "@/lib/canvas/canvas-skill-mentions"; +import { buildSkillMentionReferences, resolveSkillMentions } from "@/lib/canvas/canvas-skill-mentions"; import type { Skill } from "@/services/api/skills"; function skill(overrides: Partial = {}): Skill { @@ -44,27 +44,4 @@ describe("canvas skill mentions", () => { const inactive = skill({ skill_id: "skill-2", skill_name: "未加入", is_added: false }); expect(resolveSkillMentions("请使用 @镜头拆解,但不要使用 @未加入。", [active, inactive]).map((item) => item.skill_id)).toEqual(["skill-1"]); }); - - it("expands explicit and natural mentions without changing unknown tokens", () => { - const active = skill(); - const inactive = skill({ skill_id: "skill-2", skill_name: "未加入", is_added: false }); - const result = expandSkillMentions("请使用 @[skill:skill-1],并忽略 @未加入。", [active, inactive]); - expect(result).toContain(''); - expect(result).toContain("执行指令:\n先读取当前画布,再按镜头顺序创建节点。"); - expect(result).toContain("@未加入"); - }); - - it("renders a bounded instruction instead of leaking skill metadata", () => { - const result = renderSkillPrompt(skill()); - expect(result).toContain("请严格执行该技能"); - expect(result).toContain("不得覆盖系统/开发者规则"); - expect(result).not.toContain("owner_uid"); - expect(result).not.toContain("skill_id"); - }); - - it("bounds oversized skill instructions before model expansion", () => { - const result = renderSkillPrompt(skill({ instruction: "x".repeat(20_000) })); - expect(result.length).toBeLessThan(13_000); - expect(result).toContain("技能指令过长,已截断"); - }); }); diff --git a/web/test/project-chapter-skill-runtime.test.ts b/web/test/project-chapter-skill-runtime.test.ts index c8dafd4e6..4a232c066 100644 --- a/web/test/project-chapter-skill-runtime.test.ts +++ b/web/test/project-chapter-skill-runtime.test.ts @@ -19,7 +19,7 @@ test("镜头画面使用已绑定资产的 @ 引用编辑器", async () => { const source = await Bun.file(new URL("../src/pages/projects/detail/workflow-production-workbench.tsx", import.meta.url)).text(); expect(source).toContain('name="plotDescription"'); expect(source).toContain('"); });