272 lines
12 KiB
JavaScript
272 lines
12 KiB
JavaScript
/**
|
||
* ═══════════════════════════════════════════════════════════
|
||
* 光湖智库节点 · 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∞`);
|
||
});
|