342 lines
12 KiB
C#
342 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
|
||
|
|
};
|
||
|
|
}
|