|
|
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;
|
|
|
|
|
|
// 将当前可用表单 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;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// <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 0:Loading),并立即异步加载参数填充。
|
|
|
/// 卡片自治完成「加载参数 → 用户编辑 → 点击生成 → 设置参数 + 执行成图」全流程。
|
|
|
/// </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;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
} |