Skip to main content
Glama
hero-repository.ts8.25 kB
import { Hero, HeroRepository, HeroConfig } from './types.js'; import { DEFAULT_HEROES, HERO_SOURCES, validateHero, sanitizeHero } from './hero-sources.js'; import { CacheManager, CacheKeyGenerator } from './utils/cache.js'; import { fetchHeroData } from './utils/network.js'; import { DEFAULT_CONFIG } from './constants.js'; /** * 远程英雄仓库实现 * 支持多源数据获取、智能缓存和优雅降级 */ export class RemoteHeroRepository implements HeroRepository { private heroes: Map<string, Hero> = new Map(); private lastFetchTime: number = 0; private cacheDuration: number = DEFAULT_CONFIG.CACHE_DURATION; private localHeroes: Hero[] = []; private cache: CacheManager; private isWarmedUp: boolean = false; constructor(localHeroes: Hero[] = []) { this.localHeroes = this.validateLocalHeroes(localHeroes); this.cache = new CacheManager(); this.warmUpCache(); } /** * 获取所有英雄 * 优先级:本地英雄 > 缓存英雄 > 远程英雄 > 默认英雄 */ async getAllHeroes(): Promise<Hero[]> { // 1. 检查缓存是否有效 if (this.isCacheValid()) { return this.mergeHeroes(Array.from(this.heroes.values())); } // 2. 尝试从远程源获取 const remoteHeroes = await this.fetchFromRemoteSources(); if (remoteHeroes.length > 0) { this.updateCache(remoteHeroes); return this.mergeHeroes(remoteHeroes); } // 3. 降级到默认英雄 console.warn('所有远程源均失败,使用默认英雄'); return this.fallbackToDefault(); } /** * 根据ID获取英雄 */ async getHeroById(id: string): Promise<Hero | null> { const allHeroes = await this.getAllHeroes(); return allHeroes.find(p => p.id === id) || null; } /** * 从配置更新英雄仓库 */ async updateFromConfig(config: HeroConfig): Promise<void> { if (!config.heroes || !Array.isArray(config.heroes)) { throw new Error('配置中未找到有效的英雄数组'); } // 验证配置中的英雄 const validHeroes = config.heroes .map(sanitizeHero) .filter((p): p is Hero => p !== null); if (validHeroes.length === 0) { throw new Error('配置中没有有效的英雄'); } // 更新本地英雄(配置中的英雄被视为本地英雄) this.localHeroes = validHeroes.map(p => ({ ...p, source: 'local' })); // 清除缓存以强制重新获取 this.clearCache(); console.log(`已从配置更新 ${validHeroes.length} 个英雄`); } /** * 验证本地英雄格式 */ private validateLocalHeroes(heroes: Hero[]): Hero[] { return heroes .filter((hero: any) => { const isValid = validateHero(hero); if (!isValid) { console.warn(`无效的本地英雄被跳过:`, hero); } return isValid; }) .map(p => ({ ...p, source: 'local' as const })); } /** * 从多个远程源获取英雄数据 */ private async fetchFromRemoteSources(): Promise<Hero[]> { console.error('开始从远程源获取英雄数据...'); for (const url of HERO_SOURCES) { try { const rawData = await fetchHeroData(url); if (rawData.length > 0) { const heroes = this.processRemoteHeroes(rawData); if (heroes.length > 0) { console.error(`成功从 ${url} 获取 ${heroes.length} 个英雄`); return heroes; } } } catch (error) { console.warn(`从 ${url} 获取英雄失败:`, error); continue; } } console.warn('所有远程源均获取失败'); return []; } /** * 处理远程英雄数据 */ private processRemoteHeroes(rawData: any[]): Hero[] { const validHeroes: Hero[] = []; for (const rawHero of rawData) { const hero = sanitizeHero(rawHero); if (hero) { hero.source = 'remote'; validHeroes.push(hero); } } return validHeroes; } /** * 合并英雄数据(本地优先) */ private mergeHeroes(baseHeroes: Hero[]): Hero[] { const mergedMap = new Map<string, Hero>(); // 1. 先添加基础英雄(远程或默认) baseHeroes.forEach((hero: any) => { mergedMap.set(hero.id, hero); }); // 2. 本地英雄覆盖同ID英雄(优先级最高) this.localHeroes.forEach((hero: any) => { if (mergedMap.has(hero.id)) { console.log(`本地英雄覆盖远程英雄: ${hero.name} (${hero.id})`); } mergedMap.set(hero.id, hero); }); const result = Array.from(mergedMap.values()); console.error(`合并后共有 ${result.length} 个英雄`); return result; } /** * 降级到默认英雄 */ private fallbackToDefault(): Hero[] { const defaultWithSource = DEFAULT_HEROES.map(p => ({ ...p, source: 'default' as const })); this.updateCache(defaultWithSource); return this.mergeHeroes(defaultWithSource); } /** * 检查缓存是否有效 */ private isCacheValid(): boolean { const now = Date.now(); const isValid = (now - this.lastFetchTime < this.cacheDuration) && this.heroes.size > 0; if (!isValid && this.heroes.size > 0) { console.error('英雄缓存已过期,需要重新获取'); } return isValid; } /** * 更新缓存 */ private updateCache(heroes: Hero[]): void { this.heroes.clear(); heroes.forEach((hero: any) => { this.heroes.set(hero.id, hero); }); this.lastFetchTime = Date.now(); console.error(`缓存已更新,共 ${heroes.length} 个英雄`); } /** * 清除缓存 */ private clearCache(): void { this.heroes.clear(); this.lastFetchTime = 0; this.isWarmedUp = false; console.log('英雄缓存已清除'); } /** * 缓存预热 */ private warmUpCache(): void { if (this.isWarmedUp) return; setTimeout(async () => { try { await this.getAllHeroes(); this.isWarmedUp = true; console.error('英雄缓存预热完成'); } catch (error) { console.warn('缓存预热失败:', error); } }, 1000); } /** * 获取仓库统计信息 */ getStats(): RepositoryStats { const heroes = Array.from(this.heroes.values()); const bySource = heroes.reduce((acc: any, hero: any) => { const source = hero.source || 'unknown'; acc[source] = (acc[source] || 0) + 1; return acc; }, {} as Record<string, number>); return { totalHeroes: heroes.length, localHeroes: this.localHeroes.length, cachedHeroes: this.heroes.size, lastFetchTime: this.lastFetchTime, cacheValid: this.isCacheValid(), bySource, cacheStats: this.cache.getStats() }; } /** * 手动刷新缓存 */ async refreshCache(): Promise<void> { this.clearCache(); await this.getAllHeroes(); } /** * 搜索英雄 */ async searchHeroes(query: string): Promise<Hero[]> { const allHeroes = await this.getAllHeroes(); const lowerQuery = query.toLowerCase(); return allHeroes.filter((hero: any) => hero.name.toLowerCase().includes(lowerQuery) || (hero.description || '').toLowerCase().includes(lowerQuery) || hero.id.toLowerCase().includes(lowerQuery) || hero.tags?.some((tag: any) => tag.toLowerCase().includes(lowerQuery)) ); } /** * 按分类获取英雄 */ async getHeroesByCategory(category: string): Promise<Hero[]> { const allHeroes = await this.getAllHeroes(); return allHeroes.filter((hero: any) => hero.category === category); } /** * 获取所有分类 */ async getCategories(): Promise<string[]> { const allHeroes = await this.getAllHeroes(); const categories = new Set<string>(); allHeroes.forEach((hero: any) => { if (hero.category) { categories.add(hero.category); } }); return Array.from(categories).sort(); } } export interface RepositoryStats { totalHeroes: number; localHeroes: number; cachedHeroes: number; lastFetchTime: number; cacheValid: boolean; bySource: Record<string, number>; cacheStats: any; }

Implementation Reference

Latest Blog Posts

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/juyitingmcp/juyitingmcp'

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