feat: AI 对话支持 A2UI 表单/审查/图表与 Agent 工具

This commit is contained in:
2026-08-05 17:11:00 +08:00
parent 644c35ce53
commit 0e6e3e2d96
64 changed files with 8395 additions and 6434 deletions

View File

@@ -35,6 +35,7 @@ interface AiSsePayload {
form?: AiFormSchema;
review?: AiReviewSchema;
chart?: AiChartSchema;
wizard?: unknown;
retry?: AiModelRetryInfo;
message?:
| string
@@ -46,8 +47,6 @@ interface AiSsePayload {
toolRuns?: AiToolRun[];
attachments?: AiAttachment[];
replyToMessageId?: number | null;
feedback?: 'like' | 'dislike' | null;
feedbackReason?: string | null;
metadata?: Record<string, unknown> | null;
};
error?: string;
@@ -79,29 +78,10 @@ function mergeForms(
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[] {
function mergeById<T extends { id: string }>(
current: T[] | undefined,
incoming: T | T[] | undefined,
): T[] {
const items = Array.isArray(incoming) ? incoming : incoming ? [incoming] : [];
if (!items.length) return current ?? [];
const next = [...(current ?? [])];
@@ -165,6 +145,28 @@ function normalizeToolRuns(toolRuns: AiToolRun[] | undefined, fallback: AiToolRu
}));
}
function applyMessagePayload(
message: AiChatMessage,
nested: AiSsePayload['message'],
payload: AiSsePayload,
): void {
if (typeof nested !== 'object' || nested === null) return;
message.forms = mergeForms(
message.forms,
(nested.metadata?.a2uiForm as AiFormSchema | undefined) ?? payload.form,
);
message.reviews = mergeById<AiReviewSchema>(
message.reviews,
(nested.metadata?.a2uiReview as AiReviewSchema | undefined) ?? payload.review,
);
message.charts = mergeById<AiChartSchema>(
message.charts,
(nested.metadata?.a2uiChart as AiChartSchema | AiChartSchema[] | undefined) ?? payload.chart,
);
message.replyToMessageId = nested.replyToMessageId ?? message.replyToMessageId;
message.metadata = nested.metadata ?? message.metadata;
}
export function reduceAiSseMessage(
originMessage: AiChatMessage | undefined,
chunk?: AiSseChunk,
@@ -179,22 +181,7 @@ export function reduceAiSseMessage(
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;
applyMessagePayload(message, nested, payload);
} else if (event === 'reasoning.delta') {
message.retrying = null;
message.reasoningContent += payload.delta ?? payload.reasoningContent ?? '';
@@ -206,9 +193,11 @@ export function reduceAiSseMessage(
} 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);
message.reviews = mergeById<AiReviewSchema>(message.reviews, payload.review);
} else if (event === 'ui.chart' && payload.chart) {
message.charts = mergeCharts(message.charts, payload.chart);
message.charts = mergeById<AiChartSchema>(message.charts, payload.chart);
} 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') {
@@ -227,22 +216,7 @@ export function reduceAiSseMessage(
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;
applyMessagePayload(message, nested, payload);
message.retrying = null;
} else if (event === 'message.cancelled') {
message.id = payload.messageId ?? message.id;
@@ -280,6 +254,16 @@ export async function authenticatedFetch(
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 = {
@@ -304,6 +288,7 @@ export async function authenticatedFetch(
localAttachments: _localAttachments,
reloadMessage: _reloadMessage,
regenerateMessageId: _regenerateMessageId,
editMessageId: _editMessageId,
formSubmission: _formSubmission,
reviewSubmission: _reviewSubmission,
...payload
@@ -331,10 +316,7 @@ export class GongxueAiChatProvider extends AbstractChatProvider<
/** Routes events that target another (already streamed) message. */
onExternalReview?: (messageId: number, review: AiReviewSchema) => void;
constructor(
url: string,
onSettled?: (result?: { ok: boolean; aborted?: boolean }) => void,
) {
constructor(url: string, onSettled?: (result?: { ok: boolean; aborted?: boolean }) => void) {
super({
request: XRequest<AiChatInput, AiSseChunk, AiChatMessage>(url, {
manual: true,
@@ -369,11 +351,16 @@ export class GongxueAiChatProvider extends AbstractChatProvider<
formSubmission: requestParams.formSubmission,
reviewSubmission: requestParams.reviewSubmission,
regenerateMessageId: requestParams.regenerateMessageId,
editMessageId: requestParams.editMessageId,
reloadMessage: requestParams.reloadMessage,
};
}
transformLocalMessage(requestParams: Partial<AiChatInput>): AiChatMessage {
transformLocalMessage(requestParams: Partial<AiChatInput>): AiChatMessage | AiChatMessage[] {
if (requestParams.editMessageId) {
// 编辑消息不需要新增用户气泡store 里已原位更新原消息。
return [];
}
if (requestParams.formSubmission) {
return {
role: 'user',