Skip to content

Commit fb06f13

Browse files
committed
add life target
1 parent d7b1584 commit fb06f13

11 files changed

Lines changed: 1324 additions & 1 deletion

App.config

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,16 @@
8787
<!-- 安静时段(24小时制,跨天区间) -->
8888
<add key="QuietHoursStart" value="22" />
8989
<add key="QuietHoursEnd" value="8" />
90+
<!-- 策略层配置 -->
91+
<add key="StrategyCycleIntervalMinutes" value="30" />
92+
<!-- 1~7 对应 周一~周日 -->
93+
<add key="WeeklyReviewPublishDay" value="1" />
94+
<add key="StrategyTopFocusCount" value="5" />
95+
<add key="DecisionWeightLongTerm" value="0.45" />
96+
<add key="DecisionWeightUrgency" value="0.35" />
97+
<add key="DecisionWeightStrengthFit" value="1.0" />
98+
<add key="DecisionWeightEnergyFit" value="1.0" />
99+
<add key="DecisionWeightRiskPenalty" value="1.0" />
90100
</appSettings>
91101
<userSettings>
92102
<TimeTask.Properties.Settings>

DecisionEngine.cs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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+
}

GoalHierarchyEngine.cs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text.Json;
6+
using System.Text.RegularExpressions;
7+
8+
namespace TimeTask
9+
{
10+
public class GoalHierarchyItem
11+
{
12+
public string GoalId { get; set; }
13+
public string GoalDescription { get; set; }
14+
public string TimeHorizon { get; set; }
15+
public List<string> YearlyThemes { get; set; } = new List<string>();
16+
public List<string> QuarterlyMilestones { get; set; } = new List<string>();
17+
public List<string> WeeklyCommitments { get; set; } = new List<string>();
18+
}
19+
20+
public class GoalHierarchySnapshot
21+
{
22+
public DateTime GeneratedAt { get; set; } = DateTime.Now;
23+
public List<GoalHierarchyItem> Goals { get; set; } = new List<GoalHierarchyItem>();
24+
}
25+
26+
public class GoalHierarchyEngine
27+
{
28+
private readonly string _filePath;
29+
private readonly object _sync = new object();
30+
31+
public GoalHierarchyEngine(string dataPath)
32+
{
33+
string strategyPath = Path.Combine(dataPath, "strategy");
34+
Directory.CreateDirectory(strategyPath);
35+
_filePath = Path.Combine(strategyPath, "goal_hierarchy.json");
36+
}
37+
38+
public GoalHierarchySnapshot BuildAndPersist(List<LongTermGoal> activeGoals, List<ItemGrid> allTasks, DateTime now)
39+
{
40+
lock (_sync)
41+
{
42+
var snapshot = Build(activeGoals, allTasks, now);
43+
try
44+
{
45+
var options = new JsonSerializerOptions { WriteIndented = true };
46+
File.WriteAllText(_filePath, JsonSerializer.Serialize(snapshot, options));
47+
}
48+
catch (Exception ex)
49+
{
50+
Console.WriteLine($"GoalHierarchyEngine persist failed: {ex.Message}");
51+
}
52+
return snapshot;
53+
}
54+
}
55+
56+
private static GoalHierarchySnapshot Build(List<LongTermGoal> activeGoals, List<ItemGrid> allTasks, DateTime now)
57+
{
58+
var goals = activeGoals ?? new List<LongTermGoal>();
59+
var tasks = allTasks ?? new List<ItemGrid>();
60+
var result = new GoalHierarchySnapshot { GeneratedAt = now };
61+
62+
foreach (var goal in goals.Where(g => g != null && g.IsActive))
63+
{
64+
var related = tasks
65+
.Where(t => t != null && t.IsActive && string.Equals(t.LongTermGoalId, goal.Id, StringComparison.OrdinalIgnoreCase))
66+
.OrderBy(t => t.OriginalScheduledDay > 0 ? t.OriginalScheduledDay : int.MaxValue)
67+
.ThenBy(t => t.CreatedDate)
68+
.ToList();
69+
70+
int durationDays = ParseDurationDays(goal.TotalDuration);
71+
string horizon = durationDays <= 120 ? "1年内目标" : durationDays <= 365 ? "1-3年目标" : "3年以上目标";
72+
73+
var yearlyThemes = BuildYearlyThemes(goal, related);
74+
var milestones = BuildQuarterlyMilestones(related);
75+
var weekly = related.Take(5).Select(t => t.Task).Where(t => !string.IsNullOrWhiteSpace(t)).Distinct().ToList();
76+
77+
result.Goals.Add(new GoalHierarchyItem
78+
{
79+
GoalId = goal.Id,
80+
GoalDescription = goal.Description,
81+
TimeHorizon = horizon,
82+
YearlyThemes = yearlyThemes,
83+
QuarterlyMilestones = milestones,
84+
WeeklyCommitments = weekly
85+
});
86+
}
87+
88+
return result;
89+
}
90+
91+
private static List<string> BuildYearlyThemes(LongTermGoal goal, List<ItemGrid> related)
92+
{
93+
var themes = new List<string>();
94+
if (goal != null && goal.IsLearningPlan)
95+
{
96+
themes.Add("能力建设与系统学习");
97+
}
98+
99+
int highImportant = related.Count(t => string.Equals(t.Importance, "High", StringComparison.OrdinalIgnoreCase));
100+
if (highImportant >= Math.Max(1, related.Count / 2))
101+
{
102+
themes.Add("高价值任务优先推进");
103+
}
104+
105+
var keywordThemes = related
106+
.SelectMany(t => (t.Task ?? string.Empty).Split(new[] { ' ', ',', ',', '。', ';', ';', '-', '_', '/', '\\' }, StringSplitOptions.RemoveEmptyEntries))
107+
.Where(w => w.Length >= 2)
108+
.GroupBy(w => w, StringComparer.OrdinalIgnoreCase)
109+
.OrderByDescending(g => g.Count())
110+
.Take(2)
111+
.Select(g => $"围绕「{g.Key}」持续深化")
112+
.ToList();
113+
themes.AddRange(keywordThemes);
114+
115+
if (!themes.Any())
116+
{
117+
themes.Add("聚焦核心目标,持续推进关键里程碑");
118+
}
119+
return themes.Distinct().Take(4).ToList();
120+
}
121+
122+
private static List<string> BuildQuarterlyMilestones(List<ItemGrid> related)
123+
{
124+
var result = new List<string>();
125+
if (related == null || !related.Any())
126+
{
127+
return result;
128+
}
129+
130+
var grouped = related
131+
.Select(t =>
132+
{
133+
int day = t.OriginalScheduledDay > 0 ? t.OriginalScheduledDay : 90;
134+
int q = Math.Min(4, Math.Max(1, ((day - 1) / 30) + 1));
135+
return new { Quarter = q, Task = t.Task };
136+
})
137+
.GroupBy(x => x.Quarter)
138+
.OrderBy(g => g.Key);
139+
140+
foreach (var g in grouped)
141+
{
142+
string picks = string.Join("、", g.Select(x => x.Task).Where(t => !string.IsNullOrWhiteSpace(t)).Take(2));
143+
if (string.IsNullOrWhiteSpace(picks))
144+
{
145+
continue;
146+
}
147+
result.Add($"Q{g.Key}: 完成 {picks}");
148+
}
149+
return result.Take(8).ToList();
150+
}
151+
152+
private static int ParseDurationDays(string text)
153+
{
154+
if (string.IsNullOrWhiteSpace(text))
155+
{
156+
return 90;
157+
}
158+
159+
var match = Regex.Match(text, @"\d+");
160+
if (!match.Success || !int.TryParse(match.Value, out int value))
161+
{
162+
return 90;
163+
}
164+
165+
if (text.Contains("年")) return value * 365;
166+
if (text.Contains("月")) return value * 30;
167+
if (text.Contains("周")) return value * 7;
168+
return value;
169+
}
170+
}
171+
}

0 commit comments

Comments
 (0)