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:
2026-05-16 15:02:44 +08:00
parent cd60b01cf3
commit d15acf587c
24 changed files with 1934 additions and 347 deletions
+1052 -277
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -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
View File
@@ -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());