UNPKG

aiwf

Version:

AI Workflow Framework for Claude Code with multi-language support (Korean/English)

433 lines (386 loc) 12.8 kB
import fs from 'fs'; import path from 'path'; /** * 토큰 사용량 데이터를 저장하고 관리하는 클래스 */ export class TokenStorage { constructor(baseDir = '.aiwf/token-data') { this.baseDir = baseDir; this.dataFile = path.join(baseDir, 'token-usage.json'); this.sessionDir = path.join(baseDir, 'sessions'); this.reportsDir = path.join(baseDir, 'reports'); this.ensureDirectories(); this.loadData(); } /** * 필요한 디렉토리를 생성합니다 */ ensureDirectories() { try { const dirs = [this.baseDir, this.sessionDir, this.reportsDir]; for (const dir of dirs) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } } catch (error) { console.error('디렉토리 생성 중 오류 발생:', error); } } /** * 토큰 사용량 데이터를 로드합니다 */ loadData() { try { if (fs.existsSync(this.dataFile)) { const data = JSON.parse(fs.readFileSync(this.dataFile, 'utf8')); this.data = { totalTokens: 0, sessionsCount: 0, lastUpdated: Date.now(), dailyUsage: {}, commandStats: {}, compressionStats: { totalOriginal: 0, totalCompressed: 0, totalSaved: 0, avgRatio: 0 }, ...data }; } else { this.data = { totalTokens: 0, sessionsCount: 0, lastUpdated: Date.now(), dailyUsage: {}, commandStats: {}, compressionStats: { totalOriginal: 0, totalCompressed: 0, totalSaved: 0, avgRatio: 0 } }; } } catch (error) { console.error('데이터 로드 중 오류 발생:', error); this.data = this.getDefaultData(); } } /** * 기본 데이터 구조를 반환합니다 * @returns {Object} 기본 데이터 구조 */ getDefaultData() { return { totalTokens: 0, sessionsCount: 0, lastUpdated: Date.now(), dailyUsage: {}, commandStats: {}, compressionStats: { totalOriginal: 0, totalCompressed: 0, totalSaved: 0, avgRatio: 0 } }; } /** * 데이터를 저장합니다 */ saveData() { try { this.data.lastUpdated = Date.now(); fs.writeFileSync(this.dataFile, JSON.stringify(this.data, null, 2)); } catch (error) { console.error('데이터 저장 중 오류 발생:', error); } } /** * 세션 토큰 사용량을 기록합니다 * @param {Object} sessionData - 세션 데이터 */ recordSessionUsage(sessionData) { try { const today = new Date().toISOString().split('T')[0]; // 총 토큰 수 업데이트 this.data.totalTokens += sessionData.totalTokens; this.data.sessionsCount++; // 일일 사용량 업데이트 if (!this.data.dailyUsage[today]) { this.data.dailyUsage[today] = { tokens: 0, sessions: 0, commands: 0 }; } this.data.dailyUsage[today].tokens += sessionData.totalTokens; this.data.dailyUsage[today].sessions++; this.data.dailyUsage[today].commands += sessionData.commands.length; // 명령어별 통계 업데이트 for (const command of sessionData.commands) { if (!this.data.commandStats[command.command]) { this.data.commandStats[command.command] = { count: 0, totalTokens: 0, avgTokens: 0 }; } const stats = this.data.commandStats[command.command]; stats.count++; stats.totalTokens += command.totalTokens; stats.avgTokens = Math.round(stats.totalTokens / stats.count); } // 압축 통계 업데이트 if (sessionData.compressions) { for (const compression of sessionData.compressions) { this.data.compressionStats.totalOriginal += compression.originalTokens; this.data.compressionStats.totalCompressed += compression.compressedTokens; this.data.compressionStats.totalSaved += compression.savedTokens; } // 평균 압축률 계산 if (this.data.compressionStats.totalOriginal > 0) { this.data.compressionStats.avgRatio = (this.data.compressionStats.totalSaved / this.data.compressionStats.totalOriginal) * 100; } } this.saveData(); } catch (error) { console.error('세션 사용량 기록 중 오류 발생:', error); } } /** * 토큰 사용량 보고서를 생성합니다 * @param {string} type - 보고서 타입 ('daily', 'weekly', 'monthly') * @returns {Object} 보고서 데이터 */ generateUsageReport(type = 'daily') { try { const report = { type, generatedAt: Date.now(), period: this.getPeriod(type), summary: { totalTokens: this.data.totalTokens, totalSessions: this.data.sessionsCount, avgTokensPerSession: this.data.sessionsCount > 0 ? Math.round(this.data.totalTokens / this.data.sessionsCount) : 0 }, dailyUsage: this.getDailyUsageForPeriod(type), topCommands: this.getTopCommands(10), compressionStats: this.data.compressionStats, trends: this.calculateTrends(type) }; // 보고서 저장 const reportFile = path.join(this.reportsDir, `${type}_report_${Date.now()}.json`); fs.writeFileSync(reportFile, JSON.stringify(report, null, 2)); return report; } catch (error) { console.error('보고서 생성 중 오류 발생:', error); return null; } } /** * 기간에 따른 일일 사용량을 조회합니다 * @param {string} type - 기간 타입 * @returns {Object} 일일 사용량 데이터 */ getDailyUsageForPeriod(type) { const days = type === 'weekly' ? 7 : type === 'monthly' ? 30 : 1; const result = {}; for (let i = 0; i < days; i++) { const date = new Date(); date.setDate(date.getDate() - i); const dateStr = date.toISOString().split('T')[0]; result[dateStr] = this.data.dailyUsage[dateStr] || { tokens: 0, sessions: 0, commands: 0 }; } return result; } /** * 가장 많이 사용된 명령어를 조회합니다 * @param {number} limit - 조회할 명령어 수 * @returns {Array<Object>} 상위 명령어 목록 */ getTopCommands(limit = 10) { try { return Object.entries(this.data.commandStats) .sort((a, b) => b[1].totalTokens - a[1].totalTokens) .slice(0, limit) .map(([command, stats]) => ({ command, count: stats.count, totalTokens: stats.totalTokens, avgTokens: stats.avgTokens })); } catch (error) { console.error('상위 명령어 조회 중 오류 발생:', error); return []; } } /** * 사용량 트렌드를 계산합니다 * @param {string} type - 기간 타입 * @returns {Object} 트렌드 데이터 */ calculateTrends(type) { try { const dailyUsage = this.getDailyUsageForPeriod(type); const values = Object.values(dailyUsage).map(day => day.tokens); if (values.length < 2) { return { trend: 'insufficient_data', change: 0 }; } const recent = values.slice(0, Math.floor(values.length / 2)); const older = values.slice(Math.floor(values.length / 2)); const recentAvg = recent.reduce((sum, val) => sum + val, 0) / recent.length; const olderAvg = older.reduce((sum, val) => sum + val, 0) / older.length; const change = olderAvg > 0 ? ((recentAvg - olderAvg) / olderAvg) * 100 : 0; return { trend: change > 10 ? 'increasing' : change < -10 ? 'decreasing' : 'stable', change: Math.round(change), recentAvg: Math.round(recentAvg), olderAvg: Math.round(olderAvg) }; } catch (error) { console.error('트렌드 계산 중 오류 발생:', error); return { trend: 'error', change: 0 }; } } /** * 기간 정보를 반환합니다 * @param {string} type - 기간 타입 * @returns {Object} 기간 정보 */ getPeriod(type) { const now = new Date(); const start = new Date(now); switch (type) { case 'weekly': start.setDate(now.getDate() - 7); break; case 'monthly': start.setMonth(now.getMonth() - 1); break; default: start.setDate(now.getDate() - 1); } return { start: start.toISOString().split('T')[0], end: now.toISOString().split('T')[0] }; } /** * 임계값을 확인합니다 * @param {Object} thresholds - 임계값 설정 * @returns {Array<Object>} 임계값 위반 목록 */ checkThresholds(thresholds) { const violations = []; const today = new Date().toISOString().split('T')[0]; const todayUsage = this.data.dailyUsage[today] || { tokens: 0, sessions: 0 }; try { // 일일 토큰 한도 확인 if (thresholds.dailyTokenLimit && todayUsage.tokens > thresholds.dailyTokenLimit) { violations.push({ type: 'daily_token_limit', threshold: thresholds.dailyTokenLimit, current: todayUsage.tokens, message: `일일 토큰 한도(${thresholds.dailyTokenLimit})를 초과했습니다: ${todayUsage.tokens}` }); } // 일일 세션 한도 확인 if (thresholds.dailySessionLimit && todayUsage.sessions > thresholds.dailySessionLimit) { violations.push({ type: 'daily_session_limit', threshold: thresholds.dailySessionLimit, current: todayUsage.sessions, message: `일일 세션 한도(${thresholds.dailySessionLimit})를 초과했습니다: ${todayUsage.sessions}` }); } // 경고 임계값 확인 if (thresholds.warningThreshold) { const warningLimit = thresholds.dailyTokenLimit * (thresholds.warningThreshold / 100); if (todayUsage.tokens > warningLimit) { violations.push({ type: 'warning_threshold', threshold: warningLimit, current: todayUsage.tokens, message: `경고 임계값(${Math.round(warningLimit)})을 초과했습니다: ${todayUsage.tokens}` }); } } } catch (error) { console.error('임계값 확인 중 오류 발생:', error); } return violations; } /** * 저장된 보고서 목록을 조회합니다 * @returns {Array<Object>} 보고서 목록 */ getReportsList() { try { const reports = []; const files = fs.readdirSync(this.reportsDir); for (const file of files) { if (file.endsWith('.json')) { const filePath = path.join(this.reportsDir, file); const stats = fs.statSync(filePath); reports.push({ filename: file, path: filePath, size: stats.size, created: stats.birthtime, modified: stats.mtime }); } } return reports.sort((a, b) => b.created - a.created); } catch (error) { console.error('보고서 목록 조회 중 오류 발생:', error); return []; } } /** * 오래된 보고서를 정리합니다 * @param {number} daysToKeep - 보관할 일수 */ cleanupOldReports(daysToKeep = 30) { try { const cutoffTime = Date.now() - (daysToKeep * 24 * 60 * 60 * 1000); const files = fs.readdirSync(this.reportsDir); let deletedCount = 0; for (const file of files) { if (file.endsWith('.json')) { const filePath = path.join(this.reportsDir, file); const stats = fs.statSync(filePath); if (stats.birthtime.getTime() < cutoffTime) { fs.unlinkSync(filePath); deletedCount++; } } } console.log(`${deletedCount}개의 오래된 보고서를 정리했습니다.`); } catch (error) { console.error('보고서 정리 중 오류 발생:', error); } } /** * 모든 데이터를 초기화합니다 */ resetData() { try { this.data = this.getDefaultData(); this.saveData(); console.log('토큰 사용량 데이터가 초기화되었습니다.'); } catch (error) { console.error('데이터 초기화 중 오류 발생:', error); } } } export default TokenStorage;