457 lines
12 KiB
TypeScript
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,
|
|
};
|
|
}
|