@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/README.md +13 -14
- package/cordis.patch.yml +2 -1
- package/docs//351/205/215/347/275/256.md +22 -20
- package/lib/client.js +429 -1065
- package/lib/client.js.map +4 -4
- package/lib/index.js +265 -471
- package/lib/index.js.map +4 -4
- package/lib/types/client/index.d.ts +12 -11
- package/lib/types/client/locales.d.ts +8 -20
- package/lib/types/client/reading-settings-page.d.ts +7 -0
- package/lib/types/client/settings/store.d.ts +40 -32
- package/lib/types/client/settings/ui.d.ts +5 -4
- package/lib/types/client/translate/api.d.ts +5 -16
- package/lib/types/client/translate/client-cache.d.ts +1 -3
- package/lib/types/client/translate/lazy.d.ts +9 -7
- package/lib/types/client/translate/mount.d.ts +2 -6
- package/lib/types/client/translate/observer.d.ts +24 -24
- package/lib/types/client/translate/reply.d.ts +38 -0
- package/lib/types/index.d.ts +41 -12
- package/lib/types/server/cache.d.ts +13 -1
- package/lib/types/server/config.d.ts +81 -40
- package/lib/types/server/dispatcher.d.ts +25 -38
- package/lib/types/server/pipeline/{think.d.ts → blocks.d.ts} +9 -9
- package/lib/types/server/router.d.ts +6 -8
- package/lib/types/server/types.d.ts +2 -8
- package/package.json +14 -14
- package/lib/types/client/translate/think-button.d.ts +0 -19
- package/lib/types/client/translate/think.d.ts +0 -55
- package/lib/types/client/translate/viewport-observer.d.ts +0 -43
- package/lib/types/server/adapters/bing.d.ts +0 -15
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
926
|
+
source;
|
|
835
927
|
credentials;
|
|
836
|
-
constructor(
|
|
837
|
-
this.
|
|
928
|
+
constructor(source, credentials) {
|
|
929
|
+
this.source = source;
|
|
838
930
|
this.credentials = credentials;
|
|
839
931
|
}
|
|
840
932
|
getConfig() {
|
|
841
|
-
return this.
|
|
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.
|
|
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
|
|
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/
|
|
1580
|
-
var
|
|
1581
|
-
var
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
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
|
-
*
|
|
1695
|
+
* 翻译回答正文的块:本次唯一的翻译入口。
|
|
1736
1696
|
*
|
|
1737
|
-
*
|
|
1738
|
-
*
|
|
1739
|
-
*
|
|
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
|
|
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
|
|
1871
|
-
const adapter = this.adapters.get(
|
|
1872
|
-
if (!adapter || !adapter.isAvailable(config)
|
|
1873
|
-
|
|
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.
|
|
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.
|
|
1904
|
-
() => this.
|
|
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.
|
|
1925
|
-
results[block] = {
|
|
1766
|
+
this.cache.set(original.trim().toLowerCase(), finalText);
|
|
1767
|
+
results[block] = {
|
|
1768
|
+
original,
|
|
1769
|
+
translated: finalText,
|
|
1770
|
+
ok: true,
|
|
1771
|
+
cached: false,
|
|
1772
|
+
channel: CHANNEL_ID
|
|
1773
|
+
};
|
|
1926
1774
|
}
|
|
1927
1775
|
return results;
|
|
1928
1776
|
}
|
|
1929
1777
|
/**
|
|
1930
1778
|
* 一整批一次请求;失败则该批逐片段单发重试一次,仍失败的片段直接放弃
|
|
1931
|
-
*
|
|
1779
|
+
* (保留原文),不在界面上留提示。失败的片段不进缓存,因此下一次滚回视口
|
|
1780
|
+
* 时会自然重试。
|
|
1781
|
+
*
|
|
1782
|
+
* 熔断记账按「批」算:一次批请求失败记一次,逐片段重试失败再各记一次。
|
|
1932
1783
|
*/
|
|
1933
|
-
async
|
|
1784
|
+
async translateReplyBatch(adapter, batch, config) {
|
|
1934
1785
|
try {
|
|
1935
|
-
|
|
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]
|
|
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.
|
|
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]
|
|
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
|
|
1957
|
-
const timeout = config.
|
|
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:
|
|
1820
|
+
maxTokens: REPLY_MAX_OUTPUT_TOKENS,
|
|
1966
1821
|
mode: "plain"
|
|
1967
1822
|
})
|
|
1968
1823
|
];
|
|
1969
1824
|
} else {
|
|
1970
|
-
const format =
|
|
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:
|
|
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("
|
|
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(`
|
|
1845
|
+
throw new Error(`reply block ${piece.block} #${piece.index} came back empty`);
|
|
1991
1846
|
}
|
|
1992
|
-
if (
|
|
1993
|
-
throw new Error("
|
|
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("
|
|
1855
|
+
throw new Error("reply translation left a mask placeholder behind");
|
|
2001
1856
|
}
|
|
2002
|
-
out.set(
|
|
1857
|
+
out.set(replyPieceKey(piece), finalText);
|
|
2003
1858
|
});
|
|
2004
1859
|
return out;
|
|
2005
1860
|
}
|
|
2006
|
-
/**
|
|
2007
|
-
|
|
2008
|
-
const next = this.
|
|
2009
|
-
this.
|
|
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 = "
|
|
1886
|
+
const testText = "Here is what I changed.";
|
|
2033
1887
|
const start = Date.now();
|
|
2034
1888
|
try {
|
|
2035
|
-
const timeout =
|
|
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
|
|
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
|
|
2157
|
-
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.
|
|
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 [
|
|
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
|
|
2229
|
-
enabled: Schema.boolean().default(DEFAULT_CONFIG.enabled),
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
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(
|
|
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
|
|
2246
|
-
|
|
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
|
-
|
|
2252
|
-
|
|
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(() =>
|
|
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
|