diff --git a/InterfaceForward.sln b/InterfaceForward.sln index 4149f2d..474fe2c 100644 --- a/InterfaceForward.sln +++ b/InterfaceForward.sln @@ -1,9 +1,6 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterfaceForward.AppHost", "src\InterfaceForward.AppHost\InterfaceForward.AppHost.csproj", "{9609CC2D-CC83-4C01-BE36-C0429A06B238}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterfaceForward.ServiceDefaults", "src\InterfaceForward.ServiceDefaults\InterfaceForward.ServiceDefaults.csproj", "{6DAA9CF1-155A-4FC7-9EAE-7E8FA22CD23A}" -EndProject +# Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EA8DF4A8-7618-4C28-BEDC-21629B8B219B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterfaceForward.Api", "src\InterfaceForward.Api\InterfaceForward.Api.csproj", "{0E2F3A09-4EC4-4A61-85DC-9F9F770C3B65}" @@ -20,28 +17,22 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution items", "solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterfaceForward.Repositories", "src\InterfaceForward.Repositories\InterfaceForward.Repositories.csproj", "{AB53F0CC-DE7A-4AE9-B6AA-2A8283B4EB23}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E78C7664-0AED-43DB-8169-5414EBD945E7}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterfaceForward.Application", "src\InterfaceForward.Application\InterfaceForward.Application.csproj", "{4FE95CF4-F22A-45D6-B7DA-5B9DAE39ECED}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterfaceForward.Domain.Shared", "src\InterfaceForward.Domain.Shared\InterfaceForward.Domain.Shared.csproj", "{9DCF9B05-832C-4258-B7C9-0A389734F853}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterfaceForward.Application.Contracts", "src\InterfaceForward.Application.Contracts\InterfaceForward.Application.Contracts.csproj", "{79ED9047-0141-4B15-94E1-30FFD1566BAE}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{DCD9E3A4-7B28-4756-A2AA-272796356C52}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterfaceForward.Application.Tests", "tests\InterfaceForward.Application.Tests\InterfaceForward.Application.Tests.csproj", "{48C656F0-F879-420B-82C8-F00A8206EADE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9609CC2D-CC83-4C01-BE36-C0429A06B238}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9609CC2D-CC83-4C01-BE36-C0429A06B238}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9609CC2D-CC83-4C01-BE36-C0429A06B238}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9609CC2D-CC83-4C01-BE36-C0429A06B238}.Release|Any CPU.Build.0 = Release|Any CPU - {6DAA9CF1-155A-4FC7-9EAE-7E8FA22CD23A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6DAA9CF1-155A-4FC7-9EAE-7E8FA22CD23A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6DAA9CF1-155A-4FC7-9EAE-7E8FA22CD23A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6DAA9CF1-155A-4FC7-9EAE-7E8FA22CD23A}.Release|Any CPU.Build.0 = Release|Any CPU {0E2F3A09-4EC4-4A61-85DC-9F9F770C3B65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0E2F3A09-4EC4-4A61-85DC-9F9F770C3B65}.Debug|Any CPU.Build.0 = Debug|Any CPU {0E2F3A09-4EC4-4A61-85DC-9F9F770C3B65}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -62,14 +53,17 @@ Global {79ED9047-0141-4B15-94E1-30FFD1566BAE}.Debug|Any CPU.Build.0 = Debug|Any CPU {79ED9047-0141-4B15-94E1-30FFD1566BAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {79ED9047-0141-4B15-94E1-30FFD1566BAE}.Release|Any CPU.Build.0 = Release|Any CPU + {48C656F0-F879-420B-82C8-F00A8206EADE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48C656F0-F879-420B-82C8-F00A8206EADE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48C656F0-F879-420B-82C8-F00A8206EADE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48C656F0-F879-420B-82C8-F00A8206EADE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution - {9609CC2D-CC83-4C01-BE36-C0429A06B238} = {EA8DF4A8-7618-4C28-BEDC-21629B8B219B} - {6DAA9CF1-155A-4FC7-9EAE-7E8FA22CD23A} = {EA8DF4A8-7618-4C28-BEDC-21629B8B219B} {0E2F3A09-4EC4-4A61-85DC-9F9F770C3B65} = {EA8DF4A8-7618-4C28-BEDC-21629B8B219B} {AB53F0CC-DE7A-4AE9-B6AA-2A8283B4EB23} = {EA8DF4A8-7618-4C28-BEDC-21629B8B219B} {4FE95CF4-F22A-45D6-B7DA-5B9DAE39ECED} = {EA8DF4A8-7618-4C28-BEDC-21629B8B219B} {9DCF9B05-832C-4258-B7C9-0A389734F853} = {EA8DF4A8-7618-4C28-BEDC-21629B8B219B} {79ED9047-0141-4B15-94E1-30FFD1566BAE} = {EA8DF4A8-7618-4C28-BEDC-21629B8B219B} + {48C656F0-F879-420B-82C8-F00A8206EADE} = {DCD9E3A4-7B28-4756-A2AA-272796356C52} EndGlobalSection EndGlobal diff --git a/src/InterfaceForward.Api/InterfaceForward.Api.csproj b/src/InterfaceForward.Api/InterfaceForward.Api.csproj index 880ef74..2275f77 100644 --- a/src/InterfaceForward.Api/InterfaceForward.Api.csproj +++ b/src/InterfaceForward.Api/InterfaceForward.Api.csproj @@ -9,7 +9,6 @@ - diff --git a/src/InterfaceForward.Api/InterfaceForwardApiModule.cs b/src/InterfaceForward.Api/InterfaceForwardApiModule.cs index bd459d0..e13002b 100644 --- a/src/InterfaceForward.Api/InterfaceForwardApiModule.cs +++ b/src/InterfaceForward.Api/InterfaceForwardApiModule.cs @@ -106,9 +106,6 @@ public class InterfaceForwardApiModule : FakeModule app.UseFakeExceptionHandling(); - // Add Aspire default endpoints. - app.MapDefaultEndpoints(); - // 虚拟路由-网关适配 app.UsePathBase(app.Configuration["ASPNETCORE_PATHBASE"]); diff --git a/src/InterfaceForward.Api/Program.cs b/src/InterfaceForward.Api/Program.cs index b7cc59e..8398e9a 100644 --- a/src/InterfaceForward.Api/Program.cs +++ b/src/InterfaceForward.Api/Program.cs @@ -15,9 +15,6 @@ try builder.Host.UseAutofac().UseSerilog(); builder.Services.AddApplication(); - // Add service defaults & Aspire components. - builder.AddServiceDefaults(); - var app = builder.Build(); app.InitializeApplication(); app.Run(); diff --git a/src/InterfaceForward.AppHost/InterfaceForward.AppHost.csproj b/src/InterfaceForward.AppHost/InterfaceForward.AppHost.csproj deleted file mode 100644 index 9f03477..0000000 --- a/src/InterfaceForward.AppHost/InterfaceForward.AppHost.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - Exe - net8.0 - enable - enable - true - - - - - - - - - - - - diff --git a/src/InterfaceForward.AppHost/Program.cs b/src/InterfaceForward.AppHost/Program.cs deleted file mode 100644 index cf57e62..0000000 --- a/src/InterfaceForward.AppHost/Program.cs +++ /dev/null @@ -1,5 +0,0 @@ -var builder = DistributedApplication.CreateBuilder(args); - -builder.AddProject("apiservice"); - -builder.Build().Run(); \ No newline at end of file diff --git a/src/InterfaceForward.AppHost/Properties/launchSettings.json b/src/InterfaceForward.AppHost/Properties/launchSettings.json deleted file mode 100644 index c7b4e88..0000000 --- a/src/InterfaceForward.AppHost/Properties/launchSettings.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:15177", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16294" - } - } - } -} diff --git a/src/InterfaceForward.AppHost/appsettings.Development.json b/src/InterfaceForward.AppHost/appsettings.Development.json deleted file mode 100644 index 0c208ae..0000000 --- a/src/InterfaceForward.AppHost/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/src/InterfaceForward.AppHost/appsettings.json b/src/InterfaceForward.AppHost/appsettings.json deleted file mode 100644 index 31c092a..0000000 --- a/src/InterfaceForward.AppHost/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning", - "Aspire.Hosting.Dcp": "Warning" - } - } -} diff --git a/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/ExplainForwardScriptOutput.cs b/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/ExplainForwardScriptOutput.cs new file mode 100644 index 0000000..dd2d2be --- /dev/null +++ b/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/ExplainForwardScriptOutput.cs @@ -0,0 +1,8 @@ +namespace InterfaceForward.Application.Contracts.Dtos.ScriptAssistant; + +public class ExplainForwardScriptOutput +{ + public string Description { get; set; } = string.Empty; + + public List Warnings { get; set; } = []; +} diff --git a/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/GenerateForwardScriptInput.cs b/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/GenerateForwardScriptInput.cs new file mode 100644 index 0000000..a348024 --- /dev/null +++ b/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/GenerateForwardScriptInput.cs @@ -0,0 +1,16 @@ +namespace InterfaceForward.Application.Contracts.Dtos.ScriptAssistant; + +public class GenerateForwardScriptInput +{ + public string Position { get; set; } = "prefix"; + + public string Requirement { get; set; } = string.Empty; + + public List AccountFields { get; set; } = []; + + public List Headers { get; set; } = []; + + public List Queries { get; set; } = []; + + public List FormFields { get; set; } = []; +} diff --git a/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/GenerateForwardScriptOutput.cs b/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/GenerateForwardScriptOutput.cs new file mode 100644 index 0000000..d09c63d --- /dev/null +++ b/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/GenerateForwardScriptOutput.cs @@ -0,0 +1,10 @@ +namespace InterfaceForward.Application.Contracts.Dtos.ScriptAssistant; + +public class GenerateForwardScriptOutput +{ + public string Script { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + + public List Warnings { get; set; } = []; +} diff --git a/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/PreviewForwardScriptInput.cs b/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/PreviewForwardScriptInput.cs new file mode 100644 index 0000000..d51d7a7 --- /dev/null +++ b/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/PreviewForwardScriptInput.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json.Linq; + +namespace InterfaceForward.Application.Contracts.Dtos.ScriptAssistant; + +public class PreviewForwardScriptInput +{ + public string Script { get; set; } = string.Empty; + + public JObject? OriginalInput { get; set; } + + public JObject? MappedInput { get; set; } + + public JObject? TargetOutput { get; set; } + + public Dictionary AccountFields { get; set; } = []; +} diff --git a/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/PreviewForwardScriptOutput.cs b/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/PreviewForwardScriptOutput.cs new file mode 100644 index 0000000..f4dbeba --- /dev/null +++ b/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/PreviewForwardScriptOutput.cs @@ -0,0 +1,21 @@ +using InterfaceForward.Domain.Shared.Dtos; +using Newtonsoft.Json.Linq; + +namespace InterfaceForward.Application.Contracts.Dtos.ScriptAssistant; + +public class PreviewForwardScriptOutput +{ + public bool Success { get; set; } + + public string? Error { get; set; } + + public JToken? OriginalInput { get; set; } + + public JToken? MappedInput { get; set; } + + public JToken? TargetOutput { get; set; } + + public List FixedFields { get; set; } = []; + + public List FormFields { get; set; } = []; +} diff --git a/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/ValidateForwardScriptInput.cs b/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/ValidateForwardScriptInput.cs new file mode 100644 index 0000000..37cc0e5 --- /dev/null +++ b/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/ValidateForwardScriptInput.cs @@ -0,0 +1,6 @@ +namespace InterfaceForward.Application.Contracts.Dtos.ScriptAssistant; + +public class ValidateForwardScriptInput +{ + public string Script { get; set; } = string.Empty; +} diff --git a/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/ValidateForwardScriptOutput.cs b/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/ValidateForwardScriptOutput.cs new file mode 100644 index 0000000..bc622b0 --- /dev/null +++ b/src/InterfaceForward.Application.Contracts/Dtos/ScriptAssistant/ValidateForwardScriptOutput.cs @@ -0,0 +1,10 @@ +namespace InterfaceForward.Application.Contracts.Dtos.ScriptAssistant; + +public class ValidateForwardScriptOutput +{ + public bool Success { get; set; } + + public string? Error { get; set; } + + public List Warnings { get; set; } = []; +} diff --git a/src/InterfaceForward.Application/Services/ScriptAssistant/ScriptAssistantService.cs b/src/InterfaceForward.Application/Services/ScriptAssistant/ScriptAssistantService.cs new file mode 100644 index 0000000..ac6327a --- /dev/null +++ b/src/InterfaceForward.Application/Services/ScriptAssistant/ScriptAssistantService.cs @@ -0,0 +1,229 @@ +using System.ComponentModel.DataAnnotations; +using System.Text; +using System.Text.RegularExpressions; +using InterfaceForward.Application.Contracts.Dtos.ScriptAssistant; +using InterfaceForward.Application.Contracts.ForwardCore; +using InterfaceForward.Application.ForwardCore; +using InterfaceForward.Domain.Shared.Dtos; +using InterfaceForward.Domain.Shared.Enum; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; + +namespace InterfaceForward.Application.Services.ScriptAssistant; + +[ApiExplorerSettings(GroupName = "脚本助手")] +public class ScriptAssistantService : ApplicationService +{ + static readonly string[] DangerousTokens = + [ + "System.IO", "File", "Directory", "Path.", "Environment", "Process", "System.Diagnostics", + "Reflection", "Assembly", "Activator", "HttpClient", "Socket", "TcpClient", "UdpClient", + "Task.Run", "Thread", "while (true", "for (;;" + ]; + + [HttpPost("ScriptAssistant/Generate")] + public GenerateForwardScriptOutput Generate([FromBody] [Required] GenerateForwardScriptInput input) + { + var requirement = input.Requirement.Trim(); + if (requirement.IsNullOrWhiteSpace()) + { + throw new BusinessException(message: "请输入脚本需求"); + } + + var script = GenerateTemplateScript(input); + var output = new GenerateForwardScriptOutput + { + Script = script, + Description = "已根据需求生成可编辑脚本模板,请先校验并预览后再保存到接口配置。" + }; + + output.Warnings.AddRange(GetWarnings(script)); + return output; + } + + [HttpPost("ScriptAssistant/Validate")] + public ValidateForwardScriptOutput Validate([FromBody] [Required] ValidateForwardScriptInput input) + { + var output = new ValidateForwardScriptOutput(); + output.Warnings.AddRange(GetWarnings(input.Script)); + + try + { + NatashaScriptCompiler.CompileAsyncFunc(NormalizeScript(input.Script)); + output.Success = true; + } + catch (Exception ex) + { + output.Success = false; + output.Error = ex.Message; + } + + return output; + } + + [HttpPost("ScriptAssistant/Preview")] + public async Task Preview([FromBody] [Required] PreviewForwardScriptInput input) + { + var output = new PreviewForwardScriptOutput(); + try + { + var context = BuildPreviewContext(input); + var func = NatashaScriptCompiler.CompileAsyncFunc(NormalizeScript(input.Script)); + await func(context); + + output.Success = true; + output.OriginalInput = context.OriginalInterfaceInput; + output.MappedInput = context.OriginalInterfaceMappedInput; + output.TargetOutput = context.TargetInterfaceOutput; + output.FixedFields = context.FixedFieldList; + output.FormFields = context.FormFieldList; + } + catch (Exception ex) + { + output.Success = false; + output.Error = ex.Message; + } + + return output; + } + + [HttpPost("ScriptAssistant/Explain")] + public ExplainForwardScriptOutput Explain([FromBody] [Required] ValidateForwardScriptInput input) + { + var output = new ExplainForwardScriptOutput + { + Description = BuildExplanation(input.Script) + }; + output.Warnings.AddRange(GetWarnings(input.Script)); + return output; + } + + static string GenerateTemplateScript(GenerateForwardScriptInput input) + { + var builder = new StringBuilder(); + builder.AppendLine("// 请按需修改生成的模板脚本,保存前先执行校验和预览。"); + + foreach (var field in input.AccountFields.Where(x => !x.IsNullOrWhiteSpace()).Distinct()) + { + var variableName = ToVariableName(field); + builder.AppendLine($"var {variableName} = context.GetAccountFieldValueOrNull(\"{Escape(field)}\") ?? string.Empty;"); + } + + foreach (var header in input.Headers.Where(x => !x.IsNullOrWhiteSpace()).Distinct()) + { + builder.AppendLine($"context.AddHeader(\"{Escape(header)}\", string.Empty);"); + } + + foreach (var query in input.Queries.Where(x => !x.IsNullOrWhiteSpace()).Distinct()) + { + builder.AppendLine("context.FixedFieldList.Add(new FixedFieldWithValueDto"); + builder.AppendLine("{"); + builder.AppendLine($" FieldName = \"{Escape(query)}\","); + builder.AppendLine(" FieldValue = string.Empty,"); + builder.AppendLine(" FieldPosition = FieldPosition.Query"); + builder.AppendLine("});"); + } + + foreach (var formField in input.FormFields.Where(x => !x.IsNullOrWhiteSpace()).Distinct()) + { + builder.AppendLine("context.FormFieldList.Add(new InterfaceFormFieldDto"); + builder.AppendLine("{"); + builder.AppendLine($" Name = \"{Escape(formField)}\","); + builder.AppendLine(" Value = string.Empty"); + builder.AppendLine("});"); + } + + if (builder.Length == 0 || builder.ToString().Split(Environment.NewLine).Length <= 2) + { + builder.AppendLine("// 示例:读取账号字段并添加到 Header。"); + builder.AppendLine("var appKey = context.GetAccountFieldValueOrNull(\"appKey\") ?? string.Empty;"); + builder.AppendLine("context.AddHeader(\"appKey\", appKey);"); + } + + return builder.ToString(); + } + + static ForwardCoreContext BuildPreviewContext(PreviewForwardScriptInput input) + { + return new ForwardCoreContext + { + OriginalInterfaceInput = input.OriginalInput ?? new JObject(), + OriginalInterfaceMappedInput = input.MappedInput ?? new JObject(), + TargetInterfaceOutput = input.TargetOutput ?? new JObject(), + TargetInterfaceInput = string.Empty, + TargetInterface = new InterfaceDto(), + ServiceProviderRequestLog = new RequestLogDto(Guid.NewGuid()), + AccountFieldList = input.AccountFields.Select(x => new ServiceProviderAccountFieldWithValueDto + { + FieldName = x.Key, + FieldValue = x.Value + }).ToList() + }; + } + + static string NormalizeScript(string script) + { + return script.StartsWith("//async") || script.Contains("await") ? script : "//async" + Environment.NewLine + script; + } + + static List GetWarnings(string script) + { + var warnings = new List(); + foreach (var token in DangerousTokens) + { + if (script.Contains(token, StringComparison.OrdinalIgnoreCase)) + { + warnings.Add($"脚本包含高风险内容:{token}"); + } + } + + return warnings; + } + + static string BuildExplanation(string script) + { + var lines = new List(); + if (script.Contains("GetAccountFieldValueOrNull", StringComparison.OrdinalIgnoreCase)) + { + lines.Add("会读取服务商账号字段。"); + } + + if (script.Contains("AddHeader", StringComparison.OrdinalIgnoreCase) || script.Contains("FieldPosition.Header", StringComparison.OrdinalIgnoreCase)) + { + lines.Add("会向请求 Header 添加字段。"); + } + + if (script.Contains("FieldPosition.Query", StringComparison.OrdinalIgnoreCase)) + { + lines.Add("会向请求 Query 添加字段。"); + } + + if (script.Contains("FormFieldList", StringComparison.OrdinalIgnoreCase)) + { + lines.Add("会修改表单字段。"); + } + + if (script.Contains("TargetInterfaceOutput", StringComparison.OrdinalIgnoreCase)) + { + lines.Add("会读取或修改服务商响应结果。"); + } + + return lines.Count == 0 ? "暂未识别到具体操作,请查看脚本文本。" : string.Join(Environment.NewLine, lines); + } + + static string ToVariableName(string value) + { + var name = Regex.Replace(value, "[^a-zA-Z0-9_]", "_"); + if (name.Length == 0 || char.IsDigit(name[0])) + { + name = "field_" + name; + } + + return name; + } + + static string Escape(string value) + { + return value.Replace("\\", "\\\\").Replace("\"", "\\\""); + } +} diff --git a/src/InterfaceForward.ServiceDefaults/Extensions.cs b/src/InterfaceForward.ServiceDefaults/Extensions.cs deleted file mode 100644 index 6b236cd..0000000 --- a/src/InterfaceForward.ServiceDefaults/Extensions.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; -using OpenTelemetry.Metrics; -using OpenTelemetry.Trace; - -namespace Microsoft.Extensions.Hosting; - -public static class Extensions -{ - public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) - { - builder.ConfigureOpenTelemetry(); - - builder.AddDefaultHealthChecks(); - - builder.Services.AddServiceDiscovery(); - - builder.Services.ConfigureHttpClientDefaults(http => - { - // Turn on resilience by default - http.AddStandardResilienceHandler(); - - // Turn on service discovery by default - http.UseServiceDiscovery(); - }); - - return builder; - } - - public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) - { - builder.Logging.AddOpenTelemetry(logging => - { - logging.IncludeFormattedMessage = true; - logging.IncludeScopes = true; - }); - - builder.Services.AddOpenTelemetry() - .WithMetrics(metrics => - { - metrics.AddRuntimeInstrumentation() - .AddBuiltInMeters(); - }) - .WithTracing(tracing => - { - if (builder.Environment.IsDevelopment()) - { - // We want to view all traces in development - tracing.SetSampler(new AlwaysOnSampler()); - } - - tracing.AddAspNetCoreInstrumentation() - .AddGrpcClientInstrumentation() - .AddHttpClientInstrumentation(); - }); - - builder.AddOpenTelemetryExporters(); - - return builder; - } - - private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) - { - var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); - - if (useOtlpExporter) - { - builder.Services.Configure(logging => logging.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); - } - - // Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - // builder.Services.AddOpenTelemetry() - // .WithMetrics(metrics => metrics.AddPrometheusExporter()); - - // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.Exporter package) - // builder.Services.AddOpenTelemetry() - // .UseAzureMonitor(); - - return builder; - } - - public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) - { - builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); - - return builder; - } - - public static WebApplication MapDefaultEndpoints(this WebApplication app) - { - // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - // app.MapPrometheusScrapingEndpoint(); - - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); - - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions - { - Predicate = r => r.Tags.Contains("live") - }); - - return app; - } - - private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) => - meterProviderBuilder.AddMeter( - "Microsoft.AspNetCore.Hosting", - "Microsoft.AspNetCore.Server.Kestrel", - "System.Net.Http"); -} \ No newline at end of file diff --git a/src/InterfaceForward.ServiceDefaults/InterfaceForward.ServiceDefaults.csproj b/src/InterfaceForward.ServiceDefaults/InterfaceForward.ServiceDefaults.csproj deleted file mode 100644 index 2da4ee2..0000000 --- a/src/InterfaceForward.ServiceDefaults/InterfaceForward.ServiceDefaults.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Library - net8.0 - enable - enable - true - - - - - - - - - - - - - - - - diff --git a/tests/InterfaceForward.Application.Tests/InterfaceForward.Application.Tests.csproj b/tests/InterfaceForward.Application.Tests/InterfaceForward.Application.Tests.csproj new file mode 100644 index 0000000..81f0315 --- /dev/null +++ b/tests/InterfaceForward.Application.Tests/InterfaceForward.Application.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/tests/InterfaceForward.Application.Tests/UnitTest1.cs b/tests/InterfaceForward.Application.Tests/UnitTest1.cs new file mode 100644 index 0000000..0aee79f --- /dev/null +++ b/tests/InterfaceForward.Application.Tests/UnitTest1.cs @@ -0,0 +1,74 @@ +using InterfaceForward.Application.Contracts.Dtos.ScriptAssistant; +using InterfaceForward.Application.Services.ScriptAssistant; +using Newtonsoft.Json.Linq; + +namespace InterfaceForward.Application.Tests; + +public class ScriptAssistantServiceTests +{ + static ScriptAssistantServiceTests() + { + NatashaManagement.Preheating(false, false); + } + + [Fact] + public void Generate_ShouldCreateTemplateFromFields() + { + var service = new ScriptAssistantService(); + + var output = service.Generate(new GenerateForwardScriptInput + { + Requirement = "把 appKey 放到 header", + AccountFields = ["appKey"], + Headers = ["appKey"] + }); + + Assert.Contains("GetAccountFieldValueOrNull(\"appKey\")", output.Script); + Assert.Contains("context.AddHeader(\"appKey\"", output.Script); + } + + [Fact] + public void Validate_ShouldCompileGeneratedScript() + { + var service = new ScriptAssistantService(); + + var output = service.Validate(new ValidateForwardScriptInput + { + Script = "var appKey = context.GetAccountFieldValueOrNull(\"appKey\") ?? string.Empty;\ncontext.AddHeader(\"appKey\", appKey);" + }); + + Assert.True(output.Success, output.Error); + } + + [Fact] + public async Task Preview_ShouldRunScriptAndReturnChangedFields() + { + var service = new ScriptAssistantService(); + + var output = await service.Preview(new PreviewForwardScriptInput + { + Script = "var appKey = context.GetAccountFieldValueOrNull(\"appKey\") ?? string.Empty;\ncontext.AddHeader(\"appKey\", appKey);", + OriginalInput = new JObject { ["orderNo"] = "A001" }, + MappedInput = new JObject { ["targetOrderNo"] = "A001" }, + AccountFields = new Dictionary { ["appKey"] = "test-key" } + }); + + Assert.True(output.Success, output.Error); + var field = Assert.Single(output.FixedFields); + Assert.Equal("appKey", field.FieldName); + Assert.Equal("test-key", field.FieldValue); + } + + [Fact] + public void Validate_ShouldReturnWarningForDangerousToken() + { + var service = new ScriptAssistantService(); + + var output = service.Validate(new ValidateForwardScriptInput + { + Script = "System.IO.File.ReadAllText(\"a.txt\");" + }); + + Assert.Contains(output.Warnings, x => x.Contains("System.IO")); + } +} \ No newline at end of file