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:
bigcode
2026-09-30 13:27:28 +08:00
parent fec96e49cc
commit 7370cce784
38 changed files with 1281 additions and 512 deletions
+134
View File
@@ -0,0 +1,134 @@
/** Markdown 转换与 webhook markdown 字段测试(v2.1.0) */
import { describe, expect, test } from "bun:test";
import { htmlToMarkdown } from "../shared/src/markdown.ts";
import { ingestEmail } from "../console/src/services/email_ingest.ts";
import { buildWebhookPayload } from "../console/src/services/webhook_delivery.ts";
import { loginCookie, makeTestEnv, TEST_MAIL, authedFetch } from "./helpers.ts";
/** # 成员变量: 纯文本邮件样例(无 HTML part) **/
const TEST_MAIL_PLAIN = `From: Bob <bob@example.org>\r
To: hello@example.test\r
Subject: Plain Mail\r
Content-Type: text/plain\r
\r
plain body\r
`;
/** # 成员变量: HTML 邮件样例(text/html part 带 p/b 结构) **/
const TEST_MAIL_HTML = `From: Carl <carl@example.org>\r
To: hello@example.test\r
Subject: Html Mail\r
MIME-Version: 1.0\r
Content-Type: text/html\r
\r
<html><body><p>Hello <b>world</b> and <a href="https://example.com">link</a></p></body></html>\r
`;
describe("htmlToMarkdown(mailhook 同思路)", () => {
test("标题/加粗/链接/行内代码", () => {
expect(htmlToMarkdown("<h2>Hello</h2><p>World <b>bold</b> <a href='https://x.com'>link</a> <code>cfg</code></p>")).toBe(
"## Hello\n\nWorld **bold** [link](https://x.com) `cfg`",
);
});
test("列表与嵌套列表", () => {
expect(htmlToMarkdown("<ul><li>one</li><li>two<ol><li>nested</li></ol></li></ul>")).toBe(
"- one\n\n- two\n\n 1. nested",
);
});
test("表格转管道表(| 转义)", () => {
expect(htmlToMarkdown("<table><tr><th>A</th></tr><tr><td>1|2</td></tr></table>")).toBe(
"| A |\n| --- |\n| 1\\|2 |",
);
});
test("pre 代码块保留换行;blockquote 引用;hr 分隔线", () => {
expect(htmlToMarkdown("<pre>line1\nline2</pre>")).toBe("```\nline1\nline2\n```");
expect(htmlToMarkdown("<blockquote>quoted</blockquote>")).toBe("> quoted");
expect(htmlToMarkdown("<hr/>")).toBe("---");
});
test("script/style 丢弃;空内容返回空串", () => {
expect(htmlToMarkdown("<script>alert(1)</script><style>.x{}</style>")).toBe("");
expect(htmlToMarkdown("")).toBe("");
});
});
describe("邮件入库 markdown_body 与 webhook markdown 字段", () => {
test("HTML 邮件入库时存 Markdown;纯文本邮件 markdown=纯文本", async () => {
const t = await makeTestEnv();
const html = await ingestEmail(t.env, {
raw: new TextEncoder().encode(TEST_MAIL_HTML),
mailFrom: "a@b.c",
rcptTo: "hello@example.test",
accountId: t.accountId,
});
const row = await t.env.DB.prepare("SELECT markdown_body, text_body, html_body FROM emails WHERE id = ?")
.bind(html.id)
.first<{ markdown_body: string | null; text_body: string | null; html_body: string | null }>();
expect(row?.html_body).toBeTruthy();
expect(row?.markdown_body).toContain("**");
expect(row?.markdown_body?.trim().length ?? 0).toBeGreaterThan(0);
const plain = await ingestEmail(t.env, {
raw: new TextEncoder().encode(TEST_MAIL_PLAIN),
mailFrom: "a@b.c",
rcptTo: "hello@example.test",
accountId: t.accountId,
});
const row2 = await t.env.DB.prepare("SELECT markdown_body FROM emails WHERE id = ?")
.bind(plain.id)
.first<{ markdown_body: string | null }>();
expect(row2?.markdown_body).toBeTruthy();
});
test("webhook payload 含 markdown 字段(优先存量 markdown_body)", () => {
const payload = buildWebhookPayload({
id: "msg_x",
mailboxAddress: "hello@example.test",
fromAddress: "a@b.c",
toAddresses: '["hello@example.test"]',
subject: "s",
textBody: "plain body",
htmlBody: "<p>hello <b>world</b></p>",
markdownBody: null,
receivedAt: new Date().toISOString(),
});
expect(payload.markdown).toBe("hello **world**");
const payload2 = buildWebhookPayload({
id: "msg_y",
mailboxAddress: null,
fromAddress: null,
toAddresses: "[]",
subject: null,
textBody: null,
htmlBody: null,
markdownBody: "stored md",
receivedAt: new Date().toISOString(),
});
expect(payload2.markdown).toBe("stored md");
});
test("设置接口返回 api_key 元信息;生成接口明文 32 位 wmh_", async () => {
const t = await makeTestEnv();
const cookie = await loginCookie(t);
const before = (await (await authedFetch(t, cookie, "http://localhost/api/settings")).json()) as {
api_key: { has_key: boolean; key_last4: string };
};
expect(before.api_key.has_key).toBe(false);
const created = (await (
await authedFetch(t, cookie, "http://localhost/api/settings/api-key", { method: "POST" })
).json()) as { key: string };
expect(created.key.startsWith("wmh_")).toBe(true);
expect(created.key.length).toBe(32);
const after = (await (await authedFetch(t, cookie, "http://localhost/api/settings")).json()) as {
api_key: { has_key: boolean; key_last4: string };
};
expect(after.api_key.has_key).toBe(true);
expect(created.key.endsWith(after.api_key.key_last4)).toBe(true);
});
});