- 添加 memory_compacted SSE 事件处理 - 添加 CompactedMemory/CompactedMessage 类型定义 - 添加 COMPACTION_SUMMARY 消息类型 - 实现压缩数据存储到 conversation.json - 实现从 conversation.json 构建恢复数据 - 发送请求时附带 knowledgeData 用于恢复知识图谱
607 lines
19 KiB
TypeScript
607 lines
19 KiB
TypeScript
/**
|
||
* 对话服务
|
||
* 整合 SSE 通信、工具执行、用户交互
|
||
*/
|
||
import * as vscode from 'vscode';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
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, RunMode, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
|
||
import { submitToolConfirm, submitAnswer } from './apiClient';
|
||
import { ChatHistoryManager } from '../utils/chatHistoryManager';
|
||
|
||
/**
|
||
* 消息段落类型
|
||
*/
|
||
export interface MessageSegment {
|
||
type: 'text' | 'tool' | 'question' | 'agent' | 'plan';
|
||
content?: string;
|
||
toolName?: string;
|
||
toolStatus?: 'running' | 'success' | 'error';
|
||
toolResult?: string;
|
||
askId?: string;
|
||
question?: string;
|
||
options?: string[];
|
||
// 智能体相关字段
|
||
agentId?: string;
|
||
agentName?: string;
|
||
agentStatus?: 'running' | 'completed' | 'error';
|
||
agentSteps?: AgentStep[];
|
||
// 计划相关字段
|
||
planTitle?: string;
|
||
planSteps?: string[];
|
||
planSummary?: string;
|
||
}
|
||
|
||
/**
|
||
* 智能体执行步骤
|
||
*/
|
||
export interface AgentStep {
|
||
step: number;
|
||
toolName: string;
|
||
toolInput?: unknown;
|
||
toolResult?: string;
|
||
status: 'running' | 'completed' | 'error';
|
||
}
|
||
|
||
/**
|
||
* 对话回调接口
|
||
*/
|
||
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 模式) */
|
||
onToolConfirm?: (confirmId: number, toolName: string, toolInput: Record<string, unknown>) => void;
|
||
/** 计划确认请求(Plan 模式) */
|
||
onPlanConfirm?: (confirmId: number, title: string, steps: string[], summary: 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, existingTaskId?: string) {
|
||
// 支持复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||
this.taskId = existingTaskId || 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;
|
||
}
|
||
|
||
/**
|
||
* 加载知识图谱数据
|
||
* 从 .iccoder/knowledge.json 读取
|
||
*/
|
||
private loadKnowledgeData(): string | null {
|
||
console.log('[DialogSession] loadKnowledgeData 开始执行');
|
||
|
||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||
console.log('[DialogSession] 没有工作区文件夹');
|
||
return null;
|
||
}
|
||
|
||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||
const knowledgePath = path.join(workspacePath, '.iccoder', 'knowledge.json');
|
||
console.log('[DialogSession] 知识图谱路径:', knowledgePath);
|
||
|
||
try {
|
||
const exists = fs.existsSync(knowledgePath);
|
||
console.log('[DialogSession] 文件存在:', exists);
|
||
|
||
if (exists) {
|
||
const content = fs.readFileSync(knowledgePath, 'utf-8');
|
||
console.log('[DialogSession] 加载知识图谱成功, 长度:', content.length);
|
||
return content;
|
||
}
|
||
} catch (error) {
|
||
console.warn('[DialogSession] 加载知识图谱失败:', error);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取工具操作描述(用于确认对话框)
|
||
*/
|
||
private getToolDescription(toolName: string, toolInput: Record<string, unknown>): string {
|
||
const lines: string[] = [];
|
||
|
||
switch (toolName) {
|
||
case 'file_write':
|
||
lines.push(`文件路径: ${toolInput.path || '未知'}`);
|
||
if (toolInput.content) {
|
||
const content = String(toolInput.content);
|
||
lines.push(`内容长度: ${content.length} 字符`);
|
||
lines.push(`内容预览: ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`);
|
||
}
|
||
break;
|
||
case 'file_delete':
|
||
lines.push(`删除文件: ${toolInput.path || '未知'}`);
|
||
break;
|
||
case 'syntax_check':
|
||
lines.push('执行语法检查');
|
||
if (toolInput.code) {
|
||
const code = String(toolInput.code);
|
||
lines.push(`代码长度: ${code.length} 字符`);
|
||
}
|
||
break;
|
||
case 'simulation':
|
||
lines.push(`RTL文件: ${toolInput.rtlPath || '未知'}`);
|
||
lines.push(`TB文件: ${toolInput.tbPath || '未知'}`);
|
||
if (toolInput.duration) {
|
||
lines.push(`仿真时长: ${toolInput.duration}`);
|
||
}
|
||
break;
|
||
default:
|
||
lines.push(`参数: ${JSON.stringify(toolInput, null, 2)}`);
|
||
}
|
||
|
||
return lines.join('\n');
|
||
}
|
||
|
||
/**
|
||
* 发送消息并开始流式对话
|
||
*/
|
||
async sendMessage(
|
||
message: string,
|
||
callbacks: DialogCallbacks,
|
||
mode?: RunMode
|
||
): Promise<void> {
|
||
if (this.isActive) {
|
||
callbacks.onError?.('当前有对话正在进行中');
|
||
return;
|
||
}
|
||
|
||
this.isActive = true;
|
||
this.accumulatedText = '';
|
||
this.segments = [];
|
||
this.currentTextSegment = null;
|
||
|
||
const config = getConfig();
|
||
|
||
// 获取压缩数据和新消息(用于后端重启后恢复)
|
||
const historyManager = ChatHistoryManager.getInstance();
|
||
const compactedData = await historyManager.loadCompactedData(this.taskId);
|
||
const newMessages = historyManager.getNewMessagesSinceCompaction();
|
||
|
||
// 加载知识图谱数据
|
||
const knowledgeData = this.loadKnowledgeData();
|
||
console.log('[DialogSession] knowledgeData 加载结果:', knowledgeData ? `${knowledgeData.length} 字符` : 'null');
|
||
|
||
const request: DialogRequest = {
|
||
taskId: this.taskId,
|
||
message,
|
||
userId: config.userId,
|
||
mode: mode || 'agent',
|
||
compactedData: compactedData || undefined,
|
||
newMessages: newMessages.length > 0 ? newMessages : undefined,
|
||
knowledgeData: knowledgeData || undefined
|
||
};
|
||
|
||
// 追踪用户消息
|
||
historyManager.trackUserMessage(message);
|
||
|
||
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);
|
||
|
||
// 检查是否有活跃的智能体(如果有,工具执行会显示在智能体卡片内,不需要单独显示)
|
||
const hasActiveAgent = this.segments.some(
|
||
s => s.type === 'agent' && s.agentStatus === 'running'
|
||
);
|
||
|
||
if (hasActiveAgent) {
|
||
console.log('[DialogSession] onToolCall: 智能体执行中,跳过工具段落:', toolName);
|
||
} else {
|
||
// 检查是否已经有相同的工具段落(可能由 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);
|
||
if (!hasActiveAgent) {
|
||
this.updateToolSegment(toolName, 'success', '执行完成');
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
// 也不调用 callbacks.onToolComplete,避免重复
|
||
} catch (error) {
|
||
const errorMsg = error instanceof Error ? error.message : '未知错误';
|
||
if (!hasActiveAgent) {
|
||
this.updateToolSegment(toolName, 'error', errorMsg);
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
callbacks.onToolError?.(toolName, errorMsg);
|
||
}
|
||
},
|
||
|
||
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);
|
||
},
|
||
|
||
onToolConfirm: async (data: ToolConfirmEvent) => {
|
||
console.log('[DialogSession] onToolConfirm:', data.toolName, data.confirmId);
|
||
|
||
// 调用回调通知 UI 显示确认对话框
|
||
callbacks.onToolConfirm?.(data.confirmId, data.toolName, data.toolInput);
|
||
|
||
// 使用 VSCode 快速选择框显示确认对话框
|
||
const toolDescription = this.getToolDescription(data.toolName, data.toolInput);
|
||
const result = await vscode.window.showWarningMessage(
|
||
`确认执行操作: ${data.toolName}`,
|
||
{ modal: true, detail: toolDescription },
|
||
'确认执行',
|
||
'取消'
|
||
);
|
||
|
||
const approved = result === '确认执行';
|
||
console.log('[DialogSession] 用户确认结果:', approved);
|
||
|
||
// 发送确认响应到后端
|
||
try {
|
||
await submitToolConfirm({
|
||
confirmId: data.confirmId,
|
||
taskId: this.taskId,
|
||
approved
|
||
});
|
||
} catch (error) {
|
||
console.error('[DialogSession] 发送确认响应失败:', error);
|
||
}
|
||
},
|
||
|
||
onPlanConfirm: async (data: PlanConfirmEvent) => {
|
||
console.log('[DialogSession] onPlanConfirm:', data.title);
|
||
|
||
// 结束当前文本段落
|
||
this.finalizeTextSegment();
|
||
|
||
const askId = `ask_${data.confirmId}`;
|
||
|
||
// 添加计划段落到聊天界面(包含 askId 用于响应)
|
||
this.segments.push({
|
||
type: 'plan',
|
||
askId: askId,
|
||
planTitle: data.title,
|
||
planSteps: data.steps,
|
||
planSummary: data.summary
|
||
});
|
||
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
|
||
// 注册问题到前端(类似 askUser),以便用户回答时能找到
|
||
const planEvent = {
|
||
askId: askId,
|
||
question: `请确认执行计划:${data.title}`,
|
||
options: ['确认执行', '修改计划', '取消']
|
||
};
|
||
try {
|
||
await userInteractionManager.handleAskUser(planEvent as AskUserEvent, this.taskId);
|
||
} catch (error) {
|
||
console.error('[DialogSession] 处理计划确认失败:', error);
|
||
}
|
||
|
||
// 调用回调通知 UI
|
||
callbacks.onPlanConfirm?.(data.confirmId, data.title, data.steps, data.summary);
|
||
},
|
||
|
||
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);
|
||
},
|
||
|
||
// 智能体事件处理
|
||
onAgentStart: (data) => {
|
||
console.log('[DialogSession] onAgentStart:', data.agentId);
|
||
this.finalizeTextSegment();
|
||
this.segments.push({
|
||
type: 'agent',
|
||
agentId: data.agentId,
|
||
agentName: data.agentName,
|
||
content: data.instruction,
|
||
agentStatus: 'running',
|
||
agentSteps: []
|
||
});
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
},
|
||
|
||
onAgentProgress: (data) => {
|
||
console.log('[DialogSession] onAgentProgress:', data.agentId, data.step, data.status);
|
||
const agentSegment = this.segments.find(
|
||
s => s.type === 'agent' && s.agentId === data.agentId
|
||
);
|
||
if (agentSegment && agentSegment.agentSteps) {
|
||
if (data.status === 'running') {
|
||
agentSegment.agentSteps.push({
|
||
step: data.step,
|
||
toolName: data.toolName,
|
||
toolInput: data.toolInput,
|
||
status: 'running'
|
||
});
|
||
} else {
|
||
const step = agentSegment.agentSteps.find(s => s.step === data.step);
|
||
if (step) {
|
||
step.status = data.status;
|
||
step.toolResult = data.toolResult;
|
||
}
|
||
}
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
},
|
||
|
||
onAgentComplete: (data) => {
|
||
console.log('[DialogSession] onAgentComplete:', data.agentId);
|
||
const agentSegment = this.segments.find(
|
||
s => s.type === 'agent' && s.agentId === data.agentId
|
||
);
|
||
if (agentSegment) {
|
||
agentSegment.agentStatus = 'completed';
|
||
agentSegment.content = data.summary;
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
},
|
||
|
||
onAgentError: (data) => {
|
||
console.log('[DialogSession] onAgentError:', data.agentId, data.error);
|
||
const agentSegment = this.segments.find(
|
||
s => s.type === 'agent' && s.agentId === data.agentId
|
||
);
|
||
if (agentSegment) {
|
||
agentSegment.agentStatus = 'error';
|
||
agentSegment.content = data.error;
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
},
|
||
|
||
onMemoryCompacted: async (data) => {
|
||
console.log('[DialogSession] onMemoryCompacted:', data.taskId);
|
||
// 保存压缩数据到本地
|
||
await historyManager.saveCompactedData(data.compactedData);
|
||
},
|
||
|
||
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;
|
||
|
||
/**
|
||
* 创建新会话
|
||
* @param extensionPath 扩展路径
|
||
* @param existingTaskId 可选,复用现有的 taskId(用于 Plan 模式确认后继续执行)
|
||
*/
|
||
createSession(extensionPath: string, existingTaskId?: string): DialogSession {
|
||
// 如果有活跃会话,先中止
|
||
if (this.currentSession?.active) {
|
||
this.currentSession.abort();
|
||
}
|
||
this.currentSession = new DialogSession(extensionPath, existingTaskId);
|
||
return this.currentSession;
|
||
}
|
||
|
||
/**
|
||
* 获取当前会话
|
||
*/
|
||
getCurrentSession(): DialogSession | null {
|
||
return this.currentSession;
|
||
}
|
||
|
||
/**
|
||
* 中止当前会话
|
||
*/
|
||
abortCurrentSession(): void {
|
||
this.currentSession?.abort();
|
||
}
|
||
}
|
||
|
||
export const dialogManager = new DialogManager();
|