@lynn123411/dsh-chat-translate 1.2.5 → 1.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 CHANGED
@@ -12,7 +12,7 @@ DeepSeek Harness Web 界面的工具调用与思考摘要智能翻译插件。
12
12
  - **当前会话作用域**:仅翻译当前查看的会话,切换会话自动跟随新内容;视口懒加载(150px 缓冲)与文档顺序排队,译文按阅读顺序出现。
13
13
  - **智能调度与熔断保护**:1–100 动态并发限流队列、AI 30s / Bing 2s 超时、连续失败熔断自愈、在途请求合并去重、7 天 LRU 磁盘持久化缓存(`~/.dsh/dsh-chat-translate/cache.json`)。
14
14
  - **DSH 原生配置接入**:配置走 DSH `settings` 服务(用户层写入 `~/.dsh/settings.yaml` 的 `dsh-chat-translate` 段)、密钥走 DSH `credentials` 服务、设置面板经 `settingsScope` 与 `credentials` Remote API 读写——无任何自研配置文件。
15
- - **设置面板集成**:在「设置 - 聊天翻译」中提供总开关、AI/Bing 通道开关、Base URL 与模型配置、通道测试与并发数调节。
15
+ - **设置面板集成**:设置面板已迁入与 `dsh-smooth-stream`、`dsh-oil-sticky-prompt` 共用的「设置 - 阅读体验」共享页(本插件贡献其中一张卡片),页内提供总开关、AI/Bing 通道开关、Base URL 与模型配置、通道测试与并发数调节。
16
16
 
17
17
  ## 安装
18
18
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 本文档为 [@lynn123411/dsh-chat-translate](../README.md) 的配置说明(自 README 的「## 配置」段迁出,内容保持一致)。
4
4
 
5
- 1. **API Key**:打开「设置 - 聊天翻译」,在 AI 翻译卡片中直接填写 API Key 并保存(经 DSH 凭据服务写入 `~/.dsh/.credentials.yaml` 的 `TRANSLATE_API_KEY`,权限 0600,保存后立即生效;留空保存 = 清除该键)。也可以手动编辑该文件:
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 启动会失败。未配置时插件自动由 Bing 通道兜底,不会报错。
13
13
 
14
- 2. **Base URL 与模型等设置**:在「设置 - 聊天翻译」中填写 Base URL(如 `https://api.openai.com/v1` 或 `https://api.deepseek.com/v1`)与模型名(如 `gpt-4o-mini`、`deepseek-chat`),点击「测试 AI 通道」验证。所有设置(开关、并发数、超时等)由 DSH 持久化到 `~/.dsh/settings.yaml` 的 `dsh-chat-translate` 段,可直接编辑该文件(DSH 会热同步):
14
+ 2. **Base URL 与模型等设置**:在「设置 - 阅读体验」中填写 Base URL(如 `https://api.openai.com/v1` 或 `https://api.deepseek.com/v1`)与模型名(如 `gpt-4o-mini`、`deepseek-chat`),点击「测试 AI 通道」验证。所有设置(开关、并发数、超时等)由 DSH 持久化到 `~/.dsh/settings.yaml` 的 `dsh-chat-translate` 段,可直接编辑该文件(DSH 会热同步):
15
15
 
16
16
  ```yaml
17
17
  dsh-chat-translate:
package/lib/client.js CHANGED
@@ -129,6 +129,32 @@ var ClientCache = class {
129
129
  };
130
130
  var clientCache = new ClientCache();
131
131
 
132
+ // src/describe-error.ts
133
+ function describeError(err) {
134
+ if (err === null || err === void 0) return String(err);
135
+ const parts = [];
136
+ const push = (s) => {
137
+ const t = String(s || "").trim();
138
+ if (t && !parts.includes(t)) parts.push(t);
139
+ };
140
+ const head = err.message || String(err);
141
+ push(head);
142
+ const headCode = err.code;
143
+ if (headCode && !head.includes(headCode)) push("[" + headCode + "]");
144
+ let cause = err.cause;
145
+ for (let depth = 0; cause && depth < 4; depth++) {
146
+ const label = cause.message || String(cause);
147
+ push(cause.code && !label.includes(cause.code) ? label + " [" + cause.code + "]" : label);
148
+ if (Array.isArray(cause.errors)) {
149
+ for (const sub of cause.errors.slice(0, 3)) {
150
+ if (sub) push(sub.code || sub.message || String(sub));
151
+ }
152
+ }
153
+ cause = cause.cause;
154
+ }
155
+ return parts.join(" \u2190 ");
156
+ }
157
+
132
158
  // src/client/translate/api.ts
133
159
  async function requestTranslateBatch(texts, options = {}) {
134
160
  if (!Array.isArray(texts) || texts.length === 0) return [];
@@ -183,7 +209,7 @@ async function testServerChannel(channel) {
183
209
  });
184
210
  return await res.json();
185
211
  } catch (err) {
186
- return { ok: false, latencyMs: 0, error: err?.message || String(err) };
212
+ return { ok: false, latencyMs: 0, error: describeError(err) };
187
213
  }
188
214
  }
189
215
 
@@ -712,7 +738,7 @@ var ChatTranslateObserver = class {
712
738
  var chatTranslateObserver = new ChatTranslateObserver();
713
739
 
714
740
  // src/client/settings/ui.tsx
715
- var import_react = require("react");
741
+ var import_react2 = require("react");
716
742
 
717
743
  // src/client/settings/store.ts
718
744
  var SETTINGS_NAMESPACE = "dsh-chat-translate";
@@ -879,7 +905,7 @@ var SettingsStore = class {
879
905
  await this.refreshKeyStatus();
880
906
  return { ok: true };
881
907
  } catch (err) {
882
- return { ok: false, error: err?.message || String(err) };
908
+ return { ok: false, error: describeError(err) };
883
909
  }
884
910
  }
885
911
  dispose() {
@@ -1182,6 +1208,152 @@ var SETTINGS_CSS = String.raw`
1182
1208
  }
1183
1209
  `;
1184
1210
 
1211
+ // src/client/reading-settings-page.tsx
1212
+ var import_react = require("react");
1213
+ var READING_PAGE_ID = "reading";
1214
+ var READING_PAGE_ORDER = 110;
1215
+ var READING_ITEM_SLOT = "reading.settings.item";
1216
+ function readLabel(label) {
1217
+ if (typeof label === "function") return label();
1218
+ return typeof label === "string" ? label : "";
1219
+ }
1220
+ var TABLIST_STYLE = {
1221
+ display: "flex",
1222
+ alignItems: "flex-end",
1223
+ gap: "22px",
1224
+ marginTop: "2px",
1225
+ marginBottom: "16px"
1226
+ };
1227
+ var TAB_STYLE = {
1228
+ appearance: "none",
1229
+ background: "transparent",
1230
+ // No border on ANY tab: the marker below is the active tab's own element, so
1231
+ // an inactive tab has nothing that could render a line.
1232
+ border: "none",
1233
+ position: "relative",
1234
+ padding: "7px 1px 11px",
1235
+ cursor: "pointer",
1236
+ font: "inherit",
1237
+ fontSize: "13px",
1238
+ lineHeight: "20px",
1239
+ color: "var(--dsw-alias-label-tertiary, inherit)"
1240
+ };
1241
+ var TAB_ACTIVE_STYLE = {
1242
+ ...TAB_STYLE,
1243
+ color: "var(--dsw-alias-label-primary, inherit)"
1244
+ };
1245
+ var TAB_MARKER_STYLE = {
1246
+ position: "absolute",
1247
+ left: 0,
1248
+ right: 0,
1249
+ bottom: 0,
1250
+ height: "2px",
1251
+ borderRadius: "2px 2px 0 0",
1252
+ background: "var(--dsw-alias-label-primary, currentColor)"
1253
+ };
1254
+ var PANEL_STYLE = { listStyle: "none", margin: 0, padding: 0 };
1255
+ var PANEL_HIDDEN_STYLE = { ...PANEL_STYLE, display: "none" };
1256
+ function createReadingTabs(ctx) {
1257
+ const locale = ctx.get("locale");
1258
+ let version = -1;
1259
+ let revision = -1;
1260
+ let tabs = [];
1261
+ return {
1262
+ getSnapshot: () => {
1263
+ const nextVersion = ctx.slots.getVersion(READING_ITEM_SLOT);
1264
+ const nextRevision = locale === void 0 ? 0 : locale.getSnapshot().revision;
1265
+ if (nextVersion === version && nextRevision === revision) return tabs;
1266
+ version = nextVersion;
1267
+ revision = nextRevision;
1268
+ tabs = ctx.slots.entries(READING_ITEM_SLOT).map((entry) => ({
1269
+ id: entry.options.id ?? "",
1270
+ order: entry.options.order ?? 0,
1271
+ label: readLabel(entry.options.label)
1272
+ })).sort((left, right) => left.order - right.order);
1273
+ return tabs;
1274
+ },
1275
+ subscribe: (listener) => {
1276
+ const offSlots = ctx.slots.subscribe(READING_ITEM_SLOT, listener);
1277
+ const offLocale = locale?.subscribe(listener);
1278
+ return () => {
1279
+ offSlots();
1280
+ offLocale?.();
1281
+ };
1282
+ }
1283
+ };
1284
+ }
1285
+ function ReadingSettingsSection({ renderSlot, readingTabs }) {
1286
+ const tabs = (0, import_react.useSyncExternalStore)(
1287
+ readingTabs.subscribe,
1288
+ readingTabs.getSnapshot,
1289
+ readingTabs.getSnapshot
1290
+ );
1291
+ const [requested, setRequested] = (0, import_react.useState)(null);
1292
+ const selected = requested !== null && tabs.some((tab) => tab.id === requested) ? requested : tabs[0]?.id ?? null;
1293
+ if (selected === null) return null;
1294
+ return (0, import_react.createElement)(
1295
+ "div",
1296
+ null,
1297
+ (0, import_react.createElement)(
1298
+ "div",
1299
+ { role: "tablist", style: TABLIST_STYLE },
1300
+ tabs.map((tab) => (0, import_react.createElement)(
1301
+ "button",
1302
+ {
1303
+ key: tab.id,
1304
+ type: "button",
1305
+ role: "tab",
1306
+ "aria-selected": tab.id === selected,
1307
+ style: tab.id === selected ? TAB_ACTIVE_STYLE : TAB_STYLE,
1308
+ onClick: () => {
1309
+ setRequested(tab.id);
1310
+ }
1311
+ },
1312
+ tab.label,
1313
+ tab.id === selected ? (0, import_react.createElement)("span", { style: TAB_MARKER_STYLE, "aria-hidden": true }) : null
1314
+ ))
1315
+ ),
1316
+ tabs.map((tab) => (0, import_react.createElement)(
1317
+ "ul",
1318
+ {
1319
+ key: tab.id,
1320
+ role: "tabpanel",
1321
+ hidden: tab.id !== selected,
1322
+ style: tab.id === selected ? PANEL_STYLE : PANEL_HIDDEN_STYLE
1323
+ },
1324
+ renderSlot(READING_ITEM_SLOT, {}, { only: tab.id })
1325
+ ))
1326
+ );
1327
+ }
1328
+ function readingPageClaimed(ctx) {
1329
+ return ctx.slots.entries("settings.section").some((entry) => entry.options.id === READING_PAGE_ID);
1330
+ }
1331
+ function claimReadingSettingsPage(ctx, label, locale) {
1332
+ if (readingPageClaimed(ctx)) return () => {
1333
+ };
1334
+ const readingTabs = createReadingTabs(ctx);
1335
+ return ctx.slots.register({
1336
+ name: "settings.section",
1337
+ id: READING_PAGE_ID,
1338
+ order: READING_PAGE_ORDER,
1339
+ label,
1340
+ locale,
1341
+ inject: () => ({ readingTabs }),
1342
+ children: { "reading.settings.item": { kind: "list", scope: "root" } }
1343
+ }, ReadingSettingsSection);
1344
+ }
1345
+
1346
+ // src/client/locales.ts
1347
+ var NS = "settings.chatTranslate";
1348
+ var en = {
1349
+ pageNav: "Reading",
1350
+ title: "Chat translate"
1351
+ };
1352
+ var zh = {
1353
+ pageNav: "\u9605\u8BFB\u4F53\u9A8C",
1354
+ title: "\u804A\u5929\u7FFB\u8BD1"
1355
+ };
1356
+
1185
1357
  // src/client/settings/ui.tsx
1186
1358
  var import_jsx_runtime = require("react/jsx-runtime");
1187
1359
  var stylesInjected = false;
@@ -1206,14 +1378,14 @@ function Switch(props) {
1206
1378
  }
1207
1379
  );
1208
1380
  }
1209
- function TidySettingsPanel() {
1381
+ function TidySettingsPanel(_props) {
1210
1382
  ensureSettingsStyles();
1211
- const [state, setState] = (0, import_react.useState)(() => settingsStore.getState());
1212
- const [testing, setTesting] = (0, import_react.useState)(null);
1213
- const [apiKeyInput, setApiKeyInput] = (0, import_react.useState)("");
1214
- const [savingKey, setSavingKey] = (0, import_react.useState)(false);
1215
- const [keyMsg, setKeyMsg] = (0, import_react.useState)(null);
1216
- (0, import_react.useEffect)(() => {
1383
+ const [state, setState] = (0, import_react2.useState)(() => settingsStore.getState());
1384
+ const [testing, setTesting] = (0, import_react2.useState)(null);
1385
+ const [apiKeyInput, setApiKeyInput] = (0, import_react2.useState)("");
1386
+ const [savingKey, setSavingKey] = (0, import_react2.useState)(false);
1387
+ const [keyMsg, setKeyMsg] = (0, import_react2.useState)(null);
1388
+ (0, import_react2.useEffect)(() => {
1217
1389
  return settingsStore.subscribe(() => {
1218
1390
  setState(settingsStore.getState());
1219
1391
  });
@@ -1432,21 +1604,37 @@ function setupSettingsUi(ctx) {
1432
1604
  } catch (err) {
1433
1605
  console.warn("[dsh-chat-translate] Failed to bind settings scope:", err);
1434
1606
  }
1607
+ const locale = typeof ctx?.get === "function" ? ctx.get("locale") : null;
1608
+ if (locale && typeof locale.register === "function" && typeof ctx?.effect === "function") {
1609
+ ctx.effect(
1610
+ () => locale.register(NS, { zh, en }),
1611
+ "dsh-chat-translate: locale dictionaries"
1612
+ );
1613
+ }
1614
+ const t = locale && typeof locale.bind === "function" ? locale.bind(NS) : (key) => zh[key] ?? key;
1435
1615
  try {
1436
1616
  const slots = ctx?.slots || (ctx?.get ? ctx.get("slots") : null);
1437
1617
  if (!slots || typeof slots.inject !== "function") return;
1438
- slots.inject("settings.section", () => {
1439
- return slots.register(
1618
+ slots.inject(
1619
+ "settings.section",
1620
+ () => claimReadingSettingsPage(ctx, () => t("pageNav"), NS)
1621
+ );
1622
+ slots.inject(
1623
+ READING_ITEM_SLOT,
1624
+ () => slots.register(
1440
1625
  {
1441
- name: "settings.section",
1442
- id: "dsh-chat-translate",
1443
- // 约定:自有插件设置项 order 从 110 起步进 10(原生最大 100=桌面设置),保证排在所有原生项之下
1444
- order: 110,
1445
- label: () => "\u804A\u5929\u7FFB\u8BD1"
1626
+ name: READING_ITEM_SLOT,
1627
+ // id = 本插件的 Host 设置命名空间(settings.section 时代的 id 沿用)
1628
+ id: SETTINGS_NAMESPACE,
1629
+ // 卡片在共享页里的顺序:丝滑流式 10、吸顶提示 20、聊天翻译 30
1630
+ order: 30,
1631
+ // 共享页按此标签渲染 tab
1632
+ label: () => t("title"),
1633
+ locale: NS
1446
1634
  },
1447
1635
  TidySettingsPanel
1448
- );
1449
- });
1636
+ )
1637
+ );
1450
1638
  } catch (err) {
1451
1639
  console.warn("[dsh-chat-translate] Failed to inject settings section:", err);
1452
1640
  }