import { AbstractChatProvider, XRequest, type TransformMessage, type XRequestOptions, } from '@ant-design/x-sdk'; import { usePermissionStore } from '../../store/permission/permissionStore'; import { useUserStore } from '../../store/user/userStore'; import type { AiAttachment, AiChatInput, AiChatMessage, AiChartSchema, AiFormSchema, AiModelRetryInfo, AiReviewSchema, AiSseChunk, AiToolRun, } from './types'; 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; form?: AiFormSchema; review?: AiReviewSchema; chart?: AiChartSchema; retry?: AiModelRetryInfo; message?: | string | { id?: number; content?: string; reasoningContent?: string | null; status?: string; toolRuns?: AiToolRun[]; attachments?: AiAttachment[]; replyToMessageId?: number | null; feedback?: 'like' | 'dislike' | null; feedbackReason?: string | null; metadata?: Record | null; }; error?: string; } function emptyAssistant(): AiChatMessage { return { role: 'assistant', content: '', reasoningContent: '', toolRuns: [], attachments: [], forms: [], }; } function mergeForms( current: AiFormSchema[] | undefined, incoming: AiFormSchema | AiFormSchema[] | undefined, ): AiFormSchema[] { const items = Array.isArray(incoming) ? incoming : incoming ? [incoming] : []; if (!items.length) return current ?? []; const next = [...(current ?? [])]; for (const item of items) { if (item && typeof item === 'object' && !next.some((existing) => existing.id === item.id)) { next.push(item); } } return next; } function mergeReviews( current: AiReviewSchema[] | undefined, incoming: AiReviewSchema | AiReviewSchema[] | undefined, ): AiReviewSchema[] { const items = Array.isArray(incoming) ? incoming : incoming ? [incoming] : []; if (!items.length) return current ?? []; const next = [...(current ?? [])]; for (const item of items) { if (!item || typeof item !== 'object') continue; const index = next.findIndex((existing) => existing.id === item.id); if (index === -1) { next.push(item); } else { next[index] = item; } } return next; } function mergeCharts( current: AiChartSchema[] | undefined, incoming: AiChartSchema | AiChartSchema[] | undefined, ): AiChartSchema[] { const items = Array.isArray(incoming) ? incoming : incoming ? [incoming] : []; if (!items.length) return current ?? []; const next = [...(current ?? [])]; for (const item of items) { if (!item || typeof item !== 'object') continue; const index = next.findIndex((existing) => existing.id === item.id); if (index === -1) { next.push(item); } else { next[index] = item; } } return next; } 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, })); } 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; message.forms = mergeForms( message.forms, (nested?.metadata?.a2uiForm as AiFormSchema | undefined) ?? payload.form, ); message.reviews = mergeReviews( message.reviews, (nested?.metadata?.a2uiReview as AiReviewSchema | undefined) ?? payload.review, ); message.charts = mergeCharts( message.charts, (nested?.metadata?.a2uiChart as AiChartSchema | AiChartSchema[] | undefined) ?? payload.chart, ); message.replyToMessageId = nested?.replyToMessageId ?? message.replyToMessageId; message.feedback = nested?.feedback ?? message.feedback; message.feedbackReason = nested?.feedbackReason ?? message.feedbackReason; message.metadata = nested?.metadata ?? message.metadata; } 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.form' && payload.form) { message.forms = mergeForms(message.forms, payload.form); } else if (event === 'ui.review' && payload.review) { message.reviews = mergeReviews(message.reviews, payload.review); } else if (event === 'ui.chart' && payload.chart) { message.charts = mergeCharts(message.charts, payload.chart); } 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; message.forms = mergeForms( message.forms, (nested?.metadata?.a2uiForm as AiFormSchema | undefined) ?? payload.form, ); message.reviews = mergeReviews( message.reviews, (nested?.metadata?.a2uiReview as AiReviewSchema | undefined) ?? payload.review, ); message.charts = mergeCharts( message.charts, (nested?.metadata?.a2uiChart as AiChartSchema | AiChartSchema[] | undefined) ?? payload.chart, ); message.replyToMessageId = nested?.replyToMessageId ?? message.replyToMessageId; message.feedback = nested?.feedback ?? message.feedback; message.feedbackReason = nested?.feedbackReason ?? message.feedbackReason; message.metadata = nested?.metadata ?? message.metadata; 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; } export async function authenticatedFetch( input: RequestInfo | URL, init?: RequestInit, ): Promise { const headers = new Headers(init?.headers); const token = useUserStore.getState().token; if (token) headers.set('Authorization', `Bearer ${token}`); headers.set('Accept', 'text/event-stream'); let requestInput = input; let requestInit = init; if (typeof init?.body === 'string') { try { const body = JSON.parse(init.body) as AiChatInput; if (body.regenerateMessageId) { requestInput = `${String(input).replace(/\/stream$/, '')}/messages/${body.regenerateMessageId}/regenerate/stream`; requestInit = { ...init, body: JSON.stringify({ clientRequestId: body.clientRequestId, reasoningEffort: body.reasoningEffort, }), }; } else if (body.formSubmission) { requestInput = `${String(input).replace(/\/conversations\/\d+\/stream$/, '')}/forms/${body.formSubmission.formId}/submit/stream`; requestInit = { ...init, body: JSON.stringify({ clientRequestId: body.clientRequestId, values: body.formSubmission.values, reasoningEffort: body.reasoningEffort, }), }; } else if (body.reviewSubmission) { requestInput = `${String(input).replace(/\/conversations\/\d+\/stream$/, '')}/reviews/${body.reviewSubmission.reviewId}/submit/stream`; requestInit = { ...init, body: JSON.stringify({ clientRequestId: body.clientRequestId, reasoningEffort: body.reasoningEffort, }), }; } else { const { localAttachments: _localAttachments, reloadMessage: _reloadMessage, regenerateMessageId: _regenerateMessageId, formSubmission: _formSubmission, reviewSubmission: _reviewSubmission, ...payload } = body; requestInit = { ...init, body: JSON.stringify(payload) }; } } catch { requestInit = init; } } const response = await fetch(requestInput, { ...requestInit, headers }); if (response.status === 401) { useUserStore.getState().logout(); usePermissionStore.getState().clearPermissions(); window.location.href = '/login'; } return response; } export class GongxueAiChatProvider extends AbstractChatProvider< AiChatMessage, AiChatInput, AiSseChunk > { /** Routes events that target another (already streamed) message. */ onExternalReview?: (messageId: number, review: AiReviewSchema) => void; constructor( url: string, onSettled?: (result?: { ok: boolean; aborted?: boolean }) => void, ) { super({ request: XRequest(url, { manual: true, fetch: authenticatedFetch, timeout: 15_000, streamTimeout: 1_800_000, callbacks: { onUpdate: () => undefined, onSuccess: () => onSettled?.({ ok: true }), onError: (error) => onSettled?.({ ok: false, aborted: error?.name === 'AbortError', }), }, }), }); } transformParams( requestParams: Partial, options: XRequestOptions, ): AiChatInput { return { ...options.params, message: requestParams.message?.trim() || '', attachmentIds: requestParams.attachmentIds ?? [], skillKey: requestParams.skillKey ?? null, clientRequestId: requestParams.clientRequestId || crypto.randomUUID(), reasoningEffort: requestParams.reasoningEffort, localAttachments: requestParams.localAttachments, formSubmission: requestParams.formSubmission, reviewSubmission: requestParams.reviewSubmission, regenerateMessageId: requestParams.regenerateMessageId, reloadMessage: requestParams.reloadMessage, }; } transformLocalMessage(requestParams: Partial): AiChatMessage { if (requestParams.formSubmission) { return { role: 'user', content: '', reasoningContent: '', toolRuns: [], attachments: requestParams.localAttachments ?? [], metadata: { a2uiSubmit: { formTitle: requestParams.formSubmission.formTitle, }, }, }; } if (requestParams.reviewSubmission) { return { role: 'user', content: '', reasoningContent: '', toolRuns: [], attachments: requestParams.localAttachments ?? [], metadata: { a2uiReviewSubmit: { reviewTitle: requestParams.reviewSubmission.reviewTitle, }, }, }; } return { role: 'user', content: requestParams.message?.trim() || '', reasoningContent: '', toolRuns: [], attachments: requestParams.localAttachments ?? [], }; } transformMessage(info: TransformMessage): AiChatMessage { const { event, payload } = parseSsePayload(info.chunk); if ( event === 'ui.review' && payload.review && typeof payload.messageId === 'number' && info.originMessage?.id !== payload.messageId ) { // The submitted review belongs to the original assistant message; // do not merge it into the message currently being streamed. this.onExternalReview?.(payload.messageId, payload.review); return info.originMessage ?? emptyAssistant(); } return reduceAiSseMessage(info.originMessage, info.chunk); } }