Refactor AI chat: streaming, tool calls, UI polish
All checks were successful
CI / check (pull_request) Successful in 3m25s

This commit is contained in:
2026-07-24 16:27:50 +08:00
parent e605586fc9
commit 8656394b9b
55 changed files with 2774 additions and 460 deletions

View File

@@ -4,7 +4,13 @@ import {
type TransformMessage,
type XRequestOptions,
} from '@ant-design/x-sdk';
import type { AiChatInput, AiChatMessage, AiSseChunk, AiToolRun } from './types';
import type {
AiAttachment,
AiChatInput,
AiChatMessage,
AiSseChunk,
AiToolRun,
} from './types';
interface AiSsePayload {
messageId?: number;
@@ -15,9 +21,11 @@ interface AiSsePayload {
reasoningContent?: string | null;
toolCallId?: string;
toolName?: string;
skillKey?: string | null;
status?: string;
summary?: string | null;
durationMs?: number | null;
attachment?: AiAttachment;
message?:
| string
| {
@@ -26,6 +34,11 @@ interface AiSsePayload {
reasoningContent?: string | null;
status?: string;
toolRuns?: AiToolRun[];
attachments?: AiAttachment[];
replyToMessageId?: number | null;
feedback?: 'like' | 'dislike' | null;
feedbackReason?: string | null;
metadata?: Record<string, unknown> | null;
};
error?: string;
}
@@ -36,6 +49,7 @@ function emptyAssistant(): AiChatMessage {
content: '',
reasoningContent: '',
toolRuns: [],
attachments: [],
};
}
@@ -66,6 +80,7 @@ function upsertToolRun(
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,
@@ -99,6 +114,11 @@ export function reduceAiSseMessage(
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.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.reasoningContent += payload.delta ?? payload.reasoningContent ?? '';
} else if (event === 'content.delta') {
@@ -109,6 +129,10 @@ export function reduceAiSseMessage(
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;
@@ -116,6 +140,11 @@ export function reduceAiSseMessage(
message.reasoningContent =
nested?.reasoningContent ?? payload.reasoningContent ?? message.reasoningContent;
message.toolRuns = normalizeToolRuns(nested?.toolRuns, message.toolRuns);
message.attachments = nested?.attachments ?? message.attachments;
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 === 'message.cancelled') {
message.id = payload.messageId ?? message.id;
message.cancelled = true;
@@ -133,7 +162,31 @@ async function authenticatedFetch(input: RequestInfo | URL, init?: RequestInit):
const token = localStorage.getItem('token');
if (token) headers.set('Authorization', `Bearer ${token}`);
headers.set('Accept', 'text/event-stream');
const response = await fetch(input, { ...init, headers });
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 }),
};
} else {
const {
localAttachments: _localAttachments,
reloadMessage: _reloadMessage,
regenerateMessageId: _regenerateMessageId,
...payload
} = body;
requestInit = { ...init, body: JSON.stringify(payload) };
}
} catch {
requestInit = init;
}
}
const response = await fetch(requestInput, { ...requestInit, headers });
if (response.status === 401) {
localStorage.removeItem('token');
localStorage.removeItem('user');
@@ -171,6 +224,12 @@ export class GongxueAiChatProvider extends AbstractChatProvider<
return {
...options.params,
message: requestParams.message?.trim() || '',
attachmentIds: requestParams.attachmentIds ?? [],
skillKey: requestParams.skillKey ?? null,
clientRequestId: requestParams.clientRequestId || crypto.randomUUID(),
localAttachments: requestParams.localAttachments,
regenerateMessageId: requestParams.regenerateMessageId,
reloadMessage: requestParams.reloadMessage,
};
}
@@ -180,6 +239,7 @@ export class GongxueAiChatProvider extends AbstractChatProvider<
content: requestParams.message?.trim() || '',
reasoningContent: '',
toolRuns: [],
attachments: requestParams.localAttachments ?? [],
};
}