- 接入 AG-UI 运行投影、Turn 时间线和审批隔离 - 增加 Agent Skill 冻结绑定与运行时消费闭环 - 增加受控工作区、内置工具和私有 Artifact 生命周期
655 lines
18 KiB
TypeScript
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,
|
|
};
|
|
}
|