删除冗余的文件夹
This commit is contained in:
parent
922f2d75d4
commit
04121755b3
File diff suppressed because it is too large
Load Diff
@ -1,88 +0,0 @@
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import AIAgentFlowBuilderComponent from "./AIAgentFlowBuilder.vue";
|
||||
import { useFlowStore } from "./store/flowStore";
|
||||
import '@vue-flow/core/dist/style.css';
|
||||
import '@vue-flow/core/dist/theme-default.css';
|
||||
|
||||
class AIAgentFlowBuilder {
|
||||
constructor({ wrapper, frm, value }) {
|
||||
this.$wrapper = wrapper instanceof jQuery ? wrapper : $(wrapper);
|
||||
this.frm = frm;
|
||||
this.page = frm.page;
|
||||
this.pagetype = frm.pagetype;
|
||||
this.initialValue = value;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 设置全局智能体信息,供执行器使用
|
||||
this.setup_global_agent_info();
|
||||
this.setup_app();
|
||||
this.watch_changes();
|
||||
}
|
||||
|
||||
setup_global_agent_info() {
|
||||
// 设置全局变量,让执行器能够获取到智能体信息
|
||||
if (this.frm && this.frm.doc && this.frm.doc.name) {
|
||||
window.current_agent_name = this.frm.doc.name;
|
||||
}
|
||||
}
|
||||
|
||||
setup_app() {
|
||||
// 创建 Pinia 实例
|
||||
let pinia = createPinia();
|
||||
|
||||
// 创建 Vue 应用
|
||||
let app = createApp(AIAgentFlowBuilderComponent);
|
||||
|
||||
// 设置 Jingrow 全局属性
|
||||
SetVueGlobals(app);
|
||||
app.use(pinia);
|
||||
|
||||
// 创建 store 并传递必要的上下文
|
||||
this.store = useFlowStore();
|
||||
this.store.frm = this.frm;
|
||||
this.store.page = this.page;
|
||||
this.store.pagetype = this.pagetype;
|
||||
|
||||
// 挂载应用
|
||||
this.$flow_builder = app.mount(this.$wrapper.get(0));
|
||||
|
||||
// 加载现有数据
|
||||
this.load_agent_flow();
|
||||
}
|
||||
|
||||
async load_agent_flow() {
|
||||
try {
|
||||
|
||||
// 直接使用传入的初始值
|
||||
if (this.initialValue) {
|
||||
this.store.loadFlowData(this.initialValue);
|
||||
} else {
|
||||
// 如果没有初始值,加载空的流程数据
|
||||
this.store.loadFlowData({});
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
watch_changes() {
|
||||
// 监听 store 变化,标记表单为脏状态
|
||||
this.store.$subscribe((mutation, state) => {
|
||||
if (state.hasUnsavedChanges) {
|
||||
this.frm.dirty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 为外部调用提供 getFlowData 方法
|
||||
getFlowData() {
|
||||
return this.store.getFlowData();
|
||||
}
|
||||
}
|
||||
|
||||
// 注册到 Jingrow 框架
|
||||
jingrow.provide("jingrow.ui");
|
||||
jingrow.ui.AIAgentFlowBuilder = AIAgentFlowBuilder;
|
||||
export default AIAgentFlowBuilder;
|
||||
@ -1,107 +0,0 @@
|
||||
<template>
|
||||
<div v-if="modelValue" class="modal-mask" @click.self="onClose">
|
||||
<div class="modal-wrapper">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h4>{{ title }}</h4>
|
||||
<button class="modal-close" @click="onClose">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<slot name="footer">
|
||||
<button class="btn btn-primary" @click="onClose">确定</button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
function onClose() {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.18);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal-wrapper {
|
||||
width: 400px;
|
||||
max-width: 98vw;
|
||||
max-height: 90vh;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.18);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.modal-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 24px 12px 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.modal-header h4 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 28px;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
}
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
max-height: 60vh;
|
||||
min-height: 80px;
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
.btn {
|
||||
padding: 6px 18px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #222;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@ -1,235 +0,0 @@
|
||||
<template>
|
||||
<Teleport :to="computedTeleportTo">
|
||||
<div v-if="modelValue" class="node-property-modal-mask" @click.self="onClose">
|
||||
<div class="node-property-modal-content" :style="modalStyle">
|
||||
<div class="node-property-modal-header">
|
||||
<slot name="header">
|
||||
<h4>{{ computedTitle }}</h4>
|
||||
</slot>
|
||||
<button class="modal-close" @click="onClose">×</button>
|
||||
</div>
|
||||
<div class="node-property-modal-body">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="node-property-modal-footer">
|
||||
<slot name="footer">
|
||||
<button class="btn btn-secondary" @click="onClose">取消</button>
|
||||
<button class="btn btn-primary" @click="$emit('save')">保存</button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
title: { type: String, default: '' },
|
||||
nodeLabel: { type: String, default: '' },
|
||||
width: { type: [String, Number], default: 900 },
|
||||
maxHeight: { type: [String, Number], default: '90vh' },
|
||||
teleportTo: { type: [String, Object], default: null }
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue', 'close', 'save']);
|
||||
|
||||
// 计算标题:如果传入了title则使用,否则自动生成"节点属性配置-{节点名称}"格式
|
||||
const computedTitle = computed(() => {
|
||||
if (props.title) {
|
||||
return props.title;
|
||||
}
|
||||
return `节点属性-${props.nodeLabel || '节点'}`;
|
||||
});
|
||||
|
||||
// 动态计算 teleport 目标
|
||||
const computedTeleportTo = computed(() => {
|
||||
// 优先使用传入的 teleportTo prop
|
||||
if (props.teleportTo) {
|
||||
return props.teleportTo;
|
||||
}
|
||||
// 其次使用全局的 teleport 目标
|
||||
if (window.nodePropertyTeleportTarget) {
|
||||
return window.nodePropertyTeleportTarget.value;
|
||||
}
|
||||
// 最后使用默认值
|
||||
return '#ai-agent-flow-builder-container';
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
emit('update:modelValue', false);
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const modalStyle = computed(() => ({
|
||||
width: typeof props.width === 'number' ? props.width + 'px' : props.width,
|
||||
maxWidth: '98vw',
|
||||
maxHeight: typeof props.maxHeight === 'number' ? props.maxHeight + 'px' : props.maxHeight,
|
||||
background: 'white',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.node-property-modal-mask {
|
||||
position: fixed;
|
||||
z-index: 99999;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.18);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.node-property-modal-content {
|
||||
min-width: 420px;
|
||||
max-width: 98vw;
|
||||
max-height: 90vh;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.18);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.node-property-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 24px 8px 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.node-property-modal-header h4 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
}
|
||||
.node-property-modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 24px;
|
||||
max-height: 60vh;
|
||||
min-height: 120px;
|
||||
}
|
||||
.node-property-modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
.btn {
|
||||
padding: 6px 18px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #222;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #f3f4f6;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
/* 合并全局表单样式,仅作用于弹窗内容 */
|
||||
.node-property-modal-content .property-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.node-property-modal-content .property-section h5 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.node-property-modal-content .form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.node-property-modal-content .form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
}
|
||||
.node-property-modal-content .form-control {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.node-property-modal-content .form-control:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
.node-property-modal-content .form-control::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
.node-property-modal-content textarea.form-control {
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
}
|
||||
.node-property-modal-content select.form-control {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 8px center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px 12px;
|
||||
padding-right: 32px;
|
||||
appearance: none;
|
||||
}
|
||||
.node-property-modal-content .form-control-sm {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.node-property-modal-content .table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.node-property-modal-content .table th,
|
||||
.node-property-modal-content .table td {
|
||||
padding: 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
text-align: left;
|
||||
}
|
||||
.node-property-modal-content .table th {
|
||||
background-color: #f9fafb;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
.node-property-modal-content .table td {
|
||||
font-size: 12px;
|
||||
}
|
||||
.node-property-modal-content .btn-danger {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
}
|
||||
.node-property-modal-content .btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
.node-property-modal-content .form-text {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,85 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { BaseEdge, EdgeLabelRenderer, getBezierPath, useVueFlow } from '@vue-flow/core'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
sourceX: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
sourceY: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
targetX: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
targetY: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
sourcePosition: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
targetPosition: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
markerEnd: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
style: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const { removeEdges } = useVueFlow()
|
||||
|
||||
const path = computed(() => getBezierPath(props))
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseEdge :path="path[0]" />
|
||||
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
:style="{
|
||||
pointerEvents: 'all',
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)`,
|
||||
}"
|
||||
class="nodrag nopan"
|
||||
>
|
||||
<button class="edgebutton" @click="removeEdges(id)">×</button>
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.edgebutton {
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edgebutton:hover {
|
||||
box-shadow: 0 0 0 2px pink, 0 0 0 4px #f05f75;
|
||||
}
|
||||
</style>
|
||||
@ -1,462 +0,0 @@
|
||||
<template>
|
||||
<div class="execution-results">
|
||||
<div class="execution-results-body">
|
||||
<!-- 节点详情区域 -->
|
||||
<div class="execution-details">
|
||||
<div class="details-left">
|
||||
<div class="history-list">
|
||||
<!-- 执行状态卡片 -->
|
||||
<div v-if="executionResult && executionResult.success" class="success-overview-card">
|
||||
<div class="overview-card-header">
|
||||
<i class="fa fa-check-circle text-success"></i>
|
||||
<div class="overview-info-header">
|
||||
<h5>执行成功</h5>
|
||||
<p>执行了 {{ executionHistory.length }} 个节点</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="executionResult && !executionResult.success" class="error-overview-card">
|
||||
<div class="overview-card-header">
|
||||
<i class="fa fa-exclamation-circle text-danger"></i>
|
||||
<div class="overview-info-header">
|
||||
<h5>执行失败</h5>
|
||||
<p>{{ executionResult.error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-execution-overview-card">
|
||||
<div class="overview-card-header">
|
||||
<i class="fa fa-info-circle text-info"></i>
|
||||
<div class="overview-info-header">
|
||||
<h5>无执行历史</h5>
|
||||
<p>请先执行流程查看结果</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 执行历史列表 -->
|
||||
<div v-if="executionHistory && executionHistory.length > 0">
|
||||
<div
|
||||
v-for="(item, index) in executionHistory"
|
||||
:key="index"
|
||||
class="history-item"
|
||||
:class="[item.status, { active: selectedNodeDetail?.nodeId === item.nodeId }]"
|
||||
@click="selectNode(item)"
|
||||
>
|
||||
<div class="history-item-header">
|
||||
<span class="history-node-name">
|
||||
{{ item.nodeLabel || (getNodeMetadataByType(item.nodeType)?.label || item.nodeType) }}
|
||||
</span>
|
||||
<span class="history-status-badge" :class="item.status">
|
||||
{{ __(item.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="history-item-meta">
|
||||
<span class="history-time">{{ new Date(item.timestamp).toLocaleTimeString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-history">
|
||||
<div class="no-history-content">
|
||||
<i class="fa fa-history"></i>
|
||||
<p>暂无执行历史</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details-right" v-if="selectedNodeDetail">
|
||||
<div class="node-detail-header">
|
||||
<h6>{{ selectedNodeDetail.nodeLabel || (getNodeMetadataByType(selectedNodeDetail.nodeType)?.label || selectedNodeDetail.nodeType) }}</h6>
|
||||
<div class="detail-tabs">
|
||||
<button
|
||||
class="tab-btn"
|
||||
:class="{ active: nodeDetailTab === 'input' }"
|
||||
@click="nodeDetailTab = 'input'"
|
||||
>
|
||||
输入
|
||||
</button>
|
||||
<button
|
||||
class="tab-btn"
|
||||
:class="{ active: nodeDetailTab === 'output' }"
|
||||
@click="nodeDetailTab = 'output'"
|
||||
>
|
||||
输出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-detail-content">
|
||||
<div v-if="nodeDetailTab === 'input'" class="detail-panel">
|
||||
<button
|
||||
v-if="selectedNodeDetail.inputs && Object.keys(selectedNodeDetail.inputs).length > 0"
|
||||
class="copy-btn-floating"
|
||||
@click="copyContent(selectedNodeDetail.inputs)"
|
||||
:title="__('复制')"
|
||||
>
|
||||
<i class="fa fa-copy"></i>
|
||||
</button>
|
||||
<pre class="json-viewer">{{ JSON.stringify(formatDisplayContent(selectedNodeDetail.inputs || {}), null, 2) }}</pre>
|
||||
</div>
|
||||
<div v-if="nodeDetailTab === 'output'" class="detail-panel">
|
||||
<button
|
||||
v-if="selectedNodeDetail.result && Object.keys(selectedNodeDetail.result).length > 0"
|
||||
class="copy-btn-floating"
|
||||
@click="copyContent(selectedNodeDetail.result)"
|
||||
:title="__('复制')"
|
||||
>
|
||||
<i class="fa fa-copy"></i>
|
||||
</button>
|
||||
<pre class="json-viewer">{{ JSON.stringify(formatDisplayContent(selectedNodeDetail.result || {}), null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="details-right empty">
|
||||
<div class="empty-state">
|
||||
<i class="fa fa-mouse-pointer"></i>
|
||||
<p>点击左侧节点查看详情</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 复制提示 -->
|
||||
<transition name="fade">
|
||||
<div v-if="showCopyTip" class="copy-tip">{{ __('已复制') }}</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { getNodeMetadataByType } from '../utils/nodeMetadata';
|
||||
|
||||
const props = defineProps({
|
||||
executionResult: Object,
|
||||
executionHistory: Array
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const selectedNodeDetail = ref(null);
|
||||
const nodeDetailTab = ref('output'); // 'input' | 'output'
|
||||
const showCopyTip = ref(false);
|
||||
|
||||
const selectNode = (item) => {
|
||||
selectedNodeDetail.value = item;
|
||||
nodeDetailTab.value = 'output';
|
||||
};
|
||||
|
||||
const copyContent = async (content) => {
|
||||
try {
|
||||
const text = JSON.stringify(content, null, 2);
|
||||
await navigator.clipboard.writeText(text);
|
||||
showCopyTip.value = true;
|
||||
setTimeout(() => { showCopyTip.value = false; }, 2000);
|
||||
} catch (e) {
|
||||
jingrow.msgprint(__('复制失败,请手动复制'), 'Error');
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化显示内容,解析JSON字符串字段
|
||||
const formatDisplayContent = (content) => {
|
||||
if (typeof content !== 'object' || content === null) return content;
|
||||
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(content)) {
|
||||
if (typeof value === 'string' && value.trim().startsWith('{')) {
|
||||
try {
|
||||
result[key] = JSON.parse(value);
|
||||
} catch {
|
||||
result[key] = value;
|
||||
}
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.execution-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: white;
|
||||
}
|
||||
|
||||
|
||||
.overview-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
background: #f9fafb;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.overview-card-header i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.overview-info-header h5 {
|
||||
margin: 0 0 2px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.overview-info-header p {
|
||||
margin: 0;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
||||
.execution-results-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* 执行详情区域 */
|
||||
.execution-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.details-left {
|
||||
width: 300px;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.history-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
padding: 8px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.history-item.active {
|
||||
border-color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.history-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.history-node-name {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.history-status-badge {
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.history-item.success .history-status-badge {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.history-item.failed .history-status-badge {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.history-item-meta {
|
||||
font-size: 10px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.no-history {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.no-history-content {
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.no-history-content i {
|
||||
font-size: 32px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.no-history-content p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 右侧详情区域 */
|
||||
.details-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.details-right.empty {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.node-detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.node-detail-header h6 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: #374151;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #3b82f6;
|
||||
border-bottom-color: #3b82f6;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.node-detail-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.copy-btn-floating {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6B7280;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
border-radius: 5px;
|
||||
transition: background 0.15s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.copy-btn-floating:hover {
|
||||
background: #f3f3f3;
|
||||
color: #1fc76f;
|
||||
}
|
||||
|
||||
.json-viewer {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
background: white;
|
||||
color: #374151;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.copy-tip {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 60px;
|
||||
background: #fff;
|
||||
color: #222;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
font-size: 12px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -1,363 +0,0 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
import { ref, watch, onBeforeUnmount, computed } from 'vue';
|
||||
import { useFlowStore } from '../../store/flowStore';
|
||||
import NodePropertyModal from '../Common/NodePropertyModal.vue';
|
||||
import SchemaFormRenderer from '../Common/SchemaFormRenderer.vue';
|
||||
import { createSchemaLoader } from '../../utils/schemaLoader';
|
||||
import { getNodeMetadataByType } from '../../utils/nodeMetadata';
|
||||
|
||||
const props = defineProps({
|
||||
id: String,
|
||||
data: Object,
|
||||
selected: Boolean,
|
||||
type: String
|
||||
});
|
||||
|
||||
const emit = defineEmits(['edit-node']);
|
||||
|
||||
// 状态管理
|
||||
const flowStore = useFlowStore();
|
||||
const hover = ref(false);
|
||||
const showModal = ref(false);
|
||||
const localData = ref({});
|
||||
const configData = ref({});
|
||||
const formSchema = ref({});
|
||||
const schemaLoading = ref(false);
|
||||
|
||||
// 计算属性
|
||||
const nodeMetadata = computed(() => getNodeMetadataByType(props.type));
|
||||
const schemaLoader = computed(() => createSchemaLoader(props.type));
|
||||
|
||||
// 监听数据变化 - 完全动态的配置处理,支持任意节点类型
|
||||
watch(() => props.data, (newData) => {
|
||||
if (newData) {
|
||||
localData.value = {
|
||||
label: newData.label || nodeMetadata.value?.label || props.type,
|
||||
...newData.config || {}
|
||||
};
|
||||
// 保持configData用于其他用途(如果需要)
|
||||
configData.value = newData.config || {};
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 事件处理
|
||||
async function handleEdit() {
|
||||
try {
|
||||
await loadSchema();
|
||||
showModal.value = true;
|
||||
} catch (error) {
|
||||
console.error('配置加载失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSchemaUpdate(newValue) {
|
||||
localData.value = { ...localData.value, ...newValue };
|
||||
}
|
||||
|
||||
// 保存属性配置 - 动态提取所有配置字段,支持任意节点类型
|
||||
function saveProperties(close = true) {
|
||||
const { label, ...configFields } = localData.value;
|
||||
|
||||
try {
|
||||
flowStore.updateNodeData(props.id, {
|
||||
label: label,
|
||||
config: configFields
|
||||
});
|
||||
|
||||
if (close) showModal.value = false;
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 键盘快捷键
|
||||
function handleKeydown(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
saveProperties(false);
|
||||
window.saveAgentFlow?.();
|
||||
}
|
||||
}
|
||||
|
||||
watch(showModal, (val) => {
|
||||
if (val) {
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
} else {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
}
|
||||
});
|
||||
|
||||
// Schema加载
|
||||
async function loadSchema() {
|
||||
if (!schemaLoader.value) return;
|
||||
|
||||
schemaLoading.value = true;
|
||||
try {
|
||||
formSchema.value = await schemaLoader.value();
|
||||
} finally {
|
||||
schemaLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 清理
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="node"
|
||||
:class="{ selected: props.selected }"
|
||||
:style="{
|
||||
borderColor: props.selected ? (nodeMetadata?.color || '#6b7280') : '#cbd5e1',
|
||||
boxShadow: props.selected ? `0 0 0 1.5px ${nodeMetadata?.color || '#6b7280'}` : '0 1px 4px #e5e7eb'
|
||||
}"
|
||||
@mouseenter="hover = true"
|
||||
@mouseleave="hover = false"
|
||||
@dblclick.stop="handleEdit"
|
||||
>
|
||||
<!-- 输入连接点 -->
|
||||
<Handle
|
||||
v-if="!localData.hide_input_handle"
|
||||
type="target"
|
||||
id="input"
|
||||
:position="Position.Left"
|
||||
/>
|
||||
|
||||
<!-- 节点内容 -->
|
||||
<div class="node-content">
|
||||
<i
|
||||
v-if="nodeMetadata?.icon"
|
||||
:class="`fa ${nodeMetadata.icon}`"
|
||||
:style="{ color: nodeMetadata?.color || '#6b7280' }"
|
||||
/>
|
||||
<span>{{ localData.label || nodeMetadata?.label || props.type }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 输出连接点 -->
|
||||
<Handle
|
||||
v-if="localData.show_output_handle !== false"
|
||||
type="source"
|
||||
id="output"
|
||||
:position="Position.Right"
|
||||
/>
|
||||
|
||||
<!-- 编辑按钮 -->
|
||||
<button
|
||||
v-if="hover"
|
||||
class="edit-btn"
|
||||
@click="handleEdit"
|
||||
:disabled="schemaLoading"
|
||||
>
|
||||
<i :class="schemaLoading ? 'fa fa-spinner fa-spin' : 'fa fa-edit'"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 配置弹窗 -->
|
||||
<NodePropertyModal
|
||||
v-model="showModal"
|
||||
:nodeLabel="localData.label || nodeMetadata?.label || props.type"
|
||||
@save="saveProperties"
|
||||
@close="showModal = false"
|
||||
>
|
||||
<template #default>
|
||||
<!-- 基本信息 -->
|
||||
<div class="section">
|
||||
<h5>基本信息</h5>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>节点名称</label>
|
||||
<input
|
||||
v-model="localData.label"
|
||||
class="form-control"
|
||||
placeholder="输入节点名称"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>节点ID</label>
|
||||
<input
|
||||
:value="props.id"
|
||||
class="form-control"
|
||||
readonly
|
||||
style="background-color: #f8f9fa; color: #6b7280;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 节点配置 -->
|
||||
<div class="section">
|
||||
<h5>节点配置</h5>
|
||||
|
||||
<div v-if="schemaLoading" class="loading">
|
||||
<i class="fa fa-spinner fa-spin"></i> 正在加载...
|
||||
</div>
|
||||
|
||||
<SchemaFormRenderer
|
||||
v-else-if="formSchema.properties"
|
||||
:schema="formSchema"
|
||||
:modelValue="localData"
|
||||
:flowStore="flowStore"
|
||||
@update:modelValue="handleSchemaUpdate"
|
||||
/>
|
||||
|
||||
<div v-else class="no-config">
|
||||
此节点暂无可配置项
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</NodePropertyModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.node {
|
||||
width: 130px;
|
||||
height: 50px;
|
||||
border: 1.5px solid;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: #374151;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.node-content i {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.node-content span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.edit-btn:hover:not(:disabled) {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.edit-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.section h5 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin: 16px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.form-row {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.no-config {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
@ -1,451 +0,0 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
import { ref, watch, onBeforeUnmount, computed } from 'vue';
|
||||
import { useFlowStore } from '../../store/flowStore';
|
||||
import NodePropertyModal from '../Common/NodePropertyModal.vue';
|
||||
import SchemaFormRenderer from '../Common/SchemaFormRenderer.vue';
|
||||
import { createSchemaLoader } from '../../utils/schemaLoader';
|
||||
import { getNodeMetadataByType } from '../../utils/nodeMetadata';
|
||||
|
||||
const props = defineProps({
|
||||
id: String,
|
||||
data: Object,
|
||||
selected: Boolean,
|
||||
type: String
|
||||
});
|
||||
|
||||
const emit = defineEmits(['edit-node']);
|
||||
|
||||
// 状态管理
|
||||
const flowStore = useFlowStore();
|
||||
const hover = ref(false);
|
||||
const showModal = ref(false);
|
||||
const localData = ref({});
|
||||
const configData = ref({});
|
||||
const formSchema = ref({});
|
||||
const schemaLoading = ref(false);
|
||||
|
||||
// 计算属性
|
||||
const nodeMetadata = computed(() => getNodeMetadataByType(props.type));
|
||||
const schemaLoader = computed(() => createSchemaLoader(props.type));
|
||||
|
||||
// 监听数据变化 - 完全动态的配置处理,支持任意节点类型
|
||||
watch(() => props.data, (newData) => {
|
||||
if (newData) {
|
||||
localData.value = {
|
||||
label: newData.label || nodeMetadata.value?.label || props.type,
|
||||
...newData.config || {}
|
||||
};
|
||||
// 保持configData用于其他用途(如果需要)
|
||||
configData.value = newData.config || {};
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 事件处理
|
||||
async function handleEdit() {
|
||||
try {
|
||||
await loadSchema();
|
||||
showModal.value = true;
|
||||
} catch (error) {
|
||||
console.error('配置加载失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSchemaUpdate(newValue) {
|
||||
localData.value = { ...localData.value, ...newValue };
|
||||
}
|
||||
|
||||
// 保存属性配置 - 动态提取所有配置字段,支持任意节点类型
|
||||
function saveProperties(close = true) {
|
||||
const { label, ...configFields } = localData.value;
|
||||
|
||||
try {
|
||||
flowStore.updateNodeData(props.id, {
|
||||
label: label,
|
||||
config: configFields
|
||||
});
|
||||
|
||||
if (close) showModal.value = false;
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 键盘快捷键
|
||||
function handleKeydown(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
saveProperties(false);
|
||||
window.saveAgentFlow?.();
|
||||
}
|
||||
}
|
||||
|
||||
watch(showModal, (val) => {
|
||||
if (val) {
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
} else {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
}
|
||||
});
|
||||
|
||||
// Schema加载
|
||||
async function loadSchema() {
|
||||
if (!schemaLoader.value) return;
|
||||
|
||||
schemaLoading.value = true;
|
||||
try {
|
||||
formSchema.value = await schemaLoader.value();
|
||||
} finally {
|
||||
schemaLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 清理
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="node"
|
||||
:class="{ selected: props.selected }"
|
||||
:style="{
|
||||
borderColor: props.selected ? (nodeMetadata?.color || '#6b7280') : '#cbd5e1',
|
||||
boxShadow: props.selected ? `0 0 0 1.5px ${nodeMetadata?.color || '#6b7280'}` : '0 1px 4px #e5e7eb'
|
||||
}"
|
||||
@mouseenter="hover = true"
|
||||
@mouseleave="hover = false"
|
||||
@dblclick.stop="handleEdit"
|
||||
>
|
||||
<!-- 输入连接点 -->
|
||||
<Handle
|
||||
v-if="!localData.hide_input_handle"
|
||||
type="target"
|
||||
id="input"
|
||||
:position="Position.Left"
|
||||
/>
|
||||
|
||||
<!-- 节点内容 -->
|
||||
<div class="node-content">
|
||||
<i
|
||||
v-if="nodeMetadata?.icon"
|
||||
:class="`fa ${nodeMetadata.icon}`"
|
||||
:style="{ color: nodeMetadata?.color || '#6b7280' }"
|
||||
/>
|
||||
<span>{{ localData.label || nodeMetadata?.label || props.type }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 多输出连接点 -->
|
||||
<Handle
|
||||
type="source"
|
||||
id="true_output"
|
||||
:position="Position.Right"
|
||||
:style="{ top: '20%' }"
|
||||
class="handle-true"
|
||||
title="True输出"
|
||||
/>
|
||||
|
||||
<Handle
|
||||
type="source"
|
||||
id="output"
|
||||
:position="Position.Right"
|
||||
:style="{ top: '50%' }"
|
||||
class="handle-main"
|
||||
title="主流程输出"
|
||||
/>
|
||||
|
||||
<Handle
|
||||
type="source"
|
||||
id="false_output"
|
||||
:position="Position.Right"
|
||||
:style="{ top: '80%' }"
|
||||
class="handle-false"
|
||||
title="False输出"
|
||||
/>
|
||||
|
||||
<!-- 编辑按钮 -->
|
||||
<button
|
||||
v-if="hover"
|
||||
class="edit-btn"
|
||||
@click="handleEdit"
|
||||
:disabled="schemaLoading"
|
||||
>
|
||||
<i :class="schemaLoading ? 'fa fa-spinner fa-spin' : 'fa fa-edit'"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 配置弹窗 -->
|
||||
<NodePropertyModal
|
||||
v-model="showModal"
|
||||
:nodeLabel="localData.label || nodeMetadata?.label || props.type"
|
||||
@save="saveProperties"
|
||||
@close="showModal = false"
|
||||
>
|
||||
<template #default>
|
||||
<!-- 基本信息 -->
|
||||
<div class="section">
|
||||
<h5>基本信息</h5>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>节点名称</label>
|
||||
<input
|
||||
v-model="localData.label"
|
||||
class="form-control"
|
||||
placeholder="输入节点名称"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>节点ID</label>
|
||||
<input
|
||||
:value="props.id"
|
||||
class="form-control"
|
||||
readonly
|
||||
style="background-color: #f8f9fa; color: #6b7280;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 节点配置 -->
|
||||
<div class="section">
|
||||
<h5>节点配置</h5>
|
||||
|
||||
<div v-if="schemaLoading" class="loading">
|
||||
<i class="fa fa-spinner fa-spin"></i> 正在加载...
|
||||
</div>
|
||||
|
||||
<SchemaFormRenderer
|
||||
v-else-if="formSchema.properties"
|
||||
:schema="formSchema"
|
||||
:modelValue="localData"
|
||||
:flowStore="flowStore"
|
||||
@update:modelValue="handleSchemaUpdate"
|
||||
/>
|
||||
|
||||
<div v-else class="no-config">
|
||||
此节点暂无可配置项
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</NodePropertyModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.node {
|
||||
width: 130px;
|
||||
height: 50px;
|
||||
border: 1.5px solid;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: #374151;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.node-content i {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.node-content span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.edit-btn:hover:not(:disabled) {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.edit-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.section h5 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin: 16px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.form-row {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.no-config {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 多输出接口样式 */
|
||||
.handle-true,
|
||||
.handle-true.vue-flow__handle {
|
||||
background: #fff !important;
|
||||
border: 2.5px solid #10b981 !important;
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.15) !important;
|
||||
transition: all 0.2s ease;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
}
|
||||
|
||||
.handle-true:hover,
|
||||
.handle-true.vue-flow__handle:hover {
|
||||
background: #f0fdf4 !important;
|
||||
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2) !important;
|
||||
border-color: #059669 !important;
|
||||
}
|
||||
|
||||
.handle-main,
|
||||
.handle-main.vue-flow__handle {
|
||||
background: #fff !important;
|
||||
border: 2.5px solid #3b82f6 !important;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15) !important;
|
||||
transition: all 0.2s ease;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
}
|
||||
|
||||
.handle-main:hover,
|
||||
.handle-main.vue-flow__handle:hover {
|
||||
background: #eff6ff !important;
|
||||
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2) !important;
|
||||
border-color: #2563eb !important;
|
||||
}
|
||||
|
||||
.handle-false,
|
||||
.handle-false.vue-flow__handle {
|
||||
background: #fff !important;
|
||||
border: 2.5px solid #ef4444 !important;
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.15) !important;
|
||||
transition: all 0.2s ease;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
}
|
||||
|
||||
.handle-false:hover,
|
||||
.handle-false.vue-flow__handle:hover {
|
||||
background: #fef2f2 !important;
|
||||
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.2) !important;
|
||||
border-color: #dc2626 !important;
|
||||
}
|
||||
|
||||
/* 接口连接线样式 */
|
||||
.handle-true + .vue-flow__edge-path {
|
||||
stroke: #10b981;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.handle-main + .vue-flow__edge-path {
|
||||
stroke: #3b82f6;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.handle-false + .vue-flow__edge-path {
|
||||
stroke: #ef4444;
|
||||
stroke-width: 2;
|
||||
}
|
||||
</style>
|
||||
@ -1,529 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { useFlowStore } from '../../store/flowStore';
|
||||
import { NODE_GROUPS, getAllNodeTypes } from '../../utils/nodeMetadata';
|
||||
|
||||
// 从统一管理文件获取所有节点类型
|
||||
const ALL_NODE_TYPES = getAllNodeTypes();
|
||||
|
||||
const flowStore = useFlowStore();
|
||||
const draggedNode = ref(null);
|
||||
const searchText = ref('');
|
||||
const activeTab = ref('all'); // all, recent, favorite
|
||||
const favorites = ref(JSON.parse(localStorage.getItem('ai-agent-node-favorites') || '[]'));
|
||||
const recent = ref(JSON.parse(localStorage.getItem('ai-agent-node-recent') || '[]'));
|
||||
const tooltip = ref({ show: false, text: '', x: 0, y: 0, width: 200, direction: 'left' });
|
||||
const isFullscreen = ref(false);
|
||||
let tooltipTimer = null;
|
||||
|
||||
// 监听 recent-node-updated 事件,收到后刷新 recent
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('recent-node-updated', () => {
|
||||
recent.value = JSON.parse(localStorage.getItem('ai-agent-node-recent') || '[]');
|
||||
});
|
||||
}
|
||||
|
||||
// 监听全屏状态变化
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('fullscreenchange', () => {
|
||||
isFullscreen.value = !!(
|
||||
document.fullscreenElement ||
|
||||
document.webkitFullscreenElement ||
|
||||
document.mozFullScreenElement ||
|
||||
document.msFullscreenElement
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 拖拽事件
|
||||
const onDragStart = (event, nodeType) => {
|
||||
draggedNode.value = nodeType;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('application/vueflow', nodeType.type);
|
||||
event.dataTransfer.setData('text/plain', nodeType.type);
|
||||
};
|
||||
const onDragEnd = () => { draggedNode.value = null; };
|
||||
|
||||
function getCanvasCenter() {
|
||||
const vueFlowRoot = document.querySelector('.vue-flow-container');
|
||||
if (!vueFlowRoot) return { x: 200, y: 200 };
|
||||
const rect = vueFlowRoot.getBoundingClientRect();
|
||||
const centerX = rect.width / 2 + vueFlowRoot.scrollLeft;
|
||||
const centerY = rect.height / 2 + vueFlowRoot.scrollTop;
|
||||
if (window.vueFlowInstance && typeof window.vueFlowInstance.project === 'function') {
|
||||
const pos = window.vueFlowInstance.project({ x: centerX, y: centerY });
|
||||
pos.x += Math.random() * 20 - 10;
|
||||
pos.y += Math.random() * 20 - 10;
|
||||
return pos;
|
||||
}
|
||||
return { x: centerX, y: centerY };
|
||||
}
|
||||
|
||||
const addNode = (nodeType) => {
|
||||
const position = getCanvasCenter();
|
||||
const newNode = {
|
||||
id: `${nodeType.type}-${Date.now()}`,
|
||||
type: nodeType.type,
|
||||
position,
|
||||
data: {}
|
||||
};
|
||||
flowStore.addNode(newNode);
|
||||
flowStore.selectNode(newNode.id);
|
||||
// 记录最近使用
|
||||
addRecent(nodeType.type);
|
||||
};
|
||||
|
||||
function addFavorite(type) {
|
||||
if (!favorites.value.includes(type)) {
|
||||
favorites.value.push(type);
|
||||
localStorage.setItem('ai-agent-node-favorites', JSON.stringify(favorites.value));
|
||||
}
|
||||
}
|
||||
function removeFavorite(type) {
|
||||
favorites.value = favorites.value.filter(t => t !== type);
|
||||
localStorage.setItem('ai-agent-node-favorites', JSON.stringify(favorites.value));
|
||||
}
|
||||
function toggleFavorite(type) {
|
||||
if (favorites.value.includes(type)) removeFavorite(type);
|
||||
else addFavorite(type);
|
||||
}
|
||||
function addRecent(type) {
|
||||
recent.value = [type, ...recent.value.filter(t => t !== type)].slice(0, 12);
|
||||
localStorage.setItem('ai-agent-node-recent', JSON.stringify(recent.value));
|
||||
}
|
||||
|
||||
const nodeTypeCount = computed(() => {
|
||||
const counts = {};
|
||||
flowStore.nodes.forEach(node => {
|
||||
counts[node.type] = (counts[node.type] || 0) + 1;
|
||||
});
|
||||
return counts;
|
||||
});
|
||||
|
||||
const allNodeTypes = computed(() => {
|
||||
let list = ALL_NODE_TYPES;
|
||||
if (searchText.value.trim()) {
|
||||
const kw = searchText.value.trim().toLowerCase();
|
||||
list = list.filter(n => n.label.toLowerCase().includes(kw) || n.type.toLowerCase().includes(kw));
|
||||
}
|
||||
return list;
|
||||
});
|
||||
|
||||
const filteredNodeTypes = computed(() => {
|
||||
let list = ALL_NODE_TYPES;
|
||||
if (searchText.value.trim()) {
|
||||
const kw = searchText.value.trim().toLowerCase();
|
||||
list = list.filter(n => n.label.toLowerCase().includes(kw) || n.type.toLowerCase().includes(kw));
|
||||
}
|
||||
if (activeTab.value === 'favorite') {
|
||||
list = list.filter(n => favorites.value.includes(n.type));
|
||||
} else if (activeTab.value === 'recent') {
|
||||
list = recent.value.map(type => list.find(n => n.type === type)).filter(Boolean);
|
||||
}
|
||||
return list;
|
||||
});
|
||||
|
||||
const groupedNodeTypes = computed(() => {
|
||||
// 按分组返回 {group, label, nodes:[]} 数组
|
||||
const groups = [];
|
||||
NODE_GROUPS.forEach(g => {
|
||||
const nodes = filteredNodeTypes.value.filter(n => n.group === g.group || (!n.group && g.group === '其他'));
|
||||
if (nodes.length) groups.push({ ...g, nodes });
|
||||
});
|
||||
// 兜底:如果还有未分组节点,归入“其他”
|
||||
const otherNodes = filteredNodeTypes.value.filter(n => !n.group || !NODE_GROUPS.some(g => g.group === n.group));
|
||||
if (otherNodes.length && !groups.some(g => g.group === '其他')) {
|
||||
groups.push({ group: '其他', label: '其他', nodes: otherNodes });
|
||||
}
|
||||
return groups;
|
||||
});
|
||||
|
||||
function showTooltip(event, text) {
|
||||
tooltip.value.show = true;
|
||||
tooltip.value.text = text;
|
||||
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
const tooltipWidth = 200;
|
||||
const tooltipHeight = 38;
|
||||
const gap = 8;
|
||||
|
||||
// 默认向左弹出,垂直居中
|
||||
let x = rect.left - tooltipWidth - gap;
|
||||
let y = rect.top + (rect.height / 2) - (tooltipHeight / 2);
|
||||
let direction = 'left';
|
||||
|
||||
// 边界处理
|
||||
if (x < 8) {
|
||||
// 左侧空间不足,向右弹出
|
||||
x = rect.right + gap;
|
||||
direction = 'right';
|
||||
}
|
||||
// 上下边界
|
||||
const minY = 8;
|
||||
const maxY = window.innerHeight - tooltipHeight - 8;
|
||||
if (y < minY) y = minY;
|
||||
if (y > maxY) y = maxY;
|
||||
|
||||
tooltip.value.x = x;
|
||||
tooltip.value.y = y;
|
||||
tooltip.value.width = tooltipWidth;
|
||||
tooltip.value.direction = direction;
|
||||
}
|
||||
function showTooltipWithDelay(event, text) {
|
||||
clearTimeout(tooltipTimer);
|
||||
tooltipTimer = setTimeout(() => showTooltip(event, text), 200);
|
||||
}
|
||||
function hideTooltip() {
|
||||
clearTimeout(tooltipTimer);
|
||||
tooltip.value.show = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="node-palette">
|
||||
<div class="palette-header">
|
||||
<div class="palette-tabs">
|
||||
<button :class="['tab-btn', {active: activeTab==='all'}]" @click="activeTab='all'">全部</button>
|
||||
<button :class="['tab-btn', {active: activeTab==='recent'}]" @click="activeTab='recent'">最近使用</button>
|
||||
<button :class="['tab-btn', {active: activeTab==='favorite'}]" @click="activeTab='favorite'">收藏</button>
|
||||
</div>
|
||||
<input v-model="searchText" class="palette-search" placeholder="搜索节点类型/名称..." />
|
||||
</div>
|
||||
<div class="palette-content">
|
||||
<template v-if="activeTab==='all'">
|
||||
<div v-for="group in groupedNodeTypes" :key="group.group" class="node-group">
|
||||
<div class="group-label">{{ group.label }}</div>
|
||||
<div class="node-types-grid">
|
||||
<div v-for="nodeType in group.nodes" :key="nodeType.type" class="node-type-item"
|
||||
:class="{ 'dragging': draggedNode?.type === nodeType.type, 'is-favorite': favorites.includes(nodeType.type) }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, nodeType)"
|
||||
@dragend="onDragEnd"
|
||||
@dblclick="addNode(nodeType)"
|
||||
@click.right.prevent="toggleFavorite(nodeType.type)"
|
||||
@mouseenter="e => showTooltipWithDelay(e, nodeType.description)"
|
||||
@mouseleave="hideTooltip()"
|
||||
>
|
||||
<div class="node-type-icon" :style="{ color: nodeType.color }">
|
||||
<i :class="`fa ${nodeType.icon}`"></i>
|
||||
</div>
|
||||
<div class="node-type-label">
|
||||
{{ nodeType.label }}
|
||||
<span v-if="nodeTypeCount[nodeType.type]" class="node-count">
|
||||
({{ nodeTypeCount[nodeType.type] }})
|
||||
</span>
|
||||
<i class="fa fa-star favorite-icon" v-if="favorites.includes(nodeType.type)" style="color:#f59e0b;margin-left:2px;font-size:13px;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="groupedNodeTypes.length===0" class="empty-tip">无匹配节点</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="group in groupedNodeTypes" :key="group.group" class="node-group">
|
||||
<div class="group-label">{{ group.label }}</div>
|
||||
<div class="node-types-grid">
|
||||
<div v-for="nodeType in group.nodes" :key="nodeType.type" class="node-type-item"
|
||||
:class="{ 'dragging': draggedNode?.type === nodeType.type, 'is-favorite': favorites.includes(nodeType.type) }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, nodeType)"
|
||||
@dragend="onDragEnd"
|
||||
@dblclick="addNode(nodeType)"
|
||||
@click.right.prevent="toggleFavorite(nodeType.type)"
|
||||
@mouseenter="e => showTooltipWithDelay(e, nodeType.description)"
|
||||
@mouseleave="hideTooltip()"
|
||||
>
|
||||
<div class="node-type-icon" :style="{ color: nodeType.color }">
|
||||
<i :class="`fa ${nodeType.icon}`"></i>
|
||||
</div>
|
||||
<div class="node-type-label">
|
||||
{{ nodeType.label }}
|
||||
<span v-if="nodeTypeCount[nodeType.type]" class="node-count">
|
||||
({{ nodeTypeCount[nodeType.type] }})
|
||||
</span>
|
||||
<i class="fa fa-star favorite-icon" v-if="favorites.includes(nodeType.type)" style="color:#f59e0b;margin-left:2px;font-size:13px;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="filteredNodeTypes.length===0" class="empty-tip">无匹配节点</div>
|
||||
</template>
|
||||
<div class="palette-tips">
|
||||
<div class="tip-item">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<span>拖拽节点到画布添加</span>
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<i class="fa fa-mouse-pointer"></i>
|
||||
<span>双击节点快速添加</span>
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<i class="fa fa-star"></i>
|
||||
<span>右键节点可收藏/取消收藏</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tooltip -->
|
||||
<transition name="fade">
|
||||
<div v-if="tooltip.show" class="custom-tooltip" :class="tooltip.direction" :style="{ left: tooltip.x + 'px', top: tooltip.y + 'px', width: tooltip.width + 'px' }">
|
||||
{{ tooltip.text }}
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.node-palette {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.palette-header {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
padding: 12px 12px 8px 12px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.palette-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.tab-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s, background 0.2s;
|
||||
}
|
||||
.tab-btn.active {
|
||||
color: #3b82f6;
|
||||
background: none; /* 移除背景色 */
|
||||
border-bottom: 2px solid #3b82f6;
|
||||
}
|
||||
.palette-search {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
.palette-search:focus {
|
||||
border-color: #6366f1;
|
||||
}
|
||||
.palette-content {
|
||||
flex: 1;
|
||||
padding: 12px 12px 0 12px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.node-group {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.group-label {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.node-types-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px 6px;
|
||||
}
|
||||
.node-type-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 2px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: #f9fafb;
|
||||
cursor: grab;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
.node-type-item:hover {
|
||||
background: #f3f4f6;
|
||||
border-color: #d1d5db;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.node-type-item:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.node-type-item.dragging {
|
||||
opacity: 0.5;
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
.node-type-icon {
|
||||
font-size: 20px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.node-type-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.node-count {
|
||||
font-size: 11px;
|
||||
background: #e5e7eb;
|
||||
color: #6b7280;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.favorite-icon {
|
||||
margin-left: 2px;
|
||||
}
|
||||
.is-favorite {
|
||||
}
|
||||
.empty-tip {
|
||||
color: #b91c1c;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
margin: 18px 0 0 0;
|
||||
}
|
||||
.palette-tips {
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.tip-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
.tip-item i {
|
||||
color: #9ca3af;
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
.custom-tooltip {
|
||||
position: fixed;
|
||||
z-index: 9999999;
|
||||
min-width: 120px;
|
||||
max-width: 260px;
|
||||
background: rgba(255,255,255,0.98);
|
||||
color: #222;
|
||||
font-size: 13px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.10);
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 10px 16px;
|
||||
pointer-events: none;
|
||||
white-space: pre-line;
|
||||
line-height: 1.5;
|
||||
opacity: 0.98;
|
||||
transition: opacity 0.13s;
|
||||
text-align: left;
|
||||
}
|
||||
.custom-tooltip.top .tooltip-arrow {
|
||||
top: 100%;
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-top: 7px solid #fff;
|
||||
border-bottom: none;
|
||||
filter: drop-shadow(0 1px 2px rgba(0,0,0,0.08));
|
||||
}
|
||||
.custom-tooltip.bottom .tooltip-arrow {
|
||||
bottom: 100%;
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-bottom: 7px solid #fff;
|
||||
border-top: none;
|
||||
filter: drop-shadow(0 -1px 2px rgba(0,0,0,0.08));
|
||||
}
|
||||
.custom-tooltip.left .tooltip-arrow {
|
||||
position: absolute;
|
||||
right: -7px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0; height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.custom-tooltip.left .tooltip-arrow::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 0; height: 0;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
border-left: 8px solid #fff;
|
||||
margin-right: -1px;
|
||||
box-shadow: -1px 0 2px rgba(0,0,0,0.08);
|
||||
border-radius: 2px 0 0 2px;
|
||||
border-left: 1px solid #e5e7eb;
|
||||
}
|
||||
.custom-tooltip.left {
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.custom-tooltip.right .tooltip-arrow {
|
||||
position: absolute;
|
||||
left: -7px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0; height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.custom-tooltip.right .tooltip-arrow::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 0; height: 0;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
border-right: 8px solid #fff;
|
||||
margin-left: -1px;
|
||||
box-shadow: 1px 0 2px rgba(0,0,0,0.08);
|
||||
border-radius: 0 2px 2px 0;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
.custom-tooltip.right {
|
||||
border-left: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* 全屏模式下的tooltip样式 */
|
||||
.ai-agent-flow-builder.fullscreen .custom-tooltip {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(229, 231, 235, 0.8);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
||||
}
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.13s;
|
||||
}
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@ -1,418 +0,0 @@
|
||||
/**
|
||||
* 流程执行器
|
||||
* 管理和执行节点流程
|
||||
*/
|
||||
|
||||
import { getNodeMetadataByType } from '../utils/nodeMetadata';
|
||||
|
||||
export class FlowExecutor {
|
||||
constructor() {
|
||||
this.executionContext = {};
|
||||
this.executionHistory = [];
|
||||
this.isExecuting = false;
|
||||
|
||||
// 预构建的执行路径映射
|
||||
this.executionPaths = new Map();
|
||||
this.conditionNodes = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行流程
|
||||
* @param {Array} nodes - 节点数组
|
||||
* @param {Array} edges - 边数组
|
||||
* @param {Object} initialData - 初始数据
|
||||
* @returns {Promise<Object>} 执行结果
|
||||
*/
|
||||
async executeFlow(nodes, edges, initialData = {}) {
|
||||
if (this.isExecuting) {
|
||||
throw new Error('流程正在执行中,请等待完成');
|
||||
}
|
||||
|
||||
this.isExecuting = true;
|
||||
this.executionContext = { ...initialData };
|
||||
this.executionHistory = [];
|
||||
|
||||
try {
|
||||
// 创建节点的深拷贝,避免修改原始数据
|
||||
const executionNodes = JSON.parse(JSON.stringify(nodes));
|
||||
const executionEdges = JSON.parse(JSON.stringify(edges));
|
||||
|
||||
// 预构建执行图(一次性构建,避免重复计算)
|
||||
this.buildExecutionGraph(executionNodes, executionEdges);
|
||||
|
||||
// 执行流程
|
||||
const result = await this.executeGraph();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result: result,
|
||||
history: this.executionHistory,
|
||||
context: this.executionContext
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
history: this.executionHistory,
|
||||
context: this.executionContext
|
||||
};
|
||||
} finally {
|
||||
this.isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预构建执行图(一次性构建,避免重复计算)
|
||||
* @param {Array} nodes - 节点数组
|
||||
* @param {Array} edges - 边数组
|
||||
*/
|
||||
buildExecutionGraph(nodes, edges) {
|
||||
// 构建节点映射
|
||||
this.nodes = new Map(nodes.map(node => [node.id, node]));
|
||||
|
||||
// 构建边映射
|
||||
this.edges = edges;
|
||||
|
||||
// 预构建条件分支映射(O(n)时间复杂度)
|
||||
this.conditionNodes.clear();
|
||||
this.executionPaths.clear();
|
||||
|
||||
for (const edge of edges) {
|
||||
const sourceNode = this.nodes.get(edge.source);
|
||||
const targetNode = this.nodes.get(edge.target);
|
||||
|
||||
if (sourceNode?.type === 'condition_check') {
|
||||
// 初始化条件节点
|
||||
if (!this.conditionNodes.has(edge.source)) {
|
||||
this.conditionNodes.set(edge.source, {
|
||||
truePath: new Set(),
|
||||
falsePath: new Set(),
|
||||
mainPath: new Set() // 新增主流程路径
|
||||
});
|
||||
}
|
||||
|
||||
const conditionNode = this.conditionNodes.get(edge.source);
|
||||
if (edge.sourceHandle === 'true_output') {
|
||||
conditionNode.truePath.add(edge.target);
|
||||
} else if (edge.sourceHandle === 'false_output') {
|
||||
conditionNode.falsePath.add(edge.target);
|
||||
} else if (edge.sourceHandle === 'output') {
|
||||
conditionNode.mainPath.add(edge.target); // 主流程路径
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建拓扑排序(一次性计算)
|
||||
this.executionOrder = this.topologicalSort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拓扑排序(使用新的数据结构)
|
||||
* @returns {Array} 执行顺序
|
||||
*/
|
||||
topologicalSort() {
|
||||
// 构建依赖关系映射
|
||||
const dependencies = new Map();
|
||||
const inDegree = new Map();
|
||||
|
||||
// 初始化
|
||||
for (const nodeId of this.nodes.keys()) {
|
||||
dependencies.set(nodeId, []);
|
||||
inDegree.set(nodeId, 0);
|
||||
}
|
||||
|
||||
// 构建依赖关系
|
||||
for (const edge of this.edges) {
|
||||
const sourceId = edge.source;
|
||||
const targetId = edge.target;
|
||||
|
||||
if (dependencies.has(targetId)) {
|
||||
dependencies.get(targetId).push(sourceId);
|
||||
inDegree.set(targetId, inDegree.get(targetId) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行拓扑排序
|
||||
const queue = [];
|
||||
const result = [];
|
||||
|
||||
// 找到所有入度为0的节点
|
||||
for (const [nodeId, degree] of inDegree) {
|
||||
if (degree === 0) {
|
||||
queue.push(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
while (queue.length > 0) {
|
||||
const nodeId = queue.shift();
|
||||
result.push(nodeId);
|
||||
|
||||
// 更新依赖节点的入度
|
||||
for (const [depNodeId, deps] of dependencies) {
|
||||
if (deps.includes(nodeId)) {
|
||||
const newDegree = inDegree.get(depNodeId) - 1;
|
||||
inDegree.set(depNodeId, newDegree);
|
||||
if (newDegree === 0) {
|
||||
queue.push(depNodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有环
|
||||
if (result.length !== this.nodes.size) {
|
||||
throw new Error('流程中存在循环依赖');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行图(使用预构建的路径映射)
|
||||
* @returns {Promise<Object>} 执行结果
|
||||
*/
|
||||
async executeGraph() {
|
||||
const context = {
|
||||
node_results: {},
|
||||
flow_id: this.executionContext.agent_name
|
||||
};
|
||||
|
||||
const results = {};
|
||||
const executedNodes = new Set();
|
||||
|
||||
// 按拓扑排序执行节点
|
||||
for (const nodeId of this.executionOrder) {
|
||||
const node = this.nodes.get(nodeId);
|
||||
|
||||
// 检查节点是否应该被执行(O(1)时间复杂度)
|
||||
if (!this.shouldExecuteNode(node, context, executedNodes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 执行节点
|
||||
const result = await this.executeNode(node, context);
|
||||
results[nodeId] = result;
|
||||
context.node_results[nodeId] = result;
|
||||
executedNodes.add(nodeId);
|
||||
|
||||
// 记录执行历史
|
||||
this.recordExecutionHistory(node, result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 判断节点是否应该被执行(O(1)时间复杂度)
|
||||
* @param {Object} node - 节点对象
|
||||
* @param {Object} context - 执行上下文
|
||||
* @param {Set} executedNodes - 已执行节点集合
|
||||
* @returns {boolean} 是否应该执行
|
||||
*/
|
||||
shouldExecuteNode(node, context, executedNodes) {
|
||||
// 如果节点已经执行过,跳过
|
||||
if (executedNodes.has(node.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否被条件判断节点控制
|
||||
for (const [conditionId, paths] of this.conditionNodes) {
|
||||
if (paths.truePath.has(node.id) || paths.falsePath.has(node.id) || paths.mainPath.has(node.id)) {
|
||||
const conditionResult = context.node_results[conditionId];
|
||||
|
||||
// 主流程路径的节点总是执行
|
||||
if (paths.mainPath.has(node.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (conditionResult?.condition_met !== undefined) {
|
||||
// 根据条件结果决定是否执行
|
||||
const shouldExecute = paths.truePath.has(node.id)
|
||||
? conditionResult.condition_met
|
||||
: !conditionResult.condition_met;
|
||||
|
||||
if (!shouldExecute) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个节点
|
||||
* @param {Object} node - 节点对象
|
||||
* @param {Object} context - 执行上下文
|
||||
* @returns {Promise<Object>} 执行结果
|
||||
*/
|
||||
async executeNode(node, context) {
|
||||
node.status = 'executing';
|
||||
this.updateNodeStatus(node.id, 'executing');
|
||||
|
||||
try {
|
||||
// 构建节点输入
|
||||
const nodeInputs = this.buildNodeInputs(node, context);
|
||||
const config = this.buildNodeConfig(node);
|
||||
|
||||
// 直接在context中加入当前节点ID和流程数据
|
||||
context.current_node_id = node.id;
|
||||
context.flow_data = {
|
||||
nodes: Array.from(this.nodes.values()),
|
||||
edges: this.edges
|
||||
};
|
||||
|
||||
// 调用节点API
|
||||
const response = await fetch(`/api/action/jingrow.ai.pagetype.ai_agent.nodes.${node.type}.${node.type}.execute`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Jingrow-CSRF-Token': jingrow.csrf_token
|
||||
},
|
||||
body: JSON.stringify({ context, inputs: nodeInputs, config })
|
||||
});
|
||||
|
||||
const resJson = await response.json();
|
||||
|
||||
if (!resJson.message || resJson.message.success === false) {
|
||||
throw new Error(resJson.message?.error || '节点执行失败');
|
||||
}
|
||||
|
||||
// 更新节点状态
|
||||
node.status = 'success';
|
||||
node.result = resJson.message;
|
||||
this.updateNodeStatus(node.id, 'success');
|
||||
|
||||
return resJson.message;
|
||||
|
||||
} catch (error) {
|
||||
// 处理执行失败
|
||||
node.status = 'failed';
|
||||
node.error = error.message;
|
||||
this.updateNodeStatus(node.id, 'failed', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建节点输入(O(1)时间复杂度)
|
||||
* @param {Object} node - 节点对象
|
||||
* @param {Object} context - 执行上下文
|
||||
* @returns {Object} 节点输入
|
||||
*/
|
||||
buildNodeInputs(node, context) {
|
||||
const inputs = {};
|
||||
|
||||
if (node.data?.inputs) {
|
||||
for (const [key, ref] of Object.entries(node.data.inputs)) {
|
||||
inputs[key] = context.node_results[ref.from]?.[ref.field];
|
||||
}
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建节点配置
|
||||
* @param {Object} node - 节点对象
|
||||
* @returns {Object} 节点配置
|
||||
*/
|
||||
buildNodeConfig(node) {
|
||||
const { inputs, config: nodeConfig = {}, ...rest } = node.data || {};
|
||||
return { ...nodeConfig, ...rest };
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录执行历史
|
||||
* @param {Object} node - 节点对象
|
||||
* @param {Object} result - 执行结果
|
||||
*/
|
||||
recordExecutionHistory(node, result) {
|
||||
let nodeLabel = node.data?.label;
|
||||
if (!nodeLabel) {
|
||||
const metadata = getNodeMetadataByType(node.type);
|
||||
nodeLabel = metadata ? metadata.label : node.type;
|
||||
}
|
||||
|
||||
this.executionHistory.push({
|
||||
nodeId: node.id,
|
||||
nodeType: node.type,
|
||||
nodeLabel: nodeLabel,
|
||||
status: 'success',
|
||||
result: result,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建节点上下文
|
||||
* @param {String} nodeId - 节点ID
|
||||
* @param {Object} graph - 执行图
|
||||
* @param {Object} results - 执行结果
|
||||
* @returns {Object} 节点上下文
|
||||
*/
|
||||
buildNodeContext(nodeId, graph, results) {
|
||||
const context = {
|
||||
...this.executionContext
|
||||
};
|
||||
|
||||
// 添加依赖节点的结果
|
||||
const dependencies = graph.dependencies[nodeId] || [];
|
||||
dependencies.forEach(depNodeId => {
|
||||
const depResult = results[depNodeId];
|
||||
if (depResult && depResult.success) {
|
||||
const depNode = graph.nodes[depNodeId];
|
||||
context[depNode.type] = depResult.data;
|
||||
}
|
||||
});
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新节点状态(用于UI更新)
|
||||
* @param {String} nodeId - 节点ID
|
||||
* @param {String} status - 状态
|
||||
* @param {String} error - 错误信息
|
||||
*/
|
||||
updateNodeStatus(nodeId, status, error = null) {
|
||||
// 这里可以触发UI更新事件
|
||||
const event = new CustomEvent('nodeStatusUpdate', {
|
||||
detail: { nodeId, status, error }
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行历史
|
||||
* @returns {Array} 执行历史
|
||||
*/
|
||||
getExecutionHistory() {
|
||||
return this.executionHistory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行上下文
|
||||
* @returns {Object} 执行上下文
|
||||
*/
|
||||
getExecutionContext() {
|
||||
return this.executionContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置执行器
|
||||
*/
|
||||
reset() {
|
||||
this.executionContext = {};
|
||||
this.executionHistory = [];
|
||||
this.isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局执行器实例
|
||||
export const flowExecutor = new FlowExecutor();
|
||||
@ -1,359 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export const useFlowStore = defineStore('flow', () => {
|
||||
// 状态
|
||||
const nodes = ref([]);
|
||||
const edges = ref([]);
|
||||
const hasUnsavedChanges = ref(false);
|
||||
const selectedNodeId = ref(null);
|
||||
const selectedEdgeId = ref(null);
|
||||
|
||||
// 历史记录管理
|
||||
const history = ref([]);
|
||||
const currentHistoryIndex = ref(-1);
|
||||
const maxHistorySize = ref(50);
|
||||
|
||||
// Jingrow 上下文
|
||||
const frm = ref(null);
|
||||
const page = ref(null);
|
||||
const pagetype = ref(null);
|
||||
|
||||
// 计算属性
|
||||
const selectedNode = computed(() => {
|
||||
return nodes.value.find(node => node.id === selectedNodeId.value);
|
||||
});
|
||||
|
||||
const selectedEdge = computed(() => {
|
||||
return edges.value.find(edge => edge.id === selectedEdgeId.value);
|
||||
});
|
||||
|
||||
const nodeCount = computed(() => nodes.value.length);
|
||||
const edgeCount = computed(() => edges.value.length);
|
||||
|
||||
// 历史记录相关计算属性
|
||||
const canUndo = computed(() => currentHistoryIndex.value > 0);
|
||||
const canRedo = computed(() => currentHistoryIndex.value < history.value.length - 1);
|
||||
|
||||
// 节点操作
|
||||
const addNode = (node) => {
|
||||
saveToHistory();
|
||||
nodes.value.push(node);
|
||||
markDirty();
|
||||
};
|
||||
|
||||
const removeNode = (nodeId) => {
|
||||
saveToHistory();
|
||||
// 移除节点
|
||||
nodes.value = nodes.value.filter(node => node.id !== nodeId);
|
||||
|
||||
// 移除相关的边
|
||||
edges.value = edges.value.filter(edge =>
|
||||
edge.source !== nodeId && edge.target !== nodeId
|
||||
);
|
||||
|
||||
// 取消选择
|
||||
if (selectedNodeId.value === nodeId) {
|
||||
selectedNodeId.value = null;
|
||||
}
|
||||
|
||||
markDirty();
|
||||
};
|
||||
|
||||
const updateNode = (nodeId, updates) => {
|
||||
const nodeIndex = nodes.value.findIndex(node => node.id === nodeId);
|
||||
if (nodeIndex !== -1) {
|
||||
nodes.value[nodeIndex] = { ...nodes.value[nodeIndex], ...updates };
|
||||
markDirty();
|
||||
}
|
||||
};
|
||||
|
||||
const updateNodeData = (nodeId, newData) => {
|
||||
const nodeIndex = nodes.value.findIndex(node => node.id === nodeId);
|
||||
if (nodeIndex !== -1) {
|
||||
const updatedNode = {
|
||||
...nodes.value[nodeIndex],
|
||||
data: {
|
||||
...nodes.value[nodeIndex].data,
|
||||
...(newData.label !== undefined ? { label: newData.label } : {}),
|
||||
...(newData.inputs !== undefined ? { inputs: { ...nodes.value[nodeIndex].data.inputs, ...newData.inputs } } : {}),
|
||||
config: { ...nodes.value[nodeIndex].data.config, ...(newData.config || {}) }
|
||||
}
|
||||
};
|
||||
const newNodes = [...nodes.value];
|
||||
newNodes.splice(nodeIndex, 1, updatedNode);
|
||||
nodes.value = newNodes;
|
||||
markDirty();
|
||||
}
|
||||
};
|
||||
|
||||
const updateNodePosition = (nodeId, position) => {
|
||||
const nodeIndex = nodes.value.findIndex(node => node.id === nodeId);
|
||||
if (nodeIndex !== -1) {
|
||||
nodes.value[nodeIndex].position = position;
|
||||
markDirty();
|
||||
}
|
||||
};
|
||||
|
||||
// 查找方法
|
||||
const findNode = (nodeId) => {
|
||||
return nodes.value.find(node => node.id === nodeId);
|
||||
};
|
||||
|
||||
const findEdge = (edgeId) => {
|
||||
return edges.value.find(edge => edge.id === edgeId);
|
||||
};
|
||||
|
||||
// 检查连接是否已存在
|
||||
const hasConnection = (source, target, sourceHandle = 'output', targetHandle = 'input') => {
|
||||
return edges.value.some(edge =>
|
||||
edge.source === source &&
|
||||
edge.target === target &&
|
||||
edge.sourceHandle === sourceHandle &&
|
||||
edge.targetHandle === targetHandle
|
||||
);
|
||||
};
|
||||
|
||||
// 边操作
|
||||
const addEdge = (edge) => {
|
||||
saveToHistory();
|
||||
edges.value.push(edge);
|
||||
markDirty();
|
||||
};
|
||||
|
||||
const removeEdge = (edgeId) => {
|
||||
saveToHistory();
|
||||
edges.value = edges.value.filter(edge => edge.id !== edgeId);
|
||||
|
||||
// 取消选择
|
||||
if (selectedEdgeId.value === edgeId) {
|
||||
selectedEdgeId.value = null;
|
||||
}
|
||||
|
||||
markDirty();
|
||||
};
|
||||
|
||||
const updateEdge = (edgeId, updates) => {
|
||||
const edgeIndex = edges.value.findIndex(edge => edge.id === edgeId);
|
||||
if (edgeIndex !== -1) {
|
||||
edges.value[edgeIndex] = { ...edges.value[edgeIndex], ...updates };
|
||||
markDirty();
|
||||
}
|
||||
};
|
||||
|
||||
// 选择操作
|
||||
const selectNode = (nodeId) => {
|
||||
selectedNodeId.value = nodeId;
|
||||
selectedEdgeId.value = null;
|
||||
};
|
||||
|
||||
const selectEdge = (edgeId) => {
|
||||
selectedEdgeId.value = edgeId;
|
||||
selectedNodeId.value = null;
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
selectedNodeId.value = null;
|
||||
selectedEdgeId.value = null;
|
||||
};
|
||||
|
||||
// 流程数据操作
|
||||
const loadFlowData = (flowData) => {
|
||||
|
||||
if (flowData) {
|
||||
nodes.value = flowData.nodes || [];
|
||||
edges.value = flowData.edges || [];
|
||||
hasUnsavedChanges.value = false;
|
||||
} else {
|
||||
nodes.value = [];
|
||||
edges.value = [];
|
||||
hasUnsavedChanges.value = false;
|
||||
}
|
||||
|
||||
// 初始化历史记录
|
||||
clearHistory();
|
||||
saveToHistory();
|
||||
};
|
||||
|
||||
const getFlowData = () => {
|
||||
return {
|
||||
nodes: nodes.value,
|
||||
edges: edges.value,
|
||||
metadata: {
|
||||
nodeCount: nodeCount.value,
|
||||
edgeCount: edgeCount.value,
|
||||
createdAt: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const resetFlow = () => {
|
||||
nodes.value = [];
|
||||
edges.value = [];
|
||||
selectedNodeId.value = null;
|
||||
selectedEdgeId.value = null;
|
||||
hasUnsavedChanges.value = false;
|
||||
};
|
||||
|
||||
// 状态管理
|
||||
const markDirty = () => {
|
||||
hasUnsavedChanges.value = true;
|
||||
};
|
||||
|
||||
const markClean = () => {
|
||||
hasUnsavedChanges.value = false;
|
||||
};
|
||||
|
||||
// 历史记录管理
|
||||
const saveToHistory = () => {
|
||||
const currentState = {
|
||||
nodes: JSON.parse(JSON.stringify(nodes.value)),
|
||||
edges: JSON.parse(JSON.stringify(edges.value)),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 如果当前不在历史记录的末尾,删除当前位置之后的所有记录
|
||||
if (currentHistoryIndex.value < history.value.length - 1) {
|
||||
history.value = history.value.slice(0, currentHistoryIndex.value + 1);
|
||||
}
|
||||
|
||||
// 添加新的历史记录
|
||||
history.value.push(currentState);
|
||||
currentHistoryIndex.value = history.value.length - 1;
|
||||
|
||||
// 限制历史记录大小
|
||||
if (history.value.length > maxHistorySize.value) {
|
||||
history.value.shift();
|
||||
currentHistoryIndex.value--;
|
||||
}
|
||||
};
|
||||
|
||||
const undo = () => {
|
||||
if (canUndo.value) {
|
||||
currentHistoryIndex.value--;
|
||||
const previousState = history.value[currentHistoryIndex.value];
|
||||
nodes.value = JSON.parse(JSON.stringify(previousState.nodes));
|
||||
edges.value = JSON.parse(JSON.stringify(previousState.edges));
|
||||
markDirty();
|
||||
}
|
||||
};
|
||||
|
||||
const redo = () => {
|
||||
if (canRedo.value) {
|
||||
currentHistoryIndex.value++;
|
||||
const nextState = history.value[currentHistoryIndex.value];
|
||||
nodes.value = JSON.parse(JSON.stringify(nextState.nodes));
|
||||
edges.value = JSON.parse(JSON.stringify(nextState.edges));
|
||||
markDirty();
|
||||
}
|
||||
};
|
||||
|
||||
const clearHistory = () => {
|
||||
history.value = [];
|
||||
currentHistoryIndex.value = -1;
|
||||
};
|
||||
|
||||
// 本地存储
|
||||
const saveToStorage = () => {
|
||||
const flowData = getFlowData();
|
||||
localStorage.setItem('ai-agent-flow-data', JSON.stringify(flowData));
|
||||
};
|
||||
|
||||
const loadFromStorage = () => {
|
||||
try {
|
||||
const savedData = localStorage.getItem('ai-agent-flow-data');
|
||||
if (savedData) {
|
||||
const flowData = JSON.parse(savedData);
|
||||
loadFlowData(flowData);
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('Failed to load flow data from storage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 数据验证
|
||||
const validateFlow = () => {
|
||||
const errors = [];
|
||||
|
||||
// 检查孤立节点
|
||||
const connectedNodes = new Set();
|
||||
edges.value.forEach(edge => {
|
||||
connectedNodes.add(edge.source);
|
||||
connectedNodes.add(edge.target);
|
||||
});
|
||||
|
||||
const isolatedNodes = nodes.value.filter(node =>
|
||||
!connectedNodes.has(node.id) && nodes.value.length > 1
|
||||
);
|
||||
|
||||
if (isolatedNodes.length > 0) {
|
||||
errors.push(`Found ${isolatedNodes.length} isolated node(s)`);
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
nodes,
|
||||
edges,
|
||||
hasUnsavedChanges,
|
||||
selectedNodeId,
|
||||
selectedEdgeId,
|
||||
frm,
|
||||
page,
|
||||
pagetype,
|
||||
|
||||
// 计算属性
|
||||
selectedNode,
|
||||
selectedEdge,
|
||||
nodeCount,
|
||||
edgeCount,
|
||||
canUndo,
|
||||
canRedo,
|
||||
|
||||
// 节点操作
|
||||
addNode,
|
||||
removeNode,
|
||||
updateNode,
|
||||
updateNodeData,
|
||||
updateNodePosition,
|
||||
findNode,
|
||||
findEdge,
|
||||
hasConnection,
|
||||
|
||||
// 边操作
|
||||
addEdge,
|
||||
removeEdge,
|
||||
updateEdge,
|
||||
|
||||
// 选择操作
|
||||
selectNode,
|
||||
selectEdge,
|
||||
clearSelection,
|
||||
|
||||
// 流程数据操作
|
||||
loadFlowData,
|
||||
getFlowData,
|
||||
resetFlow,
|
||||
|
||||
// 状态管理
|
||||
markDirty,
|
||||
markClean,
|
||||
saveToHistory,
|
||||
undo,
|
||||
redo,
|
||||
clearHistory,
|
||||
|
||||
// 本地存储
|
||||
saveToStorage,
|
||||
loadFromStorage,
|
||||
|
||||
// 数据验证
|
||||
validateFlow
|
||||
};
|
||||
});
|
||||
@ -1,132 +0,0 @@
|
||||
// 节点元数据统一管理文件
|
||||
// 直接在文件中定义所有节点元数据
|
||||
|
||||
import GenericNode from '../components/Nodes/GenericNode.vue';
|
||||
import MultiOutputNode from '../components/Nodes/MultiOutputNode.vue';
|
||||
|
||||
// 节点类型分组定义
|
||||
export const NODE_GROUPS = [
|
||||
{
|
||||
group: '输入',
|
||||
label: '输入',
|
||||
types: []
|
||||
},
|
||||
{
|
||||
group: '输出',
|
||||
label: '输出',
|
||||
types: []
|
||||
},
|
||||
{
|
||||
group: 'AI',
|
||||
label: 'AI',
|
||||
types: []
|
||||
},
|
||||
{
|
||||
group: '控制',
|
||||
label: '控制',
|
||||
types: []
|
||||
},
|
||||
{
|
||||
group: '其他',
|
||||
label: '其他',
|
||||
types: []
|
||||
}
|
||||
];
|
||||
|
||||
// 所有节点元数据定义
|
||||
const NODE_METADATA_MAP = {
|
||||
input_record: {
|
||||
type: 'input_record',
|
||||
label: '输入记录',
|
||||
icon: 'fa-file-text',
|
||||
color: '#10b981',
|
||||
description: '接收页面记录数据',
|
||||
group: '输入',
|
||||
component: GenericNode
|
||||
},
|
||||
create_record: {
|
||||
type: 'create_record',
|
||||
label: '创建记录',
|
||||
icon: 'fa-plus',
|
||||
color: '#10b981',
|
||||
description: '创建新的页面记录',
|
||||
group: '输出',
|
||||
component: GenericNode
|
||||
},
|
||||
update_record: {
|
||||
type: 'update_record',
|
||||
label: '更新记录',
|
||||
icon: 'fa-save',
|
||||
color: '#ef4444',
|
||||
description: '更新目标记录的字段值',
|
||||
group: '输出',
|
||||
component: GenericNode
|
||||
},
|
||||
ai_content_generation: {
|
||||
type: 'ai_content_generation',
|
||||
label: 'AI大模型',
|
||||
icon: 'fa-cogs',
|
||||
color: '#6366f1',
|
||||
description: '调用LLM大语言模型生成内容',
|
||||
group: 'AI',
|
||||
component: GenericNode
|
||||
},
|
||||
ai_image_generation: {
|
||||
type: 'ai_image_generation',
|
||||
label: 'AI绘画',
|
||||
icon: 'fa-image',
|
||||
color: '#10b981',
|
||||
description: '使用AI绘画工具生成图像',
|
||||
group: 'AI',
|
||||
component: GenericNode
|
||||
},
|
||||
image_upload: {
|
||||
type: 'image_upload',
|
||||
label: '图片上传',
|
||||
icon: 'fa-upload',
|
||||
color: '#8b5cf6',
|
||||
description: '将图片上传到文件库',
|
||||
group: '输出',
|
||||
component: GenericNode
|
||||
},
|
||||
condition_check: {
|
||||
type: 'condition_check',
|
||||
label: '条件判断',
|
||||
icon: 'fa-question-circle',
|
||||
color: '#f59e0b',
|
||||
description: '检查指定记录是否存在,控制流程执行',
|
||||
group: '控制',
|
||||
component: MultiOutputNode
|
||||
}
|
||||
};
|
||||
|
||||
// 收集所有节点元数据
|
||||
export function getAllNodeTypes() {
|
||||
return Object.values(NODE_METADATA_MAP);
|
||||
}
|
||||
|
||||
// 根据类型获取节点元数据
|
||||
export function getNodeMetadataByType(type) {
|
||||
return NODE_METADATA_MAP[type];
|
||||
}
|
||||
|
||||
// 获取指定分组的节点类型
|
||||
export function getNodeTypesByGroup(group) {
|
||||
return Object.values(NODE_METADATA_MAP).filter(nodeType => nodeType.group === group);
|
||||
}
|
||||
|
||||
// 获取所有节点类型名称
|
||||
export function getAllNodeTypeNames() {
|
||||
return Object.keys(NODE_METADATA_MAP);
|
||||
}
|
||||
|
||||
// 获取所有节点组件映射(用于 Vue Flow)
|
||||
export function getNodeComponents() {
|
||||
const components = {};
|
||||
Object.keys(NODE_METADATA_MAP).forEach(type => {
|
||||
if (NODE_METADATA_MAP[type].component) {
|
||||
components[type] = NODE_METADATA_MAP[type].component;
|
||||
}
|
||||
});
|
||||
return components;
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
/**
|
||||
* 通用Schema加载函数
|
||||
* @param {string} nodeType - 节点类型
|
||||
* @returns {Promise<Object>} Schema配置对象
|
||||
*/
|
||||
export async function loadNodeSchema(nodeType) {
|
||||
try {
|
||||
const response = await jingrow.call({
|
||||
method: 'jingrow.ai.utils.node_schema.get_node_schema',
|
||||
args: { node_type: nodeType }
|
||||
});
|
||||
|
||||
if (response.message) {
|
||||
return response.message;
|
||||
} else {
|
||||
throw new Error('Schema加载失败:响应为空');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Schema加载失败:', error);
|
||||
jingrow.msgprint({
|
||||
title: 'Schema加载失败',
|
||||
message: `无法加载${nodeType}节点配置,请检查网络连接`,
|
||||
indicator: 'red'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建针对特定节点类型的Schema加载器
|
||||
* @param {string} nodeType - 节点类型
|
||||
* @returns {Function} 异步加载函数
|
||||
*/
|
||||
export function createSchemaLoader(nodeType) {
|
||||
return () => loadNodeSchema(nodeType);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@ -1,215 +0,0 @@
|
||||
jingrow.ui.form.on("AI Node Schema", {
|
||||
refresh(frm) {
|
||||
if (frm.pg.node_type) {
|
||||
frm.add_custom_button('编辑节点Schema', function() {
|
||||
open_schema_editor(frm);
|
||||
});
|
||||
|
||||
frm.add_custom_button('从文件加载Schema', function() {
|
||||
load_schema_from_file(frm, frm.pg.node_type);
|
||||
});
|
||||
|
||||
frm.add_custom_button('保存到文件', function() {
|
||||
save_schema_to_file(frm);
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定 node_type 字段的失去焦点事件
|
||||
if (frm.fields_dict.node_type && frm.fields_dict.node_type.$input) {
|
||||
frm.fields_dict.node_type.$input.off('blur.node_schema_loader');
|
||||
frm.fields_dict.node_type.$input.on('blur.node_schema_loader', function() {
|
||||
if (frm.pg.node_type) {
|
||||
load_schema_from_file(frm, frm.pg.node_type);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function open_schema_editor(frm) {
|
||||
if (!frm.pg.node_type) {
|
||||
jingrow.msgprint("请先选择节点类型", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
let d = new jingrow.ui.Dialog({
|
||||
title: `编辑 ${frm.pg.node_type} Schema`,
|
||||
size: "extra-large",
|
||||
primary_action_label: null,
|
||||
primary_action: null,
|
||||
secondary_action_label: null,
|
||||
secondary_action: null
|
||||
});
|
||||
|
||||
d.$body.html(`
|
||||
<div id="schema-builder-container">
|
||||
<div id="schema-builder"></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
d.show();
|
||||
|
||||
init_vue_schema_editor(d, frm);
|
||||
}
|
||||
|
||||
function init_vue_schema_editor(dialog, frm) {
|
||||
const container = dialog.$body.find('#schema-builder')[0];
|
||||
|
||||
load_schema_builder_components().then(() => {
|
||||
create_schema_builder_app(container, frm, dialog);
|
||||
}).catch(error => {
|
||||
jingrow.msgprint(`加载 Schema 编辑器失败: ${error.message}`, "错误");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function create_schema_builder_app(container, frm, dialog) {
|
||||
const schemaBuilder = new jingrow.ui.SchemaBuilder({
|
||||
wrapper: container,
|
||||
frm: frm,
|
||||
pagetype: frm.pg.pagetype,
|
||||
customize: false
|
||||
});
|
||||
|
||||
dialog.schema_builder = schemaBuilder;
|
||||
|
||||
if (schemaBuilder.store) {
|
||||
schemaBuilder.store.saveSchemaToForm = () => {
|
||||
schemaBuilder.saveSchemaToForm();
|
||||
};
|
||||
}
|
||||
|
||||
container.addEventListener('schema-save', (event) => {
|
||||
const schemaData = event.detail.schemaData;
|
||||
if (schemaData) {
|
||||
frm.set_value('node_schema', JSON.stringify(schemaData, null, 2));
|
||||
frm.save().then(() => {
|
||||
jingrow.msgprint("Schema 保存成功");
|
||||
}).catch(error => {
|
||||
jingrow.msgprint(`保存失败: ${error.message}`, "错误");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (frm.pg.node_schema) {
|
||||
let initialSchema = {};
|
||||
if (typeof frm.pg.node_schema === 'string') {
|
||||
try {
|
||||
initialSchema = JSON.parse(frm.pg.node_schema);
|
||||
} catch (e) {
|
||||
initialSchema = {};
|
||||
}
|
||||
} else if (typeof frm.pg.node_schema === 'object') {
|
||||
initialSchema = frm.pg.node_schema;
|
||||
}
|
||||
|
||||
if (schemaBuilder.store && schemaBuilder.store.loadSchema) {
|
||||
schemaBuilder.store.loadSchema(initialSchema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function load_schema_builder_components() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (jingrow.ui && jingrow.ui.SchemaBuilder) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
jingrow.require('schema_builder.bundle.js').then(() => {
|
||||
if (jingrow.ui && jingrow.ui.SchemaBuilder) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('组件加载失败'));
|
||||
}
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function save_schema_from_editor(frm, dialog) {
|
||||
try {
|
||||
let schemaData = null;
|
||||
if (dialog.schema_builder && typeof dialog.schema_builder.getSchemaData === 'function') {
|
||||
try {
|
||||
schemaData = dialog.schema_builder.getSchemaData();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (!schemaData) {
|
||||
jingrow.msgprint("无法获取 Schema 数据", "错误");
|
||||
return;
|
||||
}
|
||||
|
||||
const prettyJson = JSON.stringify(schemaData, null, 2);
|
||||
frm.set_value('node_schema', prettyJson);
|
||||
|
||||
frm.save().then(() => {
|
||||
if (dialog.schema_builder && dialog.schema_builder.store) {
|
||||
dialog.schema_builder.store.dirty = false;
|
||||
}
|
||||
jingrow.msgprint("Schema 保存成功");
|
||||
dialog.hide();
|
||||
}).catch((error) => {
|
||||
jingrow.msgprint("保存失败", "错误");
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
jingrow.msgprint(`保存失败: ${e.message}`, "错误");
|
||||
}
|
||||
}
|
||||
|
||||
function load_schema_from_file(frm, node_type) {
|
||||
jingrow.call('jform.jform.pagetype.ai_node_schema.ai_node_schema.load_node_schema', {
|
||||
node_type: node_type
|
||||
}).then(response => {
|
||||
// 从 response.data 获取数据,避免被包装在 message 字段中
|
||||
const schema_data = response.data || response;
|
||||
|
||||
let parsedSchema = {};
|
||||
if (typeof schema_data === 'string') {
|
||||
try {
|
||||
parsedSchema = JSON.parse(schema_data);
|
||||
} catch (e) {
|
||||
parsedSchema = {};
|
||||
}
|
||||
} else if (typeof schema_data === 'object') {
|
||||
parsedSchema = schema_data;
|
||||
}
|
||||
|
||||
// 将 schema 转换为格式化的 JSON 字符串,确保多行显示
|
||||
const formattedSchema = JSON.stringify(parsedSchema, null, 2);
|
||||
frm.set_value('node_schema', formattedSchema);
|
||||
// 移除加载成功的提示弹窗
|
||||
}).catch(e => {
|
||||
jingrow.msgprint(`加载失败: ${e.message}`, "错误");
|
||||
});
|
||||
}
|
||||
|
||||
function save_schema_to_file(frm) {
|
||||
if (!frm.pg.node_type || !frm.pg.node_schema) {
|
||||
jingrow.msgprint("请先选择节点类型并设置 Schema 数据", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
let schemaData = frm.pg.node_schema;
|
||||
if (typeof schemaData === 'string') {
|
||||
try {
|
||||
schemaData = JSON.parse(schemaData);
|
||||
} catch (e) {
|
||||
jingrow.msgprint("Schema 数据格式错误", "错误");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
jingrow.call('jform.jform.pagetype.ai_node_schema.ai_node_schema.save_node_schema', {
|
||||
node_type: frm.pg.node_type,
|
||||
schema_data: schemaData
|
||||
}).then(() => {
|
||||
jingrow.msgprint("Schema 已保存到文件");
|
||||
}).catch(e => {
|
||||
jingrow.msgprint(`保存失败: ${e.message}`, "错误");
|
||||
});
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-08-27 01:45:28.762568",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"node_type",
|
||||
"node_schema"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "node_type",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Node Type",
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "node_schema",
|
||||
"fieldtype": "JSON",
|
||||
"label": "Node Schema"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-30 21:26:25.549033",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Jform",
|
||||
"name": "AI Node Schema",
|
||||
"owner": "Administrator",
|
||||
"pagetype": "PageType",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
# Copyright (c) 2025, Jingrow and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import jingrow
|
||||
import os
|
||||
import json
|
||||
from jingrow.model.document import Document
|
||||
|
||||
|
||||
|
||||
class AINodeSchema(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from jingrow.types import DF
|
||||
|
||||
node_schema: DF.JSON | None
|
||||
node_type: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
"""验证 schema JSON 格式"""
|
||||
if self.node_schema:
|
||||
json.dumps(self.node_schema)
|
||||
|
||||
def before_save(self):
|
||||
"""保存前验证"""
|
||||
self.validate()
|
||||
|
||||
def on_update(self):
|
||||
"""更新后自动保存到文件"""
|
||||
if self.node_type and self.node_schema:
|
||||
save_node_schema(self.node_type, self.node_schema)
|
||||
|
||||
|
||||
# 独立的API端点函数
|
||||
@jingrow.whitelist()
|
||||
def load_node_schema(node_type):
|
||||
"""加载指定节点类型的 schema"""
|
||||
|
||||
schema_file = jingrow.get_app_path("jingrow", "ai", "pagetype", "ai_agent", "nodes", node_type, f"{node_type}.json")
|
||||
|
||||
if not os.path.exists(schema_file):
|
||||
return None
|
||||
|
||||
with open(schema_file, 'r', encoding='utf-8') as f:
|
||||
schema_data = json.load(f)
|
||||
jingrow.response["data"] = schema_data
|
||||
return None
|
||||
|
||||
|
||||
@jingrow.whitelist()
|
||||
def save_node_schema(node_type, schema_data):
|
||||
"""保存节点 schema 到文件"""
|
||||
# 如果是字符串,先解析为对象
|
||||
if isinstance(schema_data, str):
|
||||
schema_data = json.loads(schema_data)
|
||||
|
||||
schema_file = jingrow.get_app_path("jingrow", "ai", "pagetype", "ai_agent", "nodes", node_type, f"{node_type}.json")
|
||||
os.makedirs(os.path.dirname(schema_file), exist_ok=True)
|
||||
|
||||
with open(schema_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(schema_data, f, ensure_ascii=False, indent=2, separators=(',', ': '))
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2025, Jingrow and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import jingrow
|
||||
from jingrow.tests.utils import JingrowTestCase
|
||||
|
||||
|
||||
class TestAINodeSchema(JingrowTestCase):
|
||||
pass
|
||||
Loading…
x
Reference in New Issue
Block a user