From c3491b55bd40c850effaec7826ac405dde787350 Mon Sep 17 00:00:00 2001 From: zggsong Date: Sun, 14 Apr 2024 18:43:08 +0800 Subject: [PATCH] feat: add ollama service #55 --- src/STranslate.Model/Enums.cs | 4 + src/STranslate.Style/Resources/ollama.png | Bin 0 -> 890 bytes src/STranslate.Style/STranslate.Style.csproj | 3 + src/STranslate.Style/Styles/IconStyle.xaml | 1 + src/STranslate/Helper/ConfigHelper.cs | 1 + src/STranslate/ViewModels/InputViewModel.cs | 2 + src/STranslate/ViewModels/OutputViewModel.cs | 81 +++-- .../History/HistoryContentViewModel.cs | 1 + .../ViewModels/Preference/ServiceViewModel.cs | 3 + .../Preference/Services/TranslatorOllama.cs | 323 ++++++++++++++++++ .../Service/TextOllamaServicesPage.xaml | 200 +++++++++++ .../Service/TextOllamaServicesPage.xaml.cs | 49 +++ 12 files changed, 642 insertions(+), 26 deletions(-) create mode 100644 src/STranslate.Style/Resources/ollama.png create mode 100644 src/STranslate/ViewModels/Preference/Services/TranslatorOllama.cs create mode 100644 src/STranslate/Views/Preference/Service/TextOllamaServicesPage.xaml create mode 100644 src/STranslate/Views/Preference/Service/TextOllamaServicesPage.xaml.cs diff --git a/src/STranslate.Model/Enums.cs b/src/STranslate.Model/Enums.cs index 5d5c7d33..d9c29dbe 100644 --- a/src/STranslate.Model/Enums.cs +++ b/src/STranslate.Model/Enums.cs @@ -216,6 +216,7 @@ public enum ServiceType STranslateService, EcdictService, ChatglmService, + OllamaService, } public enum TTSType @@ -330,6 +331,9 @@ public enum IconType [Description("腾讯OCR")] TencentOCR, + + [Description("Ollama")] + Ollama, } /// diff --git a/src/STranslate.Style/Resources/ollama.png b/src/STranslate.Style/Resources/ollama.png new file mode 100644 index 0000000000000000000000000000000000000000..e1130b2384db9f759a7894a3ef7d0c0ad5d43503 GIT binary patch literal 890 zcmV-=1BLvFP)X|6tF}4%nzZp#u zTb5*5l9i5V2`Objj|tHqqKGIY3d-p_Q9*Pe`bV_ybUJtO6ClMQQ9^W+uOY))@h-6A zQt_uq#H%QGowJ19ZYL*`iQI0tjoQ*QRlHcEA#d#sVvKatyY4LMbUgduauD&L<=lxMKn0-{df7z~;T=(q zC)I#zoC6GF%%dM2muDAXfHg!BIAmch?YPD>AXvW}%(O$Ph72~?f*VRPeD!oo_51zm zOl?Pkiic{*l@EM8uJZwUEEy=^0qZ62avd%tt_|2N2=6F9RFa?g;T2CdWzIt6OGCI8 z3c1km+1r46;)#J6M}Qnkge4pJW-~!T6}3Ub<+SgX-Roc5Y|B*g!Xsl_bDs?Tx?p~-v~oXU`8AZ5K>&4#sjq@)6;j@kLV1C(P81-ahd{79`S z#m>ctw;>9b;l*4wwdb;I%ekxh6o`ScQO%{LE$^BG Never + + Never + Never diff --git a/src/STranslate.Style/Styles/IconStyle.xaml b/src/STranslate.Style/Styles/IconStyle.xaml index 8c45cd43..b02b868b 100644 --- a/src/STranslate.Style/Styles/IconStyle.xaml +++ b/src/STranslate.Style/Styles/IconStyle.xaml @@ -25,4 +25,5 @@ + \ No newline at end of file diff --git a/src/STranslate/Helper/ConfigHelper.cs b/src/STranslate/Helper/ConfigHelper.cs index 235947a0..f5e40f70 100644 --- a/src/STranslate/Helper/ConfigHelper.cs +++ b/src/STranslate/Helper/ConfigHelper.cs @@ -614,6 +614,7 @@ public override ITranslator ReadJson(JsonReader reader, Type objectType, ITransl (int)ServiceType.VolcengineService => new TranslatorVolcengine(), (int)ServiceType.EcdictService => new TranslatorEcdict(), (int)ServiceType.ChatglmService => new TranslatorChatglm(), + (int)ServiceType.OllamaService => new TranslatorOllama(), //TODO: 新接口需要适配 _ => throw new NotSupportedException($"Unsupported ServiceType: {type}") }; diff --git a/src/STranslate/ViewModels/InputViewModel.cs b/src/STranslate/ViewModels/InputViewModel.cs index 08dbc4fc..eb2cf48b 100644 --- a/src/STranslate/ViewModels/InputViewModel.cs +++ b/src/STranslate/ViewModels/InputViewModel.cs @@ -171,6 +171,7 @@ private bool PreviousHandle() case ServiceType.GeminiService: case ServiceType.OpenAIService: case ServiceType.ChatglmService: + case ServiceType.OllamaService: { //流式处理目前给AI使用,所以可以传递识别语言给AI做更多处理 //Auto则转换为识别语种 @@ -502,6 +503,7 @@ public override ITranslator ReadJson(JsonReader reader, Type objectType, ITransl (int)ServiceType.VolcengineService => new TranslatorVolcengine(), (int)ServiceType.EcdictService => new TranslatorEcdict(), (int)ServiceType.ChatglmService => new TranslatorChatglm(), + (int)ServiceType.OllamaService => new TranslatorOllama(), _ => new TranslatorApi() }; diff --git a/src/STranslate/ViewModels/OutputViewModel.cs b/src/STranslate/ViewModels/OutputViewModel.cs index e9e36c60..34a81939 100644 --- a/src/STranslate/ViewModels/OutputViewModel.cs +++ b/src/STranslate/ViewModels/OutputViewModel.cs @@ -2,11 +2,14 @@ using CommunityToolkit.Mvvm.Input; using GongSolutions.Wpf.DragDrop; using STranslate.Helper; +using STranslate.Log; using STranslate.Model; using STranslate.Util; using STranslate.ViewModels.Preference; +using System; using System.ComponentModel; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -21,37 +24,63 @@ public partial class OutputViewModel : ObservableObject, IDropTarget [RelayCommand(IncludeCancelCommand = true)] private async Task SingleTranslateAsync(ITranslator service, CancellationToken token) { - var inputVM = Singleton.Instance; - var sourceLang = Singleton.Instance.SourceLang; - var targetLang = Singleton.Instance.TargetLang; - - var idetify = LangEnum.auto; - //如果是自动则获取自动识别后的目标语种 - if (targetLang == LangEnum.auto) + try { - var autoRet = StringUtil.AutomaticLanguageRecognition(inputVM.InputContent); - idetify = autoRet.Item1; - targetLang = autoRet.Item2; + var inputVM = Singleton.Instance; + var sourceLang = Singleton.Instance.SourceLang; + var targetLang = Singleton.Instance.TargetLang; + + var idetify = LangEnum.auto; + //如果是自动则获取自动识别后的目标语种 + if (targetLang == LangEnum.auto) + { + var autoRet = StringUtil.AutomaticLanguageRecognition(inputVM.InputContent); + idetify = autoRet.Item1; + targetLang = autoRet.Item2; + } + + //根据不同服务类型区分-默认非流式请求数据,若走此种方式请求则无需添加 + //TODO: 新接口需要适配 + switch (service.Type) + { + case ServiceType.GeminiService: + case ServiceType.OpenAIService: + case ServiceType.ChatglmService: + case ServiceType.OllamaService: + { + //流式处理目前给AI使用,所以可以传递识别语言给AI做更多处理 + //Auto则转换为识别语种 + sourceLang = sourceLang == LangEnum.auto ? idetify : sourceLang; + await inputVM.StreamHandlerAsync(service, inputVM.InputContent, sourceLang, targetLang, token); + break; + } + + default: + await inputVM.NonStreamHandlerAsync(service, inputVM.InputContent, sourceLang, targetLang, token); + break; + } } - - //根据不同服务类型区分-默认非流式请求数据,若走此种方式请求则无需添加 - //TODO: 新接口需要适配 - switch (service.Type) + catch (Exception exception) { - case ServiceType.GeminiService: - case ServiceType.OpenAIService: - case ServiceType.ChatglmService: - { - //流式处理目前给AI使用,所以可以传递识别语言给AI做更多处理 - //Auto则转换为识别语种 - sourceLang = sourceLang == LangEnum.auto ? idetify : sourceLang; - await inputVM.StreamHandlerAsync(service, inputVM.InputContent, sourceLang, targetLang, token); + var errorMessage = ""; + var isCancelMsg = false; + switch (exception) + { + case TaskCanceledException: + errorMessage = token.IsCancellationRequested ? "请求取消" : "请求超时"; + isCancelMsg = token.IsCancellationRequested; break; - } + case HttpRequestException: + errorMessage = "请求出错"; + break; + } + + service.Data = TranslationResult.Fail($"{errorMessage}: {exception.Message}", exception); - default: - await inputVM.NonStreamHandlerAsync(service, inputVM.InputContent, sourceLang, targetLang, token); - break; + if (isCancelMsg) + LogService.Logger.Debug($"[{service.Name}({service.Identify})] {errorMessage}, 请求API: {service.Url}, 异常信息: {exception.Message}"); + else + LogService.Logger.Error($"[{service.Name}({service.Identify})] {errorMessage}, 请求API: {service.Url}, 异常信息: {exception.Message}"); } } diff --git a/src/STranslate/ViewModels/Preference/History/HistoryContentViewModel.cs b/src/STranslate/ViewModels/Preference/History/HistoryContentViewModel.cs index fa9ea73c..df91844a 100644 --- a/src/STranslate/ViewModels/Preference/History/HistoryContentViewModel.cs +++ b/src/STranslate/ViewModels/Preference/History/HistoryContentViewModel.cs @@ -142,6 +142,7 @@ public override ITranslator ReadJson(JsonReader reader, Type objectType, ITransl (int)ServiceType.VolcengineService => new TranslatorVolcengine(), (int)ServiceType.EcdictService => new TranslatorEcdict(), (int)ServiceType.ChatglmService => new TranslatorChatglm(), + (int)ServiceType.OllamaService => new TranslatorOllama(), //TODO: 新接口需要适配 _ => throw new NotSupportedException($"Unsupported ServiceType: {type}") }; diff --git a/src/STranslate/ViewModels/Preference/ServiceViewModel.cs b/src/STranslate/ViewModels/Preference/ServiceViewModel.cs index b13e7c37..549a9806 100644 --- a/src/STranslate/ViewModels/Preference/ServiceViewModel.cs +++ b/src/STranslate/ViewModels/Preference/ServiceViewModel.cs @@ -36,6 +36,7 @@ public ServiceViewModel() TransServices.Add(new TranslatorCaiyun()); TransServices.Add(new TranslatorVolcengine()); TransServices.Add(new TranslatorChatglm()); + TransServices.Add(new TranslatorOllama()); ResetView(); } @@ -105,6 +106,7 @@ private void TogglePage(ITranslator service) ServiceType.VolcengineService => string.Format("{0}TextVolcengineServicesPage", head), ServiceType.EcdictService => string.Format("{0}TextEcdictServicesPage", head), ServiceType.ChatglmService => string.Format("{0}TextChatglmServicesPage", head), + ServiceType.OllamaService => string.Format("{0}TextOllamaServicesPage", head), _ => string.Format("{0}TextApiServicePage", head) }; @@ -136,6 +138,7 @@ private void Add(List list) TranslatorVolcengine volcengine => volcengine.Clone(), TranslatorEcdict ecdict => ecdict.Clone(), TranslatorChatglm chatglm => chatglm.Clone(), + TranslatorOllama ollama => ollama.Clone(), _ => throw new InvalidOperationException($"Unsupported service type: {service.GetType().Name}") }); diff --git a/src/STranslate/ViewModels/Preference/Services/TranslatorOllama.cs b/src/STranslate/ViewModels/Preference/Services/TranslatorOllama.cs new file mode 100644 index 00000000..daf10f02 --- /dev/null +++ b/src/STranslate/ViewModels/Preference/Services/TranslatorOllama.cs @@ -0,0 +1,323 @@ + +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using STranslate.Model; +using STranslate.Util; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace STranslate.ViewModels.Preference.Services +{ + public partial class TranslatorOllama : ObservableObject, ITranslator + { + #region Constructor + + public TranslatorOllama() : this(Guid.NewGuid(), "http://localhost:11443", "Ollama") + { + } + + public TranslatorOllama(Guid guid, string url, string name = "", IconType icon = IconType.Ollama, string appID = "", string appKey = "", bool isEnabled = true, ServiceType type = ServiceType.OllamaService) + { + Identify = guid; + Url = url; + Name = name; + Icon = icon; + AppID = appID; + AppKey = appKey; + IsEnabled = isEnabled; + Type = type; + } + + #endregion Constructor + + #region Properties + + [ObservableProperty] + private Guid _identify = Guid.Empty; + + [JsonIgnore] + [ObservableProperty] + private ServiceType _type = 0; + + [JsonIgnore] + [ObservableProperty] + public bool _isEnabled = true; + + [JsonIgnore] + [ObservableProperty] + private string _name = string.Empty; + + [JsonIgnore] + [ObservableProperty] + private IconType _icon = IconType.Bing; + + [JsonIgnore] + [ObservableProperty] + [property: DefaultValue("")] + [property: JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string _url = string.Empty; + + [JsonIgnore] + [ObservableProperty] + [property: DefaultValue("")] + [property: JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string _appID = string.Empty; + + [JsonIgnore] + [ObservableProperty] + [property: DefaultValue("")] + [property: JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string _appKey = string.Empty; + + [JsonIgnore] + [ObservableProperty] + private bool _autoExpander = true; + + [JsonIgnore] + [ObservableProperty] + [property: DefaultValue("")] + [property: JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + private string _model = "gpt-3.5-turbo"; + + [JsonIgnore] + [ObservableProperty] + [property: JsonIgnore] + public TranslationResult _data = TranslationResult.Reset; + + [JsonIgnore] + public Dictionary Icons { get; private set; } = ConstStr.ICONDICT; + + #region Show/Hide Encrypt Info + + [JsonIgnore] + [ObservableProperty] + [property: JsonIgnore] + private bool _keyHide = true; + + private void ShowEncryptInfo() => KeyHide = !KeyHide; + + private RelayCommand? showEncryptInfoCommand; + + [JsonIgnore] + public IRelayCommand ShowEncryptInfoCommand => showEncryptInfoCommand ??= new RelayCommand(new Action(ShowEncryptInfo)); + + #endregion Show/Hide Encrypt Info + + #region Prompt + + [JsonIgnore] + [ObservableProperty] + private BindingList _userDefinePrompts = + [ + new UserDefinePrompt("翻译", [new Prompt("system", "You are a professional, authentic translation engine. You only return the translated text, without any explanations."), new Prompt("user", "Please translate into $target (avoid explaining the original text):\r\n\r\n$content")], true), + new UserDefinePrompt("润色", [new Prompt("system", "You are a text embellisher, you can only embellish the text, never interpret it."), new Prompt("user", "Embellish the following text in $source: $content")]), + new UserDefinePrompt("总结", [new Prompt("system", "You are a text summarizer, you can only summarize the text, never interpret it."), new Prompt("user", "Summarize the following text in $source: $content")]), + ]; + + [RelayCommand] + [property: JsonIgnore] + private void SelectedPrompt(List obj) + { + var userDefinePrompt = (UserDefinePrompt)obj.First(); + foreach (var item in UserDefinePrompts) + { + item.Enabled = false; + } + userDefinePrompt.Enabled = true; + + if (obj.Count == 2) Singleton.Instance.SaveCommand.Execute(null); + } + + [RelayCommand] + [property: JsonIgnore] + private void UpdatePrompt(UserDefinePrompt userDefinePrompt) + { + var dialog = new Views.Preference.Service.PromptDialog(ServiceType.OpenAIService, (UserDefinePrompt)userDefinePrompt.Clone()); + if (dialog.ShowDialog() ?? false) + { + var tmp = ((PromptViewModel)dialog.DataContext).UserDefinePrompt; + userDefinePrompt.Name = tmp.Name; + userDefinePrompt.Prompts = tmp.Prompts; + } + } + + [RelayCommand] + [property: JsonIgnore] + private void DeletePrompt(UserDefinePrompt userDefinePrompt) + { + UserDefinePrompts.Remove(userDefinePrompt); + } + + [RelayCommand] + [property: JsonIgnore] + private void AddPrompt() + { + var userDefinePrompt = new UserDefinePrompt("Undefined", []); + var dialog = new Views.Preference.Service.PromptDialog(ServiceType.OpenAIService, userDefinePrompt); + if (dialog.ShowDialog() ?? false) + { + var tmp = ((PromptViewModel)dialog.DataContext).UserDefinePrompt; + userDefinePrompt.Name = tmp.Name; + userDefinePrompt.Prompts = tmp.Prompts; + UserDefinePrompts.Add(userDefinePrompt); + } + } + + #endregion Prompt + + #endregion Properties + + #region Interface Implementation + + public async Task TranslateAsync(object request, Action OnDataReceived, CancellationToken token) + { + if (string.IsNullOrEmpty(Url)) + throw new Exception("请先完善配置"); + + if (request is not RequestModel req) + throw new Exception($"请求数据出错: {request}"); + + //检查语种 + var source = LangConverter(req.SourceLang) ?? throw new Exception($"该服务不支持{req.SourceLang.GetDescription()}"); + var target = LangConverter(req.TargetLang) ?? throw new Exception($"该服务不支持{req.TargetLang.GetDescription()}"); + var content = req.Text; + + UriBuilder uriBuilder = new(Url); + + if (!uriBuilder.Path.EndsWith("/api/chat")) + { + uriBuilder.Path = "/api/chat"; + } + + // 选择模型 + var a_model = Model.Trim(); + a_model = string.IsNullOrEmpty(a_model) ? "gpt-3.5-turbo" : a_model; + + // 替换Prompt关键字 + var a_messages = (UserDefinePrompts.FirstOrDefault(x => x.Enabled)?.Prompts ?? throw new Exception("请先完善Propmpt配置")).Clone(); + a_messages.ToList().ForEach(item => item.Content = item.Content.Replace("$source", source).Replace("$target", target).Replace("$content", content)); + + // 构建请求数据 + var reqData = new + { + model = a_model, + messages = a_messages, + //temperature = 1.0, + stream = true + }; + + var jsonData = JsonConvert.SerializeObject(reqData); + + await HttpUtil.PostAsync( + uriBuilder.Uri, + jsonData, + AppKey, + msg => + { + if (string.IsNullOrEmpty(msg?.Trim())) + return; + + var preprocessString = msg./*Replace("data:", "").*/Trim(); + + // 解析JSON数据 + var parsedData = JsonConvert.DeserializeObject(preprocessString); + + if (parsedData is null) + return; + + // 如果done不是true、false或者done标记为true则结束读取结果 + if (!bool.TryParse(parsedData["done"]?.ToString() ?? "", out bool done) || done) + return; + + // 提取content的值 + var contentValue = parsedData["message"]?["content"]?.ToString(); + + if (string.IsNullOrEmpty(contentValue)) + return; + + OnDataReceived?.Invoke(contentValue); + }, + token + ); + + } + + public Task TranslateAsync(object request, CancellationToken token) + { + throw new NotImplementedException(); + } + + public ITranslator Clone() + { + return new TranslatorOllama + { + Identify = this.Identify, + Type = this.Type, + IsEnabled = this.IsEnabled, + Icon = this.Icon, + Name = this.Name, + Url = this.Url, + Data = TranslationResult.Reset, + AppID = this.AppID, + AppKey = this.AppKey, + UserDefinePrompts = this.UserDefinePrompts, + AutoExpander = this.AutoExpander, + Icons = this.Icons, + KeyHide = this.KeyHide, + Model = this.Model, + }; + } + + /// + /// https://zh.wikipedia.org/wiki/ISO_639-1%E4%BB%A3%E7%A0%81%E5%88%97%E8%A1%A8 + /// + /// + /// + public string? LangConverter(LangEnum lang) + { + return lang switch + { + LangEnum.auto => "auto", + LangEnum.zh_cn => "zh-cn", + LangEnum.zh_tw => "zh-tw", + LangEnum.yue => "yue", + LangEnum.ja => "ja", + LangEnum.en => "en", + LangEnum.ko => "ko", + LangEnum.fr => "fr", + LangEnum.es => "es", + LangEnum.ru => "ru", + LangEnum.de => "de", + LangEnum.it => "it", + LangEnum.tr => "tr", + LangEnum.pt_pt => "pt_pt", + LangEnum.pt_br => "pt_br", + LangEnum.vi => "vi", + LangEnum.id => "id", + LangEnum.th => "th", + LangEnum.ms => "ms", + LangEnum.ar => "ar", + LangEnum.hi => "hi", + LangEnum.mn_cy => "mn_cy", + LangEnum.mn_mo => "mn_mo", + LangEnum.km => "km", + LangEnum.nb_no => "nb_no", + LangEnum.nn_no => "nn_no", + LangEnum.fa => "fa", + LangEnum.sv => "sv", + LangEnum.pl => "pl", + LangEnum.nl => "nl", + LangEnum.uk => "uk", + _ => "auto" + }; + } + + #endregion Interface Implementation + } +} \ No newline at end of file diff --git a/src/STranslate/Views/Preference/Service/TextOllamaServicesPage.xaml b/src/STranslate/Views/Preference/Service/TextOllamaServicesPage.xaml new file mode 100644 index 00000000..def6037d --- /dev/null +++ b/src/STranslate/Views/Preference/Service/TextOllamaServicesPage.xaml @@ -0,0 +1,200 @@ + + + + + + + + + + + + +