Initial commit to new repository
This commit is contained in:
212
src/AxCopilot/Services/UsageStatisticsService.cs
Normal file
212
src/AxCopilot/Services/UsageStatisticsService.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 일별 사용 통계를 기록하고 조회합니다.
|
||||
/// 저장 위치: %APPDATA%\AxCopilot\stats\YYYY-MM-DD.json
|
||||
/// 30일 초과 파일은 자동 삭제(롤링).
|
||||
/// </summary>
|
||||
internal static class UsageStatisticsService
|
||||
{
|
||||
private static readonly string _statsDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AxCopilot", "stats");
|
||||
|
||||
private static DailyUsageStats _today = new();
|
||||
private static bool _initialized;
|
||||
private static readonly object _lock = new();
|
||||
private const int RetentionDays = 30;
|
||||
|
||||
// ─── 공개 API ────────────────────────────────────────────────────────────
|
||||
|
||||
public static void RecordLauncherOpen()
|
||||
{
|
||||
EnsureInitialized();
|
||||
lock (_lock) { _today.LauncherOpens++; }
|
||||
_ = SaveTodayAsync();
|
||||
}
|
||||
|
||||
public static void RecordCommandUsage(string commandKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(commandKey)) return;
|
||||
EnsureInitialized();
|
||||
lock (_lock)
|
||||
{
|
||||
_today.CommandUsage.TryGetValue(commandKey, out var cur);
|
||||
_today.CommandUsage[commandKey] = cur + 1;
|
||||
}
|
||||
_ = SaveTodayAsync();
|
||||
}
|
||||
|
||||
public static void AddActiveSeconds(int seconds)
|
||||
{
|
||||
if (seconds <= 0) return;
|
||||
EnsureInitialized();
|
||||
lock (_lock) { _today.ActiveSeconds += seconds; }
|
||||
_ = SaveTodayAsync();
|
||||
}
|
||||
|
||||
/// <summary>당일 기록된 PC 활성 시간(초)을 반환합니다. 재시작 전 누적분 포함.</summary>
|
||||
public static int GetTodayActiveSeconds()
|
||||
{
|
||||
EnsureInitialized();
|
||||
lock (_lock) { return _today.ActiveSeconds; }
|
||||
}
|
||||
|
||||
/// <summary>AX Agent 대화를 탭별로 기록합니다.</summary>
|
||||
public static void RecordChat(string tab)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tab)) tab = "Chat";
|
||||
EnsureInitialized();
|
||||
lock (_lock)
|
||||
{
|
||||
_today.ChatCounts.TryGetValue(tab, out var cur);
|
||||
_today.ChatCounts[tab] = cur + 1;
|
||||
}
|
||||
_ = SaveTodayAsync();
|
||||
}
|
||||
|
||||
/// <summary>토큰 사용량을 기록합니다.</summary>
|
||||
public static void RecordTokens(int promptTokens, int completionTokens)
|
||||
{
|
||||
if (promptTokens <= 0 && completionTokens <= 0) return;
|
||||
EnsureInitialized();
|
||||
lock (_lock)
|
||||
{
|
||||
_today.PromptTokens += promptTokens;
|
||||
_today.CompletionTokens += completionTokens;
|
||||
_today.TotalTokens += promptTokens + completionTokens;
|
||||
}
|
||||
_ = SaveTodayAsync();
|
||||
}
|
||||
|
||||
/// <summary>최근 <paramref name="days"/>일치 통계를 날짜 오름차순으로 반환합니다.</summary>
|
||||
public static List<DailyUsageStats> GetStats(int days = 30)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var result = new List<DailyUsageStats>();
|
||||
var today = DateTime.Today;
|
||||
|
||||
for (int i = days - 1; i >= 0; i--)
|
||||
{
|
||||
var date = today.AddDays(-i);
|
||||
var dateStr = date.ToString("yyyy-MM-dd");
|
||||
|
||||
if (dateStr == _today.Date)
|
||||
{
|
||||
DailyUsageStats snapshot;
|
||||
lock (_lock) { snapshot = CloneToday(); }
|
||||
result.Add(snapshot);
|
||||
continue;
|
||||
}
|
||||
|
||||
var path = GetFilePath(dateStr);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
var stat = JsonSerializer.Deserialize<DailyUsageStats>(json);
|
||||
if (stat != null) result.Add(stat);
|
||||
else result.Add(new DailyUsageStats { Date = dateStr });
|
||||
}
|
||||
catch { result.Add(new DailyUsageStats { Date = dateStr }); }
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(new DailyUsageStats { Date = dateStr });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── 내부 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static void EnsureInitialized()
|
||||
{
|
||||
if (_initialized) return;
|
||||
lock (_lock)
|
||||
{
|
||||
if (_initialized) return;
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(_statsDir);
|
||||
var todayStr = DateTime.Today.ToString("yyyy-MM-dd");
|
||||
var path = GetFilePath(todayStr);
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
var stat = JsonSerializer.Deserialize<DailyUsageStats>(json);
|
||||
_today = stat ?? new DailyUsageStats { Date = todayStr };
|
||||
}
|
||||
else
|
||||
{
|
||||
_today = new DailyUsageStats { Date = todayStr };
|
||||
}
|
||||
|
||||
PurgeOldFiles();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"통계 서비스 초기화 실패: {ex.Message}");
|
||||
_today = new DailyUsageStats { Date = DateTime.Today.ToString("yyyy-MM-dd") };
|
||||
}
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PurgeOldFiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cutoff = DateTime.Today.AddDays(-RetentionDays);
|
||||
foreach (var file in Directory.GetFiles(_statsDir, "????-??-??.json"))
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(file);
|
||||
if (DateTime.TryParse(name, out var fileDate) && fileDate < cutoff)
|
||||
File.Delete(file);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"통계 파일 정리 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SaveTodayAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
DailyUsageStats snapshot;
|
||||
lock (_lock) { snapshot = CloneToday(); }
|
||||
|
||||
var path = GetFilePath(snapshot.Date);
|
||||
var json = JsonSerializer.Serialize(snapshot, new JsonSerializerOptions { WriteIndented = false });
|
||||
await File.WriteAllTextAsync(path, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"통계 저장 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static DailyUsageStats CloneToday() => new()
|
||||
{
|
||||
Date = _today.Date,
|
||||
LauncherOpens = _today.LauncherOpens,
|
||||
ActiveSeconds = _today.ActiveSeconds,
|
||||
CommandUsage = new Dictionary<string, int>(_today.CommandUsage),
|
||||
ChatCounts = new Dictionary<string, int>(_today.ChatCounts),
|
||||
TotalTokens = _today.TotalTokens,
|
||||
PromptTokens = _today.PromptTokens,
|
||||
CompletionTokens = _today.CompletionTokens,
|
||||
};
|
||||
|
||||
private static string GetFilePath(string dateStr)
|
||||
=> Path.Combine(_statsDir, $"{dateStr}.json");
|
||||
}
|
||||
Reference in New Issue
Block a user