Skip to main content
Glama

MCPDemo - Visual SQL Chat Platform

by Ayi456
PanelManager.ts13.8 kB
import { Panel, PanelConfig, PanelManagerOptions, AddPanelResult, PanelInfo } from './types.js'; import { generateSecureId, isValidId, calculateExpiryTime, isExpired, generateCacheKey, formatDateTime } from './utils.js'; import { executeMysqlQuery, executeRedisCommand, getRedisClient } from './database.js'; import { ValidationError } from './utils/errors.js'; export class PanelManager { private options: Required<PanelManagerOptions>; private baseUrl: string; constructor(baseUrl: string, options: PanelManagerOptions = {}) { this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; this.options = { defaultTtl: options.defaultTtl || 24 * 60 * 60 * 7 , // 7天 maxTtl: options.maxTtl || 24 * 60 * 60 * 30, // 30天 idLength: options.idLength || 16 }; } async addPanel(osspath: string, options?: { user_id?: string; title?: string; description?: string; is_public?: boolean; ttl?: number; }): Promise<AddPanelResult> { const finalTtl = options?.ttl || this.options.defaultTtl; // 验证输入参数 if (!osspath || typeof osspath !== 'string' || osspath.length === 0) { throw new Error('文件路径不能为空'); } if (finalTtl > this.options.maxTtl) { throw new Error(`TTL不能超过最大值 ${this.options.maxTtl} 秒`); } // 生成唯一ID const id = await this.generateUniqueId(); const expiresAt = calculateExpiryTime(finalTtl); try { await this.insertPanelToDatabase(id, osspath, expiresAt, { user_id: options?.user_id, title: options?.title, description: options?.description, is_public: options?.is_public || false }); await this.cachePanelData(id, osspath, finalTtl); const url = `${this.baseUrl}/panel/${id}`; return { id, url, osspath, title: options?.title, description: options?.description, is_public: options?.is_public || false, expires_at: expiresAt, ttl: finalTtl }; } catch (error) { // 如果缓存写入失败,尝试清理数据库记录 try { await this.deletePanelFromDatabase(id); } catch (cleanupError) { console.error('清理数据库记录失败', cleanupError); } throw error; } } async getPanel(id: string): Promise<string | null> { if (!isValidId(id, this.options.idLength)) { return null; } try { // 1. 先查询Redis缓存 const cachedPath = await this.getCachedPanelData(id); if (cachedPath) { const panel = await this.getPanelFromDatabase(id); if (!panel || isExpired(panel.expires_at)) { this.markPanelAsExpired(id).catch(err => console.error('标记Panel为过期失败', err) ); return null; } // 未过期,更新访问计数并返回缓存的路径 this.updateVisitCount(id).catch(err => console.error('更新访问计数失败:', err) ); return cachedPath; } // 2. 缓存未命中,查询MySQL数据库 const panel = await this.getPanelFromDatabase(id); if (!panel) { return null; } // 3. 检查是否过期 if (isExpired(panel.expires_at)) { // 标记为过期并从缓存中删除 this.markPanelAsExpired(id).catch(err => console.error('标记Panel为过期失败', err) ); return null; } // 4. 缓存预热:将数据写回Redis const remainingTtl = Math.floor((panel.expires_at.getTime() - Date.now()) / 1000); if (remainingTtl > 0) { this.cachePanelData(id, panel.osspath, remainingTtl).catch(err => console.error('缓存预热失败:', err) ); } // 5. 更新访问计数 this.updateVisitCount(id).catch(err => console.error('更新访问计数失败:', err) ); return panel.osspath; } catch (error) { console.error('获取Panel失败:', error); return null; } } /** * 获取用户 Panels 列表 */ async getUserPanels(args: import('./types.js').GetUserPanelsArgs): Promise<import('./types.js').UserPanelsResult> { const { user_id, page = 1, limit = 10, status = 'all', is_public } = args; // 验证参数 if (!user_id) { throw new Error('用户ID不能为空'); } // 严格验证分页参数,防止SQL注入 const safePage = Math.max(1, Math.floor(Number(page) || 1)); const safeLimit = Math.min(100, Math.max(1, Math.floor(Number(limit) || 10))); if (!Number.isFinite(safePage) || !Number.isFinite(safeLimit)) { throw new Error('分页参数无效'); } try { // 构建查询条件 let whereConditions = ['user_id = ?']; let queryParams: any[] = [user_id]; // 状态过滤 if (status === 'active') { whereConditions.push('status = ? AND expires_at > NOW()'); queryParams.push('active'); } else if (status === 'expired') { whereConditions.push('(status = ? OR expires_at <= NOW())'); queryParams.push('expired'); } // 公开状态过滤 if (is_public !== undefined) { whereConditions.push('is_public = ?'); queryParams.push(is_public ? 1 : 0); } const whereClause = whereConditions.join(' AND '); // 查询总数 const countResult = await executeMysqlQuery<Array<{count: number}>>( `SELECT COUNT(*) as count FROM panels WHERE ${whereClause}`, queryParams ); const total = countResult[0]?.count || 0; // 查询列表数据 - 使用安全的分页参数 const offset = (safePage - 1) * safeLimit; const panels = await executeMysqlQuery<Panel[]>( `SELECT * FROM panels WHERE ${whereClause} ORDER BY created_at DESC LIMIT ${safeLimit} OFFSET ${offset}`, queryParams ); // 转换为 PanelInfo 格式并检查缓存状态 const panelInfos: import('./types.js').PanelInfo[] = await Promise.all( panels.map(async (panel) => { const is_cached = await this.isPanelCached(panel.id); return { id: panel.id, user_id: panel.user_id, osspath: panel.osspath, title: panel.title, description: panel.description, is_public: panel.is_public, created_at: panel.created_at, expires_at: panel.expires_at, visit_count: panel.visit_count, status: panel.status, is_cached }; }) ); return { panels: panelInfos, total, page, limit, has_more: total > page * limit }; } catch (error) { throw new Error(`获取用户Panels失败: ${error}`); } } /** * 更新 Panel 信息 */ async updatePanelInfo(panelId: string, updates: { title?: string; description?: string; is_public?: boolean; }): Promise<void> { if (!panelId) { throw new ValidationError('Panel ID不能为空'); } // 构建更新字段 const updateFields: string[] = []; const updateValues: any[] = []; if (updates.title !== undefined) { updateFields.push('title = ?'); updateValues.push(updates.title); } if (updates.description !== undefined) { updateFields.push('description = ?'); updateValues.push(updates.description); } if (updates.is_public !== undefined) { updateFields.push('is_public = ?'); updateValues.push(updates.is_public); } if (updateFields.length === 0) { throw new ValidationError('没有提供要更新的字段'); } updateFields.push('updated_at = NOW()'); updateValues.push(panelId); try { const result = await executeMysqlQuery( `UPDATE panels SET ${updateFields.join(', ')} WHERE id = ?`, updateValues ); // 检查是否实际更新了记录 if ((result as any).affectedRows === 0) { throw new ValidationError('Panel不存在或无权限修改'); } } catch (error) { throw new Error(`更新Panel信息失败: ${error}`); } } async getPanelInfo(id: string): Promise<PanelInfo | null> { if (!isValidId(id, this.options.idLength)) { throw new ValidationError('无效的Panel ID格式'); } try { // 查询数据库获取完整信息 const panel = await this.getPanelFromDatabase(id); if (!panel) { return null; } // 检查是否在缓存中 const isCached = await this.isPanelCached(id); return { id: panel.id, user_id: panel.user_id, osspath: panel.osspath, title: panel.title, description: panel.description, is_public: panel.is_public, created_at: panel.created_at, expires_at: panel.expires_at, visit_count: panel.visit_count, status: isExpired(panel.expires_at) ? 'expired' : panel.status, is_cached: isCached }; } catch (error) { throw new Error(`获取Panel信息失败: ${error}`); } } /** * 生成唯一ID */ private async generateUniqueId(): Promise<string> { let attempts = 0; const maxAttempts = 10; while (attempts < maxAttempts) { const id = generateSecureId(this.options.idLength); // 检查ID是否已存�? const exists = await this.checkIdExists(id); if (!exists) { return id; } attempts++; } throw new Error('无法生成唯一ID,请稍后重试'); } /** * 检查ID是否已存在 */ private async checkIdExists(id: string): Promise<boolean> { try { const result = await executeMysqlQuery<any[]>( 'SELECT 1 FROM panels WHERE id = ? LIMIT 1', [id] ); return result.length > 0; } catch (error) { throw new Error(`检查ID存在性失败: ${error}`); } } /** * 将Panel插入数据库 */ private async insertPanelToDatabase( id: string, osspath: string, expiresAt: Date, options?: { user_id?: string; title?: string; description?: string; is_public?: boolean; } ): Promise<void> { try { await executeMysqlQuery( 'INSERT INTO panels (id, user_id, osspath, title, description, is_public, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)', [ id, options?.user_id || null, osspath, options?.title || null, options?.description || null, options?.is_public || false, expiresAt ] ); } catch (error) { throw new Error(`插入Panel到数据库失败: ${error}`); } } /** * 从数据库获取Panel */ private async getPanelFromDatabase(id: string): Promise<Panel | null> { try { const result = await executeMysqlQuery<Panel[]>( 'SELECT * FROM panels WHERE id = ? LIMIT 1', [id] ); return result.length > 0 ? result[0] : null; } catch (error) { throw new Error(`从数据库获取Panel失败: ${error}`); } } /** * 从数据库删除Panel */ private async deletePanelFromDatabase(id: string): Promise<void> { try { await executeMysqlQuery( 'DELETE FROM panels WHERE id = ?', [id] ); } catch (error) { throw new Error(`从数据库删除Panel失败: ${error}`); } } /** * 缓存Panel数据到Redis */ private async cachePanelData(id: string, osspath: string, ttl: number): Promise<void> { try { await executeRedisCommand(async () => { const client = getRedisClient(); const key = generateCacheKey(id); await client.setEx(key, ttl, osspath); }); } catch (error) { throw new Error(`缓存Panel数据失败: ${error}`); } } /** * 从Redis获取缓存的Panel数据 */ private async getCachedPanelData(id: string): Promise<string | null> { try { return await executeRedisCommand(async () => { const client = getRedisClient(); const key = generateCacheKey(id); return await client.get(key); }); } catch (error) { console.error('获取缓存Panel数据失败:', error); return null; } } /** * 检查Panel是否在缓存中 */ private async isPanelCached(id: string): Promise<boolean> { try { return await executeRedisCommand(async () => { const client = getRedisClient(); const key = generateCacheKey(id); const exists = await client.exists(key); return exists === 1; }); } catch (error) { return false; } } /** * 更新访问计数 */ private async updateVisitCount(id: string): Promise<void> { try { await executeMysqlQuery( 'UPDATE panels SET visit_count = visit_count + 1 WHERE id = ?', [id] ); } catch (error) { throw new Error(`更新访问计数失败: ${error}`); } } /** * 标记Panel为过期 */ private async markPanelAsExpired(id: string): Promise<void> { try { // 更新数据库状态 await executeMysqlQuery( 'UPDATE panels SET status = "expired" WHERE id = ?', [id] ); await executeRedisCommand(async () => { const client = getRedisClient(); const key = generateCacheKey(id); await client.del(key); }); } catch (error) { console.error('标记Panel为过期失败', error); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Ayi456/visual-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server