feat: 全新智能体功能

- 基于先进智能体框架,增加智能体编排功能
- 增加智能体聊天,并对接持久化
This commit is contained in:
2026-05-25 11:42:48 +08:00
parent 6c3d98eaac
commit 72df00f25b
168 changed files with 22045 additions and 400 deletions

View File

@@ -0,0 +1,420 @@
import type {ServerSentEventMessage} from 'fetch-event-stream';
import type {
ChatTimelineItem,
ChatTimelineKnowledgeHit,
ChatTimelineMessageItem,
ChatTimelineToolApprovalPayload,
} from '@easyflow/common-ui';
import {ChatTimelineBuilder} from '@easyflow/common-ui';
import type {AgentChatMessageRecord} from '../api';
export interface AgentSseEnvelope {
domain: string;
payload: Record<string, any>;
type: string;
}
function asText(value: unknown) {
return value === null || value === undefined ? '' : String(value);
}
function asRecord(value: unknown): Record<string, any> {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, any>)
: {};
}
function asArray(value: unknown): any[] {
return Array.isArray(value) ? value : [];
}
function asTimestamp(value: unknown) {
if (!value) {
return Date.now();
}
const timestamp = new Date(String(value)).getTime();
return Number.isFinite(timestamp) ? timestamp : Date.now();
}
function normalizeRole(value: unknown): 'assistant' | 'system' | 'user' {
const role = asText(value).toLowerCase();
if (role === 'assistant' || role === 'system' || role === 'user') {
return role;
}
return 'assistant';
}
function normalizeToolName(value: unknown) {
return asText(value).trim();
}
function normalizeToolCallId(payload: Record<string, any>) {
return asText(payload.toolCallId ?? payload.tool_call_id ?? payload.id);
}
function normalizeMetadata(record: AgentChatMessageRecord) {
return {
createdAt: asTimestamp(record.created),
id: `history-${record.id || record.roundId || Date.now()}`,
roundId: asText(record.roundId),
roundNo: record.roundNo,
selectedVariantIndex: record.selectedVariantIndex,
switchable: false,
variantCount: record.variantCount,
variantIndex: record.variantIndex,
} satisfies Partial<ChatTimelineMessageItem>;
}
function assistantMetadata(
record: AgentChatMessageRecord,
suffix?: string,
): Partial<ChatTimelineMessageItem> {
const metadata = normalizeMetadata(record);
return suffix ? { ...metadata, id: `${metadata.id}-${suffix}` } : metadata;
}
function normalizeKnowledgeItems(payload: Record<string, any>) {
const rawItems =
asArray(payload.items).length > 0
? asArray(payload.items)
: asArray(payload.knowledgeReferences).length > 0
? asArray(payload.knowledgeReferences)
: asArray(payload.knowledgeCitations);
return rawItems
.map((item, index): ChatTimelineKnowledgeHit => {
const source = asRecord(item);
const metadata = asRecord(source.metadata);
const documentName = asText(
source.documentName ?? source.title ?? metadata.documentName,
);
const sourceFileName = asText(
source.sourceFileName ?? metadata.sourceFileName,
);
const chunkContent = asText(
source.chunkContent ?? source.content ?? source.text ?? source.summary,
);
return {
...source,
id: asText(source.id ?? source.chunkId ?? index),
chunkContent,
content: asText(source.content ?? source.text ?? source.summary),
documentId: asText(source.documentId ?? metadata.documentId),
documentName,
knowledgeId: asText(source.knowledgeId ?? payload.knowledgeId),
knowledgeName: asText(source.knowledgeName ?? payload.knowledgeName),
metadata,
score: source.score ?? source.similarity,
sourceFileName,
sourceUri: asText(source.sourceUri ?? metadata.sourceUri),
title: documentName || sourceFileName || asText(source.source),
};
})
.filter((item) => item.chunkContent || item.title || item.documentName);
}
function buildApprovalPayload(payload: Record<string, any>) {
return {
expiresAt: asText(payload.expiresAt),
input: payload.input,
metadata: payload.metadata,
requestId: asText(payload.requestId),
resumeToken: asText(payload.resumeToken),
toolCallId: normalizeToolCallId(payload),
toolDisplayName: asText(payload.toolDisplayName),
toolName: normalizeToolName(payload.toolName ?? payload.name) || '工具调用',
toolType: asText(payload.toolType),
} satisfies ChatTimelineToolApprovalPayload;
}
function appendAssistantText(
items: ChatTimelineItem[],
record: AgentChatMessageRecord,
content: unknown,
suffix?: string,
metadata?: Partial<ChatTimelineMessageItem>,
) {
const text = asText(content);
if (!text) {
return;
}
ChatTimelineBuilder.appendMessageDelta(
items,
text,
{
...assistantMetadata(record, suffix),
...metadata,
},
);
}
function appendAssistantThinking(
items: ChatTimelineItem[],
record: AgentChatMessageRecord,
content: unknown,
suffix?: string,
metadata?: Partial<ChatTimelineMessageItem>,
) {
const text = asText(content);
if (!text) {
return;
}
ChatTimelineBuilder.appendThinkingDelta(
items,
text,
{
...assistantMetadata(record, suffix),
...metadata,
},
);
}
function projectHistoryChain(
items: ChatTimelineItem[],
record: AgentChatMessageRecord,
) {
const payload = asRecord(record.contentPayload);
let hasAssistantText = false;
let hasAssistantThinking = false;
const displayChains = asArray(payload.displayChains ?? payload.chains);
for (const chain of displayChains) {
const item = asRecord(chain);
const reasoning = item.reasoningContent ?? item.reasoning_content;
if (reasoning) {
appendAssistantThinking(items, record, reasoning, 'thinking');
hasAssistantThinking = true;
continue;
}
const toolName = normalizeToolName(item.name ?? item.toolName);
if (toolName) {
ChatTimelineBuilder.upsertToolCall(items, {
input: item.arguments ?? item.input,
output: item.result ?? item.output,
status: asText(item.status) === 'TOOL_RESULT' ? 'success' : 'running',
toolCallId: asText(item.id ?? item.toolCallId),
toolName,
});
}
}
const messageChain = asArray(payload.messageChain);
for (const chain of messageChain) {
const item = asRecord(chain);
const role = asText(item.role).toLowerCase();
if (role === 'assistant') {
appendAssistantThinking(items, record, item.reasoningContent, 'thinking');
if (item.reasoningContent) {
hasAssistantThinking = true;
}
if (!payload.agentResult && item.content) {
appendAssistantText(items, record, item.content, 'text');
hasAssistantText = true;
}
for (const toolCall of asArray(item.toolCalls)) {
const tool = asRecord(toolCall);
ChatTimelineBuilder.upsertToolCall(items, {
input: tool.arguments ?? tool.input,
status: 'running',
toolCallId: asText(tool.id ?? tool.toolCallId),
toolName: normalizeToolName(tool.name ?? tool.toolName),
});
}
continue;
}
if (role === 'tool') {
ChatTimelineBuilder.upsertToolCall(items, {
output: item.content ?? item.result,
status: 'success',
toolCallId: asText(item.toolCallId ?? item.id),
toolName: normalizeToolName(item.name ?? item.toolName) || '工具调用',
});
}
}
return {
hasAssistantText,
hasAssistantThinking,
};
}
function appendHistoryRecord(
items: ChatTimelineItem[],
record: AgentChatMessageRecord,
) {
const role = normalizeRole(record.senderRole);
const metadata = normalizeMetadata(record);
if (role === 'user') {
ChatTimelineBuilder.appendUserMessage(items, record.contentText, metadata);
return;
}
if (role === 'system') {
ChatTimelineBuilder.appendError(items, record.contentText || '系统消息');
return;
}
const payload = asRecord(record.contentPayload);
const agentResult = asRecord(payload.agentResult);
const chainProjection = projectHistoryChain(items, record);
if (!chainProjection.hasAssistantThinking) {
appendAssistantThinking(
items,
record,
payload.reasoningContent ?? agentResult.reasoning,
'thinking',
);
}
if (!chainProjection.hasAssistantText) {
appendAssistantText(
items,
record,
agentResult.text ?? payload.content ?? record.contentText,
chainProjection.hasAssistantThinking ? 'text' : undefined,
);
}
const knowledgeItems = normalizeKnowledgeItems({
...payload,
items:
payload.knowledgeCitations ??
agentResult.knowledgeReferences ??
payload.knowledgeReferences,
});
if (knowledgeItems.length > 0) {
ChatTimelineBuilder.appendKnowledge(items, knowledgeItems);
}
ChatTimelineBuilder.finalize(items);
}
export function recordsToTimelineItems(records: AgentChatMessageRecord[] = []) {
const items: ChatTimelineItem[] = [];
for (const record of records) {
appendHistoryRecord(items, record);
}
ChatTimelineBuilder.finalize(items);
return items;
}
export function parseAgentSseMessage(message: ServerSentEventMessage) {
const raw = message.data || '';
if (!raw) {
return undefined;
}
try {
const data = JSON.parse(raw);
return {
domain: asText(
data.domain ?? data.eventDomain ?? data.typeDomain,
).toUpperCase(),
payload: asRecord(data.payload ?? data.data ?? data),
type: asText(
data.type ?? data.eventType ?? data.chatType ?? data.event,
).toUpperCase(),
} satisfies AgentSseEnvelope;
} catch {
return {
domain: 'LLM',
payload: { delta: raw },
type: 'MESSAGE',
} satisfies AgentSseEnvelope;
}
}
export function applyAgentSseEnvelope(
items: ChatTimelineItem[],
envelope: AgentSseEnvelope,
metadata?: Partial<ChatTimelineMessageItem>,
) {
const { domain, payload, type } = envelope;
if (domain === 'LLM' && type === 'MESSAGE') {
ChatTimelineBuilder.appendMessageDelta(
items,
payload.delta ?? payload.text,
metadata,
);
return;
}
if (domain === 'LLM' && type === 'THINKING') {
ChatTimelineBuilder.appendThinkingDelta(
items,
payload.reasoning ?? payload.delta ?? payload.text,
metadata,
);
return;
}
if (domain === 'TOOL' && type === 'FORM_REQUEST') {
ChatTimelineBuilder.appendToolApproval(
items,
buildApprovalPayload(payload),
);
return;
}
if (domain === 'TOOL' && type === 'FORM_APPROVING') {
ChatTimelineBuilder.markToolApproving(items, {
requestId: asText(payload.requestId),
resumeToken: asText(payload.resumeToken),
toolCallId: normalizeToolCallId(payload),
});
return;
}
if (domain === 'TOOL' && type === 'FORM_REJECTED') {
ChatTimelineBuilder.markToolRejected(items, {
reason: asText(payload.reason),
requestId: asText(payload.requestId),
resumeToken: asText(payload.resumeToken),
toolCallId: normalizeToolCallId(payload),
});
return;
}
if (domain === 'TOOL' && (type === 'TOOL_CALL' || type === 'TOOL_RESULT')) {
ChatTimelineBuilder.upsertToolCall(items, {
input: payload.input ?? payload.toolInput,
output: payload.output ?? payload.result ?? payload.text,
status: type === 'TOOL_RESULT' ? 'success' : 'running',
statusKey: asText(payload.statusKey) || undefined,
toolCallId: normalizeToolCallId(payload),
toolName: normalizeToolName(
payload.toolDisplayName ?? payload.toolName ?? payload.name,
),
});
return;
}
if (domain === 'BUSINESS' && type === 'CITATIONS') {
ChatTimelineBuilder.appendKnowledge(
items,
normalizeKnowledgeItems(payload),
);
return;
}
if (domain === 'BUSINESS' && type === 'STATUS') {
if (asText(payload.statusKey) === 'memory-compression') {
ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
compressed:
typeof payload.compressed === 'boolean'
? payload.compressed
: undefined,
label: asText(payload.label),
phase: asText(payload.phase),
status: asText(payload.status),
statusKey: asText(payload.statusKey),
});
return;
}
if (asText(payload.statusKey) === 'knowledge-retrieval') {
ChatTimelineBuilder.upsertKnowledgeRetrievalStatus(
items,
asText(payload.status) === 'running' ? 'running' : 'done',
asText(payload.statusKey),
);
}
return;
}
if (domain === 'SYSTEM' && type === 'DONE') {
ChatTimelineBuilder.finalize(items);
return;
}
if (domain === 'ERROR' || type === 'ERROR') {
ChatTimelineBuilder.appendError(
items,
payload.message ?? payload.error ?? '请求失败',
);
}
}