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:
2026-08-08 09:33:53 +08:00
parent a644a8de42
commit bcd2d1a559
7 changed files with 98 additions and 79 deletions

View File

@@ -123,38 +123,35 @@ describe('AI chat SSE message reducer', () => {
expect(message.id).toBe(9);
});
it('merges ui.form events into the assistant message by id', () => {
const form = {
id: 'form-1',
title: '新增学生',
submitLabel: '提交创建',
fields: [
{ name: 'name', label: '姓名', type: 'input', required: true },
{ name: 'gender', label: '性别', type: 'select', options: [{ label: '男', value: 'male' }] },
],
};
it('ignores legacy ui.form events and only consumes ui.artifact (过渡期双发)', () => {
let message = reduceAiSseMessage(undefined, {
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, {
event: 'ui.form',
data: JSON.stringify({ messageId: 8, form: { ...form, id: 'form-1' } }),
});
message = reduceAiSseMessage(message, {
event: 'ui.form',
event: 'ui.artifact',
data: JSON.stringify({
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.forms?.[0]).toMatchObject({ id: 'form-1', title: '新增学生' });
expect(message.forms?.[1]).toMatchObject({ id: 'form-2' });
expect(message.uiArtifacts).toHaveLength(1);
expect(message.uiArtifacts?.[0].payload).toMatchObject({ id: 'form-1', title: '新增学生' });
});
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, {
event: 'ui.artifact',
data: JSON.stringify({
@@ -185,8 +182,8 @@ describe('AI chat SSE message reducer', () => {
});
expect(message.uiArtifacts).toHaveLength(2);
expect(message.forms?.[0]).toMatchObject({ id: 'form-1', title: '新增学生' });
expect(message.reviews?.[0]).toMatchObject({ id: 'review-1', status: 'expired' });
expect(message.uiArtifacts?.[0].payload).toMatchObject({ id: 'form-1', title: '新增学生' });
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');
});
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 = {
id: 'review-1',
title: '开学导入',
@@ -246,8 +243,9 @@ describe('AI chat SSE message reducer', () => {
}),
});
expect(message.reviews).toHaveLength(1);
expect(message.reviews?.[0]).toMatchObject({ id: 'review-1', status: 'submitted' });
// 旧事件不再写入 legacy 列表
expect(message.reviews).toBeUndefined();
expect(message.uiArtifacts).toHaveLength(0);
});
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: '批量导入' });
});
it('merges ui.chart events into the assistant message by id', () => {
it('ignores legacy ui.chart events and only consumes ui.artifact', () => {
const chart = {
id: 'chart-1',
title: '各班级人数',
@@ -327,9 +325,9 @@ describe('AI chat SSE message reducer', () => {
}),
});
expect(message.charts).toHaveLength(2);
expect(message.charts?.[0]).toMatchObject({ id: 'chart-1', chartType: 'bar' });
expect(message.charts?.[1]).toMatchObject({ id: 'chart-2' });
// 旧事件不再写入 legacy 列表
expect(message.charts).toBeUndefined();
expect(message.uiArtifacts).toHaveLength(0);
});
it('restores persisted charts from message.completed metadata', () => {