agent/sub_agent/static/app.js

3360 lines
145 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 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 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.trim()) {
assistant.actions.push({
id: `history-think-${Date.now()}-${Math.random()}`,
type: 'thinking',
content: allThinkingContent.trim(),
streaming: false,
timestamp: Date.now()
});
}
let textContent = content
.replace(/<think>[\s\S]*?<\/think>/g, '')
.replace(/<thinking>[\s\S]*?<\/thinking>/g, '')
.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);
}
},
openSubAgent(agent) {
if (this.isSubAgentView || !agent || !agent.task_id) {
return;
}
if (!agent || !agent.task_id) {
return;
}
const { protocol, hostname } = window.location;
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 = `${protocol}//${hostname}:8092`;
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': '📖',
'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;
// 匹配 <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();
}
},
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: `
<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();
});