2026-09-19 15:00:04 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* SQLite 数据层(§4.1)
|
|
|
|
|
|
* 表:settings / ignored_packages / logs
|
|
|
|
|
|
*/
|
|
|
|
|
|
import Database from 'better-sqlite3'
|
|
|
|
|
|
import fs from 'node:fs'
|
|
|
|
|
|
import path from 'node:path'
|
|
|
|
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
|
|
|
|
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
|
|
|
|
// 数据目录:环境变量 DATA_DIR 可覆盖(Docker 挂载 manager-data)
|
|
|
|
|
|
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data')
|
|
|
|
|
|
fs.mkdirSync(DATA_DIR, { recursive: true })
|
|
|
|
|
|
|
|
|
|
|
|
const db = new Database(path.join(DATA_DIR, 'manager.db'))
|
|
|
|
|
|
db.pragma('journal_mode = WAL')
|
|
|
|
|
|
|
|
|
|
|
|
db.exec(`
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
|
|
|
|
key TEXT PRIMARY KEY,
|
|
|
|
|
|
value TEXT
|
|
|
|
|
|
);
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS ignored_packages (
|
|
|
|
|
|
pkg_name TEXT PRIMARY KEY,
|
|
|
|
|
|
ignored_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
|
);
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS logs (
|
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
2026-09-19 21:58:44 +08:00
|
|
|
|
action TEXT DEFAULT 'clean',
|
2026-09-19 15:00:04 +08:00
|
|
|
|
pkg_name TEXT,
|
|
|
|
|
|
deleted_versions TEXT,
|
|
|
|
|
|
deleted_count INTEGER,
|
|
|
|
|
|
freed_space TEXT,
|
|
|
|
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
|
);
|
2026-09-21 15:47:22 +08:00
|
|
|
|
CREATE TABLE IF NOT EXISTS pkglist (
|
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
|
pkg_name TEXT NOT NULL UNIQUE,
|
|
|
|
|
|
version TEXT,
|
|
|
|
|
|
category TEXT DEFAULT '',
|
|
|
|
|
|
status TEXT DEFAULT 'pending',
|
|
|
|
|
|
auto INTEGER DEFAULT 0,
|
|
|
|
|
|
created_at DATETIME
|
|
|
|
|
|
);
|
2026-09-19 15:00:04 +08:00
|
|
|
|
`)
|
|
|
|
|
|
|
2026-09-21 09:56:11 +08:00
|
|
|
|
// 北京时间(Asia/Shanghai 固定 UTC+8,无夏令时):SQLite DEFAULT CURRENT_TIMESTAMP 是 UTC,
|
|
|
|
|
|
// 与前端按本地时间展示相差 8 小时,故写入与迁移统一显式使用北京时间墙上时间字符串
|
|
|
|
|
|
// (格式与前端 slice 兼容:YYYY-MM-DD HH:MM:SS,不带时区后缀,避免被二次误解)
|
|
|
|
|
|
const pad2 = n => String(n).padStart(2, '0')
|
|
|
|
|
|
export function nowIso() {
|
|
|
|
|
|
const d = new Date(Date.now() + 8 * 3600_000)
|
|
|
|
|
|
return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function utcToShanghai(iso) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const ts = Date.parse(iso)
|
|
|
|
|
|
if (!Number.isFinite(ts)) return null
|
|
|
|
|
|
const d = new Date(ts + 8 * 3600_000)
|
|
|
|
|
|
return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}`
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 21:58:44 +08:00
|
|
|
|
// 旧库迁移:早期 logs 表无 action 列,运行时补齐(忽略/取消忽略等操作依赖该列区分类型)
|
|
|
|
|
|
const logCols = db.prepare('PRAGMA table_info(logs)').all().map(c => c.name)
|
|
|
|
|
|
if (!logCols.includes('action')) {
|
|
|
|
|
|
db.exec("ALTER TABLE logs ADD COLUMN action TEXT DEFAULT 'clean'")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 22:45:12 +08:00
|
|
|
|
// 旧库迁移 v2:pkglist 增加 cached 列——标记「写入清单时 storage 已缓存该包」(done 的来源之一,
|
|
|
|
|
|
// 与「通过清单拉取完成」区分,前端据此展示「已在缓存 / 已完成」两种 done)
|
|
|
|
|
|
const pkglistCols = db.prepare('PRAGMA table_info(pkglist)').all().map(c => c.name)
|
|
|
|
|
|
if (!pkglistCols.includes('cached')) {
|
|
|
|
|
|
db.exec("ALTER TABLE pkglist ADD COLUMN cached INTEGER DEFAULT 0")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 09:56:11 +08:00
|
|
|
|
// 旧库迁移 v1:历史日志/忽略记录的时间戳是 UTC,统一转成北京时间;settings 标志位防重复执行
|
|
|
|
|
|
function migrateTimezone() {
|
|
|
|
|
|
if (getSetting('timezone_migrated_v1')) return
|
|
|
|
|
|
db.transaction(() => {
|
|
|
|
|
|
for (const { id, created_at: t } of db.prepare('SELECT id, created_at FROM logs').all()) {
|
|
|
|
|
|
if (t) {
|
|
|
|
|
|
const local = utcToShanghai(t)
|
|
|
|
|
|
if (local) db.prepare('UPDATE logs SET created_at = ? WHERE id = ?').run(local, id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const { pkg_name: n, ignored_at: t } of db.prepare('SELECT pkg_name, ignored_at FROM ignored_packages').all()) {
|
|
|
|
|
|
if (t) {
|
|
|
|
|
|
const local = utcToShanghai(t)
|
|
|
|
|
|
if (local) db.prepare('UPDATE ignored_packages SET ignored_at = ? WHERE pkg_name = ?').run(local, n)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
setSetting('timezone_migrated_v1', '1')
|
|
|
|
|
|
})()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 15:00:04 +08:00
|
|
|
|
// —— settings ——
|
|
|
|
|
|
export function getSetting(key, def = null) {
|
|
|
|
|
|
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key)
|
|
|
|
|
|
return row ? row.value : def
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function setSetting(key, value) {
|
|
|
|
|
|
db.prepare(
|
|
|
|
|
|
'INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
|
|
|
|
|
|
).run(key, String(value))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// —— ignored ——
|
|
|
|
|
|
export function getIgnored() {
|
|
|
|
|
|
return db.prepare('SELECT pkg_name FROM ignored_packages ORDER BY ignored_at DESC').all().map(r => r.pkg_name)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function getIgnoredRows() {
|
|
|
|
|
|
return db.prepare('SELECT pkg_name, ignored_at FROM ignored_packages ORDER BY ignored_at DESC').all()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function isIgnored(pkgName) {
|
|
|
|
|
|
return !!db.prepare('SELECT 1 FROM ignored_packages WHERE pkg_name = ?').get(pkgName)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function addIgnored(pkgName) {
|
|
|
|
|
|
db.prepare(
|
2026-09-21 09:56:11 +08:00
|
|
|
|
'INSERT INTO ignored_packages (pkg_name, ignored_at) VALUES (?, ?) ON CONFLICT(pkg_name) DO NOTHING',
|
|
|
|
|
|
).run(pkgName, nowIso())
|
2026-09-19 15:00:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function removeIgnored(pkgName) {
|
|
|
|
|
|
db.prepare('DELETE FROM ignored_packages WHERE pkg_name = ?').run(pkgName)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// —— logs ——
|
2026-09-19 21:58:44 +08:00
|
|
|
|
export function addLog({ action = 'clean', pkgName, deletedVersions = null, deletedCount = 0, freedSpace = null }) {
|
2026-09-19 15:00:04 +08:00
|
|
|
|
const info = db.prepare(
|
2026-09-21 09:56:11 +08:00
|
|
|
|
'INSERT INTO logs (action, pkg_name, deleted_versions, deleted_count, freed_space, created_at) VALUES (?, ?, ?, ?, ?, ?)',
|
|
|
|
|
|
).run(action, pkgName, deletedVersions, deletedCount, freedSpace, nowIso())
|
2026-09-19 15:00:04 +08:00
|
|
|
|
return info.lastInsertRowid
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function getLogs(limit = 50) {
|
|
|
|
|
|
return db.prepare(
|
|
|
|
|
|
'SELECT * FROM logs ORDER BY id DESC LIMIT ?',
|
|
|
|
|
|
).all(limit)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 09:56:11 +08:00
|
|
|
|
// 模块加载时执行时间迁移(幂等)
|
|
|
|
|
|
migrateTimezone()
|
|
|
|
|
|
|
2026-09-19 15:00:04 +08:00
|
|
|
|
export default db
|