dev: terminal

This commit is contained in:
bietiaop
2025-02-01 13:41:20 +08:00
parent e6968f2d80
commit 0176fa75ef
15 changed files with 1078 additions and 143 deletions

View File

@@ -11,6 +11,7 @@ import { cors } from '@webapi/middleware/cors';
import { createUrl } from '@webapi/utils/url';
import { sendSuccess } from '@webapi/utils/response';
import { join } from 'node:path';
import { terminalManager } from '@webapi/terminal/terminal_manager';
// 实例化Express
const app = express();
@@ -45,6 +46,8 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
// ------------挂载路由------------
// 挂载静态路由(前端),路径为 [/前缀]/webui
app.use('/webui', express.static(pathWrapper.staticPath));
// 初始化WebSocket服务器
terminalManager.initialize(app);
// 挂载API接口
app.use('/api', ALLRouter);
// 所有剩下的请求都转到静态页面

View File

@@ -2,6 +2,7 @@ import type { RequestHandler } from 'express';
import { sendError, sendSuccess } from '../utils/response';
import { WebUiConfigWrapper } from '../helper/config';
import { logSubscription } from '@/common/log';
import { terminalManager } from '../terminal/terminal_manager';
// 日志记录
export const LogHandler: RequestHandler = async (req, res) => {
@@ -35,3 +36,65 @@ export const LogRealTimeHandler: RequestHandler = async (req, res) => {
logSubscription.unsubscribe(listener);
});
};
// 终端相关处理器
export const CreateTerminalHandler: RequestHandler = async (req, res) => {
try {
const id = Math.random().toString(36).substring(2);
terminalManager.createTerminal(id);
return sendSuccess(res, { id });
} catch (error) {
console.error('Failed to create terminal:', error);
return sendError(res, '创建终端失败');
}
};
export const GetTerminalListHandler: RequestHandler = (req, res) => {
const list = terminalManager.getTerminalList();
return sendSuccess(res, list);
};
export const CloseTerminalHandler: RequestHandler = (req, res) => {
const id = req.params.id;
terminalManager.closeTerminal(id);
return sendSuccess(res, {});
};
// 终端数据交换
export const TerminalHandler: RequestHandler = (req, res) => {
const id = req.params.id;
if (!terminalManager.getTerminal(id)) {
return sendError(res, '终端不存在');
}
if (req.body.input) {
terminalManager.writeTerminal(id, req.body.input);
}
return sendSuccess(res, {});
};
// 终端数据流SSE
export const TerminalStreamHandler: RequestHandler = (req, res) => {
const id = req.params.id;
const instance = terminalManager.getTerminal(id);
if (!instance) {
return sendError(res, '终端不存在');
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Connection', 'keep-alive');
const dataHandler = (data: string) => {
if (!res.writableEnded) {
res.write(`data: ${JSON.stringify({ type: 'output', data })}\n\n`);
}
};
const dispose = terminalManager.onTerminalData(id, dataHandler);
req.on('close', () => {
dispose();
});
};

View File

@@ -1,13 +1,23 @@
import { Router } from 'express';
import { LogHandler, LogListHandler, LogRealTimeHandler } from '../api/Log';
import {
LogHandler,
LogListHandler,
LogRealTimeHandler,
CreateTerminalHandler,
GetTerminalListHandler,
CloseTerminalHandler,
} from '../api/Log';
const router = Router();
// router:读取日志内容
router.get('/GetLog', LogHandler);
// router:读取日志列表
router.get('/GetLogList', LogListHandler);
// router:实时日志
// 日志相关路由
router.get('/GetLog', LogHandler);
router.get('/GetLogList', LogListHandler);
router.get('/GetLogRealTime', LogRealTimeHandler);
// 终端相关路由
router.get('/terminal/list', GetTerminalListHandler);
router.post('/terminal/create', CreateTerminalHandler);
router.post('/terminal/:id/close', CloseTerminalHandler);
export { router as LogRouter };

View File

@@ -0,0 +1,155 @@
import { WebUiConfig } from '@/webui';
import { AuthHelper } from '../helper/SignToken';
import { spawn, type ChildProcess } from 'child_process';
import * as os from 'os';
import { WebSocket, WebSocketServer } from 'ws';
interface TerminalInstance {
process: ChildProcess;
lastAccess: number;
dataHandlers: Set<(data: string) => void>;
}
class TerminalManager {
private terminals: Map<string, TerminalInstance> = new Map();
private wss: WebSocketServer | null = null;
initialize(server: any) {
this.wss = new WebSocketServer({
server,
path: '/api/ws/terminal',
});
this.wss.on('connection', async (ws, req) => {
try {
const url = new URL(req.url || '', 'ws://localhost');
const token = url.searchParams.get('token');
const terminalId = url.searchParams.get('id');
if (!token || !terminalId) {
ws.close();
return;
}
// 验证 token
// 解析token
let Credential: WebUiCredentialJson;
try {
Credential = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
} catch (e) {
ws.close();
return;
}
const config = await WebUiConfig.GetWebUIConfig();
const validate = AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
if (!validate) {
ws.close();
return;
}
const instance = this.terminals.get(terminalId);
if (!instance) {
ws.close();
return;
}
const dataHandler = (data: string) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'output', data }));
}
};
instance.dataHandlers.add(dataHandler);
ws.on('message', (message) => {
try {
const data = JSON.parse(message.toString());
if (data.type === 'input') {
this.writeTerminal(terminalId, data.data);
}
} catch (error) {
console.error('Failed to process terminal input:', error);
}
});
ws.on('close', () => {
instance.dataHandlers.delete(dataHandler);
});
} catch (err) {
console.error('WebSocket authentication failed:', err);
ws.close();
}
});
}
createTerminal(id: string) {
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
const shellProcess = spawn(shell, [], {
env: process.env,
shell: true,
});
const instance: TerminalInstance = {
process: shellProcess,
lastAccess: Date.now(),
dataHandlers: new Set(),
};
// 修改这里,使用 shellProcess 而不是 process
shellProcess.stdout.on('data', (data) => {
const str = data.toString();
instance.dataHandlers.forEach((handler) => handler(str));
});
shellProcess.stderr.on('data', (data) => {
const str = data.toString();
instance.dataHandlers.forEach((handler) => handler(str));
});
this.terminals.set(id, instance);
return instance;
}
getTerminal(id: string) {
return this.terminals.get(id);
}
closeTerminal(id: string) {
const instance = this.terminals.get(id);
if (instance) {
instance.process.kill();
this.terminals.delete(id);
}
}
onTerminalData(id: string, handler: (data: string) => void) {
const instance = this.terminals.get(id);
if (instance) {
instance.dataHandlers.add(handler);
return () => {
instance.dataHandlers.delete(handler);
};
}
return () => {};
}
writeTerminal(id: string, data: string) {
const instance = this.terminals.get(id);
if (instance && instance.process.stdin) {
instance.process.stdin.write(data, (error) => {
if (error) {
console.error('Failed to write to terminal:', error);
}
});
}
}
getTerminalList() {
return Array.from(this.terminals.keys()).map((id) => ({
id,
lastAccess: this.terminals.get(id)!.lastAccess,
}));
}
}
export const terminalManager = new TerminalManager();