Files
usa/server/index.js
2026-03-06 11:41:09 +08:00

122 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const http = require('http')
const path = require('path')
const fs = require('fs')
const express = require('express')
const cors = require('cors')
const { WebSocketServer } = require('ws')
const db = require('./db')
const routes = require('./routes')
const { getSituation } = require('./situationData')
const app = express()
const PORT = process.env.API_PORT || 3001
// 爬虫通知用的共享密钥API_CRAWLER_TOKEN仅在服务端与爬虫进程间传递
const CRAWLER_TOKEN = process.env.API_CRAWLER_TOKEN || ''
app.set('trust proxy', 1)
app.use(cors())
app.use(express.json())
app.use('/api', routes)
app.get('/api/health', (_, res) => res.json({ ok: true }))
app.post('/api/crawler/notify', (req, res) => {
// 若配置了 API_CRAWLER_TOKEN则要求爬虫携带 X-Crawler-Token 头
if (CRAWLER_TOKEN) {
const token = req.headers['x-crawler-token']
if (typeof token !== 'string' || token !== CRAWLER_TOKEN) {
return res.status(401).json({ error: 'unauthorized' })
}
}
notifyCrawlerUpdate()
res.json({ ok: true })
})
// 生产环境:提供前端静态文件(含修订页 /edit依赖 SPA fallback
const distPath = path.join(__dirname, '..', 'dist')
if (fs.existsSync(distPath)) {
app.use(express.static(distPath))
// 非 API/WS 的请求一律返回 index.html由前端路由处理 /、/edit、/db 等
app.get('*', (req, res, next) => {
if (!req.path.startsWith('/api') && req.path !== '/ws') {
res.sendFile(path.join(distPath, 'index.html'))
} else next()
})
} else {
console.warn('[server] dist 目录不存在,前端页面(含 /edit 修订页)不可用。请在项目根目录执行 npm run build 后再启动。')
}
const server = http.createServer(app)
const { getStats } = require('./stats')
const wss = new WebSocketServer({ server, path: '/ws' })
wss.on('connection', (ws) => {
ws.send(JSON.stringify({ type: 'situation', data: getSituation(), stats: getStats() }))
})
// 版本含 situation、situation_update、key_location 基地态势,任一变化都会触发广播,保证爬虫 AI 更新基地后前端实时刷新
function getBroadcastVersion() {
try {
const meta = db.prepare('SELECT updated_at FROM situation WHERE id = 1').get()
const row = db.prepare('SELECT COUNT(*) as c FROM situation_update').get()
const usAtt = db.prepare("SELECT COUNT(*) as c FROM key_location WHERE side='us' AND status='attacked'").get()
const irAtt = db.prepare("SELECT COUNT(*) as c FROM key_location WHERE side='iran' AND status='attacked'").get()
return `${meta?.updated_at || ''}_${row?.c ?? 0}_b${usAtt?.c ?? 0}_${irAtt?.c ?? 0}`
} catch (_) {
return ''
}
}
let lastBroadcastVersion = null
function broadcastSituation(force = false) {
if (!force && wss.clients.size === 0) return
const version = getBroadcastVersion()
if (!force && version === lastBroadcastVersion) return
try {
const data = JSON.stringify({ type: 'situation', data: getSituation(), stats: getStats() })
wss.clients.forEach((c) => {
if (c.readyState === 1) c.send(data)
})
lastBroadcastVersion = version
} catch (_) {}
}
app.set('broadcastSituation', () => broadcastSituation(true))
if (typeof routes.setBroadcastSituation === 'function') {
routes.setBroadcastSituation(() => broadcastSituation(true))
}
const BROADCAST_INTERVAL_MS = Math.max(0, parseInt(process.env.BROADCAST_INTERVAL_MS, 10) || 30000)
if (BROADCAST_INTERVAL_MS > 0) {
setInterval(() => broadcastSituation(false), BROADCAST_INTERVAL_MS)
}
// 供爬虫调用:先从磁盘重载 DB纳入爬虫写入再更新 situation.updated_at 并立即广播;前端据此实时更新基地态势等
function notifyCrawlerUpdate() {
try {
const db = require('./db')
db.reloadFromFile()
db.prepare("INSERT OR REPLACE INTO situation (id, data, updated_at) VALUES (1, '{}', ?)").run(new Date().toISOString())
broadcastSituation(true) // 强制推送,确保基地数/被袭数等随 AI 清洗实时更新
const n = db.prepare('SELECT COUNT(*) as c FROM situation_update').get().c
const usB = db.prepare("SELECT COUNT(*) as c FROM key_location WHERE side='us'").get().c
const irB = db.prepare("SELECT COUNT(*) as c FROM key_location WHERE side='iran'").get().c
console.log('[crawler/notify] DB 已重载并广播situation_update:', n, '基地 us/iran:', usB, irB)
} catch (e) {
console.error('[crawler/notify]', e?.message || e)
}
}
db.initDb().then(() => {
server.listen(PORT, () => {
console.log(`API + WebSocket running at http://localhost:${PORT}`)
console.log(`Swagger docs at http://localhost:${PORT}/api-docs`)
})
}).catch((err) => {
console.error('DB init failed:', err)
process.exit(1)
})