mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
dev: terminal
This commit is contained in:
@@ -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);
|
||||
// 所有剩下的请求都转到静态页面
|
||||
|
@@ -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();
|
||||
});
|
||||
};
|
||||
|
@@ -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 };
|
||||
|
155
src/webui/src/terminal/terminal_manager.ts
Normal file
155
src/webui/src/terminal/terminal_manager.ts
Normal 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();
|
Reference in New Issue
Block a user