Compare commits

...

3 Commits

Author SHA1 Message Date
Vincent Yang
72beefbebe
chore: update toolchain to Go 1.24.5 in go.mod 2025-07-13 23:24:51 +08:00
Vincent Yang
2b0e1e3cbb
fix: 503 unable to request. close #196 close #194 close #193 close #191 close #179 close #178 2025-07-13 23:14:24 +08:00
Vincent Yang
8a605887ff
refactor: update translation request handling to use new LMT_handle_texts method and improve response processing
- Renamed makeRequest to makeRequestWithBody for clarity.
- Introduced new TextItem and TextResponse types for better structure in translation requests and responses.
- Updated translation logic to handle multiple texts and alternatives.
- Enhanced error handling for blocked requests and translation failures.
- Adjusted timestamp and random number generation for improved request uniqueness.
2025-07-13 23:06:47 +08:00
4 changed files with 123 additions and 192 deletions

2
go.mod
View File

@ -2,7 +2,7 @@ module github.com/OwO-Network/DeepLX
go 1.24.0 go 1.24.0
toolchain go1.24.2 toolchain go1.24.5
require ( require (
github.com/abadojack/whatlanggo v1.0.1 github.com/abadojack/whatlanggo v1.0.1

View File

@ -2,7 +2,7 @@
* @Author: Vincent Young * @Author: Vincent Young
* @Date: 2024-09-16 11:59:24 * @Date: 2024-09-16 11:59:24
* @LastEditors: Vincent Yang * @LastEditors: Vincent Yang
* @LastEditTime: 2025-04-08 14:26:33 * @LastEditTime: 2025-07-13 23:09:49
* @FilePath: /DeepLX/translate/translate.go * @FilePath: /DeepLX/translate/translate.go
* @Telegram: https://t.me/missuo * @Telegram: https://t.me/missuo
* @GitHub: https://github.com/missuo * @GitHub: https://github.com/missuo
@ -29,28 +29,16 @@ import (
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
// makeRequest makes an HTTP request to DeepL API // makeRequestWithBody makes an HTTP request with pre-formatted body using minimal headers
func makeRequest(postData *PostData, proxyURL string, dlSession string) (gjson.Result, error) { func makeRequestWithBody(postStr string, proxyURL string, dlSession string) (gjson.Result, error) {
urlFull := "https://www2.deepl.com/jsonrpc" urlFull := "https://www2.deepl.com/jsonrpc"
postStr := formatPostString(postData)
// Create a new req client // Create a new req client
client := req.C().SetTLSFingerprintRandomized() client := req.C().SetTLSFingerprintRandomized()
// Set headers // Set minimal headers like TypeScript version
headers := http.Header{ headers := http.Header{
"Content-Type": []string{"application/json"}, "Content-Type": []string{"application/json"},
"User-Agent": []string{"DeepL/1627620 CFNetwork/3826.500.62.2.1 Darwin/24.4.0"},
"Accept": []string{"*/*"},
"X-App-Os-Name": []string{"iOS"},
"X-App-Os-Version": []string{"18.4.0"},
"Accept-Language": []string{"en-US,en;q=0.9"},
"Accept-Encoding": []string{"gzip, deflate, br"}, // Keep this!
"X-App-Device": []string{"iPhone16,2"},
"Referer": []string{"https://www.deepl.com/"},
"X-Product": []string{"translator"},
"X-App-Build": []string{"1627620"},
"X-App-Version": []string{"25.1"},
} }
if dlSession != "" { if dlSession != "" {
@ -77,17 +65,22 @@ func makeRequest(postData *PostData, proxyURL string, dlSession string) (gjson.R
return gjson.Result{}, err return gjson.Result{}, err
} }
// Check for blocked status like TypeScript version
if resp.StatusCode == 429 {
return gjson.Result{}, fmt.Errorf("too many requests, your IP has been blocked by DeepL temporarily, please don't request it frequently in a short time")
}
var bodyReader io.Reader var bodyReader io.Reader
contentEncoding := resp.Header.Get("Content-Encoding") contentEncoding := resp.Header.Get("Content-Encoding")
switch contentEncoding { switch contentEncoding {
case "br": case "br":
bodyReader = brotli.NewReader(resp.Body) bodyReader = brotli.NewReader(resp.Body)
case "gzip": case "gzip":
bodyReader, err = gzip.NewReader(resp.Body) // Use gzip.NewReader bodyReader, err = gzip.NewReader(resp.Body)
if err != nil { if err != nil {
return gjson.Result{}, fmt.Errorf("failed to create gzip reader: %w", err) return gjson.Result{}, fmt.Errorf("failed to create gzip reader: %w", err)
} }
case "deflate": // Less common, but good to handle case "deflate":
bodyReader = flate.NewReader(resp.Body) bodyReader = flate.NewReader(resp.Body)
default: default:
bodyReader = resp.Body bodyReader = resp.Body
@ -109,182 +102,86 @@ func TranslateByDeepLX(sourceLang, targetLang, text string, tagHandling string,
}, nil }, nil
} }
if tagHandling == "" { // Get detected language if source language is auto
tagHandling = "plaintext" if sourceLang == "auto" || sourceLang == "" {
sourceLang = strings.ToUpper(whatlanggo.DetectLang(text).Iso6391())
} }
// Split text by newlines and store them for later reconstruction // Prepare translation request using new LMT_handle_texts method
textParts := strings.Split(text, "\n") id := getRandomNumber()
var translatedParts []string iCount := getICount(text)
var allAlternatives [][]string // Store alternatives for each part timestamp := getTimeStamp(iCount)
for _, part := range textParts { postData := &PostData{
if strings.TrimSpace(part) == "" { Jsonrpc: "2.0",
translatedParts = append(translatedParts, "") Method: "LMT_handle_texts",
allAlternatives = append(allAlternatives, []string{""}) ID: id,
continue Params: Params{
} Splitting: "newlines",
Lang: Lang{
// Get detected language if source language is auto SourceLangUserSelected: sourceLang,
if sourceLang == "auto" || sourceLang == "" { TargetLang: targetLang,
sourceLang = strings.ToUpper(whatlanggo.DetectLang(part).Iso6391())
}
// Prepare jobs from split result
var jobs []Job
jobs = append(jobs, Job{
Kind: "default",
PreferredNumBeams: 4,
RawEnContextBefore: []string{},
RawEnContextAfter: []string{},
Sentences: []Sentence{{
Prefix: "",
Text: text,
ID: 0,
}},
})
hasRegionalVariant := false
targetLangCode := targetLang
targetLangParts := strings.Split(targetLang, "-")
if len(targetLangParts) > 1 {
targetLangCode = targetLangParts[0]
hasRegionalVariant = true
}
// Prepare translation request
id := getRandomNumber()
postData := &PostData{
Jsonrpc: "2.0",
Method: "LMT_handle_jobs",
ID: id,
Params: Params{
CommonJobParams: CommonJobParams{
Mode: "translate",
Formality: "undefined",
TranscribeAs: "romanize",
AdvancedMode: false,
TextType: tagHandling,
WasSpoken: false,
},
Lang: Lang{
SourceLangUserSelected: "auto",
TargetLang: strings.ToUpper(targetLangCode),
SourceLangComputed: strings.ToUpper(sourceLang),
},
Jobs: jobs,
Timestamp: getTimeStamp(getICount(part)),
}, },
} Texts: []TextItem{{
Text: text,
if hasRegionalVariant { RequestAlternatives: 3,
postData = &PostData{ }},
Jsonrpc: "2.0", Timestamp: timestamp,
Method: "LMT_handle_jobs", },
ID: id,
Params: Params{
CommonJobParams: CommonJobParams{
Mode: "translate",
Formality: "undefined",
TranscribeAs: "romanize",
AdvancedMode: false,
TextType: tagHandling,
WasSpoken: false,
RegionalVariant: targetLang,
},
Lang: Lang{
SourceLangUserSelected: "auto",
TargetLang: strings.ToUpper(targetLangCode),
SourceLangComputed: strings.ToUpper(sourceLang),
},
Jobs: jobs,
Timestamp: getTimeStamp(getICount(part)),
},
}
}
// Make translation request
result, err := makeRequest(postData, proxyURL, dlSession)
if err != nil {
return DeepLXTranslationResult{
Code: http.StatusServiceUnavailable,
Message: err.Error(),
}, nil
}
// Process translation results
var partTranslation string
var partAlternatives []string
translations := result.Get("result.translations").Array()
if len(translations) > 0 {
// Process main translation
for _, translation := range translations {
partTranslation += translation.Get("beams.0.sentences.0.text").String() + " "
}
partTranslation = strings.TrimSpace(partTranslation)
// Process alternatives
numBeams := len(translations[0].Get("beams").Array())
for i := 1; i < numBeams; i++ { // Start from 1 since 0 is the main translation
var altText string
for _, translation := range translations {
beams := translation.Get("beams").Array()
if i < len(beams) {
altText += beams[i].Get("sentences.0.text").String() + " "
}
}
if altText != "" {
partAlternatives = append(partAlternatives, strings.TrimSpace(altText))
}
}
}
if partTranslation == "" {
return DeepLXTranslationResult{
Code: http.StatusServiceUnavailable,
Message: "Translation failed",
}, nil
}
translatedParts = append(translatedParts, partTranslation)
allAlternatives = append(allAlternatives, partAlternatives)
} }
// Join all translated parts with newlines // Format and apply body manipulation method like TypeScript
translatedText := strings.Join(translatedParts, "\n") postStr := formatPostString(postData)
postStr = handlerBodyMethod(id, postStr)
// Combine alternatives with proper newline handling // Make translation request
var combinedAlternatives []string result, err := makeRequestWithBody(postStr, proxyURL, dlSession)
maxAlts := 0 if err != nil {
for _, alts := range allAlternatives { return DeepLXTranslationResult{
if len(alts) > maxAlts { Code: http.StatusServiceUnavailable,
maxAlts = len(alts) Message: err.Error(),
}, nil
}
// Process translation results using new format
textsArray := result.Get("result.texts").Array()
if len(textsArray) == 0 {
return DeepLXTranslationResult{
Code: http.StatusServiceUnavailable,
Message: "Translation failed",
}, nil
}
// Get main translation
mainText := textsArray[0].Get("text").String()
if mainText == "" {
return DeepLXTranslationResult{
Code: http.StatusServiceUnavailable,
Message: "Translation failed",
}, nil
}
// Get alternatives
var alternatives []string
alternativesArray := textsArray[0].Get("alternatives").Array()
for _, alt := range alternativesArray {
altText := alt.Get("text").String()
if altText != "" {
alternatives = append(alternatives, altText)
} }
} }
// Create combined alternatives preserving line structure // Get detected source language from response
for i := 0; i < maxAlts; i++ { detectedLang := result.Get("result.lang").String()
var altParts []string if detectedLang != "" {
for j, alts := range allAlternatives { sourceLang = detectedLang
if i < len(alts) {
altParts = append(altParts, alts[i])
} else if len(translatedParts[j]) == 0 {
altParts = append(altParts, "") // Keep empty lines
} else {
altParts = append(altParts, translatedParts[j]) // Use main translation if no alternative
}
}
combinedAlternatives = append(combinedAlternatives, strings.Join(altParts, "\n"))
} }
return DeepLXTranslationResult{ return DeepLXTranslationResult{
Code: http.StatusOK, Code: http.StatusOK,
ID: getRandomNumber(), // Using new ID for the complete translation ID: id,
Data: translatedText, Data: mainText,
Alternatives: combinedAlternatives, Alternatives: alternatives,
SourceLang: sourceLang, SourceLang: sourceLang,
TargetLang: targetLang, TargetLang: targetLang,
Method: map[bool]string{true: "Pro", false: "Free"}[dlSession != ""], Method: map[bool]string{true: "Pro", false: "Free"}[dlSession != ""],

View File

@ -46,8 +46,22 @@ type Job struct {
Sentences []Sentence `json:"sentences"` Sentences []Sentence `json:"sentences"`
} }
// TextItem represents a text item for translation
type TextItem struct {
Text string `json:"text"`
RequestAlternatives int `json:"requestAlternatives"`
}
// Params represents parameters for translation requests // Params represents parameters for translation requests
type Params struct { type Params struct {
Splitting string `json:"splitting"`
Lang Lang `json:"lang"`
Texts []TextItem `json:"texts"`
Timestamp int64 `json:"timestamp"`
}
// LegacyParams represents the old parameters structure for jobs (kept for compatibility)
type LegacyParams struct {
CommonJobParams CommonJobParams `json:"commonJobParams"` CommonJobParams CommonJobParams `json:"commonJobParams"`
Lang Lang `json:"lang"` Lang Lang `json:"lang"`
Jobs []Job `json:"jobs"` Jobs []Job `json:"jobs"`
@ -62,8 +76,26 @@ type PostData struct {
Params Params `json:"params"` Params Params `json:"params"`
} }
// TranslationResponse represents the response from translation // TextResponse represents a single text response
type TextResponse struct {
Text string `json:"text"`
Alternatives []struct {
Text string `json:"text"`
} `json:"alternatives"`
}
// TranslationResponse represents the response from LMT_handle_texts
type TranslationResponse struct { type TranslationResponse struct {
Jsonrpc string `json:"jsonrpc"`
ID int64 `json:"id"`
Result struct {
Lang string `json:"lang"`
Texts []TextResponse `json:"texts"`
} `json:"result"`
}
// LegacyTranslationResponse represents the old response format (kept for compatibility)
type LegacyTranslationResponse struct {
Jsonrpc string `json:"jsonrpc"` Jsonrpc string `json:"jsonrpc"`
ID int64 `json:"id"` ID int64 `json:"id"`
Result struct { Result struct {

View File

@ -28,7 +28,7 @@ func getICount(translateText string) int64 {
func getRandomNumber() int64 { func getRandomNumber() int64 {
src := rand.NewSource(time.Now().UnixNano()) src := rand.NewSource(time.Now().UnixNano())
rng := rand.New(src) rng := rand.New(src)
num := rng.Int63n(99999) + 8300000 num := rng.Int63n(99999) + 100000
return num * 1000 return num * 1000
} }
@ -37,7 +37,7 @@ func getTimeStamp(iCount int64) int64 {
ts := time.Now().UnixMilli() ts := time.Now().UnixMilli()
if iCount != 0 { if iCount != 0 {
iCount = iCount + 1 iCount = iCount + 1
return ts - ts%iCount + iCount return ts - (ts % iCount) + iCount
} }
return ts return ts
} }
@ -46,12 +46,14 @@ func getTimeStamp(iCount int64) int64 {
func formatPostString(postData *PostData) string { func formatPostString(postData *PostData) string {
postBytes, _ := json.Marshal(postData) postBytes, _ := json.Marshal(postData)
postStr := string(postBytes) postStr := string(postBytes)
if (postData.ID+5)%29 == 0 || (postData.ID+3)%13 == 0 {
postStr = strings.Replace(postStr, `"method":"`, `"method" : "`, 1)
} else {
postStr = strings.Replace(postStr, `"method":"`, `"method": "`, 1)
}
return postStr return postStr
} }
// handlerBodyMethod manipulates the request body based on random number calculation
func handlerBodyMethod(random int64, body string) string {
calc := (random+5)%29 == 0 || (random+3)%13 == 0
if calc {
return strings.Replace(body, `"method":"`, `"method" : "`, 1)
}
return strings.Replace(body, `"method":"`, `"method": "`, 1)
}