@lynn123411/dsh-chat-translate 2.0.0 → 3.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/lib/index.js CHANGED
@@ -864,56 +864,21 @@ defineMethod("transform", [
864
864
  "preserve"
865
865
  ], ({ inner }, isInner) => inner.toString(isInner));
866
866
 
867
- // node_modules/.pnpm/@deepseek-ai+dsh-home-paths@0.1.7-rc.2_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-home-paths/lib/index.js
868
- import { homedir } from "node:os";
869
- import { basename, dirname, join, resolve as resolve2 } from "node:path";
870
- var DSH_HOME_DIR_NAME = ".dsh";
871
- var DEFAULT_DSH_HOME_DISPLAY = `~/${DSH_HOME_DIR_NAME}`;
872
- var DSH_HOME_ENV = "DSH_HOME";
873
- function defaultDshHome() {
874
- return join(homedir(), DSH_HOME_DIR_NAME);
875
- }
876
- function expandHomePath(path2) {
877
- if (path2 === "~") return homedir();
878
- if (path2.startsWith("~/") || path2.startsWith("~\\")) return join(homedir(), path2.slice(2));
879
- return path2;
880
- }
881
- function resolveDshHome(configured, env = process.env) {
882
- const fromEnv = env[DSH_HOME_ENV];
883
- return resolve2(expandHomePath(configured ?? (fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : defaultDshHome())));
884
- }
885
- function dshHomePath(...segments) {
886
- return join(resolveDshHome(), ...segments);
887
- }
888
-
889
867
  // src/server/config.ts
890
- import * as fs from "node:fs/promises";
891
868
  var AI_TIMEOUT_MIN = 500;
892
869
  var AI_TIMEOUT_MAX = 9e5;
893
- var SETTINGS_NAMESPACE = "dsh-chat-translate";
894
870
  var DEFAULT_CONFIG = {
895
871
  enabled: true,
896
872
  aiTimeoutMs: 6e5,
897
873
  baseUrl: "",
898
- model: "",
899
- targetLang: "zh-Hans"
874
+ model: ""
900
875
  };
901
- var RETIRED_CONFIG_KEYS = [
902
- "channels",
903
- "concurrency",
904
- "timeoutMs",
905
- "thinkTimeoutMs",
906
- "aiEnabled",
907
- "bingEnabled",
908
- "thinkEnabled"
909
- ];
910
876
  function readPluginConfig(refs) {
911
877
  return {
912
878
  enabled: refs.enabled.get(),
913
879
  aiTimeoutMs: refs.aiTimeoutMs.get(),
914
880
  baseUrl: refs.baseUrl.get(),
915
- model: refs.model.get(),
916
- targetLang: refs.targetLang.get()
881
+ model: refs.model.get()
917
882
  };
918
883
  }
919
884
  function createLiveConfigSource(onVolatileUpdate, refs) {
@@ -943,91 +908,6 @@ var ConfigManager = class {
943
908
  return this.source.watch(listener);
944
909
  }
945
910
  };
946
- function sanitizePatch(input) {
947
- const next = {};
948
- if (typeof input.enabled === "boolean") next.enabled = input.enabled;
949
- if (typeof input.aiTimeoutMs === "number" && Number.isFinite(input.aiTimeoutMs)) {
950
- next.aiTimeoutMs = Math.min(
951
- Math.max(Math.round(input.aiTimeoutMs), AI_TIMEOUT_MIN),
952
- AI_TIMEOUT_MAX
953
- );
954
- }
955
- if (typeof input.baseUrl === "string") next.baseUrl = input.baseUrl.trim();
956
- if (typeof input.model === "string") next.model = input.model.trim();
957
- if (typeof input.targetLang === "string" && input.targetLang.trim()) {
958
- next.targetLang = input.targetLang.trim();
959
- }
960
- return next;
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
- }
982
- async function migrateLegacyConfigFile(settings, legacyPath) {
983
- let raw;
984
- try {
985
- raw = await fs.readFile(legacyPath, "utf-8");
986
- } catch {
987
- return false;
988
- }
989
- let legacy;
990
- try {
991
- legacy = JSON.parse(raw);
992
- } catch {
993
- await fs.unlink(legacyPath).catch(() => {
994
- });
995
- return false;
996
- }
997
- if (typeof legacy !== "object" || legacy === null || Array.isArray(legacy)) {
998
- await fs.unlink(legacyPath).catch(() => {
999
- });
1000
- return false;
1001
- }
1002
- const record = legacy;
1003
- const descriptor = settings.describe().find((d) => d.ns === SETTINGS_NAMESPACE);
1004
- if (descriptor?.user !== void 0) {
1005
- await fs.unlink(legacyPath).catch(() => {
1006
- });
1007
- return false;
1008
- }
1009
- const patch = sanitizePatch(record);
1010
- const ops = Object.entries(patch).map(([field, value]) => ({
1011
- op: "set",
1012
- path: [field],
1013
- value
1014
- }));
1015
- if (ops.length === 0) {
1016
- await fs.unlink(legacyPath).catch(() => {
1017
- });
1018
- return false;
1019
- }
1020
- try {
1021
- await settings.mutate(SETTINGS_NAMESPACE, ops, descriptor?.revision);
1022
- } catch (err) {
1023
- console.warn("[dsh-chat-translate] Legacy config migration failed; will retry on next boot:", err);
1024
- return false;
1025
- }
1026
- await fs.unlink(legacyPath).catch((err) => {
1027
- console.warn("[dsh-chat-translate] Failed to remove legacy config file:", err);
1028
- });
1029
- return true;
1030
- }
1031
911
 
1032
912
  // src/server/credentials.ts
1033
913
  var TRANSLATE_API_KEY_REF = "TRANSLATE_API_KEY";
@@ -1094,9 +974,31 @@ var CredentialsReader = class {
1094
974
  };
1095
975
 
1096
976
  // src/server/cache.ts
1097
- import * as fs2 from "node:fs/promises";
977
+ import * as fs from "node:fs/promises";
1098
978
  import * as path from "node:path";
1099
979
 
980
+ // node_modules/.pnpm/@deepseek-ai+dsh-home-paths@0.1.7-rc.2_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-home-paths/lib/index.js
981
+ import { homedir } from "node:os";
982
+ import { basename, dirname, join, resolve as resolve2 } from "node:path";
983
+ var DSH_HOME_DIR_NAME = ".dsh";
984
+ var DEFAULT_DSH_HOME_DISPLAY = `~/${DSH_HOME_DIR_NAME}`;
985
+ var DSH_HOME_ENV = "DSH_HOME";
986
+ function defaultDshHome() {
987
+ return join(homedir(), DSH_HOME_DIR_NAME);
988
+ }
989
+ function expandHomePath(path2) {
990
+ if (path2 === "~") return homedir();
991
+ if (path2.startsWith("~/") || path2.startsWith("~\\")) return join(homedir(), path2.slice(2));
992
+ return path2;
993
+ }
994
+ function resolveDshHome(configured, env = process.env) {
995
+ const fromEnv = env[DSH_HOME_ENV];
996
+ return resolve2(expandHomePath(configured ?? (fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : defaultDshHome())));
997
+ }
998
+ function dshHomePath(...segments) {
999
+ return join(resolveDshHome(), ...segments);
1000
+ }
1001
+
1100
1002
  // src/server/pipeline/mask-tokens.ts
1101
1003
  var RANDOM_LETTERS = "abcdefghijklmnopqrstuvwxyz";
1102
1004
  function randomTokenId() {
@@ -1115,8 +1017,6 @@ function createMaskTokenFormat() {
1115
1017
  }
1116
1018
  var MASK_TOKEN_PATTERN_SOURCE = "\u27E6([a-z]{4})(\\d+)\u27E7";
1117
1019
  var MASK_TOKEN_PREFIX_PATTERN_SOURCE = "\u27E6\\s*([a-z]{4})(\\d+)";
1118
- var LEGACY_MASK_PATTERN_SOURCE = "_{1,2}DSH\\s*_*\\s*MASKx?\\s*(?:_?\\s*\\d+|_*([a-z]{2,8})\\s*_+\\s*(\\d+))_{0,2}";
1119
- var LEGACY_MASK_TOKEN_PATTERN_SOURCE = LEGACY_MASK_PATTERN_SOURCE;
1120
1020
  function matchMaskToken(text, start, id) {
1121
1021
  const complete = new RegExp(`^(?:${MASK_TOKEN_PATTERN_SOURCE})`, "iu").exec(text.slice(start));
1122
1022
  const truncated = complete ? null : new RegExp(`^(?:${MASK_TOKEN_PREFIX_PATTERN_SOURCE})`, "iu").exec(text.slice(start));
@@ -1131,17 +1031,6 @@ function hasMaskResidue(text) {
1131
1031
  if (!text) return false;
1132
1032
  return new RegExp(MASK_TOKEN_PATTERN_SOURCE, "iu").test(text);
1133
1033
  }
1134
- function hasLegacyMaskResidue(text) {
1135
- if (!text) return false;
1136
- return new RegExp(LEGACY_MASK_PATTERN_SOURCE, "iu").test(text);
1137
- }
1138
- function hasAnyMaskResidue(text) {
1139
- return hasMaskResidue(text) || hasLegacyMaskResidue(text);
1140
- }
1141
- function findLegacyMaskTokens(text) {
1142
- if (!text) return [];
1143
- return text.match(new RegExp(LEGACY_MASK_TOKEN_PATTERN_SOURCE, "giu")) ?? [];
1144
- }
1145
1034
 
1146
1035
  // src/server/pipeline/masking.ts
1147
1036
  var MAX_UNMASK_PASSES = 1e4;
@@ -1162,12 +1051,10 @@ var ContentMaskingPipeline = class {
1162
1051
  if (!text || typeof text !== "string") {
1163
1052
  return {
1164
1053
  maskedText: text,
1165
- unmask: (t) => t,
1166
- legacyFragments: []
1054
+ unmask: (t) => t
1167
1055
  };
1168
1056
  }
1169
1057
  const masks = [];
1170
- const legacyIndexes = /* @__PURE__ */ new Set();
1171
1058
  const inserted = [];
1172
1059
  const format = createMaskTokenFormat();
1173
1060
  const addFragment = (match, before, after, onPlace) => {
@@ -1186,12 +1073,7 @@ var ContentMaskingPipeline = class {
1186
1073
  const whole = args[args.length - 1];
1187
1074
  return addFragment(match, whole[offset - 1], whole[offset + match.length], onPlace);
1188
1075
  });
1189
- let processed = wrapRule(
1190
- text,
1191
- new RegExp(LEGACY_MASK_TOKEN_PATTERN_SOURCE, "giu"),
1192
- (index) => legacyIndexes.add(index)
1193
- );
1194
- processed = wrapRule(processed, /(?:```|~~~)[\s\S]*?(?:```|~~~)/g);
1076
+ let processed = wrapRule(text, /(?:```|~~~)[\s\S]*?(?:```|~~~)/g);
1195
1077
  processed = wrapRule(processed, /`[^`\n]+`/g);
1196
1078
  processed = wrapRule(processed, /https?:\/\/[^\s)\];,;"'<>]+/g);
1197
1079
  processed = wrapRule(
@@ -1205,7 +1087,7 @@ var ContentMaskingPipeline = class {
1205
1087
  const unmask = (translatedText) => {
1206
1088
  if (!translatedText) return translatedText;
1207
1089
  if (masks.length === 0) {
1208
- if (hasAnyMaskResidue(translatedText)) {
1090
+ if (hasMaskResidue(translatedText)) {
1209
1091
  throw new MaskRestoreError(
1210
1092
  "translation carries a mask token while the source had none",
1211
1093
  0,
@@ -1214,16 +1096,15 @@ var ContentMaskingPipeline = class {
1214
1096
  }
1215
1097
  return translatedText;
1216
1098
  }
1217
- return replaceMaskTokens(translatedText, format.id, masks, inserted, legacyIndexes);
1099
+ return replaceMaskTokens(translatedText, format.id, masks, inserted);
1218
1100
  };
1219
1101
  return {
1220
1102
  maskedText: processed,
1221
- unmask,
1222
- legacyFragments: [...legacyIndexes].map((index) => masks[index]).filter((f) => f !== void 0)
1103
+ unmask
1223
1104
  };
1224
1105
  }
1225
1106
  };
1226
- function replaceMaskTokens(text, id, masks, inserted, legacyIndexes = /* @__PURE__ */ new Set()) {
1107
+ function replaceMaskTokens(text, id, masks, inserted) {
1227
1108
  const complete = new RegExp(MASK_TOKEN_PATTERN_SOURCE, "giu");
1228
1109
  const prefix = new RegExp(MASK_TOKEN_PREFIX_PATTERN_SOURCE, "giu");
1229
1110
  const seen = /* @__PURE__ */ new Set();
@@ -1265,15 +1146,7 @@ function replaceMaskTokens(text, id, masks, inserted, legacyIndexes = /* @__PURE
1265
1146
  start = nextStart(whole, prefixCandidate);
1266
1147
  }
1267
1148
  out += text.slice(cursor);
1268
- const documented = /* @__PURE__ */ new Set();
1269
- for (const index of legacyIndexes) {
1270
- const fragment = masks[index];
1271
- if (fragment !== void 0) documented.add(fragment.toLowerCase());
1272
- }
1273
- const residue = (out.match(new RegExp(LEGACY_MASK_TOKEN_PATTERN_SOURCE, "giu")) ?? []).filter(
1274
- (fragment) => !documented.has(fragment.toLowerCase())
1275
- );
1276
- if (seen.size !== masks.length || hasMaskResidue(out) || residue.length > 0) {
1149
+ if (seen.size !== masks.length || hasMaskResidue(out)) {
1277
1150
  throw new MaskRestoreError(
1278
1151
  `translation does not reproduce every protected fragment (expected ${masks.length}, found ${seen.size})`,
1279
1152
  masks.length,
@@ -1283,76 +1156,51 @@ function replaceMaskTokens(text, id, masks, inserted, legacyIndexes = /* @__PURE
1283
1156
  return out;
1284
1157
  }
1285
1158
  function isMaskLeak(translatedText) {
1286
- return hasAnyMaskResidue(translatedText);
1159
+ return hasMaskResidue(translatedText);
1287
1160
  }
1288
1161
 
1162
+ // src/server/prompt-revision.ts
1163
+ var PROMPT_REVISION = "r1";
1164
+
1289
1165
  // src/server/cache.ts
1290
1166
  var TTL_MS = 7 * 24 * 60 * 60 * 1e3;
1291
1167
  var LruDiskCache = class {
1292
1168
  cache = /* @__PURE__ */ new Map();
1293
1169
  maxEntries;
1294
1170
  filePath;
1171
+ revision;
1295
1172
  saveTimer = null;
1296
1173
  dirty = false;
1297
- /** Legacy root-level cache file (<=1.1); moved under the plugin subdir. */
1298
- legacyPath;
1299
- /** Only the original cache file carries a pre-1.2 root-level predecessor. */
1300
- hasLegacyPredecessor;
1301
- constructor(maxEntries = 1e3, fileName = "cache.json") {
1174
+ constructor(maxEntries = 1e3, fileName = "cache.json", revision = PROMPT_REVISION) {
1302
1175
  this.maxEntries = maxEntries;
1176
+ this.revision = revision;
1303
1177
  this.filePath = dshHomePath("dsh-chat-translate", fileName);
1304
- this.legacyPath = dshHomePath("dsh-chat-translate-cache.json");
1305
- this.hasLegacyPredecessor = fileName === "cache.json";
1306
1178
  }
1307
1179
  async init() {
1308
- if (!this.hasLegacyPredecessor) {
1309
- await this.loadFile(this.filePath);
1310
- return;
1311
- }
1312
- let readPath = this.filePath;
1313
1180
  try {
1314
- await fs2.access(this.filePath);
1315
- await fs2.unlink(this.legacyPath).catch(() => {
1316
- });
1317
- } catch {
1318
- try {
1319
- await fs2.mkdir(path.dirname(this.filePath), { recursive: true });
1320
- await fs2.rename(this.legacyPath, this.filePath);
1321
- readPath = this.filePath;
1322
- } catch {
1323
- readPath = this.legacyPath;
1181
+ const content = await fs.readFile(this.filePath, "utf-8");
1182
+ const doc = JSON.parse(content);
1183
+ if (doc && typeof doc === "object" && doc.rev === this.revision && typeof doc.entries === "object" && doc.entries !== null) {
1184
+ this.loadEntries(doc.entries);
1324
1185
  }
1325
- }
1326
- await this.loadFile(readPath);
1327
- if (readPath !== this.filePath) {
1328
- await fs2.unlink(this.legacyPath).catch(() => {
1329
- });
1186
+ } catch {
1330
1187
  }
1331
1188
  }
1332
- /** Load one cache document into memory, dropping poisoned or malformed entries. */
1333
- async loadFile(readPath) {
1334
- try {
1335
- const content = await fs2.readFile(readPath, "utf-8");
1336
- const obj = JSON.parse(content);
1337
- if (obj && typeof obj === "object") {
1338
- for (const [k, raw] of Object.entries(obj)) {
1339
- if (typeof raw === "string") {
1340
- if (!isMaskLeak(raw)) this.cache.set(k, { t: 0, v: raw });
1341
- } else if (raw && typeof raw === "object" && typeof raw.v === "string") {
1342
- const entry = raw;
1343
- if (typeof entry.t === "number" && Number.isFinite(entry.t) && !isMaskLeak(entry.v)) {
1344
- this.cache.set(k, entry);
1345
- }
1346
- }
1189
+ /** Adopt loaded entries, dropping poisoned or malformed ones. */
1190
+ loadEntries(raw) {
1191
+ for (const [k, entry] of Object.entries(raw)) {
1192
+ if (entry && typeof entry === "object" && typeof entry.v === "string") {
1193
+ const value = entry;
1194
+ if (typeof value.t === "number" && Number.isFinite(value.t) && !isMaskLeak(value.v)) {
1195
+ this.cache.set(k, value);
1347
1196
  }
1348
1197
  }
1349
- } catch {
1350
1198
  }
1351
1199
  }
1352
1200
  get(key) {
1353
1201
  const entry = this.cache.get(key);
1354
1202
  if (entry === void 0) return void 0;
1355
- if (entry.t > 0 && Date.now() - entry.t > TTL_MS) {
1203
+ if (Date.now() - entry.t > TTL_MS) {
1356
1204
  this.cache.delete(key);
1357
1205
  return void 0;
1358
1206
  }
@@ -1399,18 +1247,19 @@ var LruDiskCache = class {
1399
1247
  }
1400
1248
  const tmpPath = `${this.filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
1401
1249
  try {
1402
- const obj = {};
1250
+ const entries = {};
1403
1251
  for (const [k, v] of this.cache.entries()) {
1404
- obj[k] = v;
1252
+ entries[k] = v;
1405
1253
  }
1406
- await fs2.mkdir(path.dirname(this.filePath), { recursive: true });
1407
- await fs2.writeFile(tmpPath, JSON.stringify(obj, null, 2), "utf-8");
1408
- await fs2.rename(tmpPath, this.filePath);
1254
+ const doc = { rev: this.revision, entries };
1255
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
1256
+ await fs.writeFile(tmpPath, JSON.stringify(doc, null, 2), "utf-8");
1257
+ await fs.rename(tmpPath, this.filePath);
1409
1258
  this.dirty = false;
1410
1259
  } catch (err) {
1411
1260
  console.warn("[dsh-chat-translate] Failed to write cache file atomically:", err);
1412
1261
  try {
1413
- await fs2.unlink(tmpPath);
1262
+ await fs.unlink(tmpPath);
1414
1263
  } catch {
1415
1264
  }
1416
1265
  if (this.dirty) {
@@ -1428,17 +1277,6 @@ var LruDiskCache = class {
1428
1277
  }
1429
1278
  }
1430
1279
  };
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
- }
1442
1280
 
1443
1281
  // src/describe-error.ts
1444
1282
  function describeError(err) {
@@ -1467,26 +1305,11 @@ function describeError(err) {
1467
1305
  }
1468
1306
 
1469
1307
  // src/server/adapters/openai.ts
1470
- var LANG_HINTS = {
1471
- "zh-hans": "Simplified Chinese",
1472
- "zh-cn": "Simplified Chinese",
1473
- "zh": "Simplified Chinese",
1474
- "zh-tw": "Traditional Chinese",
1475
- "zh-hant": "Traditional Chinese",
1476
- en: "English",
1477
- ja: "Japanese",
1478
- ko: "Korean",
1479
- fr: "French",
1480
- de: "German",
1481
- es: "Spanish",
1482
- ru: "Russian",
1483
- pt: "Portuguese",
1484
- it: "Italian"
1485
- };
1486
- function buildSystemPrompt(langName, mode) {
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.`;
1308
+ var TARGET_LANGUAGE = "Simplified Chinese";
1309
+ function buildSystemPrompt(mode) {
1310
+ const base = `You are a professional translator and editor. Rewrite the user's message as natural, idiomatic ${TARGET_LANGUAGE} the way a native writer would produce it. This applies to every passage, including passages already written in ${TARGET_LANGUAGE}: smooth out stiff or machine-translated phrasing while keeping the meaning, tone, proper names, numbers and technical terms unchanged. Never drop content and never add content. The message is Markdown that will be re-rendered as Markdown: translate the prose only, and keep every Markdown construct exactly as authored \u2014 headings, list markers, table pipes and cell order, quote markers, bold and emphasis runs, links, and inline code. Never add, remove, reorder, or nest Markdown syntax of your own. Output ONLY the resulting text \u2014 no explanations, no quotation marks, no extra words, no code fence around the whole answer. 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.`;
1488
1311
  if (mode === "plain") return base;
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.`;
1312
+ 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 rewrite on the following lines. Rewrite each part on its own, keep the parts in the original order, and never merge two parts into one answer.`;
1490
1313
  }
1491
1314
  var OpenAiCompatibleAdapter = class {
1492
1315
  id = "openai";
@@ -1508,7 +1331,6 @@ var OpenAiCompatibleAdapter = class {
1508
1331
  if (!baseUrl || !model) {
1509
1332
  throw new Error("OpenAI channel: baseUrl or model is not configured");
1510
1333
  }
1511
- const langName = LANG_HINTS[(config.targetLang || "zh-Hans").toLowerCase()] || config.targetLang || "Simplified Chinese";
1512
1334
  const response = await fetch(`${baseUrl}/chat/completions`, {
1513
1335
  method: "POST",
1514
1336
  headers: {
@@ -1520,7 +1342,7 @@ var OpenAiCompatibleAdapter = class {
1520
1342
  temperature: 0,
1521
1343
  ...options?.maxTokens === void 0 ? {} : { max_tokens: options.maxTokens },
1522
1344
  messages: [
1523
- { role: "system", content: buildSystemPrompt(langName, options?.mode ?? "plain") },
1345
+ { role: "system", content: buildSystemPrompt(options?.mode ?? "plain") },
1524
1346
  { role: "user", content: text }
1525
1347
  ]
1526
1348
  }),
@@ -1670,6 +1492,8 @@ function hasBatchResidue(text) {
1670
1492
 
1671
1493
  // src/server/dispatcher.ts
1672
1494
  var CHANNEL_ID = "openai";
1495
+ var ContentRejectedError = class extends Error {
1496
+ };
1673
1497
  function replyPieceKey(piece) {
1674
1498
  return `${piece.block}:${piece.index}`;
1675
1499
  }
@@ -1714,6 +1538,15 @@ var TranslationDispatcher = class {
1714
1538
  const adapter = this.adapters.get(CHANNEL_ID);
1715
1539
  if (!adapter || !adapter.isAvailable(config)) return results;
1716
1540
  if (this.isCircuitOpen(CHANNEL_ID)) return results;
1541
+ try {
1542
+ return await this.serveReplyBlocks(blocks, results, adapter, config);
1543
+ } finally {
1544
+ const state = this.circuitStates.get(CHANNEL_ID);
1545
+ if (state) state.probeInFlight = false;
1546
+ }
1547
+ }
1548
+ /** 熔断授予之后的正文执行段:切块、打包、请求、拼装。 */
1549
+ async serveReplyBlocks(blocks, results, adapter, config) {
1717
1550
  const pieces = [];
1718
1551
  for (let block = 0; block < blocks.length; block++) {
1719
1552
  const text = (blocks[block] ?? "").trim();
@@ -1735,8 +1568,7 @@ var TranslationDispatcher = class {
1735
1568
  block,
1736
1569
  index,
1737
1570
  text: mask.maskedText,
1738
- mask,
1739
- legacy: new Set(mask.legacyFragments.map((fragment) => fragment.toLowerCase()))
1571
+ mask
1740
1572
  });
1741
1573
  }
1742
1574
  }
@@ -1780,6 +1612,8 @@ var TranslationDispatcher = class {
1780
1612
  * 时会自然重试。
1781
1613
  *
1782
1614
  * 熔断记账按「批」算:一次批请求失败记一次,逐片段重试失败再各记一次。
1615
+ * 内容级拒收除外:响应都正常回来了,通道是活的——拒收记一次成功,
1616
+ * 坏内容只作废坏内容自己,不把其他行挡在冷却期外。
1783
1617
  */
1784
1618
  async translateReplyBatch(adapter, batch, config) {
1785
1619
  try {
@@ -1787,7 +1621,7 @@ var TranslationDispatcher = class {
1787
1621
  this.recordSuccess(CHANNEL_ID);
1788
1622
  return outcome;
1789
1623
  } catch (err) {
1790
- this.recordFailure(CHANNEL_ID);
1624
+ this.accountBatch(err);
1791
1625
  console.warn(
1792
1626
  `[dsh-chat-translate] reply batch of ${batch.length} failed, retrying per block: ${describeError(err)}`
1793
1627
  );
@@ -1799,7 +1633,7 @@ var TranslationDispatcher = class {
1799
1633
  out.set(key, value);
1800
1634
  }
1801
1635
  } catch (err) {
1802
- this.recordFailure(CHANNEL_ID);
1636
+ this.accountBatch(err);
1803
1637
  console.warn(
1804
1638
  `[dsh-chat-translate] reply block ${piece.block} #${piece.index} failed, keeping the original: ${describeError(err)}`
1805
1639
  );
@@ -1807,6 +1641,14 @@ var TranslationDispatcher = class {
1807
1641
  }
1808
1642
  return out;
1809
1643
  }
1644
+ /** 失败记账的分类:传输故障计数,内容拒收证明通道活着。 */
1645
+ accountBatch(err) {
1646
+ if (err instanceof ContentRejectedError) {
1647
+ this.recordSuccess(CHANNEL_ID);
1648
+ } else {
1649
+ this.recordFailure(CHANNEL_ID);
1650
+ }
1651
+ }
1810
1652
  /** 发出一次正文请求并把结果还原成每个片段的最终译文。 */
1811
1653
  async requestReplyBatch(adapter, batch, config) {
1812
1654
  const timeout = config.aiTimeoutMs || 6e5;
@@ -1831,7 +1673,7 @@ var TranslationDispatcher = class {
1831
1673
  );
1832
1674
  const parts = splitBatchTranslation(answer, format, batch.length);
1833
1675
  if (parts === null) {
1834
- throw new Error("reply block markers did not survive the translation");
1676
+ throw new ContentRejectedError("reply block markers did not survive the translation");
1835
1677
  }
1836
1678
  answers = parts;
1837
1679
  }
@@ -1845,14 +1687,16 @@ var TranslationDispatcher = class {
1845
1687
  throw new Error(`reply block ${piece.block} #${piece.index} came back empty`);
1846
1688
  }
1847
1689
  if (hasBatchResidue(answer)) {
1848
- throw new Error("reply translation left a block marker behind");
1690
+ throw new ContentRejectedError("reply translation left a block marker behind");
1849
1691
  }
1850
- const finalText = piece.mask.unmask(answer);
1851
- const leftovers = findLegacyMaskTokens(finalText).filter(
1852
- (fragment) => !piece.legacy.has(fragment.toLowerCase())
1853
- );
1854
- if (hasMaskResidue(finalText) || leftovers.length > 0) {
1855
- throw new Error("reply translation left a mask placeholder behind");
1692
+ let finalText;
1693
+ try {
1694
+ finalText = piece.mask.unmask(answer);
1695
+ } catch (unmaskErr) {
1696
+ throw new ContentRejectedError(describeError(unmaskErr));
1697
+ }
1698
+ if (hasMaskResidue(finalText)) {
1699
+ throw new ContentRejectedError("reply translation left a mask placeholder behind");
1856
1700
  }
1857
1701
  out.set(replyPieceKey(piece), finalText);
1858
1702
  });
@@ -2017,8 +1861,7 @@ var Config = Schema.object({
2017
1861
  enabled: Schema.boolean().default(DEFAULT_CONFIG.enabled).volatile(),
2018
1862
  aiTimeoutMs: Schema.number().min(AI_TIMEOUT_MIN).max(AI_TIMEOUT_MAX).default(DEFAULT_CONFIG.aiTimeoutMs).volatile(),
2019
1863
  baseUrl: Schema.string().default(DEFAULT_CONFIG.baseUrl).volatile(),
2020
- model: Schema.string().default(DEFAULT_CONFIG.model).volatile(),
2021
- targetLang: Schema.string().default(DEFAULT_CONFIG.targetLang).volatile()
1864
+ model: Schema.string().default(DEFAULT_CONFIG.model).volatile()
2022
1865
  });
2023
1866
  function apply(ctx, config) {
2024
1867
  const credentials = new CredentialsReader(ctx.credentials);
@@ -2036,14 +1879,7 @@ function apply(ctx, config) {
2036
1879
  "dsh-chat-translate: settings page policy"
2037
1880
  );
2038
1881
  });
2039
- const legacyConfigPath = dshHomePath("dsh-chat-translate-config.json");
2040
- const initPromise = Promise.all([
2041
- credentials.init(),
2042
- cache.init(),
2043
- migrateLegacyConfigFile(ctx.settings, legacyConfigPath),
2044
- retireRemovedConfigKeys(ctx.settings),
2045
- retireStoreFiles(["think-cache.json"])
2046
- ]).catch((err) => {
1882
+ const initPromise = Promise.all([credentials.init(), cache.init()]).catch((err) => {
2047
1883
  console.warn("[dsh-chat-translate] Initialization error:", err);
2048
1884
  });
2049
1885
  ctx.on("credentials/reference-updated", (ref) => {