feat: v2.1.0 界面与凭据改进 —— 单一 API/MCP Key、邮件 Markdown 正文、/inboxs 域名筛选、暗色主题
- 单一 Key:API 与 MCP 共用一个 wmh_ Key,凭据迁移至 settings 表(0004 删除 api_credentials) - Markdown:入库时 HTML→Markdown(mailhook 同思路),webhook payload 新增 markdown 字段 - /emails 路由改为 /inboxs 并支持域名筛选;设置按钮移入头像下拉 - 主题切换按钮(头像左侧,亮/暗持久化);设置模态仅 全局 Webhook/账号密码 两 tab 且高度限屏 - 列表操作列按钮加 SVG 图标;mailboxes 去 Webhook 列;最近邮件加收件人列
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 一次性 E2E 验证:authorize -> ingest 发送带 HTML 正文的邮件,
|
||||
* 等 webhook 投递后检查 receiver 收到的 payload 是否包含 markdown 字段。
|
||||
*/
|
||||
|
||||
import { signWorkerRequest, generateWmailKey } from "../shared/src/index.ts";
|
||||
|
||||
const CONSOLE = "http://127.0.0.1:18787";
|
||||
|
||||
/** # 函数
|
||||
* 登录拿会话 cookie
|
||||
*/
|
||||
async function login(): Promise<string> {
|
||||
const resp = await fetch(`${CONSOLE}/api/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: "admin", password: "admin" }),
|
||||
});
|
||||
const setCookie = resp.headers.get("Set-Cookie") ?? "";
|
||||
return /wmh_session=[^;]+/.exec(setCookie)?.[0] ?? "";
|
||||
}
|
||||
|
||||
const cookie = await login();
|
||||
if (!cookie) throw new Error("login failed");
|
||||
|
||||
/** 取 worker secret(签名用) */
|
||||
const secretResp = await fetch(`${CONSOLE}/api/accounts`, { headers: { Cookie: cookie } });
|
||||
const accounts = (await secretResp.json()) as { items: { id: string; status: string }[] };
|
||||
const account = accounts.items.find((a) => a.status === "active");
|
||||
if (!account) throw new Error("no active account");
|
||||
const wsResp = await fetch(`${CONSOLE}/api/accounts/${account.id}/worker-secret`, { headers: { Cookie: cookie } });
|
||||
const { worker_id, secret } = (await wsResp.json()) as { worker_id: string; secret: string };
|
||||
console.log("worker:", worker_id, "secret:", secret.slice(0, 8) + "…");
|
||||
|
||||
/** 带结构化 HTML 的测试邮件(标题/加粗/链接/列表/表格) */
|
||||
const mime = [
|
||||
"From: E2E <e2e@example.org>",
|
||||
"To: hello@example.test",
|
||||
"Subject: Markdown 全链测试",
|
||||
"Message-ID: <md-e2e-1@example.org>",
|
||||
"MIME-Version: 1.0",
|
||||
'Content-Type: text/html; charset="utf-8"',
|
||||
"",
|
||||
"<html><body><h2>升级通知</h2><p>您的配额已 <b>用尽</b>,请前往 <a href=\"https://example.com/billing\">账单页</a> 处理。</p>",
|
||||
"<ul><li>方案 A</li><li>方案 B</li></ul>",
|
||||
"<table><tr><th>项目</th><th>用量</th></tr><tr><td>存储</td><td>98%</td></tr></table></body></html>",
|
||||
"",
|
||||
].join("\r\n");
|
||||
|
||||
const recipient = "hello@example.test";
|
||||
const sender = "e2e@example.org";
|
||||
|
||||
// Phase 1: authorize
|
||||
const auth = await signWorkerRequest(secret, worker_id, "POST", `${CONSOLE}/internal/email/authorize`,
|
||||
JSON.stringify({ recipient, sender, message_id: "md-e2e-1@example.org" }));
|
||||
const authResp = await fetch(`${CONSOLE}/internal/email/authorize`, {
|
||||
method: "POST",
|
||||
headers: { ...auth, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ recipient, sender, message_id: "md-e2e-1@example.org" }),
|
||||
});
|
||||
const decision = (await authResp.json()) as { decision: string };
|
||||
console.log("authorize:", authResp.status, JSON.stringify(decision));
|
||||
if (decision.decision !== "accept") throw new Error("not accepted");
|
||||
|
||||
// Phase 2: ingest(raw MIME)
|
||||
const ingestAuth = await signWorkerRequest(secret, worker_id, "POST", `${CONSOLE}/internal/email/ingest`, mime);
|
||||
const ingestResp = await fetch(`${CONSOLE}/internal/email/ingest`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...ingestAuth,
|
||||
"Content-Type": "message/rfc822",
|
||||
"X-WmailHub-Recipient": recipient,
|
||||
"X-WmailHub-Sender": sender,
|
||||
},
|
||||
body: mime,
|
||||
});
|
||||
const ingestData = (await ingestResp.json()) as { ok: boolean; id: string; webhook: string | null };
|
||||
console.log("ingest:", ingestResp.status, JSON.stringify(ingestData));
|
||||
|
||||
// 触发 cron 投递
|
||||
const cron = await fetch(`${CONSOLE}/api/admin/cron/run`, { method: "POST", headers: { Cookie: cookie } });
|
||||
console.log("cron:", cron.status, JSON.stringify(await cron.json()));
|
||||
|
||||
// 检查 receiver 侧收到的 payload(直接查库里这条邮件的 markdown_body + receiver 日志)
|
||||
const detail = await fetch(`${CONSOLE}/api/emails/${ingestData.id}`, { headers: { Cookie: cookie } });
|
||||
const emailDetail = (await detail.json()) as { email: { markdown_body: string | null; subject: string } };
|
||||
console.log("markdown_body from DB:", JSON.stringify(emailDetail.email.markdown_body));
|
||||
if (!emailDetail.email.markdown_body?.includes("**用尽**")) throw new Error("markdown missing bold");
|
||||
|
||||
// 等 receiver 落日志后抓最后一条校验 markdown 字段
|
||||
await Bun.write("/tmp/last_payload_check.txt", "pending");
|
||||
const tail = await Bun.file("/tmp/receiver.log").text();
|
||||
const lines = tail.trim().split("\n").filter(Boolean);
|
||||
const last = lines[lines.length - 1];
|
||||
const payload = JSON.parse(last) as Record<string, unknown>;
|
||||
console.log("receiver last:", JSON.stringify(payload).slice(0, 300));
|
||||
const md = String(payload.markdown ?? "");
|
||||
if (!md.includes("**用尽**") || !md.includes("98%")) {
|
||||
console.log("FULL MARKDOWN:", md);
|
||||
throw new Error("receiver payload missing markdown");
|
||||
}
|
||||
console.log("\n=== E2E OK: webhook payload 含完整 Markdown ===");
|
||||
Reference in New Issue
Block a user