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() {
}>
} />
} />
+ } />
} />
} />