Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/agents/composables/useAgentDesignerState.ts
陈子默 4e8640dcaf feat: 完善 Agent 标准交互与安全运行时
- 接入 AG-UI 运行投影、Turn 时间线和审批隔离

- 增加 Agent Skill 冻结绑定与运行时消费闭环

- 增加受控工作区、内置工具和私有 Artifact 生命周期
2026-08-19 22:13:41 +08:00

655 lines
18 KiB
TypeScript

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