Implement meeting assistant v1

This commit is contained in:
2026-05-20 02:06:16 +02:00
parent 90df1edc03
commit 0297bcc0f6
120 changed files with 11883 additions and 180 deletions
@@ -0,0 +1,126 @@
using MeetingAssistant.MeetingNotes;
using Microsoft.Extensions.Options;
namespace MeetingAssistant.Summary;
public interface IMeetingSummaryArtifactResolver
{
Task<MeetingSessionArtifacts?> ResolveBySummaryPathAsync(
string summaryPath,
CancellationToken cancellationToken);
}
public sealed class MeetingSummaryArtifactResolver : IMeetingSummaryArtifactResolver
{
private readonly MeetingAssistantOptions options;
private readonly IMeetingNoteStore meetingNoteStore;
public MeetingSummaryArtifactResolver(
IOptions<MeetingAssistantOptions> options,
IMeetingNoteStore meetingNoteStore)
{
this.options = options.Value;
this.meetingNoteStore = meetingNoteStore;
}
public async Task<MeetingSessionArtifacts?> ResolveBySummaryPathAsync(
string summaryPath,
CancellationToken cancellationToken)
{
var requestedSummaryPath = ResolveRequestedSummaryPath(summaryPath);
if (requestedSummaryPath is null)
{
return null;
}
var meetingNotesFolder = VaultPath.Resolve(options.Vault.MeetingNotesFolder);
if (!Directory.Exists(meetingNotesFolder))
{
return null;
}
foreach (var meetingNotePath in Directory.EnumerateFiles(meetingNotesFolder, "*.md"))
{
var note = await meetingNoteStore.ReadAsync(meetingNotePath, cancellationToken);
var noteSummaryPath = ResolveNoteLink(note.Frontmatter.Summary, note.Path);
if (!PathsEqual(requestedSummaryPath, noteSummaryPath))
{
continue;
}
return new MeetingSessionArtifacts(
note.Path,
ResolveNoteLink(note.Frontmatter.Transcript, note.Path),
ResolveNoteLink(note.Frontmatter.AssistantContext, note.Path),
requestedSummaryPath);
}
return null;
}
private string? ResolveRequestedSummaryPath(string summaryPath)
{
if (string.IsNullOrWhiteSpace(summaryPath))
{
return null;
}
var summariesFolder = VaultPath.Resolve(options.Vault.SummariesFolder);
var requested = Environment.ExpandEnvironmentVariables(summaryPath);
var fullPath = Path.IsPathRooted(requested)
? Path.GetFullPath(requested)
: Path.GetFullPath(Path.Combine(summariesFolder, requested));
return IsWithinDirectory(summariesFolder, fullPath) ? fullPath : null;
}
private static string ResolveNoteLink(string pathOrLink, string sourceNotePath)
{
if (string.IsNullOrWhiteSpace(pathOrLink))
{
return "";
}
var sourceFolder = Path.GetDirectoryName(sourceNotePath) ?? "";
var target = ExtractWikiLinkTarget(pathOrLink) ?? pathOrLink;
target = target.Replace('/', Path.DirectorySeparatorChar);
if (!Path.HasExtension(target))
{
target += ".md";
}
return Path.GetFullPath(Path.IsPathRooted(target)
? target
: Path.Combine(sourceFolder, target));
}
private static string? ExtractWikiLinkTarget(string value)
{
if (!value.StartsWith("[[", StringComparison.Ordinal) ||
!value.EndsWith("]]", StringComparison.Ordinal))
{
return null;
}
var inner = value[2..^2];
var labelSeparator = inner.IndexOf('|', StringComparison.Ordinal);
return labelSeparator < 0 ? inner : inner[..labelSeparator];
}
private static bool PathsEqual(string left, string right)
{
return string.Equals(
Path.GetFullPath(left),
Path.GetFullPath(right),
StringComparison.OrdinalIgnoreCase);
}
private static bool IsWithinDirectory(string directory, string path)
{
var normalizedDirectory = Path.GetFullPath(directory)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
var normalizedPath = Path.GetFullPath(path);
return normalizedPath.StartsWith(normalizedDirectory, StringComparison.OrdinalIgnoreCase);
}
}