Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/agents/components/AgentToolForm.vue
陈子默 c316eff5be feat: 归档 XL10 异步工具业务编译层
- 将 AgentDefinitionCompiler 升级为 AgentRuntimeCompiler

- 接入 Workflow 和 Plugin 的同步/异步工具编译与 Redis 任务态

- 增加异步执行配置开关、聊天时间线聚合和后端测试
2026-06-04 15:23:56 +08:00

244 lines
5.8 KiB
Vue

<script setup lang="ts">
/* eslint-disable vue/no-mutating-props */
import type { AgentOption, AgentToolBinding } from '../types';
import { computed } from 'vue';
import {
ElButton,
ElEmpty,
ElForm,
ElFormItem,
ElInput,
ElOption,
ElSelect,
ElSwitch,
} from 'element-plus';
const props = defineProps<{
binding: AgentToolBinding;
kind: 'mcp' | 'plugin' | 'workflow';
options: AgentOption[];
}>();
const emit = defineEmits<{
change: [];
remove: [];
}>();
const SAFE_TOOL_NAME_PATTERN = /^[\w-]+$/;
function isSafeToolName(name?: string) {
return SAFE_TOOL_NAME_PATTERN.test(String(name || ''));
}
function resolveToolName(option?: AgentOption) {
const raw = option?.raw || {};
if (isSafeToolName(raw.englishName)) {
return String(raw.englishName);
}
if (isSafeToolName(raw.name)) {
return String(raw.name);
}
const prefix = props.kind === 'workflow' ? 'workflow' : 'plugin';
return `${prefix}_${option?.value || Date.now()}`;
}
function shouldSyncToolName() {
const name = String(props.binding.toolName || '');
if (!name) return true;
const prefix = props.kind === 'workflow' ? 'workflow_' : 'plugin_';
return name.startsWith(prefix);
}
const targetValue = computed({
get() {
return props.binding.targetId ? String(props.binding.targetId) : '';
},
set(value: string) {
props.binding.targetId = value;
},
});
const resourceLabel = computed(() => {
if (props.kind === 'workflow') return '工作流';
if (props.kind === 'mcp') return 'MCP';
return '插件工具';
});
const selectedMcpTools = computed(() => {
if (props.kind !== 'mcp') {
return [];
}
const option = props.options.find(
(item) => String(item.value) === String(props.binding.targetId),
);
const tools = option?.raw?.tools || props.binding.resourceSummary?.tools;
return Array.isArray(tools) ? tools : [];
});
const selectedMcpToolCount = computed(() => selectedMcpTools.value.length);
const asyncExecutionEnabled = computed({
get() {
return String(props.binding.optionsJson?.executionMode || '').toUpperCase() === 'ASYNC';
},
set(value: boolean) {
props.binding.optionsJson = {
...(props.binding.optionsJson || {}),
executionMode: value ? 'ASYNC' : 'SYNC',
};
emit('change');
},
});
function handleTargetChange(value: string) {
const option = props.options.find(
(item) => String(item.value) === String(value),
);
props.binding.resourceSummary = option?.raw || {};
if (props.kind === 'mcp') {
props.binding.targetId = option?.raw?.mcpId
? String(option.raw.mcpId)
: value;
props.binding.toolName = '';
emit('change');
return;
}
if (shouldSyncToolName()) {
props.binding.toolName = resolveToolName(option);
}
emit('change');
}
</script>
<template>
<ElForm label-position="top" class="agent-form">
<ElFormItem :label="resourceLabel" required>
<ElSelect
v-model="targetValue"
filterable
placeholder="选择资源"
@change="handleTargetChange"
>
<ElOption
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
<div v-if="kind === 'mcp'" class="agent-form__mcp-tools">
<div class="agent-form__mcp-tools-header">
<span>工具列表</span>
<span>{{ selectedMcpToolCount }} </span>
</div>
<div v-if="selectedMcpTools.length > 0" class="agent-form__mcp-tool-list">
<div
v-for="tool in selectedMcpTools"
:key="tool.name || tool.title"
class="agent-form__mcp-tool"
>
<div class="agent-form__mcp-tool-name">
{{ tool.name || tool.title || '未命名工具' }}
</div>
<div v-if="tool.description" class="agent-form__mcp-tool-desc">
{{ tool.description }}
</div>
</div>
</div>
<ElEmpty v-else description="暂无工具" :image-size="64" />
</div>
<ElFormItem v-if="kind !== 'mcp'" label="工具名称" required>
<ElInput
v-model="binding.toolName"
placeholder="仅支持英文、数字、下划线或中划线"
@input="emit('change')"
/>
</ElFormItem>
<ElFormItem label="执行前确认">
<ElSwitch v-model="binding.hitlEnabled" @change="emit('change')" />
</ElFormItem>
<ElFormItem v-if="kind !== 'mcp'" label="异步执行">
<ElSwitch v-model="asyncExecutionEnabled" />
</ElFormItem>
<ElButton
class="agent-form__danger"
type="danger"
plain
@click="emit('remove')"
>
删除节点
</ElButton>
</ElForm>
</template>
<style scoped>
.agent-form {
padding: 16px;
}
.agent-form :deep(.el-select) {
width: 100%;
}
.agent-form__mcp-tools {
margin-bottom: 16px;
}
.agent-form__mcp-tools-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.agent-form__mcp-tools-header span:last-child {
font-size: 12px;
font-weight: 500;
color: var(--el-text-color-secondary);
}
.agent-form__mcp-tool-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.agent-form__mcp-tool {
padding: 10px 12px;
background: var(--el-fill-color-lighter);
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
}
.agent-form__mcp-tool-name {
overflow: hidden;
font-size: 13px;
font-weight: 600;
line-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-form__mcp-tool-desc {
display: -webkit-box;
margin-top: 4px;
overflow: hidden;
font-size: 12px;
line-height: 18px;
color: var(--el-text-color-secondary);
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.agent-form__danger {
width: 100%;
margin-top: 8px;
}
</style>