/** * 本地开发一键启动:console + email worker(docs/11,Node 已移除)。 * bun run dev # 常规启动(复用既有数据) * bun run dev -- --reset # 清空本地数据后全新接入 * * 流程: * 1. 构建前端(vite build) * 2. 启动 Console(:18787,自动迁移 + admin/admin 种子) * 3. 已有 OAuth 接入的账号则为其配置未激活域名的 Email Workers * (域名 setup 会部署账号级 Email Worker + 签发 WorkerCredential),然后启动本地 Email Worker * 4. 没有账号时只跑 Console:Cloudflare OAuth 需要浏览器人工授权,不再有模拟通道 * 5. Ctrl-C 一起退出 */ const ROOT = new URL("..", import.meta.url).pathname; const CONSOLE_PORT = 18787; const WORKER_PORT = 18789; const BASE = `http://127.0.0.1:${CONSOLE_PORT}`; const reset = process.argv.includes("--reset"); if (reset) { await Bun.$`rm -rf ${ROOT}console/data`.quiet(); console.log("[dev] 本地数据已重置"); } /** 前端构建(产物已存在时可跳过:SKIP_BUILD=1) */ if (!process.env.SKIP_BUILD) { console.log("[dev] 构建前端..."); await Bun.$`bun run build`.cwd(ROOT); } interface Child { name: string; proc: Bun.Subprocess<"ignore", "inherit", "inherit">; } const children: Child[] = []; /** 启动子进程并登记 */ function spawn(name: string, args: string[], cwd: string, env?: Record) { const proc = Bun.spawn(["bun", ...args], { cwd, env: { ...process.env, ...env }, stdout: "inherit", stderr: "inherit", stdin: "ignore", }); children.push({ name, proc }); return proc; } /** 等待 HTTP 就绪 */ async function waitReady(url: string, label: string, tries = 60): Promise { for (let i = 0; i < tries; i++) { try { const resp = await fetch(url); if (resp.ok) return; } catch { // not ready } await new Promise((r) => setTimeout(r, 500)); } throw new Error(`${label} 未就绪:${url}`); } // 1. Console spawn("console", ["console/src/local/main.ts"], ROOT); await waitReady(`${BASE}/api/health`, "console"); console.log("[dev] console ready"); // 2. 登录拿会话 const loginResp = await fetch(`${BASE}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "admin", password: "admin" }), }); if (!loginResp.ok) throw new Error("admin 登录失败"); const cookie = (loginResp.headers.get("set-cookie") ?? "").split(";")[0]; const authHeaders = { Cookie: cookie, "Content-Type": "application/json" }; // 3. 账号:OAuth 授权必须由浏览器人工完成,这里只消费已接入的账号 const accountsResp = await fetch(`${BASE}/api/accounts`, { headers: authHeaders }); const existing = ((await accountsResp.json()) as { items: { id: string; name: string }[] }).items; if (existing.length === 0) { console.log(""); console.log("======== WmailHub 本地 Console 已就绪(尚未接入 Cloudflare 账号)========"); console.log(` Console: ${BASE} (admin / admin)`); console.log(` 下一步 : 打开 Console → 账号 → 添加账号,完成 Cloudflare OAuth 授权`); console.log(` (需先配置 CF_OAUTH_CLIENT_ID / CF_OAUTH_CLIENT_SECRET,并在 Cloudflare`); console.log(` 登记回调 ${BASE}/api/oauth/cloudflare/callback)`); console.log(" 接入成功后重新执行 `bun run dev` 即可一并拉起本地 Email Worker"); console.log("================================================================"); console.log(""); } else { const accountId = existing[0].id; console.log(`[dev] 使用已接入账号 ${accountId}`); // 4. 域名同步后逐域名配置 Email Workers(部署 Worker + Routing + catch-all;幂等) const domainsResp = await fetch(`${BASE}/api/domains`, { headers: authHeaders }); const domains = ((await domainsResp.json()) as { items: { id: string; name: string; account_id: string | null; status: string }[] }).items; for (const d of domains.filter((x) => x.account_id === accountId && x.status !== "active")) { const setup = (await ( await fetch(`${BASE}/api/domains/${d.id}/setup`, { method: "POST", headers: authHeaders }) ).json()) as { ok: boolean; error?: string }; if (!setup.ok) throw new Error(`域名 ${d.name} 配置 Email Workers 失败:${setup.error}`); console.log(`[dev] 域名 ${d.name} Email Workers 已配置`); } // 5. Email Worker(authorize/ingest 直连 Console;凭据来自 WorkerCredential) const secretResp = (await ( await fetch(`${BASE}/api/accounts/${accountId}/worker-secret`, { headers: authHeaders }) ).json()) as { worker_id: string; secret: string }; spawn("worker", ["worker/src/local.ts"], ROOT, { CONSOLE_URL: BASE, WORKER_ID: secretResp.worker_id, WORKER_SECRET: secretResp.secret, WMAILHUB_WORKER_PORT: String(WORKER_PORT), }); await waitReady(`http://127.0.0.1:${WORKER_PORT}/__health`, "email worker"); console.log("[dev] email worker ready"); console.log(""); console.log("======== WmailHub 本地环境已就绪 ========"); console.log(` Console : ${BASE} (admin / admin)`); console.log(` Email Worker: http://127.0.0.1:${WORKER_PORT}/__health (worker_id=${secretResp.worker_id})`); console.log(" 发测试邮件 : curl -X POST http://127.0.0.1:18789/__email_test \\"); console.log(" -H 'X-WmailHub-Test-Secret: ' \\"); console.log(" -H 'X-WmailHub-Test-To: hello@example.test' --data-binary @mail.eml"); console.log("=========================================="); } /** 退出时统一收割子进程 */ function shutdown() { for (const { name, proc } of children) { proc.kill(); console.log(`[dev] ${name} stopped`); } process.exit(0); } process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); // 任一子进程退出则整体退出 for (const { name, proc } of children) { void proc.exited.then((code) => { console.error(`[dev] ${name} exited with ${code}`); shutdown(); }); }