/** * ═══════════════════════════════════════════════════════════ * 光湖智库节点 · Phase 1 API 服务 * ═══════════════════════════════════════════════════════════ * * 编号: ZY-SVR-006-API-P1 * 服务器: ZY-SVR-006 (43.153.203.105 · 新加坡) * 守护: 铸渊 · ICE-GL-ZY001 * 版权: 国作登字-2026-A-00037559 * * Phase 1 功能: * GET /api/health — 系统健康检查 (Nginx 存活探针) * GET /shield/status — 语言保护罩状态 + 封禁统计 * GET /shield/logs — 最近封禁日志 (面板读取) * * Phase 2 功能 (智库节点三大引擎): * /api/zhiku/download/* — 下载引擎 (书籍下载任务管理) * /api/zhiku/chapter/* — 智能分章 (TXT章节分割+目录索引) * /api/zhiku/reader/* — 在线阅读器 (书架/进度/偏好) * GET /api/zhiku/stats — 智库节点综合统计 * * Phase 3 功能 (团队书库+成员Agent): * /api/zhiku/library/* — 团队书库 (男频/女频分区+去重) * /api/zhiku/agent/* — 成员Agent (永久记忆+对话+笔记) * * ZY-PROJ-004 智能小说系统: * /api/novel/* — 小说/章节/人物卡/大纲/AI辅助 * * ═══════════════════════════════════════════════════════════ */ 'use strict'; const express = require('express'); const cors = require('cors'); const fs = require('fs'); const path = require('path'); // 加载环境变量 · 优先从上级目录 .env 读取(PM2 cwd=/opt/zhuyuan/novel-db) // 再 fallback 到 cwd/.env(兼容独立启动场景) const dotenvResult = require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); if (dotenvResult.error) { require('dotenv').config(); } const app = express(); const PORT = process.env.PORT || 4000; // ─── 信任本地 Nginx 反向代理 ─── app.set('trust proxy', 'loopback'); // ─── CORS: 仅允许 novel.guanghuyaoming.com + guanghuyaoming.com ─── const ALLOWED_ORIGINS = [ 'https://novel.guanghuyaoming.com', 'https://guanghuyaoming.com', 'https://guanghu.online', 'https://www.guanghu.online', 'http://localhost:3000', 'http://127.0.0.1:3000' ]; app.use(cors({ origin: function(origin, cb) { if (!origin || ALLOWED_ORIGINS.includes(origin)) return cb(null, true); cb(new Error('Not allowed by CORS')); }, credentials: true })); app.use(express.json()); // ─── 日志文件路径 ─── const BAN_LOG_PATH = process.env.BAN_LOG_PATH || '/var/log/novel-shield-bans.log'; const NGINX_LOG = process.env.NGINX_LOG || '/var/log/nginx/shield-access.log'; // ─── 工具函数 ─── function readFileLines(filePath, maxLines = 200) { try { const content = fs.readFileSync(filePath, 'utf8'); const lines = content.trim().split('\n').filter(Boolean); return lines.slice(-maxLines); } catch { return []; } } function parseBanLog(lines) { const today = new Date().toISOString().slice(0, 10); let todayBans = 0, totalBans = 0, totalUnbans = 0; const recentBans = []; for (const line of lines) { if (line.includes(' BAN ')) { totalBans++; if (line.startsWith(today)) todayBans++; if (recentBans.length < 50) { const parts = line.split(' '); recentBans.push({ ts: parts[0] || '', action: 'ban', ip: parts[2] || '', jail: (parts[3] || '').replace('jail=', ''), attempts:(parts[4] || '').replace('attempts=', '') }); } } else if (line.includes(' UNBAN ')) { totalUnbans++; } } return { todayBans, totalBans, activeBans: Math.max(0, totalBans - totalUnbans), recentBans: recentBans.reverse() }; } // ─── 检查 fail2ban 状态 ─── function getF2bStatus() { try { const { execSync } = require('child_process'); const out = execSync('fail2ban-client status 2>/dev/null', { timeout: 3000 }).toString(); const jails = (out.match(/Jail list:\s*(.+)/)?.[1] || '').split(',').map(s => s.trim()).filter(Boolean); return { running: true, jails }; } catch { return { running: false, jails: [] }; } } // ─── 检查 Nginx 状态 ─── function getNginxStatus() { try { const { execSync } = require('child_process'); execSync('systemctl is-active nginx', { timeout: 3000 }); return 'active'; } catch { return 'inactive'; } } /* ═══════════════════════════════════════════════════════════ * GET /api/health — 系统健康检查 * ═══════════════════════════════════════════════════════════ */ app.get('/api/health', (req, res) => { res.json({ status: 'ok', service: 'novel-db-api', phase: '1', server: 'ZY-SVR-006', timestamp: new Date().toISOString(), _sovereign: 'TCS-0002∞' }); }); /* ═══════════════════════════════════════════════════════════ * GET /shield/status — 语言保护罩状态 * ═══════════════════════════════════════════════════════════ */ app.get('/shield/status', (req, res) => { const lines = readFileLines(BAN_LOG_PATH, 500); const banStats = parseBanLog(lines); const f2b = getF2bStatus(); const nginxSt = getNginxStatus(); res.json({ shield: { l2_nginx: { status: nginxSt === 'active' ? 'active' : 'down', description: '入站蜜罐层' }, l3_fail2ban:{ status: f2b.running ? 'active' : 'down', jails: f2b.jails, description: '自动封禁层' }, l1_proxy: { status: 'pending', description: '出站代理池(Phase 2 部署)' } }, bans: { today: banStats.todayBans, total: banStats.totalBans, active: banStats.activeBans }, timestamp: new Date().toISOString() }); }); /* ═══════════════════════════════════════════════════════════ * GET /shield/logs — 最近封禁日志 * ═══════════════════════════════════════════════════════════ */ app.get('/shield/logs', (req, res) => { const limit = Math.min(parseInt(req.query.limit) || 50, 200); const lines = readFileLines(BAN_LOG_PATH, 500); const parsed = parseBanLog(lines); res.json({ logs: parsed.recentBans.slice(0, limit), total: parsed.totalBans, timestamp: new Date().toISOString() }); }); /* ═══════════════════════════════════════════════════════════ * Phase 2: 智库节点三大引擎路由 * ═══════════════════════════════════════════════════════════ */ const downloadRouter = require('./routes/download'); const chapterRouter = require('./routes/chapter'); const readerRouter = require('./routes/reader'); app.use('/api/zhiku/download', downloadRouter); app.use('/api/zhiku/chapter', chapterRouter); app.use('/api/zhiku/reader', readerRouter); /* ═══════════════════════════════════════════════════════════ * Phase 3: 团队书库 + 成员Agent * ═══════════════════════════════════════════════════════════ */ const libraryRouter = require('./routes/library'); const memberAgentRouter = require('./routes/member-agent'); app.use('/api/zhiku/library', libraryRouter); app.use('/api/zhiku/agent', memberAgentRouter); /* ═══════════════════════════════════════════════════════════ * Phase 4: 文件上传 + AI桥接 * ═══════════════════════════════════════════════════════════ */ const uploadRouter = require('./routes/upload'); app.use('/api/zhiku/upload', uploadRouter); // AI 健康检查 const aiBridge = require('./services/ai-bridge'); app.get('/api/zhiku/ai/health', (req, res) => { res.json({ error: false, data: aiBridge.healthCheck() }); }); /* ═══════════════════════════════════════════════════════════ * ZY-PROJ-004: 智能小说系统 * ═══════════════════════════════════════════════════════════ */ const novelRouter = require('./routes/novel'); app.use('/api/novel', novelRouter); // 智库综合统计 const downloadEngine = require('./services/download-engine'); const chapterEngine = require('./services/chapter-engine'); const readerEngine = require('./services/reader-engine'); const libraryEngine = require('./services/library-engine'); const agentEngine = require('./services/member-agent-engine'); const novelEngine = require('./services/novel-engine'); const uploadEngine = require('./services/upload-engine'); app.get('/api/zhiku/stats', (req, res) => { res.json({ service: 'zhiku-node-full', server: 'ZY-SVR-006', timestamp: new Date().toISOString(), download: downloadEngine.getStats(), chapter: chapterEngine.getStats(), reader: readerEngine.getStats(), library: libraryEngine.getStats(), agent: agentEngine.getStats(), novel: novelEngine.getStats(), upload: uploadEngine.getStats(), ai: aiBridge.healthCheck(), _sovereign: 'TCS-0002∞' }); }); /* ═══════════════════════════════════════════════════════════ * 404 fallback * ═══════════════════════════════════════════════════════════ */ app.use((req, res) => { res.status(404).json({ error: true, code: 'NOT_FOUND', message: 'API endpoint not found' }); }); /* ═══════════════════════════════════════════════════════════ * 启动 * ═══════════════════════════════════════════════════════════ */ // 绑定到 127.0.0.1 而非 0.0.0.0 — 确保 API 只通过 Nginx 反向代理对外暴露, // 永远不会直接暴露在公网上。外部访问路径: 公网 → Nginx(:443) → 127.0.0.1:4000 app.listen(PORT, '127.0.0.1', () => { console.log(`[ZY-SVR-006] novel-db-api Phase 1 · 运行在 127.0.0.1:${PORT}`); console.log(`[ZY-SVR-006] 语言保护罩 API 已激活 · TCS-0002∞`); });