177 lines
6.3 KiB
C#
177 lines
6.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Volo.Abp.Domain.Services;
|
|
|
|
namespace Sozsoft.Mcp.Services;
|
|
|
|
/// <summary>
|
|
/// Modelin ekran ve bilesen uretirken okudugu sozlesme: depodaki
|
|
/// <c>.github/instructions/lowcode*.instructions.md</c> dosyalari. Claude Code ayni dosyalari
|
|
/// depodan okur; MCP istemcisi ise bu servisten bolum bolum alir, boylece iki yolun bildigi
|
|
/// sozlesme ayrisamaz.
|
|
/// </summary>
|
|
public interface IMcpDevGuide
|
|
{
|
|
/// <summary>Okunabilen belgelerin baslik agaci; klasor bulunamazsa bos.</summary>
|
|
Task<IReadOnlyList<McpDevGuideHeading>> GetHeadingsAsync();
|
|
|
|
/// <summary>
|
|
/// Bir bolumun metni. <paramref name="section"/> bolum numarasi (<c>4.3</c>) ya da baslik
|
|
/// metninin bir parcasidir; bulunamazsa null.
|
|
/// </summary>
|
|
Task<McpDevGuideSection?> GetSectionAsync(string document, string section);
|
|
}
|
|
|
|
/// <param name="Document">Belgenin kisa adi (<see cref="McpDevGuide.Documents"/>).</param>
|
|
/// <param name="Id">Bolum numarasi; numarasiz basliklarda baslik metni.</param>
|
|
public sealed record McpDevGuideHeading(string Document, string Id, string Title, int Level);
|
|
|
|
public sealed record McpDevGuideSection(McpDevGuideHeading Heading, string Content, IReadOnlyList<McpDevGuideHeading> Children);
|
|
|
|
public partial class McpDevGuide(IConfiguration configuration) : DomainService, IMcpDevGuide
|
|
{
|
|
/// <summary>Konteynerde mount noktasi; bos ise depo klasoru calisma dizininden yukari aranir.</summary>
|
|
public const string ConfigurationKey = "App:DevGuidePath";
|
|
|
|
/// <summary>Kisa ad → dosya. Yalnizca uretim sozlesmesi sunulur; kod standardi ve karar kurallari degil.</summary>
|
|
public static readonly IReadOnlyDictionary<string, string> Documents = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["lowcode"] = "lowcode.instructions.md",
|
|
["reference"] = "lowcode-reference.instructions.md",
|
|
};
|
|
|
|
/// <summary>
|
|
/// Tek bir yanitta donen metnin tavani. Ust seviye bolumlerin bazilari istemcilerin tek
|
|
/// arac cevabi icin kabul ettigi boyutu asar; bu durumda alt basliklar ayrica istenir.
|
|
/// </summary>
|
|
private const int MaxSectionLength = 60_000;
|
|
|
|
[GeneratedRegex(@"^(#{2,4})\s+(?:(\d+(?:\.\d+)*)\.?\s+)?(.+?)\s*$")]
|
|
private static partial Regex HeadingPattern();
|
|
|
|
public async Task<IReadOnlyList<McpDevGuideHeading>> GetHeadingsAsync()
|
|
{
|
|
var headings = new List<McpDevGuideHeading>();
|
|
foreach (var document in Documents.Keys)
|
|
{
|
|
var lines = await ReadLinesAsync(document);
|
|
headings.AddRange(ParseHeadings(document, lines).Select(heading => heading.Heading));
|
|
}
|
|
|
|
return headings;
|
|
}
|
|
|
|
public async Task<McpDevGuideSection?> GetSectionAsync(string document, string section)
|
|
{
|
|
var lines = await ReadLinesAsync(document);
|
|
var headings = ParseHeadings(document, lines);
|
|
var wanted = section.Trim().TrimEnd('.');
|
|
|
|
var index = headings.FindIndex(h => string.Equals(h.Heading.Id, wanted, StringComparison.OrdinalIgnoreCase));
|
|
if (index < 0)
|
|
{
|
|
index = headings.FindIndex(h => h.Heading.Title.Contains(wanted, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
if (index < 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var match = headings[index];
|
|
var next = headings.FindIndex(index + 1, h => h.Heading.Level <= match.Heading.Level);
|
|
var end = next < 0 ? lines.Length : headings[next].Line;
|
|
var children = headings
|
|
.Skip(index + 1)
|
|
.TakeWhile(h => h.Line < end)
|
|
.Where(h => h.Heading.Level == match.Heading.Level + 1)
|
|
.ToList();
|
|
|
|
var content = string.Join('\n', lines[match.Line..end]);
|
|
if (content.Length > MaxSectionLength && children.Count > 0)
|
|
{
|
|
// Bolumun giris metni tek basina anlamlidir; geri kalani alt baslik olarak istenir.
|
|
content = string.Join('\n', lines[match.Line..children[0].Line]);
|
|
}
|
|
|
|
return new McpDevGuideSection(match.Heading, content, [.. children.Select(child => child.Heading)]);
|
|
}
|
|
|
|
private async Task<string[]> ReadLinesAsync(string document)
|
|
{
|
|
if (!Documents.TryGetValue(document, out var fileName))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var directory = ResolveDirectory();
|
|
var path = directory is null ? null : Path.Combine(directory, fileName);
|
|
if (path is null || !File.Exists(path))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
return (await File.ReadAllTextAsync(path)).Replace("\r\n", "\n").Split('\n');
|
|
}
|
|
|
|
private string? ResolveDirectory()
|
|
{
|
|
var configured = configuration[ConfigurationKey];
|
|
if (!string.IsNullOrWhiteSpace(configured))
|
|
{
|
|
return Path.GetFullPath(configured);
|
|
}
|
|
|
|
for (var dir = new DirectoryInfo(Directory.GetCurrentDirectory()); dir != null; dir = dir.Parent)
|
|
{
|
|
var candidate = Path.Combine(dir.FullName, ".github", "instructions");
|
|
if (Directory.Exists(candidate))
|
|
{
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Kod bloklari icindeki <c>##</c> ile baslayan satirlar (ornegin SQL yorumlari) baslik
|
|
/// sayilmaz; aksi halde bolum sinirlari ornek kodun ortasinda kesilirdi.
|
|
/// </summary>
|
|
private static List<(McpDevGuideHeading Heading, int Line)> ParseHeadings(string document, string[] lines)
|
|
{
|
|
var headings = new List<(McpDevGuideHeading, int)>();
|
|
var inFence = false;
|
|
|
|
for (var i = 0; i < lines.Length; i++)
|
|
{
|
|
if (lines[i].TrimStart().StartsWith("```", StringComparison.Ordinal))
|
|
{
|
|
inFence = !inFence;
|
|
continue;
|
|
}
|
|
|
|
if (inFence)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var match = HeadingPattern().Match(lines[i]);
|
|
if (!match.Success)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var title = match.Groups[3].Value;
|
|
var id = match.Groups[2].Success ? match.Groups[2].Value : title;
|
|
headings.Add((new McpDevGuideHeading(document, id, title, match.Groups[1].Length), i));
|
|
}
|
|
|
|
return headings;
|
|
}
|
|
}
|