feat: 添加服务模块,包含缓存管理、事件管理和预加载服务

This commit is contained in:
catlog22
2026-01-12 21:47:11 +08:00
parent ca77c114dd
commit fd9c55162d
5 changed files with 625 additions and 135 deletions

View File

@@ -106,6 +106,7 @@ const MODULE_FILES = [
'i18n.js', // Must be loaded first for translations 'i18n.js', // Must be loaded first for translations
'utils.js', 'utils.js',
'state.js', 'state.js',
'services.js', // CacheManager, EventManager, PreloadService - must be before main.js
'api.js', 'api.js',
'components/theme.js', 'components/theme.js',
'components/modals.js', 'components/modals.js',

View File

@@ -5,6 +5,9 @@ document.addEventListener('DOMContentLoaded', async () => {
// Initialize Lucide icons (must be first to render SVG icons) // Initialize Lucide icons (must be first to render SVG icons)
try { lucide.createIcons(); } catch (e) { console.error('Lucide icons init failed:', e); } 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) // Initialize i18n (must be early to translate static content)
try { initI18n(); } catch (e) { console.error('I18n init failed:', e); } 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');
}

View File

@@ -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<void>}
*/
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;

View File

@@ -10,6 +10,69 @@ var cliWrapperEndpoints = []; // CLI封装 endpoints from /api/cli/settings
var cliToolConfig = null; // Store loaded CLI config var cliToolConfig = null; // Store loaded CLI config
var predefinedModels = {}; // Store predefined models per tool 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 ========== // ========== CSRF Token Management ==========
var csrfToken = null; // Store CSRF token for state-changing requests var csrfToken = null; // Store CSRF token for state-changing requests
@@ -290,12 +353,23 @@ window.syncEndpointToCliTools = syncEndpointToCliTools;
// ========== CLI Tool Configuration ========== // ========== CLI Tool Configuration ==========
async function loadCliToolConfig() { async function loadCliToolConfig() {
// 尝试从缓存获取
var cached = getCliCachedData('toolConfig');
if (cached) {
cliToolConfig = cached.config || null;
predefinedModels = cached.predefinedModels || {};
return cached;
}
try { try {
var response = await fetch('/api/cli/config'); var response = await fetch('/api/cli/config');
if (!response.ok) throw new Error('Failed to load CLI config'); if (!response.ok) throw new Error('Failed to load CLI config');
var data = await response.json(); var data = await response.json();
cliToolConfig = data.config || null; cliToolConfig = data.config || null;
predefinedModels = data.predefinedModels || {}; predefinedModels = data.predefinedModels || {};
// 缓存结果
setCliCacheData('toolConfig', data);
return data; return data;
} catch (err) { } catch (err) {
console.error('Failed to load CLI config:', err); console.error('Failed to load CLI config:', err);
@@ -650,29 +724,50 @@ function initToolConfigModalEvents(tool, currentConfig, models) {
} }
// ========== Rendering ========== // ========== 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'); * 构建 CLI Manager 骨架屏
var searchInput = document.getElementById('searchInput'); * @returns {string} HTML 字符串
if (statsGrid) statsGrid.style.display = 'none'; */
if (searchInput) searchInput.parentElement.style.display = 'none'; function buildCliManagerSkeleton() {
return '<div class="space-y-6">' +
// Load data (including CodexLens status for tools section) '<div class="flex items-center justify-between mb-4">' +
// loadCliToolsConfig() ensures cli-tools.json is auto-created if missing '<h2 class="text-lg font-semibold">' + (t('nav.cliManager') || 'CLI Status') + '</h2>' +
await Promise.all([ '</div>' +
loadCliToolsConfig(), '<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">' +
loadCliToolStatus(), // 左侧 Tools 区域骨架
loadCodexLensStatus(), '<div class="card p-4">' +
loadCcwInstallations(), '<div class="animate-pulse space-y-4">' +
loadCcwEndpointTools(), '<div class="h-4 bg-muted rounded w-1/3"></div>' +
loadLitellmApiEndpoints(), '<div class="h-16 bg-muted rounded"></div>' +
loadCliCustomEndpoints(), '<div class="h-16 bg-muted rounded"></div>' +
loadCliWrapperEndpoints() '<div class="h-16 bg-muted rounded"></div>' +
]); '</div>' +
'</div>' +
// 右侧 CCW 区域骨架
'<div class="card p-4">' +
'<div class="animate-pulse space-y-4">' +
'<div class="h-4 bg-muted rounded w-1/3"></div>' +
'<div class="h-20 bg-muted rounded"></div>' +
'<div class="h-20 bg-muted rounded"></div>' +
'</div>' +
'</div>' +
'</div>' +
// 底部区域骨架
'<div class="card p-4">' +
'<div class="animate-pulse space-y-4">' +
'<div class="h-4 bg-muted rounded w-1/4"></div>' +
'<div class="h-12 bg-muted rounded"></div>' +
'</div>' +
'</div>' +
'</div>';
}
/**
* 渲染 CLI Manager 实际内容(内部容器结构 + 各子面板)
* @param {HTMLElement} container - 主容器元素
*/
function renderCliManagerContent(container) {
container.innerHTML = '<div class="status-manager">' + container.innerHTML = '<div class="status-manager">' +
'<div class="status-two-column">' + '<div class="status-two-column">' +
'<div class="cli-section" id="tools-section"></div>' + '<div class="cli-section" id="tools-section"></div>' +
@@ -684,23 +779,87 @@ async function renderCliManager() {
'</div>' + '</div>' +
'<section id="storageCard" class="mb-6"></section>'; '<section id="storageCard" class="mb-6"></section>';
// Render sub-panels // 渲染子面板
renderToolsSection(); renderToolsSection();
renderCcwSection(); renderCcwSection();
renderLanguageSettingsSection(); renderLanguageSettingsSection();
renderCliSettingsSection(); renderCliSettingsSection();
renderCcwEndpointToolsSection(); renderCcwEndpointToolsSection();
// Initialize storage manager card // 初始化存储管理器卡片
if (typeof initStorageManager === 'function') { if (typeof initStorageManager === 'function') {
initStorageManager(); 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(); if (window.lucide) lucide.createIcons();
// Sync active executions to restore running state // 2. 尝试从缓存渲染(快速展示)
await syncActiveExecutions(); 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 = '<div class="card p-4 text-center">' +
'<i data-lucide="alert-circle" class="w-8 h-8 text-muted-foreground mx-auto mb-2"></i>' +
'<p class="text-muted-foreground">' + (t('common.loadFailed') || 'Failed to load data') + '</p>' +
'<button class="btn btn-sm mt-2" onclick="renderCliManager()">' +
'<i data-lucide="refresh-cw" class="w-3 h-3 mr-1"></i>' +
(t('common.retry') || 'Retry') +
'</button>' +
'</div>';
if (window.lucide) lucide.createIcons();
}
}
// 同步活动执行
syncActiveExecutions();
} }
// ========== Helper Functions ========== // ========== Helper Functions ==========

View File

@@ -2,124 +2,107 @@
// Extracted from cli-manager.js for better maintainability // 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; const CACHE_KEY_MAP = {
workspaceStatus: 'workspace-status',
// Cache storage for CodexLens data config: 'codexlens-config',
const codexLensCache = { rerankerConfig: 'codexlens-reranker-config',
workspaceStatus: { data: null, timestamp: 0 }, status: 'codexlens-status',
config: { data: null, timestamp: 0 }, env: 'codexlens-env',
rerankerConfig: { data: null, timestamp: 0 }, models: 'codexlens-models',
status: { data: null, timestamp: 0 }, rerankerModels: 'codexlens-reranker-models',
env: { data: null, timestamp: 0 }, semanticStatus: 'codexlens-semantic-status',
models: { data: null, timestamp: 0 }, gpuList: 'codexlens-gpu-list',
rerankerModels: { data: null, timestamp: 0 }, indexes: 'codexlens-indexes'
semanticStatus: { data: null, timestamp: 0 },
gpuList: { data: null, timestamp: 0 },
indexes: { data: null, timestamp: 0 }
}; };
/** /**
* Check if cache is valid (not expired) * 兼容性函数:检查缓存是否有效
* @param {string} key - Cache key * @param {string} key - 旧缓存键
* @param {number} ttl - Optional custom TTL
* @returns {boolean} * @returns {boolean}
*/ */
function isCacheValid(key, ttl = CODEXLENS_CACHE_TTL) { function isCacheValid(key) {
const cache = codexLensCache[key]; if (!window.cacheManager) return false;
if (!cache || !cache.data) return false; const newKey = CACHE_KEY_MAP[key] || key;
return (Date.now() - cache.timestamp) < ttl; return window.cacheManager.isValid(newKey);
} }
/** /**
* Get cached data * 兼容性函数:获取缓存数据
* @param {string} key - Cache key * @param {string} key - 旧缓存键
* @returns {*} Cached data or null * @returns {*}
*/ */
function getCachedData(key) { 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 {string} key - 旧缓存键
* @param {*} data - Data to cache * @param {*} data - 数据
* @param {number} ttl - 可选的 TTL
*/ */
function setCacheData(key, data) { function setCacheData(key, data, ttl = 180000) {
if (codexLensCache[key]) { if (!window.cacheManager) return;
codexLensCache[key].data = data; const newKey = CACHE_KEY_MAP[key] || key;
codexLensCache[key].timestamp = Date.now(); 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) { function invalidateCache(key) {
if (key && codexLensCache[key]) { if (!window.cacheManager) return;
codexLensCache[key].data = null; if (key) {
codexLensCache[key].timestamp = 0; const newKey = CACHE_KEY_MAP[key] || key;
} else if (!key) { window.cacheManager.invalidate(newKey);
Object.keys(codexLensCache).forEach(function(k) { } else {
codexLensCache[k].data = null; // 清除所有 codexlens 相关缓存
codexLensCache[k].timestamp = 0; 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 * 预加载 CodexLens 数据
* Called immediately when entering the CodexLens page * 现在委托给全局 PreloadService
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function preloadCodexLensData() { async function preloadCodexLensData() {
console.log('[CodexLens] Starting preload...'); console.log('[CodexLens] Preload delegated to PreloadService');
if (!window.preloadService) {
// Skip if already preloading or cache is valid console.warn('[CodexLens] PreloadService not available');
if (codexLensPreloadPromises.inProgress) { return;
console.log('[CodexLens] Preload already in progress, skipping');
return codexLensPreloadPromises.inProgress;
} }
// Check if all caches are valid // 注册额外的数据源(如果尚未注册)
var allCacheValid = isCacheValid('config') && isCacheValid('models') && const additionalSources = [
isCacheValid('rerankerConfig') && isCacheValid('rerankerModels') && { key: 'codexlens-config', url: '/api/codexlens/config', priority: true, ttl: 300000 },
isCacheValid('semanticStatus') && isCacheValid('env'); { key: 'codexlens-reranker-config', url: '/api/codexlens/reranker/config', priority: false, ttl: 300000 },
if (allCacheValid) { { key: 'codexlens-reranker-models', url: '/api/codexlens/reranker/models', priority: false, ttl: 600000 },
console.log('[CodexLens] All caches valid, skipping preload'); { key: 'codexlens-semantic-status', url: '/api/codexlens/semantic/status', priority: false, ttl: 300000 },
return Promise.resolve(); { key: 'codexlens-env', url: '/api/codexlens/env', priority: false, ttl: 300000 }
} ];
// Start preloading all endpoints in parallel additionalSources.forEach(function(src) {
codexLensPreloadPromises.inProgress = Promise.all([ if (!window.preloadService.sources.has(src.key)) {
// Config and models window.preloadService.register(src.key,
!isCacheValid('config') ? fetch('/api/codexlens/config').then(r => r.json()).then(d => setCacheData('config', d)) : Promise.resolve(), () => fetch(src.url).then(r => r.ok ? r.json() : Promise.reject(r)),
!isCacheValid('models') ? fetch('/api/codexlens/models').then(r => r.json()).then(d => setCacheData('models', d)) : Promise.resolve(), { isHighPriority: src.priority, ttl: src.ttl }
// 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;
}); });
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) * 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 * @param {boolean} forceRefresh - Force refresh, bypass cache
*/ */
async function refreshWorkspaceIndexStatus(forceRefresh) { async function refreshWorkspaceIndexStatus(forceRefresh) {
@@ -156,38 +139,47 @@ async function refreshWorkspaceIndexStatus(forceRefresh) {
// If neither container nor header elements exist, nothing to update // If neither container nor header elements exist, nothing to update
if (!container && !headerFtsEl) return; if (!container && !headerFtsEl) return;
// Check cache first (unless force refresh) // Render function
if (!forceRefresh && isCacheValid('workspaceStatus')) { var render = function(result) {
var cachedResult = getCachedData('workspaceStatus'); updateWorkspaceStatusUI(result, container, headerFtsEl, headerVectorEl);
updateWorkspaceStatusUI(cachedResult, container, headerFtsEl, headerVectorEl); };
return;
}
// Show loading state in container // 1. Try to render from cache immediately
if (container) { var cachedResult = getCachedData('workspaceStatus');
if (cachedResult && !forceRefresh) {
render(cachedResult);
} else if (container) {
// Show skeleton screen
container.innerHTML = '<div class="text-xs text-muted-foreground text-center py-2">' + container.innerHTML = '<div class="text-xs text-muted-foreground text-center py-2">' +
'<i data-lucide="loader-2" class="w-4 h-4 animate-spin inline mr-1"></i> ' + (t('common.loading') || 'Loading...') + '<i data-lucide="loader-2" class="w-4 h-4 animate-spin inline mr-1"></i> ' + (t('common.loading') || 'Loading...') +
'</div>'; '</div>';
if (window.lucide) lucide.createIcons(); if (window.lucide) lucide.createIcons();
} }
try { // 2. Listen for data update events
var response = await fetch('/api/codexlens/workspace-status?path=' + encodeURIComponent(projectPath || '')); if (window.eventManager) {
var result = await response.json(); 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 // 3. Trigger background loading
setCacheData('workspaceStatus', result); if (window.preloadService) {
try {
updateWorkspaceStatusUI(result, container, headerFtsEl, headerVectorEl); var freshData = await window.preloadService.preload('workspace-status', { force: forceRefresh });
} catch (err) { render(freshData);
console.error('[CodexLens] Failed to load workspace status:', err); } catch (err) {
if (headerFtsEl) headerFtsEl.textContent = '--'; console.error('[CodexLens] Failed to load workspace status:', err);
if (headerVectorEl) headerVectorEl.textContent = '--'; if (headerFtsEl) headerFtsEl.textContent = '--';
if (container) { if (headerVectorEl) headerVectorEl.textContent = '--';
container.innerHTML = '<div class="text-xs text-destructive text-center py-2">' + if (container) {
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' + container.innerHTML = '<div class="text-xs text-destructive text-center py-2">' +
(t('common.error') || 'Error') + ': ' + err.message + '<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
'</div>'; (t('common.error') || 'Error') + ': ' + err.message +
'</div>';
}
} }
} }