using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Windows; using System.Windows.Controls; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Text.Json; using System.Threading; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using ArcGIS.Desktop.Core; using ArcGIS.Desktop.Core.Geoprocessing; using LinkToolAddin.client; using LinkToolAddin.common; using LinkToolAddin.host; using LinkToolAddin.host.llm; using LinkToolAddin.host.llm.entity; using LinkToolAddin.message; using LinkToolAddin.resource; using LinkToolAddin.server; using log4net; using log4net.Appender; using log4net.Config; using log4net.Layout; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Server; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace LinkToolAddin.ui.dockpane { public class ItemModel { public string Content { get; set; } } /// /// Interaction logic for DialogDockpaneView.xaml /// public partial class DialogDockpaneView : UserControl { private static ILog log = LogManager.GetLogger(typeof(DialogDockpaneView)); private List idList = new List(); private ConcurrentDictionary messageDict = new ConcurrentDictionary(); private ConcurrentDictionary borderItemsDict = new ConcurrentDictionary(); public DialogDockpaneView() { InitLogger(); InitializeComponent(); DataContext = this; } private async void TestServer_OnClick(object sender, RoutedEventArgs e) { log.Info("TestServer Clicked"); } protected void InitLogger() { // 1. 创建控制台输出器(Appender) var consoleAppender = new ConsoleAppender { Layout = new PatternLayout("%date [%thread] %-5level %logger - %message%newline"), Threshold = log4net.Core.Level.Info // 仅输出 Info 及以上级别 }; consoleAppender.ActivateOptions(); // 激活配置 // 2. 创建文件滚动输出器(按大小滚动) var fileAppender = new RollingFileAppender { File = Path.Combine("Logs", "D:\\linktool_app.log"), // 日志文件路径 AppendToFile = true, // 追加模式 RollingStyle = RollingFileAppender.RollingMode.Size, // 按文件大小滚动 MaxSizeRollBackups = 10, // 保留 10 个历史文件 MaximumFileSize = "1MB", // 单个文件最大 1MB StaticLogFileName = true, // 固定文件名(否则自动追加序号) Layout = new PatternLayout("%date [%thread] %-5level %logger - %message%newline"), Threshold = log4net.Core.Level.Info // 仅输出 Info 及以上级别 }; fileAppender.ActivateOptions(); // 激活配置 // 3. 直接通过 BasicConfigurator 注册 Appender BasicConfigurator.Configure(consoleAppender, fileAppender); log = LogManager.GetLogger(typeof(DialogDockpaneView)); // 测试日志输出 log.Debug("Debug 日志(控制台可见)"); log.Info("Info 日志(控制台和文件可见)"); log.Error("Error 日志(严重问题)"); } private void SendButton_OnClick(object sender, RoutedEventArgs e) { StatusTextBlock.Text = "正在读取用户输入和工具列表"; string question = QuestionTextbox.Text; string defaultGdbPath = Project.Current.DefaultGeodatabasePath; string gdbPath = @""; long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); MessageListItem userMsg = new ChatMessageItem() { content = question, role = "user", type = MessageType.CHAT_MESSAGE, id = timestamp.ToString() }; Border userMsgBoder = GetUserChatBorder(userMsg); idList.Add(timestamp.ToString()); messageDict[timestamp.ToString()] = userMsg; QuestionTextbox.Text = ""; borderItemsDict[timestamp.ToString()] = userMsgBoder; ChatHistoryStackPanel.Children.Add(userMsgBoder); string model = (ModelComboBox.SelectedItem as ComboBoxItem).Content.ToString() is null ? "qwen3-235b-a22b" : (ModelComboBox.SelectedItem as ComboBoxItem).Content.ToString(); if (model == "自定义") { model = ShowInputBox("自定义模型", "请输入模型名称:", ""); } ScrollViewer.ScrollToBottom(); Gateway.SendMessageStream(question,model,defaultGdbPath,NewMessage_Recall); } private string ShowInputBox(string title, string message, string defaultValue = "") { // 创建一个自定义的输入对话框 var dialog = new System.Windows.Window { Title = title, Width = 300, Height = 150, WindowStyle = WindowStyle.ToolWindow, ResizeMode = ResizeMode.NoResize, Topmost = true, WindowStartupLocation = WindowStartupLocation.CenterOwner }; // 设置对话框内容 var stackPanel = new StackPanel { Margin = new Thickness(10) }; stackPanel.Children.Add(new TextBlock { Text = message, Margin = new Thickness(0, 0, 0, 5) }); var textBox = new TextBox { Text = defaultValue, Margin = new Thickness(0, 0, 0, 10) }; stackPanel.Children.Add(textBox); var buttonPanel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right }; var okButton = new Button { Content = "确定", Width = 75, Margin = new Thickness(0, 0, 5, 0) }; okButton.Click += (sender, e) => dialog.Close(); var cancelButton = new Button { Content = "取消", Width = 75 }; cancelButton.Click += (sender, e) => { textBox.Text = null; dialog.Close(); }; buttonPanel.Children.Add(okButton); buttonPanel.Children.Add(cancelButton); stackPanel.Children.Add(buttonPanel); dialog.Content = stackPanel; // 显示对话框并获取结果 dialog.ShowDialog(); return textBox.Text; } public void NewMessage_Recall(MessageListItem msg) { string msgId = msg.id; log.Info(msg.content); double verticalOffset = ScrollViewer.VerticalOffset; double viewportHeight = ScrollViewer.ViewportHeight; double contentHeight = ChatHistoryStackPanel.ActualHeight; double tolerance = 24; if (!idList.Contains(msgId)) { //不存在该消息,需添加到ListView中 if (msg.content == "") { if (msg.type == MessageType.END_TAG) { StatusTextBlock.Text = ""; } return; } idList.Add(msgId); messageDict[msgId] = msg; if (msg.role == "user") { if (msg.type == MessageType.TOOL_MESSAGE) { Border border = GetToolChatBorder(msg); borderItemsDict[msgId] = border; ChatHistoryStackPanel.Children.Add(border); StatusTextBlock.Text = "正在执行工具"; }else if (msg.type == MessageType.CHAT_MESSAGE) { Border border = GetUserChatBorder(msg); borderItemsDict[msgId] = border; ChatHistoryStackPanel.Children.Add(border); StatusTextBlock.Text = "正在读取用户输入"; } } else if(msg.role == "assistant") { if (msg.type == MessageType.REASON_MESSAGE) { Border border = GetAiReasonBorder(msg); borderItemsDict[msgId] = border; ChatHistoryStackPanel.Children.Add(border); StatusTextBlock.Text = "深度思考中"; }else if (msg.type == MessageType.CHAT_MESSAGE) { Border border = GetAiChatBorder(msg); borderItemsDict[msgId] = border; ChatHistoryStackPanel.Children.Add(border); StatusTextBlock.Text = "回答生成中"; } } else if (msg.role == "system") { if (msg.type == MessageType.WARNING) { Border border = GetWarningChatBorder(msg); borderItemsDict[msgId] = border; ChatHistoryStackPanel.Children.Add(border); StatusTextBlock.Text = "出现警告信息"; }else if (msg.type == MessageType.ERROR) { Border border = GetWarningChatBorder(msg); borderItemsDict[msgId] = border; ChatHistoryStackPanel.Children.Add(border); StatusTextBlock.Text = "出现错误信息"; } } } else { //已有该消息,只需要修改内容 messageDict[msgId] = msg; if (msg.content == "") { if (msg.type == MessageType.END_TAG) { StatusTextBlock.Text = ""; } ChatHistoryStackPanel.Children.Remove(borderItemsDict[msgId]); borderItemsDict.TryRemove(msgId, out Border border); messageDict.TryRemove(msgId, out MessageListItem tempMsg); idList.Remove(msgId); } if (msg.role == "user") { if (msg.type == MessageType.TOOL_MESSAGE) { Border borderItem = borderItemsDict[msgId]; Grid grid = borderItem.Child as Grid; TextBlock textBlock = grid.Children[1] as TextBlock; ToolMessageItem msgItem = msg as ToolMessageItem; textBlock.Text = msgItem.toolName + " | " + msgItem.status; Button resButton = grid.Children[3] as Button; resButton.Tag = msg; StatusTextBlock.Text = "正在执行工具"; }else if (msg.type == MessageType.CHAT_MESSAGE) { Border borderItem = borderItemsDict[msgId]; Grid grid = borderItem.Child as Grid; TextBox textBox = grid.Children[1] as TextBox; textBox.Text = msg.content; StatusTextBlock.Text = "正在读取用户输入"; } } else if(msg.role == "assistant") { if (msg.type == MessageType.REASON_MESSAGE) { Border borderItem = borderItemsDict[msgId]; Grid grid = borderItem.Child as Grid; TextBox textBox = grid.Children[0] as TextBox; textBox.Text = msg.content; StatusTextBlock.Text = "深度思考中"; }else if (msg.type == MessageType.CHAT_MESSAGE) { Border borderItem = borderItemsDict[msgId]; Grid grid = borderItem.Child as Grid; TextBox textBox = grid.Children[1] as TextBox; textBox.Text = msg.content; StatusTextBlock.Text = "回答生成中"; } }else if (msg.role == "system") { if (msg.type == MessageType.WARNING) { Border borderItem = borderItemsDict[msgId]; Grid grid = borderItem.Child as Grid; TextBox textBox = grid.Children[1] as TextBox; textBox.Text = msg.content; StatusTextBlock.Text = "出现警告信息"; }else if (msg.type == MessageType.ERROR) { Border borderItem = borderItemsDict[msgId]; Grid grid = borderItem.Child as Grid; TextBox textBox = grid.Children[1] as TextBox; textBox.Text = msg.content; } } } if (Math.Abs(verticalOffset + viewportHeight - contentHeight) < tolerance) { ScrollViewer.ScrollToBottom(); } } private Border GetAiChatBorder(MessageListItem msg) { Border border = new Border(); border.Margin = new Thickness(8, 12, 8, 12); border.BorderThickness = new Thickness(0); // border.Background = Brushes.DarkSeaGreen; Grid grid = new Grid(); Image icon = new Image() { Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.linktool.png"), Stretch = Stretch.Fill, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top }; grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(24, GridUnitType.Pixel)}); grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(1, GridUnitType.Star)}); TextBox textBox = new TextBox(); grid.Children.Add(icon); grid.Children.Add(textBox); Grid.SetColumn(icon, 0); Grid.SetColumn(textBox, 1); textBox.IsReadOnly = true; textBox.BorderThickness = new Thickness(0); textBox.Background = Brushes.Transparent; textBox.Text = msg.content; textBox.TextWrapping = TextWrapping.Wrap; border.Child = grid; return border; } private Border GetAiReasonBorder(MessageListItem msg) { Border border = new Border(); border.Margin = new Thickness(8, 12, 8, 12); border.BorderThickness = new Thickness(0); // border.Background = Brushes.DarkSeaGreen; Grid grid = new Grid(); Image icon = new Image() { Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.linktool.png"), Stretch = Stretch.Fill, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top }; Image fold = new Image() { Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.fold.png"), Stretch = Stretch.Fill, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top }; Image unfold = new Image() { Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.unfold.png"), Stretch = Stretch.Fill, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top }; grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(24, GridUnitType.Pixel)}); grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(1, GridUnitType.Star)}); grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(16, GridUnitType.Pixel)}); TextBox textBox = new TextBox(); // grid.Children.Add(icon); grid.Children.Add(textBox); // Grid.SetColumn(icon, 0); Grid.SetColumn(textBox, 1); textBox.IsReadOnly = true; textBox.Foreground = Brushes.Gray; textBox.BorderThickness = new Thickness(2,0,0,0); textBox.BorderBrush = Brushes.Gray; textBox.Background = Brushes.Transparent; textBox.Text = msg.content; textBox.TextWrapping = TextWrapping.Wrap; Button button = new Button(); StackPanel panel = new StackPanel(); panel.Orientation = Orientation.Horizontal; panel.Children.Add(fold); button.Content = panel; button.BorderThickness = new Thickness(0); button.Background = Brushes.Transparent; button.Width = 16; button.Height = 16; button.Padding = new Thickness(0); button.HorizontalAlignment = HorizontalAlignment.Center; button.VerticalAlignment = VerticalAlignment.Top; button.Tag = "fold"; button.Click += FoldButton_OnClick; Grid.SetColumn(button, 2); grid.Children.Add(button); border.Child = grid; return border; } private void FoldButton_OnClick(object sender, RoutedEventArgs e) { Button button = sender as Button; string tag = button.Tag.ToString(); if (tag == "fold") { button.Tag = "unfold"; Grid grid = button.Parent as Grid; TextBox textBox = grid.Children[0] as TextBox; textBox.Visibility = Visibility.Collapsed; StackPanel stackPanel = button.Content as StackPanel; Image unfold = new Image() { Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.unfold.png"), Stretch = Stretch.Fill, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top }; stackPanel.Children.Clear(); stackPanel.Children.Add(unfold); } else { button.Tag = "fold"; Image fold = new Image() { Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.fold.png"), Stretch = Stretch.Fill, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top }; Grid grid = button.Parent as Grid; TextBox textBox = grid.Children[0] as TextBox; textBox.Visibility = Visibility.Visible; StackPanel stackPanel = button.Content as StackPanel; stackPanel.Children.Clear(); stackPanel.Children.Add(fold); } } private Border GetUserChatBorder(MessageListItem msg) { Border border = new Border(); border.Margin = new Thickness(8, 12, 8, 12); border.BorderThickness = new Thickness(0); // border.Background = Brushes.DarkSeaGreen; Grid grid = new Grid(); Image icon = new Image() { Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.user.png"), Stretch = Stretch.Fill, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top }; grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(1, GridUnitType.Star)}); grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(24, GridUnitType.Pixel)}); TextBox textBox = new TextBox(); textBox.HorizontalAlignment = HorizontalAlignment.Right; grid.Children.Add(icon); grid.Children.Add(textBox); Grid.SetColumn(icon, 1); Grid.SetColumn(textBox, 0); textBox.Background = Brushes.Transparent; textBox.IsReadOnly = true; textBox.BorderThickness = new Thickness(0); textBox.Text = msg.content; textBox.TextWrapping = TextWrapping.Wrap; border.Child = grid; return border; } private Border GetWarningChatBorder(MessageListItem msg) { Border border = new Border(); border.Margin = new Thickness(24); border.Padding = new Thickness(8); border.BorderThickness = new Thickness(1); border.BorderBrush = Brushes.DarkGoldenrod; border.CornerRadius = new CornerRadius(3); border.Background = Brushes.Moccasin; Grid grid = new Grid(); // Image icon = new Image() // { // Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.tool.png"), // Stretch = Stretch.Fill, // HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center // }; // icon.Margin = new Thickness(0, 0, 8, 0); // grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(24, GridUnitType.Pixel)}); grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(1, GridUnitType.Star)}); TextBlock textBlock = new TextBlock(); textBlock.Text = (msg as ChatMessageItem).content; textBlock.TextWrapping = TextWrapping.Wrap; // grid.Children.Add(icon); grid.Children.Add(textBlock); // Grid.SetColumn(icon, 0); Grid.SetColumn(textBlock, 1); border.Child = grid; return border; } private Border GetErrorChatBorder(MessageListItem msg) { Border border = new Border(); border.Margin = new Thickness(24); border.Padding = new Thickness(8); border.BorderThickness = new Thickness(1); border.BorderBrush = Brushes.Crimson; border.CornerRadius = new CornerRadius(3); border.Background = Brushes.LightPink; Grid grid = new Grid(); // Image icon = new Image() // { // Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.tool.png"), // Stretch = Stretch.Fill, // HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center // }; // icon.Margin = new Thickness(0, 0, 8, 0); grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(24, GridUnitType.Pixel)}); grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(1, GridUnitType.Star)}); TextBlock textBlock = new TextBlock(); textBlock.Text = (msg as ChatMessageItem).content; textBlock.TextWrapping = TextWrapping.Wrap; // grid.Children.Add(icon); grid.Children.Add(textBlock); // Grid.SetColumn(icon, 0); Grid.SetColumn(textBlock, 1); border.Child = grid; return border; } private Border GetToolChatBorder(MessageListItem msg) { Border border = new Border(); border.Margin = new Thickness(24); border.Padding = new Thickness(8); border.BorderThickness = new Thickness(1); border.BorderBrush = Brushes.Gray; border.CornerRadius = new CornerRadius(3); // border.Background = Brushes.DarkSeaGreen; Grid grid = new Grid(); Image icon = new Image() { Source = LocalResource.ReadImageByResource("LinkToolAddin.resource.img.tool.png"), Stretch = Stretch.Fill, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center }; icon.Margin = new Thickness(0, 0, 8, 0); grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(24, GridUnitType.Pixel)}); grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(1, GridUnitType.Star)}); grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(36, GridUnitType.Pixel)}); grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(36, GridUnitType.Pixel)}); TextBlock textBlock = new TextBlock(); ToolMessageItem toolMsg = msg as ToolMessageItem; textBlock.Text = toolMsg.toolName + " | " + toolMsg.status; textBlock.TextWrapping = TextWrapping.Wrap; grid.Children.Add(icon); grid.Children.Add(textBlock); Grid.SetColumn(icon, 0); Grid.SetColumn(textBlock, 1); Button argsButton = new Button(); argsButton.Content = "参数"; argsButton.Tag = msg as ToolMessageItem; argsButton.Click += ToolArgsButton_OnClick; Button resButton = new Button(); resButton.Content = "结果"; resButton.Tag = msg as ToolMessageItem; resButton.Click += ToolResButton_OnClick; grid.Children.Add(argsButton); Grid.SetColumn(argsButton, 2); grid.Children.Add(resButton); Grid.SetColumn(resButton, 3); if (toolMsg.toolParams.ContainsKey("toolName")) { grid.ColumnDefinitions.Add(new ColumnDefinition(){Width = new GridLength(36, GridUnitType.Pixel)}); Button checkButton = new Button(); checkButton.Content = "检查"; checkButton.Tag = toolMsg; checkButton.Click += ToolCheckButton_OnClick; grid.Children.Add(checkButton); Grid.SetColumn(checkButton, 4); } border.Child = grid; return border; } private void ToolCheckButton_OnClick(object sender, RoutedEventArgs e) { try { Button button = sender as Button; ToolMessageItem toolItem = button.Tag as ToolMessageItem; string gisToolName = toolItem.toolParams["toolName"] as string; JArray gisToolParams = toolItem.toolParams["toolParams"] as JArray; List gitToolParamsList = (gisToolParams as JArray).Select(token => token.ToString()).ToList(); Geoprocessing.OpenToolDialog(gisToolName,gitToolParamsList); }catch (Exception ex) { log.Error(ex); ArcGIS.Desktop.Framework.Dialogs.MessageBox.Show(ex.Message,"打开工具失败"); } } private void ToolArgsButton_OnClick(object sender, RoutedEventArgs e) { Button button = sender as Button; ToolMessageItem toolItem = button.Tag as ToolMessageItem; ArcGIS.Desktop.Framework.Dialogs.MessageBox.Show(JsonConvert.SerializeObject(toolItem.toolParams),toolItem.toolName+"工具参数"); } private void ToolResButton_OnClick(object sender, RoutedEventArgs e) { Button button = sender as Button; ToolMessageItem toolItem = button.Tag as ToolMessageItem; ArcGIS.Desktop.Framework.Dialogs.MessageBox.Show(toolItem.result,toolItem.toolName+"运行结果"); } private void TestButton_OnClick(object sender, RoutedEventArgs e) { long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); MessageListItem toolMessageItem = new ToolMessageItem { toolName = "toolName", toolParams = new Dictionary(), type = MessageType.TOOL_MESSAGE, status = "success", content = "JsonConvert.SerializeObject(toolResponse)", id = (timestamp + 1).ToString(), role = "user" }; NewMessage_Recall(toolMessageItem); } private void ClearButton_OnClick(object sender, RoutedEventArgs e) { idList.Clear(); messageDict.Clear(); borderItemsDict.Clear(); ChatHistoryStackPanel.Children.Clear(); QuestionTextbox.Clear(); StatusTextBlock.Text = ""; } private void TopButton_OnClick(object sender, RoutedEventArgs e) { ScrollViewer.ScrollToTop(); } private void BottomButton_OnClick(object sender, RoutedEventArgs e) { ScrollViewer.ScrollToBottom(); } private void StopButton_OnClick(object sender, RoutedEventArgs e) { Gateway.StopConversation(); StatusTextBlock.Text = ""; } private void ModelComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { log.Info("ModelComboBox_OnSelectionChanged"); string model = ModelComboBox.SelectedValue.ToString(); log.Info(model); } private void QuestionTextbox_OnKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) { // Shift + Enter: 插入换行 QuestionTextbox.SelectedText = Environment.NewLine; e.Handled = true; } else { // Enter: 触发发送 e.Handled = true; SendButton_OnClick(sender, null); } } } private void QuestionTextbox_OnPreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) { // Shift + Enter: 插入换行 QuestionTextbox.SelectedText = Environment.NewLine; e.Handled = true; } else { // Enter: 触发发送 e.Handled = true; SendButton_OnClick(sender, null); } } } } }