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_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-..." />
</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>
{/* SMTP Settings */}

View File

@ -56,7 +56,7 @@ export async function PUT(req: NextRequest) {
const body = await req.json() as Record<string, string>;
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",
];

View File

@ -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<DailyStats> {
),
// 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<DailyStats> {
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<DailyStats> {
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<DailyStats> {
async function generateAiSummary(
stats: DailyStats,
model: string,
apiKey: string
apiKey: string,
basicApiUrl: string
): Promise<string> {
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",

View File

@ -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<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`, {
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);