diff --git a/package.json b/package.json index 79072de..7421297 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,9 @@ "GM": true, "unsafeWindow": true, "globalThis": true, - "messenger": true + "messenger": true, + "LanguageDetector": true, + "Translator": true } }, "browserslist": { diff --git a/src/apis/index.js b/src/apis/index.js index 31d3af5..42c9d32 100644 --- a/src/apis/index.js +++ b/src/apis/index.js @@ -10,12 +10,19 @@ import { API_SPE_TYPES, DEFAULT_API_SETTING, OPT_TRANS_MICROSOFT, + MSG_BUILTINAI_DETECT, + MSG_BUILTINAI_TRANSLATE, + OPT_TRANS_BUILTINAI, } from "../config"; -import { sha256 } from "../libs/utils"; +import { sha256, withTimeout } from "../libs/utils"; import { kissLog } from "../libs/log"; import { handleTranslate, handleMicrosoftLangdetect } from "./trans"; import { getHttpCachePolyfill, putHttpCachePolyfill } from "../libs/cache"; import { getBatchQueue } from "../libs/batchQueue"; +import { isBuiltinAIAvailable } from "../libs/browser"; +import { chromeDetect, chromeTranslate } from "../libs/builtinAI"; +import { fnPolyfill } from "../libs/fetch"; +import { getFetchPool } from "../libs/pool"; /** * 同步数据 @@ -332,6 +339,52 @@ export const apiTencentLangdetect = async (text) => { return ""; }; +/** + * 浏览器内置AI语言识别 + * @param {*} text + * @returns + */ +export const apiBuiltinAIDetect = async (text) => { + if (!isBuiltinAIAvailable) { + return ""; + } + + const [lang, error] = await fnPolyfill({ + fn: chromeDetect, + msg: MSG_BUILTINAI_DETECT, + text, + }); + if (!error) { + return lang; + } + + return ""; +}; + +/** + * 浏览器内置AI翻译 + * @param {*} param0 + * @returns + */ +const apiBuiltinAITranslate = ({ text, from, to, apiSetting }) => { + if (!isBuiltinAIAvailable) { + return ["", true]; + } + + const { fetchInterval, fetchLimit, httpTimeout } = apiSetting; + const fetchPool = getFetchPool(fetchInterval, fetchLimit); + return withTimeout( + fetchPool.push(fnPolyfill, { + fn: chromeTranslate, + msg: MSG_BUILTINAI_TRANSLATE, + text, + from, + to, + }), + httpTimeout + ); +}; + /** * 统一翻译接口 * @param {*} param0 @@ -382,7 +435,14 @@ export const apiTranslate = async ({ // 请求接口数据 let trText = ""; let srLang = ""; - if (useBatchFetch && API_SPE_TYPES.batch.has(apiType)) { + if (apiType === OPT_TRANS_BUILTINAI) { + [trText, srLang] = await apiBuiltinAITranslate({ + text, + from, + to, + apiSetting, + }); + } else if (useBatchFetch && API_SPE_TYPES.batch.has(apiType)) { const { apiSlug, batchInterval, batchSize, batchLength } = apiSetting; const key = `${apiSlug}_${fromLang}_${toLang}`; const queue = getBatchQueue(key, handleTranslate, { @@ -426,7 +486,7 @@ export const apiTranslate = async ({ } } - const isSame = fromLang !== "auto" && srLang === to; + const isSame = fromLang === "auto" && srLang === to; // 插入缓存 if (useCache && trText) { diff --git a/src/background.js b/src/background.js index 1a573f0..429be40 100644 --- a/src/background.js +++ b/src/background.js @@ -13,6 +13,8 @@ import { MSG_INJECT_JS, MSG_INJECT_CSS, MSG_UPDATE_CSP, + MSG_BUILTINAI_DETECT, + MSG_BUILTINAI_TRANSLATE, DEFAULT_CSPLIST, DEFAULT_ORILIST, CMD_TOGGLE_TRANSLATE, @@ -31,6 +33,7 @@ import { saveRule } from "./libs/rules"; import { getCurTabId } from "./libs/msg"; import { injectInlineJs, injectInternalCss } from "./libs/injector"; import { kissLog } from "./libs/log"; +import { chromeDetect, chromeTranslate } from "./libs/builtinAI"; globalThis.ContextType = "BACKGROUND"; @@ -234,43 +237,54 @@ browser.runtime.onStartup.addListener(async () => { trySyncAllSubRules({ subrulesList }); }); +/** + * 向当前活动标签页注入脚本或CSS + */ +const injectToCurrentTab = async (func, args) => { + const tabId = await getCurTabId(); + return browser.scripting.executeScript({ + target: { tabId, allFrames: true }, + func: func, + args: [args], + world: "MAIN", + }); +}; + +// 动作处理器映射表 +const messageHandlers = { + [MSG_FETCH]: (args) => fetchHandle(args), + [MSG_GET_HTTPCACHE]: (args) => getHttpCache(args), + [MSG_PUT_HTTPCACHE]: (args) => putHttpCache(args), + [MSG_OPEN_OPTIONS]: () => browser.runtime.openOptionsPage(), + [MSG_SAVE_RULE]: (args) => saveRule(args), + [MSG_INJECT_JS]: (args) => injectToCurrentTab(injectInlineJs, args), + [MSG_INJECT_CSS]: (args) => injectToCurrentTab(injectInternalCss, args), + [MSG_UPDATE_CSP]: (args) => updateCspRules(args), + [MSG_CONTEXT_MENUS]: (args) => addContextMenus(args), + [MSG_COMMAND_SHORTCUTS]: () => browser.commands.getAll(), + [MSG_BUILTINAI_DETECT]: (args) => chromeDetect(args), + [MSG_BUILTINAI_TRANSLATE]: (args) => chromeTranslate(args), +}; + /** * 监听消息 + * todo: 返回含错误的结构化信息 */ browser.runtime.onMessage.addListener(async ({ action, args }) => { - switch (action) { - case MSG_FETCH: - return await fetchHandle(args); - case MSG_GET_HTTPCACHE: - return await getHttpCache(args.input, args.init); - case MSG_PUT_HTTPCACHE: - return await putHttpCache(args.input, args.init, args.data); - case MSG_OPEN_OPTIONS: - return await browser.runtime.openOptionsPage(); - case MSG_SAVE_RULE: - return await saveRule(args); - case MSG_INJECT_JS: - return await browser.scripting.executeScript({ - target: { tabId: await getCurTabId(), allFrames: true }, - func: injectInlineJs, - args: [args], - world: "MAIN", - }); - case MSG_INJECT_CSS: - return await browser.scripting.executeScript({ - target: { tabId: await getCurTabId(), allFrames: true }, - func: injectInternalCss, - args: [args], - world: "MAIN", - }); - case MSG_UPDATE_CSP: - return await updateCspRules(args); - case MSG_CONTEXT_MENUS: - return await addContextMenus(args); - case MSG_COMMAND_SHORTCUTS: - return await browser.commands.getAll(); - default: - throw new Error(`message action is unavailable: ${action}`); + const handler = messageHandlers[action]; + + if (!handler) { + const errorMessage = `Message action is unavailable: ${action}`; + kissLog("runtime onMessage", action, new Error(errorMessage)); + return null; + } + + try { + const result = await handler(args); + return result; + } catch (err) { + kissLog("runtime onMessage", action, err); + return null; } }); diff --git a/src/config/api.js b/src/config/api.js index 5c7c512..71d3c9e 100644 --- a/src/config/api.js +++ b/src/config/api.js @@ -13,7 +13,7 @@ export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符 export const INPUT_PLACE_KEY = "{{key}}"; // 占位符 export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符 -export const OPT_DICT_BAIDU = "Baidu"; +// export const OPT_DICT_BAIDU = "Baidu"; export const OPT_DICT_BING = "Bing"; export const OPT_DICT_YOUDAO = "Youdao"; export const OPT_DICT_ALL = [OPT_DICT_BING, OPT_DICT_YOUDAO]; @@ -24,6 +24,7 @@ export const OPT_SUG_YOUDAO = "Youdao"; export const OPT_SUG_ALL = [OPT_SUG_BAIDU, OPT_SUG_YOUDAO]; export const OPT_SUG_MAP = new Set(OPT_SUG_ALL); +export const OPT_TRANS_BUILTINAI = "BuiltinAI"; export const OPT_TRANS_GOOGLE = "Google"; export const OPT_TRANS_GOOGLE_2 = "Google2"; export const OPT_TRANS_MICROSOFT = "Microsoft"; @@ -45,10 +46,11 @@ export const OPT_TRANS_CUSTOMIZE = "Custom"; // 内置支持的翻译引擎 export const OPT_ALL_TYPES = [ + OPT_TRANS_BUILTINAI, OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE_2, OPT_TRANS_MICROSOFT, - OPT_TRANS_BAIDU, + // OPT_TRANS_BAIDU, OPT_TRANS_TENCENT, OPT_TRANS_VOLCENGINE, OPT_TRANS_DEEPL, @@ -66,12 +68,15 @@ export const OPT_ALL_TYPES = [ ]; export const OPT_LANGDETECTOR_ALL = [ + OPT_TRANS_BUILTINAI, OPT_TRANS_GOOGLE, OPT_TRANS_MICROSOFT, OPT_TRANS_BAIDU, OPT_TRANS_TENCENT, ]; +export const OPT_LANGDETECTOR_MAP = new Set(OPT_LANGDETECTOR_ALL); + // 翻译引擎特殊集合 export const API_SPE_TYPES = { // 内置翻译 @@ -130,7 +135,6 @@ export const API_SPE_TYPES = { OPT_TRANS_OPENROUTER, OPT_TRANS_CUSTOMIZE, ]), - detector: new Set(OPT_LANGDETECTOR_ALL), }; export const BUILTIN_STONES = [ @@ -205,6 +209,11 @@ export const OPT_LANGS_SPEC_DEFAULT_UC = new Map( OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]) ); export const OPT_LANGS_TO_SPEC = { + [OPT_TRANS_BUILTINAI]: new Map([ + ...OPT_LANGS_SPEC_DEFAULT, + ["zh-CN", "zh"], + ["zh-TW", "zh"], + ]), [OPT_TRANS_GOOGLE]: OPT_LANGS_SPEC_DEFAULT, [OPT_TRANS_GOOGLE_2]: OPT_LANGS_SPEC_DEFAULT, [OPT_TRANS_MICROSOFT]: new Map([ @@ -392,6 +401,7 @@ const defaultApi = { }; const defaultApiOpts = { + [OPT_TRANS_BUILTINAI]: defaultApi, [OPT_TRANS_GOOGLE]: { ...defaultApi, url: "https://translate.googleapis.com/translate_a/single", diff --git a/src/config/i18n.js b/src/config/i18n.js index 0282bf2..13947f3 100644 --- a/src/config/i18n.js +++ b/src/config/i18n.js @@ -525,9 +525,14 @@ export const I18N = { zh_TW: `自建 kiss-wroker 資料同步服務`, }, about_api: { - zh: `暂未列出的接口,理论上都可以通过自定义接口的形式支持。`, - en: `Interfaces that have not yet been launched can theoretically be supported through custom interfaces.`, - zh_TW: `暫未列出的介面,理論上都可透過自訂介面的形式支援。`, + zh: `1、其中 BuiltinAI 为浏览器内置AI翻译,目前仅 Chrome 138 及以上版本得到支持。`, + en: `1. BuiltinAI is the browser's built-in AI translation, which is currently only supported by Chrome 138 and above.`, + zh_TW: `1.其中 BuiltinAI 為瀏覽器內建AI翻譯,目前僅 Chrome 138 以上版本支援。`, + }, + about_api_2: { + zh: `2、暂未列出的接口,理论上都可以通过自定义接口的形式支持。`, + en: `2. Interfaces that have not yet been launched can theoretically be supported through custom interfaces.`, + zh_TW: `2、暫未列出的介面,理論上都可透過自訂介面的形式支援。`, }, about_api_proxy: { zh: `查看自建一个翻译接口代理`, diff --git a/src/config/msg.js b/src/config/msg.js index 50d6d83..fe38579 100644 --- a/src/config/msg.js +++ b/src/config/msg.js @@ -22,3 +22,5 @@ export const MSG_COMMAND_SHORTCUTS = "command_shortcuts"; export const MSG_INJECT_JS = "inject_js"; export const MSG_INJECT_CSS = "inject_css"; export const MSG_UPDATE_CSP = "update_csp"; +export const MSG_BUILTINAI_DETECT = "builtinai_detect"; +export const MSG_BUILTINAI_TRANSLATE = "builtinai_translte"; diff --git a/src/config/rules.js b/src/config/rules.js index 060ff80..cda5f67 100644 --- a/src/config/rules.js +++ b/src/config/rules.js @@ -25,8 +25,8 @@ export const OPT_STYLE_ALL = [ OPT_STYLE_LINE, OPT_STYLE_DOTLINE, OPT_STYLE_DASHLINE, - OPT_STYLE_DASHBOX, OPT_STYLE_WAVYLINE, + OPT_STYLE_DASHBOX, OPT_STYLE_FUZZY, OPT_STYLE_HIGHLIGHT, OPT_STYLE_BLOCKQUOTE, diff --git a/src/config/setting.js b/src/config/setting.js index 104ebbc..2f79e6e 100644 --- a/src/config/setting.js +++ b/src/config/setting.js @@ -1,6 +1,6 @@ import { - OPT_DICT_BAIDU, - OPT_SUG_BAIDU, + OPT_DICT_BING, + OPT_SUG_YOUDAO, DEFAULT_HTTP_TIMEOUT, OPT_TRANS_MICROSOFT, DEFAULT_API_LIST, @@ -91,8 +91,8 @@ export const DEFAULT_TRANBOX_SETTING = { followSelection: false, // 翻译框是否跟随选中文本 triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式 // extStyles: "", // 附加样式 - enDict: OPT_DICT_BAIDU, // 英文词典 - enSug: OPT_SUG_BAIDU, // 英文建议 + enDict: OPT_DICT_BING, // 英文词典 + enSug: OPT_SUG_YOUDAO, // 英文建议 }; // 订阅列表 diff --git a/src/hooks/Api.js b/src/hooks/Api.js index 2842784..c4d6008 100644 --- a/src/hooks/Api.js +++ b/src/hooks/Api.js @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { DEFAULT_API_LIST, API_SPE_TYPES } from "../config"; import { useSetting } from "./Setting"; @@ -12,6 +12,19 @@ function useApiState() { export function useApiList() { const { transApis, updateSetting } = useApiState(); + useEffect(() => { + const curSlugs = new Set(transApis.map((api) => api.apiSlug)); + const missApis = DEFAULT_API_LIST.filter( + (api) => !curSlugs.has(api.apiSlug) + ); + if (missApis.length > 0) { + updateSetting((prev) => ({ + ...prev, + transApis: [...(prev?.transApis || []), ...missApis], + })); + } + }, [transApis, updateSetting]); + const userApis = useMemo( () => transApis @@ -55,7 +68,9 @@ export function useApiList() { (apiSlug) => { updateSetting((prev) => ({ ...prev, - transApis: (prev?.transApis || []).filter((api) => api.apiSlug !== apiSlug), + transApis: (prev?.transApis || []).filter( + (api) => api.apiSlug !== apiSlug + ), })); }, [updateSetting] diff --git a/src/libs/browser.js b/src/libs/browser.js index 2745e27..79ec26f 100644 --- a/src/libs/browser.js +++ b/src/libs/browser.js @@ -15,3 +15,6 @@ function _browser() { export const browser = _browser(); export const isBg = () => globalThis?.ContextType === "BACKGROUND"; + +export const isBuiltinAIAvailable = + "LanguageDetector" in globalThis && "Translator" in globalThis; diff --git a/src/libs/builtinAI.js b/src/libs/builtinAI.js new file mode 100644 index 0000000..1fe2d85 --- /dev/null +++ b/src/libs/builtinAI.js @@ -0,0 +1,168 @@ +import { kissLog, logger } from "./log"; + +/** + * Chrome 浏览器内置翻译 + */ +class ChromeTranslator { + #translatorMap = new Map(); + #detectorPromise = null; + + constructor(options = {}) { + this.onProgress = options.onProgress || this.#defaultProgressHandler; + } + + #defaultProgressHandler(type, progress) { + kissLog(`Downloading ${type} model: ${progress}%`); + } + + #getDetectorPromise() { + if (!this.#detectorPromise) { + this.#detectorPromise = (async () => { + try { + const availability = await LanguageDetector.availability(); + if (availability === "unavailable") { + throw new Error("LanguageDetector unavailable"); + } + + return await LanguageDetector.create({ + monitor: (m) => this._monitorProgress(m, "detector"), + }); + } catch (error) { + this.#detectorPromise = null; + throw error; + } + })(); + } + + return this.#detectorPromise; + } + + #createTranslator(sourceLanguage, targetLanguage) { + const key = `${sourceLanguage}_${targetLanguage}`; + if (this.#translatorMap.has(key)) { + return this.#translatorMap.get(key); + } + + const translatorPromise = (async () => { + try { + const avail = await Translator.availability({ + sourceLanguage, + targetLanguage, + }); + if (avail === "unavailable") { + throw new Error( + `Translator ${sourceLanguage}_${targetLanguage} unavailable` + ); + } + + const translator = await Translator.create({ + sourceLanguage, + targetLanguage, + monitor: (m) => this._monitorProgress(m, `translator (${key})`), + }); + this.#translatorMap.set(key, translator); + + return translator; + } catch (error) { + this.#translatorMap.delete(key); + throw error; + } + })(); + + this.#translatorMap.set(key, translatorPromise); + return translatorPromise; + } + + _monitorProgress(monitorable, type) { + monitorable.addEventListener("downloadprogress", (e) => { + const progress = e.total > 0 ? Math.round((e.loaded / e.total) * 100) : 0; + this.onProgress(type, progress); + }); + } + + async detectLanguage(text, confidenceThreshold = 0.4) { + if (!text) { + return ["", "Input text cannot be empty."]; + } + + try { + const detector = await this.#getDetectorPromise(); + const results = await detector.detect(text); + + if (!results || results.length === 0) { + return ["", "No language could be detected."]; + } + + const { detectedLanguage, confidence } = results[0]; + if (confidence < confidenceThreshold) { + return [ + "", + `Confidence of test results (${detectedLanguage} ${confidence.toFixed( + 2 + )}) below the set threshold ${confidenceThreshold}。`, + ]; + } + + return [detectedLanguage, ""]; + } catch (error) { + kissLog("detectLanguage", error, `(${text})`); + return ["", error.message]; + } + } + + async translateText(text, targetLanguage, sourceLanguage = "auto") { + if (!text || !targetLanguage || typeof text !== "string") { + return ["", sourceLanguage, "Input text cannot be empty."]; + } + + try { + let finalSourceLanguage = sourceLanguage; + if (sourceLanguage === "auto") { + const [detectedLanguage, detectionError] = + await this.detectLanguage(text); + if (detectionError || !detectedLanguage) { + const reason = + detectionError || "Unable to determine source language."; + return [ + "", + finalSourceLanguage, + `Automatic detection of source language failed: ${reason}`, + ]; + } + finalSourceLanguage = detectedLanguage; + } + + if (finalSourceLanguage === targetLanguage) { + return ["", finalSourceLanguage, "Same lang"]; + } + + const translator = await this.#createTranslator( + finalSourceLanguage, + targetLanguage + ); + const translatedText = await translator.translate(text); + + return [translatedText, finalSourceLanguage, ""]; + } catch (error) { + kissLog("translateText", error, `(${text})`); + + if ( + error && + error.message && + error.message.includes("Other generic failures occurred") + ) { + logger.error("Generic failure detected, resetting translator cache."); + this.#translatorMap.clear(); + } + + return ["", sourceLanguage, error.message]; + } + } +} + +const chromeTranslator = new ChromeTranslator(); + +export const chromeDetect = (args) => + chromeTranslator.detectLanguage(args.text); +export const chromeTranslate = (args) => + chromeTranslator.translateText(args.text, args.to, args.from); diff --git a/src/libs/cache.js b/src/libs/cache.js index 78e20d8..ce9c43c 100644 --- a/src/libs/cache.js +++ b/src/libs/cache.js @@ -45,7 +45,7 @@ const newCacheReq = async (input, init) => { * @param {*} init * @returns */ -export const getHttpCache = async (input, init) => { +export const getHttpCache = async ({ input, init }) => { try { const req = await newCacheReq(input, init); const cache = await caches.open(CACHE_NAME); @@ -65,12 +65,12 @@ export const getHttpCache = async (input, init) => { * @param {*} init * @param {*} data */ -export const putHttpCache = async ( +export const putHttpCache = async ({ input, init, data, - maxAge = DEFAULT_CACHE_TIMEOUT // todo: 从设置里面读取最大缓存时间 -) => { + maxAge = DEFAULT_CACHE_TIMEOUT, // todo: 从设置里面读取最大缓存时间 +}) => { try { const req = await newCacheReq(input, init); const cache = await caches.open(CACHE_NAME); @@ -132,7 +132,7 @@ export const getHttpCachePolyfill = (input, init) => { } // 油猴/网页/BackgroundPage - return getHttpCache(input, init); + return getHttpCache({ input, init }); }; /** @@ -149,5 +149,5 @@ export const putHttpCachePolyfill = (input, init, data) => { } // 油猴/网页/BackgroundPage - return putHttpCache(input, init, data); + return putHttpCache({ input, init, data }); }; diff --git a/src/libs/detect.js b/src/libs/detect.js index 98caff2..c2151ca 100644 --- a/src/libs/detect.js +++ b/src/libs/detect.js @@ -5,7 +5,8 @@ import { OPT_TRANS_TENCENT, OPT_LANGS_TO_CODE, OPT_LANGS_MAP, - API_SPE_TYPES, + OPT_TRANS_BUILTINAI, + OPT_LANGDETECTOR_MAP, } from "../config"; import { browser } from "./browser"; import { @@ -13,6 +14,7 @@ import { apiMicrosoftLangdetect, apiBaiduLangdetect, apiTencentLangdetect, + apiBuiltinAIDetect, } from "../apis"; import { kissLog } from "./log"; @@ -21,6 +23,7 @@ const langdetectFns = { [OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect, [OPT_TRANS_BAIDU]: apiBaiduLangdetect, [OPT_TRANS_TENCENT]: apiTencentLangdetect, + [OPT_TRANS_BUILTINAI]: apiBuiltinAIDetect, }; /** @@ -31,8 +34,8 @@ const langdetectFns = { export const tryDetectLang = async (text, langDetector = "-") => { let deLang = ""; - // 远程识别 - if (API_SPE_TYPES.detector.has(langDetector)) { + // 内置AI/远程识别 + if (OPT_LANGDETECTOR_MAP.has(langDetector)) { try { const lang = await langdetectFns[langDetector](text); if (lang) { diff --git a/src/libs/fetch.js b/src/libs/fetch.js index bdad2d7..28488d4 100644 --- a/src/libs/fetch.js +++ b/src/libs/fetch.js @@ -101,14 +101,14 @@ export const fetchHandle = async ({ input, init, opts }) => { * @param {*} args * @returns */ -const fetchPolyfill = (args) => { +export const fnPolyfill = ({ fn, msg = MSG_FETCH, ...args }) => { // 插件 if (isExt && !isBg()) { - return sendBgMsg(MSG_FETCH, args); + return sendBgMsg(msg, { ...args }); } // 油猴/网页/BackgroundPage - return fetchHandle(args); + return fn({ ...args }); }; /** @@ -138,9 +138,9 @@ export const fetchData = async ( // 通过任务池发送请求 if (usePool) { const fetchPool = getFetchPool(fetchInterval, fetchLimit); - return fetchPool.push(fetchPolyfill, { input, init, opts }); + return fetchPool.push(fnPolyfill, { fn: fetchHandle, input, init, opts }); } // 直接请求 - return fetchPolyfill({ input, init, opts }); + return fnPolyfill({ fn: fetchHandle, input, init, opts }); }; diff --git a/src/libs/translator.js b/src/libs/translator.js index 844364e..53ccd0c 100644 --- a/src/libs/translator.js +++ b/src/libs/translator.js @@ -274,12 +274,6 @@ export class Translator { #glossary = {}; // AI词典 #textClass = {}; // 译文样式class #textSheet = ""; // 译文样式字典 - #apiSetting = null; - #placeholder = { - startDelimiter: "{", - endDelimiter: "}", - tagName: "i", - }; #isUserscript = false; #transboxManager = null; // 划词翻译 @@ -309,20 +303,30 @@ export class Translator { return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`; } - constructor(rule = {}, setting = {}, isUserscript = false) { - this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting }; - this.#rule = { ...Translator.DEFAULT_RULE, ...rule }; - this.#apiSetting = + // 接口参数 + // todo: 不用频繁查找计算 + get #apiSetting() { + return ( this.#setting.transApis.find( (api) => api.apiSlug === this.#rule.apiSlug - ) || DEFAULT_API_SETTING; + ) || DEFAULT_API_SETTING + ); + } + + // 占位符 + get #placeholder() { const [startDelimiter, endDelimiter] = this.#apiSetting.placeholder.split(" "); - this.#placeholder = { + return { startDelimiter, endDelimiter, tagName: this.#apiSetting.placetag, }; + } + + constructor(rule = {}, setting = {}, isUserscript = false) { + this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting }; + this.#rule = { ...Translator.DEFAULT_RULE, ...rule }; this.#isUserscript = isUserscript; this.#eventName = genEventName(); diff --git a/src/libs/utils.js b/src/libs/utils.js index 5b325f3..eba1944 100644 --- a/src/libs/utils.js +++ b/src/libs/utils.js @@ -333,3 +333,20 @@ export const parseUrlPattern = (href) => { } return href; }; + +/** + * 带超时的任务 + * @param {Promise|Function} task - 任务 + * @param {number} timeout - 超时时间 (毫秒) + * @param {string} [timeoutMsg] - 超时错误提示 + * @returns {Promise} + */ +export const withTimeout = (task, timeout, timeoutMsg = "Task timed out") => { + const promise = typeof task === "function" ? task() : task; + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error(timeoutMsg)), timeout) + ), + ]); +}; diff --git a/src/views/Options/Apis.js b/src/views/Options/Apis.js index 0c08e2c..4bdcc3e 100644 --- a/src/views/Options/Apis.js +++ b/src/views/Options/Apis.js @@ -30,6 +30,7 @@ import { OPT_TRANS_OLLAMA, OPT_TRANS_CUSTOMIZE, OPT_TRANS_NIUTRANS, + OPT_TRANS_BUILTINAI, DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_INTERVAL, DEFAULT_HTTP_TIMEOUT, @@ -253,32 +254,33 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) { onChange={handleChange} /> - {!API_SPE_TYPES.machine.has(apiType) && ( - <> - - - - )} + {!API_SPE_TYPES.machine.has(apiType) && + apiType !== OPT_TRANS_BUILTINAI && ( + <> + + + + )} {API_SPE_TYPES.ai.has(apiType) && ( <> @@ -606,65 +608,71 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) { - - - - {apiType !== OPT_TRANS_CUSTOMIZE && ( + {apiType !== OPT_TRANS_BUILTINAI && ( <> + {" "} - {i18n("request_hook_helper")} - - } + helperText={i18n("custom_header_help")} /> - {i18n("response_hook_helper")} - - } + helperText={i18n("custom_body_help")} /> )} + + {apiType !== OPT_TRANS_CUSTOMIZE && + apiType !== OPT_TRANS_BUILTINAI && ( + <> + + {i18n("request_hook_helper")} + + } + /> + + {i18n("response_hook_helper")} + + } + /> + + )} )} @@ -782,7 +790,11 @@ export default function Apis() { return ( - {i18n("about_api")} + + {i18n("about_api")} +
+ {i18n("about_api_2")} +