refactor(admin): A2UI 双轨收敛,统一 artifact 协议
- mergeArtifactIntoMessage 不再派发 legacy 列表,只维护 uiArtifacts - 渲染层从 uiArtifacts 派生 forms/reviews/charts,为空时回退 legacy (历史消息兼容,老 metadata 仅 a2uiForm/a2uiReview/a2uiChart) - sseReducer 删除 ui.form/ui.review/ui.chart 旧事件分支,只消费 ui.artifact(后端过渡期双发,旧事件将被忽略) - types.ts legacy 字段标注 deprecated;契约文档同步现状 - 测试更新:旧事件忽略 + uiArtifacts 合并/恢复断言 aislop scan: 5 引擎 0 issues
This commit is contained in:
@@ -17,6 +17,7 @@ import { message } from '../../ui/app-message';
|
|||||||
import { DynamicChart } from './DynamicChart';
|
import { DynamicChart } from './DynamicChart';
|
||||||
import { DynamicForm } from './DynamicForm';
|
import { DynamicForm } from './DynamicForm';
|
||||||
import { DynamicReview } from './DynamicReview';
|
import { DynamicReview } from './DynamicReview';
|
||||||
|
import { deriveCharts, deriveForms, deriveReviews } from './uiArtifacts';
|
||||||
import { ArtifactErrorBoundary } from './ArtifactErrorBoundary';
|
import { ArtifactErrorBoundary } from './ArtifactErrorBoundary';
|
||||||
import { LiteCodeHighlighter } from './LiteCodeHighlighter';
|
import { LiteCodeHighlighter } from './LiteCodeHighlighter';
|
||||||
import { LiteMermaid } from './LiteMermaid';
|
import { LiteMermaid } from './LiteMermaid';
|
||||||
@@ -226,6 +227,11 @@ export const AiMessageContent: React.FC<AiMessageContentProps> = ({
|
|||||||
const streaming = status === 'loading' || status === 'updating';
|
const streaming = status === 'loading' || status === 'updating';
|
||||||
const formSubmission = message.metadata?.a2uiSubmit;
|
const formSubmission = message.metadata?.a2uiSubmit;
|
||||||
const reviewSubmission = message.metadata?.a2uiReviewSubmit;
|
const reviewSubmission = message.metadata?.a2uiReviewSubmit;
|
||||||
|
// 统一 artifact 优先,历史消息(仅 legacy 字段)回退
|
||||||
|
const forms = deriveForms(message).length > 0 ? deriveForms(message) : (message.forms ?? []);
|
||||||
|
const reviews =
|
||||||
|
deriveReviews(message).length > 0 ? deriveReviews(message) : (message.reviews ?? []);
|
||||||
|
const charts = deriveCharts(message).length > 0 ? deriveCharts(message) : (message.charts ?? []);
|
||||||
const sourceMeta = message.metadata?.a2uiSources;
|
const sourceMeta = message.metadata?.a2uiSources;
|
||||||
const sourceItems = Array.isArray(sourceMeta)
|
const sourceItems = Array.isArray(sourceMeta)
|
||||||
? sourceMeta
|
? sourceMeta
|
||||||
@@ -374,7 +380,7 @@ export const AiMessageContent: React.FC<AiMessageContentProps> = ({
|
|||||||
onClick={(item) => void handleOpenSource(item as { url?: string })}
|
onClick={(item) => void handleOpenSource(item as { url?: string })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(message.forms ?? []).map((form) => (
|
{(forms ?? []).map((form) => (
|
||||||
<ArtifactErrorBoundary key={form.id} title="表单">
|
<ArtifactErrorBoundary key={form.id} title="表单">
|
||||||
<DynamicForm
|
<DynamicForm
|
||||||
form={form}
|
form={form}
|
||||||
@@ -383,7 +389,7 @@ export const AiMessageContent: React.FC<AiMessageContentProps> = ({
|
|||||||
/>
|
/>
|
||||||
</ArtifactErrorBoundary>
|
</ArtifactErrorBoundary>
|
||||||
))}
|
))}
|
||||||
{(message.reviews ?? []).map((review: AiReviewSchema) => (
|
{(reviews ?? []).map((review: AiReviewSchema) => (
|
||||||
<ArtifactErrorBoundary key={review.id} title="导入预览">
|
<ArtifactErrorBoundary key={review.id} title="导入预览">
|
||||||
<DynamicReview
|
<DynamicReview
|
||||||
review={review}
|
review={review}
|
||||||
@@ -395,7 +401,7 @@ export const AiMessageContent: React.FC<AiMessageContentProps> = ({
|
|||||||
/>
|
/>
|
||||||
</ArtifactErrorBoundary>
|
</ArtifactErrorBoundary>
|
||||||
))}
|
))}
|
||||||
{(message.charts ?? []).map((chart: AiChartSchema) => (
|
{(charts ?? []).map((chart: AiChartSchema) => (
|
||||||
<ArtifactErrorBoundary key={chart.id} title="图表">
|
<ArtifactErrorBoundary key={chart.id} title="图表">
|
||||||
<DynamicChart chart={chart} />
|
<DynamicChart chart={chart} />
|
||||||
</ArtifactErrorBoundary>
|
</ArtifactErrorBoundary>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ describe('AI chat history mapper', () => {
|
|||||||
expect(mapped.message.forms?.[0]).toMatchObject({ id: 'form-9', title: '新增学生' });
|
expect(mapped.message.forms?.[0]).toMatchObject({ id: 'form-9', title: '新增学生' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('restores uiArtifacts from message metadata and derives legacy lists', () => {
|
it('restores uiArtifacts from message metadata (统一协议)', () => {
|
||||||
const mapped = mapHistoryMessage({
|
const mapped = mapHistoryMessage({
|
||||||
id: 8,
|
id: 8,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@@ -120,8 +120,14 @@ describe('AI chat history mapper', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mapped.message.uiArtifacts).toHaveLength(2);
|
expect(mapped.message.uiArtifacts).toHaveLength(2);
|
||||||
expect(mapped.message.forms?.[0]).toMatchObject({ id: 'form-10', status: 'submitted' });
|
expect(mapped.message.uiArtifacts?.[0].payload).toMatchObject({
|
||||||
expect(mapped.message.reviews?.[0]).toMatchObject({ id: 'review-10', status: 'expired' });
|
id: 'form-10',
|
||||||
|
status: 'submitted',
|
||||||
|
});
|
||||||
|
expect(mapped.message.uiArtifacts?.[1].payload).toMatchObject({
|
||||||
|
id: 'review-10',
|
||||||
|
status: 'expired',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('restores a persisted A2UI review from message metadata', () => {
|
it('restores a persisted A2UI review from message metadata', () => {
|
||||||
|
|||||||
@@ -123,38 +123,35 @@ describe('AI chat SSE message reducer', () => {
|
|||||||
expect(message.id).toBe(9);
|
expect(message.id).toBe(9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('merges ui.form events into the assistant message by id', () => {
|
it('ignores legacy ui.form events and only consumes ui.artifact (过渡期双发)', () => {
|
||||||
const form = {
|
|
||||||
id: 'form-1',
|
|
||||||
title: '新增学生',
|
|
||||||
submitLabel: '提交创建',
|
|
||||||
fields: [
|
|
||||||
{ name: 'name', label: '姓名', type: 'input', required: true },
|
|
||||||
{ name: 'gender', label: '性别', type: 'select', options: [{ label: '男', value: 'male' }] },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
let message = reduceAiSseMessage(undefined, {
|
let message = reduceAiSseMessage(undefined, {
|
||||||
event: 'ui.form',
|
event: 'ui.form',
|
||||||
data: JSON.stringify({ messageId: 8, form }),
|
data: JSON.stringify({ messageId: 8, form: { id: 'form-1', title: '新增学生', fields: [] } }),
|
||||||
});
|
});
|
||||||
|
// 旧事件不再写入 legacy 列表
|
||||||
|
expect(message.forms).toHaveLength(0);
|
||||||
|
expect(message.uiArtifacts).toHaveLength(0);
|
||||||
|
|
||||||
message = reduceAiSseMessage(message, {
|
message = reduceAiSseMessage(message, {
|
||||||
event: 'ui.form',
|
event: 'ui.artifact',
|
||||||
data: JSON.stringify({ messageId: 8, form: { ...form, id: 'form-1' } }),
|
|
||||||
});
|
|
||||||
message = reduceAiSseMessage(message, {
|
|
||||||
event: 'ui.form',
|
|
||||||
data: JSON.stringify({
|
data: JSON.stringify({
|
||||||
messageId: 8,
|
messageId: 8,
|
||||||
form: { id: 'form-2', title: '入住确认', fields: [] },
|
artifact: {
|
||||||
|
id: 'form-1',
|
||||||
|
type: 'form',
|
||||||
|
status: 'pending',
|
||||||
|
messageId: 8,
|
||||||
|
conversationId: 3,
|
||||||
|
payload: { id: 'form-1', title: '新增学生', fields: [] },
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(message.forms).toHaveLength(2);
|
expect(message.uiArtifacts).toHaveLength(1);
|
||||||
expect(message.forms?.[0]).toMatchObject({ id: 'form-1', title: '新增学生' });
|
expect(message.uiArtifacts?.[0].payload).toMatchObject({ id: 'form-1', title: '新增学生' });
|
||||||
expect(message.forms?.[1]).toMatchObject({ id: 'form-2' });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('merges ui.artifact events into uiArtifacts and legacy lists by id', () => {
|
it('merges ui.artifact events into uiArtifacts by id', () => {
|
||||||
let message = reduceAiSseMessage(undefined, {
|
let message = reduceAiSseMessage(undefined, {
|
||||||
event: 'ui.artifact',
|
event: 'ui.artifact',
|
||||||
data: JSON.stringify({
|
data: JSON.stringify({
|
||||||
@@ -185,8 +182,8 @@ describe('AI chat SSE message reducer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(message.uiArtifacts).toHaveLength(2);
|
expect(message.uiArtifacts).toHaveLength(2);
|
||||||
expect(message.forms?.[0]).toMatchObject({ id: 'form-1', title: '新增学生' });
|
expect(message.uiArtifacts?.[0].payload).toMatchObject({ id: 'form-1', title: '新增学生' });
|
||||||
expect(message.reviews?.[0]).toMatchObject({ id: 'review-1', status: 'expired' });
|
expect(message.uiArtifacts?.[1].payload).toMatchObject({ id: 'review-1', status: 'expired' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -213,7 +210,7 @@ describe('AI chat SSE message reducer', () => {
|
|||||||
expect(message.forms?.[0].id).toBe('form-9');
|
expect(message.forms?.[0].id).toBe('form-9');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('merges ui.review events into the assistant message and updates by id', () => {
|
it('ignores legacy ui.review events and only consumes ui.artifact', () => {
|
||||||
const review = {
|
const review = {
|
||||||
id: 'review-1',
|
id: 'review-1',
|
||||||
title: '开学导入',
|
title: '开学导入',
|
||||||
@@ -246,8 +243,9 @@ describe('AI chat SSE message reducer', () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(message.reviews).toHaveLength(1);
|
// 旧事件不再写入 legacy 列表
|
||||||
expect(message.reviews?.[0]).toMatchObject({ id: 'review-1', status: 'submitted' });
|
expect(message.reviews).toBeUndefined();
|
||||||
|
expect(message.uiArtifacts).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows model retrying state and clears it when content starts', () => {
|
it('shows model retrying state and clears it when content starts', () => {
|
||||||
@@ -301,7 +299,7 @@ describe('AI chat SSE message reducer', () => {
|
|||||||
expect(message.reviews?.[0]).toMatchObject({ id: 'review-9', title: '批量导入' });
|
expect(message.reviews?.[0]).toMatchObject({ id: 'review-9', title: '批量导入' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('merges ui.chart events into the assistant message by id', () => {
|
it('ignores legacy ui.chart events and only consumes ui.artifact', () => {
|
||||||
const chart = {
|
const chart = {
|
||||||
id: 'chart-1',
|
id: 'chart-1',
|
||||||
title: '各班级人数',
|
title: '各班级人数',
|
||||||
@@ -327,9 +325,9 @@ describe('AI chat SSE message reducer', () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(message.charts).toHaveLength(2);
|
// 旧事件不再写入 legacy 列表
|
||||||
expect(message.charts?.[0]).toMatchObject({ id: 'chart-1', chartType: 'bar' });
|
expect(message.charts).toBeUndefined();
|
||||||
expect(message.charts?.[1]).toMatchObject({ id: 'chart-2' });
|
expect(message.uiArtifacts).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('restores persisted charts from message.completed metadata', () => {
|
it('restores persisted charts from message.completed metadata', () => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type {
|
|||||||
AiSseChunk,
|
AiSseChunk,
|
||||||
AiToolRun,
|
AiToolRun,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { mergeArtifactIntoMessage, mergeById, mergeForms } from './uiArtifacts';
|
import { mergeArtifactIntoMessage, mergeById } from './uiArtifacts';
|
||||||
|
|
||||||
export interface AiSsePayload {
|
export interface AiSsePayload {
|
||||||
messageId?: number;
|
messageId?: number;
|
||||||
@@ -112,7 +112,9 @@ function applyMessagePayload(
|
|||||||
payload: AiSsePayload,
|
payload: AiSsePayload,
|
||||||
): void {
|
): void {
|
||||||
if (typeof nested !== 'object' || nested === null) return;
|
if (typeof nested !== 'object' || nested === null) return;
|
||||||
message.forms = mergeForms(
|
// 历史消息兼容:老数据只有 metadata.a2uiForm/a2uiReview/a2uiChart,
|
||||||
|
// 恢复为 legacy 字段供渲染层在 uiArtifacts 为空时回退使用。
|
||||||
|
message.forms = mergeById<AiFormSchema>(
|
||||||
message.forms,
|
message.forms,
|
||||||
(nested.metadata?.a2uiForm as AiFormSchema | undefined) ?? payload.form,
|
(nested.metadata?.a2uiForm as AiFormSchema | undefined) ?? payload.form,
|
||||||
);
|
);
|
||||||
@@ -159,13 +161,9 @@ export function reduceAiSseMessage(
|
|||||||
message.content += payload.delta ?? payload.content ?? '';
|
message.content += payload.delta ?? payload.content ?? '';
|
||||||
} else if (event === 'model.retrying' && payload.retry) {
|
} else if (event === 'model.retrying' && payload.retry) {
|
||||||
message.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 = mergeById<AiReviewSchema>(message.reviews, payload.review);
|
|
||||||
} else if (event === 'ui.chart' && payload.chart) {
|
|
||||||
message.charts = mergeById<AiChartSchema>(message.charts, payload.chart);
|
|
||||||
} else if (event === 'ui.artifact' && payload.artifact) {
|
} else if (event === 'ui.artifact' && payload.artifact) {
|
||||||
|
// 统一 artifact 事件:后端过渡期仍双发 ui.form/ui.review/ui.chart,
|
||||||
|
// 前端只消费 ui.artifact,legacy 列表由渲染层从 uiArtifacts 派生。
|
||||||
mergeArtifactIntoMessage(message, payload.artifact);
|
mergeArtifactIntoMessage(message, payload.artifact);
|
||||||
} else if (event === 'ui.import_wizard' && payload.wizard) {
|
} else if (event === 'ui.import_wizard' && payload.wizard) {
|
||||||
message.metadata = { ...message.metadata, a2uiImportWizard: payload.wizard };
|
message.metadata = { ...message.metadata, a2uiImportWizard: payload.wizard };
|
||||||
|
|||||||
@@ -171,9 +171,13 @@ export interface AiChatMessage {
|
|||||||
reasoningContent: string;
|
reasoningContent: string;
|
||||||
toolRuns: AiToolRun[];
|
toolRuns: AiToolRun[];
|
||||||
attachments: AiAttachment[];
|
attachments: AiAttachment[];
|
||||||
|
/** @deprecated 仅历史消息兼容读取(metadata.a2uiForm);新数据统一走 uiArtifacts */
|
||||||
forms?: AiFormSchema[];
|
forms?: AiFormSchema[];
|
||||||
|
/** @deprecated 仅历史消息兼容读取(metadata.a2uiReview);新数据统一走 uiArtifacts */
|
||||||
reviews?: AiReviewSchema[];
|
reviews?: AiReviewSchema[];
|
||||||
|
/** @deprecated 仅历史消息兼容读取(metadata.a2uiChart);新数据统一走 uiArtifacts */
|
||||||
charts?: AiChartSchema[];
|
charts?: AiChartSchema[];
|
||||||
|
/** 统一 A2UI 制品协议(唯一事实源) */
|
||||||
uiArtifacts?: AiArtifactSchema[];
|
uiArtifacts?: AiArtifactSchema[];
|
||||||
replyToMessageId?: number | null;
|
replyToMessageId?: number | null;
|
||||||
metadata?: Record<string, unknown> | null;
|
metadata?: Record<string, unknown> | null;
|
||||||
|
|||||||
@@ -25,43 +25,49 @@ export function mergeById<T extends { id: string }>(
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
export 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 payloadOf(artifact: AiArtifactSchema): unknown {
|
function payloadOf(artifact: AiArtifactSchema): unknown {
|
||||||
return artifact.payload && typeof artifact.payload === 'object' ? artifact.payload : {};
|
return artifact.payload && typeof artifact.payload === 'object' ? artifact.payload : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将统一 artifact 归入 uiArtifacts,并按类型派发到 legacy 列表。
|
* 将统一 artifact 归入 uiArtifacts。
|
||||||
* payload 来自服务端契约(表单/审阅/图表/预检/向导),按类型做单次断言。
|
*
|
||||||
|
* 注意:不再派发到 legacy 列表(forms/reviews/charts)——渲染层从
|
||||||
|
* uiArtifacts 派生,legacy 字段仅保留给历史消息(metadata 中只有
|
||||||
|
* a2uiForm/a2uiReview/a2uiChart 的老数据)作兼容读取。
|
||||||
*/
|
*/
|
||||||
export function mergeArtifactIntoMessage(
|
export function mergeArtifactIntoMessage(
|
||||||
message: AiChatMessage,
|
message: AiChatMessage,
|
||||||
artifact: AiArtifactSchema,
|
artifact: AiArtifactSchema,
|
||||||
): AiChatMessage {
|
): AiChatMessage {
|
||||||
message.uiArtifacts = mergeById<AiArtifactSchema>(message.uiArtifacts, artifact);
|
message.uiArtifacts = mergeById<AiArtifactSchema>(message.uiArtifacts, artifact);
|
||||||
const payload = payloadOf(artifact);
|
void payloadOf(artifact);
|
||||||
if (artifact.type === 'form') {
|
|
||||||
message.forms = mergeForms(message.forms, payload as AiFormSchema);
|
|
||||||
} else if (artifact.type === 'review') {
|
|
||||||
message.reviews = mergeById<AiReviewSchema>(message.reviews, payload as AiReviewSchema);
|
|
||||||
} else if (artifact.type === 'chart') {
|
|
||||||
message.charts = mergeById<AiChartSchema>(message.charts, payload as AiChartSchema);
|
|
||||||
} else if (artifact.type === 'import_wizard') {
|
|
||||||
message.metadata = { ...message.metadata, a2uiImportWizard: payload };
|
|
||||||
}
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 uiArtifacts 派生 legacy 列表(渲染用)。
|
||||||
|
* 仅当 message 上没有显式 legacy 数据(历史消息)时,渲染层回退到 message.forms 等。
|
||||||
|
*/
|
||||||
|
export function deriveForms(message: AiChatMessage): AiFormSchema[] {
|
||||||
|
return (message.uiArtifacts ?? [])
|
||||||
|
.filter((artifact) => artifact.type === 'form')
|
||||||
|
.map((artifact) => artifact.payload)
|
||||||
|
.filter((payload): payload is AiFormSchema => Boolean(payload) && typeof payload === 'object');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deriveReviews(message: AiChatMessage): AiReviewSchema[] {
|
||||||
|
return (message.uiArtifacts ?? [])
|
||||||
|
.filter((artifact) => artifact.type === 'review')
|
||||||
|
.map((artifact) => artifact.payload)
|
||||||
|
.filter(
|
||||||
|
(payload): payload is AiReviewSchema => Boolean(payload) && typeof payload === 'object',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deriveCharts(message: AiChatMessage): AiChartSchema[] {
|
||||||
|
return (message.uiArtifacts ?? [])
|
||||||
|
.filter((artifact) => artifact.type === 'chart')
|
||||||
|
.map((artifact) => artifact.payload)
|
||||||
|
.filter((payload): payload is AiChartSchema => Boolean(payload) && typeof payload === 'object');
|
||||||
|
}
|
||||||
|
|||||||
@@ -148,14 +148,15 @@ interface AiUiForm {
|
|||||||
|
|
||||||
- **统一类型**:`types.ts` 以 `AiArtifactSchema`(`message.uiArtifacts`)为唯一事实源,
|
- **统一类型**:`types.ts` 以 `AiArtifactSchema`(`message.uiArtifacts`)为唯一事实源,
|
||||||
`artifact.type` ∈ `form | review | chart | import_wizard`,`payload` 携带各类型 schema。
|
`artifact.type` ∈ `form | review | chart | import_wizard`,`payload` 携带各类型 schema。
|
||||||
- **合并逻辑**:`uiArtifacts.ts` 的 `mergeArtifactIntoMessage` 将新 artifact upsert 进
|
- **合并逻辑**:`uiArtifacts.ts` 的 `mergeArtifactIntoMessage` 只维护 `uiArtifacts`
|
||||||
`uiArtifacts`,并**向后兼容**地派发到 legacy 字段(`forms`/`reviews`/`charts`)供老消息渲染。
|
(不再派发 legacy);渲染层经 `deriveForms/deriveReviews/deriveCharts` 从 artifact 派生。
|
||||||
- **渲染层**:`AiMessageContent.tsx` 消费 legacy 列表渲染 `DynamicForm`/`DynamicReview`/
|
- **SSE 事件**:后端过渡期仍双发 `ui.form/ui.review/ui.chart` 与 `ui.artifact`,
|
||||||
`DynamicChart`;每个制品用 `ArtifactErrorBoundary` 包裹(单个渲染失败不影响整条气泡)。
|
前端**只消费 `ui.artifact`**,旧事件分支已删除(后端后续可移除旧发射)。
|
||||||
|
- **历史兼容**:老消息 metadata 中只有 `a2uiForm/a2uiReview/a2uiChart`(无 uiArtifacts)时,
|
||||||
|
`sseReducer`/`message-mappers` 恢复为 legacy 字段(`message.forms/reviews/charts`,
|
||||||
|
已在 `types.ts` 标注 deprecated),渲染层在 uiArtifacts 为空时回退使用;
|
||||||
|
新数据一律只写 `uiArtifacts`。
|
||||||
- **A2UI 组件实现**:`DynamicForm`/`DynamicReview`/`DynamicChart` 共享
|
- **A2UI 组件实现**:`DynamicForm`/`DynamicReview`/`DynamicChart` 共享
|
||||||
`useSubmissionState`(提交状态:防重复提交 + 失败可重试)与 `useXCardSurface`
|
`useSubmissionState`(提交状态:防重复提交 + 失败可重试)与 `useXCardSurface`
|
||||||
(XCard commands 增量更新 + createSurface 自动去重)。
|
(XCard commands 增量更新 + createSurface 自动去重)。
|
||||||
- **SSE 事件 / 提交与确认接口**:与上文契约一致,未变更。
|
- **提交与确认接口**:与上文契约一致,未变更。
|
||||||
|
|
||||||
后续若彻底移除 legacy 字段,需保证历史消息(持久化 metadata 中的 `forms`/`reviews`/`charts`)
|
|
||||||
仍可渲染——建议先迁移历史数据或保留兼容读取。
|
|
||||||
|
|||||||
Reference in New Issue
Block a user