@lynn123411/dsh-chat-translate 3.0.1 → 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.
- package/README.md +7 -7
- package/docs//351/205/215/347/275/256.md +6 -5
- package/lib/client.js +312 -184
- package/lib/client.js.map +4 -4
- package/lib/index.js +369 -335
- package/lib/index.js.map +4 -4
- package/lib/types/client/chat/assistant-step.d.ts +7 -3
- package/lib/types/client/chat/click-guard.d.ts +10 -9
- package/lib/types/client/chat/row-plan.d.ts +26 -8
- package/lib/types/client/chat/styles.d.ts +51 -6
- package/lib/types/client/chat/translate-store.d.ts +16 -16
- package/lib/types/client/locales.d.ts +20 -2
- package/lib/types/client/translate/api.d.ts +1 -8
- package/lib/types/server/cache.d.ts +1 -1
- package/lib/types/server/dispatcher.d.ts +30 -26
- package/lib/types/server/pipeline/blocks.d.ts +7 -11
- package/lib/types/server/pipeline/segments.d.ts +68 -0
- package/lib/types/server/prompt-revision.d.ts +1 -1
- package/lib/types/server/types.d.ts +15 -13
- package/package.json +4 -2
- package/lib/types/server/pipeline/mask-tokens.d.ts +0 -64
- package/lib/types/server/pipeline/masking.d.ts +0 -45
|
@@ -6,9 +6,13 @@
|
|
|
6
6
|
* 1. 正文块从数据层拿**原始 markdown**(`AssistantBlock.text`),译文同样按
|
|
7
7
|
* markdown 交给宿主公开基线组件 `MarkdownText` 重渲染:格式保真由官方
|
|
8
8
|
* 渲染器负责,本文件不产生任何自拼 markup。
|
|
9
|
-
* 2.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* 2. 「已翻译/没译成」是渲染状态:落定、开关开启、进入视口后逐行首跑;成功
|
|
10
|
+
* 的块(包括模型认为原样最好的块)挂译文、左缘蓝线;失败的块保持原文、
|
|
11
|
+
* 左缘挂红实线;已登记在途而尚无结果的块显灰脉动。
|
|
12
|
+
* 3. 切换与补跑都在**左缘热区**上,不在正文上:正文整块可点会让「选中一段
|
|
13
|
+
* 文字复制」与「切换译文/原文」抢同一次点击;热区收成贴着那条线的窄带后,
|
|
14
|
+
* 正文的单击落字、拖选复制都不再误触。失败块的 ↻ 常驻在红线旁,点它整行
|
|
15
|
+
* 补跑,无限次、无自动重试。
|
|
12
16
|
*
|
|
13
17
|
* 除正文外的行内容与宿主逐分支等价:reasoning 行走折叠(含 Turn-process
|
|
14
18
|
* 隐藏与 beforematch 揭示)、连续 image 组交回 owner 的 renderMessageImages、
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 左缘热区的点击判定:只有「按下与松手都落在热区上」的干净点击才切换/补跑。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 热区没有子元素,所以浏览器派发过来的 click 若 `target === currentTarget`,
|
|
5
|
+
* 就说明按下与松手都在热区里;反之(在热区按下、往右拖进正文选字再松手)click
|
|
6
|
+
* 会派发给两者的公共祖先,target 不是热区,天然判为拖选。这不是启发式,是
|
|
7
|
+
* click 事件的分派语义——正文整块不再是点击目标,误触从根上消失。
|
|
8
|
+
* 纯 DOM 判定,不依赖 React,可独立测试。
|
|
6
9
|
*/
|
|
7
10
|
interface MinimalPointerEvent {
|
|
8
11
|
/** 点击命中的最深元素。 */
|
|
9
|
-
target:
|
|
10
|
-
|
|
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
|
|
27
|
+
export declare function isCleanHotspotClick(event: MinimalPointerEvent, view: MinimalWindow | undefined): boolean;
|
|
27
28
|
export {};
|
|
@@ -6,14 +6,20 @@
|
|
|
6
6
|
* groupPart 怎么过滤、tool-call 块为什么在这里被跳过(工具行是 flow 里
|
|
7
7
|
* 独立的行,由宿主自己渲染)、连续 image 怎么合成一组、未知块落 JSON、
|
|
8
8
|
* 「已停止」标记的出现条件。
|
|
9
|
-
* 2.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* 2. 翻译呈现的单点判断:正文块在什么状态下挂什么线,直接产出 styles.ts
|
|
10
|
+
* 的 `ProseMark`(词汇与类名映射单点在 styles.ts)。判据一次算清:成功落定
|
|
11
|
+
* =蓝线(用户点了读原文则灰细线——`originalKeys` 进来折进 mark);失败
|
|
12
|
+
* (通道伤)=红实线一根,`fail` 一句技术细节只喂悬停文案;行已登记在途且
|
|
13
|
+
* 这块尚无结果=灰脉动;其余(空白块、流式、开关关、未登记)=无线。模型
|
|
14
|
+
* 原样返回同样算成功:标记传达「这段过了模型」,不是「变过」。
|
|
15
|
+
* 空白块(trim 后为空)不进送译清单、不占下标、永不挂线——它没送过模型,
|
|
16
|
+
* 重试也永远不会成,挂线只会留下消不掉的标记。
|
|
12
17
|
*
|
|
13
|
-
* 渲染层(assistant-step.tsx
|
|
18
|
+
* 渲染层(assistant-step.tsx)只消费 mark;改动这里的等价分支必须同时对照
|
|
14
19
|
* 宿主实现。
|
|
15
20
|
*/
|
|
16
21
|
import type { BlockOutcome } from './translate-store.ts';
|
|
22
|
+
import type { ProseMark } from './styles.ts';
|
|
17
23
|
/** 助手行块数据的结构性镜像(值边不跨包,type-only 世界)。 */
|
|
18
24
|
export interface AssistantBlockLike {
|
|
19
25
|
readonly kind: string;
|
|
@@ -29,12 +35,19 @@ export type RowPlanEntry =
|
|
|
29
35
|
blockIndex: number;
|
|
30
36
|
text: string;
|
|
31
37
|
}
|
|
32
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* 正文块:mark 即左缘线状态(线型、脉动、热区动作全由它决定);
|
|
40
|
+
* translated 仅在成功时非 null,供 mark==='translated' 时换源文本渲染;
|
|
41
|
+
* fail 仅在 mark==='failed' 时非 null——服务端的技术一句,Tooltip 悬停
|
|
42
|
+
* 报因用,缺省时只报「翻译失败」。线型不从这里出:失败一律同一条红实线。
|
|
43
|
+
*/
|
|
33
44
|
| {
|
|
34
45
|
type: 'prose';
|
|
35
46
|
key: number;
|
|
36
47
|
text: string;
|
|
37
48
|
translated: string | null;
|
|
49
|
+
mark: ProseMark;
|
|
50
|
+
fail: string | null;
|
|
38
51
|
} | {
|
|
39
52
|
type: 'images';
|
|
40
53
|
key: number;
|
|
@@ -50,7 +63,7 @@ export type RowPlanEntry =
|
|
|
50
63
|
export interface RowPlan {
|
|
51
64
|
/** null = 整行不渲染(与宿主的空行守卫一致)。 */
|
|
52
65
|
entries: RowPlanEntry[] | null;
|
|
53
|
-
/**
|
|
66
|
+
/** 送翻译池的正文文本,按正文块出现顺序(空白块除外),与结果下标对齐。 */
|
|
54
67
|
texts: string[];
|
|
55
68
|
}
|
|
56
69
|
export interface RowPlanInput {
|
|
@@ -64,9 +77,14 @@ export interface RowPlanInput {
|
|
|
64
77
|
/** 可译门(enabled 且通道三要素齐备):false 时一切按原文呈现,不挂线。 */
|
|
65
78
|
canTranslate: boolean;
|
|
66
79
|
/**
|
|
67
|
-
* 该行正文块的翻译结果,与 texts 同下标;null =
|
|
68
|
-
*
|
|
80
|
+
* 该行正文块的翻译结果,与 texts 同下标;null = 该批尚未落定。
|
|
81
|
+
* 未登记或换代时整个缺省。
|
|
69
82
|
*/
|
|
70
83
|
outcomes?: readonly (BlockOutcome | null)[];
|
|
84
|
+
/** 池里这行的状态:pending 时未落定的块显灰脉动,未登记什么都不显。 */
|
|
85
|
+
rowStatus?: 'pending' | 'settled';
|
|
86
|
+
/** 用户点了「读原文」的正文块(按条目 key):把译文态折成 original-view。
|
|
87
|
+
* 触发它的是左缘热区上的切换,不是点正文。 */
|
|
88
|
+
originalKeys?: ReadonlySet<number>;
|
|
71
89
|
}
|
|
72
90
|
export declare function planAssistantRow(input: RowPlanInput): RowPlan;
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 助手行接管所需的样式:与宿主 `AssistantMarkdown` / `ReasoningRow` 逐条对齐
|
|
3
3
|
* 的等价规则(字号、行距、gap、粘性折叠头、running 微光、summary 遮罩等),
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 加上本插件唯一新增的视觉词汇——正文块左缘的线与贴着它的那条热区:
|
|
5
|
+
* 蓝粗线(1px 主色)=正在读译文;灰细线(0.5px 中性)=有译文但正在读原文
|
|
6
|
+
* (再点即切回);红实线(1px error 色)=翻译失败(通道伤:超时、断流、空
|
|
7
|
+
* 返回);灰脉动=整行在途、这块尚无结果;无线=没送过模型(含空白块)或开关关闭。
|
|
8
|
+
* 译文的结构漂移不再挂红线——由宿主按原文修回、重掷或照收,红线只剩通道伤一种。
|
|
9
|
+
*
|
|
10
|
+
* 点击不落在正文上:`dsh-ct-hotspot` 是贴着左缘线的窄热区(线左侧 4px + 线
|
|
11
|
+
* 右侧那 12px 缩进,随块高铺满),切换与补跑都由它承载,正文只做选中/复制。
|
|
12
|
+
* 悬停不改任何视觉——只有鼠标在热区上变手型;键盘聚焦(且仅键盘聚焦)时线
|
|
13
|
+
* 加亮,粗细始终只表示译文/原文。失败块的 ↻ 常驻在红线旁,是补跑的可见入口。
|
|
6
14
|
*
|
|
7
15
|
* 全部颜色与尺寸走 `--dsw-alias-*` / `--dsh-*` 主题别名,中性平边按官方规则
|
|
8
16
|
* 画 0.5px hairline;动画声明尊重 `prefers-reduced-motion`。样式随 bundle 注入
|
|
@@ -10,10 +18,47 @@
|
|
|
10
18
|
*/
|
|
11
19
|
export declare const ASSISTANT_CSS: string;
|
|
12
20
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
21
|
+
* 正文块左缘线的状态词汇:单点在 row-plan 算出(`planAssistantRow` 直接产出
|
|
22
|
+
* mark),渲染层只消费、样式层只翻成类名——「线型与脉动」不再多处重算。
|
|
23
|
+
* 失败只有 `failed` 一种:同一条红实线,败因走悬停文案。
|
|
24
|
+
*/
|
|
25
|
+
export type ProseMark = 'translated' | 'original-view' | 'failed' | 'inflight' | null;
|
|
26
|
+
/** mark → 热区动作单点:切换、重试、没有热区三选一(渲染层照此接线)。 */
|
|
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} 的窄读法,测试与渲染层共用)。 */
|
|
46
|
+
export declare function proseAction(mark: ProseMark): ProseAction;
|
|
47
|
+
/**
|
|
48
|
+
* mark → 正文块类名单点:只有线型。点击不在这层——正文整块不可点,切换与
|
|
49
|
+
* 补跑由左缘热区({@link HOTSPOT_CLASS})承载,所以这里不再有可点态/光标类。
|
|
50
|
+
*/
|
|
51
|
+
export declare function proseClassNames(mark: ProseMark): string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* 缩进单点:能翻译(开关开 + 通道齐备)时所有正文块一律左缩进,与「这块是否
|
|
54
|
+
* 已挂线」无关——译文落定的那一刻正文不右移,块与块左缘也对齐;关掉开关或
|
|
55
|
+
* 通道没配好时不缩进,排版回到宿主原样(此时正文会横移一次,是刻意的取舍)。
|
|
16
56
|
*/
|
|
17
|
-
export declare function
|
|
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";
|
|
18
62
|
/** 注入接管样式;重复调用是空操作。 */
|
|
19
63
|
export declare function ensureAssistantStyles(): void;
|
|
64
|
+
export {};
|
|
@@ -1,33 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ReplyBlockResult } from '../../server/types.ts';
|
|
2
2
|
/** 一行正文的按块翻译结果,与请求的 texts 数组按下标对齐。 */
|
|
3
3
|
export interface BlockOutcome {
|
|
4
4
|
/** 模型产出的中文(Markdown 源文本);失败时等于原文。 */
|
|
5
5
|
translated: string;
|
|
6
|
-
/**
|
|
6
|
+
/** 该块是否翻译成功。 */
|
|
7
7
|
ok: boolean;
|
|
8
|
+
/** 仅 ok=false 时可能出现:服务端给出的技术细节,拼在标签后进悬停文案。 */
|
|
9
|
+
detail?: string;
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
12
|
* 一行正文的翻译状态。outcomes 与 texts 同下标,null = 该块所在批尚未落定:
|
|
11
13
|
* 每批返回就地填入并通知,长回答按阅读顺序逐段出中文,不憋到最后一批。
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* 自然重试,成功的块由宿主磁盘缓存直接命中、不再花模型调用。重走有上限
|
|
15
|
-
* (MAX_ROW_ATTEMPTS):确定性失败(如弱模型丢占位符)不该被滚动无限续杯,
|
|
16
|
-
* 封顶后保持原文直到文本换代重置。
|
|
14
|
+
* 全部批落定后 status 转 settled——落定即终态:这里没有任何自动补跑,
|
|
15
|
+
* 失败块的救活只由用户的点击发起(ensure 的 manual 参数)。
|
|
17
16
|
*/
|
|
18
17
|
export interface RowState {
|
|
19
|
-
status: 'pending' | '
|
|
18
|
+
status: 'pending' | 'settled';
|
|
20
19
|
texts: readonly string[];
|
|
21
20
|
outcomes: readonly (BlockOutcome | null)[];
|
|
22
|
-
/** 这一代文本已经完整跑过几轮(首跑计 1)。 */
|
|
23
|
-
attempts: number;
|
|
24
21
|
}
|
|
25
22
|
/** 送译一行正文的取数面:默认走宿主路由,测试注入假实现。 */
|
|
26
23
|
export type ReplyFetcher = (texts: string[]) => Promise<ReplyBlockResult[]>;
|
|
27
24
|
/** 同时在池的行数上限:LRU 淘汰最久未读的整行;行文本本身仍归宿主会话持有。 */
|
|
28
25
|
export declare const MAX_TRANSLATED_ROWS = 200;
|
|
29
|
-
/** 同一代文本最多完整跑几轮;封顶后不再打通道,直到文本换代重置额度。 */
|
|
30
|
-
export declare const MAX_ROW_ATTEMPTS = 3;
|
|
31
26
|
/**
|
|
32
27
|
* 一次 HTTP 请求装载的估算 token 上限:与宿主的打包输入上限同值同源
|
|
33
28
|
* (blocks.ts 的 REPLY_MAX_INPUT_TOKENS)——客户端按宿主的打包窗口切批,
|
|
@@ -41,8 +36,10 @@ export declare function chunkTexts(texts: readonly string[]): string[][];
|
|
|
41
36
|
*
|
|
42
37
|
* 行键由渲染层给出(会话内锚点序号),文本数组参与一致性判断:同键不同文本
|
|
43
38
|
* 视为行换了一代内容,整行重新请求——这保证重渲染或会话切换后不会把旧译文
|
|
44
|
-
*
|
|
45
|
-
*
|
|
39
|
+
* 配到新文本上。请求幂等:同键同文本且仍在途的 ensure 不发第二个请求;已落定
|
|
40
|
+
* 的行同样短路——**除非** manual=true,那是用户点了某个红线块发起的手动补跑,
|
|
41
|
+
* 无限次、不设额度。手动补跑时上一代已成功的块保持挂线(结果从上一代种下,
|
|
42
|
+
* 服务端磁盘缓存让它们秒回),只有尚无译文的块显示在途脉动。在途期间换代,
|
|
46
43
|
* 迟到的旧代结果直接丢弃。
|
|
47
44
|
*
|
|
48
45
|
* 本模块不依赖 React:渲染层用 useSyncExternalStore 订阅版本,再读 getState。
|
|
@@ -60,13 +57,16 @@ export declare class ChatTranslateStore {
|
|
|
60
57
|
getState(rowKey: string): RowState | undefined;
|
|
61
58
|
/**
|
|
62
59
|
* 确保该行按当前文本被翻译。
|
|
63
|
-
* @
|
|
60
|
+
* @param manual - 用户点击失败块的左缘热区发起的补跑:已落定的同代行只有这条路会重发。
|
|
61
|
+
* @returns 是否发出了新的请求。
|
|
64
62
|
*/
|
|
65
|
-
ensure(rowKey: string, texts: readonly string[]): boolean;
|
|
63
|
+
ensure(rowKey: string, texts: readonly string[], manual?: boolean): boolean;
|
|
66
64
|
private run;
|
|
67
65
|
private prune;
|
|
68
66
|
private bump;
|
|
69
67
|
}
|
|
68
|
+
/** 文本清单是否同代(逐位相等):池的幂等判据,渲染层拿它核对登记行属于当前文本。 */
|
|
69
|
+
export declare function sameTexts(a: readonly string[], b: readonly string[]): boolean;
|
|
70
70
|
/** 供测试注入取数面创建独立池。 */
|
|
71
71
|
export declare function createTranslateStore(fetcher: ReplyFetcher): ChatTranslateStore;
|
|
72
72
|
/** 正文翻译池:客户端半边唯一的按行译文登记处。 */
|
|
@@ -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
|
|
14
|
+
readonly masterDesc: "自动把助手回答正文翻成中文(如 {example}):折叠块里的过程正文与最终汇总一视同仁,整段中文的块也照送,由模型一并改写成自然中文、顺掉机器腔。Think 卡与工具调用行不翻,代码块原样保留。正文块的左缘一条竖线报出状态:正在读译文是蓝线,备着译文正读原文是灰细线,没译成是红实线(鼠标悬停热区报出通道细节——超时、断流或空返回;译文的结构漂移由宿主自动修回或重掷,不再因格式问题挂红线),正在请求是灰脉动线,没送过模型或开关关闭没有线。切换不在正文上:贴着那条线的窄带才是操作区,鼠标移到它上面变手型,点一下在译文与原文间切换——正文整块不再响应点击,选字、拖选复制都不会误触;模型认为原样最好、返回一模一样的文字时同样算翻译过、同样可切。挂红线的块右侧常驻一个小 ↻,点那条窄带即整行重新翻译,已成功的块保持显示不打扰。通道失败不自动重试,恢复后由你点那条窄带救活。译文按原 markdown 格式重新渲染,表格、列表、加粗与行内代码保持结构。回答还在流式输出时不翻,落定后按阅读顺序逐段出中文。只作用于当前查看的会话,译文不写回会话上下文。输入框下方那一行有同款开关,两处状态同步。";
|
|
15
15
|
readonly aiTitle: "AI 翻译(OpenAI 兼容协议)";
|
|
16
16
|
readonly aiRule: "只有这一条通道:Key / Base URL / 模型齐备才会翻译,缺任何一项都保持原文。";
|
|
17
17
|
readonly badgeConfigured: "已配置";
|
|
@@ -36,10 +36,15 @@ export declare const zh: {
|
|
|
36
36
|
readonly testOk: "连接正常,延迟 {latency}ms";
|
|
37
37
|
readonly testFail: "失败:{error}";
|
|
38
38
|
readonly behaviorTitle: "翻译范围";
|
|
39
|
-
readonly behaviorRule: "
|
|
39
|
+
readonly behaviorRule: "助手回答正文一律翻译,包括最终汇总与整段中文的块:中文块同样交给模型改写成自然中文,意义、术语与数字不变;空白块不送译、不挂线。思考链正文、工具调用标题与折叠摘要不翻译,代码块及其内部原样保留。";
|
|
40
40
|
readonly dockLabel: "译";
|
|
41
41
|
readonly dockOn: "正文翻译已开启,点击关闭";
|
|
42
42
|
readonly dockOff: "正文翻译已关闭,点击开启";
|
|
43
|
+
readonly failTitle: "翻译失败";
|
|
44
|
+
readonly retryAria: "翻译失败,点左缘那条窄带可按 Enter 重试";
|
|
45
|
+
readonly toggleToOriginalAria: "查看原文";
|
|
46
|
+
readonly toggleToTranslatedAria: "查看译文";
|
|
47
|
+
readonly inflightAria: "翻译中,此处暂无动作";
|
|
43
48
|
};
|
|
44
49
|
/** Locale keys this plugin renders. */
|
|
45
50
|
export type ChatTranslateLocaleKey = keyof typeof zh;
|
|
@@ -50,3 +55,16 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
50
55
|
'settings.chatTranslate': ChatTranslateLocaleKey;
|
|
51
56
|
}
|
|
52
57
|
}
|
|
58
|
+
export declare function bindRowCopy(locale: {
|
|
59
|
+
bind?(namespace: string): (key: string) => string;
|
|
60
|
+
} | null | undefined): void;
|
|
61
|
+
export declare function rowCopy(): {
|
|
62
|
+
retryAria: string;
|
|
63
|
+
/** 热区动作的读屏标签:读译文时提示可切回原文,反之亦然。 */
|
|
64
|
+
toggleToOriginalAria: string;
|
|
65
|
+
toggleToTranslatedAria: string;
|
|
66
|
+
/** 在途热区的读屏标签:说清此刻按下去没有动作。 */
|
|
67
|
+
inflightAria: string;
|
|
68
|
+
/** 失败块悬停文案:本地化标签 + 服务端原样带来的技术一句(通道伤)。 */
|
|
69
|
+
failTitle: (detail?: string | null) => string;
|
|
70
|
+
};
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export interface ReplyBlockResult {
|
|
3
|
-
original: string;
|
|
4
|
-
translated: string;
|
|
5
|
-
ok: boolean;
|
|
6
|
-
cached: boolean;
|
|
7
|
-
channel: string;
|
|
8
|
-
}
|
|
1
|
+
import type { ReplyBlockResult } from '../../server/types.ts';
|
|
9
2
|
/**
|
|
10
3
|
* 请求翻译回答正文的块。宿主侧按输入上限打包、串行发送,客户端超时交给
|
|
11
4
|
* 宿主(一个长回答可能要几分钟),这里不设自己的截止时间。
|
|
@@ -7,7 +7,7 @@ export declare class LruDiskCache {
|
|
|
7
7
|
private dirty;
|
|
8
8
|
constructor(maxEntries?: number, fileName?: string, revision?: string);
|
|
9
9
|
init(): Promise<void>;
|
|
10
|
-
/** Adopt loaded entries, dropping
|
|
10
|
+
/** Adopt loaded entries, dropping malformed ones. */
|
|
11
11
|
private loadEntries;
|
|
12
12
|
get(key: string): string | undefined;
|
|
13
13
|
set(key: string, value: string): void;
|
|
@@ -5,10 +5,7 @@ import type { KeyReader } from './credentials.ts';
|
|
|
5
5
|
export declare class TranslationDispatcher {
|
|
6
6
|
private configManager;
|
|
7
7
|
private cache;
|
|
8
|
-
private credentials;
|
|
9
|
-
private masking;
|
|
10
8
|
private adapters;
|
|
11
|
-
private circuitStates;
|
|
12
9
|
/** 正文请求的串行队列尾,保证同时最多一个在途请求。 */
|
|
13
10
|
private replyTail;
|
|
14
11
|
constructor(configManager: ConfigManager, cache: LruDiskCache, credentials?: KeyReader);
|
|
@@ -16,45 +13,52 @@ export declare class TranslationDispatcher {
|
|
|
16
13
|
/**
|
|
17
14
|
* 翻译回答正文的块:本次唯一的翻译入口。
|
|
18
15
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* 每个块先按 markdown 结构切成段:代码围栏与纯空白段**逐字保留、永不送
|
|
17
|
+
* 模型**;散文段再按输入上限切片、各自成片段。相邻片段打包成一个请求;整批
|
|
18
|
+
* 失败时退回逐片段单发。每个片段的译文过 `repairShape`(段落块对齐 + 前缀
|
|
19
|
+
* 修回 + 空行重排)并通过链接目标回填才算修好。
|
|
22
20
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* **形状问题不否决内容**:修不好的片段自动重掷一次;再修不好就照收模型的
|
|
22
|
+
* 译文(链接数不等时不回填,其余原样)——原文一键可回,可读的译文优先于
|
|
23
|
+
* 一根红线。失败只剩通道伤一种:超时、断流、空返回,红实线 + 悬停报细节,
|
|
24
|
+
* 救活由用户的点击发起。
|
|
25
|
+
*
|
|
26
|
+
* 调用方传进来的每个块要么整块译出、要么整块保持原文:任一片段缺失都让该块
|
|
27
|
+
* 作废,避免半中半英的段落;失败块带 `detail` 一句技术细节。每行只跑首跑
|
|
28
|
+
* 一次,通道恢复后由点击救活。
|
|
29
|
+
*
|
|
30
|
+
* 客户端按与宿主同源的 4096 估算 token 切批,所以一个 markdown 块可能跨多次
|
|
31
|
+
* 调用;每次调用都独立决定成败,不会出现「前一段已挂译文、后一段失败」的
|
|
32
|
+
* 半截结果。
|
|
25
33
|
*/
|
|
26
34
|
translateReplyBlocks(blocks: string[]): Promise<ReplyBlockResult[]>;
|
|
27
|
-
/**
|
|
35
|
+
/** 通道可用之后的正文执行段:切段、打包、请求、拼装。 */
|
|
28
36
|
private serveReplyBlocks;
|
|
29
37
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
38
|
+
* 一整批一次请求;批内修不好的片段与整批的失败都退回逐片段单发。单发仍
|
|
39
|
+
* 修不好的**照收**(见 {@link translateReplyBlocks})——只有通道伤才让片段
|
|
40
|
+
* 缺失,失败的片段不进缓存。
|
|
33
41
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* 坏内容只作废坏内容自己,不把其他行挡在冷却期外。
|
|
42
|
+
* 每次通道失败都记进片段账本({@link FailureLedger}):批请求的失败摊到
|
|
43
|
+
* 批内每个片段,单发的失败只记该片段;块级细节由缺失片段的账本合成。
|
|
37
44
|
*/
|
|
38
45
|
private translateReplyBatch;
|
|
39
|
-
/**
|
|
40
|
-
private
|
|
41
|
-
/**
|
|
42
|
-
|
|
46
|
+
/** 片段的最终拼装:头尾空白 + (可选)链接回填 + 译文。 */
|
|
47
|
+
private assemble;
|
|
48
|
+
/**
|
|
49
|
+
* 发出一次正文请求,返回逐片段的裸答案(打包标记已剥除、首尾已 trim)。
|
|
50
|
+
* 空返回与打包拆不回都按通道失败抛出——形状对不对在这里不算败因。
|
|
51
|
+
*/
|
|
52
|
+
private fetchReplyAnswers;
|
|
43
53
|
/** 串行执行器:同时最多一个在途请求。 */
|
|
44
54
|
private runReplySerial;
|
|
45
55
|
/**
|
|
46
56
|
* 通道探针:设置面板的「测试 AI 通道」按钮走这里。探测文本用一句英文正文,
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* 探针是用户主动发起的连通性检查,因此不写也不读熔断账本:连点几次测试
|
|
50
|
-
* 不该把正文翻译挡在冷却期外,正文的失败也不该让按钮变哑。
|
|
57
|
+
* 与真实请求同形;它一次请求都不写正文账本,正文的成败也不影响按钮。
|
|
51
58
|
*/
|
|
52
59
|
testChannel(channelId: string): Promise<{
|
|
53
60
|
ok: boolean;
|
|
54
61
|
latencyMs: number;
|
|
55
62
|
error?: string;
|
|
56
63
|
}>;
|
|
57
|
-
private isCircuitOpen;
|
|
58
|
-
private recordSuccess;
|
|
59
|
-
private recordFailure;
|
|
60
64
|
}
|
|
@@ -16,20 +16,13 @@ export declare function estimateTokens(text: string): number;
|
|
|
16
16
|
* 片段按原顺序拼回即为该块的完整译文。
|
|
17
17
|
*/
|
|
18
18
|
export declare function splitOversizedBlock(text: string, maxTokens?: number): string[];
|
|
19
|
-
/** 一个待翻译的片段:属于哪个块、块内第几段、以及该片段的掩码结果。 */
|
|
20
|
-
export interface ReplyPieceShell<TMask> {
|
|
21
|
-
block: number;
|
|
22
|
-
index: number;
|
|
23
|
-
text: string;
|
|
24
|
-
mask: TMask;
|
|
25
|
-
}
|
|
26
19
|
/** 把片段按原顺序打包成尽量少的请求,且每批不超过输入上限。 */
|
|
27
20
|
export declare function packPieces<T extends {
|
|
28
21
|
text: string;
|
|
29
22
|
}>(pieces: T[], maxTokens?: number): T[][];
|
|
30
23
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
24
|
+
* 打包标记:`⟪<4 letters><index>⟫`——打包请求的框架,每段以标记行开头。
|
|
25
|
+
* 它只活在一次请求的负载里;译文残留 ⟪…⟫ 由宿主直接剥除,不成败因。
|
|
33
26
|
*/
|
|
34
27
|
export interface BatchFormat {
|
|
35
28
|
id: string;
|
|
@@ -45,5 +38,8 @@ export declare function buildBatchPayload(pieces: string[], format: BatchFormat)
|
|
|
45
38
|
* 的标记;任何一条不满足都返回 null,由调用方作废整批并退回单块重试。
|
|
46
39
|
*/
|
|
47
40
|
export declare function splitBatchTranslation(translated: string, format: BatchFormat, count: number): string[] | null;
|
|
48
|
-
/**
|
|
49
|
-
|
|
41
|
+
/**
|
|
42
|
+
* 剥除译文里残留的 ⟪…⟫ 打包标记:标记只是请求脚手架,模型带回来就删掉——
|
|
43
|
+
* 形状问题不否决内容,残留也不再是拒收理由。
|
|
44
|
+
*/
|
|
45
|
+
export declare function stripBatchMarkers(text: string): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 结构重装配:正文块的 markdown 按结构切开,代码围栏逐字保留,散文段送模型,
|
|
3
|
+
* 回来后**按原结构拼回**——「翻译后 markdown 语法没问题」由构造与形状核对保证:
|
|
4
|
+
* 模型从来见不到占位符,也就谈不上带不带回。
|
|
5
|
+
*
|
|
6
|
+
* 三件事:
|
|
7
|
+
* 1. `splitMarkdownSegments`:把块切成 code / prose 段的有序清单,拼接恒等于原文。
|
|
8
|
+
* code 段(``` 或 ~~~ 围栏,含未闭合的到块尾)永不送模型;4 空格缩进代码不
|
|
9
|
+
* 单独识别(聊天正文里几乎不出现,无记号行的行首缩进由形状签名兜住,
|
|
10
|
+
* 记号行的缩进则随前缀修回)。
|
|
11
|
+
* 2. `lineSignature`:一行 markdown 的结构签名——块记号的**类别**(标题 h、
|
|
12
|
+
* 引用 q、无序列表 l、有序列表 n)、无记号行的缩进宽、表格竖线数、链接个数。
|
|
13
|
+
* 记号的层级、字符、序号与列表缩进不进签名——渲染等价的漂移交给 repairShape
|
|
14
|
+
* 修回,而不是把整块长回答一票否决。真破渲染的仍是:正文行并拢/拆开、表格
|
|
15
|
+
* 列错位、记号整个丢(列表变段落)、链接丢没、无记号行缩进漂移(变代码块)。
|
|
16
|
+
* 3. `restoreLinkTargets`:链接按出现序对齐后,把原文的 URL 逐个拼回译文——
|
|
17
|
+
* 模型只译 `[文字]`,`(URL)` 由构造保住。个数不等在签名核对里已经出局。
|
|
18
|
+
* 引用式定义行 `[id]: URL` 同账:计数进签名、目标由回填保住。
|
|
19
|
+
* 4. `repairShape`:核对与修形一次过——空行漂移按原文布局重排(弱模型最爱
|
|
20
|
+
* 吞空行,非空行签名对得上就不算破损),行首「前导空白 + 块记号」逐行换
|
|
21
|
+
* 回原文同款(`##`→`###`、`-`→`*`、序号、引用深度、列表缩进的漂移就此
|
|
22
|
+
* 消失)。修不了才回报错误行与理由,进 content 败因的悬停文案。
|
|
23
|
+
*/
|
|
24
|
+
export interface MarkdownSegment {
|
|
25
|
+
readonly kind: 'prose' | 'code';
|
|
26
|
+
readonly text: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 把块切成有序段清单;`segments.map(s => s.text).join('') === text` 恒成立。
|
|
30
|
+
* 每段带着自己行尾的换行结束(段与段之间不存在丢失的分隔符),所以散文段的
|
|
31
|
+
* 头尾空白天然包含段间距,重装配逐字拼得回。
|
|
32
|
+
*/
|
|
33
|
+
export declare function splitMarkdownSegments(text: string): MarkdownSegment[];
|
|
34
|
+
/** 形状核对的只读错误视图:repairShape 的 error 侧。null = 可成立。 */
|
|
35
|
+
export declare function shapeMismatch(original: string, translated: string): string | null;
|
|
36
|
+
export interface ShapeRepair {
|
|
37
|
+
/** 修好的文本:原文骨架(空行布局 + 行首前缀)+ 译文正文;null = 修不了。 */
|
|
38
|
+
text: string | null;
|
|
39
|
+
/** 修不了时的一句理由,直接进悬停文案。 */
|
|
40
|
+
error: string | null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 形状修复:核对与修形一次过,能修的绝不成败因。骨架按**段落块**对齐——
|
|
44
|
+
* 1. 块数必须相等(模型把两段并拢、或把一段劈开,才是真破损);空行布局
|
|
45
|
+
* 一律取原文——模型吞空行、多空行都被重排消化。
|
|
46
|
+
* 2. 块内**结构行**逐一对齐(记号类别、表格竖线数、链接个数、缩进代码的
|
|
47
|
+
* 缩进),行首前缀修回原文同款:`##`→`###`、`-`→`*`、序号、引用深度、
|
|
48
|
+
* 列表缩进的漂移就此消失。
|
|
49
|
+
* 3. 块内**散文行自由重排**:弱模型把硬折行的英文段落译成一整行中文是常态,
|
|
50
|
+
* 软换行渲染无感;只查整块的链接总数不丢——重排吞不掉 `[文字](URL)`。
|
|
51
|
+
* 4. 块首行的结构/散文属性不得互换:段落被并进列表项(或反之)是渲染破损。
|
|
52
|
+
*/
|
|
53
|
+
export declare function repairShape(original: string, translated: string): ShapeRepair;
|
|
54
|
+
/**
|
|
55
|
+
* 把原文的链接目标逐个拼回译文:内联 `[文字](URL)` 与引用式定义行 `[id]: URL`
|
|
56
|
+
* 都算——两侧已按签名核对确认等数,按出现序对齐,译文只贡献 `[文字]` 与排版。
|
|
57
|
+
*/
|
|
58
|
+
export declare function restoreLinkTargets(original: string, translated: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* 两侧链接总数是否相等——回填的前置条件:等数才谈得上按出现序对齐;
|
|
61
|
+
* 不等时回填会把原文 URL 串到错的链接上,宁可不回填。
|
|
62
|
+
*/
|
|
63
|
+
export declare function linkCountsMatch(a: string, b: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* 只修形不核对的旧口:转手 repairShape——行首前缀修回与空行重排都在那里。
|
|
66
|
+
* 修不了时原样返回(调用方自己先看 repairShape 的 error)。
|
|
67
|
+
*/
|
|
68
|
+
export declare function restoreLineShapes(original: string, translated: string): string;
|
|
@@ -4,17 +4,6 @@ export interface PluginConfig {
|
|
|
4
4
|
baseUrl: string;
|
|
5
5
|
model: string;
|
|
6
6
|
}
|
|
7
|
-
export interface TranslateItemResult {
|
|
8
|
-
original: string;
|
|
9
|
-
translated: string;
|
|
10
|
-
channel: string;
|
|
11
|
-
cached: boolean;
|
|
12
|
-
}
|
|
13
|
-
export interface TranslateResponse {
|
|
14
|
-
ok: boolean;
|
|
15
|
-
results: TranslateItemResult[];
|
|
16
|
-
error?: string;
|
|
17
|
-
}
|
|
18
7
|
/** Per-request knobs an adapter may honor; channels that cannot use them ignore the argument. */
|
|
19
8
|
export interface TranslateAdapterOptions {
|
|
20
9
|
/** Generation cap written into the request body; omitted lets the server decide. */
|
|
@@ -28,11 +17,24 @@ export interface ITranslationAdapter {
|
|
|
28
17
|
isAvailable(config: PluginConfig): boolean;
|
|
29
18
|
translate(text: string, signal: AbortSignal, config: PluginConfig, options?: TranslateAdapterOptions): Promise<string>;
|
|
30
19
|
}
|
|
31
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* One reply block's outcome, aligned by index with the request's block list.
|
|
22
|
+
* Failure means the channel was hurt (timeout, HTTP error, broken stream,
|
|
23
|
+
* empty return) — the answer's markdown shape is never a failure reason:
|
|
24
|
+
* drift is repaired by construction, re-rolled once, or accepted as written
|
|
25
|
+
* (the original stays one click away). `detail` carries the technical
|
|
26
|
+
* one-liner for the hover text.
|
|
27
|
+
*/
|
|
32
28
|
export interface ReplyBlockResult {
|
|
33
29
|
original: string;
|
|
34
30
|
translated: string;
|
|
35
31
|
ok: boolean;
|
|
36
32
|
cached: boolean;
|
|
37
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Present only when `ok` is false: the technical one-liner behind the
|
|
35
|
+
* localized label (e.g. `channel timed out after 20000ms` or the expanded
|
|
36
|
+
* fetch error). The client shows `label — detail`, dropping the tail when
|
|
37
|
+
* no detail exists.
|
|
38
|
+
*/
|
|
39
|
+
detail?: string;
|
|
38
40
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lynn123411/dsh-chat-translate",
|
|
3
|
-
"version": "3.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": {
|