Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

272 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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