@lynn123411/dsh-chat-translate 1.3.2 → 1.7.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 +14 -9
- package/docs//351/205/215/347/275/256.md +11 -1
- package/lib/client.js +1527 -489
- package/lib/client.js.map +4 -4
- package/lib/index.js +565 -173
- package/lib/index.js.map +4 -4
- package/lib/types/client/index.d.ts +7 -5
- package/lib/types/client/locales.d.ts +51 -4
- package/lib/types/client/reading-settings-page.d.ts +1 -1
- package/lib/types/client/settings/store.d.ts +17 -4
- package/lib/types/client/settings/ui.d.ts +5 -3
- package/lib/types/client/translate/api.d.ts +13 -0
- package/lib/types/client/translate/client-cache.d.ts +6 -1
- package/lib/types/client/translate/hold.d.ts +135 -0
- package/lib/types/client/translate/lazy.d.ts +8 -0
- package/lib/types/client/translate/mount.d.ts +22 -0
- package/lib/types/client/translate/observer.d.ts +57 -0
- package/lib/types/client/translate/think-button.d.ts +19 -0
- package/lib/types/client/translate/think.d.ts +55 -0
- package/lib/types/client/translate/viewport-observer.d.ts +14 -2
- package/lib/types/index.d.ts +11 -18
- package/lib/types/server/adapters/base.d.ts +1 -1
- package/lib/types/server/adapters/openai.d.ts +2 -2
- package/lib/types/server/cache.d.ts +5 -1
- package/lib/types/server/config.d.ts +35 -7
- package/lib/types/server/dispatcher.d.ts +30 -3
- package/lib/types/server/pipeline/mask-tokens.d.ts +54 -48
- package/lib/types/server/pipeline/masking.d.ts +35 -12
- package/lib/types/server/pipeline/think.d.ts +49 -0
- package/lib/types/server/router.d.ts +24 -3
- package/lib/types/server/types.d.ts +18 -1
- package/package.json +23 -10
package/lib/client.js
CHANGED
|
@@ -29,39 +29,54 @@ __export(index_exports, {
|
|
|
29
29
|
inject: () => inject,
|
|
30
30
|
lazyQueue: () => lazyQueue,
|
|
31
31
|
name: () => name,
|
|
32
|
+
rowHold: () => rowHold,
|
|
32
33
|
settingsStore: () => settingsStore,
|
|
33
34
|
setupSettingsUi: () => setupSettingsUi
|
|
34
35
|
});
|
|
35
36
|
module.exports = __toCommonJS(index_exports);
|
|
36
37
|
|
|
37
38
|
// src/server/pipeline/mask-tokens.ts
|
|
38
|
-
var MASK_TOKEN_PATTERN_SOURCE = "
|
|
39
|
+
var MASK_TOKEN_PATTERN_SOURCE = "\u27E6([a-z]{4})(\\d+)\u27E7";
|
|
40
|
+
var LEGACY_MASK_PATTERN_SOURCE = "_{1,2}DSH\\s*_*\\s*MASKx?\\s*(?:_?\\s*\\d+|_*([a-z]{2,8})\\s*_+\\s*(\\d+))_{0,2}";
|
|
39
41
|
function hasMaskResidue(text) {
|
|
40
42
|
if (!text) return false;
|
|
41
|
-
return new RegExp(MASK_TOKEN_PATTERN_SOURCE, "
|
|
43
|
+
return new RegExp(MASK_TOKEN_PATTERN_SOURCE, "iu").test(text);
|
|
44
|
+
}
|
|
45
|
+
function hasLegacyMaskResidue(text) {
|
|
46
|
+
if (!text) return false;
|
|
47
|
+
return new RegExp(LEGACY_MASK_PATTERN_SOURCE, "iu").test(text);
|
|
48
|
+
}
|
|
49
|
+
function hasAnyMaskResidue(text) {
|
|
50
|
+
return hasMaskResidue(text) || hasLegacyMaskResidue(text);
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
// src/server/pipeline/masking.ts
|
|
45
54
|
function isMaskLeak(translatedText) {
|
|
46
|
-
return
|
|
55
|
+
return hasAnyMaskResidue(translatedText);
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
// src/client/translate/client-cache.ts
|
|
50
|
-
var
|
|
51
|
-
var
|
|
59
|
+
var TITLE_CACHE_KEY = "dsh-chat-translate:cache";
|
|
60
|
+
var TITLE_MAX_ENTRIES = 500;
|
|
61
|
+
var THINK_CACHE_KEY = "dsh-chat-translate:think-cache";
|
|
62
|
+
var THINK_MAX_ENTRIES = 300;
|
|
52
63
|
var TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
53
64
|
var ClientCache = class {
|
|
54
65
|
// Map preserves insertion order in JS, enabling true LRU semantics.
|
|
55
66
|
memCache = /* @__PURE__ */ new Map();
|
|
56
67
|
dirty = false;
|
|
57
68
|
saveTimer = null;
|
|
58
|
-
|
|
69
|
+
storageKey;
|
|
70
|
+
maxEntries;
|
|
71
|
+
constructor(storageKey = TITLE_CACHE_KEY, maxEntries = TITLE_MAX_ENTRIES) {
|
|
72
|
+
this.storageKey = storageKey;
|
|
73
|
+
this.maxEntries = maxEntries;
|
|
59
74
|
this.load();
|
|
60
75
|
}
|
|
61
76
|
load() {
|
|
62
77
|
if (typeof localStorage === "undefined") return;
|
|
63
78
|
try {
|
|
64
|
-
const raw = localStorage.getItem(
|
|
79
|
+
const raw = localStorage.getItem(this.storageKey);
|
|
65
80
|
if (raw) {
|
|
66
81
|
const obj = JSON.parse(raw);
|
|
67
82
|
if (obj && typeof obj === "object") {
|
|
@@ -107,7 +122,7 @@ var ClientCache = class {
|
|
|
107
122
|
const key = text.trim().toLowerCase();
|
|
108
123
|
if (this.memCache.has(key)) {
|
|
109
124
|
this.memCache.delete(key);
|
|
110
|
-
} else if (this.memCache.size >=
|
|
125
|
+
} else if (this.memCache.size >= this.maxEntries) {
|
|
111
126
|
const oldestKey = this.memCache.keys().next().value;
|
|
112
127
|
if (oldestKey !== void 0) {
|
|
113
128
|
this.memCache.delete(oldestKey);
|
|
@@ -132,7 +147,7 @@ var ClientCache = class {
|
|
|
132
147
|
for (const [k, v] of this.memCache.entries()) {
|
|
133
148
|
obj[k] = v;
|
|
134
149
|
}
|
|
135
|
-
localStorage.setItem(
|
|
150
|
+
localStorage.setItem(this.storageKey, JSON.stringify(obj));
|
|
136
151
|
} catch {
|
|
137
152
|
}
|
|
138
153
|
}
|
|
@@ -146,95 +161,14 @@ var ClientCache = class {
|
|
|
146
161
|
}
|
|
147
162
|
};
|
|
148
163
|
var clientCache = new ClientCache();
|
|
149
|
-
|
|
150
|
-
// src/describe-error.ts
|
|
151
|
-
function describeError(err) {
|
|
152
|
-
if (err === null || err === void 0) return String(err);
|
|
153
|
-
const parts = [];
|
|
154
|
-
const push = (s) => {
|
|
155
|
-
const t = String(s || "").trim();
|
|
156
|
-
if (t && !parts.includes(t)) parts.push(t);
|
|
157
|
-
};
|
|
158
|
-
const head = err.message || String(err);
|
|
159
|
-
push(head);
|
|
160
|
-
const headCode = err.code;
|
|
161
|
-
if (headCode && !head.includes(headCode)) push("[" + headCode + "]");
|
|
162
|
-
let cause = err.cause;
|
|
163
|
-
for (let depth = 0; cause && depth < 4; depth++) {
|
|
164
|
-
const label = cause.message || String(cause);
|
|
165
|
-
push(cause.code && !label.includes(cause.code) ? label + " [" + cause.code + "]" : label);
|
|
166
|
-
if (Array.isArray(cause.errors)) {
|
|
167
|
-
for (const sub of cause.errors.slice(0, 3)) {
|
|
168
|
-
if (sub) push(sub.code || sub.message || String(sub));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
cause = cause.cause;
|
|
172
|
-
}
|
|
173
|
-
return parts.join(" \u2190 ");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// src/client/translate/api.ts
|
|
177
|
-
async function requestTranslateBatch(texts, options = {}) {
|
|
178
|
-
if (!Array.isArray(texts) || texts.length === 0) return [];
|
|
179
|
-
const validTexts = texts.map((t) => typeof t === "string" ? t : "");
|
|
180
|
-
if (validTexts.length === 0) return [];
|
|
181
|
-
const controller = new AbortController();
|
|
182
|
-
const timeoutId = options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : null;
|
|
183
|
-
const effectiveSignal = options.signal ? options.signal.aborted ? options.signal : controller.signal : controller.signal;
|
|
184
|
-
if (options.signal) {
|
|
185
|
-
options.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
186
|
-
}
|
|
187
|
-
try {
|
|
188
|
-
const res = await fetch("/api/dsh-chat-translate/translate", {
|
|
189
|
-
method: "POST",
|
|
190
|
-
headers: {
|
|
191
|
-
"Content-Type": "application/json"
|
|
192
|
-
},
|
|
193
|
-
body: JSON.stringify({ texts: validTexts }),
|
|
194
|
-
signal: effectiveSignal
|
|
195
|
-
});
|
|
196
|
-
if (res.ok) {
|
|
197
|
-
const data = await res.json();
|
|
198
|
-
if (data.ok && Array.isArray(data.results) && data.results.length > 0) {
|
|
199
|
-
return data.results;
|
|
200
|
-
} else if (data.error) {
|
|
201
|
-
console.warn(`[dsh-chat-translate] \u7FFB\u8BD1\u63A5\u53E3\u9519\u8BEF: ${data.error}`);
|
|
202
|
-
}
|
|
203
|
-
} else {
|
|
204
|
-
console.warn(`[dsh-chat-translate] \u7FFB\u8BD1\u8BF7\u6C42\u5931\u8D25 (HTTP ${res.status})`);
|
|
205
|
-
}
|
|
206
|
-
} catch (err) {
|
|
207
|
-
if (err?.name === "AbortError") {
|
|
208
|
-
}
|
|
209
|
-
} finally {
|
|
210
|
-
if (timeoutId !== null) {
|
|
211
|
-
clearTimeout(timeoutId);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return validTexts.map((t) => ({
|
|
215
|
-
original: t,
|
|
216
|
-
translated: t,
|
|
217
|
-
channel: "fallback-client",
|
|
218
|
-
cached: false
|
|
219
|
-
}));
|
|
220
|
-
}
|
|
221
|
-
async function testServerChannel(channel) {
|
|
222
|
-
try {
|
|
223
|
-
const res = await fetch("/api/dsh-chat-translate/test-channel", {
|
|
224
|
-
method: "POST",
|
|
225
|
-
headers: { "Content-Type": "application/json" },
|
|
226
|
-
body: JSON.stringify({ channel })
|
|
227
|
-
});
|
|
228
|
-
return await res.json();
|
|
229
|
-
} catch (err) {
|
|
230
|
-
return { ok: false, latencyMs: 0, error: describeError(err) };
|
|
231
|
-
}
|
|
232
|
-
}
|
|
164
|
+
var thinkClientCache = new ClientCache(THINK_CACHE_KEY, THINK_MAX_ENTRIES);
|
|
233
165
|
|
|
234
166
|
// src/client/translate/mount.ts
|
|
235
167
|
var CLASS_ORIGINAL_HIDDEN = "dsh-tidy-original-hidden";
|
|
236
168
|
var CLASS_ORIGINAL_SHOWN = "dsh-tidy-original-shown";
|
|
237
169
|
var CLASS_TRANSLATED_BLOCK = "dsh-tidy-translated-block";
|
|
170
|
+
var CLASS_RUN = "dsh-tidy-run";
|
|
171
|
+
var RUN_ATTRIBUTE = "data-tidy-run";
|
|
238
172
|
var NonDestructiveTranslationMount = class {
|
|
239
173
|
/**
|
|
240
174
|
* Mounts a translated string onto the target element non-destructively.
|
|
@@ -286,6 +220,72 @@ var NonDestructiveTranslationMount = class {
|
|
|
286
220
|
element.dataset.tidyTranslated = "true";
|
|
287
221
|
element.dataset.original = originalText;
|
|
288
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* 把一段连续的行内节点包进自建容器后挂载译文。
|
|
225
|
+
*
|
|
226
|
+
* 混合块(列表项文字 + 子列表 / 代码块)里的行内片段没有自己的元素,
|
|
227
|
+
* 只能先包一层再走同一条非侵入式挂载;嵌套的块级子节点留在容器之外,
|
|
228
|
+
* 排版原样保留。
|
|
229
|
+
* @returns 自建容器;节点不再相邻或没有共同父节点时返回 null。
|
|
230
|
+
*/
|
|
231
|
+
static mountRun(nodes, translated, options = {}) {
|
|
232
|
+
const first = nodes[0];
|
|
233
|
+
const parent = first?.parentNode;
|
|
234
|
+
if (!first || !parent || !parent.ownerDocument) return null;
|
|
235
|
+
if (!nodes.every((node) => node.parentNode === parent)) return null;
|
|
236
|
+
const originalText = options.originalText ?? nodes.map((node) => node.textContent ?? "").join("").trim();
|
|
237
|
+
if (!originalText) return null;
|
|
238
|
+
const run = parent.ownerDocument.createElement("span");
|
|
239
|
+
run.className = CLASS_RUN;
|
|
240
|
+
run.setAttribute(RUN_ATTRIBUTE, "true");
|
|
241
|
+
parent.insertBefore(run, first);
|
|
242
|
+
for (const node of nodes) run.appendChild(node);
|
|
243
|
+
this.mount(run, translated, { originalText, interactive: options.interactive });
|
|
244
|
+
return run;
|
|
245
|
+
}
|
|
246
|
+
/** 还原行内片段并撤掉自建容器。 */
|
|
247
|
+
static unmountRun(run) {
|
|
248
|
+
this.unmount(run);
|
|
249
|
+
const parent = run.parentNode;
|
|
250
|
+
if (!parent) return;
|
|
251
|
+
while (run.firstChild) parent.insertBefore(run.firstChild, run);
|
|
252
|
+
parent.removeChild(run);
|
|
253
|
+
}
|
|
254
|
+
/** 还原一个作用域内我们挂过的全部译文(元素单位与行内片段都算)。 */
|
|
255
|
+
static restore(scope) {
|
|
256
|
+
const mounted = scope.querySelectorAll('[data-tidy-translated="true"]');
|
|
257
|
+
for (const element of mounted) {
|
|
258
|
+
if (element.getAttribute(RUN_ATTRIBUTE) === "true") this.unmountRun(element);
|
|
259
|
+
else this.unmount(element);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** 我们自己的挂载容器与还原容器,观察器必须跳过它们。 */
|
|
263
|
+
static isOwnNode(element) {
|
|
264
|
+
return element.classList?.contains(CLASS_TRANSLATED_BLOCK) === true || element.classList?.contains(CLASS_ORIGINAL_HIDDEN) === true || element.classList?.contains(CLASS_ORIGINAL_SHOWN) === true || element.classList?.contains(CLASS_RUN) === true;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 把一个作用域里的全部挂载译文一起翻面。思考链只由卡片标题右侧的按钮调用
|
|
268
|
+
* 它,正文里的点击不再改变原文与译文。
|
|
269
|
+
*/
|
|
270
|
+
static toggleGroup(scope, showOriginal) {
|
|
271
|
+
const mounted = scope.querySelectorAll('[data-tidy-translated="true"]');
|
|
272
|
+
for (const element of mounted) {
|
|
273
|
+
const origWrapper = element.querySelector(
|
|
274
|
+
`:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
|
|
275
|
+
);
|
|
276
|
+
const transWrapper = element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
|
|
277
|
+
if (!origWrapper || !transWrapper) continue;
|
|
278
|
+
if (showOriginal) {
|
|
279
|
+
origWrapper.style.display = "inline";
|
|
280
|
+
origWrapper.className = CLASS_ORIGINAL_SHOWN;
|
|
281
|
+
transWrapper.style.display = "none";
|
|
282
|
+
} else {
|
|
283
|
+
origWrapper.style.display = "none";
|
|
284
|
+
origWrapper.className = CLASS_ORIGINAL_HIDDEN;
|
|
285
|
+
transWrapper.style.display = "inline";
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
289
|
/**
|
|
290
290
|
* Unmounts translation and restores original DOM nodes completely.
|
|
291
291
|
*/
|
|
@@ -344,6 +344,397 @@ var NonDestructiveTranslationMount = class {
|
|
|
344
344
|
}
|
|
345
345
|
};
|
|
346
346
|
|
|
347
|
+
// src/client/translate/hold.ts
|
|
348
|
+
var HOLD_DEADLINE_MS = 5e3;
|
|
349
|
+
var RELEASE_BUDGET_MS = 900;
|
|
350
|
+
var RELEASE_STEP_MIN_MS = 60;
|
|
351
|
+
var RELEASE_STEP_MAX_MS = 220;
|
|
352
|
+
var REVEAL_HOLD_ATTRIBUTE = "data-dsh-reveal-hold";
|
|
353
|
+
var ROW_ENTRANCE_HOST_SELECTOR = "[data-entrance]";
|
|
354
|
+
var FLOW_SELECTOR = "[data-chat-flow]";
|
|
355
|
+
var FLOW_ITEM_SELECTOR = "[data-chat-flow-key]";
|
|
356
|
+
var USER_KINDS = /* @__PURE__ */ new Set(["user", "steering", "command-input"]);
|
|
357
|
+
function replayRowEntrance(element) {
|
|
358
|
+
const host = element.closest(ROW_ENTRANCE_HOST_SELECTOR) ?? element.querySelector(ROW_ENTRANCE_HOST_SELECTOR);
|
|
359
|
+
if (!host) return;
|
|
360
|
+
if (host.getAttribute("data-entrance") === "active") {
|
|
361
|
+
host.style.animation = "none";
|
|
362
|
+
void host.offsetWidth;
|
|
363
|
+
host.style.animation = "";
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
host.setAttribute("data-entrance", "active");
|
|
367
|
+
}
|
|
368
|
+
var RowHoldQueue = class {
|
|
369
|
+
/** 按阅读顺序排列的扣留行。 */
|
|
370
|
+
entries = [];
|
|
371
|
+
/** 结果回来时只知道 span,不知道条目,所以另留一份索引。 */
|
|
372
|
+
bySpan = /* @__PURE__ */ new WeakMap();
|
|
373
|
+
/** 当前挂起着的流程条目 → 隐藏之前它自己的行内 display。 */
|
|
374
|
+
marked = /* @__PURE__ */ new Map();
|
|
375
|
+
/** 已轮到上屏、正在按阅读顺序逐行吐出的条目。 */
|
|
376
|
+
releasing = [];
|
|
377
|
+
releaseTimer = null;
|
|
378
|
+
/**
|
|
379
|
+
* 隐藏整行并启动放行上限。幂等:同一行在途请求期间文本又变了时只更新文本并
|
|
380
|
+
* 重新等待,不重置不可见窗口。
|
|
381
|
+
*/
|
|
382
|
+
hold(span, root, text) {
|
|
383
|
+
const existing = this.bySpan.get(span) ?? this.entries.find((entry2) => entry2.root === root);
|
|
384
|
+
if (existing) {
|
|
385
|
+
if (existing.span !== span) {
|
|
386
|
+
if (existing.span !== null) this.bySpan.delete(existing.span);
|
|
387
|
+
existing.span = span;
|
|
388
|
+
this.bySpan.set(span, existing);
|
|
389
|
+
}
|
|
390
|
+
if (existing.text !== text) {
|
|
391
|
+
existing.text = text;
|
|
392
|
+
existing.translation = null;
|
|
393
|
+
existing.ready = false;
|
|
394
|
+
if (existing.timer === null && typeof window !== "undefined") {
|
|
395
|
+
existing.timer = window.setTimeout(() => this.readyEntry(existing, null), HOLD_DEADLINE_MS);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
this.sync();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const item = root.closest(FLOW_ITEM_SELECTOR);
|
|
402
|
+
if (!item) return;
|
|
403
|
+
const entry = {
|
|
404
|
+
item,
|
|
405
|
+
span,
|
|
406
|
+
root,
|
|
407
|
+
text,
|
|
408
|
+
translation: null,
|
|
409
|
+
ready: false,
|
|
410
|
+
timer: null
|
|
411
|
+
};
|
|
412
|
+
if (typeof window !== "undefined") {
|
|
413
|
+
entry.timer = window.setTimeout(() => this.readyEntry(entry, null), HOLD_DEADLINE_MS);
|
|
414
|
+
}
|
|
415
|
+
this.insert(entry);
|
|
416
|
+
this.bySpan.set(span, entry);
|
|
417
|
+
this.sync();
|
|
418
|
+
}
|
|
419
|
+
/** 该文本所属行的扣留状态;返回 undefined 表示没有扣留。 */
|
|
420
|
+
stateFor(span) {
|
|
421
|
+
return this.bySpan.get(span);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* 译文(或失败判定)就绪。真正上屏要等它排到队首:入队顺序就是阅读顺序。
|
|
425
|
+
* @param span - 等待中的摘要 span。
|
|
426
|
+
* @param translation - 译文;null 表示这一行没有译文可等,放行原文。
|
|
427
|
+
*/
|
|
428
|
+
ready(span, translation) {
|
|
429
|
+
const entry = this.bySpan.get(span);
|
|
430
|
+
if (!entry) return;
|
|
431
|
+
if (entry.ready && (translation === null || entry.translation !== null)) return;
|
|
432
|
+
this.readyEntry(entry, translation);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* 扣留期间新出现的会话流内容:排在队首后面的一律先不上屏。
|
|
436
|
+
*
|
|
437
|
+
* 藏的是整个流程条目,不是某一张卡片:条目里的东西有的归揭示引擎管(回答正文、
|
|
438
|
+
* 思考正文、译文,标记会让它们只积压不推进),有的宿主直接落盘(工具卡片正文、
|
|
439
|
+
* 不翻译的读写文件行),只冻结揭示挡不住后者。整条藏起来两种都覆盖,而且条目
|
|
440
|
+
* 本身在 React 重渲染里不会被换掉,卡片被换掉也照样藏得住。
|
|
441
|
+
* @param node - 这次变更里新增的元素,或其祖先所在的那一片子树。
|
|
442
|
+
*/
|
|
443
|
+
noteAppended(node) {
|
|
444
|
+
const head = this.entries[0];
|
|
445
|
+
if (!head) return;
|
|
446
|
+
const enclosed = node.matches(FLOW_ITEM_SELECTOR) ? [node, ...node.querySelectorAll(FLOW_ITEM_SELECTOR)] : [...node.querySelectorAll(FLOW_ITEM_SELECTOR)];
|
|
447
|
+
if (enclosed.length > 0) {
|
|
448
|
+
for (const item2 of enclosed) this.holdItem(item2, head);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const item = node.closest(FLOW_ITEM_SELECTOR);
|
|
452
|
+
if (item !== null) this.holdItem(item, head);
|
|
453
|
+
}
|
|
454
|
+
/** 挂起一个条目:整条不可见,并且不推进它里面的揭示。 */
|
|
455
|
+
holdItem(item, head) {
|
|
456
|
+
if (!this.follows(head.item, item)) return;
|
|
457
|
+
const kind = item.getAttribute("data-chat-flow-kind");
|
|
458
|
+
if (kind !== null && USER_KINDS.has(kind)) return;
|
|
459
|
+
this.mark(item);
|
|
460
|
+
}
|
|
461
|
+
follows(head, item) {
|
|
462
|
+
return (head.compareDocumentPosition(item) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* 藏住整条流程条目。用 `display: none` 而不是 `opacity`/`visibility`:条目内部的
|
|
466
|
+
* 节点可以显式声明 `visibility: visible`(系统提示卡片就是这么自管的),那样会从
|
|
467
|
+
* 隐藏的祖先里逃出来;而且只要条目还参与布局,会话流的 16px 兄弟间距就还在——每
|
|
468
|
+
* 一条被扣留的行都会在屏幕底部留一段等宽的空白。整个盒子移除,等待期间占位为零,
|
|
469
|
+
* 轮到它时再连同入场淡入一起撑开。
|
|
470
|
+
*/
|
|
471
|
+
mark(item) {
|
|
472
|
+
if (this.marked.has(item)) return;
|
|
473
|
+
this.marked.set(item, item.style.display);
|
|
474
|
+
item.setAttribute(REVEAL_HOLD_ATTRIBUTE, "");
|
|
475
|
+
item.style.display = "none";
|
|
476
|
+
}
|
|
477
|
+
/** 解除挂起;`animate` 为真时补一段行入场淡入,避免内容一下子蹦出来。 */
|
|
478
|
+
unmark(item, animate) {
|
|
479
|
+
const previous = this.marked.get(item);
|
|
480
|
+
if (previous === void 0) return;
|
|
481
|
+
this.marked.delete(item);
|
|
482
|
+
item.removeAttribute(REVEAL_HOLD_ATTRIBUTE);
|
|
483
|
+
item.style.display = previous;
|
|
484
|
+
if (animate) replayRowEntrance(item);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* 排进放行队列。按阅读顺序插入,所以逐行吐出的顺序就是行在流里的顺序。
|
|
488
|
+
*/
|
|
489
|
+
enqueueRelease(item) {
|
|
490
|
+
if (this.releasing.includes(item)) return;
|
|
491
|
+
const index = this.releasing.findIndex(
|
|
492
|
+
(other) => (item.compareDocumentPosition(other) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
|
|
493
|
+
);
|
|
494
|
+
if (index === -1) this.releasing.push(item);
|
|
495
|
+
else this.releasing.splice(index, 0, item);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* 取出下一个还连在文档里的待放行条目。
|
|
499
|
+
* @returns 待放行条目;队列空或只剩已断开节点时返回 undefined。
|
|
500
|
+
*/
|
|
501
|
+
takeRelease() {
|
|
502
|
+
while (this.releasing.length > 0) {
|
|
503
|
+
const item = this.releasing.shift();
|
|
504
|
+
if (item.isConnected) return item;
|
|
505
|
+
this.unmark(item, false);
|
|
506
|
+
}
|
|
507
|
+
return void 0;
|
|
508
|
+
}
|
|
509
|
+
/** 队伍越长步长越短:几行时让淡入跑完,几十行时也不至于让读者干等。 */
|
|
510
|
+
releaseStepMs() {
|
|
511
|
+
const step = RELEASE_BUDGET_MS / (this.releasing.length + 1);
|
|
512
|
+
return Math.min(RELEASE_STEP_MAX_MS, Math.max(RELEASE_STEP_MIN_MS, step));
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* 逐行放行:每一步只让一行出现,然后按当时的队伍长度安排下一步。队首(刚解锁
|
|
516
|
+
* 的那一行)走的是同步的那一步,所以它不会比译文晚一帧上屏。
|
|
517
|
+
*/
|
|
518
|
+
pumpRelease() {
|
|
519
|
+
if (this.releaseTimer !== null || typeof window === "undefined") return;
|
|
520
|
+
const item = this.takeRelease();
|
|
521
|
+
if (item === void 0) return;
|
|
522
|
+
this.unmark(item, true);
|
|
523
|
+
if (this.releasing.length === 0) return;
|
|
524
|
+
this.releaseTimer = window.setTimeout(() => {
|
|
525
|
+
this.releaseTimer = null;
|
|
526
|
+
this.pumpRelease();
|
|
527
|
+
}, this.releaseStepMs());
|
|
528
|
+
}
|
|
529
|
+
/** 丢掉逐行放行的进度(关闭、换会话、断开观察器时用)。 */
|
|
530
|
+
clearReleases() {
|
|
531
|
+
if (this.releaseTimer !== null && typeof window !== "undefined") {
|
|
532
|
+
window.clearTimeout(this.releaseTimer);
|
|
533
|
+
}
|
|
534
|
+
this.releaseTimer = null;
|
|
535
|
+
this.releasing = [];
|
|
536
|
+
}
|
|
537
|
+
/** 按阅读顺序入队:入队顺序就是上屏顺序。 */
|
|
538
|
+
insert(entry) {
|
|
539
|
+
const index = this.entries.findIndex((other) => this.follows(entry.item, other.item));
|
|
540
|
+
if (index === -1) this.entries.push(entry);
|
|
541
|
+
else this.entries.splice(index, 0, entry);
|
|
542
|
+
}
|
|
543
|
+
/** 译文(或超时判定)落地,交给队首判定。 */
|
|
544
|
+
readyEntry(entry, translation) {
|
|
545
|
+
if (entry.timer !== null && typeof window !== "undefined") {
|
|
546
|
+
window.clearTimeout(entry.timer);
|
|
547
|
+
entry.timer = null;
|
|
548
|
+
}
|
|
549
|
+
entry.translation = translation;
|
|
550
|
+
entry.ready = true;
|
|
551
|
+
this.drain();
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* 按顺序解锁:队首就绪就挂译文并把它交给逐行放行,然后看新的队首。译文在条目
|
|
555
|
+
* 仍是 `display: none` 时挂上,所以屏幕上不会出现完整中文被画一帧再被截断的闪烁;
|
|
556
|
+
* 真正撑开这一行由 {@link pumpRelease} 执行。
|
|
557
|
+
*/
|
|
558
|
+
drain() {
|
|
559
|
+
this.entries = this.entries.filter((entry) => {
|
|
560
|
+
if (entry.item.isConnected) return true;
|
|
561
|
+
if (entry.span !== null) this.bySpan.delete(entry.span);
|
|
562
|
+
return false;
|
|
563
|
+
});
|
|
564
|
+
while (this.entries.length > 0 && this.entries[0].ready) {
|
|
565
|
+
const entry = this.entries.shift();
|
|
566
|
+
if (entry.span !== null) this.bySpan.delete(entry.span);
|
|
567
|
+
if (entry.span !== null && entry.translation !== null && entry.span.isConnected) {
|
|
568
|
+
NonDestructiveTranslationMount.mount(entry.span, entry.translation, {
|
|
569
|
+
originalText: entry.text
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
this.sync();
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* 让挂起标记与队首对齐:队首自己以及排在它后面的每个流程条目都藏起来,其余
|
|
577
|
+
* 交给逐行放行。放行让队首前移时,之前被它挡住的条目就在这一步进入放行队列。
|
|
578
|
+
*
|
|
579
|
+
* 没有「立即恢复」的旁路:放行一律排队逐行吐出,所以放行途中新到的队首不会把
|
|
580
|
+
* 正在等的那些行一次性抖出来(`releaseAll` 是唯一的立即路径)。
|
|
581
|
+
*/
|
|
582
|
+
sync() {
|
|
583
|
+
const head = this.entries[0];
|
|
584
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
585
|
+
if (head) {
|
|
586
|
+
const flow = head.item.closest(FLOW_SELECTOR);
|
|
587
|
+
let reached = false;
|
|
588
|
+
if (flow) {
|
|
589
|
+
for (const item of flow.querySelectorAll(FLOW_ITEM_SELECTOR)) {
|
|
590
|
+
if (item === head.item) reached = true;
|
|
591
|
+
if (reached) wanted.add(item);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (!reached) wanted.add(head.item);
|
|
595
|
+
}
|
|
596
|
+
for (const item of [...this.marked.keys()]) {
|
|
597
|
+
if (!wanted.has(item)) this.enqueueRelease(item);
|
|
598
|
+
}
|
|
599
|
+
if (this.releasing.length > 0) {
|
|
600
|
+
this.releasing = this.releasing.filter((item) => !wanted.has(item));
|
|
601
|
+
}
|
|
602
|
+
for (const item of wanted) this.mark(item);
|
|
603
|
+
this.pumpRelease();
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* 全部放行:断开观察器与关闭总开关时用。批量放行不做入场动画,也不排队,
|
|
607
|
+
* 否则整屏会一起闪一遍、或者留下一个跑不完的定时器。
|
|
608
|
+
*/
|
|
609
|
+
releaseAll() {
|
|
610
|
+
this.clearReleases();
|
|
611
|
+
for (const entry of this.entries) {
|
|
612
|
+
if (entry.timer !== null && typeof window !== "undefined") {
|
|
613
|
+
window.clearTimeout(entry.timer);
|
|
614
|
+
}
|
|
615
|
+
if (entry.span !== null) this.bySpan.delete(entry.span);
|
|
616
|
+
}
|
|
617
|
+
this.entries = [];
|
|
618
|
+
for (const item of [...this.marked.keys()]) this.unmark(item, false);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
var rowHold = new RowHoldQueue();
|
|
622
|
+
|
|
623
|
+
// src/describe-error.ts
|
|
624
|
+
function describeError(err) {
|
|
625
|
+
if (err === null || err === void 0) return String(err);
|
|
626
|
+
const parts = [];
|
|
627
|
+
const push = (s) => {
|
|
628
|
+
const t = String(s || "").trim();
|
|
629
|
+
if (t && !parts.includes(t)) parts.push(t);
|
|
630
|
+
};
|
|
631
|
+
const head = err.message || String(err);
|
|
632
|
+
push(head);
|
|
633
|
+
const headCode = err.code;
|
|
634
|
+
if (headCode && !head.includes(headCode)) push("[" + headCode + "]");
|
|
635
|
+
let cause = err.cause;
|
|
636
|
+
for (let depth = 0; cause && depth < 4; depth++) {
|
|
637
|
+
const label = cause.message || String(cause);
|
|
638
|
+
push(cause.code && !label.includes(cause.code) ? label + " [" + cause.code + "]" : label);
|
|
639
|
+
if (Array.isArray(cause.errors)) {
|
|
640
|
+
for (const sub of cause.errors.slice(0, 3)) {
|
|
641
|
+
if (sub) push(sub.code || sub.message || String(sub));
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
cause = cause.cause;
|
|
645
|
+
}
|
|
646
|
+
return parts.join(" \u2190 ");
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/client/translate/api.ts
|
|
650
|
+
async function requestTranslateBatch(texts, options = {}) {
|
|
651
|
+
if (!Array.isArray(texts) || texts.length === 0) return [];
|
|
652
|
+
const validTexts = texts.map((t) => typeof t === "string" ? t : "");
|
|
653
|
+
if (validTexts.length === 0) return [];
|
|
654
|
+
const controller = new AbortController();
|
|
655
|
+
const timeoutId = options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : null;
|
|
656
|
+
const effectiveSignal = options.signal ? options.signal.aborted ? options.signal : controller.signal : controller.signal;
|
|
657
|
+
if (options.signal) {
|
|
658
|
+
options.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
const res = await fetch("/api/dsh-chat-translate/translate", {
|
|
662
|
+
method: "POST",
|
|
663
|
+
headers: {
|
|
664
|
+
"Content-Type": "application/json"
|
|
665
|
+
},
|
|
666
|
+
body: JSON.stringify({ texts: validTexts }),
|
|
667
|
+
signal: effectiveSignal
|
|
668
|
+
});
|
|
669
|
+
if (res.ok) {
|
|
670
|
+
const data = await res.json();
|
|
671
|
+
if (data.ok && Array.isArray(data.results) && data.results.length > 0) {
|
|
672
|
+
return data.results;
|
|
673
|
+
} else if (data.error) {
|
|
674
|
+
console.warn(`[dsh-chat-translate] \u7FFB\u8BD1\u63A5\u53E3\u9519\u8BEF: ${data.error}`);
|
|
675
|
+
}
|
|
676
|
+
} else {
|
|
677
|
+
console.warn(`[dsh-chat-translate] \u7FFB\u8BD1\u8BF7\u6C42\u5931\u8D25 (HTTP ${res.status})`);
|
|
678
|
+
}
|
|
679
|
+
} catch (err) {
|
|
680
|
+
if (err?.name === "AbortError") {
|
|
681
|
+
}
|
|
682
|
+
} finally {
|
|
683
|
+
if (timeoutId !== null) {
|
|
684
|
+
clearTimeout(timeoutId);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return validTexts.map((t) => ({
|
|
688
|
+
original: t,
|
|
689
|
+
translated: t,
|
|
690
|
+
channel: "fallback-client",
|
|
691
|
+
cached: false
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
async function requestTranslateThink(blocks) {
|
|
695
|
+
const fallback = () => blocks.map((block) => ({
|
|
696
|
+
original: block,
|
|
697
|
+
translated: block,
|
|
698
|
+
ok: false,
|
|
699
|
+
cached: false,
|
|
700
|
+
channel: "fallback-client"
|
|
701
|
+
}));
|
|
702
|
+
if (!Array.isArray(blocks) || blocks.length === 0) return [];
|
|
703
|
+
try {
|
|
704
|
+
const res = await fetch("/api/dsh-chat-translate/translate-think", {
|
|
705
|
+
method: "POST",
|
|
706
|
+
headers: { "Content-Type": "application/json" },
|
|
707
|
+
body: JSON.stringify({ blocks })
|
|
708
|
+
});
|
|
709
|
+
if (res.ok) {
|
|
710
|
+
const data = await res.json();
|
|
711
|
+
if (data.ok && Array.isArray(data.results) && data.results.length === blocks.length) {
|
|
712
|
+
return data.results;
|
|
713
|
+
}
|
|
714
|
+
if (data.error) {
|
|
715
|
+
console.warn(`[dsh-chat-translate] \u601D\u8003\u94FE\u7FFB\u8BD1\u63A5\u53E3\u9519\u8BEF: ${data.error}`);
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
console.warn(`[dsh-chat-translate] \u601D\u8003\u94FE\u7FFB\u8BD1\u8BF7\u6C42\u5931\u8D25 (HTTP ${res.status})`);
|
|
719
|
+
}
|
|
720
|
+
} catch (err) {
|
|
721
|
+
console.warn(`[dsh-chat-translate] \u601D\u8003\u94FE\u7FFB\u8BD1\u8BF7\u6C42\u5F02\u5E38: ${describeError(err)}`);
|
|
722
|
+
}
|
|
723
|
+
return fallback();
|
|
724
|
+
}
|
|
725
|
+
async function testServerChannel(channel) {
|
|
726
|
+
try {
|
|
727
|
+
const res = await fetch("/api/dsh-chat-translate/test-channel", {
|
|
728
|
+
method: "POST",
|
|
729
|
+
headers: { "Content-Type": "application/json" },
|
|
730
|
+
body: JSON.stringify({ channel })
|
|
731
|
+
});
|
|
732
|
+
return await res.json();
|
|
733
|
+
} catch (err) {
|
|
734
|
+
return { ok: false, latencyMs: 0, error: describeError(err) };
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
347
738
|
// src/client/translate/viewport-observer.ts
|
|
348
739
|
var StreamDebounceViewportObserver = class {
|
|
349
740
|
intersectionObserver = null;
|
|
@@ -351,7 +742,7 @@ var StreamDebounceViewportObserver = class {
|
|
|
351
742
|
* Pending per-element streaming debounce timers. A Map (not a WeakMap)
|
|
352
743
|
* because disconnect() must enumerate and clear every pending timer: a
|
|
353
744
|
* WeakMap cannot be iterated, so a debounce armed just before the switch was
|
|
354
|
-
* turned off would still fire
|
|
745
|
+
* turned off would still fire register() afterwards.
|
|
355
746
|
*/
|
|
356
747
|
streamingTimers = /* @__PURE__ */ new Map();
|
|
357
748
|
pendingQueue = [];
|
|
@@ -375,7 +766,7 @@ var StreamDebounceViewportObserver = class {
|
|
|
375
766
|
if (entry.isIntersecting && entry.target instanceof HTMLElement) {
|
|
376
767
|
const el = entry.target;
|
|
377
768
|
this.intersectionObserver?.unobserve(el);
|
|
378
|
-
const text = el.dataset.tidyPendingText ||
|
|
769
|
+
const text = el.dataset.tidyPendingText || NonDestructiveTranslationMount.extractVisibleText(el) || "";
|
|
379
770
|
if (text) {
|
|
380
771
|
delete el.dataset.tidyPendingText;
|
|
381
772
|
this.enqueueBatch(el, text);
|
|
@@ -396,6 +787,16 @@ var StreamDebounceViewportObserver = class {
|
|
|
396
787
|
* If streaming updates characterData repeatedly within debounceMs, the timer resets.
|
|
397
788
|
*/
|
|
398
789
|
observeWithDebounce(element, text, immediate = false) {
|
|
790
|
+
this.armDebounce(element, text, immediate, false);
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* 扣留行的取文本路径:保留同一套流式防抖,跳过可见性判定——行此刻是
|
|
794
|
+
* 隐藏的(永远不进入视口),放行由扣留控制器负责。
|
|
795
|
+
*/
|
|
796
|
+
observeHeld(element, text) {
|
|
797
|
+
this.armDebounce(element, text, false, true);
|
|
798
|
+
}
|
|
799
|
+
armDebounce(element, text, immediate, skipViewport) {
|
|
399
800
|
if (!element || !text) return;
|
|
400
801
|
const existingTimer = this.streamingTimers.get(element);
|
|
401
802
|
if (existingTimer !== void 0) {
|
|
@@ -403,22 +804,22 @@ var StreamDebounceViewportObserver = class {
|
|
|
403
804
|
this.streamingTimers.delete(element);
|
|
404
805
|
}
|
|
405
806
|
if (immediate || this.options.debounceMs <= 0) {
|
|
406
|
-
this.
|
|
807
|
+
this.register(element, text, skipViewport);
|
|
407
808
|
return;
|
|
408
809
|
}
|
|
409
810
|
const timer = window.setTimeout(() => {
|
|
410
811
|
if (this.streamingTimers.get(element) !== timer) return;
|
|
411
812
|
this.streamingTimers.delete(element);
|
|
412
813
|
if (element.isConnected) {
|
|
413
|
-
const latestText =
|
|
414
|
-
this.
|
|
814
|
+
const latestText = NonDestructiveTranslationMount.extractVisibleText(element) || text;
|
|
815
|
+
this.register(element, latestText, skipViewport);
|
|
415
816
|
}
|
|
416
817
|
}, this.options.debounceMs);
|
|
417
818
|
this.streamingTimers.set(element, timer);
|
|
418
819
|
}
|
|
419
|
-
|
|
820
|
+
register(element, text, skipViewport) {
|
|
420
821
|
if (!element.isConnected) return;
|
|
421
|
-
if (!this.intersectionObserver) {
|
|
822
|
+
if (skipViewport || !this.intersectionObserver || element.closest(`[${REVEAL_HOLD_ATTRIBUTE}]`) !== null) {
|
|
422
823
|
this.enqueueBatch(element, text);
|
|
423
824
|
return;
|
|
424
825
|
}
|
|
@@ -479,76 +880,583 @@ var LazyTranslationQueue = class {
|
|
|
479
880
|
onVisibleBatch: (items) => this.handleVisibleBatch(items)
|
|
480
881
|
});
|
|
481
882
|
}
|
|
482
|
-
setEnabled(enabled) {
|
|
483
|
-
this.enabled = enabled;
|
|
484
|
-
if (!enabled) {
|
|
485
|
-
this.viewportObserver.disconnect();
|
|
883
|
+
setEnabled(enabled) {
|
|
884
|
+
this.enabled = enabled;
|
|
885
|
+
if (!enabled) {
|
|
886
|
+
this.viewportObserver.disconnect();
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
observe(element, text, immediate = false) {
|
|
890
|
+
if (!this.enabled || !element.isConnected) return;
|
|
891
|
+
const cached = clientCache.get(text);
|
|
892
|
+
if (cached) {
|
|
893
|
+
this.applyTranslation(element, cached, text);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
this.viewportObserver.observeWithDebounce(element, text, immediate);
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* 扣留行的取文本路径。命中缓存时同步就绪(没有需要遮住的等待),否则走无可见
|
|
900
|
+
* 性判定的防抖批次;译文挂载由扣留队列在轮到这一行时执行,好让逐字与对数淡入
|
|
901
|
+
* 从它真正上屏的那一刻开始。
|
|
902
|
+
*/
|
|
903
|
+
observeHeld(element, text) {
|
|
904
|
+
if (!this.enabled || !element.isConnected) return;
|
|
905
|
+
const cached = clientCache.get(text);
|
|
906
|
+
if (cached) {
|
|
907
|
+
rowHold.ready(element, cached);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
this.viewportObserver.observeHeld(element, text);
|
|
911
|
+
}
|
|
912
|
+
async handleVisibleBatch(items) {
|
|
913
|
+
if (!this.enabled || items.length === 0) return;
|
|
914
|
+
const sorted = [...items].sort((a, b) => {
|
|
915
|
+
if (a.element === b.element) return 0;
|
|
916
|
+
const pos = a.element.compareDocumentPosition(b.element);
|
|
917
|
+
return pos & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
|
918
|
+
});
|
|
919
|
+
const textMap = /* @__PURE__ */ new Map();
|
|
920
|
+
for (const item of sorted) {
|
|
921
|
+
if (!item.element.isConnected) continue;
|
|
922
|
+
const cached = clientCache.get(item.text);
|
|
923
|
+
if (cached) {
|
|
924
|
+
if (this.stillMatches(item.element, item.text)) {
|
|
925
|
+
if (rowHold.stateFor(item.element)) rowHold.ready(item.element, cached);
|
|
926
|
+
else this.applyTranslation(item.element, cached, item.text);
|
|
927
|
+
}
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
const list = textMap.get(item.text) || [];
|
|
931
|
+
list.push(item.element);
|
|
932
|
+
textMap.set(item.text, list);
|
|
933
|
+
}
|
|
934
|
+
const uniqueTexts = Array.from(textMap.keys());
|
|
935
|
+
if (uniqueTexts.length === 0) return;
|
|
936
|
+
const results = await requestTranslateBatch(uniqueTexts);
|
|
937
|
+
for (const res of results) {
|
|
938
|
+
const entries = textMap.get(res.original) || [];
|
|
939
|
+
if (res.translated && res.translated.trim() && res.channel !== "fallback" && res.channel !== "fallback-client" && res.translated.trim() !== res.original.trim()) {
|
|
940
|
+
clientCache.set(res.original, res.translated);
|
|
941
|
+
for (const entry of entries) {
|
|
942
|
+
if (!entry.isConnected || !this.enabled) continue;
|
|
943
|
+
const held = rowHold.stateFor(entry);
|
|
944
|
+
if (held && held.text !== res.original) continue;
|
|
945
|
+
if (!this.stillMatches(entry, res.original)) continue;
|
|
946
|
+
if (held) rowHold.ready(entry, res.translated);
|
|
947
|
+
else this.applyTranslation(entry, res.translated, res.original);
|
|
948
|
+
}
|
|
949
|
+
} else {
|
|
950
|
+
for (const entry of entries) {
|
|
951
|
+
const held = rowHold.stateFor(entry);
|
|
952
|
+
if (held && held.text === res.original) rowHold.ready(entry, null);
|
|
953
|
+
}
|
|
954
|
+
console.debug(`[dsh-chat-translate] \u7FFB\u8BD1\u672A\u6210\u529F (\u964D\u7EA7\u4FDD\u7559\u539F\u6587): "${res.original.slice(0, 40)}"`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
/** 元素当前承载的原文是否仍是这次结果对应的那一份。 */
|
|
959
|
+
stillMatches(element, original) {
|
|
960
|
+
const current = NonDestructiveTranslationMount.extractVisibleText(element);
|
|
961
|
+
return !current || current === original;
|
|
962
|
+
}
|
|
963
|
+
applyTranslation(element, translated, original) {
|
|
964
|
+
if (!element.isConnected || !this.enabled) return;
|
|
965
|
+
NonDestructiveTranslationMount.mount(element, translated, {
|
|
966
|
+
originalText: original
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
disconnect() {
|
|
970
|
+
this.viewportObserver.disconnect();
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
var lazyQueue = new LazyTranslationQueue();
|
|
974
|
+
|
|
975
|
+
// src/server/pipeline/think.ts
|
|
976
|
+
var CJK_CHAR = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/;
|
|
977
|
+
function estimateTokens(text) {
|
|
978
|
+
let cjk = 0;
|
|
979
|
+
for (const char of text) {
|
|
980
|
+
if (CJK_CHAR.test(char)) cjk += 1;
|
|
981
|
+
}
|
|
982
|
+
return cjk + Math.ceil((text.length - cjk) / 3);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// src/client/translate/think-button.ts
|
|
986
|
+
var THINK_BUTTON_CLASS = "dsh-tidy-think-button";
|
|
987
|
+
var STYLE_TAG_ID = "@lynn123411/dsh-chat-translate/think-button.css";
|
|
988
|
+
var THINK_BUTTON_CSS = `
|
|
989
|
+
/* \u601D\u8003\u5361\u6807\u9898\u53F3\u4FA7\u7684\u7FFB\u8BD1\u6309\u94AE\uFF1A\u4E09\u6001\uFF08\u672A\u7FFB\u8BD1 / \u7FFB\u8BD1\u4E2D / \u5DF2\u7FFB\u8BD1\uFF09\u4E0E\u65CB\u8F6C\u52A8\u753B\u3002 */
|
|
990
|
+
.dsh-tidy-think-button {
|
|
991
|
+
display: inline-flex;
|
|
992
|
+
align-items: center;
|
|
993
|
+
justify-content: center;
|
|
994
|
+
position: relative;
|
|
995
|
+
flex: none;
|
|
996
|
+
width: 18px;
|
|
997
|
+
height: 18px;
|
|
998
|
+
margin: 0 6px 0 2px;
|
|
999
|
+
padding: 0;
|
|
1000
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
1001
|
+
border-radius: 6px;
|
|
1002
|
+
background: transparent;
|
|
1003
|
+
color: var(--dsw-alias-label-tertiary, currentColor);
|
|
1004
|
+
font-family: inherit;
|
|
1005
|
+
font-size: 10px;
|
|
1006
|
+
line-height: 1;
|
|
1007
|
+
cursor: pointer;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.dsh-tidy-think-button:hover:not([disabled]) {
|
|
1011
|
+
border-color: var(--dsw-alias-label-secondary, rgba(128, 128, 128, 0.6));
|
|
1012
|
+
color: var(--dsw-alias-label-primary, currentColor);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
.dsh-tidy-think-button[data-state='translated'] {
|
|
1016
|
+
border-color: transparent;
|
|
1017
|
+
background: color-mix(in srgb, var(--dsw-static-deepseek-500, #4d6bfe) 18%, transparent);
|
|
1018
|
+
color: var(--dsw-static-deepseek-500, #4d6bfe);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
.dsh-tidy-think-button[data-state='working'] {
|
|
1022
|
+
color: transparent;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
.dsh-tidy-think-button[data-state='working']::after {
|
|
1026
|
+
content: '';
|
|
1027
|
+
position: absolute;
|
|
1028
|
+
inset: 3px;
|
|
1029
|
+
border: 1.5px solid var(--dsw-alias-label-caption, rgba(128, 128, 128, 0.45));
|
|
1030
|
+
border-top-color: var(--dsw-static-deepseek-500, #4d6bfe);
|
|
1031
|
+
border-radius: 50%;
|
|
1032
|
+
animation: dsh-tidy-think-spin 0.8s linear infinite;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
.dsh-tidy-think-button[disabled] {
|
|
1036
|
+
opacity: 0.35;
|
|
1037
|
+
cursor: default;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
@keyframes dsh-tidy-think-spin {
|
|
1041
|
+
to { transform: rotate(360deg); }
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1045
|
+
.dsh-tidy-think-button[data-state='working']::after {
|
|
1046
|
+
animation: none;
|
|
1047
|
+
border-color: var(--dsw-static-deepseek-500, #4d6bfe);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
`;
|
|
1051
|
+
function ensureThinkButtonStyles() {
|
|
1052
|
+
if (typeof document === "undefined") return;
|
|
1053
|
+
if (document.querySelector('style[data-plugin-css="' + STYLE_TAG_ID + '"]') !== null) return;
|
|
1054
|
+
const style = document.createElement("style");
|
|
1055
|
+
style.dataset.plugin = "@lynn123411/dsh-chat-translate";
|
|
1056
|
+
style.dataset.pluginCss = STYLE_TAG_ID;
|
|
1057
|
+
style.textContent = THINK_BUTTON_CSS;
|
|
1058
|
+
document.head?.appendChild(style);
|
|
1059
|
+
}
|
|
1060
|
+
function createThinkButton(onActivate) {
|
|
1061
|
+
ensureThinkButtonStyles();
|
|
1062
|
+
const element = document.createElement("button");
|
|
1063
|
+
element.type = "button";
|
|
1064
|
+
element.className = THINK_BUTTON_CLASS;
|
|
1065
|
+
element.textContent = "\u8BD1";
|
|
1066
|
+
element.dataset.state = "idle";
|
|
1067
|
+
element.setAttribute("aria-label", "\u7FFB\u8BD1\u601D\u8003\u94FE");
|
|
1068
|
+
element.title = "\u7FFB\u8BD1\u601D\u8003\u94FE";
|
|
1069
|
+
element.addEventListener("click", (event) => {
|
|
1070
|
+
event.stopPropagation();
|
|
1071
|
+
event.preventDefault();
|
|
1072
|
+
if (element.disabled) return;
|
|
1073
|
+
onActivate();
|
|
1074
|
+
});
|
|
1075
|
+
const setState = (state) => {
|
|
1076
|
+
element.dataset.state = state;
|
|
1077
|
+
const label = state === "translated" ? "\u663E\u793A\u601D\u8003\u539F\u6587" : "\u7FFB\u8BD1\u601D\u8003\u94FE";
|
|
1078
|
+
element.setAttribute("aria-label", label);
|
|
1079
|
+
element.title = label;
|
|
1080
|
+
};
|
|
1081
|
+
return {
|
|
1082
|
+
element,
|
|
1083
|
+
getState: () => element.dataset.state ?? "idle",
|
|
1084
|
+
setState,
|
|
1085
|
+
setDisabled: (disabled, reason) => {
|
|
1086
|
+
element.disabled = disabled;
|
|
1087
|
+
if (disabled) element.title = reason;
|
|
1088
|
+
else setState(element.dataset.state ?? "idle");
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/client/translate/think.ts
|
|
1094
|
+
var THINK_CARD_SELECTOR = '[data-variant="think"]';
|
|
1095
|
+
var THINK_BODY_SELECTOR = '[class*="thinkBody" i]';
|
|
1096
|
+
var EXPANDED_SELECTOR = "[data-open]";
|
|
1097
|
+
var BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
1098
|
+
"P",
|
|
1099
|
+
"H1",
|
|
1100
|
+
"H2",
|
|
1101
|
+
"H3",
|
|
1102
|
+
"H4",
|
|
1103
|
+
"H5",
|
|
1104
|
+
"H6",
|
|
1105
|
+
"LI",
|
|
1106
|
+
"BLOCKQUOTE",
|
|
1107
|
+
"TD",
|
|
1108
|
+
"TH",
|
|
1109
|
+
"DT",
|
|
1110
|
+
"DD",
|
|
1111
|
+
"FIGCAPTION"
|
|
1112
|
+
]);
|
|
1113
|
+
var CONTAINER_TAGS = /* @__PURE__ */ new Set(["UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR", "DIV", "SECTION"]);
|
|
1114
|
+
var NESTED_BOUNDARY_SELECTOR = [...BLOCK_TAGS, ...CONTAINER_TAGS, "PRE"].join(",").toLowerCase();
|
|
1115
|
+
var REQUEST_TOKENS = 2048;
|
|
1116
|
+
var SYNC_DEBOUNCE_MS = 120;
|
|
1117
|
+
function isMountedElement(element) {
|
|
1118
|
+
return element.dataset?.tidyTranslated === "true";
|
|
1119
|
+
}
|
|
1120
|
+
function hasNestedBoundary(element) {
|
|
1121
|
+
return element.querySelector(NESTED_BOUNDARY_SELECTOR) !== null;
|
|
1122
|
+
}
|
|
1123
|
+
function collectThinkUnits(body) {
|
|
1124
|
+
const units = [];
|
|
1125
|
+
const pushRun = (nodes) => {
|
|
1126
|
+
if (nodes.length === 0) return;
|
|
1127
|
+
const text = nodes.map((node) => node.textContent ?? "").join("").trim();
|
|
1128
|
+
if (!text) return;
|
|
1129
|
+
const anchor = nodes[0];
|
|
1130
|
+
if (anchor === void 0) return;
|
|
1131
|
+
units.push({ nodes, text, anchor });
|
|
1132
|
+
};
|
|
1133
|
+
const walk = (container) => {
|
|
1134
|
+
let run = [];
|
|
1135
|
+
const flush = () => {
|
|
1136
|
+
pushRun(run);
|
|
1137
|
+
run = [];
|
|
1138
|
+
};
|
|
1139
|
+
for (const node of Array.from(container.childNodes)) {
|
|
1140
|
+
if (node.nodeType !== 1) {
|
|
1141
|
+
run.push(node);
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
const element = node;
|
|
1145
|
+
if (NonDestructiveTranslationMount.isOwnNode(element)) {
|
|
1146
|
+
flush();
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
if (element.tagName === "PRE") {
|
|
1150
|
+
flush();
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
if (BLOCK_TAGS.has(element.tagName)) {
|
|
1154
|
+
flush();
|
|
1155
|
+
collectBlock(element);
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (CONTAINER_TAGS.has(element.tagName)) {
|
|
1159
|
+
flush();
|
|
1160
|
+
walk(element);
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
run.push(element);
|
|
1164
|
+
}
|
|
1165
|
+
flush();
|
|
1166
|
+
};
|
|
1167
|
+
const collectBlock = (element) => {
|
|
1168
|
+
if (isMountedElement(element)) return;
|
|
1169
|
+
if (!hasNestedBoundary(element)) {
|
|
1170
|
+
const text = (element.textContent ?? "").trim();
|
|
1171
|
+
if (text) units.push({ element, text, anchor: element });
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
walk(element);
|
|
1175
|
+
};
|
|
1176
|
+
walk(body);
|
|
1177
|
+
return units;
|
|
1178
|
+
}
|
|
1179
|
+
function chunkUnits(units, maxTokens = REQUEST_TOKENS) {
|
|
1180
|
+
const chunks = [];
|
|
1181
|
+
let current = [];
|
|
1182
|
+
let tokens = 0;
|
|
1183
|
+
for (const unit of units) {
|
|
1184
|
+
const cost = Math.max(estimateTokens(unit.text), 1);
|
|
1185
|
+
if (current.length > 0 && tokens + cost > maxTokens) {
|
|
1186
|
+
chunks.push(current);
|
|
1187
|
+
current = [];
|
|
1188
|
+
tokens = 0;
|
|
1189
|
+
}
|
|
1190
|
+
current.push(unit);
|
|
1191
|
+
tokens += cost;
|
|
1192
|
+
}
|
|
1193
|
+
if (current.length > 0) chunks.push(current);
|
|
1194
|
+
return chunks;
|
|
1195
|
+
}
|
|
1196
|
+
var ThinkTranslateController = class {
|
|
1197
|
+
/** 总开关与设置里的思考链开关同时打开。 */
|
|
1198
|
+
enabled = false;
|
|
1199
|
+
/** AI 通道开关打开且已配置;缺任何一条都不注入按钮。 */
|
|
1200
|
+
configured = false;
|
|
1201
|
+
states = /* @__PURE__ */ new WeakMap();
|
|
1202
|
+
syncTimers = /* @__PURE__ */ new WeakMap();
|
|
1203
|
+
/** 只处理当前会话的容器;由观察器在会话切换时更新。 */
|
|
1204
|
+
scope = null;
|
|
1205
|
+
/** 观察器把当前会话的根交给控制器,插入按钮时只看这一棵子树。 */
|
|
1206
|
+
setScope(scope) {
|
|
1207
|
+
this.scope = scope;
|
|
1208
|
+
}
|
|
1209
|
+
isEnabled() {
|
|
1210
|
+
return this.enabled;
|
|
1211
|
+
}
|
|
1212
|
+
setEnabled(enabled) {
|
|
1213
|
+
if (enabled === this.enabled) return;
|
|
1214
|
+
this.enabled = enabled;
|
|
1215
|
+
this.applyVisibility();
|
|
1216
|
+
}
|
|
1217
|
+
setConfigured(configured) {
|
|
1218
|
+
if (configured === this.configured) return;
|
|
1219
|
+
this.configured = configured;
|
|
1220
|
+
this.applyVisibility();
|
|
1221
|
+
}
|
|
1222
|
+
/** 按钮是否可以存在:两个开关与 AI 通道配置缺一不可。 */
|
|
1223
|
+
usable() {
|
|
1224
|
+
return this.enabled && this.configured;
|
|
1225
|
+
}
|
|
1226
|
+
applyVisibility() {
|
|
1227
|
+
if (typeof document === "undefined") return;
|
|
1228
|
+
if (this.usable()) {
|
|
1229
|
+
this.syncAll();
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
for (const element of document.querySelectorAll("." + THINK_BUTTON_CLASS)) {
|
|
1233
|
+
element.remove();
|
|
1234
|
+
}
|
|
1235
|
+
for (const card of document.querySelectorAll(THINK_CARD_SELECTOR)) {
|
|
1236
|
+
const state = this.states.get(card);
|
|
1237
|
+
if (state !== void 0) {
|
|
1238
|
+
state.button = null;
|
|
1239
|
+
state.state = "idle";
|
|
1240
|
+
state.expandedHandled = false;
|
|
1241
|
+
}
|
|
1242
|
+
NonDestructiveTranslationMount.restore(card);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
/** 扫描当前会话里的全部思考卡(开关、配置或会话变化后调用)。 */
|
|
1246
|
+
syncAll() {
|
|
1247
|
+
const scope = this.scope ?? (typeof document === "undefined" ? null : document);
|
|
1248
|
+
if (scope === null) return;
|
|
1249
|
+
for (const card of scope.querySelectorAll(THINK_CARD_SELECTOR)) {
|
|
1250
|
+
this.syncCard(card);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
/** 防抖后同步一张卡片。 */
|
|
1254
|
+
syncCard(card) {
|
|
1255
|
+
if (typeof window === "undefined") return;
|
|
1256
|
+
const existing = this.syncTimers.get(card);
|
|
1257
|
+
if (existing !== void 0) window.clearTimeout(existing);
|
|
1258
|
+
const timer = window.setTimeout(() => {
|
|
1259
|
+
this.syncTimers.delete(card);
|
|
1260
|
+
this.runSync(card);
|
|
1261
|
+
}, SYNC_DEBOUNCE_MS);
|
|
1262
|
+
this.syncTimers.set(card, timer);
|
|
1263
|
+
}
|
|
1264
|
+
stateFor(card) {
|
|
1265
|
+
let state = this.states.get(card);
|
|
1266
|
+
if (state === void 0) {
|
|
1267
|
+
state = { button: null, state: "idle", expandedHandled: false };
|
|
1268
|
+
this.states.set(card, state);
|
|
486
1269
|
}
|
|
1270
|
+
return state;
|
|
487
1271
|
}
|
|
488
|
-
|
|
489
|
-
if (!
|
|
490
|
-
const
|
|
491
|
-
if (
|
|
492
|
-
this.
|
|
1272
|
+
runSync(card) {
|
|
1273
|
+
if (!card.isConnected) return;
|
|
1274
|
+
const state = this.stateFor(card);
|
|
1275
|
+
if (!this.usable()) {
|
|
1276
|
+
this.discardCard(card, state);
|
|
493
1277
|
return;
|
|
494
1278
|
}
|
|
495
|
-
|
|
1279
|
+
const expanded = card.querySelector(EXPANDED_SELECTOR) !== null;
|
|
1280
|
+
if (expanded) this.ensureButton(card, state);
|
|
1281
|
+
else {
|
|
1282
|
+
this.removeButton(state);
|
|
1283
|
+
state.expandedHandled = false;
|
|
1284
|
+
}
|
|
1285
|
+
if (state.state !== "working") {
|
|
1286
|
+
state.state = this.isShowingTranslation(card) ? "translated" : "idle";
|
|
1287
|
+
}
|
|
1288
|
+
state.button?.setState(state.state);
|
|
1289
|
+
this.updateDisabled(card, state);
|
|
1290
|
+
if (!expanded || state.expandedHandled || state.state === "working") return;
|
|
1291
|
+
state.expandedHandled = true;
|
|
1292
|
+
this.showCachedWhenComplete(card, state);
|
|
1293
|
+
}
|
|
1294
|
+
updateDisabled(card, state) {
|
|
1295
|
+
const running = card.dataset.state === "running";
|
|
1296
|
+
state.button?.setDisabled(running && state.state !== "working", "\u601D\u8003\u8F93\u51FA\u4E2D\uFF0C\u7ED3\u675F\u540E\u53EF\u7FFB\u8BD1");
|
|
1297
|
+
}
|
|
1298
|
+
ensureButton(card, state) {
|
|
1299
|
+
if (state.button !== null) return;
|
|
1300
|
+
const row = card.querySelector("[data-disclosure-row]");
|
|
1301
|
+
if (row === null) return;
|
|
1302
|
+
const button = createThinkButton(() => this.activate(card));
|
|
1303
|
+
const title = row.querySelector('[class*="title" i]');
|
|
1304
|
+
if (title !== null && title.parentElement === row) {
|
|
1305
|
+
row.insertBefore(button.element, title.nextSibling);
|
|
1306
|
+
} else {
|
|
1307
|
+
row.appendChild(button.element);
|
|
1308
|
+
}
|
|
1309
|
+
state.button = button;
|
|
1310
|
+
button.setState(state.state);
|
|
1311
|
+
}
|
|
1312
|
+
/** 折叠或停用时撤掉按钮,卡片的翻译状态不变。 */
|
|
1313
|
+
removeButton(state) {
|
|
1314
|
+
state.button?.element.remove();
|
|
1315
|
+
state.button = null;
|
|
1316
|
+
}
|
|
1317
|
+
/** 插件停用:撤掉按钮并还原这张卡片上的全部译文。 */
|
|
1318
|
+
discardCard(card, state) {
|
|
1319
|
+
this.removeButton(state);
|
|
1320
|
+
state.state = "idle";
|
|
1321
|
+
state.expandedHandled = false;
|
|
1322
|
+
NonDestructiveTranslationMount.restore(card);
|
|
1323
|
+
}
|
|
1324
|
+
/** 卡片当前显示的是译文(至少有一个译文块可见)。 */
|
|
1325
|
+
isShowingTranslation(card) {
|
|
1326
|
+
const blocks = card.querySelectorAll("." + CLASS_TRANSLATED_BLOCK);
|
|
1327
|
+
for (const block of blocks) {
|
|
1328
|
+
if (block.style.display !== "none") return true;
|
|
1329
|
+
}
|
|
1330
|
+
return false;
|
|
496
1331
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
1332
|
+
bodyOf(card) {
|
|
1333
|
+
return card.querySelector(THINK_BODY_SELECTOR);
|
|
1334
|
+
}
|
|
1335
|
+
/** 展开时若整条都命中缓存,直接显示中文,不点按钮也不发请求。 */
|
|
1336
|
+
showCachedWhenComplete(card, state) {
|
|
1337
|
+
const body = this.bodyOf(card);
|
|
1338
|
+
if (body === null || state.state === "working") return;
|
|
1339
|
+
const units = collectThinkUnits(body);
|
|
1340
|
+
if (units.length === 0) return;
|
|
1341
|
+
const cached = units.map((unit) => thinkClientCache.get(unit.text));
|
|
1342
|
+
if (cached.some((value) => value === void 0)) return;
|
|
1343
|
+
units.forEach((unit, index) => this.applyUnit(unit, cached[index]));
|
|
1344
|
+
state.state = this.isShowingTranslation(card) ? "translated" : "idle";
|
|
1345
|
+
state.button?.setState(state.state);
|
|
1346
|
+
}
|
|
1347
|
+
/** 按钮被点击;按钮只在展开的卡片上存在。 */
|
|
1348
|
+
activate(card) {
|
|
1349
|
+
const state = this.stateFor(card);
|
|
1350
|
+
if (!this.usable() || state.state === "working") return;
|
|
1351
|
+
if (card.dataset.state === "running") return;
|
|
1352
|
+
if (state.state === "translated") {
|
|
1353
|
+
NonDestructiveTranslationMount.toggleGroup(card, true);
|
|
1354
|
+
state.state = "idle";
|
|
1355
|
+
state.button?.setState("idle");
|
|
1356
|
+
return;
|
|
515
1357
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1358
|
+
this.translateCard(card, state);
|
|
1359
|
+
}
|
|
1360
|
+
/** 翻译这张卡片的全部单位:命中缓存的先挂上,其余按块请求。 */
|
|
1361
|
+
translateCard(card, state) {
|
|
1362
|
+
const body = this.bodyOf(card);
|
|
1363
|
+
if (body === null) return;
|
|
1364
|
+
if (card.querySelector("." + CLASS_TRANSLATED_BLOCK) !== null) {
|
|
1365
|
+
NonDestructiveTranslationMount.toggleGroup(card, false);
|
|
1366
|
+
}
|
|
1367
|
+
const units = collectThinkUnits(body);
|
|
1368
|
+
if (units.length === 0) {
|
|
1369
|
+
state.state = this.isShowingTranslation(card) ? "translated" : "idle";
|
|
1370
|
+
state.button?.setState(state.state);
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const missing = [];
|
|
1374
|
+
for (const unit of units) {
|
|
1375
|
+
const cached = thinkClientCache.get(unit.text);
|
|
1376
|
+
if (cached === void 0) missing.push(unit);
|
|
1377
|
+
else this.applyUnit(unit, cached);
|
|
1378
|
+
}
|
|
1379
|
+
if (missing.length === 0) {
|
|
1380
|
+
state.state = this.isShowingTranslation(card) ? "translated" : "idle";
|
|
1381
|
+
state.button?.setState(state.state);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
state.state = "working";
|
|
1385
|
+
state.button?.setState("working");
|
|
1386
|
+
void this.translateUnits(card, state, missing);
|
|
1387
|
+
}
|
|
1388
|
+
async translateUnits(card, state, units) {
|
|
1389
|
+
let failed = false;
|
|
1390
|
+
for (const chunk of chunkUnits(units, REQUEST_TOKENS)) {
|
|
1391
|
+
if (!this.usable()) {
|
|
1392
|
+
failed = true;
|
|
1393
|
+
break;
|
|
1394
|
+
}
|
|
1395
|
+
const texts = chunk.map((unit) => unit.text);
|
|
1396
|
+
let results = [];
|
|
1397
|
+
try {
|
|
1398
|
+
results = await requestTranslateThink(texts);
|
|
1399
|
+
} catch {
|
|
1400
|
+
failed = true;
|
|
1401
|
+
}
|
|
1402
|
+
const byText = new Map(results.map((result) => [result.original, result]));
|
|
1403
|
+
for (const unit of chunk) {
|
|
1404
|
+
const result = byText.get(unit.text);
|
|
1405
|
+
if (result === void 0 || !result.ok || !result.translated.trim() || result.translated.trim() === unit.text.trim()) {
|
|
1406
|
+
failed = true;
|
|
1407
|
+
continue;
|
|
527
1408
|
}
|
|
528
|
-
|
|
529
|
-
|
|
1409
|
+
thinkClientCache.set(unit.text, result.translated);
|
|
1410
|
+
this.applyUnit(unit, result.translated);
|
|
530
1411
|
}
|
|
531
1412
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
1413
|
+
state.state = !failed && this.isShowingTranslation(card) ? "translated" : "idle";
|
|
1414
|
+
state.button?.setState(state.state);
|
|
1415
|
+
this.updateDisabled(card, state);
|
|
1416
|
+
}
|
|
1417
|
+
/** 把译文挂到单位上;期间文本或节点已经变动的单位直接放弃(结果已进缓存)。 */
|
|
1418
|
+
applyUnit(unit, translated) {
|
|
1419
|
+
if (unit.element !== void 0) {
|
|
1420
|
+
const element = unit.element;
|
|
1421
|
+
if (!element.isConnected || isMountedElement(element)) return;
|
|
1422
|
+
if ((element.textContent ?? "").trim() !== unit.text) return;
|
|
1423
|
+
NonDestructiveTranslationMount.mount(element, translated, {
|
|
1424
|
+
originalText: unit.text,
|
|
1425
|
+
interactive: false
|
|
1426
|
+
});
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
const nodes = unit.nodes ?? [];
|
|
1430
|
+
if (nodes.length === 0 || nodes.some((node) => !node.isConnected)) return;
|
|
1431
|
+
const parent = nodes[0]?.parentNode ?? null;
|
|
1432
|
+
if (parent === null || !nodes.every((node) => node.parentNode === parent)) return;
|
|
1433
|
+
const text = nodes.map((node) => node.textContent ?? "").join("").trim();
|
|
1434
|
+
if (text !== unit.text) return;
|
|
1435
|
+
NonDestructiveTranslationMount.mountRun(nodes, translated, {
|
|
1436
|
+
originalText: unit.text,
|
|
1437
|
+
interactive: false
|
|
537
1438
|
});
|
|
538
1439
|
}
|
|
539
|
-
disconnect() {
|
|
540
|
-
this.viewportObserver.disconnect();
|
|
541
|
-
}
|
|
542
1440
|
};
|
|
543
|
-
var
|
|
1441
|
+
var thinkTranslator = new ThinkTranslateController();
|
|
544
1442
|
|
|
545
1443
|
// src/client/translate/observer.ts
|
|
546
1444
|
var TOOL_TITLE_SELECTOR = '[class*="summary" i]';
|
|
1445
|
+
var THINK_CARD_SELECTOR2 = '[data-variant="think"]';
|
|
547
1446
|
var SESSION_ROOT_SELECTOR = "[data-conversation-scroll], [data-chat-flow]";
|
|
548
1447
|
var ROOT_CHECK_INTERVAL_MS = 3e3;
|
|
1448
|
+
var SUMMARY_SUFFIX_PATTERN = /suffix/i;
|
|
1449
|
+
var LIVE_TURN_SELECTOR = '[role="status"][aria-live="polite"]';
|
|
1450
|
+
var ROW_ROOT_SELECTOR = "[data-variant]";
|
|
1451
|
+
var FLOW_ITEM_SELECTOR2 = "[data-chat-flow-key]";
|
|
1452
|
+
var FLOW_SELECTOR2 = "[data-chat-flow]";
|
|
1453
|
+
function isSummarySuffixSpan(span) {
|
|
1454
|
+
return SUMMARY_SUFFIX_PATTERN.test(span.className || "");
|
|
1455
|
+
}
|
|
549
1456
|
function isToolSummarySpan(span) {
|
|
550
1457
|
if (!span || span.nodeType !== 1) return false;
|
|
551
1458
|
if (span.hasAttribute("aria-hidden")) return false;
|
|
1459
|
+
if (span.closest(THINK_CARD_SELECTOR2)) return false;
|
|
552
1460
|
if (span.querySelector?.('[class*="title"], [class*="leading"], [class*="chevron"], [class*="sep"], [class*="summary" i]')) {
|
|
553
1461
|
return false;
|
|
554
1462
|
}
|
|
@@ -569,24 +1477,36 @@ function isToolSummarySpan(span) {
|
|
|
569
1477
|
}
|
|
570
1478
|
return false;
|
|
571
1479
|
}
|
|
572
|
-
function thinkBlockStillRunning(span) {
|
|
573
|
-
const thinkRoot = span.closest('[data-variant="think"]');
|
|
574
|
-
return !!thinkRoot && thinkRoot.dataset.state === "running";
|
|
575
|
-
}
|
|
576
1480
|
var ChatTranslateObserver = class {
|
|
577
1481
|
observer = null;
|
|
578
1482
|
rootElement = null;
|
|
579
1483
|
rootCheckTimer = null;
|
|
580
1484
|
isEnabled = true;
|
|
1485
|
+
/** 设置里的思考链开关;与总开关一起决定按钮是否存在。 */
|
|
1486
|
+
thinkEnabled = false;
|
|
1487
|
+
/**
|
|
1488
|
+
* 至少一条工具标题通道可用(AI 配置齐全或 Bing 打开)。两条都关时没有
|
|
1489
|
+
* 可等的译文,扣留立即失去意义。
|
|
1490
|
+
*/
|
|
1491
|
+
channelsAvailable = true;
|
|
1492
|
+
/**
|
|
1493
|
+
* 每个会话流已经见过的末尾行。追加在它之后的行算「新出现」,插在它之前
|
|
1494
|
+
* 的行(历史回放、加载更早)不算。
|
|
1495
|
+
*/
|
|
1496
|
+
flowTail = /* @__PURE__ */ new WeakMap();
|
|
1497
|
+
/** 初始扫描期间不扣留:那时看到的行都是历史。 */
|
|
1498
|
+
suppressHold = false;
|
|
581
1499
|
constructor() {
|
|
582
1500
|
this.handleMutations = this.handleMutations.bind(this);
|
|
583
1501
|
}
|
|
584
1502
|
setEnabled(enabled) {
|
|
585
1503
|
this.isEnabled = enabled;
|
|
1504
|
+
thinkTranslator.setEnabled(enabled && this.thinkEnabled);
|
|
586
1505
|
if (enabled) {
|
|
587
1506
|
lazyQueue.setEnabled(true);
|
|
588
1507
|
this.start();
|
|
589
1508
|
} else {
|
|
1509
|
+
rowHold.releaseAll();
|
|
590
1510
|
this.restoreOriginals();
|
|
591
1511
|
this.disconnect();
|
|
592
1512
|
lazyQueue.setEnabled(false);
|
|
@@ -594,10 +1514,26 @@ var ChatTranslateObserver = class {
|
|
|
594
1514
|
}
|
|
595
1515
|
restoreOriginals() {
|
|
596
1516
|
const scope = this.rootElement ?? document;
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
1517
|
+
NonDestructiveTranslationMount.restore(scope);
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* 设置里的思考链开关:关掉就撤掉所有翻译按钮并还原思考卡里的译文,工具标题
|
|
1521
|
+
* 译文不动。
|
|
1522
|
+
*/
|
|
1523
|
+
setThinkEnabled(enabled) {
|
|
1524
|
+
this.thinkEnabled = enabled;
|
|
1525
|
+
thinkTranslator.setEnabled(this.isEnabled && enabled);
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* AI 通道的可用性(通道开关打开且 Key / Base URL / 模型齐全)。不可用时同样
|
|
1529
|
+
* 不注入按钮:思考链翻译只能走这条通道。
|
|
1530
|
+
*/
|
|
1531
|
+
setThinkConfigured(configured) {
|
|
1532
|
+
thinkTranslator.setConfigured(configured);
|
|
1533
|
+
}
|
|
1534
|
+
/** 工具标题通道可用性:两条都关时不扣留,也没有译文可等。 */
|
|
1535
|
+
setChannelsAvailable(available) {
|
|
1536
|
+
this.channelsAvailable = available;
|
|
601
1537
|
}
|
|
602
1538
|
/**
|
|
603
1539
|
* The active session's scroll container. Only the currently-viewed session
|
|
@@ -633,7 +1569,14 @@ var ChatTranslateObserver = class {
|
|
|
633
1569
|
const findAndObserveRoot = () => {
|
|
634
1570
|
const root = this.findRoot(documentRef);
|
|
635
1571
|
this.rootElement = root;
|
|
636
|
-
|
|
1572
|
+
thinkTranslator.setScope(root);
|
|
1573
|
+
this.suppressHold = true;
|
|
1574
|
+
try {
|
|
1575
|
+
this.scanContainer(root);
|
|
1576
|
+
} finally {
|
|
1577
|
+
this.suppressHold = false;
|
|
1578
|
+
}
|
|
1579
|
+
this.scanThink(root);
|
|
637
1580
|
if (!this.observer) {
|
|
638
1581
|
this.observer = new MutationObserver(this.handleMutations);
|
|
639
1582
|
this.observer.observe(root, {
|
|
@@ -641,7 +1584,15 @@ var ChatTranslateObserver = class {
|
|
|
641
1584
|
subtree: true,
|
|
642
1585
|
attributes: true,
|
|
643
1586
|
characterData: true,
|
|
644
|
-
attributeFilter: [
|
|
1587
|
+
attributeFilter: [
|
|
1588
|
+
"data-state",
|
|
1589
|
+
"data-tool",
|
|
1590
|
+
"data-variant",
|
|
1591
|
+
"data-sample",
|
|
1592
|
+
"aria-expanded",
|
|
1593
|
+
"data-open",
|
|
1594
|
+
"data-expanded"
|
|
1595
|
+
]
|
|
645
1596
|
});
|
|
646
1597
|
}
|
|
647
1598
|
};
|
|
@@ -672,34 +1623,115 @@ var ChatTranslateObserver = class {
|
|
|
672
1623
|
if (!this.isEnabled) return;
|
|
673
1624
|
for (const mutation of mutations) {
|
|
674
1625
|
if (mutation.type === "childList") {
|
|
1626
|
+
let addedElement = false;
|
|
1627
|
+
let changedOwner = null;
|
|
675
1628
|
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
|
676
1629
|
const node = mutation.addedNodes[i];
|
|
677
1630
|
if (node instanceof HTMLElement) {
|
|
678
|
-
|
|
1631
|
+
const owner = this.mountedOwner(node);
|
|
1632
|
+
if (owner) {
|
|
1633
|
+
changedOwner = owner;
|
|
679
1634
|
continue;
|
|
680
1635
|
}
|
|
1636
|
+
if (NonDestructiveTranslationMount.isOwnNode(node)) continue;
|
|
1637
|
+
addedElement = true;
|
|
1638
|
+
this.noteRevealHold(node);
|
|
681
1639
|
this.scanNode(node);
|
|
1640
|
+
this.scanThink(node);
|
|
682
1641
|
}
|
|
683
1642
|
}
|
|
684
|
-
} else if (mutation.type === "attributes") {
|
|
685
1643
|
const target = mutation.target;
|
|
686
1644
|
if (target instanceof HTMLElement) {
|
|
687
|
-
|
|
688
|
-
|
|
1645
|
+
const owner = this.mountedOwner(target);
|
|
1646
|
+
if (owner) {
|
|
1647
|
+
changedOwner = owner;
|
|
1648
|
+
} else if (!addedElement && !NonDestructiveTranslationMount.isOwnNode(target)) {
|
|
1649
|
+
this.scanNode(target);
|
|
1650
|
+
this.scanThink(target);
|
|
689
1651
|
}
|
|
1652
|
+
}
|
|
1653
|
+
if (changedOwner) this.handleOriginalTextChange(changedOwner);
|
|
1654
|
+
} else if (mutation.type === "attributes") {
|
|
1655
|
+
const target = mutation.target;
|
|
1656
|
+
if (target instanceof HTMLElement) {
|
|
1657
|
+
if (NonDestructiveTranslationMount.isOwnNode(target)) continue;
|
|
690
1658
|
this.scanNode(target);
|
|
1659
|
+
this.scanThink(target);
|
|
691
1660
|
}
|
|
692
1661
|
} else if (mutation.type === "characterData") {
|
|
1662
|
+
const owner = this.mountedOwner(mutation.target);
|
|
1663
|
+
if (owner) {
|
|
1664
|
+
this.handleOriginalTextChange(owner);
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
693
1667
|
const parent = mutation.target.parentElement;
|
|
694
1668
|
if (parent instanceof HTMLElement) {
|
|
695
|
-
if (
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
1669
|
+
if (NonDestructiveTranslationMount.isOwnNode(parent)) continue;
|
|
698
1670
|
this.scanNode(parent);
|
|
1671
|
+
this.scanThink(parent);
|
|
699
1672
|
}
|
|
700
1673
|
}
|
|
701
1674
|
}
|
|
702
1675
|
}
|
|
1676
|
+
/**
|
|
1677
|
+
* 扣留期间新出现的会话流内容(下一个回答节点、下一个工具行)同样要先挂起:
|
|
1678
|
+
* 它们排在还没上屏的行后面,先出现就会占住那一行的位置。
|
|
1679
|
+
*/
|
|
1680
|
+
noteRevealHold(node) {
|
|
1681
|
+
rowHold.noteAppended(node);
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* 变更发生在我们移进隐藏容器的原文里时,返回挂载了译文的那个元素。React
|
|
1685
|
+
* 就地改写它自己创建的文本节点,所以「原文变了」只能从这条路径看到。
|
|
1686
|
+
*/
|
|
1687
|
+
mountedOwner(node) {
|
|
1688
|
+
const element = node instanceof HTMLElement ? node : node.parentElement;
|
|
1689
|
+
if (!element) return null;
|
|
1690
|
+
const wrapper = element.closest(
|
|
1691
|
+
`.${CLASS_ORIGINAL_HIDDEN}, .${CLASS_ORIGINAL_SHOWN}`
|
|
1692
|
+
);
|
|
1693
|
+
if (!wrapper) return null;
|
|
1694
|
+
const owner = wrapper.parentElement;
|
|
1695
|
+
return owner instanceof HTMLElement && owner.dataset.tidyTranslated === "true" ? owner : null;
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* 已挂载行的隐藏原文被上游改写了(ask_user 计数、失败摘要、todo 计数):
|
|
1699
|
+
* 命中缓存就原地换译文,否则请求新译文后原地换。行始终可见,不出现英文。
|
|
1700
|
+
*/
|
|
1701
|
+
handleOriginalTextChange(owner) {
|
|
1702
|
+
if (!isToolSummarySpan(owner)) return;
|
|
1703
|
+
const current = NonDestructiveTranslationMount.extractVisibleText(owner);
|
|
1704
|
+
if (!current || current === owner.dataset.original) return;
|
|
1705
|
+
const cached = clientCache.get(current);
|
|
1706
|
+
if (cached) {
|
|
1707
|
+
NonDestructiveTranslationMount.mount(owner, cached, { originalText: current });
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
lazyQueue.observe(owner, current);
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* 该行是否该被扣留:当前 turn 仍在产出、这行是新追加到流末尾的、不是后缀、
|
|
1714
|
+
* 通道可用,且不在初始扫描期间。判断顺带把每个会话流见过的末尾行推进到
|
|
1715
|
+
* 最新,供后续判定使用;第一次见到的会话流先按历史处理。
|
|
1716
|
+
*/
|
|
1717
|
+
shouldHold(span) {
|
|
1718
|
+
if (isSummarySuffixSpan(span)) return false;
|
|
1719
|
+
const flow = span.closest(FLOW_SELECTOR2);
|
|
1720
|
+
if (!flow) return false;
|
|
1721
|
+
const item = span.closest(FLOW_ITEM_SELECTOR2);
|
|
1722
|
+
if (!item) return false;
|
|
1723
|
+
const known = this.flowTail.get(flow);
|
|
1724
|
+
if (known === void 0) {
|
|
1725
|
+
const items = flow.querySelectorAll(FLOW_ITEM_SELECTOR2);
|
|
1726
|
+
this.flowTail.set(flow, items[items.length - 1] ?? item);
|
|
1727
|
+
return false;
|
|
1728
|
+
}
|
|
1729
|
+
const appended = known === item || (known.compareDocumentPosition(item) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
|
|
1730
|
+
if (appended) this.flowTail.set(flow, item);
|
|
1731
|
+
if (this.suppressHold || !appended) return false;
|
|
1732
|
+
if (!this.channelsAvailable) return false;
|
|
1733
|
+
return flow.querySelector(LIVE_TURN_SELECTOR) !== null;
|
|
1734
|
+
}
|
|
703
1735
|
scanContainer(container) {
|
|
704
1736
|
const spans = container.querySelectorAll(TOOL_TITLE_SELECTOR);
|
|
705
1737
|
spans.forEach((span) => {
|
|
@@ -719,18 +1751,34 @@ var ChatTranslateObserver = class {
|
|
|
719
1751
|
}
|
|
720
1752
|
});
|
|
721
1753
|
}
|
|
1754
|
+
/**
|
|
1755
|
+
* 交给思考正文翻译控制器:命中节点所在的卡片,以及它下面(或它就是)的
|
|
1756
|
+
* 全部思考卡片。
|
|
1757
|
+
*/
|
|
1758
|
+
scanThink(node) {
|
|
1759
|
+
if (!thinkTranslator.isEnabled()) return;
|
|
1760
|
+
if (node instanceof HTMLElement) {
|
|
1761
|
+
const card = node.closest(THINK_CARD_SELECTOR2);
|
|
1762
|
+
if (card) thinkTranslator.syncCard(card);
|
|
1763
|
+
}
|
|
1764
|
+
if (!node.querySelectorAll) return;
|
|
1765
|
+
node.querySelectorAll(THINK_CARD_SELECTOR2).forEach((card) => {
|
|
1766
|
+
thinkTranslator.syncCard(card);
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
722
1769
|
processSpan(span) {
|
|
723
|
-
if (thinkBlockStillRunning(span)) return;
|
|
724
1770
|
if (NonDestructiveTranslationMount.isMounted(span)) {
|
|
725
|
-
const original = NonDestructiveTranslationMount.getOriginal(span);
|
|
726
|
-
if (original) {
|
|
727
|
-
const cached2 = clientCache.get(original);
|
|
728
|
-
if (cached2) return;
|
|
729
|
-
}
|
|
730
1771
|
return;
|
|
731
1772
|
}
|
|
732
1773
|
const text = NonDestructiveTranslationMount.extractVisibleText(span);
|
|
733
1774
|
if (!text) return;
|
|
1775
|
+
const root = span.closest(ROW_ROOT_SELECTOR);
|
|
1776
|
+
const held = rowHold.stateFor(span) !== void 0;
|
|
1777
|
+
if (root && (held || this.shouldHold(span))) {
|
|
1778
|
+
rowHold.hold(span, root, text);
|
|
1779
|
+
this.translateHeld(span, text);
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
734
1782
|
const cached = clientCache.get(text);
|
|
735
1783
|
if (cached) {
|
|
736
1784
|
NonDestructiveTranslationMount.mount(span, cached, {
|
|
@@ -740,6 +1788,19 @@ var ChatTranslateObserver = class {
|
|
|
740
1788
|
}
|
|
741
1789
|
lazyQueue.observe(span, text);
|
|
742
1790
|
}
|
|
1791
|
+
/**
|
|
1792
|
+
* 扣留行的取文本路径:命中缓存立即就绪,其余交给懒加载队列。译文真正挂载要等
|
|
1793
|
+
* 队列轮到它——提前挂上去会把逐字与对数淡入在被挂起期间跑完。
|
|
1794
|
+
*/
|
|
1795
|
+
translateHeld(span, text) {
|
|
1796
|
+
if (rowHold.stateFor(span)?.ready === true) return;
|
|
1797
|
+
const cached = clientCache.get(text);
|
|
1798
|
+
if (cached) {
|
|
1799
|
+
rowHold.ready(span, cached);
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
lazyQueue.observeHeld(span, text);
|
|
1803
|
+
}
|
|
743
1804
|
disconnect() {
|
|
744
1805
|
if (this.rootCheckTimer !== null) {
|
|
745
1806
|
clearInterval(this.rootCheckTimer);
|
|
@@ -749,6 +1810,7 @@ var ChatTranslateObserver = class {
|
|
|
749
1810
|
this.observer.disconnect();
|
|
750
1811
|
this.observer = null;
|
|
751
1812
|
}
|
|
1813
|
+
rowHold.releaseAll();
|
|
752
1814
|
lazyQueue.disconnect();
|
|
753
1815
|
this.rootElement = null;
|
|
754
1816
|
}
|
|
@@ -757,8 +1819,11 @@ var chatTranslateObserver = new ChatTranslateObserver();
|
|
|
757
1819
|
|
|
758
1820
|
// src/client/settings/ui.tsx
|
|
759
1821
|
var import_react2 = require("react");
|
|
1822
|
+
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
760
1823
|
|
|
761
1824
|
// src/client/settings/store.ts
|
|
1825
|
+
var THINK_TIMEOUT_MIN = 500;
|
|
1826
|
+
var THINK_TIMEOUT_MAX = 9e5;
|
|
762
1827
|
var SETTINGS_NAMESPACE = "dsh-chat-translate";
|
|
763
1828
|
var TRANSLATE_API_KEY_REF = "TRANSLATE_API_KEY";
|
|
764
1829
|
var DEFAULT_STATE = {
|
|
@@ -766,6 +1831,8 @@ var DEFAULT_STATE = {
|
|
|
766
1831
|
concurrency: 3,
|
|
767
1832
|
aiEnabled: true,
|
|
768
1833
|
bingEnabled: true,
|
|
1834
|
+
thinkEnabled: true,
|
|
1835
|
+
thinkTimeoutMs: 6e5,
|
|
769
1836
|
baseUrl: "",
|
|
770
1837
|
model: "",
|
|
771
1838
|
aiConfigured: false
|
|
@@ -819,6 +1886,14 @@ var SettingsStore = class {
|
|
|
819
1886
|
}
|
|
820
1887
|
if (typeof value.aiEnabled === "boolean") next.aiEnabled = value.aiEnabled;
|
|
821
1888
|
if (typeof value.bingEnabled === "boolean") next.bingEnabled = value.bingEnabled;
|
|
1889
|
+
if (typeof value.thinkEnabled === "boolean") next.thinkEnabled = value.thinkEnabled;
|
|
1890
|
+
const thinkTimeout = value.thinkTimeoutMs;
|
|
1891
|
+
if (typeof thinkTimeout === "number" && Number.isFinite(thinkTimeout)) {
|
|
1892
|
+
next.thinkTimeoutMs = Math.min(
|
|
1893
|
+
Math.max(Math.round(thinkTimeout), THINK_TIMEOUT_MIN),
|
|
1894
|
+
THINK_TIMEOUT_MAX
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
822
1897
|
if (typeof value.baseUrl === "string") next.baseUrl = value.baseUrl;
|
|
823
1898
|
if (typeof value.model === "string") next.model = value.model;
|
|
824
1899
|
this.applyState(next);
|
|
@@ -833,6 +1908,17 @@ var SettingsStore = class {
|
|
|
833
1908
|
} catch {
|
|
834
1909
|
}
|
|
835
1910
|
}
|
|
1911
|
+
try {
|
|
1912
|
+
chatTranslateObserver.setThinkEnabled(this.state.enabled && this.state.thinkEnabled);
|
|
1913
|
+
chatTranslateObserver.setThinkConfigured(this.state.aiEnabled && this.state.aiConfigured);
|
|
1914
|
+
} catch {
|
|
1915
|
+
}
|
|
1916
|
+
try {
|
|
1917
|
+
chatTranslateObserver.setChannelsAvailable(
|
|
1918
|
+
this.state.aiEnabled && this.state.aiConfigured || this.state.bingEnabled
|
|
1919
|
+
);
|
|
1920
|
+
} catch {
|
|
1921
|
+
}
|
|
836
1922
|
this.notify();
|
|
837
1923
|
}
|
|
838
1924
|
notify() {
|
|
@@ -872,14 +1958,31 @@ var SettingsStore = class {
|
|
|
872
1958
|
if (typeof partial.concurrency === "number" && Number.isFinite(partial.concurrency)) {
|
|
873
1959
|
sanitizedConcurrency = Math.min(Math.max(Math.round(partial.concurrency), 1), 100);
|
|
874
1960
|
}
|
|
1961
|
+
let sanitizedThinkTimeout = this.state.thinkTimeoutMs;
|
|
1962
|
+
if (typeof partial.thinkTimeoutMs === "number" && Number.isFinite(partial.thinkTimeoutMs)) {
|
|
1963
|
+
sanitizedThinkTimeout = Math.min(
|
|
1964
|
+
Math.max(Math.round(partial.thinkTimeoutMs), THINK_TIMEOUT_MIN),
|
|
1965
|
+
THINK_TIMEOUT_MAX
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
875
1968
|
const next = {
|
|
876
1969
|
...this.state,
|
|
877
1970
|
...partial,
|
|
878
|
-
concurrency: sanitizedConcurrency
|
|
1971
|
+
concurrency: sanitizedConcurrency,
|
|
1972
|
+
thinkTimeoutMs: sanitizedThinkTimeout
|
|
879
1973
|
};
|
|
880
1974
|
this.applyState(next);
|
|
881
1975
|
if (this.scope) {
|
|
882
|
-
const fields = [
|
|
1976
|
+
const fields = [
|
|
1977
|
+
"enabled",
|
|
1978
|
+
"concurrency",
|
|
1979
|
+
"aiEnabled",
|
|
1980
|
+
"bingEnabled",
|
|
1981
|
+
"thinkEnabled",
|
|
1982
|
+
"thinkTimeoutMs",
|
|
1983
|
+
"baseUrl",
|
|
1984
|
+
"model"
|
|
1985
|
+
];
|
|
883
1986
|
for (const field of fields) {
|
|
884
1987
|
if (partial[field] !== void 0) {
|
|
885
1988
|
this.pendingFields.add(field);
|
|
@@ -899,11 +2002,13 @@ var SettingsStore = class {
|
|
|
899
2002
|
if (!this.scope) return;
|
|
900
2003
|
const fields = [...this.pendingFields];
|
|
901
2004
|
this.pendingFields.clear();
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
2005
|
+
if (fields.length === 0) return;
|
|
2006
|
+
const ops = fields.map((field) => ({
|
|
2007
|
+
op: "set",
|
|
2008
|
+
path: [field],
|
|
2009
|
+
value: this.state[field]
|
|
2010
|
+
}));
|
|
2011
|
+
await this.scope.mutate(ops).catch(() => {
|
|
907
2012
|
});
|
|
908
2013
|
}
|
|
909
2014
|
async testChannel(channel) {
|
|
@@ -951,33 +2056,36 @@ var SETTINGS_CSS = String.raw`
|
|
|
951
2056
|
gap: 16px;
|
|
952
2057
|
max-width: 580px;
|
|
953
2058
|
padding-bottom: 32px;
|
|
954
|
-
color: var(--dsw-alias-label-primary
|
|
955
|
-
font
|
|
2059
|
+
color: var(--dsw-alias-label-primary);
|
|
2060
|
+
font: var(--dsw-font-s-14);
|
|
956
2061
|
}
|
|
957
2062
|
|
|
958
2063
|
.dsh-tidy-card {
|
|
959
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.2));
|
|
960
|
-
border-radius: 12px;
|
|
961
|
-
padding: 16px 18px;
|
|
962
|
-
background: var(--dsw-alias-bg-card, rgba(128, 128, 128, 0.05));
|
|
963
2064
|
display: flex;
|
|
964
2065
|
flex-direction: column;
|
|
965
2066
|
gap: 12px;
|
|
2067
|
+
padding: 16px 18px;
|
|
2068
|
+
border: 0.5px solid var(--dsw-alias-border-l2);
|
|
2069
|
+
border-radius: 12px;
|
|
2070
|
+
/* The shipped settings-card surface; Input's own bg-layer-1 then reads as a
|
|
2071
|
+
recessed field in the dark theme instead of vanishing into the card. */
|
|
2072
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
966
2073
|
}
|
|
967
2074
|
|
|
968
2075
|
.dsh-tidy-title {
|
|
969
|
-
font-size: 14px;
|
|
970
|
-
font-weight: 600;
|
|
971
|
-
color: var(--dsw-alias-label-primary, inherit);
|
|
972
2076
|
display: flex;
|
|
973
2077
|
align-items: center;
|
|
974
2078
|
justify-content: space-between;
|
|
2079
|
+
font: var(--dsw-font-s-strong-14);
|
|
975
2080
|
}
|
|
976
2081
|
|
|
977
2082
|
.dsh-tidy-desc {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
2083
|
+
color: var(--dsw-alias-label-secondary);
|
|
2084
|
+
font: var(--dsw-font-xxs-12);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
.dsh-tidy-desc code {
|
|
2088
|
+
font-family: var(--ds-font-family-code);
|
|
981
2089
|
}
|
|
982
2090
|
|
|
983
2091
|
.dsh-tidy-row {
|
|
@@ -989,7 +2097,7 @@ var SETTINGS_CSS = String.raw`
|
|
|
989
2097
|
}
|
|
990
2098
|
|
|
991
2099
|
.dsh-tidy-row + .dsh-tidy-row {
|
|
992
|
-
border-top:
|
|
2100
|
+
border-top: 0.5px solid var(--dsw-alias-border-l3);
|
|
993
2101
|
}
|
|
994
2102
|
|
|
995
2103
|
.dsh-tidy-row-info {
|
|
@@ -998,231 +2106,53 @@ var SETTINGS_CSS = String.raw`
|
|
|
998
2106
|
}
|
|
999
2107
|
|
|
1000
2108
|
.dsh-tidy-row-title {
|
|
1001
|
-
font
|
|
1002
|
-
font-weight: 500;
|
|
2109
|
+
font: var(--dsw-font-xs-strong-13);
|
|
1003
2110
|
}
|
|
1004
2111
|
|
|
1005
2112
|
.dsh-tidy-row-desc {
|
|
1006
|
-
font-size: 11px;
|
|
1007
|
-
color: var(--dsw-alias-label-secondary, rgba(128, 128, 128, 0.7));
|
|
1008
2113
|
margin-top: 2px;
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
/* Switch */
|
|
1013
|
-
.dsh-tidy-switch {
|
|
1014
|
-
position: relative;
|
|
1015
|
-
width: 38px;
|
|
1016
|
-
height: 22px;
|
|
1017
|
-
flex: none;
|
|
1018
|
-
cursor: pointer;
|
|
1019
|
-
border-radius: 999px;
|
|
1020
|
-
border: none;
|
|
1021
|
-
background: rgba(128, 128, 128, 0.3);
|
|
1022
|
-
transition: background 0.15s ease;
|
|
1023
|
-
padding: 0;
|
|
1024
|
-
outline: none;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
.dsh-tidy-switch[aria-checked="true"] {
|
|
1028
|
-
background: #3b82f6;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
.dsh-tidy-switch::after {
|
|
1032
|
-
content: "";
|
|
1033
|
-
position: absolute;
|
|
1034
|
-
top: 2px;
|
|
1035
|
-
left: 2px;
|
|
1036
|
-
width: 18px;
|
|
1037
|
-
height: 18px;
|
|
1038
|
-
border-radius: 50%;
|
|
1039
|
-
background: #ffffff;
|
|
1040
|
-
transition: transform 0.15s ease;
|
|
1041
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
.dsh-tidy-switch[aria-checked="true"]::after {
|
|
1045
|
-
transform: translateX(16px);
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
/* Inputs */
|
|
1049
|
-
.dsh-tidy-input-group {
|
|
1050
|
-
display: flex;
|
|
1051
|
-
flex-direction: column;
|
|
1052
|
-
gap: 6px;
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
.dsh-tidy-label {
|
|
1056
|
-
font-size: 12px;
|
|
1057
|
-
font-weight: 500;
|
|
1058
|
-
display: flex;
|
|
1059
|
-
align-items: center;
|
|
1060
|
-
justify-content: space-between;
|
|
2114
|
+
color: var(--dsw-alias-label-secondary);
|
|
2115
|
+
font: var(--dsw-font-xxxs-11);
|
|
1061
2116
|
}
|
|
1062
2117
|
|
|
2118
|
+
/* Field widths ride Input's wrapper span; the atom owns its own appearance. */
|
|
1063
2119
|
.dsh-tidy-input-row {
|
|
1064
2120
|
display: flex;
|
|
2121
|
+
align-items: center;
|
|
1065
2122
|
gap: 8px;
|
|
1066
2123
|
}
|
|
1067
2124
|
|
|
1068
|
-
.dsh-tidy-
|
|
1069
|
-
|
|
1070
|
-
min-width: 0;
|
|
1071
|
-
padding: 8px 12px;
|
|
1072
|
-
border-radius: 8px;
|
|
1073
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));
|
|
1074
|
-
background: var(--dsw-alias-bg-input, rgba(0, 0, 0, 0.05));
|
|
1075
|
-
color: inherit;
|
|
1076
|
-
font-size: 12px;
|
|
1077
|
-
outline: none;
|
|
1078
|
-
transition: border-color 0.15s;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
.dsh-tidy-input:focus {
|
|
1082
|
-
border-color: #3b82f6;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
.dsh-tidy-btn {
|
|
1086
|
-
padding: 6px 14px;
|
|
1087
|
-
border-radius: 8px;
|
|
1088
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.3));
|
|
1089
|
-
background: transparent;
|
|
1090
|
-
color: inherit;
|
|
1091
|
-
font-size: 12px;
|
|
1092
|
-
cursor: pointer;
|
|
1093
|
-
transition: all 0.15s ease;
|
|
1094
|
-
white-space: nowrap;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
.dsh-tidy-btn:hover {
|
|
1098
|
-
background: rgba(128, 128, 128, 0.1);
|
|
1099
|
-
border-color: rgba(128, 128, 128, 0.5);
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
.dsh-tidy-btn:disabled {
|
|
1103
|
-
opacity: 0.5;
|
|
1104
|
-
cursor: not-allowed;
|
|
2125
|
+
.dsh-tidy-field-wide {
|
|
2126
|
+
width: 260px;
|
|
1105
2127
|
}
|
|
1106
2128
|
|
|
1107
|
-
.dsh-tidy-
|
|
1108
|
-
|
|
1109
|
-
padding: 2px 6px;
|
|
1110
|
-
border-radius: 4px;
|
|
1111
|
-
font-weight: 500;
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
.dsh-tidy-badge-ok {
|
|
1115
|
-
background: rgba(34, 197, 94, 0.15);
|
|
1116
|
-
color: #22c55e;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
.dsh-tidy-badge-none {
|
|
1120
|
-
background: rgba(156, 163, 175, 0.15);
|
|
1121
|
-
color: #9ca3af;
|
|
2129
|
+
.dsh-tidy-field-timeout {
|
|
2130
|
+
width: 120px;
|
|
1122
2131
|
}
|
|
1123
2132
|
|
|
1124
|
-
.dsh-tidy-
|
|
1125
|
-
|
|
1126
|
-
color: #f59e0b;
|
|
2133
|
+
.dsh-tidy-field-count {
|
|
2134
|
+
width: 88px;
|
|
1127
2135
|
}
|
|
1128
2136
|
|
|
1129
2137
|
.dsh-tidy-test-result {
|
|
1130
|
-
font-size: 12px;
|
|
1131
2138
|
margin-left: 10px;
|
|
2139
|
+
font: var(--dsw-font-xxs-12);
|
|
1132
2140
|
}
|
|
1133
2141
|
|
|
1134
2142
|
.dsh-tidy-test-result.ok {
|
|
1135
|
-
color:
|
|
2143
|
+
color: var(--dsw-alias-state-success-primary);
|
|
1136
2144
|
}
|
|
1137
2145
|
|
|
1138
2146
|
.dsh-tidy-test-result.fail {
|
|
1139
|
-
color:
|
|
2147
|
+
color: var(--dsw-alias-state-error-primary);
|
|
1140
2148
|
}
|
|
1141
2149
|
|
|
1142
2150
|
.dsh-tidy-behavior-list {
|
|
1143
|
-
padding-left: 18px;
|
|
1144
|
-
margin: 4px 0 0;
|
|
1145
|
-
display: flex;
|
|
1146
|
-
flex-direction: column;
|
|
1147
|
-
gap: 4px;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
/* Priority list */
|
|
1151
|
-
.dsh-tidy-priority-list {
|
|
1152
2151
|
display: flex;
|
|
1153
2152
|
flex-direction: column;
|
|
1154
|
-
gap: 8px;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
.dsh-tidy-priority-item {
|
|
1158
|
-
display: flex;
|
|
1159
|
-
align-items: center;
|
|
1160
|
-
justify-content: space-between;
|
|
1161
|
-
padding: 8px 12px;
|
|
1162
|
-
border-radius: 8px;
|
|
1163
|
-
background: rgba(128, 128, 128, 0.06);
|
|
1164
|
-
border: 1px solid var(--dsw-alias-border-l3, rgba(128, 128, 128, 0.12));
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
.dsh-tidy-priority-name {
|
|
1168
|
-
font-size: 12px;
|
|
1169
|
-
font-weight: 500;
|
|
1170
|
-
display: flex;
|
|
1171
|
-
align-items: center;
|
|
1172
|
-
gap: 8px;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
.dsh-tidy-order-badge {
|
|
1176
|
-
display: inline-flex;
|
|
1177
|
-
align-items: center;
|
|
1178
|
-
justify-content: center;
|
|
1179
|
-
width: 18px;
|
|
1180
|
-
height: 18px;
|
|
1181
|
-
border-radius: 50%;
|
|
1182
|
-
background: #3b82f6;
|
|
1183
|
-
color: #ffffff;
|
|
1184
|
-
font-size: 11px;
|
|
1185
|
-
font-weight: 600;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
.dsh-tidy-btn-group {
|
|
1189
|
-
display: flex;
|
|
1190
2153
|
gap: 4px;
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
.dsh-tidy-icon-btn {
|
|
1194
|
-
width: 26px;
|
|
1195
|
-
height: 26px;
|
|
1196
|
-
padding: 0;
|
|
1197
|
-
display: flex;
|
|
1198
|
-
align-items: center;
|
|
1199
|
-
justify-content: center;
|
|
1200
|
-
border-radius: 6px;
|
|
1201
|
-
border: 1px solid rgba(128, 128, 128, 0.2);
|
|
1202
|
-
background: transparent;
|
|
1203
|
-
color: inherit;
|
|
1204
|
-
cursor: pointer;
|
|
1205
|
-
font-size: 12px;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
.dsh-tidy-icon-btn:hover:not(:disabled) {
|
|
1209
|
-
background: rgba(128, 128, 128, 0.15);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
.dsh-tidy-icon-btn:disabled {
|
|
1213
|
-
opacity: 0.3;
|
|
1214
|
-
cursor: default;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
/* Slider / Select */
|
|
1218
|
-
.dsh-tidy-select {
|
|
1219
|
-
padding: 6px 10px;
|
|
1220
|
-
border-radius: 8px;
|
|
1221
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.3));
|
|
1222
|
-
background: var(--dsw-alias-bg-input, rgba(0, 0, 0, 0.05));
|
|
1223
|
-
color: inherit;
|
|
1224
|
-
font-size: 12px;
|
|
1225
|
-
outline: none;
|
|
2154
|
+
margin: 4px 0 0;
|
|
2155
|
+
padding-left: 18px;
|
|
1226
2156
|
}
|
|
1227
2157
|
`;
|
|
1228
2158
|
|
|
@@ -1251,14 +2181,12 @@ var TAB_STYLE = {
|
|
|
1251
2181
|
position: "relative",
|
|
1252
2182
|
padding: "7px 1px 11px",
|
|
1253
2183
|
cursor: "pointer",
|
|
1254
|
-
font: "
|
|
1255
|
-
|
|
1256
|
-
lineHeight: "20px",
|
|
1257
|
-
color: "var(--dsw-alias-label-tertiary, inherit)"
|
|
2184
|
+
font: "var(--dsw-font-xs-13)",
|
|
2185
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
1258
2186
|
};
|
|
1259
2187
|
var TAB_ACTIVE_STYLE = {
|
|
1260
2188
|
...TAB_STYLE,
|
|
1261
|
-
color: "var(--dsw-alias-label-primary
|
|
2189
|
+
color: "var(--dsw-alias-label-primary)"
|
|
1262
2190
|
};
|
|
1263
2191
|
var TAB_MARKER_STYLE = {
|
|
1264
2192
|
position: "absolute",
|
|
@@ -1267,7 +2195,7 @@ var TAB_MARKER_STYLE = {
|
|
|
1267
2195
|
bottom: 0,
|
|
1268
2196
|
height: "2px",
|
|
1269
2197
|
borderRadius: "2px 2px 0 0",
|
|
1270
|
-
background: "var(--dsw-alias-label-primary
|
|
2198
|
+
background: "var(--dsw-alias-label-primary)"
|
|
1271
2199
|
};
|
|
1272
2200
|
var PANEL_STYLE = { listStyle: "none", margin: 0, padding: 0 };
|
|
1273
2201
|
var PANEL_HIDDEN_STYLE = { ...PANEL_STYLE, display: "none" };
|
|
@@ -1363,13 +2291,93 @@ function claimReadingSettingsPage(ctx, label, locale) {
|
|
|
1363
2291
|
|
|
1364
2292
|
// src/client/locales.ts
|
|
1365
2293
|
var NS = "settings.chatTranslate";
|
|
1366
|
-
var en = {
|
|
1367
|
-
pageNav: "Reading",
|
|
1368
|
-
title: "Chat translate"
|
|
1369
|
-
};
|
|
1370
2294
|
var zh = {
|
|
1371
2295
|
pageNav: "\u9605\u8BFB\u4F53\u9A8C",
|
|
1372
|
-
title: "\u804A\u5929\u7FFB\u8BD1"
|
|
2296
|
+
title: "\u804A\u5929\u7FFB\u8BD1",
|
|
2297
|
+
masterTitle: "\u7FFB\u8BD1\u603B\u5F00\u5173",
|
|
2298
|
+
enableTranslation: "\u542F\u7528\u7FFB\u8BD1",
|
|
2299
|
+
masterDesc: "\u81EA\u52A8\u5C06\u5F53\u524D\u4F1A\u8BDD\u4E2D\u5DE5\u5177\u8C03\u7528\u6807\u9898\uFF08\u5982 {example}\uFF09\u7FFB\u8BD1\u4E3A\u4E2D\u6587\uFF0C\u70B9\u51FB\u8BD1\u6587\u53EF\u539F\u5730\u5207\u6362\u539F\u6587/\u8BD1\u6587\u3002\u601D\u8003\u6B63\u6587\u9ED8\u8BA4\u4E0D\u7FFB\u8BD1\uFF0C\u7531 AI \u5361\u7247\u91CC\u7684\u300C\u663E\u793A\u601D\u8003\u94FE\u7FFB\u8BD1\u6309\u94AE\u300D\u63A7\u5236\uFF1A\u6253\u5F00\u540E\u5C55\u5F00\u7684\u601D\u8003\u5361\u53F3\u4FA7\u51FA\u73B0\u6309\u94AE\uFF0C\u70B9\u51FB\u624D\u7FFB\u8BD1\u3002\u601D\u8003\u5361\u6298\u53E0\u8D77\u6765\u7684\u90A3\u4E00\u884C\u6458\u8981\u6C38\u4E0D\u7FFB\u8BD1\u3002\u4EC5\u4F5C\u7528\u4E8E\u5F53\u524D\u67E5\u770B\u7684\u4F1A\u8BDD\uFF0C\u56DE\u7B54\u6B63\u6587\u6C38\u4E0D\u7FFB\u8BD1\u3002",
|
|
2300
|
+
aiTitle: "AI \u7FFB\u8BD1\uFF08OpenAI \u517C\u5BB9\u534F\u8BAE\uFF09",
|
|
2301
|
+
enableAi: "\u542F\u7528 AI \u7FFB\u8BD1\u901A\u9053",
|
|
2302
|
+
badgeConfigured: "\u5DF2\u914D\u7F6E",
|
|
2303
|
+
badgeUnconfigured: "\u672A\u914D\u7F6E",
|
|
2304
|
+
aiFallbackNote: "\u672A\u914D\u7F6E\u65F6 AI \u901A\u9053\u81EA\u52A8\u8DF3\u8FC7\uFF0C\u7531 Bing \u515C\u5E95\u3002",
|
|
2305
|
+
apiKeyTitle: "API Key",
|
|
2306
|
+
apiKeyDesc: "\u4FDD\u5B58\u81F3 {home} \u7684 {ref}\uFF0C\u4FDD\u5B58\u540E\u7ACB\u5373\u751F\u6548\uFF1B\u7559\u7A7A\u4FDD\u5B58 = \u6E05\u9664",
|
|
2307
|
+
apiKeyPlaceholderConfigured: "\u5DF2\u914D\u7F6E\uFF08\u5982\u9700\u66F4\u6362\u8BF7\u76F4\u63A5\u8F93\u5165\uFF09",
|
|
2308
|
+
save: "\u4FDD\u5B58",
|
|
2309
|
+
saving: "\u4FDD\u5B58\u4E2D\u2026",
|
|
2310
|
+
keySaved: "\u5DF2\u4FDD\u5B58\u5230 {home}\uFF0C\u7ACB\u5373\u751F\u6548",
|
|
2311
|
+
keyCleared: "\u5DF2\u6E05\u9664 API Key\uFF08AI \u901A\u9053\u5C06\u4E0D\u53EF\u7528\uFF0CBing \u515C\u5E95\uFF09",
|
|
2312
|
+
saveFailed: "\u4FDD\u5B58\u5931\u8D25\uFF1A{error}",
|
|
2313
|
+
unknownError: "\u672A\u77E5\u9519\u8BEF",
|
|
2314
|
+
baseUrlTitle: "Base URL",
|
|
2315
|
+
baseUrlDesc: "\u4EFB\u610F OpenAI \u517C\u5BB9\u670D\u52A1\u7AEF\u70B9\uFF0C\u5982 {url1}\u3001{url2}",
|
|
2316
|
+
modelTitle: "\u6A21\u578B",
|
|
2317
|
+
modelDesc: "\u5982 {model1}\u3001{model2}\uFF1B\u7559\u7A7A\u89C6\u4E3A\u672A\u914D\u7F6E",
|
|
2318
|
+
test: "\u6D4B\u8BD5 AI \u901A\u9053",
|
|
2319
|
+
testing: "\u6D4B\u8BD5\u4E2D\u2026",
|
|
2320
|
+
testOk: "\u8FDE\u63A5\u6B63\u5E38\uFF0C\u5EF6\u8FDF {latency}ms",
|
|
2321
|
+
testFail: "\u5931\u8D25\uFF1A{error}",
|
|
2322
|
+
thinkEnableTitle: "\u663E\u793A\u601D\u8003\u94FE\u7FFB\u8BD1\u6309\u94AE",
|
|
2323
|
+
thinkEnableDesc: "\u5728\u5C55\u5F00\u7684\u601D\u8003\u5361\u7684\u300CThink\u300D\u53F3\u4FA7\u663E\u793A\u4E00\u4E2A\u6309\u94AE\uFF0C\u70B9\u51FB\u624D\u7FFB\u8BD1\u8FD9\u6761\u601D\u8003\u6B63\u6587\uFF08\u4E0D\u4F1A\u81EA\u52A8\u7FFB\u8BD1\uFF09\uFF1B\u6298\u53E0\u8D77\u6765\u7684\u5361\u7247\u6CA1\u6709\u6309\u94AE\uFF0C\u5C55\u5F00\u540E\u6309\u94AE\u624D\u51FA\u73B0\u3002\u7FFB\u8BD1\u4E2D\u6309\u94AE\u8F6C\u5708\uFF0C\u7FFB\u597D\u540E\u5B9E\u5FC3\u9AD8\u4EAE\uFF0C\u518D\u70B9\u4E00\u4E0B\u6574\u6761\u5207\u56DE\u539F\u6587\uFF08\u6B63\u6587\u91CC\u7684\u70B9\u51FB\u4E0D\u5207\u6362\uFF09\u3002\u4EE3\u7801\u5757\u4E0E\u6298\u53E0\u6458\u8981\u4E0D\u52A8\uFF0CBing \u901A\u9053\u4E0D\u53C2\u4E0E\u3002\u5173\u95ED\u6B64\u5F00\u5173\u4F1A\u64A4\u6389\u6309\u94AE\u5E76\u8FD8\u539F\u5DF2\u7FFB\u8BD1\u5185\u5BB9\u3002",
|
|
2324
|
+
thinkTimeoutTitle: "\u601D\u8003\u94FE\u7FFB\u8BD1\u8D85\u65F6",
|
|
2325
|
+
thinkTimeoutDesc: "\u5355\u4F4D\u6BEB\u79D2\uFF0C\u8303\u56F4 500-900000\u3002\u6574\u5757\u601D\u8003\u53EF\u80FD\u8981\u51E0\u5206\u949F\uFF0C\u9ED8\u8BA4 600000\u3002",
|
|
2326
|
+
bingTitle: "Bing \u7F51\u9875\u7FFB\u8BD1\uFF08\u514D Key \u515C\u5E95\uFF09",
|
|
2327
|
+
enableBing: "\u542F\u7528 Bing \u7FFB\u8BD1\u901A\u9053",
|
|
2328
|
+
bingDesc: "\u5185\u7F6E\u514D Key \u7FFB\u8BD1\u901A\u9053\uFF0C\u65E0\u9700\u4EFB\u4F55\u914D\u7F6E\uFF0C\u53EA\u7FFB\u8BD1\u5DE5\u5177\u8C03\u7528\u6807\u9898\u3002AI \u672A\u914D\u7F6E\u6216\u8BF7\u6C42\u5931\u8D25\u65F6\u81EA\u52A8\u515C\u5E95\uFF1B\u601D\u8003\u94FE\u7FFB\u8BD1\u6C38\u8FDC\u4E0D\u8D70\u8FD9\u6761\u901A\u9053\u3002AI \u4E0E Bing \u540C\u65F6\u5173\u95ED\u5219\u5DE5\u5177\u8C03\u7528\u6807\u9898\u4E5F\u4E0D\u7FFB\u8BD1\u3002",
|
|
2329
|
+
behaviorTitle: "\u901A\u9053\u884C\u4E3A",
|
|
2330
|
+
behavior1: "AI \u5F00\u542F\u4E14\u5DF2\u914D\u7F6E \u2192 AI \u4F18\u5148\u7FFB\u8BD1\uFF0C\u5931\u8D25\u81EA\u52A8\u964D\u7EA7 Bing",
|
|
2331
|
+
behavior2: "AI \u5F00\u542F\u4F46\u672A\u914D\u7F6E + Bing \u5F00\u542F \u2192 \u7531 Bing \u7FFB\u8BD1",
|
|
2332
|
+
behavior3: "AI \u5F00\u542F\u4F46\u672A\u914D\u7F6E + Bing \u5173\u95ED \u2192 \u4E0D\u7FFB\u8BD1",
|
|
2333
|
+
behavior4: "AI \u5173\u95ED + Bing \u5F00\u542F \u2192 \u76F4\u63A5\u4F7F\u7528 Bing",
|
|
2334
|
+
behavior5: "AI \u5173\u95ED + Bing \u5173\u95ED \u2192 \u4E0D\u7FFB\u8BD1",
|
|
2335
|
+
concurrencyTitle: "\u6700\u5927\u7FFB\u8BD1\u5E76\u53D1\u6570",
|
|
2336
|
+
concurrencyDesc: "\u63A7\u5236\u89C6\u53E3\u6EDA\u52A8\u4E0E\u591A\u5DE5\u5177\u5361\u7247\u65F6\u7684\u6700\u5927\u5E76\u884C\u8BF7\u6C42\u6570\uFF08\u8303\u56F4 1-100\uFF0C\u63A8\u8350 3\uFF09\u3002"
|
|
2337
|
+
};
|
|
2338
|
+
var en = {
|
|
2339
|
+
pageNav: "Reading",
|
|
2340
|
+
title: "Chat translate",
|
|
2341
|
+
masterTitle: "Translation master switch",
|
|
2342
|
+
enableTranslation: "Enable translation",
|
|
2343
|
+
masterDesc: "Translates tool-call titles in the current session (e.g. {example}) into Chinese; click a translation to switch back to the original in place. Think text stays untranslated by default and is controlled by \u201Cshow think-chain translate button\u201D in the AI card: once on, a button appears at the right of every expanded think card and translates only when clicked. The collapsed summary line of a think card is never translated. Applies to the session you are viewing only; reply text is never translated.",
|
|
2344
|
+
aiTitle: "AI translation (OpenAI-compatible)",
|
|
2345
|
+
enableAi: "Enable the AI translation channel",
|
|
2346
|
+
badgeConfigured: "Configured",
|
|
2347
|
+
badgeUnconfigured: "Not configured",
|
|
2348
|
+
aiFallbackNote: "When unconfigured the AI channel is skipped and Bing takes over.",
|
|
2349
|
+
apiKeyTitle: "API Key",
|
|
2350
|
+
apiKeyDesc: "Saved as {ref} in {home}; takes effect immediately. Saving an empty value clears it.",
|
|
2351
|
+
apiKeyPlaceholderConfigured: "Configured (type a new value to replace it)",
|
|
2352
|
+
save: "Save",
|
|
2353
|
+
saving: "Saving\u2026",
|
|
2354
|
+
keySaved: "Saved to {home}; takes effect immediately",
|
|
2355
|
+
keyCleared: "API Key cleared (the AI channel becomes unavailable and Bing takes over)",
|
|
2356
|
+
saveFailed: "Save failed: {error}",
|
|
2357
|
+
unknownError: "Unknown error",
|
|
2358
|
+
baseUrlTitle: "Base URL",
|
|
2359
|
+
baseUrlDesc: "Any OpenAI-compatible endpoint, e.g. {url1} or {url2}",
|
|
2360
|
+
modelTitle: "Model",
|
|
2361
|
+
modelDesc: "e.g. {model1} or {model2}; empty counts as unconfigured",
|
|
2362
|
+
test: "Test the AI channel",
|
|
2363
|
+
testing: "Testing\u2026",
|
|
2364
|
+
testOk: "Connected, {latency}ms latency",
|
|
2365
|
+
testFail: "Failed: {error}",
|
|
2366
|
+
thinkEnableTitle: "Show the think-chain translate button",
|
|
2367
|
+
thinkEnableDesc: "Shows a button to the right of \u201CThink\u201D on every expanded think card; it translates that think text only when clicked, never automatically. Collapsed cards carry no button; it appears once the card is expanded. While translating, the button spins; once done it highlights solid, and clicking it again switches the whole block back to the original (clicks inside the text do not switch). Code blocks and the collapsed summary stay untouched, and the Bing channel is not involved. Turning this off removes the button and restores already translated content.",
|
|
2368
|
+
thinkTimeoutTitle: "Think-chain translation timeout",
|
|
2369
|
+
thinkTimeoutDesc: "In milliseconds, range 500-900000. A whole think block can take minutes; default 600000.",
|
|
2370
|
+
bingTitle: "Bing web translation (key-free fallback)",
|
|
2371
|
+
enableBing: "Enable the Bing translation channel",
|
|
2372
|
+
bingDesc: "A built-in key-free channel that needs no configuration and translates tool-call titles only. It takes over automatically when AI is unconfigured or a request fails; think-chain translation never uses this channel. With both AI and Bing off, tool-call titles are not translated either.",
|
|
2373
|
+
behaviorTitle: "Channel behavior",
|
|
2374
|
+
behavior1: "AI on and configured \u2192 AI translates first, falling back to Bing on failure",
|
|
2375
|
+
behavior2: "AI on but unconfigured + Bing on \u2192 Bing translates",
|
|
2376
|
+
behavior3: "AI on but unconfigured + Bing off \u2192 nothing is translated",
|
|
2377
|
+
behavior4: "AI off + Bing on \u2192 Bing is used directly",
|
|
2378
|
+
behavior5: "AI off + Bing off \u2192 nothing is translated",
|
|
2379
|
+
concurrencyTitle: "Max translation concurrency",
|
|
2380
|
+
concurrencyDesc: "Caps parallel requests while the viewport scrolls across multiple tool cards (range 1-100, recommended 3)."
|
|
1373
2381
|
};
|
|
1374
2382
|
|
|
1375
2383
|
// src/client/settings/ui.tsx
|
|
@@ -1383,21 +2391,17 @@ function ensureSettingsStyles() {
|
|
|
1383
2391
|
document.head.appendChild(el);
|
|
1384
2392
|
stylesInjected = true;
|
|
1385
2393
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
{
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
"aria-checked": props.checked,
|
|
1394
|
-
onClick: props.onChange,
|
|
1395
|
-
"aria-label": props.label
|
|
1396
|
-
}
|
|
1397
|
-
);
|
|
2394
|
+
var CREDENTIALS_PATH = "~/.dsh/.credentials.yaml";
|
|
2395
|
+
function withSamples(text, samples) {
|
|
2396
|
+
return text.split(/(\{\w+\})/g).map((part, index) => {
|
|
2397
|
+
const name2 = /^\{\w+\}$/.test(part) ? part.slice(1, -1) : "";
|
|
2398
|
+
const sample = name2 === "" ? void 0 : samples[name2];
|
|
2399
|
+
return sample === void 0 ? part : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: sample }, index);
|
|
2400
|
+
});
|
|
1398
2401
|
}
|
|
1399
|
-
function TidySettingsPanel(
|
|
2402
|
+
function TidySettingsPanel(props) {
|
|
1400
2403
|
ensureSettingsStyles();
|
|
2404
|
+
const { t } = props;
|
|
1401
2405
|
const [state, setState] = (0, import_react2.useState)(() => settingsStore.getState());
|
|
1402
2406
|
const [testing, setTesting] = (0, import_react2.useState)(null);
|
|
1403
2407
|
const [apiKeyInput, setApiKeyInput] = (0, import_react2.useState)("");
|
|
@@ -1411,7 +2415,7 @@ function TidySettingsPanel(_props) {
|
|
|
1411
2415
|
const runTest = async (channel) => {
|
|
1412
2416
|
setTesting({ channel, running: true });
|
|
1413
2417
|
const res = await settingsStore.testChannel(channel);
|
|
1414
|
-
const message = res.ok ?
|
|
2418
|
+
const message = res.ok ? t("testOk").replace("{latency}", String(res.latencyMs)) : t("testFail").replace("{error}", res.error || t("unknownError"));
|
|
1415
2419
|
setTesting({ channel, running: false, ok: res.ok, message });
|
|
1416
2420
|
};
|
|
1417
2421
|
const handleSaveKey = async () => {
|
|
@@ -1423,176 +2427,211 @@ function TidySettingsPanel(_props) {
|
|
|
1423
2427
|
const cleared = !apiKeyInput.trim();
|
|
1424
2428
|
setKeyMsg({
|
|
1425
2429
|
ok: true,
|
|
1426
|
-
text: cleared ? "
|
|
2430
|
+
text: cleared ? t("keyCleared") : t("keySaved").replace("{home}", CREDENTIALS_PATH)
|
|
1427
2431
|
});
|
|
1428
2432
|
setApiKeyInput("");
|
|
1429
2433
|
} else {
|
|
1430
|
-
setKeyMsg({
|
|
2434
|
+
setKeyMsg({
|
|
2435
|
+
ok: false,
|
|
2436
|
+
text: t("saveFailed").replace("{error}", res.error || t("unknownError"))
|
|
2437
|
+
});
|
|
1431
2438
|
}
|
|
1432
2439
|
};
|
|
1433
2440
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-settings", children: [
|
|
1434
2441
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
|
|
1435
2442
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-title", children: [
|
|
1436
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "
|
|
2443
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("masterTitle") }),
|
|
1437
2444
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1438
|
-
Switch,
|
|
2445
|
+
import_dsh_client_ui_primitives.Switch,
|
|
1439
2446
|
{
|
|
1440
2447
|
checked: state.enabled,
|
|
1441
|
-
onChange: () => settingsStore.update({ enabled:
|
|
1442
|
-
label: "
|
|
2448
|
+
onChange: (next) => settingsStore.update({ enabled: next }),
|
|
2449
|
+
label: t("enableTranslation")
|
|
1443
2450
|
}
|
|
1444
2451
|
)
|
|
1445
2452
|
] }),
|
|
1446
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
1447
|
-
"\u81EA\u52A8\u5C06\u5F53\u524D\u4F1A\u8BDD\u4E2D\u5DE5\u5177\u8C03\u7528\u6807\u9898\u4E0E\u601D\u8003\u6298\u53E0\u6458\u8981\uFF08\u5982 ",
|
|
1448
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "Locate DSH home directory structure" }),
|
|
1449
|
-
"\uFF09\u7FFB\u8BD1\u4E3A\u4E2D\u6587\uFF0C \u70B9\u51FB\u8BD1\u6587\u53EF\u539F\u5730\u5207\u6362\u539F\u6587/\u8BD1\u6587\u3002\u601D\u8003\u6298\u53E0\u6458\u8981\u4EC5\u5728\u601D\u8003\u5B8C\u5168\u7ED3\u675F\u540E\u624D\u7FFB\u8BD1\uFF08\u4E0D\u7FFB\u8BD1\u6D41\u5F0F\u4E2D\u95F4\u6001\uFF09\u3002 \u4EC5\u4F5C\u7528\u4E8E\u5F53\u524D\u67E5\u770B\u7684\u4F1A\u8BDD\uFF0C\u5BF9\u8BDD\u6B63\u6587\u6C38\u4E0D\u7FFB\u8BD1\u3002"
|
|
1450
|
-
] })
|
|
2453
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: withSamples(t("masterDesc"), { example: "Locate DSH home directory structure" }) })
|
|
1451
2454
|
] }),
|
|
1452
2455
|
state.enabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1453
2456
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
|
|
1454
2457
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-title", children: [
|
|
1455
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "
|
|
2458
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("aiTitle") }),
|
|
1456
2459
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1457
|
-
Switch,
|
|
2460
|
+
import_dsh_client_ui_primitives.Switch,
|
|
1458
2461
|
{
|
|
1459
2462
|
checked: state.aiEnabled,
|
|
1460
|
-
onChange: () => settingsStore.update({ aiEnabled:
|
|
1461
|
-
label: "
|
|
2463
|
+
onChange: (next) => settingsStore.update({ aiEnabled: next }),
|
|
2464
|
+
label: t("enableAi")
|
|
1462
2465
|
}
|
|
1463
2466
|
)
|
|
1464
2467
|
] }),
|
|
1465
2468
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-desc", children: [
|
|
1466
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2469
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { tone: state.aiConfigured ? "success" : "warning", children: t(state.aiConfigured ? "badgeConfigured" : "badgeUnconfigured") }),
|
|
1467
2470
|
" ",
|
|
1468
|
-
"
|
|
2471
|
+
t("aiFallbackNote")
|
|
1469
2472
|
] }),
|
|
1470
2473
|
state.aiEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1471
2474
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
1472
2475
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
1473
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: "
|
|
1474
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "TRANSLATE_API_KEY" }),
|
|
1479
|
-
"\uFF0C\u4FDD\u5B58\u540E\u7ACB\u5373\u751F\u6548\uFF1B\u7559\u7A7A\u4FDD\u5B58 = \u6E05\u9664"
|
|
1480
|
-
] })
|
|
2476
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("apiKeyTitle") }),
|
|
2477
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("apiKeyDesc"), {
|
|
2478
|
+
home: CREDENTIALS_PATH,
|
|
2479
|
+
ref: TRANSLATE_API_KEY_REF
|
|
2480
|
+
}) })
|
|
1481
2481
|
] }),
|
|
1482
2482
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-input-row", children: [
|
|
1483
2483
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1484
|
-
|
|
2484
|
+
import_dsh_client_ui_primitives.Input,
|
|
1485
2485
|
{
|
|
1486
2486
|
type: "password",
|
|
1487
|
-
className: "dsh-tidy-
|
|
1488
|
-
placeholder: state.aiConfigured ? "
|
|
2487
|
+
className: "dsh-tidy-field-wide",
|
|
2488
|
+
placeholder: state.aiConfigured ? t("apiKeyPlaceholderConfigured") : "sk-...",
|
|
1489
2489
|
value: apiKeyInput,
|
|
1490
2490
|
onChange: (e) => setApiKeyInput(e.target.value),
|
|
1491
|
-
|
|
1492
|
-
"aria-label": "API Key"
|
|
2491
|
+
"aria-label": t("apiKeyTitle")
|
|
1493
2492
|
}
|
|
1494
2493
|
),
|
|
1495
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2494
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2495
|
+
import_dsh_client_ui_primitives.Button,
|
|
2496
|
+
{
|
|
2497
|
+
variant: "primary",
|
|
2498
|
+
size: "sm",
|
|
2499
|
+
disabled: savingKey,
|
|
2500
|
+
onClick: handleSaveKey,
|
|
2501
|
+
children: savingKey ? t("saving") : t("save")
|
|
2502
|
+
}
|
|
2503
|
+
)
|
|
1496
2504
|
] })
|
|
1497
2505
|
] }),
|
|
1498
2506
|
keyMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `dsh-tidy-test-result ${keyMsg.ok ? "ok" : "fail"}`, children: keyMsg.text }),
|
|
1499
2507
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
1500
2508
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
1501
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: "
|
|
1502
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
" ",
|
|
1507
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "https://api.deepseek.com/v1" })
|
|
1508
|
-
] })
|
|
2509
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("baseUrlTitle") }),
|
|
2510
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("baseUrlDesc"), {
|
|
2511
|
+
url1: "https://api.openai.com/v1",
|
|
2512
|
+
url2: "https://api.deepseek.com/v1"
|
|
2513
|
+
}) })
|
|
1509
2514
|
] }),
|
|
1510
2515
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1511
|
-
|
|
2516
|
+
import_dsh_client_ui_primitives.Input,
|
|
1512
2517
|
{
|
|
1513
2518
|
type: "text",
|
|
1514
|
-
className: "dsh-tidy-
|
|
2519
|
+
className: "dsh-tidy-field-wide",
|
|
1515
2520
|
placeholder: "https://api.openai.com/v1",
|
|
1516
2521
|
value: state.baseUrl,
|
|
1517
2522
|
onChange: (e) => settingsStore.update({ baseUrl: e.target.value }),
|
|
1518
|
-
|
|
1519
|
-
"aria-label": "AI Base URL"
|
|
2523
|
+
"aria-label": t("baseUrlTitle")
|
|
1520
2524
|
}
|
|
1521
2525
|
)
|
|
1522
2526
|
] }),
|
|
1523
2527
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
1524
2528
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
1525
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: "
|
|
1526
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "deepseek-chat" }),
|
|
1531
|
-
"\uFF1B\u7559\u7A7A\u89C6\u4E3A\u672A\u914D\u7F6E"
|
|
1532
|
-
] })
|
|
2529
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("modelTitle") }),
|
|
2530
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("modelDesc"), {
|
|
2531
|
+
model1: "gpt-4o-mini",
|
|
2532
|
+
model2: "deepseek-chat"
|
|
2533
|
+
}) })
|
|
1533
2534
|
] }),
|
|
1534
2535
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1535
|
-
|
|
2536
|
+
import_dsh_client_ui_primitives.Input,
|
|
1536
2537
|
{
|
|
1537
2538
|
type: "text",
|
|
1538
|
-
className: "dsh-tidy-
|
|
2539
|
+
className: "dsh-tidy-field-wide",
|
|
1539
2540
|
placeholder: "gpt-4o-mini",
|
|
1540
2541
|
value: state.model,
|
|
1541
2542
|
onChange: (e) => settingsStore.update({ model: e.target.value }),
|
|
1542
|
-
|
|
1543
|
-
"aria-label": "AI \u6A21\u578B"
|
|
2543
|
+
"aria-label": t("modelTitle")
|
|
1544
2544
|
}
|
|
1545
2545
|
)
|
|
1546
2546
|
] }),
|
|
1547
2547
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
1548
2548
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1549
|
-
|
|
2549
|
+
import_dsh_client_ui_primitives.Button,
|
|
1550
2550
|
{
|
|
1551
|
-
|
|
1552
|
-
|
|
2551
|
+
variant: "outline",
|
|
2552
|
+
size: "sm",
|
|
1553
2553
|
disabled: testing?.running,
|
|
1554
2554
|
onClick: () => runTest("openai"),
|
|
1555
|
-
children: testing?.running ? "
|
|
2555
|
+
children: testing?.running ? t("testing") : t("test")
|
|
1556
2556
|
}
|
|
1557
2557
|
),
|
|
1558
2558
|
testing?.channel === "openai" && !testing.running && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-tidy-test-result ${testing.ok ? "ok" : "fail"}`, children: testing.message })
|
|
2559
|
+
] }),
|
|
2560
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
2561
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
2562
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("thinkEnableTitle") }),
|
|
2563
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: t("thinkEnableDesc") })
|
|
2564
|
+
] }),
|
|
2565
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2566
|
+
import_dsh_client_ui_primitives.Switch,
|
|
2567
|
+
{
|
|
2568
|
+
checked: state.thinkEnabled,
|
|
2569
|
+
onChange: (next) => settingsStore.update({ thinkEnabled: next }),
|
|
2570
|
+
label: t("thinkEnableTitle")
|
|
2571
|
+
}
|
|
2572
|
+
)
|
|
2573
|
+
] }),
|
|
2574
|
+
state.thinkEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
2575
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
2576
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("thinkTimeoutTitle") }),
|
|
2577
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: t("thinkTimeoutDesc") })
|
|
2578
|
+
] }),
|
|
2579
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2580
|
+
import_dsh_client_ui_primitives.Input,
|
|
2581
|
+
{
|
|
2582
|
+
type: "number",
|
|
2583
|
+
className: "dsh-tidy-field-timeout",
|
|
2584
|
+
min: 500,
|
|
2585
|
+
max: 9e5,
|
|
2586
|
+
step: 1e3,
|
|
2587
|
+
value: state.thinkTimeoutMs,
|
|
2588
|
+
onChange: (e) => {
|
|
2589
|
+
const val = parseInt(e.target.value, 10);
|
|
2590
|
+
if (!Number.isFinite(val)) return;
|
|
2591
|
+
settingsStore.update({
|
|
2592
|
+
thinkTimeoutMs: Math.min(Math.max(val, 500), 9e5)
|
|
2593
|
+
});
|
|
2594
|
+
},
|
|
2595
|
+
"aria-label": t("thinkTimeoutTitle")
|
|
2596
|
+
}
|
|
2597
|
+
)
|
|
1559
2598
|
] })
|
|
1560
2599
|
] })
|
|
1561
2600
|
] }),
|
|
1562
2601
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
|
|
1563
2602
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-title", children: [
|
|
1564
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "
|
|
2603
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("bingTitle") }),
|
|
1565
2604
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1566
|
-
Switch,
|
|
2605
|
+
import_dsh_client_ui_primitives.Switch,
|
|
1567
2606
|
{
|
|
1568
2607
|
checked: state.bingEnabled,
|
|
1569
|
-
onChange: () => settingsStore.update({ bingEnabled:
|
|
1570
|
-
label: "
|
|
2608
|
+
onChange: (next) => settingsStore.update({ bingEnabled: next }),
|
|
2609
|
+
label: t("enableBing")
|
|
1571
2610
|
}
|
|
1572
2611
|
)
|
|
1573
2612
|
] }),
|
|
1574
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: "
|
|
2613
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: t("bingDesc") })
|
|
1575
2614
|
] }),
|
|
1576
2615
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
|
|
1577
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: "
|
|
2616
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("behaviorTitle") }),
|
|
1578
2617
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { className: "dsh-tidy-desc dsh-tidy-behavior-list", children: [
|
|
1579
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "
|
|
1580
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "
|
|
1581
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "
|
|
1582
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "
|
|
1583
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "
|
|
2618
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: t("behavior1") }),
|
|
2619
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: t("behavior2") }),
|
|
2620
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: t("behavior3") }),
|
|
2621
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: t("behavior4") }),
|
|
2622
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: t("behavior5") })
|
|
1584
2623
|
] })
|
|
1585
2624
|
] }),
|
|
1586
2625
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-card", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
1587
2626
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
1588
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: "
|
|
1589
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: "
|
|
2627
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("concurrencyTitle") }),
|
|
2628
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: t("concurrencyDesc") })
|
|
1590
2629
|
] }),
|
|
1591
2630
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1592
|
-
|
|
2631
|
+
import_dsh_client_ui_primitives.Input,
|
|
1593
2632
|
{
|
|
1594
2633
|
type: "number",
|
|
1595
|
-
className: "dsh-tidy-
|
|
2634
|
+
className: "dsh-tidy-field-count",
|
|
1596
2635
|
min: 1,
|
|
1597
2636
|
max: 100,
|
|
1598
2637
|
step: 1,
|
|
@@ -1602,8 +2641,7 @@ function TidySettingsPanel(_props) {
|
|
|
1602
2641
|
if (!Number.isFinite(val)) return;
|
|
1603
2642
|
settingsStore.update({ concurrency: Math.min(Math.max(val, 1), 100) });
|
|
1604
2643
|
},
|
|
1605
|
-
|
|
1606
|
-
"aria-label": "\u6700\u5927\u7FFB\u8BD1\u5E76\u53D1\u6570"
|
|
2644
|
+
"aria-label": t("concurrencyTitle")
|
|
1607
2645
|
}
|
|
1608
2646
|
)
|
|
1609
2647
|
] }) })
|
|
@@ -1660,7 +2698,7 @@ function setupSettingsUi(ctx) {
|
|
|
1660
2698
|
|
|
1661
2699
|
// src/client/index.ts
|
|
1662
2700
|
var name = "dsh-chat-translate";
|
|
1663
|
-
var inject = ["slots", "settingsScope", "remote", "remote.credentials"];
|
|
2701
|
+
var inject = ["slots", "settingsScope", "remote", "remote.credentials", "locale"];
|
|
1664
2702
|
function apply(ctx) {
|
|
1665
2703
|
ctx.effect(() => chatTranslateObserver.start(document), "dsh-chat-translate: title translate observer");
|
|
1666
2704
|
ctx.effect(() => setupSettingsUi(ctx), "dsh-chat-translate: settings section");
|