using System; using System.Collections.Generic; using System.Data; using System.Globalization; using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Configuration; using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Services; using Sozsoft.Platform; using Sozsoft.Platform.Data.Seeds; using Sozsoft.Platform.DeveloperKit; using Sozsoft.Platform.Entities; using Sozsoft.Platform.Enums; using Sozsoft.Platform.ListForms; using Sozsoft.Platform.ListForms.Select; using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Domain.Repositories; using Volo.Abp.PermissionManagement; namespace Sozsoft.Mcp; /// /// 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. Invoke/Write yetkisi /// yalnizca MCP kapisidir, arkadaki yetkinin yerine gecmez. /// [Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)] public class McpDevelopAppService( IMcpServerConfigProvider serverConfigProvider, IMcpDevGuide devGuide, IMcpTableCreator tableCreator, IListFormWizardAppService wizardAppService, ICustomComponentAppService componentAppService, IListFormSelectAppService listFormSelectAppService, IRepository menuRepository, IRepository permissionRepository, IRepository devRequestRepository, IMcpDevRequestTimesheetRecorder timesheetRecorder, IConfiguration configuration) : McpAppService(serverConfigProvider), IMcpDevelopAppService { private const string DefaultGuideDocument = "lowcode"; /// Index cevabinda listelenen en derin baslik; daha derini bolum istenince gorunur. private const int IndexHeadingLevel = 3; /// Custom Components ekraninin tek sayfada dondurebildigi ust sinir. private const int ComponentPageSize = 1000; private const string DevRequestUpdatePermission = PlatformConsts.AppCodes.DevRequests + ".Update"; /// Talep ekranindaki alt gorev tarih kutusunun okudugu bicim. private const string SubTaskDateFormat = "yyyy-MM-dd"; /// verify_wizard cevabindaki kontrol adlari. private static class VerifyChecks { public const string WizardFile = "wizard_file"; public const string Install = "install"; public const string Menu = "menu"; public const string Permission = "permission"; public const string Select = "select"; public const string Component = "component"; public const string SeedClosure = "seed_closure"; } public async Task 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)] }; } /// /// Tablo tasarimcisiyla ayni yetkiyi ister: ham DDL liste yetkilerini asar, Develop yalnizca /// MCP kapisidir. /// public async Task 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)); await timesheetRecorder.RecordAsync(McpTimesheetTexts.CreateTable, created.TableName); return new McpCreateTableResultDto { TableName = created.TableName, Script = created.Script, SeedFilePath = created.SeedFilePath }; } public async Task> 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 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 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(); // Menu sirasi modelin karari degildir: yeni ekran kardeslerinin sonuna eklenir (deploy // bagimlilik sirasiyla yapildigi icin lookup kaynagi tuketen ekranin ustunde kalir), // guncelleme ekranin mevcut yerini korur. wizard.MenuOrder = wizard.EditFileName is null ? 0 : (await wizardAppService.GetFile(wizard.EditFileName)).Wizard?.MenuOrder ?? 0; // Satirlar deploy'dan once dogrulanir; gecersiz girdi yarim bir deploy birakmaz. var seedRows = ParseSeedRows(input.SeedRows, wizard); await wizardAppService.Create(wizard); var files = await wizardAppService.GetFiles(); var file = files.FirstOrDefault(file => string.Equals(file.WizardName, wizard.WizardName?.Trim(), StringComparison.OrdinalIgnoreCase)); if (file != null && seedRows.Count > 0) await WriteSeedRowsAsync(file.FileName, seedRows); await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeployWizard, file?.FileName ?? wizard.WizardName, seedRows.Count); return file; } /// /// Demo satirlarini okur ve ekranin alanlarina gore dogrular. Satirlar yalnizca SeedData acik /// liste ekraninda kabul edilir: migrate onlari ekranin veri dosyasindan ekler. /// private List> ParseSeedRows(string? json, ListFormWizardDto wizard) { if (string.IsNullOrWhiteSpace(json)) return []; if (wizard.ComponentKind == WizardComponentKindEnum.Custom || !wizard.SeedData) throw new UserFriendlyException(L[McpErrorCodes.SeedRowsNotAllowed]); List>? rows; try { rows = JsonSerializer.Deserialize>>(json); } catch (JsonException) { rows = null; } if (rows is null || rows.Any(row => row is null)) throw new UserFriendlyException(L[McpErrorCodes.SeedRowsInvalid]); var fields = wizard.Groups.SelectMany(group => group.Items).Select(item => item.FieldName) .Append(wizard.KeyFieldName) .Where(name => !string.IsNullOrWhiteSpace(name)) .ToHashSet(StringComparer.OrdinalIgnoreCase); var unknown = rows.SelectMany(row => row.Keys).Where(key => !fields.Contains(key)) .Distinct(StringComparer.OrdinalIgnoreCase).ToList(); if (unknown.Count > 0) throw new UserFriendlyException(L[McpErrorCodes.SeedRowsUnknownFields, string.Join(", ", unknown), string.Join(", ", fields)]); return rows; } /// /// Demo satirlarini ekranin veri dosyasina (ListForm.SeedFilePath) yazar; veritabanina /// dokunmaz. Migrate ekrani kurduktan sonra ListFormSeedDataApplier satirlari ekler ve /// kiraci/audit alanlarini doldurur. Guid anahtar burada uretilir: dosya her uygulandiginda ayni /// kayda karsilik gelir. Dosyada ayni icerikli satir varsa (tekrar deploy) yeniden eklenmez. /// private async Task WriteSeedRowsAsync(string fileName, List> rows) { var listForm = (await wizardAppService.GetFile(fileName)).ListForm; var relativePath = listForm?.SeedFilePath?.Replace('\\', '/').Trim('/'); if (listForm == null || string.IsNullOrWhiteSpace(relativePath) || SeedPathResolver.IsSeedFolderPath(relativePath)) throw new UserFriendlyException(L[McpErrorCodes.SeedRowsNotAllowed]); var path = SeedPathResolver.GetScopePath(configuration, CurrentTenant.Id, relativePath.Split('/')); var seedFile = File.Exists(path) ? JsonSerializer.Deserialize(await File.ReadAllTextAsync(path), ListFormSeedDataFileDto.JsonOptions) : null; seedFile ??= new ListFormSeedDataFileDto { ListFormCode = listForm.ListFormCode, KeyFieldName = listForm.KeyFieldName }; var keyFieldName = seedFile.KeyFieldName; var existing = seedFile.Rows.Select(row => RowContent(row, keyFieldName)).ToHashSet(StringComparer.Ordinal); foreach (var row in rows) { var values = row.ToDictionary(item => item.Key, item => (object)item.Value); if (!existing.Add(RowContent(values, keyFieldName))) continue; if (listForm.KeyFieldDbSourceType == DbType.Guid && !values.Keys.Contains(keyFieldName, StringComparer.OrdinalIgnoreCase)) values[keyFieldName] = GuidGenerator.Create(); seedFile.Rows.Add(values); } Directory.CreateDirectory(Path.GetDirectoryName(path)!); await File.WriteAllTextAsync(path, ListFormSeedDataFileDto.Serialize(seedFile)); } /// Satirin anahtar disindaki icerigi; tekrar deploy'da ayni satiri tanimak icin. private static string RowContent(Dictionary row, string keyFieldName) => string.Join('\u001f', row .Where(item => !string.Equals(item.Key, keyFieldName, StringComparison.OrdinalIgnoreCase)) .OrderBy(item => item.Key, StringComparer.OrdinalIgnoreCase) .Select(item => $"{item.Key.ToLowerInvariant()}={JsonSerializer.Serialize(item.Value)}")); /// /// Ekranin tablosu ancak acikken ve Wizard Manager'daki onayla ayni /// kosullarda (secilebilir aday) dusurulur; onayi kullanicidan almak istemcinin isidir. /// public async Task DeleteWizardAsync(string fileName, bool dropTables) { await EnsureToolAllowedAsync(McpToolNames.DeleteWizard); var input = new WizardDeleteInput { FileName = fileName }; if (dropTables) { var analysis = await wizardAppService.DeleteAnalysis(fileName); input.DropTables = [.. analysis.Tables.Where(t => t.CanDrop).Select(t => t.TableName)]; } var result = await wizardAppService.DeleteFile(input); await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteWizard, fileName); return result; } /// /// Ekrani kullanicinin gorecegi yoldan dener: menu kaydi, menunun istedigi yetki, ekranin /// sorgusu (cagiranin yetkisi ve kiraci filtresiyle, tek satir) ve Custom yolunda bilesen. /// Hicbir kontrol veri yazmaz; test satiri eklemek SeedSync acik ekranlarda seed dosyasina /// iz birakirdi. Seed kapanisi engellemez: ekran bu ortamda calisir, eksik dosya yalnizca /// migrate ya da baska bir ortamda ortaya cikar. /// public async Task VerifyWizardAsync(string fileName) { await EnsureToolAllowedAsync(McpToolNames.VerifyWizard); var result = new McpVerifyResultDto { FileName = fileName }; ListFormWizardDto? wizard; try { wizard = (await wizardAppService.GetFile(fileName)).Wizard; } catch (Exception ex) { result.Checks.Add(Failed(VerifyChecks.WizardFile, L[McpErrorCodes.VerifyWizardFileInvalid, fileName, ex.Message])); return result; } if (wizard is null) { result.Checks.Add(Failed(VerifyChecks.WizardFile, L[McpErrorCodes.InvalidWizard])); return result; } var listFormCode = wizard.ComponentKind == WizardComponentKindEnum.Custom ? null : wizard.ResolveListFormCode(); result.ListFormCode = listFormCode; result.MenuUrl = wizard.MenuUrl; result.Checks.Add(new McpVerifyCheckDto { Name = VerifyChecks.WizardFile, Passed = true }); // Deploy yalnizca seed dosyalarini yazar; menu, yetki ve ekran DB Migrate'ten sonra vardir. // Kurulmamis bir wizard'in geri kalan kontrolleri anlamsiz hatalar uretirdi. var file = (await wizardAppService.GetFiles()) .FirstOrDefault(f => string.Equals(f.FileName, fileName, StringComparison.OrdinalIgnoreCase)); if (file?.IsPending == true) { result.Checks.Add(Failed(VerifyChecks.Install, L[McpErrorCodes.VerifyWizardPending, fileName])); return result; } var permissionName = listFormCode; if (wizard.CreateMenu) { var menu = await VerifyMenuAsync(wizard, result.Checks); permissionName = string.IsNullOrWhiteSpace(menu?.RequiredPermissionName) ? permissionName : menu.RequiredPermissionName; } if (!string.IsNullOrWhiteSpace(permissionName)) { var permission = await permissionRepository.FirstOrDefaultAsync(p => p.Name == permissionName); result.Checks.Add(permission is { IsEnabled: true } ? new McpVerifyCheckDto { Name = VerifyChecks.Permission, Passed = true, Message = permissionName } : Failed(VerifyChecks.Permission, L[McpErrorCodes.VerifyPermissionMissing, permissionName])); } result.Checks.Add(listFormCode is null ? await VerifyComponentAsync(wizard) : await VerifySelectAsync(listFormCode)); result.Checks.Add(await VerifySeedClosureAsync(fileName)); result.Passed = result.Checks.TrueForAll(check => check.Passed || !check.Blocking); return result; } /// /// Durum kurali modelin dikkatine birakilmaz: talep Ready to Start degilse reddedilir, uygunsa /// ayni unit of work'te In Progress'e alinir. MCP yetki vermez; ekranin guncelleme yetkisi /// ayrica istenir. /// public async Task StartDevRequestAsync(string request) { await EnsureToolAllowedAsync(McpToolNames.StartDevRequest); await AuthorizationService.CheckAsync(DevRequestUpdatePermission); var devRequest = await FindDevRequestAsync(request); if (!string.Equals(devRequest.Status, McpConsts.DevRequestStatuses.ReadyToStart, StringComparison.Ordinal)) { throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotReady, devRequest.Title ?? devRequest.Id.ToString(), devRequest.Status ?? string.Empty]); } devRequest.Status = McpConsts.DevRequestStatuses.InProgress; await devRequestRepository.UpdateAsync(devRequest, autoSave: true); await timesheetRecorder.BeginAsync(devRequest.Id); return ToDto(devRequest); } public async Task GetDevRequestAsync(string request) { await EnsureToolAllowedAsync(McpToolNames.GetDevRequest); await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.DevRequests); var devRequest = await FindDevRequestAsync(request); await timesheetRecorder.BeginAsync(devRequest.Id); return ToDto(devRequest); } /// /// Durum kurali modelin dikkatine birakilmaz: yalnizca Backlog ya da Analysis durumundaki talep /// analiz edilir. Analysis'teki talebin analizi begenilmediyse yeniden yazilir; Ready to Start'a /// kullanici alir. /// public async Task SaveDevRequestAnalysisAsync(string request, string html) { await EnsureToolAllowedAsync(McpToolNames.SaveDevRequestAnalysis); await AuthorizationService.CheckAsync(DevRequestUpdatePermission); if (string.IsNullOrWhiteSpace(html)) { throw new UserFriendlyException(L[McpErrorCodes.DevRequestAnalysisRequired]); } var devRequest = await FindDevRequestAsync(request); if (devRequest.Status is not (McpConsts.DevRequestStatuses.Backlog or McpConsts.DevRequestStatuses.Analysis)) { throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotAnalyzable, devRequest.Title ?? devRequest.Id.ToString(), devRequest.Status ?? string.Empty]); } devRequest.Analysis = html.Trim(); devRequest.Status = McpConsts.DevRequestStatuses.Analysis; await devRequestRepository.UpdateAsync(devRequest, autoSave: true); await timesheetRecorder.RecordAsync(McpTimesheetTexts.Analysis); return ToDto(devRequest); } /// /// Aciklama talebin yazildigi haliyle kalir ve base64 gorsel tasir; uretim notlari analize /// eklenir. Ekleme sunucuda, var olan metne dokunmadan yapilir. /// public async Task AppendDevRequestNoteAsync(string request, string html) { await EnsureToolAllowedAsync(McpToolNames.AppendDevRequestNote); await AuthorizationService.CheckAsync(DevRequestUpdatePermission); if (string.IsNullOrWhiteSpace(html)) { throw new UserFriendlyException(L[McpErrorCodes.DevRequestNoteRequired]); } var devRequest = await FindDevRequestAsync(request); devRequest.Analysis = string.Concat(devRequest.Analysis, html.Trim()); await devRequestRepository.UpdateAsync(devRequest, autoSave: true); } /// /// Atanan kisi ve tarih modelin dikkatine birakilmaz: satiri yazan cagiran kullanicidir /// (ekranin atanan listesi kullanici adiyla calisir), bitis tarihi yazildigi gundur. /// Var olan satirlara dokunulmaz. /// public async Task AddDevRequestSubTasksAsync(string request, List titles) { await EnsureToolAllowedAsync(McpToolNames.AddDevRequestSubTasks); await AuthorizationService.CheckAsync(DevRequestUpdatePermission); var items = (titles ?? []).Select(title => title?.Trim()).Where(title => !string.IsNullOrEmpty(title)).ToList(); if (items.Count == 0) { throw new UserFriendlyException(L[McpErrorCodes.DevRequestSubTasksRequired]); } var devRequest = await FindDevRequestAsync(request); JsonArray subTasks; try { subTasks = string.IsNullOrWhiteSpace(devRequest.SubTasks) ? [] : JsonNode.Parse(devRequest.SubTasks) as JsonArray ?? throw new JsonException(); } catch (JsonException) { throw new UserFriendlyException(L[McpErrorCodes.DevRequestSubTasksInvalid, devRequest.Title ?? devRequest.Id.ToString()]); } var assignee = CurrentUser.UserName ?? string.Empty; var dueDate = Clock.Now.ToString(SubTaskDateFormat, CultureInfo.InvariantCulture); foreach (var title in items) { subTasks.Add(new JsonObject { ["id"] = GuidGenerator.Create().ToString(), ["title"] = title, ["assignee"] = assignee, ["dueDate"] = dueDate, ["completed"] = true }); } devRequest.SubTasks = subTasks.ToJsonString(); await devRequestRepository.UpdateAsync(devRequest, autoSave: true); } private static McpDevRequestDto ToDto(DevRequest devRequest) { var (description, images) = McpDevRequestImages.Extract(devRequest.Description); return new McpDevRequestDto { Id = devRequest.Id, Title = devRequest.Title, Status = devRequest.Status, Description = description, Images = images, Analysis = devRequest.Analysis, Priority = devRequest.Priority, Tags = devRequest.Tags, DueDate = devRequest.DueDate, UserName = devRequest.UserName, TenantId = devRequest.TenantId, SubTasks = devRequest.SubTasks }; } /// Id ile ya da tam baslikla bulur; ayni basligi tasiyan birden fazla talep varsa Id istenir. private async Task FindDevRequestAsync(string? request) { var value = request?.Trim() ?? string.Empty; if (Guid.TryParse(value, out var id)) { return await devRequestRepository.FindAsync(id) ?? throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotFound, value]); } var matches = value.Length == 0 ? [] : await devRequestRepository.GetListAsync(r => r.Title == value); return matches.Count switch { 0 => throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotFound, value]), 1 => matches[0], _ => throw new UserFriendlyException(L[McpErrorCodes.DevRequestAmbiguous, value, string.Join(", ", matches.Select(r => r.Id))]) }; } /// /// Kapanis analizi wizard export'unun yetkisini ister; o yetki dogrulamanin kendisini /// dusurmesin diye yoksa kontrol atlanir ve nedeni raporlanir. /// private async Task VerifySeedClosureAsync(string fileName) { var check = new McpVerifyCheckDto { Name = VerifyChecks.SeedClosure, Blocking = false }; if (!await AuthorizationService.IsGrantedAsync(PlatformConsts.AppCodes.Listforms.Wizards.Export)) { check.Message = L[McpErrorCodes.VerifySeedClosureSkipped, PlatformConsts.AppCodes.Listforms.Wizards.Export]; return check; } var warnings = (await wizardAppService.ExportAnalysis(fileName)).Warnings; if (warnings.Count == 0) { check.Passed = true; return check; } check.Message = L[McpErrorCodes.VerifySeedClosureIncomplete, string.Join(", ", warnings)]; return check; } private async Task VerifyMenuAsync(ListFormWizardDto wizard, List checks) { var menuCode = wizard.ResolveMenuCode(); var menu = await menuRepository.FirstOrDefaultAsync(m => m.Code == menuCode); if (menu is null) { checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuMissing, menuCode])); } else if (menu.IsDisabled) { checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuDisabled, menuCode])); } else if (!string.Equals(menu.Url, wizard.MenuUrl, StringComparison.OrdinalIgnoreCase)) { checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuUrlMismatch, menuCode, menu.Url, wizard.MenuUrl])); } else { checks.Add(new McpVerifyCheckDto { Name = VerifyChecks.Menu, Passed = true, Message = menu.Url }); } return menu; } /// /// Ekranin sorgusunu query_list ile ayni servisten calistirir; kolon, veri kaynagi ve yetki /// hatalari burada ortaya cikar. /// private async Task VerifySelectAsync(string listFormCode) { try { var select = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto { ListFormCode = listFormCode, Take = 1, RequireTotalCount = true }); return new McpVerifyCheckDto { Name = VerifyChecks.Select, Passed = true, Message = L[McpErrorCodes.VerifySelectPassed, select.TotalCount] }; } catch (Exception ex) { return Failed(VerifyChecks.Select, L[McpErrorCodes.VerifySelectFailed, ex.Message]); } } private async Task VerifyComponentAsync(ListFormWizardDto wizard) { var component = await FindComponentAsync(wizard.CustomComponentName); if (component is null) { return Failed(VerifyChecks.Component, L[McpErrorCodes.ComponentNotFound, wizard.CustomComponentName]); } if (!component.IsActive) { return Failed(VerifyChecks.Component, L[McpErrorCodes.VerifyComponentInactive, component.Name]); } if (wizard.CreateMenu && !string.Equals(component.RoutePath, wizard.MenuUrl, StringComparison.OrdinalIgnoreCase)) { return Failed(VerifyChecks.Component, L[McpErrorCodes.VerifyComponentRouteMismatch, component.Name, component.RoutePath ?? string.Empty, wizard.MenuUrl]); } return new McpVerifyCheckDto { Name = VerifyChecks.Component, Passed = true, Message = component.RoutePath }; } private static McpVerifyCheckDto Failed(string name, string message) => new() { Name = name, Passed = false, Message = message }; public async Task> 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 GetComponentAsync(string name) { await EnsureToolAllowedAsync(McpToolNames.GetComponent); return await FindComponentAsync(name); } public async Task 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]); } var created = 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 }); await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveComponent, created.Name); return created; } var updated = 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 }); await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveComponent, updated.Name); return updated; } 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); await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteComponent, component.Name); } /// /// Wizard'i seed dosyasi biciminden ({"Wizard": {...}}) ya da duz cevaplardan okur. /// Iki bicim de dosyanin okuma kuralindan gecer; boylece Menu/Language bloklari ve /// WizardCode/ModuleId adlari dosyadakiyle ayni anlama gelir. /// 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> GetAllComponentsAsync() { var page = await componentAppService.GetListAsync(new PagedAndSortedResultRequestDto { MaxResultCount = ComponentPageSize, Sorting = nameof(CustomComponentDto.Name) }); return page.Items; } private async Task 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)); } /// /// Props icinde tasarimci belgesi varsa ve kip acikca code degilse kod belgeden /// uretilir; tasarimcinin varsayilan kipi visual'dir. /// 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 }; }