diff --git a/STranslate.Model/Enums.cs b/STranslate.Model/Enums.cs index b395114c..91559b1d 100644 --- a/STranslate.Model/Enums.cs +++ b/STranslate.Model/Enums.cs @@ -102,7 +102,8 @@ public enum ServiceType ApiService = 0, BaiduService, BingService, - OpenAIService + OpenAIService, + GeminiService, } /// @@ -118,6 +119,7 @@ public enum IconType Youdao, Bing, OpenAI, + Gemini, } /// diff --git a/STranslate.Style/Resources/gemini.png b/STranslate.Style/Resources/gemini.png new file mode 100644 index 00000000..0855e34b Binary files /dev/null and b/STranslate.Style/Resources/gemini.png differ diff --git a/STranslate.Style/STranslate.Style.csproj b/STranslate.Style/STranslate.Style.csproj index d9c7777f..96e97ae2 100644 --- a/STranslate.Style/STranslate.Style.csproj +++ b/STranslate.Style/STranslate.Style.csproj @@ -40,6 +40,9 @@ Never + + Never + Never diff --git a/STranslate.Style/Styles/IconStyle.xaml b/STranslate.Style/Styles/IconStyle.xaml index 2b7cdfbe..a1422d54 100644 --- a/STranslate.Style/Styles/IconStyle.xaml +++ b/STranslate.Style/Styles/IconStyle.xaml @@ -9,4 +9,5 @@ + \ No newline at end of file diff --git a/STranslate/Helper/ConfigHelper.cs b/STranslate/Helper/ConfigHelper.cs index ac914c23..000ca321 100644 --- a/STranslate/Helper/ConfigHelper.cs +++ b/STranslate/Helper/ConfigHelper.cs @@ -309,6 +309,7 @@ JsonSerializer serializer (int)ServiceType.BaiduService => new TranslatorBaidu(), (int)ServiceType.BingService => new TranslatorBing(), (int)ServiceType.OpenAIService => new TranslatorOpenAI(), + (int)ServiceType.GeminiService => new TranslatorGemini(), //TODO: 新接口需要适配 _ => throw new NotSupportedException($"Unsupported ServiceType: {type}") }; diff --git a/STranslate/ViewModels/InputViewModel.cs b/STranslate/ViewModels/InputViewModel.cs index 499c4c50..4642a2c5 100644 --- a/STranslate/ViewModels/InputViewModel.cs +++ b/STranslate/ViewModels/InputViewModel.cs @@ -193,6 +193,9 @@ await Parallel.ForEachAsync( case ServiceType.OpenAIService: await ServiceHandler.OpenAIHandlerAsync(service, InputContent, sourceStr, targetStr, token); break; + case ServiceType.GeminiService: + await ServiceHandler.GeminiHandlerAsync(service, InputContent, sourceStr, targetStr, token); + break; default: break; } @@ -400,6 +403,7 @@ public class CurrentTranslatorConverter : JsonConverter (int)ServiceType.BaiduService => new TranslatorBaidu(), (int)ServiceType.BingService => new TranslatorBing(), (int)ServiceType.OpenAIService => new TranslatorOpenAI(), + (int)ServiceType.GeminiService => new TranslatorGemini(), _ => new TranslatorApi(), }; diff --git a/STranslate/ViewModels/Preference/History/HistoryContentViewModel.cs b/STranslate/ViewModels/Preference/History/HistoryContentViewModel.cs index 243f5717..76e2e729 100644 --- a/STranslate/ViewModels/Preference/History/HistoryContentViewModel.cs +++ b/STranslate/ViewModels/Preference/History/HistoryContentViewModel.cs @@ -92,6 +92,7 @@ JsonSerializer serializer (int)ServiceType.BaiduService => new TranslatorBaidu(), (int)ServiceType.BingService => new TranslatorBing(), (int)ServiceType.OpenAIService => new TranslatorOpenAI(), + (int)ServiceType.GeminiService => new TranslatorGemini(), //TODO: 新接口需要适配 _ => throw new NotSupportedException($"Unsupported ServiceType: {type}") }; diff --git a/STranslate/ViewModels/Preference/ServiceViewModel.cs b/STranslate/ViewModels/Preference/ServiceViewModel.cs index dd4f42a3..814d9256 100644 --- a/STranslate/ViewModels/Preference/ServiceViewModel.cs +++ b/STranslate/ViewModels/Preference/ServiceViewModel.cs @@ -26,6 +26,7 @@ public ServiceViewModel() TransServices.Add(new TranslatorBaidu()); TransServices.Add(new TranslatorBing()); TransServices.Add(new TranslatorOpenAI()); + TransServices.Add(new TranslatorGemini()); ResetView(); } @@ -87,6 +88,7 @@ private void TogglePage(ITranslator service) ServiceType.BaiduService => string.Format("{0}TextBaiduServicesPage", head), ServiceType.BingService => string.Format("{0}TextBingServicesPage", head), ServiceType.OpenAIService => string.Format("{0}TextOpenAIServicesPage", head), + ServiceType.GeminiService => string.Format("{0}TextGeminiServicesPage", head), _ => string.Format("{0}TextApiServicePage", head) }; @@ -111,6 +113,7 @@ private void Add(List list) TranslatorBaidu baidu => baidu.DeepClone(), TranslatorBing bing => bing.DeepClone(), TranslatorOpenAI openAI => openAI.DeepClone(), + TranslatorGemini gemini => gemini.DeepClone(), _ => throw new InvalidOperationException($"Unsupported service type: {service.GetType().Name}") }); diff --git a/STranslate/ViewModels/Preference/Services/TranslatorBaidu.cs b/STranslate/ViewModels/Preference/Services/TranslatorBaidu.cs index d43c4b61..4bce3f28 100644 --- a/STranslate/ViewModels/Preference/Services/TranslatorBaidu.cs +++ b/STranslate/ViewModels/Preference/Services/TranslatorBaidu.cs @@ -126,10 +126,11 @@ public bool KeyHide } } } - - [RelayCommand] - private void ShowEncryptInfo(string obj) + + private void ShowEncryptInfo(string? obj) { + if (obj == null) return; + if (obj.Equals(nameof(AppID))) { IdHide = !IdHide; @@ -140,6 +141,10 @@ private void ShowEncryptInfo(string obj) } } + private RelayCommand? showEncryptInfoCommand; + [JsonIgnore] + public IRelayCommand ShowEncryptInfoCommand => showEncryptInfoCommand ??= new RelayCommand(new Action(ShowEncryptInfo)); + #endregion Show/Hide Encrypt Info public async Task TranslateAsync(object request, CancellationToken token) diff --git a/STranslate/ViewModels/Preference/Services/TranslatorBing.cs b/STranslate/ViewModels/Preference/Services/TranslatorBing.cs index ad5e7930..0a41c328 100644 --- a/STranslate/ViewModels/Preference/Services/TranslatorBing.cs +++ b/STranslate/ViewModels/Preference/Services/TranslatorBing.cs @@ -131,9 +131,10 @@ public bool KeyHide } } - [RelayCommand] - private void ShowEncryptInfo(string obj) + private void ShowEncryptInfo(string? obj) { + if (obj == null) return; + if (obj.Equals(nameof(AppID))) { IdHide = !IdHide; @@ -144,6 +145,10 @@ private void ShowEncryptInfo(string obj) } } + private RelayCommand? showEncryptInfoCommand; + [JsonIgnore] + public IRelayCommand ShowEncryptInfoCommand => showEncryptInfoCommand ??= new RelayCommand(new Action(ShowEncryptInfo)); + #endregion Show/Hide Encrypt Info public async Task TranslateAsync(object request, CancellationToken token) diff --git a/STranslate/ViewModels/Preference/Services/TranslatorGemini.cs b/STranslate/ViewModels/Preference/Services/TranslatorGemini.cs new file mode 100644 index 00000000..114fc5ce --- /dev/null +++ b/STranslate/ViewModels/Preference/Services/TranslatorGemini.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Security.Policy; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using STranslate.Model; +using STranslate.Util; + +namespace STranslate.ViewModels.Preference.Services +{ + public partial class TranslatorGemini : ObservableObject, ITranslator + { + public TranslatorGemini() + : this(Guid.NewGuid(), "https://generativelanguage.googleapis.com", "Gemini") { } + + public TranslatorGemini( + Guid guid, + string url, + string name = "", + IconType icon = IconType.Gemini, + string appID = "", + string appKey = "", + bool isEnabled = true, + ServiceType type = ServiceType.GeminiService + ) + { + Identify = guid; + Url = url; + Name = name; + Icon = icon; + AppID = appID; + AppKey = appKey; + IsEnabled = isEnabled; + Type = type; + } + + [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.Gemini; + + [JsonIgnore] + [ObservableProperty] + public string _url = string.Empty; + + [JsonIgnore] + [ObservableProperty] + public string _AppID = string.Empty; + + [JsonIgnore] + [ObservableProperty] + public string _appKey = string.Empty; + + [JsonIgnore] + public object _data = string.Empty; + + [JsonIgnore] + public object Data + { + get => _data; + set + { + if (_data != value) + { + OnPropertyChanging(nameof(Data)); + _data = value; + OnPropertyChanged(nameof(Data)); + } + } + } + + [JsonIgnore] + public List Icons { get; private set; } = Enum.GetValues(typeof(IconType)).OfType().ToList(); + + #region Show/Hide Encrypt Info + + [JsonIgnore] + private bool _keyHide = true; + + [JsonIgnore] + public bool KeyHide + { + get => _keyHide; + set + { + if (_keyHide != value) + { + OnPropertyChanging(nameof(KeyHide)); + _keyHide = value; + OnPropertyChanged(nameof(KeyHide)); + } + } + } + + + private void ShowEncryptInfo() => KeyHide = !KeyHide; + + private RelayCommand? showEncryptInfoCommand; + [JsonIgnore] + public IRelayCommand ShowEncryptInfoCommand => showEncryptInfoCommand ??= new RelayCommand(new Action(ShowEncryptInfo)); + + #endregion Show/Hide Encrypt Info + + [Obsolete] + public async Task TranslateAsync(object request, CancellationToken token) + { + try + { + if (string.IsNullOrEmpty(Url) || string.IsNullOrEmpty(AppKey)) + throw new Exception("请先完善配置"); + + if (!Url.EndsWith("completions")) + { + Url = Url.TrimEnd('/') + "/completions"; + } + + if (request != null) + { + var jsonData = JsonConvert.SerializeObject(request); + + // 构建请求 + var client = new HttpClient(new SocketsHttpHandler()); + var req = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri(Url), + Content = new StringContent(jsonData, Encoding.UTF8, "application/json") + }; + req.Headers.Add("Authorization", $"Bearer {AppKey}"); + + // 发送请求 + using var response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, token); + // 获取响应流 + using var responseStream = await response.Content.ReadAsStreamAsync(token); + using var reader = new System.IO.StreamReader(responseStream); + // 逐行读取并输出结果 + while (!reader.EndOfStream || token.IsCancellationRequested) + { + var line = await reader.ReadLineAsync(token); + + if (string.IsNullOrEmpty(line?.Trim())) + continue; + + var preprocessString = line.Replace("data:", "").Trim(); + + // 结束标记 + if (preprocessString.Equals("[DONE]")) + break; + + // 解析JSON数据 + var parsedData = JsonConvert.DeserializeObject(preprocessString); + + if (parsedData is null) + continue; + + // 提取content的值 + var contentValue = parsedData["choices"]?.FirstOrDefault()?["delta"]?["content"]?.ToString(); + + if (string.IsNullOrEmpty(contentValue)) + continue; + + // 输出 + Data += contentValue; + //Debug.Write(contentValue); + } + } + } + catch (Exception ex) + { + Data = ex.Message; + } + + return Task.FromResult(null); + } + } +} diff --git a/STranslate/ViewModels/Preference/Services/TranslatorOpenAI.cs b/STranslate/ViewModels/Preference/Services/TranslatorOpenAI.cs index 0103ffde..47b16f7f 100644 --- a/STranslate/ViewModels/Preference/Services/TranslatorOpenAI.cs +++ b/STranslate/ViewModels/Preference/Services/TranslatorOpenAI.cs @@ -114,9 +114,12 @@ public bool KeyHide } } - [RelayCommand] private void ShowEncryptInfo() => KeyHide = !KeyHide; + private RelayCommand? showEncryptInfoCommand; + [JsonIgnore] + public IRelayCommand ShowEncryptInfoCommand => showEncryptInfoCommand ??= new RelayCommand(new Action(ShowEncryptInfo)); + #endregion Show/Hide Encrypt Info [Obsolete] diff --git a/STranslate/ViewModels/ServiceHandler.cs b/STranslate/ViewModels/ServiceHandler.cs index d801055f..5a11b972 100644 --- a/STranslate/ViewModels/ServiceHandler.cs +++ b/STranslate/ViewModels/ServiceHandler.cs @@ -1,13 +1,14 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using STranslate.Model; -using STranslate.Util; -using System; +using System; using System.Linq; using System.Net.Http; +using System.Security.Policy; using System.Text; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using STranslate.Model; +using STranslate.Util; namespace STranslate.ViewModels { @@ -113,18 +114,19 @@ public static async Task OpenAIHandlerAsync(ITranslator service, string content, if (string.IsNullOrEmpty(service.Url) || string.IsNullOrEmpty(service.AppKey)) throw new Exception("请先完善配置"); - if (!service.Url.EndsWith("/v1/completions")) + UriBuilder uriBuilder = new(service.Url); + + if (!uriBuilder.Path.EndsWith("/v1/completions")) { - service.Url = service.Url.TrimEnd('/') + "/v1/completions"; + uriBuilder.Path = uriBuilder.Path.TrimEnd('/') + "/v1/completions"; } + // 构建请求数据 var reqData = new { model = "gpt-3.5-turbo", - messages = new[] { - new { role = "user", content = $"Translate the following text to {target}: {content}" } - }, - temperature = 0, + messages = new[] { new { role = "user", content = $"Translate the following text to {target}: {content}" } }, + temperature = 1.0, stream = true }; @@ -136,7 +138,7 @@ public static async Task OpenAIHandlerAsync(ITranslator service, string content, var req = new HttpRequestMessage { Method = HttpMethod.Post, - RequestUri = new Uri(service.Url), + RequestUri = uriBuilder.Uri, Content = new StringContent(jsonData, Encoding.UTF8, "application/json") }; req.Headers.Add("Authorization", $"Bearer {service.AppKey}"); @@ -146,31 +148,110 @@ public static async Task OpenAIHandlerAsync(ITranslator service, string content, // 获取响应流 using var responseStream = await response.Content.ReadAsStreamAsync(token); using var reader = new System.IO.StreamReader(responseStream); + if (!response.IsSuccessStatusCode) + throw new Exception(response.ReasonPhrase); // 逐行读取并输出结果 while (!reader.EndOfStream || token.IsCancellationRequested) { var line = await reader.ReadLineAsync(token); - if (string.IsNullOrEmpty(line?.Trim())) continue; + if (string.IsNullOrEmpty(line?.Trim())) + continue; var preprocessString = line.Replace("data:", "").Trim(); // 结束标记 - if (preprocessString.Equals("[DONE]")) break; + if (preprocessString.Equals("[DONE]")) + break; // 解析JSON数据 var parsedData = JsonConvert.DeserializeObject(preprocessString); - if (parsedData is null) continue; + if (parsedData is null) + continue; // 提取content的值 var contentValue = parsedData["choices"]?.FirstOrDefault()?["delta"]?["content"]?.ToString(); - if (string.IsNullOrEmpty(contentValue)) continue; + if (string.IsNullOrEmpty(contentValue)) + continue; // 输出 - service.Data += contentValue; + lock (service) + { + service.Data += contentValue; + } + } + + if (string.IsNullOrEmpty(service.Data?.ToString())) + service.Data = "未获取到内容"; + } + catch (Exception ex) + { + service.Data = ex.Message; + } + } + + /// + /// Gemini + /// + /// + /// + /// + /// + /// + /// + public static async Task GeminiHandlerAsync(ITranslator service, string content, string source, string target, CancellationToken token) + { + try + { + if (string.IsNullOrEmpty(service.Url) || string.IsNullOrEmpty(service.AppKey)) + throw new Exception("请先完善配置"); + + UriBuilder uriBuilder = new(service.Url); + + if (!uriBuilder.Path.EndsWith("/v1beta/models/gemini-pro:streamGenerateContent")) + { + uriBuilder.Path = uriBuilder.Path.TrimEnd('/') + "/v1beta/models/gemini-pro:streamGenerateContent"; + } + + uriBuilder.Query = $"key={service.AppKey}"; + + // 构建请求数据 + var reqData = new { contents = new[] { new { parts = new[] { new { text = $"Translate the following text to {target}: {content}" } } } } }; + + // 为了流式输出与MVVM还是放这里吧 + var jsonData = JsonConvert.SerializeObject(reqData); + + // 构建请求 + var client = new HttpClient(new SocketsHttpHandler()); + var req = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = uriBuilder.Uri, + Content = new StringContent(jsonData, Encoding.UTF8, "application/json") + }; + + // 发送请求 + using var response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, token); + // 获取响应流 + using var responseStream = await response.Content.ReadAsStreamAsync(token); + using var reader = new System.IO.StreamReader(responseStream); + if (!response.IsSuccessStatusCode) + throw new Exception(response.ReasonPhrase); + // 逐行读取并输出结果 + while (!reader.EndOfStream) + { + string line = await reader.ReadLineAsync(token) ?? ""; + line = line.Trim(); + if (line.StartsWith("\"text\":")) + { + service.Data += line.Replace("\"text\": ", "").Replace("\"", ""); + } } + + if (string.IsNullOrEmpty(service.Data?.ToString())) + service.Data = "未获取到内容"; } catch (Exception ex) { @@ -178,4 +259,4 @@ public static async Task OpenAIHandlerAsync(ITranslator service, string content, } } } -} \ No newline at end of file +} diff --git a/STranslate/Views/Preference/Service/TextGeminiServicesPage.xaml b/STranslate/Views/Preference/Service/TextGeminiServicesPage.xaml new file mode 100644 index 00000000..9aecc3cc --- /dev/null +++ b/STranslate/Views/Preference/Service/TextGeminiServicesPage.xaml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +