feat: DevTools综合升级 — 记忆查询 + 会话监看 + WebUI侧边栏重构
- docs: 17个文件重命名为 YYYY-MM-DD.HH-mm-SS-内容.md 格式 - config: 管理员凭据移至 backend/.env (ADMIN_USERNAME/PASSWORD) - gateway: 新增 SessionState 会话追踪 + GET /api/v1/admin/sessions - devtools: 新增7个代理端点 (dashboard/sessions/memory) - devtools: WebUI重构为侧边栏 + 5面板 (仪表盘/记忆/会话/服务/性能)
This commit is contained in:
+1052
-277
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,9 @@ const ROOT = path.resolve(__dirname, '../..');
|
||||
|
||||
export const DEVTOOLS_PORT = process.env.DEVTOOLS_PORT || 9090;
|
||||
export const LOGS_DIR = path.resolve(__dirname, '../logs');
|
||||
export const GATEWAY_URL = process.env.GATEWAY_URL || 'http://localhost:8080';
|
||||
export const ADMIN_USERNAME = process.env.ADMIN_USERNAME || 'admin';
|
||||
export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'cyrene-dev-admin';
|
||||
|
||||
export const SERVICES = {
|
||||
'ai-core': {
|
||||
@@ -20,9 +23,6 @@ export const SERVICES = {
|
||||
command: './main',
|
||||
env: {
|
||||
AI_CORE_PORT: '8081',
|
||||
LLM_API_URL: process.env.LLM_API_URL || 'https://api.openai.com/v1',
|
||||
LLM_API_KEY: process.env.LLM_API_KEY || '',
|
||||
LLM_MODEL: process.env.LLM_MODEL || 'gpt-4o',
|
||||
PERSONA_DIR: './internal/persona',
|
||||
},
|
||||
healthUrl: 'http://localhost:8081/api/v1/health',
|
||||
|
||||
+157
-1
@@ -16,7 +16,7 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
import { processManager } from './process-manager.js';
|
||||
import { performanceMonitor } from './performance.js';
|
||||
import { SERVICES, DEVTOOLS_PORT, LOGS_DIR, logFile } from './config.js';
|
||||
import { SERVICES, DEVTOOLS_PORT, LOGS_DIR, logFile, GATEWAY_URL, ADMIN_USERNAME, ADMIN_PASSWORD } from './config.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -62,6 +62,67 @@ processManager.on('log', (serviceId, stream, text) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ========== Gateway 代理辅助函数 ==========
|
||||
|
||||
/** 缓存的 JWT token 和过期时间 */
|
||||
let cachedToken = null;
|
||||
let tokenExpiry = 0;
|
||||
|
||||
/**
|
||||
* 获取 Gateway JWT token (通过 admin 凭据登录,缓存直到过期)
|
||||
*/
|
||||
async function getGatewayToken() {
|
||||
if (cachedToken && Date.now() < tokenExpiry - 60000) {
|
||||
return cachedToken;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(`${GATEWAY_URL}/api/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: ADMIN_USERNAME, password: ADMIN_PASSWORD }),
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
console.error('[Gateway代理] 登录失败:', resp.status);
|
||||
return null;
|
||||
}
|
||||
const data = await resp.json();
|
||||
cachedToken = data.token;
|
||||
tokenExpiry = data.expires ? data.expires * 1000 : Date.now() + 3600000;
|
||||
return cachedToken;
|
||||
} catch (err) {
|
||||
console.error('[Gateway代理] 登录异常:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理请求到 Gateway,自动携带 JWT token
|
||||
* @param {string} path - Gateway API 路径 (如 /api/v1/memory/search?user_id=...)
|
||||
* @param {object} opts - fetch 选项
|
||||
*/
|
||||
async function proxyToGateway(path, opts = {}) {
|
||||
const token = await getGatewayToken();
|
||||
if (!token) {
|
||||
return { status: 502, body: { error: '无法连接到 Gateway 认证服务' } };
|
||||
}
|
||||
|
||||
const url = `${GATEWAY_URL}${path}`;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
...opts.headers,
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await fetch(url, { ...opts, headers, signal: AbortSignal.timeout(15000) });
|
||||
const body = await resp.json().catch(() => null);
|
||||
return { status: resp.status, body };
|
||||
} catch (err) {
|
||||
return { status: 502, body: { error: `Gateway 不可达: ${err.message}` } };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== REST API 路由 ==========
|
||||
|
||||
// ---- 健康检查 ----
|
||||
@@ -74,6 +135,101 @@ app.get('/api/health', (_req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// ---- 仪表盘数据 (必须在 /api/services/:id 之前以避免路由冲突) ----
|
||||
app.get('/api/dashboard', async (_req, res) => {
|
||||
try {
|
||||
const [services, perfSnapshot, sessionsResult] = await Promise.all([
|
||||
Promise.resolve(processManager.getStatus()),
|
||||
performanceMonitor.getSnapshot(),
|
||||
proxyToGateway('/api/v1/admin/sessions').catch(() => ({ status: 502, body: { sessions: [], total: 0 } })),
|
||||
]);
|
||||
|
||||
let runningCount = 0, totalCpu = 0, totalMem = 0;
|
||||
for (const svc of Object.values(services)) {
|
||||
if (svc.status === 'running') runningCount++;
|
||||
}
|
||||
for (const p of Object.values(perfSnapshot)) {
|
||||
totalCpu += p.cpu || 0;
|
||||
totalMem += p.mem || 0;
|
||||
}
|
||||
|
||||
const sessionsData = sessionsResult.body || {};
|
||||
const activeSessions = sessionsData.total || sessionsData.sessions?.length || 0;
|
||||
let totalMessages = 0;
|
||||
if (sessionsData.sessions) {
|
||||
for (const s of sessionsData.sessions) {
|
||||
totalMessages += (s.message_count || 0);
|
||||
}
|
||||
}
|
||||
|
||||
let memoryCount = null;
|
||||
try {
|
||||
const token = await getGatewayToken();
|
||||
if (token) {
|
||||
const memResp = await fetch(`${GATEWAY_URL}/api/v1/memory?user_id=admin_admin`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (memResp.ok) {
|
||||
const memData = await memResp.json();
|
||||
memoryCount = Array.isArray(memData) ? memData.length : (memData.memories ? memData.memories.length : null);
|
||||
}
|
||||
}
|
||||
} catch { /* 忽略 */ }
|
||||
|
||||
const sysMem = process.memoryUsage();
|
||||
|
||||
res.json({
|
||||
timestamp: Date.now(),
|
||||
services: { total: Object.keys(services).length, running: runningCount, list: services },
|
||||
performance: { totalCpu: Math.round(totalCpu * 100) / 100, totalMem: Math.round(totalMem * 100) / 100, perService: perfSnapshot },
|
||||
sessions: { active: activeSessions, totalMessages },
|
||||
memory: { total: memoryCount },
|
||||
system: { heapUsedMB: Math.round(sysMem.heapUsed / 1024 / 1024 * 100) / 100, heapTotalMB: Math.round(sysMem.heapTotal / 1024 / 1024 * 100) / 100, uptime: process.uptime() },
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: `获取仪表盘数据失败: ${err.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// ---- 会话监看代理 (必须在 /api/services/:id 之前) ----
|
||||
app.get('/api/sessions', async (_req, res) => {
|
||||
const result = await proxyToGateway('/api/v1/admin/sessions');
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
app.get('/api/sessions/:id', async (req, res) => {
|
||||
const result = await proxyToGateway(`/api/v1/admin/sessions/${req.params.id}`);
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// ---- 记忆管理代理 (必须在 /api/services/:id 之前) ----
|
||||
app.get('/api/memory/search', async (req, res) => {
|
||||
const { user_id, q } = req.query;
|
||||
if (!user_id || !q) return res.status(400).json({ error: '缺少 user_id 或 q 参数' });
|
||||
const qs = new URLSearchParams({ user_id, q }).toString();
|
||||
const result = await proxyToGateway(`/api/v1/memory/search?${qs}`);
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
app.get('/api/memory/list', async (req, res) => {
|
||||
const { user_id } = req.query;
|
||||
if (!user_id) return res.status(400).json({ error: '缺少 user_id 参数' });
|
||||
const qs = new URLSearchParams({ user_id }).toString();
|
||||
const result = await proxyToGateway(`/api/v1/memory?${qs}`);
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
app.post('/api/memory/add', async (req, res) => {
|
||||
const { user_id, content, category, priority } = req.body;
|
||||
if (!user_id || !content) return res.status(400).json({ error: '缺少 user_id 或 content' });
|
||||
const result = await proxyToGateway('/api/v1/memory', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ user_id, content, category: category || 'other', priority: priority || 1 }),
|
||||
});
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// ---- 服务状态 ----
|
||||
app.get('/api/services', (_req, res) => {
|
||||
res.json(processManager.getStatus());
|
||||
|
||||
Reference in New Issue
Block a user