聊天组件优化
This commit is contained in:
parent
5a58d1c298
commit
eae12d45ad
@ -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
|
|
||||||
};
|
|
||||||
});
|
|
@ -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);
|
||||||
|
|
||||||
// 添加sender组件ref
|
// 添加sender组件ref - 注意类型声明,确保能访问组件的方法
|
||||||
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任一状态为true,则认为是loading状态
|
||||||
|
return globalLoading || localLoading || hasPendingMessage;
|
||||||
|
});
|
||||||
|
|
||||||
// 取消生成
|
// 取消生成
|
||||||
function onCancelGeneration() {
|
function onCancelGeneration() {
|
||||||
if (chatState.loading) {
|
if (chatState.loading || localSending.value) {
|
||||||
console.log('用户取消了生成');
|
log('用户取消了生成');
|
||||||
|
|
||||||
// 调用店铺的cancelGeneration方法来取消请求
|
// 重置本地发送状态
|
||||||
|
localSending.value = false;
|
||||||
|
|
||||||
|
// 调用store的cancelGeneration方法来取消请求
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改onSendMessage函数,在消息发送完成后明确强制更新UI
|
||||||
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;
|
||||||
// 使用组件的API而不是直接操作DOM
|
|
||||||
// Sender组件会自动根据:loading属性更新状态
|
|
||||||
// 触发重新渲染
|
|
||||||
if (senderRef.value) {
|
|
||||||
log('使用ref API更新sender组件');
|
|
||||||
// 强制组件重新渲染,通过key或其他Vue特性
|
|
||||||
chatState.senderKey = Date.now(); // 确保在chatState中添加senderKey属性
|
|
||||||
} 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();
|
||||||
|
// 确保actualLoadingState计算属性能够感知到pending状态的消息
|
||||||
|
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;
|
// 使用scrollTo方法,这更符合Vue的风格
|
||||||
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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 不执行消息列表的清空和重新赋值操作,避免触发不必要的重渲染
|
// 消息加载结束后(从loading变为非loading)聚焦输入框
|
||||||
} else {
|
// 只有在所有loading来源都为false时,且之前至少有一个loading源为true时,才聚焦
|
||||||
// 消息加载完成后自动聚焦到输入框
|
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);
|
||||||
|
|
||||||
|
// 使用nextTick确保DOM已更新
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 延迟300ms,确保DOM已渲染
|
|
||||||
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);
|
|
||||||
|
|
||||||
// 每5秒检查一次WebSocket连接状态
|
// 每5秒检查一次WebSocket连接状态
|
||||||
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('输入框已聚焦,但仍将尝试聚焦');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用nextTick确保DOM已更新
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (senderRef.value) {
|
// 只使用组件引用进行聚焦,遵循Vue最佳实践
|
||||||
// 使用Sender组件提供的focus方法
|
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 状态返回不同的按钮组
|
// 只使用组件自带的loading状态,不添加任何自定义样式或DOM操作
|
||||||
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>
|
||||||
|
@ -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) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user