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.

576 lines
24 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 * as vscode from 'vscode';
import * as path from 'path';
import * as diff from 'diff';
// 定义全局变量来存储面板状态
let panel: vscode.WebviewPanel | undefined;
let currentSessionHistory: { role: string; content: string }[] = [];
// 存储工作区变更的文件,包含状态信息
let workspaceChanges: {
[key: string]: {
original: string;
modified: string;
status: 'pending' | 'accepted' | 'rejected'
}
} = {};
export function activate(context: vscode.ExtensionContext) {
console.log('Extension activated');
// 注册辅助侧边栏视图提供商
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", () => {
// 聚焦到辅助侧边栏视图
vscode.commands.executeCommand('workbench.action.focusAuxiliaryBar');
});
// 注册右键菜单命令:添加选中内容到对话
const addToChatCommand = vscode.commands.registerCommand('extension.addToChat', async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showWarningMessage("请先打开一个编辑器");
return;
}
const selection = editor.selection;
if (selection.isEmpty) {
vscode.window.showWarningMessage("请先选中一段代码");
return;
}
const selectedText = editor.document.getText(selection);
// 发送选中的代码到侧边栏视图
provider.sendSelectedCode(selectedText);
});
// 添加到 subscriptions
context.subscriptions.push(openWebviewCommand, addToChatCommand);
}
class AISidebarViewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'ai-chat-sidebar-view';
private _view?: vscode.WebviewView;
private _extensionPath: string;
private _context: vscode.ExtensionContext;
constructor(extensionPath: string, context: vscode.ExtensionContext) {
this._extensionPath = extensionPath;
this._context = context;
}
public resolveWebviewView(
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'))
]
};
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
webviewView.webview.onDidReceiveMessage(async (message) => {
switch (message.command) {
case 'ask':
const question = message.text;
const fileContent = message.fileContent;
const history = currentSessionHistory;
currentSessionHistory = [...history, { role: 'user', content: question }];
this._postMessage({ command: 'addMessage', role: 'user', content: question });
try {
const response = await callQwenAPI(question, history, fileContent, this._context);
console.log("response:", response);
const aiMessage = response.choices[0]?.message?.content || '未获取到回复';
// 检查是否包含代码修改
const codeDiff = generateCodeDiff(fileContent, aiMessage);
console.log("codeDiff:",codeDiff)
// 如果有代码差异,保存到工作区变更中
if (codeDiff && fileContent && message.fileContentPath) {
workspaceChanges[message.fileContentPath] = {
original: fileContent,
modified: codeDiff.modifiedCode,
status: 'pending'
};
// 通知 WebView 更新工作区文件列表
this._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
}
// 更新当前会话历史
currentSessionHistory = [...currentSessionHistory, { role: 'assistant', content: aiMessage }];
this._postMessage({
command: 'addMessage',
role: 'ai',
content: aiMessage,
codeDiff: codeDiff // 发送代码差异信息
});
} catch (error) {
this._postMessage({ command: 'addMessage', role: 'ai', content: '调用失败:' + error.message });
}
this._postMessage({ command: 'hideLoading' });
break;
case 'selectContextFile':
// 执行文件选择逻辑
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._postMessage({
command: 'setContextFile',
fileName: selectedFileUri.fsPath,
fileContent: decodedContent
});
}
break;
case 'getFileList':
// 执行获取文件列表逻辑
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._postMessage({
command: 'fileList',
files: files
});
}
break;
case 'selectFileByPath':
// 根据路径选择文件
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._postMessage({
command: 'setContextFile',
fileName: fileUri.fsPath,
fileContent: decodedContent
});
} catch (error) {
vscode.window.showErrorMessage(`无法读取文件: ${error.message}`);
}
break;
case 'showCodeDiffInEditor':
// 在编辑器中显示代码差异
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) {
vscode.window.showErrorMessage(`显示代码差异失败: ${error.message}`);
}
break;
case 'acceptChanges':
// 接受代码变更
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._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
vscode.window.showInformationMessage('代码变更已应用');
} catch (error) {
vscode.window.showErrorMessage(`应用代码变更失败: ${error.message}`);
}
break;
case 'rejectChanges':
// 拒绝代码变更
try {
const { filePath } = message;
// 更新工作区变更状态
if (workspaceChanges[filePath]) {
workspaceChanges[filePath].status = 'rejected';
}
// 通知 WebView 更新工作区文件列表
this._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
vscode.window.showInformationMessage('代码变更已拒绝');
} catch (error) {
vscode.window.showErrorMessage(`拒绝代码变更失败: ${error.message}`);
}
break;
case 'openWorkspaceFile':
// 打开工作区文件
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) {
vscode.window.showErrorMessage(`打开文件失败: ${error.message}`);
}
break;
case 'getCodeChanges':
// 获取代码变更
this._postMessage({
command: 'codeChanges',
changes: workspaceChanges
});
break;
case 'restoreState':
// 恢复面板状态
currentSessionHistory = message.history || [];
// 恢复显示历史消息
currentSessionHistory.forEach(msg => {
this._postMessage({
command: 'addMessage',
role: msg.role,
content: msg.content,
codeDiff: null
});
});
// 恢复工作区变更
if (message.workspaceChanges) {
this._postMessage({
command: 'updateWorkspaceFiles',
files: message.workspaceChanges
});
}
break;
}
});
}
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'))
);
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(
question: string,
history: any[],
fileContent: string,
context: vscode.ExtensionContext
): Promise<any> {
const apiKey = 'dev-token';
const url = 'https://aicomp.ngsk.tech:7001/api/v1/chat/completions';
const messages = [
{
role: 'system',
content: `你是一个代码助手,请根据当前文件内容和历史对话回答问题。请使用中文回答。`
},
...history,
{
role: 'user',
content: `【当前文件内容】:
\`\`\`${fileContent}\`\`\`
【用户提问】:
${question}`
}
];
console.log("messages:",messages)
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'qwen2.5-local',
messages
})
});
if (!response.ok) {
throw new Error('API 请求失败');
}
return await response.json();
} catch (e) {
console.error(e);
throw e;
}
}
function generateCodeDiff(originalCode: string, aiResponse: string): { added: string[]; removed: string[]; modifiedCode: string } | null {
console.log("模型返回值:",aiResponse)
// 提取代码块中的代码
const codeBlockRegex = /```(?:([a-zA-Z0-9]+))?\s*[\n\r]([\s\S]*?)\s*```/g;
const matches = [...aiResponse.matchAll(codeBlockRegex)];
console.log("正则过滤:",matches)
if (matches.length > 0) {
const modifiedCode = matches[0][2]; // 提取第一个代码块中的代码
// 智能标准化函数
const smartNormalize = (code: string): string => {
return code
.split('\n')
.map(line => line.trimEnd()) // 使用 trimEnd() 移除行尾空白
.filter(line => line !== null && line !== undefined) // 过滤空行
.join('\n')
.trim(); // 整体去除首尾空白
};
const normalizedOriginal = smartNormalize(originalCode);
const normalizedModified = smartNormalize(modifiedCode);
console.log("原始内容:",normalizedOriginal)
console.log("模型内容:",normalizedModified)
// 如果标准化后的内容相同,则没有差异
if (normalizedOriginal === normalizedModified) {
return null;
}
// 使用更精确的 diff 算法
const diffResult = diff.diffLines(normalizedOriginal, normalizedModified, { ignoreWhitespace: true });
const added: string[] = [];
const removed: string[] = [];
diffResult.forEach(part => {
if (part.added) {
added.push(part.value);
} else if (part.removed) {
removed.push(part.value);
}
});
// 返回差异结果,无论是否有差异
return {
added,
removed,
modifiedCode: normalizedModified
};
}
return null;
}
function getWebviewContent(styleUri: vscode.Uri, scriptUri: vscode.Uri,highlightScriptUri:vscode.Uri,highlightStyleUri:vscode.Uri,markedScriptUri:vscode.Uri): string {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AI Chat</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="${styleUri}" />
<link href="${highlightStyleUri}" rel="stylesheet" />
</head>
<body>
<div class="chat-container">
<!--对话区-->
<div id="chat-box"></div>
<!--文件选择-->
<div class="context-container">
<button id="select-context-btn" class="select-context-btn" title="选择上下文文件">+</button>
<div id="context-placeholder" class="context-placeholder">
<span class="placeholder-text">添加上下文</span>
</div>
<div id="context-tab" class="context-tab hidden">
<span id="context-file-name" class="file-name">添加上下文</span>
<button id="close-context-btn" class="close-btn" title="清除上下文">×</button>
</div>
</div>
<!-- 工作区文件列表 -->
<div class="workspace-container" style="display: none;">
<h3>工作区变更</h3>
<div id="workspace-files" class="workspace-files">
<div class="no-changes">暂无文件变更</div>
</div>
</div>
<!-- 文件浏览器模态框 -->
<div id="file-browser-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3>选择上下文文件</h3>
<span id="close-modal" class="close-modal">&times;</span>
</div>
<div class="modal-body">
<div class="file-search">
<input type="text" id="file-search-input" placeholder="搜索文件..." />
</div>
<div id="file-list" class="file-list">
<div class="loading">加载中...</div>
</div>
</div>
</div>
</div>
<!-- 代码差异模态框 -->
<div id="diff-modal" class="modal hidden">
<div class="modal-content diff-modal-content">
<div class="modal-header">
<h3>代码差异对比</h3>
<span id="close-diff-modal" class="close-modal">&times;</span>
</div>
<div class="modal-body">
<div id="diff-content" class="diff-content"></div>
<div class="diff-actions">
<button id="accept-changes-btn" class="diff-action-btn accept">接受变更</button>
<button id="reject-changes-btn" class="diff-action-btn reject">拒绝变更</button>
</div>
</div>
</div>
</div>
<!-- 聊天输入表单 -->
<form id="chat-form">
<input type="text" id="user-input" placeholder="输入你的问题..." required />
<button type="submit">发送</button>
</form>
<div id="loading" class="hidden">AI 正在思考...</div>
</div>
<script src="${scriptUri}"></script>
<script src="${highlightScriptUri}"></script>
<script src="${markedScriptUri}"></script>
</body>
</html>`;
}