Initial commit

This commit is contained in:
shunfeng.zhou 2025-04-13 14:22:32 +08:00
commit bf58260ff0
38 changed files with 21692 additions and 0 deletions

8
.cursorignore Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

88
package.json Normal file
View 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
View 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();
}
}

View 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

Binary file not shown.

15
tsconfig.json Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

36
webview/package.json Normal file
View 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
View 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>

View 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

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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); // 使setTimeoutDOM
});
</script>
<style module lang="scss">
.messageList {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
</style>

View 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.vscodeApiacquireVsCodeApiSafe
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 APIWebSocket
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 APISettingsModal');
// 使window.vscodeApiChatViewProvider
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 APIVSCode
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;
// successtrue
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
View 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');

View 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
View 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
};
});

View 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
};
});

View 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);

View 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;
}
}
}

View 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
View 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
View 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" }]
}

View 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
View 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 *;`,
},
},
},
});