// static/app-enhanced.js - 修复版,正确实现Token实时更新 function updateViewportHeightVar() { const docEl = document.documentElement; const visualViewport = window.visualViewport; if (visualViewport) { const vh = visualViewport.height; const bottomInset = Math.max( 0, (window.innerHeight || docEl.clientHeight || vh) - visualViewport.height - visualViewport.offsetTop ); docEl.style.setProperty('--app-viewport', `${vh}px`); docEl.style.setProperty('--app-bottom-inset', `${bottomInset}px`); } else { const height = window.innerHeight || docEl.clientHeight; if (height) { docEl.style.setProperty('--app-viewport', `${height}px`); } docEl.style.setProperty('--app-bottom-inset', 'env(safe-area-inset-bottom, 0px)'); } } updateViewportHeightVar(); window.addEventListener('resize', updateViewportHeightVar); window.addEventListener('orientationchange', updateViewportHeightVar); window.addEventListener('pageshow', updateViewportHeightVar); if (window.visualViewport) { window.visualViewport.addEventListener('resize', updateViewportHeightVar); window.visualViewport.addEventListener('scroll', updateViewportHeightVar); } const SOCKET_IO_CDN_SOURCES = [ 'https://cdn.socket.io/4.7.5/socket.io.min.js', 'https://cdn.jsdelivr.net/npm/socket.io-client@4.7.5/dist/socket.io.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.5/socket.io.min.js' ]; const ICONS = Object.freeze({ bot: '/static/icons/bot.svg', book: '/static/icons/book.svg', brain: '/static/icons/brain.svg', camera: '/static/icons/camera.svg', check: '/static/icons/check.svg', checkbox: '/static/icons/checkbox.svg', circleAlert: '/static/icons/circle-alert.svg', clipboard: '/static/icons/clipboard.svg', clock: '/static/icons/clock.svg', eye: '/static/icons/eye.svg', file: '/static/icons/file.svg', flag: '/static/icons/flag.svg', folder: '/static/icons/folder.svg', folderOpen: '/static/icons/folder-open.svg', globe: '/static/icons/globe.svg', hammer: '/static/icons/hammer.svg', info: '/static/icons/info.svg', laptop: '/static/icons/laptop.svg', menu: '/static/icons/menu.svg', monitor: '/static/icons/monitor.svg', octagon: '/static/icons/octagon.svg', pencil: '/static/icons/pencil.svg', python: '/static/icons/python.svg', recycle: '/static/icons/recycle.svg', save: '/static/icons/save.svg', search: '/static/icons/search.svg', settings: '/static/icons/settings.svg', sparkles: '/static/icons/sparkles.svg', stickyNote: '/static/icons/sticky-note.svg', terminal: '/static/icons/terminal.svg', trash: '/static/icons/trash.svg', triangleAlert: '/static/icons/triangle-alert.svg', user: '/static/icons/user.svg', wrench: '/static/icons/wrench.svg', x: '/static/icons/x.svg', zap: '/static/icons/zap.svg' }); const TOOL_ICON_MAP = Object.freeze({ append_to_file: 'pencil', close_sub_agent: 'octagon', create_file: 'file', create_folder: 'folder', create_sub_agent: 'bot', delete_file: 'trash', extract_webpage: 'globe', focus_file: 'eye', modify_file: 'pencil', ocr_image: 'camera', read_file: 'book', rename_file: 'pencil', run_command: 'terminal', run_python: 'python', save_webpage: 'save', sleep: 'clock', todo_create: 'stickyNote', todo_finish: 'flag', todo_finish_confirm: 'circleAlert', todo_update_task: 'check', terminal_input: 'terminal', terminal_reset: 'recycle', terminal_session: 'monitor', terminal_snapshot: 'clipboard', unfocus_file: 'eye', update_memory: 'brain', wait_sub_agent: 'clock', web_search: 'search', trigger_easter_egg: 'sparkles' }); const TOOL_CATEGORY_ICON_MAP = Object.freeze({ network: 'globe', file_edit: 'pencil', read_focus: 'eye', terminal_realtime: 'monitor', terminal_command: 'terminal', memory: 'brain', todo: 'stickyNote', sub_agent: 'bot', easter_egg: 'sparkles' }); function injectScriptSequentially(urls, onSuccess, onFailure) { let index = 0; const tryLoad = () => { if (index >= urls.length) { onFailure(); return; } const url = urls[index]; const script = document.createElement('script'); script.src = url; script.async = false; script.onload = () => { if (typeof io !== 'undefined') { console.log(`Socket.IO 已从 ${url} 加载`); onSuccess(); } else { index += 1; tryLoad(); } }; script.onerror = () => { console.warn(`无法从 ${url} 加载 Socket.IO,尝试下一个源`); index += 1; tryLoad(); }; document.head.appendChild(script); }; tryLoad(); } async function ensureSocketIOLoaded() { if (typeof io !== 'undefined') { return true; } return await new Promise((resolve) => { injectScriptSequentially( SOCKET_IO_CDN_SOURCES, () => resolve(true), () => resolve(false) ); }); } async function bootstrapApp() { // 检查必要的库是否加载 if (typeof Vue === 'undefined') { console.error('错误:Vue.js 未加载'); document.body.innerHTML = '
...
return html.replace(/]*)>([\s\S]*?)<\/code><\/pre>/g, (match, attributes, content) => {
// 提取语言
const langMatch = attributes.match(/class="[^"]*language-(\w+)/);
const language = langMatch ? langMatch[1] : 'text';
// 生成唯一ID
const blockId = `code-${Date.now()}-${counter++}`;
// 转义引号用于data属性
const escapedContent = content
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
// 构建新的HTML,保持code元素原样
return `
${language}
${content}
`;
});
},
getLanguageClass(path) {
const ext = path.split('.').pop().toLowerCase();
const langMap = {
'py': 'language-python',
'js': 'language-javascript',
'html': 'language-html',
'css': 'language-css',
'json': 'language-json',
'md': 'language-markdown',
'txt': 'language-plain'
};
return langMap[ext] || 'language-plain';
},
scrollToBottom() {
setTimeout(() => {
const messagesArea = this.$refs.messagesArea;
if (messagesArea) {
// 标记为程序触发的滚动
if (this._setScrollingFlag) {
this._setScrollingFlag(true);
}
messagesArea.scrollTop = messagesArea.scrollHeight;
// 滚动完成后重置标记
setTimeout(() => {
if (this._setScrollingFlag) {
this._setScrollingFlag(false);
}
}, 100);
}
}, 50);
},
conditionalScrollToBottom() {
// 严格检查:只在明确允许时才滚动
if (this.autoScrollEnabled === true && this.userScrolling === false) {
this.scrollToBottom();
}
},
toggleScrollLock() {
const currentlyLocked = this.autoScrollEnabled && !this.userScrolling;
if (currentlyLocked) {
this.autoScrollEnabled = false;
this.userScrolling = true;
} else {
this.autoScrollEnabled = true;
this.userScrolling = false;
this.scrollToBottom();
}
},
scrollThinkingToBottom(blockId) {
if (!this.thinkingScrollLocks.get(blockId)) return;
const refName = `thinkingContent-${blockId}`;
const elRef = this.$refs[refName];
const el = Array.isArray(elRef) ? elRef[0] : elRef;
if (el) {
el.scrollTop = el.scrollHeight;
}
},
handleThinkingScroll(blockId, event) {
const el = event.target;
const threshold = 12;
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
this.thinkingScrollLocks.set(blockId, atBottom);
},
// 面板调整方法
startResize(panel, event) {
this.isResizing = true;
this.resizingPanel = panel;
if (panel === 'right' && this.rightCollapsed) {
this.rightCollapsed = false;
if (this.rightWidth < this.minPanelWidth) {
this.rightWidth = this.minPanelWidth;
}
}
document.addEventListener('mousemove', this.handleResize);
document.addEventListener('mouseup', this.stopResize);
document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize';
event.preventDefault();
},
handleResize(event) {
if (!this.isResizing) return;
const containerWidth = document.querySelector('.main-container').offsetWidth;
if (this.resizingPanel === 'left') {
let newWidth = event.clientX - (this.sidebarCollapsed ? 60 : 300);
newWidth = Math.max(this.minPanelWidth, Math.min(newWidth, this.maxPanelWidth));
this.leftWidth = newWidth;
} else if (this.resizingPanel === 'right') {
let newWidth = containerWidth - event.clientX;
newWidth = Math.max(this.minPanelWidth, Math.min(newWidth, this.maxPanelWidth));
this.rightWidth = newWidth;
} else if (this.resizingPanel === 'conversation') {
// 对话侧边栏宽度调整
let newWidth = event.clientX;
newWidth = Math.max(200, Math.min(newWidth, 400));
// 这里可以动态调整对话侧边栏宽度,暂时不实现
}
},
stopResize() {
this.isResizing = false;
this.resizingPanel = null;
document.removeEventListener('mousemove', this.handleResize);
document.removeEventListener('mouseup', this.stopResize);
document.body.style.userSelect = '';
document.body.style.cursor = '';
},
async handleEasterEggPayload(payload) {
if (!payload) {
return;
}
let parsed = payload;
if (typeof payload === 'string') {
try {
parsed = JSON.parse(payload);
} catch (error) {
console.warn('无法解析彩蛋结果:', payload);
return;
}
}
if (!parsed || typeof parsed !== 'object') {
return;
}
if (!parsed.success) {
if (parsed.error) {
console.warn('彩蛋触发失败:', parsed.error);
}
await this.destroyEasterEggEffect(true);
return;
}
const effectName = (parsed.effect || '').toLowerCase();
if (!effectName) {
console.warn('彩蛋结果缺少 effect 字段');
return;
}
await this.startEasterEggEffect(effectName, parsed);
},
async startEasterEggEffect(effectName, payload = {}) {
const registry = window.EasterEggRegistry;
if (!registry) {
console.warn('EasterEggRegistry 尚未加载,无法播放彩蛋');
return;
}
if (!registry.has(effectName)) {
console.warn('未注册的彩蛋 effect:', effectName);
await this.destroyEasterEggEffect(true);
return;
}
const root = this.$refs.easterEggRoot;
if (!root) {
console.warn('未找到彩蛋根节点');
return;
}
await this.destroyEasterEggEffect(true);
this.easterEgg.active = true;
this.easterEgg.effect = effectName;
this.easterEgg.payload = payload;
const instance = registry.start(effectName, {
root,
payload,
app: this
});
if (!instance) {
this.finishEasterEggCleanup();
return;
}
this.easterEgg.instance = instance;
this.easterEgg.destroyPromise = null;
this.easterEgg.destroying = false;
if (this.easterEgg.cleanupTimer) {
clearTimeout(this.easterEgg.cleanupTimer);
}
const durationSeconds = Math.max(8, Number(payload.duration_seconds) || 45);
this.easterEgg.cleanupTimer = setTimeout(() => {
const cleanup = this.destroyEasterEggEffect(false);
if (cleanup && typeof cleanup.catch === 'function') {
cleanup.catch(() => {});
}
}, durationSeconds * 1000);
if (payload.message) {
console.info(`[彩蛋] ${payload.display_name || effectName}: ${payload.message}`);
}
},
destroyEasterEggEffect(forceImmediate = false) {
if (this.easterEgg.cleanupTimer) {
clearTimeout(this.easterEgg.cleanupTimer);
this.easterEgg.cleanupTimer = null;
}
const instance = this.easterEgg.instance;
if (!instance) {
this.finishEasterEggCleanup();
return Promise.resolve();
}
if (this.easterEgg.destroying) {
return this.easterEgg.destroyPromise || Promise.resolve();
}
this.easterEgg.destroying = true;
let result;
try {
result = instance.destroy({
immediate: forceImmediate,
payload: this.easterEgg.payload,
root: this.$refs.easterEggRoot || null
});
} catch (error) {
console.warn('销毁彩蛋时发生错误:', error);
this.easterEgg.destroying = false;
this.finishEasterEggCleanup();
return Promise.resolve();
}
const finalize = () => {
this.easterEgg.destroyPromise = null;
this.easterEgg.destroying = false;
this.finishEasterEggCleanup();
};
if (result && typeof result.then === 'function') {
this.easterEgg.destroyPromise = result.then(() => {
finalize();
}).catch((error) => {
console.warn('彩蛋清理失败:', error);
finalize();
});
return this.easterEgg.destroyPromise;
} else {
finalize();
return Promise.resolve();
}
},
finishEasterEggCleanup() {
if (this.easterEgg.cleanupTimer) {
clearTimeout(this.easterEgg.cleanupTimer);
this.easterEgg.cleanupTimer = null;
}
const root = this.$refs.easterEggRoot;
if (root) {
root.innerHTML = '';
}
this.easterEgg.active = false;
this.easterEgg.effect = null;
this.easterEgg.payload = null;
this.easterEgg.instance = null;
this.easterEgg.destroyPromise = null;
this.easterEgg.destroying = false;
},
// 格式化token显示(修复NaN问题)
formatTokenCount(tokens) {
// 确保tokens是数字,防止NaN
const num = Number(tokens) || 0;
if (num < 1000) {
return num.toString();
} else if (num < 1000000) {
return (num / 1000).toFixed(1) + 'K';
} else {
return (num / 1000000).toFixed(1) + 'M';
}
},
formatBytes(bytes) {
if (bytes === null || bytes === undefined) {
return '—';
}
const value = Number(bytes);
if (!Number.isFinite(value)) {
return '—';
}
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let display = value;
let unitIndex = 0;
while (display >= 1024 && unitIndex < units.length - 1) {
display /= 1024;
unitIndex++;
}
const decimals = display >= 10 || unitIndex === 0 ? 0 : 1;
return `${display.toFixed(decimals)} ${units[unitIndex]}`;
},
formatPercentage(value) {
if (typeof value !== 'number' || Number.isNaN(value)) {
return '—';
}
return `${value.toFixed(1)}%`;
},
formatRate(bytesPerSecond) {
if (bytesPerSecond === null || bytesPerSecond === undefined) {
return '—';
}
const value = Number(bytesPerSecond);
if (!Number.isFinite(value)) {
return '—';
}
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
let display = value;
let unitIndex = 0;
while (display >= 1024 && unitIndex < units.length - 1) {
display /= 1024;
unitIndex++;
}
const decimals = display >= 10 || unitIndex === 0 ? 0 : 1;
return `${display.toFixed(decimals)} ${units[unitIndex]}`;
},
hasContainerStats() {
return !!(this.containerStatus && this.containerStatus.mode === 'docker' && this.containerStatus.stats);
},
quotaTypeLabel(type) {
switch (type) {
case 'thinking':
return '思考模型';
case 'search':
return '搜索';
default:
return '常规模型';
}
},
formatResetTime(iso) {
if (!iso) {
return '--:--';
}
const date = new Date(iso);
if (Number.isNaN(date.getTime())) {
return '--:--';
}
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
},
formatQuotaValue(entry) {
if (!entry) {
return '--';
}
const count = Number(entry.count) || 0;
const limit = Number(entry.limit) || 0;
if (!limit) {
return `${count}`;
}
return `${count} / ${limit}`;
},
quotaResetSummary() {
const parts = [];
['fast', 'thinking', 'search'].forEach((type) => {
const entry = this.usageQuota && this.usageQuota[type];
if (entry && entry.reset_at && Number(entry.count || 0) > 0) {
parts.push(`${this.quotaTypeLabel(type)} ${this.formatResetTime(entry.reset_at)}`);
}
});
return parts.join(' · ');
},
isQuotaExceeded(type) {
const entry = this.usageQuota && this.usageQuota[type];
if (!entry) {
return false;
}
const limit = Number(entry.limit) || 0;
if (!limit) {
return false;
}
return Number(entry.count || 0) >= limit;
},
showQuotaToast(payload) {
if (!payload) {
return;
}
const type = payload.type || 'fast';
const resetAt = payload.reset_at || (this.usageQuota[type] && this.usageQuota[type].reset_at);
const label = this.quotaTypeLabel(type);
const timeText = this.formatResetTime(resetAt);
this.quotaToast = {
message: `${label} 配额已用完,将在 ${timeText} 重置`,
type
};
if (this.quotaToastTimer) {
clearTimeout(this.quotaToastTimer);
}
this.quotaToastTimer = setTimeout(() => {
this.quotaToast = null;
this.quotaToastTimer = null;
}, 5000);
},
containerStatusText() {
if (!this.containerStatus) {
return '未知';
}
if (this.containerStatus.mode !== 'docker') {
return '宿主机模式';
}
const state = this.containerStatus.state;
if (state && state.status) {
return state.status;
}
return (state && state.running === false) ? '已停止' : '运行中';
},
containerStatusClass() {
if (!this.containerStatus) {
return {};
}
if (this.containerStatus.mode !== 'docker') {
return { 'status-pill--host': true };
}
const stopped = this.containerStatus.state && this.containerStatus.state.running === false;
return {
'status-pill--running': !stopped,
'status-pill--stopped': stopped
};
}
}
});
app.component('file-node', {
name: 'FileNode',
props: {
node: {
type: Object,
required: true
},
level: {
type: Number,
default: 0
},
expandedFolders: {
type: Object,
required: true
}
},
emits: ['toggle-folder', 'context-menu'],
computed: {
isExpanded() {
if (this.node.type !== 'folder') {
return false;
}
const value = this.expandedFolders[this.node.path];
return value === undefined ? true : value;
},
folderPadding() {
return {
paddingLeft: `${12 + this.level * 16}px`
};
},
filePadding() {
return {
paddingLeft: `${40 + this.level * 16}px`
};
}
},
methods: {
iconStyle(iconKey) {
const iconPath = ICONS[iconKey];
return iconPath ? { '--icon-src': `url(${iconPath})` } : {};
},
toggle() {
if (this.node.type === 'folder') {
this.$emit('toggle-folder', this.node.path);
}
}
},
template: `
{{ node.name }}
{{ node.annotation }}
`
});
app.mount('#app');
console.log('Vue应用初始化完成');
}
window.addEventListener('load', () => {
bootstrapApp();
});