sozsoft-platform/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs
2026-09-18 16:47:23 +03:00

341 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Sozsoft.Mcp.Dtos;
using Sozsoft.Mcp.Services;
using Sozsoft.Platform;
using Sozsoft.Platform.DeveloperKit;
using Sozsoft.Platform.ListForms;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Mcp;
/// <summary>
/// Ekran ve bilesen gelistirme yuzeyi. Uretim Wizard ve Custom Components servislerinden
/// gecer: menu, yetki, dil anahtari, seed dosyasi ve bagimlilik kurallari ekrandaki yolla
/// aynidir, yetki kontrolu de o servislerin kendi politikalarindan gelir. Develop yetkisi
/// yalnizca MCP kapisidir, arkadaki yetkinin yerine gecmez.
/// </summary>
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop)]
public class McpDevelopAppService(
IMcpServerConfigProvider serverConfigProvider,
IMcpDevGuide devGuide,
IMcpTableCreator tableCreator,
IListFormWizardAppService wizardAppService,
ICustomComponentAppService componentAppService)
: McpAppService(serverConfigProvider), IMcpDevelopAppService
{
private const string DefaultGuideDocument = "lowcode";
/// <summary>Index cevabinda listelenen en derin baslik; daha derini bolum istenince gorunur.</summary>
private const int IndexHeadingLevel = 3;
/// <summary>Custom Components ekraninin tek sayfada dondurebildigi ust sinir.</summary>
private const int ComponentPageSize = 1000;
public async Task<McpDevGuideDto> GetDevGuideAsync(string? document, string? section)
{
await EnsureToolAllowedAsync(McpToolNames.GetDevGuide);
if (string.IsNullOrWhiteSpace(section))
{
var headings = await devGuide.GetHeadingsAsync();
if (headings.Count == 0)
{
throw new UserFriendlyException(L[McpErrorCodes.DevGuideUnavailable, McpDevGuide.ConfigurationKey]);
}
return new McpDevGuideDto
{
Sections = [.. headings
.Where(h => h.Level <= IndexHeadingLevel)
.Where(h => string.IsNullOrWhiteSpace(document) || string.Equals(h.Document, document, StringComparison.OrdinalIgnoreCase))
.Select(ToDto)]
};
}
var result = await devGuide.GetSectionAsync(
string.IsNullOrWhiteSpace(document) ? DefaultGuideDocument : document,
section)
?? throw new UserFriendlyException(L[McpErrorCodes.DevGuideSectionNotFound, section]);
return new McpDevGuideDto
{
Heading = ToDto(result.Heading),
Content = result.Content,
Sections = [.. result.Children.Select(ToDto)]
};
}
/// <summary>
/// Tablo tasarimcisiyla ayni yetkiyi ister: ham DDL liste yetkilerini asar, Develop yalnizca
/// MCP kapisidir.
/// </summary>
public async Task<McpCreateTableResultDto> CreateTableAsync(McpCreateTableRequestDto input)
{
Check.NotNull(input, nameof(input));
await EnsureToolAllowedAsync(McpToolNames.CreateTable);
await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.SqlQueryManagers.Default);
var created = await tableCreator.CreateAsync(new McpTableDefinition(
input.TableName,
[.. (input.Columns ?? []).Select(column => new McpTableColumn(
column.Name, column.Type, column.MaxLength, column.IsNullable, column.DefaultValue))],
input.FullAudited,
input.MultiTenant));
return new McpCreateTableResultDto
{
TableName = created.TableName,
Script = created.Script,
SeedFilePath = created.SeedFilePath
};
}
public async Task<List<WizardFileInfoDto>> GetWizardsAsync(string? search)
{
await EnsureToolAllowedAsync(McpToolNames.ListWizards);
var files = await wizardAppService.GetFiles();
if (string.IsNullOrWhiteSpace(search))
{
return files;
}
return [.. files.Where(file =>
Contains(file.FileName, search) || Contains(file.WizardName, search) ||
Contains(file.ListFormCode, search) || Contains(file.CustomComponentName, search))];
}
public async Task<McpWizardFileDto> GetWizardAsync(string fileName)
{
await EnsureToolAllowedAsync(McpToolNames.GetWizard);
var seed = await wizardAppService.GetFile(fileName);
// Model dosyayi Claude Code'un depoda gordugu bicimde gorur; deploy_wizard ayni bicimi
// geri kabul eder.
using var document = JsonDocument.Parse(seed.ToSeedJson());
return new McpWizardFileDto { FileName = fileName, Seed = document.RootElement.Clone() };
}
public async Task<WizardFileInfoDto?> DeployWizardAsync(McpWizardDeployRequestDto input)
{
Check.NotNull(input, nameof(input));
await EnsureToolAllowedAsync(McpToolNames.DeployWizard);
var wizard = ParseWizard(input.Wizard);
wizard.EditFileName = string.IsNullOrWhiteSpace(input.ReplaceFileName) ? null : input.ReplaceFileName.Trim();
await wizardAppService.Create(wizard);
var files = await wizardAppService.GetFiles();
return files.FirstOrDefault(file => string.Equals(file.WizardName, wizard.WizardName?.Trim(), StringComparison.OrdinalIgnoreCase));
}
public async Task<WizardDeleteResultDto> DeleteWizardAsync(string fileName)
{
await EnsureToolAllowedAsync(McpToolNames.DeleteWizard);
return await wizardAppService.DeleteFile(fileName);
}
public async Task<List<McpComponentSummaryDto>> GetComponentsAsync(string? search)
{
await EnsureToolAllowedAsync(McpToolNames.ListComponents);
var components = await GetAllComponentsAsync();
return [.. components
.Where(component => string.IsNullOrWhiteSpace(search)
|| Contains(component.Name, search)
|| Contains(component.RoutePath, search)
|| Contains(component.Description, search))
.Select(component => new McpComponentSummaryDto
{
Id = component.Id,
Name = component.Name,
RoutePath = component.RoutePath,
Description = component.Description,
IsActive = component.IsActive,
IsVisualDesigner = IsVisualDesignerDocument(component.Props)
})];
}
public async Task<CustomComponentDto?> GetComponentAsync(string name)
{
await EnsureToolAllowedAsync(McpToolNames.GetComponent);
return await FindComponentAsync(name);
}
public async Task<CustomComponentDto> SaveComponentAsync(McpComponentSaveRequestDto input)
{
Check.NotNull(input, nameof(input));
await EnsureToolAllowedAsync(McpToolNames.SaveComponent);
if (string.IsNullOrWhiteSpace(input.Name))
{
throw new UserFriendlyException(L[McpErrorCodes.ComponentNameRequired]);
}
var existing = await FindComponentAsync(input.Name);
var props = input.Props ?? existing?.Props;
// Tasarimci belgesinde kod belgeden uretilir; buraya yazilan kod tasarimcidaki ilk
// kayitta sessizce ezilirdi. Belge MCP'de uretilemedigi icin bu bilesenler tasarimcida kalir.
if (IsVisualDesignerDocument(props))
{
throw new UserFriendlyException(L[McpErrorCodes.VisualDesignerComponent, input.Name]);
}
if (existing is null)
{
if (string.IsNullOrWhiteSpace(input.Code))
{
throw new UserFriendlyException(L[McpErrorCodes.ComponentCodeRequired]);
}
return await componentAppService.CreateAsync(new CreateUpdateCustomComponentDto
{
Name = input.Name.Trim(),
Code = input.Code,
RoutePath = input.RoutePath,
Description = input.Description,
Props = props,
Dependencies = input.Dependencies,
IsActive = input.IsActive ?? true
});
}
return await componentAppService.UpdateAsync(existing.Id, new CreateUpdateCustomComponentDto
{
Name = existing.Name,
Code = string.IsNullOrWhiteSpace(input.Code) ? existing.Code : input.Code,
RoutePath = input.RoutePath ?? existing.RoutePath,
Description = input.Description ?? existing.Description,
Props = props,
Dependencies = input.Dependencies ?? existing.Dependencies,
IsActive = input.IsActive ?? existing.IsActive
});
}
public async Task DeleteComponentAsync(string name)
{
await EnsureToolAllowedAsync(McpToolNames.DeleteComponent);
var component = await FindComponentAsync(name)
?? throw new UserFriendlyException(L[McpErrorCodes.ComponentNotFound, name]);
await componentAppService.DeleteAsync(component.Id);
}
/// <summary>
/// Wizard'i seed dosyasi biciminden (<c>{"Wizard": {...}}</c>) ya da duz cevaplardan okur.
/// Iki bicim de dosyanin okuma kuralindan gecer; boylece Menu/Language bloklari ve
/// WizardCode/ModuleId adlari dosyadakiyle ayni anlama gelir.
/// </summary>
private ListFormWizardDto ParseWizard(string? json)
{
JsonObject? root;
try
{
root = string.IsNullOrWhiteSpace(json) ? null : JsonNode.Parse(json) as JsonObject;
}
catch (JsonException)
{
root = null;
}
if (root is null)
{
throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]);
}
var seedJson = root.ContainsKey(nameof(WizardSeedFileDto.Wizard))
? root.ToJsonString()
: new JsonObject { [nameof(WizardSeedFileDto.Wizard)] = root }.ToJsonString();
try
{
return WizardSeedFileDto.FromSeedJson(seedJson)?.Wizard
?? throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]);
}
catch (JsonException)
{
throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]);
}
}
private async Task<IReadOnlyList<CustomComponentDto>> GetAllComponentsAsync()
{
var page = await componentAppService.GetListAsync(new PagedAndSortedResultRequestDto
{
MaxResultCount = ComponentPageSize,
Sorting = nameof(CustomComponentDto.Name)
});
return page.Items;
}
private async Task<CustomComponentDto?> FindComponentAsync(string? name)
{
if (string.IsNullOrWhiteSpace(name))
{
return null;
}
var components = await GetAllComponentsAsync();
return components.FirstOrDefault(component => string.Equals(component.Name, name.Trim(), StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Props icinde tasarimci belgesi varsa ve kip acikca <c>code</c> degilse kod belgeden
/// uretilir; tasarimcinin varsayilan kipi <c>visual</c>'dir.
/// </summary>
private static bool IsVisualDesignerDocument(string? props)
{
if (string.IsNullOrWhiteSpace(props))
{
return false;
}
try
{
using var document = JsonDocument.Parse(props);
if (document.RootElement.ValueKind != JsonValueKind.Object ||
!document.RootElement.TryGetProperty("visualDesigner", out var designer) ||
designer.ValueKind != JsonValueKind.Object)
{
return false;
}
return !designer.TryGetProperty("sourceMode", out var mode) ||
mode.ValueKind != JsonValueKind.String ||
!string.Equals(mode.GetString(), "code", StringComparison.Ordinal);
}
catch (JsonException)
{
return false;
}
}
private static bool Contains(string? value, string search)
=> value?.Contains(search.Trim(), StringComparison.OrdinalIgnoreCase) == true;
private static McpDevGuideHeadingDto ToDto(McpDevGuideHeading heading) => new()
{
Document = heading.Document,
Id = heading.Id,
Title = heading.Title,
Level = heading.Level
};
}