agent-Specialization/static/src/app/methods/conversation.ts
JOJO 2e54f2c343 feat(terminal): 实时终端改造为侧边栏面板,支持拖拽分割与自动开关
- 将实时终端从独立页面改造为 Vue3 侧边栏面板,与 Git 面板上下排列
- 使用 xterm.js + Grid 布局,支持三主题配色适配
- 上下拖拽分割条,动态调整终端/Git 面板比例
- 输入栏上方新增终端数量按钮,点击快捷打开
- 个人空间新增「自动打开终端面板」开关(外观与显示分类)
- REST API /api/terminals 获取终端列表,多时机自动刷新
- 有终端自动打开面板,无终端自动关闭
2026-06-12 00:18:27 +08:00

781 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @ts-nocheck
import { debugLog, traceLog } from './common';
import { usePersonalizationStore } from '../../stores/personalization';
export const conversationMethods = {
// 完整重置所有状态
resetAllStates(reason = 'unspecified', options: { preserveMonitorWindows?: boolean } = {}) {
console.log('[DEBUG_AWAITING] ===== resetAllStates =====', { reason });
// 如果正在等待子智能体完成,不重置任务状态
if (this.waitingForSubAgent) {
debugLog('跳过状态重置:正在等待子智能体完成', { reason });
return;
}
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();
// 新增将所有未完成的工具标记为已完成并清理awaitingFirstContent状态
const assistantMsgsBefore = this.messages
.filter((m) => m.role === 'assistant')
.map((m) => ({
awaitingFirstContent: m.awaitingFirstContent,
generatingLabel: m.generatingLabel
}));
this.messages.forEach((msg) => {
if (msg.role === 'assistant') {
// 清理等待动画状态
if (msg.awaitingFirstContent) {
msg.awaitingFirstContent = false;
}
if (msg.generatingLabel) {
msg.generatingLabel = '';
}
// 清理工具状态
if (msg.actions) {
msg.actions.forEach((action) => {
if (
action.type === 'tool' &&
(action.tool.status === 'preparing' || action.tool.status === 'running')
) {
action.tool.status = 'completed';
}
});
}
}
});
const assistantMsgsAfter = this.messages
.filter((m) => m.role === 'assistant')
.map((m) => ({
awaitingFirstContent: m.awaitingFirstContent,
generatingLabel: m.generatingLabel
}));
console.log('[DEBUG_AWAITING] resetAllStates 清理完成', {
before: assistantMsgsBefore,
after: assistantMsgsAfter
});
// 清理Markdown缓存
if (this.markdownCache) {
this.markdownCache.clear();
}
this.chatClearThinkingLocks();
// 强制更新视图
this.$forceUpdate();
this.inputSetSettingsOpen(false);
this.inputSetToolMenuOpen(false);
this.inputSetQuickMenuOpen(false);
this.modeMenuOpen = false;
this.permissionMenuOpen = false;
this.inputSetLineCount(1);
this.inputSetMultiline(false);
this.inputClearMessage();
this.runtimeQueuedMessages = [];
this.runtimeGuidanceFallbackQueue = [];
this.runtimeQueueAutoSendInProgress = false;
this.runtimeQueueSyncLockKey = '';
this.runtimeQueueSyncLockUntil = 0;
if (typeof this.clearRuntimeQueueSuppressionState === 'function') {
this.clearRuntimeQueueSuppressionState();
} else {
this.runtimeQueueSuppressedMessageIds = new Set();
this.runtimeGuidanceSuppressedTextCounts = {};
}
this.composerReservedHeight = 80;
this.inputClearSelectedImages();
this.inputSetImagePickerOpen(false);
this.imageEntries = [];
this.imageLoading = false;
this.conversationHasImages = false;
this.toolSetSettingsLoading(false);
this.toolSetSettings([]);
this.pendingToolApprovals = [];
this.decidingApprovalIds = [];
this.autoApprovalTitle = '自动审批记录';
// 切换对话/工作区/新建视图时,清理上一轮目标模式的本地完成提示。
// 如果目标任务仍在运行,后续 restoreTaskState 会重新恢复运行态。
this.goalModeArmed = false;
this.goalRunning = false;
this.goalProgress = null;
this.goalDialogOpen = false;
debugLog('前端状态重置完成');
this._scrollListenerReady = false;
this._manualScrollSuppressUntil = 0;
this._escapedByUserScroll = false;
this._autoRelockCooldownUntil = 0;
this.$nextTick(() => {
this.ensureScrollListener();
const composerRef = typeof this.getInputComposerRef === 'function' ? this.getInputComposerRef() : null;
if (composerRef && typeof composerRef.emitComposerHeight === 'function') {
composerRef.emitComposerHeight();
}
});
// 重置已加载对话标记,便于后续重新加载新对话历史
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 &&
!this.isExplicitNewConversationRoute()
) {
// 只有在初始化完成后,才自动加载第一个对话
// 避免与 bootstrapRoute 冲突
if (this.initialRouteResolved) {
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);
const preserveListPosition = Boolean(options.preserveListPosition);
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;
}
if ((this.compressionInProgress || this.compressing) && !force) {
const confirmed = await this.confirmAction({
title: '压缩进行中',
message: '对话正在压缩中,切换对话会导致压缩失败,确认要继续吗?',
confirmText: '确认',
cancelText: '取消'
});
if (!confirmed) {
this.suppressTitleTyping = false;
this.titleReady = true;
return;
}
try {
if (this.currentConversationId) {
await fetch(`/api/conversations/${this.currentConversationId}/compression_cancel`, {
method: 'POST'
});
}
} catch (e) {
console.warn('取消压缩失败:', e);
}
this.compressionInProgress = false;
this.compressing = false;
this.compressionMode = '';
this.compressionStage = '';
if (this.compressionToastId) {
this.uiDismissToast(this.compressionToastId);
this.compressionToastId = null;
}
}
// 注意:加载已有对话时必须保留该对话自身的模型/模式,不能套用用户默认值。
// 多工作区并行后,切换对话只切换视图,不再取消后台任务;停止当前轮询避免事件写入新对话界面。
try {
const { useTaskStore } = await import('../../stores/task');
const taskStore = useTaskStore();
if (taskStore.hasActiveTask || this.taskInProgress) {
taskStore.clearTask();
if (typeof this.clearProcessedEvents === 'function') {
this.clearProcessedEvents();
}
}
this.clearLocalTaskUiState?.(`switch-conversation:${conversationId}`);
} catch (error) {
console.error('[切换对话] 停止本地轮询失败:', error);
}
await this.persistComposerDraftNow({
reason: `switch-conversation:${conversationId}`,
force: true,
keepalive: true
}).catch(() => {});
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 });
if (typeof result.run_mode === 'string') {
this.runMode = result.run_mode;
this.thinkingMode =
typeof result.thinking_mode === 'boolean'
? result.thinking_mode
: result.run_mode !== 'fast';
} else if (typeof result.thinking_mode === 'boolean') {
this.thinkingMode = result.thinking_mode;
this.runMode = result.thinking_mode ? 'thinking' : 'fast';
}
if (typeof result.model_key === 'string' && result.model_key) {
this.modelSet(result.model_key);
}
// 2. 更新当前对话信息
this.skipConversationHistoryReload = true;
this.currentConversationId = conversationId;
this.refreshProjectGitSummary?.();
this.fetchTerminalCount();
this.currentConversationTitle = result.title;
this.titleReady = true;
this.suppressTitleTyping = false;
this.startTitleTyping(this.currentConversationTitle, { animate: false });
if (!preserveListPosition) {
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();
await this.refreshRunningWorkspaceTasks?.();
const visibleTask =
typeof this.getVisibleWorkspaceTaskForConversation === 'function'
? this.getVisibleWorkspaceTaskForConversation(conversationId)
: null;
const visibleTaskStatus = String(visibleTask?.status || '');
const activeStatuses = new Set(['pending', 'running', 'cancel_requested']);
const terminalStatuses = new Set(['succeeded', 'failed', 'canceled']);
if (visibleTask && activeStatuses.has(visibleTaskStatus)) {
// 切回正在运行的对话时,必须恢复该任务的 REST 轮询和输入区忙碌态;
// 否则只切换了历史视图,后续输出不会续接到当前对话,发送按钮也不会变成停止按钮。
await this.restoreTaskState?.();
} else if (visibleTask && terminalStatuses.has(visibleTaskStatus)) {
// 用户已点进完成后的“待查看”对话,清除列表里的完成标记。
this.acknowledgeCompletedWorkspaceTask?.(visibleTask.task_id);
}
this.fetchPermissionMode();
this.fetchExecutionMode();
await this.fetchVersioningStatus(conversationId, { silent: true });
await this.maybePromptVersioningMismatch(result.versioning);
this.fetchPendingToolApprovals();
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() {
console.log('[DEBUG_AWAITING] ===== createNewConversation =====');
if (this.compressionInProgress || this.compressing) {
const confirmed = await this.confirmAction({
title: '压缩进行中',
message: '对话正在压缩中,切换对话会导致压缩失败,确认要继续吗?',
confirmText: '确认',
cancelText: '取消'
});
if (!confirmed) {
return;
}
try {
if (this.currentConversationId) {
await fetch(`/api/conversations/${this.currentConversationId}/compression_cancel`, {
method: 'POST'
});
}
} catch (e) {
console.warn('取消压缩失败:', e);
}
this.compressionInProgress = false;
this.compressing = false;
this.compressionMode = '';
this.compressionStage = '';
if (this.compressionToastId) {
this.uiDismissToast(this.compressionToastId);
this.compressionToastId = null;
}
}
debugLog('创建新对话...');
traceLog('createNewConversation:start', {
currentConversationId: this.currentConversationId,
convCount: Array.isArray(this.conversations) ? this.conversations.length : 'n/a'
});
this.logMessageState('createNewConversation:start');
await this.persistComposerDraftNow({
reason: 'create-new-conversation',
force: true,
keepalive: true
}).catch(() => {});
// 应用个性化设置中的默认模型和思考模式
try {
const personalizationStore = usePersonalizationStore();
if (personalizationStore.loaded) {
const defaultRunMode = personalizationStore.form.default_run_mode;
const defaultModel = personalizationStore.form.default_model;
if (defaultRunMode) {
this.runMode = defaultRunMode;
debugLog('应用默认运行模式:', defaultRunMode);
}
if (defaultModel) {
this.currentModelKey = defaultModel;
debugLog('应用默认模型:', defaultModel);
}
// 根据默认运行模式设置思考模式
if (defaultRunMode === 'thinking') {
this.thinkingMode = true;
} else if (defaultRunMode === 'fast') {
this.thinkingMode = false;
}
}
} catch (error) {
console.warn('应用个性化默认设置失败:', error);
}
// 创建新对话只切换视图,不再取消后台任务;停止当前轮询避免事件串写。
try {
const { useTaskStore } = await import('../../stores/task');
const taskStore = useTaskStore();
if (taskStore.hasActiveTask || this.taskInProgress) {
taskStore.clearTask();
if (typeof this.clearProcessedEvents === 'function') {
this.clearProcessedEvents();
}
}
this.clearLocalTaskUiState?.('create-new-conversation');
} catch (error) {
console.error('[创建新对话] 停止本地轮询失败:', error);
}
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;
const waitForAnimation = (durationMs) =>
new Promise((resolve) => {
window.setTimeout(resolve, durationMs);
});
debugLog('新对话创建成功:', newConversationId);
traceLog('createNewConversation:created', { newConversationId });
// 在本地列表插入占位,避免等待刷新
const placeholder = {
id: newConversationId,
title: '新对话',
updated_at: new Date().toISOString(),
total_messages: 0,
total_tools: 0
};
this.conversationInsertAnimations = {
...this.conversationInsertAnimations,
[newConversationId]: 'create'
};
this.conversationListAnimationMode = 'create';
this.conversations = [
placeholder,
...this.conversations.filter((conv) => conv && conv.id !== newConversationId)
];
window.setTimeout(() => {
const rest = { ...this.conversationInsertAnimations };
delete rest[newConversationId];
this.conversationInsertAnimations = rest;
this.conversationListAnimationMode = 'idle';
}, 700);
await this.applyPendingVersioningToConversation(newConversationId);
// 直接加载新对话,确保状态一致
// 如果 socket 事件已把 currentConversationId 设置为新ID则强制加载一次以同步状态
await this.loadConversation(newConversationId, { force: true });
traceLog('createNewConversation:after-load', {
newConversationId,
currentConversationId: this.currentConversationId
});
// 等待顶部插入动画完成,避免刷新列表时打断“整体下移 + 从左到右出现”。
await waitForAnimation(540);
// 刷新对话列表获取最新统计
this.conversationsOffset = 0;
await this.loadConversationsList();
traceLog('createNewConversation:after-refresh', {
newConversationId,
conversationsLen: Array.isArray(this.conversations) ? this.conversations.length : 'n/a'
});
await this.refreshRunningWorkspaceTasks?.();
} 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;
}
const waitForAnimation = (durationMs) =>
new Promise((resolve) => {
window.setTimeout(resolve, durationMs);
});
// 确认弹窗自身有 250ms 退出动画;等弹窗完全消失后,再开始列表删除动画。
await waitForAnimation(270);
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');
}
// 先给目标项加 deleting 类执行左滑动画,再从本地列表移除。
// 这比完全依赖 transition-group leave 更稳定:即使列表即将为空或分支切换,也能先播放离场动画。
this.conversationListAnimationMode = 'delete';
const deletingIds = Array.isArray(this.pendingDeletingConversationIds)
? this.pendingDeletingConversationIds
: [];
if (!deletingIds.includes(conversationId)) {
this.pendingDeletingConversationIds = [...deletingIds, conversationId];
}
await this.$nextTick();
await waitForAnimation(430);
this.conversations = this.conversations.filter(
(conversation) => conversation.id !== conversationId
);
this.searchResults = this.searchResults.filter(
(conversation) => conversation.id !== conversationId
);
this.pendingDeletingConversationIds = (Array.isArray(this.pendingDeletingConversationIds)
? this.pendingDeletingConversationIds
: []
).filter((id) => id !== conversationId);
// 等待“左滑离场 + 0.1s 后集体上移”动画结束,避免刷新列表打断过渡。
await waitForAnimation(180);
// 刷新对话列表
this.conversationsOffset = 0;
await this.loadConversationsList();
this.conversationListAnimationMode = 'idle';
} else {
console.error('删除对话失败:', result.message);
this.uiPushToast({
title: '删除对话失败',
message: result.message || '服务器未返回成功状态',
type: 'error'
});
}
} catch (error) {
this.pendingDeletingConversationIds = (Array.isArray(this.pendingDeletingConversationIds)
? this.pendingDeletingConversationIds
: []
).filter((id) => id !== conversationId);
this.conversationListAnimationMode = 'idle';
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;
const waitForAnimation = (durationMs) =>
new Promise((resolve) => {
window.setTimeout(resolve, durationMs);
});
if (newId) {
const sourceIndex = this.conversations.findIndex(
(conv) => conv && conv.id === conversationId
);
const sourceConversation = sourceIndex >= 0 ? this.conversations[sourceIndex] : null;
const duplicateTitle =
result.load_result?.title || sourceConversation?.title || '复制的对话';
const duplicatePlaceholder = {
id: newId,
title: duplicateTitle,
updated_at: new Date().toISOString(),
total_messages: sourceConversation?.total_messages || 0,
total_tools: sourceConversation?.total_tools || 0
};
this.conversationInsertAnimations = {
...this.conversationInsertAnimations,
[conversationId]: 'duplicateSource',
[newId]: 'duplicate'
};
this.conversationListAnimationMode = 'duplicate';
const withoutDuplicate = this.conversations.filter((conv) => conv && conv.id !== newId);
const insertIndex = withoutDuplicate.findIndex(
(conv) => conv && conv.id === conversationId
);
if (insertIndex >= 0) {
this.conversations = [
...withoutDuplicate.slice(0, insertIndex + 1),
duplicatePlaceholder,
...withoutDuplicate.slice(insertIndex + 1)
];
} else {
this.conversations = [duplicatePlaceholder, ...withoutDuplicate];
}
window.setTimeout(() => {
const rest = { ...this.conversationInsertAnimations };
delete rest[conversationId];
delete rest[newId];
this.conversationInsertAnimations = rest;
this.conversationListAnimationMode = 'idle';
}, 860);
// 先让“目标下方钻出 + 下方整体下移”动画播完,再加载复制出的对话。
await waitForAnimation(860);
await this.loadConversation(newId, { force: true, preserveListPosition: true });
}
} 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'
});
}
}
};