diff --git a/ccw/src/core/dashboard-generator.ts b/ccw/src/core/dashboard-generator.ts index 103add03..ddc31e99 100644 --- a/ccw/src/core/dashboard-generator.ts +++ b/ccw/src/core/dashboard-generator.ts @@ -106,6 +106,7 @@ const MODULE_FILES = [ 'i18n.js', // Must be loaded first for translations 'utils.js', 'state.js', + 'services.js', // CacheManager, EventManager, PreloadService - must be before main.js 'api.js', 'components/theme.js', 'components/modals.js', diff --git a/ccw/src/templates/dashboard-js/main.js b/ccw/src/templates/dashboard-js/main.js index bb2d0759..cbe6c478 100644 --- a/ccw/src/templates/dashboard-js/main.js +++ b/ccw/src/templates/dashboard-js/main.js @@ -5,6 +5,9 @@ document.addEventListener('DOMContentLoaded', async () => { // Initialize Lucide icons (must be first to render SVG icons) try { lucide.createIcons(); } catch (e) { console.error('Lucide icons init failed:', e); } + // Initialize preload services (must be early to start data fetching) + try { initPreloadServices(); } catch (e) { console.error('Preload services init failed:', e); } + // Initialize i18n (must be early to translate static content) try { initI18n(); } catch (e) { console.error('I18n init failed:', e); } @@ -90,3 +93,49 @@ document.addEventListener('DOMContentLoaded', async () => { } }); }); + +/** + * 初始化预加载服务 + * 创建缓存管理器、事件管理器和预加载服务,并注册数据源 + */ +function initPreloadServices() { + // 初始化服务实例 + window.cacheManager = new CacheManager('ccw.cache.'); + window.eventManager = new EventManager(); + window.preloadService = new PreloadService(window.cacheManager, window.eventManager); + + // 注册高优先级数据源(页面进入时立即预加载) + window.preloadService.register('dashboard-init', + () => fetch('/api/codexlens/dashboard-init').then(r => r.ok ? r.json() : Promise.reject(r)), + { isHighPriority: true, ttl: 300000 } // 5分钟 + ); + + window.preloadService.register('workspace-status', + () => { + const path = encodeURIComponent(projectPath || ''); + return fetch('/api/codexlens/workspace-status?path=' + path).then(r => r.ok ? r.json() : Promise.reject(r)); + }, + { isHighPriority: true, ttl: 120000 } // 2分钟 + ); + + window.preloadService.register('cli-status', + () => fetch('/api/cli/status').then(r => r.ok ? r.json() : Promise.reject(r)), + { isHighPriority: true, ttl: 300000 } // 5分钟 + ); + + // 注册中优先级数据源 + window.preloadService.register('codexlens-models', + () => fetch('/api/codexlens/models').then(r => r.ok ? r.json() : Promise.reject(r)), + { isHighPriority: false, ttl: 600000 } // 10分钟 + ); + + window.preloadService.register('cli-config', + () => fetch('/api/cli/config').then(r => r.ok ? r.json() : Promise.reject(r)), + { isHighPriority: false, ttl: 300000 } // 5分钟 + ); + + // 立即触发高优先级预加载(静默后台执行) + window.preloadService.runInitialPreload(); + + console.log('[Preload] Services initialized, high-priority preload started'); +} diff --git a/ccw/src/templates/dashboard-js/services.js b/ccw/src/templates/dashboard-js/services.js new file mode 100644 index 00000000..1cceeede --- /dev/null +++ b/ccw/src/templates/dashboard-js/services.js @@ -0,0 +1,289 @@ +// ======================================== +// Services Module +// ======================================== +// 核心服务类:缓存管理、事件管理、预加载服务 + +// ========== CacheManager ========== +/** + * 基于 sessionStorage 的缓存管理器 + * 支持 TTL 过期机制 + */ +class CacheManager { + /** + * 创建缓存管理器实例 + * @param {string} prefix - 缓存键前缀 + * @param {Storage} storage - 存储对象(默认 sessionStorage) + */ + constructor(prefix = 'ccw.cache.', storage = sessionStorage) { + this.prefix = prefix; + this.storage = storage; + } + + /** + * 获取缓存数据 + * @param {string} key - 缓存键 + * @returns {*} 缓存数据,过期或不存在返回 null + */ + get(key) { + try { + const fullKey = this.prefix + key; + const raw = this.storage.getItem(fullKey); + if (!raw) return null; + + const cached = JSON.parse(raw); + // 检查是否过期 + if (cached.timestamp && Date.now() > cached.timestamp) { + this.storage.removeItem(fullKey); + return null; + } + + return cached.data; + } catch (e) { + console.error('[CacheManager] 获取缓存失败:', e); + return null; + } + } + + /** + * 设置缓存数据 + * @param {string} key - 缓存键 + * @param {*} data - 要缓存的数据 + * @param {number} ttl - 过期时间(毫秒),默认 3 分钟 + */ + set(key, data, ttl = 180000) { + try { + const fullKey = this.prefix + key; + const cached = { + data: data, + timestamp: Date.now() + ttl + }; + this.storage.setItem(fullKey, JSON.stringify(cached)); + } catch (e) { + console.error('[CacheManager] 设置缓存失败:', e); + } + } + + /** + * 检查缓存是否有效 + * @param {string} key - 缓存键 + * @returns {boolean} 缓存是否存在且未过期 + */ + isValid(key) { + try { + const fullKey = this.prefix + key; + const raw = this.storage.getItem(fullKey); + if (!raw) return false; + + const cached = JSON.parse(raw); + if (cached.timestamp && Date.now() > cached.timestamp) { + return false; + } + return true; + } catch (e) { + return false; + } + } + + /** + * 使单个缓存失效 + * @param {string} key - 缓存键 + */ + invalidate(key) { + try { + const fullKey = this.prefix + key; + this.storage.removeItem(fullKey); + } catch (e) { + console.error('[CacheManager] 使缓存失效失败:', e); + } + } + + /** + * 清除所有带前缀的缓存 + */ + invalidateAll() { + try { + const keysToRemove = []; + for (let i = 0; i < this.storage.length; i++) { + const key = this.storage.key(i); + if (key && key.startsWith(this.prefix)) { + keysToRemove.push(key); + } + } + keysToRemove.forEach(key => this.storage.removeItem(key)); + } catch (e) { + console.error('[CacheManager] 清除所有缓存失败:', e); + } + } +} + + +// ========== EventManager ========== +/** + * 简单的发布/订阅事件管理器 + */ +class EventManager { + /** + * 创建事件管理器实例 + */ + constructor() { + this.events = {}; + } + + /** + * 订阅事件 + * @param {string} eventName - 事件名称 + * @param {Function} fn - 回调函数 + */ + on(eventName, fn) { + if (!this.events[eventName]) { + this.events[eventName] = []; + } + this.events[eventName].push(fn); + } + + /** + * 取消订阅事件 + * @param {string} eventName - 事件名称 + * @param {Function} fn - 要移除的回调函数 + */ + off(eventName, fn) { + if (!this.events[eventName]) return; + this.events[eventName] = this.events[eventName].filter(f => f !== fn); + } + + /** + * 触发事件 + * @param {string} eventName - 事件名称 + * @param {*} data - 传递给回调的数据 + */ + emit(eventName, data) { + if (!this.events[eventName]) return; + this.events[eventName].forEach(fn => { + try { + fn(data); + } catch (e) { + console.error('[EventManager] 事件回调执行失败:', eventName, e); + } + }); + } +} + + +// ========== PreloadService ========== +/** + * 预加载服务 + * 管理数据源注册和预加载,防止重复请求 + */ +class PreloadService { + /** + * 创建预加载服务实例 + * @param {CacheManager} cacheManager - 缓存管理器实例 + * @param {EventManager} eventManager - 事件管理器实例 + */ + constructor(cacheManager, eventManager) { + this.cacheManager = cacheManager; + this.eventManager = eventManager; + // 已注册的数据源 + this.sources = new Map(); + // 进行中的请求 Promise,防止重复请求 + this.inFlight = new Map(); + } + + /** + * 注册数据源 + * @param {string} key - 数据源标识 + * @param {Function} fetchFn - 获取数据的异步函数 + * @param {Object} options - 配置选项 + * @param {boolean} options.isHighPriority - 是否高优先级(初始预加载) + * @param {number} options.ttl - 缓存 TTL(毫秒) + */ + register(key, fetchFn, options = {}) { + this.sources.set(key, { + fetchFn: fetchFn, + isHighPriority: options.isHighPriority || false, + ttl: options.ttl || 180000 + }); + } + + /** + * 预加载指定数据源 + * @param {string} key - 数据源标识 + * @param {Object} options - 预加载选项 + * @param {boolean} options.force - 是否强制刷新(忽略缓存) + * @returns {Promise<*>} 预加载的数据 + */ + async preload(key, options = {}) { + const source = this.sources.get(key); + if (!source) { + console.warn('[PreloadService] 未找到数据源:', key); + return null; + } + + // 检查缓存(非强制刷新时) + if (!options.force && this.cacheManager.isValid(key)) { + return this.cacheManager.get(key); + } + + // 检查是否有进行中的请求 + if (this.inFlight.has(key)) { + return this.inFlight.get(key); + } + + // 创建新请求 + const requestPromise = this._doFetch(key, source); + this.inFlight.set(key, requestPromise); + + try { + const result = await requestPromise; + return result; + } finally { + // 请求完成后移除 + this.inFlight.delete(key); + } + } + + /** + * 执行实际的数据获取 + * @private + */ + async _doFetch(key, source) { + try { + const data = await source.fetchFn(); + // 存入缓存 + this.cacheManager.set(key, data, source.ttl); + // 发出更新事件 + this.eventManager.emit('data:updated:' + key, data); + this.eventManager.emit('data:updated', { key: key, data: data }); + return data; + } catch (e) { + console.error('[PreloadService] 预加载失败:', key, e); + // 发出错误事件 + this.eventManager.emit('data:error:' + key, e); + throw e; + } + } + + /** + * 运行所有高优先级预加载 + * @returns {Promise} + */ + async runInitialPreload() { + const highPriorityKeys = []; + this.sources.forEach((source, key) => { + if (source.isHighPriority) { + highPriorityKeys.push(key); + } + }); + + // 并行执行所有高优先级预加载 + await Promise.allSettled( + highPriorityKeys.map(key => this.preload(key)) + ); + } +} + + +// ========== 导出到 window ========== +window.CacheManager = CacheManager; +window.EventManager = EventManager; +window.PreloadService = PreloadService; diff --git a/ccw/src/templates/dashboard-js/views/cli-manager.js b/ccw/src/templates/dashboard-js/views/cli-manager.js index 31d0c0ac..b446e0ab 100644 --- a/ccw/src/templates/dashboard-js/views/cli-manager.js +++ b/ccw/src/templates/dashboard-js/views/cli-manager.js @@ -10,6 +10,69 @@ var cliWrapperEndpoints = []; // CLI封装 endpoints from /api/cli/settings var cliToolConfig = null; // Store loaded CLI config var predefinedModels = {}; // Store predefined models per tool +// ========== Cache Key Mapping ========== +// 缓存键映射(旧键名 -> 新键名) +var CLI_CACHE_KEY_MAP = { + toolConfig: 'cli-config', + toolStatus: 'cli-status', + installations: 'cli-installations', + endpointTools: 'cli-endpoint-tools', + litellmEndpoints: 'cli-litellm-endpoints', + customEndpoints: 'cli-custom-endpoints', + wrapperEndpoints: 'cli-wrapper-endpoints' +}; + +// ========== CLI Cache Bridge ========== + +/** + * 获取 CLI 缓存数据 + * @param {string} key - 缓存键 + * @returns {*} 缓存的数据或 null + */ +function getCliCachedData(key) { + if (!window.cacheManager) return null; + var newKey = CLI_CACHE_KEY_MAP[key] || key; + return window.cacheManager.get(newKey); +} + +/** + * 设置 CLI 缓存数据 + * @param {string} key - 缓存键 + * @param {*} data - 要缓存的数据 + * @param {number} ttl - 缓存 TTL(毫秒),默认 5 分钟 + */ +function setCliCacheData(key, data, ttl) { + if (!window.cacheManager) return; + ttl = ttl || 300000; + var newKey = CLI_CACHE_KEY_MAP[key] || key; + window.cacheManager.set(newKey, data, ttl); +} + +/** + * 注册 CLI 相关数据源到预加载服务 + * 仅在数据源尚未注册时添加 + */ +function registerCliDataSources() { + if (!window.preloadService) return; + + var sources = [ + { key: 'cli-installations', url: '/api/ccw/installations', priority: false, ttl: 300000 }, + { key: 'cli-endpoint-tools', url: '/api/ccw/tools', priority: false, ttl: 300000 }, + { key: 'cli-litellm-endpoints', url: '/api/litellm-api/config', priority: false, ttl: 300000 }, + { key: 'cli-custom-endpoints', url: '/api/cli/endpoints', priority: false, ttl: 300000 }, + { key: 'cli-wrapper-endpoints', url: '/api/cli/settings', priority: false, ttl: 300000 } + ]; + + sources.forEach(function(src) { + if (!window.preloadService.sources.has(src.key)) { + window.preloadService.register(src.key, + function() { return fetch(src.url).then(function(r) { return r.ok ? r.json() : Promise.reject(r); }); }, + { isHighPriority: src.priority, ttl: src.ttl } + ); + } + }); +} + // ========== CSRF Token Management ========== var csrfToken = null; // Store CSRF token for state-changing requests @@ -290,12 +353,23 @@ window.syncEndpointToCliTools = syncEndpointToCliTools; // ========== CLI Tool Configuration ========== async function loadCliToolConfig() { + // 尝试从缓存获取 + var cached = getCliCachedData('toolConfig'); + if (cached) { + cliToolConfig = cached.config || null; + predefinedModels = cached.predefinedModels || {}; + return cached; + } + try { var response = await fetch('/api/cli/config'); if (!response.ok) throw new Error('Failed to load CLI config'); var data = await response.json(); cliToolConfig = data.config || null; predefinedModels = data.predefinedModels || {}; + + // 缓存结果 + setCliCacheData('toolConfig', data); return data; } catch (err) { console.error('Failed to load CLI config:', err); @@ -650,29 +724,50 @@ function initToolConfigModalEvents(tool, currentConfig, models) { } // ========== Rendering ========== -async function renderCliManager() { - var container = document.getElementById('mainContent'); - if (!container) return; - // Hide stats grid and search for CLI view - var statsGrid = document.getElementById('statsGrid'); - var searchInput = document.getElementById('searchInput'); - if (statsGrid) statsGrid.style.display = 'none'; - if (searchInput) searchInput.parentElement.style.display = 'none'; - - // Load data (including CodexLens status for tools section) - // loadCliToolsConfig() ensures cli-tools.json is auto-created if missing - await Promise.all([ - loadCliToolsConfig(), - loadCliToolStatus(), - loadCodexLensStatus(), - loadCcwInstallations(), - loadCcwEndpointTools(), - loadLitellmApiEndpoints(), - loadCliCustomEndpoints(), - loadCliWrapperEndpoints() - ]); +/** + * 构建 CLI Manager 骨架屏 + * @returns {string} HTML 字符串 + */ +function buildCliManagerSkeleton() { + return '
' + + '
' + + '

' + (t('nav.cliManager') || 'CLI Status') + '

' + + '
' + + '
' + + // 左侧 Tools 区域骨架 + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + // 右侧 CCW 区域骨架 + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + // 底部区域骨架 + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
'; +} +/** + * 渲染 CLI Manager 实际内容(内部容器结构 + 各子面板) + * @param {HTMLElement} container - 主容器元素 + */ +function renderCliManagerContent(container) { container.innerHTML = '
' + '
' + '
' + @@ -684,23 +779,87 @@ async function renderCliManager() { '
' + '
'; - // Render sub-panels + // 渲染子面板 renderToolsSection(); renderCcwSection(); renderLanguageSettingsSection(); renderCliSettingsSection(); renderCcwEndpointToolsSection(); - // Initialize storage manager card + // 初始化存储管理器卡片 if (typeof initStorageManager === 'function') { initStorageManager(); } - // Initialize Lucide icons + // 初始化 Lucide 图标 + if (window.lucide) lucide.createIcons(); +} + +async function renderCliManager() { + var container = document.getElementById('mainContent'); + if (!container) return; + + // 隐藏统计网格和搜索框 + var statsGrid = document.getElementById('statsGrid'); + var searchInput = document.getElementById('searchInput'); + if (statsGrid) statsGrid.style.display = 'none'; + if (searchInput) searchInput.parentElement.style.display = 'none'; + + // 注册数据源(如果尚未注册) + registerCliDataSources(); + + // 1. 立即显示骨架屏 + container.innerHTML = buildCliManagerSkeleton(); if (window.lucide) lucide.createIcons(); - // Sync active executions to restore running state - await syncActiveExecutions(); + // 2. 尝试从缓存渲染(快速展示) + var cachedConfig = getCliCachedData('toolConfig'); + var cachedStatus = getCliCachedData('toolStatus'); + var hasCachedData = cachedConfig && cachedStatus; + + if (hasCachedData) { + // 应用缓存数据 + cliToolConfig = cachedConfig.config; + predefinedModels = cachedConfig.predefinedModels || {}; + // 立即渲染缓存数据 + renderCliManagerContent(container); + console.log('[CLI Manager] Rendered from cache'); + } + + // 3. 后台加载最新数据 + try { + await Promise.all([ + loadCliToolsConfig(), + loadCliToolStatus(), + loadCodexLensStatus(), + loadCcwInstallations(), + loadCcwEndpointTools(), + loadLitellmApiEndpoints(), + loadCliCustomEndpoints(), + loadCliWrapperEndpoints() + ]); + + // 4. 用最新数据更新 UI(如果之前未渲染或数据有变化) + renderCliManagerContent(container); + console.log('[CLI Manager] Rendered with fresh data'); + } catch (err) { + console.error('[CLI Manager] Failed to load data:', err); + // 如果没有缓存数据且加载失败,显示错误提示 + if (!hasCachedData) { + container.innerHTML = '
' + + '' + + '

' + (t('common.loadFailed') || 'Failed to load data') + '

' + + '' + + '
'; + if (window.lucide) lucide.createIcons(); + } + } + + // 同步活动执行 + syncActiveExecutions(); } // ========== Helper Functions ========== diff --git a/ccw/src/templates/dashboard-js/views/codexlens-manager.js b/ccw/src/templates/dashboard-js/views/codexlens-manager.js index c87faab2..90d11962 100644 --- a/ccw/src/templates/dashboard-js/views/codexlens-manager.js +++ b/ccw/src/templates/dashboard-js/views/codexlens-manager.js @@ -2,124 +2,107 @@ // Extracted from cli-manager.js for better maintainability // ============================================================ -// CACHE MANAGEMENT +// CACHE BRIDGE - 使用全局 PreloadService // ============================================================ -// Cache TTL in milliseconds (30 seconds default) -const CODEXLENS_CACHE_TTL = 30000; - -// Cache storage for CodexLens data -const codexLensCache = { - workspaceStatus: { data: null, timestamp: 0 }, - config: { data: null, timestamp: 0 }, - rerankerConfig: { data: null, timestamp: 0 }, - status: { data: null, timestamp: 0 }, - env: { data: null, timestamp: 0 }, - models: { data: null, timestamp: 0 }, - rerankerModels: { data: null, timestamp: 0 }, - semanticStatus: { data: null, timestamp: 0 }, - gpuList: { data: null, timestamp: 0 }, - indexes: { data: null, timestamp: 0 } +// 缓存键映射(旧键名 -> 新键名) +const CACHE_KEY_MAP = { + workspaceStatus: 'workspace-status', + config: 'codexlens-config', + rerankerConfig: 'codexlens-reranker-config', + status: 'codexlens-status', + env: 'codexlens-env', + models: 'codexlens-models', + rerankerModels: 'codexlens-reranker-models', + semanticStatus: 'codexlens-semantic-status', + gpuList: 'codexlens-gpu-list', + indexes: 'codexlens-indexes' }; /** - * Check if cache is valid (not expired) - * @param {string} key - Cache key - * @param {number} ttl - Optional custom TTL + * 兼容性函数:检查缓存是否有效 + * @param {string} key - 旧缓存键 * @returns {boolean} */ -function isCacheValid(key, ttl = CODEXLENS_CACHE_TTL) { - const cache = codexLensCache[key]; - if (!cache || !cache.data) return false; - return (Date.now() - cache.timestamp) < ttl; +function isCacheValid(key) { + if (!window.cacheManager) return false; + const newKey = CACHE_KEY_MAP[key] || key; + return window.cacheManager.isValid(newKey); } /** - * Get cached data - * @param {string} key - Cache key - * @returns {*} Cached data or null + * 兼容性函数:获取缓存数据 + * @param {string} key - 旧缓存键 + * @returns {*} */ function getCachedData(key) { - return codexLensCache[key]?.data || null; + if (!window.cacheManager) return null; + const newKey = CACHE_KEY_MAP[key] || key; + return window.cacheManager.get(newKey); } /** - * Set cache data - * @param {string} key - Cache key - * @param {*} data - Data to cache + * 兼容性函数:设置缓存数据 + * @param {string} key - 旧缓存键 + * @param {*} data - 数据 + * @param {number} ttl - 可选的 TTL */ -function setCacheData(key, data) { - if (codexLensCache[key]) { - codexLensCache[key].data = data; - codexLensCache[key].timestamp = Date.now(); - } +function setCacheData(key, data, ttl = 180000) { + if (!window.cacheManager) return; + const newKey = CACHE_KEY_MAP[key] || key; + window.cacheManager.set(newKey, data, ttl); } /** - * Invalidate specific cache or all caches - * @param {string} key - Cache key (optional, if not provided clears all) + * 兼容性函数:使缓存失效 + * @param {string} key - 旧缓存键(可选,不提供则清除所有) */ function invalidateCache(key) { - if (key && codexLensCache[key]) { - codexLensCache[key].data = null; - codexLensCache[key].timestamp = 0; - } else if (!key) { - Object.keys(codexLensCache).forEach(function(k) { - codexLensCache[k].data = null; - codexLensCache[k].timestamp = 0; + if (!window.cacheManager) return; + if (key) { + const newKey = CACHE_KEY_MAP[key] || key; + window.cacheManager.invalidate(newKey); + } else { + // 清除所有 codexlens 相关缓存 + Object.values(CACHE_KEY_MAP).forEach(function(k) { + window.cacheManager.invalidate(k); }); } } -// Preload promises for tracking in-flight requests -var codexLensPreloadPromises = {}; - /** - * Preload CodexLens data in the background - * Called immediately when entering the CodexLens page + * 预加载 CodexLens 数据 + * 现在委托给全局 PreloadService * @returns {Promise} */ async function preloadCodexLensData() { - console.log('[CodexLens] Starting preload...'); - - // Skip if already preloading or cache is valid - if (codexLensPreloadPromises.inProgress) { - console.log('[CodexLens] Preload already in progress, skipping'); - return codexLensPreloadPromises.inProgress; + console.log('[CodexLens] Preload delegated to PreloadService'); + if (!window.preloadService) { + console.warn('[CodexLens] PreloadService not available'); + return; } - // Check if all caches are valid - var allCacheValid = isCacheValid('config') && isCacheValid('models') && - isCacheValid('rerankerConfig') && isCacheValid('rerankerModels') && - isCacheValid('semanticStatus') && isCacheValid('env'); - if (allCacheValid) { - console.log('[CodexLens] All caches valid, skipping preload'); - return Promise.resolve(); - } + // 注册额外的数据源(如果尚未注册) + const additionalSources = [ + { key: 'codexlens-config', url: '/api/codexlens/config', priority: true, ttl: 300000 }, + { key: 'codexlens-reranker-config', url: '/api/codexlens/reranker/config', priority: false, ttl: 300000 }, + { key: 'codexlens-reranker-models', url: '/api/codexlens/reranker/models', priority: false, ttl: 600000 }, + { key: 'codexlens-semantic-status', url: '/api/codexlens/semantic/status', priority: false, ttl: 300000 }, + { key: 'codexlens-env', url: '/api/codexlens/env', priority: false, ttl: 300000 } + ]; - // Start preloading all endpoints in parallel - codexLensPreloadPromises.inProgress = Promise.all([ - // Config and models - !isCacheValid('config') ? fetch('/api/codexlens/config').then(r => r.json()).then(d => setCacheData('config', d)) : Promise.resolve(), - !isCacheValid('models') ? fetch('/api/codexlens/models').then(r => r.json()).then(d => setCacheData('models', d)) : Promise.resolve(), - // Reranker config and models - !isCacheValid('rerankerConfig') ? fetch('/api/codexlens/reranker/config').then(r => r.json()).then(d => setCacheData('rerankerConfig', d)) : Promise.resolve(), - !isCacheValid('rerankerModels') ? fetch('/api/codexlens/reranker/models').then(r => r.json()).then(d => setCacheData('rerankerModels', d)).catch(() => null) : Promise.resolve(), - // Workspace status - !isCacheValid('workspaceStatus') ? fetch('/api/codexlens/workspace-status?path=' + encodeURIComponent(projectPath || '')).then(r => r.json()).then(d => setCacheData('workspaceStatus', d)).catch(() => null) : Promise.resolve(), - // Semantic status (for FastEmbed detection) - !isCacheValid('semanticStatus') ? fetch('/api/codexlens/semantic/status').then(r => r.json()).then(d => setCacheData('semanticStatus', d)).catch(() => null) : Promise.resolve(), - // Environment variables - !isCacheValid('env') ? fetch('/api/codexlens/env').then(r => r.json()).then(d => { if (d.success) setCacheData('env', d); }).catch(() => null) : Promise.resolve() - ]).then(function() { - console.log('[CodexLens] Preload completed'); - codexLensPreloadPromises.inProgress = null; - }).catch(function(err) { - console.warn('[CodexLens] Preload error:', err); - codexLensPreloadPromises.inProgress = null; + additionalSources.forEach(function(src) { + if (!window.preloadService.sources.has(src.key)) { + window.preloadService.register(src.key, + () => fetch(src.url).then(r => r.ok ? r.json() : Promise.reject(r)), + { isHighPriority: src.priority, ttl: src.ttl } + ); + } }); - return codexLensPreloadPromises.inProgress; + // 触发预加载 + const preloadKeys = ['workspace-status', 'codexlens-config', 'codexlens-models']; + await Promise.all(preloadKeys.map(key => window.preloadService.preload(key).catch(() => null))); } // ============================================================ @@ -145,7 +128,7 @@ function escapeHtml(str) { /** * Refresh workspace index status (FTS and Vector coverage) - * Updates both the detailed panel (if exists) and header badges + * Uses progressive rendering: show cached data first, auto-refresh after background update * @param {boolean} forceRefresh - Force refresh, bypass cache */ async function refreshWorkspaceIndexStatus(forceRefresh) { @@ -156,38 +139,47 @@ async function refreshWorkspaceIndexStatus(forceRefresh) { // If neither container nor header elements exist, nothing to update if (!container && !headerFtsEl) return; - // Check cache first (unless force refresh) - if (!forceRefresh && isCacheValid('workspaceStatus')) { - var cachedResult = getCachedData('workspaceStatus'); - updateWorkspaceStatusUI(cachedResult, container, headerFtsEl, headerVectorEl); - return; - } + // Render function + var render = function(result) { + updateWorkspaceStatusUI(result, container, headerFtsEl, headerVectorEl); + }; - // Show loading state in container - if (container) { + // 1. Try to render from cache immediately + var cachedResult = getCachedData('workspaceStatus'); + if (cachedResult && !forceRefresh) { + render(cachedResult); + } else if (container) { + // Show skeleton screen container.innerHTML = '
' + ' ' + (t('common.loading') || 'Loading...') + '
'; if (window.lucide) lucide.createIcons(); } - try { - var response = await fetch('/api/codexlens/workspace-status?path=' + encodeURIComponent(projectPath || '')); - var result = await response.json(); + // 2. Listen for data update events + if (window.eventManager) { + var handleUpdate = function(data) { + render(data); + }; + // Use one-time listener or remove at appropriate time + window.eventManager.on('data:updated:workspace-status', handleUpdate); + } - // Cache the result - setCacheData('workspaceStatus', result); - - updateWorkspaceStatusUI(result, container, headerFtsEl, headerVectorEl); - } catch (err) { - console.error('[CodexLens] Failed to load workspace status:', err); - if (headerFtsEl) headerFtsEl.textContent = '--'; - if (headerVectorEl) headerVectorEl.textContent = '--'; - if (container) { - container.innerHTML = '
' + - ' ' + - (t('common.error') || 'Error') + ': ' + err.message + - '
'; + // 3. Trigger background loading + if (window.preloadService) { + try { + var freshData = await window.preloadService.preload('workspace-status', { force: forceRefresh }); + render(freshData); + } catch (err) { + console.error('[CodexLens] Failed to load workspace status:', err); + if (headerFtsEl) headerFtsEl.textContent = '--'; + if (headerVectorEl) headerVectorEl.textContent = '--'; + if (container) { + container.innerHTML = '
' + + ' ' + + (t('common.error') || 'Error') + ': ' + err.message + + '
'; + } } }