feat: 实现Redis集成协议并重构项目控制台

refactor(backend): 重构后端服务以支持Redis协议
feat(backend): 添加Redis客户端和服务模块
feat(backend): 实现命令和日志路由处理Redis交互
refactor(frontend): 重构前端状态管理和组件结构
feat(frontend): 实现基于Redis的日志和命令功能
docs: 添加Redis集成协议文档
chore: 更新ESLint配置和依赖
This commit is contained in:
2026-01-12 19:55:51 +08:00
parent 95a4613965
commit 19e65d78dc
29 changed files with 1061 additions and 349 deletions

View File

@@ -1,22 +1,64 @@
import express from 'express'
const router = express.Router()
import express from 'express';
const router = express.Router();
import { getRedisClient } from '../services/redisClient.js';
import { projectControlKey } from '../services/redisKeys.js';
function isNonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}
// 发送指令
router.post('/', (req, res) => {
const { command } = req.body
if (!command) {
router.post('/', async (req, res) => {
const { targetProjectName, command, args } = req.body;
if (!isNonEmptyString(targetProjectName)) {
return res.status(400).json({
success: false,
message: '指令内容不能为空'
})
message: 'targetProjectName 不能为空',
});
}
// 这里将实现发送指令到Redis队列的逻辑
res.status(200).json({
success: true,
message: '指令已发送到Redis队列'
})
})
export default router
if (!isNonEmptyString(command)) {
return res.status(400).json({
success: false,
message: '指令内容不能为空',
});
}
const commandId = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const payload = {
id: commandId,
timestamp: new Date().toISOString(),
source: 'BLS Project Console',
command: command.trim(),
args: typeof args === 'object' && args !== null ? args : undefined,
};
try {
const redis = req.app?.locals?.redis || (await getRedisClient());
if (!redis?.isReady) {
return res.status(503).json({
success: false,
message: 'Redis 未就绪',
});
}
const key = projectControlKey(targetProjectName.trim());
await redis.rPush(key, JSON.stringify(payload));
return res.status(200).json({
success: true,
message: '指令已写入目标项目控制队列',
commandId,
});
} catch (err) {
console.error('Failed to enqueue command', err);
return res.status(500).json({
success: false,
message: '写入Redis失败',
});
}
});
export default router;

View File

@@ -1,12 +1,79 @@
import express from 'express'
const router = express.Router()
import express from 'express';
const router = express.Router();
import { getRedisClient } from '../services/redisClient.js';
import { projectConsoleKey, projectStatusKey } from '../services/redisKeys.js';
function parsePositiveInt(value, defaultValue) {
const num = Number.parseInt(String(value), 10);
if (!Number.isFinite(num) || num <= 0) return defaultValue;
return num;
}
// 获取日志列表
router.get('/', (req, res) => {
// 这里将实现从Redis读取日志的逻辑
res.status(200).json({
logs: []
})
})
router.get('/', async (req, res) => {
const projectName = typeof req.query.projectName === 'string' ? req.query.projectName.trim() : '';
const limit = parsePositiveInt(req.query.limit, 200);
export default router
if (!projectName) {
return res.status(200).json({
logs: [],
projectStatus: null,
});
}
try {
const redis = req.app?.locals?.redis || (await getRedisClient());
if (!redis?.isReady) {
return res.status(503).json({
logs: [],
projectStatus: null,
message: 'Redis 未就绪',
});
}
const key = projectConsoleKey(projectName);
const list = await redis.lRange(key, -limit, -1);
const logs = list
.map((raw, idx) => {
try {
const parsed = JSON.parse(raw);
const timestamp = parsed.timestamp || new Date().toISOString();
const level = (parsed.level || 'info').toString().toLowerCase();
const message = parsed.message != null ? String(parsed.message) : '';
return {
id: parsed.id || `log-${timestamp}-${idx}`,
timestamp,
level,
message,
metadata: parsed.metadata && typeof parsed.metadata === 'object' ? parsed.metadata : undefined,
};
} catch {
return {
id: `log-${Date.now()}-${idx}`,
timestamp: new Date().toISOString(),
level: 'info',
message: raw,
};
}
});
const status = await redis.get(projectStatusKey(projectName));
return res.status(200).json({
logs,
projectStatus: status || null,
});
} catch (err) {
console.error('Failed to read logs', err);
return res.status(500).json({
logs: [],
projectStatus: null,
message: '读取Redis失败',
});
}
});
export default router;

View File

@@ -1,25 +1,46 @@
import express from 'express'
import cors from 'cors'
import logRoutes from './routes/logs.js'
import commandRoutes from './routes/commands.js'
import express from 'express';
import cors from 'cors';
import logRoutes from './routes/logs.js';
import commandRoutes from './routes/commands.js';
import { getRedisClient } from './services/redisClient.js';
const app = express()
const PORT = 3001
const app = express();
const PORT = 3001;
// 中间件
app.use(cors())
app.use(express.json())
app.use(cors());
app.use(express.json());
// 路由
app.use('/api/logs', logRoutes)
app.use('/api/commands', commandRoutes)
app.use('/api/logs', logRoutes);
app.use('/api/commands', commandRoutes);
// 健康检查
app.get('/api/health', (req, res) => {
res.status(200).json({ status: 'ok' })
})
res.status(200).json({ status: 'ok' });
});
// 启动服务器
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})
const server = app.listen(PORT, async () => {
console.log(`Server running on port ${PORT}`);
try {
const redis = await getRedisClient();
app.locals.redis = redis;
console.log('[redis] client attached to app.locals');
} catch (err) {
console.error('[redis] failed to connect on startup', err);
}
});
process.on('SIGINT', async () => {
try {
if (app.locals.redis) {
await app.locals.redis.quit();
}
} catch {
// ignore
} finally {
server.close(() => process.exit(0));
}
});

View File

@@ -0,0 +1,88 @@
import { createClient } from 'redis';
let client = null;
let connectPromise = null;
let lastErrorLogAt = 0;
const ERROR_LOG_THROTTLE_MS = 60_000;
function parseIntOrDefault(value, defaultValue) {
const num = Number.parseInt(value, 10);
return Number.isFinite(num) ? num : defaultValue;
}
export async function getRedisClient() {
if (client) return client;
const host = process.env.REDIS_HOST || 'localhost';
const port = parseIntOrDefault(process.env.REDIS_PORT, 6379);
const password = process.env.REDIS_PASSWORD || undefined;
const db = parseIntOrDefault(process.env.REDIS_DB, 0);
const url = `redis://${host}:${port}`;
client = createClient({
url,
password,
database: db,
socket: {
connectTimeout: parseIntOrDefault(process.env.REDIS_CONNECT_TIMEOUT_MS, 2000),
reconnectStrategy: (retries) => {
// exponential-ish backoff with cap
return Math.min(1000 * Math.max(1, retries), 30_000);
},
},
});
client.on('error', (err) => {
const now = Date.now();
if (now - lastErrorLogAt >= ERROR_LOG_THROTTLE_MS) {
lastErrorLogAt = now;
const code = err && typeof err === 'object' && 'code' in err ? String(err.code) : 'UNKNOWN';
const message = err && typeof err === 'object' && 'message' in err ? String(err.message) : String(err);
const summary = `${code}: ${message.split('\n')[0]}`;
console.error('[redis] error', summary);
}
});
client.on('connect', () => {
console.log('[redis] connect');
});
client.on('ready', () => {
console.log('[redis] ready');
});
client.on('end', () => {
console.log('[redis] end');
});
connectPromise = client.connect().catch(() => {
// allow retries via next call
connectPromise = null;
return null;
});
return client;
}
export async function ensureRedisReady(options = {}) {
const { timeoutMs = 2000 } = options;
const redis = await getRedisClient();
if (redis.isReady) return true;
if (!connectPromise) {
connectPromise = redis.connect().catch(() => {
connectPromise = null;
return null;
});
}
try {
await Promise.race([
connectPromise,
new Promise((_, reject) => setTimeout(() => reject(new Error('Redis connect timeout')), timeoutMs)),
]);
} catch {
// ignore
}
return redis.isReady;
}

View File

@@ -0,0 +1,11 @@
export function projectStatusKey(projectName) {
return `${projectName}_项目状态`;
}
export function projectConsoleKey(projectName) {
return `${projectName}_项目控制台`;
}
export function projectControlKey(projectName) {
return `${projectName}_控制`;
}