You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kev/Drawer/AI/ViewModels/MainWindowViewModel.cs

1336 lines
52 KiB
C#

1 month ago
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<Task<IReadOnlyList<IStorageFile>>>? 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<ChatMessageModel> _chatMessages;
[ObservableProperty]
private ObservableCollection<ChatSessionItemViewModel> _sessions;
private ChatSessionItemViewModel? _selectedSessionItem;
[ObservableProperty]
private ObservableCollection<PendingFileModel> _pendingFiles = new();
private WorkflowMode _currentWorkflowMode = WorkflowMode.Ask;
/// <summary>
/// 工作流取消令牌源;执行中时非 null用于取消或超时
/// </summary>
private CancellationTokenSource? _workflowCts;
/// <summary>
/// 工作流默认超时时间
/// </summary>
private static readonly TimeSpan DefaultWorkflowTimeout = TimeSpan.FromMinutes(10);
/// <summary>
/// 当前是否有工作流正在执行(用于显示取消按钮)
/// </summary>
[ObservableProperty]
private bool _isWorkflowRunning;
/// <summary>
/// 当前工作模式(用于 ComboBox 绑定)
/// </summary>
public WorkflowMode CurrentWorkflowMode
{
get => _currentWorkflowMode;
set
{
if (SetProperty(ref _currentWorkflowMode, value))
{
OnPropertyChanged(nameof(CurrentWorkflowModeIndex));
OnWorkflowModeChanged();
}
}
}
/// <summary>
/// 当前工作模式的索引(用于 ComboBox SelectedIndex 绑定)
/// </summary>
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<ChatMessageModel>();
Sessions = new ObservableCollection<ChatSessionItemViewModel>();
// 订阅会话管理器事件
_sessionManager.SessionsChanged += OnSessionsChanged;
_sessionManager.CurrentSessionChanged += OnCurrentSessionChanged;
// 初始化会话列表
RefreshSessions();
// 如果没有当前会话,创建一个新的
if (_sessionManager.CurrentSession == null)
{
CreateNewSession();
}
else
{
SwitchToSession(_sessionManager.CurrentSession);
}
}
/// <summary>
/// 获取当前会话
/// </summary>
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);
}
}
/// <summary>
/// 清理聊天界面内容
/// </summary>
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;
// 将当前可用表单 SchemaYAML合并进本会话的 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;
}
}
}
/// <summary>
/// 从会话存储构建 UI 消息列表(仅从 Store 读取,不解析 History
/// </summary>
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);
}
/// <summary>
/// 工作模式改变时的处理
/// </summary>
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<PendingFileModel> 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);
}
/// <summary>
/// 为表单中的文件路径字段打开文件选择框,并将选中的路径写入该字段
/// </summary>
[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}");
}
}
/// <summary>
/// 为参数集卡片中的文件路径项打开文件选择框,并将选中的路径写入该项
/// </summary>
[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}");
}
}
/// <summary>
/// 将参数集卡片中的当前值提交到网格化模块(设置网格化参数)
/// </summary>
[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}");
}
}
/// <summary>
/// 在指定会话中插入一条表单消息AI 侧展示,供用户填写并提交)。同时写入该会话的 Store。
/// 仅当目标会话为当前显示会话时更新 ChatMessages 并滚动,避免串会话。
/// </summary>
/// <param name="targetSession">要插入的会话;为 null 时使用当前选中会话(兼容旧调用)</param>
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();
}
}
/// <summary>
/// 在指定会话中插入一条参数集消息(将业务层 JSON 转为卡片展示)。同时写入该会话的 Store。
/// 仅当目标会话为当前显示会话时更新 ChatMessages 并滚动,避免串会话。
/// </summary>
/// <param name="json">参数 JSON</param>
/// <param name="title">卡片标题</param>
/// <param name="targetSession">要插入的会话;为 null 时使用当前选中会话(兼容旧调用)</param>
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();
}
}
/// <summary>
/// 在指定会话中插入一张网格化参数设置综合卡片Phase 0Loading并立即异步加载参数填充。
/// 卡片自治完成「加载参数 → 用户编辑 → 点击生成 → 设置参数 + 执行成图」全流程。
/// </summary>
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);
}
/// <summary>
/// 异步从业务层获取网格化参数,填充到卡片的 Items 中,并切换卡片到 Ready 阶段。
/// </summary>
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;
});
}
}
/// <summary>
/// 在指定会话中插入一张散点文件加载综合卡片Phase 0等待用户选择文件
/// </summary>
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();
}
}
/// <summary>
/// 表单提交:收集表单值,作为结构化用户消息注入对话并请求 AI 继续
/// </summary>
[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();
});
}
}
/// <summary>
/// 取消当前正在执行的工作流
/// </summary>
[RelayCommand(CanExecute = nameof(CanCancelWorkflow))]
private void CancelWorkflow()
{
_workflowCts?.Cancel();
}
private bool CanCancelWorkflow() => IsWorkflowRunning;
/// <summary>
/// 执行工作流模式
/// </summary>
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());
}
}
/// <summary>
/// 执行 Ask 模式(普通对话)
/// </summary>
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;
}
}
/// <summary>
/// 节流滚动到底部,用于流式更新时减少滚动调用频率
/// </summary>
private void ThrottledScrollToBottom()
{
var now = DateTime.Now;
var timeSinceLastScroll = (now - _lastScrollTime).TotalMilliseconds;
// 如果距离上次滚动超过节流时间,才执行滚动
if (timeSinceLastScroll >= ScrollThrottleMs)
{
_lastScrollTime = now;
RequestScrollToBottom?.Invoke();
}
}
// ── 散点文件加载综合卡片命令 ─────────────────────────────────────────
/// <summary>
/// 为散点加载卡片中的文件路径字段打开文件选择框
/// </summary>
[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}");
}
}
/// <summary>
/// 提交散点文件加载:直接调用应用层 API 完成「加载文件→获取列信息→展示列匹配」,
/// 整个过程不经过 AI由卡片本身驱动。
/// </summary>
[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<string>(field.Options) : new List<string>(),
Required = true,
CurrentValue = defaultValue,
});
}
// ⑤ 切换到 FileLoaded 阶段(显示预览 + 列头匹配)
card.Phase = Models.SpecialMessages.XyzLoadPhase.FileLoaded;
RequestScrollToBottom?.Invoke();
}
catch (Exception ex)
{
card.StatusMessage = $"加载失败:{ex.Message}";
}
finally
{
card.IsLoading = false;
}
}
/// <summary>
/// 确认列头匹配:直接调用应用层 API 完成匹配,切换到 Completed 阶段,
/// 最后生成统一信息摘要发给 AI 以继续后续对话。
/// </summary>
[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}";
}
}
/// <summary>
/// 点击网格化参数卡片中的"生成"按钮:收集用户编辑后的参数值,依次调用设置参数和执行成图,
/// 完成后生成摘要作为用户消息注入对话并请求 AI 继续。
/// </summary>
[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<string, object> { { "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 = "生成";
}
}
/// <summary>
/// 从 GetColumns 返回的 JSON 构建列头匹配表单定义(内部复用 FormRequestNotifier 的逻辑)。
/// </summary>
private static Models.Form.FormDefinition? BuildColumnMatchDefinitionFromJson(string json)
{
try
{
var obj = Newtonsoft.Json.Linq.JObject.Parse(json);
var required = obj["RequiredColumns"]?.ToObject<List<string>>();
var available = obj["AvailableColumns"]?.ToObject<List<string>>();
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<string>(available),
Required = true,
}).ToList(),
};
}
catch
{
return null;
}
}
}
}