@lynn123411/dsh-chat-translate 3.2.0 → 3.3.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 +4 -4
- package/assets/icon.svg +6 -0
- package/docs//351/205/215/347/275/256.md +2 -2
- package/lib/client.js +73 -211
- package/lib/client.js.map +4 -4
- package/lib/index.js +1 -1
- package/lib/index.js.map +3 -3
- package/lib/types/client/index.d.ts +11 -12
- package/lib/types/client/locales.d.ts +1 -3
- package/lib/types/client/settings/plugin-page.d.ts +18 -0
- package/lib/types/client/settings/ui.d.ts +12 -9
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +20 -13
- package/lib/types/client/reading-settings-page.d.ts +0 -92
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @lynn123411/dsh-chat-translate
|
|
2
2
|
|
|
3
|
-
DeepSeek Harness Web 界面的**助手回答正文翻译**插件。它接管助手行的渲染:正文块不论源语言,一律送翻译模型加工成自然简体中文(如 `I traced the failing path to a stale lock file.` → 中文;模型写出的生硬机器腔中文同样送一遍润色——弱模型的中文恰恰最需要加工),译文按原 markdown 结构重新渲染。只翻正文:Think
|
|
3
|
+
DeepSeek Harness Web 界面的**助手回答正文翻译**插件。它接管助手行的渲染:正文块不论源语言,一律送翻译模型加工成自然简体中文(如 `I traced the failing path to a stale lock file.` → 中文;模型写出的生硬机器腔中文同样送一遍润色——弱模型的中文恰恰最需要加工),译文按原 markdown 结构重新渲染。只翻正文:Think 卡的思考链、工具调用行、用户消息、代码块、空白块不动。正文块的左缘竖线报出状态:译文蓝粗线、备着译文的原文灰细线、失败红实线(悬停报通道细节:超时、断流、空返回)、在途灰脉动、没送过模型无线。切换与补跑都在**贴着那条线的窄带上**(鼠标移上去变手型,点一下切双语;挂红线的块点它手动补跑),正文整块不再响应点击——选中文字、拖选复制都不会误触。不污染会话上下文。开关有两个入口:输入框下方那一行的「译」胶囊与本插件配置页的总开关,同一个状态两处同步。配置详见 [docs/配置.md](./docs/配置.md)。
|
|
4
4
|
|
|
5
5
|
## 特性
|
|
6
6
|
|
|
@@ -12,11 +12,11 @@ DeepSeek Harness Web 界面的**助手回答正文翻译**插件。它接管助
|
|
|
12
12
|
- **失败只有一种:通道伤**:块级失败不进缓存、保持原文,左缘挂红实线,悬停左缘那条窄带报出通道细节(超时、断流、空返回)。译文的结构漂移不再是失败理由(见下条)。红线块右侧常驻一个小 ↻,点那条窄带即整行手动补跑,已成功的块保持挂线展示(宿主磁盘缓存让它们秒回,不花模型调用),只有尚无译文的块重新脉动;在途时窄带仍在(键盘焦点不掉)但按下去是空操作,光标也不再变手型。通道失败不自动重试:一行首跑一次,落定即终态,救活由用户发起。失败仍在控制台留一条警告。
|
|
13
13
|
- **按行幂等的翻译池**:客户端按会话锚点键登记整行结果,同文本重复渲染不重发请求;行内容换代则整行重译,迟到的旧代结果作废。
|
|
14
14
|
- **结构重装配,形状问题只修不拒**:宿主先把块按 markdown 结构切段——代码围栏与纯空白段**逐字保留、永不送模型**——散文段再按输入上限切片(单段超限按空行、单行、句末切开)、相邻片段打包成一个请求(输入上限 4096 估算 token、输出上限 8192 token)。回来的译文过**形状修复**(`repairShape`):按段落块对齐,块内结构行(记号类别、表格竖线数、链接个数、缩进代码缩进)逐一对齐并把行首前缀修回原文同款(`##`→`###`、`-`→`*`、序号、引用深度、列表缩进的漂移就此消失),散文行自由重排(软换行渲染无感),空行布局一律取原文,`[文字](URL)` 的 URL 按出现序逐字拼回。修不齐的片段自动重掷一次;仍不齐就照收模型的译文——原文一键可回,可读的译文优先于红线。反引号、行内代码内容一律不拦:是样式不是骨架。
|
|
15
|
-
- **单通道:AI 翻译(OpenAI 兼容协议)**:对接任意 OpenAI 兼容的 `chat/completions` 服务(OpenAI、DeepSeek、通义、Ollama、本地 vLLM 等)。Key / Base URL / 模型三者齐备才翻译,缺任何一项保持原文,不做降级兜底。API Key 经 DSH 凭据服务读写 `~/.dsh/.credentials.yaml` 的 `TRANSLATE_API_KEY`;Base URL
|
|
15
|
+
- **单通道:AI 翻译(OpenAI 兼容协议)**:对接任意 OpenAI 兼容的 `chat/completions` 服务(OpenAI、DeepSeek、通义、Ollama、本地 vLLM 等)。Key / Base URL / 模型三者齐备才翻译,缺任何一项保持原文,不做降级兜底。API Key 经 DSH 凭据服务读写 `~/.dsh/.credentials.yaml` 的 `TRANSLATE_API_KEY`;Base URL 与模型在本插件的配置页配置。
|
|
16
16
|
- **串行与超时**:同时最多一个在途请求,单次超时 `aiTimeoutMs`(默认 600000 毫秒,范围 500–900000)——每行首跑与每次手点都等到真实结果,嫌等就把超时调小。磁盘缓存池 `~/.dsh/dsh-chat-translate/cache.json`(7 天 TTL、1000 条 LRU)**按提示词修订号整体作废**:文档里登记 `rev`,换提示词语义后旧译文不会串代。
|
|
17
17
|
- **跟随宿主显示设置**:Think 行的折叠摘要预览跟随「工作细节」模式(读 `ui-chat` 配置的 `transcriptView`,含 legacy 取值映射);设置缺席时按 standard。
|
|
18
|
-
- **DSH 原生配置接入**:配置就是本插件在 profile
|
|
19
|
-
-
|
|
18
|
+
- **DSH 原生配置接入**:配置就是本插件在 profile 里的插件配置——配置页经 `ctx.configForms` 读写条目 `dsh-chat-translate` 的 `config`,用户层落在当前 profile 的 patch;无任何自研配置文件,译文从不写回会话上下文。
|
|
19
|
+
- **配置页挂在宿主「插件」页**:配置卡片注册进 `plugins.bundle.config`(key 为包名 `@lynn123411/dsh-chat-translate`),路径是「插件」→本插件详情页 —— 配置直接铺在描述之下、行列表之上。页面由宿主画标题、图标与面包屑,卡片提供总开关、API Key、Base URL、模型、单次请求超时与「测试 AI 通道」;设置页里不再有任何本插件的入口。做法与约束见仓库 `docs/rules/plugin-config-page.md`。
|
|
20
20
|
|
|
21
21
|
## 安装
|
|
22
22
|
|
package/assets/icon.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<path d="M2.5 6.5A2.5 2.5 0 0 1 5 4h6.5A2.5 2.5 0 0 1 14 6.5v3.5A2.5 2.5 0 0 1 11.5 12.5H7.5L4.5 15v-2.5A2.5 2.5 0 0 1 2.5 10z"/>
|
|
3
|
+
<path d="M10 13A2.5 2.5 0 0 1 12.5 10.5h6.5A2.5 2.5 0 0 1 21.5 13v3.5A2.5 2.5 0 0 1 19 19h-4L12 21.5V19A2.5 2.5 0 0 1 10 16.5z"/>
|
|
4
|
+
<path d="M5.5 7.8h5.5"/>
|
|
5
|
+
<path d="M5.5 10.1h3.5"/>
|
|
6
|
+
</svg>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 本文档为 [@lynn123411/dsh-chat-translate](../README.md) 的配置说明。
|
|
4
4
|
|
|
5
|
-
1. **API Key
|
|
5
|
+
1. **API Key**:打开「插件」→ 本插件详情页(配置就在描述之下的 AI 翻译卡片里),直接填写 API Key 并保存(经 DSH 凭据服务写入 `~/.dsh/.credentials.yaml` 的 `TRANSLATE_API_KEY`,权限 0600,保存后立即生效;留空保存 = 清除该键)。也可以手动编辑该文件:
|
|
6
6
|
|
|
7
7
|
```yaml
|
|
8
8
|
refs:
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
> ⚠️ **注意**:DSH 的凭据加载器要求 refs 键值非空。手动编辑时要么填写真实 Key,要么**直接删除该行**——不要写成 `TRANSLATE_API_KEY: ""` 这类空值,否则 DSH 启动会失败。未配置时插件不翻译,正文保持原文,不会报错。
|
|
13
13
|
|
|
14
|
-
2. **Base URL
|
|
14
|
+
2. **Base URL 与模型等设置**:在本插件的配置页里填写 Base URL(如 `https://api.openai.com/v1` 或 `https://api.deepseek.com/v1`)与模型名(如 `gpt-4o-mini`、`deepseek-chat`),点击「测试 AI 通道」验证。所有设置(开关、超时、Base URL、模型)都是本插件在 profile 里的插件配置:写入当前 profile 的 patch(`~/.dsh/profiles/<profile>/cordis.patch.yml`)中 `dsh-chat-translate` 条目的 `config`。开关 `enabled` 有两个入口:本插件配置页的总开关,与聊天输入框下方那一行(composer dock)的「译」胶囊——两处读写同一个字段,状态同步;这一行只在会话形态渲染,新建会话的 Hero 形态没有它。字段都是可实时更新的,改完立即生效,可直接编辑该文件,不需重载:
|
|
15
15
|
|
|
16
16
|
```yaml
|
|
17
17
|
- id: dsh-chat-translate
|
package/lib/client.js
CHANGED
|
@@ -50,7 +50,7 @@ __export(index_exports, {
|
|
|
50
50
|
module.exports = __toCommonJS(index_exports);
|
|
51
51
|
|
|
52
52
|
// src/client/settings/ui.tsx
|
|
53
|
-
var
|
|
53
|
+
var import_react = require("react");
|
|
54
54
|
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
55
55
|
|
|
56
56
|
// src/describe-error.ts
|
|
@@ -313,7 +313,7 @@ var SETTINGS_CSS = String.raw`
|
|
|
313
313
|
display: flex;
|
|
314
314
|
flex-direction: column;
|
|
315
315
|
gap: 16px;
|
|
316
|
-
|
|
316
|
+
width: 100%;
|
|
317
317
|
padding-bottom: 32px;
|
|
318
318
|
color: var(--dsw-alias-label-primary);
|
|
319
319
|
font: var(--dsw-font-s-14);
|
|
@@ -338,7 +338,14 @@ var SETTINGS_CSS = String.raw`
|
|
|
338
338
|
font: var(--dsw-font-s-strong-14);
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
/*
|
|
342
|
+
* Prose keeps a reading measure even though the card spans the host column: the
|
|
343
|
+
* master description is a 500-character paragraph, and at full width it would
|
|
344
|
+
* run to ~76 CJK characters per line. 46em tracks the element's own 12px font,
|
|
345
|
+
* so the measure survives a font-size change.
|
|
346
|
+
*/
|
|
341
347
|
.dsh-tidy-desc {
|
|
348
|
+
max-width: 46em;
|
|
342
349
|
color: var(--dsw-alias-label-secondary);
|
|
343
350
|
font: var(--dsw-font-xxs-12);
|
|
344
351
|
}
|
|
@@ -404,144 +411,9 @@ var SETTINGS_CSS = String.raw`
|
|
|
404
411
|
|
|
405
412
|
`;
|
|
406
413
|
|
|
407
|
-
// src/client/reading-settings-page.tsx
|
|
408
|
-
var import_react = require("react");
|
|
409
|
-
var READING_PAGE_ID = "reading";
|
|
410
|
-
var READING_PAGE_ORDER = 110;
|
|
411
|
-
var READING_ITEM_SLOT = "reading.settings.item";
|
|
412
|
-
function readLabel(label) {
|
|
413
|
-
if (typeof label === "function") return label();
|
|
414
|
-
return typeof label === "string" ? label : "";
|
|
415
|
-
}
|
|
416
|
-
var TABLIST_STYLE = {
|
|
417
|
-
display: "flex",
|
|
418
|
-
alignItems: "flex-end",
|
|
419
|
-
gap: "22px",
|
|
420
|
-
marginTop: "2px",
|
|
421
|
-
marginBottom: "16px"
|
|
422
|
-
};
|
|
423
|
-
var TAB_STYLE = {
|
|
424
|
-
appearance: "none",
|
|
425
|
-
background: "transparent",
|
|
426
|
-
// No border on ANY tab: the marker below is the active tab's own element, so
|
|
427
|
-
// an inactive tab has nothing that could render a line.
|
|
428
|
-
border: "none",
|
|
429
|
-
position: "relative",
|
|
430
|
-
padding: "7px 1px 11px",
|
|
431
|
-
cursor: "pointer",
|
|
432
|
-
font: "var(--dsw-font-xs-13)",
|
|
433
|
-
color: "var(--dsw-alias-label-tertiary)"
|
|
434
|
-
};
|
|
435
|
-
var TAB_ACTIVE_STYLE = {
|
|
436
|
-
...TAB_STYLE,
|
|
437
|
-
color: "var(--dsw-alias-label-primary)"
|
|
438
|
-
};
|
|
439
|
-
var TAB_MARKER_STYLE = {
|
|
440
|
-
position: "absolute",
|
|
441
|
-
left: 0,
|
|
442
|
-
right: 0,
|
|
443
|
-
bottom: 0,
|
|
444
|
-
height: "2px",
|
|
445
|
-
borderRadius: "2px 2px 0 0",
|
|
446
|
-
background: "var(--dsw-alias-label-primary)"
|
|
447
|
-
};
|
|
448
|
-
var PANEL_STYLE = { listStyle: "none", margin: 0, padding: 0 };
|
|
449
|
-
var PANEL_HIDDEN_STYLE = { ...PANEL_STYLE, display: "none" };
|
|
450
|
-
function createReadingTabs(ctx) {
|
|
451
|
-
const locale = ctx.get("locale");
|
|
452
|
-
let version = -1;
|
|
453
|
-
let revision = -1;
|
|
454
|
-
let tabs = [];
|
|
455
|
-
return {
|
|
456
|
-
getSnapshot: () => {
|
|
457
|
-
const nextVersion = ctx.slots.getVersion(READING_ITEM_SLOT);
|
|
458
|
-
const nextRevision = locale === void 0 ? 0 : locale.getSnapshot().revision;
|
|
459
|
-
if (nextVersion === version && nextRevision === revision) return tabs;
|
|
460
|
-
version = nextVersion;
|
|
461
|
-
revision = nextRevision;
|
|
462
|
-
tabs = ctx.slots.entries(READING_ITEM_SLOT).map((entry) => ({
|
|
463
|
-
id: entry.options.id ?? "",
|
|
464
|
-
order: entry.options.order ?? 0,
|
|
465
|
-
label: readLabel(entry.options.label)
|
|
466
|
-
})).sort((left, right) => left.order - right.order);
|
|
467
|
-
return tabs;
|
|
468
|
-
},
|
|
469
|
-
subscribe: (listener) => {
|
|
470
|
-
const offSlots = ctx.slots.subscribe(READING_ITEM_SLOT, listener);
|
|
471
|
-
const offLocale = locale?.subscribe(listener);
|
|
472
|
-
return () => {
|
|
473
|
-
offSlots();
|
|
474
|
-
offLocale?.();
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
function ReadingSettingsSection({ renderSlot, readingTabs }) {
|
|
480
|
-
const tabs = (0, import_react.useSyncExternalStore)(
|
|
481
|
-
readingTabs.subscribe,
|
|
482
|
-
readingTabs.getSnapshot,
|
|
483
|
-
readingTabs.getSnapshot
|
|
484
|
-
);
|
|
485
|
-
const [requested, setRequested] = (0, import_react.useState)(null);
|
|
486
|
-
const selected = requested !== null && tabs.some((tab) => tab.id === requested) ? requested : tabs[0]?.id ?? null;
|
|
487
|
-
if (selected === null) return null;
|
|
488
|
-
return (0, import_react.createElement)(
|
|
489
|
-
"div",
|
|
490
|
-
null,
|
|
491
|
-
(0, import_react.createElement)(
|
|
492
|
-
"div",
|
|
493
|
-
{ role: "tablist", style: TABLIST_STYLE },
|
|
494
|
-
tabs.map((tab) => (0, import_react.createElement)(
|
|
495
|
-
"button",
|
|
496
|
-
{
|
|
497
|
-
key: tab.id,
|
|
498
|
-
type: "button",
|
|
499
|
-
role: "tab",
|
|
500
|
-
"aria-selected": tab.id === selected,
|
|
501
|
-
style: tab.id === selected ? TAB_ACTIVE_STYLE : TAB_STYLE,
|
|
502
|
-
onClick: () => {
|
|
503
|
-
setRequested(tab.id);
|
|
504
|
-
}
|
|
505
|
-
},
|
|
506
|
-
tab.label,
|
|
507
|
-
tab.id === selected ? (0, import_react.createElement)("span", { style: TAB_MARKER_STYLE, "aria-hidden": true }) : null
|
|
508
|
-
))
|
|
509
|
-
),
|
|
510
|
-
tabs.map((tab) => (0, import_react.createElement)(
|
|
511
|
-
"ul",
|
|
512
|
-
{
|
|
513
|
-
key: tab.id,
|
|
514
|
-
role: "tabpanel",
|
|
515
|
-
hidden: tab.id !== selected,
|
|
516
|
-
style: tab.id === selected ? PANEL_STYLE : PANEL_HIDDEN_STYLE
|
|
517
|
-
},
|
|
518
|
-
renderSlot(READING_ITEM_SLOT, {}, { only: tab.id })
|
|
519
|
-
))
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
function readingPageClaimed(ctx) {
|
|
523
|
-
return ctx.slots.entries("settings.section").some((entry) => entry.options.id === READING_PAGE_ID);
|
|
524
|
-
}
|
|
525
|
-
function claimReadingSettingsPage(ctx, label, locale) {
|
|
526
|
-
if (readingPageClaimed(ctx)) return () => {
|
|
527
|
-
};
|
|
528
|
-
const readingTabs = createReadingTabs(ctx);
|
|
529
|
-
return ctx.slots.register({
|
|
530
|
-
name: "settings.section",
|
|
531
|
-
id: READING_PAGE_ID,
|
|
532
|
-
order: READING_PAGE_ORDER,
|
|
533
|
-
label,
|
|
534
|
-
locale,
|
|
535
|
-
inject: () => ({ readingTabs }),
|
|
536
|
-
children: { "reading.settings.item": { kind: "list", scope: "root" } }
|
|
537
|
-
}, ReadingSettingsSection);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
414
|
// src/client/locales.ts
|
|
541
415
|
var NS = "settings.chatTranslate";
|
|
542
416
|
var zh = {
|
|
543
|
-
pageNav: "\u9605\u8BFB\u4F53\u9A8C",
|
|
544
|
-
title: "\u6B63\u6587\u7FFB\u8BD1",
|
|
545
417
|
masterTitle: "\u7FFB\u8BD1\u603B\u5F00\u5173",
|
|
546
418
|
enableTranslation: "\u542F\u7528\u7FFB\u8BD1",
|
|
547
419
|
masterDesc: "\u81EA\u52A8\u628A\u52A9\u624B\u56DE\u7B54\u6B63\u6587\u7FFB\u6210\u4E2D\u6587\uFF08\u5982 {example}\uFF09\uFF1A\u6298\u53E0\u5757\u91CC\u7684\u8FC7\u7A0B\u6B63\u6587\u4E0E\u6700\u7EC8\u6C47\u603B\u4E00\u89C6\u540C\u4EC1\uFF0C\u6574\u6BB5\u4E2D\u6587\u7684\u5757\u4E5F\u7167\u9001\uFF0C\u7531\u6A21\u578B\u4E00\u5E76\u6539\u5199\u6210\u81EA\u7136\u4E2D\u6587\u3001\u987A\u6389\u673A\u5668\u8154\u3002Think \u5361\u4E0E\u5DE5\u5177\u8C03\u7528\u884C\u4E0D\u7FFB\uFF0C\u4EE3\u7801\u5757\u539F\u6837\u4FDD\u7559\u3002\u6B63\u6587\u5757\u7684\u5DE6\u7F18\u4E00\u6761\u7AD6\u7EBF\u62A5\u51FA\u72B6\u6001\uFF1A\u6B63\u5728\u8BFB\u8BD1\u6587\u662F\u84DD\u7EBF\uFF0C\u5907\u7740\u8BD1\u6587\u6B63\u8BFB\u539F\u6587\u662F\u7070\u7EC6\u7EBF\uFF0C\u6CA1\u8BD1\u6210\u662F\u7EA2\u5B9E\u7EBF\uFF08\u9F20\u6807\u60AC\u505C\u70ED\u533A\u62A5\u51FA\u901A\u9053\u7EC6\u8282\u2014\u2014\u8D85\u65F6\u3001\u65AD\u6D41\u6216\u7A7A\u8FD4\u56DE\uFF1B\u8BD1\u6587\u7684\u7ED3\u6784\u6F02\u79FB\u7531\u5BBF\u4E3B\u81EA\u52A8\u4FEE\u56DE\u6216\u91CD\u63B7\uFF0C\u4E0D\u518D\u56E0\u683C\u5F0F\u95EE\u9898\u6302\u7EA2\u7EBF\uFF09\uFF0C\u6B63\u5728\u8BF7\u6C42\u662F\u7070\u8109\u52A8\u7EBF\uFF0C\u6CA1\u9001\u8FC7\u6A21\u578B\u6216\u5F00\u5173\u5173\u95ED\u6CA1\u6709\u7EBF\u3002\u5207\u6362\u4E0D\u5728\u6B63\u6587\u4E0A\uFF1A\u8D34\u7740\u90A3\u6761\u7EBF\u7684\u7A84\u5E26\u624D\u662F\u64CD\u4F5C\u533A\uFF0C\u9F20\u6807\u79FB\u5230\u5B83\u4E0A\u9762\u53D8\u624B\u578B\uFF0C\u70B9\u4E00\u4E0B\u5728\u8BD1\u6587\u4E0E\u539F\u6587\u95F4\u5207\u6362\u2014\u2014\u6B63\u6587\u6574\u5757\u4E0D\u518D\u54CD\u5E94\u70B9\u51FB\uFF0C\u9009\u5B57\u3001\u62D6\u9009\u590D\u5236\u90FD\u4E0D\u4F1A\u8BEF\u89E6\uFF1B\u6A21\u578B\u8BA4\u4E3A\u539F\u6837\u6700\u597D\u3001\u8FD4\u56DE\u4E00\u6A21\u4E00\u6837\u7684\u6587\u5B57\u65F6\u540C\u6837\u7B97\u7FFB\u8BD1\u8FC7\u3001\u540C\u6837\u53EF\u5207\u3002\u6302\u7EA2\u7EBF\u7684\u5757\u53F3\u4FA7\u5E38\u9A7B\u4E00\u4E2A\u5C0F \u21BB\uFF0C\u70B9\u90A3\u6761\u7A84\u5E26\u5373\u6574\u884C\u91CD\u65B0\u7FFB\u8BD1\uFF0C\u5DF2\u6210\u529F\u7684\u5757\u4FDD\u6301\u663E\u793A\u4E0D\u6253\u6270\u3002\u901A\u9053\u5931\u8D25\u4E0D\u81EA\u52A8\u91CD\u8BD5\uFF0C\u6062\u590D\u540E\u7531\u4F60\u70B9\u90A3\u6761\u7A84\u5E26\u6551\u6D3B\u3002\u8BD1\u6587\u6309\u539F markdown \u683C\u5F0F\u91CD\u65B0\u6E32\u67D3\uFF0C\u8868\u683C\u3001\u5217\u8868\u3001\u52A0\u7C97\u4E0E\u884C\u5185\u4EE3\u7801\u4FDD\u6301\u7ED3\u6784\u3002\u56DE\u7B54\u8FD8\u5728\u6D41\u5F0F\u8F93\u51FA\u65F6\u4E0D\u7FFB\uFF0C\u843D\u5B9A\u540E\u6309\u9605\u8BFB\u987A\u5E8F\u9010\u6BB5\u51FA\u4E2D\u6587\u3002\u53EA\u4F5C\u7528\u4E8E\u5F53\u524D\u67E5\u770B\u7684\u4F1A\u8BDD\uFF0C\u8BD1\u6587\u4E0D\u5199\u56DE\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u3002\u8F93\u5165\u6846\u4E0B\u65B9\u90A3\u4E00\u884C\u6709\u540C\u6B3E\u5F00\u5173\uFF0C\u4E24\u5904\u72B6\u6001\u540C\u6B65\u3002",
|
|
@@ -580,8 +452,6 @@ var zh = {
|
|
|
580
452
|
inflightAria: "\u7FFB\u8BD1\u4E2D\uFF0C\u6B64\u5904\u6682\u65E0\u52A8\u4F5C"
|
|
581
453
|
};
|
|
582
454
|
var en = {
|
|
583
|
-
pageNav: "Reading",
|
|
584
|
-
title: "Reply translate",
|
|
585
455
|
masterTitle: "Translation master switch",
|
|
586
456
|
enableTranslation: "Enable translation",
|
|
587
457
|
masterDesc: "Automatically translates assistant reply prose (e.g. {example}) into Chinese: text inside disclosure blocks and the final summary are treated alike, and blocks written entirely in Chinese go through the model too, which rewrites them into natural Chinese and smooths out machine-flavored phrasing. Think cards and tool-call rows are never translated, and code blocks stay untouched. Each prose block reports its state with a left-edge line: blue while its translation reads, thin grey while the original reads with a translation waiting one click away, solid red when translation failed (hover the slim strip beside the line to read the channel detail \u2014 a timeout, a broken stream or an empty return; the answer's Markdown shape drift is repaired or re-rolled by the host, so formatting no longer draws a red line), a grey pulse while a request is in flight, and nothing at all when the block was never sent or the switch is off. Switching does not live on the prose: a slim strip hugging that line is the control \u2014 the pointer turns into a hand over it, and one click swaps translation and original, while the prose itself no longer reacts to clicks, so selecting or drag-copying text never triggers a swap by accident. A reply the model returned unchanged still counts as translated and stays switchable. A red-lined block keeps a small \u21BB beside it; clicking the strip re-runs the whole row, with already-successful blocks kept on screen undisturbed. Channel failures are never retried automatically: once the channel recovers, a click on that strip revives the row. Translations are re-rendered as Markdown, so tables, lists, bold runs and inline code keep their structure. A reply that is still streaming is left alone until it settles, then prose turns to Chinese paragraph by paragraph in reading order. Applies to the session you are viewing only; translations never enter the conversation context. The same switch sits on the row below the composer, sharing its state.",
|
|
@@ -639,6 +509,16 @@ function rowCopy() {
|
|
|
639
509
|
};
|
|
640
510
|
}
|
|
641
511
|
|
|
512
|
+
// src/client/settings/plugin-page.ts
|
|
513
|
+
var BUNDLE_NAME = "@lynn123411/dsh-chat-translate";
|
|
514
|
+
var BUNDLE_CONFIG_SLOT = "plugins.bundle.config";
|
|
515
|
+
var BUNDLE_CONFIG_KEY = BUNDLE_NAME;
|
|
516
|
+
function setupPluginPage(slots, register) {
|
|
517
|
+
if (!slots || typeof slots.inject !== "function") return () => {
|
|
518
|
+
};
|
|
519
|
+
return slots.inject(BUNDLE_CONFIG_SLOT, register);
|
|
520
|
+
}
|
|
521
|
+
|
|
642
522
|
// src/client/settings/ui.tsx
|
|
643
523
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
644
524
|
var stylesInjected = false;
|
|
@@ -659,14 +539,14 @@ function withSamples(text, samples) {
|
|
|
659
539
|
});
|
|
660
540
|
}
|
|
661
541
|
function TidySettingsPanel(props) {
|
|
662
|
-
ensureSettingsStyles();
|
|
663
542
|
const { t } = props;
|
|
664
|
-
|
|
665
|
-
const [
|
|
666
|
-
const [
|
|
667
|
-
const [
|
|
668
|
-
const [
|
|
669
|
-
(0,
|
|
543
|
+
ensureSettingsStyles();
|
|
544
|
+
const [state, setState] = (0, import_react.useState)(() => settingsStore.getState());
|
|
545
|
+
const [testing, setTesting] = (0, import_react.useState)(null);
|
|
546
|
+
const [apiKeyInput, setApiKeyInput] = (0, import_react.useState)("");
|
|
547
|
+
const [savingKey, setSavingKey] = (0, import_react.useState)(false);
|
|
548
|
+
const [keyMsg, setKeyMsg] = (0, import_react.useState)(null);
|
|
549
|
+
(0, import_react.useEffect)(() => {
|
|
670
550
|
return settingsStore.subscribe(() => {
|
|
671
551
|
setState(settingsStore.getState());
|
|
672
552
|
});
|
|
@@ -853,52 +733,34 @@ function setupSettingsUi(ctx) {
|
|
|
853
733
|
"dsh-chat-translate: locale dictionaries"
|
|
854
734
|
);
|
|
855
735
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
order: 30,
|
|
875
|
-
// 共享页按此标签渲染 tab
|
|
876
|
-
label: () => t("title"),
|
|
877
|
-
locale: NS
|
|
878
|
-
},
|
|
879
|
-
TidySettingsPanel
|
|
880
|
-
)
|
|
881
|
-
);
|
|
882
|
-
return () => {
|
|
883
|
-
offCard();
|
|
884
|
-
offPage();
|
|
885
|
-
};
|
|
886
|
-
}),
|
|
887
|
-
"dsh-chat-translate: shared reading page"
|
|
888
|
-
);
|
|
889
|
-
} catch (err) {
|
|
890
|
-
console.warn("[dsh-chat-translate] Failed to inject settings section:", err);
|
|
891
|
-
}
|
|
736
|
+
ctx.effect(
|
|
737
|
+
() => configForms.whileServed([SETTINGS_NAMESPACE], () => setupPluginPage(
|
|
738
|
+
ctx.slots,
|
|
739
|
+
() => ctx.slots.register(
|
|
740
|
+
{
|
|
741
|
+
name: BUNDLE_CONFIG_SLOT,
|
|
742
|
+
// The bundle's package name: the key that puts this entry on this
|
|
743
|
+
// bundle's own page, above its rows.
|
|
744
|
+
key: BUNDLE_CONFIG_KEY,
|
|
745
|
+
// The page draws the title, icon and crumb itself; the card keeps its
|
|
746
|
+
// own chrome, controls and section order.
|
|
747
|
+
locale: NS
|
|
748
|
+
},
|
|
749
|
+
TidySettingsPanel
|
|
750
|
+
)
|
|
751
|
+
)),
|
|
752
|
+
"dsh-chat-translate: plugins page config"
|
|
753
|
+
);
|
|
892
754
|
}
|
|
893
755
|
|
|
894
756
|
// src/client/composer/dock-toggle.tsx
|
|
895
|
-
var
|
|
757
|
+
var import_react2 = require("react");
|
|
896
758
|
var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
897
759
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
898
760
|
function ComposerTranslateToggle(props) {
|
|
899
761
|
const { t } = props;
|
|
900
|
-
const [state, setState] = (0,
|
|
901
|
-
(0,
|
|
762
|
+
const [state, setState] = (0, import_react2.useState)(() => settingsStore.getState());
|
|
763
|
+
(0, import_react2.useEffect)(() => settingsStore.subscribe(() => setState(settingsStore.getState())), []);
|
|
902
764
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.Tooltip, { label: () => state.enabled ? t("dockOn") : t("dockOff"), side: "top", align: "end", portal: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
903
765
|
import_dsh_client_ui_primitives2.Pill,
|
|
904
766
|
{
|
|
@@ -931,7 +793,7 @@ function setupComposerToggle(ctx) {
|
|
|
931
793
|
|
|
932
794
|
// src/client/chat/assistant-step.tsx
|
|
933
795
|
var React = __toESM(require("react"), 1);
|
|
934
|
-
var
|
|
796
|
+
var import_react3 = require("react");
|
|
935
797
|
var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
936
798
|
|
|
937
799
|
// src/client/chat/row-plan.ts
|
|
@@ -1363,8 +1225,8 @@ var subscribeSettings = (listener) => settingsStore.subscribe(listener);
|
|
|
1363
1225
|
var subscribeTranslate = (listener) => chatTranslate.subscribe(listener);
|
|
1364
1226
|
function useSettledInView(ref, armed) {
|
|
1365
1227
|
const noObserver = typeof IntersectionObserver === "undefined";
|
|
1366
|
-
const [inView, setInView] = (0,
|
|
1367
|
-
(0,
|
|
1228
|
+
const [inView, setInView] = (0, import_react3.useState)(noObserver);
|
|
1229
|
+
(0, import_react3.useEffect)(() => {
|
|
1368
1230
|
if (noObserver || !armed) return;
|
|
1369
1231
|
const element = ref.current;
|
|
1370
1232
|
if (element === null) return;
|
|
@@ -1381,7 +1243,7 @@ function useSettledInView(ref, armed) {
|
|
|
1381
1243
|
}, [armed, noObserver, ref]);
|
|
1382
1244
|
return inView;
|
|
1383
1245
|
}
|
|
1384
|
-
var ReasoningRow = (0,
|
|
1246
|
+
var ReasoningRow = (0, import_react3.memo)(function ReasoningRow2({
|
|
1385
1247
|
text,
|
|
1386
1248
|
running,
|
|
1387
1249
|
usePresentation,
|
|
@@ -1389,13 +1251,13 @@ var ReasoningRow = (0, import_react4.memo)(function ReasoningRow2({
|
|
|
1389
1251
|
t
|
|
1390
1252
|
}) {
|
|
1391
1253
|
const { expanded, toggle } = useDisclosure();
|
|
1392
|
-
const labels = (0,
|
|
1254
|
+
const labels = (0, import_react3.useMemo)(() => markdownLabels(t), [t]);
|
|
1393
1255
|
const summaryText = running ? latestCompletedParagraphFirstLine(text) : firstLine(text);
|
|
1394
|
-
const summary = (0,
|
|
1256
|
+
const summary = (0, import_react3.useMemo)(() => summaryText.replaceAll("**", ""), [summaryText]);
|
|
1395
1257
|
const preview = usePresentation(
|
|
1396
1258
|
(policy2) => !expanded && summary !== "" && (running || policy2.settledReasoningPreview)
|
|
1397
1259
|
);
|
|
1398
|
-
const collapsedContent = (0,
|
|
1260
|
+
const collapsedContent = (0, import_react3.useMemo)(
|
|
1399
1261
|
() => React.createElement(
|
|
1400
1262
|
React.Fragment,
|
|
1401
1263
|
null,
|
|
@@ -1408,7 +1270,7 @@ var ReasoningRow = (0, import_react4.memo)(function ReasoningRow2({
|
|
|
1408
1270
|
),
|
|
1409
1271
|
[running, summary]
|
|
1410
1272
|
);
|
|
1411
|
-
const content = (0,
|
|
1273
|
+
const content = (0, import_react3.useMemo)(
|
|
1412
1274
|
() => expanded ? React.createElement(
|
|
1413
1275
|
"div",
|
|
1414
1276
|
{ className: "dsh-ct-think-body" },
|
|
@@ -1517,32 +1379,32 @@ function ProseBlock({
|
|
|
1517
1379
|
})
|
|
1518
1380
|
);
|
|
1519
1381
|
}
|
|
1520
|
-
var AssistantStepView = (0,
|
|
1382
|
+
var AssistantStepView = (0, import_react3.memo)(function AssistantStepView2(props) {
|
|
1521
1383
|
ensureAssistantStyles();
|
|
1522
1384
|
const { node, groupPart, useDisclosure, useTurnData, turnProcess, openFile, renderMessageImages, fileMentions, usePresentation, t } = props;
|
|
1523
1385
|
const data = node.data;
|
|
1524
1386
|
const streaming = data.status === "running";
|
|
1525
1387
|
const interrupted = data.status === "interrupted";
|
|
1526
|
-
const enabled = (0,
|
|
1527
|
-
const aiConfigured = (0,
|
|
1528
|
-
(0,
|
|
1388
|
+
const enabled = (0, import_react3.useSyncExternalStore)(subscribeSettings, () => settingsStore.getState().enabled);
|
|
1389
|
+
const aiConfigured = (0, import_react3.useSyncExternalStore)(subscribeSettings, () => settingsStore.getState().aiConfigured);
|
|
1390
|
+
(0, import_react3.useSyncExternalStore)(subscribeTranslate, () => chatTranslate.getVersion());
|
|
1529
1391
|
const canTranslate = enabled && aiConfigured;
|
|
1530
1392
|
const rowKey = `a${node.anchorSeq ?? `t${data.turn}:s${data.step}`}`;
|
|
1531
|
-
const labels = (0,
|
|
1532
|
-
const pathImages = (0,
|
|
1393
|
+
const labels = (0, import_react3.useMemo)(() => markdownLabels(t), [t]);
|
|
1394
|
+
const pathImages = (0, import_react3.useMemo)(
|
|
1533
1395
|
() => ({ resolve: (value) => localPathMediaUrl(document.baseURI, value) }),
|
|
1534
1396
|
[]
|
|
1535
1397
|
);
|
|
1536
1398
|
const turn = node.location?.kind === "turn" || node.location?.kind === "step" ? node.location.turn : void 0;
|
|
1537
1399
|
const tail = useTurnData("turn-tail");
|
|
1538
|
-
const owner = (0,
|
|
1400
|
+
const owner = (0, import_react3.useMemo)(() => {
|
|
1539
1401
|
if (turn?.status !== "closed" || data.finalNode === void 0) return void 0;
|
|
1540
1402
|
if (tail?.closing?.finalNode?.seq !== data.finalNode.seq) return void 0;
|
|
1541
1403
|
return { turn, seq: data.finalNode.seq, openFile };
|
|
1542
1404
|
}, [data.finalNode, openFile, tail, turn]);
|
|
1543
|
-
const mentions = (0,
|
|
1544
|
-
const blocks = (0,
|
|
1545
|
-
const plan = (0,
|
|
1405
|
+
const mentions = (0, import_react3.useMemo)(() => owner === void 0 ? void 0 : fileMentions(owner), [fileMentions, owner]);
|
|
1406
|
+
const blocks = (0, import_react3.useMemo)(() => data.blocks ?? [], [data.blocks]);
|
|
1407
|
+
const plan = (0, import_react3.useMemo)(
|
|
1546
1408
|
() => planAssistantRow({
|
|
1547
1409
|
blocks,
|
|
1548
1410
|
streaming,
|
|
@@ -1553,16 +1415,16 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
|
|
|
1553
1415
|
}),
|
|
1554
1416
|
[blocks, streaming, interrupted, groupPart, canTranslate]
|
|
1555
1417
|
);
|
|
1556
|
-
const rootRef = (0,
|
|
1418
|
+
const rootRef = (0, import_react3.useRef)(null);
|
|
1557
1419
|
const inView = useSettledInView(rootRef, !streaming && canTranslate && plan.entries !== null && plan.texts.length > 0);
|
|
1558
|
-
(0,
|
|
1420
|
+
(0, import_react3.useEffect)(() => {
|
|
1559
1421
|
if (streaming || !canTranslate || !inView || plan.entries === null || plan.texts.length === 0) return;
|
|
1560
1422
|
chatTranslate.ensure(rowKey, plan.texts);
|
|
1561
1423
|
}, [streaming, canTranslate, inView, rowKey, plan]);
|
|
1562
|
-
const [originalKeys, setOriginalKeys] = (0,
|
|
1424
|
+
const [originalKeys, setOriginalKeys] = (0, import_react3.useState)(() => /* @__PURE__ */ new Set());
|
|
1563
1425
|
const registered = chatTranslate.getState(rowKey);
|
|
1564
1426
|
const rowState = registered !== void 0 && sameTexts(registered.texts, plan.texts) ? registered : void 0;
|
|
1565
|
-
const displayed = (0,
|
|
1427
|
+
const displayed = (0, import_react3.useMemo)(
|
|
1566
1428
|
() => planAssistantRow({
|
|
1567
1429
|
blocks,
|
|
1568
1430
|
streaming,
|
|
@@ -1576,10 +1438,10 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
|
|
|
1576
1438
|
[blocks, streaming, interrupted, groupPart, canTranslate, rowState, originalKeys]
|
|
1577
1439
|
);
|
|
1578
1440
|
const reasoningHidden = turnProcess !== void 0 && turnProcess.foldable && turnProcess.spec.answerStep === data.step && turnProcess.spec.inlineReasoning && !turnProcess.open;
|
|
1579
|
-
const revealProcess = (0,
|
|
1441
|
+
const revealProcess = (0, import_react3.useCallback)(() => {
|
|
1580
1442
|
turnProcess?.setOpen(true);
|
|
1581
1443
|
}, [turnProcess]);
|
|
1582
|
-
const toggleBlock = (0,
|
|
1444
|
+
const toggleBlock = (0, import_react3.useCallback)((key) => {
|
|
1583
1445
|
setOriginalKeys((current) => {
|
|
1584
1446
|
const next = new Set(current);
|
|
1585
1447
|
if (next.has(key)) next.delete(key);
|
|
@@ -1587,7 +1449,7 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
|
|
|
1587
1449
|
return next;
|
|
1588
1450
|
});
|
|
1589
1451
|
}, []);
|
|
1590
|
-
const retryRow = (0,
|
|
1452
|
+
const retryRow = (0, import_react3.useCallback)(() => {
|
|
1591
1453
|
chatTranslate.ensure(rowKey, plan.texts, true);
|
|
1592
1454
|
}, [rowKey, plan]);
|
|
1593
1455
|
if (displayed.entries === null) return null;
|
|
@@ -1773,7 +1635,7 @@ function apply(ctx) {
|
|
|
1773
1635
|
chatPresentation.attach(form);
|
|
1774
1636
|
return () => chatPresentation.detach();
|
|
1775
1637
|
}, "dsh-chat-translate: chat presentation source");
|
|
1776
|
-
ctx.effect(() => setupSettingsUi(ctx), "dsh-chat-translate:
|
|
1638
|
+
ctx.effect(() => setupSettingsUi(ctx), "dsh-chat-translate: plugins page config");
|
|
1777
1639
|
ctx.effect(() => setupComposerToggle(ctx), "dsh-chat-translate: composer dock toggle");
|
|
1778
1640
|
bindRowCopy(ctx.locale);
|
|
1779
1641
|
}
|