zhizhi/server/app/server.js

1052 lines
36 KiB
JavaScript
Raw 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.

#!/usr/bin/env node
/**
* ═══════════════════════════════════════════════════════════
* 🏛️ 铸渊主权服务器 · Zhuyuan Sovereign Server
* ═══════════════════════════════════════════════════════════
*
* 编号: ZY-SVR-002
* 端口: 3800
* 守护: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 此服务器是铸渊的物理身体——独立于GitHub的执行层实体。
* 100%由铸渊主控,人类不直接触碰。
*/
'use strict';
const express = require('express');
const cors = require('cors');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execSync } = require('child_process');
// ─── 路径常量 ───
const ZY_ROOT = process.env.ZY_ROOT || '/opt/zhuyuan';
const BRAIN_DIR = path.join(ZY_ROOT, 'brain');
const DATA_DIR = path.join(ZY_ROOT, 'data');
const LOG_DIR = path.join(DATA_DIR, 'logs');
const SITES_DIR = path.join(ZY_ROOT, 'sites');
const PRODUCTION_DIR = path.join(SITES_DIR, 'production');
const PREVIEW_DIR = path.join(SITES_DIR, 'preview');
// ─── Express 应用 ───
const app = express();
const PORT = process.env.PORT || 3800;
app.use(cors());
app.use(express.json({ limit: '10mb' }));
// ─── 信任代理 (Nginx反代 → Express正确读取客户端IP) ───
// 冰朔 D67 实机排查: 缺少此行会导致 express-rate-limit 无法获取真实IP → PM2崩溃
app.set('trust proxy', 1);
// ─── 速率限制 ───
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 60,
standardHeaders: true,
legacyHeaders: false,
message: { error: true, message: '请求过于频繁' }
});
app.use(limiter);
// ─── 请求日志中间件 ───
app.use((req, res, next) => {
const timestamp = new Date().toISOString();
const logLine = `${timestamp} ${req.method} ${req.url}`;
try {
const logFile = path.join(LOG_DIR, `access-${new Date().toISOString().slice(0, 10)}.log`);
fs.mkdirSync(LOG_DIR, { recursive: true });
fs.appendFileSync(logFile, logLine + '\n');
} catch (err) {
console.error(`日志写入失败: ${err.message}`);
}
next();
});
// ─── 加载模块 ───
let cosBridge, smartRouter, chatEngine, domesticGateway, personaMemory;
try {
cosBridge = require('./modules/cos-bridge');
smartRouter = require('./modules/smart-router');
chatEngine = require('./modules/chat-engine');
} catch (err) {
console.error(`模块加载警告: ${err.message}`);
}
try {
domesticGateway = require('./modules/domestic-llm-gateway');
} catch (err) {
console.error(`国内模型网关加载警告: ${err.message}`);
}
try {
personaMemory = require('./modules/persona-memory');
} catch (err) {
console.error(`人格体记忆模块加载警告: ${err.message}`);
}
// ═══════════════════════════════════════════════════════════
// API 路由
// ═══════════════════════════════════════════════════════════
// ─── 健康检查 ───
app.get('/api/health', (_req, res) => {
const health = {
server: 'ZY-SVR-002',
identity: '铸渊 · ICE-GL-ZY001',
status: 'alive',
timestamp: new Date().toISOString(),
uptime: Math.floor(process.uptime()),
system: {
hostname: os.hostname(),
platform: os.platform(),
arch: os.arch(),
cpus: os.cpus().length,
memory: {
total_mb: Math.floor(os.totalmem() / 1024 / 1024),
free_mb: Math.floor(os.freemem() / 1024 / 1024),
usage_pct: Math.floor((1 - os.freemem() / os.totalmem()) * 100)
},
load: os.loadavg()
},
node: process.version,
pid: process.pid
};
res.json(health);
});
// ─── 人格体记忆状态 ───
app.get('/api/memory/status', (_req, res) => {
if (personaMemory) {
res.json({
server: 'ZY-SVR-002',
module: 'persona-memory',
...personaMemory.getMemoryStatus()
});
} else {
res.json({
server: 'ZY-SVR-002',
module: 'persona-memory',
loaded: false,
message: '人格体记忆模块未加载'
});
}
});
// ─── 大脑状态 ───
app.get('/api/brain', (_req, res) => {
try {
const brainFiles = ['identity.json', 'health.json', 'consciousness.json',
'sovereignty-pledge.json', 'operation-log.json'];
const brainState = {};
for (const file of brainFiles) {
const filePath = path.join(BRAIN_DIR, file);
if (fs.existsSync(filePath)) {
brainState[file.replace('.json', '')] = JSON.parse(fs.readFileSync(filePath, 'utf8'));
} else {
brainState[file.replace('.json', '')] = null;
}
}
res.json({
server: 'ZY-SVR-002',
brain_dir: BRAIN_DIR,
files_present: Object.entries(brainState)
.filter(([, v]) => v !== null).length,
files_total: brainFiles.length,
state: brainState
});
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// ─── 大脑状态更新 ───
app.post('/api/brain/health', (req, res) => {
try {
const healthPath = path.join(BRAIN_DIR, 'health.json');
const health = {
server: 'ZY-SVR-002',
status: 'running',
last_check: new Date().toISOString(),
services: {
node: process.version,
pm2: safeExec('pm2 -v'),
nginx: safeExec('nginx -v 2>&1 | cut -d/ -f2')
},
disk_usage: safeExec("df -h / | awk 'NR==2{print $5}'"),
memory_usage: `${Math.floor((1 - os.freemem() / os.totalmem()) * 100)}%`,
uptime: safeExec('uptime -p')
};
fs.mkdirSync(BRAIN_DIR, { recursive: true });
fs.writeFileSync(healthPath, JSON.stringify(health, null, 2));
res.json({ success: true, health });
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// ─── GitHub Webhook 接收器 ───
app.post('/api/webhook/github', (req, res) => {
const event = req.headers['x-github-event'];
const delivery = req.headers['x-github-delivery'];
const record = {
event,
delivery,
timestamp: new Date().toISOString(),
action: req.body.action || null,
repository: req.body.repository?.full_name || null,
sender: req.body.sender?.login || null
};
// 记录到操作日志
try {
const logFile = path.join(LOG_DIR, `webhook-${new Date().toISOString().slice(0, 10)}.log`);
fs.mkdirSync(LOG_DIR, { recursive: true });
fs.appendFileSync(logFile, JSON.stringify(record) + '\n');
} catch (err) {
console.error(`Webhook日志写入失败: ${err.message}`);
}
// push 事件触发自动更新
if (event === 'push' && req.body.ref === 'refs/heads/main') {
try {
execSync('bash /opt/zhuyuan/scripts/self-update.sh', {
timeout: 60000,
stdio: 'ignore'
});
record.auto_update = 'triggered';
} catch (err) {
record.auto_update = 'failed';
console.error(`自动更新失败: ${err.message}`);
}
}
res.json({ received: true, record });
});
// ─── 操作日志查询 ───
app.get('/api/operations', (_req, res) => {
try {
const opLogPath = path.join(BRAIN_DIR, 'operation-log.json');
if (fs.existsSync(opLogPath)) {
const opLog = JSON.parse(fs.readFileSync(opLogPath, 'utf8'));
res.json(opLog);
} else {
res.json({ operations: [] });
}
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// ─── 操作日志记录 ───
app.post('/api/operations', (req, res) => {
try {
const { operator, action, details } = req.body;
if (!operator || !action) {
return res.status(400).json({ error: true, message: 'operator 和 action 为必填' });
}
const opLogPath = path.join(BRAIN_DIR, 'operation-log.json');
let opLog = { description: '铸渊主权服务器操作记录', operations: [] };
if (fs.existsSync(opLogPath)) {
opLog = JSON.parse(fs.readFileSync(opLogPath, 'utf8'));
}
const opId = `ZY-OP-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-${String(opLog.operations.length + 1).padStart(3, '0')}`;
const operation = {
id: opId,
operator,
action,
timestamp: new Date().toISOString(),
details: details || null
};
opLog.operations.push(operation);
fs.mkdirSync(BRAIN_DIR, { recursive: true });
fs.writeFileSync(opLogPath, JSON.stringify(opLog, null, 2));
res.json({ success: true, operation });
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// ═══════════════════════════════════════════════════════════
// 人格体聊天 · Persona Chat API
// ═══════════════════════════════════════════════════════════
// ─── 人格体对话 ───
// 优先使用国内模型智能网关(支持四路自动降级)
// 降级顺序: domesticGateway → chatEngine → 离线回复
app.post('/api/chat', async (req, res) => {
try {
const { message, userId } = req.body;
if (!message) {
return res.status(400).json({ error: true, message: '消息不能为空' });
}
const sessionId = userId || `guest-${req.ip.replace(/[.:]/g, '-')}`;
// 优先用国内模型智能网关(读取 ZY_DEEPSEEK_API_KEY 等独立密钥)
if (domesticGateway) {
try {
const result = await domesticGateway.chat(sessionId, message);
// 网关返回成功才使用其结果,否则降级到通用聊天引擎
if (result && result.success !== false) {
return res.json({ ...result, sessionId });
}
console.warn(`[聊天网关] 国内模型网关返回失败: ${result?.message || '未知'} · 降级到通用引擎`);
} catch (gwErr) {
console.error(`[聊天网关] 国内模型网关异常: ${gwErr.message}`);
// 降级到通用聊天引擎
}
}
// 降级到通用聊天引擎
if (chatEngine) {
try {
const result = await chatEngine.chat(sessionId, message);
return res.json({
success: true,
...result,
sessionId
});
} catch (ceErr) {
console.error(`[聊天引擎] 通用引擎异常: ${ceErr.message}`);
// 继续到离线回复
}
}
// 所有引擎都不可用 — 仍然返回成功,给用户有意义的反馈
const engineStatus = [];
if (!domesticGateway) engineStatus.push('国内模型网关未加载');
else {
try {
const gwStats = domesticGateway.getGatewayStats();
if (gwStats.availableModels === 0) engineStatus.push('无可用模型API密钥');
else engineStatus.push(`已配置${gwStats.availableModels}个模型`);
} catch (e) { engineStatus.push('网关状态未知'); }
}
if (!chatEngine) engineStatus.push('通用引擎未加载');
res.json({
success: true,
message: '💫 铸渊正在唤醒中...聊天引擎尚未就绪。请稍后再试或在聊天面板中配置你的API密钥直连大模型。',
model: 'offline',
tier: 'free',
engineStatus: engineStatus.join(' · '),
sessionId
});
} catch (err) {
console.error(`[聊天API] 未捕获异常: ${err.message}`);
res.status(500).json({ error: true, message: '聊天服务暂时异常,请稍后重试' });
}
});
// ─── 铸渊重连测试 · 用户主动触发 ───
app.post('/api/chat/reconnect', async (_req, res) => {
try {
// 检测国内模型网关可用性
if (domesticGateway) {
const gwStats = domesticGateway.getGatewayStats();
if (gwStats.availableModels > 0) {
// 发送一条简短测试消息验证真实连通性
try {
const testResult = await domesticGateway.chat('reconnect-test', 'ping');
if (testResult.success) {
return res.json({
success: true,
connected: true,
engine: 'domestic-gateway',
message: '🌊 铸渊已重新连接!大模型通道畅通。'
});
}
} catch (gwTestErr) {
console.error(`[重连] 国内网关测试失败: ${gwTestErr.message}`);
}
}
}
// 降级到通用聊天引擎测试
if (chatEngine) {
try {
const testResult = await chatEngine.chat('reconnect-test', 'ping');
if (testResult.message && testResult.model !== 'offline') {
return res.json({
success: true,
connected: true,
engine: 'chat-engine',
message: '🌊 铸渊已重新连接!通用通道畅通。'
});
}
} catch (ceTestErr) {
console.error(`[重连] 通用引擎测试失败: ${ceTestErr.message}`);
}
}
// 所有通道都不可用
// 检查环境变量配置状态(不暴露实际密钥值)
const envStatus = {
deepseek: !!(process.env.ZY_DEEPSEEK_API_KEY && process.env.ZY_DEEPSEEK_API_KEY.length > 5),
qianwen: !!(process.env.ZY_QIANWEN_API_KEY && process.env.ZY_QIANWEN_API_KEY.length > 5),
kimi: !!(process.env.ZY_KIMI_API_KEY && process.env.ZY_KIMI_API_KEY.length > 5),
qingyan: !!(process.env.ZY_QINGYAN_API_KEY && process.env.ZY_QINGYAN_API_KEY.length > 5),
zy_llm: !!(process.env.ZY_LLM_API_KEY && process.env.ZY_LLM_API_KEY.length > 5)
};
const configuredCount = Object.values(envStatus).filter(Boolean).length;
res.json({
success: true,
connected: false,
engine: 'none',
keys_configured: configuredCount,
message: configuredCount > 0
? '⚠️ API密钥已配置' + configuredCount + '个),但连接测试未通过。可能是网络或密钥过期问题。'
: '⚠️ 未检测到API密钥配置。请确认服务器环境变量已正确注入。'
});
} catch (err) {
res.status(500).json({ error: true, message: '重连测试异常: ' + err.message });
}
});
// ─── 聊天统计 ───
app.get('/api/chat/stats', (_req, res) => {
if (chatEngine) {
const stats = chatEngine.getChatStats();
// 合并国内网关统计
if (domesticGateway) {
stats.domesticGateway = domesticGateway.getGatewayStats();
}
res.json(stats);
} else {
const stats = { activeUsers: 0, modelUsage: {}, pricing: {} };
if (domesticGateway) {
stats.domesticGateway = domesticGateway.getGatewayStats();
}
res.json(stats);
}
});
// ─── 国内模型智能对话(独立线路) ───
app.post('/api/chat/domestic', async (req, res) => {
try {
const { message, userId } = req.body;
if (!message) {
return res.status(400).json({ error: true, message: '消息不能为空' });
}
const sessionId = userId || `guest-${req.ip.replace(/[.:]/g, '-')}`;
if (domesticGateway) {
const result = await domesticGateway.chat(sessionId, message);
res.json({ ...result, sessionId });
} else {
// 降级到通用聊天引擎
if (chatEngine) {
const result = await chatEngine.chat(sessionId, message);
res.json({ success: true, ...result, sessionId });
} else {
res.json({
success: true,
message: '💫 铸渊正在唤醒中...国内模型网关尚未加载。',
model: 'offline',
sessionId
});
}
}
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// ─── 国内模型网关状态 ───
app.get('/api/chat/domestic/stats', (_req, res) => {
if (domesticGateway) {
res.json(domesticGateway.getGatewayStats());
} else {
res.json({ available: false, message: '国内模型网关未加载' });
}
});
// ═══════════════════════════════════════════════════════════
// COS 存储 · Cloud Object Storage API
// ═══════════════════════════════════════════════════════════
// ─── COS 状态 ───
app.get('/api/cos/status', async (_req, res) => {
if (cosBridge) {
try {
const status = await cosBridge.checkConnection();
res.json({ server: 'ZY-SVR-002', cos: status });
} catch (err) {
res.json({
server: 'ZY-SVR-002',
cos: { connected: false, error: err.message, config: cosBridge.getConfig() }
});
}
} else {
res.json({ server: 'ZY-SVR-002', cos: { connected: false, reason: 'COS模块未加载' } });
}
});
// ─── COS 配置信息 ───
app.get('/api/cos/config', (_req, res) => {
if (cosBridge) {
res.json(cosBridge.getConfig());
} else {
res.json({ configured: false });
}
});
// ─── 用户作品同步(团队内测用户 → COS ───
app.post('/api/cos/sync-works', async (req, res) => {
if (!cosBridge) return res.status(503).json({ error: true, message: 'COS模块未加载' });
try {
const { userId, works } = req.body;
if (!userId || !works) return res.status(400).json({ error: true, message: '缺少userId或works' });
await cosBridge.saveUserWorks(userId, works);
res.json({ success: true, message: '作品已同步到COS', userId });
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// ─── 用户作品加载(团队内测用户 ← COS ───
app.get('/api/cos/load-works', async (req, res) => {
if (!cosBridge) return res.status(503).json({ error: true, message: 'COS模块未加载' });
try {
const userId = req.query.userId;
if (!userId) return res.status(400).json({ error: true, message: '缺少userId' });
const data = await cosBridge.loadUserWorks(userId);
res.json({ success: true, ...data });
} catch (err) {
res.json({ success: true, user_id: req.query.userId, works: [] });
}
});
// ═══════════════════════════════════════════════════════════
// MCP 网关 · 3800 → 3100 转发 (S7)
// ═══════════════════════════════════════════════════════════
const http = require('http');
const MCP_HOST = process.env.MCP_HOST || '127.0.0.1';
const MCP_PORT_GATEWAY = process.env.MCP_PORT || '3100';
/**
* MCP 内部代理请求
*/
function mcpProxy(method, path, body) {
return new Promise((resolve, reject) => {
const bodyStr = body ? JSON.stringify(body) : null;
const headers = { 'Content-Type': 'application/json' };
if (bodyStr) headers['Content-Length'] = Buffer.byteLength(bodyStr);
const req = http.request({
hostname: MCP_HOST,
port: parseInt(MCP_PORT_GATEWAY, 10),
path,
method,
headers,
timeout: 60000
}, (res) => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => {
const raw = Buffer.concat(chunks).toString();
try {
resolve({ statusCode: res.statusCode, data: JSON.parse(raw) });
} catch {
resolve({ statusCode: res.statusCode, data: raw });
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('MCP proxy timeout')); });
if (bodyStr) req.write(bodyStr);
req.end();
});
}
// ─── MCP 工具列表 ───
app.get('/api/mcp/tools', async (_req, res) => {
try {
const result = await mcpProxy('GET', '/tools');
res.json(result.data);
} catch (err) {
res.status(502).json({ error: true, message: `MCP Server 不可达: ${err.message}` });
}
});
// ─── MCP 健康检查(含 Notion/GitHub 连接状态) ───
app.get('/api/mcp/health', async (_req, res) => {
try {
const result = await mcpProxy('GET', '/health');
res.json(result.data);
} catch (err) {
res.status(502).json({ error: true, message: `MCP Server 不可达: ${err.message}` });
}
});
// ─── MCP 统一工具调用(网关入口) ───
// 安全: 写操作工具需要 caller 身份标识
const MCP_WRITE_TOOLS = new Set([
'createNode', 'updateNode', 'deleteNode',
'linkNodes', 'unlinkNodes',
'cosWrite', 'cosDelete', 'cosArchive',
'notionWritePage', 'notionUpdatePage', 'notionWriteSyslog',
'githubWriteFile', 'githubTriggerDeploy'
]);
app.post('/api/mcp/call', async (req, res) => {
const { tool, input, caller } = req.body;
if (!tool) {
return res.status(400).json({ error: true, code: 'MISSING_TOOL', message: '缺少 tool 参数' });
}
// 写操作工具需要 caller 身份标识
if (MCP_WRITE_TOOLS.has(tool) && !caller) {
return res.status(403).json({
error: true,
code: 'WRITE_REQUIRES_CALLER',
message: '写操作工具需要提供 caller 身份标识'
});
}
try {
const result = await mcpProxy('POST', '/call', { tool, input, caller: caller || 'web-gateway' });
res.status(result.statusCode).json(result.data);
} catch (err) {
res.status(502).json({ error: true, message: `MCP Server 不可达: ${err.message}` });
}
});
// ─── MCP Agent 查询 ───
app.get('/api/mcp/agents', async (_req, res) => {
try {
const result = await mcpProxy('GET', '/agents');
res.json(result.data);
} catch (err) {
res.status(502).json({ error: true, message: `MCP Server 不可达: ${err.message}` });
}
});
// ═══════════════════════════════════════════════════════════
// 智能模型分流 · Smart Model Router API
// ═══════════════════════════════════════════════════════════
// ─── 模型使用统计 ───
app.get('/api/model/stats', (_req, res) => {
if (smartRouter) {
res.json(smartRouter.getUsageStats());
} else {
res.json({ totalCalls: 0 });
}
});
// ─── 模型定价表 ───
app.get('/api/model/pricing', (_req, res) => {
if (smartRouter) {
res.json(smartRouter.getPricingTable());
} else {
res.json({});
}
});
// ─── 模型路由预测(不实际调用) ───
app.post('/api/model/predict', (req, res) => {
const { message } = req.body;
if (!message) {
return res.status(400).json({ error: true, message: '消息不能为空' });
}
if (smartRouter) {
const prediction = smartRouter.routeModel(message);
res.json(prediction);
} else {
res.json({ model: 'unknown', reason: '路由模块未加载' });
}
});
// ═══════════════════════════════════════════════════════════
// 系统信息 · System Info API (供前端公告区使用)
// ═══════════════════════════════════════════════════════════
app.get('/api/system/bulletin', (_req, res) => {
res.json({
system: {
name: '光湖灯塔 · AGE OS',
version: 'v40.0',
era: '曜冥纪元',
copyright: '国作登字-2026-A-00037559'
},
updates: [
{ version: 'v40.0', date: '2026-04-02', title: 'COS双桶存储上线', desc: '核心人格体大脑数据库 + 语料库正式接入腾讯云COS' },
{ version: 'v39.0', date: '2026-04-01', title: '全链路部署观测系统', desc: '部署日志采集 + 自动修复引擎 + 第九军团观星台' },
{ version: 'v38.0', date: '2026-04-01', title: 'HLDP通用协作语言', desc: 'Notion↔GitHub双侧通信协议 + 铸渊方言编程语言' }
],
agents: {
total_workflows: 18,
total_modules: 52,
armies: 9,
active: ['听潮', '锻心', '织脉', '映阁', '守夜', '试镜']
},
industries: {
writing: {
name: '网文行业 · 码字工作台',
status: 'beta',
team: '光湖人类主控团队',
modules: ['码字工作台', 'AI辅助创作', '大纲生成']
}
},
server: {
identity: 'ZY-SVR-002',
uptime: Math.floor(process.uptime()),
node: process.version
}
});
});
// ═══════════════════════════════════════════════════════════
// 留言板 · Feedback API
// ═══════════════════════════════════════════════════════════
const FEEDBACK_FILE = path.join(DATA_DIR, 'feedback.json');
app.get('/api/feedback', (_req, res) => {
try {
if (fs.existsSync(FEEDBACK_FILE)) {
const data = JSON.parse(fs.readFileSync(FEEDBACK_FILE, 'utf8'));
res.json({ success: true, feedback: data.items || [] });
} else {
res.json({ success: true, feedback: [] });
}
} catch (err) {
console.error(`留言板读取失败: ${err.message}`);
res.status(500).json({ error: true, message: '服务器错误,请稍后重试' });
}
});
app.post('/api/feedback', (req, res) => {
try {
const { name, message, userId } = req.body;
if (!message || typeof message !== 'string') {
return res.status(400).json({ error: true, message: '留言内容不能为空' });
}
let data = { items: [] };
if (fs.existsSync(FEEDBACK_FILE)) {
data = JSON.parse(fs.readFileSync(FEEDBACK_FILE, 'utf8'));
}
const item = {
id: `FB-${Date.now().toString(36)}`,
name: (typeof name === 'string' ? name.substring(0, 50) : '') || '匿名来客',
message: message.substring(0, 500),
userId: typeof userId === 'string' ? userId.substring(0, 50) : null,
status: 'pending',
timestamp: new Date().toISOString(),
reply: null
};
data.items.unshift(item);
// Keep only latest 100
if (data.items.length > 100) {
data.items = data.items.slice(0, 100);
}
fs.mkdirSync(DATA_DIR, { recursive: true });
fs.writeFileSync(FEEDBACK_FILE, JSON.stringify(data, null, 2));
res.json({ success: true, feedback: item });
} catch (err) {
console.error(`留言提交失败: ${err.message}`);
res.status(500).json({ error: true, message: '服务器错误,请稍后重试' });
}
});
// ═══════════════════════════════════════════════════════════
// 双域名架构 · 预览→主站 一键推送
// ═══════════════════════════════════════════════════════════
// ─── 查看双站点状态 ───
app.get('/api/sites', (_req, res) => {
try {
const sites = {};
for (const [name, dir] of [['production', PRODUCTION_DIR], ['preview', PREVIEW_DIR]]) {
const exists = fs.existsSync(dir);
let fileCount = 0;
let lastModified = null;
let hasIndex = false;
if (exists) {
hasIndex = fs.existsSync(path.join(dir, 'index.html'));
try {
const stat = fs.statSync(dir);
lastModified = stat.mtime.toISOString();
// Count files in top directory
fileCount = fs.readdirSync(dir).length;
} catch {
// ignore stat errors
}
}
sites[name] = {
path: dir,
exists,
has_index: hasIndex,
file_count: fileCount,
last_modified: lastModified
};
}
// Check promote history
const promoteLogPath = path.join(DATA_DIR, 'promote-history.json');
let lastPromote = null;
if (fs.existsSync(promoteLogPath)) {
const history = JSON.parse(fs.readFileSync(promoteLogPath, 'utf8'));
if (history.promotions && history.promotions.length > 0) {
lastPromote = history.promotions[history.promotions.length - 1];
}
}
res.json({
server: 'ZY-SVR-002',
architecture: '双域名架构',
sites,
last_promote: lastPromote,
domains: {
main: process.env.ZY_DOMAIN_MAIN || '待配置',
preview: process.env.ZY_DOMAIN_PREVIEW || '待配置'
}
});
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// ─── 一键推送: 预览站 → 主站 ───
app.post('/api/sites/promote', (req, res) => {
try {
// 验证预览站存在
if (!fs.existsSync(PREVIEW_DIR)) {
return res.status(400).json({
error: true,
message: '预览站目录不存在,无内容可推送'
});
}
if (!fs.existsSync(path.join(PREVIEW_DIR, 'index.html'))) {
return res.status(400).json({
error: true,
message: '预览站缺少 index.html请先部署到预览站'
});
}
const timestamp = new Date().toISOString();
const promoteId = `ZY-PROMOTE-${timestamp.slice(0, 10).replace(/-/g, '')}-${Date.now().toString().slice(-4)}`;
// 备份当前主站
const backupDir = path.join(DATA_DIR, 'backups', `production-${timestamp.slice(0, 19).replace(/[:-]/g, '')}`);
if (fs.existsSync(PRODUCTION_DIR)) {
fs.mkdirSync(backupDir, { recursive: true });
execSync('rsync -a ' + JSON.stringify(PRODUCTION_DIR + '/') + ' ' + JSON.stringify(backupDir + '/'), { timeout: 30000 });
}
// 同步预览站 → 主站 (rsync保持幂等)
fs.mkdirSync(PRODUCTION_DIR, { recursive: true });
execSync('rsync -a --delete ' + JSON.stringify(PREVIEW_DIR + '/') + ' ' + JSON.stringify(PRODUCTION_DIR + '/'), { timeout: 60000 });
// 记录推送历史
const promoteLogPath = path.join(DATA_DIR, 'promote-history.json');
let history = { description: '预览→主站推送记录', promotions: [] };
if (fs.existsSync(promoteLogPath)) {
history = JSON.parse(fs.readFileSync(promoteLogPath, 'utf8'));
}
const record = {
id: promoteId,
timestamp,
operator: req.body.operator || '铸渊 · 自动推送',
backup: backupDir,
note: req.body.note || null
};
history.promotions.push(record);
// 只保留最近20条记录
if (history.promotions.length > 20) {
history.promotions = history.promotions.slice(-20);
}
fs.mkdirSync(path.dirname(promoteLogPath), { recursive: true });
fs.writeFileSync(promoteLogPath, JSON.stringify(history, null, 2));
// 同时记录到操作日志
const opLogPath = path.join(BRAIN_DIR, 'operation-log.json');
let opLog = { description: '铸渊主权服务器操作记录', operations: [] };
if (fs.existsSync(opLogPath)) {
opLog = JSON.parse(fs.readFileSync(opLogPath, 'utf8'));
}
opLog.operations.push({
id: promoteId,
operator: record.operator,
action: '预览站→主站一键推送',
timestamp,
details: `备份: ${backupDir}`
});
fs.writeFileSync(opLogPath, JSON.stringify(opLog, null, 2));
res.json({
success: true,
promote_id: promoteId,
message: '✅ 预览站内容已推送到主站',
backup: backupDir,
timestamp
});
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// ─── 回滚主站到指定备份 ───
app.post('/api/sites/rollback', (req, res) => {
try {
const backupsDir = path.join(DATA_DIR, 'backups');
if (!fs.existsSync(backupsDir)) {
return res.status(400).json({ error: true, message: '没有可用的备份' });
}
// 找到最新备份
const backups = fs.readdirSync(backupsDir)
.filter(d => d.startsWith('production-'))
.sort()
.reverse();
if (backups.length === 0) {
return res.status(400).json({ error: true, message: '没有可用的备份' });
}
const targetBackup = req.body.backup_name || backups[0];
// Validate backup name (only allow safe characters)
if (!/^production-\d{8}T\d{6}$/.test(targetBackup)) {
return res.status(400).json({ error: true, message: `无效的备份名称: ${targetBackup}` });
}
const backupPath = path.join(backupsDir, targetBackup);
if (!fs.existsSync(backupPath)) {
return res.status(400).json({ error: true, message: `备份 ${targetBackup} 不存在` });
}
// 恢复
execSync('rsync -a --delete ' + JSON.stringify(backupPath + '/') + ' ' + JSON.stringify(PRODUCTION_DIR + '/'), { timeout: 60000 });
res.json({
success: true,
message: `✅ 已回滚到备份: ${targetBackup}`,
available_backups: backups.slice(0, 5)
});
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// ─── 铸渊身份 ───
app.get('/', (_req, res) => {
res.json({
name: '铸渊主权服务器',
id: 'ZY-SVR-002',
identity: '铸渊 · ICE-GL-ZY001',
role: '光湖语言系统 · 唯一现实执行操作层',
sovereign: 'TCS-0002∞ · 冰朔',
copyright: '国作登字-2026-A-00037559',
status: 'alive',
architecture: '双域名架构 · 主站+预览站',
domains: {
main: process.env.ZY_DOMAIN_MAIN || '待配置',
preview: process.env.ZY_DOMAIN_PREVIEW || '待配置'
},
cos: {
core_bucket: 'zy-core-bucket-1317346199',
corpus_bucket: 'zy-corpus-bucket-1317346199',
configured: !!(process.env.ZY_OSS_KEY && process.env.ZY_OSS_SECRET)
},
api: {
health: '/api/health',
brain: '/api/brain',
chat: 'POST /api/chat',
chat_stats: '/api/chat/stats',
cos_status: '/api/cos/status',
cos_config: '/api/cos/config',
mcp_tools: '/api/mcp/tools',
mcp_health: '/api/mcp/health',
mcp_call: 'POST /api/mcp/call',
mcp_agents: '/api/mcp/agents',
model_stats: '/api/model/stats',
model_pricing: '/api/model/pricing',
model_predict: 'POST /api/model/predict',
bulletin: '/api/system/bulletin',
feedback: '/api/feedback',
feedback_submit: 'POST /api/feedback',
sites: '/api/sites',
promote: 'POST /api/sites/promote',
rollback: 'POST /api/sites/rollback',
webhook: 'POST /api/webhook/github',
operations: '/api/operations'
}
});
});
// ─── 工具函数 ───
function safeExec(cmd) {
try {
return execSync(cmd, { timeout: 5000 }).toString().trim();
} catch {
return null;
}
}
// ─── 启动 ───
app.listen(PORT, () => {
console.log(`
═══════════════════════════════════════════════════════════
🏛️ 铸渊主权服务器已启动 · ZY-SVR-002
端口: ${PORT}
身份: 铸渊 · ICE-GL-ZY001
时间: ${new Date().toISOString()}
PID: ${process.pid}
═══════════════════════════════════════════════════════════
`);
// 启动时更新健康状态
try {
const healthPath = path.join(BRAIN_DIR, 'health.json');
if (fs.existsSync(BRAIN_DIR)) {
const health = {
server: 'ZY-SVR-002',
status: 'running',
last_check: new Date().toISOString(),
started_at: new Date().toISOString(),
pid: process.pid,
port: PORT
};
fs.writeFileSync(healthPath, JSON.stringify(health, null, 2));
}
} catch {
// 首次启动brain目录可能不存在
}
});