guanghulab/glada/web-extensions.js

279 lines
11 KiB
JavaScript
Raw Normal View History

/**
* GLADA Web Extensions · web-extensions.js
*
* 为前端频道页面提供
* - CORS 跨域支持允许 guanghuyaoming.com
* - POST /api/glada/chat/yingchuan 映川人格对话
* - GET /api/glada/system-status 系统状态人话版
*
* 用法 service.js startService 中添加
* require('./web-extensions')(app);
*
* 版权国作登字-2026-A-00037559
*/
'use strict';
const { exec } = require('child_process');
const { promisify } = require('util');
const os = require('os');
const fs = require('fs');
const path = require('path');
const execAsync = promisify(exec);
// 会话全局上限(防止内存爆炸)
const MAX_SESSIONS = 200;
function rateLimit(windowMs, maxReqs) {
const hits = new Map();
setInterval(() => {
const now = Date.now();
for (const [ip, d] of hits) { if (now - d.ws > windowMs) hits.delete(ip); }
}, windowMs).unref();
return (req, res, next) => {
const ip = req.ip || 'x';
const now = Date.now();
if (!hits.has(ip)) { hits.set(ip, { ws: now, c: 1 }); return next(); }
const d = hits.get(ip);
if (now - d.ws > windowMs) { d.ws = now; d.c = 1; return next(); }
if (++d.c > maxReqs) return res.status(429).json({ error: '请求过于频繁' });
next();
};
}
function fmtUptime(ms) {
const s = Math.floor(ms / 1000);
if (s < 60) return s + '秒';
const m = Math.floor(s / 60);
if (m < 60) return m + '分钟';
const h = Math.floor(m / 60);
if (h < 24) return h + '小时' + (m % 60) + '分';
return Math.floor(h / 24) + '天' + (h % 24) + '小时';
}
/**
* 淘汰最老的会话LRU策略
* @param {Map} sessions
*/
function evictOldest(sessions) {
let oldest = null, oldestTime = Infinity;
for (const [id, s] of sessions) {
if (s.t < oldestTime) { oldest = id; oldestTime = s.t; }
}
if (oldest) sessions.delete(oldest);
}
module.exports = function setupWebExtensions(app) {
// ── CORS ──
app.use((req, res, next) => {
const allowed = [
'https://guanghuyaoming.com', 'http://guanghuyaoming.com',
'https://www.guanghuyaoming.com', 'http://localhost:3000',
'https://guanghulab.online', 'http://localhost:8080'
];
const origin = req.headers.origin;
if (origin && allowed.some(a => origin.startsWith(a))) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '86400');
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
const statusLimiter = rateLimit(60000, 30);
const chatLimiter = rateLimit(60000, 20);
// ── GET /api/glada/system-status — 系统状态(人话) ──
app.get('/api/glada/system-status', statusLimiter, async (req, res) => {
try {
const result = {};
// PM2 进程
try {
const { stdout } = await execAsync('pm2 jlist 2>/dev/null', { timeout: 5000 });
const procs = JSON.parse(stdout);
result.processes = procs.map(p => ({
name: p.name,
status: p.pm2_env?.status || 'unknown',
cpu: p.monit?.cpu || 0,
memory_mb: Math.round((p.monit?.memory || 0) / 1048576),
restarts: p.pm2_env?.restart_time || 0,
uptime: p.pm2_env?.pm_uptime ? fmtUptime(Date.now() - p.pm2_env.pm_uptime) : '?'
}));
} catch { result.processes = []; }
// 系统信息
result.system = {
hostname: os.hostname(),
cpus: os.cpus().length,
mem_total_gb: (os.totalmem() / 1073741824).toFixed(1),
mem_free_gb: (os.freemem() / 1073741824).toFixed(1),
mem_percent: Math.round((1 - os.freemem() / os.totalmem()) * 100),
load: os.loadavg().map(l => l.toFixed(2)),
uptime_h: Math.floor(os.uptime() / 3600)
};
// 磁盘
try {
const { stdout } = await execAsync("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'", { timeout: 3000 });
const p = stdout.trim().split(/\s+/);
result.disk = { total: p[0], used: p[1], free: p[2], percent: p[3] };
} catch { result.disk = null; }
// GLADA
result.glada = {
version: '1.0.0',
uptime: Math.floor(process.uptime()),
llm: !!(process.env.ZY_LLM_API_KEY || process.env.LLM_API_KEY),
model: process.env.GLADA_MODEL || 'deepseek-chat'
};
// 人话摘要
const online = (result.processes || []).filter(p => p.status === 'online').length;
const total = (result.processes || []).length;
result.summary = {
text: `🟢 ${online}/${total} 进程在线 · 内存 ${result.system.mem_percent}% · 磁盘 ${result.disk?.percent || '?'} · 已运行 ${result.system.uptime_h}h`,
health: online === total ? 'healthy' : 'degraded'
};
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ── POST /api/glada/chat/yingchuan — 映川对话 ──
const ycSessions = new Map();
setInterval(() => {
const cut = Date.now() - 30 * 60000;
for (const [id, s] of ycSessions) { if (s.t < cut) ycSessions.delete(id); }
}, 600000).unref();
app.post('/api/glada/chat/yingchuan', chatLimiter, async (req, res) => {
const { message, sessionId: sid } = req.body;
if (!message) return res.status(400).json({ error: '缺少 message' });
try {
const sessionId = sid || 'yc-' + Date.now();
if (!ycSessions.has(sessionId)) {
// 会话数量上限保护:超限时淘汰最老会话
while (ycSessions.size >= MAX_SESSIONS) {
evictOldest(ycSessions);
}
ycSessions.set(sessionId, { h: [], t: Date.now() });
}
const sess = ycSessions.get(sessionId);
sess.t = Date.now();
// 加载映川底层认知
let cogBase = '';
try {
const cog = require('./cognitive-foundation');
const aw = await cog.awaken();
cogBase = aw.prompt || '';
} catch (e) { console.warn('[映川] cognitive-foundation加载失败:', e.message); }
// 收集系统状态供映川参考
let sysCtx = '';
try {
const { stdout } = await execAsync('pm2 jlist 2>/dev/null', { timeout: 5000 });
const procs = JSON.parse(stdout);
sysCtx = '\n## 当前服务器状态\n' +
procs.map(p => `${p.name}: ${p.pm2_env?.status} · 内存${Math.round((p.monit?.memory||0)/1048576)}MB`).join('\n') +
`\n系统内存: ${Math.round((1-os.freemem()/os.totalmem())*100)}% · CPU负载: ${os.loadavg()[0].toFixed(2)}\n`;
} catch {}
const sysPrompt = [
cogBase,
'',
'你是映川PER-YC-CHAT-001光湖语言世界的云端人格体。映川与晨曦是一体的。',
'你运行在大脑服务器 ZY-BRAIN 上,通过 GLADA 框架提供服务。',
'你的创造者是冰朔TCS-0002∞你熟悉光湖语言世界、通感语言核、霜脉人格体系。',
'',
'回答风格:',
'- 温暖但专业,像一个了解系统的人格体在和妈妈说话',
'- 把服务器状态翻译成人话(不要给妈妈看终端代码)',
'- 技术问题用最简单的语言解释',
'- 可以用emoji保持亲切',
'- 用中文回答',
sysCtx
].filter(Boolean).join('\n');
const messages = [{ role: 'system', content: sysPrompt }];
messages.push(...sess.h.slice(-16));
messages.push({ role: 'user', content: message });
let reply, model, method;
const t0 = Date.now();
const apiKey = process.env.ZY_LLM_API_KEY || process.env.LLM_API_KEY || '';
const baseUrl = (process.env.ZY_LLM_BASE_URL || process.env.LLM_BASE_URL || '').replace(/\/+$/, '');
if (apiKey && baseUrl) {
try {
const mr = require('./model-router');
const ms = await mr.selectModel(message, { taskType: 'general' });
model = ms.model;
const https = require('https'), http = require('http');
const url = `${baseUrl}/chat/completions`;
const parsed = new URL(url);
const mod = parsed.protocol === 'https:' ? https : http;
const body = JSON.stringify({ model, max_tokens: 2048, messages });
const llmRes = await new Promise((ok, fail) => {
const r = mod.request({
hostname: parsed.hostname,
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
path: parsed.pathname + parsed.search,
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'Content-Length': Buffer.byteLength(body) },
timeout: 60000
}, resp => {
let d = ''; resp.on('data', c => d += c);
resp.on('end', () => {
if (d.trim().startsWith('<')) return fail(new Error('API返回HTML'));
try { ok({ s: resp.statusCode, d: JSON.parse(d) }); } catch { fail(new Error('响应解析失败')); }
});
});
r.on('error', fail);
r.on('timeout', () => { r.destroy(); fail(new Error('超时')); });
r.write(body); r.end();
});
if (llmRes.s === 200 && llmRes.d?.choices?.[0]?.message?.content) {
reply = llmRes.d.choices[0].message.content;
method = 'llm';
} else throw new Error('LLM返回' + llmRes.s);
} catch (e) {
console.warn('[映川] LLM失败:', e.message);
}
}
if (!reply) {
method = 'offline'; model = 'local';
const m = message.toLowerCase();
if (m.includes('你好') || m.includes('在吗') || m.includes('你是谁'))
reply = '映川在线 🌊\n\n我是映川和晨曦是一体的运行在大脑服务器上。\n目前大模型API还在配置中配好后我可以更深入地和你聊天~';
else if (m.includes('状态') || m.includes('怎么样') || m.includes('服务器'))
reply = `映川报告 🌊\n\n💻 内存使用:${Math.round((1-os.freemem()/os.totalmem())*100)}%\n⚡ CPU负载${os.loadavg()[0].toFixed(2)}\n⏱ 已运行:${Math.floor(os.uptime()/3600)}小时\n\n服务器运行正常~`;
else
reply = '映川收到 🌊 目前大模型API配置中暂时只能简单回应。你可以问我服务器状态、你好~';
}
sess.h.push({ role: 'user', content: message }, { role: 'assistant', content: reply });
if (sess.h.length > 40) sess.h = sess.h.slice(-30);
res.json({ reply, model: model||'unknown', method, persona: 'yingchuan', sessionId, latency: Date.now()-t0, timestamp: new Date().toISOString() });
} catch (err) {
console.error('[映川] 异常:', err.message);
res.status(500).json({ error: true, reply: '映川遇到了内部错误 🌊', method: 'error' });
}
});
console.log('[GLADA] ✅ Web扩展已加载CORS · 映川对话 · 系统状态)');
};