feat(canvas): 云端 Agent - 完成策略分层、节点能力注册与可解释审批改造
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
import type { AgentApprovalPreview, AgentApprovalPreviewItem, AgentApprovalPreviewOperation } from "@/services/api/agent";
|
||||
|
||||
type RecordValue = Record<string, unknown>;
|
||||
|
||||
export type AgentApprovalPresentation = AgentApprovalPreview & { source: "server" | "fallback" };
|
||||
|
||||
function record(value: unknown): RecordValue | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value as RecordValue : null;
|
||||
}
|
||||
|
||||
function text(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
function text(value: unknown, max = 240): string {
|
||||
if (typeof value !== "string") return "";
|
||||
const compact = value.replace(/[\u0000-\u001f\u007f]/gu, " ").trim();
|
||||
return compact.length > max ? `${compact.slice(0, max)}…` : compact;
|
||||
}
|
||||
|
||||
function approvalArguments(value: unknown): RecordValue | null {
|
||||
@@ -17,35 +23,108 @@ function approvalArguments(value: unknown): RecordValue | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function agentApprovalPresentation(detail: unknown) {
|
||||
function shortReference(value: unknown): string {
|
||||
const id = text(value, 80);
|
||||
if (!id) return "未知目标";
|
||||
return id.length <= 16 ? id : `${id.slice(0, 8)}…${id.slice(-4)}`;
|
||||
}
|
||||
|
||||
function nodeTypeLabel(value: unknown): string {
|
||||
const type = text(value, 32).toLowerCase();
|
||||
return ({ text: "文本", markdown: "Markdown", image: "图片", video: "视频", audio: "音频", frame: "画框", script: "脚本" } as Record<string, string>)[type] || (type ? "节点" : "");
|
||||
}
|
||||
|
||||
function fieldLabel(key: string, nodeType: string): string {
|
||||
if (key === "title") return "节点名称";
|
||||
if (key === "content") return ["image", "video", "audio"].includes(nodeType.toLowerCase()) ? "下一版提示词" : "正文";
|
||||
return key === "composerContent" ? "下一版提示词" : "已配置字段";
|
||||
}
|
||||
|
||||
function operation(value: unknown): AgentApprovalPreviewOperation | null {
|
||||
return value === "add_node" || value === "update_node" || value === "connect_nodes" || value === "generate_media" ? value : null;
|
||||
}
|
||||
|
||||
function normalizeServerPreview(value: unknown): AgentApprovalPresentation | null {
|
||||
const source = record(value);
|
||||
if (!source || !Array.isArray(source.items)) return null;
|
||||
const rawItems = source.items;
|
||||
const items: AgentApprovalPreviewItem[] = [];
|
||||
for (const raw of rawItems) {
|
||||
const item = record(raw);
|
||||
const op = operation(item?.operation);
|
||||
const summary = text(item?.summary);
|
||||
if (!item || !op || !summary) continue;
|
||||
items.push({
|
||||
operation: op,
|
||||
nodeId: text(item.nodeId, 120) || undefined,
|
||||
nodeTitle: text(item.nodeTitle, 120) || undefined,
|
||||
resultTitle: text(item.resultTitle, 120) || undefined,
|
||||
nodeType: text(item.nodeType, 32) || undefined,
|
||||
nodeTypeLabel: text(item.nodeTypeLabel, 48) || undefined,
|
||||
targetNodeId: text(item.targetNodeId, 120) || undefined,
|
||||
targetNodeTitle: text(item.targetNodeTitle, 120) || undefined,
|
||||
targetNodeType: text(item.targetNodeType, 32) || undefined,
|
||||
fields: Array.isArray(item.fields) ? item.fields.map((field) => text(field, 64)).filter(Boolean).slice(0, 12) : undefined,
|
||||
details: Array.isArray(item.details) ? item.details.map((detail) => text(detail, 180)).filter(Boolean).slice(0, 12) : undefined,
|
||||
summary,
|
||||
});
|
||||
}
|
||||
const title = text(source.title, 120);
|
||||
const description = text(source.description, 500);
|
||||
if (!title || !description || !items.length) return null;
|
||||
return { kind: text(source.kind, 64) || "agent_approval", title, description, items, source: "server" };
|
||||
}
|
||||
|
||||
function canvasFallback(args: RecordValue | null): AgentApprovalPresentation {
|
||||
const rawOps = Array.isArray(args?.ops) ? args.ops : [];
|
||||
const items: AgentApprovalPreviewItem[] = [];
|
||||
for (const raw of rawOps) {
|
||||
const op = record(raw);
|
||||
const kind = operation(op?.type);
|
||||
if (!op || !kind || kind === "generate_media") continue;
|
||||
const type = text(op.nodeType, 32).toLowerCase();
|
||||
const label = nodeTypeLabel(type);
|
||||
if (kind === "add_node") {
|
||||
const title = text(op.title, 120) || `未命名${label || "节点"}`;
|
||||
items.push({ operation: kind, nodeTitle: title, nodeType: type || undefined, nodeTypeLabel: label || undefined, fields: ["节点名称", label === "文本" || label === "Markdown" ? "正文" : "初始内容"], summary: `新增${label || "节点"}《${title}》` });
|
||||
continue;
|
||||
}
|
||||
if (kind === "update_node") {
|
||||
const patch = record(op.patch);
|
||||
const id = shortReference(op.id);
|
||||
const fields = patch ? Object.keys(patch).map((key) => fieldLabel(key, type)).filter(Boolean) : [];
|
||||
const resultTitle = text(patch?.title, 120);
|
||||
const target = `目标节点(引用 ${id})`;
|
||||
items.push({ operation: kind, nodeTitle: target, nodeType: type || undefined, nodeTypeLabel: label || undefined, resultTitle: resultTitle || undefined, fields: fields.length ? [...new Set(fields)] : ["已配置字段"], summary: `修改${label || "节点"}《${target}》的${fields.length ? [...new Set(fields)].join("、") : "已配置字段"}${resultTitle ? `,名称改为《${resultTitle}》` : ""}` });
|
||||
continue;
|
||||
}
|
||||
const from = shortReference(op.fromNodeId);
|
||||
const to = shortReference(op.toNodeId);
|
||||
items.push({ operation: kind, nodeTitle: `来源(引用 ${from})`, targetNodeTitle: `目标(引用 ${to})`, summary: `建立来源(引用 ${from})→目标(引用 ${to})的引用连线` });
|
||||
}
|
||||
const counts = items.reduce<Record<string, number>>((out, item) => { out[item.operation] = (out[item.operation] || 0) + 1; return out; }, {});
|
||||
const parts = [counts.add_node ? `新增 ${counts.add_node} 个节点` : "", counts.update_node ? `修改 ${counts.update_node} 个节点` : "", counts.connect_nodes ? `建立 ${counts.connect_nodes} 条引用连线` : ""].filter(Boolean);
|
||||
return { source: "fallback", kind: "canvas_mutation", title: "确认画布修改", description: parts.length ? `Agent 准备${parts.join(",")}。历史审批缺少画布快照,以下目标使用安全引用标识;无法确认具体目标时不会代替当前画布。` : "审批参数不完整,无法确认具体修改目标,请重新读取画布后再申请审批。", items, };
|
||||
}
|
||||
|
||||
function mediaFallback(args: RecordValue | null, payload: RecordValue | null): AgentApprovalPresentation {
|
||||
const mode = text(args?.mode, 32).toLowerCase();
|
||||
const type = mode === "video" ? "视频" : mode === "image" ? "图片" : mode === "audio" ? "音频" : "媒体";
|
||||
const references = Array.isArray(args?.referenceNodeIds) ? args.referenceNodeIds.filter((id) => typeof id === "string") : [];
|
||||
const title = text(args?.title, 120) || `未命名${type}`;
|
||||
const details = [text(payload?.modelName) ? `模型:${text(payload?.modelName)}` : text(args?.channelModelKey) || text(args?.logicalModelId) ? `模型:${text(args?.channelModelKey) || text(args?.logicalModelId)}` : "", references.length ? `引用 ${references.length} 个画布资产,并建立连线` : "不引用画布媒体资产", typeof args?.durationSeconds === "number" && args.durationSeconds > 0 ? `时长:${args.durationSeconds} 秒` : "", text(args?.size) ? `画幅:${text(args?.size, 40)}` : "", text(args?.quality) ? `质量:${text(args?.quality, 40)}` : "", typeof args?.videoGenerateAudio === "boolean" ? `音频:${args.videoGenerateAudio ? "开启" : "关闭"}` : ""].filter(Boolean);
|
||||
return { source: "fallback", kind: "media_generation", title: `确认生成${type}`, description: `${type}草稿节点和引用连线已创建,尚未提交生成。确认规格后批准才会提交收费任务;拒绝则保留草稿,结果自动回写画布。`, items: [{ operation: "generate_media", nodeTitle: title, nodeType: mode || undefined, nodeTypeLabel: type, details, summary: `生成${type}《${title}》` }] };
|
||||
}
|
||||
|
||||
export function agentApprovalPresentation(detail: unknown): AgentApprovalPresentation {
|
||||
const payload = record(detail);
|
||||
const serverPreview = normalizeServerPreview(payload?.preview);
|
||||
if (serverPreview) return serverPreview;
|
||||
const call = record(payload?.call);
|
||||
const fn = record(call?.function);
|
||||
const name = text(fn?.name) || text(payload?.toolName);
|
||||
const args = approvalArguments(fn?.arguments ?? payload?.arguments);
|
||||
|
||||
if (name === "canvas_apply_ops") {
|
||||
const ops = Array.isArray(args?.ops) ? args.ops.map(record).filter((op): op is RecordValue => Boolean(op)) : [];
|
||||
const added = ops.filter((op) => op.type === "add_node").length;
|
||||
const updated = ops.filter((op) => op.type === "update_node").length;
|
||||
const connected = ops.filter((op) => op.type === "connect_nodes").length;
|
||||
const action = [added ? `新增 ${added} 个节点` : "", updated ? `修改 ${updated} 个节点` : "", connected ? `建立 ${connected} 条引用连线` : ""].filter(Boolean).join(",");
|
||||
const titles = ops.map((op) => text(op.title)).filter(Boolean).slice(0, 3).map((title) => title.slice(0, 64));
|
||||
return { title: "确认画布修改", description: action ? `Agent 准备${action},确认后才会写入画布。` : "Agent 准备修改当前画布,确认后才会写入。", items: titles };
|
||||
}
|
||||
if (name === "generate_media") {
|
||||
const type = args?.mode === "video" ? "视频" : args?.mode === "image" ? "图片" : "媒体";
|
||||
const references = Array.isArray(args?.referenceNodeIds) ? args.referenceNodeIds.filter((id) => typeof id === "string") : [];
|
||||
const items = [
|
||||
text(payload?.modelName) ? `模型:${text(payload?.modelName)}` : (text(args?.channelModelKey) || text(args?.logicalModelId)) ? `模型:${text(args?.channelModelKey) || text(args?.logicalModelId)}` : "",
|
||||
text(args?.title) ? `节点:${text(args?.title).slice(0, 64)}` : "",
|
||||
references.length ? `引用 ${references.length} 个画布资产,并建立连线` : "不引用画布媒体资产",
|
||||
typeof args?.durationSeconds === "number" && args.durationSeconds > 0 ? `时长:${args.durationSeconds} 秒` : "",
|
||||
text(args?.size) ? `画幅:${text(args?.size)}` : "",
|
||||
text(args?.quality) ? `质量:${text(args?.quality)}` : "",
|
||||
typeof args?.videoGenerateAudio === "boolean" ? `音频:${args.videoGenerateAudio ? "开启" : "关闭"}` : "",
|
||||
].filter(Boolean);
|
||||
return { title: `确认生成${type}`, description: `${type}草稿节点和引用连线已创建,尚未提交生成。确认模型与画幅后批准才会提交收费任务;拒绝则保留草稿,结果自动回写画布。`, items };
|
||||
}
|
||||
return { title: "确认执行操作", description: "Agent 请求执行一项操作,确认后才会继续。", items: [] as string[] };
|
||||
}
|
||||
if (name === "canvas_apply_ops") return canvasFallback(args);
|
||||
if (name === "generate_media") return mediaFallback(args, payload);
|
||||
return { source: "fallback", kind: "agent_approval", title: "确认执行操作", description: "Agent 请求执行一项操作,但审批参数无法识别;请重新读取画布后再申请审批。", items: [] };
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
|
||||
export type AgentCanvasAction = { nodeId: string; title: string; nodeType: string; action: string };
|
||||
export type AgentCanvasAction = { nodeId: string; title: string; nodeType: string; action: string; fields?: string[]; resultTitle?: string; targetNodeId?: string; targetTitle?: string; targetNodeType?: string };
|
||||
|
||||
export function agentRecord(value: unknown): Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
@@ -17,15 +17,21 @@ export function agentCanvasActions(toolName: string, detail: unknown, references
|
||||
const result = agentRecord(payload.result);
|
||||
const args = agentToolArguments(detail);
|
||||
const actions: AgentCanvasAction[] = [];
|
||||
const add = (id: unknown, title: unknown, kind: unknown, action: string) => {
|
||||
const add = (id: unknown, title: unknown, kind: unknown, action: string, details: Partial<AgentCanvasAction> = {}) => {
|
||||
if (typeof id !== "string" || !id || actions.some((item) => item.nodeId === id && item.action === action)) return;
|
||||
const reference = references.find((item) => item.nodeId === id);
|
||||
actions.push({ nodeId: id, title: typeof title === "string" && title ? title : reference?.title || id, nodeType: typeof kind === "string" ? kind : reference?.kind || "", action });
|
||||
actions.push({ nodeId: id, title: typeof title === "string" && title ? title : reference?.title || id, nodeType: typeof kind === "string" ? kind : reference?.kind || "", action, ...details });
|
||||
};
|
||||
if (Array.isArray(payload.actions)) {
|
||||
for (const value of payload.actions) {
|
||||
const action = agentRecord(value);
|
||||
add(action.nodeId, action.title, action.nodeType, String(action.action || "updated"));
|
||||
add(action.nodeId, action.title, action.nodeType, String(action.action || "updated"), {
|
||||
fields: Array.isArray(action.fields) ? action.fields.filter((field): field is string => typeof field === "string") : undefined,
|
||||
resultTitle: typeof action.resultTitle === "string" ? action.resultTitle : undefined,
|
||||
targetNodeId: typeof action.targetNodeId === "string" ? action.targetNodeId : undefined,
|
||||
targetTitle: typeof action.targetTitle === "string" ? action.targetTitle : undefined,
|
||||
targetNodeType: typeof action.targetNodeType === "string" ? action.targetNodeType : undefined,
|
||||
});
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
@@ -53,7 +59,13 @@ export function agentCanvasActions(toolName: string, detail: unknown, references
|
||||
}
|
||||
|
||||
export function agentCanvasActionLabel(action: AgentCanvasAction) {
|
||||
const kind = ({ image: "图片", video: "视频", audio: "音频", text: "文本", script: "分镜" } as Record<string, string>)[action.nodeType] || "";
|
||||
const kind = ({ image: "图片", video: "视频", audio: "音频", text: "文本", markdown: "Markdown", script: "分镜" } as Record<string, string>)[action.nodeType] || "";
|
||||
if (action.action === "referenced" && action.targetTitle) {
|
||||
const targetKind = ({ image: "图片", video: "视频", audio: "音频", text: "文本", markdown: "Markdown", script: "分镜" } as Record<string, string>)[action.targetNodeType || ""] || "";
|
||||
return `建立引用:${kind}节点《${action.title}》 → ${targetKind}节点《${action.targetTitle}》`;
|
||||
}
|
||||
const verb = ({ created: "创建了", updated: "更新了", referenced: "引用了", read: "读取了", generating: "已提交生成:", generated: "已生成", failed: "生成未完成:" } as Record<string, string>)[action.action] || "定位";
|
||||
return `${verb}${kind}节点《${action.title}》`;
|
||||
const fieldText = action.fields?.length ? `(修改:${action.fields.join("、")})` : "";
|
||||
const titleText = action.resultTitle ? `,名称改为《${action.resultTitle}》` : "";
|
||||
return `${verb}${kind}节点《${action.title}》${fieldText}${titleText}`;
|
||||
}
|
||||
|
||||
@@ -19,3 +19,11 @@ export function agentErrorPresentation(cause: unknown, fallback = "Agent 执行
|
||||
// 渠道、鉴权、配额等业务错误保留真实语义,不能按 message 猜测为服务未部署。
|
||||
return { title: fallback, text: text || fallback };
|
||||
}
|
||||
|
||||
export function agentSubmissionErrorTitle(cause: unknown, accepted: boolean) {
|
||||
if (accepted) return "运行已接收,但本地提交记录清理失败";
|
||||
const status = cause instanceof ApiError ? cause.status : undefined;
|
||||
if (status && [400, 401, 403, 404, 422].includes(status)) return "请求已被服务端拒绝";
|
||||
if (status) return "服务端已返回错误;重试将核对原请求,不重复创建";
|
||||
return "未收到服务端确认;重试将核对原请求,不重复创建";
|
||||
}
|
||||
|
||||
92
web/src/lib/canvas/canvas-node-prompt.test.ts
Normal file
92
web/src/lib/canvas/canvas-node-prompt.test.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import "./node-registry/definitions/index.ts";
|
||||
// @ts-expect-error -- Node 原生 TypeScript 测试运行器需要保留扩展名。
|
||||
import { canvasNodeHasCommittedContent, writeCanvasNodePrompt } from "./canvas-node-prompt.ts";
|
||||
// @ts-expect-error -- Node 原生 TypeScript 测试运行器需要保留扩展名。
|
||||
import { CanvasNodeType, type CanvasNodeData } from "../../types/canvas.ts";
|
||||
|
||||
function node(type: CanvasNodeData["type"], metadata: CanvasNodeData["metadata"] = {}): CanvasNodeData {
|
||||
return {
|
||||
id: `${type}-node`,
|
||||
type,
|
||||
title: "测试节点",
|
||||
position: { x: 0, y: 0 },
|
||||
width: 320,
|
||||
height: 180,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
test("媒体节点已有结果时只更新下一版提示词草稿", () => {
|
||||
for (const type of [CanvasNodeType.Image, CanvasNodeType.Video, CanvasNodeType.Audio]) {
|
||||
const current = node(type, {
|
||||
content: `https://example.com/${type}`,
|
||||
prompt: "已提交提示词",
|
||||
composerContent: "旧草稿",
|
||||
status: "success",
|
||||
});
|
||||
|
||||
assert.equal(canvasNodeHasCommittedContent(current), true);
|
||||
assert.deepEqual(writeCanvasNodePrompt(current, "下一版提示词").metadata, {
|
||||
content: `https://example.com/${type}`,
|
||||
prompt: "已提交提示词",
|
||||
composerContent: "下一版提示词",
|
||||
status: "success",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("空媒体草稿同时初始化提交提示词和编辑提示词", () => {
|
||||
const current = node(CanvasNodeType.Video, { status: "idle" });
|
||||
|
||||
assert.equal(canvasNodeHasCommittedContent(current), false);
|
||||
assert.deepEqual(writeCanvasNodePrompt(current, "第一版提示词").metadata, {
|
||||
status: "idle",
|
||||
prompt: "第一版提示词",
|
||||
composerContent: "第一版提示词",
|
||||
});
|
||||
});
|
||||
|
||||
test("图片仅保留 storageKey 时仍视为已有媒体结果", () => {
|
||||
const current = node(CanvasNodeType.Image, {
|
||||
storageKey: "canvas/image/result.png",
|
||||
prompt: "已提交提示词",
|
||||
});
|
||||
|
||||
assert.equal(canvasNodeHasCommittedContent(current), true);
|
||||
assert.equal(writeCanvasNodePrompt(current, "新草稿").metadata?.prompt, "已提交提示词");
|
||||
});
|
||||
|
||||
test("文本提示词本身不构成已提交内容", () => {
|
||||
const draft = node(CanvasNodeType.Text, { prompt: "文本草稿" });
|
||||
const committed = node(CanvasNodeType.Text, { content: "已生成文本", prompt: "已提交提示词" });
|
||||
|
||||
assert.equal(canvasNodeHasCommittedContent(draft), false);
|
||||
assert.equal(canvasNodeHasCommittedContent(committed), true);
|
||||
assert.equal(writeCanvasNodePrompt(draft, "新文本草稿").metadata?.prompt, "新文本草稿");
|
||||
assert.equal(writeCanvasNodePrompt(committed, "下一版文本草稿").metadata?.prompt, "已提交提示词");
|
||||
});
|
||||
|
||||
test("调用方可先清理失败任务元数据,再由统一写入器保存草稿", () => {
|
||||
const current = node(CanvasNodeType.Video, {
|
||||
content: "https://example.com/video.mp4",
|
||||
prompt: "已提交提示词",
|
||||
promptTemplateOperation: "storyboard-video",
|
||||
promptTemplateVariables: { shot: "1" },
|
||||
taskId: "failed-task",
|
||||
});
|
||||
const metadata = { ...current.metadata, taskId: undefined };
|
||||
|
||||
const updated = writeCanvasNodePrompt(current, "修订草稿", {
|
||||
metadata,
|
||||
clearPromptTemplate: true,
|
||||
});
|
||||
|
||||
assert.equal(updated.metadata?.prompt, "已提交提示词");
|
||||
assert.equal(updated.metadata?.composerContent, "修订草稿");
|
||||
assert.equal(updated.metadata?.taskId, undefined);
|
||||
assert.equal(updated.metadata?.promptTemplateOperation, undefined);
|
||||
assert.equal(updated.metadata?.promptTemplateVariables, undefined);
|
||||
});
|
||||
37
web/src/lib/canvas/canvas-node-prompt.ts
Normal file
37
web/src/lib/canvas/canvas-node-prompt.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { getNodeResourceKind } from "@/lib/canvas/node-registry";
|
||||
import type { CanvasNodeData, CanvasNodeMetadata } from "@/types/canvas";
|
||||
|
||||
export type WriteCanvasNodePromptOptions = {
|
||||
metadata?: CanvasNodeMetadata;
|
||||
clearPromptTemplate?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A generated or authored result is committed once the node registry exposes it
|
||||
* as a resource. Text-like resources additionally require persisted content so
|
||||
* a draft prompt alone is not mistaken for a submitted result.
|
||||
*/
|
||||
export function canvasNodeHasCommittedContent(node: CanvasNodeData) {
|
||||
const resourceKind = getNodeResourceKind(node);
|
||||
if (resourceKind === "text") return Boolean(node.metadata?.content?.trim());
|
||||
return resourceKind !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the editable prompt draft without replacing the prompt snapshot that
|
||||
* produced an existing result. Callers may provide sanitized task metadata,
|
||||
* but prompt ownership remains centralized here.
|
||||
*/
|
||||
export function writeCanvasNodePrompt(node: CanvasNodeData, prompt: string, options: WriteCanvasNodePromptOptions = {}) {
|
||||
const metadata = options.metadata ?? node.metadata;
|
||||
const clearPromptTemplate = options.clearPromptTemplate ?? Boolean(metadata?.promptTemplateOperation);
|
||||
const promptTemplateMetadata = clearPromptTemplate
|
||||
? { promptTemplateOperation: undefined, promptTemplateVariables: undefined }
|
||||
: {};
|
||||
return {
|
||||
...node,
|
||||
metadata: canvasNodeHasCommittedContent(node)
|
||||
? { ...metadata, ...promptTemplateMetadata, composerContent: prompt }
|
||||
: { ...metadata, ...promptTemplateMetadata, prompt, composerContent: prompt },
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { imageReferenceLabel } from "@/lib/image-reference-prompt";
|
||||
import { canvasNodeVideoPreviewUrl, canvasVideoAssetPreviewUrl } from "@/lib/canvas/canvas-media-preview";
|
||||
import { writeCanvasNodePrompt } from "@/lib/canvas/canvas-node-prompt";
|
||||
import { getNodeResourceKind } from "@/lib/canvas/node-registry";
|
||||
import { seedanceReferenceLabel } from "@/lib/seedance-video";
|
||||
import type { Skill } from "@/services/api/skills";
|
||||
@@ -215,19 +216,6 @@ function canvasReferenceIdentityChanged(previousReferences: CanvasResourceRefere
|
||||
return previousReferences.some((reference) => nextLabelByNodeId.get(reference.nodeId) !== reference.label);
|
||||
}
|
||||
|
||||
export function writeCanvasNodePrompt(node: CanvasNodeData, prompt: string) {
|
||||
const hasExistingContent = (node.type === CanvasNodeType.Text && Boolean(node.metadata?.content?.trim())) || (node.type === CanvasNodeType.Image && Boolean(node.metadata?.content));
|
||||
const promptTemplateMetadata = node.metadata?.promptTemplateOperation
|
||||
? { promptTemplateOperation: undefined, promptTemplateVariables: undefined }
|
||||
: {};
|
||||
return {
|
||||
...node,
|
||||
metadata: hasExistingContent
|
||||
? { ...node.metadata, ...promptTemplateMetadata, composerContent: prompt }
|
||||
: { ...node.metadata, ...promptTemplateMetadata, prompt, composerContent: prompt },
|
||||
};
|
||||
}
|
||||
|
||||
function removeCanvasMentionToken(value: string, token: string) {
|
||||
return replaceCanvasMentionToken(value, token, "");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user