@lynn123411/dsh-chat-translate 1.9.0 → 3.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 +14 -14
- package/docs//351/205/215/347/275/256.md +13 -17
- package/lib/client.js +949 -1615
- package/lib/client.js.map +4 -4
- package/lib/index.js +174 -641
- package/lib/index.js.map +4 -4
- package/lib/types/client/chat/assistant-step.d.ts +79 -0
- package/lib/types/client/chat/click-guard.d.ts +27 -0
- package/lib/types/client/chat/presentation.d.ts +51 -0
- package/lib/types/client/chat/row-plan.d.ts +72 -0
- package/lib/types/client/chat/styles.d.ts +19 -0
- package/lib/types/client/chat/translate-store.d.ts +73 -0
- package/lib/types/client/composer/dock-toggle.d.ts +26 -0
- package/lib/types/client/index.d.ts +26 -17
- package/lib/types/client/locales.d.ts +11 -20
- package/lib/types/client/settings/store.d.ts +5 -8
- package/lib/types/client/translate/api.d.ts +5 -16
- package/lib/types/index.d.ts +8 -23
- package/lib/types/server/cache.d.ts +4 -7
- package/lib/types/server/config.d.ts +10 -66
- package/lib/types/server/credentials.d.ts +3 -3
- package/lib/types/server/dispatcher.d.ts +31 -38
- package/lib/types/server/pipeline/{think.d.ts → blocks.d.ts} +9 -9
- package/lib/types/server/pipeline/mask-tokens.d.ts +4 -22
- package/lib/types/server/pipeline/masking.d.ts +7 -12
- package/lib/types/server/prompt-revision.d.ts +14 -0
- package/lib/types/server/router.d.ts +6 -8
- package/lib/types/server/types.d.ts +2 -9
- package/package.json +3 -3
- package/lib/types/client/translate/client-cache.d.ts +0 -19
- package/lib/types/client/translate/lazy.d.ts +0 -14
- package/lib/types/client/translate/mount.d.ts +0 -61
- package/lib/types/client/translate/observer.d.ts +0 -52
- 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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
window.__ModuleLoader__.load({ id: "@lynn123411/dsh-chat-translate", factory: (require) => { var module = { exports: {} }; var exports = module.exports;
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
4
5
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
9
|
var __export = (target, all) => {
|
|
8
10
|
for (var name2 in all)
|
|
@@ -16,151 +18,40 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
18
|
}
|
|
17
19
|
return to;
|
|
18
20
|
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
19
29
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
30
|
|
|
21
31
|
// src/client/index.ts
|
|
22
32
|
var index_exports = {};
|
|
23
33
|
__export(index_exports, {
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
AssistantStepView: () => AssistantStepView,
|
|
35
|
+
ComposerTranslateToggle: () => ComposerTranslateToggle,
|
|
36
|
+
POLICY_BY_MODE: () => POLICY_BY_MODE,
|
|
26
37
|
apply: () => apply,
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
chatPresentation: () => chatPresentation,
|
|
39
|
+
chatTranslate: () => chatTranslate,
|
|
40
|
+
createChatPresentation: () => createChatPresentation,
|
|
41
|
+
createTranslateStore: () => createTranslateStore,
|
|
42
|
+
ensureAssistantStyles: () => ensureAssistantStyles,
|
|
29
43
|
inject: () => inject,
|
|
30
|
-
lazyQueue: () => lazyQueue,
|
|
31
44
|
name: () => name,
|
|
45
|
+
planAssistantRow: () => planAssistantRow,
|
|
32
46
|
settingsStore: () => settingsStore,
|
|
47
|
+
setupComposerToggle: () => setupComposerToggle,
|
|
33
48
|
setupSettingsUi: () => setupSettingsUi
|
|
34
49
|
});
|
|
35
50
|
module.exports = __toCommonJS(index_exports);
|
|
36
51
|
|
|
37
|
-
// src/
|
|
38
|
-
var
|
|
39
|
-
var
|
|
40
|
-
function hasMaskResidue(text) {
|
|
41
|
-
if (!text) return false;
|
|
42
|
-
return new RegExp(MASK_TOKEN_PATTERN_SOURCE, "iu").test(text);
|
|
43
|
-
}
|
|
44
|
-
function hasLegacyMaskResidue(text) {
|
|
45
|
-
if (!text) return false;
|
|
46
|
-
return new RegExp(LEGACY_MASK_PATTERN_SOURCE, "iu").test(text);
|
|
47
|
-
}
|
|
48
|
-
function hasAnyMaskResidue(text) {
|
|
49
|
-
return hasMaskResidue(text) || hasLegacyMaskResidue(text);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// src/server/pipeline/masking.ts
|
|
53
|
-
function isMaskLeak(translatedText) {
|
|
54
|
-
return hasAnyMaskResidue(translatedText);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// src/client/translate/client-cache.ts
|
|
58
|
-
var TITLE_CACHE_KEY = "dsh-chat-translate:cache";
|
|
59
|
-
var TITLE_MAX_ENTRIES = 500;
|
|
60
|
-
var THINK_CACHE_KEY = "dsh-chat-translate:think-cache";
|
|
61
|
-
var THINK_MAX_ENTRIES = 300;
|
|
62
|
-
var TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
63
|
-
var ClientCache = class {
|
|
64
|
-
// Map preserves insertion order in JS, enabling true LRU semantics.
|
|
65
|
-
memCache = /* @__PURE__ */ new Map();
|
|
66
|
-
dirty = false;
|
|
67
|
-
saveTimer = null;
|
|
68
|
-
storageKey;
|
|
69
|
-
maxEntries;
|
|
70
|
-
constructor(storageKey = TITLE_CACHE_KEY, maxEntries = TITLE_MAX_ENTRIES) {
|
|
71
|
-
this.storageKey = storageKey;
|
|
72
|
-
this.maxEntries = maxEntries;
|
|
73
|
-
this.load();
|
|
74
|
-
}
|
|
75
|
-
load() {
|
|
76
|
-
if (typeof localStorage === "undefined") return;
|
|
77
|
-
try {
|
|
78
|
-
const raw = localStorage.getItem(this.storageKey);
|
|
79
|
-
if (raw) {
|
|
80
|
-
const obj = JSON.parse(raw);
|
|
81
|
-
if (obj && typeof obj === "object") {
|
|
82
|
-
for (const [k, entry] of Object.entries(obj)) {
|
|
83
|
-
if (typeof entry === "string") {
|
|
84
|
-
this.memCache.set(k, { t: 0, v: entry });
|
|
85
|
-
} else if (entry && typeof entry === "object" && typeof entry.v === "string") {
|
|
86
|
-
this.memCache.set(k, entry);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
} catch {
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
get(text) {
|
|
95
|
-
const key = text.trim().toLowerCase();
|
|
96
|
-
const entry = this.memCache.get(key);
|
|
97
|
-
if (entry === void 0) return void 0;
|
|
98
|
-
if (typeof entry.v === "string" && isMaskLeak(entry.v)) {
|
|
99
|
-
this.memCache.delete(key);
|
|
100
|
-
this.dirty = true;
|
|
101
|
-
this.scheduleSave();
|
|
102
|
-
return void 0;
|
|
103
|
-
}
|
|
104
|
-
if (entry.v && entry.v.trim().toLowerCase() === key) {
|
|
105
|
-
this.memCache.delete(key);
|
|
106
|
-
this.dirty = true;
|
|
107
|
-
this.scheduleSave();
|
|
108
|
-
return void 0;
|
|
109
|
-
}
|
|
110
|
-
if (entry.t > 0 && Date.now() - entry.t > TTL_MS) {
|
|
111
|
-
this.memCache.delete(key);
|
|
112
|
-
this.dirty = true;
|
|
113
|
-
this.scheduleSave();
|
|
114
|
-
return void 0;
|
|
115
|
-
}
|
|
116
|
-
this.memCache.delete(key);
|
|
117
|
-
this.memCache.set(key, entry);
|
|
118
|
-
return entry.v;
|
|
119
|
-
}
|
|
120
|
-
set(text, translated) {
|
|
121
|
-
const key = text.trim().toLowerCase();
|
|
122
|
-
if (this.memCache.has(key)) {
|
|
123
|
-
this.memCache.delete(key);
|
|
124
|
-
} else if (this.memCache.size >= this.maxEntries) {
|
|
125
|
-
const oldestKey = this.memCache.keys().next().value;
|
|
126
|
-
if (oldestKey !== void 0) {
|
|
127
|
-
this.memCache.delete(oldestKey);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
this.memCache.set(key, { t: Date.now(), v: translated });
|
|
131
|
-
this.dirty = true;
|
|
132
|
-
this.scheduleSave();
|
|
133
|
-
}
|
|
134
|
-
scheduleSave() {
|
|
135
|
-
if (this.saveTimer !== null || typeof window === "undefined") return;
|
|
136
|
-
this.saveTimer = window.setTimeout(() => {
|
|
137
|
-
this.saveTimer = null;
|
|
138
|
-
this.flushSync();
|
|
139
|
-
}, 2e3);
|
|
140
|
-
}
|
|
141
|
-
flushSync() {
|
|
142
|
-
if (!this.dirty || typeof localStorage === "undefined") return;
|
|
143
|
-
this.dirty = false;
|
|
144
|
-
try {
|
|
145
|
-
const obj = {};
|
|
146
|
-
for (const [k, v] of this.memCache.entries()) {
|
|
147
|
-
obj[k] = v;
|
|
148
|
-
}
|
|
149
|
-
localStorage.setItem(this.storageKey, JSON.stringify(obj));
|
|
150
|
-
} catch {
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
clear() {
|
|
154
|
-
this.memCache.clear();
|
|
155
|
-
this.dirty = true;
|
|
156
|
-
this.flushSync();
|
|
157
|
-
}
|
|
158
|
-
size() {
|
|
159
|
-
return this.memCache.size;
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
var clientCache = new ClientCache();
|
|
163
|
-
var thinkClientCache = new ClientCache(THINK_CACHE_KEY, THINK_MAX_ENTRIES);
|
|
52
|
+
// src/client/settings/ui.tsx
|
|
53
|
+
var import_react2 = require("react");
|
|
54
|
+
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
164
55
|
|
|
165
56
|
// src/describe-error.ts
|
|
166
57
|
function describeError(err) {
|
|
@@ -189,51 +80,7 @@ function describeError(err) {
|
|
|
189
80
|
}
|
|
190
81
|
|
|
191
82
|
// 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) {
|
|
83
|
+
async function requestTranslateReply(blocks) {
|
|
237
84
|
const fallback = () => blocks.map((block) => ({
|
|
238
85
|
original: block,
|
|
239
86
|
translated: block,
|
|
@@ -243,7 +90,7 @@ async function requestTranslateThink(blocks) {
|
|
|
243
90
|
}));
|
|
244
91
|
if (!Array.isArray(blocks) || blocks.length === 0) return [];
|
|
245
92
|
try {
|
|
246
|
-
const res = await fetch("/api/dsh-chat-translate/translate-
|
|
93
|
+
const res = await fetch("/api/dsh-chat-translate/translate-reply", {
|
|
247
94
|
method: "POST",
|
|
248
95
|
headers: { "Content-Type": "application/json" },
|
|
249
96
|
body: JSON.stringify({ blocks })
|
|
@@ -254,13 +101,13 @@ async function requestTranslateThink(blocks) {
|
|
|
254
101
|
return data.results;
|
|
255
102
|
}
|
|
256
103
|
if (data.error) {
|
|
257
|
-
console.warn(`[dsh-chat-translate] \
|
|
104
|
+
console.warn(`[dsh-chat-translate] \u6B63\u6587\u7FFB\u8BD1\u63A5\u53E3\u9519\u8BEF: ${data.error}`);
|
|
258
105
|
}
|
|
259
106
|
} else {
|
|
260
|
-
console.warn(`[dsh-chat-translate] \
|
|
107
|
+
console.warn(`[dsh-chat-translate] \u6B63\u6587\u7FFB\u8BD1\u8BF7\u6C42\u5931\u8D25 (HTTP ${res.status})`);
|
|
261
108
|
}
|
|
262
109
|
} catch (err) {
|
|
263
|
-
console.warn(`[dsh-chat-translate] \
|
|
110
|
+
console.warn(`[dsh-chat-translate] \u6B63\u6587\u7FFB\u8BD1\u8BF7\u6C42\u5F02\u5E38: ${describeError(err)}`);
|
|
264
111
|
}
|
|
265
112
|
return fallback();
|
|
266
113
|
}
|
|
@@ -277,1174 +124,14 @@ async function testServerChannel(channel) {
|
|
|
277
124
|
}
|
|
278
125
|
}
|
|
279
126
|
|
|
280
|
-
// src/client/translate/mount.ts
|
|
281
|
-
var CLASS_ORIGINAL_HIDDEN = "dsh-tidy-original-hidden";
|
|
282
|
-
var CLASS_ORIGINAL_SHOWN = "dsh-tidy-original-shown";
|
|
283
|
-
var CLASS_TRANSLATED_BLOCK = "dsh-tidy-translated-block";
|
|
284
|
-
var CLASS_RUN = "dsh-tidy-run";
|
|
285
|
-
var RUN_ATTRIBUTE = "data-tidy-run";
|
|
286
|
-
var NonDestructiveTranslationMount = class {
|
|
287
|
-
/**
|
|
288
|
-
* Mounts a translated string onto the target element non-destructively.
|
|
289
|
-
*/
|
|
290
|
-
static mount(element, translated, options = {}) {
|
|
291
|
-
if (!element || !element.ownerDocument) return;
|
|
292
|
-
const doc = element.ownerDocument;
|
|
293
|
-
let transWrapper = element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
|
|
294
|
-
let origWrapper = element.querySelector(
|
|
295
|
-
`:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
|
|
296
|
-
);
|
|
297
|
-
if (transWrapper && origWrapper) {
|
|
298
|
-
transWrapper.textContent = translated;
|
|
299
|
-
element.dataset.tidyTranslated = "true";
|
|
300
|
-
if (options.originalText) element.dataset.original = options.originalText;
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
const originalText = options.originalText ?? this.extractVisibleText(element);
|
|
304
|
-
origWrapper = doc.createElement("span");
|
|
305
|
-
origWrapper.className = CLASS_ORIGINAL_HIDDEN;
|
|
306
|
-
origWrapper.style.display = "none";
|
|
307
|
-
while (element.firstChild) {
|
|
308
|
-
origWrapper.appendChild(element.firstChild);
|
|
309
|
-
}
|
|
310
|
-
transWrapper = doc.createElement("span");
|
|
311
|
-
transWrapper.className = CLASS_TRANSLATED_BLOCK;
|
|
312
|
-
transWrapper.textContent = translated;
|
|
313
|
-
const interactive = options.interactive !== false;
|
|
314
|
-
if (interactive) {
|
|
315
|
-
const showOriginal = (e) => {
|
|
316
|
-
e.stopPropagation();
|
|
317
|
-
if (!origWrapper || !transWrapper) return;
|
|
318
|
-
origWrapper.style.display = "inline";
|
|
319
|
-
origWrapper.className = CLASS_ORIGINAL_SHOWN;
|
|
320
|
-
transWrapper.style.display = "none";
|
|
321
|
-
};
|
|
322
|
-
const showTranslated = (e) => {
|
|
323
|
-
e.stopPropagation();
|
|
324
|
-
if (!origWrapper || !transWrapper) return;
|
|
325
|
-
origWrapper.style.display = "none";
|
|
326
|
-
origWrapper.className = CLASS_ORIGINAL_HIDDEN;
|
|
327
|
-
transWrapper.style.display = "inline";
|
|
328
|
-
};
|
|
329
|
-
transWrapper.addEventListener("click", showOriginal);
|
|
330
|
-
origWrapper.addEventListener("click", showTranslated);
|
|
331
|
-
}
|
|
332
|
-
element.appendChild(transWrapper);
|
|
333
|
-
element.appendChild(origWrapper);
|
|
334
|
-
element.dataset.tidyTranslated = "true";
|
|
335
|
-
element.dataset.original = originalText;
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* 把一段连续的行内节点包进自建容器后挂载译文。
|
|
339
|
-
*
|
|
340
|
-
* 混合块(列表项文字 + 子列表 / 代码块)里的行内片段没有自己的元素,
|
|
341
|
-
* 只能先包一层再走同一条非侵入式挂载;嵌套的块级子节点留在容器之外,
|
|
342
|
-
* 排版原样保留。
|
|
343
|
-
* @returns 自建容器;节点不再相邻或没有共同父节点时返回 null。
|
|
344
|
-
*/
|
|
345
|
-
static mountRun(nodes, translated, options = {}) {
|
|
346
|
-
const first = nodes[0];
|
|
347
|
-
const parent = first?.parentNode;
|
|
348
|
-
if (!first || !parent || !parent.ownerDocument) return null;
|
|
349
|
-
if (!nodes.every((node) => node.parentNode === parent)) return null;
|
|
350
|
-
const originalText = options.originalText ?? nodes.map((node) => node.textContent ?? "").join("").trim();
|
|
351
|
-
if (!originalText) return null;
|
|
352
|
-
const run = parent.ownerDocument.createElement("span");
|
|
353
|
-
run.className = CLASS_RUN;
|
|
354
|
-
run.setAttribute(RUN_ATTRIBUTE, "true");
|
|
355
|
-
parent.insertBefore(run, first);
|
|
356
|
-
for (const node of nodes) run.appendChild(node);
|
|
357
|
-
this.mount(run, translated, { originalText, interactive: options.interactive });
|
|
358
|
-
return run;
|
|
359
|
-
}
|
|
360
|
-
/** 还原行内片段并撤掉自建容器。 */
|
|
361
|
-
static unmountRun(run) {
|
|
362
|
-
this.unmount(run);
|
|
363
|
-
const parent = run.parentNode;
|
|
364
|
-
if (!parent) return;
|
|
365
|
-
while (run.firstChild) parent.insertBefore(run.firstChild, run);
|
|
366
|
-
parent.removeChild(run);
|
|
367
|
-
}
|
|
368
|
-
/** 还原一个作用域内我们挂过的全部译文(元素单位与行内片段都算)。 */
|
|
369
|
-
static restore(scope) {
|
|
370
|
-
const mounted = scope.querySelectorAll('[data-tidy-translated="true"]');
|
|
371
|
-
for (const element of mounted) {
|
|
372
|
-
if (element.getAttribute(RUN_ATTRIBUTE) === "true") this.unmountRun(element);
|
|
373
|
-
else this.unmount(element);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
/** 我们自己的挂载容器与还原容器,观察器必须跳过它们。 */
|
|
377
|
-
static isOwnNode(element) {
|
|
378
|
-
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
|
-
}
|
|
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
|
-
/**
|
|
404
|
-
* Unmounts translation and restores original DOM nodes completely.
|
|
405
|
-
*/
|
|
406
|
-
static unmount(element) {
|
|
407
|
-
if (!element) return;
|
|
408
|
-
const origWrapper = element.querySelector(
|
|
409
|
-
`:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
|
|
410
|
-
);
|
|
411
|
-
const transWrapper = element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
|
|
412
|
-
if (origWrapper) {
|
|
413
|
-
while (origWrapper.firstChild) {
|
|
414
|
-
element.insertBefore(origWrapper.firstChild, origWrapper);
|
|
415
|
-
}
|
|
416
|
-
origWrapper.remove();
|
|
417
|
-
}
|
|
418
|
-
if (transWrapper) {
|
|
419
|
-
transWrapper.remove();
|
|
420
|
-
}
|
|
421
|
-
if (!origWrapper && element.dataset.original) {
|
|
422
|
-
element.textContent = element.dataset.original;
|
|
423
|
-
}
|
|
424
|
-
delete element.dataset.tidyTranslated;
|
|
425
|
-
delete element.dataset.original;
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
|
-
* Checks if an element has non-destructive translation mounted.
|
|
429
|
-
*/
|
|
430
|
-
static isMounted(element) {
|
|
431
|
-
return element.dataset.tidyTranslated === "true" && !!element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Gets the original text recorded on the element or contained in origWrapper.
|
|
435
|
-
*/
|
|
436
|
-
static getOriginal(element) {
|
|
437
|
-
if (element.dataset.original) return element.dataset.original;
|
|
438
|
-
const origWrapper = element.querySelector(
|
|
439
|
-
`:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
|
|
440
|
-
);
|
|
441
|
-
return origWrapper ? origWrapper.textContent?.trim() : void 0;
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Extracts text content excluding our own translation wrappers.
|
|
445
|
-
*/
|
|
446
|
-
static extractVisibleText(element) {
|
|
447
|
-
const origWrapper = element.querySelector(
|
|
448
|
-
`:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
|
|
449
|
-
);
|
|
450
|
-
if (origWrapper) {
|
|
451
|
-
return origWrapper.textContent?.trim() || "";
|
|
452
|
-
}
|
|
453
|
-
const transWrapper = element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
|
|
454
|
-
if (transWrapper) {
|
|
455
|
-
return transWrapper.textContent?.trim() || "";
|
|
456
|
-
}
|
|
457
|
-
return element.textContent?.trim() || "";
|
|
458
|
-
}
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
// src/client/translate/viewport-observer.ts
|
|
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
|
|
670
|
-
var CJK_CHAR = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/;
|
|
671
|
-
function estimateTokens(text) {
|
|
672
|
-
let cjk = 0;
|
|
673
|
-
for (const char of text) {
|
|
674
|
-
if (CJK_CHAR.test(char)) cjk += 1;
|
|
675
|
-
}
|
|
676
|
-
return cjk + Math.ceil((text.length - cjk) / 3);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// src/client/translate/think-button.ts
|
|
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]";
|
|
791
|
-
var BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
792
|
-
"P",
|
|
793
|
-
"H1",
|
|
794
|
-
"H2",
|
|
795
|
-
"H3",
|
|
796
|
-
"H4",
|
|
797
|
-
"H5",
|
|
798
|
-
"H6",
|
|
799
|
-
"LI",
|
|
800
|
-
"BLOCKQUOTE",
|
|
801
|
-
"TD",
|
|
802
|
-
"TH",
|
|
803
|
-
"DT",
|
|
804
|
-
"DD",
|
|
805
|
-
"FIGCAPTION"
|
|
806
|
-
]);
|
|
807
|
-
var CONTAINER_TAGS = /* @__PURE__ */ new Set(["UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR", "DIV", "SECTION"]);
|
|
808
|
-
var NESTED_BOUNDARY_SELECTOR = [...BLOCK_TAGS, ...CONTAINER_TAGS, "PRE"].join(",").toLowerCase();
|
|
809
|
-
var REQUEST_TOKENS = 2048;
|
|
810
|
-
var SYNC_DEBOUNCE_MS = 120;
|
|
811
|
-
function isMountedElement(element) {
|
|
812
|
-
return element.dataset?.tidyTranslated === "true";
|
|
813
|
-
}
|
|
814
|
-
function hasNestedBoundary(element) {
|
|
815
|
-
return element.querySelector(NESTED_BOUNDARY_SELECTOR) !== null;
|
|
816
|
-
}
|
|
817
|
-
function collectThinkUnits(body) {
|
|
818
|
-
const units = [];
|
|
819
|
-
const pushRun = (nodes) => {
|
|
820
|
-
if (nodes.length === 0) return;
|
|
821
|
-
const text = nodes.map((node) => node.textContent ?? "").join("").trim();
|
|
822
|
-
if (!text) return;
|
|
823
|
-
const anchor = nodes[0];
|
|
824
|
-
if (anchor === void 0) return;
|
|
825
|
-
units.push({ nodes, text, anchor });
|
|
826
|
-
};
|
|
827
|
-
const walk = (container) => {
|
|
828
|
-
let run = [];
|
|
829
|
-
const flush = () => {
|
|
830
|
-
pushRun(run);
|
|
831
|
-
run = [];
|
|
832
|
-
};
|
|
833
|
-
for (const node of Array.from(container.childNodes)) {
|
|
834
|
-
if (node.nodeType !== 1) {
|
|
835
|
-
run.push(node);
|
|
836
|
-
continue;
|
|
837
|
-
}
|
|
838
|
-
const element = node;
|
|
839
|
-
if (NonDestructiveTranslationMount.isOwnNode(element)) {
|
|
840
|
-
flush();
|
|
841
|
-
continue;
|
|
842
|
-
}
|
|
843
|
-
if (element.tagName === "PRE") {
|
|
844
|
-
flush();
|
|
845
|
-
continue;
|
|
846
|
-
}
|
|
847
|
-
if (BLOCK_TAGS.has(element.tagName)) {
|
|
848
|
-
flush();
|
|
849
|
-
collectBlock(element);
|
|
850
|
-
continue;
|
|
851
|
-
}
|
|
852
|
-
if (CONTAINER_TAGS.has(element.tagName)) {
|
|
853
|
-
flush();
|
|
854
|
-
walk(element);
|
|
855
|
-
continue;
|
|
856
|
-
}
|
|
857
|
-
run.push(element);
|
|
858
|
-
}
|
|
859
|
-
flush();
|
|
860
|
-
};
|
|
861
|
-
const collectBlock = (element) => {
|
|
862
|
-
if (isMountedElement(element)) return;
|
|
863
|
-
if (!hasNestedBoundary(element)) {
|
|
864
|
-
const text = (element.textContent ?? "").trim();
|
|
865
|
-
if (text) units.push({ element, text, anchor: element });
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
walk(element);
|
|
869
|
-
};
|
|
870
|
-
walk(body);
|
|
871
|
-
return units;
|
|
872
|
-
}
|
|
873
|
-
function chunkUnits(units, maxTokens = REQUEST_TOKENS) {
|
|
874
|
-
const chunks = [];
|
|
875
|
-
let current = [];
|
|
876
|
-
let tokens = 0;
|
|
877
|
-
for (const unit of units) {
|
|
878
|
-
const cost = Math.max(estimateTokens(unit.text), 1);
|
|
879
|
-
if (current.length > 0 && tokens + cost > maxTokens) {
|
|
880
|
-
chunks.push(current);
|
|
881
|
-
current = [];
|
|
882
|
-
tokens = 0;
|
|
883
|
-
}
|
|
884
|
-
current.push(unit);
|
|
885
|
-
tokens += cost;
|
|
886
|
-
}
|
|
887
|
-
if (current.length > 0) chunks.push(current);
|
|
888
|
-
return chunks;
|
|
889
|
-
}
|
|
890
|
-
var ThinkTranslateController = class {
|
|
891
|
-
/** 总开关与设置里的思考链开关同时打开。 */
|
|
892
|
-
enabled = false;
|
|
893
|
-
/** AI 通道开关打开且已配置;缺任何一条都不注入按钮。 */
|
|
894
|
-
configured = false;
|
|
895
|
-
states = /* @__PURE__ */ new WeakMap();
|
|
896
|
-
syncTimers = /* @__PURE__ */ new WeakMap();
|
|
897
|
-
/** 只处理当前会话的容器;由观察器在会话切换时更新。 */
|
|
898
|
-
scope = null;
|
|
899
|
-
/** 观察器把当前会话的根交给控制器,插入按钮时只看这一棵子树。 */
|
|
900
|
-
setScope(scope) {
|
|
901
|
-
this.scope = scope;
|
|
902
|
-
}
|
|
903
|
-
isEnabled() {
|
|
904
|
-
return this.enabled;
|
|
905
|
-
}
|
|
906
|
-
setEnabled(enabled) {
|
|
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);
|
|
1028
|
-
}
|
|
1029
|
-
/** 展开时若整条都命中缓存,直接显示中文,不点按钮也不发请求。 */
|
|
1030
|
-
showCachedWhenComplete(card, state) {
|
|
1031
|
-
const body = this.bodyOf(card);
|
|
1032
|
-
if (body === null || state.state === "working") return;
|
|
1033
|
-
const units = collectThinkUnits(body);
|
|
1034
|
-
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
|
-
const missing = [];
|
|
1068
|
-
for (const unit of units) {
|
|
1069
|
-
const cached = thinkClientCache.get(unit.text);
|
|
1070
|
-
if (cached === void 0) missing.push(unit);
|
|
1071
|
-
else this.applyUnit(unit, cached);
|
|
1072
|
-
}
|
|
1073
|
-
if (missing.length === 0) {
|
|
1074
|
-
state.state = this.isShowingTranslation(card) ? "translated" : "idle";
|
|
1075
|
-
state.button?.setState(state.state);
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
state.state = "working";
|
|
1079
|
-
state.button?.setState("working");
|
|
1080
|
-
void this.translateUnits(card, state, missing);
|
|
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
|
-
}
|
|
1089
|
-
const texts = chunk.map((unit) => unit.text);
|
|
1090
|
-
let results = [];
|
|
1091
|
-
try {
|
|
1092
|
-
results = await requestTranslateThink(texts);
|
|
1093
|
-
} catch {
|
|
1094
|
-
failed = true;
|
|
1095
|
-
}
|
|
1096
|
-
const byText = new Map(results.map((result) => [result.original, result]));
|
|
1097
|
-
for (const unit of chunk) {
|
|
1098
|
-
const result = byText.get(unit.text);
|
|
1099
|
-
if (result === void 0 || !result.ok || !result.translated.trim() || result.translated.trim() === unit.text.trim()) {
|
|
1100
|
-
failed = true;
|
|
1101
|
-
continue;
|
|
1102
|
-
}
|
|
1103
|
-
thinkClientCache.set(unit.text, result.translated);
|
|
1104
|
-
this.applyUnit(unit, result.translated);
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
state.state = !failed && this.isShowingTranslation(card) ? "translated" : "idle";
|
|
1108
|
-
state.button?.setState(state.state);
|
|
1109
|
-
this.updateDisabled(card, state);
|
|
1110
|
-
}
|
|
1111
|
-
/** 把译文挂到单位上;期间文本或节点已经变动的单位直接放弃(结果已进缓存)。 */
|
|
1112
|
-
applyUnit(unit, translated) {
|
|
1113
|
-
if (unit.element !== void 0) {
|
|
1114
|
-
const element = unit.element;
|
|
1115
|
-
if (!element.isConnected || isMountedElement(element)) return;
|
|
1116
|
-
if ((element.textContent ?? "").trim() !== unit.text) return;
|
|
1117
|
-
NonDestructiveTranslationMount.mount(element, translated, {
|
|
1118
|
-
originalText: unit.text,
|
|
1119
|
-
interactive: false
|
|
1120
|
-
});
|
|
1121
|
-
return;
|
|
1122
|
-
}
|
|
1123
|
-
const nodes = unit.nodes ?? [];
|
|
1124
|
-
if (nodes.length === 0 || nodes.some((node) => !node.isConnected)) return;
|
|
1125
|
-
const parent = nodes[0]?.parentNode ?? null;
|
|
1126
|
-
if (parent === null || !nodes.every((node) => node.parentNode === parent)) return;
|
|
1127
|
-
const text = nodes.map((node) => node.textContent ?? "").join("").trim();
|
|
1128
|
-
if (text !== unit.text) return;
|
|
1129
|
-
NonDestructiveTranslationMount.mountRun(nodes, translated, {
|
|
1130
|
-
originalText: unit.text,
|
|
1131
|
-
interactive: false
|
|
1132
|
-
});
|
|
1133
|
-
}
|
|
1134
|
-
};
|
|
1135
|
-
var thinkTranslator = new ThinkTranslateController();
|
|
1136
|
-
|
|
1137
|
-
// src/client/translate/observer.ts
|
|
1138
|
-
var TOOL_TITLE_SELECTOR = '[class*="summary" i]';
|
|
1139
|
-
var THINK_CARD_SELECTOR2 = '[data-variant="think"]';
|
|
1140
|
-
var SESSION_ROOT_SELECTOR = "[data-conversation-scroll], [data-chat-flow]";
|
|
1141
|
-
var ROOT_CHECK_INTERVAL_MS = 3e3;
|
|
1142
|
-
function isToolSummarySpan(span) {
|
|
1143
|
-
if (!span || span.nodeType !== 1) return false;
|
|
1144
|
-
if (span.hasAttribute("aria-hidden")) return false;
|
|
1145
|
-
if (span.closest(THINK_CARD_SELECTOR2)) return false;
|
|
1146
|
-
if (span.querySelector?.('[class*="title"], [class*="leading"], [class*="chevron"], [class*="sep"], [class*="summary" i]')) {
|
|
1147
|
-
return false;
|
|
1148
|
-
}
|
|
1149
|
-
const cls = span.className || "";
|
|
1150
|
-
if (/title|leading|icon|badge|chevron|separator|sep\b|row\b|root\b|card\b/i.test(cls)) return false;
|
|
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;
|
|
1165
|
-
}
|
|
1166
|
-
var ChatTranslateObserver = class {
|
|
1167
|
-
observer = null;
|
|
1168
|
-
rootElement = null;
|
|
1169
|
-
rootCheckTimer = null;
|
|
1170
|
-
isEnabled = true;
|
|
1171
|
-
/** 设置里的思考链开关;与总开关一起决定按钮是否存在。 */
|
|
1172
|
-
thinkEnabled = false;
|
|
1173
|
-
constructor() {
|
|
1174
|
-
this.handleMutations = this.handleMutations.bind(this);
|
|
1175
|
-
}
|
|
1176
|
-
setEnabled(enabled) {
|
|
1177
|
-
this.isEnabled = enabled;
|
|
1178
|
-
thinkTranslator.setEnabled(enabled && this.thinkEnabled);
|
|
1179
|
-
if (enabled) {
|
|
1180
|
-
lazyQueue.setEnabled(true);
|
|
1181
|
-
this.start();
|
|
1182
|
-
} else {
|
|
1183
|
-
this.restoreOriginals();
|
|
1184
|
-
this.disconnect();
|
|
1185
|
-
lazyQueue.setEnabled(false);
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
restoreOriginals() {
|
|
1189
|
-
const scope = this.rootElement ?? document;
|
|
1190
|
-
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);
|
|
1206
|
-
}
|
|
1207
|
-
/**
|
|
1208
|
-
* The active session's scroll container. Only the currently-viewed session
|
|
1209
|
-
* is translated; switching sessions replaces this subtree and the observer
|
|
1210
|
-
* naturally follows the new content.
|
|
1211
|
-
*/
|
|
1212
|
-
isVisible(el) {
|
|
1213
|
-
if (el.hasAttribute("hidden")) return false;
|
|
1214
|
-
if (el.style.display === "none") return false;
|
|
1215
|
-
try {
|
|
1216
|
-
return el.getClientRects().length > 0;
|
|
1217
|
-
} catch {
|
|
1218
|
-
return true;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
findRoot(documentRef) {
|
|
1222
|
-
if (!documentRef.body) {
|
|
1223
|
-
return documentRef.documentElement;
|
|
1224
|
-
}
|
|
1225
|
-
const candidates = documentRef.querySelectorAll(SESSION_ROOT_SELECTOR);
|
|
1226
|
-
for (const el of candidates) {
|
|
1227
|
-
if (this.isVisible(el)) {
|
|
1228
|
-
return el;
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
return documentRef.body;
|
|
1232
|
-
}
|
|
1233
|
-
start(documentRef = document) {
|
|
1234
|
-
if (!this.isEnabled || typeof window === "undefined") {
|
|
1235
|
-
return () => {
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
const findAndObserveRoot = () => {
|
|
1239
|
-
const root = this.findRoot(documentRef);
|
|
1240
|
-
this.rootElement = root;
|
|
1241
|
-
thinkTranslator.setScope(root);
|
|
1242
|
-
this.scanContainer(root);
|
|
1243
|
-
this.scanThink(root);
|
|
1244
|
-
if (!this.observer) {
|
|
1245
|
-
this.observer = new MutationObserver(this.handleMutations);
|
|
1246
|
-
this.observer.observe(root, {
|
|
1247
|
-
childList: true,
|
|
1248
|
-
subtree: true,
|
|
1249
|
-
attributes: true,
|
|
1250
|
-
characterData: true,
|
|
1251
|
-
attributeFilter: [
|
|
1252
|
-
"data-state",
|
|
1253
|
-
"data-tool",
|
|
1254
|
-
"data-variant",
|
|
1255
|
-
"data-sample",
|
|
1256
|
-
"aria-expanded",
|
|
1257
|
-
"data-open",
|
|
1258
|
-
"data-expanded"
|
|
1259
|
-
]
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
};
|
|
1263
|
-
findAndObserveRoot();
|
|
1264
|
-
this.scheduleRootCheck();
|
|
1265
|
-
return () => {
|
|
1266
|
-
this.disconnect();
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
scheduleRootCheck() {
|
|
1270
|
-
if (this.rootCheckTimer !== null || typeof window === "undefined") return;
|
|
1271
|
-
this.rootCheckTimer = window.setInterval(() => {
|
|
1272
|
-
if (this.rootElement && this.rootElement.isConnected && this.isVisible(this.rootElement)) {
|
|
1273
|
-
return;
|
|
1274
|
-
}
|
|
1275
|
-
this.restart();
|
|
1276
|
-
}, ROOT_CHECK_INTERVAL_MS);
|
|
1277
|
-
}
|
|
1278
|
-
restart() {
|
|
1279
|
-
if (typeof window === "undefined") return;
|
|
1280
|
-
const wasEnabled = this.isEnabled;
|
|
1281
|
-
this.disconnect();
|
|
1282
|
-
if (wasEnabled) {
|
|
1283
|
-
this.start();
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
handleMutations(mutations) {
|
|
1287
|
-
if (!this.isEnabled) return;
|
|
1288
|
-
for (const mutation of mutations) {
|
|
1289
|
-
if (mutation.type === "childList") {
|
|
1290
|
-
let addedElement = false;
|
|
1291
|
-
let changedOwner = null;
|
|
1292
|
-
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
|
1293
|
-
const node = mutation.addedNodes[i];
|
|
1294
|
-
if (node instanceof HTMLElement) {
|
|
1295
|
-
const owner = this.mountedOwner(node);
|
|
1296
|
-
if (owner) {
|
|
1297
|
-
changedOwner = owner;
|
|
1298
|
-
continue;
|
|
1299
|
-
}
|
|
1300
|
-
if (NonDestructiveTranslationMount.isOwnNode(node)) continue;
|
|
1301
|
-
addedElement = true;
|
|
1302
|
-
this.scanNode(node);
|
|
1303
|
-
this.scanThink(node);
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
const target = mutation.target;
|
|
1307
|
-
if (target instanceof HTMLElement) {
|
|
1308
|
-
const owner = this.mountedOwner(target);
|
|
1309
|
-
if (owner) {
|
|
1310
|
-
changedOwner = owner;
|
|
1311
|
-
} else if (!addedElement && !NonDestructiveTranslationMount.isOwnNode(target)) {
|
|
1312
|
-
this.scanNode(target);
|
|
1313
|
-
this.scanThink(target);
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
if (changedOwner) this.handleOriginalTextChange(changedOwner);
|
|
1317
|
-
} else if (mutation.type === "attributes") {
|
|
1318
|
-
const target = mutation.target;
|
|
1319
|
-
if (target instanceof HTMLElement) {
|
|
1320
|
-
if (NonDestructiveTranslationMount.isOwnNode(target)) continue;
|
|
1321
|
-
this.scanNode(target);
|
|
1322
|
-
this.scanThink(target);
|
|
1323
|
-
}
|
|
1324
|
-
} else if (mutation.type === "characterData") {
|
|
1325
|
-
const owner = this.mountedOwner(mutation.target);
|
|
1326
|
-
if (owner) {
|
|
1327
|
-
this.handleOriginalTextChange(owner);
|
|
1328
|
-
continue;
|
|
1329
|
-
}
|
|
1330
|
-
const parent = mutation.target.parentElement;
|
|
1331
|
-
if (parent instanceof HTMLElement) {
|
|
1332
|
-
if (NonDestructiveTranslationMount.isOwnNode(parent)) continue;
|
|
1333
|
-
this.scanNode(parent);
|
|
1334
|
-
this.scanThink(parent);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
/**
|
|
1340
|
-
* 变更发生在我们移进隐藏容器的原文里时,返回挂载了译文的那个元素。React
|
|
1341
|
-
* 就地改写它自己创建的文本节点,所以「原文变了」只能从这条路径看到。
|
|
1342
|
-
*/
|
|
1343
|
-
mountedOwner(node) {
|
|
1344
|
-
const element = node instanceof HTMLElement ? node : node.parentElement;
|
|
1345
|
-
if (!element) return null;
|
|
1346
|
-
const wrapper = element.closest(
|
|
1347
|
-
`.${CLASS_ORIGINAL_HIDDEN}, .${CLASS_ORIGINAL_SHOWN}`
|
|
1348
|
-
);
|
|
1349
|
-
if (!wrapper) return null;
|
|
1350
|
-
const owner = wrapper.parentElement;
|
|
1351
|
-
return owner instanceof HTMLElement && owner.dataset.tidyTranslated === "true" ? owner : null;
|
|
1352
|
-
}
|
|
1353
|
-
/**
|
|
1354
|
-
* 已挂载行的隐藏原文被上游改写了(ask_user 计数、失败摘要、todo 计数):
|
|
1355
|
-
* 命中缓存就原地换译文,否则请求新译文后原地换。行始终可见,不出现英文。
|
|
1356
|
-
*/
|
|
1357
|
-
handleOriginalTextChange(owner) {
|
|
1358
|
-
if (!isToolSummarySpan(owner)) return;
|
|
1359
|
-
const current = NonDestructiveTranslationMount.extractVisibleText(owner);
|
|
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;
|
|
1365
|
-
}
|
|
1366
|
-
lazyQueue.observe(owner, current);
|
|
1367
|
-
}
|
|
1368
|
-
scanContainer(container) {
|
|
1369
|
-
const spans = container.querySelectorAll(TOOL_TITLE_SELECTOR);
|
|
1370
|
-
spans.forEach((span) => {
|
|
1371
|
-
if (isToolSummarySpan(span)) {
|
|
1372
|
-
this.processSpan(span);
|
|
1373
|
-
}
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
scanNode(node) {
|
|
1377
|
-
if (node.matches?.(TOOL_TITLE_SELECTOR) && isToolSummarySpan(node)) {
|
|
1378
|
-
this.processSpan(node);
|
|
1379
|
-
}
|
|
1380
|
-
const spans = node.querySelectorAll(TOOL_TITLE_SELECTOR);
|
|
1381
|
-
spans.forEach((span) => {
|
|
1382
|
-
if (isToolSummarySpan(span)) {
|
|
1383
|
-
this.processSpan(span);
|
|
1384
|
-
}
|
|
1385
|
-
});
|
|
1386
|
-
}
|
|
1387
|
-
/**
|
|
1388
|
-
* 交给思考正文翻译控制器:命中节点所在的卡片,以及它下面(或它就是)的
|
|
1389
|
-
* 全部思考卡片。
|
|
1390
|
-
*/
|
|
1391
|
-
scanThink(node) {
|
|
1392
|
-
if (!thinkTranslator.isEnabled()) return;
|
|
1393
|
-
if (node instanceof HTMLElement) {
|
|
1394
|
-
const card = node.closest(THINK_CARD_SELECTOR2);
|
|
1395
|
-
if (card) thinkTranslator.syncCard(card);
|
|
1396
|
-
}
|
|
1397
|
-
if (!node.querySelectorAll) return;
|
|
1398
|
-
node.querySelectorAll(THINK_CARD_SELECTOR2).forEach((card) => {
|
|
1399
|
-
thinkTranslator.syncCard(card);
|
|
1400
|
-
});
|
|
1401
|
-
}
|
|
1402
|
-
processSpan(span) {
|
|
1403
|
-
if (NonDestructiveTranslationMount.isMounted(span)) {
|
|
1404
|
-
return;
|
|
1405
|
-
}
|
|
1406
|
-
const text = NonDestructiveTranslationMount.extractVisibleText(span);
|
|
1407
|
-
if (!text) return;
|
|
1408
|
-
const cached = clientCache.get(text);
|
|
1409
|
-
if (cached) {
|
|
1410
|
-
NonDestructiveTranslationMount.mount(span, cached, {
|
|
1411
|
-
originalText: text
|
|
1412
|
-
});
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
lazyQueue.observe(span, text);
|
|
1416
|
-
}
|
|
1417
|
-
disconnect() {
|
|
1418
|
-
if (this.rootCheckTimer !== null) {
|
|
1419
|
-
clearInterval(this.rootCheckTimer);
|
|
1420
|
-
this.rootCheckTimer = null;
|
|
1421
|
-
}
|
|
1422
|
-
if (this.observer) {
|
|
1423
|
-
this.observer.disconnect();
|
|
1424
|
-
this.observer = null;
|
|
1425
|
-
}
|
|
1426
|
-
lazyQueue.disconnect();
|
|
1427
|
-
this.rootElement = null;
|
|
1428
|
-
}
|
|
1429
|
-
};
|
|
1430
|
-
var chatTranslateObserver = new ChatTranslateObserver();
|
|
1431
|
-
|
|
1432
|
-
// src/client/settings/ui.tsx
|
|
1433
|
-
var import_react2 = require("react");
|
|
1434
|
-
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
1435
|
-
|
|
1436
127
|
// src/client/settings/store.ts
|
|
1437
|
-
var
|
|
1438
|
-
var
|
|
128
|
+
var AI_TIMEOUT_MIN = 500;
|
|
129
|
+
var AI_TIMEOUT_MAX = 9e5;
|
|
1439
130
|
var SETTINGS_NAMESPACE = "dsh-chat-translate";
|
|
1440
131
|
var TRANSLATE_API_KEY_REF = "TRANSLATE_API_KEY";
|
|
1441
132
|
var DEFAULT_STATE = {
|
|
1442
133
|
enabled: true,
|
|
1443
|
-
|
|
1444
|
-
aiEnabled: true,
|
|
1445
|
-
bingEnabled: true,
|
|
1446
|
-
thinkEnabled: true,
|
|
1447
|
-
thinkTimeoutMs: 6e5,
|
|
134
|
+
aiTimeoutMs: 6e5,
|
|
1448
135
|
baseUrl: "",
|
|
1449
136
|
model: "",
|
|
1450
137
|
aiConfigured: false
|
|
@@ -1492,39 +179,20 @@ var SettingsStore = class {
|
|
|
1492
179
|
if (!value || typeof value !== "object") return;
|
|
1493
180
|
const next = { ...this.state };
|
|
1494
181
|
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
|
|
182
|
+
const timeout = value.aiTimeoutMs;
|
|
183
|
+
if (typeof timeout === "number" && Number.isFinite(timeout)) {
|
|
184
|
+
next.aiTimeoutMs = Math.min(
|
|
185
|
+
Math.max(Math.round(timeout), AI_TIMEOUT_MIN),
|
|
186
|
+
AI_TIMEOUT_MAX
|
|
1507
187
|
);
|
|
1508
188
|
}
|
|
1509
189
|
if (typeof value.baseUrl === "string") next.baseUrl = value.baseUrl;
|
|
1510
|
-
if (typeof value.model === "string") next.model = value.model;
|
|
1511
|
-
this.applyState(next);
|
|
1512
|
-
}
|
|
1513
|
-
applyState(next) {
|
|
1514
|
-
next.aiConfigured = Boolean(next.baseUrl && next.model && this.keyConfigured);
|
|
1515
|
-
|
|
1516
|
-
this.state = next;
|
|
1517
|
-
if (enabledChanged) {
|
|
1518
|
-
try {
|
|
1519
|
-
chatTranslateObserver.setEnabled(this.state.enabled);
|
|
1520
|
-
} catch {
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
try {
|
|
1524
|
-
chatTranslateObserver.setThinkEnabled(this.state.enabled && this.state.thinkEnabled);
|
|
1525
|
-
chatTranslateObserver.setThinkConfigured(this.state.aiEnabled && this.state.aiConfigured);
|
|
1526
|
-
} catch {
|
|
1527
|
-
}
|
|
190
|
+
if (typeof value.model === "string") next.model = value.model;
|
|
191
|
+
this.applyState(next);
|
|
192
|
+
}
|
|
193
|
+
applyState(next) {
|
|
194
|
+
next.aiConfigured = Boolean(next.baseUrl && next.model && this.keyConfigured);
|
|
195
|
+
this.state = next;
|
|
1528
196
|
this.notify();
|
|
1529
197
|
}
|
|
1530
198
|
notify() {
|
|
@@ -1560,35 +228,21 @@ var SettingsStore = class {
|
|
|
1560
228
|
* (conflict-safe recovery).
|
|
1561
229
|
*/
|
|
1562
230
|
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
|
|
231
|
+
let sanitizedTimeout = this.state.aiTimeoutMs;
|
|
232
|
+
if (typeof partial.aiTimeoutMs === "number" && Number.isFinite(partial.aiTimeoutMs)) {
|
|
233
|
+
sanitizedTimeout = Math.min(
|
|
234
|
+
Math.max(Math.round(partial.aiTimeoutMs), AI_TIMEOUT_MIN),
|
|
235
|
+
AI_TIMEOUT_MAX
|
|
1572
236
|
);
|
|
1573
237
|
}
|
|
1574
238
|
const next = {
|
|
1575
239
|
...this.state,
|
|
1576
240
|
...partial,
|
|
1577
|
-
|
|
1578
|
-
thinkTimeoutMs: sanitizedThinkTimeout
|
|
241
|
+
aiTimeoutMs: sanitizedTimeout
|
|
1579
242
|
};
|
|
1580
243
|
this.applyState(next);
|
|
1581
244
|
if (this.form) {
|
|
1582
|
-
const fields = [
|
|
1583
|
-
"enabled",
|
|
1584
|
-
"concurrency",
|
|
1585
|
-
"aiEnabled",
|
|
1586
|
-
"bingEnabled",
|
|
1587
|
-
"thinkEnabled",
|
|
1588
|
-
"thinkTimeoutMs",
|
|
1589
|
-
"baseUrl",
|
|
1590
|
-
"model"
|
|
1591
|
-
];
|
|
245
|
+
const fields = ["enabled", "aiTimeoutMs", "baseUrl", "model"];
|
|
1592
246
|
for (const field of fields) {
|
|
1593
247
|
if (partial[field] !== void 0) {
|
|
1594
248
|
this.pendingFields.add(field);
|
|
@@ -1736,10 +390,6 @@ var SETTINGS_CSS = String.raw`
|
|
|
1736
390
|
width: 120px;
|
|
1737
391
|
}
|
|
1738
392
|
|
|
1739
|
-
.dsh-tidy-field-count {
|
|
1740
|
-
width: 88px;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
393
|
.dsh-tidy-test-result {
|
|
1744
394
|
margin-left: 10px;
|
|
1745
395
|
font: var(--dsw-font-xxs-12);
|
|
@@ -1753,13 +403,6 @@ var SETTINGS_CSS = String.raw`
|
|
|
1753
403
|
color: var(--dsw-alias-state-error-primary);
|
|
1754
404
|
}
|
|
1755
405
|
|
|
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
406
|
`;
|
|
1764
407
|
|
|
1765
408
|
// src/client/reading-settings-page.tsx
|
|
@@ -1899,91 +542,73 @@ function claimReadingSettingsPage(ctx, label, locale) {
|
|
|
1899
542
|
var NS = "settings.chatTranslate";
|
|
1900
543
|
var zh = {
|
|
1901
544
|
pageNav: "\u9605\u8BFB\u4F53\u9A8C",
|
|
1902
|
-
title: "\
|
|
545
|
+
title: "\u6B63\u6587\u7FFB\u8BD1",
|
|
1903
546
|
masterTitle: "\u7FFB\u8BD1\u603B\u5F00\u5173",
|
|
1904
547
|
enableTranslation: "\u542F\u7528\u7FFB\u8BD1",
|
|
1905
|
-
masterDesc: "\u81EA\u52A8\
|
|
548
|
+
masterDesc: "\u81EA\u52A8\u628A\u52A9\u624B\u56DE\u7B54\u6B63\u6587\u7FFB\u6210\u4E2D\u6587\uFF08\u5982 {example}\uFF09\uFF1A\u6298\u53E0\u5757\u91CC\u7684\u8FC7\u7A0B\u6B63\u6587\u4E0E\u6700\u7EC8\u6C47\u603B\u4E00\u89C6\u540C\u4EC1\uFF0C\u6574\u6BB5\u4E2D\u6587\u7684\u5757\u4E5F\u7167\u9001\uFF0C\u7531\u6A21\u578B\u4E00\u5E76\u6539\u5199\u6210\u81EA\u7136\u4E2D\u6587\u3001\u987A\u6389\u673A\u5668\u8154\u3002Think \u5361\u4E0E\u5DE5\u5177\u8C03\u7528\u884C\u4E0D\u7FFB\uFF0C\u4EE3\u7801\u5757\u539F\u6837\u4FDD\u7559\u3002\u7FFB\u8BD1\u8FC7\u7684\u5757\u5DE6\u7F18\u6709\u4E00\u6761\u7AD6\u7EBF\uFF1A\u663E\u793A\u8BD1\u6587\u65F6\u662F\u7A0D\u7C97\u7684\u84DD\u7EBF\uFF0C\u70B9\u56DE\u539F\u6587\u53D8\u7EC6\u7070\u7EBF\uFF08\u63D0\u793A\u8FD9\u91CC\u5907\u7740\u8BD1\u6587\uFF0C\u518D\u70B9\u5373\u5207\u56DE\uFF09\uFF0C\u6CA1\u8BD1\u6210\u7684\u6CA1\u6709\u7EBF\u2014\u2014\u7EBF\u7684\u989C\u8272\u4E0E\u7C97\u7EC6\u4E00\u773C\u5206\u6E05\u300C\u6709\u6CA1\u6709\u8BD1\u300D\u548C\u300C\u6B63\u5728\u8BFB\u54EA\u4E00\u7248\u300D\u3002\u70B9\u51FB\u5DF2\u7FFB\u8BD1\u7684\u5757\u53EF\u5728\u8BD1\u6587\u4E0E\u539F\u6587\u95F4\u5207\u6362\uFF0C\u6A21\u578B\u8BA4\u4E3A\u539F\u6837\u6700\u597D\u3001\u8FD4\u56DE\u4E00\u6A21\u4E00\u6837\u7684\u6587\u5B57\u65F6\u540C\u6837\u7B97\u7FFB\u8BD1\u8FC7\u3001\u540C\u6837\u53EF\u5207\u3002\u8BD1\u6587\u6309\u539F markdown \u683C\u5F0F\u91CD\u65B0\u6E32\u67D3\uFF0C\u8868\u683C\u3001\u5217\u8868\u3001\u52A0\u7C97\u4E0E\u884C\u5185\u4EE3\u7801\u4FDD\u6301\u7ED3\u6784\u3002\u56DE\u7B54\u8FD8\u5728\u6D41\u5F0F\u8F93\u51FA\u65F6\u4E0D\u7FFB\uFF0C\u843D\u5B9A\u540E\u6309\u9605\u8BFB\u987A\u5E8F\u9010\u6BB5\u51FA\u4E2D\u6587\u3002\u53EA\u4F5C\u7528\u4E8E\u5F53\u524D\u67E5\u770B\u7684\u4F1A\u8BDD\uFF0C\u8BD1\u6587\u4E0D\u5199\u56DE\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u3002\u8F93\u5165\u6846\u4E0B\u65B9\u90A3\u4E00\u884C\u6709\u540C\u6B3E\u5F00\u5173\uFF0C\u4E24\u5904\u72B6\u6001\u540C\u6B65\u3002",
|
|
1906
549
|
aiTitle: "AI \u7FFB\u8BD1\uFF08OpenAI \u517C\u5BB9\u534F\u8BAE\uFF09",
|
|
1907
|
-
|
|
550
|
+
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
551
|
badgeConfigured: "\u5DF2\u914D\u7F6E",
|
|
1909
552
|
badgeUnconfigured: "\u672A\u914D\u7F6E",
|
|
1910
|
-
aiFallbackNote: "\u672A\u914D\u7F6E\u65F6 AI \u901A\u9053\u81EA\u52A8\u8DF3\u8FC7\uFF0C\u7531 Bing \u515C\u5E95\u3002",
|
|
1911
553
|
apiKeyTitle: "API Key",
|
|
1912
554
|
apiKeyDesc: "\u4FDD\u5B58\u81F3 {home} \u7684 {ref}\uFF0C\u4FDD\u5B58\u540E\u7ACB\u5373\u751F\u6548\uFF1B\u7559\u7A7A\u4FDD\u5B58 = \u6E05\u9664",
|
|
1913
555
|
apiKeyPlaceholderConfigured: "\u5DF2\u914D\u7F6E\uFF08\u5982\u9700\u66F4\u6362\u8BF7\u76F4\u63A5\u8F93\u5165\uFF09",
|
|
1914
556
|
save: "\u4FDD\u5B58",
|
|
1915
557
|
saving: "\u4FDD\u5B58\u4E2D\u2026",
|
|
1916
558
|
keySaved: "\u5DF2\u4FDD\u5B58\u5230 {home}\uFF0C\u7ACB\u5373\u751F\u6548",
|
|
1917
|
-
keyCleared: "\u5DF2\u6E05\u9664 API Key\
|
|
559
|
+
keyCleared: "\u5DF2\u6E05\u9664 API Key\uFF08\u672A\u914D\u7F6E\u65F6\u6B63\u6587\u4FDD\u6301\u539F\u6587\uFF0C\u4E0D\u4F1A\u7FFB\u8BD1\uFF09",
|
|
1918
560
|
saveFailed: "\u4FDD\u5B58\u5931\u8D25\uFF1A{error}",
|
|
1919
561
|
unknownError: "\u672A\u77E5\u9519\u8BEF",
|
|
1920
562
|
baseUrlTitle: "Base URL",
|
|
1921
563
|
baseUrlDesc: "\u4EFB\u610F OpenAI \u517C\u5BB9\u670D\u52A1\u7AEF\u70B9\uFF0C\u5982 {url1}\u3001{url2}",
|
|
1922
564
|
modelTitle: "\u6A21\u578B",
|
|
1923
565
|
modelDesc: "\u5982 {model1}\u3001{model2}\uFF1B\u7559\u7A7A\u89C6\u4E3A\u672A\u914D\u7F6E",
|
|
566
|
+
timeoutTitle: "\u5355\u6B21\u8BF7\u6C42\u8D85\u65F6",
|
|
567
|
+
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
568
|
test: "\u6D4B\u8BD5 AI \u901A\u9053",
|
|
1925
569
|
testing: "\u6D4B\u8BD5\u4E2D\u2026",
|
|
1926
570
|
testOk: "\u8FDE\u63A5\u6B63\u5E38\uFF0C\u5EF6\u8FDF {latency}ms",
|
|
1927
571
|
testFail: "\u5931\u8D25\uFF1A{error}",
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
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"
|
|
572
|
+
behaviorTitle: "\u7FFB\u8BD1\u8303\u56F4",
|
|
573
|
+
behaviorRule: "\u52A9\u624B\u56DE\u7B54\u6B63\u6587\u4E00\u5F8B\u7FFB\u8BD1\uFF0C\u5305\u62EC\u6700\u7EC8\u6C47\u603B\u4E0E\u6574\u6BB5\u4E2D\u6587\u7684\u5757\uFF1A\u4E2D\u6587\u5757\u540C\u6837\u4EA4\u7ED9\u6A21\u578B\u6539\u5199\u6210\u81EA\u7136\u4E2D\u6587\uFF0C\u610F\u4E49\u3001\u672F\u8BED\u4E0E\u6570\u5B57\u4E0D\u53D8\u3002\u601D\u8003\u94FE\u6B63\u6587\u3001\u5DE5\u5177\u8C03\u7528\u6807\u9898\u4E0E\u6298\u53E0\u6458\u8981\u4E0D\u7FFB\u8BD1\uFF0C\u4EE3\u7801\u5757\u53CA\u5176\u5185\u90E8\u539F\u6837\u4FDD\u7559\u3002",
|
|
574
|
+
dockLabel: "\u8BD1",
|
|
575
|
+
dockOn: "\u6B63\u6587\u7FFB\u8BD1\u5DF2\u5F00\u542F\uFF0C\u70B9\u51FB\u5173\u95ED",
|
|
576
|
+
dockOff: "\u6B63\u6587\u7FFB\u8BD1\u5DF2\u5173\u95ED\uFF0C\u70B9\u51FB\u5F00\u542F"
|
|
1943
577
|
};
|
|
1944
578
|
var en = {
|
|
1945
579
|
pageNav: "Reading",
|
|
1946
|
-
title: "
|
|
580
|
+
title: "Reply translate",
|
|
1947
581
|
masterTitle: "Translation master switch",
|
|
1948
582
|
enableTranslation: "Enable translation",
|
|
1949
|
-
masterDesc: "Translates
|
|
583
|
+
masterDesc: "Translates assistant reply prose (e.g. {example}) into Chinese: text inside disclosure blocks and the final summary are treated alike, and blocks written entirely in Chinese go through the model too, which rewrites them into natural Chinese and smooths out machine-flavored phrasing. Think cards and tool-call rows are never translated, and code blocks stay untouched. A translated block carries a left edge line: a slightly thicker blue one while its translation shows, a thin grey one while you read the original (a translation is waiting one click away), and none at all when nothing translated \u2014 colour and weight together tell at a glance both what exists and what you are reading. Click any translated block to switch between translation and original; even when the model decides the original wording is already best and returns it unchanged, it still counts as translated and stays switchable. Translations are re-rendered as Markdown, so tables, lists, bold runs and inline code keep their structure. A reply that is still streaming is left alone until it settles, then prose turns to Chinese paragraph by paragraph in reading order. Applies to the session you are viewing only; translations never enter the conversation context. The same switch sits on the row below the composer, sharing its state.",
|
|
1950
584
|
aiTitle: "AI translation (OpenAI-compatible)",
|
|
1951
|
-
|
|
585
|
+
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
586
|
badgeConfigured: "Configured",
|
|
1953
587
|
badgeUnconfigured: "Not configured",
|
|
1954
|
-
aiFallbackNote: "When unconfigured the AI channel is skipped and Bing takes over.",
|
|
1955
588
|
apiKeyTitle: "API Key",
|
|
1956
589
|
apiKeyDesc: "Saved as {ref} in {home}; takes effect immediately. Saving an empty value clears it.",
|
|
1957
590
|
apiKeyPlaceholderConfigured: "Configured (type a new value to replace it)",
|
|
1958
591
|
save: "Save",
|
|
1959
592
|
saving: "Saving\u2026",
|
|
1960
593
|
keySaved: "Saved to {home}; takes effect immediately",
|
|
1961
|
-
keyCleared: "API Key cleared (
|
|
594
|
+
keyCleared: "API Key cleared (with nothing configured, replies stay in their original language)",
|
|
1962
595
|
saveFailed: "Save failed: {error}",
|
|
1963
596
|
unknownError: "Unknown error",
|
|
1964
597
|
baseUrlTitle: "Base URL",
|
|
1965
598
|
baseUrlDesc: "Any OpenAI-compatible endpoint, e.g. {url1} or {url2}",
|
|
1966
599
|
modelTitle: "Model",
|
|
1967
600
|
modelDesc: "e.g. {model1} or {model2}; empty counts as unconfigured",
|
|
601
|
+
timeoutTitle: "Per-request timeout",
|
|
602
|
+
timeoutDesc: "In milliseconds, range 500-900000. One long reply can take minutes; default 600000.",
|
|
1968
603
|
test: "Test the AI channel",
|
|
1969
604
|
testing: "Testing\u2026",
|
|
1970
605
|
testOk: "Connected, {latency}ms latency",
|
|
1971
606
|
testFail: "Failed: {error}",
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
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)."
|
|
607
|
+
behaviorTitle: "What gets translated",
|
|
608
|
+
behaviorRule: "All assistant reply prose, the final summary and Chinese-only blocks included: Chinese blocks are also handed to the model, which rewrites them into natural Chinese while keeping meaning, terms and numbers unchanged. Think-chain text, tool-call titles and folded summaries are never translated; code blocks and their contents stay untouched.",
|
|
609
|
+
dockLabel: "Translate",
|
|
610
|
+
dockOn: "Translation is on; click to turn it off",
|
|
611
|
+
dockOff: "Translation is off; click to turn it on"
|
|
1987
612
|
};
|
|
1988
613
|
|
|
1989
614
|
// src/client/settings/ui.tsx
|
|
@@ -2056,201 +681,131 @@ function TidySettingsPanel(props) {
|
|
|
2056
681
|
}
|
|
2057
682
|
)
|
|
2058
683
|
] }),
|
|
2059
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: withSamples(t("masterDesc"), { example: "
|
|
684
|
+
/* @__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
685
|
] }),
|
|
2061
686
|
state.enabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
2062
687
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
|
|
2063
688
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-title", children: [
|
|
2064
689
|
/* @__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
|
-
)
|
|
690
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { tone: state.aiConfigured ? "success" : "warning", children: t(state.aiConfigured ? "badgeConfigured" : "badgeUnconfigured") })
|
|
2073
691
|
] }),
|
|
2074
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
2075
|
-
|
|
2076
|
-
" ",
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("apiKeyTitle") }),
|
|
2083
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("apiKeyDesc"), {
|
|
2084
|
-
home: CREDENTIALS_PATH,
|
|
2085
|
-
ref: TRANSLATE_API_KEY_REF
|
|
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
|
-
)
|
|
692
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: t("aiRule") }),
|
|
693
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
694
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
695
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("apiKeyTitle") }),
|
|
696
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("apiKeyDesc"), {
|
|
697
|
+
home: CREDENTIALS_PATH,
|
|
698
|
+
ref: TRANSLATE_API_KEY_REF
|
|
699
|
+
}) })
|
|
2132
700
|
] }),
|
|
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
|
-
] }),
|
|
701
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-input-row", children: [
|
|
2141
702
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2142
703
|
import_dsh_client_ui_primitives.Input,
|
|
2143
704
|
{
|
|
2144
|
-
type: "
|
|
705
|
+
type: "password",
|
|
2145
706
|
className: "dsh-tidy-field-wide",
|
|
2146
|
-
placeholder: "
|
|
2147
|
-
value:
|
|
2148
|
-
onChange: (e) =>
|
|
2149
|
-
"aria-label": t("
|
|
707
|
+
placeholder: state.aiConfigured ? t("apiKeyPlaceholderConfigured") : "sk-...",
|
|
708
|
+
value: apiKeyInput,
|
|
709
|
+
onChange: (e) => setApiKeyInput(e.target.value),
|
|
710
|
+
"aria-label": t("apiKeyTitle")
|
|
2150
711
|
}
|
|
2151
|
-
)
|
|
2152
|
-
] }),
|
|
2153
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
712
|
+
),
|
|
2154
713
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2155
714
|
import_dsh_client_ui_primitives.Button,
|
|
2156
715
|
{
|
|
2157
|
-
variant: "
|
|
716
|
+
variant: "primary",
|
|
2158
717
|
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")
|
|
718
|
+
disabled: savingKey,
|
|
719
|
+
onClick: handleSaveKey,
|
|
720
|
+
children: savingKey ? t("saving") : t("save")
|
|
2202
721
|
}
|
|
2203
722
|
)
|
|
2204
723
|
] })
|
|
2205
|
-
] })
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
724
|
+
] }),
|
|
725
|
+
keyMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `dsh-tidy-test-result ${keyMsg.ok ? "ok" : "fail"}`, children: keyMsg.text }),
|
|
726
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
727
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
728
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("baseUrlTitle") }),
|
|
729
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("baseUrlDesc"), {
|
|
730
|
+
url1: "https://api.openai.com/v1",
|
|
731
|
+
url2: "https://api.deepseek.com/v1"
|
|
732
|
+
}) })
|
|
733
|
+
] }),
|
|
734
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
735
|
+
import_dsh_client_ui_primitives.Input,
|
|
736
|
+
{
|
|
737
|
+
type: "text",
|
|
738
|
+
className: "dsh-tidy-field-wide",
|
|
739
|
+
placeholder: "https://api.openai.com/v1",
|
|
740
|
+
value: state.baseUrl,
|
|
741
|
+
onChange: (e) => settingsStore.update({ baseUrl: e.target.value }),
|
|
742
|
+
"aria-label": t("baseUrlTitle")
|
|
743
|
+
}
|
|
744
|
+
)
|
|
745
|
+
] }),
|
|
746
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
747
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
748
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("modelTitle") }),
|
|
749
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("modelDesc"), {
|
|
750
|
+
model1: "gpt-4o-mini",
|
|
751
|
+
model2: "deepseek-chat"
|
|
752
|
+
}) })
|
|
753
|
+
] }),
|
|
754
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
755
|
+
import_dsh_client_ui_primitives.Input,
|
|
756
|
+
{
|
|
757
|
+
type: "text",
|
|
758
|
+
className: "dsh-tidy-field-wide",
|
|
759
|
+
placeholder: "gpt-4o-mini",
|
|
760
|
+
value: state.model,
|
|
761
|
+
onChange: (e) => settingsStore.update({ model: e.target.value }),
|
|
762
|
+
"aria-label": t("modelTitle")
|
|
763
|
+
}
|
|
764
|
+
)
|
|
765
|
+
] }),
|
|
766
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
767
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
|
|
768
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("timeoutTitle") }),
|
|
769
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: t("timeoutDesc") })
|
|
770
|
+
] }),
|
|
2210
771
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2211
|
-
import_dsh_client_ui_primitives.
|
|
772
|
+
import_dsh_client_ui_primitives.Input,
|
|
2212
773
|
{
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
774
|
+
type: "number",
|
|
775
|
+
className: "dsh-tidy-field-timeout",
|
|
776
|
+
min: 500,
|
|
777
|
+
max: 9e5,
|
|
778
|
+
step: 1e3,
|
|
779
|
+
value: state.aiTimeoutMs,
|
|
780
|
+
onChange: (e) => {
|
|
781
|
+
const val = parseInt(e.target.value, 10);
|
|
782
|
+
if (!Number.isFinite(val)) return;
|
|
783
|
+
settingsStore.update({
|
|
784
|
+
aiTimeoutMs: Math.min(Math.max(val, 500), 9e5)
|
|
785
|
+
});
|
|
786
|
+
},
|
|
787
|
+
"aria-label": t("timeoutTitle")
|
|
2216
788
|
}
|
|
2217
789
|
)
|
|
2218
790
|
] }),
|
|
2219
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
791
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
|
|
792
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
793
|
+
import_dsh_client_ui_primitives.Button,
|
|
794
|
+
{
|
|
795
|
+
variant: "outline",
|
|
796
|
+
size: "sm",
|
|
797
|
+
disabled: testing?.running,
|
|
798
|
+
onClick: () => runTest("openai"),
|
|
799
|
+
children: testing?.running ? t("testing") : t("test")
|
|
800
|
+
}
|
|
801
|
+
),
|
|
802
|
+
testing?.channel === "openai" && !testing.running && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-tidy-test-result ${testing.ok ? "ok" : "fail"}`, children: testing.message })
|
|
803
|
+
] })
|
|
2220
804
|
] }),
|
|
2221
805
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
|
|
2222
806
|
/* @__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
|
-
] }) })
|
|
807
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: t("behaviorRule") })
|
|
808
|
+
] })
|
|
2254
809
|
] })
|
|
2255
810
|
] });
|
|
2256
811
|
}
|
|
@@ -2308,12 +863,791 @@ function setupSettingsUi(ctx) {
|
|
|
2308
863
|
}
|
|
2309
864
|
}
|
|
2310
865
|
|
|
866
|
+
// src/client/composer/dock-toggle.tsx
|
|
867
|
+
var import_react3 = require("react");
|
|
868
|
+
var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
869
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
870
|
+
function ComposerTranslateToggle(props) {
|
|
871
|
+
const { t } = props;
|
|
872
|
+
const [state, setState] = (0, import_react3.useState)(() => settingsStore.getState());
|
|
873
|
+
(0, import_react3.useEffect)(() => settingsStore.subscribe(() => setState(settingsStore.getState())), []);
|
|
874
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.Tooltip, { label: () => state.enabled ? t("dockOn") : t("dockOff"), side: "top", align: "end", portal: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
875
|
+
import_dsh_client_ui_primitives2.Pill,
|
|
876
|
+
{
|
|
877
|
+
active: state.enabled,
|
|
878
|
+
onClick: () => void settingsStore.update({ enabled: !settingsStore.getState().enabled }),
|
|
879
|
+
children: t("dockLabel")
|
|
880
|
+
}
|
|
881
|
+
) });
|
|
882
|
+
}
|
|
883
|
+
function setupComposerToggle(ctx) {
|
|
884
|
+
if (typeof window === "undefined") return () => {
|
|
885
|
+
};
|
|
886
|
+
const slots = ctx?.slots;
|
|
887
|
+
if (!slots || typeof slots.inject !== "function") return () => {
|
|
888
|
+
};
|
|
889
|
+
return slots.inject(
|
|
890
|
+
"conversation.composer.dock",
|
|
891
|
+
() => slots.register(
|
|
892
|
+
{
|
|
893
|
+
name: "conversation.composer.dock",
|
|
894
|
+
id: "dsh-chat-translate-toggle",
|
|
895
|
+
// -2 排在 dsh-a6api 胶囊(-1)的左侧,整行最左;官方统计胶囊是 0。
|
|
896
|
+
order: -2,
|
|
897
|
+
locale: NS
|
|
898
|
+
},
|
|
899
|
+
ComposerTranslateToggle
|
|
900
|
+
)
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// src/client/chat/assistant-step.tsx
|
|
905
|
+
var React = __toESM(require("react"), 1);
|
|
906
|
+
var import_react4 = require("react");
|
|
907
|
+
var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
908
|
+
|
|
909
|
+
// src/server/pipeline/blocks.ts
|
|
910
|
+
var REPLY_MAX_INPUT_TOKENS = 4096;
|
|
911
|
+
var CJK_CHAR = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/;
|
|
912
|
+
function estimateTokens(text) {
|
|
913
|
+
let cjk = 0;
|
|
914
|
+
for (const char of text) {
|
|
915
|
+
if (CJK_CHAR.test(char)) cjk += 1;
|
|
916
|
+
}
|
|
917
|
+
return cjk + Math.ceil((text.length - cjk) / 3);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// src/client/chat/translate-store.ts
|
|
921
|
+
var MAX_TRANSLATED_ROWS = 200;
|
|
922
|
+
var MAX_ROW_ATTEMPTS = 3;
|
|
923
|
+
var REQUEST_TOKEN_BUDGET = REPLY_MAX_INPUT_TOKENS;
|
|
924
|
+
function chunkTexts(texts) {
|
|
925
|
+
const batches = [];
|
|
926
|
+
let current = [];
|
|
927
|
+
let tokens = 0;
|
|
928
|
+
for (const text of texts) {
|
|
929
|
+
const cost = Math.max(estimateTokens(text), 1);
|
|
930
|
+
if (current.length > 0 && tokens + cost > REQUEST_TOKEN_BUDGET) {
|
|
931
|
+
batches.push(current);
|
|
932
|
+
current = [];
|
|
933
|
+
tokens = 0;
|
|
934
|
+
}
|
|
935
|
+
current.push(text);
|
|
936
|
+
tokens += cost;
|
|
937
|
+
}
|
|
938
|
+
if (current.length > 0) batches.push(current);
|
|
939
|
+
return batches;
|
|
940
|
+
}
|
|
941
|
+
var ChatTranslateStore = class {
|
|
942
|
+
rows = /* @__PURE__ */ new Map();
|
|
943
|
+
listeners = /* @__PURE__ */ new Set();
|
|
944
|
+
version = 0;
|
|
945
|
+
fetch;
|
|
946
|
+
constructor(fetcher) {
|
|
947
|
+
this.fetch = fetcher;
|
|
948
|
+
}
|
|
949
|
+
/** 单调版本号:任何状态变化(含单批落定)都前进一位,供渲染层判等重渲。 */
|
|
950
|
+
getVersion() {
|
|
951
|
+
return this.version;
|
|
952
|
+
}
|
|
953
|
+
subscribe = (listener) => {
|
|
954
|
+
this.listeners.add(listener);
|
|
955
|
+
return () => {
|
|
956
|
+
this.listeners.delete(listener);
|
|
957
|
+
};
|
|
958
|
+
};
|
|
959
|
+
/** 读一行状态,并把该行刷成最近使用。 */
|
|
960
|
+
getState(rowKey) {
|
|
961
|
+
const state = this.rows.get(rowKey);
|
|
962
|
+
if (state === void 0) return void 0;
|
|
963
|
+
this.rows.delete(rowKey);
|
|
964
|
+
this.rows.set(rowKey, state);
|
|
965
|
+
return state;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* 确保该行按当前文本被翻译。
|
|
969
|
+
* @returns 是否发出了新的请求(同代 done/pending 与换代前的登记之外都是 false)。
|
|
970
|
+
*/
|
|
971
|
+
ensure(rowKey, texts) {
|
|
972
|
+
const current = this.rows.get(rowKey);
|
|
973
|
+
let attempts = 1;
|
|
974
|
+
if (current !== void 0 && sameTexts(current.texts, texts)) {
|
|
975
|
+
if (current.status !== "partial") return false;
|
|
976
|
+
if (current.attempts >= MAX_ROW_ATTEMPTS) return false;
|
|
977
|
+
attempts = current.attempts + 1;
|
|
978
|
+
}
|
|
979
|
+
const snapshot = [...texts];
|
|
980
|
+
const next = {
|
|
981
|
+
status: "pending",
|
|
982
|
+
texts: snapshot,
|
|
983
|
+
outcomes: snapshot.map(() => null),
|
|
984
|
+
attempts
|
|
985
|
+
};
|
|
986
|
+
this.rows.delete(rowKey);
|
|
987
|
+
this.rows.set(rowKey, next);
|
|
988
|
+
this.prune();
|
|
989
|
+
this.bump();
|
|
990
|
+
if (snapshot.length > 0) void this.run(rowKey, snapshot);
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
async run(rowKey, texts) {
|
|
994
|
+
const outcomes = texts.map(() => null);
|
|
995
|
+
let offset = 0;
|
|
996
|
+
for (const batch of chunkTexts(texts)) {
|
|
997
|
+
let results = [];
|
|
998
|
+
try {
|
|
999
|
+
results = await this.fetch(batch);
|
|
1000
|
+
} catch {
|
|
1001
|
+
results = [];
|
|
1002
|
+
}
|
|
1003
|
+
for (let i = 0; i < batch.length; i++) {
|
|
1004
|
+
const result = results[i];
|
|
1005
|
+
if (result !== void 0 && result.ok && result.translated && result.translated.trim()) {
|
|
1006
|
+
outcomes[offset + i] = { translated: result.translated, ok: true };
|
|
1007
|
+
} else {
|
|
1008
|
+
outcomes[offset + i] = { translated: batch[i] ?? texts[offset + i] ?? "", ok: false };
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
offset += batch.length;
|
|
1012
|
+
const live = this.rows.get(rowKey);
|
|
1013
|
+
if (live === void 0 || live.status !== "pending" || !sameTexts(live.texts, texts)) return;
|
|
1014
|
+
this.rows.set(rowKey, {
|
|
1015
|
+
status: "pending",
|
|
1016
|
+
texts: live.texts,
|
|
1017
|
+
outcomes: [...outcomes],
|
|
1018
|
+
attempts: live.attempts
|
|
1019
|
+
});
|
|
1020
|
+
this.bump();
|
|
1021
|
+
}
|
|
1022
|
+
const state = this.rows.get(rowKey);
|
|
1023
|
+
if (state?.status === "pending" && sameTexts(state.texts, texts)) {
|
|
1024
|
+
const anyFailed = outcomes.some((outcome) => outcome === null || !outcome.ok);
|
|
1025
|
+
this.rows.set(rowKey, {
|
|
1026
|
+
status: anyFailed ? "partial" : "done",
|
|
1027
|
+
texts: state.texts,
|
|
1028
|
+
outcomes,
|
|
1029
|
+
attempts: state.attempts
|
|
1030
|
+
});
|
|
1031
|
+
this.bump();
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
prune() {
|
|
1035
|
+
while (this.rows.size > MAX_TRANSLATED_ROWS) {
|
|
1036
|
+
const oldest = this.rows.keys().next().value;
|
|
1037
|
+
if (oldest === void 0) break;
|
|
1038
|
+
this.rows.delete(oldest);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
bump() {
|
|
1042
|
+
this.version += 1;
|
|
1043
|
+
for (const listener of [...this.listeners]) {
|
|
1044
|
+
try {
|
|
1045
|
+
listener();
|
|
1046
|
+
} catch {
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
function sameTexts(a, b) {
|
|
1052
|
+
if (a === b) return true;
|
|
1053
|
+
if (a.length !== b.length) return false;
|
|
1054
|
+
for (let i = 0; i < a.length; i++) {
|
|
1055
|
+
if (a[i] !== b[i]) return false;
|
|
1056
|
+
}
|
|
1057
|
+
return true;
|
|
1058
|
+
}
|
|
1059
|
+
function createTranslateStore(fetcher) {
|
|
1060
|
+
return new ChatTranslateStore(fetcher);
|
|
1061
|
+
}
|
|
1062
|
+
var chatTranslate = createTranslateStore(requestTranslateReply);
|
|
1063
|
+
|
|
1064
|
+
// src/client/chat/row-plan.ts
|
|
1065
|
+
function planAssistantRow(input) {
|
|
1066
|
+
const { blocks, streaming, interrupted, groupPart, canTranslate, outcomes } = input;
|
|
1067
|
+
const hasVisible = streaming || interrupted || blocks.some((block) => block.kind !== "tool-call");
|
|
1068
|
+
if (!hasVisible) return { entries: null, texts: [] };
|
|
1069
|
+
const texts = [];
|
|
1070
|
+
if (groupPart !== "reasoning") {
|
|
1071
|
+
for (const block of blocks) {
|
|
1072
|
+
if (block.kind === "text") texts.push(block.text ?? "");
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
const entries = [];
|
|
1076
|
+
let textIndex = 0;
|
|
1077
|
+
let imageRun = null;
|
|
1078
|
+
const flushImages = () => {
|
|
1079
|
+
if (imageRun !== null) {
|
|
1080
|
+
entries.push({ type: "images", key: entries.length, attachments: imageRun });
|
|
1081
|
+
imageRun = null;
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
1085
|
+
const block = blocks[i];
|
|
1086
|
+
if (block === void 0) continue;
|
|
1087
|
+
if (groupPart === "reasoning" && block.kind !== "reasoning") continue;
|
|
1088
|
+
if (groupPart === "response" && block.kind === "reasoning") continue;
|
|
1089
|
+
switch (block.kind) {
|
|
1090
|
+
case "text": {
|
|
1091
|
+
flushImages();
|
|
1092
|
+
const index = textIndex;
|
|
1093
|
+
textIndex += 1;
|
|
1094
|
+
const outcome = canTranslate && !streaming && outcomes !== void 0 ? outcomes[index] : null;
|
|
1095
|
+
entries.push({
|
|
1096
|
+
type: "prose",
|
|
1097
|
+
key: entries.length,
|
|
1098
|
+
text: block.text ?? "",
|
|
1099
|
+
translated: outcome !== null && outcome !== void 0 && outcome.ok ? outcome.translated : null
|
|
1100
|
+
});
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
case "reasoning":
|
|
1104
|
+
flushImages();
|
|
1105
|
+
entries.push({ type: "reasoning", key: entries.length, blockIndex: i, text: block.text ?? "" });
|
|
1106
|
+
break;
|
|
1107
|
+
case "image":
|
|
1108
|
+
if (imageRun === null) imageRun = [];
|
|
1109
|
+
imageRun.push(block.attachment);
|
|
1110
|
+
break;
|
|
1111
|
+
case "tool-call":
|
|
1112
|
+
flushImages();
|
|
1113
|
+
break;
|
|
1114
|
+
default:
|
|
1115
|
+
flushImages();
|
|
1116
|
+
entries.push({ type: "unknown", key: entries.length, block: block.block ?? null });
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
flushImages();
|
|
1120
|
+
const showStopped = interrupted && (groupPart === void 0 || groupPart === "response" || !blocks.some((block) => block.kind !== "reasoning" && block.kind !== "tool-call"));
|
|
1121
|
+
if (showStopped) entries.push({ type: "stopped", key: entries.length });
|
|
1122
|
+
return { entries, texts };
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// src/client/chat/click-guard.ts
|
|
1126
|
+
function isBareBlockClick(event, view) {
|
|
1127
|
+
const target = event.target;
|
|
1128
|
+
const interactive = typeof target?.closest === "function" ? target.closest('a,button,[role="button"]') : null;
|
|
1129
|
+
if (interactive !== null && interactive !== void 0 && interactive !== event.currentTarget) return false;
|
|
1130
|
+
const selection = typeof view?.getSelection === "function" ? view.getSelection() : null;
|
|
1131
|
+
if (selection !== null && selection !== void 0) {
|
|
1132
|
+
if (selection.isCollapsed === false && String(selection.toString?.() ?? "") !== "") return false;
|
|
1133
|
+
}
|
|
1134
|
+
return true;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// src/client/chat/styles.ts
|
|
1138
|
+
var STYLE_TAG_ID = "@lynn123411/dsh-chat-translate/chat-assistant.css";
|
|
1139
|
+
var ASSISTANT_CSS = [
|
|
1140
|
+
// ---- 行骨架(宿主 AssistantMarkdown.root/body 等价) ----
|
|
1141
|
+
".dsh-ct-root{display:flex;flex-direction:column;font-size:var(--dsh-content-font-size,14px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-primary)}",
|
|
1142
|
+
".dsh-ct-body{display:flex;flex-direction:column;gap:16px}",
|
|
1143
|
+
".dsh-ct-body>[data-turn-process-inline][hidden]{margin-bottom:-16px}",
|
|
1144
|
+
// 宽表格溢出内容列(宿主等价规则,类名 md-table-wide 由 MarkdownText 自己产出)
|
|
1145
|
+
".dsh-ct-body .md-table-wide{--dsh-table-spare:max(0px, calc((100cqw - var(--dsh-chat-content-width)) / 2));--dsh-table-lead:calc(var(--dsh-table-spare) + min(var(--dsh-chat-content-width), 100cqw) - 100%);box-sizing:border-box;width:calc(100% + var(--dsh-table-lead) + var(--dsh-table-spare));max-width:none;margin-left:calc(-1 * var(--dsh-table-lead));padding-left:var(--dsh-table-lead)}",
|
|
1146
|
+
".dsh-ct-body .md-table-wide>table{z-index:1;position:relative}",
|
|
1147
|
+
// ---- 已停止标记(宿主 .hWmORq_stopped 等价) ----
|
|
1148
|
+
".dsh-ct-stopped{border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary);align-self:flex-start;padding:0 6px;font-size:11px;line-height:18px}",
|
|
1149
|
+
// ---- 「已翻译」交互与标记:可点态常驻(含左缘内缩);区分走色相+粗细
|
|
1150
|
+
// (0.5px 虚线会被抗锯齿糊成实线)——蓝粗线=读译文,灰细线=读原文备着译文 ----
|
|
1151
|
+
".dsh-ct-prose-clickable{padding-left:12px;cursor:pointer}",
|
|
1152
|
+
".dsh-ct-prose-clickable:focus-visible{outline:1.5px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px;border-radius:var(--dsw-radius-sm)}",
|
|
1153
|
+
".dsh-ct-prose-translated{border-left:1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary) 65%, transparent)}",
|
|
1154
|
+
".dsh-ct-prose-original{border-left:0.5px solid var(--dsw-alias-border-l2)}",
|
|
1155
|
+
// ---- ReasoningRow(宿主 lcKema_* 等价,前缀换成 dsh-ct-think) ----
|
|
1156
|
+
".dsh-ct-think-root{flex-direction:column;display:flex}",
|
|
1157
|
+
".dsh-ct-think-root:not([data-expanded]){contain:size layout;height:calc(24px + var(--dsh-content-font-delta,0px))}",
|
|
1158
|
+
".dsh-ct-think-row{position:relative;overflow:hidden}",
|
|
1159
|
+
".dsh-ct-think-root[data-expanded] [data-open] [data-disclosure-row]{z-index:1;background:var(--dsw-alias-bg-base);position:sticky;top:0}",
|
|
1160
|
+
'.dsh-ct-think-root[data-state=running] .dsh-ct-think-row:after{content:"";inset-block:0;background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite dsh-ct-reasoning-sweep;position:absolute;left:0}',
|
|
1161
|
+
"@keyframes dsh-ct-reasoning-sweep{0%{left:-300px}90%,to{left:100%}}",
|
|
1162
|
+
"@media (prefers-reduced-motion:reduce){.dsh-ct-think-root[data-state=running] .dsh-ct-think-row:after{animation:none}}",
|
|
1163
|
+
".dsh-ct-think-leading{flex-shrink:0}",
|
|
1164
|
+
".dsh-ct-think-chevron{color:var(--dsw-alias-label-secondary)}",
|
|
1165
|
+
".dsh-ct-think-title{font-weight:400}",
|
|
1166
|
+
".dsh-ct-think-separator{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}",
|
|
1167
|
+
".dsh-ct-think-summary{min-width:0;color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));white-space:nowrap;flex:auto;overflow:hidden}",
|
|
1168
|
+
".dsh-ct-think-summary-text{text-overflow:ellipsis;display:block;overflow:hidden}",
|
|
1169
|
+
".dsh-ct-think-summary[data-streaming]{mask-image:linear-gradient(90deg,currentcolor calc(100% - 48px),transparent)}",
|
|
1170
|
+
".dsh-ct-think-summary[data-streaming] .dsh-ct-think-summary-text{text-overflow:clip;overflow:visible}",
|
|
1171
|
+
".dsh-ct-think-root:not([data-preview]) .dsh-ct-think-separator,.dsh-ct-think-root:not([data-preview]) .dsh-ct-think-summary{display:none}",
|
|
1172
|
+
".dsh-ct-think-body{padding:4px 0 4px calc(22px + var(--dsh-content-font-delta,0px));min-width:0}",
|
|
1173
|
+
// ---- 无障碍:屏幕阅读器专用文本 ----
|
|
1174
|
+
".dsh-ct-visually-hidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}"
|
|
1175
|
+
].join("");
|
|
1176
|
+
function proseClassNames(processed, showOriginal) {
|
|
1177
|
+
if (!processed) return void 0;
|
|
1178
|
+
return showOriginal ? "dsh-ct-prose-clickable dsh-ct-prose-original" : "dsh-ct-prose-clickable dsh-ct-prose-translated";
|
|
1179
|
+
}
|
|
1180
|
+
function ensureAssistantStyles() {
|
|
1181
|
+
if (typeof document === "undefined") return;
|
|
1182
|
+
const selector = `style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`;
|
|
1183
|
+
if (document.querySelector(selector) !== null) return;
|
|
1184
|
+
const tag = document.createElement("style");
|
|
1185
|
+
tag.dataset.plugin = "@lynn123411/dsh-chat-translate";
|
|
1186
|
+
tag.dataset.pluginCss = STYLE_TAG_ID;
|
|
1187
|
+
tag.textContent = ASSISTANT_CSS;
|
|
1188
|
+
document.head.appendChild(tag);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/client/chat/assistant-step.tsx
|
|
1192
|
+
var thinkIcon = () => React.createElement(import_dsh_client_ui_primitives3.IconThinkOutlineRegular, { size: 14 });
|
|
1193
|
+
function markdownLabels(t) {
|
|
1194
|
+
return {
|
|
1195
|
+
code: {
|
|
1196
|
+
copyLabel: t("copy"),
|
|
1197
|
+
copiedLabel: t("copied"),
|
|
1198
|
+
toolbarLabels: {
|
|
1199
|
+
codeLabel: t("codeBlock.title"),
|
|
1200
|
+
wrapLabel: t("codeBlock.wrap"),
|
|
1201
|
+
unwrapLabel: t("codeBlock.unwrap")
|
|
1202
|
+
}
|
|
1203
|
+
},
|
|
1204
|
+
footnotes: t("markdown.footnotes")
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
function isWindowsStylePath(value) {
|
|
1208
|
+
return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith("\\\\");
|
|
1209
|
+
}
|
|
1210
|
+
function isAbsoluteWorkspacePath(path) {
|
|
1211
|
+
return path.startsWith("/") || isWindowsStylePath(path);
|
|
1212
|
+
}
|
|
1213
|
+
function fileMediaUrl(base, path) {
|
|
1214
|
+
if (!/^https?:/u.test(base) && !base.startsWith("dsh-app://app/") || !isAbsoluteWorkspacePath(path)) return void 0;
|
|
1215
|
+
if (/^[/\\]{2}/u.test(path) || /[\u0000-\u001f\u007f]/u.test(path)) return void 0;
|
|
1216
|
+
return new URL(`api/file?path=${encodeURIComponent(path)}`, base).href;
|
|
1217
|
+
}
|
|
1218
|
+
function localPathMediaUrl(base, value) {
|
|
1219
|
+
let path;
|
|
1220
|
+
try {
|
|
1221
|
+
path = decodeURIComponent(value.split(/[?#]/u)[0] ?? "");
|
|
1222
|
+
} catch {
|
|
1223
|
+
return void 0;
|
|
1224
|
+
}
|
|
1225
|
+
return fileMediaUrl(base, path);
|
|
1226
|
+
}
|
|
1227
|
+
function firstLine(text) {
|
|
1228
|
+
const newline = text.indexOf("\n");
|
|
1229
|
+
return newline === -1 ? text : text.slice(0, newline);
|
|
1230
|
+
}
|
|
1231
|
+
function latestCompletedParagraphFirstLine(text) {
|
|
1232
|
+
let summary = "";
|
|
1233
|
+
let paragraphStart = 0;
|
|
1234
|
+
const separator = /\r?\n(?:[\t ]*\r?\n)+/g;
|
|
1235
|
+
for (; ; ) {
|
|
1236
|
+
const nextParagraph = separator.exec(text);
|
|
1237
|
+
const paragraphEnd = nextParagraph === null ? text.length : nextParagraph.index + nextParagraph[0].indexOf("\n");
|
|
1238
|
+
const newline = text.indexOf("\n", paragraphStart);
|
|
1239
|
+
if (newline !== -1 && newline <= paragraphEnd) {
|
|
1240
|
+
const candidate = text.slice(paragraphStart, newline).trim();
|
|
1241
|
+
if (candidate !== "") summary = candidate;
|
|
1242
|
+
}
|
|
1243
|
+
if (nextParagraph === null) return summary;
|
|
1244
|
+
paragraphStart = nextParagraph.index + nextParagraph[0].length;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
function useSearchableHidden(hidden, reveal) {
|
|
1248
|
+
const ref = React.useRef(null);
|
|
1249
|
+
React.useLayoutEffect(() => {
|
|
1250
|
+
const element = ref.current;
|
|
1251
|
+
if (element === null) return;
|
|
1252
|
+
if (hidden && element.contains(element.ownerDocument.activeElement)) {
|
|
1253
|
+
reveal();
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
if (hidden) element.setAttribute("hidden", "until-found");
|
|
1257
|
+
else element.removeAttribute("hidden");
|
|
1258
|
+
}, [hidden, reveal]);
|
|
1259
|
+
React.useEffect(() => {
|
|
1260
|
+
const element = ref.current;
|
|
1261
|
+
if (element === null) return;
|
|
1262
|
+
element.addEventListener("beforematch", reveal);
|
|
1263
|
+
return () => {
|
|
1264
|
+
element.removeEventListener("beforematch", reveal);
|
|
1265
|
+
};
|
|
1266
|
+
}, [reveal]);
|
|
1267
|
+
return ref;
|
|
1268
|
+
}
|
|
1269
|
+
var subscribeSettings = (listener) => settingsStore.subscribe(listener);
|
|
1270
|
+
var subscribeTranslate = (listener) => chatTranslate.subscribe(listener);
|
|
1271
|
+
function useSettledInView(ref, armed) {
|
|
1272
|
+
const noObserver = typeof IntersectionObserver === "undefined";
|
|
1273
|
+
const [inView, setInView] = (0, import_react4.useState)(noObserver);
|
|
1274
|
+
(0, import_react4.useEffect)(() => {
|
|
1275
|
+
if (noObserver || !armed) return;
|
|
1276
|
+
const element = ref.current;
|
|
1277
|
+
if (element === null) return;
|
|
1278
|
+
const observer = new IntersectionObserver(
|
|
1279
|
+
(entries) => {
|
|
1280
|
+
setInView(entries.some((entry) => entry.isIntersecting));
|
|
1281
|
+
},
|
|
1282
|
+
{ rootMargin: "150px" }
|
|
1283
|
+
);
|
|
1284
|
+
observer.observe(element);
|
|
1285
|
+
return () => {
|
|
1286
|
+
observer.disconnect();
|
|
1287
|
+
};
|
|
1288
|
+
}, [armed, noObserver, ref]);
|
|
1289
|
+
return inView;
|
|
1290
|
+
}
|
|
1291
|
+
var ReasoningRow = (0, import_react4.memo)(function ReasoningRow2({
|
|
1292
|
+
text,
|
|
1293
|
+
running,
|
|
1294
|
+
usePresentation,
|
|
1295
|
+
useDisclosure,
|
|
1296
|
+
t
|
|
1297
|
+
}) {
|
|
1298
|
+
const { expanded, toggle } = useDisclosure();
|
|
1299
|
+
const labels = (0, import_react4.useMemo)(() => markdownLabels(t), [t]);
|
|
1300
|
+
const summaryText = running ? latestCompletedParagraphFirstLine(text) : firstLine(text);
|
|
1301
|
+
const summary = (0, import_react4.useMemo)(() => summaryText.replaceAll("**", ""), [summaryText]);
|
|
1302
|
+
const preview = usePresentation(
|
|
1303
|
+
(policy2) => !expanded && summary !== "" && (running || policy2.settledReasoningPreview)
|
|
1304
|
+
);
|
|
1305
|
+
const collapsedContent = (0, import_react4.useMemo)(
|
|
1306
|
+
() => React.createElement(
|
|
1307
|
+
React.Fragment,
|
|
1308
|
+
null,
|
|
1309
|
+
React.createElement("span", { className: "dsh-ct-think-separator", "aria-hidden": true }),
|
|
1310
|
+
React.createElement(
|
|
1311
|
+
"span",
|
|
1312
|
+
{ className: "dsh-ct-think-summary", "data-streaming": running || void 0 },
|
|
1313
|
+
React.createElement("span", { className: "dsh-ct-think-summary-text" }, summary)
|
|
1314
|
+
)
|
|
1315
|
+
),
|
|
1316
|
+
[running, summary]
|
|
1317
|
+
);
|
|
1318
|
+
const content = (0, import_react4.useMemo)(
|
|
1319
|
+
() => expanded ? React.createElement(
|
|
1320
|
+
"div",
|
|
1321
|
+
{ className: "dsh-ct-think-body" },
|
|
1322
|
+
React.createElement(import_dsh_client_ui_primitives3.MarkdownText, { text, streaming: running, labels, variant: "compact" })
|
|
1323
|
+
) : void 0,
|
|
1324
|
+
[expanded, labels, running, text]
|
|
1325
|
+
);
|
|
1326
|
+
return React.createElement(
|
|
1327
|
+
"div",
|
|
1328
|
+
{
|
|
1329
|
+
className: "dsh-ct-think-root",
|
|
1330
|
+
"data-variant": "think",
|
|
1331
|
+
"data-state": running ? "running" : "ok",
|
|
1332
|
+
"data-expanded": expanded || void 0,
|
|
1333
|
+
"data-preview": preview || void 0
|
|
1334
|
+
},
|
|
1335
|
+
running && React.createElement("span", { className: "dsh-ct-visually-hidden" }, t("row.running")),
|
|
1336
|
+
React.createElement(
|
|
1337
|
+
import_dsh_client_ui_primitives3.DisclosureRow,
|
|
1338
|
+
{
|
|
1339
|
+
rowClassName: "dsh-ct-think-row",
|
|
1340
|
+
leadingClassName: "dsh-ct-think-leading",
|
|
1341
|
+
titleClassName: "dsh-ct-think-title",
|
|
1342
|
+
chevronClassName: "dsh-ct-think-chevron",
|
|
1343
|
+
icon: thinkIcon(),
|
|
1344
|
+
title: t("message.think"),
|
|
1345
|
+
open: expanded,
|
|
1346
|
+
expandable: true,
|
|
1347
|
+
expandOnRowClick: true,
|
|
1348
|
+
onToggle: toggle,
|
|
1349
|
+
collapsedContent
|
|
1350
|
+
},
|
|
1351
|
+
content
|
|
1352
|
+
)
|
|
1353
|
+
);
|
|
1354
|
+
});
|
|
1355
|
+
function blockClick(event, onToggle) {
|
|
1356
|
+
const guardEvent = {
|
|
1357
|
+
target: event.target,
|
|
1358
|
+
currentTarget: event.currentTarget
|
|
1359
|
+
};
|
|
1360
|
+
if (isBareBlockClick(guardEvent, typeof window === "undefined" ? void 0 : window)) onToggle();
|
|
1361
|
+
}
|
|
1362
|
+
function ProseBlock({
|
|
1363
|
+
text,
|
|
1364
|
+
translated,
|
|
1365
|
+
showOriginal,
|
|
1366
|
+
onToggle,
|
|
1367
|
+
streaming,
|
|
1368
|
+
labels,
|
|
1369
|
+
mentions,
|
|
1370
|
+
pathImages
|
|
1371
|
+
}) {
|
|
1372
|
+
const processed = translated !== null;
|
|
1373
|
+
const showTranslation = processed && !showOriginal;
|
|
1374
|
+
const source = showTranslation ? translated : text;
|
|
1375
|
+
return React.createElement(
|
|
1376
|
+
"div",
|
|
1377
|
+
{
|
|
1378
|
+
// 色相+粗细随显示态:蓝粗线=译文,灰细线=有译文但读原文(仍可点切回),无线=没译。
|
|
1379
|
+
className: proseClassNames(processed, showOriginal),
|
|
1380
|
+
"data-translated": showTranslation ? "true" : void 0,
|
|
1381
|
+
// 容器内含链接等交互内容,不套 role=button(非法嵌套);可聚焦 + 键盘
|
|
1382
|
+
// Enter/Space 即切换,满足官方「键盘可达」门。
|
|
1383
|
+
tabIndex: processed ? 0 : void 0,
|
|
1384
|
+
onClick: processed ? (event) => blockClick(event, onToggle) : void 0,
|
|
1385
|
+
onKeyDown: processed ? (event) => {
|
|
1386
|
+
if (event.target !== event.currentTarget) return;
|
|
1387
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1388
|
+
event.preventDefault();
|
|
1389
|
+
onToggle();
|
|
1390
|
+
}
|
|
1391
|
+
} : void 0
|
|
1392
|
+
},
|
|
1393
|
+
React.createElement(import_dsh_client_ui_primitives3.MarkdownText, {
|
|
1394
|
+
text: source,
|
|
1395
|
+
streaming,
|
|
1396
|
+
labels,
|
|
1397
|
+
fileMentions: mentions,
|
|
1398
|
+
pathImages
|
|
1399
|
+
})
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(props) {
|
|
1403
|
+
ensureAssistantStyles();
|
|
1404
|
+
const { node, groupPart, useDisclosure, useTurnData, turnProcess, openFile, renderMessageImages, fileMentions, usePresentation, t } = props;
|
|
1405
|
+
const data = node.data;
|
|
1406
|
+
const streaming = data.status === "running";
|
|
1407
|
+
const interrupted = data.status === "interrupted";
|
|
1408
|
+
const enabled = (0, import_react4.useSyncExternalStore)(subscribeSettings, () => settingsStore.getState().enabled);
|
|
1409
|
+
const aiConfigured = (0, import_react4.useSyncExternalStore)(subscribeSettings, () => settingsStore.getState().aiConfigured);
|
|
1410
|
+
(0, import_react4.useSyncExternalStore)(subscribeTranslate, () => chatTranslate.getVersion());
|
|
1411
|
+
const canTranslate = enabled && aiConfigured;
|
|
1412
|
+
const rowKey = `a${node.anchorSeq ?? `t${data.turn}:s${data.step}`}`;
|
|
1413
|
+
const labels = (0, import_react4.useMemo)(() => markdownLabels(t), [t]);
|
|
1414
|
+
const pathImages = (0, import_react4.useMemo)(
|
|
1415
|
+
() => ({ resolve: (value) => localPathMediaUrl(document.baseURI, value) }),
|
|
1416
|
+
[]
|
|
1417
|
+
);
|
|
1418
|
+
const turn = node.location?.kind === "turn" || node.location?.kind === "step" ? node.location.turn : void 0;
|
|
1419
|
+
const tail = useTurnData("turn-tail");
|
|
1420
|
+
const owner = (0, import_react4.useMemo)(() => {
|
|
1421
|
+
if (turn?.status !== "closed" || data.finalNode === void 0) return void 0;
|
|
1422
|
+
if (tail?.closing?.finalNode?.seq !== data.finalNode.seq) return void 0;
|
|
1423
|
+
return { turn, seq: data.finalNode.seq, openFile };
|
|
1424
|
+
}, [data.finalNode, openFile, tail, turn]);
|
|
1425
|
+
const mentions = (0, import_react4.useMemo)(() => owner === void 0 ? void 0 : fileMentions(owner), [fileMentions, owner]);
|
|
1426
|
+
const blocks = (0, import_react4.useMemo)(() => data.blocks ?? [], [data.blocks]);
|
|
1427
|
+
const plan = (0, import_react4.useMemo)(
|
|
1428
|
+
() => planAssistantRow({
|
|
1429
|
+
blocks,
|
|
1430
|
+
streaming,
|
|
1431
|
+
interrupted,
|
|
1432
|
+
groupPart,
|
|
1433
|
+
canTranslate,
|
|
1434
|
+
outcomes: void 0
|
|
1435
|
+
}),
|
|
1436
|
+
[blocks, streaming, interrupted, groupPart, canTranslate]
|
|
1437
|
+
);
|
|
1438
|
+
const rootRef = (0, import_react4.useRef)(null);
|
|
1439
|
+
const inView = useSettledInView(rootRef, !streaming && canTranslate && plan.entries !== null && plan.texts.length > 0);
|
|
1440
|
+
(0, import_react4.useEffect)(() => {
|
|
1441
|
+
if (streaming || !canTranslate || !inView || plan.entries === null || plan.texts.length === 0) return;
|
|
1442
|
+
chatTranslate.ensure(rowKey, plan.texts);
|
|
1443
|
+
}, [streaming, canTranslate, inView, rowKey, plan]);
|
|
1444
|
+
const rowState = chatTranslate.getState(rowKey);
|
|
1445
|
+
const displayed = (0, import_react4.useMemo)(
|
|
1446
|
+
() => planAssistantRow({
|
|
1447
|
+
blocks,
|
|
1448
|
+
streaming,
|
|
1449
|
+
interrupted,
|
|
1450
|
+
groupPart,
|
|
1451
|
+
canTranslate,
|
|
1452
|
+
outcomes: rowState?.outcomes
|
|
1453
|
+
}),
|
|
1454
|
+
[blocks, streaming, interrupted, groupPart, canTranslate, rowState]
|
|
1455
|
+
);
|
|
1456
|
+
const reasoningHidden = turnProcess !== void 0 && turnProcess.foldable && turnProcess.spec.answerStep === data.step && turnProcess.spec.inlineReasoning && !turnProcess.open;
|
|
1457
|
+
const revealProcess = (0, import_react4.useCallback)(() => {
|
|
1458
|
+
turnProcess?.setOpen(true);
|
|
1459
|
+
}, [turnProcess]);
|
|
1460
|
+
const [originalKeys, setOriginalKeys] = (0, import_react4.useState)(() => /* @__PURE__ */ new Set());
|
|
1461
|
+
const toggleBlock = (0, import_react4.useCallback)((key) => {
|
|
1462
|
+
setOriginalKeys((current) => {
|
|
1463
|
+
const next = new Set(current);
|
|
1464
|
+
if (next.has(key)) next.delete(key);
|
|
1465
|
+
else next.add(key);
|
|
1466
|
+
return next;
|
|
1467
|
+
});
|
|
1468
|
+
}, []);
|
|
1469
|
+
if (displayed.entries === null) return null;
|
|
1470
|
+
const last = blocks.length - 1;
|
|
1471
|
+
const rendered = [];
|
|
1472
|
+
for (const entry of displayed.entries) {
|
|
1473
|
+
switch (entry.type) {
|
|
1474
|
+
case "prose":
|
|
1475
|
+
rendered.push(
|
|
1476
|
+
React.createElement(ProseBlock, {
|
|
1477
|
+
key: entry.key,
|
|
1478
|
+
text: entry.text,
|
|
1479
|
+
translated: entry.translated,
|
|
1480
|
+
showOriginal: originalKeys.has(entry.key),
|
|
1481
|
+
onToggle: () => toggleBlock(entry.key),
|
|
1482
|
+
streaming,
|
|
1483
|
+
labels,
|
|
1484
|
+
mentions,
|
|
1485
|
+
pathImages
|
|
1486
|
+
})
|
|
1487
|
+
);
|
|
1488
|
+
break;
|
|
1489
|
+
case "reasoning":
|
|
1490
|
+
rendered.push(
|
|
1491
|
+
React.createElement(
|
|
1492
|
+
ProcessReasoning,
|
|
1493
|
+
{
|
|
1494
|
+
key: entry.key,
|
|
1495
|
+
hidden: reasoningHidden,
|
|
1496
|
+
reveal: revealProcess,
|
|
1497
|
+
children: React.createElement(ReasoningRow, {
|
|
1498
|
+
text: entry.text,
|
|
1499
|
+
running: streaming && entry.blockIndex === last,
|
|
1500
|
+
usePresentation,
|
|
1501
|
+
useDisclosure,
|
|
1502
|
+
t
|
|
1503
|
+
})
|
|
1504
|
+
}
|
|
1505
|
+
)
|
|
1506
|
+
);
|
|
1507
|
+
break;
|
|
1508
|
+
case "images":
|
|
1509
|
+
rendered.push(
|
|
1510
|
+
React.createElement(React.Fragment, {
|
|
1511
|
+
key: entry.key
|
|
1512
|
+
}, renderMessageImages({ images: entry.attachments.map((attachment) => ({ attachment })), align: "start" }))
|
|
1513
|
+
);
|
|
1514
|
+
break;
|
|
1515
|
+
case "unknown":
|
|
1516
|
+
rendered.push(
|
|
1517
|
+
React.createElement(import_dsh_client_ui_primitives3.JsonBlock, {
|
|
1518
|
+
key: entry.key,
|
|
1519
|
+
label: t("message.unknownBlock"),
|
|
1520
|
+
payload: entry.block,
|
|
1521
|
+
truncatedLabel: (total) => t("json.truncated", { total })
|
|
1522
|
+
})
|
|
1523
|
+
);
|
|
1524
|
+
break;
|
|
1525
|
+
case "stopped":
|
|
1526
|
+
rendered.push(React.createElement("span", { key: entry.key, className: "dsh-ct-stopped" }, t("message.stopped")));
|
|
1527
|
+
break;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
return React.createElement(
|
|
1531
|
+
"div",
|
|
1532
|
+
{ ref: rootRef, className: "dsh-ct-root", "data-streaming": streaming || void 0 },
|
|
1533
|
+
React.createElement("div", { className: "dsh-ct-body" }, rendered)
|
|
1534
|
+
);
|
|
1535
|
+
});
|
|
1536
|
+
function ProcessReasoning(props) {
|
|
1537
|
+
const ref = useSearchableHidden(props.hidden, props.reveal);
|
|
1538
|
+
return React.createElement(
|
|
1539
|
+
"div",
|
|
1540
|
+
{ ref, "data-turn-process-inline": props.hidden || void 0 },
|
|
1541
|
+
props.children
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// src/client/chat/presentation.ts
|
|
1546
|
+
function policy(mode, foldCompletedTurns, stepGrouping, liveProcessDetail, settledReasoningPreview) {
|
|
1547
|
+
return Object.freeze({ mode, foldCompletedTurns, stepGrouping, liveProcessDetail, settledReasoningPreview });
|
|
1548
|
+
}
|
|
1549
|
+
var POLICY_BY_MODE = Object.freeze({
|
|
1550
|
+
compact: policy("compact", true, "collapsed", false, false),
|
|
1551
|
+
standard: policy("standard", true, "collapsed", true, true),
|
|
1552
|
+
detailed: policy("detailed", true, "history", true, true),
|
|
1553
|
+
verbose: policy("verbose", false, "none", false, true),
|
|
1554
|
+
normal: policy("standard", true, "collapsed", true, true),
|
|
1555
|
+
expanded: policy("detailed", true, "history", true, true)
|
|
1556
|
+
});
|
|
1557
|
+
var DEFAULT_MODE = "standard";
|
|
1558
|
+
var ChatPresentation = class {
|
|
1559
|
+
policy = POLICY_BY_MODE[DEFAULT_MODE];
|
|
1560
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1561
|
+
form = null;
|
|
1562
|
+
unsubscribe = null;
|
|
1563
|
+
attach(form) {
|
|
1564
|
+
this.detach();
|
|
1565
|
+
if (!form) {
|
|
1566
|
+
this.policy = POLICY_BY_MODE[DEFAULT_MODE];
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
this.form = form;
|
|
1570
|
+
this.read();
|
|
1571
|
+
this.unsubscribe = form.subscribe(() => this.read());
|
|
1572
|
+
}
|
|
1573
|
+
detach() {
|
|
1574
|
+
this.unsubscribe?.();
|
|
1575
|
+
this.unsubscribe = null;
|
|
1576
|
+
this.form = null;
|
|
1577
|
+
}
|
|
1578
|
+
getSnapshot = () => this.policy;
|
|
1579
|
+
subscribe = (listener) => {
|
|
1580
|
+
this.listeners.add(listener);
|
|
1581
|
+
return () => {
|
|
1582
|
+
this.listeners.delete(listener);
|
|
1583
|
+
};
|
|
1584
|
+
};
|
|
1585
|
+
read() {
|
|
1586
|
+
const form = this.form;
|
|
1587
|
+
if (!form) return;
|
|
1588
|
+
let value;
|
|
1589
|
+
try {
|
|
1590
|
+
const snap = form.getSnapshot();
|
|
1591
|
+
value = snap.status === "ready" ? snap.value : void 0;
|
|
1592
|
+
} catch {
|
|
1593
|
+
value = void 0;
|
|
1594
|
+
}
|
|
1595
|
+
const raw = typeof value?.transcriptView === "string" ? value.transcriptView : "";
|
|
1596
|
+
const next = POLICY_BY_MODE[raw] ?? POLICY_BY_MODE[DEFAULT_MODE];
|
|
1597
|
+
if (next !== this.policy) {
|
|
1598
|
+
this.policy = next;
|
|
1599
|
+
for (const listener of [...this.listeners]) {
|
|
1600
|
+
try {
|
|
1601
|
+
listener();
|
|
1602
|
+
} catch {
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
function createChatPresentation() {
|
|
1609
|
+
return new ChatPresentation();
|
|
1610
|
+
}
|
|
1611
|
+
var chatPresentation = createChatPresentation();
|
|
1612
|
+
|
|
2311
1613
|
// src/client/index.ts
|
|
2312
1614
|
var name = "dsh-chat-translate";
|
|
2313
1615
|
var inject = ["slots", "configForms", "remote", "remote.credentials", "locale"];
|
|
2314
1616
|
function apply(ctx) {
|
|
2315
|
-
|
|
1617
|
+
ensureAssistantStyles();
|
|
1618
|
+
ctx.effect(() => {
|
|
1619
|
+
const slots = ctx.slots;
|
|
1620
|
+
if (!slots || typeof slots.inject !== "function") return () => {
|
|
1621
|
+
};
|
|
1622
|
+
return slots.inject(
|
|
1623
|
+
"conversation.chat.node",
|
|
1624
|
+
() => slots.register(
|
|
1625
|
+
{
|
|
1626
|
+
name: "conversation.chat.node",
|
|
1627
|
+
key: "assistant-step",
|
|
1628
|
+
// 行内的折叠、停止、未知块等 chrome 文案复用宿主 `chat` 词典,
|
|
1629
|
+
// 两侧渲染逐字一致;t 由座次按该命名空间合成。
|
|
1630
|
+
locale: "chat",
|
|
1631
|
+
priority: -1,
|
|
1632
|
+
// 思考行的 settledReasoningPreview 来自活的工作细节模式。
|
|
1633
|
+
inject: () => ({ hooks: { presentation: chatPresentation } })
|
|
1634
|
+
},
|
|
1635
|
+
AssistantStepView
|
|
1636
|
+
)
|
|
1637
|
+
);
|
|
1638
|
+
}, "dsh-chat-translate: assistant step takeover");
|
|
1639
|
+
ctx.effect(() => {
|
|
1640
|
+
let form = null;
|
|
1641
|
+
try {
|
|
1642
|
+
form = ctx.configForms?.get("ui-chat") ?? null;
|
|
1643
|
+
} catch {
|
|
1644
|
+
form = null;
|
|
1645
|
+
}
|
|
1646
|
+
chatPresentation.attach(form);
|
|
1647
|
+
return () => chatPresentation.detach();
|
|
1648
|
+
}, "dsh-chat-translate: chat presentation source");
|
|
2316
1649
|
ctx.effect(() => setupSettingsUi(ctx), "dsh-chat-translate: settings section");
|
|
1650
|
+
ctx.effect(() => setupComposerToggle(ctx), "dsh-chat-translate: composer dock toggle");
|
|
2317
1651
|
}
|
|
2318
1652
|
return module.exports; } });
|
|
2319
1653
|
//# sourceMappingURL=client.js.map
|