customize api

This commit is contained in:
Gabe Yuan
2023-09-06 14:57:02 +08:00
parent f772fa000c
commit c7c5866131
10 changed files with 232 additions and 231 deletions

View File

@@ -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];
};

View File

@@ -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)`,

View File

@@ -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];

View File

@@ -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 };
}

View File

@@ -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 () => {},
});

View File

@@ -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);

View File

@@ -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 });

View File

@@ -211,6 +211,10 @@ export class Translator {
};
_register = () => {
if (this._rule.fromLang === this._rule.toLang) {
return;
}
// 搜索节点
this._queryNodes();

View File

@@ -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 <CircularProgress sx={{ marginLeft: "2em" }} size={16} />;
return <CircularProgress size={16} />;
}
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 && (
<TextField
size="small"
label={"HEADERS"}
name="headers"
value={headers}
onChange={handleChange}
multiline
/>
)}
<Stack direction="row" spacing={2}>
<TestButton translator={translator} api={api} />
@@ -131,6 +125,10 @@ function ApiFields({ translator }) {
</Button>
)}
</Stack>
{translator === OPT_TRANS_CUSTOMIZE && (
<pre>{i18n("custom_api_help")}</pre>
)}
</Stack>
);
}
@@ -155,7 +153,22 @@ function ApiAccordion({ translator }) {
}
export default function Apis() {
return OPT_TRANS_ALL.map((translator) => (
const i18n = useI18n();
return (
<Box>
<Stack spacing={3}>
<Alert severity="info">
<Link href={URL_KISS_PROXY} target="_blank">
{i18n("about_api_proxy")}
</Link>
</Alert>
<Box>
{OPT_TRANS_ALL.map((translator) => (
<ApiAccordion key={translator} translator={translator} />
));
))}
</Box>
</Stack>
</Box>
);
}

View File

@@ -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 <CircularProgress sx={{ marginLeft: "2em" }} size={12} />;
}
return (
<Link sx={{ marginLeft: "1em" }} component="button" onClick={handleApiTest}>
{i18n("click_test")}
</Link>
);
}
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() {
</Link>
</FormHelperText>
</FormControl>
<TextField
size="small"
label={
<>
{i18n("google_api")}
{googleUrl && (
<TestLink translator={OPT_TRANS_GOOGLE} setting={setting} />
)}
</>
}
name="googleUrl"
value={googleUrl}
onChange={handleChange}
helperText={
<Link href={URL_KISS_PROXY} target="_blank">
{i18n("about_api_proxy")}
</Link>
}
/>
<TextField
size="small"
label={
<>
{i18n("deepl_api")}
{deeplUrl && (
<TestLink translator={OPT_TRANS_DEEPL} setting={setting} />
)}
</>
}
name="deeplUrl"
value={deeplUrl}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("deepl_key")}
name="deeplKey"
value={deeplKey}
onChange={handleChange}
/>
<TextField
size="small"
label={
<>
{i18n("openai_api")}
{openaiUrl && openaiPrompt && (
<TestLink translator={OPT_TRANS_OPENAI} setting={setting} />
)}
</>
}
name="openaiUrl"
value={openaiUrl}
onChange={handleChange}
helperText={
<Link href={URL_KISS_PROXY} target="_blank">
{i18n("about_api_proxy")}
</Link>
}
/>
<TextField
size="small"
type="password"
label={i18n("openai_key")}
name="openaiKey"
value={openaiKey}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("openai_model")}
name="openaiModel"
value={openaiModel}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("openai_prompt")}
name="openaiPrompt"
value={openaiPrompt}
onChange={handleChange}
multiline
/>
</Stack>
</Box>
);