Skip to main content
Glama
config-manager.ts12.1 kB
import { z } from 'zod'; import { readFileSync, writeFileSync, existsSync, watchFile } from 'fs'; import { join } from 'path'; // 網站配置結構 const WebsiteSchema = z.object({ name: z.string(), url: z.string().url(), description: z.string().optional(), enabled: z.boolean().default(true), customSettings: z.object({ timeout: z.number().min(1000).max(120000).optional(), retries: z.number().min(0).max(10).optional(), useStealthBrowser: z.boolean().optional(), extractMainContent: z.boolean().optional(), removeAds: z.boolean().optional(), preserveImages: z.boolean().optional(), preserveLinks: z.boolean().optional(), customUserAgent: z.string().optional(), customHeaders: z.record(z.string()).optional(), rateLimit: z.object({ requestsPerSecond: z.number().min(0.1).max(100).optional(), burstLimit: z.number().min(1).max(1000).optional() }).optional() }).optional() }); // 全域設定結構 const GlobalSettingsSchema = z.object({ defaultTimeout: z.number().min(1000).max(120000).default(30000), defaultRetries: z.number().min(0).max(10).default(3), defaultUserAgent: z.string().default('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'), maxConcurrentRequests: z.number().min(1).max(50).default(5), enableGlobalRateLimit: z.boolean().default(true), globalRateLimit: z.object({ requestsPerSecond: z.number().min(0.1).max(100).default(2), burstLimit: z.number().min(1).max(1000).default(10) }), stealthBrowser: z.object({ enabled: z.boolean().default(false), headless: z.boolean().default(true), blockResources: z.array(z.string()).default(['image', 'font', 'media']), useProxy: z.boolean().default(false), proxyUrl: z.string().optional() }), contentProcessing: z.object({ removeAds: z.boolean().default(true), removeNavigation: z.boolean().default(true), removeFooter: z.boolean().default(true), removeSidebar: z.boolean().default(true), extractMainContent: z.boolean().default(true), preserveImages: z.boolean().default(false), preserveLinks: z.boolean().default(true), minTextLength: z.number().min(0).default(100), maxTextLength: z.number().min(1000).optional(), generateSummary: z.boolean().default(true) }), caching: z.object({ enabled: z.boolean().default(true), ttl: z.number().min(60).max(86400).default(3600), // 1 hour maxSize: z.number().min(10).max(10000).default(1000), // max cached items useRedis: z.boolean().default(false), redisUrl: z.string().optional() }), logging: z.object({ level: z.enum(['error', 'warn', 'info', 'debug']).default('info'), enableFileLogging: z.boolean().default(true), enablePerformanceLogging: z.boolean().default(true), logDirectory: z.string().default('./logs') }), monitoring: z.object({ enabled: z.boolean().default(true), metricsPort: z.number().min(1000).max(65535).optional(), healthCheckInterval: z.number().min(1000).max(300000).default(30000) }) }); // 主配置結構 const ConfigSchema = z.object({ websites: z.array(WebsiteSchema), settings: GlobalSettingsSchema.optional() }); export type Website = z.infer<typeof WebsiteSchema>; export type GlobalSettings = z.infer<typeof GlobalSettingsSchema>; export type Config = z.infer<typeof ConfigSchema>; export class ConfigManager { private config: Config; private configPath: string; private watchers: Set<(config: Config) => void> = new Set(); private isWatching: boolean = false; constructor(configPath?: string) { this.configPath = configPath || join(process.cwd(), 'config.json'); this.config = this.loadConfig(); this.setupFileWatcher(); } private loadConfig(): Config { if (!existsSync(this.configPath)) { console.warn(`配置文件不存在: ${this.configPath},使用默認配置`); return this.getDefaultConfig(); } try { const rawConfig = readFileSync(this.configPath, 'utf-8'); const parsedConfig = JSON.parse(rawConfig); // 驗證配置 const validatedConfig = ConfigSchema.parse(parsedConfig); // 合併默認設定 const defaultSettings = GlobalSettingsSchema.parse({}); validatedConfig.settings = { ...defaultSettings, ...validatedConfig.settings }; console.log(`成功載入配置文件: ${this.configPath}`); return validatedConfig; } catch (error) { console.error(`載入配置文件失敗: ${error instanceof Error ? error.message : String(error)}`); console.log('使用默認配置'); return this.getDefaultConfig(); } } private getDefaultConfig(): Config { return { websites: [ { name: 'example', url: 'https://example.com', description: 'Example website', enabled: true } ], settings: GlobalSettingsSchema.parse({}) }; } private setupFileWatcher(): void { if (this.isWatching) return; if (existsSync(this.configPath)) { watchFile(this.configPath, (curr, prev) => { if (curr.mtime > prev.mtime) { console.log('檢測到配置文件變更,正在重新載入...'); try { const newConfig = this.loadConfig(); this.config = newConfig; this.notifyWatchers(newConfig); console.log('配置文件重新載入成功'); } catch (error) { console.error('配置文件重新載入失敗:', error); } } }); this.isWatching = true; } } private notifyWatchers(config: Config): void { this.watchers.forEach(callback => { try { callback(config); } catch (error) { console.error('配置更新回調執行失敗:', error); } }); } // 公共方法 getConfig(): Config { return this.config; } getWebsites(): Website[] { return this.config.websites.filter(site => site.enabled); } getWebsiteByName(name: string): Website | undefined { return this.config.websites.find(site => site.name === name && site.enabled); } getGlobalSettings(): GlobalSettings { return this.config.settings || GlobalSettingsSchema.parse({}); } addWebsite(website: Omit<Website, 'enabled'>): void { const newWebsite: Website = { ...website, enabled: true }; try { WebsiteSchema.parse(newWebsite); this.config.websites.push(newWebsite); this.saveConfig(); console.log(`成功添加網站: ${website.name}`); } catch (error) { throw new Error(`添加網站失敗: ${error instanceof Error ? error.message : String(error)}`); } } updateWebsite(name: string, updates: Partial<Website>): void { const index = this.config.websites.findIndex(site => site.name === name); if (index === -1) { throw new Error(`網站不存在: ${name}`); } const updatedWebsite = { ...this.config.websites[index], ...updates }; try { WebsiteSchema.parse(updatedWebsite); this.config.websites[index] = updatedWebsite; this.saveConfig(); console.log(`成功更新網站: ${name}`); } catch (error) { throw new Error(`更新網站失敗: ${error instanceof Error ? error.message : String(error)}`); } } removeWebsite(name: string): void { const index = this.config.websites.findIndex(site => site.name === name); if (index === -1) { throw new Error(`網站不存在: ${name}`); } this.config.websites.splice(index, 1); this.saveConfig(); console.log(`成功移除網站: ${name}`); } toggleWebsite(name: string): void { const website = this.config.websites.find(site => site.name === name); if (!website) { throw new Error(`網站不存在: ${name}`); } website.enabled = !website.enabled; this.saveConfig(); console.log(`網站 ${name} 已${website.enabled ? '啟用' : '停用'}`); } updateGlobalSettings(settings: Partial<GlobalSettings>): void { const currentSettings = this.getGlobalSettings(); const newSettings = { ...currentSettings, ...settings }; try { GlobalSettingsSchema.parse(newSettings); this.config.settings = newSettings; this.saveConfig(); console.log('全域設定更新成功'); } catch (error) { throw new Error(`更新全域設定失敗: ${error instanceof Error ? error.message : String(error)}`); } } private saveConfig(): void { try { const configData = JSON.stringify(this.config, null, 2); writeFileSync(this.configPath, configData, 'utf-8'); this.notifyWatchers(this.config); } catch (error) { throw new Error(`保存配置文件失敗: ${error instanceof Error ? error.message : String(error)}`); } } // 配置驗證 validateConfig(): { isValid: boolean; errors: string[] } { try { ConfigSchema.parse(this.config); return { isValid: true, errors: [] }; } catch (error) { if (error instanceof z.ZodError) { const errors = error.errors.map(err => `${err.path.join('.')}: ${err.message}`); return { isValid: false, errors }; } return { isValid: false, errors: [String(error)] }; } } // 配置統計 getStats(): { totalWebsites: number; enabledWebsites: number; disabledWebsites: number; configSize: number; lastModified?: Date; } { const stats = { totalWebsites: this.config.websites.length, enabledWebsites: this.config.websites.filter(site => site.enabled).length, disabledWebsites: this.config.websites.filter(site => !site.enabled).length, configSize: JSON.stringify(this.config).length, lastModified: undefined as Date | undefined }; if (existsSync(this.configPath)) { const stat = require('fs').statSync(this.configPath); stats.lastModified = stat.mtime; } return stats; } // 監聽配置變更 onConfigChange(callback: (config: Config) => void): () => void { this.watchers.add(callback); // 返回取消監聽的函數 return () => { this.watchers.delete(callback); }; } // 導出配置 exportConfig(): string { return JSON.stringify(this.config, null, 2); } // 導入配置 importConfig(configData: string): void { try { const parsedConfig = JSON.parse(configData); const validatedConfig = ConfigSchema.parse(parsedConfig); this.config = validatedConfig; this.saveConfig(); console.log('配置導入成功'); } catch (error) { throw new Error(`導入配置失敗: ${error instanceof Error ? error.message : String(error)}`); } } // 重置為默認配置 resetToDefault(): void { this.config = this.getDefaultConfig(); this.saveConfig(); console.log('配置已重置為默認值'); } // 獲取網站的合併設定(全域設定 + 網站自訂設定) getMergedSettingsForWebsite(websiteName: string): GlobalSettings & Website['customSettings'] { const website = this.getWebsiteByName(websiteName); const globalSettings = this.getGlobalSettings(); if (!website) { return globalSettings; } return { ...globalSettings, ...website.customSettings, timeout: website.customSettings?.timeout || globalSettings.defaultTimeout, retries: website.customSettings?.retries || globalSettings.defaultRetries }; } } // 單例模式的全域配置管理器 let globalConfigManager: ConfigManager | null = null; export function getConfigManager(configPath?: string): ConfigManager { if (!globalConfigManager) { globalConfigManager = new ConfigManager(configPath); } return globalConfigManager; } // 便利函數 export function getConfig(): Config { return getConfigManager().getConfig(); } export function getGlobalSettings(): GlobalSettings { return getConfigManager().getGlobalSettings(); } export function getWebsites(): Website[] { return getConfigManager().getWebsites(); }

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/SunZhi-Will/website-to-markdown-mcp'

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