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

@@ -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
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
* @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 });
};

View File

@@ -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) {

View File

@@ -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 });
};

View File

@@ -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();

View File

@@ -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)
),
]);
};