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 @@
+ addFormItem(e, 'outputParams')"
+ class="addFormItem"
+ >
+
+
+
+
+
+
+
+
+
+
+ 输出参数
+
+
+
+ addSdAgentItem(e)"
+ class="addFormItem"
+ >
+
+
+
+
+
+ deleteSdAgentItem(path, 'outputParams')"
+ />
+
+
+
+
+
+
保存
@@ -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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+