diff --git a/package-lock.json b/package-lock.json index 511fb68..166b1d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index dbd2732..8c592ef 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "yo": "^4.3.1" }, "dependencies": { + "@types/node": "^22.14.1", "ws": "^8.18.1" } } diff --git a/src/components/ChatPanel.vue b/src/components/ChatPanel.vue new file mode 100644 index 0000000..10bf487 --- /dev/null +++ b/src/components/ChatPanel.vue @@ -0,0 +1,108 @@ + try { + // 设置加载状态 + chatState.loading = true; + log('设置loading状态为true,准备发送到WebSocket'); + + // 立即创建一个空的AI响应消息占位符,显示loading状态 + 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); + } \ No newline at end of file diff --git a/src/extension/ChatViewProvider.ts b/src/extension/ChatViewProvider.ts index 4789cee..0706829 100644 --- a/src/extension/ChatViewProvider.ts +++ b/src/extension/ChatViewProvider.ts @@ -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 { - + AI Chat diff --git a/webview/src/components/InputArea.vue b/webview/src/components/InputArea.vue deleted file mode 100644 index 9d82077..0000000 --- a/webview/src/components/InputArea.vue +++ /dev/null @@ -1,137 +0,0 @@ - - - - - \ No newline at end of file diff --git a/webview/src/components/MessageItem.vue b/webview/src/components/MessageItem.vue deleted file mode 100644 index eb554e1..0000000 --- a/webview/src/components/MessageItem.vue +++ /dev/null @@ -1,82 +0,0 @@ - - - - - \ No newline at end of file diff --git a/webview/src/components/MessageList.vue b/webview/src/components/MessageList.vue deleted file mode 100644 index c576c46..0000000 --- a/webview/src/components/MessageList.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - - - \ No newline at end of file diff --git a/webview/src/components/SettingsModal.vue b/webview/src/components/SettingsModal.vue index fa4fdf0..e6a0001 100644 --- a/webview/src/components/SettingsModal.vue +++ b/webview/src/components/SettingsModal.vue @@ -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}`; + + // 从chatStore获取lastInitializedConfig + // 通过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 { + // 需要使用$patch方法更新Pinia状态 + chatStore.$patch((state) => { + if (state.chatState) { + state.chatState.lastInitializedConfig = configFingerprint; + } + }); + console.log('已更新lastInitializedConfig:', configFingerprint); + } catch (err) { + console.error('更新lastInitializedConfig失败:', err); + } } // 延时手动触发重连 diff --git a/webview/src/main.ts b/webview/src/main.ts index e0fd92b..a25b498 100644 --- a/webview/src/main.ts +++ b/webview/src/main.ts @@ -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应用 diff --git a/webview/src/patch.js b/webview/src/patch.js new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/webview/src/patch.js @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webview/src/store/chat.ts b/webview/src/store/chat.ts index b871d3a..38b5e4b 100644 --- a/webview/src/store/chat.ts +++ b/webview/src/store/chat.ts @@ -1,12 +1,22 @@ import { defineStore } from 'pinia'; import { ref, reactive } from 'vue'; +import { v4 } from 'uuid'; // 定义消息类型 export interface ChatMessage { id: string; content: string; - role: 'user' | 'assistant'; - createAt: Date; + role?: 'user' | 'assistant' | 'system'; + timestamp?: number; + createAt?: Date; + pending?: boolean; + error?: boolean; +} + +export interface SendMessageOptions { + messageId?: string; + model?: string; + [key: string]: any; } // WebSocket事件类型 @@ -15,7 +25,8 @@ export enum WebSocketEvent { DISCONNECTED = 'disconnected', MESSAGE = 'message', ERROR = 'error', - MODEL_LIST = 'model_list' + MODEL_LIST = 'model_list', + CHAT_UPDATE = 'chat_update' } // 定义VSCode API接口 @@ -28,25 +39,33 @@ try { console.warn('VSCode API不可用,可能是在浏览器开发环境中运行'); } -// WebSocket服务类 - 浏览器环境下实现 +// 添加常量定义 +const REQUEST_TIMEOUT = 5000; // 请求超时时间(毫秒) + +// WebSocket服务- 浏览器环境下实现 export class WebSocketService { private socket: WebSocket | null = null; - private requestMap = new Map void>(); + private backoffTime = 1000; private nextRequestId = 1; + private requestHandlers: Map void> = new Map(); private reconnectAttempts = 0; - private reconnectTimer: number | null = null; + private reconnectTimer: NodeJS.Timeout | null = null; private readonly MAX_RECONNECT_ATTEMPTS = 5; private readonly RECONNECT_DELAY = 2000; private isReconnecting = false; - private pingInterval: number | null = null; public isConnected = false; + public isLoading = false; + public isWaitingForReply = false; + public lastError: Error | null = null; + public currentModel = ''; // 当前选择的模型 + public messages: ChatMessage[] = []; // 聊天消息历史 private eventListeners: Map void>> = new Map(); constructor(private apiUrl: string, private apiKey: string, private logger: (message: string) => void = console.log) {} /** - * 连接到WebSocket服务器 + * 连接到WebSocket服务 */ public connect(): void { if (this.socket?.readyState === WebSocket.OPEN) { @@ -55,12 +74,12 @@ export class WebSocketService { } try { - this.logger(`[WebSocketService] 尝试连接到: ${this.apiUrl}`); + this.logger(`[WebSocketService] 尝试连接 ${this.apiUrl}`); // 检查URL格式 let wsUrl = this.apiUrl; - // 确保URL以/ws结尾,但避免重复 + // 确保URL以ws结尾,但避免重复 if (!wsUrl.endsWith('/ws')) { if (wsUrl.endsWith('/')) { wsUrl += 'ws'; @@ -76,7 +95,47 @@ export class WebSocketService { } this.logger(`[WebSocketService] 最终WebSocket URL: ${wsUrl}`); + + // 标记是否需要尝试使用127.0.0.1代替localhost + let shouldTryWithIP = false; + + // 检查是否使用了localhost + if (wsUrl.includes('localhost')) { + shouldTryWithIP = true; + } + + // 首先尝试原始URL连接 this.socket = new WebSocket(wsUrl); + + // 设置连接超时,如果3秒内未连接成功,尝试备用方式 + const connectionTimeout = window.setTimeout(() => { + // 检查连接状态 + if (this.socket && this.socket.readyState !== WebSocket.OPEN) { + this.logger('[WebSocketService] 连接超时,尝试备用连接方式'); + + // 关闭当前连接 + try { + this.socket.close(); + } catch (err) { + // 忽略关闭错误 + } + + // 如果使用了localhost,尝试使用127.0.0.1替换 + if (shouldTryWithIP) { + const ipUrl = wsUrl.replace('localhost', '127.0.0.1'); + this.logger(`[WebSocketService] 尝试使用IP地址替代localhost: ${ipUrl}`); + + // 创建新的WebSocket连接 + this.socket = new WebSocket(ipUrl); + this.setupEventHandlers(); + } + } + }, 3000); + + // 保存连接超时处理器,以便在setupEventHandlers中清除 + (this.socket as any)._connectionTimeout = connectionTimeout; + + // 设置事件处理器 this.setupEventHandlers(); } catch (error) { this.logger(`[WebSocketService] 连接失败: ${error}`); @@ -143,10 +202,16 @@ export class WebSocketService { */ private setupEventHandlers(): void { if (!this.socket) { - this.logger('[WebSocketService] 无法设置事件处理器: socket为null'); + this.logger('[WebSocketService] 无法设置事件处理器 socket为null'); return; } + // 清除连接超时 + if ((this.socket as any)._connectionTimeout) { + window.clearTimeout((this.socket as any)._connectionTimeout); + delete (this.socket as any)._connectionTimeout; + } + // 记录连接建立时间 let connectionEstablishedTime = 0; @@ -159,14 +224,10 @@ export class WebSocketService { // 延迟重置重连计数器,确保连接稳定 setTimeout(() => { if (this.isConnected) { - this.reconnectAttempts = 0; this.logger('[WebSocketService] 连接保持稳定,重置重连计数器'); } }, 5000); - // 设置定时ping保持连接 - this.setupPingInterval(); - this.emit(WebSocketEvent.CONNECTED); this.logger('[WebSocketService] 已触发CONNECTED事件'); @@ -185,59 +246,183 @@ export class WebSocketService { this.socket.onmessage = (event: MessageEvent) => { try { const data = event.data; + // 检查是否是HTTP错误响应 const stringData = typeof data === 'string' ? data : data.toString(); + // 完整记录服务器响应,确保能看到全部内容 + this.logger(`WebSocket完整响应原始数据(长度: ${stringData.length}):\n${stringData}`); + // 尝试解析JSON let response; try { response = JSON.parse(stringData); + this.logger(`成功解析WebSocket响应为JSON: ${JSON.stringify(response, null, 2)}`); } catch (jsonError) { - this.logger(`无法解析JSON响应: ${jsonError}, 原始消息: ${stringData.substring(0, 100)}`); - this.emit(WebSocketEvent.ERROR, `服务器返回的不是有效JSON: ${stringData.substring(0, 50)}...`); + this.logger(`无法解析JSON响应: ${jsonError}, 原始消息: ${stringData}`); + + // 尝试处理非JSON响应(如纯文本响应) + if (typeof stringData === 'string' && stringData.trim().length > 0) { + this.logger(`使用纯文本方式处理响应: ${stringData}`); + + // 查找最新的请求ID + if (this.requestHandlers.size > 0) { + // 使用最大的数字作为最新请求ID + const keys = Array.from(this.requestHandlers.keys()); + let latestRequestId = keys[0]; + for (const key of keys) { + if (key > latestRequestId) { + latestRequestId = key; + } + } + + const callback = this.requestHandlers.get(latestRequestId); + if (callback) { + this.logger(`将纯文本响应传递给请求ID ${latestRequestId}`); + callback(stringData); // 直接将文本传递给回调 + // 处理完成后删除处理器,避免重复 + this.requestHandlers.delete(latestRequestId); + } else { + this.logger(`找到最新请求ID ${latestRequestId},但没有相应的回调处理器`); + } + } else { + this.logger(`没有活跃的请求,无法处理纯文本响应`); + + // 尝试创建通用消息 + const genericMessage: ChatMessage = { + id: `generic-${Date.now()}`, + content: stringData, + role: 'assistant', + timestamp: Date.now() + }; + + // 发送通用消息 + this.emit(WebSocketEvent.MESSAGE, genericMessage); + } + return; + } + + this.emit(WebSocketEvent.ERROR, `服务器返回的不是有效JSON: ${stringData}`); // 自动发送默认模型列表,避免前端卡住 this.emit(WebSocketEvent.MODEL_LIST, this.getDefaultModels()); return; } - this.logger(`收到WebSocket消息: ${JSON.stringify(response)}`); + // 处理错误响应 + if (response.error) { + this.handleErrorResponse(response); + return; + } // 处理模型列表响应(特殊处理) if (response.models && Array.isArray(response.models)) { + this.logger(`收到模型列表响应: ${JSON.stringify(response.models)}`); + // 查找对应的模型列表请求 - let modelRequestId = -1; - for (const [reqId, _] of this.requestMap.entries()) { - if (reqId < 100) { // 假设小ID是模型列表请求 + let modelRequestId = 0; + for (const [reqId, _] of this.requestHandlers.entries()) { + // 尝试通过检查是否为较小的数字来猜测是模型列表请求 + if (reqId < 100) { modelRequestId = reqId; break; } } - if (modelRequestId !== -1) { - const callback = this.requestMap.get(modelRequestId); + if (modelRequestId > 0) { + const callback = this.requestHandlers.get(modelRequestId); if (callback) { + this.logger(`调用模型列表请求处理器: ${modelRequestId}`); callback(response); - this.requestMap.delete(modelRequestId); + this.requestHandlers.delete(modelRequestId); + } else { + this.logger(`找到模型列表请求ID ${modelRequestId},但没有相应的回调处理器`); } + } else { + this.logger(`收到模型列表,但没有找到对应的模型列表请求`); } // 无论找到对应请求与否,都通知模型列表更新 this.emit(WebSocketEvent.MODEL_LIST, response.models); + return; } - // 处理常规请求响应 - 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); + + // 尝试查找响应中的任何文本内容 + const hasTextContent = this.hasAnyTextContent(response); + if (hasTextContent) { + this.logger(`响应中包含文本内容`); + } else { + this.logger(`响应中不包含文本内容: ${JSON.stringify(response)}`); + } + + // 获取请求ID - 只使用标准格式 request_id + const requestId = response.request_id; + + // 处理消息命令的响应 + if (requestId !== undefined) { + // 将请求ID转换为数字 + const numRequestId = typeof requestId === 'string' ? parseInt(requestId, 10) : requestId; + + if (!isNaN(numRequestId) && this.requestHandlers.has(numRequestId)) { + const callback = this.requestHandlers.get(numRequestId); + if (callback) { + // 向处理器传递响应 + this.logger(`调用请求${numRequestId}的回调处理器`); + callback(response); + + // 检查是否是流式响应的最后一个包 + const isLastPacket = + response.stream_finsh === true || // 文档中的拼写错误 + response.stream_finish === true; // 可能的正确拼写 + + if (isLastPacket) { + this.logger(`收到最后一个数据包,移除请求处理器: ${numRequestId}`); + this.requestHandlers.delete(numRequestId); + } + } else { + this.logger(`找到请求ID ${numRequestId},但没有相应的回调处理器`); } + return; } } + + // 如果响应中有文本内容但没有请求ID,创建通用消息 + if (hasTextContent) { + this.logger(`收到没有请求ID的响应,但包含文本内容,作为通用消息处理`); + + // 尝试从响应中提取任何有用的文本 + let content = this.extractResponseContent(response); + + // 创建一个通用消息 + const genericMessage: ChatMessage = { + id: `generic-${Date.now()}`, + content: content, + role: 'assistant', + timestamp: Date.now() + }; + + // 发送通用消息 + this.emit(WebSocketEvent.MESSAGE, genericMessage); + return; + } + + // 其他未识别的响应处理为通用消息 + this.logger(`收到未知类型的WebSocket消息,作为通用消息处理: ${JSON.stringify(response)}`); + + // 尝试从响应中提取任何有用的文本 + let content = this.extractResponseContent(response); + + // 创建一个通用消息 + const genericMessage: ChatMessage = { + id: `generic-${Date.now()}`, + content: content, + role: 'assistant', + timestamp: Date.now() + }; + + // 发送通用消息 + this.emit(WebSocketEvent.MESSAGE, genericMessage); } catch (error) { - this.logger(`解析WebSocket消息失败: ${error}, 原始消息: ${typeof event.data === 'string' ? event.data.substring(0, 100) : '非文本数据'}`); + this.logger(`解析WebSocket消息失败: ${error}, 原始消息: ${typeof event.data === 'string' ? event.data : '非文本数据'}`); // 自动发送默认模型列表,避免前端卡住 this.emit(WebSocketEvent.MODEL_LIST, this.getDefaultModels()); } @@ -249,14 +434,14 @@ export class WebSocketService { // 检查是否包含isTrusted属性 if (event && 'isTrusted' in event && event.isTrusted) { - this.logger('[WebSocketService] 这是一个受信任的错误事件,可能是证书或安全设置导致的'); + this.logger('[WebSocketService] 这是一个受信任的错误事件,可能是证书或安全设置导致'); // 获取更多诊断信息 let diagInfo = ''; // 检查是否是localhost if (this.apiUrl.includes('localhost') || this.apiUrl.includes('127.0.0.1')) { - diagInfo += '本地连接(localhost)可能需要特殊权限; '; + diagInfo += '本地连接(localhost)可能需要特殊权限 '; // 建议使用127.0.0.1而不是localhost if (this.apiUrl.includes('localhost')) { @@ -266,15 +451,15 @@ export class WebSocketService { // 检查是否使用了wss安全连接 if (this.apiUrl.startsWith('wss://')) { - diagInfo += '使用了安全WebSocket连接,请确认服务器证书有效; '; + diagInfo += '使用了安全WebSocket连接,请确认服务器证书有效 '; } else { // 如果使用ws不安全连接,可能是混合内容问题 - diagInfo += 'VS Code中使用不安全WebSocket(ws://)可能受到限制,建议使用安全连接(wss://); '; + diagInfo += 'VS Code中使用不安全WebSocket(ws://)可能受到限制,建议使用安全连接wss://); '; } // 检查WebView上下文 if (typeof window !== 'undefined' && 'acquireVsCodeApi' in window) { - diagInfo += 'WebView环境中可能有额外的安全限制; '; + diagInfo += 'WebView环境中可能有额外的安全限制 '; } const errorMessage = `连接错误(isTrusted): ${diagInfo}请检查服务器配置和网络连接`; @@ -315,164 +500,266 @@ export class WebSocketService { } /** - * 获取模型列表 + * 生成唯一的请求ID */ - public async getModelList(): Promise { - try { - const requestId = this.nextRequestId++; - - // 按照API文档使用list_model命令 - const request = { - request_id: requestId, - cmd: 'list_model' // 使用文档中指定的命令 - }; - - this.logger(`请求模型列表 (ID: ${requestId})`); - - return new Promise((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(); + private generateRequestId(): number { + return this.nextRequestId++; + } + + /** + * 将消息添加到消息列表 + * @param message 聊天消息 + */ + private addMessageToList(message: ChatMessage) { + // 添加消息到列表 + this.messages.push(message); + // 触发消息列表更新事件 + this.emit(WebSocketEvent.CHAT_UPDATE); + } + + /** + * 更新消息列表中的特定消息 + * @param message 要更新的消息 + */ + public updateMessage(message: ChatMessage) { + const index = this.messages.findIndex(msg => msg.id === message.id); + if (index !== -1) { + // 更新消息 + this.messages[index] = message; + // 触发消息列表更新事件 + this.emit(WebSocketEvent.CHAT_UPDATE); + } else { + this.logger(`尝试更新不存在的消息: ${message.id}`); } } /** - * 发送聊天消息 + * 添加请求处理器 + * @param requestId 请求ID + * @param callback 回调函数 */ - public async sendChatMessage(message: string, model: string): Promise { - if (!this.isConnected) { - this.logger('无法发送消息: WebSocket未连接'); + private addRequest(requestId: number, callback: (response: any) => void): void { + this.requestHandlers.set(requestId, callback); + + // 设置超时处理 + setTimeout(() => { + if (this.requestHandlers.has(requestId)) { + this.logger(`请求 ${requestId} 超时,移除处理器`); + this.requestHandlers.delete(requestId); + callback({ error: '请求超时' }); + } + }, REQUEST_TIMEOUT); + } + + /** + * 获取模型列表 + */ + public getModelList(): Promise { + return new Promise((resolve, reject) => { + if (!this.isConnected || !this.socket) { + this.logger('尝试获取模型列表,但WebSocket未连接'); + return reject(new Error('WebSocket未连接')); + } + + const requestId = this.generateRequestId(); + + // 创建符合API文档格式的请求 + const request = { + cmd: 'list_model', + request_id: requestId + }; + + this.logger(`请求模型列表: ${JSON.stringify(request)}`); + + // 添加请求处理器 + this.addRequest(requestId, (response) => { + if (response.error) { + this.logger(`获取模型列表失败: ${response.error}`); + return reject(new Error(`获取模型列表失败: ${response.error}`)); + } + + // 确保模型列表存在且是数组 + if (response.models && Array.isArray(response.models)) { + this.logger(`成功获取模型列表: ${response.models.join(', ')}`); + resolve(response.models); + } else { + this.logger('获取模型列表失败:返回格式不正确'); + reject(new Error('获取模型列表失败:返回格式不正确')); + } + }); + + // 发送请求 + this.socket.send(JSON.stringify(request)); + }); + } + + /** + * 发送聊天消息 + * @param content 消息内容 + * @param modelId 模型ID + * @param conversationId 会话ID + * @returns + */ + public sendChatMessage( + content: string, + modelId?: string, + conversationId?: string + ): Promise { + return new Promise((resolve, reject) => { + // 创建用户消息对象 + const userMsg: ChatMessage = { + id: v4(), + role: "user", + content, + createAt: new Date() + }; + + // 将用户消息添加到消息列表 + this.addMessageToList(userMsg); + + // 创建请求ID并实例化AI响应消息 + const requestId = this.generateRequestId(); + const aiMsg: ChatMessage = { + id: v4(), + role: "assistant", + content: "正在思考中...", + pending: true, + createAt: new Date() + }; + + // 将AI消息添加到消息列表 + this.addMessageToList(aiMsg); + + // 设置超时处理,如果10秒内没有收到真正的响应,重置loading状态 + setTimeout(() => { + if (aiMsg.pending) { + this.logger(`10秒后未收到真正的AI响应,重置loading状态`); + aiMsg.pending = false; + aiMsg.content = "抱歉,未能及时获取到回复,请稍后再试。"; + this.isLoading = false; + this.isWaitingForReply = false; + this.updateMessage(aiMsg); + this.emit(WebSocketEvent.CHAT_UPDATE); + } + }, 10000); + + // 注册响应处理器 + this.requestHandlers.set(requestId, (data: any) => { + try { + this.logger(`收到AI响应: ${JSON.stringify(data)}`); + + // 根据响应更新AI消息 + const responseContent = data.msg || data.message || data.answer || data.content || ""; + + // 确保响应内容不是用户输入的内容副本 + if (responseContent !== content) { + aiMsg.content = responseContent; + aiMsg.pending = false; + + // 如果响应中包含时间戳,则更新createAt + if (data.timestamp) { + aiMsg.createAt = new Date(data.timestamp); + } + + // 更新消息列表 + this.updateMessage(aiMsg); + + // 通知聊天视图刷新 + this.emit(WebSocketEvent.CHAT_UPDATE); + + // 标记加载完成 + this.isLoading = false; + this.isWaitingForReply = false; + } else { + // 如果响应内容与用户输入相同,保持"正在思考中..."状态 + this.logger(`收到的响应内容与用户输入相同,等待真正的AI响应`); + } + + // 解决Promise + resolve(aiMsg); + } catch (error) { + this.logger(`处理AI响应时出错: ${error}`); + aiMsg.content = "处理响应时发生错误"; + aiMsg.error = true; + aiMsg.pending = false; + this.updateMessage(aiMsg); + reject(error); + } + }); + + // 准备并发送请求 + const request = { + cmd: "exec_chat", + request_id: requestId, + msg: content, + model: modelId, + stream: false + }; + + this.logger(`发送聊天请求: ${JSON.stringify(request)}`); + this.sendRequest(request); + }); + } + + /** + * 发送请求到WebSocket服务器 + * @param request 请求对象 + */ + public sendRequest(request: any): void { + if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { + this.logger(`无法发送请求,WebSocket未连接: ${JSON.stringify(request)}`); 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'}`); + try { + // 确保请求有正确的格式 + if (!request.cmd) { + // 如果没有cmd字段,尝试添加 + if (request.msg) { + request.cmd = 'exec_chat'; + } else { + request.cmd = 'unknown'; + } + } + + // 发送JSON格式化的请求 + this.socket.send(JSON.stringify(request)); + this.logger(`已发送请求: ${JSON.stringify(request)}`); + } catch (error) { + this.logger(`发送请求失败: ${error}, 请求: ${JSON.stringify(request)}`); + throw error; + } + } + + /** + * 从响应中提取内容 + */ + private extractResponseContent(response: any): string { + // 提取响应内容 + let content = ''; + + if (typeof response === 'string') { + content = response; + } else if (response && response.msg) { + content = response.msg; + } else if (response && response.content) { + content = response.content; + } else if (response && this.hasAnyTextContent(response)) { + // 检查常用文本字段 + const textFields = ['msg', 'message', 'content', 'text', 'response']; + for (const field of textFields) { + if (typeof response[field] === 'string' && response[field].trim().length > 0) { + content = response[field]; + break; + } + } + } else if (response) { + content = JSON.stringify(response); + } else { + content = '收到空响应'; } - 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((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); - } - }); + return content; } /** @@ -483,42 +770,23 @@ export class WebSocketService { case WebSocket.CONNECTING: return '正在连接 (CONNECTING: 0)'; case WebSocket.OPEN: - return '已连接 (OPEN: 1)'; + return '已连接(OPEN: 1)'; case WebSocket.CLOSING: return '正在关闭 (CLOSING: 2)'; case WebSocket.CLOSED: - return '已关闭 (CLOSED: 3)'; + 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.logger(`达到最大重连尝试次数(${this.MAX_RECONNECT_ATTEMPTS}),停止重连`); this.emit(WebSocketEvent.ERROR, `连接失败:已尝试 ${this.MAX_RECONNECT_ATTEMPTS} 次重连,请检查网络或服务器状态后手动重连`); } return; @@ -530,49 +798,26 @@ export class WebSocketService { // 使用指数退避策略和随机抖动 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秒 + 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); + }, delay) as unknown as NodeJS.Timeout; } /** * 清除所有定时器 */ 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; - } - } - /** * 获取默认模型列表 */ @@ -592,19 +837,120 @@ export class WebSocketService { 1004: '保留', 1005: '未提供状态码', 1006: '异常关闭', - 1007: '数据类型不一致', + 1007: '数据类型不一致,', 1008: '违反策略', 1009: '消息太大', - 1010: '必需的扩展缺失', + 1010: '必需的扩展缺失,', 1011: '内部错误', 1012: '服务重启', 1013: '临时错误', - 1014: '服务器超载', + 1014: '服务器超,', 1015: 'TLS握手失败' }; return explanations[code] || `未知代码: ${code}`; } + + /** + * 检查响应是否包含任何文本内容 + */ + private hasAnyTextContent(response: any): boolean { + if (!response || typeof response !== 'object') { + return false; + } + + // 检查常用文本字段 + const textFields = ['msg', 'message', 'content', 'text', 'response']; + for (const field of textFields) { + if (typeof response[field] === 'string' && response[field].trim().length > 0) { + return true; + } + } + + // 检查任何字符串属性 + for (const key in response) { + if (typeof response[key] === 'string' && response[key].trim().length > 0) { + return true; + } + } + + return false; + } + + // 处理错误响应 + private handleErrorResponse(response: any): void { + // 创建格式化的错误消息 + const formattedError = `服务器错误: ${response.error}`; + this.logger(formattedError); + + // 区分不同类型的错误,但保持连接状态 + // 在文档中,错误通过error字段返回,我们需要区分严重错误和格式错误 + if (response.error && (response.error === 'invalid request format' || response.error === 'unknown cmd')) { + // 对于请求格式错误,只记录但不改变连接状态 + this.logger(`请求格式错误,但连接仍然有效: ${JSON.stringify(response)}`); + } else { + // 其他错误可能是服务器问题或连接问题,触发ERROR事件 + this.emit(WebSocketEvent.ERROR, formattedError); + } + + // 对于模型相关错误,特别处理 + if (response.error && (response.error.includes('model') || response.error.includes('unknown model'))) { + this.logger('检测到模型相关错误,通知前端'); + + const errorMessage: ChatMessage = { + id: `error-model-${Date.now()}`, + content: `错误: 模型 "${response.model || '未知'}" 不可用或不存在,请尝试其他模型`, + role: 'assistant', + timestamp: Date.now() + }; + + this.emit(WebSocketEvent.MESSAGE, errorMessage); + } + + // 对于请求相关的错误,将错误传递给请求处理器 + if (response.request_id !== undefined) { + // 将请求ID转换为number类型 + const numRequestId = typeof response.request_id === 'string' + ? parseInt(response.request_id, 10) + : response.request_id; + + if (!isNaN(numRequestId) && this.requestHandlers.has(numRequestId)) { + const callback = this.requestHandlers.get(numRequestId); + if (callback) { + this.logger(`将错误响应传递给请求处理器: ${numRequestId}`); + callback(response); + // 处理完成后移除处理器 + this.requestHandlers.delete(numRequestId); + } + } else { + this.logger(`错误响应没有有效的请求ID或找不到对应的请求处理器`); + + // 创建一个通用错误消息 + const errorMessage: ChatMessage = { + id: `error-${Date.now()}`, + content: `错误: ${response.error}`, + role: 'assistant', + timestamp: Date.now() + }; + + // 发送错误消息 + this.emit(WebSocketEvent.MESSAGE, errorMessage); + } + } else { + this.logger(`错误响应没有请求ID`); + + // 创建一个通用错误消息 + const errorMessage: ChatMessage = { + id: `error-${Date.now()}`, + content: `错误: ${response.error}`, + role: 'assistant', + timestamp: Date.now() + }; + + // 发送错误消息 + this.emit(WebSocketEvent.MESSAGE, errorMessage); + } + } } // 创建聊天状态管理 @@ -635,12 +981,12 @@ export const useChatStore = defineStore('chat', () => { }); } } catch (error) { - console.error('向VSCode发送日志失败:', error); + console.error('向VSCode发送日志失败', error); } }; - logToVSCode(`初始化WebSocket服务开始: ${apiUrl}`); - console.log(`[ChatStore] 初始化WebSocket服务开始: ${apiUrl}`); + logToVSCode(`初始化WebSocket服务开始 ${apiUrl}`); + console.log(`[ChatStore] 初始化WebSocket服务开始 ${apiUrl}`); try { if (!apiUrl || !apiKey) { @@ -666,11 +1012,20 @@ export const useChatStore = defineStore('chat', () => { // 确保日志同时发送到控制台和VSCode console.log(`[WebSocketService] ${message}`); - if (window.vscodeApi) { - window.vscodeApi.postMessage({ - type: 'log', - message: `[WebSocketService] ${message}` - }); + try { + if (window.vscodeApi) { + window.vscodeApi.postMessage({ + type: 'log', + message: `[WebSocketService] ${message}` + }); + } else if (vscode) { + vscode.postMessage({ + type: 'log', + message: `[WebSocketService] ${message}` + }); + } + } catch (err) { + console.error('向VSCode发送日志失败', err); } }); @@ -717,7 +1072,7 @@ export const useChatStore = defineStore('chat', () => { // 设置WebSocket事件监听 function setupWebSocketEvents() { if (!wsService.value) { - console.error('[ChatStore] setupWebSocketEvents失败: wsService为null'); + console.warn('[ChatStore] WebSocket服务未初始化,无法设置事件监听'); return; } @@ -743,19 +1098,95 @@ export const useChatStore = defineStore('chat', () => { }); wsService.value.on(WebSocketEvent.MESSAGE, (message: ChatMessage) => { - console.log('[ChatStore] 收到WebSocket MESSAGE事件'); - // 查找是否已存在具有相同ID的消息 - const existingIndex = chatState.messages.findIndex(m => m.id === message.id); + console.log('[ChatStore] 收到WebSocket MESSAGE事件', message); + + // 检查是否是用户消息,如果是则需要检查是否已存在完全相同的内 + if (message.role === 'user') { + // 对于用户消息,检查是否已经存在相同内容的用户消息 + const existingSameUser = chatState.messages.find(m => + m.role === 'user' && m.content === message.content + ); + + if (existingSameUser) { + console.log('[ChatStore] 跳过添加已存在的用户消息', message.content); + return; // 直接退出,不处理重复的用户消息 + } + } + + // 检查是否是助手的最终消息(包含stream_finish标记) + const isAssistantFinalMessage = + message.role === 'assistant' && + (message.content.includes('"stream_finish":true') || + message.content.includes('"stream_finsh":true') || + message.content.includes('无响应内容')); + + // 跟踪是否需要添加消息 + let shouldAddMessage = true; + + // 查找是否已存在具有相同ID前缀的消息 + const existingIndex = chatState.messages.findIndex(m => { + // 对于助手消息,我们只需要检查id的前缀部分(不包含流序列号) + if (message.role === 'assistant' && m.role === 'assistant') { + // 如果是响应消息,可能是response-123的形式 + if (message.id.startsWith('response-') && m.id.startsWith('response-')) { + // 提取基本ID部分 + const idParts = message.id.split('-'); + const existingIdParts = m.id.split('-'); + + // 如果基本格式相同(response-数字),认为是同一消息的更新 + if (idParts.length >= 2 && existingIdParts.length >= 2) { + return idParts[1] === existingIdParts[1]; + } + } + } + + // 对于用户消息检查完整ID + return m.id === message.id; + }); + + // 检查消息是否跟上一条消息内容相同 + if (existingIndex === -1 && chatState.messages.length > 0) { + const lastMessage = chatState.messages[chatState.messages.length - 1]; + + // 如果消息内容相同且角色相同,这可能是重复触发 + if (lastMessage.role === message.role && lastMessage.content === message.content) { + console.log('[ChatStore] 跳过添加与上一条消息内容完全相同的消息'); + shouldAddMessage = false; + } + } + + // 防止添加完全空白的消息 + if (message.content.trim() === '' && message.role === 'assistant') { + // 仅当不是现有消息的更新时跳过 + if (existingIndex === -1) { + console.log('[ChatStore] 跳过添加空内容的助手消息'); + shouldAddMessage = false; + } + } if (existingIndex >= 0) { // 更新现有消息 - chatState.messages[existingIndex] = message; - } else { + console.log('[ChatStore] 更新现有消息', { + id: chatState.messages[existingIndex].id, + oldLength: chatState.messages[existingIndex].content.length, + newLength: message.content.length + }); + + // 只有当新消息内容更长或是最终消息时才更新 + if (message.content.length > chatState.messages[existingIndex].content.length || isAssistantFinalMessage) { + chatState.messages[existingIndex] = message; + } + } else if (shouldAddMessage) { // 添加新消息 + console.log('[ChatStore] 添加新消息', message); chatState.messages.push(message); } - chatState.loading = false; + // 如果是助手的最终消息,重置加载状态 + if (isAssistantFinalMessage) { + console.log('[ChatStore] 收到助手最终消息,重置加载状态'); + chatState.loading = false; + } }); wsService.value.on(WebSocketEvent.MODEL_LIST, (models: string[]) => { @@ -772,35 +1203,82 @@ export const useChatStore = defineStore('chat', () => { } }); + wsService.value.on(WebSocketEvent.CHAT_UPDATE, () => { + console.log('[ChatStore] 收到WebSocket CHAT_UPDATE事件'); + + // 从WebSocketService中获取最新消息列表 + if (wsService.value && wsService.value.messages.length > 0) { + chatState.messages = [...wsService.value.messages]; + + // 检查最新的助手消息是否不再处于pending状态 + const lastAssistantMsg = chatState.messages + .filter(msg => msg.role === 'assistant') + .pop(); + + if (lastAssistantMsg && !lastAssistantMsg.pending) { + console.log('[ChatStore] 检测到助手消息非pending状态,重置loading状态'); + chatState.loading = false; + } + } + }); + console.log('[ChatStore] WebSocket事件监听器设置完成'); } // 发送消息 - async function sendMessage(content: string) { - console.log('Store: 开始发送消息', { content, connectionStatus: chatState.connectionStatus }); + async function sendMessage(content: string): Promise { + // 记录函数调用到VSCode日志 + const logToVSCode = (message: string) => { + try { + if (window.vscodeApi) { + window.vscodeApi.postMessage({ + type: 'log', + message: `[ChatStore] ${message}` + }); + } + } catch (error) { + console.error('向VSCode发送日志失败', error); + } + }; - if (!wsService.value || !wsService.value.isConnected) { - console.error('Store: WebSocket未连接,无法发送消息'); + console.log('[ChatStore] 开始发送消息', { content, connectionStatus: chatState.connectionStatus }); + logToVSCode(`开始发送消息 ${content.substring(0, 30)}... (连接状态 ${chatState.connectionStatus})`); + + if (!wsService.value) { + console.error('[ChatStore] WebSocket服务未初始化,无法发送消息'); + logToVSCode('WebSocket服务未初始化,无法发送消息'); + chatState.errorMessage = 'WebSocket服务未初始化,请刷新页面重试'; + chatState.connectionStatus = 'error'; + throw new Error('WebSocket服务未初始化'); + } + + if (!wsService.value.isConnected) { + console.error('[ChatStore] WebSocket未连接,无法发送消息', wsService.value); + logToVSCode(`WebSocket未连接,无法发送消息(isConnected: ${wsService.value.isConnected})`); chatState.errorMessage = '未连接到AI服务,请先连接'; chatState.connectionStatus = 'error'; throw new Error('WebSocket未连接'); } - if (!content.trim() || chatState.loading) { - console.log('Store: 消息为空或正在加载中,不发送'); - return; + if (!content.trim()) { + console.log('[ChatStore] 消息为空,不发送'); + logToVSCode('消息为空,不发送'); + return ''; } // 确保有当前模型 if (!chatState.currentModel && chatState.availableModels.length > 0) { - console.log('Store: 自动选择模型', chatState.availableModels[0]); + console.log('[ChatStore] 自动选择模型', chatState.availableModels[0]); + logToVSCode(`自动选择模型: ${chatState.availableModels[0]}`); chatState.currentModel = chatState.availableModels[0]; } else if (!chatState.currentModel) { - console.error('Store: 没有可用模型'); + console.error('[ChatStore] 没有可用模型'); + logToVSCode('没有可用模型'); throw new Error('没有可用的AI模型'); } - console.log(`Store: 准备发送消息到WebSocket,使用模型: ${chatState.currentModel}`); + console.log(`[ChatStore] 准备发送消息到WebSocket,使用模型 ${chatState.currentModel}`); + logToVSCode(`准备发送消息到WebSocket,使用模型 ${chatState.currentModel}`); chatState.loading = true; try { @@ -808,16 +1286,32 @@ export const useChatStore = defineStore('chat', () => { const startTime = Date.now(); // 直接发送消息到WebSocket服务 - const response = await wsService.value.sendChatMessage(content, chatState.currentModel); + console.log('[ChatStore] 调用 wsService.sendChatMessage...'); + logToVSCode('调用 wsService.sendChatMessage开始'); + + if (typeof wsService.value.sendChatMessage !== 'function') { + console.error('[ChatStore] wsService.sendChatMessage不是一个函数', wsService.value); + logToVSCode(`wsService.sendChatMessage不是一个函数 ${JSON.stringify(wsService.value)}`); + throw new Error('WebSocket服务方法不可用'); + } + + logToVSCode('调用sendChatMessage直接...'); + wsService.value.sendChatMessage(content, chatState.currentModel); + logToVSCode('sendChatMessage调用完成,已发送消息'); + + // 不立即重置loading状态,等待真正的响应通过事件更新 + // 响应处理已经在WebSocketService中注册,会自动更新aiMsg // 计算响应时间 const responseTime = Date.now() - startTime; - console.log(`Store: 收到WebSocket响应,耗时: ${responseTime}ms`); + console.log(`[ChatStore] 消息已发送,等待服务器响应,发送耗时: ${responseTime}ms`); + logToVSCode(`消息已发送,等待服务器响应,发送耗时: ${responseTime}ms,内容前50个字符 ${content.substring(0, 50)}`); - return response; + return content; } catch (error) { - console.error('Store: 发送消息失败:', error); - chatState.errorMessage = `发送消息失败: ${error instanceof Error ? error.message : '未知错误'}`; + console.error('[ChatStore] 发送消息失败', error); + logToVSCode(`发送消息失败 ${error instanceof Error ? error.message : '未知错误'}`); + chatState.errorMessage = `发送消息失败 ${error instanceof Error ? error.message : '未知错误'}`; chatState.connectionStatus = 'error'; chatState.loading = false; throw error; @@ -873,4 +1367,4 @@ export const useChatStore = defineStore('chat', () => { clearMessages, reconnect }; -}); \ No newline at end of file +});