feat(admin): daily-report support custom basic_api_url for AI endpoint

This commit is contained in:
ZhenYi 2026-04-22 20:53:43 +08:00
parent c41f4efc04
commit da96cdd236
4 changed files with 42 additions and 17 deletions

View File

@ -18,6 +18,7 @@ interface AiConfig {
ai_model?: string; ai_model?: string;
ai_api_key?: string; ai_api_key?: string;
ai_enabled?: string; ai_enabled?: string;
basic_api_url?: string;
smtp_host?: string; smtp_host?: string;
smtp_port?: string; smtp_port?: string;
smtp_username?: string; smtp_username?: string;
@ -318,6 +319,15 @@ export default function DailyReportPage() {
onChange={e => setAiForm(f => ({ ...f, ai_api_key: e.target.value }))} onChange={e => setAiForm(f => ({ ...f, ai_api_key: e.target.value }))}
placeholder="sk-..." /> placeholder="sk-..." />
</div> </div>
<div className="form-group">
<label className="form-label"> API </label>
<input className="form-input" value={aiForm.basic_api_url || ""}
onChange={e => setAiForm(f => ({ ...f, basic_api_url: e.target.value }))}
placeholder="https://api.openai.com留空使用默认地址" />
<span style={{ fontSize: "12px", color: "#737373" }}>
OpenAI Cloudflare AI GatewayOneAPI
</span>
</div>
</div> </div>
{/* SMTP Settings */} {/* SMTP Settings */}

View File

@ -56,7 +56,7 @@ export async function PUT(req: NextRequest) {
const body = await req.json() as Record<string, string>; const body = await req.json() as Record<string, string>;
const adminUserId = parseInt(req.headers.get("x-admin-user-id") || "0", 10); const adminUserId = parseInt(req.headers.get("x-admin-user-id") || "0", 10);
const allowedKeys = [ 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", "smtp_host", "smtp_port", "smtp_username", "smtp_password", "smtp_from", "smtp_tls",
"report_enabled", "report_enabled",
]; ];

View File

@ -50,6 +50,7 @@ interface AiConfig {
ai_model: string; ai_model: string;
ai_api_key: string; ai_api_key: string;
ai_enabled: string; ai_enabled: string;
basic_api_url: string;
smtp_host: string; smtp_host: string;
smtp_port: string; smtp_port: string;
smtp_username: string; smtp_username: string;
@ -62,10 +63,11 @@ interface AiConfig {
// ─── Main handler ───────────────────────────────────────────────────────────── // ─── Main handler ─────────────────────────────────────────────────────────────
export async function POST(req: NextRequest) { 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 cronSecret = req.headers.get("x-cron-secret");
const expectedSecret = process.env.DAILY_REPORT_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 }); return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
} }
@ -101,7 +103,12 @@ export async function POST(req: NextRequest) {
// ── Generate AI summary ────────────────────────────────────────────────── // ── Generate AI summary ──────────────────────────────────────────────────
let aiSummary = ""; let aiSummary = "";
if (cfg.ai_enabled === "true" && cfg.ai_api_key) { 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 ───────────────────────────────────────────────── // ── Build email content ─────────────────────────────────────────────────
@ -172,17 +179,17 @@ async function collectDailyStats(): Promise<DailyStats> {
), ),
// Active users today (users who sent messages) // Active users today (users who sent messages)
query<{ count: string }>( 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`, FROM room_message WHERE created_at >= $1`,
[todayStr] [todayStr]
), ),
// Top room by message count today // Top room by message count today
query<{ room_id: string; room_name: string; message_count: string }>( query<{ room: string; room_name: string; message_count: string }>(
`SELECT rm.room_id, r.name as room_name, COUNT(*)::text as message_count `SELECT rm.room, r.name as room_name, COUNT(*)::text as message_count
FROM room_message rm 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 WHERE rm.created_at >= $1
GROUP BY rm.room_id, r.name GROUP BY rm.room, r.name
ORDER BY COUNT(*) DESC ORDER BY COUNT(*) DESC
LIMIT 1`, LIMIT 1`,
[todayStr] [todayStr]
@ -196,7 +203,7 @@ async function collectDailyStats(): Promise<DailyStats> {
activeUsers: parseInt(activeUserRow.rows[0]?.count || "0", 10), activeUsers: parseInt(activeUserRow.rows[0]?.count || "0", 10),
newCommits: 0, // filled below newCommits: 0, // filled below
topRoom: topRoomRow.rows[0] ? { topRoom: topRoomRow.rows[0] ? {
id: topRoomRow.rows[0].room_id, id: topRoomRow.rows[0].room,
name: topRoomRow.rows[0].room_name, name: topRoomRow.rows[0].room_name,
messageCount: parseInt(topRoomRow.rows[0].message_count || "0", 10), messageCount: parseInt(topRoomRow.rows[0].message_count || "0", 10),
} : null, } : null,
@ -221,7 +228,7 @@ async function collectDailyStats(): Promise<DailyStats> {
const messages = await query<{ content: string; created_at: string }>( const messages = await query<{ content: string; created_at: string }>(
`SELECT content, created_at::text `SELECT content, created_at::text
FROM room_message FROM room_message
WHERE room_id = $1 AND created_at >= $2 WHERE room = $1 AND created_at >= $2
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 20`, LIMIT 20`,
[stats.topRoom.id, todayStr] [stats.topRoom.id, todayStr]
@ -240,7 +247,8 @@ async function collectDailyStats(): Promise<DailyStats> {
async function generateAiSummary( async function generateAiSummary(
stats: DailyStats, stats: DailyStats,
model: string, model: string,
apiKey: string apiKey: string,
basicApiUrl: string
): Promise<string> { ): Promise<string> {
const systemPrompt = `你是一名平台运营分析师。请根据以下每日平台数据生成一段简洁的中文总结100-200字分析今日平台的关键变化和亮点。注意 const systemPrompt = `你是一名平台运营分析师。请根据以下每日平台数据生成一段简洁的中文总结100-200字分析今日平台的关键变化和亮点。注意
1. 1.
@ -275,8 +283,11 @@ async function generateAiSummary(
${topRoomContext} ${topRoomContext}
${userMessagesSection}`; ${userMessagesSection}`;
const baseUrl = basicApiUrl || "https://api.openai.com";
const chatEndpoint = `${baseUrl.replace(/\/$/, "")}/v1/chat/completions`;
try { try {
const response = await fetch("https://api.openai.com/v1/chat/completions", { const response = await fetch(chatEndpoint, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@ -30,12 +30,16 @@ async function runReport() {
// Call generate endpoint internally (server-side fetch, no auth needed for cron) // 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 baseUrl = process.env.NEXT_PUBLIC_APP_URL || `http://localhost:${process.env.PORT || 3000}`;
const headers: Record<string, string> = {
"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`, { const res = await fetch(`${baseUrl}/api/admin/daily-report/generate`, {
method: "POST", method: "POST",
headers: { headers,
"Content-Type": "application/json",
"x-cron-internal": "true", // internal marker, not x-cron-secret
},
}); });
const data = await res.json().catch(() => ({})); const data = await res.json().catch(() => ({}));
console.log("[daily-report-cron] Result:", res.status, data); console.log("[daily-report-cron] Result:", res.status, data);