|
| 1 | +using System; |
| 2 | +using System.Collections.Generic; |
| 3 | +using System.IO; |
| 4 | +using System.Linq; |
| 5 | +using System.Text.Json; |
| 6 | + |
| 7 | +namespace TimeTask |
| 8 | +{ |
| 9 | + public class DecisionEngineOptions |
| 10 | + { |
| 11 | + public double LongTermWeight { get; set; } = 0.45; |
| 12 | + public double UrgencyWeight { get; set; } = 0.35; |
| 13 | + public double StrengthFitWeight { get; set; } = 1.0; |
| 14 | + public double EnergyFitWeight { get; set; } = 1.0; |
| 15 | + public double RiskPenaltyWeight { get; set; } = 1.0; |
| 16 | + public int SnapshotTopCount { get; set; } = 5; |
| 17 | + } |
| 18 | + |
| 19 | + public class TaskDecisionScore |
| 20 | + { |
| 21 | + public string TaskName { get; set; } |
| 22 | + public string GoalId { get; set; } |
| 23 | + public string Importance { get; set; } |
| 24 | + public string Urgency { get; set; } |
| 25 | + public double Score { get; set; } |
| 26 | + public double LongTermValue { get; set; } |
| 27 | + public double UrgencyValue { get; set; } |
| 28 | + public double StrengthFit { get; set; } |
| 29 | + public double EnergyFit { get; set; } |
| 30 | + public double RiskPenalty { get; set; } |
| 31 | + public List<string> Reasons { get; set; } = new List<string>(); |
| 32 | + } |
| 33 | + |
| 34 | + public class DecisionEngine |
| 35 | + { |
| 36 | + private readonly string _decisionPath; |
| 37 | + private readonly object _sync = new object(); |
| 38 | + private DecisionEngineOptions _options; |
| 39 | + |
| 40 | + public DecisionEngine(string dataPath, DecisionEngineOptions options = null) |
| 41 | + { |
| 42 | + string strategyPath = Path.Combine(dataPath, "strategy"); |
| 43 | + Directory.CreateDirectory(strategyPath); |
| 44 | + _decisionPath = Path.Combine(strategyPath, "decision_focus_snapshot.json"); |
| 45 | + _options = options ?? new DecisionEngineOptions(); |
| 46 | + } |
| 47 | + |
| 48 | + public void SetOptions(DecisionEngineOptions options) |
| 49 | + { |
| 50 | + _options = options ?? new DecisionEngineOptions(); |
| 51 | + } |
| 52 | + |
| 53 | + public List<TaskDecisionScore> RankTasks(List<ItemGrid> tasks, LifeProfileSnapshot lifeProfile, string activeGoalId, DateTime now) |
| 54 | + { |
| 55 | + tasks ??= new List<ItemGrid>(); |
| 56 | + var activeTasks = tasks.Where(t => t != null && t.IsActive).ToList(); |
| 57 | + lifeProfile ??= new LifeProfileSnapshot(); |
| 58 | + |
| 59 | + var ranked = activeTasks |
| 60 | + .Select(t => BuildScore(t, lifeProfile, activeGoalId, now, _options)) |
| 61 | + .OrderByDescending(s => s.Score) |
| 62 | + .ThenBy(s => s.TaskName, StringComparer.OrdinalIgnoreCase) |
| 63 | + .ToList(); |
| 64 | + |
| 65 | + return ranked; |
| 66 | + } |
| 67 | + |
| 68 | + public string BuildFocusBrief(List<TaskDecisionScore> ranked, int topN = 3) |
| 69 | + { |
| 70 | + if (ranked == null || ranked.Count == 0) |
| 71 | + { |
| 72 | + return "当前没有可聚焦的活跃任务。"; |
| 73 | + } |
| 74 | + |
| 75 | + int take = Math.Max(1, Math.Min(topN, ranked.Count)); |
| 76 | + var top = ranked.Take(take).ToList(); |
| 77 | + string head = string.Join("、", top.Select(t => t.TaskName)); |
| 78 | + return $"本周期建议聚焦:{head}"; |
| 79 | + } |
| 80 | + |
| 81 | + public void PersistSnapshot(List<TaskDecisionScore> ranked, DateTime now) |
| 82 | + { |
| 83 | + if (ranked == null) |
| 84 | + { |
| 85 | + return; |
| 86 | + } |
| 87 | + |
| 88 | + lock (_sync) |
| 89 | + { |
| 90 | + try |
| 91 | + { |
| 92 | + var payload = new |
| 93 | + { |
| 94 | + generatedAt = now, |
| 95 | + top = ranked.Take(Math.Max(1, _options.SnapshotTopCount)).ToList() |
| 96 | + }; |
| 97 | + var options = new JsonSerializerOptions { WriteIndented = true }; |
| 98 | + File.WriteAllText(_decisionPath, JsonSerializer.Serialize(payload, options)); |
| 99 | + } |
| 100 | + catch (Exception ex) |
| 101 | + { |
| 102 | + Console.WriteLine($"DecisionEngine persist failed: {ex.Message}"); |
| 103 | + } |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + private static TaskDecisionScore BuildScore(ItemGrid task, LifeProfileSnapshot lifeProfile, string activeGoalId, DateTime now, DecisionEngineOptions options) |
| 108 | + { |
| 109 | + options ??= new DecisionEngineOptions(); |
| 110 | + double longTerm = 0.15; |
| 111 | + if (!string.IsNullOrWhiteSpace(task.LongTermGoalId)) |
| 112 | + { |
| 113 | + longTerm += 0.55; |
| 114 | + if (string.Equals(task.LongTermGoalId, activeGoalId, StringComparison.OrdinalIgnoreCase)) |
| 115 | + { |
| 116 | + longTerm += 0.15; |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + bool highImportance = string.Equals(task.Importance, "High", StringComparison.OrdinalIgnoreCase); |
| 121 | + bool highUrgency = string.Equals(task.Urgency, "High", StringComparison.OrdinalIgnoreCase); |
| 122 | + double urgency = (highImportance, highUrgency) switch |
| 123 | + { |
| 124 | + (true, true) => 0.65, |
| 125 | + (true, false) => 0.55, |
| 126 | + (false, true) => 0.45, |
| 127 | + _ => 0.25 |
| 128 | + }; |
| 129 | + |
| 130 | + bool hasGoal = !string.IsNullOrWhiteSpace(task.LongTermGoalId); |
| 131 | + bool strengthGoalOrientation = (lifeProfile.Strengths ?? new List<string>()) |
| 132 | + .Any(s => string.Equals(s, "goal_oriented", StringComparison.OrdinalIgnoreCase)); |
| 133 | + double strengthFit = hasGoal && strengthGoalOrientation ? 0.15 : 0.0; |
| 134 | + |
| 135 | + int currentHour = now.Hour; |
| 136 | + var peakHours = lifeProfile.PeakHours ?? new List<int>(); |
| 137 | + bool inPeakWindow = peakHours.Any(h => Math.Abs(h - currentHour) <= 1); |
| 138 | + double energyFit = inPeakWindow ? 0.12 : 0.02; |
| 139 | + |
| 140 | + double riskPenalty = 0.0; |
| 141 | + if (task.LastProgressDate < now.AddDays(-3)) |
| 142 | + { |
| 143 | + riskPenalty += 0.1; |
| 144 | + } |
| 145 | + if (!hasGoal && !highUrgency) |
| 146 | + { |
| 147 | + riskPenalty += 0.08; |
| 148 | + } |
| 149 | + if ((lifeProfile.RiskTriggers ?? new List<string>()) |
| 150 | + .Any(s => string.Equals(s, "urgency_overload", StringComparison.OrdinalIgnoreCase)) && |
| 151 | + highUrgency && !hasGoal) |
| 152 | + { |
| 153 | + riskPenalty += 0.08; |
| 154 | + } |
| 155 | + |
| 156 | + double score = (longTerm * options.LongTermWeight) |
| 157 | + + (urgency * options.UrgencyWeight) |
| 158 | + + (strengthFit * options.StrengthFitWeight) |
| 159 | + + (energyFit * options.EnergyFitWeight) |
| 160 | + - (riskPenalty * options.RiskPenaltyWeight); |
| 161 | + |
| 162 | + var reasons = new List<string>(); |
| 163 | + if (hasGoal) reasons.Add("绑定长期目标"); |
| 164 | + if (highImportance && highUrgency) reasons.Add("重要且紧急"); |
| 165 | + else if (highImportance) reasons.Add("重要性高"); |
| 166 | + else if (highUrgency) reasons.Add("紧急性高"); |
| 167 | + if (inPeakWindow) reasons.Add("匹配高效时段"); |
| 168 | + if (riskPenalty > 0.12) reasons.Add("存在执行风险"); |
| 169 | + |
| 170 | + return new TaskDecisionScore |
| 171 | + { |
| 172 | + TaskName = task.Task ?? "(未命名任务)", |
| 173 | + GoalId = task.LongTermGoalId, |
| 174 | + Importance = task.Importance, |
| 175 | + Urgency = task.Urgency, |
| 176 | + Score = Math.Round(score, 4), |
| 177 | + LongTermValue = Math.Round(longTerm, 4), |
| 178 | + UrgencyValue = Math.Round(urgency, 4), |
| 179 | + StrengthFit = Math.Round(strengthFit, 4), |
| 180 | + EnergyFit = Math.Round(energyFit, 4), |
| 181 | + RiskPenalty = Math.Round(riskPenalty, 4), |
| 182 | + Reasons = reasons |
| 183 | + }; |
| 184 | + } |
| 185 | + } |
| 186 | +} |
0 commit comments