专为油猴脚本 (Userscript) 打造的高性能单文件翻译 SDK。内置 7 大开箱即用稳定翻译源,提供两级缓存 (L1/L2)、并发去重、自动批处理、故障转移 (Failover) 与半开熔断防护等核心能力。
Dit script moet niet direct worden geïnstalleerd - het is een bibliotheek voor andere scripts om op te nemen met de meta-richtlijn // @require https://update.greasyfork.org/scripts/591255/1902101/TransKit.js
// ==UserScript==
// @name TransKit
// @namespace You Boy
// @version 1.0.1
// @description 专为油猴脚本 (Userscript) 打造的高性能单文件翻译 SDK。内置 7 大开箱即用稳定翻译源,提供两级缓存 (L1/L2)、并发去重、自动批处理、故障转移 (Failover) 与半开熔断防护等核心能力。
// @author You Boy
// @license MIT
// @grant none
// @connect translate.google.com
// @connect translate.googleapis.com
// @connect edge.microsoft.com
// @connect transmart.qq.com
// @connect www2.deepl.com
// @connect api.interpreter.caiyunai.com
// @connect m.youdao.com
// ==/UserScript==
(function (global) {
"use strict";
/* ============================================================================
* TransKit v1 - Userscript Translation SDK (Final Release)
* 单文件、面向 Userscript 环境的翻译 SDK 规范实现
* ============================================================================ */
var VERSION = "1.0.2";
/* ============================================================================
* 1. 基础工具类 (Utils)
* ============================================================================ */
var Utils = {
getGlobal: function (name) {
try {
return global[name];
} catch (e) {
return undefined;
}
},
isFunction: function (fn) {
return typeof fn === "function";
},
isObject: function (obj) {
return obj !== null && typeof obj === "object";
},
isString: function (value) {
return typeof value === "string";
},
isArray: function (value) {
return Array.isArray(value);
},
now: function () {
return Date.now();
},
randomId: function (prefix) {
return (
(prefix || "tk") +
"_" +
Date.now().toString(36) +
"_" +
Math.random().toString(36).slice(2, 10)
);
},
guid: function () {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0;
var v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
},
clamp: function (value, min, max) {
return Math.max(min, Math.min(max, value));
},
encodeForm: function (obj) {
var result = [];
Object.keys(obj).forEach(function (key) {
var value = obj[key];
if (value === undefined || value === null) value = "";
result.push(encodeURIComponent(key) + "=" + encodeURIComponent(String(value)));
});
return result.join("&");
},
safeJSON: function (text) {
if (typeof text !== "string") return text;
try {
return JSON.parse(text);
} catch (e) {
return null;
}
},
sleep: function (ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
},
// 使用 Math.imul 的双 32-bit (DJB2 + FNV-1a) 组合哈希,显著降低短字符串缓存 Key 碰撞概率
fastHash: function (str) {
var h1 = 5381;
var h2 = 2166136261;
var i = str.length;
while (i) {
var code = str.charCodeAt(--i);
h1 = (h1 * 33) ^ code;
h2 = Math.imul(h2 ^ code, 16777619);
}
return (h1 >>> 0).toString(36) + (h2 >>> 0).toString(36);
}
};
/* ============================================================================
* 2. 统一异常体系 (Error Contract)
* ============================================================================ */
function TransKitError(message, code, details) {
this.name = "TransKitError";
this.message = message || "TransKit error";
this.code = code || "TRANSKIT_ERROR";
this.details = details || null;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
TransKitError.prototype = Object.create(Error.prototype);
TransKitError.prototype.constructor = TransKitError;
function ProviderError(message, code, details) {
TransKitError.call(this, message, code || "PROVIDER_ERROR", details);
this.name = "ProviderError";
}
ProviderError.prototype = Object.create(TransKitError.prototype);
ProviderError.prototype.constructor = ProviderError;
function TranslationError(message, code, details) {
TransKitError.call(this, message, code || "TRANSLATION_ERROR", details);
this.name = "TranslationError";
}
TranslationError.prototype = Object.create(TransKitError.prototype);
TranslationError.prototype.constructor = TranslationError;
function NetworkError(message, code, details) {
TransKitError.call(this, message, code || "NETWORK_ERROR", details);
this.name = "NetworkError";
}
NetworkError.prototype = Object.create(TransKitError.prototype);
NetworkError.prototype.constructor = NetworkError;
function TimeoutError(message, code, details) {
TransKitError.call(this, message, code || "REQUEST_TIMEOUT", details);
this.name = "TimeoutError";
}
TimeoutError.prototype = Object.create(TransKitError.prototype);
TimeoutError.prototype.constructor = TimeoutError;
function CacheError(message, code, details) {
TransKitError.call(this, message, code || "CACHE_ERROR", details);
this.name = "CacheError";
}
CacheError.prototype = Object.create(TransKitError.prototype);
CacheError.prototype.constructor = CacheError;
function ConfigurationError(message, code, details) {
TransKitError.call(this, message, code || "CONFIGURATION_ERROR", details);
this.name = "ConfigurationError";
}
ConfigurationError.prototype = Object.create(TransKitError.prototype);
ConfigurationError.prototype.constructor = ConfigurationError;
function CircuitOpenError(message, code, details) {
TransKitError.call(this, message, code || "CIRCUIT_OPEN", details);
this.name = "CircuitOpenError";
}
CircuitOpenError.prototype = Object.create(TransKitError.prototype);
CircuitOpenError.prototype.constructor = CircuitOpenError;
/* ============================================================================
* 3. 加密与哈希服务 (Crypto Service)
* ============================================================================ */
function utf8Bytes(str) {
var bytes = [];
str = String(str);
for (var i = 0; i < str.length; i++) {
var c = str.charCodeAt(i);
if (c < 0x80) {
bytes.push(c);
} else if (c < 0x800) {
bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
} else if (c >= 0xd800 && c <= 0xdbff && i + 1 < str.length) {
var c2 = str.charCodeAt(++i);
var cp = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
bytes.push(
0xf0 | (cp >> 18),
0x80 | ((cp >> 12) & 0x3f),
0x80 | ((cp >> 6) & 0x3f),
0x80 | (cp & 0x3f)
);
} else {
bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
}
}
return bytes;
}
function utf8Decode(bytes) {
var result = "";
for (var i = 0; i < bytes.length; ) {
var c = bytes[i++];
if (c < 0x80) {
result += String.fromCharCode(c);
} else if (c < 0xe0) {
var c2 = bytes[i++];
result += String.fromCharCode(((c & 0x1f) << 6) | (c2 & 0x3f));
} else if (c < 0xf0) {
var c3 = bytes[i++];
var c4 = bytes[i++];
result += String.fromCharCode(((c & 0x0f) << 12) | ((c3 & 0x3f) << 6) | (c4 & 0x3f));
} else {
var c5 = bytes[i++];
var c6 = bytes[i++];
var c7 = bytes[i++];
var cp = ((c & 0x07) << 18) | ((c5 & 0x3f) << 12) | ((c6 & 0x3f) << 6) | (c7 & 0x3f);
cp -= 0x10000;
result += String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff));
}
}
return result;
}
function leftRotate(x, c) {
return (x << c) | (x >>> (32 - c));
}
function md5WordsFromBytes(bytes) {
var originalLength = bytes.length;
var bitLength = originalLength * 8;
var paddedBytes = bytes.slice();
paddedBytes.push(0x80);
while (paddedBytes.length % 64 !== 56) paddedBytes.push(0);
for (var i = 0; i < 8; i++) {
paddedBytes.push(bitLength % 256);
bitLength = Math.floor(bitLength / 256);
}
var a0 = 0x67452301, b0 = 0xefcdab89, c0 = 0x98badcfe, d0 = 0x10325476;
var k = [], s = [
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
];
for (i = 0; i < 64; i++) k[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 4294967296);
for (var offset = 0; offset < paddedBytes.length; offset += 64) {
var m = new Array(16);
for (i = 0; i < 16; i++) {
var p = offset + i * 4;
m[i] = paddedBytes[p] | (paddedBytes[p + 1] << 8) | (paddedBytes[p + 2] << 16) | (paddedBytes[p + 3] << 24);
}
var a = a0, b = b0, c = c0, d = d0;
for (i = 0; i < 64; i++) {
var f, g;
if (i < 16) { f = (b & c) | (~b & d); g = i; }
else if (i < 32) { f = (d & b) | (~d & c); g = (5 * i + 1) % 16; }
else if (i < 48) { f = b ^ c ^ d; g = (3 * i + 5) % 16; }
else { f = c ^ (b | ~d); g = (7 * i) % 16; }
var temp = d; d = c; c = b;
b = (b + leftRotate((a + f + k[i] + m[g]) | 0, s[i])) | 0;
a = temp;
}
a0 = (a0 + a) | 0; b0 = (b0 + b) | 0; c0 = (c0 + c) | 0; d0 = (d0 + d) | 0;
}
return [a0, b0, c0, d0];
}
function md5HexFromBytes(bytes) {
var words = md5WordsFromBytes(bytes);
var result = "";
for (var i = 0; i < words.length; i++) {
var word = words[i];
for (var j = 0; j < 4; j++) {
var b = (word >>> (j * 8)) & 0xff;
result += (b < 16 ? "0" : "") + b.toString(16);
}
}
return result;
}
function md5Hex(str) {
return md5HexFromBytes(utf8Bytes(str));
}
function hexToBytes(hex) {
var bytes = [];
for (var c = 0; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substr(c, 2), 16));
}
return bytes;
}
var BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function base64EncodeBytes(bytes) {
var result = "";
for (var i = 0; i < bytes.length; i += 3) {
var a = bytes[i], b = i + 1 < bytes.length ? bytes[i + 1] : 0, c = i + 2 < bytes.length ? bytes[i + 2] : 0;
var triple = (a << 16) | (b << 8) | c;
result += BASE64_CHARS[(triple >> 18) & 63];
result += BASE64_CHARS[(triple >> 12) & 63];
result += i + 1 < bytes.length ? BASE64_CHARS[(triple >> 6) & 63] : "=";
result += i + 2 < bytes.length ? BASE64_CHARS[triple & 63] : "=";
}
return result;
}
function base64Encode(str) {
return base64EncodeBytes(utf8Bytes(str));
}
function base64Decode(str) {
var binary = "";
try {
if (typeof atob === "function") {
binary = atob(str);
} else if (global && typeof global.atob === "function") {
binary = global.atob(str);
} else {
throw new Error("atob unavailable");
}
} catch (e) {
throw new TransKitError("Base64 decode failed", "BASE64_DECODE_ERROR", e);
}
var bytes = [];
for (var i = 0; i < binary.length; i++) bytes.push(binary.charCodeAt(i));
return utf8Decode(bytes);
}
function hmacMd5(message, key) {
var keyBytes = utf8Bytes(key);
if (keyBytes.length > 64) keyBytes = hexToBytes(md5HexFromBytes(keyBytes));
while (keyBytes.length < 64) keyBytes.push(0);
var ipad = [], opad = [];
for (var i = 0; i < 64; i++) {
ipad.push(keyBytes[i] ^ 0x36);
opad.push(keyBytes[i] ^ 0x5c);
}
var inner = ipad.concat(utf8Bytes(message));
var innerHashHex = md5HexFromBytes(inner);
var innerHashBytes = hexToBytes(innerHashHex);
var outer = opad.concat(innerHashBytes);
return md5HexFromBytes(outer);
}
function xorEncrypt(text, key) {
var textBytes = utf8Bytes(text);
var keyBytes = utf8Bytes(key);
var out = [];
for (var i = 0; i < textBytes.length; i++) {
out.push(textBytes[i] ^ keyBytes[i % keyBytes.length]);
}
return base64EncodeBytes(out);
}
function xorDecrypt(ciphertext, key) {
var bytes = [];
var binary = atob(ciphertext);
for (var i = 0; i < binary.length; i++) bytes.push(binary.charCodeAt(i));
var keyBytes = utf8Bytes(key);
var out = [];
for (var j = 0; j < bytes.length; j++) {
out.push(bytes[j] ^ keyBytes[j % keyBytes.length]);
}
return utf8Decode(out);
}
var CryptoService = {
md5: function (value) {
return md5Hex(String(value));
},
hmacMd5: function (value, key) {
return hmacMd5(String(value), String(key));
},
hmacMD5Base64: function (value, key) {
var hex = hmacMd5(String(value), String(key));
return base64EncodeBytes(hexToBytes(hex));
},
base64Encode: function (value) {
return base64Encode(String(value));
},
base64Decode: function (value) {
return base64Decode(String(value));
},
xorEncrypt: xorEncrypt,
xorDecrypt: xorDecrypt
};
/* ============================================================================
* 4. Runtime 注册表管理与深度配置合并
* ============================================================================ */
var Runtime = {
inflight: new Map(),
breakers: new Map(),
services: new Map(),
providers: new Map(),
strategies: new Map(),
_config: {}
};
function mergeConfig(base, override) {
base = base || {};
override = override || {};
var result = Object.assign({}, base, override);
if (base.cache || override.cache) {
result.cache = Object.assign({}, base.cache || {}, override.cache || {});
}
if (base.circuitBreaker || override.circuitBreaker) {
result.circuitBreaker = Object.assign({}, base.circuitBreaker || {}, override.circuitBreaker || {});
}
if (base.retry || override.retry) {
if (typeof override.retry === "number" || typeof base.retry === "number") {
result.retry = override.retry !== undefined ? override.retry : base.retry;
} else {
result.retry = Object.assign({}, base.retry || {}, override.retry || {});
}
}
if (base.services || override.services) {
result.services = Object.assign({}, base.services || {}, override.services || {});
}
return result;
}
// Service Registry
var ServiceRegistry = {
register: function (id, Service, options) {
options = options || {};
if (Runtime.services.has(id) && !options.override) {
throw new ConfigurationError("Service already exists: " + id, "SERVICE_EXISTS");
}
var instance = typeof Service === "function" ? new Service(options.options || {}) : Service;
Runtime.services.set(id, instance);
return instance;
},
get: function (id) {
return Runtime.services.get(id);
},
list: function () {
return Array.from(Runtime.services.keys());
},
unregister: function (id) {
return Runtime.services.delete(id);
}
};
// Provider Registry
var ProviderRegistry = {
register: function (Provider, options) {
options = options || {};
var instance = typeof Provider === "function" ? new Provider(options) : Provider;
var id = options.id || instance.id || Provider.id;
if (!id) {
throw new ConfigurationError("Provider id is required", "PROVIDER_ID_REQUIRED");
}
if (Runtime.providers.has(id) && !options.override) {
throw new ConfigurationError("Provider already exists: " + id, "PROVIDER_EXISTS");
}
instance.id = id;
if (instance.priority === undefined) {
instance.priority = Provider.priority !== undefined ? Provider.priority : 0;
}
if (!instance.capabilities) {
instance.capabilities = {
translate: true,
detect: false,
batch: false,
streaming: false
};
}
Runtime.providers.set(id, instance);
return instance;
},
get: function (id) {
return Runtime.providers.get(id);
},
list: function () {
return Array.from(Runtime.providers.values()).map(function (provider) {
return {
id: provider.id,
name: provider.name || provider.id,
version: provider.version || VERSION,
priority: provider.priority || 0,
capabilities: Object.assign({}, provider.capabilities || {})
};
});
},
unregister: function (id) {
return Runtime.providers.delete(id);
}
};
// Strategy Registry
var StrategyRegistry = {
register: function (id, Strategy, options) {
options = options || {};
if (Runtime.strategies.has(id) && !options.override) {
throw new ConfigurationError("Strategy already exists: " + id, "STRATEGY_EXISTS");
}
var instance = typeof Strategy === "function" ? new Strategy(options.options || {}) : Strategy;
Runtime.strategies.set(id, instance);
return instance;
},
get: function (id) {
return Runtime.strategies.get(id);
},
list: function () {
return Array.from(Runtime.strategies.keys());
},
unregister: function (id) {
return Runtime.strategies.delete(id);
}
};
var DefaultStrategy = {
id: "default",
resolve: function (providers) {
return providers.slice().sort(function (a, b) {
return (b.priority || 0) - (a.priority || 0);
});
}
};
// 动态解析 Service 优先级:请求级 -> 实例级 -> 全局级 -> Registry
function resolveService(serviceId, requestOpts, instanceOpts) {
if (requestOpts && requestOpts.services && requestOpts.services[serviceId]) {
return requestOpts.services[serviceId];
}
if (instanceOpts && instanceOpts.services && instanceOpts.services[serviceId]) {
return instanceOpts.services[serviceId];
}
if (Runtime._config && Runtime._config.services && Runtime._config.services[serviceId]) {
return Runtime._config.services[serviceId];
}
return ServiceRegistry.get(serviceId);
}
/* ============================================================================
* 5. 内置标准服务 (Request & Storage Services)
* ============================================================================ */
// 5.1 Request Service
function RequestService(options) {
this.options = options || {};
}
RequestService.prototype.request = function (options) {
var gm = Utils.getGlobal("GM_xmlhttpRequest");
var gmModern = global.GM && global.GM.xmlHttpRequest;
if (Utils.isFunction(gmModern)) {
return this._gmRequest(gmModern.bind(global.GM), options);
}
if (Utils.isFunction(gm)) {
return this._gmRequest(gm, options);
}
return this._nativeRequest(options);
};
RequestService.prototype._gmRequest = function (fn, options) {
var self = this;
return new Promise(function (resolve, reject) {
var timeout = options.timeout || self.options.timeout || 10000;
var finished = false;
var timer = setTimeout(function () {
if (!finished) {
finished = true;
reject(new TimeoutError("Request timeout", "REQUEST_TIMEOUT"));
}
}, timeout);
var config = Object.assign({}, options);
config.timeout = timeout;
config.onload = function (response) {
if (finished) return;
finished = true;
clearTimeout(timer);
if (response.status >= 200 && response.status < 300) {
resolve({
status: response.status,
headers: response.responseHeaders || {},
responseText: response.responseText,
response: response.response
});
} else {
reject(new NetworkError("HTTP " + response.status, "HTTP_ERROR", response));
}
};
config.onerror = function (error) {
if (finished) return;
finished = true;
clearTimeout(timer);
reject(new NetworkError("Network request failed", "NETWORK_ERROR", error));
};
config.ontimeout = function () {
if (finished) return;
finished = true;
clearTimeout(timer);
reject(new TimeoutError("Request timeout", "REQUEST_TIMEOUT"));
};
try {
fn(config);
} catch (e) {
if (!finished) {
finished = true;
clearTimeout(timer);
reject(new NetworkError(e.message, "REQUEST_ERROR", e));
}
}
});
};
RequestService.prototype._nativeRequest = function (options) {
var timeout = options.timeout || this.options.timeout || 10000;
if (typeof fetch === "function") {
var controller = typeof AbortController !== "undefined" ? new AbortController() : null;
var timer = controller ? setTimeout(function () { controller.abort(); }, timeout) : null;
return fetch(options.url, {
method: options.method || "GET",
headers: options.headers || {},
body: options.data,
signal: controller ? controller.signal : options.signal
})
.then(function (res) {
if (timer) clearTimeout(timer);
if (!res.ok) throw new NetworkError("HTTP " + res.status, "HTTP_ERROR");
return res.text().then(function (text) {
return {
status: res.status,
headers: {},
responseText: text,
response: text
};
});
})
.catch(function (err) {
if (timer) clearTimeout(timer);
if (err && err.name === "AbortError") throw new TimeoutError("Request timeout", "REQUEST_TIMEOUT");
throw err;
});
}
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(options.method || "GET", options.url, true);
if (options.headers) {
Object.keys(options.headers).forEach(function (key) {
xhr.setRequestHeader(key, options.headers[key]);
});
}
xhr.timeout = timeout;
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
status: xhr.status,
headers: {},
responseText: xhr.responseText,
response: xhr.response
});
} else {
reject(new NetworkError("HTTP " + xhr.status, "HTTP_ERROR"));
}
};
xhr.onerror = function () { reject(new NetworkError("Network request failed", "NETWORK_ERROR")); };
xhr.ontimeout = function () { reject(new TimeoutError("Request timeout", "REQUEST_TIMEOUT")); };
xhr.send(options.data || null);
});
};
// 5.2 Storage Service
function StorageService() {}
StorageService.prototype.get = function (key) {
var gm = Utils.getGlobal("GM_getValue");
if (Utils.isFunction(gm)) return Promise.resolve(gm(key, null));
if (global.GM && Utils.isFunction(global.GM.getValue)) return global.GM.getValue(key, null);
try {
if (global.localStorage) {
var value = global.localStorage.getItem(key);
return Promise.resolve(value === null ? null : JSON.parse(value));
}
} catch (e) {
return Promise.reject(new CacheError(e.message, "STORAGE_GET_ERROR"));
}
return Promise.resolve(null);
};
StorageService.prototype.set = function (key, value) {
var gm = Utils.getGlobal("GM_setValue");
if (Utils.isFunction(gm)) { gm(key, value); return Promise.resolve(); }
if (global.GM && Utils.isFunction(global.GM.setValue)) return global.GM.setValue(key, value);
try {
if (global.localStorage) global.localStorage.setItem(key, JSON.stringify(value));
return Promise.resolve();
} catch (e) {
return Promise.reject(new CacheError(e.message, "STORAGE_SET_ERROR"));
}
};
StorageService.prototype.delete = function (key) {
var gm = Utils.getGlobal("GM_deleteValue");
if (Utils.isFunction(gm)) { gm(key); return Promise.resolve(); }
if (global.GM && Utils.isFunction(global.GM.deleteValue)) return global.GM.deleteValue(key);
try {
if (global.localStorage) global.localStorage.removeItem(key);
return Promise.resolve();
} catch (e) {
return Promise.reject(new CacheError(e.message, "STORAGE_DELETE_ERROR"));
}
};
StorageService.prototype.remove = StorageService.prototype.delete;
StorageService.prototype.getMany = function (keys) {
var self = this;
if (global.GM && Utils.isFunction(global.GM.getValues)) {
return global.GM.getValues(keys);
}
return Promise.all(keys.map(function (k) { return self.get(k); })).then(function (values) {
var result = {};
keys.forEach(function (k, i) { result[k] = values[i]; });
return result;
});
};
StorageService.prototype.setMany = function (entries) {
var self = this;
if (global.GM && Utils.isFunction(global.GM.setValues)) {
return global.GM.setValues(entries);
}
var promises = Object.keys(entries).map(function (k) {
return self.set(k, entries[k]);
});
return Promise.all(promises).then(function () {});
};
StorageService.prototype.deleteMany = function (keys) {
var self = this;
if (global.GM && Utils.isFunction(global.GM.deleteValues)) {
return global.GM.deleteValues(keys);
}
var promises = keys.map(function (k) { return self.delete(k); });
return Promise.all(promises).then(function () {});
};
StorageService.prototype.clearPrefix = function (prefix) {
var self = this;
var gmLive = Utils.getGlobal("GM_listValues");
if (Utils.isFunction(gmLive)) {
try {
var keys = gmLive();
var toDelete = keys.filter(function (k) { return k.indexOf(prefix) === 0; });
return self.deleteMany(toDelete);
} catch (e) {}
}
if (global.GM && Utils.isFunction(global.GM.listValues)) {
return global.GM.listValues().then(function (keys) {
var toDelete = keys.filter(function (k) { return k.indexOf(prefix) === 0; });
return self.deleteMany(toDelete);
});
}
try {
if (global.localStorage) {
var toDeleteLS = [];
for (var i = 0; i < global.localStorage.length; i++) {
var k = global.localStorage.key(i);
if (k && k.indexOf(prefix) === 0) toDeleteLS.push(k);
}
toDeleteLS.forEach(function (k) { global.localStorage.removeItem(k); });
}
} catch (e) {}
return Promise.resolve();
};
/* ============================================================================
* 6. 缓存服务 (CacheService)
* 支持 L1/L2 两级缓存、热刷新及容量上限淘汰 (maxEntries)
* ============================================================================ */
function CacheService(storage, options) {
this.storage = storage;
this.options = Object.assign(
{
enabled: true,
ttl: 7 * 24 * 60 * 60 * 1000,
refreshOnHit: true,
refreshInterval: 6 * 60 * 60 * 1000,
maxEntries: 10000,
persistent: true
},
options || {}
);
this.memory = new Map();
}
// 动态 Storage 解析器:支持“请求级 -> 实例级 -> 全局级 -> 注册表”优先链
CacheService.prototype._getStorage = function (translator, requestOpts) {
if (requestOpts && requestOpts.services && requestOpts.services.storage) {
return requestOpts.services.storage;
}
if (translator) {
return resolveService("storage", requestOpts, translator.options);
}
return this.storage || ServiceRegistry.get("storage");
};
CacheService.prototype._memoryKey = function (text, source, target) {
return String(text) + "\u0001" + String(source) + "\u0001" + String(target);
};
CacheService.prototype._persistentKey = function (text, source, target) {
var raw = String(text) + "\u0001" + String(source) + "\u0001" + String(target);
return "transkit:cache:" + Utils.fastHash(raw);
};
CacheService.prototype._checkCapacity = function () {
var max = this.options.maxEntries || 10000;
if (this.memory.size < max) return;
var now = Date.now();
this.memory.forEach(function (val, key, map) {
if (val.expiresAt <= now) map.delete(key);
});
if (this.memory.size >= max) {
var targetSize = Math.floor(max * 0.8);
var entries = Array.from(this.memory.entries()).sort(function (a, b) {
return (a[1].lastRefreshAt || a[1].lastRefresh || 0) - (b[1].lastRefreshAt || b[1].lastRefresh || 0);
});
for (var i = 0; i < entries.length && this.memory.size > targetSize; i++) {
this.memory.delete(entries[i][0]);
}
}
};
CacheService.prototype.get = function (text, source, target, translator, requestOpts) {
if (!this.options.enabled) return Promise.resolve(null);
var memoryKey = this._memoryKey(text, source, target);
var now = Date.now();
var memory = this.memory.get(memoryKey);
if (memory && memory.expiresAt > now) {
memory.hitCount = (memory.hitCount || 0) + 1;
memory.lastHitAt = now;
if (
this.options.refreshOnHit &&
now - (memory.lastRefreshAt || memory.lastRefresh || 0) >= this.options.refreshInterval
) {
var newExpiresAt = now + this.options.ttl;
memory.expiresAt = newExpiresAt;
memory.lastRefreshAt = now;
memory.lastRefresh = now;
if (translator) translator._log("CACHE", "Hot Cache Refreshed (L1)", { text: text });
if (this.options.persistent) {
this._persist(text, source, target, memory.value, newExpiresAt, memory, translator, requestOpts);
}
} else if (translator) {
translator._log("CACHE", "Hit L1 Memory Cache", { text: text });
}
return Promise.resolve({
value: memory.value || memory.translatedText,
fromCache: true,
level: "L1"
});
}
if (memory) this.memory.delete(memoryKey);
if (!this.options.persistent) return Promise.resolve(null);
var key = this._persistentKey(text, source, target);
var self = this;
var storage = this._getStorage(translator, requestOpts);
return storage.get(key).then(function (entry) {
if (!entry) {
if (translator) translator._log("CACHE", "Cache Miss", { text: text });
return null;
}
if (entry.expiresAt && entry.expiresAt <= Date.now()) {
storage.delete(key);
if (translator) translator._log("CACHE", "L2 Cache Expired", { text: text });
return null;
}
self._checkCapacity();
var val = entry.value || entry.translatedText;
self.memory.set(memoryKey, {
originalText: text,
translatedText: val,
value: val,
source: source,
target: target,
expiresAt: entry.expiresAt,
createdAt: entry.createdAt || Date.now(),
hitCount: (entry.hitCount || 0) + 1,
lastHitAt: Date.now(),
lastRefreshAt: Date.now(),
lastRefresh: Date.now()
});
if (translator) translator._log("CACHE", "Hit L2 Storage Cache", { text: text });
return {
value: val,
fromCache: true,
level: "L2"
};
});
};
CacheService.prototype.set = function (text, source, target, value, translator, requestOpts) {
if (!this.options.enabled) return Promise.resolve();
var now = Date.now();
var expiresAt = now + this.options.ttl;
var memoryKey = this._memoryKey(text, source, target);
var record = {
originalText: text,
translatedText: value,
value: value,
source: source,
target: target,
createdAt: now,
expiresAt: expiresAt,
hitCount: 0,
lastHitAt: now,
lastRefreshAt: now,
lastRefresh: now
};
this._checkCapacity();
this.memory.set(memoryKey, record);
if (!this.options.persistent) return Promise.resolve();
return this._persist(text, source, target, value, expiresAt, record, translator, requestOpts);
};
CacheService.prototype._persist = function (text, source, target, value, expiresAt, record, translator, requestOpts) {
var key = this._persistentKey(text, source, target);
var storage = this._getStorage(translator, requestOpts);
return storage.set(key, Object.assign({
originalText: text,
translatedText: value,
value: value,
source: source,
target: target,
createdAt: Date.now(),
expiresAt: expiresAt
}, record || {}));
};
CacheService.prototype.clear = function (translator, requestOpts) {
this.memory.clear();
var storage = this._getStorage(translator, requestOpts);
if (storage && Utils.isFunction(storage.clearPrefix)) {
return storage.clearPrefix("transkit:cache:");
}
return Promise.resolve();
};
/* ============================================================================
* 7. 全局熔断器 (Circuit Breaker - 带半开探针锁)
* ============================================================================ */
function CircuitBreaker(providerId, options) {
this.providerId = providerId;
this.options = Object.assign(
{
enabled: false,
failureThreshold: 10,
cooldown: 6 * 60 * 60 * 1000
},
options || {}
);
this.failures = 0;
this.state = "closed";
this.openedAt = 0;
this.halfOpenProbeCount = 0;
}
CircuitBreaker.prototype.allow = function () {
if (!this.options.enabled) return true;
if (this.state === "closed") return true;
var cooldown = this.options.cooldown || this.options.resetTimeout || 21600000;
if (this.state === "open" && Date.now() - this.openedAt >= cooldown) {
this.state = "half-open";
this.halfOpenProbeCount = 0;
}
if (this.state === "half-open") {
if (this.halfOpenProbeCount > 0) {
return false; // 探针锁:半开恢复期严格限制只放行 1 个试探请求
}
this.halfOpenProbeCount++;
return true;
}
return false;
};
CircuitBreaker.prototype.success = function () {
if (!this.options.enabled) return;
this.failures = 0;
this.state = "closed";
this.openedAt = 0;
this.halfOpenProbeCount = 0;
};
CircuitBreaker.prototype.failure = function () {
if (!this.options.enabled) return;
this.failures++;
this.halfOpenProbeCount = 0;
if (this.failures >= this.options.failureThreshold) {
this.state = "open";
this.openedAt = Date.now();
}
};
/* ============================================================================
* 8. Provider 统一上下文创建 (自动包装及动态注入 Timeout 与 Services)
* ============================================================================ */
function responseText(response) {
return response && response.responseText !== undefined ? response.responseText : response;
}
function jsonResponse(response) {
return JSON.parse(responseText(response));
}
function createProviderContext(translator, request, requestOpts) {
var effectiveTimeout = (requestOpts && requestOpts.timeout !== undefined)
? requestOpts.timeout
: (translator && translator.options && translator.options.timeout !== undefined)
? translator.options.timeout
: 10000;
var rawReqService = resolveService("request", requestOpts, translator ? translator.options : null);
// 包装 Request Service,保证请求级/实例级 timeout 自动继承
var wrappedReqService = {
request: function (opts) {
var reqOpts = Object.assign({ timeout: effectiveTimeout }, opts || {});
return rawReqService.request(reqOpts);
}
};
var resolvedCache = (requestOpts && requestOpts.services && requestOpts.services.cache)
? requestOpts.services.cache
: (Runtime.services.has("cache") && ServiceRegistry.get("cache"))
? ServiceRegistry.get("cache")
: (translator ? translator.cache : null);
return {
request: request,
services: {
request: wrappedReqService,
storage: resolveService("storage", requestOpts, translator ? translator.options : null),
cache: resolvedCache,
crypto: resolveService("crypto", requestOpts, translator ? translator.options : null) || CryptoService
},
utils: Utils
};
}
/* ============================================================================
* 9. 保留实测稳定可用的 7 大内置 Provider 实现
* ============================================================================ */
// 9.1 Google Provider
function GoogleProvider() {
this.id = "google";
this.name = "Google Translate";
this.version = VERSION;
this.priority = 100;
this.capabilities = { translate: true, detect: false, batch: false, streaming: false };
}
GoogleProvider.prototype.translate = function (request, context) {
var url =
"https://translate.google.com/translate_a/t?client=gtx" +
"&sl=" + encodeURIComponent(request.source || "auto") +
"&tl=" + encodeURIComponent(request.target || "zh-CN") +
"&q=" + encodeURIComponent(request.text);
return context.services.request.request({ method: "GET", url: url }).then(function (res) {
var data = JSON.parse(responseText(res));
if (!data || !data[0] || !data[0][0]) {
throw new ProviderError("Invalid Google response", "INVALID_RESPONSE");
}
return { text: data[0][0] };
});
};
// 9.2 Bing Provider
function BingProvider() {
this.id = "bing";
this.name = "Bing Translator";
this.version = VERSION;
this.priority = 90;
this.capabilities = { translate: true, detect: false, batch: false, streaming: false };
}
BingProvider.prototype.translate = function (request, context) {
var target = request.target === "zh-CN" ? "zh-Hans" : request.target === "zh-TW" ? "zh-Hant" : request.target || "zh-Hans";
var source = request.source && request.source !== "auto" ? request.source : "";
var url = "https://edge.microsoft.com/translate/translatetext?from=" + encodeURIComponent(source) + "&to=" + encodeURIComponent(target);
return context.services.request.request({
method: "POST",
url: url,
headers: { "Content-Type": "application/json" },
data: JSON.stringify([request.text])
}).then(function (res) {
var data = jsonResponse(res);
var text = data && data[0] && data[0].translations && data[0].translations[0] && data[0].translations[0].text;
if (!text) throw new ProviderError("Invalid Bing response", "INVALID_RESPONSE");
return { text: text };
});
};
// 9.3 Tencent AI Provider
function TencentAIProvider() {
this.id = "tencent-ai";
this.name = "Tencent AI Translate";
this.version = VERSION;
this.priority = 82;
this.capabilities = { translate: true, detect: false, batch: false, streaming: false };
}
TencentAIProvider.prototype.translate = function (request, context) {
var timestamp = Date.now();
var body = {
header: {
fn: "auto_translation",
client_key: "browser-chrome-121.0.0-Windows_10-" + Utils.guid() + "-" + timestamp,
session: "", user: ""
},
type: "plain",
model_category: "normal",
text_domain: "",
source: { lang: request.source || "auto", text_list: [request.text] },
target: { lang: request.target === "zh-CN" ? "zh" : request.target || "zh" }
};
return context.services.request.request({
method: "POST",
url: "https://transmart.qq.com/api/imt",
headers: {
"Content-Type": "application/json",
Origin: "https://transmart.qq.com",
Referer: "https://transmart.qq.com/"
},
data: JSON.stringify(body)
}).then(function (res) {
var data = jsonResponse(res);
var text = data && data.auto_translation && data.auto_translation[0];
if (!text) throw new ProviderError("Invalid Tencent AI response", "INVALID_RESPONSE");
return { text: text };
});
};
// 9.4 DeepL Provider
function DeepLProvider() {
this.id = "deepl";
this.name = "DeepL";
this.version = VERSION;
this.priority = 70;
this.capabilities = { translate: true, detect: false, batch: false, streaming: false };
}
DeepLProvider.prototype._timestamp = function (text) {
var count = (text.match(/i/g) || []).length;
var ts = Date.now();
if (count !== 0) return ts - (ts % (count + 1)) + (count + 1);
return ts;
};
DeepLProvider.prototype.translate = function (request, context) {
var id = (Math.floor(Math.random() * 99999) + 100000) * 1000;
var postData = JSON.stringify({
jsonrpc: "2.0",
method: "LMT_handle_texts",
id: id,
params: {
splitting: "newlines",
lang: {
source_lang_user_selected: (request.source && request.source !== "auto") ? request.source.toUpperCase() : "auto",
target_lang: request.target === "zh-CN" ? "ZH" : String(request.target || "ZH").split("-")[0].toUpperCase()
},
texts: [{ text: request.text, requestAlternatives: 3 }],
timestamp: this._timestamp(request.text)
}
});
if ((id + 5) % 29 === 0 || (id + 3) % 13 === 0) {
postData = postData.replace('"method":"', '"method" : "');
} else {
postData = postData.replace('"method":"', '"method": "');
}
return context.services.request.request({
method: "POST",
url: "https://www2.deepl.com/jsonrpc",
headers: {
"Content-Type": "application/json",
Origin: "https://www.deepl.com",
Referer: "https://www.deepl.com/"
},
data: postData
}).then(function (res) {
var data = jsonResponse(res);
var text = data && data.result && data.result.texts && data.result.texts[0] && data.result.texts[0].text;
if (!text) throw new ProviderError("Invalid DeepL response", "INVALID_RESPONSE");
return { text: text };
});
};
// 9.5 Caiyun Provider
function CaiyunProvider() {
this.id = "caiyun";
this.name = "Caiyun Translate";
this.version = VERSION;
this.priority = 65;
this.capabilities = { translate: true, detect: false, batch: false, streaming: false };
this.browserId = null;
this.jwt = null;
}
CaiyunProvider.prototype._decode = function (encoded) {
var source = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
var target = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm";
var dict = {};
for (var i = 0; i < source.length; i++) dict[source[i]] = target[i];
var replaced = String(encoded).split("").map(function (ch) { return dict[ch] || ch; }).join("");
return base64Decode(replaced);
};
CaiyunProvider.prototype._ensureAuth = function (context) {
var self = this;
if (self.browserId && self.jwt) return Promise.resolve();
self.browserId = md5Hex(Math.random().toString());
return context.services.request.request({
method: "POST",
url: "https://api.interpreter.caiyunai.com/v1/user/jwt/generate",
headers: {
"Content-Type": "application/json",
"X-Authorization": "token:qgemv4jr1y38jyq6vhvi",
Origin: "https://fanyi.caiyunapp.com"
},
data: JSON.stringify({ browser_id: self.browserId })
}).then(function (res) {
var data = jsonResponse(res);
if (!data.jwt) throw new ProviderError("Caiyun JWT unavailable", "AUTH_INIT_ERROR");
self.jwt = data.jwt;
});
};
CaiyunProvider.prototype.translate = function (request, context) {
var self = this;
var src = (request.source && request.source !== "auto") ? request.source : "auto";
var tgt = request.target === "zh-TW" ? "zh" : (request.target || "zh");
var transType = src + "2" + (tgt === "zh-CN" ? "zh" : tgt);
return self._ensureAuth(context).then(function () {
return context.services.request.request({
method: "POST",
url: "https://api.interpreter.caiyunai.com/v1/translator",
headers: {
"Content-Type": "application/json",
"X-Authorization": "token:qgemv4jr1y38jyq6vhvi",
"T-Authorization": self.jwt
},
data: JSON.stringify({
source: [request.text],
trans_type: transType,
detect: true,
browser_id: self.browserId
})
});
}).then(function (res) {
var data = jsonResponse(res);
if (!data || !data.target) throw new ProviderError("Invalid Caiyun response", "INVALID_RESPONSE");
var text = data.target.map(function (item) { return self._decode(item); }).join("\n");
if (!text) throw new ProviderError("Empty Caiyun result", "EMPTY_RESULT");
return { text: text };
});
};
// 9.6 Youdao Mobile Provider
function YoudaoMobileProvider() {
this.id = "youdao-mobile";
this.name = "Youdao Mobile";
this.version = VERSION;
this.priority = 40;
this.capabilities = { translate: true, detect: false, batch: false, streaming: false };
}
YoudaoMobileProvider.prototype.translate = function (request, context) {
var body = Utils.encodeForm({ inputtext: request.text, type: "AUTO" });
return context.services.request.request({
method: "POST",
url: "http://m.youdao.com/translate",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
data: body
}).then(function (res) {
var html = responseText(res);
var match = /id="translateResult">\s*?<li>([\s\S]*?)<\/li>\s*?<\/ul/.exec(html);
if (!match) throw new ProviderError("Invalid Youdao mobile response", "INVALID_RESPONSE");
return { text: match[1].replace(/<[^>]+>/g, "").trim() };
});
};
// 9.7 Google Mobile Provider
function GoogleMobileProvider() {
this.id = "google-mobile";
this.name = "Google Mobile Translate";
this.version = VERSION;
this.priority = 35;
this.capabilities = { translate: true, detect: false, batch: false, streaming: false };
}
GoogleMobileProvider.prototype.translate = function (request, context) {
var url = "https://translate.google.com/m?tl=" + encodeURIComponent(request.target || "zh-CN") + "&q=" + encodeURIComponent(request.text);
return context.services.request.request({ method: "GET", url: url }).then(function (res) {
var html = responseText(res);
var match = /class="result-container">((?:.|\n)*?)<\/div/.exec(html);
if (!match) throw new ProviderError("Invalid Google mobile response", "INVALID_RESPONSE");
return { text: match[1].replace(/<[^>]+>/g, "").trim() };
});
};
/* ============================================================================
* 10. 语种检测 (Language Detection Helper)
* ============================================================================ */
function detectLanguage(text, context) {
var query = String(text || "").trim().slice(0, 100);
if (!query) return Promise.resolve("auto");
function localDetect(str) {
if (/[\u3040-\u30ff\u31f0-\u31ff]/.test(str)) return "ja";
if (/[\uac00-\ud7af\u1100-\u11ff]/.test(str)) return "ko";
if (/[\u4e00-\u9fa5]/.test(str)) return "zh";
if (/[\u0400-\u04ff]/.test(str)) return "ru";
return "en";
}
function tryBing() {
var bingUrl = "https://edge.microsoft.com/translate/translatetext?from=&to=zh-Hans";
return context.services.request.request({
method: "POST",
url: bingUrl,
headers: { "Content-Type": "application/json" },
data: JSON.stringify([query]),
timeout: 4000
}).then(function (res) {
var data = jsonResponse(res);
var lang = data && data[0] && data[0].detectedLanguage && data[0].detectedLanguage.language;
if (!lang) throw new Error("Bing detect empty");
return lang === "zh-Hans" ? "zh" : lang;
});
}
var googleUrl = "https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=zh-CN&dt=t&q=" + encodeURIComponent(query);
return context.services.request.request({
method: "GET",
url: googleUrl,
timeout: 4000
}).then(function (res) {
var data = jsonResponse(res);
var lang = data && data[2];
if (!lang) throw new Error("Google detect empty");
return lang;
}).catch(function () {
return tryBing();
}).catch(function () {
return localDetect(query);
});
}
/* ============================================================================
* 11. 核心 Translator 类定义
* 符合 Spec 34 规范要求的全部默认配置
* ============================================================================ */
function Translator(options) {
var DEFAULTS = {
source: "auto",
target: "zh-CN",
providers: "auto",
batchSize: 30,
timeout: 10000,
retry: 0,
debug: false,
cache: {
enabled: true,
ttl: 7 * 24 * 60 * 60 * 1000, // 7 天
refreshOnHit: true,
refreshInterval: 6 * 60 * 60 * 1000, // 6 小时
maxEntries: 10000,
persistent: true
},
circuitBreaker: {
enabled: false,
failureThreshold: 10,
cooldown: 6 * 60 * 60 * 1000 // 6 小时
}
};
this.options = mergeConfig(DEFAULTS, options || {});
var storageService = resolveService("storage", null, this.options);
this.cache = new CacheService(storageService, this.options.cache);
}
// Debug 日志打印实现
Translator.prototype._log = function (type, message, details) {
if (!this.options.debug) return;
if (global.console && console.log) {
var prefix = "[TransKit:" + type + "]";
if (details !== undefined) {
console.log(prefix, message, details);
} else {
console.log(prefix, message);
}
}
};
Translator.prototype._resolveProviders = function (requestOpts) {
var configured = (requestOpts && requestOpts.providers) || this.options.providers;
var list = Array.from(Runtime.providers.values());
if (configured === "auto" || configured === undefined) {
var defaultStrat = StrategyRegistry.get("default");
if (defaultStrat && Utils.isFunction(defaultStrat.resolve)) {
return defaultStrat.resolve(list);
}
return DefaultStrategy.resolve(list);
}
if (typeof configured === "string") {
var strat = StrategyRegistry.get(configured);
if (strat && Utils.isFunction(strat.resolve)) {
return strat.resolve(list);
}
var provider = Runtime.providers.get(configured);
if (!provider) {
throw new ConfigurationError("Provider or Strategy not found: " + configured, "PROVIDER_NOT_FOUND");
}
return [provider];
}
if (Array.isArray(configured)) {
var result = [];
configured.forEach(function (id) {
var p = typeof id === "string" ? Runtime.providers.get(id) : id;
if (!p) {
throw new ConfigurationError("Provider not found: " + id, "PROVIDER_NOT_FOUND");
}
result.push(p);
});
return result;
}
throw new ConfigurationError("Invalid providers configuration", "INVALID_PROVIDERS");
};
Translator.prototype._getBreaker = function (provider) {
var id = provider.id;
var breaker = Runtime.breakers.get(id);
if (!breaker) {
breaker = new CircuitBreaker(id, this.options.circuitBreaker);
Runtime.breakers.set(id, breaker);
}
breaker.options = Object.assign({}, breaker.options, this.options.circuitBreaker);
return breaker;
};
Translator.prototype._callProvider = function (provider, request, context) {
var breaker = this._getBreaker(provider);
if (!breaker.allow()) {
this._log("CIRCUIT", "Circuit open, skipping provider: " + provider.id);
throw new CircuitOpenError("Circuit is open: " + provider.id, "CIRCUIT_OPEN", { provider: provider.id });
}
var started = Date.now();
var self = this;
self._log("PROVIDER", "Calling provider: " + provider.id, { text: request.text });
var initPromise;
if (Utils.isFunction(provider.initialize)) {
if (!provider._initPromise) {
provider._initPromise = Promise.resolve(provider.initialize(context)).catch(function (err) {
provider._initPromise = null;
throw err;
});
}
initPromise = provider._initPromise;
} else {
initPromise = Promise.resolve();
}
return initPromise
.then(function () {
return provider.translate(request, context);
})
.then(function (result) {
var duration = Date.now() - started;
if (!result || typeof result.text !== "string") {
throw new ProviderError("Provider returned invalid result", "INVALID_PROVIDER_RESULT", { provider: provider.id });
}
breaker.success();
self._log("PROVIDER", "Provider success: " + provider.id, { duration: duration });
return {
text: result.text,
provider: provider.id,
success: true,
duration: duration
};
})
.catch(function (error) {
breaker.failure();
self._log("PROVIDER", "Provider failed: " + provider.id, { error: error ? error.message : error });
throw error;
});
};
Translator.prototype._executeTranslation = function (request, requestOpts) {
var providers = this._resolveProviders(requestOpts);
var attempts = [];
var retryConfig = (requestOpts && requestOpts.retry !== undefined)
? requestOpts.retry
: this.options.retry;
var retryTimes = typeof retryConfig === "number"
? retryConfig
: Math.max(0, (retryConfig && retryConfig.times) || 0);
var retryDelay = (retryConfig && typeof retryConfig === "object")
? (retryConfig.delay || 0)
: 0;
var context = createProviderContext(this, request, requestOpts);
var self = this;
function attemptProvider(provider) {
var currentTry = 0;
function run() {
currentTry++;
return self._callProvider(provider, request, context)
.then(function (result) {
attempts.push({ provider: provider.id, success: true, duration: result.duration });
return result;
})
.catch(function (error) {
attempts.push({
provider: provider.id,
success: false,
duration: 0,
error: error ? error.code || error.message : "UNKNOWN_ERROR"
});
if (currentTry <= retryTimes) {
self._log("RETRY", "Retrying provider: " + provider.id, { try: currentTry });
return Utils.sleep(retryDelay).then(run);
}
throw error;
});
}
return run();
}
function next(index, lastError) {
if (index >= providers.length) {
throw new TranslationError("All providers failed", "ALL_PROVIDERS_FAILED", {
attempts: attempts,
lastError: lastError
});
}
var provider = providers[index];
self._log("FAILOVER", "Trying provider " + (index + 1) + "/" + providers.length + ": " + provider.id);
return attemptProvider(provider).catch(function (error) {
return next(index + 1, error);
});
}
return Promise.resolve()
.then(function () { return next(0, null); })
.then(function (result) {
result.attempts = attempts;
return result;
})
.catch(function (error) {
if (error instanceof TranslationError) {
error.details = Object.assign({}, error.details || {}, { attempts: attempts });
}
throw error;
});
};
/* ============================================================================
* 12. 单项目翻译流程与 Inflight 去重
* ============================================================================ */
Translator.prototype._translateOne = function (text, index, requestOpts) {
if (text === "") {
return Promise.resolve({
originalText: "",
text: "",
success: true,
provider: null,
fromCache: false,
duration: 0,
attempts: []
});
}
var source = (requestOpts && requestOpts.source) || this.options.source || "auto";
var target = (requestOpts && requestOpts.target) || this.options.target || "zh-CN";
var configuredProviders = (requestOpts && requestOpts.providers) || this.options.providers;
var providersKey = Utils.isArray(configuredProviders)
? configuredProviders.join(",")
: String(configuredProviders || "auto");
var request = {
text: text,
source: source,
target: target,
requestId: Utils.randomId("req"),
signal: requestOpts ? requestOpts.signal : null
};
var inflightKey =
String(text) + "\u0001" +
String(source) + "\u0001" +
String(target) + "\u0001" +
providersKey;
var existing = Runtime.inflight.get(inflightKey);
if (existing) {
this._log("INFLIGHT", "Merging into existing inflight request", { text: text, providers: providersKey });
return existing.then(function (res) { return Object.assign({}, res); });
}
var started = Date.now();
var self = this;
var promise = this.cache.get(text, source, target, self, requestOpts)
.then(function (cached) {
if (cached) {
return {
originalText: text,
text: cached.value,
success: true,
provider: null,
fromCache: true,
duration: Date.now() - started,
attempts: []
};
}
return self._executeTranslation(request, requestOpts).then(function (result) {
return self.cache.set(text, source, target, result.text, self, requestOpts)
.catch(function (e) {
self._log("CACHE", "Cache write failed non-fatally", { error: e.message });
})
.then(function () {
return {
originalText: text,
text: result.text,
success: true,
provider: result.provider,
fromCache: false,
duration: Date.now() - started,
attempts: result.attempts
};
});
});
})
.catch(function (error) {
return {
originalText: text,
text: null,
success: false,
provider: null,
fromCache: false,
duration: Date.now() - started,
attempts: error && error.details && error.details.attempts ? error.details.attempts : [],
error: error
};
});
Runtime.inflight.set(inflightKey, promise);
promise.finally(function () {
if (Runtime.inflight.get(inflightKey) === promise) {
Runtime.inflight.delete(inflightKey);
}
});
return promise;
};
/* ============================================================================
* 13. Batch 批量处理与顺序组队去重
* ============================================================================ */
Translator.prototype._batch = function (texts, requestOpts) {
var self = this;
var unique = [];
var indexMap = new Map();
texts.forEach(function (text) {
if (!indexMap.has(text)) {
indexMap.set(text, unique.length);
unique.push(text);
}
});
var rawBatchSize = (requestOpts && requestOpts.batchSize !== undefined)
? requestOpts.batchSize
: this.options.batchSize;
var batchSize = Math.max(1, rawBatchSize || 30);
var results = new Array(unique.length);
function processGroup(start) {
if (start >= unique.length) return Promise.resolve();
var group = unique.slice(start, start + batchSize);
self._log("BATCH", "Processing batch group " + (start / batchSize + 1), { count: group.length });
return Promise.all(
group.map(function (text) {
return self._translateOne(text, start, requestOpts);
})
).then(function (groupResults) {
groupResults.forEach(function (res, offset) {
results[start + offset] = res;
});
return processGroup(start + batchSize);
});
}
return processGroup(0).then(function () {
return texts.map(function (text) {
var res = results[indexMap.get(text)];
return Object.assign({}, res);
});
});
};
/* ============================================================================
* 14. 公开 API 实现 (translate / translateWithDetails / detect)
* ============================================================================ */
Translator.prototype.translateWithDetails = function (input, options) {
if (typeof input === "string") {
return this._translateOne(input, 0, options).then(function (res) {
return [res];
});
}
if (Array.isArray(input)) {
return this._batch(input, options);
}
return Promise.reject(
new ConfigurationError("translate input must be string or string[]", "INVALID_TRANSLATE_INPUT")
);
};
Translator.prototype.translate = function (input, options) {
if (typeof input === "string") {
if (input === "") return Promise.resolve("");
return this._translateOne(input, 0, options).then(function (res) {
if (!res.success) {
throw res.error || new TranslationError("Translation failed", "TRANSLATION_FAILED");
}
return res.text;
});
}
if (Array.isArray(input)) {
return this._batch(input, options).then(function (results) {
var failed = results.find(function (item) { return !item.success; });
if (failed) {
throw failed.error || new TranslationError("One or more translations failed", "TRANSLATION_FAILED", { results: results });
}
return results.map(function (item) { return item.text; });
});
}
return Promise.reject(
new ConfigurationError("translate input must be string or string[]", "INVALID_TRANSLATE_INPUT")
);
};
Translator.prototype.detect = function (text) {
if (text === "") return Promise.resolve("");
var context = createProviderContext(this, { text: text }, null);
return detectLanguage(text, context);
};
Translator.prototype.getInflightCount = function () {
return Runtime.inflight.size;
};
Translator.prototype.getProviderStatus = function () {
var result = {};
Runtime.providers.forEach(function (provider, id) {
var breaker = Runtime.breakers.get(id);
result[id] = {
state: breaker ? breaker.state : "closed",
failures: breaker ? breaker.failures : 0
};
});
return result;
};
/* ============================================================================
* 15. TransKit 全局入口与单例 / 诊断 API
* ============================================================================ */
var defaultTranslator = null;
function getDefaultTranslator() {
if (!defaultTranslator) {
defaultTranslator = new Translator(Runtime._config);
}
return defaultTranslator;
}
var TransKit = {
version: VERSION,
Utils: Utils,
errors: {
TransKitError: TransKitError,
ProviderError: ProviderError,
TranslationError: TranslationError,
NetworkError: NetworkError,
TimeoutError: TimeoutError,
CacheError: CacheError,
ConfigurationError: ConfigurationError,
CircuitOpenError: CircuitOpenError
},
crypto: CryptoService,
providers: ProviderRegistry,
services: ServiceRegistry,
strategies: StrategyRegistry,
configure: function (options) {
Runtime._config = mergeConfig(Runtime._config, options || {});
defaultTranslator = null;
return TransKit;
},
create: function (options) {
var merged = mergeConfig(Runtime._config, options || {});
return new Translator(merged);
},
translate: function (input, options) {
if (options) {
return TransKit.create(options).translate(input, options);
}
return getDefaultTranslator().translate(input);
},
translateWithDetails: function (input, options) {
if (options) {
return TransKit.create(options).translateWithDetails(input, options);
}
return getDefaultTranslator().translateWithDetails(input);
},
detect: function (text) {
return TransKit.create().detect(text);
},
clearCache: function () {
return getDefaultTranslator().cache.clear(getDefaultTranslator());
},
runtime: {
getInflightCount: function () {
return Runtime.inflight.size;
},
getInflightKeys: function () {
return Array.from(Runtime.inflight.keys());
},
getCircuitState: function (id) {
if (id) {
var breaker = Runtime.breakers.get(id);
return {
state: breaker ? breaker.state : "closed",
failures: breaker ? breaker.failures : 0,
openedAt: breaker ? breaker.openedAt : 0
};
}
var result = {};
ProviderRegistry.list().forEach(function (item) {
var breaker = Runtime.breakers.get(item.id);
result[item.id] = {
state: breaker ? breaker.state : "closed",
failures: breaker ? breaker.failures : 0,
openedAt: breaker ? breaker.openedAt : 0
};
});
return result;
},
resetCircuit: function (id) {
if (id) {
var breaker = Runtime.breakers.get(id);
if (breaker) breaker.success();
} else {
Runtime.breakers.clear();
}
},
clearCache: function () {
return getDefaultTranslator().cache.clear(getDefaultTranslator());
}
}
};
/* ============================================================================
* 16. 注册内置 Service, Strategy 及 7 大稳定 Provider
* ============================================================================ */
ServiceRegistry.register("request", RequestService, { override: true });
ServiceRegistry.register("storage", StorageService, { override: true });
ServiceRegistry.register("crypto", CryptoService, { override: true });
StrategyRegistry.register("default", DefaultStrategy, { override: true });
ProviderRegistry.register(GoogleProvider, { override: true });
ProviderRegistry.register(BingProvider, { override: true });
ProviderRegistry.register(TencentAIProvider, { override: true });
ProviderRegistry.register(DeepLProvider, { override: true });
ProviderRegistry.register(CaiyunProvider, { override: true });
ProviderRegistry.register(YoudaoMobileProvider, { override: true });
ProviderRegistry.register(GoogleMobileProvider, { override: true });
TransKit.builtinProviders = {
GoogleProvider: GoogleProvider,
BingProvider: BingProvider,
TencentAIProvider: TencentAIProvider,
DeepLProvider: DeepLProvider,
CaiyunProvider: CaiyunProvider,
YoudaoMobileProvider: YoudaoMobileProvider,
GoogleMobileProvider: GoogleMobileProvider
};
/* ============================================================================
* 17. 宿主环境导出
* ============================================================================ */
global.TransKit = TransKit;
})(
typeof globalThis !== "undefined"
? globalThis
: typeof unsafeWindow !== "undefined"
? unsafeWindow
: window
);