feat: 完成 Agent MCP 对接
- 增加 MCP 连接类型、环境检测接口和容器运行环境支持 - 将 Agent 编排改为绑定整体 MCP 并编译为 runtime McpSpec - 优化 MCP 工具展示、审批、草稿试运行和画布回显稳定性
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type {AgentCapabilityKind} from '../types';
|
||||
import type { AgentCapabilityKind } from '../types';
|
||||
|
||||
import {computed, onBeforeUnmount, onMounted, ref} from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
|
||||
import {Connection, Files, Loading, Plus, Share, VideoPlay,} from '@element-plus/icons-vue';
|
||||
import {
|
||||
Connection,
|
||||
Files,
|
||||
Link,
|
||||
Loading,
|
||||
Plus,
|
||||
Share,
|
||||
VideoPlay,
|
||||
} from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps<{
|
||||
offlineDisabled?: boolean;
|
||||
@@ -47,6 +55,12 @@ const capabilityItems = [
|
||||
desc: '执行工具能力',
|
||||
icon: Connection,
|
||||
},
|
||||
{
|
||||
kind: 'mcp' as const,
|
||||
title: 'MCP',
|
||||
desc: '连接外部工具',
|
||||
icon: Link,
|
||||
},
|
||||
];
|
||||
|
||||
function handleAdd(kind: AgentCapabilityKind) {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type {AgentDraftState, AgentOption, AgentValidationIssue,} from '../types';
|
||||
import type {
|
||||
AgentDraftState,
|
||||
AgentOption,
|
||||
AgentValidationIssue,
|
||||
} from '../types';
|
||||
|
||||
import {computed} from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import {Close} from '@element-plus/icons-vue';
|
||||
import {ElButton} from 'element-plus';
|
||||
import { Close } from '@element-plus/icons-vue';
|
||||
import { ElButton } from 'element-plus';
|
||||
|
||||
import AgentBaseForm from './AgentBaseForm.vue';
|
||||
import AgentKnowledgeForm from './AgentKnowledgeForm.vue';
|
||||
@@ -15,6 +19,7 @@ const props = defineProps<{
|
||||
categories: AgentOption[];
|
||||
issues: AgentValidationIssue[];
|
||||
knowledges: AgentOption[];
|
||||
mcps: AgentOption[];
|
||||
models: AgentOption[];
|
||||
pluginTools: AgentOption[];
|
||||
state: AgentDraftState;
|
||||
@@ -40,11 +45,18 @@ const selectedTool = computed(() => {
|
||||
return props.state.toolBindings.find((item) => item.localId === localId);
|
||||
});
|
||||
|
||||
const selectedToolKind = computed(() =>
|
||||
String(selectedTool.value?.toolType || '').toUpperCase() === 'WORKFLOW'
|
||||
? 'workflow'
|
||||
: 'plugin',
|
||||
);
|
||||
const selectedToolKind = computed(() => {
|
||||
const toolType = String(selectedTool.value?.toolType || '').toUpperCase();
|
||||
if (toolType === 'WORKFLOW') return 'workflow';
|
||||
if (toolType === 'MCP') return 'mcp';
|
||||
return 'plugin';
|
||||
});
|
||||
|
||||
const selectedToolOptions = computed(() => {
|
||||
if (selectedToolKind.value === 'workflow') return props.workflows;
|
||||
if (selectedToolKind.value === 'mcp') return props.mcps;
|
||||
return props.pluginTools;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -100,7 +112,7 @@ const selectedToolKind = computed(() =>
|
||||
v-else-if="selectedTool"
|
||||
:binding="selectedTool"
|
||||
:kind="selectedToolKind"
|
||||
:options="selectedToolKind === 'workflow' ? workflows : pluginTools"
|
||||
:options="selectedToolOptions"
|
||||
@change="emit('change')"
|
||||
@remove="emit('removeCapability')"
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-mutating-props */
|
||||
import type {AgentOption, AgentToolBinding} from '../types';
|
||||
import type { AgentOption, AgentToolBinding } from '../types';
|
||||
|
||||
import {ElButton, ElForm, ElFormItem, ElInput, ElOption, ElSelect, ElSwitch,} from 'element-plus';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElEmpty,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
} from 'element-plus';
|
||||
|
||||
const props = defineProps<{
|
||||
binding: AgentToolBinding;
|
||||
kind: 'plugin' | 'workflow';
|
||||
kind: 'mcp' | 'plugin' | 'workflow';
|
||||
options: AgentOption[];
|
||||
}>();
|
||||
|
||||
@@ -15,7 +26,7 @@ const emit = defineEmits<{
|
||||
remove: [];
|
||||
}>();
|
||||
|
||||
const SAFE_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
||||
const SAFE_TOOL_NAME_PATTERN = /^[\w-]+$/;
|
||||
|
||||
function isSafeToolName(name?: string) {
|
||||
return SAFE_TOOL_NAME_PATTERN.test(String(name || ''));
|
||||
@@ -40,11 +51,47 @@ function shouldSyncToolName() {
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -54,9 +101,9 @@ function handleTargetChange(value: string) {
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" class="agent-form">
|
||||
<ElFormItem :label="kind === 'workflow' ? '工作流' : '插件工具'" required>
|
||||
<ElFormItem :label="resourceLabel" required>
|
||||
<ElSelect
|
||||
v-model="binding.targetId"
|
||||
v-model="targetValue"
|
||||
filterable
|
||||
placeholder="选择资源"
|
||||
@change="handleTargetChange"
|
||||
@@ -69,7 +116,28 @@ function handleTargetChange(value: string) {
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="工具名称" required>
|
||||
<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="仅支持英文、数字、下划线或中划线"
|
||||
@@ -99,6 +167,59 @@ function handleTargetChange(value: string) {
|
||||
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;
|
||||
|
||||
@@ -34,8 +34,11 @@ import '@tinyflow-ai/vue/dist/index.css';
|
||||
|
||||
const props = defineProps<{
|
||||
knowledgeOptions?: AgentOption[];
|
||||
mcpOptions?: AgentOption[];
|
||||
pluginOptions?: AgentOption[];
|
||||
selectedNodeId: string;
|
||||
state: AgentDraftState;
|
||||
workflowOptions?: AgentOption[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -52,6 +55,11 @@ const canvasModel = useAgentStudioModel(
|
||||
layout,
|
||||
() => canvasSize.value,
|
||||
() => props.knowledgeOptions || [],
|
||||
() => ({
|
||||
mcp: props.mcpOptions || [],
|
||||
plugin: props.pluginOptions || [],
|
||||
workflow: props.workflowOptions || [],
|
||||
}),
|
||||
);
|
||||
const liveNodes = ref<AgentStudioNodeView[]>([]);
|
||||
const liveViewport = ref<AgentStudioViewport>({ x: 250, y: 100, zoom: 1 });
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type {AgentStudioNodeData} from './types';
|
||||
import type { AgentStudioNodeData } from './types';
|
||||
|
||||
import {computed} from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import {Connection, Cpu, Files, Share} from '@element-plus/icons-vue';
|
||||
import {ElIcon} from 'element-plus';
|
||||
import { Connection, Cpu, Files, Link, Share } from '@element-plus/icons-vue';
|
||||
import { ElIcon } from 'element-plus';
|
||||
|
||||
const props = defineProps<{
|
||||
data: AgentStudioNodeData;
|
||||
@@ -14,6 +14,7 @@ const iconComponent = computed(() => {
|
||||
const icons = {
|
||||
base: Cpu,
|
||||
knowledge: Files,
|
||||
mcp: Link,
|
||||
plugin: Connection,
|
||||
workflow: Share,
|
||||
};
|
||||
@@ -108,6 +109,7 @@ const iconComponent = computed(() => {
|
||||
}
|
||||
|
||||
.agent-studio-node--knowledge,
|
||||
.agent-studio-node--mcp,
|
||||
.agent-studio-node--workflow,
|
||||
.agent-studio-node--plugin {
|
||||
min-height: 78px;
|
||||
|
||||
Reference in New Issue
Block a user