@lynn123411/dsh-chat-translate 1.3.2 → 1.5.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.
@@ -1,24 +1,24 @@
1
1
  /**
2
2
  * Mask placeholder tokens — single source of truth for the wire format, the
3
- * tolerant matcher and the leftover detector.
3
+ * matcher and the leftover detector.
4
4
  *
5
- * Wire format: `__DSHMASKx<letters>_<index>__` (e.g. `__DSHMASKxkbdt_3__`).
5
+ * Wire format: `⟦xkbdt3⟧` — U+27E6/U+27E7 (mathematical white square brackets)
6
+ * around `<4-letter random id><index>`.
6
7
  *
7
- * Design constraints, each one a measured failure of the previous
8
- * `__DSH_MASK_<index>__` format against real MT engines:
8
+ * Design constraints, each one a measured failure of an earlier format against
9
+ * real MT engines:
9
10
  *
10
- * - No underscore between `DSH` and `MASK`. Small models routinely drop that
11
- * one separator (`__DSH_MASK_1__` -> `__DSHMASK_1__`), which defeated the
12
- * old strict unmask regex and leaked the raw token into the UI.
13
- * - A random letter run (`x<letters>`) makes the token collision-free against
14
- * the source text and unambiguous to parse: the index is the digit run after
15
- * the last `_`, so no lookbehind is needed.
16
- * - The leading/trailing `__` stay: they survive MT and mark the token as
17
- * emphasis to the model.
18
- *
19
- * `matchMaskToken` stays tolerant of the *legacy* format and of the spacing /
20
- * casing / separator damage observed in the wild, because poisoned translations
21
- * already sit in the on-disk and browser caches.
11
+ * - No letters spelling a pronounceable word and no underscores. The previous
12
+ * `__DSHMASKxkbdt_3__` format named a token that small models abbreviated
13
+ * back to its recognizable core: the UI showed bare `DSH` runs instead of
14
+ * the protected fragments.
15
+ * - Random letters keep two mask passes in the same session from colliding and
16
+ * make the token unambiguous in the translated text.
17
+ * - `matchMaskToken` accepts exactly what this module emits (plus the same
18
+ * token with a dropped closing bracket, an engine rewrite seen in practice).
19
+ * It deliberately does NOT accept anything else: a translation that damaged
20
+ * a token beyond recognition is discarded rather than repaired, because
21
+ * repairing it is what put mixed or duplicated text on screen.
22
22
  */
23
23
  export declare function randomTokenId(): string;
24
24
  export interface MaskTokenFormat {
@@ -29,48 +29,54 @@ export interface MaskTokenFormat {
29
29
  }
30
30
  export declare function createMaskTokenFormat(): MaskTokenFormat;
31
31
  /**
32
- * Loose matcher covering every observed engine rewrite:
33
- *
34
- * __DSHMASKxkbdt_3__ exact
35
- * __DSHMASKXKBDT_3__ case folded
36
- * __DSH MASK xkbdt _ 3__ spaces inserted around the structural underscores
37
- * __DSHMASKkbdt_3__ the `x` id separator eaten
38
- * _DSHMASKxkbdt_3_ one boundary underscore dropped
39
- * __DSH_MASK_3__ / __dsh_mask_3__ legacy format (no random id)
40
- *
41
- * Both boundaries and the internal separators are optional, and the `x` id
42
- * separator may be replaced by whitespace/dot/hyphen. The two index branches
43
- * stay unambiguous: an id is always a letter run, the index always digits.
44
- * Capture groups: `[1] = legacy index` (undefined for the current format),
45
- * `[2] = token id` (undefined for the legacy format), `[3] = current index`.
32
+ * Scanner for complete tokens of this format. A token whose closing bracket the
33
+ * engine dropped is still resolved (see `matchMaskToken`), but it is not
34
+ * recognized as a complete token here: the space `mask()` inserts sits outside
35
+ * that token, and matching a truncated one would strip a space of the
36
+ * translation's own.
37
+ */
38
+ export declare const MASK_TOKEN_PATTERN_SOURCE = "\u27E6([a-z]{4})(\\d+)\u27E7";
39
+ /**
40
+ * The opening bracket, the id and the index of a token, without its closing
41
+ * bracket. Used to resolve a token the engine truncated.
46
42
  */
47
- export declare const MASK_TOKEN_PATTERN_SOURCE = "_*\\s*DSH\\s*_*\\s*MASK\\s*(?:_?\\s*(\\d+)|_*[xX]?[\\s._-]*([a-z]{2,8})\\s*_+\\s*(\\d+))(?:_{0,2}(?=[^\\w]|$))?";
43
+ export declare const MASK_TOKEN_PREFIX_PATTERN_SOURCE = "\u27E6\\s*([a-z]{4})(\\d+)";
44
+ /** Regex source for one retired-format token, for text that still contains one. */
45
+ export declare const LEGACY_MASK_TOKEN_PATTERN_SOURCE = "_{1,2}DSH\\s*_*\\s*MASKx?\\s*(?:_?\\s*\\d+|_*([a-z]{2,8})\\s*_+\\s*(\\d+))_{0,2}";
48
46
  export interface MaskTokenMatch {
47
+ /** Index into the mask list of the `mask()` call that produced the token. */
49
48
  index: number;
50
- /** false when the token carried an id from a different `mask()` call. */
51
- acceptsId: boolean;
52
49
  /** Length of the matched token in characters. */
53
50
  length: number;
51
+ /** True when the closing bracket was missing and only the prefix matched. */
52
+ truncated: boolean;
54
53
  }
55
54
  /**
56
- * Match a single mask token at `start` in `text`.
55
+ * Match exactly one mask token of this format at `start` in `text`.
57
56
  *
58
- * `acceptsId` is false when the token's random id belongs to another masking
59
- * pass — the caller must then leave the token untouched, because its index
60
- * would resolve to unrelated content.
57
+ * The id must match the id of the masking pass that owns the token: every mask
58
+ * pass carries its own random id, so a token carrying another id belongs to
59
+ * another call and must not be resolved here.
61
60
  */
62
61
  export declare function matchMaskToken(text: string, start: number, id: string): MaskTokenMatch | null;
63
- /** True when translated text still carries a mask token of any format. */
62
+ /** Every token of this format in `text`, left to right. */
63
+ export declare function findMaskTokens(text: string): Array<{
64
+ match: MaskTokenMatch;
65
+ raw: string;
66
+ }>;
67
+ /** True when `text` still carries a token of the current format. */
64
68
  export declare function hasMaskResidue(text: string): boolean;
65
- /** Every mask-token-looking fragment in `text`, verbatim. */
66
- export declare function maskResidues(text: string): string[];
69
+ /** True when `text` carries a token of a retired format. */
70
+ export declare function hasLegacyMaskResidue(text: string): boolean;
67
71
  /**
68
- * True when `translatedText` exposes a mask token that was NOT already part of
69
- * `originalText`.
70
- *
71
- * A placeholder surviving the round trip is a leak; but a source text that
72
- * legitimately talks *about* placeholders (this plugin's own documentation,
73
- * say) must survive translation, so tokens the source already carried are not
74
- * treated as residue.
72
+ * Residue test for a value that must never be shown or cached: either a
73
+ * current-format token or any retired-format token.
74
+ */
75
+ export declare function hasAnyMaskResidue(text: string): boolean;
76
+ /**
77
+ * Retired-format tokens inside source text, so `mask()` can protect them with a
78
+ * current-format token of their own. A source that talks about the old
79
+ * placeholder format must survive translation, and the unmask step rejects any
80
+ * retired-format token it sees.
75
81
  */
76
- export declare function hasNewMaskResidue(originalText: string, translatedText: string): boolean;
82
+ export declare function findLegacyMaskTokens(text: string): string[];
@@ -1,27 +1,50 @@
1
+ import { hasMaskResidue } from './mask-tokens.ts';
1
2
  export interface MaskResult {
2
3
  maskedText: string;
4
+ /**
5
+ * Restore every protected fragment. Throws `MaskRestoreError` when the
6
+ * translated text did not carry the token sequence back intact; the caller
7
+ * must discard that translation instead of showing a repaired guess.
8
+ */
3
9
  unmask: (translatedText: string) => string;
10
+ /**
11
+ * Retired-format tokens the source itself contained. They come back on
12
+ * purpose and are content, so the caller's leak check must not flag them.
13
+ */
14
+ legacyFragments: string[];
15
+ }
16
+ /** Which spaces `mask()` inserted directly before and after a token. */
17
+ export interface InsertedSpacing {
18
+ leading: boolean;
19
+ trailing: boolean;
20
+ }
21
+ export declare class MaskRestoreError extends Error {
22
+ readonly expectedCount: number;
23
+ readonly foundCount: number;
24
+ constructor(message: string, expectedCount: number, foundCount: number);
4
25
  }
5
26
  export declare class ContentMaskingPipeline {
6
27
  mask(text: string): MaskResult;
7
28
  }
8
29
  /**
9
- * Resolve every in-range mask token in `text` through `resolve`.
30
+ * Restore every token of `id` in `text` from `masks`.
31
+ *
32
+ * Each fragment of the pass is resolved exactly once, no matter how the engine
33
+ * reordered the sentence: the index embedded in the token identifies the
34
+ * fragment, and the random id identifies the masking pass that owns it. The
35
+ * spaces recorded in `inserted` are removed together with their token, so the
36
+ * spacing the translation produced around the fragment survives untouched.
10
37
  *
11
- * A token whose random id belongs to another masking pass, an out-of-range
12
- * index and any unmatched text are all kept verbatim; `hasMaskResidue()` on the
13
- * result is what flags a leaked token.
38
+ * @throws MaskRestoreError when the text does not carry every token exactly
39
+ * once, or still shows a current-format token or a retired token that the
40
+ * source did not already contain — the caller discards such a translation
41
+ * instead of rendering a repaired guess.
14
42
  */
15
- export declare function replaceMaskTokens(text: string, id: string, resolve: (index: number) => string | undefined): string;
43
+ export declare function replaceMaskTokens(text: string, id: string, masks: string[], inserted?: InsertedSpacing[], legacyIndexes?: ReadonlySet<number>): string;
16
44
  /**
17
- * True when a translated string would still show a mask token to the user.
45
+ * True when a translated string still shows a mask token to the user.
18
46
  * Callers use it to discard a poisoned translation (and to evict poisoned
19
47
  * cache entries written by earlier releases).
20
48
  */
21
49
  export declare function isMaskLeak(translatedText: string): boolean;
22
- /**
23
- * Same as `isMaskLeak`, but a token the source text already contained is not a
24
- * leak: text that talks about placeholders (this plugin's docs, a bug report)
25
- * must round-trip untouched.
26
- */
27
- export declare function isMaskLeakAgainst(originalText: string, translatedText: string): boolean;
50
+ export { hasMaskResidue };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * 思考正文翻译的请求预算。每次请求的输入估算 token 上限,与本地服务的
3
+ * prefill 批次(MAX_NUM_BATCHED_TOKENS)保持同一量级;输出上限写进请求体的
4
+ * max_tokens,避免长文本把剩余上下文全部花在生成上。
5
+ */
6
+ export declare const THINK_MAX_INPUT_TOKENS = 4096;
7
+ export declare const THINK_MAX_OUTPUT_TOKENS = 8192;
8
+ /**
9
+ * 保守估算一段文本占用的 token 数:汉字一字一 token,其余每三个字符一 token。
10
+ * 实测本机 hy-mt2-1.8b 的英文语料约为每 3.3 字符一 token,这里取 3 作为上界,
11
+ * 宁可把请求切得比实际需要更碎,也不让服务端收到超出预期的 prefill。
12
+ */
13
+ export declare function estimateTokens(text: string): number;
14
+ /**
15
+ * 把一个块切成不超过上限的片段:优先空行,其次单行,再次句末,最后硬切。
16
+ * 片段按原顺序拼回即为该块的完整译文。
17
+ */
18
+ export declare function splitOversizedBlock(text: string, maxTokens?: number): string[];
19
+ /** 一个待翻译的片段:属于哪个块、块内第几段、以及该片段的掩码结果。 */
20
+ export interface ThinkPieceShell<TMask> {
21
+ block: number;
22
+ index: number;
23
+ text: string;
24
+ mask: TMask;
25
+ }
26
+ /** 把片段按原顺序打包成尽量少的请求,且每批不超过输入上限。 */
27
+ export declare function packPieces<T extends {
28
+ text: string;
29
+ }>(pieces: T[], maxTokens?: number): T[][];
30
+ /**
31
+ * 块分隔标记:`⟪<4 letters><index>⟫`。外括号刻意与掩码占位符的 ⟦⟧ 不同形,
32
+ * 掩码残留检测(只认 ⟦⟧)因此不会把这个标记当成泄漏。
33
+ */
34
+ export interface ThinkBatchFormat {
35
+ id: string;
36
+ token: (index: number) => string;
37
+ }
38
+ export declare function createThinkBatchFormat(): ThinkBatchFormat;
39
+ /** 每个片段以单独一行标记开头,段与段之间空一行。 */
40
+ export declare function buildBatchPayload(pieces: string[], format: ThinkBatchFormat): string;
41
+ /**
42
+ * 按标记把整批译文切回每个片段。
43
+ *
44
+ * 序号必须恰好出现一次、按 0..count-1 升序、每段非空,且不得混入其它批次
45
+ * 的标记;任何一条不满足都返回 null,由调用方作废整批并退回单块重试。
46
+ */
47
+ export declare function splitBatchTranslation(translated: string, format: ThinkBatchFormat, count: number): string[] | null;
48
+ /** 翻译结果里是否还留着块标记(用于判定整批作废)。 */
49
+ export declare function hasThinkBatchResidue(text: string): boolean;
@@ -1,4 +1,25 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import type { ConfigManager } from './config.ts';
1
+ import type { ConnectionFetchRoute } from '@deepseek-ai/dsh-client-connection';
3
2
  import type { TranslationDispatcher } from './dispatcher.ts';
4
- export declare function createHttpHandler(configManager: ConfigManager, dispatcher: TranslationDispatcher): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
3
+ /**
4
+ * Translation proxy surface, carried by Connection's exact Fetch routes below
5
+ * the shared `/api` channel.
6
+ *
7
+ * Config and credentials have no HTTP endpoints: since 1.2 the settings panel
8
+ * reads and writes through DSH's own channels — the client `SettingsScope`
9
+ * service and the `credentials` Remote API — so the plugin owns exactly three
10
+ * routes: short-text batch translation, think-chain block translation, and the
11
+ * channel probe.
12
+ */
13
+ /** Batch-translation route path. */
14
+ export declare const TRANSLATE_ROUTE_PATH = "/api/dsh-chat-translate/translate";
15
+ /** Single-channel probe route path. */
16
+ export declare const TEST_CHANNEL_ROUTE_PATH = "/api/dsh-chat-translate/test-channel";
17
+ /** Think-chain block translation route path. */
18
+ export declare const THINK_ROUTE_PATH = "/api/dsh-chat-translate/translate-think";
19
+ /**
20
+ * Build the plugin's exact Fetch routes.
21
+ * @param dispatcher - translation coordinator the routes proxy to.
22
+ * @param ready - settles once host-side async resources are usable; every
23
+ * request waits for it before touching the dispatcher.
24
+ */
25
+ export declare function createFetchRoutes(dispatcher: TranslationDispatcher, ready?: Promise<unknown>): ConnectionFetchRoute[];
@@ -3,8 +3,10 @@ export interface PluginConfig {
3
3
  concurrency: number;
4
4
  timeoutMs: number;
5
5
  aiTimeoutMs: number;
6
+ thinkTimeoutMs: number;
6
7
  aiEnabled: boolean;
7
8
  bingEnabled: boolean;
9
+ thinkEnabled: boolean;
8
10
  baseUrl: string;
9
11
  model: string;
10
12
  targetLang: string;
@@ -20,9 +22,24 @@ export interface TranslateResponse {
20
22
  results: TranslateItemResult[];
21
23
  error?: string;
22
24
  }
25
+ /** Per-request knobs an adapter may honor; channels that cannot use them ignore the argument. */
26
+ export interface TranslateAdapterOptions {
27
+ /** Generation cap written into the request body; omitted lets the server decide. */
28
+ maxTokens?: number;
29
+ /** Prompt family: one plain sentence, or a marked multi-part packing. */
30
+ mode?: 'plain' | 'blocks';
31
+ }
23
32
  export interface ITranslationAdapter {
24
33
  readonly id: string;
25
34
  readonly name: string;
26
35
  isAvailable(config: PluginConfig): boolean;
27
- translate(text: string, signal: AbortSignal, config: PluginConfig): Promise<string>;
36
+ translate(text: string, signal: AbortSignal, config: PluginConfig, options?: TranslateAdapterOptions): Promise<string>;
37
+ }
38
+ /** One reasoning block's outcome, aligned by index with the request's block list. */
39
+ export interface ThinkBlockResult {
40
+ original: string;
41
+ translated: string;
42
+ ok: boolean;
43
+ cached: boolean;
44
+ channel: string;
28
45
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lynn123411/dsh-chat-translate",
3
- "version": "1.3.2",
4
- "description": "Tool-call & think-summary translation with dual AI (OpenAI-compatible) + Bing channels for the DeepSeek Harness Web UI",
3
+ "version": "1.5.0",
4
+ "description": "Tool-call & think-chain translation with dual AI (OpenAI-compatible) + Bing channels for the DeepSeek Harness Web UI",
5
5
  "license": "MIT",
6
6
  "author": "lynn123411",
7
7
  "repository": {
@@ -47,7 +47,9 @@
47
47
  "client": {
48
48
  "platform": "web",
49
49
  "immediately": true,
50
- "inject": []
50
+ "inject": [
51
+ "@deepseek-ai/dsh-client-ui-primitives"
52
+ ]
51
53
  }
52
54
  },
53
55
  "files": [
@@ -59,17 +61,22 @@
59
61
  "scripts": {
60
62
  "build": "node build.mjs",
61
63
  "typecheck": "tsc -p tsconfig.json --noEmit",
62
- "test": "node scripts/test-client-wiring.mjs && node scripts/test-regression-suite.mjs && node scripts/test-channel-logic.mjs && node scripts/test-store.mjs && node scripts/test-toggle-restore.mjs"
64
+ "test": "node scripts/test-client-wiring.mjs && node scripts/test-regression-suite.mjs && node scripts/test-channel-logic.mjs && node scripts/test-store.mjs && node scripts/test-toggle-restore.mjs && node scripts/test-think-pipeline.mjs && node scripts/test-think-client.mjs",
65
+ "assert:lib-untracked": "node scripts/assert-lib-untracked.mjs",
66
+ "check": "pnpm typecheck && pnpm build && pnpm test && pnpm assert:lib-untracked"
63
67
  },
64
68
  "engines": {
65
- "node": "^22.19 || >=24"
69
+ "node": "^22.19 || >=24",
70
+ "dsh": "0.1.6-alpha.2"
66
71
  },
67
72
  "devDependencies": {
68
73
  "@deepseek-ai/cordis": "4.0.2",
69
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.1",
70
- "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.1",
71
- "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.1",
72
- "@deepseek-ai/dsh-home-paths": "^0.1.5-rc.1",
74
+ "@deepseek-ai/dsh-client-connection": "0.1.6-alpha.2",
75
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.6-alpha.2",
76
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.6-alpha.2",
77
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.6-alpha.2",
78
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.2",
79
+ "@deepseek-ai/dsh-home-paths": "0.1.6-alpha.2",
73
80
  "@deepseek-ai/schemastery": "^3.18.2",
74
81
  "@eslint/js": "^9.30.0",
75
82
  "@types/node": "^22.19.0",
@@ -79,6 +86,12 @@
79
86
  "typescript": "^6.0.0"
80
87
  },
81
88
  "peerDependencies": {
82
- "@deepseek-ai/dsh-home-paths": "^0.1.5-rc.1"
89
+ "@deepseek-ai/dsh-client-ui-primitives": "*",
90
+ "@deepseek-ai/dsh-home-paths": "0.1.6-alpha.2"
91
+ },
92
+ "peerDependenciesMeta": {
93
+ "@deepseek-ai/dsh-client-ui-primitives": {
94
+ "optional": true
95
+ }
83
96
  }
84
97
  }