refactor(extension): 重构 AI聊天扩展

master
钟良源 6 months ago
parent 1651e4caf7
commit 196a5eba2a

@ -3,6 +3,7 @@
src/** src/**
.gitignore .gitignore
.yarnrc .yarnrc
.idea
vsc-extension-quickstart.md vsc-extension-quickstart.md
**/tsconfig.json **/tsconfig.json
**/eslint.config.mjs **/eslint.config.mjs

@ -3,7 +3,6 @@ import * as path from 'path';
import * as diff from 'diff'; import * as diff from 'diff';
// 定义全局变量来存储面板状态 // 定义全局变量来存储面板状态
let panel: vscode.WebviewPanel | undefined;
let currentSessionHistory: { role: string; content: string }[] = []; let currentSessionHistory: { role: string; content: string }[] = [];
// 存储工作区变更的文件,包含状态信息 // 存储工作区变更的文件,包含状态信息
let workspaceChanges: { let workspaceChanges: {
@ -24,9 +23,20 @@ export function activate(context: vscode.ExtensionContext) {
statusBarItem.show(); statusBarItem.show();
// 注册打开 Webview 的命令 // 注册侧边栏视图提供商
const provider = new AISidebarViewProvider(context.extensionPath, context);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
'ai-chat-sidebar-view',
provider,
{ webviewOptions: { retainContextWhenHidden: true } }
)
);
// 注册打开 Webview 的命令(保持向后兼容)
const openWebviewCommand = vscode.commands.registerCommand("ai-chat.openWebview", () => { const openWebviewCommand = vscode.commands.registerCommand("ai-chat.openWebview", () => {
openWebview(context.extensionPath, context.globalState, context); // 聚焦到主侧边栏视图
vscode.commands.executeCommand('workbench.action.focusSideBar');
}); });
// 注册右键菜单命令:添加选中内容到对话 // 注册右键菜单命令:添加选中内容到对话
@ -46,147 +56,43 @@ export function activate(context: vscode.ExtensionContext) {
const selectedText = editor.document.getText(selection); const selectedText = editor.document.getText(selection);
// 检查 panel 是否存在 // 发送选中的代码到侧边栏视图
if (panel && panel.webview) { provider.sendSelectedCode(selectedText);
panel.webview.postMessage({
command: 'addToInput',
role: 'user',
content: selectedText
});
} else {
vscode.window.showWarningMessage("请先打开对话窗口");
}
});
// 注册选择文件命令
const selectFileCommand = vscode.commands.registerCommand('ai-chat.selectFileAsContext', async () => {
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);
// 发送文件内容到 Webview
if (panel && panel.webview) {
panel.webview.postMessage({
command: 'setContextFile',
fileName: selectedFileUri.fsPath,
fileContent: decodedContent
});
}
}
}); });
// 注册获取工作区文件列表的命令 // 添加到 subscriptions
const getFileListCommand = vscode.commands.registerCommand('ai-chat.getFileList', async () => { context.subscriptions.push(statusBarItem, openWebviewCommand, addToChatCommand);
if (panel && panel.webview) {
try {
// 获取工作区根路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
panel.webview.postMessage({
command: 'fileList',
files: [],
error: '未找到工作区'
});
return;
} }
const rootPath = workspaceFolders[0].uri; class AISidebarViewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'ai-chat-sidebar-view';
// 查找所有文件 private _view?: vscode.WebviewView;
const fileUris = await vscode.workspace.findFiles('**/*', '**/node_modules/**'); private _extensionPath: string;
private _context: vscode.ExtensionContext;
// 转换为相对路径和基本信息 constructor(extensionPath: string, context: vscode.ExtensionContext) {
const files = fileUris.map(uri => { this._extensionPath = extensionPath;
const relativePath = vscode.workspace.asRelativePath(uri); this._context = context;
const isDirectory = uri.path.endsWith('/');
return {
path: uri.fsPath,
relativePath: relativePath,
name: path.basename(uri.fsPath),
isDirectory: isDirectory
};
});
panel?.webview.postMessage({
command: 'fileList',
files: files
});
} catch (error: any) {
panel?.webview.postMessage({
command: 'fileList',
files: [],
error: error.message
});
} }
}
});
// 添加到 subscriptions public resolveWebviewView(
context.subscriptions.push(statusBarItem, openWebviewCommand, addToChatCommand,selectFileCommand,getFileListCommand); webviewView: vscode.WebviewView,
} context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken,
function openWebview(
extensionPath: string,
globalState: vscode.Memento,
context: vscode.ExtensionContext // 添加 context 参数
) { ) {
// 如果面板已经存在,直接显示它而不是创建新的 this._view = webviewView;
if (panel) {
panel.reveal(vscode.ViewColumn.Beside);
// 发送当前状态到 WebView
if (panel.webview) {
panel.webview.postMessage({
command: 'restoreState',
history: currentSessionHistory,
workspaceChanges: workspaceChanges
});
}
return;
}
panel = vscode.window.createWebviewPanel( webviewView.webview.options = {
'aiChatWebview',
'AI Chat',
vscode.ViewColumn.Beside,
{
enableScripts: true, enableScripts: true,
localResourceRoots: [vscode.Uri.file(path.join(extensionPath, 'media', 'webview'))] localResourceRoots: [
} vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview'))
); ]
};
const styleUri = panel.webview.asWebviewUri(
vscode.Uri.file(path.join(extensionPath, 'media', 'webview', 'style.css'))
);
const scriptUri = panel.webview.asWebviewUri(
vscode.Uri.file(path.join(extensionPath, 'media', 'webview', 'script.js'))
);
const highlightScriptUri = panel.webview.asWebviewUri(
vscode.Uri.file(path.join(extensionPath, 'media', 'webview', 'highlight.min.js'))
);
const highlightStyleUri = panel.webview.asWebviewUri(
vscode.Uri.file(path.join(extensionPath, 'media', 'webview', 'styles', 'atom-one-dark.css'))
);
const markedScriptUri = panel.webview.asWebviewUri(
vscode.Uri.file(path.join(context.extensionPath, 'media', 'webview', 'marked.min.js'))
);
panel.webview.html = getWebviewContent(styleUri, scriptUri,highlightScriptUri,highlightStyleUri,markedScriptUri); webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
panel.webview.onDidReceiveMessage( webviewView.webview.onDidReceiveMessage(async (message) => {
async (message) => {
switch (message.command) { switch (message.command) {
case 'ask': case 'ask':
const question = message.text; const question = message.text;
@ -195,10 +101,10 @@ function openWebview(
currentSessionHistory = [...history, { role: 'user', content: question }]; currentSessionHistory = [...history, { role: 'user', content: question }];
panel?.webview.postMessage({ command: 'addMessage', role: 'user', content: question }); this._postMessage({ command: 'addMessage', role: 'user', content: question });
try { try {
const response = await callQwenAPI(question, history, fileContent, context); const response = await callQwenAPI(question, history, fileContent, this._context);
console.log("response:", response); console.log("response:", response);
const aiMessage = response.choices[0]?.message?.content || '未获取到回复'; const aiMessage = response.choices[0]?.message?.content || '未获取到回复';
@ -215,7 +121,7 @@ function openWebview(
}; };
// 通知 WebView 更新工作区文件列表 // 通知 WebView 更新工作区文件列表
panel?.webview.postMessage({ this._postMessage({
command: 'updateWorkspaceFiles', command: 'updateWorkspaceFiles',
files: workspaceChanges files: workspaceChanges
}); });
@ -224,21 +130,20 @@ function openWebview(
// 更新当前会话历史 // 更新当前会话历史
currentSessionHistory = [...currentSessionHistory, { role: 'assistant', content: aiMessage }]; currentSessionHistory = [...currentSessionHistory, { role: 'assistant', content: aiMessage }];
panel?.webview.postMessage({ this._postMessage({
command: 'addMessage', command: 'addMessage',
role: 'ai', role: 'ai',
content: aiMessage, content: aiMessage,
codeDiff: codeDiff // 发送代码差异信息 codeDiff: codeDiff // 发送代码差异信息
}); });
} catch (error: any) { } catch (error: any) {
panel?.webview.postMessage({ command: 'addMessage', role: 'ai', content: '调用失败:' + error.message }); this._postMessage({ command: 'addMessage', role: 'ai', content: '调用失败:' + error.message });
} }
panel?.webview.postMessage({ command: 'hideLoading' }); this._postMessage({ command: 'hideLoading' });
break; break;
case 'selectContextFile': case 'selectContextFile':
// 执行文件选择逻辑 // 执行文件选择逻辑
try {
const uris = await vscode.window.showOpenDialog({ const uris = await vscode.window.showOpenDialog({
canSelectFiles: true, canSelectFiles: true,
canSelectFolders: false, canSelectFolders: false,
@ -251,20 +156,15 @@ function openWebview(
const fileContent = await vscode.workspace.fs.readFile(selectedFileUri); const fileContent = await vscode.workspace.fs.readFile(selectedFileUri);
const decodedContent = new TextDecoder("utf-8").decode(fileContent); const decodedContent = new TextDecoder("utf-8").decode(fileContent);
panel?.webview.postMessage({ this._postMessage({
command: 'setContextFile', command: 'setContextFile',
fileName: selectedFileUri.fsPath, fileName: selectedFileUri.fsPath,
fileContent: decodedContent fileContent: decodedContent
}); });
} }
} catch (error: any) {
// 忽略错误
console.error(`文件操作错误: ${error.message}`);
}
break; break;
case 'getFileList': case 'getFileList':
// 执行获取文件列表逻辑 // 执行获取文件列表逻辑
try {
const workspaceFolders = vscode.workspace.workspaceFolders; const workspaceFolders = vscode.workspace.workspaceFolders;
if (workspaceFolders && workspaceFolders.length > 0) { if (workspaceFolders && workspaceFolders.length > 0) {
const fileUris = await vscode.workspace.findFiles('**/*', '**/node_modules/**'); const fileUris = await vscode.workspace.findFiles('**/*', '**/node_modules/**');
@ -279,14 +179,11 @@ function openWebview(
}; };
}); });
panel?.webview.postMessage({ this._postMessage({
command: 'fileList', command: 'fileList',
files: files files: files
}); });
} }
} catch (error: any) {
// 忽略错误
}
break; break;
case 'selectFileByPath': case 'selectFileByPath':
@ -296,7 +193,7 @@ function openWebview(
const fileContent = await vscode.workspace.fs.readFile(fileUri); const fileContent = await vscode.workspace.fs.readFile(fileUri);
const decodedContent = new TextDecoder("utf-8").decode(fileContent); const decodedContent = new TextDecoder("utf-8").decode(fileContent);
panel?.webview.postMessage({ this._postMessage({
command: 'setContextFile', command: 'setContextFile',
fileName: fileUri.fsPath, fileName: fileUri.fsPath,
fileContent: decodedContent fileContent: decodedContent
@ -354,7 +251,7 @@ function openWebview(
} }
// 通知 WebView 更新工作区文件列表 // 通知 WebView 更新工作区文件列表
panel?.webview.postMessage({ this._postMessage({
command: 'updateWorkspaceFiles', command: 'updateWorkspaceFiles',
files: workspaceChanges files: workspaceChanges
}); });
@ -375,7 +272,7 @@ function openWebview(
} }
// 通知 WebView 更新工作区文件列表 // 通知 WebView 更新工作区文件列表
panel?.webview.postMessage({ this._postMessage({
command: 'updateWorkspaceFiles', command: 'updateWorkspaceFiles',
files: workspaceChanges files: workspaceChanges
}); });
@ -418,7 +315,7 @@ function openWebview(
break; break;
case 'getCodeChanges': case 'getCodeChanges':
// 获取代码变更 // 获取代码变更
panel?.webview.postMessage({ this._postMessage({
command: 'codeChanges', command: 'codeChanges',
changes: workspaceChanges changes: workspaceChanges
}); });
@ -429,7 +326,7 @@ function openWebview(
// 恢复显示历史消息 // 恢复显示历史消息
currentSessionHistory.forEach(msg => { currentSessionHistory.forEach(msg => {
panel?.webview.postMessage({ this._postMessage({
command: 'addMessage', command: 'addMessage',
role: msg.role, role: msg.role,
content: msg.content, content: msg.content,
@ -439,23 +336,56 @@ function openWebview(
// 恢复工作区变更 // 恢复工作区变更
if (message.workspaceChanges) { if (message.workspaceChanges) {
panel?.webview.postMessage({ this._postMessage({
command: 'updateWorkspaceFiles', command: 'updateWorkspaceFiles',
files: message.workspaceChanges files: message.workspaceChanges
}); });
} }
break; break;
} }
}, });
undefined, }
context.subscriptions
public sendSelectedCode(code: string) {
if (this._view) {
this._view.show?.(true);
this._postMessage({
command: 'addToInput',
role: 'user',
content: code
});
}
}
private _getHtmlForWebview(webview: vscode.Webview) {
const styleUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'style.css'))
); );
// // 添加 dispose 监听器 const scriptUri = webview.asWebviewUri(
panel.onDidDispose(() => { vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'script.js'))
// 不再清空会话历史,让用户在重新打开面板时保留上下文 );
panel = undefined;
}, null, context.subscriptions); const highlightScriptUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'highlight.min.js'))
);
const highlightStyleUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'styles', 'atom-one-dark.css'))
);
const markedScriptUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'marked.min.js'))
);
return getWebviewContent(styleUri, scriptUri, highlightScriptUri, highlightStyleUri, markedScriptUri);
}
private _postMessage(message: any) {
if (this._view) {
this._view.webview.postMessage(message);
}
}
} }
async function callQwenAPI( async function callQwenAPI(

Loading…
Cancel
Save