From c7c586613174214db893e6162ea293f03e4f36fe Mon Sep 17 00:00:00 2001 From: Gabe Yuan Date: Wed, 6 Sep 2023 14:57:02 +0800 Subject: [PATCH] customize api --- src/apis/index.js | 139 ++++++++++++++++++++------------- src/config/i18n.js | 97 +++++++++++++++++++++++ src/config/index.js | 14 +--- src/hooks/Api.js | 4 +- src/hooks/Setting.js | 2 +- src/hooks/Translate.js | 5 +- src/libs/fetch.js | 4 + src/libs/translator.js | 4 + src/views/Options/Apis.js | 49 +++++++----- src/views/Options/Setting.js | 145 +---------------------------------- 10 files changed, 232 insertions(+), 231 deletions(-) diff --git a/src/apis/index.js b/src/apis/index.js index da3857d..eef30e5 100644 --- a/src/apis/index.js +++ b/src/apis/index.js @@ -6,7 +6,6 @@ import { OPT_TRANS_DEEPL, OPT_TRANS_OPENAI, OPT_TRANS_CUSTOMIZE, - URL_MICROSOFT_TRANS, OPT_LANGS_SPECIAL, PROMPT_PLACE_FROM, PROMPT_PLACE_TO, @@ -49,8 +48,13 @@ export const apiFetchRules = (url, isBg = false) => * @param {*} from * @returns */ -const apiGoogleTranslate = async (translator, text, to, from, setting) => { - const { url, key } = setting; +const apiGoogleTranslate = async ( + translator, + text, + to, + from, + { url, key, useCache = true } +) => { const params = { client: "gtx", dt: "t", @@ -61,15 +65,19 @@ const apiGoogleTranslate = async (translator, text, to, from, setting) => { q: text, }; const input = `${url}?${queryString.stringify(params)}`; - return fetchPolyfill(input, { + const res = await fetchPolyfill(input, { headers: { "Content-type": "application/json", }, - useCache: true, + useCache, usePool: true, translator, token: key, }); + const trText = res.sentences.map((item) => item.trans).join(" "); + const isSame = to === res.src; + + return [trText, isSame]; }; /** @@ -79,23 +87,33 @@ const apiGoogleTranslate = async (translator, text, to, from, setting) => { * @param {*} from * @returns */ -const apiMicrosoftTranslate = (translator, text, to, from) => { +const apiMicrosoftTranslate = async ( + translator, + text, + to, + from, + { url, useCache = true } +) => { const params = { from, to, "api-version": "3.0", }; - const input = `${URL_MICROSOFT_TRANS}?${queryString.stringify(params)}`; - return fetchPolyfill(input, { + const input = `${url}?${queryString.stringify(params)}`; + const res = await fetchPolyfill(input, { headers: { "Content-type": "application/json", }, method: "POST", body: JSON.stringify([{ Text: text }]), - useCache: true, + useCache, usePool: true, translator, }); + const trText = res[0].translations[0].text; + const isSame = to === res[0].detectedLanguage?.language; + + return [trText, isSame]; }; /** @@ -105,8 +123,13 @@ const apiMicrosoftTranslate = (translator, text, to, from) => { * @param {*} from * @returns */ -const apiDeepLTranslate = (translator, text, to, from, setting) => { - const { url, key } = setting; +const apiDeepLTranslate = async ( + translator, + text, + to, + from, + { url, key, useCache = true } +) => { const data = { text: [text], target_lang: to, @@ -115,17 +138,21 @@ const apiDeepLTranslate = (translator, text, to, from, setting) => { if (from) { data.source_lang = from; } - return fetchPolyfill(url, { + const res = await fetchPolyfill(url, { headers: { "Content-type": "application/json", }, method: "POST", body: JSON.stringify(data), - useCache: true, + useCache, usePool: true, translator, token: key, }); + const trText = res.translations.map((item) => item.text).join(" "); + const isSame = to === res.translations[0].detected_source_language; + + return [trText, isSame]; }; /** @@ -135,12 +162,17 @@ const apiDeepLTranslate = (translator, text, to, from, setting) => { * @param {*} from * @returns */ -const apiOpenaiTranslate = async (translator, text, to, from, setting) => { - let { url, key, model, prompt } = setting; +const apiOpenaiTranslate = async ( + translator, + text, + to, + from, + { url, key, model, prompt, useCache = true } +) => { prompt = prompt .replaceAll(PROMPT_PLACE_FROM, from) .replaceAll(PROMPT_PLACE_TO, to); - return fetchPolyfill(url, { + const res = await fetchPolyfill(url, { headers: { "Content-type": "application/json", }, @@ -160,11 +192,17 @@ const apiOpenaiTranslate = async (translator, text, to, from, setting) => { temperature: 0, max_tokens: 256, }), - useCache: true, + useCache, usePool: true, translator, token: key, }); + const trText = res?.choices?.[0].message.content; + const sLang = await tryDetectLang(text); + const tLang = await tryDetectLang(trText); + const isSame = text === trText || (sLang && tLang && sLang === tLang); + + return [trText, isSame]; }; /** @@ -174,12 +212,16 @@ const apiOpenaiTranslate = async (translator, text, to, from, setting) => { * @param {*} from * @returns */ -const apiCustomizeTranslate = async (translator, text, to, from, setting) => { - let { url, key, headers } = setting; - return fetchPolyfill(url, { +const apiCustomTranslate = async ( + translator, + text, + to, + from, + { url, key, useCache = true } +) => { + const res = await fetchPolyfill(url, { headers: { "Content-type": "application/json", - ...JSON.parse(headers), }, method: "POST", body: JSON.stringify({ @@ -187,11 +229,15 @@ const apiCustomizeTranslate = async (translator, text, to, from, setting) => { from, to, }), - useCache: true, + useCache, usePool: true, translator, token: key, }); + const trText = res.text; + const isSame = to === res.from; + + return [trText, isSame]; }; /** @@ -199,40 +245,29 @@ const apiCustomizeTranslate = async (translator, text, to, from, setting) => { * @param {*} param0 * @returns */ -export const apiTranslate = async ({ +export const apiTranslate = ({ translator, - q, + text, fromLang, toLang, - setting, + apiSetting, }) => { - let trText = ""; - let isSame = false; + const from = OPT_LANGS_SPECIAL[translator]?.get(fromLang) ?? fromLang; + const to = OPT_LANGS_SPECIAL[translator]?.get(toLang) ?? toLang; + const callApi = (api) => api(translator, text, to, from, apiSetting); - let from = OPT_LANGS_SPECIAL?.[translator]?.get(fromLang) ?? fromLang; - let to = OPT_LANGS_SPECIAL?.[translator]?.get(toLang) ?? toLang; - - if (translator === OPT_TRANS_GOOGLE) { - const res = await apiGoogleTranslate(translator, q, to, from, setting); - trText = res.sentences.map((item) => item.trans).join(" "); - isSame = to === res.src; - } else if (translator === OPT_TRANS_MICROSOFT) { - const res = await apiMicrosoftTranslate(translator, q, to, from); - trText = res[0].translations[0].text; - isSame = to === res[0].detectedLanguage?.language; - } else if (translator === OPT_TRANS_DEEPL) { - const res = await apiDeepLTranslate(translator, q, to, from, setting); - trText = res.translations.map((item) => item.text).join(" "); - isSame = to === (from || res.translations[0].detected_source_language); - } else if (translator === OPT_TRANS_OPENAI) { - const res = await apiOpenaiTranslate(translator, q, to, from, setting); - trText = res?.choices?.[0].message.content; - const sLang = await tryDetectLang(q); - const tLang = await tryDetectLang(trText); - isSame = q === trText || (sLang && tLang && sLang === tLang); - } else if (translator === OPT_TRANS_CUSTOMIZE) { - // todo + switch (translator) { + case OPT_TRANS_GOOGLE: + return callApi(apiGoogleTranslate); + case OPT_TRANS_MICROSOFT: + return callApi(apiMicrosoftTranslate); + case OPT_TRANS_DEEPL: + return callApi(apiDeepLTranslate); + case OPT_TRANS_OPENAI: + return callApi(apiOpenaiTranslate); + case OPT_TRANS_CUSTOMIZE: + return callApi(apiCustomTranslate); + default: + return ["", false]; } - - return [trText, isSame]; }; diff --git a/src/config/i18n.js b/src/config/i18n.js index 79dbabd..6b01457 100644 --- a/src/config/i18n.js +++ b/src/config/i18n.js @@ -3,6 +3,99 @@ export const UI_LANGS = [ ["zh", "中文"], ]; +const customApiLangs = `["en", "English - English"], +["zh-CN", "Simplified Chinese - 简体中文"], +["zh-TW", "Traditional Chinese - 繁體中文"], +["ar", "Arabic - العربية"], +["bg", "Bulgarian - Български"], +["ca", "Catalan - Català"], +["hr", "Croatian - Hrvatski"], +["cs", "Czech - Čeština"], +["da", "Danish - Dansk"], +["nl", "Dutch - Nederlands"], +["fi", "Finnish - Suomi"], +["fr", "French - Français"], +["de", "German - Deutsch"], +["el", "Greek - Ελληνικά"], +["hi", "Hindi - हिन्दी"], +["hu", "Hungarian - Magyar"], +["id", "Indonesian - Indonesia"], +["it", "Italian - Italiano"], +["ja", "Japanese - 日本語"], +["ko", "Korean - 한국어"], +["ms", "Malay - Melayu"], +["mt", "Maltese - Malti"], +["nb", "Norwegian - Norsk Bokmål"], +["pl", "Polish - Polski"], +["pt", "Portuguese - Português"], +["ro", "Romanian - Română"], +["ru", "Russian - Русский"], +["sk", "Slovak - Slovenčina"], +["sl", "Slovenian - Slovenščina"], +["es", "Spanish - Español"], +["sv", "Swedish - Svenska"], +["ta", "Tamil - தமிழ்"], +["te", "Telugu - తెలుగు"], +["th", "Thai - ไทย"], +["tr", "Turkish - Türkçe"], +["uk", "Ukrainian - Українська"], +["vi", "Vietnamese - Tiếng Việt"], +`; + +const customApiHelpZH = `/// 自定义翻译源接口说明 +// 请求(Request)数据将按下面规范发送 +{ + url: {{YOUR_URL}}, + method: "POST", + headers: { + "Content-type": "application/json", + "Authorization"] = "Bearer {{YOUR_KEY}}" + }, + body: { + text, // 需要翻译的文字 + from, // 源语言,可能为空,表示需要接口自动识别语言 + to, // 目标语言 + } +} + +// 返回(Response)数据需符合下面的JSON规范 +{ + text, // 翻译后的文字 + from, // 识别的源语言 + to, // 目标语言(可选) +} + +// 支持的语言代码如下 +${customApiLangs} +`; + +const customApiHelpEN = `/// Custom translation source interface description +// Request data will be sent according to the following specifications +{ + url: {{YOUR_URL}}, + method: "POST", + headers: { + "Content-type": "application/json", + "Authorization"] = "Bearer {{YOUR_KEY}}" + }, + body: { + text, // text to be translated + from, // Source language, may be empty + to, // Target language + } +} + +// The returned data must conform to the following JSON specification +{ + text, // translated text + from, // Recognized source language + to, // Target language (optional) +} + +// The supported language codes are as follows +${customApiLangs} +`; + export const I18N = { app_name: { zh: `简约翻译`, @@ -12,6 +105,10 @@ export const I18N = { zh: `翻译`, en: `Translate`, }, + custom_api_help: { + zh: customApiHelpZH, + en: customApiHelpEN, + }, translate_alt: { zh: `翻译 (Alt+Q)`, en: `Translate (Alt+Q)`, diff --git a/src/config/index.js b/src/config/index.js index 606b3b0..a105bfa 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -64,14 +64,12 @@ export const URL_KISS_RULES_NEW_ISSUE = export const URL_RAW_PREFIX = "https://raw.githubusercontent.com/fishjar/kiss-translator/master"; export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth"; -export const URL_MICROSOFT_TRANS = - "https://api-edge.cognitive.microsofttranslator.com/translate"; export const OPT_TRANS_GOOGLE = "Google"; export const OPT_TRANS_MICROSOFT = "Microsoft"; export const OPT_TRANS_DEEPL = "DeepL"; export const OPT_TRANS_OPENAI = "OpenAI"; -export const OPT_TRANS_CUSTOMIZE = "Customize"; +export const OPT_TRANS_CUSTOMIZE = "Custom"; export const OPT_TRANS_ALL = [ OPT_TRANS_GOOGLE, OPT_TRANS_MICROSOFT, @@ -135,6 +133,7 @@ export const OPT_LANGS_SPECIAL = { [OPT_TRANS_OPENAI]: new Map( OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) ), + [OPT_TRANS_CUSTOMIZE]: new Map([["auto", ""]]), }; export const OPT_STYLE_NONE = "style_none"; // 无 @@ -204,6 +203,7 @@ export const DEFAULT_SUBRULES_LIST = [ export const DEFAULT_TRANS_APIS = { [OPT_TRANS_GOOGLE]: { url: "https://translate.googleapis.com/translate_a/single", + key: "", }, [OPT_TRANS_MICROSOFT]: { url: "https://api-edge.cognitive.microsofttranslator.com/translate", @@ -222,7 +222,6 @@ export const DEFAULT_TRANS_APIS = { [OPT_TRANS_CUSTOMIZE]: { url: "", key: "", - headers: "", }, }; @@ -243,13 +242,6 @@ export const DEFAULT_SETTING = { subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表 owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则 transApis: DEFAULT_TRANS_APIS, // 翻译接口 - googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口 - deeplUrl: "https://api-free.deepl.com/v2/translate", - deeplKey: "", - openaiUrl: "https://api.openai.com/v1/chat/completions", - openaiKey: "", - openaiModel: "gpt-4", - openaiPrompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`, }; export const DEFAULT_RULES = [GLOBLA_RULE]; diff --git a/src/hooks/Api.js b/src/hooks/Api.js index ec659bb..37ac6f8 100644 --- a/src/hooks/Api.js +++ b/src/hooks/Api.js @@ -5,8 +5,6 @@ import { useSetting } from "./Setting"; export function useApi(translator) { const { setting, updateSetting } = useSetting(); const apis = setting?.transApis || DEFAULT_TRANS_APIS; - const api = apis[translator] || {}; - console.log("apis", translator, apis); const updateApi = useCallback( async (obj) => { @@ -22,5 +20,5 @@ export function useApi(translator) { await updateSetting({ transApis }); }, [translator, apis, updateSetting]); - return { api, updateApi, resetApi }; + return { api: apis[translator] || {}, updateApi, resetApi }; } diff --git a/src/hooks/Setting.js b/src/hooks/Setting.js index 1272a17..a919244 100644 --- a/src/hooks/Setting.js +++ b/src/hooks/Setting.js @@ -6,7 +6,7 @@ import { createContext, useCallback, useContext, useMemo } from "react"; import { debounce } from "../libs/utils"; const SettingContext = createContext({ - setting: null, + setting: {}, updateSetting: async () => {}, reloadSetting: async () => {}, }); diff --git a/src/hooks/Translate.js b/src/hooks/Translate.js index f9c3af3..2065217 100644 --- a/src/hooks/Translate.js +++ b/src/hooks/Translate.js @@ -2,6 +2,7 @@ import { useEffect } from "react"; import { useState } from "react"; import { tryDetectLang } from "../libs"; import { apiTranslate } from "../apis"; +import { DEFAULT_TRANS_APIS } from "../config"; /** * 翻译hook @@ -28,10 +29,10 @@ export function useTranslate(q, rule, setting) { } else { const [trText, isSame] = await apiTranslate({ translator, - q, + text: q, fromLang, toLang, - setting: setting[translator], + apiSetting: (setting.transApis || DEFAULT_TRANS_APIS)[translator], }); setText(trText); setSamelang(isSame); diff --git a/src/libs/fetch.js b/src/libs/fetch.js index 3360616..ccddd96 100644 --- a/src/libs/fetch.js +++ b/src/libs/fetch.js @@ -177,6 +177,10 @@ export const fetchData = async ( * @returns */ export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => { + if (!input.trim()) { + throw new Error("URL is empty"); + } + // 插件 if (isExt && !isBg) { const res = await sendBgMsg(MSG_FETCH, { input, opts }); diff --git a/src/libs/translator.js b/src/libs/translator.js index a1f00eb..c251070 100644 --- a/src/libs/translator.js +++ b/src/libs/translator.js @@ -211,6 +211,10 @@ export class Translator { }; _register = () => { + if (this._rule.fromLang === this._rule.toLang) { + return; + } + // 搜索节点 this._queryNodes(); diff --git a/src/views/Options/Apis.js b/src/views/Options/Apis.js index 1917fdd..829a97d 100644 --- a/src/views/Options/Apis.js +++ b/src/views/Options/Apis.js @@ -7,6 +7,7 @@ import { OPT_TRANS_MICROSOFT, OPT_TRANS_OPENAI, OPT_TRANS_CUSTOMIZE, + URL_KISS_PROXY, } from "../../config"; import { useState } from "react"; import { useI18n } from "../../hooks/I18n"; @@ -15,9 +16,12 @@ import Accordion from "@mui/material/Accordion"; import AccordionSummary from "@mui/material/AccordionSummary"; import AccordionDetails from "@mui/material/AccordionDetails"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import Alert from "@mui/material/Alert"; import { useAlert } from "../../hooks/Alert"; import { useApi } from "../../hooks/Api"; import { apiTranslate } from "../../apis"; +import Box from "@mui/material/Box"; +import Link from "@mui/material/Link"; function TestButton({ translator, api }) { const i18n = useI18n(); @@ -28,10 +32,10 @@ function TestButton({ translator, api }) { setLoading(true); const [text] = await apiTranslate({ translator, - q: "hello world", - fromLang: "auto", + text: "hello world", + fromLang: "en", toLang: "zh-CN", - setting: api, + apiSetting: { ...api, useCache: false }, }); if (!text) { throw new Error("empty reault"); @@ -45,7 +49,7 @@ function TestButton({ translator, api }) { }; if (loading) { - return ; + return ; } return ( @@ -58,7 +62,7 @@ function TestButton({ translator, api }) { function ApiFields({ translator }) { const i18n = useI18n(); const { api, updateApi, resetApi } = useApi(translator); - const { url = "", key = "", model = "", prompt = "", headers = "" } = api; + const { url = "", key = "", model = "", prompt = "" } = api; const handleChange = (e) => { const { name, value } = e.target; @@ -106,16 +110,6 @@ function ApiFields({ translator }) { /> )} - {translator === OPT_TRANS_CUSTOMIZE && ( - - )} @@ -131,6 +125,10 @@ function ApiFields({ translator }) { )} + + {translator === OPT_TRANS_CUSTOMIZE && ( +
{i18n("custom_api_help")}
+ )} ); } @@ -155,7 +153,22 @@ function ApiAccordion({ translator }) { } export default function Apis() { - return OPT_TRANS_ALL.map((translator) => ( - - )); + const i18n = useI18n(); + return ( + + + + + {i18n("about_api_proxy")} + + + + + {OPT_TRANS_ALL.map((translator) => ( + + ))} + + + + ); } diff --git a/src/views/Options/Setting.js b/src/views/Options/Setting.js index c9683eb..4cbf0db 100644 --- a/src/views/Options/Setting.js +++ b/src/views/Options/Setting.js @@ -10,55 +10,8 @@ import FormHelperText from "@mui/material/FormHelperText"; import { useSetting } from "../../hooks/Setting"; import { limitNumber } from "../../libs/utils"; import { useI18n } from "../../hooks/I18n"; -import { apiTranslate } from "../../apis"; import { useAlert } from "../../hooks/Alert"; -import CircularProgress from "@mui/material/CircularProgress"; -import { - UI_LANGS, - URL_KISS_PROXY, - TRANS_NEWLINE_LENGTH, - CACHE_NAME, - OPT_TRANS_GOOGLE, - OPT_TRANS_DEEPL, - OPT_TRANS_OPENAI, -} from "../../config"; -import { useState } from "react"; - -function TestLink({ translator, setting }) { - const i18n = useI18n(); - const alert = useAlert(); - const [loading, setLoading] = useState(false); - const handleApiTest = async () => { - try { - setLoading(true); - const [text] = await apiTranslate({ - translator, - q: "hello world", - fromLang: "en", - toLang: "zh-CN", - setting, - }); - if (!text) { - throw new Error("empty reault"); - } - alert.success(i18n("test_success")); - } catch (err) { - alert.error(`${i18n("test_failed")}: ${err.message}`); - } finally { - setLoading(false); - } - }; - - if (loading) { - return ; - } - - return ( - - {i18n("click_test")} - - ); -} +import { UI_LANGS, TRANS_NEWLINE_LENGTH, CACHE_NAME } from "../../config"; export default function Settings() { const i18n = useI18n(); @@ -102,17 +55,10 @@ export default function Settings() { const { uiLang, - googleUrl, fetchLimit, fetchInterval, minLength, maxLength, - openaiUrl, - deeplUrl = "", - deeplKey = "", - openaiKey, - openaiModel, - openaiPrompt, clearCache, newlineLength = TRANS_NEWLINE_LENGTH, } = setting; @@ -198,95 +144,6 @@ export default function Settings() { - - - {i18n("google_api")} - {googleUrl && ( - - )} - - } - name="googleUrl" - value={googleUrl} - onChange={handleChange} - helperText={ - - {i18n("about_api_proxy")} - - } - /> - - - {i18n("deepl_api")} - {deeplUrl && ( - - )} - - } - name="deeplUrl" - value={deeplUrl} - onChange={handleChange} - /> - - - - - {i18n("openai_api")} - {openaiUrl && openaiPrompt && ( - - )} - - } - name="openaiUrl" - value={openaiUrl} - onChange={handleChange} - helperText={ - - {i18n("about_api_proxy")} - - } - /> - - - - - - );