初步实现正常聊天

This commit is contained in:
shunfeng.zhou 2025-04-14 23:22:12 +08:00
parent bc69a9964e
commit 1fafb3a2bf
15 changed files with 2320 additions and 747 deletions

9
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "vscode-ai-chat-plugin",
"version": "0.0.1",
"dependencies": {
"@types/node": "^22.14.1",
"ws": "^8.18.1"
},
"devDependencies": {
@ -1016,10 +1017,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.14.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
"dev": true,
"version": "22.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@ -11219,7 +11219,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/unicorn-magic": {

View File

@ -83,6 +83,7 @@
"yo": "^4.3.1"
},
"dependencies": {
"@types/node": "^22.14.1",
"ws": "^8.18.1"
}
}

View File

@ -0,0 +1,108 @@
try {
//
chatState.loading = true;
log('设置loading状态为true准备发送到WebSocket');
// AIloading
const placeholderMessage: ChatMessage = {
id: currentGenerationId,
content: '',
role: 'assistant',
createAt: new Date()
};
chatState.messages.push(placeholderMessage);
log('添加了AI响应占位消息', placeholderMessage);
//
nextTick(() => {
scrollToBottom();
log('添加占位符后再次滚动到底部');
});
//
let loadingTimeout = setTimeout(() => {
if (chatState.loading) {
log('响应超时自动重置loading状态');
chatState.loading = false;
//
const timeoutIndex = chatState.messages.findIndex((m: ChatMessage) => m.id === currentGenerationId);
if (timeoutIndex !== -1) {
chatState.messages[timeoutIndex].content = '响应超时,请稍后再试或检查服务器状态';
log('更新占位消息为超时提示');
} else {
//
const timeoutMessage: ChatMessage = {
id: `timeout-${Date.now()}`,
content: '响应超时,请稍后再试或检查服务器状态',
role: 'assistant',
createAt: new Date()
};
chatState.messages.push(timeoutMessage);
log('添加了超时提示消息');
}
//
nextTick(() => {
scrollToBottom();
});
}
}, 15000);
//
const sortedMessages = [...chatState.messages].sort((a: ChatMessage, b: ChatMessage) =>
a.createAt.getTime() - b.createAt.getTime()
);
//
const assistantMessages = sortedMessages.filter((m: ChatMessage) => m.role === 'assistant');
const latestAssistantMessage = assistantMessages.length > 0
? assistantMessages[assistantMessages.length - 1]
: null;
log('最新的助手消息:', latestAssistantMessage?.id);
//
const userMessageContents = new Set();
//
sortedMessages.forEach((message: ChatMessage) => {
const isAIMessage = message.role === 'assistant';
const isLatestAIMessage = isAIMessage && latestAssistantMessage && message.id === latestAssistantMessage.id;
//
if (message.content.trim() === '' && !isLatestAIMessage) {
return;
}
//
if (message.role === 'user') {
//
if (userMessageContents.has(message.content)) {
log('跳过重复的用户消息:', message.content.substring(0, 20));
return;
}
//
userMessageContents.add(message.content);
// 使key
const userKey = `user-${message.content}`;
const bubbleMsg = {
key: message.id,
role: message.role as 'user' | 'assistant',
content: message.content || '',
loading: false,
timestamp: message.createAt ? message.createAt.toISOString() : new Date().toISOString()
};
uniqueMessagesMap.set(userKey, bubbleMsg);
return;
}
// ... existing code ...
});
} catch (error) {
console.error('发送消息时出错:', error);
}

View File

@ -49,24 +49,32 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
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}`);
// 根据消息来源分类日志
if (message.message) {
// ChatPanel组件日志处理
if (message.message.includes('[ChatPanel]')) {
this._logger(`📱 ChatPanel日志: ${message.message}`);
}
// 为SettingsModal相关日志添加特殊标记
else if (message.message.includes('SettingsModal')) {
this._logger(`⚙️ SettingsModal日志: ${message.message}`);
}
// 为WebSocket连接相关日志添加更详细的输出
else if (message.message.includes('WebSocket') ||
message.message.includes('ChatStore') ||
message.message.includes('initWebSocketService')
) {
this._logger(`🔌 WebSocket日志: ${message.message}`);
}
// 为重要日志添加特殊标记
else if (message.message.includes('【重要】')) {
this._logger(`⚠️ ${message.message}`);
}
else {
this._logger(`WebView日志: ${message.message}`);
}
} else {
this._logger(`WebView日志: ${JSON.stringify(message)}`);
}
break;
case 'error':
@ -357,7 +365,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
<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}';">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src ws://localhost:8080; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';">
<title>AI Chat</title>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();

487
src/store/chat.ts Normal file
View File

@ -0,0 +1,487 @@
// @ts-ignore - 在构建时忽略模块查找错误
import { defineStore } from 'pinia';
// @ts-ignore - 在构建时忽略模块查找错误
import { ref, reactive, computed } from 'vue';
// 使用正确的超时类型声明
type TimeoutType = ReturnType<typeof setTimeout>;
// 定义消息类型
export interface ChatMessage {
id: string;
content: string;
role: 'user' | 'assistant' | 'system';
timestamp: number;
}
export interface SendMessageOptions {
messageId?: string;
model?: string;
[key: string]: any;
}
// WebSocket事件类型
export enum WebSocketEvent {
CONNECTED = 'connected',
DISCONNECTED = 'disconnected',
MESSAGE = 'message',
ERROR = 'error',
MODEL_LIST = 'model_list'
}
// 定义VSCode API接口
let vscode: any = undefined;
// @ts-ignore
try {
// @ts-ignore
vscode = acquireVsCodeApi();
} catch (error) {
// 在开发环境下可能没有VSCode API
console.warn('VSCode API不可用可能是在浏览器开发环境中运行');
}
// WebSocket服务类 - 浏览器环境下实现
export class WebSocketService {
private socket: WebSocket | null = null;
private requestMap = new Map<string, (response: any) => void>();
private nextRequestId = 1;
private reconnectAttempts = 0;
private reconnectTimer: TimeoutType | null = null;
private readonly MAX_RECONNECT_ATTEMPTS = 5;
private readonly RECONNECT_DELAY = 2000;
private isReconnecting = false;
private pingInterval: TimeoutType | 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}api_key=${this.apiKey}`;
}
this.logger(`[WebSocketService] 最终WebSocket URL: ${wsUrl}`);
this.socket = new WebSocket(wsUrl);
this.setupEventHandlers();
} catch (error) {
this.logger(`[WebSocketService] 连接失败: ${error}`);
this.emit(WebSocketEvent.ERROR, `连接失败: ${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
* @param message
* @param opts
* @returns Promise
*/
sendChatMessage(message: string, opts: SendMessageOptions = {}): Promise<string> {
return new Promise((resolve, reject) => {
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
this.logger('[WebSocketService] WebSocket not connected, cannot send message');
return reject('WebSocket未连接');
}
const requestId = Date.now().toString();
// 创建符合API文档格式的请求
const request = {
cmd: 'exec_chat',
request_id: requestId,
msg: message,
model: opts.model || '',
stream: false // 目前不支持流式响应
};
this.logger(`[WebSocketService] Sending message: ${JSON.stringify(request)}`);
// 注册请求处理函数
this.registerRequestHandler(requestId, (response) => {
if (response.error) {
this.logger(`[WebSocketService] Chat error: ${response.error}`);
reject(response.error);
} else if (response.reply !== undefined) {
// 发送AI回复到消息列表
this.emit(WebSocketEvent.MESSAGE, {
id: `ai-${Date.now()}`,
content: response.reply,
role: 'assistant',
timestamp: Date.now()
});
resolve(response.reply);
} else {
this.logger('[WebSocketService] Invalid chat response format');
reject('无效的聊天响应格式');
}
});
// 发送请求
this.socket.send(JSON.stringify(request));
// 发送用户消息到消息列表
this.emit(WebSocketEvent.MESSAGE, {
id: opts.messageId || `user-${Date.now()}`,
content: message,
role: 'user',
timestamp: Date.now()
});
});
}
/**
*
* @returns Promise
*/
getModelList(): Promise<string[]> {
return new Promise((resolve, reject) => {
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
this.logger('[WebSocketService] WebSocket not connected, cannot get model list');
return reject('WebSocket未连接');
}
const requestId = Date.now().toString();
// 创建符合API文档格式的请求
const request = {
cmd: 'list_model',
request_id: requestId
};
this.logger(`[WebSocketService] Requesting model list: ${JSON.stringify(request)}`);
// 注册请求处理函数
this.registerRequestHandler(requestId, (response) => {
if (response.error) {
this.logger(`[WebSocketService] Model list error: ${response.error}`);
reject(response.error);
} else if (response.models && Array.isArray(response.models)) {
this.logger(`[WebSocketService] Got model list: ${response.models.join(', ')}`);
resolve(response.models);
} else {
this.logger('[WebSocketService] Invalid model list response format');
reject('无效的模型列表响应格式');
}
});
// 发送请求
this.socket.send(JSON.stringify(request));
});
}
/**
*
*/
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++;
this.reconnectTimer = setTimeout(() => {
this.isReconnecting = false;
this.connect();
}, 2000);
}
/**
*
*/
private clearTimers(): void {
if (this.pingInterval !== null) {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
if (this.reconnectTimer !== null) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
/**
* WebSocket事件处理器
*/
private setupEventHandlers(): void {
if (!this.socket) {
this.logger('[WebSocketService] 无法设置事件处理器: socket为null');
return;
}
this.socket.onopen = () => {
this.logger('[WebSocketService] WebSocket连接已建立成功');
this.isConnected = true;
this.emit(WebSocketEvent.CONNECTED);
};
this.socket.onmessage = (event) => {
this.logger(`[WebSocketService] 收到消息: ${event.data}`);
try {
const data = JSON.parse(event.data);
// 检查是否有request_id
if (data.request_id) {
// 查找这个请求是否有注册的回调处理函数
const handler = this.requestMap.get(data.request_id);
if (handler) {
handler(data);
this.requestMap.delete(data.request_id);
return;
}
}
// 如果没有request_id或没有找到对应的处理函数才作为普通消息广播
// 防止重复处理消息
this.logger('[WebSocketService] 收到未处理的消息,作为通用消息广播');
this.emit(WebSocketEvent.MESSAGE, data);
} catch (error) {
console.error('解析WebSocket消息失败:', error);
}
};
this.socket.onerror = () => {
this.logger('[WebSocketService] WebSocket错误');
this.emit(WebSocketEvent.ERROR, '连接错误');
this.scheduleReconnect();
};
this.socket.onclose = () => {
this.logger('[WebSocketService] WebSocket连接关闭');
this.isConnected = false;
this.clearTimers();
this.emit(WebSocketEvent.DISCONNECTED);
this.scheduleReconnect();
};
}
/**
*
* @param requestId ID
* @param handler
*/
private registerRequestHandler(requestId: string, handler: (response: any) => void): void {
this.requestMap.set(requestId, handler);
// 设置超时处理,避免请求没有响应导致内存泄漏
setTimeout(() => {
if (this.requestMap.has(requestId)) {
this.logger(`[WebSocketService] 请求 ${requestId} 超时,移除处理函数`);
this.requestMap.delete(requestId);
}
}, 30000); // 30秒超时
}
}
// 创建聊天状态管理
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) {
if (wsService.value) {
wsService.value.disconnect();
}
wsService.value = new WebSocketService(apiUrl, apiKey);
wsService.value.on(WebSocketEvent.CONNECTED, () => {
chatState.connectionStatus = 'connected';
chatState.errorMessage = '';
// 当连接建立后,自动获取模型列表
refreshModels();
});
wsService.value.on(WebSocketEvent.DISCONNECTED, () => {
chatState.connectionStatus = 'disconnected';
});
wsService.value.on(WebSocketEvent.ERROR, (error: string) => {
chatState.errorMessage = error;
chatState.connectionStatus = 'error';
});
wsService.value.on(WebSocketEvent.MESSAGE, (message: ChatMessage) => {
chatState.messages.push(message);
chatState.loading = false;
});
chatState.connectionStatus = 'connecting';
wsService.value.connect();
}
// 发送消息
async function sendMessage(content: string): Promise<string> {
if (!wsService.value) {
throw new Error('WebSocket服务未初始化');
}
if (!content.trim()) {
return '';
}
chatState.loading = true;
try {
// 不需要手动添加消息到聊天历史WebSocketService会通过事件发送
const response = await wsService.value.sendChatMessage(content, { model: chatState.currentModel });
return response;
} catch (error) {
chatState.errorMessage = `发送消息失败: ${error}`;
throw error;
} finally {
chatState.loading = false;
}
}
// 刷新模型列表
async function refreshModels() {
if (!wsService.value) {
chatState.errorMessage = '无法获取模型列表WebSocket服务未初始化';
return;
}
chatState.loadingModels = true;
try {
const models = await wsService.value.getModelList();
chatState.availableModels = models;
// 如果当前模型不在可用列表中,并且有可用模型,则设置为第一个可用模型
if (models.length > 0 && !models.includes(chatState.currentModel)) {
chatState.currentModel = models[0];
}
} catch (error) {
chatState.errorMessage = `获取模型列表失败: ${error}`;
} finally {
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

@ -10,6 +10,7 @@
"license": "ISC",
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@types/uuid": "^10.0.0",
"ant-design-vue": "^4.2.6",
"ant-design-x-vue": "^1.0.7",
"axios": "^1.6.0",
@ -18,6 +19,7 @@
"marked": "^15.0.7",
"pinia": "^2.1.0",
"utf-8-validate": "^6.0.5",
"uuid": "^11.1.0",
"vue": "^3.3.0",
"vue-router": "^4.5.0"
},
@ -952,6 +954,12 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@ -2452,6 +2460,19 @@
"node": ">=6.14.2"
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vite": {
"version": "4.5.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.11.tgz",

View File

@ -12,6 +12,7 @@
"license": "ISC",
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@types/uuid": "^10.0.0",
"ant-design-vue": "^4.2.6",
"ant-design-x-vue": "^1.0.7",
"axios": "^1.6.0",
@ -20,6 +21,7 @@
"marked": "^15.0.7",
"pinia": "^2.1.0",
"utf-8-validate": "^6.0.5",
"uuid": "^11.1.0",
"vue": "^3.3.0",
"vue-router": "^4.5.0"
},

File diff suppressed because it is too large Load Diff

View File

@ -1,137 +0,0 @@
<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

@ -1,82 +0,0 @@
<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

@ -1,49 +0,0 @@
<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

@ -410,15 +410,46 @@ onMounted(() => {
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服务初始化调用完成'
});
//
const configFingerprint = `${formState.apiHost}:${formState.apiKey}`;
// chatStorelastInitializedConfig
// useChatStore()访Pinia
const chatStateRef = chatStore.$state?.chatState || {};
const lastConfig = chatStateRef.lastInitializedConfig || '';
if (lastConfig === configFingerprint) {
console.log('跳过重复的WebSocket初始化配置完全相同');
if (vscode) {
vscode.postMessage({
type: 'log',
message: '跳过重复的WebSocket初始化配置完全相同'
});
}
} else {
// WebSocket
console.log('调用chatStore.initWebSocketService开始');
chatStore.initWebSocketService(formState.apiHost, formState.apiKey);
console.log('WebSocket服务初始化调用完成');
if (vscode) {
vscode.postMessage({
type: 'log',
message: 'WebSocket服务初始化调用完成'
});
}
// lastInitializedConfig
try {
// 使$patchPinia
chatStore.$patch((state) => {
if (state.chatState) {
state.chatState.lastInitializedConfig = configFingerprint;
}
});
console.log('已更新lastInitializedConfig:', configFingerprint);
} catch (err) {
console.error('更新lastInitializedConfig失败:', err);
}
}
//

View File

@ -23,11 +23,38 @@ let vscodeGlobal = window.vscodeApi; // 直接使用window.vscodeApi它已经
// 记录API状态
if (vscodeGlobal) {
console.log('main.ts: VSCode API已由扩展初始化直接使用');
// 发送初始化成功消息到VSCode
vscodeGlobal.postMessage({
type: 'log',
message: '[主程序] VSCode API初始化成功WebView已加载完成'
});
window.__VSCODE_API_INITIALIZED__ = true;
} else {
console.warn('main.ts: VSCode API未在window中找到');
window.__VSCODE_API_INITIALIZED__ = false;
// 尝试获取VSCode API
try {
if (typeof acquireVsCodeApi === 'function') {
window.vscodeApi = acquireVsCodeApi();
console.log('main.ts: 成功通过acquireVsCodeApi()获取VSCode API');
// 发送到VSCode
window.vscodeApi.postMessage({
type: 'log',
message: '[主程序] 通过acquireVsCodeApi()成功获取VSCode API'
});
window.__VSCODE_API_INITIALIZED__ = true;
} else {
console.error('main.ts: acquireVsCodeApi不是函数');
}
} catch (err) {
console.error('main.ts: 获取VSCode API失败', err);
}
// 处理情况如果在dev环境中提供一个虚拟的VSCode API
// 警告:这只用于开发,不应该在生产环境使用
if (process.env.NODE_ENV === 'development') {
@ -65,9 +92,23 @@ if (vscodeGlobal) {
}
}
// 每5秒检查一次VSCode API状态调试用
setInterval(() => {
console.log('VSCode API状态:', window.vscodeApi ? '可用' : '不可用');
// 每5秒检查一次VSCode API状态并通知VSCode仅前3次
let checkCount = 0;
const apiCheckInterval = setInterval(() => {
const isAvailable = !!window.vscodeApi;
console.log('VSCode API状态:', isAvailable ? '可用' : '不可用');
if (isAvailable && window.vscodeApi) {
window.vscodeApi.postMessage({
type: 'log',
message: `[主程序] VSCode API状态检查 #${checkCount+1}: 可用`
});
}
checkCount++;
if (checkCount >= 3) {
clearInterval(apiCheckInterval);
}
}, 5000);
// 创建Vue应用

1
webview/src/patch.js Normal file
View File

@ -0,0 +1 @@

File diff suppressed because it is too large Load Diff