feat(ai): 补齐 Cursor 与 CodeBuddy 会话态聊天链路

- 新增 SessionChatProvider 接口,补齐非流式对话的会话态复用能力
- 为 Cursor Agent 和 CodeBuddy CLI 同步实现流式与非流式会话续接及状态持久化
- CustomProvider 补充会话态透传,统一 custom provider 的会话复用行为
- Service 新增 AIChatSendInSession,聊天主链路非流式回退改走带 session 的发送接口
- 保留原 AIChatSend 无状态语义,避免标题生成和记忆压缩污染主会话上下文
- 补充前后端定向测试,覆盖会话恢复、续接发送和前端回退分流
This commit is contained in:
Syngnat
2026-06-18 13:35:08 +08:00
parent b588235b62
commit 06dd9507ee
12 changed files with 1392 additions and 137 deletions

View File

@@ -38,8 +38,8 @@ describe('aiChatPayloadDispatch', () => {
expect(setSending).not.toHaveBeenCalled();
});
it('appends a non-stream assistant message when only AIChatSend is available', async () => {
const AIChatSend = vi.fn().mockResolvedValue({
it('appends a non-stream assistant message when session-aware send is available', async () => {
const AIChatSendInSession = vi.fn().mockResolvedValue({
success: true,
content: 'done',
reasoning_content: 'thinking',
@@ -51,7 +51,7 @@ describe('aiChatPayloadDispatch', () => {
(globalThis as any).window = {
go: {
aiservice: {
Service: { AIChatSend },
Service: { AIChatSendInSession },
},
},
};
@@ -67,6 +67,7 @@ describe('aiChatPayloadDispatch', () => {
});
expect(result).toBe('send');
expect(AIChatSendInSession).toHaveBeenCalledWith('session-1', [{ role: 'user', content: 'hello' }], []);
expect(addAIChatMessage).toHaveBeenCalledWith('session-1', expect.objectContaining({
id: 'msg-send',
role: 'assistant',
@@ -79,7 +80,7 @@ describe('aiChatPayloadDispatch', () => {
});
it('settles the pending assistant message when falling back to non-stream send', async () => {
const AIChatSend = vi.fn().mockResolvedValue({
const AIChatSendInSession = vi.fn().mockResolvedValue({
success: true,
content: 'done',
reasoning_content: 'thinking',
@@ -91,7 +92,7 @@ describe('aiChatPayloadDispatch', () => {
(globalThis as any).window = {
go: {
aiservice: {
Service: { AIChatSend },
Service: { AIChatSendInSession },
},
},
};
@@ -108,6 +109,7 @@ describe('aiChatPayloadDispatch', () => {
});
expect(result).toBe('send');
expect(AIChatSendInSession).toHaveBeenCalledWith('session-1', [{ role: 'user', content: 'hello' }], []);
expect(addAIChatMessage).not.toHaveBeenCalled();
expect(updateAIChatMessage).toHaveBeenCalledWith('session-1', 'assistant-connecting', expect.objectContaining({
content: 'done',
@@ -119,6 +121,44 @@ describe('aiChatPayloadDispatch', () => {
expect(setSending).toHaveBeenCalledWith(false);
});
it('falls back to stateless AIChatSend when session-aware send is unavailable', async () => {
const AIChatSend = vi.fn().mockResolvedValue({
success: true,
content: 'done',
reasoning_content: 'thinking',
});
const addAIChatMessage = vi.fn();
const setSending = vi.fn();
(globalThis as any).window = {
go: {
aiservice: {
Service: { AIChatSend },
},
},
};
const result = await dispatchAIChatPayload({
sid: 'session-1',
messages: [{ role: 'user', content: 'hello' }],
tools: [],
addAIChatMessage,
setSending,
nextMessageId: () => 'msg-send',
});
expect(result).toBe('send');
expect(AIChatSend).toHaveBeenCalledWith([{ role: 'user', content: 'hello' }], []);
expect(addAIChatMessage).toHaveBeenCalledWith('session-1', expect.objectContaining({
id: 'msg-send',
role: 'assistant',
content: 'done',
thinking: 'thinking',
reasoning_content: 'thinking',
}));
expect(setSending).toHaveBeenCalledWith(false);
});
it('emits the unavailable message when the AI service is missing', async () => {
const addAIChatMessage = vi.fn();
const setSending = vi.fn();

View File

@@ -8,6 +8,7 @@ import { sanitizeErrorMsg } from '../../utils/aiChatRuntime';
interface AIChatService {
AIChatStream?: (sid: string, messages: any[], tools: AIChatToolDefinition[]) => Promise<any>;
AIChatSendInSession?: (sid: string, messages: any[], tools: AIChatToolDefinition[]) => Promise<any>;
AIChatSend?: (messages: any[], tools: AIChatToolDefinition[]) => Promise<any>;
}
@@ -92,8 +93,10 @@ export const dispatchAIChatPayload = async ({
return 'stream';
}
if (service?.AIChatSend) {
const result = await service.AIChatSend(messages, tools);
if (service?.AIChatSendInSession || service?.AIChatSend) {
const result = service?.AIChatSendInSession
? await service.AIChatSendInSession(sid, messages, tools)
: await service!.AIChatSend!(messages, tools);
const rawError = result?.error || '未知错误';
const cleanError = sanitizeErrorMsg(rawError);

View File

@@ -8,6 +8,8 @@ export function AIChatCancel(arg1:string):Promise<void>;
export function AIChatSend(arg1:Array<ai.Message>,arg2:Array<ai.Tool>):Promise<Record<string, any>>;
export function AIChatSendInSession(arg1:string,arg2:Array<ai.Message>,arg3:Array<ai.Tool>):Promise<Record<string, any>>;
export function AIChatStream(arg1:string,arg2:Array<ai.Message>,arg3:Array<ai.Tool>):Promise<void>;
export function AICheckSQL(arg1:string):Promise<ai.SafetyResult>;

View File

@@ -14,6 +14,10 @@ export function AIChatSend(arg1, arg2) {
return window['go']['aiservice']['Service']['AIChatSend'](arg1, arg2);
}
export function AIChatSendInSession(arg1, arg2, arg3) {
return window['go']['aiservice']['Service']['AIChatSendInSession'](arg1, arg2, arg3);
}
export function AIChatStream(arg1, arg2, arg3) {
return window['go']['aiservice']['Service']['AIChatStream'](arg1, arg2, arg3);
}