213 lines
7.0 KiB
C#
213 lines
7.0 KiB
C#
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");
|
|
}
|