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
'utils.js',
'state.js',
'services.js', // CacheManager, EventManager, PreloadService - must be before main.js
'api.js',
'components/theme.js',
'components/modals.js',

View File

@@ -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');
}

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 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 '<div class="space-y-6">' +
'<div class="flex items-center justify-between mb-4">' +
'<h2 class="text-lg font-semibold">' + (t('nav.cliManager') || 'CLI Status') + '</h2>' +
'</div>' +
'<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">' +
// 左侧 Tools 区域骨架
'<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-16 bg-muted rounded"></div>' +
'<div class="h-16 bg-muted rounded"></div>' +
'<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">' +
'<div class="status-two-column">' +
'<div class="cli-section" id="tools-section"></div>' +
@@ -684,23 +779,87 @@ async function renderCliManager() {
'</div>' +
'<section id="storageCard" class="mb-6"></section>';
// 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 = '<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 ==========

View File

@@ -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<void>}
*/
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 = '<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...') +
'</div>';
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 = '<div class="text-xs text-destructive text-center py-2">' +
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
(t('common.error') || 'Error') + ': ' + err.message +
'</div>';
// 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 = '<div class="text-xs text-destructive text-center py-2">' +
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
(t('common.error') || 'Error') + ': ' + err.message +
'</div>';
}
}
}