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) { const id = resource?.id ? String(resource.id) : createLocalId(prefix); return `${prefix}_${id}`; } function toolKindFromType( toolType?: string, ): Exclude { const normalized = String(toolType || '').toUpperCase(); if (normalized === 'WORKFLOW') return 'workflow'; if (normalized === 'MCP') return 'mcp'; return 'plugin'; } function resolveToolName( kind: Exclude, resource?: Record, ) { 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, ): Record { 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({ 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) { 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, resource?: Record, ) { 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, }; }