diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 2c28ab18a..9c67221a6 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -1644,3 +1644,11 @@ v2(用户反馈首版难看后重做):菜单改用 antd Dropdown(placeme - 验收用户身份、订单号、渠道交易号和完整用户 ID 查询,与状态过滤、分页组合使用。 - 验收用户详情、订单详情、订单号复制和用户不存在的历史订单;关闭侧栏保留列表状态。 - 检查同步状态、关单确认、明暗主题、长文本和窄屏固定操作列。 +## 视频生成显式 @文本 引用内联(2026-09-11) + +修复普通模型视频渠道(非声明式工作流)丢弃显式 @文本N 引用内容的问题:`buildComposerGenerationContext` 此前受 `promptOnly` 门槛限制,提示词里的 @文本N 只被改写成「【文本N】」占位标记,文本节点真实内容不追加,导致发给上游的视频提示词只剩无意义占位符(如「任务要求提示词:【文本1】」)。现在显式 @文本 引用始终内联真实内容并在提示词末尾以「【文本N】\n正文」块追加;`promptOnly` 拦截自动上游连线文本的原有语义(提交 8f20240d)不变——未显式引用的连线文本在视频模式下仍不自动拼接,图片仍走结构化参考图通道。提示词超长时按既有校验明确报错,不静默截断。专项测试(canvas-node-generation-mentions.test.ts,新增 3 例)与前端全量测试通过(与 main 基线相比无新增失败),需浏览器验收: + +- [ ] 画布视频节点提示词同时 @图片1 和 @文本1(文本节点含多行正文)后用方舟或普通视频渠道生成:抓上游请求,text 为「原提示词 + 【文本1】标记 + 空行 + 【文本1】\n文本正文」,参考图仍为结构化 image_url(方舟素材绑定时为 asset:// 引用)。 +- [ ] 视频节点只连图片和文本节点、提示词不写任何 @ 引用时:text 仍只有输入框原文,连线文本不自动拼入(8f20240d 行为保持)。 +- [ ] 图片、文本、音频模式生成行为不变;显式 @文本 引用的图片节点重试同样内联文本内容。 +- [ ] 引用超长文本(超过模型提示词上限)时提交被拦截并提示具体字符数,不出现静默截断或直接丢弃。 diff --git a/web/src/components/canvas/canvas-node-generation.ts b/web/src/components/canvas/canvas-node-generation.ts index 47e1c46d0..6d6e0d920 100644 --- a/web/src/components/canvas/canvas-node-generation.ts +++ b/web/src/components/canvas/canvas-node-generation.ts @@ -229,7 +229,8 @@ function buildComposerGenerationContext( } nextPrompt += normalizedPrompt.slice(lastIndex); - if (textBlocks.length && !promptOnly) nextPrompt = `${nextPrompt.trim()}\n\n${textBlocks.join("\n\n")}`; + // 显式 @文本 引用是用户写进输入框的内容,必须内联真实文本;promptOnly 只拦自动上游文本。 + if (textBlocks.length) nextPrompt = `${nextPrompt.trim()}\n\n${textBlocks.join("\n\n")}`; if (autoIncludeWorkflowMedia) { // RunningHub/ComfyUI 工作流按保存的字段槽位接收图片、视频和音频; // 配置节点不能因为提示词里没有逐个 @ 就丢失已连接媒体。 diff --git a/web/src/pages/canvas/use-canvas-generation-executor.ts b/web/src/pages/canvas/use-canvas-generation-executor.ts index 09cedfb2e..e5469f7bc 100644 --- a/web/src/pages/canvas/use-canvas-generation-executor.ts +++ b/web/src/pages/canvas/use-canvas-generation-executor.ts @@ -143,7 +143,7 @@ export function useCanvasGenerationExecutor({ // AutoDL/其他声明式视频协议需要结构化参考素材;只有普通 // 模型视频接口才把提示词视为纯文本输入。 const usesWorkflowProvider = Boolean(mode !== "text" && generationConfig.taskWorkflowProvider && generationConfig.taskWorkflowProvider !== "model"); - // 普通视频协议只保留输入框文本;声明式工作流还要保留连接媒体。 + // 普通视频协议只保留输入框文本(显式 @文本 引用仍会展开为真实内容);声明式工作流还要保留连接媒体。 const promptOnly = mode === "video" && !usesWorkflowProvider; try { const baseContext = buildNodeGenerationContext( diff --git a/web/test/canvas-node-generation-mentions.test.ts b/web/test/canvas-node-generation-mentions.test.ts index 2612d2fde..1cbe40e71 100644 --- a/web/test/canvas-node-generation-mentions.test.ts +++ b/web/test/canvas-node-generation-mentions.test.ts @@ -106,3 +106,38 @@ describe("canvas node generation position mentions", () => { expect(context.prompt).toBe("让 进入画面"); }); }); + +describe("canvas node generation video text references", () => { + test("视频 promptOnly 下显式 @文本 引用仍内联真实内容,图片保持结构化引用", () => { + const target = targetNode(); + const image = node("image-a", CanvasNodeType.Image, "data:image/png;base64,a"); + const note = node("note", CanvasNodeType.Text, "角色设定:禾禾\n光影氛围:暗调"); + const context = buildNodeGenerationContext(target.id, [image, note, target], [connection(image.id), connection(note.id)], "参考图:@图片1\n任务要求:@文本1", [], true); + + expect(context.prompt).toBe("参考图:@图片1\n任务要求:【文本1】\n\n【文本1】\n角色设定:禾禾\n光影氛围:暗调"); + expect(context.referenceImages.map((item) => item.id)).toEqual(["image-a"]); + expect(context.textCount).toBe(1); + expect(context.imageCount).toBe(1); + }); + + test("视频 promptOnly 下未显式引用的连线文本不会自动拼进提示词", () => { + const target = targetNode(); + const image = node("image-a", CanvasNodeType.Image, "data:image/png;base64,a"); + const note = node("note", CanvasNodeType.Text, "角色设定:禾禾"); + const context = buildNodeGenerationContext(target.id, [image, note, target], [connection(image.id), connection(note.id)], "生成一段暗调舞蹈视频", [], true); + + expect(context.prompt).toBe("生成一段暗调舞蹈视频"); + expect(context.referenceImages.map((item) => item.id)).toEqual(["image-a"]); + expect(context.textCount).toBe(0); + }); + + test("视频 promptOnly 非显式引用路径仍丢弃上游文本,关闭后恢复自动拼接", () => { + const target = targetNode(); + const note = node("note", CanvasNodeType.Text, "角色设定:禾禾"); + const nodes = [note, target]; + const connections = [connection(note.id)]; + + expect(buildNodeGenerationContext(target.id, nodes, connections, "跳舞", [], true).prompt).toBe("跳舞"); + expect(buildNodeGenerationContext(target.id, nodes, connections, "跳舞", [], false).prompt).toBe("跳舞\n\n角色设定:禾禾"); + }); +});