feat: support builtin AI

This commit is contained in:
Gabe
2025-10-04 21:25:54 +08:00
parent c353c88db8
commit 7b2b48f0d1
23 changed files with 558 additions and 243 deletions

View File

@@ -51,7 +51,9 @@
"GM": true, "GM": true,
"unsafeWindow": true, "unsafeWindow": true,
"globalThis": true, "globalThis": true,
"messenger": true "messenger": true,
"LanguageDetector": true,
"Translator": true
} }
}, },
"browserslist": { "browserslist": {

View File

@@ -10,12 +10,19 @@ import {
API_SPE_TYPES, API_SPE_TYPES,
DEFAULT_API_SETTING, DEFAULT_API_SETTING,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
MSG_BUILTINAI_DETECT,
MSG_BUILTINAI_TRANSLATE,
OPT_TRANS_BUILTINAI,
} from "../config"; } from "../config";
import { sha256 } from "../libs/utils"; import { sha256, withTimeout } from "../libs/utils";
import { kissLog } from "../libs/log"; import { kissLog } from "../libs/log";
import { handleTranslate, handleMicrosoftLangdetect } from "./trans"; import { handleTranslate, handleMicrosoftLangdetect } from "./trans";
import { getHttpCachePolyfill, putHttpCachePolyfill } from "../libs/cache"; import { getHttpCachePolyfill, putHttpCachePolyfill } from "../libs/cache";
import { getBatchQueue } from "../libs/batchQueue"; 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 ""; 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 * @param {*} param0
@@ -382,7 +435,14 @@ export const apiTranslate = async ({
// 请求接口数据 // 请求接口数据
let trText = ""; let trText = "";
let srLang = ""; 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 { apiSlug, batchInterval, batchSize, batchLength } = apiSetting;
const key = `${apiSlug}_${fromLang}_${toLang}`; const key = `${apiSlug}_${fromLang}_${toLang}`;
const queue = getBatchQueue(key, handleTranslate, { 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) { if (useCache && trText) {

View File

@@ -13,6 +13,8 @@ import {
MSG_INJECT_JS, MSG_INJECT_JS,
MSG_INJECT_CSS, MSG_INJECT_CSS,
MSG_UPDATE_CSP, MSG_UPDATE_CSP,
MSG_BUILTINAI_DETECT,
MSG_BUILTINAI_TRANSLATE,
DEFAULT_CSPLIST, DEFAULT_CSPLIST,
DEFAULT_ORILIST, DEFAULT_ORILIST,
CMD_TOGGLE_TRANSLATE, CMD_TOGGLE_TRANSLATE,
@@ -31,6 +33,7 @@ import { saveRule } from "./libs/rules";
import { getCurTabId } from "./libs/msg"; import { getCurTabId } from "./libs/msg";
import { injectInlineJs, injectInternalCss } from "./libs/injector"; import { injectInlineJs, injectInternalCss } from "./libs/injector";
import { kissLog } from "./libs/log"; import { kissLog } from "./libs/log";
import { chromeDetect, chromeTranslate } from "./libs/builtinAI";
globalThis.ContextType = "BACKGROUND"; globalThis.ContextType = "BACKGROUND";
@@ -234,43 +237,54 @@ browser.runtime.onStartup.addListener(async () => {
trySyncAllSubRules({ subrulesList }); 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 }) => { browser.runtime.onMessage.addListener(async ({ action, args }) => {
switch (action) { const handler = messageHandlers[action];
case MSG_FETCH:
return await fetchHandle(args); if (!handler) {
case MSG_GET_HTTPCACHE: const errorMessage = `Message action is unavailable: ${action}`;
return await getHttpCache(args.input, args.init); kissLog("runtime onMessage", action, new Error(errorMessage));
case MSG_PUT_HTTPCACHE: return null;
return await putHttpCache(args.input, args.init, args.data); }
case MSG_OPEN_OPTIONS:
return await browser.runtime.openOptionsPage(); try {
case MSG_SAVE_RULE: const result = await handler(args);
return await saveRule(args); return result;
case MSG_INJECT_JS: } catch (err) {
return await browser.scripting.executeScript({ kissLog("runtime onMessage", action, err);
target: { tabId: await getCurTabId(), allFrames: true }, return null;
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}`);
} }
}); });

View File

@@ -13,7 +13,7 @@ export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符 export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符 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_BING = "Bing";
export const OPT_DICT_YOUDAO = "Youdao"; export const OPT_DICT_YOUDAO = "Youdao";
export const OPT_DICT_ALL = [OPT_DICT_BING, OPT_DICT_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_ALL = [OPT_SUG_BAIDU, OPT_SUG_YOUDAO];
export const OPT_SUG_MAP = new Set(OPT_SUG_ALL); 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 = "Google";
export const OPT_TRANS_GOOGLE_2 = "Google2"; export const OPT_TRANS_GOOGLE_2 = "Google2";
export const OPT_TRANS_MICROSOFT = "Microsoft"; export const OPT_TRANS_MICROSOFT = "Microsoft";
@@ -45,10 +46,11 @@ export const OPT_TRANS_CUSTOMIZE = "Custom";
// 内置支持的翻译引擎 // 内置支持的翻译引擎
export const OPT_ALL_TYPES = [ export const OPT_ALL_TYPES = [
OPT_TRANS_BUILTINAI,
OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE,
OPT_TRANS_GOOGLE_2, OPT_TRANS_GOOGLE_2,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU, // OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT, OPT_TRANS_TENCENT,
OPT_TRANS_VOLCENGINE, OPT_TRANS_VOLCENGINE,
OPT_TRANS_DEEPL, OPT_TRANS_DEEPL,
@@ -66,12 +68,15 @@ export const OPT_ALL_TYPES = [
]; ];
export const OPT_LANGDETECTOR_ALL = [ export const OPT_LANGDETECTOR_ALL = [
OPT_TRANS_BUILTINAI,
OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU, OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT, OPT_TRANS_TENCENT,
]; ];
export const OPT_LANGDETECTOR_MAP = new Set(OPT_LANGDETECTOR_ALL);
// 翻译引擎特殊集合 // 翻译引擎特殊集合
export const API_SPE_TYPES = { export const API_SPE_TYPES = {
// 内置翻译 // 内置翻译
@@ -130,7 +135,6 @@ export const API_SPE_TYPES = {
OPT_TRANS_OPENROUTER, OPT_TRANS_OPENROUTER,
OPT_TRANS_CUSTOMIZE, OPT_TRANS_CUSTOMIZE,
]), ]),
detector: new Set(OPT_LANGDETECTOR_ALL),
}; };
export const BUILTIN_STONES = [ 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()]) OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()])
); );
export const OPT_LANGS_TO_SPEC = { 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]: OPT_LANGS_SPEC_DEFAULT,
[OPT_TRANS_GOOGLE_2]: OPT_LANGS_SPEC_DEFAULT, [OPT_TRANS_GOOGLE_2]: OPT_LANGS_SPEC_DEFAULT,
[OPT_TRANS_MICROSOFT]: new Map([ [OPT_TRANS_MICROSOFT]: new Map([
@@ -392,6 +401,7 @@ const defaultApi = {
}; };
const defaultApiOpts = { const defaultApiOpts = {
[OPT_TRANS_BUILTINAI]: defaultApi,
[OPT_TRANS_GOOGLE]: { [OPT_TRANS_GOOGLE]: {
...defaultApi, ...defaultApi,
url: "https://translate.googleapis.com/translate_a/single", url: "https://translate.googleapis.com/translate_a/single",

View File

@@ -525,9 +525,14 @@ export const I18N = {
zh_TW: `自建 kiss-wroker 資料同步服務`, zh_TW: `自建 kiss-wroker 資料同步服務`,
}, },
about_api: { about_api: {
zh: `暂未列出的接口,理论上都可以通过自定义接口的形式支持。`, zh: `1、其中 BuiltinAI 为浏览器内置AI翻译目前仅 Chrome 138 及以上版本得到支持。`,
en: `Interfaces that have not yet been launched can theoretically be supported through custom interfaces.`, en: `1. BuiltinAI is the browser's built-in AI translation, which is currently only supported by Chrome 138 and above.`,
zh_TW: `暫未列出的介面,理論上都可透過自訂介面的形式支援。`, 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: { about_api_proxy: {
zh: `查看自建一个翻译接口代理`, zh: `查看自建一个翻译接口代理`,

View File

@@ -22,3 +22,5 @@ export const MSG_COMMAND_SHORTCUTS = "command_shortcuts";
export const MSG_INJECT_JS = "inject_js"; export const MSG_INJECT_JS = "inject_js";
export const MSG_INJECT_CSS = "inject_css"; export const MSG_INJECT_CSS = "inject_css";
export const MSG_UPDATE_CSP = "update_csp"; export const MSG_UPDATE_CSP = "update_csp";
export const MSG_BUILTINAI_DETECT = "builtinai_detect";
export const MSG_BUILTINAI_TRANSLATE = "builtinai_translte";

View File

@@ -25,8 +25,8 @@ export const OPT_STYLE_ALL = [
OPT_STYLE_LINE, OPT_STYLE_LINE,
OPT_STYLE_DOTLINE, OPT_STYLE_DOTLINE,
OPT_STYLE_DASHLINE, OPT_STYLE_DASHLINE,
OPT_STYLE_DASHBOX,
OPT_STYLE_WAVYLINE, OPT_STYLE_WAVYLINE,
OPT_STYLE_DASHBOX,
OPT_STYLE_FUZZY, OPT_STYLE_FUZZY,
OPT_STYLE_HIGHLIGHT, OPT_STYLE_HIGHLIGHT,
OPT_STYLE_BLOCKQUOTE, OPT_STYLE_BLOCKQUOTE,

View File

@@ -1,6 +1,6 @@
import { import {
OPT_DICT_BAIDU, OPT_DICT_BING,
OPT_SUG_BAIDU, OPT_SUG_YOUDAO,
DEFAULT_HTTP_TIMEOUT, DEFAULT_HTTP_TIMEOUT,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
DEFAULT_API_LIST, DEFAULT_API_LIST,
@@ -91,8 +91,8 @@ export const DEFAULT_TRANBOX_SETTING = {
followSelection: false, // 翻译框是否跟随选中文本 followSelection: false, // 翻译框是否跟随选中文本
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式 triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
// extStyles: "", // 附加样式 // extStyles: "", // 附加样式
enDict: OPT_DICT_BAIDU, // 英文词典 enDict: OPT_DICT_BING, // 英文词典
enSug: OPT_SUG_BAIDU, // 英文建议 enSug: OPT_SUG_YOUDAO, // 英文建议
}; };
// 订阅列表 // 订阅列表

View File

@@ -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 { DEFAULT_API_LIST, API_SPE_TYPES } from "../config";
import { useSetting } from "./Setting"; import { useSetting } from "./Setting";
@@ -12,6 +12,19 @@ function useApiState() {
export function useApiList() { export function useApiList() {
const { transApis, updateSetting } = useApiState(); 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( const userApis = useMemo(
() => () =>
transApis transApis
@@ -55,7 +68,9 @@ export function useApiList() {
(apiSlug) => { (apiSlug) => {
updateSetting((prev) => ({ updateSetting((prev) => ({
...prev, ...prev,
transApis: (prev?.transApis || []).filter((api) => api.apiSlug !== apiSlug), transApis: (prev?.transApis || []).filter(
(api) => api.apiSlug !== apiSlug
),
})); }));
}, },
[updateSetting] [updateSetting]

View File

@@ -15,3 +15,6 @@ function _browser() {
export const browser = _browser(); export const browser = _browser();
export const isBg = () => globalThis?.ContextType === "BACKGROUND"; export const isBg = () => globalThis?.ContextType === "BACKGROUND";
export const isBuiltinAIAvailable =
"LanguageDetector" in globalThis && "Translator" in globalThis;

168
src/libs/builtinAI.js Normal file
View 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);

View File

@@ -45,7 +45,7 @@ const newCacheReq = async (input, init) => {
* @param {*} init * @param {*} init
* @returns * @returns
*/ */
export const getHttpCache = async (input, init) => { export const getHttpCache = async ({ input, init }) => {
try { try {
const req = await newCacheReq(input, init); const req = await newCacheReq(input, init);
const cache = await caches.open(CACHE_NAME); const cache = await caches.open(CACHE_NAME);
@@ -65,12 +65,12 @@ export const getHttpCache = async (input, init) => {
* @param {*} init * @param {*} init
* @param {*} data * @param {*} data
*/ */
export const putHttpCache = async ( export const putHttpCache = async ({
input, input,
init, init,
data, data,
maxAge = DEFAULT_CACHE_TIMEOUT // todo: 从设置里面读取最大缓存时间 maxAge = DEFAULT_CACHE_TIMEOUT, // todo: 从设置里面读取最大缓存时间
) => { }) => {
try { try {
const req = await newCacheReq(input, init); const req = await newCacheReq(input, init);
const cache = await caches.open(CACHE_NAME); const cache = await caches.open(CACHE_NAME);
@@ -132,7 +132,7 @@ export const getHttpCachePolyfill = (input, init) => {
} }
// 油猴/网页/BackgroundPage // 油猴/网页/BackgroundPage
return getHttpCache(input, init); return getHttpCache({ input, init });
}; };
/** /**
@@ -149,5 +149,5 @@ export const putHttpCachePolyfill = (input, init, data) => {
} }
// 油猴/网页/BackgroundPage // 油猴/网页/BackgroundPage
return putHttpCache(input, init, data); return putHttpCache({ input, init, data });
}; };

View File

@@ -5,7 +5,8 @@ import {
OPT_TRANS_TENCENT, OPT_TRANS_TENCENT,
OPT_LANGS_TO_CODE, OPT_LANGS_TO_CODE,
OPT_LANGS_MAP, OPT_LANGS_MAP,
API_SPE_TYPES, OPT_TRANS_BUILTINAI,
OPT_LANGDETECTOR_MAP,
} from "../config"; } from "../config";
import { browser } from "./browser"; import { browser } from "./browser";
import { import {
@@ -13,6 +14,7 @@ import {
apiMicrosoftLangdetect, apiMicrosoftLangdetect,
apiBaiduLangdetect, apiBaiduLangdetect,
apiTencentLangdetect, apiTencentLangdetect,
apiBuiltinAIDetect,
} from "../apis"; } from "../apis";
import { kissLog } from "./log"; import { kissLog } from "./log";
@@ -21,6 +23,7 @@ const langdetectFns = {
[OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect, [OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect,
[OPT_TRANS_BAIDU]: apiBaiduLangdetect, [OPT_TRANS_BAIDU]: apiBaiduLangdetect,
[OPT_TRANS_TENCENT]: apiTencentLangdetect, [OPT_TRANS_TENCENT]: apiTencentLangdetect,
[OPT_TRANS_BUILTINAI]: apiBuiltinAIDetect,
}; };
/** /**
@@ -31,8 +34,8 @@ const langdetectFns = {
export const tryDetectLang = async (text, langDetector = "-") => { export const tryDetectLang = async (text, langDetector = "-") => {
let deLang = ""; let deLang = "";
// 远程识别 // 内置AI/远程识别
if (API_SPE_TYPES.detector.has(langDetector)) { if (OPT_LANGDETECTOR_MAP.has(langDetector)) {
try { try {
const lang = await langdetectFns[langDetector](text); const lang = await langdetectFns[langDetector](text);
if (lang) { if (lang) {

View File

@@ -101,14 +101,14 @@ export const fetchHandle = async ({ input, init, opts }) => {
* @param {*} args * @param {*} args
* @returns * @returns
*/ */
const fetchPolyfill = (args) => { export const fnPolyfill = ({ fn, msg = MSG_FETCH, ...args }) => {
// 插件 // 插件
if (isExt && !isBg()) { if (isExt && !isBg()) {
return sendBgMsg(MSG_FETCH, args); return sendBgMsg(msg, { ...args });
} }
// 油猴/网页/BackgroundPage // 油猴/网页/BackgroundPage
return fetchHandle(args); return fn({ ...args });
}; };
/** /**
@@ -138,9 +138,9 @@ export const fetchData = async (
// 通过任务池发送请求 // 通过任务池发送请求
if (usePool) { if (usePool) {
const fetchPool = getFetchPool(fetchInterval, fetchLimit); 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 });
}; };

View File

@@ -274,12 +274,6 @@ export class Translator {
#glossary = {}; // AI词典 #glossary = {}; // AI词典
#textClass = {}; // 译文样式class #textClass = {}; // 译文样式class
#textSheet = ""; // 译文样式字典 #textSheet = ""; // 译文样式字典
#apiSetting = null;
#placeholder = {
startDelimiter: "{",
endDelimiter: "}",
tagName: "i",
};
#isUserscript = false; #isUserscript = false;
#transboxManager = null; // 划词翻译 #transboxManager = null; // 划词翻译
@@ -309,20 +303,30 @@ export class Translator {
return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`; return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
} }
constructor(rule = {}, setting = {}, isUserscript = false) { // 接口参数
this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting }; // todo: 不用频繁查找计算
this.#rule = { ...Translator.DEFAULT_RULE, ...rule }; get #apiSetting() {
this.#apiSetting = return (
this.#setting.transApis.find( this.#setting.transApis.find(
(api) => api.apiSlug === this.#rule.apiSlug (api) => api.apiSlug === this.#rule.apiSlug
) || DEFAULT_API_SETTING; ) || DEFAULT_API_SETTING
);
}
// 占位符
get #placeholder() {
const [startDelimiter, endDelimiter] = const [startDelimiter, endDelimiter] =
this.#apiSetting.placeholder.split(" "); this.#apiSetting.placeholder.split(" ");
this.#placeholder = { return {
startDelimiter, startDelimiter,
endDelimiter, endDelimiter,
tagName: this.#apiSetting.placetag, 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.#isUserscript = isUserscript;
this.#eventName = genEventName(); this.#eventName = genEventName();

View File

@@ -333,3 +333,20 @@ export const parseUrlPattern = (href) => {
} }
return 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)
),
]);
};

View File

@@ -30,6 +30,7 @@ import {
OPT_TRANS_OLLAMA, OPT_TRANS_OLLAMA,
OPT_TRANS_CUSTOMIZE, OPT_TRANS_CUSTOMIZE,
OPT_TRANS_NIUTRANS, OPT_TRANS_NIUTRANS,
OPT_TRANS_BUILTINAI,
DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT,
DEFAULT_FETCH_INTERVAL, DEFAULT_FETCH_INTERVAL,
DEFAULT_HTTP_TIMEOUT, DEFAULT_HTTP_TIMEOUT,
@@ -253,32 +254,33 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
onChange={handleChange} onChange={handleChange}
/> />
{!API_SPE_TYPES.machine.has(apiType) && ( {!API_SPE_TYPES.machine.has(apiType) &&
<> apiType !== OPT_TRANS_BUILTINAI && (
<TextField <>
size="small" <TextField
label={"URL"} size="small"
name="url" label={"URL"}
value={url} name="url"
onChange={handleChange} value={url}
multiline={apiType === OPT_TRANS_DEEPLX} onChange={handleChange}
maxRows={10} multiline={apiType === OPT_TRANS_DEEPLX}
helperText={ maxRows={10}
apiType === OPT_TRANS_DEEPLX ? i18n("mulkeys_help") : "" helperText={
} apiType === OPT_TRANS_DEEPLX ? i18n("mulkeys_help") : ""
/> }
<TextField />
size="small" <TextField
label={"KEY"} size="small"
name="key" label={"KEY"}
value={key} name="key"
onChange={handleChange} value={key}
multiline={API_SPE_TYPES.mulkeys.has(apiType)} onChange={handleChange}
maxRows={10} multiline={API_SPE_TYPES.mulkeys.has(apiType)}
helperText={keyHelper} maxRows={10}
/> helperText={keyHelper}
</> />
)} </>
)}
{API_SPE_TYPES.ai.has(apiType) && ( {API_SPE_TYPES.ai.has(apiType) && (
<> <>
@@ -606,65 +608,71 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
</Grid> </Grid>
</Box> </Box>
<TextField {apiType !== OPT_TRANS_BUILTINAI && (
size="small"
label={i18n("custom_header")}
name="customHeader"
value={customHeader}
onChange={handleChange}
multiline
maxRows={10}
helperText={i18n("custom_header_help")}
/>
<TextField
size="small"
label={i18n("custom_body")}
name="customBody"
value={customBody}
onChange={handleChange}
multiline
maxRows={10}
helperText={i18n("custom_body_help")}
/>
{apiType !== OPT_TRANS_CUSTOMIZE && (
<> <>
{" "}
<TextField <TextField
size="small" size="small"
label={"Request Hook"} label={i18n("custom_header")}
name="reqHook" name="customHeader"
value={reqHook} value={customHeader}
onChange={handleChange} onChange={handleChange}
multiline multiline
maxRows={10} maxRows={10}
FormHelperTextProps={{ helperText={i18n("custom_header_help")}
component: "div",
}}
helperText={
<Box component="pre" sx={{ overflowX: "auto" }}>
{i18n("request_hook_helper")}
</Box>
}
/> />
<TextField <TextField
size="small" size="small"
label={"Response Hook"} label={i18n("custom_body")}
name="resHook" name="customBody"
value={resHook} value={customBody}
onChange={handleChange} onChange={handleChange}
multiline multiline
maxRows={10} maxRows={10}
FormHelperTextProps={{ helperText={i18n("custom_body_help")}
component: "div",
}}
helperText={
<Box component="pre" sx={{ overflowX: "auto" }}>
{i18n("response_hook_helper")}
</Box>
}
/> />
</> </>
)} )}
{apiType !== OPT_TRANS_CUSTOMIZE &&
apiType !== OPT_TRANS_BUILTINAI && (
<>
<TextField
size="small"
label={"Request Hook"}
name="reqHook"
value={reqHook}
onChange={handleChange}
multiline
maxRows={10}
FormHelperTextProps={{
component: "div",
}}
helperText={
<Box component="pre" sx={{ overflowX: "auto" }}>
{i18n("request_hook_helper")}
</Box>
}
/>
<TextField
size="small"
label={"Response Hook"}
name="resHook"
value={resHook}
onChange={handleChange}
multiline
maxRows={10}
FormHelperTextProps={{
component: "div",
}}
helperText={
<Box component="pre" sx={{ overflowX: "auto" }}>
{i18n("response_hook_helper")}
</Box>
}
/>
</>
)}
</> </>
)} )}
@@ -782,7 +790,11 @@ export default function Apis() {
return ( return (
<Box> <Box>
<Stack spacing={3}> <Stack spacing={3}>
<Alert severity="info">{i18n("about_api")}</Alert> <Alert severity="info">
{i18n("about_api")}
<br />
{i18n("about_api_2")}
</Alert>
<Box> <Box>
<Button <Button

View File

@@ -5,7 +5,6 @@ import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary"; import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails"; import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
// import CircularProgress from "@mui/material/CircularProgress";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { useFavWords } from "../../hooks/FavWords"; import { useFavWords } from "../../hooks/FavWords";
@@ -18,11 +17,9 @@ import ClearAllIcon from "@mui/icons-material/ClearAll";
import Alert from "@mui/material/Alert"; import Alert from "@mui/material/Alert";
import { isValidWord } from "../../libs/utils"; import { isValidWord } from "../../libs/utils";
import { kissLog } from "../../libs/log"; import { kissLog } from "../../libs/log";
import { apiTranslate } from "../../apis";
import { OPT_TRANS_BAIDU, PHONIC_MAP } from "../../config";
import { useConfirm } from "../../hooks/Confirm"; import { useConfirm } from "../../hooks/Confirm";
import { useSetting } from "../../hooks/Setting"; import { useSetting } from "../../hooks/Setting";
import { DICT_MAP } from "../Selection/DictMap"; import { dictHandlers } from "../Selection/DictHandler";
function FavAccordion({ word, index }) { function FavAccordion({ word, index }) {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
@@ -83,17 +80,20 @@ export default function FavWords() {
const handleTranslation = async () => { const handleTranslation = async () => {
const { enDict } = setting?.tranboxSetting; const { enDict } = setting?.tranboxSetting;
const dict = DICT_MAP[enDict]; const dict = dictHandlers[enDict];
if (!dict) return ""; if (!dict) return "";
const tranList = []; const tranList = [];
for (const word of wordList) { for (const word of wordList) {
try { try {
const data = await dict.apiFn(word); const data = await dict.apiFn(word);
const tran = dict.toText(data); const tran = dict
tranList.push([word, tran].join("\n")); .toText(data)
.map((line) => `- ${line}`)
.join("\n");
tranList.push([`## ${word}`, tran].join("\n"));
} catch (err) { } catch (err) {
// skip kissLog("export translation", err);
} }
} }

View File

@@ -289,6 +289,64 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem> <MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
</TextField> </TextField>
</Grid> </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}> <Grid item xs={12} sm={12} md={6} lg={3}>
<TextField <TextField
select select
@@ -354,22 +412,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
<MenuItem value={"true"}>{i18n("enable")}</MenuItem> <MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField> </TextField>
</Grid> </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}> <Grid item xs={12} sm={12} md={6} lg={3}>
<TextField <TextField
select select
@@ -386,64 +429,23 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
<MenuItem value={"true"}>{i18n("enable")}</MenuItem> <MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField> </TextField>
</Grid> </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}> <Grid item xs={12} sm={12} md={6} lg={3}>
<TextField <TextField
select select

View File

@@ -340,7 +340,7 @@ export default function Settings() {
size="small" size="small"
name="langDetector" name="langDetector"
value={langDetector} value={langDetector}
label={i18n("detect_lang_remote")} label={i18n("detected_lang")}
onChange={handleChange} onChange={handleChange}
> >
<MenuItem value={"-"}>{i18n("disable")}</MenuItem> <MenuItem value={"-"}>{i18n("disable")}</MenuItem>

View File

@@ -9,10 +9,10 @@ import {
OPT_LANGS_TO, OPT_LANGS_TO,
OPT_TRANBOX_TRIGGER_CLICK, OPT_TRANBOX_TRIGGER_CLICK,
OPT_TRANBOX_TRIGGER_ALL, OPT_TRANBOX_TRIGGER_ALL,
OPT_DICT_BAIDU, OPT_DICT_BING,
OPT_DICT_ALL, OPT_DICT_ALL,
OPT_SUG_ALL, OPT_SUG_ALL,
OPT_SUG_BAIDU, OPT_SUG_YOUDAO,
} from "../../config"; } from "../../config";
import ShortcutInput from "./ShortcutInput"; import ShortcutInput from "./ShortcutInput";
import FormControlLabel from "@mui/material/FormControlLabel"; import FormControlLabel from "@mui/material/FormControlLabel";
@@ -69,8 +69,8 @@ export default function Tranbox() {
followSelection = false, followSelection = false,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK, triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
// extStyles = "", // extStyles = "",
enDict = OPT_DICT_BAIDU, enDict = OPT_DICT_BING,
enSug = OPT_SUG_BAIDU, enSug = OPT_SUG_YOUDAO,
} = tranboxSetting; } = tranboxSetting;
return ( return (

View File

@@ -7,7 +7,7 @@ import Divider from "@mui/material/Divider";
import Alert from "@mui/material/Alert"; import Alert from "@mui/material/Alert";
import CopyBtn from "./CopyBtn"; import CopyBtn from "./CopyBtn";
import { useAsyncNow } from "../../hooks/Fetch"; import { useAsyncNow } from "../../hooks/Fetch";
import { DICT_MAP } from "./DictMap"; import { dictHandlers } from "./DictHandler";
function DictBody({ text, setCopyText, dict }) { function DictBody({ text, setCopyText, dict }) {
const { loading, error, data } = useAsyncNow(dict.apiFn, text); const { loading, error, data } = useAsyncNow(dict.apiFn, text);
@@ -17,7 +17,7 @@ function DictBody({ text, setCopyText, dict }) {
return; return;
} }
const copyText = [text, dict.toText(data)].join("\n"); const copyText = [text, dict.toText(data).join("\n")].join("\n");
setCopyText(copyText); setCopyText(copyText);
}, [data, text, dict, setCopyText]); }, [data, text, dict, setCopyText]);
@@ -46,7 +46,7 @@ function DictBody({ text, setCopyText, dict }) {
export default function DictCont({ text, enDict }) { export default function DictCont({ text, enDict }) {
const [copyText, setCopyText] = useState(text); const [copyText, setCopyText] = useState(text);
const dict = DICT_MAP[enDict]; const dict = dictHandlers[enDict];
return ( return (
<Stack spacing={1}> <Stack spacing={1}>

View File

@@ -3,13 +3,11 @@ import AudioBtn from "./AudioBtn";
import { OPT_DICT_BING, OPT_DICT_YOUDAO } from "../../config"; import { OPT_DICT_BING, OPT_DICT_YOUDAO } from "../../config";
import { apiMicrosoftDict, apiYoudaoDict } from "../../apis"; import { apiMicrosoftDict, apiYoudaoDict } from "../../apis";
export const DICT_MAP = { export const dictHandlers = {
[OPT_DICT_BING]: { [OPT_DICT_BING]: {
apiFn: apiMicrosoftDict, apiFn: apiMicrosoftDict,
toText: (data) => toText: (data) =>
data.trs data.trs?.map(({ pos, def }) => `${pos ? `[${pos}] ` : ""}${def}`) || [],
?.map(({ pos, def }) => `${pos ? `[${pos}] ` : ""}${def}`)
.join("\n"),
uiAudio: (data) => ( uiAudio: (data) => (
<Typography component="div"> <Typography component="div">
{data?.aus.map(({ key, audio, phonetic }) => ( {data?.aus.map(({ key, audio, phonetic }) => (
@@ -38,9 +36,9 @@ export const DICT_MAP = {
[OPT_DICT_YOUDAO]: { [OPT_DICT_YOUDAO]: {
apiFn: apiYoudaoDict, apiFn: apiYoudaoDict,
toText: (data) => toText: (data) =>
data?.ec?.word?.trs data?.ec?.word?.trs?.map(
?.map(({ pos, tran }) => `${pos ? `[${pos}] ` : ""}${tran}`) ({ pos, tran }) => `${pos ? `[${pos}] ` : ""}${tran}`
.join("\n"), ) || [],
uiAudio: () => null, uiAudio: () => null,
uiTrans: (data) => ( uiTrans: (data) => (
<Typography component="ul"> <Typography component="ul">