agent-Specialization/static/src/app/methods/conversation/load.ts
JOJO 16900b0a20 fix(chat): 修复子智能体完成通知不实时显示,压缩残留锁死输入与切换
- 通知:inject_runtime_user_message 不再把子智能体任务 id 透传为事件顶层
  task_id(sender setdefault 不覆盖,前端任务归属守卫丢弃事件;socket 通道
 在轮询模式下不渲染,双通道断头导致只能刷新后从历史看到)。后台指令因透传
 的是 command_id 而不受影响
- 通知:修复 _persist_and_echo_preceding_notice 回显写在 except 分支内,
 批量通知前 N-1 条从不实时回显
- 压缩:移除「压缩中切换/新建对话需确认并取消压缩」拦截(多对话独立运行后
 无存在意义)
- 压缩:新增 compressionConversationId,输入/发送/停止锁按对话隔离,/new
 新建页不再被其他对话的压缩状态锁定
- 压缩:metadata 记录 compression_pid,status/compression_status 读取时按
 pid 懒清理进程重启后的残留 in_progress 标记;run_deep_compression 异常时
 自动清理标记
2026-08-12 22:27:31 +08:00

290 lines
12 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 = {
/**
* 侧边栏对话类型(普通/多智能体)切换:
* 重置平铺列表分页并重新加载,同时刷新所有已展开的工作区分组。
* 由 ConversationSidebar 的 conversation-type-change 事件触发;
* store 中的 sidebarConversationType 已由组件先行写入。
*/
/**
* 侧边栏对话类型(普通/多智能体)切换后的补载:
* 列表引用已由 store setSidebarConversationType 同步交换(零请求切换),
* 此处仅兜底「目标类型从未加载过」的场景(如登录后首次直接切换),
* 按需补载当前类型的主列表与工作区分组首页。
*/
async handleSidebarConversationTypeChange(_type?: 'normal' | 'multi_agent') {
const { useConversationStore } = await import('../../../stores/conversation');
const conversationStore = useConversationStore();
const listType = conversationStore.sidebarConversationType;
if (!conversationStore.conversationsCache[listType]?.loaded) {
this.conversationsOffset = 0;
this.hasMoreConversations = false;
await this.loadConversationsList();
}
try {
const groups = Array.isArray(conversationStore.workspaceGroups)
? conversationStore.workspaceGroups
: [];
for (const group of groups) {
if (group && group.workspaceId && !group.pagingByType?.[listType]?.loaded) {
await conversationStore.loadWorkspaceConversations(group.workspaceId);
}
}
} catch (error) {
console.error('补载工作区分组对话失败:', error);
}
},
async loadConversationsList() {
const queryOffset = this.conversationsOffset;
const queryLimit = this.conversationsLimit;
const { useConversationStore } = await import('../../../stores/conversation');
const conversationStore = useConversationStore();
/* 锁定发起时类型:响应期间用户可能已切换过滤器,数据始终写入对应类型缓存 */
const listType = conversationStore.sidebarConversationType;
const refreshToken =
queryOffset === 0 ? ++this.conversationListRefreshToken : this.conversationListRefreshToken;
const requestSeq = ++this.conversationListRequestSeq;
this.conversationsLoading = true;
try {
// 列表过滤由请求发起时的侧边栏类型过滤器(普通/多智能体)决定
const maParam = listType === 'multi_agent' ? '&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;
}
/* 原地写入该类型缓存(当前显示类型的缓存与 conversations 同一引用,原地修改即同步显示) */
const cache = conversationStore.conversationsCache[listType];
const items = data.data.conversations;
if (queryOffset === 0) {
cache.list.splice(0, cache.list.length, ...items);
} else {
cache.list.push(...items);
}
cache.offset = queryOffset;
cache.hasMore = data.data.has_more;
cache.loaded = true;
/* 仅仍是当前显示类型时同步扁平字段与首对话自动加载 */
if (listType === conversationStore.sidebarConversationType) {
this.conversations = cache.list;
this.hasMoreConversations = cache.hasMore;
if (this.currentConversationId) {
this.promoteConversationToTop(this.currentConversationId);
}
if (
queryOffset === 0 &&
!this.currentConversationId &&
cache.list.length > 0 &&
!this.isExplicitNewConversationRoute()
) {
// 只有在初始化完成后,才自动加载第一个对话
// 避免与 bootstrapRoute 冲突
if (this.initialRouteResolved) {
const latestConversation = cache.list[0];
if (latestConversation && latestConversation.id) {
await this.loadConversation(latestConversation.id);
}
}
}
}
debugLog(`已加载 ${cache.list.length} 个对话`);
/* 首页加载成功后后台补载另一类型,保证切换过滤器时零等待 */
const otherType = listType === 'multi_agent' ? 'normal' : 'multi_agent';
if (queryOffset === 0 && !conversationStore.conversationsCache[otherType].loaded) {
this.loadConversationTypeCache(otherType).catch(() => {});
}
} else {
console.error('加载对话列表失败:', data.error);
}
} catch (error) {
console.error('加载对话列表异常:', error);
} finally {
if (refreshToken === this.conversationListRefreshToken) {
this.conversationsLoading = false;
}
}
},
/** 后台补载指定类型的首页列表缓存已加载则跳过refreshToken 快照防 reset/刷新后旧响应污染 */
async loadConversationTypeCache(type: 'normal' | 'multi_agent') {
const { useConversationStore } = await import('../../../stores/conversation');
const conversationStore = useConversationStore();
const cache = conversationStore.conversationsCache[type];
if (!cache || cache.loaded) return;
const tokenAtStart = this.conversationListRefreshToken;
const maParam = type === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0';
try {
const response = await fetch(
`/api/conversations?limit=${this.conversationsLimit}&offset=0${maParam}`
);
const data = await response.json();
if (!data?.success) return;
/* reset/刷新后旧响应丢弃;并发补载去重 */
if (tokenAtStart !== this.conversationListRefreshToken || cache.loaded) return;
const items = data.data?.conversations || [];
cache.list.push(...items);
cache.offset = 0;
cache.hasMore = !!data.data?.has_more;
cache.loaded = true;
} catch (error) {
console.error('补载对话列表缓存异常:', error);
}
},
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) : '';
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;
}
// 切换对话前把 debounce 中的推理强度保存立即落盘(随请求带原对话 id
// 不会串到新对话fire-and-forget不阻塞加载
this.flushReasoningEffortSave?.();
// 已移除「压缩中切换对话需确认/取消压缩」拦截:多对话独立运行后,压缩在
// 后端随对话任务进行,切换视图不影响压缩本身;压缩锁也按对话隔离
// compressionActiveForCurrentConversation不再存在对话级全局锁。
// 注意:加载已有对话时必须保留该对话自身的模型/模式,不能套用用户默认值。
// 多工作区并行后,切换对话只切换视图,不再取消后台任务;停止当前轮询避免事件写入新对话界面。
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 {
// 统一加载协议:一次 bootstrap 替代「PUT load + GET messages + GET tasks」串行链。
// 模式/模型应用、历史渲染、运行中任务恢复均在 enterConversation 内完成。
const result = await this.enterConversation(conversationId, {
source: 'sidebar',
workspaceId,
urlMode: 'push',
preserveListPosition,
resetUI: true
});
if (result.success) {
debugLog('对话 bootstrap 成功:', result);
traceLog('loadConversation:api-success', { conversationId, title: result.title });
this.currentConversationTitle = result.title;
this.titleReady = true;
this.suppressTitleTyping = false;
this.startTitleTyping(this.currentConversationTitle, { animate: false });
this.subAgentFetch();
this.fetchTodoList();
await this.refreshRunningWorkspaceTasks?.();
const visibleTask =
typeof this.getVisibleWorkspaceTaskForConversation === 'function'
? this.getVisibleWorkspaceTaskForConversation(conversationId)
: null;
const visibleTaskStatus = String(visibleTask?.status || '');
const terminalStatuses = new Set(['succeeded', 'failed', 'canceled']);
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'
});
}
}
};
/* 工作区分组加载统一走 store 版stores/conversation.ts 的 loadWorkspaceConversations/
loadMoreWorkspaceConversations/loadWorkspaceConversationTypeCache双类型缓存已在那里适配
此处不再保留重复实现 */