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_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 Gateway、OneAPI 等
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SMTP Settings */}
|
{/* SMTP Settings */}
|
||||||
|
|||||||
@ -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",
|
||||||
];
|
];
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user