From f908372b4ee7565cd0f7f3e732b682ff3d925862 Mon Sep 17 00:00:00 2001 From: Gabe Yuan Date: Sun, 12 May 2024 16:10:11 +0800 Subject: [PATCH] feat: support hook for custom api --- package.json | 3 +- pnpm-lock.yaml | 11 ++++++- src/apis/index.js | 28 +++++------------- src/apis/trans.js | 43 +++++++++++---------------- src/config/i18n.js | 61 ++++++++++++++++++++++++++++++--------- src/config/index.js | 4 ++- src/libs/interpreter.js | 16 ++++++++++ src/views/Options/Apis.js | 32 +++++++++++++------- 8 files changed, 125 insertions(+), 73 deletions(-) create mode 100644 src/libs/interpreter.js diff --git a/package.json b/package.json index 317482f..4aaa22e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "react-markdown": "^8.0.7", "react-router-dom": "^6.16.0", "react-scripts": "5.0.1", + "sval": "^0.5.2", "webdav": "^5.3.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: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", - "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", "eject": "react-scripts eject" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0926f89..360e4a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ dependencies: react-scripts: 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) + sval: + specifier: ^0.5.2 + version: 0.5.2 webdav: specifier: ^5.3.0 version: 5.3.0 @@ -6504,7 +6507,7 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.10.0 + acorn: 8.11.3 acorn-globals: 6.0.0 cssom: 0.4.4 cssstyle: 2.3.0 @@ -9348,6 +9351,12 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 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: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} diff --git a/src/apis/index.js b/src/apis/index.js index e41baf8..c0b842f 100644 --- a/src/apis/index.js +++ b/src/apis/index.js @@ -33,6 +33,7 @@ import { OPT_LANGS_SPECIAL, } from "../config"; 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_4: case OPT_TRANS_CUSTOMIZE_5: - trText = res.text; - isSame = to === res.from; - - const { customOption } = apiSetting; - if (customOption?.trim()) { - try { - const opt = JSON.parse(customOption); - 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}`); - } + const { resHook } = apiSetting; + if (resHook?.trim()) { + interpreter.run(`exports.resHook = ${resHook}`); + [trText, isSame] = interpreter.exports.resHook(res, text, from, to); + } else { + trText = res.text; + isSame = to === res.from; } - break; default: } diff --git a/src/apis/trans.js b/src/apis/trans.js index 331983e..b332e4b 100644 --- a/src/apis/trans.js +++ b/src/apis/trans.js @@ -32,6 +32,7 @@ import { import { msAuth } from "../libs/auth"; import { genDeeplFree } from "./deepl"; import { genBaidu } from "./baidu"; +import interpreter from "../libs/interpreter"; const keyMap = new Map(); const urlMap = new Map(); @@ -293,20 +294,27 @@ const genCloudflareAI = ({ text, from, to, url, key }) => { return [url, init]; }; -const genCustom = ({ text, from, to, url, key, customOption }) => { - const replaceInput = (str) => - str - .replaceAll(INPUT_PLACE_URL, url) - .replaceAll(INPUT_PLACE_FROM, from) - .replaceAll(INPUT_PLACE_TO, to) - .replaceAll(INPUT_PLACE_TEXT, text.replaceAll(`"`, `\n`)) - .replaceAll(INPUT_PLACE_KEY, key); +const genCustom = ({ text, from, to, url, key, reqHook }) => { + url = url + .replaceAll(INPUT_PLACE_URL, url) + .replaceAll(INPUT_PLACE_FROM, from) + .replaceAll(INPUT_PLACE_TO, to) + .replaceAll(INPUT_PLACE_TEXT, text) + .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 = { text, from, to, }; - const init = { + init = { headers: { "Content-type": "application/json", }, @@ -316,23 +324,6 @@ const genCustom = ({ text, from, to, url, key, customOption }) => { if (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]; }; diff --git a/src/config/i18n.js b/src/config/i18n.js index b74dfbe..d4dc2d5 100644 --- a/src/config/i18n.js +++ b/src/config/i18n.js @@ -42,7 +42,23 @@ const customApiLangs = `["en", "English - English"], ["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}}", "method": "POST", "headers": { @@ -50,18 +66,12 @@ const customDefaultOption = `{ "Authorization": "Bearer {{key}}" }, "body": { - "text": "{{text}}", - "from": "{{from}}", - "to": "{{to}}" + "text": "{{text}}", // 待翻译文字 + "from": "{{from}}", // 文字的语言(可能为空) + "to": "{{to}}", // 目标语言 }, - "resPattern": { - "text": "text", - "from": "from" - } -}`; +} -const customApiHelpZH = `// 自定义选项范例 -${customDefaultOption} // 返回数据默认格式 { @@ -70,20 +80,43 @@ ${customDefaultOption} to: "", // 目标语言(可选) } + +// Hook 范例 +${hookExample} + + // 支持的语言代码如下 ${customApiLangs} `; -const customApiHelpEN = `// Example of custom options -${customDefaultOption} +const customApiHelpEN = `// Default request +{ + "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 from: "", // Recognized source language to: "", // Target language (optional) } + +/// Hook Example +${hookExample} + + // The supported language codes are as follows ${customApiLangs} `; diff --git a/src/config/index.js b/src/config/index.js index a18dea7..02d7388 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -485,7 +485,9 @@ export const DEFAULT_SUBRULES_LIST = [ const defaultCustomApi = { url: "", key: "", - customOption: "", + customOption: "", // (作废) + reqHook: "", // request 钩子函数 + resHook: "", // response 钩子函数 fetchLimit: DEFAULT_FETCH_LIMIT, fetchInterval: DEFAULT_FETCH_INTERVAL, }; diff --git a/src/libs/interpreter.js b/src/libs/interpreter.js new file mode 100644 index 0000000..d6b90a8 --- /dev/null +++ b/src/libs/interpreter.js @@ -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; diff --git a/src/views/Options/Apis.js b/src/views/Options/Apis.js index 385db2d..b762a60 100644 --- a/src/views/Options/Apis.js +++ b/src/views/Options/Apis.js @@ -119,7 +119,8 @@ function ApiFields({ translator }) { fetchInterval = DEFAULT_FETCH_INTERVAL, dictNo = "", memoryNo = "", - customOption = "", + reqHook = "", + resHook = "", } = api; const handleChange = (e) => { @@ -244,15 +245,26 @@ function ApiFields({ translator }) { )} {translator.startsWith(OPT_TRANS_CUSTOMIZE) && ( - + <> + + + )}