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")}
+
+
+
+ {GlobalItem}
+ {enabledApis.map((api) => (
+
+ ))}
+
+
+
+
+ {GlobalItem}
+ {OPT_LANGS_FROM.map(([lang, name]) => (
+
+ ))}
+
+
+
+
+ {GlobalItem}
+ {OPT_LANGS_TO.map(([lang, name]) => (
+
+ ))}
+
+
+
{i18n("enable")}
-
-
- {GlobalItem}
-
-
-
-
+
{i18n("enable")}
+
+
+ {GlobalItem}
+
+
+
+
-
-
- {GlobalItem}
- {enabledApis.map((api) => (
-
- ))}
-
-
-
-
- {GlobalItem}
- {OPT_LANGS_FROM.map(([lang, name]) => (
-
- ))}
-
-
-
-
- {GlobalItem}
- {OPT_LANGS_TO.map(([lang, name]) => (
-
- ))}
-
-
diff --git a/src/views/Options/Tranbox.js b/src/views/Options/Tranbox.js
index e2a7390..a9790c1 100644
--- a/src/views/Options/Tranbox.js
+++ b/src/views/Options/Tranbox.js
@@ -9,10 +9,10 @@ import {
OPT_LANGS_TO,
OPT_TRANBOX_TRIGGER_CLICK,
OPT_TRANBOX_TRIGGER_ALL,
- OPT_DICT_BAIDU,
+ OPT_DICT_BING,
OPT_DICT_ALL,
OPT_SUG_ALL,
- OPT_SUG_BAIDU,
+ OPT_SUG_YOUDAO,
} from "../../config";
import ShortcutInput from "./ShortcutInput";
import FormControlLabel from "@mui/material/FormControlLabel";
@@ -69,8 +69,8 @@ export default function Tranbox() {
followSelection = false,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
// extStyles = "",
- enDict = OPT_DICT_BAIDU,
- enSug = OPT_SUG_BAIDU,
+ enDict = OPT_DICT_BING,
+ enSug = OPT_SUG_YOUDAO,
} = tranboxSetting;
return (
diff --git a/src/views/Selection/DictCont.js b/src/views/Selection/DictCont.js
index 06e714e..416c2a5 100644
--- a/src/views/Selection/DictCont.js
+++ b/src/views/Selection/DictCont.js
@@ -7,7 +7,7 @@ import Divider from "@mui/material/Divider";
import Alert from "@mui/material/Alert";
import CopyBtn from "./CopyBtn";
import { useAsyncNow } from "../../hooks/Fetch";
-import { DICT_MAP } from "./DictMap";
+import { dictHandlers } from "./DictHandler";
function DictBody({ text, setCopyText, dict }) {
const { loading, error, data } = useAsyncNow(dict.apiFn, text);
@@ -17,7 +17,7 @@ function DictBody({ text, setCopyText, dict }) {
return;
}
- const copyText = [text, dict.toText(data)].join("\n");
+ const copyText = [text, dict.toText(data).join("\n")].join("\n");
setCopyText(copyText);
}, [data, text, dict, setCopyText]);
@@ -46,7 +46,7 @@ function DictBody({ text, setCopyText, dict }) {
export default function DictCont({ text, enDict }) {
const [copyText, setCopyText] = useState(text);
- const dict = DICT_MAP[enDict];
+ const dict = dictHandlers[enDict];
return (
diff --git a/src/views/Selection/DictMap.js b/src/views/Selection/DictHandler.js
similarity index 84%
rename from src/views/Selection/DictMap.js
rename to src/views/Selection/DictHandler.js
index 2d29eaa..4a81aca 100644
--- a/src/views/Selection/DictMap.js
+++ b/src/views/Selection/DictHandler.js
@@ -3,13 +3,11 @@ import AudioBtn from "./AudioBtn";
import { OPT_DICT_BING, OPT_DICT_YOUDAO } from "../../config";
import { apiMicrosoftDict, apiYoudaoDict } from "../../apis";
-export const DICT_MAP = {
+export const dictHandlers = {
[OPT_DICT_BING]: {
apiFn: apiMicrosoftDict,
toText: (data) =>
- data.trs
- ?.map(({ pos, def }) => `${pos ? `[${pos}] ` : ""}${def}`)
- .join("\n"),
+ data.trs?.map(({ pos, def }) => `${pos ? `[${pos}] ` : ""}${def}`) || [],
uiAudio: (data) => (
{data?.aus.map(({ key, audio, phonetic }) => (
@@ -38,9 +36,9 @@ export const DICT_MAP = {
[OPT_DICT_YOUDAO]: {
apiFn: apiYoudaoDict,
toText: (data) =>
- data?.ec?.word?.trs
- ?.map(({ pos, tran }) => `${pos ? `[${pos}] ` : ""}${tran}`)
- .join("\n"),
+ data?.ec?.word?.trs?.map(
+ ({ pos, tran }) => `${pos ? `[${pos}] ` : ""}${tran}`
+ ) || [],
uiAudio: () => null,
uiTrans: (data) => (