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,15 +3,14 @@ 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: {
[key: string]: { [key: string]: {
original: string; original: string;
modified: string; modified: string;
status: 'pending' | 'accepted' | 'rejected' status: 'pending' | 'accepted' | 'rejected'
} }
} = {}; } = {};
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
@ -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
});
}
}
});
// 注册获取工作区文件列表的命令
const getFileListCommand = vscode.commands.registerCommand('ai-chat.getFileList', async () => {
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;
// 查找所有文件
const fileUris = await vscode.workspace.findFiles('**/*', '**/node_modules/**');
// 转换为相对路径和基本信息
const files = fileUris.map(uri => {
const relativePath = vscode.workspace.asRelativePath(uri);
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 // 添加到 subscriptions
context.subscriptions.push(statusBarItem, openWebviewCommand, addToChatCommand,selectFileCommand,getFileListCommand); context.subscriptions.push(statusBarItem, openWebviewCommand, addToChatCommand);
} }
function openWebview( class AISidebarViewProvider implements vscode.WebviewViewProvider {
extensionPath: string, public static readonly viewType = 'ai-chat-sidebar-view';
globalState: vscode.Memento,
context: vscode.ExtensionContext // 添加 context 参数 private _view?: vscode.WebviewView;
) { private _extensionPath: string;
// 如果面板已经存在,直接显示它而不是创建新的 private _context: vscode.ExtensionContext;
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(
'aiChatWebview',
'AI Chat',
vscode.ViewColumn.Beside,
{
enableScripts: true,
localResourceRoots: [vscode.Uri.file(path.join(extensionPath, 'media', 'webview'))]
}
);
const styleUri = panel.webview.asWebviewUri(
vscode.Uri.file(path.join(extensionPath, 'media', 'webview', 'style.css'))
);
const scriptUri = panel.webview.asWebviewUri( constructor(extensionPath: string, context: vscode.ExtensionContext) {
vscode.Uri.file(path.join(extensionPath, 'media', 'webview', 'script.js')) this._extensionPath = extensionPath;
); this._context = context;
}
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( public resolveWebviewView(
vscode.Uri.file(path.join(context.extensionPath, 'media', 'webview', 'marked.min.js')) webviewView: vscode.WebviewView,
); context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken,
) {
this._view = webviewView;
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview'))
]
};
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,68 +130,59 @@ 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, canSelectMany: false,
canSelectMany: false, title: "选择一个文件作为上下文"
title: "选择一个文件作为上下文" });
});
if (uris && uris.length > 0) { if (uris && uris.length > 0) {
const selectedFileUri = uris[0]; const selectedFileUri = uris[0];
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/**');
const files = fileUris.map(uri => {
const files = fileUris.map(uri => { const relativePath = vscode.workspace.asRelativePath(uri);
const relativePath = vscode.workspace.asRelativePath(uri); return {
return { path: uri.fsPath,
path: uri.fsPath, relativePath: relativePath,
relativePath: relativePath, name: path.basename(uri.fsPath),
name: path.basename(uri.fsPath), isDirectory: false // 简化处理,实际可以根据扩展名判断
isDirectory: false // 简化处理,实际可以根据扩展名判断 };
}; });
});
panel?.webview.postMessage({ this._postMessage({
command: 'fileList', command: 'fileList',
files: files files: files
}); });
}
} catch (error: any) {
// 忽略错误
} }
break; break;
@ -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
});
}
}
// // 添加 dispose 监听器 private _getHtmlForWebview(webview: vscode.Webview) {
panel.onDidDispose(() => { const styleUri = webview.asWebviewUri(
// 不再清空会话历史,让用户在重新打开面板时保留上下文 vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'style.css'))
panel = undefined; );
}, null, context.subscriptions);
const scriptUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'script.js'))
);
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