import type { AgentCapabilityKind, AgentDraftState, AgentInfo, AgentKnowledgeBinding, AgentOption, AgentSkillBinding, AgentSkillSummary, AgentToolBinding, AgentValidationIssue, } from '../types'; import { computed, reactive } from 'vue'; import { buildAgentExecutionConfig, createDefaultBuiltinTools, normalizeBuiltinTools, } from '../builtin-tools'; 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-]+$/; export interface AgentBindingChangeSet { knowledge: boolean; skill: boolean; tool: boolean; } 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: { builtinTools: createDefaultBuiltinTools(), 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, builtinTools: normalizeBuiltinTools( source.executionConfigJson?.builtinTools, ), 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, }; } function toSkillSummary(source?: Record): AgentSkillSummary { const raw = source || {}; return { binaryExcludedCount: Math.max( 0, Number(raw.binaryExcludedCount ?? raw.binaryResourceCount) || 0, ), description: String(raw.description || ''), displayName: String(raw.displayName || raw.name || '技能'), hasUpdate: Boolean(raw.hasUpdate), snapshotHash: String(raw.snapshotHash || ''), textResourceCount: Math.max(0, Number(raw.textResourceCount) || 0), toolCount: Math.max(0, Number(raw.toolCount) || 0), visibilityScope: String(raw.visibilityScope || 'PRIVATE'), }; } function normalizeSkillBinding( binding: AgentSkillBinding, index: number, ): AgentSkillBinding { return { id: binding.id, skillId: binding.skillId, resourceSummary: toSkillSummary(binding.resourceSummary), sortNo: binding.sortNo ?? index + 1, }; } export function useAgentDesignerState() { const state = reactive({ agent: createEmptyAgent(), knowledgeBindings: [], skillBindings: [], toolBindings: [], selectedNodeId: BASE_NODE_ID, panelMode: 'base', dirty: false, }); let savedBindingSignatures = { knowledge: '', skill: '', tool: '', }; 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, }; } if (state.selectedNodeId === 'skills') { return { kind: 'skill' as AgentCapabilityKind, binding: undefined, }; } 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.skillBindings = (agent?.skillBindings || []).map((binding, index) => normalizeSkillBinding(binding, index), ); state.selectedNodeId = BASE_NODE_ID; state.panelMode = 'base'; state.dirty = false; commitBindingBaseline(); } 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 appendSkillOptions(options: AgentOption[]) { let addedCount = 0; const existingIds = new Set( state.skillBindings.map((binding) => String(binding.skillId || '')), ); for (const option of options) { if (state.skillBindings.length >= 20) break; const skillId = String(option.value || '').trim(); if (!skillId || existingIds.has(skillId)) continue; state.skillBindings.push( normalizeSkillBinding( { skillId, resourceSummary: toSkillSummary({ ...option.raw, displayName: option.label, }), }, state.skillBindings.length, ), ); existingIds.add(skillId); addedCount += 1; } if (addedCount > 0) { state.selectedNodeId = 'skills'; state.panelMode = 'capability'; markDirty(); } } function replaceSkillBindings(bindings: AgentSkillBinding[]) { state.skillBindings = bindings.map((binding, index) => normalizeSkillBinding(binding, index), ); } function removeSkill(skillId?: number | string) { const normalizedId = String(skillId || ''); const next = state.skillBindings.filter( (binding) => String(binding.skillId || '') !== normalizedId, ); if (next.length === state.skillBindings.length) return; state.skillBindings = next.map((binding, index) => ({ ...binding, sortNo: index + 1, })); if (state.skillBindings.length === 0) selectBase(); markDirty(); } function moveSkill(skillId: number | string | undefined, offset: -1 | 1) { const currentIndex = state.skillBindings.findIndex( (binding) => String(binding.skillId || '') === String(skillId || ''), ); const targetIndex = currentIndex + offset; if ( currentIndex < 0 || targetIndex < 0 || targetIndex >= state.skillBindings.length ) { return; } const next = [...state.skillBindings]; const [current] = next.splice(currentIndex, 1); if (!current) return; next.splice(targetIndex, 0, current); state.skillBindings = next.map((binding, index) => ({ ...binding, sortNo: index + 1, })); markDirty(); } function removeSelectedCapability() { const selected = selectedCapability.value; if (!selected || selected.kind === 'skill') return; 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(options?: { canDisableShellApproval?: boolean; }): AgentInfo { const memoryConfigJson = state.agent.memoryConfigJson || {}; const compressionParameter = tokenOnlyCompressionParameter( memoryConfigJson.compressionParameter, ); const { maxAttachedMessageCount: _maxAttachedMessageCount, ...restMemoryConfigJson } = memoryConfigJson; const { knowledgeBindings: _knowledgeBindings, skillBindings: _skillBindings, toolBindings: _toolBindings, ...agent } = state.agent; return { ...agent, interactionConfigJson: buildInteractionConfigPayload( state.agent.interactionConfigJson, ), generationConfigJson: { ...state.agent.generationConfigJson, stream: state.agent.generationConfigJson?.stream !== false, }, executionConfigJson: buildAgentExecutionConfig( state.agent.executionConfigJson, Boolean(options?.canDisableShellApproval), ), memoryConfigJson: { ...restMemoryConfigJson, compressionParameter: { ...compressionParameter, enabled: true, }, }, status: state.agent.status ?? 1, }; } function buildKnowledgePayload(agentId?: number | string) { return state.knowledgeBindings.map((binding, index) => ({ agentId, knowledgeId: binding.knowledgeId, enabled: binding.enabled !== false, optionsJson: binding.optionsJson || {}, retrievalMode: binding.retrievalMode || 'HYBRID', sortNo: index + 1, })); } function buildToolPayload(agentId?: number | string) { return state.toolBindings.map((binding, index) => { const isMcp = String(binding.toolType || '').toUpperCase() === 'MCP'; return { agentId, enabled: binding.enabled !== false, hitlConfigJson: binding.hitlConfigJson || {}, hitlEnabled: Boolean(binding.hitlEnabled), optionsJson: binding.optionsJson || {}, targetId: binding.targetId, toolType: binding.toolType, toolName: isMcp ? '' : binding.toolName, sortNo: index + 1, }; }); } function buildSkillPayload() { return state.skillBindings.map((binding, index) => ({ skillId: binding.skillId, sortNo: index + 1, })); } function bindingSignature(value: unknown) { return JSON.stringify(canonicalValue(value)); } function canonicalValue(value: unknown): unknown { if (Array.isArray(value)) { return value.map((item) => canonicalValue(item)); } if (value && typeof value === 'object') { return Object.fromEntries( Object.entries(value as Record) .filter(([, item]) => item !== undefined) .sort(([first], [second]) => first.localeCompare(second)) .map(([key, item]) => [key, canonicalValue(item)]), ); } return value; } function currentBindingSignatures() { return { knowledge: bindingSignature(buildKnowledgePayload()), skill: bindingSignature(buildSkillPayload()), tool: bindingSignature(buildToolPayload()), }; } function getBindingChanges(): AgentBindingChangeSet { const current = currentBindingSignatures(); return { knowledge: current.knowledge !== savedBindingSignatures.knowledge, skill: current.skill !== savedBindingSignatures.skill, tool: current.tool !== savedBindingSignatures.tool, }; } function commitBindingBaseline() { savedBindingSignatures = currentBindingSignatures(); } reset(); return { BASE_NODE_ID, state, selectedCapability, addKnowledgeNode, appendSkillOptions, addToolNode, buildKnowledgePayload, buildPayloadAgent, buildSkillPayload, buildToolPayload, commitBindingBaseline, getBindingChanges, markDirty, moveSkill, openTryout, removeSkill, removeSelectedCapability, replaceSkillBindings, reset, selectBase, selectNode, validate, }; }