@lynn123411/dsh-chat-translate 1.5.0 → 1.8.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 CHANGED
@@ -5,8 +5,8 @@ DeepSeek Harness Web 界面的工具调用与思考链智能翻译插件。自
5
5
  ## 特性
6
6
 
7
7
  - **工具调用标题自动翻译**:仅在渲染层将工具调用动作描述(如 `Locate DSH home directory structure`)翻译为中文,回答正文永不翻译。
8
- - **思考链翻译(按钮触发,仅 AI 通道)**:每张思考卡的「Think」右侧有一个 18px 按钮,点击才翻译这条思考正文,**不会自动翻译**;折叠时按钮排在标题与折叠摘要之间。三态:未翻译=描边「译」字,翻译中=圈内圆环旋转(系统开启「减弱动态效果」时改为静态高亮),已翻译=实心高亮,再点一次整条切回原文(正文里的点击不再切换,思考链一律靠这个按钮变更)。思考还在流式输出(`data-state="running"`)时按钮置灰;折叠状态下点击会先自动展开这张卡片;翻译中再点忽略;失败时按钮停止动画、回到可翻译状态,可以再点重试。
9
- - **缓存与显示规则**:展开卡片时若整条思考的段落都已命中本地缓存,直接显示中文(不发请求);只命中一部分时展开仍是原文,点按钮时已命中的立刻出现、其余才发请求;翻译中途折叠卡片,本次翻译会跑完并进缓存,再展开点一下即可显示。思考卡折叠起来的那一行摘要永不翻译;代码块原样保留;混合块(列表项文字加子列表或代码块)只翻行内文字,嵌套结构留在原位。
8
+ - **思考链翻译(按钮触发,仅 AI 通道)**:展开的思考卡在「Think」右侧有一个 18px 按钮,点击才翻译这条思考正文,**不会自动翻译**;折叠的卡片没有按钮,展开后按钮才出现。三态:未翻译=描边「译」字,翻译中=圈内圆环旋转(系统开启「减弱动态效果」时改为静态高亮),已翻译=实心高亮,再点一次整条切回原文(正文里的点击不再切换,思考链一律靠这个按钮变更)。思考还在流式输出(`data-state="running"`)时按钮置灰;翻译中再点忽略;失败时按钮停止动画、回到可翻译状态,可以再点重试。
9
+ - **缓存与显示规则**:展开卡片时若整条思考的段落都已命中本地缓存,直接显示中文(不发请求);只命中一部分时展开仍是原文,点按钮时已命中的立刻出现、其余才发请求;翻译中途折叠卡片,本次翻译会跑完并进缓存,再展开时整条命中缓存就直接显示中文。思考卡折叠起来的那一行摘要永不翻译;代码块原样保留;混合块(列表项文字加子列表或代码块)只翻行内文字,嵌套结构留在原位。
10
10
  - **按块翻译与打包**:正文按 markdown 块级元素切分,相邻块打包成一个请求,输入上限 4096 估算 token、输出上限 8192 token;单个块超过输入上限时按空行、单行、句末依次切分,译回后拼回;客户端按 2048 估算 token 分块,逐块请求、逐块挂载,中文一段一段出现。请求走独立串行队列,同时最多一个在途,超时独立(默认 600 秒,可调至 900 秒),译文进独立缓存池(客户端与磁盘各 300 条),与工具标题互不挤占。
11
11
  - **AI 翻译通道(OpenAI 兼容协议)**:对接任意 OpenAI 兼容的 `chat/completions` 服务(OpenAI、DeepSeek、通义、Ollama、本地 vLLM 等),Base URL 与模型可在设置面板配置;API Key 经 DSH 凭据服务读写 `~/.dsh/.credentials.yaml` 的 `TRANSLATE_API_KEY`,填写后立即生效。工具标题与思考链都由它承接。
12
12
  - **Bing 免 Key 兜底通道**:内置微软 Bing 网页翻译(免 Key、国内直连),只用于工具调用标题的兜底;AI 未配置或请求失败时自动降级,思考链翻译永远不走这条通道。双通道均有独立开关,同时关闭则不翻译。
@@ -15,8 +15,7 @@ DeepSeek Harness Web 界面的工具调用与思考链智能翻译插件。自
15
15
  - **当前会话作用域**:仅翻译当前查看的会话,切换会话自动跟随新内容;视口懒加载(150px 缓冲)与文档顺序排队,译文按阅读顺序出现。
16
16
  - **智能调度与熔断保护**:工具标题走 1–100 动态并发限流队列、AI 30s / Bing 2s 超时、连续失败熔断自愈、在途请求合并去重、7 天 LRU 磁盘持久化缓存(工具标题 `~/.dsh/dsh-chat-translate/cache.json`,思考链 `~/.dsh/dsh-chat-translate/think-cache.json`)。
17
17
  - **DSH 原生配置接入**:配置走 DSH `settings` 服务(用户层写入 `~/.dsh/settings.yaml` 的 `dsh-chat-translate` 段)、密钥走 DSH `credentials` 服务、设置面板经 `settingsScope` 与 `credentials` Remote API 读写——无任何自研配置文件。
18
- - **设置面板集成**:设置面板挂在与 `dsh-smooth-stream` 共用的「设置 - 阅读体验」共享页(本插件贡献其中一张卡片),页内提供总开关、AI/Bing 通道开关、「显示思考链翻译按钮」(默认打开)与思考链超时、Base URL 与模型配置、通道测试与并发数调节。按钮的显示还要求 AI 通道打开且已配置(Key / Base URL / 模型齐全),缺任何一条都不注入按钮并还原已翻译内容。
19
- - **与 dsh-smooth-stream 协同**:该插件的思考卡即使折叠也把正文留在 DOM 里,本插件因此能在思考流式期间就地翻译定型段落;它挂载的译文容器沿用 `dsh-tidy-translated-block`,会由 smooth-stream 的逐字显示机制按字符显现。
18
+ - **设置面板集成**:设置面板挂在「设置 - 阅读体验」页(本插件贡献其中一张卡片),页内提供总开关、AI/Bing 通道开关、「显示思考链翻译按钮」(默认打开)与思考链超时、Base URL 与模型配置、通道测试与并发数调节。按钮的显示还要求 AI 通道打开且已配置(Key / Base URL / 模型齐全),缺任何一条都不注入按钮并还原已翻译内容。
20
19
 
21
20
  ## 安装
22
21
 
@@ -28,10 +28,10 @@
28
28
 
29
29
  > 从 1.2 起,插件不再读写独立的 `~/.dsh/dsh-chat-translate-config.json`:升级后首次加载会自动把旧文件的值迁移进上面的 settings 段并删除旧文件(若你已在 UI 中改过设置,则以你的设置为准)。
30
30
 
31
- 3. **思考链翻译按钮**:AI 卡片里的「显示思考链翻译按钮」默认打开;按钮的显示还要求总开关打开、AI 通道打开且已配置(Key / Base URL / 模型齐全),缺任何一条都不注入按钮并还原已翻译内容。按钮在每张思考卡的「Think」右侧(折叠时排在标题与折叠摘要之间),点击才翻译这条思考正文,不会自动翻译。
31
+ 3. **思考链翻译按钮**:AI 卡片里的「显示思考链翻译按钮」默认打开;按钮的显示还要求总开关打开、AI 通道打开且已配置(Key / Base URL / 模型齐全),缺任何一条都不注入按钮并还原已翻译内容。按钮只在展开的思考卡上出现,位于「Think」右侧;折叠的卡片没有按钮,展开后才插入。点击才翻译这条思考正文,不会自动翻译。
32
32
 
33
33
  - 三态:未翻译=描边「译」字,翻译中=圈内圆环旋转(系统开启「减弱动态效果」时为静态高亮),已翻译=实心高亮;再点一次整条切回原文;正文里的点击不再改变原文与译文,思考链一律靠这个按钮变更。
34
- - 思考还在流式输出(`data-state="running"`)时按钮置灰;折叠状态下点击会先自动展开卡片;翻译中再点忽略;失败时按钮停止动画并回到可翻译状态,可以再点重试。
34
+ - 思考还在流式输出(`data-state="running"`)时按钮置灰;翻译中再点忽略;失败时按钮停止动画并回到可翻译状态,可以再点重试。
35
35
  - 缓存规则:展开时若整条思考的段落都命中本地缓存就直接显示中文(不发请求);只命中一部分时展开仍是原文,点按钮时已命中的立刻出现、其余才发请求;翻译中途折叠卡片,本次翻译会跑完并进缓存。
36
36
  - 翻译范围:展开的思考正文;折叠起来的那一行摘要永不翻译,代码块原样保留,混合块只翻行内文字、嵌套子列表留在原位。只走 AI 通道,Bing 从不参与。
37
37
  - 请求方式:客户端按 2048 估算 token 分块,宿主按块打包(输入上限 4096 估算 token、输出上限 8192 token),走独立串行队列(同时最多一个在途请求),超时由 `thinkTimeoutMs` 控制(默认 600000 毫秒,范围 500–900000)。译文进独立缓存池(客户端与磁盘各 300 条),与工具标题互不挤占。
package/lib/client.js CHANGED
@@ -465,7 +465,7 @@ var StreamDebounceViewportObserver = class {
465
465
  * Pending per-element streaming debounce timers. A Map (not a WeakMap)
466
466
  * because disconnect() must enumerate and clear every pending timer: a
467
467
  * WeakMap cannot be iterated, so a debounce armed just before the switch was
468
- * turned off would still fire registerForViewport() afterwards.
468
+ * turned off would still fire register() afterwards.
469
469
  */
470
470
  streamingTimers = /* @__PURE__ */ new Map();
471
471
  pendingQueue = [];
@@ -489,7 +489,7 @@ var StreamDebounceViewportObserver = class {
489
489
  if (entry.isIntersecting && entry.target instanceof HTMLElement) {
490
490
  const el = entry.target;
491
491
  this.intersectionObserver?.unobserve(el);
492
- const text = el.dataset.tidyPendingText || el.textContent?.trim() || "";
492
+ const text = el.dataset.tidyPendingText || NonDestructiveTranslationMount.extractVisibleText(el) || "";
493
493
  if (text) {
494
494
  delete el.dataset.tidyPendingText;
495
495
  this.enqueueBatch(el, text);
@@ -510,6 +510,9 @@ var StreamDebounceViewportObserver = class {
510
510
  * If streaming updates characterData repeatedly within debounceMs, the timer resets.
511
511
  */
512
512
  observeWithDebounce(element, text, immediate = false) {
513
+ this.armDebounce(element, text, immediate);
514
+ }
515
+ armDebounce(element, text, immediate) {
513
516
  if (!element || !text) return;
514
517
  const existingTimer = this.streamingTimers.get(element);
515
518
  if (existingTimer !== void 0) {
@@ -517,20 +520,20 @@ var StreamDebounceViewportObserver = class {
517
520
  this.streamingTimers.delete(element);
518
521
  }
519
522
  if (immediate || this.options.debounceMs <= 0) {
520
- this.registerForViewport(element, text);
523
+ this.register(element, text);
521
524
  return;
522
525
  }
523
526
  const timer = window.setTimeout(() => {
524
527
  if (this.streamingTimers.get(element) !== timer) return;
525
528
  this.streamingTimers.delete(element);
526
529
  if (element.isConnected) {
527
- const latestText = element.textContent?.trim() || text;
528
- this.registerForViewport(element, latestText);
530
+ const latestText = NonDestructiveTranslationMount.extractVisibleText(element) || text;
531
+ this.register(element, latestText);
529
532
  }
530
533
  }, this.options.debounceMs);
531
534
  this.streamingTimers.set(element, timer);
532
535
  }
533
- registerForViewport(element, text) {
536
+ register(element, text) {
534
537
  if (!element.isConnected) return;
535
538
  if (!this.intersectionObserver) {
536
539
  this.enqueueBatch(element, text);
@@ -620,7 +623,9 @@ var LazyTranslationQueue = class {
620
623
  if (!item.element.isConnected) continue;
621
624
  const cached = clientCache.get(item.text);
622
625
  if (cached) {
623
- this.applyTranslation(item.element, cached, item.text);
626
+ if (this.stillMatches(item.element, item.text)) {
627
+ this.applyTranslation(item.element, cached, item.text);
628
+ }
624
629
  continue;
625
630
  }
626
631
  const list = textMap.get(item.text) || [];
@@ -631,19 +636,24 @@ var LazyTranslationQueue = class {
631
636
  if (uniqueTexts.length === 0) return;
632
637
  const results = await requestTranslateBatch(uniqueTexts);
633
638
  for (const res of results) {
639
+ const entries = textMap.get(res.original) || [];
634
640
  if (res.translated && res.translated.trim() && res.channel !== "fallback" && res.channel !== "fallback-client" && res.translated.trim() !== res.original.trim()) {
635
641
  clientCache.set(res.original, res.translated);
636
- const entries = textMap.get(res.original) || [];
637
642
  for (const entry of entries) {
638
- if (entry.isConnected && this.enabled) {
639
- this.applyTranslation(entry, res.translated, res.original);
640
- }
643
+ if (!entry.isConnected || !this.enabled) continue;
644
+ if (!this.stillMatches(entry, res.original)) continue;
645
+ this.applyTranslation(entry, res.translated, res.original);
641
646
  }
642
- } else if (res.channel === "fallback" || res.channel === "fallback-client") {
647
+ } else {
643
648
  console.debug(`[dsh-chat-translate] \u7FFB\u8BD1\u672A\u6210\u529F (\u964D\u7EA7\u4FDD\u7559\u539F\u6587): "${res.original.slice(0, 40)}"`);
644
649
  }
645
650
  }
646
651
  }
652
+ /** 元素当前承载的原文是否仍是这次结果对应的那一份。 */
653
+ stillMatches(element, original) {
654
+ const current = NonDestructiveTranslationMount.extractVisibleText(element);
655
+ return !current || current === original;
656
+ }
647
657
  applyTranslation(element, translated, original) {
648
658
  if (!element.isConnected || !this.enabled) return;
649
659
  NonDestructiveTranslationMount.mount(element, translated, {
@@ -921,7 +931,6 @@ var ThinkTranslateController = class {
921
931
  if (state !== void 0) {
922
932
  state.button = null;
923
933
  state.state = "idle";
924
- state.pendingActivate = false;
925
934
  state.expandedHandled = false;
926
935
  }
927
936
  NonDestructiveTranslationMount.restore(card);
@@ -949,7 +958,7 @@ var ThinkTranslateController = class {
949
958
  stateFor(card) {
950
959
  let state = this.states.get(card);
951
960
  if (state === void 0) {
952
- state = { button: null, state: "idle", expandedHandled: false, pendingActivate: false };
961
+ state = { button: null, state: "idle", expandedHandled: false };
953
962
  this.states.set(card, state);
954
963
  }
955
964
  return state;
@@ -958,23 +967,20 @@ var ThinkTranslateController = class {
958
967
  if (!card.isConnected) return;
959
968
  const state = this.stateFor(card);
960
969
  if (!this.usable()) {
961
- this.removeButton(card, state, true);
970
+ this.discardCard(card, state);
962
971
  return;
963
972
  }
964
973
  const expanded = card.querySelector(EXPANDED_SELECTOR) !== null;
965
- if (!expanded) state.expandedHandled = false;
966
- this.ensureButton(card, state);
974
+ if (expanded) this.ensureButton(card, state);
975
+ else {
976
+ this.removeButton(state);
977
+ state.expandedHandled = false;
978
+ }
967
979
  if (state.state !== "working") {
968
980
  state.state = this.isShowingTranslation(card) ? "translated" : "idle";
969
981
  }
970
982
  state.button?.setState(state.state);
971
983
  this.updateDisabled(card, state);
972
- if (state.pendingActivate) {
973
- if (card.dataset.state === "running") return;
974
- state.pendingActivate = false;
975
- this.translateCard(card, state);
976
- return;
977
- }
978
984
  if (!expanded || state.expandedHandled || state.state === "working") return;
979
985
  state.expandedHandled = true;
980
986
  this.showCachedWhenComplete(card, state);
@@ -997,12 +1003,17 @@ var ThinkTranslateController = class {
997
1003
  state.button = button;
998
1004
  button.setState(state.state);
999
1005
  }
1000
- removeButton(card, state, restore) {
1006
+ /** 折叠或停用时撤掉按钮,卡片的翻译状态不变。 */
1007
+ removeButton(state) {
1001
1008
  state.button?.element.remove();
1002
1009
  state.button = null;
1010
+ }
1011
+ /** 插件停用:撤掉按钮并还原这张卡片上的全部译文。 */
1012
+ discardCard(card, state) {
1013
+ this.removeButton(state);
1003
1014
  state.state = "idle";
1004
- state.pendingActivate = false;
1005
- if (restore) NonDestructiveTranslationMount.restore(card);
1015
+ state.expandedHandled = false;
1016
+ NonDestructiveTranslationMount.restore(card);
1006
1017
  }
1007
1018
  /** 卡片当前显示的是译文(至少有一个译文块可见)。 */
1008
1019
  isShowingTranslation(card) {
@@ -1027,18 +1038,11 @@ var ThinkTranslateController = class {
1027
1038
  state.state = this.isShowingTranslation(card) ? "translated" : "idle";
1028
1039
  state.button?.setState(state.state);
1029
1040
  }
1030
- /** 按钮被点击。 */
1041
+ /** 按钮被点击;按钮只在展开的卡片上存在。 */
1031
1042
  activate(card) {
1032
1043
  const state = this.stateFor(card);
1033
1044
  if (!this.usable() || state.state === "working") return;
1034
1045
  if (card.dataset.state === "running") return;
1035
- if (card.querySelector(EXPANDED_SELECTOR) === null) {
1036
- state.pendingActivate = true;
1037
- const row = card.querySelector("[data-disclosure-row]") ?? card;
1038
- row.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
1039
- this.syncCard(card);
1040
- return;
1041
- }
1042
1046
  if (state.state === "translated") {
1043
1047
  NonDestructiveTranslationMount.toggleGroup(card, true);
1044
1048
  state.state = "idle";
@@ -1284,9 +1288,15 @@ var ChatTranslateObserver = class {
1284
1288
  for (const mutation of mutations) {
1285
1289
  if (mutation.type === "childList") {
1286
1290
  let addedElement = false;
1291
+ let changedOwner = null;
1287
1292
  for (let i = 0; i < mutation.addedNodes.length; i++) {
1288
1293
  const node = mutation.addedNodes[i];
1289
1294
  if (node instanceof HTMLElement) {
1295
+ const owner = this.mountedOwner(node);
1296
+ if (owner) {
1297
+ changedOwner = owner;
1298
+ continue;
1299
+ }
1290
1300
  if (NonDestructiveTranslationMount.isOwnNode(node)) continue;
1291
1301
  addedElement = true;
1292
1302
  this.scanNode(node);
@@ -1294,12 +1304,16 @@ var ChatTranslateObserver = class {
1294
1304
  }
1295
1305
  }
1296
1306
  const target = mutation.target;
1297
- if (!addedElement && target instanceof HTMLElement) {
1298
- if (!NonDestructiveTranslationMount.isOwnNode(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)) {
1299
1312
  this.scanNode(target);
1300
1313
  this.scanThink(target);
1301
1314
  }
1302
1315
  }
1316
+ if (changedOwner) this.handleOriginalTextChange(changedOwner);
1303
1317
  } else if (mutation.type === "attributes") {
1304
1318
  const target = mutation.target;
1305
1319
  if (target instanceof HTMLElement) {
@@ -1308,6 +1322,11 @@ var ChatTranslateObserver = class {
1308
1322
  this.scanThink(target);
1309
1323
  }
1310
1324
  } else if (mutation.type === "characterData") {
1325
+ const owner = this.mountedOwner(mutation.target);
1326
+ if (owner) {
1327
+ this.handleOriginalTextChange(owner);
1328
+ continue;
1329
+ }
1311
1330
  const parent = mutation.target.parentElement;
1312
1331
  if (parent instanceof HTMLElement) {
1313
1332
  if (NonDestructiveTranslationMount.isOwnNode(parent)) continue;
@@ -1317,6 +1336,35 @@ var ChatTranslateObserver = class {
1317
1336
  }
1318
1337
  }
1319
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
+ }
1320
1368
  scanContainer(container) {
1321
1369
  const spans = container.querySelectorAll(TOOL_TITLE_SELECTOR);
1322
1370
  spans.forEach((span) => {
@@ -1353,11 +1401,6 @@ var ChatTranslateObserver = class {
1353
1401
  }
1354
1402
  processSpan(span) {
1355
1403
  if (NonDestructiveTranslationMount.isMounted(span)) {
1356
- const original = NonDestructiveTranslationMount.getOriginal(span);
1357
- if (original) {
1358
- const cached2 = clientCache.get(original);
1359
- if (cached2) return;
1360
- }
1361
1404
  return;
1362
1405
  }
1363
1406
  const text = NonDestructiveTranslationMount.extractVisibleText(span);
@@ -1859,7 +1902,7 @@ var zh = {
1859
1902
  title: "\u804A\u5929\u7FFB\u8BD1",
1860
1903
  masterTitle: "\u7FFB\u8BD1\u603B\u5F00\u5173",
1861
1904
  enableTranslation: "\u542F\u7528\u7FFB\u8BD1",
1862
- masterDesc: "\u81EA\u52A8\u5C06\u5F53\u524D\u4F1A\u8BDD\u4E2D\u5DE5\u5177\u8C03\u7528\u6807\u9898\uFF08\u5982 {example}\uFF09\u7FFB\u8BD1\u4E3A\u4E2D\u6587\uFF0C\u70B9\u51FB\u8BD1\u6587\u53EF\u539F\u5730\u5207\u6362\u539F\u6587/\u8BD1\u6587\u3002\u601D\u8003\u6B63\u6587\u9ED8\u8BA4\u4E0D\u7FFB\u8BD1\uFF0C\u7531 AI \u5361\u7247\u91CC\u7684\u300C\u663E\u793A\u601D\u8003\u94FE\u7FFB\u8BD1\u6309\u94AE\u300D\u63A7\u5236\uFF1A\u6253\u5F00\u540E\u6BCF\u5F20\u601D\u8003\u5361\u53F3\u4FA7\u51FA\u73B0\u6309\u94AE\uFF0C\u70B9\u51FB\u624D\u7FFB\u8BD1\u3002\u601D\u8003\u5361\u6298\u53E0\u8D77\u6765\u7684\u90A3\u4E00\u884C\u6458\u8981\u6C38\u4E0D\u7FFB\u8BD1\u3002\u4EC5\u4F5C\u7528\u4E8E\u5F53\u524D\u67E5\u770B\u7684\u4F1A\u8BDD\uFF0C\u56DE\u7B54\u6B63\u6587\u6C38\u4E0D\u7FFB\u8BD1\u3002",
1905
+ masterDesc: "\u81EA\u52A8\u5C06\u5F53\u524D\u4F1A\u8BDD\u4E2D\u5DE5\u5177\u8C03\u7528\u6807\u9898\uFF08\u5982 {example}\uFF09\u7FFB\u8BD1\u4E3A\u4E2D\u6587\uFF0C\u70B9\u51FB\u8BD1\u6587\u53EF\u539F\u5730\u5207\u6362\u539F\u6587/\u8BD1\u6587\u3002\u601D\u8003\u6B63\u6587\u9ED8\u8BA4\u4E0D\u7FFB\u8BD1\uFF0C\u7531 AI \u5361\u7247\u91CC\u7684\u300C\u663E\u793A\u601D\u8003\u94FE\u7FFB\u8BD1\u6309\u94AE\u300D\u63A7\u5236\uFF1A\u6253\u5F00\u540E\u5C55\u5F00\u7684\u601D\u8003\u5361\u53F3\u4FA7\u51FA\u73B0\u6309\u94AE\uFF0C\u70B9\u51FB\u624D\u7FFB\u8BD1\u3002\u601D\u8003\u5361\u6298\u53E0\u8D77\u6765\u7684\u90A3\u4E00\u884C\u6458\u8981\u6C38\u4E0D\u7FFB\u8BD1\u3002\u4EC5\u4F5C\u7528\u4E8E\u5F53\u524D\u67E5\u770B\u7684\u4F1A\u8BDD\uFF0C\u56DE\u7B54\u6B63\u6587\u6C38\u4E0D\u7FFB\u8BD1\u3002",
1863
1906
  aiTitle: "AI \u7FFB\u8BD1\uFF08OpenAI \u517C\u5BB9\u534F\u8BAE\uFF09",
1864
1907
  enableAi: "\u542F\u7528 AI \u7FFB\u8BD1\u901A\u9053",
1865
1908
  badgeConfigured: "\u5DF2\u914D\u7F6E",
@@ -1883,7 +1926,7 @@ var zh = {
1883
1926
  testOk: "\u8FDE\u63A5\u6B63\u5E38\uFF0C\u5EF6\u8FDF {latency}ms",
1884
1927
  testFail: "\u5931\u8D25\uFF1A{error}",
1885
1928
  thinkEnableTitle: "\u663E\u793A\u601D\u8003\u94FE\u7FFB\u8BD1\u6309\u94AE",
1886
- thinkEnableDesc: "\u5728\u6BCF\u5F20\u601D\u8003\u5361\u7684\u300CThink\u300D\u53F3\u4FA7\u663E\u793A\u4E00\u4E2A\u6309\u94AE\uFF0C\u70B9\u51FB\u624D\u7FFB\u8BD1\u8FD9\u6761\u601D\u8003\u6B63\u6587\uFF08\u4E0D\u4F1A\u81EA\u52A8\u7FFB\u8BD1\uFF09\uFF1B\u7FFB\u8BD1\u4E2D\u6309\u94AE\u8F6C\u5708\uFF0C\u7FFB\u597D\u540E\u5B9E\u5FC3\u9AD8\u4EAE\uFF0C\u518D\u70B9\u4E00\u4E0B\u6574\u6761\u5207\u56DE\u539F\u6587\uFF08\u6B63\u6587\u91CC\u7684\u70B9\u51FB\u4E0D\u5207\u6362\uFF09\u3002\u4EE3\u7801\u5757\u4E0E\u6298\u53E0\u6458\u8981\u4E0D\u52A8\uFF0CBing \u901A\u9053\u4E0D\u53C2\u4E0E\u3002\u5173\u95ED\u6B64\u5F00\u5173\u4F1A\u64A4\u6389\u6309\u94AE\u5E76\u8FD8\u539F\u5DF2\u7FFB\u8BD1\u5185\u5BB9\u3002",
1929
+ thinkEnableDesc: "\u5728\u5C55\u5F00\u7684\u601D\u8003\u5361\u7684\u300CThink\u300D\u53F3\u4FA7\u663E\u793A\u4E00\u4E2A\u6309\u94AE\uFF0C\u70B9\u51FB\u624D\u7FFB\u8BD1\u8FD9\u6761\u601D\u8003\u6B63\u6587\uFF08\u4E0D\u4F1A\u81EA\u52A8\u7FFB\u8BD1\uFF09\uFF1B\u6298\u53E0\u8D77\u6765\u7684\u5361\u7247\u6CA1\u6709\u6309\u94AE\uFF0C\u5C55\u5F00\u540E\u6309\u94AE\u624D\u51FA\u73B0\u3002\u7FFB\u8BD1\u4E2D\u6309\u94AE\u8F6C\u5708\uFF0C\u7FFB\u597D\u540E\u5B9E\u5FC3\u9AD8\u4EAE\uFF0C\u518D\u70B9\u4E00\u4E0B\u6574\u6761\u5207\u56DE\u539F\u6587\uFF08\u6B63\u6587\u91CC\u7684\u70B9\u51FB\u4E0D\u5207\u6362\uFF09\u3002\u4EE3\u7801\u5757\u4E0E\u6298\u53E0\u6458\u8981\u4E0D\u52A8\uFF0CBing \u901A\u9053\u4E0D\u53C2\u4E0E\u3002\u5173\u95ED\u6B64\u5F00\u5173\u4F1A\u64A4\u6389\u6309\u94AE\u5E76\u8FD8\u539F\u5DF2\u7FFB\u8BD1\u5185\u5BB9\u3002",
1887
1930
  thinkTimeoutTitle: "\u601D\u8003\u94FE\u7FFB\u8BD1\u8D85\u65F6",
1888
1931
  thinkTimeoutDesc: "\u5355\u4F4D\u6BEB\u79D2\uFF0C\u8303\u56F4 500-900000\u3002\u6574\u5757\u601D\u8003\u53EF\u80FD\u8981\u51E0\u5206\u949F\uFF0C\u9ED8\u8BA4 600000\u3002",
1889
1932
  bingTitle: "Bing \u7F51\u9875\u7FFB\u8BD1\uFF08\u514D Key \u515C\u5E95\uFF09",
@@ -1903,7 +1946,7 @@ var en = {
1903
1946
  title: "Chat translate",
1904
1947
  masterTitle: "Translation master switch",
1905
1948
  enableTranslation: "Enable translation",
1906
- masterDesc: "Translates tool-call titles in the current session (e.g. {example}) into Chinese; click a translation to switch back to the original in place. Think text stays untranslated by default and is controlled by \u201Cshow think-chain translate button\u201D in the AI card: once on, a button appears at the right of every think card and translates only when clicked. The collapsed summary line of a think card is never translated. Applies to the session you are viewing only; reply text is never translated.",
1949
+ masterDesc: "Translates tool-call titles in the current session (e.g. {example}) into Chinese; click a translation to switch back to the original in place. Think text stays untranslated by default and is controlled by \u201Cshow think-chain translate button\u201D in the AI card: once on, a button appears at the right of every expanded think card and translates only when clicked. The collapsed summary line of a think card is never translated. Applies to the session you are viewing only; reply text is never translated.",
1907
1950
  aiTitle: "AI translation (OpenAI-compatible)",
1908
1951
  enableAi: "Enable the AI translation channel",
1909
1952
  badgeConfigured: "Configured",
@@ -1927,7 +1970,7 @@ var en = {
1927
1970
  testOk: "Connected, {latency}ms latency",
1928
1971
  testFail: "Failed: {error}",
1929
1972
  thinkEnableTitle: "Show the think-chain translate button",
1930
- thinkEnableDesc: "Shows a button to the right of \u201CThink\u201D on every think card; it translates that think text only when clicked, never automatically. While translating, the button spins; once done it highlights solid, and clicking it again switches the whole block back to the original (clicks inside the text do not switch). Code blocks and the collapsed summary stay untouched, and the Bing channel is not involved. Turning this off removes the button and restores already translated content.",
1973
+ thinkEnableDesc: "Shows a button to the right of \u201CThink\u201D on every expanded think card; it translates that think text only when clicked, never automatically. Collapsed cards carry no button; it appears once the card is expanded. While translating, the button spins; once done it highlights solid, and clicking it again switches the whole block back to the original (clicks inside the text do not switch). Code blocks and the collapsed summary stay untouched, and the Bing channel is not involved. Turning this off removes the button and restores already translated content.",
1931
1974
  thinkTimeoutTitle: "Think-chain translation timeout",
1932
1975
  thinkTimeoutDesc: "In milliseconds, range 500-900000. A whole think block can take minutes; default 600000.",
1933
1976
  bingTitle: "Bing web translation (key-free fallback)",