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