@lynn123411/dsh-chat-translate 1.8.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -14
- package/cordis.patch.yml +2 -1
- package/docs//351/205/215/347/275/256.md +22 -20
- package/lib/client.js +429 -1065
- package/lib/client.js.map +4 -4
- package/lib/index.js +265 -471
- package/lib/index.js.map +4 -4
- package/lib/types/client/index.d.ts +12 -11
- package/lib/types/client/locales.d.ts +8 -20
- package/lib/types/client/reading-settings-page.d.ts +7 -0
- package/lib/types/client/settings/store.d.ts +40 -32
- package/lib/types/client/settings/ui.d.ts +5 -4
- package/lib/types/client/translate/api.d.ts +5 -16
- package/lib/types/client/translate/client-cache.d.ts +1 -3
- package/lib/types/client/translate/lazy.d.ts +9 -7
- package/lib/types/client/translate/mount.d.ts +2 -6
- package/lib/types/client/translate/observer.d.ts +24 -24
- package/lib/types/client/translate/reply.d.ts +38 -0
- package/lib/types/index.d.ts +41 -12
- package/lib/types/server/cache.d.ts +13 -1
- package/lib/types/server/config.d.ts +81 -40
- package/lib/types/server/dispatcher.d.ts +25 -38
- package/lib/types/server/pipeline/{think.d.ts → blocks.d.ts} +9 -9
- package/lib/types/server/router.d.ts +6 -8
- package/lib/types/server/types.d.ts +2 -8
- package/package.json +14 -14
- package/lib/types/client/translate/think-button.d.ts +0 -19
- package/lib/types/client/translate/think.d.ts +0 -55
- package/lib/types/client/translate/viewport-observer.d.ts +0 -43
- package/lib/types/server/adapters/bing.d.ts +0 -15
package/lib/client.js
CHANGED
|
@@ -22,13 +22,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
24
|
NonDestructiveTranslationMount: () => NonDestructiveTranslationMount,
|
|
25
|
-
StreamDebounceViewportObserver: () => StreamDebounceViewportObserver,
|
|
26
25
|
apply: () => apply,
|
|
27
26
|
chatTranslateObserver: () => chatTranslateObserver,
|
|
27
|
+
chunkUnits: () => chunkUnits,
|
|
28
28
|
clientCache: () => clientCache,
|
|
29
|
+
collectReplyUnits: () => collectReplyUnits,
|
|
29
30
|
inject: () => inject,
|
|
30
31
|
lazyQueue: () => lazyQueue,
|
|
31
32
|
name: () => name,
|
|
33
|
+
needsTranslation: () => needsTranslation,
|
|
34
|
+
replyTranslator: () => replyTranslator,
|
|
32
35
|
settingsStore: () => settingsStore,
|
|
33
36
|
setupSettingsUi: () => setupSettingsUi
|
|
34
37
|
});
|
|
@@ -55,10 +58,9 @@ function isMaskLeak(translatedText) {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
// src/client/translate/client-cache.ts
|
|
58
|
-
var
|
|
59
|
-
var
|
|
60
|
-
var
|
|
61
|
-
var THINK_MAX_ENTRIES = 300;
|
|
61
|
+
var CACHE_KEY = "dsh-chat-translate:cache";
|
|
62
|
+
var MAX_ENTRIES = 500;
|
|
63
|
+
var RETIRED_CACHE_KEYS = ["dsh-chat-translate:think-cache"];
|
|
62
64
|
var TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
63
65
|
var ClientCache = class {
|
|
64
66
|
// Map preserves insertion order in JS, enabling true LRU semantics.
|
|
@@ -67,7 +69,7 @@ var ClientCache = class {
|
|
|
67
69
|
saveTimer = null;
|
|
68
70
|
storageKey;
|
|
69
71
|
maxEntries;
|
|
70
|
-
constructor(storageKey =
|
|
72
|
+
constructor(storageKey = CACHE_KEY, maxEntries = MAX_ENTRIES) {
|
|
71
73
|
this.storageKey = storageKey;
|
|
72
74
|
this.maxEntries = maxEntries;
|
|
73
75
|
this.load();
|
|
@@ -159,8 +161,17 @@ var ClientCache = class {
|
|
|
159
161
|
return this.memCache.size;
|
|
160
162
|
}
|
|
161
163
|
};
|
|
164
|
+
function retireClientCaches() {
|
|
165
|
+
if (typeof localStorage === "undefined") return;
|
|
166
|
+
for (const key of RETIRED_CACHE_KEYS) {
|
|
167
|
+
try {
|
|
168
|
+
localStorage.removeItem(key);
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
retireClientCaches();
|
|
162
174
|
var clientCache = new ClientCache();
|
|
163
|
-
var thinkClientCache = new ClientCache(THINK_CACHE_KEY, THINK_MAX_ENTRIES);
|
|
164
175
|
|
|
165
176
|
// src/describe-error.ts
|
|
166
177
|
function describeError(err) {
|
|
@@ -189,51 +200,7 @@ function describeError(err) {
|
|
|
189
200
|
}
|
|
190
201
|
|
|
191
202
|
// src/client/translate/api.ts
|
|
192
|
-
async function
|
|
193
|
-
if (!Array.isArray(texts) || texts.length === 0) return [];
|
|
194
|
-
const validTexts = texts.map((t) => typeof t === "string" ? t : "");
|
|
195
|
-
if (validTexts.length === 0) return [];
|
|
196
|
-
const controller = new AbortController();
|
|
197
|
-
const timeoutId = options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : null;
|
|
198
|
-
const effectiveSignal = options.signal ? options.signal.aborted ? options.signal : controller.signal : controller.signal;
|
|
199
|
-
if (options.signal) {
|
|
200
|
-
options.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
201
|
-
}
|
|
202
|
-
try {
|
|
203
|
-
const res = await fetch("/api/dsh-chat-translate/translate", {
|
|
204
|
-
method: "POST",
|
|
205
|
-
headers: {
|
|
206
|
-
"Content-Type": "application/json"
|
|
207
|
-
},
|
|
208
|
-
body: JSON.stringify({ texts: validTexts }),
|
|
209
|
-
signal: effectiveSignal
|
|
210
|
-
});
|
|
211
|
-
if (res.ok) {
|
|
212
|
-
const data = await res.json();
|
|
213
|
-
if (data.ok && Array.isArray(data.results) && data.results.length > 0) {
|
|
214
|
-
return data.results;
|
|
215
|
-
} else if (data.error) {
|
|
216
|
-
console.warn(`[dsh-chat-translate] \u7FFB\u8BD1\u63A5\u53E3\u9519\u8BEF: ${data.error}`);
|
|
217
|
-
}
|
|
218
|
-
} else {
|
|
219
|
-
console.warn(`[dsh-chat-translate] \u7FFB\u8BD1\u8BF7\u6C42\u5931\u8D25 (HTTP ${res.status})`);
|
|
220
|
-
}
|
|
221
|
-
} catch (err) {
|
|
222
|
-
if (err?.name === "AbortError") {
|
|
223
|
-
}
|
|
224
|
-
} finally {
|
|
225
|
-
if (timeoutId !== null) {
|
|
226
|
-
clearTimeout(timeoutId);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return validTexts.map((t) => ({
|
|
230
|
-
original: t,
|
|
231
|
-
translated: t,
|
|
232
|
-
channel: "fallback-client",
|
|
233
|
-
cached: false
|
|
234
|
-
}));
|
|
235
|
-
}
|
|
236
|
-
async function requestTranslateThink(blocks) {
|
|
203
|
+
async function requestTranslateReply(blocks) {
|
|
237
204
|
const fallback = () => blocks.map((block) => ({
|
|
238
205
|
original: block,
|
|
239
206
|
translated: block,
|
|
@@ -243,7 +210,7 @@ async function requestTranslateThink(blocks) {
|
|
|
243
210
|
}));
|
|
244
211
|
if (!Array.isArray(blocks) || blocks.length === 0) return [];
|
|
245
212
|
try {
|
|
246
|
-
const res = await fetch("/api/dsh-chat-translate/translate-
|
|
213
|
+
const res = await fetch("/api/dsh-chat-translate/translate-reply", {
|
|
247
214
|
method: "POST",
|
|
248
215
|
headers: { "Content-Type": "application/json" },
|
|
249
216
|
body: JSON.stringify({ blocks })
|
|
@@ -254,13 +221,13 @@ async function requestTranslateThink(blocks) {
|
|
|
254
221
|
return data.results;
|
|
255
222
|
}
|
|
256
223
|
if (data.error) {
|
|
257
|
-
console.warn(`[dsh-chat-translate] \
|
|
224
|
+
console.warn(`[dsh-chat-translate] \u6B63\u6587\u7FFB\u8BD1\u63A5\u53E3\u9519\u8BEF: ${data.error}`);
|
|
258
225
|
}
|
|
259
226
|
} else {
|
|
260
|
-
console.warn(`[dsh-chat-translate] \
|
|
227
|
+
console.warn(`[dsh-chat-translate] \u6B63\u6587\u7FFB\u8BD1\u8BF7\u6C42\u5931\u8D25 (HTTP ${res.status})`);
|
|
261
228
|
}
|
|
262
229
|
} catch (err) {
|
|
263
|
-
console.warn(`[dsh-chat-translate] \
|
|
230
|
+
console.warn(`[dsh-chat-translate] \u6B63\u6587\u7FFB\u8BD1\u8BF7\u6C42\u5F02\u5E38: ${describeError(err)}`);
|
|
264
231
|
}
|
|
265
232
|
return fallback();
|
|
266
233
|
}
|
|
@@ -377,29 +344,6 @@ var NonDestructiveTranslationMount = class {
|
|
|
377
344
|
static isOwnNode(element) {
|
|
378
345
|
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;
|
|
379
346
|
}
|
|
380
|
-
/**
|
|
381
|
-
* 把一个作用域里的全部挂载译文一起翻面。思考链只由卡片标题右侧的按钮调用
|
|
382
|
-
* 它,正文里的点击不再改变原文与译文。
|
|
383
|
-
*/
|
|
384
|
-
static toggleGroup(scope, showOriginal) {
|
|
385
|
-
const mounted = scope.querySelectorAll('[data-tidy-translated="true"]');
|
|
386
|
-
for (const element of mounted) {
|
|
387
|
-
const origWrapper = element.querySelector(
|
|
388
|
-
`:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
|
|
389
|
-
);
|
|
390
|
-
const transWrapper = element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
|
|
391
|
-
if (!origWrapper || !transWrapper) continue;
|
|
392
|
-
if (showOriginal) {
|
|
393
|
-
origWrapper.style.display = "inline";
|
|
394
|
-
origWrapper.className = CLASS_ORIGINAL_SHOWN;
|
|
395
|
-
transWrapper.style.display = "none";
|
|
396
|
-
} else {
|
|
397
|
-
origWrapper.style.display = "none";
|
|
398
|
-
origWrapper.className = CLASS_ORIGINAL_HIDDEN;
|
|
399
|
-
transWrapper.style.display = "inline";
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
347
|
/**
|
|
404
348
|
* Unmounts translation and restores original DOM nodes completely.
|
|
405
349
|
*/
|
|
@@ -458,215 +402,7 @@ var NonDestructiveTranslationMount = class {
|
|
|
458
402
|
}
|
|
459
403
|
};
|
|
460
404
|
|
|
461
|
-
// src/
|
|
462
|
-
var StreamDebounceViewportObserver = class {
|
|
463
|
-
intersectionObserver = null;
|
|
464
|
-
/**
|
|
465
|
-
* Pending per-element streaming debounce timers. A Map (not a WeakMap)
|
|
466
|
-
* because disconnect() must enumerate and clear every pending timer: a
|
|
467
|
-
* WeakMap cannot be iterated, so a debounce armed just before the switch was
|
|
468
|
-
* turned off would still fire register() afterwards.
|
|
469
|
-
*/
|
|
470
|
-
streamingTimers = /* @__PURE__ */ new Map();
|
|
471
|
-
pendingQueue = [];
|
|
472
|
-
batchFlushTimer = null;
|
|
473
|
-
options;
|
|
474
|
-
constructor(options) {
|
|
475
|
-
this.options = {
|
|
476
|
-
rootMargin: options.rootMargin ?? "150px 0px",
|
|
477
|
-
debounceMs: options.debounceMs ?? 400,
|
|
478
|
-
onVisibleBatch: options.onVisibleBatch
|
|
479
|
-
};
|
|
480
|
-
this.initIntersectionObserver();
|
|
481
|
-
}
|
|
482
|
-
initIntersectionObserver() {
|
|
483
|
-
if (typeof window === "undefined" || typeof IntersectionObserver === "undefined") {
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
this.intersectionObserver = new IntersectionObserver(
|
|
487
|
-
(entries) => {
|
|
488
|
-
for (const entry of entries) {
|
|
489
|
-
if (entry.isIntersecting && entry.target instanceof HTMLElement) {
|
|
490
|
-
const el = entry.target;
|
|
491
|
-
this.intersectionObserver?.unobserve(el);
|
|
492
|
-
const text = el.dataset.tidyPendingText || NonDestructiveTranslationMount.extractVisibleText(el) || "";
|
|
493
|
-
if (text) {
|
|
494
|
-
delete el.dataset.tidyPendingText;
|
|
495
|
-
this.enqueueBatch(el, text);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
},
|
|
500
|
-
{
|
|
501
|
-
root: null,
|
|
502
|
-
// viewport
|
|
503
|
-
rootMargin: this.options.rootMargin,
|
|
504
|
-
threshold: 0
|
|
505
|
-
}
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Observe an element with streaming debounce.
|
|
510
|
-
* If streaming updates characterData repeatedly within debounceMs, the timer resets.
|
|
511
|
-
*/
|
|
512
|
-
observeWithDebounce(element, text, immediate = false) {
|
|
513
|
-
this.armDebounce(element, text, immediate);
|
|
514
|
-
}
|
|
515
|
-
armDebounce(element, text, immediate) {
|
|
516
|
-
if (!element || !text) return;
|
|
517
|
-
const existingTimer = this.streamingTimers.get(element);
|
|
518
|
-
if (existingTimer !== void 0) {
|
|
519
|
-
clearTimeout(existingTimer);
|
|
520
|
-
this.streamingTimers.delete(element);
|
|
521
|
-
}
|
|
522
|
-
if (immediate || this.options.debounceMs <= 0) {
|
|
523
|
-
this.register(element, text);
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
const timer = window.setTimeout(() => {
|
|
527
|
-
if (this.streamingTimers.get(element) !== timer) return;
|
|
528
|
-
this.streamingTimers.delete(element);
|
|
529
|
-
if (element.isConnected) {
|
|
530
|
-
const latestText = NonDestructiveTranslationMount.extractVisibleText(element) || text;
|
|
531
|
-
this.register(element, latestText);
|
|
532
|
-
}
|
|
533
|
-
}, this.options.debounceMs);
|
|
534
|
-
this.streamingTimers.set(element, timer);
|
|
535
|
-
}
|
|
536
|
-
register(element, text) {
|
|
537
|
-
if (!element.isConnected) return;
|
|
538
|
-
if (!this.intersectionObserver) {
|
|
539
|
-
this.enqueueBatch(element, text);
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
element.dataset.tidyPendingText = text;
|
|
543
|
-
this.intersectionObserver.observe(element);
|
|
544
|
-
}
|
|
545
|
-
enqueueBatch(element, text) {
|
|
546
|
-
this.pendingQueue.push({ element, text });
|
|
547
|
-
if (this.batchFlushTimer === null && typeof window !== "undefined") {
|
|
548
|
-
this.batchFlushTimer = window.setTimeout(() => {
|
|
549
|
-
this.batchFlushTimer = null;
|
|
550
|
-
this.flushQueue();
|
|
551
|
-
}, 50);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
flushQueue() {
|
|
555
|
-
if (this.pendingQueue.length === 0) return;
|
|
556
|
-
const batch = [...this.pendingQueue];
|
|
557
|
-
this.pendingQueue = [];
|
|
558
|
-
this.options.onVisibleBatch(batch);
|
|
559
|
-
}
|
|
560
|
-
unobserve(element) {
|
|
561
|
-
const timer = this.streamingTimers.get(element);
|
|
562
|
-
if (timer !== void 0) {
|
|
563
|
-
clearTimeout(timer);
|
|
564
|
-
this.streamingTimers.delete(element);
|
|
565
|
-
}
|
|
566
|
-
delete element.dataset.tidyPendingText;
|
|
567
|
-
if (this.intersectionObserver) {
|
|
568
|
-
this.intersectionObserver.unobserve(element);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
disconnect() {
|
|
572
|
-
if (this.batchFlushTimer !== null) {
|
|
573
|
-
clearTimeout(this.batchFlushTimer);
|
|
574
|
-
this.batchFlushTimer = null;
|
|
575
|
-
}
|
|
576
|
-
for (const timer of this.streamingTimers.values()) {
|
|
577
|
-
clearTimeout(timer);
|
|
578
|
-
}
|
|
579
|
-
this.streamingTimers.clear();
|
|
580
|
-
this.pendingQueue = [];
|
|
581
|
-
if (this.intersectionObserver) {
|
|
582
|
-
this.intersectionObserver.disconnect();
|
|
583
|
-
this.initIntersectionObserver();
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
// src/client/translate/lazy.ts
|
|
589
|
-
var LazyTranslationQueue = class {
|
|
590
|
-
enabled = true;
|
|
591
|
-
viewportObserver;
|
|
592
|
-
constructor() {
|
|
593
|
-
this.viewportObserver = new StreamDebounceViewportObserver({
|
|
594
|
-
rootMargin: "150px 0px",
|
|
595
|
-
debounceMs: 400,
|
|
596
|
-
onVisibleBatch: (items) => this.handleVisibleBatch(items)
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
setEnabled(enabled) {
|
|
600
|
-
this.enabled = enabled;
|
|
601
|
-
if (!enabled) {
|
|
602
|
-
this.viewportObserver.disconnect();
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
observe(element, text, immediate = false) {
|
|
606
|
-
if (!this.enabled || !element.isConnected) return;
|
|
607
|
-
const cached = clientCache.get(text);
|
|
608
|
-
if (cached) {
|
|
609
|
-
this.applyTranslation(element, cached, text);
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
this.viewportObserver.observeWithDebounce(element, text, immediate);
|
|
613
|
-
}
|
|
614
|
-
async handleVisibleBatch(items) {
|
|
615
|
-
if (!this.enabled || items.length === 0) return;
|
|
616
|
-
const sorted = [...items].sort((a, b) => {
|
|
617
|
-
if (a.element === b.element) return 0;
|
|
618
|
-
const pos = a.element.compareDocumentPosition(b.element);
|
|
619
|
-
return pos & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
|
620
|
-
});
|
|
621
|
-
const textMap = /* @__PURE__ */ new Map();
|
|
622
|
-
for (const item of sorted) {
|
|
623
|
-
if (!item.element.isConnected) continue;
|
|
624
|
-
const cached = clientCache.get(item.text);
|
|
625
|
-
if (cached) {
|
|
626
|
-
if (this.stillMatches(item.element, item.text)) {
|
|
627
|
-
this.applyTranslation(item.element, cached, item.text);
|
|
628
|
-
}
|
|
629
|
-
continue;
|
|
630
|
-
}
|
|
631
|
-
const list = textMap.get(item.text) || [];
|
|
632
|
-
list.push(item.element);
|
|
633
|
-
textMap.set(item.text, list);
|
|
634
|
-
}
|
|
635
|
-
const uniqueTexts = Array.from(textMap.keys());
|
|
636
|
-
if (uniqueTexts.length === 0) return;
|
|
637
|
-
const results = await requestTranslateBatch(uniqueTexts);
|
|
638
|
-
for (const res of results) {
|
|
639
|
-
const entries = textMap.get(res.original) || [];
|
|
640
|
-
if (res.translated && res.translated.trim() && res.channel !== "fallback" && res.channel !== "fallback-client" && res.translated.trim() !== res.original.trim()) {
|
|
641
|
-
clientCache.set(res.original, res.translated);
|
|
642
|
-
for (const entry of entries) {
|
|
643
|
-
if (!entry.isConnected || !this.enabled) continue;
|
|
644
|
-
if (!this.stillMatches(entry, res.original)) continue;
|
|
645
|
-
this.applyTranslation(entry, res.translated, res.original);
|
|
646
|
-
}
|
|
647
|
-
} else {
|
|
648
|
-
console.debug(`[dsh-chat-translate] \u7FFB\u8BD1\u672A\u6210\u529F (\u964D\u7EA7\u4FDD\u7559\u539F\u6587): "${res.original.slice(0, 40)}"`);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
/** 元素当前承载的原文是否仍是这次结果对应的那一份。 */
|
|
653
|
-
stillMatches(element, original) {
|
|
654
|
-
const current = NonDestructiveTranslationMount.extractVisibleText(element);
|
|
655
|
-
return !current || current === original;
|
|
656
|
-
}
|
|
657
|
-
applyTranslation(element, translated, original) {
|
|
658
|
-
if (!element.isConnected || !this.enabled) return;
|
|
659
|
-
NonDestructiveTranslationMount.mount(element, translated, {
|
|
660
|
-
originalText: original
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
disconnect() {
|
|
664
|
-
this.viewportObserver.disconnect();
|
|
665
|
-
}
|
|
666
|
-
};
|
|
667
|
-
var lazyQueue = new LazyTranslationQueue();
|
|
668
|
-
|
|
669
|
-
// src/server/pipeline/think.ts
|
|
405
|
+
// src/server/pipeline/blocks.ts
|
|
670
406
|
var CJK_CHAR = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/;
|
|
671
407
|
function estimateTokens(text) {
|
|
672
408
|
let cjk = 0;
|
|
@@ -676,118 +412,7 @@ function estimateTokens(text) {
|
|
|
676
412
|
return cjk + Math.ceil((text.length - cjk) / 3);
|
|
677
413
|
}
|
|
678
414
|
|
|
679
|
-
// src/client/translate/
|
|
680
|
-
var THINK_BUTTON_CLASS = "dsh-tidy-think-button";
|
|
681
|
-
var STYLE_TAG_ID = "@lynn123411/dsh-chat-translate/think-button.css";
|
|
682
|
-
var THINK_BUTTON_CSS = `
|
|
683
|
-
/* \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 */
|
|
684
|
-
.dsh-tidy-think-button {
|
|
685
|
-
display: inline-flex;
|
|
686
|
-
align-items: center;
|
|
687
|
-
justify-content: center;
|
|
688
|
-
position: relative;
|
|
689
|
-
flex: none;
|
|
690
|
-
width: 18px;
|
|
691
|
-
height: 18px;
|
|
692
|
-
margin: 0 6px 0 2px;
|
|
693
|
-
padding: 0;
|
|
694
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
695
|
-
border-radius: 6px;
|
|
696
|
-
background: transparent;
|
|
697
|
-
color: var(--dsw-alias-label-tertiary, currentColor);
|
|
698
|
-
font-family: inherit;
|
|
699
|
-
font-size: 10px;
|
|
700
|
-
line-height: 1;
|
|
701
|
-
cursor: pointer;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
.dsh-tidy-think-button:hover:not([disabled]) {
|
|
705
|
-
border-color: var(--dsw-alias-label-secondary, rgba(128, 128, 128, 0.6));
|
|
706
|
-
color: var(--dsw-alias-label-primary, currentColor);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
.dsh-tidy-think-button[data-state='translated'] {
|
|
710
|
-
border-color: transparent;
|
|
711
|
-
background: color-mix(in srgb, var(--dsw-static-deepseek-500, #4d6bfe) 18%, transparent);
|
|
712
|
-
color: var(--dsw-static-deepseek-500, #4d6bfe);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
.dsh-tidy-think-button[data-state='working'] {
|
|
716
|
-
color: transparent;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
.dsh-tidy-think-button[data-state='working']::after {
|
|
720
|
-
content: '';
|
|
721
|
-
position: absolute;
|
|
722
|
-
inset: 3px;
|
|
723
|
-
border: 1.5px solid var(--dsw-alias-label-caption, rgba(128, 128, 128, 0.45));
|
|
724
|
-
border-top-color: var(--dsw-static-deepseek-500, #4d6bfe);
|
|
725
|
-
border-radius: 50%;
|
|
726
|
-
animation: dsh-tidy-think-spin 0.8s linear infinite;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
.dsh-tidy-think-button[disabled] {
|
|
730
|
-
opacity: 0.35;
|
|
731
|
-
cursor: default;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
@keyframes dsh-tidy-think-spin {
|
|
735
|
-
to { transform: rotate(360deg); }
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
@media (prefers-reduced-motion: reduce) {
|
|
739
|
-
.dsh-tidy-think-button[data-state='working']::after {
|
|
740
|
-
animation: none;
|
|
741
|
-
border-color: var(--dsw-static-deepseek-500, #4d6bfe);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
`;
|
|
745
|
-
function ensureThinkButtonStyles() {
|
|
746
|
-
if (typeof document === "undefined") return;
|
|
747
|
-
if (document.querySelector('style[data-plugin-css="' + STYLE_TAG_ID + '"]') !== null) return;
|
|
748
|
-
const style = document.createElement("style");
|
|
749
|
-
style.dataset.plugin = "@lynn123411/dsh-chat-translate";
|
|
750
|
-
style.dataset.pluginCss = STYLE_TAG_ID;
|
|
751
|
-
style.textContent = THINK_BUTTON_CSS;
|
|
752
|
-
document.head?.appendChild(style);
|
|
753
|
-
}
|
|
754
|
-
function createThinkButton(onActivate) {
|
|
755
|
-
ensureThinkButtonStyles();
|
|
756
|
-
const element = document.createElement("button");
|
|
757
|
-
element.type = "button";
|
|
758
|
-
element.className = THINK_BUTTON_CLASS;
|
|
759
|
-
element.textContent = "\u8BD1";
|
|
760
|
-
element.dataset.state = "idle";
|
|
761
|
-
element.setAttribute("aria-label", "\u7FFB\u8BD1\u601D\u8003\u94FE");
|
|
762
|
-
element.title = "\u7FFB\u8BD1\u601D\u8003\u94FE";
|
|
763
|
-
element.addEventListener("click", (event) => {
|
|
764
|
-
event.stopPropagation();
|
|
765
|
-
event.preventDefault();
|
|
766
|
-
if (element.disabled) return;
|
|
767
|
-
onActivate();
|
|
768
|
-
});
|
|
769
|
-
const setState = (state) => {
|
|
770
|
-
element.dataset.state = state;
|
|
771
|
-
const label = state === "translated" ? "\u663E\u793A\u601D\u8003\u539F\u6587" : "\u7FFB\u8BD1\u601D\u8003\u94FE";
|
|
772
|
-
element.setAttribute("aria-label", label);
|
|
773
|
-
element.title = label;
|
|
774
|
-
};
|
|
775
|
-
return {
|
|
776
|
-
element,
|
|
777
|
-
getState: () => element.dataset.state ?? "idle",
|
|
778
|
-
setState,
|
|
779
|
-
setDisabled: (disabled, reason) => {
|
|
780
|
-
element.disabled = disabled;
|
|
781
|
-
if (disabled) element.title = reason;
|
|
782
|
-
else setState(element.dataset.state ?? "idle");
|
|
783
|
-
}
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// src/client/translate/think.ts
|
|
788
|
-
var THINK_CARD_SELECTOR = '[data-variant="think"]';
|
|
789
|
-
var THINK_BODY_SELECTOR = '[class*="thinkBody" i]';
|
|
790
|
-
var EXPANDED_SELECTOR = "[data-open]";
|
|
415
|
+
// src/client/translate/reply.ts
|
|
791
416
|
var BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
792
417
|
"P",
|
|
793
418
|
"H1",
|
|
@@ -807,19 +432,22 @@ var BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
|
807
432
|
var CONTAINER_TAGS = /* @__PURE__ */ new Set(["UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR", "DIV", "SECTION"]);
|
|
808
433
|
var NESTED_BOUNDARY_SELECTOR = [...BLOCK_TAGS, ...CONTAINER_TAGS, "PRE"].join(",").toLowerCase();
|
|
809
434
|
var REQUEST_TOKENS = 2048;
|
|
810
|
-
var
|
|
435
|
+
var HAS_LATIN = /[A-Za-z]/;
|
|
811
436
|
function isMountedElement(element) {
|
|
812
437
|
return element.dataset?.tidyTranslated === "true";
|
|
813
438
|
}
|
|
814
439
|
function hasNestedBoundary(element) {
|
|
815
440
|
return element.querySelector(NESTED_BOUNDARY_SELECTOR) !== null;
|
|
816
441
|
}
|
|
817
|
-
function
|
|
442
|
+
function needsTranslation(text) {
|
|
443
|
+
return HAS_LATIN.test(text);
|
|
444
|
+
}
|
|
445
|
+
function collectReplyUnits(body) {
|
|
818
446
|
const units = [];
|
|
819
447
|
const pushRun = (nodes) => {
|
|
820
448
|
if (nodes.length === 0) return;
|
|
821
449
|
const text = nodes.map((node) => node.textContent ?? "").join("").trim();
|
|
822
|
-
if (!text) return;
|
|
450
|
+
if (!text || !needsTranslation(text)) return;
|
|
823
451
|
const anchor = nodes[0];
|
|
824
452
|
if (anchor === void 0) return;
|
|
825
453
|
units.push({ nodes, text, anchor });
|
|
@@ -862,7 +490,7 @@ function collectThinkUnits(body) {
|
|
|
862
490
|
if (isMountedElement(element)) return;
|
|
863
491
|
if (!hasNestedBoundary(element)) {
|
|
864
492
|
const text = (element.textContent ?? "").trim();
|
|
865
|
-
if (text) units.push({ element, text, anchor: element });
|
|
493
|
+
if (text && needsTranslation(text)) units.push({ element, text, anchor: element });
|
|
866
494
|
return;
|
|
867
495
|
}
|
|
868
496
|
walk(element);
|
|
@@ -887,226 +515,65 @@ function chunkUnits(units, maxTokens = REQUEST_TOKENS) {
|
|
|
887
515
|
if (current.length > 0) chunks.push(current);
|
|
888
516
|
return chunks;
|
|
889
517
|
}
|
|
890
|
-
var
|
|
891
|
-
/**
|
|
892
|
-
|
|
893
|
-
/**
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
if (enabled === this.enabled) return;
|
|
908
|
-
this.enabled = enabled;
|
|
909
|
-
this.applyVisibility();
|
|
910
|
-
}
|
|
911
|
-
setConfigured(configured) {
|
|
912
|
-
if (configured === this.configured) return;
|
|
913
|
-
this.configured = configured;
|
|
914
|
-
this.applyVisibility();
|
|
915
|
-
}
|
|
916
|
-
/** 按钮是否可以存在:两个开关与 AI 通道配置缺一不可。 */
|
|
917
|
-
usable() {
|
|
918
|
-
return this.enabled && this.configured;
|
|
919
|
-
}
|
|
920
|
-
applyVisibility() {
|
|
921
|
-
if (typeof document === "undefined") return;
|
|
922
|
-
if (this.usable()) {
|
|
923
|
-
this.syncAll();
|
|
924
|
-
return;
|
|
925
|
-
}
|
|
926
|
-
for (const element of document.querySelectorAll("." + THINK_BUTTON_CLASS)) {
|
|
927
|
-
element.remove();
|
|
928
|
-
}
|
|
929
|
-
for (const card of document.querySelectorAll(THINK_CARD_SELECTOR)) {
|
|
930
|
-
const state = this.states.get(card);
|
|
931
|
-
if (state !== void 0) {
|
|
932
|
-
state.button = null;
|
|
933
|
-
state.state = "idle";
|
|
934
|
-
state.expandedHandled = false;
|
|
935
|
-
}
|
|
936
|
-
NonDestructiveTranslationMount.restore(card);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
/** 扫描当前会话里的全部思考卡(开关、配置或会话变化后调用)。 */
|
|
940
|
-
syncAll() {
|
|
941
|
-
const scope = this.scope ?? (typeof document === "undefined" ? null : document);
|
|
942
|
-
if (scope === null) return;
|
|
943
|
-
for (const card of scope.querySelectorAll(THINK_CARD_SELECTOR)) {
|
|
944
|
-
this.syncCard(card);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
/** 防抖后同步一张卡片。 */
|
|
948
|
-
syncCard(card) {
|
|
949
|
-
if (typeof window === "undefined") return;
|
|
950
|
-
const existing = this.syncTimers.get(card);
|
|
951
|
-
if (existing !== void 0) window.clearTimeout(existing);
|
|
952
|
-
const timer = window.setTimeout(() => {
|
|
953
|
-
this.syncTimers.delete(card);
|
|
954
|
-
this.runSync(card);
|
|
955
|
-
}, SYNC_DEBOUNCE_MS);
|
|
956
|
-
this.syncTimers.set(card, timer);
|
|
957
|
-
}
|
|
958
|
-
stateFor(card) {
|
|
959
|
-
let state = this.states.get(card);
|
|
960
|
-
if (state === void 0) {
|
|
961
|
-
state = { button: null, state: "idle", expandedHandled: false };
|
|
962
|
-
this.states.set(card, state);
|
|
963
|
-
}
|
|
964
|
-
return state;
|
|
965
|
-
}
|
|
966
|
-
runSync(card) {
|
|
967
|
-
if (!card.isConnected) return;
|
|
968
|
-
const state = this.stateFor(card);
|
|
969
|
-
if (!this.usable()) {
|
|
970
|
-
this.discardCard(card, state);
|
|
971
|
-
return;
|
|
972
|
-
}
|
|
973
|
-
const expanded = card.querySelector(EXPANDED_SELECTOR) !== null;
|
|
974
|
-
if (expanded) this.ensureButton(card, state);
|
|
975
|
-
else {
|
|
976
|
-
this.removeButton(state);
|
|
977
|
-
state.expandedHandled = false;
|
|
978
|
-
}
|
|
979
|
-
if (state.state !== "working") {
|
|
980
|
-
state.state = this.isShowingTranslation(card) ? "translated" : "idle";
|
|
981
|
-
}
|
|
982
|
-
state.button?.setState(state.state);
|
|
983
|
-
this.updateDisabled(card, state);
|
|
984
|
-
if (!expanded || state.expandedHandled || state.state === "working") return;
|
|
985
|
-
state.expandedHandled = true;
|
|
986
|
-
this.showCachedWhenComplete(card, state);
|
|
987
|
-
}
|
|
988
|
-
updateDisabled(card, state) {
|
|
989
|
-
const running = card.dataset.state === "running";
|
|
990
|
-
state.button?.setDisabled(running && state.state !== "working", "\u601D\u8003\u8F93\u51FA\u4E2D\uFF0C\u7ED3\u675F\u540E\u53EF\u7FFB\u8BD1");
|
|
991
|
-
}
|
|
992
|
-
ensureButton(card, state) {
|
|
993
|
-
if (state.button !== null) return;
|
|
994
|
-
const row = card.querySelector("[data-disclosure-row]");
|
|
995
|
-
if (row === null) return;
|
|
996
|
-
const button = createThinkButton(() => this.activate(card));
|
|
997
|
-
const title = row.querySelector('[class*="title" i]');
|
|
998
|
-
if (title !== null && title.parentElement === row) {
|
|
999
|
-
row.insertBefore(button.element, title.nextSibling);
|
|
1000
|
-
} else {
|
|
1001
|
-
row.appendChild(button.element);
|
|
1002
|
-
}
|
|
1003
|
-
state.button = button;
|
|
1004
|
-
button.setState(state.state);
|
|
1005
|
-
}
|
|
1006
|
-
/** 折叠或停用时撤掉按钮,卡片的翻译状态不变。 */
|
|
1007
|
-
removeButton(state) {
|
|
1008
|
-
state.button?.element.remove();
|
|
1009
|
-
state.button = null;
|
|
1010
|
-
}
|
|
1011
|
-
/** 插件停用:撤掉按钮并还原这张卡片上的全部译文。 */
|
|
1012
|
-
discardCard(card, state) {
|
|
1013
|
-
this.removeButton(state);
|
|
1014
|
-
state.state = "idle";
|
|
1015
|
-
state.expandedHandled = false;
|
|
1016
|
-
NonDestructiveTranslationMount.restore(card);
|
|
1017
|
-
}
|
|
1018
|
-
/** 卡片当前显示的是译文(至少有一个译文块可见)。 */
|
|
1019
|
-
isShowingTranslation(card) {
|
|
1020
|
-
const blocks = card.querySelectorAll("." + CLASS_TRANSLATED_BLOCK);
|
|
1021
|
-
for (const block of blocks) {
|
|
1022
|
-
if (block.style.display !== "none") return true;
|
|
1023
|
-
}
|
|
1024
|
-
return false;
|
|
1025
|
-
}
|
|
1026
|
-
bodyOf(card) {
|
|
1027
|
-
return card.querySelector(THINK_BODY_SELECTOR);
|
|
518
|
+
var ReplyTranslateController = class {
|
|
519
|
+
/** 正文容器 → 在途 Promise,避免同一条回答被重复翻译。 */
|
|
520
|
+
inFlight = /* @__PURE__ */ new WeakMap();
|
|
521
|
+
/**
|
|
522
|
+
* 翻译一条回答的正文容器。已命中缓存的单位立即挂上,其余按块请求;
|
|
523
|
+
* 失败的块保留原文(结果不入缓存,下次滚回视口会自然重试)。
|
|
524
|
+
* @param body - 正文容器(markdown body)。
|
|
525
|
+
* @returns 本次翻译完成后结算(已在途则返回同一次在途)。
|
|
526
|
+
*/
|
|
527
|
+
translateBody(body) {
|
|
528
|
+
const running = this.inFlight.get(body);
|
|
529
|
+
if (running !== void 0) return running;
|
|
530
|
+
const task = this.run(body).finally(() => {
|
|
531
|
+
this.inFlight.delete(body);
|
|
532
|
+
});
|
|
533
|
+
this.inFlight.set(body, task);
|
|
534
|
+
return task;
|
|
1028
535
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
const body = this.bodyOf(card);
|
|
1032
|
-
if (body === null || state.state === "working") return;
|
|
1033
|
-
const units = collectThinkUnits(body);
|
|
536
|
+
async run(body) {
|
|
537
|
+
const units = collectReplyUnits(body);
|
|
1034
538
|
if (units.length === 0) return;
|
|
1035
|
-
const cached = units.map((unit) => thinkClientCache.get(unit.text));
|
|
1036
|
-
if (cached.some((value) => value === void 0)) return;
|
|
1037
|
-
units.forEach((unit, index) => this.applyUnit(unit, cached[index]));
|
|
1038
|
-
state.state = this.isShowingTranslation(card) ? "translated" : "idle";
|
|
1039
|
-
state.button?.setState(state.state);
|
|
1040
|
-
}
|
|
1041
|
-
/** 按钮被点击;按钮只在展开的卡片上存在。 */
|
|
1042
|
-
activate(card) {
|
|
1043
|
-
const state = this.stateFor(card);
|
|
1044
|
-
if (!this.usable() || state.state === "working") return;
|
|
1045
|
-
if (card.dataset.state === "running") return;
|
|
1046
|
-
if (state.state === "translated") {
|
|
1047
|
-
NonDestructiveTranslationMount.toggleGroup(card, true);
|
|
1048
|
-
state.state = "idle";
|
|
1049
|
-
state.button?.setState("idle");
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
this.translateCard(card, state);
|
|
1053
|
-
}
|
|
1054
|
-
/** 翻译这张卡片的全部单位:命中缓存的先挂上,其余按块请求。 */
|
|
1055
|
-
translateCard(card, state) {
|
|
1056
|
-
const body = this.bodyOf(card);
|
|
1057
|
-
if (body === null) return;
|
|
1058
|
-
if (card.querySelector("." + CLASS_TRANSLATED_BLOCK) !== null) {
|
|
1059
|
-
NonDestructiveTranslationMount.toggleGroup(card, false);
|
|
1060
|
-
}
|
|
1061
|
-
const units = collectThinkUnits(body);
|
|
1062
|
-
if (units.length === 0) {
|
|
1063
|
-
state.state = this.isShowingTranslation(card) ? "translated" : "idle";
|
|
1064
|
-
state.button?.setState(state.state);
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
539
|
const missing = [];
|
|
1068
540
|
for (const unit of units) {
|
|
1069
|
-
const cached =
|
|
541
|
+
const cached = clientCache.get(unit.text);
|
|
1070
542
|
if (cached === void 0) missing.push(unit);
|
|
1071
543
|
else this.applyUnit(unit, cached);
|
|
1072
544
|
}
|
|
1073
|
-
if (missing.length === 0)
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
async translateUnits(card, state, units) {
|
|
1083
|
-
let failed = false;
|
|
1084
|
-
for (const chunk of chunkUnits(units, REQUEST_TOKENS)) {
|
|
1085
|
-
if (!this.usable()) {
|
|
1086
|
-
failed = true;
|
|
1087
|
-
break;
|
|
1088
|
-
}
|
|
545
|
+
if (missing.length === 0) return;
|
|
546
|
+
await this.translateUnits(missing);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* 逐块请求并挂载。失败的块保持原文:界面上不留提示,控制台留一条警告,结果
|
|
550
|
+
* 不进缓存,因此这一块在下次进入视口时会自然重试。
|
|
551
|
+
*/
|
|
552
|
+
async translateUnits(units) {
|
|
553
|
+
for (const chunk of chunkUnits(units)) {
|
|
1089
554
|
const texts = chunk.map((unit) => unit.text);
|
|
1090
|
-
let results
|
|
555
|
+
let results;
|
|
1091
556
|
try {
|
|
1092
|
-
results = await
|
|
1093
|
-
} catch {
|
|
1094
|
-
|
|
557
|
+
results = await requestTranslateReply(texts);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
console.warn(
|
|
560
|
+
`[dsh-chat-translate] \u6B63\u6587\u7FFB\u8BD1\u5757\u8BF7\u6C42\u5931\u8D25\uFF0C\u4FDD\u7559\u539F\u6587: ${err?.message ?? err}`
|
|
561
|
+
);
|
|
562
|
+
continue;
|
|
1095
563
|
}
|
|
1096
564
|
const byText = new Map(results.map((result) => [result.original, result]));
|
|
1097
565
|
for (const unit of chunk) {
|
|
1098
566
|
const result = byText.get(unit.text);
|
|
1099
567
|
if (result === void 0 || !result.ok || !result.translated.trim() || result.translated.trim() === unit.text.trim()) {
|
|
1100
|
-
|
|
568
|
+
console.warn(
|
|
569
|
+
`[dsh-chat-translate] \u6B63\u6587\u7FFB\u8BD1\u672A\u6210\u529F\uFF0C\u4FDD\u7559\u539F\u6587: "${unit.text.slice(0, 40)}"`
|
|
570
|
+
);
|
|
1101
571
|
continue;
|
|
1102
572
|
}
|
|
1103
|
-
|
|
573
|
+
clientCache.set(unit.text, result.translated);
|
|
1104
574
|
this.applyUnit(unit, result.translated);
|
|
1105
575
|
}
|
|
1106
576
|
}
|
|
1107
|
-
state.state = !failed && this.isShowingTranslation(card) ? "translated" : "idle";
|
|
1108
|
-
state.button?.setState(state.state);
|
|
1109
|
-
this.updateDisabled(card, state);
|
|
1110
577
|
}
|
|
1111
578
|
/** 把译文挂到单位上;期间文本或节点已经变动的单位直接放弃(结果已进缓存)。 */
|
|
1112
579
|
applyUnit(unit, translated) {
|
|
@@ -1115,8 +582,7 @@ var ThinkTranslateController = class {
|
|
|
1115
582
|
if (!element.isConnected || isMountedElement(element)) return;
|
|
1116
583
|
if ((element.textContent ?? "").trim() !== unit.text) return;
|
|
1117
584
|
NonDestructiveTranslationMount.mount(element, translated, {
|
|
1118
|
-
originalText: unit.text
|
|
1119
|
-
interactive: false
|
|
585
|
+
originalText: unit.text
|
|
1120
586
|
});
|
|
1121
587
|
return;
|
|
1122
588
|
}
|
|
@@ -1127,58 +593,131 @@ var ThinkTranslateController = class {
|
|
|
1127
593
|
const text = nodes.map((node) => node.textContent ?? "").join("").trim();
|
|
1128
594
|
if (text !== unit.text) return;
|
|
1129
595
|
NonDestructiveTranslationMount.mountRun(nodes, translated, {
|
|
1130
|
-
originalText: unit.text
|
|
1131
|
-
interactive: false
|
|
596
|
+
originalText: unit.text
|
|
1132
597
|
});
|
|
1133
598
|
}
|
|
1134
599
|
};
|
|
1135
|
-
var
|
|
600
|
+
var replyTranslator = new ReplyTranslateController();
|
|
601
|
+
|
|
602
|
+
// src/client/translate/lazy.ts
|
|
603
|
+
var VIEWPORT_MARGIN = "150px 0px";
|
|
604
|
+
var SETTLE_DEBOUNCE_MS = 120;
|
|
605
|
+
var LazyTranslationQueue = class {
|
|
606
|
+
enabled = true;
|
|
607
|
+
observer = null;
|
|
608
|
+
pending = /* @__PURE__ */ new Map();
|
|
609
|
+
setEnabled(enabled) {
|
|
610
|
+
this.enabled = enabled;
|
|
611
|
+
if (!enabled) {
|
|
612
|
+
this.disconnect();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
observe(body, immediate = false) {
|
|
616
|
+
if (!this.enabled || !body.isConnected) return;
|
|
617
|
+
if (immediate || typeof IntersectionObserver === "undefined") {
|
|
618
|
+
void replyTranslator.translateBody(body);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const observer = this.intersectionObserver();
|
|
622
|
+
if (observer === null) {
|
|
623
|
+
void replyTranslator.translateBody(body);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
observer.observe(body);
|
|
627
|
+
}
|
|
628
|
+
intersectionObserver() {
|
|
629
|
+
if (typeof window === "undefined") return null;
|
|
630
|
+
if (this.observer !== null) return this.observer;
|
|
631
|
+
this.observer = new IntersectionObserver(
|
|
632
|
+
(entries) => {
|
|
633
|
+
for (const entry of entries) {
|
|
634
|
+
if (!entry.isIntersecting) continue;
|
|
635
|
+
const body = entry.target;
|
|
636
|
+
if (!(body instanceof HTMLElement)) continue;
|
|
637
|
+
this.observer?.unobserve(body);
|
|
638
|
+
this.scheduleTranslation(body);
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
{ root: null, rootMargin: VIEWPORT_MARGIN, threshold: 0 }
|
|
642
|
+
);
|
|
643
|
+
return this.observer;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* 进入视口后稍等一拍再翻,避开「刚出现、还在布局」的那一瞬。已经在等的那条
|
|
647
|
+
* 不重复排队。
|
|
648
|
+
*/
|
|
649
|
+
scheduleTranslation(body) {
|
|
650
|
+
if (typeof window === "undefined") {
|
|
651
|
+
void replyTranslator.translateBody(body);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (this.pending.has(body)) return;
|
|
655
|
+
const timer = window.setTimeout(() => {
|
|
656
|
+
this.pending.delete(body);
|
|
657
|
+
if (!this.enabled || !body.isConnected) return;
|
|
658
|
+
void replyTranslator.translateBody(body);
|
|
659
|
+
}, SETTLE_DEBOUNCE_MS);
|
|
660
|
+
this.pending.set(body, timer);
|
|
661
|
+
}
|
|
662
|
+
disconnect() {
|
|
663
|
+
if (typeof window !== "undefined") {
|
|
664
|
+
for (const timer of this.pending.values()) window.clearTimeout(timer);
|
|
665
|
+
}
|
|
666
|
+
this.pending.clear();
|
|
667
|
+
if (this.observer) {
|
|
668
|
+
this.observer.disconnect();
|
|
669
|
+
this.observer = null;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
var lazyQueue = new LazyTranslationQueue();
|
|
1136
674
|
|
|
1137
675
|
// src/client/translate/observer.ts
|
|
1138
|
-
var
|
|
1139
|
-
var
|
|
676
|
+
var REPLY_ROW_SELECTOR = '[data-chat-flow-kind][data-chat-group-part="response"]';
|
|
677
|
+
var MARKDOWN_BODY_SELECTOR = '[class*="body" i]';
|
|
678
|
+
var PROCESS_MEMBER_SELECTOR = "[data-turn-process-member]";
|
|
679
|
+
var PROCESS_ANSWER_SELECTOR = "[data-turn-process-answer]";
|
|
680
|
+
var EXCLUDED_SELECTOR = [
|
|
681
|
+
'[data-variant="think"]',
|
|
682
|
+
"[data-chat-call-id]",
|
|
683
|
+
'[data-slot="tool.call.toolview"]',
|
|
684
|
+
'[data-chat-group-part="reasoning"]'
|
|
685
|
+
].join(",");
|
|
1140
686
|
var SESSION_ROOT_SELECTOR = "[data-conversation-scroll], [data-chat-flow]";
|
|
1141
687
|
var ROOT_CHECK_INTERVAL_MS = 3e3;
|
|
1142
|
-
function
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
const rawToggle = (span.textContent || "").trim();
|
|
1152
|
-
if (rawToggle.length <= 12 && /^(展开|收起|展开全部|收起全部|Expand|Collapse|Show more|Show less|Think|思考)$/i.test(rawToggle)) {
|
|
1153
|
-
return false;
|
|
1154
|
-
}
|
|
1155
|
-
if (rawToggle === "Think" || rawToggle === "\u601D\u8003") return false;
|
|
1156
|
-
if (span.closest('button, [role="button"]') && rawToggle.length <= 12 && /展开|收起|Expand|Collapse|Think|思考/i.test(rawToggle)) {
|
|
1157
|
-
return false;
|
|
1158
|
-
}
|
|
1159
|
-
if (span.closest(
|
|
1160
|
-
'[data-chat-call-id], [data-slot="tool.call.toolview"], [data-sample], [data-variant], [data-tool], [data-disclosure-row]'
|
|
1161
|
-
)) {
|
|
1162
|
-
return true;
|
|
1163
|
-
}
|
|
1164
|
-
return false;
|
|
688
|
+
function isStreaming(root) {
|
|
689
|
+
return root.hasAttribute("data-streaming");
|
|
690
|
+
}
|
|
691
|
+
function isInsideProcessDisclosure(element) {
|
|
692
|
+
const row = element.closest(PROCESS_MEMBER_SELECTOR);
|
|
693
|
+
if (row === null || !row.contains(element)) return false;
|
|
694
|
+
if (row.matches(PROCESS_ANSWER_SELECTOR)) return false;
|
|
695
|
+
if (row.querySelector(PROCESS_ANSWER_SELECTOR) !== null) return false;
|
|
696
|
+
return true;
|
|
1165
697
|
}
|
|
1166
698
|
var ChatTranslateObserver = class {
|
|
1167
699
|
observer = null;
|
|
1168
700
|
rootElement = null;
|
|
1169
701
|
rootCheckTimer = null;
|
|
1170
702
|
isEnabled = true;
|
|
1171
|
-
/**
|
|
1172
|
-
|
|
703
|
+
/**
|
|
704
|
+
* 已经交给翻译器的正文容器,避免同一条回答在每次 DOM 变动时重扫。总开关重开
|
|
705
|
+
* 时清空:关着的那段时间落定的回答需要补翻。
|
|
706
|
+
*/
|
|
707
|
+
handled = /* @__PURE__ */ new WeakSet();
|
|
1173
708
|
constructor() {
|
|
1174
709
|
this.handleMutations = this.handleMutations.bind(this);
|
|
1175
710
|
}
|
|
1176
711
|
setEnabled(enabled) {
|
|
712
|
+
if (enabled === this.isEnabled) return;
|
|
1177
713
|
this.isEnabled = enabled;
|
|
1178
|
-
thinkTranslator.setEnabled(enabled && this.thinkEnabled);
|
|
1179
714
|
if (enabled) {
|
|
1180
715
|
lazyQueue.setEnabled(true);
|
|
716
|
+
this.handled = /* @__PURE__ */ new WeakSet();
|
|
717
|
+
this.observer?.disconnect();
|
|
718
|
+
this.observer = null;
|
|
1181
719
|
this.start();
|
|
720
|
+
if (this.rootElement !== null) this.scanContainer(this.rootElement);
|
|
1182
721
|
} else {
|
|
1183
722
|
this.restoreOriginals();
|
|
1184
723
|
this.disconnect();
|
|
@@ -1188,21 +727,7 @@ var ChatTranslateObserver = class {
|
|
|
1188
727
|
restoreOriginals() {
|
|
1189
728
|
const scope = this.rootElement ?? document;
|
|
1190
729
|
NonDestructiveTranslationMount.restore(scope);
|
|
1191
|
-
|
|
1192
|
-
/**
|
|
1193
|
-
* 设置里的思考链开关:关掉就撤掉所有翻译按钮并还原思考卡里的译文,工具标题
|
|
1194
|
-
* 译文不动。
|
|
1195
|
-
*/
|
|
1196
|
-
setThinkEnabled(enabled) {
|
|
1197
|
-
this.thinkEnabled = enabled;
|
|
1198
|
-
thinkTranslator.setEnabled(this.isEnabled && enabled);
|
|
1199
|
-
}
|
|
1200
|
-
/**
|
|
1201
|
-
* AI 通道的可用性(通道开关打开且 Key / Base URL / 模型齐全)。不可用时同样
|
|
1202
|
-
* 不注入按钮:思考链翻译只能走这条通道。
|
|
1203
|
-
*/
|
|
1204
|
-
setThinkConfigured(configured) {
|
|
1205
|
-
thinkTranslator.setConfigured(configured);
|
|
730
|
+
this.handled = /* @__PURE__ */ new WeakSet();
|
|
1206
731
|
}
|
|
1207
732
|
/**
|
|
1208
733
|
* The active session's scroll container. Only the currently-viewed session
|
|
@@ -1238,9 +763,7 @@ var ChatTranslateObserver = class {
|
|
|
1238
763
|
const findAndObserveRoot = () => {
|
|
1239
764
|
const root = this.findRoot(documentRef);
|
|
1240
765
|
this.rootElement = root;
|
|
1241
|
-
thinkTranslator.setScope(root);
|
|
1242
766
|
this.scanContainer(root);
|
|
1243
|
-
this.scanThink(root);
|
|
1244
767
|
if (!this.observer) {
|
|
1245
768
|
this.observer = new MutationObserver(this.handleMutations);
|
|
1246
769
|
this.observer.observe(root, {
|
|
@@ -1248,14 +771,14 @@ var ChatTranslateObserver = class {
|
|
|
1248
771
|
subtree: true,
|
|
1249
772
|
attributes: true,
|
|
1250
773
|
characterData: true,
|
|
774
|
+
// `data-streaming` 从 markdown 根上消失就是落定信号;`data-turn-process-*`
|
|
775
|
+
// 是内核改判「哪些 flow item 属于折叠块」时动的属性,它一变就要重扫。
|
|
1251
776
|
attributeFilter: [
|
|
1252
|
-
"data-
|
|
1253
|
-
"data-
|
|
1254
|
-
"data-
|
|
1255
|
-
"data-
|
|
1256
|
-
"
|
|
1257
|
-
"data-open",
|
|
1258
|
-
"data-expanded"
|
|
777
|
+
"data-streaming",
|
|
778
|
+
"data-chat-flow-kind",
|
|
779
|
+
"data-chat-group-part",
|
|
780
|
+
"data-turn-process-member",
|
|
781
|
+
"data-turn-process-answer"
|
|
1259
782
|
]
|
|
1260
783
|
});
|
|
1261
784
|
}
|
|
@@ -1288,131 +811,101 @@ var ChatTranslateObserver = class {
|
|
|
1288
811
|
for (const mutation of mutations) {
|
|
1289
812
|
if (mutation.type === "childList") {
|
|
1290
813
|
let addedElement = false;
|
|
1291
|
-
let changedOwner = null;
|
|
1292
814
|
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
|
1293
815
|
const node = mutation.addedNodes[i];
|
|
1294
816
|
if (node instanceof HTMLElement) {
|
|
1295
|
-
const owner = this.mountedOwner(node);
|
|
1296
|
-
if (owner) {
|
|
1297
|
-
changedOwner = owner;
|
|
1298
|
-
continue;
|
|
1299
|
-
}
|
|
1300
817
|
if (NonDestructiveTranslationMount.isOwnNode(node)) continue;
|
|
1301
818
|
addedElement = true;
|
|
1302
819
|
this.scanNode(node);
|
|
1303
|
-
this.scanThink(node);
|
|
1304
820
|
}
|
|
1305
821
|
}
|
|
1306
822
|
const target = mutation.target;
|
|
1307
823
|
if (target instanceof HTMLElement) {
|
|
1308
|
-
|
|
1309
|
-
if (owner) {
|
|
1310
|
-
changedOwner = owner;
|
|
1311
|
-
} else if (!addedElement && !NonDestructiveTranslationMount.isOwnNode(target)) {
|
|
824
|
+
if (!addedElement && !NonDestructiveTranslationMount.isOwnNode(target)) {
|
|
1312
825
|
this.scanNode(target);
|
|
1313
|
-
this.scanThink(target);
|
|
1314
826
|
}
|
|
1315
827
|
}
|
|
1316
|
-
if (changedOwner) this.handleOriginalTextChange(changedOwner);
|
|
1317
828
|
} else if (mutation.type === "attributes") {
|
|
1318
829
|
const target = mutation.target;
|
|
1319
830
|
if (target instanceof HTMLElement) {
|
|
1320
831
|
if (NonDestructiveTranslationMount.isOwnNode(target)) continue;
|
|
1321
832
|
this.scanNode(target);
|
|
1322
|
-
this.scanThink(target);
|
|
1323
833
|
}
|
|
1324
834
|
} else if (mutation.type === "characterData") {
|
|
1325
|
-
const owner = this.mountedOwner(mutation.target);
|
|
1326
|
-
if (owner) {
|
|
1327
|
-
this.handleOriginalTextChange(owner);
|
|
1328
|
-
continue;
|
|
1329
|
-
}
|
|
1330
835
|
const parent = mutation.target.parentElement;
|
|
1331
836
|
if (parent instanceof HTMLElement) {
|
|
1332
837
|
if (NonDestructiveTranslationMount.isOwnNode(parent)) continue;
|
|
1333
838
|
this.scanNode(parent);
|
|
1334
|
-
this.scanThink(parent);
|
|
1335
839
|
}
|
|
1336
840
|
}
|
|
1337
841
|
}
|
|
1338
842
|
}
|
|
1339
843
|
/**
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
844
|
+
* 扫一棵子树,把里面**全部**该翻的正文容器交给翻译器。
|
|
845
|
+
*
|
|
846
|
+
* 不再「命中第一个就返回」:一个容器里可以有好几行 response(折叠块里每一步
|
|
847
|
+
* 露出的一段正文就是一行),只取第一个会把后面那些整片漏掉。
|
|
1342
848
|
*/
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
if (
|
|
1346
|
-
const
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
if (!wrapper) return null;
|
|
1350
|
-
const owner = wrapper.parentElement;
|
|
1351
|
-
return owner instanceof HTMLElement && owner.dataset.tidyTranslated === "true" ? owner : null;
|
|
849
|
+
scanContainer(container) {
|
|
850
|
+
this.tryBodies(container);
|
|
851
|
+
if (container === null) return;
|
|
852
|
+
for (const row of container.querySelectorAll(REPLY_ROW_SELECTOR)) {
|
|
853
|
+
this.tryRow(row);
|
|
854
|
+
}
|
|
1352
855
|
}
|
|
1353
|
-
/**
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
if (!current || current === owner.dataset.original) return;
|
|
1361
|
-
const cached = clientCache.get(current);
|
|
1362
|
-
if (cached) {
|
|
1363
|
-
NonDestructiveTranslationMount.mount(owner, cached, { originalText: current });
|
|
1364
|
-
return;
|
|
856
|
+
/** 扫一个刚变动过的节点(含它所在的这一行,以及它内部的每一行)。 */
|
|
857
|
+
scanNode(node) {
|
|
858
|
+
this.tryBodies(node);
|
|
859
|
+
const row = node.closest(REPLY_ROW_SELECTOR);
|
|
860
|
+
if (row !== null && row !== node) this.tryRow(row);
|
|
861
|
+
for (const child of node.querySelectorAll(REPLY_ROW_SELECTOR)) {
|
|
862
|
+
this.tryRow(child);
|
|
1365
863
|
}
|
|
1366
|
-
lazyQueue.observe(owner, current);
|
|
1367
864
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
}
|
|
865
|
+
/** 一行 flow item 里的**每一个**正文容器;工具行、Think 卡会被排除规则挡掉。 */
|
|
866
|
+
tryRow(row) {
|
|
867
|
+
this.tryBodies(row);
|
|
868
|
+
if (row === null) return;
|
|
869
|
+
for (const body of row.querySelectorAll(MARKDOWN_BODY_SELECTOR)) {
|
|
870
|
+
this.tryBody(body);
|
|
871
|
+
}
|
|
1375
872
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
873
|
+
/** 判定并交给翻译器:这个元素自己或它内部的每一条落定正文容器。 */
|
|
874
|
+
tryBodies(element) {
|
|
875
|
+
this.tryBody(element);
|
|
876
|
+
if (element === null) return;
|
|
877
|
+
for (const body of element.querySelectorAll(MARKDOWN_BODY_SELECTOR)) {
|
|
878
|
+
this.tryBody(body);
|
|
1379
879
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
880
|
+
}
|
|
881
|
+
/** 判定并交给翻译器:这一个元素自己是不是一条该翻的正文容器。 */
|
|
882
|
+
tryBody(element) {
|
|
883
|
+
if (!(element instanceof HTMLElement)) return;
|
|
884
|
+
if (!this.isTranslatableBody(element)) return;
|
|
885
|
+
this.requestTranslate(element);
|
|
1386
886
|
}
|
|
1387
887
|
/**
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
888
|
+
* 这个元素自己是不是一条该翻的正文容器:落定的、落在过程折叠块里的 markdown
|
|
889
|
+
* 正文,且不被排除规则挡住(Think 卡、工具行、最终汇总都不算)。
|
|
1390
890
|
*/
|
|
1391
|
-
|
|
1392
|
-
if (!
|
|
1393
|
-
if (
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
});
|
|
891
|
+
isTranslatableBody(element) {
|
|
892
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
893
|
+
if (!element.matches(MARKDOWN_BODY_SELECTOR)) return false;
|
|
894
|
+
if (element.closest(EXCLUDED_SELECTOR) !== null) return false;
|
|
895
|
+
if (element.closest(REPLY_ROW_SELECTOR) === null) return false;
|
|
896
|
+
if (!isInsideProcessDisclosure(element)) return false;
|
|
897
|
+
const root = element.closest("[data-streaming]") ?? element;
|
|
898
|
+
if (isStreaming(root)) return false;
|
|
899
|
+
return true;
|
|
1401
900
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
if (
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
NonDestructiveTranslationMount.mount(span, cached, {
|
|
1411
|
-
originalText: text
|
|
1412
|
-
});
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
lazyQueue.observe(span, text);
|
|
901
|
+
/**
|
|
902
|
+
* 把一条落定的正文交给视口懒队列:视口内立刻翻、视口外的等滚到再翻,
|
|
903
|
+
* 命中缓存的由翻译器立即挂上。同一条回答只登记一次。
|
|
904
|
+
*/
|
|
905
|
+
requestTranslate(body) {
|
|
906
|
+
if (this.handled.has(body)) return;
|
|
907
|
+
this.handled.add(body);
|
|
908
|
+
lazyQueue.observe(body);
|
|
1416
909
|
}
|
|
1417
910
|
disconnect() {
|
|
1418
911
|
if (this.rootCheckTimer !== null) {
|
|
@@ -1434,17 +927,13 @@ var import_react2 = require("react");
|
|
|
1434
927
|
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
1435
928
|
|
|
1436
929
|
// src/client/settings/store.ts
|
|
1437
|
-
var
|
|
1438
|
-
var
|
|
930
|
+
var AI_TIMEOUT_MIN = 500;
|
|
931
|
+
var AI_TIMEOUT_MAX = 9e5;
|
|
1439
932
|
var SETTINGS_NAMESPACE = "dsh-chat-translate";
|
|
1440
933
|
var TRANSLATE_API_KEY_REF = "TRANSLATE_API_KEY";
|
|
1441
934
|
var DEFAULT_STATE = {
|
|
1442
935
|
enabled: true,
|
|
1443
|
-
|
|
1444
|
-
aiEnabled: true,
|
|
1445
|
-
bingEnabled: true,
|
|
1446
|
-
thinkEnabled: true,
|
|
1447
|
-
thinkTimeoutMs: 6e5,
|
|
936
|
+
aiTimeoutMs: 6e5,
|
|
1448
937
|
baseUrl: "",
|
|
1449
938
|
model: "",
|
|
1450
939
|
aiConfigured: false
|
|
@@ -1452,9 +941,9 @@ var DEFAULT_STATE = {
|
|
|
1452
941
|
var SettingsStore = class {
|
|
1453
942
|
state = { ...DEFAULT_STATE };
|
|
1454
943
|
listeners = /* @__PURE__ */ new Set();
|
|
1455
|
-
|
|
944
|
+
form = null;
|
|
1456
945
|
credentials = null;
|
|
1457
|
-
|
|
946
|
+
unsubscribeForm = null;
|
|
1458
947
|
keyConfigured = false;
|
|
1459
948
|
writeTimer = null;
|
|
1460
949
|
pendingFields = /* @__PURE__ */ new Set();
|
|
@@ -1462,15 +951,15 @@ var SettingsStore = class {
|
|
|
1462
951
|
* Bind the DSH services. Called once from the settings UI setup; re-binding
|
|
1463
952
|
* (e.g. after a reconnect) detaches the previous subscription first.
|
|
1464
953
|
*/
|
|
1465
|
-
attach(
|
|
1466
|
-
if (this.
|
|
1467
|
-
this.
|
|
1468
|
-
this.
|
|
954
|
+
attach(form, credentials) {
|
|
955
|
+
if (this.unsubscribeForm) {
|
|
956
|
+
this.unsubscribeForm();
|
|
957
|
+
this.unsubscribeForm = null;
|
|
1469
958
|
}
|
|
1470
|
-
this.
|
|
959
|
+
this.form = form;
|
|
1471
960
|
this.credentials = credentials;
|
|
1472
|
-
if (
|
|
1473
|
-
this.
|
|
961
|
+
if (form) {
|
|
962
|
+
this.unsubscribeForm = form.subscribe(() => this.derive());
|
|
1474
963
|
this.derive();
|
|
1475
964
|
}
|
|
1476
965
|
void this.refreshKeyStatus();
|
|
@@ -1484,26 +973,19 @@ var SettingsStore = class {
|
|
|
1484
973
|
this.listeners.delete(listener);
|
|
1485
974
|
};
|
|
1486
975
|
}
|
|
1487
|
-
/** Map the resolved
|
|
976
|
+
/** Map the resolved section into client state and notify. */
|
|
1488
977
|
derive() {
|
|
1489
|
-
if (!this.
|
|
1490
|
-
const snap = this.
|
|
978
|
+
if (!this.form) return;
|
|
979
|
+
const snap = this.form.getSnapshot();
|
|
1491
980
|
const value = snap.value;
|
|
1492
981
|
if (!value || typeof value !== "object") return;
|
|
1493
982
|
const next = { ...this.state };
|
|
1494
983
|
if (typeof value.enabled === "boolean") next.enabled = value.enabled;
|
|
1495
|
-
const
|
|
1496
|
-
if (typeof
|
|
1497
|
-
next.
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
if (typeof value.bingEnabled === "boolean") next.bingEnabled = value.bingEnabled;
|
|
1501
|
-
if (typeof value.thinkEnabled === "boolean") next.thinkEnabled = value.thinkEnabled;
|
|
1502
|
-
const thinkTimeout = value.thinkTimeoutMs;
|
|
1503
|
-
if (typeof thinkTimeout === "number" && Number.isFinite(thinkTimeout)) {
|
|
1504
|
-
next.thinkTimeoutMs = Math.min(
|
|
1505
|
-
Math.max(Math.round(thinkTimeout), THINK_TIMEOUT_MIN),
|
|
1506
|
-
THINK_TIMEOUT_MAX
|
|
984
|
+
const timeout = value.aiTimeoutMs;
|
|
985
|
+
if (typeof timeout === "number" && Number.isFinite(timeout)) {
|
|
986
|
+
next.aiTimeoutMs = Math.min(
|
|
987
|
+
Math.max(Math.round(timeout), AI_TIMEOUT_MIN),
|
|
988
|
+
AI_TIMEOUT_MAX
|
|
1507
989
|
);
|
|
1508
990
|
}
|
|
1509
991
|
if (typeof value.baseUrl === "string") next.baseUrl = value.baseUrl;
|
|
@@ -1520,11 +1002,6 @@ var SettingsStore = class {
|
|
|
1520
1002
|
} catch {
|
|
1521
1003
|
}
|
|
1522
1004
|
}
|
|
1523
|
-
try {
|
|
1524
|
-
chatTranslateObserver.setThinkEnabled(this.state.enabled && this.state.thinkEnabled);
|
|
1525
|
-
chatTranslateObserver.setThinkConfigured(this.state.aiEnabled && this.state.aiConfigured);
|
|
1526
|
-
} catch {
|
|
1527
|
-
}
|
|
1528
1005
|
this.notify();
|
|
1529
1006
|
}
|
|
1530
1007
|
notify() {
|
|
@@ -1552,43 +1029,29 @@ var SettingsStore = class {
|
|
|
1552
1029
|
}
|
|
1553
1030
|
/**
|
|
1554
1031
|
* Optimistically apply locally, then persist each touched field through the
|
|
1555
|
-
*
|
|
1032
|
+
* configuration form. Writes are trailing-debounced (300ms) so typing in the
|
|
1556
1033
|
* baseUrl/model inputs collapses into a single queued mutation instead of
|
|
1557
1034
|
* one write per keystroke — which would otherwise flash stale mirror values
|
|
1558
|
-
* back into the inputs between commits. A failed write makes the
|
|
1559
|
-
*
|
|
1035
|
+
* back into the inputs between commits. A failed write makes the form reload
|
|
1036
|
+
* its mirror, which re-derives this store from the Host document
|
|
1560
1037
|
* (conflict-safe recovery).
|
|
1561
1038
|
*/
|
|
1562
1039
|
async update(partial) {
|
|
1563
|
-
let
|
|
1564
|
-
if (typeof partial.
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
if (typeof partial.thinkTimeoutMs === "number" && Number.isFinite(partial.thinkTimeoutMs)) {
|
|
1569
|
-
sanitizedThinkTimeout = Math.min(
|
|
1570
|
-
Math.max(Math.round(partial.thinkTimeoutMs), THINK_TIMEOUT_MIN),
|
|
1571
|
-
THINK_TIMEOUT_MAX
|
|
1040
|
+
let sanitizedTimeout = this.state.aiTimeoutMs;
|
|
1041
|
+
if (typeof partial.aiTimeoutMs === "number" && Number.isFinite(partial.aiTimeoutMs)) {
|
|
1042
|
+
sanitizedTimeout = Math.min(
|
|
1043
|
+
Math.max(Math.round(partial.aiTimeoutMs), AI_TIMEOUT_MIN),
|
|
1044
|
+
AI_TIMEOUT_MAX
|
|
1572
1045
|
);
|
|
1573
1046
|
}
|
|
1574
1047
|
const next = {
|
|
1575
1048
|
...this.state,
|
|
1576
1049
|
...partial,
|
|
1577
|
-
|
|
1578
|
-
thinkTimeoutMs: sanitizedThinkTimeout
|
|
1050
|
+
aiTimeoutMs: sanitizedTimeout
|
|
1579
1051
|
};
|
|
1580
1052
|
this.applyState(next);
|
|
1581
|
-
if (this.
|
|
1582
|
-
const fields = [
|
|
1583
|
-
"enabled",
|
|
1584
|
-
"concurrency",
|
|
1585
|
-
"aiEnabled",
|
|
1586
|
-
"bingEnabled",
|
|
1587
|
-
"thinkEnabled",
|
|
1588
|
-
"thinkTimeoutMs",
|
|
1589
|
-
"baseUrl",
|
|
1590
|
-
"model"
|
|
1591
|
-
];
|
|
1053
|
+
if (this.form) {
|
|
1054
|
+
const fields = ["enabled", "aiTimeoutMs", "baseUrl", "model"];
|
|
1592
1055
|
for (const field of fields) {
|
|
1593
1056
|
if (partial[field] !== void 0) {
|
|
1594
1057
|
this.pendingFields.add(field);
|
|
@@ -1605,7 +1068,7 @@ var SettingsStore = class {
|
|
|
1605
1068
|
}, 300);
|
|
1606
1069
|
}
|
|
1607
1070
|
async flushWrite() {
|
|
1608
|
-
if (!this.
|
|
1071
|
+
if (!this.form) return;
|
|
1609
1072
|
const fields = [...this.pendingFields];
|
|
1610
1073
|
this.pendingFields.clear();
|
|
1611
1074
|
if (fields.length === 0) return;
|
|
@@ -1614,7 +1077,7 @@ var SettingsStore = class {
|
|
|
1614
1077
|
path: [field],
|
|
1615
1078
|
value: this.state[field]
|
|
1616
1079
|
}));
|
|
1617
|
-
await this.
|
|
1080
|
+
await this.form.mutate(ops).catch(() => {
|
|
1618
1081
|
});
|
|
1619
1082
|
}
|
|
1620
1083
|
async testChannel(channel) {
|
|
@@ -1643,11 +1106,11 @@ var SettingsStore = class {
|
|
|
1643
1106
|
this.writeTimer = null;
|
|
1644
1107
|
}
|
|
1645
1108
|
this.pendingFields.clear();
|
|
1646
|
-
if (this.
|
|
1647
|
-
this.
|
|
1648
|
-
this.
|
|
1109
|
+
if (this.unsubscribeForm) {
|
|
1110
|
+
this.unsubscribeForm();
|
|
1111
|
+
this.unsubscribeForm = null;
|
|
1649
1112
|
}
|
|
1650
|
-
this.
|
|
1113
|
+
this.form = null;
|
|
1651
1114
|
this.credentials = null;
|
|
1652
1115
|
this.listeners.clear();
|
|
1653
1116
|
}
|
|
@@ -1736,10 +1199,6 @@ var SETTINGS_CSS = String.raw`
|
|
|
1736
1199
|
width: 120px;
|
|
1737
1200
|
}
|
|
1738
1201
|
|
|
1739
|
-
.dsh-tidy-field-count {
|
|
1740
|
-
width: 88px;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
1202
|
.dsh-tidy-test-result {
|
|
1744
1203
|
margin-left: 10px;
|
|
1745
1204
|
font: var(--dsw-font-xxs-12);
|
|
@@ -1753,13 +1212,6 @@ var SETTINGS_CSS = String.raw`
|
|
|
1753
1212
|
color: var(--dsw-alias-state-error-primary);
|
|
1754
1213
|
}
|
|
1755
1214
|
|
|
1756
|
-
.dsh-tidy-behavior-list {
|
|
1757
|
-
display: flex;
|
|
1758
|
-
flex-direction: column;
|
|
1759
|
-
gap: 4px;
|
|
1760
|
-
margin: 4px 0 0;
|
|
1761
|
-
padding-left: 18px;
|
|
1762
|
-
}
|
|
1763
1215
|
`;
|
|
1764
1216
|
|
|
1765
1217
|
// src/client/reading-settings-page.tsx
|
|
@@ -1899,91 +1351,67 @@ function claimReadingSettingsPage(ctx, label, locale) {
|
|
|
1899
1351
|
var NS = "settings.chatTranslate";
|
|
1900
1352
|
var zh = {
|
|
1901
1353
|
pageNav: "\u9605\u8BFB\u4F53\u9A8C",
|
|
1902
|
-
title: "\
|
|
1354
|
+
title: "\u6B63\u6587\u7FFB\u8BD1",
|
|
1903
1355
|
masterTitle: "\u7FFB\u8BD1\u603B\u5F00\u5173",
|
|
1904
1356
|
enableTranslation: "\u542F\u7528\u7FFB\u8BD1",
|
|
1905
|
-
masterDesc: "\u81EA\u52A8\
|
|
1357
|
+
masterDesc: "\u81EA\u52A8\u628A\u56DE\u7B54\u6B63\u6587\u91CC\u9700\u8981\u9605\u8BFB\u7684\u82F1\u6587\u7FFB\u6210\u4E2D\u6587\uFF08\u5982 {example}\uFF09\uFF0C\u601D\u8003\u5361\u4E0E\u5DE5\u5177\u8C03\u7528\u884C\u4E00\u5F8B\u4E0D\u7FFB\uFF0C\u4EE3\u7801\u5757\u539F\u6837\u4FDD\u7559\u3002\u8BD1\u6587\u6302\u5728\u539F\u6587\u4F4D\u7F6E\u4E0A\uFF0C\u70B9\u51FB\u8BD1\u6587\u53EF\u539F\u5730\u5207\u56DE\u539F\u6587\u3002\u56DE\u7B54\u8FD8\u5728\u6D41\u5F0F\u8F93\u51FA\u65F6\u4E0D\u7FFB\uFF0C\u843D\u5B9A\u540E\u6574\u6761\u8F6C\u4E3A\u4E2D\u6587\uFF1B\u5DF2\u7ECF\u5199\u597D\u7684\u4E2D\u6587\u6BB5\u843D\u539F\u6837\u4FDD\u7559\u3002\u53EA\u4F5C\u7528\u4E8E\u5F53\u524D\u67E5\u770B\u7684\u4F1A\u8BDD\uFF0C\u8BD1\u6587\u4E0D\u5199\u56DE\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u3002",
|
|
1906
1358
|
aiTitle: "AI \u7FFB\u8BD1\uFF08OpenAI \u517C\u5BB9\u534F\u8BAE\uFF09",
|
|
1907
|
-
|
|
1359
|
+
aiRule: "\u53EA\u6709\u8FD9\u4E00\u6761\u901A\u9053\uFF1AKey / Base URL / \u6A21\u578B\u9F50\u5907\u624D\u4F1A\u7FFB\u8BD1\uFF0C\u7F3A\u4EFB\u4F55\u4E00\u9879\u90FD\u4FDD\u6301\u539F\u6587\u3002",
|
|
1908
1360
|
badgeConfigured: "\u5DF2\u914D\u7F6E",
|
|
1909
1361
|
badgeUnconfigured: "\u672A\u914D\u7F6E",
|
|
1910
|
-
aiFallbackNote: "\u672A\u914D\u7F6E\u65F6 AI \u901A\u9053\u81EA\u52A8\u8DF3\u8FC7\uFF0C\u7531 Bing \u515C\u5E95\u3002",
|
|
1911
1362
|
apiKeyTitle: "API Key",
|
|
1912
1363
|
apiKeyDesc: "\u4FDD\u5B58\u81F3 {home} \u7684 {ref}\uFF0C\u4FDD\u5B58\u540E\u7ACB\u5373\u751F\u6548\uFF1B\u7559\u7A7A\u4FDD\u5B58 = \u6E05\u9664",
|
|
1913
1364
|
apiKeyPlaceholderConfigured: "\u5DF2\u914D\u7F6E\uFF08\u5982\u9700\u66F4\u6362\u8BF7\u76F4\u63A5\u8F93\u5165\uFF09",
|
|
1914
1365
|
save: "\u4FDD\u5B58",
|
|
1915
1366
|
saving: "\u4FDD\u5B58\u4E2D\u2026",
|
|
1916
1367
|
keySaved: "\u5DF2\u4FDD\u5B58\u5230 {home}\uFF0C\u7ACB\u5373\u751F\u6548",
|
|
1917
|
-
keyCleared: "\u5DF2\u6E05\u9664 API Key\
|
|
1368
|
+
keyCleared: "\u5DF2\u6E05\u9664 API Key\uFF08\u672A\u914D\u7F6E\u65F6\u6B63\u6587\u4FDD\u6301\u539F\u6587\uFF0C\u4E0D\u4F1A\u7FFB\u8BD1\uFF09",
|
|
1918
1369
|
saveFailed: "\u4FDD\u5B58\u5931\u8D25\uFF1A{error}",
|
|
1919
1370
|
unknownError: "\u672A\u77E5\u9519\u8BEF",
|
|
1920
1371
|
baseUrlTitle: "Base URL",
|
|
1921
1372
|
baseUrlDesc: "\u4EFB\u610F OpenAI \u517C\u5BB9\u670D\u52A1\u7AEF\u70B9\uFF0C\u5982 {url1}\u3001{url2}",
|
|
1922
1373
|
modelTitle: "\u6A21\u578B",
|
|
1923
1374
|
modelDesc: "\u5982 {model1}\u3001{model2}\uFF1B\u7559\u7A7A\u89C6\u4E3A\u672A\u914D\u7F6E",
|
|
1375
|
+
timeoutTitle: "\u5355\u6B21\u8BF7\u6C42\u8D85\u65F6",
|
|
1376
|
+
timeoutDesc: "\u5355\u4F4D\u6BEB\u79D2\uFF0C\u8303\u56F4 500-900000\u3002\u4E00\u6761\u957F\u56DE\u7B54\u53EF\u80FD\u8981\u51E0\u5206\u949F\uFF0C\u9ED8\u8BA4 600000\u3002",
|
|
1924
1377
|
test: "\u6D4B\u8BD5 AI \u901A\u9053",
|
|
1925
1378
|
testing: "\u6D4B\u8BD5\u4E2D\u2026",
|
|
1926
1379
|
testOk: "\u8FDE\u63A5\u6B63\u5E38\uFF0C\u5EF6\u8FDF {latency}ms",
|
|
1927
1380
|
testFail: "\u5931\u8D25\uFF1A{error}",
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
thinkTimeoutTitle: "\u601D\u8003\u94FE\u7FFB\u8BD1\u8D85\u65F6",
|
|
1931
|
-
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",
|
|
1932
|
-
bingTitle: "Bing \u7F51\u9875\u7FFB\u8BD1\uFF08\u514D Key \u515C\u5E95\uFF09",
|
|
1933
|
-
enableBing: "\u542F\u7528 Bing \u7FFB\u8BD1\u901A\u9053",
|
|
1934
|
-
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",
|
|
1935
|
-
behaviorTitle: "\u901A\u9053\u884C\u4E3A",
|
|
1936
|
-
behavior1: "AI \u5F00\u542F\u4E14\u5DF2\u914D\u7F6E \u2192 AI \u4F18\u5148\u7FFB\u8BD1\uFF0C\u5931\u8D25\u81EA\u52A8\u964D\u7EA7 Bing",
|
|
1937
|
-
behavior2: "AI \u5F00\u542F\u4F46\u672A\u914D\u7F6E + Bing \u5F00\u542F \u2192 \u7531 Bing \u7FFB\u8BD1",
|
|
1938
|
-
behavior3: "AI \u5F00\u542F\u4F46\u672A\u914D\u7F6E + Bing \u5173\u95ED \u2192 \u4E0D\u7FFB\u8BD1",
|
|
1939
|
-
behavior4: "AI \u5173\u95ED + Bing \u5F00\u542F \u2192 \u76F4\u63A5\u4F7F\u7528 Bing",
|
|
1940
|
-
behavior5: "AI \u5173\u95ED + Bing \u5173\u95ED \u2192 \u4E0D\u7FFB\u8BD1",
|
|
1941
|
-
concurrencyTitle: "\u6700\u5927\u7FFB\u8BD1\u5E76\u53D1\u6570",
|
|
1942
|
-
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"
|
|
1381
|
+
behaviorTitle: "\u7FFB\u8BD1\u8303\u56F4",
|
|
1382
|
+
behaviorRule: "\u9700\u8981\u9605\u8BFB\u7684\u56DE\u7B54\u6B63\u6587\uFF1B\u601D\u8003\u94FE\u6B63\u6587\u3001\u5DE5\u5177\u8C03\u7528\u6807\u9898\u4E0E\u6298\u53E0\u6458\u8981\u6C38\u4E0D\u7FFB\u8BD1\uFF0C\u4EE3\u7801\u5757\u53CA\u5176\u5185\u90E8\u539F\u6837\u4FDD\u7559\u3002"
|
|
1943
1383
|
};
|
|
1944
1384
|
var en = {
|
|
1945
1385
|
pageNav: "Reading",
|
|
1946
|
-
title: "
|
|
1386
|
+
title: "Reply translate",
|
|
1947
1387
|
masterTitle: "Translation master switch",
|
|
1948
1388
|
enableTranslation: "Enable translation",
|
|
1949
|
-
masterDesc: "Translates
|
|
1389
|
+
masterDesc: "Translates the English prose in assistant replies (e.g. {example}) into Chinese. Think cards and tool-call rows are never translated, and code blocks stay untouched. The translation is mounted in place of the original and clicking it switches back to the original in place. A reply that is still streaming is left alone until it settles, and paragraphs already written in Chinese pass through unchanged. Applies to the session you are viewing only; translations never enter the conversation context.",
|
|
1950
1390
|
aiTitle: "AI translation (OpenAI-compatible)",
|
|
1951
|
-
|
|
1391
|
+
aiRule: "This is the only channel: translation runs when Key, Base URL and model are all present; missing any one leaves the text as it is.",
|
|
1952
1392
|
badgeConfigured: "Configured",
|
|
1953
1393
|
badgeUnconfigured: "Not configured",
|
|
1954
|
-
aiFallbackNote: "When unconfigured the AI channel is skipped and Bing takes over.",
|
|
1955
1394
|
apiKeyTitle: "API Key",
|
|
1956
1395
|
apiKeyDesc: "Saved as {ref} in {home}; takes effect immediately. Saving an empty value clears it.",
|
|
1957
1396
|
apiKeyPlaceholderConfigured: "Configured (type a new value to replace it)",
|
|
1958
1397
|
save: "Save",
|
|
1959
1398
|
saving: "Saving\u2026",
|
|
1960
1399
|
keySaved: "Saved to {home}; takes effect immediately",
|
|
1961
|
-
keyCleared: "API Key cleared (
|
|
1400
|
+
keyCleared: "API Key cleared (with nothing configured, replies stay in their original language)",
|
|
1962
1401
|
saveFailed: "Save failed: {error}",
|
|
1963
1402
|
unknownError: "Unknown error",
|
|
1964
1403
|
baseUrlTitle: "Base URL",
|
|
1965
1404
|
baseUrlDesc: "Any OpenAI-compatible endpoint, e.g. {url1} or {url2}",
|
|
1966
1405
|
modelTitle: "Model",
|
|
1967
1406
|
modelDesc: "e.g. {model1} or {model2}; empty counts as unconfigured",
|
|
1407
|
+
timeoutTitle: "Per-request timeout",
|
|
1408
|
+
timeoutDesc: "In milliseconds, range 500-900000. One long reply can take minutes; default 600000.",
|
|
1968
1409
|
test: "Test the AI channel",
|
|
1969
1410
|
testing: "Testing\u2026",
|
|
1970
1411
|
testOk: "Connected, {latency}ms latency",
|
|
1971
1412
|
testFail: "Failed: {error}",
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
thinkTimeoutTitle: "Think-chain translation timeout",
|
|
1975
|
-
thinkTimeoutDesc: "In milliseconds, range 500-900000. A whole think block can take minutes; default 600000.",
|
|
1976
|
-
bingTitle: "Bing web translation (key-free fallback)",
|
|
1977
|
-
enableBing: "Enable the Bing translation channel",
|
|
1978
|
-
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.",
|
|
1979
|
-
behaviorTitle: "Channel behavior",
|
|
1980
|
-
behavior1: "AI on and configured \u2192 AI translates first, falling back to Bing on failure",
|
|
1981
|
-
behavior2: "AI on but unconfigured + Bing on \u2192 Bing translates",
|
|
1982
|
-
behavior3: "AI on but unconfigured + Bing off \u2192 nothing is translated",
|
|
1983
|
-
behavior4: "AI off + Bing on \u2192 Bing is used directly",
|
|
1984
|
-
behavior5: "AI off + Bing off \u2192 nothing is translated",
|
|
1985
|
-
concurrencyTitle: "Max translation concurrency",
|
|
1986
|
-
concurrencyDesc: "Caps parallel requests while the viewport scrolls across multiple tool cards (range 1-100, recommended 3)."
|
|
1413
|
+
behaviorTitle: "What gets translated",
|
|
1414
|
+
behaviorRule: "The assistant reply prose you have to read. Think-chain text, tool-call titles and folded summaries are never translated; code blocks and their contents stay untouched."
|
|
1987
1415
|
};
|
|
1988
1416
|
|
|
1989
1417
|
// src/client/settings/ui.tsx
|
|
@@ -2056,218 +1484,145 @@ function TidySettingsPanel(props) {
|
|
|
2056
1484
|
}
|
|
2057
1485
|
)
|
|
2058
1486
|
] }),
|
|
2059
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: withSamples(t("masterDesc"), { example: "
|
|
1487
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: withSamples(t("masterDesc"), { example: "I traced the failing path to a stale lock file." }) })
|
|
2060
1488
|
] }),
|
|
2061
1489
|
state.enabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
2062
1490
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
|
|
2063
1491
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-title", children: [
|
|
2064
1492
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("aiTitle") }),
|
|
2065
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2066
|
-
import_dsh_client_ui_primitives.Switch,
|
|
2067
|
-
{
|
|
2068
|
-
checked: state.aiEnabled,
|
|
2069
|
-
onChange: (next) => settingsStore.update({ aiEnabled: next }),
|
|
2070
|
-
label: t("enableAi")
|
|
2071
|
-
}
|
|
2072
|
-
)
|
|
2073
|
-
] }),
|
|
2074
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-desc", children: [
|
|
2075
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { tone: state.aiConfigured ? "success" : "warning", children: t(state.aiConfigured ? "badgeConfigured" : "badgeUnconfigured") }),
|
|
2076
|
-
" ",
|
|
2077
|
-
t("aiFallbackNote")
|
|
1493
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { tone: state.aiConfigured ? "success" : "warning", children: t(state.aiConfigured ? "badgeConfigured" : "badgeUnconfigured") })
|
|
2078
1494
|
] }),
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
] }),
|
|
2088
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-input-row", children: [
|
|
2089
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2090
|
-
import_dsh_client_ui_primitives.Input,
|
|
2091
|
-
{
|
|
2092
|
-
type: "password",
|
|
2093
|
-
className: "dsh-tidy-field-wide",
|
|
2094
|
-
placeholder: state.aiConfigured ? t("apiKeyPlaceholderConfigured") : "sk-...",
|
|
2095
|
-
value: apiKeyInput,
|
|
2096
|
-
onChange: (e) => setApiKeyInput(e.target.value),
|
|
2097
|
-
"aria-label": t("apiKeyTitle")
|
|
2098
|
-
}
|
|
2099
|
-
),
|
|
2100
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2101
|
-
import_dsh_client_ui_primitives.Button,
|
|
2102
|
-
{
|
|
2103
|
-
variant: "primary",
|
|
2104
|
-
size: "sm",
|
|
2105
|
-
disabled: savingKey,
|
|
2106
|
-
onClick: handleSaveKey,
|
|
2107
|
-
children: savingKey ? t("saving") : t("save")
|
|
2108
|
-
}
|
|
2109
|
-
)
|
|
2110
|
-
] })
|
|
2111
|
-
] }),
|
|
2112
|
-
keyMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `dsh-tidy-test-result ${keyMsg.ok ? "ok" : "fail"}`, children: keyMsg.text }),
|
|
2113
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
2114
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
2115
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("baseUrlTitle") }),
|
|
2116
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("baseUrlDesc"), {
|
|
2117
|
-
url1: "https://api.openai.com/v1",
|
|
2118
|
-
url2: "https://api.deepseek.com/v1"
|
|
2119
|
-
}) })
|
|
2120
|
-
] }),
|
|
2121
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2122
|
-
import_dsh_client_ui_primitives.Input,
|
|
2123
|
-
{
|
|
2124
|
-
type: "text",
|
|
2125
|
-
className: "dsh-tidy-field-wide",
|
|
2126
|
-
placeholder: "https://api.openai.com/v1",
|
|
2127
|
-
value: state.baseUrl,
|
|
2128
|
-
onChange: (e) => settingsStore.update({ baseUrl: e.target.value }),
|
|
2129
|
-
"aria-label": t("baseUrlTitle")
|
|
2130
|
-
}
|
|
2131
|
-
)
|
|
1495
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: t("aiRule") }),
|
|
1496
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
1497
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
1498
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("apiKeyTitle") }),
|
|
1499
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("apiKeyDesc"), {
|
|
1500
|
+
home: CREDENTIALS_PATH,
|
|
1501
|
+
ref: TRANSLATE_API_KEY_REF
|
|
1502
|
+
}) })
|
|
2132
1503
|
] }),
|
|
2133
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
2134
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
2135
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("modelTitle") }),
|
|
2136
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("modelDesc"), {
|
|
2137
|
-
model1: "gpt-4o-mini",
|
|
2138
|
-
model2: "deepseek-chat"
|
|
2139
|
-
}) })
|
|
2140
|
-
] }),
|
|
1504
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-input-row", children: [
|
|
2141
1505
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2142
1506
|
import_dsh_client_ui_primitives.Input,
|
|
2143
1507
|
{
|
|
2144
|
-
type: "
|
|
1508
|
+
type: "password",
|
|
2145
1509
|
className: "dsh-tidy-field-wide",
|
|
2146
|
-
placeholder: "
|
|
2147
|
-
value:
|
|
2148
|
-
onChange: (e) =>
|
|
2149
|
-
"aria-label": t("
|
|
1510
|
+
placeholder: state.aiConfigured ? t("apiKeyPlaceholderConfigured") : "sk-...",
|
|
1511
|
+
value: apiKeyInput,
|
|
1512
|
+
onChange: (e) => setApiKeyInput(e.target.value),
|
|
1513
|
+
"aria-label": t("apiKeyTitle")
|
|
2150
1514
|
}
|
|
2151
|
-
)
|
|
2152
|
-
] }),
|
|
2153
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
1515
|
+
),
|
|
2154
1516
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2155
1517
|
import_dsh_client_ui_primitives.Button,
|
|
2156
1518
|
{
|
|
2157
|
-
variant: "
|
|
1519
|
+
variant: "primary",
|
|
2158
1520
|
size: "sm",
|
|
2159
|
-
disabled:
|
|
2160
|
-
onClick:
|
|
2161
|
-
children:
|
|
2162
|
-
}
|
|
2163
|
-
),
|
|
2164
|
-
testing?.channel === "openai" && !testing.running && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-tidy-test-result ${testing.ok ? "ok" : "fail"}`, children: testing.message })
|
|
2165
|
-
] }),
|
|
2166
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
2167
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
2168
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("thinkEnableTitle") }),
|
|
2169
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: t("thinkEnableDesc") })
|
|
2170
|
-
] }),
|
|
2171
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2172
|
-
import_dsh_client_ui_primitives.Switch,
|
|
2173
|
-
{
|
|
2174
|
-
checked: state.thinkEnabled,
|
|
2175
|
-
onChange: (next) => settingsStore.update({ thinkEnabled: next }),
|
|
2176
|
-
label: t("thinkEnableTitle")
|
|
2177
|
-
}
|
|
2178
|
-
)
|
|
2179
|
-
] }),
|
|
2180
|
-
state.thinkEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
2181
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
2182
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("thinkTimeoutTitle") }),
|
|
2183
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: t("thinkTimeoutDesc") })
|
|
2184
|
-
] }),
|
|
2185
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2186
|
-
import_dsh_client_ui_primitives.Input,
|
|
2187
|
-
{
|
|
2188
|
-
type: "number",
|
|
2189
|
-
className: "dsh-tidy-field-timeout",
|
|
2190
|
-
min: 500,
|
|
2191
|
-
max: 9e5,
|
|
2192
|
-
step: 1e3,
|
|
2193
|
-
value: state.thinkTimeoutMs,
|
|
2194
|
-
onChange: (e) => {
|
|
2195
|
-
const val = parseInt(e.target.value, 10);
|
|
2196
|
-
if (!Number.isFinite(val)) return;
|
|
2197
|
-
settingsStore.update({
|
|
2198
|
-
thinkTimeoutMs: Math.min(Math.max(val, 500), 9e5)
|
|
2199
|
-
});
|
|
2200
|
-
},
|
|
2201
|
-
"aria-label": t("thinkTimeoutTitle")
|
|
1521
|
+
disabled: savingKey,
|
|
1522
|
+
onClick: handleSaveKey,
|
|
1523
|
+
children: savingKey ? t("saving") : t("save")
|
|
2202
1524
|
}
|
|
2203
1525
|
)
|
|
2204
1526
|
] })
|
|
2205
|
-
] })
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
1527
|
+
] }),
|
|
1528
|
+
keyMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `dsh-tidy-test-result ${keyMsg.ok ? "ok" : "fail"}`, children: keyMsg.text }),
|
|
1529
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
1530
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
1531
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("baseUrlTitle") }),
|
|
1532
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("baseUrlDesc"), {
|
|
1533
|
+
url1: "https://api.openai.com/v1",
|
|
1534
|
+
url2: "https://api.deepseek.com/v1"
|
|
1535
|
+
}) })
|
|
1536
|
+
] }),
|
|
1537
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1538
|
+
import_dsh_client_ui_primitives.Input,
|
|
1539
|
+
{
|
|
1540
|
+
type: "text",
|
|
1541
|
+
className: "dsh-tidy-field-wide",
|
|
1542
|
+
placeholder: "https://api.openai.com/v1",
|
|
1543
|
+
value: state.baseUrl,
|
|
1544
|
+
onChange: (e) => settingsStore.update({ baseUrl: e.target.value }),
|
|
1545
|
+
"aria-label": t("baseUrlTitle")
|
|
1546
|
+
}
|
|
1547
|
+
)
|
|
1548
|
+
] }),
|
|
1549
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
1550
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
1551
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("modelTitle") }),
|
|
1552
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("modelDesc"), {
|
|
1553
|
+
model1: "gpt-4o-mini",
|
|
1554
|
+
model2: "deepseek-chat"
|
|
1555
|
+
}) })
|
|
1556
|
+
] }),
|
|
1557
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1558
|
+
import_dsh_client_ui_primitives.Input,
|
|
1559
|
+
{
|
|
1560
|
+
type: "text",
|
|
1561
|
+
className: "dsh-tidy-field-wide",
|
|
1562
|
+
placeholder: "gpt-4o-mini",
|
|
1563
|
+
value: state.model,
|
|
1564
|
+
onChange: (e) => settingsStore.update({ model: e.target.value }),
|
|
1565
|
+
"aria-label": t("modelTitle")
|
|
1566
|
+
}
|
|
1567
|
+
)
|
|
1568
|
+
] }),
|
|
1569
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
1570
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
1571
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("timeoutTitle") }),
|
|
1572
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: t("timeoutDesc") })
|
|
1573
|
+
] }),
|
|
2210
1574
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2211
|
-
import_dsh_client_ui_primitives.
|
|
1575
|
+
import_dsh_client_ui_primitives.Input,
|
|
2212
1576
|
{
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
1577
|
+
type: "number",
|
|
1578
|
+
className: "dsh-tidy-field-timeout",
|
|
1579
|
+
min: 500,
|
|
1580
|
+
max: 9e5,
|
|
1581
|
+
step: 1e3,
|
|
1582
|
+
value: state.aiTimeoutMs,
|
|
1583
|
+
onChange: (e) => {
|
|
1584
|
+
const val = parseInt(e.target.value, 10);
|
|
1585
|
+
if (!Number.isFinite(val)) return;
|
|
1586
|
+
settingsStore.update({
|
|
1587
|
+
aiTimeoutMs: Math.min(Math.max(val, 500), 9e5)
|
|
1588
|
+
});
|
|
1589
|
+
},
|
|
1590
|
+
"aria-label": t("timeoutTitle")
|
|
2216
1591
|
}
|
|
2217
1592
|
)
|
|
2218
1593
|
] }),
|
|
2219
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
1594
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
1595
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1596
|
+
import_dsh_client_ui_primitives.Button,
|
|
1597
|
+
{
|
|
1598
|
+
variant: "outline",
|
|
1599
|
+
size: "sm",
|
|
1600
|
+
disabled: testing?.running,
|
|
1601
|
+
onClick: () => runTest("openai"),
|
|
1602
|
+
children: testing?.running ? t("testing") : t("test")
|
|
1603
|
+
}
|
|
1604
|
+
),
|
|
1605
|
+
testing?.channel === "openai" && !testing.running && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-tidy-test-result ${testing.ok ? "ok" : "fail"}`, children: testing.message })
|
|
1606
|
+
] })
|
|
2220
1607
|
] }),
|
|
2221
1608
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
|
|
2222
1609
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("behaviorTitle") }),
|
|
2223
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
2224
|
-
|
|
2225
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: t("behavior2") }),
|
|
2226
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: t("behavior3") }),
|
|
2227
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: t("behavior4") }),
|
|
2228
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: t("behavior5") })
|
|
2229
|
-
] })
|
|
2230
|
-
] }),
|
|
2231
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-card", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
2232
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
2233
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("concurrencyTitle") }),
|
|
2234
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: t("concurrencyDesc") })
|
|
2235
|
-
] }),
|
|
2236
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2237
|
-
import_dsh_client_ui_primitives.Input,
|
|
2238
|
-
{
|
|
2239
|
-
type: "number",
|
|
2240
|
-
className: "dsh-tidy-field-count",
|
|
2241
|
-
min: 1,
|
|
2242
|
-
max: 100,
|
|
2243
|
-
step: 1,
|
|
2244
|
-
value: state.concurrency,
|
|
2245
|
-
onChange: (e) => {
|
|
2246
|
-
const val = parseInt(e.target.value, 10);
|
|
2247
|
-
if (!Number.isFinite(val)) return;
|
|
2248
|
-
settingsStore.update({ concurrency: Math.min(Math.max(val, 1), 100) });
|
|
2249
|
-
},
|
|
2250
|
-
"aria-label": t("concurrencyTitle")
|
|
2251
|
-
}
|
|
2252
|
-
)
|
|
2253
|
-
] }) })
|
|
1610
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: t("behaviorRule") })
|
|
1611
|
+
] })
|
|
2254
1612
|
] })
|
|
2255
1613
|
] });
|
|
2256
1614
|
}
|
|
2257
1615
|
function setupSettingsUi(ctx) {
|
|
2258
1616
|
if (typeof window === "undefined") return;
|
|
1617
|
+
const configForms = ctx.configForms;
|
|
1618
|
+
const remoteCredentials = ctx.remote?.credentials ?? null;
|
|
2259
1619
|
try {
|
|
2260
|
-
|
|
2261
|
-
const remoteCredentials = ctx?.remote?.credentials || (ctx?.get ? ctx.get("remote.credentials") : null);
|
|
2262
|
-
if (settingsScope && typeof settingsScope.bind === "function") {
|
|
2263
|
-
const scope = settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
|
|
2264
|
-
settingsStore.attach(scope, remoteCredentials ?? null);
|
|
2265
|
-
}
|
|
1620
|
+
settingsStore.attach(configForms.get(SETTINGS_NAMESPACE), remoteCredentials);
|
|
2266
1621
|
} catch (err) {
|
|
2267
|
-
console.warn("[dsh-chat-translate] Failed to bind
|
|
1622
|
+
console.warn("[dsh-chat-translate] Failed to bind the configuration form:", err);
|
|
2268
1623
|
}
|
|
2269
|
-
const locale =
|
|
2270
|
-
if (locale && typeof locale.register === "function"
|
|
1624
|
+
const locale = ctx.locale;
|
|
1625
|
+
if (locale && typeof locale.register === "function") {
|
|
2271
1626
|
ctx.effect(
|
|
2272
1627
|
() => locale.register(NS, { zh, en }),
|
|
2273
1628
|
"dsh-chat-translate: locale dictionaries"
|
|
@@ -2275,27 +1630,36 @@ function setupSettingsUi(ctx) {
|
|
|
2275
1630
|
}
|
|
2276
1631
|
const t = locale && typeof locale.bind === "function" ? locale.bind(NS) : (key) => zh[key] ?? key;
|
|
2277
1632
|
try {
|
|
2278
|
-
const slots = ctx
|
|
1633
|
+
const slots = ctx.slots;
|
|
2279
1634
|
if (!slots || typeof slots.inject !== "function") return;
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
1635
|
+
ctx.effect(
|
|
1636
|
+
() => configForms.whileServed([SETTINGS_NAMESPACE], () => {
|
|
1637
|
+
const offPage = slots.inject(
|
|
1638
|
+
"settings.section",
|
|
1639
|
+
() => claimReadingSettingsPage(ctx, () => t("pageNav"), NS)
|
|
1640
|
+
);
|
|
1641
|
+
const offCard = slots.inject(
|
|
1642
|
+
READING_ITEM_SLOT,
|
|
1643
|
+
() => slots.register(
|
|
1644
|
+
{
|
|
1645
|
+
name: READING_ITEM_SLOT,
|
|
1646
|
+
// id = 本插件的配置条目 id(= profile entry id = 设置命名空间)
|
|
1647
|
+
id: SETTINGS_NAMESPACE,
|
|
1648
|
+
// 卡片在共享页里的顺序:丝滑流式 10、吸顶提示 20、聊天翻译 30
|
|
1649
|
+
order: 30,
|
|
1650
|
+
// 共享页按此标签渲染 tab
|
|
1651
|
+
label: () => t("title"),
|
|
1652
|
+
locale: NS
|
|
1653
|
+
},
|
|
1654
|
+
TidySettingsPanel
|
|
1655
|
+
)
|
|
1656
|
+
);
|
|
1657
|
+
return () => {
|
|
1658
|
+
offCard();
|
|
1659
|
+
offPage();
|
|
1660
|
+
};
|
|
1661
|
+
}),
|
|
1662
|
+
"dsh-chat-translate: shared reading page"
|
|
2299
1663
|
);
|
|
2300
1664
|
} catch (err) {
|
|
2301
1665
|
console.warn("[dsh-chat-translate] Failed to inject settings section:", err);
|
|
@@ -2304,9 +1668,9 @@ function setupSettingsUi(ctx) {
|
|
|
2304
1668
|
|
|
2305
1669
|
// src/client/index.ts
|
|
2306
1670
|
var name = "dsh-chat-translate";
|
|
2307
|
-
var inject = ["slots", "
|
|
1671
|
+
var inject = ["slots", "configForms", "remote", "remote.credentials", "locale"];
|
|
2308
1672
|
function apply(ctx) {
|
|
2309
|
-
ctx.effect(() => chatTranslateObserver.start(document), "dsh-chat-translate:
|
|
1673
|
+
ctx.effect(() => chatTranslateObserver.start(document), "dsh-chat-translate: reply translate observer");
|
|
2310
1674
|
ctx.effect(() => setupSettingsUi(ctx), "dsh-chat-translate: settings section");
|
|
2311
1675
|
}
|
|
2312
1676
|
return module.exports; } });
|