feat: 完善 Agent 标准交互与安全运行时
- 接入 AG-UI 运行投影、Turn 时间线和审批隔离 - 增加 Agent Skill 冻结绑定与运行时消费闭环 - 增加受控工作区、内置工具和私有 Artifact 生命周期
This commit is contained in:
@@ -3,12 +3,20 @@ import type {
|
||||
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,
|
||||
@@ -20,6 +28,12 @@ import {
|
||||
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)}`;
|
||||
}
|
||||
@@ -35,7 +49,7 @@ function buildFallbackToolName(prefix: string, resource?: Record<string, any>) {
|
||||
|
||||
function toolKindFromType(
|
||||
toolType?: string,
|
||||
): Exclude<AgentCapabilityKind, 'knowledge'> {
|
||||
): Exclude<AgentCapabilityKind, 'knowledge' | 'skill'> {
|
||||
const normalized = String(toolType || '').toUpperCase();
|
||||
if (normalized === 'WORKFLOW') return 'workflow';
|
||||
if (normalized === 'MCP') return 'mcp';
|
||||
@@ -43,7 +57,7 @@ function toolKindFromType(
|
||||
}
|
||||
|
||||
function resolveToolName(
|
||||
kind: Exclude<AgentCapabilityKind, 'knowledge'>,
|
||||
kind: Exclude<AgentCapabilityKind, 'knowledge' | 'skill'>,
|
||||
resource?: Record<string, any>,
|
||||
) {
|
||||
if (isSafeToolName(resource?.englishName)) {
|
||||
@@ -96,6 +110,7 @@ export function createEmptyAgent(): AgentInfo {
|
||||
categoryId: '',
|
||||
visibilityScope: 'PRIVATE',
|
||||
executionConfigJson: {
|
||||
builtinTools: createDefaultBuiltinTools(),
|
||||
documentContextBudgetTokens: 20_000,
|
||||
},
|
||||
modelId: '',
|
||||
@@ -134,6 +149,9 @@ function normalizeAgent(agent?: AgentInfo): AgentInfo {
|
||||
},
|
||||
executionConfigJson: {
|
||||
...source.executionConfigJson,
|
||||
builtinTools: normalizeBuiltinTools(
|
||||
source.executionConfigJson?.builtinTools,
|
||||
),
|
||||
documentContextBudgetTokens:
|
||||
Number(source.executionConfigJson?.documentContextBudgetTokens) > 0
|
||||
? Number(source.executionConfigJson?.documentContextBudgetTokens)
|
||||
@@ -202,15 +220,50 @@ function normalizeToolBinding(
|
||||
};
|
||||
}
|
||||
|
||||
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:')) {
|
||||
@@ -232,6 +285,12 @@ export function useAgentDesignerState() {
|
||||
binding,
|
||||
};
|
||||
}
|
||||
if (state.selectedNodeId === 'skills') {
|
||||
return {
|
||||
kind: 'skill' as AgentCapabilityKind,
|
||||
binding: undefined,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
@@ -247,9 +306,13 @@ export function useAgentDesignerState() {
|
||||
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() {
|
||||
@@ -281,7 +344,7 @@ export function useAgentDesignerState() {
|
||||
}
|
||||
|
||||
function addToolNode(
|
||||
kind: Exclude<AgentCapabilityKind, 'knowledge'>,
|
||||
kind: Exclude<AgentCapabilityKind, 'knowledge' | 'skill'>,
|
||||
resource?: Record<string, any>,
|
||||
) {
|
||||
let toolType = 'PLUGIN';
|
||||
@@ -306,9 +369,84 @@ export function useAgentDesignerState() {
|
||||
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?.binding?.localId) return;
|
||||
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,
|
||||
@@ -371,7 +509,9 @@ export function useAgentDesignerState() {
|
||||
return issues;
|
||||
}
|
||||
|
||||
function buildPayloadAgent(): AgentInfo {
|
||||
function buildPayloadAgent(options?: {
|
||||
canDisableShellApproval?: boolean;
|
||||
}): AgentInfo {
|
||||
const memoryConfigJson = state.agent.memoryConfigJson || {};
|
||||
const compressionParameter = tokenOnlyCompressionParameter(
|
||||
memoryConfigJson.compressionParameter,
|
||||
@@ -380,8 +520,14 @@ export function useAgentDesignerState() {
|
||||
maxAttachedMessageCount: _maxAttachedMessageCount,
|
||||
...restMemoryConfigJson
|
||||
} = memoryConfigJson;
|
||||
const {
|
||||
knowledgeBindings: _knowledgeBindings,
|
||||
skillBindings: _skillBindings,
|
||||
toolBindings: _toolBindings,
|
||||
...agent
|
||||
} = state.agent;
|
||||
return {
|
||||
...state.agent,
|
||||
...agent,
|
||||
interactionConfigJson: buildInteractionConfigPayload(
|
||||
state.agent.interactionConfigJson,
|
||||
),
|
||||
@@ -389,17 +535,10 @@ export function useAgentDesignerState() {
|
||||
...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,
|
||||
),
|
||||
),
|
||||
},
|
||||
executionConfigJson: buildAgentExecutionConfig(
|
||||
state.agent.executionConfigJson,
|
||||
Boolean(options?.canDisableShellApproval),
|
||||
),
|
||||
memoryConfigJson: {
|
||||
...restMemoryConfigJson,
|
||||
compressionParameter: {
|
||||
@@ -413,9 +552,11 @@ export function useAgentDesignerState() {
|
||||
|
||||
function buildKnowledgePayload(agentId?: number | string) {
|
||||
return state.knowledgeBindings.map((binding, index) => ({
|
||||
...binding,
|
||||
agentId,
|
||||
knowledgeId: binding.knowledgeId,
|
||||
enabled: binding.enabled !== false,
|
||||
optionsJson: binding.optionsJson || {},
|
||||
retrievalMode: binding.retrievalMode || 'HYBRID',
|
||||
sortNo: index + 1,
|
||||
}));
|
||||
}
|
||||
@@ -424,16 +565,66 @@ export function useAgentDesignerState() {
|
||||
return state.toolBindings.map((binding, index) => {
|
||||
const isMcp = String(binding.toolType || '').toUpperCase() === 'MCP';
|
||||
return {
|
||||
...binding,
|
||||
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 {
|
||||
@@ -441,13 +632,20 @@ export function useAgentDesignerState() {
|
||||
state,
|
||||
selectedCapability,
|
||||
addKnowledgeNode,
|
||||
appendSkillOptions,
|
||||
addToolNode,
|
||||
buildKnowledgePayload,
|
||||
buildPayloadAgent,
|
||||
buildSkillPayload,
|
||||
buildToolPayload,
|
||||
commitBindingBaseline,
|
||||
getBindingChanges,
|
||||
markDirty,
|
||||
moveSkill,
|
||||
openTryout,
|
||||
removeSkill,
|
||||
removeSelectedCapability,
|
||||
replaceSkillBindings,
|
||||
reset,
|
||||
selectBase,
|
||||
selectNode,
|
||||
|
||||
Reference in New Issue
Block a user