// 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 = '
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 urlParams = new URLSearchParams(window.location.search || '');
const pathName = window.location.pathname || '';
const plusRouteMatch = pathName.match(/\/?([^\/+]+)\+([^\/]+)/i);
const slashRouteMatch = pathName.match(/sub_agent[_/](.+)$/i);
let detectedVariant = window.APP_VARIANT || urlParams.get('variant') || null;
let detectedTaskId = window.SUB_AGENT_TASK_ID || urlParams.get('task_id') || null;
let detectedConversationId = window.SUB_AGENT_CONVERSATION_ID || urlParams.get('conversation_id') || null;
if (!detectedVariant) {
if (plusRouteMatch || slashRouteMatch) {
detectedVariant = 'sub_agent';
} else {
detectedVariant = 'main';
}
}
if (!detectedTaskId) {
if (plusRouteMatch) {
detectedTaskId = slashRouteMatch ? slashRouteMatch[1] : plusRouteMatch[2];
} else if (slashRouteMatch) {
detectedTaskId = slashRouteMatch[1];
}
}
if (!detectedConversationId && plusRouteMatch) {
detectedConversationId = plusRouteMatch[1];
}
const initialVariant = detectedVariant;
const initialSubAgentTaskId = detectedTaskId;
const initialSubAgentConversationId = detectedConversationId;
const app = createApp({
data() {
return {
// 子智能体模式
variant: initialVariant,
subAgentTaskId: initialSubAgentTaskId || '',
subAgentConversationId: initialSubAgentConversationId
? (initialSubAgentConversationId.startsWith('conv_') ? initialSubAgentConversationId : `conv_${initialSubAgentConversationId}`)
: null,
subAgentTaskInfo: {
task_id: initialSubAgentTaskId || '',
agent_id: '',
summary: '',
status: '',
message: '',
workspace_dir: '',
references_dir: '',
deliverables_dir: '',
target_project_dir: '',
reference_manifest: [],
last_tool: ''
},
readonlyNoticeShown: false,
// 连接状态
isConnected: false,
socket: null,
// 系统信息
projectPath: '',
agentVersion: '',
thinkingMode: '未知',
// 消息相关
messages: [],
inputMessage: '',
// 当前消息索引
currentMessageIndex: -1,
streamingMessage: false,
// 停止功能状态
stopRequested: false,
terminating: false,
// 路由相关
initialRouteResolved: false,
// 文件相关
fileTree: [],
focusedFiles: {},
expandedFolders: {},
// 展开状态管理
expandedBlocks: new Set(),
// 滚动控制
userScrolling: false,
autoScrollEnabled: true,
scrollListenerAttached: false,
scrollListenerRetryWarned: false,
// 面板宽度控制
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, // 默认收起对话侧边栏
panelMode: 'files', // files | todo | subAgents
subAgents: [],
subAgentPollTimer: null,
conversations: [],
conversationsLoading: false,
hasMoreConversations: false,
loadingMoreConversations: false,
currentConversationId: null,
currentConversationTitle: '当前对话',
// 搜索功能
searchQuery: '',
searchTimer: null,
// 分页
conversationsOffset: 0,
conversationsLimit: 20,
// 对话压缩状态
compressing: false,
// 设置菜单状态
settingsOpen: false,
thinkingScrollLocks: new Map(),
// 工具控制菜单
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: '🗒️',
sub_agent: '🤖'
},
// 右键菜单相关
contextMenu: {
visible: false,
x: 0,
y: 0,
node: null
},
onDocumentClick: null,
onWindowScroll: null,
onKeydownListener: null
}
},
async mounted() {
console.log('Vue应用已挂载');
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);
if (this.isSubAgentView) {
document.documentElement.classList.add('sub-agent-view');
document.body.classList.add('sub-agent-view');
await this.bootstrapSubAgentRoute();
this.initSocket();
this.initScrollListener();
this.loadSubAgentInitialData();
this.subAgentPollTimer = setInterval(() => {
this.fetchSubAgentTaskInfo(true);
}, 4000);
} else {
await this.bootstrapRoute();
this.initSocket();
this.initScrollListener();
setTimeout(() => {
this.loadInitialData();
}, 500);
this.fetchSubAgents();
this.subAgentPollTimer = setInterval(() => {
if (this.panelMode === 'subAgents') {
this.fetchSubAgents();
}
}, 5000);
}
},
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;
}
if (this.subAgentPollTimer) {
clearInterval(this.subAgentPollTimer);
this.subAgentPollTimer = null;
}
if (this.isSubAgentView) {
document.documentElement.classList.remove('sub-agent-view');
document.body.classList.remove('sub-agent-view');
}
},
computed: {
isSubAgentView() {
return this.variant === 'sub_agent';
}
},
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 bootstrapSubAgentRoute() {
if (!this.isSubAgentView) {
await this.bootstrapRoute();
return;
}
if (this.subAgentConversationId) {
this.currentConversationId = this.subAgentConversationId;
this.currentConversationTitle = this.subAgentTaskInfo.summary || '子智能体对话';
this.initialRouteResolved = true;
return;
}
if (!this.subAgentTaskId) {
this.initialRouteResolved = true;
return;
}
const info = await this.fetchSubAgentTaskInfo(true);
if (info && info.sub_conversation_id) {
const convId = info.sub_conversation_id;
this.subAgentConversationId = convId;
this.currentConversationId = convId;
this.currentConversationTitle = info.summary || this.currentConversationTitle;
}
this.initialRouteResolved = true;
},
async loadSubAgentInitialData() {
if (!this.isSubAgentView) {
this.loadInitialData();
return;
}
const info = await this.fetchSubAgentTaskInfo();
await this.fetchSubAgentConversation();
await this.fetchSubAgentFileTree();
await this.fetchTodoList();
if (!this.socket || !this.socket.connected) {
setTimeout(() => {
if (!this.socket || !this.socket.connected) {
console.warn('WebSocket尚未建立,启用只读回放模式。');
this.isConnected = true;
if (!this.readonlyNoticeShown) {
this.readonlyNoticeShown = true;
const status = (info && info.status) ? info.status : 'unknown';
this.addSystemMessage(`🔗 子智能体连接尚未建立,已进入只读模式(当前状态:${status})。`);
}
}
}, 1200);
}
},
async fetchSubAgentTaskInfo(silent = false) {
if (!this.subAgentTaskId) {
return null;
}
try {
const resp = await fetch(`/tasks/${encodeURIComponent(this.subAgentTaskId)}`);
if (!resp.ok) {
let payload;
try {
payload = await resp.json();
} catch (err) {
payload = { error: await resp.text() };
}
if (resp.status === 404) {
const fallback = {
task_id: this.subAgentTaskId,
status: 'archived',
summary: this.currentConversationTitle || '',
message: payload && payload.message ? payload.message : '任务不存在'
};
this.subAgentTaskInfo = { ...this.subAgentTaskInfo, ...fallback };
return fallback;
}
throw new Error(payload && (payload.error || payload.message) ? (payload.error || payload.message) : '加载失败');
}
const data = await resp.json();
if (!data.success) {
throw new Error(data.error || '加载失败');
}
this.subAgentTaskInfo = {
...this.subAgentTaskInfo,
...data
};
if (data.task_id && data.task_id !== this.subAgentTaskId) {
this.subAgentTaskId = data.task_id;
if (this.socket && this.socket.connected) {
this.socket.emit('sub_agent_join', { task_id: data.task_id });
}
}
if (data.workspace_dir) {
this.projectPath = data.workspace_dir;
}
if (data.sub_conversation_id && !this.subAgentConversationId) {
const convId = data.sub_conversation_id;
this.subAgentConversationId = convId;
this.currentConversationId = convId;
}
if (!silent && data.summary) {
this.currentConversationTitle = data.summary;
}
return data;
} catch (error) {
if (!silent) {
console.warn('获取子智能体任务信息失败:', error);
}
if (!this.isConnected) {
this.addSystemMessage(`⚠️ 无法获取子智能体任务信息:${error.message || error}`);
}
return null;
}
},
async fetchSubAgentConversation() {
if (!this.subAgentTaskId) {
return;
}
try {
const resp = await fetch(`/tasks/${encodeURIComponent(this.subAgentTaskId)}/conversation`);
if (!resp.ok) {
const fallbackHandled = await this.tryFetchArchivedConversation();
if (!fallbackHandled) {
throw new Error(await resp.text());
}
return;
}
const data = await resp.json();
if (!data.success) {
const fallbackHandled = await this.tryFetchArchivedConversation();
if (!fallbackHandled) {
throw new Error(data.error || '加载失败');
}
return;
}
if (data.conversation_id) {
this.currentConversationId = data.conversation_id;
}
const transformed = this.transformHistoryMessages(data.messages || []);
this.messages = transformed;
this.$nextTick(() => this.scrollToBottom(true));
if (!this.currentConversationTitle && this.subAgentTaskInfo.summary) {
this.currentConversationTitle = this.subAgentTaskInfo.summary;
}
} catch (error) {
console.warn('加载子智能体对话失败:', error);
if (!this.isConnected) {
this.isConnected = true;
}
this.addSystemMessage(`⚠️ 加载子智能体对话失败:${error.message || error}`);
}
},
async tryFetchArchivedConversation() {
if (!this.currentConversationId) {
return false;
}
try {
const response = await fetch(`/sub_agent/conversations/${this.currentConversationId}`);
if (!response.ok) {
return false;
}
const payload = await response.json();
if (!payload.success) {
return false;
}
const transformed = this.transformHistoryMessages(payload.messages || []);
this.messages = transformed;
this.$nextTick(() => this.scrollToBottom(true));
return true;
} catch (err) {
console.warn('加载归档子智能体对话失败:', err);
return false;
}
},
async fetchSubAgentFileTree() {
if (!this.isSubAgentView || !this.subAgentTaskId) {
return;
}
try {
const resp = await fetch(`/tasks/${encodeURIComponent(this.subAgentTaskId)}/files`);
if (!resp.ok) {
throw new Error(await resp.text());
}
const data = await resp.json();
if (data.success && data.data) {
this.updateFileTree(data.data);
} else {
console.warn('获取子智能体文件树失败:', data.error || data.message);
}
} catch (error) {
console.warn('加载子智能体文件树失败:', error);
}
},
async sendSubAgentMessage() {
if (!this.subAgentTaskId || this.sending) {
return;
}
const text = (this.inputMessage || '').trim();
if (!text) {
return;
}
const userMessage = {
role: 'user',
content: text,
metadata: {},
actions: [],
streamingText: '',
streamingThinking: '',
currentStreamingType: null,
timestamp: new Date().toISOString()
};
this.messages.push(userMessage);
this.currentMessageIndex = -1;
this.inputMessage = '';
this.sending = true;
this.autoScrollEnabled = true;
this.scrollToBottom(true);
try {
await fetch(`/tasks/${encodeURIComponent(this.subAgentTaskId)}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text })
});
} catch (error) {
console.warn('发送子智能体消息失败:', error);
} finally {
this.sending = false;
}
},
async stopSubAgentTask() {
if (!this.subAgentTaskId || this.stopping) {
return;
}
this.stopping = true;
this.stopRequested = true;
try {
await fetch(`/tasks/${encodeURIComponent(this.subAgentTaskId)}/stop`, { method: 'POST' });
} catch (error) {
console.warn('停止子智能体任务失败:', error);
} finally {
this.stopping = false;
this.stopRequested = false;
}
},
async terminateSubAgentTask() {
if (!this.subAgentTaskId || this.terminating) {
return;
}
const shouldTerminate = window.confirm('确定要立即关闭子智能体吗?此操作无法撤销。');
if (!shouldTerminate) {
return;
}
this.terminating = true;
try {
const resp = await fetch(`/tasks/${encodeURIComponent(this.subAgentTaskId)}/terminate`, { method: 'POST' });
let data = null;
try {
data = await resp.json();
} catch (err) {
data = { success: false, error: await resp.text() };
}
if (!resp.ok || !data.success) {
const message = (data && (data.error || data.message)) || '关闭失败';
alert(`关闭子智能体失败:${message}`);
return;
}
this.addSystemMessage('🛑 子智能体已被手动关闭。');
} catch (error) {
console.warn('关闭子智能体失败:', error);
alert(`关闭子智能体失败:${error.message || error}`);
} finally {
this.terminating = false;
}
},
formatPathDisplay(path) {
if (!path) {
return '—';
}
return path.replace(this.projectPath, '.');
},
formatSubAgentStatus(status) {
if (!status) return '未知';
const map = {
running: '进行中',
pending: '等待中',
completed: '已完成',
failed: '失败',
timeout: '超时',
terminated: '已关闭'
};
return map[status] || status;
},
async bootstrapRoute() {
if (this.isSubAgentView) {
await this.bootstrapSubAgentRoute();
return;
}
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();
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() {
if (this.scrollListenerAttached) {
return;
}
const attach = () => {
const messagesArea = this.$refs.messagesArea;
if (!messagesArea) {
if (!this.scrollListenerRetryWarned) {
console.warn('消息区域未找到,等待渲染后重试');
this.scrollListenerRetryWarned = true;
}
setTimeout(attach, 200);
return;
}
this.scrollListenerRetryWarned = false;
this.scrollListenerAttached = true;
let isProgrammaticScroll = false;
const bottomThreshold = 12;
this._setScrollingFlag = (flag) => {
isProgrammaticScroll = !!flag;
};
messagesArea.addEventListener('scroll', () => {
if (isProgrammaticScroll) {
return;
}
const scrollTop = messagesArea.scrollTop;
const scrollHeight = messagesArea.scrollHeight;
const clientHeight = messagesArea.clientHeight;
const isAtBottom = scrollHeight - scrollTop - clientHeight < bottomThreshold;
if (isAtBottom) {
this.userScrolling = false;
this.autoScrollEnabled = true;
} else {
this.userScrolling = true;
this.autoScrollEnabled = false;
}
});
};
this.$nextTick(attach);
},
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;
this.readonlyNoticeShown = false;
console.log('WebSocket已连接');
if (this.isSubAgentView) {
if (this.subAgentTaskId) {
this.socket.emit('sub_agent_join', { task_id: this.subAgentTaskId });
}
} else {
this.resetAllStates();
}
});
this.socket.on('disconnect', () => {
this.isConnected = false;
console.log('WebSocket已断开');
if (!this.isSubAgentView) {
this.resetAllStates();
}
});
this.socket.on('connect_error', (error) => {
console.error('WebSocket连接错误:', error.message);
});
this.socket.on('todo_updated', (data) => {
console.log('收到todo更新事件:', data);
if (!this.isSubAgentView && data && data.conversation_id) {
this.currentConversationId = data.conversation_id;
}
if (this.isSubAgentView && data && data.conversation_id && this.subAgentConversationId && data.conversation_id !== this.subAgentConversationId) {
return;
}
this.todoList = data && data.todo_list ? data.todo_list : null;
});
// 系统就绪
if (!this.isSubAgentView) {
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事件
// ==========================================
// 监听对话变更事件
if (!this.isSubAgentView) {
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 = '';
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}`);
}
});
} else {
this.socket.on('sub_agent_status', (data) => {
if (data && data.task_id === this.subAgentTaskId) {
this.subAgentTaskInfo.status = data.status || this.subAgentTaskInfo.status;
if (data.reason) {
this.subAgentTaskInfo.message = data.reason;
} else if (data.message) {
this.subAgentTaskInfo.message = data.message;
}
}
});
}
// 监听对话加载事件
this.socket.on('conversation_loaded', (data) => {
console.log('对话已加载:', data);
if (data.clear_ui) {
// 清理当前UI状态,准备显示历史内容
this.resetAllStates();
}
// 延迟获取并显示历史对话内容
setTimeout(() => {
this.fetchAndDisplayHistory();
}, 300);
// 延迟获取Token统计(累计+当前上下文)
setTimeout(() => {
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}`;
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;
if (lastAction.blockId) {
setTimeout(() => {
this.expandedBlocks.delete(lastAction.blockId);
this.$forceUpdate();
}, 1000);
this.$nextTick(() => this.scrollThinkingToBottom(lastAction.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 (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(() => {
}, 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.socket.on('focused_files_update', (data) => {
this.focusedFiles = data || {};
// 聚焦文件变化时更新当前上下文Token(关键修复)
if (this.currentConversationId) {
setTimeout(() => {
}, 500);
}
});
// 文件树更新
this.socket.on('file_tree_update', (data) => {
this.updateFileTree(data);
// 文件树变化时也可能影响上下文
if (this.currentConversationId) {
setTimeout(() => {
}, 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();
} 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.toolSettingsLoading = false;
this.toolSettings = [];
console.log('前端状态重置完成');
},
async loadInitialData() {
if (this.isSubAgentView) {
await this.loadSubAgentInitialData();
return;
}
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统计
} 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);
}
},
// ==========================================
// 对话管理核心功能
// ==========================================
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(() => {
}, 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 {
const response = await fetch(`/api/conversations/${this.currentConversationId}/messages`);
if (!response.ok) {
console.warn('无法获取消息历史,尝试备用方法');
const statusResponse = await fetch('/api/status');
const status = await statusResponse.json();
if (status.conversation_history && Array.isArray(status.conversation_history)) {
this.renderHistoryMessages(status.conversation_history);
}
return;
}
const payload = await response.json();
if (payload.success && payload.data && Array.isArray(payload.data.messages)) {
this.renderHistoryMessages(payload.data.messages);
} else {
console.warn('历史消息结构异常:', payload);
this.messages = [];
}
} catch (error) {
console.error('获取历史对话失败:', error);
this.messages = [];
}
},
renderHistoryMessages(historyMessages) {
const transformed = this.transformHistoryMessages(historyMessages);
this.messages = transformed;
this.$forceUpdate();
this.$nextTick(() => this.scrollToBottom());
},
transformHistoryMessages(historyMessages = []) {
if (!Array.isArray(historyMessages)) {
return [];
}
const output = [];
let currentAssistantMessage = null;
const flushAssistant = () => {
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
output.push(currentAssistantMessage);
}
currentAssistantMessage = null;
};
const ensureAssistantMessage = () => {
if (!currentAssistantMessage) {
currentAssistantMessage = {
role: 'assistant',
actions: [],
streamingThinking: '',
streamingText: '',
currentStreamingType: null
};
}
return currentAssistantMessage;
};
const parseToolArguments = (toolCall) => {
if (!toolCall || !toolCall.function) {
return {};
}
const raw = toolCall.function.arguments;
if (!raw) {
return {};
}
try {
return typeof raw === 'string' ? JSON.parse(raw || '{}') : raw;
} catch (error) {
console.warn('解析工具参数失败:', error);
return {};
}
};
const attachToolResult = (message, assistantMsg) => {
if (!assistantMsg || !assistantMsg.actions) {
return false;
}
const actions = assistantMsg.actions;
let toolAction = null;
if (message.tool_call_id) {
toolAction = actions.find(action => action.type === 'tool' && action.tool.id === message.tool_call_id);
}
if (!toolAction && message.name) {
const candidates = actions.filter(action => action.type === 'tool' && action.tool.name === message.name);
toolAction = candidates[candidates.length - 1];
}
if (!toolAction) {
return false;
}
let resultObj;
try {
resultObj = JSON.parse(message.content);
} catch (_) {
resultObj = message.content ? { output: message.content } : {};
}
toolAction.tool.status = resultObj.success === false ? 'failed' : 'completed';
toolAction.tool.result = resultObj;
if (message.name === 'append_to_file' && resultObj && typeof resultObj === 'object') {
const summary = {
path: resultObj.path || '未知文件',
success: resultObj.success !== false,
summary: resultObj.message || (resultObj.success === false ? '追加失败' : '追加完成'),
lines: resultObj.lines || 0,
bytes: resultObj.bytes || 0,
forced: !!resultObj.forced
};
assistantMsg.actions.push({
id: `history-append-${Date.now()}-${Math.random()}`,
type: 'append',
append: summary,
timestamp: Date.now()
});
}
return true;
};
const isToolResultNotice = (message) => {
if (!message || typeof message.content !== 'string') {
return false;
}
return message.content.trim().startsWith('[工具结果]');
};
const applyToolResultNotice = (message, assistantMsg) => {
if (!isToolResultNotice(message) || !assistantMsg) {
return false;
}
const trimmed = message.content.trim();
const headerMatch = trimmed.match(/\[工具结果\]\s*([^\s(]+)(?:\s*\(tool_call_id=([^)]+)\))?/);
const remainder = headerMatch ? trimmed.slice(headerMatch[0].length).trim() : '';
const synthetic = {
name: headerMatch ? headerMatch[1] : message.name,
tool_call_id: message.tool_call_id || (headerMatch ? headerMatch[2] : undefined),
content: remainder || '{}'
};
return attachToolResult(synthetic, assistantMsg);
};
historyMessages.forEach((message) => {
const role = (message.role || 'assistant').toLowerCase();
if (role === 'user') {
flushAssistant();
output.push({
role: 'user',
content: message.content || '',
metadata: message.metadata || {},
timestamp: message.timestamp
});
return;
}
if (role === 'assistant') {
const assistant = ensureAssistantMessage();
const content = message.content || '';
const metadata = message.metadata || {};
const appendPayloadMeta = metadata.append_payload;
const modifyPayloadMeta = metadata.modify_payload;
const reasoningText = (message.reasoning_content || '').trim();
if (reasoningText) {
assistant.actions.push({
id: `history-think-${Date.now()}-${Math.random()}`,
type: 'thinking',
content: reasoningText,
streaming: false,
timestamp: Date.now()
});
}
const textContent = content.trim();
if (appendPayloadMeta) {
assistant.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()
});
} else if (modifyPayloadMeta) {
assistant.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()
});
}
if (textContent && !appendPayloadMeta && !modifyPayloadMeta) {
assistant.actions.push({
id: `history-text-${Date.now()}-${Math.random()}`,
type: 'text',
content: textContent,
streaming: false,
timestamp: Date.now()
});
}
if (message.tool_calls && Array.isArray(message.tool_calls)) {
message.tool_calls.forEach((toolCall, tcIndex) => {
assistant.actions.push({
id: `history-tool-${toolCall.id || Date.now()}-${tcIndex}`,
type: 'tool',
tool: {
id: toolCall.id,
name: toolCall.function?.name,
arguments: parseToolArguments(toolCall),
status: 'preparing',
result: null
},
timestamp: Date.now()
});
});
}
return;
}
if (role === 'tool') {
if (!attachToolResult(message, currentAssistantMessage)) {
flushAssistant();
output.push({
role: 'system',
content: message.content || '',
metadata: message.metadata || {},
timestamp: message.timestamp
});
}
return;
}
if (applyToolResultNotice(message, currentAssistantMessage)) {
return;
}
flushAssistant();
output.push({
role: message.role || 'system',
content: message.content || '',
metadata: message.metadata || {},
timestamp: message.timestamp
});
});
flushAssistant();
return output;
},
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)}`);
// 重置状态
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();
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
};
},
cycleSidebarPanel() {
if (this.isSubAgentView) {
this.panelMode = this.panelMode === 'files' ? 'todo' : 'files';
if (this.panelMode === 'files') {
this.fetchSubAgentFileTree();
} else {
this.fetchTodoList();
}
return;
}
const order = ['files', 'todo', 'subAgents'];
const nextIndex = (order.indexOf(this.panelMode) + 1) % order.length;
this.panelMode = order[nextIndex];
if (this.panelMode === 'subAgents') {
this.fetchSubAgents();
}
},
formatTaskStatus(task) {
if (!task) {
return '';
}
return task.status === 'done'
? `${this.todoDoneEmoji} 完成`
: `${this.todoPendingEmoji} 未完成`;
},
toolCategoryEmoji(categoryId) {
return this.toolCategoryEmojis[categoryId] || '⚙️';
},
async fetchSubAgents() {
if (this.isSubAgentView) {
return;
}
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 (this.isSubAgentView || !agent || !agent.task_id) {
return;
}
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 base = this.getSubAgentBaseUrl();
const pathSuffix = convSegment
? `/${convSegment}+${agentLabel}`
: `/sub_agent/${agent.task_id}`;
const params = new URLSearchParams();
params.set('task_id', agent.task_id);
if (agent.conversation_id) {
params.set('conversation_id', agent.conversation_id);
}
const url = `${base}${pathSuffix}?${params.toString()}`;
window.open(url, '_blank');
},
async fetchTodoList() {
if (this.isSubAgentView) {
return;
}
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();
}
},
async sendMessage() {
if (this.isSubAgentView) {
await this.sendSubAgentMessage();
return;
}
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) {
}
}, 1000);
},
// 新增:停止任务方法
stopTask() {
if (this.isSubAgentView) {
this.stopSubAgentTask();
return;
}
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': '📖',
'ocr_image': '📸',
'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': '❗',
'create_sub_agent': '🤖',
'wait_sub_agent': '⏳'
};
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',
'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 = '';
}
}
});
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: `
📄
{{ node.name }}
{{ node.annotation }}
`
});
app.mount('#app');
console.log('Vue应用初始化完成');
}
window.addEventListener('load', () => {
bootstrapApp();
});