import 'dotenv/config' import path from 'node:path' import fs from 'node:fs' import express from 'express' import cors from 'cors' import authRouter from './routes/auth.js' import dataRouter from './routes/data.js' import newsRouter from './routes/news.js' import alertsRouter from './routes/alerts.js' import articlesRouter from './routes/articles.js' import otaRouter from './routes/ota.js' import apizeroRouter from './routes/apizero.js' import monitorsRouter from './routes/monitors.js' import mihomeRouter from './routes/mihome.js' import checkinRouter from './routes/checkin.js' import checklistsRouter from './routes/checklists.js' import projectsRouter, { columnRouter } from './routes/projects.js' import tasksRouter from './routes/tasks.js' import powersyncRouter from './routes/powersync.js' const PORT = Number(process.env.PORT) || 3001 const DIST_DIR = path.join(process.cwd(), 'dist') const HAS_DIST = fs.existsSync(DIST_DIR) const app = express() app.use(cors()) app.use(express.json()) // 健康检查 app.get('/api/health', (_req, res) => res.json({ ok: true, ts: Date.now() })) // 业务路由 app.use('/api/auth', authRouter) // OTA 接口是公开的 (检查/下载更新不需要登录), 必须挂载在带 authRequired 的 router 之前, // 否则 /api/ota/* 请求会先进入 dataRouter 等并被 authRequired 以 401 拦截 app.use('/api', otaRouter) // PowerSync: token 签发 + 离线上行队列 (自带 authRequired, 同样必须挂在带 authRequired 的业务 router 之前) app.use('/api/powersync', powersyncRouter) app.use('/api', dataRouter) app.use('/api', newsRouter) app.use('/api', alertsRouter) app.use('/api', articlesRouter) app.use('/api', apizeroRouter) app.use('/api', monitorsRouter) app.use('/api/mihome', mihomeRouter) // V0.2 打卡 / 日历 / 清单 app.use('/api/checkin', checkinRouter) app.use('/api/checklists', checklistsRouter) // V0.2 任务模块: 项目 / 列 / 任务 app.use('/api/projects', projectsRouter) app.use('/api/columns', columnRouter) app.use('/api', tasksRouter) // 静态资源 (Vite build 产物) if (HAS_DIST) { app.use(express.static(DIST_DIR, { index: false, maxAge: '1h', etag: true })) // SPA fallback: 非 /api/* 的请求一律返回 index.html, 由前端路由处理 app.get(/^\/(?!api\/).*/, (_req, res, next) => { const indexHtml = path.join(DIST_DIR, 'index.html') if (!fs.existsSync(indexHtml)) return next() res.sendFile(indexHtml) }) } // 统一错误处理 app.use((err, _req, res, _next) => { console.error('[api error]', err) res.status(500).json({ error: err.message || '服务器内部错误' }) }) // 启动定时任务: 每天凌晨 3 点拉取新闻; 0.2.1 验证部署以 DISABLE_CRON=1 关闭 (避免重复外呼) if (process.env.DISABLE_CRON !== '1') { import('./cron.js').catch((e) => console.error('[cron] 加载失败:', e.message)) // 米家环境监控定时调度 (独立注册, 不与 cron.js 绑定, 见 server/mihome/scheduler.js) import('./mihome/scheduler.js').catch((e) => console.error('[cron] 米家调度注册失败:', e.message)) } app.listen(PORT, () => { console.log(`[iboard-api] listening on http://127.0.0.1:${PORT}`) console.log(`[iboard-api] static dist: ${HAS_DIST ? DIST_DIR : '(not built, API only)'}`) })