agent-Specialization/static/src/app/methods/conversation/load.ts
JOJO 4e1d08f106 fix(frontend): 修复多个前端交互问题
- 修复运行时 intent Unicode 转义显示异常(后端 extract_intent_from_partial 解码)
- 修复 ask_user 弹窗问题/说明不换行
- 修复刷新页面后等待回答提示不恢复(添加 fetchPendingUserQuestions 并在初始化路径调用)
- 修复回答问题弹窗长问题撑满窗口(header/context 限高滚动)
- 修复角色编辑器模型选择菜单定位与滚动
- 在 AGENTS.md 中再次强调前端调试日志必须统一筛选词

测试:/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9 -m unittest test.test_server_refactor_smoke; npm run build
2026-07-15 00:31:13 +08:00

333 lines
13 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';
import {
} from './shared';
export const loadMethods = {
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 maParam = this.multiAgentMode ? '&multi_agent_mode=1' : '&multi_agent_mode=0';
const response = await fetch(`/api/conversations?limit=${queryLimit}&offset=${queryOffset}${maParam}`);
const data = await response.json();
if (data.success) {
if (refreshToken < this.conversationListRefreshToken) {
debugLog('忽略已过期的对话列表响应', {
requestSeq,
responseOffset: queryOffset,
refreshToken,
currentRefreshToken: this.conversationListRefreshToken
});
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);
const workspaceId = options.workspaceId ? String(options.workspaceId) : '';
const isHostLikeMode = Boolean(this.versioningHostMode || this.dockerProjectMode);
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多工作区模式下携带目标 workspace_id避免并发/session 漂移导致加载错误工作区)
const loadUrl = workspaceId && isHostLikeMode
? `/api/conversations/${conversationId}/load?workspace_id=${encodeURIComponent(workspaceId)}`
: `/api/conversations/${conversationId}/load`;
const response = await fetch(loadUrl, {
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);
}
if (typeof result.multi_agent_mode === 'boolean') {
this.multiAgentMode = result.multi_agent_mode;
}
// 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);
}
const urlPrefix = this.multiAgentMode ? '/multiagent/' : '/';
history.pushState(
{ conversationId },
'',
`${urlPrefix}${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();
this.fetchNetworkPermission();
await this.fetchVersioningStatus(conversationId, { silent: true });
this.fetchPendingToolApprovals();
this.fetchPendingUserQuestions();
this.refreshProjectGitSummary();
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'
});
}
},
async loadWorkspaceConversations(workspaceId: string, { reset = false } = {}) {
if (!workspaceId) return;
const { useConversationStore } = await import('../../../stores/conversation');
const conversationStore = useConversationStore();
let group = conversationStore.workspaceGroups.find((g: any) => g.workspaceId === workspaceId);
if (!group) {
group = {
workspaceId,
conversations: [],
loading: true,
hasMore: false,
loadingMore: false,
offset: 0,
limit: 20,
expanded: true
};
conversationStore.workspaceGroups.push(group);
}
if (reset) {
group.conversations = [];
group.offset = 0;
group.hasMore = false;
}
if (group.loading || group.loadingMore) return;
group.loading = true;
try {
const maParam = this.multiAgentMode ? '&multi_agent_mode=1' : '&multi_agent_mode=0';
const response = await fetch(`/api/conversations?workspace_id=${encodeURIComponent(workspaceId)}&limit=${group.limit}&offset=${group.offset}${maParam}`);
const data = await response.json();
if (data.success) {
const items = (data.data?.conversations || []).map((conv: any) => ({
id: conv.id,
title: conv.title,
updated_at: conv.updated_at,
total_messages: conv.total_messages,
total_tools: conv.total_tools
}));
if (group.offset === 0) {
group.conversations = items;
} else {
group.conversations.push(...items);
}
group.hasMore = !!data.data?.has_more;
} else {
console.error('加载工作区对话失败:', data.error);
}
} catch (error) {
console.error('加载工作区对话异常:', error);
} finally {
group.loading = false;
}
},
async loadMoreWorkspaceConversations(workspaceId: string) {
const { useConversationStore } = await import('../../../stores/conversation');
const conversationStore = useConversationStore();
const group = conversationStore.workspaceGroups.find((g: any) => g.workspaceId === workspaceId);
if (!group || group.loadingMore || !group.hasMore) return;
group.loadingMore = true;
group.offset += group.limit;
await this.loadWorkspaceConversations(workspaceId);
group.loadingMore = false;
}
};