C#实现本地AI聊天功能(Deepseek R1及其他模型)。
前言
1、C#实现本地AI聊天功能
WPF+OllamaSharpe实现本地聊天功能,可以选择使用Deepseek 及其他模型。
2、此程序默认你已经安装好了Ollama。
在运行前需要线安装好Ollama,如何安装请自行搜索
Ollama下载地址: https://ollama.org.cn
Ollama模型下载地址: https://ollama.org.cn/library
基本运行环境: 根据自己使用的AI搜索对应模型基本配置,有需要使用GPU运行的模型。
此程序除了安装Ollama外,无需安装其他配置。
.
3、相关依赖
OllamaSharpe:启用本地Ollama服务
Markdig.wpf : Markdown格式化输出功能。
Microsoft.Xaml.Behaviors.Wpf :解决部分不能进行命令绑定的控件实现命令绑定功能。
运行

项目
项目结构
项目结构包含如下目录:
.
Commands: 用于命令绑定
Models : 视图对应的模型
Services :一些操作服务
ViewModels:视图模型,主要的业务处理
Views :视图以及一些视图控件的样式资源
具体如下图:

项目代码
Commands
EventsCommand
using System.Windows.Input;
/// <summary>
/// 事件命令:
/// 有些控件的无法绑定命令,但是想要实现命令绑定功能,可通过创建该命令实现。
/// 需要引用Microsoft.Xaml.Behaviors.Wpf组合实现。
/// </summary>
public class EventsCommand<T> : ICommand
{private readonly Action<T> _execute;private readonly Func<T, bool> _canExecute;public EventsCommand(Action<T> execute, Func<T, bool> canExecute = null){_execute = execute ?? throw new ArgumentNullException(nameof(execute));_canExecute = canExecute;}public bool CanExecute(object parameter){return _canExecute?.Invoke((T)parameter) ?? true;}public void Execute(object parameter){_execute((T)parameter);}public event EventHandler CanExecuteChanged{add { CommandManager.RequerySuggested += value; }remove { CommandManager.RequerySuggested -= value; }}
}
ParameterCommand
using System.Windows.Input;
namespace OfflineAI.Commands
{/// <summary>/// 参数命令:/// 可以带参数的命令:/// </summary>public class ParameterCommand : ICommand{public Action<object> execute;public ParameterCommand(Action<object> execute){this.execute = execute;}public event EventHandler? CanExecuteChanged;public bool CanExecute(object? parameter){return CanExecuteChanged != null;}public void Execute(object? parameter){execute?.Invoke(parameter);}}
}
ParameterlessCommand
using System.Windows.Input;
namespace OfflineAI.Commands
{/// <summary>/// 无参数命令:/// 无参数的命令:/// </summary>public class ParameterlessCommand : ICommand{private Action _execute;public ParameterlessCommand(Action execute){_execute = execute;}public event EventHandler? CanExecuteChanged;public bool CanExecute(object? parameter){return CanExecuteChanged != null;}public void Execute(object? parameter){_execute.Invoke();}}
}
Models
ChatRecordModel
namespace OfflineAI.Models
{/// <summary>/// 聊天记录模型/// </summary>public class ChatRecordModel{public ChatRecordModel(int id, string dateTime, string name,string fullName, string data){Id = id;DateTime = dateTime;Name = name;FullName = fullName;Data = data;}/// <summary>/// ID/// </summary>public int Id { get; set; }/// <summary>/// 日期/// </summary>public string DateTime { get; set; }/// <summary>/// 名称/// </summary>public string Name { get; set; }/// <summary>/// 完整名称/// </summary>public string FullName { get; set; }/// <summary>/// 数据/// </summary>public string Data { get; set; }}
}
FileOperationModel
namespace OfflineAI.Models
{public class FileOperationModel{/// <summary>/// 是否生成目录/// </summary>public bool IsGenerateDirectory { get; set; }/// <summary>/// 文件目录/// </summary>public string Directory { get; set; }/// <summary>/// 日期目录(生成的目录)/// </summary>public string DirectoryDateTime { get; set; }/// <summary>/// 文件名称(全路径)/// </summary>public string FileName { get; set; }/// <summary>/// 文件名称(生成文件全路径)/// </summary>public string FileNameDateTime { get; set; }}
}
Services
FileOperation
using OfflineAI.Models;
using System.IO;
namespace OfflineAI.Services
{/// <summary>/// 文件操作类:/// 1、2025-02-24:添加创建日期目录方法。输入文件名,添加时间目录。/// 2、2025-02-24:添加写入数据到文件方法(.txt格式)/// </summary>public class FileOperation{private FileOperationModel _fileOperation;#region 构造函数public FileOperation(string fileName){_fileOperation = new FileOperationModel();_fileOperation.IsGenerateDirectory = true;UpdataFileName(fileName);}#endregion#region 公共方法/// <summary>/// 更新文件名/// </summary>public void UpdataFileName(string fileName){if (Path.GetExtension(fileName).ToLower().Equals("txt"))_fileOperation.FileName = fileName;else_fileOperation.FileName = fileName + ".txt";_fileOperation.Directory = Path.GetDirectoryName(fileName);CreateDateTime();_fileOperation.FileNameDateTime = $"{_fileOperation.DirectoryDateTime}\\{Path.GetFileName(_fileOperation.FileName)}";}/// <summary>/// 写入文本/// </summary>public void WriteTxt(string data){SaveDataAsTxt(data);}/// <summary>/// 写入文本,指定文件名/// </summary>public void WriteTxt(string fileName, string data){UpdataFileName(fileName);SaveDataAsTxt(data);}public string ReadTxt(string fileName){// 使用 using 语句确保资源被正确释放using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))using (StreamReader sr = new StreamReader(fs)){return sr.ReadToEnd();}}/// <summary>/// 获取指定目录下的所有文件(*.txt)/// </summary>public string[] GetFiles(){string[] files = Directory.GetFiles(_fileOperation.Directory, "*.txt", SearchOption.AllDirectories);return files;}/// <summary>/// 获取指定目录下的所有文件(*.txt)/// </summary>public static string[] GetFiles(string directory){string[] files = Directory.GetFiles(directory, "*.txt", SearchOption.AllDirectories);return files;}#endregion#region 私有方法/// <summary>/// 保存数据为Txt类型的文本/// </summary>private void SaveDataAsTxt(string data){if (_fileOperation.IsGenerateDirectory){try{string fileName = _fileOperation.FileName;if (_fileOperation.IsGenerateDirectory){fileName = _fileOperation.FileNameDateTime;}using (FileStream fileStream = new FileStream(fileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)){using (StreamWriter writer = new StreamWriter(fileStream)){writer.Write(data);}}Console.WriteLine("数据已成功写入文件。");}catch (Exception ex){Console.WriteLine("写入文件时发生错误: " + ex.Message);}}}/// <summary>/// 创建日期目录/// </summary>private void CreateDateTime(){if (_fileOperation.IsGenerateDirectory){string path = $"{_fileOperation.Directory}\\{DateTime.Now.ToString("yyyy")}";Directory.CreateDirectory($"{path}");path = $"{path}\\{DateTime.Now.ToString("yyyyMMdd")}\\";Directory.CreateDirectory($"{path}");_fileOperation.DirectoryDateTime = path;}}#endregion}
}
ProcessService
using System.ComponentModel;
using System.Diagnostics;
namespace OfflineAI.Services
{public class ProcessService{/// <summary>/// 执行CMD指令/// </summary>public static bool ExecuteCommand(string command){// 创建一个新的进程启动信息ProcessStartInfo processStartInfo = new ProcessStartInfo{FileName = "cmd.exe", // 设置要启动的程序为cmd.exeArguments = $"/C {command}", // 设置要执行的命令UseShellExecute = true, // 使用操作系统shell启动进程CreateNoWindow = false, //不创建窗体};try{Process process = Process.Start(processStartInfo);// 启动进程process.WaitForExit(); // 等待进程退出process.Close(); // 返回是否成功执行return process.ExitCode == 0;}catch (Exception ex){Debug.WriteLine($"发生错误: {ex.Message}");// 其他异常处理return false;}}}
}
ShareOllamaObject
using OfflineAI.Services;
using OllamaSharp;
using System.Collections.ObjectModel;namespace OfflineAI.Sevices
{/// <summary>/// 共享Ollama对象类:保持Ollama对象一致才能使用当前对象实现对话/// 作 者:吾与谁归/// 时 间:2025年02月18日/// 功 能:/// 1) 2025-02-18:使用cmd命令启动Ollama服务,目前使用ollama list();/// 2) 2025-02-18:初始化模型参数,在初始化时启用GPU、连接ollama、初始化模型。/// </summary>public class ShareOllamaObject{#region 字段|属性|集合#region 字段private bool _connected = false; //连接状态private Chat chat; //构建交互式聊天模型对象。private OllamaApiClient _ollama; //OllamaAPI对象private string _selectModel; //选择的模型名称#endregion#region 属性/// <summary>/// 连接状态/// </summary>public bool Connected{get { return _connected; }set { _connected = value; }}public string SelectModel { get => _selectModel; set => _selectModel = value; }/// <summary>/// 构建交互式聊天模型对象。/// </summary>public Chat Chat{get { return chat; }set { chat = value; }}/// <summary>/// OllamaAPI对象/// </summary>public OllamaApiClient Ollama{get { return _ollama; }set { _ollama = value; }}#endregion#region 集合/// <summary>/// 模型列表/// </summary>public ObservableCollection<string> ModelList { get; set; }#endregion#endregion#region 构造函数public ShareOllamaObject(){ProcessService.ExecuteCommand("ollama list");Initialize("llama3.2:3b");ProcessService.GetProcessId("ollama");}#endregion#region 其他方法/// <summary>/// 初始化方法/// </summary>private void Initialize( string modelName){try{// 设置默认设备为GPUEnvironment.SetEnvironmentVariable("OLLAMA_DEFAULT_DEVICE", "gpu");//连接Ollama,并设置初始模型Ollama = new OllamaApiClient(new Uri("http://localhost:11434"));//获取本地可用的模型列表ModelList = (ObservableCollection<string>)GetModelList();//遍历查找是否包含llama3.2:3b模型var tmepModelName = ModelList.FirstOrDefault(name => name.ToLower().Contains("llama3.2:3b"));//设置的模型不为空if (tmepModelName != null){Ollama.SelectedModel = tmepModelName;}//模型列表不为空else if (ModelList.Count > 0){_ollama.SelectedModel = ModelList[ModelList.Count - 1];}//Ollama服务启用成功SelectModel = _ollama.SelectedModel;_connected = true;chat = new Chat(_ollama);}catch (Exception){_connected = false; //Ollama服务启用失败}}/// <summary>/// 获取模型里列表/// </summary>public Collection<string> GetModelList(){var models = _ollama.ListLocalModelsAsync();var modelList = new ObservableCollection<string>();foreach (var model in models.Result){modelList.Add(model.Name);}return modelList;}public void ReCreateChat(){chat = new Chat(_ollama);}#endregion}
}
ViewModels
MainViewModel
using OfflineAI.Sevices;
using OfflineAI.Commands;
using OfflineAI.Views;
using System.Windows;
using System.Diagnostics;
using System.Windows.Input;
using System.ComponentModel;
using System.Windows.Controls;
using System.Collections.ObjectModel;
using System.IO;
using OfflineAI.Services;
using OfflineAI.Models;
namespace OfflineAI.ViewModels
{/// <summary>/// 主窗体视图模型:/// 作者:吾与谁归/// 时间:2025年02月17日(首次创建时间)/// 更新: /// 1、2025-02-17:添加折叠栏展开|折叠功能。/// 2、2025-02-17:视图切换功能 1)系统设置 2) 聊天/// 3、2025-02-18:关闭窗体时提示是否关闭,释放相关资源。/// 4、2025-02-19:添加首页功能,和修改新聊天功能。点击新聊天会创建新的会话(Chat)。/// 5、2025-02-20:窗体加载时传递Ollama对象。/// 6、2025-02-24:添加了窗体加载时,加载聊天记录的功能。/// </summary>public class MainViewModel : PropertyChangedBase{#region 字段、属性、集合、命令#region 字段private UserControl _currentView; //当前视图private ShareOllamaObject _ollamaService; //共享Ollama服务对象private string _selectedModel; //选择的模型private ObservableCollection<string> _modelListCollection; //模型列表private int _expandedBarWidth = 50; //折叠栏宽度private string _directory; //目录private string _fileName; //文件private ObservableCollection<ChatRecordModel> _chatRecordCollection;public event Action<string> LoadChatRecordEventHandler;#endregion#region 属性/// <summary>/// 当前显示视图/// </summary>public UserControl CurrentView { get => _currentView;set{if (_currentView != value){_currentView = value;OnPropertyChanged();}}}public ShareOllamaObject OllamaService{get => _ollamaService;set{if (_ollamaService != value){_ollamaService = value;OnPropertyChanged();}}}public string SelectedModel { get => _selectedModel;set{if (_selectedModel != value){_selectedModel = value;OllamaService.Ollama.SelectedModel = value;OllamaService.Chat.Model = value;OnPropertyChanged();}}}public int ExpandedBarWidth{get => _expandedBarWidth;set{if (_expandedBarWidth != value){_expandedBarWidth = value;OnPropertyChanged();}}}#endregion#region 集合/// <summary>/// 视图集合,保存视图/// </summary>public ObservableCollection<UserControl> ViewCollection { get; set; }public ObservableCollection<string> ModelListCollection{get => _modelListCollection;set{if (_modelListCollection != value){_modelListCollection = value;OnPropertyChanged();}}}public ObservableCollection<ChatRecordModel> ChatRecordCollection{get => _chatRecordCollection;set{if (_chatRecordCollection != value){_chatRecordCollection = value;OnPropertyChanged();}}}#endregion#region 命令/// <summary>/// 展开功能菜单命令/// </summary>public ICommand ExpandedMenuCommand { get; set; }/// <summary>/// 折叠功能菜单命令/// </summary>public ICommand CollapsedMenuCommand { get; set; }/// <summary>/// 切换视图命令/// </summary>public ICommand SwitchViewCommand { get; set; }/// <summary>/// 窗体关闭命令/// </summary>public ICommand ClosingWindowCommand { get; set; }/// <summary>/// 窗体加载命令/// </summary>public ICommand LoadedWindowCommand { get; set; }/// <summary>/// 聊天记录鼠标按下命令/// </summary>public ICommand ChatRecordMouseDownCommand { get; set; }#endregion#endregion#region 构造函数public MainViewModel(){Initialize();}/// <summary>/// 初始化方法/// </summary>public void Initialize(){//初始化Ollama_ollamaService = new ShareOllamaObject();ModelListCollection = _ollamaService.ModelList;SelectedModel = _ollamaService.SelectModel;//创建命令SwitchViewCommand = new ParameterCommand(SwitchViewTrigger);LoadedWindowCommand = new EventsCommand<object>(LoadedWindowTrigger);CollapsedMenuCommand = new EventsCommand<object>(CollapsedMenuTrigger);ExpandedMenuCommand = new EventsCommand<object>(ExpandedMenuTrigger);ClosingWindowCommand = new EventsCommand<object>(ClosingWindowTrigger);ChatRecordMouseDownCommand = new EventsCommand<ChatRecordModel>(ChatRecordMouseDownTrigger);ViewCollection = new ObservableCollection<UserControl>();//添加视图到集合ViewCollection.Add(new SystemSettingView());ViewCollection.Add(new UserChatView());//默认显示窗体CurrentView = ViewCollection[1];//折叠栏折叠状态ExpandedBarWidth = 25;//加载聊天记录LoadChatRecord();}#endregion#region 命令方法/// <summary>/// 聊天记录鼠标按下/// </summary>private void ChatRecordMouseDownTrigger(ChatRecordModel obj){Debug.Print(obj.ToString());OnLoadChatRecordCallBack(obj.FullName.ToString());}/// <summary>/// 触发主视图窗体加载方法/// </summary>private void LoadedWindowTrigger(object sender){Debug.Print(sender?.ToString());var userView = ViewCollection.FirstOrDefault(obj => obj is UserChatView) as UserChatView;userView.UserWindow.Ollama = _ollamaService;LoadChatRecordEventHandler += userView.UserWindow.LoadChatRecordCallback;}/// <summary>/// 触发关闭窗体方法/// </summary>private void ClosingWindowTrigger(object obj){if (obj is CancelEventArgs cancelEventArgs){if (MessageBox.Show("确定要关闭程序吗?", "确认关闭", MessageBoxButton.YesNo) == MessageBoxResult.No){cancelEventArgs.Cancel = true; // 取消关闭}else{ClearingResources();}}}/// <summary>/// 视图切换命令触发的方法/// </summary>private void SwitchViewTrigger(object obj){Debug.WriteLine(obj.ToString());switch (obj.ToString()){case "SystemSettingView":CurrentView = ViewCollection[0];break;case "UserChatView":CurrentView = ViewCollection[1];break;case "NewUserChatView":UserChatView newChatView = new UserChatView();OllamaService.ReCreateChat();newChatView.UserWindow.Ollama = OllamaService;ViewCollection[1] = newChatView;CurrentView = newChatView;break;}}/// <summary>/// 折叠菜单触发方法/// </summary>private void CollapsedMenuTrigger(object e){ExpandedBarWidth = 25;Debug.WriteLine("折叠");}/// <summary>/// 展开菜单触发方法/// </summary>private void ExpandedMenuTrigger(object e){ExpandedBarWidth = 250;Debug.WriteLine("展开");}#endregion#region 其他方法/// <summary>/// 加载聊天记录/// </summary>private void LoadChatRecord(){_directory = $"{Environment.CurrentDirectory}\\Record";string[] files = FileOperation.GetFiles(_directory);ObservableCollection<ChatRecordModel> records = new ObservableCollection<ChatRecordModel>();string name = string.Empty;string data = string.Empty;foreach (var item in files){name = Path.GetFileNameWithoutExtension(item);data = File.ReadAllLines(item)[3];if (data.Trim().Length > 1 ){records.Add(new ChatRecordModel(records.Count, name, name, item, data.Substring(1)));}}ChatRecordCollection = records;}/// <summary>/// 触发事件:加载聊天记录回调/// </summary>private void OnLoadChatRecordCallBack(object sender){LoadChatRecordEventHandler.Invoke(sender.ToString());}/// <summary>/// 释放资源:窗体关闭时触发/// </summary>private void ClearingResources(){//ProcessService.GetPIDAndCloseByPort(11434);}#endregion}
}
PropertyChangedBase
using System.ComponentModel;
using System.Runtime.CompilerServices;namespace OfflineAI.ViewModels
{/// <summary>/// 属性变更基类/// </summary>public class PropertyChangedBase : INotifyPropertyChanged{public event PropertyChangedEventHandler? PropertyChanged;protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}}
}
UserChatViewModel
using Markdig.Wpf;
using OfflineAI.Commands;
using OfflineAI.Services;
using OfflineAI.Sevices;
using System.Diagnostics;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms;
using System.Windows.Input;
namespace OfflineAI.ViewModels
{/// <summary>/// 描述:用户聊天视图模型:/// 作者:吾与谁归/// 时间: 2025年2月19日/// 更新:/// 1、 2025-02-19:添加AI聊天功能,输出问题及结果到UI,并使用Markdown相关的库做简单渲染。/// 2、 2025-02-20:优化了构造函数,使用无参构造,方便在设计器中直接绑定数据上下文(感觉)。/// 3、 2025-02-20:滚轮滑动显示内容,提交问题后滚动显示内容,鼠标右键点击内容停止继续滚动,回答结束停止滚动。/// 4、 2025-02-24:添加聊天记录保存功能。/// 5、 2025-02-24:添加聊天记录加载功能,通过点击记录列表显示。/// </summary>public class UserChatViewModel:PropertyChangedBase{#region 字段、属性、集合、命令#region 字段private bool _isAutoScrolling = false; //是否自动滚动private string _currentInputText; //当前输入文本private string _messageContent; //消息内容private string _directory; //目录private string _fileName; //文件名private MarkdownViewer _markdownViewer; //MarkdownViewer控件private ScrollViewer _scrollViewer; //ScrollViewer滑动控件private StringBuilder _message = new StringBuilder(); //消息字符串拼接private CancellationToken cancellationToken; //异步线程取消标记private FileOperation _fileIO; //文件IOprivate ShareOllamaObject _ollama; //Ollama 对象实例private string _submitButtonName;#endregion#region 属性/// <summary>/// 提交按钮名称/// </summary>public string SubmitButtonName{get => _submitButtonName;set{if (_submitButtonName != value){_submitButtonName = value;OnPropertyChanged();}}}/// <summary>/// 消息内容/// </summary>public string? MessageContent{get => _messageContent;set{_messageContent = value;OnPropertyChanged();}}/// <summary>/// 当前输入文本/// </summary>public string CurrentInputText{get => _currentInputText;set{if (_currentInputText != value){_currentInputText = value;OnPropertyChanged();}}}/// <summary>/// 共享Ollama对象 /// </summary>public ShareOllamaObject Ollama {get => _ollama;set{if (_ollama != value){_ollama = value;OnPropertyChanged();}}}/// <summary>/// 自动滚动消息/// </summary>public bool IsAutoScrolling{get => _isAutoScrolling;set{if (_isAutoScrolling != value){_isAutoScrolling = value;OnPropertyChanged();}}}#endregion#region 集合#endregion#region 命令/// <summary>/// 展开功能菜单命令/// </summary>public ICommand LoadFileCommand { get; set; }/// <summary>/// 提交命令/// </summary>public ICommand SubmiQuestionCommand { get; set; }/// <summary>/// 鼠标滚动/// </summary>public ICommand MouseWheelCommand { get; set; }/// <summary>/// 鼠标按下/// </summary>public ICommand MouseDownCommand { get; set; }/// <summary>/// Markdown对象命令/// </summary>public ICommand MarkdownOBJCommand { get; set; }/// <summary>/// 滑动条加载/// </summary>public ICommand ScrollLoadedCommand { get; set; }#endregion#endregion#region 构造函数public UserChatViewModel(){Initialize();}#endregion#region 初始化方法/// <summary>/// 初始化方法/// </summary>public void Initialize(){//文件加载LoadFileCommand = new ParameterCommand(LoadFileTrigger);MouseWheelCommand = new EventsCommand<MouseWheelEventArgs>(MouseWheelTrigger);MouseDownCommand = new EventsCommand<MouseButtonEventArgs>(MouseDownTrigger);MarkdownOBJCommand = new EventsCommand<object>(MarkdownOBJTrigger);SubmiQuestionCommand = new ParameterlessCommand(SubmitQuestionTrigger);ScrollLoadedCommand = new EventsCommand<RoutedEventArgs>(ScrollLoadedTrigger);//SubmitButtonName = "提交";//日志记录_directory = $"{Environment.CurrentDirectory}\\Record\\";_fileName = $"{_directory}\\{DateTime.Now.ToString("yyyyMMddHHmmss")}";_fileIO = new FileOperation($"{_fileName}");//}#endregion#region 命令方法/// <summary>/// 加载文件/// </summary>private void LoadFileTrigger(object obj){OpenFileDialog openFile = new OpenFileDialog();openFile.Multiselect = true;if (openFile.ShowDialog() == DialogResult.OK){string[] files = openFile.FileNames;if (files.Count() > 1){foreach (var item in files){Debug.WriteLine(item);}}else{Debug.WriteLine(openFile.FileName);}}}/// <summary>/// 提交: 提交问题到AI并获取返回结果/// </summary>private async void SubmitQuestionTrigger(){_ = Task.Delay(1);string input = CurrentInputText;try{if (!SubmintChecked(input)) return; SubmitButtonName = "停止";_message.Clear();_isAutoScrolling = true;AppendText($"##{Environment.NewLine}");AppendText($"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}]{Environment.NewLine}");AppendText($"## 【User】{Environment.NewLine}");AppendText($">{input}{Environment.NewLine}");AppendText($"{Environment.NewLine}");AppendText($"## 【AI】{Environment.NewLine}");await foreach (var answerToken in Ollama.Chat.SendAsync(input)){AppendText(answerToken);await Task.Delay(20);if (_isAutoScrolling) _scrollViewer.ScrollToEnd();//是否自动滚动}AppendText($"{Environment.NewLine}{Environment.NewLine}");}catch (Exception ex){AppendText($"Error: {ex.Message}");AppendText($"{Environment.NewLine}{Environment.NewLine}");}//回答完成_fileIO.WriteTxt($"{_fileName}", _message.ToString());CurrentInputText = string.Empty;_isAutoScrolling = false;SubmitButtonName = "提交";}/// <summary>/// 鼠标滚动上下滑动/// </summary>private void MouseWheelTrigger(MouseWheelEventArgs e){try{// 获取 ScrollViewer 对象if (e.Source is FrameworkElement element && element.Parent is ScrollViewer scrollViewer){// 获取当前的垂直偏移量double currentOffset = scrollViewer.VerticalOffset;if (e.Delta > 0){scrollViewer.ScrollToVerticalOffset(currentOffset - e.Delta);}else{scrollViewer.ScrollToVerticalOffset(currentOffset - e.Delta);}// 标记事件已处理,防止默认滚动行为e.Handled = true;}}catch (Exception ex){Debug.Print(ex.Message);}}/// <summary>/// Markdown中鼠标按下/// </summary>private void MouseDownTrigger(MouseButtonEventArgs args){if (args.LeftButton == MouseButtonState.Pressed){IsAutoScrolling = false;Debug.Print("Mouse Down...");}}/// <summary>/// 滚动栏触发/// </summary>private void ScrollLoadedTrigger(RoutedEventArgs args){if (args.Source is ScrollViewer scrollView ){_scrollViewer = scrollView;Debug.Print("Scroll loaded...");}}/// <summary>/// Markdown控件对象更新触发/// </summary>private void MarkdownOBJTrigger(object obj){if (_markdownViewer != null) return;if (obj is MarkdownViewer markdownViewer){_markdownViewer = markdownViewer;_markdownViewer.Markdown = "";}}#endregion#region 其他方法/// <summary>/// 输出文本/// </summary>public void AppendText(string newText){Debug.Print(newText);_markdownViewer.Markdown += newText;_message.Append(newText);}/// <summary>/// 提交校验/// </summary>private bool SubmintChecked(string input){if (string.IsNullOrEmpty(input)) return false;if (input.Length<2) return false;if (input.Equals("停止")) return false;return true;}#endregion#region 回调方法/// <summary>/// 加载聊天记录回调/// </summary>public void LoadChatRecordCallback(string path){Debug.Print(path);_scrollViewer.ScrollToTop();_markdownViewer.Markdown = _fileIO. ReadTxt(path);}#endregion}
}
Views
UserChatView
<UserControl x:Class="OfflineAI.Views.UserChatView"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:behavior="http://schemas.microsoft.com/xaml/behaviors"xmlns:local="clr-namespace:OfflineAI.Views"xmlns:markdig ="clr-namespace:Markdig.Wpf;assembly=Markdig.Wpf"xmlns:viewmodels="clr-namespace:OfflineAI.ViewModels"mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"HorizontalAlignment="Stretch" VerticalAlignment="Stretch"><!--绑定数据上下文--><UserControl.DataContext><viewmodels:UserChatViewModel x:Name="UserWindow"/></UserControl.DataContext><Grid><!--命令绑定事件:窗体加载时传参数Markdown控件对象。在Grid中创建,否则会出现null异常--><behavior:Interaction.Triggers><behavior:EventTrigger EventName="Loaded"><behavior:InvokeCommandAction Command="{Binding MarkdownOBJCommand}"CommandParameter="{Binding ElementName=MarkdownContent}"/></behavior:EventTrigger></behavior:Interaction.Triggers><!--定义行--><Grid.RowDefinitions><RowDefinition Height="*"/><RowDefinition Height="300"/></Grid.RowDefinitions><!--行背景色--><Border Grid.Row="0" Background="#FFFFFF"/><Border Grid.Row="1" Background="#5E5E5E"/><Grid><!--markdown 滑动条--><ScrollViewer Background="#AEAEAE"x:Name="MarkDownScrollViewer"><behavior:Interaction.Triggers><behavior:EventTrigger EventName="Loaded"><behavior:InvokeCommandAction Command="{Binding ScrollLoadedCommand}"PassEventArgsToCommand="True"/></behavior:EventTrigger></behavior:Interaction.Triggers><!--markdown--><markdig:MarkdownViewerName="MarkdownContent"><!--命令绑定事件:鼠标滚动显示内容--><behavior:Interaction.Triggers><!--鼠标滚动命令事件--><behavior:EventTrigger EventName="PreviewMouseWheel"><behavior:InvokeCommandAction Command="{Binding MouseWheelCommand}"PassEventArgsToCommand="True"/></behavior:EventTrigger><!--鼠标点击命令事件--><behavior:EventTrigger EventName="PreviewMouseDown"><behavior:InvokeCommandAction Command="{Binding MouseDownCommand}"PassEventArgsToCommand="True"/></behavior:EventTrigger></behavior:Interaction.Triggers></markdig:MarkdownViewer></ScrollViewer></Grid><!--第三行内容:显示回话内容--><Grid Grid.Row="1" Margin="2"><!--定义三行--><Grid.RowDefinitions><RowDefinition Height="25"/><RowDefinition Height="*"/><RowDefinition Height="30"/></Grid.RowDefinitions><!--设置Border样式--><Border Grid.Row="0" Margin="150,0,150,0" Background="#5E5E5E"><Border.BorderThickness>2,2,2,0</Border.BorderThickness><Border.BorderBrush><SolidColorBrush Color="#000000"/></Border.BorderBrush></Border><Border Grid.Row="1" Margin="150,0,150,0" Background="#5E5E5E"><Border.BorderThickness>2,0,2,0</Border.BorderThickness><Border.BorderBrush><SolidColorBrush Color="#000000"/></Border.BorderBrush></Border><Border Grid.Row="2" Margin="150,0,150,0" Background="#5E5E5E"><Border.BorderThickness>2,0,2,2</Border.BorderThickness><Border.BorderBrush><SolidColorBrush Color="#000000"/></Border.BorderBrush></Border><!--第2行内容区域--><Grid Grid.Row="1" Margin="150,0,150,0"><TextBox x:Name="InputBox" Background="#5E5E5E"Text="{Binding CurrentInputText , Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Row="1" Margin="5" AcceptsReturn="True" VerticalScrollBarVisibility="Auto"><!--回车发送--><TextBox.InputBindings><KeyBinding Command="{Binding SubmiQuestionCommand}" Key="Enter"/></TextBox.InputBindings></TextBox></Grid><!--第3行内容区域--><Grid Grid.Row="2" Margin="150,0,150,0"><WrapPanel HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,5,0"><Button Width="50" Command="{Binding LoadFileCommand}"><Image Width="24" Height="24"Source="/Views/Resources/append24-black.png" HorizontalAlignment="Right" VerticalAlignment="Center"/></Button><Button Width="50" Command="{Binding SubmiQuestionCommand}" Content="{Binding SubmitButtonName}"></Button></WrapPanel></Grid></Grid></Grid>
</UserControl>
SystemSettingView
<UserControl x:Class="OfflineAI.Views.SystemSettingView"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:OfflineAI.Views"xmlns:viewModels="clr-namespace:OfflineAI.ViewModels"mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"HorizontalAlignment="Stretch" VerticalAlignment="Stretch"><Grid><StackPanel Background="#FFFFFF" Margin="5"><TextBox FontSize="36" IsReadOnly="True"HorizontalContentAlignment="Center" VerticalContentAlignment="Center">系统设置</TextBox><CheckBox Width="200" Margin="5" HorizontalAlignment="Left" IsChecked="True">是否滚动显示</CheckBox><ComboBox Width="200" Margin="5" HorizontalAlignment="Left"></ComboBox></StackPanel></Grid>
</UserControl>
Styles \ ButtonStyle.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"><!-- 定义圆角按钮的静态样式 --><Style x:Key="RoundCornerButtonStyle" TargetType="Button"><Setter Property="Background"><Setter.Value><LinearGradientBrush StartPoint="0,0" EndPoint="1,0"><GradientStop Color="#04D3F2" Offset="0.6" /><GradientStop Color="#FFAB0D" Offset="2.8" /></LinearGradientBrush></Setter.Value></Setter><Setter Property="BorderBrush" Value="DarkGray"/><Setter Property="BorderThickness" Value="0"/><Setter Property="Padding" Value="5"/><Setter Property="Margin" Value="10"/><Setter Property="Width" Value="60"/><Setter Property="Height" Value="20"/><!--设置模板样式--><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><!--使用 Border 元素作为按钮的主要容器。 roundedRectangle:名称,方便在触发器中引用。Background:绑定背景色到按钮的 Background 属性。BorderBrush:绑定边框颜色到按钮的 BorderBrush 属性。BorderThickness:绑定边框宽度到按钮的 BorderThickness 属性。CornerRadius:设置边框的圆角半径为10,使按钮具有圆角效果。ContentPresenter:用于显示按钮的内容(如文本或图标)。--><Border x:Name="roundedRectangle" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="10"><!-- 设置顶部圆角 --> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/></Border><ControlTemplate.Triggers><!-- 鼠标悬停时 --><Trigger Property="IsMouseOver" Value="True"><Setter TargetName="roundedRectangle" Property="Background"><Setter.Value><LinearGradientBrush StartPoint="0,0" EndPoint="1,0"><GradientStop Color="#FFB3B3" Offset="0.4" /><GradientStop Color="#D68B8B" Offset="0.7" /></LinearGradientBrush></Setter.Value></Setter></Trigger><!-- 按钮被按下时 --><Trigger Property="IsPressed" Value="True"><Setter TargetName="roundedRectangle" Property="Background"><Setter.Value><LinearGradientBrush StartPoint="0,0" EndPoint="1,0"><GradientStop Color="#D68B8B" Offset="0.4" /><GradientStop Color="#A05252" Offset="0.7" /></LinearGradientBrush></Setter.Value></Setter></Trigger></ControlTemplate.Triggers></ControlTemplate></Setter.Value></Setter></Style><!-- 定义带图标的按钮的静态样式 --><Style x:Key="IconButtonStyle" TargetType="Button"><Setter Property="Background"><Setter.Value><LinearGradientBrush StartPoint="0,0" EndPoint="1,0"><GradientStop Color="#AED3D2" Offset="0.3" /><!-- 淡色 --><GradientStop Color="#F0FBFF" Offset="0.7" /><!-- 深色 --></LinearGradientBrush></Setter.Value></Setter><Setter Property="BorderBrush" Value="DarkGray"></Setter><Setter Property="BorderThickness" Value="0"></Setter><Setter Property="Padding" Value="5"></Setter><Setter Property="Margin" Value="5 5 5 5"></Setter><Setter Property="FontSize" Value="20"></Setter><!-- 调整宽度以适应图标和文本 --><Setter Property="Height" Value="50"></Setter><!-- 调整高度以适应图标和文本 --><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><Border x:Name="roundedRectangle" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="10"><!-- 使用 StackPanel 来布局图标和文本 --><StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"><ContentPresenter Content="{TemplateBinding Content}" /></StackPanel></Border><ControlTemplate.Triggers><!-- 鼠标悬停时 --><Trigger Property="IsMouseOver" Value="True"><Setter TargetName="roundedRectangle" Property="Background"><Setter.Value><LinearGradientBrush StartPoint="0,0" EndPoint="1,0"><GradientStop Color="#FFB3B3" Offset="0.4" /><GradientStop Color="#D68B8B" Offset="0.7" /></LinearGradientBrush></Setter.Value></Setter></Trigger><!-- 按钮被按下时 --><Trigger Property="IsPressed" Value="True"><Setter TargetName="roundedRectangle" Property="Background"><Setter.Value><LinearGradientBrush StartPoint="0,0" EndPoint="1,0"><GradientStop Color="#D68B8B" Offset="0.4" /><GradientStop Color="#A05252" Offset="0.7" /></LinearGradientBrush></Setter.Value></Setter></Trigger></ControlTemplate.Triggers></ControlTemplate></Setter.Value></Setter></Style>
</ResourceDictionary>
MainWindow
<Window x:Class="OfflineAI.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:behavior="http://schemas.microsoft.com/xaml/behaviors"xmlns:local="clr-namespace:OfflineAI"xmlns:viewmodels="clr-namespace:OfflineAI.ViewModels" WindowStartupLocation="CenterScreen"mc:Ignorable="d"Title="ChatAI" Height="800" Width="1000"Icon="/Views/Resources/app-logo128.ico"MinHeight="600" MinWidth="800"><!--绑定上下文--><Window.DataContext><viewmodels:MainViewModel></viewmodels:MainViewModel></Window.DataContext><!--样式资源--><Window.Resources><ResourceDictionary><!--资源字典: 添加控件样式--><ResourceDictionary.MergedDictionaries><ResourceDictionary Source="Views/Styles/ButtonStyle.xaml"/><ResourceDictionary Source="Views/Styles/ComboBoxStyle.xaml"/></ResourceDictionary.MergedDictionaries></ResourceDictionary></Window.Resources><!--事件命令绑定--><behavior:Interaction.Triggers><!--窗体加载命令绑定--><behavior:EventTrigger EventName="Loaded"><behavior:InvokeCommandAction Command="{Binding LoadedWindowCommand}" PassEventArgsToCommand="True"/></behavior:EventTrigger><!--窗体关闭命令绑定--><behavior:EventTrigger EventName="Closing"><behavior:InvokeCommandAction Command="{Binding ClosingWindowCommand}" PassEventArgsToCommand="True"/></behavior:EventTrigger></behavior:Interaction.Triggers><Grid><!-- 定义3列:--><Grid.ColumnDefinitions><ColumnDefinition Width="auto"/><ColumnDefinition Width="*"/><ColumnDefinition Width="10"/></Grid.ColumnDefinitions><!-- 定义2行 --><Grid.RowDefinitions><RowDefinition Height="*"/><RowDefinition Height="20"/></Grid.RowDefinitions><!-- 折叠栏 Expander --><Expander x:Name="expanderBox" Grid.Row="0" Grid.Column="0" Header="" Background="#AABBBB" ExpandDirection="Left"IsExpanded="False"FlowDirection="LeftToRight" Width="{Binding ExpandedBarWidth}"><!--命令绑定事件--><behavior:Interaction.Triggers><!--折叠栏展开命令绑定--><behavior:EventTrigger EventName="Expanded"><behavior:InvokeCommandAction Command="{Binding ExpandedMenuCommand}" /></behavior:EventTrigger><!--折叠栏折叠命令绑定--><behavior:EventTrigger EventName="Collapsed"><behavior:InvokeCommandAction Command="{Binding CollapsedMenuCommand}" /></behavior:EventTrigger></behavior:Interaction.Triggers><ScrollViewer Background="#AEAEAE" x:Name="RecordScrollViewer"><ListBox ItemsSource="{Binding ChatRecordCollection}" Margin="5"><ListBox.ItemTemplate><DataTemplate><!-- 显示消息内容 --><TextBlock Text="{Binding Data}" Margin="10,0,0,0"><behavior:Interaction.Triggers><!--鼠标点击命令事件--><behavior:EventTrigger EventName="PreviewMouseDown"><behavior:InvokeCommandActionCommand="{Binding DataContext.ChatRecordMouseDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"CommandParameter="{Binding}"PassEventArgsToCommand="True"/></behavior:EventTrigger></behavior:Interaction.Triggers></TextBlock></DataTemplate></ListBox.ItemTemplate></ListBox></ScrollViewer></Expander><!-- 右侧内容区域 --><Border Background="LightGray" Grid.Row="0" Grid.Column="1" Padding="10"/><!--主要区域--><Grid Grid.Row="0" Grid.Column="1" Margin="3"><!--定义三行--><Grid.RowDefinitions><RowDefinition Height="50"/><RowDefinition Height="*"/><RowDefinition Height="350"/></Grid.RowDefinitions><!--设置背景色--><Border Grid.Row="0" Background="#99BBCC"/><Border Grid.Row="1" Background="#FFFFFF" Grid.RowSpan="2"/><!--第一行内容:左对齐内容--><WrapPanel VerticalAlignment="Center"><!--视图切换:首页--><Button x:Name="Btn_HomePage" Width="50" Height="36" FontSize="16"Style="{StaticResource IconButtonStyle}" Command="{Binding SwitchViewCommand}"CommandParameter="UserChatView"><StackPanel Orientation="Horizontal"><Image Source="Views/Resources/home24-black.png"Margin="5" Width="24" Height="24"HorizontalAlignment="Center" VerticalAlignment="Center"/></StackPanel></Button><!--视图切换:新聊天界面--><Button x:Name="Btn_Chat" Width="100" Height="36" FontSize="16"Style="{StaticResource IconButtonStyle}" Command="{Binding SwitchViewCommand}"CommandParameter="NewUserChatView"><StackPanel Orientation="Horizontal"><Image Source="Views/Resources/edit24-black.png"Margin="5" Width="24" Height="24"HorizontalAlignment="Center" VerticalAlignment="Center"/><TextBlock Text="新聊天" VerticalAlignment="Center"/></StackPanel></Button><!--模型列表--><Label Content="模型:" Margin="5" FontSize="18" VerticalAlignment="Center"/><ComboBox x:Name="Cbx_ModelList" Style="{StaticResource RoundComboBoxStyle}" ItemsSource="{Binding ModelListCollection}"SelectedItem="{Binding SelectedModel}"></ComboBox></WrapPanel><!--第一行内容:右对齐内容--><WrapPanel Margin="0,0,0,0" HorizontalAlignment="Right" VerticalAlignment="Center" ><Button Background="#99BBCC" Command="{Binding SwitchViewCommand}"CommandParameter="SystemSettingView"><Image Source="/Views/Resources/setting64.png" Margin="5" Width="24" Height="24"HorizontalAlignment="Right" VerticalAlignment="Center"/></Button></WrapPanel><!--第二行内容:显示当前视图--><ContentControl Grid.Row="1" Margin="5,5,5,5"Content="{Binding CurrentView}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" Grid.RowSpan="2"/></Grid></Grid>
</Window>
总结
以上为项目的全部代码。
实现功能:
1、添加折叠栏展开|折叠功能。
2、视图切换功能 1)系统设置 2) 聊天
3、关闭窗体时提示是否关闭,释放相关资源。
4、添加首页功能,和修改新聊天功能。点击新聊天会创建新的会话(Chat)。
5、窗体加载时传递Ollama对象。
6、添加了窗体加载时,加载聊天记录的功能。
7、添加AI聊天功能,输出问题及结果到UI,并使用Markdown相关的库做简单渲染。
8、优化了构造函数,使用无参构造,方便在设计器中直接绑定数据上下文(感觉)。
9、 滚轮滑动显示内容,提交问题后滚动显示内容,鼠标右键点击内容停止继续滚动,回答结束停止滚动。
10、添加聊天记录保存功能。
11、添加聊天记录加载功能,通过点击记录列表显示。
待完善:
1、使用deepseek r*模型时,控件刷新会把 的前面的一部分吞掉,使用Debug打印的是完整的问题,初步怀疑是异步刷新UI更不上的问题。
2、想使用Markdown的高级渲染功能使用起来,目前仅是简单的渲染(有空要做出来)。
3、聊天记录仅仅是显示功能,没有实现承接聊天记录回答问题。
4、参考网页端的功能开发更多功能。
项目下载地址:https://github.com/timenodes/OfflineAI
相关文章:
C#实现本地AI聊天功能(Deepseek R1及其他模型)。
前言 1、C#实现本地AI聊天功能 WPFOllamaSharpe实现本地聊天功能,可以选择使用Deepseek 及其他模型。 2、此程序默认你已经安装好了Ollama。 在运行前需要线安装好Ollama,如何安装请自行搜索 Ollama下载地址: https://ollama.org.cn Ollama模型下载地址…...
Metal 学习笔记四:顶点函数
到目前为止,您已经完成了 3D 模型和图形管道。现在,是时候看看 Metal 中两个可编程阶段中的第一个阶段,即顶点阶段,更具体地说,是顶点函数。 着色器函数 定义着色器函数时,可以为其指定一个属性。您将在本…...
C# string转unicode字符
在 C# 中,将字符串转换为 Unicode 字符(即每个字符的 Unicode 码点)可以通过遍历字符串中的每个字符并获取其 Unicode 值来实现。Unicode 值是一个整数,表示字符在 Unicode 标准中的唯一编号。 以下是实现方法: 1. 获…...
HITCON2017SSRFME-学习复盘
代码审计 192.168.122.15 <?phpif (isset($_SERVER[HTTP_X_FORWARDED_FOR])) {$http_x_headers explode(,, $_SERVER[HTTP_X_FORWARDED_FOR]);//用逗号分割多个IP$_SERVER[REMOTE_ADDR] $http_x_headers[0];}echo $_SERVER["REMOTE_ADDR"];//给第一个IP发送请…...
【Http和Https区别】
概念: 一、Http协议 HTTP(超文本传输协议)是一种用于传输超媒体文档(如HTML)的应用层协议,主要用于Web浏览器和服务器之间的通信。http也是客户端和服务器之间请求与响应的标准协议,客户端通常…...
2025数学建模竞赛汇总,错过再等一年
01、2025第十届数维杯大学生数学建模挑战赛(小国赛) 竞赛介绍:数学建模行业内仅次于国赛和美赛的的第三赛事,被多所高校认定为国家级二类竞赛。赛题类型是国内唯一和高教社杯国赛题型风格完全一致的全国性数学建模竞赛࿰…...
基于SSM的《计算机网络》题库管理系统(源码+lw+部署文档+讲解),源码可白嫖!
摘 要 《计算机网络》题库管理系统是一种新颖的考试管理模式,因为系统是用Java技术进行开发。系统分为三个用户进行登录并操作,分别是管理员、教师和学生。教师在系统后台新增试题和试卷,学生进行在线考试,还能对考生记录、错题…...
ReentrantLock 用法与源码剖析笔记
📒 ReentrantLock 用法与源码剖析笔记 🚀 一、ReentrantLock 核心特性 🔄 可重入性:同一线程可重复获取锁(最大递归次数为 Integer.MAX_VALUE)🔧 公平性:支持公平锁(按等…...
矩阵的 正定(Positive Definite)与负定(Negative Definite):从Fisher信息矩阵看“曲率”的秘密
矩阵的正定与负定:从Fisher信息矩阵看“曲率”的秘密 在数学和统计学中,矩阵的“正定性”和“负定性”是一对重要概念,尤其在优化、统计推断和机器学习中频繁出现。比如,Fisher信息矩阵(Fisher Information Matrix, F…...
被裁20240927 --- WSL-Ubuntu20.04安装cuda、cuDNN、tensorRT
cuda、cuDNN、tensorRT的使用场景 1. CUDA(Compute Unified Device Architecture) 作用: GPU 通用计算:CUDA 是 NVIDIA 的并行计算平台和编程模型,允许开发者直接利用 GPU 的并行计算能力,加速通用计算任…...
uniapp写的h5跳转小程序
使用场景: 我们对接第三方支付的时候,对方只提供了原生小程序id和appid,由我们的app和h5平台跳转至小程序。 遇到的问题: app跳转本地正常,线上报错如下 解决办法: 需要去微信开放平台申请应用appid 易…...
[SWPUCTF 2022 新生赛]ez_rce
打开题目就在线环境,发现只有一句话:真的什么都没有吗 F12查看控制台和源代码也没发现任何信息,然后用虚拟机里面的dirsearch扫一下这个网站就能得到: 然后这里扫出来的结果查看的直接就是robots.txt,然后就能看到: …...
递归、搜索与回溯算法 —— 名词解析
目录 一、递归 1、什么是递归? 2、递归的数学类比 3、为什么要用到递归? 问题具有递归结构: 代码简洁易懂: 解决复杂问题: 处理嵌套结构: 4、如何理解递归? 明确基准条件: …...
【docker】docker swarm lock和unlock的区别,以及旧节点重启的隐患
docker swarm lock/unlock 的作用 Docker Swarm 提供了**加密集群状态(Encrypted Raft logs)**的功能,可以防止 Swarm 集群的管理数据(如任务分配、集群配置等)在磁盘上被未授权访问。 docker swarm lock:…...
Grafana使用日志5--如何重置Grafana密码
背景 有时候当账号太多的时候,根本记不住所有的账号密码,这时候就很容易登录失败,这时候怎么办呢? 接下来就让我来给大家演示一下Grafana的账号如果忘记了的话,该怎么找回自己的账号密码 操作 让我们来看一下具体的…...
ELK搭建初入
ELK搭建: 1、安装ElasticSearch (用于存储收集到的日志信息) 解压安装包 tar -xzvf elasticsearch-8.17.2-linux-x86_64.tar.gz 启动es:bin/elasticsearch –d(默认端口号9200) 浏览器输入es地址。出现…...
JVM 高级面试题及答案整理,最新面试题
JVM中的垃圾收集器有哪些,它们的工作原理是什么? JVM中的垃圾收集器主要包括以下几种: 1、 Serial收集器:它是一个单线程收集器,工作时会暂停所有其他工作线程("Stop-The-World")&a…...
第9章:LangChain结构化输出-示例5(基于大模型如何精确匹配POJO的字段)
如何使用LangChain4j框架创建和使用多种AI服务。它通过定义接口和注解,将自然语言处理任务(如情感分析、数字提取、日期提取、POJO提取等)封装为服务,并通过LangChain4j的AiServices动态生成这些服务的实现。 本章主要讲述基于LangChain调用大模型如何进行结构化输出的真实…...
ref和reactive的区别 Vue3
Vue3中ref和reactive的区别 ref 可以定义基本数据类型,也可定义对象类型的响应式数据 reactive 只能定义对象类型的响应式数据 ref和reactive定义对象类型的响应式数据有什么不同 不同点1 ref定义的响应式数据,取值时需要先 .value 不同点2 替换整…...
基于MATLAB的OFDM通信系统仿真设计
下面将为你详细介绍基于MATLAB的OFDM通信系统仿真设计的步骤和示例代码。 1. OFDM系统原理概述 正交频分复用(OFDM)是一种多载波调制技术,它将高速数据流通过串并转换,分配到多个正交的子载波上进行传输,这样可以有效…...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...
云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地
借阿里云中企出海大会的东风,以**「云启出海,智联未来|打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办,现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...
【Linux】C语言执行shell指令
在C语言中执行Shell指令 在C语言中,有几种方法可以执行Shell指令: 1. 使用system()函数 这是最简单的方法,包含在stdlib.h头文件中: #include <stdlib.h>int main() {system("ls -l"); // 执行ls -l命令retu…...
【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】
1. BluetoothProperties介绍 libsysprop/srcs/android/sysprop/BluetoothProperties.sysprop BluetoothProperties.sysprop 是 Android AOSP 中的一种 系统属性定义文件(System Property Definition File),用于声明和管理 Bluetooth 模块相…...
Module Federation 和 Native Federation 的比较
前言 Module Federation 是 Webpack 5 引入的微前端架构方案,允许不同独立构建的应用在运行时动态共享模块。 Native Federation 是 Angular 官方基于 Module Federation 理念实现的专为 Angular 优化的微前端方案。 概念解析 Module Federation (模块联邦) Modul…...
CMake 从 GitHub 下载第三方库并使用
有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...
【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习
禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…...
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
以下是一个完整的 Angular 微前端示例,其中使用的是 Module Federation 和 npx-build-plus 实现了主应用(Shell)与子应用(Remote)的集成。 🛠️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...
springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
