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模型下载地址…...
git 查询包含某个文件夹的步骤
步骤 1:拉取最新的远程分支信息 确保本地缓存的远程分支信息是最新的: bash 复制 git fetch --all 步骤 2:遍历所有远程分支并检查目标文件夹 使用 git ls-tree 检查每个分支是否包含目标文件夹。以下脚本会列出所有包含 your_folder_pa…...
微软开源神器OmniParser-v2.0本地部署教程
安装python环境 我这里是以前安装好的版本:python 3.11.5,这里不再介绍,有需要的可以在网上找教程。 安装Anaconda 我这里是以前安装好的版本:conda 23.7.4,这里也不再介绍,有需要的可以在网上找教程。 …...
解决 Git 合并冲突:当本地修改与远程提交冲突时
目录 错误原因分析 解决方法 1. 暂存本地修改并合并(保留更改) 2. 丢弃本地修改(强制覆盖) 3. 暂存修改后合并(推荐:使用 git stash) 4. 选择性合并(手动处理冲突文件…...
VScode中Markdown PDF无法正确输出包含数学公式的pdf解决方案
在使用VScode的Markdown PDF插件时,可能会遇到无法正确输出包含公式的PDF文件的问题。下面为你提供一种有效的解决方案。 具体操作步骤 步骤一:定位模板文件 在安装Markdown PDF插件后,你需要找到对应的模板文件。该文件的路径通常如下&am…...
uniapp 网络请求封装(uni.request 与 uView-Plus)
一、背景 在开发项目中,需要经常与后端服务器进行交互;为了提高开发效率和代码维护性,以及降低重复性代码,便对网络请求进行封装统一管理。 二、创建环境文件 2.1、根目录新建utils文件夹,utils文件夹内新建env.js文…...
Jtti.cc:站群服务器SEO优化建议,如何分配多IP?
站群优化的核心目标之一是尽可能通过多个网站互相引导流量,从而提升主站的权重。这时候,多IP的分配至关重要,因为搜索引擎会检测到同一IP下的网站之间的关联性。如果一个IP地址下有过多的相似站点,搜索引擎可能会认为这些站点存在…...
银行系统功能架构设计元模型
1. 元模型核心目标 规范性:定义功能模块的标准化描述方式,便于跨团队协作。可复用性:抽象通用组件,减少重复开发。可扩展性:支持未来业务创新和技术升级(如开放银行API集成)。2. 元模型层级结构 采用分层架构模式,分为以下核心层级: **(1) 业务功能层** …...
uniapp写的h5跳转小程序
使用场景: 我们对接第三方支付的时候,对方只提供了原生小程序id和appid,由我们的app和h5平台跳转至小程序。 遇到的问题: app跳转本地正常,线上报错如下 解决办法: 需要去微信开放平台申请应用appid 易…...
DeepSeek点燃AI大模型战火:编程语言争霸,谁将问鼎“终极武器”王座?
DeepSeek点燃AI大模型战火:编程语言争霸,谁将问鼎“终极武器”王座? 一、DeepSeek:AI大模型竞赛的“导火索” 2023年,中国AI公司深度求索(DeepSeek)发布DeepSeek-R1大模型,凭借其超…...
游戏引擎学习第123天
仓库:https://gitee.com/mrxiao_com/2d_game_3 黑板:线程同步/通信 目标是从零开始编写一个完整的游戏。我们不使用引擎,也不依赖任何库,完全自己编写游戏所需的所有代码。我们做这个节目不仅是为了教育目的,同时也是因为编程本…...
钉钉快捷免登录 通过浏览器打开第三方系统,
一、钉钉内跳转至浏览器的实现 使用钉钉JSAPI的跳转接口 在钉钉内通过dd.biz.navigation.openLink方法强制在系统浏览器中打开链接。此方法需在钉钉开发者后台配置应用权限,确保应用具备调用该API的资格37。 示例代码: dd.ready(() > {dd.biz.navigat…...
塔能科技构建智慧隧道生态系统——城市升级改造的协同创新典范
一、智慧隧道生态系统的概念与意义 (一)概念解析 智慧隧道生态系统是一个涵盖多方面协同关系的复杂概念。在隧道建设方面,它不仅仅是简单的挖掘和结构搭建,而是将智能化技术融入其中,例如采用先进的传感器技术&#x…...
在Anaconda的虚拟环境中安装R,并在vscode中使用
在 Anaconda 的虚拟环境中使用 R,并且希望在 VS Code 中同时使用 Python 和 R,确实需要同时安装 Python 和 R。这是因为 VS Code 的 Jupyter 插件和内核管理依赖于 Python,而 R 则作为 Jupyter 的另一个内核运行。 以下是具体的操作步骤和逻…...
创建型模式 - 建造者模式 (Builder Pattern)
创建型模式 - 建造者模式 (Builder Pattern) 建造者模式是一种创建型设计模式,它将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。 需求描述 在游戏开发中,创建一个复杂的游戏角色,角色具有多种属性&…...
路由追踪核心技术深度解析:Traceroute与Tracert命令实战指南(跨平台/抓包/网络安全防护)
目录 路由器是什么? 路由器的基本功能: 路由追踪技术(Traceroute) 路由追踪的工作原理 实现技术 路由追踪的输出示例 路由追踪的用途 traceroute 命令(Linux 和 macOS) 基本语法 常用选项 示例 …...
音视频入门基础:RTP专题(12)——RTP中的NAL Unit Type简介
一、引言 RTP封装H.264时,RTP对NALU Header的nal_unit_type附加了扩展含义。 由《音视频入门基础:H.264专题(4)——NALU Header:forbidden_zero_bit、nal_ref_idc、nal_unit_type简介》可以知道,nal_unit…...
HTTP GET 请求示例
鸿蒙操作系统(HarmonyOS)是华为公司自主研发的面向全场景的分布式操作系统,旨在为用户提供一个安全、流畅且跨设备无缝连接的体验。它支持多种终端设备,如智能手机、平板电脑、智能电视、汽车等,并实现了模块化解耦&am…...
GO 快速升级Go版本
由于底层依赖升级了,那我们也要跟着升,go老版本已经不足满足需求了,必须要将版本升级到1.22.0以上 查看当前Go版本 命令查看go版本 go version [rootlocalhost local]# go version go version go1.21.4 linux/amd64 [rootlocalhost local]# …...
ELK搭建初入
ELK搭建: 1、安装ElasticSearch (用于存储收集到的日志信息) 解压安装包 tar -xzvf elasticsearch-8.17.2-linux-x86_64.tar.gz 启动es:bin/elasticsearch –d(默认端口号9200) 浏览器输入es地址。出现…...
【redis】数据类型之Bitfields
Redis的Bitfields(位域)与Bitmaps一样,在Redis中并不是一种独立的数据类型,而是一种基于字符串的数据结构,用于处理位级别的操作。允许用户将一个Redis字符串视作由一系列二进制位组成的数组,并对这些位进行…...
vscode软件中引入vant组件
一、vant简介 Vant 是一个轻量、可靠的移动端组件库,于 2017 年开源。 目前 Vant 官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本,并由社区团队维护 React 版本和支付宝小程序版本。 官网:介绍 - Vant Weapp 里面的快速上手的教程&a…...
DDR3模块、HDMI、晶振的布局原则
DDR3模块的布局原则 1.1片DDR就尽量靠近 我们CPU去摆放 2.DDRx2片,相对CPU需要严格对称 3.DDR滤波电容靠近管脚进行放置 4.端接匹配电阻摆放:串联端接电阻放置CPU端,并联端接电阻放置DDR端 5.地址线、控制线、时钟线都是单向传输,…...
夜莺监控 - 边缘告警引擎架构详解
前言 夜莺类似 Grafana 可以接入多个数据源,查询数据源的数据做告警和展示。但是有些数据源所在的机房和中心机房之间网络链路不好,如果由 n9e 进程去周期性查询数据并判定告警,那在网络链路抖动或拥塞的时候,告警就不稳定了。所…...
【CSP/信奥赛通关课(六):信奥赛STL专题】
CSP/信奥赛通关课(六):信奥赛STL专题 课程简介: 讲解信奥赛C中的STL核心组件:容器、迭代器、算法等,分析重点案例,让学生在实践的过程中熟练掌握信奥赛C相关的STL重要知识点。 课程教学目标&…...
【数据结构初阶第十五节】堆的应用(堆排序 + Top-K问题)
必须有为成功付出代价的决心,然后想办法付出这个代价。云边有个稻草人-CSDN博客 对于本节我们要提前掌握前一节课堆的相关实现才能学好本次的知识,一定要多画图多敲代码看看实现的效果是啥(Crazy!)开始吧! …...
SSL/TLS 协议、SSL证书 和 SSH协议 的区别和联系
下面是 SSL/TLS 协议、SSL证书 和 SSH协议 的区别和联系,包含它们的英文全称和中文全称: 属性SSL/TLS 协议SSL证书SSH 协议英文全称Secure Sockets Layer / Transport Layer SecuritySecure Sockets Layer CertificateSecure Shell Protocol中文全称安全…...
数据结构与算法-图论-最短路和其他的结合
介绍 最短路算法常与深度优先搜索(DFS)、动态规划(DP)、二分答案、拓扑排序等算法结合使用: - 最短路与DFS结合:在一些图的路径问题中,当需要访问特定的多个结点,且数据范围较小时…...
C++day6
编写一个如下场景: 有一个英雄Hero类,私有成员,攻击,防御,速度,生命值,以及所有的set get 方法 编写一个 武器 Weapon 类,拥有私有成员攻击力,以及set get 方法 编写一个…...
【初阶数据结构】星河中的光影 “排” 象:排序(下)
文章目录 4.交换排序4.1 冒泡排序(BubbleSort)4.2 快速排序(QuickSort)4.2.1 hoare版本4.2.2 挖坑法4.2.3 前后指针法4.2.4 非递归实现 5.归并排序(MergeSort)5.1 递归实现5.2 非递归实现5.2.1 一次性全部拷…...
C++ 练习1
阐述g 有哪些常用的选项,该选项有什么作用 选项作用-o <file>指定输出文件名(默认生成 a.out)-c仅编译生成目标文件(.o 文件),不链接-E只进行预处理,输出预处理后的代码(展开…...
Ajax数据采集与分析详解
文章目录 1. 什么是 Ajax?2. Ajax 的工作原理3. Ajax 在网页中的应用场景4. 爬取 Ajax 数据的方法4.1 分析网络请求4.2 模拟 Ajax 请求4.3 使用 Selenium 模拟浏览器4.4 使用 Headless 浏览器 5. 处理动态参数6. 处理分页和滚动加载7. 处理反爬虫机制8. 数据存储9. …...
协方差(Covariance)与得分函数:从Fisher信息矩阵看统计关联
协方差与得分函数:从Fisher信息矩阵看统计关联 协方差(Covariance)是统计学中一个基础但强大的概念,它描述了两个随机变量之间的关系。在Fisher信息矩阵中,协方差以一种特别的形式出现:得分函数的协方差。…...
【CSS 选择器的特异度 CSS 继承 CSS 求值过程解析 CSS 布局方式及相关技术】
以下是关于 CSS 选择器特异度、继承、求值过程及布局技术 的详细解析,结合核心概念和实际应用场景: 一、CSS 选择器特异度(Specificity) 1. 特异度规则 特异度用于决定当多个选择器作用于同一元素时,哪个样式优先级更…...
Vue+ElementPlus的一些问题修复汇总
目录 一、ElementPlusVue-router做侧边栏问题 二、 组件样式问题 2.1修改文字颜色、大小、粗细、边框的颜色 2.2修改聚焦后文字的颜色、边框的颜色 2.3修改鼠标悬浮时文字的颜色、边框的颜色 三、 组件样式问题 3.1修改文字颜色、大小、粗细 四、 样式问题 4.1当数据为空…...
单链表删除算法(p=L; j=0;与p=p->next;j=1的辨析)
算法描述 Status ListDelete(LinkList &L,int i) { //在带头结点的单链表 L 中,删除第 i 个元素 pL; j0; while ((p->next) && (j<i-1)) {pp->next; j;} if (!(p->next)||(j>i-1)) return ERROR; qp->nex…...
从单片机的启动说起一个单片机到点灯发生了什么下——使用GPIO点一个灯
目录 前言 HAL库对GPIO的抽象 核心分析:HAL_GPIO_Init 前言 我们终于到达了熟悉的地方,对GPIO的初始化。经过漫长的铺垫,我们终于历经千辛万苦,来到了这里。关于GPIO的八种模式等更加详细的细节,由于只是点个灯&am…...
vue2项目打包后js文件过大, 首次加载缓慢
vue2项目打包后js文件过大, 首次加载缓慢 安装插件 npm i compression-webpack-plugin6.1.1 -D配置vue.config.js const CompressionWebpackPlugin require(compression-webpack-plugin)module.exports {configureWebpack: {plugins:[new CompressionWebpackPlugin({filen…...
llama.cpp 一键运行本地大模型 - Windows
文章目录 llama.cpp 一键运行本地大模型 - Windows嘿,咱来唠唠 llama.cpp 这玩意儿!gguf 格式是啥?咱得好好说道说道基座模型咋选?所需物料,咱得准备齐全咯核心命令,得记牢啦运行方式咋选?测试应…...
Android 老项目 jcenter 库失效
最近重新维护了一些老项目发现大部分jcenter库失效了, Could not resolve com.xx:2.1.3. 如果你也遇到了,不妨试试 替换为 aliyun的jcenter服务,就不用一个个找代替库了。 project 下的 build.gradle 文件添加: maven { url htt…...
MyBatis简明教程
MyBatis 是一个用于简化数据库操作的持久层框架,它的核心思想是 将 SQL 与 Java 代码解耦,让开发者专注于 SQL 的编写,同时自动处理重复的数据库操作步骤。 一、核心思想:SQL 与 Java 解耦 传统 JDBC 需要开发者手动管理数据库连…...
【Golang 面试题】每日 3 题(六十八)
✍个人博客:Pandaconda-CSDN博客 📣专栏地址:http://t.csdnimg.cn/UWz06 📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话,欢迎点赞👍收藏…...
DeepSeek个人知识库
deepseek构建个人知识库 安装软件链接 : 安装链接 先在本地把deepseek跑起来,本地部署deepseek见前文链接: 本地部署ollama # 目前软件只支持1.5b小模型,将就着用 ollama run deepseek-r1:1.5b等服务器启动后开启软件 上传文件 输入消息 (…...
力扣练习之字符串的最大公因子
使用语言:c 题目: 对于字符串 s 和 t,只有在 s t t t ... t t(t 自身连接 1 次或多次)时,我们才认定 “t 能除尽 s”。 给定两个字符串 str1 和 str2 。返回 最长字符串 x,要求满足 x 能…...
姿态矩阵/旋转矩阵/反对称阵
物理意义,端点矢量角速率叉乘本身向量; 负号是动系b看固定系i是相反的; 一个固定 在惯性导航解算中,旋转矢量的叉乘用于描述姿态矩阵的微分方程。你提到的公式中, ω i b b \boldsymbol{\omega}_{ib}^b \times ωibb…...
项目一 - 任务3:搭建Java集成开发环境IntelliJ IDEA
通过本次实战,我们成功搭建了Java集成开发环境IntelliJ IDEA,并完成了多个任务。首先,安装了IntelliJ IDEA并进行了个性化设置,如选择主题、调整字体和编码等。接着,创建了Java项目、包和类,编写并运行了简…...
C++的类型转换
目录 一、隐式类型转换的触发场景 1.基本数据类型间的转换 i.提升转换 ii.截断转换 2.类与对象的转换 i.单参数构造函数 ii.类型转换运算符 3.继承体系中的指针/引用转换 向上转型 二、隐式转换的风险与问题 1.意外行为 2.二义性错误 3.性能损耗 三、C强制类型转…...
嵌入式项目:STM32刷卡指纹智能门禁系统
本文详细介绍基于STM32的刷卡指纹智能门禁系统。 获取资料/指导答疑/技术交流/选题/帮助,请点链接: https://gitee.com/zengzhaorong/share_contact/blob/master/stm32.txt 1 系统功能 1.1 功能概述 本系统由STM32硬件端(下位机)…...
DeepSeek基础之机器学习
文章目录 一、核心概念总结(一)机器学习基本定义(二)基本术语(三)假设空间(四)归纳偏好(五)“没有免费的午餐”定理(NFL 定理) 二、重…...
Docker 搭建 Nginx 服务器
系列文章目录 Docker 搭建 Nginx 服务器 系列文章目录前言一、准备工作二、设置 Nginx 容器的目录结构三、启动一个临时的 Nginx 容器来复制配置文件四、复制 Nginx 配置文件到本地目录五、删除临时 Nginx 容器六、创建并运行 Nginx 容器,挂载本地目录七、修改 ngin…...