3360 lines
145 KiB
JavaScript
3360 lines
145 KiB
JavaScript
// 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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
|
||
// 构建新的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();
|
||
});
|