删除冗余的文件夹

This commit is contained in:
jingrow 2025-09-11 03:57:42 +08:00
parent 922f2d75d4
commit 04121755b3
21 changed files with 0 additions and 6257 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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;

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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();

View File

@ -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
};
});

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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}`, "错误");
});
}

View File

@ -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
}

View File

@ -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=(',', ': '))

View File

@ -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