197 lines
6.7 KiB
TypeScript
197 lines
6.7 KiB
TypeScript
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 { AiArtifactSchema, AiChatInput, AiChatMessage, AiReviewSchema, AiSseChunk } from './types';
|
||
import { emptyAssistant, parseSsePayload, reduceAiSseMessage } from './sseReducer';
|
||
|
||
export { parseSsePayload, reduceAiSseMessage };
|
||
|
||
export async function authenticatedFetch(
|
||
input: RequestInfo | URL,
|
||
init?: RequestInit,
|
||
): Promise<Response> {
|
||
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.editMessageId) {
|
||
requestInput = `${String(input).replace(/\/stream$/, '')}/messages/${body.editMessageId}/edit/stream`;
|
||
requestInit = {
|
||
...init,
|
||
body: JSON.stringify({
|
||
content: body.message,
|
||
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,
|
||
editMessageId: _editMessageId,
|
||
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) {
|
||
// 提示由登录页读取展示:直接弹 toast 会被跳转销毁
|
||
sessionStorage.setItem('login_expired_hint', '1');
|
||
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;
|
||
onExternalArtifact?: (messageId: number, artifact: AiArtifactSchema) => void;
|
||
|
||
constructor(url: string, onSettled?: (result?: { ok: boolean; aborted?: boolean }) => void) {
|
||
super({
|
||
request: XRequest<AiChatInput, AiSseChunk, AiChatMessage>(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<AiChatInput>,
|
||
options: XRequestOptions<AiChatInput, AiSseChunk, AiChatMessage>,
|
||
): 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,
|
||
editMessageId: requestParams.editMessageId,
|
||
reloadMessage: requestParams.reloadMessage,
|
||
};
|
||
}
|
||
|
||
transformLocalMessage(requestParams: Partial<AiChatInput>): AiChatMessage | AiChatMessage[] {
|
||
if (requestParams.editMessageId) {
|
||
// 编辑消息不需要新增用户气泡,store 里已原位更新原消息。
|
||
return [];
|
||
}
|
||
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, AiSseChunk>): AiChatMessage {
|
||
const { event, payload } = parseSsePayload(info.chunk);
|
||
if (
|
||
event === 'ui.artifact' &&
|
||
payload.artifact &&
|
||
typeof payload.messageId === 'number' &&
|
||
info.originMessage?.id !== payload.messageId
|
||
) {
|
||
this.onExternalArtifact?.(payload.messageId, payload.artifact);
|
||
return info.originMessage ?? emptyAssistant();
|
||
}
|
||
return reduceAiSseMessage(info.originMessage, info.chunk);
|
||
}
|
||
}
|