dsh-email 0.15.1 → 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 +5 -0
- package/docs/VALIDATION.md +2 -0
- package/docs/validation-language-2026-10-03.md +16 -0
- package/lib/client.js +36 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
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
|
+
|
|
7
12
|
## 0.15.1 (2026-10-03)
|
|
8
13
|
|
|
9
14
|
- 修复 email_draft 使用宿主不支持的 JSON Schema maxItems,导致邮件工具注册失败的问题。附件最多 10 个的限制仍在执行前校验,并在工具说明中显示。
|
package/docs/VALIDATION.md
CHANGED
|
@@ -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
|
|
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" },
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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:
|
|
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 =
|
|
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: () =>
|
|
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
|
}
|