diff --git a/backend/scan-manager.js b/backend/scan-manager.js index 4f2c3ae..6460668 100644 --- a/backend/scan-manager.js +++ b/backend/scan-manager.js @@ -28,6 +28,9 @@ export function isDemoMode() { let rawCache = null let rawCacheAt = null +// 包列表构建缓存(rawCache/ignored/threshold 不变时复用,避免每次请求全量 map 7000+ 条) +let packagesCache = null + // —— 异步扫描状态(后台重扫时供前端轮询进度) —— export const scanState = { scanning: false, done: false, packages: 0, bytes: 0, tgzBytes: 0, diskBytes: null, @@ -227,6 +230,7 @@ export function hasScanCache() { export function invalidateScan() { rawCache = null rawCacheAt = null + packagesCache = null // 清空包列表构建缓存 clearPkgCache() // storage 目录切换 / 清理动作后废弃增量缓存,避免旧目录条目残留 } @@ -235,6 +239,7 @@ export function getScannedAt() { } // 构建包列表(统一状态计算 + 排序 + 忽略标记 + 来源筛选) +// 缓存全量列表:rawCache/ignored/threshold 不变时复用,避免每次请求全量 map 7000+ 条 export function buildPackages(keyword = '', origin = 'all') { const threshold = Number(getSetting('version_threshold', DEFAULT_VERSION_THRESHOLD)) const ignoredRows = getIgnoredRows() @@ -242,27 +247,34 @@ export function buildPackages(keyword = '', origin = 'all') { const ignoredAtMap = Object.fromEntries(ignoredRows.map(r => [r.pkg_name, (r.ignored_at || '').slice(0, 10)])) const raw = getRawPackages() || [] - let list = raw.map(p => { - let status - if (p.status === 'broken') status = 'broken' - else if (ignoredSet.has(p.name)) status = 'ignored' - else status = p.versions > threshold ? 'warn' : 'ok' - return { - name: p.name, - versions: p.versions, - size: p.size, - lastUpdated: p.lastUpdated, - visits: p.visits || 0, - distTags: p.distTags || {}, - status, - ignoredAt: ignoredAtMap[p.name] || null, - description: p.description || null, - homepage: p.homepage || null, - // 原版界面字段:来源(private/proxy)与最新版本;列表不携带 readme/versionsDetail(响应体小),详情接口按需返回 - origin: p.origin || null, - latest: p.latest || null, - } - }) + // 缓存 key:raw 长度 + ignored 内容 hash + threshold(任一变化即失效) + const ignoredHash = ignoredRows.map(r => `${r.pkg_name}:${r.ignored_at || ''}`).join('|') + const cacheKey = `${raw.length}_${ignoredHash}_${threshold}` + if (!packagesCache || packagesCache.key !== cacheKey) { + const list = raw.map(p => { + let status + if (p.status === 'broken') status = 'broken' + else if (ignoredSet.has(p.name)) status = 'ignored' + else status = p.versions > threshold ? 'warn' : 'ok' + return { + name: p.name, + versions: p.versions, + size: p.size, + lastUpdated: p.lastUpdated, + visits: p.visits || 0, + distTags: p.distTags || {}, + status, + ignoredAt: ignoredAtMap[p.name] || null, + description: p.description || null, + homepage: p.homepage || null, + // 原版界面字段:来源(private/proxy)与最新版本;列表不携带 readme/versionsDetail(响应体小),详情接口按需返回 + origin: p.origin || null, + latest: p.latest || null, + } + }) + packagesCache = { key: cacheKey, list } + } + let list = packagesCache.list // 检索:不区分大小写子串,匹配包名(版本号匹配由前端本地完成,避免列表携带明细) const kw = keyword.trim().toLowerCase() @@ -276,7 +288,7 @@ export function buildPackages(keyword = '', origin = 'all') { // 排序:预警置顶 → 正常 → 忽略 → 异常;同状态按版本数降序 const rank = { warn: 0, ok: 1, ignored: 2, broken: 3 } - list.sort((a, b) => { + list = [...list].sort((a, b) => { if (rank[a.status] !== rank[b.status]) return rank[a.status] - rank[b.status] return b.versions - a.versions }) diff --git a/backend/server.js b/backend/server.js index f1214c7..6bfc5e2 100644 --- a/backend/server.js +++ b/backend/server.js @@ -20,6 +20,7 @@ process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || '8' import express from 'express' import path from 'node:path' import fs from 'node:fs' +import zlib from 'node:zlib' import { fileURLToPath } from 'node:url' import { resolveStorageDir } from './scanner.js' import { isDemoMode, scanState, runScanAsync } from './scan-manager.js' @@ -40,6 +41,26 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) const app = express() app.use(express.json({ limit: '1mb' })) +// 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() +}) + const PORT = Number(process.env.PORT || 3000) // —— 路由装配:登录/登出/主题公开,其余 /api/* 需鉴权 —— diff --git a/docker-compose.yml b/docker-compose.yml index 11fd761..d357829 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ # storage 需读写挂载:清理版本要删除 .tgz 并回写 package.json 元数据(只读会报 EROFS); # 容器以 root 运行以写入 verdaccio 卷(其属主为 verdaccio 用户 uid 10001,非本容器 uid 10000) services: - verdaccio-manager: + verdaccio-mate: image: verdaccio-mate:latest # 已导入的 amd64 镜像(docker load 后直接可用) restart: always user: "0:0" # root:清理需写 verdaccio 卷;vcm-data 命名卷 root 亦可写 diff --git a/verdaccio-mate-amd64.tar b/verdaccio-mate-amd64.tar deleted file mode 100644 index e188ef6..0000000 Binary files a/verdaccio-mate-amd64.tar and /dev/null differ