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 +10 -0
- package/docs/VALIDATION.md +8 -0
- package/docs/validation-language-2026-10-03.md +16 -0
- package/lib/client.js +36 -12
- package/lib/tools.js +1 -1
- package/package.json +1 -1
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、发件账号、正文、附件上传与预览下载,再明确确认发送。
|
package/docs/VALIDATION.md
CHANGED
|
@@ -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
|
|
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
|
}
|
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' },
|
|
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 }; },
|