聊天组件优化

This commit is contained in:
shunfeng.zhou 2025-04-17 22:19:07 +08:00
parent 5a58d1c298
commit eae12d45ad
3 changed files with 358 additions and 745 deletions

View File

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

View File

@ -41,9 +41,9 @@
:content="msg.content" :content="msg.content"
:placement="msg.role === 'user' ? 'end' : 'start'" :placement="msg.role === 'user' ? 'end' : 'start'"
:loading="msg.pending" :loading="msg.pending"
:avatar="bubbleRoles[msg.role].avatar" :avatar="bubbleRoles[msg.role as keyof BubbleRoles].avatar"
:variant="msg.role === 'user' ? 'filled' : 'outlined'" :variant="msg.role === 'user' ? 'filled' : 'outlined'"
:messageRender="(content) => renderMessageContent(content)" :messageRender="(content: MessageContent) => renderMessageContent(content)"
:styles="{ :styles="{
container: { container: {
maxWidth: '85%' maxWidth: '85%'
@ -67,14 +67,13 @@
ref="senderRef" ref="senderRef"
v-model:value="inputValue" v-model:value="inputValue"
:placeholder="chatState.currentModel ? `使用 ${chatState.currentModel} 模型对话` : '请先选择AI模型'" :placeholder="chatState.currentModel ? `使用 ${chatState.currentModel} 模型对话` : '请先选择AI模型'"
:loading="chatState.loading"
:disabled="chatState.connectionStatus !== 'connected'" :disabled="chatState.connectionStatus !== 'connected'"
:auto-size="{ minRows: 1, maxRows: 5 }" :auto-size="{ minRows: 1, maxRows: 5 }"
submitType="enter" submitType="enter"
@submit="onSendMessage" @submit="onSendMessage"
@cancel="onCancelGeneration" @cancel="onCancelGeneration"
class="chat-sender" class="chat-sender"
:key="`sender-${chatState.senderKey || 'default'}`" :key="`sender-${chatState.senderKey || 'fixed'}`"
:actions="renderSenderActions" :actions="renderSenderActions"
/> />
@ -87,9 +86,10 @@
placeholder="选择模型" placeholder="选择模型"
size="small" size="small"
:bordered="false" :bordered="false"
style="width: 160px;" style="min-width: 200px; max-width: 300px; width: auto;"
@change="selectModel" @change="selectModel"
@dropdownVisibleChange="onDropdownVisibleChange" @dropdownVisibleChange="onDropdownVisibleChange"
:dropdownMatchSelectWidth="false"
> >
<template #prefixIcon> <template #prefixIcon>
<robot-outlined /> <robot-outlined />
@ -113,7 +113,6 @@ import {
UserOutlined, UserOutlined,
LoadingOutlined LoadingOutlined
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import Header from './Header.vue';
import { useChatStore, ChatMessage, WebSocketEvent } from '../store/chat'; import { useChatStore, ChatMessage, WebSocketEvent } from '../store/chat';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -130,26 +129,35 @@ declare global {
} }
} }
//
const logLevel = ref(2);
// VS Code // VS Code
function log(message: string, ...args: any[]) { function log(...args: any[]) {
// //
console.log(`[ChatPanel] ${message}`, ...args); const message = args.join(' ');
// VS Code //
try { const isLowPriorityLog = message.includes('[watchEffect]') ||
if (window.vscodeApi) { message.includes('重置聚焦状态') ||
window.vscodeApi.postMessage({ message.includes('检测到消息列表变化');
//
if (isLowPriorityLog && logLevel.value < 2) {
return;
}
console.log('[ChatPanel]', ...args);
const codeApi = acquireVsCodeApiSafe();
if (codeApi) {
try {
codeApi.postMessage({
type: 'log', type: 'log',
message: `[ChatPanel] ${message} ${args.map(arg => JSON.stringify(arg)).join(' ')}` message: `[ChatPanel] ${args.join(' ')}`
});
} else if (vscode) {
vscode.postMessage({
type: 'log',
message: `[ChatPanel] ${message} ${args.map(arg => JSON.stringify(arg)).join(' ')}`
}); });
} catch (err) {
console.error('向VSCode发送日志失败', err);
} }
} catch (err) {
console.error('发送日志到VS Code失败:', err);
} }
} }
@ -166,15 +174,42 @@ const inputValue = ref('');
// ref // ref
const scrollContainerRef = ref<HTMLElement | null>(null); const scrollContainerRef = ref<HTMLElement | null>(null);
// senderref // senderref - 访
const senderRef = ref<HTMLElement | null>(null); const senderRef = ref<any>(null);
//
const inputFocused = ref(false);
// - chatState
const localSending = ref(false);
// actualLoadingState
const actualLoadingState = computed(() => {
// pending
const hasPendingMessage = chatMessagesForBubble.value.some(msg => msg.pending === true);
// loading
const globalLoading = chatState.loading;
const localLoading = localSending.value;
//
if (logLevel.value > 1) {
log(`计算actualLoadingState - global=${globalLoading}, local=${localLoading}, hasPending=${hasPendingMessage}, 消息数=${chatMessagesForBubble.value.length}`);
}
// trueloading
return globalLoading || localLoading || hasPendingMessage;
});
// //
function onCancelGeneration() { function onCancelGeneration() {
if (chatState.loading) { if (chatState.loading || localSending.value) {
console.log('用户取消了生成'); log('用户取消了生成');
// cancelGeneration //
localSending.value = false;
// storecancelGeneration
const canceled = chatStore.cancelGeneration(); const canceled = chatStore.cancelGeneration();
log('取消请求结果:', canceled ? '成功' : '失败'); log('取消请求结果:', canceled ? '成功' : '失败');
@ -184,9 +219,11 @@ function onCancelGeneration() {
chatStore.setLoading(false); chatStore.setLoading(false);
} }
// // UI
nextTick(() => { nextTick(() => {
focusInputField(); // focusInputField
// UI
log('取消生成后UI已更新');
}); });
} }
} }
@ -199,6 +236,31 @@ interface VSCodeAPI {
// VSCode API // VSCode API
const vscode: VSCodeAPI | null = acquireVsCodeApiSafe(); const vscode: VSCodeAPI | null = acquireVsCodeApiSafe();
// script
interface BubbleRole {
placement: string;
avatar: {
icon: any;
style: {
backgroundColor: string;
color: string;
borderRadius: string;
display: string;
alignItems: string;
justifyContent: string;
fontSize: string;
};
};
}
interface BubbleRoles {
user: BubbleRole;
assistant: BubbleRole;
}
// MessageContent
type MessageContent = string;
// //
const bubbleRoles = { const bubbleRoles = {
user: { user: {
@ -296,181 +358,271 @@ const chatMessagesForBubble = computed(() => {
return result; return result;
}); });
// // checkAndFixLoadingState使Vue
function checkAndFixLoadingState() {
// loading
const isGlobalLoading = chatState.loading;
const isLocalLoading = localSending.value;
const hasPendingMessage = chatMessagesForBubble.value.some(msg => msg.pending === true);
const shouldBeLoading = isGlobalLoading || isLocalLoading || hasPendingMessage;
//
if (shouldBeLoading !== chatState.loading) {
log(`[checkAndFixLoadingState] 状态不一致,修复 - 应该loading=${shouldBeLoading}, 当前=${chatState.loading}`);
chatStore.setLoading(shouldBeLoading);
}
//
if (localSending.value !== (shouldBeLoading && !chatState.loading)) {
localSending.value = shouldBeLoading && !chatState.loading;
}
//
lastLoadingState.value = shouldBeLoading;
lastPendingState.value = hasPendingMessage;
// senderKey
if (shouldBeLoading !== actualLoadingState.value || Date.now() - chatState.senderKey > 5000) {
chatState.senderKey = Date.now();
}
}
// onSendMessageUI
async function onSendMessage(content: string) { async function onSendMessage(content: string) {
try { try {
log('发送消息:', content); log(`[onSendMessage] 开始发送消息,长度=${content.length}`);
// //
if (!content.trim()) { if (!content.trim()) {
log('消息内容为空,不发送');
return; return;
} }
// WebSocket // WebSocket
if (chatState.connectionStatus !== 'connected') { if (chatState.connectionStatus !== 'connected') {
log(`WebSocket连接状态异常: ${chatState.connectionStatus},无法发送消息`);
return; return;
} }
// //
if (!chatState.currentModel) { if (!chatState.currentModel) {
log('没有选择当前模型,无法发送消息');
return; return;
} }
// //
chatState.errorMessage = ''; chatState.errorMessage = '';
// // loading
const sendContext = { inputFocused.value = false;
loading: true,
content,
timestamp: Date.now()
};
// //
chatStore.setLoading(true); localSending.value = true;
// 使 //
nextTick(() => { const messageContent = content;
// 使APIDOM
// Sender:loading
//
if (senderRef.value) {
log('使用ref API更新sender组件');
// keyVue
chatState.senderKey = Date.now(); // chatStatesenderKey
} else {
log('未能获取sender组件ref');
}
});
// //
const userMsg: ChatMessage = { const userMsg: ChatMessage = {
id: generateUUID(), id: generateUUID(),
role: 'user', role: 'user',
content, content: messageContent,
timestamp: new Date().toISOString(), createAt: new Date(),
key: `user-${generateUUID()}`,
pending: false pending: false
}; };
// //
chatStore.addMessageToList(userMsg); chatStore.addMessageToList(userMsg);
log('创建AI消息占位符'); // AI
// AI
const placeHolderAIMsg: ChatMessage = { const placeHolderAIMsg: ChatMessage = {
id: generateUUID(), id: generateUUID(),
role: 'assistant', role: 'assistant',
content: '...', content: '...',
timestamp: new Date().toISOString(), createAt: new Date(),
key: `ai-${generateUUID()}`, pending: true
pending: true //
}; };
// AI // AI
chatStore.addMessageToList(placeHolderAIMsg); chatStore.addMessageToList(placeHolderAIMsg);
//
scrollToBottom();
// //
inputValue.value = ''; inputValue.value = '';
// // loading
await nextTick();
chatStore.setLoading(true);
//
await nextTick();
// actualLoadingStatepending
checkAndFixLoadingState();
log(`[onSendMessage] 更新后状态 - globalLoading=${chatState.loading}, localSending=${localSending.value}, computed=${actualLoadingState.value}`);
//
scrollToBottom();
//
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
log('响应超时提示'); if (localSending.value) {
if (sendContext.loading) { // AI
// AI
const index = chatState.messages.findIndex(m => m.id === placeHolderAIMsg.id); const index = chatState.messages.findIndex(m => m.id === placeHolderAIMsg.id);
if (index !== -1) { if (index !== -1) {
const updatedMsg = { ...chatState.messages[index] }; const updatedMsg = { ...chatState.messages[index] };
updatedMsg.content = '正在生成回复,请稍候...'; updatedMsg.content = '正在生成回复,请稍候...';
updatedMsg.pending = true; // pending
chatStore.updateMessage(index, updatedMsg); chatStore.updateMessage(index, updatedMsg);
} }
} }
}, 5000); }, 5000);
try { try {
log('调用 chatStore.sendChatMessage');
// store // store
await chatStore.sendChatMessage(content, chatState.currentModel, placeHolderAIMsg.id); await chatStore.sendChatMessage(messageContent, chatState.currentModel, placeHolderAIMsg.id);
clearTimeout(timeout); clearTimeout(timeout);
sendContext.loading = false;
// loading //
nextTick(() => { localSending.value = false;
scrollToBottom();
}); //
log(`[onSendMessage] 消息发送成功重置localSending=${localSending.value}`);
//
scrollToBottom();
log('消息发送成功');
return true; return true;
} catch (error) { } catch (error) {
clearTimeout(timeout); clearTimeout(timeout);
sendContext.loading = false;
log('发送消息出错:', error); //
localSending.value = false;
log(`[onSendMessage] 发送失败重置localSending=${localSending.value}`);
// //
const index = chatState.messages.findIndex(m => m.id === placeHolderAIMsg.id); const index = chatState.messages.findIndex(m => m.id === placeHolderAIMsg.id);
if (index !== -1) { if (index !== -1) {
const errorMsg = { ...chatState.messages[index] }; const errorMsg = { ...chatState.messages[index] };
errorMsg.content = `消息发送失败: ${error instanceof Error ? error.message : '未知错误'}`; errorMsg.content = `消息发送失败: ${error instanceof Error ? error.message : '未知错误'}`;
errorMsg.pending = false; errorMsg.pending = false; // pending
chatStore.updateMessage(index, errorMsg); chatStore.updateMessage(index, errorMsg);
} }
return false; return false;
} }
} catch (error) { } catch (error) {
log('onSendMessage函数执行出错:', error); localSending.value = false;
log(`[onSendMessage] 函数执行出错: ${error}`);
return false; return false;
} }
} }
// //
const scrollToBottom = () => { const scrollToBottom = () => {
// 使computedStyle访DOM
nextTick(() => { nextTick(() => {
if (scrollContainerRef.value) { if (scrollContainerRef.value) {
scrollContainerRef.value.scrollTop = scrollContainerRef.value.scrollHeight; // 使scrollToVue
log('滚动容器找到,当前高度:', scrollContainerRef.value.scrollHeight); scrollContainerRef.value.scrollTo({
} else { top: scrollContainerRef.value.scrollHeight,
log('滚动容器未找到'); behavior: 'smooth'
});
} }
}); });
}; };
// //
const lastWatchEffectTime = ref(0);
const lastMessageWatchTime = ref(0);
const lastLoadingState = ref(false);
const lastPendingState = ref(false);
const lastMsgCount = ref(0);
const lastFocusTime = ref(0);
// -
watchEffect(() => { watchEffect(() => {
// chatState.loading // loading
const isLoading = chatState.loading; const isGlobalLoading = chatState.loading;
const isLocalLoading = localSending.value;
const hasPendingMessage = chatMessagesForBubble.value.some(msg => msg.pending === true);
const shouldBeLoading = isGlobalLoading || isLocalLoading || hasPendingMessage;
const messageCount = chatMessagesForBubble.value.length;
log('聊天状态变化:', isLoading ? 'loading' : 'not loading'); //
const stateChanged = shouldBeLoading !== lastLoadingState.value ||
hasPendingMessage !== lastPendingState.value ||
messageCount !== lastMsgCount.value;
// // 300ms
if (isLoading) { const now = Date.now();
// key if (!stateChanged && now - lastWatchEffectTime.value < 300) {
if (!chatState.senderKey || Date.now() - chatState.senderKey > 1000) { return;
chatState.senderKey = Date.now(); }
//
if (stateChanged || now - lastWatchEffectTime.value > 1000) {
lastWatchEffectTime.value = now;
lastLoadingState.value = shouldBeLoading;
lastPendingState.value = hasPendingMessage;
lastMsgCount.value = messageCount;
//
log(`[watchEffect] 状态变化 - global=${isGlobalLoading}, local=${isLocalLoading}, pending=${hasPendingMessage}, computed=${shouldBeLoading}`);
// API
if (shouldBeLoading !== chatState.loading) {
chatStore.setLoading(shouldBeLoading);
log(`[watchEffect] 更新全局loading状态=${shouldBeLoading}`);
} }
// // (loadingloading)
} else { // loadingfalseloadingtrue
// if (!shouldBeLoading && lastLoadingState.value) {
focusInputField(); log('检测到AI回复完成准备聚焦输入框');
// DOM
nextTick(() => {
//
setTimeout(() => {
// inputFocused
inputFocused.value = false;
focusInputField();
}, 100);
});
}
} }
}); });
// // Vue -
watchEffect(() => { watchEffect(() => {
// loading //
if (chatMessagesForBubble.value.length && !chatState.loading) { const messages = chatMessagesForBubble.value;
// DOM
//
const now = Date.now();
if (messages.length === lastMsgCount.value && now - lastMessageWatchTime.value < 300) {
return;
}
//
lastMessageWatchTime.value = now;
lastMsgCount.value = messages.length;
if (messages.length > 0) {
log('检测到消息列表变化,消息数量:', messages.length);
// 使nextTickDOM
nextTick(() => { nextTick(() => {
//
scrollToBottom(); scrollToBottom();
//
// pending
const hasPendingMessage = messages.some(msg => msg.pending);
if (!chatState.loading && !hasPendingMessage && !lastLoadingState.value) {
log('消息渲染完成,自动聚焦输入框');
// DOM
setTimeout(() => {
focusInputField();
}, 100);
}
}); });
} }
}); });
@ -530,20 +682,31 @@ onMounted(() => {
// WebSocket // WebSocket
setupWebSocketMessageListener(); setupWebSocketMessageListener();
//
// //
if (chatState.availableModels.length === 0) { if (chatState.availableModels.length === 0) {
chatStore.updateMessages([]); chatStore.updateMessages([]);
chatStore.$patch({
availableModels: [ // 使
'local qwen2.5-coder:7b', const defaultModels = [
'local codellama:7b', 'local qwen2.5-coder:7b',
'local deepseek-coder:6.7b', 'local codellama:7b',
'Claude Sonnet', 'local deepseek-coder:6.7b',
'GPT-4' 'Claude Sonnet',
] 'GPT-4'
];
// 使chatState$patch
nextTick(() => {
try {
//
chatState.availableModels = defaultModels;
chatState.currentModel = defaultModels[0];
log('设置默认模型列表:', chatState.availableModels);
} catch (err) {
log('设置默认模型失败:', err);
}
}); });
chatStore.setCurrentModel(chatState.availableModels[0]);
log('设置默认模型列表:', chatState.availableModels);
} }
// //
@ -579,44 +742,6 @@ onMounted(() => {
// VSCode // VSCode
window.addEventListener('message', handleVSCodeMessage); window.addEventListener('message', handleVSCodeMessage);
//
const messageObserver = new MutationObserver(mutations => {
//
if (mutations.some(m => m.addedNodes.length > 0)) {
log('检测到DOM变化自动滚动到底部');
scrollToBottom();
}
});
// 300msDOM
setTimeout(() => {
// 使
const container = document.querySelector('.conversationContainer');
if (container) {
log('找到消息容器,设置变化监听');
messageObserver.observe(container, { childList: true, subtree: true });
//
scrollToBottom();
} else {
// 使
log('使用主选择器未找到容器,尝试备用选择器');
const fallbackContainers = document.querySelectorAll('div[class*="conversationContainer"]');
if (fallbackContainers.length > 0) {
log('使用备用选择器找到容器,数量:', fallbackContainers.length);
//
fallbackContainers.forEach(element => {
messageObserver.observe(element, { childList: true, subtree: true });
});
//
scrollToBottom();
} else {
log('错误: 无法找到任何消息容器,无法设置变化监听');
}
}
}, 300);
// 5WebSocket // 5WebSocket
const connectionCheckInterval = setInterval(() => { const connectionCheckInterval = setInterval(() => {
log('定时检查连接状态:', chatState.connectionStatus); log('定时检查连接状态:', chatState.connectionStatus);
@ -630,6 +755,10 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
clearInterval(connectionCheckInterval); clearInterval(connectionCheckInterval);
}); });
// -
log('[Lifecycle] 组件已挂载,初始化检查');
checkAndFixLoadingState();
}); });
// VSCode // VSCode
@ -656,7 +785,12 @@ function handleVSCodeMessage(event: MessageEvent) {
chatStore.initWebSocketService(message.settings.apiHost, message.settings.apiKey); chatStore.initWebSocketService(message.settings.apiHost, message.settings.apiKey);
// //
chatStore.$patch({ lastInitializedConfig: configFingerprint }); try {
//
chatState.lastInitializedConfig = configFingerprint;
} catch (err) {
log('存储配置指纹失败:', err);
}
} else { } else {
log('收到的API配置不完整无法初始化WebSocket连接'); log('收到的API配置不完整无法初始化WebSocket连接');
} }
@ -775,7 +909,7 @@ function formatMessage(content: string): string {
]; ];
for (const response of commonResponses) { for (const response of commonResponses) {
const regex = new RegExp(`(${response})\\s*\\1+`, 'g'); const regex = new RegExp("(" + response + ")\\s*\\1+", 'g');
text = text.replace(regex, '$1'); text = text.replace(regex, '$1');
} }
@ -801,74 +935,74 @@ function formatMessage(content: string): string {
return text; return text;
} }
// //
function renderMessageContent(content: string) { function renderMessageContent(content: MessageContent) {
const formattedContent = formatMessage(content); const formattedContent = formatMessage(content);
return h('div', { innerHTML: formattedContent }); return h('div', { innerHTML: formattedContent });
} }
// - 使API // - 使API
function focusInputField() { function focusInputField() {
// 200ms
const now = Date.now();
if (now - lastFocusTime.value < 200) {
log('输入框最近已聚焦,跳过重复聚焦');
return;
}
//
lastFocusTime.value = now;
//
if (inputFocused.value) {
log('输入框已聚焦,但仍将尝试聚焦');
}
// 使nextTickDOM
nextTick(() => { nextTick(() => {
if (senderRef.value) { // 使Vue
// 使Senderfocus if (senderRef.value && typeof senderRef.value.focus === 'function') {
senderRef.value.focus && senderRef.value.focus({ log('通过组件ref聚焦输入框');
cursor: 'end' try {
}); //
log('使用组件API聚焦到输入框'); inputFocused.value = true;
//
senderRef.value.focus();
// 便
setTimeout(() => {
inputFocused.value = false;
if (logLevel.value > 1) {
log('重置聚焦状态完成');
}
}, 500);
} catch (err) {
log('通过组件ref聚焦失败:', err);
inputFocused.value = false;
}
} else { } else {
log('未能获取sender组件ref'); log('组件ref不可用或没有focus方法');
inputFocused.value = false;
} }
}); });
} }
// a-x-sender // a-x-sender
function renderSenderActions(_: any, info: any) { function renderSenderActions(_: any, info: any) {
const { SendButton, LoadingButton, ClearButton } = info.components; const { SendButton, LoadingButton } = info.components;
// loading // 使loadingDOM
if (chatState.loading) { if (actualLoadingState.value) {
log('渲染 sender 的 loading 按钮状态'); // 使LoadingButton
return ( return h(LoadingButton, {
h('div', { style: { display: 'flex', gap: '8px' } }, [ onClick: onCancelGeneration
h(ClearButton, { });
onClick: () => {
inputValue.value = '';
},
style: {
marginRight: '4px'
}
}),
h(LoadingButton, {
type: 'primary',
onClick: onCancelGeneration,
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}
})
])
);
} }
// return h(SendButton, {
return ( disabled: !_.value?.trim()
h('div', { style: { display: 'flex', gap: '8px' } }, [ });
h(ClearButton, {
onClick: () => {
inputValue.value = '';
},
style: {
marginRight: '4px'
}
}),
h(SendButton, {
type: 'primary',
disabled: !inputValue.value.trim()
})
])
);
} }
// UUID // UUID
@ -885,6 +1019,24 @@ $content-padding: 16px;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
background-color: var(--vscode-editor-background); background-color: var(--vscode-editor-background);
:global(.conversationContainer) {
flex: 1;
overflow-y: auto;
padding: 0;
position: relative;
display: flex;
flex-direction: column;
scroll-behavior: smooth;
}
:global(.message-content) {
white-space: pre-wrap;
word-break: break-word;
line-height: 1.6;
max-width: 100%;
overflow-x: auto;
}
} }
.header { .header {
@ -997,56 +1149,4 @@ $content-padding: 16px;
width: 100%; width: 100%;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
:global(.message-content) {
white-space: pre-wrap;
word-break: break-word;
line-height: 1.6;
max-width: 100%;
overflow-x: auto;
}
:global(.conversationContainer) {
flex: 1;
overflow-y: auto;
padding: 0;
position: relative;
display: flex;
flex-direction: column;
scroll-behavior: smooth;
}
:global(.chat-sender) {
width: 100%;
:global(.ant-spin) {
margin-right: 8px;
}
:global(.a-x-sender-container) {
border-color: var(--vscode-input-border);
background-color: var(--vscode-input-background);
&:focus-within {
border-color: var(--vscode-focusBorder);
}
}
:global(.a-x-sender-input) {
color: var(--vscode-input-foreground);
&::placeholder {
color: var(--vscode-input-placeholderForeground);
}
}
:global(.a-x-sender-button) {
color: var(--vscode-button-foreground);
background-color: var(--vscode-button-background);
&:hover {
background-color: var(--vscode-button-hoverBackground);
}
}
}
</style> </style>

View File

@ -644,7 +644,7 @@ export class WebSocketService {
// 设置超时处理如果10秒内没有收到真正的响应重置loading状态 // 设置超时处理如果10秒内没有收到真正的响应重置loading状态
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
if (aiMsg.pending) { if (aiMsg.pending) {
this.logger(`10秒后未收到真正的AI响应重置loading状态`); this.logger(`5分钟后未收到真正的AI响应重置loading状态`);
aiMsg.pending = false; aiMsg.pending = false;
aiMsg.content = "抱歉,未能及时获取到回复,请稍后再试。"; aiMsg.content = "抱歉,未能及时获取到回复,请稍后再试。";
this.isLoading = false; this.isLoading = false;
@ -652,7 +652,7 @@ export class WebSocketService {
this.updateMessage(aiMsg); this.updateMessage(aiMsg);
this.emit(WebSocketEvent.CHAT_UPDATE); this.emit(WebSocketEvent.CHAT_UPDATE);
} }
}, 10000); }, 300000);
// 注册响应处理器 // 注册响应处理器
this.requestHandlers.set(requestId, (data: any) => { this.requestHandlers.set(requestId, (data: any) => {