Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/agents/AgentDesigner.vue
陈子默 c78074a969 feat: 按模型上下文自动配置智能体压缩阈值
- 从 llm.json 解析上下文与输出上限,并增强短模型 ID 匹配

- 切换模型、保存和试运行时按预算公式重算,目录缺失时回退 30K

- 修复模型列表接口未返回能力元数据
2026-07-30 14:20:10 +08:00

611 lines
16 KiB
Vue

<script setup lang="ts">
/* cspell:ignore tryit */
import type {
AgentCapabilityKind,
AgentOption,
AgentValidationIssue,
} from './types';
import { computed, onActivated, onDeactivated, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ArrowLeft } from '@element-plus/icons-vue';
import { ElButton, 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,
getMcpPage,
getMcpTools,
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';
import { resolveAgentCompressionTokenThreshold } from './compression-threshold';
import { createMcpToolLoader } from './mcpToolLoader';
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(true);
const contentReady = ref(false);
const canvasActive = ref(true);
const saveLoading = ref(false);
const offlineLoading = ref(false);
const publishLoading = ref(false);
const mcpToolsLoading = ref<Record<string, boolean>>({});
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 fetchMcpToolResource = createMcpToolLoader(async (id) => {
const res = await getMcpTools(id);
return res.errorCode === 0 ? res.data : undefined;
});
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 () => {
void loadDeferredOptions();
try {
await Promise.all([loadCriticalOptions(), loadAgent()]);
} finally {
contentReady.value = true;
pageLoading.value = false;
}
});
onActivated(() => {
canvasActive.value = true;
});
onDeactivated(() => {
canvasActive.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 loadCriticalOptions() {
const [categoryResult, modelResult] = await Promise.allSettled([
api.get('/api/v1/agentCategory/visibleList', {
params: { sortKey: 'sortNo', sortType: 'asc' },
}),
getAgentModels(),
]);
if (categoryResult.status === 'fulfilled') {
categories.value = (categoryResult.value.data || []).map((item: any) => ({
label: item.categoryName || item.name,
value: String(item.id),
raw: item,
}));
}
if (modelResult.status === 'fulfilled') {
models.value = (modelResult.value.data || []).map((item: any) => ({
label: item.title || item.name,
value: String(item.id),
raw: item,
}));
}
}
async function loadDeferredOptions() {
const [knowledgeResult, workflowResult, pluginResult, mcpResult] =
await Promise.allSettled([
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 },
}),
getMcpPage(),
]);
if (knowledgeResult.status === 'fulfilled') {
knowledges.value = (knowledgeResult.value.data || []).map((item: any) => ({
label: item.title || item.name,
value: String(item.id),
raw: item,
}));
}
if (workflowResult.status === 'fulfilled') {
workflows.value = (
(workflowResult.value.data?.records ||
workflowResult.value.data ||
[]) as any[]
).map((item) => ({
label: item.title || item.name,
value: String(item.id),
raw: item,
}));
}
if (pluginResult.status === 'fulfilled') {
pluginTools.value = flattenPluginTools(
pluginResult.value.data?.records || pluginResult.value.data || [],
);
}
if (mcpResult.status === 'fulfilled') {
mcps.value = mapMcpOptions(
mcpResult.value.data?.records || mcpResult.value.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 loadMcpToolsForOption(id: number | string) {
const key = String(id || '').trim();
if (!key || mcpToolsLoading.value[key]) return;
mcpToolsLoading.value = {
...mcpToolsLoading.value,
[key]: true,
};
try {
const resource = await fetchMcpToolResource(key);
if (!resource) return;
const currentOption = mcps.value.find((item) => String(item.value) === key);
const mergedResource = {
...currentOption?.raw,
...resource,
tools: Array.isArray(resource.tools) ? resource.tools : [],
};
if (currentOption) {
mcps.value = mcps.value.map((item) =>
String(item.value) === key
? {
...item,
label:
resource.title || resource.name || currentOption.label || 'MCP',
raw: mergedResource,
}
: item,
);
}
state.toolBindings.forEach((binding) => {
if (
String(binding.toolType || '').toUpperCase() === 'MCP' &&
String(binding.targetId || '') === key
) {
binding.resourceSummary = {
...binding.resourceSummary,
...mergedResource,
};
}
});
} catch (error) {
console.error('加载 MCP 工具失败', error);
} finally {
const nextLoading = { ...mcpToolsLoading.value };
delete nextLoading[key];
mcpToolsLoading.value = nextLoading;
}
}
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;
}
function syncAgentCompressionThreshold() {
const selectedModel = models.value.find(
(model) => model.value === String(state.agent.modelId ?? ''),
);
const memoryConfig = (state.agent.memoryConfigJson ||= {});
const compressionParameter = (memoryConfig.compressionParameter ||= {});
compressionParameter.minCompressionTokenThreshold =
resolveAgentCompressionTokenThreshold({
contextWindowTokens: selectedModel?.raw?.contextWindowTokens,
generationConfig: state.agent.generationConfigJson,
maxOutputTokens: selectedModel?.raw?.maxOutputTokens,
systemPrompt: state.agent.promptConfigJson?.systemPrompt,
toolBindings: state.toolBindings,
});
}
async function handleSave(showMessage = true) {
if (!runValidation()) return false;
syncAgentCompressionThreshold();
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;
syncAgentCompressionThreshold();
openTryout();
}
function handleCloseTryout() {
selectBase();
}
function handleBack() {
router.push(AGENT_TAB_PAGE_KEY);
}
</script>
<template>
<div v-loading="pageLoading" class="agent-designer">
<ElButton
class="agent-designer__back"
:icon="ArrowLeft"
aria-label="返回智能体列表"
@click="handleBack"
>
返回智能体列表
</ElButton>
<template v-if="contentReady">
<AgentStudioCanvas
:active="canvasActive"
: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"
:mcp-tools-loading="mcpToolsLoading"
:issues="issues"
@change="markDirty"
@remove-capability="removeSelectedCapability"
@close-tryout="handleCloseTryout"
@load-mcp-tools="loadMcpToolsForOption"
@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"
/>
</template>
</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);
}
.agent-designer__back {
position: absolute;
top: var(--space-6);
left: var(--space-6);
z-index: 10;
border-radius: var(--radius-control);
box-shadow: var(--shadow-subtle);
}
</style>