- 添加 answeredQuestions Map 存储已回答问题的状态 - 在重新渲染时恢复选中状态和 answered 类 - 已回答的问题自动隐藏输入框并禁用点击事件 - 确保用户选择在页面更新时保持显示
338 lines
10 KiB
TypeScript
338 lines
10 KiB
TypeScript
/**
|
||
* 对话服务
|
||
* 整合 SSE 通信、工具执行、用户交互
|
||
*/
|
||
import * as vscode from 'vscode';
|
||
import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from './sseHandler';
|
||
import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor';
|
||
import { userInteractionManager } from './userInteraction';
|
||
import { getConfig } from '../config/settings';
|
||
import type { DialogRequest, ToolCallRequest, AskUserEvent } from '../types/api';
|
||
|
||
/**
|
||
* 消息段落类型
|
||
*/
|
||
export interface MessageSegment {
|
||
type: 'text' | 'tool' | 'question';
|
||
content?: string;
|
||
toolName?: string;
|
||
toolStatus?: 'running' | 'success' | 'error';
|
||
toolResult?: string;
|
||
askId?: string;
|
||
question?: string;
|
||
options?: string[];
|
||
}
|
||
|
||
/**
|
||
* 对话回调接口
|
||
*/
|
||
export interface DialogCallbacks {
|
||
/** 收到文本(可能多次调用,流式) */
|
||
onText?: (text: string, isStreaming: boolean) => void;
|
||
/** 工具开始执行 */
|
||
onToolStart?: (toolName: string) => void;
|
||
/** 工具执行完成 */
|
||
onToolComplete?: (toolName: string, result: string) => void;
|
||
/** 工具执行错误 */
|
||
onToolError?: (toolName: string, error: string) => void;
|
||
/** 显示问题(ask_user) */
|
||
onQuestion?: (askId: string, question: string, options: string[]) => void;
|
||
/** 实时更新段落(流式过程中) */
|
||
onSegmentUpdate?: (segments: MessageSegment[]) => void;
|
||
/** 对话完成,返回所有段落 */
|
||
onComplete?: (segments: MessageSegment[]) => void;
|
||
/** 错误 */
|
||
onError?: (message: string) => void;
|
||
/** 通知消息 */
|
||
onNotification?: (message: string) => void;
|
||
}
|
||
|
||
/**
|
||
* 对话会话
|
||
*/
|
||
export class DialogSession {
|
||
private taskId: string;
|
||
private sseController: SSEController | null = null;
|
||
private toolContext: ToolExecutorContext;
|
||
private accumulatedText = '';
|
||
private isActive = false;
|
||
private segments: MessageSegment[] = [];
|
||
private currentTextSegment: MessageSegment | null = null;
|
||
|
||
constructor(extensionPath: string) {
|
||
this.taskId = generateTaskId();
|
||
this.toolContext = createToolExecutorContext(extensionPath);
|
||
}
|
||
|
||
/**
|
||
* 添加文本到当前文本段落
|
||
*/
|
||
private appendText(text: string): void {
|
||
if (!this.currentTextSegment) {
|
||
this.currentTextSegment = { type: 'text', content: '' };
|
||
this.segments.push(this.currentTextSegment);
|
||
}
|
||
this.currentTextSegment.content = (this.currentTextSegment.content || '') + text;
|
||
}
|
||
|
||
/**
|
||
* 结束当前文本段落
|
||
*/
|
||
private finalizeTextSegment(): void {
|
||
this.currentTextSegment = null;
|
||
}
|
||
|
||
/**
|
||
* 添加工具段落
|
||
*/
|
||
private addToolSegment(toolName: string, status: 'running' | 'success' | 'error', result?: string): MessageSegment {
|
||
this.finalizeTextSegment();
|
||
const segment: MessageSegment = {
|
||
type: 'tool',
|
||
toolName,
|
||
toolStatus: status,
|
||
toolResult: result
|
||
};
|
||
this.segments.push(segment);
|
||
return segment;
|
||
}
|
||
|
||
/**
|
||
* 更新工具段落状态
|
||
*/
|
||
private updateToolSegment(toolName: string, status: 'success' | 'error', result?: string): void {
|
||
// 找到最后一个匹配的工具段落
|
||
for (let i = this.segments.length - 1; i >= 0; i--) {
|
||
const seg = this.segments[i];
|
||
if (seg.type === 'tool' && seg.toolName === toolName && seg.toolStatus === 'running') {
|
||
seg.toolStatus = status;
|
||
seg.toolResult = result;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取任务ID
|
||
*/
|
||
getTaskId(): string {
|
||
return this.taskId;
|
||
}
|
||
|
||
/**
|
||
* 是否活跃
|
||
*/
|
||
get active(): boolean {
|
||
return this.isActive;
|
||
}
|
||
|
||
/**
|
||
* 发送消息并开始流式对话
|
||
*/
|
||
async sendMessage(
|
||
message: string,
|
||
callbacks: DialogCallbacks
|
||
): Promise<void> {
|
||
if (this.isActive) {
|
||
callbacks.onError?.('当前有对话正在进行中');
|
||
return;
|
||
}
|
||
|
||
this.isActive = true;
|
||
this.accumulatedText = '';
|
||
this.segments = [];
|
||
this.currentTextSegment = null;
|
||
|
||
const config = getConfig();
|
||
const request: DialogRequest = {
|
||
taskId: this.taskId,
|
||
message,
|
||
userId: config.userId,
|
||
toolMode: 'AGENT'
|
||
};
|
||
|
||
const sseCallbacks: SSECallbacks = {
|
||
onTextDelta: (data) => {
|
||
this.accumulatedText += data.text;
|
||
this.appendText(data.text);
|
||
console.log('[DialogSession] onTextDelta, 累积文本长度:', this.accumulatedText.length);
|
||
callbacks.onText?.(this.accumulatedText, true);
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
},
|
||
|
||
onToolCall: async (data: ToolCallRequest) => {
|
||
const toolName = data.params.name;
|
||
console.log('[DialogSession] onToolCall:', toolName);
|
||
// 检查是否已经有相同的工具段落(可能由 onToolStart 添加)
|
||
const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop();
|
||
if (lastToolSegment && lastToolSegment.toolName === toolName && lastToolSegment.toolStatus === 'running') {
|
||
console.log('[DialogSession] onToolCall: 跳过重复的工具段落:', toolName);
|
||
} else {
|
||
this.addToolSegment(toolName, 'running');
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
// 注意:不在这里调用 callbacks.onToolStart,避免与 onToolStart 事件重复
|
||
try {
|
||
await executeToolCall(data, this.toolContext);
|
||
this.updateToolSegment(toolName, 'success', '执行完成');
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
// 也不调用 callbacks.onToolComplete,避免重复
|
||
} catch (error) {
|
||
const errorMsg = error instanceof Error ? error.message : '未知错误';
|
||
this.updateToolSegment(toolName, 'error', errorMsg);
|
||
callbacks.onToolError?.(toolName, errorMsg);
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
},
|
||
|
||
onToolStart: (data) => {
|
||
console.log('[DialogSession] onToolStart:', data.tool_name);
|
||
// 检查是否已经有相同的工具段落(可能由 onToolCall 添加)
|
||
const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop();
|
||
if (lastToolSegment && lastToolSegment.toolName === data.tool_name && lastToolSegment.toolStatus === 'running') {
|
||
console.log('[DialogSession] 跳过重复的工具段落:', data.tool_name);
|
||
} else {
|
||
this.addToolSegment(data.tool_name, 'running');
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
console.log('[DialogSession] segments 数量:', this.segments.length);
|
||
callbacks.onToolStart?.(data.tool_name);
|
||
},
|
||
|
||
onToolComplete: (data) => {
|
||
this.updateToolSegment(data.tool_name, 'success', data.result);
|
||
callbacks.onToolComplete?.(data.tool_name, data.result);
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
},
|
||
|
||
onToolError: (data) => {
|
||
this.updateToolSegment(data.tool_name, 'error', data.error);
|
||
callbacks.onToolError?.(data.tool_name, data.error);
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
},
|
||
|
||
onAskUser: async (data: AskUserEvent) => {
|
||
this.finalizeTextSegment();
|
||
this.segments.push({
|
||
type: 'question',
|
||
askId: data.askId,
|
||
question: data.question,
|
||
options: data.options
|
||
});
|
||
// 实时发送段落更新(包含问题)
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
// 同时调用 onQuestion 用于更新状态栏等
|
||
callbacks.onQuestion?.(data.askId, data.question, data.options);
|
||
try {
|
||
await userInteractionManager.handleAskUser(data, this.taskId);
|
||
} catch (error) {
|
||
console.error('[DialogSession] 处理用户问题失败:', error);
|
||
}
|
||
},
|
||
|
||
onComplete: (data) => {
|
||
this.isActive = false;
|
||
this.finalizeTextSegment();
|
||
// 发送所有段落
|
||
callbacks.onComplete?.(this.segments);
|
||
},
|
||
|
||
onError: (data) => {
|
||
this.isActive = false;
|
||
callbacks.onError?.(data.message);
|
||
},
|
||
|
||
onWarning: (data) => {
|
||
callbacks.onNotification?.(`⚠️ ${data.message}`);
|
||
},
|
||
|
||
onNotification: (data) => {
|
||
callbacks.onNotification?.(data.message);
|
||
},
|
||
|
||
onOpen: () => {
|
||
console.log('[DialogSession] SSE 连接已建立');
|
||
},
|
||
|
||
onClose: () => {
|
||
console.log('[DialogSession] SSE 连接已关闭');
|
||
this.isActive = false;
|
||
}
|
||
};
|
||
|
||
try {
|
||
this.sseController = await startStreamDialog(request, sseCallbacks);
|
||
} catch (error) {
|
||
this.isActive = false;
|
||
const errorMsg = error instanceof Error ? error.message : '连接失败';
|
||
callbacks.onError?.(errorMsg);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 中止当前对话
|
||
*/
|
||
abort(): void {
|
||
if (this.sseController) {
|
||
this.sseController.abort();
|
||
this.sseController = null;
|
||
}
|
||
this.isActive = false;
|
||
userInteractionManager.cancelAll();
|
||
}
|
||
|
||
/**
|
||
* 提交用户回答
|
||
*/
|
||
async submitAnswer(
|
||
askId: string,
|
||
selected?: string[],
|
||
customInput?: string
|
||
): Promise<void> {
|
||
await userInteractionManager.receiveAnswer(askId, selected, customInput);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 全局对话会话管理
|
||
*/
|
||
class DialogManager {
|
||
private currentSession: DialogSession | null = null;
|
||
|
||
/**
|
||
* 创建新会话
|
||
*/
|
||
createSession(extensionPath: string): DialogSession {
|
||
// 如果有活跃会话,先中止
|
||
if (this.currentSession?.active) {
|
||
this.currentSession.abort();
|
||
}
|
||
this.currentSession = new DialogSession(extensionPath);
|
||
return this.currentSession;
|
||
}
|
||
|
||
/**
|
||
* 获取当前会话
|
||
*/
|
||
getCurrentSession(): DialogSession | null {
|
||
return this.currentSession;
|
||
}
|
||
|
||
/**
|
||
* 中止当前会话
|
||
*/
|
||
abortCurrentSession(): void {
|
||
this.currentSession?.abort();
|
||
}
|
||
}
|
||
|
||
export const dialogManager = new DialogManager();
|