feat: support hook for custom api

This commit is contained in:
Gabe Yuan
2024-05-12 16:10:11 +08:00
parent 5d44ff4913
commit f908372b4e
8 changed files with 125 additions and 73 deletions

View File

@@ -17,6 +17,7 @@
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-router-dom": "^6.16.0", "react-router-dom": "^6.16.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"sval": "^0.5.2",
"webdav": "^5.3.0", "webdav": "^5.3.0",
"webextension-polyfill": "^0.10.0" "webextension-polyfill": "^0.10.0"
}, },
@@ -31,7 +32,7 @@
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/", "build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
"build:rules": "babel-node src/rules.js", "build:rules": "babel-node src/rules.js",
"build": "pnpm build:chrome && pnpm build:edge && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules", "build": "pnpm build:chrome && pnpm build:edge && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
"pack": "cd build && zip -r chrome.zip chrome && zip -r edge.zip edge && cd firefox && zip -r ../firefox.zip .", "zip": "cd build && zip -r chrome.zip chrome && zip -r edge.zip edge && cd firefox && zip -r ../firefox.zip .",
"test": "react-app-rewired test", "test": "react-app-rewired test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },

11
pnpm-lock.yaml generated
View File

@@ -41,6 +41,9 @@ dependencies:
react-scripts: react-scripts:
specifier: 5.0.1 specifier: 5.0.1
version: 5.0.1(@babel/plugin-syntax-flow@7.24.1)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.57.0)(react@18.2.0)(typescript@5.4.5) version: 5.0.1(@babel/plugin-syntax-flow@7.24.1)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.57.0)(react@18.2.0)(typescript@5.4.5)
sval:
specifier: ^0.5.2
version: 0.5.2
webdav: webdav:
specifier: ^5.3.0 specifier: ^5.3.0
version: 5.3.0 version: 5.3.0
@@ -6504,7 +6507,7 @@ packages:
optional: true optional: true
dependencies: dependencies:
abab: 2.0.6 abab: 2.0.6
acorn: 8.10.0 acorn: 8.11.3
acorn-globals: 6.0.0 acorn-globals: 6.0.0
cssom: 0.4.4 cssom: 0.4.4
cssstyle: 2.3.0 cssstyle: 2.3.0
@@ -9348,6 +9351,12 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
/sval@0.5.2:
resolution: {integrity: sha512-cMN4SWqQ8K2DypYVZ1DVsicvXsr4gQmAYR2faKwHttJFJAqjfc4+taG9esMIP0hMP5+4Caun99n6y+4T6nCPEA==}
dependencies:
acorn: 8.11.3
dev: false
/svg-parser@2.0.4: /svg-parser@2.0.4:
resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==}

View File

@@ -33,6 +33,7 @@ import {
OPT_LANGS_SPECIAL, OPT_LANGS_SPECIAL,
} from "../config"; } from "../config";
import { sha256 } from "../libs/utils"; import { sha256 } from "../libs/utils";
import interpreter from "../libs/interpreter";
/** /**
* 同步数据 * 同步数据
@@ -275,27 +276,14 @@ export const apiTranslate = async ({
case OPT_TRANS_CUSTOMIZE_3: case OPT_TRANS_CUSTOMIZE_3:
case OPT_TRANS_CUSTOMIZE_4: case OPT_TRANS_CUSTOMIZE_4:
case OPT_TRANS_CUSTOMIZE_5: case OPT_TRANS_CUSTOMIZE_5:
trText = res.text; const { resHook } = apiSetting;
isSame = to === res.from; if (resHook?.trim()) {
interpreter.run(`exports.resHook = ${resHook}`);
const { customOption } = apiSetting; [trText, isSame] = interpreter.exports.resHook(res, text, from, to);
if (customOption?.trim()) { } else {
try { trText = res.text;
const opt = JSON.parse(customOption); isSame = to === res.from;
const textPattern = opt.resPattern?.text;
const fromPattern = opt.resPattern?.from;
if (textPattern) {
trText = textPattern.split(".").reduce((pre, cur) => pre[cur], res);
}
if (fromPattern) {
isSame =
to === fromPattern.split(".").reduce((pre, cur) => pre[cur], res);
}
} catch (err) {
throw new Error(`custom option parse err: ${err}`);
}
} }
break; break;
default: default:
} }

View File

@@ -32,6 +32,7 @@ import {
import { msAuth } from "../libs/auth"; import { msAuth } from "../libs/auth";
import { genDeeplFree } from "./deepl"; import { genDeeplFree } from "./deepl";
import { genBaidu } from "./baidu"; import { genBaidu } from "./baidu";
import interpreter from "../libs/interpreter";
const keyMap = new Map(); const keyMap = new Map();
const urlMap = new Map(); const urlMap = new Map();
@@ -293,20 +294,27 @@ const genCloudflareAI = ({ text, from, to, url, key }) => {
return [url, init]; return [url, init];
}; };
const genCustom = ({ text, from, to, url, key, customOption }) => { const genCustom = ({ text, from, to, url, key, reqHook }) => {
const replaceInput = (str) => url = url
str .replaceAll(INPUT_PLACE_URL, url)
.replaceAll(INPUT_PLACE_URL, url) .replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_FROM, from) .replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TO, to) .replaceAll(INPUT_PLACE_TEXT, text)
.replaceAll(INPUT_PLACE_TEXT, text.replaceAll(`"`, `\n`)) .replaceAll(INPUT_PLACE_KEY, key);
.replaceAll(INPUT_PLACE_KEY, key); let init = {};
if (reqHook?.trim()) {
interpreter.run(`exports.reqHook = ${reqHook}`);
[url, init] = interpreter.exports.reqHook(text, from, to, url, key);
return [url, init];
}
const data = { const data = {
text, text,
from, from,
to, to,
}; };
const init = { init = {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
}, },
@@ -316,23 +324,6 @@ const genCustom = ({ text, from, to, url, key, customOption }) => {
if (key) { if (key) {
init.headers.Authorization = `Bearer ${key}`; init.headers.Authorization = `Bearer ${key}`;
} }
url = replaceInput(url);
if (customOption?.trim()) {
try {
const opt = JSON.parse(replaceInput(customOption));
opt.url && (url = opt.url);
opt.headers && (init.headers = opt.headers);
opt.method && (init.method = opt.method);
if (init.method === "GET") {
delete init.body;
} else {
opt.body && (init.body = JSON.stringify(opt.body));
}
} catch (err) {
throw new Error(`custom option parse err: ${err}`);
}
}
return [url, init]; return [url, init];
}; };

View File

@@ -42,7 +42,23 @@ const customApiLangs = `["en", "English - English"],
["vi", "Vietnamese - Tiếng Việt"], ["vi", "Vietnamese - Tiếng Việt"],
`; `;
const customDefaultOption = `{ const hookExample = `// URL
https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN
// Request Hook
(text, from, to, url, key) => [url, {
headers: {
"Content-type": "application/json",
},
method: "GET",
body: null,
}]
// Response Hook
(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]`;
const customApiHelpZH = `// 请求数据默认格式
{
"url": "{{url}}", "url": "{{url}}",
"method": "POST", "method": "POST",
"headers": { "headers": {
@@ -50,18 +66,12 @@ const customDefaultOption = `{
"Authorization": "Bearer {{key}}" "Authorization": "Bearer {{key}}"
}, },
"body": { "body": {
"text": "{{text}}", "text": "{{text}}", // 待翻译文字
"from": "{{from}}", "from": "{{from}}", // 文字的语言(可能为空)
"to": "{{to}}" "to": "{{to}}", // 目标语言
}, },
"resPattern": { }
"text": "text",
"from": "from"
}
}`;
const customApiHelpZH = `// 自定义选项范例
${customDefaultOption}
// 返回数据默认格式 // 返回数据默认格式
{ {
@@ -70,20 +80,43 @@ ${customDefaultOption}
to: "", // 目标语言(可选) to: "", // 目标语言(可选)
} }
// Hook 范例
${hookExample}
// 支持的语言代码如下 // 支持的语言代码如下
${customApiLangs} ${customApiLangs}
`; `;
const customApiHelpEN = `// Example of custom options const customApiHelpEN = `// Default request
${customDefaultOption} {
"url": "{{url}}",
"method": "POST",
"headers": {
"Content-type": "application/json",
"Authorization": "Bearer {{key}}"
},
"body": {
"text": "{{text}}", // Text to be translated
"from": "{{from}}", // The language of the text (may be empty)
"to": "{{to}}", // Target language
},
}
// Return data default format
// Default response
{ {
text: "", // translated text text: "", // translated text
from: "", // Recognized source language from: "", // Recognized source language
to: "", // Target language (optional) to: "", // Target language (optional)
} }
/// Hook Example
${hookExample}
// The supported language codes are as follows // The supported language codes are as follows
${customApiLangs} ${customApiLangs}
`; `;

View File

@@ -485,7 +485,9 @@ export const DEFAULT_SUBRULES_LIST = [
const defaultCustomApi = { const defaultCustomApi = {
url: "", url: "",
key: "", key: "",
customOption: "", customOption: "", // (作废)
reqHook: "", // request 钩子函数
resHook: "", // response 钩子函数
fetchLimit: DEFAULT_FETCH_LIMIT, fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL, fetchInterval: DEFAULT_FETCH_INTERVAL,
}; };

16
src/libs/interpreter.js Normal file
View File

@@ -0,0 +1,16 @@
import Sval from "sval";
const interpreter = new Sval({
// ECMA Version of the code
// 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15
// or 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024
// or "latest"
ecmaVer: "latest",
// Code source type
// "script" or "module"
sourceType: "script",
// Whether the code runs in a sandbox
sandBox: true,
});
export default interpreter;

View File

@@ -119,7 +119,8 @@ function ApiFields({ translator }) {
fetchInterval = DEFAULT_FETCH_INTERVAL, fetchInterval = DEFAULT_FETCH_INTERVAL,
dictNo = "", dictNo = "",
memoryNo = "", memoryNo = "",
customOption = "", reqHook = "",
resHook = "",
} = api; } = api;
const handleChange = (e) => { const handleChange = (e) => {
@@ -244,15 +245,26 @@ function ApiFields({ translator }) {
)} )}
{translator.startsWith(OPT_TRANS_CUSTOMIZE) && ( {translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
<TextField <>
size="small" <TextField
label={i18n("custom_option")} size="small"
name="customOption" label={"Request Hook"}
value={customOption} name="reqHook"
onChange={handleChange} value={reqHook}
multiline onChange={handleChange}
maxRows={10} multiline
/> maxRows={10}
/>
<TextField
size="small"
label={"Response Hook"}
name="resHook"
value={resHook}
onChange={handleChange}
multiline
maxRows={10}
/>
</>
)} )}
<TextField <TextField