@lynn123411/dsh-chat-translate 1.8.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/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.3/node_modules/@deepseek-ai/cosmokit/lib/index.js
1
+ // node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.5/node_modules/@deepseek-ai/cosmokit/lib/index.js
2
2
  function isNullable(value) {
3
3
  return value === null || value === void 0;
4
4
  }
@@ -17,6 +17,32 @@ function pick(source, keys, forced) {
17
17
  for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
18
18
  return result;
19
19
  }
20
+ var write = Symbol.for("cosmokit.volatile.write");
21
+ function snapshot(value, ancestors = /* @__PURE__ */ new Set()) {
22
+ if (typeof value === "function") throw new TypeError("volatile config cannot contain functions");
23
+ if (value === null || typeof value !== "object") return value;
24
+ if (ancestors.has(value)) throw new TypeError("volatile config cannot contain cycles");
25
+ ancestors.add(value);
26
+ try {
27
+ if (Array.isArray(value)) return Object.freeze(value.map((item) => snapshot(item, ancestors)));
28
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("volatile config objects must be plain objects or arrays");
29
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item, ancestors)])));
30
+ } finally {
31
+ ancestors.delete(value);
32
+ }
33
+ }
34
+ function createVolatile(value) {
35
+ let current = snapshot(value);
36
+ return Object.freeze({
37
+ get: () => current,
38
+ [write]: (value2) => {
39
+ current = value2;
40
+ }
41
+ });
42
+ }
43
+ function isVolatile(value) {
44
+ return typeof value === "object" && value !== null && write in value;
45
+ }
20
46
  function is(type, value) {
21
47
  if (arguments.length === 1) return (value2) => is(type, value2);
22
48
  return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
@@ -95,24 +121,37 @@ function clone(source, refs = /* @__PURE__ */ new Map()) {
95
121
  return result;
96
122
  }
97
123
  function deepEqual(a, b, strict) {
98
- if (a === b) return true;
99
- if (!strict && isNullable(a) && isNullable(b)) return true;
100
- if (typeof a !== typeof b) return false;
101
- if (typeof a !== "object") return false;
102
- if (!a || !b) return false;
103
- function check(test, then) {
104
- return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
105
- }
106
- return check(Array.isArray, (a2, b2) => a2.length === b2.length && a2.every((item, index) => deepEqual(item, b2[index]))) ?? check(is("Date"), (a2, b2) => a2.valueOf() === b2.valueOf()) ?? check(is("RegExp"), (a2, b2) => a2.source === b2.source && a2.flags === b2.flags) ?? check(isArrayBufferLike, (a2, b2) => {
107
- if (a2.byteLength !== b2.byteLength) return false;
108
- const viewA = new Uint8Array(a2);
109
- const viewB = new Uint8Array(b2);
110
- for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
111
- return true;
112
- }) ?? Object.keys({
113
- ...a,
114
- ...b
115
- }).every((key) => deepEqual(a[key], b[key], strict));
124
+ const ancestors = /* @__PURE__ */ new Set();
125
+ function compare(a2, b2) {
126
+ if (a2 === b2) return true;
127
+ if (isVolatile(a2) || isVolatile(b2)) return isVolatile(a2) && isVolatile(b2);
128
+ if (!strict && isNullable(a2) && isNullable(b2)) return true;
129
+ if (typeof a2 !== typeof b2 || typeof a2 !== "object" || !a2 || !b2) return false;
130
+ if (ancestors.has(a2)) return false;
131
+ function check(test, then) {
132
+ return test(a2) ? test(b2) ? then(a2, b2) : false : test(b2) ? false : void 0;
133
+ }
134
+ ancestors.add(a2);
135
+ try {
136
+ return check(Array.isArray, (a3, b3) => {
137
+ if (a3.length !== b3.length) return false;
138
+ for (let index = 0; index < a3.length; index++) if (!compare(a3[index], b3[index])) return false;
139
+ return true;
140
+ }) ?? check(is("Date"), (a3, b3) => a3.valueOf() === b3.valueOf()) ?? check(is("URL"), (a3, b3) => a3.href === b3.href) ?? check(is("RegExp"), (a3, b3) => a3.source === b3.source && a3.flags === b3.flags) ?? check(isArrayBufferLike, (a3, b3) => {
141
+ if (a3.byteLength !== b3.byteLength) return false;
142
+ const viewA = new Uint8Array(a3);
143
+ const viewB = new Uint8Array(b3);
144
+ for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
145
+ return true;
146
+ }) ?? ((!strict || [a2, b2].every((value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) && Object.keys({
147
+ ...a2,
148
+ ...b2
149
+ }).every((key) => compare(a2[key], b2[key])));
150
+ } finally {
151
+ ancestors.delete(a2);
152
+ }
153
+ }
154
+ return compare(a, b);
116
155
  }
117
156
  var Time;
118
157
  (function(Time2) {
@@ -184,7 +223,7 @@ var Time;
184
223
  Time2.template = template;
185
224
  })(Time || (Time = {}));
186
225
 
187
- // node_modules/.pnpm/@deepseek-ai+schemastery@3.18.2/node_modules/@deepseek-ai/schemastery/lib/index.mjs
226
+ // node_modules/.pnpm/@deepseek-ai+schemastery@3.18.4/node_modules/@deepseek-ai/schemastery/lib/index.mjs
188
227
  var kSchema = Symbol.for("schemastery");
189
228
  var kValidationError = Symbol.for("ValidationError");
190
229
  globalThis.__schemastery_index__ ??= 0;
@@ -361,6 +400,7 @@ Schema.prototype.pattern = function pattern(regexp) {
361
400
  return schema;
362
401
  };
363
402
  Schema.prototype.simplify = function simplify(value) {
403
+ if (isVolatile(value)) value = value.get();
364
404
  if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
365
405
  if (isNullable(value)) return value;
366
406
  if (this.type === "object" || this.type === "dict") {
@@ -418,12 +458,49 @@ for (const key of [
418
458
  };
419
459
  return schema;
420
460
  } });
461
+ Schema.prototype.volatile = function volatile() {
462
+ if (this.meta.volatile) throw new TypeError("volatile schema is already wrapped");
463
+ return this.extra("volatile", true);
464
+ };
421
465
  var resolvers = {};
466
+ var checkedVolatile = Symbol("checked-volatile-schema");
467
+ function validateVolatileSchema(schema, path2 = [], blocked = false, seen = /* @__PURE__ */ new Map()) {
468
+ const states = seen.get(schema) ?? /* @__PURE__ */ new Set();
469
+ if (states.has(blocked)) return;
470
+ states.add(blocked);
471
+ seen.set(schema, states);
472
+ if (schema.meta?.volatile && blocked) throw new ValidationError("volatile fields require a fixed object path without an enclosing volatile field", { path: path2 });
473
+ const nested = blocked || !!schema.meta?.volatile;
474
+ if (schema.dict) for (const [key, child] of Object.entries(schema.dict)) validateVolatileSchema(child, [...path2, key], nested, seen);
475
+ if (schema.sKey) validateVolatileSchema(schema.sKey, [...path2, "<key>"], true, seen);
476
+ if (schema.inner && (schema.type !== "lazy" || schema.inner[kSchema])) validateVolatileSchema(schema.inner, [...path2, "*"], true, seen);
477
+ if (schema.list) for (let index = 0; index < schema.list.length; index++) validateVolatileSchema(schema.list[index], [...path2, String(index)], true, seen);
478
+ }
422
479
  Schema.extend = function extend(type, resolve3) {
423
480
  resolvers[type] = resolve3;
424
481
  };
425
482
  Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
426
483
  if (!schema) return [data];
484
+ if (!options[checkedVolatile]) {
485
+ validateVolatileSchema(schema, options.path);
486
+ options = {
487
+ ...options,
488
+ [checkedVolatile]: true
489
+ };
490
+ }
491
+ if (schema.meta?.volatile) {
492
+ const inner = Schema(schema);
493
+ inner.meta = {
494
+ ...schema.meta,
495
+ volatile: false
496
+ };
497
+ const [value, adapted] = Schema.resolve(data, inner, options, strict);
498
+ try {
499
+ return [createVolatile(value), adapted];
500
+ } catch (error) {
501
+ throw new ValidationError(error instanceof Error ? error.message : String(error), options);
502
+ }
503
+ }
427
504
  if (options.ignore?.(data, schema)) return [data];
428
505
  if (isNullable(data) && schema.type !== "lazy") {
429
506
  if (schema.meta.required) throw new ValidationError(`missing required value`, options);
@@ -531,6 +608,7 @@ Schema.extend("lazy", (data, schema, options, strict) => {
531
608
  ...schema.meta,
532
609
  ...schema.inner.meta
533
610
  };
611
+ validateVolatileSchema(schema.inner, options.path, true);
534
612
  }
535
613
  return Schema.resolve(data, schema.inner, options, strict);
536
614
  });
@@ -630,7 +708,7 @@ function property(data, key, schema, options) {
630
708
  } catch (e) {
631
709
  if (!options?.autofix) throw e;
632
710
  delete data[key];
633
- return schema.meta.default;
711
+ return schema.meta.volatile ? createVolatile(schema.meta.default) : schema.meta.default;
634
712
  }
635
713
  }
636
714
  Schema.extend("array", (data, { inner, meta }, options) => {
@@ -786,7 +864,7 @@ defineMethod("transform", [
786
864
  "preserve"
787
865
  ], ({ inner }, isInner) => inner.toString(isInner));
788
866
 
789
- // node_modules/.pnpm/@deepseek-ai+dsh-home-paths@0.1.6-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-home-paths/lib/index.js
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
790
868
  import { homedir } from "node:os";
791
869
  import { basename, dirname, join, resolve as resolve2 } from "node:path";
792
870
  var DSH_HOME_DIR_NAME = ".dsh";
@@ -810,35 +888,49 @@ function dshHomePath(...segments) {
810
888
 
811
889
  // src/server/config.ts
812
890
  import * as fs from "node:fs/promises";
813
- var MAX_CONCURRENCY = 100;
814
891
  var AI_TIMEOUT_MIN = 500;
815
- var AI_TIMEOUT_MAX = 12e4;
816
- var THINK_TIMEOUT_MIN = 500;
817
- var THINK_TIMEOUT_MAX = 9e5;
818
- var THINK_CACHE_ENTRIES = 300;
892
+ var AI_TIMEOUT_MAX = 9e5;
819
893
  var SETTINGS_NAMESPACE = "dsh-chat-translate";
820
894
  var DEFAULT_CONFIG = {
821
895
  enabled: true,
822
- concurrency: 3,
823
- timeoutMs: 2e3,
824
- aiTimeoutMs: 3e4,
825
- thinkTimeoutMs: 6e5,
826
- aiEnabled: true,
827
- bingEnabled: true,
828
- thinkEnabled: true,
896
+ aiTimeoutMs: 6e5,
829
897
  baseUrl: "",
830
898
  model: "",
831
899
  targetLang: "zh-Hans"
832
900
  };
901
+ var RETIRED_CONFIG_KEYS = [
902
+ "channels",
903
+ "concurrency",
904
+ "timeoutMs",
905
+ "thinkTimeoutMs",
906
+ "aiEnabled",
907
+ "bingEnabled",
908
+ "thinkEnabled"
909
+ ];
910
+ function readPluginConfig(refs) {
911
+ return {
912
+ enabled: refs.enabled.get(),
913
+ aiTimeoutMs: refs.aiTimeoutMs.get(),
914
+ baseUrl: refs.baseUrl.get(),
915
+ model: refs.model.get(),
916
+ targetLang: refs.targetLang.get()
917
+ };
918
+ }
919
+ function createLiveConfigSource(onVolatileUpdate, refs) {
920
+ return {
921
+ get: () => readPluginConfig(refs),
922
+ watch: (listener) => onVolatileUpdate(() => listener(readPluginConfig(refs)))
923
+ };
924
+ }
833
925
  var ConfigManager = class {
834
- scope;
926
+ source;
835
927
  credentials;
836
- constructor(scope, credentials) {
837
- this.scope = scope;
928
+ constructor(source, credentials) {
929
+ this.source = source;
838
930
  this.credentials = credentials;
839
931
  }
840
932
  getConfig() {
841
- return this.scope.get();
933
+ return this.source.get();
842
934
  }
843
935
  /** Whether the AI channel has every required piece: baseUrl, model and key. */
844
936
  isAiConfigured() {
@@ -848,47 +940,18 @@ var ConfigManager = class {
848
940
  );
849
941
  }
850
942
  onConfigChange(listener) {
851
- return this.scope.watch(listener);
852
- }
853
- /**
854
- * Merge a partial update into the settings namespace. Values are sanitized
855
- * here (bounds, trimming) so the schema's own constraints act as a second
856
- * line of defence rather than the only one.
857
- *
858
- * @internal Test-only. No production path calls this: the host writes the
859
- * settings namespace from the browser through the DSH settings service, and
860
- * this facade is only driven by `scripts/test-*.mjs` (regression suite).
861
- * Kept (with this marker) so those tests keep exercising the sanitizer.
862
- */
863
- async updateConfig(partial) {
864
- await this.scope.update(sanitizePatch({ ...partial }));
865
- return this.getConfig();
943
+ return this.source.watch(listener);
866
944
  }
867
945
  };
868
946
  function sanitizePatch(input) {
869
947
  const next = {};
870
948
  if (typeof input.enabled === "boolean") next.enabled = input.enabled;
871
- if (typeof input.aiEnabled === "boolean") next.aiEnabled = input.aiEnabled;
872
- if (typeof input.bingEnabled === "boolean") next.bingEnabled = input.bingEnabled;
873
- if (typeof input.thinkEnabled === "boolean") next.thinkEnabled = input.thinkEnabled;
874
- if (typeof input.concurrency === "number" && Number.isFinite(input.concurrency)) {
875
- next.concurrency = Math.min(Math.max(Math.round(input.concurrency), 1), MAX_CONCURRENCY);
876
- }
877
- if (typeof input.timeoutMs === "number" && Number.isFinite(input.timeoutMs)) {
878
- next.timeoutMs = Math.min(Math.max(Math.round(input.timeoutMs), 500), 1e4);
879
- }
880
949
  if (typeof input.aiTimeoutMs === "number" && Number.isFinite(input.aiTimeoutMs)) {
881
950
  next.aiTimeoutMs = Math.min(
882
951
  Math.max(Math.round(input.aiTimeoutMs), AI_TIMEOUT_MIN),
883
952
  AI_TIMEOUT_MAX
884
953
  );
885
954
  }
886
- if (typeof input.thinkTimeoutMs === "number" && Number.isFinite(input.thinkTimeoutMs)) {
887
- next.thinkTimeoutMs = Math.min(
888
- Math.max(Math.round(input.thinkTimeoutMs), THINK_TIMEOUT_MIN),
889
- THINK_TIMEOUT_MAX
890
- );
891
- }
892
955
  if (typeof input.baseUrl === "string") next.baseUrl = input.baseUrl.trim();
893
956
  if (typeof input.model === "string") next.model = input.model.trim();
894
957
  if (typeof input.targetLang === "string" && input.targetLang.trim()) {
@@ -896,6 +959,26 @@ function sanitizePatch(input) {
896
959
  }
897
960
  return next;
898
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
+ }
899
982
  async function migrateLegacyConfigFile(settings, legacyPath) {
900
983
  let raw;
901
984
  try {
@@ -1213,7 +1296,7 @@ var LruDiskCache = class {
1213
1296
  dirty = false;
1214
1297
  /** Legacy root-level cache file (<=1.1); moved under the plugin subdir. */
1215
1298
  legacyPath;
1216
- /** Only the original title cache carries a pre-1.2 root-level predecessor. */
1299
+ /** Only the original cache file carries a pre-1.2 root-level predecessor. */
1217
1300
  hasLegacyPredecessor;
1218
1301
  constructor(maxEntries = 1e3, fileName = "cache.json") {
1219
1302
  this.maxEntries = maxEntries;
@@ -1345,6 +1428,17 @@ var LruDiskCache = class {
1345
1428
  }
1346
1429
  }
1347
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
+ }
1348
1442
 
1349
1443
  // src/describe-error.ts
1350
1444
  function describeError(err) {
@@ -1372,129 +1466,6 @@ function describeError(err) {
1372
1466
  return parts.join(" \u2190 ");
1373
1467
  }
1374
1468
 
1375
- // src/server/adapters/bing.ts
1376
- var TRANSLATOR_URL = "https://cn.bing.com/translator";
1377
- var TRANSLATE_URL = "https://cn.bing.com/ttranslatev3?isVertical=1&&IG={IG}&IID=translator.5025.1";
1378
- 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";
1379
- var IG_RES = [
1380
- /_IG="([a-zA-Z0-9]+)"/,
1381
- /,IG:"([a-zA-Z0-9]+)"/,
1382
- /IG:"([a-zA-Z0-9]+)"/,
1383
- /"IG":"([a-zA-Z0-9]+)"/
1384
- ];
1385
- var ABUSE_RES = [
1386
- /params_AbusePreventionHelper\s*=\s*\[\s*(\d+)\s*,\s*"([^"]+)"/,
1387
- /var\s+params_AbusePreventionHelper\s*=\s*\[\s*(\d+)\s*,\s*"([^"]+)"/
1388
- ];
1389
- var cachedTokens = null;
1390
- var tokensFetchedAt = 0;
1391
- var TOKEN_TTL_MS = 15 * 60 * 1e3;
1392
- var inFlightTokenPromise = null;
1393
- function parseTokens(html) {
1394
- let ig;
1395
- for (const re of IG_RES) {
1396
- const m = re.exec(html);
1397
- if (m && m[1]) {
1398
- ig = m[1];
1399
- break;
1400
- }
1401
- }
1402
- let key;
1403
- let token;
1404
- for (const re of ABUSE_RES) {
1405
- const m = re.exec(html);
1406
- if (m && m[1] && m[2]) {
1407
- key = m[1];
1408
- token = m[2];
1409
- break;
1410
- }
1411
- }
1412
- if (!ig || !key || !token) {
1413
- throw new Error(`Bing translator page: missing tokens (ig: ${!!ig}, key: ${!!key}, token: ${!!token})`);
1414
- }
1415
- return { ig, key, token };
1416
- }
1417
- async function fetchTokens(signal, forceRefresh = false) {
1418
- if (!forceRefresh && cachedTokens && Date.now() - tokensFetchedAt < TOKEN_TTL_MS) {
1419
- return cachedTokens;
1420
- }
1421
- if (inFlightTokenPromise) {
1422
- return inFlightTokenPromise;
1423
- }
1424
- inFlightTokenPromise = (async () => {
1425
- try {
1426
- const response = await fetch(TRANSLATOR_URL, {
1427
- headers: {
1428
- "User-Agent": UA,
1429
- Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
1430
- },
1431
- signal
1432
- });
1433
- if (!response.ok) {
1434
- throw new Error(`Bing translator page responded with status ${response.status}`);
1435
- }
1436
- const html = await response.text();
1437
- const tokens = parseTokens(html);
1438
- cachedTokens = tokens;
1439
- tokensFetchedAt = Date.now();
1440
- return tokens;
1441
- } finally {
1442
- inFlightTokenPromise = null;
1443
- }
1444
- })();
1445
- return inFlightTokenPromise;
1446
- }
1447
- var BingWebAdapter = class {
1448
- id = "bing";
1449
- name = "\u5FAE\u8F6F Bing \u7F51\u9875\u7FFB\u8BD1 (\u514DKey\u76F4\u8FDE)";
1450
- isAvailable(_config) {
1451
- return true;
1452
- }
1453
- async translate(text, signal, config) {
1454
- const targetLang = config.targetLang || "zh-Hans";
1455
- return this.executeTranslate(text, signal, targetLang, false);
1456
- }
1457
- async executeTranslate(text, signal, targetLang, isRetry) {
1458
- const tokens = await fetchTokens(signal, isRetry);
1459
- const body = new URLSearchParams({
1460
- fromLang: "auto-detect",
1461
- text,
1462
- to: targetLang,
1463
- key: tokens.key,
1464
- token: tokens.token,
1465
- tryFetchingGenderDebiasedTranslations: "true"
1466
- });
1467
- const response = await fetch(TRANSLATE_URL.replace("{IG}", tokens.ig), {
1468
- method: "POST",
1469
- headers: {
1470
- "User-Agent": UA,
1471
- Referer: "https://cn.bing.com/translator/",
1472
- Origin: "https://cn.bing.com",
1473
- "Content-Type": "application/x-www-form-urlencoded"
1474
- },
1475
- body,
1476
- signal
1477
- });
1478
- if (!response.ok) {
1479
- cachedTokens = null;
1480
- if (!isRetry && (response.status === 400 || response.status === 401 || response.status === 403)) {
1481
- return this.executeTranslate(text, signal, targetLang, true);
1482
- }
1483
- throw new Error(`Bing translate responded with status ${response.status}`);
1484
- }
1485
- const json = await response.json();
1486
- const translated = json?.[0]?.translations?.[0]?.text?.trim();
1487
- if (!translated) {
1488
- cachedTokens = null;
1489
- if (!isRetry) {
1490
- return this.executeTranslate(text, signal, targetLang, true);
1491
- }
1492
- throw new Error("Bing translate returned an empty result");
1493
- }
1494
- return translated;
1495
- }
1496
- };
1497
-
1498
1469
  // src/server/adapters/openai.ts
1499
1470
  var LANG_HINTS = {
1500
1471
  "zh-hans": "Simplified Chinese",
@@ -1513,7 +1484,7 @@ var LANG_HINTS = {
1513
1484
  it: "Italian"
1514
1485
  };
1515
1486
  function buildSystemPrompt(langName, mode) {
1516
- 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.`;
1517
1488
  if (mode === "plain") return base;
1518
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.`;
1519
1490
  }
@@ -1525,9 +1496,7 @@ var OpenAiCompatibleAdapter = class {
1525
1496
  this.credentials = credentials;
1526
1497
  }
1527
1498
  isAvailable(config) {
1528
- return Boolean(
1529
- config.aiEnabled && config.baseUrl?.trim() && config.model?.trim() && this.credentials.getApiKey()
1530
- );
1499
+ return Boolean(config.baseUrl?.trim() && config.model?.trim() && this.credentials.getApiKey());
1531
1500
  }
1532
1501
  async translate(text, signal, config, options) {
1533
1502
  const apiKey = this.credentials.getApiKey();
@@ -1576,9 +1545,9 @@ var OpenAiCompatibleAdapter = class {
1576
1545
  }
1577
1546
  };
1578
1547
 
1579
- // src/server/pipeline/think.ts
1580
- var THINK_MAX_INPUT_TOKENS = 4096;
1581
- var THINK_MAX_OUTPUT_TOKENS = 8192;
1548
+ // src/server/pipeline/blocks.ts
1549
+ var REPLY_MAX_INPUT_TOKENS = 4096;
1550
+ var REPLY_MAX_OUTPUT_TOKENS = 8192;
1582
1551
  var CJK_CHAR = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/;
1583
1552
  function estimateTokens(text) {
1584
1553
  let cjk = 0;
@@ -1619,7 +1588,7 @@ function hardSlice(text, maxTokens) {
1619
1588
  if (current !== "") out.push(current);
1620
1589
  return out;
1621
1590
  }
1622
- function splitOversizedBlock(text, maxTokens = THINK_MAX_INPUT_TOKENS) {
1591
+ function splitOversizedBlock(text, maxTokens = REPLY_MAX_INPUT_TOKENS) {
1623
1592
  if (estimateTokens(text) <= maxTokens) return [text];
1624
1593
  let chunks = splitKeepingSeparator(text, /(\n{2,})/);
1625
1594
  if (chunks.length <= 1) chunks = splitKeepingSeparator(text, /(\n)/);
@@ -1644,7 +1613,7 @@ function splitOversizedBlock(text, maxTokens = THINK_MAX_INPUT_TOKENS) {
1644
1613
  if (current !== "") out.push(current);
1645
1614
  return out;
1646
1615
  }
1647
- function packPieces(pieces, maxTokens = THINK_MAX_INPUT_TOKENS) {
1616
+ function packPieces(pieces, maxTokens = REPLY_MAX_INPUT_TOKENS) {
1648
1617
  const batches = [];
1649
1618
  let current = [];
1650
1619
  let tokens = 0;
@@ -1661,7 +1630,7 @@ function packPieces(pieces, maxTokens = THINK_MAX_INPUT_TOKENS) {
1661
1630
  if (current.length > 0) batches.push(current);
1662
1631
  return batches;
1663
1632
  }
1664
- function createThinkBatchFormat() {
1633
+ function createBatchFormat() {
1665
1634
  const id = randomTokenId();
1666
1635
  return {
1667
1636
  id,
@@ -1695,170 +1664,44 @@ function splitBatchTranslation(translated, format, count) {
1695
1664
  }
1696
1665
  return parts;
1697
1666
  }
1698
- function hasThinkBatchResidue(text) {
1667
+ function hasBatchResidue(text) {
1699
1668
  return new RegExp(BATCH_TOKEN_PATTERN_SOURCE, "u").test(text);
1700
1669
  }
1701
1670
 
1702
1671
  // src/server/dispatcher.ts
1703
- function thinkPieceKey(piece) {
1672
+ var CHANNEL_ID = "openai";
1673
+ function replyPieceKey(piece) {
1704
1674
  return `${piece.block}:${piece.index}`;
1705
1675
  }
1706
1676
  var TranslationDispatcher = class {
1707
1677
  configManager;
1708
1678
  cache;
1709
- /** 思考正文译文的独立缓存池,与工具标题的池互不挤占。 */
1710
- thinkCache;
1711
1679
  credentials;
1712
1680
  masking = new ContentMaskingPipeline();
1713
1681
  adapters = /* @__PURE__ */ new Map();
1714
1682
  circuitStates = /* @__PURE__ */ new Map();
1715
- inFlightMap = /* @__PURE__ */ new Map();
1716
- activeCount = 0;
1717
- queue = [];
1718
- /** 思考链请求的串行队列尾,保证同时最多一个在途请求。 */
1719
- thinkTail = Promise.resolve();
1720
- constructor(configManager, cache, credentials, thinkCache = new LruDiskCache(THINK_CACHE_ENTRIES, "think-cache.json")) {
1683
+ /** 正文请求的串行队列尾,保证同时最多一个在途请求。 */
1684
+ replyTail = Promise.resolve();
1685
+ constructor(configManager, cache, credentials) {
1721
1686
  this.configManager = configManager;
1722
1687
  this.cache = cache;
1723
- this.thinkCache = thinkCache;
1724
1688
  this.credentials = credentials ?? { getApiKey: () => "" };
1725
1689
  this.registerAdapter(new OpenAiCompatibleAdapter(this.credentials));
1726
- this.registerAdapter(new BingWebAdapter());
1727
- this.configManager.onConfigChange(() => {
1728
- this.processNext();
1729
- });
1730
1690
  }
1731
1691
  registerAdapter(adapter) {
1732
1692
  this.adapters.set(adapter.id, adapter);
1733
1693
  }
1734
1694
  /**
1735
- * Decide which channels are active for the current config, in priority order.
1695
+ * 翻译回答正文的块:本次唯一的翻译入口。
1736
1696
  *
1737
- * Truth table (user contract):
1738
- * - AI on + configured + Bing on -> [openai, bing] (AI first, Bing fallback)
1739
- * - AI on + NOT configured + Bing on -> [bing]
1740
- * - AI on + NOT configured + Bing off -> [] (no translation)
1741
- * - AI off + Bing on -> [bing]
1742
- * - AI off + Bing off -> [] (no translation)
1743
- */
1744
- computeChannels(config) {
1745
- const channels = [];
1746
- for (const [id, adapter] of this.adapters) {
1747
- if (id === "openai") {
1748
- if (config.aiEnabled && config.baseUrl?.trim() && config.model?.trim() && this.credentials.getApiKey()) {
1749
- channels.push(id);
1750
- }
1751
- continue;
1752
- }
1753
- if (id === "bing") {
1754
- if (config.bingEnabled) channels.push(id);
1755
- continue;
1756
- }
1757
- if (adapter.isAvailable(config)) channels.push(id);
1758
- }
1759
- return channels;
1760
- }
1761
- async translateBatch(texts, forceRefresh = false) {
1762
- return Promise.all(texts.map((t) => this.translateOne(t, forceRefresh)));
1763
- }
1764
- async translateOne(rawText, forceRefresh = false) {
1765
- const text = rawText.trim();
1766
- if (!text) {
1767
- return { original: rawText, translated: rawText, channel: "none", cached: true };
1768
- }
1769
- const config = this.configManager.getConfig();
1770
- if (!config.enabled) {
1771
- return { original: rawText, translated: rawText, channel: "disabled", cached: true };
1772
- }
1773
- const cacheKey = text.toLowerCase();
1774
- if (!forceRefresh) {
1775
- const cached = this.cache.get(cacheKey);
1776
- if (cached) {
1777
- return { original: rawText, translated: cached, channel: "cache", cached: true };
1778
- }
1779
- }
1780
- if (!forceRefresh) {
1781
- const inFlight = this.inFlightMap.get(cacheKey);
1782
- if (inFlight) {
1783
- return inFlight;
1784
- }
1785
- }
1786
- const { maskedText, unmask, legacyFragments } = this.masking.mask(text);
1787
- const documentedLegacy = new Set(legacyFragments.map((fragment) => fragment.toLowerCase()));
1788
- const taskPromise = this.enqueueTask(async () => {
1789
- const currentConfig = this.configManager.getConfig();
1790
- const channels = this.computeChannels(currentConfig);
1791
- for (const chId of channels) {
1792
- const adapter = this.adapters.get(chId);
1793
- if (!adapter || !adapter.isAvailable(currentConfig) || this.isCircuitOpen(chId)) {
1794
- continue;
1795
- }
1796
- try {
1797
- const timeout = chId === "openai" ? currentConfig.aiTimeoutMs || 3e4 : currentConfig.timeoutMs || 2e3;
1798
- const abortCtrl = new AbortController();
1799
- const timer = setTimeout(() => abortCtrl.abort(), timeout);
1800
- let translatedMasked = "";
1801
- try {
1802
- translatedMasked = await adapter.translate(maskedText, abortCtrl.signal, currentConfig);
1803
- } finally {
1804
- clearTimeout(timer);
1805
- }
1806
- const cleaned = translatedMasked?.trim();
1807
- if (cleaned && cleaned.length > 0) {
1808
- const finalTranslated = unmask(cleaned);
1809
- const legacyLeftovers = findLegacyMaskTokens(finalTranslated).filter(
1810
- (fragment) => !documentedLegacy.has(fragment.toLowerCase())
1811
- );
1812
- if (hasMaskResidue(finalTranslated) || legacyLeftovers.length > 0) {
1813
- this.recordFailure(chId);
1814
- console.warn(
1815
- `[dsh-chat-translate] channel ${chId} left a mask placeholder in the translation, discarding its result | text: ${text.slice(0, 60)}`
1816
- );
1817
- continue;
1818
- }
1819
- this.recordSuccess(chId);
1820
- this.cache.set(cacheKey, finalTranslated);
1821
- return {
1822
- original: rawText,
1823
- translated: finalTranslated,
1824
- channel: chId,
1825
- cached: false
1826
- };
1827
- }
1828
- this.recordFailure(chId);
1829
- console.warn(
1830
- `[dsh-chat-translate] channel ${chId} returned an empty translation | text: ${text.slice(0, 60)}`
1831
- );
1832
- } catch (err) {
1833
- this.recordFailure(chId);
1834
- const detail = err instanceof MaskRestoreError ? ` (protected fragments expected ${err.expectedCount}, restored ${err.foundCount})` : "";
1835
- console.warn(
1836
- `[dsh-chat-translate] channel ${chId} failed: ${describeError(err)}${detail} | text: ${text.slice(0, 60)}`
1837
- );
1838
- }
1839
- }
1840
- return { original: rawText, translated: rawText, channel: "fallback", cached: false };
1841
- });
1842
- if (!forceRefresh) {
1843
- this.inFlightMap.set(cacheKey, taskPromise);
1844
- }
1845
- try {
1846
- return await taskPromise;
1847
- } finally {
1848
- if (!forceRefresh) {
1849
- this.inFlightMap.delete(cacheKey);
1850
- }
1851
- }
1852
- }
1853
- /**
1854
- * 翻译思考正文的块。整条链路与工具标题翻译分离:只走 AI 通道、独立串行
1855
- * 队列、独立超时、独立缓存池,Bing 从不参与。
1697
+ * 每个块先按输入上限切成片段,再各自掩码、相邻片段打包成一个请求;整批失败
1698
+ * 时退回逐片段单发。调用方传进来的每个块要么整块译出、要么整块保持原文:
1699
+ * 任一片段缺失(含掩码还原不通过)都让该块作废,避免半中半英的段落。
1856
1700
  *
1857
- * 每个块先按输入上限切成片段,再各自掩码、相邻片段打包成一个请求;整批
1858
- * 失败时退回逐片段单发,仍失败的片段保留原文。块内任一片段失败即整块不译,
1859
- * 避免半中半英的段落。
1701
+ * 客户端按 2048 估算 token 分块,所以一个 markdown 块可能跨多次调用;每次
1702
+ * 调用都独立决定成败,不会出现「前一段已挂译文、后一段失败」的半截结果。
1860
1703
  */
1861
- async translateThinkBlocks(blocks) {
1704
+ async translateReplyBlocks(blocks) {
1862
1705
  const results = blocks.map((original) => ({
1863
1706
  original,
1864
1707
  translated: original,
@@ -1867,16 +1710,15 @@ var TranslationDispatcher = class {
1867
1710
  channel: "none"
1868
1711
  }));
1869
1712
  const config = this.configManager.getConfig();
1870
- if (!config.enabled || !config.thinkEnabled) return results;
1871
- const adapter = this.adapters.get("openai");
1872
- if (!adapter || !adapter.isAvailable(config) || this.isChannelCoolingDown("openai")) {
1873
- return results;
1874
- }
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;
1875
1717
  const pieces = [];
1876
1718
  for (let block = 0; block < blocks.length; block++) {
1877
1719
  const text = (blocks[block] ?? "").trim();
1878
1720
  if (!text) continue;
1879
- const cached = this.thinkCache.get(text.toLowerCase());
1721
+ const cached = this.cache.get(text.toLowerCase());
1880
1722
  if (cached) {
1881
1723
  results[block] = {
1882
1724
  original: blocks[block],
@@ -1900,8 +1742,8 @@ var TranslationDispatcher = class {
1900
1742
  }
1901
1743
  const translated = /* @__PURE__ */ new Map();
1902
1744
  for (const batch of packPieces(pieces)) {
1903
- const outcome = await this.runThinkSerial(
1904
- () => this.translateThinkBatch(adapter, batch, config)
1745
+ const outcome = await this.runReplySerial(
1746
+ () => this.translateReplyBatch(adapter, batch, config)
1905
1747
  );
1906
1748
  for (const [key, value] of outcome) translated.set(key, value);
1907
1749
  }
@@ -1921,40 +1763,53 @@ var TranslationDispatcher = class {
1921
1763
  if (!complete) continue;
1922
1764
  const original = blocks[block];
1923
1765
  const finalText = parts.join("");
1924
- this.thinkCache.set(original.trim().toLowerCase(), finalText);
1925
- results[block] = { original, translated: finalText, ok: true, cached: false, channel: "openai" };
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
+ };
1926
1774
  }
1927
1775
  return results;
1928
1776
  }
1929
1777
  /**
1930
1778
  * 一整批一次请求;失败则该批逐片段单发重试一次,仍失败的片段直接放弃
1931
- * (保留原文),不在界面上留提示。
1779
+ * (保留原文),不在界面上留提示。失败的片段不进缓存,因此下一次滚回视口
1780
+ * 时会自然重试。
1781
+ *
1782
+ * 熔断记账按「批」算:一次批请求失败记一次,逐片段重试失败再各记一次。
1932
1783
  */
1933
- async translateThinkBatch(adapter, batch, config) {
1784
+ async translateReplyBatch(adapter, batch, config) {
1934
1785
  try {
1935
- return await this.requestThinkBatch(adapter, batch, config);
1786
+ const outcome = await this.requestReplyBatch(adapter, batch, config);
1787
+ this.recordSuccess(CHANNEL_ID);
1788
+ return outcome;
1936
1789
  } catch (err) {
1790
+ this.recordFailure(CHANNEL_ID);
1937
1791
  console.warn(
1938
- `[dsh-chat-translate] think batch of ${batch.length} failed, retrying per block: ${describeError(err)}`
1792
+ `[dsh-chat-translate] reply batch of ${batch.length} failed, retrying per block: ${describeError(err)}`
1939
1793
  );
1940
1794
  }
1941
1795
  const out = /* @__PURE__ */ new Map();
1942
1796
  for (const piece of batch) {
1943
1797
  try {
1944
- for (const [key, value] of await this.requestThinkBatch(adapter, [piece], config)) {
1798
+ for (const [key, value] of await this.requestReplyBatch(adapter, [piece], config)) {
1945
1799
  out.set(key, value);
1946
1800
  }
1947
1801
  } catch (err) {
1802
+ this.recordFailure(CHANNEL_ID);
1948
1803
  console.warn(
1949
- `[dsh-chat-translate] think block ${piece.block} #${piece.index} failed, keeping the original: ${describeError(err)}`
1804
+ `[dsh-chat-translate] reply block ${piece.block} #${piece.index} failed, keeping the original: ${describeError(err)}`
1950
1805
  );
1951
1806
  }
1952
1807
  }
1953
1808
  return out;
1954
1809
  }
1955
- /** 发出一次思考链请求并把结果还原成每个片段的最终译文。 */
1956
- async requestThinkBatch(adapter, batch, config) {
1957
- const timeout = config.thinkTimeoutMs || 6e5;
1810
+ /** 发出一次正文请求并把结果还原成每个片段的最终译文。 */
1811
+ async requestReplyBatch(adapter, batch, config) {
1812
+ const timeout = config.aiTimeoutMs || 6e5;
1958
1813
  const abortCtrl = new AbortController();
1959
1814
  const timer = setTimeout(() => abortCtrl.abort(), timeout);
1960
1815
  let answers;
@@ -1962,21 +1817,21 @@ var TranslationDispatcher = class {
1962
1817
  if (batch.length === 1) {
1963
1818
  answers = [
1964
1819
  await adapter.translate(batch[0].text, abortCtrl.signal, config, {
1965
- maxTokens: THINK_MAX_OUTPUT_TOKENS,
1820
+ maxTokens: REPLY_MAX_OUTPUT_TOKENS,
1966
1821
  mode: "plain"
1967
1822
  })
1968
1823
  ];
1969
1824
  } else {
1970
- const format = createThinkBatchFormat();
1825
+ const format = createBatchFormat();
1971
1826
  const answer = await adapter.translate(
1972
1827
  buildBatchPayload(batch.map((piece) => piece.text), format),
1973
1828
  abortCtrl.signal,
1974
1829
  config,
1975
- { maxTokens: THINK_MAX_OUTPUT_TOKENS, mode: "blocks" }
1830
+ { maxTokens: REPLY_MAX_OUTPUT_TOKENS, mode: "blocks" }
1976
1831
  );
1977
1832
  const parts = splitBatchTranslation(answer, format, batch.length);
1978
1833
  if (parts === null) {
1979
- throw new Error("think block markers did not survive the translation");
1834
+ throw new Error("reply block markers did not survive the translation");
1980
1835
  }
1981
1836
  answers = parts;
1982
1837
  }
@@ -1987,39 +1842,38 @@ var TranslationDispatcher = class {
1987
1842
  batch.forEach((piece, index) => {
1988
1843
  const answer = (answers[index] ?? "").trim();
1989
1844
  if (!answer) {
1990
- throw new Error(`think block ${piece.block} #${piece.index} came back empty`);
1845
+ throw new Error(`reply block ${piece.block} #${piece.index} came back empty`);
1991
1846
  }
1992
- if (hasThinkBatchResidue(answer)) {
1993
- throw new Error("think translation left a block marker behind");
1847
+ if (hasBatchResidue(answer)) {
1848
+ throw new Error("reply translation left a block marker behind");
1994
1849
  }
1995
1850
  const finalText = piece.mask.unmask(answer);
1996
1851
  const leftovers = findLegacyMaskTokens(finalText).filter(
1997
1852
  (fragment) => !piece.legacy.has(fragment.toLowerCase())
1998
1853
  );
1999
1854
  if (hasMaskResidue(finalText) || leftovers.length > 0) {
2000
- throw new Error("think translation left a mask placeholder behind");
1855
+ throw new Error("reply translation left a mask placeholder behind");
2001
1856
  }
2002
- out.set(thinkPieceKey(piece), finalText);
1857
+ out.set(replyPieceKey(piece), finalText);
2003
1858
  });
2004
1859
  return out;
2005
1860
  }
2006
- /** 串行执行器:同时最多一个在途请求,与工具标题的并发池完全独立。 */
2007
- runThinkSerial(task) {
2008
- const next = this.thinkTail.then(task, task);
2009
- this.thinkTail = next.then(
1861
+ /** 串行执行器:同时最多一个在途请求。 */
1862
+ runReplySerial(task) {
1863
+ const next = this.replyTail.then(task, task);
1864
+ this.replyTail = next.then(
2010
1865
  () => void 0,
2011
1866
  () => void 0
2012
1867
  );
2013
1868
  return next;
2014
1869
  }
2015
1870
  /**
2016
- * 只读判断某通道是否还在熔断冷却期。思考链翻译只借用这个判断避开已经
2017
- * 出问题的服务,不改动熔断状态本身(那是工具标题通道的记账)。
1871
+ * 通道探针:设置面板的「测试 AI 通道」按钮走这里。探测文本用一句英文正文,
1872
+ * 与真实请求同形。
1873
+ *
1874
+ * 探针是用户主动发起的连通性检查,因此不写也不读熔断账本:连点几次测试
1875
+ * 不该把正文翻译挡在冷却期外,正文的失败也不该让按钮变哑。
2018
1876
  */
2019
- isChannelCoolingDown(channelId) {
2020
- const state = this.circuitStates.get(channelId);
2021
- return state !== void 0 && state.state === "open" && Date.now() < state.openUntil;
2022
- }
2023
1877
  async testChannel(channelId) {
2024
1878
  const adapter = this.adapters.get(channelId);
2025
1879
  const config = this.configManager.getConfig();
@@ -2029,10 +1883,10 @@ var TranslationDispatcher = class {
2029
1883
  if (!adapter.isAvailable(config)) {
2030
1884
  return { ok: false, latencyMs: 0, error: `Channel ${channelId} is not configured or disabled` };
2031
1885
  }
2032
- const testText = "List files in current directory";
1886
+ const testText = "Here is what I changed.";
2033
1887
  const start = Date.now();
2034
1888
  try {
2035
- const timeout = channelId === "openai" ? Math.min(config.aiTimeoutMs || 3e4, 3e4) : 4e3;
1889
+ const timeout = Math.min(config.aiTimeoutMs || 3e4, 3e4);
2036
1890
  const abortCtrl = new AbortController();
2037
1891
  const timer = setTimeout(() => abortCtrl.abort(), timeout);
2038
1892
  let res = "";
@@ -2050,43 +1904,6 @@ var TranslationDispatcher = class {
2050
1904
  return { ok: false, latencyMs: Date.now() - start, error: describeError(err) };
2051
1905
  }
2052
1906
  }
2053
- enqueueTask(task) {
2054
- return new Promise((resolve3, reject) => {
2055
- const exec = async () => {
2056
- this.activeCount++;
2057
- try {
2058
- const result = await task();
2059
- resolve3(result);
2060
- } catch (err) {
2061
- reject(err);
2062
- } finally {
2063
- this.activeCount--;
2064
- this.processNext();
2065
- }
2066
- };
2067
- const maxConcurrency = Math.min(
2068
- Math.max(this.configManager.getConfig().concurrency || 3, 1),
2069
- MAX_CONCURRENCY
2070
- );
2071
- if (this.activeCount < maxConcurrency) {
2072
- exec();
2073
- } else {
2074
- this.queue.push(exec);
2075
- }
2076
- });
2077
- }
2078
- processNext() {
2079
- const maxConcurrency = Math.min(
2080
- Math.max(this.configManager.getConfig().concurrency || 3, 1),
2081
- MAX_CONCURRENCY
2082
- );
2083
- while (this.queue.length > 0 && this.activeCount < maxConcurrency) {
2084
- const next = this.queue.shift();
2085
- if (next) {
2086
- next();
2087
- }
2088
- }
2089
- }
2090
1907
  isCircuitOpen(channelId) {
2091
1908
  let state = this.circuitStates.get(channelId);
2092
1909
  if (!state) return false;
@@ -2136,9 +1953,8 @@ var TranslationDispatcher = class {
2136
1953
  };
2137
1954
 
2138
1955
  // src/server/router.ts
2139
- var TRANSLATE_ROUTE_PATH = "/api/dsh-chat-translate/translate";
1956
+ var REPLY_ROUTE_PATH = "/api/dsh-chat-translate/translate-reply";
2140
1957
  var TEST_CHANNEL_ROUTE_PATH = "/api/dsh-chat-translate/test-channel";
2141
- var THINK_ROUTE_PATH = "/api/dsh-chat-translate/translate-think";
2142
1958
  function sendJson(status, body) {
2143
1959
  return new Response(JSON.stringify(body), {
2144
1960
  status,
@@ -2153,36 +1969,8 @@ async function readJson(request) {
2153
1969
  }
2154
1970
  }
2155
1971
  function createFetchRoutes(dispatcher, ready = Promise.resolve()) {
2156
- const translate = {
2157
- path: TRANSLATE_ROUTE_PATH,
2158
- methods: ["POST"],
2159
- requestBody: "buffered",
2160
- fetch: async (request) => {
2161
- await ready;
2162
- try {
2163
- const body = await readJson(request);
2164
- if (!body.ok) return body.response;
2165
- const parsed = body.value;
2166
- const rawTexts = parsed.texts !== void 0 ? parsed.texts : parsed.text;
2167
- let texts = [];
2168
- if (Array.isArray(rawTexts)) {
2169
- texts = rawTexts.filter((t) => typeof t === "string");
2170
- } else if (typeof rawTexts === "string") {
2171
- texts = [rawTexts];
2172
- }
2173
- const forceRefresh = Boolean(parsed.forceRefresh);
2174
- if (texts.length === 0) {
2175
- return sendJson(200, { ok: true, results: [] });
2176
- }
2177
- const results = await dispatcher.translateBatch(texts, forceRefresh);
2178
- return sendJson(200, { ok: true, results });
2179
- } catch (err) {
2180
- return sendJson(500, { ok: false, error: describeError(err) });
2181
- }
2182
- }
2183
- };
2184
- const think = {
2185
- path: THINK_ROUTE_PATH,
1972
+ const reply = {
1973
+ path: REPLY_ROUTE_PATH,
2186
1974
  methods: ["POST"],
2187
1975
  requestBody: "buffered",
2188
1976
  fetch: async (request) => {
@@ -2195,7 +1983,7 @@ function createFetchRoutes(dispatcher, ready = Promise.resolve()) {
2195
1983
  if (blocks.length === 0) {
2196
1984
  return sendJson(200, { ok: true, results: [] });
2197
1985
  }
2198
- const results = await dispatcher.translateThinkBlocks(blocks);
1986
+ const results = await dispatcher.translateReplyBlocks(blocks);
2199
1987
  return sendJson(200, { ok: true, results });
2200
1988
  } catch (err) {
2201
1989
  return sendJson(500, { ok: false, error: describeError(err) });
@@ -2219,37 +2007,42 @@ function createFetchRoutes(dispatcher, ready = Promise.resolve()) {
2219
2007
  }
2220
2008
  }
2221
2009
  };
2222
- return [translate, think, testChannel];
2010
+ return [reply, testChannel];
2223
2011
  }
2224
2012
 
2225
2013
  // src/index.ts
2226
2014
  var name = "dsh-chat-translate";
2227
2015
  var inject = ["settings", "credentials"];
2228
- var CONFIG_SCHEMA = Schema.object({
2229
- enabled: Schema.boolean().default(DEFAULT_CONFIG.enabled),
2230
- concurrency: Schema.number().min(1).max(MAX_CONCURRENCY).default(DEFAULT_CONFIG.concurrency),
2231
- timeoutMs: Schema.number().min(500).max(1e4).default(DEFAULT_CONFIG.timeoutMs),
2232
- aiTimeoutMs: Schema.number().min(AI_TIMEOUT_MIN).max(AI_TIMEOUT_MAX).default(DEFAULT_CONFIG.aiTimeoutMs),
2233
- thinkTimeoutMs: Schema.number().min(THINK_TIMEOUT_MIN).max(THINK_TIMEOUT_MAX).default(DEFAULT_CONFIG.thinkTimeoutMs),
2234
- aiEnabled: Schema.boolean().default(DEFAULT_CONFIG.aiEnabled),
2235
- bingEnabled: Schema.boolean().default(DEFAULT_CONFIG.bingEnabled),
2236
- thinkEnabled: Schema.boolean().default(DEFAULT_CONFIG.thinkEnabled),
2237
- baseUrl: Schema.string().default(DEFAULT_CONFIG.baseUrl),
2238
- model: Schema.string().default(DEFAULT_CONFIG.model),
2239
- targetLang: Schema.string().default(DEFAULT_CONFIG.targetLang)
2016
+ var Config = Schema.object({
2017
+ enabled: Schema.boolean().default(DEFAULT_CONFIG.enabled).volatile(),
2018
+ aiTimeoutMs: Schema.number().min(AI_TIMEOUT_MIN).max(AI_TIMEOUT_MAX).default(DEFAULT_CONFIG.aiTimeoutMs).volatile(),
2019
+ 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()
2240
2022
  });
2241
- function apply(ctx) {
2023
+ function apply(ctx, config) {
2242
2024
  const credentials = new CredentialsReader(ctx.credentials);
2243
- const configManager = new ConfigManager(ctx.settings.register(SETTINGS_NAMESPACE, CONFIG_SCHEMA), credentials);
2025
+ const configManager = new ConfigManager(
2026
+ // DSH commits an accepted live edit into the Config refs and notifies this
2027
+ // plugin's own fiber through `loader/volatile-update`.
2028
+ createLiveConfigSource((listener) => ctx.on("loader/volatile-update", listener), config),
2029
+ credentials
2030
+ );
2244
2031
  const cache = new LruDiskCache(1e3);
2245
- const thinkCache = new LruDiskCache(THINK_CACHE_ENTRIES, "think-cache.json");
2246
- const dispatcher = new TranslationDispatcher(configManager, cache, credentials, thinkCache);
2032
+ const dispatcher = new TranslationDispatcher(configManager, cache, credentials);
2033
+ ctx.inject(["settings"], (child) => {
2034
+ child.effect(
2035
+ () => child.settings.configure({ auto: false }, child.fiber),
2036
+ "dsh-chat-translate: settings page policy"
2037
+ );
2038
+ });
2247
2039
  const legacyConfigPath = dshHomePath("dsh-chat-translate-config.json");
2248
2040
  const initPromise = Promise.all([
2249
2041
  credentials.init(),
2250
2042
  cache.init(),
2251
- thinkCache.init(),
2252
- migrateLegacyConfigFile(ctx.settings, legacyConfigPath)
2043
+ migrateLegacyConfigFile(ctx.settings, legacyConfigPath),
2044
+ retireRemovedConfigKeys(ctx.settings),
2045
+ retireStoreFiles(["think-cache.json"])
2253
2046
  ]).catch((err) => {
2254
2047
  console.warn("[dsh-chat-translate] Initialization error:", err);
2255
2048
  });
@@ -2263,13 +2056,14 @@ function apply(ctx) {
2263
2056
  (route) => connectionCtx.connection.fetch.register(route)
2264
2057
  );
2265
2058
  return () => {
2266
- void Promise.all(disposers.map((dispose) => dispose())).then(() => Promise.all([cache.dispose(), thinkCache.dispose()])).catch((err) => {
2059
+ void Promise.all(disposers.map((dispose) => dispose())).then(() => cache.dispose()).catch((err) => {
2267
2060
  console.warn("[dsh-chat-translate] Dispose translation routes error:", err);
2268
2061
  });
2269
2062
  };
2270
2063
  });
2271
2064
  }
2272
2065
  export {
2066
+ Config,
2273
2067
  apply,
2274
2068
  inject,
2275
2069
  name