feat: 归档 XL10 异步工具业务编译层

- 将 AgentDefinitionCompiler 升级为 AgentRuntimeCompiler

- 接入 Workflow 和 Plugin 的同步/异步工具编译与 Redis 任务态

- 增加异步执行配置开关、聊天时间线聚合和后端测试
This commit is contained in:
2026-06-04 15:23:56 +08:00
parent 1ea863cb2c
commit c316eff5be
26 changed files with 2859 additions and 62 deletions

View File

@@ -5,6 +5,7 @@ import type {
ChatTimelineKnowledgeHit,
ChatTimelineMessageItem,
ChatTimelineToolApprovalPayload,
ChatTimelineToolStatus,
} from '@easyflow/common-ui';
import {ChatTimelineBuilder} from '@easyflow/common-ui';
@@ -30,6 +31,17 @@ function asArray(value: unknown): any[] {
return Array.isArray(value) ? value : [];
}
function asyncToolTimelineStatus(
payload: Record<string, any>,
): ChatTimelineToolStatus {
const status = asText(payload.status).toUpperCase();
if (status === 'SUCCEEDED') return 'success';
if (status === 'FAILED' || status === 'TIMEOUT' || status === 'CANCELLED') {
return 'error';
}
return 'running';
}
function asTimestamp(value: unknown) {
if (!value) {
return Date.now();
@@ -427,19 +439,29 @@ export function applyAgentSseEnvelope(
return;
}
if (domain === 'TOOL' && (type === 'TOOL_CALL' || type === 'TOOL_RESULT')) {
const asyncTool = payload.asyncTool === true;
const toolName = normalizeToolName(
payload.toolDisplayName ?? payload.toolName ?? payload.name,
);
ChatTimelineBuilder.upsertToolCall(items, {
input: payload.input ?? payload.toolInput,
output: payload.output ?? payload.result ?? payload.text,
status: type === 'TOOL_RESULT' ? 'success' : 'running',
output: asyncTool
? payload.summary ?? payload.label ?? payload.output ?? payload.result ?? payload.text
: payload.output ?? payload.result ?? payload.text,
status: asyncTool
? asyncToolTimelineStatus(payload)
: type === 'TOOL_RESULT'
? 'success'
: 'running',
statusKey: statusKeyForProjection(
payload,
metadata,
'knowledge-retrieval',
),
toolCallId: normalizeToolCallId(payload),
toolName: normalizeToolName(
payload.toolDisplayName ?? payload.toolName ?? payload.name,
),
toolCallId: asyncTool
? asText(payload.toolCallId ?? payload.taskId ?? payload.id)
: normalizeToolCallId(payload),
toolName,
});
return;
}

View File

@@ -79,6 +79,19 @@ const selectedMcpTools = computed(() => {
const selectedMcpToolCount = computed(() => selectedMcpTools.value.length);
const asyncExecutionEnabled = computed({
get() {
return String(props.binding.optionsJson?.executionMode || '').toUpperCase() === 'ASYNC';
},
set(value: boolean) {
props.binding.optionsJson = {
...(props.binding.optionsJson || {}),
executionMode: value ? 'ASYNC' : 'SYNC',
};
emit('change');
},
});
function handleTargetChange(value: string) {
const option = props.options.find(
(item) => String(item.value) === String(value),
@@ -147,6 +160,9 @@ function handleTargetChange(value: string) {
<ElFormItem label="执行前确认">
<ElSwitch v-model="binding.hitlEnabled" @change="emit('change')" />
</ElFormItem>
<ElFormItem v-if="kind !== 'mcp'" label="异步执行">
<ElSwitch v-model="asyncExecutionEnabled" />
</ElFormItem>
<ElButton
class="agent-form__danger"
type="danger"

View File

@@ -145,6 +145,12 @@ 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,
@@ -154,9 +160,9 @@ function normalizeToolBinding(
String(
binding.id ||
createLocalId(String(binding.toolType || 'tool').toLowerCase()),
),
),
toolName: normalizeBindingToolName(binding),
optionsJson: binding.optionsJson || {},
optionsJson,
sortNo: binding.sortNo ?? index + 1,
};
}

View File

@@ -2,6 +2,7 @@ import type {
ChatTimelineItem,
ChatTimelineKnowledgeHit,
ChatTimelineMessageItem,
ChatTimelineToolStatus,
} from '@easyflow/common-ui';
import {ChatTimelineBuilder} from '@easyflow/common-ui';
@@ -503,18 +504,25 @@ function projectEventToTimeline(
const displayToolName = asText(
payload.toolDisplayName ?? rawToolName ?? '工具',
);
const asyncTool = payload.asyncTool === true;
ChatTimelineBuilder.upsertToolCall(items, {
input: payload.input ?? payload.toolInput,
output: payload.output ?? payload.result ?? payload.text,
status: type === 'TOOL_RESULT' ? 'success' : 'running',
output: asyncTool
? payload.summary ?? payload.label ?? payload.output ?? payload.result ?? payload.text
: payload.output ?? payload.result ?? payload.text,
status: asyncTool
? asyncToolTimelineStatus(payload)
: type === 'TOOL_RESULT'
? 'success'
: 'running',
statusKey: statusKeyForProjection(
payload,
roundId,
variantIndex,
'knowledge-retrieval',
),
toolCallId: asText(payload.toolCallId ?? payload.tool_call_id ?? payload.id),
toolName: isHiddenToolName(rawToolName) ? rawToolName : displayToolName,
toolCallId: asText(payload.toolCallId ?? payload.taskId ?? payload.tool_call_id ?? payload.id),
toolName: asyncTool ? displayToolName : isHiddenToolName(rawToolName) ? rawToolName : displayToolName,
});
return;
}
@@ -560,6 +568,15 @@ function projectEventToTimeline(
}
}
function asyncToolTimelineStatus(payload: Record<string, unknown>): ChatTimelineToolStatus {
const status = asText(payload.status).toUpperCase();
if (status === 'SUCCEEDED') return 'success';
if (status === 'FAILED' || status === 'TIMEOUT' || status === 'CANCELLED') {
return 'error';
}
return 'running';
}
function sortedRounds(rounds: Map<string, AgentTryoutRawRound>) {
return [...rounds.values()].sort(
(first, second) =>