✨ Added context menu
This commit is contained in:
277
internal/common/translator/bing_translator.go
Normal file
277
internal/common/translator/bing_translator.go
Normal file
@@ -0,0 +1,277 @@
|
||||
// Package translator 提供文本翻译功能
|
||||
package translator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// BingTranslator Bing翻译器结构体
|
||||
type BingTranslator struct {
|
||||
BingHost string // Bing服务主机
|
||||
httpClient *http.Client // HTTP客户端
|
||||
Timeout time.Duration // 请求超时时间
|
||||
}
|
||||
|
||||
// 常量定义
|
||||
const (
|
||||
bingDefaultTimeout = 30 * time.Second
|
||||
defaultBingHost = "cn.bing.com" // 使用cn.bing.com作为默认域名
|
||||
)
|
||||
|
||||
// 错误定义
|
||||
var (
|
||||
ErrBingNetworkError = errors.New("bing translator network error")
|
||||
ErrBingParseError = errors.New("bing translator parse error")
|
||||
ErrBingTokenError = errors.New("failed to get bing translator token")
|
||||
)
|
||||
|
||||
// BingTranslationParams Bing翻译所需的参数
|
||||
type BingTranslationParams struct {
|
||||
Token string // token参数
|
||||
Key string // key参数
|
||||
IG string // IG参数
|
||||
}
|
||||
|
||||
// NewBingTranslator 创建一个新的Bing翻译器实例
|
||||
func NewBingTranslator() *BingTranslator {
|
||||
translator := &BingTranslator{
|
||||
BingHost: defaultBingHost,
|
||||
Timeout: bingDefaultTimeout,
|
||||
httpClient: &http.Client{Timeout: bingDefaultTimeout},
|
||||
}
|
||||
|
||||
return translator
|
||||
}
|
||||
|
||||
// SetTimeout 设置请求超时时间
|
||||
func (t *BingTranslator) SetTimeout(timeout time.Duration) {
|
||||
t.Timeout = timeout
|
||||
t.httpClient.Timeout = timeout
|
||||
}
|
||||
|
||||
// SetBingHost 设置Bing主机
|
||||
func (t *BingTranslator) SetBingHost(host string) {
|
||||
t.BingHost = host
|
||||
}
|
||||
|
||||
// Translate 使用标准语言标签进行文本翻译
|
||||
func (t *BingTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) {
|
||||
return t.translate(text, from.String(), to.String())
|
||||
}
|
||||
|
||||
// TranslateWithParams 使用简单字符串参数进行文本翻译
|
||||
func (t *BingTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) {
|
||||
return t.translate(text, params.From, params.To)
|
||||
}
|
||||
|
||||
// translate 执行实际翻译操作
|
||||
func (t *BingTranslator) translate(text, from, to string) (string, error) {
|
||||
// 获取翻译所需的参数
|
||||
params, err := t.ExtractBingTranslationParams()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to extract bing translation params: %w", err)
|
||||
}
|
||||
|
||||
// 执行翻译
|
||||
return t.GetBingTranslation(params.Token, params.Key, params.IG, text, from, to)
|
||||
}
|
||||
|
||||
// ExtractBingTranslationParams 提取Bing翻译所需的参数
|
||||
func (t *BingTranslator) ExtractBingTranslationParams() (*BingTranslationParams, error) {
|
||||
// 发送GET请求获取网页内容
|
||||
url := fmt.Sprintf("https://%s/translator?mkt=zh-CN", t.BingHost)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
|
||||
resp, err := t.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrBingNetworkError, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to access Bing translator page: status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 读取响应内容
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
pageContent := string(body)
|
||||
|
||||
// 模式1: 标准的params_AbusePreventionHelper数组
|
||||
keyPattern := regexp.MustCompile(`params_AbusePreventionHelper\s*=\s*\[([^\]]+)\]`)
|
||||
keyMatch := keyPattern.FindStringSubmatch(pageContent)
|
||||
|
||||
var key, token string
|
||||
|
||||
if len(keyMatch) >= 2 {
|
||||
// 提取并解析数组
|
||||
paramsStr := keyMatch[1]
|
||||
paramsList := strings.Split(paramsStr, ",")
|
||||
|
||||
if len(paramsList) >= 2 {
|
||||
// 清理引号
|
||||
key = strings.Trim(paramsList[0], `"' `)
|
||||
token = strings.Trim(paramsList[1], `"' `)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果标准模式失败,尝试备用模式
|
||||
if key == "" || token == "" {
|
||||
// 模式2: 查找_G.Token和_G.Key
|
||||
tokenPattern := regexp.MustCompile(`_G\.Token\s*=\s*["']([^"']+)["']`)
|
||||
tokenMatch := tokenPattern.FindStringSubmatch(pageContent)
|
||||
|
||||
keyPattern := regexp.MustCompile(`_G\.Key\s*=\s*["']?([^"',]+)["']?`)
|
||||
keyMatch := keyPattern.FindStringSubmatch(pageContent)
|
||||
|
||||
if len(tokenMatch) >= 2 && len(keyMatch) >= 2 {
|
||||
token = tokenMatch[1]
|
||||
key = keyMatch[1]
|
||||
}
|
||||
}
|
||||
|
||||
// 如果仍然失败,尝试JSON格式
|
||||
if key == "" || token == "" {
|
||||
jsonPattern := regexp.MustCompile(`"token"\s*:\s*"([^"]+)"\s*,\s*"key"\s*:\s*"?([^",]+)"?`)
|
||||
jsonMatch := jsonPattern.FindStringSubmatch(pageContent)
|
||||
|
||||
if len(jsonMatch) >= 3 {
|
||||
token = jsonMatch[1]
|
||||
key = jsonMatch[2]
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有模式都失败
|
||||
if key == "" || token == "" {
|
||||
return nil, fmt.Errorf("%w: unable to extract token and key", ErrBingTokenError)
|
||||
}
|
||||
|
||||
// 查找并提取 IG 参数,尝试多种格式
|
||||
var ig string
|
||||
|
||||
// 模式1: 标准IG格式
|
||||
igPattern := regexp.MustCompile(`IG["']?\s*:\s*["']([^"']+)["']`)
|
||||
igMatch := igPattern.FindStringSubmatch(pageContent)
|
||||
|
||||
if len(igMatch) >= 2 {
|
||||
ig = igMatch[1]
|
||||
} else {
|
||||
// 模式2: 备用IG格式
|
||||
igPattern = regexp.MustCompile(`"IG"\s*:\s*"([^"]+)"`)
|
||||
igMatch = igPattern.FindStringSubmatch(pageContent)
|
||||
|
||||
if len(igMatch) >= 2 {
|
||||
ig = igMatch[1]
|
||||
} else {
|
||||
// 模式3: _G.IG格式
|
||||
igPattern = regexp.MustCompile(`_G\.IG\s*=\s*["']([^"']+)["']`)
|
||||
igMatch = igPattern.FindStringSubmatch(pageContent)
|
||||
|
||||
if len(igMatch) >= 2 {
|
||||
ig = igMatch[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有IG提取模式都失败
|
||||
if ig == "" {
|
||||
return nil, fmt.Errorf("%w: unable to extract IG parameter", ErrBingTokenError)
|
||||
}
|
||||
|
||||
return &BingTranslationParams{
|
||||
Token: token,
|
||||
Key: key,
|
||||
IG: ig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetBingTranslation 获取Bing翻译结果
|
||||
func (t *BingTranslator) GetBingTranslation(token, key, ig, text, fromLang, toLang string) (string, error) {
|
||||
// URL编码文本
|
||||
encodedText := url.QueryEscape(text)
|
||||
|
||||
// 构建POST请求的payload
|
||||
payload := fmt.Sprintf("fromLang=%s&to=%s&text=%s&token=%s&key=%s",
|
||||
fromLang, toLang, encodedText, token, key)
|
||||
|
||||
// 构建URL
|
||||
urlStr := fmt.Sprintf("https://%s/ttranslatev3?isVertical=1&IG=%s&IID=translator.5028", t.BingHost, ig)
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequest("POST", urlStr, strings.NewReader(payload))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("Host", t.BingHost)
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Origin", fmt.Sprintf("https://%s", t.BingHost))
|
||||
req.Header.Set("Referer", fmt.Sprintf("https://%s/translator", t.BingHost))
|
||||
|
||||
// 发送请求
|
||||
resp, err := t.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrBingNetworkError, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 判断请求是否成功
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("API error: status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 读取响应体
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
// 检查响应内容
|
||||
if len(body) == 0 {
|
||||
return "", fmt.Errorf("translation API returned empty response")
|
||||
}
|
||||
|
||||
// 使用最简单的结构体解析JSON
|
||||
var response []struct {
|
||||
Translations []struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"translations"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return "", fmt.Errorf("%w: JSON parsing error: %v", ErrBingParseError, err)
|
||||
}
|
||||
|
||||
// 检查解析结果
|
||||
if len(response) == 0 || len(response[0].Translations) == 0 {
|
||||
return "", fmt.Errorf("%w: invalid response format", ErrBingParseError)
|
||||
}
|
||||
|
||||
// 返回翻译结果
|
||||
return response[0].Translations[0].Text, nil
|
||||
}
|
318
internal/common/translator/deepl_translator.go
Normal file
318
internal/common/translator/deepl_translator.go
Normal file
@@ -0,0 +1,318 @@
|
||||
// Package translator 提供文本翻译功能
|
||||
package translator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// DeeplTranslator DeepL翻译器结构体
|
||||
type DeeplTranslator struct {
|
||||
DeeplHost string // DeepL服务主机
|
||||
httpClient *http.Client // HTTP客户端
|
||||
Timeout time.Duration // 请求超时时间
|
||||
}
|
||||
|
||||
// 常量定义
|
||||
const (
|
||||
deeplDefaultTimeout = 30 * time.Second
|
||||
defaultDeeplHost = "www2.deepl.com" // 默认DeepL API主机
|
||||
deeplJsonRpcUrl = "https://www2.deepl.com/jsonrpc" // DeepL JSON-RPC API
|
||||
)
|
||||
|
||||
// 错误定义
|
||||
var (
|
||||
ErrDeeplNetworkError = errors.New("deepl translator network error")
|
||||
ErrDeeplUnsupportedLang = errors.New("deepl translator unsupported language")
|
||||
ErrDeeplResponseError = errors.New("deepl translator response error")
|
||||
)
|
||||
|
||||
// 语言映射
|
||||
var deeplLangMap = map[string]string{
|
||||
"auto": "auto",
|
||||
"de": "DE",
|
||||
"en": "EN",
|
||||
"es": "ES",
|
||||
"fr": "FR",
|
||||
"it": "IT",
|
||||
"ja": "JA",
|
||||
"ko": "KO",
|
||||
"nl": "NL",
|
||||
"pl": "PL",
|
||||
"pt": "PT",
|
||||
"ru": "RU",
|
||||
"zh": "ZH",
|
||||
"bg": "BG",
|
||||
"cs": "CS",
|
||||
"da": "DA",
|
||||
"el": "EL",
|
||||
"et": "ET",
|
||||
"fi": "FI",
|
||||
"hu": "HU",
|
||||
"lt": "LT",
|
||||
"lv": "LV",
|
||||
"ro": "RO",
|
||||
"sk": "SK",
|
||||
"sl": "SL",
|
||||
"sv": "SV",
|
||||
}
|
||||
|
||||
// 反向语言映射
|
||||
var deeplLangMapReverse = map[string]string{
|
||||
"auto": "auto",
|
||||
"DE": "de",
|
||||
"EN": "en",
|
||||
"ES": "es",
|
||||
"FR": "fr",
|
||||
"IT": "it",
|
||||
"JA": "ja",
|
||||
"KO": "ko",
|
||||
"NL": "nl",
|
||||
"PL": "pl",
|
||||
"PT": "pt",
|
||||
"RU": "ru",
|
||||
"ZH": "zh",
|
||||
"BG": "bg",
|
||||
"CS": "cs",
|
||||
"DA": "da",
|
||||
"EL": "el",
|
||||
"ET": "et",
|
||||
"FI": "fi",
|
||||
"HU": "hu",
|
||||
"LT": "lt",
|
||||
"LV": "lv",
|
||||
"RO": "ro",
|
||||
"SK": "sk",
|
||||
"SL": "sl",
|
||||
"SV": "sv",
|
||||
}
|
||||
|
||||
// DeeplRequest DeepL请求结构体
|
||||
type DeeplRequest struct {
|
||||
Jsonrpc string `json:"jsonrpc"`
|
||||
Method string `json:"method"`
|
||||
ID int64 `json:"id"`
|
||||
Params DeeplReqParams `json:"params"`
|
||||
}
|
||||
|
||||
// DeeplReqParams DeepL请求参数结构体
|
||||
type DeeplReqParams struct {
|
||||
Texts []DeeplText `json:"texts"`
|
||||
Splitting string `json:"splitting"`
|
||||
Lang DeeplLang `json:"lang"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// DeeplText DeepL文本结构体
|
||||
type DeeplText struct {
|
||||
Text string `json:"text"`
|
||||
RequestAlternatives int `json:"requestAlternatives"`
|
||||
}
|
||||
|
||||
// DeeplLang DeepL语言结构体
|
||||
type DeeplLang struct {
|
||||
SourceLangUserSelected string `json:"source_lang_user_selected"`
|
||||
TargetLang string `json:"target_lang"`
|
||||
}
|
||||
|
||||
// DeeplResponse DeepL响应结构体
|
||||
type DeeplResponse struct {
|
||||
Jsonrpc string `json:"jsonrpc"`
|
||||
ID int64 `json:"id"`
|
||||
Result DeeplResult `json:"result"`
|
||||
}
|
||||
|
||||
// DeeplResult DeepL结果结构体
|
||||
type DeeplResult struct {
|
||||
Texts []DeeplResultText `json:"texts"`
|
||||
Lang string `json:"lang"`
|
||||
LangIsConfident bool `json:"lang_is_confident"`
|
||||
DetectedLanguages map[string]float64 `json:"detectedLanguages"`
|
||||
}
|
||||
|
||||
// DeeplResultText DeepL结果文本结构体
|
||||
type DeeplResultText struct {
|
||||
Text string `json:"text"`
|
||||
Alternatives []DeeplAlternative `json:"alternatives,omitempty"`
|
||||
}
|
||||
|
||||
// DeeplAlternative DeepL替代翻译结构体
|
||||
type DeeplAlternative struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// NewDeeplTranslator 创建一个新的DeepL翻译器实例
|
||||
func NewDeeplTranslator() *DeeplTranslator {
|
||||
translator := &DeeplTranslator{
|
||||
DeeplHost: defaultDeeplHost,
|
||||
Timeout: deeplDefaultTimeout,
|
||||
httpClient: &http.Client{Timeout: deeplDefaultTimeout},
|
||||
}
|
||||
|
||||
return translator
|
||||
}
|
||||
|
||||
// SetTimeout 设置请求超时时间
|
||||
func (t *DeeplTranslator) SetTimeout(timeout time.Duration) {
|
||||
t.Timeout = timeout
|
||||
t.httpClient.Timeout = timeout
|
||||
}
|
||||
|
||||
// SetDeeplHost 设置DeepL主机
|
||||
func (t *DeeplTranslator) SetDeeplHost(host string) {
|
||||
t.DeeplHost = host
|
||||
}
|
||||
|
||||
// Translate 使用标准语言标签进行文本翻译
|
||||
func (t *DeeplTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) {
|
||||
return t.translate(text, from.String(), to.String())
|
||||
}
|
||||
|
||||
// TranslateWithParams 使用简单字符串参数进行文本翻译
|
||||
func (t *DeeplTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) {
|
||||
tries := params.Tries
|
||||
if tries == 0 {
|
||||
tries = defaultNumberOfRetries
|
||||
}
|
||||
|
||||
var result string
|
||||
var lastError error
|
||||
|
||||
for i := 0; i < tries; i++ {
|
||||
if i > 0 && params.Delay > 0 {
|
||||
time.Sleep(params.Delay)
|
||||
}
|
||||
|
||||
result, lastError = t.translate(text, params.From, params.To)
|
||||
if lastError == nil {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", lastError
|
||||
}
|
||||
|
||||
// translate 执行实际翻译操作
|
||||
func (t *DeeplTranslator) translate(text, from, to string) (string, error) {
|
||||
// 转换语言代码为DeepL格式
|
||||
sourceLang, ok := deeplLangMap[strings.ToLower(from)]
|
||||
if !ok && from != "auto" {
|
||||
sourceLang = "auto"
|
||||
}
|
||||
|
||||
targetLang, ok := deeplLangMap[strings.ToLower(to)]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w: language '%s' not supported by DeepL", ErrDeeplUnsupportedLang, to)
|
||||
}
|
||||
|
||||
// 准备请求数据
|
||||
id := getRandomNumber()
|
||||
iCount := getICount(text)
|
||||
timestamp := getTimeStamp(iCount)
|
||||
|
||||
// 构建请求体
|
||||
reqParams := DeeplReqParams{
|
||||
Texts: []DeeplText{
|
||||
{
|
||||
Text: text,
|
||||
RequestAlternatives: 3,
|
||||
},
|
||||
},
|
||||
Splitting: "newlines",
|
||||
Lang: DeeplLang{
|
||||
SourceLangUserSelected: sourceLang,
|
||||
TargetLang: targetLang,
|
||||
},
|
||||
Timestamp: timestamp,
|
||||
}
|
||||
|
||||
request := DeeplRequest{
|
||||
Jsonrpc: "2.0",
|
||||
Method: "LMT_handle_texts",
|
||||
ID: id,
|
||||
Params: reqParams,
|
||||
}
|
||||
|
||||
// 序列化请求
|
||||
jsonData, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
// 特殊处理method字段格式
|
||||
postStr := string(jsonData)
|
||||
if (id+5)%29 == 0 || (id+3)%13 == 0 {
|
||||
postStr = strings.Replace(postStr, `"method":"`, `"method" : "`, 1)
|
||||
} else {
|
||||
postStr = strings.Replace(postStr, `"method":"`, `"method": "`, 1)
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
req, err := http.NewRequest("POST", deeplJsonRpcUrl, bytes.NewBuffer([]byte(postStr)))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
|
||||
|
||||
resp, err := t.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrDeeplNetworkError, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("%w: status code %d", ErrDeeplNetworkError, resp.StatusCode)
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var deeplResp DeeplResponse
|
||||
err = json.Unmarshal(body, &deeplResp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrDeeplResponseError, err)
|
||||
}
|
||||
|
||||
// 检查是否有有效的结果
|
||||
if len(deeplResp.Result.Texts) == 0 {
|
||||
return "", fmt.Errorf("%w: no translation result", ErrDeeplResponseError)
|
||||
}
|
||||
|
||||
// 返回翻译结果
|
||||
return deeplResp.Result.Texts[0].Text, nil
|
||||
}
|
||||
|
||||
// getICount 获取文本中'i'字符的数量
|
||||
func getICount(text string) int {
|
||||
return strings.Count(text, "i")
|
||||
}
|
||||
|
||||
// getRandomNumber 生成随机数
|
||||
func getRandomNumber() int64 {
|
||||
return int64(rand.Intn(99999)+100000) * 1000
|
||||
}
|
||||
|
||||
// getTimeStamp 获取时间戳
|
||||
func getTimeStamp(iCount int) int64 {
|
||||
ts := time.Now().UnixMilli()
|
||||
if iCount != 0 {
|
||||
iCount++
|
||||
return ts - (ts % int64(iCount)) + int64(iCount)
|
||||
}
|
||||
return ts
|
||||
}
|
311
internal/common/translator/google_translator.go
Normal file
311
internal/common/translator/google_translator.go
Normal file
@@ -0,0 +1,311 @@
|
||||
// Package translator 提供文本翻译功能
|
||||
package translator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// 错误定义
|
||||
var (
|
||||
ErrBadNetwork = errors.New("bad network, please check your internet connection")
|
||||
)
|
||||
|
||||
// GoogleTranslator Google翻译器结构体,统一管理翻译功能
|
||||
type GoogleTranslator struct {
|
||||
GoogleHost string // Google服务主机
|
||||
vm *otto.Otto // JavaScript虚拟机
|
||||
ttk otto.Value // 翻译token缓存
|
||||
httpClient *http.Client // HTTP客户端
|
||||
Timeout time.Duration // 请求超时时间
|
||||
}
|
||||
|
||||
// NewGoogleTranslator 创建一个新的Google翻译器实例
|
||||
func NewGoogleTranslator() *GoogleTranslator {
|
||||
translator := &GoogleTranslator{
|
||||
GoogleHost: "google.com",
|
||||
vm: otto.New(),
|
||||
Timeout: defaultTimeout,
|
||||
httpClient: &http.Client{Timeout: defaultTimeout},
|
||||
}
|
||||
|
||||
// 初始化ttk
|
||||
translator.ttk, _ = otto.ToValue("0")
|
||||
|
||||
return translator
|
||||
}
|
||||
|
||||
// SetTimeout 设置请求超时时间
|
||||
func (t *GoogleTranslator) SetTimeout(timeout time.Duration) {
|
||||
t.Timeout = timeout
|
||||
t.httpClient.Timeout = timeout
|
||||
}
|
||||
|
||||
// SetGoogleHost 设置Google主机
|
||||
func (t *GoogleTranslator) SetGoogleHost(host string) {
|
||||
t.GoogleHost = host
|
||||
}
|
||||
|
||||
// Translate 使用Go语言提供的标准语言标签进行文本翻译
|
||||
func (t *GoogleTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) {
|
||||
return t.translate(text, from.String(), to.String(), false, defaultNumberOfRetries, 0)
|
||||
}
|
||||
|
||||
// TranslateWithParams 使用简单字符串参数进行文本翻译
|
||||
func (t *GoogleTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) {
|
||||
tries := params.Tries
|
||||
if tries == 0 {
|
||||
tries = defaultNumberOfRetries
|
||||
}
|
||||
|
||||
return t.translate(text, params.From, params.To, true, tries, params.Delay)
|
||||
}
|
||||
|
||||
// translate 执行实际翻译操作
|
||||
func (t *GoogleTranslator) translate(text, from, to string, withVerification bool, tries int, delay time.Duration) (string, error) {
|
||||
if tries == 0 {
|
||||
tries = defaultNumberOfRetries
|
||||
}
|
||||
|
||||
if withVerification {
|
||||
if _, err := language.Parse(from); err != nil && from != "auto" {
|
||||
log.Println("[WARNING], '" + from + "' is a invalid language, switching to 'auto'")
|
||||
from = "auto"
|
||||
}
|
||||
if _, err := language.Parse(to); err != nil {
|
||||
log.Println("[WARNING], '" + to + "' is a invalid language, switching to 'en'")
|
||||
to = "en"
|
||||
}
|
||||
}
|
||||
|
||||
textValue, _ := otto.ToValue(text)
|
||||
urlStr := fmt.Sprintf("https://translate.%s/translate_a/single", t.GoogleHost)
|
||||
token := t.getToken(textValue)
|
||||
|
||||
data := map[string]string{
|
||||
"client": "gtx",
|
||||
"sl": from,
|
||||
"tl": to,
|
||||
"hl": to,
|
||||
"ie": "UTF-8",
|
||||
"oe": "UTF-8",
|
||||
"otf": "1",
|
||||
"ssel": "0",
|
||||
"tsel": "0",
|
||||
"kc": "7",
|
||||
"q": text,
|
||||
}
|
||||
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
parameters := url.Values{}
|
||||
for k, v := range data {
|
||||
parameters.Add(k, v)
|
||||
}
|
||||
for _, v := range []string{"at", "bd", "ex", "ld", "md", "qca", "rw", "rm", "ss", "t"} {
|
||||
parameters.Add("dt", v)
|
||||
}
|
||||
|
||||
parameters.Add("tk", token)
|
||||
u.RawQuery = parameters.Encode()
|
||||
|
||||
var r *http.Response
|
||||
for tries > 0 {
|
||||
req, err := http.NewRequest("GET", u.String(), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
r, err = t.httpClient.Do(req)
|
||||
if err != nil {
|
||||
if errors.Is(err, http.ErrHandlerTimeout) {
|
||||
return "", ErrBadNetwork
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if r.StatusCode == http.StatusOK {
|
||||
break
|
||||
}
|
||||
|
||||
if r.StatusCode == http.StatusForbidden {
|
||||
tries--
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
var resp []interface{}
|
||||
err = json.Unmarshal(raw, &resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
responseText := ""
|
||||
for _, obj := range resp[0].([]interface{}) {
|
||||
if len(obj.([]interface{})) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
t, ok := obj.([]interface{})[0].(string)
|
||||
if ok {
|
||||
responseText += t
|
||||
}
|
||||
}
|
||||
|
||||
return responseText, nil
|
||||
}
|
||||
|
||||
// getToken 获取翻译API所需的token
|
||||
func (t *GoogleTranslator) getToken(text otto.Value) string {
|
||||
ttk, err := t.updateTTK()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
tk, err := t.generateToken(text, ttk)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.Replace(tk.String(), "&tk=", "", -1)
|
||||
}
|
||||
|
||||
// updateTTK 更新TTK值
|
||||
func (t *GoogleTranslator) updateTTK() (otto.Value, error) {
|
||||
timestamp := time.Now().UnixNano() / 3600000
|
||||
now := math.Floor(float64(timestamp))
|
||||
ttk, err := strconv.ParseFloat(t.ttk.String(), 64)
|
||||
if err != nil {
|
||||
return otto.UndefinedValue(), err
|
||||
}
|
||||
|
||||
if ttk == now {
|
||||
return t.ttk, nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://translate.%s", t.GoogleHost), nil)
|
||||
if err != nil {
|
||||
return otto.UndefinedValue(), err
|
||||
}
|
||||
|
||||
resp, err := t.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return otto.UndefinedValue(), err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return otto.UndefinedValue(), err
|
||||
}
|
||||
|
||||
matches := regexp.MustCompile(`tkk:\s?'(.+?)'`).FindStringSubmatch(string(body))
|
||||
if len(matches) > 0 {
|
||||
v, err := otto.ToValue(matches[0])
|
||||
if err != nil {
|
||||
return otto.UndefinedValue(), err
|
||||
}
|
||||
t.ttk = v
|
||||
return v, nil
|
||||
}
|
||||
|
||||
return t.ttk, nil
|
||||
}
|
||||
|
||||
// generateToken 生成翻译API所需的token
|
||||
func (t *GoogleTranslator) generateToken(a otto.Value, TTK otto.Value) (otto.Value, error) {
|
||||
err := t.vm.Set("x", a)
|
||||
if err != nil {
|
||||
return otto.UndefinedValue(), err
|
||||
}
|
||||
|
||||
_ = t.vm.Set("internalTTK", TTK)
|
||||
|
||||
result, err := t.vm.Run(`
|
||||
function sM(a) {
|
||||
var b;
|
||||
if (null !== yr)
|
||||
b = yr;
|
||||
else {
|
||||
b = wr(String.fromCharCode(84));
|
||||
var c = wr(String.fromCharCode(75));
|
||||
b = [b(), b()];
|
||||
b[1] = c();
|
||||
b = (yr = window[b.join(c())] || "") || ""
|
||||
}
|
||||
var d = wr(String.fromCharCode(116))
|
||||
, c = wr(String.fromCharCode(107))
|
||||
, d = [d(), d()];
|
||||
d[1] = c();
|
||||
c = "&" + d.join("") + "=";
|
||||
d = b.split(".");
|
||||
b = Number(d[0]) || 0;
|
||||
for (var e = [], f = 0, g = 0; g < a.length; g++) {
|
||||
var l = a.charCodeAt(g);
|
||||
128 > l ? e[f++] = l : (2048 > l ? e[f++] = l >> 6 | 192 : (55296 == (l & 64512) && g + 1 < a.length && 56320 == (a.charCodeAt(g + 1) & 64512) ? (l = 65536 + ((l & 1023) << 10) + (a.charCodeAt(++g) & 1023),
|
||||
e[f++] = l >> 18 | 240,
|
||||
e[f++] = l >> 12 & 63 | 128) : e[f++] = l >> 12 | 224,
|
||||
e[f++] = l >> 6 & 63 | 128),
|
||||
e[f++] = l & 63 | 128)
|
||||
}
|
||||
a = b;
|
||||
for (f = 0; f < e.length; f++)
|
||||
a += e[f],
|
||||
a = xr(a, "+-a^+6");
|
||||
a = xr(a, "+-3^+b+-f");
|
||||
a ^= Number(d[1]) || 0;
|
||||
0 > a && (a = (a & 2147483647) + 2147483648);
|
||||
a %= 1E6;
|
||||
return c + (a.toString() + "." + (a ^ b))
|
||||
}
|
||||
|
||||
var yr = null;
|
||||
var wr = function(a) {
|
||||
return function() {
|
||||
return a
|
||||
}
|
||||
}
|
||||
, xr = function(a, b) {
|
||||
for (var c = 0; c < b.length - 2; c += 3) {
|
||||
var d = b.charAt(c + 2)
|
||||
, d = "a" <= d ? d.charCodeAt(0) - 87 : Number(d)
|
||||
, d = "+" == b.charAt(c + 1) ? a >>> d : a << d;
|
||||
a = "+" == b.charAt(c) ? a + d & 4294967295 : a ^ d
|
||||
}
|
||||
return a
|
||||
};
|
||||
|
||||
var window = {
|
||||
TKK: internalTTK
|
||||
};
|
||||
|
||||
sM(x)
|
||||
`)
|
||||
if err != nil {
|
||||
return otto.UndefinedValue(), err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
73
internal/common/translator/translator.go
Normal file
73
internal/common/translator/translator.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Package translator 提供文本翻译功能
|
||||
package translator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// TranslationParams 用于指定翻译参数
|
||||
type TranslationParams struct {
|
||||
From string // 源语言
|
||||
To string // 目标语言
|
||||
Tries int // 重试次数
|
||||
Delay time.Duration // 重试延迟
|
||||
}
|
||||
|
||||
// 常量定义
|
||||
const (
|
||||
defaultNumberOfRetries = 2
|
||||
defaultTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// TranslatorType 翻译器类型
|
||||
type TranslatorType string
|
||||
|
||||
const (
|
||||
// GoogleTranslatorType 谷歌翻译器
|
||||
GoogleTranslatorType TranslatorType = "google"
|
||||
// BingTranslatorType 必应翻译器
|
||||
BingTranslatorType TranslatorType = "bing"
|
||||
// YoudaoTranslatorType 有道翻译器
|
||||
YoudaoTranslatorType TranslatorType = "youdao"
|
||||
// DeeplTranslatorType DeepL翻译器
|
||||
DeeplTranslatorType TranslatorType = "deepl"
|
||||
)
|
||||
|
||||
// Translator 翻译器接口,定义所有翻译器必须实现的方法
|
||||
type Translator interface {
|
||||
// Translate 使用Go语言提供的标准语言标签进行文本翻译
|
||||
Translate(text string, from language.Tag, to language.Tag) (string, error)
|
||||
|
||||
// TranslateWithParams 使用简单字符串参数进行文本翻译
|
||||
TranslateWithParams(text string, params TranslationParams) (string, error)
|
||||
|
||||
// SetTimeout 设置请求超时时间
|
||||
SetTimeout(timeout time.Duration)
|
||||
}
|
||||
|
||||
// TranslatorFactory 翻译器工厂,用于创建不同类型的翻译器
|
||||
type TranslatorFactory struct{}
|
||||
|
||||
// NewTranslatorFactory 创建一个新的翻译器工厂
|
||||
func NewTranslatorFactory() *TranslatorFactory {
|
||||
return &TranslatorFactory{}
|
||||
}
|
||||
|
||||
// Create 根据类型创建翻译器
|
||||
func (f *TranslatorFactory) Create(translatorType TranslatorType) (Translator, error) {
|
||||
switch translatorType {
|
||||
case GoogleTranslatorType:
|
||||
return NewGoogleTranslator(), nil
|
||||
case BingTranslatorType:
|
||||
return NewBingTranslator(), nil
|
||||
case YoudaoTranslatorType:
|
||||
return NewYoudaoTranslator(), nil
|
||||
case DeeplTranslatorType:
|
||||
return NewDeeplTranslator(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported translator type: %s", translatorType)
|
||||
}
|
||||
}
|
186
internal/common/translator/youdao_translator.go
Normal file
186
internal/common/translator/youdao_translator.go
Normal file
@@ -0,0 +1,186 @@
|
||||
// Package translator 提供文本翻译功能
|
||||
package translator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// YoudaoTranslator 有道翻译器结构体
|
||||
type YoudaoTranslator struct {
|
||||
httpClient *http.Client // HTTP客户端
|
||||
Timeout time.Duration // 请求超时时间
|
||||
}
|
||||
|
||||
// 常量定义
|
||||
const (
|
||||
youdaoDefaultTimeout = 30 * time.Second
|
||||
youdaoTranslateURL = "https://m.youdao.com/translate"
|
||||
)
|
||||
|
||||
// 错误定义
|
||||
var (
|
||||
ErrYoudaoNetworkError = errors.New("youdao translator network error")
|
||||
ErrYoudaoParseError = errors.New("youdao translator parse error")
|
||||
)
|
||||
|
||||
// NewYoudaoTranslator 创建一个新的有道翻译器实例
|
||||
func NewYoudaoTranslator() *YoudaoTranslator {
|
||||
translator := &YoudaoTranslator{
|
||||
Timeout: youdaoDefaultTimeout,
|
||||
httpClient: &http.Client{Timeout: youdaoDefaultTimeout},
|
||||
}
|
||||
|
||||
return translator
|
||||
}
|
||||
|
||||
// SetTimeout 设置请求超时时间
|
||||
func (t *YoudaoTranslator) SetTimeout(timeout time.Duration) {
|
||||
t.Timeout = timeout
|
||||
t.httpClient.Timeout = timeout
|
||||
}
|
||||
|
||||
// Translate 使用标准语言标签进行文本翻译
|
||||
func (t *YoudaoTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) {
|
||||
// 有道翻译不需要指定源语言和目标语言,它会自动检测
|
||||
return t.translate(text)
|
||||
}
|
||||
|
||||
// TranslateWithParams 使用简单字符串参数进行文本翻译
|
||||
func (t *YoudaoTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) {
|
||||
// 有道翻译不需要指定源语言和目标语言,它会自动检测
|
||||
return t.translate(text)
|
||||
}
|
||||
|
||||
// translate 执行实际翻译操作
|
||||
func (t *YoudaoTranslator) translate(text string) (string, error) {
|
||||
// 构建表单数据
|
||||
form := url.Values{}
|
||||
form.Add("inputtext", text)
|
||||
form.Add("type", "AUTO")
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequest("POST", youdaoTranslateURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36")
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
|
||||
// 发送请求
|
||||
resp, err := t.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrYoudaoNetworkError, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 判断请求是否成功
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("API error: status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 读取响应体
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
// 解析HTML响应
|
||||
result, err := t.extractTranslationResult(string(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// extractTranslationResult 从HTML响应中提取翻译结果
|
||||
func (t *YoudaoTranslator) extractTranslationResult(htmlContent string) (string, error) {
|
||||
// 方法1:使用正则表达式提取翻译结果
|
||||
pattern := regexp.MustCompile(`<ul id="translateResult"[^>]*>.*?<li[^>]*>(.*?)</li>`)
|
||||
matches := pattern.FindStringSubmatch(htmlContent)
|
||||
|
||||
if len(matches) >= 2 {
|
||||
// 清理HTML标签
|
||||
result := matches[1]
|
||||
result = strings.ReplaceAll(result, "<br>", "\n")
|
||||
result = t.stripHTMLTags(result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 方法2:使用HTML解析器提取翻译结果
|
||||
doc, err := html.Parse(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: failed to parse HTML", ErrYoudaoParseError)
|
||||
}
|
||||
|
||||
// 查找翻译结果元素
|
||||
result := t.findTranslateResult(doc)
|
||||
if result != "" {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w: could not find translation result", ErrYoudaoParseError)
|
||||
}
|
||||
|
||||
// stripHTMLTags 移除HTML标签
|
||||
func (t *YoudaoTranslator) stripHTMLTags(input string) string {
|
||||
// 简单的HTML标签移除
|
||||
re := regexp.MustCompile("<[^>]*>")
|
||||
return re.ReplaceAllString(input, "")
|
||||
}
|
||||
|
||||
// findTranslateResult 在HTML文档中查找翻译结果
|
||||
func (t *YoudaoTranslator) findTranslateResult(n *html.Node) string {
|
||||
if n.Type == html.ElementNode && n.Data == "ul" {
|
||||
for _, attr := range n.Attr {
|
||||
if attr.Key == "id" && attr.Val == "translateResult" {
|
||||
// 找到了translateResult元素,提取其中的文本
|
||||
var result string
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
if c.Type == html.ElementNode && c.Data == "li" {
|
||||
return t.extractText(c)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 递归查找子节点
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
result := t.findTranslateResult(c)
|
||||
if result != "" {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractText 提取节点中的文本内容
|
||||
func (t *YoudaoTranslator) extractText(n *html.Node) string {
|
||||
if n.Type == html.TextNode {
|
||||
return n.Data
|
||||
}
|
||||
|
||||
var result string
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
result += t.extractText(c)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
Reference in New Issue
Block a user