feat: 完善 Agent 标准交互与安全运行时
- 接入 AG-UI 运行投影、Turn 时间线和审批隔离 - 增加 Agent Skill 冻结绑定与运行时消费闭环 - 增加受控工作区、内置工具和私有 Artifact 生命周期
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
import type { ServerSentEventMessage } from 'fetch-event-stream';
|
||||
|
||||
import type {
|
||||
ChatDocumentAttachment,
|
||||
ChatImageAttachment,
|
||||
@@ -7,9 +5,11 @@ import type {
|
||||
ChatTimelineMessageItem,
|
||||
} from '@easyflow/common-ui';
|
||||
|
||||
import type { AguiEvent } from '../../shared/agent-agui/client';
|
||||
import type {
|
||||
AgentInfo,
|
||||
AgentKnowledgeBinding,
|
||||
AgentSkillBinding,
|
||||
AgentToolBinding,
|
||||
} from '../types';
|
||||
|
||||
@@ -17,8 +17,9 @@ import { ref } from 'vue';
|
||||
|
||||
import { ChatTimelineBuilder } from '@easyflow/common-ui';
|
||||
|
||||
import { sseClient } from '#/api/request';
|
||||
import { EventType } from '@ag-ui/client';
|
||||
|
||||
import { EasyFlowAguiClient } from '../../shared/agent-agui/client';
|
||||
import { clearAgentDraftSession } from '../api';
|
||||
import { useAgentTryoutRawRounds } from './useAgentTryoutRawRounds';
|
||||
|
||||
@@ -26,24 +27,6 @@ function resolveDraftSessionId(agent: AgentInfo) {
|
||||
return `agent-draft-${agent.id || agent.localId || 'unsaved'}`;
|
||||
}
|
||||
|
||||
function parseEventData(message: ServerSentEventMessage) {
|
||||
const raw = message.data || '';
|
||||
if (!raw) return {};
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return { payload: { delta: raw } };
|
||||
}
|
||||
}
|
||||
|
||||
function resolveEnvelope(data: any) {
|
||||
return {
|
||||
domain: data.domain || data.eventDomain || data.typeDomain,
|
||||
type: data.type || data.eventType || data.chatType || data.event,
|
||||
payload: data.payload ?? data.data ?? data,
|
||||
};
|
||||
}
|
||||
|
||||
function asText(value: unknown) {
|
||||
return value === null || value === undefined ? '' : String(value);
|
||||
}
|
||||
@@ -54,23 +37,78 @@ function asRecord(value: unknown): Record<string, unknown> {
|
||||
: {};
|
||||
}
|
||||
|
||||
function isEndOfRoundEvent(domain: string, type: string) {
|
||||
return domain === 'SYSTEM' && type === 'DONE';
|
||||
function draftAgentTransport(agent: AgentInfo) {
|
||||
return {
|
||||
avatar: agent.avatar,
|
||||
categoryId: agent.categoryId,
|
||||
description: agent.description,
|
||||
executionConfigJson: agent.executionConfigJson,
|
||||
generationConfigJson: agent.generationConfigJson,
|
||||
id: agent.id,
|
||||
memoryConfigJson: agent.memoryConfigJson,
|
||||
modelConfigJson: agent.modelConfigJson,
|
||||
modelId: agent.modelId,
|
||||
name: agent.name,
|
||||
promptConfigJson: agent.promptConfigJson,
|
||||
publishStatus: agent.publishStatus,
|
||||
status: agent.status,
|
||||
visibilityScope: agent.visibilityScope,
|
||||
};
|
||||
}
|
||||
|
||||
function draftToolBindingTransport(binding: AgentToolBinding) {
|
||||
return {
|
||||
enabled: binding.enabled,
|
||||
hitlConfigJson: binding.hitlConfigJson,
|
||||
hitlEnabled: binding.hitlEnabled,
|
||||
id: binding.id,
|
||||
optionsJson: binding.optionsJson,
|
||||
sortNo: binding.sortNo,
|
||||
targetId: binding.targetId,
|
||||
toolName: binding.toolName,
|
||||
toolType: binding.toolType,
|
||||
};
|
||||
}
|
||||
|
||||
function draftKnowledgeBindingTransport(binding: AgentKnowledgeBinding) {
|
||||
return {
|
||||
enabled: binding.enabled,
|
||||
id: binding.id,
|
||||
knowledgeId: binding.knowledgeId,
|
||||
optionsJson: binding.optionsJson,
|
||||
retrievalMode: binding.retrievalMode,
|
||||
sortNo: binding.sortNo,
|
||||
};
|
||||
}
|
||||
|
||||
function draftSkillBindingTransport(binding: AgentSkillBinding) {
|
||||
return {
|
||||
skillId: binding.skillId,
|
||||
sortNo: binding.sortNo,
|
||||
};
|
||||
}
|
||||
|
||||
interface DraftRuntimeContext {
|
||||
agent: AgentInfo;
|
||||
knowledgeBindings: AgentKnowledgeBinding[];
|
||||
skillBindings: AgentSkillBinding[];
|
||||
toolBindings: AgentToolBinding[];
|
||||
}
|
||||
|
||||
interface ActiveDraftRun {
|
||||
roundId: string;
|
||||
sessionId: string;
|
||||
stopped: boolean;
|
||||
}
|
||||
|
||||
export function useAgentTryoutStream() {
|
||||
const timelineItems = ref<ChatTimelineItemType[]>([]);
|
||||
const loading = ref(false);
|
||||
let rawRounds: ReturnType<typeof useAgentTryoutRawRounds> | undefined;
|
||||
let activeRoundId = '';
|
||||
let activeSessionId = '';
|
||||
let userStopped = false;
|
||||
let activeRun: ActiveDraftRun | undefined;
|
||||
const aguiClient = new EasyFlowAguiClient();
|
||||
|
||||
function errorMessageOf(error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
@@ -87,18 +125,11 @@ export function useAgentTryoutStream() {
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function isAbortError(error: unknown) {
|
||||
const message = errorMessageOf(error).toLowerCase();
|
||||
return message.includes('abort');
|
||||
}
|
||||
|
||||
function shouldIgnoreStoppedError(error: unknown) {
|
||||
return userStopped && isAbortError(error);
|
||||
}
|
||||
|
||||
function finishStoppedRun() {
|
||||
finishAssistant();
|
||||
rawRounds?.flush();
|
||||
function finishStoppedRun(roundId: string) {
|
||||
if (roundId) {
|
||||
rawRounds?.failRound(roundId);
|
||||
rebuildTimeline();
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -141,79 +172,18 @@ export function useAgentTryoutStream() {
|
||||
}
|
||||
|
||||
function markToolApproving(payload: {
|
||||
requestId?: string;
|
||||
resumeToken?: string;
|
||||
approvalId?: string;
|
||||
toolCallId?: string;
|
||||
}) {
|
||||
rawRounds?.recordEvent(activeRoundId, {
|
||||
domain: 'TOOL',
|
||||
payload,
|
||||
type: 'FORM_APPROVING',
|
||||
});
|
||||
rawRounds?.flush();
|
||||
rebuildTimeline();
|
||||
ChatTimelineBuilder.markToolApproving(timelineItems.value, payload);
|
||||
}
|
||||
|
||||
function markToolRejected(payload: {
|
||||
approvalId?: string;
|
||||
reason?: string;
|
||||
requestId?: string;
|
||||
resumeToken?: string;
|
||||
toolCallId?: string;
|
||||
}) {
|
||||
rawRounds?.recordEvent(activeRoundId, {
|
||||
domain: 'TOOL',
|
||||
payload,
|
||||
type: 'FORM_REJECTED',
|
||||
});
|
||||
rawRounds?.flush();
|
||||
rebuildTimeline();
|
||||
}
|
||||
|
||||
function handleMessage(message: ServerSentEventMessage) {
|
||||
const data = parseEventData(message);
|
||||
const envelope = resolveEnvelope(data);
|
||||
const domain = String(envelope.domain || '').toUpperCase();
|
||||
const type = String(envelope.type || '').toUpperCase();
|
||||
const payload = envelope.payload || {};
|
||||
|
||||
if (activeRoundId) {
|
||||
const runtimeEvent = rawRounds?.recordEvent(activeRoundId, {
|
||||
domain,
|
||||
payload,
|
||||
type,
|
||||
});
|
||||
if (runtimeEvent) {
|
||||
rawRounds?.projectEvent(
|
||||
timelineItems.value,
|
||||
activeRoundId,
|
||||
runtimeEvent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === 'LLM' && type === 'MESSAGE') {
|
||||
return;
|
||||
}
|
||||
if (domain === 'LLM' && type === 'THINKING') {
|
||||
const text = asText(payload.reasoning ?? payload.delta ?? payload.text);
|
||||
if (!text) return;
|
||||
return;
|
||||
}
|
||||
if (domain === 'TOOL' && type === 'FORM_REQUEST') {
|
||||
return;
|
||||
}
|
||||
if (domain === 'TOOL' && (type === 'TOOL_CALL' || type === 'TOOL_RESULT')) {
|
||||
return;
|
||||
}
|
||||
if (domain === 'BUSINESS' && type === 'CITATIONS') {
|
||||
return;
|
||||
}
|
||||
if (domain === 'BUSINESS' && type === 'STATUS') {
|
||||
return;
|
||||
}
|
||||
if (isEndOfRoundEvent(domain, type)) {
|
||||
markRoundCompleted(activeRoundId);
|
||||
}
|
||||
ChatTimelineBuilder.markToolRejected(timelineItems.value, payload);
|
||||
}
|
||||
|
||||
async function runDraft(payload: {
|
||||
@@ -226,6 +196,7 @@ export function useAgentTryoutStream() {
|
||||
onAccepted?: () => Promise<void> | void;
|
||||
prompt: string;
|
||||
sessionId?: string;
|
||||
skillBindings: AgentSkillBinding[];
|
||||
toolBindings: AgentToolBinding[];
|
||||
}) {
|
||||
syncDraftContext(payload, false, payload.sessionId);
|
||||
@@ -237,59 +208,83 @@ export function useAgentTryoutStream() {
|
||||
payload.images,
|
||||
payload.documents,
|
||||
);
|
||||
const run: ActiveDraftRun = {
|
||||
roundId: activeRoundId,
|
||||
sessionId: activeSessionId,
|
||||
stopped: false,
|
||||
};
|
||||
activeRun = run;
|
||||
rebuildTimeline();
|
||||
loading.value = true;
|
||||
userStopped = false;
|
||||
let accepted = false;
|
||||
await sseClient.post(
|
||||
'/api/v1/agent/chat/draft',
|
||||
{
|
||||
agent: payload.agent,
|
||||
documentUploadIds: payload.documentUploadIds,
|
||||
imageUploadIds: payload.imageUploadIds,
|
||||
knowledgeBindings: payload.knowledgeBindings,
|
||||
prompt: payload.prompt,
|
||||
sessionId: activeSessionId,
|
||||
toolBindings: payload.toolBindings,
|
||||
},
|
||||
{
|
||||
onMessage: (message) => {
|
||||
const envelope = resolveEnvelope(parseEventData(message));
|
||||
const domain = String(envelope.domain || '').toUpperCase();
|
||||
const type = String(envelope.type || '').toUpperCase();
|
||||
if (!accepted && domain === 'SYSTEM' && type === 'INPUT_ACCEPTED') {
|
||||
accepted = true;
|
||||
void payload.onAccepted?.();
|
||||
}
|
||||
handleMessage(message);
|
||||
},
|
||||
onError: (error) => {
|
||||
if (shouldIgnoreStoppedError(error)) {
|
||||
return;
|
||||
}
|
||||
rawRounds?.recordEvent(activeRoundId, {
|
||||
domain: 'SYSTEM',
|
||||
payload: {
|
||||
message: error?.message ?? '试运行失败,请稍后再试',
|
||||
try {
|
||||
await aguiClient.run({
|
||||
forwardedProps: {
|
||||
easyflow: {
|
||||
draft: {
|
||||
agent: draftAgentTransport(payload.agent),
|
||||
knowledgeBindings: payload.knowledgeBindings.map((binding) =>
|
||||
draftKnowledgeBindingTransport(binding),
|
||||
),
|
||||
skillBindings: payload.skillBindings.map((binding) =>
|
||||
draftSkillBindingTransport(binding),
|
||||
),
|
||||
toolBindings: payload.toolBindings.map((binding) =>
|
||||
draftToolBindingTransport(binding),
|
||||
),
|
||||
},
|
||||
type: 'ERROR',
|
||||
});
|
||||
rebuildTimeline();
|
||||
finishAssistant();
|
||||
rawRounds?.flush();
|
||||
loading.value = false;
|
||||
input: {
|
||||
documentUploadIds: payload.documentUploadIds,
|
||||
imageUploadIds: payload.imageUploadIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
onFinished: () => {
|
||||
if (userStopped) {
|
||||
return;
|
||||
}
|
||||
finishAssistant();
|
||||
markRoundCompleted(activeRoundId);
|
||||
rawRounds?.flush();
|
||||
loading.value = false;
|
||||
onEvent(event) {
|
||||
if (activeRun !== run) return;
|
||||
const runtimeEvent = rawRounds?.recordEvent(run.roundId, event);
|
||||
if (!runtimeEvent) return;
|
||||
rawRounds?.projectEvent(
|
||||
timelineItems.value,
|
||||
run.roundId,
|
||||
runtimeEvent,
|
||||
() => {
|
||||
if (accepted) return;
|
||||
accepted = true;
|
||||
void payload.onAccepted?.();
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
threadId: run.sessionId,
|
||||
url: '/api/v1/agent/agui/run/draft',
|
||||
userMessage: {
|
||||
content: payload.prompt,
|
||||
id: `user-${run.roundId}`,
|
||||
role: 'user',
|
||||
},
|
||||
});
|
||||
if (activeRun === run && !run.stopped) {
|
||||
finishAssistant();
|
||||
markRoundCompleted(run.roundId);
|
||||
}
|
||||
} catch (error) {
|
||||
if (activeRun === run && !run.stopped) {
|
||||
const runError = {
|
||||
message: errorMessageOf(error) || '试运行失败,请稍后再试',
|
||||
runId: run.roundId,
|
||||
threadId: run.sessionId,
|
||||
type: EventType.RUN_ERROR,
|
||||
} as AguiEvent;
|
||||
rawRounds?.recordEvent(run.roundId, runError);
|
||||
rawRounds?.projectEvent(timelineItems.value, run.roundId, runError);
|
||||
finishAssistant();
|
||||
rawRounds?.flush();
|
||||
}
|
||||
} finally {
|
||||
if (activeRun === run) {
|
||||
activeRun = undefined;
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendDraft(payload: {
|
||||
@@ -302,6 +297,7 @@ export function useAgentTryoutStream() {
|
||||
onAccepted?: () => Promise<void> | void;
|
||||
prompt: string;
|
||||
sessionId?: string;
|
||||
skillBindings: AgentSkillBinding[];
|
||||
toolBindings: AgentToolBinding[];
|
||||
}) {
|
||||
await runDraft(payload);
|
||||
@@ -329,8 +325,9 @@ export function useAgentTryoutStream() {
|
||||
|
||||
async function clearDraftSession() {
|
||||
if (loading.value) {
|
||||
userStopped = true;
|
||||
sseClient.abort();
|
||||
if (activeRun) activeRun.stopped = true;
|
||||
activeRun = undefined;
|
||||
aguiClient.abort();
|
||||
loading.value = false;
|
||||
}
|
||||
const sessionId = activeSessionId;
|
||||
@@ -346,16 +343,20 @@ export function useAgentTryoutStream() {
|
||||
if (!loading.value) {
|
||||
return;
|
||||
}
|
||||
userStopped = true;
|
||||
sseClient.abort();
|
||||
finishStoppedRun();
|
||||
const stoppedRoundId = activeRun?.roundId || activeRoundId;
|
||||
if (activeRun) activeRun.stopped = true;
|
||||
activeRun = undefined;
|
||||
aguiClient.abort();
|
||||
finishStoppedRun(stoppedRoundId);
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
if (loading.value) {
|
||||
userStopped = true;
|
||||
sseClient.abort();
|
||||
finishStoppedRun();
|
||||
const stoppedRoundId = activeRun?.roundId || activeRoundId;
|
||||
if (activeRun) activeRun.stopped = true;
|
||||
activeRun = undefined;
|
||||
aguiClient.abort();
|
||||
finishStoppedRun(stoppedRoundId);
|
||||
return;
|
||||
}
|
||||
rawRounds?.flush();
|
||||
|
||||
Reference in New Issue
Block a user