- 增加 MCP 连接类型、环境检测接口和容器运行环境支持 - 将 Agent 编排改为绑定整体 MCP 并编译为 runtime McpSpec - 优化 MCP 工具展示、审批、草稿试运行和画布回显稳定性
476 lines
12 KiB
Vue
476 lines
12 KiB
Vue
<script setup lang="ts">
|
|
/* cspell:ignore tryit */
|
|
import type {
|
|
AgentCapabilityKind,
|
|
AgentOption,
|
|
AgentValidationIssue,
|
|
} from './types';
|
|
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
import { tryit } from 'radash';
|
|
|
|
import { api } from '#/api/request';
|
|
import {
|
|
canAiResourceOffline,
|
|
canAiResourcePublish,
|
|
canAiResourceRepublish,
|
|
isAiResourceApprovalPending,
|
|
} from '#/views/ai/shared/publish-status';
|
|
|
|
import {
|
|
getAgentDetail,
|
|
getAgentModels,
|
|
getPublishedKnowledgeList,
|
|
saveAgent,
|
|
submitAgentOfflineApproval,
|
|
submitAgentPublishApproval,
|
|
updateAgent,
|
|
updateAgentKnowledgeBindings,
|
|
updateAgentToolBindings,
|
|
} from './api';
|
|
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';
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const AGENT_TAB_PAGE_KEY = '/ai/agents';
|
|
const DEFAULT_AGENT_TITLE = '未命名智能体';
|
|
const {
|
|
state,
|
|
addKnowledgeNode,
|
|
addToolNode,
|
|
buildKnowledgePayload,
|
|
buildPayloadAgent,
|
|
buildToolPayload,
|
|
markDirty,
|
|
openTryout,
|
|
removeSelectedCapability,
|
|
reset,
|
|
selectBase,
|
|
selectNode,
|
|
validate,
|
|
} = useAgentDesignerState();
|
|
|
|
const pageLoading = ref(false);
|
|
const saveLoading = ref(false);
|
|
const offlineLoading = ref(false);
|
|
const publishLoading = ref(false);
|
|
const issues = ref<AgentValidationIssue[]>([]);
|
|
const categories = ref<AgentOption[]>([]);
|
|
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(() => {
|
|
if (
|
|
canAiResourceRepublish(
|
|
state.agent.displayPublishStatus,
|
|
state.agent.publishStatus,
|
|
)
|
|
) {
|
|
return '重新发布';
|
|
}
|
|
return '发布';
|
|
});
|
|
|
|
const offlineVisible = computed(() =>
|
|
canAiResourceOffline(
|
|
state.agent.displayPublishStatus,
|
|
state.agent.publishStatus,
|
|
),
|
|
);
|
|
|
|
const publishDisabled = computed(() => {
|
|
if (!state.agent.id) return true;
|
|
if (
|
|
isAiResourceApprovalPending(
|
|
state.agent.displayPublishStatus,
|
|
state.agent.publishStatus,
|
|
)
|
|
) {
|
|
return true;
|
|
}
|
|
return !(
|
|
canAiResourcePublish(
|
|
state.agent.displayPublishStatus,
|
|
state.agent.publishStatus,
|
|
) ||
|
|
canAiResourceRepublish(
|
|
state.agent.displayPublishStatus,
|
|
state.agent.publishStatus,
|
|
)
|
|
);
|
|
});
|
|
|
|
const offlineDisabled = computed(() => {
|
|
if (!state.agent.id) return true;
|
|
return !offlineVisible.value;
|
|
});
|
|
|
|
onMounted(async () => {
|
|
pageLoading.value = true;
|
|
try {
|
|
await Promise.all([loadOptions(), loadAgent()]);
|
|
} finally {
|
|
pageLoading.value = false;
|
|
}
|
|
});
|
|
|
|
async function loadAgent() {
|
|
if (isNew.value) {
|
|
reset();
|
|
syncNavTitle(DEFAULT_AGENT_TITLE, { force: true });
|
|
return;
|
|
}
|
|
const [, res] = await tryit(getAgentDetail)(String(route.params.id));
|
|
if (res?.errorCode === 0) {
|
|
reset(res.data);
|
|
syncNavTitle(resolveAgentTitle(res.data), { force: !hasNavTitle() });
|
|
}
|
|
}
|
|
|
|
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]
|
|
: route.query.navTitle;
|
|
return typeof navTitle === 'string' && navTitle.trim();
|
|
}
|
|
|
|
function resolveAgentTitle(agent = state.agent) {
|
|
return String(agent.name || '').trim() || DEFAULT_AGENT_TITLE;
|
|
}
|
|
|
|
function syncNavTitle(title: string, options: { force?: boolean } = {}) {
|
|
const normalizedTitle = String(title || '').trim() || DEFAULT_AGENT_TITLE;
|
|
const query = route.query as Record<string, any>;
|
|
const currentNavTitle = Array.isArray(query.navTitle)
|
|
? query.navTitle[0]
|
|
: query.navTitle;
|
|
const currentPageKey = Array.isArray(query.pageKey)
|
|
? query.pageKey[0]
|
|
: query.pageKey;
|
|
|
|
if (
|
|
!options.force &&
|
|
currentNavTitle === normalizedTitle &&
|
|
currentPageKey === AGENT_TAB_PAGE_KEY
|
|
) {
|
|
return;
|
|
}
|
|
|
|
router.replace({
|
|
path: route.path,
|
|
query: {
|
|
...query,
|
|
pageKey: AGENT_TAB_PAGE_KEY,
|
|
navTitle: normalizedTitle,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function loadOptions() {
|
|
const [categoryRes, modelRes, knowledgeRes, workflowRes, pluginRes, mcpRes] =
|
|
await Promise.all([
|
|
api.get('/api/v1/agentCategory/visibleList', {
|
|
params: { sortKey: 'sortNo', sortType: 'asc' },
|
|
}),
|
|
getAgentModels(),
|
|
getPublishedKnowledgeList(),
|
|
api.get('/api/v1/workflow/page', {
|
|
params: { pageNumber: 1, pageSize: 200 },
|
|
}),
|
|
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) => ({
|
|
label: item.categoryName || item.name,
|
|
value: String(item.id),
|
|
raw: item,
|
|
}));
|
|
models.value = (modelRes.data || []).map((item: any) => ({
|
|
label: item.title || item.name,
|
|
value: String(item.id),
|
|
raw: item,
|
|
}));
|
|
knowledges.value = (knowledgeRes.data || []).map((item: any) => ({
|
|
label: item.title || item.name,
|
|
value: String(item.id),
|
|
raw: item,
|
|
}));
|
|
workflows.value = (
|
|
(workflowRes.data?.records || workflowRes.data || []) as any[]
|
|
).map((item) => ({
|
|
label: item.title || item.name,
|
|
value: String(item.id),
|
|
raw: item,
|
|
}));
|
|
pluginTools.value = flattenPluginTools(
|
|
pluginRes.data?.records || pluginRes.data || [],
|
|
);
|
|
mcps.value = mapMcpOptions(mcpRes.data?.records || mcpRes.data || []);
|
|
}
|
|
|
|
function flattenPluginTools(list: any[]): AgentOption[] {
|
|
const result: AgentOption[] = [];
|
|
list.forEach((plugin) => {
|
|
const tools = Array.isArray(plugin.tools) ? plugin.tools : [];
|
|
tools.forEach((tool: any) => {
|
|
result.push({
|
|
label: tool.name || tool.title,
|
|
value: String(tool.id),
|
|
raw: { ...tool, pluginName: plugin.name || plugin.title },
|
|
});
|
|
});
|
|
});
|
|
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();
|
|
return;
|
|
}
|
|
addToolNode(kind);
|
|
}
|
|
|
|
function handleSelectNode(nodeId: string) {
|
|
selectNode(nodeId);
|
|
}
|
|
|
|
function handleSelectIssue(nodeId: string) {
|
|
selectNode(nodeId);
|
|
}
|
|
|
|
function runValidation() {
|
|
issues.value = validate();
|
|
if (issues.value.length > 0) {
|
|
selectNode(issues.value[0]!.nodeId);
|
|
ElMessage.warning('请先完成必要配置');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function handleSave(showMessage = true) {
|
|
if (!runValidation()) return false;
|
|
saveLoading.value = true;
|
|
try {
|
|
const agentPayload = buildPayloadAgent();
|
|
const agentRes = state.agent.id
|
|
? await updateAgent(agentPayload)
|
|
: await saveAgent(agentPayload);
|
|
if (agentRes.errorCode !== 0 || !agentRes.data?.id) {
|
|
return false;
|
|
}
|
|
|
|
const id = agentRes.data.id;
|
|
const toolBindingRes = await updateAgentToolBindings(
|
|
id,
|
|
buildToolPayload(id),
|
|
);
|
|
if (toolBindingRes.errorCode !== 0) {
|
|
return false;
|
|
}
|
|
const knowledgeBindingRes = await updateAgentKnowledgeBindings(
|
|
id,
|
|
buildKnowledgePayload(id),
|
|
);
|
|
if (knowledgeBindingRes.errorCode !== 0) {
|
|
return false;
|
|
}
|
|
|
|
state.agent = {
|
|
...state.agent,
|
|
...agentRes.data,
|
|
id,
|
|
};
|
|
state.dirty = false;
|
|
const title = resolveAgentTitle();
|
|
if (isNew.value) {
|
|
await router.replace({
|
|
path: `/ai/agents/designer/${id}`,
|
|
query: {
|
|
...route.query,
|
|
pageKey: AGENT_TAB_PAGE_KEY,
|
|
navTitle: title,
|
|
},
|
|
});
|
|
} else {
|
|
syncNavTitle(title, { force: true });
|
|
}
|
|
if (showMessage) {
|
|
ElMessage.success('已保存');
|
|
}
|
|
return true;
|
|
} finally {
|
|
saveLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function handlePublish() {
|
|
if (!state.agent.id) return;
|
|
const saved = await handleSave(false);
|
|
if (!saved) return;
|
|
|
|
try {
|
|
await ElMessageBox.confirm('确认提交发布审批?', '提示', {
|
|
confirmButtonText: '确认',
|
|
cancelButtonText: '取消',
|
|
type: 'info',
|
|
});
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
publishLoading.value = true;
|
|
try {
|
|
const res = await submitAgentPublishApproval(String(state.agent.id));
|
|
if (res.errorCode === 0) {
|
|
ElMessage.success(res.message || '已提交');
|
|
await refreshAgentLifecycleState();
|
|
}
|
|
} finally {
|
|
publishLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function handleOffline() {
|
|
if (!state.agent.id) return;
|
|
const saved = await handleSave(false);
|
|
if (!saved) return;
|
|
|
|
try {
|
|
await ElMessageBox.confirm('确认提交下线审批?', '提示', {
|
|
confirmButtonText: '确认',
|
|
cancelButtonText: '取消',
|
|
type: 'warning',
|
|
});
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
offlineLoading.value = true;
|
|
try {
|
|
const res = await submitAgentOfflineApproval(String(state.agent.id));
|
|
if (res.errorCode === 0) {
|
|
ElMessage.success(res.message || '已提交');
|
|
await refreshAgentLifecycleState();
|
|
}
|
|
} finally {
|
|
offlineLoading.value = false;
|
|
}
|
|
}
|
|
|
|
function handleTryout() {
|
|
if (!runValidation()) return;
|
|
openTryout();
|
|
}
|
|
|
|
function handleCloseTryout() {
|
|
selectBase();
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div v-loading="pageLoading" class="agent-designer">
|
|
<AgentStudioCanvas
|
|
:state="state"
|
|
:knowledge-options="knowledges"
|
|
:mcp-options="mcps"
|
|
:plugin-options="pluginTools"
|
|
:selected-node-id="state.selectedNodeId"
|
|
:workflow-options="workflows"
|
|
@select="handleSelectNode"
|
|
/>
|
|
<AgentInspectorPanel
|
|
:state="state"
|
|
:models="models"
|
|
:categories="categories"
|
|
:knowledges="knowledges"
|
|
:workflows="workflows"
|
|
:plugin-tools="pluginTools"
|
|
:mcps="mcps"
|
|
:issues="issues"
|
|
@change="markDirty"
|
|
@remove-capability="removeSelectedCapability"
|
|
@close-tryout="handleCloseTryout"
|
|
@select-issue="handleSelectIssue"
|
|
/>
|
|
<AgentCommandBar
|
|
:save-loading="saveLoading"
|
|
:publish-loading="publishLoading"
|
|
:publish-disabled="publishDisabled"
|
|
:publish-text="publishText"
|
|
:offline-disabled="offlineDisabled"
|
|
:offline-loading="offlineLoading"
|
|
:offline-visible="offlineVisible"
|
|
@add="handleAdd"
|
|
@save="handleSave()"
|
|
@offline="handleOffline"
|
|
@publish="handlePublish"
|
|
@tryout="handleTryout"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.agent-designer {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
background:
|
|
radial-gradient(
|
|
circle at 50% 42%,
|
|
var(--el-color-primary-light-9),
|
|
transparent 32%
|
|
),
|
|
var(--el-fill-color-extra-light);
|
|
}
|
|
</style>
|