@lynn123411/dsh-chat-translate 1.3.0 → 1.3.2
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 +2 -2
- package/lib/client.js +18 -0
- package/lib/client.js.map +3 -3
- package/lib/index.js +146 -52
- package/lib/index.js.map +4 -4
- package/lib/types/server/pipeline/mask-tokens.d.ts +76 -0
- package/lib/types/server/pipeline/masking.d.ts +20 -0
- package/package.json +2 -3
- package/LICENSE +0 -21
package/lib/index.js
CHANGED
|
@@ -996,6 +996,134 @@ var CredentialsReader = class {
|
|
|
996
996
|
// src/server/cache.ts
|
|
997
997
|
import * as fs2 from "node:fs/promises";
|
|
998
998
|
import * as path from "node:path";
|
|
999
|
+
|
|
1000
|
+
// src/server/pipeline/mask-tokens.ts
|
|
1001
|
+
var RANDOM_LETTERS = "abcdefghijklmnopqrstuvwxyz";
|
|
1002
|
+
function randomTokenId() {
|
|
1003
|
+
let out = "";
|
|
1004
|
+
for (let i = 0; i < 4; i++) {
|
|
1005
|
+
out += RANDOM_LETTERS[Math.floor(Math.random() * RANDOM_LETTERS.length)];
|
|
1006
|
+
}
|
|
1007
|
+
return out;
|
|
1008
|
+
}
|
|
1009
|
+
function createMaskTokenFormat() {
|
|
1010
|
+
const id = randomTokenId();
|
|
1011
|
+
return {
|
|
1012
|
+
id,
|
|
1013
|
+
token: (index) => `__DSHMASKx${id}_${index}__`
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
var MASK_TOKEN_PATTERN_SOURCE = "_*\\s*DSH\\s*_*\\s*MASK\\s*(?:_?\\s*(\\d+)|_*[xX]?[\\s._-]*([a-z]{2,8})\\s*_+\\s*(\\d+))(?:_{0,2}(?=[^\\w]|$))?";
|
|
1017
|
+
function matchMaskToken(text, start, id) {
|
|
1018
|
+
const anchored = new RegExp(`^(?:${MASK_TOKEN_PATTERN_SOURCE})`, "i").exec(text.slice(start));
|
|
1019
|
+
if (!anchored) return null;
|
|
1020
|
+
const index = Number.parseInt(anchored[1] ?? anchored[3] ?? "", 10);
|
|
1021
|
+
if (Number.isNaN(index)) return null;
|
|
1022
|
+
const tokenId = anchored[2];
|
|
1023
|
+
const acceptsId = tokenId === void 0 || tokenId.toLowerCase() === id.toLowerCase();
|
|
1024
|
+
return { index, acceptsId, length: anchored[0].length };
|
|
1025
|
+
}
|
|
1026
|
+
function hasMaskResidue(text) {
|
|
1027
|
+
if (!text) return false;
|
|
1028
|
+
return new RegExp(MASK_TOKEN_PATTERN_SOURCE, "i").test(text);
|
|
1029
|
+
}
|
|
1030
|
+
function maskResidues(text) {
|
|
1031
|
+
if (!text) return [];
|
|
1032
|
+
return text.match(new RegExp(MASK_TOKEN_PATTERN_SOURCE, "gi")) ?? [];
|
|
1033
|
+
}
|
|
1034
|
+
function hasNewMaskResidue(originalText, translatedText) {
|
|
1035
|
+
const residues = maskResidues(translatedText);
|
|
1036
|
+
if (residues.length === 0) return false;
|
|
1037
|
+
const presentInSource = /* @__PURE__ */ new Map();
|
|
1038
|
+
for (const fragment of maskResidues(originalText)) {
|
|
1039
|
+
const key = fragment.toLowerCase();
|
|
1040
|
+
presentInSource.set(key, (presentInSource.get(key) ?? 0) + 1);
|
|
1041
|
+
}
|
|
1042
|
+
for (const fragment of residues) {
|
|
1043
|
+
const key = fragment.toLowerCase();
|
|
1044
|
+
const remaining = presentInSource.get(key) ?? 0;
|
|
1045
|
+
if (remaining === 0) return true;
|
|
1046
|
+
presentInSource.set(key, remaining - 1);
|
|
1047
|
+
}
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// src/server/pipeline/masking.ts
|
|
1052
|
+
var MASK_MARKER = /DSH\s*_?\s*MASK/i;
|
|
1053
|
+
var MAX_UNMASK_PASSES = 1e4;
|
|
1054
|
+
var ContentMaskingPipeline = class {
|
|
1055
|
+
mask(text) {
|
|
1056
|
+
if (!text || typeof text !== "string") {
|
|
1057
|
+
return {
|
|
1058
|
+
maskedText: text,
|
|
1059
|
+
unmask: (t) => t
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
const masks = [];
|
|
1063
|
+
const format = createMaskTokenFormat();
|
|
1064
|
+
const addMask = (match) => {
|
|
1065
|
+
if (MASK_MARKER.test(match)) return match;
|
|
1066
|
+
const idx = masks.length;
|
|
1067
|
+
masks.push(match);
|
|
1068
|
+
return format.token(idx);
|
|
1069
|
+
};
|
|
1070
|
+
let processed = text;
|
|
1071
|
+
processed = processed.replace(/(?:```|~~~)[\s\S]*?(?:```|~~~)/g, (m) => addMask(m));
|
|
1072
|
+
processed = processed.replace(/`[^`\n]+`/g, (m) => addMask(m));
|
|
1073
|
+
processed = processed.replace(/https?:\/\/[^\s)\];,;"'<>]+/g, (m) => addMask(m));
|
|
1074
|
+
processed = processed.replace(
|
|
1075
|
+
/(?:\/[\w.\-\\\/]+|(?<=[\w.\-])\.\.?[\\\/][\w.\-\\\/]+|(?<=^|[\s([{"'`])[a-zA-Z]:[\\\/][\w.\-\\\/]+|(?<![^\s([{"'`])\b(?:[\w.\-]+\/)+[\w.\-]+\.[a-zA-Z0-9]+\b|(?<![^\s([{"'`])\b[\w.\-]+\.(?:ts|tsx|js|jsx|json|ya?ml|md|py|go|rs|c|cpp|h|hpp|css|scss|html|sh|bash|mjs|cjs|toml|lock|log|env|svg|png|jpe?g|gif|tar|gz|zip|xml|sql)\b)/g,
|
|
1076
|
+
(m) => addMask(m)
|
|
1077
|
+
);
|
|
1078
|
+
processed = processed.replace(
|
|
1079
|
+
/(?<=^|[\s(\[{"'])((?:--[a-zA-Z0-9_\-]+(?:=[^\s"'<>]+)?)|(?:-[a-zA-Z0-9]+))(?=[\s)\]}",:;!?]|$)/g,
|
|
1080
|
+
(m) => addMask(m)
|
|
1081
|
+
);
|
|
1082
|
+
const unmask = (translatedText) => {
|
|
1083
|
+
if (!translatedText || masks.length === 0) {
|
|
1084
|
+
return translatedText;
|
|
1085
|
+
}
|
|
1086
|
+
return replaceMaskTokens(
|
|
1087
|
+
translatedText,
|
|
1088
|
+
format.id,
|
|
1089
|
+
(index) => index >= 0 && index < masks.length ? masks[index] : void 0
|
|
1090
|
+
);
|
|
1091
|
+
};
|
|
1092
|
+
return {
|
|
1093
|
+
maskedText: processed,
|
|
1094
|
+
unmask
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
function replaceMaskTokens(text, id, resolve3) {
|
|
1099
|
+
const pattern2 = new RegExp(MASK_TOKEN_PATTERN_SOURCE, "i");
|
|
1100
|
+
let out = "";
|
|
1101
|
+
let cursor = 0;
|
|
1102
|
+
let passes = 0;
|
|
1103
|
+
while (cursor < text.length && passes++ < MAX_UNMASK_PASSES) {
|
|
1104
|
+
const candidate = pattern2.exec(text.slice(cursor));
|
|
1105
|
+
if (!candidate) break;
|
|
1106
|
+
const start = cursor + candidate.index;
|
|
1107
|
+
const match = matchMaskToken(text, start, id);
|
|
1108
|
+
const length = match?.length ?? candidate[0].length;
|
|
1109
|
+
if (length <= 0) {
|
|
1110
|
+
cursor = start + 1;
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
const replacement = match?.acceptsId ? resolve3(match.index) : void 0;
|
|
1114
|
+
out += text.slice(cursor, start) + (replacement ?? text.slice(start, start + length));
|
|
1115
|
+
cursor = start + length;
|
|
1116
|
+
}
|
|
1117
|
+
return out + text.slice(cursor);
|
|
1118
|
+
}
|
|
1119
|
+
function isMaskLeak(translatedText) {
|
|
1120
|
+
return hasMaskResidue(translatedText);
|
|
1121
|
+
}
|
|
1122
|
+
function isMaskLeakAgainst(originalText, translatedText) {
|
|
1123
|
+
return hasNewMaskResidue(originalText, translatedText);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/server/cache.ts
|
|
999
1127
|
var TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1000
1128
|
var LruDiskCache = class {
|
|
1001
1129
|
cache = /* @__PURE__ */ new Map();
|
|
@@ -1031,10 +1159,10 @@ var LruDiskCache = class {
|
|
|
1031
1159
|
if (obj && typeof obj === "object") {
|
|
1032
1160
|
for (const [k, raw] of Object.entries(obj)) {
|
|
1033
1161
|
if (typeof raw === "string") {
|
|
1034
|
-
this.cache.set(k, { t: 0, v: raw });
|
|
1162
|
+
if (!isMaskLeak(raw)) this.cache.set(k, { t: 0, v: raw });
|
|
1035
1163
|
} else if (raw && typeof raw === "object" && typeof raw.v === "string") {
|
|
1036
1164
|
const entry = raw;
|
|
1037
|
-
if (typeof entry.t === "number" && Number.isFinite(entry.t)) {
|
|
1165
|
+
if (typeof entry.t === "number" && Number.isFinite(entry.t) && !isMaskLeak(entry.v)) {
|
|
1038
1166
|
this.cache.set(k, entry);
|
|
1039
1167
|
}
|
|
1040
1168
|
}
|
|
@@ -1054,11 +1182,19 @@ var LruDiskCache = class {
|
|
|
1054
1182
|
this.cache.delete(key);
|
|
1055
1183
|
return void 0;
|
|
1056
1184
|
}
|
|
1185
|
+
if (isMaskLeak(entry.v)) {
|
|
1186
|
+
this.cache.delete(key);
|
|
1187
|
+
return void 0;
|
|
1188
|
+
}
|
|
1057
1189
|
this.cache.delete(key);
|
|
1058
1190
|
this.cache.set(key, entry);
|
|
1059
1191
|
return entry.v;
|
|
1060
1192
|
}
|
|
1061
1193
|
set(key, value) {
|
|
1194
|
+
if (isMaskLeak(value)) {
|
|
1195
|
+
console.warn("[dsh-chat-translate] refusing to cache a translation with a leaked mask placeholder");
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1062
1198
|
if (this.cache.has(key)) {
|
|
1063
1199
|
this.cache.delete(key);
|
|
1064
1200
|
} else if (this.cache.size >= this.maxEntries) {
|
|
@@ -1320,7 +1456,7 @@ var OpenAiCompatibleAdapter = class {
|
|
|
1320
1456
|
messages: [
|
|
1321
1457
|
{
|
|
1322
1458
|
role: "system",
|
|
1323
|
-
content: `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.
|
|
1459
|
+
content: `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 parts of the message are replaced by opaque placeholder markers (an uppercase token starting with "DSH" surrounded by double underscores). Copy every such marker into your output character-for-character, unchanged and in place. Never invent a marker that is not present in the message and never remove one.`
|
|
1324
1460
|
},
|
|
1325
1461
|
{ role: "user", content: text }
|
|
1326
1462
|
]
|
|
@@ -1346,55 +1482,6 @@ var OpenAiCompatibleAdapter = class {
|
|
|
1346
1482
|
}
|
|
1347
1483
|
};
|
|
1348
1484
|
|
|
1349
|
-
// src/server/pipeline/masking.ts
|
|
1350
|
-
var ContentMaskingPipeline = class {
|
|
1351
|
-
mask(text) {
|
|
1352
|
-
if (!text || typeof text !== "string") {
|
|
1353
|
-
return {
|
|
1354
|
-
maskedText: text,
|
|
1355
|
-
unmask: (t) => t
|
|
1356
|
-
};
|
|
1357
|
-
}
|
|
1358
|
-
const masks = [];
|
|
1359
|
-
const addMask = (match) => {
|
|
1360
|
-
const idx = masks.length;
|
|
1361
|
-
masks.push(match);
|
|
1362
|
-
return `__DSH_MASK_${idx}__`;
|
|
1363
|
-
};
|
|
1364
|
-
let processed = text;
|
|
1365
|
-
processed = processed.replace(/(?:```|~~~)[\s\S]*?(?:```|~~~)/g, (m) => addMask(m));
|
|
1366
|
-
processed = processed.replace(/`[^`\n]+`/g, (m) => addMask(m));
|
|
1367
|
-
processed = processed.replace(/https?:\/\/[^\s)\];,;"'<>]+/g, (m) => addMask(m));
|
|
1368
|
-
processed = processed.replace(
|
|
1369
|
-
/(?:(?:\/|[a-zA-Z]:[\\\/]|\.\.?[\\\/])[\w.\-\\\/]+|\b(?:[\w.\-]+\/)+[\w.\-]+\.[a-zA-Z0-9]+\b|\b[\w.\-]+\.(?:ts|tsx|js|jsx|json|ya?ml|md|py|go|rs|c|cpp|h|hpp|css|scss|html|sh|bash|mjs|cjs|toml|lock|log|env|svg|png|jpe?g|gif|tar|gz|zip|xml|sql)\b)/g,
|
|
1370
|
-
(m) => addMask(m)
|
|
1371
|
-
);
|
|
1372
|
-
processed = processed.replace(
|
|
1373
|
-
/(?<=^|[\s(\[{"'])((?:--[a-zA-Z0-9_\-]+(?:=[^\s"'<>]+)?)|(?:-[a-zA-Z0-9]+))(?=[\s)\]}",:;!?]|$)/g,
|
|
1374
|
-
(m) => addMask(m)
|
|
1375
|
-
);
|
|
1376
|
-
const unmask = (translatedText) => {
|
|
1377
|
-
if (!translatedText || masks.length === 0) {
|
|
1378
|
-
return translatedText;
|
|
1379
|
-
}
|
|
1380
|
-
return translatedText.replace(
|
|
1381
|
-
/__\s*DSH\s*_\s*MASK\s*_\s*(\d+)\s*__/gi,
|
|
1382
|
-
(_fullMatch, indexStr) => {
|
|
1383
|
-
const idx = parseInt(indexStr, 10);
|
|
1384
|
-
if (!Number.isNaN(idx) && idx >= 0 && idx < masks.length) {
|
|
1385
|
-
return masks[idx];
|
|
1386
|
-
}
|
|
1387
|
-
return _fullMatch;
|
|
1388
|
-
}
|
|
1389
|
-
);
|
|
1390
|
-
};
|
|
1391
|
-
return {
|
|
1392
|
-
maskedText: processed,
|
|
1393
|
-
unmask
|
|
1394
|
-
};
|
|
1395
|
-
}
|
|
1396
|
-
};
|
|
1397
|
-
|
|
1398
1485
|
// src/server/dispatcher.ts
|
|
1399
1486
|
var TranslationDispatcher = class {
|
|
1400
1487
|
configManager;
|
|
@@ -1493,6 +1580,13 @@ var TranslationDispatcher = class {
|
|
|
1493
1580
|
const cleaned = translatedMasked?.trim();
|
|
1494
1581
|
if (cleaned && cleaned.length > 0) {
|
|
1495
1582
|
const finalTranslated = unmask(cleaned);
|
|
1583
|
+
if (isMaskLeakAgainst(text, finalTranslated)) {
|
|
1584
|
+
this.recordFailure(chId);
|
|
1585
|
+
console.warn(
|
|
1586
|
+
`[dsh-chat-translate] channel ${chId} leaked a mask placeholder, discarding its result | text: ${text.slice(0, 60)}`
|
|
1587
|
+
);
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1496
1590
|
this.recordSuccess(chId);
|
|
1497
1591
|
this.cache.set(cacheKey, finalTranslated);
|
|
1498
1592
|
return {
|