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);