diff --git a/KitX Script/Kscript.CSharp.Compiler/Examples/EndToEndTest.cs b/KitX Script/Kscript.CSharp.Compiler/Examples/EndToEndTest.cs new file mode 100644 index 0000000..1cc63e8 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Compiler/Examples/EndToEndTest.cs @@ -0,0 +1,392 @@ +using Kscript.CSharp.Parser; +using Kscript.CSharp.Parser.Core; +using Kscript.CSharp.Compiler; +using Kscript.CSharp.Parser.Models; +using KitX.Shared.CSharp.Plugin; +using System.Reflection; + +namespace Kscript.CSharp.Compiler.Examples; + +/// +/// 端到端测试:用户脚本 → Compiler → Parser → PluginManager调用 +/// 验证完整的调用链是否正常工作 +/// +public static class EndToEndTest +{ + /// + /// 运行端到端测试 + /// + public static async Task RunTest() + { + Console.WriteLine("=== KitX.CSharp.Compiler 端到端测试 ===\n"); + + try + { + // 1. 初始化插件管理器 + Console.WriteLine("1. 初始化插件管理器..."); + Parser.Parser.SetPluginManager(new MockPluginManager()); + + // 2. 创建测试插件清单 + Console.WriteLine("2. 创建测试插件清单..."); + var plugins = CreateTestPlugins(); + + // 3. 生成动态程序集 + Console.WriteLine("3. 生成动态程序集..."); + var assembly = Parser.Parser.Generate(plugins, "EndToEndTestAssembly"); + Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); + + // 4. 验证生成的程序集 + Console.WriteLine("\n4. 验证生成的程序集..."); + await ValidateGeneratedAssembly(assembly); + + // 5. 创建脚本执行器并测试 + Console.WriteLine("\n5. 创建脚本执行器并测试..."); + await TestScriptExecution(assembly); + + // 6. 测试错误处理 + Console.WriteLine("\n6. 测试错误处理..."); + await TestErrorHandling(assembly); + + // 7. 性能测试 + Console.WriteLine("\n7. 性能测试..."); + await PerformanceTest(assembly); + + Console.WriteLine("\n=== 端到端测试完成 ==="); + Console.WriteLine("✅ 所有测试通过,C#脚本可以成功调用生成的插件方法!"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ 端到端测试失败: {ex.Message}"); + Console.WriteLine($"详细错误信息:\n{ex}"); + } + } + + /// + /// 创建测试插件数据 + /// + /// 插件信息列表 + private static List CreateTestPlugins() + { + return new List + { + new PluginInfo + { + Name = "SampleCalculator", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Add", + ReturnValueType = "int", + Parameters = new List + { + new Parameter { Name = "a", Type = "int", IsOptional = false }, + new Parameter { Name = "b", Type = "int", IsOptional = false } + } + }, + new Function + { + Name = "Multiply", + ReturnValueType = "double", + Parameters = new List + { + new Parameter { Name = "x", Type = "double", IsOptional = false }, + new Parameter { Name = "y", Type = "double", IsOptional = false } + } + }, + new Function + { + Name = "Divide", + ReturnValueType = "double", + Parameters = new List + { + new Parameter { Name = "numerator", Type = "double", IsOptional = false }, + new Parameter { Name = "denominator", Type = "double", IsOptional = false }, + new Parameter { Name = "decimals", Type = "int", IsOptional = true, Value = "2" } + } + } + } + }, + new PluginInfo + { + Name = "StringToolkit", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Reverse", + ReturnValueType = "string", + Parameters = new List + { + new Parameter { Name = "text", Type = "string", IsOptional = false } + } + }, + new Function + { + Name = "Concat", + ReturnValueType = "string", + Parameters = new List + { + new Parameter { Name = "str1", Type = "string", IsOptional = false }, + new Parameter { Name = "str2", Type = "string", IsOptional = false } + } + } + } + }, + new PluginInfo + { + Name = "KitXWF", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Print", + ReturnValueType = "void", + Parameters = new List + { + new Parameter { Name = "message", Type = "string", IsOptional = false } + } + } + } + } + }; + } + + /// + /// 验证生成的程序集 + /// + /// 生成的程序集 + private static async Task ValidateGeneratedAssembly(Assembly assembly) + { + Console.WriteLine(" 验证程序集结构..."); + + // 获取所有插件类型 + var types = Parser.Parser.GetPluginTypes(assembly); + Console.WriteLine($" 发现 {types.Count} 个插件类型:"); + + foreach (var type in types) + { + Console.WriteLine($" - {type.Name}"); + + // 获取所有方法 + var methods = Parser.Parser.GetPluginMethods(type); + Console.WriteLine($" 包含 {methods.Count} 个方法:"); + + foreach (var method in methods) + { + var parameters = method.GetParameters(); + var paramList = string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}")); + Console.WriteLine($" - {method.ReturnType.Name} {method.Name}({paramList})"); + } + } + + Console.WriteLine(" ✓ 程序集结构验证完成"); + } + + /// + /// 测试脚本执行 + /// + /// 生成的程序集 + private static async Task TestScriptExecution(Assembly assembly) + { + using var executor = new ScriptExecutor(); + + // 添加程序集引用 + executor.AddAssemblyReference(assembly); + Console.WriteLine(" ✓ 已添加程序集引用到脚本执行器"); + + // 测试基本计算脚本 + Console.WriteLine("\n 测试基本计算脚本..."); + string basicScript = @" + // 测试加法 + var sum = SampleCalculator.Add(10, 20); + KitXWF.Print(string.Format(""加法结果: {0}"", sum)); + + // 测试乘法 + var product = SampleCalculator.Multiply(3.5, 2.8); + KitXWF.Print(string.Format(""乘法结果: {0}"", product)); + + // 测试字符串反转 + var reversed = StringToolkit.Reverse(""Hello KitX""); + KitXWF.Print(string.Format(""字符串反转结果: {0}"", reversed)); + + return new { Sum = sum, Product = product, Reversed = reversed }; + "; + + var basicResult = await executor.ExecuteAsync(basicScript, "BasicTest"); + Console.WriteLine($" ✓ 基本脚本执行成功: Sum={basicResult.Sum}, Product={basicResult.Product}, Reversed={basicResult.Reversed}"); + + // 测试复杂业务逻辑脚本 + Console.WriteLine("\n 测试复杂业务逻辑脚本..."); + string complexScript = @" + // 模拟订单处理 + var orderItems = new[] + { + new { Name = ""商品A"", Price = 10.5, Quantity = 2 }, + new { Name = ""商品B"", Price = 25.0, Quantity = 1 }, + new { Name = ""商品C"", Price = 5.75, Quantity = 3 } + }; + + var totalAmount = 0.0; + var itemCount = orderItems.Length; + + foreach (var item in orderItems) + { + var itemTotal = SampleCalculator.Multiply(item.Price, (double)item.Quantity); + totalAmount = totalAmount + itemTotal; + KitXWF.Print(string.Format(""处理商品: {0}, 小计: {1}"", item.Name, itemTotal)); + } + + // 计算平均价格 + var averagePrice = SampleCalculator.Divide(totalAmount, (double)itemCount, 2); + + // 生成订单摘要 + var summary = StringToolkit.Concat(string.Format(""订单总额: {0}, 平均价格: {1}"", totalAmount, averagePrice), "" (已处理)""); + + return new { + TotalAmount = totalAmount, + ItemCount = itemCount, + AveragePrice = averagePrice, + Summary = summary + }; + "; + + var complexResult = await executor.ExecuteAsync(complexScript, "ComplexTest"); + Console.WriteLine($" ✓ 复杂脚本执行成功: Total={complexResult.TotalAmount}, Items={complexResult.ItemCount}, Avg={complexResult.AveragePrice}"); + + // 测试脚本验证功能 + Console.WriteLine("\n 测试脚本验证功能..."); + var validScript = "var x = 10; return x * 2;"; + var invalidScript = "var x = 10; return x *; // 语法错误"; + + var validValidation = executor.ValidateScript(validScript); + var invalidValidation = executor.ValidateScript(invalidScript); + + Console.WriteLine($" ✓ 有效脚本验证: {validValidation.IsValid} - {validValidation.Message}"); + Console.WriteLine($" ✓ 无效脚本验证: {!invalidValidation.IsValid} - {invalidValidation.Message}"); + + // 显示执行器状态 + var status = executor.GetStatus(); + Console.WriteLine($" 执行器状态: {status.ReferencedAssembliesCount} 个程序集, {status.GlobalVariablesCount} 个全局变量"); + } + + /// + /// 测试错误处理 + /// + /// 生成的程序集 + private static async Task TestErrorHandling(Assembly assembly) + { + using var executor = new ScriptExecutor(); + executor.AddAssemblyReference(assembly); + + // 测试编译错误 + Console.WriteLine(" 测试编译错误处理..."); + try + { + var syntaxErrorScript = "var x = 10; var y = ; return x + y;"; // 故意语法错误 + await executor.ExecuteAsync(syntaxErrorScript); + Console.WriteLine(" ❌ 应该捕获编译错误但没有"); + } + catch (ScriptExecutionException ex) + { + Console.WriteLine($" ✓ 成功捕获编译错误: {ex.Message}"); + } + + // 测试运行时错误 + Console.WriteLine("\n 测试运行时错误处理..."); + try + { + var runtimeErrorScript = @" + var result = SampleCalculator.Divide(10, 0); // 除零错误 + return result; + "; + + await executor.ExecuteAsync(runtimeErrorScript); + Console.WriteLine(" ❌ 应该捕获运行时错误但没有"); + } + catch (ScriptExecutionException ex) + { + Console.WriteLine($" ✓ 成功捕获运行时错误: {ex.Message}"); + } + + // 测试插件方法不存在错误 + Console.WriteLine("\n 测试插件方法不存在错误..."); + try + { + var methodNotFoundScript = "var result = SampleCalculator.NonExistentMethod(10, 20); return result;"; + await executor.ExecuteAsync(methodNotFoundScript); + Console.WriteLine(" ❌ 应该捕获方法不存在错误但没有"); + } + catch (ScriptExecutionException ex) + { + Console.WriteLine($" ✓ 成功捕获方法不存在错误: {ex.Message}"); + } + } + + /// + /// 性能测试 + /// + /// 生成的程序集 + private static async Task PerformanceTest(Assembly assembly) + { + using var executor = new ScriptExecutor(); + executor.AddAssemblyReference(assembly); + + Console.WriteLine(" 执行性能测试..."); + + var testScript = @" + using System.Collections.Generic; + var results = new List(); + for (int i = 0; i < 10; i++) + { + var sum = SampleCalculator.Add(i, i * 2); + results.Add(sum); + } + return results.Count; + "; + + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + // 执行多次取平均值 + const int iterations = 10; + var times = new List(); + + for (int i = 0; i < iterations; i++) + { + stopwatch.Restart(); + await executor.ExecuteAsync(testScript, $"PerfTest_{i}"); + times.Add(stopwatch.ElapsedMilliseconds); + } + + var avgTime = times.Average(); + var minTime = times.Min(); + var maxTime = times.Max(); + + Console.WriteLine($" 性能测试结果 ({iterations} 次执行):"); + Console.WriteLine($" - 平均执行时间: {avgTime:F2} ms"); + Console.WriteLine($" - 最快执行时间: {minTime} ms"); + Console.WriteLine($" - 最慢执行时间: {maxTime} ms"); + Console.WriteLine($" - 性能评级: {GetPerformanceRating(avgTime)}"); + } + + /// + /// 获取性能评级 + /// + /// 平均执行时间 + /// 性能评级 + private static string GetPerformanceRating(double avgTime) + { + return avgTime switch + { + < 10 => "优秀", + < 50 => "良好", + < 100 => "一般", + < 200 => "较差", + _ => "很差" + }; + } +} + diff --git a/KitX Script/Kscript.CSharp.Compiler/Examples/ScriptExecutionExample.cs b/KitX Script/Kscript.CSharp.Compiler/Examples/ScriptExecutionExample.cs new file mode 100644 index 0000000..149319a --- /dev/null +++ b/KitX Script/Kscript.CSharp.Compiler/Examples/ScriptExecutionExample.cs @@ -0,0 +1,259 @@ +using Kscript.CSharp.Parser; +using Kscript.CSharp.Parser.Core; +using Kscript.CSharp.Compiler; +using Kscript.CSharp.Parser.Models; +using System.Text.Json; +using System.Reflection; +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Compiler.Examples; + +/// +/// C#脚本执行示例 +/// +public static class ScriptExecutionExample +{ + /// + /// 运行脚本执行示例 + /// + public static async Task RunExample() + { + Console.WriteLine("=== Kscript.CSharp.Compiler 脚本执行示例 ===\n"); + + try + { + // 1. 初始化插件管理器 + Console.WriteLine("1. 初始化插件管理器..."); + Parser.Parser.SetPluginManager(new MockPluginManager()); + + // 2. 创建示例插件数据 + var plugins = new List + { + new PluginInfo + { + Name = "SampleCalculator", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Add", + ReturnValueType = "int", + Parameters = new List + { + new Parameter { Name = "a", Type = "int", IsOptional = false }, + new Parameter { Name = "b", Type = "int", IsOptional = false } + } + }, + new Function + { + Name = "Multiply", + ReturnValueType = "double", + Parameters = new List + { + new Parameter { Name = "x", Type = "double", IsOptional = false }, + new Parameter { Name = "y", Type = "double", IsOptional = false } + } + } + } + }, + new PluginInfo + { + Name = "StringToolkit", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Reverse", + ReturnValueType = "string", + Parameters = new List + { + new Parameter { Name = "text", Type = "string", IsOptional = false } + } + }, + new Function + { + Name = "ToUpper", + ReturnValueType = "string", + Parameters = new List + { + new Parameter { Name = "text", Type = "string", IsOptional = false } + } + } + } + }, + new PluginInfo + { + Name = "KitXWF", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Print", + ReturnValueType = "void", + Parameters = new List + { + new Parameter { Name = "message", Type = "string", IsOptional = false } + } + } + } + } + }; + + // 3. 生成动态程序集 + Console.WriteLine("2. 生成动态程序集..."); + var assembly = Parser.Parser.Generate(plugins, "ScriptExampleAssembly"); + Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); + + // 4. 创建脚本执行器 + Console.WriteLine("\n3. 创建脚本执行器..."); + using var executor = new ScriptExecutor(); + + // 5. 添加程序集引用 + executor.AddAssemblyReference(assembly); + Console.WriteLine(" ✓ 已添加程序集引用"); + + // 6. 设置全局变量 + executor.SetGlobalVariable("PI", Math.PI); + executor.SetGlobalVariable("AppName", "KitX Script Engine"); + Console.WriteLine(" ✓ 已设置全局变量"); + + // 7. 执行简单计算脚本 + Console.WriteLine("\n4. 执行简单计算脚本..."); + string simpleScript = @" + var sum = SampleCalculator.Add(15, 25); + var product = SampleCalculator.Multiply(3.5, 2.8); + return new { Sum = sum, Product = product }; + "; + + var simpleResult = await executor.ExecuteAsync(simpleScript); + Console.WriteLine($" 计算结果: Sum = {simpleResult.Sum}, Product = {simpleResult.Product}"); + + // 8. 执行字符串处理脚本 + Console.WriteLine("\n5. 执行字符串处理脚本..."); + string stringScript = @" + var reversed = StringToolkit.Reverse(""Hello World""); + var upper = StringToolkit.ToUpper(""kitx script""); + return new { Original = ""Hello World"", Reversed = reversed, Upper = upper }; + "; + + var stringResult = await executor.ExecuteAsync(stringScript); + Console.WriteLine($" 字符串处理结果:"); + Console.WriteLine($" Original: {stringResult.Original}"); + Console.WriteLine($" Reversed: {stringResult.Reversed}"); + Console.WriteLine($" Upper: {stringResult.Upper}"); + + // 9. 执行复杂业务逻辑脚本 + Console.WriteLine("\n6. 执行复杂业务逻辑脚本..."); + string complexScript = @" + // 模拟一个简单的业务场景:计算订单总价 + var orderItems = new[] + { + new { Name = ""商品A"", Price = 10.5, Quantity = 2 }, + new { Name = ""商品B"", Price = 25.0, Quantity = 1 }, + new { Name = ""商品C"", Price = 5.75, Quantity = 3 } + }; + + var total = 0.0; + foreach (var item in orderItems) + { + var itemTotal = SampleCalculator.Multiply(item.Price, item.Quantity); + total = total + itemTotal; // 直接使用double加法 + KitXWF.Print(string.Format(""商品: {0}, 单价: {1}, 数量: {2}, 小计: {3}"", item.Name, item.Price, item.Quantity, itemTotal)); + } + + // 使用字符串处理生成订单摘要 + var summary = StringToolkit.ToUpper(string.Format(""订单总价: {0}"", total)); + return new { Total = total, Summary = summary, ItemCount = orderItems.Length }; + "; + + var complexResult = await executor.ExecuteAsync(complexScript); + Console.WriteLine($" 复杂业务逻辑结果:"); + Console.WriteLine($" 订单总价: {complexResult.Total}"); + Console.WriteLine($" 订单摘要: {complexResult.Summary}"); + Console.WriteLine($" 商品数量: {complexResult.ItemCount}"); + + // 10. 显示执行器状态 + Console.WriteLine("\n7. 脚本执行器状态:"); + var status = executor.GetStatus(); + Console.WriteLine($" 引用程序集数量: {status.ReferencedAssembliesCount}"); + Console.WriteLine($" 全局变量数量: {status.GlobalVariablesCount}"); + Console.WriteLine($" 命名空间数量: {status.UsingsCount}"); + Console.WriteLine($" 程序集列表: {string.Join("", "", status.AssemblyNames)}"); + + // 11. 脚本验证示例 + Console.WriteLine("\n8. 脚本验证示例:"); + var validScript = "var x = 10; var y = 20; return x + y;"; + var invalidScript = "var x = 10; var y = ; return x + y;"; // 故意的语法错误 + + var validResult = executor.ValidateScript(validScript); + Console.WriteLine($" 有效脚本验证: {validResult.IsValid} - {validResult.Message}"); + + var invalidResult = executor.ValidateScript(invalidScript); + Console.WriteLine($" 无效脚本验证: {invalidResult.IsValid} - {invalidResult.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ 示例执行失败: {ex.Message}"); + Console.WriteLine($"详细错误信息:\n{ex}"); + } + + Console.WriteLine("\n=== 脚本执行示例完成 ==="); + } + + /// + /// 演示脚本安全性和错误处理 + /// + public static async Task RunSecurityExample() + { + Console.WriteLine("=== 脚本安全性示例 ===\n"); + + using var executor = new ScriptExecutor(); + + // 1. 测试无限循环检测 + Console.WriteLine("1. 测试无限循环检测..."); + string infiniteLoopScript = @" + while (true) + { + // 这会导致无限循环 + } + "; + + try + { + // 设置超时机制(CSharpScript本身不支持,但可以通过其他方式实现) + var task = executor.ExecuteAsync(infiniteLoopScript); + var timeoutTask = Task.Delay(5000); // 5秒超时 + + var completedTask = await Task.WhenAny(task, timeoutTask); + if (completedTask == timeoutTask) + { + Console.WriteLine(" ✓ 检测到潜在的超时/无限循环"); + } + } + catch (Exception ex) + { + Console.WriteLine($" ✓ 安全机制生效: {ex.Message}"); + } + + // 2. 测试恶意代码检测 + Console.WriteLine("\n2. 测试恶意代码检测..."); + string maliciousScript = @" + // 尝试访问文件系统 + System.IO.File.WriteAllText(""malicious.txt"", ""This is malicious""); + return ""File created""; + "; + + try + { + var result = await executor.ExecuteAsync(maliciousScript); + Console.WriteLine(" ⚠️ 警告: 脚本执行成功,可能存在安全风险"); + } + catch (Exception ex) + { + Console.WriteLine($" ✓ 安全机制阻止了恶意操作: {ex.Message}"); + } + } +} diff --git a/KitX Script/Kscript.CSharp.Compiler/Kscript.CSharp.Compiler.csproj b/KitX Script/Kscript.CSharp.Compiler/Kscript.CSharp.Compiler.csproj new file mode 100644 index 0000000..1cadee8 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Compiler/Kscript.CSharp.Compiler.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + preview + enable + enable + Kscript.CSharp.Compiler + Exe + + + + + + + + + + + + + diff --git a/KitX Script/Kscript.CSharp.Compiler/Program.cs b/KitX Script/Kscript.CSharp.Compiler/Program.cs new file mode 100644 index 0000000..75319a1 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Compiler/Program.cs @@ -0,0 +1,44 @@ +using Kscript.CSharp.Compiler.Examples; + +namespace Kscript.CSharp.Compiler; + +/// +/// Kscript.CSharp.Compiler 主程序入口 +/// +public static class Program +{ + /// + /// 主入口方法 + /// + /// 命令行参数 + public static async Task Main(string[] args) + { + Console.OutputEncoding = System.Text.Encoding.UTF8; + Console.WriteLine("🚀 欢迎使用 KitX.CSharp.Compiler!"); + Console.WriteLine("==================================================\n"); + + try + { + // 运行脚本执行示例 + await ScriptExecutionExample.RunExample(); + + Console.WriteLine("\n" + new string('=', 60)); + + // 运行端到端测试 + await EndToEndTest.RunTest(); + + Console.WriteLine("\n=================================================="); + Console.WriteLine("✅ 所有示例运行完成!"); + Console.WriteLine("\n按任意键退出..."); + Console.ReadKey(); + } + catch (Exception ex) + { + Console.WriteLine($"\n❌ 程序运行失败: {ex.Message}"); + Console.WriteLine($"详细错误信息:\n{ex}"); + + Console.WriteLine("\n按任意键退出..."); + Console.ReadKey(); + } + } +} diff --git a/KitX Script/Kscript.CSharp.Compiler/ScriptExecutor.cs b/KitX Script/Kscript.CSharp.Compiler/ScriptExecutor.cs new file mode 100644 index 0000000..e648cf9 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Compiler/ScriptExecutor.cs @@ -0,0 +1,261 @@ +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; +using System.Reflection; +using System.IO; +using System.Reflection.Emit; +using Kscript.CSharp.Parser.Core; +using Kscript.CSharp.Parser.Models; + +namespace Kscript.CSharp.Compiler; + +/// +/// C#脚本执行器,用于执行用户编写的C#脚本并调用生成的插件方法 +/// +public class ScriptExecutor : IDisposable +{ + private readonly Dictionary _globalVariables = new(); + private readonly List _referencedAssemblies = new(); + private readonly List _usings = new(); + private ScriptOptions? _scriptOptions; + private bool _disposed = false; + + /// + /// 构造函数 + /// + public ScriptExecutor() + { + _scriptOptions = ScriptOptions.Default + .WithImports("System", "System.Math", "System.Collections.Generic", "System.Linq") + .WithEmitDebugInformation(true); + } + + /// + /// 添加程序集引用 + /// + /// 要引用的程序集 + public void AddAssemblyReference(Assembly assembly) + { + if (assembly == null) + throw new ArgumentNullException(nameof(assembly)); + + if (!_referencedAssemblies.Contains(assembly)) + { + _referencedAssemblies.Add(assembly); + Console.WriteLine($"[ScriptExecutor] 已添加程序集引用: {assembly.FullName}"); + } + } + + /// + /// 添加命名空间引用 + /// + /// 命名空间 + public void AddUsing(string @namespace) + { + if (string.IsNullOrEmpty(@namespace)) + throw new ArgumentException("命名空间不能为空", nameof(@namespace)); + + if (!_usings.Contains(@namespace)) + { + _usings.Add(@namespace); + Console.WriteLine($"[ScriptExecutor] 已添加命名空间引用: {@namespace}"); + } + } + + /// + /// 设置全局变量 + /// + /// 变量名 + /// 变量值 + public void SetGlobalVariable(string name, object value) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("变量名不能为空", nameof(name)); + + _globalVariables[name] = value; + Console.WriteLine($"[ScriptExecutor] 已设置全局变量: {name} = {value}"); + } + + /// + /// 执行C#脚本 + /// + /// 返回值类型 + /// 脚本代码 + /// 程序集名称(用于调试) + /// 执行结果 + public async Task ExecuteAsync(string script, string assemblyName = "ScriptAssembly") + { + if (string.IsNullOrEmpty(script)) + throw new ArgumentException("脚本代码不能为空", nameof(script)); + + try + { + Console.WriteLine($"[ScriptExecutor] 开始执行脚本: {assemblyName}"); + Console.WriteLine($"[ScriptExecutor] 脚本代码: {script.Substring(0, Math.Min(script.Length, 100))}..."); + + // 更新脚本选项,包含所有引用 + var options = ScriptOptions.Default + .WithReferences(_referencedAssemblies) + .WithImports(_usings.Concat(new[] { "System.Console" })) + .WithEmitDebugInformation(true); + + // 创建脚本对象 + var csharpScript = CSharpScript.Create(script, options); + + // 执行脚本(不使用全局变量) + var result = await csharpScript.RunAsync(null); + + Console.WriteLine($"[ScriptExecutor] 脚本执行成功,结果: {result.ReturnValue}"); + return result.ReturnValue; + } + catch (CompilationErrorException ex) + { + Console.WriteLine($"[ScriptExecutor] 脚本编译错误: {ex.Message}"); + throw new ScriptExecutionException($"脚本编译失败: {ex.Message}", ex); + } + catch (Exception ex) + { + Console.WriteLine($"[ScriptExecutor] 脚本执行异常: {ex.Message}"); + throw new ScriptExecutionException($"脚本执行失败: {ex.Message}", ex); + } + } + + /// + /// 执行脚本(无返回值) + /// + /// 脚本代码 + /// 程序集名称 + public async Task ExecuteAsync(string script, string assemblyName = "ScriptAssembly") + { + await ExecuteAsync(script, assemblyName); + } + + /// + /// 验证脚本语法 + /// + /// 脚本代码 + /// 验证结果 + public ScriptValidationResult ValidateScript(string script) + { + if (string.IsNullOrEmpty(script)) + return new ScriptValidationResult(false, "脚本代码不能为空"); + + try + { + // 更新脚本选项,包含所有引用 + var options = ScriptOptions.Default + .WithReferences(_referencedAssemblies) + .WithEmitDebugInformation(true); + + var csharpScript = CSharpScript.Create(script, options); + + // 尝试编译以检查语法 + var diagnostics = csharpScript.Compile(); + + if (!diagnostics.Any()) + { + return new ScriptValidationResult(true, "脚本语法正确"); + } + else + { + var errors = string.Join("\n", diagnostics.Select(d => d.ToString())); + return new ScriptValidationResult(false, $"脚本语法错误:\n{errors}"); + } + } + catch (Exception ex) + { + return new ScriptValidationResult(false, $"脚本验证异常: {ex.Message}"); + } + } + + /// + /// 获取当前状态信息 + /// + /// 状态信息 + public ScriptExecutorStatus GetStatus() + { + return new ScriptExecutorStatus + { + ReferencedAssembliesCount = _referencedAssemblies.Count, + GlobalVariablesCount = _globalVariables.Count, + UsingsCount = _usings.Count, + AssemblyNames = _referencedAssemblies.Select(a => a.GetName().Name).ToList() + }; + } + + /// + /// 清除所有引用和变量 + /// + public void Clear() + { + _referencedAssemblies.Clear(); + _globalVariables.Clear(); + _usings.Clear(); + Console.WriteLine("[ScriptExecutor] 已清除所有引用和变量"); + } + + /// + /// 释放资源 + /// + public void Dispose() + { + if (!_disposed) + { + Clear(); + _disposed = true; + Console.WriteLine("[ScriptExecutor] 资源已释放"); + } + } +} + +/// +/// 脚本全局变量容器 +/// +public class ScriptGlobals +{ + private readonly Dictionary _variables; + + public ScriptGlobals(Dictionary variables) + { + _variables = variables ?? new Dictionary(); + } + + /// + /// 动态属性访问器 + /// + public object this[string name] + { + get + { + if (_variables.TryGetValue(name, out var value)) + return value; + throw new KeyNotFoundException($"未找到全局变量: {name}"); + } + } +} + +/// +/// 脚本验证结果 +/// +public record ScriptValidationResult(bool IsValid, string Message); + +/// +/// 脚本执行器状态 +/// +public record ScriptExecutorStatus +{ + public int ReferencedAssembliesCount { get; init; } + public int GlobalVariablesCount { get; init; } + public int UsingsCount { get; init; } + public List AssemblyNames { get; init; } = new(); +} + +/// +/// 脚本执行异常 +/// +public class ScriptExecutionException : Exception +{ + public ScriptExecutionException(string message, Exception? innerException = null) + : base(message, innerException) + { + } +} diff --git a/KitX Script/Kscript.CSharp.Parser.Examples/Examples/BasicUsageExample.cs b/KitX Script/Kscript.CSharp.Parser.Examples/Examples/BasicUsageExample.cs new file mode 100644 index 0000000..6d15ce9 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser.Examples/Examples/BasicUsageExample.cs @@ -0,0 +1,265 @@ +using System.Reflection; +using System.Text.Json; +using KitX.Shared.CSharp.Plugin; +using Kscript.CSharp.Parser; +using Kscript.CSharp.Parser.Core; + +namespace Kscript.CSharp.Parser.Examples; + +/// +/// 基础用法示例 +/// +public static class BasicUsageExample +{ + /// + /// 演示基本功能 + /// + public static async Task RunExample() + { + Console.WriteLine("=== KitX.CSharp.Parser 基础用法示例 ===\n"); + + try + { + // 0. 初始化插件管理器 + Console.WriteLine("0. 初始化插件管理器..."); + if (!Parser.IsInitialized) + { + Parser.SetPluginManager(new MockPluginManager()); + Console.WriteLine(" ✓ 已设置 MockPluginManager"); + } + + // 1. 从现有的 example.json 文件加载插件清单 + Console.WriteLine("1. 加载插件清单..."); + var exampleJsonPath = Path.Combine(".", "example.json"); + if (!File.Exists(exampleJsonPath)) + { + Console.WriteLine($" 警告: example.json 文件不存在于 {exampleJsonPath}"); + Console.WriteLine(" 将使用内置示例数据"); + RunWithBuiltInData(); + return; + } + + var assembly = await Parser.GenerateFromFileAsync(exampleJsonPath, "ExamplePluginAssembly"); + Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); + + // 2. 获取生成的插件类型 + Console.WriteLine("\n2. 分析生成的插件类型..."); + var pluginTypes = Parser.GetPluginTypes(assembly); + Console.WriteLine($" 发现 {pluginTypes.Count} 个插件类:"); + + foreach (var type in pluginTypes) + { + Console.WriteLine($" - {type.Name}"); + var methods = Parser.GetPluginMethods(type); + foreach (var method in methods) + { + var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}")); + Console.WriteLine($" └── {method.ReturnType.Name} {method.Name}({parameters})"); + } + } + + // 3. 动态调用插件方法 + Console.WriteLine("\n3. 动态调用插件方法..."); + DynamicInvokeExample(assembly); + + // 4. 缓存统计 + Console.WriteLine("\n4. 缓存统计信息..."); + var stats = Parser.GetCacheStatistics(); + Console.WriteLine($" {stats}"); + + // 5. 演示缓存效果 + Console.WriteLine("\n5. 测试缓存效果..."); + + // 5.1 先检查是否存在缓存 + var plugins = JsonSerializer.Deserialize>(await File.ReadAllTextAsync(exampleJsonPath), new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true + }); + var hasCacheBefore = Parser.HasCache(plugins!, "ExamplePluginAssembly"); + Console.WriteLine($" 生成前检查缓存存在: {hasCacheBefore}"); + + // 5.2 第二次生成(应该使用缓存) + var assembly2 = await Parser.GenerateFromFileAsync(exampleJsonPath, "ExamplePluginAssembly"); + Console.WriteLine($" 第二次生成是否使用缓存: {assembly == assembly2}"); + + // 5.3 强制重新生成(应该绕过缓存) + var assembly3 = Parser.Generate(plugins!, "ExamplePluginAssembly", useCache: false); + Console.WriteLine($" 强制重新生成是否使用新程序集: {assembly3 != assembly2}"); + + // 5.4 检查强制重新生成后的缓存状态 + var hasCacheAfterRegenerate = Parser.HasCache(plugins!, "ExamplePluginAssembly"); + Console.WriteLine($" 重新生成后缓存存在: {hasCacheAfterRegenerate}"); + + } + catch (Exception ex) + { + Console.WriteLine($"❌ 示例执行失败: {ex.Message}"); + Console.WriteLine($"详细信息: {ex}"); + } + } + + /// + /// 使用内置数据运行示例 + /// + private static void RunWithBuiltInData() + { + // 确保插件管理器已初始化 + if (!Parser.IsInitialized) + { + Parser.SetPluginManager(new MockPluginManager()); + } + + // 创建示例插件数据 + var plugins = new List + { + new PluginInfo + { + Name = "TestCalculator", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Add", + ReturnValueType = "int", + Parameters = new List + { + new Parameter { Name = "a", Type = "int", IsOptional = false }, + new Parameter { Name = "b", Type = "int", IsOptional = false } + } + }, + new Function + { + Name = "Multiply", + ReturnValueType = "double", + Parameters = new List + { + new Parameter { Name = "x", Type = "double", IsOptional = false }, + new Parameter { Name = "y", Type = "double", IsOptional = false } + } + } + } + } + }; + + Console.WriteLine(" 使用内置示例数据..."); + var assembly = Parser.Generate(plugins, "BuiltInExampleAssembly"); + Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); + + DynamicInvokeExample(assembly); + } + + /// + /// 动态调用示例 + /// + private static void DynamicInvokeExample(Assembly assembly) + { + var pluginTypes = Parser.GetPluginTypes(assembly); + + foreach (var type in pluginTypes) + { + Console.WriteLine($"\n 调用插件: {type.Name}"); + var methods = Parser.GetPluginMethods(type); + + foreach (var method in methods) + { + try + { + object? result = null; + var parameters = method.GetParameters(); + + // 根据方法名和参数生成测试数据 + object[] args = GenerateTestArguments(method.Name, parameters); + + Console.WriteLine($" 调用 {method.Name}({string.Join(", ", args)})"); + + // 动态调用方法 + result = method.Invoke(null, args); + + Console.WriteLine($" → 结果: {result}"); + } + catch (Exception ex) + { + Console.WriteLine($" → 调用失败: {ex.Message}"); + } + } + } + } + + /// + /// 生成测试参数 + /// + private static object[] GenerateTestArguments(string methodName, ParameterInfo[] parameters) + { + var args = new object[parameters.Length]; + + for (int i = 0; i < parameters.Length; i++) + { + var param = parameters[i]; + + // 根据参数类型和方法名生成合适的测试值 + args[i] = param.ParameterType.Name switch + { + "Int32" => methodName switch + { + "Add" => i == 0 ? 10 : 20, + "Divide" when param.Name == "decimals" => 2, + _ => i + 1 + }, + "Double" => methodName switch + { + "Divide" => i == 0 ? 10.0 : 3.0, + "Multiply" => i == 0 ? 2.5 : 4.0, + _ => (i + 1) * 1.5 + }, + "String" => methodName switch + { + "Reverse" => "Hello", + "ToUpper" => "world", + _ => $"test{i}" + }, + _ => GetDefaultValue(param.ParameterType) + }; + } + + return args; + } + + /// + /// 获取类型默认值 + /// + private static object GetDefaultValue(Type type) + { + if (type.IsValueType) + return Activator.CreateInstance(type)!; + + return type == typeof(string) ? "default" : null!; + } + + /// + /// 演示高级功能 + /// + public static void RunAdvancedExample() + { + Console.WriteLine("\n=== 高级功能演示 ===\n"); + + try + { + // 1. 清除缓存 + Console.WriteLine("1. 清除所有缓存..."); + Parser.ClearCache(); + Console.WriteLine(" ✓ 缓存已清除"); + + // 2. 重新设置插件管理器 + Console.WriteLine("\n2. 重新设置插件管理器..."); + Parser.SetPluginManager(new MockPluginManager()); + Console.WriteLine(" ✓ 已重新设置插件管理器"); + + } + catch (Exception ex) + { + Console.WriteLine($"❌ 高级功能演示失败: {ex.Message}"); + } + } +} diff --git a/KitX Script/Kscript.CSharp.Parser.Examples/Examples/Program.cs b/KitX Script/Kscript.CSharp.Parser.Examples/Examples/Program.cs new file mode 100644 index 0000000..d9ae99c --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser.Examples/Examples/Program.cs @@ -0,0 +1,165 @@ +using Kscript.CSharp.Parser.Examples; +using Kscript.CSharp.Parser.Core; +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Parser.Examples; + +/// +/// 演示程序入口 +/// +public static class Program +{ + /// + /// 主入口方法 + /// + public static async Task Main(string[] args) + { + Console.OutputEncoding = System.Text.Encoding.UTF8; + Console.WriteLine("🚀 欢迎使用 KitX.CSharp.Parser 演示程序!"); + Console.WriteLine("==================================================\n"); + + try + { + // 首先运行简化后的功能测试 + Console.WriteLine("=== 测试简化后的 Parser 功能 ==="); + TestSimplifiedParser(); + + Console.WriteLine("\n" + new string('=', 60)); + + // 运行基础用法示例 + await BasicUsageExample.RunExample(); + + // 运行高级功能示例 + BasicUsageExample.RunAdvancedExample(); + + // 运行实际插件管理器示例 + Console.WriteLine("\n" + new string('=', 60)); + RealPluginManagerExample.RunExample(); + + Console.WriteLine("\n" + new string('=', 60)); + RealPluginManagerExample.DashboardIntegrationExample(); + + Console.WriteLine("\n=================================================="); + Console.WriteLine("✅ 所有示例运行完成!"); + } + catch (Exception ex) + { + Console.WriteLine($"\n❌ 程序运行失败: {ex.Message}"); + Console.WriteLine($"详细错误信息:\n{ex}"); + } + + Console.WriteLine("\n按任意键退出..."); + Console.ReadKey(); + } + + /// + /// 测试简化后的 Parser 功能 + /// + private static void TestSimplifiedParser() + { + Console.WriteLine("1. 测试默认插件管理器设置..."); + + // 测试使用 MockPluginManager 中已有的插件 + var testPlugins = new List + { + new PluginInfo + { + Name = "SampleCalculator", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Add", + ReturnValueType = "int", + Parameters = new List + { + new Parameter { Name = "a", Type = "int", IsOptional = false }, + new Parameter { Name = "b", Type = "int", IsOptional = false } + } + } + } + } + }; + + try + { + // 先设置插件管理器 + Parser.SetPluginManager(new MockPluginManager()); + + // 使用 MockPluginManager 生成程序集 + var assembly = Parser.Generate(testPlugins, "TestAssembly"); + Console.WriteLine(" ✓ 默认 MockPluginManager 工作正常"); + + // 获取生成的类型 + var types = Parser.GetPluginTypes(assembly); + Console.WriteLine($" ✓ 生成了 {types.Count} 个插件类型"); + + foreach (var type in types) + { + var methods = Parser.GetPluginMethods(type); + Console.WriteLine($" - {type.Name}: {methods.Count} 个方法"); + } + } + catch (Exception ex) + { + Console.WriteLine($" ❌ 测试失败: {ex.Message}"); + } + + Console.WriteLine("2. 测试自定义插件管理器设置..."); + try + { + // 设置新的插件管理器 + Parser.SetPluginManager(new MockPluginManager()); + var assembly2 = Parser.Generate(testPlugins, "TestAssembly2"); + Console.WriteLine(" ✓ 自定义插件管理器设置工作正常"); + } + catch (Exception ex) + { + Console.WriteLine($" ❌ 自定义设置测试失败: {ex.Message}"); + } + + Console.WriteLine("3. 测试空参数处理..."); + try + { + // 测试 null 参数应该抛出异常 + Parser.SetPluginManager(null!); + Console.WriteLine(" ❌ 空参数测试失败: 应该抛出异常但没有"); + } + catch (ArgumentNullException) + { + Console.WriteLine(" ✓ 空参数正确抛出 ArgumentNullException"); + } + catch (Exception ex) + { + Console.WriteLine($" ❌ 空参数测试失败: {ex.Message}"); + } + + Console.WriteLine("4. 测试插件存在性检测..."); + try + { + var mockManager = new MockPluginManager(); + Parser.SetPluginManager(mockManager); + + // 测试存在的插件 + var existsSampleCalculator = mockManager.IsPluginExists("SampleCalculator"); + Console.WriteLine($" ✓ SampleCalculator 存在: {existsSampleCalculator}"); + + var existsAddMethod = mockManager.IsMethodExists("SampleCalculator", "Add"); + Console.WriteLine($" ✓ SampleCalculator.Add 方法存在: {existsAddMethod}"); + + // 测试不存在的插件 + var existsNonExistent = mockManager.IsPluginExists("NonExistentPlugin"); + Console.WriteLine($" ✓ NonExistentPlugin 存在: {existsNonExistent}"); + + var existsNonExistentMethod = mockManager.IsMethodExists("SampleCalculator", "NonExistentMethod"); + Console.WriteLine($" ✓ SampleCalculator.NonExistentMethod 方法存在: {existsNonExistentMethod}"); + + Console.WriteLine(" ✓ 插件存在性检测功能工作正常"); + } + catch (Exception ex) + { + Console.WriteLine($" ❌ 插件存在性检测测试失败: {ex.Message}"); + } + } +} diff --git a/KitX Script/Kscript.CSharp.Parser.Examples/Examples/RealPluginManagerExample.cs b/KitX Script/Kscript.CSharp.Parser.Examples/Examples/RealPluginManagerExample.cs new file mode 100644 index 0000000..b44528e --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser.Examples/Examples/RealPluginManagerExample.cs @@ -0,0 +1,130 @@ +using KitX.Shared.CSharp.Plugin; +using Kscript.CSharp.Parser; +using Kscript.CSharp.Parser.Core; +using System.Text.Json; + +namespace Kscript.CSharp.Parser.Examples; + +/// +/// 实际插件管理器使用示例 +/// +public static class RealPluginManagerExample +{ + /// + /// 演示如何使用 RealPluginManager + /// + public static void RunExample() + { + Console.WriteLine("=== KitX.CSharp.Parser 实际插件管理器使用示例 ===\n"); + + try + { + // 1. 设置插件管理器 + Console.WriteLine("1. 设置插件管理器..."); + + // 这里应该传入实际的 PluginsServer 实例和日志记录器 + // 由于这是一个示例,我们使用 null 作为占位符 + // 在实际使用中,这里应该传入真实的 PluginsServer 实例和日志记录器 + // 例如: + // var pluginsServer = KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance; + // var logger = Serilog.Log.Logger; + // Parser.SetPluginManager(new RealPluginManager(pluginsServer, message => logger.Information(message))); + + Console.WriteLine(" 使用 MockPluginManager 进行演示..."); + Parser.SetPluginManager(new MockPluginManager()); + + Console.WriteLine(" ✓ 插件管理器已设置"); + + // 2. 创建示例插件数据 + Console.WriteLine("\n2. 创建示例插件数据..."); + var examplePlugins = new List + { + new PluginInfo + { + Name = "SampleCalculator", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Add", + ReturnValueType = "int", + Parameters = new List + { + new Parameter { Name = "a", Type = "int", IsOptional = false }, + new Parameter { Name = "b", Type = "int", IsOptional = false } + } + } + } + } + }; + + Console.WriteLine(" ✓ 示例插件数据已创建"); + + // 3. 生成动态程序集 + Console.WriteLine("\n3. 生成动态程序集..."); + var assembly = Parser.Generate(examplePlugins, "RealPluginManagerExampleAssembly"); + Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); + + // 4. 获取生成的插件类型 + Console.WriteLine("\n4. 分析生成的插件类型..."); + var pluginTypes = Parser.GetPluginTypes(assembly); + Console.WriteLine($" 发现 {pluginTypes.Count} 个插件类:"); + + foreach (var type in pluginTypes) + { + Console.WriteLine($" - {type.Name}"); + var methods = Parser.GetPluginMethods(type); + foreach (var method in methods) + { + var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}")); + Console.WriteLine($" └── {method.ReturnType.Name} {method.Name}({parameters})"); + } + } + + Console.WriteLine("\n=== 示例完成 ==="); + Console.WriteLine("\n注意:在实际使用中,您需要:"); + Console.WriteLine("1. 确保 KitX Dashboard 正在运行"); + Console.WriteLine("2. 传入真实的 PluginsServer.Instance"); + Console.WriteLine("3. 传入真实的日志记录器实例"); + Console.WriteLine("4. 确保插件已正确连接到 Dashboard"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ 示例执行失败: {ex.Message}"); + Console.WriteLine($"详细信息: {ex}"); + } + } + + /// + /// 演示如何在 KitX Dashboard 中集成 + /// + public static void DashboardIntegrationExample() + { + Console.WriteLine("=== KitX Dashboard 集成示例 ===\n"); + + Console.WriteLine("在 KitX Dashboard 中使用 RealPluginManager 的步骤:\n"); + + Console.WriteLine("1. 在 Dashboard 启动时设置插件管理器工厂:"); + Console.WriteLine("```csharp"); + Console.WriteLine("// 在 Dashboard 的初始化代码中"); + Console.WriteLine("Parser.SetPluginManagerFactory(() => new RealPluginManager("); + Console.WriteLine(" KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance,"); + Console.WriteLine(" message => Log.Information(message)"); + Console.WriteLine("));"); + Console.WriteLine("```"); + + Console.WriteLine("\n2. 生成插件程序集:"); + Console.WriteLine("```csharp"); + Console.WriteLine("var assembly = Parser.Generate(plugins, \"DashboardPluginAssembly\");"); + Console.WriteLine("```"); + + Console.WriteLine("\n3. 在脚本中使用生成的 API:"); + Console.WriteLine("```csharp"); + Console.WriteLine("// 现在可以在脚本中直接调用"); + Console.WriteLine("var result = SampleCalculator.Add(10, 20);"); + Console.WriteLine("```"); + + Console.WriteLine("\n这样,脚本中的插件调用将通过真实的 KitX Dashboard 插件系统执行!"); + } +} diff --git a/KitX Script/Kscript.CSharp.Parser.Examples/Kscript.CSharp.Parser.Examples.csproj b/KitX Script/Kscript.CSharp.Parser.Examples/Kscript.CSharp.Parser.Examples.csproj new file mode 100644 index 0000000..64343b5 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser.Examples/Kscript.CSharp.Parser.Examples.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + preview + enable + enable + Kscript.CSharp.Parser.Examples + Exe + Kscript.CSharp.Parser.Examples.Program + + + + + + + + + + + + diff --git a/KitX Script/Kscript.CSharp.Parser.Examples/example.json b/KitX Script/Kscript.CSharp.Parser.Examples/example.json new file mode 100644 index 0000000..60ed38c --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser.Examples/example.json @@ -0,0 +1,154 @@ +[ + { + "Name": "SampleCalculator", + "Version": "1.0.0", + "DisplayName": { + "en": "Calculator", + "zh": "计算器" + }, + "AuthorName": "KitX Devs", + "AuthorLink": "https://github.com/Crequency/KitX", + "PublisherName": "Crequency", + "PublisherLink": "https://kitx.cc", + "SimpleDescription": { + "en": "A simple calculator plugin.", + "zh": "简易计算器插件" + }, + "ComplexDescription": {}, + "TotalDescriptionInMarkdown": {}, + "IconInBase64": "", + "PublishDate": "2025-07-29T00:00:00Z", + "LastUpdateDate": "2025-07-29T00:00:00Z", + "IsMarketVersion": true, + "Tags": { + "en": "math,utility", + "zh": "数学,工具" + }, + "Functions": [ + { + "Name": "Add", + "DisplayNames": { + "en": "Add two integers", + "zh": "整数相加" + }, + "Parameters": [ + { + "Name": "a", + "DisplayNames": { + "en": "First number", + "zh": "第一个数" + }, + "Type": "int", + "Value": "0", + "IsOptional": false + }, + { + "Name": "b", + "DisplayNames": { + "en": "Second number", + "zh": "第二个数" + }, + "Type": "int", + "Value": "0", + "IsOptional": false + } + ], + "ReturnValue": "Sum of a and b", + "ReturnValueType": "int" + }, + { + "Name": "Divide", + "DisplayNames": { + "en": "Divide with optional precision", + "zh": "带可选精度的除法" + }, + "Parameters": [ + { + "Name": "numerator", + "Type": "double", + "Value": "0", + "IsOptional": false + }, + { + "Name": "denominator", + "Type": "double", + "Value": "1", + "IsOptional": false + }, + { + "Name": "decimals", + "Type": "int", + "Value": "2", + "IsOptional": true + } + ], + "ReturnValue": "Rounded quotient", + "ReturnValueType": "double" + } + ], + "RootStartupFileName": "SampleCalculator.dll" + }, + { + "Name": "StringToolkit", + "Version": "1.2.0", + "DisplayName": { + "en": "String Toolkit", + "zh": "字符串工具箱" + }, + "AuthorName": "Community", + "AuthorLink": "https://example.com", + "PublisherName": "KitX", + "PublisherLink": "https://kitx.cc", + "SimpleDescription": { + "en": "Utility functions for string manipulation.", + "zh": "字符串处理工具集" + }, + "ComplexDescription": {}, + "TotalDescriptionInMarkdown": {}, + "IconInBase64": "", + "PublishDate": "2025-07-29T00:00:00Z", + "LastUpdateDate": "2025-07-29T00:00:00Z", + "IsMarketVersion": false, + "Tags": { + "en": "string,utility,text", + "zh": "字符串,工具,文本" + }, + "Functions": [ + { + "Name": "Reverse", + "DisplayNames": { + "en": "Reverse string", + "zh": "反转字符串" + }, + "Parameters": [ + { + "Name": "text", + "Type": "string", + "Value": "", + "IsOptional": false + } + ], + "ReturnValue": "Reversed text", + "ReturnValueType": "string" + }, + { + "Name": "ToUpper", + "DisplayNames": { + "en": "Convert to upper case", + "zh": "转大写" + }, + "Parameters": [ + { + "Name": "text", + "Type": "string", + "Value": "", + "IsOptional": false + } + ], + "ReturnValue": "Uppercase text", + "ReturnValueType": "string" + } + ], + "RootStartupFileName": "StringToolkit.dll" + } + ] diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs new file mode 100644 index 0000000..4e0e193 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs @@ -0,0 +1,145 @@ +using System.Collections.Concurrent; +using System.Text; + +namespace Kscript.CSharp.Parser.CodeGen; + +/// +/// 程序集缓存管理器,避免重复生成相同的程序集 +/// +public static class AssemblyCache +{ + /// + /// 程序集缓存字典,Key 为插件清单的哈希值,Value 为生成的程序集 + /// + private static readonly ConcurrentDictionary _assemblyCache = new(); + + /// + /// 获取或生成程序集 + /// + /// 插件信息列表 + /// 程序集名称 + /// 插件管理器实例 + /// 缓存的或新生成的程序集 + public static Assembly GetOrCreateAssembly(List plugins, string assemblyName, Core.IPluginManager pluginManager) + { + var cacheKey = GenerateCacheKey(plugins, assemblyName); + + // 尝试从缓存获取 + if (_assemblyCache.TryGetValue(cacheKey, out var cachedAssembly)) + { + return cachedAssembly; + } + + // 生成新的程序集 + var newAssembly = MethodEmitter.GenerateAssembly(plugins, assemblyName, pluginManager); + + // 添加到缓存 + _assemblyCache.TryAdd(cacheKey, newAssembly); + + return newAssembly; + } + + /// + /// 生成插件清单的缓存键 + /// + /// 插件信息列表 + /// 程序集名称 + /// 缓存键字符串 + private static string GenerateCacheKey(List plugins, string assemblyName) + { + // 使用轻量级字符串哈希,避免复杂的JSON序列化和SHA256计算 + var keyBuilder = new StringBuilder(); + keyBuilder.Append(assemblyName); + keyBuilder.Append($"|{plugins.Count}"); + + foreach (var plugin in plugins.OrderBy(p => p.Name)) + { + keyBuilder.Append($"|{plugin.Name}:{plugin.Version}:{plugin.Functions.Count}"); + foreach (var function in plugin.Functions.OrderBy(f => f.Name)) + { + keyBuilder.Append($":{function.Name}:{function.ReturnValueType}:{function.Parameters.Count}"); + } + } + + return keyBuilder.ToString().GetHashCode().ToString("X"); + } + + /// + /// 清除所有缓存 + /// + public static void ClearCache() + { + _assemblyCache.Clear(); + // 清除所有程序集加载上下文 + MethodEmitter.ClearAllAssemblyContexts(); + } + + /// + /// 获取缓存统计信息 + /// + /// 缓存统计信息 + public static CacheStatistics GetStatistics() + { + return new CacheStatistics + { + CachedAssemblyCount = _assemblyCache.Count, + CacheKeys = _assemblyCache.Keys.ToList() + }; + } + + /// + /// 检查是否存在缓存 + /// + /// 插件信息列表 + /// 程序集名称 + /// 是否存在缓存 + public static bool HasCache(List plugins, string assemblyName = "DynamicPluginAssembly") + { + var cacheKey = GenerateCacheKey(plugins, assemblyName); + return _assemblyCache.ContainsKey(cacheKey); + } + + /// + /// 强制重新生成程序集(绕过缓存) + /// + /// 插件信息列表 + /// 程序集名称 + /// 插件管理器实例 + /// 新生成的程序集 + public static Assembly ForceRegenerate(List plugins, Core.IPluginManager pluginManager, string assemblyName = "DynamicPluginAssembly") + { + var cacheKey = GenerateCacheKey(plugins, assemblyName); + + // 移除现有缓存 + _assemblyCache.TryRemove(cacheKey, out _); + + // 生成新的程序集(这会自动卸载旧的程序集加载上下文) + var newAssembly = MethodEmitter.GenerateAssembly(plugins, assemblyName, pluginManager); + + // 添加到缓存 + _assemblyCache.TryAdd(cacheKey, newAssembly); + + return newAssembly; + } +} + +/// +/// 缓存统计信息 +/// +public class CacheStatistics +{ + /// + /// 缓存的程序集数量 + /// + public int CachedAssemblyCount { get; set; } + + /// + /// 缓存键列表 + /// + public List CacheKeys { get; set; } = new(); + + public override string ToString() + { + return $"缓存统计: {CachedAssemblyCount} 个程序集"; + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs new file mode 100644 index 0000000..e732fb4 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs @@ -0,0 +1,365 @@ +using Kscript.CSharp.Parser.Core; +using Kscript.CSharp.Parser.Exceptions; +using Kscript.CSharp.Parser.Models; +using System.Reflection.Emit; +using System.Runtime.Loader; +using System.Collections.Concurrent; + +namespace Kscript.CSharp.Parser.CodeGen; + +/// +/// 可收集的程序集加载上下文,支持程序集卸载 +/// +public class CollectibleAssemblyLoadContext : AssemblyLoadContext +{ + public CollectibleAssemblyLoadContext(string name) : base(name, isCollectible: true) + { + } + + protected override Assembly Load(AssemblyName assemblyName) + { + // 让默认上下文处理核心程序集加载 + return null; + } +} + +/// +/// IL 方法生成器,负责生成插件调用的静态方法 +/// +public static class MethodEmitter +{ + /// + /// 程序集加载上下文缓存,用于隔离和卸载程序集 + /// + private static readonly ConcurrentDictionary _loadContexts = new(); + /// + /// 为插件生成动态程序集 + /// + /// 插件信息列表 + /// 程序集名称 + /// 插件管理器实例 + /// 生成的动态程序集 + public static Assembly GenerateAssembly(List plugins, + string assemblyName, IPluginManager pluginManager) + { + try + { + // 如果已存在相同名称的程序集加载上下文,先卸载它 + if (_loadContexts.TryGetValue(assemblyName, out var existingContext)) + { + UnloadAssemblyContext(assemblyName, existingContext); + } + + // 创建新的可卸载的程序集加载上下文 + var loadContext = new CollectibleAssemblyLoadContext(assemblyName); + _loadContexts.TryAdd(assemblyName, loadContext); + + // 使用PersistedAssemblyBuilder创建可保存的动态程序集 + var persistedAssemblyBuilder = new PersistedAssemblyBuilder( + new AssemblyName(assemblyName), + typeof(object).Assembly); + + var moduleBuilder = persistedAssemblyBuilder.DefineDynamicModule($"{assemblyName}.dll"); + + // 为每个插件生成静态类 + foreach (var plugin in plugins) + { + GeneratePluginClass(moduleBuilder, plugin, pluginManager); + } + + // 创建所有类型 + foreach (var plugin in plugins) + { + // 确保所有类型都已创建 + // 类型在GeneratePluginClass中已经创建 + } + + // 保存到临时文件并重新加载为可引用的程序集 + var tempPath = Path.Combine(Path.GetTempPath(), $"{assemblyName}_{Guid.NewGuid()}.dll"); + + using (var fileStream = File.Create(tempPath)) + { + persistedAssemblyBuilder.Save(fileStream); + } + + // 使用自定义加载上下文从文件加载程序集 + var assembly = loadContext.LoadFromAssemblyPath(tempPath); + + Console.WriteLine($"[MethodEmitter] 使用CollectibleAssemblyLoadContext生成并加载程序集: {assemblyName} -> {tempPath}"); + + // 注册临时文件删除(在程序退出时) + AppDomain.CurrentDomain.ProcessExit += (s, e) => + { + try + { + if (File.Exists(tempPath)) + File.Delete(tempPath); + } + catch + { + // 忽略删除错误 + } + }; + + return assembly; + } + catch (Exception ex) + { + throw new ParserException($"生成程序集失败: {assemblyName}", ex); + } + } + + /// + /// 卸载程序集加载上下文 + /// + /// 程序集名称 + /// 要卸载的加载上下文 + private static void UnloadAssemblyContext(string assemblyName, CollectibleAssemblyLoadContext context) + { + try + { + Console.WriteLine($"[MethodEmitter] 卸载程序集加载上下文: {assemblyName}"); + + // 从缓存中移除 + _loadContexts.TryRemove(assemblyName, out _); + + // 卸载上下文 + context.Unload(); + + // 等待卸载完成 + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + catch (Exception ex) + { + Console.WriteLine($"[MethodEmitter] 卸载程序集加载上下文失败: {assemblyName}, 错误: {ex.Message}"); + } + } + + /// + /// 清除所有程序集加载上下文 + /// + public static void ClearAllAssemblyContexts() + { + foreach (var kvp in _loadContexts) + { + UnloadAssemblyContext(kvp.Key, kvp.Value); + } + } + + /// + /// 为单个插件生成静态类 + /// + private static void GeneratePluginClass(ModuleBuilder moduleBuilder, PluginInfo plugin, IPluginManager pluginManager) + { + try + { + // 创建静态类 + var typeBuilder = moduleBuilder.DefineType( + plugin.Name, + TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Abstract | TypeAttributes.Sealed); + + // 添加插件管理器字段 + var pluginManagerField = typeBuilder.DefineField( + "_pluginManager", + typeof(IPluginManager), + FieldAttributes.Private | FieldAttributes.Static); + + // 生成静态构造函数来初始化插件管理器 + GenerateStaticConstructor(typeBuilder, pluginManagerField, pluginManager); + + // 为每个功能生成静态方法 + foreach (var function in plugin.Functions) + { + GeneratePluginMethod(typeBuilder, plugin.Name, function, pluginManagerField); + } + + // 创建类型 + typeBuilder.CreateType(); + } + catch (Exception ex) + { + throw new ParserException($"生成IL代码失败: {plugin.Name} 类", ex); + } + } + + /// + /// 生成静态构造函数 + /// + private static void GenerateStaticConstructor(TypeBuilder typeBuilder, FieldBuilder pluginManagerField, IPluginManager pluginManager) + { + var constructorBuilder = typeBuilder.DefineConstructor( + MethodAttributes.Private | MethodAttributes.Static | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + CallingConventions.Standard, + Type.EmptyTypes); + + var il = constructorBuilder.GetILGenerator(); + + // 使用提供的插件管理器实例 + il.Emit(OpCodes.Ldtoken, pluginManager.GetType()); + il.Emit(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle")!); + il.Emit(OpCodes.Call, typeof(Activator).GetMethod("CreateInstance", new[] { typeof(Type) })!); + il.Emit(OpCodes.Castclass, typeof(IPluginManager)); + + il.Emit(OpCodes.Stsfld, pluginManagerField); + il.Emit(OpCodes.Ret); + } + + /// + /// 为插件功能生成静态方法 + /// + private static void GeneratePluginMethod(TypeBuilder typeBuilder, string pluginName, Function function, FieldBuilder pluginManagerField) + { + try + { + // 解析参数类型 + var parameterTypes = function.Parameters + .Select(p => TypeMapper.MapType(p.Type)) + .ToArray(); + + // 解析返回值类型 + var returnType = TypeMapper.MapType(function.ReturnValueType); + + // 定义方法 + var methodBuilder = typeBuilder.DefineMethod( + function.Name, + MethodAttributes.Public | MethodAttributes.Static, + returnType, + parameterTypes); + + // 设置参数名称 + for (int i = 0; i < function.Parameters.Count; i++) + { + var param = function.Parameters[i]; + var paramBuilder = methodBuilder.DefineParameter(i + 1, ParameterAttributes.None, param.Name); + + // 如果参数是可选的,设置默认值 + if (param.IsOptional && !string.IsNullOrEmpty(param.Value)) + { + var defaultValue = ConvertDefaultValue(param.Value, parameterTypes[i]); + if (defaultValue != null) + { + paramBuilder.SetConstant(defaultValue); + } + } + } + + // 生成方法体IL代码 + GenerateMethodBody(methodBuilder, pluginName, function, parameterTypes, returnType, pluginManagerField); + } + catch (Exception ex) + { + throw new ParserException($"生成IL代码失败: {pluginName}.{function.Name}", ex); + } + } + + /// + /// 生成方法体IL代码 + /// + private static void GenerateMethodBody(MethodBuilder methodBuilder, string pluginName, Function function, + Type[] parameterTypes, Type returnType, FieldBuilder pluginManagerField) + { + var il = methodBuilder.GetILGenerator(); + + // 声明局部变量 + var callInfoLocal = il.DeclareLocal(typeof(PluginCallInfo)); + var parametersArrayLocal = il.DeclareLocal(typeof(object[])); + var parameterTypesArrayLocal = il.DeclareLocal(typeof(Type[])); + LocalBuilder? resultLocal = null; + + if (returnType != typeof(void)) + { + resultLocal = il.DeclareLocal(returnType); + } + + // 创建参数数组 + il.Emit(OpCodes.Ldc_I4, parameterTypes.Length); + il.Emit(OpCodes.Newarr, typeof(object)); + il.Emit(OpCodes.Stloc, parametersArrayLocal); + + // 创建参数类型数组 + il.Emit(OpCodes.Ldc_I4, parameterTypes.Length); + il.Emit(OpCodes.Newarr, typeof(Type)); + il.Emit(OpCodes.Stloc, parameterTypesArrayLocal); + + // 填充参数数组和类型数组 + for (int i = 0; i < parameterTypes.Length; i++) + { + // 填充参数值 + il.Emit(OpCodes.Ldloc, parametersArrayLocal); + il.Emit(OpCodes.Ldc_I4, i); + il.Emit(OpCodes.Ldarg, i); + + // 如果是值类型,需要装箱 + if (parameterTypes[i].IsValueType) + { + il.Emit(OpCodes.Box, parameterTypes[i]); + } + + il.Emit(OpCodes.Stelem_Ref); + + // 填充参数类型 + il.Emit(OpCodes.Ldloc, parameterTypesArrayLocal); + il.Emit(OpCodes.Ldc_I4, i); + il.Emit(OpCodes.Ldtoken, parameterTypes[i]); + il.Emit(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle")!); + il.Emit(OpCodes.Stelem_Ref); + } + + // 创建 PluginCallInfo 实例 + il.Emit(OpCodes.Ldstr, pluginName); + il.Emit(OpCodes.Ldstr, function.Name); + il.Emit(OpCodes.Ldloc, parametersArrayLocal); + il.Emit(OpCodes.Ldloc, parameterTypesArrayLocal); + il.Emit(OpCodes.Newobj, typeof(PluginCallInfo).GetConstructor(new[] + { + typeof(string), typeof(string), typeof(object[]), typeof(Type[]) + })!); + il.Emit(OpCodes.Stloc, callInfoLocal); + + // 调用插件管理器 + il.Emit(OpCodes.Ldsfld, pluginManagerField); + il.Emit(OpCodes.Ldloc, callInfoLocal); + + if (returnType == typeof(void)) + { + // 调用无返回值的方法 + var voidCallMethod = typeof(IPluginManager).GetMethods() + .First(m => m.Name == "Call" && !m.IsGenericMethod && m.GetParameters().Length == 1); + il.Emit(OpCodes.Callvirt, voidCallMethod); + } + else + { + // 调用有返回值的泛型方法 + var genericCallMethod = typeof(IPluginManager).GetMethods() + .First(m => m.Name == "Call" && m.IsGenericMethod && m.GetParameters().Length == 1) + .MakeGenericMethod(returnType); + il.Emit(OpCodes.Callvirt, genericCallMethod); + il.Emit(OpCodes.Stloc, resultLocal!); + il.Emit(OpCodes.Ldloc, resultLocal!); + } + + il.Emit(OpCodes.Ret); + } + + /// + /// 转换默认值 + /// + private static object? ConvertDefaultValue(string value, Type targetType) + { + try + { + if (targetType == typeof(string)) + return value; + + if (string.IsNullOrEmpty(value)) + return null; + + return Convert.ChangeType(value, targetType); + } + catch + { + return null; + } + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/TypeMapper.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/TypeMapper.cs new file mode 100644 index 0000000..1810ffb --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/TypeMapper.cs @@ -0,0 +1,86 @@ +using System.Collections.Concurrent; + +namespace Kscript.CSharp.Parser.CodeGen; + +/// +/// 类型映射器,将字符串类型名映射到 System.Type +/// +public static class TypeMapper +{ + /// + /// 基础类型映射表 + /// + private static readonly Dictionary _basicTypeMap = new() + { + { "void", typeof(void) }, + { "bool", typeof(bool) }, + { "byte", typeof(byte) }, + { "sbyte", typeof(sbyte) }, + { "short", typeof(short) }, + { "ushort", typeof(ushort) }, + { "int", typeof(int) }, + { "uint", typeof(uint) }, + { "long", typeof(long) }, + { "ulong", typeof(ulong) }, + { "float", typeof(float) }, + { "double", typeof(double) }, + { "decimal", typeof(decimal) }, + { "char", typeof(char) }, + { "string", typeof(string) }, + { "object", typeof(object) } + }; + + /// + /// 类型缓存 + /// + private static readonly ConcurrentDictionary _typeCache = new(); + + /// + /// 映射字符串类型名到 System.Type + /// + /// 类型名字符串 + /// 对应的 Type 对象,如果找不到则返回 typeof(object) + public static Type MapType(string typeName) + { + if (string.IsNullOrWhiteSpace(typeName)) + return typeof(object); + + // 先检查缓存 + if (_typeCache.TryGetValue(typeName, out var cachedType)) + return cachedType; + + // 检查基础类型 + if (_basicTypeMap.TryGetValue(typeName, out var basicType)) + { + _typeCache[typeName] = basicType; + return basicType; + } + + // 尝试直接解析类型 + try + { + var resolvedType = Type.GetType(typeName); + if (resolvedType != null) + { + _typeCache[typeName] = resolvedType; + return resolvedType; + } + } + catch + { + // 发生异常时回退到 object 类型 + } + + // 回退到 object 类型 + _typeCache[typeName] = typeof(object); + return typeof(object); + } + + /// + /// 清除类型缓存 + /// + public static void ClearCache() + { + _typeCache.Clear(); + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/Core/IPluginManager.cs b/KitX Script/Kscript.CSharp.Parser/Core/IPluginManager.cs new file mode 100644 index 0000000..e66e9ea --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Core/IPluginManager.cs @@ -0,0 +1,38 @@ +using Kscript.CSharp.Parser.Models; + +namespace Kscript.CSharp.Parser.Core; + +/// +/// 插件管理器接口 - 模拟接口,实际实现由外部项目提供 +/// +public interface IPluginManager +{ + /// + /// 调用插件方法 + /// + /// 返回值类型 + /// 调用信息 + /// 插件方法的返回值 + T Call(PluginCallInfo callInfo); + + /// + /// 调用插件方法(无返回值) + /// + /// 调用信息 + void Call(PluginCallInfo callInfo); + + /// + /// 检查插件是否存在 + /// + /// 插件名称 + /// 插件是否存在 + bool IsPluginExists(string pluginName); + + /// + /// 检查插件方法是否存在 + /// + /// 插件名称 + /// 方法名称 + /// 方法是否存在 + bool IsMethodExists(string pluginName, string methodName); +} diff --git a/KitX Script/Kscript.CSharp.Parser/Core/IPluginServiceProvider.cs b/KitX Script/Kscript.CSharp.Parser/Core/IPluginServiceProvider.cs new file mode 100644 index 0000000..badc3e9 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Core/IPluginServiceProvider.cs @@ -0,0 +1,44 @@ +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Parser.Core; + +/// +/// 插件服务提供者接口 - 由Dashboard实现 +/// 用于解耦Parser和Dashboard,避免直接依赖和反射 +/// +public interface IPluginServiceProvider +{ + /// + /// 获取所有运行中的插件信息 + /// + /// 运行中的插件信息集合 + IEnumerable GetRunningPlugins(); + + /// + /// 根据插件名称查找插件信息 + /// + /// 插件名称 + /// 插件信息,如果未找到则返回null + PluginInfo? FindPlugin(string pluginName); + + /// + /// 查找插件连接器 + /// + /// 插件信息 + /// 插件连接器对象,如果未找到则返回null + object? FindConnector(PluginInfo pluginInfo); + + /// + /// 向插件连接器发送请求 + /// + /// 插件连接器 + /// 请求对象 + /// 异步任务 + Task SendRequestAsync(object connector, object request); + + /// + /// 订阅插件响应事件 + /// + /// 响应处理器,接收(requestId, responseJson) + void SubscribeToResponses(Action responseHandler); +} diff --git a/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs b/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs new file mode 100644 index 0000000..ecc1710 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs @@ -0,0 +1,167 @@ +using Kscript.CSharp.Parser.Models; + +namespace Kscript.CSharp.Parser.Core; + +/// +/// 模拟插件管理器 - 用于调试和测试 +/// +public class MockPluginManager : IPluginManager +{ + /// + /// 模拟的插件数据,用于验证调用 + /// + private readonly Dictionary> _mockData = new() + { + { + "SampleCalculator", new Dictionary + { + { "Add", (int a, int b) => a + b }, + { "Multiply", (double x, double y) => x * y }, + { "Divide", (double numerator, double denominator, int decimals = 2) => Math.Round(numerator / denominator, decimals) } + } + }, + { + "StringToolkit", new Dictionary + { + { "Reverse", (string text) => new string(text.Reverse().ToArray()) }, + { "ToUpper", (string text) => text.ToUpperInvariant() }, + { "Concat", (string str1, string str2) => str1 + str2 } + } + }, + { + "KitXWF", new Dictionary + { + { "Print", (string message) => Console.WriteLine(message) } + } + } + }; + + /// + /// 调用插件方法 + /// + public T Call(PluginCallInfo callInfo) + { + Console.WriteLine($"[MockPluginManager] 调用插件方法: {callInfo}"); + Console.WriteLine($"[MockPluginManager] 参数类型: [{string.Join(", ", callInfo.ParameterTypes.Select(t => t.Name))}]"); + Console.WriteLine($"[MockPluginManager] 期望返回类型: {typeof(T).Name}"); + + // 简单的模拟实现 + if (_mockData.TryGetValue(callInfo.PluginName, out var plugin) && + plugin.TryGetValue(callInfo.MethodName, out var method)) + { + try + { + var result = InvokeMockMethod(callInfo, method); + Console.WriteLine($"[MockPluginManager] 返回结果: {result}"); + + if (result is T typedResult) + return typedResult; + + // 尝试类型转换 + if (result != null && typeof(T) != typeof(void)) + { + var convertedResult = Convert.ChangeType(result, typeof(T)); + return (T)convertedResult; + } + } + catch (Exception ex) + { + Console.WriteLine($"[MockPluginManager] 调用异常: {ex.Message}"); + } + } + else + { + Console.WriteLine($"[MockPluginManager] 未找到插件方法: {callInfo.PluginName}.{callInfo.MethodName}"); + } + + // 返回默认值 + if (typeof(T) == typeof(void)) + return default(T)!; + + return (T)GetDefaultValue(typeof(T))!; + } + + /// + /// 调用插件方法(无返回值) + /// + public void Call(PluginCallInfo callInfo) + { + Call(callInfo); + } + + /// + /// 检查插件是否存在 + /// + public bool IsPluginExists(string pluginName) + { + var exists = _mockData.ContainsKey(pluginName); + Console.WriteLine($"[MockPluginManager] 检查插件 '{pluginName}' 是否存在: {exists}"); + return exists; + } + + /// + /// 检查插件方法是否存在 + /// + public bool IsMethodExists(string pluginName, string methodName) + { + var exists = _mockData.TryGetValue(pluginName, out var plugin) && + plugin.ContainsKey(methodName); + Console.WriteLine($"[MockPluginManager] 检查方法 '{pluginName}.{methodName}' 是否存在: {exists}"); + return exists; + } + + /// + /// 调用模拟方法 + /// + private object? InvokeMockMethod(PluginCallInfo callInfo, object method) + { + // 根据插件和方法名进行简单的模拟计算 + switch (callInfo.PluginName) + { + case "SampleCalculator": + switch (callInfo.MethodName) + { + case "Add" when callInfo.Parameters.Length >= 2: + return Convert.ToInt32(callInfo.Parameters[0]) + Convert.ToInt32(callInfo.Parameters[1]); + case "Multiply" when callInfo.Parameters.Length >= 2: + return Convert.ToDouble(callInfo.Parameters[0]) * Convert.ToDouble(callInfo.Parameters[1]); + case "Divide" when callInfo.Parameters.Length >= 2: + return callInfo.Parameters.Length >= 3 + ? Math.Round(Convert.ToDouble(callInfo.Parameters[0]) / Convert.ToDouble(callInfo.Parameters[1]), Convert.ToInt32(callInfo.Parameters[2])) + : Math.Round(Convert.ToDouble(callInfo.Parameters[0]) / Convert.ToDouble(callInfo.Parameters[1]), 2); + } + break; + case "KitXWF": + switch (callInfo.MethodName) + { + case "Print" when callInfo.Parameters.Length >= 1: + Console.WriteLine($"[KitXWF] {callInfo.Parameters[0]?.ToString() ?? ""}"); + return null; + } + break; + case "StringToolkit": + switch (callInfo.MethodName) + { + case "Reverse" when callInfo.Parameters.Length >= 1: + return new string(callInfo.Parameters[0].ToString()?.Reverse().ToArray() ?? Array.Empty()); + case "ToUpper" when callInfo.Parameters.Length >= 1: + return callInfo.Parameters[0].ToString()?.ToUpperInvariant() ?? string.Empty; + case "Concat" when callInfo.Parameters.Length >= 2: + return callInfo.Parameters[0].ToString() + callInfo.Parameters[1].ToString(); + } + break; + } + + return $"MockResult_{callInfo.MethodName}"; + } + + /// + /// 获取类型的默认值 + /// + private static object? GetDefaultValue(Type type) + { + if (type.IsValueType) + return Activator.CreateInstance(type); + return null; + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs b/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs new file mode 100644 index 0000000..1f7d94c --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs @@ -0,0 +1,272 @@ +using System.Text.Json; +using Kscript.CSharp.Parser.Models; +using KitX.Shared.CSharp.Plugin; +using KitX.Shared.CSharp.WebCommand; + +using Serilog; +using System.Collections.Concurrent; + +namespace Kscript.CSharp.Parser.Core; + +/// +/// 实际的插件管理器 - 使用 KitX Dashboard 的插件系统进行真实调用 +/// +public class RealPluginManager : IPluginManager +{ + private readonly IPluginServiceProvider _serviceProvider; + private readonly Action _infoLogger; + private readonly Action _errorLogger; + + private readonly JsonSerializerOptions _serializerOptions = new() + { + WriteIndented = true, + IncludeFields = true, + PropertyNameCaseInsensitive = true, + }; + + // 用于存储等待响应的请求 + private readonly ConcurrentDictionary> _pendingRequests = new(); + + /// + /// 构造函数 + /// + /// 插件服务提供者实例 + /// 信息日志记录器 + /// 错误日志记录器 + public RealPluginManager( + IPluginServiceProvider serviceProvider, + Action? infoLogger = null, + Action? errorLogger = null) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + _infoLogger = infoLogger ?? (message => Console.WriteLine(message)); + _errorLogger = errorLogger ?? (message => Console.WriteLine(message)); + + _infoLogger("[RealPluginManager] 正在初始化 RealPluginManager..."); + + // 订阅插件响应事件 + _serviceProvider.SubscribeToResponses(HandlePluginResponse); + + _infoLogger("[RealPluginManager] RealPluginManager 初始化完成"); + } + + /// + /// 调用插件方法 + /// + public T Call(PluginCallInfo callInfo) + { + try + { + _infoLogger($"[RealPluginManager] 开始调用插件方法: {callInfo}"); + + // 查找插件信息 + var pluginInfo = _serviceProvider.FindPlugin(callInfo.PluginName); + if (pluginInfo == null) + { + _infoLogger($"[RealPluginManager] 未找到插件: {callInfo.PluginName}"); + return GetDefaultResult(); + } + + // 查找插件连接器 + var connector = _serviceProvider.FindConnector(pluginInfo); + if (connector == null) + { + _infoLogger($"[RealPluginManager] 插件 {callInfo.PluginName} 未连接"); + return GetDefaultResult(); + } + + // 创建并发送请求 + var result = SendPluginRequest(connector, callInfo).GetAwaiter().GetResult(); + + _infoLogger($"[RealPluginManager] 插件调用完成: {callInfo} -> 结果: {result}"); + return result; + } + catch (Exception ex) + { + _errorLogger($"[RealPluginManager] 调用插件方法失败: {callInfo} - 异常: {ex.Message}"); + return GetDefaultResult(); + } + } + + /// + /// 调用插件方法(无返回值) + /// + public void Call(PluginCallInfo callInfo) + { + Call(callInfo); + } + + /// + /// 检查插件是否存在 + /// + public bool IsPluginExists(string pluginName) + { + try + { + var pluginInfo = _serviceProvider.FindPlugin(pluginName); + var exists = pluginInfo != null; + + _infoLogger($"[RealPluginManager] 检查插件 '{pluginName}' 是否存在: {exists}"); + return exists; + } + catch (Exception ex) + { + _errorLogger($"[RealPluginManager] 检查插件存在性失败: {pluginName} - 异常: {ex.Message}"); + return false; + } + } + + /// + /// 检查插件方法是否存在 + /// + public bool IsMethodExists(string pluginName, string methodName) + { + try + { + var pluginInfo = _serviceProvider.FindPlugin(pluginName); + var exists = pluginInfo?.Functions.Any(f => f.Name == methodName) ?? false; + + _infoLogger($"[RealPluginManager] 检查方法 '{pluginName}.{methodName}' 是否存在: {exists}"); + return exists; + } + catch (Exception ex) + { + _errorLogger($"[RealPluginManager] 检查方法存在性失败: {pluginName}.{methodName} - 异常: {ex.Message}"); + return false; + } + } + + /// + /// 发送插件请求 + /// + private async Task SendPluginRequest(object connector, PluginCallInfo callInfo) + { + try + { + // 创建 Connector 实例 + var connectorInstance = new Connector() + .SetSerializer(x => JsonSerializer.Serialize(x, _serializerOptions)) + .SetSender(request => { + _serviceProvider.SendRequestAsync(connector, request).ConfigureAwait(false); + }); + + // 构建参数列表 + var functionArgs = new List(); + for (int i = 0; i < callInfo.Parameters.Length; i++) + { + var paramType = callInfo.ParameterTypes[i]; + var paramValue = callInfo.Parameters[i]; + + functionArgs.Add(new Parameter + { + Name = $"param{i}", + Type = paramType.Name, + // TODO: 等待KitX数据传输标准制定完成后,替换为标准序列化方法 + Value = paramValue?.ToString() ?? string.Empty, + IsOptional = false + }); + } + + // 生成唯一的请求ID + var requestId = Guid.NewGuid().ToString(); + + // 创建任务完成源,用于等待响应 + var tcs = new TaskCompletionSource(); + _pendingRequests[requestId] = tcs; + + // 发送请求 + var request = connectorInstance + .Request() + .ReceiveCommand() + .UpdateCommand(cmd => + { + cmd.FunctionName = callInfo.MethodName; + cmd.FunctionArgs = functionArgs; + cmd.PluginConnectionId = callInfo.PluginName; + cmd.Tags = new Dictionary + { + ["RequestId"] = requestId + }; + return cmd; + }) + .Send(); + + // 等待响应,设置超时时间为30秒 + var timeout = TimeSpan.FromSeconds(30); + var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(timeout)); + + if (completedTask == tcs.Task) + { + // 获取到响应 + var responseJson = tcs.Task.Result; + _infoLogger($"[RealPluginManager] 收到插件响应: {responseJson}"); + + // 尝试反序列化响应 + try + { + if (typeof(T) == typeof(string)) + { + return (T)(object)responseJson; + } + else if (typeof(T) == typeof(void)) + { + return default(T)!; + } + else + { + return JsonSerializer.Deserialize(responseJson, _serializerOptions) ?? GetDefaultResult(); + } + } + catch (JsonException ex) + { + _errorLogger($"[RealPluginManager] 反序列化响应失败: {ex.Message}"); + return GetDefaultResult(); + } + } + else + { + // 超时 + _errorLogger($"[RealPluginManager] 插件调用超时: {callInfo.PluginName}.{callInfo.MethodName}"); + _pendingRequests.TryRemove(requestId, out _); + return GetDefaultResult(); + } + } + catch (Exception ex) + { + _errorLogger($"[RealPluginManager] 发送插件请求失败: {callInfo.PluginName}.{callInfo.MethodName} - 异常: {ex.Message}"); + return GetDefaultResult(); + } + } + + /// + /// 处理插件响应 + /// + public void HandlePluginResponse(string requestId, string responseJson) + { + if (_pendingRequests.TryRemove(requestId, out var tcs)) + { + tcs.SetResult(responseJson); + } + else + { + _infoLogger($"[RealPluginManager] 收到未知请求的响应: {requestId}"); + } + } + + /// + /// 获取默认结果 + /// + private T GetDefaultResult() + { + if (typeof(T) == typeof(void)) + return default(T)!; + + if (typeof(T) == typeof(string)) + return (T)(object)string.Empty; + + if (typeof(T).IsValueType) + return (T)Activator.CreateInstance(typeof(T))!; + + return default(T)!; + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/Exceptions/ParserException.cs b/KitX Script/Kscript.CSharp.Parser/Exceptions/ParserException.cs new file mode 100644 index 0000000..a46417e --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Exceptions/ParserException.cs @@ -0,0 +1,19 @@ +namespace Kscript.CSharp.Parser.Exceptions; + +/// +/// 解析器异常 +/// +public class ParserException : Exception +{ + public ParserException() : base() + { + } + + public ParserException(string message) : base(message) + { + } + + public ParserException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/GlobalUsings.cs b/KitX Script/Kscript.CSharp.Parser/GlobalUsings.cs new file mode 100644 index 0000000..3708735 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/GlobalUsings.cs @@ -0,0 +1,6 @@ +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Reflection; +global using System.Reflection.Emit; +global using KitX.Shared.CSharp.Plugin; diff --git a/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj new file mode 100644 index 0000000..fe53ec8 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + preview + enable + enable + Kscript.CSharp.Parser + + + + + + + + + + + + diff --git a/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.sln b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.sln new file mode 100644 index 0000000..2b571ef --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kscript.CSharp.Parser", "Kscript.CSharp.Parser.csproj", "{17B35B70-0BC3-479B-83D6-ED1513D6C4A6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {17B35B70-0BC3-479B-83D6-ED1513D6C4A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17B35B70-0BC3-479B-83D6-ED1513D6C4A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17B35B70-0BC3-479B-83D6-ED1513D6C4A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17B35B70-0BC3-479B-83D6-ED1513D6C4A6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C88E6290-4CEE-41DA-B062-81147E9E898E} + EndGlobalSection +EndGlobal diff --git a/KitX Script/Kscript.CSharp.Parser/LLMDocs/InitPlan.md b/KitX Script/Kscript.CSharp.Parser/LLMDocs/InitPlan.md new file mode 100644 index 0000000..b3ca63c --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/LLMDocs/InitPlan.md @@ -0,0 +1,76 @@ +现在让我为你详细规划 KitX.CSharp.Parser 项目的架构和实现方案: + +## 🏗️ 项目架构概览 + +```mermaid +graph TD + A[JSON 插件清单] --> B[Parser.Generate] + B --> C[PluginInfo 模型解析] + C --> D[TypeMapper 类型映射] + D --> E[MethodEmitter IL生成] + E --> F[AssemblyCache 缓存管理] + F --> G[动态程序集] + G --> H[脚本调用: Plugin.Method] + + subgraph "核心组件" + I[Parser - 主入口] + J[TypeMapper - 类型映射] + K[MethodEmitter - IL生成] + L[AssemblyCache - 缓存] + M[PluginManager - 模拟调用] + end +``` + +## 📁 详细目录结构 + +``` +KitX.CSharp.Parser/ +├─ src/ +│ ├─ Parser.cs # 主入口静态类 +│ ├─ Models/ +│ │ ├─ PluginInfo.cs # 插件信息模型 +│ │ ├─ FunctionInfo.cs # 功能信息模型 +│ │ ├─ ParameterInfo.cs # 参数信息模型 +│ │ └─ PluginCallInfo.cs # 插件调用信息 +│ ├─ CodeGen/ +│ │ ├─ TypeMapper.cs # 字符串到Type的映射 +│ │ ├─ MethodEmitter.cs # IL方法生成器 +│ │ └─ AssemblyCache.cs # 程序集缓存管理 +│ ├─ Core/ +│ │ ├─ IPluginManager.cs # 插件管理器接口 +│ │ └─ MockPluginManager.cs # 模拟实现 +│ └─ Exceptions/ +│ └─ ParserException.cs # 自定义异常 +├─ examples/ +│ └─ ConsoleDemo/ # 演示程序 +├─ KitX.CSharp.Parser.csproj # 项目文件 +└─ GlobalUsings.cs # 全局using声明 +``` + +## 🔧 核心技术要点 + +1. **类型映射策略**: + - 基础类型:`int` → `System.Int32` + - 泛型类型:`List` → `List` + - 自定义类型回退机制 + +2. **IL 生成模式**: + ```csharp + // 生成的方法体伪代码 + public static T MethodName(params...) + { + var callInfo = new PluginCallInfo(pluginName, methodName, parameters); + return PluginManager.Call(callInfo); + } + ``` + +3. **缓存机制**: + - 按插件清单 Hash 缓存程序集 + - 支持热更新时的缓存失效 + +## 🎯 关键实现亮点 + +- **类型安全**:编译时类型检查,运行时类型转换 +- **性能优化**:程序集缓存避免重复生成 +- **扩展性**:支持自定义类型映射注册 +- **调试友好**:可选的磁盘程序集输出 diff --git a/KitX Script/Kscript.CSharp.Parser/Models/PluginCallInfo.cs b/KitX Script/Kscript.CSharp.Parser/Models/PluginCallInfo.cs new file mode 100644 index 0000000..3cc0cc6 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Models/PluginCallInfo.cs @@ -0,0 +1,44 @@ +namespace Kscript.CSharp.Parser.Models; + +/// +/// 插件调用信息,用于传递给 PluginManager.Call 的参数(模拟,可能根据外部实现发生更改) +/// +public class PluginCallInfo +{ + /// + /// 插件名称 + /// + public string PluginName { get; set; } = string.Empty; + + /// + /// 方法名称 + /// + public string MethodName { get; set; } = string.Empty; + + /// + /// 方法参数数组 + /// + public object[] Parameters { get; set; } = Array.Empty(); + + /// + /// 参数类型数组 + /// + public Type[] ParameterTypes { get; set; } = Array.Empty(); + + public PluginCallInfo() + { + } + + public PluginCallInfo(string pluginName, string methodName, object[] parameters, Type[] parameterTypes) + { + PluginName = pluginName; + MethodName = methodName; + Parameters = parameters; + ParameterTypes = parameterTypes; + } + + public override string ToString() + { + return $"{PluginName}.{MethodName}({string.Join(", ", Parameters)})"; + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/Parser.cs b/KitX Script/Kscript.CSharp.Parser/Parser.cs new file mode 100644 index 0000000..d9b701a --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Parser.cs @@ -0,0 +1,266 @@ +using System.Text.Json; +using Kscript.CSharp.Parser.CodeGen; +using Kscript.CSharp.Parser.Core; +using Kscript.CSharp.Parser.Exceptions; + +namespace Kscript.CSharp.Parser; + +/// +/// KitX.CSharp.Parser 主入口类 +/// 将插件清单 JSON 即时编译成可直接调用的 C# 静态 API +/// +public static class Parser +{ + /// + /// 插件管理器实例 + /// + private static IPluginManager? _pluginManager; + + /// + /// 获取Parser初始化状态 + /// + public static bool IsInitialized => _pluginManager != null; + + /// + /// 设置插件管理器 + /// + /// 插件管理器实例 + public static void SetPluginManager(IPluginManager pluginManager) + { + _pluginManager = pluginManager ?? throw new ArgumentNullException(nameof(pluginManager)); + } + + /// + /// 确保Parser已正确初始化 + /// + /// 当Parser未初始化时抛出 + private static void EnsureInitialized() + { + if (_pluginManager == null) + { + throw new InvalidOperationException("Parser 未初始化。请先调用 SetPluginManager() 设置插件管理器。"); + } + } + + /// + /// 确保所需插件已加载 + /// + /// 插件信息列表 + /// 当所需插件未加载时抛出 + private static void EnsurePluginsLoaded(List plugins) + { + foreach (var plugin in plugins) + { + if (!_pluginManager!.IsPluginExists(plugin.Name)) + { + // TODO: 实现插件动态加载逻辑 + // 目前先抛出异常,待后续实现插件加载功能 + throw new InvalidOperationException($"所需插件未加载: {plugin.Name}。请确保插件已正确安装并启动。"); + } + } + } + + /// + /// 从插件信息列表生成动态程序集 + /// + /// 插件信息列表 + /// 程序集名称 + /// 是否使用缓存 + /// 生成的动态程序集 + public static Assembly Generate(List plugins, string assemblyName = "DynamicPluginAssembly", bool useCache = true) + { + if (plugins == null || plugins.Count == 0) + { + throw new ArgumentException("插件列表不能为空", nameof(plugins)); + } + + EnsureInitialized(); + EnsurePluginsLoaded(plugins); + + try + { + if (useCache) + { + return AssemblyCache.GetOrCreateAssembly(plugins, assemblyName, _pluginManager!); + } + else + { + return AssemblyCache.ForceRegenerate(plugins, _pluginManager!, assemblyName); + } + } + catch (Exception ex) when (!(ex is ParserException)) + { + throw new ParserException($"生成程序集失败: {assemblyName}", ex); + } + } + + /// + /// 从 JSON 字符串生成动态程序集 + /// + /// 插件清单 JSON 字符串 + /// 程序集名称 + /// 是否使用缓存 + /// 生成的动态程序集 + public static Assembly GenerateFromJson(string jsonString, string assemblyName = "DynamicPluginAssembly", bool useCache = true) + { + if (string.IsNullOrWhiteSpace(jsonString)) + { + throw new ArgumentException("JSON 字符串不能为空", nameof(jsonString)); + } + + try + { + var plugins = JsonSerializer.Deserialize>(jsonString, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true + }); + + if (plugins == null) + { + throw new ParserException("JSON 反序列化失败,结果为 null"); + } + + return Generate(plugins, assemblyName, useCache); + } + catch (JsonException ex) + { + throw new ParserException($"JSON 格式错误: {ex.Message}", ex); + } + } + + /// + /// 从 JSON 文件生成动态程序集 + /// + /// JSON 文件路径 + /// 程序集名称 + /// 是否使用缓存 + /// 生成的动态程序集 + public static async Task GenerateFromFileAsync(string jsonFilePath, string assemblyName = "DynamicPluginAssembly", bool useCache = true) + { + if (string.IsNullOrWhiteSpace(jsonFilePath)) + { + throw new ArgumentException("文件路径不能为空", nameof(jsonFilePath)); + } + + if (!File.Exists(jsonFilePath)) + { + throw new FileNotFoundException($"文件不存在: {jsonFilePath}"); + } + + try + { + var jsonString = await File.ReadAllTextAsync(jsonFilePath); + return GenerateFromJson(jsonString, assemblyName, useCache); + } + catch (Exception ex) when (!(ex is ParserException)) + { + throw new ParserException($"读取文件失败: {jsonFilePath}", ex); + } + } + + + /// + /// 清除所有缓存 + /// + public static void ClearCache() + { + AssemblyCache.ClearCache(); + TypeMapper.ClearCache(); + } + + /// + /// 获取缓存统计信息 + /// + /// 缓存统计信息 + public static CacheStatistics GetCacheStatistics() + { + return AssemblyCache.GetStatistics(); + } + + /// + /// 检查插件清单是否已有缓存 + /// + /// 插件信息列表 + /// 程序集名称 + /// 是否存在缓存 + public static bool HasCache(List plugins, string assemblyName = "DynamicPluginAssembly") + { + return AssemblyCache.HasCache(plugins, assemblyName); + } + + /// + /// 将程序集保存到磁盘(用于调试) + /// + /// 要保存的程序集 + /// 保存路径 + /// 是否覆盖现有文件 + public static void SaveAssemblyToFile(Assembly assembly, string filePath, bool overwrite = false) + { + if (assembly == null) + throw new ArgumentNullException(nameof(assembly)); + + if (string.IsNullOrWhiteSpace(filePath)) + throw new ArgumentException("文件路径不能为空", nameof(filePath)); + + if (File.Exists(filePath) && !overwrite) + throw new InvalidOperationException($"文件已存在且不允许覆盖: {filePath}"); + + try + { + // 注意:在 .NET 8.0 中,动态程序集默认是不可保存的 + // 这个方法主要用于调试目的,实际保存功能需要特殊处理 + Console.WriteLine($"[Parser] 注意: .NET 8.0 中动态程序集无法直接保存到磁盘"); + Console.WriteLine($"[Parser] 程序集信息: {assembly.FullName}"); + Console.WriteLine($"[Parser] 包含类型: {string.Join(", ", assembly.GetTypes().Select(t => t.Name))}"); + } + catch (Exception ex) + { + throw new ParserException($"保存程序集失败: {filePath}", ex); + } + } + + /// + /// 获取程序集中的所有插件类型 + /// + /// 程序集 + /// 插件类型列表 + public static List GetPluginTypes(Assembly assembly) + { + if (assembly == null) + throw new ArgumentNullException(nameof(assembly)); + + try + { + return assembly.GetTypes() + .Where(t => t.IsClass && t.IsAbstract && t.IsSealed) // 静态类 + .ToList(); + } + catch (Exception ex) + { + throw new ParserException("获取插件类型失败", ex); + } + } + + /// + /// 获取插件类型的所有方法信息 + /// + /// 插件类型 + /// 方法信息列表 + public static List GetPluginMethods(Type pluginType) + { + if (pluginType == null) + throw new ArgumentNullException(nameof(pluginType)); + + try + { + return pluginType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(m => !m.IsSpecialName) // 排除特殊方法如属性访问器 + .ToList(); + } + catch (Exception ex) + { + throw new ParserException($"获取插件方法失败: {pluginType.Name}", ex); + } + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/README.md b/KitX Script/Kscript.CSharp.Parser/README.md new file mode 100644 index 0000000..fff54a1 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/README.md @@ -0,0 +1,218 @@ +# KitX.CSharp.Parser +> 将插件清单 JSON 即时编译成可直接调用的 C# 静态 API + +--- + +## 🌱 项目定位 + +KitX.CSharp.Parser 是 **KitX 工作流子系统** 的核心组件。它专注于一个核心功能: + +**把插件清单(JSON)→ 运行时静态类/方法**,让脚本引擎可以像调用普通静态方法一样使用插件功能。 + +``` +JSON 清单 ─→ Parser ─→ 内存程序集 ─→ 脚本侧: SamplePlugin.Method(...) +``` + +--- + +## 🎯 核心特性 + +- ✅ **动态 IL 生成** - 使用 `Reflection.Emit` 生成高性能静态方法 +- ✅ **智能类型映射** - 支持基础类型和自定义类型 +- ✅ **程序集缓存** - 避免重复生成,提升性能 +- ✅ **简化设计** - 直接依赖注入,无复杂抽象层 +- ✅ **异常处理** - 完善的错误处理机制 +- ✅ **调试友好** - 详细的日志输出和统计信息 + +--- + +## 🚀 快速开始 + +### 基本用法 + +```csharp +using Kscript.CSharp.Parser; + +// 1. 设置插件管理器(必须) +Parser.SetPluginManager(new MockPluginManager()); + +// 2. 从 JSON 文件生成程序集 +var assembly = await Parser.GenerateFromFileAsync("plugins.json"); + +// 3. 从插件信息列表生成程序集 +var plugins = new List { /* ... */ }; +var assembly = Parser.Generate(plugins); + +// 4. 脚本中直接调用生成的方法 +int sum = SampleCalculator.Add(10, 20); +string reversed = StringToolkit.Reverse("Hello"); +``` + +### 运行演示 + +```bash +# 构建项目 +dotnet build + +# 运行演示程序 +dotnet run +``` + +--- + +## 🔌 API 参考 + +### Parser 静态类 + +#### 生成方法 + +| 方法 | 描述 | 参数 | +|------|------|------| +| `Generate()` | 从插件信息列表生成程序集 | `plugins`, `assemblyName`, `useCache` | +| `GenerateFromJson()` | 从 JSON 字符串生成程序集 | `jsonString`, `assemblyName`, `useCache` | +| `GenerateFromFileAsync()` | 从 JSON 文件异步生成程序集 | `jsonFilePath`, `assemblyName`, `useCache` | + +#### 配置方法 + +| 方法 | 描述 | +|------|------| +| `SetPluginManager()` | 设置插件管理器实例 | `IPluginManager pluginManager` | +| `ClearCache()` | 清除所有缓存 | +| `GetCacheStatistics()` | 获取缓存统计信息 | + +#### 分析方法 + +| 方法 | 描述 | +|------|------| +| `GetPluginTypes()` | 获取程序集中的所有插件类型 | +| `GetPluginMethods()` | 获取插件类型的所有方法信息 | + +--- + +## 📊 类型映射支持 + +| JSON 字符串 | CLR 类型 | 示例 | +|---|---|---| +| `"void"` | `System.Void` | 无返回值 | +| `"bool"` | `System.Boolean` | 布尔参数 | +| `"int"` | `System.Int32` | 整数参数 | +| `"double"` | `System.Double` | 浮点数参数 | +| `"string"` | `System.String` | 字符串参数 | +| `"object"` | `System.Object` | 对象参数 | + +--- + +## 🏗️ 项目结构 + +``` +KitX.CSharp.Parser/ +├─ Parser.cs # 主入口静态类 +├─ Models/ +│ └─ PluginCallInfo.cs # 插件调用信息模型 +├─ CodeGen/ +│ ├─ TypeMapper.cs # 字符串 → Type 映射 +│ ├─ MethodEmitter.cs # IL 方法生成器 +│ └─ AssemblyCache.cs # 程序集缓存管理 +├─ Core/ +│ ├─ IPluginManager.cs # 插件管理器接口 +│ └─ MockPluginManager.cs # 模拟实现 +├─ Exceptions/ +│ └─ ParserException.cs # 自定义异常 +├─ Examples/ +│ ├─ BasicUsageExample.cs # 基础用法示例 +│ └─ Program.cs # 演示程序入口 +├─ example.json # 插件清单示例 +├─ README.md # 项目说明 +└─ USAGE.md # 详细使用指南 +``` + +--- + +## 🎯 运行结果示例 + +``` +🚀 欢迎使用 KitX.CSharp.Parser 演示程序! + +=== 测试简化后的 Parser 功能 === +1. 测试默认插件管理器设置... + ✓ 默认 MockPluginManager 工作正常 + +2. 测试自定义插件管理器设置... + ✓ 自定义插件管理器设置工作正常 + +3. 测试空参数处理... + ✓ 空参数正确抛出 ArgumentNullException + +4. 测试插件存在性检测... + ✓ SampleCalculator 存在: True + ✓ SampleCalculator.Add 方法存在: True + ✓ NonExistentPlugin 存在: False + ✓ SampleCalculator.NonExistentMethod 方法存在: False + ✓ 插件存在性检测功能工作正常 + +=== KitX.CSharp.Parser 基础用法示例 === +1. 加载插件清单... + ✓ 成功生成程序集: ExamplePluginAssembly + +2. 分析生成的插件类型... + 发现 2 个插件类: + - SampleCalculator + └── Int32 Add(Int32 a, Int32 b) + └── Double Divide(Double numerator, Double denominator, Int32 decimals) + - StringToolkit + └── String Reverse(String text) + └── String ToUpper(String text) + +3. 动态调用插件方法... + 调用 Add(10, 20) → 结果: 30 + 调用 Divide(10, 3, 2) → 结果: 3.33 + 调用 Reverse(Hello) → 结果: olleH + 调用 ToUpper(world) → 结果: WORLD + +4. 缓存统计信息... + 缓存统计: 1 个程序集 + +5. 测试缓存效果... + 第二次生成是否使用缓存: True + 强制重新生成是否使用新程序集: True + 重新生成后缓存存在: True + +✅ 所有示例运行完成! +``` + +--- + +## 🔧 实际插件管理器集成 + +### 设置真实插件管理器 + +```csharp +// 在 KitX Dashboard 启动时设置 +Parser.SetPluginManager(new RealPluginManager( + KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance, + message => Log.Information(message) +)); +``` + +### 在脚本中使用 + +```csharp +// 现在可以直接调用,将通过真实的 KitX Dashboard 插件系统执行 +var result = SampleCalculator.Add(10, 20); +``` + +--- + +## 📝 注意事项 + +- ✅ **简化设计**:采用直接依赖注入,移除了复杂的工厂函数模式 +- ✅ **必须初始化**:使用前必须调用 `SetPluginManager()` 设置插件管理器 +- ✅ **插件验证**:生成程序集前会检查所需插件是否存在 +- ✅ **缓存优化**:相同插件清单只生成一次,提升性能 +- ✅ **异常安全**:完善的参数验证和错误处理机制 + +--- + +## 🎉 总结 + +KitX.CSharp.Parser 提供了一个简洁、高效、易维护的解决方案,用于将插件清单转换为可直接调用的 C# API。通过简化的设计和完善的错误处理,确保了在生产环境中的稳定性和可靠性。 diff --git a/KitX Script/Kscript.CSharp.Parser/USAGE.md b/KitX Script/Kscript.CSharp.Parser/USAGE.md new file mode 100644 index 0000000..7e0f2e7 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/USAGE.md @@ -0,0 +1,271 @@ +# KitX.CSharp.Parser 使用指南 + +## 📖 概述 + +KitX.CSharp.Parser 是 KitX 工作流子系统的核心组件,它能够将插件清单 JSON 即时编译成可直接调用的 C# 静态 API。通过动态 IL 生成技术,让脚本引擎可以像调用普通静态方法一样使用插件功能。 + +## 🚀 快速开始 + +### 基本用法 + +```csharp +using Kscript.CSharp.Parser; + +// 1. 设置插件管理器(必须) +Parser.SetPluginManager(new MockPluginManager()); + +// 2. 从 JSON 字符串生成程序集 +var jsonString = File.ReadAllText("plugins.json"); +var assembly = Parser.GenerateFromJson(jsonString); + +// 3. 从文件生成程序集 +var assembly = await Parser.GenerateFromFileAsync("plugins.json"); + +// 4. 从插件信息列表生成程序集 +var plugins = new List { /* ... */ }; +var assembly = Parser.Generate(plugins); + +// 5. 脚本中直接调用生成的方法 +int sum = SampleCalculator.Add(10, 20); +string reversed = StringToolkit.Reverse("Hello"); +``` + +### 运行演示 + +```bash +# 构建项目 +dotnet build + +# 运行演示程序 +dotnet run +``` + +## 🔧 API 参考 + +### Parser 静态类 + +#### 生成方法 + +| 方法 | 描述 | 参数 | +|------|------|------| +| `Generate()` | 从插件信息列表生成程序集 | `plugins`, `assemblyName`, `useCache` | +| `GenerateFromJson()` | 从 JSON 字符串生成程序集 | `jsonString`, `assemblyName`, `useCache` | +| `GenerateFromFileAsync()` | 从 JSON 文件异步生成程序集 | `jsonFilePath`, `assemblyName`, `useCache` | + +#### 配置方法 + +| 方法 | 描述 | +|------|------| +| `SetPluginManager()` | 设置插件管理器实例 | `IPluginManager pluginManager` | +| `ClearCache()` | 清除所有缓存 | +| `GetCacheStatistics()` | 获取缓存统计信息 | + +#### 分析方法 + +| 方法 | 描述 | +|------|------| +| `GetPluginTypes()` | 获取程序集中的所有插件类型 | +| `GetPluginMethods()` | 获取插件类型的所有方法信息 | +| `HasCache()` | 检查是否存在缓存 | + +## 📊 类型映射系统 + +### 支持的基础类型 + +| JSON 字符串 | CLR 类型 | 示例 | +|---|---|---| +| `"void"` | `System.Void` | 无返回值 | +| `"bool"` | `System.Boolean` | 布尔参数 | +| `"int"` | `System.Int32` | 整数参数 | +| `"double"` | `System.Double` | 浮点数参数 | +| `"string"` | `System.String` | 字符串参数 | +| `"object"` | `System.Object` | 对象参数 | + +## 🏗️ 架构设计 + +### 核心组件 + +1. **Parser** - 主入口类,统一API接口 +2. **TypeMapper** - 类型映射器,处理字符串到Type的转换 +3. **MethodEmitter** - IL方法生成器,核心IL生成逻辑 +4. **AssemblyCache** - 程序集缓存,提升性能 +5. **PluginManager** - 插件管理器接口,处理实际调用 + +### 工作流程 + +```mermaid +graph TD + A[JSON 插件清单] --> B[Parser.Generate] + B --> C[TypeMapper 类型映射] + C --> D[MethodEmitter IL生成] + D --> E[AssemblyCache 缓存] + E --> F[动态程序集] + F --> G[脚本调用] +``` + +## 📝 插件清单格式 + +### 基本结构 + +```json +[ + { + "Name": "PluginName", + "Version": "1.0.0", + "Functions": [ + { + "Name": "MethodName", + "ReturnValueType": "int", + "Parameters": [ + { + "Name": "paramName", + "Type": "string", + "IsOptional": false, + "Value": "defaultValue" + } + ] + } + ] + } +] +``` + +### 完整示例 + +参考项目根目录的 `example.json` 文件,包含完整的插件定义示例。 + +## ⚡ 性能优化 + +### 缓存机制 + +- **程序集缓存**: 相同插件清单只生成一次 +- **类型映射缓存**: 避免重复类型解析 +- **智能哈希**: 基于插件内容生成缓存键 + +### 最佳实践 + +```csharp +// 1. 复用程序集 +var assembly = Parser.Generate(plugins, useCache: true); + +// 2. 批量注册自定义类型 +Parser.RegisterCustomType("DateTime", typeof(DateTime)); +Parser.RegisterCustomType("Guid", typeof(Guid)); + +// 3. 合理使用强制重新生成 +if (pluginChanged) +{ + assembly = Parser.Generate(plugins, useCache: false); +} +``` + +## 🚨 错误处理 + +### 异常类型 + +- `ParserException` - 解析器相关异常 +- `ArgumentException` - 参数错误 +- `FileNotFoundException` - 文件不存在 +- `InvalidOperationException` - 初始化状态错误 + +### 异常处理示例 + +```csharp +try +{ + var assembly = Parser.GenerateFromJson(jsonString); +} +catch (ParserException ex) +{ + Console.WriteLine($"解析失败: {ex.Message}"); +} +catch (JsonException ex) +{ + Console.WriteLine($"JSON 格式错误: {ex.Message}"); +} +``` + +## 🔍 调试功能 + +### 日志输出 + +使用 `MockPluginManager` 时会输出详细的调用日志: + +``` +[MockPluginManager] 调用插件方法: SampleCalculator.Add(10, 20) +[MockPluginManager] 参数类型: [Int32, Int32] +[MockPluginManager] 期望返回类型: Int32 +[MockPluginManager] 返回结果: 30 +``` + +### 缓存统计 + +```csharp +var stats = Parser.GetCacheStatistics(); +Console.WriteLine($"缓存统计: {stats.CachedAssemblyCount} 个程序集"); +``` + +## 🔌 扩展点 + +### 自定义插件管理器 + +```csharp +public class CustomPluginManager : IPluginManager +{ + public T Call(PluginCallInfo callInfo) + { + // 实现自定义调用逻辑 + return default(T); + } + + // 实现其他接口方法... +} + +// 设置自定义管理器 +Parser.SetPluginManager(new CustomPluginManager()); +``` + +### 自定义类型映射(已移除,未来可能会用更好的方案重新添加回来,取决于上游协议) + +```csharp +// 扩展类型映射 +TypeMapper.RegisterCustomType("Vector3", typeof(UnityEngine.Vector3)); +TypeMapper.RegisterCustomType("Color", typeof(System.Drawing.Color)); +``` + +## 🔄 与其他系统集成 + +### 脚本引擎集成 + +生成的程序集可直接用于各种 C# 脚本引擎: + +- **Roslyn** - Microsoft.CodeAnalysis +- **CSharpScript** - Microsoft.CodeAnalysis.CSharp.Scripting +- **CS-Script** - 第三方脚本引擎 + +### 示例集成代码 + +```csharp +// 在脚本中使用生成的API +var Script = @" + int result = SampleCalculator.Add(10, 20); + string reversed = StringToolkit.Reverse(""Hello""); + return result; +"; + +// 通过脚本引擎执行 +var assembly = Parser.Generate(plugins); +var ScriptResult = ExecuteScript(Script, assembly); +``` + +## 📚 更多示例 + +完整的使用示例请参考: + +- `Examples/BasicUsageExample.cs` - 基础功能演示 +- `Examples/Program.cs` - 控制台演示程序 +- `RealPluginManagerExample.cs` - "真实"插件管理器示例 + +## 🎯 总结 + +KitX.CSharp.Parser 提供了一个简洁、高效、易维护的解决方案,用于将插件清单转换为可直接调用的 C# API。通过简化的设计和完善的错误处理,确保了在生产环境中的稳定性和可靠性。 diff --git a/KitX Script/Kscript.CSharp.Parser/example.json b/KitX Script/Kscript.CSharp.Parser/example.json new file mode 100644 index 0000000..60ed38c --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/example.json @@ -0,0 +1,154 @@ +[ + { + "Name": "SampleCalculator", + "Version": "1.0.0", + "DisplayName": { + "en": "Calculator", + "zh": "计算器" + }, + "AuthorName": "KitX Devs", + "AuthorLink": "https://github.com/Crequency/KitX", + "PublisherName": "Crequency", + "PublisherLink": "https://kitx.cc", + "SimpleDescription": { + "en": "A simple calculator plugin.", + "zh": "简易计算器插件" + }, + "ComplexDescription": {}, + "TotalDescriptionInMarkdown": {}, + "IconInBase64": "", + "PublishDate": "2025-07-29T00:00:00Z", + "LastUpdateDate": "2025-07-29T00:00:00Z", + "IsMarketVersion": true, + "Tags": { + "en": "math,utility", + "zh": "数学,工具" + }, + "Functions": [ + { + "Name": "Add", + "DisplayNames": { + "en": "Add two integers", + "zh": "整数相加" + }, + "Parameters": [ + { + "Name": "a", + "DisplayNames": { + "en": "First number", + "zh": "第一个数" + }, + "Type": "int", + "Value": "0", + "IsOptional": false + }, + { + "Name": "b", + "DisplayNames": { + "en": "Second number", + "zh": "第二个数" + }, + "Type": "int", + "Value": "0", + "IsOptional": false + } + ], + "ReturnValue": "Sum of a and b", + "ReturnValueType": "int" + }, + { + "Name": "Divide", + "DisplayNames": { + "en": "Divide with optional precision", + "zh": "带可选精度的除法" + }, + "Parameters": [ + { + "Name": "numerator", + "Type": "double", + "Value": "0", + "IsOptional": false + }, + { + "Name": "denominator", + "Type": "double", + "Value": "1", + "IsOptional": false + }, + { + "Name": "decimals", + "Type": "int", + "Value": "2", + "IsOptional": true + } + ], + "ReturnValue": "Rounded quotient", + "ReturnValueType": "double" + } + ], + "RootStartupFileName": "SampleCalculator.dll" + }, + { + "Name": "StringToolkit", + "Version": "1.2.0", + "DisplayName": { + "en": "String Toolkit", + "zh": "字符串工具箱" + }, + "AuthorName": "Community", + "AuthorLink": "https://example.com", + "PublisherName": "KitX", + "PublisherLink": "https://kitx.cc", + "SimpleDescription": { + "en": "Utility functions for string manipulation.", + "zh": "字符串处理工具集" + }, + "ComplexDescription": {}, + "TotalDescriptionInMarkdown": {}, + "IconInBase64": "", + "PublishDate": "2025-07-29T00:00:00Z", + "LastUpdateDate": "2025-07-29T00:00:00Z", + "IsMarketVersion": false, + "Tags": { + "en": "string,utility,text", + "zh": "字符串,工具,文本" + }, + "Functions": [ + { + "Name": "Reverse", + "DisplayNames": { + "en": "Reverse string", + "zh": "反转字符串" + }, + "Parameters": [ + { + "Name": "text", + "Type": "string", + "Value": "", + "IsOptional": false + } + ], + "ReturnValue": "Reversed text", + "ReturnValueType": "string" + }, + { + "Name": "ToUpper", + "DisplayNames": { + "en": "Convert to upper case", + "zh": "转大写" + }, + "Parameters": [ + { + "Name": "text", + "Type": "string", + "Value": "", + "IsOptional": false + } + ], + "ReturnValue": "Uppercase text", + "ReturnValueType": "string" + } + ], + "RootStartupFileName": "StringToolkit.dll" + } + ] diff --git a/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs b/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs new file mode 100644 index 0000000..d051b2e --- /dev/null +++ b/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs @@ -0,0 +1,15 @@ +using KitX.Shared.CSharp.Device; + +namespace Kscript.CSharp.Interfaces +{ + public interface IComposer + { + Task RequestLocalDevice(); + Task RequestMainController(); + Task RequestRandomDesktopDevice(); + Task RequestRandomMobileDevice(); + Task RequestDeviceByFilter(Func filter); + Task RequestUserSelectedDevice(IEnumerable candidates); + Task> RequestDeviceList(); + } +} diff --git a/KitX Script/Kscript.CSharp/Interfaces/IDevice.cs b/KitX Script/Kscript.CSharp/Interfaces/IDevice.cs new file mode 100644 index 0000000..029b635 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Interfaces/IDevice.cs @@ -0,0 +1,14 @@ +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Interfaces +{ + public interface IDevice + { + DeviceInfo Info { get; } + Task RequestPlugin(string idOrName); + Task> GetPluginList(); + bool HasPlugin(string idOrName); + Task CreatePluginInstance(PluginInfo info); + } +} diff --git a/KitX Script/Kscript.CSharp/Interfaces/IDeviceEventListener.cs b/KitX Script/Kscript.CSharp/Interfaces/IDeviceEventListener.cs new file mode 100644 index 0000000..3071b78 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Interfaces/IDeviceEventListener.cs @@ -0,0 +1,34 @@ +using KitX.Shared.CSharp.Device; + +namespace Kscript.CSharp.Interfaces +{ + /// + /// 设备事件监听器接口 + /// + public interface IDeviceEventListener + { + /// + /// 当设备列表更新时触发 + /// + /// 更新后的设备列表 + void OnDeviceListUpdated(IEnumerable devices); + + /// + /// 当有新设备添加时触发 + /// + /// 新添加的设备 + void OnDeviceAdded(DeviceInfo device); + + /// + /// 当设备被移除时触发 + /// + /// 被移除的设备 + void OnDeviceRemoved(DeviceInfo device); + + /// + /// 当设备状态变化时触发 + /// + /// 状态发生变化的设备 + void OnDeviceStatusChanged(DeviceInfo device); + } +} \ No newline at end of file diff --git a/KitX Script/Kscript.CSharp/Interfaces/IFunction.cs b/KitX Script/Kscript.CSharp/Interfaces/IFunction.cs new file mode 100644 index 0000000..3f71963 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Interfaces/IFunction.cs @@ -0,0 +1,12 @@ +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Interfaces +{ + public interface IFunction + { + Function Info { get; } + PluginInfo AssociatedPlugin { get; } + Task Invoke(params string[] parameters); + Type GetReturnType(); + } +} diff --git a/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs b/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs new file mode 100644 index 0000000..2e290d0 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs @@ -0,0 +1,16 @@ +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Interfaces +{ + public interface IPlugin + { + PluginInfo Info { get; } + DeviceInfo AssociatedDevice { get; } + Task RequestFunction(string idOrName); + Task> GetFunctionList(); + Task GetFunctionByType(string type); + Task ExecuteFunction(string functionName, params string[] parameters); + bool HasFunction(string idOrName); + } +} diff --git a/KitX Script/Kscript.CSharp/Kscript.CSharp.csproj b/KitX Script/Kscript.CSharp/Kscript.CSharp.csproj new file mode 100644 index 0000000..c2dce81 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Kscript.CSharp.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + True + + + + $(Version) + $(Version) + 24.10.$([System.DateTime]::UtcNow.Date.Subtract($([System.DateTime]::Parse("2024-02-07"))).TotalDays).$([System.Math]::Floor($([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes))) + + + + Kscript.CSharp + StarDustSeemsInk,Dynesshely + Crequency + KitX Script CSharp + AGPL-3.0-only + True + + + + + + diff --git a/KitX Script/Kscript.CSharp/Kscript.CSharp.sln b/KitX Script/Kscript.CSharp/Kscript.CSharp.sln new file mode 100644 index 0000000..bf8c8ad --- /dev/null +++ b/KitX Script/Kscript.CSharp/Kscript.CSharp.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kscript.CSharp", "Kscript.CSharp.csproj", "{F935BEC1-159C-96F1-83A8-DE0F8509777F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F935BEC1-159C-96F1-83A8-DE0F8509777F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F935BEC1-159C-96F1-83A8-DE0F8509777F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F935BEC1-159C-96F1-83A8-DE0F8509777F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F935BEC1-159C-96F1-83A8-DE0F8509777F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CFC1FA4A-256C-4413-BD89-42FCAB741EBB} + EndGlobalSection +EndGlobal diff --git a/KitX Script/Kscript.CSharp/Services/Composer.cs b/KitX Script/Kscript.CSharp/Services/Composer.cs new file mode 100644 index 0000000..0f1f520 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Services/Composer.cs @@ -0,0 +1,264 @@ +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; +using KitX.Shared.CSharp.WebCommand; +using Kscript.CSharp.Interfaces; +using Kscript.CSharp.Utils; +using System.Text.Json; + +namespace Kscript.CSharp.Services +{ + /// + /// 设备管理器 + /// + public class Composer : IComposer, IDisposable + { + private readonly Connector _connector; + private readonly DeviceCache _deviceCache; + private readonly DeviceRequestBuilder _requestBuilder; + private readonly HashSet _listeners = new(); + private bool _isDisposed; + + private readonly object _lockObject = new(); + private Dictionary _lastKnownDevices = new(); + + // 设备离线判断阈值(超过这个时间没有收到更新则认为设备离线) + private static readonly TimeSpan DeviceOfflineThreshold = TimeSpan.FromMinutes(2); + + public Composer(Connector? connector = null) + { + _connector = connector ?? Connector.Instance; + _deviceCache = new DeviceCache(); + _requestBuilder = new DeviceRequestBuilder(_connector); + } + + /// + /// 添加设备事件监听器 + /// + public void AddListener(IDeviceEventListener listener) + { + if (_isDisposed) + throw new ObjectDisposedException(nameof(Composer)); + + lock (_lockObject) + { + _listeners.Add(listener); + } + } + + /// + /// 移除设备事件监听器 + /// + public void RemoveListener(IDeviceEventListener listener) + { + if (_isDisposed) + return; + + lock (_lockObject) + { + _listeners.Remove(listener); + } + } + + /// + /// 获取本地设备 + /// + public async Task RequestLocalDevice() + { + var devices = await RequestDeviceList(); + var localDevice = devices.FirstOrDefault(NetworkUtils.IsLocalDevice); + return localDevice != null ? new Device(localDevice, _connector) : null; + } + + /// + /// 获取主控制器设备 + /// + public async Task RequestMainController() + { + var devices = await RequestDeviceList(); + var mainController = devices.FirstOrDefault(x => x.IsMainDevice); + return mainController != null ? new Device(mainController, _connector) : null; + } + + /// + /// 获取随机桌面设备 + /// + public async Task RequestRandomDesktopDevice() + { + var devices = await RequestDeviceList(); + var desktopDevices = devices.Where(x => x.DeviceOSType == OperatingSystems.Windows + || x.DeviceOSType == OperatingSystems.MacOS + || x.DeviceOSType == OperatingSystems.Linux) + .ToList(); + + if (!desktopDevices.Any()) + return null; + + var random = new Random(); + var randomDevice = desktopDevices[random.Next(desktopDevices.Count)]; + return new Device(randomDevice, _connector); + } + + /// + /// 获取随机移动设备 + /// + public async Task RequestRandomMobileDevice() + { + var devices = await RequestDeviceList(); + var mobileDevices = devices.Where(x => x.DeviceOSType == OperatingSystems.Android + || x.DeviceOSType == OperatingSystems.IOS) + .ToList(); + + if (!mobileDevices.Any()) + return null; + + var random = new Random(); + var randomDevice = mobileDevices[random.Next(mobileDevices.Count)]; + return new Device(randomDevice, _connector); + } + + /// + /// 根据过滤器获取设备 + /// + public async Task RequestDeviceByFilter(Func filter) + { + var devices = await RequestDeviceList(); + var matchedDevice = devices.FirstOrDefault(filter); + return matchedDevice != null ? new Device(matchedDevice, _connector) : null; + } + + /// + /// 获取用户选择的设备 + /// + public async Task RequestUserSelectedDevice(IEnumerable candidates) + { + var response = await _requestBuilder + .WithFunction("SelectDevice", candidates) + .ExecuteAsync(async response => + { + var result = await ResponseHandler.HandleStringResponse(response); + if (string.IsNullOrEmpty(result)) + return null; + + return candidates.FirstOrDefault(d => + d.Device.MacAddress.ToString().Equals(result, StringComparison.OrdinalIgnoreCase)); + }); + + return response != null ? new Device(response, _connector) : null; + } + + /// + /// 获取设备列表 + /// + public async Task> RequestDeviceList() + { + return await _deviceCache.GetOrAdd("devices", async () => + { + var response = await _requestBuilder + .WithFunction("GetDeviceList") + .ExecuteAsync(async response => + { + var result = await ResponseHandler.HandlePrefixedResponse>( + response, + "DeviceList:"); + + if (result != null) + UpdateDeviceStates(result); + + return result ?? new List(); + }); + + return response; + }); + } + + private void UpdateDeviceStates(IEnumerable newDevices) + { + if (_isDisposed) + return; + + lock (_lockObject) + { + var newDevicesDict = newDevices.ToDictionary(d => d.Device.MacAddress.ToString()); + + // Find removed devices + var removedDevices = _lastKnownDevices.Keys + .Except(newDevicesDict.Keys) + .Select(key => _lastKnownDevices[key]) + .ToList(); + + // Find added devices + var addedDevices = newDevicesDict.Keys + .Except(_lastKnownDevices.Keys) + .Select(key => newDevicesDict[key]) + .ToList(); + + // Find changed devices + var changedDevices = _lastKnownDevices.Keys + .Intersect(newDevicesDict.Keys) + .Where(key => HasDeviceChanged(_lastKnownDevices[key], newDevicesDict[key])) + .Select(key => newDevicesDict[key]) + .ToList(); + + // Update last known devices + _lastKnownDevices = newDevicesDict; + + // Notify listeners + var listeners = _listeners.ToList(); + foreach (var listener in listeners) + { + try + { + foreach (var device in removedDevices) + listener.OnDeviceRemoved(device); + + foreach (var device in addedDevices) + listener.OnDeviceAdded(device); + + foreach (var device in changedDevices) + listener.OnDeviceStatusChanged(device); + + listener.OnDeviceListUpdated(newDevices); + } + catch (Exception ex) + { + // Log error but continue with other listeners + System.Diagnostics.Debug.WriteLine($"Error in device listener: {ex.Message}"); + } + } + } + } + + private bool HasDeviceChanged(DeviceInfo oldDevice, DeviceInfo newDevice) + { + // 检查设备在线状态(基于最后一次发送时间) + var oldDeviceOnline = DateTime.UtcNow - oldDevice.SendTime < DeviceOfflineThreshold; + var newDeviceOnline = DateTime.UtcNow - newDevice.SendTime < DeviceOfflineThreshold; + + if (oldDeviceOnline != newDeviceOnline) + return true; + + // 检查关键属性变化 + return oldDevice.PluginsCount != newDevice.PluginsCount || + oldDevice.Device.IPv4 != newDevice.Device.IPv4 || + oldDevice.Device.IPv6 != newDevice.Device.IPv6 || + oldDevice.PluginsServerPort != newDevice.PluginsServerPort || + oldDevice.DevicesServerPort != newDevice.DevicesServerPort || + oldDevice.IsMainDevice != newDevice.IsMainDevice || + oldDevice.DeviceOSVersion != newDevice.DeviceOSVersion; + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + _deviceCache.Dispose(); + lock (_lockObject) + { + _listeners.Clear(); + _lastKnownDevices.Clear(); + } + } + } +} diff --git a/KitX Script/Kscript.CSharp/Services/Device.cs b/KitX Script/Kscript.CSharp/Services/Device.cs new file mode 100644 index 0000000..52d222d --- /dev/null +++ b/KitX Script/Kscript.CSharp/Services/Device.cs @@ -0,0 +1,124 @@ +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; +using KitX.Shared.CSharp.WebCommand; +using System.Text.Json; +using Kscript.CSharp.Interfaces; + +namespace Kscript.CSharp.Services +{ + public class Device : IDevice + { + private readonly Connector _connector; + private Dictionary _pluginCache = new(); + + public DeviceInfo Info { get; } + + public Device(DeviceInfo info, Connector? connector = null) + { + Info = info; + _connector = connector ?? Connector.Instance; + } + + public async Task RequestPlugin(string Name) + { + var plugins = await GetPluginList(); + var pluginInfo = plugins.FirstOrDefault(p => + p.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)); + + return pluginInfo != null ? await CreatePluginInstance(pluginInfo) : null; + } + + public async Task> GetPluginList() + { + if (_pluginCache.Any()) + return _pluginCache.Values; + + var tcs = new TaskCompletionSource>(); + + void OnResponse(Request response) + { + try + { + response.Match( + response.GetContent(content => + { + if (string.IsNullOrEmpty(content)) + { + tcs.SetResult(Array.Empty()); + return content; + } + + // With the following code + var plugins = JsonSerializer.Deserialize>(content); + + // Update cache + _pluginCache = plugins.ToDictionary( + p => p.Name, + p => p, + StringComparer.OrdinalIgnoreCase + ); + + tcs.SetResult(plugins); + return content; + }), + matchCommand: content => + { + if (content.StartsWith("Error:")) + { + var error = content.Substring("Error:".Length); + tcs.SetException(new InvalidOperationException($"Failed to get plugin list: {error}")); + } + else if (content.StartsWith("PluginList:")) + { + var pluginListJson = content.Substring("PluginList:".Length); + var plugins = JsonSerializer.Deserialize>(pluginListJson); + + // Update cache + _pluginCache = plugins.ToDictionary( + p => p.Name, + p => p, + StringComparer.OrdinalIgnoreCase + ); + + tcs.SetResult(plugins); + } + } + ); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + } + + // Request plugin list from device + _connector.Request() + .UpdateCommand(cmd => + { + cmd.FunctionName = "GetPluginList"; + return cmd; + }) + .UpdateRequest(req => + { + req.Type = RequestTypes.Command; + req.Version = RequestVersions.V1; + req.Target = Info.Device; + return req; + }) + .SetSender(OnResponse) + .Send(); + + return await tcs.Task; + } + + public bool HasPlugin(string Name) + { + return _pluginCache.ContainsKey(Name); + } + + public async Task CreatePluginInstance(PluginInfo info) + { + return new Plugin(info, Info, _connector); + } + } +} diff --git a/KitX Script/Kscript.CSharp/Services/Function.cs b/KitX Script/Kscript.CSharp/Services/Function.cs new file mode 100644 index 0000000..f9aefba --- /dev/null +++ b/KitX Script/Kscript.CSharp/Services/Function.cs @@ -0,0 +1,156 @@ +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; +using KitX.Shared.CSharp.WebCommand; +using Kscript.CSharp.Interfaces; + +namespace Kscript.CSharp.Services +{ + public class Function : IFunction + { + private readonly Connector _connector; + private readonly DeviceInfo _deviceInfo; + + public KitX.Shared.CSharp.Plugin.Function Info { get; } + public PluginInfo AssociatedPlugin { get; } + + public Function(KitX.Shared.CSharp.Plugin.Function info, PluginInfo pluginInfo, DeviceInfo deviceInfo, Connector? connector = null) + { + Info = info; + AssociatedPlugin = pluginInfo; + _deviceInfo = deviceInfo; + _connector = connector ?? Connector.Instance; + } + + public async Task Invoke(params string[] parameters) + { + _connector.Request() + .UpdateCommand(cmd => + { + cmd.FunctionName = Info.Name; + cmd.FunctionArgs = parameters.Select(p => new Parameter { Value = p }).ToList(); + return cmd; + }) + .UpdateRequest(req => + { + req.Target = _deviceInfo.Device; + return req; + }) + .Send(); + + var tcs = new TaskCompletionSource(); + + void OnResponse(Request response) + { + try + { + response.Match( + response.GetContent(content => + { + if (string.IsNullOrEmpty(content)) + { + tcs.SetResult(null); + return content; + } + + // Parse response based on return type + var returnType = GetReturnType(); + var result = ParseResponse(content, returnType); + tcs.SetResult(result); + return content; + }), + matchCommand: content => + { + if (content.StartsWith("Error:")) + { + var error = content.Substring("Error:".Length); + tcs.SetException(new InvalidOperationException($"Function execution failed: {error}")); + } + else if (content.StartsWith("Result:")) + { + var resultJson = content.Substring("Result:".Length); + var returnType = GetReturnType(); + var result = ParseResponse(resultJson, returnType); + tcs.SetResult(result); + } + } + ); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + } + + _connector.Request() + .SetSender(OnResponse); + + return await tcs.Task; + } + + public Type GetReturnType() + { + // Try to get the type from the assembly first + var type = Type.GetType(Info.ReturnValueType); + if (type != null) + return type; + + // Handle common type names that might not be fully qualified + switch (Info.ReturnValueType.ToLower()) + { + case "string": + return typeof(string); + case "int": + case "int32": + return typeof(int); + case "long": + case "int64": + return typeof(long); + case "float": + case "single": + return typeof(float); + case "double": + return typeof(double); + case "bool": + case "boolean": + return typeof(bool); + case "void": + return typeof(void); + default: + return typeof(object); + } + } + + private object ParseResponse(string content, Type returnType) + { + if (string.IsNullOrEmpty(content)) + return null; + + try + { + if (returnType == typeof(void)) + return null; + + // Handle primitive types + if (returnType == typeof(string)) + return content; + if (returnType == typeof(int)) + return int.Parse(content); + if (returnType == typeof(long)) + return long.Parse(content); + if (returnType == typeof(float)) + return float.Parse(content); + if (returnType == typeof(double)) + return double.Parse(content); + if (returnType == typeof(bool)) + return bool.Parse(content); + + // For complex types, deserialize from JSON + return System.Text.Json.JsonSerializer.Deserialize(content, returnType); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to parse response as {returnType.Name}: {ex.Message}", ex); + } + } + } +} diff --git a/KitX Script/Kscript.CSharp/Services/Plugin.cs b/KitX Script/Kscript.CSharp/Services/Plugin.cs new file mode 100644 index 0000000..b3f9a45 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Services/Plugin.cs @@ -0,0 +1,182 @@ +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; +using KitX.Shared.CSharp.WebCommand; +using Kscript.CSharp.Interfaces; + +namespace Kscript.CSharp.Services +{ + public class Plugin : IPlugin + { + private readonly Connector _connector; + + public PluginInfo Info { get; } + public DeviceInfo AssociatedDevice { get; } + + public Plugin(PluginInfo info, DeviceInfo deviceInfo, Connector? connector = null) + { + Info = info; + AssociatedDevice = deviceInfo; + _connector = connector ?? Connector.Instance; + } + + public async Task RequestFunction(string Name) + { + var functions = await GetFunctionList(); + var function = functions.FirstOrDefault(f => + f.Info.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)); + return function; + } + + public async Task> GetFunctionList() + { + var functions = Info.Functions.Select(f => + new Function(f, Info, AssociatedDevice, _connector) as IFunction); + return await Task.FromResult(functions); + } + + public async Task GetFunctionByType(string type) + { + var functions = await GetFunctionList(); + return functions.FirstOrDefault(f => + f.Info.ReturnValueType.Equals(type, StringComparison.OrdinalIgnoreCase)); + } + + public async Task ExecuteFunction(string functionName, params string[] parameters) + { + _connector.Request() + .UpdateCommand(cmd => + { + cmd.FunctionName = functionName; + cmd.FunctionArgs = parameters.Select(p => new Parameter { Value = p }).ToList(); + return cmd; + }) + .UpdateRequest(req => + { + req.Target = AssociatedDevice.Device; + return req; + }) + .Send(); + + // 处理响应 + var tcs = new TaskCompletionSource(); + + void OnResponse(Request response) + { + try + { + response.Match( + response.GetContent(content => + { + var returnType = GetFunctionReturnType(functionName); + var result = ParseFunctionResponse(content, returnType); + tcs.SetResult(result); + return content; + }), + matchCommand: content => + { + if (content.StartsWith("Error:")) + { + var error = content.Substring("Error:".Length); + tcs.SetException(new InvalidOperationException($"Function execution failed: {error}")); + } + else if (content.StartsWith("Result:")) + { + var resultJson = content.Substring("Result:".Length); + var returnType = GetFunctionReturnType(functionName); + var result = ParseFunctionResponse(resultJson, returnType); + tcs.SetResult(result); + } + } + ); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + } + + _connector.Request() + .SetSender(OnResponse); + + return await tcs.Task; + } + + private Type GetFunctionReturnType(string functionName) + { + var function = Info.Functions.FirstOrDefault(f => + f.Name.Equals(functionName, StringComparison.OrdinalIgnoreCase)); + + if (function.Equals(default(Function))) + throw new InvalidOperationException($"Function '{functionName}' not found in plugin '{Info.Name}'"); + + // Try to get the type from the assembly first + var type = Type.GetType(function.ReturnValueType); + if (type != null) + return type; + + // Handle common type names that might not be fully qualified + switch (function.ReturnValueType.ToLower()) + { + case "string": + return typeof(string); + case "int": + case "int32": + return typeof(int); + case "long": + case "int64": + return typeof(long); + case "float": + case "single": + return typeof(float); + case "double": + return typeof(double); + case "bool": + case "boolean": + return typeof(bool); + case "void": + return typeof(void); + default: + return typeof(object); + } + } + + private object ParseFunctionResponse(string content, Type returnType) + { + if (string.IsNullOrEmpty(content)) + return null; + + try + { + if (returnType == typeof(void)) + return null; + + // Handle primitive types + if (returnType == typeof(string)) + return content; + if (returnType == typeof(int)) + return int.Parse(content); + if (returnType == typeof(long)) + return long.Parse(content); + if (returnType == typeof(float)) + return float.Parse(content); + if (returnType == typeof(double)) + return double.Parse(content); + if (returnType == typeof(bool)) + return bool.Parse(content); + + // For complex types, deserialize from JSON + return System.Text.Json.JsonSerializer.Deserialize(content, returnType); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to parse response as {returnType.Name}: {ex.Message}", ex); + } + } + + public bool HasFunction(string Name) + { + return Info.Functions.Any(f => + f.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/KitX Script/Kscript.CSharp/Utils/DeviceCache.cs b/KitX Script/Kscript.CSharp/Utils/DeviceCache.cs new file mode 100644 index 0000000..7598a04 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Utils/DeviceCache.cs @@ -0,0 +1,193 @@ +using System.Collections.Concurrent; +using KitX.Shared.CSharp.Device; + +namespace Kscript.CSharp.Utils +{ + /// + /// 设备信息缓存管理器 + /// + public class DeviceCache : IDisposable + { + private class CacheEntry + { + public T Value { get; set; } + public DateTime Expiration { get; set; } + public DateTime CreatedAt { get; } = DateTime.UtcNow; + private int _accessCount; + + public bool IsExpired => DateTime.UtcNow > Expiration; + + public int GetAccessCount() + { + return Interlocked.CompareExchange(ref _accessCount, 0, 0); + } + + public void IncrementAccess() + { + Interlocked.Increment(ref _accessCount); + } + } + + private readonly ConcurrentDictionary>> _cache = new(); + private readonly TimeSpan _defaultExpiration; + private readonly Timer _cleanupTimer; + private volatile bool _isDisposed; + + /// + /// 初始化设备缓存 + /// + /// 默认的缓存过期时间 + /// 缓存清理的时间间隔 + public DeviceCache( + TimeSpan? defaultExpiration = null, + TimeSpan? cleanupInterval = null) + { + _defaultExpiration = defaultExpiration ?? TimeSpan.FromMinutes(1); + var interval = cleanupInterval ?? TimeSpan.FromMinutes(5); + + _cleanupTimer = new Timer( + CleanupCallback, + null, + interval, + interval + ); + } + + /// + /// 设置缓存项 + /// + public void Set(string key, IEnumerable value, TimeSpan? expiration = null) + { + var entry = new CacheEntry> + { + Value = value, + Expiration = DateTime.UtcNow.Add(expiration ?? _defaultExpiration) + }; + + _cache.AddOrUpdate(key, _ => entry, (_, __) => entry); + } + + /// + /// 尝试获取缓存项 + /// + public IEnumerable? Get(string key) + { + var entry = _cache.GetOrAdd(key, _ => null); + if (entry == null || entry.IsExpired) + { + _cache.TryRemove(key, out _); + return null; + } + + entry.IncrementAccess(); + return entry.Value; + } + + /// + /// 检查缓存项是否存在且有效 + /// + public bool IsValid(string key) + { + var entry = _cache.GetOrAdd(key, _ => null); + return entry != null && !entry.IsExpired; + } + + /// + /// 获取缓存项,如果不存在或已过期则使用提供的函数获取新值 + /// + public async Task> GetOrAdd( + string key, + Func>> valueFactory, + TimeSpan? expiration = null) + { + var existingValue = Get(key); + if (existingValue != null) + return existingValue; + + var newValue = await valueFactory(); + Set(key, newValue, expiration); + return newValue; + } + + /// + /// 移除缓存项 + /// + public bool Remove(string key) + { + return _cache.TryRemove(key, out _); + } + + /// + /// 清除所有缓存 + /// + public void Clear() + { + _cache.Clear(); + } + + /// + /// 获取缓存统计信息 + /// + public CacheStatistics GetStatistics() + { + var now = DateTime.UtcNow; + var entries = _cache.ToArray(); + + return new CacheStatistics + { + TotalItems = entries.Length, + ExpiredItems = entries.Count(x => x.Value.IsExpired), + TotalAccesses = entries.Sum(x => x.Value.GetAccessCount()), + AverageAccessesPerItem = entries.Length > 0 + ? entries.Average(x => x.Value.GetAccessCount()) + : 0, + OldestItemAge = entries.Length > 0 + ? (now - entries.Min(x => x.Value.CreatedAt)) + : TimeSpan.Zero, + AverageItemAge = entries.Length > 0 + ? TimeSpan.FromTicks((long)entries.Average(x => + (now - x.Value.CreatedAt).Ticks)) + : TimeSpan.Zero + }; + } + + private void CleanupCallback(object? state) + { + if (_isDisposed) + return; + + var expiredKeys = _cache + .Where(kvp => kvp.Value.IsExpired) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var key in expiredKeys) + { + _cache.TryRemove(key, out _); + } + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + _cleanupTimer.Dispose(); + Clear(); + } + } + + /// + /// 缓存统计信息 + /// + public class CacheStatistics + { + public int TotalItems { get; set; } + public int ExpiredItems { get; set; } + public int TotalAccesses { get; set; } + public double AverageAccessesPerItem { get; set; } + public TimeSpan OldestItemAge { get; set; } + public TimeSpan AverageItemAge { get; set; } + } +} \ No newline at end of file diff --git a/KitX Script/Kscript.CSharp/Utils/DeviceRequestBuilder.cs b/KitX Script/Kscript.CSharp/Utils/DeviceRequestBuilder.cs new file mode 100644 index 0000000..8ffa998 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Utils/DeviceRequestBuilder.cs @@ -0,0 +1,141 @@ +using System.Text.Json; +using KitX.Shared.CSharp.WebCommand; +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Utils +{ + /// + /// 设备请求构建器 + /// + public class DeviceRequestBuilder + { + private readonly Connector _connector; + private Command _command; + private Request _request; + + public DeviceRequestBuilder(Connector connector) + { + _connector = connector; + _command = new Command(); + _request = new Request + { + Type = RequestTypes.Command, + Version = RequestVersions.V1 + }; + } + + /// + /// 设置请求的函数名和参数 + /// + public DeviceRequestBuilder WithFunction(string name, params object[] args) + { + _command.FunctionName = name; + _command.FunctionArgs = args.Select(a => new Parameter + { + Value = JsonSerializer.Serialize(a), + Type = a.GetType().Name + }).ToList(); + return this; + } + + /// + /// 设置请求的目标设备 + /// + public DeviceRequestBuilder WithTarget(DeviceLocator? target) + { + _request.Target = target; + return this; + } + + /// + /// 设置请求的目标设备信息 + /// + public DeviceRequestBuilder WithTargetDevice(DeviceInfo device) + { + return WithTarget(device.Device); + } + + /// + /// 执行请求并处理响应 + /// + public async Task ExecuteAsync(Func> responseHandler) + { + var tcs = new TaskCompletionSource(); + + void OnResponse(Request response) + { + try + { + var result = responseHandler(response); + result.ContinueWith(t => + { + if (t.IsFaulted) + tcs.SetException(t.Exception ?? new Exception("Unknown error")); + else if (t.IsCanceled) + tcs.SetCanceled(); + else + tcs.SetResult(t.Result); + }); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + } + + _connector.Request() + .UpdateCommand(cmd => + { + cmd.FunctionName = _command.FunctionName; + cmd.FunctionArgs = _command.FunctionArgs; + return cmd; + }) + .UpdateRequest(req => + { + req.Type = _request.Type; + req.Version = _request.Version; + req.Target = _request.Target; + return req; + }) + .SetSender(OnResponse) + .Send(); + + return await tcs.Task; + } + + /// + /// 重置构建器状态 + /// + public DeviceRequestBuilder Reset() + { + _command = new Command(); + _request = new Request + { + Type = RequestTypes.Command, + Version = RequestVersions.V1 + }; + return this; + } + + /// + /// 创建请求克隆 + /// + public DeviceRequestBuilder Clone() + { + var clone = new DeviceRequestBuilder(_connector); + clone._command = new Command + { + FunctionName = _command.FunctionName, + FunctionArgs = _command.FunctionArgs?.ToList() + }; + clone._request = new Request + { + Type = _request.Type, + Version = _request.Version, + Target = _request.Target + }; + return clone; + } + } +} \ No newline at end of file diff --git a/KitX Script/Kscript.CSharp/Utils/NetworkUtils.cs b/KitX Script/Kscript.CSharp/Utils/NetworkUtils.cs new file mode 100644 index 0000000..f5c0eac --- /dev/null +++ b/KitX Script/Kscript.CSharp/Utils/NetworkUtils.cs @@ -0,0 +1,93 @@ +using System.Net.NetworkInformation; +using KitX.Shared.CSharp.Device; + +namespace Kscript.CSharp.Utils +{ + /// + /// 提供网络相关的工具方法 + /// + public static class NetworkUtils + { + /// + /// 检查给定的设备是否是本地设备 + /// + /// 要检查的设备信息 + /// 如果是本地设备返回true,否则返回false + public static bool IsLocalDevice(DeviceInfo device) + { + if (device.Device.IPv4 == null) + return false; + + // 获取本机所有IP地址 + var localIPs = GetLocalIPAddresses(); + + // 检查设备IP是否与本机IP匹配 + return localIPs.Contains(device.Device.IPv4) || + IsLocalNetworkAddress(device.Device.IPv4); + } + + /// + /// 获取本机所有网络接口的IP地址 + /// + private static HashSet GetLocalIPAddresses() + { + return new HashSet( + NetworkInterface.GetAllNetworkInterfaces() + .Where(n => n.OperationalStatus == OperationalStatus.Up) + .SelectMany(n => n.GetIPProperties().UnicastAddresses) + .Select(a => a.Address.ToString()) + ); + } + + /// + /// 检查给定的IP地址是否为本地网络地址 + /// + private static bool IsLocalNetworkAddress(string ip) + { + return ip.StartsWith("127.") || // Loopback + ip.StartsWith("169.254.") || // Link-local + ip.StartsWith("192.168.") || // 私有网络 + ip.StartsWith("10.") || // 私有网络 + ip.StartsWith("172.") || // 私有网络 + ip.Equals("::1"); // IPv6 loopback + } + + /// + /// 检查给定的IP地址是否为私有网络地址 + /// + public static bool IsPrivateNetworkAddress(string ip) + { + return ip.StartsWith("192.168.") || // Class C private network + ip.StartsWith("10.") || // Class A private network + (ip.StartsWith("172.") && // Class B private network + TryGetSecondOctet(ip, out var secondOctet) && + secondOctet >= 16 && secondOctet <= 31); + } + + private static bool TryGetSecondOctet(string ip, out int octet) + { + octet = 0; + var parts = ip.Split('.'); + if (parts.Length < 2) return false; + return int.TryParse(parts[1], out octet); + } + + /// + /// 判断两个IP地址是否在同一子网内 + /// + public static bool IsInSameSubnet(string ip1, string ip2, string subnetMask) + { + var ip1Parts = ip1.Split('.').Select(byte.Parse).ToArray(); + var ip2Parts = ip2.Split('.').Select(byte.Parse).ToArray(); + var maskParts = subnetMask.Split('.').Select(byte.Parse).ToArray(); + + for (int i = 0; i < 4; i++) + { + if ((ip1Parts[i] & maskParts[i]) != (ip2Parts[i] & maskParts[i])) + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/KitX Script/Kscript.CSharp/Utils/ResponseHandler.cs b/KitX Script/Kscript.CSharp/Utils/ResponseHandler.cs new file mode 100644 index 0000000..bba69b5 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Utils/ResponseHandler.cs @@ -0,0 +1,124 @@ +using System.Text.Json; +using KitX.Shared.CSharp.WebCommand; + +namespace Kscript.CSharp.Utils +{ + /// + /// 统一处理网络响应的工具类 + /// + public static class ResponseHandler + { + /// + /// 处理网络响应并返回指定类型的结果 + /// + /// 响应结果的类型 + /// 网络响应 + /// 处理响应内容的函数 + /// 处理命令响应的函数 + /// 处理后的结果 + public static async Task HandleResponse( + Request response, + Func contentHandler, + Func commandHandler) + { + return await Task.Run(() => + { + try + { + T? result = default; + response.Match( + response.GetContent(content => + { + result = contentHandler(content); + return content; + }), + matchCommand: command => + { + result = commandHandler(command); + } + ); + return result; + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to handle response: {ex.Message}", ex); + } + }); + } + + /// + /// 处理JSON响应并反序列化为指定类型 + /// + public static async Task HandleJsonResponse( + Request response, + JsonSerializerOptions? options = null) + { + return await HandleResponse( + response, + content => string.IsNullOrEmpty(content) ? default : + JsonSerializer.Deserialize(content, options), + command => + { + if (command.StartsWith("Error:")) + { + var error = command.Substring("Error:".Length); + throw new InvalidOperationException($"Command error: {error}"); + } + return default; + } + ); + } + + /// + /// 处理带前缀的命令响应 + /// + public static async Task HandlePrefixedResponse( + Request response, + string prefix, + JsonSerializerOptions? options = null) + { + return await HandleResponse( + response, + content => string.IsNullOrEmpty(content) ? default : + JsonSerializer.Deserialize(content, options), + command => + { + if (command.StartsWith("Error:")) + { + var error = command.Substring("Error:".Length); + throw new InvalidOperationException($"Command error: {error}"); + } + else if (command.StartsWith(prefix)) + { + var json = command.Substring(prefix.Length); + return JsonSerializer.Deserialize(json, options); + } + return default; + } + ); + } + + /// + /// 处理简单的字符串响应 + /// + public static async Task HandleStringResponse( + Request response, + bool throwOnError = true) + { + return await HandleResponse( + response, + content => content ?? string.Empty, + command => + { + if (throwOnError && command.StartsWith("Error:")) + { + var error = command.Substring("Error:".Length); + throw new InvalidOperationException($"Command error: {error}"); + } + return command; + } + ); + } + } +} \ No newline at end of file diff --git a/KitX Shared/KitX.Shared.CSharp/KitX.Shared.CSharp.csproj b/KitX Shared/KitX.Shared.CSharp/KitX.Shared.CSharp.csproj index 03d1926..6246e52 100644 --- a/KitX Shared/KitX.Shared.CSharp/KitX.Shared.CSharp.csproj +++ b/KitX Shared/KitX.Shared.CSharp/KitX.Shared.CSharp.csproj @@ -37,4 +37,8 @@ + + + + diff --git a/KitX Shared/KitX.Shared.CSharp/Workflow/WorkflowInfo.cs b/KitX Shared/KitX.Shared.CSharp/Workflow/WorkflowInfo.cs new file mode 100644 index 0000000..ac3ee1e --- /dev/null +++ b/KitX Shared/KitX.Shared.CSharp/Workflow/WorkflowInfo.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace KitX.Shared.CSharp.Workflow; + +public class WorkflowInfo +{ + public string Name { get; set; } = string.Empty; + + public string Version { get; set; } = string.Empty; + + public Dictionary DisplayName { get; set; } = []; + + public string AuthorName { get; set; } = string.Empty; + + public string AuthorLink { get; set; } = string.Empty; + + public string PublisherName { get; set; } = string.Empty; + + public string PublisherLink { get; set; } = string.Empty; + + public Dictionary SimpleDescription { get; set; } = []; + + public Dictionary ComplexDescription { get; set; } = []; + + public Dictionary TotalDescriptionInMarkdown { get; set; } = []; + + public string IconInBase64 { get; set; } = string.Empty; + + public DateTime PublishDate { get; set; } + + public DateTime LastUpdateDate { get; set; } + + public bool IsMarketVersion { get; set; } + + public string OriginalFilePath { get; set; } = string.Empty; +}