diff --git a/src/Basic/Senparc.Ncf.XncfBase.Tests/Functions/FunctionHelperTests.cs b/src/Basic/Senparc.Ncf.XncfBase.Tests/Functions/FunctionHelperTests.cs index 23e8eb6ad..9fb9a866a 100644 --- a/src/Basic/Senparc.Ncf.XncfBase.Tests/Functions/FunctionHelperTests.cs +++ b/src/Basic/Senparc.Ncf.XncfBase.Tests/Functions/FunctionHelperTests.cs @@ -5,8 +5,10 @@ using Senparc.Ncf.XncfBase.FunctionRenders; using Senparc.Ncf.XncfBase.Tests; using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace Senparc.Ncf.XncfBase.Functions.Tests @@ -36,7 +38,25 @@ public override async Task LoadData(IServiceProvider serviceProvider) } } - [FunctionRender("设置参数", "设置备份间隔时间、备份文件路径等参数", typeof(Register))] + public class SetSelectionConfigFunctionAppRequest : FunctionAppRequestBase + { + [Required] + [System.ComponentModel.Description("智能体||从下拉中选择,或直接手动输入")] + [FunctionParameterUi(ParameterType.DropDownList, nameof(AgentOptions), Filterable = true, AllowCreate = true)] + public string AgentName { get; set; } + + [JsonIgnore] + public SelectionList AgentOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); + + public override Task LoadData(IServiceProvider serviceProvider) + { + AgentOptions.Items.Add(new SelectionItem("PromptCatalyzer", "PromptCatalyzer", defaultSelected: true)); + AgentOptions.Items.Add(new SelectionItem("Scorer", "Scorer")); + return Task.CompletedTask; + } + } + + [FunctionRender("设置参数", "设置备份间隔时间、备份文件路径等参数", typeof(TestModuleRegister))] public async Task SetConfig(SetConfigFunctionAppRequest request) { return await this.GetStringResponseAsync(async (response, logger) => @@ -44,6 +64,15 @@ public async Task SetConfig(SetConfigFunctionAppRequest reque return request.BackupPath; }); } + + [FunctionRender("设置智能体", "测试简单值 + 下拉元数据", typeof(TestModuleRegister))] + public async Task SetSelectionConfig(SetSelectionConfigFunctionAppRequest request) + { + return await this.GetStringResponseAsync(async (response, logger) => + { + return request.AgentName; + }); + } } @@ -64,5 +93,32 @@ public void GetFunctionParameterInfoAsyncTest() Assert.IsTrue(result.Count > 0); Console.WriteLine(result.ToJson(true)); } + + [TestMethod()] + public void GetFunctionParameterInfoWithUiAttributeTest() + { + var functionBag = Senparc.Ncf.XncfBase.Register.FunctionRenderCollection[typeof(TestModuleRegister)].Values + .First(z => z.MethodInfo.Name == nameof(TestFunctionAppService.SetSelectionConfig)); + + var result = FunctionHelper.GetFunctionParameterInfoAsync(base._serviceProvider, functionBag, true).GetAwaiter().GetResult(); + var agentName = result.First(z => z.Name == nameof(TestFunctionAppService.SetSelectionConfigFunctionAppRequest.AgentName)); + + Assert.AreEqual(ParameterType.DropDownList, agentName.ParameterType); + Assert.AreEqual("String", agentName.SystemType); + Assert.AreEqual(2, agentName.SelectionList.Items.Count); + Assert.IsTrue(agentName.Filterable); + Assert.IsTrue(agentName.AllowCreate); + } + + [TestMethod()] + public void NormalizeLegacySelectionListJsonToStringRequestTest() + { + var rawJson = "{\"AgentName\":{\"SelectedValues\":[\"PromptCatalyzer\"]}}"; + var normalizedJson = FunctionRequestParameterNormalizer.NormalizeJson(rawJson, typeof(TestFunctionAppService.SetSelectionConfigFunctionAppRequest)); + + var result = Senparc.CO2NET.Helpers.SerializerHelper.GetObject(normalizedJson, typeof(TestFunctionAppService.SetSelectionConfigFunctionAppRequest)) as TestFunctionAppService.SetSelectionConfigFunctionAppRequest; + + Assert.AreEqual("PromptCatalyzer", result.AgentName); + } } } \ No newline at end of file diff --git a/src/Basic/Senparc.Ncf.XncfBase/FunctionRenders/FunctionRequestParameterNormalizer.cs b/src/Basic/Senparc.Ncf.XncfBase/FunctionRenders/FunctionRequestParameterNormalizer.cs new file mode 100644 index 000000000..3cfb81ad5 --- /dev/null +++ b/src/Basic/Senparc.Ncf.XncfBase/FunctionRenders/FunctionRequestParameterNormalizer.cs @@ -0,0 +1,225 @@ +using Senparc.Ncf.XncfBase.Functions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Senparc.Ncf.XncfBase.FunctionRenders +{ + /// + /// Normalizes request payloads so legacy SelectionList JSON and simple string JSON + /// can both bind to FunctionRender request types. + /// + public static class FunctionRequestParameterNormalizer + { + public static string NormalizeJson(string rawJson, Type requestType) + { + if (string.IsNullOrWhiteSpace(rawJson) || requestType == null) + { + return rawJson; + } + + JsonNode rootNode; + try + { + rootNode = JsonNode.Parse(rawJson); + } + catch + { + return rawJson; + } + + if (rootNode is not JsonObject jsonObject) + { + return rawJson; + } + + var properties = requestType.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(z => z.CanWrite) + .ToList(); + + foreach (var property in properties) + { + if (!TryGetJsonProperty(jsonObject, property.Name, out var jsonKey, out var currentNode)) + { + continue; + } + + if (property.PropertyType == typeof(SelectionList)) + { + jsonObject[jsonKey] = NormalizeSelectionListNode(currentNode); + continue; + } + + var uiAttr = property.GetCustomAttribute(); + jsonObject[jsonKey] = NormalizeSimpleNode(property.PropertyType, currentNode, uiAttr); + } + + return jsonObject.ToJsonString(new JsonSerializerOptions + { + WriteIndented = false + }); + } + + private static JsonNode NormalizeSelectionListNode(JsonNode currentNode) + { + if (currentNode is JsonObject currentObject && TryGetJsonProperty(currentObject, "SelectedValues", out _, out _)) + { + return currentNode; + } + + var values = ExtractValues(currentNode, forMultiple: true); + var selectedValues = new JsonArray(values.Select(z => JsonValue.Create(z)).ToArray()); + + return new JsonObject + { + ["SelectedValues"] = selectedValues + }; + } + + private static JsonNode NormalizeSimpleNode(Type targetType, JsonNode currentNode, FunctionParameterUiAttribute uiAttr) + { + var forMultiple = uiAttr?.ParameterType == ParameterType.CheckBoxList || targetType == typeof(string[]); + var values = ExtractValues(currentNode, forMultiple); + + if (targetType == typeof(string[])) + { + return new JsonArray(values.Select(z => JsonValue.Create(z)).ToArray()); + } + + if (targetType == typeof(string)) + { + if (currentNode is JsonValue valueNode && TryGetString(valueNode, out var stringValue)) + { + return JsonValue.Create(stringValue); + } + + return JsonValue.Create(forMultiple ? string.Join("\n", values) : values.FirstOrDefault()); + } + + var firstValue = values.FirstOrDefault(); + if (firstValue == null) + { + return null; + } + + if (IsNullableType(targetType, typeof(int)) && int.TryParse(firstValue, out var intValue)) + { + return JsonValue.Create(intValue); + } + + if (IsNullableType(targetType, typeof(long)) && long.TryParse(firstValue, out var longValue)) + { + return JsonValue.Create(longValue); + } + + if (IsNullableType(targetType, typeof(bool)) && bool.TryParse(firstValue, out var boolValue)) + { + return JsonValue.Create(boolValue); + } + + return JsonValue.Create(firstValue); + } + + private static List ExtractValues(JsonNode currentNode, bool forMultiple) + { + if (currentNode == null) + { + return new List(); + } + + if (currentNode is JsonObject currentObject) + { + if (TryGetJsonProperty(currentObject, "SelectedValues", out _, out var selectedValuesNode)) + { + return ExtractValues(selectedValuesNode, true); + } + + return new List(); + } + + if (currentNode is JsonArray currentArray) + { + return currentArray.SelectMany(z => ExtractValues(z, true)) + .Where(z => !string.IsNullOrWhiteSpace(z)) + .Distinct(StringComparer.Ordinal) + .ToList(); + } + + if (currentNode is JsonValue currentValue && TryGetString(currentValue, out var stringValue)) + { + return SplitInput(stringValue, forMultiple); + } + + return new List(); + } + + private static List SplitInput(string input, bool forMultiple) + { + if (string.IsNullOrWhiteSpace(input)) + { + return new List(); + } + + if (!forMultiple) + { + return new List { input.Trim() }; + } + + return input.Split(new[] { ',', ',', ';', ';', '\n', '\r', '|' }, StringSplitOptions.RemoveEmptyEntries) + .Select(z => z.Trim()) + .Where(z => !string.IsNullOrWhiteSpace(z)) + .Distinct(StringComparer.Ordinal) + .ToList(); + } + + private static bool TryGetJsonProperty(JsonObject jsonObject, string propertyName, out string actualKey, out JsonNode currentNode) + { + actualKey = null; + currentNode = null; + + foreach (var item in jsonObject) + { + if (string.Equals(item.Key, propertyName, StringComparison.OrdinalIgnoreCase)) + { + actualKey = item.Key; + currentNode = item.Value; + return true; + } + } + + return false; + } + + private static bool TryGetString(JsonValue jsonValue, out string result) + { + result = null; + + try + { + result = jsonValue.GetValue(); + return true; + } + catch + { + try + { + result = jsonValue.ToString(); + return true; + } + catch + { + return false; + } + } + } + + private static bool IsNullableType(Type actualType, Type targetType) + { + var underlyingType = Nullable.GetUnderlyingType(actualType) ?? actualType; + return underlyingType == targetType; + } + } +} \ No newline at end of file diff --git a/src/Basic/Senparc.Ncf.XncfBase/Functions/FunctionHelper.cs b/src/Basic/Senparc.Ncf.XncfBase/Functions/FunctionHelper.cs index d6f8ae4ea..cbbf06358 100644 --- a/src/Basic/Senparc.Ncf.XncfBase/Functions/FunctionHelper.cs +++ b/src/Basic/Senparc.Ncf.XncfBase/Functions/FunctionHelper.cs @@ -12,6 +12,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text.Json.Serialization; using System.Text; using System.Threading.Tasks; @@ -69,10 +70,33 @@ public static async Task> GetFunctionParameterInfoAs List result = new List(); foreach (var prop in props) { + if (ShouldIgnoreProperty(prop)) + { + continue; + } + SelectionList selectionList = null; + var filterable = false; + var allowCreate = false; parameterType = ParameterType.Text;//默认为文本内容 - //判断是否存在选项 - if (prop.PropertyType == typeof(SelectionList)) + + var functionParameterUi = prop.GetCustomAttribute(); + if (functionParameterUi != null) + { + parameterType = functionParameterUi.ParameterType; + filterable = functionParameterUi.Filterable; + allowCreate = functionParameterUi.AllowCreate; + + if (!functionParameterUi.SelectionListPropertyName.IsNullOrEmpty() && paraObj != null) + { + var selectionProp = functionParameterType.GetProperty(functionParameterUi.SelectionListPropertyName); + if (selectionProp?.PropertyType == typeof(SelectionList)) + { + selectionList = selectionProp.GetValue(paraObj, null) as SelectionList; + } + } + } + else if (prop.PropertyType == typeof(SelectionList)) { var selections = prop.GetValue(paraObj, null) as SelectionList; switch (selections.SelectionType) @@ -130,12 +154,23 @@ public static async Task> GetFunctionParameterInfoAs } var functionParamInfo = new FunctionParameterInfo(name, title, description, isRequired, systemType, parameterType, - selectionList ?? new SelectionList(SelectionType.Unknown), value, maxLength); + selectionList ?? new SelectionList(SelectionType.Unknown), value, maxLength, + filterable, allowCreate); result.Add(functionParamInfo); } return result; } + private static bool ShouldIgnoreProperty(PropertyInfo propertyInfo) + { + if (propertyInfo.GetCustomAttribute() != null) + { + return true; + } + + return propertyInfo.CustomAttributes.Any(z => z.AttributeType.FullName == "Newtonsoft.Json.JsonIgnoreAttribute"); + } + /// /// 给 SelectionList 对象添加当前解决方案中的 XNCF 项目 /// (扫描当前解决方案包含的所有领域项目) diff --git a/src/Basic/Senparc.Ncf.XncfBase/Functions/Parameters/FunctionParameterInfo.cs b/src/Basic/Senparc.Ncf.XncfBase/Functions/Parameters/FunctionParameterInfo.cs index 9dc8a7dde..83d96634b 100644 --- a/src/Basic/Senparc.Ncf.XncfBase/Functions/Parameters/FunctionParameterInfo.cs +++ b/src/Basic/Senparc.Ncf.XncfBase/Functions/Parameters/FunctionParameterInfo.cs @@ -61,12 +61,23 @@ public class FunctionParameterInfo /// public SelectionList SelectionList { get; set; } + /// + /// 下拉框是否允许搜索 + /// + public bool Filterable { get; set; } + + /// + /// 下拉框是否允许创建自定义值 + /// + public bool AllowCreate { get; set; } + public FunctionParameterInfo() { } public FunctionParameterInfo(string name, string title, string description, - bool isRequired, string systemType, ParameterType parameterType, SelectionList selectionList, object value, int maxLength) + bool isRequired, string systemType, ParameterType parameterType, SelectionList selectionList, object value, int maxLength, + bool filterable = false, bool allowCreate = false) { Name = name; Title = title; @@ -77,6 +88,8 @@ public FunctionParameterInfo(string name, string title, string description, ParameterType = parameterType; Value = value; MaxLength = maxLength; + Filterable = filterable; + AllowCreate = allowCreate; } } } diff --git a/src/Basic/Senparc.Ncf.XncfBase/Functions/Parameters/FunctionParameterUiAttribute.cs b/src/Basic/Senparc.Ncf.XncfBase/Functions/Parameters/FunctionParameterUiAttribute.cs new file mode 100644 index 000000000..51e019dd7 --- /dev/null +++ b/src/Basic/Senparc.Ncf.XncfBase/Functions/Parameters/FunctionParameterUiAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Senparc.Ncf.XncfBase +{ + /// + /// Declares how a simple request property should be rendered in the Function UI. + /// + [AttributeUsage(AttributeTargets.Property)] + public class FunctionParameterUiAttribute : Attribute + { + public ParameterType ParameterType { get; } + + public string SelectionListPropertyName { get; } + + public bool Filterable { get; set; } + + public bool AllowCreate { get; set; } + + public FunctionParameterUiAttribute(ParameterType parameterType, string selectionListPropertyName = null) + { + ParameterType = parameterType; + SelectionListPropertyName = selectionListPropertyName; + } + } +} \ No newline at end of file diff --git a/src/Basic/Senparc.Ncf.XncfBase/Senparc.Ncf.XncfBase.csproj b/src/Basic/Senparc.Ncf.XncfBase/Senparc.Ncf.XncfBase.csproj index ab484f0d9..d2ff37ef6 100644 --- a/src/Basic/Senparc.Ncf.XncfBase/Senparc.Ncf.XncfBase.csproj +++ b/src/Basic/Senparc.Ncf.XncfBase/Senparc.Ncf.XncfBase.csproj @@ -2,7 +2,7 @@ net8.0 - 0.22.17-preview.1 + 0.22.18-preview.1 10.0 Senparc.Ncf.XncfBase Senparc.Ncf.XncfBase @@ -55,6 +55,7 @@ [2025-06-27] v0.22.13-preview.1 Add dynamic XNCF Template dynamic generator [2025-11-01] update basic libraries, including Senparc.AI [2026-01-07] v0.22.16-preview.1 Add rootDir parameter to FunctionHelper.LoadXncfProjects() method + [2026-04-24] Simplify FunctionRender request parameters and keep SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease diff --git a/src/Extensions/Senparc.Xncf.AIAgentsHub/OHS/Local/AppService/MyFuctionAppService.cs b/src/Extensions/Senparc.Xncf.AIAgentsHub/OHS/Local/AppService/MyFuctionAppService.cs index 8a984a5df..cece1506f 100644 --- a/src/Extensions/Senparc.Xncf.AIAgentsHub/OHS/Local/AppService/MyFuctionAppService.cs +++ b/src/Extensions/Senparc.Xncf.AIAgentsHub/OHS/Local/AppService/MyFuctionAppService.cs @@ -37,7 +37,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques */ double calcResult = request.Number1; - var theOperator = request.Operator.SelectedValues.FirstOrDefault(); + var theOperator = request.Operator; switch (theOperator) { case "+": @@ -68,7 +68,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques Action raisePower = power => { - if (request.Power.SelectedValues.Contains(power.ToString())) + if ((request.Power ?? Array.Empty()).Contains(power.ToString())) { var oldValue = calcResult; calcResult = Math.Pow(calcResult, power); diff --git a/src/Extensions/Senparc.Xncf.AIAgentsHub/OHS/Local/PL/MyFunctionRequest.cs b/src/Extensions/Senparc.Xncf.AIAgentsHub/OHS/Local/PL/MyFunctionRequest.cs index 19a2b0e7b..635d25fa2 100644 --- a/src/Extensions/Senparc.Xncf.AIAgentsHub/OHS/Local/PL/MyFunctionRequest.cs +++ b/src/Extensions/Senparc.Xncf.AIAgentsHub/OHS/Local/PL/MyFunctionRequest.cs @@ -1,7 +1,9 @@ using Senparc.Ncf.XncfBase.FunctionRenders; using Senparc.Ncf.XncfBase.Functions; +using Senparc.Ncf.XncfBase; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace Senparc.Xncf.AIAgentsHub.OHS.Local.PL { @@ -22,7 +24,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase public int Number2 { get; set; } [Description("运算符||")]//下拉列表 - public SelectionList Operator { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { + [FunctionParameterUi(ParameterType.DropDownList, nameof(OperatorOptions))] + public string Operator { get; set; } + + [JsonIgnore] + public SelectionList OperatorOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { new SelectionItem("+","加法","数字1 + 数字2",false), new SelectionItem("-","减法","数字1 - 数字2",true), new SelectionItem("×","乘法","数字1 × 数字2",false), @@ -30,7 +36,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase }); [Description("计算平方||")]//多选框 - public SelectionList Power { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(PowerOptions))] + public string[] Power { get; set; } + + [JsonIgnore] + public SelectionList PowerOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("2","平方","计算上述结果之后再计算平方",false), new SelectionItem("3","三次方","计算上述结果之后再计算三次方",false) }); diff --git a/src/Extensions/Senparc.Xncf.AIAgentsHub/Senparc.Xncf.AIAgentsHub.csproj b/src/Extensions/Senparc.Xncf.AIAgentsHub/Senparc.Xncf.AIAgentsHub.csproj index 8235fd508..338bfbadc 100644 --- a/src/Extensions/Senparc.Xncf.AIAgentsHub/Senparc.Xncf.AIAgentsHub.csproj +++ b/src/Extensions/Senparc.Xncf.AIAgentsHub/Senparc.Xncf.AIAgentsHub.csproj @@ -1,7 +1,7 @@ net8.0 - 0.11.12-preview.1 + 0.11.13-preview.1 Senparc.Xncf.AIAgentsHub Senparc.Xncf.AIAgentsHub true @@ -49,6 +49,7 @@ [2025-06-20] v0.11.9-preview1 Add MCP functions in XncfRegisterBase class [2025-11-01] update basic libraries, including Senparc.AI + [2026-04-24] Simplify FunctionRender request parameters and keep SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease diff --git a/src/Extensions/Senparc.Xncf.AIKernel/OHS/Local/PL/AIModelStudioRequest.cs b/src/Extensions/Senparc.Xncf.AIKernel/OHS/Local/PL/AIModelStudioRequest.cs index 6581b5a7a..396d4eab4 100644 --- a/src/Extensions/Senparc.Xncf.AIKernel/OHS/Local/PL/AIModelStudioRequest.cs +++ b/src/Extensions/Senparc.Xncf.AIKernel/OHS/Local/PL/AIModelStudioRequest.cs @@ -7,10 +7,12 @@ using Senparc.AI.Interfaces; using Senparc.AI.Kernel; using Senparc.CO2NET.Trace; +using Senparc.Ncf.XncfBase; using Senparc.Ncf.XncfBase.FunctionRenders; using Senparc.Ncf.XncfBase.Functions; using Senparc.Xncf.AIKernel.Domain.Models.DatabaseModel.Dto; using Senparc.Xncf.AIKernel.Domain.Services; +using System.Text.Json.Serialization; namespace Senparc.Xncf.AIKernel.OHS.Local.PL { @@ -18,7 +20,11 @@ public class AIModelStudioRequest_RunModelAsync : FunctionAppRequestBase { [Required] [Description("选择模型||")]//下拉列表 - public SelectionList Model { get; set; } = new SelectionList(SelectionType.CheckBoxList, new List()); + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(ModelOptions))] + public string[] Model { get; set; } + + [JsonIgnore] + public SelectionList ModelOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new List()); //TODO: 更多 AI 参数 @@ -60,7 +66,7 @@ public override async Task LoadData(IServiceProvider serviceProvider) { var value = z.ModelAlias; var text = z.DisplayText; - Model.Items.Add(new SelectionItem(value, text, $"来自:{z.From}", false) { BindData = z.SenparcAiSetting }); + ModelOptions.Items.Add(new SelectionItem(value, text, $"来自:{z.From}", false) { BindData = z.SenparcAiSetting }); }); await base.LoadData(serviceProvider); diff --git a/src/Extensions/Senparc.Xncf.AIKernel/OHS/Local/PL/MyFunctionRequest.cs b/src/Extensions/Senparc.Xncf.AIKernel/OHS/Local/PL/MyFunctionRequest.cs index 1737c90d1..3e3acabe7 100644 --- a/src/Extensions/Senparc.Xncf.AIKernel/OHS/Local/PL/MyFunctionRequest.cs +++ b/src/Extensions/Senparc.Xncf.AIKernel/OHS/Local/PL/MyFunctionRequest.cs @@ -1,7 +1,9 @@ using Senparc.Ncf.XncfBase.FunctionRenders; +using Senparc.Ncf.XncfBase; using Senparc.Ncf.XncfBase.Functions; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace Senparc.Xncf.AIKernel.OHS.Local.PL { @@ -22,7 +24,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase public int Number2 { get; set; } [Description("运算符||")]//下拉列表 - public SelectionList Operator { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { + [FunctionParameterUi(ParameterType.DropDownList, nameof(OperatorOptions))] + public string Operator { get; set; } + + [JsonIgnore] + public SelectionList OperatorOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { new SelectionItem("+","加法","数字1 + 数字2",false), new SelectionItem("-","减法","数字1 - 数字2",true), new SelectionItem("×","乘法","数字1 × 数字2",false), @@ -30,7 +36,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase }); [Description("计算平方||")]//多选框 - public SelectionList Power { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(PowerOptions))] + public string[] Power { get; set; } + + [JsonIgnore] + public SelectionList PowerOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("2","平方","计算上述结果之后再计算平方",false), new SelectionItem("3","三次方","计算上述结果之后再计算三次方",false) }); diff --git a/src/Extensions/Senparc.Xncf.AIKernel/Senparc.Xncf.AIKernel.csproj b/src/Extensions/Senparc.Xncf.AIKernel/Senparc.Xncf.AIKernel.csproj index c19b84da9..ee4e41332 100644 --- a/src/Extensions/Senparc.Xncf.AIKernel/Senparc.Xncf.AIKernel.csproj +++ b/src/Extensions/Senparc.Xncf.AIKernel/Senparc.Xncf.AIKernel.csproj @@ -1,7 +1,7 @@ net8.0 - 0.12.24-preview.1 + 0.12.25-preview.1 Senparc.Xncf.AIKernel Senparc.Xncf.AIKernel true @@ -54,6 +54,7 @@ [2025-05-18] v0.12.18-preview1 Update Installation [2025-06-20] v0.12.20-preview Add MCP functions in XncfRegisterBase class [2026-03-24] v0.12.24-preview.1 update Senparc.AI.Kernel to v0.28.0 + [2026-04-24] Simplify FunctionRender request parameters and keep SelectionList UI metadata compatibility icon.jpg https://github.com/NeuCharFramework/NcfPackageSources diff --git a/src/Extensions/Senparc.Xncf.AgentsManager/Application/AppService/AgentTemplateAppService.cs b/src/Extensions/Senparc.Xncf.AgentsManager/Application/AppService/AgentTemplateAppService.cs index 95c097360..6b28b124a 100644 --- a/src/Extensions/Senparc.Xncf.AgentsManager/Application/AppService/AgentTemplateAppService.cs +++ b/src/Extensions/Senparc.Xncf.AgentsManager/Application/AppService/AgentTemplateAppService.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http.Timeouts; +using Microsoft.AspNetCore.Http.Timeouts; using Microsoft.AspNetCore.Mvc; using ModelContextProtocol.Client; using Senparc.CO2NET; @@ -22,6 +22,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Senparc.CO2NET.Extensions; namespace Senparc.Xncf.AgentsManager.OHS.Local.AppService @@ -43,10 +44,11 @@ public AgentTemplateAppService(IServiceProvider serviceProvider, AgentsTemplateS [FunctionRender("Agent 模板管理", "Agent 模板管理", typeof(Register))] public async Task AgentTemplateManage(AgentTemplate_ManageRequest request) { + Console.Write(request.ToJson(true)); return await this.GetStringResponseAsync(async (response, logger) => { SenparcAI_GetByVersionResponse promptResult; - var promptCode = request.GetySystemMessagePromptCode(); + var promptCode = await NormalizePromptCodeAsync(request.GetSystemMessagePromptCode()); try { @@ -63,7 +65,7 @@ public async Task AgentTemplateManage(AgentTemplate_ManageReq var agentTemplateDto = new AgentTemplateDto(request.Name, promptCode, true, request.Description, promptCode, - Enum.Parse(request.HookRobotType.SelectedValues.FirstOrDefault()), request.HookRobotParameter, request.FunctionCallNames); + Enum.Parse(request.HookRobotType), request.HookRobotParameter, request.FunctionCallNames); await this._agentsTemplateService.UpdateAgentTemplateAsync(request.Id, agentTemplateDto); @@ -74,6 +76,145 @@ public async Task AgentTemplateManage(AgentTemplate_ManageReq }); } +//[ApiBind] + [FunctionRender("从 PromptCode 快速创建智能体", "根据 PromptCode 快速创建智能体。支持靶场级别(如:RangeName)、靶道级别(如:RangeName-T1)、完整定位(如:RangeName-T1-A1)", typeof(Register))] + public async Task CreateAgentFromPromptCode(AgentTemplate_CreateFromPromptCodeRequest request) + { + return await this.GetStringResponseAsync(async (response, logger) => + { + try{ + Console.Write(request.ToJson(true)); + var promptCode = request.GetPromptCode();//await NormalizePromptCodeAsync(request.GetPromptCode()); + + if (string.IsNullOrEmpty(promptCode)) + { + return "请选择或手动输入 PromptCode"; + } + + if (string.IsNullOrEmpty(request.Name)) + { + return "请输入智能体名称"; + } + + // 检查是否已有使用该 PromptCode 前缀的智能体 + var existingAgents = await this._agentsTemplateService.GetObjectListAsync(0, 0, + z => z.PromptCode != null && z.PromptCode.StartsWith(promptCode), + z => z.Id, Ncf.Core.Enums.OrderingType.Descending); + + if (existingAgents.TotalCount > 0) + { + var existingNames = string.Join("、", existingAgents.Select(z => z.Name)); + logger.Append($"⚠️ 注意:当前 PromptCode({promptCode})已有 {existingAgents.TotalCount} 个智能体使用:{existingNames}"); + logger.Append("已继续创建新智能体。"); + } + + var agentTemplateDto = new AgentTemplateDto(request.Name, promptCode, true, + request.Description ?? "", promptCode, + HookRobotType.None, "", null, request.FunctionCallNames); + + await this._agentsTemplateService.UpdateAgentTemplateAsync(0, agentTemplateDto); + + logger.Append($"✅ 智能体「{request.Name}」创建成功!"); + logger.Append($"使用的 PromptCode:{promptCode}"); + }catch(Exception ex){ + +logger.Append($"❌ 创建智能体失败:{ex.Message}"); + } + return logger.ToString(); + }); + } + + [FunctionRender("搜索 Agent 模板并返回 ID", "根据名称或 PromptCode 搜索最匹配的 AgentTemplate,并返回可选 ID。支持多个关键词。", typeof(Register))] + public async Task FindAgentTemplate(AgentTemplate_FindByNameRequest request) + { + return await this.GetStringResponseAsync(async (response, logger) => + { + if (string.IsNullOrWhiteSpace(request.Query)) + { + return "请输入搜索词(名称、PromptCode 或关键字)"; + } + + var topN = request.TopN <= 0 ? 5 : Math.Min(request.TopN, 20); + var aliasMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["提示词优化器"] = "PromptCatalyzer", + ["优化器"] = "PromptCatalyzer" + }; + + var keywords = request.Query + .Split(new[] { ',', ',', ';', ';', '\n', '\r', '|' }, StringSplitOptions.RemoveEmptyEntries) + .Select(z => z.Trim()) + .Where(z => !string.IsNullOrWhiteSpace(z)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (keywords.Count == 0) + { + return "请输入有效搜索词"; + } + + var enabledAgents = await _agentsTemplateService.GetFullListAsync(z => z.Enable, z => z.Id, Ncf.Core.Enums.OrderingType.Descending); + + foreach (var keywordRaw in keywords) + { + var keyword = aliasMap.TryGetValue(keywordRaw, out var alias) ? alias : keywordRaw; + var exact = enabledAgents + .Where(z => string.Equals(z.Name, keyword, StringComparison.OrdinalIgnoreCase) + || string.Equals(z.PromptCode, keyword, StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(z => z.Id) + .ToList(); + + var fuzzy = enabledAgents + .Where(z => + (!string.IsNullOrWhiteSpace(z.Name) && z.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase)) + || (!string.IsNullOrWhiteSpace(z.PromptCode) && z.PromptCode.Contains(keyword, StringComparison.OrdinalIgnoreCase))) + .OrderByDescending(z => z.Id) + .ToList(); + + var candidates = exact.Count > 0 + ? exact + : fuzzy; + + logger.Append($"关键词:{keywordRaw}"); + if (candidates.Count == 0) + { + logger.Append(" 未找到可用 AgentTemplate"); + continue; + } + + foreach (var c in candidates.Take(topN)) + { + logger.Append($" ID={c.Id} | 名称={c.Name} | PromptCode={c.PromptCode}"); + } + } + + return logger.ToString(); + }); + } + + /// + /// 将靶场别称开头的 PromptCode 归一化为 RangeName 开头,避免把 Alias 存入 SystemMessage。 + /// + private async Task NormalizePromptCodeAsync(string promptCode) + { + if (string.IsNullOrWhiteSpace(promptCode)) + { + return promptCode; + } + + var normalizedPromptCode = promptCode.Trim(); + var splitIndex = normalizedPromptCode.IndexOf('-'); + var rangePrefix = splitIndex >= 0 ? normalizedPromptCode.Substring(0, splitIndex) : normalizedPromptCode; + var suffix = splitIndex >= 0 ? normalizedPromptCode.Substring(splitIndex) : string.Empty; + + var promptRange = await _promptRangeService.GetObjectAsync(z => z.RangeName == rangePrefix || z.Alias == rangePrefix); + if (promptRange == null || string.IsNullOrWhiteSpace(promptRange.RangeName)) + { + return normalizedPromptCode; + } + + return promptRange.RangeName + suffix; + } /// /// 获取 AgentTemplate 的列表 @@ -316,6 +457,32 @@ public async Task> DeleteBatch([FromBody] List ids) }); } +/// + /// 根据 PromptCode 前缀获取匹配的 AgentTemplate 列表 + /// + /// PromptCode(支持前缀匹配,如"RangeName"、"RangeName-T1"、"RangeName-T1-A1") + /// + [ApiBind] + public async Task>> GetListByPromptCode(string promptCode) + { + return await this.GetResponseAsync>(async (response, logger) => + { + if (string.IsNullOrEmpty(promptCode)) + { + return new List(); + } + + var list = await this._agentsTemplateService.GetObjectListAsync(0, 0, + z => z.PromptCode != null && z.PromptCode.StartsWith(promptCode), + z => z.Id, Ncf.Core.Enums.OrderingType.Descending); + + var result = list.Select(z => + _agentsTemplateService.Mapping(z)).ToList(); + + return result; + }); + } + /// /// 获取所有已注册的 AI Plugin 类型 /// diff --git a/src/Extensions/Senparc.Xncf.AgentsManager/Application/AppService/ChatGroupAppService.cs b/src/Extensions/Senparc.Xncf.AgentsManager/Application/AppService/ChatGroupAppService.cs index 86872bec0..22ce9f94c 100644 --- a/src/Extensions/Senparc.Xncf.AgentsManager/Application/AppService/ChatGroupAppService.cs +++ b/src/Extensions/Senparc.Xncf.AgentsManager/Application/AppService/ChatGroupAppService.cs @@ -14,7 +14,9 @@ using Senparc.Xncf.AgentsManager.OHS.Local.PL; using Senparc.Xncf.AIKernel.Domain.Models.DatabaseModel.Dto; using Senparc.Xncf.AIKernel.Domain.Services; +using Senparc.Xncf.PromptRange.Domain.Models; using Senparc.Xncf.PromptRange.Domain.Services; +using Senparc.Xncf.PromptRange.OHS.Local.PL.Request; using Senparc.Xncf.PromptRange.OHS.Local.PL.Response; using System; using System.Collections.Generic; @@ -31,6 +33,7 @@ public class ChatGroupAppService : AppServiceBase private readonly AIModelService _aIModelService; private readonly ChatTaskService _chatTaskService; private readonly PromptItemService _promptItemService; + private readonly PromptRangeService _promptRangeService; public ChatGroupAppService(IServiceProvider serviceProvider, ChatGroupService chatGroupService, @@ -38,7 +41,8 @@ public ChatGroupAppService(IServiceProvider serviceProvider, AgentsTemplateService agentsTemplateService, AIModelService aIModelService, ChatTaskService chatTaskService, - PromptItemService promptItemService) : base(serviceProvider) + PromptItemService promptItemService, + PromptRangeService promptRangeService) : base(serviceProvider) { this._chatGroupService = chatGroupService; this._chatGroupMemeberService = chatGroupMemeberService; @@ -46,6 +50,7 @@ public ChatGroupAppService(IServiceProvider serviceProvider, this._aIModelService = aIModelService; this._chatTaskService = chatTaskService; this._promptItemService = promptItemService; + this._promptRangeService = promptRangeService; } [FunctionRender("管理 ChatGroup", "管理 ChatGroup", typeof(Register))] @@ -53,36 +58,33 @@ public async Task ManageChatGroupManage(ChatGroup_ManageChatG { return await this.GetStringResponseAsync(async (response, logger) => { - //群主 - if (request.Admin.SelectedValues.Count() == 0 || !int.TryParse(request.Admin.SelectedValues.First(), out int adminId)) - { - return "必须选择一位群主,请到 AgentTemplate 中设置!"; - } + var enabledAgents = await _agentsTemplateService.GetFullListAsync(z => z.Enable, z => z.Id, Ncf.Core.Enums.OrderingType.Descending); - //对接人 - if (request.EnterAgent.SelectedValues.Count() == 0 || !int.TryParse(request.EnterAgent.SelectedValues.First(), out int enterAgentId)) - { - return "必须选择一位对接人,请到 AgentTemplate 中设置!"; - } - - //var agentsTemplateAdmin = await _agentsTemplateService.GetAgentTemplateAsync(adminId); - //var agentsTemplateEnterAgent = await _agentsTemplateService.GetAgentTemplateAsync(enterAgent); + // 固定逻辑:群主和对接人优先使用“主持人”名称中评分最高的 Agent;不存在则自动创建。 + var preferredHost = await EnsurePreferredHostAgentAsync(enabledAgents, logger); + var adminId = preferredHost.Id; + var enterAgentId = preferredHost.Id; //TODO:封装到 Service 中 ChatGroup chatGroup = null; var chatGroupDto = new ChatGroupDto(request.Name, true, ChatGroupState.Unstart, request.Description, adminId, enterAgentId); var isNew = false; - if (request.ChatGroup.IsSelected("New")) + if (string.Equals(request.ChatGroup, "New", StringComparison.OrdinalIgnoreCase)) { //新建 chatGroup = new ChatGroup(chatGroupDto); - isNew = false; + isNew = true; } else { - int.TryParse(request.ChatGroup.SelectedValues.First(), out int chatGroupId); + int.TryParse(request.ChatGroup, out int chatGroupId); chatGroup = await _chatGroupService.GetObjectAsync(z => z.Id == chatGroupId); - _chatGroupService.Mapper.Map(chatGroupDto); + if (chatGroup == null) + { + return $"未找到需要编辑的 ChatGroup,ID:{chatGroupId}"; + } + + chatGroup.Update(chatGroupDto); } await _chatGroupService.SaveObjectAsync(chatGroup); @@ -91,7 +93,30 @@ public async Task ManageChatGroupManage(ChatGroup_ManageChatG //添加成员 var memberList = new List(); - var memberIdList = request.Members.SelectedValues.Select(z => int.Parse(z)).ToList(); + var rawMemberInputs = new List(); + rawMemberInputs.AddRange((request.Members ?? Array.Empty()).Where(z => !string.IsNullOrWhiteSpace(z))); + rawMemberInputs.AddRange(SplitInputs(request.MemberNamesOrIds)); + + var memberIdList = new List(); + foreach (var memberInput in rawMemberInputs.Distinct(StringComparer.OrdinalIgnoreCase)) + { + var memberResult = ResolveAgentId(enabledAgents, memberInput); + if (!memberResult.Success) + { + return $"群成员选择失败:{memberResult.ErrorMessage}"; + } + + if (!memberIdList.Contains(memberResult.AgentId)) + { + memberIdList.Add(memberResult.AgentId); + } + } + + if (memberIdList.Count == 0) + { + return "请至少提供一个群成员(支持 ID 或名称)"; + } + //合并“对接人”为成员 if (!memberIdList.Contains(chatGroupDto.EnterAgentTemplateId)) { @@ -113,44 +138,221 @@ public async Task ManageChatGroupManage(ChatGroup_ManageChatG }); } + private async Task EnsurePreferredHostAgentAsync(List enabledAgents, AppServiceLogger logger) + { + var hostCandidates = enabledAgents + .Where(z => !string.IsNullOrWhiteSpace(z.Name) + && z.Name.Contains("主持人", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (hostCandidates.Count > 0) + { + var scored = new List<(AgentTemplate Agent, float Score)>(); + foreach (var candidate in hostCandidates) + { + var score = await GetAgentScoreByPromptCodeAsync(candidate.PromptCode); + scored.Add((candidate, score)); + } + + var selected = scored + .OrderByDescending(z => z.Score) + .ThenByDescending(z => z.Agent.Id) + .First().Agent; + + logger.Append($"已自动选择主持人 Agent:{selected.Name}(ID:{selected.Id})"); + return selected; + } + + var promptRange = await _promptRangeService.AddAsync("智能主持人"); + + var chatModel = await _aIModelService.GetObjectAsync( + z => z.Show && z.ConfigModelType == Senparc.Xncf.AIKernel.Domain.Models.ConfigModelType.Chat) + ?? await _aIModelService.GetObjectAsync( + z => z.ConfigModelType == Senparc.Xncf.AIKernel.Domain.Models.ConfigModelType.Chat); + + if (chatModel == null) + { + throw new NcfExceptionBase("未找到可用的 Chat 类型 AI 模型,无法自动创建主持人 Agent"); + } + + var promptItem = await _promptItemService.AddPromptItemAsync(new PromptItem_AddRequest + { + RangeId = promptRange.Id, + ModelId = chatModel.Id, + Content = "你是一个多智能体小组的主持人(群主)。职责:维护讨论秩序、拆解任务、分配发言顺序、在信息不足时追问澄清、在出现分歧时推动收敛,并输出清晰的阶段结论。你应保持中立、简洁、可执行,不直接替代成员完成专业内容。", + IsTopTactic = true, + IsNewTactic = false, + IsNewSubTactic = false, + IsNewAiming = false, + NumsOfResults = 0, + MaxToken = 3000, + Temperature = 0.4f, + TopP = 0.8f, + FrequencyPenalty = 0, + PresencePenalty = 0, + StopSequences = null, + IsDraft = true, + Note = "自动主持人", + ExpectedResultsJson = string.Empty, + Prefix = string.Empty, + Suffix = string.Empty, + VariableDictJson = string.Empty + }); + + var hostAgent = new AgentTemplate( + name: "智能主持人", + systemMessage: promptItem.FullVersion, + enable: true, + description: "系统自动创建的主持人 Agent(用于群主与对接人)", + promptCode: promptItem.FullVersion, + hookRobotType: HookRobotType.None, + hookRobotParameter: string.Empty, + avastar: null, + functionCallNames: null, + mcpEndpoints: null); + + await _agentsTemplateService.SaveObjectAsync(hostAgent); + + logger.Append($"未找到主持人 Agent,已自动创建:{hostAgent.Name}(ID:{hostAgent.Id}),PromptCode:{hostAgent.PromptCode}"); + return hostAgent; + } + + private async Task GetAgentScoreByPromptCodeAsync(string promptCode) + { + if (string.IsNullOrWhiteSpace(promptCode)) + { + return -1; + } + + try + { + var promptItem = await _promptItemService.GetBestPromptAsync(promptCode, true); + return promptItem == null ? -1 : (float)promptItem.EvalAvgScore; + } + catch + { + return -1; + } + } + + private static readonly Dictionary AgentNameAliasMap = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["提示词优化器"] = "PromptCatalyzer", + ["优化器"] = "PromptCatalyzer" + }; + + private static List SplitInputs(string rawInput) + { + if (string.IsNullOrWhiteSpace(rawInput)) + { + return new List(); + } + + return rawInput + .Split(new[] { ',', ',', ';', ';', '\n', '\r', '|' }, StringSplitOptions.RemoveEmptyEntries) + .Select(z => z.Trim()) + .Where(z => !string.IsNullOrWhiteSpace(z)) + .ToList(); + } + + private (bool Success, int AgentId, string ErrorMessage) ResolveAgentId(List enabledAgents, string rawInput) + { + if (string.IsNullOrWhiteSpace(rawInput)) + { + return (false, 0, "输入为空,请传入 Agent ID、名称或 PromptCode"); + } + + var input = rawInput.Trim(); + + if (int.TryParse(input, out var agentId)) + { + var byId = enabledAgents.FirstOrDefault(z => z.Id == agentId); + if (byId == null) + { + return (false, 0, $"未找到启用状态的 Agent,ID:{agentId}"); + } + return (true, byId.Id, string.Empty); + } + + if (AgentNameAliasMap.TryGetValue(input, out var aliasName)) + { + input = aliasName; + } + + var exactName = enabledAgents.Where(z => string.Equals(z.Name, input, StringComparison.OrdinalIgnoreCase)).ToList(); + if (exactName.Count == 1) + { + return (true, exactName[0].Id, string.Empty); + } + + var exactPromptCode = enabledAgents.Where(z => string.Equals(z.PromptCode, input, StringComparison.OrdinalIgnoreCase)).ToList(); + if (exactPromptCode.Count == 1) + { + return (true, exactPromptCode[0].Id, string.Empty); + } + + var fuzzy = enabledAgents.Where(z => + (!string.IsNullOrWhiteSpace(z.Name) && z.Name.Contains(input, StringComparison.OrdinalIgnoreCase)) + || (!string.IsNullOrWhiteSpace(z.PromptCode) && z.PromptCode.Contains(input, StringComparison.OrdinalIgnoreCase))) + .OrderByDescending(z => z.Id) + .ToList(); + + if (fuzzy.Count == 1) + { + return (true, fuzzy[0].Id, string.Empty); + } + + if (fuzzy.Count > 1) + { + var options = string.Join(";", fuzzy.Take(5).Select(z => $"{z.Id}:{z.Name}({z.PromptCode})")); + return (false, 0, $"输入“{rawInput}”匹配到多个 Agent,请指定 ID:{options}"); + } + + return (false, 0, $"未找到匹配的 Agent:{rawInput}"); + } + [FunctionRender("启动 ChatGroup", "启动 ChatGroup", typeof(Register))] public async Task RunChatGroup(ChatGroup_RunChatGroupRequest request) { return await this.GetStringResponseAsync(async (response, logger) => { //群主 - if (request.ChatGroups.SelectedValues.Count() == 0) + if ((request.ChatGroups?.Length ?? 0) == 0) { return "至少选择一个组!"; } - var aiModelSelected = request.AIModel.SelectedValues.FirstOrDefault(); - var aiSetting = Senparc.AI.Config.SenparcAiSetting; - if (aiModelSelected != "Default") - { - int.TryParse(aiModelSelected, out int aiModelId); - var aiModel = await _aIModelService.GetObjectAsync(z => z.Id == aiModelId); - if (aiModel == null) - { - throw new NcfExceptionBase($"当前选择的 AI 模型不存在:{aiModelSelected}"); - } + var aiModelSelected = request.AIModel; + var aiModelId = 0; - var aiModelDto = _aIModelService.Mapper.Map(aiModel); - - aiSetting = _aIModelService.BuildSenparcAiSetting(aiModelDto); + if (!string.IsNullOrWhiteSpace(aiModelSelected) + && !string.Equals(aiModelSelected, "Default", StringComparison.OrdinalIgnoreCase) + && int.TryParse(aiModelSelected, out var selectedModelId) + && selectedModelId > 0) + { + aiModelId = selectedModelId; } - List tasks = new List(); - - foreach (var chatGroupId in request.ChatGroups.SelectedValues.Select(z => int.Parse(z))) + foreach (var chatGroupId in request.ChatGroups.Select(z => int.Parse(z))) { - var task = _chatGroupService.RunChatGroup(logger, chatGroupId, request.Command, aiSetting, request.Individuation.IsSelected("1")); - tasks.Add(task); - } - - Task.WaitAll(tasks.ToArray()); + var runRequest = new ChatGroup_RunGroupRequest + { + Name = $"ChatGroup-{chatGroupId}-{DateTime.Now:yyyyMMddHHmmss}", + ChatGroupId = chatGroupId, + AiModelId = aiModelId, + PromptCommand = request.Command, + Description = "由 FunctionRender 启动", + Personality = request.Individuation, + HookPlatform = HookPlatform.None, + HookParameter = string.Empty, + ChatMaxRound = ChatGroupService.ChatMaxRound + }; + + await _chatGroupService.RunChatGroupInThread(runRequest); + } - return logger.ToString(); + return "已创建并启动任务,请到 ChatTask 列表查看执行状态。"; }); } @@ -520,7 +722,7 @@ public async Task DeleteChatGroup(ChatGroup_DeleteChatGroupRe { return await this.GetStringResponseAsync(async (response, logger) => { - if (request.ChatGroups.SelectedValues.Count() == 0) + if ((request.ChatGroups?.Length ?? 0) == 0) { return "请选择要删除的对话!"; } @@ -531,7 +733,7 @@ public async Task DeleteChatGroup(ChatGroup_DeleteChatGroupRe return "请勾选\"确认删除\"复选框来确认删除操作!"; } - var chatGroupIdList = request.ChatGroups.SelectedValues + var chatGroupIdList = request.ChatGroups .Where(z => int.TryParse(z, out _)) .Select(z => int.Parse(z)) .ToList(); diff --git a/src/Extensions/Senparc.Xncf.AgentsManager/Application/DTOs/AgentTemplateRequest.cs b/src/Extensions/Senparc.Xncf.AgentsManager/Application/DTOs/AgentTemplateRequest.cs index 9d3fe7406..b6446e763 100644 --- a/src/Extensions/Senparc.Xncf.AgentsManager/Application/DTOs/AgentTemplateRequest.cs +++ b/src/Extensions/Senparc.Xncf.AgentsManager/Application/DTOs/AgentTemplateRequest.cs @@ -1,8 +1,9 @@ -using log4net.Core; +using log4net.Core; using Microsoft.Extensions.DependencyInjection; using Senparc.CO2NET.Extensions; using Senparc.Ncf.Core.Enums; using Senparc.Ncf.Core.Extensions; +using Senparc.Ncf.XncfBase; using Senparc.Ncf.XncfBase.FunctionRenders; using Senparc.Ncf.XncfBase.Functions; using Senparc.Xncf.AgentsManager.Models.DatabaseModel; @@ -16,6 +17,7 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; @@ -33,12 +35,13 @@ public class AgentTemplate_ManageRequest : FunctionAppRequestBase public int Id { get; set; } [Required] - [Description("SystemMessage||SystemMessage 的 PromptRangeCode(支持自选模式)")] - public SelectionList SystemMessagePromptCodeSelection { get; set; } = new SelectionList(SelectionType.DropDownList); - - [Description("手动输入 SystemMessage||SystemMessage 的 PromptRangeCode(支持自选模式),当 SystemMessage 选择“手动输入 SystemMessage”时必须在此处输入")] + [Description("SystemMessage||SystemMessage 的 PromptRangeCode(支持搜索、下拉和手动输入)")] + [FunctionParameterUi(ParameterType.DropDownList, nameof(SystemMessagePromptCodeOptions), Filterable = true, AllowCreate = true)] public string SystemMessagePromptCode { get; set; } + [JsonIgnore] + public SelectionList SystemMessagePromptCodeOptions { get; set; } = new SelectionList(SelectionType.DropDownList); + [Description("说明||对 Agent Template 进行说明,此信息不会对模型效果产生影响")] @@ -46,7 +49,11 @@ public class AgentTemplate_ManageRequest : FunctionAppRequestBase [Required] [Description("外接平台||需要对外发布消息的平台")] - public SelectionList HookRobotType { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); + [FunctionParameterUi(ParameterType.DropDownList, nameof(HookRobotTypeOptions))] + public string HookRobotType { get; set; } + + [JsonIgnore] + public SelectionList HookRobotTypeOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); //TODO:可以选择多个通道 @@ -56,18 +63,14 @@ public class AgentTemplate_ManageRequest : FunctionAppRequestBase [Description("Function Calls||Function Calls 名称列表,多个用逗号分隔")] public string FunctionCallNames { get; set; } - public string GetySystemMessagePromptCode() + public string GetSystemMessagePromptCode() { - var selectionValue = SystemMessagePromptCodeSelection.SelectedValues.FirstOrDefault(); - if (selectionValue != "0") - { - return selectionValue; - } - else - { - return SystemMessagePromptCode; - } + return SystemMessagePromptCode?.Trim(); + } + public string GetySystemMessagePromptCode() + { + return GetSystemMessagePromptCode(); } public override async Task LoadData(IServiceProvider serviceProvider) @@ -78,12 +81,65 @@ public override async Task LoadData(IServiceProvider serviceProvider) var hookRobotTypeItems = Enum.GetValues(); foreach (var item in hookRobotTypeItems) { - HookRobotType.Items.Add(new SelectionItem(((int)item).ToString(), item.ToString(), item.ToString(), false)); + HookRobotTypeOptions.Items.Add(new SelectionItem(((int)item).ToString(), item.ToString(), item.ToString(), item == Models.DatabaseModel.HookRobotType.None)); } - await PromptRangeItemHelper.LoadPromptRangeItemSelection(serviceProvider, SystemMessagePromptCodeSelection); + await PromptRangeItemHelper.LoadPromptRangeItemSelection(serviceProvider, SystemMessagePromptCodeOptions); + } + + + } + + /// + /// 从 PromptCode 快速创建智能体的请求 + /// + public class AgentTemplate_CreateFromPromptCodeRequest : FunctionAppRequestBase + { + [Required] + [MaxLength(50)] + [Description("智能体名称||新智能体的名称")] + public string Name { get; set; } + + // [Required] + // [Description("PromptCode 作用范围||选择覆盖范围:靶场名称(Range级别):Range、靶道前缀(Tactic级别):Tactic、或完整版本号(精确定位):PromptCode,只能严格从 Range、Tactic、PromptCode 中选择")] + // public string ScopeSelection { get; set; } + + [Description("手动输入 PromptCode||手动输入 PromptCode(支持靶场名称、靶道前缀或完整版本号),当选择[手动输入 SystemMessage]时必须在此处输入")] + public string ManualPromptCode { get; set; } + + [Description("说明||对新智能体的说明(可选)")] + public string Description { get; set; } + + [Description("Function Calls||Function Calls 名称列表,多个用逗号分隔(可选)")] + public string FunctionCallNames { get; set; } + + public string GetPromptCode() + { + // if (!string.IsNullOrEmpty(ScopeSelection)) + // { + // return ScopeSelection; + // } + return ManualPromptCode; } + // public override async Task LoadData(IServiceProvider serviceProvider) + // { + // await base.LoadData(serviceProvider); + + // await PromptRangeItemHelper.LoadPromptRangeItemSelection(serviceProvider, ScopeSelection); + // } + } + + /// + /// 搜索 AgentTemplate 并返回可用 ID 的请求 + /// + public class AgentTemplate_FindByNameRequest : FunctionAppRequestBase + { + [Required] + [Description("搜索词||支持名称、PromptCode 或关键字,可输入多个,使用逗号、分号、换行分隔")] + public string Query { get; set; } + [Description("最大返回数量||每个搜索词的最大候选数,默认 5")] + public int TopN { get; set; } = 5; } } diff --git a/src/Extensions/Senparc.Xncf.AgentsManager/Application/DTOs/ChatGroupRequest.cs b/src/Extensions/Senparc.Xncf.AgentsManager/Application/DTOs/ChatGroupRequest.cs index 77a6e57fc..b0d584eae 100644 --- a/src/Extensions/Senparc.Xncf.AgentsManager/Application/DTOs/ChatGroupRequest.cs +++ b/src/Extensions/Senparc.Xncf.AgentsManager/Application/DTOs/ChatGroupRequest.cs @@ -10,18 +10,24 @@ using Senparc.Xncf.AgentsManager.Domain.Services; using Microsoft.Extensions.DependencyInjection; using Senparc.Ncf.Service; +using Senparc.Ncf.XncfBase; using Senparc.Xncf.AgentsManager.Models.DatabaseModel.Models; using Senparc.Xncf.XncfBuilder.OHS.PL; using System.Web.Mvc; using Senparc.Xncf.AgentsManager.Domain.Models.DatabaseModel; +using System.Text.Json.Serialization; namespace Senparc.Xncf.AgentsManager.OHS.Local.PL { public class ChatGroup_ManageChatGroupRequest : FunctionAppRequestBase { [Description("选择组||选择需要操作的组,或新增")] - public SelectionList ChatGroup { get; set; } = new SelectionList(SelectionType.DropDownList, new List { - new SelectionItem("New","新建组","新建",true) + [FunctionParameterUi(ParameterType.DropDownList, nameof(ChatGroupOptions))] + public string ChatGroup { get; set; } + + [JsonIgnore] + public SelectionList ChatGroupOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new List { + new SelectionItem("New","新建组","新建",true) }); [Required] @@ -29,17 +35,35 @@ public class ChatGroup_ManageChatGroupRequest : FunctionAppRequestBase [Description("群名称||群名称")] public string Name { get; set; } - [Required] - [Description("群成员||群成员")] - public SelectionList Members { get; set; } = new SelectionList(SelectionType.CheckBoxList, new List()); + [Description("群成员||群成员(可不选,改用“群成员(手动输入)”)")] + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(MembersOptions))] + public string[] Members { get; set; } - [Required] - [Description("群主||群管理员,群管理员不会被合并到“群成员”中,通常不参与显式的发言。")] - public SelectionList Admin { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); + [JsonIgnore] + public SelectionList MembersOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new List()); - [Required] - [Description("对接人||对接人,即接受命令的人,通常也是期待返回期望结果的人。对接人也会被合并到“群成员”中")] - public SelectionList EnterAgent { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); + [Description("群成员(手动输入)||可选。支持名称、ID、PromptCode,多个值可用逗号、分号、换行分隔")] + public string MemberNamesOrIds { get; set; } + + [Description("群主||优先自动使用评分最高的“主持人”Agent;手动输入仅作兼容")] + [FunctionParameterUi(ParameterType.DropDownList, nameof(AdminOptions))] + public string Admin { get; set; } + + [JsonIgnore] + public SelectionList AdminOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); + + [Description("群主(手动输入)||可选。支持名称、ID 或 PromptCode")] + public string AdminNameOrId { get; set; } + + [Description("对接人||优先自动使用评分最高的“主持人”Agent;手动输入仅作兼容")] + [FunctionParameterUi(ParameterType.DropDownList, nameof(EnterAgentOptions))] + public string EnterAgent { get; set; } + + [JsonIgnore] + public SelectionList EnterAgentOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); + + [Description("对接人(手动输入)||可选。支持名称、ID 或 PromptCode")] + public string EnterAgentNameOrId { get; set; } [MaxLength(200)] @@ -53,17 +77,17 @@ public override async Task LoadData(IServiceProvider serviceProvider) var chatGroups = await chatGroupService.GetFullListAsync(z => true, z => z.Id, Ncf.Core.Enums.OrderingType.Ascending); chatGroups.Select(z => new SelectionItem(z.Id.ToString(), z.Name, z.Description)) - .ToList().ForEach(z => ChatGroup.Items.Add(z)); + .ToList().ForEach(z => ChatGroupOptions.Items.Add(z)); //Agent var agentTemplateService = serviceProvider.GetService(); var agentsTemplates = await agentTemplateService.GetFullListAsync(z => z.Enable, z => z.Name, Ncf.Core.Enums.OrderingType.Ascending); - Members.Items = agentsTemplates.Select(z => new SelectionItem(z.Id.ToString(), z.Name, z.Description)).ToList(); - Admin.Items = agentsTemplates.Select(z => new SelectionItem(z.Id.ToString(), z.Name, z.Description)).ToList(); - EnterAgent.Items = agentsTemplates.Select(z => new SelectionItem(z.Id.ToString(), z.Name, z.Description)).ToList(); + MembersOptions.Items = agentsTemplates.Select(z => new SelectionItem(z.Id.ToString(), z.Name, z.Description)).ToList(); + AdminOptions.Items = agentsTemplates.Select(z => new SelectionItem(z.Id.ToString(), z.Name, z.Description)).ToList(); + EnterAgentOptions.Items = agentsTemplates.Select(z => new SelectionItem(z.Id.ToString(), z.Name, z.Description)).ToList(); - var admin = Admin.Items.FirstOrDefault(z => z.Text == "群主"); + var admin = AdminOptions.Items.FirstOrDefault(z => z.Text == "群主"); if (admin != null) { admin.DefaultSelected = true; @@ -76,16 +100,28 @@ public override async Task LoadData(IServiceProvider serviceProvider) public class ChatGroup_RunChatGroupRequest : FunctionAppRequestBase { [Description("选择组||选择需要运行的组")] - public SelectionList ChatGroups { get; set; } = new SelectionList(SelectionType.CheckBoxList, new List()); + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(ChatGroupsOptions))] + public string[] ChatGroups { get; set; } + + [JsonIgnore] + public SelectionList ChatGroupsOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new List()); [Description("AI 模型||请选择运行此程序的外围 AI 模型")] - public SelectionList AIModel { get; set; } = new SelectionList(SelectionType.DropDownList, new List + [FunctionParameterUi(ParameterType.DropDownList, nameof(AIModelOptions))] + public string AIModel { get; set; } + + [JsonIgnore] + public SelectionList AIModelOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new List { //new SelectionItem("Default","系统默认","通过系统默认配置的固定 AI 模型信息",true) }); [Description("个性化智能体||")] - public SelectionList Individuation { get; set; } = new SelectionList(SelectionType.CheckBoxList, new List + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(IndividuationOptions))] + public bool Individuation { get; set; } = true; + + [JsonIgnore] + public SelectionList IndividuationOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new List { new SelectionItem("1","是","采用个性化 AI 参数运行 Agent",true) }); @@ -101,10 +137,10 @@ public override async Task LoadData(IServiceProvider serviceProvider) var chatGroupService = serviceProvider.GetService>(); var chatGroups = await chatGroupService.GetFullListAsync(z => true, z => z.Id, Ncf.Core.Enums.OrderingType.Ascending); - ChatGroups.Items = chatGroups.Select(z => new SelectionItem(z.Id.ToString(), z.Name, z.Description)).ToList(); + ChatGroupsOptions.Items = chatGroups.Select(z => new SelectionItem(z.Id.ToString(), z.Name, z.Description)).ToList(); //载入 AI 模型 - await BuildXncfRequestHelper.LoadAiModelData(serviceProvider, AIModel); + await BuildXncfRequestHelper.LoadAiModelData(serviceProvider, AIModelOptions); await base.LoadData(serviceProvider); } @@ -168,7 +204,11 @@ public class ChatGroup_RunGroupRequest public class ChatGroup_DeleteChatGroupRequest : FunctionAppRequestBase { [Description("选择要删除的对话||选择需要删除的对话(包括所有消息和任务)")] - public SelectionList ChatGroups { get; set; } = new SelectionList(SelectionType.CheckBoxList, new List()); + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(ChatGroupsOptions))] + public string[] ChatGroups { get; set; } + + [JsonIgnore] + public SelectionList ChatGroupsOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new List()); [Description("确认删除||勾选此项以确认删除此对话及其所有数据")] public bool ConfirmDelete { get; set; } @@ -179,7 +219,7 @@ public override async Task LoadData(IServiceProvider serviceProvider) var chatGroupService = serviceProvider.GetService(); var chatGroups = await chatGroupService.GetFullListAsync(z => true, z => z.Id, Ncf.Core.Enums.OrderingType.Descending); - ChatGroups.Items = chatGroups.Select(z => new SelectionItem( + ChatGroupsOptions.Items = chatGroups.Select(z => new SelectionItem( z.Id.ToString(), z.Name, $"{z.Description} (创建时间: {z.AddTime:yyyy-MM-dd HH:mm})" diff --git a/src/Extensions/Senparc.Xncf.AgentsManager/Areas/Admin/Pages/AgentsManager/Index.cshtml b/src/Extensions/Senparc.Xncf.AgentsManager/Areas/Admin/Pages/AgentsManager/Index.cshtml index 4abec97c9..f3e9e7e3c 100644 --- a/src/Extensions/Senparc.Xncf.AgentsManager/Areas/Admin/Pages/AgentsManager/Index.cshtml +++ b/src/Extensions/Senparc.Xncf.AgentsManager/Areas/Admin/Pages/AgentsManager/Index.cshtml @@ -1218,6 +1218,17 @@
+
+ + + + + +
+
+ + + + + +
(); var chatGroup = await chatGroupService.GetObjectAsync(x => x.Id == groupId); + + // 默认模型继承规则:未显式指定模型时,优先继承当前群主使用的 Prompt 模型。 + if (aiModelId <= 0 && chatGroup != null) + { + var adminAgent = await agentTemplateService.GetObjectAsync(z => z.Id == chatGroup.AdminAgentTemplateId); + if (adminAgent != null && !adminAgent.PromptCode.IsNullOrEmpty()) + { + try + { + var adminPrompt = await promptItemService.GetBestPromptAsync(adminAgent.PromptCode, true); + if (adminPrompt != null && adminPrompt.ModelId > 0) + { + aiModelId = adminPrompt.ModelId; + } + } + catch + { + // 保持 aiModelId=0,继续走系统默认模型。 + } + } + } + var chatGroupDto = chatGroupService.Mapping(chatGroup); var chatTaskDto = new ChatTaskDto(request.Name, groupId, aiModelId, ChatTask_Status.Waiting, userCommand, request.Description, personality, request.HookPlatform, request.HookParameter, false, diff --git a/src/Extensions/Senparc.Xncf.AgentsManager/Senparc.Xncf.AgentsManager.csproj b/src/Extensions/Senparc.Xncf.AgentsManager/Senparc.Xncf.AgentsManager.csproj index 047c783cc..314722724 100644 --- a/src/Extensions/Senparc.Xncf.AgentsManager/Senparc.Xncf.AgentsManager.csproj +++ b/src/Extensions/Senparc.Xncf.AgentsManager/Senparc.Xncf.AgentsManager.csproj @@ -1,7 +1,7 @@ net8.0 - 0.9.18-preview.1 + 0.9.19-preview.1 Senparc.Xncf.AgentsManager Senparc.Xncf.AgentsManager true @@ -63,6 +63,7 @@ [2026-03-24] v0.9.18-preview.1 update Senparc.AI.Agents to v0.7.1 [2025-11-01] update basic libraries, including Senparc.AI + [2026-04-24] Simplify FunctionRender request parameters and keep SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease diff --git a/src/Extensions/Senparc.Xncf.AgentsManager/wwwroot/js/AgentsManager/agent-3d.js b/src/Extensions/Senparc.Xncf.AgentsManager/wwwroot/js/AgentsManager/agent-3d.js index 50265e149..52c9d9f61 100644 --- a/src/Extensions/Senparc.Xncf.AgentsManager/wwwroot/js/AgentsManager/agent-3d.js +++ b/src/Extensions/Senparc.Xncf.AgentsManager/wwwroot/js/AgentsManager/agent-3d.js @@ -15,6 +15,7 @@ const scaleDivisor = options.scaleDivisor || 26; const sizeAttenuation = typeof options.sizeAttenuation === 'boolean' ? options.sizeAttenuation : true; const maxLineLength = options.maxLineLength || 30; + const maxLines = options.maxLines || 8; const maxWorldWidth = options.maxWorldWidth || 15; const maxWorldHeight = options.maxWorldHeight || 9; const bg = options.background || 'rgba(10,20,30,0.82)'; @@ -25,13 +26,38 @@ const ctx = canvas.getContext('2d'); ctx.font = 'bold ' + fontSize + 'px sans-serif'; - const rows = String(text || '').split('\n').map(function (line) { + const rows = []; + String(text || '').split('\n').forEach(function (line) { const lineText = String(line || ''); - if (lineText.length <= maxLineLength) { - return lineText; + if (!lineText) { + rows.push(''); + return; + } + let start = 0; + while (start < lineText.length) { + rows.push(lineText.slice(start, start + maxLineLength)); + start += maxLineLength; + if (rows.length >= maxLines) { + break; + } } - return lineText.slice(0, maxLineLength - 1) + '…'; }); + + if (rows.length === 0) { + rows.push(''); + } + + if (rows.length > maxLines) { + rows.length = maxLines; + } + + if (rows.length >= maxLines) { + const last = rows[maxLines - 1] || ''; + if (last.length >= maxLineLength) { + rows[maxLines - 1] = last.slice(0, maxLineLength - 1) + '…'; + } + } + const width = Math.max.apply(null, rows.map(function (line) { return ctx.measureText(line).width; })) + padding * 2; const rowHeight = Math.ceil(fontSize * 1.45); const height = rowHeight * rows.length + padding * 2; @@ -47,8 +73,8 @@ const x = 2; const y = 2; - const w = canvas.width - 4; - const h = canvas.height - 4; + const w = width - 4; + const h = height - 4; const r = 10; ctx.beginPath(); @@ -83,8 +109,11 @@ sizeAttenuation: sizeAttenuation }); const sprite = new THREE.Sprite(material); - const worldWidth = Math.min(canvas.width / dpr / scaleDivisor, maxWorldWidth); - const worldHeight = Math.min(canvas.height / dpr / scaleDivisor, maxWorldHeight); + const rawWorldWidth = width / scaleDivisor; + const rawWorldHeight = height / scaleDivisor; + const fitRatio = Math.min(1, maxWorldWidth / rawWorldWidth, maxWorldHeight / rawWorldHeight); + const worldWidth = rawWorldWidth * fitRatio; + const worldHeight = rawWorldHeight * fitRatio; sprite.scale.set(worldWidth, worldHeight, 1); return sprite; } diff --git a/src/Extensions/Senparc.Xncf.AgentsManager/wwwroot/js/AgentsManager/index.js b/src/Extensions/Senparc.Xncf.AgentsManager/wwwroot/js/AgentsManager/index.js index 726fca63f..4a7c6002c 100644 --- a/src/Extensions/Senparc.Xncf.AgentsManager/wwwroot/js/AgentsManager/index.js +++ b/src/Extensions/Senparc.Xncf.AgentsManager/wwwroot/js/AgentsManager/index.js @@ -383,6 +383,7 @@ var app = new Vue({ functionCallInputValue: '', functionCallTags: [], // 用于编辑时临时存储标签 pluginTypes: [], // 存储所有可用的插件类型 + agentAutoAttachXncf: false, // 是否自动附加所有 XNCF 功能插件 // MCP Endpoints相关 mcpEndpointInputVisible: false, mcpEndpointNameValue: '', @@ -1913,6 +1914,7 @@ var app = new Vue({ this.functionCallTags = [] this.functionCallInputVisible = false this.functionCallInputValue = '' + this.agentAutoAttachXncf = false } }) .catch(_ => { }); @@ -3310,7 +3312,7 @@ var app = new Vue({ // 删除 Function Call 标签 handleFunctionCallClose(tag) { - const currentNames = this.agentForm.functionCallNames.split(',').filter(x => x); + const currentNames = this.getFunctionCallNamesList(); const index = currentNames.indexOf(tag); if (index > -1) { currentNames.splice(index, 1); @@ -3318,6 +3320,28 @@ var app = new Vue({ } this.functionCallTags = currentNames; }, + + // 获取当前 functionCallNames 的数组形式 + getFunctionCallNamesList() { + return this.agentForm.functionCallNames + ? this.agentForm.functionCallNames.split(',').filter(x => x) + : []; + }, + + // 自动附加所有 XNCF 功能插件 + handleAutoAttachXncfChange(val) { + if (val) { + // 开启时:将所有可用插件类型合并到 functionCallNames + const currentNames = this.getFunctionCallNamesList(); + const allNames = [...new Set([...currentNames, ...this.pluginTypes])]; + this.agentForm.functionCallNames = allNames.join(','); + } else { + // 关闭时:移除所有自动添加的插件类型(保留用户手动添加的) + const currentNames = this.getFunctionCallNamesList(); + const manualNames = currentNames.filter(name => !this.pluginTypes.includes(name)); + this.agentForm.functionCallNames = manualNames.join(','); + } + }, // 测试MCP Endpoint连接 async testMcpEndpoint(name, endpoint) { diff --git a/src/Extensions/Senparc.Xncf.ChangeNamespace/OHS/Local/NameSpaceAppService.cs b/src/Extensions/Senparc.Xncf.ChangeNamespace/OHS/Local/NameSpaceAppService.cs index cb88a616d..6e08a7e91 100644 --- a/src/Extensions/Senparc.Xncf.ChangeNamespace/OHS/Local/NameSpaceAppService.cs +++ b/src/Extensions/Senparc.Xncf.ChangeNamespace/OHS/Local/NameSpaceAppService.cs @@ -185,7 +185,7 @@ public async Task DownloadSourceCode(NameSpace_DownloadSource { return await this.GetResponseAsync(async (response, logger) => { - if (Enum.TryParse(request.Site.SelectedValues.FirstOrDefault(), out var siteType)) + if (Enum.TryParse(request.Site, out var siteType)) { switch (siteType) { diff --git a/src/Extensions/Senparc.Xncf.ChangeNamespace/OHS/PL/NameSpaceRequest.cs b/src/Extensions/Senparc.Xncf.ChangeNamespace/OHS/PL/NameSpaceRequest.cs index 3267ab862..5c7467650 100644 --- a/src/Extensions/Senparc.Xncf.ChangeNamespace/OHS/PL/NameSpaceRequest.cs +++ b/src/Extensions/Senparc.Xncf.ChangeNamespace/OHS/PL/NameSpaceRequest.cs @@ -5,6 +5,8 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Text; +using Senparc.Ncf.XncfBase.Functions.Parameters; +using System.Text.Json.Serialization; namespace Senparc.Xncf.ChangeNamespace.OHS.PL { @@ -30,7 +32,11 @@ public class NameSpace_DownloadSourceCodeRequest : FunctionAppRequestBase ///
z [Required] [Description("源码来源||目前更新最快的是 GitHub,Gitee(码云)在国内下载速度更快,但是不能确定是最新代码,下载前请注意核对最新 GitHub 上的版本。")] - public SelectionList Site { get; set; } = new SelectionList(SelectionType.DropDownList, new[] + [FunctionParameterUi(ParameterType.DropDownList, nameof(SiteOptions))] + public string Site { get; set; } + + [JsonIgnore] + public SelectionList SiteOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { new SelectionItem(Parameters_Site.GitHub.ToString(),Parameters_Site.GitHub.ToString()), new SelectionItem(Parameters_Site.Gitee.ToString(),Parameters_Site.Gitee.ToString()) diff --git a/src/Extensions/Senparc.Xncf.ChangeNamespace/Senparc.Xncf.ChangeNamespace.csproj b/src/Extensions/Senparc.Xncf.ChangeNamespace/Senparc.Xncf.ChangeNamespace.csproj index 6d5f596a0..f8474bf29 100644 --- a/src/Extensions/Senparc.Xncf.ChangeNamespace/Senparc.Xncf.ChangeNamespace.csproj +++ b/src/Extensions/Senparc.Xncf.ChangeNamespace/Senparc.Xncf.ChangeNamespace.csproj @@ -1,7 +1,7 @@ netstandard2.1 - 0.20.12-preview.1 + 0.20.13-preview.1 Senparc.Xncf.ChangeNamespace Senparc.Xncf.ChangeNamespace true @@ -28,6 +28,7 @@ [2025-06-20] v0.20.9-preview1 Add MCP functions in XncfRegisterBase class [2025-11-01] update basic libraries, including Senparc.AI + [2026-04-24] Simplify FunctionRender request parameters and keep SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease diff --git a/src/Extensions/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit/OHS/Local/AppService/DatabaseBackupAppService.cs b/src/Extensions/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit/OHS/Local/AppService/DatabaseBackupAppService.cs index d276c70d7..5ff70060c 100644 --- a/src/Extensions/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit/OHS/Local/AppService/DatabaseBackupAppService.cs +++ b/src/Extensions/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit/OHS/Local/AppService/DatabaseBackupAppService.cs @@ -41,11 +41,12 @@ public async Task Backup(DatabaseBackup_BackupRequest request try { var path = request.Path; + var options = request.Options ?? Array.Empty(); if (File.Exists(path)) { var copyPath = path + ".last.bak"; - if (request.Options.SelectedValues.Contains($"{(int)BackupDatabaseOptions.如果文件存在则不覆盖}")) + if (options.Contains($"{(int)BackupDatabaseOptions.如果文件存在则不覆盖}")) { logger.Append("检测到同名文件,停止覆盖。地址:" + copyPath); return response.Data = logger.Append("检测到同名文件,停止覆盖!"); @@ -73,7 +74,7 @@ public async Task Backup(DatabaseBackup_BackupRequest request logger.Append("执行完毕,备份结束。affectRows:" + affectRows); } - if (request.Options.SelectedValues.Contains($"{(int)BackupDatabaseOptions.校验备份成功}")) + if (options.Contains($"{(int)BackupDatabaseOptions.校验备份成功}")) { logger.Append("检查备份文件:" + path); if (File.Exists(path)) diff --git a/src/Extensions/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit/OHS/Local/PL/DatabaseBackupRequest.cs b/src/Extensions/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit/OHS/Local/PL/DatabaseBackupRequest.cs index 02e8dc101..c78b2e2b0 100644 --- a/src/Extensions/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit/OHS/Local/PL/DatabaseBackupRequest.cs +++ b/src/Extensions/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit/OHS/Local/PL/DatabaseBackupRequest.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Senparc.Ncf.Service; +using Senparc.Ncf.XncfBase; using Senparc.Ncf.XncfBase.FunctionRenders; using Senparc.Ncf.XncfBase.Functions; using System; @@ -8,6 +9,7 @@ using System.ComponentModel.DataAnnotations; using System.Text; using System.Threading.Tasks; +using System.Text.Json.Serialization; namespace Senparc.Xncf.DatabaseToolkit.OHS.Local.PL { @@ -25,7 +27,11 @@ public class DatabaseBackup_BackupRequest : FunctionAppRequestBase public string Path { get; set; } [Description("选项||备份数据库选项")] - public SelectionList Options { get; set; } = new SelectionList(SelectionType.CheckBoxList, + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(OptionsList))] + public string[] Options { get; set; } + + [JsonIgnore] + public SelectionList OptionsList { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem($"{(int)BackupDatabaseOptions.如果文件存在则不覆盖}","如果文件存在,则不覆盖","文件已存在的情况下,不会执行备份操作"), new SelectionItem($"{(int)BackupDatabaseOptions.校验备份成功}","备份完成后校验.bak文件是否更新成功","此操作只能检查文件是否被更新,无法检测文件内部内容",true), diff --git a/src/Extensions/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit.csproj b/src/Extensions/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit.csproj index f586db401..13844ffa2 100644 --- a/src/Extensions/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit.csproj +++ b/src/Extensions/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit/Senparc.Xncf.DatabaseToolkit.csproj @@ -4,7 +4,7 @@ - 0.24.13-preview.1 + 0.24.14-preview.1 Senparc.Xncf.DatabaseToolkit Senparc.Xncf.DatabaseToolkit true @@ -43,6 +43,7 @@ [2025-06-20] v0.24.10-preview1 Add MCP functions in XncfRegisterBase class [2025-11-01] update basic libraries, including Senparc.AI + [2026-04-24] Simplify FunctionRender request parameters and keep SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease diff --git a/src/Extensions/Senparc.Xncf.DynamicData/OHS/Local/AppService/MyFuctionAppService.cs b/src/Extensions/Senparc.Xncf.DynamicData/OHS/Local/AppService/MyFuctionAppService.cs index 4d09a8856..13263a826 100644 --- a/src/Extensions/Senparc.Xncf.DynamicData/OHS/Local/AppService/MyFuctionAppService.cs +++ b/src/Extensions/Senparc.Xncf.DynamicData/OHS/Local/AppService/MyFuctionAppService.cs @@ -37,7 +37,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques */ double calcResult = request.Number1; - var theOperator = request.Operator.SelectedValues.FirstOrDefault(); + var theOperator = request.Operator; switch (theOperator) { case "+": @@ -68,7 +68,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques Action raisePower = power => { - if (request.Power.SelectedValues.Contains(power.ToString())) + if ((request.Power ?? Array.Empty()).Contains(power.ToString())) { var oldValue = calcResult; calcResult = Math.Pow(calcResult, power); diff --git a/src/Extensions/Senparc.Xncf.DynamicData/OHS/Local/PL/MyFunctionRequest.cs b/src/Extensions/Senparc.Xncf.DynamicData/OHS/Local/PL/MyFunctionRequest.cs index a8202ccc3..a562a667d 100644 --- a/src/Extensions/Senparc.Xncf.DynamicData/OHS/Local/PL/MyFunctionRequest.cs +++ b/src/Extensions/Senparc.Xncf.DynamicData/OHS/Local/PL/MyFunctionRequest.cs @@ -1,7 +1,9 @@ using Senparc.Ncf.XncfBase.FunctionRenders; using Senparc.Ncf.XncfBase.Functions; +using Senparc.Ncf.XncfBase; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace Senparc.Xncf.DynamicData.OHS.Local.PL { @@ -22,7 +24,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase public int Number2 { get; set; } [Description("运算符||")]//下拉列表 - public SelectionList Operator { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { + [FunctionParameterUi(ParameterType.DropDownList, nameof(OperatorOptions))] + public string Operator { get; set; } + + [JsonIgnore] + public SelectionList OperatorOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { new SelectionItem("+","加法","数字1 + 数字2",false), new SelectionItem("-","减法","数字1 - 数字2",true), new SelectionItem("×","乘法","数字1 × 数字2",false), @@ -30,7 +36,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase }); [Description("计算平方||")]//多选框 - public SelectionList Power { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(PowerOptions))] + public string[] Power { get; set; } + + [JsonIgnore] + public SelectionList PowerOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("2","平方","计算上述结果之后再计算平方",false), new SelectionItem("3","三次方","计算上述结果之后再计算三次方",false) }); diff --git a/src/Extensions/Senparc.Xncf.DynamicData/Senparc.Xncf.DynamicData.csproj b/src/Extensions/Senparc.Xncf.DynamicData/Senparc.Xncf.DynamicData.csproj index fff95cf46..5dd6b58d6 100644 --- a/src/Extensions/Senparc.Xncf.DynamicData/Senparc.Xncf.DynamicData.csproj +++ b/src/Extensions/Senparc.Xncf.DynamicData/Senparc.Xncf.DynamicData.csproj @@ -1,7 +1,7 @@ net8.0 - 0.4.15-preview.1 + 0.4.16-preview.1 Senparc.Xncf.DynamicData Senparc.Xncf.DynamicData true @@ -38,6 +38,7 @@ [2025-06-20] v0.4.12-pre-alpha Add MCP functions in XncfRegisterBase class [2025-11-01] update basic libraries, including Senparc.AI + [2026-04-24] Simplify FunctionRender request parameters and keep SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease diff --git a/src/Extensions/Senparc.Xncf.FileManager/OHS/Local/AppService/MyFuctionAppService.cs b/src/Extensions/Senparc.Xncf.FileManager/OHS/Local/AppService/MyFuctionAppService.cs index 63d0033c9..a5e85fe61 100644 --- a/src/Extensions/Senparc.Xncf.FileManager/OHS/Local/AppService/MyFuctionAppService.cs +++ b/src/Extensions/Senparc.Xncf.FileManager/OHS/Local/AppService/MyFuctionAppService.cs @@ -37,7 +37,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques */ double calcResult = request.Number1; - var theOperator = request.Operator.SelectedValues.FirstOrDefault(); + var theOperator = request.Operator; switch (theOperator) { case "+": @@ -68,7 +68,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques Action raisePower = power => { - if (request.Power.SelectedValues.Contains(power.ToString())) + if ((request.Power ?? Array.Empty()).Contains(power.ToString())) { var oldValue = calcResult; calcResult = Math.Pow(calcResult, power); diff --git a/src/Extensions/Senparc.Xncf.FileManager/OHS/Local/PL/MyFunctionRequest.cs b/src/Extensions/Senparc.Xncf.FileManager/OHS/Local/PL/MyFunctionRequest.cs index 6268957a8..49d675c4b 100644 --- a/src/Extensions/Senparc.Xncf.FileManager/OHS/Local/PL/MyFunctionRequest.cs +++ b/src/Extensions/Senparc.Xncf.FileManager/OHS/Local/PL/MyFunctionRequest.cs @@ -1,7 +1,9 @@ using Senparc.Ncf.XncfBase.FunctionRenders; using Senparc.Ncf.XncfBase.Functions; +using Senparc.Ncf.XncfBase; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace Senparc.Xncf.FileManager.OHS.Local.PL { @@ -22,7 +24,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase public int Number2 { get; set; } [Description("运算符||")]//下拉列表 - public SelectionList Operator { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { + [FunctionParameterUi(ParameterType.DropDownList, nameof(OperatorOptions))] + public string Operator { get; set; } + + [JsonIgnore] + public SelectionList OperatorOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { new SelectionItem("+","加法","数字1 + 数字2",false), new SelectionItem("-","减法","数字1 - 数字2",true), new SelectionItem("×","乘法","数字1 × 数字2",false), @@ -30,7 +36,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase }); [Description("计算平方||")]//多选框 - public SelectionList Power { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(PowerOptions))] + public string[] Power { get; set; } + + [JsonIgnore] + public SelectionList PowerOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("2","平方","计算上述结果之后再计算平方",false), new SelectionItem("3","三次方","计算上述结果之后再计算三次方",false) }); diff --git a/src/Extensions/Senparc.Xncf.FileManager/Senparc.Xncf.FileManager.csproj b/src/Extensions/Senparc.Xncf.FileManager/Senparc.Xncf.FileManager.csproj index ccbda79b2..84a8c72d7 100644 --- a/src/Extensions/Senparc.Xncf.FileManager/Senparc.Xncf.FileManager.csproj +++ b/src/Extensions/Senparc.Xncf.FileManager/Senparc.Xncf.FileManager.csproj @@ -1,7 +1,7 @@ net8.0 - 0.1.10-preview.1 + 0.1.11-preview.1 Senparc.Xncf.FileManager Senparc.Xncf.FileManager true @@ -24,6 +24,7 @@ [2025-07-27] v0.1.6-preview.1 update Knowledge Base [2025-11-01] update basic libraries, including Senparc.AI [2026-01-07] v0.1.9-preview.1 Reset all database migrations + [2026-04-24] Simplify FunctionRender request parameters and keep SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease diff --git a/src/Extensions/Senparc.Xncf.KnowledgeBase/OHS/Local/AppService/MyFuctionAppService.cs b/src/Extensions/Senparc.Xncf.KnowledgeBase/OHS/Local/AppService/MyFuctionAppService.cs index db26a8889..26f3b4334 100644 --- a/src/Extensions/Senparc.Xncf.KnowledgeBase/OHS/Local/AppService/MyFuctionAppService.cs +++ b/src/Extensions/Senparc.Xncf.KnowledgeBase/OHS/Local/AppService/MyFuctionAppService.cs @@ -37,7 +37,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques */ double calcResult = request.Number1; - var theOperator = request.Operator.SelectedValues.FirstOrDefault(); + var theOperator = request.Operator; switch (theOperator) { case "+": @@ -68,7 +68,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques Action raisePower = power => { - if (request.Power.SelectedValues.Contains(power.ToString())) + if ((request.Power ?? Array.Empty()).Contains(power.ToString())) { var oldValue = calcResult; calcResult = Math.Pow(calcResult, power); diff --git a/src/Extensions/Senparc.Xncf.KnowledgeBase/OHS/Local/PL/MyFunctionRequest.cs b/src/Extensions/Senparc.Xncf.KnowledgeBase/OHS/Local/PL/MyFunctionRequest.cs index 798ee41f4..b0ca17ae7 100644 --- a/src/Extensions/Senparc.Xncf.KnowledgeBase/OHS/Local/PL/MyFunctionRequest.cs +++ b/src/Extensions/Senparc.Xncf.KnowledgeBase/OHS/Local/PL/MyFunctionRequest.cs @@ -1,7 +1,9 @@ using Senparc.Ncf.XncfBase.FunctionRenders; using Senparc.Ncf.XncfBase.Functions; +using Senparc.Ncf.XncfBase; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace Senparc.Xncf.KnowledgeBase.OHS.Local.PL { @@ -22,7 +24,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase public int Number2 { get; set; } [Description("运算符||")]//下拉列表 - public SelectionList Operator { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { + [FunctionParameterUi(ParameterType.DropDownList, nameof(OperatorOptions))] + public string Operator { get; set; } + + [JsonIgnore] + public SelectionList OperatorOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { new SelectionItem("+","加法","数字1 + 数字2",false), new SelectionItem("-","减法","数字1 - 数字2",true), new SelectionItem("×","乘法","数字1 × 数字2",false), @@ -30,7 +36,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase }); [Description("计算平方||")]//多选框 - public SelectionList Power { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(PowerOptions))] + public string[] Power { get; set; } + + [JsonIgnore] + public SelectionList PowerOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("2","平方","计算上述结果之后再计算平方",false), new SelectionItem("3","三次方","计算上述结果之后再计算三次方",false) }); diff --git a/src/Extensions/Senparc.Xncf.KnowledgeBase/Senparc.Xncf.KnowledgeBase.csproj b/src/Extensions/Senparc.Xncf.KnowledgeBase/Senparc.Xncf.KnowledgeBase.csproj index 57d737fcb..5edac4b2f 100644 --- a/src/Extensions/Senparc.Xncf.KnowledgeBase/Senparc.Xncf.KnowledgeBase.csproj +++ b/src/Extensions/Senparc.Xncf.KnowledgeBase/Senparc.Xncf.KnowledgeBase.csproj @@ -1,7 +1,7 @@ net8.0 - 0.1.9-preview.1 + 0.1.10-preview.1 Senparc.Xncf.KnowledgeBase Senparc.Xncf.KnowledgeBase true @@ -22,6 +22,7 @@ [2025-07-27] v0.1.5-preview1s update Knowledge Base [2025-11-01] update basic libraries, including Senparc.AI [2025-01-07] v0.1.8-preview.1 Reset all database migrations + [2026-04-24] Simplify FunctionRender request parameters and keep SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease diff --git a/src/Extensions/Senparc.Xncf.MCP/OHS/Local/AppService/MyFuctionAppService.cs b/src/Extensions/Senparc.Xncf.MCP/OHS/Local/AppService/MyFuctionAppService.cs index d512d3766..3746db47e 100644 --- a/src/Extensions/Senparc.Xncf.MCP/OHS/Local/AppService/MyFuctionAppService.cs +++ b/src/Extensions/Senparc.Xncf.MCP/OHS/Local/AppService/MyFuctionAppService.cs @@ -105,7 +105,7 @@ public async Task GetMcpResult(MyFunction_MCPCallRequest requ // 根据 MCP 服务器选择来确定端点 string endpoint; - var selectedMcpServer = request.McpServerSelection.SelectedValues.FirstOrDefault(); + var selectedMcpServer = request.McpServerSelection; if (!string.IsNullOrEmpty(selectedMcpServer) && selectedMcpServer != "Manual") { @@ -278,7 +278,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques */ double calcResult = request.Number1; - var theOperator = request.Operator.SelectedValues.FirstOrDefault(); + var theOperator = request.Operator; switch (theOperator) { case "+": @@ -309,7 +309,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques Action raisePower = power => { - if (request.Power.SelectedValues.Contains(power.ToString())) + if ((request.Power ?? Array.Empty()).Contains(power.ToString())) { var oldValue = calcResult; calcResult = Math.Pow(calcResult, power); diff --git a/src/Extensions/Senparc.Xncf.MCP/OHS/Local/PL/MyFunctionRequest.cs b/src/Extensions/Senparc.Xncf.MCP/OHS/Local/PL/MyFunctionRequest.cs index 1c46dbeac..95bb405da 100644 --- a/src/Extensions/Senparc.Xncf.MCP/OHS/Local/PL/MyFunctionRequest.cs +++ b/src/Extensions/Senparc.Xncf.MCP/OHS/Local/PL/MyFunctionRequest.cs @@ -1,4 +1,5 @@ using Senparc.Ncf.XncfBase.FunctionRenders; +using Senparc.Ncf.XncfBase; using Senparc.Ncf.XncfBase.Functions; using System.ComponentModel; using System.ComponentModel.DataAnnotations; @@ -7,14 +8,18 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using System.Linq; -using Senparc.Ncf.XncfBase; +using System.Text.Json.Serialization; namespace Senparc.Xncf.MCP.OHS.Local.PL { public class MyFunction_MCPCallRequest : FunctionAppRequestBase { [Description("MCP 服务器选择||选择已注册的 MCP 服务器,或选择 手动输入 自定义服务器地址")] - public SelectionList McpServerSelection { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); + [FunctionParameterUi(ParameterType.DropDownList, nameof(McpServerSelectionOptions))] + public string McpServerSelection { get; set; } + + [JsonIgnore] + public SelectionList McpServerSelectionOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); [Description("MCP 服务器地址||MCP 服务器地址,当选择 手动输入 时需要填写,默认为 http://localhost:5000/mcp-senparc-xncf-mcp/sse")] public string Endpoint { get; set; } @@ -26,7 +31,7 @@ public class MyFunction_MCPCallRequest : FunctionAppRequestBase public override async Task LoadData(IServiceProvider serviceProvider) { // 添加手动输入选项 - McpServerSelection.Items.Add(new SelectionItem("Manual", "手动输入", "手动输入 MCP 服务器地址", true)); + McpServerSelectionOptions.Items.Add(new SelectionItem("Manual", "手动输入", "手动输入 MCP 服务器地址", true)); // 从 XncfRegisterManager 获取已注册的 MCP 服务器 var mcpServers = XncfRegisterManager.McpServerInfoCollection.Values.ToList(); @@ -38,7 +43,7 @@ public override async Task LoadData(IServiceProvider serviceProvider) // 使用服务器的唯一标识作为 Value,而不是路由 var serverKey = $"{mcpServer.XncfName}|{mcpServer.McpRoute}"; - McpServerSelection.Items.Add(new SelectionItem(serverKey, displayText, description)); + McpServerSelectionOptions.Items.Add(new SelectionItem(serverKey, displayText, description)); } await base.LoadData(serviceProvider); @@ -61,7 +66,11 @@ public class MyFunction_CaculateRequest : FunctionAppRequestBase public int Number2 { get; set; } [Description("运算符||")]//下拉列表 - public SelectionList Operator { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { + [FunctionParameterUi(ParameterType.DropDownList, nameof(OperatorOptions))] + public string Operator { get; set; } + + [JsonIgnore] + public SelectionList OperatorOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { new SelectionItem("+","加法","数字1 + 数字2",false), new SelectionItem("-","减法","数字1 - 数字2",true), new SelectionItem("×","乘法","数字1 × 数字2",false), @@ -69,7 +78,11 @@ public class MyFunction_CaculateRequest : FunctionAppRequestBase }); [Description("计算平方||")]//多选框 - public SelectionList Power { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(PowerOptions))] + public string[] Power { get; set; } + + [JsonIgnore] + public SelectionList PowerOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("2","平方","计算上述结果之后再计算平方",false), new SelectionItem("3","三次方","计算上述结果之后再计算三次方",false) }); diff --git a/src/Extensions/Senparc.Xncf.MCP/Senparc.Xncf.MCP.csproj b/src/Extensions/Senparc.Xncf.MCP/Senparc.Xncf.MCP.csproj index 6025295ca..c3dae91dc 100644 --- a/src/Extensions/Senparc.Xncf.MCP/Senparc.Xncf.MCP.csproj +++ b/src/Extensions/Senparc.Xncf.MCP/Senparc.Xncf.MCP.csproj @@ -1,7 +1,7 @@ net8.0 - 0.2.6-preview.1 + 0.2.7-preview.1 Senparc.Xncf.MCP Senparc.Xncf.MCP true @@ -25,6 +25,7 @@ [2025-06-28] v0.2.3-preview.1 Add MCP server selection in MyFunction_MCPCallRequest [2025-11-01] update basic libraries, including Senparc.AI + [2026-04-24] Simplify FunctionRender request parameters and keep SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease diff --git a/src/Extensions/Senparc.Xncf.PromptRange/Application/AppServices/PromptRangeAppService.cs b/src/Extensions/Senparc.Xncf.PromptRange/Application/AppServices/PromptRangeAppService.cs index 41f480261..3bd6777c7 100644 --- a/src/Extensions/Senparc.Xncf.PromptRange/Application/AppServices/PromptRangeAppService.cs +++ b/src/Extensions/Senparc.Xncf.PromptRange/Application/AppServices/PromptRangeAppService.cs @@ -6,9 +6,12 @@ using Senparc.CO2NET.WebApi; using Senparc.Ncf.Core.AppServices; using Senparc.Ncf.Core.Exceptions; +using Senparc.Ncf.XncfBase.FunctionRenders; +using Senparc.Xncf.PromptRange.OHS.Local.PL.Request; using Senparc.Xncf.PromptRange.Domain.Models.Entities; using Senparc.Xncf.PromptRange.Domain.Services; using Senparc.Xncf.PromptRange.Models.DatabaseModel.Dto; +using Senparc.CO2NET.Extensions; namespace Senparc.Xncf.PromptRange.OHS.Local.AppService; @@ -123,4 +126,39 @@ public async Task DeleteAsync(int rangeId) } ); } + + //[ApiBind] + /// + /// FunctionRender:查看靶场 PromptCode 列表(用于创建智能体) + /// + [FunctionRender("查看 PromptCode 列表", "查看所有靶场和靶道的 PromptCode,可用于在 AgentsManager 中快速创建智能体", typeof(Register))] + public async Task ViewPromptCodeList(PromptRange_ViewPromptCodeRequest request) + { + return await this.GetStringResponseAsync(async (response, logger) => + { + var promptItemService = base.GetService(); + var filterRangeName = request?.FilterRangeName?.Trim(); + var tree = await promptItemService.GetPromptRangeTreeList(true, true, filterRangeName); + + logger.Append("=== PromptCode 列表(可用于在 AgentsManager 中创建智能体)==="); + logger.Append(""); + logger.Append($"筛选条件:{(filterRangeName.IsNullOrEmpty() ? "全部靶场" : filterRangeName)}"); + logger.Append(""); + logger.Append("覆盖范围说明:"); + logger.Append(" 靶场级别(Range):使用靶场名称作为 PromptCode,匹配该靶场下的最优 Prompt"); + logger.Append(" 靶道级别(Tactic):使用「靶场名称-T战术编号」,匹配该战术下的最优 Prompt"); + logger.Append(" 完整定位(Full):使用完整版本号,精确匹配指定 Prompt"); + logger.Append(""); + + foreach (var item in tree) + { + logger.Append($"[{item.Level}] {item.Text} → PromptCode: {item.Value}"); + } + + logger.Append(""); + logger.Append("提示:在 AgentsManager 模块中,使用[从 PromptCode 快速创建智能体]功能可基于以上 PromptCode 快速创建智能体。"); + + return logger.ToString(); + }); + } } \ No newline at end of file diff --git a/src/Extensions/Senparc.Xncf.PromptRange/Application/DTOs/Request/PromptRange_ViewPromptCodeRequest.cs b/src/Extensions/Senparc.Xncf.PromptRange/Application/DTOs/Request/PromptRange_ViewPromptCodeRequest.cs new file mode 100644 index 000000000..e3b1cb360 --- /dev/null +++ b/src/Extensions/Senparc.Xncf.PromptRange/Application/DTOs/Request/PromptRange_ViewPromptCodeRequest.cs @@ -0,0 +1,13 @@ +using Senparc.Ncf.XncfBase.FunctionRenders; +using System.ComponentModel; + +namespace Senparc.Xncf.PromptRange.OHS.Local.PL.Request; + +/// +/// 查看 PromptCode 列表的请求(用于 FunctionRender) +/// +public class PromptRange_ViewPromptCodeRequest : FunctionAppRequestBase +{ + [Description("筛选靶场名称||输入靶场名称关键字进行过滤(可为空,表示显示所有靶场)")] + public string FilterRangeName { get; set; } +} diff --git a/src/Extensions/Senparc.Xncf.PromptRange/Areas/Admin/Pages/PromptRange/Prompt.cshtml b/src/Extensions/Senparc.Xncf.PromptRange/Areas/Admin/Pages/PromptRange/Prompt.cshtml index 653a67d92..0f1869a09 100644 --- a/src/Extensions/Senparc.Xncf.PromptRange/Areas/Admin/Pages/PromptRange/Prompt.cshtml +++ b/src/Extensions/Senparc.Xncf.PromptRange/Areas/Admin/Pages/PromptRange/Prompt.cshtml @@ -73,6 +73,10 @@ style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; font-weight: bold;"> 导图 + + 创建智能体 + @@ -775,7 +779,89 @@ :close-on-click-modal="false" center @@close="mapDialogClose"> -
+ @*工具栏*@ +
+ 重置视角 + 全屏 + 适应视图 + +
+
🖱️ 鼠标操作
+
左键拖拽:旋转视角
+
右键拖拽:平移场景
+
滚轮:缩放
+
单击节点:高亮
+
双击节点:跳转靶道
+
⌨️ 键盘快捷键
+
W / A / S / D:前后左右平移
+
Q / E:深度平移(前后)
+
R:重置视角
+
F:适应视图
+
+/-:放大/缩小
+
+ 快捷键 +
+
+ 共 {{ map3dNodes.length }} 个节点 +
+
+
+ + @*快速创建智能体 dialog*@ + + + + + + + + + 靶场级别 + + + + + + 靶道级别 + + + + + + 完整定位 + + + + + + + + {{ createAgentPromptCode }} + + + + + + + + + @*已有智能体提示*@ + + + @*新增模型 dialog*@ diff --git a/src/Extensions/Senparc.Xncf.PromptRange/wwwroot/js/PromptRange/prompt.js b/src/Extensions/Senparc.Xncf.PromptRange/wwwroot/js/PromptRange/prompt.js index 854569936..bd8802a2e 100644 --- a/src/Extensions/Senparc.Xncf.PromptRange/wwwroot/js/PromptRange/prompt.js +++ b/src/Extensions/Senparc.Xncf.PromptRange/wwwroot/js/PromptRange/prompt.js @@ -176,6 +176,7 @@ var app = new Vue({ map3dNodeMap: new Map(), // 节点映射,用于快速查找 map3dLastAnimationTime: 0, // 上次动画更新时间(用于节流) map3dCurrentNodes: [], // 缓存当前选中的节点(性能优化) + _map3dKeydownHandler: null, // 键盘事件处理器 // 靶场 fieldFormVisible: false, fieldFormSubmitLoading: false, @@ -325,6 +326,17 @@ var app = new Vue({ comparePromptBId: null, // 对比的Prompt B的ID comparePromptA: null, // 对比的Prompt A的完整数据 comparePromptB: null, // 对比的Prompt B的完整数据 + // 快速创建智能体 + createAgentDialogVisible: false, // 创建智能体对话框显示状态 + createAgentLoading: false, // 创建中加载状态 + createAgentForm: { + name: '', // 智能体名称 + scopeType: 'full', // 覆盖范围: 'range'=靶场级别, 'tactic'=靶道级别, 'full'=完整定位 + promptCode: '', // 最终使用的 PromptCode + description: '', // 说明 + functionCallNames: '' // Function Calls + }, + createAgentExistingList: [], // 当前 PromptCode 下已有的智能体列表 // 自定义滚动条缩略图 showScrollbarThumbnails: false, scrollInfo: { @@ -342,6 +354,27 @@ var app = new Vue({ return result }, + // 计算创建智能体时使用的 PromptCode(根据范围选择) + createAgentPromptCode() { + if (!this.promptDetail || !this.promptDetail.fullVersion) { + return '' + } + const fullVersion = this.promptDetail.fullVersion + const parts = fullVersion.split('-') + const rangeName = parts[0] || '' + const tacticPart = parts[1] || '' + + switch (this.createAgentForm.scopeType) { + case 'range': + return rangeName + case 'tactic': + return tacticPart ? `${rangeName}-${tacticPart}` : rangeName + case 'full': + default: + return fullVersion + } + }, + // 获取可选择的Prompt列表(用于对比对话框) availablePrompts() { return this.promptOpt || []; @@ -2886,6 +2919,119 @@ var app = new Vue({ } }, + // 打开创建智能体对话框 + openCreateAgentDialog() { + if (!this.promptField) { + this.$message({ + message: '请先选择靶场', + type: 'warning' + }) + return + } + if (!this.promptid || !this.promptDetail || !this.promptDetail.fullVersion) { + this.$message({ + message: '请先选择靶道以确定 PromptCode', + type: 'warning' + }) + return + } + // 重置表单 + this.createAgentForm = { + name: this.promptDetail.nickName || this.promptDetail.fullVersion, + scopeType: 'full', + promptCode: '', + description: '', + functionCallNames: '' + } + this.createAgentExistingList = [] + this.createAgentDialogVisible = true + // 检查是否已有智能体使用该 PromptCode + this.$nextTick(() => { + this.checkExistingAgentsByPromptCode() + }) + }, + + // 切换 PromptCode 范围时重新检查 + onCreateAgentScopeChange() { + this.checkExistingAgentsByPromptCode() + }, + + // 检查当前 PromptCode 下已有的智能体 + async checkExistingAgentsByPromptCode() { + const promptCode = this.createAgentPromptCode + if (!promptCode) return + try { + const res = await servicePR.get( + `/api/Senparc.Xncf.AgentsManager/AgentTemplateAppService/Xncf.AgentsManager_AgentTemplateAppService.GetListByPromptCode?promptCode=${encodeURIComponent(promptCode)}` + ) + if (res.data && res.data.success) { + this.createAgentExistingList = res.data.data || [] + } + } catch (e) { + // 忽略错误(AgentsManager 模块可能未安装) + console.warn('检查智能体时出错(请确认 AgentsManager 模块已安装):', e) + this.createAgentExistingList = [] + } + }, + + // 提交创建智能体 + async submitCreateAgent() { + if (!this.createAgentForm.name) { + this.$message({ message: '请输入智能体名称', type: 'warning' }) + return + } + const promptCode = this.createAgentPromptCode + if (!promptCode) { + this.$message({ message: '无效的 PromptCode', type: 'error' }) + return + } + + // 如果已有智能体,需确认 + if (this.createAgentExistingList.length > 0) { + try { + await this.$confirm( + `当前 PromptCode(${promptCode})已有 ${this.createAgentExistingList.length} 个智能体使用。是否继续创建新智能体?`, + '确认创建', + { confirmButtonText: '继续创建', cancelButtonText: '取消', type: 'warning' } + ) + } catch { + return + } + } + + this.createAgentLoading = true + try { + const agentData = { + id: 0, + name: this.createAgentForm.name, + systemMessage: promptCode, + promptCode: promptCode, + enable: true, + description: this.createAgentForm.description || '', + hookRobotType: 0, + hookRobotParameter: '', + avastar: '/images/AgentsManager/avatar/avatar1.png', + functionCallNames: this.createAgentForm.functionCallNames || '', + mcpEndpoints: '' + } + const res = await servicePR.post( + '/api/Senparc.Xncf.AgentsManager/AgentTemplateAppService/Xncf.AgentsManager_AgentTemplateAppService.SetItem', + agentData + ) + if (res.data && res.data.success) { + this.$message({ message: `智能体「${this.createAgentForm.name}」创建成功!`, type: 'success' }) + this.createAgentDialogVisible = false + } else { + this.$message({ message: '创建失败:' + (res.data?.msg || '未知错误'), type: 'error' }) + } + } catch (e) { + console.error('创建智能体失败:', e) + this.$message({ message: '创建失败,请确认 AgentsManager 模块已安装', type: 'error' }) + } finally { + this.createAgentLoading = false + } + }, + // 打开导图对话框 openMapDialog() { if (!this.promptField) { @@ -2915,8 +3061,141 @@ var app = new Vue({ // 关闭导图对话框 mapDialogClose() { this.destroyMap3D() + // 退出全屏(如果全屏状态) + if (document.fullscreenElement) { + document.exitFullscreen() + } }, - + + // 重置 3D 视角到初始位置 + resetMap3DView() { + if (!this.map3dCamera || !this.map3dControls) return + this.map3dCamera.position.set(30, 30, 50) + this.map3dControls.target.set(0, 0, 0) + this.map3dControls.update() + this.map3dNeedsAnimationUpdate = true + }, + + // 适应视图:将所有节点都纳入视野 + fitMap3DView() { + if (!this.map3dCamera || !this.map3dControls || !this.map3dNodes || this.map3dNodes.length === 0) return + // 计算所有节点的包围盒中心 + let minX = Infinity, maxX = -Infinity + let minY = Infinity, maxY = -Infinity + let minZ = Infinity, maxZ = -Infinity + this.map3dNodes.forEach(node => { + if (node.position) { + minX = Math.min(minX, node.position.x) + maxX = Math.max(maxX, node.position.x) + minY = Math.min(minY, node.position.y) + maxY = Math.max(maxY, node.position.y) + minZ = Math.min(minZ, node.position.z) + maxZ = Math.max(maxZ, node.position.z) + } + }) + const centerX = (minX + maxX) / 2 + const centerY = (minY + maxY) / 2 + const centerZ = (minZ + maxZ) / 2 + const size = Math.max(maxX - minX, maxY - minY, maxZ - minZ) + const distance = size * 1.5 + 50 + this.map3dCamera.position.set(centerX + distance * 0.4, centerY + distance * 0.4, centerZ + distance) + this.map3dControls.target.set(centerX, centerY, centerZ) + this.map3dControls.update() + this.map3dNeedsAnimationUpdate = true + }, + + // 切换全屏模式 + toggleMap3DFullscreen() { + const container = document.getElementById('map3dContainer') + if (!container) return + if (!document.fullscreenElement) { + container.requestFullscreen().catch(e => { + console.warn('全屏请求失败:', e) + }) + } else { + document.exitFullscreen() + } + }, + + // 注册 3D 键盘快捷键 + registerMap3DKeyboard() { + if (this._map3dKeydownHandler) return + const SPEED = 3 + this._map3dKeydownHandler = (e) => { + if (!this.mapDialogVisible || !this.map3dCamera) return + if (e.ctrlKey || e.metaKey || e.altKey) return + + // 优先使用物理键位(e.code)避免中文输入法/非英文布局下 e.key 不稳定导致快捷键失效 + const key = (e.key || '').toLowerCase() + const code = e.code || '' + let actionKey = key + + if (code.startsWith('Key') && code.length === 4) { + actionKey = code.slice(3).toLowerCase() + } else if (code === 'NumpadAdd') { + actionKey = '+' + } else if (code === 'NumpadSubtract') { + actionKey = '-' + } else if (code === 'Equal' && (key === '' || key === 'unidentified' || key === 'process')) { + actionKey = '=' + } else if (code === 'Minus' && (key === '' || key === 'unidentified' || key === 'process')) { + actionKey = '-' + } + + const hasControls = !!(this.map3dControls && this.map3dControls.target) + const moveBy = (x, y, z) => { + this.map3dCamera.position.x += x + this.map3dCamera.position.y += y + this.map3dCamera.position.z += z + if (hasControls) { + this.map3dControls.target.x += x + this.map3dControls.target.y += y + this.map3dControls.target.z += z + } + } + + let moved = false + // WASD + QE 平移 + switch (actionKey) { + case 'w': moveBy(0, SPEED, 0); moved = true; break + case 's': moveBy(0, -SPEED, 0); moved = true; break + case 'a': moveBy(-SPEED, 0, 0); moved = true; break + case 'd': moveBy(SPEED, 0, 0); moved = true; break + case 'q': moveBy(0, 0, SPEED); moved = true; break + case 'e': moveBy(0, 0, -SPEED); moved = true; break + case 'r': this.resetMap3DView(); moved = true; break + case 'f': this.fitMap3DView(); moved = true; break + case '+': + case '=': this.map3dCamera.position.z -= SPEED * 2; moved = true; break + case '-': this.map3dCamera.position.z += SPEED * 2; moved = true; break + } + if (moved) { + if (this.map3dControls) { + this.map3dControls.update() + } + this.map3dNeedsAnimationUpdate = true + console.debug('[Map3D Keyboard] key:', actionKey, { + rawKey: e.key, + code: e.code, + camera: { + x: this.map3dCamera.position.x, + y: this.map3dCamera.position.y, + z: this.map3dCamera.position.z + }, + target: hasControls ? { + x: this.map3dControls.target.x, + y: this.map3dControls.target.y, + z: this.map3dControls.target.z + } : null + }) + e.preventDefault() + e.stopPropagation() + } + } + window.addEventListener('keydown', this._map3dKeydownHandler, true) + console.debug('[Map3D Keyboard] shortcut listener registered') + }, + // 初始化 3D 导图 initMap3D() { const container = document.getElementById('map3dContainer') @@ -3031,6 +3310,14 @@ var app = new Vue({ // 处理窗口大小变化 window.addEventListener('resize', this.handleMap3DResize) + + // 注册键盘快捷键 + this.registerMap3DKeyboard() + + // 自动适应视图(延迟一点等节点渲染完成) + this.$nextTick(() => { + setTimeout(() => this.fitMap3DView(), 200) + }) }, // 构建树状结构数据 @@ -5288,6 +5575,12 @@ var app = new Vue({ // 跟踪是否需要渲染 let needsRender = false + // 检查外部触发的渲染请求(如键盘操作、重置视角等) + if (this.map3dNeedsAnimationUpdate) { + needsRender = true + this.map3dNeedsAnimationUpdate = false + } + // 更新控制器(启用阻尼时需要每帧更新) if (this.map3dControls) { // update() 返回 true 表示相机位置发生了变化 @@ -5689,6 +5982,12 @@ var app = new Vue({ destroyMap3D() { window.removeEventListener('resize', this.handleMap3DResize) + // 移除键盘事件处理器 + if (this._map3dKeydownHandler) { + window.removeEventListener('keydown', this._map3dKeydownHandler, true) + this._map3dKeydownHandler = null + } + // 移除点击事件 if (this.map3dRenderer && this.map3dRenderer.domElement && this.map3dClickHandler) { this.map3dRenderer.domElement.removeEventListener('click', this.map3dClickHandler) diff --git a/src/Extensions/Senparc.Xncf.SenMapic/OHS/Local/AppService/MyFuctionAppService.cs b/src/Extensions/Senparc.Xncf.SenMapic/OHS/Local/AppService/MyFuctionAppService.cs index acc5b53b0..ac4f81475 100644 --- a/src/Extensions/Senparc.Xncf.SenMapic/OHS/Local/AppService/MyFuctionAppService.cs +++ b/src/Extensions/Senparc.Xncf.SenMapic/OHS/Local/AppService/MyFuctionAppService.cs @@ -83,7 +83,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques */ double calcResult = request.Number1; - var theOperator = request.Operator.SelectedValues.FirstOrDefault(); + var theOperator = request.Operator; switch (theOperator) { case "+": @@ -114,7 +114,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques Action raisePower = power => { - if (request.Power.SelectedValues.Contains(power.ToString())) + if ((request.Power ?? Array.Empty()).Contains(power.ToString())) { var oldValue = calcResult; calcResult = Math.Pow(calcResult, power); diff --git a/src/Extensions/Senparc.Xncf.SenMapic/OHS/Local/PL/MyFunctionRequest.cs b/src/Extensions/Senparc.Xncf.SenMapic/OHS/Local/PL/MyFunctionRequest.cs index 66f32edb3..98ca315ab 100644 --- a/src/Extensions/Senparc.Xncf.SenMapic/OHS/Local/PL/MyFunctionRequest.cs +++ b/src/Extensions/Senparc.Xncf.SenMapic/OHS/Local/PL/MyFunctionRequest.cs @@ -1,26 +1,28 @@ using Senparc.Ncf.XncfBase.FunctionRenders; using Senparc.Ncf.XncfBase.Functions; +using Senparc.Ncf.XncfBase; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace Senparc.Xncf.SenMapic.OHS.Local.PL { - public class MyFunction_SenMapicRequest: FunctionAppRequestBase -{ - [Required] - [Description("网址||请输入要爬取的网址")] -public string Url { get; set; } + public class MyFunction_SenMapicRequest : FunctionAppRequestBase + { + [Required] + [Description("网址||请输入要爬取的网址")] + public string Url { get; set; } -[Required] -[Description("深度||请输入最大要爬取的深度")] -public int Deepth { get; set; } + [Required] + [Description("深度||请输入最大要爬取的深度")] + public int Deepth { get; set; } -[Required] -[Description("网页数量||请输入要爬取的最大数量")] -public int PageNumber { get; set; } + [Required] + [Description("网页数量||请输入要爬取的最大数量")] + public int PageNumber { get; set; } -} - public class MyFunction_CaculateRequest: FunctionAppRequestBase + } + public class MyFunction_CaculateRequest : FunctionAppRequestBase { [Required] [MaxLength(50)] @@ -36,16 +38,24 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase [Description("数字||数字2")] public int Number2 { get; set; } - [Description("运算符||")]//下拉列表 - public SelectionList Operator { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { - new SelectionItem("+","加法","数字1 + 数字2",false), - new SelectionItem("-","减法","数字1 - 数字2",true), + [Description("运算符||只能在以下选项中选择:+ - × ")]//下拉列表 + [FunctionParameterUi(ParameterType.DropDownList, nameof(OperatorOptions))] + public string Operator { get; set; } + + [JsonIgnore] + public SelectionList OperatorOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { + new SelectionItem("+","加法","数字1 + 数字2",true), + new SelectionItem("-","减法","数字1 - 数字2",false), new SelectionItem("×","乘法","数字1 × 数字2",false), new SelectionItem("÷","除法","数字1 ÷ 数字2",false) }); - [Description("计算平方||")]//多选框 - public SelectionList Power { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [Description("计算平方||如果传入,只能在以下选项中选择:2 3")]//多选框 + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(PowerOptions))] + public string[] Power { get; set; } + + [JsonIgnore] + public SelectionList PowerOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("2","平方","计算上述结果之后再计算平方",false), new SelectionItem("3","三次方","计算上述结果之后再计算三次方",false) }); diff --git a/src/Extensions/Senparc.Xncf.SenMapic/Senparc.Xncf.SenMapic.csproj b/src/Extensions/Senparc.Xncf.SenMapic/Senparc.Xncf.SenMapic.csproj index 5fbfbff9d..1fbb9834f 100644 --- a/src/Extensions/Senparc.Xncf.SenMapic/Senparc.Xncf.SenMapic.csproj +++ b/src/Extensions/Senparc.Xncf.SenMapic/Senparc.Xncf.SenMapic.csproj @@ -1,7 +1,7 @@ net8.0 - 0.1.12-preview.1 + 0.1.13-preview.1 Senparc.Xncf.SenMapic Senparc.Xncf.SenMapic true @@ -28,6 +28,7 @@ [2025-06-20] v0.1.9-alpha Add MCP functions in XncfRegisterBase class [2025-11-01] update basic libraries, including Senparc.AI + [2026-04-24] Simplify FunctionRender request parameters and keep SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease diff --git a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/Senparc.Xncf.XncfBuilder.Template.csproj b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/Senparc.Xncf.XncfBuilder.Template.csproj index 132fc3535..5f3f20aa0 100644 --- a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/Senparc.Xncf.XncfBuilder.Template.csproj +++ b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/Senparc.Xncf.XncfBuilder.Template.csproj @@ -2,7 +2,7 @@ net8.0 Template - 0.12.10-preview.1 + 0.12.11-preview.1 Senparc.Xncf.XncfBuilder.Template XNCF Template JeffreySu @@ -54,6 +54,7 @@ v0.12.8-beta1 添加达梦数据库迁移(测试) v0.12.9-preview.7 更新 CO2NET.WebApi、DatabasePlant 等基础库 [2025-11-01] update basic libraries, including Senparc.AI + [2026-04-24] Update FunctionRender request templates for simplified values and SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Apache-2.0 diff --git a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/templates/template1/OHS/Local/AppService/MyFuctionAppService.cs b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/templates/template1/OHS/Local/AppService/MyFuctionAppService.cs index 4391cfabd..1571508c3 100644 --- a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/templates/template1/OHS/Local/AppService/MyFuctionAppService.cs +++ b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/templates/template1/OHS/Local/AppService/MyFuctionAppService.cs @@ -37,7 +37,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques */ double calcResult = request.Number1; - var theOperator = request.Operator.SelectedValues.FirstOrDefault(); + var theOperator = request.Operator; switch (theOperator) { case "+": @@ -68,7 +68,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques Action raisePower = power => { - if (request.Power.SelectedValues.Contains(power.ToString())) + if ((request.Power ?? Array.Empty()).Contains(power.ToString())) { var oldValue = calcResult; calcResult = Math.Pow(calcResult, power); diff --git a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/templates/template1/OHS/Local/PL/MyFunctionRequest.cs b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/templates/template1/OHS/Local/PL/MyFunctionRequest.cs index 020e7db31..61feb5107 100644 --- a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/templates/template1/OHS/Local/PL/MyFunctionRequest.cs +++ b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/templates/template1/OHS/Local/PL/MyFunctionRequest.cs @@ -1,7 +1,9 @@ using Senparc.Ncf.XncfBase.FunctionRenders; +using Senparc.Ncf.XncfBase; using Senparc.Ncf.XncfBase.Functions; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace Template_OrgName.Xncf.Template_XncfName.OHS.Local.PL { @@ -22,7 +24,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase public int Number2 { get; set; } [Description("运算符||")]//下拉列表 - public SelectionList Operator { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { + [FunctionParameterUi(ParameterType.DropDownList, nameof(OperatorOptions))] + public string Operator { get; set; } + + [JsonIgnore] + public SelectionList OperatorOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { new SelectionItem("+","加法","数字1 + 数字2",false), new SelectionItem("-","减法","数字1 - 数字2",true), new SelectionItem("×","乘法","数字1 × 数字2",false), @@ -30,7 +36,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase }); [Description("计算平方||")]//多选框 - public SelectionList Power { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(PowerOptions))] + public string[] Power { get; set; } + + [JsonIgnore] + public SelectionList PowerOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("2","平方","计算上述结果之后再计算平方",false), new SelectionItem("3","三次方","计算上述结果之后再计算三次方",false) }); diff --git a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/templates/template1/Template_OrgName.Xncf.Template_XncfName.csproj b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/templates/template1/Template_OrgName.Xncf.Template_XncfName.csproj index bd71434a1..e528d5448 100644 --- a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/templates/template1/Template_OrgName.Xncf.Template_XncfName.csproj +++ b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.Template/templates/template1/Template_OrgName.Xncf.Template_XncfName.csproj @@ -1,7 +1,7 @@ net8.0 - 1.0.1-preview.1 + 1.0.2-preview.1 Template_OrgName.Xncf.Template_XncfName Template_OrgName.Xncf.Template_XncfName true @@ -19,6 +19,7 @@ icon.jpg v1.0.0 创世 + [2026-04-24] Update FunctionRender request templates for simplified values and SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease diff --git a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/BuildXncfAppService.AI.MCP.cs b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/BuildXncfAppService.AI.MCP.cs index 91fdd4904..6b02a0ca3 100644 --- a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/BuildXncfAppService.AI.MCP.cs +++ b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/BuildXncfAppService.AI.MCP.cs @@ -69,35 +69,20 @@ public async Task Build( MenuName = menuName, Icon = icon, Description = description, - UseSammple = new Ncf.XncfBase.Functions.SelectionList(Ncf.XncfBase.Functions.SelectionType.CheckBoxList, new[] { - new Ncf.XncfBase.Functions.SelectionItem("1","使用示例","使用示例",true), - }), - UseModule = new Ncf.XncfBase.Functions.SelectionList(Ncf.XncfBase.Functions.SelectionType.CheckBoxList, new[] { - new Ncf.XncfBase.Functions.SelectionItem("database","数据库","使用数据库",true), - }), + UseSammple = true, + UseModule = new[] { "database" }, // UseWeb = new Ncf.XncfBase.Functions.SelectionList( Ncf.XncfBase.Functions.SelectionType.CheckBoxList, new[] { // new Ncf.XncfBase.Functions.SelectionItem("1","使用Web","使用Web",true), // }), // UseWebApi = new Ncf.XncfBase.Functions.SelectionList( Ncf.XncfBase.Functions.SelectionType.CheckBoxList, new[] { // new Ncf.XncfBase.Functions.SelectionItem("1","使用WebApi","使用WebApi",true), // }), - NewSlnFile = new Ncf.XncfBase.Functions.SelectionList(Ncf.XncfBase.Functions.SelectionType.CheckBoxList, new[] { - new Ncf.XncfBase.Functions.SelectionItem("backup","备份 .sln 文件(推荐)","如果使用覆盖现有 .sln 文件,对当前文件进行备份",true), - }), - TemplatePackage = new Ncf.XncfBase.Functions.SelectionList(Ncf.XncfBase.Functions.SelectionType.DropDownList, new[] { - new Ncf.XncfBase.Functions.SelectionItem("no","已安装,不需要安装新版本","请确保已经在本地安装过版本(无论新旧),否则将自动从在线获取",true), - }), - FrameworkVersion = new Ncf.XncfBase.Functions.SelectionList(Ncf.XncfBase.Functions.SelectionType.DropDownList, new[] { - new Ncf.XncfBase.Functions.SelectionItem("net8.0","net8.0","使用 .NET 8.0",false), - }) + NewSlnFile = new[] { "backup" }, + TemplatePackage = "no", + FrameworkVersion = "net8.0" }; request.SlnFilePath = request.GetSlnFilePath(); - request.UseSammple.SelectedValues = new[] { "1" }; - request.UseModule.SelectedValues = new[] { "database" }; - request.NewSlnFile.SelectedValues = new[] { "backup" }; - request.TemplatePackage.SelectedValues = new[] { "no" }; - request.FrameworkVersion.SelectedValues = new[] { "net8.0" }; Console.WriteLine("XNCF Builder parameters:" + request.ToJson(true)); diff --git a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/BuildXncfAppService.AI.cs b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/BuildXncfAppService.AI.cs index 3c49e282e..b2a103347 100644 --- a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/BuildXncfAppService.AI.cs +++ b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/BuildXncfAppService.AI.cs @@ -37,11 +37,11 @@ public async Task CreateDatabaseEntity(BuildXncf_CreateDataba return await this.GetStringResponseAsync(async (response, logger) => { var promptBuilderService = base.ServiceProvider.GetRequiredService(); - var aiModelSelected = request.AIModel.SelectedValues.FirstOrDefault(); + var aiModelSelected = request.AIModel; ISenparcAiSetting aiSetting = null; #region PromptRange 是否已经初始化 - if (request.UseDatabasePrompt.IsSelected("1")) + if (request.UseDatabasePrompt) { var promptRangeRegister = new Xncf.PromptRange.Register(); var promptRangeModule = this._xncfModuleService.GetObject(z => z.Uid == promptRangeRegister.Uid); @@ -96,7 +96,7 @@ public async Task CreateDatabaseEntity(BuildXncf_CreateDataba var input = request.Requirement; - var projectPath = request.InjectDomain.SelectedValues.FirstOrDefault(); + var projectPath = request.InjectDomain; if (projectPath.IsNullOrEmpty() || projectPath == "N/A") { @@ -122,7 +122,7 @@ public async Task CreateDatabaseEntity(BuildXncf_CreateDataba #region 生成实体 DTO - if (request.MoreActions.IsSelected("BuildDto")) + if ((request.MoreActions ?? Array.Empty()).Contains("BuildDto")) { var entityDtoResult = await promptBuilderService.RunPromptAsync(aiSetting, Domain.PromptBuildType.EntityDtoClass, fileContent, className, null, projectPath, @namespace); logger.Append("生成实体 DTO:"); @@ -140,7 +140,7 @@ public async Task CreateDatabaseEntity(BuildXncf_CreateDataba #endregion #region 进行 Migration - if (request.MoreActions.IsSelected("BuildMigration")) + if ((request.MoreActions ?? Array.Empty()).Contains("BuildMigration")) { await Console.Out.WriteLineAsync("进入 Migration,可能耗时较长,请等待"); logger.Append(); @@ -199,11 +199,11 @@ public async Task CreateDatabaseEntity(BuildXncf_CreateDataba await requestObj.LoadData(this.ServiceProvider); //指定项目路径 - requestObj.ProjectPath.SelectedValues = new[] { projectPath }; + requestObj.ProjectPath = projectPath; //选中所有数据库 - requestObj.DatabaseTypes.SelectedValues = requestObj.DatabaseTypes.Items.Select(z => z.Value).ToArray(); + requestObj.DatabaseTypes = requestObj.DatabaseTypeOptions.Items.Select(z => z.Value).ToArray(); //选中输出详情 - requestObj.OutputVerbose.SelectedValues = new[] { requestObj.OutputVerbose.Items.First().Value }; + requestObj.OutputVerbose = true; var databaseMigrationsAppService = base.ServiceProvider.GetRequiredService(); var migrationResult = await databaseMigrationsAppService.AddMigration(requestObj); @@ -241,8 +241,8 @@ public async Task InitPrompt(BuildXncf_InitPromptRequest requ { return await this.GetStringResponseAsync(async (response, logger) => { - var needOverride = request.Override.SelectedValues.Contains("1"); - var aiModel = request.AIModel.SelectedValues.FirstOrDefault(); + var needOverride = request.Override; + var aiModel = request.AIModel; var promptBuilderService = base.ServiceProvider.GetRequiredService(); var log = await promptBuilderService.InitPromptAsync("XncfBuilderPlugin", needOverride, aiModel); diff --git a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/BuildXncfAppService.cs b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/BuildXncfAppService.cs index 303a3856c..1adb63f22 100644 --- a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/BuildXncfAppService.cs +++ b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/BuildXncfAppService.cs @@ -129,7 +129,7 @@ async Task ReadOutputAsync(Process process) logger.Append(templateOutput); var unInstallTemplatePackage = !templateOutput.Contains("Custom XNCF Module Template"); var installPackageCmd = string.Empty; - switch (request.TemplatePackage.SelectedValues.First()) + switch (request.TemplatePackage) { case "online": logger.Append("配置在线安装 XNCF 模板"); @@ -180,20 +180,21 @@ async Task ReadOutputAsync(Process process) } var frameworkVersion = string.IsNullOrEmpty(request.OtherFrameworkVersion) - ? request.FrameworkVersion.SelectedValues.First() + ? request.FrameworkVersion : request.OtherFrameworkVersion; string xncfBaseVersion = getLibVersionParam("Senparc.Ncf.XncfBase.dll", "XncfBaseVersion"); string ncfAreaBaseVersion = getLibVersionParam("Senparc.Ncf.AreaBase.dll", "NcfAreaBaseVersion"); - var isUseSample = request.UseSammple.SelectedValues.Contains("1"); - var isUseDatabase = isUseSample || request.UseModule.SelectedValues.Contains("database"); + var useModules = request.UseModule ?? Array.Empty(); + var isUseSample = request.UseSammple; + var isUseDatabase = isUseSample || useModules.Contains("database"); //var useSample = getBoolParam(isUseSample, "Sample"); - var useFunction = request.UseModule.SelectedValues.Contains("function"); - var isUseWeb = isUseSample || request.UseModule.SelectedValues.Contains("web"); + var useFunction = useModules.Contains("function"); + var isUseWeb = isUseSample || useModules.Contains("web"); //var useWeb = getBoolParam(isUseWeb, "Web"); //var useDatabase = getBoolParam(isUseDatabase, "Database"); - var useWebApi = request.UseModule.SelectedValues.Contains("webapi"); + var useWebApi = useModules.Contains("webapi"); //采用一个独立的进程 var args = new List @@ -349,7 +350,8 @@ public async Task Build(BuildXncf_BuildRequest request) else if (File.Exists(request.SlnFilePath)) { //是否创建新的 .sln 文件 - var useNewSlnFile = request.NewSlnFile.SelectedValues.Contains("new"); + var newSlnFileOptions = request.NewSlnFile ?? Array.Empty(); + var useNewSlnFile = newSlnFileOptions.Contains("new"); var slnFileName = Path.GetFileName(request.SlnFilePath); string newSlnFileName = slnFileName; @@ -363,7 +365,7 @@ public async Task Build(BuildXncf_BuildRequest request) } else { - var backupSln = request.NewSlnFile.SelectedValues.Contains("backup"); + var backupSln = newSlnFileOptions.Contains("backup"); var backupFileName = $"{slnFileName}-backup-{SystemTime.Now.DateTime.ToString("yyyyMMdd_HHmmss")}.sln"; var backupFilePath = Path.Combine(Path.GetDirectoryName(request.SlnFilePath), backupFileName); File.Copy(request.SlnFilePath, backupFilePath); diff --git a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/DatabaseMigrationsAppService.cs b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/DatabaseMigrationsAppService.cs index 36ea26d25..99a468023 100644 --- a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/DatabaseMigrationsAppService.cs +++ b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/Local/DatabaseMigrationsAppService.cs @@ -43,7 +43,8 @@ public async Task AddMigration(DatabaseMigrations_MigrationRe { return await this.GetStringResponseAsync(async (response, logger) => { - if (request.DatabaseTypes.SelectedValues.Count() == 0) + var databaseTypes = request.DatabaseTypes ?? Array.Empty(); + if (databaseTypes.Length == 0) { response.Success = false; response.Data = "至少选择 1 个数据库!"; @@ -63,10 +64,10 @@ public async Task AddMigration(DatabaseMigrations_MigrationRe commandTexts.Add(@$"cd ""{projectPath}"""); //执行迁移 - foreach (var dbType in request.DatabaseTypes.SelectedValues) + foreach (var dbType in databaseTypes) { string migrationDir = GetMigrationDir(request, dbType); - var outputVerbose = request.OutputVerbose.SelectedValues.Contains("1") ? " -v" : ""; + var outputVerbose = request.OutputVerbose ? " -v" : ""; //数据库上下文实体名称 var dbContextName = request.DbContextName; @@ -166,7 +167,7 @@ public async Task AddMigration(DatabaseMigrations_MigrationRe logger.Append(""); logger.Append("==== 版本号更新 ===="); - var updateVesionType = request.UpdateVersion.SelectedValues.FirstOrDefault(); + var updateVesionType = request.UpdateVersion; if (updateVesionType != "0") { var registerFile = Path.Combine(projectPath, "Register.cs"); diff --git a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/PL/BuildXncfRequest.AI.cs b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/PL/BuildXncfRequest.AI.cs index 24b5a9f9e..21c87cdc7 100644 --- a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/PL/BuildXncfRequest.AI.cs +++ b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/PL/BuildXncfRequest.AI.cs @@ -6,11 +6,13 @@ using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Senparc.Ncf.XncfBase; using Senparc.Ncf.XncfBase.FunctionRenders; using Senparc.Ncf.XncfBase.Functions; using Senparc.Xncf.AIKernel.OHS.Local.AppService; using Senparc.AI.Exceptions; using Senparc.Xncf.AIKernel.Models; +using System.Text.Json.Serialization; namespace Senparc.Xncf.XncfBuilder.OHS.PL { @@ -52,10 +54,18 @@ public class BuildXncf_CreateDatabaseEntityRequest : FunctionAppRequestBase public string Requirement { get; set; } [Description("领域||指定需要生成到的领域")] - public SelectionList InjectDomain { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); + [FunctionParameterUi(ParameterType.DropDownList, nameof(InjectDomainOptions))] + public string InjectDomain { get; set; } + + [JsonIgnore] + public SelectionList InjectDomainOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); [Description("后续操作||指定生成数据库实体后的后续操作")] - public SelectionList MoreActions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(MoreActionsOptions))] + public string[] MoreActions { get; set; } + + [JsonIgnore] + public SelectionList MoreActionsOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("BuildDto","创建 DTO","创建 DTO 对象(已强制生成)",true), new SelectionItem("BuildMigration","直接生成数据库迁移信息","使用 EF Core Migration 生成迁移信息(建议查看后进行)",true), new SelectionItem("CreateRepository","创建 Repository","创建和实体匹配的 Repository",false), @@ -64,12 +74,20 @@ public class BuildXncf_CreateDatabaseEntityRequest : FunctionAppRequestBase }); [Description("使用 PromptRange ||指定 Prompt 来源。如果选中,系统将自动安装 PromptRange 模块并初始化 Prompt(此时需要提前配置好系统默认 AI 模型),全程无需任何人为干预;如不选中此选项,请在运行项目下 Domain/PromptPlugins/ 文件夹下存放 XncfBuilderPlugin 文件夹及所有文件内容。")] - public SelectionList UseDatabasePrompt { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(UseDatabasePromptOptions))] + public bool UseDatabasePrompt { get; set; } = true; + + [JsonIgnore] + public SelectionList UseDatabasePromptOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("1","是","",true) }); [Description("AI 模型||当不使用 PromptRange 时,需要选择生成代码所使用的 AI 模型")] - public SelectionList AIModel { get; set; } = new SelectionList(SelectionType.DropDownList, new List + [FunctionParameterUi(ParameterType.DropDownList, nameof(AIModelOptions))] + public string AIModel { get; set; } + + [JsonIgnore] + public SelectionList AIModelOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new List { //new SelectionItem("Default","系统默认","通过系统默认配置的固定 AI 模型信息",true) }); @@ -78,10 +96,10 @@ public override async Task LoadData(IServiceProvider serviceProvider) { //扫描当前解决方案包含的所有领域项目 var newItems = FunctionHelper.LoadXncfProjects(true, null,"Senparc.Areas.Admin"); - newItems.ForEach(z => InjectDomain.Items.Add(z)); + newItems.ForEach(z => InjectDomainOptions.Items.Add(z)); //载入 AI 模型 - await BuildXncfRequestHelper.LoadAiModelData(serviceProvider, AIModel); + await BuildXncfRequestHelper.LoadAiModelData(serviceProvider, AIModelOptions); await base.LoadData(serviceProvider); } @@ -90,12 +108,20 @@ public override async Task LoadData(IServiceProvider serviceProvider) public class BuildXncf_InitPromptRequest : FunctionAppRequestBase { [Description("覆盖||如果记录已存在,则删除 XncfBuilderPlugin 靶场,使用官方版本重建")] - public SelectionList Override { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(OverrideOptions))] + public bool Override { get; set; } + + [JsonIgnore] + public SelectionList OverrideOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("1","是","",false) }); [Description("AI 模型||请选择新建的靶场(Range)中所有靶道(Tactics)使用的 AI 模型")] - public SelectionList AIModel { get; set; } = new SelectionList(SelectionType.DropDownList, new List + [FunctionParameterUi(ParameterType.DropDownList, nameof(AIModelOptions))] + public string AIModel { get; set; } + + [JsonIgnore] + public SelectionList AIModelOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new List { //new SelectionItem("Default","系统默认","通过系统默认配置的固定 AI 模型信息",true) }); @@ -103,7 +129,7 @@ public class BuildXncf_InitPromptRequest : FunctionAppRequestBase public override async Task LoadData(IServiceProvider serviceProvider) { //载入 AI 模型 - await BuildXncfRequestHelper.LoadAiModelData(serviceProvider, AIModel); + await BuildXncfRequestHelper.LoadAiModelData(serviceProvider, AIModelOptions); await base.LoadData(serviceProvider); } diff --git a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/PL/BuildXncfRequest.cs b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/PL/BuildXncfRequest.cs index 8a248a181..821812cc4 100644 --- a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/PL/BuildXncfRequest.cs +++ b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/PL/BuildXncfRequest.cs @@ -2,6 +2,7 @@ using Senparc.CO2NET.Extensions; using Senparc.CO2NET.Trace; using Senparc.Ncf.Service; +using Senparc.Ncf.XncfBase; using Senparc.Ncf.XncfBase.FunctionRenders; using Senparc.Ncf.XncfBase.Functions; using Senparc.Xncf.AIKernel.Domain.Services; @@ -14,6 +15,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Text.Json.Serialization; namespace Senparc.Xncf.XncfBuilder.OHS.PL { @@ -25,21 +27,33 @@ public class BuildXncf_BuildRequest : FunctionAppRequestBase public string SlnFilePath { get; set; } [Description("配置解决方案文件(.sln)||")] - public SelectionList NewSlnFile { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(NewSlnFileOptions))] + public string[] NewSlnFile { get; set; } + + [JsonIgnore] + public SelectionList NewSlnFileOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("backup","备份 .sln 文件(推荐)","如果使用覆盖现有 .sln 文件,对当前文件进行备份",true), new SelectionItem("new","生成新的 .sln 文件","如果不选择,将覆盖现有 .sln 文件(不会影响已有功能,但如果 sln 解决方案正在运行,可能会触发自动重启服务),并推荐使用备份功能",false), }); [MaxLength(250)] [Description("安装新模板||安装 XNCF 的模板,如果重新安装可能需要 30-40s,如果已安装过模板,可选择【已安装】,以节省模板获取时间。")] - public SelectionList TemplatePackage { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { + [FunctionParameterUi(ParameterType.DropDownList, nameof(TemplatePackageOptions))] + public string TemplatePackage { get; set; } + + [JsonIgnore] + public SelectionList TemplatePackageOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { new SelectionItem("online","在线获取(从 Nuget.org 等在线环境获取最新版本,时间会略长)","从 Nuget.org 等在线环境获取最新版本,时间会略长",false), new SelectionItem("local","本地安装(从 .sln 同级目录下安装 Senparc.Xncf.XncfBuilder.Template.*.nupkg 包)","从 .sln 同级目录下安装 Senparc.Xncf.XncfBuilder.Template.*.nupkg 包",false), new SelectionItem("no","已安装,不需要安装新版本","请确保已经在本地安装过版本(无论新旧),否则将自动从在线获取",true), }); [Description("目标框架版本||指定项目的 TFM(Target Framework Moniker)")] - public SelectionList FrameworkVersion { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { + [FunctionParameterUi(ParameterType.DropDownList, nameof(FrameworkVersionOptions))] + public string FrameworkVersion { get; set; } + + [JsonIgnore] + public SelectionList FrameworkVersionOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { //new SelectionItem("netstandard2.1","netstandard2.1","使用 .NET Standard 2.1(兼容 .NET Core 3.1 和 .NET 5.0-8.0)",true), //new SelectionItem("netcoreapp3.1","netcoreapp3.1","使用 .NET Core 3.1",false), //new SelectionItem("net6.0","net6.0","使用 .NET 6.0",false), @@ -86,7 +100,11 @@ public class BuildXncf_BuildRequest : FunctionAppRequestBase public string Description { get; set; } [Description("功能配置||")] - public SelectionList UseModule { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(UseModuleOptions))] + public string[] UseModule { get; set; } + + [JsonIgnore] + public SelectionList UseModuleOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("function","配置“函数”功能","是否需要使用函数模块(Function)",false), new SelectionItem("database","配置“数据库”功能","是否需要使用数据库模块(Database),将配置空数据库",false), new SelectionItem("webapi","配置“WebApi”功能","是否需要使用WebApi模块(WebApi)",false), @@ -94,7 +112,11 @@ public class BuildXncf_BuildRequest : FunctionAppRequestBase }); [Description("安装 Sample||")] - public SelectionList UseSammple { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(UseSammpleOptions))] + public bool UseSammple { get; set; } + + [JsonIgnore] + public SelectionList UseSammpleOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("1","是","是否安装数据库示例,由于展示需要,将自动安装上述“数据库”、“Web(Area) 页面”功能",false), }); diff --git a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/PL/DatabaseMigrationRequest.cs b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/PL/DatabaseMigrationRequest.cs index 73b9f1bce..e862c43fb 100644 --- a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/PL/DatabaseMigrationRequest.cs +++ b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/OHS/PL/DatabaseMigrationRequest.cs @@ -3,6 +3,7 @@ using Senparc.Ncf.Core.Exceptions; using Senparc.Ncf.Core.Models; using Senparc.Ncf.Service; +using Senparc.Ncf.XncfBase; using Senparc.Ncf.XncfBase.FunctionRenders; using Senparc.Ncf.XncfBase.Functions; using Senparc.Xncf.XncfBuilder.Domain.Models.Services; @@ -14,6 +15,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Text.Json.Serialization; namespace Senparc.Xncf.XncfBuilder.OHS.PL { @@ -25,14 +27,22 @@ public class DatabaseMigrations_MigrationRequest : FunctionAppRequestBase public string DatabasePlantPath { get; set; } [Description("XNCF 项目路径||选择 XNCF 项目根目录的完整物理路径")] - public SelectionList ProjectPath { get; set; } = new SelectionList(SelectionType.DropDownList); + [FunctionParameterUi(ParameterType.DropDownList, nameof(ProjectPathOptions))] + public string ProjectPath { get; set; } + + [JsonIgnore] + public SelectionList ProjectPathOptions { get; set; } = new SelectionList(SelectionType.DropDownList); [MaxLength(250)] [Description("自定义 XNCF 项目路径||仅当“XNCF 项目路径”选择“自定义路径”时有效。输入 XNCF 项目根目录的完整物理路径,如:E:\\Senparc项目\\NeuCharFramework\\NCF\\src\\MyDemo.Xncf.NewApp\\")] public string CustomProjectPath { get; set; } [Description("生成数据库类型||更多类型陆续添加中")] - public SelectionList DatabaseTypes { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(DatabaseTypeOptions))] + public string[] DatabaseTypes { get; set; } + + [JsonIgnore] + public SelectionList DatabaseTypeOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem(MultipleDatabaseType.Sqlite.ToString(),MultipleDatabaseType.Sqlite.ToString(),"",true), new SelectionItem(MultipleDatabaseType.SqlServer.ToString(),MultipleDatabaseType.SqlServer.ToString(),"",true), new SelectionItem(MultipleDatabaseType.MySql.ToString(),MultipleDatabaseType.MySql.ToString(),"",true), @@ -53,7 +63,11 @@ public class DatabaseMigrations_MigrationRequest : FunctionAppRequestBase [Description("自动更新版本号||自动更新 Register.cs 中的版本号")] - public SelectionList UpdateVersion { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { + [FunctionParameterUi(ParameterType.DropDownList, nameof(UpdateVersionOptions))] + public string UpdateVersion { get; set; } + + [JsonIgnore] + public SelectionList UpdateVersionOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { new SelectionItem("0","不更新","",true), new SelectionItem("1","主版本号(Major) + 1","",false), new SelectionItem("2","次版本号(Minor) + 1","",false), @@ -62,7 +76,11 @@ public class DatabaseMigrations_MigrationRequest : FunctionAppRequestBase [Description("输出详细日志||使用 add-migration 的 -v 参数")] - public SelectionList OutputVerbose { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(OutputVerboseOptions))] + public bool OutputVerbose { get; set; } + + [JsonIgnore] + public SelectionList OutputVerboseOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("1","使用","",false) }); @@ -77,7 +95,7 @@ public override async Task LoadData(IServiceProvider serviceProvider) { //TODO:单独生成一个表来记录 - this.ProjectPath.Items.Add(new SelectionItem("N/A", "自定义路径", "", true)); + this.ProjectPathOptions.Items.Add(new SelectionItem("N/A", "自定义路径", "", true)); //添加“停机坪”路径 var configService = serviceProvider.GetService(); @@ -91,13 +109,13 @@ public override async Task LoadData(IServiceProvider serviceProvider) //添加当前解决方案的项目选项 var projectList = FunctionHelper.LoadXncfProjects(false, null, "Senparc.Areas.Admin"); - projectList.OrderBy(z=>z.Value).ToList().ForEach(z => ProjectPath.Items.Add(z)); + projectList.OrderBy(z=>z.Value).ToList().ForEach(z => ProjectPathOptions.Items.Add(z)); //添加 NcfPackageSource 项目的解决方案的项目选项 var sourceRootDir = Path.Combine(System.IO.Directory.GetCurrentDirectory(), "..", "..", "..", "src"); Console.WriteLine("查找 Source 项目源文件根目录:" + sourceRootDir); var sourceProjectList = FunctionHelper.LoadXncfProjects(false, sourceRootDir, "Senparc.Areas.Admin"); - sourceProjectList.OrderBy(z => z.Value).ToList().ForEach(z => ProjectPath.Items.Add(z)); + sourceProjectList.OrderBy(z => z.Value).ToList().ForEach(z => ProjectPathOptions.Items.Add(z)); } } catch @@ -114,7 +132,7 @@ public override async Task LoadData(IServiceProvider serviceProvider) /// public string GetProjectPath(DatabaseMigrations_MigrationRequest request) { - var projectPath = request.ProjectPath.SelectedValues.FirstOrDefault(); + var projectPath = request.ProjectPath; if (projectPath == "N/A") { projectPath = request.CustomProjectPath; diff --git a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.csproj b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.csproj index dff05ddb5..2fe0bde3f 100644 --- a/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.csproj +++ b/src/Extensions/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder/Senparc.Xncf.XncfBuilder.csproj @@ -1,7 +1,7 @@ net8.0 - 0.35.7-preview.1 + 0.35.8-preview.1 latest Senparc.Xncf.XncfBuilder Senparc.Xncf.XncfBuilder @@ -75,6 +75,7 @@ [2025-06-28] v0.35.3-preview.10 Add dynamic XNCF Template dynamic generator, support for dotnet CLI and Visual Studio together [2025-11-01] update basic libraries, including Senparc.AI [2026-01-07] v0.35.6-preview.1 Add NcfSourcePackage project path to DatabaseMigrationRequest loading method + [2026-04-24] Simplify FunctionRender request parameters and keep SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease diff --git a/src/Extensions/System/Senparc.Xncf.XncfModuleManager/OHS/Local/AppService/XncfStateAppService.cs b/src/Extensions/System/Senparc.Xncf.XncfModuleManager/OHS/Local/AppService/XncfStateAppService.cs index 236c588d7..db81b53ea 100644 --- a/src/Extensions/System/Senparc.Xncf.XncfModuleManager/OHS/Local/AppService/XncfStateAppService.cs +++ b/src/Extensions/System/Senparc.Xncf.XncfModuleManager/OHS/Local/AppService/XncfStateAppService.cs @@ -34,7 +34,7 @@ public async Task ShowFunctions(XncfState_ShowFunctionsReques { return await this.GetStringResponseAsync(async (response, logger) => { - var selectedRegisterUid = request.XncfModule.SelectedValues.FirstOrDefault(); + var selectedRegisterUid = request.XncfModule; var register = XncfRegisterManager.RegisterList.FirstOrDefault(z => z.Uid == selectedRegisterUid); if (register == null) { @@ -89,7 +89,7 @@ public async Task InstallAndOpenModule(XncfState_InstallAndOp { return await this.GetStringResponseAsync(async (response, logger) => { - var selectedRegisterUid = request.XncfModule.SelectedValues.FirstOrDefault(); + var selectedRegisterUid = request.XncfModule; if (string.IsNullOrWhiteSpace(selectedRegisterUid)) { response.Data = logger.Append("请先选择需要安装的 XNCF 模块。"); diff --git a/src/Extensions/System/Senparc.Xncf.XncfModuleManager/OHS/Local/PL/XncfStateRequest.cs b/src/Extensions/System/Senparc.Xncf.XncfModuleManager/OHS/Local/PL/XncfStateRequest.cs index 34f4a7194..0f7fad231 100644 --- a/src/Extensions/System/Senparc.Xncf.XncfModuleManager/OHS/Local/PL/XncfStateRequest.cs +++ b/src/Extensions/System/Senparc.Xncf.XncfModuleManager/OHS/Local/PL/XncfStateRequest.cs @@ -7,14 +7,20 @@ using Senparc.Ncf.XncfBase; using Senparc.Ncf.XncfBase.FunctionRenders; using Senparc.Ncf.XncfBase.Functions; +using Senparc.Ncf.XncfBase.Functions.Parameters; using Senparc.Xncf.XncfModuleManager.Domain.Services; +using System.Text.Json.Serialization; namespace Senparc.Xncf.XncfModuleManager.OHS.Local.PL { public class XncfState_ShowFunctionsRequest : FunctionAppRequestBase { [Description("XNCF 模块||查看具体 XNCF 模块的 Function 情况")] - public SelectionList XncfModule { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); + [FunctionParameterUi(ParameterType.DropDownList, nameof(XncfModuleOptions))] + public string XncfModule { get; set; } + + [JsonIgnore] + public SelectionList XncfModuleOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); public override Task LoadData(IServiceProvider serviceProvider) { @@ -23,7 +29,7 @@ public override Task LoadData(IServiceProvider serviceProvider) .ToList(); foreach (var item in registers) { - XncfModule.Items.Add(new SelectionItem(item.Uid, item.Name)); + XncfModuleOptions.Items.Add(new SelectionItem(item.Uid, item.Name)); } return base.LoadData(serviceProvider); @@ -33,7 +39,11 @@ public override Task LoadData(IServiceProvider serviceProvider) public class XncfState_InstallAndOpenModuleRequest : FunctionAppRequestBase { [Description("XNCF 模块||选择需要安装并开放的模块")] - public SelectionList XncfModule { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); + [FunctionParameterUi(ParameterType.DropDownList, nameof(XncfModuleOptions))] + public string XncfModule { get; set; } + + [JsonIgnore] + public SelectionList XncfModuleOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new List()); public override async Task LoadData(IServiceProvider serviceProvider) { @@ -47,15 +57,15 @@ public override async Task LoadData(IServiceProvider serviceProvider) foreach (var register in canInstallRegisters) { - XncfModule.Items.Add(new SelectionItem(register.Uid, $"{register.MenuName} ({register.Name})")); + XncfModuleOptions.Items.Add(new SelectionItem(register.Uid, $"{register.MenuName} ({register.Name})")); } } - if (XncfModule.Items.Count == 0) + if (XncfModuleOptions.Items.Count == 0) { foreach (var register in XncfRegisterManager.RegisterList.Where(z => !z.IgnoreInstall).OrderBy(z => z.MenuName)) { - XncfModule.Items.Add(new SelectionItem(register.Uid, $"{register.MenuName} ({register.Name})")); + XncfModuleOptions.Items.Add(new SelectionItem(register.Uid, $"{register.MenuName} ({register.Name})")); } } diff --git a/src/Extensions/System/Senparc.Xncf.XncfModuleManager/[5950]Senparc.Xncf.XncfModuleManager.csproj b/src/Extensions/System/Senparc.Xncf.XncfModuleManager/[5950]Senparc.Xncf.XncfModuleManager.csproj index 7d6c9e23d..006601c4c 100644 --- a/src/Extensions/System/Senparc.Xncf.XncfModuleManager/[5950]Senparc.Xncf.XncfModuleManager.csproj +++ b/src/Extensions/System/Senparc.Xncf.XncfModuleManager/[5950]Senparc.Xncf.XncfModuleManager.csproj @@ -1,7 +1,7 @@ net8.0 - 0.13.16-preview.1 + 0.13.17-preview.1 Senparc.Xncf.XncfModuleManager Senparc.Xncf.XncfModuleManager true @@ -41,6 +41,7 @@ [2025-06-20] v0.13.13-preview1 Add MCP functions in XncfRegisterBase class [2025-11-01] update basic libraries, including Senparc.AI + [2026-04-24] Simplify FunctionRender request parameters and keep SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease diff --git a/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/AdminChat/Chat.cshtml b/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/AdminChat/Chat.cshtml index 1d992f477..b246abf65 100644 --- a/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/AdminChat/Chat.cshtml +++ b/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/AdminChat/Chat.cshtml @@ -70,6 +70,24 @@ {{ currentSessionTitle || 'AI 智能助手' }}
+ + +
+ {{ item.name }} + {{ item.description }} +
+
+
+
+
+ AI 算力 +
+ + +
+ {{ item.name }} + {{ item.description }} +
+
+
+
+ + z.ParentId == null && + (string.Equals(z.MenuName, "系统管理", StringComparison.OrdinalIgnoreCase) + || string.Equals(z.MenuName, "System Management", StringComparison.OrdinalIgnoreCase))) + ?? dest.FirstOrDefault(z => + z.ParentId == null && + dest.Any(child => child.ParentId == z.Id && string.Equals(child.MenuName, "管理员管理", StringComparison.OrdinalIgnoreCase))); + + var adminChatParentId = systemManagementMenu?.Id; + var hasAdminChatMenu = dest.Any(z => string.Equals(z.Url, "/Admin/AdminChat/Chat", StringComparison.OrdinalIgnoreCase)); + + if (!hasAdminChatMenu) + { + dest.Insert(0,new SysMenuDto() + { + MenuName = "AI 智能助手", + Url = "/Admin/AdminChat/Chat", + Icon = "fa fa-comments-o", + Id = (index++).ToString(), + ParentId = adminChatParentId + }); + } + GetSysMenuTreesRecursive(dest, sysMenuTrees, null); return sysMenuTrees; } diff --git a/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/Shared/_MenuPartial.cshtml b/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/Shared/_MenuPartial.cshtml index 1693ad088..881a6ef3a 100644 --- a/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/Shared/_MenuPartial.cshtml +++ b/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/Shared/_MenuPartial.cshtml @@ -23,6 +23,7 @@ { return "current-page-xncf"; } + return ""; } diff --git a/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/XncfModule/Start.cshtml b/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/XncfModule/Start.cshtml index 8da8bfcd5..70b071116 100644 --- a/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/XncfModule/Start.cshtml +++ b/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/XncfModule/Start.cshtml @@ -197,7 +197,11 @@ v-model="runData[item.name].value" :maxlength="item.maxLength===0 ? 500 : item.maxLength" :type="item.parameterType===3 ? 'password' : 'text'"> - + diff --git a/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/XncfModule/Start.cshtml.cs b/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/XncfModule/Start.cshtml.cs index b084d059e..06d632d9d 100644 --- a/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/XncfModule/Start.cshtml.cs +++ b/tools/NcfSimulatedSite/Senparc.Areas.Admin/Areas/Admin/Pages/XncfModule/Start.cshtml.cs @@ -185,7 +185,8 @@ public async Task OnPostRunFunctionAsync([FromBody] ExecuteFuncPa switch (paramCount) { case 1: - var requestPara = SerializerHelper.GetObject(executeFuncParamDto2.XncfFunctionParams, functionParameterType) as IAppRequest; + var normalizedJson = FunctionRequestParameterNormalizer.NormalizeJson(executeFuncParamDto2.XncfFunctionParams, functionParameterType); + var requestPara = SerializerHelper.GetObject(normalizedJson, functionParameterType) as IAppRequest; paras = new[] { requestPara }; break; case 0: diff --git a/tools/NcfSimulatedSite/Senparc.Areas.Admin/Domain/Models/DatabaseModel/Dto/AdminChatMessageDto.cs b/tools/NcfSimulatedSite/Senparc.Areas.Admin/Domain/Models/DatabaseModel/Dto/AdminChatMessageDto.cs index 2ec5eeab4..4a07f466f 100644 --- a/tools/NcfSimulatedSite/Senparc.Areas.Admin/Domain/Models/DatabaseModel/Dto/AdminChatMessageDto.cs +++ b/tools/NcfSimulatedSite/Senparc.Areas.Admin/Domain/Models/DatabaseModel/Dto/AdminChatMessageDto.cs @@ -77,6 +77,11 @@ public class ChatMessageInputDto [Required] public int SessionId { get; set; } + /// + /// 选中的 AIModelId,0 表示系统级 SenparcAiSetting + /// + public int AiModelId { get; set; } + /// /// 消息内容 /// diff --git a/tools/NcfSimulatedSite/Senparc.Areas.Admin/Domain/Models/DatabaseModel/Dto/AdminChatSessionDto.cs b/tools/NcfSimulatedSite/Senparc.Areas.Admin/Domain/Models/DatabaseModel/Dto/AdminChatSessionDto.cs index cdca4063a..17a01dc4f 100644 --- a/tools/NcfSimulatedSite/Senparc.Areas.Admin/Domain/Models/DatabaseModel/Dto/AdminChatSessionDto.cs +++ b/tools/NcfSimulatedSite/Senparc.Areas.Admin/Domain/Models/DatabaseModel/Dto/AdminChatSessionDto.cs @@ -80,6 +80,11 @@ public class CreateChatSessionInputDto /// public string InitialMessage { get; set; } + /// + /// 选中的 AIModelId,0 表示系统级 SenparcAiSetting + /// + public int AiModelId { get; set; } + /// /// 关联的模块 UID 列表 /// diff --git a/tools/NcfSimulatedSite/Senparc.Areas.Admin/Domain/Services/AdminChatAiService.cs b/tools/NcfSimulatedSite/Senparc.Areas.Admin/Domain/Services/AdminChatAiService.cs index 403ed3adf..5224aaf6f 100644 --- a/tools/NcfSimulatedSite/Senparc.Areas.Admin/Domain/Services/AdminChatAiService.cs +++ b/tools/NcfSimulatedSite/Senparc.Areas.Admin/Domain/Services/AdminChatAiService.cs @@ -10,6 +10,9 @@ using Senparc.Ncf.Core.Exceptions; using Senparc.Ncf.Core.AppServices; using Senparc.Ncf.XncfBase; +using Senparc.Xncf.AIKernel.Domain.Models.DatabaseModel.Dto; +using Senparc.Xncf.AIKernel.Domain.Services; +using Senparc.Ncf.XncfBase.FunctionRenders; using System; using System.Collections.Generic; using System.Globalization; @@ -21,7 +24,8 @@ namespace Senparc.Areas.Admin.Domain.Services { /// - /// AdminChatAiService:管理后台聊天 AI 调用服务(直接使用 appsettings 的 SenparcAiSetting) + /// AdminChatAiService:管理后台聊天 AI 调用服务。 + /// 默认使用 appsettings 中的 SenparcAiSetting,也支持按请求切换到 AIKernel 中配置的 Chat 模型。 /// public class AdminChatAiService { @@ -55,19 +59,13 @@ public AdminChatAiService( /// 会话 Id。 /// 用户 Id。 /// 用户输入消息。 + /// 可选 AIModelId,0 表示系统级 SenparcAiSetting。 /// 返回回复文本与模型标识。 - public async Task<(string response, string modelIdentifier)> GenerateResponseAsync(int sessionId, int userId, string userMessage) + public async Task<(string response, string modelIdentifier)> GenerateResponseAsync(int sessionId, int userId, string userMessage, int aiModelId = 0) { - var setting = Senparc.AI.Config.SenparcAiSetting as SenparcAiSetting; - if (setting == null) - { - throw new NcfExceptionBase("未读取到 SenparcAiSetting,请检查 appsettings.json 配置。"); - } + var showLoadedFunctionsInConsole = true;//是否输出 function 的 schema 信息到控制台,便于调试和验证 Function Calling 功能是否正确加载了函数 - if (setting.AiPlatform == AiPlatform.UnSet) - { - throw new NcfExceptionBase("SenparcAiSetting.AiPlatform 仍为 UnSet,请先在 appsettings.json 中设置可用平台。"); - } + var (setting, modelIdentifier) = await ResolveChatSettingAsync(aiModelId); var (messages, _) = await _messageService.GetSessionMessagesAsync(sessionId); var modules = await _sessionModuleService.GetSessionModulesAsync(sessionId); @@ -106,6 +104,7 @@ public AdminChatAiService( var importedFunctionCount = 0; var importedFunctionSignatures = new List(); + var loadedFunctionDebugLines = new List(); foreach (var pluginGroup in functionPluginGroups) { @@ -134,7 +133,6 @@ public AdminChatAiService( var kernelFunction = KernelFunctionFactory.CreateFromMethod(functionBag.MethodInfo, plugin, options); kernelFunctions.Add(kernelFunction); - importedFunctionSignatures.Add($"{pluginName}.{functionBag.MethodInfo.Name}({functionBag.FunctionRenderAttribute?.Description ?? "N/A"})"); } catch (Exception ex) { @@ -151,9 +149,11 @@ public AdminChatAiService( continue; } - iWantToRun.Kernel.Plugins.AddFromFunctions(pluginName, kernelFunctions); + var addedPlugin = iWantToRun.Kernel.Plugins.AddFromFunctions(pluginName, kernelFunctions); + importedFunctionSignatures.AddRange(addedPlugin.Select(kernelFunction => $"{kernelFunction.Metadata.PluginName}.{kernelFunction.Metadata.Name}({kernelFunction.Metadata.Description ?? "N/A"})")); + loadedFunctionDebugLines.AddRange(BuildKernelPluginDebugLines(addedPlugin)); importedPluginNames.Add(pluginName); - importedFunctionCount += kernelFunctions.Count; + importedFunctionCount += addedPlugin.Count(); } catch (Exception ex) { @@ -170,6 +170,13 @@ public AdminChatAiService( moduleUids.Count, string.Join(",", moduleUids)); } + else + { + if (showLoadedFunctionsInConsole) + { + WriteLoadedFunctionsToConsole(sessionId, userId, loadedFunctionDebugLines); + } + } _logger.LogInformation( "AdminChat FunctionCalling 插件加载完成:SessionId={SessionId}, UserId={UserId}, ModuleCount={ModuleCount}, Modules={Modules}, Plugins={Plugins}, Functions={Functions}, FunctionList={FunctionList}", @@ -197,7 +204,70 @@ public AdminChatAiService( result = "抱歉,我暂时没有生成有效回复,请稍后再试。"; } - return (result, ResolveModelIdentifier(setting)); + return (result, modelIdentifier); + } + + private async Task<(SenparcAiSetting setting, string modelIdentifier)> ResolveChatSettingAsync(int aiModelId) + { + var defaultSetting = Senparc.AI.Config.SenparcAiSetting as SenparcAiSetting; + if (defaultSetting == null) + { + throw new NcfExceptionBase("未读取到 SenparcAiSetting,请检查 appsettings.json 配置。"); + } + + if (defaultSetting.AiPlatform == AiPlatform.UnSet) + { + throw new NcfExceptionBase("SenparcAiSetting.AiPlatform 仍为 UnSet,请先在 appsettings.json 中设置可用平台。"); + } + + if (aiModelId <= 0) + { + return (defaultSetting, ResolveModelIdentifier(defaultSetting)); + } + + if (!await IsAiKernelAvailableAsync()) + { + throw new NcfExceptionBase("当前系统未安装或未启用 AIKernel 模块,无法切换到指定 AI 模型。"); + } + + var aiModelService = _serviceProvider.GetService(typeof(AIModelService)) as AIModelService; + if (aiModelService == null) + { + throw new NcfExceptionBase("未能解析 AIModelService,无法加载指定 AI 模型。"); + } + + var aiModel = await aiModelService.GetObjectAsync(z => z.Id == aiModelId); + if (aiModel == null) + { + throw new NcfExceptionBase($"当前选择的 AI 模型不存在:{aiModelId}"); + } + + if (aiModel.ConfigModelType != Senparc.Xncf.AIKernel.Domain.Models.ConfigModelType.Chat) + { + throw new NcfExceptionBase($"当前选择的 AI 模型不是 Chat 类型:{aiModelId}"); + } + + var aiModelDto = aiModelService.Mapper.Map(aiModel); + var selectedSetting = aiModelService.BuildSenparcAiSetting(aiModelDto); + var selectedModelIdentifier = !string.IsNullOrWhiteSpace(aiModelDto.Alias) + ? $"{aiModelDto.Alias} [{ResolveModelIdentifier(selectedSetting)}]" + : ResolveModelIdentifier(selectedSetting); + + return (selectedSetting, selectedModelIdentifier); + } + + private async Task IsAiKernelAvailableAsync() + { + var aiKernelRegister = XncfRegisterManager.RegisterList.FirstOrDefault(z => + string.Equals(z.Name, "Senparc.Xncf.AIKernel", StringComparison.OrdinalIgnoreCase)); + + if (aiKernelRegister == null) + { + return false; + } + + var registerManager = new XncfRegisterManager(_serviceProvider); + return await registerManager.CheckXncfAvailable(aiKernelRegister); } private static string BuildFunctionPluginName(Type pluginType) @@ -225,6 +295,93 @@ private static string ComputeShortHash(string input, int length) return hex.Length > length ? hex.Substring(0, length) : hex; } + private static void WriteLoadedFunctionsToConsole(int sessionId, int userId, List loadedFunctionDebugLines) + { + if (loadedFunctionDebugLines == null || loadedFunctionDebugLines.Count == 0) + { + return; + } + + Console.WriteLine($"[AdminChat Functions] SessionId={sessionId}, UserId={userId}, LoadedFunctions={loadedFunctionDebugLines.Count(line => line.StartsWith("- Function:", StringComparison.Ordinal))}"); + foreach (var line in loadedFunctionDebugLines) + { + Console.WriteLine(line); + } + } + + private static List BuildKernelPluginDebugLines(KernelPlugin kernelPlugin) + { + var lines = new List(); + foreach (var kernelFunction in kernelPlugin) + { + lines.AddRange(BuildKernelFunctionDebugLines(kernelFunction)); + } + + return lines; + } + + private static List BuildKernelFunctionDebugLines(KernelFunction kernelFunction) + { + var metadata = kernelFunction.Metadata; + var functionParameters = metadata.Parameters?.ToList() ?? new List(); + var lines = new List + { + $"- Function: {metadata.PluginName}.{metadata.Name}", + $" Description: {metadata.Description ?? "(none)"}", + $" ReturnType: {metadata.ReturnParameter.ParameterType?.FullName ?? "(none)"}", + $" ReturnSchema: {FormatSchema(metadata.ReturnParameter.Schema)}" + }; + + if (functionParameters == null || functionParameters.Count == 0) + { + lines.Add(" Parameters: (none)"); + return lines; + } + + lines.Add($" Parameters: {functionParameters.Count}"); + foreach (var parameter in functionParameters) + { + lines.Add($" - {parameter.Name}: type={parameter.ParameterType?.FullName ?? "(none)"}, required={parameter.IsRequired}, description={FormatInlineValue(parameter.Description)}, default={FormatParameterValue(parameter.DefaultValue)}, schema={FormatSchema(parameter.Schema)}"); + } + + return lines; + } + + private static string FormatParameterValue(object value) + { + if (value == null) + { + return "(null)"; + } + + if (value is string stringValue) + { + return FormatInlineValue(stringValue); + } + + if (value is IEnumerable stringValues) + { + return $"[{string.Join(", ", stringValues.Select(FormatInlineValue))}]"; + } + + return FormatInlineValue(value.ToString()); + } + + private static string FormatSchema(KernelJsonSchema schema) + { + return schema == null ? "(none)" : FormatInlineValue(schema.ToString()); + } + + private static string FormatInlineValue(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return "(empty)"; + } + + return value.Replace("\r", " ").Replace("\n", " ").Trim(); + } + private static string BuildSystemMessage(List modules) { var sb = new StringBuilder(); diff --git a/tools/NcfSimulatedSite/Senparc.Areas.Admin/OHS/Local/AppService/AdminChatAppService.cs b/tools/NcfSimulatedSite/Senparc.Areas.Admin/OHS/Local/AppService/AdminChatAppService.cs index 6a8c54f87..2fe3049b2 100644 --- a/tools/NcfSimulatedSite/Senparc.Areas.Admin/OHS/Local/AppService/AdminChatAppService.cs +++ b/tools/NcfSimulatedSite/Senparc.Areas.Admin/OHS/Local/AppService/AdminChatAppService.cs @@ -10,6 +10,7 @@ using Senparc.Ncf.Core.Models; using Senparc.Ncf.XncfBase; using Microsoft.Extensions.Localization; +using Senparc.Xncf.AIKernel.Domain.Services; using System; using System.Collections.Generic; using System.Linq; @@ -94,7 +95,7 @@ await _messageService.AddMessageAsync( ChatMessageRoleType.User, request.InitialMessage); - var (aiResponse, modelIdentifier) = await _chatAiService.GenerateResponseAsync(session.Id, userId, request.InitialMessage); + var (aiResponse, modelIdentifier) = await _chatAiService.GenerateResponseAsync(session.Id, userId, request.InitialMessage, request.AiModelId); await _messageService.AddMessageAsync( session.Id, ChatMessageRoleType.Assistant, @@ -226,7 +227,7 @@ public async Task> SendMessageAsync([FromBo await _sessionService.UpdateLastMessageTimeAsync(request.SessionId); - var (aiResponse, modelIdentifier) = await _chatAiService.GenerateResponseAsync(request.SessionId, userId, request.Content); + var (aiResponse, modelIdentifier) = await _chatAiService.GenerateResponseAsync(request.SessionId, userId, request.Content, request.AiModelId); var assistantMessage = await _messageService.AddMessageAsync( request.SessionId, @@ -392,6 +393,68 @@ public async Task> GetSessionModulesA }); } + /// + /// 获取 Chat 可选 AI 模型列表。 + /// + [ApiBind(ApiRequestMethod = ApiRequestMethod.Get)] + public async Task> GetAiModelOptionsAsync() + { + return await this.GetResponseAsync, GetAiModelOptionsResponse>(async (response, logger) => + { + var optionList = new List + { + new AiModelOptionDto + { + Id = 0, + Name = "系统级 SenparcAiSetting", + Description = "使用 appsettings.json 中当前生效的默认 Chat 配置", + IsDefault = true + } + }; + + var aiKernelRegister = XncfRegisterManager.RegisterList.FirstOrDefault(z => + string.Equals(z.Name, "Senparc.Xncf.AIKernel", StringComparison.OrdinalIgnoreCase)); + + var aiKernelAvailable = false; + if (aiKernelRegister != null) + { + var registerManager = new XncfRegisterManager(ServiceProvider); + aiKernelAvailable = await registerManager.CheckXncfAvailable(aiKernelRegister); + } + + if (aiKernelAvailable) + { + var aiModelService = ServiceProvider.GetService(typeof(AIModelService)) as AIModelService; + if (aiModelService != null) + { + var aiModels = await aiModelService.GetFullListAsync(z => + z.Show && z.ConfigModelType == Senparc.Xncf.AIKernel.Domain.Models.ConfigModelType.Chat); + + optionList.AddRange(aiModels + .OrderByDescending(z => z.Show) + .ThenBy(z => z.Alias) + .Select(z => new AiModelOptionDto + { + Id = z.Id, + Name = !string.IsNullOrWhiteSpace(z.Alias) + ? $"{z.Alias} ({z.ModelId})" + : $"{z.DeploymentName} ({z.ModelId})", + Description = string.IsNullOrWhiteSpace(z.Note) + ? $"{z.AiPlatform} | {z.Endpoint}" + : z.Note, + IsDefault = false + })); + } + } + + return new GetAiModelOptionsResponse + { + AiKernelAvailable = aiKernelAvailable, + Models = optionList + }; + }); + } + private static AdminChatSessionModuleDto MapModuleDtoWithRegisterInfo(AdminChatSessionModuleDto dto) { if (dto == null) @@ -497,5 +560,25 @@ public class GetSessionModulesResponse public List Modules { get; set; } } + /// + /// 获取 AI 模型选项响应 + /// + public class GetAiModelOptionsResponse + { + public bool AiKernelAvailable { get; set; } + public List Models { get; set; } + } + + /// + /// AI 模型选项 + /// + public class AiModelOptionDto + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public bool IsDefault { get; set; } + } + #endregion } diff --git a/tools/NcfSimulatedSite/Senparc.Areas.Admin/OHS/Local/AppService/ModuleAppService.cs b/tools/NcfSimulatedSite/Senparc.Areas.Admin/OHS/Local/AppService/ModuleAppService.cs index 639542d7f..00e0ab078 100644 --- a/tools/NcfSimulatedSite/Senparc.Areas.Admin/OHS/Local/AppService/ModuleAppService.cs +++ b/tools/NcfSimulatedSite/Senparc.Areas.Admin/OHS/Local/AppService/ModuleAppService.cs @@ -438,7 +438,8 @@ public async Task> RunFunctionAsync( switch (paramCount) { case 1: - var requestPara = SerializerHelper.GetObject(executeFuncParamDto2.XncfFunctionParams, functionParameterType) as IAppRequest; + var normalizedJson = FunctionRequestParameterNormalizer.NormalizeJson(executeFuncParamDto2.XncfFunctionParams, functionParameterType); + var requestPara = SerializerHelper.GetObject(normalizedJson, functionParameterType) as IAppRequest; paras = new[] { requestPara }; break; case 0: diff --git a/tools/NcfSimulatedSite/Senparc.Areas.Admin/wwwroot/js/Admin/Pages/AdminChat/Chat.js b/tools/NcfSimulatedSite/Senparc.Areas.Admin/wwwroot/js/Admin/Pages/AdminChat/Chat.js index 226f55e20..f3f208440 100644 --- a/tools/NcfSimulatedSite/Senparc.Areas.Admin/wwwroot/js/Admin/Pages/AdminChat/Chat.js +++ b/tools/NcfSimulatedSite/Senparc.Areas.Admin/wwwroot/js/Admin/Pages/AdminChat/Chat.js @@ -6,6 +6,10 @@ var chatApp = new Vue({ currentSessionId: 0, currentSessionTitle: '', currentSessionModules: [], + currentSessionAiModelId: 0, + aiModelOptions: [], + aiKernelAvailable: false, + loadingAiModelOptions: false, sessionList: [], messageList: [], inputMessage: '', @@ -34,6 +38,10 @@ var chatApp = new Vue({ } } + this.currentSessionAiModelId = this.getSessionAiModelId(this.currentSessionId); + this.launcherAiModelId = this.currentSessionAiModelId; + + this.loadAiModelOptions(); this.loadSessionList(); if (this.currentSessionId > 0) { @@ -41,6 +49,30 @@ var chatApp = new Vue({ } }, methods: { + async loadAiModelOptions() { + this.loadingAiModelOptions = true; + try { + const response = await service.get('/api/Senparc.Areas.Admin/AdminChatAppService/Areas.Admin_AdminChatAppService.GetAiModelOptionsAsync'); + + if (response.data && response.data.success && response.data.data) { + this.aiKernelAvailable = !!response.data.data.aiKernelAvailable; + this.aiModelOptions = response.data.data.models || []; + } else { + this.aiModelOptions = [{ id: 0, name: '系统级 SenparcAiSetting', description: '使用 appsettings.json 中当前生效的默认 Chat 配置', isDefault: true }]; + } + } catch (error) { + console.error('加载 AI 模型失败:', error); + this.aiModelOptions = [{ id: 0, name: '系统级 SenparcAiSetting', description: '使用 appsettings.json 中当前生效的默认 Chat 配置', isDefault: true }]; + } finally { + this.loadingAiModelOptions = false; + } + }, + + handleCurrentSessionAiModelChange(value) { + this.currentSessionAiModelId = this.normalizeAiModelId(value); + this.setSessionAiModelId(this.currentSessionId, this.currentSessionAiModelId); + }, + handleChatInputKeydown(event) { // 保持与首页一致:Ctrl+Enter (Windows/Linux) 或 Cmd+Enter (Mac) 发送。 if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { @@ -80,6 +112,7 @@ var chatApp = new Vue({ this.currentSessionTitle = session.title; this.messageList = session.messages || []; this.currentSessionModules = session.modules || []; + this.currentSessionAiModelId = this.getSessionAiModelId(this.currentSessionId); this.clearMessageSelection(); this.isManageMode = false; @@ -131,6 +164,7 @@ var chatApp = new Vue({ try { const requestData = { sessionId: this.currentSessionId, + aiModelId: this.normalizeAiModelId(this.currentSessionAiModelId), content: messageContent }; @@ -194,6 +228,7 @@ var chatApp = new Vue({ if (this.currentSessionId === sessionId) return; this.currentSessionId = sessionId; + this.currentSessionAiModelId = this.getSessionAiModelId(sessionId); this.messageList = []; this.currentSessionModules = []; this.clearMessageSelection(); @@ -202,6 +237,7 @@ var chatApp = new Vue({ }, async createNewSession() { + this.launcherAiModelId = this.currentSessionAiModelId || this.launcherAiModelId || 0; this.currentSessionId = 0; this.currentSessionTitle = ''; this.currentSessionModules = []; diff --git a/tools/NcfSimulatedSite/Senparc.Areas.Admin/wwwroot/js/Admin/Pages/AdminChat/ChatLauncherMixin.js b/tools/NcfSimulatedSite/Senparc.Areas.Admin/wwwroot/js/Admin/Pages/AdminChat/ChatLauncherMixin.js index f696a78de..a805e7e60 100644 --- a/tools/NcfSimulatedSite/Senparc.Areas.Admin/wwwroot/js/Admin/Pages/AdminChat/ChatLauncherMixin.js +++ b/tools/NcfSimulatedSite/Senparc.Areas.Admin/wwwroot/js/Admin/Pages/AdminChat/ChatLauncherMixin.js @@ -2,7 +2,10 @@ window.ChatLauncherMixin = { data() { return { moduleStorageKey: 'ncf.admin.chat.selectedModuleUids', + aiModelStorageKey: 'ncf.admin.chat.sessionAiModelMap', chatInputText: '', + launcherAiModelId: 0, + sessionAiModelMap: {}, selectedModules: [], moduleSelectorVisible: false, moduleSearchKeyword: '', @@ -65,6 +68,7 @@ window.ChatLauncherMixin = { }, mounted() { this.restoreSelectedModuleUids(); + this.restoreSessionAiModelMap(); this.ensureModuleOptionsLoaded(false); }, watch: { @@ -109,6 +113,57 @@ window.ChatLauncherMixin = { } }, + normalizeAiModelId(value) { + const parsedValue = Number.parseInt(value, 10); + return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : 0; + }, + + persistSessionAiModelMap() { + try { + localStorage.setItem(this.aiModelStorageKey, JSON.stringify(this.sessionAiModelMap || {})); + } catch (error) { + console.warn('保存会话 AI 模型失败:', error); + } + }, + + restoreSessionAiModelMap() { + try { + const raw = localStorage.getItem(this.aiModelStorageKey); + if (!raw) { + return; + } + + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return; + } + + this.sessionAiModelMap = Object.keys(parsed).reduce((result, key) => { + result[key] = this.normalizeAiModelId(parsed[key]); + return result; + }, {}); + } catch (error) { + console.warn('读取会话 AI 模型失败:', error); + } + }, + + setSessionAiModelId(sessionId, aiModelId) { + if (!sessionId) { + return; + } + + this.$set(this.sessionAiModelMap, String(sessionId), this.normalizeAiModelId(aiModelId)); + this.persistSessionAiModelMap(); + }, + + getSessionAiModelId(sessionId) { + if (!sessionId) { + return 0; + } + + return this.normalizeAiModelId(this.sessionAiModelMap[String(sessionId)]); + }, + syncSelectedModulesFromUids() { const uidSet = new Set(this.selectedModuleUids); this.selectedModules = this.availableModules.filter((item) => uidSet.has(item.uid)); @@ -223,12 +278,14 @@ window.ChatLauncherMixin = { try { const requestData = { initialMessage: this.chatInputText.trim(), + aiModelId: this.normalizeAiModelId(this.launcherAiModelId), moduleUids: this.selectedModules.map((item) => item.uid) }; const response = await service.post('/api/Senparc.Areas.Admin/AdminChatAppService/Areas.Admin_AdminChatAppService.CreateSessionAsync', requestData); if (response.data && response.data.success && response.data.data) { const sessionId = response.data.data.sessionId; + this.setSessionAiModelId(sessionId, this.launcherAiModelId); window.location.href = '/Admin/AdminChat/Chat?sessionId=' + sessionId; return; } diff --git a/tools/NcfSimulatedSite/Senparc.Areas.Admin/wwwroot/js/Admin/Pages/XncfModule/start.js b/tools/NcfSimulatedSite/Senparc.Areas.Admin/wwwroot/js/Admin/Pages/XncfModule/start.js index 1eb94fd61..6a1fba17a 100644 --- a/tools/NcfSimulatedSite/Senparc.Areas.Admin/wwwroot/js/Admin/Pages/XncfModule/start.js +++ b/tools/NcfSimulatedSite/Senparc.Areas.Admin/wwwroot/js/Admin/Pages/XncfModule/start.js @@ -49,6 +49,20 @@ this.getList(); }, methods: { + normalizeMultiValue(value) { + if (Array.isArray(value)) { + return value; + } + + if (typeof value !== 'string' || value.length === 0) { + return []; + } + + return value + .split(/[;,,;\n\r|]+/) + .map(item => item.trim()) + .filter(item => item.length > 0); + }, async getList() { const uid = resizeUrl().uid; const res = await service.get(`/Admin/XncfModule/Start?handler=Detail&uid=${uid}`); @@ -92,10 +106,10 @@ // 多选 if (res.parameterType === 2 && res.selectionList.items) { this.runData[res.name] = {}; - this.runData[res.name].value = []; + this.runData[res.name].value = this.normalizeMultiValue(res.value); this.runData[res.name].item = res; res.selectionList.items.map(ele => { - if (ele.defaultSelected) { + if (ele.defaultSelected && this.runData[res.name].value.indexOf(ele.value) < 0) { this.runData[res.name].value.push(ele.value); } }); @@ -103,10 +117,10 @@ // 下拉框value if (res.parameterType === 1 && res.selectionList.items) { this.runData[res.name] = {}; - this.runData[res.name].value = ''; + this.runData[res.name].value = res.value || ''; this.runData[res.name].item = res; res.selectionList.items.map(ele => { - if (ele.defaultSelected) { + if (!this.runData[res.name].value && ele.defaultSelected) { this.runData[res.name].value = ele.value; } }); @@ -172,13 +186,10 @@ }); return; } else { - xncfFunctionParams[i] = {}; - xncfFunctionParams[i].SelectedValues = []; - xncfFunctionParams[i].SelectedValues = this.runData[i].value; - + xncfFunctionParams[i] = this.runData[i].value; } } - // 下拉框value为字符串,但接口要数组 + // 下拉框 if (this.runData[i].item.parameterType === 1) { if (this.runData[i].item.isRequired && this.runData[i].value.length === 0) { this.$notify({ @@ -188,9 +199,7 @@ }); return; } else { - xncfFunctionParams[i] = {}; - xncfFunctionParams[i].SelectedValues = []; - xncfFunctionParams[i].SelectedValues[0] = this.runData[i].value; + xncfFunctionParams[i] = this.runData[i].value; } } // 输入框 diff --git a/tools/NcfSimulatedSite/Senparc.Web/appsettings.json b/tools/NcfSimulatedSite/Senparc.Web/appsettings.json index 28ec5c0d4..80a40ebc3 100644 --- a/tools/NcfSimulatedSite/Senparc.Web/appsettings.json +++ b/tools/NcfSimulatedSite/Senparc.Web/appsettings.json @@ -53,14 +53,14 @@ //Senparc.AI 设置 "SenparcAiSetting": { "IsDebug": true, - "AiPlatform": "UnSet", //注意修改为自己平台对应的枚举值 + "AiPlatform": "NeuCharAI", //注意修改为自己平台对应的枚举值 "VectorDB": { "Type": "Memory", "ConnectionString": "" }, "NeuCharAIKeys": { "ApiKey": "", //在 https://www.neuchar.com/Developer/AiApp 申请 - "NeuCharEndpoint": "https://www.neuchar.com/", //查看 ApiKey 时可看到 DeveloperId + "NeuCharEndpoint": "https://www.neuchar.com/2", //查看 ApiKey 时可看到 DeveloperId "ModelName": { "Chat": "gpt-35-turbo", "Embedding": "text-embedding-ada-002", diff --git a/tools/NcfSimulatedSite/Senparc.Xncf.Accounts/OHS/Local/PL/MyFunctionRequest.cs b/tools/NcfSimulatedSite/Senparc.Xncf.Accounts/OHS/Local/PL/MyFunctionRequest.cs index 68a0d900e..71d5dc2fb 100644 --- a/tools/NcfSimulatedSite/Senparc.Xncf.Accounts/OHS/Local/PL/MyFunctionRequest.cs +++ b/tools/NcfSimulatedSite/Senparc.Xncf.Accounts/OHS/Local/PL/MyFunctionRequest.cs @@ -1,7 +1,9 @@ using Senparc.Ncf.XncfBase.FunctionRenders; +using Senparc.Ncf.XncfBase; using Senparc.Ncf.XncfBase.Functions; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace Senparc.Xncf.Accounts.OHS.Local.PL { @@ -22,7 +24,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase public int Number2 { get; set; } [Description("运算符||")]//下拉列表 - public SelectionList Operator { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { + [FunctionParameterUi(ParameterType.DropDownList, nameof(OperatorOptions))] + public string Operator { get; set; } + + [JsonIgnore] + public SelectionList OperatorOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { new SelectionItem("+","加法","数字1 + 数字2",false), new SelectionItem("-","减法","数字1 - 数字2",true), new SelectionItem("×","乘法","数字1 × 数字2",false), @@ -30,7 +36,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase }); [Description("计算平方||")]//多选框 - public SelectionList Power { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(PowerOptions))] + public string[] Power { get; set; } + + [JsonIgnore] + public SelectionList PowerOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("2","平方","计算上述结果之后再计算平方",false), new SelectionItem("3","三次方","计算上述结果之后再计算三次方",false) }); diff --git a/tools/NcfSimulatedSite/Senparc.Xncf.Accounts/Senparc.Xncf.Accounts.csproj b/tools/NcfSimulatedSite/Senparc.Xncf.Accounts/Senparc.Xncf.Accounts.csproj index d4585e748..13c144d96 100644 --- a/tools/NcfSimulatedSite/Senparc.Xncf.Accounts/Senparc.Xncf.Accounts.csproj +++ b/tools/NcfSimulatedSite/Senparc.Xncf.Accounts/Senparc.Xncf.Accounts.csproj @@ -1,7 +1,7 @@  net8.0 - 0.1 + 0.2 Senparc.Xncf.Accounts Senparc.Xncf.Accounts true @@ -18,6 +18,7 @@ https://sdk.weixin.senparc.com/Images/logo-square-ncf.jpg--> v0.1 创世 + [2026-04-24] Simplify FunctionRender request parameters and keep SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease;Debug-Dapr;NcfDebugForPromptRange;ModifyPublish diff --git a/tools/NcfSimulatedSite/Template_OrgName.Xncf.Template_XncfName/OHS/Local/AppService/MyFuctionAppService.cs b/tools/NcfSimulatedSite/Template_OrgName.Xncf.Template_XncfName/OHS/Local/AppService/MyFuctionAppService.cs index 4391cfabd..1571508c3 100644 --- a/tools/NcfSimulatedSite/Template_OrgName.Xncf.Template_XncfName/OHS/Local/AppService/MyFuctionAppService.cs +++ b/tools/NcfSimulatedSite/Template_OrgName.Xncf.Template_XncfName/OHS/Local/AppService/MyFuctionAppService.cs @@ -37,7 +37,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques */ double calcResult = request.Number1; - var theOperator = request.Operator.SelectedValues.FirstOrDefault(); + var theOperator = request.Operator; switch (theOperator) { case "+": @@ -68,7 +68,7 @@ public async Task Calculate(MyFunction_CaculateRequest reques Action raisePower = power => { - if (request.Power.SelectedValues.Contains(power.ToString())) + if ((request.Power ?? Array.Empty()).Contains(power.ToString())) { var oldValue = calcResult; calcResult = Math.Pow(calcResult, power); diff --git a/tools/NcfSimulatedSite/Template_OrgName.Xncf.Template_XncfName/OHS/Local/PL/MyFunctionRequest.cs b/tools/NcfSimulatedSite/Template_OrgName.Xncf.Template_XncfName/OHS/Local/PL/MyFunctionRequest.cs index 020e7db31..61feb5107 100644 --- a/tools/NcfSimulatedSite/Template_OrgName.Xncf.Template_XncfName/OHS/Local/PL/MyFunctionRequest.cs +++ b/tools/NcfSimulatedSite/Template_OrgName.Xncf.Template_XncfName/OHS/Local/PL/MyFunctionRequest.cs @@ -1,7 +1,9 @@ using Senparc.Ncf.XncfBase.FunctionRenders; +using Senparc.Ncf.XncfBase; using Senparc.Ncf.XncfBase.Functions; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace Template_OrgName.Xncf.Template_XncfName.OHS.Local.PL { @@ -22,7 +24,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase public int Number2 { get; set; } [Description("运算符||")]//下拉列表 - public SelectionList Operator { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { + [FunctionParameterUi(ParameterType.DropDownList, nameof(OperatorOptions))] + public string Operator { get; set; } + + [JsonIgnore] + public SelectionList OperatorOptions { get; set; } = new SelectionList(SelectionType.DropDownList, new[] { new SelectionItem("+","加法","数字1 + 数字2",false), new SelectionItem("-","减法","数字1 - 数字2",true), new SelectionItem("×","乘法","数字1 × 数字2",false), @@ -30,7 +36,11 @@ public class MyFunction_CaculateRequest: FunctionAppRequestBase }); [Description("计算平方||")]//多选框 - public SelectionList Power { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { + [FunctionParameterUi(ParameterType.CheckBoxList, nameof(PowerOptions))] + public string[] Power { get; set; } + + [JsonIgnore] + public SelectionList PowerOptions { get; set; } = new SelectionList(SelectionType.CheckBoxList, new[] { new SelectionItem("2","平方","计算上述结果之后再计算平方",false), new SelectionItem("3","三次方","计算上述结果之后再计算三次方",false) }); diff --git a/tools/NcfSimulatedSite/Template_OrgName.Xncf.Template_XncfName/Template_OrgName.Xncf.Template_XncfName.csproj b/tools/NcfSimulatedSite/Template_OrgName.Xncf.Template_XncfName/Template_OrgName.Xncf.Template_XncfName.csproj index 005297ef3..96f218bd0 100644 --- a/tools/NcfSimulatedSite/Template_OrgName.Xncf.Template_XncfName/Template_OrgName.Xncf.Template_XncfName.csproj +++ b/tools/NcfSimulatedSite/Template_OrgName.Xncf.Template_XncfName/Template_OrgName.Xncf.Template_XncfName.csproj @@ -1,7 +1,7 @@ net8.0 - 1.0.0 + 1.0.1 Template_OrgName.Xncf.Template_XncfName Template_OrgName.Xncf.Template_XncfName true @@ -19,6 +19,7 @@ icon.jpg v1.0.0 创世 + [2026-04-24] Update FunctionRender request templates for simplified values and SelectionList UI metadata compatibility https://github.com/NeuCharFramework/NcfPackageSources Debug;Release;Test;TemplateRelease