- 修复运行时 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
333 lines
13 KiB
TypeScript
333 lines
13 KiB
TypeScript
// @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;
|
||
}
|
||
};
|