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 d0d1ba497..8d89cd1b6 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 @@ -1101,6 +1101,74 @@ + @* AI 对话标签页 *@ + +
+ @* AI 对话头部工具栏 *@ +
+ AI 模型: + + + + 清空对话 + 💡 通过自然语言描述你的任务,AI 将自动匹配或创建智能体组来执行 +
+ @* 消息列表区域 *@ +
+
+ +

和 AI 对话来启动 Agents 任务

+

例如:帮我分析一段代码,或者写一篇文章

+
+ +
+ @* 输入区域 *@ +
+ + +
+ 发送 +
Ctrl+Enter
+
+
+
+
@* 抽屉 新增|编辑 智能体 540px *@ diff --git a/src/Extensions/Senparc.Xncf.AgentsManager/OHS/Local/AppService/AiChatAppService.cs b/src/Extensions/Senparc.Xncf.AgentsManager/OHS/Local/AppService/AiChatAppService.cs new file mode 100644 index 000000000..c2a2a7879 --- /dev/null +++ b/src/Extensions/Senparc.Xncf.AgentsManager/OHS/Local/AppService/AiChatAppService.cs @@ -0,0 +1,440 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel.ChatCompletion; +using Senparc.AI; +using Senparc.AI.Entities; +using Senparc.AI.Kernel; +using Senparc.AI.Kernel.Handlers; +using Senparc.CO2NET; +using Senparc.CO2NET.WebApi; +using Senparc.Ncf.Core.AppServices; +using Senparc.Ncf.Core.Exceptions; +using Senparc.Xncf.AgentsManager.Domain.Services; +using Senparc.Xncf.AgentsManager.Models.DatabaseModel; +using Senparc.Xncf.AgentsManager.Models.DatabaseModel.Models; +using Senparc.Xncf.AgentsManager.Models.DatabaseModel.Models.Dto; +using Senparc.Xncf.AgentsManager.OHS.Local.PL; +using Senparc.Xncf.AIKernel.Domain.Models.DatabaseModel.Dto; +using Senparc.Xncf.AIKernel.Domain.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Senparc.Xncf.AgentsManager.OHS.Local.AppService +{ + /// + /// AI 对话 AppService,用于通过自然语言对话启动 AgentsManager 任务 + /// + public class AiChatAppService : AppServiceBase + { + private readonly ChatGroupService _chatGroupService; + private readonly ChatGroupMemberService _chatGroupMemberService; + private readonly AgentsTemplateService _agentsTemplateService; + private readonly AIModelService _aiModelService; + + public AiChatAppService( + IServiceProvider serviceProvider, + ChatGroupService chatGroupService, + ChatGroupMemberService chatGroupMemberService, + AgentsTemplateService agentsTemplateService, + AIModelService aiModelService) : base(serviceProvider) + { + _chatGroupService = chatGroupService; + _chatGroupMemberService = chatGroupMemberService; + _agentsTemplateService = agentsTemplateService; + _aiModelService = aiModelService; + } + + /// + /// 发送 AI 对话消息,AI 将理解用户意图并自动匹配或建议创建 ChatGroup 来运行任务 + /// + [ApiBind(ApiRequestMethod = ApiRequestMethod.Post)] + public async Task> SendMessage(AiChat_SendMessageRequest request) + { + return await this.GetResponseAsync(async (response, logger) => + { + if (string.IsNullOrWhiteSpace(request.UserMessage)) + { + throw new NcfExceptionBase("消息内容不能为空"); + } + + // 获取 AI 设置 + var aiSetting = Senparc.AI.Config.SenparcAiSetting; + if (request.AiModelId > 0) + { + var aiModel = await _aiModelService.GetObjectAsync(z => z.Id == request.AiModelId); + if (aiModel == null) + { + throw new NcfExceptionBase($"当前选择的 AI 模型不存在:{request.AiModelId}"); + } + var aiModelDto = _aiModelService.Mapper.Map(aiModel); + aiSetting = _aiModelService.BuildSenparcAiSetting(aiModelDto); + } + + // 获取所有可用的 AgentTemplate 列表 + var agentTemplates = await _agentsTemplateService.GetFullListAsync(z => z.Enable, z => z.Id, Ncf.Core.Enums.OrderingType.Ascending); + var agentTemplateList = agentTemplates.Select(z => new + { + z.Id, + z.Name, + z.Description, + z.SystemMessage + }).ToList(); + + // 获取所有现有 ChatGroup 列表 + var chatGroups = await _chatGroupService.GetFullListAsync(z => true, z => z.Id, Ncf.Core.Enums.OrderingType.Ascending); + var chatGroupList = new List(); + foreach (var cg in chatGroups) + { + var members = await _chatGroupMemberService.GetFullListAsync(z => z.ChatGroupId == cg.Id); + var memberIds = members.Select(z => z.AgentTemplateId).ToList(); + var memberNames = agentTemplates.Where(a => memberIds.Contains(a.Id)).Select(a => a.Name).ToList(); + chatGroupList.Add(new + { + cg.Id, + cg.Name, + cg.Description, + AdminAgentTemplateId = cg.AdminAgentTemplateId, + EnterAgentTemplateId = cg.EnterAgentTemplateId, + Members = string.Join("、", memberNames) + }); + } + + // 构建系统 Prompt + var systemPrompt = BuildSystemPrompt(agentTemplateList, chatGroupList); + + // 构建消息历史 + var promptConfigParameter = new PromptConfigParameter() + { + MaxTokens = 2000, + Temperature = 0.3, + TopP = 0.5, + }; + + var semanticAiHandler = new SemanticAiHandler((SenparcAiSetting)aiSetting); + var iWantToRun = semanticAiHandler.ChatConfig( + promptConfigParameter, + userId: $"AiChat_{Guid.NewGuid():N}", + maxHistoryStore: 20, + chatSystemMessage: systemPrompt, + senparcAiSetting: (SenparcAiSetting)aiSetting); + + // 将历史对话注入 + var hisgoryArgName = "history"; + var chatHistoryFromKernel = iWantToRun.StoredAiArguments.KernelArguments[hisgoryArgName] as ChatHistory; + if (chatHistoryFromKernel != null && request.ChatHistory?.Count > 0) + { + foreach (var msg in request.ChatHistory) + { + if (msg.Role == "user") + chatHistoryFromKernel.AddUserMessage(msg.Content); + else if (msg.Role == "assistant") + chatHistoryFromKernel.AddAssistantMessage(msg.Content); + } + iWantToRun.StoredAiArguments.KernelArguments[hisgoryArgName] = chatHistoryFromKernel; + } + + // 调用 AI + var aiResult = await semanticAiHandler.ChatAsync(iWantToRun, request.UserMessage, historyArgName: hisgoryArgName); + var aiResponseText = aiResult.OutputString; + + // 解析 AI 返回的结构化结果 + var responseDto = ParseAiResponse(aiResponseText, agentTemplates, request.AiModelId); + + return responseDto; + }); + } + + /// + /// 用户确认后,自动创建 ChatGroup 并运行任务(通过 FunctionRender 对应方法调用) + /// + [ApiBind(ApiRequestMethod = ApiRequestMethod.Post)] + public async Task> ConfirmCreateAndRunGroup(AiChat_SuggestedGroupDto suggestedGroup) + { + return await this.GetResponseAsync(async (response, logger) => + { + if (suggestedGroup == null) + { + throw new NcfExceptionBase("建议的 ChatGroup 信息不能为空"); + } + + // 通过 FunctionRender 机制调用 SetChatGroup 方法(复用已有逻辑) + var chatGroupAppService = base.GetRequiredService(); + + var chatGroupDto = new ChatGroupDto( + suggestedGroup.Name, + true, + ChatGroupState.Unstart, + suggestedGroup.Description, + suggestedGroup.AdminAgentTemplateId, + suggestedGroup.EnterAgentTemplateId); + + // 调用 ChatGroupAppService 中的 SetChatGroup 方法(与 FunctionRender 方法对应) + var setGroupResponse = await chatGroupAppService.SetChatGroup(chatGroupDto, suggestedGroup.MemberAgentTemplateIds ?? new List()); + + if (setGroupResponse.Success == false) + { + throw new NcfExceptionBase($"创建 ChatGroup 失败:{setGroupResponse.ErrorMessage}"); + } + + var newGroupId = setGroupResponse.Data?.ChatGroupDto?.Id ?? 0; + if (newGroupId == 0) + { + throw new NcfExceptionBase("创建 ChatGroup 成功但未获取到 ID"); + } + + // 获取 AI 设置 + var aiSetting = Senparc.AI.Config.SenparcAiSetting; + if (suggestedGroup.AiModelId > 0) + { + var aiModel = await _aiModelService.GetObjectAsync(z => z.Id == suggestedGroup.AiModelId); + if (aiModel != null) + { + var aiModelDto = _aiModelService.Mapper.Map(aiModel); + aiSetting = _aiModelService.BuildSenparcAiSetting(aiModelDto); + } + } + + // 通过 FunctionRender 机制调用 RunGroup 方法(复用已有逻辑) + var runRequest = new ChatGroup_RunGroupRequest + { + Name = $"AI对话任务-{DateTime.Now:yyyyMMddHHmmss}", + ChatGroupId = newGroupId, + AiModelId = suggestedGroup.AiModelId, + PromptCommand = suggestedGroup.TaskCommand, + Description = $"由 AI 对话自动创建并启动的任务", + Personality = true + }; + + // 使用 EventBus 模式,在独立线程中运行任务(不阻塞) + var _ = Task.Run(async () => + { + try + { + await _chatGroupService.RunChatGroupInThread(runRequest); + } + catch (Exception ex) + { + Senparc.CO2NET.Trace.SenparcTrace.BaseExceptionLog(ex); + } + }); + + var taskName = runRequest.Name; + + return new AiChat_SendMessageResponse + { + AiMessage = $"✅ 已成功创建组「{suggestedGroup.Name}」并启动任务「{taskName}」!任务正在后台运行中,您可以切换到「任务」标签页查看进度。", + ResponseType = AiChatResponseType.TaskStarted, + StartedGroupId = newGroupId, + StartedTaskName = taskName + }; + }); + } + + /// + /// 直接运行已有 ChatGroup 的任务(不创建新组) + /// + [ApiBind(ApiRequestMethod = ApiRequestMethod.Post)] + public async Task> RunExistingGroup(AiChat_RunExistingGroupRequest request) + { + return await this.GetResponseAsync(async (response, logger) => + { + var chatGroup = await _chatGroupService.GetObjectAsync(z => z.Id == request.ChatGroupId); + if (chatGroup == null) + { + throw new NcfExceptionBase($"未找到 ChatGroup(ID:{request.ChatGroupId})"); + } + + var runRequest = new ChatGroup_RunGroupRequest + { + Name = $"AI对话任务-{DateTime.Now:yyyyMMddHHmmss}", + ChatGroupId = request.ChatGroupId, + AiModelId = request.AiModelId, + PromptCommand = request.TaskCommand, + Description = "由 AI 对话自动启动的任务", + Personality = true + }; + + // 使用 EventBus 模式,在独立线程中运行任务(不阻塞) + var _ = Task.Run(async () => + { + try + { + await _chatGroupService.RunChatGroupInThread(runRequest); + } + catch (Exception ex) + { + Senparc.CO2NET.Trace.SenparcTrace.BaseExceptionLog(ex); + } + }); + + return new AiChat_SendMessageResponse + { + AiMessage = $"✅ 已使用组「{chatGroup.Name}」启动任务「{runRequest.Name}」!任务正在后台运行中,您可以切换到「任务」标签页查看进度。", + ResponseType = AiChatResponseType.TaskStarted, + StartedGroupId = request.ChatGroupId, + StartedTaskName = runRequest.Name + }; + }); + } + + #region 私有辅助方法 + + /// + /// 构建系统 Prompt + /// + private string BuildSystemPrompt( + IEnumerable agentTemplates, + IEnumerable chatGroups) + { + var agentJson = JsonSerializer.Serialize(agentTemplates, new JsonSerializerOptions { WriteIndented = false }); + var groupJson = JsonSerializer.Serialize(chatGroups, new JsonSerializerOptions { WriteIndented = false }); + + var sb = new StringBuilder(); + sb.AppendLine("你是一个 AgentsManager 任务助手,帮助用户通过自然语言启动 AI 多智能体协作任务。"); + sb.AppendLine(); + sb.AppendLine("## 可用的智能体(AgentTemplate)列表:"); + sb.AppendLine(agentJson); + sb.AppendLine(); + sb.AppendLine("## 现有的智能体组(ChatGroup)列表:"); + sb.AppendLine(groupJson); + sb.AppendLine(); + sb.AppendLine("## 你的职责:"); + sb.AppendLine("1. 理解用户的任务需求"); + sb.AppendLine("2. 判断现有 ChatGroup 是否适合执行该任务:"); + sb.AppendLine(" - 如果有合适的组,返回 JSON 格式:{\"action\":\"run_existing\",\"groupId\":组ID,\"reason\":\"原因\",\"taskCommand\":\"任务命令\"}"); + sb.AppendLine(" - 如果没有合适的组,分析需要哪些智能体,返回 JSON 格式:{\"action\":\"create_group\",\"reason\":\"为什么需要创建新组\",\"suggestion\":{\"name\":\"建议的组名称\",\"description\":\"组说明\",\"adminAgentTemplateId\":群主ID,\"enterAgentTemplateId\":对接人ID,\"memberIds\":[成员ID列表],\"taskCommand\":\"任务命令\"}}"); + sb.AppendLine("3. 如果用户的输入不是任务需求(如闲聊、问候),正常回复,返回 JSON 格式:{\"action\":\"message\",\"content\":\"你的回复内容\"}"); + sb.AppendLine(); + sb.AppendLine("## 重要说明:"); + sb.AppendLine("- adminAgentTemplateId 通常选择名称包含\"主\"或者排在首位的智能体"); + sb.AppendLine("- enterAgentTemplateId 是负责接收用户命令的智能体(对接人)"); + sb.AppendLine("- memberIds 包含所有参与任务的智能体 ID"); + sb.AppendLine("- 请严格按照 JSON 格式返回,不要添加任何 Markdown 代码块标记或额外说明"); + sb.Append("- 只返回纯 JSON,不要有其他文字"); + return sb.ToString(); + } + + /// + /// 解析 AI 返回结果 + /// + private AiChat_SendMessageResponse ParseAiResponse( + string aiResponseText, + IEnumerable agentTemplates, + int aiModelId) + { + try + { + // 清理可能的 Markdown 代码块标记 + var cleanedText = aiResponseText + .Replace("```json", "") + .Replace("```", "") + .Trim(); + + var jsonDoc = JsonDocument.Parse(cleanedText); + var root = jsonDoc.RootElement; + + var action = root.GetProperty("action").GetString(); + + switch (action) + { + case "run_existing": + var groupId = root.GetProperty("groupId").GetInt32(); + var reason = root.TryGetProperty("reason", out var reasonEl) ? reasonEl.GetString() : ""; + var taskCmd = root.TryGetProperty("taskCommand", out var tcEl) ? tcEl.GetString() : ""; + return new AiChat_SendMessageResponse + { + AiMessage = $"已找到合适的组来执行您的任务。{reason}\n\n任务命令:{taskCmd}", + ResponseType = AiChatResponseType.SuggestRunTask, + SuggestedGroupId = groupId, + SuggestedGroup = new AiChat_SuggestedGroupDto + { + TaskCommand = taskCmd, + AiModelId = aiModelId + } + }; + + case "create_group": + var createReason = root.TryGetProperty("reason", out var crEl) ? crEl.GetString() : ""; + var suggestion = root.GetProperty("suggestion"); + + var suggestedName = suggestion.TryGetProperty("name", out var nameEl) ? nameEl.GetString() : "新智能体组"; + var suggestedDesc = suggestion.TryGetProperty("description", out var descEl) ? descEl.GetString() : ""; + var adminId = suggestion.TryGetProperty("adminAgentTemplateId", out var adminEl) ? adminEl.GetInt32() : 0; + var enterId = suggestion.TryGetProperty("enterAgentTemplateId", out var enterEl) ? enterEl.GetInt32() : 0; + var memberIds = new List(); + if (suggestion.TryGetProperty("memberIds", out var memberIdsEl)) + { + foreach (var idEl in memberIdsEl.EnumerateArray()) + { + memberIds.Add(idEl.GetInt32()); + } + } + var createTaskCmd = suggestion.TryGetProperty("taskCommand", out var ctcEl) ? ctcEl.GetString() : ""; + + // 获取智能体名称列表用于展示 + var agentList = agentTemplates.ToList(); + var memberNames = agentList.Where(a => memberIds.Contains(a.Id)).Select(a => a.Name).ToList(); + var adminName = agentList.FirstOrDefault(a => a.Id == adminId)?.Name ?? $"ID:{adminId}"; + var enterName = agentList.FirstOrDefault(a => a.Id == enterId)?.Name ?? $"ID:{enterId}"; + + var sb = new StringBuilder(); + sb.AppendLine($"当前没有合适的组来执行您的任务。{createReason}"); + sb.AppendLine(); + sb.AppendLine($"**建议创建新组:**"); + sb.AppendLine($"- **组名称**:{suggestedName}"); + if (!string.IsNullOrEmpty(suggestedDesc)) + sb.AppendLine($"- **说明**:{suggestedDesc}"); + sb.AppendLine($"- **群主**:{adminName}"); + sb.AppendLine($"- **对接人**:{enterName}"); + if (memberNames.Count > 0) + sb.AppendLine($"- **成员**:{string.Join("、", memberNames)}"); + sb.AppendLine($"- **任务命令**:{createTaskCmd}"); + sb.AppendLine(); + sb.AppendLine("是否确认创建该组并运行任务?"); + + return new AiChat_SendMessageResponse + { + AiMessage = sb.ToString(), + ResponseType = AiChatResponseType.SuggestCreateGroup, + SuggestedGroup = new AiChat_SuggestedGroupDto + { + Name = suggestedName, + Description = suggestedDesc, + AdminAgentTemplateId = adminId, + EnterAgentTemplateId = enterId, + MemberAgentTemplateIds = memberIds, + TaskCommand = createTaskCmd, + AiModelId = aiModelId + } + }; + + case "message": + default: + var content = root.TryGetProperty("content", out var contentEl) + ? contentEl.GetString() + : aiResponseText; + return new AiChat_SendMessageResponse + { + AiMessage = content, + ResponseType = AiChatResponseType.Message + }; + } + } + catch (Exception ex) + { + // 解析失败时,将原始文本作为普通消息返回(AI 可能返回非 JSON 格式的内容) + Senparc.CO2NET.Trace.SenparcTrace.Log($"AiChatAppService ParseAiResponse 解析异常: {ex.Message},原始响应: {aiResponseText?.Substring(0, Math.Min(200, aiResponseText?.Length ?? 0))}"); + return new AiChat_SendMessageResponse + { + AiMessage = aiResponseText, + ResponseType = AiChatResponseType.Message + }; + } + } + + #endregion + } +} diff --git a/src/Extensions/Senparc.Xncf.AgentsManager/OHS/Local/PL/AiChatRequest.cs b/src/Extensions/Senparc.Xncf.AgentsManager/OHS/Local/PL/AiChatRequest.cs new file mode 100644 index 000000000..ac97fe9e2 --- /dev/null +++ b/src/Extensions/Senparc.Xncf.AgentsManager/OHS/Local/PL/AiChatRequest.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; + +namespace Senparc.Xncf.AgentsManager.OHS.Local.PL +{ + /// + /// AI 对话消息记录 + /// + public class AiChatMessageDto + { + /// + /// 角色:user / assistant + /// + public string Role { get; set; } + + /// + /// 消息内容 + /// + public string Content { get; set; } + } + + /// + /// 发送 AI Chat 消息的请求 + /// + public class AiChat_SendMessageRequest + { + /// + /// 用户输入的消息 + /// + public string UserMessage { get; set; } + + /// + /// 历史对话记录 + /// + public List ChatHistory { get; set; } = new List(); + + /// + /// 使用的 AI 模型 ID(0 表示使用默认配置) + /// + public int AiModelId { get; set; } = 0; + + /// + /// 待确认的 ChatGroup 建议信息(在用户确认后自动创建并运行) + /// + public AiChat_SuggestedGroupDto PendingGroup { get; set; } + } + + /// + /// AI 建议创建的 ChatGroup 信息 + /// + public class AiChat_SuggestedGroupDto + { + /// + /// 建议的组名称 + /// + public string Name { get; set; } + + /// + /// 组说明 + /// + public string Description { get; set; } + + /// + /// 群主 AgentTemplate ID + /// + public int AdminAgentTemplateId { get; set; } + + /// + /// 对接人 AgentTemplate ID + /// + public int EnterAgentTemplateId { get; set; } + + /// + /// 成员 AgentTemplate ID 列表 + /// + public List MemberAgentTemplateIds { get; set; } = new List(); + + /// + /// 用户的任务命令(用于运行时传入) + /// + public string TaskCommand { get; set; } + + /// + /// 使用的 AI 模型 ID + /// + public int AiModelId { get; set; } + } + + /// + /// 运行已有 ChatGroup 的请求 + /// + public class AiChat_RunExistingGroupRequest + { + /// + /// ChatGroup ID + /// + public int ChatGroupId { get; set; } + + /// + /// 任务命令 + /// + public string TaskCommand { get; set; } + + /// + /// AI 模型 ID(0 表示使用默认配置) + /// + public int AiModelId { get; set; } = 0; + } +} diff --git a/src/Extensions/Senparc.Xncf.AgentsManager/OHS/Local/PL/AiChatResponse.cs b/src/Extensions/Senparc.Xncf.AgentsManager/OHS/Local/PL/AiChatResponse.cs new file mode 100644 index 000000000..4ed009ab4 --- /dev/null +++ b/src/Extensions/Senparc.Xncf.AgentsManager/OHS/Local/PL/AiChatResponse.cs @@ -0,0 +1,72 @@ +using Senparc.Xncf.AgentsManager.Models.DatabaseModel.Models.Dto; +using System.Collections.Generic; + +namespace Senparc.Xncf.AgentsManager.OHS.Local.PL +{ + /// + /// AI Chat 消息类型 + /// + public enum AiChatResponseType + { + /// + /// 普通回复 + /// + Message = 0, + + /// + /// 建议创建新组(等待用户确认) + /// + SuggestCreateGroup = 1, + + /// + /// 已找到合适的组,建议直接运行任务 + /// + SuggestRunTask = 2, + + /// + /// 已开始创建并运行任务 + /// + TaskStarted = 3, + + /// + /// 错误 + /// + Error = 4 + } + + /// + /// AI Chat 发送消息的响应 + /// + public class AiChat_SendMessageResponse + { + /// + /// AI 回复内容(用于展示给用户) + /// + public string AiMessage { get; set; } + + /// + /// 响应类型 + /// + public AiChatResponseType ResponseType { get; set; } + + /// + /// 建议创建的 ChatGroup 信息(当 ResponseType 为 SuggestCreateGroup 时有值) + /// + public AiChat_SuggestedGroupDto SuggestedGroup { get; set; } + + /// + /// 建议运行的 ChatGroup ID(当 ResponseType 为 SuggestRunTask 时有值) + /// + public int? SuggestedGroupId { get; set; } + + /// + /// 已启动的任务所属 ChatGroup ID(当 ResponseType 为 TaskStarted 时有值) + /// + public int? StartedGroupId { get; set; } + + /// + /// 已启动任务的名称 + /// + public string StartedTaskName { get; set; } + } +} diff --git a/src/Extensions/Senparc.Xncf.AgentsManager/Register.cs b/src/Extensions/Senparc.Xncf.AgentsManager/Register.cs index dfabc6254..ea55518ab 100644 --- a/src/Extensions/Senparc.Xncf.AgentsManager/Register.cs +++ b/src/Extensions/Senparc.Xncf.AgentsManager/Register.cs @@ -17,6 +17,7 @@ using Senparc.Xncf.AgentsManager.Models.DatabaseModel; using Senparc.Xncf.AgentsManager.Models.DatabaseModel.Models; using Senparc.Xncf.AgentsManager.Models.DatabaseModel.Models.Dto; +using Senparc.Xncf.AgentsManager.OHS.Local.AppService; using Senparc.Xncf.XncfBuilder.OHS.Local; using System; using System.Linq; @@ -108,6 +109,9 @@ public override IServiceCollection AddXncfModule(IServiceCollection services, IC services.AddScoped(); services.AddScoped(); + //AI Chat + services.AddScoped(); + //测试 services.AddScoped(); diff --git a/src/Extensions/Senparc.Xncf.AgentsManager/wwwroot/css/AgentsManager/index.css b/src/Extensions/Senparc.Xncf.AgentsManager/wwwroot/css/AgentsManager/index.css index 38e3303a4..289b0791f 100644 --- a/src/Extensions/Senparc.Xncf.AgentsManager/wwwroot/css/AgentsManager/index.css +++ b/src/Extensions/Senparc.Xncf.AgentsManager/wwwroot/css/AgentsManager/index.css @@ -1824,4 +1824,84 @@ height:100%; .mcp-endpoint-actions { display: flex; align-items: center; -} \ No newline at end of file +} +/* AI 对话标签页样式 */ +.aiChatMessagesList { + display: flex; + flex-direction: column; + gap: 16px; +} + +.aiChatMessage { + display: flex; + gap: 10px; + align-items: flex-start; + max-width: 85%; +} + +.aiChatMessage-user { + flex-direction: row-reverse; + margin-left: auto; +} + +.aiChatMessage-ai { + flex-direction: row; + margin-right: auto; +} + +.aiChatMessage-bubble { + background: #f0f2f5; + border-radius: 12px; + padding: 10px 14px; + font-size: 14px; + line-height: 1.6; + max-width: 100%; + word-break: break-word; +} + +.aiChatMessage-user .aiChatMessage-bubble { + background: #409eff; + color: #fff; +} + +.aiChatMessage-avatar img { + width: 32px; + height: 32px; + border-radius: 50%; + object-fit: cover; +} + +.aiChatMessage-loading { + display: flex; + align-items: center; + gap: 4px; + padding: 12px 16px; +} + +.aiChatMessage-loading span { + width: 8px; + height: 8px; + border-radius: 50%; + background: #409eff; + animation: aiChatDot 1.2s infinite; + display: inline-block; +} + +.aiChatMessage-loading span:nth-child(2) { + animation-delay: 0.2s; +} + +.aiChatMessage-loading span:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes aiChatDot { + 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; } + 40% { transform: scale(1); opacity: 1; } +} + +.aiChatAction { + border-top: 1px solid rgba(0,0,0,0.08); + padding-top: 8px; + margin-top: 8px; +} 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 9d84dbd3e..b68b928ce 100644 --- a/src/Extensions/Senparc.Xncf.AgentsManager/wwwroot/js/AgentsManager/index.js +++ b/src/Extensions/Senparc.Xncf.AgentsManager/wwwroot/js/AgentsManager/index.js @@ -390,6 +390,14 @@ var app = new Vue({ mcpEndpointEditMode: false, mcpEndpointOriginalName: '', currentMcpTools: [], // 当前查看的MCP工具列表 + // AI 对话 ---start + aiChatMessages: [], // 对话消息列表 + aiChatInput: '', // 当前输入 + aiChatLoading: false, // 加载状态 + aiChatConfirming: false, // 确认操作加载状态 + aiChatModelId: null, // 使用的 AI 模型 ID(null 表示默认) + aiChatModelList: [], // 可选 AI 模型列表 + // AI 对话 ---end }; }, computed: { @@ -1528,6 +1536,10 @@ var app = new Vue({ if (this.tabsActiveName === 'third') { this.gettaskListData('task') } + // AI 对话 + if (this.tabsActiveName === 'fourth') { + this.loadAiChatModelList() + } }, // 筛选输入变化 @@ -3364,6 +3376,255 @@ Vue.component('load-more-select', { }) } - } + }, + + // ========= AI 对话相关方法 ========= + + // 加载 AI 模型列表 + async loadAiChatModelList() { + try { + const res = await serviceAM.post('/api/Senparc.Xncf.AIKernel/AIModelAppService/Xncf.AIKernel_AIModelAppService.GetListAsync', { + pageIndex: 0, + pageSize: 0 + }) + const data = res?.data ?? {} + if (data.success) { + // 只显示 Chat 类型的模型(configModelType === 1) + this.aiChatModelList = (data.data ?? []).filter(m => m.configModelType === 1) + } + } catch (e) { + console.error('加载 AI 模型列表失败', e) + } + }, + + // 发送 AI 对话消息 + async sendAiChatMessage() { + const userInput = (this.aiChatInput || '').trim() + if (!userInput) return + if (this.aiChatLoading) return + + // 添加用户消息 + this.aiChatMessages.push({ + role: 'user', + content: userInput, + contentHtml: userInput.replace(/\n/g, '
') + }) + this.aiChatInput = '' + + // 滚动到底部 + this.$nextTick(() => { + const el = this.$refs.aiChatMessages + if (el) el.scrollTop = el.scrollHeight + }) + + this.aiChatLoading = true + + try { + // 构建历史记录(只发送 user/assistant 角色,不包含最新的用户消息) + // 先截取除最后一条外的所有消息,再过滤角色 + const chatHistory = this.aiChatMessages + .slice(0, -1) + .filter(m => m.role === 'user' || m.role === 'assistant') + .map(m => ({ role: m.role, content: m.content })) + + const payload = { + userMessage: userInput, + chatHistory: chatHistory, + aiModelId: this.aiChatModelId || 0 + } + + const res = await serviceAM.post( + '/api/Senparc.Xncf.AgentsManager/AiChatAppService/Xncf.AgentsManager_AiChatAppService.SendMessage', + payload + ) + + const data = res?.data ?? {} + if (data.success) { + const aiData = data.data ?? {} + const aiMessage = aiData.aiMessage || '(无响应)' + const responseType = aiData.responseType ?? 0 + const suggestedGroup = aiData.suggestedGroup || null + const suggestedGroupId = aiData.suggestedGroupId || null + + // 渲染 Markdown + let contentHtml = aiMessage + if (typeof marked !== 'undefined') { + try { contentHtml = marked.parse(aiMessage) } catch (e) { } + } + + // 添加 AI 消息 + this.aiChatMessages.push({ + role: 'assistant', + content: aiMessage, + contentHtml: contentHtml, + responseType: responseType, + suggestedGroup: suggestedGroup, + suggestedGroupId: suggestedGroupId, + confirmed: false + }) + } else { + this.aiChatMessages.push({ + role: 'assistant', + content: data.errorMessage || '请求失败,请稍后重试', + contentHtml: data.errorMessage || '请求失败,请稍后重试', + responseType: 4 + }) + } + } catch (e) { + this.aiChatMessages.push({ + role: 'assistant', + content: '网络错误,请稍后重试:' + (e.message || ''), + contentHtml: '网络错误,请稍后重试:' + (e.message || ''), + responseType: 4 + }) + } finally { + this.aiChatLoading = false + this.$nextTick(() => { + const el = this.$refs.aiChatMessages + if (el) el.scrollTop = el.scrollHeight + }) + } + }, + + // 用户确认创建新组并运行任务(通过 FunctionRender 对应方法) + async confirmCreateAndRunGroup(msg, idx) { + if (!msg.suggestedGroup) return + this.aiChatConfirming = true + try { + const res = await serviceAM.post( + '/api/Senparc.Xncf.AgentsManager/AiChatAppService/Xncf.AgentsManager_AiChatAppService.ConfirmCreateAndRunGroup', + msg.suggestedGroup + ) + const data = res?.data ?? {} + if (data.success) { + const aiData = data.data ?? {} + const aiMessage = aiData.aiMessage || '✅ 任务已启动!' + let contentHtml = aiMessage + if (typeof marked !== 'undefined') { + try { contentHtml = marked.parse(aiMessage) } catch (e) { } + } + // 标记当前消息为已确认 + this.$set(this.aiChatMessages[idx], 'confirmed', true) + // 添加确认成功的 AI 回复 + this.aiChatMessages.push({ + role: 'assistant', + content: aiMessage, + contentHtml: contentHtml, + responseType: 3, + startedGroupId: aiData.startedGroupId, + startedTaskName: aiData.startedTaskName + }) + // 使用 EventBus 通知:切换到任务标签页 + this.$nextTick(() => { + this.$message({ message: '任务已启动,可切换到「任务」标签页查看进度', type: 'success', duration: 3000 }) + }) + } else { + this.aiChatMessages.push({ + role: 'assistant', + content: '创建失败:' + (data.errorMessage || '未知错误'), + contentHtml: '创建失败:' + (data.errorMessage || '未知错误'), + responseType: 4 + }) + } + } catch (e) { + this.aiChatMessages.push({ + role: 'assistant', + content: '操作失败:' + (e.message || ''), + contentHtml: '操作失败:' + (e.message || ''), + responseType: 4 + }) + } finally { + this.aiChatConfirming = false + this.$nextTick(() => { + const el = this.$refs.aiChatMessages + if (el) el.scrollTop = el.scrollHeight + }) + } + }, + + // 用户确认运行已有组 + async confirmRunExistingGroup(msg, idx) { + if (!msg.suggestedGroupId) return + this.aiChatConfirming = true + try { + // 获取最近一条用户消息作为任务命令 + const lastUserMsg = [...this.aiChatMessages].reverse().find(m => m.role === 'user') + const taskCommand = (msg.suggestedGroup && msg.suggestedGroup.taskCommand) || (lastUserMsg && lastUserMsg.content) || '' + + const res = await serviceAM.post( + '/api/Senparc.Xncf.AgentsManager/AiChatAppService/Xncf.AgentsManager_AiChatAppService.RunExistingGroup', + { + chatGroupId: msg.suggestedGroupId, + taskCommand: taskCommand, + aiModelId: this.aiChatModelId || 0 + } + ) + const data = res?.data ?? {} + if (data.success) { + const aiData = data.data ?? {} + const aiMessage = aiData.aiMessage || '✅ 任务已启动!' + let contentHtml = aiMessage + if (typeof marked !== 'undefined') { + try { contentHtml = marked.parse(aiMessage) } catch (e) { } + } + this.$set(this.aiChatMessages[idx], 'confirmed', true) + this.aiChatMessages.push({ + role: 'assistant', + content: aiMessage, + contentHtml: contentHtml, + responseType: 3, + startedGroupId: aiData.startedGroupId, + startedTaskName: aiData.startedTaskName + }) + this.$nextTick(() => { + this.$message({ message: '任务已启动,可切换到「任务」标签页查看进度', type: 'success', duration: 3000 }) + }) + } else { + this.aiChatMessages.push({ + role: 'assistant', + content: '启动失败:' + (data.errorMessage || '未知错误'), + contentHtml: '启动失败:' + (data.errorMessage || '未知错误'), + responseType: 4 + }) + } + } catch (e) { + this.aiChatMessages.push({ + role: 'assistant', + content: '操作失败:' + (e.message || ''), + contentHtml: '操作失败:' + (e.message || ''), + responseType: 4 + }) + } finally { + this.aiChatConfirming = false + this.$nextTick(() => { + const el = this.$refs.aiChatMessages + if (el) el.scrollTop = el.scrollHeight + }) + } + }, + + // 取消 AI 建议 + cancelSuggestion(idx) { + this.$set(this.aiChatMessages[idx], 'confirmed', true) + this.aiChatMessages.push({ + role: 'assistant', + content: '已取消。如有其他需要,请继续描述您的任务。', + contentHtml: '已取消。如有其他需要,请继续描述您的任务。', + responseType: 0 + }) + }, + + // 清空对话 + clearAiChat() { + this.$confirm('确认清空全部对话记录?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + this.aiChatMessages = [] + this.aiChatInput = '' + }).catch(() => {}) + }, + }, })