import type { Env } from '../types'; export const CN_OFFSET_MS = 8 * 60 * 60 * 1000; /** 毫秒时间戳 → "YYYY-MM-DD HH:mm:ss"(Asia/Shanghai,中国无夏令时) */ export function formatDateCN(ms: number): string { const d = new Date((ms || 0) + CN_OFFSET_MS); const p = (n: number, len = 2) => String(n).padStart(len, '0'); return ( `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ` + `${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}` ); } export function now(): number { return Date.now(); } // ------------------------------------------------------------------ 响应封装 export function json(data: unknown, init: ResponseInit = {}): Response { const headers = new Headers(init.headers); headers.set('Content-Type', 'application/json; charset=utf-8'); return new Response(JSON.stringify(data ?? {}), { ...init, headers }); } /** Artalk 成功响应:RespData 是「裸数据」,RespSuccess 是 {"msg":"Success"} */ export function ok(data: unknown): Response { return json(data); } export function okMsg(msg = 'Success', extra: Record = {}): Response { return json({ msg, ...extra }); } /** Artalk 错误响应:{"msg": "..."} + 非 200 状态码 */ export function fail(status: number, msg: string, extra: Record = {}): Response { return json({ msg, ...extra }, { status }); } // ------------------------------------------------------------------ 请求解析 export function getClientIP(req: Request): string { return ( req.headers.get('CF-Connecting-IP') || (req.headers.get('X-Forwarded-For') || '').split(',')[0].trim() || '127.0.0.1' ); } export function getUserAgent(req: Request): string { return req.headers.get('User-Agent') || ''; } export async function readBody(req: Request): Promise> { const ct = req.headers.get('Content-Type') || ''; try { if (ct.includes('application/json')) { const v = await req.json(); return v && typeof v === 'object' ? (v as Record) : {}; } if (ct.includes('form-urlencoded')) { const text = await req.text(); const out: Record = {}; for (const [k, v] of new URLSearchParams(text)) out[k] = v; return out; } const text = await req.text(); if (!text) return {}; try { const v = JSON.parse(text); return v && typeof v === 'object' ? v : {}; } catch { const out: Record = {}; for (const [k, v] of new URLSearchParams(text)) out[k] = v; return out; } } catch { return {}; } } export function qp(url: URL, key: string): string { return url.searchParams.get(key) ?? ''; } export function qInt(url: URL, key: string, dft = 0): number { const v = parseInt(url.searchParams.get(key) ?? '', 10); return Number.isFinite(v) ? v : dft; } export function qBool(url: URL, key: string): boolean { const v = (url.searchParams.get(key) ?? '').toLowerCase(); return v === '1' || v === 'true' || v === 'yes'; } // ------------------------------------------------------------------ 校验 export function isEmail(s: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((s || '').trim()); } export function isUrl(s: string): boolean { try { const u = new URL(s); return u.protocol === 'http:' || u.protocol === 'https:'; } catch { return false; } } export function trimTo(s: string, max: number): string { const v = String(s ?? ''); return v.length > max ? v.slice(0, max) : v; } // ------------------------------------------------------------------ CORS export function corsHeaders(req: Request, env: Env): Headers { const headers = new Headers(); const origin = req.headers.get('Origin') || ''; const allow = (env.ALLOWED_ORIGINS || '') .split(',') .map((s) => s.trim()) .filter(Boolean); if (origin && (allow.includes(origin) || allow.includes('*'))) { headers.set('Access-Control-Allow-Origin', origin); } else if (!origin) { headers.set('Access-Control-Allow-Origin', '*'); } headers.set('Vary', 'Origin'); headers.set('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS'); headers.set( 'Access-Control-Allow-Headers', 'Content-Type,Authorization,X-Requested-With,X-Api-Version', ); headers.set('Access-Control-Max-Age', '86400'); return headers; }