/** Console API 客户端:统一 JSON 请求、错误处理、401 跳登录 */ /** # 成员变量: 未登录回调(由应用注册,跳转登录页) **/ let onUnauthorized: (() => void) | null = null; /** # 函数 * 注册 401 处理器 */ export function setUnauthorizedHandler(fn: () => void): void { onUnauthorized = fn; } /** # 函数 * 通用请求封装 * 参数: method/url/body(JSON) * 返回值: 解析后的 JSON;非 2xx 抛 ApiError */ export async function api( url: string, method: string = "GET", body?: unknown, ): Promise { const resp = await fetch(url, { method, credentials: "include", headers: body !== undefined ? { "Content-Type": "application/json" } : undefined, body: body !== undefined ? JSON.stringify(body) : undefined, }); if (resp.status === 401) { onUnauthorized?.(); throw new ApiError(401, "未登录或会话已过期"); } const text = await resp.text(); let data: unknown = null; try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; } if (!resp.ok) { const message = (data as { error?: string } | null)?.error ?? `请求失败 (${resp.status})`; throw new ApiError(resp.status, message, data); } return data as T; } /** # 成员变量: API 错误 **/ export class ApiError extends Error { constructor( public status: number, message: string, public data?: unknown, ) { super(message); } } /** # 函数 * 分页查询串拼接 */ export function qs(params: Record): string { const sp = new URLSearchParams(); for (const [k, v] of Object.entries(params)) { if (v !== undefined && v !== "") sp.set(k, String(v)); } const s = sp.toString(); return s ? `?${s}` : ""; } /* ---- 会话 ---- */ /** 登录 */ export const login = (username: string, password: string) => api<{ ok: boolean; user: { id: string; username: string } }>("/api/auth/login", "POST", { username, password }); /** 登出 */ export const logout = () => api("/api/auth/logout", "POST"); /** 当前用户 */ export const me = () => api<{ user: { id: string; username: string } }>("/api/auth/me"); /** 健康检查(含版本号) */ export const health = () => api<{ ok: boolean; name: string; version: string }>("/api/health"); /* ---- 概览 ---- */ /** Dashboard 统计 */ export const stats = () => api<{ accounts_total: number; accounts_active: number; domains: number; domains_active: number; mailboxes: number; emails: number; emails_expired: number; global_webhook: number; deliveries_pending: number; oauth_connections: number; worker_credentials: number; recent_emails: { id: string; subject: string; from_address: string; to_addresses: string; received_at: string; size: number; }[]; }>("/api/admin/stats"); /** 手动触发 Cron */ export const runCron = () => api<{ ok: boolean; stats: Record }>("/api/admin/cron/run", "POST"); /* ---- 账号(Cloudflare 账户) ---- */ export interface AccountRow { id: string; name: string; cloudflare_account_id: string | null; worker_id: string | null; status: string; created_at: string; } export const listAccounts = () => api<{ items: AccountRow[]; total: number }>("/api/accounts"); export interface AccountDetail { account: AccountRow; steps: Record; oauth: { id: string; account_id: string | null; created_at: string } | null; domains: { id: string; name: string; status: string; worker_id: string | null }[]; credential: { id: string; worker_id: string; secret_last4: string; status: string; created_at: string } | null; } export const getAccount = (id: string) => api(`/api/accounts/${id}`); export const updateAccount = (id: string, body: { name?: string }) => api(`/api/accounts/${id}`, "PATCH", body); export const deleteAccount = (id: string) => api(`/api/accounts/${id}`, "DELETE"); /** 用 Cloudflare API Token 添加账号(与 OAuth 并列的第二种接入方式)。 * 后端会先用 token 探一次 Cloudflare 确认可用、且与填写的账户 ID 一致,再建账号并自动接入。 */ export const createAccountByToken = (body: { name: string; cloudflare_account_id?: string; token: string; }) => api<{ ok: boolean; account_id: string; error?: string }>("/api/accounts/token", "POST", body); /** 轮换 Worker credential:secret 仅本次返回可见;已配置域名自动重新部署 */ export const rotateAccount = (id: string) => api<{ ok: boolean; worker_id: string; credential_id: string; secret: string; redeployed: string[] }>( `/api/accounts/${id}/rotate`, "POST", ); /** 本地/运维:查看账号当前 Worker credential(secret 用于部署/联调) */ export const getAccountWorkerSecret = (id: string) => api<{ worker_id: string; secret: string }>(`/api/accounts/${id}/worker-secret`); /* ---- OAuth ---- */ /** OAuth 是否已配置(cloudflare = 可用;unconfigured = 缺少 client id/secret) */ export const oauthMode = () => api<{ mode: "cloudflare" | "unconfigured"; authorize_url_base: string | null }>("/api/oauth/mode"); /** 开始授权(自动创建账号记录;授权后自动接入) */ export const oauthStart = () => api<{ url: string; state: string }>("/api/oauth/cloudflare/start"); /* ---- 域名 ---- */ export interface DomainRow { id: string; name: string; status: string; setup_error: string | null; worker_id: string | null; zone_id: string | null; account_id: string | null; account_name: string | null; webhook_url: string | null; webhook_headers: string | null; webhook_has_key: number; mailbox_count: number; created_at: string; } export const listDomains = () => api<{ items: DomainRow[]; total: number }>("/api/domains"); /** 域名页账号筛选项 */ export const listDomainAccounts = () => api<{ items: { id: string; name: string }[] }>("/api/domains/filters/accounts"); /** 为域名配置 Email Workers(Worker + Email Routing + catch-all) */ export const setupDomain = (id: string) => api<{ ok: boolean; error?: string }>(`/api/domains/${id}/setup`, "POST"); /** 保存域名级 Webhook(url 空串清除;key undefined 保留原值) */ export const putDomainWebhook = ( id: string, body: { url: string; headers?: Record; key?: string }, ) => api(`/api/domains/${id}/webhook`, "PUT", body); export const deleteDomainWebhook = (id: string) => api(`/api/domains/${id}/webhook`, "DELETE"); export const deleteDomain = (id: string) => api(`/api/domains/${id}`, "DELETE"); /* ---- 邮箱 ---- */ export interface MailboxRow { id: string; address: string; status: string; is_catch_all: number; retention_seconds: number | null; domain_id: string; domain_name: string; webhook_url: string | null; webhook_headers: string | null; webhook_has_key: number; email_count: number; created_at: string; } export const listMailboxes = (domainId?: string) => api<{ items: MailboxRow[]; total: number }>(`/api/mailboxes${qs({ domain_id: domainId })}`); export const createMailbox = (body: { domain_id: string; address: string; retention_seconds?: number; catch_all?: boolean }) => api<{ ok: boolean; id: string; address: string }>("/api/mailboxes", "POST", body); export const patchMailbox = (id: string, body: { status?: string; retention_seconds?: number | null }) => api(`/api/mailboxes/${id}`, "PATCH", body); /** 保存邮箱级 Webhook(url 空串清除;key undefined 保留原值) */ export const putMailboxWebhook = ( id: string, body: { url: string; headers?: Record; key?: string }, ) => api(`/api/mailboxes/${id}/webhook`, "PUT", body); export const deleteMailboxWebhook = (id: string) => api(`/api/mailboxes/${id}/webhook`, "DELETE"); export const deleteMailbox = (id: string) => api(`/api/mailboxes/${id}`, "DELETE"); /* ---- 邮件 ---- */ export interface EmailRow { id: string; mailbox_id: string | null; mailbox_address: string | null; message_id: string | null; from_address: string | null; to_addresses: string; subject: string | null; size: number; received_at: string; expires_at: string; attachment_count: number; } export const listEmails = (params: { domain_id?: string; mailbox_id?: string; q?: string; limit?: number; offset?: number; }) => api<{ items: EmailRow[]; total: number }>(`/api/emails${qs(params)}`); export interface EmailDetail { email: EmailRow & { text_body: string | null; html_body: string | null; raw_storage_key: string; cc_addresses: string }; attachments: { id: string; filename: string; content_type: string; size: number; content_id: string | null }[]; } export const getEmail = (id: string) => api(`/api/emails/${id}`); export const deleteEmail = (id: string) => api(`/api/emails/${id}`, "DELETE"); /* ---- 批量删除(列表复选框) ---- */ /** # 成员变量: 批量删除结果 **/ export interface BatchDeleteResult { ok: boolean; /** 成功删除的主记录数 **/ deleted: number; /** 级联删除的邮箱数(域名批量删除时有值) **/ mailboxes?: number; /** 级联删除的邮件数(域名/邮箱批量删除时有值) **/ emails?: number; /** 删除失败的 id 及原因(不存在/已删除) **/ failed: { id: string; reason: string }[]; } export const batchDeleteAccounts = (ids: string[]) => api("/api/accounts/batch-delete", "POST", { ids }); export const batchDeleteDomains = (ids: string[]) => api("/api/domains/batch-delete", "POST", { ids }); export const batchDeleteMailboxes = (ids: string[]) => api("/api/mailboxes/batch-delete", "POST", { ids }); export const batchDeleteEmails = (ids: string[]) => api("/api/emails/batch-delete", "POST", { ids }); /* ---- 设置 ---- */ export interface SettingsInfo { username: string; webhook: { url: string; headers: Record; has_key: boolean; key_last4: string }; api_key: { has_key: boolean; key_last4: string; created_at: string | null }; /** Cloudflare OAuth 客户端凭据元信息(不含明文 secret) */ cf_oauth: { client_id: string; has_secret: boolean; secret_last4: string; /** settings = 后台设置;env = 部署变量兜底;none = 未配置 */ source: "settings" | "env" | "none"; /** 实际请求的 scope(点号分隔,空格分隔多个) */ scope: string; }; /** 需要在 Cloudflare Dashboard 登记的授权回调地址 */ redirect_uri: string; } export const getSettings = () => api("/api/settings"); /** 保存设置 * webhook.key / cf_oauth.secret 语义一致:undefined 保留原值、空串清除、非空设置新值 */ export const putSettings = (body: { webhook?: { url: string; headers: Record; key?: string }; cf_oauth?: { client_id: string; secret?: string; scope?: string }; }) => api("/api/settings", "PUT", body); /** # 成员变量: 授权配置自检结果 **/ export interface OAuthPreflightResult { ok: boolean; location: string; error: string | null; description: string | null; hint: string | null; used: { client_id: string; redirect_uri: string; scope: string }; /** scope 探测:allowed=App 已授权、denied=还需在选择器里补勾 */ scopes: { allowed: string[]; denied: string[]; probed: boolean; note: string | null }; } /** 本应用默认需要的 scope(点号分隔)——与后端 CF_OAUTH_SCOPES_DEFAULT 保持一致 */ export const CF_OAUTH_SCOPES_DEFAULT = "zone.read workers-scripts.write email-routing-rule.write zone-settings.write"; /** 授权配置自检:探测 Cloudflare 授权端点是否接受当前配置(不产生授权) */ export const oauthPreflight = () => api("/api/oauth/cloudflare/preflight"); /** 清除后台保存的 Cloudflare OAuth 客户端凭据(env 兜底不受影响) */ export const deleteCfOAuth = () => api("/api/settings/cf-oauth", "DELETE"); export const changePassword = (old_password: string, new_password: string) => api("/api/settings/password", "PUT", { old_password, new_password }); /* ---- API/MCP Key(单一 Key,设置模态窗口内使用;API 与 MCP 共用) ---- */ /** 生成(轮换)Key:明文仅本次返回 */ export const createApiKey = () => api<{ ok: boolean; key: string; created_at: string }>("/api/settings/api-key", "POST"); /** 撤销并删除 Key */ export const deleteApiKey = () => api("/api/settings/api-key", "DELETE"); /* ---- 随机邮箱名 ---- */ /** # 成员变量: 随机用户名来源(namedb,返回 { username, first, last, gender }) **/ const NAMEDB_URL = "https://namedb.adx9.com/api/username"; /** 从 namedb 拉一个随机用户名(用于邮箱地址的本地部分)。 * 返回值: 用户名字符串;失败时抛错,由调用方决定提示文案。 */ export async function fetchRandomUsername(): Promise { const resp = await fetch(NAMEDB_URL); if (!resp.ok) throw new Error(`随机用户名服务返回 ${resp.status}`); const data = (await resp.json()) as { username?: string }; const name = (data.username ?? "").trim(); if (!name) throw new Error("随机用户名服务没有返回可用名字"); return name; }