From f772fa000c75fefbf8afa12b09735b5032ac8c2d Mon Sep 17 00:00:00 2001 From: Gabe Yuan Date: Wed, 6 Sep 2023 00:25:46 +0800 Subject: [PATCH] customize api... --- src/apis/index.js | 55 ++++++++--- src/config/i18n.js | 8 ++ src/config/index.js | 29 ++++++ src/hooks/Api.js | 26 ++++++ src/hooks/Translate.js | 2 +- src/libs/fetch.js | 16 ++-- src/views/Options/Apis.js | 161 +++++++++++++++++++++++++++++++++ src/views/Options/Navigator.js | 7 ++ src/views/Options/index.js | 2 + 9 files changed, 286 insertions(+), 20 deletions(-) create mode 100644 src/hooks/Api.js create mode 100644 src/views/Options/Apis.js diff --git a/src/apis/index.js b/src/apis/index.js index 81d36f3..da3857d 100644 --- a/src/apis/index.js +++ b/src/apis/index.js @@ -5,6 +5,7 @@ import { OPT_TRANS_MICROSOFT, OPT_TRANS_DEEPL, OPT_TRANS_OPENAI, + OPT_TRANS_CUSTOMIZE, URL_MICROSOFT_TRANS, OPT_LANGS_SPECIAL, PROMPT_PLACE_FROM, @@ -49,7 +50,7 @@ export const apiFetchRules = (url, isBg = false) => * @returns */ const apiGoogleTranslate = async (translator, text, to, from, setting) => { - const { googleUrl } = setting; + const { url, key } = setting; const params = { client: "gtx", dt: "t", @@ -59,7 +60,7 @@ const apiGoogleTranslate = async (translator, text, to, from, setting) => { tl: to, q: text, }; - const input = `${googleUrl}?${queryString.stringify(params)}`; + const input = `${url}?${queryString.stringify(params)}`; return fetchPolyfill(input, { headers: { "Content-type": "application/json", @@ -67,6 +68,7 @@ const apiGoogleTranslate = async (translator, text, to, from, setting) => { useCache: true, usePool: true, translator, + token: key, }); }; @@ -104,7 +106,7 @@ const apiMicrosoftTranslate = (translator, text, to, from) => { * @returns */ const apiDeepLTranslate = (translator, text, to, from, setting) => { - const { deeplUrl, deeplKey } = setting; + const { url, key } = setting; const data = { text: [text], target_lang: to, @@ -113,7 +115,7 @@ const apiDeepLTranslate = (translator, text, to, from, setting) => { if (from) { data.source_lang = from; } - return fetchPolyfill(deeplUrl, { + return fetchPolyfill(url, { headers: { "Content-type": "application/json", }, @@ -122,7 +124,7 @@ const apiDeepLTranslate = (translator, text, to, from, setting) => { useCache: true, usePool: true, translator, - token: deeplKey, + token: key, }); }; @@ -134,17 +136,17 @@ const apiDeepLTranslate = (translator, text, to, from, setting) => { * @returns */ const apiOpenaiTranslate = async (translator, text, to, from, setting) => { - const { openaiUrl, openaiKey, openaiModel, openaiPrompt } = setting; - let prompt = openaiPrompt + let { url, key, model, prompt } = setting; + prompt = prompt .replaceAll(PROMPT_PLACE_FROM, from) .replaceAll(PROMPT_PLACE_TO, to); - return fetchPolyfill(openaiUrl, { + return fetchPolyfill(url, { headers: { "Content-type": "application/json", }, method: "POST", body: JSON.stringify({ - model: openaiModel, + model: model, messages: [ { role: "system", @@ -161,7 +163,34 @@ const apiOpenaiTranslate = async (translator, text, to, from, setting) => { useCache: true, usePool: true, translator, - token: openaiKey, + token: key, + }); +}; + +/** + * 自定义接口 翻译 + * @param {*} text + * @param {*} to + * @param {*} from + * @returns + */ +const apiCustomizeTranslate = async (translator, text, to, from, setting) => { + let { url, key, headers } = setting; + return fetchPolyfill(url, { + headers: { + "Content-type": "application/json", + ...JSON.parse(headers), + }, + method: "POST", + body: JSON.stringify({ + text, + from, + to, + }), + useCache: true, + usePool: true, + translator, + token: key, }); }; @@ -190,17 +219,19 @@ export const apiTranslate = async ({ } 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; + 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 === res.translations[0].detected_source_language; + 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 } return [trText, isSame]; diff --git a/src/config/i18n.js b/src/config/i18n.js index ad5c803..79dbabd 100644 --- a/src/config/i18n.js +++ b/src/config/i18n.js @@ -24,6 +24,10 @@ export const I18N = { zh: `规则设置`, en: `Rules Setting`, }, + apis_setting: { + zh: `接口设置`, + en: `Apis Setting`, + }, sync_setting: { zh: `同步设置`, en: `Sync Setting`, @@ -360,4 +364,8 @@ export const I18N = { zh: `求助`, en: `Help`, }, + restore_default: { + zh: `恢复默认`, + en: `Restore Default`, + }, }; diff --git a/src/config/index.js b/src/config/index.js index 5ad965d..606b3b0 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -71,11 +71,13 @@ 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_ALL = [ OPT_TRANS_GOOGLE, OPT_TRANS_MICROSOFT, OPT_TRANS_DEEPL, OPT_TRANS_OPENAI, + OPT_TRANS_CUSTOMIZE, ]; export const OPT_LANGS_TO = [ @@ -198,6 +200,32 @@ export const DEFAULT_SUBRULES_LIST = [ }, ]; +// 翻译接口 +export const DEFAULT_TRANS_APIS = { + [OPT_TRANS_GOOGLE]: { + url: "https://translate.googleapis.com/translate_a/single", + }, + [OPT_TRANS_MICROSOFT]: { + url: "https://api-edge.cognitive.microsofttranslator.com/translate", + authUrl: "https://edge.microsoft.com/translate/auth", + }, + [OPT_TRANS_DEEPL]: { + url: "https://api-free.deepl.com/v2/translate", + key: "", + }, + [OPT_TRANS_OPENAI]: { + url: "https://api.openai.com/v1/chat/completion", + key: "", + model: "gpt-4", + prompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`, + }, + [OPT_TRANS_CUSTOMIZE]: { + url: "", + key: "", + headers: "", + }, +}; + export const TRANS_MIN_LENGTH = 5; // 最短翻译长度 export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度 export const TRANS_NEWLINE_LENGTH = 40; // 换行字符数 @@ -214,6 +242,7 @@ export const DEFAULT_SETTING = { injectRules: true, // 是否注入订阅规则 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: "", diff --git a/src/hooks/Api.js b/src/hooks/Api.js new file mode 100644 index 0000000..ec659bb --- /dev/null +++ b/src/hooks/Api.js @@ -0,0 +1,26 @@ +import { useCallback } from "react"; +import { DEFAULT_TRANS_APIS } from "../config"; +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) => { + const api = apis[translator] || {}; + const transApis = { ...apis, [translator]: { ...api, ...obj } }; + await updateSetting({ transApis }); + }, + [translator, apis, updateSetting] + ); + + const resetApi = useCallback(async () => { + const transApis = { ...apis, [translator]: DEFAULT_TRANS_APIS[translator] }; + await updateSetting({ transApis }); + }, [translator, apis, updateSetting]); + + return { api, updateApi, resetApi }; +} diff --git a/src/hooks/Translate.js b/src/hooks/Translate.js index 62d3ea1..f9c3af3 100644 --- a/src/hooks/Translate.js +++ b/src/hooks/Translate.js @@ -31,7 +31,7 @@ export function useTranslate(q, rule, setting) { q, fromLang, toLang, - setting, + setting: setting[translator], }); setText(trText); setSamelang(isSame); diff --git a/src/libs/fetch.js b/src/libs/fetch.js index 2f787fd..3360616 100644 --- a/src/libs/fetch.js +++ b/src/libs/fetch.js @@ -67,13 +67,15 @@ const newCacheReq = async (request) => { * @returns */ const fetchApi = async ({ input, init = {}, translator, token }) => { - if (translator === OPT_TRANS_MICROSOFT) { - init.headers["Authorization"] = `Bearer ${token}`; // Microsoft - } else if (translator === OPT_TRANS_DEEPL) { - init.headers["Authorization"] = `DeepL-Auth-Key ${token}`; // DeepL - } else if (translator === OPT_TRANS_OPENAI) { - init.headers["Authorization"] = `Bearer ${token}`; // OpenAI - init.headers["api-key"] = token; // Azure OpenAI + if (token) { + if (translator === OPT_TRANS_DEEPL) { + init.headers["Authorization"] = `DeepL-Auth-Key ${token}`; // DeepL + } else if (translator === OPT_TRANS_OPENAI) { + init.headers["Authorization"] = `Bearer ${token}`; // OpenAI + init.headers["api-key"] = token; // Azure OpenAI + } else { + init.headers["Authorization"] = `Bearer ${token}`; // Microsoft & others + } } if (isGm) { diff --git a/src/views/Options/Apis.js b/src/views/Options/Apis.js new file mode 100644 index 0000000..1917fdd --- /dev/null +++ b/src/views/Options/Apis.js @@ -0,0 +1,161 @@ +import Stack from "@mui/material/Stack"; +import TextField from "@mui/material/TextField"; +import Button from "@mui/material/Button"; +import CircularProgress from "@mui/material/CircularProgress"; +import { + OPT_TRANS_ALL, + OPT_TRANS_MICROSOFT, + OPT_TRANS_OPENAI, + OPT_TRANS_CUSTOMIZE, +} from "../../config"; +import { useState } from "react"; +import { useI18n } from "../../hooks/I18n"; +import Typography from "@mui/material/Typography"; +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 { useAlert } from "../../hooks/Alert"; +import { useApi } from "../../hooks/Api"; +import { apiTranslate } from "../../apis"; + +function TestButton({ translator, api }) { + 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: "auto", + toLang: "zh-CN", + setting: api, + }); + 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 ( + + ); +} + +function ApiFields({ translator }) { + const i18n = useI18n(); + const { api, updateApi, resetApi } = useApi(translator); + const { url = "", key = "", model = "", prompt = "", headers = "" } = api; + + const handleChange = (e) => { + const { name, value } = e.target; + updateApi({ + [name]: value, + }); + }; + + return ( + + {translator !== OPT_TRANS_MICROSOFT && ( + <> + + + + )} + {translator === OPT_TRANS_OPENAI && ( + <> + + + + )} + {translator === OPT_TRANS_CUSTOMIZE && ( + + )} + + + + {translator !== OPT_TRANS_MICROSOFT && ( + + )} + + + ); +} + +function ApiAccordion({ translator }) { + const [expanded, setExpanded] = useState(false); + + const handleChange = (e) => { + setExpanded((pre) => !pre); + }; + + return ( + + }> + {translator} + + + {expanded && } + + + ); +} + +export default function Apis() { + return OPT_TRANS_ALL.map((translator) => ( + + )); +} diff --git a/src/views/Options/Navigator.js b/src/views/Options/Navigator.js index e4b1b88..7bf08d8 100644 --- a/src/views/Options/Navigator.js +++ b/src/views/Options/Navigator.js @@ -10,6 +10,7 @@ import InfoIcon from "@mui/icons-material/Info"; import DesignServicesIcon from "@mui/icons-material/DesignServices"; import { useI18n } from "../../hooks/I18n"; import SyncIcon from "@mui/icons-material/Sync"; +import ApiIcon from "@mui/icons-material/Api"; function LinkItem({ label, url, icon }) { const match = useMatch(url); @@ -36,6 +37,12 @@ export default function Navigator(props) { url: "/rules", icon: , }, + { + id: "apis_setting", + label: i18n("apis_setting"), + url: "/apis", + icon: , + }, { id: "sync", label: i18n("sync_setting"), diff --git a/src/views/Options/index.js b/src/views/Options/index.js index 16824a8..97f693b 100644 --- a/src/views/Options/index.js +++ b/src/views/Options/index.js @@ -17,6 +17,7 @@ import Divider from "@mui/material/Divider"; import Stack from "@mui/material/Stack"; import { adaptScript } from "../../libs/gm"; import Alert from "@mui/material/Alert"; +import Apis from "./Apis"; export default function Options() { const [error, setError] = useState(""); @@ -124,6 +125,7 @@ export default function Options() { }> } /> } /> + } /> } /> } />