@lynn123411/dsh-chat-translate 3.1.0 → 3.2.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.
@@ -7,9 +7,12 @@
7
7
  * markdown 交给宿主公开基线组件 `MarkdownText` 重渲染:格式保真由官方
8
8
  * 渲染器负责,本文件不产生任何自拼 markup。
9
9
  * 2. 「已翻译/没译成」是渲染状态:落定、开关开启、进入视口后逐行首跑;成功
10
- * 的块(包括模型认为原样最好的块)挂译文、左缘蓝线、点击在译文与原文间
11
- * 切换;失败的块保持原文、左缘挂红实线(悬停报出败因),整块可点=手动
12
- * 整行补跑,无限次、无自动重试;已登记在途而尚无结果的块显灰脉动。
10
+ * 的块(包括模型认为原样最好的块)挂译文、左缘蓝线;失败的块保持原文、
11
+ * 左缘挂红实线;已登记在途而尚无结果的块显灰脉动。
12
+ * 3. 切换与补跑都在**左缘热区**上,不在正文上:正文整块可点会让「选中一段
13
+ * 文字复制」与「切换译文/原文」抢同一次点击;热区收成贴着那条线的窄带后,
14
+ * 正文的单击落字、拖选复制都不再误触。失败块的 ↻ 常驻在红线旁,点它整行
15
+ * 补跑,无限次、无自动重试。
13
16
  *
14
17
  * 除正文外的行内容与宿主逐分支等价:reasoning 行走折叠(含 Turn-process
15
18
  * 隐藏与 beforematch 揭示)、连续 image 组交回 owner 的 renderMessageImages、
@@ -1,15 +1,16 @@
1
1
  /**
2
- * 已翻译块的点击守卫:切换只认「落在块空白正文上的裸点击」。
2
+ * 左缘热区的点击判定:只有「按下与松手都落在热区上」的干净点击才切换/补跑。
3
3
  *
4
- * 块内含链接、代码块复制钮等交互元素——它们的点击归自己,不连带切块;
5
- * 拖选译文松手产生的 click 同样忽略。纯 DOM 判定,不依赖 React,可独立测试。
4
+ * 热区没有子元素,所以浏览器派发过来的 click 若 `target === currentTarget`,
5
+ * 就说明按下与松手都在热区里;反之(在热区按下、往右拖进正文选字再松手)click
6
+ * 会派发给两者的公共祖先,target 不是热区,天然判为拖选。这不是启发式,是
7
+ * click 事件的分派语义——正文整块不再是点击目标,误触从根上消失。
8
+ * 纯 DOM 判定,不依赖 React,可独立测试。
6
9
  */
7
10
  interface MinimalPointerEvent {
8
11
  /** 点击命中的最深元素。 */
9
- target: {
10
- closest?(selector: string): unknown;
11
- } | null;
12
- /** 挂了本守卫的已翻译块容器。 */
12
+ target: unknown;
13
+ /** 挂了本守卫的热区元素本身。 */
13
14
  currentTarget: unknown;
14
15
  }
15
16
  interface MinimalWindow {
@@ -21,7 +22,7 @@ interface MinimalWindow {
21
22
  /**
22
23
  * @param event - click 事件(结构面:target/currentTarget)。
23
24
  * @param view - 可注入的 window(测试传假对象;运行时传全局 window)。
24
- * @returns 该点击是否应当触发原文/译文切换。
25
+ * @returns 该点击是否应当触发切换/补跑。
25
26
  */
26
- export declare function isBareBlockClick(event: MinimalPointerEvent, view: MinimalWindow | undefined): boolean;
27
+ export declare function isCleanHotspotClick(event: MinimalPointerEvent, view: MinimalWindow | undefined): boolean;
27
28
  export {};
@@ -36,7 +36,7 @@ export type RowPlanEntry =
36
36
  text: string;
37
37
  }
38
38
  /**
39
- * 正文块:mark 即左缘线状态(可点性、重试指引、脉动全由它决定);
39
+ * 正文块:mark 即左缘线状态(线型、脉动、热区动作全由它决定);
40
40
  * translated 仅在成功时非 null,供 mark==='translated' 时换源文本渲染;
41
41
  * fail 仅在 mark==='failed' 时非 null——服务端的技术一句,Tooltip 悬停
42
42
  * 报因用,缺省时只报「翻译失败」。线型不从这里出:失败一律同一条红实线。
@@ -83,7 +83,8 @@ export interface RowPlanInput {
83
83
  outcomes?: readonly (BlockOutcome | null)[];
84
84
  /** 池里这行的状态:pending 时未落定的块显灰脉动,未登记什么都不显。 */
85
85
  rowStatus?: 'pending' | 'settled';
86
- /** 用户点了「读原文」的正文块(按条目 key):把译文态折成 original-view。 */
86
+ /** 用户点了「读原文」的正文块(按条目 key):把译文态折成 original-view。
87
+ * 触发它的是左缘热区上的切换,不是点正文。 */
87
88
  originalKeys?: ReadonlySet<number>;
88
89
  }
89
90
  export declare function planAssistantRow(input: RowPlanInput): RowPlan;
@@ -1,13 +1,17 @@
1
1
  /**
2
2
  * 助手行接管所需的样式:与宿主 `AssistantMarkdown` / `ReasoningRow` 逐条对齐
3
3
  * 的等价规则(字号、行距、gap、粘性折叠头、running 微光、summary 遮罩等),
4
- * 加上本插件唯一新增的视觉词汇——正文块左缘的线:
4
+ * 加上本插件唯一新增的视觉词汇——正文块左缘的线与贴着它的那条热区:
5
5
  * 蓝粗线(1px 主色)=正在读译文;灰细线(0.5px 中性)=有译文但正在读原文
6
6
  * (再点即切回);红实线(1px error 色)=翻译失败(通道伤:超时、断流、空
7
- * 返回),悬停报出细节,点它整行补跑;灰脉动=整行在途、这块尚无结果;
8
- * 无线=没送过模型(含空白块)或开关关闭。
7
+ * 返回);灰脉动=整行在途、这块尚无结果;无线=没送过模型(含空白块)或开关关闭。
9
8
  * 译文的结构漂移不再挂红线——由宿主按原文修回、重掷或照收,红线只剩通道伤一种。
10
9
  *
10
+ * 点击不落在正文上:`dsh-ct-hotspot` 是贴着左缘线的窄热区(线左侧 4px + 线
11
+ * 右侧那 12px 缩进,随块高铺满),切换与补跑都由它承载,正文只做选中/复制。
12
+ * 悬停不改任何视觉——只有鼠标在热区上变手型;键盘聚焦(且仅键盘聚焦)时线
13
+ * 加亮,粗细始终只表示译文/原文。失败块的 ↻ 常驻在红线旁,是补跑的可见入口。
14
+ *
11
15
  * 全部颜色与尺寸走 `--dsw-alias-*` / `--dsh-*` 主题别名,中性平边按官方规则
12
16
  * 画 0.5px hairline;动画声明尊重 `prefers-reduced-motion`。样式随 bundle 注入
13
17
  * 一次(`<style data-plugin-css>`),类名用固定前缀,不依赖宿主哈希类名。
@@ -19,10 +23,42 @@ export declare const ASSISTANT_CSS: string;
19
23
  * 失败只有 `failed` 一种:同一条红实线,败因走悬停文案。
20
24
  */
21
25
  export type ProseMark = 'translated' | 'original-view' | 'failed' | 'inflight' | null;
22
- /** mark → 点击动作单点:切换、重试、不可点三选一(渲染层照此接线)。 */
26
+ /** mark → 热区动作单点:切换、重试、没有热区三选一(渲染层照此接线)。 */
23
27
  export type ProseAction = 'toggle' | 'retry' | null;
28
+ /**
29
+ * mark → 正文块类名与热区动作的单点表。
30
+ *
31
+ * 一张表而不是两个并列 switch:线型(类名)与可点性(动作)从同一行读出来,
32
+ * 加一种 mark 时不可能只改一处、留下「有线但点不动」或「可点但没有线」的
33
+ * 半截状态。`action` 为 null 表示没有热区——没线的块(没送过模型、开关关闭、
34
+ * 通道没配好、空白块)没有可切换的东西。
35
+ */
36
+ interface ProsePresentation {
37
+ /** 正文块的状态类名;null 的块只有基础类。 */
38
+ className: string | null;
39
+ /** 热区动作:切换、补跑、或没有热区。 */
40
+ action: ProseAction;
41
+ }
42
+ /** 每个正文块都挂的基础类:定位父级(热区以它为基准绝对定位)。 */
43
+ export declare const PROSE_BASE_CLASS = "dsh-ct-prose";
44
+ export declare function prosePresentation(mark: ProseMark): ProsePresentation;
45
+ /** mark → 热区动作({@link prosePresentation} 的窄读法,测试与渲染层共用)。 */
24
46
  export declare function proseAction(mark: ProseMark): ProseAction;
25
- /** mark → 类名单点:可点态共用 padding 与光标,线型之间切换不跳字。 */
47
+ /**
48
+ * mark → 正文块类名单点:只有线型。点击不在这层——正文整块不可点,切换与
49
+ * 补跑由左缘热区({@link HOTSPOT_CLASS})承载,所以这里不再有可点态/光标类。
50
+ */
26
51
  export declare function proseClassNames(mark: ProseMark): string | undefined;
52
+ /**
53
+ * 缩进单点:能翻译(开关开 + 通道齐备)时所有正文块一律左缩进,与「这块是否
54
+ * 已挂线」无关——译文落定的那一刻正文不右移,块与块左缘也对齐;关掉开关或
55
+ * 通道没配好时不缩进,排版回到宿主原样(此时正文会横移一次,是刻意的取舍)。
56
+ */
57
+ export declare function proseIndentClassName(canTranslate: boolean): string | undefined;
58
+ /** 左缘热区的类名单点:切换/补跑的唯一点击目标。 */
59
+ export declare const HOTSPOT_CLASS = "dsh-ct-hotspot";
60
+ /** 在途热区:仍在,但按下去是空操作,光标不再变手型。 */
61
+ export declare const HOTSPOT_IDLE_ATTR = "data-idle";
27
62
  /** 注入接管样式;重复调用是空操作。 */
28
63
  export declare function ensureAssistantStyles(): void;
64
+ export {};
@@ -57,7 +57,7 @@ export declare class ChatTranslateStore {
57
57
  getState(rowKey: string): RowState | undefined;
58
58
  /**
59
59
  * 确保该行按当前文本被翻译。
60
- * @param manual - 用户点击失败块发起的补跑:已落定的同代行只有这条路会重发。
60
+ * @param manual - 用户点击失败块的左缘热区发起的补跑:已落定的同代行只有这条路会重发。
61
61
  * @returns 是否发出了新的请求。
62
62
  */
63
63
  ensure(rowKey: string, texts: readonly string[], manual?: boolean): boolean;
@@ -11,7 +11,7 @@ export declare const zh: {
11
11
  readonly title: "正文翻译";
12
12
  readonly masterTitle: "翻译总开关";
13
13
  readonly enableTranslation: "启用翻译";
14
- readonly masterDesc: "自动把助手回答正文翻成中文(如 {example}):折叠块里的过程正文与最终汇总一视同仁,整段中文的块也照送,由模型一并改写成自然中文、顺掉机器腔。Think 卡与工具调用行不翻,代码块原样保留。正文块的左缘一条竖线报出状态:正在读译文是蓝线,备着译文正读原文是灰细线,没译成是红实线(鼠标悬停报出通道细节——超时、断流或空返回;译文的结构漂移由宿主自动修回或重掷,不再因格式问题挂红线),正在请求是灰脉动线,没送过模型或开关关闭没有线。点击已翻译的块在译文与原文间切换——模型认为原样最好、返回一模一样的文字时同样算翻译过、同样可切;点击挂红线的块则整行重新翻译,已成功的块保持显示不打扰。通道失败不自动重试,恢复后由你的点击救活。译文按原 markdown 格式重新渲染,表格、列表、加粗与行内代码保持结构。回答还在流式输出时不翻,落定后按阅读顺序逐段出中文。只作用于当前查看的会话,译文不写回会话上下文。输入框下方那一行有同款开关,两处状态同步。";
14
+ readonly masterDesc: "自动把助手回答正文翻成中文(如 {example}):折叠块里的过程正文与最终汇总一视同仁,整段中文的块也照送,由模型一并改写成自然中文、顺掉机器腔。Think 卡与工具调用行不翻,代码块原样保留。正文块的左缘一条竖线报出状态:正在读译文是蓝线,备着译文正读原文是灰细线,没译成是红实线(鼠标悬停热区报出通道细节——超时、断流或空返回;译文的结构漂移由宿主自动修回或重掷,不再因格式问题挂红线),正在请求是灰脉动线,没送过模型或开关关闭没有线。切换不在正文上:贴着那条线的窄带才是操作区,鼠标移到它上面变手型,点一下在译文与原文间切换——正文整块不再响应点击,选字、拖选复制都不会误触;模型认为原样最好、返回一模一样的文字时同样算翻译过、同样可切。挂红线的块右侧常驻一个小 ↻,点那条窄带即整行重新翻译,已成功的块保持显示不打扰。通道失败不自动重试,恢复后由你点那条窄带救活。译文按原 markdown 格式重新渲染,表格、列表、加粗与行内代码保持结构。回答还在流式输出时不翻,落定后按阅读顺序逐段出中文。只作用于当前查看的会话,译文不写回会话上下文。输入框下方那一行有同款开关,两处状态同步。";
15
15
  readonly aiTitle: "AI 翻译(OpenAI 兼容协议)";
16
16
  readonly aiRule: "只有这一条通道:Key / Base URL / 模型齐备才会翻译,缺任何一项都保持原文。";
17
17
  readonly badgeConfigured: "已配置";
@@ -41,7 +41,10 @@ export declare const zh: {
41
41
  readonly dockOn: "正文翻译已开启,点击关闭";
42
42
  readonly dockOff: "正文翻译已关闭,点击开启";
43
43
  readonly failTitle: "翻译失败";
44
- readonly retryAria: "翻译失败,按 Enter 重试";
44
+ readonly retryAria: "翻译失败,点左缘那条窄带可按 Enter 重试";
45
+ readonly toggleToOriginalAria: "查看原文";
46
+ readonly toggleToTranslatedAria: "查看译文";
47
+ readonly inflightAria: "翻译中,此处暂无动作";
45
48
  };
46
49
  /** Locale keys this plugin renders. */
47
50
  export type ChatTranslateLocaleKey = keyof typeof zh;
@@ -57,6 +60,11 @@ export declare function bindRowCopy(locale: {
57
60
  } | null | undefined): void;
58
61
  export declare function rowCopy(): {
59
62
  retryAria: string;
63
+ /** 热区动作的读屏标签:读译文时提示可切回原文,反之亦然。 */
64
+ toggleToOriginalAria: string;
65
+ toggleToTranslatedAria: string;
66
+ /** 在途热区的读屏标签:说清此刻按下去没有动作。 */
67
+ inflightAria: string;
60
68
  /** 失败块悬停文案:本地化标签 + 服务端原样带来的技术一句(通道伤)。 */
61
69
  failTitle: (detail?: string | null) => string;
62
70
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynn123411/dsh-chat-translate",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Assistant reply-body translation for the DeepSeek Harness Web UI (OpenAI-compatible channel)",
5
5
  "license": "MIT",
6
6
  "author": "lynn123411",
@@ -61,7 +61,7 @@
61
61
  "scripts": {
62
62
  "build": "node build.mjs",
63
63
  "typecheck": "tsc -p tsconfig.json --noEmit",
64
- "test": "node scripts/test-client-wiring.mjs && node scripts/test-chat-translate.mjs && node scripts/test-regression-suite.mjs && node scripts/test-channel-logic.mjs && node scripts/test-store.mjs && node scripts/test-reply-pipeline.mjs",
64
+ "test": "node scripts/test-client-wiring.mjs && node scripts/test-render.mjs && node scripts/test-chat-translate.mjs && node scripts/test-regression-suite.mjs && node scripts/test-channel-logic.mjs && node scripts/test-store.mjs && node scripts/test-reply-pipeline.mjs",
65
65
  "assert:lib-untracked": "node scripts/assert-lib-untracked.mjs",
66
66
  "check": "pnpm typecheck && pnpm build && pnpm test && pnpm assert:lib-untracked"
67
67
  },
@@ -83,6 +83,8 @@
83
83
  "@types/react": "^19.2.18",
84
84
  "esbuild": "^0.25.0",
85
85
  "jsdom": "^26.1.0",
86
+ "react": "^18.3.1",
87
+ "react-dom": "^18.3.1",
86
88
  "typescript": "^6.0.0"
87
89
  },
88
90
  "peerDependencies": {