DeepCodeGenius-vscode/src/extension/ChatViewProvider.ts

407 lines
13 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';
export class ChatViewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'aiChatView';
private _view?: vscode.WebviewView;
private _logger: (message: string) => void;
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);
this._logger('WebView HTML内容已设置');
// 处理来自Webview的消息
webviewView.webview.onDidReceiveMessage(message => {
this._logger(`收到WebView消息: ${message.type}`);
switch (message.type) {
case 'getSettings':
this._logger(`处理getSettings请求`);
this._sendSettings();
break;
case 'saveSettings':
this._logger(`保存设置: ${JSON.stringify(message.settings, (key, value) =>
key === 'apiKey' ? '***' : value)}`);
this._saveSettings(message.settings);
break;
case 'log':
// 为SettingsModal相关日志添加特殊标记
if (message.message && message.message.includes('SettingsModal')) {
this._logger(`【SettingsModal日志】: ${message.message}`);
}
// 为WebSocket连接相关日志添加更详细的输出
else if (message.message && (
message.message.includes('WebSocket') ||
message.message.includes('ChatStore') ||
message.message.includes('initWebSocketService')
)) {
this._logger(`WebSocket日志: ${message.message}`);
}
// 为重要日志添加特殊标记
else if (message.message && message.message.includes('【重要】')) {
this._logger(`⚠️ ${message.message}`);
}
else {
this._logger(`WebView日志: ${message.message}`);
}
break;
case 'error':
this._logger(`WebView错误: ${message.message}`);
if (message.stack) {
this._logger(`错误堆栈: ${message.stack}`);
}
break;
}
});
// 初始化时发送主题颜色
this._sendThemeColors();
// 监听主题变化
vscode.window.onDidChangeActiveColorTheme(() => {
this._sendThemeColors();
});
}
// 发送主题颜色
private _sendThemeColors() {
if (!this._view) return;
// 获取当前主题颜色
const theme = vscode.window.activeColorTheme;
const isDark = theme.kind === vscode.ColorThemeKind.Dark;
const isLight = theme.kind === vscode.ColorThemeKind.Light;
// 获取常用颜色
const colors = {
// 基础颜色
foreground: this._getColor('foreground'),
background: this._getColor('editor.background'),
// 文本颜色
descriptionForeground: this._getColor('descriptionForeground'),
errorForeground: this._getColor('errorForeground'),
// 按钮颜色
buttonBackground: this._getColor('button.background'),
buttonForeground: this._getColor('button.foreground'),
buttonHoverBackground: this._getColor('button.hoverBackground'),
// 输入框颜色
inputBackground: this._getColor('input.background'),
inputForeground: this._getColor('input.foreground'),
inputPlaceholderForeground: this._getColor('input.placeholderForeground'),
inputBorder: this._getColor('input.border'),
// 链接颜色
linkForeground: this._getColor('textLink.foreground'),
linkActiveForeground: this._getColor('textLink.activeForeground'),
// 其他界面元素
panelBorder: this._getColor('panel.border'),
// 主题类型
isDark,
isLight
};
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'
};
try {
// 尝试从配置中获取颜色
const themeOverride = vscode.workspace.getConfiguration('workbench')
.get<Record<string, string>>('colorCustomizations', {});
// 首先尝试从用户配置的覆盖颜色中获取
if (themeOverride[colorId]) {
return themeOverride[colorId];
}
// 然后从默认颜色中获取
return defaultColors[colorId] || '#cccccc';
} catch (error) {
this._logger(`获取颜色失败 ${colorId}: ${error}`);
// 回退到默认颜色
return defaultColors[colorId] || '#cccccc';
}
}
// 发送设置
private _sendSettings() {
if (!this._view) return;
const config = vscode.workspace.getConfiguration('aiChat');
const settings = {
apiHost: config.get('apiHost', ''),
apiKey: config.get('apiKey', '')
};
this._view.webview.postMessage({
type: 'settings',
settings
});
}
// 保存设置
private _saveSettings(settings: { apiHost: string; apiKey: string }) {
if (!settings) return;
this._logger(`准备保存设置: apiHost=${settings.apiHost}, apiKey=***`);
const config = vscode.workspace.getConfiguration('aiChat');
let hasChanges = false;
// 保存成功标志
let success = true;
let errorMessage = '';
// 检查并更新API主机
const updateHost = async () => {
if (settings.apiHost === config.get('apiHost')) return;
hasChanges = true;
this._logger('更新API主机设置');
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}`);
}
};
// 检查并更新API密钥
const updateKey = async () => {
if (settings.apiKey === config.get('apiKey')) return;
hasChanges = true;
this._logger('更新API密钥设置');
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}`);
}
};
// 执行所有更新操作
const updateAll = async () => {
await updateHost();
await updateKey();
// 通知前端设置已保存
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}`
});
}
}
};
// 启动更新过程
updateAll().catch(error => {
this._logger(`保存设置过程中发生未捕获异常: ${error}`);
// 确保前端收到失败消息
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');
// 生成随机nonce以增强安全性
const nonce = this._getNonce();
// 检查dist目录是否存在
if (fs.existsSync(distPath)) {
try {
// 读取生产环境的index.html
let indexHtml = fs.readFileSync(
path.join(distPath, 'index.html'),
'utf-8'
);
// 添加CSP策略和nonce
if (!indexHtml.includes('content-security-policy')) {
indexHtml = indexHtml.replace(
'<head>',
`<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https: data:; script-src 'nonce-${nonce}';">`
);
}
// 确保有VSCode API脚本
if (!indexHtml.includes('acquireVsCodeApi')) {
indexHtml = indexHtml.replace(
'<head>',
`<head>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
window.vscodeApi = vscode;
</script>`
);
}
// 将资源路径替换为Webview可访问的URI
indexHtml = indexHtml.replace(
/(href|src)="([^"]*)"/g,
(_, attribute, url) => {
// 跳过外部URL或数据URI
if (url.startsWith('http') || url.startsWith('data:')) {
return `${attribute}="${url}"`;
}
// 转换为Webview URI
const resourceUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'webview', 'dist', url)
);
// 为脚本添加nonce
if (attribute === 'src' && url.endsWith('.js')) {
return `${attribute}="${resourceUri}" nonce="${nonce}"`;
}
return `${attribute}="${resourceUri}"`;
}
);
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);
}
}
// 获取回退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">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';">
<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>
`;
}
// 生成随机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;
}
}