dsh-email 0.15.0 → 0.15.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@
4
4
 
5
5
  [历史英文记录](docs/CHANGELOG.en.md)
6
6
 
7
+ ## 0.15.2 (2026-10-03)
8
+
9
+ - 修复语言切换后设置菜单滞后一轮;英文草稿、账号、发送失败引导不再回退中文,保留结果不明时禁止盲目重发的提示。
10
+ - 验收范围见 [中英文界面检查](docs/validation-language-2026-10-03.md)。
11
+
12
+ ## 0.15.1 (2026-10-03)
13
+
14
+ - 修复 email_draft 使用宿主不支持的 JSON Schema maxItems,导致邮件工具注册失败的问题。附件最多 10 个的限制仍在执行前校验,并在工具说明中显示。
15
+ - 增加所有邮件工具的 schema 兼容回归和 11 个附件提前拒绝回归;实际使用官方 Harness 0.2.1-alpha.1 检查注册、输出和 PTC 工具声明。
16
+
7
17
  ## 0.15.0 (2026-10-03)
8
18
 
9
19
  - 新增 email_draft 和本地草稿工作台,支持编辑 To/Cc、发件账号、正文、附件上传与预览下载,再明确确认发送。
@@ -1,5 +1,13 @@
1
1
  # dsh-email 验证记录
2
2
 
3
+ [0.15.2 中英文界面验收](validation-language-2026-10-03.md)。
4
+
5
+ ## 0.15.1(2026-10-03)
6
+
7
+ Windows / Node 24.16.0 下 330 项测试全部通过,无跳过。新回归在修复前定位到 email_draft 的 maxItems 参数声明,修复后通过;11 个附件仍在读取文件、保存草稿或发送之前被拒绝。
8
+
9
+ 官方 Harness 0.2.1-alpha.1(公开 npm 安装)和 0.2.0-rc.2(官方源码构建)均验证了 18 个独立插件同载:109 个工具、35 个技能、19 项输出检查,TypeScript/Python PTC 声明生成通过。邮件的 11 个工具全部注册成功。本次未调用真实邮箱或发送邮件,也不表示原生桌面新工作台已逐项操作验收。
10
+
3
11
  本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
4
12
 
5
13
  ## 0.15.0 · 2026-10-03
@@ -0,0 +1,16 @@
1
+ # 0.15.2:中英文界面验收
2
+
3
+ 2026-10-03,Windows / Node 24.16.0。采用 E 盘独立 DSH_HOME、合成资料与无密钥 profile;用户资料、账号、密钥、桌面窗口与 VPN 保持原状。
4
+
5
+ - 自动测试:331 项全部通过。
6
+ - 可见 Web 操作:英文界面新建并保存中文草稿,未配置账号时的预览引导为英文;中文界面读取同一份草稿,标题和正文保留。语言菜单回调顺序及发送结果不明的防重发提示有回归覆盖。
7
+ - 宿主:官方 DSH 0.2.1-alpha.1 Web;官方 RC2 与 alpha SDK 同载 18 组件,110 工具、35 插件技能、19 输出样例注册与 schema 检查通过。无账号的健康结果不代表真实连接。
8
+ - 边界:没有发送真实邮件。收件人配置与发件账号配置分别给出恢复引导。
9
+
10
+ 中英文共用同一份资料;界面翻译不会改写标题、正文、路径、引用或项目文件。系统文件选择控件和文件对话框跟随 Windows / 浏览器语言。未知技术诊断与原始引擎日志保留原文;英文界面提供操作恢复引导。
11
+
12
+ 此记录不代表原生桌面窗口内的新功能已逐项操作通过。发布包完整性、GitHub CI 与公开 npm 全新安装在发布时另行核对。
13
+
14
+ ## English scope
15
+
16
+ The Chinese and English UI share the same data. UI translation preserves original content, paths and project files. Visible interactions were performed in an isolated official alpha Web profile; both RC2 and alpha SDK registration/schema checks passed. Native file controls use the OS/browser language, and original diagnostic logs are retained. Browser results do not certify native desktop interaction or real-service delivery.
package/lib/client.js CHANGED
@@ -12,6 +12,23 @@ const h = React.createElement;
12
12
 
13
13
  // Harness supplies document.baseURI for reverse-proxy mounts and in-page navigation.
14
14
  const ROUTE = "_dsh/dsh-email/settings";
15
+
16
+ function emailError(error) {
17
+ const message = typeof error === 'string' ? error : error?.message || String(error || '');
18
+ if (UI_LANG !== 'en') return message;
19
+ if (error?.status === 401) return 'Your DSH session expired. Reopen DSH and try again.';
20
+ if (!/[\u3400-\u9fff]/.test(message)) return message;
21
+ if (/结果.*未|不要.*重发|不要.*发送|本地保存失败|回执未能保存/.test(message)) return 'The send outcome is uncertain or its receipt could not be saved. Check the mailbox before sending again to avoid duplicate delivery.';
22
+ if (/已提交|不能再次发送/.test(message)) return 'This draft was submitted or its outcome is uncertain. Check the mailbox before copying it to a new draft.';
23
+ if (/预览已过期|draft-preview-expired/.test(message + ' ' + (error?.code || ''))) return 'The send review expired or was already used. Review the current draft again.';
24
+ if (/改变|已更新|冲突|重新加载/.test(message)) return 'The draft, account or rules changed. Reload the latest version and review your input before retrying.';
25
+ if (/请先填写并保存邮件主题/.test(message)) return 'Fill in and save the email subject first.';
26
+ if (/附件/.test(message)) return 'Check the attachments. Save the draft first; use up to 10 readable files and 20 MiB total. Remove invalid files and add them again.';
27
+ if (/账号|配置不完整|尚未登录|OAuth|设备码/.test(message)) return 'Check the selected email account. Complete its credentials or OAuth sign-in in Account settings, then test the connection.';
28
+ if (/收件人|邮箱地址/.test(message)) return 'Check the recipient addresses and trusted-recipient rules before reviewing the draft again.';
29
+ return 'The email operation failed. Check the account settings and DSH logs, then retry. For a send operation, check the mailbox before retrying.';
30
+ }
31
+
15
32
  /** 「编辑即保存」的防抖窗口:连续打字只落一次盘。 */
16
33
  const SAVE_DEBOUNCE_MS = 800;
17
34
 
@@ -32,6 +49,7 @@ async function api(action, payload) {
32
49
  // anything else is not, and the message alone cannot tell them apart.
33
50
  error.code = body && body.error ? body.error.code : undefined;
34
51
  error.status = res.status;
52
+ error.message = emailError(error);
35
53
  throw error;
36
54
  }
37
55
  return body.value;
@@ -41,8 +59,8 @@ async function api(action, payload) {
41
59
  * 登录失败的文案:平铺的 message 优先,退回 error.message,再退回调用方给的兜底。
42
60
  */
43
61
  function oauthErrorMessage(body, fallback) {
44
- if (body && typeof body.message === "string" && body.message !== "") return body.message;
45
- if (body && body.error && typeof body.error.message === "string" && body.error.message !== "") return body.error.message;
62
+ if (body && typeof body.message === "string" && body.message !== "") return emailError(body.message);
63
+ if (body && body.error && typeof body.error.message === "string" && body.error.message !== "") return emailError(body.error);
46
64
  return fallback;
47
65
  }
48
66
 
@@ -620,6 +638,7 @@ function localeToLang(active) {
620
638
  * 弹窗,所以这里是单槽而不是列表;弹窗销毁时它会把自己的钩子摘掉。
621
639
  */
622
640
  var relocalize = null;
641
+ var activeLocaleService = null;
623
642
 
624
643
  /**
625
644
  * 取 locale 服务。两条路都试,因为「服务在不在」和「ctx 长什么样」是两件事:
@@ -650,6 +669,7 @@ function localeServiceOf(ctx) {
650
669
  */
651
670
  function subscribeLocale(ctx) {
652
671
  const service = localeServiceOf(ctx);
672
+ activeLocaleService = service;
653
673
  if (service === null) return () => {};
654
674
  const apply = () => {
655
675
  try {
@@ -1257,7 +1277,7 @@ function OauthLoginPanel(props) {
1257
1277
 
1258
1278
  if (state === "error") {
1259
1279
  return h("div", { className: "dshe-oauth" }, [
1260
- h("div", { className: "dshe-alert error" }, String(session.message || t("oauth.loginFailed"))),
1280
+ h("div", { className: "dshe-alert error" }, emailError(session.message || t("oauth.loginFailed"))),
1261
1281
  clientIdField(),
1262
1282
  h("div", { className: "dshe-oauth-actions" }, [
1263
1283
  h("button", {
@@ -1379,7 +1399,7 @@ function AccountCardsEditor(props) {
1379
1399
  try {
1380
1400
  built = names.map((name) => draftToInput(draftsNow[name]));
1381
1401
  } catch (e) {
1382
- incomplete(e && e.message ? e.message : String(e));
1402
+ incomplete(emailError(e));
1383
1403
  return null;
1384
1404
  }
1385
1405
  if (built.some((card) => String(card.name || "").trim() === "")) {
@@ -1486,7 +1506,7 @@ function AccountCardsEditor(props) {
1486
1506
  } catch (e) {
1487
1507
  if (seq !== oauthCurrent(name)) return;
1488
1508
  setRowStatus((cur) => { const next = Object.assign({}, cur); delete next[name]; return next; });
1489
- setOauthOf(name, { state: "error", message: e && e.message ? e.message : String(e) });
1509
+ setOauthOf(name, { state: "error", message: emailError(e) });
1490
1510
  }
1491
1511
  };
1492
1512
 
@@ -1498,7 +1518,7 @@ function AccountCardsEditor(props) {
1498
1518
  body = await apiOauth("oauthPoll", { account: name });
1499
1519
  } catch (e) {
1500
1520
  if (seq !== oauthCurrent(name)) return;
1501
- setOauthOf(name, { state: "error", message: e && e.message ? e.message : String(e) });
1521
+ setOauthOf(name, { state: "error", message: emailError(e) });
1502
1522
  return;
1503
1523
  }
1504
1524
  if (seq !== oauthCurrent(name)) return;
@@ -1635,7 +1655,7 @@ function AccountCardsEditor(props) {
1635
1655
  }));
1636
1656
  } catch (e) {
1637
1657
  setRowStatus((cur) => Object.assign({}, cur, {
1638
- [draftName]: { kind: "error", text: t("card.testFailed", { message: e && e.message ? e.message : String(e) }) },
1658
+ [draftName]: { kind: "error", text: t("card.testFailed", { message: emailError(e) }) },
1639
1659
  }));
1640
1660
  } finally {
1641
1661
  setBusy("");
@@ -2148,7 +2168,7 @@ async function draftApi(action, payload, upload) {
2148
2168
  init.body = upload;
2149
2169
  } else init.body = JSON.stringify({ ...payload, action });
2150
2170
  const response = await fetch(upload ? DRAFT_ROUTE + "/upload" : DRAFT_ROUTE, init), result = await response.json();
2151
- if (!response.ok || !result.ok) { const error = new Error(result.error && result.error.message || dt("邮件操作失败", "Email action failed")); error.code = result.error && result.error.code; throw error; }
2171
+ if (!response.ok || !result.ok) { const error = new Error(result.error && result.error.message || dt("邮件操作失败", "Email action failed")); error.code = result.error && result.error.code; error.status = response.status; error.message = emailError(error); throw error; }
2152
2172
  return result.value;
2153
2173
  }
2154
2174
  const draftForm = (draft) => ({ account: draft.account || "", to: draft.to || "", cc: draft.cc || "", subject: draft.subject || "", text: draft.text || "", attachments: draft.attachments.map(file => file.id) });
@@ -2347,7 +2367,7 @@ function EmailSettingsSection() {
2347
2367
  setCardsDetail(snap.accountsDetail);
2348
2368
  setPresetsAtLoad(snap.presets);
2349
2369
  } catch (e) {
2350
- setError(e && e.message ? e.message : String(e));
2370
+ setError(emailError(e));
2351
2371
  } finally {
2352
2372
  setBusy(false);
2353
2373
  }
@@ -2425,7 +2445,7 @@ function EmailSettingsSection() {
2425
2445
  if (seq !== seqRef.current) return;
2426
2446
  // 失败时清掉「正在飞」的签名,否则同一个值再也不会被重排(点重试即可再存)。
2427
2447
  pendingSignatureRef.current = null;
2428
- setSaveState({ kind: "error", text: e && e.message ? e.message : String(e) });
2448
+ setSaveState({ kind: "error", text: emailError(e) });
2429
2449
  }
2430
2450
  };
2431
2451
 
@@ -2515,7 +2535,7 @@ function EmailSettingsSection() {
2515
2535
  return { ok: true };
2516
2536
  } catch (e) {
2517
2537
  if (seq !== cardsSeqRef.current) return { ok: false, reason: "superseded" };
2518
- const message = e && e.message ? e.message : String(e);
2538
+ const message = emailError(e);
2519
2539
  setCardsNotice({ kind: "error", text: t("notice.cardsNotWritten", { message }) });
2520
2540
  setSaveState({ kind: "error", text: message });
2521
2541
  return { ok: false, reason: message };
@@ -2776,7 +2796,11 @@ function apply(ctx) {
2776
2796
  name: "settings.section",
2777
2797
  id: "dsh-email",
2778
2798
  order: 45,
2779
- label: () => t("nav.title"),
2799
+ label: () => {
2800
+ let lang = UI_LANG;
2801
+ try { if (activeLocaleService?.getSnapshot) lang = localeToLang(activeLocaleService.getSnapshot().active); } catch {}
2802
+ return UI[lang]["nav.title"];
2803
+ },
2780
2804
  inject: () => ({}),
2781
2805
  }, EmailSettingsSection));
2782
2806
  }
package/lib/tools.js CHANGED
@@ -11,7 +11,7 @@ export function buildEmailTools(runtime, drafts) {
11
11
  ...(drafts ? [{
12
12
  name: 'email_draft',
13
13
  description: '准备可编辑的本地邮件草稿,不投递邮件。用户在设置 → 邮件 → 草稿中编辑、检查附件预览并明确确认后才发送。可在邮箱尚未配置时准备文字草稿;不用于回复原邮件线程。',
14
- parameters: { type: 'object', properties: { account: { type: 'string', description: '发件账号名,省略时使用默认账号' }, to: { type: 'string', description: 'To 地址,支持显示名和地址组' }, cc: { type: 'string' }, subject: { type: 'string' }, text: { type: 'string', description: '纯文本正文' }, attachments: { type: 'array', items: { type: 'string' }, maxItems: 10, description: '本地附件路径,用户在预览中检查后才提交' } }, required: ['to', 'subject', 'text'], additionalProperties: false },
14
+ parameters: { type: 'object', properties: { account: { type: 'string', description: '发件账号名,省略时使用默认账号' }, to: { type: 'string', description: 'To 地址,支持显示名和地址组' }, cc: { type: 'string' }, subject: { type: 'string' }, text: { type: 'string', description: '纯文本正文' }, attachments: { type: 'array', items: { type: 'string' }, description: '最多 10 个本地附件路径,用户在预览中检查后才提交' } }, required: ['to', 'subject', 'text'], additionalProperties: false },
15
15
  output: { schema: { type: 'object', properties: { id: { type: 'string' }, state: { type: 'string' }, subject: { type: 'string' } }, required: ['id', 'state', 'subject'] }, render: (_args, value) => [{ type: 'text', text: '本地草稿已保存,邮件未发送。请在设置 → 邮件 → 草稿中打开并检查。\n草稿编号:' + value.id + '\n主题:' + value.subject }] },
16
16
  timeoutMs: QUERY_TIMEOUT_MS,
17
17
  async execute(args, exec) { const draft = await drafts.create(args, executionSignal(exec)); return { id: draft.id, state: draft.state, subject: draft.subject }; },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-email",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "在 DSH 中收发、搜索和整理邮件,支持多个邮箱账号。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",