/** * 本地开发一键启动:console + email worker(docs/11,Node 已移除)。 * bun run dev # 常规启动(复用既有数据) * bun run dev -- --reset # 清空本地数据后全新接入 * * 流程: * 1. 构建前端(vite build) * 2. 启动 Console(:18787,自动迁移 + admin/admin 种子) * 3. 确保存在账号(无则 OAuth sim 自动创建账号),并逐域名配置 Email Workers * (域名 setup 会部署账号级 Email Worker + 签发 WorkerCredential) * 4. 启动 Email Worker(:18789,authorize/ingest 直连 Console) * 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; let accountId: string; if (existing.length > 0) { accountId = existing[0].id; console.log(`[dev] 复用既有账号 ${accountId}`); } else { // 添加账号:OAuth start 自动创建账号,授权后自动接入 const startResp = await fetch(`${BASE}/api/oauth/cloudflare/start`, { headers: authHeaders }); const { state } = (await startResp.json()) as { state: string }; const approve = await fetch(`${BASE}/api/oauth/cloudflare/sim/approve`, { method: "POST", headers: authHeaders, body: JSON.stringify({ state }), }); const approveRes = (await approve.json()) as { ok: boolean; error?: string; account_id: string }; if (!approveRes.ok) throw new Error(`自动创建账号失败:${approveRes.error}`); accountId = approveRes.account_id; console.log(`[dev] OAuth(sim)已创建账号 ${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(); }); }