feat: 完成 Agent MCP 对接

- 增加 MCP 连接类型、环境检测接口和容器运行环境支持

- 将 Agent 编排改为绑定整体 MCP 并编译为 runtime McpSpec

- 优化 MCP 工具展示、审批、草稿试运行和画布回显稳定性
This commit is contained in:
2026-05-29 11:09:21 +08:00
parent e39f7521e2
commit cc3bb9cff0
33 changed files with 2405 additions and 127 deletions

View File

@@ -1,14 +1,18 @@
<script setup lang="ts">
/* cspell:ignore tryit */
import type {AgentCapabilityKind, AgentOption, AgentValidationIssue,} from './types';
import type {
AgentCapabilityKind,
AgentOption,
AgentValidationIssue,
} from './types';
import {computed, onMounted, ref} from 'vue';
import {useRoute, useRouter} from 'vue-router';
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import {ElMessage, ElMessageBox} from 'element-plus';
import {tryit} from 'radash';
import { ElMessage, ElMessageBox } from 'element-plus';
import { tryit } from 'radash';
import {api} from '#/api/request';
import { api } from '#/api/request';
import {
canAiResourceOffline,
canAiResourcePublish,
@@ -30,7 +34,7 @@ import {
import AgentStudioCanvas from './components/agent-studio/AgentStudioCanvas.vue';
import AgentCommandBar from './components/AgentCommandBar.vue';
import AgentInspectorPanel from './components/AgentInspectorPanel.vue';
import {useAgentDesignerState} from './composables/useAgentDesignerState';
import { useAgentDesignerState } from './composables/useAgentDesignerState';
const route = useRoute();
const router = useRouter();
@@ -62,6 +66,7 @@ const models = ref<AgentOption[]>([]);
const knowledges = ref<AgentOption[]>([]);
const workflows = ref<AgentOption[]>([]);
const pluginTools = ref<AgentOption[]>([]);
const mcps = ref<AgentOption[]>([]);
const isNew = computed(() => String(route.params.id || '') === 'new');
const publishText = computed(() => {
@@ -132,6 +137,21 @@ async function loadAgent() {
}
}
async function refreshAgentLifecycleState() {
if (!state.agent.id) return;
const [, res] = await tryit(getAgentDetail)(String(state.agent.id));
if (res?.errorCode !== 0 || !res.data) {
return;
}
const agentState = { ...res.data };
delete agentState.knowledgeBindings;
delete agentState.toolBindings;
state.agent = {
...state.agent,
...agentState,
};
}
function hasNavTitle() {
const navTitle = Array.isArray(route.query.navTitle)
? route.query.navTitle[0]
@@ -172,7 +192,7 @@ function syncNavTitle(title: string, options: { force?: boolean } = {}) {
}
async function loadOptions() {
const [categoryRes, modelRes, knowledgeRes, workflowRes, pluginRes] =
const [categoryRes, modelRes, knowledgeRes, workflowRes, pluginRes, mcpRes] =
await Promise.all([
api.get('/api/v1/agentCategory/visibleList', {
params: { sortKey: 'sortNo', sortType: 'asc' },
@@ -185,6 +205,9 @@ async function loadOptions() {
api.get('/api/v1/plugin/pageByCategory', {
params: { pageNumber: 1, pageSize: 200, category: 0 },
}),
api.get('/api/v1/mcp/pageTools', {
params: { pageNumber: 1, pageSize: 200, status: 1 },
}),
]);
categories.value = (categoryRes.data || []).map((item: any) => ({
@@ -212,6 +235,7 @@ async function loadOptions() {
pluginTools.value = flattenPluginTools(
pluginRes.data?.records || pluginRes.data || [],
);
mcps.value = mapMcpOptions(mcpRes.data?.records || mcpRes.data || []);
}
function flattenPluginTools(list: any[]): AgentOption[] {
@@ -229,6 +253,22 @@ function flattenPluginTools(list: any[]): AgentOption[] {
return result;
}
function mapMcpOptions(list: any[]): AgentOption[] {
return list.map((mcp) => ({
label: mcp.title || mcp.name || 'MCP',
value: String(mcp.id),
raw: {
...mcp,
id: mcp.id,
mcpId: mcp.id,
mcpTitle: mcp.title || mcp.name,
title: mcp.title || mcp.name,
tools: Array.isArray(mcp.tools) ? mcp.tools : [],
approvalRequired: Boolean(mcp.approvalRequired),
},
}));
}
async function handleAdd(kind: AgentCapabilityKind) {
if (kind === 'knowledge') {
addKnowledgeNode();
@@ -331,7 +371,7 @@ async function handlePublish() {
const res = await submitAgentPublishApproval(String(state.agent.id));
if (res.errorCode === 0) {
ElMessage.success(res.message || '已提交');
await loadAgent();
await refreshAgentLifecycleState();
}
} finally {
publishLoading.value = false;
@@ -358,7 +398,7 @@ async function handleOffline() {
const res = await submitAgentOfflineApproval(String(state.agent.id));
if (res.errorCode === 0) {
ElMessage.success(res.message || '已提交');
await loadAgent();
await refreshAgentLifecycleState();
}
} finally {
offlineLoading.value = false;
@@ -380,7 +420,10 @@ function handleCloseTryout() {
<AgentStudioCanvas
:state="state"
:knowledge-options="knowledges"
:mcp-options="mcps"
:plugin-options="pluginTools"
:selected-node-id="state.selectedNodeId"
:workflow-options="workflows"
@select="handleSelectNode"
/>
<AgentInspectorPanel
@@ -390,6 +433,7 @@ function handleCloseTryout() {
:knowledges="knowledges"
:workflows="workflows"
:plugin-tools="pluginTools"
:mcps="mcps"
:issues="issues"
@change="markDirty"
@remove-capability="removeSelectedCapability"

View File

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

View File

@@ -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')"
/>

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import {describe, expect, it} from 'vitest';
import {resolveCapabilityNodePosition} from './useAgentStudioModel';
import {useAgentStudioModel, resolveCapabilityNodePosition} from './useAgentStudioModel';
describe('resolveCapabilityNodePosition', () => {
it('无视口信息时沿用默认左侧列位置', () => {
@@ -57,3 +57,47 @@ describe('resolveCapabilityNodePosition', () => {
).toEqual({ x: 128, y: 256 });
});
});
describe('useAgentStudioModel', () => {
it('MCP 绑定缺少资源快照时从选项中回显节点信息', () => {
const model = useAgentStudioModel(
{
agent: {
name: '测试智能体',
},
dirty: false,
knowledgeBindings: [],
panelMode: 'capability',
selectedNodeId: 'tool:mcp-1',
toolBindings: [
{
localId: 'mcp-1',
targetId: '1001',
toolType: 'MCP',
},
],
},
() => 'tool:mcp-1',
undefined,
undefined,
undefined,
() => ({
mcp: [
{
label: 'context7',
value: '1001',
raw: {
id: '1001',
title: 'context7',
tools: [{ name: 'resolve-library-id' }, { name: 'query-docs' }],
},
},
],
}),
);
const mcpNode = model.value.nodes.find((node) => node.id === 'tool:mcp-1');
expect(mcpNode?.data.title).toBe('context7 · 2 个工具');
expect(mcpNode?.data.detail).toBe('context7 · 2 个工具');
});
});

View File

@@ -12,7 +12,7 @@ import type {
AgentToolBinding,
} from '../../types';
import {computed} from 'vue';
import { computed } from 'vue';
const BASE_NODE_ID = 'agent-base';
const BASE_POSITION = { x: 430, y: 260 };
@@ -57,19 +57,66 @@ function buildKnowledgeTitle(
);
}
function buildToolTitle(binding: AgentToolBinding) {
function findMatchedToolOption(
binding: AgentToolBinding,
options: AgentOption[],
) {
const targetId = String(binding.targetId || '');
if (!targetId) return undefined;
return options.find((item) => {
const raw = item.raw || {};
return (
String(item.value) === targetId ||
String(raw.id || '') === targetId ||
String(raw.mcpId || '') === targetId
);
});
}
function buildToolTitle(binding: AgentToolBinding, options: AgentOption[] = []) {
const matchedOption = findMatchedToolOption(binding, options);
return firstText(
binding.resourceSummary?.title,
binding.resourceSummary?.name,
binding.resourceSummary?.label,
binding.resourceSummary?.displayName,
binding.resourceSummary?.mcpTitle,
binding.resourceSnapshot?.title,
binding.resourceSnapshot?.name,
binding.resourceSnapshot?.label,
binding.resourceSnapshot?.displayName,
binding.resourceSnapshot?.mcpTitle,
matchedOption?.label,
matchedOption?.raw?.title,
matchedOption?.raw?.name,
matchedOption?.raw?.label,
matchedOption?.raw?.displayName,
matchedOption?.raw?.mcpTitle,
binding.toolName,
);
}
function buildToolDetail(binding: AgentToolBinding, fallback: string) {
function buildToolDetail(
binding: AgentToolBinding,
fallback: string,
options: AgentOption[] = [],
) {
if (String(binding.toolType || '').toUpperCase() === 'MCP') {
const matchedOption = findMatchedToolOption(binding, options);
const resourceName = buildToolTitle(binding, options);
const tools =
binding.resourceSummary?.tools ||
binding.resourceSnapshot?.tools ||
matchedOption?.raw?.tools ||
[];
const toolCount = Array.isArray(tools) ? tools.length : 0;
if (resourceName && toolCount > 0) {
return `${resourceName} · ${toolCount} 个工具`;
}
return resourceName || fallback;
}
const toolName = firstText(binding.toolName);
const resourceName = buildToolTitle(binding);
const resourceName = buildToolTitle(binding, options);
if (toolName && resourceName && toolName !== resourceName) {
return `${resourceName} / ${toolName}`;
}
@@ -157,6 +204,11 @@ export function useAgentStudioModel(
layout?: AgentStudioLayoutSnapshot,
canvasSize?: () => AgentStudioCanvasSize | undefined,
knowledgeOptions?: () => AgentOption[],
toolOptions?: () => {
mcp?: AgentOption[];
plugin?: AgentOption[];
workflow?: AgentOption[];
},
) {
return computed(() => {
const positionOf = (nodeId: string, fallback: { x: number; y: number }) =>
@@ -214,10 +266,20 @@ export function useAgentStudioModel(
});
const toolNodes = state.toolBindings.map((binding, index) => {
const nodeId = `tool:${binding.localId}`;
const isWorkflow =
String(binding.toolType || '').toUpperCase() === 'WORKFLOW';
const fallback = isWorkflow ? '待选择工作流' : '待选择插件工具';
const detail = buildToolDetail(binding, fallback);
const toolType = String(binding.toolType || '').toUpperCase();
const isWorkflow = toolType === 'WORKFLOW';
const isMcp = toolType === 'MCP';
const matchedOptions = isWorkflow
? toolOptions?.().workflow || []
: isMcp
? toolOptions?.().mcp || []
: toolOptions?.().plugin || [];
const fallback = isWorkflow
? '待选择工作流'
: isMcp
? '待选择 MCP'
: '待选择插件工具';
const detail = buildToolDetail(binding, fallback, matchedOptions);
const position = resolveCapabilityNodePosition({
canvasSize: size,
fallbackIndex: state.knowledgeBindings.length + index,
@@ -233,14 +295,20 @@ export function useAgentStudioModel(
width: CAPABILITY_NODE_WIDTH,
height: CAPABILITY_NODE_HEIGHT,
data: {
badge: isWorkflow ? '工作流' : '插件',
badge: isWorkflow ? '工作流' : isMcp ? 'MCP' : '插件',
detail,
iconKey: isWorkflow ? 'workflow' : 'plugin',
iconKey: isWorkflow ? 'workflow' : isMcp ? 'mcp' : 'plugin',
id: nodeId,
kind: isWorkflow ? 'workflow' : 'plugin',
kind: isWorkflow ? 'workflow' : isMcp ? 'mcp' : 'plugin',
selected: selectedNodeId() === nodeId,
title:
detail === fallback ? (isWorkflow ? '工作流' : '插件') : detail,
detail === fallback
? isWorkflow
? '工作流'
: isMcp
? 'MCP'
: '插件'
: detail,
} satisfies AgentStudioNodeData,
};
});

View File

@@ -7,10 +7,10 @@ import type {
AgentValidationIssue,
} from '../types';
import {computed, reactive} from 'vue';
import { computed, reactive } from 'vue';
const BASE_NODE_ID = 'agent-base';
const SAFE_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
const SAFE_TOOL_NAME_PATTERN = /^[\w-]+$/;
function createLocalId(prefix: string) {
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
@@ -25,6 +25,15 @@ function buildFallbackToolName(prefix: string, resource?: Record<string, any>) {
return `${prefix}_${id}`;
}
function toolKindFromType(
toolType?: string,
): Exclude<AgentCapabilityKind, 'knowledge'> {
const normalized = String(toolType || '').toUpperCase();
if (normalized === 'WORKFLOW') return 'workflow';
if (normalized === 'MCP') return 'mcp';
return 'plugin';
}
function resolveToolName(
kind: Exclude<AgentCapabilityKind, 'knowledge'>,
resource?: Record<string, any>,
@@ -36,19 +45,19 @@ function resolveToolName(
return String(resource?.name);
}
return buildFallbackToolName(
kind === 'workflow' ? 'workflow' : 'plugin',
kind === 'workflow' ? 'workflow' : kind === 'mcp' ? 'mcp' : 'plugin',
resource,
);
}
function normalizeBindingToolName(binding: AgentToolBinding) {
if (String(binding.toolType || '').toUpperCase() === 'MCP') {
return '';
}
if (isSafeToolName(binding.toolName)) {
return String(binding.toolName);
}
const kind =
String(binding.toolType || '').toUpperCase() === 'WORKFLOW'
? 'workflow'
: 'plugin';
const kind = toolKindFromType(binding.toolType);
const resource = {
...(binding.resourceSnapshot || {}),
...(binding.resourceSummary || {}),
@@ -178,10 +187,7 @@ export function useAgentDesignerState() {
(item) => item.localId === localId,
);
return {
kind:
String(binding?.toolType || '').toUpperCase() === 'WORKFLOW'
? ('workflow' as AgentCapabilityKind)
: ('plugin' as AgentCapabilityKind),
kind: toolKindFromType(binding?.toolType) as AgentCapabilityKind,
binding,
};
}
@@ -237,12 +243,17 @@ export function useAgentDesignerState() {
kind: Exclude<AgentCapabilityKind, 'knowledge'>,
resource?: Record<string, any>,
) {
const toolType = kind === 'workflow' ? 'WORKFLOW' : 'PLUGIN';
const toolType =
kind === 'workflow' ? 'WORKFLOW' : kind === 'mcp' ? 'MCP' : 'PLUGIN';
const binding = normalizeToolBinding(
{
toolType,
targetId: resource?.id ? String(resource.id) : '',
toolName: resolveToolName(kind, resource),
targetId: resource?.mcpId
? String(resource.mcpId)
: resource?.id
? String(resource.id)
: '',
toolName: kind === 'mcp' ? '' : resolveToolName(kind, resource),
resourceSummary: resource || {},
},
state.toolBindings.length,
@@ -299,6 +310,9 @@ export function useAgentDesignerState() {
if (!binding.targetId) {
issues.push({ nodeId, field: 'targetId', message: '请选择能力资源' });
}
if (String(binding.toolType || '').toUpperCase() === 'MCP') {
return;
}
if (!String(binding.toolName || '').trim()) {
issues.push({ nodeId, field: 'toolName', message: '请填写工具名称' });
} else if (!isSafeToolName(binding.toolName)) {
@@ -340,13 +354,17 @@ export function useAgentDesignerState() {
}
function buildToolPayload(agentId?: number | string) {
return state.toolBindings.map((binding, index) => ({
...binding,
agentId,
enabled: binding.enabled !== false,
hitlEnabled: Boolean(binding.hitlEnabled),
sortNo: index + 1,
}));
return state.toolBindings.map((binding, index) => {
const isMcp = String(binding.toolType || '').toUpperCase() === 'MCP';
return {
...binding,
agentId,
enabled: binding.enabled !== false,
hitlEnabled: Boolean(binding.hitlEnabled),
toolName: isMcp ? '' : binding.toolName,
sortNo: index + 1,
};
});
}
reset();

View File

@@ -1,7 +1,7 @@
/* cspell:ignore hitl */
export type AgentPanelMode = 'base' | 'capability' | 'tryout';
export type AgentCapabilityKind = 'knowledge' | 'plugin' | 'workflow';
export type AgentCapabilityKind = 'knowledge' | 'plugin' | 'workflow' | 'mcp';
export interface AgentInfo {
id?: number | string;
@@ -33,7 +33,7 @@ export interface AgentInfo {
export interface AgentToolBinding {
id?: number | string;
agentId?: number | string;
toolType: 'PLUGIN' | 'WORKFLOW' | string;
toolType: 'MCP' | 'PLUGIN' | 'WORKFLOW' | string;
targetId?: number | string;
toolName?: string;
enabled?: boolean;