Skip to content

Commit 53f56e5

Browse files
committed
add obsidian
1 parent fb06f13 commit 53f56e5

16 files changed

Lines changed: 1821 additions & 13 deletions

App.config

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,26 @@
9797
<add key="DecisionWeightStrengthFit" value="1.0" />
9898
<add key="DecisionWeightEnergyFit" value="1.0" />
9999
<add key="DecisionWeightRiskPenalty" value="1.0" />
100+
<!-- 本地知识管理同步 -->
101+
<add key="KnowledgeSyncEnabled" value="true" />
102+
<add key="KnowledgeSyncIntervalMinutes" value="60" />
103+
<add key="ObsidianVaultPath" value="" />
104+
<add key="ObsidianIncludeSubfolders" value="true" />
105+
<add key="ObsidianMaxFilesPerSync" value="200" />
106+
<add key="KnowledgeRealtimeWatchEnabled" value="true" />
107+
<add key="KnowledgeSyncDebounceSeconds" value="8" />
108+
<add key="KnowledgeAutoImportEnabled" value="true" />
109+
<add key="KnowledgeAutoImportMinConfidence" value="0.90" />
110+
<add key="KnowledgeAutoImportMaxPerRun" value="6" />
111+
<add key="KnowledgeSmartNotifyEnabled" value="true" />
112+
<add key="KnowledgeRulesPath" value="configs\knowledge_rules.yaml" />
113+
<add key="KnowledgePromptTemplatePath" value="configs\task_extract_prompt.txt" />
114+
<add key="KnowledgeSyncStatePath" value="" />
115+
<add key="KnowledgeCaptureEnabled" value="true" />
116+
<add key="KnowledgeWritebackEnabled" value="true" />
117+
<add key="KnowledgeWritebackToSourceNote" value="true" />
118+
<add key="KnowledgeArtifactsPath" value="data\knowledge" />
119+
<add key="KnowledgeObsidianInboxNote" value="_TimeTask/Knowledge Inbox.md" />
100120
</appSettings>
101121
<userSettings>
102122
<TimeTask.Properties.Settings>

DraftViewerWindow.xaml.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,10 @@ private void AddDraftsToQuadrants(List<TaskDraft> drafts)
9292
ReminderTime = draft.ReminderTime,
9393
IsActiveInQuadrant = true,
9494
InactiveWarningCount = 0,
95-
Result = string.Empty
95+
Result = string.Empty,
96+
SourceTaskID = string.IsNullOrWhiteSpace(draft.SourceNotePath)
97+
? $"draft:{draft.Id}"
98+
: $"obsidian:{draft.SourceNotePath.Replace('\\', '/')}"
9699
};
97100

98101
items.Insert(0, newItem);

FunAsrRuntimeManager.cs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -963,20 +963,14 @@ private static FunAsrRuntimeBootstrapResult TryUsePrebuiltRuntime(string runtime
963963
return FunAsrRuntimeBootstrapResult.NotReady(string.Empty, "bundle-not-found");
964964
}
965965

966-
string prebuiltRoot = Path.Combine(runtimeRoot, "prebuilt");
967-
string marker = Path.Combine(prebuiltRoot, ".bundle.marker");
968966
string currentSignature = $"{bundle}|{new FileInfo(bundle).Length}|{File.GetLastWriteTimeUtc(bundle):o}";
969-
string existingSignature = File.Exists(marker) ? File.ReadAllText(marker, Encoding.UTF8).Trim() : string.Empty;
967+
string signatureFolder = ComputeStableFolderName(currentSignature);
968+
string prebuiltRoot = Path.Combine(runtimeRoot, "prebuilt", signatureFolder);
970969

971-
if (!Directory.Exists(prebuiltRoot) || !string.Equals(currentSignature, existingSignature, StringComparison.Ordinal))
970+
if (!Directory.Exists(prebuiltRoot))
972971
{
973-
if (Directory.Exists(prebuiltRoot))
974-
{
975-
try { Directory.Delete(prebuiltRoot, true); } catch { }
976-
}
977972
Directory.CreateDirectory(prebuiltRoot);
978973
ZipFile.ExtractToDirectory(bundle, prebuiltRoot);
979-
File.WriteAllText(marker, currentSignature, Encoding.UTF8);
980974
VoiceRuntimeLog.Info($"FunASR prebuilt runtime extracted: bundle={bundle}, target={prebuiltRoot}");
981975
}
982976

@@ -995,6 +989,23 @@ private static FunAsrRuntimeBootstrapResult TryUsePrebuiltRuntime(string runtime
995989
}
996990
}
997991

992+
private static string ComputeStableFolderName(string signature)
993+
{
994+
try
995+
{
996+
using (var sha1 = System.Security.Cryptography.SHA1.Create())
997+
{
998+
byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(signature ?? string.Empty));
999+
string hex = BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
1000+
return hex.Substring(0, 16);
1001+
}
1002+
}
1003+
catch
1004+
{
1005+
return "default";
1006+
}
1007+
}
1008+
9981009
private static string[] BuildBundleCandidates(string configuredBundlePath, string runtimeRoot)
9991010
{
10001011
var list = new System.Collections.Generic.List<string>();

KnowledgeArtifactService.cs

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
using System;
2+
using System.Configuration;
3+
using System.IO;
4+
using System.Security.Cryptography;
5+
using System.Text;
6+
7+
namespace TimeTask
8+
{
9+
public sealed class KnowledgeArtifactService
10+
{
11+
private readonly string _appRootPath;
12+
private readonly string _obsidianVaultPath;
13+
private readonly string _localArtifactsPath;
14+
private readonly string _obsidianInboxNote;
15+
private readonly bool _enabled;
16+
private readonly bool _writebackEnabled;
17+
private readonly bool _writebackToSourceNote;
18+
19+
private KnowledgeArtifactService(
20+
string appRootPath,
21+
string obsidianVaultPath,
22+
string localArtifactsPath,
23+
string obsidianInboxNote,
24+
bool enabled,
25+
bool writebackEnabled,
26+
bool writebackToSourceNote)
27+
{
28+
_appRootPath = appRootPath;
29+
_obsidianVaultPath = obsidianVaultPath;
30+
_localArtifactsPath = localArtifactsPath;
31+
_obsidianInboxNote = obsidianInboxNote;
32+
_enabled = enabled;
33+
_writebackEnabled = writebackEnabled;
34+
_writebackToSourceNote = writebackToSourceNote;
35+
}
36+
37+
public bool IsEnabled => _enabled;
38+
39+
public static KnowledgeArtifactService CreateFromAppSettings(string appRootPath)
40+
{
41+
string ReadRaw(string key, string defaultValue = "")
42+
{
43+
try
44+
{
45+
string value = ConfigurationManager.AppSettings[key];
46+
return string.IsNullOrWhiteSpace(value) ? defaultValue : value.Trim();
47+
}
48+
catch
49+
{
50+
return defaultValue;
51+
}
52+
}
53+
54+
bool ReadBool(string key, bool defaultValue)
55+
{
56+
string raw = ReadRaw(key, defaultValue.ToString());
57+
return bool.TryParse(raw, out bool value) ? value : defaultValue;
58+
}
59+
60+
string ResolvePath(string configuredPath, string fallbackRelativePath)
61+
{
62+
string path = string.IsNullOrWhiteSpace(configuredPath) ? fallbackRelativePath : configuredPath;
63+
if (Path.IsPathRooted(path))
64+
{
65+
return path;
66+
}
67+
return Path.Combine(appRootPath, path);
68+
}
69+
70+
bool enabled = ReadBool("KnowledgeCaptureEnabled", true);
71+
string obsidianVaultPath = ReadRaw("ObsidianVaultPath", string.Empty);
72+
string localArtifactsPath = ResolvePath(ReadRaw("KnowledgeArtifactsPath"), @"data\knowledge");
73+
string obsidianInboxNote = ReadRaw("KnowledgeObsidianInboxNote", "_TimeTask/Knowledge Inbox.md");
74+
bool writebackEnabled = ReadBool("KnowledgeWritebackEnabled", true);
75+
bool writebackToSource = ReadBool("KnowledgeWritebackToSourceNote", true);
76+
77+
return new KnowledgeArtifactService(
78+
appRootPath,
79+
obsidianVaultPath,
80+
localArtifactsPath,
81+
obsidianInboxNote.Replace('\\', '/'),
82+
enabled,
83+
writebackEnabled,
84+
writebackToSource);
85+
}
86+
87+
public void CaptureCompletion(ItemGrid task)
88+
{
89+
if (!_enabled || task == null || string.IsNullOrWhiteSpace(task.Task))
90+
{
91+
return;
92+
}
93+
94+
string artifactId = BuildArtifactId(task);
95+
string markdown = BuildArtifactMarkdown(task, artifactId);
96+
SaveLocalArtifact(markdown);
97+
98+
if (_writebackEnabled)
99+
{
100+
TryWritebackToObsidian(task, artifactId, markdown);
101+
}
102+
}
103+
104+
private static string BuildArtifactId(ItemGrid task)
105+
{
106+
string raw = $"{task.Task}|{task.CompletionTime?.ToUniversalTime().Ticks ?? DateTime.UtcNow.Ticks}|{task.SourceTaskID}";
107+
using (var sha1 = SHA1.Create())
108+
{
109+
byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(raw));
110+
return BitConverter.ToString(hash).Replace("-", string.Empty).Substring(0, 12).ToLowerInvariant();
111+
}
112+
}
113+
114+
private static string BuildArtifactMarkdown(ItemGrid task, string artifactId)
115+
{
116+
DateTime completedAt = task.CompletionTime ?? DateTime.Now;
117+
string source = string.IsNullOrWhiteSpace(task.SourceTaskID) ? "manual" : task.SourceTaskID;
118+
string quadrant = $"{task.Importance}/{task.Urgency}";
119+
var sb = new StringBuilder();
120+
sb.AppendLine($"## Task Recap - {task.Task}");
121+
sb.AppendLine();
122+
sb.AppendLine($"- artifact_id: {artifactId}");
123+
sb.AppendLine($"- completed_at: {completedAt:yyyy-MM-dd HH:mm:ss}");
124+
sb.AppendLine($"- source: {source}");
125+
sb.AppendLine($"- quadrant: {quadrant}");
126+
sb.AppendLine();
127+
sb.AppendLine("### Summary");
128+
sb.AppendLine("- Outcome: Completed");
129+
sb.AppendLine("- Notes: Add key learnings or pitfalls here.");
130+
sb.AppendLine();
131+
return sb.ToString();
132+
}
133+
134+
private void SaveLocalArtifact(string markdown)
135+
{
136+
string monthFile = $"{DateTime.Now:yyyy-MM}.md";
137+
Directory.CreateDirectory(_localArtifactsPath);
138+
string path = Path.Combine(_localArtifactsPath, monthFile);
139+
File.AppendAllText(path, markdown + Environment.NewLine, Encoding.UTF8);
140+
}
141+
142+
private void TryWritebackToObsidian(ItemGrid task, string artifactId, string markdown)
143+
{
144+
if (string.IsNullOrWhiteSpace(_obsidianVaultPath) || !Directory.Exists(_obsidianVaultPath))
145+
{
146+
return;
147+
}
148+
149+
string marker = $"<!-- TIMETASK:ARTIFACT:{artifactId} -->";
150+
string content = marker + Environment.NewLine + markdown;
151+
string completedLine = BuildCompletedTaskLine(task);
152+
153+
string inboxPath = Path.Combine(_obsidianVaultPath, _obsidianInboxNote.Replace('/', Path.DirectorySeparatorChar));
154+
AppendIfNotExists(inboxPath, marker, content);
155+
UpsertManagedTaskLine(inboxPath, completedLine);
156+
157+
if (_writebackToSourceNote && TryResolveSourceNotePath(task.SourceTaskID, out string sourceNoteRelativePath))
158+
{
159+
string sourcePath = Path.Combine(_obsidianVaultPath, sourceNoteRelativePath.Replace('/', Path.DirectorySeparatorChar));
160+
AppendIfNotExists(sourcePath, marker, content);
161+
UpsertManagedTaskLine(sourcePath, completedLine);
162+
}
163+
}
164+
165+
private static string BuildCompletedTaskLine(ItemGrid task)
166+
{
167+
DateTime completedAt = task.CompletionTime ?? DateTime.Now;
168+
return $"- [x] {task.Task} (completed: {completedAt:yyyy-MM-dd})";
169+
}
170+
171+
private static bool TryResolveSourceNotePath(string sourceTaskId, out string relativePath)
172+
{
173+
relativePath = null;
174+
if (string.IsNullOrWhiteSpace(sourceTaskId))
175+
{
176+
return false;
177+
}
178+
179+
const string prefix = "obsidian:";
180+
if (!sourceTaskId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
181+
{
182+
return false;
183+
}
184+
185+
relativePath = sourceTaskId.Substring(prefix.Length).Trim();
186+
return !string.IsNullOrWhiteSpace(relativePath);
187+
}
188+
189+
private static void AppendIfNotExists(string filePath, string marker, string content)
190+
{
191+
try
192+
{
193+
string directory = Path.GetDirectoryName(filePath);
194+
if (!string.IsNullOrWhiteSpace(directory))
195+
{
196+
Directory.CreateDirectory(directory);
197+
}
198+
199+
string existing = File.Exists(filePath) ? File.ReadAllText(filePath, Encoding.UTF8) : string.Empty;
200+
if (existing.IndexOf(marker, StringComparison.OrdinalIgnoreCase) >= 0)
201+
{
202+
return;
203+
}
204+
205+
string block = Environment.NewLine + content + Environment.NewLine;
206+
File.AppendAllText(filePath, block, Encoding.UTF8);
207+
}
208+
catch
209+
{
210+
}
211+
}
212+
213+
private static void UpsertManagedTaskLine(string filePath, string line)
214+
{
215+
const string begin = "<!-- TIMETASK:BEGIN -->";
216+
const string end = "<!-- TIMETASK:END -->";
217+
218+
try
219+
{
220+
string directory = Path.GetDirectoryName(filePath);
221+
if (!string.IsNullOrWhiteSpace(directory))
222+
{
223+
Directory.CreateDirectory(directory);
224+
}
225+
226+
string existing = File.Exists(filePath) ? File.ReadAllText(filePath, Encoding.UTF8) : string.Empty;
227+
int beginIdx = existing.IndexOf(begin, StringComparison.Ordinal);
228+
int endIdx = existing.IndexOf(end, StringComparison.Ordinal);
229+
230+
if (beginIdx >= 0 && endIdx > beginIdx)
231+
{
232+
int contentStart = beginIdx + begin.Length;
233+
string block = existing.Substring(contentStart, endIdx - contentStart);
234+
if (block.IndexOf(line, StringComparison.OrdinalIgnoreCase) >= 0)
235+
{
236+
return;
237+
}
238+
239+
string updatedBlock = block.TrimEnd() + Environment.NewLine + line + Environment.NewLine;
240+
string updated = existing.Substring(0, contentStart) + Environment.NewLine + updatedBlock + existing.Substring(endIdx);
241+
File.WriteAllText(filePath, updated, Encoding.UTF8);
242+
return;
243+
}
244+
245+
string managedBlock = Environment.NewLine + begin + Environment.NewLine + line + Environment.NewLine + end + Environment.NewLine;
246+
File.AppendAllText(filePath, managedBlock, Encoding.UTF8);
247+
}
248+
catch
249+
{
250+
}
251+
}
252+
}
253+
}

0 commit comments

Comments
 (0)