feat: support builtin AI
This commit is contained in:
@@ -51,7 +51,9 @@
|
||||
"GM": true,
|
||||
"unsafeWindow": true,
|
||||
"globalThis": true,
|
||||
"messenger": true
|
||||
"messenger": true,
|
||||
"LanguageDetector": true,
|
||||
"Translator": true
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: `查看自建一个翻译接口代理`,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, // 英文建议
|
||||
};
|
||||
|
||||
// 订阅列表
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
168
src/libs/builtinAI.js
Normal file
168
src/libs/builtinAI.js
Normal file
@@ -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);
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -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,7 +254,8 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
{!API_SPE_TYPES.machine.has(apiType) && (
|
||||
{!API_SPE_TYPES.machine.has(apiType) &&
|
||||
apiType !== OPT_TRANS_BUILTINAI && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
@@ -606,6 +608,9 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{apiType !== OPT_TRANS_BUILTINAI && (
|
||||
<>
|
||||
{" "}
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("custom_header")}
|
||||
@@ -626,8 +631,11 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
maxRows={10}
|
||||
helperText={i18n("custom_body_help")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{apiType !== OPT_TRANS_CUSTOMIZE && (
|
||||
{apiType !== OPT_TRANS_CUSTOMIZE &&
|
||||
apiType !== OPT_TRANS_BUILTINAI && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
@@ -782,7 +790,11 @@ export default function Apis() {
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Alert severity="info">{i18n("about_api")}</Alert>
|
||||
<Alert severity="info">
|
||||
{i18n("about_api")}
|
||||
<br />
|
||||
{i18n("about_api_2")}
|
||||
</Alert>
|
||||
|
||||
<Box>
|
||||
<Button
|
||||
|
||||
@@ -5,7 +5,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 CircularProgress from "@mui/material/CircularProgress";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useFavWords } from "../../hooks/FavWords";
|
||||
@@ -18,11 +17,9 @@ import ClearAllIcon from "@mui/icons-material/ClearAll";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import { isValidWord } from "../../libs/utils";
|
||||
import { kissLog } from "../../libs/log";
|
||||
import { apiTranslate } from "../../apis";
|
||||
import { OPT_TRANS_BAIDU, PHONIC_MAP } from "../../config";
|
||||
import { useConfirm } from "../../hooks/Confirm";
|
||||
import { useSetting } from "../../hooks/Setting";
|
||||
import { DICT_MAP } from "../Selection/DictMap";
|
||||
import { dictHandlers } from "../Selection/DictHandler";
|
||||
|
||||
function FavAccordion({ word, index }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
@@ -83,17 +80,20 @@ export default function FavWords() {
|
||||
|
||||
const handleTranslation = async () => {
|
||||
const { enDict } = setting?.tranboxSetting;
|
||||
const dict = DICT_MAP[enDict];
|
||||
const dict = dictHandlers[enDict];
|
||||
if (!dict) return "";
|
||||
|
||||
const tranList = [];
|
||||
for (const word of wordList) {
|
||||
try {
|
||||
const data = await dict.apiFn(word);
|
||||
const tran = dict.toText(data);
|
||||
tranList.push([word, tran].join("\n"));
|
||||
const tran = dict
|
||||
.toText(data)
|
||||
.map((line) => `- ${line}`)
|
||||
.join("\n");
|
||||
tranList.push([`## ${word}`, tran].join("\n"));
|
||||
} catch (err) {
|
||||
// skip
|
||||
kissLog("export translation", err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -289,6 +289,64 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="apiSlug"
|
||||
value={apiSlug}
|
||||
label={i18n("translate_service")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
{enabledApis.map((api) => (
|
||||
<MenuItem key={api.apiSlug} value={api.apiSlug}>
|
||||
{api.apiName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="fromLang"
|
||||
value={fromLang}
|
||||
label={i18n("from_lang")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="toLang"
|
||||
value={toLang}
|
||||
label={i18n("to_lang")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
@@ -354,22 +412,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transTag"
|
||||
value={transTag}
|
||||
label={i18n("translation_element_tag")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
<MenuItem value={"span"}>{`<span>`}</MenuItem>
|
||||
<MenuItem value={"font"}>{`<font>`}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
@@ -386,64 +429,23 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transTag"
|
||||
value={transTag}
|
||||
label={i18n("translation_element_tag")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
<MenuItem value={"span"}>{`<span>`}</MenuItem>
|
||||
<MenuItem value={"font"}>{`<font>`}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="apiSlug"
|
||||
value={apiSlug}
|
||||
label={i18n("translate_service")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
{enabledApis.map((api) => (
|
||||
<MenuItem key={api.apiSlug} value={api.apiSlug}>
|
||||
{api.apiName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="fromLang"
|
||||
value={fromLang}
|
||||
label={i18n("from_lang")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="toLang"
|
||||
value={toLang}
|
||||
label={i18n("to_lang")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
|
||||
@@ -340,7 +340,7 @@ export default function Settings() {
|
||||
size="small"
|
||||
name="langDetector"
|
||||
value={langDetector}
|
||||
label={i18n("detect_lang_remote")}
|
||||
label={i18n("detected_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
<Stack spacing={1}>
|
||||
|
||||
@@ -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) => (
|
||||
<Typography component="div">
|
||||
{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) => (
|
||||
<Typography component="ul">
|
||||
Reference in New Issue
Block a user