300 lines
10 KiB
JavaScript
300 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* ═══════════════════════════════════════════════════════════
|
||
* 光湖智库节点 · v2.1 · 书岚人格体 + 守护Agent
|
||
* ═══════════════════════════════════════════════════════════
|
||
*
|
||
* 项目编号: ZY-PROJ-006
|
||
* 服务器: ZY-SVR-006 (43.153.203.105 · 新加坡)
|
||
* 域名: guanghu.online
|
||
* 端口: 3006 (绑定 127.0.0.1 · Nginx 反代)
|
||
* 守护: 铸渊 · ICE-GL-ZY001
|
||
* 版权: 国作登字-2026-A-00037559
|
||
*
|
||
* v2.1 新增:
|
||
* 书岚(AG-SL-WEB-001)— 四层人格提示词系统
|
||
* 守护Agent(AG-SL-GUARDIAN-001)— 活的提示词注入代理
|
||
* 聊天工具技能包(AG-SL-TOOLKIT-001)— 视觉化排版工具
|
||
* 搜索结果带「在线阅读」「下载」操作按钮
|
||
* [REMEMBER:tag] 偏好记忆标记
|
||
* GET /api/agent/status — 书岚系统状态
|
||
*
|
||
* v2.0:
|
||
* POST /api/auth/send-code — 发送QQ邮箱验证码
|
||
* POST /api/auth/verify — 验证码验证 → 签发用户Token
|
||
* GET /api/auth/session — 获取当前会话
|
||
* POST /api/auth/logout — 退出登录
|
||
* GET /api/search — 真实搜索(番茄+七猫数据源+本地库)
|
||
* POST /api/download/start — 真实下载任务(数据源→COS桶)
|
||
* GET /api/download/status/:taskId — 下载任务状态
|
||
* POST /api/agent/chat — 书岚对话(LLM真实调用)
|
||
* GET /api/agent/memory — Agent记忆查看
|
||
*
|
||
* 保留旧接口兼容:
|
||
* GET /api/health, POST /api/checkout, POST /api/return
|
||
* GET /api/book/:id, GET /api/download/:id, GET /api/read/:id
|
||
* /api/mirror/*
|
||
*
|
||
* 架构法理: 5TH-LE-LK-ZHIKU-ARCH-002
|
||
* ═══════════════════════════════════════════════════════════
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const express = require('express');
|
||
const cors = require('cors');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const crypto = require('crypto');
|
||
const http = require('http');
|
||
const https = require('https');
|
||
const jwt = require('jsonwebtoken');
|
||
const rateLimit = require('express-rate-limit');
|
||
const os = require('os');
|
||
const nodemailer = require('nodemailer');
|
||
|
||
// ─── 加载环境变量 ───
|
||
require('dotenv').config({ path: path.join(__dirname, '.env') });
|
||
|
||
// ─── 七层镜防 + 镜面 Agent ───
|
||
const { registerShield, registerShieldRoutes } = require('./mirror-shield');
|
||
const mirrorAgent = require('./mirror-agent');
|
||
const { getEnabledSources } = require('./mirror-agent/config');
|
||
|
||
// ─── 书岚 Agent 系统 ───
|
||
const shulanAgent = require('./shulan-agent');
|
||
|
||
// ─── 内置数据源直连(FQWeb/SwiftCat不可用时的fallback · 可选模块) ───
|
||
let builtinSource = null;
|
||
try {
|
||
builtinSource = require('./builtin-source');
|
||
} catch (err) {
|
||
console.warn(`[ZY-SVR-006] ⚠️ builtin-source 模块未找到 (${err.message})。内置直连搜索/下载将不可用,但核心认证/Agent功能不受影响。`);
|
||
}
|
||
|
||
// ─── 铸渊哨兵 · 自动运维Agent(永久记忆 + 书源监测 + 自动修复) ───
|
||
let sentinel = null;
|
||
try {
|
||
const ZhuyuanSentinel = require('./zhuyuan-sentinel');
|
||
sentinel = new ZhuyuanSentinel({
|
||
dataDir: process.env.ZY_ZHIKU_DATA_DIR || path.join(__dirname, '..', 'data'),
|
||
builtinSource,
|
||
mirrorAgent
|
||
});
|
||
sentinel.init();
|
||
} catch (err) {
|
||
console.warn(`[ZY-SVR-006] ⚠️ 铸渊哨兵加载失败 (${err.message})。自动运维功能不可用。`);
|
||
}
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT || 3006;
|
||
const JWT_SECRET = process.env.ZY_ZHIKU_JWT_SECRET;
|
||
if (!JWT_SECRET) {
|
||
console.error('[ZY-SVR-006] ⚠️ 严重: ZY_ZHIKU_JWT_SECRET 未配置。生产环境必须设置此变量。');
|
||
if (process.env.NODE_ENV === 'production') {
|
||
console.error('[ZY-SVR-006] 生产环境缺少 JWT 密钥,拒绝启动。');
|
||
process.exit(1);
|
||
}
|
||
}
|
||
const JWT_SECRET_FINAL = JWT_SECRET || crypto.randomBytes(32).toString('hex');
|
||
const TOKEN_TTL = parseInt(process.env.ZY_ZHIKU_TOKEN_TTL, 10) || 86400; // 用户token延长至24h
|
||
const DATA_DIR = process.env.ZY_ZHIKU_DATA_DIR || path.join(__dirname, '..', 'data');
|
||
const LOG_DIR = process.env.ZY_ZHIKU_LOG_DIR || '/var/log/zhiku';
|
||
const DOMAIN = process.env.ZY_ZHIKU_DOMAIN || 'guanghu.online';
|
||
const USERS_DIR = path.join(DATA_DIR, 'users');
|
||
const AGENTS_DIR = path.join(DATA_DIR, 'agents');
|
||
const TASKS_DIR = path.join(DATA_DIR, 'tasks');
|
||
const START_TIME = Date.now();
|
||
|
||
// 数据源API地址
|
||
const FANQIE_API = process.env.ZY_FANQIE_API_URL || 'http://127.0.0.1:9999';
|
||
const QIMAO_API = process.env.ZY_QIMAO_API_URL || 'http://127.0.0.1:7700';
|
||
const EXTERNAL_SERVICE_TIMEOUT_MS = 3000; // 外部服务(FQWeb/SwiftCat)超时·内置直连已覆盖时无需等
|
||
|
||
// LLM API配置
|
||
const DEEPSEEK_API_URL = process.env.ZY_DEEPSEEK_API_URL || 'https://api.deepseek.com/v1/chat/completions';
|
||
const DEEPSEEK_API_KEY = process.env.ZY_DEEPSEEK_API_KEY || '';
|
||
|
||
// 邮件发送配置(复用主站 3800 的邮件服务,或直接用SMTP)
|
||
const SMTP_HOST = process.env.ZY_SMTP_HOST || '';
|
||
const SMTP_PORT = process.env.ZY_SMTP_PORT || '465';
|
||
const SMTP_USER = process.env.ZY_SMTP_USER || '';
|
||
const SMTP_PASS = process.env.ZY_SMTP_PASS || '';
|
||
// 也支持通过主站3800转发邮件
|
||
const MAIN_API_URL = process.env.ZY_MAIN_API_URL || 'http://127.0.0.1:3800';
|
||
|
||
/**
|
||
* 测试SMTP连接是否可用
|
||
*/
|
||
async function testSmtpConnection() {
|
||
if (!SMTP_USER || !SMTP_PASS) return { connected: false, error: 'SMTP配置未设置' };
|
||
|
||
const transporter = nodemailer.createTransport({
|
||
host: SMTP_HOST || autoDetectSmtpHost(SMTP_USER),
|
||
port: parseInt(SMTP_PORT, 10),
|
||
secure: true,
|
||
auth: {
|
||
user: SMTP_USER,
|
||
pass: SMTP_PASS
|
||
},
|
||
connectionTimeout: 5000
|
||
});
|
||
|
||
try {
|
||
await transporter.verify();
|
||
return { connected: true };
|
||
} catch (err) {
|
||
return { connected: false, error: err.message };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据邮箱地址自动检测 SMTP 主机
|
||
* QQ邮箱→smtp.qq.com, 163邮箱→smtp.163.com, 其他→空
|
||
*/
|
||
function autoDetectSmtpHost(email) {
|
||
if (!email || typeof email !== 'string' || !email.includes('@')) return '';
|
||
const domain = (email.split('@')[1] || '').toLowerCase();
|
||
if (!domain) return '';
|
||
if (domain === 'qq.com' || domain === 'foxmail.com') return 'smtp.qq.com';
|
||
if (domain === '163.com') return 'smtp.163.com';
|
||
if (domain === '126.com') return 'smtp.126.com';
|
||
if (domain === 'outlook.com' || domain === 'hotmail.com') return 'smtp-mail.outlook.com';
|
||
if (domain === 'gmail.com') return 'smtp.gmail.com';
|
||
return '';
|
||
}
|
||
|
||
// ─── 确保目录存在 ───
|
||
[DATA_DIR, LOG_DIR, USERS_DIR, AGENTS_DIR, TASKS_DIR,
|
||
path.join(DATA_DIR, 'books'), path.join(DATA_DIR, 'index')].forEach(dir => {
|
||
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
||
});
|
||
|
||
// ─── 信任 Nginx 反代 ───
|
||
app.set('trust proxy', 'loopback');
|
||
|
||
// ─── 七层镜防 · 最先注册(最外层防御) ───
|
||
registerShield(app);
|
||
|
||
// ─── CORS: 仅允许 guanghu.* 系域名 ───
|
||
const ALLOWED_ORIGINS = [
|
||
`https://${DOMAIN}`,
|
||
`https://www.${DOMAIN}`,
|
||
'https://guanghuyaoming.com',
|
||
'https://www.guanghuyaoming.com',
|
||
'http://localhost:3000',
|
||
'http://127.0.0.1:3000'
|
||
];
|
||
app.use(cors({
|
||
origin(origin, cb) {
|
||
if (!origin || ALLOWED_ORIGINS.some(o => origin === o || origin.endsWith(`.${DOMAIN}`))) {
|
||
return cb(null, true);
|
||
}
|
||
cb(new Error('Not allowed by CORS'));
|
||
},
|
||
credentials: true
|
||
}));
|
||
|
||
app.use(express.json({ limit: '2mb' }));
|
||
|
||
// ─── 全局速率限制 ───
|
||
app.use(rateLimit({
|
||
windowMs: 60 * 1000,
|
||
max: 120,
|
||
standardHeaders: true,
|
||
legacyHeaders: false,
|
||
message: { error: true, code: 'RATE_LIMIT', message: '请求过于频繁,请稍后再试' }
|
||
}));
|
||
|
||
// ─── 请求日志 ───
|
||
app.use((req, res, next) => {
|
||
const ts = new Date().toISOString();
|
||
const line = `${ts} ${req.method} ${req.url} ${req.ip}\n`;
|
||
try {
|
||
const logFile = path.join(LOG_DIR, `api-${ts.slice(0, 10)}.log`);
|
||
fs.appendFileSync(logFile, line);
|
||
} catch {}
|
||
next();
|
||
});
|
||
|
||
/* ═══════════════════════════════════════════════════════════
|
||
* 健康检查端点 · 扩展系统监控能力
|
||
* ═══════════════════════════════════════════════════════════ */
|
||
|
||
/**
|
||
* 获取系统磁盘使用情况
|
||
*/
|
||
function getDiskUsage() {
|
||
try {
|
||
const stats = fs.statSync('/');
|
||
return {
|
||
total: stats.blocks * stats.blksize,
|
||
free: stats.bfree * stats.blksize,
|
||
used: (stats.blocks - stats.bfree) * stats.blksize,
|
||
percent: ((stats.blocks - stats.bfree) / stats.blocks * 100).toFixed(2)
|
||
};
|
||
} catch (err) {
|
||
return { error: err.message };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取系统内存使用情况
|
||
*/
|
||
function getMemoryUsage() {
|
||
try {
|
||
const total = os.totalmem();
|
||
const free = os.freemem();
|
||
return {
|
||
total,
|
||
free,
|
||
used: total - free,
|
||
percent: ((total - free) / total * 100).toFixed(2)
|
||
};
|
||
} catch (err) {
|
||
return { error: err.message };
|
||
}
|
||
}
|
||
|
||
// 健康检查端点
|
||
app.get('/api/health', async (req, res) => {
|
||
const smtpTest = await testSmtpConnection();
|
||
const diskUsage = getDiskUsage();
|
||
const memoryUsage = getMemoryUsage();
|
||
|
||
res.json({
|
||
status: 'running',
|
||
version: '2.1',
|
||
uptime: Math.floor(process.uptime()),
|
||
server_time: new Date().toISOString(),
|
||
last_request_time: new Date().toISOString(),
|
||
disk_usage: diskUsage,
|
||
memory_usage: memoryUsage,
|
||
smtp: smtpTest,
|
||
loadavg: os.loadavg(),
|
||
cpus: os.cpus().length,
|
||
platform: os.platform(),
|
||
arch: os.arch(),
|
||
node_version: process.version,
|
||
start_time: new Date(START_TIME).toISOString()
|
||
});
|
||
});
|
||
|
||
// ─── 注册七层镜防路由 ───
|
||
registerShieldRoutes(app);
|
||
|
||
// ─── 书岚 Agent 系统路由 ───
|
||
shulanAgent.registerRoutes(app);
|
||
|
||
// ─── 内置数据源路由(如果加载成功) ───
|
||
if (builtinSource) {
|
||
builtinSource.registerRoutes(app);
|
||
}
|
||
|
||
// ─── 启动服务 ───
|
||
app.listen(PORT, '127.0.0.1', () => {
|
||
console.log(`[ZY-SVR-006] 光湖智库节点已启动 · 监听 127.0.0.1:${PORT}`);
|
||
});
|
||
|
||
// 导出用于测试
|
||
module.exports = app; |