refactor(admin): AI 对话收敛为统一 ui.artifact 协议,移除 legacy 事件处理
This commit is contained in:
@@ -123,34 +123,6 @@ describe('AI chat SSE message reducer', () => {
|
||||
expect(message.id).toBe(9);
|
||||
});
|
||||
|
||||
it('ignores legacy ui.form events and only consumes ui.artifact (过渡期双发)', () => {
|
||||
let message = reduceAiSseMessage(undefined, {
|
||||
event: 'ui.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.artifact',
|
||||
data: JSON.stringify({
|
||||
messageId: 8,
|
||||
artifact: {
|
||||
id: 'form-1',
|
||||
type: 'form',
|
||||
status: 'pending',
|
||||
messageId: 8,
|
||||
conversationId: 3,
|
||||
payload: { id: 'form-1', title: '新增学生', fields: [] },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(message.uiArtifacts).toHaveLength(1);
|
||||
expect(message.uiArtifacts?.[0].payload).toMatchObject({ id: 'form-1', title: '新增学生' });
|
||||
});
|
||||
|
||||
it('merges ui.artifact events into uiArtifacts by id', () => {
|
||||
let message = reduceAiSseMessage(undefined, {
|
||||
event: 'ui.artifact',
|
||||
@@ -210,44 +182,6 @@ describe('AI chat SSE message reducer', () => {
|
||||
expect(message.forms?.[0].id).toBe('form-9');
|
||||
});
|
||||
|
||||
it('ignores legacy ui.review events and only consumes ui.artifact', () => {
|
||||
const review = {
|
||||
id: 'review-1',
|
||||
title: '开学导入',
|
||||
summary: '来自报名 Excel',
|
||||
status: 'pending',
|
||||
sections: [
|
||||
{
|
||||
key: 'students',
|
||||
type: 'students',
|
||||
title: '学生',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
],
|
||||
rows: [{ name: '张三', phone: '13800138000' }],
|
||||
issues: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
let message = reduceAiSseMessage(undefined, {
|
||||
event: 'ui.review',
|
||||
data: JSON.stringify({ messageId: 8, review }),
|
||||
});
|
||||
message = reduceAiSseMessage(message, {
|
||||
event: 'ui.review',
|
||||
data: JSON.stringify({
|
||||
messageId: 8,
|
||||
review: { ...review, status: 'submitted', resultSummary: '{"students":{"created":1}}' },
|
||||
}),
|
||||
});
|
||||
|
||||
// 旧事件不再写入 legacy 列表
|
||||
expect(message.reviews).toBeUndefined();
|
||||
expect(message.uiArtifacts).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('shows model retrying state and clears it when content starts', () => {
|
||||
let message = reduceAiSseMessage(undefined, {
|
||||
event: 'model.retrying',
|
||||
@@ -299,37 +233,6 @@ describe('AI chat SSE message reducer', () => {
|
||||
expect(message.reviews?.[0]).toMatchObject({ id: 'review-9', title: '批量导入' });
|
||||
});
|
||||
|
||||
it('ignores legacy ui.chart events and only consumes ui.artifact', () => {
|
||||
const chart = {
|
||||
id: 'chart-1',
|
||||
title: '各班级人数',
|
||||
chartType: 'bar',
|
||||
columns: [
|
||||
{ key: 'className', title: '班级' },
|
||||
{ key: 'count', title: '人数' },
|
||||
],
|
||||
rows: [
|
||||
{ className: '一班', count: 20 },
|
||||
{ className: '二班', count: 15 },
|
||||
],
|
||||
};
|
||||
let message = reduceAiSseMessage(undefined, {
|
||||
event: 'ui.chart',
|
||||
data: JSON.stringify({ messageId: 8, chart }),
|
||||
});
|
||||
message = reduceAiSseMessage(message, {
|
||||
event: 'ui.chart',
|
||||
data: JSON.stringify({
|
||||
messageId: 8,
|
||||
chart: { ...chart, id: 'chart-2', title: '女生人数' },
|
||||
}),
|
||||
});
|
||||
|
||||
// 旧事件不再写入 legacy 列表
|
||||
expect(message.charts).toBeUndefined();
|
||||
expect(message.uiArtifacts).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('restores persisted charts from message.completed metadata', () => {
|
||||
const message = reduceAiSseMessage(undefined, {
|
||||
event: 'message.completed',
|
||||
@@ -457,15 +360,16 @@ describe('AI chat SSE message reducer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('routes submit-time ui.review to the original message instead of the streaming one', () => {
|
||||
it('routes ui.artifact targeting another message to the external handler', () => {
|
||||
const provider = new GongxueAiChatProvider('http://x/api/ai/chat/conversations/3/stream');
|
||||
const onExternalReview = vi.fn();
|
||||
provider.onExternalReview = onExternalReview;
|
||||
const review = {
|
||||
id: 'review-1',
|
||||
title: '批量导入',
|
||||
const onExternalArtifact = vi.fn();
|
||||
provider.onExternalArtifact = onExternalArtifact;
|
||||
const artifact = {
|
||||
id: 'artifact-1',
|
||||
type: 'form',
|
||||
status: 'submitted',
|
||||
sections: [],
|
||||
messageId: 12,
|
||||
payload: { id: 'form-1', title: '批量导入', status: 'submitted' },
|
||||
};
|
||||
const origin = {
|
||||
id: 13,
|
||||
@@ -474,31 +378,37 @@ describe('AI chat SSE message reducer', () => {
|
||||
reasoningContent: '',
|
||||
toolRuns: [],
|
||||
attachments: [],
|
||||
reviews: [],
|
||||
uiArtifacts: [],
|
||||
};
|
||||
const next = provider.transformMessage({
|
||||
originMessage: origin,
|
||||
chunk: { event: 'ui.review', data: JSON.stringify({ messageId: 12, review }) },
|
||||
chunk: { event: 'ui.artifact', data: JSON.stringify({ messageId: 12, artifact }) },
|
||||
status: 'updating',
|
||||
chunks: [],
|
||||
responseHeaders: {} as Headers,
|
||||
});
|
||||
|
||||
expect(onExternalReview).toHaveBeenCalledWith(12, review);
|
||||
expect(onExternalArtifact).toHaveBeenCalledWith(12, artifact);
|
||||
expect(next).toBe(origin);
|
||||
expect(next.reviews ?? []).toHaveLength(0);
|
||||
expect(next.uiArtifacts ?? []).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('routes ui.review without an origin message to the external handler', () => {
|
||||
it('routes ui.artifact without an origin message to the external handler', () => {
|
||||
const provider = new GongxueAiChatProvider('http://x/api/ai/chat/conversations/3/stream');
|
||||
const onExternalReview = vi.fn();
|
||||
provider.onExternalReview = onExternalReview;
|
||||
const onExternalArtifact = vi.fn();
|
||||
provider.onExternalArtifact = onExternalArtifact;
|
||||
const next = provider.transformMessage({
|
||||
chunk: {
|
||||
event: 'ui.review',
|
||||
event: 'ui.artifact',
|
||||
data: JSON.stringify({
|
||||
messageId: 12,
|
||||
review: { id: 'review-1', title: '批量导入', status: 'submitted', sections: [] },
|
||||
artifact: {
|
||||
id: 'artifact-1',
|
||||
type: 'review',
|
||||
status: 'submitted',
|
||||
messageId: 12,
|
||||
payload: { id: 'review-1', title: '批量导入', status: 'submitted', sections: [] },
|
||||
},
|
||||
}),
|
||||
},
|
||||
status: 'updating',
|
||||
@@ -506,11 +416,11 @@ describe('AI chat SSE message reducer', () => {
|
||||
responseHeaders: {} as Headers,
|
||||
});
|
||||
|
||||
expect(onExternalReview).toHaveBeenCalledWith(
|
||||
expect(onExternalArtifact).toHaveBeenCalledWith(
|
||||
12,
|
||||
expect.objectContaining({ id: 'review-1' }),
|
||||
expect.objectContaining({ id: 'artifact-1' }),
|
||||
);
|
||||
expect(next.reviews ?? []).toHaveLength(0);
|
||||
expect(next.uiArtifacts ?? []).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('tolerates non-JSON event data', () => {
|
||||
|
||||
Reference in New Issue
Block a user