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#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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;
}
}
}
}