@lynn123411/dsh-chat-translate 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -14
- package/docs//351/205/215/347/275/256.md +14 -14
- package/lib/client.js +371 -1013
- package/lib/client.js.map +4 -4
- package/lib/index.js +125 -428
- package/lib/index.js.map +4 -4
- package/lib/types/client/index.d.ts +3 -3
- package/lib/types/client/locales.d.ts +8 -20
- package/lib/types/client/settings/store.d.ts +5 -8
- package/lib/types/client/translate/api.d.ts +5 -16
- package/lib/types/client/translate/client-cache.d.ts +1 -3
- package/lib/types/client/translate/lazy.d.ts +9 -7
- package/lib/types/client/translate/mount.d.ts +2 -6
- package/lib/types/client/translate/observer.d.ts +24 -24
- package/lib/types/client/translate/reply.d.ts +38 -0
- package/lib/types/index.d.ts +0 -12
- package/lib/types/server/cache.d.ts +13 -1
- package/lib/types/server/config.d.ts +28 -18
- package/lib/types/server/dispatcher.d.ts +25 -38
- package/lib/types/server/pipeline/{think.d.ts → blocks.d.ts} +9 -9
- package/lib/types/server/router.d.ts +4 -6
- package/lib/types/server/types.d.ts +2 -8
- package/package.json +3 -3
- package/lib/types/client/translate/think-button.d.ts +0 -19
- package/lib/types/client/translate/think.d.ts +0 -55
- package/lib/types/client/translate/viewport-observer.d.ts +0 -43
- package/lib/types/server/adapters/bing.d.ts +0 -15
package/lib/index.js
CHANGED
|
@@ -888,36 +888,29 @@ function dshHomePath(...segments) {
|
|
|
888
888
|
|
|
889
889
|
// src/server/config.ts
|
|
890
890
|
import * as fs from "node:fs/promises";
|
|
891
|
-
var MAX_CONCURRENCY = 100;
|
|
892
891
|
var AI_TIMEOUT_MIN = 500;
|
|
893
|
-
var AI_TIMEOUT_MAX =
|
|
894
|
-
var THINK_TIMEOUT_MIN = 500;
|
|
895
|
-
var THINK_TIMEOUT_MAX = 9e5;
|
|
896
|
-
var THINK_CACHE_ENTRIES = 300;
|
|
892
|
+
var AI_TIMEOUT_MAX = 9e5;
|
|
897
893
|
var SETTINGS_NAMESPACE = "dsh-chat-translate";
|
|
898
894
|
var DEFAULT_CONFIG = {
|
|
899
895
|
enabled: true,
|
|
900
|
-
|
|
901
|
-
timeoutMs: 2e3,
|
|
902
|
-
aiTimeoutMs: 3e4,
|
|
903
|
-
thinkTimeoutMs: 6e5,
|
|
904
|
-
aiEnabled: true,
|
|
905
|
-
bingEnabled: true,
|
|
906
|
-
thinkEnabled: true,
|
|
896
|
+
aiTimeoutMs: 6e5,
|
|
907
897
|
baseUrl: "",
|
|
908
898
|
model: "",
|
|
909
899
|
targetLang: "zh-Hans"
|
|
910
900
|
};
|
|
901
|
+
var RETIRED_CONFIG_KEYS = [
|
|
902
|
+
"channels",
|
|
903
|
+
"concurrency",
|
|
904
|
+
"timeoutMs",
|
|
905
|
+
"thinkTimeoutMs",
|
|
906
|
+
"aiEnabled",
|
|
907
|
+
"bingEnabled",
|
|
908
|
+
"thinkEnabled"
|
|
909
|
+
];
|
|
911
910
|
function readPluginConfig(refs) {
|
|
912
911
|
return {
|
|
913
912
|
enabled: refs.enabled.get(),
|
|
914
|
-
concurrency: refs.concurrency.get(),
|
|
915
|
-
timeoutMs: refs.timeoutMs.get(),
|
|
916
913
|
aiTimeoutMs: refs.aiTimeoutMs.get(),
|
|
917
|
-
thinkTimeoutMs: refs.thinkTimeoutMs.get(),
|
|
918
|
-
aiEnabled: refs.aiEnabled.get(),
|
|
919
|
-
bingEnabled: refs.bingEnabled.get(),
|
|
920
|
-
thinkEnabled: refs.thinkEnabled.get(),
|
|
921
914
|
baseUrl: refs.baseUrl.get(),
|
|
922
915
|
model: refs.model.get(),
|
|
923
916
|
targetLang: refs.targetLang.get()
|
|
@@ -953,27 +946,12 @@ var ConfigManager = class {
|
|
|
953
946
|
function sanitizePatch(input) {
|
|
954
947
|
const next = {};
|
|
955
948
|
if (typeof input.enabled === "boolean") next.enabled = input.enabled;
|
|
956
|
-
if (typeof input.aiEnabled === "boolean") next.aiEnabled = input.aiEnabled;
|
|
957
|
-
if (typeof input.bingEnabled === "boolean") next.bingEnabled = input.bingEnabled;
|
|
958
|
-
if (typeof input.thinkEnabled === "boolean") next.thinkEnabled = input.thinkEnabled;
|
|
959
|
-
if (typeof input.concurrency === "number" && Number.isFinite(input.concurrency)) {
|
|
960
|
-
next.concurrency = Math.min(Math.max(Math.round(input.concurrency), 1), MAX_CONCURRENCY);
|
|
961
|
-
}
|
|
962
|
-
if (typeof input.timeoutMs === "number" && Number.isFinite(input.timeoutMs)) {
|
|
963
|
-
next.timeoutMs = Math.min(Math.max(Math.round(input.timeoutMs), 500), 1e4);
|
|
964
|
-
}
|
|
965
949
|
if (typeof input.aiTimeoutMs === "number" && Number.isFinite(input.aiTimeoutMs)) {
|
|
966
950
|
next.aiTimeoutMs = Math.min(
|
|
967
951
|
Math.max(Math.round(input.aiTimeoutMs), AI_TIMEOUT_MIN),
|
|
968
952
|
AI_TIMEOUT_MAX
|
|
969
953
|
);
|
|
970
954
|
}
|
|
971
|
-
if (typeof input.thinkTimeoutMs === "number" && Number.isFinite(input.thinkTimeoutMs)) {
|
|
972
|
-
next.thinkTimeoutMs = Math.min(
|
|
973
|
-
Math.max(Math.round(input.thinkTimeoutMs), THINK_TIMEOUT_MIN),
|
|
974
|
-
THINK_TIMEOUT_MAX
|
|
975
|
-
);
|
|
976
|
-
}
|
|
977
955
|
if (typeof input.baseUrl === "string") next.baseUrl = input.baseUrl.trim();
|
|
978
956
|
if (typeof input.model === "string") next.model = input.model.trim();
|
|
979
957
|
if (typeof input.targetLang === "string" && input.targetLang.trim()) {
|
|
@@ -981,6 +959,26 @@ function sanitizePatch(input) {
|
|
|
981
959
|
}
|
|
982
960
|
return next;
|
|
983
961
|
}
|
|
962
|
+
async function retireRemovedConfigKeys(settings) {
|
|
963
|
+
const descriptor = settings.describe().find((d) => d.ns === SETTINGS_NAMESPACE);
|
|
964
|
+
const user = descriptor?.user;
|
|
965
|
+
if (typeof user !== "object" || user === null || Array.isArray(user)) return false;
|
|
966
|
+
const record = user;
|
|
967
|
+
const ops = RETIRED_CONFIG_KEYS.filter(
|
|
968
|
+
(key) => record[key] !== void 0
|
|
969
|
+
).map((key) => ({ op: "unset", path: [key] }));
|
|
970
|
+
if (ops.length === 0) return false;
|
|
971
|
+
try {
|
|
972
|
+
await settings.mutate(SETTINGS_NAMESPACE, ops, descriptor?.revision);
|
|
973
|
+
} catch (err) {
|
|
974
|
+
console.warn(
|
|
975
|
+
"[dsh-chat-translate] Failed to retire removed config keys; will retry on next boot:",
|
|
976
|
+
err
|
|
977
|
+
);
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
984
982
|
async function migrateLegacyConfigFile(settings, legacyPath) {
|
|
985
983
|
let raw;
|
|
986
984
|
try {
|
|
@@ -1298,7 +1296,7 @@ var LruDiskCache = class {
|
|
|
1298
1296
|
dirty = false;
|
|
1299
1297
|
/** Legacy root-level cache file (<=1.1); moved under the plugin subdir. */
|
|
1300
1298
|
legacyPath;
|
|
1301
|
-
/** Only the original
|
|
1299
|
+
/** Only the original cache file carries a pre-1.2 root-level predecessor. */
|
|
1302
1300
|
hasLegacyPredecessor;
|
|
1303
1301
|
constructor(maxEntries = 1e3, fileName = "cache.json") {
|
|
1304
1302
|
this.maxEntries = maxEntries;
|
|
@@ -1430,6 +1428,17 @@ var LruDiskCache = class {
|
|
|
1430
1428
|
}
|
|
1431
1429
|
}
|
|
1432
1430
|
};
|
|
1431
|
+
async function retireStoreFiles(fileNames) {
|
|
1432
|
+
let removed = false;
|
|
1433
|
+
for (const fileName of fileNames) {
|
|
1434
|
+
try {
|
|
1435
|
+
await fs2.unlink(dshHomePath("dsh-chat-translate", fileName));
|
|
1436
|
+
removed = true;
|
|
1437
|
+
} catch {
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return removed;
|
|
1441
|
+
}
|
|
1433
1442
|
|
|
1434
1443
|
// src/describe-error.ts
|
|
1435
1444
|
function describeError(err) {
|
|
@@ -1457,129 +1466,6 @@ function describeError(err) {
|
|
|
1457
1466
|
return parts.join(" \u2190 ");
|
|
1458
1467
|
}
|
|
1459
1468
|
|
|
1460
|
-
// src/server/adapters/bing.ts
|
|
1461
|
-
var TRANSLATOR_URL = "https://cn.bing.com/translator";
|
|
1462
|
-
var TRANSLATE_URL = "https://cn.bing.com/ttranslatev3?isVertical=1&&IG={IG}&IID=translator.5025.1";
|
|
1463
|
-
var UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
1464
|
-
var IG_RES = [
|
|
1465
|
-
/_IG="([a-zA-Z0-9]+)"/,
|
|
1466
|
-
/,IG:"([a-zA-Z0-9]+)"/,
|
|
1467
|
-
/IG:"([a-zA-Z0-9]+)"/,
|
|
1468
|
-
/"IG":"([a-zA-Z0-9]+)"/
|
|
1469
|
-
];
|
|
1470
|
-
var ABUSE_RES = [
|
|
1471
|
-
/params_AbusePreventionHelper\s*=\s*\[\s*(\d+)\s*,\s*"([^"]+)"/,
|
|
1472
|
-
/var\s+params_AbusePreventionHelper\s*=\s*\[\s*(\d+)\s*,\s*"([^"]+)"/
|
|
1473
|
-
];
|
|
1474
|
-
var cachedTokens = null;
|
|
1475
|
-
var tokensFetchedAt = 0;
|
|
1476
|
-
var TOKEN_TTL_MS = 15 * 60 * 1e3;
|
|
1477
|
-
var inFlightTokenPromise = null;
|
|
1478
|
-
function parseTokens(html) {
|
|
1479
|
-
let ig;
|
|
1480
|
-
for (const re of IG_RES) {
|
|
1481
|
-
const m = re.exec(html);
|
|
1482
|
-
if (m && m[1]) {
|
|
1483
|
-
ig = m[1];
|
|
1484
|
-
break;
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
let key;
|
|
1488
|
-
let token;
|
|
1489
|
-
for (const re of ABUSE_RES) {
|
|
1490
|
-
const m = re.exec(html);
|
|
1491
|
-
if (m && m[1] && m[2]) {
|
|
1492
|
-
key = m[1];
|
|
1493
|
-
token = m[2];
|
|
1494
|
-
break;
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
if (!ig || !key || !token) {
|
|
1498
|
-
throw new Error(`Bing translator page: missing tokens (ig: ${!!ig}, key: ${!!key}, token: ${!!token})`);
|
|
1499
|
-
}
|
|
1500
|
-
return { ig, key, token };
|
|
1501
|
-
}
|
|
1502
|
-
async function fetchTokens(signal, forceRefresh = false) {
|
|
1503
|
-
if (!forceRefresh && cachedTokens && Date.now() - tokensFetchedAt < TOKEN_TTL_MS) {
|
|
1504
|
-
return cachedTokens;
|
|
1505
|
-
}
|
|
1506
|
-
if (inFlightTokenPromise) {
|
|
1507
|
-
return inFlightTokenPromise;
|
|
1508
|
-
}
|
|
1509
|
-
inFlightTokenPromise = (async () => {
|
|
1510
|
-
try {
|
|
1511
|
-
const response = await fetch(TRANSLATOR_URL, {
|
|
1512
|
-
headers: {
|
|
1513
|
-
"User-Agent": UA,
|
|
1514
|
-
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
1515
|
-
},
|
|
1516
|
-
signal
|
|
1517
|
-
});
|
|
1518
|
-
if (!response.ok) {
|
|
1519
|
-
throw new Error(`Bing translator page responded with status ${response.status}`);
|
|
1520
|
-
}
|
|
1521
|
-
const html = await response.text();
|
|
1522
|
-
const tokens = parseTokens(html);
|
|
1523
|
-
cachedTokens = tokens;
|
|
1524
|
-
tokensFetchedAt = Date.now();
|
|
1525
|
-
return tokens;
|
|
1526
|
-
} finally {
|
|
1527
|
-
inFlightTokenPromise = null;
|
|
1528
|
-
}
|
|
1529
|
-
})();
|
|
1530
|
-
return inFlightTokenPromise;
|
|
1531
|
-
}
|
|
1532
|
-
var BingWebAdapter = class {
|
|
1533
|
-
id = "bing";
|
|
1534
|
-
name = "\u5FAE\u8F6F Bing \u7F51\u9875\u7FFB\u8BD1 (\u514DKey\u76F4\u8FDE)";
|
|
1535
|
-
isAvailable(_config) {
|
|
1536
|
-
return true;
|
|
1537
|
-
}
|
|
1538
|
-
async translate(text, signal, config) {
|
|
1539
|
-
const targetLang = config.targetLang || "zh-Hans";
|
|
1540
|
-
return this.executeTranslate(text, signal, targetLang, false);
|
|
1541
|
-
}
|
|
1542
|
-
async executeTranslate(text, signal, targetLang, isRetry) {
|
|
1543
|
-
const tokens = await fetchTokens(signal, isRetry);
|
|
1544
|
-
const body = new URLSearchParams({
|
|
1545
|
-
fromLang: "auto-detect",
|
|
1546
|
-
text,
|
|
1547
|
-
to: targetLang,
|
|
1548
|
-
key: tokens.key,
|
|
1549
|
-
token: tokens.token,
|
|
1550
|
-
tryFetchingGenderDebiasedTranslations: "true"
|
|
1551
|
-
});
|
|
1552
|
-
const response = await fetch(TRANSLATE_URL.replace("{IG}", tokens.ig), {
|
|
1553
|
-
method: "POST",
|
|
1554
|
-
headers: {
|
|
1555
|
-
"User-Agent": UA,
|
|
1556
|
-
Referer: "https://cn.bing.com/translator/",
|
|
1557
|
-
Origin: "https://cn.bing.com",
|
|
1558
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
1559
|
-
},
|
|
1560
|
-
body,
|
|
1561
|
-
signal
|
|
1562
|
-
});
|
|
1563
|
-
if (!response.ok) {
|
|
1564
|
-
cachedTokens = null;
|
|
1565
|
-
if (!isRetry && (response.status === 400 || response.status === 401 || response.status === 403)) {
|
|
1566
|
-
return this.executeTranslate(text, signal, targetLang, true);
|
|
1567
|
-
}
|
|
1568
|
-
throw new Error(`Bing translate responded with status ${response.status}`);
|
|
1569
|
-
}
|
|
1570
|
-
const json = await response.json();
|
|
1571
|
-
const translated = json?.[0]?.translations?.[0]?.text?.trim();
|
|
1572
|
-
if (!translated) {
|
|
1573
|
-
cachedTokens = null;
|
|
1574
|
-
if (!isRetry) {
|
|
1575
|
-
return this.executeTranslate(text, signal, targetLang, true);
|
|
1576
|
-
}
|
|
1577
|
-
throw new Error("Bing translate returned an empty result");
|
|
1578
|
-
}
|
|
1579
|
-
return translated;
|
|
1580
|
-
}
|
|
1581
|
-
};
|
|
1582
|
-
|
|
1583
1469
|
// src/server/adapters/openai.ts
|
|
1584
1470
|
var LANG_HINTS = {
|
|
1585
1471
|
"zh-hans": "Simplified Chinese",
|
|
@@ -1598,7 +1484,7 @@ var LANG_HINTS = {
|
|
|
1598
1484
|
it: "Italian"
|
|
1599
1485
|
};
|
|
1600
1486
|
function buildSystemPrompt(langName, mode) {
|
|
1601
|
-
const base = `You are a professional translator. Translate the user's message into ${langName}. Output ONLY the translated text \u2014 no explanations, no quotation marks, no extra words. Some fragments of the message are opaque code between the bracket characters U+27E6 and U+27E7. Copy each such fragment into your output character-for-character, keeping its position in the sentence. Never invent such a fragment and never remove one.`;
|
|
1487
|
+
const base = `You are a professional translator. Translate the user's message into ${langName}. If a passage is already in ${langName}, keep it exactly as it is. Output ONLY the translated text \u2014 no explanations, no quotation marks, no extra words. Some fragments of the message are opaque code between the bracket characters U+27E6 and U+27E7. Copy each such fragment into your output character-for-character, keeping its position in the sentence. Never invent such a fragment and never remove one.`;
|
|
1602
1488
|
if (mode === "plain") return base;
|
|
1603
1489
|
return base + ` The message is a sequence of parts. Each part starts with its own line holding a marker: an index between the bracket characters U+27EA and U+27EB. For every part, output its marker line unchanged, then that part's translation on the following lines. Translate each part on its own, keep the parts in the original order, and never merge two parts into one answer.`;
|
|
1604
1490
|
}
|
|
@@ -1610,9 +1496,7 @@ var OpenAiCompatibleAdapter = class {
|
|
|
1610
1496
|
this.credentials = credentials;
|
|
1611
1497
|
}
|
|
1612
1498
|
isAvailable(config) {
|
|
1613
|
-
return Boolean(
|
|
1614
|
-
config.aiEnabled && config.baseUrl?.trim() && config.model?.trim() && this.credentials.getApiKey()
|
|
1615
|
-
);
|
|
1499
|
+
return Boolean(config.baseUrl?.trim() && config.model?.trim() && this.credentials.getApiKey());
|
|
1616
1500
|
}
|
|
1617
1501
|
async translate(text, signal, config, options) {
|
|
1618
1502
|
const apiKey = this.credentials.getApiKey();
|
|
@@ -1661,9 +1545,9 @@ var OpenAiCompatibleAdapter = class {
|
|
|
1661
1545
|
}
|
|
1662
1546
|
};
|
|
1663
1547
|
|
|
1664
|
-
// src/server/pipeline/
|
|
1665
|
-
var
|
|
1666
|
-
var
|
|
1548
|
+
// src/server/pipeline/blocks.ts
|
|
1549
|
+
var REPLY_MAX_INPUT_TOKENS = 4096;
|
|
1550
|
+
var REPLY_MAX_OUTPUT_TOKENS = 8192;
|
|
1667
1551
|
var CJK_CHAR = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/;
|
|
1668
1552
|
function estimateTokens(text) {
|
|
1669
1553
|
let cjk = 0;
|
|
@@ -1704,7 +1588,7 @@ function hardSlice(text, maxTokens) {
|
|
|
1704
1588
|
if (current !== "") out.push(current);
|
|
1705
1589
|
return out;
|
|
1706
1590
|
}
|
|
1707
|
-
function splitOversizedBlock(text, maxTokens =
|
|
1591
|
+
function splitOversizedBlock(text, maxTokens = REPLY_MAX_INPUT_TOKENS) {
|
|
1708
1592
|
if (estimateTokens(text) <= maxTokens) return [text];
|
|
1709
1593
|
let chunks = splitKeepingSeparator(text, /(\n{2,})/);
|
|
1710
1594
|
if (chunks.length <= 1) chunks = splitKeepingSeparator(text, /(\n)/);
|
|
@@ -1729,7 +1613,7 @@ function splitOversizedBlock(text, maxTokens = THINK_MAX_INPUT_TOKENS) {
|
|
|
1729
1613
|
if (current !== "") out.push(current);
|
|
1730
1614
|
return out;
|
|
1731
1615
|
}
|
|
1732
|
-
function packPieces(pieces, maxTokens =
|
|
1616
|
+
function packPieces(pieces, maxTokens = REPLY_MAX_INPUT_TOKENS) {
|
|
1733
1617
|
const batches = [];
|
|
1734
1618
|
let current = [];
|
|
1735
1619
|
let tokens = 0;
|
|
@@ -1746,7 +1630,7 @@ function packPieces(pieces, maxTokens = THINK_MAX_INPUT_TOKENS) {
|
|
|
1746
1630
|
if (current.length > 0) batches.push(current);
|
|
1747
1631
|
return batches;
|
|
1748
1632
|
}
|
|
1749
|
-
function
|
|
1633
|
+
function createBatchFormat() {
|
|
1750
1634
|
const id = randomTokenId();
|
|
1751
1635
|
return {
|
|
1752
1636
|
id,
|
|
@@ -1780,170 +1664,44 @@ function splitBatchTranslation(translated, format, count) {
|
|
|
1780
1664
|
}
|
|
1781
1665
|
return parts;
|
|
1782
1666
|
}
|
|
1783
|
-
function
|
|
1667
|
+
function hasBatchResidue(text) {
|
|
1784
1668
|
return new RegExp(BATCH_TOKEN_PATTERN_SOURCE, "u").test(text);
|
|
1785
1669
|
}
|
|
1786
1670
|
|
|
1787
1671
|
// src/server/dispatcher.ts
|
|
1788
|
-
|
|
1672
|
+
var CHANNEL_ID = "openai";
|
|
1673
|
+
function replyPieceKey(piece) {
|
|
1789
1674
|
return `${piece.block}:${piece.index}`;
|
|
1790
1675
|
}
|
|
1791
1676
|
var TranslationDispatcher = class {
|
|
1792
1677
|
configManager;
|
|
1793
1678
|
cache;
|
|
1794
|
-
/** 思考正文译文的独立缓存池,与工具标题的池互不挤占。 */
|
|
1795
|
-
thinkCache;
|
|
1796
1679
|
credentials;
|
|
1797
1680
|
masking = new ContentMaskingPipeline();
|
|
1798
1681
|
adapters = /* @__PURE__ */ new Map();
|
|
1799
1682
|
circuitStates = /* @__PURE__ */ new Map();
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
/** 思考链请求的串行队列尾,保证同时最多一个在途请求。 */
|
|
1804
|
-
thinkTail = Promise.resolve();
|
|
1805
|
-
constructor(configManager, cache, credentials, thinkCache = new LruDiskCache(THINK_CACHE_ENTRIES, "think-cache.json")) {
|
|
1683
|
+
/** 正文请求的串行队列尾,保证同时最多一个在途请求。 */
|
|
1684
|
+
replyTail = Promise.resolve();
|
|
1685
|
+
constructor(configManager, cache, credentials) {
|
|
1806
1686
|
this.configManager = configManager;
|
|
1807
1687
|
this.cache = cache;
|
|
1808
|
-
this.thinkCache = thinkCache;
|
|
1809
1688
|
this.credentials = credentials ?? { getApiKey: () => "" };
|
|
1810
1689
|
this.registerAdapter(new OpenAiCompatibleAdapter(this.credentials));
|
|
1811
|
-
this.registerAdapter(new BingWebAdapter());
|
|
1812
|
-
this.configManager.onConfigChange(() => {
|
|
1813
|
-
this.processNext();
|
|
1814
|
-
});
|
|
1815
1690
|
}
|
|
1816
1691
|
registerAdapter(adapter) {
|
|
1817
1692
|
this.adapters.set(adapter.id, adapter);
|
|
1818
1693
|
}
|
|
1819
1694
|
/**
|
|
1820
|
-
*
|
|
1695
|
+
* 翻译回答正文的块:本次唯一的翻译入口。
|
|
1821
1696
|
*
|
|
1822
|
-
*
|
|
1823
|
-
*
|
|
1824
|
-
*
|
|
1825
|
-
* - AI on + NOT configured + Bing off -> [] (no translation)
|
|
1826
|
-
* - AI off + Bing on -> [bing]
|
|
1827
|
-
* - AI off + Bing off -> [] (no translation)
|
|
1828
|
-
*/
|
|
1829
|
-
computeChannels(config) {
|
|
1830
|
-
const channels = [];
|
|
1831
|
-
for (const [id, adapter] of this.adapters) {
|
|
1832
|
-
if (id === "openai") {
|
|
1833
|
-
if (config.aiEnabled && config.baseUrl?.trim() && config.model?.trim() && this.credentials.getApiKey()) {
|
|
1834
|
-
channels.push(id);
|
|
1835
|
-
}
|
|
1836
|
-
continue;
|
|
1837
|
-
}
|
|
1838
|
-
if (id === "bing") {
|
|
1839
|
-
if (config.bingEnabled) channels.push(id);
|
|
1840
|
-
continue;
|
|
1841
|
-
}
|
|
1842
|
-
if (adapter.isAvailable(config)) channels.push(id);
|
|
1843
|
-
}
|
|
1844
|
-
return channels;
|
|
1845
|
-
}
|
|
1846
|
-
async translateBatch(texts, forceRefresh = false) {
|
|
1847
|
-
return Promise.all(texts.map((t) => this.translateOne(t, forceRefresh)));
|
|
1848
|
-
}
|
|
1849
|
-
async translateOne(rawText, forceRefresh = false) {
|
|
1850
|
-
const text = rawText.trim();
|
|
1851
|
-
if (!text) {
|
|
1852
|
-
return { original: rawText, translated: rawText, channel: "none", cached: true };
|
|
1853
|
-
}
|
|
1854
|
-
const config = this.configManager.getConfig();
|
|
1855
|
-
if (!config.enabled) {
|
|
1856
|
-
return { original: rawText, translated: rawText, channel: "disabled", cached: true };
|
|
1857
|
-
}
|
|
1858
|
-
const cacheKey = text.toLowerCase();
|
|
1859
|
-
if (!forceRefresh) {
|
|
1860
|
-
const cached = this.cache.get(cacheKey);
|
|
1861
|
-
if (cached) {
|
|
1862
|
-
return { original: rawText, translated: cached, channel: "cache", cached: true };
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
if (!forceRefresh) {
|
|
1866
|
-
const inFlight = this.inFlightMap.get(cacheKey);
|
|
1867
|
-
if (inFlight) {
|
|
1868
|
-
return inFlight;
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
const { maskedText, unmask, legacyFragments } = this.masking.mask(text);
|
|
1872
|
-
const documentedLegacy = new Set(legacyFragments.map((fragment) => fragment.toLowerCase()));
|
|
1873
|
-
const taskPromise = this.enqueueTask(async () => {
|
|
1874
|
-
const currentConfig = this.configManager.getConfig();
|
|
1875
|
-
const channels = this.computeChannels(currentConfig);
|
|
1876
|
-
for (const chId of channels) {
|
|
1877
|
-
const adapter = this.adapters.get(chId);
|
|
1878
|
-
if (!adapter || !adapter.isAvailable(currentConfig) || this.isCircuitOpen(chId)) {
|
|
1879
|
-
continue;
|
|
1880
|
-
}
|
|
1881
|
-
try {
|
|
1882
|
-
const timeout = chId === "openai" ? currentConfig.aiTimeoutMs || 3e4 : currentConfig.timeoutMs || 2e3;
|
|
1883
|
-
const abortCtrl = new AbortController();
|
|
1884
|
-
const timer = setTimeout(() => abortCtrl.abort(), timeout);
|
|
1885
|
-
let translatedMasked = "";
|
|
1886
|
-
try {
|
|
1887
|
-
translatedMasked = await adapter.translate(maskedText, abortCtrl.signal, currentConfig);
|
|
1888
|
-
} finally {
|
|
1889
|
-
clearTimeout(timer);
|
|
1890
|
-
}
|
|
1891
|
-
const cleaned = translatedMasked?.trim();
|
|
1892
|
-
if (cleaned && cleaned.length > 0) {
|
|
1893
|
-
const finalTranslated = unmask(cleaned);
|
|
1894
|
-
const legacyLeftovers = findLegacyMaskTokens(finalTranslated).filter(
|
|
1895
|
-
(fragment) => !documentedLegacy.has(fragment.toLowerCase())
|
|
1896
|
-
);
|
|
1897
|
-
if (hasMaskResidue(finalTranslated) || legacyLeftovers.length > 0) {
|
|
1898
|
-
this.recordFailure(chId);
|
|
1899
|
-
console.warn(
|
|
1900
|
-
`[dsh-chat-translate] channel ${chId} left a mask placeholder in the translation, discarding its result | text: ${text.slice(0, 60)}`
|
|
1901
|
-
);
|
|
1902
|
-
continue;
|
|
1903
|
-
}
|
|
1904
|
-
this.recordSuccess(chId);
|
|
1905
|
-
this.cache.set(cacheKey, finalTranslated);
|
|
1906
|
-
return {
|
|
1907
|
-
original: rawText,
|
|
1908
|
-
translated: finalTranslated,
|
|
1909
|
-
channel: chId,
|
|
1910
|
-
cached: false
|
|
1911
|
-
};
|
|
1912
|
-
}
|
|
1913
|
-
this.recordFailure(chId);
|
|
1914
|
-
console.warn(
|
|
1915
|
-
`[dsh-chat-translate] channel ${chId} returned an empty translation | text: ${text.slice(0, 60)}`
|
|
1916
|
-
);
|
|
1917
|
-
} catch (err) {
|
|
1918
|
-
this.recordFailure(chId);
|
|
1919
|
-
const detail = err instanceof MaskRestoreError ? ` (protected fragments expected ${err.expectedCount}, restored ${err.foundCount})` : "";
|
|
1920
|
-
console.warn(
|
|
1921
|
-
`[dsh-chat-translate] channel ${chId} failed: ${describeError(err)}${detail} | text: ${text.slice(0, 60)}`
|
|
1922
|
-
);
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
return { original: rawText, translated: rawText, channel: "fallback", cached: false };
|
|
1926
|
-
});
|
|
1927
|
-
if (!forceRefresh) {
|
|
1928
|
-
this.inFlightMap.set(cacheKey, taskPromise);
|
|
1929
|
-
}
|
|
1930
|
-
try {
|
|
1931
|
-
return await taskPromise;
|
|
1932
|
-
} finally {
|
|
1933
|
-
if (!forceRefresh) {
|
|
1934
|
-
this.inFlightMap.delete(cacheKey);
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
/**
|
|
1939
|
-
* 翻译思考正文的块。整条链路与工具标题翻译分离:只走 AI 通道、独立串行
|
|
1940
|
-
* 队列、独立超时、独立缓存池,Bing 从不参与。
|
|
1697
|
+
* 每个块先按输入上限切成片段,再各自掩码、相邻片段打包成一个请求;整批失败
|
|
1698
|
+
* 时退回逐片段单发。调用方传进来的每个块要么整块译出、要么整块保持原文:
|
|
1699
|
+
* 任一片段缺失(含掩码还原不通过)都让该块作废,避免半中半英的段落。
|
|
1941
1700
|
*
|
|
1942
|
-
*
|
|
1943
|
-
*
|
|
1944
|
-
* 避免半中半英的段落。
|
|
1701
|
+
* 客户端按 2048 估算 token 分块,所以一个 markdown 块可能跨多次调用;每次
|
|
1702
|
+
* 调用都独立决定成败,不会出现「前一段已挂译文、后一段失败」的半截结果。
|
|
1945
1703
|
*/
|
|
1946
|
-
async
|
|
1704
|
+
async translateReplyBlocks(blocks) {
|
|
1947
1705
|
const results = blocks.map((original) => ({
|
|
1948
1706
|
original,
|
|
1949
1707
|
translated: original,
|
|
@@ -1952,16 +1710,15 @@ var TranslationDispatcher = class {
|
|
|
1952
1710
|
channel: "none"
|
|
1953
1711
|
}));
|
|
1954
1712
|
const config = this.configManager.getConfig();
|
|
1955
|
-
if (!config.enabled
|
|
1956
|
-
const adapter = this.adapters.get(
|
|
1957
|
-
if (!adapter || !adapter.isAvailable(config)
|
|
1958
|
-
|
|
1959
|
-
}
|
|
1713
|
+
if (!config.enabled) return results;
|
|
1714
|
+
const adapter = this.adapters.get(CHANNEL_ID);
|
|
1715
|
+
if (!adapter || !adapter.isAvailable(config)) return results;
|
|
1716
|
+
if (this.isCircuitOpen(CHANNEL_ID)) return results;
|
|
1960
1717
|
const pieces = [];
|
|
1961
1718
|
for (let block = 0; block < blocks.length; block++) {
|
|
1962
1719
|
const text = (blocks[block] ?? "").trim();
|
|
1963
1720
|
if (!text) continue;
|
|
1964
|
-
const cached = this.
|
|
1721
|
+
const cached = this.cache.get(text.toLowerCase());
|
|
1965
1722
|
if (cached) {
|
|
1966
1723
|
results[block] = {
|
|
1967
1724
|
original: blocks[block],
|
|
@@ -1985,8 +1742,8 @@ var TranslationDispatcher = class {
|
|
|
1985
1742
|
}
|
|
1986
1743
|
const translated = /* @__PURE__ */ new Map();
|
|
1987
1744
|
for (const batch of packPieces(pieces)) {
|
|
1988
|
-
const outcome = await this.
|
|
1989
|
-
() => this.
|
|
1745
|
+
const outcome = await this.runReplySerial(
|
|
1746
|
+
() => this.translateReplyBatch(adapter, batch, config)
|
|
1990
1747
|
);
|
|
1991
1748
|
for (const [key, value] of outcome) translated.set(key, value);
|
|
1992
1749
|
}
|
|
@@ -2006,40 +1763,53 @@ var TranslationDispatcher = class {
|
|
|
2006
1763
|
if (!complete) continue;
|
|
2007
1764
|
const original = blocks[block];
|
|
2008
1765
|
const finalText = parts.join("");
|
|
2009
|
-
this.
|
|
2010
|
-
results[block] = {
|
|
1766
|
+
this.cache.set(original.trim().toLowerCase(), finalText);
|
|
1767
|
+
results[block] = {
|
|
1768
|
+
original,
|
|
1769
|
+
translated: finalText,
|
|
1770
|
+
ok: true,
|
|
1771
|
+
cached: false,
|
|
1772
|
+
channel: CHANNEL_ID
|
|
1773
|
+
};
|
|
2011
1774
|
}
|
|
2012
1775
|
return results;
|
|
2013
1776
|
}
|
|
2014
1777
|
/**
|
|
2015
1778
|
* 一整批一次请求;失败则该批逐片段单发重试一次,仍失败的片段直接放弃
|
|
2016
|
-
*
|
|
1779
|
+
* (保留原文),不在界面上留提示。失败的片段不进缓存,因此下一次滚回视口
|
|
1780
|
+
* 时会自然重试。
|
|
1781
|
+
*
|
|
1782
|
+
* 熔断记账按「批」算:一次批请求失败记一次,逐片段重试失败再各记一次。
|
|
2017
1783
|
*/
|
|
2018
|
-
async
|
|
1784
|
+
async translateReplyBatch(adapter, batch, config) {
|
|
2019
1785
|
try {
|
|
2020
|
-
|
|
1786
|
+
const outcome = await this.requestReplyBatch(adapter, batch, config);
|
|
1787
|
+
this.recordSuccess(CHANNEL_ID);
|
|
1788
|
+
return outcome;
|
|
2021
1789
|
} catch (err) {
|
|
1790
|
+
this.recordFailure(CHANNEL_ID);
|
|
2022
1791
|
console.warn(
|
|
2023
|
-
`[dsh-chat-translate]
|
|
1792
|
+
`[dsh-chat-translate] reply batch of ${batch.length} failed, retrying per block: ${describeError(err)}`
|
|
2024
1793
|
);
|
|
2025
1794
|
}
|
|
2026
1795
|
const out = /* @__PURE__ */ new Map();
|
|
2027
1796
|
for (const piece of batch) {
|
|
2028
1797
|
try {
|
|
2029
|
-
for (const [key, value] of await this.
|
|
1798
|
+
for (const [key, value] of await this.requestReplyBatch(adapter, [piece], config)) {
|
|
2030
1799
|
out.set(key, value);
|
|
2031
1800
|
}
|
|
2032
1801
|
} catch (err) {
|
|
1802
|
+
this.recordFailure(CHANNEL_ID);
|
|
2033
1803
|
console.warn(
|
|
2034
|
-
`[dsh-chat-translate]
|
|
1804
|
+
`[dsh-chat-translate] reply block ${piece.block} #${piece.index} failed, keeping the original: ${describeError(err)}`
|
|
2035
1805
|
);
|
|
2036
1806
|
}
|
|
2037
1807
|
}
|
|
2038
1808
|
return out;
|
|
2039
1809
|
}
|
|
2040
|
-
/**
|
|
2041
|
-
async
|
|
2042
|
-
const timeout = config.
|
|
1810
|
+
/** 发出一次正文请求并把结果还原成每个片段的最终译文。 */
|
|
1811
|
+
async requestReplyBatch(adapter, batch, config) {
|
|
1812
|
+
const timeout = config.aiTimeoutMs || 6e5;
|
|
2043
1813
|
const abortCtrl = new AbortController();
|
|
2044
1814
|
const timer = setTimeout(() => abortCtrl.abort(), timeout);
|
|
2045
1815
|
let answers;
|
|
@@ -2047,21 +1817,21 @@ var TranslationDispatcher = class {
|
|
|
2047
1817
|
if (batch.length === 1) {
|
|
2048
1818
|
answers = [
|
|
2049
1819
|
await adapter.translate(batch[0].text, abortCtrl.signal, config, {
|
|
2050
|
-
maxTokens:
|
|
1820
|
+
maxTokens: REPLY_MAX_OUTPUT_TOKENS,
|
|
2051
1821
|
mode: "plain"
|
|
2052
1822
|
})
|
|
2053
1823
|
];
|
|
2054
1824
|
} else {
|
|
2055
|
-
const format =
|
|
1825
|
+
const format = createBatchFormat();
|
|
2056
1826
|
const answer = await adapter.translate(
|
|
2057
1827
|
buildBatchPayload(batch.map((piece) => piece.text), format),
|
|
2058
1828
|
abortCtrl.signal,
|
|
2059
1829
|
config,
|
|
2060
|
-
{ maxTokens:
|
|
1830
|
+
{ maxTokens: REPLY_MAX_OUTPUT_TOKENS, mode: "blocks" }
|
|
2061
1831
|
);
|
|
2062
1832
|
const parts = splitBatchTranslation(answer, format, batch.length);
|
|
2063
1833
|
if (parts === null) {
|
|
2064
|
-
throw new Error("
|
|
1834
|
+
throw new Error("reply block markers did not survive the translation");
|
|
2065
1835
|
}
|
|
2066
1836
|
answers = parts;
|
|
2067
1837
|
}
|
|
@@ -2072,39 +1842,38 @@ var TranslationDispatcher = class {
|
|
|
2072
1842
|
batch.forEach((piece, index) => {
|
|
2073
1843
|
const answer = (answers[index] ?? "").trim();
|
|
2074
1844
|
if (!answer) {
|
|
2075
|
-
throw new Error(`
|
|
1845
|
+
throw new Error(`reply block ${piece.block} #${piece.index} came back empty`);
|
|
2076
1846
|
}
|
|
2077
|
-
if (
|
|
2078
|
-
throw new Error("
|
|
1847
|
+
if (hasBatchResidue(answer)) {
|
|
1848
|
+
throw new Error("reply translation left a block marker behind");
|
|
2079
1849
|
}
|
|
2080
1850
|
const finalText = piece.mask.unmask(answer);
|
|
2081
1851
|
const leftovers = findLegacyMaskTokens(finalText).filter(
|
|
2082
1852
|
(fragment) => !piece.legacy.has(fragment.toLowerCase())
|
|
2083
1853
|
);
|
|
2084
1854
|
if (hasMaskResidue(finalText) || leftovers.length > 0) {
|
|
2085
|
-
throw new Error("
|
|
1855
|
+
throw new Error("reply translation left a mask placeholder behind");
|
|
2086
1856
|
}
|
|
2087
|
-
out.set(
|
|
1857
|
+
out.set(replyPieceKey(piece), finalText);
|
|
2088
1858
|
});
|
|
2089
1859
|
return out;
|
|
2090
1860
|
}
|
|
2091
|
-
/**
|
|
2092
|
-
|
|
2093
|
-
const next = this.
|
|
2094
|
-
this.
|
|
1861
|
+
/** 串行执行器:同时最多一个在途请求。 */
|
|
1862
|
+
runReplySerial(task) {
|
|
1863
|
+
const next = this.replyTail.then(task, task);
|
|
1864
|
+
this.replyTail = next.then(
|
|
2095
1865
|
() => void 0,
|
|
2096
1866
|
() => void 0
|
|
2097
1867
|
);
|
|
2098
1868
|
return next;
|
|
2099
1869
|
}
|
|
2100
1870
|
/**
|
|
2101
|
-
*
|
|
2102
|
-
*
|
|
1871
|
+
* 通道探针:设置面板的「测试 AI 通道」按钮走这里。探测文本用一句英文正文,
|
|
1872
|
+
* 与真实请求同形。
|
|
1873
|
+
*
|
|
1874
|
+
* 探针是用户主动发起的连通性检查,因此不写也不读熔断账本:连点几次测试
|
|
1875
|
+
* 不该把正文翻译挡在冷却期外,正文的失败也不该让按钮变哑。
|
|
2103
1876
|
*/
|
|
2104
|
-
isChannelCoolingDown(channelId) {
|
|
2105
|
-
const state = this.circuitStates.get(channelId);
|
|
2106
|
-
return state !== void 0 && state.state === "open" && Date.now() < state.openUntil;
|
|
2107
|
-
}
|
|
2108
1877
|
async testChannel(channelId) {
|
|
2109
1878
|
const adapter = this.adapters.get(channelId);
|
|
2110
1879
|
const config = this.configManager.getConfig();
|
|
@@ -2114,10 +1883,10 @@ var TranslationDispatcher = class {
|
|
|
2114
1883
|
if (!adapter.isAvailable(config)) {
|
|
2115
1884
|
return { ok: false, latencyMs: 0, error: `Channel ${channelId} is not configured or disabled` };
|
|
2116
1885
|
}
|
|
2117
|
-
const testText = "
|
|
1886
|
+
const testText = "Here is what I changed.";
|
|
2118
1887
|
const start = Date.now();
|
|
2119
1888
|
try {
|
|
2120
|
-
const timeout =
|
|
1889
|
+
const timeout = Math.min(config.aiTimeoutMs || 3e4, 3e4);
|
|
2121
1890
|
const abortCtrl = new AbortController();
|
|
2122
1891
|
const timer = setTimeout(() => abortCtrl.abort(), timeout);
|
|
2123
1892
|
let res = "";
|
|
@@ -2135,43 +1904,6 @@ var TranslationDispatcher = class {
|
|
|
2135
1904
|
return { ok: false, latencyMs: Date.now() - start, error: describeError(err) };
|
|
2136
1905
|
}
|
|
2137
1906
|
}
|
|
2138
|
-
enqueueTask(task) {
|
|
2139
|
-
return new Promise((resolve3, reject) => {
|
|
2140
|
-
const exec = async () => {
|
|
2141
|
-
this.activeCount++;
|
|
2142
|
-
try {
|
|
2143
|
-
const result = await task();
|
|
2144
|
-
resolve3(result);
|
|
2145
|
-
} catch (err) {
|
|
2146
|
-
reject(err);
|
|
2147
|
-
} finally {
|
|
2148
|
-
this.activeCount--;
|
|
2149
|
-
this.processNext();
|
|
2150
|
-
}
|
|
2151
|
-
};
|
|
2152
|
-
const maxConcurrency = Math.min(
|
|
2153
|
-
Math.max(this.configManager.getConfig().concurrency || 3, 1),
|
|
2154
|
-
MAX_CONCURRENCY
|
|
2155
|
-
);
|
|
2156
|
-
if (this.activeCount < maxConcurrency) {
|
|
2157
|
-
exec();
|
|
2158
|
-
} else {
|
|
2159
|
-
this.queue.push(exec);
|
|
2160
|
-
}
|
|
2161
|
-
});
|
|
2162
|
-
}
|
|
2163
|
-
processNext() {
|
|
2164
|
-
const maxConcurrency = Math.min(
|
|
2165
|
-
Math.max(this.configManager.getConfig().concurrency || 3, 1),
|
|
2166
|
-
MAX_CONCURRENCY
|
|
2167
|
-
);
|
|
2168
|
-
while (this.queue.length > 0 && this.activeCount < maxConcurrency) {
|
|
2169
|
-
const next = this.queue.shift();
|
|
2170
|
-
if (next) {
|
|
2171
|
-
next();
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
1907
|
isCircuitOpen(channelId) {
|
|
2176
1908
|
let state = this.circuitStates.get(channelId);
|
|
2177
1909
|
if (!state) return false;
|
|
@@ -2221,9 +1953,8 @@ var TranslationDispatcher = class {
|
|
|
2221
1953
|
};
|
|
2222
1954
|
|
|
2223
1955
|
// src/server/router.ts
|
|
2224
|
-
var
|
|
1956
|
+
var REPLY_ROUTE_PATH = "/api/dsh-chat-translate/translate-reply";
|
|
2225
1957
|
var TEST_CHANNEL_ROUTE_PATH = "/api/dsh-chat-translate/test-channel";
|
|
2226
|
-
var THINK_ROUTE_PATH = "/api/dsh-chat-translate/translate-think";
|
|
2227
1958
|
function sendJson(status, body) {
|
|
2228
1959
|
return new Response(JSON.stringify(body), {
|
|
2229
1960
|
status,
|
|
@@ -2238,36 +1969,8 @@ async function readJson(request) {
|
|
|
2238
1969
|
}
|
|
2239
1970
|
}
|
|
2240
1971
|
function createFetchRoutes(dispatcher, ready = Promise.resolve()) {
|
|
2241
|
-
const
|
|
2242
|
-
path:
|
|
2243
|
-
methods: ["POST"],
|
|
2244
|
-
requestBody: "buffered",
|
|
2245
|
-
fetch: async (request) => {
|
|
2246
|
-
await ready;
|
|
2247
|
-
try {
|
|
2248
|
-
const body = await readJson(request);
|
|
2249
|
-
if (!body.ok) return body.response;
|
|
2250
|
-
const parsed = body.value;
|
|
2251
|
-
const rawTexts = parsed.texts !== void 0 ? parsed.texts : parsed.text;
|
|
2252
|
-
let texts = [];
|
|
2253
|
-
if (Array.isArray(rawTexts)) {
|
|
2254
|
-
texts = rawTexts.filter((t) => typeof t === "string");
|
|
2255
|
-
} else if (typeof rawTexts === "string") {
|
|
2256
|
-
texts = [rawTexts];
|
|
2257
|
-
}
|
|
2258
|
-
const forceRefresh = Boolean(parsed.forceRefresh);
|
|
2259
|
-
if (texts.length === 0) {
|
|
2260
|
-
return sendJson(200, { ok: true, results: [] });
|
|
2261
|
-
}
|
|
2262
|
-
const results = await dispatcher.translateBatch(texts, forceRefresh);
|
|
2263
|
-
return sendJson(200, { ok: true, results });
|
|
2264
|
-
} catch (err) {
|
|
2265
|
-
return sendJson(500, { ok: false, error: describeError(err) });
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
};
|
|
2269
|
-
const think = {
|
|
2270
|
-
path: THINK_ROUTE_PATH,
|
|
1972
|
+
const reply = {
|
|
1973
|
+
path: REPLY_ROUTE_PATH,
|
|
2271
1974
|
methods: ["POST"],
|
|
2272
1975
|
requestBody: "buffered",
|
|
2273
1976
|
fetch: async (request) => {
|
|
@@ -2280,7 +1983,7 @@ function createFetchRoutes(dispatcher, ready = Promise.resolve()) {
|
|
|
2280
1983
|
if (blocks.length === 0) {
|
|
2281
1984
|
return sendJson(200, { ok: true, results: [] });
|
|
2282
1985
|
}
|
|
2283
|
-
const results = await dispatcher.
|
|
1986
|
+
const results = await dispatcher.translateReplyBlocks(blocks);
|
|
2284
1987
|
return sendJson(200, { ok: true, results });
|
|
2285
1988
|
} catch (err) {
|
|
2286
1989
|
return sendJson(500, { ok: false, error: describeError(err) });
|
|
@@ -2304,7 +2007,7 @@ function createFetchRoutes(dispatcher, ready = Promise.resolve()) {
|
|
|
2304
2007
|
}
|
|
2305
2008
|
}
|
|
2306
2009
|
};
|
|
2307
|
-
return [
|
|
2010
|
+
return [reply, testChannel];
|
|
2308
2011
|
}
|
|
2309
2012
|
|
|
2310
2013
|
// src/index.ts
|
|
@@ -2312,13 +2015,7 @@ var name = "dsh-chat-translate";
|
|
|
2312
2015
|
var inject = ["settings", "credentials"];
|
|
2313
2016
|
var Config = Schema.object({
|
|
2314
2017
|
enabled: Schema.boolean().default(DEFAULT_CONFIG.enabled).volatile(),
|
|
2315
|
-
concurrency: Schema.number().min(1).max(MAX_CONCURRENCY).default(DEFAULT_CONFIG.concurrency).volatile(),
|
|
2316
|
-
timeoutMs: Schema.number().min(500).max(1e4).default(DEFAULT_CONFIG.timeoutMs).volatile(),
|
|
2317
2018
|
aiTimeoutMs: Schema.number().min(AI_TIMEOUT_MIN).max(AI_TIMEOUT_MAX).default(DEFAULT_CONFIG.aiTimeoutMs).volatile(),
|
|
2318
|
-
thinkTimeoutMs: Schema.number().min(THINK_TIMEOUT_MIN).max(THINK_TIMEOUT_MAX).default(DEFAULT_CONFIG.thinkTimeoutMs).volatile(),
|
|
2319
|
-
aiEnabled: Schema.boolean().default(DEFAULT_CONFIG.aiEnabled).volatile(),
|
|
2320
|
-
bingEnabled: Schema.boolean().default(DEFAULT_CONFIG.bingEnabled).volatile(),
|
|
2321
|
-
thinkEnabled: Schema.boolean().default(DEFAULT_CONFIG.thinkEnabled).volatile(),
|
|
2322
2019
|
baseUrl: Schema.string().default(DEFAULT_CONFIG.baseUrl).volatile(),
|
|
2323
2020
|
model: Schema.string().default(DEFAULT_CONFIG.model).volatile(),
|
|
2324
2021
|
targetLang: Schema.string().default(DEFAULT_CONFIG.targetLang).volatile()
|
|
@@ -2332,8 +2029,7 @@ function apply(ctx, config) {
|
|
|
2332
2029
|
credentials
|
|
2333
2030
|
);
|
|
2334
2031
|
const cache = new LruDiskCache(1e3);
|
|
2335
|
-
const
|
|
2336
|
-
const dispatcher = new TranslationDispatcher(configManager, cache, credentials, thinkCache);
|
|
2032
|
+
const dispatcher = new TranslationDispatcher(configManager, cache, credentials);
|
|
2337
2033
|
ctx.inject(["settings"], (child) => {
|
|
2338
2034
|
child.effect(
|
|
2339
2035
|
() => child.settings.configure({ auto: false }, child.fiber),
|
|
@@ -2344,8 +2040,9 @@ function apply(ctx, config) {
|
|
|
2344
2040
|
const initPromise = Promise.all([
|
|
2345
2041
|
credentials.init(),
|
|
2346
2042
|
cache.init(),
|
|
2347
|
-
|
|
2348
|
-
|
|
2043
|
+
migrateLegacyConfigFile(ctx.settings, legacyConfigPath),
|
|
2044
|
+
retireRemovedConfigKeys(ctx.settings),
|
|
2045
|
+
retireStoreFiles(["think-cache.json"])
|
|
2349
2046
|
]).catch((err) => {
|
|
2350
2047
|
console.warn("[dsh-chat-translate] Initialization error:", err);
|
|
2351
2048
|
});
|
|
@@ -2359,7 +2056,7 @@ function apply(ctx, config) {
|
|
|
2359
2056
|
(route) => connectionCtx.connection.fetch.register(route)
|
|
2360
2057
|
);
|
|
2361
2058
|
return () => {
|
|
2362
|
-
void Promise.all(disposers.map((dispose) => dispose())).then(() =>
|
|
2059
|
+
void Promise.all(disposers.map((dispose) => dispose())).then(() => cache.dispose()).catch((err) => {
|
|
2363
2060
|
console.warn("[dsh-chat-translate] Dispose translation routes error:", err);
|
|
2364
2061
|
});
|
|
2365
2062
|
};
|