using AI.AgentIntegration; using AI.Interface; using AI.Models; using AI.Models.Form; using AI.Models.SpecialMessages; using AI.Service; using AI.Utils; using AI.Workflow; using System.Text; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; using Avalonia; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using System.ClientModel.Primitives; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading; namespace AI.ViewModels { public partial class MainWindowViewModel : ViewModelBase { public event Action? RequestScrollToBottom; public event Func>>? RequestFileSelection; private readonly IChatBackend _chatBackend; private readonly ChatSessionManager _sessionManager; private readonly IFormRegistry _formRegistry; private readonly IAppController _appController; private ChatSession? _currentSession; // 用于节流滚动调用,减少滚动频率 private DateTime _lastScrollTime = DateTime.MinValue; private const int ScrollThrottleMs = 50; // 每50ms最多滚动一次 [ObservableProperty] private string? _chatInput; [ObservableProperty] private ObservableCollection _chatMessages; [ObservableProperty] private ObservableCollection _sessions; private ChatSessionItemViewModel? _selectedSessionItem; [ObservableProperty] private ObservableCollection _pendingFiles = new(); private WorkflowMode _currentWorkflowMode = WorkflowMode.Ask; /// /// 工作流取消令牌源;执行中时非 null,用于取消或超时 /// private CancellationTokenSource? _workflowCts; /// /// 工作流默认超时时间 /// private static readonly TimeSpan DefaultWorkflowTimeout = TimeSpan.FromMinutes(10); /// /// 当前是否有工作流正在执行(用于显示取消按钮) /// [ObservableProperty] private bool _isWorkflowRunning; /// /// 当前工作模式(用于 ComboBox 绑定) /// public WorkflowMode CurrentWorkflowMode { get => _currentWorkflowMode; set { if (SetProperty(ref _currentWorkflowMode, value)) { OnPropertyChanged(nameof(CurrentWorkflowModeIndex)); OnWorkflowModeChanged(); } } } /// /// 当前工作模式的索引(用于 ComboBox SelectedIndex 绑定) /// public int CurrentWorkflowModeIndex { get => (int)CurrentWorkflowMode; set { if (value >= 0 && value <= 1) { CurrentWorkflowMode = (WorkflowMode)value; } } } public ChatSessionItemViewModel? SelectedSessionItem { get => _selectedSessionItem; set { if (SetProperty(ref _selectedSessionItem, value)) { if (value != null) { SwitchToSession(value.Session); } } } } [ObservableProperty] private bool _isLastMessage; public MainWindowViewModel(IChatBackend chatBackend, ChatSessionManager sessionManager, IFormRegistry formRegistry, IAppController appController) { _chatBackend = chatBackend; _sessionManager = sessionManager; _formRegistry = formRegistry ?? throw new ArgumentNullException(nameof(formRegistry)); _appController = appController ?? throw new ArgumentNullException(nameof(appController)); ChatMessages = new ObservableCollection(); Sessions = new ObservableCollection(); // 订阅会话管理器事件 _sessionManager.SessionsChanged += OnSessionsChanged; _sessionManager.CurrentSessionChanged += OnCurrentSessionChanged; // 初始化会话列表 RefreshSessions(); // 如果没有当前会话,创建一个新的 if (_sessionManager.CurrentSession == null) { CreateNewSession(); } else { SwitchToSession(_sessionManager.CurrentSession); } } /// /// 获取当前会话 /// public ChatSession? CurrentSession => _currentSession; private void OnSessionsChanged(object? sender, EventArgs e) { Dispatcher.UIThread.InvokeAsync(RefreshSessions); } private void OnCurrentSessionChanged(object? sender, ChatSession? session) { Dispatcher.UIThread.InvokeAsync(() => { SwitchToSession(session); }); } private void RefreshSessions() { var currentSessionId = _currentSession?.Id; Sessions.Clear(); foreach (var session in _sessionManager.GetAllSessions()) { var item = new ChatSessionItemViewModel(session); if (session.Id == currentSessionId) { item.IsSelected = true; SelectedSessionItem = item; } Sessions.Add(item); } } /// /// 清理聊天界面内容 /// private void ClearChatUI() { ChatMessages.Clear(); PendingFiles.Clear(); ChatInput = string.Empty; SelectedSessionItem = null; // 清除所有会话项的选中状态 foreach (var item in Sessions) { item.IsSelected = false; } } private void SwitchToSession(ChatSession? session) { if (session == null) { // 如果没有会话,清理会话状态和 UI 内容 _currentSession = null; ClearChatUI(); return; } _currentSession = session; _sessionManager.CurrentSession = session; // 将当前可用表单 Schema(YAML)合并进本会话的 System 消息,存在则替换、避免重复追加 session.EnsureFormsYamlInSystem( AppPrompt.InteractiveGuidePrompt, FormSchemaYamlGenerator.ToYaml(_formRegistry.GetAll())); // 恢复会话的工作模式 CurrentWorkflowMode = session.WorkflowMode; // 从会话历史加载消息到 UI LoadMessagesFromSession(session); // 更新选中状态 foreach (var item in Sessions) { item.IsSelected = item.Session.Id == session.Id; if (item.IsSelected) { SelectedSessionItem = item; } } } /// /// 从会话存储构建 UI 消息列表(仅从 Store 读取,不解析 History)。 /// private void LoadMessagesFromSession(ChatSession session) { ChatMessages.Clear(); foreach (var entry in session.Store.Entries) { if (entry is Models.Store.TextConversationEntry text) { if (text.Role == AuthorRole.System) { continue; } var authorType = text.Role == AuthorRole.User ? AuthorType.User : AuthorType.AI; ChatMessages.Add(new ChatMessageModel(authorType, text.Content ?? string.Empty)); } else if (entry is Models.Store.SpecialConversationEntry special) { var specialMsg = SpecialMessageDeserializer.Deserialize(special.Type, special.Payload, _formRegistry); if (specialMsg == null) { ChatMessages.Add(new ChatMessageModel(AuthorType.AI, $"[无法还原:{special.Type}]")); continue; } var msgType = specialMsg.TypeName switch { "Form" => MessageType.Form, "ParameterSet" => MessageType.ParameterSet, "Table" => MessageType.Table, "ColumnMatch" => MessageType.ColumnMatch, "WorkflowStatus" => MessageType.WorkflowStatus, "KnowledgeBase" => MessageType.KnowledgeBase, "XyzLoadCard" => MessageType.XyzLoadCard, _ => MessageType.Text }; var model = new ChatMessageModel(AuthorType.AI, string.Empty, lastMessage: false) { Type = msgType, SpecialContent = specialMsg }; if (specialMsg is Models.SpecialMessages.KnowledgeBaseMessage kbMsg) { model.Message = "[知识库参考]\n" + (kbMsg.RawContent ?? string.Empty); } ChatMessages.Add(model); } } if (ChatMessages.Count > 0) { ChatMessages.Last()!.LastMessage = true; } } [RelayCommand] private void CreateNewSession() { var newSession = _sessionManager.CreateSession(); newSession.WorkflowMode = CurrentWorkflowMode; // 通过 EnsureFormsYamlInSystem 写入引导词 + 表单 Schema YAML(存在则替换) newSession.EnsureFormsYamlInSystem( AppPrompt.InteractiveGuidePrompt, FormSchemaYamlGenerator.ToYaml(_formRegistry.GetAll())); SwitchToSession(newSession); } /// /// 工作模式改变时的处理 /// private void OnWorkflowModeChanged() { if (_currentSession != null) { _currentSession.WorkflowMode = CurrentWorkflowMode; } } [RelayCommand] private void DeleteSession(string? sessionId) { if (string.IsNullOrEmpty(sessionId)) { return; } var session = _sessionManager.GetSession(sessionId); if (session == null) { return; } // 如果删除的是当前会话,会话管理器会自动切换到其他会话 _sessionManager.DeleteSession(sessionId); } [RelayCommand] private async Task SendMessage() { if ( _currentSession == null) { return; } var currentUserInput = ChatInput; ChatInput = string.Empty; List currentPendingFiles = PendingFiles.ToList(); PendingFiles.Clear(); foreach (var file in currentPendingFiles) { if (string.IsNullOrEmpty(file.Name)) { continue; } var fileMessage = new ChatMessageModel(AuthorType.User, $"相关文件: {file.Name}") { Type = MessageType.File, FileName = file.Name, FileSize = file.Size, }; ChatMessages.Add(fileMessage); string userMessage = $"用户提供了\"{file.StorageFile.Path.LocalPath}\"文件"; _currentSession.AddUserMessage(userMessage); } if (string.IsNullOrEmpty(currentUserInput)) { return; } ChatMessages.ToList().ForEach(m => m.LastMessage = false); ChatMessages.Add(new ChatMessageModel(AuthorType.User, currentUserInput)); // 根据工作模式选择处理方式 if (CurrentWorkflowMode == WorkflowMode.Workflow) { // 工作流模式:使用 Planner + ReAct await ExecuteWorkflowAsync(currentUserInput); } else { // Ask 模式:普通对话 await ExecuteAskModeAsync(currentUserInput); } // 更新会话列表项的显示信息 var sessionItem = Sessions.FirstOrDefault(s => s.Session.Id == _currentSession.Id); sessionItem?.Refresh(); } private void Rollback(string currentUserInput, int historyCountBefore, ChatMessageModel aiMessageModel) { if (_currentSession != null && _currentSession.History.Count > historyCountBefore) { _currentSession.History.RemoveAt(_currentSession.History.Count - 1); _currentSession.Store.RemoveLast(); // 与 Store 同步:移除刚加入的用户消息 } ChatMessages.Remove(aiMessageModel); ChatInput = currentUserInput; } [RelayCommand] private async Task AddPendingFile() { try { // 通过事件请求文件选择 var files = await RequestFileSelection?.Invoke(); if (files != null && files.Count > 0) { foreach (var file in files) { // 获取文件大小 var stream = await file.OpenReadAsync(); var size = stream.Length; stream.Dispose(); PendingFiles.Add(new PendingFileModel { StorageFile = file, Size = size }); } } } catch (Exception ex) { Console.WriteLine($"文件选择错误: {ex.Message}"); } } [RelayCommand] private void RemovePendingFile(PendingFileModel file) { PendingFiles.Remove(file); } /// /// 为表单中的文件路径字段打开文件选择框,并将选中的路径写入该字段 /// [RelayCommand] private async Task PickFileForField(FormFieldEntry? entry) { if (entry == null) return; try { var files = await RequestFileSelection?.Invoke(); if (files != null && files.Count > 0 && files[0].Path != null) { entry.CurrentValue = files[0].Path.LocalPath; } } catch (Exception ex) { Console.WriteLine($"文件选择错误: {ex.Message}"); } } /// /// 为参数集卡片中的文件路径项打开文件选择框,并将选中的路径写入该项 /// [RelayCommand] private async Task PickFileForParameterItem(ParameterSetItem? item) { if (item == null) return; try { var files = await RequestFileSelection?.Invoke(); if (files != null && files.Count > 0 && files[0].Path != null) { item.ValueText = files[0].Path.LocalPath; } } catch (Exception ex) { Console.WriteLine($"文件选择错误: {ex.Message}"); } } /// /// 将参数集卡片中的当前值提交到网格化模块(设置网格化参数) /// [RelayCommand] private async Task ApplyParameterSet(ParameterSetMessage? paramMsg) { if (paramMsg == null || paramMsg.Items.Count == 0) return; try { var jobj = new Newtonsoft.Json.Linq.JObject(); foreach (var item in paramMsg.Items) { if (string.IsNullOrEmpty(item.Name)) continue; if (item.FieldType == ParameterSetFieldType.Number && double.TryParse(item.ValueText?.Trim(), System.Globalization.NumberStyles.Any, null, out var num)) jobj[item.Name] = num; else jobj[item.Name] = item.ValueText ?? string.Empty; } var json = jobj.ToString(); var action = AppAction.CreateSetParameters(json); var result = await _appController.ExecuteAsync(action); // 可选:在界面提示成功/失败 if (!result.Success && !string.IsNullOrEmpty(result.Message)) System.Diagnostics.Debug.WriteLine($"应用参数: {result.Message}"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"应用参数失败: {ex.Message}"); } } /// /// 在指定会话中插入一条表单消息(AI 侧展示,供用户填写并提交)。同时写入该会话的 Store。 /// 仅当目标会话为当前显示会话时更新 ChatMessages 并滚动,避免串会话。 /// /// 要插入的会话;为 null 时使用当前选中会话(兼容旧调用) public void AddFormMessage(FormDefinition definition, ChatSession? targetSession = null) { var formMsg = new FormRequestMessage(definition); var serialized = SpecialMessageSerializer.Serialize(formMsg); targetSession ??= _currentSession; targetSession?.AppendSpecialEntry("Form", serialized); if (targetSession != null && targetSession == _currentSession) { var message = new ChatMessageModel(AuthorType.AI, string.Empty, lastMessage: true) { Type = MessageType.Form, SpecialContent = formMsg, }; ChatMessages.ToList().ForEach(m => m.LastMessage = false); ChatMessages.Add(message); RequestScrollToBottom?.Invoke(); } } /// /// 在指定会话中插入一条参数集消息(将业务层 JSON 转为卡片展示)。同时写入该会话的 Store。 /// 仅当目标会话为当前显示会话时更新 ChatMessages 并滚动,避免串会话。 /// /// 参数 JSON /// 卡片标题 /// 要插入的会话;为 null 时使用当前选中会话(兼容旧调用) public void AddParameterSetMessage(string json, string? title = null, ChatSession? targetSession = null) { var paramMsg = new ParameterSetMessage(); if (!string.IsNullOrWhiteSpace(title)) paramMsg.Title = title; paramMsg.LoadFromJson(json ?? string.Empty); var serialized = SpecialMessageSerializer.Serialize(paramMsg); targetSession ??= _currentSession; targetSession?.AppendSpecialEntry("ParameterSet", serialized); if (targetSession != null && targetSession == _currentSession) { var message = new ChatMessageModel(AuthorType.AI, string.Empty, lastMessage: true) { Type = MessageType.ParameterSet, SpecialContent = paramMsg, }; ChatMessages.ToList().ForEach(m => m.LastMessage = false); ChatMessages.Add(message); RequestScrollToBottom?.Invoke(); } } /// /// 在指定会话中插入一张网格化参数设置综合卡片(Phase 0:Loading),并立即异步加载参数填充。 /// 卡片自治完成「加载参数 → 用户编辑 → 点击生成 → 设置参数 + 执行成图」全流程。 /// public async void AddGriddingParamCardMessage(ChatSession? targetSession = null) { var card = new Models.SpecialMessages.GriddingParamCardMessage(); var serialized = SpecialMessageSerializer.Serialize(card); targetSession ??= _currentSession; targetSession?.AppendSpecialEntry("GriddingParamCard", serialized); ChatMessageModel? messageModel = null; if (targetSession != null && targetSession == _currentSession) { messageModel = new ChatMessageModel(AuthorType.AI, string.Empty, lastMessage: true) { Type = MessageType.GriddingParamCard, SpecialContent = card, }; ChatMessages.ToList().ForEach(m => m.LastMessage = false); ChatMessages.Add(messageModel); RequestScrollToBottom?.Invoke(); } // 卡片展示后立即异步加载参数 await LoadGriddingParamCardItemsAsync(card); } /// /// 异步从业务层获取网格化参数,填充到卡片的 Items 中,并切换卡片到 Ready 阶段。 /// private async Task LoadGriddingParamCardItemsAsync(Models.SpecialMessages.GriddingParamCardMessage card) { try { var result = await _appController.ExecuteAsync( AppAction.CreateAction(AgentIntegration.AppActionType.GriddingModuleGetParameters)); if (result.Success && !string.IsNullOrWhiteSpace(result.Message)) { var tempMsg = new Models.SpecialMessages.ParameterSetMessage(); tempMsg.LoadFromJson(result.Message); await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { card.Items.Clear(); foreach (var item in tempMsg.Items) card.Items.Add(item); card.Phase = Models.SpecialMessages.GriddingParamCardPhase.Ready; RequestScrollToBottom?.Invoke(); }); } else { await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { card.StatusMessage = result.Message ?? "获取网格化参数失败,请重试"; card.Phase = Models.SpecialMessages.GriddingParamCardPhase.Error; }); } } catch (Exception ex) { await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { card.StatusMessage = $"加载参数失败:{ex.Message}"; card.Phase = Models.SpecialMessages.GriddingParamCardPhase.Error; }); } } /// /// 在指定会话中插入一张散点文件加载综合卡片(Phase 0:等待用户选择文件)。 /// public void AddXyzLoadCardMessage(ChatSession? targetSession = null) { var card = new Models.SpecialMessages.XyzLoadCardMessage(); var serialized = SpecialMessageSerializer.Serialize(card); targetSession ??= _currentSession; targetSession?.AppendSpecialEntry("XyzLoadCard", serialized); if (targetSession != null && targetSession == _currentSession) { var message = new ChatMessageModel(AuthorType.AI, string.Empty, lastMessage: true) { Type = MessageType.XyzLoadCard, SpecialContent = card, }; ChatMessages.ToList().ForEach(m => m.LastMessage = false); ChatMessages.Add(message); RequestScrollToBottom?.Invoke(); } } /// /// 表单提交:收集表单值,作为结构化用户消息注入对话并请求 AI 继续 /// [RelayCommand] private async Task SubmitForm(object? parameter) { if (parameter is not ChatMessageModel message || message.Type != MessageType.Form || message.SpecialContent is not FormRequestMessage formMsg || _currentSession == null) { return; } var values = formMsg.GetValues(); var sb = new StringBuilder(); sb.Append("用户填写了表单【").Append(formMsg.Definition.Title).Append("】"); if (!string.IsNullOrEmpty(formMsg.Definition.SubmitTarget)) { sb.Append("(目标:").Append(formMsg.Definition.SubmitTarget).Append(")"); } sb.Append(":"); foreach (var kv in values) { sb.Append(" ").Append(kv.Key).Append("=").Append(kv.Value is string s ? s : kv.Value); } var userText = sb.ToString(); ChatMessages.ToList().ForEach(m => m.LastMessage = false); ChatMessages.Add(new ChatMessageModel(AuthorType.User, userText)); formMsg.SubmitLabel = "已提交"; _currentSession.AddUserMessage(userText); var aiMessageModel = new ChatMessageModel(AuthorType.AI, string.Empty, lastMessage: true); ChatMessages.Add(aiMessageModel); RequestScrollToBottom?.Invoke(); try { var response = await _chatBackend.AskAsync(userText, _currentSession, AppPrompt.InteractiveGuidePrompt); await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { aiMessageModel.Message = response; RequestScrollToBottom?.Invoke(); }); } catch (Exception ex) { await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { aiMessageModel.Message = "处理表单提交时出错: " + ex.Message; RequestScrollToBottom?.Invoke(); }); } } /// /// 取消当前正在执行的工作流 /// [RelayCommand(CanExecute = nameof(CanCancelWorkflow))] private void CancelWorkflow() { _workflowCts?.Cancel(); } private bool CanCancelWorkflow() => IsWorkflowRunning; /// /// 执行工作流模式 /// private async Task ExecuteWorkflowAsync(string goal) { int historyCountBefore = _currentSession!.History.Count; // 若已有工作流在跑,先取消 _workflowCts?.Cancel(); _workflowCts?.Dispose(); _workflowCts = new CancellationTokenSource(); IsWorkflowRunning = true; ((CommunityToolkit.Mvvm.Input.IRelayCommand)CancelWorkflowCommand).NotifyCanExecuteChanged(); // 创建工作流状态消息 var workflowStatusMessage = new WorkflowStatusMessage { Title = "工作流执行中..." }; var workflowMessage = new ChatMessageModel(AuthorType.AI, string.Empty, lastMessage: true) { Type = MessageType.WorkflowStatus, SpecialContent = workflowStatusMessage }; await Dispatcher.UIThread.InvokeAsync(() => { ChatMessages.Add(workflowMessage); RequestScrollToBottom?.Invoke(); }); try { // 创建 ReAct 工作流 Agent var agent = new Workflow.Agent(_chatBackend) { Name = goal, Description = goal }; // 订阅事件 agent.PlanCompleted += (sender, e) => { Dispatcher.UIThread.InvokeAsync(() => { workflowStatusMessage.Title = $"工作流计划(共 {e.Plan.Steps.Count} 个步骤)"; var stepModels = Workflow.Agent.ConvertPlanToStepModels(e.Plan); workflowStatusMessage.Steps = stepModels; }); }; agent.StepStatusUpdated += (sender, e) => { Dispatcher.UIThread.InvokeAsync(() => { // 更新对应步骤的状态 var stepModel = workflowStatusMessage.Steps.FirstOrDefault(s => s.Id == e.Step.Id); if (stepModel != null) { stepModel.Status = e.Step.Status switch { PlanStepStatus.Pending => WorkflowStepStatus.Pending, PlanStepStatus.Running => WorkflowStepStatus.Running, PlanStepStatus.Completed => WorkflowStepStatus.Completed, PlanStepStatus.Failed => WorkflowStepStatus.Failed, _ => WorkflowStepStatus.Pending }; if (!string.IsNullOrEmpty(e.Step.Result)) { stepModel.OutputResult = e.Step.Result; } } }); }; agent.StepThoughtUpdated += (sender, e) => { Dispatcher.UIThread.InvokeAsync(() => { // 创建独立的思考消息,像 CodeAgent 一样顺序显示 var thoughtMessage = new ChatMessageModel(AuthorType.AI, e.Thought, lastMessage: false) { Type = MessageType.Text }; // 标记最后一条消息 if (ChatMessages.Count > 0) { ChatMessages.Last().LastMessage = false; } thoughtMessage.LastMessage = true; ChatMessages.Add(thoughtMessage); RequestScrollToBottom?.Invoke(); }); }; // 执行工作流(带取消与超时) string result = await agent.ExecuteAsync(goal, _workflowCts.Token, DefaultWorkflowTimeout); // 更新最终结果 await Dispatcher.UIThread.InvokeAsync(() => { // 确保之前的消息不是最后一条 if (ChatMessages.Count > 0) { ChatMessages.Last().LastMessage = false; } workflowStatusMessage.Title = "工作流执行完成"; workflowMessage.Message = result; workflowMessage.Type = MessageType.Text; workflowMessage.SpecialContent = null; workflowMessage.LastMessage = true; RequestScrollToBottom?.Invoke(); }); // 添加到会话历史 _currentSession.AddUserMessage(goal); _currentSession.AddAssistantMessage(result); } catch (OperationCanceledException) { await Dispatcher.UIThread.InvokeAsync(() => { workflowStatusMessage.Title = "工作流已取消或超时"; workflowMessage.Message = "工作流已被用户取消或已达到超时时间。"; workflowMessage.Type = MessageType.Text; workflowMessage.SpecialContent = null; }); } catch (Exception ex) { await Dispatcher.UIThread.InvokeAsync(() => { workflowStatusMessage.Title = "工作流执行失败"; workflowMessage.Message = $"执行失败: {ex.Message}"; workflowMessage.Type = MessageType.Text; workflowMessage.SpecialContent = null; }); if (_currentSession.History.Count > historyCountBefore) { _currentSession.History.RemoveAt(_currentSession.History.Count - 1); } } finally { _workflowCts?.Dispose(); _workflowCts = null; IsWorkflowRunning = false; _ = Dispatcher.UIThread.InvokeAsync(() => ((CommunityToolkit.Mvvm.Input.IRelayCommand)CancelWorkflowCommand).NotifyCanExecuteChanged()); } } /// /// 执行 Ask 模式(普通对话) /// private async Task ExecuteAskModeAsync(string currentUserInput) { // 流式请求后端,传入当前会话 // AskStreamAsync 内部会通过 ChatSession 添加用户消息和助手消息,并自动更新会话状态 int historyCountBefore = _currentSession!.History.Count; // 创建 AI 消息模型用于流式更新 var aiMessageModel = new ChatMessageModel(AuthorType.AI, string.Empty, lastMessage: true); ChatMessages.Add(aiMessageModel); RequestScrollToBottom?.Invoke(); try { // 使用流式方式获取响应,每次收到片段立即更新 UI await foreach (var chunk in _chatBackend.AskStreamAsync(currentUserInput, _currentSession, AppPrompt.InteractiveGuidePrompt)) { // 在 UI 线程上更新消息内容 await Dispatcher.UIThread.InvokeAsync(() => { // 使用 MarkdownBuilder.Append 进行高效的流式更新(LiveMarkdown.Avalonia 推荐方式) aiMessageModel.MarkdownBuilder.Append(chunk); // 使用节流滚动,减少滚动调用频率,避免滚动条跳跃 ThrottledScrollToBottom(); }); } // 流式更新完成后,确保最后滚动一次到底部 await Dispatcher.UIThread.InvokeAsync(() => { RequestScrollToBottom?.Invoke(); }); } catch (HttpOperationException) // 当网络异常时,我们将最后一条消息退回到输入框,方便用户再次点击发送 { Rollback(currentUserInput, historyCountBefore, aiMessageModel); return; } catch (Exception) { Rollback(currentUserInput, historyCountBefore, aiMessageModel); return; } } /// /// 节流滚动到底部,用于流式更新时减少滚动调用频率 /// private void ThrottledScrollToBottom() { var now = DateTime.Now; var timeSinceLastScroll = (now - _lastScrollTime).TotalMilliseconds; // 如果距离上次滚动超过节流时间,才执行滚动 if (timeSinceLastScroll >= ScrollThrottleMs) { _lastScrollTime = now; RequestScrollToBottom?.Invoke(); } } // ── 散点文件加载综合卡片命令 ───────────────────────────────────────── /// /// 为散点加载卡片中的文件路径字段打开文件选择框 /// [RelayCommand] private async Task PickFileForXyzLoadCard(Models.SpecialMessages.XyzLoadCardMessage? card) { if (card == null) return; try { var files = await RequestFileSelection?.Invoke(); if (files != null && files.Count > 0 && files[0].Path != null) { card.FilePath = files[0].Path.LocalPath; } } catch (Exception ex) { Console.WriteLine($"文件选择错误: {ex.Message}"); } } /// /// 提交散点文件加载:直接调用应用层 API 完成「加载文件→获取列信息→展示列匹配」, /// 整个过程不经过 AI,由卡片本身驱动。 /// [RelayCommand] private async Task SubmitXyzLoadCard(object? parameter) { if (parameter is not ChatMessageModel message || message.Type != MessageType.XyzLoadCard || message.SpecialContent is not Models.SpecialMessages.XyzLoadCardMessage card) { return; } var path = card.FilePath?.Trim(); if (string.IsNullOrWhiteSpace(path)) { return; } // 进入加载中状态 card.IsLoading = true; card.StatusMessage = string.Empty; try { // ① 调用应用层加载散点文件 var loadResult = await _appController.ExecuteAsync(AppAction.CreateLoadXyz(path)); if (!loadResult.Success) { card.StatusMessage = loadResult.Message ?? "文件加载失败,请检查路径是否正确"; card.IsLoading = false; return; } // ② 构建 CSV 数据预览(本地解析,最多 20 行) var tableMessage = Utils.CsvPreviewHelper.TryBuildCsvPreviewTable(path, maxPreviewRows: 20); if (tableMessage != null) { card.TablePreview = tableMessage; } // ③ 从应用层获取必需列与可用列 var columnsResult = await _appController.ExecuteAsync( AppAction.CreateAction(AgentIntegration.AppActionType.GriddingModuleGetColumns)); if (!columnsResult.Success || string.IsNullOrWhiteSpace(columnsResult.Message)) { card.StatusMessage = columnsResult.Message ?? "无法获取列信息,请重试"; card.IsLoading = false; return; } // ④ 解析列信息并填充列头匹配字段 var columnDef = BuildColumnMatchDefinitionFromJson(columnsResult.Message); if (columnDef == null || columnDef.Fields.Count == 0) { card.StatusMessage = "列信息格式无效,无法构建列头匹配"; card.IsLoading = false; return; } card.ColumnMatchDefinition = columnDef; card.ColumnMatchFields.Clear(); foreach (var field in columnDef.Fields) { // 尝试智能预选:若可用列中有与必需列同名的项则预选它,否则选第一项 var defaultValue = field.Options?.FirstOrDefault( o => string.Equals(o, field.Id, StringComparison.OrdinalIgnoreCase)) ?? field.Options?.FirstOrDefault() ?? string.Empty; card.ColumnMatchFields.Add(new Models.Form.FormFieldEntry { Id = field.Id, Label = field.Label, Type = Models.Form.FormFieldType.Choice, Options = field.Options != null ? new List(field.Options) : new List(), Required = true, CurrentValue = defaultValue, }); } // ⑤ 切换到 FileLoaded 阶段(显示预览 + 列头匹配) card.Phase = Models.SpecialMessages.XyzLoadPhase.FileLoaded; RequestScrollToBottom?.Invoke(); } catch (Exception ex) { card.StatusMessage = $"加载失败:{ex.Message}"; } finally { card.IsLoading = false; } } /// /// 确认列头匹配:直接调用应用层 API 完成匹配,切换到 Completed 阶段, /// 最后生成统一信息摘要发给 AI 以继续后续对话。 /// [RelayCommand] private async Task ConfirmXyzColumnMatch(object? parameter) { if (parameter is not ChatMessageModel message || message.Type != MessageType.XyzLoadCard || message.SpecialContent is not Models.SpecialMessages.XyzLoadCardMessage card || _currentSession == null) { return; } if (card.ColumnMatchFields.Count == 0) { return; } // ① 构建匹配参数字典(必需列 → 选中的可用列) var mappingParams = card.ColumnMatchFields .Where(f => !string.IsNullOrEmpty(f.CurrentValue)) .ToDictionary(f => f.Id, f => (object)f.CurrentValue); // ② 切换卡片状态(先禁用按钮) card.MatchButtonLabel = "正在确认..."; try { // ③ 调用应用层执行列头匹配 var matchResult = await _appController.ExecuteAsync( AppAction.CreateAction(AgentIntegration.AppActionType.GriddingModuleMatchColumns, mappingParams)); if (!matchResult.Success) { card.MatchButtonLabel = "确认匹配"; card.StatusMessage = matchResult.Message ?? "列头匹配失败,请重试"; return; } await _appController.ExecuteAsync( AppAction.CreateAction(AppActionType.GriddingModuleImport, null)); // ④ 切换卡片到已完成状态 card.MatchButtonLabel = "已确认"; card.Phase = Models.SpecialMessages.XyzLoadPhase.Completed; card.StatusMessage = string.Empty; // ⑤ 生成统一信息摘要,作为用户消息发给 AI var sb = new StringBuilder(); sb.AppendLine("散点文件加载完成,详情如下:"); sb.Append(" • 文件路径:").AppendLine(card.FilePath); if (card.TablePreview != null) { sb.Append(" • 数据:共 ").Append(card.TablePreview.TotalRowCount).Append(" 行,") .Append(card.TablePreview.ColumnNames.Count).AppendLine(" 列"); if (card.TablePreview.ColumnNames.Count > 0) { sb.Append(" • 文件列:").AppendLine(string.Join(", ", card.TablePreview.ColumnNames)); } } if (card.ColumnMatchFields.Count > 0) { var mappingDesc = string.Join(",", card.ColumnMatchFields .Select(f => $"{f.Label} → {f.CurrentValue}")); sb.Append(" • 列头匹配:").AppendLine(mappingDesc); } var summaryText = sb.ToString().TrimEnd(); // ⑥ 加入对话流 ChatMessages.ToList().ForEach(m => m.LastMessage = false); ChatMessages.Add(new ChatMessageModel(AuthorType.User, summaryText)); _currentSession.AddUserMessage(summaryText); var aiMessageModel = new ChatMessageModel(AuthorType.AI, string.Empty, lastMessage: true); ChatMessages.Add(aiMessageModel); RequestScrollToBottom?.Invoke(); // ⑦ 请求 AI 继续后续步骤 try { var response = await _chatBackend.AskAsync(summaryText, _currentSession, AppPrompt.InteractiveGuidePrompt); await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { aiMessageModel.Message = response; RequestScrollToBottom?.Invoke(); }); } catch (Exception ex) { await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { aiMessageModel.Message = "与 AI 通信时出错: " + ex.Message; RequestScrollToBottom?.Invoke(); }); } } catch (Exception ex) { card.MatchButtonLabel = "确认匹配"; card.StatusMessage = $"操作失败:{ex.Message}"; } } /// /// 点击网格化参数卡片中的"生成"按钮:收集用户编辑后的参数值,依次调用设置参数和执行成图, /// 完成后生成摘要作为用户消息注入对话并请求 AI 继续。 /// [RelayCommand] private async Task SubmitGriddingParamCard(object? parameter) { if (parameter is not ChatMessageModel message || message.Type != MessageType.GriddingParamCard || message.SpecialContent is not Models.SpecialMessages.GriddingParamCardMessage card || _currentSession == null) { return; } if (card.Phase != Models.SpecialMessages.GriddingParamCardPhase.Ready) { return; } card.GenerateButtonLabel = "生成中..."; card.Phase = Models.SpecialMessages.GriddingParamCardPhase.Generating; try { // ① 收集当前编辑值,构建参数字典并序列化为 YAML var paramsDict = card.Items .Where(i => !string.IsNullOrWhiteSpace(i.Name)) .ToDictionary(i => i.Name, i => (object)(i.ValueText ?? string.Empty)); var serializer = new SerializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .Build(); var paramsYaml = serializer.Serialize(paramsDict); // ② 设置参数 var setAction = AppAction.CreateAction( AgentIntegration.AppActionType.GriddingModuleSetParameters, new Dictionary { { "parameters", paramsYaml } }); var setResult = await _appController.ExecuteAsync(setAction); if (!setResult.Success) { card.StatusMessage = setResult.Message ?? "设置参数失败,请重试"; card.Phase = Models.SpecialMessages.GriddingParamCardPhase.Error; card.GenerateButtonLabel = "生成"; return; } // ③ 执行成图 var runResult = await _appController.ExecuteAsync( AppAction.CreateAction(AgentIntegration.AppActionType.GriddingModuleRun)); if (!runResult.Success) { card.StatusMessage = runResult.Message ?? "成图失败,请重试"; card.Phase = Models.SpecialMessages.GriddingParamCardPhase.Error; card.GenerateButtonLabel = "生成"; return; } // ④ 切换卡片到完成状态 card.Phase = Models.SpecialMessages.GriddingParamCardPhase.Done; card.StatusMessage = string.Empty; // ⑤ 生成摘要,作为用户消息注入对话 var sb = new StringBuilder(); sb.AppendLine("网格化成图完成,详情如下:"); foreach (var item in card.Items) { if (!string.IsNullOrWhiteSpace(item.Name)) sb.Append(" • ").Append(item.Name).Append(":").AppendLine(item.ValueText ?? string.Empty); } var summaryText = sb.ToString().TrimEnd(); // ⑥ 加入对话流 ChatMessages.ToList().ForEach(m => m.LastMessage = false); ChatMessages.Add(new ChatMessageModel(AuthorType.User, summaryText)); _currentSession.AddUserMessage(summaryText); var aiMessageModel = new ChatMessageModel(AuthorType.AI, string.Empty, lastMessage: true); ChatMessages.Add(aiMessageModel); RequestScrollToBottom?.Invoke(); // ⑦ 请求 AI 继续 try { var response = await _chatBackend.AskAsync(summaryText, _currentSession, AppPrompt.InteractiveGuidePrompt); await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { aiMessageModel.Message = response; RequestScrollToBottom?.Invoke(); }); } catch (Exception ex) { await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { aiMessageModel.Message = "与 AI 通信时出错: " + ex.Message; RequestScrollToBottom?.Invoke(); }); } } catch (Exception ex) { card.StatusMessage = $"操作失败:{ex.Message}"; card.Phase = Models.SpecialMessages.GriddingParamCardPhase.Error; card.GenerateButtonLabel = "生成"; } } /// /// 从 GetColumns 返回的 JSON 构建列头匹配表单定义(内部复用 FormRequestNotifier 的逻辑)。 /// private static Models.Form.FormDefinition? BuildColumnMatchDefinitionFromJson(string json) { try { var obj = Newtonsoft.Json.Linq.JObject.Parse(json); var required = obj["RequiredColumns"]?.ToObject>(); var available = obj["AvailableColumns"]?.ToObject>(); if (required == null || required.Count == 0 || available == null) return null; return new Models.Form.FormDefinition { Id = "gridding-match-columns", Title = "列头匹配", SubmitTarget = "GriddingModuleMatchColumns", SubmitLabel = "确认匹配", Fields = required.Select(req => new Models.Form.FormField { Id = req, Label = req, Type = Models.Form.FormFieldType.Choice, Options = new List(available), Required = true, }).ToList(), }; } catch { return null; } } } }