2026-09-18 13:47:23 +00:00
|
|
|
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);
|
2026-10-02 06:07:01 +00:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Metninde <paramref name="term"/> gecen bolumler, gecme sayisina gore. Baslik olmayan bir
|
|
|
|
|
/// terim (editor adi, alan adi) istendiginde modele nereye bakacagini soyler.
|
|
|
|
|
/// </summary>
|
|
|
|
|
Task<IReadOnlyList<McpDevGuideHeading>> SearchAsync(string term, int maxResults);
|
2026-09-18 13:47:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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)]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-02 06:07:01 +00:00
|
|
|
public async Task<IReadOnlyList<McpDevGuideHeading>> SearchAsync(string term, int maxResults)
|
|
|
|
|
{
|
|
|
|
|
var wanted = term.Trim();
|
|
|
|
|
if (wanted.Length == 0)
|
|
|
|
|
{
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var hits = new List<(McpDevGuideHeading Heading, int Count, int Order)>();
|
|
|
|
|
foreach (var document in Documents.Keys)
|
|
|
|
|
{
|
|
|
|
|
var lines = await ReadLinesAsync(document);
|
|
|
|
|
var headings = ParseHeadings(document, lines);
|
|
|
|
|
|
|
|
|
|
// Her satir kendisinden onceki en yakin basliga sayilir; boylece en dar bolum doner.
|
|
|
|
|
for (var i = 0; i < headings.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
var end = i + 1 < headings.Count ? headings[i + 1].Line : lines.Length;
|
|
|
|
|
var count = lines[headings[i].Line..end]
|
|
|
|
|
.Count(line => line.Contains(wanted, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
if (count > 0)
|
|
|
|
|
{
|
|
|
|
|
hits.Add((headings[i].Heading, count, hits.Count));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [.. hits
|
|
|
|
|
.OrderByDescending(hit => hit.Count)
|
|
|
|
|
.ThenBy(hit => hit.Order)
|
|
|
|
|
.Take(maxResults)
|
|
|
|
|
.Select(hit => hit.Heading)];
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-18 13:47:23 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|