diff --git a/admin/src/app/admin/daily-report/page.tsx b/admin/src/app/admin/daily-report/page.tsx index 9bf979a..c220c71 100644 --- a/admin/src/app/admin/daily-report/page.tsx +++ b/admin/src/app/admin/daily-report/page.tsx @@ -18,6 +18,7 @@ interface AiConfig { ai_model?: string; ai_api_key?: string; ai_enabled?: string; + basic_api_url?: string; smtp_host?: string; smtp_port?: string; smtp_username?: string; @@ -318,6 +319,15 @@ export default function DailyReportPage() { onChange={e => setAiForm(f => ({ ...f, ai_api_key: e.target.value }))} placeholder="sk-..." /> +
+ + setAiForm(f => ({ ...f, basic_api_url: e.target.value }))} + placeholder="https://api.openai.com(留空使用默认地址)" /> + + 支持 OpenAI 兼容接口,如 Cloudflare AI Gateway、OneAPI 等 + +
{/* SMTP Settings */} diff --git a/admin/src/app/api/admin/daily-report/ai-config/route.ts b/admin/src/app/api/admin/daily-report/ai-config/route.ts index 3bff3da..b9f52da 100644 --- a/admin/src/app/api/admin/daily-report/ai-config/route.ts +++ b/admin/src/app/api/admin/daily-report/ai-config/route.ts @@ -56,7 +56,7 @@ export async function PUT(req: NextRequest) { const body = await req.json() as Record; const adminUserId = parseInt(req.headers.get("x-admin-user-id") || "0", 10); const allowedKeys = [ - "ai_model", "ai_api_key", "ai_enabled", + "ai_model", "ai_api_key", "ai_enabled", "basic_api_url", "smtp_host", "smtp_port", "smtp_username", "smtp_password", "smtp_from", "smtp_tls", "report_enabled", ]; diff --git a/admin/src/app/api/admin/daily-report/generate/route.ts b/admin/src/app/api/admin/daily-report/generate/route.ts index 94a20d1..42f8301 100644 --- a/admin/src/app/api/admin/daily-report/generate/route.ts +++ b/admin/src/app/api/admin/daily-report/generate/route.ts @@ -50,6 +50,7 @@ interface AiConfig { ai_model: string; ai_api_key: string; ai_enabled: string; + basic_api_url: string; smtp_host: string; smtp_port: string; smtp_username: string; @@ -62,10 +63,11 @@ interface AiConfig { // ─── Main handler ───────────────────────────────────────────────────────────── export async function POST(req: NextRequest) { - // Verify cron secret (optional, set in K8s CronJob annotation) + // Verify cron — accept internal marker OR secret from K8s CronJob + const cronInternal = req.headers.get("x-cron-internal"); const cronSecret = req.headers.get("x-cron-secret"); const expectedSecret = process.env.DAILY_REPORT_CRON_SECRET; - if (expectedSecret && cronSecret !== expectedSecret) { + if (!cronInternal && (!expectedSecret || cronSecret !== expectedSecret)) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } @@ -101,7 +103,12 @@ export async function POST(req: NextRequest) { // ── Generate AI summary ────────────────────────────────────────────────── let aiSummary = ""; if (cfg.ai_enabled === "true" && cfg.ai_api_key) { - aiSummary = await generateAiSummary(stats, cfg.ai_model || "gpt-4o-mini", cfg.ai_api_key); + aiSummary = await generateAiSummary( + stats, + cfg.ai_model || "gpt-4o-mini", + cfg.ai_api_key, + cfg.basic_api_url || "" + ); } // ── Build email content ───────────────────────────────────────────────── @@ -172,17 +179,17 @@ async function collectDailyStats(): Promise { ), // Active users today (users who sent messages) query<{ count: string }>( - `SELECT COUNT(DISTINCT user_id)::text as count + `SELECT COUNT(DISTINCT sender_id)::text as count FROM room_message WHERE created_at >= $1`, [todayStr] ), // Top room by message count today - query<{ room_id: string; room_name: string; message_count: string }>( - `SELECT rm.room_id, r.name as room_name, COUNT(*)::text as message_count + query<{ room: string; room_name: string; message_count: string }>( + `SELECT rm.room, r.name as room_name, COUNT(*)::text as message_count FROM room_message rm - JOIN room r ON r.id = rm.room_id + JOIN room r ON r.id = rm.room WHERE rm.created_at >= $1 - GROUP BY rm.room_id, r.name + GROUP BY rm.room, r.name ORDER BY COUNT(*) DESC LIMIT 1`, [todayStr] @@ -196,7 +203,7 @@ async function collectDailyStats(): Promise { activeUsers: parseInt(activeUserRow.rows[0]?.count || "0", 10), newCommits: 0, // filled below topRoom: topRoomRow.rows[0] ? { - id: topRoomRow.rows[0].room_id, + id: topRoomRow.rows[0].room, name: topRoomRow.rows[0].room_name, messageCount: parseInt(topRoomRow.rows[0].message_count || "0", 10), } : null, @@ -221,7 +228,7 @@ async function collectDailyStats(): Promise { const messages = await query<{ content: string; created_at: string }>( `SELECT content, created_at::text FROM room_message - WHERE room_id = $1 AND created_at >= $2 + WHERE room = $1 AND created_at >= $2 ORDER BY created_at DESC LIMIT 20`, [stats.topRoom.id, todayStr] @@ -240,7 +247,8 @@ async function collectDailyStats(): Promise { async function generateAiSummary( stats: DailyStats, model: string, - apiKey: string + apiKey: string, + basicApiUrl: string ): Promise { const systemPrompt = `你是一名平台运营分析师。请根据以下每日平台数据,生成一段简洁的中文总结(100-200字),分析今日平台的关键变化和亮点。注意: 1. 用专业但易懂的语言 @@ -275,8 +283,11 @@ async function generateAiSummary( ${topRoomContext} ${userMessagesSection}`; + const baseUrl = basicApiUrl || "https://api.openai.com"; + const chatEndpoint = `${baseUrl.replace(/\/$/, "")}/v1/chat/completions`; + try { - const response = await fetch("https://api.openai.com/v1/chat/completions", { + const response = await fetch(chatEndpoint, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/admin/src/lib/daily-report-cron.ts b/admin/src/lib/daily-report-cron.ts index 4efe2c3..a01222d 100644 --- a/admin/src/lib/daily-report-cron.ts +++ b/admin/src/lib/daily-report-cron.ts @@ -30,12 +30,16 @@ async function runReport() { // Call generate endpoint internally (server-side fetch, no auth needed for cron) const baseUrl = process.env.NEXT_PUBLIC_APP_URL || `http://localhost:${process.env.PORT || 3000}`; + const headers: Record = { + "Content-Type": "application/json", + "x-cron-internal": "true", + }; + if (process.env.DAILY_REPORT_CRON_SECRET) { + headers["x-cron-secret"] = process.env.DAILY_REPORT_CRON_SECRET; + } const res = await fetch(`${baseUrl}/api/admin/daily-report/generate`, { method: "POST", - headers: { - "Content-Type": "application/json", - "x-cron-internal": "true", // internal marker, not x-cron-secret - }, + headers, }); const data = await res.json().catch(() => ({})); console.log("[daily-report-cron] Result:", res.status, data);