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 @@
-
-
-
-
- {{ message.content }}
-
-
-
-
-
-
-
-
\ 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
+});