using AudioWallpaper.ActivityWatch; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace AudioWallpaper.ActivityWatch { internal class ActivityAnalyzer { private readonly ActivityAnalyzerConfig _config; private readonly TimeZoneInfo _timeZone; public ActivityAnalyzer(ActivityAnalyzerConfig config = null) { _config = config ?? new ActivityAnalyzerConfig(); // 初始化时区 try { _timeZone = TimeZoneInfo.FindSystemTimeZoneById(GetWindowsTimeZoneId(_config.Timezone)); } catch (TimeZoneNotFoundException) { Console.WriteLine($"警告:未知时区 '{_config.Timezone}',使用默认时区 'China Standard Time'"); _timeZone = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"); } } private string GetWindowsTimeZoneId(string timezone) { // 映射一些常见的时区名称到Windows时区ID var timeZoneMap = new Dictionary { { "Asia/Shanghai", "China Standard Time" }, { "UTC", "UTC" }, { "US/Eastern", "Eastern Standard Time" }, { "US/Pacific", "Pacific Standard Time" }, { "Europe/London", "GMT Standard Time" }, { "Europe/Paris", "W. Europe Standard Time" } }; return timeZoneMap.ContainsKey(timezone) ? timeZoneMap[timezone] : timezone; } private DateTime? ParseTimestamp(string timestampStr) { if (string.IsNullOrEmpty(timestampStr)) return null; try { // 方法1:直接解析ISO格式 if (DateTime.TryParse(timestampStr, out DateTime dt)) { // 转换到指定时区 if (dt.Kind == DateTimeKind.Utc) { return TimeZoneInfo.ConvertTimeFromUtc(dt, _timeZone); } else if (dt.Kind == DateTimeKind.Unspecified) { return TimeZoneInfo.ConvertTime(dt, _timeZone); } return dt; } // 方法2:手动解析 string cleanTimestamp = timestampStr.Replace('T', ' '); if (cleanTimestamp.Contains('+')) cleanTimestamp = cleanTimestamp.Split('+')[0]; else if (cleanTimestamp.Contains('Z')) cleanTimestamp = cleanTimestamp.Replace("Z", ""); // 移除毫秒部分 cleanTimestamp = Regex.Replace(cleanTimestamp, @"\.\d+", ""); if (DateTime.TryParseExact(cleanTimestamp, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime parsedDt)) { return TimeZoneInfo.ConvertTime(parsedDt, _timeZone); } return null; } catch (Exception ex) { Console.WriteLine($"警告:无法解析时间戳 '{timestampStr}': {ex.Message}"); return null; } } /// /// 解析从ActivityWatchClient获取的原始活动数据 /// /// ActivityWatchClient.GetEventsAsync()返回的数据 /// 解析后的活动条目列表 public List ParseActivityData(List> rawData) { var activities = new List(); if (rawData == null || !rawData.Any()) return activities; foreach (var entry in rawData) { var processed = ProcessDictionaryEntry(entry); if (processed != null) activities.AddRange(processed); } return activities.OrderBy(a => a.Timestamp).ToList(); } /// /// 从JSON字符串解析活动数据(兼容原有功能) /// /// JSON格式的活动数据 /// 解析后的活动条目列表 public List ParseActivityDataFromJson(string jsonString) { var activities = new List(); if (string.IsNullOrEmpty(jsonString)) return activities; try { // 尝试解析为JSON数组 var jsonArray = JsonSerializer.Deserialize>(jsonString); foreach (var entry in jsonArray) { var processed = ProcessEntry(entry); if (processed != null) activities.AddRange(processed); } } catch (JsonException) { // 逐行解析 foreach (var line in jsonString.Split('\n')) { var trimmedLine = line.Trim(); if (string.IsNullOrEmpty(trimmedLine)) continue; try { var entry = JsonSerializer.Deserialize(trimmedLine); var processed = ProcessEntry(entry); if (processed != null) activities.AddRange(processed); } catch (JsonException) { // 忽略无法解析的行 } } } return activities.OrderBy(a => a.Timestamp).ToList(); } private List ProcessEntry(JsonElement entry) { try { var data = entry.TryGetProperty("data", out var dataElement) ? dataElement : entry; if (!data.TryGetProperty("app", out var appElement) || !data.TryGetProperty("title", out var titleElement)) return null; var app = appElement.GetString(); var title = titleElement.GetString(); if (string.IsNullOrEmpty(app) || string.IsNullOrEmpty(title)) return null; var duration = entry.TryGetProperty("duration", out var durationElement) ? durationElement.GetDouble() : 0.0; if (duration < _config.MinDuration || _config.ExcludedApps.Contains(app) || _config.ExcludedTitles.Any(keyword => title.Contains(keyword))) return null; var timestampStr = entry.TryGetProperty("timestamp", out var timestampElement) ? timestampElement.GetString() : ""; var parsedTimestamp = ParseTimestamp(timestampStr); if (!parsedTimestamp.HasValue) return null; return new List { new ActivityEntry { Timestamp = parsedTimestamp.Value, Duration = duration, App = app, Title = title, RawTitle = title } }; } catch (Exception) { return null; } } private List ProcessDictionaryEntry(Dictionary entry) { try { Dictionary data; if (entry.ContainsKey("data")) { if (entry["data"] is JsonElement je && je.ValueKind == JsonValueKind.Object) { // ✅ 将 JsonElement 转为 Dictionary data = JsonSerializer.Deserialize>(je.GetRawText()); } else if (entry["data"] is Dictionary dict) { data = dict; } else { return null; } } else { data = entry; } if (!data.ContainsKey("app") || !data.ContainsKey("title")) return null; var app = data["app"].ToString(); var title = data["title"].ToString(); if (string.IsNullOrEmpty(app) || string.IsNullOrEmpty(title)) return null; var duration = entry.ContainsKey("duration") ? GetValue(entry["duration"]) : 0.0; if (duration < _config.MinDuration || _config.ExcludedApps.Contains(app) || _config.ExcludedTitles.Any(keyword => title.Contains(keyword))) return null; var timestampStr = entry.ContainsKey("timestamp") ? entry["timestamp"].ToString() : ""; var parsedTimestamp = ParseTimestamp(timestampStr); if (!parsedTimestamp.HasValue) return null; return new List { new ActivityEntry { Timestamp = parsedTimestamp.Value, Duration = duration, App = app, Title = title, RawTitle = title } }; } catch (Exception e) { return null; } } private T GetValue(object value, T defaultValue = default) { try { if (value == null || value is DBNull) return defaultValue; // 处理 JsonElement 特殊情况 if (value is JsonElement je) { if (typeof(T) == typeof(string)) return (T)(object)je.ToString(); if (typeof(T) == typeof(int) && je.TryGetInt32(out var i)) return (T)(object)i; if (typeof(T) == typeof(long) && je.TryGetInt64(out var l)) return (T)(object)l; if (typeof(T) == typeof(double) && je.TryGetDouble(out var d)) return (T)(object)d; if (typeof(T) == typeof(float) && je.TryGetSingle(out var f)) return (T)(object)f; if (typeof(T) == typeof(DateTime) && je.TryGetDateTime(out var dt)) return (T)(object)dt; // fallback: 尝试反序列化整个对象 return JsonSerializer.Deserialize(je.GetRawText()); } // 如果已经是目标类型,直接返回 if (value is T tVal) return tVal; // 尝试 Convert.ChangeType return (T)Convert.ChangeType(value, typeof(T)); } catch { return defaultValue; } } private List SplitActivityAcrossSlices(ActivityEntry activity, int sliceDurationMinutes) { var startTime = activity.Timestamp; var duration = activity.Duration; var endTime = startTime.AddSeconds(duration); var sliceInterval = TimeSpan.FromMinutes(sliceDurationMinutes); // 计算开始时间片的边界 var startSlice = new DateTime(startTime.Year, startTime.Month, startTime.Day, startTime.Hour, (startTime.Minute / sliceDurationMinutes) * sliceDurationMinutes, 0); // 如果活动完全在一个时间片内,直接返回 var nextSliceBoundary = startSlice.Add(sliceInterval); if (endTime <= nextSliceBoundary) return new List { activity }; // 将活动分割到多个时间片 var splitActivities = new List(); var currentSliceStart = startSlice; var remainingDuration = duration; while (remainingDuration > 0 && currentSliceStart < endTime) { var currentSliceEnd = currentSliceStart.Add(sliceInterval); // 计算当前时间片中的活动开始时间 var activityStartInSlice = startTime > currentSliceStart ? startTime : currentSliceStart; var activityEndInSlice = endTime < currentSliceEnd ? endTime : currentSliceEnd; // 计算在当前时间片中的持续时间 var durationInSlice = (activityEndInSlice - activityStartInSlice).TotalSeconds; if (durationInSlice > 0) { splitActivities.Add(new ActivityEntry { Timestamp = activityStartInSlice, Duration = durationInSlice, App = activity.App, Title = activity.Title, RawTitle = activity.RawTitle }); } currentSliceStart = currentSliceEnd; remainingDuration -= durationInSlice; } return splitActivities; } /// /// 分析活动数据并生成时间片结果 /// /// 活动条目列表 /// 时间片分析结果 public List AnalyzeTimeSlices(List activities) { if (!activities.Any()) return new List(); var startTime = activities.First().Timestamp; var endTime = activities.Last().Timestamp; var interval = _config.TimeSliceMinutes; var sliceDict = new Dictionary(); // 分配活动到时间片 foreach (var activity in activities) { var splitActivities = SplitActivityAcrossSlices(activity, interval); foreach (var splitActivity in splitActivities) { var sliceKey = new DateTime(splitActivity.Timestamp.Year, splitActivity.Timestamp.Month, splitActivity.Timestamp.Day, splitActivity.Timestamp.Hour, (splitActivity.Timestamp.Minute / interval) * interval, 0); if (!sliceDict.ContainsKey(sliceKey)) sliceDict[sliceKey] = new TimeSliceData(); var sliceData = sliceDict[sliceKey]; sliceData.ActiveSeconds += splitActivity.Duration; if (!sliceData.AppUsage.ContainsKey(splitActivity.App)) sliceData.AppUsage[splitActivity.App] = 0; sliceData.AppUsage[splitActivity.App] += splitActivity.Duration; sliceData.WindowTitles.Add(splitActivity.RawTitle); sliceData.Events.Add(splitActivity); // 关键词分析 var titleLower = splitActivity.Title.ToLower(); if (_config.WorkKeywords.Any(kw => titleLower.Contains(kw.ToLower()))) sliceData.KeywordsMatched++; if (_config.DistractionKeywords.Any(kw => titleLower.Contains(kw.ToLower()))) sliceData.DistractionCount++; } } // 计算切换次数和主导应用 foreach (var kvp in sliceDict) { var data = kvp.Value; // 计算应用切换次数 if (data.Events.Count > 1) { var prevApp = data.Events[0].App; for (int i = 1; i < data.Events.Count; i++) { if (data.Events[i].App != prevApp) data.SwitchCount++; prevApp = data.Events[i].App; } } // 确定主导应用 if (data.AppUsage.Any()) { data.DominantProcess = data.AppUsage.OrderByDescending(x => x.Value).First().Key; } } // 构建最终输出 var results = new List(); var current = new DateTime(startTime.Year, startTime.Month, startTime.Day, startTime.Hour, (startTime.Minute / interval) * interval, 0); while (current <= endTime) { if (sliceDict.ContainsKey(current)) { var data = sliceDict[current]; // 计算专注分数 var maxSeconds = interval * 60; var timeUtilization = Math.Min(1.0, data.ActiveSeconds / maxSeconds); var switchPenalty = Math.Min(1.0, data.SwitchCount / 10.0); var keywordBoost = Math.Min(0.3, data.KeywordsMatched * 0.1); var distractionPenalty = Math.Min(0.5, data.DistractionCount * 0.2); var focusScore = (timeUtilization * 0.7 + keywordBoost) * (1 - switchPenalty) - distractionPenalty; focusScore = Math.Max(0.0, Math.Min(1.0, focusScore)); // 根据阈值确定专注状态 var focusState = "无活动"; if (data.ActiveSeconds > 0) { var sortedThresholds = _config.FocusThresholds.OrderByDescending(x => x.Value); foreach (var threshold in sortedThresholds) { if (focusScore >= threshold.Value) { focusState = threshold.Key; break; } } } var result = new TimeSliceResult { TimeSlice = current.ToString("yyyy-MM-dd HH:mm"), Timezone = _config.Timezone, DominantProcess = data.DominantProcess ?? "none", ActiveSeconds = Math.Round(data.ActiveSeconds, 1), SwitchCount = data.SwitchCount, KeywordsMatched = data.KeywordsMatched, DistractionEvents = data.DistractionCount, WindowTitles = data.WindowTitles.ToList(), FocusScore = Math.Round(focusScore, 2), FocusState = focusState, ProductivityScore = Math.Round(focusScore * timeUtilization * 100, 1) }; if (_config.OutputVerbose) { result.AppUtilization = data.AppUsage.ToDictionary( kvp => kvp.Key, kvp => Math.Round(kvp.Value, 1) ); } results.Add(result); } else { // 无活动的空时间片 results.Add(new TimeSliceResult { TimeSlice = current.ToString("yyyy-MM-dd HH:mm"), Timezone = _config.Timezone, DominantProcess = "none", ActiveSeconds = 0, SwitchCount = 0, KeywordsMatched = 0, DistractionEvents = 0, WindowTitles = new List(), FocusScore = 0.0, FocusState = "无活动", ProductivityScore = 0.0 }); } current = current.AddMinutes(interval); } return results; } /// /// 完整的分析流程:从ActivityWatchClient数据到时间片结果 /// /// ActivityWatchClient.GetEventsAsync()返回的数据 /// 时间片分析结果 public List AnalyzeFromRawData(List> rawData) { var activities = ParseActivityData(rawData); return AnalyzeTimeSlices(activities); } /// /// 将结果保存为JSON文件 /// /// 分析结果 /// 输出文件路径 public async Task SaveResultsToFileAsync(List results, string filePath) { var options = new JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; var jsonString = JsonSerializer.Serialize(results, options); await File.WriteAllTextAsync(filePath, jsonString); } /// /// 从配置文件加载配置 /// /// 配置文件路径 /// 配置对象 public static async Task LoadConfigFromFileAsync(string configFilePath) { try { var configJson = await File.ReadAllTextAsync(configFilePath); return JsonSerializer.Deserialize(configJson); } catch (Exception) { return new ActivityAnalyzerConfig(); } } } }