/** * 系统信息与版本更新检查。 * 挂载点:/api/system */ const express = require('express'); const updater = require('../lib/updater'); const branding = require('../lib/branding'); const auth = require('../lib/auth'); const webUpdate = require('../lib/webUpdate'); const fs = require('fs'); const path = require('path'); const router = express.Router(); function adminOnly(req, res, next) { if (!req.user) return res.status(401).json({ error: '请先登录' }); if (req.user.role !== 'admin') return res.status(403).json({ error: '仅系统管理员可执行系统更新' }); next(); } router.get('/update/status', adminOnly, async (req, res) => { const status = await webUpdate.readStatus(); let log = ''; if (/^[a-f0-9]{16}$/.test(status.id || '')) { const data = path.resolve(process.env.DATA_DIR || path.join(__dirname, '../data')); try { const file = path.join(data, 'system-update', status.id, 'update.log'); const fd = fs.openSync(file, 'r'); try { const size = fs.fstatSync(fd).size; const buffer = Buffer.alloc(Math.min(size, 12000)); fs.readSync(fd, buffer, 0, buffer.length, Math.max(0, size - buffer.length)); log = buffer.toString('utf8'); } finally { fs.closeSync(fd); } } catch { /* 任务尚未开始写日志 */ } } res.setHeader('Cache-Control', 'no-store'); res.json({ ...status, log, capability: webUpdate.capability() }); }); router.post('/update', adminOnly, async (req, res) => { try { auth.verifyCurrentPassword(req, req.body?.current_password); const status = await webUpdate.start(req); auth.audit({ username: req.user.username, action: 'system_update', detail: `网页更新至 ${status.version}`, req }); res.status(202).json(status); } catch (error) { res.status(error.statusCode || 500).json({ error: error.message }); } }); // 简易缓存:GitHub 未认证请求每小时只有 60 次配额,连续点「检查更新」不该把额度刷完。 const CACHE_TTL_MS = 30 * 1000; let cache = { key: '', at: 0, payload: null }; /** * GET /api/system/info * 当前版本与运行环境。轻量、不发外部请求,设置页打开时就能调。 */ router.get('/info', auth.requireAuth, (req, res) => { try { res.json(updater.systemInfo()); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * GET /api/system/check-update?repo=owner/repo&force=1 * 对比远端仓库版本与本机版本。repo 省略时用默认仓库(Drsakura/happy-export)。 */ router.get('/check-update', auth.requireAuth, async (req, res) => { try { const repo = updater.normalizeRepo(process.env.HAPPY_UPDATE_REPO) || updater.DEFAULT_REPO; const force = String(req.query.force || '') === '1'; if (!force && cache.payload && cache.key === repo && Date.now() - cache.at < CACHE_TTL_MS) { return res.json({ ...cache.payload, cached: true }); } const result = await updater.checkUpdate(repo); cache = { key: repo, at: Date.now(), payload: result }; res.json({ ...result, cached: false }); } catch (error) { // checkUpdate 内部已兜底,走到这里说明是意料之外的问题 res.status(500).json({ ok: false, error: error.message }); } }); /** * GET /api/system/branding * 品牌与组织名。公开接口 —— 登录页在拿到会话之前就得知道系统叫什么。 * 只回显示用的名字,不含任何业务数据。 */ router.get('/branding', (req, res) => { try { res.json(branding.getBranding()); } catch (error) { res.status(500).json({ error: error.message }); } }); module.exports = router;