@lynn123411/dsh-chat-translate 1.3.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +14 -9
  2. package/docs//351/205/215/347/275/256.md +11 -1
  3. package/lib/client.js +1527 -489
  4. package/lib/client.js.map +4 -4
  5. package/lib/index.js +565 -173
  6. package/lib/index.js.map +4 -4
  7. package/lib/types/client/index.d.ts +7 -5
  8. package/lib/types/client/locales.d.ts +51 -4
  9. package/lib/types/client/reading-settings-page.d.ts +1 -1
  10. package/lib/types/client/settings/store.d.ts +17 -4
  11. package/lib/types/client/settings/ui.d.ts +5 -3
  12. package/lib/types/client/translate/api.d.ts +13 -0
  13. package/lib/types/client/translate/client-cache.d.ts +6 -1
  14. package/lib/types/client/translate/hold.d.ts +135 -0
  15. package/lib/types/client/translate/lazy.d.ts +8 -0
  16. package/lib/types/client/translate/mount.d.ts +22 -0
  17. package/lib/types/client/translate/observer.d.ts +57 -0
  18. package/lib/types/client/translate/think-button.d.ts +19 -0
  19. package/lib/types/client/translate/think.d.ts +55 -0
  20. package/lib/types/client/translate/viewport-observer.d.ts +14 -2
  21. package/lib/types/index.d.ts +11 -18
  22. package/lib/types/server/adapters/base.d.ts +1 -1
  23. package/lib/types/server/adapters/openai.d.ts +2 -2
  24. package/lib/types/server/cache.d.ts +5 -1
  25. package/lib/types/server/config.d.ts +35 -7
  26. package/lib/types/server/dispatcher.d.ts +30 -3
  27. package/lib/types/server/pipeline/mask-tokens.d.ts +54 -48
  28. package/lib/types/server/pipeline/masking.d.ts +35 -12
  29. package/lib/types/server/pipeline/think.d.ts +49 -0
  30. package/lib/types/server/router.d.ts +24 -3
  31. package/lib/types/server/types.d.ts +18 -1
  32. package/package.json +23 -10
@@ -3,10 +3,11 @@ export declare const name = "dsh-chat-translate";
3
3
  /**
4
4
  * Declared services: slots for the shared 「阅读体验」 settings page (page claim
5
5
  * plus the card registration), settingsScope for the per-namespace settings
6
- * mirror, and the remote + remote.credentials pair for the credentials Remote
7
- * namespace — the runtime withholds any service not declared here, so
8
- * `ctx.remote.credentials` would be undefined otherwise. The locale service is
9
- * optional and read through `ctx.get`, like the renderer's slots registry.
6
+ * mirror, the remote + remote.credentials pair for the credentials Remote
7
+ * namespace, and locale for the card's own dictionary. The runtime withholds any
8
+ * service not declared here; `locale` is declared rather than sampled because
9
+ * `ctx.get('locale')` at apply time can beat the locale plugin's `provide`,
10
+ * which silently left the card's dictionary unregistered.
10
11
  */
11
12
  export declare const inject: string[];
12
13
  interface ClientContext {
@@ -20,7 +21,7 @@ interface ClientContext {
20
21
  };
21
22
  }
22
23
  /**
23
- * Mount the tool-call / think-summary translation observer and the settings UI
24
+ * Mount the tool-call / think-chain translation observer and the settings UI
24
25
  * card inside the shared reading-settings page.
25
26
  * @param ctx - DSH browser client context.
26
27
  */
@@ -30,5 +31,6 @@ export { setupSettingsUi } from './settings/ui.tsx';
30
31
  export { NonDestructiveTranslationMount } from './translate/mount.ts';
31
32
  export { StreamDebounceViewportObserver } from './translate/viewport-observer.ts';
32
33
  export { clientCache } from './translate/client-cache.ts';
34
+ export { rowHold } from './translate/hold.ts';
33
35
  export { lazyQueue } from './translate/lazy.ts';
34
36
  export { settingsStore } from './settings/store.ts';
@@ -1,12 +1,59 @@
1
1
  /** Locale bundles for the chat-translate card inside the shared 「阅读体验」 page. */
2
2
  /** Dictionary namespace owned by this plugin's settings card. */
3
3
  export declare const NS = "settings.chatTranslate";
4
+ /**
5
+ * Chinese copy — the single source of truth for the key set. `{name}` markers
6
+ * are either template params filled through `t(key, params)` / `.replace()`, or
7
+ * inline technical samples rendered by `withSamples` at the call site.
8
+ */
9
+ export declare const zh: {
10
+ readonly pageNav: "阅读体验";
11
+ readonly title: "聊天翻译";
12
+ readonly masterTitle: "翻译总开关";
13
+ readonly enableTranslation: "启用翻译";
14
+ readonly masterDesc: "自动将当前会话中工具调用标题(如 {example})翻译为中文,点击译文可原地切换原文/译文。思考正文默认不翻译,由 AI 卡片里的「显示思考链翻译按钮」控制:打开后展开的思考卡右侧出现按钮,点击才翻译。思考卡折叠起来的那一行摘要永不翻译。仅作用于当前查看的会话,回答正文永不翻译。";
15
+ readonly aiTitle: "AI 翻译(OpenAI 兼容协议)";
16
+ readonly enableAi: "启用 AI 翻译通道";
17
+ readonly badgeConfigured: "已配置";
18
+ readonly badgeUnconfigured: "未配置";
19
+ readonly aiFallbackNote: "未配置时 AI 通道自动跳过,由 Bing 兜底。";
20
+ readonly apiKeyTitle: "API Key";
21
+ readonly apiKeyDesc: "保存至 {home} 的 {ref},保存后立即生效;留空保存 = 清除";
22
+ readonly apiKeyPlaceholderConfigured: "已配置(如需更换请直接输入)";
23
+ readonly save: "保存";
24
+ readonly saving: "保存中…";
25
+ readonly keySaved: "已保存到 {home},立即生效";
26
+ readonly keyCleared: "已清除 API Key(AI 通道将不可用,Bing 兜底)";
27
+ readonly saveFailed: "保存失败:{error}";
28
+ readonly unknownError: "未知错误";
29
+ readonly baseUrlTitle: "Base URL";
30
+ readonly baseUrlDesc: "任意 OpenAI 兼容服务端点,如 {url1}、{url2}";
31
+ readonly modelTitle: "模型";
32
+ readonly modelDesc: "如 {model1}、{model2};留空视为未配置";
33
+ readonly test: "测试 AI 通道";
34
+ readonly testing: "测试中…";
35
+ readonly testOk: "连接正常,延迟 {latency}ms";
36
+ readonly testFail: "失败:{error}";
37
+ readonly thinkEnableTitle: "显示思考链翻译按钮";
38
+ readonly thinkEnableDesc: "在展开的思考卡的「Think」右侧显示一个按钮,点击才翻译这条思考正文(不会自动翻译);折叠起来的卡片没有按钮,展开后按钮才出现。翻译中按钮转圈,翻好后实心高亮,再点一下整条切回原文(正文里的点击不切换)。代码块与折叠摘要不动,Bing 通道不参与。关闭此开关会撤掉按钮并还原已翻译内容。";
39
+ readonly thinkTimeoutTitle: "思考链翻译超时";
40
+ readonly thinkTimeoutDesc: "单位毫秒,范围 500-900000。整块思考可能要几分钟,默认 600000。";
41
+ readonly bingTitle: "Bing 网页翻译(免 Key 兜底)";
42
+ readonly enableBing: "启用 Bing 翻译通道";
43
+ readonly bingDesc: "内置免 Key 翻译通道,无需任何配置,只翻译工具调用标题。AI 未配置或请求失败时自动兜底;思考链翻译永远不走这条通道。AI 与 Bing 同时关闭则工具调用标题也不翻译。";
44
+ readonly behaviorTitle: "通道行为";
45
+ readonly behavior1: "AI 开启且已配置 → AI 优先翻译,失败自动降级 Bing";
46
+ readonly behavior2: "AI 开启但未配置 + Bing 开启 → 由 Bing 翻译";
47
+ readonly behavior3: "AI 开启但未配置 + Bing 关闭 → 不翻译";
48
+ readonly behavior4: "AI 关闭 + Bing 开启 → 直接使用 Bing";
49
+ readonly behavior5: "AI 关闭 + Bing 关闭 → 不翻译";
50
+ readonly concurrencyTitle: "最大翻译并发数";
51
+ readonly concurrencyDesc: "控制视口滚动与多工具卡片时的最大并行请求数(范围 1-100,推荐 3)。";
52
+ };
4
53
  /** Locale keys this plugin renders. */
5
- export type ChatTranslateLocaleKey = 'pageNav' | 'title';
6
- /** English copy. */
54
+ export type ChatTranslateLocaleKey = keyof typeof zh;
55
+ /** English copy; the same key set as {@link zh}. */
7
56
  export declare const en: Record<ChatTranslateLocaleKey, string>;
8
- /** Chinese copy. */
9
- export declare const zh: Record<ChatTranslateLocaleKey, string>;
10
57
  declare module '@deepseek-ai/dsh-client-ui-slots' {
11
58
  interface LocaleNamespaceMap {
12
59
  'settings.chatTranslate': ChatTranslateLocaleKey;
@@ -19,7 +19,7 @@
19
19
  * mounted but hidden, so a card's local state survives a tab switch.
20
20
  *
21
21
  * Keep this file identical across the participating plugins
22
- * (`dsh-smooth-stream`, `dsh-oil-sticky-prompt`, `dsh-chat-translate`).
22
+ * (`dsh-smooth-stream`, `dsh-chat-translate`).
23
23
  * Participants own their own card component, locale dictionaries, settings
24
24
  * namespace and Host half — only the page shell below is shared, because
25
25
  * cross-plugin value imports are forbidden by the client bundle purity gate.
@@ -3,10 +3,15 @@ export interface ClientSettingsState {
3
3
  concurrency: number;
4
4
  aiEnabled: boolean;
5
5
  bingEnabled: boolean;
6
+ thinkEnabled: boolean;
7
+ thinkTimeoutMs: number;
6
8
  baseUrl: string;
7
9
  model: string;
8
10
  aiConfigured: boolean;
9
11
  }
12
+ /** 思考链翻译超时的取值范围,与宿主半边保持一致。 */
13
+ export declare const THINK_TIMEOUT_MIN = 500;
14
+ export declare const THINK_TIMEOUT_MAX = 900000;
10
15
  /** Settings namespace + credentials ref, mirroring the host constants. */
11
16
  export declare const SETTINGS_NAMESPACE = "dsh-chat-translate";
12
17
  export declare const TRANSLATE_API_KEY_REF = "TRANSLATE_API_KEY";
@@ -16,6 +21,14 @@ export declare const TRANSLATE_API_KEY_REF = "TRANSLATE_API_KEY";
16
21
  * `credentials` Remote namespace. Keeping them structural keeps this bundle
17
22
  * free of host-service imports and lets tests inject fakes.
18
23
  */
24
+ export type SettingsPathOpLike = {
25
+ op: 'set';
26
+ path: string[];
27
+ value: unknown;
28
+ } | {
29
+ op: 'unset';
30
+ path: string[];
31
+ };
19
32
  export interface SettingsScopeLike {
20
33
  getSnapshot(): {
21
34
  status: string;
@@ -23,8 +36,7 @@ export interface SettingsScopeLike {
23
36
  writable: boolean;
24
37
  };
25
38
  subscribe(listener: () => void): () => void;
26
- set(field: string, value: unknown): Promise<unknown>;
27
- unset(field: string): Promise<unknown>;
39
+ mutate(ops: readonly SettingsPathOpLike[]): Promise<unknown>;
28
40
  }
29
41
  /** Shape of every DSH client Remote call: {ok, value} / {ok, error} wrapper. */
30
42
  export type RemoteResult<T> = {
@@ -52,8 +64,9 @@ export interface CredentialsRemoteLike {
52
64
  *
53
65
  * Since 1.2 there is no localStorage overlay and no custom config HTTP
54
66
  * endpoint: the store derives from the bound `settingsScope` (which mirrors
55
- * the host document and folds every write answer back), and writes go
56
- * through `scope.set` — serialized, revision-checked, persisted by DSH.
67
+ * the host document and folds every write answer back), and each debounced
68
+ * flush commits its touched fields as one `scope.mutate` batch — one
69
+ * revision fence, serialized and persisted by DSH.
57
70
  * Without a bound scope (e.g. non-loopback pages, unit tests) it degrades to
58
71
  * an in-memory store with the same semantics DSH itself uses for memory
59
72
  * persistence.
@@ -1,13 +1,15 @@
1
1
  import * as React from 'react';
2
- import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
+ import { NS } from '../locales.ts';
3
4
  /**
4
5
  * Chat-translate's card inside the shared 「阅读体验」 settings page. The card
5
6
  * keeps its own chrome (title rows, switches, credential form); the shared page
6
7
  * only stacks the participants' cards.
7
- * @param _props - renderer-bound seat of the `reading.settings.item` slot.
8
+ * @param props - renderer-bound seat of the `reading.settings.item` slot plus
9
+ * the `t` seat its `locale: NS` registration injects.
8
10
  * @returns The card element.
9
11
  */
10
- export declare function TidySettingsPanel(_props: PropsRuntime<'reading.settings.item'>): React.ReactElement;
12
+ export declare function TidySettingsPanel(props: PropsRuntime<'reading.settings.item'> & PropsLocale<typeof NS>): React.ReactElement;
11
13
  /**
12
14
  * Bind the settings store to DSH's native settings/credentials services and
13
15
  * join the shared 「阅读体验」 settings page: whichever participant activates
@@ -8,7 +8,20 @@ export interface TranslateBatchOptions {
8
8
  signal?: AbortSignal;
9
9
  timeoutMs?: number;
10
10
  }
11
+ /** 思考正文一个块的翻译结果,与请求的块列表按下标对齐。 */
12
+ export interface ThinkBlockResult {
13
+ original: string;
14
+ translated: string;
15
+ ok: boolean;
16
+ cached: boolean;
17
+ channel: string;
18
+ }
11
19
  export declare function requestTranslateBatch(texts: string[], options?: TranslateBatchOptions): Promise<TranslateItemResult[]>;
20
+ /**
21
+ * 请求翻译思考正文的块。宿主侧按输入上限打包、串行发送,客户端超时交给
22
+ * 宿主(思考块可能要几分钟),这里不设自己的截止时间。
23
+ */
24
+ export declare function requestTranslateThink(blocks: string[]): Promise<ThinkBlockResult[]>;
12
25
  export declare function testServerChannel(channel: string): Promise<{
13
26
  ok: boolean;
14
27
  latencyMs: number;
@@ -2,7 +2,9 @@ export declare class ClientCache {
2
2
  private memCache;
3
3
  private dirty;
4
4
  private saveTimer;
5
- constructor();
5
+ private storageKey;
6
+ private maxEntries;
7
+ constructor(storageKey?: string, maxEntries?: number);
6
8
  private load;
7
9
  get(text: string): string | undefined;
8
10
  set(text: string, translated: string): void;
@@ -11,4 +13,7 @@ export declare class ClientCache {
11
13
  clear(): void;
12
14
  size(): number;
13
15
  }
16
+ /** 工具标题译文缓存。 */
14
17
  export declare const clientCache: ClientCache;
18
+ /** 思考正文译文缓存:独立存储键与容量,不挤占工具标题的缓存。 */
19
+ export declare const thinkClientCache: ClientCache;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * 扣留队列:工具调用 / 命令行在标题译文就绪之前整行不可见,原文不落屏。
3
+ *
4
+ * 扣留按阅读顺序放行。队首那一行还在等译文时,排在它后面的每个流程条目整条不可见
5
+ * 并挂上 {@link REVEAL_HOLD_ATTRIBUTE}:标记让 smooth-stream 不再推进条目里的逐字
6
+ * 揭示(积压原样留着),`display: none` 让条目里的任何东西都上不了屏——不翻译的行、
7
+ * 宿主直接落盘的文本、已经逐字reveal了一半的正文,都得等轮到它。队首一就绪,它先出现
8
+ * (译文挂载 + 整行对数淡入),再轮到下一条。没有这层顺序,靠后的行会先占住位置,等
9
+ * 靠前的行出现时再被顶下去。
10
+ *
11
+ * 藏法是整条 `display: none`,不是「占着位置但透明」:条目一旦进入布局就带上了
12
+ * 会话流的 16px 兄弟间距(`.column > :not(:empty) ~ :not(:empty)`),积压几行就是
13
+ * 屏幕底部一条几十像素的空白,且这空白要等轮到它才被填上。移除整个盒子,等待期间
14
+ * 的占位就是零。
15
+ *
16
+ * 放行是一步一行:队首解锁时排在它后面的行可能已经积了一长串(工具行、思考行都在
17
+ * 继续产出),一次全放会让整屏同时淡入。所以解锁的行先进 {@link RowHoldQueue} 的
18
+ * 放行队列,由定时器逐行吐出,待放行越多步长越短(见 RELEASE_BUDGET_MS),既不
19
+ * 突兀也不会让长队拖住屏幕。
20
+ *
21
+ * 等待有上限:超过 HOLD_DEADLINE_MS 仍无译文就放行原文,译文之后到达时再原地挂载。
22
+ */
23
+ /** 一行最多被扣留多久,到点放行原文。 */
24
+ export declare const HOLD_DEADLINE_MS = 5000;
25
+ /**
26
+ * 挂起标记:打在会话流条目(`[data-chat-flow-key]`)上,表示「这条还没轮到上屏」。
27
+ * smooth-stream 读它来暂停条目内的揭示引擎,字面量在两边各写一份
28
+ * (`src/client/revealHold.ts`)。
29
+ */
30
+ export declare const REVEAL_HOLD_ATTRIBUTE = "data-dsh-reveal-hold";
31
+ export interface RowHoldEntry {
32
+ /** 会话流条目:扣留的边界,挂在它上面的标记决定后面哪些条目被挂起。 */
33
+ item: HTMLElement;
34
+ /** 被翻译的摘要 span;React 可能在扣留期间换掉它。工具行本身不翻译时为 null。 */
35
+ span: HTMLElement | null;
36
+ /** 行根(`[data-variant]` 卡片)。它只用来辨认「同一行」是否已经入队。 */
37
+ root: HTMLElement;
38
+ /** 当前正在等待译文的原文。 */
39
+ text: string;
40
+ /** 拿到的译文;null 表示这一行确定没有译文可等(失败 / 超时)。 */
41
+ translation: string | null;
42
+ /** 译文(或失败判定)已就绪,只等排到队首。没有译文可等的行入场即就绪。 */
43
+ ready: boolean;
44
+ timer: number | null;
45
+ }
46
+ declare class RowHoldQueue {
47
+ /** 按阅读顺序排列的扣留行。 */
48
+ private entries;
49
+ /** 结果回来时只知道 span,不知道条目,所以另留一份索引。 */
50
+ private bySpan;
51
+ /** 当前挂起着的流程条目 → 隐藏之前它自己的行内 display。 */
52
+ private marked;
53
+ /** 已轮到上屏、正在按阅读顺序逐行吐出的条目。 */
54
+ private releasing;
55
+ private releaseTimer;
56
+ /**
57
+ * 隐藏整行并启动放行上限。幂等:同一行在途请求期间文本又变了时只更新文本并
58
+ * 重新等待,不重置不可见窗口。
59
+ */
60
+ hold(span: HTMLElement, root: HTMLElement, text: string): void;
61
+ /** 该文本所属行的扣留状态;返回 undefined 表示没有扣留。 */
62
+ stateFor(span: HTMLElement): RowHoldEntry | undefined;
63
+ /**
64
+ * 译文(或失败判定)就绪。真正上屏要等它排到队首:入队顺序就是阅读顺序。
65
+ * @param span - 等待中的摘要 span。
66
+ * @param translation - 译文;null 表示这一行没有译文可等,放行原文。
67
+ */
68
+ ready(span: HTMLElement, translation: string | null): void;
69
+ /**
70
+ * 扣留期间新出现的会话流内容:排在队首后面的一律先不上屏。
71
+ *
72
+ * 藏的是整个流程条目,不是某一张卡片:条目里的东西有的归揭示引擎管(回答正文、
73
+ * 思考正文、译文,标记会让它们只积压不推进),有的宿主直接落盘(工具卡片正文、
74
+ * 不翻译的读写文件行),只冻结揭示挡不住后者。整条藏起来两种都覆盖,而且条目
75
+ * 本身在 React 重渲染里不会被换掉,卡片被换掉也照样藏得住。
76
+ * @param node - 这次变更里新增的元素,或其祖先所在的那一片子树。
77
+ */
78
+ noteAppended(node: HTMLElement): void;
79
+ /** 挂起一个条目:整条不可见,并且不推进它里面的揭示。 */
80
+ private holdItem;
81
+ private follows;
82
+ /**
83
+ * 藏住整条流程条目。用 `display: none` 而不是 `opacity`/`visibility`:条目内部的
84
+ * 节点可以显式声明 `visibility: visible`(系统提示卡片就是这么自管的),那样会从
85
+ * 隐藏的祖先里逃出来;而且只要条目还参与布局,会话流的 16px 兄弟间距就还在——每
86
+ * 一条被扣留的行都会在屏幕底部留一段等宽的空白。整个盒子移除,等待期间占位为零,
87
+ * 轮到它时再连同入场淡入一起撑开。
88
+ */
89
+ private mark;
90
+ /** 解除挂起;`animate` 为真时补一段行入场淡入,避免内容一下子蹦出来。 */
91
+ private unmark;
92
+ /**
93
+ * 排进放行队列。按阅读顺序插入,所以逐行吐出的顺序就是行在流里的顺序。
94
+ */
95
+ private enqueueRelease;
96
+ /**
97
+ * 取出下一个还连在文档里的待放行条目。
98
+ * @returns 待放行条目;队列空或只剩已断开节点时返回 undefined。
99
+ */
100
+ private takeRelease;
101
+ /** 队伍越长步长越短:几行时让淡入跑完,几十行时也不至于让读者干等。 */
102
+ private releaseStepMs;
103
+ /**
104
+ * 逐行放行:每一步只让一行出现,然后按当时的队伍长度安排下一步。队首(刚解锁
105
+ * 的那一行)走的是同步的那一步,所以它不会比译文晚一帧上屏。
106
+ */
107
+ private pumpRelease;
108
+ /** 丢掉逐行放行的进度(关闭、换会话、断开观察器时用)。 */
109
+ private clearReleases;
110
+ /** 按阅读顺序入队:入队顺序就是上屏顺序。 */
111
+ private insert;
112
+ /** 译文(或超时判定)落地,交给队首判定。 */
113
+ private readyEntry;
114
+ /**
115
+ * 按顺序解锁:队首就绪就挂译文并把它交给逐行放行,然后看新的队首。译文在条目
116
+ * 仍是 `display: none` 时挂上,所以屏幕上不会出现完整中文被画一帧再被截断的闪烁;
117
+ * 真正撑开这一行由 {@link pumpRelease} 执行。
118
+ */
119
+ private drain;
120
+ /**
121
+ * 让挂起标记与队首对齐:队首自己以及排在它后面的每个流程条目都藏起来,其余
122
+ * 交给逐行放行。放行让队首前移时,之前被它挡住的条目就在这一步进入放行队列。
123
+ *
124
+ * 没有「立即恢复」的旁路:放行一律排队逐行吐出,所以放行途中新到的队首不会把
125
+ * 正在等的那些行一次性抖出来(`releaseAll` 是唯一的立即路径)。
126
+ */
127
+ private sync;
128
+ /**
129
+ * 全部放行:断开观察器与关闭总开关时用。批量放行不做入场动画,也不排队,
130
+ * 否则整屏会一起闪一遍、或者留下一个跑不完的定时器。
131
+ */
132
+ releaseAll(): void;
133
+ }
134
+ export declare const rowHold: RowHoldQueue;
135
+ export {};
@@ -4,7 +4,15 @@ declare class LazyTranslationQueue {
4
4
  constructor();
5
5
  setEnabled(enabled: boolean): void;
6
6
  observe(element: HTMLElement, text: string, immediate?: boolean): void;
7
+ /**
8
+ * 扣留行的取文本路径。命中缓存时同步就绪(没有需要遮住的等待),否则走无可见
9
+ * 性判定的防抖批次;译文挂载由扣留队列在轮到这一行时执行,好让逐字与对数淡入
10
+ * 从它真正上屏的那一刻开始。
11
+ */
12
+ observeHeld(element: HTMLElement, text: string): void;
7
13
  private handleVisibleBatch;
14
+ /** 元素当前承载的原文是否仍是这次结果对应的那一份。 */
15
+ private stillMatches;
8
16
  private applyTranslation;
9
17
  disconnect(): void;
10
18
  }
@@ -11,6 +11,8 @@
11
11
  export declare const CLASS_ORIGINAL_HIDDEN = "dsh-tidy-original-hidden";
12
12
  export declare const CLASS_ORIGINAL_SHOWN = "dsh-tidy-original-shown";
13
13
  export declare const CLASS_TRANSLATED_BLOCK = "dsh-tidy-translated-block";
14
+ /** 混合块里的行内片段容器(我们自建、自拆)。 */
15
+ export declare const CLASS_RUN = "dsh-tidy-run";
14
16
  export interface MountOptions {
15
17
  originalText?: string;
16
18
  interactive?: boolean;
@@ -20,6 +22,26 @@ export declare class NonDestructiveTranslationMount {
20
22
  * Mounts a translated string onto the target element non-destructively.
21
23
  */
22
24
  static mount(element: HTMLElement, translated: string, options?: MountOptions): void;
25
+ /**
26
+ * 把一段连续的行内节点包进自建容器后挂载译文。
27
+ *
28
+ * 混合块(列表项文字 + 子列表 / 代码块)里的行内片段没有自己的元素,
29
+ * 只能先包一层再走同一条非侵入式挂载;嵌套的块级子节点留在容器之外,
30
+ * 排版原样保留。
31
+ * @returns 自建容器;节点不再相邻或没有共同父节点时返回 null。
32
+ */
33
+ static mountRun(nodes: Node[], translated: string, options?: MountOptions): HTMLElement | null;
34
+ /** 还原行内片段并撤掉自建容器。 */
35
+ static unmountRun(run: HTMLElement): void;
36
+ /** 还原一个作用域内我们挂过的全部译文(元素单位与行内片段都算)。 */
37
+ static restore(scope: ParentNode): void;
38
+ /** 我们自己的挂载容器与还原容器,观察器必须跳过它们。 */
39
+ static isOwnNode(element: HTMLElement): boolean;
40
+ /**
41
+ * 把一个作用域里的全部挂载译文一起翻面。思考链只由卡片标题右侧的按钮调用
42
+ * 它,正文里的点击不再改变原文与译文。
43
+ */
44
+ static toggleGroup(scope: ParentNode, showOriginal: boolean): void;
23
45
  /**
24
46
  * Unmounts translation and restores original DOM nodes completely.
25
47
  */
@@ -3,9 +3,35 @@ export declare class ChatTranslateObserver {
3
3
  private rootElement;
4
4
  private rootCheckTimer;
5
5
  private isEnabled;
6
+ /** 设置里的思考链开关;与总开关一起决定按钮是否存在。 */
7
+ private thinkEnabled;
8
+ /**
9
+ * 至少一条工具标题通道可用(AI 配置齐全或 Bing 打开)。两条都关时没有
10
+ * 可等的译文,扣留立即失去意义。
11
+ */
12
+ private channelsAvailable;
13
+ /**
14
+ * 每个会话流已经见过的末尾行。追加在它之后的行算「新出现」,插在它之前
15
+ * 的行(历史回放、加载更早)不算。
16
+ */
17
+ private flowTail;
18
+ /** 初始扫描期间不扣留:那时看到的行都是历史。 */
19
+ private suppressHold;
6
20
  constructor();
7
21
  setEnabled(enabled: boolean): void;
8
22
  private restoreOriginals;
23
+ /**
24
+ * 设置里的思考链开关:关掉就撤掉所有翻译按钮并还原思考卡里的译文,工具标题
25
+ * 译文不动。
26
+ */
27
+ setThinkEnabled(enabled: boolean): void;
28
+ /**
29
+ * AI 通道的可用性(通道开关打开且 Key / Base URL / 模型齐全)。不可用时同样
30
+ * 不注入按钮:思考链翻译只能走这条通道。
31
+ */
32
+ setThinkConfigured(configured: boolean): void;
33
+ /** 工具标题通道可用性:两条都关时不扣留,也没有译文可等。 */
34
+ setChannelsAvailable(available: boolean): void;
9
35
  /**
10
36
  * The active session's scroll container. Only the currently-viewed session
11
37
  * is translated; switching sessions replaces this subtree and the observer
@@ -17,9 +43,40 @@ export declare class ChatTranslateObserver {
17
43
  private scheduleRootCheck;
18
44
  private restart;
19
45
  private handleMutations;
46
+ /**
47
+ * 扣留期间新出现的会话流内容(下一个回答节点、下一个工具行)同样要先挂起:
48
+ * 它们排在还没上屏的行后面,先出现就会占住那一行的位置。
49
+ */
50
+ private noteRevealHold;
51
+ /**
52
+ * 变更发生在我们移进隐藏容器的原文里时,返回挂载了译文的那个元素。React
53
+ * 就地改写它自己创建的文本节点,所以「原文变了」只能从这条路径看到。
54
+ */
55
+ private mountedOwner;
56
+ /**
57
+ * 已挂载行的隐藏原文被上游改写了(ask_user 计数、失败摘要、todo 计数):
58
+ * 命中缓存就原地换译文,否则请求新译文后原地换。行始终可见,不出现英文。
59
+ */
60
+ private handleOriginalTextChange;
61
+ /**
62
+ * 该行是否该被扣留:当前 turn 仍在产出、这行是新追加到流末尾的、不是后缀、
63
+ * 通道可用,且不在初始扫描期间。判断顺带把每个会话流见过的末尾行推进到
64
+ * 最新,供后续判定使用;第一次见到的会话流先按历史处理。
65
+ */
66
+ private shouldHold;
20
67
  private scanContainer;
21
68
  private scanNode;
69
+ /**
70
+ * 交给思考正文翻译控制器:命中节点所在的卡片,以及它下面(或它就是)的
71
+ * 全部思考卡片。
72
+ */
73
+ private scanThink;
22
74
  private processSpan;
75
+ /**
76
+ * 扣留行的取文本路径:命中缓存立即就绪,其余交给懒加载队列。译文真正挂载要等
77
+ * 队列轮到它——提前挂上去会把逐字与对数淡入在被挂起期间跑完。
78
+ */
79
+ private translateHeld;
23
80
  disconnect(): void;
24
81
  }
25
82
  export declare const chatTranslateObserver: ChatTranslateObserver;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 思考卡标题右侧的翻译按钮。
3
+ *
4
+ * 三态:idle(描边「译」字,点击开始翻译)、working(圈内圆环旋转,点击忽略)、
5
+ * translated(实心高亮,点击整条切回原文)。样式只在首次使用注入一次,配色沿用
6
+ * 内核的 dsw-* 变量;系统开启「减弱动态效果」时圆环不旋转。
7
+ */
8
+ export type ThinkButtonState = 'idle' | 'working' | 'translated';
9
+ export declare const THINK_BUTTON_CLASS = "dsh-tidy-think-button";
10
+ /** 注入按钮样式,重复调用无副作用。 */
11
+ export declare function ensureThinkButtonStyles(): void;
12
+ export interface ThinkButtonHandle {
13
+ element: HTMLButtonElement;
14
+ getState(): ThinkButtonState;
15
+ setState(state: ThinkButtonState): void;
16
+ setDisabled(disabled: boolean, reason: string): void;
17
+ }
18
+ /** 创建一个按钮实例;onActivate 由调用方决定点下去做什么。 */
19
+ export declare function createThinkButton(onActivate: () => void): ThinkButtonHandle;
@@ -0,0 +1,55 @@
1
+ /** 稳定的翻译单位:一块级元素,或一段行内节点。 */
2
+ export interface ThinkUnit {
3
+ element?: HTMLElement;
4
+ nodes?: Node[];
5
+ text: string;
6
+ anchor: Node;
7
+ }
8
+ /** 收集正文里全部可翻译单位,跳过已挂译文的部分与代码块。 */
9
+ export declare function collectThinkUnits(body: HTMLElement): ThinkUnit[];
10
+ /** 按估算 token 把单位切成若干次客户端请求,顺序不变。 */
11
+ export declare function chunkUnits(units: ThinkUnit[], maxTokens?: number): ThinkUnit[][];
12
+ declare class ThinkTranslateController {
13
+ /** 总开关与设置里的思考链开关同时打开。 */
14
+ private enabled;
15
+ /** AI 通道开关打开且已配置;缺任何一条都不注入按钮。 */
16
+ private configured;
17
+ private states;
18
+ private syncTimers;
19
+ /** 只处理当前会话的容器;由观察器在会话切换时更新。 */
20
+ private scope;
21
+ /** 观察器把当前会话的根交给控制器,插入按钮时只看这一棵子树。 */
22
+ setScope(scope: ParentNode): void;
23
+ isEnabled(): boolean;
24
+ setEnabled(enabled: boolean): void;
25
+ setConfigured(configured: boolean): void;
26
+ /** 按钮是否可以存在:两个开关与 AI 通道配置缺一不可。 */
27
+ private usable;
28
+ private applyVisibility;
29
+ /** 扫描当前会话里的全部思考卡(开关、配置或会话变化后调用)。 */
30
+ syncAll(): void;
31
+ /** 防抖后同步一张卡片。 */
32
+ syncCard(card: HTMLElement): void;
33
+ private stateFor;
34
+ private runSync;
35
+ private updateDisabled;
36
+ private ensureButton;
37
+ /** 折叠或停用时撤掉按钮,卡片的翻译状态不变。 */
38
+ private removeButton;
39
+ /** 插件停用:撤掉按钮并还原这张卡片上的全部译文。 */
40
+ private discardCard;
41
+ /** 卡片当前显示的是译文(至少有一个译文块可见)。 */
42
+ private isShowingTranslation;
43
+ private bodyOf;
44
+ /** 展开时若整条都命中缓存,直接显示中文,不点按钮也不发请求。 */
45
+ private showCachedWhenComplete;
46
+ /** 按钮被点击;按钮只在展开的卡片上存在。 */
47
+ private activate;
48
+ /** 翻译这张卡片的全部单位:命中缓存的先挂上,其余按块请求。 */
49
+ private translateCard;
50
+ private translateUnits;
51
+ /** 把译文挂到单位上;期间文本或节点已经变动的单位直接放弃(结果已进缓存)。 */
52
+ private applyUnit;
53
+ }
54
+ export declare const thinkTranslator: ThinkTranslateController;
55
+ export {};
@@ -5,6 +5,12 @@
5
5
  * 1. True IntersectionObserver for viewport visibility detection (rootMargin: '150px 0px').
6
6
  * 2. Typing/streaming debounce (default 400ms) to avoid translating partial streaming sentences.
7
7
  * 3. Batch queuing to group multiple visible elements into efficient batch requests.
8
+ *
9
+ * 扣留行({@link observeHeld})复用同一条防抖与批次,只跳过可见性判定:
10
+ * 行此刻是隐藏的,等不到 IntersectionObserver 回调。注册时再兜一道:
11
+ * 任何落在被挂起条目(`data-dsh-reveal-hold`)里的元素同样直接进批次——
12
+ * 整条 `display: none` 的条目永远不 intersecting,包括那些已经过了扣留队列、
13
+ * 正排在逐行放行队伍里的行。
8
14
  */
9
15
  export interface ViewportObserverOptions {
10
16
  rootMargin?: string;
@@ -20,7 +26,7 @@ export declare class StreamDebounceViewportObserver {
20
26
  * Pending per-element streaming debounce timers. A Map (not a WeakMap)
21
27
  * because disconnect() must enumerate and clear every pending timer: a
22
28
  * WeakMap cannot be iterated, so a debounce armed just before the switch was
23
- * turned off would still fire registerForViewport() afterwards.
29
+ * turned off would still fire register() afterwards.
24
30
  */
25
31
  private streamingTimers;
26
32
  private pendingQueue;
@@ -33,7 +39,13 @@ export declare class StreamDebounceViewportObserver {
33
39
  * If streaming updates characterData repeatedly within debounceMs, the timer resets.
34
40
  */
35
41
  observeWithDebounce(element: HTMLElement, text: string, immediate?: boolean): void;
36
- private registerForViewport;
42
+ /**
43
+ * 扣留行的取文本路径:保留同一套流式防抖,跳过可见性判定——行此刻是
44
+ * 隐藏的(永远不进入视口),放行由扣留控制器负责。
45
+ */
46
+ observeHeld(element: HTMLElement, text: string): void;
47
+ private armDebounce;
48
+ private register;
37
49
  private enqueueBatch;
38
50
  private flushQueue;
39
51
  unobserve(element: HTMLElement): void;
@@ -1,30 +1,23 @@
1
+ import type { HostConnectionHandle } from '@deepseek-ai/dsh-client-connection';
2
+ import { type SettingsMigrationTarget } from './server/config.ts';
1
3
  /** Stable Cordis loader name. */
2
4
  export declare const name = "dsh-chat-translate";
3
5
  /**
4
- * Hard dependencies: webServer serves the translation proxy; settings and
5
- * credentials are the DSH-owned config/secret surfaces this plugin now rides
6
- * on (no standalone config file since 1.2).
6
+ * Hard dependencies: settings and credentials are the DSH-owned config/secret
7
+ * surfaces this plugin rides on (no standalone config file since 1.2). The
8
+ * translation routes register through the connection service when a Web
9
+ * carrier composes it, so apply takes that service through ctx.inject and the
10
+ * plugin still loads in compositions without one.
7
11
  */
8
12
  export declare const inject: string[];
9
13
  interface HostContext {
10
- webServer?: {
11
- register(route: {
12
- kind: 'prefix' | 'exact';
13
- path: string;
14
- handler: (req: any, res: any) => Promise<void> | void;
15
- }): () => void;
16
- };
17
- settings: {
14
+ connection: HostConnectionHandle;
15
+ settings: SettingsMigrationTarget & {
18
16
  register(ns: string, schema: unknown): {
19
17
  get(): any;
20
18
  watch(listener: (config: any) => void): () => void;
21
19
  update(patch: Record<string, unknown>): Promise<unknown>;
22
20
  };
23
- describe(): Array<{
24
- ns: string;
25
- user?: unknown;
26
- }>;
27
- update(ns: string, patch: Record<string, unknown>): Promise<unknown>;
28
21
  };
29
22
  credentials: {
30
23
  resolve(ref: string): Promise<{
@@ -40,8 +33,8 @@ interface HostContext {
40
33
  unset(ref: string): Promise<void>;
41
34
  };
42
35
  on(event: string, listener: (...args: any[]) => void): () => void;
43
- effect(factory: () => void | (() => void), label: string): void;
44
- get?(serviceName: string): any;
36
+ /** Optional-service injection: the callback runs once connection is present. */
37
+ inject(names: string[], callback: (ctx: HostContext) => void | (() => void)): void;
45
38
  }
46
39
  /** Mount the host half; provides the translation proxy and rides DSH config. */
47
40
  export declare function apply(ctx: HostContext): void;