Files
gongxue-base/apps/admin/src/components/AiChat/sseReducer.ts

198 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type {
AiArtifactSchema,
AiAttachment,
AiChartSchema,
AiChatMessage,
AiFormSchema,
AiModelRetryInfo,
AiReviewSchema,
AiSseChunk,
AiToolRun,
} from './types';
import { mergeArtifactIntoMessage, mergeById } from './uiArtifacts';
export interface AiSsePayload {
messageId?: number;
userMessageId?: number;
assistantMessageId?: number;
delta?: string;
content?: string;
reasoningContent?: string | null;
toolCallId?: string;
toolName?: string;
skillKey?: string | null;
status?: string;
summary?: string | null;
durationMs?: number | null;
attachment?: AiAttachment;
artifact?: AiArtifactSchema;
wizard?: unknown;
retry?: AiModelRetryInfo;
message?:
| string
| {
id?: number;
content?: string;
reasoningContent?: string | null;
status?: string;
toolRuns?: AiToolRun[];
attachments?: AiAttachment[];
replyToMessageId?: number | null;
metadata?: Record<string, unknown> | null;
};
error?: string;
}
export function emptyAssistant(): AiChatMessage {
return {
role: 'assistant',
content: '',
reasoningContent: '',
toolRuns: [],
attachments: [],
forms: [],
uiArtifacts: [],
};
}
export function parseSsePayload(chunk?: AiSseChunk): {
event: string;
payload: AiSsePayload;
} {
if (!chunk) return { event: '', payload: {} };
const event = chunk.event?.trim() || 'message';
if (!chunk.data || chunk.data === '[DONE]') return { event, payload: {} };
try {
const parsed: unknown = JSON.parse(chunk.data);
return {
event,
payload: parsed && typeof parsed === 'object' ? (parsed as AiSsePayload) : {},
};
} catch {
return { event, payload: { delta: chunk.data } };
}
}
function upsertToolRun(
toolRuns: AiToolRun[],
payload: AiSsePayload,
fallbackStatus: AiToolRun['status'],
): AiToolRun[] {
const toolCallId = payload.toolCallId || `${payload.toolName || 'tool'}-${toolRuns.length}`;
const next: AiToolRun = {
toolCallId,
toolName: payload.toolName || '查询工具',
skillKey: payload.skillKey,
status: (payload.status as AiToolRun['status']) || fallbackStatus,
summary: payload.summary,
resultSummary: fallbackStatus === 'running' ? undefined : payload.summary,
argumentsSummary: fallbackStatus === 'running' ? payload.summary : undefined,
durationMs: payload.durationMs,
};
const index = toolRuns.findIndex((item) => item.toolCallId === toolCallId);
if (index === -1) return [...toolRuns, next];
return toolRuns.map((item, itemIndex) => (itemIndex === index ? { ...item, ...next } : item));
}
function normalizeToolRuns(toolRuns: AiToolRun[] | undefined, fallback: AiToolRun[]): AiToolRun[] {
if (!toolRuns) return fallback;
return toolRuns.map((tool) => ({
...tool,
status: tool.status === 'error' ? 'failed' : tool.status,
summary: tool.resultSummary ?? tool.argumentsSummary ?? tool.summary,
}));
}
function applyMessagePayload(
message: AiChatMessage,
nested: AiSsePayload['message'],
): void {
if (typeof nested !== 'object' || nested === null) return;
// 历史消息兼容:老数据只有 metadata.a2uiForm/a2uiReview/a2uiChart
// 恢复为 legacy 字段供渲染层在 uiArtifacts 为空时回退使用。
message.forms = mergeById<AiFormSchema>(
message.forms,
nested.metadata?.a2uiForm as AiFormSchema | undefined,
);
message.reviews = mergeById<AiReviewSchema>(
message.reviews,
nested.metadata?.a2uiReview as AiReviewSchema | undefined,
);
message.charts = mergeById<AiChartSchema>(
message.charts,
nested.metadata?.a2uiChart as AiChartSchema | AiChartSchema[] | undefined,
);
const artifacts = nested.metadata?.uiArtifacts;
if (Array.isArray(artifacts)) {
for (const artifact of artifacts) {
if (artifact && typeof artifact === 'object' && typeof artifact.id === 'string') {
mergeArtifactIntoMessage(message, artifact as AiArtifactSchema);
}
}
}
message.replyToMessageId = nested.replyToMessageId ?? message.replyToMessageId;
message.metadata = nested.metadata ?? message.metadata;
}
export function reduceAiSseMessage(
originMessage: AiChatMessage | undefined,
chunk?: AiSseChunk,
): AiChatMessage {
const message = originMessage ? { ...originMessage } : emptyAssistant();
const { event, payload } = parseSsePayload(chunk);
if (event === 'message.created') {
const nested = typeof payload.message === 'object' ? payload.message : undefined;
message.id = nested?.id ?? payload.assistantMessageId ?? payload.messageId ?? message.id;
message.content = nested?.content ?? message.content;
message.reasoningContent = nested?.reasoningContent ?? message.reasoningContent;
message.toolRuns = normalizeToolRuns(nested?.toolRuns, message.toolRuns);
message.attachments = nested?.attachments ?? message.attachments;
applyMessagePayload(message, nested);
} else if (event === 'reasoning.delta') {
message.retrying = null;
message.reasoningContent += payload.delta ?? payload.reasoningContent ?? '';
} else if (event === 'content.delta') {
message.retrying = null;
message.content += payload.delta ?? payload.content ?? '';
} else if (event === 'model.retrying' && payload.retry) {
message.retrying = payload.retry;
} else if (event === 'ui.artifact' && payload.artifact) {
// 统一 artifact 事件legacy 列表由渲染层从 uiArtifacts 派生。
mergeArtifactIntoMessage(message, payload.artifact);
} else if (event === 'ui.import_wizard' && payload.wizard) {
message.metadata = { ...message.metadata, a2uiImportWizard: payload.wizard };
} else if (event === 'tool.started') {
message.toolRuns = upsertToolRun(message.toolRuns, payload, 'running');
} else if (event === 'tool.completed') {
message.toolRuns = upsertToolRun(message.toolRuns, payload, 'success');
} else if (event === 'tool.failed') {
message.toolRuns = upsertToolRun(message.toolRuns, payload, 'failed');
} else if (event === 'attachment.processed' && payload.attachment) {
if (!message.attachments.some((item) => item.id === payload.attachment?.id)) {
message.attachments = [...message.attachments, payload.attachment];
}
} else if (event === 'message.completed') {
const nested = typeof payload.message === 'object' ? payload.message : undefined;
message.id = nested?.id ?? payload.messageId ?? message.id;
message.content = nested?.content ?? payload.content ?? message.content;
message.reasoningContent =
nested?.reasoningContent ?? payload.reasoningContent ?? message.reasoningContent;
message.toolRuns = normalizeToolRuns(nested?.toolRuns, message.toolRuns);
message.attachments = nested?.attachments ?? message.attachments;
applyMessagePayload(message, nested);
message.retrying = null;
} else if (event === 'message.cancelled') {
message.id = payload.messageId ?? message.id;
message.cancelled = true;
message.retrying = null;
} else if (event === 'error') {
message.retrying = null;
message.error =
(typeof payload.message === 'string' ? payload.message : undefined) ||
payload.error ||
'AI 回答生成失败';
}
return message;
}