fix: 适配翻译新api

main
zggsong 3 weeks ago
parent 14faaa592b
commit 44ee45df5b

@ -1,7 +1,9 @@
using System.IO.Compression; using System.IO.Compression;
using System.Net;
using System.Text; using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace STranslateDLL; namespace STranslateDLL;
@ -10,7 +12,20 @@ public static class LocalMode
{ {
private static long _nextId; private static long _nextId;
private static bool _hasInit = false; private static bool _hasInit;
private static readonly Dictionary<bool, string> TextTypeDic = new()
{
{ true, "richtext" },
{ false, "plaintext" }
};
private static JsonSerializerOptions GetOptions =>
new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
};
private static void Initial() private static void Initial()
{ {
@ -23,39 +38,122 @@ public static class LocalMode
public static async Task<string> ExecuteAsync( public static async Task<string> ExecuteAsync(
string content, string content,
string? sourceLang = null, string sourceLang = "auto",
string targetLang = "ZH", string targetLang = "ZH",
CancellationToken? token = null CancellationToken? token = null
) )
{ {
if (!_hasInit) if (!_hasInit) Initial();
var getToken = token ?? CancellationToken.None;
var splitResult = await SplitTextAsync(content, getToken);
//TODO: Error handling
var splitObj = JsonSerializer.Deserialize<JsonObject>(splitResult);
if (sourceLang.Equals("auto", StringComparison.CurrentCultureIgnoreCase) || sourceLang.Equals(""))
{ {
Initial(); sourceLang = splitObj?["result"]?["lang"]?["detected"]?.ToString()?.ToUpper() ?? "auto";
} }
var getToken = token ?? CancellationToken.None;
var timeSpan = GenerateTimestamp(content); var jobs = new List<Job>();
var chunks = splitObj?["result"]?["texts"]?[0]?["chunks"];
if (chunks is JsonArray chunkArray)
{
for (var i = 0; i < chunkArray.Count; i++)
{
var sentence = chunkArray[i]?["sentences"]?[0];
var contextBefore = Array.Empty<string>();
var contextAfter = Array.Empty<string>();
if (i > 0)
{
contextBefore = [chunkArray[i - 1]?["sentences"]?[0]?["text"]?.ToString() ?? ""];
}
if (i < chunkArray.Count - 1)
{
contextAfter = [chunkArray[i + 1]?["sentences"]?[0]?["text"]?.ToString() ?? ""];
}
var job = new Job
{
Kind = "default",
PreferredNumBeams = 4,
RawEnContextBefore = contextBefore,
RawEnContextAfter = contextAfter,
Sentences =
[
new Sentence
{
Prefix = sentence?["prefix"]?.ToString() ?? "",
Text = sentence?["text"]?.ToString() ?? "",
Id = i + 1,
}
]
};
jobs.Add(job);
}
}
var hasRegionalVariant = false;
var targetLangCode = targetLang;
var targetLangParts = targetLang.Split("-");
if (targetLangParts.Length > 1)
{
targetLangCode = targetLangParts[0];
hasRegionalVariant = true;
}
var id = CreateId(); var id = CreateId();
var reqStr = GenerateRequestStr(content, sourceLang, targetLang, timeSpan, id); var reqData = new DeepLRequest
{
Jsonrpc = "2.0",
Method = "LMT_handle_jobs",
Id = id,
Params = new ReqParams
{
CommonJobParams = new ReqParamsCommonJobParams
{
Mode = "translate",
RegionalVariant = hasRegionalVariant ? targetLang : null
},
Lang = new ReqParamsLang
{
// SourceLangUserSelected = sourceLang,
SourceLangComputed = sourceLang.ToUpper(),
TargetLang = targetLangCode.ToUpper()
},
Jobs = jobs.ToArray(),
Priority = 1,
Timestamp = GenerateTimestamp(content)
}
};
var json = JsonSerializer.Serialize(reqData, GetOptions);
json = AdjustJsonContent(json, id);
using var client = new HttpClient(); using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://www2.deepl.com/jsonrpc") var request = new HttpRequestMessage(HttpMethod.Post,
"https://www2.deepl.com/jsonrpc?client=chrome-extension,1.28.0&method=LMT_handle_jobs")
{ {
Content = new StringContent(reqStr, Encoding.UTF8, "application/json") Content = new StringContent(json, Encoding.UTF8, "application/json")
}; };
// Add headers to the request
request.Headers.Add("User-Agent", "DeepL-iOS/2.4.0 iOS 15.7.1 (iPhone14,2)");
request.Headers.Add("Accept", "*/*"); request.Headers.Add("Accept", "*/*");
request.Headers.Add("x-app-os-name", "IOS"); request.Headers.Add("Accept-Language", "en-US,en;q=0.9,zh-CN;q=0.8,zh-TW;q=0.7,zh-HK;q=0.6,zh;q=0.5");
request.Headers.Add("x-app-os-version", "15.7.1"); request.Headers.Add("Authorization", "None");
request.Headers.Add("Accept-Language", "en-US,en;q=0.9"); request.Headers.Add("Cache-Control", "no-cache");
request.Headers.Add("Accept-Encoding", "gzip,deflate,br"); // request.Headers.Add("Content-Type", "application/json");
//request.Headers.Add("Content-Type", "application/json"); request.Headers.Add("DNT", "1");
request.Headers.Add("x-app-device", "iPhone14,2"); request.Headers.Add("Origin", "chrome-extension://cofdbpoegempjloogbagkncekinflcnj");
request.Headers.Add("x-app-build", "353"); request.Headers.Add("Pragma", "no-cache");
request.Headers.Add("x-app-version", "2.4"); request.Headers.Add("Priority", "u=1, i");
request.Headers.Add("Referer", "https://www.deepl.com/"); request.Headers.Add("Referer", "https://www.deepl.com/");
request.Headers.Add("Connection", "keep-alive"); request.Headers.Add("Sec-Fetch-Dest", "empty");
request.Headers.Add("Sec-Fetch-Mode", "cors");
request.Headers.Add("Sec-Fetch-Site", "none");
request.Headers.Add("Sec-GPC", "1");
request.Headers.Add("User-Agent",
"DeepLBrowserExtension/1.28.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36");
var resp = await client.SendAsync(request, getToken); var resp = await client.SendAsync(request, getToken);
//resp.EnsureSuccessStatusCode(); //resp.EnsureSuccessStatusCode();
@ -76,28 +174,124 @@ public static class LocalMode
{ {
responseBody = await resp.Content.ReadAsStringAsync(getToken); responseBody = await resp.Content.ReadAsStringAsync(getToken);
} }
var jNode = JsonNode.Parse(responseBody);
var data = jNode?["result"]?["translations"]?[0]?["beams"]?[0]?["sentences"]?[0]?["text"]?.ToString();
// data = UnicodeToString(data);
var deeplResp = JsonSerializer.Deserialize<DeepLResponse>(responseBody); var errorMsg = jNode?["error"]?["message"]?.ToString();
var detailsMsg = jNode?["error"]?["data"]?["what"]?.ToString();
var error = $"Error: {errorMsg}\nDetails: {detailsMsg}";
var response = new Response { Code = resp.StatusCode.GetHashCode() }; var response = new Response
{
string defaultMessage = Code = resp.StatusCode.GetHashCode(),
resp.StatusCode == System.Net.HttpStatusCode.OK Data = data ?? error
? "Empty Result" };
: "Empty Error Message";
response.Data =
deeplResp?.Result?.Texts?.FirstOrDefault()?.Text
?? deeplResp?.Error?.Message
?? defaultMessage;
return JsonSerializer.Serialize(response, GetOptions); return JsonSerializer.Serialize(response, GetOptions);
} }
private static async Task<string> SplitTextAsync(string text, CancellationToken? token)
{
var id = CreateId();
var getToken = token ?? CancellationToken.None;
var requestData = new DeepLRequest
{
Jsonrpc = "2.0",
Method = "LMT_split_text",
Id = id,
Params = new ReqParams
{
CommonJobParams = new ReqParamsCommonJobParams
{
Mode = "translate"
},
Lang = new ReqParamsLang
{
SourceLangUserSelected = "auto"
},
Texts = [text],
TextType = TextTypeDic[ /*tagHandling*/false || IsRichText(text)]
}
};
var json = JsonSerializer.Serialize(requestData, GetOptions);
json = AdjustJsonContent(json, id);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post,
"https://www2.deepl.com/jsonrpc?client=chrome-extension,1.28.0&method=LMT_split_text")
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
// Add headers to the request
request.Headers.Add("Accept", "*/*");
request.Headers.Add("Accept-Language", "en-US,en;q=0.9,zh-CN;q=0.8,zh-TW;q=0.7,zh-HK;q=0.6,zh;q=0.5");
request.Headers.Add("Authorization", "None");
request.Headers.Add("Cache-Control", "no-cache");
// request.Headers.Add("Content-Type", "application/json");
request.Headers.Add("DNT", "1");
request.Headers.Add("Origin", "chrome-extension://cofdbpoegempjloogbagkncekinflcnj");
request.Headers.Add("Pragma", "no-cache");
request.Headers.Add("Priority", "u=1, i");
request.Headers.Add("Referer", "https://www.deepl.com/");
request.Headers.Add("Sec-Fetch-Dest", "empty");
request.Headers.Add("Sec-Fetch-Mode", "cors");
request.Headers.Add("Sec-Fetch-Site", "none");
request.Headers.Add("Sec-GPC", "1");
request.Headers.Add("User-Agent",
"DeepLBrowserExtension/1.28.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36");
var resp = await client.SendAsync(request, getToken);
string responseBody;
if (resp.Content.Headers.ContentEncoding.Contains("br"))
{
await using var responseStream = await resp.Content.ReadAsStreamAsync(getToken);
await using var decompressionStream = new BrotliStream(
responseStream,
CompressionMode.Decompress
);
using var streamReader = new StreamReader(decompressionStream);
responseBody = await streamReader.ReadToEndAsync(getToken);
}
else
{
responseBody = await resp.Content.ReadAsStringAsync(getToken);
}
return responseBody;
}
private static string? UnicodeToString(string? srcText)
{
if (srcText == null) return default;
var dst = "";
var src = srcText;
var len = srcText.Length / 6;
for (var i = 0; i <= len - 1; i++)
{
var str = "";
str = src[..6][2..];
src = src[6..];
var bytes = new byte[2];
bytes[1] = byte.Parse(int.Parse(str[..2], System.Globalization.NumberStyles.HexNumber).ToString());
bytes[0] = byte.Parse(int.Parse(str.Substring(2, 2), System.Globalization.NumberStyles.HexNumber).ToString());
dst += Encoding.Unicode.GetString(bytes);
}
return dst;
}
private static bool IsRichText(string text)
{
return text.Contains('<') && text.Contains('>');
}
private static long GenerateTimestamp(string texts) private static long GenerateTimestamp(string texts)
{ {
long iCount = texts.Split('i').Length - 1; long iCount = texts.Split('i').Length - 1;
var ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
return iCount != 0 ? ts - ts % (iCount + 1) + (iCount + 1) : ts; return iCount != 0 ? ts - ts % (iCount + 1) + iCount + 1 : ts;
} }
private static long CreateId() private static long CreateId()
@ -109,62 +303,11 @@ public static class LocalMode
{ {
string method; string method;
if ((id + 3) % 13 == 0 || (id + 5) % 29 == 0) if ((id + 3) % 13 == 0 || (id + 5) % 29 == 0)
{
method = "\"method\" : \""; method = "\"method\" : \"";
}
else else
{
method = "\"method\": \""; method = "\"method\": \"";
}
return json.Replace("\"method\":\"", method); return json.Replace("\"method\":\"", method);
} }
private static JsonSerializerOptions GetOptions =>
new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private static string GenerateRequestStr(
string text,
string? sourceLang,
string targetLang,
long timeSpan,
long id
)
{
var req = new DeepLRequest
{
Jsonrpc = "2.0",
Method = "LMT_handle_texts",
Params = new ReqParams
{
Texts = [new ReqParamsTexts { Text = text, RequestAlternatives = 0 }],
Splitting = "newlines",
Lang = new ReqParamsLang
{
SourceLangUserSelected = sourceLang,
TargetLang = targetLang
},
Timestamp = timeSpan,
CommonJobParams = new ReqParamsCommonJobParams
{
WasSpoken = false,
TranscribeAS = ""
}
},
Id = id
};
var json = JsonSerializer.Serialize(req, GetOptions);
var count = json.Length > 300 ? 0 : 3;
req.Params.Texts.First().RequestAlternatives = count;
json = JsonSerializer.Serialize(req, GetOptions);
json = AdjustJsonContent(json, id);
return json;
}
} }
public class Response public class Response
@ -173,46 +316,34 @@ public class Response
public string Data { get; set; } = ""; public string Data { get; set; } = "";
} }
#region Request
public class DeepLRequest public class DeepLRequest
{ {
[JsonPropertyName("jsonrpc")] [JsonPropertyName("jsonrpc")] public string Jsonrpc { get; set; } = "";
public string Jsonrpc { get; set; } = "";
[JsonPropertyName("method")] [JsonPropertyName("method")] public string Method { get; set; } = "";
public string Method { get; set; } = "";
[JsonPropertyName("params")] [JsonPropertyName("params")] public ReqParams? Params { get; set; }
public ReqParams? Params { get; set; }
[JsonPropertyName("id")] [JsonPropertyName("id")] public long Id { get; set; }
public long Id { get; set; }
} }
public class ReqParams public class ReqParams
{ {
[JsonPropertyName("texts")] [JsonPropertyName("commonJobParams")] public ReqParamsCommonJobParams? CommonJobParams { get; set; }
public ReqParamsTexts[]? Texts { get; set; }
[JsonPropertyName("splitting")] [JsonPropertyName("lang")] public ReqParamsLang? Lang { get; set; }
public string Splitting { get; set; } = "";
[JsonPropertyName("lang")] [JsonPropertyName("texts")] public string[]? Texts { get; set; }
public ReqParamsLang? Lang { get; set; }
[JsonPropertyName("timestamp")] [JsonPropertyName("textType")] public string? TextType { get; set; }
public long Timestamp { get; set; }
[JsonPropertyName("commonJobParams")] [JsonPropertyName("jobs")] public Job[]? Jobs { get; set; }
public ReqParamsCommonJobParams? CommonJobParams { get; set; }
}
public class ReqParamsTexts [JsonPropertyName("priority")] public int Priority { get; set; }
{
[JsonPropertyName("text")]
public string Text { get; set; } = "";
[JsonPropertyName("requestAlternatives")] [JsonPropertyName("timestamp")] public long Timestamp { get; set; }
public int RequestAlternatives { get; set; }
} }
public class ReqParamsLang public class ReqParamsLang
@ -220,94 +351,50 @@ public class ReqParamsLang
[JsonPropertyName("source_lang_user_selected")] [JsonPropertyName("source_lang_user_selected")]
public string? SourceLangUserSelected { get; set; } public string? SourceLangUserSelected { get; set; }
[JsonPropertyName("target_lang")] [JsonPropertyName("source_lang_computed")]
public string TargetLang { get; set; } = ""; public string? SourceLangComputed { get; set; }
[JsonPropertyName("target_lang")] public string TargetLang { get; set; } = "";
} }
public class ReqParamsCommonJobParams public class ReqParamsCommonJobParams
{ {
[JsonPropertyName("wasSpoken")] [JsonPropertyName("mode")] public string Mode { get; set; } = "";
public bool WasSpoken { get; set; }
[JsonPropertyName("transcribe_as")] [JsonPropertyName("regionalVariant")] public string? RegionalVariant { get; set; }
public string? TranscribeAS { get; set; }
} }
public class DeepLResponse public class Job
{ {
[JsonPropertyName("jsonrpc")] [JsonPropertyName("kind")] public string Kind { get; set; } = "";
public string Jsonrpc { get; set; } = "";
[JsonPropertyName("preferred_num_beams")]
public int PreferredNumBeams { get; set; }
[JsonPropertyName("id")] [JsonPropertyName("raw_en_context_before")]
public long Id { get; set; } public string[]? RawEnContextBefore { get; set; }
[JsonPropertyName("result")] [JsonPropertyName("raw_en_context_after")]
public RespResult? Result { get; set; } public string[]? RawEnContextAfter { get; set; }
[JsonPropertyName("error")] [JsonPropertyName("sentences")] public Sentence[]? Sentences { get; set; }
public RespError? Error { get; set; }
} }
public class RespResult public class Sentence
{ {
[JsonPropertyName("texts")] [JsonPropertyName("id")] public int Id { get; set; }
public RespResultText[]? Texts { get; set; }
[JsonPropertyName("lang")] [JsonPropertyName("prefix")] public string Prefix { get; set; } = "";
public string Lang { get; set; } = "";
[JsonPropertyName("lang_is_confident")] [JsonPropertyName("text")] public string Text { get; set; } = "";
public bool LangIsConfident { get; set; }
[JsonPropertyName("detectedLanguages")]
public RespResultDetectedLanguages? DetectedLanguages { get; set; }
} }
public class RespResultText #endregion
{
[JsonPropertyName("alternatives")]
public object[]? Alternatives { get; set; }
[JsonPropertyName("text")]
public string Text { get; set; } = "";
}
public class RespResultDetectedLanguages
{
public float EN { get; set; }
public float DE { get; set; }
public float FR { get; set; }
public float ES { get; set; }
public float PT { get; set; }
public float IT { get; set; }
public float NL { get; set; }
public float PL { get; set; }
public float RU { get; set; }
public float ZH { get; set; }
public float JA { get; set; }
public float BG { get; set; }
public float CS { get; set; }
public float DA { get; set; }
public float EL { get; set; }
public float ET { get; set; }
public float FI { get; set; }
public float HU { get; set; }
public float LT { get; set; }
public float LV { get; set; }
public float RO { get; set; }
public float SK { get; set; }
public float SL { get; set; }
public float SV { get; set; }
public float TR { get; set; }
public float ID { get; set; }
public float Unsupported { get; set; }
}
public class RespError public class RespError
{ {
[JsonPropertyName("code")] [JsonPropertyName("code")] public int Code { get; set; }
public int Code { get; set; }
[JsonPropertyName("message")] [JsonPropertyName("message")] public string Message { get; set; } = "";
public string Message { get; set; } = ""; }
}

@ -1,9 +1,11 @@
using System.Diagnostics; using System.Diagnostics;
using STranslateDLL;
using Xunit; using Xunit;
using Xunit.Abstractions;
namespace STranslateDLL.Tests; namespace STranslateDLLTests;
public class LocalModeTests public class LocalModeTests(ITestOutputHelper testOutputHelper)
{ {
[Fact()] [Fact()]
public async Task ExecuteAsyncTestAsync() public async Task ExecuteAsyncTestAsync()
@ -11,8 +13,8 @@ public class LocalModeTests
try try
{ {
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
var ret = await LocalMode.ExecuteAsync("Hello World", "auto", "ZH", cts.Token); var ret = await LocalMode.ExecuteAsync("Hello World", "EN", "ZH", cts.Token);
Debug.WriteLine(ret); testOutputHelper.WriteLine(ret);
} }
catch (Exception ex) catch (Exception ex)
{ {

Loading…
Cancel
Save