v0.2.7: 修复Redis连接 + 启动管理后台

- 修复Redis认证 (配置密码)
- 启动Python管理后台 (端口9531, 15个功能开关)
- 统一版本号 0.2.7
- 更新docker-compose.yml (镜像版本/Redis URL/Admin服务)
This commit is contained in:
2026-05-17 02:22:18 +08:00
commit 83cbfaf03f
164 changed files with 25195 additions and 0 deletions

View File

@@ -0,0 +1,327 @@
import Database from 'better-sqlite3';
import path from 'path';
import bcrypt from 'bcryptjs';
import config from '../config';
import { formatLocalDateTime } from '../utils/time';
let db: Database.Database | null = null;
export function getDb(): Database.Database {
if (db) return db;
const dbDir = path.dirname(config.dbPath);
const fs = require('fs');
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
db = new Database(config.dbPath);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
// Performance indexes (IF NOT EXISTS ensures idempotent)
db.exec(`
CREATE INDEX IF NOT EXISTS idx_cc_type_active ON cloud_configs(cloud_type, is_active);
CREATE INDEX IF NOT EXISTS idx_cc_uid ON cloud_configs(cookie_uid);
CREATE INDEX IF NOT EXISTS idx_cc_verification ON cloud_configs(verification_status);
`);
runMigrations(db);
seedAdmin(db);
return db;
}
function runMigrations(db: Database.Database): void {
db.exec(`
CREATE TABLE IF NOT EXISTS admins (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
last_login TEXT
);
CREATE TABLE IF NOT EXISTS cloud_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cloud_type TEXT NOT NULL,
cookie TEXT,
nickname TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
storage_used TEXT,
storage_total TEXT,
checkin_status TEXT NOT NULL DEFAULT 'none',
last_checkin_at TEXT,
checkin_message TEXT,
consecutive_failures INTEGER DEFAULT 0,
last_used_at TEXT,
total_saves INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS promotions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
image_url TEXT,
link_url TEXT,
position TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
active INTEGER NOT NULL DEFAULT 1,
click_count INTEGER NOT NULL DEFAULT 0,
start_time TEXT,
end_time TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS save_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_type TEXT,
source_title TEXT,
source_url TEXT,
target_cloud TEXT,
share_url TEXT,
share_pwd TEXT,
file_size TEXT,
file_count INTEGER DEFAULT 0,
duration_ms INTEGER DEFAULT 0,
status TEXT NOT NULL DEFAULT '',
error_message TEXT,
ip_address TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS search_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT,
intent TEXT,
result_count INTEGER DEFAULT 0,
ip_address TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS hot_keywords (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT UNIQUE NOT NULL,
search_count INTEGER NOT NULL DEFAULT 1,
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS system_configs (
key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT '',
description TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS content_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT UNIQUE NOT NULL,
title TEXT,
description TEXT,
tags TEXT,
cover TEXT,
source TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
`);
seedSystemConfigs(db);
migrateSaveRecords(db);
migrateContentCache(db);
migrateCloudConfigs(db);
cleanupOldSaveRecords(db);
}
/** 迁移: 给已有 save_records 表补充新列 */
function migrateSaveRecords(db: Database.Database): void {
const newCols: { col: string; def: string }[] = [
{ col: 'share_pwd', def: 'TEXT' },
{ col: 'file_count', def: 'INTEGER DEFAULT 0' },
{ col: 'folder_count', def: 'INTEGER DEFAULT 0' },
{ col: 'duration_ms', def: 'INTEGER DEFAULT 0' },
{ col: 'status', def: "TEXT NOT NULL DEFAULT ''" },
{ col: 'error_message', def: 'TEXT' },
{ col: 'folder_name', def: 'TEXT' },
{ col: 'request_url', def: 'TEXT' },
{ col: 'ip_location', def: 'TEXT' },
{ col: 'original_folder_name', def: 'TEXT' },
];
for (const { col, def } of newCols) {
try {
db.exec(`ALTER TABLE save_records ADD COLUMN ${col} ${def}`);
} catch {
// Column already exists — ignore
}
}
}
/** 迁移: 给 content_cache 表加 douban_url 列 */
function migrateContentCache(db: Database.Database): void {
const columns: { col: string; def: string }[] = [
{ col: 'douban_url', def: 'TEXT' },
{ col: 'rating', def: 'TEXT' },
{ col: 'rating_count', def: 'TEXT' },
{ col: 'year', def: 'TEXT' },
{ col: 'genres', def: 'TEXT' },
{ col: 'directors', def: 'TEXT' },
{ col: 'actors', def: 'TEXT' },
{ col: 'region', def: 'TEXT' },
{ col: 'duration', def: 'TEXT' },
];
for (const { col, def } of columns) {
try {
db.exec(`ALTER TABLE content_cache ADD COLUMN ${col} ${def}`);
} catch {
// Column already exists — ignore
}
}
// 修复旧记录source 为 NULL 但实际有 TMDB 数据的,标记为 tmdb
db.exec(`UPDATE content_cache SET source = 'tmdb' WHERE source IS NULL AND title IS NOT NULL AND title != ''`);
}
/** 迁移: 给 cloud_configs 表去UNIQUE约束 + 加签到/轮训字段 */
function migrateCloudConfigs(db: Database.Database): void {
// 加新列
const newCols: { col: string; def: string }[] = [
{ col: 'checkin_status', def: "TEXT NOT NULL DEFAULT 'none'" },
{ col: 'last_checkin_at', def: 'TEXT' },
{ col: 'checkin_message', def: 'TEXT' },
{ col: 'consecutive_failures', def: 'INTEGER DEFAULT 0' },
{ col: 'last_used_at', def: 'TEXT' },
{ col: 'total_saves', def: 'INTEGER DEFAULT 0' },
];
for (const { col, def } of newCols) {
try { db.exec(`ALTER TABLE cloud_configs ADD COLUMN ${col} ${def}`); } catch {}
}
// 检查旧表是否有 UNIQUE 约束,有则重建表
const row = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='cloud_configs'`).get() as any;
if (row && row.sql && row.sql.includes('cloud_type TEXT UNIQUE')) {
db.exec(`
CREATE TABLE IF NOT EXISTS cloud_configs_v2 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cloud_type TEXT NOT NULL,
cookie TEXT,
nickname TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
storage_used TEXT,
storage_total TEXT,
checkin_status TEXT NOT NULL DEFAULT 'none',
last_checkin_at TEXT,
checkin_message TEXT,
consecutive_failures INTEGER DEFAULT 0,
last_used_at TEXT,
total_saves INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
INSERT INTO cloud_configs_v2 (id, cloud_type, cookie, nickname, is_active, storage_used, storage_total, checkin_status, last_checkin_at, checkin_message, consecutive_failures, last_used_at, total_saves, created_at, updated_at)
SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total, COALESCE(checkin_status,'none'), last_checkin_at, checkin_message, COALESCE(consecutive_failures,0), last_used_at, COALESCE(total_saves,0), created_at, updated_at FROM cloud_configs;
DROP TABLE cloud_configs;
ALTER TABLE cloud_configs_v2 RENAME TO cloud_configs;
`);
console.log('[DB] cloud_configs migration: UNIQUE constraint removed, new fields added');
}
// Migration 2: Add verification_status column
const row2 = db.prepare("SELECT sql FROM sqlite_master WHERE name='cloud_configs' AND sql LIKE '%verification_status%'").get();
if (!row2) {
db.exec("ALTER TABLE cloud_configs ADD COLUMN verification_status TEXT DEFAULT NULL");
console.log('[DB] cloud_configs migration: verification_status column added');
}
// Migration 3: Add cookie_uid column
const hasCookieUid = db.prepare("SELECT sql FROM sqlite_master WHERE name='cloud_configs' AND sql LIKE '%cookie_uid%'").get();
if (!hasCookieUid) {
db.exec("ALTER TABLE cloud_configs ADD COLUMN cookie_uid TEXT DEFAULT NULL");
console.log('[DB] cloud_configs migration: cookie_uid column added');
}
// Migration 4: Add promotion_account column
const hasPromotionAccount = db.prepare("SELECT sql FROM sqlite_master WHERE name='cloud_configs' AND sql LIKE '%promotion_account%'").get();
if (!hasPromotionAccount) {
db.exec("ALTER TABLE cloud_configs ADD COLUMN promotion_account TEXT DEFAULT NULL");
console.log('[DB] cloud_configs migration: promotion_account column added');
}
}
function seedAdmin(db: Database.Database): void {
const existing = db.prepare('SELECT id FROM admins WHERE username = ?').get(config.adminUsername);
if (existing) return;
const salt = bcrypt.genSaltSync(10);
const hash = bcrypt.hashSync(config.adminPassword, salt);
db.prepare(
'INSERT INTO admins (username, password_hash) VALUES (?, ?)'
).run(config.adminUsername, hash);
console.log(`[DB] Admin user "${config.adminUsername}" created`);
}
function seedSystemConfigs(db: Database.Database): void {
const defaults: { key: string; value: string; description: string }[] = [
{ key: 'pansou_url', value: config.pansouUrl, description: 'PanSou 搜索引擎服务地址' },
{ key: 'video_parser_url', value: config.videoParserUrl, description: '视频解析服务地址' },
{ key: 'validation_concurrency', value: String(config.validation.concurrency), description: '链接验证并发数' },
{ key: 'validation_timeout', value: String(config.validation.timeout), description: '链接验证超时(ms)' },
{ key: 'validation_cache_ttl_valid', value: String(config.validation.cacheTtlValid), description: '有效链接缓存时间(s)' },
{ key: 'validation_cache_ttl_invalid', value: String(config.validation.cacheTtlInvalid), description: '无效链接缓存时间(s)' },
{ key: 'search_proxy_enabled', value: 'false', description: '搜索代理开关true/false' },
{ key: 'search_proxy_url', value: '', description: '搜索代理地址 (如 http://127.0.0.1:7890)' },
{ key: 'search_strategy', value: 'wait_all', description: '搜索结果展示方式: wait_all=等待全部后展示, stream_channel=频道逐步展示' },
{ key: 'link_validation_enabled', value: 'true', description: '资源链接有效性检测开关true/false' },
{ key: 'cloud_enabled_quark', value: 'true', description: '夸克网盘' },
{ key: 'cloud_enabled_baidu', value: 'true', description: '百度网盘' },
{ key: 'cloud_enabled_aliyun', value: 'true', description: '阿里云盘' },
{ key: 'cloud_enabled_115', value: 'true', description: '115 网盘' },
{ key: 'cloud_enabled_tianyi', value: 'true', description: '天翼云盘' },
{ key: 'cloud_enabled_123pan', value: 'true', description: '123 云盘' },
{ key: 'cloud_enabled_uc', value: 'true', description: 'UC 网盘' },
{ key: 'cloud_enabled_xunlei', value: 'true', description: '迅雷网盘' },
{ key: 'cloud_enabled_pikpak', value: 'true', description: 'PikPak 网盘' },
{ key: 'cloud_enabled_magnet', value: 'true', description: '磁力链接' },
{ key: 'cloud_enabled_ed2k', value: 'true', description: '电驴链接' },
{ key: 'cloud_enabled_others', value: 'false', description: '其他类型(默认关闭)' },
{ key: 'search_result_limit', value: '10', description: '每类网盘最多展示的有效结果数' },
{ key: 'search_fallback_image', value: '', description: '无图资源的兜底封面图 URL留空使用渐变色' },
{ key: 'site_logo', value: '', description: '网站 LOGO 图片 URL留空使用默认图标/文字)' },
{ key: 'site_name', value: 'CloudSearch', description: '网站名称(显示在首页标题/页脚)' },
{ key: 'site_disclaimer', value: '本站为非盈利性个人站点所有资源仅供学习、研究使用版权归原作者所有。请于下载后24小时内删除切勿用于商业或非法用途。若侵犯了您的权益请联系我们邮箱3337598077@qq.com我们将及时处理。', description: '网站底部免责声明' },
{ key: 'site_marquee', value: '📢 欢迎使用CloudSearch所有资源仅供学习交流请于下载后24小时内删除', description: '搜索栏下方滚动通知文字(从右往左滚动显示)' },
{ key: 'tmdb_api_token', value: '', description: 'TMDB API 读取令牌(用于增强豆瓣内容信息)' },
{ key: 'ip_geo_api_url', value: 'https://cn.apihz.cn/api/ip/chaapi.php?id=10014356&key=ca7ccb3b9ca044dd993c8604bc9afd93&ip={ip}&td=0', description: 'IP 归属地查询接口({ip} 会被替换为实际IP' },
{ key: 'ip_geo_api_key', value: '', description: 'IP 归属地备用 API Key留空使用默认' },
{ key: 'title_filter_rules', value: '', description: '搜索结果标题过滤规则(一行一条:纯文本直接移除 / 正则用/包围/' },
{ key: 'timezone', value: 'Asia/Shanghai', description: '系统时区(如 Asia/Shanghai、America/New_York、UTC' },
{ key: 'redis_url', value: 'redis://redis:6379', description: 'Redis 连接地址(用于缓存优化)' },
{ key: 'pansou_auth_token', value: '', description: 'PanSou API 认证令牌(用于私有搜索服务)' },
{ key: 'pansou_web_enabled', value: 'false', description: '启用 PanSou Web 端访问(在 /pansou 路径提供 PanSou 搜索引擎管理界面)' },
{ key: 'cleanup_enabled', value: 'true', description: '启用自动清理(每天检查一次,移入回收站+清空日志+清空回收站)' },
{ key: 'cleanup_file_retention_days', value: '7', description: '云盘文件保留天数(超过此天数的日期文件夹将被移入回收站)' },
{ key: 'cleanup_log_retention_days', value: '30', description: '转存日志保留天数' },
{ key: 'cleanup_empty_trash', value: 'true', description: '清理时是否清空回收站(永久删除释放空间)' },
{ key: 'cleanup_space_threshold_enabled', value: 'false', description: '启用空间阈值自动清理已用空间超过XX%时按比例删除最旧的转存文件)' },
{ key: 'cleanup_space_threshold_percent', value: '90', description: '空间使用阈值百分比(超过此值时触发强制清理)' },
{ key: 'cleanup_space_threshold_delete_percent', value: '10', description: '触发阈值清理时释放总空间的百分比(如 10 表示累计删除最旧文件直到达到总空间的 10%6TB 总空间 → 释放 ~600GB' },
{ key: 'save_reuse_enabled', value: 'true', description: '启用分享链接复用(相同原始链接不再重复转存,直接复用之前的分享链接)' },
{ key: 'cleanup_last_run', value: '', description: '上次自动清理时间' },
{ key: 'cleanup_last_stats', value: '', description: '上次清理结果统计JSON' },
];
const insert = db.prepare(
'INSERT OR IGNORE INTO system_configs (key, value, description) VALUES (?, ?, ?)'
);
for (const entry of defaults) {
insert.run(entry.key, entry.value, entry.description);
}
}
/** 清理 60 天前的转存记录 */
function cleanupOldSaveRecords(db: Database.Database): void {
const cutoff = formatLocalDateTime(new Date(Date.now() - 60 * 24 * 60 * 60 * 1000));
const deleted = db.prepare('DELETE FROM save_records WHERE created_at < ?').run(cutoff);
console.log(`[DB] Cleaned up ${deleted.changes} save records older than 60 days (before ${cutoff})`);
}
export default getDb;

View File

@@ -0,0 +1,306 @@
import Database from 'better-sqlite3';
import path from 'path';
import bcrypt from 'bcryptjs';
import config from '../config';
import { formatLocalDateTime } from '../utils/time';
let db: Database.Database | null = null;
export function getDb(): Database.Database {
if (db) return db;
const dbDir = path.dirname(config.dbPath);
const fs = require('fs');
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
db = new Database(config.dbPath);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
runMigrations(db);
seedAdmin(db);
return db;
}
function runMigrations(db: Database.Database): void {
db.exec(`
CREATE TABLE IF NOT EXISTS admins (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
last_login TEXT
);
CREATE TABLE IF NOT EXISTS cloud_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cloud_type TEXT NOT NULL,
cookie TEXT,
nickname TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
storage_used TEXT,
storage_total TEXT,
checkin_status TEXT NOT NULL DEFAULT 'none',
last_checkin_at TEXT,
checkin_message TEXT,
consecutive_failures INTEGER DEFAULT 0,
last_used_at TEXT,
total_saves INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS promotions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
image_url TEXT,
link_url TEXT,
position TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
active INTEGER NOT NULL DEFAULT 1,
click_count INTEGER NOT NULL DEFAULT 0,
start_time TEXT,
end_time TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS save_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_type TEXT,
source_title TEXT,
source_url TEXT,
target_cloud TEXT,
share_url TEXT,
share_pwd TEXT,
file_size TEXT,
file_count INTEGER DEFAULT 0,
duration_ms INTEGER DEFAULT 0,
status TEXT NOT NULL DEFAULT '',
error_message TEXT,
ip_address TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS search_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT,
intent TEXT,
result_count INTEGER DEFAULT 0,
ip_address TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS hot_keywords (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT UNIQUE NOT NULL,
search_count INTEGER NOT NULL DEFAULT 1,
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS system_configs (
key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT '',
description TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS content_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT UNIQUE NOT NULL,
title TEXT,
description TEXT,
tags TEXT,
cover TEXT,
source TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
`);
seedSystemConfigs(db);
migrateSaveRecords(db);
migrateContentCache(db);
migrateCloudConfigs(db);
cleanupOldSaveRecords(db);
}
/** 迁移: 给已有 save_records 表补充新列 */
function migrateSaveRecords(db: Database.Database): void {
const newCols: { col: string; def: string }[] = [
{ col: 'share_pwd', def: 'TEXT' },
{ col: 'file_count', def: 'INTEGER DEFAULT 0' },
{ col: 'folder_count', def: 'INTEGER DEFAULT 0' },
{ col: 'duration_ms', def: 'INTEGER DEFAULT 0' },
{ col: 'status', def: "TEXT NOT NULL DEFAULT ''" },
{ col: 'error_message', def: 'TEXT' },
{ col: 'folder_name', def: 'TEXT' },
{ col: 'request_url', def: 'TEXT' },
{ col: 'ip_location', def: 'TEXT' },
{ col: 'original_folder_name', def: 'TEXT' },
];
for (const { col, def } of newCols) {
try {
db.exec(`ALTER TABLE save_records ADD COLUMN ${col} ${def}`);
} catch {
// Column already exists — ignore
}
}
}
/** 迁移: 给 content_cache 表加 douban_url 列 */
function migrateContentCache(db: Database.Database): void {
const columns: { col: string; def: string }[] = [
{ col: 'douban_url', def: 'TEXT' },
{ col: 'rating', def: 'TEXT' },
{ col: 'rating_count', def: 'TEXT' },
{ col: 'year', def: 'TEXT' },
{ col: 'genres', def: 'TEXT' },
{ col: 'directors', def: 'TEXT' },
{ col: 'actors', def: 'TEXT' },
{ col: 'region', def: 'TEXT' },
{ col: 'duration', def: 'TEXT' },
];
for (const { col, def } of columns) {
try {
db.exec(`ALTER TABLE content_cache ADD COLUMN ${col} ${def}`);
} catch {
// Column already exists — ignore
}
}
// 修复旧记录source 为 NULL 但实际有 TMDB 数据的,标记为 tmdb
db.exec(`UPDATE content_cache SET source = 'tmdb' WHERE source IS NULL AND title IS NOT NULL AND title != ''`);
}
/** 迁移: 给 cloud_configs 表去UNIQUE约束 + 加签到/轮训字段 */
function migrateCloudConfigs(db: Database.Database): void {
// 加新列
const newCols: { col: string; def: string }[] = [
{ col: 'checkin_status', def: "TEXT NOT NULL DEFAULT 'none'" },
{ col: 'last_checkin_at', def: 'TEXT' },
{ col: 'checkin_message', def: 'TEXT' },
{ col: 'consecutive_failures', def: 'INTEGER DEFAULT 0' },
{ col: 'last_used_at', def: 'TEXT' },
{ col: 'total_saves', def: 'INTEGER DEFAULT 0' },
];
for (const { col, def } of newCols) {
try { db.exec(`ALTER TABLE cloud_configs ADD COLUMN ${col} ${def}`); } catch {}
}
// 检查旧表是否有 UNIQUE 约束,有则重建表
const row = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='cloud_configs'`).get() as any;
if (row && row.sql && row.sql.includes('cloud_type TEXT UNIQUE')) {
db.exec(`
CREATE TABLE IF NOT EXISTS cloud_configs_v2 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cloud_type TEXT NOT NULL,
cookie TEXT,
nickname TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
storage_used TEXT,
storage_total TEXT,
checkin_status TEXT NOT NULL DEFAULT 'none',
last_checkin_at TEXT,
checkin_message TEXT,
consecutive_failures INTEGER DEFAULT 0,
last_used_at TEXT,
total_saves INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
INSERT INTO cloud_configs_v2 (id, cloud_type, cookie, nickname, is_active, storage_used, storage_total, checkin_status, last_checkin_at, checkin_message, consecutive_failures, last_used_at, total_saves, created_at, updated_at)
SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total, COALESCE(checkin_status,'none'), last_checkin_at, checkin_message, COALESCE(consecutive_failures,0), last_used_at, COALESCE(total_saves,0), created_at, updated_at FROM cloud_configs;
DROP TABLE cloud_configs;
ALTER TABLE cloud_configs_v2 RENAME TO cloud_configs;
`);
console.log('[DB] cloud_configs migration: UNIQUE constraint removed, new fields added');
}
// Migration 2: Add verification_status column
const row2 = db.prepare("SELECT sql FROM sqlite_master WHERE name='cloud_configs' AND sql LIKE '%verification_status%'").get();
if (!row2) {
db.exec("ALTER TABLE cloud_configs ADD COLUMN verification_status TEXT DEFAULT NULL");
console.log('[DB] cloud_configs migration: verification_status column added');
}
}
function seedAdmin(db: Database.Database): void {
const existing = db.prepare('SELECT id FROM admins WHERE username = ?').get(config.adminUsername);
if (existing) return;
const salt = bcrypt.genSaltSync(10);
const hash = bcrypt.hashSync(config.adminPassword, salt);
db.prepare(
'INSERT INTO admins (username, password_hash) VALUES (?, ?)'
).run(config.adminUsername, hash);
console.log(`[DB] Admin user "${config.adminUsername}" created`);
}
function seedSystemConfigs(db: Database.Database): void {
const defaults: { key: string; value: string; description: string }[] = [
{ key: 'pansou_url', value: config.pansouUrl, description: 'PanSou 搜索引擎服务地址' },
{ key: 'video_parser_url', value: config.videoParserUrl, description: '视频解析服务地址' },
{ key: 'validation_concurrency', value: String(config.validation.concurrency), description: '链接验证并发数' },
{ key: 'validation_timeout', value: String(config.validation.timeout), description: '链接验证超时(ms)' },
{ key: 'validation_cache_ttl_valid', value: String(config.validation.cacheTtlValid), description: '有效链接缓存时间(s)' },
{ key: 'validation_cache_ttl_invalid', value: String(config.validation.cacheTtlInvalid), description: '无效链接缓存时间(s)' },
{ key: 'search_proxy_enabled', value: 'false', description: '搜索代理开关true/false' },
{ key: 'search_proxy_url', value: '', description: '搜索代理地址 (如 http://127.0.0.1:7890)' },
{ key: 'search_strategy', value: 'wait_all', description: '搜索结果展示方式: wait_all=等待全部后展示, stream_channel=频道逐步展示' },
{ key: 'link_validation_enabled', value: 'true', description: '资源链接有效性检测开关true/false' },
{ key: 'cloud_enabled_quark', value: 'true', description: '夸克网盘' },
{ key: 'cloud_enabled_baidu', value: 'true', description: '百度网盘' },
{ key: 'cloud_enabled_aliyun', value: 'true', description: '阿里云盘' },
{ key: 'cloud_enabled_115', value: 'true', description: '115 网盘' },
{ key: 'cloud_enabled_tianyi', value: 'true', description: '天翼云盘' },
{ key: 'cloud_enabled_123pan', value: 'true', description: '123 云盘' },
{ key: 'cloud_enabled_uc', value: 'true', description: 'UC 网盘' },
{ key: 'cloud_enabled_xunlei', value: 'true', description: '迅雷网盘' },
{ key: 'cloud_enabled_pikpak', value: 'true', description: 'PikPak 网盘' },
{ key: 'cloud_enabled_magnet', value: 'true', description: '磁力链接' },
{ key: 'cloud_enabled_ed2k', value: 'true', description: '电驴链接' },
{ key: 'cloud_enabled_others', value: 'false', description: '其他类型(默认关闭)' },
{ key: 'search_result_limit', value: '10', description: '每类网盘最多展示的有效结果数' },
{ key: 'search_fallback_image', value: '', description: '无图资源的兜底封面图 URL留空使用渐变色' },
{ key: 'site_logo', value: '', description: '网站 LOGO 图片 URL留空使用默认图标/文字)' },
{ key: 'site_name', value: 'CloudSearch', description: '网站名称(显示在首页标题/页脚)' },
{ key: 'site_disclaimer', value: '本站为非盈利性个人站点所有资源仅供学习、研究使用版权归原作者所有。请于下载后24小时内删除切勿用于商业或非法用途。若侵犯了您的权益请联系我们邮箱3337598077@qq.com我们将及时处理。', description: '网站底部免责声明' },
{ key: 'site_marquee', value: '📢 欢迎使用CloudSearch所有资源仅供学习交流请于下载后24小时内删除', description: '搜索栏下方滚动通知文字(从右往左滚动显示)' },
{ key: 'tmdb_api_token', value: '', description: 'TMDB API 读取令牌(用于增强豆瓣内容信息)' },
{ key: 'ip_geo_api_url', value: 'https://cn.apihz.cn/api/ip/chaapi.php?id=10014356&key=ca7ccb3b9ca044dd993c8604bc9afd93&ip={ip}&td=0', description: 'IP 归属地查询接口({ip} 会被替换为实际IP' },
{ key: 'ip_geo_api_key', value: '', description: 'IP 归属地备用 API Key留空使用默认' },
{ key: 'title_filter_rules', value: '', description: '搜索结果标题过滤规则(一行一条:纯文本直接移除 / 正则用/包围/' },
{ key: 'timezone', value: 'Asia/Shanghai', description: '系统时区(如 Asia/Shanghai、America/New_York、UTC' },
{ key: 'redis_url', value: 'redis://redis:6379', description: 'Redis 连接地址(用于缓存优化)' },
{ key: 'pansou_auth_token', value: '', description: 'PanSou API 认证令牌(用于私有搜索服务)' },
{ key: 'pansou_web_enabled', value: 'false', description: '启用 PanSou Web 端访问(在 /pansou 路径提供 PanSou 搜索引擎管理界面)' },
{ key: 'cleanup_enabled', value: 'true', description: '启用自动清理(每天检查一次,移入回收站+清空日志+清空回收站)' },
{ key: 'cleanup_file_retention_days', value: '7', description: '云盘文件保留天数(超过此天数的日期文件夹将被移入回收站)' },
{ key: 'cleanup_log_retention_days', value: '30', description: '转存日志保留天数' },
{ key: 'cleanup_empty_trash', value: 'true', description: '清理时是否清空回收站(永久删除释放空间)' },
{ key: 'cleanup_space_threshold_enabled', value: 'false', description: '启用空间阈值自动清理已用空间超过XX%时按比例删除最旧的转存文件)' },
{ key: 'cleanup_space_threshold_percent', value: '90', description: '空间使用阈值百分比(超过此值时触发强制清理)' },
{ key: 'cleanup_space_threshold_delete_percent', value: '10', description: '触发阈值清理时释放总空间的百分比(如 10 表示累计删除最旧文件直到达到总空间的 10%6TB 总空间 → 释放 ~600GB' },
{ key: 'save_reuse_enabled', value: 'true', description: '启用分享链接复用(相同原始链接不再重复转存,直接复用之前的分享链接)' },
{ key: 'cleanup_last_run', value: '', description: '上次自动清理时间' },
{ key: 'cleanup_last_stats', value: '', description: '上次清理结果统计JSON' },
];
const insert = db.prepare(
'INSERT OR IGNORE INTO system_configs (key, value, description) VALUES (?, ?, ?)'
);
for (const entry of defaults) {
insert.run(entry.key, entry.value, entry.description);
}
}
/** 清理 60 天前的转存记录 */
function cleanupOldSaveRecords(db: Database.Database): void {
const cutoff = formatLocalDateTime(new Date(Date.now() - 60 * 24 * 60 * 60 * 1000));
const deleted = db.prepare('DELETE FROM save_records WHERE created_at < ?').run(cutoff);
console.log(`[DB] Cleaned up ${deleted.changes} save records older than 60 days (before ${cutoff})`);
}
export default getDb;