diff --git a/src/store/chat.ts b/src/store/chat.ts deleted file mode 100644 index e9df92e..0000000 --- a/src/store/chat.ts +++ /dev/null @@ -1,487 +0,0 @@ -// @ts-ignore - 在构建时忽略模块查找错误 -import { defineStore } from 'pinia'; -// @ts-ignore - 在构建时忽略模块查找错误 -import { ref, reactive, computed } from 'vue'; - -// 使用正确的超时类型声明 -type TimeoutType = ReturnType; - -// 定义消息类型 -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 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 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 { - 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 { - 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(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 { - 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 - }; -}); \ No newline at end of file diff --git a/webview/src/components/ChatPanel.vue b/webview/src/components/ChatPanel.vue index a7eb5e4..058a817 100644 --- a/webview/src/components/ChatPanel.vue +++ b/webview/src/components/ChatPanel.vue @@ -41,9 +41,9 @@ :content="msg.content" :placement="msg.role === 'user' ? 'end' : 'start'" :loading="msg.pending" - :avatar="bubbleRoles[msg.role].avatar" + :avatar="bubbleRoles[msg.role as keyof BubbleRoles].avatar" :variant="msg.role === 'user' ? 'filled' : 'outlined'" - :messageRender="(content) => renderMessageContent(content)" + :messageRender="(content: MessageContent) => renderMessageContent(content)" :styles="{ container: { maxWidth: '85%' @@ -67,14 +67,13 @@ ref="senderRef" v-model:value="inputValue" :placeholder="chatState.currentModel ? `使用 ${chatState.currentModel} 模型对话` : '请先选择AI模型'" - :loading="chatState.loading" :disabled="chatState.connectionStatus !== 'connected'" :auto-size="{ minRows: 1, maxRows: 5 }" submitType="enter" @submit="onSendMessage" @cancel="onCancelGeneration" class="chat-sender" - :key="`sender-${chatState.senderKey || 'default'}`" + :key="`sender-${chatState.senderKey || 'fixed'}`" :actions="renderSenderActions" /> @@ -87,9 +86,10 @@ placeholder="选择模型" size="small" :bordered="false" - style="width: 160px;" + style="min-width: 200px; max-width: 300px; width: auto;" @change="selectModel" @dropdownVisibleChange="onDropdownVisibleChange" + :dropdownMatchSelectWidth="false" >