@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/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. Preserve every placeholder like __DSH_MASK_0__ exactly as-is.`
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 {