feat: script mcp

This commit is contained in:
xiaolipro 2026-06-03 11:44:16 +08:00
parent 4f31bba29d
commit 432ea493d0
21 changed files with 427 additions and 224 deletions

View File

@ -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

View File

@ -9,7 +9,6 @@
<ItemGroup>
<ProjectReference Include="..\InterfaceForward.Application\InterfaceForward.Application.csproj" />
<ProjectReference Include="..\InterfaceForward.ServiceDefaults\InterfaceForward.ServiceDefaults.csproj"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.18" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />

View File

@ -106,9 +106,6 @@ public class InterfaceForwardApiModule : FakeModule
app.UseFakeExceptionHandling();
// Add Aspire default endpoints.
app.MapDefaultEndpoints();
// 虚拟路由-网关适配
app.UsePathBase(app.Configuration["ASPNETCORE_PATHBASE"]);

View File

@ -15,9 +15,6 @@ try
builder.Host.UseAutofac().UseSerilog();
builder.Services.AddApplication<InterfaceForwardApiModule>();
// Add service defaults & Aspire components.
builder.AddServiceDefaults();
var app = builder.Build();
app.InitializeApplication();
app.Run();

View File

@ -1,20 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting" Version="8.2.2" />
<PackageReference Include="Aspire.Hosting.AppHost" Version="8.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\InterfaceForward.Api\InterfaceForward.Api.csproj" />
</ItemGroup>
</Project>

View File

@ -1,5 +0,0 @@
var builder = DistributedApplication.CreateBuilder(args);
builder.AddProject<Projects.InterfaceForward_Api>("apiservice");
builder.Build().Run();

View File

@ -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"
}
}
}
}

View File

@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}

View File

@ -0,0 +1,8 @@
namespace InterfaceForward.Application.Contracts.Dtos.ScriptAssistant;
public class ExplainForwardScriptOutput
{
public string Description { get; set; } = string.Empty;
public List<string> Warnings { get; set; } = [];
}

View File

@ -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<string> AccountFields { get; set; } = [];
public List<string> Headers { get; set; } = [];
public List<string> Queries { get; set; } = [];
public List<string> FormFields { get; set; } = [];
}

View File

@ -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<string> Warnings { get; set; } = [];
}

View File

@ -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<string, string> AccountFields { get; set; } = [];
}

View File

@ -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<FixedFieldWithValueDto> FixedFields { get; set; } = [];
public List<InterfaceFormFieldDto> FormFields { get; set; } = [];
}

View File

@ -0,0 +1,6 @@
namespace InterfaceForward.Application.Contracts.Dtos.ScriptAssistant;
public class ValidateForwardScriptInput
{
public string Script { get; set; } = string.Empty;
}

View File

@ -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<string> Warnings { get; set; } = [];
}

View File

@ -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<PreviewForwardScriptOutput> 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<string> GetWarnings(string script)
{
var warnings = new List<string>();
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<string>();
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("\"", "\\\"");
}
}

View File

@ -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<OpenTelemetryLoggerOptions>(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");
}

View File

@ -1,24 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="8.0.0-preview.1.23557.2"/>
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.0-beta.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.8.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.7.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.7.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.7.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.7.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\InterfaceForward.Application\InterfaceForward.Application.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@ -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<NatashaDomainCreator>(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<string, string> { ["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"));
}
}