agent-Specialization/static/src/app.ts
2025-11-30 00:09:05 +08:00

2360 lines
100 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
// static/app-enhanced.js - 修复版正确实现Token实时更新
import katex from 'katex';
import { mapActions, mapState, mapWritableState } from 'pinia';
import ChatArea from './components/chat/ChatArea.vue';
import ConversationSidebar from './components/sidebar/ConversationSidebar.vue';
import LeftPanel from './components/panels/LeftPanel.vue';
import FocusPanel from './components/panels/FocusPanel.vue';
import TokenDrawer from './components/token/TokenDrawer.vue';
import PersonalizationDrawer from './components/personalization/PersonalizationDrawer.vue';
import QuickMenu from './components/input/QuickMenu.vue';
import InputComposer from './components/input/InputComposer.vue';
import AppShell from './components/shell/AppShell.vue';
import { useUiStore } from './stores/ui';
import { useConversationStore } from './stores/conversation';
import { useChatStore } from './stores/chat';
import { useInputStore } from './stores/input';
import { useToolStore } from './stores/tool';
import { useResourceStore } from './stores/resource';
import { useUploadStore } from './stores/upload';
import { useFileStore } from './stores/file';
import { useSubAgentStore } from './stores/subAgent';
import { useFocusStore } from './stores/focus';
import { usePersonalizationStore } from './stores/personalization';
import { useChatActionStore } from './stores/chatActions';
import { ICONS, TOOL_CATEGORY_ICON_MAP } from './utils/icons';
import { initializeLegacySocket } from './composables/useLegacySocket';
import { useConnectionStore } from './stores/connection';
import { useEasterEgg } from './composables/useEasterEgg';
import { renderMarkdown as renderMarkdownHelper } from './composables/useMarkdownRenderer';
import {
formatTokenCount,
formatBytes,
formatPercentage,
formatRate,
formatResetTime,
formatQuotaValue,
quotaTypeLabel,
buildQuotaResetSummary,
isQuotaExceeded as isQuotaExceededUtil,
buildQuotaToastMessage
} from './utils/formatters';
import {
getToolIcon,
getToolAnimationClass,
getToolStatusText,
getToolDescription,
cloneToolArguments,
buildToolLabel,
formatSearchTopic,
formatSearchTime,
getLanguageClass
} from './utils/chatDisplay';
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';
window.katex = katex;
function updateViewportHeightVar() {
const docEl = document.documentElement;
const visualViewport = window.visualViewport;
if (visualViewport) {
const vh = visualViewport.height;
const bottomInset = Math.max(
0,
(window.innerHeight || docEl.clientHeight || vh) - visualViewport.height - visualViewport.offsetTop
);
docEl.style.setProperty('--app-viewport', `${vh}px`);
docEl.style.setProperty('--app-bottom-inset', `${bottomInset}px`);
} else {
const height = window.innerHeight || docEl.clientHeight;
if (height) {
docEl.style.setProperty('--app-viewport', `${height}px`);
}
docEl.style.setProperty('--app-bottom-inset', 'env(safe-area-inset-bottom, 0px)');
}
}
updateViewportHeightVar();
window.addEventListener('resize', updateViewportHeightVar);
window.addEventListener('orientationchange', updateViewportHeightVar);
window.addEventListener('pageshow', updateViewportHeightVar);
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', updateViewportHeightVar);
window.visualViewport.addEventListener('scroll', updateViewportHeightVar);
}
const ENABLE_APP_DEBUG_LOGS = false;
function debugLog(...args) {
if (!ENABLE_APP_DEBUG_LOGS) {
return;
}
debugLog(...args);
}
const appOptions = {
data() {
return {
// 路由相关
initialRouteResolved: false,
// 工具状态跟踪
preparingTools: new Map(),
activeTools: new Map(),
toolActionIndex: new Map(),
toolStacks: new Map(),
// ==========================================
// 对话管理相关状态
// ==========================================
// 搜索功能
// ==========================================
// Token统计相关状态修复版
// ==========================================
// 对话压缩状态
compressing: false,
skipConversationLoadedEvent: false,
skipConversationHistoryReload: false,
_scrollListenerReady: false,
historyLoading: false,
mobileViewportQuery: null,
// 工具控制菜单
icons: ICONS,
toolCategoryIcons: TOOL_CATEGORY_ICON_MAP
}
},
created() {
const actionStore = useChatActionStore();
actionStore.registerDependencies({
pushToast: (payload) => this.uiPushToast(payload),
autoResizeInput: () => this.autoResizeInput(),
focusComposer: () => {
const inputEl = this.getComposerElement('stadiumInput');
if (inputEl && typeof inputEl.focus === 'function') {
inputEl.focus();
}
},
isConnected: () => this.isConnected,
getSocket: () => this.socket,
downloadResource: (url, filename) => this.downloadResource(url, filename)
});
},
async mounted() {
debugLog('Vue应用已挂载');
if (window.ensureCsrfToken) {
window.ensureCsrfToken().catch((err) => {
console.warn('CSRF token 初始化失败:', err);
});
}
await this.bootstrapRoute();
await this.initSocket();
this.$nextTick(() => {
this.ensureScrollListener();
});
// 延迟加载初始数据
setTimeout(() => {
this.loadInitialData();
}, 500);
document.addEventListener('click', this.handleClickOutsideQuickMenu);
document.addEventListener('click', this.handleClickOutsidePanelMenu);
document.addEventListener('click', this.handleClickOutsideMobileMenu);
window.addEventListener('popstate', this.handlePopState);
window.addEventListener('keydown', this.handleMobileOverlayEscape);
this.setupMobileViewportWatcher();
this.subAgentFetch();
this.subAgentStartPolling();
this.$nextTick(() => {
this.autoResizeInput();
});
this.resourceStartContainerStatsPolling();
this.resourceStartProjectStoragePolling();
this.resourceStartUsageQuotaPolling();
},
computed: {
...mapWritableState(useConnectionStore, [
'isConnected',
'socket',
'stopRequested',
'projectPath',
'agentVersion',
'thinkingMode'
]),
...mapState(useFileStore, ['contextMenu', 'fileTree', 'expandedFolders', 'todoList']),
...mapWritableState(useUiStore, [
'sidebarCollapsed',
'workspaceCollapsed',
'panelMode',
'panelMenuOpen',
'leftWidth',
'rightWidth',
'rightCollapsed',
'isResizing',
'resizingPanel',
'minPanelWidth',
'maxPanelWidth',
'quotaToast',
'toastQueue',
'confirmDialog',
'easterEgg',
'isMobileViewport',
'mobileOverlayMenuOpen',
'activeMobileOverlay'
]),
...mapWritableState(useConversationStore, [
'conversations',
'conversationsLoading',
'hasMoreConversations',
'loadingMoreConversations',
'currentConversationId',
'currentConversationTitle',
'searchQuery',
'searchTimer',
'conversationsOffset',
'conversationsLimit'
]),
...mapWritableState(useChatStore, [
'messages',
'currentMessageIndex',
'streamingMessage',
'expandedBlocks',
'autoScrollEnabled',
'userScrolling',
'thinkingScrollLocks'
]),
...mapWritableState(useInputStore, [
'inputMessage',
'inputLineCount',
'inputIsMultiline',
'inputIsFocused',
'quickMenuOpen',
'toolMenuOpen',
'settingsOpen'
]),
...mapWritableState(useToolStore, [
'preparingTools',
'activeTools',
'toolActionIndex',
'toolStacks',
'toolSettings',
'toolSettingsLoading'
]),
...mapWritableState(useResourceStore, [
'tokenPanelCollapsed',
'currentContextTokens',
'currentConversationTokens',
'projectStorage',
'containerStatus',
'containerNetRate',
'usageQuota'
]),
...mapWritableState(useFocusStore, ['focusedFiles']),
...mapWritableState(useUploadStore, ['uploading'])
},
beforeUnmount() {
document.removeEventListener('click', this.handleClickOutsideQuickMenu);
document.removeEventListener('click', this.handleClickOutsidePanelMenu);
document.removeEventListener('click', this.handleClickOutsideMobileMenu);
window.removeEventListener('popstate', this.handlePopState);
window.removeEventListener('keydown', this.handleMobileOverlayEscape);
this.teardownMobileViewportWatcher();
this.subAgentStopPolling();
this.resourceStopContainerStatsPolling();
this.resourceStopProjectStoragePolling();
this.resourceStopUsageQuotaPolling();
const cleanup = this.destroyEasterEggEffect(true);
if (cleanup && typeof cleanup.catch === 'function') {
cleanup.catch(() => {});
}
},
watch: {
inputMessage() {
this.autoResizeInput();
},
currentConversationId: {
immediate: false,
handler(newValue, oldValue) {
debugLog('currentConversationId 变化', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload });
this.logMessageState('watch:currentConversationId', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload });
if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) {
return;
}
if (this.skipConversationHistoryReload) {
this.skipConversationHistoryReload = false;
return;
}
if (oldValue && newValue === oldValue) {
return;
}
this.fetchAndDisplayHistory();
this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens();
}
}
},
methods: {
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();
},
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;
}
const nextState = !this.workspaceCollapsed;
this.uiSetWorkspaceCollapsed(nextState);
if (nextState) {
this.uiSetPanelMenuOpen(false);
}
},
handleMobileOverlayEscape(event) {
if (event.key !== 'Escape' || !this.isMobileViewport) {
return;
}
if (this.mobileOverlayMenuOpen) {
this.uiSetMobileOverlayMenuOpen(false);
return;
}
if (this.activeMobileOverlay) {
this.closeMobileOverlay();
}
},
...mapActions(useUiStore, {
uiToggleSidebar: 'toggleSidebar',
uiSetSidebarCollapsed: 'setSidebarCollapsed',
uiSetWorkspaceCollapsed: 'setWorkspaceCollapsed',
uiToggleWorkspaceCollapsed: 'toggleWorkspaceCollapsed',
uiSetPanelMode: 'setPanelMode',
uiSetPanelMenuOpen: 'setPanelMenuOpen',
uiTogglePanelMenu: 'togglePanelMenu',
uiSetMobileViewport: 'setIsMobileViewport',
uiSetMobileOverlayMenuOpen: 'setMobileOverlayMenuOpen',
uiToggleMobileOverlayMenu: 'toggleMobileOverlayMenu',
uiSetActiveMobileOverlay: 'setActiveMobileOverlay',
uiCloseMobileOverlay: 'closeMobileOverlay',
uiPushToast: 'pushToast',
uiUpdateToast: 'updateToast',
uiDismissToast: 'dismissToast',
uiShowQuotaToastMessage: 'showQuotaToastMessage',
uiDismissQuotaToast: 'dismissQuotaToast',
uiRequestConfirm: 'requestConfirm',
uiResolveConfirm: 'resolveConfirm'
}),
...mapActions(useChatStore, {
chatExpandBlock: 'expandBlock',
chatCollapseBlock: 'collapseBlock',
chatClearExpandedBlocks: 'clearExpandedBlocks',
chatSetThinkingLock: 'setThinkingLock',
chatClearThinkingLocks: 'clearThinkingLocks',
chatSetScrollState: 'setScrollState',
chatEnableAutoScroll: 'enableAutoScroll',
chatDisableAutoScroll: 'disableAutoScroll',
chatToggleScrollLockState: 'toggleScrollLockState',
chatAddUserMessage: 'addUserMessage',
chatStartAssistantMessage: 'startAssistantMessage',
chatStartThinkingAction: 'startThinkingAction',
chatAppendThinkingChunk: 'appendThinkingChunk',
chatCompleteThinkingAction: 'completeThinking',
chatStartTextAction: 'startTextAction',
chatAppendTextChunk: 'appendTextChunk',
chatCompleteTextAction: 'completeText',
chatAddSystemMessage: 'addSystemMessage',
chatAddAppendPayloadAction: 'addAppendPayloadAction',
chatAddModifyPayloadAction: 'addModifyPayloadAction',
chatEnsureAssistantMessage: 'ensureAssistantMessage'
}),
...mapActions(useInputStore, {
inputSetFocused: 'setInputFocused',
inputToggleQuickMenu: 'toggleQuickMenu',
inputCloseMenus: 'closeMenus',
inputOpenQuickMenu: 'openQuickMenu',
inputSetQuickMenuOpen: 'setQuickMenuOpen',
inputToggleToolMenu: 'toggleToolMenu',
inputSetToolMenuOpen: 'setToolMenuOpen',
inputToggleSettingsMenu: 'toggleSettingsMenu',
inputSetSettingsOpen: 'setSettingsOpen',
inputSetMessage: 'setInputMessage',
inputClearMessage: 'clearInputMessage',
inputSetLineCount: 'setInputLineCount',
inputSetMultiline: 'setInputMultiline'
}),
...mapActions(useToolStore, {
toolRegisterAction: 'registerToolAction',
toolUnregisterAction: 'unregisterToolAction',
toolFindAction: 'findToolAction',
toolTrackAction: 'trackToolAction',
toolReleaseAction: 'releaseToolAction',
toolGetLatestAction: 'getLatestActiveToolAction',
toolResetTracking: 'resetToolTracking',
toolSetSettings: 'setToolSettings',
toolSetSettingsLoading: 'setToolSettingsLoading'
}),
...mapActions(useResourceStore, {
resourceUpdateCurrentContextTokens: 'updateCurrentContextTokens',
resourceFetchConversationTokenStatistics: 'fetchConversationTokenStatistics',
resourceSetCurrentContextTokens: 'setCurrentContextTokens',
resourceToggleTokenPanel: 'toggleTokenPanel',
resourceApplyStatusSnapshot: 'applyStatusSnapshot',
resourceUpdateContainerStatus: 'updateContainerStatus',
resourceStartContainerStatsPolling: 'startContainerStatsPolling',
resourceStopContainerStatsPolling: 'stopContainerStatsPolling',
resourceStartProjectStoragePolling: 'startProjectStoragePolling',
resourceStopProjectStoragePolling: 'stopProjectStoragePolling',
resourceStartUsageQuotaPolling: 'startUsageQuotaPolling',
resourceStopUsageQuotaPolling: 'stopUsageQuotaPolling',
resourcePollContainerStats: 'pollContainerStats',
resourcePollProjectStorage: 'pollProjectStorage',
resourceFetchUsageQuota: 'fetchUsageQuota',
resourceResetTokenStatistics: 'resetTokenStatistics',
resourceSetUsageQuota: 'setUsageQuota'
}),
...mapActions(useUploadStore, {
uploadHandleSelected: 'handleSelectedFiles'
}),
...mapActions(useFileStore, {
fileFetchTree: 'fetchFileTree',
fileSetTreeFromResponse: 'setFileTreeFromResponse',
fileFetchTodoList: 'fetchTodoList',
fileSetTodoList: 'setTodoList',
fileHideContextMenu: 'hideContextMenu'
}),
...mapActions(useSubAgentStore, {
subAgentFetch: 'fetchSubAgents',
subAgentStartPolling: 'startPolling',
subAgentStopPolling: 'stopPolling'
}),
...mapActions(useFocusStore, {
focusFetchFiles: 'fetchFocusedFiles',
focusSetFiles: 'setFocusedFiles'
}),
...mapActions(usePersonalizationStore, {
personalizationOpenDrawer: 'openDrawer'
}),
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;
},
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;
},
toolCategoryIcon(categoryId) {
return this.toolCategoryIcons[categoryId] || 'settings';
},
openGuiFileManager() {
window.open('/file-manager', '_blank');
},
findMessageByAction(action) {
if (!action) {
return null;
}
for (const message of this.messages) {
if (!message.actions) {
continue;
}
if (message.actions.includes(action)) {
return message;
}
}
return null;
},
renderMarkdown(content, isStreaming = false) {
return renderMarkdownHelper(content, isStreaming);
},
hasContainerStats() {
return !!(
this.containerStatus &&
this.containerStatus.mode === 'docker' &&
this.containerStatus.stats
);
},
containerStatusClass() {
if (!this.containerStatus) {
return 'status-pill--host';
}
if (this.containerStatus.mode !== 'docker') {
return 'status-pill--host';
}
const rawStatus = (
this.containerStatus.state &&
(this.containerStatus.state.status || this.containerStatus.state.Status)
) || '';
const status = String(rawStatus).toLowerCase();
if (status.includes('running')) {
return 'status-pill--running';
}
if (status.includes('paused')) {
return 'status-pill--stopped';
}
if (status.includes('exited') || status.includes('dead')) {
return 'status-pill--stopped';
}
return 'status-pill--running';
},
containerStatusText() {
if (!this.containerStatus) {
return '未知';
}
if (this.containerStatus.mode !== 'docker') {
return '宿主机模式';
}
const rawStatus = (
this.containerStatus.state &&
(this.containerStatus.state.status || this.containerStatus.state.Status)
) || '';
const status = String(rawStatus).toLowerCase();
if (status.includes('running')) {
return '运行中';
}
if (status.includes('paused')) {
return '已暂停';
}
if (status.includes('exited') || status.includes('dead')) {
return '已停止';
}
return rawStatus || '容器模式';
},
formatTime(value) {
if (!value) {
return '未知时间';
}
let date;
if (typeof value === 'number') {
date = new Date(value);
} else if (typeof value === 'string') {
const parsed = Date.parse(value);
if (!Number.isNaN(parsed)) {
date = new Date(parsed);
} else {
const numeric = Number(value);
if (!Number.isNaN(numeric)) {
date = new Date(numeric);
}
}
} else if (value instanceof Date) {
date = value;
}
if (!date || Number.isNaN(date.getTime())) {
return String(value);
}
const now = Date.now();
const diff = now - date.getTime();
if (diff < 60000) {
return '刚刚';
}
if (diff < 3600000) {
const mins = Math.floor(diff / 60000);
return `${mins} 分钟前`;
}
if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours} 小时前`;
}
const formatter = new Intl.DateTimeFormat('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
return formatter.format(date);
},
async bootstrapRoute() {
const path = window.location.pathname.replace(/^\/+/, '');
if (!path || path === 'new') {
this.currentConversationId = null;
this.currentConversationTitle = '新对话';
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 || '对话';
history.replaceState({ conversationId: convId }, '', `/${this.stripConversationPrefix(convId)}`);
} else {
history.replaceState({}, '', '/new');
this.currentConversationId = null;
this.currentConversationTitle = '新对话';
}
} catch (error) {
console.warn('初始化路由失败:', error);
history.replaceState({}, '', '/new');
this.currentConversationId = null;
this.currentConversationTitle = '新对话';
} 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;
},
async downloadFile(path) {
if (!path) {
this.fileHideContextMenu();
return;
}
const url = `/api/download/file?path=${encodeURIComponent(path)}`;
const name = path.split('/').pop() || 'file';
await this.downloadResource(url, name);
},
async downloadFolder(path) {
if (!path) {
this.fileHideContextMenu();
return;
}
const url = `/api/download/folder?path=${encodeURIComponent(path)}`;
const segments = path.split('/').filter(Boolean);
const folderName = segments.length ? segments.pop() : 'folder';
await this.downloadResource(url, `${folderName}.zip`);
},
async downloadResource(url, filename) {
try {
const response = await fetch(url);
if (!response.ok) {
let message = response.statusText;
try {
const errorData = await response.json();
message = errorData.error || errorData.message || message;
} catch (err) {
message = await response.text();
}
this.uiPushToast({
title: '下载失败',
message: message || '无法完成下载',
type: 'error'
});
return;
}
const blob = await response.blob();
const downloadName = filename || 'download';
const link = document.createElement('a');
const href = URL.createObjectURL(blob);
link.href = href;
link.download = downloadName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
} catch (error) {
console.error('下载失败:', error);
this.uiPushToast({
title: '下载失败',
message: error.message || String(error),
type: 'error'
});
} finally {
this.fileHideContextMenu();
}
},
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;
if (isAtBottom) {
this.chatSetScrollState({ userScrolling: false, autoScrollEnabled: true });
} else {
this.chatSetScrollState({ userScrolling: true, autoScrollEnabled: false });
}
});
},
async initSocket() {
await initializeLegacySocket(this);
},
cleanupStaleToolActions() {
this.messages.forEach(msg => {
if (!msg.actions) {
return;
}
msg.actions.forEach(action => {
if (action.type !== 'tool' || !action.tool) {
return;
}
if (['running', 'preparing'].includes(action.tool.status)) {
action.tool.status = 'stale';
action.tool.message = action.tool.message || '已被新的响应中断';
this.toolUnregisterAction(action);
}
});
});
this.preparingTools.clear();
this.toolActionIndex.clear();
},
hasPendingToolActions() {
const mapHasEntries = map => map && typeof map.size === 'number' && map.size > 0;
if (mapHasEntries(this.preparingTools) || mapHasEntries(this.activeTools)) {
return true;
}
if (!Array.isArray(this.messages)) {
return false;
}
return this.messages.some(msg => {
if (!msg || msg.role !== 'assistant' || !Array.isArray(msg.actions)) {
return false;
}
return msg.actions.some(action => {
if (!action || action.type !== 'tool' || !action.tool) {
return false;
}
if (action.tool.awaiting_content) {
return true;
}
const status = typeof action.tool.status === 'string'
? action.tool.status.toLowerCase()
: '';
return !status || ['preparing', 'running', 'pending', 'queued'].includes(status);
});
});
},
maybeResetStreamingState(reason = 'unspecified') {
if (!this.streamingMessage) {
return false;
}
if (this.hasPendingToolActions()) {
return false;
}
this.streamingMessage = false;
this.stopRequested = false;
debugLog('流式状态已结束', { reason });
return true;
},
// 完整重置所有状态
resetAllStates(reason = 'unspecified') {
debugLog('重置所有前端状态', { reason, conversationId: this.currentConversationId });
this.logMessageState('resetAllStates:before-cleanup', { reason });
this.fileHideContextMenu();
// 重置消息和流状态
this.streamingMessage = false;
this.currentMessageIndex = -1;
this.stopRequested = false;
// 清理工具状态
this.toolResetTracking();
// 新增:将所有未完成的工具标记为已完成
this.messages.forEach(msg => {
if (msg.role === 'assistant' && msg.actions) {
msg.actions.forEach(action => {
if (action.type === 'tool' &&
(action.tool.status === 'preparing' || action.tool.status === 'running')) {
action.tool.status = 'completed';
}
});
}
});
// 重置滚动状态
this.chatEnableAutoScroll();
// 清理Markdown缓存
if (this.markdownCache) {
this.markdownCache.clear();
}
this.chatClearThinkingLocks();
// 强制更新视图
this.$forceUpdate();
this.inputSetSettingsOpen(false);
this.inputSetToolMenuOpen(false);
this.inputSetQuickMenuOpen(false);
this.inputSetLineCount(1);
this.inputSetMultiline(false);
this.inputClearMessage();
this.toolSetSettingsLoading(false);
this.toolSetSettings([]);
debugLog('前端状态重置完成');
this._scrollListenerReady = false;
this.$nextTick(() => {
this.ensureScrollListener();
});
this.logMessageState('resetAllStates:after-cleanup', { reason });
},
// 重置Token统计
openRealtimeTerminal() {
const { protocol, hostname, port } = window.location;
const target = `${protocol}//${hostname}${port ? ':' + port : ''}/terminal`;
window.open(target, '_blank');
},
resetTokenStatistics() {
this.resourceResetTokenStatistics();
},
async loadInitialData() {
try {
debugLog('加载初始数据...');
await this.fileFetchTree();
await this.focusFetchFiles();
await this.fileFetchTodoList();
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);
await this.fetchUsageQuota();
// 获取当前对话信息
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
if (statusConversationId) {
if (!this.currentConversationId) {
this.skipConversationHistoryReload = true;
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;
}
// 初始化时调用一次,因为 skipConversationHistoryReload 会阻止 watch 触发
await this.fetchAndDisplayHistory();
// 获取当前对话的Token统计
this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens();
} catch (e) {
console.warn('获取当前对话标题失败:', e);
}
}
}
await this.loadToolSettings(true);
debugLog('初始数据加载完成');
} catch (error) {
console.error('加载初始数据失败:', error);
}
},
// ==========================================
// Token / 资源状态封装Pinia store
// ==========================================
async updateCurrentContextTokens() {
await this.resourceUpdateCurrentContextTokens(this.currentConversationId);
},
async fetchConversationTokenStatistics() {
await this.resourceFetchConversationTokenStatistics(this.currentConversationId);
},
toggleTokenPanel() {
this.resourceToggleTokenPanel();
},
applyStatusSnapshot(status) {
this.resourceApplyStatusSnapshot(status);
},
updateContainerStatus(status) {
this.resourceUpdateContainerStatus(status);
},
pollContainerStats() {
return this.resourcePollContainerStats();
},
startContainerStatsPolling() {
this.resourceStartContainerStatsPolling();
},
stopContainerStatsPolling() {
this.resourceStopContainerStatsPolling();
},
pollProjectStorage() {
return this.resourcePollProjectStorage();
},
startProjectStoragePolling() {
this.resourceStartProjectStoragePolling();
},
stopProjectStoragePolling() {
this.resourceStopProjectStoragePolling();
},
fetchUsageQuota() {
return this.resourceFetchUsageQuota();
},
startUsageQuotaPolling() {
this.resourceStartUsageQuotaPolling();
},
stopUsageQuotaPolling() {
this.resourceStopUsageQuotaPolling();
},
// ==========================================
// 对话管理核心功能
// ==========================================
async loadConversationsList() {
this.conversationsLoading = true;
try {
const response = await fetch(`/api/conversations?limit=${this.conversationsLimit}&offset=${this.conversationsOffset}`);
const data = await response.json();
if (data.success) {
if (this.conversationsOffset === 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) {
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 {
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) {
debugLog('加载对话:', conversationId);
this.logMessageState('loadConversation:start', { conversationId });
if (conversationId === this.currentConversationId) {
debugLog('已是当前对话,跳过加载');
return;
}
try {
// 1. 调用加载API
const response = await fetch(`/api/conversations/${conversationId}/load`, {
method: 'PUT'
});
const result = await response.json();
if (result.success) {
debugLog('对话加载API成功:', result);
// 2. 更新当前对话信息
this.skipConversationHistoryReload = true;
this.currentConversationId = conversationId;
this.currentConversationTitle = result.title;
this.promoteConversationToTop(conversationId);
history.pushState({ conversationId }, '', `/${this.stripConversationPrefix(conversationId)}`);
this.skipConversationLoadedEvent = true;
// 3. 重置UI状态
this.resetAllStates(`loadConversation:${conversationId}`);
this.subAgentFetch();
this.fetchTodoList();
// 4. 历史对话内容和Token统计由后端的 conversation_loaded 事件触发
// 不在此处重复调用,避免双重加载
// Socket.IO 的 conversation_loaded 事件会处理:
// - fetchAndDisplayHistory()
// - fetchConversationTokenStatistics()
// - updateCurrentContextTokens()
} 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'
});
}
},
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 fetchAndDisplayHistory() {
if (this.historyLoading) {
debugLog('历史消息正在加载,跳过重复请求');
return;
}
this.historyLoading = true;
try {
debugLog('开始获取历史对话内容...');
this.logMessageState('fetchAndDisplayHistory:start', { conversationId: this.currentConversationId });
if (!this.currentConversationId || this.currentConversationId.startsWith('temp_')) {
debugLog('没有当前对话ID跳过历史加载');
return;
}
try {
// 使用专门的API获取对话消息历史
const messagesResponse = await fetch(`/api/conversations/${this.currentConversationId}/messages`);
if (!messagesResponse.ok) {
console.warn('无法获取消息历史,尝试备用方法');
// 备用方案通过状态API获取
const statusResponse = await fetch('/api/status');
const status = await statusResponse.json();
debugLog('系统状态:', status);
this.applyStatusSnapshot(status);
// 如果状态中有对话历史字段
if (status.conversation_history && Array.isArray(status.conversation_history)) {
this.renderHistoryMessages(status.conversation_history);
return;
}
debugLog('备用方案也无法获取历史消息');
return;
}
const messagesData = await messagesResponse.json();
debugLog('获取到消息数据:', messagesData);
if (messagesData.success && messagesData.data && messagesData.data.messages) {
const messages = messagesData.data.messages;
debugLog(`发现 ${messages.length} 条历史消息`);
if (messages.length > 0) {
// 清空当前显示的消息
this.logMessageState('fetchAndDisplayHistory:before-clear-existing');
this.messages = [];
this.logMessageState('fetchAndDisplayHistory:after-clear-existing');
// 渲染历史消息 - 这是关键功能
this.renderHistoryMessages(messages);
// 滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
debugLog('历史对话内容显示完成');
} else {
debugLog('对话存在但没有历史消息');
this.logMessageState('fetchAndDisplayHistory:no-history-clear');
this.messages = [];
this.logMessageState('fetchAndDisplayHistory:no-history-cleared');
}
} else {
debugLog('消息数据格式不正确:', messagesData);
this.logMessageState('fetchAndDisplayHistory:invalid-data-clear');
this.messages = [];
this.logMessageState('fetchAndDisplayHistory:invalid-data-cleared');
}
} catch (error) {
console.error('获取历史对话失败:', error);
debugLog('尝试不显示错误弹窗,仅在控制台记录');
// 不显示alert避免打断用户体验
this.logMessageState('fetchAndDisplayHistory:error-clear', { error: error?.message || String(error) });
this.messages = [];
this.logMessageState('fetchAndDisplayHistory:error-cleared');
}
} finally {
this.historyLoading = false;
}
},
// ==========================================
// 关键功能:渲染历史消息
// ==========================================
renderHistoryMessages(historyMessages) {
debugLog('开始渲染历史消息...', historyMessages);
debugLog('历史消息数量:', historyMessages.length);
this.logMessageState('renderHistoryMessages:start', { historyCount: historyMessages.length });
if (!Array.isArray(historyMessages)) {
console.error('历史消息不是数组格式');
return;
}
let currentAssistantMessage = null;
historyMessages.forEach((message, index) => {
debugLog(`处理消息 ${index + 1}/${historyMessages.length}:`, message.role, message);
if (message.role === 'user') {
// 用户消息 - 先结束之前的assistant消息
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
this.messages.push(currentAssistantMessage);
currentAssistantMessage = null;
}
this.messages.push({
role: 'user',
content: message.content || ''
});
debugLog('添加用户消息:', message.content?.substring(0, 50) + '...');
} else if (message.role === 'assistant') {
// AI消息 - 如果没有当前assistant消息创建一个
if (!currentAssistantMessage) {
currentAssistantMessage = {
role: 'assistant',
actions: [],
streamingThinking: '',
streamingText: '',
currentStreamingType: null,
activeThinkingId: null,
awaitingFirstContent: false,
generatingLabel: ''
};
}
const content = message.content || '';
let reasoningText = (message.reasoning_content || '').trim();
if (!reasoningText) {
const thinkPatterns = [
/<think>([\s\S]*?)<\/think>/g,
/<thinking>([\s\S]*?)<\/thinking>/g
];
let extracted = '';
for (const pattern of thinkPatterns) {
let match;
while ((match = pattern.exec(content)) !== null) {
extracted += (match[1] || '').trim() + '\n';
}
}
reasoningText = extracted.trim();
}
if (reasoningText) {
const blockId = `history-thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`;
currentAssistantMessage.actions.push({
id: `history-think-${Date.now()}-${Math.random()}`,
type: 'thinking',
content: reasoningText,
streaming: false,
timestamp: Date.now(),
blockId
});
debugLog('添加思考内容:', reasoningText.substring(0, 50) + '...');
}
// 处理普通文本内容(移除思考标签后的内容)
const metadata = message.metadata || {};
const appendPayloadMeta = metadata.append_payload;
const modifyPayloadMeta = metadata.modify_payload;
const isAppendMessage = message.name === 'append_to_file';
const isModifyMessage = message.name === 'modify_file';
const containsAppendMarkers = /<<<\s*(APPEND|MODIFY)/i.test(content || '') || /<<<END_\s*(APPEND|MODIFY)>>>/i.test(content || '');
let textContent = content;
if (!message.reasoning_content) {
textContent = textContent
.replace(/<think>[\s\S]*?<\/think>/g, '')
.replace(/<thinking>[\s\S]*?<\/thinking>/g, '')
.trim();
} else {
textContent = textContent.trim();
}
if (appendPayloadMeta) {
currentAssistantMessage.actions.push({
id: `history-append-payload-${Date.now()}-${Math.random()}`,
type: 'append_payload',
append: {
path: appendPayloadMeta.path || '未知文件',
forced: !!appendPayloadMeta.forced,
success: appendPayloadMeta.success === undefined ? true : !!appendPayloadMeta.success,
lines: appendPayloadMeta.lines ?? null,
bytes: appendPayloadMeta.bytes ?? null
},
timestamp: Date.now()
});
debugLog('添加append占位信息:', appendPayloadMeta.path);
} else if (modifyPayloadMeta) {
currentAssistantMessage.actions.push({
id: `history-modify-payload-${Date.now()}-${Math.random()}`,
type: 'modify_payload',
modify: {
path: modifyPayloadMeta.path || '未知文件',
total: modifyPayloadMeta.total_blocks ?? null,
completed: modifyPayloadMeta.completed || [],
failed: modifyPayloadMeta.failed || [],
forced: !!modifyPayloadMeta.forced,
details: modifyPayloadMeta.details || []
},
timestamp: Date.now()
});
debugLog('添加modify占位信息:', modifyPayloadMeta.path);
}
if (textContent && !appendPayloadMeta && !modifyPayloadMeta && !isAppendMessage && !isModifyMessage && !containsAppendMarkers) {
currentAssistantMessage.actions.push({
id: `history-text-${Date.now()}-${Math.random()}`,
type: 'text',
content: textContent,
streaming: false,
timestamp: Date.now()
});
debugLog('添加文本内容:', textContent.substring(0, 50) + '...');
}
// 处理工具调用
if (message.tool_calls && Array.isArray(message.tool_calls)) {
message.tool_calls.forEach((toolCall, tcIndex) => {
let arguments_obj = {};
try {
arguments_obj = typeof toolCall.function.arguments === 'string'
? JSON.parse(toolCall.function.arguments || '{}')
: (toolCall.function.arguments || {});
} catch (e) {
console.warn('解析工具参数失败:', e);
arguments_obj = {};
}
currentAssistantMessage.actions.push({
id: `history-tool-${toolCall.id || Date.now()}-${tcIndex}`,
type: 'tool',
tool: {
id: toolCall.id,
name: toolCall.function.name,
arguments: arguments_obj,
status: 'preparing',
result: null
},
timestamp: Date.now()
});
debugLog('添加工具调用:', toolCall.function.name);
});
}
} else if (message.role === 'tool') {
// 工具结果 - 更新当前assistant消息中对应的工具
if (currentAssistantMessage) {
// 查找对应的工具action - 使用更灵活的匹配
let toolAction = null;
// 优先按tool_call_id匹配
if (message.tool_call_id) {
toolAction = currentAssistantMessage.actions.find(action =>
action.type === 'tool' &&
action.tool.id === message.tool_call_id
);
}
// 如果找不到按name匹配最后一个同名工具
if (!toolAction && message.name) {
const sameNameTools = currentAssistantMessage.actions.filter(action =>
action.type === 'tool' &&
action.tool.name === message.name
);
toolAction = sameNameTools[sameNameTools.length - 1]; // 取最后一个
}
if (toolAction) {
// 解析工具结果
let result;
try {
// 尝试解析为JSON
result = JSON.parse(message.content);
} catch (e) {
// 如果不是JSON就作为纯文本
result = {
output: message.content,
success: true
};
}
toolAction.tool.status = 'completed';
toolAction.tool.result = result;
if (result && typeof result === 'object') {
if (result.error) {
toolAction.tool.message = result.error;
} else if (result.message && !toolAction.tool.message) {
toolAction.tool.message = result.message;
}
}
if (message.name === 'append_to_file' && result && result.message) {
toolAction.tool.message = result.message;
}
debugLog(`更新工具结果: ${message.name} -> ${message.content?.substring(0, 50)}...`);
// append_to_file 的摘要在 append_payload 占位中呈现,此处无需重复
} else {
console.warn('找不到对应的工具调用:', message.name, message.tool_call_id);
}
}
} else {
// 其他类型消息如system- 先结束当前assistant消息
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
this.messages.push(currentAssistantMessage);
currentAssistantMessage = null;
}
debugLog('处理其他类型消息:', message.role);
this.messages.push({
role: message.role,
content: message.content || ''
});
}
});
// 处理最后一个assistant消息
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
this.messages.push(currentAssistantMessage);
}
debugLog(`历史消息渲染完成,共 ${this.messages.length} 条消息`);
this.logMessageState('renderHistoryMessages:after-render');
// 强制更新视图
this.$forceUpdate();
// 确保滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
setTimeout(() => {
const blockCount = this.$el && this.$el.querySelectorAll
? this.$el.querySelectorAll('.message-block').length
: 'N/A';
debugLog('[Messages] DOM 渲染统计', {
blocks: blockCount,
conversationId: this.currentConversationId
});
}, 0);
});
},
async createNewConversation() {
debugLog('创建新对话...');
this.logMessageState('createNewConversation:start');
try {
const response = await fetch('/api/conversations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
thinking_mode: this.thinkingMode
})
});
const result = await response.json();
if (result.success) {
debugLog('新对话创建成功:', result.conversation_id);
// 清空当前消息
this.logMessageState('createNewConversation:before-clear');
this.messages = [];
this.logMessageState('createNewConversation:after-clear');
this.currentConversationId = result.conversation_id;
this.currentConversationTitle = '新对话';
history.pushState({ conversationId: this.currentConversationId }, '', `/${this.stripConversationPrefix(this.currentConversationId)}`);
// 重置Token统计
this.resetTokenStatistics();
// 重置状态
this.resetAllStates('createNewConversation');
// 刷新对话列表
this.conversationsOffset = 0;
await this.loadConversationsList();
} 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;
}
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');
}
// 刷新对话列表
this.conversationsOffset = 0;
await this.loadConversationsList();
} 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 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;
if (newId) {
this.currentConversationId = newId;
}
this.conversationsOffset = 0;
await this.loadConversationsList();
} 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'
});
}
},
searchConversations() {
// 简单的搜索功能实际实现可以调用搜索API
if (this.searchTimer) {
clearTimeout(this.searchTimer);
}
this.searchTimer = setTimeout(() => {
if (this.searchQuery.trim()) {
debugLog('搜索对话:', this.searchQuery);
// TODO: 实现搜索API调用
// this.searchConversationsAPI(this.searchQuery);
} else {
// 清空搜索,重新加载全部对话
this.conversationsOffset = 0;
this.loadConversationsList();
}
}, 300);
},
handleSidebarSearch(value) {
this.searchQuery = value;
this.searchConversations();
},
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() {
this.personalizationOpenDrawer();
},
fetchTodoList() {
return this.fileFetchTodoList();
},
fetchSubAgents() {
return this.subAgentFetch();
},
async toggleThinkingMode() {
const nextMode = !this.thinkingMode;
try {
const response = await fetch('/api/thinking-mode', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ thinking_mode: nextMode })
});
const data = await response.json();
if (response.ok && data.success) {
const actual = typeof data.data === 'boolean' ? data.data : nextMode;
this.thinkingMode = actual;
return;
}
throw new Error(data.message || data.error || '切换失败');
} catch (error) {
console.error('切换思考模式失败:', error);
this.uiPushToast({
title: '切换思考模式失败',
message: error.message || '请稍后重试',
type: 'error'
});
}
},
triggerFileUpload() {
if (this.uploading) {
return;
}
const input = this.getComposerElement('fileUploadInput');
if (input) {
input.click();
}
},
handleFileSelected(files) {
this.uploadHandleSelected(files);
},
handleSendOrStop() {
if (this.streamingMessage) {
this.stopTask();
} else {
this.sendMessage();
}
},
sendMessage() {
if (this.streamingMessage || !this.isConnected) {
return;
}
if (!this.inputMessage.trim()) {
return;
}
const quotaType = this.thinkingMode ? 'thinking' : 'fast';
if (this.isQuotaExceeded(quotaType)) {
this.showQuotaToast({ type: quotaType });
return;
}
const message = this.inputMessage;
if (message.startsWith('/')) {
this.socket.emit('send_command', { command: message });
this.inputClearMessage();
this.autoResizeInput();
return;
}
this.chatAddUserMessage(message);
this.socket.emit('send_message', { message: message, conversation_id: this.currentConversationId });
this.inputClearMessage();
this.inputSetLineCount(1);
this.inputSetMultiline(false);
this.chatEnableAutoScroll();
this.scrollToBottom();
this.autoResizeInput();
// 发送消息后延迟更新当前上下文Token关键修复恢复原逻辑
setTimeout(() => {
if (this.currentConversationId) {
this.updateCurrentContextTokens();
}
}, 1000);
},
// 新增:停止任务方法
stopTask() {
if (this.streamingMessage && !this.stopRequested) {
this.socket.emit('stop_task');
this.stopRequested = true;
debugLog('发送停止请求');
}
},
async clearChat() {
const confirmed = await this.confirmAction({
title: '清除对话',
message: '确定要清除所有对话记录吗?该操作不可撤销。',
confirmText: '清除',
cancelText: '取消'
});
if (confirmed) {
this.socket.emit('send_command', { command: '/clear' });
}
},
async compressConversation() {
if (!this.currentConversationId) {
this.uiPushToast({
title: '无法压缩',
message: '当前没有可压缩的对话。',
type: 'info'
});
return;
}
if (this.compressing) {
return;
}
const confirmed = await this.confirmAction({
title: '压缩对话',
message: '确定要压缩当前对话记录吗?压缩后会生成新的对话副本。',
confirmText: '压缩',
cancelText: '取消'
});
if (!confirmed) {
return;
}
this.compressing = true;
try {
const response = await fetch(`/api/conversations/${this.currentConversationId}/compress`, {
method: 'POST'
});
const result = await response.json();
if (response.ok && result.success) {
const newId = result.compressed_conversation_id;
if (newId) {
this.currentConversationId = newId;
}
debugLog('对话压缩完成:', result);
} else {
const message = result.message || result.error || '压缩失败';
this.uiPushToast({
title: '压缩失败',
message,
type: 'error'
});
}
} catch (error) {
console.error('压缩对话异常:', error);
this.uiPushToast({
title: '压缩对话异常',
message: error.message || '请稍后重试',
type: 'error'
});
} finally {
this.compressing = false;
}
},
toggleToolMenu() {
if (!this.isConnected) {
return;
}
const nextState = this.inputToggleToolMenu();
if (nextState) {
this.inputSetSettingsOpen(false);
if (!this.quickMenuOpen) {
this.inputOpenQuickMenu();
}
this.loadToolSettings(true);
} else {
this.inputSetToolMenuOpen(false);
}
},
toggleQuickMenu() {
if (!this.isConnected) {
return;
}
this.inputToggleQuickMenu();
},
closeQuickMenu() {
this.inputCloseMenus();
},
handleQuickUpload() {
if (this.uploading || !this.isConnected) {
return;
}
this.triggerFileUpload();
},
handleQuickModeToggle() {
if (!this.isConnected || this.streamingMessage) {
return;
}
this.toggleThinkingMode();
},
handleInputChange() {
this.autoResizeInput();
},
handleInputFocus() {
this.inputSetFocused(true);
this.closeQuickMenu();
},
handleInputBlur() {
this.inputSetFocused(false);
},
handleRealtimeTerminalClick() {
if (!this.isConnected) {
return;
}
this.openRealtimeTerminal();
},
handleFocusPanelToggleClick() {
if (!this.isConnected) {
return;
}
this.toggleFocusPanel();
},
handleTokenPanelToggleClick() {
if (!this.currentConversationId) {
return;
}
this.toggleTokenPanel();
},
handleCompressConversationClick() {
if (this.compressing || this.streamingMessage || !this.isConnected) {
return;
}
this.compressConversation();
},
autoResizeInput() {
this.$nextTick(() => {
const textarea = this.getComposerElement('stadiumInput');
if (!textarea || !(textarea instanceof HTMLTextAreaElement)) {
return;
}
const previousHeight = textarea.offsetHeight;
textarea.style.height = 'auto';
const computedStyle = window.getComputedStyle(textarea);
const lineHeight = parseFloat(computedStyle.lineHeight || '20') || 20;
const maxHeight = lineHeight * 6;
const targetHeight = Math.min(textarea.scrollHeight, maxHeight);
this.inputSetLineCount(Math.max(1, Math.round(targetHeight / lineHeight)));
this.inputSetMultiline(targetHeight > lineHeight * 1.4);
if (Math.abs(targetHeight - previousHeight) <= 0.5) {
textarea.style.height = `${targetHeight}px`;
return;
}
textarea.style.height = `${previousHeight}px`;
void textarea.offsetHeight;
requestAnimationFrame(() => {
textarea.style.height = `${targetHeight}px`;
});
});
},
handleClickOutsideQuickMenu(event) {
if (!this.quickMenuOpen) {
return;
}
const shell = this.getComposerElement('stadiumShellOuter') || this.getComposerElement('compactInputShell');
if (shell && shell.contains(event.target)) {
return;
}
this.closeQuickMenu();
},
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);
},
applyToolSettingsSnapshot(categories) {
if (!Array.isArray(categories)) {
console.warn('[ToolSettings] Snapshot skipped: categories not array', categories);
return;
}
const normalized = categories.map((item) => ({
id: item.id,
label: item.label || item.id,
enabled: !!item.enabled,
tools: Array.isArray(item.tools) ? item.tools : []
}));
debugLog('[ToolSettings] Snapshot applied', {
received: categories.length,
normalized,
anyEnabled: normalized.some(cat => cat.enabled),
toolExamples: normalized.slice(0, 3)
});
this.toolSetSettings(normalized);
this.toolSetSettingsLoading(false);
},
async loadToolSettings(force = false) {
if (!this.isConnected && !force) {
debugLog('[ToolSettings] Skip load: disconnected & not forced');
return;
}
if (this.toolSettingsLoading) {
debugLog('[ToolSettings] Skip load: already loading');
return;
}
if (!force && this.toolSettings.length > 0) {
debugLog('[ToolSettings] Skip load: already have settings');
return;
}
debugLog('[ToolSettings] Fetch start', { force, hasConnection: this.isConnected });
this.toolSetSettingsLoading(true);
try {
const response = await fetch('/api/tool-settings');
const data = await response.json();
debugLog('[ToolSettings] Fetch response', { status: response.status, data });
if (response.ok && data.success && Array.isArray(data.categories)) {
this.applyToolSettingsSnapshot(data.categories);
} else {
console.warn('获取工具设置失败:', data);
this.toolSetSettingsLoading(false);
}
} catch (error) {
console.error('获取工具设置异常:', error);
this.toolSetSettingsLoading(false);
}
},
async updateToolCategory(categoryId, enabled) {
if (!this.isConnected) {
return;
}
if (this.toolSettingsLoading) {
return;
}
const previousSnapshot = this.toolSettings.map((item) => ({ ...item }));
const updatedSettings = this.toolSettings.map((item) => {
if (item.id === categoryId) {
return { ...item, enabled };
}
return item;
});
this.toolSetSettings(updatedSettings);
try {
const response = await fetch('/api/tool-settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
category: categoryId,
enabled
})
});
const data = await response.json();
if (response.ok && data.success && Array.isArray(data.categories)) {
this.applyToolSettingsSnapshot(data.categories);
} else {
console.warn('更新工具设置失败:', data);
this.toolSetSettings(previousSnapshot);
}
} catch (error) {
console.error('更新工具设置异常:', error);
this.toolSetSettings(previousSnapshot);
}
this.toolSetSettingsLoading(false);
},
toggleSettings() {
if (!this.isConnected) {
return;
}
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();
},
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);
},
getToolIcon,
getToolAnimationClass,
getToolStatusText,
getToolDescription,
cloneToolArguments,
buildToolLabel,
formatSearchTopic,
formatSearchTime,
getLanguageClass,
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);
},
async handleEasterEggPayload(payload) {
const controller = useEasterEgg();
await controller.handlePayload(payload, this);
},
async startEasterEggEffect(effectName, payload = {}) {
const controller = useEasterEgg();
await controller.startEffect(effectName, payload, this);
},
destroyEasterEggEffect(forceImmediate = false) {
const controller = useEasterEgg();
return controller.destroyEffect(forceImmediate);
},
finishEasterEggCleanup() {
const controller = useEasterEgg();
controller.finishCleanup();
},
formatTokenCount,
formatBytes,
formatPercentage,
formatRate,
quotaTypeLabel,
formatResetTime,
formatQuotaValue,
quotaResetSummary() {
return buildQuotaResetSummary(this.usageQuota);
},
isQuotaExceeded(type) {
return isQuotaExceededUtil(this.usageQuota, type);
},
showQuotaToast(payload) {
if (!payload) {
return;
}
const type = payload.type || 'fast';
const message = buildQuotaToastMessage(type, this.usageQuota, payload.reset_at);
this.uiShowQuotaToastMessage(message, type);
},
confirmAction(options = {}) {
return this.uiRequestConfirm(options);
}
}
};
(appOptions as any).components = {
ChatArea,
ConversationSidebar,
LeftPanel,
FocusPanel,
TokenDrawer,
PersonalizationDrawer,
QuickMenu,
InputComposer,
AppShell
};
export default appOptions;