v2.4.0 修 Email Routing 假成功 + 加 API Token 接入与邮箱/域名页交互改进

问题 6(收不到邮件、Dashboard 邮件路由里看不到域名)的根因:
  上一版把「开启 Email Routing」改成了 PUT /zones/{id}/email/routing,
  但实测该接口(以及 PATCH)是**纯回显** —— 返回 success:true 但 enabled 纹丝不动,
  域名在 Cloudflare 侧仍是 unconfigured、连 MX 记录都没有。Console 却照它的返回值
  把域名标成「已配置 Email Workers」,于是「显示配好了却收不到邮件」。
  实测只有 POST /zones/{id}/email/routing/enable 真的开启(线上对 adx9.com 与
  galaxyemail.com 逐一验证)。

后端
- cfapi: 改用 /enable,并在开启后回读 GET 校验 enabled===true,不满足即判失败;
  新增 CFApiError(带 Cloudflare 错误码)与 getEmailRoutingSettings
- cfapi: 错误码 2008(Non-Cloudflare MX records exist)翻译成中文并点名冲突的 MX 记录
- provision: Email Worker 统一命名 wmailhub-email(原先按账号 id 拼名),
  部署走 PUT,同名即覆盖更新
- db: 新增 decryptAccountToken —— 按账号取 token,不再取「最近一条连接」,
  并跳过已过期的 OAuth token;oauth_connections.account_id 真正写入
- resources: 新增 POST /api/accounts/token(API Token 方式添加账号):
  先校验 token 与账户 ID,确认可用才建账号并自动接入

前端
- 账号:「添加账号」改为模态窗口,OAuth / API Token 两个 tab,各自带说明
- 域名: 删除「Email Worker」列;「设置 Email Workers / 重新配置」先弹确认框
  (说明会改 MX 与路由),执行期间按钮 loading 且互相禁用;error 状态改成可读的 Alert + 重试
- 邮箱: 删除「状态」列(已由「启用」开关承担);地址列加复制按钮(带弹跳+变绿动画);
  「添加邮箱」窗口的域名只列已装 Email Workers 的、支持搜索、默认选中最近使用的域名;
  地址框可一键从 namedb 取随机用户名;勾选 catch-all 后地址框禁用
- 收件箱: 域名筛选只显示已装 Email Workers 的域名

测试
- cf-mock 复刻「PUT/PATCH 回 success 但不改 enabled」与 2008,锁住本次修复
- oauth.test 新增 4 例:2008 冲突、回读校验、API Token 接入、Token/账户 ID 校验失败
- ui-smoke 覆盖新增的添加账号/添加邮箱/确认框等交互(28/28 通过)

文档: docs/02 补「三种开启接口只有 /enable 有效」的实测表、2008 处理、
两种接入方式与按账号取 token 的说明
This commit is contained in:
bigcode
2026-09-30 18:20:44 +08:00
parent e7db6762fd
commit ecce90dfab
20 changed files with 1107 additions and 143 deletions
+96
View File
@@ -143,6 +143,102 @@ for (const path of ["/accounts", "/domains", "/mailboxes", "/inboxs"]) {
if (path === "/mailboxes") await page.screenshot({ path: `${SHOT_DIR}/11-batch-selected.png` });
}
// 6b. 添加账号模态窗口:OAuth / API Token 两种授权方式 + 各自说明
await page.goto(`${BASE}/accounts`, { waitUntil: "domcontentloaded" });
await page.waitForSelector(".ant-table", { timeout: 8000 });
await page.click("button:has-text('添加账号')");
await page.waitForSelector(".ant-modal:has-text('添加 Cloudflare 账号')", { timeout: 8000 });
const addTabs = await page.locator(".ant-modal .ant-tabs-tab").allInnerTexts();
check(
"添加账号模态窗口含 OAuth / API Token 两种授权方式",
addTabs.some((t) => t.includes("OAuth")) && addTabs.some((t) => t.includes("API Token")),
addTabs.join(" | "),
);
check(
"OAuth 方式带说明(不需在本系统保存密钥)",
(await page.locator(".ant-modal :text('不需要在本机保存任何密钥')").count()) > 0,
);
await page.screenshot({ path: `${SHOT_DIR}/12a-add-account-oauth.png` });
await page.click(".ant-modal .ant-tabs-tab:has-text('API Token')");
await page.waitForSelector(".ant-modal :text('账户名称')", { timeout: 8000 });
check(
"API Token 方式含 账户名称 / 账户 ID / API Token 三个字段",
(await page.locator(".ant-modal :text('账户 ID')").count()) > 0 &&
(await page.locator(".ant-modal input[type=password]").count()) > 0,
);
check(
"API Token 方式带说明(会先校验再接入)",
(await page.locator(".ant-modal :text('确认可用后才建账号')").count()) > 0,
);
await page.screenshot({ path: `${SHOT_DIR}/12b-add-account-token.png` });
await page.keyboard.press("Escape");
await page.waitForTimeout(400);
// 6c. 添加邮箱模态窗口:域名只列已装 Email Workers 的、随机用户名、catch-all 开关禁用地址
await page.goto(`${BASE}/mailboxes`, { waitUntil: "domcontentloaded" });
await page.waitForSelector(".ant-table", { timeout: 8000 });
check(
"邮箱列表不再有「状态」列(已由「启用」开关承担)",
(await page.locator(".ant-table-thead th:has-text('状态')").count()) === 0,
);
check(
"地址列带复制按钮",
(await page.locator(".ant-table-tbody button.copy-btn").count()) > 0,
);
await page.click("button:has-text('添加邮箱')");
await page.waitForSelector(".ant-modal:has-text('添加邮箱')", { timeout: 8000 });
await page.locator(".ant-modal .ant-select").first().click();
await page.waitForTimeout(600);
const domainOptions = await page.locator(".ant-select-dropdown .ant-select-item-option-content").allInnerTexts();
check("添加邮箱的域名下拉有可选项", domainOptions.length > 0, domainOptions.join(" | "));
await page.keyboard.press("Escape");
await page.waitForTimeout(300);
// 随机用户名:点一下应把名字库返回的名字填进地址框
await page.click(".ant-modal button:has(.anticon-thunderbolt)");
await page.waitForFunction(
() => [...document.querySelectorAll(".ant-modal input")].some((i) => (i as HTMLInputElement).value?.length),
undefined,
{ timeout: 15000 },
);
const addressInput = page.locator(".ant-modal input").nth(1);
const filled = (await addressInput.inputValue()).trim();
check("随机用户名可从名字库取到并填入地址框", filled.length > 0, filled);
// 勾上 catch-all 后地址输入框应被禁用
await page.locator(".ant-modal .ant-switch").first().click();
await page.waitForTimeout(600);
check("勾选 catch-all 后地址输入框被禁用", await addressInput.isDisabled());
await page.screenshot({ path: `${SHOT_DIR}/13-add-mailbox.png` });
await page.keyboard.press("Escape");
await page.waitForTimeout(400);
// 6d. 「设置 Email Workers」点击后先弹确认框,不直接发请求
await page.goto(`${BASE}/domains`, { waitUntil: "domcontentloaded" });
await page.waitForSelector(".ant-table-tbody button", { timeout: 8000 });
check(
"域名列表不再有「Email Worker」列(统一命名,状态列已足够)",
(await page.locator(".ant-table-thead th:has-text('Email Worker')").count()) === 0,
);
const setupBtn = page
.locator(".ant-table-tbody button")
.filter({ hasText: /设置 Email Workers|重新配置/ })
.first();
if ((await setupBtn.count()) > 0) {
await setupBtn.click();
await page.waitForSelector(".ant-modal-confirm, .ant-popconfirm, .ant-popover", { timeout: 8000 });
const confirmText =
(await page.locator(".ant-modal-confirm, .ant-popconfirm, .ant-popover").first().innerText().catch(() => "")) ?? "";
check(
"点「设置 Email Workers」先弹确认框(说明会改 MX 与路由)",
confirmText.includes("Email Routing"),
confirmText.split("\n")[0],
);
await page.waitForTimeout(600); // 等确认框进场动画,截图才有内容
await page.screenshot({ path: `${SHOT_DIR}/14-setup-confirm.png` });
await page.keyboard.press("Escape");
} else {
check("点「设置 Email Workers」先弹确认框(说明会改 MX 与路由)", false, "找不到配置按钮");
}
await browser.close();
const failed = results.filter((r) => !r.ok);