Files
RhythmicWallpaper/AudioVisualizer/ActivityWatch/ActivityAnalyzer.cs

498 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, string>
{
{ "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;
}
}
/// <summary>
/// 解析从ActivityWatchClient获取的原始活动数据
/// </summary>
/// <param name="rawData">ActivityWatchClient.GetEventsAsync()返回的数据</param>
/// <returns>解析后的活动条目列表</returns>
public List<ActivityEntry> ParseActivityData(List<Dictionary<string, object>> rawData) {
var activities = new List<ActivityEntry>();
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();
}
/// <summary>
/// 从JSON字符串解析活动数据兼容原有功能
/// </summary>
/// <param name="jsonString">JSON格式的活动数据</param>
/// <returns>解析后的活动条目列表</returns>
public List<ActivityEntry> ParseActivityDataFromJson(string jsonString) {
var activities = new List<ActivityEntry>();
if (string.IsNullOrEmpty(jsonString))
return activities;
try {
// 尝试解析为JSON数组
var jsonArray = JsonSerializer.Deserialize<List<JsonElement>>(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<JsonElement>(trimmedLine);
var processed = ProcessEntry(entry);
if (processed != null)
activities.AddRange(processed);
} catch (JsonException) {
// 忽略无法解析的行
}
}
}
return activities.OrderBy(a => a.Timestamp).ToList();
}
private List<ActivityEntry> 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<ActivityEntry>
{
new ActivityEntry
{
Timestamp = parsedTimestamp.Value,
Duration = duration,
App = app,
Title = title,
RawTitle = title
}
};
} catch (Exception) {
return null;
}
}
private List<ActivityEntry> ProcessDictionaryEntry(Dictionary<string, object> entry) {
try {
Dictionary<string, object> data;
if (entry.ContainsKey("data")) {
if (entry["data"] is JsonElement je && je.ValueKind == JsonValueKind.Object) {
// ✅ 将 JsonElement 转为 Dictionary<string, object>
data = JsonSerializer.Deserialize<Dictionary<string, object>>(je.GetRawText());
} else if (entry["data"] is Dictionary<string, object> 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<double>(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<ActivityEntry>
{
new ActivityEntry
{
Timestamp = parsedTimestamp.Value,
Duration = duration,
App = app,
Title = title,
RawTitle = title
}
};
} catch (Exception e) {
return null;
}
}
private T GetValue<T>(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<T>(je.GetRawText());
}
// 如果已经是目标类型,直接返回
if (value is T tVal)
return tVal;
// 尝试 Convert.ChangeType
return (T)Convert.ChangeType(value, typeof(T));
} catch {
return defaultValue;
}
}
private List<ActivityEntry> 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<ActivityEntry> { activity };
// 将活动分割到多个时间片
var splitActivities = new List<ActivityEntry>();
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;
}
/// <summary>
/// 分析活动数据并生成时间片结果
/// </summary>
/// <param name="activities">活动条目列表</param>
/// <returns>时间片分析结果</returns>
public List<TimeSliceResult> AnalyzeTimeSlices(List<ActivityEntry> activities) {
if (!activities.Any())
return new List<TimeSliceResult>();
var startTime = activities.First().Timestamp;
var endTime = activities.Last().Timestamp;
var interval = _config.TimeSliceMinutes;
var sliceDict = new Dictionary<DateTime, TimeSliceData>();
// 分配活动到时间片
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<TimeSliceResult>();
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<string>(),
FocusScore = 0.0,
FocusState = "无活动",
ProductivityScore = 0.0
});
}
current = current.AddMinutes(interval);
}
return results;
}
/// <summary>
/// 完整的分析流程从ActivityWatchClient数据到时间片结果
/// </summary>
/// <param name="rawData">ActivityWatchClient.GetEventsAsync()返回的数据</param>
/// <returns>时间片分析结果</returns>
public List<TimeSliceResult> AnalyzeFromRawData(List<Dictionary<string, object>> rawData) {
var activities = ParseActivityData(rawData);
return AnalyzeTimeSlices(activities);
}
/// <summary>
/// 将结果保存为JSON文件
/// </summary>
/// <param name="results">分析结果</param>
/// <param name="filePath">输出文件路径</param>
public async Task SaveResultsToFileAsync(List<TimeSliceResult> 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);
}
/// <summary>
/// 从配置文件加载配置
/// </summary>
/// <param name="configFilePath">配置文件路径</param>
/// <returns>配置对象</returns>
public static async Task<ActivityAnalyzerConfig> LoadConfigFromFileAsync(string configFilePath) {
try {
var configJson = await File.ReadAllTextAsync(configFilePath);
return JsonSerializer.Deserialize<ActivityAnalyzerConfig>(configJson);
} catch (Exception) {
return new ActivityAnalyzerConfig();
}
}
}
}