Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/agents/composables/useAgentDesignerState.ts
陈子默 6df3dd9981 feat: 支持智能体可见范围管理
- 未分类智能体按可见范围绕过分类白名单

- 提供个人、部门、公开范围配置及列表状态标签
2026-07-31 16:45:41 +08:00

457 lines
12 KiB
TypeScript

import type {
AgentCapabilityKind,
AgentDraftState,
AgentInfo,
AgentKnowledgeBinding,
AgentToolBinding,
AgentValidationIssue,
} from '../types';
import { computed, reactive } from 'vue';
import { DEFAULT_AGENT_COMPRESSION_TOKEN_THRESHOLD } from '../compression-threshold';
import {
buildInteractionConfigPayload,
createEmptyInteractionConfig,
normalizeInteractionConfig,
validateInteractionConfig,
} from '../interaction-config';
const BASE_NODE_ID = 'agent-base';
const SAFE_TOOL_NAME_PATTERN = /^[\w-]+$/;
function createLocalId(prefix: string) {
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
}
function isSafeToolName(name?: string) {
return SAFE_TOOL_NAME_PATTERN.test(String(name || ''));
}
function buildFallbackToolName(prefix: string, resource?: Record<string, any>) {
const id = resource?.id ? String(resource.id) : createLocalId(prefix);
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>,
) {
if (isSafeToolName(resource?.englishName)) {
return String(resource?.englishName);
}
if (isSafeToolName(resource?.name)) {
return String(resource?.name);
}
let prefix = 'plugin';
if (kind === 'workflow') {
prefix = 'workflow';
} else if (kind === 'mcp') {
prefix = 'mcp';
}
return buildFallbackToolName(prefix, resource);
}
function normalizeBindingToolName(binding: AgentToolBinding) {
if (String(binding.toolType || '').toUpperCase() === 'MCP') {
return '';
}
if (isSafeToolName(binding.toolName)) {
return String(binding.toolName);
}
const kind = toolKindFromType(binding.toolType);
const resource = {
...binding.resourceSnapshot,
...binding.resourceSummary,
id:
binding.targetId ||
binding.resourceSummary?.id ||
binding.resourceSnapshot?.id,
};
return resolveToolName(kind, resource);
}
function tokenOnlyCompressionParameter(
parameter?: Record<string, any>,
): Record<string, any> {
const normalized = { ...parameter };
delete normalized.msgThreshold;
return normalized;
}
export function createEmptyAgent(): AgentInfo {
return {
name: '未命名智能体',
description: '',
avatar: '',
categoryId: '',
visibilityScope: 'PRIVATE',
executionConfigJson: {
documentContextBudgetTokens: 20_000,
},
modelId: '',
promptConfigJson: { systemPrompt: '' },
generationConfigJson: { stream: true },
memoryConfigJson: {
compressionParameter: {
enabled: true,
lastKeep: 8,
minCompressionTokenThreshold: DEFAULT_AGENT_COMPRESSION_TOKEN_THRESHOLD,
},
},
interactionConfigJson: createEmptyInteractionConfig(),
status: 1,
};
}
function normalizeAgent(agent?: AgentInfo): AgentInfo {
const source = agent || {};
const memoryConfig = source.memoryConfigJson || {};
const compressionParameter = tokenOnlyCompressionParameter(
memoryConfig.compressionParameter,
);
const defaultCompressionParameter =
createEmptyAgent().memoryConfigJson?.compressionParameter || {};
return {
...createEmptyAgent(),
...source,
promptConfigJson: {
systemPrompt: '',
...source.promptConfigJson,
},
generationConfigJson: {
...source.generationConfigJson,
stream: source.generationConfigJson?.stream !== false,
},
executionConfigJson: {
...source.executionConfigJson,
documentContextBudgetTokens:
Number(source.executionConfigJson?.documentContextBudgetTokens) > 0
? Number(source.executionConfigJson?.documentContextBudgetTokens)
: 20_000,
},
memoryConfigJson: {
...memoryConfig,
compressionParameter: {
enabled: true,
...compressionParameter,
lastKeep:
compressionParameter.lastKeep ?? defaultCompressionParameter.lastKeep,
minCompressionTokenThreshold:
compressionParameter.minCompressionTokenThreshold ??
defaultCompressionParameter.minCompressionTokenThreshold,
},
},
interactionConfigJson: normalizeInteractionConfig(
source.interactionConfigJson,
),
};
}
function normalizeKnowledgeBinding(
binding: AgentKnowledgeBinding,
index: number,
): AgentKnowledgeBinding {
return {
...binding,
enabled: binding.enabled !== false,
localId:
binding.localId || String(binding.id || createLocalId('knowledge')),
optionsJson: {
limit: 5,
scoreThreshold: 0.5,
...binding.optionsJson,
},
retrievalMode: binding.retrievalMode || 'HYBRID',
sortNo: binding.sortNo ?? index + 1,
};
}
function normalizeToolBinding(
binding: AgentToolBinding,
index: number,
): AgentToolBinding {
const optionsJson = {
...binding.optionsJson,
};
if (String(optionsJson.executionMode || '').toUpperCase() !== 'ASYNC') {
optionsJson.executionMode = 'SYNC';
}
return {
...binding,
enabled: binding.enabled !== false,
hitlEnabled: Boolean(binding.hitlEnabled),
localId:
binding.localId ||
String(
binding.id ||
createLocalId(String(binding.toolType || 'tool').toLowerCase()),
),
toolName: normalizeBindingToolName(binding),
optionsJson,
sortNo: binding.sortNo ?? index + 1,
};
}
export function useAgentDesignerState() {
const state = reactive<AgentDraftState>({
agent: createEmptyAgent(),
knowledgeBindings: [],
toolBindings: [],
selectedNodeId: BASE_NODE_ID,
panelMode: 'base',
dirty: false,
});
const selectedCapability = computed(() => {
if (state.selectedNodeId.startsWith('knowledge:')) {
const localId = state.selectedNodeId.slice('knowledge:'.length);
return {
kind: 'knowledge' as AgentCapabilityKind,
binding: state.knowledgeBindings.find(
(item) => item.localId === localId,
),
};
}
if (state.selectedNodeId.startsWith('tool:')) {
const localId = state.selectedNodeId.slice('tool:'.length);
const binding = state.toolBindings.find(
(item) => item.localId === localId,
);
return {
kind: toolKindFromType(binding?.toolType) as AgentCapabilityKind,
binding,
};
}
return undefined;
});
function markDirty() {
state.dirty = true;
}
function reset(agent?: AgentInfo) {
state.agent = normalizeAgent(agent);
state.knowledgeBindings = (agent?.knowledgeBindings || []).map(
(binding, index) => normalizeKnowledgeBinding(binding, index),
);
state.toolBindings = (agent?.toolBindings || []).map((binding, index) =>
normalizeToolBinding(binding, index),
);
state.selectedNodeId = BASE_NODE_ID;
state.panelMode = 'base';
state.dirty = false;
}
function selectBase() {
state.selectedNodeId = BASE_NODE_ID;
state.panelMode = 'base';
}
function selectNode(nodeId: string) {
state.selectedNodeId = nodeId;
state.panelMode = nodeId === BASE_NODE_ID ? 'base' : 'capability';
}
function openTryout() {
state.panelMode = 'tryout';
}
function addKnowledgeNode(resource?: Record<string, any>) {
const binding = normalizeKnowledgeBinding(
{
knowledgeId: resource?.id ? String(resource.id) : '',
resourceSummary: resource || {},
},
state.knowledgeBindings.length,
);
state.knowledgeBindings.push(binding);
state.selectedNodeId = `knowledge:${binding.localId}`;
state.panelMode = 'capability';
markDirty();
}
function addToolNode(
kind: Exclude<AgentCapabilityKind, 'knowledge'>,
resource?: Record<string, any>,
) {
let toolType = 'PLUGIN';
if (kind === 'workflow') {
toolType = 'WORKFLOW';
} else if (kind === 'mcp') {
toolType = 'MCP';
}
const targetId = resource?.mcpId || resource?.id;
const binding = normalizeToolBinding(
{
toolType,
targetId: targetId ? String(targetId) : '',
toolName: kind === 'mcp' ? '' : resolveToolName(kind, resource),
resourceSummary: resource || {},
},
state.toolBindings.length,
);
state.toolBindings.push(binding);
state.selectedNodeId = `tool:${binding.localId}`;
state.panelMode = 'capability';
markDirty();
}
function removeSelectedCapability() {
const selected = selectedCapability.value;
if (!selected?.binding?.localId) return;
if (selected.kind === 'knowledge') {
state.knowledgeBindings = state.knowledgeBindings.filter(
(item) => item.localId !== selected.binding?.localId,
);
} else {
state.toolBindings = state.toolBindings.filter(
(item) => item.localId !== selected.binding?.localId,
);
}
selectBase();
markDirty();
}
function validate(): AgentValidationIssue[] {
const issues: AgentValidationIssue[] = [];
if (!String(state.agent.name || '').trim()) {
issues.push({
nodeId: BASE_NODE_ID,
field: 'name',
message: '请填写 Agent 名称',
});
}
if (!state.agent.modelId) {
issues.push({
nodeId: BASE_NODE_ID,
field: 'modelId',
message: '请选择模型',
});
}
issues.push(
...validateInteractionConfig(state.agent.interactionConfigJson),
);
state.knowledgeBindings.forEach((binding) => {
if (!binding.knowledgeId) {
issues.push({
nodeId: `knowledge:${binding.localId}`,
field: 'knowledgeId',
message: '请选择知识库',
});
}
});
state.toolBindings.forEach((binding) => {
const nodeId = `tool:${binding.localId}`;
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)) {
issues.push({
nodeId,
field: 'toolName',
message: '工具名称只能包含英文、数字、下划线或中划线',
});
}
});
return issues;
}
function buildPayloadAgent(): AgentInfo {
const memoryConfigJson = state.agent.memoryConfigJson || {};
const compressionParameter = tokenOnlyCompressionParameter(
memoryConfigJson.compressionParameter,
);
const {
maxAttachedMessageCount: _maxAttachedMessageCount,
...restMemoryConfigJson
} = memoryConfigJson;
return {
...state.agent,
interactionConfigJson: buildInteractionConfigPayload(
state.agent.interactionConfigJson,
),
generationConfigJson: {
...state.agent.generationConfigJson,
stream: state.agent.generationConfigJson?.stream !== false,
},
executionConfigJson: {
...state.agent.executionConfigJson,
documentContextBudgetTokens: Math.max(
1,
Math.trunc(
Number(
state.agent.executionConfigJson?.documentContextBudgetTokens,
) || 20_000,
),
),
},
memoryConfigJson: {
...restMemoryConfigJson,
compressionParameter: {
...compressionParameter,
enabled: true,
},
},
status: state.agent.status ?? 1,
};
}
function buildKnowledgePayload(agentId?: number | string) {
return state.knowledgeBindings.map((binding, index) => ({
...binding,
agentId,
enabled: binding.enabled !== false,
sortNo: index + 1,
}));
}
function buildToolPayload(agentId?: number | string) {
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();
return {
BASE_NODE_ID,
state,
selectedCapability,
addKnowledgeNode,
addToolNode,
buildKnowledgePayload,
buildPayloadAgent,
buildToolPayload,
markDirty,
openTryout,
removeSelectedCapability,
reset,
selectBase,
selectNode,
validate,
};
}