From 9ccf62abd4b19cc669b9e9139548e00a4c139fbc Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Thu, 16 Apr 2026 09:32:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=95=86=E9=81=93=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/flow.js | 24 ++ src/views/flow/components/ParamsDrawer.vue | 354 ++++++++++++++++++++- src/views/flow/config.js | 34 +- src/views/flow/nodes/function/index.js | 2 + src/views/flow/nodes/function/sdAgent.js | 213 +++++++++++++ src/views/flow/nodes/function/sdAgent.vue | 125 ++++++++ 6 files changed, 737 insertions(+), 15 deletions(-) create mode 100644 src/views/flow/nodes/function/sdAgent.js create mode 100644 src/views/flow/nodes/function/sdAgent.vue diff --git a/src/utils/flow.js b/src/utils/flow.js index 28268d5..c1b9351 100644 --- a/src/utils/flow.js +++ b/src/utils/flow.js @@ -342,5 +342,29 @@ export const transformHttpNodeData = (source) => { } }); + return result; +} + +export const transformSdAgentNodeData = (source) => { + // 初始化结果对象 + const result = {}; + + // 遍历每一个顶级节点 + source.forEach(item => { + const { name, children } = item; + if (!name) return; // 过滤无name的无效节点 + + // 处理普通节点(config/headers/params):直接取 children 数组 + if (name !== 'invokeTts') { + result[name] = children || []; + return; + } + + // 专门处理 tts 节点(特殊结构) + if (name === 'invokeTts') { + result.invokeTts = item.input; + } + }); + return result; } \ No newline at end of file diff --git a/src/views/flow/components/ParamsDrawer.vue b/src/views/flow/components/ParamsDrawer.vue index 4fe3502..d042e15 100644 --- a/src/views/flow/components/ParamsDrawer.vue +++ b/src/views/flow/components/ParamsDrawer.vue @@ -391,6 +391,14 @@ + + +
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ + 是否调用TTS接口 + + +
+
TTS参数设置
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + +
+ + + +
+
+
+
+
@@ -937,7 +1216,7 @@ import FormItemRecursive from "./FormItemRecursive.vue"; import { Plus, Minus } from "@element-plus/icons-vue"; import { ElMessage } from 'element-plus' -import { getInput, filterEmptyName, transformHttpNodeData } from "@/utils/flow"; +import { getInput, filterEmptyName, transformHttpNodeData, transformSdAgentNodeData } from "@/utils/flow"; import { emitter } from "@/utils/eventBus"; import * as monaco from 'monaco-editor'; import { nextTick, onUnmounted, watch } from "vue"; @@ -1009,6 +1288,42 @@ const confirm = async () => { } }); } + } else if (props.data.type === 'sdAgent') { + // 处理 sdAgent 节点的保存逻辑 + let inputValidate = true + let outputValidate = true + if (dynamicForm.value) { + await dynamicForm.value.validate((valid) => { + if (!valid) { + inputValidate = false + } + }); + } + if (outputFormRef.value) { + await outputFormRef.value.validate((valid) => { + if (!valid) { + outputValidate = false + } + }) + } + + // 验证通过 + if (inputValidate && outputValidate) { + const nodeParams = [] + nodeParams.push({ name: "config", type: "input", input: "", children: sdAgentNodeData.value.config, required: true }) + nodeParams.push({ name: "invokeTts", type: "input", input: sdAgentNodeData.value.invokeTts }) + if (sdAgentNodeData.value.invokeTts) { + nodeParams.push({ name: "tts", type: "input", input: "", children: sdAgentNodeData.value.tts }) + } + lf.setProperties(props.data.id, { + ...props.data.properties, + nodeParams, + outputParams: sdAgentNodeData.value.outputParams + }); + + emitter.emit("setProperties", { id: props.data.id}); + emits('close') + } } else { if (props.data.type === 'code') { if (hasErrors()) { @@ -1059,7 +1374,7 @@ const confirm = async () => { // 删除顶层表单项 const deleteTopLevelItem = (fullPath, propPath) => { -// 从顶层数据开始查找 + // 从顶层数据开始查找 let currentLevel = formData[propPath]; // 遍历路径(除最后一个索引,因为最后一个是要删除的项) @@ -1251,6 +1566,7 @@ watch( } ); +// HTTP 节点数据 const httpNodeData = ref({}); const addHttpNodeFormItem = (e, type) => { @@ -1276,6 +1592,30 @@ const handleHttpNodeDelete = (type, index) => { } }; +// 商道智能体 +const sdAgentNodeData = ref({}) + +const addSdAgentItem = (e) => { + e.stopPropagation(); + sdAgentNodeData.value.outputParams.push({ name: "", type: "string", input: "" }); +} + +const deleteSdAgentItem = (fullPath, propPath) => { + // 从顶层数据开始查找 + let currentLevel = sdAgentNodeData.value[propPath]; + + // 遍历路径(除最后一个索引,因为最后一个是要删除的项) + for (let i = 0; i < fullPath.length - 1; i++) { + const index = fullPath[i]; + // 进入下一层级 + currentLevel = currentLevel[index].children; + } + + // 最后一个索引是当前层级要删除的项 + const lastIndex = fullPath[fullPath.length - 1]; + currentLevel.splice(lastIndex, 1); +} + watch( () => props.data, () => { @@ -1304,7 +1644,6 @@ watch( const nodeParams = JSON.parse(JSON.stringify(props.data.properties.nodeParams || [])) const transformedData = transformHttpNodeData(nodeParams); httpNodeData.value = transformedData; - console.log('httpNodeData', httpNodeData.value); if (httpNodeData.value.body.type === 'json') { setTimeout(() => { if (editorInstance) { @@ -1312,6 +1651,15 @@ watch( } }, 100) } + } else if (props.data.type === 'sdAgent') { + const nodeParams = JSON.parse(JSON.stringify(props.data.properties.nodeParams || [])) + const outputParams = JSON.parse(JSON.stringify(props.data.properties.outputParams || [])) + const transformedData = transformSdAgentNodeData(nodeParams); + sdAgentNodeData.value = { + ...transformedData, + outputParams + }; + } }, { diff --git a/src/views/flow/config.js b/src/views/flow/config.js index f6d0b46..e124e2b 100644 --- a/src/views/flow/config.js +++ b/src/views/flow/config.js @@ -323,15 +323,15 @@ export const collapseList = [ nodeParams: [ { name: "input", type: "input", input: "", required: true }, { name: "code", type: "input", input: ` - // 方法定义不能修改 - function handler(params) { - // 返回值是一个可序列化成 json 的 dict 或 object - const result ={ - type: 2, - message: params.input - } - return result - } +// 方法定义不能修改 +function handler(params) { + // 返回值是一个可序列化成 json 的 dict 或 object + const result ={ + type: 2, + message: params.input + } + return result +} `, required: true } ], outputType: 'json', @@ -459,14 +459,24 @@ export const collapseList = [ }, { icon: dialogueSvg, name: "商道智能体", - type: "serviceNode", + type: "sdAgent", desc: "通过apiKey调用商道大模型", action: 'AI_AGENT_PLATFORM', nodeType: "LLM", outputType: 'json', nodeParams: [ - { name: 'apiKey', type: "input", input: "", disabled: true }, - { name: 'text', type: "input", input: "", disabled: true } + { name: "config", type: "input", input: "", children: [ + { name: 'apiKey', type: "input", input: "", disabled: true }, + { name: 'text', type: "input", input: "", disabled: true }, + ]}, + { name: 'invokeTts', type: "input", input: false }, + { name: 'tts', type: "input", input: "", children: [ + { name: 'url', type: "input", input: "", disabled: true }, + { name: 'text', type: "input", input: "", disabled: true }, + { name: 'speed', type: "input", input: "", max: 100, disabled: true }, + { name: 'voice', type: "input", input: "", componentType: 'select', selectOptions: voiceOptions(), disabled: true }, + { name: 'volume', type: "input", input: "", componentType: 'number', max: 100, disabled: true } + ], disabled: true } ], outputParams: [{ name: 'result', type: 'string', desc: '商道大模型的返回文案', disabled: true}] }, { diff --git a/src/views/flow/nodes/function/index.js b/src/views/flow/nodes/function/index.js index feb499d..3de78bd 100644 --- a/src/views/flow/nodes/function/index.js +++ b/src/views/flow/nodes/function/index.js @@ -7,6 +7,7 @@ import { registerSleepNode } from './sleep' import { registerHttpNode } from './httpNode' import { registerCodeNode } from './codeNode' import { registerCurrentLoopNode } from './currentLoopNode' +import { registerSdAgentNode } from './sdAgent' export const registerFunction = (lf) => { registerLoopBodyNode(lf) @@ -18,4 +19,5 @@ export const registerFunction = (lf) => { registerHttpNode(lf) registerCodeNode(lf) registerCurrentLoopNode(lf) + registerSdAgentNode(lf) } \ No newline at end of file diff --git a/src/views/flow/nodes/function/sdAgent.js b/src/views/flow/nodes/function/sdAgent.js new file mode 100644 index 0000000..7e03ca0 --- /dev/null +++ b/src/views/flow/nodes/function/sdAgent.js @@ -0,0 +1,213 @@ +// 导入 HtmlNode 及其模型,为后续继承做准备 +import { HtmlNode, HtmlNodeModel } from "@logicflow/core"; +// 导入 Vue 相关方法,用于渲染组件 +import { createApp, h, nextTick } from "vue"; +import ElementPlus from "element-plus"; +// 导入 Vue 组件 +// @ts-ignore +import SdAgent from "./sdAgent.vue"; +import OuterNode from "../../components/OuterNode.vue"; +import JsonViewer from 'vue3-json-viewer' + +/** + * 定义一个 元素的 HTML 节点类,继承自 HtmlNode + * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑 + */ +class SdAgentNodeHtmlNode extends HtmlNode { + resizeObserver = null; + isMounted; // 标记组件是否已挂载 + r; // 渲染函数 + app; // Vue 应用实例 + container = null; + + static reusePool = new Map(); // 节点复用池 + + /** + * 构造函数 + * @param props 传递给节点的属性,包括模型、图模型等 + */ + constructor(props) { + super(props); + this.initVueApp(props); + } + + initVueApp(props) { + this.isMounted = false; + + this.hideAnchor = false; + this.autoExpand = true; // 防止锚点被折叠 + this.anchorsPreset = "default"; // 重置锚点预设 + // 创建 元素的渲染函数 + this.r = h(OuterNode, { + model: props.model, + component: SdAgent, + properties: { + ...props.model.getProperties(), + }, + onContentChange: this.handleContentChange.bind(this), + onBindRef: this.handleComponentInstance.bind(this) + }); + + // 创建 Vue 应用实例,并指定渲染函数 + this.app = createApp({ + render: () => this.r, + }); + } + + /** + * 将 HTML 内容设置到指定的根元素上 + * @param rootEl 根元素 + */ + async setHtml(rootEl) { + const nodeId = this.props.model.id; + if (SdAgentNodeHtmlNode.reusePool.has(nodeId)) { + rootEl.appendChild(SdAgentNodeHtmlNode.reusePool.get(nodeId)); + return; + } + + if (!this.isMounted) { + this.isMounted = true; + this.container = document.createElement("div"); + this.container.style.display = "inline-block"; // 关键:确保容器自适应内容 + + rootEl.appendChild(this.container); + this.app.use(ElementPlus); + this.app.use(JsonViewer); + + this.app.mount(this.container); + await nextTick(); + this.setupSizeObserver(); + SdAgentNodeHtmlNode.reusePool.set(nodeId, this.container); + } else { + this.r.component.props.properties = this.props.model.getProperties(); + } + } + + /** + * 获取节点文本内容 + * 对于元素,返回 null,因为其内容由特定组件渲染 + * @returns {null} + */ + getText() { + return null; + } + + handleComponentInstance(data) { + // 将组件实例保存到节点模型 + this.props.model.setComponentInstance(data) + } + + handleContentChange() { + // 内容变化时强制更新尺寸 + this.updateNodeSize(); + } + + // 渲染完成后获取实际尺寸 + updateNodeSize() { + if (this.container) { + const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel; + const node = this.container.querySelector(".node__container"); + const rect = node.getBoundingClientRect(); + this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y); + } + } + + setupSizeObserver() { + // 首次渲染立即检测 + requestAnimationFrame(() => { + this.updateNodeSize(); + // 持续监听变化 + this.resizeObserver = new ResizeObserver(() => { + this.updateNodeSize(); + }); + this.resizeObserver.observe(this.container); + }); + } + + // 组件卸载时移除监听 + onDestroy() { + this.resizeObserver?.disconnect(); + } +} + +/** + * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel + * 该类主要设置节点的属性和样式 + */ +class SdAgentNodeHtmlModel extends HtmlNodeModel { + initNodeData(data) { + super.initNodeData(data); + } + + // 保存组件实例引用 + setComponentInstance(instance) { + this.componentInstance = instance; + } + + closePopover() { + this.componentInstance.closePopover() + } + + /** + * 设置节点属性 + * 包括宽度、高度、文本编辑属性等 + */ + setAttributes() { + // 初始设置为0,后续动态更新 + this.width = 0; + this.height = 0; + this.text.editable = false; + } + + updateSize(width, height) { + this.width = width + 24; + this.height = height + 50; + this.initNodeData(this); // 触发节点重绘 + } + + // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。 + getDefaultAnchor() { + let _a = this, + x = _a.x, + y = _a.y, + width = _a.width, + height = _a.height; + return [ + { + x: x + width / 2 + 4, + y: y, + name: "right", + id: "".concat(this.id, "_1"), + properties: { connectionType: "source" }, + }, + { + x: x - width / 2, + y: y, + name: "left", + id: "".concat(this.id, "_3"), + properties: { connectionType: "target" }, + }, + ]; + } + + /** + * 获取节点轮廓样式 + * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果 + * @returns {object} 节点轮廓样式 + */ + getOutlineStyle() { + const style = super.getOutlineStyle(); + style.stroke = "none"; + style.hover.stroke = "none"; + return style; + } +} + +// 导出方法注册 +export function registerSdAgentNode(lf) { + lf.register({ + type: "sdAgent", + view: SdAgentNodeHtmlNode, + model: SdAgentNodeHtmlModel + }); +} diff --git a/src/views/flow/nodes/function/sdAgent.vue b/src/views/flow/nodes/function/sdAgent.vue new file mode 100644 index 0000000..3a031ce --- /dev/null +++ b/src/views/flow/nodes/function/sdAgent.vue @@ -0,0 +1,125 @@ + + + + +