diff --git a/src/apis/index.js b/src/apis/index.js index c899457..1e57ff6 100644 --- a/src/apis/index.js +++ b/src/apis/index.js @@ -214,10 +214,9 @@ export const apiTranslate = async ({ } const { apiType, apiSlug, useBatchFetch } = apiSetting; - const from = - OPT_LANGS_SPECIAL[apiType].get(fromLang) ?? - OPT_LANGS_SPECIAL[apiType].get("auto"); - const to = OPT_LANGS_SPECIAL[apiType].get(toLang); + const langMap = OPT_LANGS_SPECIAL[apiType]; + const from = langMap.get(fromLang) ?? langMap.get("auto"); + const to = langMap.get(toLang); if (!to) { kissLog(`target lang: ${toLang} not support`); return ["", false]; @@ -249,6 +248,9 @@ export const apiTranslate = async ({ const queue = getBatchQueue({ from, to, + fromLang, + toLang, + langMap, docInfo, apiSetting, usePool, @@ -257,22 +259,32 @@ export const apiTranslate = async ({ const tranlation = await queue.addTask({ text }); if (Array.isArray(tranlation)) { [trText, srLang = ""] = tranlation; + } else if (typeof tranlation === "string") { + trText = tranlation; } } else { const translations = await handleTranslate({ texts: [text], from, to, + fromLang, + toLang, + langMap, docInfo, apiSetting, usePool, }); - if (Array.isArray(translations?.[0])) { - [trText, srLang = ""] = translations[0]; + if (Array.isArray(translations)) { + if (Array.isArray(translations[0])) { + [trText, srLang = ""] = translations[0]; + } else { + [trText, srLang = ""] = translations; + } } } - const isSame = srLang && (to.includes(srLang) || srLang.includes(to)); + // const isSame = srLang && (to.includes(srLang) || srLang.includes(to)); + const isSame = srLang && srLang.slice(0, 2) === to.slice(0, 2); // 插入缓存 if (useCache && trText) { diff --git a/src/apis/trans.js b/src/apis/trans.js index 9b70949..b1b620a 100644 --- a/src/apis/trans.js +++ b/src/apis/trans.js @@ -111,7 +111,7 @@ const parseAIRes = (raw) => { }; const genGoogle = ({ texts, from, to, url, key }) => { - const params = { + const params = queryString.stringify({ client: "gtx", dt: "t", dj: 1, @@ -119,52 +119,42 @@ const genGoogle = ({ texts, from, to, url, key }) => { sl: from, tl: to, q: texts.join(" "), - }; - const input = `${url}?${queryString.stringify(params)}`; - const init = { - headers: { - "Content-type": "application/json", - }, + }); + url = `${url}?${params}`; + const headers = { + "Content-type": "application/json", }; if (key) { - init.headers.Authorization = `Bearer ${key}`; + headers.Authorization = `Bearer ${key}`; } - return [input, init]; + return { url, headers, method: "GET" }; }; const genGoogle2 = ({ texts, from, to, url, key }) => { - const body = JSON.stringify([[texts, from, to], "wt_lib"]); - const init = { - method: "POST", - headers: { - "Content-Type": "application/json+protobuf", - "X-Goog-API-Key": key, - }, - body, + const data = [[texts, from, to], "wt_lib"]; + const headers = { + "Content-Type": "application/json+protobuf", + "X-Goog-API-Key": key, }; - return [url, init]; + return { url, data, headers }; }; -const genMicrosoft = async ({ texts, from, to }) => { - const [token] = await msAuth(); - const params = { +const genMicrosoft = ({ texts, from, to, token }) => { + const params = queryString.stringify({ from, to, "api-version": "3.0", + }); + const url = `https://api-edge.cognitive.microsofttranslator.com/translate?${params}`; + const headers = { + "Content-type": "application/json", + Authorization: `Bearer ${token}`, }; - const input = `https://api-edge.cognitive.microsofttranslator.com/translate?${queryString.stringify(params)}`; - const init = { - headers: { - "Content-type": "application/json", - Authorization: `Bearer ${token}`, - }, - method: "POST", - body: JSON.stringify(texts.map((text) => ({ Text: text }))), - }; + const data = texts.map((text) => ({ Text: text })); - return [input, init]; + return { url, data, headers }; }; const genDeepl = ({ texts, from, to, url, key }) => { @@ -174,16 +164,12 @@ const genDeepl = ({ texts, from, to, url, key }) => { source_lang: from, // split_sentences: "0", }; - const init = { - headers: { - "Content-type": "application/json", - Authorization: `DeepL-Auth-Key ${key}`, - }, - method: "POST", - body: JSON.stringify(data), + const headers = { + "Content-type": "application/json", + Authorization: `DeepL-Auth-Key ${key}`, }; - return [url, init]; + return { url, data, headers }; }; const genDeeplX = ({ texts, from, to, url, key }) => { @@ -193,18 +179,14 @@ const genDeeplX = ({ texts, from, to, url, key }) => { source_lang: from, }; - const init = { - headers: { - "Content-type": "application/json", - }, - method: "POST", - body: JSON.stringify(data), + const headers = { + "Content-type": "application/json", }; if (key) { - init.headers.Authorization = `Bearer ${key}`; + headers.Authorization = `Bearer ${key}`; } - return [url, init]; + return { url, data, headers }; }; const genNiuTrans = ({ texts, from, to, url, key, dictNo, memoryNo }) => { @@ -217,15 +199,11 @@ const genNiuTrans = ({ texts, from, to, url, key, dictNo, memoryNo }) => { memoryNo, }; - const init = { - headers: { - "Content-type": "application/json", - }, - method: "POST", - body: JSON.stringify(data), + const headers = { + "Content-type": "application/json", }; - return [url, init]; + return { url, data, headers }; }; const genTencent = ({ texts, from, to }) => { @@ -246,19 +224,15 @@ const genTencent = ({ texts, from, to }) => { }, }; - const input = "https://transmart.qq.com/api/imt"; - const init = { - headers: { - "Content-Type": "application/json", - "user-agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", - referer: "https://transmart.qq.com/zh-CN/index", - }, - method: "POST", - body: JSON.stringify(data), + const url = "https://transmart.qq.com/api/imt"; + const headers = { + "Content-Type": "application/json", + "user-agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", + referer: "https://transmart.qq.com/zh-CN/index", }; - return [input, init]; + return { url, data, headers }; }; const genVolcengine = ({ texts, from, to }) => { @@ -268,22 +242,15 @@ const genVolcengine = ({ texts, from, to }) => { text: texts.join(" "), }; - const input = "https://translate.volcengine.com/crx/translate/v1"; - const init = { - headers: { - "Content-type": "application/json", - }, - method: "POST", - body: JSON.stringify(data), + const url = "https://translate.volcengine.com/crx/translate/v1"; + const headers = { + "Content-type": "application/json", }; - return [input, init]; + return { url, data, headers }; }; const genOpenAI = ({ - texts, - from, - to, url, key, systemPrompt, @@ -291,16 +258,8 @@ const genOpenAI = ({ model, temperature, maxTokens, - customHeader, - customBody, - docInfo, hisMsgs, }) => { - systemPrompt = genSystemPrompt({ systemPrompt, from, to }); - userPrompt = genUserPrompt({ userPrompt, from, to, texts, docInfo }); - customHeader = parseJsonObj(customHeader); - customBody = parseJsonObj(customBody); - const userMsg = { role: "user", content: userPrompt, @@ -317,27 +276,18 @@ const genOpenAI = ({ ], temperature, max_completion_tokens: maxTokens, - ...customBody, }; - const init = { - headers: { - "Content-type": "application/json", - Authorization: `Bearer ${key}`, // OpenAI - "api-key": key, // Azure OpenAI - ...customHeader, - }, - method: "POST", - body: JSON.stringify(data), + const headers = { + "Content-type": "application/json", + Authorization: `Bearer ${key}`, // OpenAI + // "api-key": key, // Azure OpenAI }; - return [url, init, userMsg]; + return { url, data, headers, userMsg }; }; const genGemini = ({ - texts, - from, - to, url, key, systemPrompt, @@ -345,18 +295,11 @@ const genGemini = ({ model, temperature, maxTokens, - customHeader, - customBody, - docInfo, hisMsgs, }) => { url = url .replaceAll(INPUT_PLACE_MODEL, model) .replaceAll(INPUT_PLACE_KEY, key); - systemPrompt = genSystemPrompt({ systemPrompt, from, to }); - userPrompt = genUserPrompt({ userPrompt, from, to, texts, docInfo }); - customHeader = parseJsonObj(customHeader); - customBody = parseJsonObj(customBody); const userMsg = { role: "user", parts: [{ text: userPrompt }] }; const data = { @@ -393,25 +336,15 @@ const genGemini = ({ threshold: "BLOCK_NONE", }, ], - ...customBody, + }; + const headers = { + "Content-type": "application/json", }; - const init = { - headers: { - "Content-type": "application/json", - ...customHeader, - }, - method: "POST", - body: JSON.stringify(data), - }; - - return [url, init, userMsg]; + return { url, data, headers, userMsg }; }; const genGemini2 = ({ - texts, - from, - to, url, key, systemPrompt, @@ -419,16 +352,8 @@ const genGemini2 = ({ model, temperature, maxTokens, - customHeader, - customBody, - docInfo, hisMsgs, }) => { - systemPrompt = genSystemPrompt({ systemPrompt, from, to }); - userPrompt = genUserPrompt({ userPrompt, from, to, texts, docInfo }); - customHeader = parseJsonObj(customHeader); - customBody = parseJsonObj(customBody); - const userMsg = { role: "user", content: userPrompt, @@ -445,26 +370,17 @@ const genGemini2 = ({ ], temperature, max_tokens: maxTokens, - ...customBody, }; - const init = { - headers: { - "Content-type": "application/json", - Authorization: `Bearer ${key}`, - ...customHeader, - }, - method: "POST", - body: JSON.stringify(data), + const headers = { + "Content-type": "application/json", + Authorization: `Bearer ${key}`, }; - return [url, init, userMsg]; + return { url, data, headers, userMsg }; }; const genClaude = ({ - texts, - from, - to, url, key, systemPrompt, @@ -472,16 +388,8 @@ const genClaude = ({ model, temperature, maxTokens, - customHeader, - customBody, - docInfo, hisMsgs, }) => { - systemPrompt = genSystemPrompt({ systemPrompt, from, to }); - userPrompt = genUserPrompt({ userPrompt, from, to, texts, docInfo }); - customHeader = parseJsonObj(customHeader); - customBody = parseJsonObj(customBody); - const userMsg = { role: "user", content: userPrompt, @@ -492,28 +400,19 @@ const genClaude = ({ messages: [...hisMsgs, userMsg], temperature, max_tokens: maxTokens, - ...customBody, }; - const init = { - headers: { - "Content-type": "application/json", - "anthropic-version": "2023-06-01", - "anthropic-dangerous-direct-browser-access": "true", - "x-api-key": key, - ...customHeader, - }, - method: "POST", - body: JSON.stringify(data), + const headers = { + "Content-type": "application/json", + "anthropic-version": "2023-06-01", + "anthropic-dangerous-direct-browser-access": "true", + "x-api-key": key, }; - return [url, init, userMsg]; + return { url, data, headers, userMsg }; }; const genOpenRouter = ({ - texts, - from, - to, url, key, systemPrompt, @@ -521,16 +420,8 @@ const genOpenRouter = ({ model, temperature, maxTokens, - customHeader, - customBody, - docInfo, hisMsgs, }) => { - systemPrompt = genSystemPrompt({ systemPrompt, from, to }); - userPrompt = genUserPrompt({ userPrompt, from, to, texts, docInfo }); - customHeader = parseJsonObj(customHeader); - customBody = parseJsonObj(customBody); - const userMsg = { role: "user", content: userPrompt, @@ -547,26 +438,17 @@ const genOpenRouter = ({ ], temperature, max_tokens: maxTokens, - ...customBody, }; - const init = { - headers: { - "Content-type": "application/json", - Authorization: `Bearer ${key}`, - ...customHeader, - }, - method: "POST", - body: JSON.stringify(data), + const headers = { + "Content-type": "application/json", + Authorization: `Bearer ${key}`, }; - return [url, init, userMsg]; + return { url, data, headers, userMsg }; }; const genOllama = ({ - texts, - from, - to, think, url, key, @@ -575,16 +457,8 @@ const genOllama = ({ model, temperature, maxTokens, - customHeader, - customBody, - docInfo, hisMsgs, }) => { - systemPrompt = genSystemPrompt({ systemPrompt, from, to }); - userPrompt = genUserPrompt({ userPrompt, from, to, texts, docInfo }); - customHeader = parseJsonObj(customHeader); - customBody = parseJsonObj(customBody); - const userMsg = { role: "user", content: userPrompt, @@ -603,22 +477,16 @@ const genOllama = ({ max_tokens: maxTokens, think, stream: false, - ...customBody, }; - const init = { - headers: { - "Content-type": "application/json", - ...customHeader, - }, - method: "POST", - body: JSON.stringify(data), + const headers = { + "Content-type": "application/json", }; if (key) { - init.headers.Authorization = `Bearer ${key}`; + headers.Authorization = `Bearer ${key}`; } - return [url, init, userMsg]; + return { url, data, headers, userMsg }; }; const genCloudflareAI = ({ texts, from, to, url, key }) => { @@ -628,52 +496,65 @@ const genCloudflareAI = ({ texts, from, to, url, key }) => { target_lang: to, }; - const init = { - headers: { - "Content-type": "application/json", - Authorization: `Bearer ${key}`, - }, - method: "POST", - body: JSON.stringify(data), + const headers = { + "Content-type": "application/json", + Authorization: `Bearer ${key}`, }; - return [url, init]; + return { url, data, headers }; }; -const genCustom = ({ - texts, - from, - to, - url, - key, - reqHook, - docInfo, - hisMsgs, -}) => { - if (reqHook?.trim()) { - interpreter.run(`exports.reqHook = ${reqHook}`); - return interpreter.exports.reqHook({ - texts, - from, - to, - url, - key, - docInfo, - hisMsgs, - }); - } - +const genCustom = ({ texts, from, to, url, key }) => { const data = { texts, from, to }; - const init = { - headers: { - "Content-type": "application/json", - Authorization: `Bearer ${key}`, - }, - method: "POST", - body: JSON.stringify(data), + const headers = { + "Content-type": "application/json", + Authorization: `Bearer ${key}`, }; - return [url, init]; + return { url, data, headers }; +}; + +const genReqFuncs = { + [OPT_TRANS_GOOGLE]: genGoogle, + [OPT_TRANS_GOOGLE_2]: genGoogle2, + [OPT_TRANS_MICROSOFT]: genMicrosoft, + [OPT_TRANS_DEEPL]: genDeepl, + [OPT_TRANS_DEEPLFREE]: genDeeplFree, + [OPT_TRANS_DEEPLX]: genDeeplX, + [OPT_TRANS_NIUTRANS]: genNiuTrans, + [OPT_TRANS_BAIDU]: genBaidu, + [OPT_TRANS_TENCENT]: genTencent, + [OPT_TRANS_VOLCENGINE]: genVolcengine, + [OPT_TRANS_OPENAI]: genOpenAI, + [OPT_TRANS_GEMINI]: genGemini, + [OPT_TRANS_GEMINI_2]: genGemini2, + [OPT_TRANS_CLAUDE]: genClaude, + [OPT_TRANS_CLOUDFLAREAI]: genCloudflareAI, + [OPT_TRANS_OLLAMA]: genOllama, + [OPT_TRANS_OPENROUTER]: genOpenRouter, + [OPT_TRANS_CUSTOMIZE]: genCustom, +}; + +const genInit = ({ + url = "", + data = null, + headers = {}, + userMsg = null, + method = "POST", +}) => { + if (!url) { + throw new Error("genInit: url is empty"); + } + + const init = { + method, + headers, + }; + if (method !== "GET" && method !== "HEAD" && data) { + Object.assign(init, { body: JSON.stringify(data) }); + } + + return [url, init, userMsg]; }; /** @@ -681,66 +562,70 @@ const genCustom = ({ * @param {*} * @returns */ -export const genTransReq = ({ apiType, apiSlug, ...args }) => { - switch (apiType) { - case OPT_TRANS_DEEPL: - case OPT_TRANS_OPENAI: - case OPT_TRANS_GEMINI: - case OPT_TRANS_GEMINI_2: - case OPT_TRANS_CLAUDE: - case OPT_TRANS_CLOUDFLAREAI: - case OPT_TRANS_OLLAMA: - case OPT_TRANS_OPENROUTER: - case OPT_TRANS_NIUTRANS: - case OPT_TRANS_CUSTOMIZE: - args.key = keyPick(apiSlug, args.key, keyMap); - break; - case OPT_TRANS_DEEPLX: - args.url = keyPick(apiSlug, args.url, urlMap); - break; - default: +export const genTransReq = async ({ reqHook, resHook, ...args }) => { + const { + apiType, + apiSlug, + key, + systemPrompt, + userPrompt, + from, + to, + texts, + docInfo, + customHeader, + customBody, + } = args; + + if (API_SPE_TYPES.mulkeys.has(apiType)) { + args.key = keyPick(apiSlug, key, keyMap); } - switch (apiType) { - case OPT_TRANS_GOOGLE: - return genGoogle(args); - case OPT_TRANS_GOOGLE_2: - return genGoogle2(args); - case OPT_TRANS_MICROSOFT: - return genMicrosoft(args); - case OPT_TRANS_DEEPL: - return genDeepl(args); - case OPT_TRANS_DEEPLFREE: - return genDeeplFree(args); - case OPT_TRANS_DEEPLX: - return genDeeplX(args); - case OPT_TRANS_NIUTRANS: - return genNiuTrans(args); - case OPT_TRANS_BAIDU: - return genBaidu(args); - case OPT_TRANS_TENCENT: - return genTencent(args); - case OPT_TRANS_VOLCENGINE: - return genVolcengine(args); - case OPT_TRANS_OPENAI: - return genOpenAI(args); - case OPT_TRANS_GEMINI: - return genGemini(args); - case OPT_TRANS_GEMINI_2: - return genGemini2(args); - case OPT_TRANS_CLAUDE: - return genClaude(args); - case OPT_TRANS_CLOUDFLAREAI: - return genCloudflareAI(args); - case OPT_TRANS_OLLAMA: - return genOllama(args); - case OPT_TRANS_OPENROUTER: - return genOpenRouter(args); - case OPT_TRANS_CUSTOMIZE: - return genCustom(args); - default: - throw new Error(`[trans] ${apiType} not support`); + if (apiType === OPT_TRANS_DEEPLX) { + args.url = keyPick(apiSlug, args.url, urlMap); } + + if (API_SPE_TYPES.ai.has(apiType)) { + args.systemPrompt = genSystemPrompt({ systemPrompt, from, to }); + args.userPrompt = genUserPrompt({ userPrompt, from, to, texts, docInfo }); + } + + const { + url = "", + data = null, + headers = {}, + userMsg = null, + method = "POST", + } = genReqFuncs[apiType](args); + + // 合并用户自定义headers和body + if (customHeader?.trim()) { + Object.assign(headers, parseJsonObj(customHeader)); + } + if (customBody?.trim()) { + Object.assign(data, parseJsonObj(customBody)); + } + + // 执行 request hook + if (reqHook?.trim()) { + try { + interpreter.run(`exports.reqHook = ${reqHook}`); + const hookResult = await interpreter.exports.reqHook(args, { + url, + data, + headers, + userMsg, + method, + }); + if (hookResult && hookResult.url) { + return genInit(hookResult); + } + } catch (err) { + kissLog("run req hook", err); + } + } + + return genInit({ url, data, headers, userMsg, method }); }; /** @@ -749,10 +634,48 @@ export const genTransReq = ({ apiType, apiSlug, ...args }) => { * @param {*} param3 * @returns */ -export const parseTransRes = ( +export const parseTransRes = async ( res, - { texts, from, to, resHook, thinkIgnore, history, userMsg, apiType } + { + texts, + from, + to, + fromLang, + toLang, + langMap, + resHook, + thinkIgnore, + history, + userMsg, + apiType, + } ) => { + // 执行 response hook + if (resHook?.trim()) { + try { + interpreter.run(`exports.resHook = ${resHook}`); + const hookResult = await interpreter.exports.resHook({ + apiType, + userMsg, + res, + texts, + from, + to, + fromLang, + toLang, + langMap, + }); + if (hookResult && Array.isArray(hookResult.translations)) { + if (history && userMsg && hookResult.modelMsg) { + history.add(userMsg, hookResult.modelMsg); + } + return hookResult.translations; + } + } catch (err) { + kissLog("run res hook", err); + } + } + let modelMsg = ""; switch (apiType) { @@ -832,7 +755,9 @@ export const parseTransRes = ( case OPT_TRANS_OLLAMA: modelMsg = res?.choices?.[0]?.message; - const deepModels = thinkIgnore.split(",").filter((model) => model.trim()); + const deepModels = thinkIgnore + .split(",") + .filter((model) => model?.trim()); if (deepModels.some((model) => res?.model?.startsWith(model))) { modelMsg?.content.replace(/[\s\S]*<\/think>/i, ""); } @@ -845,23 +770,7 @@ export const parseTransRes = ( } return parseAIRes(modelMsg?.content); case OPT_TRANS_CUSTOMIZE: - if (resHook?.trim()) { - interpreter.run(`exports.resHook = ${resHook}`); - if (history) { - const [translations, modelMsg] = interpreter.exports.resHook({ - res, - texts, - from, - to, - }); - userMsg && modelMsg && history.add(userMsg, modelMsg); - return translations; - } else { - return interpreter.exports.resHook({ res, texts, from, to }); - } - } else { - return res?.map((item) => [item.text, item.src]); - } + return res?.map((item) => [item.text, item.src]); default: } @@ -877,6 +786,9 @@ export const handleTranslate = async ({ texts, from, to, + fromLang, + toLang, + langMap, docInfo, apiSetting, usePool, @@ -897,12 +809,21 @@ export const handleTranslate = async ({ hisMsgs = history.getAll(); } + let token = ""; + if (apiType === OPT_TRANS_MICROSOFT) { + [token] = await msAuth(); + } + const [input, init, userMsg] = await genTransReq({ texts, from, to, + fromLang, + toLang, + langMap, docInfo, hisMsgs, + token, ...apiSetting, }); @@ -921,6 +842,9 @@ export const handleTranslate = async ({ texts, from, to, + fromLang, + toLang, + langMap, history, userMsg, ...apiSetting, diff --git a/src/config/api.js b/src/config/api.js index 8668cc4..43cbf23 100644 --- a/src/config/api.js +++ b/src/config/api.js @@ -350,6 +350,18 @@ Output: {"translations":[{"id":1,"text":"一个React组件","sourceLangua Fail-safe: On any error, return {"translations":[]}.`; +const defaultRequestHook = `async (args, { url, data, headers, userMsg, method } = {}) => { + console.log("request hook args:", args); + // return { url, data, headers, userMsg, method }; +}`; + +const defaultResponseHook = `async ({ res, ...args }) => { + console.log("reaponse hook args:", res, args); + // const translations = [["你好", "zh"]]; + // const modelMsg = ""; + // return { translations, modelMsg }; +}`; + // 翻译接口默认参数 const defaultApi = { apiSlug: "", // 唯一标识 @@ -473,16 +485,8 @@ const defaultApiOpts = { [OPT_TRANS_CUSTOMIZE]: { ...defaultApi, url: "https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN", - reqHook: `// Request Hook -(text, from, to, url, key) => [url, { - headers: { - "Content-type": "application/json", - }, - method: "GET", - body: null, -}]`, - resHook: `// Response Hook -(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]`, + reqHook: defaultRequestHook, + resHook: defaultResponseHook, }, }; diff --git a/src/config/i18n.js b/src/config/i18n.js index 4a18f45..1cf60ad 100644 --- a/src/config/i18n.js +++ b/src/config/i18n.js @@ -136,6 +136,52 @@ https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF- ${customApiLangs} `; +const requestHookHelperZH = `1、第一个参数包含如下字段:'texts', 'from', 'to', 'url', 'key', 'model', 'systemPrompt', ... +2、返回值必须是包含以下字段的对象: 'url', 'data', 'headers', 'userMsg', 'method' +3、如返回空值,则hook函数不会产生任何效果。 + +// 示例 +async (args, { url, data, headers, userMsg, method } = {}) => { + console.log("request hook args:", args); + return { url, data, headers, userMsg, method }; +}`; + +const requestHookHelperEN = `1. The first parameter contains the following fields: 'texts', 'from', 'to', 'url', 'key', 'model', 'systemPrompt', ... +2. The return value must be an object containing the following fields: 'url', 'data', 'headers', 'userMsg', 'method' +3. If a null value is returned, the hook function will have no effect. + +// Example +async (args, { url, data, headers, userMsg, method } = {}) => { + console.log("request hook args:", args); + return { url, data, headers, userMsg, method }; +}`; + +const responsetHookHelperZH = `1、第一个参数包含如下字段:'res', ... +2、返回值必须是包含以下字段的对象: 'translations', 'modelMsg' + ('translations' 应为一个二维数组,表示 [译文,源语言] 的列表) +3、如返回空值,则hook函数不会产生任何效果。 + +// 示例 +async ({ res, ...args }) => { + console.log("reaponse hook args:", res, args); + const translations = [["你好", "zh"]]; + const modelMsg = ""; + return { translations, modelMsg }; +}`; + +const responsetHookHelperEN = `1. The first parameter contains the following fields: 'res', ... +2. The return value must be an object containing the following fields: 'translations', 'modelMsg' + ('translations' should be a two-dimensional array representing a list of [translation, source language]). +3. If a null value is returned, the hook function will have no effect. + +// Example +async ({ res, ...args }) => { + console.log("reaponse hook args:", res, args); + const translations = [["你好", "zh"]]; + const modelMsg = ""; + return { translations, modelMsg }; +}`; + export const I18N = { app_name: { zh: `简约翻译`, @@ -152,6 +198,16 @@ export const I18N = { en: customApiHelpEN, zh_TW: customApiHelpZH, }, + request_hook_helper: { + zh: requestHookHelperZH, + en: requestHookHelperEN, + zh_TW: requestHookHelperZH, + }, + response_hook_helper: { + zh: responsetHookHelperZH, + en: responsetHookHelperEN, + zh_TW: responsetHookHelperZH, + }, translate_alt: { zh: `翻译`, en: `Translate`, @@ -613,9 +669,9 @@ export const I18N = { zh_TW: `選擇器節點樣式`, }, selector_style_helper: { - zh: `开启翻译时注入,关闭翻译时不会移除。`, - en: `It is injected when translation is turned on and will not be removed when translation is turned off.`, - zh_TW: `在開啟翻譯時注入,關閉翻譯時不會移除。`, + zh: `开启翻译时注入。`, + en: `It is injected when translation is turned on.`, + zh_TW: `在開啟翻譯時注入。`, }, selector_parent_style: { zh: `选择器父节点样式`, @@ -1213,9 +1269,9 @@ export const I18N = { zh_TW: `翻譯開始 Hook`, }, translate_start_hook_helper: { - zh: `翻译前时运行,入参为: 翻译节点列表。`, - en: `Run before translation, input parameters are: translation node list.`, - zh_TW: `翻譯前時運行,入參為: 翻譯節點清單。`, + zh: `翻译前时运行,入参为: ({hostNode, parentNode, nodes})`, + en: `Run before translation, input parameters are: ({hostNode, parentNode, nodes})`, + zh_TW: `翻譯前時運行,入參為: ({hostNode, parentNode, nodes})`, }, translate_end_hook: { zh: `翻译完成钩子函数`, @@ -1223,9 +1279,9 @@ export const I18N = { zh_TW: `翻譯完成 Hook`, }, translate_end_hook_helper: { - zh: `翻译完成时运行,入参为: 翻译节点列表。`, - en: `Run when translation is complete, input parameters are: translation node list.`, - zh_TW: `翻譯完成時運行,入參為: 翻譯節點清單。`, + zh: `翻译完成时运行,入参为: ({hostNode, parentNode, nodes, wrapperNode, innerNode})`, + en: `Run when translation is complete, input parameters are: ({hostNode, parentNode, nodes, wrapperNode, innerNode})`, + zh_TW: `翻譯完成時運行,入參為: ({hostNode, parentNode, nodes, wrapperNode, innerNode})`, }, translate_remove_hook: { zh: `翻译移除钩子函数`, diff --git a/src/config/rules.js b/src/config/rules.js index 1f058f0..fc84a95 100644 --- a/src/config/rules.js +++ b/src/config/rules.js @@ -74,7 +74,7 @@ export const DEFAULT_SELECTOR = "h1, h2, h3, h4, h5, h6, li, p, dd, blockquote, figcaption, label, legend"; export const DEFAULT_IGNORE_SELECTOR = "button, code, footer, form, header, mark, nav, pre"; -export const DEFAULT_KEEP_SELECTOR = `code, img, svg, pre`; +export const DEFAULT_KEEP_SELECTOR = `code, img, svg, pre, a:has(code)`; export const DEFAULT_RULE = { pattern: "", // 匹配网址 selector: "", // 选择器 diff --git a/src/libs/batchQueue.js b/src/libs/batchQueue.js index 85ef89c..204725f 100644 --- a/src/libs/batchQueue.js +++ b/src/libs/batchQueue.js @@ -70,6 +70,9 @@ const BatchQueue = ( try { const payloads = tasksToProcess.map((item) => item.payload); const responses = await sendBatchRequest(payloads); + if (!Array.isArray(responses)) { + throw new Error("responses format error"); + } tasksToProcess.forEach((taskItem, index) => { const response = responses[index]; diff --git a/src/libs/storage.js b/src/libs/storage.js index 9db06fe..a451b20 100644 --- a/src/libs/storage.js +++ b/src/libs/storage.js @@ -62,10 +62,11 @@ async function trySetObj(key, obj) { async function getObj(key) { const val = await get(key); + if (val === null || val === undefined) return null; try { return JSON.parse(val); } catch (err) { - kissLog("parse json: ", key); + kissLog("parse json in storage err: ", key); } return null; } diff --git a/src/libs/translator.js b/src/libs/translator.js index 67b5bcc..1e78fac 100644 --- a/src/libs/translator.js +++ b/src/libs/translator.js @@ -255,6 +255,7 @@ export class Translator { #setting; // 设置选项 #rule; // 规则 #isInitialized = false; // 初始化状态 + #isJsInjected = false; // 注入用户JS #mouseHoverEnabled = false; // 鼠标悬停翻译 #enabled = false; // 全局默认状态 #runId = 0; // 用于中止过期的异步请求 @@ -1248,6 +1249,11 @@ export class Translator { // 注入JS/CSS #initInjector() { + if (this.#isJsInjected) { + return; + } + this.#isJsInjected = true; + try { const { injectJs, injectCss } = this.#rule; if (isExt) { @@ -1258,7 +1264,7 @@ export class Translator { injectCss && injectInternalCss(injectCss); } } catch (err) { - kissLog("inject js"); + kissLog("inject js", err); } } diff --git a/src/views/Options/Apis.js b/src/views/Options/Apis.js index d1e80df..27de6e1 100644 --- a/src/views/Options/Apis.js +++ b/src/views/Options/Apis.js @@ -24,6 +24,7 @@ import { apiTranslate } from "../../apis"; import Box from "@mui/material/Box"; import { limitNumber, limitFloat } from "../../libs/utils"; import ReusableAutocomplete from "./ReusableAutocomplete"; +import ShowMoreButton from "./ShowMoreButton"; import { OPT_TRANS_DEEPLX, OPT_TRANS_OLLAMA, @@ -115,6 +116,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) { const i18n = useI18n(); const [formData, setFormData] = useState({}); const [isModified, setIsModified] = useState(false); + const [showMore, setShowMore] = useState(false); const confirm = useConfirm(); useEffect(() => { @@ -282,7 +284,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) { <> - + {/* todo: 改成 ReusableAutocomplete 可选择和填写模型 */} - + - + - + - + @@ -393,27 +395,6 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) { */} - - - )} @@ -469,6 +450,14 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) { onChange={handleChange} multiline maxRows={10} + FormHelperTextProps={{ + component: "div", + }} + helperText={ + + {i18n("request_hook_helper")} + + } /> + {i18n("response_hook_helper")} + + } /> )} @@ -485,7 +482,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) { {API_SPE_TYPES.batch.has(api.apiType) && ( - + {i18n("enable")} - + - + - + - + {" "} {i18n("enable")} - + {" "} - + - + - + - + + {showMore && ( + <> + + + + {apiType !== OPT_TRANS_CUSTOMIZE && ( + <> + + {i18n("request_hook_helper")} + + } + /> + + {i18n("response_hook_helper")} + + } + /> + + )} + + )} + + + - {apiType === OPT_TRANS_CUSTOMIZE &&
{i18n("custom_api_help")}
} + {/* {apiType === OPT_TRANS_CUSTOMIZE &&
{i18n("custom_api_help")}
} */} ); } diff --git a/src/views/Options/Rules.js b/src/views/Options/Rules.js index 58c839a..848150e 100644 --- a/src/views/Options/Rules.js +++ b/src/views/Options/Rules.js @@ -24,7 +24,6 @@ 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 ExpandLessIcon from "@mui/icons-material/ExpandLess"; import { useRules } from "../../hooks/Rules"; import MenuItem from "@mui/material/MenuItem"; import Grid from "@mui/material/Grid"; @@ -62,6 +61,7 @@ import CancelIcon from "@mui/icons-material/Cancel"; import SaveIcon from "@mui/icons-material/Save"; import { kissLog } from "../../libs/log"; import { useApiList } from "../../hooks/Api"; +import ShowMoreButton from "./ShowMoreButton"; function RuleFields({ rule, rules, setShow, setKeyword }) { const initFormValues = useMemo( @@ -209,30 +209,6 @@ function RuleFields({ rule, rules, setShow, setKeyword }) { ); - const ShowMoreButton = showMore ? ( - - ) : ( - - ); - return (
@@ -293,7 +269,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) { - + {i18n("default_disabled")} - + {i18n("enable")} - + {i18n("enable")} - + - + {i18n("enable")} - + {i18n("enable")} - + {``} - + - + - + - + - {/* + {/* */} - + - + - + )} - {ShowMoreButton} ) : ( <> @@ -742,9 +717,9 @@ function RuleFields({ rule, rules, setShow, setKeyword }) { > {i18n("restore_default")} - {ShowMoreButton} )} + ) : ( // 添加 @@ -765,7 +740,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) { > {i18n("cancel")} - {ShowMoreButton} + ))} @@ -1078,7 +1053,7 @@ function SubRulesEdit({ subList, addSub, updateDataCache }) { return; } - if (subList.find((item) => item.url === url)) { + if (subList.some((item) => item.url === url)) { setInputError(i18n("error_duplicate_values")); return; } diff --git a/src/views/Options/Setting.js b/src/views/Options/Setting.js index 82e0382..b97da13 100644 --- a/src/views/Options/Setting.js +++ b/src/views/Options/Setting.js @@ -149,7 +149,7 @@ export default function Settings() { - + - + {i18n("disable")} - + {i18n("hide")} - + {i18n("fab_click_translate")} - + - + - + - + - + - + - + {i18n("secondary_context_menus")} - + - + - + - + - + { + onChange((prev) => !prev); + }; + + if (showMore) { + return ( + + ); + } + + return ( + + ); +}