import "dotenv/config"; import express from "express"; import { createServer } from "http"; import net from "net"; import path from "node:path"; import { createExpressMiddleware } from "@trpc/server/adapters/express"; import { registerOAuthRoutes } from "./oauth"; import { appRouter } from "../routers"; import { createContext } from "./context"; import { registerMediaProxy } from "./mediaProxy"; import { serveStatic } from "./static"; import { createBackgroundTask, getAdminUserId, hasRecentBackgroundTaskOfType, seedAchievementDefinitions, seedAppSettings, seedTutorials, seedVisionReferenceImages } from "../db"; import { nanoid } from "nanoid"; async function scheduleDailyNtrpRefresh() { const now = new Date(); if (now.getHours() !== 0 || now.getMinutes() > 5) { return; } const midnight = new Date(); midnight.setHours(0, 0, 0, 0); const exists = await hasRecentBackgroundTaskOfType("ntrp_refresh_all", midnight); if (exists) { return; } const adminUserId = await getAdminUserId(); if (!adminUserId) { return; } const taskId = nanoid(); await createBackgroundTask({ id: taskId, userId: adminUserId, type: "ntrp_refresh_all", title: "每日 NTRP 刷新", message: "系统已自动创建每日 NTRP 刷新任务", payload: { source: "scheduler", scheduledAt: now.toISOString() }, progress: 0, maxAttempts: 3, }); } function isPortAvailable(port: number): Promise { return new Promise(resolve => { const server = net.createServer(); server.listen(port, () => { server.close(() => resolve(true)); }); server.on("error", () => resolve(false)); }); } async function findAvailablePort(startPort: number = 3000): Promise { for (let port = startPort; port < startPort + 20; port++) { if (await isPortAvailable(port)) { return port; } } throw new Error(`No available port found starting from ${startPort}`); } async function startServer() { await seedTutorials(); await seedVisionReferenceImages(); await seedAchievementDefinitions(); await seedAppSettings(); const app = express(); const server = createServer(app); registerMediaProxy(app); // Configure body parser with larger size limit for file uploads app.use(express.json({ limit: "50mb" })); app.use(express.urlencoded({ limit: "50mb", extended: true })); app.use( "/uploads", express.static(path.resolve(process.env.LOCAL_STORAGE_DIR || "data/storage")) ); // OAuth callback under /api/oauth/callback registerOAuthRoutes(app); // tRPC API app.use( "/api/trpc", createExpressMiddleware({ router: appRouter, createContext, }) ); // development mode uses Vite, production mode uses static files if (process.env.NODE_ENV === "development") { const { setupVite } = await import("./vite"); await setupVite(app, server); } else { serveStatic(app); } const preferredPort = parseInt(process.env.PORT || "3000"); const strictPort = process.env.STRICT_PORT === "1"; const port = strictPort ? preferredPort : await findAvailablePort(preferredPort); if (port !== preferredPort) { console.log(`Port ${preferredPort} is busy, using port ${port} instead`); } server.listen(port, () => { console.log(`Server running on http://localhost:${port}/`); }); setInterval(() => { void scheduleDailyNtrpRefresh().catch((error) => { console.error("[scheduler] failed to schedule NTRP refresh", error); }); }, 60_000); } startServer().catch(console.error);