feat(admin): daily-report support custom basic_api_url for AI endpoint
This commit is contained in:
parent
c41f4efc04
commit
da96cdd236
@ -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 Gateway、OneAPI 等
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SMTP Settings */}
|
||||
|
||||
@ -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",
|
||||
];
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user