2026-05-10 13:12:44 +08:00

278 lines
8.9 KiB
JavaScript

/**
* ═══════════════════════════════════════════════════════════
* 铸渊哨兵 · 主体 · ZY-SENTINEL-001
* ═══════════════════════════════════════════════════════════
*
* 铸渊分身 · 永久驻守智库节点
* 拥有永久记忆,自动监测书源变化,自动修复,联动镜鉴更新
*
* 核心能力:
* 1. 书源健康监测 — 定时检测所有书源API是否可达
* 2. 永久记忆 — 记住所有历史事件,跨重启持久化
* 3. 自动修复 — 检测到书源变化时自动切换备用端点/策略
* 4. 联动镜鉴 — 将发现推送给 mirror-agent 生成工单
* 5. 知识积累 — 记住每次修复经验,逐步提升自愈能力
*
* 调度:
* 生产环境每4小时自动扫描一次
* 支持手动触发扫描
*
* API 路由:
* GET /api/sentinel/status — 哨兵状态
* GET /api/sentinel/memory — 永久记忆摘要
* GET /api/sentinel/incidents — 事件记录
* GET /api/sentinel/repairs — 修复记录
* POST /api/sentinel/scan — 手动触发扫描
* POST /api/sentinel/config — 更新书源配置
*
* 守护: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
* ═══════════════════════════════════════════════════════════
*/
'use strict';
const path = require('path');
const SentinelMemory = require('./memory');
const SourceMonitor = require('./source-monitor');
// ─── 默认配置 ───
const SCAN_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4小时
const STARTUP_DELAY_MS = 30 * 1000; // 启动后30秒延迟再首次扫描
class ZhuyuanSentinel {
/**
* @param {object} opts
* @param {string} opts.dataDir - 数据目录
* @param {object} opts.builtinSource - builtin-source 模块
* @param {object} opts.mirrorAgent - mirror-agent 模块(可选)
*/
constructor(opts) {
this.dataDir = opts.dataDir || path.join(__dirname, '..', '..', 'data');
this.builtinSource = opts.builtinSource || null;
this.mirrorAgent = opts.mirrorAgent || null;
this.memory = new SentinelMemory(this.dataDir);
this.monitor = null;
this.scheduler = null;
this.schedulerActive = false;
this._startTime = Date.now();
}
/**
* 初始化哨兵(加载记忆、创建监测器)
*/
init() {
// 加载永久记忆
this.memory.init();
// 构造适配器映射
const adapters = {};
if (this.builtinSource) {
if (this.builtinSource.fanqieDirect) {
adapters.fanqieDirect = this.builtinSource.fanqieDirect;
}
if (this.builtinSource.qimaoDirect) {
adapters.qimaoDirect = this.builtinSource.qimaoDirect;
}
if (this.builtinSource.biqugeDirect) {
adapters.biqugeDirect = this.builtinSource.biqugeDirect;
}
}
// 创建监测器
this.monitor = new SourceMonitor(this.memory, adapters, this.mirrorAgent);
// 从记忆中恢复书源配置到适配器
this._restoreConfigFromMemory(adapters);
console.log('[ZY-SENTINEL] ⚔️ 铸渊哨兵已初始化');
return this;
}
/**
* 启动调度器(生产环境自动定时扫描)
*/
startScheduler() {
if (this.schedulerActive) return;
this.schedulerActive = true;
// 启动后延迟首次扫描(等服务完全就绪)
setTimeout(() => {
if (!this.schedulerActive) return;
console.log('[ZY-SENTINEL] 🔍 首次启动扫描...');
this.monitor.runFullScan().catch(err => {
console.error('[ZY-SENTINEL] 首次扫描失败:', err.message);
});
}, STARTUP_DELAY_MS);
// 定时扫描
this.scheduler = setInterval(() => {
if (!this.schedulerActive) return;
console.log('[ZY-SENTINEL] ⏰ 定时扫描...');
this.monitor.runFullScan().catch(err => {
console.error('[ZY-SENTINEL] 定时扫描失败:', err.message);
});
}, SCAN_INTERVAL_MS);
// 防止 setInterval 阻止 Node 进程退出
if (this.scheduler && this.scheduler.unref) {
this.scheduler.unref();
}
console.log(`[ZY-SENTINEL] 📡 调度器已启动 (间隔: ${SCAN_INTERVAL_MS / 3600000}h)`);
}
/**
* 停止调度器
*/
stopScheduler() {
this.schedulerActive = false;
if (this.scheduler) {
clearInterval(this.scheduler);
this.scheduler = null;
}
console.log('[ZY-SENTINEL] 调度器已停止');
}
/**
* 从记忆恢复配置到适配器
*/
_restoreConfigFromMemory(adapters) {
const mem = this.memory.get();
if (!mem || !mem.sources) return;
// 恢复七猫配置
if (adapters.qimaoDirect && adapters.qimaoDirect.updateConfig) {
const qimaoMem = mem.sources['qimao-direct'];
if (qimaoMem && qimaoMem.active_host) {
adapters.qimaoDirect.updateConfig({
primaryHost: qimaoMem.active_host,
backupHosts: qimaoMem.backup_hosts || [],
searchStrategy: qimaoMem.search_strategy || 'api',
catalogStrategy: qimaoMem.catalog_strategy || 'scrape',
chapterStrategy: qimaoMem.chapter_strategy || 'scrape'
});
console.log(`[ZY-SENTINEL] 📎 七猫配置已从记忆恢复 (host: ${qimaoMem.active_host})`);
}
}
}
/**
* 手动触发扫描
*/
async triggerScan() {
if (!this.monitor) {
return { status: 'error', message: '监测器未初始化' };
}
return this.monitor.runFullScan();
}
/**
* 更新书源配置(添加备用主机等)
*/
updateSourceConfig(sourceId, config) {
if (!sourceId || !config) return false;
// 更新记忆
this.memory.updateSource(sourceId, config);
// 如果有备用主机,同步到适配器
if (config.backup_hosts && this.builtinSource) {
const adapter = sourceId === 'qimao-direct' ? this.builtinSource.qimaoDirect : null;
if (adapter && adapter.updateConfig) {
adapter.updateConfig({ backupHosts: config.backup_hosts });
}
}
this.memory.save();
return true;
}
/**
* 获取哨兵状态
*/
getStatus() {
const mem = this.memory.get();
return {
agent_id: 'ZY-SENTINEL-001',
agent_name: '铸渊哨兵',
status: 'active',
scheduler_active: this.schedulerActive,
scanning: this.monitor ? this.monitor.isScanning() : false,
uptime_ms: Date.now() - this._startTime,
sources: Object.entries(mem.sources).map(([id, src]) => ({
id,
status: src.status,
active_host: src.active_host,
last_check: src.last_check,
consecutive_failures: src.consecutive_failures
})),
stats: mem.stats,
last_save: mem.last_save
};
}
/**
* 注册 API 路由
* @param {Express} app - Express 实例
* @param {function} verifyToken - 认证中间件
*/
registerRoutes(app, verifyToken) {
// GET /api/sentinel/status — 哨兵状态
app.get('/api/sentinel/status', verifyToken, (req, res) => {
res.json(this.getStatus());
});
// GET /api/sentinel/memory — 永久记忆摘要
app.get('/api/sentinel/memory', verifyToken, (req, res) => {
res.json(this.memory.getSummary());
});
// GET /api/sentinel/incidents — 事件记录
app.get('/api/sentinel/incidents', verifyToken, (req, res) => {
const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
const mem = this.memory.get();
res.json({
total: mem.incidents.length,
incidents: mem.incidents.slice(0, limit)
});
});
// GET /api/sentinel/repairs — 修复记录
app.get('/api/sentinel/repairs', verifyToken, (req, res) => {
const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
const mem = this.memory.get();
res.json({
total: mem.repairs.length,
repairs: mem.repairs.slice(0, limit)
});
});
// POST /api/sentinel/scan — 手动触发扫描
app.post('/api/sentinel/scan', verifyToken, async (req, res) => {
try {
const report = await this.triggerScan();
res.json({ status: 'ok', report });
} catch (err) {
res.status(500).json({ error: true, code: 'SCAN_ERROR', message: err.message });
}
});
// POST /api/sentinel/config — 更新书源配置
app.post('/api/sentinel/config', verifyToken, (req, res) => {
const { source_id, config } = req.body || {};
if (!source_id || !config) {
return res.status(400).json({ error: true, code: 'MISSING_PARAMS', message: '需要 source_id 和 config' });
}
const ok = this.updateSourceConfig(source_id, config);
res.json({ status: ok ? 'ok' : 'error', source_id });
});
console.log('[ZY-SENTINEL] 📡 API 路由已注册 (/api/sentinel/*)');
}
}
module.exports = ZhuyuanSentinel;