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.

264 lines
9.6 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';
let panel: vscode.WebviewPanel | undefined;
export function activate(context: vscode.ExtensionContext) {
console.log('Extension activated');
const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
statusBarItem.text = "$(comment) Chat";
statusBarItem.tooltip = "Open AI Chat";
statusBarItem.command = "ai-chat.openWebview";
statusBarItem.show();
// 注册打开 Webview 的命令
const openWebviewCommand = vscode.commands.registerCommand("ai-chat.openWebview", () => {
openWebview(context.extensionPath, context.globalState, context);
});
// 注册右键菜单命令:添加选中内容到对话
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);
// 检查 panel 是否存在
if (panel && panel.webview) {
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
context.subscriptions.push(statusBarItem, openWebviewCommand, addToChatCommand,selectFileCommand);
}
function openWebview(
extensionPath: string,
globalState: vscode.Memento,
context: vscode.ExtensionContext // 添加 context 参数
) {
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(
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);
panel.webview.onDidReceiveMessage(
async (message) => {
switch (message.command) {
case 'ask':
const question = message.text;
const fileContent = message.fileContent;
const history = globalState.get<{ role: string; content: string }[]>('chatHistory', []);
const newHistory = [...history, { role: 'user', content: question }];
globalState.update('chatHistory', newHistory);
panel.webview.postMessage({ command: 'addMessage', role: 'user', content: question });
try {
const response = await callQwenAPI(question, history, fileContent, context);
console.log("response:",response)
const aiMessage = response.choices[0]?.message?.content || '未获取到回复';
const updatedHistory = [...newHistory, { role: 'assistant', content: aiMessage }];
globalState.update('chatHistory', updatedHistory);
panel.webview.postMessage({ command: 'addMessage', role: 'ai', content: aiMessage });
} catch (error) {
panel.webview.postMessage({ command: 'addMessage', role: 'ai', content: '调用失败:' + error.message });
}
panel.webview.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);
panel.webview.postMessage({
command: 'setContextFile',
fileName: selectedFileUri.fsPath,
fileContent: decodedContent
});
}
break;
}
},
undefined,
context.subscriptions
);
}
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: `你是一个代码助手,请根据当前文件内容和历史对话回答问题。请使用中文回答。`
},
{
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 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>
<!-- 聊天输入表单 -->
<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>`;
}