mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-08 02:14:08 +08:00
feat: 添加服务模块,包含缓存管理、事件管理和预加载服务
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
289
ccw/src/templates/dashboard-js/services.js
Normal file
289
ccw/src/templates/dashboard-js/services.js
Normal 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;
|
||||
@@ -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 ==========
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user