408 lines
16 KiB
TypeScript
408 lines
16 KiB
TypeScript
// @ts-nocheck
|
||
import { debugLog, traceLog } from './common';
|
||
|
||
export const conversationMethods = {
|
||
// 完整重置所有状态
|
||
resetAllStates(reason = 'unspecified', options: { preserveMonitorWindows?: boolean } = {}) {
|
||
debugLog('重置所有前端状态', { reason, conversationId: this.currentConversationId });
|
||
this.logMessageState('resetAllStates:before-cleanup', { reason });
|
||
this.fileHideContextMenu();
|
||
this.monitorResetVisual({
|
||
preserveBubble: true,
|
||
preservePointer: true,
|
||
preserveWindows: !!options?.preserveMonitorWindows,
|
||
preserveQueue: !!options?.preserveMonitorWindows
|
||
});
|
||
|
||
// 重置消息和流状态
|
||
this.streamingMessage = false;
|
||
this.currentMessageIndex = -1;
|
||
this.stopRequested = false;
|
||
this.taskInProgress = false;
|
||
this.dropToolEvents = false;
|
||
|
||
// 清理工具状态
|
||
this.toolResetTracking();
|
||
|
||
// 新增:将所有未完成的工具标记为已完成
|
||
this.messages.forEach(msg => {
|
||
if (msg.role === 'assistant' && msg.actions) {
|
||
msg.actions.forEach(action => {
|
||
if (action.type === 'tool' &&
|
||
(action.tool.status === 'preparing' || action.tool.status === 'running')) {
|
||
action.tool.status = 'completed';
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// 清理Markdown缓存
|
||
if (this.markdownCache) {
|
||
this.markdownCache.clear();
|
||
}
|
||
this.chatClearThinkingLocks();
|
||
|
||
// 强制更新视图
|
||
this.$forceUpdate();
|
||
|
||
this.inputSetSettingsOpen(false);
|
||
this.inputSetToolMenuOpen(false);
|
||
this.inputSetQuickMenuOpen(false);
|
||
this.modeMenuOpen = false;
|
||
this.inputSetLineCount(1);
|
||
this.inputSetMultiline(false);
|
||
this.inputClearMessage();
|
||
this.inputClearSelectedImages();
|
||
this.inputSetImagePickerOpen(false);
|
||
this.imageEntries = [];
|
||
this.imageLoading = false;
|
||
this.conversationHasImages = false;
|
||
this.toolSetSettingsLoading(false);
|
||
this.toolSetSettings([]);
|
||
|
||
debugLog('前端状态重置完成');
|
||
this._scrollListenerReady = false;
|
||
this.$nextTick(() => {
|
||
this.ensureScrollListener();
|
||
});
|
||
|
||
// 重置已加载对话标记,便于后续重新加载新对话历史
|
||
this.lastHistoryLoadedConversationId = null;
|
||
|
||
this.logMessageState('resetAllStates:after-cleanup', { reason });
|
||
},
|
||
|
||
scheduleResetAfterTask(reason = 'unspecified', options: { preserveMonitorWindows?: boolean } = {}) {
|
||
const start = Date.now();
|
||
const maxWait = 4000;
|
||
const interval = 200;
|
||
const tryReset = () => {
|
||
if (!this.monitorIsLocked || Date.now() - start >= maxWait) {
|
||
this.resetAllStates(reason, options);
|
||
return;
|
||
}
|
||
setTimeout(tryReset, interval);
|
||
};
|
||
tryReset();
|
||
},
|
||
|
||
resetTokenStatistics() {
|
||
this.resourceResetTokenStatistics();
|
||
},
|
||
|
||
// ==========================================
|
||
// 对话管理核心功能
|
||
// ==========================================
|
||
|
||
async loadConversationsList() {
|
||
const queryOffset = this.conversationsOffset;
|
||
const queryLimit = this.conversationsLimit;
|
||
const refreshToken = queryOffset === 0 ? ++this.conversationListRefreshToken : this.conversationListRefreshToken;
|
||
const requestSeq = ++this.conversationListRequestSeq;
|
||
this.conversationsLoading = true;
|
||
try {
|
||
const response = await fetch(`/api/conversations?limit=${queryLimit}&offset=${queryOffset}`);
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
if (refreshToken !== this.conversationListRefreshToken) {
|
||
debugLog('忽略已过期的对话列表响应', {
|
||
requestSeq,
|
||
responseOffset: queryOffset
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (queryOffset === 0) {
|
||
this.conversations = data.data.conversations;
|
||
} else {
|
||
this.conversations.push(...data.data.conversations);
|
||
}
|
||
if (this.currentConversationId) {
|
||
this.promoteConversationToTop(this.currentConversationId);
|
||
}
|
||
this.hasMoreConversations = data.data.has_more;
|
||
debugLog(`已加载 ${this.conversations.length} 个对话`);
|
||
|
||
if (this.conversationsOffset === 0 && !this.currentConversationId && this.conversations.length > 0) {
|
||
const latestConversation = this.conversations[0];
|
||
if (latestConversation && latestConversation.id) {
|
||
await this.loadConversation(latestConversation.id);
|
||
}
|
||
}
|
||
} else {
|
||
console.error('加载对话列表失败:', data.error);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载对话列表异常:', error);
|
||
} finally {
|
||
if (refreshToken === this.conversationListRefreshToken) {
|
||
this.conversationsLoading = false;
|
||
}
|
||
}
|
||
},
|
||
|
||
async loadMoreConversations() {
|
||
if (this.loadingMoreConversations || !this.hasMoreConversations) return;
|
||
|
||
this.loadingMoreConversations = true;
|
||
this.conversationsOffset += this.conversationsLimit;
|
||
await this.loadConversationsList();
|
||
this.loadingMoreConversations = false;
|
||
},
|
||
|
||
async loadConversation(conversationId, options = {}) {
|
||
const force = Boolean(options.force);
|
||
debugLog('加载对话:', conversationId);
|
||
traceLog('loadConversation:start', { conversationId, currentConversationId: this.currentConversationId, force });
|
||
this.logMessageState('loadConversation:start', { conversationId, force });
|
||
this.suppressTitleTyping = true;
|
||
this.titleReady = false;
|
||
this.currentConversationTitle = '';
|
||
this.titleTypingText = '';
|
||
|
||
if (!force && conversationId === this.currentConversationId) {
|
||
debugLog('已是当前对话,跳过加载');
|
||
traceLog('loadConversation:skip-same', { conversationId });
|
||
this.suppressTitleTyping = false;
|
||
this.titleReady = true;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 1. 调用加载API
|
||
const response = await fetch(`/api/conversations/${conversationId}/load`, {
|
||
method: 'PUT'
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
debugLog('对话加载API成功:', result);
|
||
traceLog('loadConversation:api-success', { conversationId, title: result.title });
|
||
|
||
// 2. 更新当前对话信息
|
||
this.skipConversationHistoryReload = true;
|
||
this.currentConversationId = conversationId;
|
||
this.currentConversationTitle = result.title;
|
||
this.titleReady = true;
|
||
this.suppressTitleTyping = false;
|
||
this.startTitleTyping(this.currentConversationTitle, { animate: false });
|
||
this.promoteConversationToTop(conversationId);
|
||
history.pushState({ conversationId }, '', `/${this.stripConversationPrefix(conversationId)}`);
|
||
this.skipConversationLoadedEvent = true;
|
||
|
||
// 3. 重置UI状态
|
||
this.resetAllStates(`loadConversation:${conversationId}`);
|
||
this.subAgentFetch();
|
||
this.fetchTodoList();
|
||
|
||
// 4. 立即加载历史和统计,确保列表切换后界面同步更新
|
||
await this.fetchAndDisplayHistory();
|
||
this.fetchConversationTokenStatistics();
|
||
this.updateCurrentContextTokens();
|
||
traceLog('loadConversation:after-history', {
|
||
conversationId,
|
||
messagesLen: Array.isArray(this.messages) ? this.messages.length : 'n/a'
|
||
});
|
||
|
||
} else {
|
||
console.error('对话加载失败:', result.message);
|
||
this.suppressTitleTyping = false;
|
||
this.titleReady = true;
|
||
this.uiPushToast({
|
||
title: '加载对话失败',
|
||
message: result.message || '服务器未返回成功状态',
|
||
type: 'error'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('加载对话异常:', error);
|
||
traceLog('loadConversation:error', { conversationId, error: error?.message || String(error) });
|
||
this.suppressTitleTyping = false;
|
||
this.titleReady = true;
|
||
this.uiPushToast({
|
||
title: '加载对话异常',
|
||
message: error.message || String(error),
|
||
type: 'error'
|
||
});
|
||
}
|
||
},
|
||
|
||
promoteConversationToTop(conversationId) {
|
||
if (!Array.isArray(this.conversations) || !conversationId) {
|
||
return;
|
||
}
|
||
const index = this.conversations.findIndex(conv => conv && conv.id === conversationId);
|
||
if (index > 0) {
|
||
const [selected] = this.conversations.splice(index, 1);
|
||
this.conversations.unshift(selected);
|
||
}
|
||
},
|
||
|
||
async createNewConversation() {
|
||
debugLog('创建新对话...');
|
||
traceLog('createNewConversation:start', {
|
||
currentConversationId: this.currentConversationId,
|
||
convCount: Array.isArray(this.conversations) ? this.conversations.length : 'n/a'
|
||
});
|
||
this.logMessageState('createNewConversation:start');
|
||
|
||
try {
|
||
const response = await fetch('/api/conversations', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
thinking_mode: this.thinkingMode,
|
||
mode: this.runMode
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
const newConversationId = result.conversation_id;
|
||
debugLog('新对话创建成功:', newConversationId);
|
||
traceLog('createNewConversation:created', { newConversationId });
|
||
|
||
// 在本地列表插入占位,避免等待刷新
|
||
const placeholder = {
|
||
id: newConversationId,
|
||
title: '新对话',
|
||
updated_at: new Date().toISOString(),
|
||
total_messages: 0,
|
||
total_tools: 0
|
||
};
|
||
this.conversations = [
|
||
placeholder,
|
||
...this.conversations.filter(conv => conv && conv.id !== newConversationId)
|
||
];
|
||
|
||
// 直接加载新对话,确保状态一致
|
||
// 如果 socket 事件已把 currentConversationId 设置为新ID,则强制加载一次以同步状态
|
||
await this.loadConversation(newConversationId, { force: true });
|
||
traceLog('createNewConversation:after-load', {
|
||
newConversationId,
|
||
currentConversationId: this.currentConversationId
|
||
});
|
||
|
||
// 刷新对话列表获取最新统计
|
||
this.conversationsOffset = 0;
|
||
await this.loadConversationsList();
|
||
traceLog('createNewConversation:after-refresh', {
|
||
newConversationId,
|
||
conversationsLen: Array.isArray(this.conversations) ? this.conversations.length : 'n/a'
|
||
});
|
||
} else {
|
||
console.error('创建对话失败:', result.message);
|
||
this.uiPushToast({
|
||
title: '创建对话失败',
|
||
message: result.message || '服务器未返回成功状态',
|
||
type: 'error'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('创建对话异常:', error);
|
||
this.uiPushToast({
|
||
title: '创建对话异常',
|
||
message: error.message || String(error),
|
||
type: 'error'
|
||
});
|
||
}
|
||
},
|
||
|
||
async deleteConversation(conversationId) {
|
||
const confirmed = await this.confirmAction({
|
||
title: '删除对话',
|
||
message: '确定要删除这个对话吗?删除后无法恢复。',
|
||
confirmText: '删除',
|
||
cancelText: '取消'
|
||
});
|
||
if (!confirmed) {
|
||
return;
|
||
}
|
||
|
||
debugLog('删除对话:', conversationId);
|
||
this.logMessageState('deleteConversation:start', { conversationId });
|
||
|
||
try {
|
||
const response = await fetch(`/api/conversations/${conversationId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
debugLog('对话删除成功');
|
||
|
||
// 如果删除的是当前对话,清空界面
|
||
if (conversationId === this.currentConversationId) {
|
||
this.logMessageState('deleteConversation:before-clear', { conversationId });
|
||
this.messages = [];
|
||
this.logMessageState('deleteConversation:after-clear', { conversationId });
|
||
this.currentConversationId = null;
|
||
this.currentConversationTitle = '';
|
||
this.resetAllStates(`deleteConversation:${conversationId}`);
|
||
this.resetTokenStatistics();
|
||
history.replaceState({}, '', '/new');
|
||
}
|
||
|
||
// 刷新对话列表
|
||
this.conversationsOffset = 0;
|
||
await this.loadConversationsList();
|
||
|
||
} else {
|
||
console.error('删除对话失败:', result.message);
|
||
this.uiPushToast({
|
||
title: '删除对话失败',
|
||
message: result.message || '服务器未返回成功状态',
|
||
type: 'error'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('删除对话异常:', error);
|
||
this.uiPushToast({
|
||
title: '删除对话异常',
|
||
message: error.message || String(error),
|
||
type: 'error'
|
||
});
|
||
}
|
||
},
|
||
|
||
async duplicateConversation(conversationId) {
|
||
debugLog('复制对话:', conversationId);
|
||
try {
|
||
const response = await fetch(`/api/conversations/${conversationId}/duplicate`, {
|
||
method: 'POST'
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok && result.success) {
|
||
const newId = result.duplicate_conversation_id;
|
||
if (newId) {
|
||
this.currentConversationId = newId;
|
||
}
|
||
|
||
this.conversationsOffset = 0;
|
||
await this.loadConversationsList();
|
||
} else {
|
||
const message = result.message || result.error || '复制失败';
|
||
this.uiPushToast({
|
||
title: '复制对话失败',
|
||
message,
|
||
type: 'error'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('复制对话异常:', error);
|
||
this.uiPushToast({
|
||
title: '复制对话异常',
|
||
message: error.message || String(error),
|
||
type: 'error'
|
||
});
|
||
}
|
||
}
|
||
};
|