2026-09-19 15:00:04 +08:00
|
|
|
|
/**
|
2026-09-22 14:29:18 +08:00
|
|
|
|
* Verdaccio 缓存可视化管理面板 — 后端服务入口
|
2026-09-19 15:00:04 +08:00
|
|
|
|
* Express + better-sqlite3,§4.2 REST API
|
|
|
|
|
|
*
|
|
|
|
|
|
* 环境变量:
|
|
|
|
|
|
* VERDACCIO_STORAGE Verdaccio 缓存目录(优先于 UI 设置)
|
|
|
|
|
|
* PORT 监听端口(默认 3000)
|
|
|
|
|
|
* DATA_DIR SQLite 数据目录(默认 backend/data)
|
2026-09-19 22:49:32 +08:00
|
|
|
|
* VCM_USERNAME 面板登录账号(默认 admin)
|
2026-09-21 16:39:20 +08:00
|
|
|
|
* VCM_PASSWORD 面板登录密码(默认 666666,Docker 部署务必通过 compose 覆盖)
|
2026-09-21 00:55:46 +08:00
|
|
|
|
* UV_THREADPOOL_SIZE libuv IO 线程池大小(默认 4 → 8:扫描/stat/readFile 并发更高,NAS 四核安全)
|
2026-09-22 14:29:18 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 模块划分:
|
|
|
|
|
|
* - scan-manager.js 扫描状态机与业务编排(缓存/进度/状态构建)
|
|
|
|
|
|
* - schedules.js 每日扫描 + 每周抓榜定时任务
|
|
|
|
|
|
* - routes/* REST 路由(按领域拆分)
|
2026-09-19 15:00:04 +08:00
|
|
|
|
*/
|
2026-09-21 00:55:46 +08:00
|
|
|
|
// 必须在 libuv 线程池首次使用前设置:扫描大量目录/文件时默认 4 线程是主要瓶颈
|
|
|
|
|
|
process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || '8'
|
2026-09-19 15:00:04 +08:00
|
|
|
|
import express from 'express'
|
|
|
|
|
|
import path from 'node:path'
|
|
|
|
|
|
import fs from 'node:fs'
|
2026-09-28 11:04:21 +08:00
|
|
|
|
import zlib from 'node:zlib'
|
2026-09-19 15:00:04 +08:00
|
|
|
|
import { fileURLToPath } from 'node:url'
|
2026-09-22 14:29:18 +08:00
|
|
|
|
import { resolveStorageDir } from './scanner.js'
|
2026-09-22 17:35:35 +08:00
|
|
|
|
import { isDemoMode, scanState, runScanAsync } from './scan-manager.js'
|
2026-09-22 14:29:18 +08:00
|
|
|
|
import { scheduleDailyScan, scheduleWeeklyTopList } from './schedules.js'
|
|
|
|
|
|
import { authRouter, authMiddleware } from './routes/auth.js'
|
|
|
|
|
|
import { thresholdsRouter } from './routes/thresholds.js'
|
|
|
|
|
|
import { ignoredRouter } from './routes/ignored.js'
|
|
|
|
|
|
import { packagesRouter } from './routes/packages.js'
|
|
|
|
|
|
import { syncRouter } from './routes/sync.js'
|
|
|
|
|
|
import { toplistRouter } from './routes/toplist.js'
|
|
|
|
|
|
import { cleanRouter } from './routes/clean.js'
|
|
|
|
|
|
import { logsRouter } from './routes/logs.js'
|
2026-09-25 22:49:25 +08:00
|
|
|
|
import { themeRouter } from './routes/theme.js'
|
2026-09-26 01:17:04 +08:00
|
|
|
|
import { verdaccioRouter } from './routes/verdaccio.js'
|
2026-09-22 14:29:18 +08:00
|
|
|
|
import { apiNotFound, errorHandler } from './http.js'
|
2026-09-19 15:00:04 +08:00
|
|
|
|
|
|
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
|
|
|
|
const app = express()
|
|
|
|
|
|
app.use(express.json({ limit: '1mb' }))
|
|
|
|
|
|
|
2026-09-28 11:04:21 +08:00
|
|
|
|
// gzip 压缩:大响应(如 /api/packages 7000+ 条)减少传输体积 60-80%,Node 内置 zlib 无需新依赖
|
|
|
|
|
|
app.use((req, res, next) => {
|
|
|
|
|
|
const accept = req.headers['accept-encoding'] || ''
|
|
|
|
|
|
if (!accept.includes('gzip')) return next()
|
|
|
|
|
|
const originalJson = res.json.bind(res)
|
|
|
|
|
|
res.json = (data) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const json = JSON.stringify(data)
|
|
|
|
|
|
if (json.length < 1024) return originalJson(data) // 小响应不压缩,避免开销
|
|
|
|
|
|
const compressed = zlib.gzipSync(json)
|
|
|
|
|
|
res.setHeader('Content-Encoding', 'gzip')
|
|
|
|
|
|
res.setHeader('Content-Type', 'application/json')
|
|
|
|
|
|
res.send(compressed)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
originalJson(data)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
next()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-09-19 15:00:04 +08:00
|
|
|
|
const PORT = Number(process.env.PORT || 3000)
|
|
|
|
|
|
|
2026-09-25 22:49:25 +08:00
|
|
|
|
// —— 路由装配:登录/登出/主题公开,其余 /api/* 需鉴权 ——
|
|
|
|
|
|
app.use('/api', themeRouter) // 公开:登录页加载时也需要主题
|
2026-09-22 14:29:18 +08:00
|
|
|
|
app.use('/api', authRouter)
|
|
|
|
|
|
app.use('/api', authMiddleware)
|
2026-09-26 01:17:04 +08:00
|
|
|
|
app.use('/api', verdaccioRouter) // Verdaccio Web API 透传(读取类;面板侧需登录)
|
2026-09-22 14:29:18 +08:00
|
|
|
|
app.use('/api', thresholdsRouter)
|
|
|
|
|
|
app.use('/api', ignoredRouter)
|
|
|
|
|
|
app.use('/api', packagesRouter)
|
|
|
|
|
|
app.use('/api', syncRouter)
|
|
|
|
|
|
app.use('/api', toplistRouter)
|
|
|
|
|
|
app.use('/api', cleanRouter)
|
|
|
|
|
|
app.use('/api', logsRouter)
|
|
|
|
|
|
// —— /api 未匹配任何路由 → JSON 404(Express 默认对非 GET 返回 HTML) ——
|
|
|
|
|
|
app.use('/api', apiNotFound)
|
2026-09-19 15:00:04 +08:00
|
|
|
|
|
|
|
|
|
|
// —— 生产:托管前端 dist ——
|
|
|
|
|
|
const distDir = path.join(__dirname, '..', 'frontend', 'dist')
|
|
|
|
|
|
if (fs.existsSync(distDir)) {
|
|
|
|
|
|
app.use(express.static(distDir))
|
|
|
|
|
|
app.get('*', (req, res) => {
|
|
|
|
|
|
if (req.path.startsWith('/api/')) return res.status(404).json({ ok: false, message: 'not found' })
|
|
|
|
|
|
res.sendFile(path.join(distDir, 'index.html'))
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 14:29:18 +08:00
|
|
|
|
// —— 统一错误处理:Express4 同步 throw 会跳到这里,返回 JSON 而非 HTML 500 ——
|
|
|
|
|
|
app.use(errorHandler)
|
|
|
|
|
|
|
2026-09-19 15:00:04 +08:00
|
|
|
|
app.listen(PORT, () => {
|
|
|
|
|
|
const storage = resolveStorageDir()
|
|
|
|
|
|
const mode = isDemoMode() ? 'demo' : 'storage'
|
2026-09-21 09:59:45 +08:00
|
|
|
|
if (!process.env.VCM_PASSWORD) {
|
|
|
|
|
|
console.warn(
|
2026-09-21 16:39:20 +08:00
|
|
|
|
'[auth] ⚠ 未设置 VCM_PASSWORD 环境变量,正在使用默认口令 666666。' +
|
2026-09-21 09:59:45 +08:00
|
|
|
|
'生产部署请务必通过 docker-compose 的环境变量覆盖,否则面板可被任意登录!',
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-09-19 15:00:04 +08:00
|
|
|
|
console.log(`[verdaccio-manager] listening on :${PORT} (mode=${mode})`)
|
|
|
|
|
|
if (mode === 'storage') console.log(`[verdaccio-manager] storage: ${storage}`)
|
|
|
|
|
|
else console.log('[verdaccio-manager] 未配置 VERDACCIO_STORAGE,运行于演示模式(设置 VCM_DEMO=0 + 真实目录可关闭)')
|
2026-09-22 17:35:35 +08:00
|
|
|
|
// 启动即扫描:容器部署后无需打开前端即可预置缓存(demo 模式跳过;
|
|
|
|
|
|
// 启动时 rawCache 恒为 null,直接触发即可,无需 invalidateScan)
|
|
|
|
|
|
if (mode === 'storage' && !scanState.scanning) {
|
|
|
|
|
|
scanState._triggerSource = 'startup'
|
2026-09-27 11:52:28 +08:00
|
|
|
|
scanState.startupScanning = true // 部署后启动扫描:供前端显示首次扫描遮罩,扫描结束即清零
|
2026-09-22 17:35:35 +08:00
|
|
|
|
runScanAsync()
|
|
|
|
|
|
console.log(`[auto-scan] 启动后立即扫描缓存目录:${storage}`)
|
|
|
|
|
|
}
|
2026-09-19 21:07:27 +08:00
|
|
|
|
scheduleDailyScan()
|
2026-09-22 10:50:43 +08:00
|
|
|
|
scheduleWeeklyTopList()
|
2026-09-22 14:29:18 +08:00
|
|
|
|
})
|