import { env } from "../../config/env.js"; import { logger } from "../observability/logger.js"; /** * Push Gateway HTTP 客户端。 * * 仲裁依据: * - M4:HTTP POST /internal/push(豁免 gRPC) * - PushGateway 软失败:不可用时返回 false,不阻断主流程 * - 鉴权头:X-Internal-Key = PUSH_INTERNAL_TOKEN * * 请求体对齐 push-gateway handler.go PushHandler: * { userId, event, data } */ export interface PushRequest { userId: string; event: string; data: Record; } export interface PushResult { sent: boolean; error?: string; } /** * 向 push-gateway 发送实时推送。 * * 软失败语义: * - PUSH_GATEWAY_URL 未配置 → 返回 { sent: false },不报错 * - 网络错误/非 2xx → 返回 { sent: false, error },logger.warn * - 成功 → 返回 { sent: true } */ export async function sendPush(req: PushRequest): Promise { if (!env.PUSH_GATEWAY_URL) { return { sent: false }; } const url = `${env.PUSH_GATEWAY_URL}/internal/push`; const headers: Record = { "Content-Type": "application/json", }; if (env.PUSH_INTERNAL_TOKEN) { headers["X-Internal-Key"] = env.PUSH_INTERNAL_TOKEN; } try { const res = await fetch(url, { method: "POST", headers, body: JSON.stringify({ userId: req.userId, event: req.event, data: req.data, }), }); if (!res.ok) { const error = `push-gateway returned ${res.status}`; logger.warn({ status: res.status, userId: req.userId }, error); return { sent: false, error }; } return { sent: true }; } catch (err) { const error = err instanceof Error ? err.message : String(err); logger.warn({ err, userId: req.userId }, "Push gateway unavailable"); return { sent: false, error }; } } /** * 批量推送(逐条发送,push-gateway 无批量 API)。 * 任一失败不影响其他,返回每条结果。 */ export async function sendPushBatch( requests: PushRequest[], ): Promise { const results: PushResult[] = []; for (const req of requests) { results.push(await sendPush(req)); } return results; } /** * 健康检查:探测 push-gateway /readyz。 * 软失败:不可用返回 false,不阻断 /readyz(仲裁 M2)。 */ export async function checkPushGateway(): Promise { if (!env.PUSH_GATEWAY_URL) return false; try { const res = await fetch(`${env.PUSH_GATEWAY_URL}/readyz`, { signal: AbortSignal.timeout(2000), }); return res.ok; } catch { return false; } }