agent-Specialization/static/app.js

3012 lines
132 KiB
JavaScript
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.

// 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'
];
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 = '<h1 style="color:red;">Vue.js 加载失败,请刷新页面</h1>';
return;
}
if (typeof io === 'undefined') {
const loaded = await ensureSocketIOLoaded();
if (!loaded || typeof io === 'undefined') {
console.error('错误Socket.IO 未加载');
document.body.innerHTML = '<h1 style="color:red;">Socket.IO 加载失败,请检查网络后刷新页面</h1>';
return;
}
}
console.log('所有依赖加载成功初始化Vue应用...');
const { createApp } = Vue;
const app = createApp({
data() {
return {
// 连接状态
isConnected: false,
socket: null,
// 系统信息
projectPath: '',
agentVersion: '',
thinkingMode: '未知',
// 消息相关
messages: [],
inputMessage: '',
// 当前消息索引
currentMessageIndex: -1,
streamingMessage: false,
// 停止功能状态
stopRequested: false,
// 路由相关
initialRouteResolved: false,
// 文件相关
fileTree: [],
focusedFiles: {},
expandedFolders: {},
// 展开状态管理
expandedBlocks: new Set(),
// 滚动控制
userScrolling: false,
autoScrollEnabled: true,
// 面板宽度控制
leftWidth: 280,
rightWidth: 420,
rightCollapsed: true,
isResizing: false,
resizingPanel: null,
minPanelWidth: 200,
maxPanelWidth: 600,
// 工具状态跟踪
preparingTools: new Map(),
activeTools: new Map(),
toolActionIndex: new Map(),
toolActionIndex: new Map(),
toolStacks: new Map(),
// ==========================================
// 对话管理相关状态
// ==========================================
// 对话历史侧边栏
sidebarCollapsed: true, // 默认收起对话侧边栏
showTodoList: false,
conversations: [],
conversationsLoading: false,
hasMoreConversations: false,
loadingMoreConversations: false,
currentConversationId: null,
currentConversationTitle: '当前对话',
// 搜索功能
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: false,
// 对话压缩状态
compressing: false,
// 设置菜单状态
settingsOpen: false,
// 工具控制菜单
toolMenuOpen: false,
toolSettings: [],
toolSettingsLoading: false,
// 文件上传状态
uploading: false,
// TODO 列表
todoList: null,
todoEmoji: '🗒️',
fileEmoji: '📁',
todoDoneEmoji: '☑️',
todoPendingEmoji: '⬜️',
toolCategoryEmojis: {
network: '🌐',
file_edit: '📝',
read_focus: '🔍',
terminal_realtime: '🖥️',
terminal_command: '⌨️',
memory: '🧠',
todo: '🗒️'
},
// 右键菜单相关
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.handleClickOutsideSettings);
document.addEventListener('click', this.handleClickOutsideToolMenu);
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);
},
beforeUnmount() {
document.removeEventListener('click', this.handleClickOutsideSettings);
document.removeEventListener('click', this.handleClickOutsideToolMenu);
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;
}
},
methods: {
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 || '';
if (data.cleared) {
// 对话被清空
this.messages = [];
this.currentConversationId = null;
this.currentConversationTitle = '';
// 重置Token统计
this.resetTokenStatistics();
history.replaceState({}, '', '/new');
}
// 刷新对话列表
this.loadConversationsList();
this.fetchTodoList();
});
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;
}
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;
}
});
// 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 = `${this.currentMessageIndex}-thinking-${msg.actions.length - 1}`;
this.expandedBlocks.add(blockId);
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();
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;
}
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 (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.$forceUpdate();
this.settingsOpen = false;
this.toolMenuOpen = false;
this.toolSettingsLoading = false;
this.toolSettings = [];
console.log('前端状态重置完成');
},
// 重置Token统计
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;
history.pushState({ conversationId }, '', `/${this.stripConversationPrefix(conversationId)}`);
// 3. 重置UI状态
this.resetAllStates();
// 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}`);
}
},
// ==========================================
// 关键功能:获取并显示历史对话内容
// ==========================================
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 || '';
const thinkPatterns = [
/<think>([\s\S]*?)<\/think>/g,
/<thinking>([\s\S]*?)<\/thinking>/g
];
let allThinkingContent = '';
for (const pattern of thinkPatterns) {
let match;
while ((match = pattern.exec(content)) !== null) {
allThinkingContent += match[1].trim() + '\n';
}
}
if (allThinkingContent) {
currentAssistantMessage.actions.push({
id: `history-think-${Date.now()}-${Math.random()}`,
type: 'thinking',
content: allThinkingContent.trim(),
streaming: false,
timestamp: Date.now()
});
console.log('添加思考内容:', allThinkingContent.substring(0, 50) + '...');
}
// 处理普通文本内容(移除思考标签后的内容)
const metadata = message.metadata || {};
const appendPayloadMeta = metadata.append_payload;
const modifyPayloadMeta = metadata.modify_payload;
let textContent = content
.replace(/<think>[\s\S]*?<\/think>/g, '')
.replace(/<thinking>[\s\S]*?<\/thinking>/g, '')
.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) {
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)}...`);
if (message.name === 'append_to_file' && result && typeof result === 'object') {
const appendSummary = {
path: result.path || '未知文件',
success: result.success !== false,
summary: result.message || (result.success === false ? '追加失败' : '追加完成'),
lines: result.lines || 0,
bytes: result.bytes || 0,
forced: !!result.forced
};
currentAssistantMessage.actions.push({
id: `history-append-${Date.now()}-${Math.random()}`,
type: 'append',
append: appendSummary,
timestamp: Date.now()
});
}
} 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;
},
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
};
},
toggleTodoPanel() {
this.showTodoList = !this.showTodoList;
},
formatTaskStatus(task) {
if (!task) {
return '';
}
return task.status === 'done'
? `${this.todoDoneEmoji} 完成`
: `${this.todoPendingEmoji} 未完成`;
},
toolCategoryEmoji(categoryId) {
return this.toolCategoryEmojis[categoryId] || '⚙️';
},
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.settingsOpen = false;
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.settingsOpen = false;
// 发送消息后延迟更新当前上下文Token关键修复恢复原逻辑
setTimeout(() => {
if (this.currentConversationId) {
this.updateCurrentContextTokens();
}
}, 1000);
},
// 新增:停止任务方法
stopTask() {
if (this.streamingMessage && !this.stopRequested) {
this.socket.emit('stop_task');
this.stopRequested = true;
console.log('发送停止请求');
}
this.settingsOpen = false;
},
clearChat() {
if (confirm('确定要清除所有对话记录吗?')) {
this.socket.emit('send_command', { command: '/clear' });
}
this.settingsOpen = false;
},
async compressConversation() {
if (!this.currentConversationId) {
alert('当前没有可压缩的对话。');
return;
}
if (this.compressing) {
return;
}
const confirmed = confirm('确定要压缩当前对话记录吗?压缩后会生成新的对话副本。');
if (!confirmed) {
return;
}
this.settingsOpen = false;
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;
this.loadToolSettings();
}
},
handleClickOutsideToolMenu(event) {
if (!this.toolMenuOpen) {
return;
}
const dropdown = this.$refs.toolDropdown;
if (dropdown && !dropdown.contains(event.target)) {
this.toolMenuOpen = 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;
}
this.settingsOpen = !this.settingsOpen;
if (this.settingsOpen) {
this.toolMenuOpen = false;
}
},
toggleFocusPanel() {
this.rightCollapsed = !this.rightCollapsed;
if (!this.rightCollapsed && this.rightWidth < this.minPanelWidth) {
this.rightWidth = this.minPanelWidth;
}
this.settingsOpen = false;
},
handleClickOutsideSettings(event) {
if (!this.settingsOpen) {
return;
}
const dropdown = this.$refs.settingsDropdown;
if (dropdown && !dropdown.contains(event.target)) {
this.settingsOpen = false;
}
},
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.name;
const icons = {
'create_file': '📄',
'sleep': '⏱️',
'read_file': '📖',
'delete_file': '🗑️',
'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': '♻️',
'todo_create': '🗒️',
'todo_update_task': '☑️',
'todo_finish': '🏁',
'todo_finish_confirm': '❗'
};
return icons[toolName] || '⚙️';
},
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'
};
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;
// 匹配 <pre><code ...>...</code></pre>
return html.replace(/<pre><code([^>]*)>([\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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
// 构建新的HTML保持code元素原样
return `
<div class="code-block-wrapper">
<div class="code-block-header">
<span class="code-language">${language}</span>
<button class="copy-code-btn" data-code="${blockId}" title="复制代码">📋</button>
</div>
<pre><code${attributes} data-code-id="${blockId}" data-original-code="${escapedContent}">${content}</code></pre>
</div>`;
});
},
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();
}
},
// 面板调整方法
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 = '';
},
// 格式化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: {
toggle() {
if (this.node.type === 'folder') {
this.$emit('toggle-folder', this.node.path);
}
}
},
template: `
<div class="file-node-wrapper" @contextmenu.stop.prevent="$emit('context-menu', { node, event: $event })">
<div v-if="node.type === 'folder'" class="file-node folder-node">
<button class="folder-header" type="button" :style="folderPadding" @click="toggle">
<span class="folder-arrow">{{ isExpanded ? '▾' : '▸' }}</span>
<span class="folder-icon">{{ isExpanded ? '📂' : '📁' }}</span>
<span class="folder-name">{{ node.name }}</span>
</button>
<div v-show="isExpanded" class="folder-children">
<file-node
v-for="child in node.children"
:key="child.path"
:node="child"
:level="level + 1"
:expanded-folders="expandedFolders"
@toggle-folder="$emit('toggle-folder', $event)"
@context-menu="$emit('context-menu', $event)"
></file-node>
</div>
</div>
<div v-else class="file-node file-leaf" :style="filePadding">
<span class="file-icon">📄</span>
<span class="file-name">{{ node.name }}</span>
<span v-if="node.annotation" class="annotation">{{ node.annotation }}</span>
</div>
</div>
`
});
app.mount('#app');
console.log('Vue应用初始化完成');
}
window.addEventListener('load', () => {
bootstrapApp();
});