agent-Specialization/static/src/app/methods/ui.ts
JOJO 43409c523e fix: 移除错误的对话切换跳转逻辑并修复工具执行返回值问题
主要修复:
1. 移除前端"取消跳转到正在运行的对话"的错误逻辑
   - 删除 switchConversation 中的任务检查和确认提示
   - 删除 createNewConversation 中的跳转回运行对话逻辑
   - 删除 loadConversation 中对未定义变量 hasActiveTask 的引用

2. 修复后端工具执行返回值问题
   - 修复 execute_tool_calls 在用户停止时返回 None 的 bug
   - 确保所有返回路径都返回包含 stopped 和 last_tool_call_time 的字典

3. 其他改进
   - 添加代码复制功能 (handleCopyCodeClick)
   - 移除 FocusPanel 相关代码
   - 更新个性化配置 (enhanced_tool_display)
   - 样式和主题优化

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 17:42:07 +08:00

1225 lines
41 KiB
TypeScript

// @ts-nocheck
import { usePolicyStore } from '../../stores/policy';
import { useModelStore } from '../../stores/model';
import { initializeLegacySocket } from '../../composables/useLegacySocket';
import { renderMarkdown as renderMarkdownHelper } from '../../composables/useMarkdownRenderer';
import {
scrollToBottom as scrollToBottomHelper,
conditionalScrollToBottom as conditionalScrollToBottomHelper,
toggleScrollLock as toggleScrollLockHelper,
scrollThinkingToBottom as scrollThinkingToBottomHelper
} from '../../composables/useScrollControl';
import {
startResize as startPanelResize,
handleResize as handlePanelResize,
stopResize as stopPanelResize
} from '../../composables/usePanelResize';
import { debugLog } from './common';
export const uiMethods = {
ensureScrollListener() {
if (this._scrollListenerReady) {
return;
}
const area = this.getMessagesAreaElement();
if (!area) {
return;
}
this.initScrollListener();
this._scrollListenerReady = true;
},
setupMobileViewportWatcher() {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
this.updateMobileViewportState(false);
return;
}
const query = window.matchMedia('(max-width: 768px)');
this.mobileViewportQuery = query;
this.updateMobileViewportState(query.matches);
if (typeof query.addEventListener === 'function') {
query.addEventListener('change', this.handleMobileViewportQueryChange);
} else if (typeof query.addListener === 'function') {
query.addListener(this.handleMobileViewportQueryChange);
}
},
teardownMobileViewportWatcher() {
const query = this.mobileViewportQuery;
if (!query) {
return;
}
if (typeof query.removeEventListener === 'function') {
query.removeEventListener('change', this.handleMobileViewportQueryChange);
} else if (typeof query.removeListener === 'function') {
query.removeListener(this.handleMobileViewportQueryChange);
}
this.mobileViewportQuery = null;
},
handleMobileViewportQueryChange(event) {
this.updateMobileViewportState(event.matches);
},
updateMobileViewportState(isMobile) {
this.uiSetMobileViewport(!!isMobile);
if (!isMobile) {
this.uiSetMobileOverlayMenuOpen(false);
this.closeMobileOverlay();
}
},
toggleMobileOverlayMenu() {
if (!this.isMobileViewport) {
return;
}
this.uiToggleMobileOverlayMenu();
},
openMobileOverlay(target) {
if (!this.isMobileViewport) {
return;
}
if (this.activeMobileOverlay === target) {
this.closeMobileOverlay();
return;
}
if (this.activeMobileOverlay === 'conversation') {
this.uiSetSidebarCollapsed(true);
}
if (target === 'conversation') {
this.uiSetSidebarCollapsed(false);
}
this.uiSetActiveMobileOverlay(target);
this.uiSetMobileOverlayMenuOpen(false);
},
closeMobileOverlay() {
if (!this.activeMobileOverlay) {
this.uiCloseMobileOverlay();
return;
}
if (this.activeMobileOverlay === 'conversation') {
this.uiSetSidebarCollapsed(true);
}
this.uiCloseMobileOverlay();
},
applyPolicyUiLocks() {
const policyStore = usePolicyStore();
const blocks = policyStore.uiBlocks;
if (blocks.collapse_workspace) {
this.uiSetWorkspaceCollapsed(true);
}
if (blocks.block_virtual_monitor && this.chatDisplayMode === 'monitor') {
this.uiSetChatDisplayMode('chat');
}
},
isPolicyBlocked(key: string, message?: string) {
const policyStore = usePolicyStore();
if (policyStore.uiBlocks[key]) {
this.uiPushToast({
title: '已被管理员禁用',
message: message || '被管理员强制禁用',
type: 'warning'
});
return true;
}
return false;
},
handleClickOutsideMobileMenu(event) {
if (!this.isMobileViewport || !this.mobileOverlayMenuOpen) {
return;
}
const trigger = this.$refs.mobilePanelTrigger;
if (trigger && typeof trigger.contains === 'function' && trigger.contains(event.target)) {
return;
}
this.uiSetMobileOverlayMenuOpen(false);
},
handleWorkspaceToggle() {
if (this.isMobileViewport) {
return;
}
if (this.isPolicyBlocked('collapse_workspace', '工作区已被管理员强制折叠')) {
this.uiSetWorkspaceCollapsed(true);
return;
}
const nextState = !this.workspaceCollapsed;
this.uiSetWorkspaceCollapsed(nextState);
if (nextState) {
this.uiSetPanelMenuOpen(false);
}
},
handleDisplayModeToggle() {
if (this.displayModeSwitchDisabled) {
// 显式提示管理员禁用
this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用');
return;
}
if (this.chatDisplayMode === 'chat' && this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用')) {
return;
}
const next = this.chatDisplayMode === 'chat' ? 'monitor' : 'chat';
this.uiSetChatDisplayMode(next);
},
handleMobileOverlayEscape(event) {
if (event.key !== 'Escape' || !this.isMobileViewport) {
return;
}
if (this.mobileOverlayMenuOpen) {
this.uiSetMobileOverlayMenuOpen(false);
return;
}
if (this.activeMobileOverlay) {
this.closeMobileOverlay();
}
},
handleMobileOverlaySelect(conversationId) {
this.loadConversation(conversationId);
this.closeMobileOverlay();
},
handleMobilePersonalClick() {
this.closeMobileOverlay();
this.uiSetMobileOverlayMenuOpen(false);
this.openPersonalPage();
},
toggleSidebar() {
if (this.isMobileViewport && this.activeMobileOverlay === 'conversation') {
this.closeMobileOverlay();
return;
}
this.uiToggleSidebar();
},
togglePanelMenu() {
this.uiTogglePanelMenu();
},
selectPanelMode(mode) {
this.uiSetPanelMode(mode);
this.uiSetPanelMenuOpen(false);
},
openPersonalPage() {
if (this.isPolicyBlocked('block_personal_space', '个人空间已被管理员禁用')) {
return;
}
this.personalizationOpenDrawer();
},
fetchTodoList() {
return this.fileFetchTodoList();
},
fetchSubAgents() {
return this.subAgentFetch();
},
async toggleThinkingMode() {
await this.handleCycleRunMode();
},
handleQuickModeToggle() {
if (!this.isConnected || this.streamingMessage) {
return;
}
this.handleCycleRunMode();
},
toggleQuickMenu() {
if (!this.isConnected) {
return;
}
const opened = this.inputToggleQuickMenu();
if (!opened) {
this.modeMenuOpen = false;
this.modelMenuOpen = false;
}
},
closeQuickMenu() {
this.inputCloseMenus();
this.modeMenuOpen = false;
this.modelMenuOpen = false;
},
toggleModeMenu() {
if (!this.isConnected || this.streamingMessage) {
return;
}
const next = !this.modeMenuOpen;
this.modeMenuOpen = next;
if (next) {
this.modelMenuOpen = false;
}
if (next) {
this.inputSetToolMenuOpen(false);
this.inputSetSettingsOpen(false);
if (!this.quickMenuOpen) {
this.inputOpenQuickMenu();
}
}
},
toggleModelMenu() {
if (!this.isConnected || this.streamingMessage) {
return;
}
const next = !this.modelMenuOpen;
this.modelMenuOpen = next;
if (next) {
this.modeMenuOpen = false;
this.inputSetToolMenuOpen(false);
this.inputSetSettingsOpen(false);
if (!this.quickMenuOpen) {
this.inputOpenQuickMenu();
}
}
},
toggleHeaderMenu() {
if (!this.isConnected) return;
this.headerMenuOpen = !this.headerMenuOpen;
if (this.headerMenuOpen) {
this.closeQuickMenu();
this.inputCloseMenus();
}
},
async handleModeSelect(mode) {
if (!this.isConnected || this.streamingMessage) {
return;
}
await this.setRunMode(mode);
},
async handleHeaderRunModeSelect(mode) {
await this.handleModeSelect(mode);
this.closeHeaderMenu();
},
async handleModelSelect(key) {
if (!this.isConnected || this.streamingMessage) {
return;
}
const policyStore = usePolicyStore();
if (policyStore.isModelDisabled(key)) {
this.uiPushToast({
title: '模型被禁用',
message: '被管理员强制禁用',
type: 'warning'
});
return;
}
if (this.conversationHasImages && !['qwen3-vl-plus', 'kimi-k2.5'].includes(key)) {
this.uiPushToast({
title: '切换失败',
message: '当前对话包含图片,仅支持 Qwen3.5 或 Kimi-k2.5',
type: 'error'
});
return;
}
const modelStore = useModelStore();
const prev = this.currentModelKey;
try {
const resp = await fetch('/api/model', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model_key: key })
});
const payload = await resp.json();
if (!resp.ok || !payload.success) {
throw new Error(payload.error || payload.message || '切换失败');
}
const data = payload.data || {};
modelStore.setModel(data.model_key || key);
if (data.run_mode) {
this.runMode = data.run_mode;
this.thinkingMode = data.thinking_mode ?? (data.run_mode !== 'fast');
} else {
// 前端兼容策略:根据模型特性自动调整运行模式
const currentModel = modelStore.currentModel;
if (currentModel?.deepOnly) {
this.runMode = 'deep';
this.thinkingMode = true;
} else if (currentModel?.fastOnly) {
this.runMode = 'fast';
this.thinkingMode = false;
} else {
this.thinkingMode = this.runMode !== 'fast';
}
}
this.uiPushToast({
title: '模型已切换',
message: modelStore.currentModel?.label || key,
type: 'success'
});
} catch (error) {
modelStore.setModel(prev);
const msg = error instanceof Error ? error.message : String(error || '切换失败');
this.uiPushToast({
title: '切换模型失败',
message: msg,
type: 'error'
});
} finally {
this.modelMenuOpen = false;
this.inputCloseMenus();
this.inputSetQuickMenuOpen(false);
}
},
async handleHeaderModelSelect(key, disabled) {
if (disabled) return;
await this.handleModelSelect(key);
this.closeHeaderMenu();
},
async handleCycleRunMode() {
const modes: Array<'fast' | 'thinking' | 'deep'> = ['fast', 'thinking', 'deep'];
const currentMode = this.resolvedRunMode;
const currentIndex = modes.indexOf(currentMode);
const nextMode = modes[(currentIndex + 1) % modes.length];
await this.setRunMode(nextMode);
},
async setRunMode(mode, options = {}) {
if (!this.isConnected || this.streamingMessage) {
this.modeMenuOpen = false;
return;
}
const modelStore = useModelStore();
const fastOnly = modelStore.currentModel?.fastOnly;
const deepOnly = modelStore.currentModel?.deepOnly;
if (fastOnly && mode !== 'fast') {
if (!options.suppressToast) {
this.uiPushToast({
title: '模式不可用',
message: '当前模型仅支持快速模式',
type: 'warning'
});
}
this.modeMenuOpen = false;
this.inputCloseMenus();
return;
}
if (deepOnly && mode !== 'deep') {
if (!options.suppressToast) {
this.uiPushToast({
title: '模式不可用',
message: '当前模型仅支持深度思考模式',
type: 'warning'
});
}
this.modeMenuOpen = false;
this.inputCloseMenus();
return;
}
if (mode === this.resolvedRunMode) {
this.modeMenuOpen = false;
this.closeQuickMenu();
return;
}
try {
const response = await fetch('/api/thinking-mode', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ mode })
});
const payload = await response.json();
if (!response.ok || !payload.success) {
throw new Error(payload.message || payload.error || '切换失败');
}
const data = payload.data || {};
this.thinkingMode = typeof data.thinking_mode === 'boolean' ? data.thinking_mode : mode !== 'fast';
this.runMode = data.mode || mode;
} catch (error) {
console.error('切换运行模式失败:', error);
const message = error instanceof Error ? error.message : String(error || '未知错误');
this.uiPushToast({
title: '切换思考模式失败',
message: message || '请稍后重试',
type: 'error'
});
} finally {
this.modeMenuOpen = false;
this.inputCloseMenus();
}
},
handleInputChange() {
this.autoResizeInput();
},
handleInputFocus() {
this.inputSetFocused(true);
this.closeQuickMenu();
},
handleInputBlur() {
this.inputSetFocused(false);
},
handleRealtimeTerminalClick() {
if (!this.isConnected) {
return;
}
if (this.isPolicyBlocked('block_realtime_terminal', '实时终端已被管理员禁用')) {
return;
}
this.openRealtimeTerminal();
},
handleFocusPanelToggleClick() {
if (!this.isConnected) {
return;
}
if (this.isPolicyBlocked('block_focus_panel', '聚焦面板已被管理员禁用')) {
return;
}
this.toggleFocusPanel();
},
handleTokenPanelToggleClick() {
if (!this.currentConversationId) {
return;
}
if (this.isPolicyBlocked('block_token_panel', '用量统计已被管理员禁用')) {
return;
}
this.toggleTokenPanel();
},
handleCompressConversationClick() {
if (this.compressing || this.streamingMessage || !this.isConnected) {
return;
}
if (this.isPolicyBlocked('block_compress_conversation', '压缩对话已被管理员禁用')) {
return;
}
this.compressConversation();
},
openReviewDialog() {
if (this.isPolicyBlocked('block_conversation_review', '对话引用已被管理员禁用')) {
return;
}
if (!this.isConnected) {
this.uiPushToast({
title: '无法使用',
message: '当前未连接,无法生成回顾文件',
type: 'warning'
});
return;
}
if (!this.conversations.length && !this.conversationsLoading) {
this.loadConversationsList();
}
const fallback = this.conversations.find((c) => c.id !== this.currentConversationId);
if (!fallback) {
this.uiPushToast({
title: '暂无可用对话',
message: '没有可供回顾的其他对话记录',
type: 'info'
});
return;
}
this.reviewSelectedConversationId = fallback.id;
this.reviewDialogOpen = true;
this.reviewPreviewLines = [];
this.reviewPreviewError = null;
this.reviewGeneratedPath = null;
this.loadReviewPreview(fallback.id);
this.closeQuickMenu();
},
closeHeaderMenu() {
this.headerMenuOpen = false;
},
handleReviewSelect(id) {
if (id === this.currentConversationId) {
this.uiPushToast({
title: '无法引用当前对话',
message: '请选择其他对话生成回顾',
type: 'warning'
});
return;
}
this.reviewSelectedConversationId = id;
this.loadReviewPreview(id);
},
async handleConfirmReview() {
if (this.reviewSubmitting) return;
if (!this.reviewSelectedConversationId) {
this.uiPushToast({
title: '请选择对话',
message: '请选择要生成回顾的对话记录',
type: 'info'
});
return;
}
if (this.reviewSelectedConversationId === this.currentConversationId) {
this.uiPushToast({
title: '无法引用当前对话',
message: '请选择其他对话生成回顾',
type: 'warning'
});
return;
}
if (!this.currentConversationId) {
this.uiPushToast({
title: '无法发送',
message: '当前没有活跃对话,无法自动发送提示消息',
type: 'warning'
});
return;
}
this.reviewSubmitting = true;
try {
const { path, char_count } = await this.generateConversationReview(this.reviewSelectedConversationId);
if (!path) {
throw new Error('未获取到生成的文件路径');
}
const count = typeof char_count === 'number' ? char_count : 0;
this.reviewGeneratedPath = path;
const suggestion =
count && count <= 10000
? '建议直接完整阅读。'
: '建议使用 read 工具进行搜索或分段阅读。';
if (this.reviewSendToModel) {
const message = `帮我继续这个任务,对话文件在 ${path},文件长 ${count || '未知'} 字符,${suggestion} 请阅读文件了解后,不要直接继续工作,而是向我汇报你的理解,然后等我做出指示。`;
const sent = this.sendAutoUserMessage(message);
if (sent) {
this.reviewDialogOpen = false;
}
} else {
this.uiPushToast({
title: '回顾文件已生成',
message: path,
type: 'success'
});
}
} catch (error) {
const msg = error instanceof Error ? error.message : String(error || '生成失败');
this.uiPushToast({
title: '生成回顾失败',
message: msg,
type: 'error'
});
} finally {
this.reviewSubmitting = false;
}
},
async loadReviewPreview(conversationId) {
this.reviewPreviewLoading = true;
this.reviewPreviewError = null;
this.reviewPreviewLines = [];
try {
const resp = await fetch(`/api/conversations/${conversationId}/review_preview?limit=${this.reviewPreviewLimit}`);
const payload = await resp.json().catch(() => ({}));
if (!resp.ok || !payload?.success) {
const msg = payload?.message || payload?.error || '获取预览失败';
throw new Error(msg);
}
this.reviewPreviewLines = payload?.data?.preview || [];
} catch (error) {
const msg = error instanceof Error ? error.message : String(error || '获取预览失败');
this.reviewPreviewError = msg;
} finally {
this.reviewPreviewLoading = false;
}
},
async generateConversationReview(conversationId) {
const response = await fetch(`/api/conversations/${conversationId}/review`, {
method: 'POST'
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || !payload?.success) {
const msg = payload?.message || payload?.error || '生成失败';
throw new Error(msg);
}
const data = payload.data || payload;
return {
path: data.path || data.file_path || data.relative_path,
char_count: data.char_count ?? data.length ?? data.size ?? 0
};
},
handleClickOutsideQuickMenu(event) {
if (!this.quickMenuOpen) {
return;
}
const shell = this.getComposerElement('stadiumShellOuter') || this.getComposerElement('compactInputShell');
if (shell && shell.contains(event.target)) {
return;
}
this.closeQuickMenu();
},
handleClickOutsideHeaderMenu(event) {
if (!this.headerMenuOpen) return;
const ribbon = this.$refs.titleRibbon as HTMLElement | undefined;
const menu = this.$refs.headerMenu as HTMLElement | undefined;
if ((ribbon && ribbon.contains(event.target)) || (menu && menu.contains(event.target))) {
return;
}
this.closeHeaderMenu();
},
handleClickOutsidePanelMenu(event) {
if (!this.panelMenuOpen) {
return;
}
const leftPanel = this.$refs.leftPanel;
const wrapper = leftPanel && leftPanel.panelMenuWrapper ? leftPanel.panelMenuWrapper : null;
if (wrapper && wrapper.contains(event.target)) {
return;
}
this.uiSetPanelMenuOpen(false);
},
isConversationBlank() {
if (!Array.isArray(this.messages) || !this.messages.length) return true;
return !this.messages.some(
(msg) => msg && (msg.role === 'user' || msg.role === 'assistant')
);
},
pickWelcomeText() {
const pool = this.blankWelcomePool;
if (!Array.isArray(pool) || !pool.length) {
this.blankWelcomeText = '有什么可以帮忙的?';
return;
}
const idx = Math.floor(Math.random() * pool.length);
this.blankWelcomeText = pool[idx];
},
refreshBlankHeroState() {
const isBlank = this.isConversationBlank();
const currentConv = this.currentConversationId || 'temp';
const needNewWelcome =
!this.blankHeroActive ||
this.lastBlankConversationId !== currentConv;
if (isBlank) {
if (needNewWelcome && !this.blankHeroExiting) {
this.pickWelcomeText();
}
this.blankHeroActive = true;
this.lastBlankConversationId = currentConv;
} else {
this.blankHeroActive = false;
this.blankHeroExiting = false;
this.lastBlankConversationId = null;
}
},
toggleSettings() {
if (!this.isConnected) {
return;
}
this.modeMenuOpen = false;
this.modelMenuOpen = false;
const nextState = this.inputToggleSettingsMenu();
if (nextState) {
this.inputSetToolMenuOpen(false);
if (!this.quickMenuOpen) {
this.inputOpenQuickMenu();
}
}
},
toggleFocusPanel() {
this.rightCollapsed = !this.rightCollapsed;
if (!this.rightCollapsed && this.rightWidth < this.minPanelWidth) {
this.rightWidth = this.minPanelWidth;
}
},
addSystemMessage(content) {
this.chatAddSystemMessage(content);
this.$forceUpdate();
this.conditionalScrollToBottom();
},
startTitleTyping(title: string, options: { animate?: boolean } = {}) {
if (this.titleTypingTimer) {
clearInterval(this.titleTypingTimer);
this.titleTypingTimer = null;
}
const target = (title || '').trim();
const animate = options.animate ?? true;
if (!animate) {
this.titleTypingTarget = target;
this.titleTypingText = target;
return;
}
const previous = (this.titleTypingText || '').trim();
if (previous === target) {
this.titleTypingTarget = target;
this.titleTypingText = target;
return;
}
this.titleTypingTarget = target;
this.titleTypingText = previous;
const frames: string[] = [];
for (let i = previous.length; i >= 0; i--) {
frames.push(previous.slice(0, i));
}
for (let j = 1; j <= target.length; j++) {
frames.push(target.slice(0, j));
}
let index = 0;
this.titleTypingTimer = window.setInterval(() => {
if (index >= frames.length) {
clearInterval(this.titleTypingTimer!);
this.titleTypingTimer = null;
this.titleTypingText = target;
return;
}
this.titleTypingText = frames[index];
index += 1;
}, 32);
},
toggleBlock(blockId) {
if (!blockId) {
return;
}
if (this.expandedBlocks && this.expandedBlocks.has(blockId)) {
this.chatCollapseBlock(blockId);
} else {
this.chatExpandBlock(blockId);
}
},
handleThinkingScroll(blockId, event) {
if (!blockId || !event || !event.target) {
return;
}
const el = event.target;
const threshold = 12;
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
this.chatSetThinkingLock(blockId, atBottom);
},
scrollToBottom() {
scrollToBottomHelper(this);
},
conditionalScrollToBottom() {
conditionalScrollToBottomHelper(this);
},
toggleScrollLock() {
toggleScrollLockHelper(this);
},
scrollThinkingToBottom(blockId) {
scrollThinkingToBottomHelper(this, blockId);
},
// 面板调整方法
startResize(panel, event) {
startPanelResize(this, panel, event);
},
handleResize(event) {
handlePanelResize(this, event);
},
stopResize() {
stopPanelResize(this);
},
getInputComposerRef() {
return this.$refs.inputComposer || null;
},
getComposerElement(field) {
const composer = this.getInputComposerRef();
const unwrap = (value: any) => {
if (!value) {
return null;
}
if (value instanceof HTMLElement) {
return value;
}
if (typeof value === 'object' && 'value' in value && !(value instanceof Window)) {
return value.value;
}
return value;
};
if (composer && composer[field]) {
return unwrap(composer[field]);
}
if (this.$refs && this.$refs[field]) {
return unwrap(this.$refs[field]);
}
return null;
},
getMessagesAreaElement() {
const ref = this.$refs.messagesArea;
if (!ref) {
return null;
}
if (ref instanceof HTMLElement) {
return ref;
}
if (ref.rootEl) {
return ref.rootEl.value || ref.rootEl;
}
if (ref.$el && ref.$el.querySelector) {
const el = ref.$el.querySelector('.messages-area');
if (el) {
return el;
}
}
return null;
},
getThinkingContentElement(blockId) {
const chatArea = this.$refs.messagesArea;
if (chatArea && typeof chatArea.getThinkingRef === 'function') {
const el = chatArea.getThinkingRef(blockId);
if (el) {
return el;
}
}
const refName = `thinkingContent-${blockId}`;
const elRef = this.$refs[refName];
if (Array.isArray(elRef)) {
return elRef[0] || null;
}
return elRef || null;
},
isOutputActive() {
return !!(this.streamingMessage || this.taskInProgress || this.hasPendingToolActions());
},
logMessageState(action, extra = {}) {
const count = Array.isArray(this.messages) ? this.messages.length : 'N/A';
debugLog('[Messages]', {
action,
count,
conversationId: this.currentConversationId,
streaming: this.streamingMessage,
...extra
});
},
iconStyle(iconKey, size) {
const iconPath = this.icons ? this.icons[iconKey] : null;
if (!iconPath) {
return {};
}
const style = { '--icon-src': `url(${iconPath})` };
if (size) {
style['--icon-size'] = size;
}
return style;
},
openGuiFileManager() {
if (this.isPolicyBlocked('block_file_manager', '文件管理器已被管理员禁用')) {
return;
}
window.open('/file-manager', '_blank');
},
renderMarkdown(content, isStreaming = false) {
return renderMarkdownHelper(content, isStreaming);
},
async bootstrapRoute() {
// 在路由解析期间抑制标题动画,避免预置“新对话”闪烁
this.suppressTitleTyping = true;
this.titleReady = false;
this.currentConversationTitle = '';
this.titleTypingText = '';
const path = window.location.pathname.replace(/^\/+/, '');
if (!path || path === 'new') {
this.currentConversationId = null;
this.currentConversationTitle = '新对话';
this.titleReady = true;
this.suppressTitleTyping = false;
this.startTitleTyping('新对话', { animate: false });
this.initialRouteResolved = true;
return;
}
const convId = path.startsWith('conv_') ? path : `conv_${path}`;
try {
const resp = await fetch(`/api/conversations/${convId}/load`, { method: 'PUT' });
const result = await resp.json();
if (result.success) {
this.currentConversationId = convId;
this.currentConversationTitle = result.title || '';
this.titleReady = true;
this.suppressTitleTyping = false;
this.startTitleTyping(this.currentConversationTitle, { animate: false });
history.replaceState({ conversationId: convId }, '', `/${this.stripConversationPrefix(convId)}`);
} else {
history.replaceState({}, '', '/new');
this.currentConversationId = null;
this.currentConversationTitle = '新对话';
this.titleReady = true;
this.suppressTitleTyping = false;
this.startTitleTyping('新对话', { animate: false });
}
} catch (error) {
console.warn('初始化路由失败:', error);
history.replaceState({}, '', '/new');
this.currentConversationId = null;
this.currentConversationTitle = '新对话';
this.titleReady = true;
this.suppressTitleTyping = false;
this.startTitleTyping('新对话', { animate: false });
} finally {
this.initialRouteResolved = true;
}
},
handlePopState(event) {
const state = event.state || {};
const convId = state.conversationId;
if (!convId) {
this.currentConversationId = null;
this.currentConversationTitle = '新对话';
this.logMessageState('handlePopState:clear-messages-no-conversation');
this.messages = [];
this.logMessageState('handlePopState:after-clear-no-conversation');
this.resetAllStates('handlePopState:no-conversation');
this.resetTokenStatistics();
return;
}
this.loadConversation(convId);
},
stripConversationPrefix(conversationId) {
if (!conversationId) return '';
return conversationId.startsWith('conv_') ? conversationId.slice(5) : conversationId;
},
initScrollListener() {
const messagesArea = this.getMessagesAreaElement();
if (!messagesArea) {
console.warn('消息区域未找到');
return;
}
this._scrollListenerReady = true;
let isProgrammaticScroll = false;
const bottomThreshold = 12;
this._setScrollingFlag = (flag) => {
isProgrammaticScroll = !!flag;
};
messagesArea.addEventListener('scroll', () => {
if (isProgrammaticScroll) {
return;
}
const scrollTop = messagesArea.scrollTop;
const scrollHeight = messagesArea.scrollHeight;
const clientHeight = messagesArea.clientHeight;
const isAtBottom = scrollHeight - scrollTop - clientHeight < bottomThreshold;
const activeLock = this.autoScrollEnabled && this.isOutputActive();
// 锁定且当前有输出时,强制贴底
if (activeLock) {
if (!isAtBottom) {
if (typeof this._setScrollingFlag === 'function') {
this._setScrollingFlag(true);
}
messagesArea.scrollTop = messagesArea.scrollHeight;
if (typeof this._setScrollingFlag === 'function') {
setTimeout(() => this._setScrollingFlag && this._setScrollingFlag(false), 16);
}
}
// 保持锁定状态下 userScrolling 为 false
this.chatSetScrollState({ userScrolling: false });
return;
}
// 未锁定或无输出:允许自由滚动,仅记录位置
this.chatSetScrollState({ userScrolling: !isAtBottom });
});
},
async initSocket() {
await initializeLegacySocket(this);
},
openRealtimeTerminal() {
const { protocol, hostname, port } = window.location;
const target = `${protocol}//${hostname}${port ? ':' + port : ''}/terminal`;
window.open(target, '_blank');
},
async loadInitialData() {
try {
debugLog('加载初始数据...');
const statusResponse = await fetch('/api/status');
const statusData = await statusResponse.json();
this.projectPath = statusData.project_path || '';
this.agentVersion = statusData.version || this.agentVersion;
this.thinkingMode = !!statusData.thinking_mode;
this.applyStatusSnapshot(statusData);
// 立即更新配额和运行模式,避免等待其他慢接口
this.fetchUsageQuota();
// 拉取管理员策略
const policyStore = usePolicyStore();
await policyStore.fetchPolicy();
this.applyPolicyUiLocks();
const focusPromise = this.focusFetchFiles();
const todoPromise = this.fileFetchTodoList();
let treePromise: Promise<any> | null = null;
const isHostMode = statusData?.container?.mode === 'host';
if (isHostMode) {
this.fileMarkTreeUnavailable('宿主机模式下文件树不可用');
} else {
treePromise = this.fileFetchTree();
}
// 获取当前对话信息
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
if (statusConversationId && !this.currentConversationId) {
this.skipConversationHistoryReload = true;
// 首次从状态恢复对话时,避免 socket 的 conversation_loaded 再次触发历史加载
this.skipConversationLoadedEvent = true;
this.suppressTitleTyping = true;
this.titleReady = false;
this.currentConversationTitle = '';
this.titleTypingText = '';
this.currentConversationId = statusConversationId;
// 如果有当前对话,尝试获取标题和历史
try {
const convResponse = await fetch(`/api/conversations/current`);
const convData = await convResponse.json();
if (convData.success && convData.data) {
this.currentConversationTitle = convData.data.title;
this.titleReady = true;
this.suppressTitleTyping = false;
this.startTitleTyping(this.currentConversationTitle, { animate: false });
} else {
this.titleReady = true;
this.suppressTitleTyping = false;
const fallbackTitle = this.currentConversationTitle || '新对话';
this.currentConversationTitle = fallbackTitle;
this.startTitleTyping(fallbackTitle, { animate: false });
}
// 初始化时调用一次,因为 skipConversationHistoryReload 会阻止 watch 触发
if (
this.lastHistoryLoadedConversationId !== this.currentConversationId ||
!Array.isArray(this.messages) ||
this.messages.length === 0
) {
await this.fetchAndDisplayHistory();
}
// 获取当前对话的Token统计
this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens();
} catch (e) {
console.warn('获取当前对话标题失败:', e);
this.titleReady = true;
this.suppressTitleTyping = false;
this.startTitleTyping(this.currentConversationTitle || '新对话', '');
}
}
// 等待其他加载项完成(允许部分失败不阻塞模式切换)
const pendingPromises = [focusPromise, todoPromise];
if (treePromise) {
pendingPromises.push(treePromise);
}
await Promise.allSettled(pendingPromises);
await this.loadToolSettings(true);
debugLog('初始数据加载完成');
} catch (error) {
console.error('加载初始数据失败:', error);
}
},
confirmAction(options = {}) {
return this.uiRequestConfirm(options);
},
async handleCopyCodeClick(event) {
const target = event.target;
if (!target || !target.classList || !target.classList.contains('copy-code-btn')) {
return;
}
const blockId = target.getAttribute('data-code');
if (!blockId) {
return;
}
// 防止重复点击
if (target.classList.contains('copied')) {
return;
}
const selector = `[data-code-id="${blockId.replace(/"/g, '\\"')}"]`;
const codeEl = document.querySelector(selector);
if (!codeEl) {
return;
}
const encoded = codeEl.getAttribute('data-original-code');
const content = encoded ? this.decodeHtmlEntities(encoded) : codeEl.textContent || '';
if (!content.trim()) {
return;
}
try {
await navigator.clipboard.writeText(content);
// 添加 copied 类,切换为对勾图标
target.classList.add('copied');
// 5秒后恢复
setTimeout(() => {
target.classList.remove('copied');
}, 5000);
} catch (error) {
console.warn('复制失败:', error);
}
},
decodeHtmlEntities(text) {
const textarea = document.createElement('textarea');
textarea.innerHTML = text;
return textarea.value;
}
};