feat: 完善 Agent 标准交互与安全运行时

- 接入 AG-UI 运行投影、Turn 时间线和审批隔离

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

- 增加受控工作区、内置工具和私有 Artifact 生命周期
This commit is contained in:
2026-08-19 22:13:41 +08:00
parent 91d66e636d
commit 4e8640dcaf
241 changed files with 24382 additions and 2777 deletions

View File

@@ -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,