DeepCodeGenius-vscode/src/extension/ChatViewProvider.ts

550 lines
18 KiB
TypeScript
Raw Normal View History

2025-04-13 14:22:32 +08:00
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
2025-04-26 13:15:30 +08:00
import * as WS from 'ws';
type ListenerParam = {
type: string,
data: any,
filePath?: string
functionName?: string
mdPath?: string
}
type WebviewViewIns = vscode.WebviewView['webview']
2025-04-13 14:22:32 +08:00
export class ChatViewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'aiChatView';
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
private _view?: vscode.WebviewView;
private _logger: (message: string) => void;
2025-04-26 13:15:30 +08:00
private _wsInstance?: WS | null
2025-04-13 14:22:32 +08:00
constructor(
private readonly _extensionUri: vscode.Uri,
logger?: (message: string) => void
) {
this._logger = logger || ((message: string) => console.log(message));
}
public resolveWebviewView(
webviewView: vscode.WebviewView,
_context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken,
) {
this._view = webviewView;
this._logger(`WebView视图已创建: ${ChatViewProvider.viewType}`);
webviewView.webview.options = {
// 启用JavaScript
enableScripts: true,
localResourceRoots: [
this._extensionUri
]
};
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
2025-04-26 13:15:30 +08:00
const webview = webviewView.webview
// 在生产环境中使用打包后的Webview文件
2025-04-13 14:22:32 +08:00
this._logger('WebView HTML内容已设置');
2025-04-26 13:15:30 +08:00
let wsInstance = null
2025-04-13 14:22:32 +08:00
// 处理来自Webview的消息
webviewView.webview.onDidReceiveMessage(message => {
this._logger(`收到WebView消息: ${message.type}`);
2025-04-26 13:15:30 +08:00
if (message.type?.startsWith('ws:')) {
this.wsConnect(message, webview)
return
}
2025-04-13 14:22:32 +08:00
switch (message.type) {
2025-04-26 13:15:30 +08:00
case 'loadMd':
this.fetchMdFile(message, webview)
break
case 'openFileAndGoToFunction':
this.openFileAndGoToFunction(message)
break
2025-04-13 14:22:32 +08:00
case 'getSettings':
this._logger(`处理getSettings请求`);
this._sendSettings();
break;
case 'saveSettings':
2025-04-26 13:15:30 +08:00
this._logger(`保存设置: ${JSON.stringify(message.settings, (key, value) =>
2025-04-13 14:22:32 +08:00
key === 'apiKey' ? '***' : value)}`);
this._saveSettings(message.settings);
break;
case 'log':
2025-04-14 23:22:12 +08:00
// 根据消息来源分类日志
if (message.message) {
// ChatPanel组件日志处理
if (message.message.includes('[ChatPanel]')) {
this._logger(`📱 ChatPanel日志: ${message.message}`);
}
// 为SettingsModal相关日志添加特殊标记
else if (message.message.includes('SettingsModal')) {
this._logger(`⚙️ SettingsModal日志: ${message.message}`);
}
// 为WebSocket连接相关日志添加更详细的输出
2025-04-26 13:15:30 +08:00
else if (message.message.includes('WebSocket') ||
message.message.includes('ChatStore') ||
2025-04-14 23:22:12 +08:00
message.message.includes('initWebSocketService')
) {
this._logger(`🔌 WebSocket日志: ${message.message}`);
2025-04-26 13:15:30 +08:00
}
2025-04-14 23:22:12 +08:00
// 为重要日志添加特殊标记
else if (message.message.includes('【重要】')) {
this._logger(`⚠️ ${message.message}`);
}
else {
this._logger(`WebView日志: ${message.message}`);
}
} else {
this._logger(`WebView日志: ${JSON.stringify(message)}`);
2025-04-13 14:22:32 +08:00
}
break;
case 'error':
this._logger(`WebView错误: ${message.message}`);
if (message.stack) {
this._logger(`错误堆栈: ${message.stack}`);
}
break;
}
});
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 初始化时发送主题颜色
this._sendThemeColors();
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 监听主题变化
vscode.window.onDidChangeActiveColorTheme(() => {
this._sendThemeColors();
});
}
// 发送主题颜色
private _sendThemeColors() {
if (!this._view) return;
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 获取当前主题颜色
const theme = vscode.window.activeColorTheme;
const isDark = theme.kind === vscode.ColorThemeKind.Dark;
const isLight = theme.kind === vscode.ColorThemeKind.Light;
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 获取常用颜色
const colors = {
// 基础颜色
foreground: this._getColor('foreground'),
background: this._getColor('editor.background'),
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 文本颜色
descriptionForeground: this._getColor('descriptionForeground'),
errorForeground: this._getColor('errorForeground'),
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 按钮颜色
buttonBackground: this._getColor('button.background'),
buttonForeground: this._getColor('button.foreground'),
buttonHoverBackground: this._getColor('button.hoverBackground'),
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 输入框颜色
inputBackground: this._getColor('input.background'),
inputForeground: this._getColor('input.foreground'),
inputPlaceholderForeground: this._getColor('input.placeholderForeground'),
inputBorder: this._getColor('input.border'),
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 链接颜色
linkForeground: this._getColor('textLink.foreground'),
linkActiveForeground: this._getColor('textLink.activeForeground'),
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 其他界面元素
panelBorder: this._getColor('panel.border'),
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 主题类型
isDark,
isLight
};
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
this._view.webview.postMessage({
type: 'themeColors',
colors
});
}
// 获取主题颜色
private _getColor(colorId: string): string | undefined {
// 提供一组默认颜色值
const defaultColors: Record<string, string> = {
'foreground': '#cccccc',
'editor.background': '#1e1e1e',
'descriptionForeground': '#999999',
'errorForeground': '#f48771',
'button.background': '#0e639c',
'button.foreground': '#ffffff',
'button.hoverBackground': '#1177bb',
'input.background': '#3c3c3c',
'input.foreground': '#cccccc',
'input.placeholderForeground': '#a6a6a6',
'input.border': '#3c3c3c',
'textLink.foreground': '#3794ff',
'textLink.activeForeground': '#3794ff',
'panel.border': '#80808059'
};
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
try {
// 尝试从配置中获取颜色
const themeOverride = vscode.workspace.getConfiguration('workbench')
.get<Record<string, string>>('colorCustomizations', {});
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 首先尝试从用户配置的覆盖颜色中获取
if (themeOverride[colorId]) {
return themeOverride[colorId];
}
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 然后从默认颜色中获取
return defaultColors[colorId] || '#cccccc';
} catch (error) {
this._logger(`获取颜色失败 ${colorId}: ${error}`);
// 回退到默认颜色
return defaultColors[colorId] || '#cccccc';
}
}
// 发送设置
private _sendSettings() {
if (!this._view) return;
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
const config = vscode.workspace.getConfiguration('aiChat');
const settings = {
apiHost: config.get('apiHost', ''),
apiKey: config.get('apiKey', '')
};
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
this._view.webview.postMessage({
type: 'settings',
settings
});
}
// 保存设置
private _saveSettings(settings: { apiHost: string; apiKey: string }) {
if (!settings) return;
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
this._logger(`准备保存设置: apiHost=${settings.apiHost}, apiKey=***`);
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
const config = vscode.workspace.getConfiguration('aiChat');
let hasChanges = false;
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 保存成功标志
let success = true;
let errorMessage = '';
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 检查并更新API主机
const updateHost = async () => {
if (settings.apiHost === config.get('apiHost')) return;
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
hasChanges = true;
this._logger('更新API主机设置');
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
try {
await config.update('apiHost', settings.apiHost, vscode.ConfigurationTarget.Global);
this._logger('已更新API主机设置');
} catch (error) {
success = false;
errorMessage = error instanceof Error ? error.message : '更新API主机失败';
this._logger(`更新API主机设置失败: ${errorMessage}`);
}
};
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 检查并更新API密钥
const updateKey = async () => {
if (settings.apiKey === config.get('apiKey')) return;
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
hasChanges = true;
this._logger('更新API密钥设置');
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
try {
await config.update('apiKey', settings.apiKey, vscode.ConfigurationTarget.Global);
this._logger('已更新API密钥设置');
} catch (error) {
success = false;
errorMessage = error instanceof Error ? error.message : '更新API密钥失败';
this._logger(`更新API密钥设置失败: ${errorMessage}`);
}
};
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 执行所有更新操作
const updateAll = async () => {
await updateHost();
await updateKey();
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 通知前端设置已保存
if (this._view) {
if (success) {
this._logger('所有设置保存成功');
this._view.webview.postMessage({
type: 'settingsSaved',
success: true
});
} else {
this._logger(`保存设置失败: ${errorMessage}`);
this._view.webview.postMessage({
type: 'settingsSaved',
success: false,
error: `保存设置失败: ${errorMessage}`
});
}
}
};
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 启动更新过程
updateAll().catch(error => {
this._logger(`保存设置过程中发生未捕获异常: ${error}`);
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 确保前端收到失败消息
if (this._view) {
this._view.webview.postMessage({
type: 'settingsSaved',
success: false,
error: '保存设置过程中发生错误'
});
}
});
}
// 生成Webview HTML内容
private _getHtmlForWebview(webview: vscode.Webview) {
// 在生产环境中使用打包后的Webview文件
const distPath = path.join(this._extensionUri.fsPath, 'webview', 'dist');
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 生成随机nonce以增强安全性
const nonce = this._getNonce();
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 检查dist目录是否存在
if (fs.existsSync(distPath)) {
try {
// 读取生产环境的index.html
let indexHtml = fs.readFileSync(
path.join(distPath, 'index.html'),
'utf-8'
);
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 添加CSP策略和nonce
if (!indexHtml.includes('content-security-policy')) {
indexHtml = indexHtml.replace(
'<head>',
`<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src ws://localhost:8080; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https: data:; script-src 'nonce-${nonce}';">`
2025-04-13 14:22:32 +08:00
);
}
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 确保有VSCode API脚本
if (!indexHtml.includes('acquireVsCodeApi')) {
indexHtml = indexHtml.replace(
'<head>',
`<head>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
window.vscodeApi = vscode;
</script>`
);
}
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 将资源路径替换为Webview可访问的URI
indexHtml = indexHtml.replace(
/(href|src)="([^"]*)"/g,
(_, attribute, url) => {
// 跳过外部URL或数据URI
if (url.startsWith('http') || url.startsWith('data:')) {
return `${attribute}="${url}"`;
}
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 转换为Webview URI
const resourceUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'webview', 'dist', url)
);
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 为脚本添加nonce
if (attribute === 'src' && url.endsWith('.js')) {
return `${attribute}="${resourceUri}" nonce="${nonce}"`;
}
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
return `${attribute}="${resourceUri}"`;
}
);
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
this._logger('WebView HTML内容已生成包含VSCode API初始化脚本');
return indexHtml;
} catch (error) {
this._logger(`生成WebView HTML时出错: ${error}`);
return this._getFallbackHtml(webview, nonce);
}
} else {
return this._getFallbackHtml(webview, nonce);
}
}
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 获取回退HTML内容
private _getFallbackHtml(webview: vscode.Webview, nonce: string) {
// 开发环境下使用占位符HTML
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
2025-04-14 23:22:12 +08:00
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src ws://localhost:8080; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';">
2025-04-13 14:22:32 +08:00
<title>AI Chat</title>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
window.vscodeApi = vscode;
</script>
<style>
body {
background-color: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
font-family: var(--vscode-font-family);
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
h1 {
margin-bottom: 20px;
}
p {
max-width: 600px;
text-align: center;
}
</style>
</head>
<body>
<h1>AI Chat</h1>
<p>
"npm run webview:build" Webview前端使 "npm run dev"
</p>
</body>
</html>
`;
}
2025-04-26 13:15:30 +08:00
2025-04-13 14:22:32 +08:00
// 生成随机nonce
private _getNonce() {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
2025-04-26 13:15:30 +08:00
wsConnect(message: ListenerParam, webview: WebviewViewIns): void | false {
switch (message.type) {
case 'ws:connect':
// 建立 WebSocket 连接
this._wsInstance = new WS('ws://47.117.75.243:8080/ws', {
headers: { 'X-Api-Key': 'simpletest2025_094' }
})
// 连接打开
this._wsInstance.on('open', () => {
webview.postMessage({ type: 'ws:open', data: true })
})
// 转发服务器消息到 Webview
this._wsInstance.on('message', (data) => {
this._logger('ws:message ===== ' + data.toString())
webview.postMessage({
type: 'ws:message',
data: data
})
})
// 错误处理
this._wsInstance.on('error', (err) => {
webview.postMessage({
type: 'ws:error',
data: '连接失败: ' + err.message
})
})
// 连接关闭处理
this._wsInstance.on('close', () => {
webview.postMessage({ type: 'ws:close' })
})
break
case 'ws:send':
// 转发 Webview 的消息到服务器
if (this._wsInstance) {
this._logger('ws:send' + message.data)
this._wsInstance.send(message.data)
}
break
case 'ws:disconnect':
if (this._wsInstance) {
this._wsInstance.close()
this._wsInstance = null
}
break
default:
return false
}
}
findFunction(symbols: vscode.DocumentSymbol[], name: string): vscode.DocumentSymbol | false {
for (const symbol of symbols) {
if (symbol.kind === vscode.SymbolKind.Function && symbol.name === name) {
return symbol;
}
if (symbol.children) {
const found = this.findFunction(symbol.children, name);
if (found) return found;
}
}
return false;
}
async openFileAndGoToFunction(message: ListenerParam): Promise<void | false> {
if (message.type !== 'openFileAndGoToFunction') { return false };
const filePath = message.filePath; // 确保路径正确,可以是绝对路径或相对于工作区
const targetFunc = message.functionName
if (!filePath || !targetFunc){
vscode.window.showWarningMessage(`未提供文件: ${filePath} 或函数: ${targetFunc}`)
return
}
try {
// 打开文件
const uri = vscode.Uri.file(filePath)
const doc = await vscode.workspace.openTextDocument(uri)
const editor = await vscode.window.showTextDocument(doc)
// 获取文档符号
const symbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>(
'vscode.executeDocumentSymbolProvider',
doc.uri
)
if (symbols) {
const funcSymbol = this.findFunction(symbols, targetFunc)
if (funcSymbol) {
// 跳转到目标位置
editor.revealRange(funcSymbol.range, vscode.TextEditorRevealType.InCenter)
editor.selection = new vscode.Selection(funcSymbol.range.start, funcSymbol.range.end)
} else {
vscode.window.showWarningMessage(`未找到函数 ${targetFunc}`)
}
}
} catch (error) {
vscode.window.showErrorMessage(`无法打开文件: ${error}`)
}
}
// 获取本地markdown文件
fetchMdFile(message: ListenerParam, webview: WebviewViewIns): false | void {
if (message.type !== 'loadMd') { return false }
if (message.mdPath) {
const filePath = path.join(this._extensionUri.fsPath, 'webview', 'dist', message.mdPath!.replace('__localWebViewPath/', ''))
try {
const content = fs.readFileSync(filePath, 'utf-8')
webview.postMessage({ type: 'mdContent', data: content })
} catch (error) {
this._logger('获取 md 内容失败' + error)
webview.postMessage({ type: 'mdContent', data: '获取 md 内容失败' })
}
return
}
webview.postMessage({ type: 'mdContent', data: '未正确传入md文件路径' })
}
}