Initial commit
This commit is contained in:
commit
bf58260ff0
8
.cursorignore
Normal file
8
.cursorignore
Normal file
@ -0,0 +1,8 @@
|
||||
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||
|
||||
.cursorignore
|
||||
.gitignore
|
||||
.vscode
|
||||
.env
|
||||
.env.local
|
||||
node_modules
|
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
out
|
||||
dist
|
||||
node_modules
|
||||
.vscode-test
|
||||
*.vsix
|
||||
.DS_Store
|
||||
webview/node_modules
|
||||
webview/dist
|
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "运行扩展",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "npm: package"
|
||||
},
|
||||
{
|
||||
"name": "附加到扩展宿主",
|
||||
"type": "extensionHost",
|
||||
"request": "attach",
|
||||
"port": 5870,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
34
.vscode/tasks.json
vendored
Normal file
34
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "compile",
|
||||
"group": "build",
|
||||
"problemMatcher": "$tsc",
|
||||
"label": "npm: compile",
|
||||
"detail": "tsc -p ./"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "package",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"label": "npm: package",
|
||||
"detail": "webpack --mode production --devtool hidden-source-map"
|
||||
}
|
||||
]
|
||||
}
|
12
.vscodeignore
Normal file
12
.vscodeignore
Normal file
@ -0,0 +1,12 @@
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
out/**
|
||||
node_modules/**
|
||||
webview/node_modules/**
|
||||
webview/src/**
|
||||
**/*.map
|
||||
.gitignore
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
**/.eslintrc.json
|
||||
**/*.ts
|
64
README.md
Normal file
64
README.md
Normal file
@ -0,0 +1,64 @@
|
||||
# VSCode AI 聊天插件
|
||||
|
||||
这是一个基于 VSCode 的 AI 聊天助手插件,提供智能对话、代码帮助等功能。
|
||||
|
||||
## 功能特点
|
||||
|
||||
- 集成聊天界面:在 VSCode 中直接与 AI 助手对话
|
||||
- 智能代码补全:获取代码建议和解决方案
|
||||
- 多种视图模式:聊天、示例、流程图、文档等
|
||||
- 现代化UI:基于Vue3、Ant Design Vue构建的美观界面
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Node.js (>= 14.x)
|
||||
- VSCode (>= 1.60.0)
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
# 安装主项目依赖
|
||||
npm install
|
||||
|
||||
# 安装Webview依赖
|
||||
cd webview
|
||||
npm install
|
||||
cd ..
|
||||
```
|
||||
|
||||
### 开发模式
|
||||
|
||||
```bash
|
||||
# 并行开发VSCode扩展和Webview
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 构建插件
|
||||
|
||||
```bash
|
||||
# 构建Webview前端
|
||||
npm run webview:build
|
||||
|
||||
# 打包VSCode扩展
|
||||
npm run package
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- VSCode扩展 API
|
||||
- TypeScript
|
||||
- Vue 3
|
||||
- Vite
|
||||
- Pinia
|
||||
- Ant Design Vue
|
||||
- Ant Design X Vue
|
||||
|
||||
## 参与贡献
|
||||
|
||||
欢迎提交Issue或PR来完善此项目。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
14454
package-lock.json
generated
Normal file
14454
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
88
package.json
Normal file
88
package.json
Normal file
@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "vscode-ai-chat-plugin",
|
||||
"displayName": "AI Chat Plugin",
|
||||
"description": "VSCode AI聊天插件",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"vscode": "^1.60.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onView:aiChatView"
|
||||
],
|
||||
"main": "./dist/extension.js",
|
||||
"contributes": {
|
||||
"viewsContainers": {
|
||||
"activitybar": [
|
||||
{
|
||||
"id": "ai-chat",
|
||||
"title": "AI 聊天",
|
||||
"icon": "$(comment-discussion)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"views": {
|
||||
"ai-chat": [
|
||||
{
|
||||
"id": "aiChatView",
|
||||
"name": "AI 聊天助手",
|
||||
"type": "webview"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "vscode-ai-chat-plugin.openChat",
|
||||
"title": "打开AI聊天"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "AI 聊天",
|
||||
"properties": {
|
||||
"aiChat.apiHost": {
|
||||
"type": "string",
|
||||
"default": "ws://47.117.75.243:8080/ws",
|
||||
"description": "AI服务的WebSocket地址"
|
||||
},
|
||||
"aiChat.apiKey": {
|
||||
"type": "string",
|
||||
"default": "simpletest2025_demo",
|
||||
"description": "AI服务的API密钥,用于身份验证"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run package",
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"pretest": "npm run compile && npm run lint",
|
||||
"lint": "eslint src --ext ts",
|
||||
"test": "node ./out/test/runTest.js",
|
||||
"package": "webpack --mode production --devtool hidden-source-map",
|
||||
"dev": "concurrently \"npm run webview:dev\" \"webpack --mode development --watch\"",
|
||||
"webview:dev": "cd webview && vite",
|
||||
"webview:build": "cd webview && vite build",
|
||||
"start:debug": "npm run webview:build && npm run compile && npm run package"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.60.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.9.0",
|
||||
"@typescript-eslint/parser": "^6.9.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^8.52.0",
|
||||
"generator-code": "^1.10.17",
|
||||
"ts-loader": "^9.5.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vscode-test": "^1.6.1",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"yo": "^4.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.18.1"
|
||||
}
|
||||
}
|
57
src/extension.ts
Normal file
57
src/extension.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { ChatViewProvider } from './extension/ChatViewProvider';
|
||||
|
||||
// 创建一个输出通道用于日志记录
|
||||
let outputChannel: vscode.OutputChannel;
|
||||
|
||||
// 日志记录函数
|
||||
function log(message: string) {
|
||||
if (!outputChannel) {
|
||||
outputChannel = vscode.window.createOutputChannel('AI Chat Plugin');
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
outputChannel.appendLine(`[${timestamp}] ${message}`);
|
||||
}
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
// 创建输出通道
|
||||
outputChannel = vscode.window.createOutputChannel('AI Chat Plugin');
|
||||
outputChannel.show(true);
|
||||
|
||||
log('VSCode AI Chat Plugin 已激活');
|
||||
|
||||
// 注册Webview视图提供程序
|
||||
const chatViewProvider = new ChatViewProvider(context.extensionUri, log);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider(
|
||||
ChatViewProvider.viewType,
|
||||
chatViewProvider
|
||||
)
|
||||
);
|
||||
|
||||
// 注册打开聊天命令
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('vscode-ai-chat-plugin.openChat', () => {
|
||||
log('执行打开聊天命令');
|
||||
vscode.commands.executeCommand('workbench.view.extension.aiChatView');
|
||||
})
|
||||
);
|
||||
|
||||
// 监听配置变更
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('aiChat')) {
|
||||
log('AI聊天配置已更改');
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
log('VSCode AI Chat Plugin 已停用');
|
||||
|
||||
if (outputChannel) {
|
||||
outputChannel.dispose();
|
||||
}
|
||||
}
|
407
src/extension/ChatViewProvider.ts
Normal file
407
src/extension/ChatViewProvider.ts
Normal file
@ -0,0 +1,407 @@
|
||||
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;
|
||||
}
|
||||
}
|
BIN
temp_tasks.json
Normal file
BIN
temp_tasks.json
Normal file
Binary file not shown.
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020"],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedParameters": true,
|
||||
"outDir": "out"
|
||||
},
|
||||
"exclude": ["node_modules", "webview", ".vscode-test"]
|
||||
}
|
49
webpack.config.js
Normal file
49
webpack.config.js
Normal file
@ -0,0 +1,49 @@
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
/**@type {import('webpack').Configuration}*/
|
||||
const config = {
|
||||
target: 'node', // vscode扩展运行在Node.js环境中
|
||||
entry: './src/extension.ts', // 入口文件
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'extension.js',
|
||||
libraryTarget: 'commonjs2',
|
||||
devtoolModuleFilenameTemplate: '../[resource-path]'
|
||||
},
|
||||
devtool: 'source-map',
|
||||
externals: {
|
||||
vscode: 'commonjs vscode' // vscode-module是vscode提供的
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
fallback: {
|
||||
bufferutil: false,
|
||||
'utf-8-validate': false,
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp: /^bufferutil$|^utf-8-validate$/
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
12
webview/index.html
Normal file
12
webview/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AI 聊天助手</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
2640
webview/package-lock.json
generated
Normal file
2640
webview/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
webview/package.json
Normal file
36
webview/package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "webview",
|
||||
"version": "1.0.0",
|
||||
"description": "VSCode AI聊天插件的WebView界面",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"ant-design-x-vue": "^1.0.7",
|
||||
"axios": "^1.6.0",
|
||||
"bufferutil": "^4.0.9",
|
||||
"dompurify": "^3.2.4",
|
||||
"marked": "^15.0.7",
|
||||
"pinia": "^2.1.0",
|
||||
"utf-8-validate": "^6.0.5",
|
||||
"vue": "^3.3.0",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/marked": "^5.0.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"sass": "^1.86.1",
|
||||
"typescript": "^5.2.0",
|
||||
"vite": "^4.5.0",
|
||||
"vue-tsc": "^1.8.27"
|
||||
}
|
||||
}
|
132
webview/src/App.vue
Normal file
132
webview/src/App.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<a-config-provider :theme="themeStore.antdThemeConfig">
|
||||
<div :class="$style.container">
|
||||
<a-layout :class="$style.layout">
|
||||
<a-layout-content :class="$style.content">
|
||||
<div :class="$style.contentHeader">
|
||||
<a-tabs v-model:activeKey="activeKey" :class="$style.tabs">
|
||||
<a-tab-pane key="chat" tab="CHAT"></a-tab-pane>
|
||||
<a-tab-pane key="examples" tab="用例"></a-tab-pane>
|
||||
<a-tab-pane key="flow" tab="流程图"></a-tab-pane>
|
||||
<a-tab-pane key="docs" tab="文档"></a-tab-pane>
|
||||
</a-tabs>
|
||||
<div :class="$style.actions">
|
||||
<a-button type="text" @click="openSettings">
|
||||
<template #icon><setting-outlined /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<component :is="currentComponent" />
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
|
||||
<!-- 设置模态框 -->
|
||||
<SettingsModal v-model:visible="showSettingsModal" />
|
||||
</div>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { SettingOutlined } from '@ant-design/icons-vue';
|
||||
import ChatPanel from './components/ChatPanel.vue';
|
||||
import ExamplesPanel from './components/ExamplesPanel.vue';
|
||||
import FlowPanel from './components/FlowPanel.vue';
|
||||
import DocsPanel from './components/DocsPanel.vue';
|
||||
import SettingsModal from './components/SettingsModal.vue';
|
||||
import { useThemeStore } from './store/themeStore';
|
||||
|
||||
// 当前选中的选项卡
|
||||
const activeKey = ref('chat');
|
||||
|
||||
// 是否显示设置模态框
|
||||
const showSettingsModal = ref(false);
|
||||
|
||||
// 组件映射表
|
||||
const componentMap = {
|
||||
chat: ChatPanel,
|
||||
examples: ExamplesPanel,
|
||||
flow: FlowPanel,
|
||||
docs: DocsPanel
|
||||
};
|
||||
|
||||
// 当前显示的组件
|
||||
const currentComponent = computed(() => {
|
||||
const key = activeKey.value as keyof typeof componentMap;
|
||||
const component = componentMap[key];
|
||||
if (!component) {
|
||||
console.warn(`未找到组件: ${key},使用默认组件 ChatPanel`);
|
||||
return ChatPanel; // 返回默认组件
|
||||
}
|
||||
return component;
|
||||
});
|
||||
|
||||
// 打开设置模态框
|
||||
const openSettings = () => {
|
||||
showSettingsModal.value = true;
|
||||
};
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
onMounted(() => {
|
||||
// 请求主题颜色
|
||||
themeStore.initialize();
|
||||
|
||||
// 监听来自VSCode的消息
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
if (message.type === 'themeColors') {
|
||||
themeStore.setThemeColors(message.colors);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.layout {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contentHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex: 1;
|
||||
:global(.ant-tabs-nav) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
margin-right: 8px;
|
||||
|
||||
button {
|
||||
color: var(--vscode-description-foreground);
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-foreground);
|
||||
background-color: var(--vscode-list-hover-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
9
webview/src/assets/logo.svg
Normal file
9
webview/src/assets/logo.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||
<rect width="32" height="32" rx="4" fill="#1890FF" />
|
||||
<path d="M16 7C11.0294 7 7 11.0294 7 16C7 20.9706 11.0294 25 16 25C20.9706 25 25 20.9706 25 16C25 11.0294 20.9706 7 16 7ZM16 9C19.866 9 23 12.134 23 16C23 19.866 19.866 23 16 23C12.134 23 9 19.866 9 16C9 12.134 12.134 9 16 9Z" fill="white" />
|
||||
<circle cx="16" cy="16" r="4" fill="white" />
|
||||
<path d="M16 4V7" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M16 25V28" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M7 16L4 16" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M28 16L25 16" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
After Width: | Height: | Size: 751 B |
379
webview/src/components/ChatPanel.vue
Normal file
379
webview/src/components/ChatPanel.vue
Normal file
@ -0,0 +1,379 @@
|
||||
<template>
|
||||
<div :class="$style.chatPanel">
|
||||
<Header :class="$style.header" title="AI 聊天" />
|
||||
|
||||
<div :class="$style.conversationContainer">
|
||||
<div v-if="chatState.connectionStatus === 'connecting'" :class="$style.statusMessage">
|
||||
<a-spin /> 正在连接AI服务...
|
||||
</div>
|
||||
<div v-else-if="chatState.connectionStatus === 'disconnected'" :class="$style.statusMessage">
|
||||
<a-alert
|
||||
type="warning"
|
||||
message="未连接到AI服务"
|
||||
description="请先在设置中配置API服务地址和密钥"
|
||||
:class="$style.alertMessage"
|
||||
/>
|
||||
<a-button type="primary" @click="reconnect" :class="$style.reconnectButton">
|
||||
重新连接
|
||||
</a-button>
|
||||
</div>
|
||||
<div v-else-if="chatState.connectionStatus === 'error'" :class="$style.statusMessage">
|
||||
<a-alert
|
||||
type="error"
|
||||
message="连接错误"
|
||||
:description="chatState.errorMessage"
|
||||
:class="$style.alertMessage"
|
||||
/>
|
||||
<a-button type="primary" @click="reconnect" :class="$style.reconnectButton">
|
||||
重新连接
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-x-conversations
|
||||
v-model:messages="chatState.messages"
|
||||
:class="$style.conversations"
|
||||
:loading="chatState.loading"
|
||||
>
|
||||
<template #avatar="{ message }">
|
||||
<a-avatar :class="$style.avatar">
|
||||
<template #icon>
|
||||
<user-outlined v-if="message.role === 'user'" />
|
||||
<robot-outlined v-else />
|
||||
</template>
|
||||
</a-avatar>
|
||||
</template>
|
||||
</a-x-conversations>
|
||||
</div>
|
||||
|
||||
<div :class="$style.footer">
|
||||
<a-x-sender
|
||||
v-model:value="inputValue"
|
||||
:placeholder="chatState.currentModel ? `使用 ${chatState.currentModel} 模型对话` : '请先选择AI模型'"
|
||||
:loading="chatState.loading"
|
||||
:disabled="chatState.loading || chatState.connectionStatus !== 'connected'"
|
||||
send-button-text="发送"
|
||||
:send-button-props="{ type: 'primary', shape: 'round' }"
|
||||
:rows="3"
|
||||
:auto-size="{ minRows: 1, maxRows: 5 }"
|
||||
@send="handleSend"
|
||||
@keydown.enter.ctrl.prevent="handleSend(inputValue)"
|
||||
/>
|
||||
|
||||
<div :class="$style.modelSelector">
|
||||
<a-select
|
||||
v-model:value="chatState.currentModel"
|
||||
:loading="chatState.loadingModels"
|
||||
:disabled="chatState.connectionStatus !== 'connected'"
|
||||
:options="chatState.availableModels.map(model => ({ value: model, label: model }))"
|
||||
placeholder="选择模型"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
style="width: 160px;"
|
||||
@change="selectModel"
|
||||
@dropdownVisibleChange="onDropdownVisibleChange"
|
||||
>
|
||||
<template #prefixIcon>
|
||||
<robot-outlined />
|
||||
</template>
|
||||
<template #notFoundContent>
|
||||
<div style="text-align: center; padding: 8px;">
|
||||
<loading-outlined v-if="chatState.loadingModels" />
|
||||
<span v-else>无可用模型</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import {
|
||||
RobotOutlined,
|
||||
UserOutlined,
|
||||
LoadingOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import Header from './Header.vue';
|
||||
import { useChatStore, ChatMessage } from '../store/chat';
|
||||
|
||||
// 使用聊天状态管理
|
||||
const chatStore = useChatStore();
|
||||
const { chatState } = chatStore;
|
||||
|
||||
// 输入框值
|
||||
const inputValue = ref('');
|
||||
|
||||
// VSCode API
|
||||
const vscode = acquireVsCodeApiSafe();
|
||||
|
||||
// 监听来自VSCode的消息
|
||||
onMounted(() => {
|
||||
console.log('ChatPanel组件已挂载');
|
||||
|
||||
// 初始化聊天状态
|
||||
if (chatState.availableModels.length === 0) {
|
||||
chatState.availableModels = [
|
||||
'qwq:latest',
|
||||
'deepseek-r1:32b',
|
||||
'qwen2.5-coder:7b'
|
||||
];
|
||||
console.log('设置默认模型列表:', chatState.availableModels);
|
||||
}
|
||||
|
||||
// 从VSCode获取配置信息
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'getSettings'
|
||||
});
|
||||
}
|
||||
|
||||
// 监听来自VSCode的消息
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
|
||||
switch (message.type) {
|
||||
case 'settings':
|
||||
if (message.settings.apiHost && message.settings.apiKey) {
|
||||
console.log('收到API配置,初始化WebSocket连接', message.settings);
|
||||
// 初始化WebSocket服务
|
||||
chatStore.initWebSocketService(message.settings.apiHost, message.settings.apiKey);
|
||||
|
||||
// 检查连接状态
|
||||
setTimeout(() => {
|
||||
if (chatState.connectionStatus === 'connecting') {
|
||||
console.log('连接正在进行中...');
|
||||
} else if (chatState.connectionStatus === 'connected') {
|
||||
console.log('WebSocket连接成功');
|
||||
} else {
|
||||
console.error('WebSocket连接失败:', chatState.errorMessage);
|
||||
}
|
||||
}, 2000); // 给2秒钟时间建立连接
|
||||
} else {
|
||||
console.warn('收到的API配置不完整,无法初始化WebSocket连接');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'connectionStatus':
|
||||
// 这个消息现在由WebSocketService自己处理
|
||||
break;
|
||||
|
||||
case 'receiveMessage':
|
||||
// 这个消息现在由WebSocketService自己处理
|
||||
break;
|
||||
|
||||
case 'clearMessages':
|
||||
chatStore.clearMessages();
|
||||
break;
|
||||
|
||||
case 'models':
|
||||
// 模型列表现在由WebSocketService自己获取和管理
|
||||
break;
|
||||
|
||||
case 'modelChanged':
|
||||
if (message.model) {
|
||||
chatStore.setCurrentModel(message.model);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
// 错误现在由WebSocketService自己处理
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 安全地获取vscode API
|
||||
function acquireVsCodeApiSafe() {
|
||||
// 直接使用全局vscodeApi实例,不尝试重新获取
|
||||
if (window.vscodeApi) {
|
||||
return window.vscodeApi;
|
||||
}
|
||||
|
||||
console.warn('VSCode API未初始化');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理发送消息
|
||||
async function handleSend(content: string) {
|
||||
console.log('发送消息触发:', content);
|
||||
|
||||
// 检查连接状态
|
||||
if (chatState.connectionStatus !== 'connected') {
|
||||
console.error('WebSocket未连接,消息无法发送');
|
||||
// 显示一个临时的错误消息
|
||||
chatState.errorMessage = 'WebSocket未连接,请确保连接已建立';
|
||||
chatState.connectionStatus = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果内容为空或者正在加载,不处理
|
||||
if (!content || !content.trim()) {
|
||||
console.log('消息内容为空,不发送');
|
||||
return;
|
||||
}
|
||||
|
||||
if (chatState.loading) {
|
||||
console.log('正在加载中,不发送');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否选择了模型
|
||||
if (!chatState.currentModel) {
|
||||
console.error('未选择AI模型,无法发送消息');
|
||||
if (chatState.availableModels.length > 0) {
|
||||
console.log('自动选择第一个可用模型:', chatState.availableModels[0]);
|
||||
chatState.currentModel = chatState.availableModels[0];
|
||||
} else {
|
||||
console.error('没有可用模型,无法发送消息');
|
||||
chatState.errorMessage = '未选择AI模型,且没有可用模型';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`开始发送消息,使用模型 ${chatState.currentModel}`);
|
||||
|
||||
// 清空输入框
|
||||
inputValue.value = '';
|
||||
|
||||
try {
|
||||
// 使用聊天存储发送消息
|
||||
await chatStore.sendMessage(content);
|
||||
console.log('消息发送成功');
|
||||
} catch (error) {
|
||||
console.error('消息发送失败:', error);
|
||||
// 在聊天消息中显示错误
|
||||
const errorMsg: ChatMessage = {
|
||||
id: `error-${Date.now()}`,
|
||||
role: 'assistant',
|
||||
content: `消息发送失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
createAt: new Date()
|
||||
};
|
||||
chatState.messages.push(errorMsg);
|
||||
chatState.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 选择AI模型
|
||||
function selectModel(model: string) {
|
||||
chatStore.setCurrentModel(model);
|
||||
}
|
||||
|
||||
// 清空对话
|
||||
function clearMessages() {
|
||||
chatStore.clearMessages();
|
||||
}
|
||||
|
||||
// 刷新模型列表
|
||||
function refreshModels() {
|
||||
chatStore.refreshModels();
|
||||
}
|
||||
|
||||
// 下拉框展开时刷新模型列表
|
||||
function onDropdownVisibleChange(open: boolean) {
|
||||
console.log('下拉框可见状态变更:', open);
|
||||
if (open) {
|
||||
refreshModels();
|
||||
}
|
||||
}
|
||||
|
||||
// 手动重连
|
||||
function reconnect() {
|
||||
console.log('手动触发重连');
|
||||
chatStore.reconnect();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
// 定义变量
|
||||
$content-padding: 16px;
|
||||
|
||||
.chatPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.conversationContainer {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.conversations {
|
||||
padding: $content-padding;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background-color: var(--vscode-button-background);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: $content-padding;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
background-color: var(--vscode-editor-background);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modelSelector {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--vscode-editor-background);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modelSelector:before {
|
||||
content: "选择模型:";
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-right: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.inputFooter {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.inputTip {
|
||||
color: var(--vscode-description-foreground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
:global(.anticon) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.statusMessage {
|
||||
padding: $content-padding;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.alertMessage {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.reconnectButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
160
webview/src/components/DocsPanel.vue
Normal file
160
webview/src/components/DocsPanel.vue
Normal file
@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div :class="$style.docsPanel">
|
||||
<Header :class="$style.header" title="使用文档" />
|
||||
<div :class="$style.content">
|
||||
<a-bubble content="欢迎使用AI助手,以下是使用指南" theme="primary" />
|
||||
|
||||
<a-thought-chain
|
||||
:title="'AI助手功能介绍'"
|
||||
:chain="featureChain"
|
||||
:class="$style.thoughtChain"
|
||||
/>
|
||||
|
||||
<a-thought-chain
|
||||
:title="'使用提示'"
|
||||
:chain="tipsChain"
|
||||
:class="$style.thoughtChain"
|
||||
/>
|
||||
|
||||
<div :class="$style.section">
|
||||
<h3>支持的命令</h3>
|
||||
<div :class="$style.commands">
|
||||
<div :class="$style.command">
|
||||
<div :class="$style.commandName">/help</div>
|
||||
<div :class="$style.commandDesc">显示帮助信息</div>
|
||||
</div>
|
||||
<div :class="$style.command">
|
||||
<div :class="$style.commandName">/clear</div>
|
||||
<div :class="$style.commandDesc">清除当前对话</div>
|
||||
</div>
|
||||
<div :class="$style.command">
|
||||
<div :class="$style.commandName">/code</div>
|
||||
<div :class="$style.commandDesc">请求代码示例</div>
|
||||
</div>
|
||||
<div :class="$style.command">
|
||||
<div :class="$style.commandName">/explain</div>
|
||||
<div :class="$style.commandDesc">请求详细解释</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.section">
|
||||
<h3>关于隐私</h3>
|
||||
<p>您的对话内容会被临时存储以便提供连续的对话体验。我们不会永久保存您的个人信息。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Header from './Header.vue';
|
||||
|
||||
// 定义ThoughtNode接口
|
||||
interface ThoughtNode {
|
||||
id: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
// 功能介绍
|
||||
const featureChain = ref<ThoughtNode[]>([
|
||||
{
|
||||
id: '1',
|
||||
content: '代码理解与生成:可以解释、生成和优化代码'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
content: '技术问题解答:回答编程、开发和计算机科学问题'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
content: '学习辅助:提供学习资源和解释复杂概念'
|
||||
}
|
||||
]);
|
||||
|
||||
// 使用提示
|
||||
const tipsChain = ref<ThoughtNode[]>([
|
||||
{
|
||||
id: '1',
|
||||
content: '提供详细的问题描述,包括背景、目标和已尝试的方法'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
content: '使用代码块分享代码,格式为```语言 代码```'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
content: '一次提问一个问题,以获得最准确的回答'
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
// 定义变量
|
||||
$content-padding: 16px;
|
||||
|
||||
.docsPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: $content-padding;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.thoughtChain {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 30px;
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
color: var(--vscode-editor-foreground, #ffffff) !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--vscode-editor-foreground, #cccccc) !important;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.commands {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.command {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--vscode-list-hover-background);
|
||||
}
|
||||
}
|
||||
|
||||
.commandName {
|
||||
font-weight: bold;
|
||||
color: var(--vscode-editor-foreground, #ffffff) !important;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.commandDesc {
|
||||
color: var(--vscode-editor-foreground, #cccccc) !important;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
87
webview/src/components/ExamplesPanel.vue
Normal file
87
webview/src/components/ExamplesPanel.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div :class="$style.examplesPanel">
|
||||
<Header :class="$style.header" title="示例提示" />
|
||||
<div :class="$style.content">
|
||||
<a-prompts
|
||||
v-model:prompts="prompts"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Header from './Header.vue';
|
||||
import { Prompts } from 'ant-design-x-vue';
|
||||
|
||||
// 定义提示接口
|
||||
interface Prompt {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
// 定义示例提示列表
|
||||
const prompts = ref<Prompt[]>([
|
||||
{
|
||||
id: '1',
|
||||
title: '解释代码',
|
||||
content: '请解释以下代码的功能和工作原理:[粘贴你的代码]',
|
||||
tags: ['代码', '解释']
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: '编写功能',
|
||||
content: '请帮我实现一个[功能描述]功能',
|
||||
tags: ['功能', '编码']
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: '代码优化',
|
||||
content: '请帮我优化以下代码:[粘贴你的代码]',
|
||||
tags: ['优化', '性能']
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: '错误排查',
|
||||
content: '我遇到了以下错误,请帮我分析原因并给出解决方案:[粘贴错误信息]',
|
||||
tags: ['错误', '调试']
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: '学习概念',
|
||||
content: '请解释[技术概念]的含义和使用场景',
|
||||
tags: ['学习', '概念']
|
||||
}
|
||||
]);
|
||||
|
||||
// 处理选择提示
|
||||
const emit = defineEmits(['select']);
|
||||
const handleSelect = (prompt: Prompt) => {
|
||||
emit('select', prompt.content);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
// 定义变量
|
||||
$content-padding: 16px;
|
||||
|
||||
.examplesPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: $content-padding;
|
||||
}
|
||||
</style>
|
190
webview/src/components/FlowPanel.vue
Normal file
190
webview/src/components/FlowPanel.vue
Normal file
@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div :class="$style.flowPanel">
|
||||
<Header :class="$style.header" title="对话流程" />
|
||||
<div :class="$style.content">
|
||||
<a-bubble content="欢迎使用AI助手对话流程指引" theme="primary" />
|
||||
|
||||
<div :class="$style.steps">
|
||||
<div :class="$style.step">
|
||||
<div :class="$style.stepNumber">1</div>
|
||||
<div :class="$style.stepContent">
|
||||
<h3>提出明确问题</h3>
|
||||
<p>描述您的需求或问题,越具体越好。可以包括代码、错误信息或者特定的技术问题。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.step">
|
||||
<div :class="$style.stepNumber">2</div>
|
||||
<div :class="$style.stepContent">
|
||||
<h3>等待AI回答</h3>
|
||||
<p>AI助手会分析您的问题并提供回答。可能包括代码示例、解释或解决方案。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.step">
|
||||
<div :class="$style.stepNumber">3</div>
|
||||
<div :class="$style.stepContent">
|
||||
<h3>提出后续问题</h3>
|
||||
<p>如果需要更多信息或者有新的问题,可以继续对话。AI会记住对话上下文。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.step">
|
||||
<div :class="$style.stepNumber">4</div>
|
||||
<div :class="$style.stepContent">
|
||||
<h3>使用示例提示</h3>
|
||||
<p>您可以使用左侧的示例提示,快速开始特定类型的对话。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.tips">
|
||||
<h3>有效对话技巧:</h3>
|
||||
<ul>
|
||||
<li>提供具体的背景信息</li>
|
||||
<li>一次专注于一个问题</li>
|
||||
<li>使用代码块分享代码</li>
|
||||
<li>如果回答不准确,提供更多详细信息</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Header from './Header.vue';
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
// 定义变量
|
||||
$content-padding: 16px;
|
||||
$dark-background: var(--vscode-editor-background);
|
||||
|
||||
.flowPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: $dark-background;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: $content-padding;
|
||||
color: var(--vscode-editor-foreground, #ffffff) !important;
|
||||
}
|
||||
|
||||
.steps {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.stepNumber {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground, #ffffff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 15px;
|
||||
flex-shrink: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stepContent {
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vscode-editor-foreground, #ffffff);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--vscode-editor-foreground, #cccccc);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin-top: 30px;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(24, 144, 255, 0.1);
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: var(--vscode-editor-foreground, #ffffff);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
|
||||
li {
|
||||
margin-bottom: 5px;
|
||||
color: var(--vscode-editor-foreground, #cccccc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flowItem {
|
||||
display: flex;
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.numberCircle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--vscode-focusBorder, var(--vscode-button-background));
|
||||
color: var(--vscode-button-foreground, #ffffff);
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.flowContent {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-editor-foreground, #ffffff);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--vscode-editor-foreground, #cccccc);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
51
webview/src/components/Header.vue
Normal file
51
webview/src/components/Header.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div :class="$style.header">
|
||||
<div :class="$style.title">{{ title }}</div>
|
||||
<div :class="$style.actions" v-if="showInfo">
|
||||
<a-button type="text" :class="$style.button">
|
||||
<template #icon><info-circle-outlined /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { InfoCircleOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
showInfo?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.header {
|
||||
height: $header-height;
|
||||
padding: 0 $content-padding;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.button {
|
||||
color: var(--vscode-description-foreground);
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-foreground);
|
||||
background-color: var(--vscode-list-hover-background);
|
||||
}
|
||||
}
|
||||
</style>
|
137
webview/src/components/InputArea.vue
Normal file
137
webview/src/components/InputArea.vue
Normal file
@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div :class="$style.inputArea">
|
||||
<div :class="$style.textareaContainer">
|
||||
<textarea
|
||||
:class="$style.textarea"
|
||||
placeholder="发送消息给AI助手..."
|
||||
v-model="inputText"
|
||||
@keydown.enter.prevent="handleSend"
|
||||
ref="textareaRef"
|
||||
></textarea>
|
||||
</div>
|
||||
<div :class="$style.actions">
|
||||
<div :class="$style.button">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 3.5C6.067 3.5 4.5 5.067 4.5 7C4.5 8.933 6.067 10.5 8 10.5C9.933 10.5 11.5 8.933 11.5 7C11.5 5.067 9.933 3.5 8 3.5ZM3.5 7C3.5 4.51472 5.51472 2.5 8 2.5C10.4853 2.5 12.5 4.51472 12.5 7C12.5 9.48528 10.4853 11.5 8 11.5C5.51472 11.5 3.5 9.48528 3.5 7Z" fill="currentColor"/>
|
||||
<path d="M7.75 5C7.75 4.72386 7.97386 4.5 8.25 4.5H8.5C8.77614 4.5 9 4.72386 9 5C9 5.27614 8.77614 5.5 8.5 5.5H8.25C7.97386 5.5 7.75 5.27614 7.75 5Z" fill="currentColor"/>
|
||||
<path d="M7.25 7C7.25 6.72386 7.47386 6.5 7.75 6.5H8.25C8.52614 6.5 8.75 6.72386 8.75 7V8.25C8.75 8.52614 8.52614 8.75 8.25 8.75C7.97386 8.75 7.75 8.52614 7.75 8.25V7.5H7.75C7.47386 7.5 7.25 7.27614 7.25 7Z" fill="currentColor"/>
|
||||
<path d="M13.5 10.5C13.5 10.2239 13.7239 10 14 10C14.2761 10 14.5 10.2239 14.5 10.5V12C14.5 13.3807 13.3807 14.5 12 14.5H4C2.61929 14.5 1.5 13.3807 1.5 12V10.5C1.5 10.2239 1.72386 10 2 10C2.27614 10 2.5 10.2239 2.5 10.5V12C2.5 12.8284 3.17157 13.5 4 13.5H12C12.8284 13.5 13.5 12.8284 13.5 12V10.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<button :class="$style.sendButton" @click="handleSend" :disabled="!inputText.trim()">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.6377 7.50843C14.8789 7.62568 15 7.88465 15 8.14286C15 8.40106 14.8789 8.66003 14.6377 8.77729L2.7558 14.7302C2.52264 14.8435 2.24207 14.841 2.01156 14.7239C1.78105 14.6067 1.63933 14.3889 1.64 14.1538L1.65522 1.88393C1.65589 1.64878 1.79856 1.43155 2.02963 1.31517C2.26069 1.19878 2.5415 1.19736 2.7737 1.3115L14.6377 7.50843ZM2.64063 2.48423L2.62752 13.5174L13.4075 8.14286L2.64063 2.48423Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const inputText = ref('');
|
||||
const textareaRef = ref<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'send', value: string): void
|
||||
}>();
|
||||
|
||||
const handleSend = () => {
|
||||
if (inputText.value.trim()) {
|
||||
emit('send', inputText.value);
|
||||
inputText.value = '';
|
||||
// 重置文本框高度
|
||||
if (textareaRef.value) {
|
||||
textareaRef.value.style.height = 'auto';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
@use "sass:color";
|
||||
|
||||
.inputArea {
|
||||
padding: 10px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
background-color: var(--vscode-editor-background);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.textareaContainer {
|
||||
flex: 1;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
background-color: var(--vscode-input-background);
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
resize: none;
|
||||
padding: 10px;
|
||||
color: var(--vscode-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--vscode-description-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vscode-description-foreground);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--vscode-list-hover-background);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.sendButton {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--vscode-button-hover-background);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--vscode-disabled-foreground);
|
||||
color: var(--vscode-description-foreground);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
82
webview/src/components/MessageItem.vue
Normal file
82
webview/src/components/MessageItem.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div :class="[$style.messageItem, message.role === 'user' ? $style.userMessage : '']">
|
||||
<div :class="$style.avatar">
|
||||
<svg v-if="message.role === 'user'" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 8C9.65685 8 11 6.65685 11 5C11 3.34315 9.65685 2 8 2C6.34315 2 5 3.34315 5 5C5 6.65685 6.34315 8 8 8Z" fill="currentColor"/>
|
||||
<path d="M8 9C5.79086 9 4 10.7909 4 13V14H12V13C12 10.7909 10.2091 9 8 9Z" fill="currentColor"/>
|
||||
</svg>
|
||||
<svg v-else width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1ZM8 13.8C4.79847 13.8 2.2 11.2015 2.2 8C2.2 4.79847 4.79847 2.2 8 2.2C11.2015 2.2 13.8 4.79847 13.8 8C13.8 11.2015 11.2015 13.8 8 13.8Z" fill="currentColor"/>
|
||||
<path d="M5.5 6.5C5.5 5.67157 6.17157 5 7 5H9C9.82843 5 10.5 5.67157 10.5 6.5V7.5C10.5 8.32843 9.82843 9 9 9H7C6.17157 9 5.5 8.32843 5.5 7.5V6.5Z" fill="currentColor"/>
|
||||
<path d="M4 10.5C4 10.2239 4.22386 10 4.5 10H11.5C11.7761 10 12 10.2239 12 10.5C12 10.7761 11.7761 11 11.5 11H4.5C4.22386 11 4 10.7761 4 10.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
{{ message.content }}
|
||||
<div :class="$style.actions">
|
||||
<div :class="$style.actionButton">复制</div>
|
||||
<div :class="$style.actionButton">插入代码</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Message {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
message: Message;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.messageItem {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.userMessage {
|
||||
background-color: $user-message-background;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $avatar-background;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: $dark-foreground-secondary;
|
||||
|
||||
&:hover {
|
||||
background-color: $hover-background;
|
||||
}
|
||||
}
|
||||
</style>
|
49
webview/src/components/MessageList.vue
Normal file
49
webview/src/components/MessageList.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div :class="$style.messageList" ref="containerRef">
|
||||
<MessageItem
|
||||
v-for="(message, index) in messages"
|
||||
:key="index"
|
||||
:message="message"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import MessageItem from './MessageItem.vue';
|
||||
|
||||
interface Message {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
messages: Message[];
|
||||
}>();
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
|
||||
// 自动滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.scrollTop = containerRef.value.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
watch(() => props.messages.length, () => {
|
||||
// 消息列表变化时,滚动到底部
|
||||
setTimeout(scrollToBottom, 50); // 使用setTimeout确保DOM已更新
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.messageList {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
656
webview/src/components/SettingsModal.vue
Normal file
656
webview/src/components/SettingsModal.vue
Normal file
@ -0,0 +1,656 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="AI服务配置"
|
||||
:footer="null"
|
||||
:mask-closable="false"
|
||||
@cancel="handleCancel"
|
||||
:width="600"
|
||||
:body-style="{ padding: '24px' }"
|
||||
class="settings-modal"
|
||||
>
|
||||
<div :class="$style.settingsContainer">
|
||||
<div :class="$style.header">
|
||||
<div :class="$style.icon">
|
||||
<setting-outlined />
|
||||
</div>
|
||||
<h2 :class="$style.title">AI 服务设置</h2>
|
||||
</div>
|
||||
|
||||
<div :class="$style.description">
|
||||
配置您的AI服务连接信息,以便使用AI聊天功能。这些设置将保存在VSCode全局配置中。
|
||||
</div>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<a-form :model="formState" layout="vertical">
|
||||
<a-form-item
|
||||
label="服务主机地址"
|
||||
name="apiHost"
|
||||
:class="$style.formItem"
|
||||
>
|
||||
<div :class="$style.inputWrapper">
|
||||
<cloud-server-outlined :class="$style.inputIcon" />
|
||||
<a-input
|
||||
v-model:value="formState.apiHost"
|
||||
placeholder="请输入服务主机地址,例如: ws://47.117.75.243:8080/ws"
|
||||
:class="$style.input"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.helpText">
|
||||
指定AI服务WebSocket连接地址,用于与AI服务通信
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="API Key"
|
||||
name="apiKey"
|
||||
:class="$style.formItem"
|
||||
>
|
||||
<div :class="$style.inputWrapper">
|
||||
<key-outlined :class="$style.inputIcon" />
|
||||
<a-input-password
|
||||
v-model:value="formState.apiKey"
|
||||
placeholder="请输入您的API密钥"
|
||||
:class="$style.input"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.helpText">
|
||||
您的API访问密钥,例如: simpletest2025_xxxx,用于验证身份
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<div :class="$style.statusMessage" v-if="message.show">
|
||||
<a-alert :type="message.type" :message="message.content" />
|
||||
</div>
|
||||
|
||||
<div :class="$style.footer">
|
||||
<a-button @click="handleCancel" :class="$style.cancelButton">
|
||||
取消
|
||||
</a-button>
|
||||
<a-button type="primary" :loading="saving" @click="saveSettings" :class="$style.saveButton">
|
||||
<template #icon><save-outlined /></template>
|
||||
保存配置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue';
|
||||
import {
|
||||
SettingOutlined,
|
||||
SaveOutlined,
|
||||
CloudServerOutlined,
|
||||
KeyOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { useChatStore } from '../store/chat';
|
||||
|
||||
// 获取chatStore实例
|
||||
const chatStore = useChatStore();
|
||||
|
||||
// 调试输出chatStore对象
|
||||
console.log('初始化chatStore:', chatStore);
|
||||
const vscodeLog = window.vscodeApi;
|
||||
if (vscodeLog) {
|
||||
vscodeLog.postMessage({
|
||||
type: 'log',
|
||||
message: `初始化chatStore: ${typeof chatStore}, 有initWebSocketService方法: ${typeof chatStore.initWebSocketService === 'function'}`
|
||||
});
|
||||
// 记录所有可用方法
|
||||
const methods = [];
|
||||
for (const key in chatStore) {
|
||||
if (typeof chatStore[key] === 'function') {
|
||||
methods.push(key);
|
||||
}
|
||||
}
|
||||
vscodeLog.postMessage({
|
||||
type: 'log',
|
||||
message: `chatStore可用方法: ${methods.join(', ')}`
|
||||
});
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:visible']);
|
||||
|
||||
// 表单数据
|
||||
const formState = reactive({
|
||||
apiHost: '',
|
||||
apiKey: ''
|
||||
});
|
||||
|
||||
// 加载状态
|
||||
const saving = ref(false);
|
||||
|
||||
// 状态消息
|
||||
const message = reactive({
|
||||
show: false,
|
||||
type: 'success',
|
||||
content: ''
|
||||
});
|
||||
|
||||
// 关闭模态框
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
|
||||
// 从VSCode加载配置
|
||||
const loadSettings = () => {
|
||||
console.log('执行loadSettings函数');
|
||||
|
||||
const vscode = window.vscodeApi;
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: '执行loadSettings函数'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// 直接使用window.vscodeApi,不通过acquireVsCodeApiSafe
|
||||
if (window.vscodeApi) {
|
||||
console.log('使用window.vscodeApi发送getSettings请求');
|
||||
window.vscodeApi.postMessage({
|
||||
type: 'getSettings'
|
||||
});
|
||||
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: '已发送getSettings请求到VSCode'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 备用方法
|
||||
const api = acquireVsCodeApiSafe();
|
||||
if (!api) {
|
||||
throw new Error('无法连接到VSCode扩展');
|
||||
}
|
||||
|
||||
api.postMessage({
|
||||
type: 'getSettings'
|
||||
});
|
||||
|
||||
console.log('通过acquireVsCodeApiSafe获取API并发送getSettings请求');
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: '通过acquireVsCodeApiSafe获取API并发送getSettings请求'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : '未知错误';
|
||||
console.error('加载配置失败:', errMsg);
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'error',
|
||||
message: `加载配置失败: ${errMsg}`
|
||||
});
|
||||
}
|
||||
showMessage('error', `无法连接到VSCode扩展: ${errMsg}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存配置到VSCode
|
||||
const saveSettings = () => {
|
||||
if (!formState.apiHost || !formState.apiKey) {
|
||||
showMessage('error', '请填写完整的API配置信息');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('开始保存配置...');
|
||||
|
||||
// 更新保存状态
|
||||
saving.value = true;
|
||||
|
||||
// 使用安全的方式获取VSCode API
|
||||
const vscode = acquireVsCodeApiSafe();
|
||||
|
||||
// 创建保存的设置对象
|
||||
const settings = {
|
||||
apiHost: formState.apiHost,
|
||||
apiKey: formState.apiKey
|
||||
};
|
||||
|
||||
// 注意:不在这里初始化WebSocket服务,而是在收到保存成功的响应后再初始化
|
||||
console.log('等待VSCode保存配置后再初始化WebSocket服务');
|
||||
|
||||
// 如果VSCode API可用,发送消息
|
||||
if (vscode) {
|
||||
try {
|
||||
console.log('向VSCode发送保存设置请求');
|
||||
vscode.postMessage({
|
||||
type: 'saveSettings',
|
||||
settings
|
||||
});
|
||||
console.log('已发送保存设置请求到VSCode');
|
||||
|
||||
// 不要立即显示成功消息,等待VSCode的确认
|
||||
} catch (error) {
|
||||
console.error('与VSCode通信失败:', error);
|
||||
showMessage('error', '与VSCode通信失败,配置无法保存到VSCode');
|
||||
saving.value = false;
|
||||
}
|
||||
} else {
|
||||
console.warn('警告:VSCode API不可用,无法保存配置到VSCode存储');
|
||||
showMessage('warning', 'VSCode连接不可用,配置已在本地更新但未保存到VSCode');
|
||||
saving.value = false;
|
||||
|
||||
// 在VSCode API不可用的情况下,直接初始化WebSocket
|
||||
console.log('VSCode API不可用,直接初始化WebSocket服务');
|
||||
chatStore.initWebSocketService(settings.apiHost, settings.apiKey);
|
||||
|
||||
// 3秒后关闭弹窗
|
||||
setTimeout(() => {
|
||||
emit('update:visible', false);
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// 安全地获取vscode API
|
||||
const acquireVsCodeApiSafe = () => {
|
||||
console.log('获取VSCode API(SettingsModal)');
|
||||
|
||||
// 直接使用全局window.vscodeApi变量(由ChatViewProvider设置)
|
||||
if (window.vscodeApi) {
|
||||
console.log('VSCode API可用(SettingsModal)');
|
||||
return window.vscodeApi;
|
||||
}
|
||||
|
||||
// 如果API不可用,记录错误
|
||||
console.error('VSCode API不可用(SettingsModal)- window.vscodeApi未定义');
|
||||
|
||||
// 显示友好的错误消息
|
||||
showMessage('error', '无法连接到VSCode扩展,请重新打开扩展视图');
|
||||
return null;
|
||||
};
|
||||
|
||||
// 显示消息
|
||||
const showMessage = (type: 'success' | 'error' | 'warning', content: string) => {
|
||||
message.type = type;
|
||||
message.content = content;
|
||||
message.show = true;
|
||||
|
||||
// 3秒后自动关闭
|
||||
setTimeout(() => {
|
||||
message.show = false;
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// 监听来自VSCode的消息
|
||||
onMounted(() => {
|
||||
// 直接使用vscode API发送日志到VSCode输出面板
|
||||
const vscode = window.vscodeApi;
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: '【重要】SettingsModal组件已挂载'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('【重要】SettingsModal组件已挂载');
|
||||
|
||||
// 检查VSCode API是否可用
|
||||
const api = acquireVsCodeApiSafe();
|
||||
if (!api) {
|
||||
console.error('组件挂载时VSCode API不可用,页面可能无法正常工作');
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'error',
|
||||
message: '组件挂载时VSCode API不可用,页面可能无法正常工作'
|
||||
});
|
||||
}
|
||||
showMessage('error', '无法连接到VSCode扩展,功能可能受限');
|
||||
} else {
|
||||
console.log('组件挂载时VSCode API可用');
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: '组件挂载时VSCode API可用'
|
||||
});
|
||||
}
|
||||
|
||||
// 直接尝试获取设置,不等待消息
|
||||
try {
|
||||
vscode.postMessage({
|
||||
type: 'getSettings'
|
||||
});
|
||||
console.log('已主动请求获取设置');
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: '已主动请求获取设置'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('请求设置失败:', error);
|
||||
}
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
const data = event.data;
|
||||
|
||||
if (data.type === 'settings') {
|
||||
// 更新表单数据
|
||||
formState.apiHost = data.settings.apiHost || '';
|
||||
formState.apiKey = data.settings.apiKey || '';
|
||||
console.log('已接收并更新设置信息:', data.settings);
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: `已接收并更新设置信息: ${JSON.stringify({ apiHost: data.settings.apiHost, apiKey: '***' })}`
|
||||
});
|
||||
}
|
||||
} else if (data.type === 'settingsSaved') {
|
||||
console.log('收到settingsSaved消息:', data);
|
||||
const vscode = window.vscodeApi;
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: `收到settingsSaved消息: ${JSON.stringify(data)}`
|
||||
});
|
||||
}
|
||||
saving.value = false;
|
||||
|
||||
// 检查success属性存在且为true
|
||||
console.log('检查success属性:', data.success, typeof data.success);
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: `检查success属性: ${data.success}, 类型: ${typeof data.success}`
|
||||
});
|
||||
}
|
||||
|
||||
// 使用严格比较及类型检查
|
||||
if (data.success === true) {
|
||||
console.log('SUCCESS为true,执行保存成功逻辑');
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: 'SUCCESS为true,执行保存成功逻辑'
|
||||
});
|
||||
}
|
||||
|
||||
showMessage('success', '配置已保存成功');
|
||||
|
||||
// 确保在保存成功后连接WebSocket服务 - 使用try/catch包裹以防出错
|
||||
try {
|
||||
console.log('保存成功,强制初始化并连接WebSocket服务', { apiHost: formState.apiHost, apiKey: '***' });
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: `保存成功,准备初始化WebSocket服务: ${formState.apiHost}`
|
||||
});
|
||||
}
|
||||
|
||||
// 检查chatStore是否存在
|
||||
if (!chatStore) {
|
||||
console.error('chatStore对象不存在');
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'error',
|
||||
message: 'chatStore对象不存在,无法初始化WebSocket'
|
||||
});
|
||||
}
|
||||
throw new Error('chatStore对象不存在');
|
||||
}
|
||||
|
||||
// 检查initWebSocketService方法是否存在
|
||||
if (typeof chatStore.initWebSocketService !== 'function') {
|
||||
console.error('chatStore.initWebSocketService不是函数');
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'error',
|
||||
message: 'chatStore.initWebSocketService不是函数'
|
||||
});
|
||||
}
|
||||
throw new Error('chatStore.initWebSocketService不是函数');
|
||||
}
|
||||
|
||||
// 调用WebSocket初始化
|
||||
console.log('调用chatStore.initWebSocketService开始');
|
||||
chatStore.initWebSocketService(formState.apiHost, formState.apiKey);
|
||||
console.log('WebSocket服务初始化调用完成');
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: 'WebSocket服务初始化调用完成'
|
||||
});
|
||||
}
|
||||
|
||||
// 延时手动触发重连
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (chatStore && typeof chatStore.reconnect === 'function') {
|
||||
console.log('额外保障措施:手动触发重连');
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: '额外保障措施:手动触发重连'
|
||||
});
|
||||
}
|
||||
chatStore.reconnect();
|
||||
} else {
|
||||
console.error('chatStore.reconnect不存在或不是函数');
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'error',
|
||||
message: 'chatStore.reconnect不存在或不是函数'
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (reconnectError) {
|
||||
console.error('触发重连失败:', reconnectError);
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'error',
|
||||
message: `触发重连失败: ${reconnectError instanceof Error ? reconnectError.message : String(reconnectError)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('初始化WebSocket服务失败:', error);
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'error',
|
||||
message: `初始化WebSocket服务失败: ${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 保存成功后关闭模态框
|
||||
setTimeout(() => {
|
||||
console.log('关闭设置模态框');
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: '关闭设置模态框'
|
||||
});
|
||||
}
|
||||
emit('update:visible', false);
|
||||
}, 1500);
|
||||
} else {
|
||||
console.error('保存失败:', data);
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: `保存失败: ${JSON.stringify(data)}`
|
||||
});
|
||||
}
|
||||
|
||||
// 显示详细的错误信息
|
||||
const errorMsg = data.error || '保存失败,请重试';
|
||||
showMessage('error', errorMsg);
|
||||
console.error('保存设置失败:', errorMsg);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('设置页面已挂载,消息监听器已设置');
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: '设置页面已挂载,消息监听器已设置'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 当模态框显示时加载配置
|
||||
watch(() => props.visible, (newValue) => {
|
||||
console.log('SettingsModal visible变更:', newValue);
|
||||
|
||||
const vscode = window.vscodeApi;
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: `SettingsModal visible变更: ${newValue}`
|
||||
});
|
||||
}
|
||||
|
||||
if (newValue) {
|
||||
console.log('模态框显示,准备加载配置');
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'log',
|
||||
message: '模态框显示,准备加载配置'
|
||||
});
|
||||
}
|
||||
loadSettings();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.settingsContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
color: var(--vscode-button-background);
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--vscode-description-foreground);
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.formItem {
|
||||
margin-bottom: 24px;
|
||||
|
||||
:global(.ant-form-item-label) {
|
||||
padding-bottom: 8px;
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inputIcon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
color: var(--vscode-description-foreground);
|
||||
font-size: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.helpText {
|
||||
margin-top: 6px;
|
||||
color: var(--vscode-description-foreground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.statusMessage {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.cancelButton, .saveButton {
|
||||
min-width: 100px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
background-color: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.settings-modal .ant-modal-content {
|
||||
background-color: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||
0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.settings-modal .ant-modal-header {
|
||||
background-color: var(--vscode-editor-background);
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.settings-modal .ant-modal-title {
|
||||
color: var(--vscode-foreground);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-modal .ant-modal-close {
|
||||
color: var(--vscode-description-foreground);
|
||||
}
|
||||
|
||||
.settings-modal .ant-modal-close:hover {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.settings-modal .ant-form-item-label > label {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
</style>
|
111
webview/src/main.ts
Normal file
111
webview/src/main.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import router from './router';
|
||||
import Antd from 'ant-design-vue';
|
||||
import { Bubble, Sender, Conversations, ThoughtChain, Prompts } from 'ant-design-x-vue';
|
||||
import 'ant-design-vue/dist/reset.css';
|
||||
import './styles/global.scss';
|
||||
import './styles/antdx-override.scss';
|
||||
|
||||
// 设置全局VSCode API
|
||||
declare global {
|
||||
interface Window {
|
||||
vscodeApi: any;
|
||||
__VSCODE_API_INITIALIZED__: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
// VSCode API初始化 - 必须在全局范围获取一次
|
||||
console.log('main.ts: 初始化VSCode API');
|
||||
let vscodeGlobal = window.vscodeApi; // 直接使用window.vscodeApi,它已经由ChatViewProvider初始化
|
||||
|
||||
// 记录API状态
|
||||
if (vscodeGlobal) {
|
||||
console.log('main.ts: VSCode API已由扩展初始化,直接使用');
|
||||
window.__VSCODE_API_INITIALIZED__ = true;
|
||||
} else {
|
||||
console.warn('main.ts: VSCode API未在window中找到');
|
||||
window.__VSCODE_API_INITIALIZED__ = false;
|
||||
|
||||
// 处理情况:如果在dev环境中,提供一个虚拟的VSCode API
|
||||
// 警告:这只用于开发,不应该在生产环境使用
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('main.ts: 创建开发环境下的模拟VSCode API');
|
||||
window.vscodeApi = {
|
||||
postMessage: (msg: any) => {
|
||||
console.log('DEV模式: 模拟VSCode消息', msg);
|
||||
|
||||
// 模拟VSCode响应
|
||||
if (msg.type === 'getSettings') {
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new MessageEvent('message', {
|
||||
data: {
|
||||
type: 'settings',
|
||||
settings: {
|
||||
apiHost: 'ws://localhost:8080',
|
||||
apiKey: 'dev_api_key'
|
||||
}
|
||||
}
|
||||
}));
|
||||
}, 100);
|
||||
} else if (msg.type === 'saveSettings') {
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new MessageEvent('message', {
|
||||
data: {
|
||||
type: 'settingsSaved',
|
||||
success: true
|
||||
}
|
||||
}));
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
console.log('main.ts: 已创建模拟VSCode API');
|
||||
}
|
||||
}
|
||||
|
||||
// 每5秒检查一次VSCode API状态(调试用)
|
||||
setInterval(() => {
|
||||
console.log('VSCode API状态:', window.vscodeApi ? '可用' : '不可用');
|
||||
}, 5000);
|
||||
|
||||
// 创建Vue应用
|
||||
const app = createApp(App);
|
||||
|
||||
// 添加favicon以防止404错误
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'icon';
|
||||
link.href = 'data:,'; // 空数据URL,不加载任何图标
|
||||
document.head.appendChild(link);
|
||||
|
||||
// 注册Pinia状态管理
|
||||
app.use(createPinia());
|
||||
|
||||
// 注册Ant Design Vue
|
||||
app.use(Antd);
|
||||
|
||||
// 注册Ant Design X Vue组件
|
||||
app.component('a-x-bubble', Bubble);
|
||||
app.component('a-x-sender', Sender);
|
||||
app.component('a-x-conversations', Conversations);
|
||||
app.component('a-x-thought-chain', ThoughtChain);
|
||||
app.component('a-x-prompts', Prompts);
|
||||
|
||||
// 注册路由
|
||||
app.use(router);
|
||||
|
||||
// 错误处理
|
||||
app.config.errorHandler = (err, instance, info) => {
|
||||
console.error('Vue Error:', err, info);
|
||||
if (window.vscodeApi) {
|
||||
window.vscodeApi.postMessage({
|
||||
type: 'error',
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
info: info
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app');
|
33
webview/src/router/index.ts
Normal file
33
webview/src/router/index.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import ChatPanel from '../components/ChatPanel.vue';
|
||||
import DocsPanel from '../components/DocsPanel.vue';
|
||||
import ExamplesPanel from '../components/ExamplesPanel.vue';
|
||||
import FlowPanel from '../components/FlowPanel.vue';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'chat',
|
||||
component: ChatPanel
|
||||
},
|
||||
{
|
||||
path: '/docs',
|
||||
name: 'docs',
|
||||
component: DocsPanel
|
||||
},
|
||||
{
|
||||
path: '/examples',
|
||||
name: 'examples',
|
||||
component: ExamplesPanel
|
||||
},
|
||||
{
|
||||
path: '/flow',
|
||||
name: 'flow',
|
||||
component: FlowPanel
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default router;
|
917
webview/src/store/chat.ts
Normal file
917
webview/src/store/chat.ts
Normal file
@ -0,0 +1,917 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, reactive } from 'vue';
|
||||
|
||||
// 定义消息类型
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
content: string;
|
||||
role: 'user' | 'assistant';
|
||||
createAt: Date;
|
||||
}
|
||||
|
||||
// WebSocket事件类型
|
||||
export enum WebSocketEvent {
|
||||
CONNECTED = 'connected',
|
||||
DISCONNECTED = 'disconnected',
|
||||
MESSAGE = 'message',
|
||||
ERROR = 'error',
|
||||
MODEL_LIST = 'model_list'
|
||||
}
|
||||
|
||||
// 定义VSCode API接口
|
||||
let vscode: any = undefined;
|
||||
|
||||
try {
|
||||
vscode = acquireVsCodeApi();
|
||||
} catch (error) {
|
||||
// 在开发环境下,可能没有VSCode API
|
||||
console.warn('VSCode API不可用,可能是在浏览器开发环境中运行');
|
||||
}
|
||||
|
||||
// WebSocket服务类 - 浏览器环境下实现
|
||||
export class WebSocketService {
|
||||
private socket: WebSocket | null = null;
|
||||
private requestMap = new Map<number, (response: any) => void>();
|
||||
private nextRequestId = 1;
|
||||
private reconnectAttempts = 0;
|
||||
private reconnectTimer: number | null = null;
|
||||
private readonly MAX_RECONNECT_ATTEMPTS = 5;
|
||||
private readonly RECONNECT_DELAY = 2000;
|
||||
private isReconnecting = false;
|
||||
private pingInterval: number | null = null;
|
||||
|
||||
public isConnected = false;
|
||||
private eventListeners: Map<string, Array<(data?: any) => void>> = new Map();
|
||||
|
||||
constructor(private apiUrl: string, private apiKey: string, private logger: (message: string) => void = console.log) {}
|
||||
|
||||
/**
|
||||
* 连接到WebSocket服务器
|
||||
*/
|
||||
public connect(): void {
|
||||
if (this.socket?.readyState === WebSocket.OPEN) {
|
||||
this.logger('[WebSocketService] 已连接');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.logger(`[WebSocketService] 尝试连接到: ${this.apiUrl}`);
|
||||
|
||||
// 检查URL格式
|
||||
let wsUrl = this.apiUrl;
|
||||
|
||||
// 确保URL以/ws结尾,但避免重复
|
||||
if (!wsUrl.endsWith('/ws')) {
|
||||
if (wsUrl.endsWith('/')) {
|
||||
wsUrl += 'ws';
|
||||
} else {
|
||||
wsUrl += '/ws';
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有API密钥,如果有则添加为查询参数
|
||||
if (this.apiKey) {
|
||||
const separator = wsUrl.includes('?') ? '&' : '?';
|
||||
wsUrl += `${separator}apiKey=${encodeURIComponent(this.apiKey)}`;
|
||||
}
|
||||
|
||||
// 检查环境和协议
|
||||
const isVsCodeWebview = typeof window !== 'undefined' && 'acquireVsCodeApi' in window;
|
||||
const isLocalhost = wsUrl.includes('localhost') || wsUrl.includes('127.0.0.1');
|
||||
|
||||
// VS Code WebView环境下可能需要wss
|
||||
if (isVsCodeWebview && wsUrl.startsWith('ws://')) {
|
||||
this.logger('[WebSocketService] 在VS Code WebView中,尝试使用安全WebSocket连接');
|
||||
wsUrl = wsUrl.replace('ws://', 'wss://');
|
||||
}
|
||||
|
||||
this.logger(`[WebSocketService] 最终WebSocket URL: ${wsUrl}`);
|
||||
this.socket = new WebSocket(wsUrl);
|
||||
this.setupEventHandlers();
|
||||
|
||||
// 处理连接失败的情况
|
||||
if (isLocalhost) {
|
||||
// 如果使用localhost失败,尝试使用127.0.0.1
|
||||
const fallbackTimeout = setTimeout(() => {
|
||||
if (this.socket?.readyState !== WebSocket.OPEN) {
|
||||
this.logger('[WebSocketService] localhost连接超时,尝试使用127.0.0.1');
|
||||
|
||||
let fallbackUrl;
|
||||
if (wsUrl.includes('localhost')) {
|
||||
fallbackUrl = wsUrl.replace('localhost', '127.0.0.1');
|
||||
} else if (wsUrl.includes('127.0.0.1')) {
|
||||
fallbackUrl = wsUrl.replace('127.0.0.1', 'localhost');
|
||||
}
|
||||
|
||||
if (fallbackUrl && fallbackUrl !== wsUrl) {
|
||||
this.logger(`[WebSocketService] 尝试备用连接: ${fallbackUrl}`);
|
||||
|
||||
// 关闭旧连接
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
}
|
||||
|
||||
// 尝试新连接
|
||||
this.socket = new WebSocket(fallbackUrl);
|
||||
this.setupEventHandlers();
|
||||
}
|
||||
}
|
||||
clearTimeout(fallbackTimeout);
|
||||
}, 3000); // 3秒后如果未连接则尝试备用地址
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger(`[WebSocketService] 连接错误: ${error instanceof Error ? error.message : String(error)}`);
|
||||
this.emit(WebSocketEvent.ERROR, `连接错误: ${error instanceof Error ? error.message : String(error)}`);
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开WebSocket连接
|
||||
*/
|
||||
public disconnect(): void {
|
||||
this.clearTimers();
|
||||
|
||||
if (this.socket) {
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch (err) {
|
||||
this.logger(`关闭WebSocket时出错: ${err}`);
|
||||
}
|
||||
this.socket = null;
|
||||
}
|
||||
|
||||
this.isConnected = false;
|
||||
this.emit(WebSocketEvent.DISCONNECTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听事件
|
||||
*/
|
||||
public on(event: WebSocketEvent, callback: (data?: any) => void): void {
|
||||
if (!this.eventListeners.has(event)) {
|
||||
this.eventListeners.set(event, []);
|
||||
}
|
||||
|
||||
this.eventListeners.get(event)?.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听
|
||||
*/
|
||||
public off(event: WebSocketEvent, callback: (data?: any) => void): void {
|
||||
const listeners = this.eventListeners.get(event);
|
||||
if (listeners) {
|
||||
const index = listeners.indexOf(callback);
|
||||
if (index !== -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发事件
|
||||
*/
|
||||
private emit(event: WebSocketEvent, data?: any): void {
|
||||
const listeners = this.eventListeners.get(event);
|
||||
if (listeners) {
|
||||
listeners.forEach(callback => callback(data));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置WebSocket事件处理器
|
||||
*/
|
||||
private setupEventHandlers(): void {
|
||||
if (!this.socket) {
|
||||
this.logger('[WebSocketService] 无法设置事件处理器: socket为null');
|
||||
return;
|
||||
}
|
||||
|
||||
// 记录连接建立时间
|
||||
let connectionEstablishedTime = 0;
|
||||
|
||||
// 使用浏览器风格的事件处理
|
||||
this.socket.onopen = () => {
|
||||
this.logger('[WebSocketService] WebSocket连接已建立成功');
|
||||
this.isConnected = true;
|
||||
connectionEstablishedTime = Date.now();
|
||||
|
||||
// 延迟重置重连计数器,确保连接稳定
|
||||
setTimeout(() => {
|
||||
if (this.isConnected) {
|
||||
this.reconnectAttempts = 0;
|
||||
this.logger('[WebSocketService] 连接保持稳定,重置重连计数器');
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// 设置定时ping保持连接
|
||||
this.setupPingInterval();
|
||||
|
||||
this.emit(WebSocketEvent.CONNECTED);
|
||||
this.logger('[WebSocketService] 已触发CONNECTED事件');
|
||||
|
||||
// 连接后立即获取模型列表
|
||||
this.logger('[WebSocketService] 开始获取初始模型列表');
|
||||
this.getModelList()
|
||||
.then(models => {
|
||||
this.logger(`[WebSocketService] 获取初始模型列表成功: ${JSON.stringify(models)}`);
|
||||
this.emit(WebSocketEvent.MODEL_LIST, models);
|
||||
})
|
||||
.catch(error => {
|
||||
this.logger(`[WebSocketService] 获取初始模型列表失败: ${error}`);
|
||||
});
|
||||
};
|
||||
|
||||
this.socket.onmessage = (event: MessageEvent) => {
|
||||
try {
|
||||
const data = event.data;
|
||||
// 检查是否是HTTP错误响应
|
||||
const stringData = typeof data === 'string' ? data : data.toString();
|
||||
|
||||
// 尝试解析JSON
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(stringData);
|
||||
} catch (jsonError) {
|
||||
this.logger(`无法解析JSON响应: ${jsonError}, 原始消息: ${stringData.substring(0, 100)}`);
|
||||
this.emit(WebSocketEvent.ERROR, `服务器返回的不是有效JSON: ${stringData.substring(0, 50)}...`);
|
||||
// 自动发送默认模型列表,避免前端卡住
|
||||
this.emit(WebSocketEvent.MODEL_LIST, this.getDefaultModels());
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger(`收到WebSocket消息: ${JSON.stringify(response)}`);
|
||||
|
||||
// 处理模型列表响应(特殊处理)
|
||||
if (response.models && Array.isArray(response.models)) {
|
||||
// 查找对应的模型列表请求
|
||||
let modelRequestId = -1;
|
||||
for (const [reqId, _] of this.requestMap.entries()) {
|
||||
if (reqId < 100) { // 假设小ID是模型列表请求
|
||||
modelRequestId = reqId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (modelRequestId !== -1) {
|
||||
const callback = this.requestMap.get(modelRequestId);
|
||||
if (callback) {
|
||||
callback(response);
|
||||
this.requestMap.delete(modelRequestId);
|
||||
}
|
||||
}
|
||||
|
||||
// 无论找到对应请求与否,都通知模型列表更新
|
||||
this.emit(WebSocketEvent.MODEL_LIST, response.models);
|
||||
}
|
||||
// 处理常规请求响应
|
||||
else if (response.request_id && this.requestMap.has(response.request_id)) {
|
||||
const callback = this.requestMap.get(response.request_id);
|
||||
if (callback) {
|
||||
callback(response);
|
||||
|
||||
// 如果不是流式响应或是最后一个包,清除请求
|
||||
if (!response.stream_seq_id || response.stream_finsh) {
|
||||
this.requestMap.delete(response.request_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger(`解析WebSocket消息失败: ${error}, 原始消息: ${typeof event.data === 'string' ? event.data.substring(0, 100) : '非文本数据'}`);
|
||||
// 自动发送默认模型列表,避免前端卡住
|
||||
this.emit(WebSocketEvent.MODEL_LIST, this.getDefaultModels());
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onerror = (event: Event) => {
|
||||
const errorDetails = JSON.stringify(event);
|
||||
this.logger(`[WebSocketService] WebSocket错误事件: ${errorDetails}`);
|
||||
|
||||
// 检查是否包含isTrusted属性
|
||||
if (event && 'isTrusted' in event && event.isTrusted) {
|
||||
this.logger('[WebSocketService] 这是一个受信任的错误事件,可能是证书或安全设置导致的');
|
||||
|
||||
// 获取更多诊断信息
|
||||
let diagInfo = '';
|
||||
|
||||
// 检查是否是localhost
|
||||
if (this.apiUrl.includes('localhost') || this.apiUrl.includes('127.0.0.1')) {
|
||||
diagInfo += '本地连接(localhost)可能需要特殊权限; ';
|
||||
|
||||
// 建议使用127.0.0.1而不是localhost
|
||||
if (this.apiUrl.includes('localhost')) {
|
||||
diagInfo += '建议尝试使用127.0.0.1替代localhost; ';
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否使用了wss安全连接
|
||||
if (this.apiUrl.startsWith('wss://')) {
|
||||
diagInfo += '使用了安全WebSocket连接,请确认服务器证书有效; ';
|
||||
} else {
|
||||
// 如果使用ws不安全连接,可能是混合内容问题
|
||||
diagInfo += 'VS Code中使用不安全WebSocket(ws://)可能受到限制,建议使用安全连接(wss://); ';
|
||||
}
|
||||
|
||||
// 检查WebView上下文
|
||||
if (typeof window !== 'undefined' && 'acquireVsCodeApi' in window) {
|
||||
diagInfo += 'WebView环境中可能有额外的安全限制; ';
|
||||
}
|
||||
|
||||
const errorMessage = `连接错误(isTrusted): ${diagInfo}请检查服务器配置和网络连接`;
|
||||
this.emit(WebSocketEvent.ERROR, errorMessage);
|
||||
} else {
|
||||
this.emit(WebSocketEvent.ERROR, `连接错误: ${errorDetails}`);
|
||||
}
|
||||
|
||||
// 重连
|
||||
if (this.socket?.readyState !== WebSocket.OPEN) {
|
||||
this.logger('[WebSocketService] Socket未处于OPEN状态,安排重连');
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onclose = (event: CloseEvent) => {
|
||||
this.logger(`[WebSocketService] WebSocket连接关闭: 代码=${event.code} (${this.getCloseEventReason(event.code)}), 原因=${event.reason || '未提供'}`);
|
||||
this.isConnected = false;
|
||||
|
||||
// 计算连接持续时间
|
||||
if (connectionEstablishedTime > 0) {
|
||||
const duration = Date.now() - connectionEstablishedTime;
|
||||
this.logger(`[WebSocketService] 连接持续时间: ${duration}ms`);
|
||||
}
|
||||
|
||||
this.clearTimers();
|
||||
this.emit(WebSocketEvent.DISCONNECTED);
|
||||
this.logger('[WebSocketService] 已触发DISCONNECTED事件');
|
||||
|
||||
// 如果不是正常关闭,尝试重连
|
||||
if (event.code !== 1000) {
|
||||
this.logger('[WebSocketService] 非正常关闭,安排重连');
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
this.logger('[WebSocketService] 所有WebSocket事件处理器已设置完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型列表
|
||||
*/
|
||||
public async getModelList(): Promise<string[]> {
|
||||
try {
|
||||
const requestId = this.nextRequestId++;
|
||||
|
||||
// 按照API文档使用list_model命令
|
||||
const request = {
|
||||
request_id: requestId,
|
||||
cmd: 'list_model' // 使用文档中指定的命令
|
||||
};
|
||||
|
||||
this.logger(`请求模型列表 (ID: ${requestId})`);
|
||||
|
||||
return new Promise<string[]>((resolve) => {
|
||||
// 设置5秒超时,超时后返回默认模型
|
||||
const timeout = setTimeout(() => {
|
||||
if (this.requestMap.has(requestId)) {
|
||||
this.logger(`模型列表请求超时 (ID: ${requestId})`);
|
||||
this.requestMap.delete(requestId);
|
||||
resolve(this.getDefaultModels());
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// 设置响应处理器
|
||||
this.requestMap.set(requestId, (response) => {
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (response.error) {
|
||||
this.logger(`获取模型列表错误: ${response.error}`);
|
||||
resolve(this.getDefaultModels());
|
||||
} else {
|
||||
// 根据文档,应该使用models字段
|
||||
const models = response.models || [];
|
||||
this.logger(`获取到模型列表: ${JSON.stringify(models)}`);
|
||||
|
||||
if (models.length === 0) {
|
||||
resolve(this.getDefaultModels());
|
||||
} else {
|
||||
resolve(models);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 发送请求
|
||||
this.sendRequest(request);
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger(`获取模型列表异常: ${error}`);
|
||||
return this.getDefaultModels();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送聊天消息
|
||||
*/
|
||||
public async sendChatMessage(message: string, model: string): Promise<string> {
|
||||
if (!this.isConnected) {
|
||||
this.logger('无法发送消息: WebSocket未连接');
|
||||
throw new Error('WebSocket未连接');
|
||||
}
|
||||
|
||||
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
||||
this.logger(`WebSocket状态异常: ${this.socket ? this.getReadyStateDescription(this.socket.readyState) : 'socket为null'}`);
|
||||
throw new Error(`WebSocket未准备好: ${this.socket ? this.getReadyStateDescription(this.socket.readyState) : 'socket为null'}`);
|
||||
}
|
||||
|
||||
const requestId = this.nextRequestId++;
|
||||
|
||||
// 按照API文档中的格式
|
||||
const request = {
|
||||
cmd: 'exec_chat', // 使用exec_chat命令
|
||||
request_id: requestId,
|
||||
msg: message, // 使用msg字段而不是content
|
||||
model: model,
|
||||
stream: true
|
||||
};
|
||||
|
||||
this.logger(`发送聊天消息 (ID: ${requestId}): ${message.substring(0, 50)}${message.length > 50 ? '...' : ''}`);
|
||||
this.logger(`使用模型: ${model}`);
|
||||
|
||||
// 创建用户消息
|
||||
const userMessage: ChatMessage = {
|
||||
id: `request-${requestId}`,
|
||||
content: message,
|
||||
role: 'user',
|
||||
createAt: new Date()
|
||||
};
|
||||
|
||||
// 通知消息已创建
|
||||
this.emit(WebSocketEvent.MESSAGE, userMessage);
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let responseContent = '';
|
||||
let responseStartTime: number | null = null;
|
||||
let lastResponseTime: number | null = null;
|
||||
|
||||
// 设置超时处理
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.logger(`聊天消息响应超时 (ID: ${requestId})`);
|
||||
this.requestMap.delete(requestId);
|
||||
reject(new Error('服务器响应超时,请稍后重试'));
|
||||
}, 60000); // 60秒超时
|
||||
|
||||
this.requestMap.set(requestId, (response) => {
|
||||
// 记录第一次收到响应的时间
|
||||
if (responseStartTime === null) {
|
||||
responseStartTime = Date.now();
|
||||
this.logger(`收到第一个响应包 (ID: ${requestId}), 延迟: ${Date.now() - lastResponseTime!}ms`);
|
||||
}
|
||||
|
||||
// 更新最后响应时间
|
||||
lastResponseTime = Date.now();
|
||||
|
||||
if (response.error) {
|
||||
clearTimeout(timeoutId);
|
||||
this.logger(`聊天消息响应错误 (ID: ${requestId}): ${response.error}`);
|
||||
reject(new Error(response.error));
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理消息内容 - 适配不同的响应字段名
|
||||
if (response.msg) {
|
||||
responseContent += response.msg;
|
||||
|
||||
// 创建助手消息
|
||||
const assistantMessage: ChatMessage = {
|
||||
id: `response-${requestId}-${response.stream_seq_id || 0}`,
|
||||
content: responseContent,
|
||||
role: 'assistant',
|
||||
createAt: new Date()
|
||||
};
|
||||
|
||||
// 通知有新消息
|
||||
this.emit(WebSocketEvent.MESSAGE, assistantMessage);
|
||||
}
|
||||
|
||||
// 检查是否是最后一个响应包
|
||||
if (!response.stream_seq_id || response.stream_finsh) {
|
||||
clearTimeout(timeoutId);
|
||||
const totalTime = Date.now() - responseStartTime!;
|
||||
this.logger(`聊天消息完成 (ID: ${requestId}), 总耗时: ${totalTime}ms`);
|
||||
resolve(responseContent);
|
||||
}
|
||||
});
|
||||
|
||||
// 记录发送时间
|
||||
lastResponseTime = Date.now();
|
||||
|
||||
try {
|
||||
this.sendRequest(request);
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
this.logger(`发送请求失败 (ID: ${requestId}): ${error}`);
|
||||
this.requestMap.delete(requestId);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取WebSocket ReadyState的描述
|
||||
*/
|
||||
private getReadyStateDescription(state: number): string {
|
||||
switch (state) {
|
||||
case WebSocket.CONNECTING:
|
||||
return '正在连接 (CONNECTING: 0)';
|
||||
case WebSocket.OPEN:
|
||||
return '已连接 (OPEN: 1)';
|
||||
case WebSocket.CLOSING:
|
||||
return '正在关闭 (CLOSING: 2)';
|
||||
case WebSocket.CLOSED:
|
||||
return '已关闭 (CLOSED: 3)';
|
||||
default:
|
||||
return `未知状态: ${state}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置心跳检测
|
||||
*/
|
||||
private setupPingInterval(): void {
|
||||
this.clearTimers();
|
||||
|
||||
// 每30秒发送一次空消息以保持连接
|
||||
this.pingInterval = window.setInterval(() => {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
// 发送空心跳消息
|
||||
this.socket.send(JSON.stringify({ type: 'ping' }));
|
||||
} catch (error) {
|
||||
this.logger(`发送心跳包失败: ${error}`);
|
||||
}
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安排重连
|
||||
*/
|
||||
private scheduleReconnect(): void {
|
||||
if (this.isReconnecting || this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
|
||||
if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
|
||||
this.logger(`达到最大重连尝试次数 (${this.MAX_RECONNECT_ATTEMPTS}),停止重连`);
|
||||
this.emit(WebSocketEvent.ERROR, `连接失败:已尝试 ${this.MAX_RECONNECT_ATTEMPTS} 次重连,请检查网络或服务器状态后手动重连`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.isReconnecting = true;
|
||||
this.reconnectAttempts++;
|
||||
|
||||
// 使用指数退避策略和随机抖动
|
||||
const baseDelay = this.RECONNECT_DELAY * Math.pow(1.5, this.reconnectAttempts - 1);
|
||||
const jitter = Math.random() * 1000; // 添加最多1秒的随机抖动
|
||||
const delay = Math.min(baseDelay + jitter, 30000); // 上限为30秒
|
||||
|
||||
this.logger(`尝试重新连接 (${this.reconnectAttempts}/${this.MAX_RECONNECT_ATTEMPTS}), 延迟 ${delay}ms`);
|
||||
|
||||
this.reconnectTimer = window.setTimeout(() => {
|
||||
this.isReconnecting = false;
|
||||
this.connect();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有定时器
|
||||
*/
|
||||
private clearTimers(): void {
|
||||
if (this.pingInterval !== null) {
|
||||
window.clearInterval(this.pingInterval);
|
||||
this.pingInterval = null;
|
||||
}
|
||||
|
||||
if (this.reconnectTimer !== null) {
|
||||
window.clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送WebSocket请求
|
||||
*/
|
||||
private sendRequest(request: any): void {
|
||||
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
||||
this.logger('无法发送请求: WebSocket未连接');
|
||||
throw new Error('WebSocket未连接');
|
||||
}
|
||||
|
||||
try {
|
||||
const requestStr = JSON.stringify(request);
|
||||
this.socket.send(requestStr);
|
||||
} catch (error) {
|
||||
this.logger(`发送WebSocket请求失败: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认模型列表
|
||||
*/
|
||||
private getDefaultModels(): string[] {
|
||||
return ['qwq:latest', 'deepseek-r1:32b', 'qwen2.5-coder:7b'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取WebSocket关闭代码解释
|
||||
*/
|
||||
private getCloseEventReason(code: number): string {
|
||||
const explanations: Record<number, string> = {
|
||||
1000: '正常关闭',
|
||||
1001: '端点离开',
|
||||
1002: '协议错误',
|
||||
1003: '无法接受数据',
|
||||
1004: '保留',
|
||||
1005: '未提供状态码',
|
||||
1006: '异常关闭',
|
||||
1007: '数据类型不一致',
|
||||
1008: '违反策略',
|
||||
1009: '消息太大',
|
||||
1010: '必需的扩展缺失',
|
||||
1011: '内部错误',
|
||||
1012: '服务重启',
|
||||
1013: '临时错误',
|
||||
1014: '服务器超载',
|
||||
1015: 'TLS握手失败'
|
||||
};
|
||||
|
||||
return explanations[code] || `未知代码: ${code}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建聊天状态管理
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
// WebSocket服务实例
|
||||
const wsService = ref<WebSocketService | null>(null);
|
||||
|
||||
// 聊天状态
|
||||
const chatState = reactive({
|
||||
messages: [] as ChatMessage[],
|
||||
loading: false,
|
||||
connectionStatus: 'disconnected' as 'connecting' | 'connected' | 'disconnected' | 'error',
|
||||
errorMessage: '',
|
||||
availableModels: [] as string[],
|
||||
currentModel: '',
|
||||
loadingModels: false
|
||||
});
|
||||
|
||||
// 初始化WebSocket服务
|
||||
function initWebSocketService(apiUrl: string, apiKey: string) {
|
||||
// 记录函数调用到VSCode日志
|
||||
const logToVSCode = (message: string) => {
|
||||
try {
|
||||
if (window.vscodeApi) {
|
||||
window.vscodeApi.postMessage({
|
||||
type: 'log',
|
||||
message: `[WebSocketInit] ${message}`
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('向VSCode发送日志失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
logToVSCode(`初始化WebSocket服务开始: ${apiUrl}`);
|
||||
console.log(`[ChatStore] 初始化WebSocket服务开始: ${apiUrl}`);
|
||||
|
||||
try {
|
||||
if (!apiUrl || !apiKey) {
|
||||
const errorMsg = 'API URL或API Key为空,无法初始化WebSocket服务';
|
||||
console.error('[ChatStore] ' + errorMsg);
|
||||
logToVSCode(errorMsg);
|
||||
chatState.errorMessage = 'API配置不完整,请先完成配置';
|
||||
chatState.connectionStatus = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已存在WebSocket服务,先断开连接
|
||||
if (wsService.value) {
|
||||
console.log('[ChatStore] 断开现有WebSocket连接');
|
||||
logToVSCode('断开现有WebSocket连接');
|
||||
wsService.value.disconnect();
|
||||
}
|
||||
|
||||
// 创建新的WebSocket服务实例
|
||||
console.log('[ChatStore] 创建新的WebSocket服务实例');
|
||||
logToVSCode('创建新的WebSocket服务实例');
|
||||
wsService.value = new WebSocketService(apiUrl, apiKey, (message) => {
|
||||
// 确保日志同时发送到控制台和VSCode
|
||||
console.log(`[WebSocketService] ${message}`);
|
||||
|
||||
if (window.vscodeApi) {
|
||||
window.vscodeApi.postMessage({
|
||||
type: 'log',
|
||||
message: `[WebSocketService] ${message}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 设置事件监听
|
||||
console.log('[ChatStore] 设置WebSocket事件监听');
|
||||
logToVSCode('设置WebSocket事件监听');
|
||||
setupWebSocketEvents();
|
||||
|
||||
// 连接WebSocket
|
||||
console.log('[ChatStore] 开始连接WebSocket');
|
||||
logToVSCode('开始连接WebSocket');
|
||||
chatState.connectionStatus = 'connecting';
|
||||
wsService.value.connect();
|
||||
|
||||
// 5秒后检查连接状态
|
||||
setTimeout(() => {
|
||||
if (!wsService.value) {
|
||||
console.error('[ChatStore] WebSocket服务实例为null');
|
||||
logToVSCode('WebSocket服务实例为null');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wsService.value.isConnected) {
|
||||
console.error('[ChatStore] WebSocket连接超时');
|
||||
logToVSCode('WebSocket连接超时');
|
||||
chatState.errorMessage = '连接超时,请检查API配置是否正确';
|
||||
chatState.connectionStatus = 'error';
|
||||
} else {
|
||||
console.log('[ChatStore] WebSocket连接成功');
|
||||
logToVSCode('WebSocket连接成功');
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
console.log('[ChatStore] 初始化WebSocket服务完成');
|
||||
logToVSCode('初始化WebSocket服务完成');
|
||||
} catch (error) {
|
||||
console.error('[ChatStore] 初始化WebSocket服务异常:', error);
|
||||
logToVSCode(`初始化WebSocket服务异常: ${error instanceof Error ? error.message : String(error)}`);
|
||||
chatState.errorMessage = `初始化WebSocket失败: ${error instanceof Error ? error.message : String(error)}`;
|
||||
chatState.connectionStatus = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
// 设置WebSocket事件监听
|
||||
function setupWebSocketEvents() {
|
||||
if (!wsService.value) {
|
||||
console.error('[ChatStore] setupWebSocketEvents失败: wsService为null');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ChatStore] 开始设置WebSocket事件监听...');
|
||||
|
||||
wsService.value.on(WebSocketEvent.CONNECTED, () => {
|
||||
console.log('[ChatStore] 收到WebSocket CONNECTED事件');
|
||||
chatState.connectionStatus = 'connected';
|
||||
chatState.errorMessage = '';
|
||||
});
|
||||
|
||||
wsService.value.on(WebSocketEvent.DISCONNECTED, () => {
|
||||
console.log('[ChatStore] 收到WebSocket DISCONNECTED事件');
|
||||
chatState.connectionStatus = 'disconnected';
|
||||
});
|
||||
|
||||
wsService.value.on(WebSocketEvent.ERROR, (error: string) => {
|
||||
console.error('[ChatStore] 收到WebSocket ERROR事件:', error);
|
||||
chatState.errorMessage = error;
|
||||
chatState.connectionStatus = 'error';
|
||||
chatState.loading = false;
|
||||
chatState.loadingModels = false;
|
||||
});
|
||||
|
||||
wsService.value.on(WebSocketEvent.MESSAGE, (message: ChatMessage) => {
|
||||
console.log('[ChatStore] 收到WebSocket MESSAGE事件');
|
||||
// 查找是否已存在具有相同ID的消息
|
||||
const existingIndex = chatState.messages.findIndex(m => m.id === message.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// 更新现有消息
|
||||
chatState.messages[existingIndex] = message;
|
||||
} else {
|
||||
// 添加新消息
|
||||
chatState.messages.push(message);
|
||||
}
|
||||
|
||||
chatState.loading = false;
|
||||
});
|
||||
|
||||
wsService.value.on(WebSocketEvent.MODEL_LIST, (models: string[]) => {
|
||||
console.log('[ChatStore] 收到WebSocket MODEL_LIST事件:', models);
|
||||
chatState.loadingModels = false;
|
||||
|
||||
if (Array.isArray(models) && models.length > 0) {
|
||||
chatState.availableModels = models;
|
||||
|
||||
// 如果没有当前模型,设置第一个为当前模型
|
||||
if (!chatState.currentModel && models.length > 0) {
|
||||
chatState.currentModel = models[0];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[ChatStore] WebSocket事件监听器设置完成');
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
async function sendMessage(content: string) {
|
||||
console.log('Store: 开始发送消息', { content, connectionStatus: chatState.connectionStatus });
|
||||
|
||||
if (!wsService.value || !wsService.value.isConnected) {
|
||||
console.error('Store: WebSocket未连接,无法发送消息');
|
||||
chatState.errorMessage = '未连接到AI服务,请先连接';
|
||||
chatState.connectionStatus = 'error';
|
||||
throw new Error('WebSocket未连接');
|
||||
}
|
||||
|
||||
if (!content.trim() || chatState.loading) {
|
||||
console.log('Store: 消息为空或正在加载中,不发送');
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保有当前模型
|
||||
if (!chatState.currentModel && chatState.availableModels.length > 0) {
|
||||
console.log('Store: 自动选择模型', chatState.availableModels[0]);
|
||||
chatState.currentModel = chatState.availableModels[0];
|
||||
} else if (!chatState.currentModel) {
|
||||
console.error('Store: 没有可用模型');
|
||||
throw new Error('没有可用的AI模型');
|
||||
}
|
||||
|
||||
console.log(`Store: 准备发送消息到WebSocket,使用模型: ${chatState.currentModel}`);
|
||||
chatState.loading = true;
|
||||
|
||||
try {
|
||||
// 记录开始时间,用于计算响应时间
|
||||
const startTime = Date.now();
|
||||
|
||||
// 直接发送消息到WebSocket服务
|
||||
const response = await wsService.value.sendChatMessage(content, chatState.currentModel);
|
||||
|
||||
// 计算响应时间
|
||||
const responseTime = Date.now() - startTime;
|
||||
console.log(`Store: 收到WebSocket响应,耗时: ${responseTime}ms`);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Store: 发送消息失败:', error);
|
||||
chatState.errorMessage = `发送消息失败: ${error instanceof Error ? error.message : '未知错误'}`;
|
||||
chatState.connectionStatus = 'error';
|
||||
chatState.loading = false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新模型列表
|
||||
async function refreshModels() {
|
||||
if (!wsService.value || !wsService.value.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
chatState.loadingModels = true;
|
||||
|
||||
try {
|
||||
const models = await wsService.value.getModelList();
|
||||
|
||||
if (Array.isArray(models) && models.length > 0) {
|
||||
chatState.availableModels = models;
|
||||
}
|
||||
|
||||
chatState.loadingModels = false;
|
||||
} catch (error) {
|
||||
chatState.loadingModels = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置当前模型
|
||||
function setCurrentModel(model: string) {
|
||||
chatState.currentModel = model;
|
||||
}
|
||||
|
||||
// 清空消息
|
||||
function clearMessages() {
|
||||
chatState.messages = [];
|
||||
}
|
||||
|
||||
// 手动重连
|
||||
function reconnect() {
|
||||
if (wsService.value) {
|
||||
wsService.value.disconnect();
|
||||
chatState.connectionStatus = 'connecting';
|
||||
wsService.value.connect();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
chatState,
|
||||
initWebSocketService,
|
||||
sendMessage,
|
||||
refreshModels,
|
||||
setCurrentModel,
|
||||
clearMessages,
|
||||
reconnect
|
||||
};
|
||||
});
|
271
webview/src/store/themeStore.ts
Normal file
271
webview/src/store/themeStore.ts
Normal file
@ -0,0 +1,271 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { theme as antdTheme } from 'ant-design-vue';
|
||||
|
||||
// 定义主题颜色接口
|
||||
export interface ThemeColors {
|
||||
// 主题类型
|
||||
themeType?: 'light' | 'dark';
|
||||
|
||||
// 基本颜色
|
||||
foreground?: string;
|
||||
background?: string;
|
||||
disabledForeground?: string;
|
||||
|
||||
// 输入框
|
||||
inputBackground?: string;
|
||||
inputForeground?: string;
|
||||
inputPlaceholderForeground?: string;
|
||||
inputBorder?: string;
|
||||
|
||||
// 按钮
|
||||
buttonBackground?: string;
|
||||
buttonForeground?: string;
|
||||
buttonHoverBackground?: string;
|
||||
buttonSecondaryBackground?: string;
|
||||
buttonSecondaryForeground?: string;
|
||||
buttonSecondaryHoverBackground?: string;
|
||||
|
||||
// 标签页
|
||||
tabActiveForeground?: string;
|
||||
tabInactiveForeground?: string;
|
||||
tabHoverForeground?: string;
|
||||
tabActiveBorder?: string;
|
||||
tabActiveBackground?: string;
|
||||
tabInactiveBackground?: string;
|
||||
tabHoverBackground?: string;
|
||||
|
||||
// 边框和分隔线
|
||||
panelBorder?: string;
|
||||
widgetBorder?: string;
|
||||
|
||||
// 链接
|
||||
textLinkForeground?: string;
|
||||
textLinkActiveForeground?: string;
|
||||
|
||||
// 滚动条
|
||||
scrollbarSliderBackground?: string;
|
||||
scrollbarSliderHoverBackground?: string;
|
||||
scrollbarSliderActiveBackground?: string;
|
||||
|
||||
// 聊天相关
|
||||
chatRequestBackground?: string;
|
||||
chatRequestBorder?: string;
|
||||
chatRequestForeground?: string;
|
||||
chatResponseBackground?: string;
|
||||
chatResponseBorder?: string;
|
||||
chatResponseForeground?: string;
|
||||
|
||||
// 代码块和预格式化文本
|
||||
textCodeBlockBackground?: string;
|
||||
textPreformatForeground?: string;
|
||||
textPreformatBackground?: string;
|
||||
textBlockQuoteBackground?: string;
|
||||
textBlockQuoteBorder?: string;
|
||||
|
||||
// 菜单
|
||||
menuBackground?: string;
|
||||
menuForeground?: string;
|
||||
menuSelectionBackground?: string;
|
||||
menuSelectionForeground?: string;
|
||||
menuBorder?: string;
|
||||
menuSeparatorBackground?: string;
|
||||
|
||||
// 列表
|
||||
listActiveSelectionBackground?: string;
|
||||
listActiveSelectionForeground?: string;
|
||||
listHoverBackground?: string;
|
||||
listHoverForeground?: string;
|
||||
|
||||
// 其他UI元素
|
||||
descriptionForeground?: string;
|
||||
editorWidgetBackground?: string;
|
||||
editorWidgetForeground?: string;
|
||||
focusBorder?: string;
|
||||
progressBarBackground?: string;
|
||||
|
||||
// 边框
|
||||
contrastBorder?: string;
|
||||
contrastActiveBorder?: string;
|
||||
}
|
||||
|
||||
// 默认颜色,用于fallback
|
||||
const defaultThemeColors: ThemeColors = {
|
||||
themeType: 'dark',
|
||||
|
||||
foreground: '#CCCCCC',
|
||||
background: '#1E1E1E',
|
||||
disabledForeground: '#888888',
|
||||
|
||||
inputBackground: '#3C3C3C',
|
||||
inputForeground: '#CCCCCC',
|
||||
inputPlaceholderForeground: '#888888',
|
||||
inputBorder: '#3C3C3C',
|
||||
|
||||
buttonBackground: '#0E639C',
|
||||
buttonForeground: '#FFFFFF',
|
||||
buttonHoverBackground: '#1177BB',
|
||||
buttonSecondaryBackground: '#3A3D41',
|
||||
buttonSecondaryForeground: '#FFFFFF',
|
||||
buttonSecondaryHoverBackground: '#45494E',
|
||||
|
||||
tabActiveForeground: '#FFFFFF',
|
||||
tabInactiveForeground: '#AAAAAA',
|
||||
tabHoverForeground: '#FFFFFF',
|
||||
tabActiveBorder: '#0E639C',
|
||||
tabActiveBackground: '#1E1E1E',
|
||||
tabInactiveBackground: '#2D2D2D',
|
||||
tabHoverBackground: '#2A2D2E',
|
||||
|
||||
panelBorder: '#808080',
|
||||
widgetBorder: '#3C3C3C',
|
||||
|
||||
textLinkForeground: '#3794FF',
|
||||
textLinkActiveForeground: '#3794FF',
|
||||
|
||||
scrollbarSliderBackground: 'rgba(121, 121, 121, 0.4)',
|
||||
scrollbarSliderHoverBackground: 'rgba(100, 100, 100, 0.7)',
|
||||
scrollbarSliderActiveBackground: 'rgba(191, 191, 191, 0.4)',
|
||||
|
||||
chatRequestBackground: 'rgba(56, 56, 56, 0.6)',
|
||||
chatRequestBorder: 'rgba(86, 86, 86, 0.6)',
|
||||
chatRequestForeground: '#CCCCCC',
|
||||
chatResponseBackground: 'rgba(38, 79, 120, 0.6)',
|
||||
chatResponseBorder: 'rgba(58, 109, 150, 0.6)',
|
||||
chatResponseForeground: '#FFFFFF',
|
||||
|
||||
textCodeBlockBackground: '#1E1E1E',
|
||||
textPreformatForeground: '#D4D4D4',
|
||||
textPreformatBackground: '#0D0D0D',
|
||||
textBlockQuoteBackground: 'rgba(68, 68, 68, 0.2)',
|
||||
textBlockQuoteBorder: '#608B4E',
|
||||
|
||||
menuBackground: '#252526',
|
||||
menuForeground: '#CCCCCC',
|
||||
menuSelectionBackground: '#04395E',
|
||||
menuSelectionForeground: '#FFFFFF',
|
||||
menuBorder: '#454545',
|
||||
menuSeparatorBackground: '#454545',
|
||||
|
||||
listActiveSelectionBackground: '#094771',
|
||||
listActiveSelectionForeground: '#FFFFFF',
|
||||
listHoverBackground: 'rgba(90, 93, 94, 0.31)',
|
||||
listHoverForeground: '#FFFFFF',
|
||||
|
||||
descriptionForeground: '#AAAAAA',
|
||||
editorWidgetBackground: '#252526',
|
||||
editorWidgetForeground: '#CCCCCC',
|
||||
focusBorder: '#007FD4',
|
||||
progressBarBackground: '#0E70C0',
|
||||
|
||||
contrastBorder: '#6F6F6F',
|
||||
contrastActiveBorder: '#F38518',
|
||||
};
|
||||
|
||||
// Ant Design暗色主题算法配置
|
||||
const darkAlgorithm = antdTheme.darkAlgorithm;
|
||||
// Ant Design亮色主题算法配置
|
||||
const defaultAlgorithm = antdTheme.defaultAlgorithm;
|
||||
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
// 主题颜色状态
|
||||
const themeColors = ref<ThemeColors>({ ...defaultThemeColors });
|
||||
|
||||
// 主题类型
|
||||
const themeType = computed(() => themeColors.value.themeType || 'dark');
|
||||
const isDarkTheme = computed(() => themeType.value === 'dark');
|
||||
|
||||
// 获取Ant Design主题配置
|
||||
const antdThemeConfig = computed(() => {
|
||||
return {
|
||||
algorithm: isDarkTheme.value ? darkAlgorithm : defaultAlgorithm,
|
||||
token: {
|
||||
colorPrimary: themeColors.value.buttonBackground || '#0E639C',
|
||||
colorLink: themeColors.value.buttonBackground || '#0E639C',
|
||||
borderRadius: 4,
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
colorPrimary: themeColors.value.buttonBackground,
|
||||
colorPrimaryHover: themeColors.value.buttonHoverBackground,
|
||||
colorPrimaryActive: themeColors.value.buttonHoverBackground,
|
||||
colorPrimaryText: themeColors.value.buttonForeground,
|
||||
},
|
||||
Input: {
|
||||
colorBgContainer: themeColors.value.inputBackground,
|
||||
colorText: themeColors.value.inputForeground,
|
||||
colorTextPlaceholder: themeColors.value.inputPlaceholderForeground,
|
||||
colorBorder: themeColors.value.inputBorder,
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 获取带有默认值的颜色
|
||||
const getColor = (colorId: keyof ThemeColors) => {
|
||||
return themeColors.value[colorId] || defaultThemeColors[colorId] || '';
|
||||
};
|
||||
|
||||
// 设置主题颜色
|
||||
const setThemeColors = (colors: ThemeColors) => {
|
||||
// 合并新颜色,保留默认值作为回退
|
||||
themeColors.value = { ...defaultThemeColors, ...colors };
|
||||
// 动态设置 CSS 变量
|
||||
applyThemeColorsToCSS();
|
||||
};
|
||||
|
||||
// 将主题颜色应用为 CSS 变量
|
||||
const applyThemeColorsToCSS = () => {
|
||||
const root = document.documentElement;
|
||||
|
||||
// 首先设置主题类型
|
||||
if (isDarkTheme.value) {
|
||||
root.classList.add('vscode-dark');
|
||||
root.classList.remove('vscode-light');
|
||||
} else {
|
||||
root.classList.add('vscode-light');
|
||||
root.classList.remove('vscode-dark');
|
||||
}
|
||||
|
||||
// 遍历所有颜色并设置为 CSS 变量
|
||||
for (const [key, value] of Object.entries(themeColors.value)) {
|
||||
if (value && key !== 'themeType') {
|
||||
// 转换 camelCase 为 kebab-case:例如 buttonBackground 变为 --vscode-button-background
|
||||
const cssVarName = '--vscode-' + key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
||||
root.style.setProperty(cssVarName, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 请求主题颜色
|
||||
const requestThemeColors = () => {
|
||||
// 判断是否在 VS Code webview 环境中
|
||||
if (typeof acquireVsCodeApi !== 'undefined') {
|
||||
try {
|
||||
const vscode = acquireVsCodeApi();
|
||||
vscode.postMessage({ type: 'getThemeColors' });
|
||||
} catch (error) {
|
||||
console.error('Failed to request theme colors from VS Code:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化
|
||||
const initialize = () => {
|
||||
requestThemeColors();
|
||||
// 在页面加载后应用默认主题颜色
|
||||
applyThemeColorsToCSS();
|
||||
};
|
||||
|
||||
return {
|
||||
themeColors,
|
||||
themeType,
|
||||
isDarkTheme,
|
||||
antdThemeConfig,
|
||||
getColor,
|
||||
setThemeColors,
|
||||
applyThemeColorsToCSS,
|
||||
requestThemeColors,
|
||||
initialize
|
||||
};
|
||||
});
|
45
webview/src/styles/_variables.scss
Normal file
45
webview/src/styles/_variables.scss
Normal file
@ -0,0 +1,45 @@
|
||||
// 颜色变量
|
||||
$primary-color: var(--vscode-button-background);
|
||||
$success-color: var(--vscode-testing-icon-passed, #52c41a);
|
||||
$warning-color: var(--vscode-editor-warning-foreground, #faad14);
|
||||
$error-color: var(--vscode-editor-error-foreground, #f5222d);
|
||||
$font-size-base: 14px;
|
||||
$heading-color: var(--vscode-editor-foreground, #ffffff);
|
||||
$text-color: var(--vscode-editor-foreground, #cccccc);
|
||||
$text-color-secondary: var(--vscode-description-foreground, #aaaaaa);
|
||||
$disabled-color: var(--vscode-disabled-foreground);
|
||||
$border-radius-base: 4px;
|
||||
$border-color-base: var(--vscode-panel-border);
|
||||
$box-shadow-base: var(--vscode-widget-shadow);
|
||||
|
||||
// 暗色主题变量
|
||||
$dark-background: var(--vscode-editor-background);
|
||||
$dark-foreground: var(--vscode-editor-foreground, #ffffff);
|
||||
$dark-foreground-secondary: var(--vscode-editor-foreground, #cccccc);
|
||||
$dark-border: var(--vscode-panel-border);
|
||||
$dark-input-background: var(--vscode-input-background);
|
||||
$dark-input-foreground: var(--vscode-input-foreground);
|
||||
$dark-button-background: var(--vscode-button-background);
|
||||
$dark-button-foreground: var(--vscode-button-foreground, #ffffff);
|
||||
$dark-button-hover-background: var(--vscode-button-hover-background);
|
||||
|
||||
// 聊天消息相关颜色
|
||||
$chat-request-background: var(--vscode-chat-request-background, rgba(37, 37, 37, 0.6));
|
||||
$chat-request-foreground: var(--vscode-chat-request-foreground, var(--vscode-foreground));
|
||||
$chat-response-background: var(--vscode-chat-response-background, rgba(45, 70, 100, 0.6));
|
||||
$chat-response-foreground: var(--vscode-chat-response-foreground, var(--vscode-foreground));
|
||||
|
||||
// 布局变量
|
||||
$header-height: 48px;
|
||||
$footer-height: 80px;
|
||||
$sidebar-width: 250px;
|
||||
$content-padding: 16px;
|
||||
|
||||
// 新增组件所需变量
|
||||
$border-color: var(--vscode-panel-border);
|
||||
$hover-background: var(--vscode-list-hover-background);
|
||||
$input-background: var(--vscode-input-background);
|
||||
$user-message-background: var(--vscode-chat-request-background, rgba(37, 37, 37, 0.6));
|
||||
$avatar-background: var(--vscode-badge-background, var(--vscode-button-background));
|
||||
$code-block-background: var(--vscode-text-code-block-background, rgba(10, 10, 10, 0.3));
|
||||
$disabled-background: var(--vscode-disabled-foreground);
|
254
webview/src/styles/antdx-override.scss
Normal file
254
webview/src/styles/antdx-override.scss
Normal file
@ -0,0 +1,254 @@
|
||||
@use './_variables.scss' as *;
|
||||
|
||||
// 移除全局强制颜色覆盖,使用有针对性的样式
|
||||
|
||||
// 聊天气泡样式 - 用户
|
||||
.ant-design-x-vue-bubble.ant-design-x-vue-bubble-user {
|
||||
.vscode-dark & {
|
||||
background-color: rgba(37, 37, 37, 0.6) !important;
|
||||
color: var(--text-color) !important;
|
||||
border: 1px solid rgba(86, 86, 86, 0.6) !important;
|
||||
}
|
||||
|
||||
.vscode-light & {
|
||||
background-color: rgba(240, 240, 240, 0.6) !important;
|
||||
color: var(--text-color) !important;
|
||||
border: 1px solid rgba(200, 200, 200, 0.6) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 聊天气泡样式 - 助手
|
||||
.ant-design-x-vue-bubble.ant-design-x-vue-bubble-assistant {
|
||||
.vscode-dark & {
|
||||
background-color: rgba(45, 70, 100, 0.6) !important;
|
||||
color: var(--text-color) !important;
|
||||
border: 1px solid rgba(58, 109, 150, 0.6) !important;
|
||||
}
|
||||
|
||||
.vscode-light & {
|
||||
background-color: rgba(230, 240, 250, 0.6) !important;
|
||||
color: var(--text-color) !important;
|
||||
border: 1px solid rgba(180, 200, 230, 0.6) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入区域文本颜色
|
||||
input, textarea, .ant-input, .ant-input-textarea {
|
||||
color: var(--vscode-input-foreground, var(--text-color)) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--vscode-input-placeholderForeground, var(--text-color-secondary)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Markdown内容样式
|
||||
.markdown-body {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--heading-color) !important;
|
||||
}
|
||||
|
||||
p, li, span {
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-color) !important;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--code-background) !important;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left-color: var(--vscode-button-background, #0E639C) !important;
|
||||
background-color: var(--code-background) !important;
|
||||
}
|
||||
|
||||
strong, b {
|
||||
color: var(--heading-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 思考链节点样式
|
||||
.ant-design-x-vue-thought-chain-title,
|
||||
.ant-design-x-vue-thought-chain-item-content,
|
||||
.ant-design-x-vue-thought-chain-item-index {
|
||||
color: var(--heading-color) !important;
|
||||
}
|
||||
|
||||
// 确保数字圆圈样式在亮暗两种主题下可见
|
||||
.numberCircle, .stepNumber {
|
||||
.vscode-dark & {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.vscode-light & {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 覆盖发送按钮样式
|
||||
.ant-design-x-vue-sender {
|
||||
background-color: var(--vscode-input-background) !important;
|
||||
border-color: var(--vscode-panel-border) !important;
|
||||
|
||||
.ant-design-x-vue-sender-textarea {
|
||||
background-color: var(--vscode-input-background) !important;
|
||||
color: var(--vscode-input-foreground) !important;
|
||||
border-color: var(--vscode-panel-border) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--vscode-input-placeholder-foreground, var(--vscode-description-foreground)) !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--vscode-focus-border) !important;
|
||||
box-shadow: 0 0 0 2px var(--vscode-focus-border) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-design-x-vue-sender-action-button {
|
||||
background-color: var(--vscode-button-background) !important;
|
||||
color: var(--vscode-button-foreground) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--vscode-button-hover-background) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-design-x-vue-sender-footer {
|
||||
color: var(--vscode-description-foreground) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 覆盖对话列表样式
|
||||
.ant-design-x-vue-conversations {
|
||||
background-color: var(--vscode-editor-background) !important;
|
||||
|
||||
.ant-design-x-vue-conversations-message-time {
|
||||
color: var(--vscode-description-foreground) !important;
|
||||
}
|
||||
|
||||
.ant-design-x-vue-conversations-loading {
|
||||
color: var(--vscode-progressBar-background, var(--vscode-button-background)) !important;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
color: var(--vscode-foreground) !important;
|
||||
|
||||
code {
|
||||
background-color: var(--vscode-text-code-block-background, rgba(10, 10, 10, 0.3)) !important;
|
||||
color: var(--vscode-text-preformat-foreground, var(--vscode-foreground)) !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: var(--vscode-text-code-block-background, rgba(10, 10, 10, 0.3)) !important;
|
||||
border-color: var(--vscode-panel-border) !important;
|
||||
|
||||
code {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--vscode-text-link-foreground) !important;
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-text-link-active-foreground) !important;
|
||||
}
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--vscode-foreground) !important;
|
||||
border-color: var(--vscode-panel-border) !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: var(--vscode-panel-border) !important;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
color: var(--vscode-foreground) !important;
|
||||
border-left-color: var(--vscode-panel-border) !important;
|
||||
background-color: var(--vscode-text-block-quote-background, rgba(20, 20, 20, 0.2)) !important;
|
||||
}
|
||||
|
||||
table {
|
||||
th, td {
|
||||
border-color: var(--vscode-panel-border) !important;
|
||||
}
|
||||
|
||||
tr:nth-child(2n) {
|
||||
background-color: var(--vscode-list-hover-background, rgba(40, 40, 40, 0.2)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 覆盖下拉菜单样式
|
||||
.ant-dropdown {
|
||||
.ant-dropdown-menu {
|
||||
background-color: var(--vscode-menu-background) !important;
|
||||
border-color: var(--vscode-panel-border) !important;
|
||||
|
||||
.ant-dropdown-menu-item {
|
||||
color: var(--vscode-menu-foreground) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--vscode-menu-selection-background) !important;
|
||||
color: var(--vscode-menu-selection-foreground) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ant Design 通用组件样式覆盖
|
||||
.ant-button {
|
||||
color: var(--vscode-button-foreground) !important;
|
||||
|
||||
&.ant-button-text {
|
||||
color: var(--vscode-foreground) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-spin {
|
||||
color: var(--vscode-progressBar-background, var(--vscode-button-background)) !important;
|
||||
}
|
||||
|
||||
.ant-menu {
|
||||
background-color: var(--vscode-menu-background) !important;
|
||||
color: var(--vscode-menu-foreground) !important;
|
||||
|
||||
.ant-menu-item {
|
||||
color: var(--vscode-menu-foreground) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--vscode-list-hover-background) !important;
|
||||
}
|
||||
|
||||
&.ant-menu-item-selected {
|
||||
background-color: var(--vscode-menu-selection-background) !important;
|
||||
color: var(--vscode-menu-selection-foreground) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标签页样式
|
||||
.ant-tabs {
|
||||
.ant-tabs-tab {
|
||||
color: var(--vscode-tab-inactive-foreground, var(--vscode-description-foreground)) !important;
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-tab-hover-foreground, var(--vscode-foreground)) !important;
|
||||
}
|
||||
|
||||
&.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||
color: var(--vscode-tab-active-foreground, var(--vscode-foreground)) !important;
|
||||
}
|
||||
}
|
||||
}
|
133
webview/src/styles/global.scss
Normal file
133
webview/src/styles/global.scss
Normal file
@ -0,0 +1,133 @@
|
||||
@use './_variables.scss' as *;
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--vscode-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-foreground);
|
||||
font-size: var(--vscode-font-size, $font-size-base);
|
||||
line-height: 1.5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// VSCode暗色主题样式
|
||||
:root.vscode-dark {
|
||||
--text-color: #cccccc;
|
||||
--text-color-secondary: #9e9e9e;
|
||||
--heading-color: #ffffff;
|
||||
--link-color: #3794ff;
|
||||
--code-background: rgba(10, 10, 10, 0.3);
|
||||
}
|
||||
|
||||
// VSCode亮色主题样式
|
||||
:root.vscode-light {
|
||||
--text-color: #333333;
|
||||
--text-color-secondary: #666666;
|
||||
--heading-color: #000000;
|
||||
--link-color: #0066cc;
|
||||
--code-background: rgba(200, 200, 200, 0.3);
|
||||
}
|
||||
|
||||
// 基础文本样式
|
||||
p, span, div, li {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
// 标题样式
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--heading-color);
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
// 链接样式
|
||||
a {
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--vscode-scrollbar-slider-background, rgba(100, 100, 100, 0.4));
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--vscode-scrollbar-slider-hover-background, rgba(100, 100, 100, 0.7));
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background: var(--vscode-scrollbar-slider-active-background, rgba(100, 100, 100, 0.8));
|
||||
}
|
||||
|
||||
// 代码块样式
|
||||
pre {
|
||||
background-color: var(--code-background);
|
||||
border-radius: $border-radius-base;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-family: var(--vscode-editor-font-family, 'Menlo', 'Monaco', 'Consolas', 'Courier New', monospace);
|
||||
margin: 1em 0;
|
||||
border: 1px solid var(--vscode-panel-border, rgba(128, 128, 128, 0.35));
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: var(--vscode-editor-font-family, 'Menlo', 'Monaco', 'Consolas', 'Courier New', monospace);
|
||||
background-color: var(--code-background);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
// Ant Design Vue 样式覆盖
|
||||
:where(.css-dev-only-do-not-override-global).ant-tabs {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
:where(.css-dev-only-do-not-override-global).ant-tabs .ant-tabs-tab {
|
||||
color: var(--vscode-description-foreground);
|
||||
}
|
||||
|
||||
:where(.css-dev-only-do-not-override-global).ant-tabs .ant-tabs-tab:hover {
|
||||
color: var(--vscode-tab-hover-foreground, var(--vscode-button-background));
|
||||
}
|
||||
|
||||
:where(.css-dev-only-do-not-override-global).ant-tabs .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||
color: var(--vscode-tab-active-foreground, var(--vscode-button-background));
|
||||
}
|
||||
|
||||
:where(.css-dev-only-do-not-override-global).ant-tabs .ant-tabs-ink-bar {
|
||||
background: var(--vscode-tab-active-border, var(--vscode-button-background));
|
||||
}
|
||||
|
||||
:where(.css-dev-only-do-not-override-global).ant-button-text {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
16
webview/src/vite-env.d.ts
vendored
Normal file
16
webview/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
// 声明VSCode API
|
||||
interface VSCodeAPI {
|
||||
postMessage(message: any): void;
|
||||
getState(): any;
|
||||
setState(state: any): void;
|
||||
}
|
||||
|
||||
declare function acquireVsCodeApi(): VSCodeAPI;
|
29
webview/tsconfig.json
Normal file
29
webview/tsconfig.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
10
webview/tsconfig.node.json
Normal file
10
webview/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
31
webview/vite.config.ts
Normal file
31
webview/vite.config.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
base: './',
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server: {
|
||||
port: 3001,
|
||||
strictPort: false,
|
||||
cors: true,
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: `@use "@/styles/_variables.scss" as *;`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user