// static/app-enhanced.js - 修复版,正确实现Token实时更新
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 SOCKET_IO_CDN_SOURCES = [
'https://cdn.socket.io/4.7.5/socket.io.min.js',
'https://cdn.jsdelivr.net/npm/socket.io-client@4.7.5/dist/socket.io.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.5/socket.io.min.js'
];
const ICONS = Object.freeze({
bot: '/static/icons/bot.svg',
book: '/static/icons/book.svg',
brain: '/static/icons/brain.svg',
camera: '/static/icons/camera.svg',
check: '/static/icons/check.svg',
checkbox: '/static/icons/checkbox.svg',
circleAlert: '/static/icons/circle-alert.svg',
clipboard: '/static/icons/clipboard.svg',
clock: '/static/icons/clock.svg',
eye: '/static/icons/eye.svg',
file: '/static/icons/file.svg',
flag: '/static/icons/flag.svg',
folder: '/static/icons/folder.svg',
folderOpen: '/static/icons/folder-open.svg',
globe: '/static/icons/globe.svg',
hammer: '/static/icons/hammer.svg',
info: '/static/icons/info.svg',
laptop: '/static/icons/laptop.svg',
menu: '/static/icons/menu.svg',
monitor: '/static/icons/monitor.svg',
octagon: '/static/icons/octagon.svg',
pencil: '/static/icons/pencil.svg',
python: '/static/icons/python.svg',
recycle: '/static/icons/recycle.svg',
save: '/static/icons/save.svg',
search: '/static/icons/search.svg',
settings: '/static/icons/settings.svg',
sparkles: '/static/icons/sparkles.svg',
stickyNote: '/static/icons/sticky-note.svg',
terminal: '/static/icons/terminal.svg',
trash: '/static/icons/trash.svg',
triangleAlert: '/static/icons/triangle-alert.svg',
user: '/static/icons/user.svg',
wrench: '/static/icons/wrench.svg',
x: '/static/icons/x.svg',
zap: '/static/icons/zap.svg'
});
const TOOL_ICON_MAP = Object.freeze({
append_to_file: 'pencil',
close_sub_agent: 'octagon',
create_file: 'file',
create_folder: 'folder',
create_sub_agent: 'bot',
delete_file: 'trash',
extract_webpage: 'globe',
focus_file: 'eye',
modify_file: 'pencil',
ocr_image: 'camera',
read_file: 'book',
rename_file: 'pencil',
run_command: 'terminal',
run_python: 'python',
save_webpage: 'save',
sleep: 'clock',
todo_create: 'stickyNote',
todo_finish: 'flag',
todo_finish_confirm: 'circleAlert',
todo_update_task: 'check',
terminal_input: 'terminal',
terminal_reset: 'recycle',
terminal_session: 'monitor',
terminal_snapshot: 'clipboard',
unfocus_file: 'eye',
update_memory: 'brain',
wait_sub_agent: 'clock',
web_search: 'search',
trigger_easter_egg: 'sparkles'
});
const TOOL_CATEGORY_ICON_MAP = Object.freeze({
network: 'globe',
file_edit: 'pencil',
read_focus: 'eye',
terminal_realtime: 'monitor',
terminal_command: 'terminal',
memory: 'brain',
todo: 'stickyNote',
sub_agent: 'bot',
easter_egg: 'sparkles'
});
function injectScriptSequentially(urls, onSuccess, onFailure) {
let index = 0;
const tryLoad = () => {
if (index >= urls.length) {
onFailure();
return;
}
const url = urls[index];
const script = document.createElement('script');
script.src = url;
script.async = false;
script.onload = () => {
if (typeof io !== 'undefined') {
console.log(`Socket.IO 已从 ${url} 加载`);
onSuccess();
} else {
index += 1;
tryLoad();
}
};
script.onerror = () => {
console.warn(`无法从 ${url} 加载 Socket.IO,尝试下一个源`);
index += 1;
tryLoad();
};
document.head.appendChild(script);
};
tryLoad();
}
async function ensureSocketIOLoaded() {
if (typeof io !== 'undefined') {
return true;
}
return await new Promise((resolve) => {
injectScriptSequentially(
SOCKET_IO_CDN_SOURCES,
() => resolve(true),
() => resolve(false)
);
});
}
async function bootstrapApp() {
// 检查必要的库是否加载
if (typeof Vue === 'undefined') {
console.error('错误:Vue.js 未加载');
document.body.innerHTML = '
Vue.js 加载失败,请刷新页面
';
return;
}
if (typeof io === 'undefined') {
const loaded = await ensureSocketIOLoaded();
if (!loaded || typeof io === 'undefined') {
console.error('错误:Socket.IO 未加载');
document.body.innerHTML = 'Socket.IO 加载失败,请检查网络后刷新页面
';
return;
}
}
console.log('所有依赖加载成功,初始化Vue应用...');
const { createApp } = Vue;
const app = createApp({
data() {
return {
// 连接状态
isConnected: false,
socket: null,
// 系统信息
projectPath: '',
agentVersion: '',
thinkingMode: true, // true=思考模式, false=快速模式
// 消息相关
messages: [],
inputMessage: '',
// 当前消息索引
currentMessageIndex: -1,
streamingMessage: false,
// 停止功能状态
stopRequested: false,
// 路由相关
initialRouteResolved: false,
// 文件相关
fileTree: [],
focusedFiles: {},
expandedFolders: {},
// 展开状态管理
expandedBlocks: new Set(),
// 滚动控制
userScrolling: false,
autoScrollEnabled: true,
// 面板宽度控制
leftWidth: 350,
rightWidth: 420,
rightCollapsed: true,
isResizing: false,
resizingPanel: null,
minPanelWidth: 350,
maxPanelWidth: 600,
// 工具状态跟踪
preparingTools: new Map(),
activeTools: new Map(),
toolActionIndex: new Map(),
toolActionIndex: new Map(),
toolStacks: new Map(),
// ==========================================
// 对话管理相关状态
// ==========================================
// 对话历史侧边栏
sidebarCollapsed: true, // 默认收起对话侧边栏
panelMode: 'files', // files | todo | subAgents
panelMenuOpen: false,
subAgents: [],
subAgentPollTimer: null,
conversations: [],
conversationsLoading: false,
hasMoreConversations: false,
loadingMoreConversations: false,
currentConversationId: null,
currentConversationTitle: '当前对话',
personalPageVisible: false,
// 搜索功能
searchQuery: '',
searchTimer: null,
// 分页
conversationsOffset: 0,
conversationsLimit: 20,
// ==========================================
// Token统计相关状态(修复版)
// ==========================================
// 当前上下文Token(动态计算,包含完整prompt)
currentContextTokens: 0,
// 累计Token统计(从对话文件和WebSocket获取)
currentConversationTokens: {
// 累计统计字段
cumulative_input_tokens: 0,
cumulative_output_tokens: 0,
cumulative_total_tokens: 0
},
// Token面板折叠状态
tokenPanelCollapsed: true,
// 对话压缩状态
compressing: false,
// 输入/快捷菜单状态
settingsOpen: false,
quickMenuOpen: false,
inputLineCount: 1,
inputIsMultiline: false,
inputIsFocused: false,
// 思考块滚动锁
thinkingScrollLocks: new Map(),
// 工具控制菜单
toolMenuOpen: false,
toolSettings: [],
toolSettingsLoading: false,
// 文件上传状态
uploading: false,
// TODO 列表
todoList: null,
icons: ICONS,
toolCategoryIcons: TOOL_CATEGORY_ICON_MAP,
easterEgg: {
active: false,
effect: null,
payload: null,
instance: null,
cleanupTimer: null,
destroying: false,
destroyPromise: null
},
// 右键菜单相关
contextMenu: {
visible: false,
x: 0,
y: 0,
node: null
},
onDocumentClick: null,
onWindowScroll: null,
onKeydownListener: null
}
},
async mounted() {
console.log('Vue应用已挂载');
await this.bootstrapRoute();
this.initSocket();
this.initScrollListener();
// 延迟加载初始数据
setTimeout(() => {
this.loadInitialData();
}, 500);
document.addEventListener('click', this.handleClickOutsideQuickMenu);
document.addEventListener('click', this.handleClickOutsidePanelMenu);
window.addEventListener('popstate', this.handlePopState);
this.onDocumentClick = (event) => {
if (!this.contextMenu.visible) {
return;
}
if (event.target && event.target.closest && event.target.closest('.context-menu')) {
return;
}
this.hideContextMenu();
};
this.onWindowScroll = () => {
if (this.contextMenu.visible) {
this.hideContextMenu();
}
};
this.onKeydownListener = (event) => {
if (event.key === 'Escape' && this.contextMenu.visible) {
this.hideContextMenu();
}
};
document.addEventListener('click', this.onDocumentClick);
window.addEventListener('scroll', this.onWindowScroll, true);
document.addEventListener('keydown', this.onKeydownListener);
this.fetchSubAgents();
this.subAgentPollTimer = setInterval(() => {
if (this.panelMode === 'subAgents') {
this.fetchSubAgents();
}
}, 5000);
this.$nextTick(() => {
this.autoResizeInput();
});
},
beforeUnmount() {
document.removeEventListener('click', this.handleClickOutsideQuickMenu);
document.removeEventListener('click', this.handleClickOutsidePanelMenu);
window.removeEventListener('popstate', this.handlePopState);
if (this.onDocumentClick) {
document.removeEventListener('click', this.onDocumentClick);
this.onDocumentClick = null;
}
if (this.onWindowScroll) {
window.removeEventListener('scroll', this.onWindowScroll, true);
this.onWindowScroll = null;
}
if (this.onKeydownListener) {
document.removeEventListener('keydown', this.onKeydownListener);
this.onKeydownListener = null;
}
if (this.subAgentPollTimer) {
clearInterval(this.subAgentPollTimer);
this.subAgentPollTimer = null;
}
const cleanup = this.destroyEasterEggEffect(true);
if (cleanup && typeof cleanup.catch === 'function') {
cleanup.catch(() => {});
}
},
watch: {
inputMessage() {
this.autoResizeInput();
}
},
methods: {
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;
},
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.messages = [];
this.resetAllStates();
this.resetTokenStatistics();
return;
}
this.loadConversation(convId);
},
stripConversationPrefix(conversationId) {
if (!conversationId) return '';
return conversationId.startsWith('conv_') ? conversationId.slice(5) : conversationId;
},
showContextMenu(payload) {
if (!payload || !payload.node) {
return;
}
const { node, event } = payload;
console.log('context menu', node.path, node.type);
if (event && typeof event.preventDefault === 'function') {
event.preventDefault();
}
if (event && typeof event.stopPropagation === 'function') {
event.stopPropagation();
}
if (!node.path && node.path !== '') {
this.hideContextMenu();
return;
}
if (node.type !== 'file' && node.type !== 'folder') {
this.hideContextMenu();
return;
}
this.hideContextMenu();
let x = (event && event.clientX) || 0;
let y = (event && event.clientY) || 0;
const menuWidth = 200;
const menuHeight = 50;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
if (x + menuWidth > viewportWidth) {
x = viewportWidth - menuWidth - 8;
}
if (y + menuHeight > viewportHeight) {
y = viewportHeight - menuHeight - 8;
}
this.contextMenu.visible = true;
this.contextMenu.x = Math.max(8, x);
this.contextMenu.y = Math.max(8, y);
this.contextMenu.node = node;
},
hideContextMenu() {
if (!this.contextMenu.visible) {
return;
}
this.contextMenu.visible = false;
this.contextMenu.node = null;
},
async downloadFile(path) {
if (!path) {
this.hideContextMenu();
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.hideContextMenu();
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();
}
alert(`下载失败: ${message}`);
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);
alert(`下载失败: ${error.message || error}`);
} finally {
this.hideContextMenu();
}
},
initScrollListener() {
const messagesArea = this.$refs.messagesArea;
if (!messagesArea) {
console.warn('消息区域未找到');
return;
}
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.userScrolling = false;
this.autoScrollEnabled = true;
} else {
this.userScrolling = true;
this.autoScrollEnabled = false;
}
});
},
initSocket() {
try {
console.log('初始化WebSocket连接...');
const usePollingOnly = window.location.hostname !== 'localhost' &&
window.location.hostname !== '127.0.0.1';
this.socket = io('/', usePollingOnly ? {
transports: ['polling'],
upgrade: false
} : {
transports: ['websocket', 'polling']
});
// 连接事件
this.socket.on('connect', () => {
this.isConnected = true;
console.log('WebSocket已连接');
// 连接时重置所有状态
this.resetAllStates();
});
this.socket.on('disconnect', () => {
this.isConnected = false;
console.log('WebSocket已断开');
// 断线时也重置状态,防止状态混乱
this.resetAllStates();
});
this.socket.on('connect_error', (error) => {
console.error('WebSocket连接错误:', error.message);
});
// ==========================================
// Token统计WebSocket事件处理(修复版)
// ==========================================
this.socket.on('token_update', (data) => {
console.log('收到token更新事件:', data);
// 只处理当前对话的token更新
if (data.conversation_id === this.currentConversationId) {
// 更新累计统计(使用后端提供的准确字段名)
this.currentConversationTokens.cumulative_input_tokens = data.cumulative_input_tokens || 0;
this.currentConversationTokens.cumulative_output_tokens = data.cumulative_output_tokens || 0;
this.currentConversationTokens.cumulative_total_tokens = data.cumulative_total_tokens || 0;
console.log(`累计Token统计更新: 输入=${data.cumulative_input_tokens}, 输出=${data.cumulative_output_tokens}, 总计=${data.cumulative_total_tokens}`);
// 同时更新当前上下文Token(关键修复)
this.updateCurrentContextTokens();
this.$forceUpdate();
}
});
this.socket.on('todo_updated', (data) => {
console.log('收到todo更新事件:', data);
if (data && data.conversation_id) {
this.currentConversationId = data.conversation_id;
}
this.todoList = data && data.todo_list ? data.todo_list : null;
});
// 系统就绪
this.socket.on('system_ready', (data) => {
this.projectPath = data.project_path || '';
this.agentVersion = data.version || this.agentVersion;
this.thinkingMode = !!data.thinking_mode;
console.log('系统就绪:', data);
// 系统就绪后立即加载对话列表
this.loadConversationsList();
});
this.socket.on('tool_settings_updated', (data) => {
console.log('收到工具设置更新:', data);
if (data && Array.isArray(data.categories)) {
this.applyToolSettingsSnapshot(data.categories);
}
});
// ==========================================
// 对话管理相关Socket事件
// ==========================================
// 监听对话变更事件
this.socket.on('conversation_changed', (data) => {
console.log('对话已切换:', data);
this.currentConversationId = data.conversation_id;
this.currentConversationTitle = data.title || '';
this.promoteConversationToTop(data.conversation_id);
if (data.cleared) {
// 对话被清空
this.messages = [];
this.currentConversationId = null;
this.currentConversationTitle = '';
// 重置Token统计
this.resetTokenStatistics();
history.replaceState({}, '', '/new');
}
// 刷新对话列表
this.loadConversationsList();
this.fetchTodoList();
this.fetchSubAgents();
});
this.socket.on('conversation_resolved', (data) => {
if (!data || !data.conversation_id) {
return;
}
const convId = data.conversation_id;
this.currentConversationId = convId;
if (data.title) {
this.currentConversationTitle = data.title;
}
this.promoteConversationToTop(convId);
const pathFragment = this.stripConversationPrefix(convId);
const currentPath = window.location.pathname.replace(/^\/+/, '');
if (data.created) {
history.pushState({ conversationId: convId }, '', `/${pathFragment}`);
} else if (currentPath !== pathFragment) {
history.replaceState({ conversationId: convId }, '', `/${pathFragment}`);
}
});
// 监听对话加载事件
this.socket.on('conversation_loaded', (data) => {
console.log('对话已加载:', data);
if (data.clear_ui) {
// 清理当前UI状态,准备显示历史内容
this.resetAllStates();
}
// 延迟获取并显示历史对话内容
setTimeout(() => {
this.fetchAndDisplayHistory();
}, 300);
// 延迟获取Token统计(累计+当前上下文)
setTimeout(() => {
this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens();
this.fetchTodoList();
}, 500);
});
// 监听对话列表更新事件
this.socket.on('conversation_list_update', (data) => {
console.log('对话列表已更新:', data);
// 刷新对话列表
this.loadConversationsList();
});
// 监听状态更新事件
this.socket.on('status_update', (status) => {
// 更新系统状态信息
if (status.conversation && status.conversation.current_id) {
this.currentConversationId = status.conversation.current_id;
}
if (typeof status.thinking_mode !== 'undefined') {
this.thinkingMode = !!status.thinking_mode;
}
});
// AI消息开始
this.socket.on('ai_message_start', () => {
console.log('AI消息开始');
this.cleanupStaleToolActions();
const newMessage = {
role: 'assistant',
actions: [],
streamingThinking: '',
streamingText: '',
currentStreamingType: null
};
this.messages.push(newMessage);
this.currentMessageIndex = this.messages.length - 1;
this.streamingMessage = true;
this.stopRequested = false;
this.autoScrollEnabled = true;
this.scrollToBottom();
});
// 思考流开始
this.socket.on('thinking_start', () => {
console.log('思考开始');
if (this.currentMessageIndex >= 0) {
const msg = this.messages[this.currentMessageIndex];
msg.streamingThinking = '';
msg.currentStreamingType = 'thinking';
const action = {
id: Date.now() + Math.random(),
type: 'thinking',
content: '',
streaming: true,
timestamp: Date.now()
};
msg.actions.push(action);
const blockId = action.blockId || `thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`;
action.blockId = blockId;
this.expandedBlocks.add(blockId);
// 开始思考时自动锁定滚动到底部
this.autoScrollEnabled = true;
this.userScrolling = false;
this.scrollToBottom();
this.thinkingScrollLocks.set(blockId, true);
this.$forceUpdate();
}
});
// 思考内容块
this.socket.on('thinking_chunk', (data) => {
if (this.currentMessageIndex >= 0) {
const msg = this.messages[this.currentMessageIndex];
msg.streamingThinking += data.content;
const lastAction = msg.actions[msg.actions.length - 1];
if (lastAction && lastAction.type === 'thinking') {
lastAction.content += data.content;
}
this.$forceUpdate();
if (lastAction && lastAction.blockId) {
this.$nextTick(() => this.scrollThinkingToBottom(lastAction.blockId));
} else {
this.conditionalScrollToBottom();
}
}
});
// 思考结束
this.socket.on('thinking_end', (data) => {
console.log('思考结束');
if (this.currentMessageIndex >= 0) {
const msg = this.messages[this.currentMessageIndex];
const lastAction = msg.actions[msg.actions.length - 1];
if (lastAction && lastAction.type === 'thinking') {
lastAction.streaming = false;
lastAction.content = data.full_content;
const blockId = lastAction.blockId || `thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`;
if (!lastAction.blockId) {
lastAction.blockId = blockId;
}
if (blockId) {
setTimeout(() => {
this.expandedBlocks.delete(blockId);
this.thinkingScrollLocks.delete(blockId);
this.$forceUpdate();
}, 1000);
this.$nextTick(() => this.scrollThinkingToBottom(blockId));
}
}
msg.streamingThinking = '';
msg.currentStreamingType = null;
this.$forceUpdate();
}
});
// 文本流开始
this.socket.on('text_start', () => {
console.log('文本开始');
if (this.currentMessageIndex >= 0) {
const msg = this.messages[this.currentMessageIndex];
msg.streamingText = '';
msg.currentStreamingType = 'text';
const action = {
id: Date.now() + Math.random(),
type: 'text',
content: '',
streaming: true,
timestamp: Date.now()
};
msg.actions.push(action);
this.$forceUpdate();
}
});
// 文本内容块
this.socket.on('text_chunk', (data) => {
if (this.currentMessageIndex >= 0) {
const msg = this.messages[this.currentMessageIndex];
msg.streamingText += data.content;
const lastAction = msg.actions[msg.actions.length - 1];
if (lastAction && lastAction.type === 'text') {
lastAction.content += data.content;
}
this.$forceUpdate();
this.conditionalScrollToBottom();
// 实时渲染LaTeX
this.renderLatexInRealtime();
}
});
// 文本结束
this.socket.on('text_end', (data) => {
console.log('文本结束');
if (this.currentMessageIndex >= 0) {
const msg = this.messages[this.currentMessageIndex];
// 查找当前流式文本的action
for (let i = msg.actions.length - 1; i >= 0; i--) {
const action = msg.actions[i];
if (action.type === 'text' && action.streaming) {
action.streaming = false;
action.content = data.full_content;
console.log('文本action已更新为完成状态');
break;
}
}
msg.streamingText = '';
msg.currentStreamingType = null;
this.$forceUpdate();
}
});
// 工具提示事件(可选)
this.socket.on('tool_hint', (data) => {
console.log('工具提示:', data.name);
// 可以在这里添加提示UI
});
// 工具准备中事件 - 实时显示
this.socket.on('tool_preparing', (data) => {
console.log('工具准备中:', data.name);
const msg = this.ensureAssistantMessage();
if (!msg) {
return;
}
const action = {
id: data.id,
type: 'tool',
tool: {
id: data.id,
name: data.name,
arguments: {},
argumentSnapshot: null,
argumentLabel: '',
status: 'preparing',
result: null,
message: data.message || `准备调用 ${data.name}...`
},
timestamp: Date.now()
};
msg.actions.push(action);
this.preparingTools.set(data.id, action);
this.registerToolAction(action, data.id);
this.trackToolAction(data.name, action);
this.$forceUpdate();
this.conditionalScrollToBottom();
});
// 工具状态更新事件 - 实时显示详细状态
this.socket.on('tool_status', (data) => {
console.log('工具状态:', data);
const target = this.findToolAction(data.id, data.preparing_id, data.execution_id);
if (target) {
target.tool.statusDetail = data.detail;
target.tool.statusType = data.status;
this.$forceUpdate();
return;
}
const fallbackAction = this.getLatestActiveToolAction(data.tool);
if (fallbackAction) {
fallbackAction.tool.statusDetail = data.detail;
fallbackAction.tool.statusType = data.status;
this.registerToolAction(fallbackAction, data.execution_id || data.id || data.preparing_id);
this.$forceUpdate();
}
});
// 工具开始(从准备转为执行)
this.socket.on('tool_start', (data) => {
console.log('工具开始执行:', data.name);
let action = null;
if (data.preparing_id && this.preparingTools.has(data.preparing_id)) {
action = this.preparingTools.get(data.preparing_id);
this.preparingTools.delete(data.preparing_id);
} else {
action = this.findToolAction(data.id, data.preparing_id, data.execution_id);
}
if (!action) {
const msg = this.ensureAssistantMessage();
if (!msg) {
return;
}
action = {
id: data.id,
type: 'tool',
tool: {
id: data.id,
name: data.name,
arguments: {},
argumentSnapshot: null,
argumentLabel: '',
status: 'running',
result: null
},
timestamp: Date.now()
};
msg.actions.push(action);
}
action.tool.status = 'running';
action.tool.arguments = data.arguments;
action.tool.argumentSnapshot = this.cloneToolArguments(data.arguments);
action.tool.argumentLabel = this.buildToolLabel(action.tool.argumentSnapshot);
action.tool.message = null;
action.tool.id = data.id;
action.tool.executionId = data.id;
this.registerToolAction(action, data.id);
this.trackToolAction(data.name, action);
this.$forceUpdate();
this.conditionalScrollToBottom();
});
// 更新action(工具完成)
this.socket.on('update_action', (data) => {
console.log('更新action:', data.id, 'status:', data.status);
let targetAction = this.findToolAction(data.id, data.preparing_id, data.execution_id);
if (!targetAction && data.preparing_id && this.preparingTools.has(data.preparing_id)) {
targetAction = this.preparingTools.get(data.preparing_id);
}
if (!targetAction) {
outer: for (const message of this.messages) {
if (!message.actions) continue;
for (const action of message.actions) {
if (action.type !== 'tool') continue;
const matchByExecution = action.tool.executionId && action.tool.executionId === data.id;
const matchByToolId = action.tool.id === data.id;
const matchByPreparingId = action.id === data.preparing_id;
if (matchByExecution || matchByToolId || matchByPreparingId) {
targetAction = action;
break outer;
}
}
}
}
if (targetAction) {
if (data.status) {
targetAction.tool.status = data.status;
}
if (data.result !== undefined) {
targetAction.tool.result = data.result;
}
if (targetAction.tool && targetAction.tool.name === 'trigger_easter_egg' && data.result !== undefined) {
const eggPromise = this.handleEasterEggPayload(data.result);
if (eggPromise && typeof eggPromise.catch === 'function') {
eggPromise.catch((error) => {
console.warn('彩蛋处理异常:', error);
});
}
}
if (data.message !== undefined) {
targetAction.tool.message = data.message;
}
if (data.awaiting_content) {
targetAction.tool.awaiting_content = true;
} else if (data.status === 'completed') {
targetAction.tool.awaiting_content = false;
}
if (!targetAction.tool.executionId && (data.execution_id || data.id)) {
targetAction.tool.executionId = data.execution_id || data.id;
}
if (data.arguments) {
targetAction.tool.arguments = data.arguments;
targetAction.tool.argumentSnapshot = this.cloneToolArguments(data.arguments);
targetAction.tool.argumentLabel = this.buildToolLabel(targetAction.tool.argumentSnapshot);
}
this.registerToolAction(targetAction, data.execution_id || data.id);
if (data.status && ["completed", "failed", "error"].includes(data.status) && !data.awaiting_content) {
this.unregisterToolAction(targetAction);
if (data.id) {
this.preparingTools.delete(data.id);
}
if (data.preparing_id) {
this.preparingTools.delete(data.preparing_id);
}
}
this.$forceUpdate();
this.conditionalScrollToBottom();
}
// 关键修复:每个工具完成后都更新当前上下文Token
if (data.status === 'completed') {
setTimeout(() => {
this.updateCurrentContextTokens();
}, 500);
}
});
this.socket.on('append_payload', (data) => {
console.log('收到append_payload事件:', data);
if (this.currentMessageIndex >= 0) {
const msg = this.messages[this.currentMessageIndex];
const action = {
id: `append-payload-${Date.now()}-${Math.random()}`,
type: 'append_payload',
append: {
path: data.path || '未知文件',
forced: !!data.forced,
success: data.success === undefined ? true : !!data.success,
lines: data.lines ?? null,
bytes: data.bytes ?? null
},
timestamp: Date.now()
};
msg.actions.push(action);
this.$forceUpdate();
this.conditionalScrollToBottom();
}
});
this.socket.on('modify_payload', (data) => {
console.log('收到modify_payload事件:', data);
if (this.currentMessageIndex >= 0) {
const msg = this.messages[this.currentMessageIndex];
const action = {
id: `modify-payload-${Date.now()}-${Math.random()}`,
type: 'modify_payload',
modify: {
path: data.path || '未知文件',
total: data.total ?? null,
completed: data.completed || [],
failed: data.failed || [],
forced: !!data.forced
},
timestamp: Date.now()
};
msg.actions.push(action);
this.$forceUpdate();
this.conditionalScrollToBottom();
}
});
// 停止请求确认
this.socket.on('stop_requested', (data) => {
console.log('停止请求已接收:', data.message);
// 可以显示提示信息
});
// 任务停止
this.socket.on('task_stopped', (data) => {
console.log('任务已停止:', data.message);
this.resetAllStates();
});
// 任务完成(重点:更新Token统计)
this.socket.on('task_complete', (data) => {
console.log('任务完成', data);
this.resetAllStates();
// 任务完成后立即更新Token统计(关键修复)
if (this.currentConversationId) {
this.updateCurrentContextTokens();
this.fetchConversationTokenStatistics();
}
});
// 聚焦文件更新
this.socket.on('focused_files_update', (data) => {
this.focusedFiles = data || {};
// 聚焦文件变化时更新当前上下文Token(关键修复)
if (this.currentConversationId) {
setTimeout(() => {
this.updateCurrentContextTokens();
}, 500);
}
});
// 文件树更新
this.socket.on('file_tree_update', (data) => {
this.updateFileTree(data);
// 文件树变化时也可能影响上下文
if (this.currentConversationId) {
setTimeout(() => {
this.updateCurrentContextTokens();
}, 500);
}
});
// 系统消息
this.socket.on('system_message', (data) => {
if (!data || !data.content) {
return;
}
this.appendSystemAction(data.content);
});
// 错误处理
this.socket.on('error', (data) => {
this.addSystemMessage(`错误: ${data.message}`);
// 仅标记当前流结束,避免状态错乱
this.streamingMessage = false;
this.stopRequested = false;
});
// 命令结果
this.socket.on('command_result', (data) => {
if (data.command === 'clear' && data.success) {
this.messages = [];
this.currentMessageIndex = -1;
this.expandedBlocks.clear();
// 清除对话时重置Token统计
this.resetTokenStatistics();
} else if (data.command === 'status' && data.success) {
this.addSystemMessage(`系统状态:\n${JSON.stringify(data.data, null, 2)}`);
} else if (!data.success) {
this.addSystemMessage(`命令失败: ${data.message}`);
}
});
} catch (error) {
console.error('Socket初始化失败:', error);
}
},
registerToolAction(action, executionId = null) {
if (!action || action.type !== 'tool') {
return;
}
const keys = new Set();
if (action.id) {
keys.add(action.id);
}
if (action.tool && action.tool.id) {
keys.add(action.tool.id);
}
if (executionId) {
keys.add(executionId);
}
if (action.tool && action.tool.executionId) {
keys.add(action.tool.executionId);
}
keys.forEach(key => {
if (!key) {
return;
}
this.toolActionIndex.set(key, action);
});
},
unregisterToolAction(action) {
if (!action || action.type !== 'tool') {
return;
}
const keysToRemove = [];
for (const [key, stored] of this.toolActionIndex.entries()) {
if (stored === action) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => this.toolActionIndex.delete(key));
if (action.tool && action.tool.name) {
this.releaseToolAction(action.tool.name, action);
}
},
findToolAction(id, preparingId, executionId) {
if (!this.toolActionIndex) {
return null;
}
const candidates = [executionId, id, preparingId];
for (const key of candidates) {
if (key && this.toolActionIndex.has(key)) {
return this.toolActionIndex.get(key);
}
}
return null;
},
trackToolAction(toolName, action) {
if (!toolName || !action) {
return;
}
if (!this.toolStacks.has(toolName)) {
this.toolStacks.set(toolName, []);
}
const stack = this.toolStacks.get(toolName);
if (!stack.includes(action)) {
stack.push(action);
}
},
releaseToolAction(toolName, action) {
if (!toolName || !this.toolStacks.has(toolName)) {
return;
}
const stack = this.toolStacks.get(toolName);
const index = stack.indexOf(action);
if (index !== -1) {
stack.splice(index, 1);
}
if (stack.length === 0) {
this.toolStacks.delete(toolName);
}
},
getLatestActiveToolAction(toolName) {
if (!toolName || !this.toolStacks.has(toolName)) {
return null;
}
const stack = this.toolStacks.get(toolName);
for (let i = stack.length - 1; i >= 0; i--) {
const action = stack[i];
if (!action || action.type !== 'tool' || !action.tool) {
continue;
}
if (['preparing', 'running', 'stale'].includes(action.tool.status)) {
return action;
}
}
return stack[stack.length - 1] || null;
},
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.unregisterToolAction(action);
}
});
});
this.preparingTools.clear();
this.toolActionIndex.clear();
},
ensureAssistantMessage() {
if (this.currentMessageIndex >= 0) {
return this.messages[this.currentMessageIndex];
}
const message = {
role: 'assistant',
actions: [],
streamingThinking: '',
streamingText: '',
currentStreamingType: null
};
this.messages.push(message);
this.currentMessageIndex = this.messages.length - 1;
return message;
},
// 完整重置所有状态
resetAllStates() {
console.log('重置所有前端状态');
this.hideContextMenu();
// 重置消息和流状态
this.streamingMessage = false;
this.currentMessageIndex = -1;
this.stopRequested = false;
// 清理工具状态
this.preparingTools.clear();
this.activeTools.clear();
this.toolActionIndex.clear();
// 新增:将所有未完成的工具标记为已完成
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.userScrolling = false;
this.autoScrollEnabled = true;
// 清理Markdown缓存
if (this.markdownCache) {
this.markdownCache.clear();
}
this.thinkingScrollLocks.clear();
// 强制更新视图
this.$forceUpdate();
this.settingsOpen = false;
this.toolMenuOpen = false;
this.quickMenuOpen = false;
this.inputLineCount = 1;
this.inputIsMultiline = false;
this.toolSettingsLoading = false;
this.toolSettings = [];
console.log('前端状态重置完成');
},
// 重置Token统计
openRealtimeTerminal() {
const { protocol, hostname, port } = window.location;
const target = `${protocol}//${hostname}${port ? ':' + port : ''}/terminal`;
window.open(target, '_blank');
},
resetTokenStatistics() {
this.currentContextTokens = 0;
this.currentConversationTokens = {
cumulative_input_tokens: 0,
cumulative_output_tokens: 0,
cumulative_total_tokens: 0
};
},
async loadInitialData() {
try {
console.log('加载初始数据...');
const filesResponse = await fetch('/api/files');
const filesData = await filesResponse.json();
this.updateFileTree(filesData);
const focusedResponse = await fetch('/api/focused');
const focusedData = await focusedResponse.json();
this.focusedFiles = focusedData || {};
await this.fetchTodoList();
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;
// 获取当前对话信息
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
if (statusConversationId) {
if (!this.currentConversationId) {
this.currentConversationId = statusConversationId;
}
// 如果有当前对话,尝试获取标题和Token统计
try {
const convResponse = await fetch(`/api/conversations/current`);
const convData = await convResponse.json();
if (convData.success && convData.data) {
this.currentConversationTitle = convData.data.title;
}
await this.fetchAndDisplayHistory();
// 获取当前对话的Token统计
this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens();
} catch (e) {
console.warn('获取当前对话标题失败:', e);
}
}
await this.loadToolSettings(true);
console.log('初始数据加载完成');
} catch (error) {
console.error('加载初始数据失败:', error);
}
},
async refreshFileTree() {
try {
const response = await fetch('/api/files');
const data = await response.json();
this.updateFileTree(data);
} catch (error) {
console.error('刷新文件树失败:', error);
}
},
// ==========================================
// Token统计相关方法(完全修复版)
// ==========================================
async updateCurrentContextTokens() {
// 获取当前上下文Token数(动态计算,包含完整prompt构建)
if (!this.currentConversationId) {
this.currentContextTokens = 0;
return;
}
try {
console.log(`正在更新当前上下文Token: ${this.currentConversationId}`);
// 关键修复:使用正确的动态API,包含文件结构+记忆+聚焦文件+终端内容+工具定义
const response = await fetch(`/api/conversations/${this.currentConversationId}/tokens`);
const data = await response.json();
if (data.success && data.data) {
this.currentContextTokens = data.data.total_tokens || 0;
console.log(`当前上下文Token更新: ${this.currentContextTokens}`);
this.$forceUpdate();
} else {
console.warn('获取当前上下文Token失败:', data.error);
this.currentContextTokens = 0;
}
} catch (error) {
console.warn('获取当前上下文Token异常:', error);
this.currentContextTokens = 0;
}
},
async fetchConversationTokenStatistics() {
// 获取对话累计Token统计(加载对话时、任务完成后调用)
if (!this.currentConversationId) {
this.resetTokenStatistics();
return;
}
try {
const response = await fetch(`/api/conversations/${this.currentConversationId}/token-statistics`);
const data = await response.json();
if (data.success && data.data) {
// 更新累计统计
this.currentConversationTokens.cumulative_input_tokens = data.data.total_input_tokens || 0;
this.currentConversationTokens.cumulative_output_tokens = data.data.total_output_tokens || 0;
this.currentConversationTokens.cumulative_total_tokens = data.data.total_tokens || 0;
console.log(`累计Token统计: 输入=${data.data.total_input_tokens}, 输出=${data.data.total_output_tokens}, 总计=${data.data.total_tokens}`);
this.$forceUpdate();
} else {
console.warn('获取Token统计失败:', data.error);
// 保持当前统计,不重置
}
} catch (error) {
console.warn('获取Token统计异常:', error);
// 保持当前统计,不重置
}
},
// Token面板折叠/展开切换
toggleTokenPanel() {
this.tokenPanelCollapsed = !this.tokenPanelCollapsed;
},
// ==========================================
// 对话管理核心功能
// ==========================================
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);
}
this.hasMoreConversations = data.data.has_more;
console.log(`已加载 ${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) {
console.log('加载对话:', conversationId);
if (conversationId === this.currentConversationId) {
console.log('已是当前对话,跳过加载');
return;
}
try {
// 1. 调用加载API
const response = await fetch(`/api/conversations/${conversationId}/load`, {
method: 'PUT'
});
const result = await response.json();
if (result.success) {
console.log('对话加载API成功:', result);
// 2. 更新当前对话信息
this.currentConversationId = conversationId;
this.currentConversationTitle = result.title;
this.promoteConversationToTop(conversationId);
history.pushState({ conversationId }, '', `/${this.stripConversationPrefix(conversationId)}`);
// 3. 重置UI状态
this.resetAllStates();
this.fetchSubAgents();
// 4. 延迟获取并显示历史对话内容(关键功能)
setTimeout(() => {
this.fetchAndDisplayHistory();
}, 300);
// 5. 获取Token统计(重点:加载历史累计统计+当前上下文)
setTimeout(() => {
this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens();
}, 500);
} else {
console.error('对话加载失败:', result.message);
alert(`加载对话失败: ${result.message}`);
}
} catch (error) {
console.error('加载对话异常:', error);
alert(`加载对话异常: ${error.message}`);
}
},
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() {
console.log('开始获取历史对话内容...');
if (!this.currentConversationId || this.currentConversationId.startsWith('temp_')) {
console.log('没有当前对话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();
console.log('系统状态:', status);
// 如果状态中有对话历史字段
if (status.conversation_history && Array.isArray(status.conversation_history)) {
this.renderHistoryMessages(status.conversation_history);
return;
}
console.log('备用方案也无法获取历史消息');
return;
}
const messagesData = await messagesResponse.json();
console.log('获取到消息数据:', messagesData);
if (messagesData.success && messagesData.data && messagesData.data.messages) {
const messages = messagesData.data.messages;
console.log(`发现 ${messages.length} 条历史消息`);
if (messages.length > 0) {
// 清空当前显示的消息
this.messages = [];
// 渲染历史消息 - 这是关键功能
this.renderHistoryMessages(messages);
// 滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
console.log('历史对话内容显示完成');
} else {
console.log('对话存在但没有历史消息');
this.messages = [];
}
} else {
console.log('消息数据格式不正确:', messagesData);
this.messages = [];
}
} catch (error) {
console.error('获取历史对话失败:', error);
console.log('尝试不显示错误弹窗,仅在控制台记录');
// 不显示alert,避免打断用户体验
this.messages = [];
}
},
// ==========================================
// 关键功能:渲染历史消息
// ==========================================
renderHistoryMessages(historyMessages) {
console.log('开始渲染历史消息...', historyMessages);
console.log('历史消息数量:', historyMessages.length);
if (!Array.isArray(historyMessages)) {
console.error('历史消息不是数组格式');
return;
}
let currentAssistantMessage = null;
historyMessages.forEach((message, index) => {
console.log(`处理消息 ${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 || ''
});
console.log('添加用户消息:', message.content?.substring(0, 50) + '...');
} else if (message.role === 'assistant') {
// AI消息 - 如果没有当前assistant消息,创建一个
if (!currentAssistantMessage) {
currentAssistantMessage = {
role: 'assistant',
actions: [],
streamingThinking: '',
streamingText: '',
currentStreamingType: null
};
}
const content = message.content || '';
let reasoningText = (message.reasoning_content || '').trim();
if (!reasoningText) {
const thinkPatterns = [
/([\s\S]*?)<\/think>/g,
/([\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
});
console.log('添加思考内容:', 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 || '') || /<<>>/i.test(content || '');
let textContent = content;
if (!message.reasoning_content) {
textContent = textContent
.replace(/[\s\S]*?<\/think>/g, '')
.replace(/[\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()
});
console.log('添加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()
});
console.log('添加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()
});
console.log('添加文本内容:', 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()
});
console.log('添加工具调用:', 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 (message.name === 'append_to_file' && result && result.message) {
toolAction.tool.message = result.message;
}
console.log(`更新工具结果: ${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;
}
console.log('处理其他类型消息:', message.role);
this.messages.push({
role: message.role,
content: message.content || ''
});
}
});
// 处理最后一个assistant消息
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
this.messages.push(currentAssistantMessage);
}
console.log(`历史消息渲染完成,共 ${this.messages.length} 条消息`);
// 强制更新视图
this.$forceUpdate();
// 确保滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
},
async createNewConversation() {
console.log('创建新对话...');
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) {
console.log('新对话创建成功:', result.conversation_id);
// 清空当前消息
this.messages = [];
this.currentConversationId = result.conversation_id;
this.currentConversationTitle = '新对话';
history.pushState({ conversationId: this.currentConversationId }, '', `/${this.stripConversationPrefix(this.currentConversationId)}`);
// 重置Token统计
this.resetTokenStatistics();
// 重置状态
this.resetAllStates();
// 刷新对话列表
this.conversationsOffset = 0;
await this.loadConversationsList();
} else {
console.error('创建对话失败:', result.message);
alert(`创建对话失败: ${result.message}`);
}
} catch (error) {
console.error('创建对话异常:', error);
alert(`创建对话异常: ${error.message}`);
}
},
async deleteConversation(conversationId) {
if (!confirm('确定要删除这个对话吗?删除后无法恢复。')) {
return;
}
console.log('删除对话:', conversationId);
try {
const response = await fetch(`/api/conversations/${conversationId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
console.log('对话删除成功');
// 如果删除的是当前对话,清空界面
if (conversationId === this.currentConversationId) {
this.messages = [];
this.currentConversationId = null;
this.currentConversationTitle = '';
this.resetAllStates();
this.resetTokenStatistics();
history.replaceState({}, '', '/new');
}
// 刷新对话列表
this.conversationsOffset = 0;
await this.loadConversationsList();
} else {
console.error('删除对话失败:', result.message);
alert(`删除对话失败: ${result.message}`);
}
} catch (error) {
console.error('删除对话异常:', error);
alert(`删除对话异常: ${error.message}`);
}
},
async duplicateConversation(conversationId) {
console.log('复制对话:', 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 || '复制失败';
alert(`复制失败: ${message}`);
}
} catch (error) {
console.error('复制对话异常:', error);
alert(`复制对话异常: ${error.message}`);
}
},
searchConversations() {
// 简单的搜索功能,实际实现可以调用搜索API
if (this.searchTimer) {
clearTimeout(this.searchTimer);
}
this.searchTimer = setTimeout(() => {
if (this.searchQuery.trim()) {
console.log('搜索对话:', this.searchQuery);
// TODO: 实现搜索API调用
// this.searchConversationsAPI(this.searchQuery);
} else {
// 清空搜索,重新加载全部对话
this.conversationsOffset = 0;
this.loadConversationsList();
}
}, 300);
},
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed;
},
openPersonalPage() {
this.personalPageVisible = true;
},
closePersonalPage() {
this.personalPageVisible = false;
},
async toggleThinkingMode() {
try {
const resp = await fetch('/api/thinking-mode', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ thinking_mode: !this.thinkingMode })
});
const data = await resp.json();
if (data.success) {
this.thinkingMode = !!data.data;
} else {
throw new Error(data.message || data.error || '切换失败');
}
} catch (error) {
console.error('切换思考模式失败:', error);
alert(`切换思考模式失败: ${error.message}`);
}
},
formatTime(timeString) {
if (!timeString) return '';
const date = new Date(timeString);
const now = new Date();
const diffMs = now - date;
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
if (diffHours < 1) {
return '刚刚';
} else if (diffHours < 24) {
return `${diffHours}小时前`;
} else if (diffDays < 7) {
return `${diffDays}天前`;
} else {
return date.toLocaleDateString('zh-CN', {
month: 'short',
day: 'numeric'
});
}
},
// ==========================================
// 原有功能保持不变
// ==========================================
updateFileTree(structure) {
const treeDictionary = structure && structure.tree ? structure.tree : {};
const buildNodes = (treeMap) => {
if (!treeMap) {
return [];
}
const entries = Object.keys(treeMap).map((name) => {
const node = treeMap[name] || {};
if (node.type === 'folder') {
return {
type: 'folder',
name,
path: node.path || name,
children: buildNodes(node.children)
};
}
return {
type: 'file',
name,
path: node.path || name,
annotation: node.annotation || ''
};
});
entries.sort((a, b) => {
if (a.type !== b.type) {
return a.type === 'folder' ? -1 : 1;
}
return a.name.localeCompare(b.name, 'zh-CN');
});
return entries;
};
const nodes = buildNodes(treeDictionary);
const expanded = { ...this.expandedFolders };
const validFolderPaths = new Set();
const ensureExpansion = (list, depth = 0) => {
list.forEach((item) => {
if (item.type === 'folder') {
validFolderPaths.add(item.path);
if (expanded[item.path] === undefined) {
expanded[item.path] = false;
}
ensureExpansion(item.children || [], depth + 1);
}
});
};
ensureExpansion(nodes);
Object.keys(expanded).forEach((path) => {
if (!validFolderPaths.has(path)) {
delete expanded[path];
}
});
this.expandedFolders = expanded;
this.fileTree = nodes;
},
toggleFolder(path) {
this.hideContextMenu();
if (!path) {
return;
}
const current = !!this.expandedFolders[path];
this.expandedFolders = {
...this.expandedFolders,
[path]: !current
};
},
cycleSidebarPanel() {
const order = ['files', 'todo', 'subAgents'];
const nextIndex = (order.indexOf(this.panelMode) + 1) % order.length;
this.panelMode = order[nextIndex];
if (this.panelMode === 'subAgents') {
this.fetchSubAgents();
}
},
togglePanelMenu() {
this.panelMenuOpen = !this.panelMenuOpen;
},
selectPanelMode(mode) {
if (this.panelMode === mode) {
this.panelMenuOpen = false;
return;
}
this.panelMode = mode;
this.panelMenuOpen = false;
if (mode === 'todo') {
this.fetchTodoList();
} else if (mode === 'subAgents') {
this.fetchSubAgents();
}
},
async fetchSubAgents() {
try {
const resp = await fetch('/api/sub_agents');
if (!resp.ok) {
throw new Error(await resp.text());
}
const data = await resp.json();
if (data.success) {
this.subAgents = Array.isArray(data.data) ? data.data : [];
}
} catch (error) {
console.error('获取子智能体列表失败:', error);
}
},
getSubAgentBaseUrl() {
const override = window.SUB_AGENT_BASE_URL || window.__SUB_AGENT_BASE_URL__;
if (override && typeof override === 'string') {
return override.replace(/\/$/, '');
}
const { protocol, hostname } = window.location;
if (hostname && hostname.includes('agent.')) {
const mappedHost = hostname.replace('agent.', 'subagent.');
return `${protocol}//${mappedHost}`;
}
return `${protocol}//${hostname}:8092`;
},
openSubAgent(agent) {
if (!agent || !agent.task_id) {
return;
}
const base = this.getSubAgentBaseUrl();
const parentConv = agent.conversation_id || this.currentConversationId || '';
const convSegment = this.stripConversationPrefix(parentConv);
const agentLabel = agent.agent_id ? `sub_agent${agent.agent_id}` : agent.task_id;
const pathSuffix = convSegment
? `/${convSegment}+${agentLabel}`
: `/sub_agent/${agent.task_id}`;
const url = `${base}${pathSuffix}`;
window.open(url, '_blank');
},
async fetchTodoList() {
try {
const response = await fetch('/api/todo-list');
const data = await response.json();
if (data.success) {
this.todoList = data.data || null;
}
} catch (error) {
console.error('获取待办列表失败:', error);
}
},
triggerFileUpload() {
if (this.uploading) {
return;
}
const input = this.$refs.fileUploadInput;
if (input) {
input.click();
}
},
handleFileSelected(event) {
const fileInput = event?.target;
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
return;
}
const [file] = fileInput.files;
this.uploadSelectedFile(file);
},
resetFileInput() {
const input = this.$refs.fileUploadInput;
if (input) {
input.value = '';
}
},
async uploadSelectedFile(file) {
if (!file || this.uploading) {
this.resetFileInput();
return;
}
this.uploading = true;
try {
const formData = new FormData();
formData.append('file', file);
formData.append('filename', file.name || '');
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
let result = {};
try {
result = await response.json();
} catch (parseError) {
throw new Error('服务器响应无法解析');
}
if (!response.ok || !result.success) {
const message = result.error || result.message || '上传失败';
throw new Error(message);
}
await this.refreshFileTree();
alert(`上传成功:${result.path || file.name}`);
} catch (error) {
console.error('文件上传失败:', error);
alert(`文件上传失败:${error.message}`);
} finally {
this.uploading = false;
this.resetFileInput();
}
},
handleSendOrStop() {
if (this.streamingMessage) {
this.stopTask();
} else {
this.sendMessage();
}
},
sendMessage() {
if (this.streamingMessage || !this.isConnected) {
return;
}
if (!this.inputMessage.trim()) {
return;
}
const message = this.inputMessage;
if (message.startsWith('/')) {
this.socket.emit('send_command', { command: message });
this.inputMessage = '';
this.autoResizeInput();
return;
}
this.messages.push({
role: 'user',
content: message
});
this.currentMessageIndex = -1;
this.socket.emit('send_message', { message: message, conversation_id: this.currentConversationId });
this.inputMessage = '';
this.autoScrollEnabled = true;
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;
console.log('发送停止请求');
}
},
clearChat() {
if (confirm('确定要清除所有对话记录吗?')) {
this.socket.emit('send_command', { command: '/clear' });
}
},
async compressConversation() {
if (!this.currentConversationId) {
alert('当前没有可压缩的对话。');
return;
}
if (this.compressing) {
return;
}
const confirmed = confirm('确定要压缩当前对话记录吗?压缩后会生成新的对话副本。');
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;
}
console.log('对话压缩完成:', result);
} else {
const message = result.message || result.error || '压缩失败';
alert(`压缩失败: ${message}`);
}
} catch (error) {
console.error('压缩对话异常:', error);
alert(`压缩对话异常: ${error.message}`);
} finally {
this.compressing = false;
}
},
toggleToolMenu() {
if (!this.isConnected) {
return;
}
const nextState = !this.toolMenuOpen;
this.toolMenuOpen = nextState;
if (nextState) {
this.settingsOpen = false;
if (!this.quickMenuOpen) {
this.quickMenuOpen = true;
}
this.loadToolSettings(true);
}
},
toggleQuickMenu() {
if (!this.isConnected) {
return;
}
const nextState = !this.quickMenuOpen;
this.quickMenuOpen = nextState;
if (!nextState) {
this.toolMenuOpen = false;
this.settingsOpen = false;
}
},
closeQuickMenu() {
this.quickMenuOpen = false;
this.toolMenuOpen = false;
this.settingsOpen = false;
},
handleQuickUpload() {
if (this.uploading || !this.isConnected) {
return;
}
this.triggerFileUpload();
},
handleQuickModeToggle() {
if (!this.isConnected || this.streamingMessage) {
return;
}
this.toggleThinkingMode();
},
handleInputChange() {
this.autoResizeInput();
},
handleInputFocus() {
this.inputIsFocused = true;
},
handleInputBlur() {
this.inputIsFocused = false;
},
autoResizeInput() {
this.$nextTick(() => {
const textarea = this.$refs.stadiumInput;
if (!textarea) {
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.inputLineCount = Math.max(1, Math.round(targetHeight / lineHeight));
this.inputIsMultiline = 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.$refs.stadiumShellOuter || this.$refs.compactInputShell;
if (shell && shell.contains(event.target)) {
return;
}
this.closeQuickMenu();
},
handleClickOutsidePanelMenu(event) {
if (!this.panelMenuOpen) {
return;
}
const wrapper = this.$refs.panelMenuWrapper;
if (wrapper && wrapper.contains(event.target)) {
return;
}
this.panelMenuOpen = false;
},
applyToolSettingsSnapshot(categories) {
if (!Array.isArray(categories)) {
return;
}
this.toolSettings = categories.map((item) => ({
id: item.id,
label: item.label || item.id,
enabled: !!item.enabled,
tools: Array.isArray(item.tools) ? item.tools : []
}));
this.toolSettingsLoading = false;
},
async loadToolSettings(force = false) {
if (!this.isConnected) {
return;
}
if (this.toolSettingsLoading) {
return;
}
if (!force && this.toolSettings.length > 0) {
return;
}
this.toolSettingsLoading = true;
try {
const response = await fetch('/api/tool-settings');
const data = await response.json();
if (response.ok && data.success && Array.isArray(data.categories)) {
this.applyToolSettingsSnapshot(data.categories);
} else {
console.warn('获取工具设置失败:', data);
this.toolSettingsLoading = false;
}
} catch (error) {
console.error('获取工具设置异常:', error);
this.toolSettingsLoading = false;
}
},
async updateToolCategory(categoryId, enabled) {
if (!this.isConnected) {
return;
}
if (this.toolSettingsLoading) {
return;
}
const previousSnapshot = this.toolSettings.map((item) => ({ ...item }));
this.toolSettings = this.toolSettings.map((item) => {
if (item.id === categoryId) {
return { ...item, enabled };
}
return item;
});
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.toolSettings = previousSnapshot;
}
} catch (error) {
console.error('更新工具设置异常:', error);
this.toolSettings = previousSnapshot;
}
this.toolSettingsLoading = false;
},
toggleSettings() {
if (!this.isConnected) {
return;
}
const nextState = !this.settingsOpen;
this.settingsOpen = nextState;
if (nextState) {
this.toolMenuOpen = false;
if (!this.quickMenuOpen) {
this.quickMenuOpen = true;
}
}
},
toggleFocusPanel() {
this.rightCollapsed = !this.rightCollapsed;
if (!this.rightCollapsed && this.rightWidth < this.minPanelWidth) {
this.rightWidth = this.minPanelWidth;
}
},
addSystemMessage(content) {
this.appendSystemAction(content);
},
appendSystemAction(content) {
const msg = this.ensureAssistantMessage();
msg.actions.push({
id: `system-${Date.now()}-${Math.random()}`,
type: 'system',
content: content,
timestamp: Date.now()
});
this.$forceUpdate();
this.conditionalScrollToBottom();
},
toggleBlock(id) {
if (this.expandedBlocks.has(id)) {
this.expandedBlocks.delete(id);
} else {
this.expandedBlocks.add(id);
}
this.$forceUpdate();
},
// 修复:工具相关方法 - 接收tool对象而不是name
getToolIcon(tool) {
const toolName = typeof tool === 'string' ? tool : (tool && tool.name);
return TOOL_ICON_MAP[toolName] || 'settings';
},
getToolAnimationClass(tool) {
// 根据工具状态返回不同的动画类
if (tool.status === 'hinted') {
return 'hint-animation pulse-slow';
} else if (tool.status === 'preparing') {
return 'preparing-animation';
} else if (tool.status === 'running') {
const animations = {
'create_file': 'file-animation',
'read_file': 'read-animation',
'delete_file': 'file-animation',
'rename_file': 'file-animation',
'modify_file': 'file-animation',
'append_to_file': 'file-animation',
'create_folder': 'file-animation',
'focus_file': 'focus-animation',
'unfocus_file': 'focus-animation',
'web_search': 'search-animation',
'extract_webpage': 'search-animation',
'save_webpage': 'file-animation',
'run_python': 'code-animation',
'run_command': 'terminal-animation',
'update_memory': 'memory-animation',
'sleep': 'wait-animation',
'terminal_session': 'terminal-animation',
'terminal_input': 'terminal-animation',
'terminal_snapshot': 'terminal-animation',
'terminal_reset': 'terminal-animation',
'todo_create': 'file-animation',
'todo_update_task': 'file-animation',
'todo_finish': 'file-animation',
'todo_finish_confirm': 'file-animation',
'create_sub_agent': 'terminal-animation',
'wait_sub_agent': 'wait-animation'
};
return animations[tool.name] || 'default-animation';
}
return '';
},
// 修复:获取工具状态文本
getToolStatusText(tool) {
// 优先使用自定义消息
if (tool.message) {
return tool.message;
}
if (tool.status === 'hinted') {
return `可能需要 ${tool.name}...`;
} else if (tool.status === 'preparing') {
return `准备调用 ${tool.name}...`;
} else if (tool.status === 'running') {
const texts = {
'create_file': '正在创建文件...',
'sleep': '正在等待...',
'delete_file': '正在删除文件...',
'rename_file': '正在重命名文件...',
'modify_file': '正在修改文件...',
'append_to_file': '正在追加文件...',
'create_folder': '正在创建文件夹...',
'focus_file': '正在聚焦文件...',
'unfocus_file': '正在取消聚焦...',
'web_search': '正在搜索网络...',
'extract_webpage': '正在提取网页...',
'save_webpage': '正在保存网页...',
'run_python': '正在执行Python代码...',
'run_command': '正在执行命令...',
'update_memory': '正在更新记忆...',
'terminal_session': '正在管理终端会话...',
'terminal_input': '正在发送终端输入...',
'terminal_snapshot': '正在获取终端快照...',
'terminal_reset': '正在重置终端...'
};
if (tool.name === 'read_file') {
const readType = ((tool.argumentSnapshot && tool.argumentSnapshot.type) ||
(tool.arguments && tool.arguments.type) || 'read').toLowerCase();
const runningMap = {
'read': '正在读取文件...',
'search': '正在执行搜索...',
'extract': '正在提取内容...'
};
return runningMap[readType] || '正在读取文件...';
}
return texts[tool.name] || '正在执行...';
} else if (tool.status === 'completed') {
// 修复:完成状态的文本
const texts = {
'create_file': '文件创建成功',
'delete_file': '文件删除成功',
'sleep': '等待完成',
'rename_file': '文件重命名成功',
'modify_file': '文件修改成功',
'append_to_file': '文件追加完成',
'create_folder': '文件夹创建成功',
'focus_file': '文件聚焦成功',
'unfocus_file': '取消聚焦成功',
'web_search': '搜索完成',
'extract_webpage': '网页提取完成',
'save_webpage': '网页保存完成(纯文本)',
'run_python': '代码执行完成',
'run_command': '命令执行完成',
'update_memory': '记忆更新成功',
'terminal_session': '终端操作完成',
'terminal_input': '终端输入完成',
'terminal_snapshot': '终端快照已返回',
'terminal_reset': '终端已重置'
};
if (tool.name === 'read_file' && tool.result && typeof tool.result === 'object') {
const readType = (tool.result.type || 'read').toLowerCase();
if (readType === 'search') {
const query = tool.result.query ? `「${tool.result.query}」` : '';
const count = typeof tool.result.returned_matches === 'number'
? tool.result.returned_matches
: (tool.result.actual_matches || 0);
return `搜索${query},得到${count}个结果`;
} else if (readType === 'extract') {
const segments = Array.isArray(tool.result.segments) ? tool.result.segments : [];
const totalLines = segments.reduce((sum, seg) => {
const start = Number(seg.line_start) || 0;
const end = Number(seg.line_end) || 0;
if (!start || !end || end < start) return sum;
return sum + (end - start + 1);
}, 0);
const displayLines = totalLines || tool.result.char_count || 0;
return `提取了${displayLines}行`;
}
return '文件读取完成';
}
return texts[tool.name] || '执行完成';
} else {
// 其他状态
return `${tool.name} - ${tool.status}`;
}
},
getToolDescription(tool) {
const args = tool.argumentSnapshot || tool.arguments;
const argumentLabel = tool.argumentLabel || this.buildToolLabel(args);
if (argumentLabel) {
return argumentLabel;
}
if (tool.statusDetail) {
return tool.statusDetail;
}
if (tool.result && typeof tool.result === 'object') {
if (tool.result.path) {
return tool.result.path.split('/').pop();
}
}
return '';
},
cloneToolArguments(args) {
if (!args || typeof args !== 'object') {
return null;
}
try {
return JSON.parse(JSON.stringify(args));
} catch (error) {
console.warn('无法克隆工具参数:', error);
return { ...args };
}
},
buildToolLabel(args) {
if (!args || typeof args !== 'object') {
return '';
}
if (args.command) {
return args.command;
}
if (args.path) {
return args.path.split('/').pop();
}
if (args.target_path) {
return args.target_path.split('/').pop();
}
if (args.query) {
return `"${args.query}"`;
}
if (args.seconds !== undefined) {
return `${args.seconds} 秒`;
}
if (args.name) {
return args.name;
}
return '';
},
formatSearchTopic(filters) {
const mapping = {
'general': '通用',
'news': '新闻',
'finance': '金融'
};
const topic = (filters && filters.topic) ? String(filters.topic).toLowerCase() : 'general';
return mapping[topic] || '通用';
},
formatSearchTime(filters) {
if (!filters) {
return '未限定时间';
}
if (filters.time_range) {
const mapping = {
'day': '过去24小时',
'week': '过去7天',
'month': '过去30天',
'year': '过去365天'
};
return mapping[filters.time_range] || `相对范围:${filters.time_range}`;
}
if (typeof filters.days === 'number') {
return `过去${filters.days}天`;
}
if (filters.start_date && filters.end_date) {
return `${filters.start_date} 至 ${filters.end_date}`;
}
return '未限定时间';
},
renderMarkdown(text, isStreaming = false) {
if (!text) return '';
if (typeof marked === 'undefined') {
return text;
}
marked.setOptions({
breaks: true,
gfm: true,
sanitize: false
});
if (!isStreaming) {
if (!this.markdownCache) {
this.markdownCache = new Map();
}
const cacheKey = `${text.length}_${text.substring(0, 100)}`;
if (this.markdownCache.has(cacheKey)) {
return this.markdownCache.get(cacheKey);
}
}
let html = marked.parse(text);
html = this.wrapCodeBlocks(html, isStreaming);
if (!isStreaming && text.length < 10000) {
if (!this.markdownCache) {
this.markdownCache = new Map();
}
this.markdownCache.set(`${text.length}_${text.substring(0, 100)}`, html);
if (this.markdownCache.size > 20) {
const firstKey = this.markdownCache.keys().next().value;
this.markdownCache.delete(firstKey);
}
}
// 只在非流式状态处理(流式状态由renderLatexInRealtime处理)
if (!isStreaming) {
setTimeout(() => {
// 代码高亮
if (typeof Prism !== 'undefined') {
const codeBlocks = document.querySelectorAll('.code-block-wrapper pre code:not([data-highlighted])');
codeBlocks.forEach(block => {
try {
Prism.highlightElement(block);
block.setAttribute('data-highlighted', 'true');
} catch (e) {
console.warn('代码高亮失败:', e);
}
});
}
// LaTeX最终渲染
if (typeof renderMathInElement !== 'undefined') {
const elements = document.querySelectorAll('.text-output .text-content:not(.streaming-text)');
elements.forEach(element => {
if (element.hasAttribute('data-math-rendered')) return;
try {
renderMathInElement(element, {
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false},
{left: '\\[', right: '\\]', display: true},
{left: '\\(', right: '\\)', display: false}
],
throwOnError: false,
trust: true
});
element.setAttribute('data-math-rendered', 'true');
} catch (e) {
console.warn('LaTeX渲染失败:', e);
}
});
}
}, 100);
}
return html;
},
// 实时LaTeX渲染(用于流式输出)
renderLatexInRealtime() {
if (typeof renderMathInElement === 'undefined') {
return;
}
// 使用requestAnimationFrame优化性能
if (this._latexRenderTimer) {
cancelAnimationFrame(this._latexRenderTimer);
}
this._latexRenderTimer = requestAnimationFrame(() => {
const elements = document.querySelectorAll('.text-output .streaming-text');
elements.forEach(element => {
try {
renderMathInElement(element, {
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false},
{left: '\\[', right: '\\]', display: true},
{left: '\\(', right: '\\)', display: false}
],
throwOnError: false,
trust: true
});
} catch (e) {
// 忽略错误,继续渲染
}
});
});
},
// 用字符串替换包装代码块
// 用字符串替换包装代码块 - 添加streaming参数
wrapCodeBlocks(html, isStreaming = false) {
// 如果是流式输出,不包装代码块,保持原样
if (isStreaming) {
return html;
}
let counter = 0;
// 匹配 ...
return html.replace(/]*)>([\s\S]*?)<\/code><\/pre>/g, (match, attributes, content) => {
// 提取语言
const langMatch = attributes.match(/class="[^"]*language-(\w+)/);
const language = langMatch ? langMatch[1] : 'text';
// 生成唯一ID
const blockId = `code-${Date.now()}-${counter++}`;
// 转义引号用于data属性
const escapedContent = content
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
// 构建新的HTML,保持code元素原样
return `
`;
});
},
getLanguageClass(path) {
const ext = path.split('.').pop().toLowerCase();
const langMap = {
'py': 'language-python',
'js': 'language-javascript',
'html': 'language-html',
'css': 'language-css',
'json': 'language-json',
'md': 'language-markdown',
'txt': 'language-plain'
};
return langMap[ext] || 'language-plain';
},
scrollToBottom() {
setTimeout(() => {
const messagesArea = this.$refs.messagesArea;
if (messagesArea) {
// 标记为程序触发的滚动
if (this._setScrollingFlag) {
this._setScrollingFlag(true);
}
messagesArea.scrollTop = messagesArea.scrollHeight;
// 滚动完成后重置标记
setTimeout(() => {
if (this._setScrollingFlag) {
this._setScrollingFlag(false);
}
}, 100);
}
}, 50);
},
conditionalScrollToBottom() {
// 严格检查:只在明确允许时才滚动
if (this.autoScrollEnabled === true && this.userScrolling === false) {
this.scrollToBottom();
}
},
toggleScrollLock() {
const currentlyLocked = this.autoScrollEnabled && !this.userScrolling;
if (currentlyLocked) {
this.autoScrollEnabled = false;
this.userScrolling = true;
} else {
this.autoScrollEnabled = true;
this.userScrolling = false;
this.scrollToBottom();
}
},
scrollThinkingToBottom(blockId) {
if (!this.thinkingScrollLocks.get(blockId)) return;
const refName = `thinkingContent-${blockId}`;
const elRef = this.$refs[refName];
const el = Array.isArray(elRef) ? elRef[0] : elRef;
if (el) {
el.scrollTop = el.scrollHeight;
}
},
handleThinkingScroll(blockId, event) {
const el = event.target;
const threshold = 12;
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
this.thinkingScrollLocks.set(blockId, atBottom);
},
// 面板调整方法
startResize(panel, event) {
this.isResizing = true;
this.resizingPanel = panel;
if (panel === 'right' && this.rightCollapsed) {
this.rightCollapsed = false;
if (this.rightWidth < this.minPanelWidth) {
this.rightWidth = this.minPanelWidth;
}
}
document.addEventListener('mousemove', this.handleResize);
document.addEventListener('mouseup', this.stopResize);
document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize';
event.preventDefault();
},
handleResize(event) {
if (!this.isResizing) return;
const containerWidth = document.querySelector('.main-container').offsetWidth;
if (this.resizingPanel === 'left') {
let newWidth = event.clientX - (this.sidebarCollapsed ? 60 : 300);
newWidth = Math.max(this.minPanelWidth, Math.min(newWidth, this.maxPanelWidth));
this.leftWidth = newWidth;
} else if (this.resizingPanel === 'right') {
let newWidth = containerWidth - event.clientX;
newWidth = Math.max(this.minPanelWidth, Math.min(newWidth, this.maxPanelWidth));
this.rightWidth = newWidth;
} else if (this.resizingPanel === 'conversation') {
// 对话侧边栏宽度调整
let newWidth = event.clientX;
newWidth = Math.max(200, Math.min(newWidth, 400));
// 这里可以动态调整对话侧边栏宽度,暂时不实现
}
},
stopResize() {
this.isResizing = false;
this.resizingPanel = null;
document.removeEventListener('mousemove', this.handleResize);
document.removeEventListener('mouseup', this.stopResize);
document.body.style.userSelect = '';
document.body.style.cursor = '';
},
async handleEasterEggPayload(payload) {
if (!payload) {
return;
}
let parsed = payload;
if (typeof payload === 'string') {
try {
parsed = JSON.parse(payload);
} catch (error) {
console.warn('无法解析彩蛋结果:', payload);
return;
}
}
if (!parsed || typeof parsed !== 'object') {
return;
}
if (!parsed.success) {
if (parsed.error) {
console.warn('彩蛋触发失败:', parsed.error);
}
await this.destroyEasterEggEffect(true);
return;
}
const effectName = (parsed.effect || '').toLowerCase();
if (!effectName) {
console.warn('彩蛋结果缺少 effect 字段');
return;
}
await this.startEasterEggEffect(effectName, parsed);
},
async startEasterEggEffect(effectName, payload = {}) {
const registry = window.EasterEggRegistry;
if (!registry) {
console.warn('EasterEggRegistry 尚未加载,无法播放彩蛋');
return;
}
if (!registry.has(effectName)) {
console.warn('未注册的彩蛋 effect:', effectName);
await this.destroyEasterEggEffect(true);
return;
}
const root = this.$refs.easterEggRoot;
if (!root) {
console.warn('未找到彩蛋根节点');
return;
}
await this.destroyEasterEggEffect(true);
this.easterEgg.active = true;
this.easterEgg.effect = effectName;
this.easterEgg.payload = payload;
const instance = registry.start(effectName, {
root,
payload,
app: this
});
if (!instance) {
this.finishEasterEggCleanup();
return;
}
this.easterEgg.instance = instance;
this.easterEgg.destroyPromise = null;
this.easterEgg.destroying = false;
if (this.easterEgg.cleanupTimer) {
clearTimeout(this.easterEgg.cleanupTimer);
}
const durationSeconds = Math.max(8, Number(payload.duration_seconds) || 45);
this.easterEgg.cleanupTimer = setTimeout(() => {
const cleanup = this.destroyEasterEggEffect(false);
if (cleanup && typeof cleanup.catch === 'function') {
cleanup.catch(() => {});
}
}, durationSeconds * 1000);
if (payload.message) {
console.info(`[彩蛋] ${payload.display_name || effectName}: ${payload.message}`);
}
},
destroyEasterEggEffect(forceImmediate = false) {
if (this.easterEgg.cleanupTimer) {
clearTimeout(this.easterEgg.cleanupTimer);
this.easterEgg.cleanupTimer = null;
}
const instance = this.easterEgg.instance;
if (!instance) {
this.finishEasterEggCleanup();
return Promise.resolve();
}
if (this.easterEgg.destroying) {
return this.easterEgg.destroyPromise || Promise.resolve();
}
this.easterEgg.destroying = true;
let result;
try {
result = instance.destroy({
immediate: forceImmediate,
payload: this.easterEgg.payload,
root: this.$refs.easterEggRoot || null
});
} catch (error) {
console.warn('销毁彩蛋时发生错误:', error);
this.easterEgg.destroying = false;
this.finishEasterEggCleanup();
return Promise.resolve();
}
const finalize = () => {
this.easterEgg.destroyPromise = null;
this.easterEgg.destroying = false;
this.finishEasterEggCleanup();
};
if (result && typeof result.then === 'function') {
this.easterEgg.destroyPromise = result.then(() => {
finalize();
}).catch((error) => {
console.warn('彩蛋清理失败:', error);
finalize();
});
return this.easterEgg.destroyPromise;
} else {
finalize();
return Promise.resolve();
}
},
finishEasterEggCleanup() {
if (this.easterEgg.cleanupTimer) {
clearTimeout(this.easterEgg.cleanupTimer);
this.easterEgg.cleanupTimer = null;
}
const root = this.$refs.easterEggRoot;
if (root) {
root.innerHTML = '';
}
this.easterEgg.active = false;
this.easterEgg.effect = null;
this.easterEgg.payload = null;
this.easterEgg.instance = null;
this.easterEgg.destroyPromise = null;
this.easterEgg.destroying = false;
},
// 格式化token显示(修复NaN问题)
formatTokenCount(tokens) {
// 确保tokens是数字,防止NaN
const num = Number(tokens) || 0;
if (num < 1000) {
return num.toString();
} else if (num < 1000000) {
return (num / 1000).toFixed(1) + 'K';
} else {
return (num / 1000000).toFixed(1) + 'M';
}
}
}
});
app.component('file-node', {
name: 'FileNode',
props: {
node: {
type: Object,
required: true
},
level: {
type: Number,
default: 0
},
expandedFolders: {
type: Object,
required: true
}
},
emits: ['toggle-folder', 'context-menu'],
computed: {
isExpanded() {
if (this.node.type !== 'folder') {
return false;
}
const value = this.expandedFolders[this.node.path];
return value === undefined ? true : value;
},
folderPadding() {
return {
paddingLeft: `${12 + this.level * 16}px`
};
},
filePadding() {
return {
paddingLeft: `${40 + this.level * 16}px`
};
}
},
methods: {
iconStyle(iconKey) {
const iconPath = ICONS[iconKey];
return iconPath ? { '--icon-src': `url(${iconPath})` } : {};
},
toggle() {
if (this.node.type === 'folder') {
this.$emit('toggle-folder', this.node.path);
}
}
},
template: `
{{ node.name }}
{{ node.annotation }}
`
});
app.mount('#app');
console.log('Vue应用初始化完成');
}
window.addEventListener('load', () => {
bootstrapApp();
});