You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ai-chat-vscode/src/MessageHandler.ts

489 lines
19 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import vscode from "vscode";
import path from "path";
import {callQwenAPI} from "./utils/modelApi";
import {generateCodeDiff, getFileExtension} from "./utils/common";
// 定义全局变量来存储面板状态
let currentSessionHistory: { role: string; content: string }[] = [];
// 存储工作区变更的文件,包含状态信息
let workspaceChanges: {
[key: string]: {
original: string;
modified: string;
status: 'pending' | 'accepted' | 'rejected'
}
} = {};
export class MessageHandler {
private provider: any; // 这里应该使用AISidebarViewProvider类型但由于循环依赖问题暂时使用any
private context: vscode.ExtensionContext;
private currentReader: ReadableStreamDefaultReader<Uint8Array> | null = null; // 添加当前reader引用
constructor(provider: any, context: vscode.ExtensionContext) {
this.provider = provider;
this.context = context;
this.currentReader = null;
}
public async handleMessage(message: any) {
switch (message.command) {
case 'ask':
await this.handleAsk(message);
break;
case 'cancelRequest':
await this.handleCancelRequest();
break;
case 'selectContextFile':
await this.handleSelectContextFile();
break;
case 'getFileList':
await this.handleGetFileList();
break;
case 'selectFileByPath':
await this.handleSelectFileByPath(message);
break;
case 'showCodeDiffInEditor':
await this.handleShowCodeDiffInEditor(message);
break;
case 'acceptChanges':
await this.handleAcceptChanges(message);
break;
case 'rejectChanges':
await this.handleRejectChanges(message);
break;
case 'openWorkspaceFile':
await this.handleOpenWorkspaceFile(message);
break;
case 'getCodeChanges':
this.handleGetCodeChanges();
break;
case 'createNewFile':
await this.handleCreateNewFile(message);
break;
case 'restoreState':
this.handleRestoreState(message);
break;
}
}
private async handleAsk(message: any) {
const question = message.text;
const fileContent = message.fileContent;
const fileName = message.fileContentPath ? path.basename(message.fileContentPath) : "current_file.txt";
const history = currentSessionHistory;
currentSessionHistory = [...history, {role: 'user', content: question}];
this.provider._postMessage({command: 'addMessage', role: 'user', content: question});
try {
const response = await callQwenAPI(question, history, fileContent, this.context, fileName);
// 发送开始流式传输的消息
this.provider._postMessage({command: 'startStream', role: 'assistant'});
// 处理流式响应
const reader = response.body?.getReader();
this.currentReader = reader; // 保存reader引用
const decoder = new TextDecoder('utf-8');
let aiMessage = '';
if (reader) {
try {
while (true) {
const {done, value} = await reader.read();
if (done) break;
const chunk = decoder.decode(value, {stream: true});
// 检查是否包含event: end事件
let processedChunk = chunk;
if (chunk.includes('event: end')) {
// 只处理event: end之前的内容
const endIndex = chunk.indexOf('event: end');
processedChunk = chunk.substring(0, endIndex);
// 处理截取后的内容
if (processedChunk.startsWith('data:')) {
// 移除data:前缀
processedChunk = processedChunk.replace(/^data:/, '');
// 逐个匹配换行符,当出现两个或更多连续换行符时删除多余的
processedChunk = processedChunk.replace(/^(\n{2,})/, (match) => {
// 删除所有连续的换行符
return '';
});
}
if (processedChunk) {
aiMessage += processedChunk;
// 发送流式数据到前端
this.provider._postMessage({
command: 'streamData',
data: processedChunk
});
}
// 停止继续处理
break;
}
if (chunk.startsWith('data:')) {
// 移除data:前缀
processedChunk = chunk.replace(/^data:/, '');
// 逐个匹配换行符,当出现两个或更多连续换行符时删除多余的
processedChunk = processedChunk.replace(/^(\n{2,})/, (match) => {
// 删除所有连续的换行符
return '';
});
}
aiMessage += processedChunk;
// 发送流式数据到前端
this.provider._postMessage({
command: 'streamData',
data: processedChunk
});
}
} catch (readError: any) {
// 检查是否是由于取消请求导致的中断
if (readError.name === 'AbortError' || readError.message.includes('cancel')) {
console.log('请求已被用户中断');
// 通知前端请求已中断
this.provider._postMessage({command: 'requestCancelled'});
// 重置状态但不抛出错误
this.provider._postMessage({command: 'hideLoading'});
return;
}
// 如果是其他错误,重新抛出
throw readError;
} finally {
// 清理reader引用
this.currentReader = null;
}
// 流传输完成后,更新会话历史
currentSessionHistory = [...currentSessionHistory, {role: 'assistant', content: aiMessage}];
// 解析AI响应中的多个代码块并生成独立的codeDiff信息
const codeBlocks = aiMessage.match(/```[\s\S]*?```/g) || [];
// 存储所有生成的codeDiff
const allCodeDiffs = [];
for (const block of codeBlocks) {
// 生成codeDiff
const codeDiff = generateCodeDiff(fileContent, block);
if (codeDiff) {
// @ts-ignore
allCodeDiffs.push(codeDiff);
}
}
console.log("allCodeDiffs:",allCodeDiffs)
// 通知前端流传输完成
this.provider._postMessage({
command: 'endStream',
content: aiMessage,
codeDiffs: allCodeDiffs // 传递多个codeDiff信息
});
// 如果有代码差异,保存到工作区变更中
if (allCodeDiffs.length > 0 && fileContent && message.fileContentPath) {
// 为每个codeDiff创建独立的workspaceChange
for (let i = 0; i < allCodeDiffs.length; i++) {
const codeDiff = allCodeDiffs[i];
const filePath = message.fileContentPath;
// 创建唯一的工作区变更键
const changeKey = `${filePath}`;
workspaceChanges[changeKey] = {
original: fileContent,
// @ts-ignore
modified: codeDiff.modifiedCode,
status: 'pending'
};
}
// 通知 WebView 更新工作区文件列表
this.provider._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
}
}
} catch (error: any) {
// 检查是否是由于取消请求导致的中断
if (error.name === 'AbortError' || error.message.includes('cancel') || error.message.includes('中断')) {
console.log('请求已被用户中断');
this.provider._postMessage({command: 'requestCancelled'});
} else {
this.provider._postMessage({command: 'addMessage', role: 'ai', content: '调用失败:' + error.message});
}
}
this.provider._postMessage({command: 'hideLoading'});
}
private async handleSelectContextFile() {
// 执行文件选择逻辑
const uris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
title: "选择一个文件作为上下文"
});
if (uris && uris.length > 0) {
const selectedFileUri = uris[0];
const fileContent = await vscode.workspace.fs.readFile(selectedFileUri);
const decodedContent = new TextDecoder("utf-8").decode(fileContent);
this.provider._postMessage({
command: 'setContextFile',
fileName: selectedFileUri.fsPath,
fileContent: decodedContent
});
}
}
private async handleGetFileList() {
// 执行获取文件列表逻辑
const workspaceFolders = vscode.workspace.workspaceFolders;
if (workspaceFolders && workspaceFolders.length > 0) {
const fileUris = await vscode.workspace.findFiles('**/*', '**/node_modules/**');
const files = fileUris.map(uri => {
const relativePath = vscode.workspace.asRelativePath(uri);
return {
path: uri.fsPath,
relativePath: relativePath,
name: path.basename(uri.fsPath),
isDirectory: false // 简化处理,实际可以根据扩展名判断
};
});
this.provider._postMessage({
command: 'fileList',
files: files
});
}
}
private async handleSelectFileByPath(message: any) {
// 根据路径选择文件
try {
const fileUri = vscode.Uri.file(message.filePath);
const fileContent = await vscode.workspace.fs.readFile(fileUri);
const decodedContent = new TextDecoder("utf-8").decode(fileContent);
this.provider._postMessage({
command: 'setContextFile',
fileName: fileUri.fsPath,
fileContent: decodedContent
});
} catch (error: any) {
vscode.window.showErrorMessage(`无法读取文件: ${error.message}`);
}
}
private async handleShowCodeDiffInEditor(message: any) {
// 在编辑器中显示代码差异
try {
const originalCode = message.originalCode || '';
const modifiedCode = message.modifiedCode || '';
// 创建原始代码的虚拟文档
const originalUri = vscode.Uri.parse('untitled:original-code.txt');
const originalDoc = await vscode.workspace.openTextDocument(originalUri);
const originalEditor = await vscode.window.showTextDocument(originalDoc, vscode.ViewColumn.One, true);
await originalEditor.edit(edit => {
edit.insert(new vscode.Position(0, 0), originalCode);
});
// 创建修改后代码的虚拟文档
const modifiedUri = vscode.Uri.parse('untitled:modified-code.txt');
const modifiedDoc = await vscode.workspace.openTextDocument(modifiedUri);
const modifiedEditor = await vscode.window.showTextDocument(modifiedDoc, vscode.ViewColumn.Two, true);
await modifiedEditor.edit(edit => {
edit.insert(new vscode.Position(0, 0), modifiedCode);
});
// 使用 VS Code 的 diff 命令显示差异
await vscode.commands.executeCommand(
'vscode.diff',
originalUri,
modifiedUri,
'Code Diff - Original ↔ Modified'
);
} catch (error: any) {
vscode.window.showErrorMessage(`显示代码差异失败: ${error.message}`);
}
}
private async handleAcceptChanges(message: any) {
// 接受代码变更
try {
const {filePath, modifiedContent} = message;
const fileUri = vscode.Uri.file(filePath);
// 将修改后的内容写入文件
const encodedContent = new TextEncoder().encode(modifiedContent);
await vscode.workspace.fs.writeFile(fileUri, encodedContent);
// 更新工作区变更状态
if (workspaceChanges[filePath]) {
workspaceChanges[filePath].status = 'accepted';
}
// 通知 WebView 更新工作区文件列表
this.provider._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
vscode.window.showInformationMessage('代码变更已应用');
} catch (error: any) {
vscode.window.showErrorMessage(`应用代码变更失败: ${error.message}`);
}
}
private async handleRejectChanges(message: any) {
// 拒绝代码变更
try {
const {filePath} = message;
// 更新工作区变更状态
if (workspaceChanges[filePath]) {
workspaceChanges[filePath].status = 'rejected';
}
// 通知 WebView 更新工作区文件列表
this.provider._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
vscode.window.showInformationMessage('代码变更已拒绝');
} catch (error: any) {
vscode.window.showErrorMessage(`拒绝代码变更失败: ${error.message}`);
}
}
private async handleOpenWorkspaceFile(message: any) {
// 打开工作区文件
try {
const {filePath} = message;
const fileUri = vscode.Uri.file(filePath);
// 检查文件是否已经打开
let fileAlreadyOpen = false;
const openedTextDocuments = vscode.workspace.textDocuments;
for (const doc of openedTextDocuments) {
if (doc.uri.fsPath === fileUri.fsPath) {
fileAlreadyOpen = true;
// 激活已打开的文件
await vscode.window.showTextDocument(doc, {
viewColumn: vscode.ViewColumn.One // 切换到已打开的文件
});
break;
}
}
// 如果文件未打开,则在新窗口打开
if (!fileAlreadyOpen) {
await vscode.window.showTextDocument(fileUri, {
viewColumn: vscode.ViewColumn.One // 在第一列打开文件
});
}
} catch (error: any) {
vscode.window.showErrorMessage(`打开文件失败: ${error.message}`);
}
}
private handleGetCodeChanges() {
// 获取代码变更
this.provider._postMessage({
command: 'codeChanges',
changes: workspaceChanges
});
}
private async handleCreateNewFile(message: any) {
// 创建新文件功能
try {
const {fileName, content, language} = message;
// 获取根工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
vscode.window.showErrorMessage('未找到工作区');
return;
}
// 根据语言确定文件扩展名
const fileExtension = getFileExtension(language);
const fullFileName = fileName.endsWith(fileExtension) ? fileName : `${fileName}${fileExtension}`;
// 创建文件路径(在根目录下)
const filePath = path.join(workspaceFolders[0].uri.fsPath, fullFileName);
const fileUri = vscode.Uri.file(filePath);
// 将内容写入文件
const encodedContent = new TextEncoder().encode(content);
await vscode.workspace.fs.writeFile(fileUri, encodedContent);
// 在编辑器中打开新创建的文件
await vscode.window.showTextDocument(fileUri, {
viewColumn: vscode.ViewColumn.One
});
vscode.window.showInformationMessage(`文件 ${fullFileName} 已创建`);
} catch (error: any) {
vscode.window.showErrorMessage(`生成代码文件失败: ${error.message}`);
}
}
private handleRestoreState(message: any) {
// 恢复面板状态
currentSessionHistory = message.history || [];
// 恢复显示历史消息
currentSessionHistory.forEach(msg => {
this.provider._postMessage({
command: 'addMessage',
role: msg.role,
content: msg.content,
codeDiff: null
});
});
// 恢复工作区变更
if (message.workspaceChanges) {
this.provider._postMessage({
command: 'updateWorkspaceFiles',
files: message.workspaceChanges
});
}
}
// 添加处理取消请求的方法
private async handleCancelRequest() {
console.log('收到取消请求');
if (this.currentReader) {
try {
await this.currentReader.cancel();
this.currentReader = null;
console.log('请求已取消');
} catch (error) {
console.error('取消请求时出错:', error);
}
}
}
}