Merge branch 'refine/detail-optimization'
CI / Backend tests (push) Canceled after 0s
CI / Frontend build (push) Canceled after 0s

This commit is contained in:
yinjun.chen1
2026-09-29 10:28:55 +08:00
4 changed files with 56 additions and 23 deletions
+34 -22
View File
@@ -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
})
+21
View File
@@ -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/* 需鉴权 ——
+1 -1
View File
@@ -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 亦可写
Binary file not shown.