Просмотр исходного кода

fix(server): 修复终端输入命令时按 Backspace 会退出终端的问题

- 在 KeyEventCycle 类中添加了对 Backspace 按键的处理逻辑
- 优化了 SshHandler 类中的数据接收流程,增加了异常捕获
- 新增了两个测试类 SSHCommandRecorder 和 TerminalParserRecorder,用于记录和解析 SSH 终端命令
bwcx_jzy 4 месяцев назад
Родитель
Сommit
d053fa5449

+ 1 - 0
CHANGELOG-BETA.md

@@ -9,6 +9,7 @@
 ### 🐞 解决BUG、优化功能
 
 1. 【server】优化 数据库表支持配置前缀 `jpom.db.table-prefix` (感谢@ccx2480)
+2. 【server】修复 终端输入命令,按Backspace 会退出终端(感谢[@dgs](https://gitee.com/dgs0924) [Gitee issues ICA57K](https://gitee.com/dromara/Jpom/issues/ICA57K) )
 
 ------
 

+ 3 - 2
modules/server/src/main/java/org/dromara/jpom/socket/handler/KeyEventCycle.java

@@ -165,7 +165,8 @@ public class KeyEventCycle {
                     }
                 }
                 str = new String(Arrays.copyOfRange(bytes, 0, bytes.length - backCount), charset);
-                buffer.insert(inputSelection-1, str);
+                // #https://gitee.com/dromara/Jpom/issues/ICA57K
+                buffer.insert(inputSelection - 1, str);
                 inputSelection += str.length();
             }
         }
@@ -175,7 +176,7 @@ public class KeyEventCycle {
     /**
      * 查找指定字节数组在原始字节数组中的位置
      *
-     * @param originalArray 原始字节数组
+     * @param originalArray   原始字节数组
      * @param byteArrayToFind 要查找的字节数组
      * @return 找到的位置索引,如果找不到返回 -1
      */

+ 5 - 4
modules/server/src/main/java/org/dromara/jpom/socket/handler/SshHandler.java

@@ -23,7 +23,6 @@ import com.alibaba.fastjson2.JSONValidator;
 import com.jcraft.jsch.ChannelShell;
 import com.jcraft.jsch.JSchException;
 import com.jcraft.jsch.Session;
-import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.jpom.common.i18n.I18nMessageUtil;
 import org.dromara.jpom.common.i18n.I18nThreadUtil;
@@ -42,7 +41,6 @@ import org.springframework.http.HttpHeaders;
 import org.springframework.web.socket.TextMessage;
 import org.springframework.web.socket.WebSocketSession;
 
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -52,7 +50,6 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Consumer;
 
 /**
  * ssh 处理2
@@ -304,7 +301,11 @@ public class SshHandler extends BaseTerminalHandler {
                 //如果没有数据来,线程会一直阻塞在这个地方等待数据。
                 while ((i = inputStream.read(buffer)) != NioUtil.EOF) {
                     byte[] tempBytes = Arrays.copyOfRange(buffer, 0, i);
-                    keyEventCycle.receive(tempBytes);
+                    try {
+                        keyEventCycle.receive(tempBytes);
+                    } catch (Exception e) {
+                        log.warn("keyEventCycle.receive", e);
+                    }
                     sendBinary(session, new String(tempBytes, machineSshModel.charset()));
                 }
             } catch (Exception e) {

+ 725 - 0
modules/sub-plugin/ssh-jsch/src/test/java/SSHCommandRecorder.java

@@ -0,0 +1,725 @@
+/**
+ * @author bwcx_jzy
+ * @since 2025/6/11
+ */
+import com.jcraft.jsch.*;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.regex.Pattern;
+
+/**
+ * SSH终端命令记录器 - 重构版
+ * 通过双向流拦截和智能解析准确记录用户执行的所有命令
+ */
+public class SSHCommandRecorder {
+
+    // ANSI转义序列模式
+    private static final Pattern ANSI_ESCAPE = Pattern.compile("\\x1B\\[[0-?]*[ -/]*[@-~]");
+
+    // 命令提示符模式(支持多种shell)
+    private static final Pattern PROMPT_PATTERN = Pattern.compile(
+        ".*?[\\$#>%]\\s*$|.*?\\w+[@:].*?[\\$#>%]\\s*$|.*?\\]\\s*[\\$#>%]\\s*$"
+    );
+
+    // 会话相关
+    private Session session;
+    private ChannelShell channel;
+    private InputStream channelInput;
+    private OutputStream channelOutput;
+
+    // 数据处理
+    private final BlockingQueue<String> inputQueue = new LinkedBlockingQueue<>();
+    private final BlockingQueue<String> outputQueue = new LinkedBlockingQueue<>();
+    private final List<CommandRecord> commandHistory = Collections.synchronizedList(new ArrayList<>());
+
+    // 状态管理
+    private volatile boolean recording = false;
+    private final TerminalState terminalState = new TerminalState();
+
+    // 线程池
+    private final ExecutorService executorService = Executors.newFixedThreadPool(4);
+
+    /**
+     * 命令记录实体
+     */
+    public static class CommandRecord {
+        private final String command;
+        private final String user;
+        private final String host;
+        private final long timestamp;
+        private final String sessionId;
+        private String output;
+        private CommandType type;
+        private long duration;
+
+        public enum CommandType {
+            TYPED,          // 直接输入
+            HISTORY_UP,     // 上键选择
+            HISTORY_DOWN,   // 下键选择
+            SEARCH,         // 搜索选择
+            TAB_COMPLETE    // Tab补全
+        }
+
+        public CommandRecord(String command, String user, String host, String sessionId) {
+            this.command = command.trim();
+            this.user = user;
+            this.host = host;
+            this.sessionId = sessionId;
+            this.timestamp = System.currentTimeMillis();
+            this.type = CommandType.TYPED;
+        }
+
+        // Getters and Setters
+        public String getCommand() { return command; }
+        public String getUser() { return user; }
+        public String getHost() { return host; }
+        public long getTimestamp() { return timestamp; }
+        public String getSessionId() { return sessionId; }
+        public String getOutput() { return output; }
+        public CommandType getType() { return type; }
+        public long getDuration() { return duration; }
+
+        public void setOutput(String output) { this.output = output; }
+        public void setType(CommandType type) { this.type = type; }
+        public void setDuration(long duration) { this.duration = duration; }
+
+        @Override
+        public String toString() {
+            return String.format("[%s] %s@%s [%s] $ %s",
+                new Date(timestamp), user, host, type, command);
+        }
+    }
+
+    /**
+     * 终端状态跟踪
+     */
+    private static class TerminalState {
+        private final StringBuilder currentLine = new StringBuilder();
+        private final StringBuilder commandBuffer = new StringBuilder();
+        private String lastPrompt = "";
+        private String currentCommand = "";
+        private boolean inCommand = false;
+        private boolean waitingForOutput = false;
+        private long commandStartTime = 0;
+        private int cursorPosition = 0;
+
+        // 特殊键检测
+        private boolean upKeyPressed = false;
+        private boolean downKeyPressed = false;
+        private boolean ctrlRPressed = false;
+        private boolean tabPressed = false;
+
+        public synchronized void reset() {
+            currentLine.setLength(0);
+            commandBuffer.setLength(0);
+            currentCommand = "";
+            inCommand = false;
+            waitingForOutput = false;
+            commandStartTime = 0;
+            resetKeyStates();
+        }
+
+        public synchronized void resetKeyStates() {
+            upKeyPressed = false;
+            downKeyPressed = false;
+            ctrlRPressed = false;
+            tabPressed = false;
+        }
+    }
+
+    /**
+     * 输入流拦截器
+     */
+    private class InputStreamInterceptor extends InputStream {
+        private final InputStream originalInput;
+        private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+        public InputStreamInterceptor(InputStream originalInput) {
+            this.originalInput = originalInput;
+        }
+
+        @Override
+        public int read() throws IOException {
+            int data = originalInput.read();
+            if (data != -1) {
+                buffer.write(data);
+                processInputByte((byte) data);
+            }
+            return data;
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            int bytesRead = originalInput.read(b, off, len);
+            if (bytesRead > 0) {
+                buffer.write(b, off, bytesRead);
+                processInputBytes(b, off, bytesRead);
+            }
+            return bytesRead;
+        }
+
+        private void processInputByte(byte b) {
+            processInputBytes(new byte[]{b}, 0, 1);
+        }
+
+        private void processInputBytes(byte[] bytes, int offset, int length) {
+            try {
+                String input = new String(bytes, offset, length, StandardCharsets.UTF_8);
+                if (!input.isEmpty()) {
+                    inputQueue.offer(input);
+                }
+            } catch (Exception e) {
+                System.err.println("Error processing input: " + e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * 输出流拦截器
+     */
+    private class OutputStreamInterceptor extends OutputStream {
+        private final OutputStream originalOutput;
+        private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+        public OutputStreamInterceptor(OutputStream originalOutput) {
+            this.originalOutput = originalOutput;
+        }
+
+        @Override
+        public void write(int b) throws IOException {
+            originalOutput.write(b);
+            buffer.write(b);
+            processOutputByte((byte) b);
+        }
+
+        @Override
+        public void write(byte[] b, int off, int len) throws IOException {
+            originalOutput.write(b, off, len);
+            buffer.write(b, off, len);
+            processOutputBytes(b, off, len);
+        }
+
+        @Override
+        public void flush() throws IOException {
+            originalOutput.flush();
+        }
+
+        private void processOutputByte(byte b) {
+            processOutputBytes(new byte[]{b}, 0, 1);
+        }
+
+        private void processOutputBytes(byte[] bytes, int offset, int length) {
+            try {
+                String output = new String(bytes, offset, length, StandardCharsets.UTF_8);
+                if (!output.isEmpty()) {
+                    outputQueue.offer(output);
+                }
+            } catch (Exception e) {
+                System.err.println("Error processing output: " + e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * 建立SSH连接并开始记录
+     */
+    public void connect(String host, int port, String username, String password) throws Exception {
+        JSch jsch = new JSch();
+        session = jsch.getSession(username, host, port);
+        session.setPassword(password);
+        session.setConfig("StrictHostKeyChecking", "no");
+        session.connect();
+
+        channel = (ChannelShell) session.openChannel("shell");
+
+        // 设置终端参数
+        channel.setPtyType("xterm-256color");
+        channel.setPtySize(120, 40, 960, 640);
+
+        // 创建管道流
+        PipedInputStream toChannelInput = new PipedInputStream();
+        PipedOutputStream fromUserInput = new PipedOutputStream(toChannelInput);
+
+        PipedInputStream fromChannelOutput = new PipedInputStream();
+        PipedOutputStream toUserOutput = new PipedOutputStream(fromChannelOutput);
+
+        // 设置拦截器
+        channelInput = new InputStreamInterceptor(System.in);
+        channelOutput = new OutputStreamInterceptor(System.out);
+
+        channel.setInputStream(toChannelInput);
+        channel.setOutputStream(toUserOutput);
+
+        channel.connect();
+
+        // 获取真实的输入输出流
+        InputStream realChannelOutput = channel.getInputStream();
+        OutputStream realChannelInput = channel.getOutputStream();
+
+        recording = true;
+
+        // 启动处理线程
+        startProcessingThreads(realChannelOutput, realChannelInput, fromUserInput, fromChannelOutput);
+
+        System.out.println("SSH连接已建立,开始记录命令...");
+    }
+
+    /**
+     * 启动处理线程
+     */
+    private void startProcessingThreads(InputStream channelOut, OutputStream channelIn,
+                                        OutputStream userIn, InputStream userOut) {
+
+        // 处理用户输入 -> SSH服务器
+        executorService.submit(() -> {
+            try {
+                Scanner scanner = new Scanner(System.in);
+                while (recording && scanner.hasNextLine()) {
+                    String line = scanner.nextLine();
+                    processUserInput(line);
+                    channelIn.write((line + "\r\n").getBytes(StandardCharsets.UTF_8));
+                    channelIn.flush();
+                }
+            } catch (Exception e) {
+                if (recording) {
+                    e.printStackTrace();
+                }
+            }
+        });
+
+        // 处理SSH服务器输出 -> 用户
+        executorService.submit(() -> {
+            try {
+                byte[] buffer = new byte[1024];
+                int bytesRead;
+                while (recording && (bytesRead = channelOut.read(buffer)) != -1) {
+                    String output = new String(buffer, 0, bytesRead, StandardCharsets.UTF_8);
+                    processServerOutput(output);
+                    System.out.print(output);
+                }
+            } catch (Exception e) {
+                if (recording) {
+                    e.printStackTrace();
+                }
+            }
+        });
+
+        // 输入分析线程
+        executorService.submit(this::processInputQueue);
+
+        // 输出分析线程
+        executorService.submit(this::processOutputQueue);
+    }
+
+    /**
+     * 处理用户输入
+     */
+    private void processUserInput(String input) {
+        synchronized (terminalState) {
+            // 检测特殊键序列
+            if (input.contains("\u001B[A")) { // 上键
+                terminalState.upKeyPressed = true;
+            } else if (input.contains("\u001B[B")) { // 下键
+                terminalState.downKeyPressed = true;
+            } else if (input.contains("\u0012")) { // Ctrl+R
+                terminalState.ctrlRPressed = true;
+            } else if (input.contains("\t")) { // Tab
+                terminalState.tabPressed = true;
+            }
+
+            // 处理正常命令输入
+            if (!input.trim().isEmpty() && !containsOnlyControlChars(input)) {
+                terminalState.currentCommand = input.trim();
+                terminalState.inCommand = true;
+                terminalState.commandStartTime = System.currentTimeMillis();
+                terminalState.waitingForOutput = true;
+            }
+        }
+    }
+
+    /**
+     * 处理服务器输出
+     */
+    private void processServerOutput(String output) {
+        synchronized (terminalState) {
+            String cleanOutput = cleanAnsiEscapes(output);
+            terminalState.currentLine.append(cleanOutput);
+
+            // 检测命令提示符
+            if (isPromptLine(cleanOutput)) {
+                if (terminalState.waitingForOutput && !terminalState.currentCommand.isEmpty()) {
+                    // 记录命令
+                    recordCommand(terminalState.currentCommand, determineCommandType());
+                    terminalState.reset();
+                }
+
+                // 提取新的提示符
+                String[] lines = cleanOutput.split("\\r?\\n");
+                for (String line : lines) {
+                    if (isPromptLine(line.trim())) {
+                        terminalState.lastPrompt = line.trim();
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 处理输入队列
+     */
+    private void processInputQueue() {
+        while (recording) {
+            try {
+                String input = inputQueue.poll(100, TimeUnit.MILLISECONDS);
+                if (input != null) {
+                    analyzeInput(input);
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                break;
+            }
+        }
+    }
+
+    /**
+     * 处理输出队列
+     */
+    private void processOutputQueue() {
+        while (recording) {
+            try {
+                String output = outputQueue.poll(100, TimeUnit.MILLISECONDS);
+                if (output != null) {
+                    analyzeOutput(output);
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                break;
+            }
+        }
+    }
+
+    /**
+     * 分析输入数据
+     */
+    private void analyzeInput(String input) {
+        // 检测控制序列
+        if (input.contains("\u001B[A")) {
+            terminalState.upKeyPressed = true;
+        } else if (input.contains("\u001B[B")) {
+            terminalState.downKeyPressed = true;
+        } else if (input.contains("\u0012")) {
+            terminalState.ctrlRPressed = true;
+        }
+
+        // 检测回车键(命令执行)
+        if (input.contains("\r") || input.contains("\n")) {
+            synchronized (terminalState) {
+                if (terminalState.inCommand) {
+                    terminalState.waitingForOutput = true;
+                }
+            }
+        }
+    }
+
+    /**
+     * 分析输出数据
+     */
+    private void analyzeOutput(String output) {
+        String cleanOutput = cleanAnsiEscapes(output);
+
+        // 检测命令回显和提示符
+        String[] lines = cleanOutput.split("\\r?\\n");
+        for (String line : lines) {
+            String trimmedLine = line.trim();
+
+            if (isPromptLine(trimmedLine)) {
+                synchronized (terminalState) {
+                    if (terminalState.waitingForOutput && !terminalState.currentCommand.isEmpty()) {
+                        recordCommand(terminalState.currentCommand, determineCommandType());
+                        terminalState.reset();
+                    }
+                    terminalState.lastPrompt = trimmedLine;
+                }
+            } else if (!trimmedLine.isEmpty() && !terminalState.inCommand) {
+                // 可能是命令回显
+                synchronized (terminalState) {
+                    if (isPotentialCommand(trimmedLine)) {
+                        terminalState.currentCommand = trimmedLine;
+                        terminalState.inCommand = true;
+                        terminalState.commandStartTime = System.currentTimeMillis();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 判断命令类型
+     */
+    private CommandRecord.CommandType determineCommandType() {
+        if (terminalState.upKeyPressed || terminalState.downKeyPressed) {
+            return terminalState.upKeyPressed ?
+                CommandRecord.CommandType.HISTORY_UP : CommandRecord.CommandType.HISTORY_DOWN;
+        } else if (terminalState.ctrlRPressed) {
+            return CommandRecord.CommandType.SEARCH;
+        } else if (terminalState.tabPressed) {
+            return CommandRecord.CommandType.TAB_COMPLETE;
+        } else {
+            return CommandRecord.CommandType.TYPED;
+        }
+    }
+
+    /**
+     * 记录命令
+     */
+    private void recordCommand(String command, CommandRecord.CommandType type) {
+        if (command == null || command.trim().isEmpty() || !isValidCommand(command)) {
+            return;
+        }
+
+        try {
+            String username = session.getUserName();
+            String hostname = session.getHost();
+            String sessionId = Integer.toHexString(session.hashCode());
+
+            CommandRecord record = new CommandRecord(command, username, hostname, sessionId);
+            record.setType(type);
+
+            if (terminalState.commandStartTime > 0) {
+                record.setDuration(System.currentTimeMillis() - terminalState.commandStartTime);
+            }
+
+            commandHistory.add(record);
+
+            // 记录到控制台和日志
+            System.out.println("\n[AUDIT] " + record);
+
+            // 异步写入文件或数据库
+            executorService.submit(() -> writeToAuditLog(record));
+
+        } catch (Exception e) {
+            System.err.println("Error recording command: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 写入审计日志
+     */
+    private void writeToAuditLog(CommandRecord record) {
+        try {
+            String logFile = "ssh_audit_" + new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date()) + ".log";
+            try (PrintWriter writer = new PrintWriter(new FileWriter(logFile, true))) {
+                writer.printf("%d|%s|%s|%s|%s|%s|%d%n",
+                    record.getTimestamp(),
+                    record.getUser(),
+                    record.getHost(),
+                    record.getSessionId(),
+                    record.getType(),
+                    record.getCommand().replace("|", "\\|"),
+                    record.getDuration());
+            }
+        } catch (IOException e) {
+            System.err.println("Error writing to audit log: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 清理ANSI转义序列
+     */
+    private String cleanAnsiEscapes(String input) {
+        if (input == null) return "";
+        return ANSI_ESCAPE.matcher(input).replaceAll("");
+    }
+
+    /**
+     * 检测是否为命令提示符行
+     */
+    private boolean isPromptLine(String line) {
+        return PROMPT_PATTERN.matcher(line).matches() ||
+            line.endsWith("$ ") || line.endsWith("# ") || line.endsWith("> ") ||
+            line.matches(".*?\\w+[@:].*?[\\$#>].*");
+    }
+
+    /**
+     * 检测是否为潜在命令
+     */
+    private boolean isPotentialCommand(String line) {
+        return line.length() > 0 &&
+            line.length() < 500 &&
+            !line.matches("^\\s*$") &&
+            !line.startsWith("Welcome") &&
+            !line.startsWith("Last login") &&
+            !line.matches(".*?\\d{4}-\\d{2}-\\d{2}.*?");
+    }
+
+    /**
+     * 验证是否为有效命令
+     */
+    private boolean isValidCommand(String command) {
+        if (command == null || command.trim().isEmpty()) {
+            return false;
+        }
+
+        String trimmed = command.trim();
+        return trimmed.length() > 0 &&
+            trimmed.length() < 1000 &&
+            !trimmed.matches("^\\s*$") &&
+            !trimmed.matches("^\\d+$") &&
+            !containsOnlyControlChars(trimmed);
+    }
+
+    /**
+     * 检查是否只包含控制字符
+     */
+    private boolean containsOnlyControlChars(String str) {
+        return str.chars().allMatch(ch -> ch < 32 || ch == 127);
+    }
+
+    /**
+     * 发送命令(用于程序化调用)
+     */
+    public void sendCommand(String command) throws Exception {
+        if (channel != null && channel.isConnected()) {
+            OutputStream out = channel.getOutputStream();
+            out.write((command + "\r\n").getBytes(StandardCharsets.UTF_8));
+            out.flush();
+        }
+    }
+
+    /**
+     * 获取命令历史记录
+     */
+    public List<CommandRecord> getCommandHistory() {
+        synchronized (commandHistory) {
+            return new ArrayList<>(commandHistory);
+        }
+    }
+
+    /**
+     * 根据条件查询命令
+     */
+    public List<CommandRecord> queryCommands(String userFilter, long startTime, long endTime,
+                                             CommandRecord.CommandType typeFilter) {
+        synchronized (commandHistory) {
+            return commandHistory.stream()
+                .filter(cmd -> userFilter == null || userFilter.equals(cmd.getUser()))
+                .filter(cmd -> cmd.getTimestamp() >= startTime && cmd.getTimestamp() <= endTime)
+                .filter(cmd -> typeFilter == null || typeFilter.equals(cmd.getType()))
+                .collect(ArrayList::new, (list, item) -> list.add(item), (list1, list2) -> list1.addAll(list2));
+        }
+    }
+
+    /**
+     * 导出审计报告
+     */
+    public void exportAuditReport(String filename) throws IOException {
+        try (PrintWriter writer = new PrintWriter(new FileWriter(filename))) {
+            writer.println("SSH Command Audit Report");
+            writer.println("Generated: " + new Date());
+           // writer.println("=".repeat(80));
+            writer.println();
+
+            synchronized (commandHistory) {
+                Map<String, Long> userStats = new HashMap<>();
+                Map<CommandRecord.CommandType, Long> typeStats = new HashMap<>();
+
+                for (CommandRecord record : commandHistory) {
+                    userStats.merge(record.getUser(), 1L, Long::sum);
+                    typeStats.merge(record.getType(), 1L, Long::sum);
+                }
+
+                writer.println("Statistics:");
+                writer.println("Total Commands: " + commandHistory.size());
+                writer.println("Users: " + userStats);
+                writer.println("Command Types: " + typeStats);
+                writer.println();
+
+                writer.println("Command Details:");
+               // writer.println("-".repeat(80));
+                for (CommandRecord record : commandHistory) {
+                    writer.printf("[%s] %s@%s [%s] [%dms] $ %s%n",
+                        new Date(record.getTimestamp()),
+                        record.getUser(),
+                        record.getHost(),
+                        record.getType(),
+                        record.getDuration(),
+                        record.getCommand());
+                }
+            }
+        }
+    }
+
+    /**
+     * 断开连接
+     */
+    public void disconnect() {
+        recording = false;
+
+        executorService.shutdown();
+        try {
+            if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
+                executorService.shutdownNow();
+            }
+        } catch (InterruptedException e) {
+            executorService.shutdownNow();
+            Thread.currentThread().interrupt();
+        }
+
+        if (channel != null && channel.isConnected()) {
+            channel.disconnect();
+        }
+
+        if (session != null && session.isConnected()) {
+            session.disconnect();
+        }
+
+        System.out.println("SSH连接已断开,命令记录已停止。");
+    }
+
+    /**
+     * 使用示例和测试
+     */
+    public static void main(String[] args) {
+        SSHCommandRecorder recorder = new SSHCommandRecorder();
+
+        try {
+            // 连接到SSH服务器
+            System.out.println("正在连接SSH服务器...");
+            recorder.connect("192.168.30.29", 22, "user", "123456+.");
+
+            // 保持连接,让用户交互
+            System.out.println("请在终端中执行命令,所有命令将被记录...");
+            System.out.println("输入 'exit' 或按 Ctrl+C 结束记录");
+
+            // 等待用户操作
+            Scanner scanner = new Scanner(System.in);
+            while (scanner.hasNextLine()) {
+                String input = scanner.nextLine();
+                if ("exit".equals(input)) {
+                    break;
+                }
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            // 导出审计报告
+            try {
+                List<CommandRecord> history = recorder.getCommandHistory();
+                System.out.println("\n=== 命令执行历史 ===");
+                history.forEach(System.out::println);
+
+                recorder.exportAuditReport("ssh_audit_report.txt");
+                System.out.println("审计报告已导出到: ssh_audit_report.txt");
+
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+
+            recorder.disconnect();
+        }
+    }
+}

+ 444 - 0
modules/sub-plugin/ssh-jsch/src/test/java/TerminalParserRecorder.java

@@ -0,0 +1,444 @@
+/**
+ * @author bwcx_jzy
+ * @since 2025/6/11
+ */
+import com.jcraft.jsch.*;
+import java.io.*;
+import java.util.*;
+import java.util.regex.Pattern;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * 基于终端解析的轻量级命令记录器
+ * 通过解析终端输出流来识别和记录执行的命令
+ */
+public class TerminalParserRecorder {
+
+    private static final Pattern PROMPT_PATTERNS = Pattern.compile(
+        ".*?[\\$#>]\\s*$|.*?\\w+@\\w+[:\\s]+.*?[\\$#>]\\s*$"
+    );
+
+    private static final Pattern COMMAND_COMPLETION_PATTERN = Pattern.compile(
+        ".*?\\[\\d+\\]\\s*.*?|.*?\\+\\s*.*?|.*?>\\s*.*?"
+    );
+
+    private Session session;
+    private ChannelShell channel;
+    private BlockingQueue<String> outputQueue;
+    private List<ExecutedCommand> commandLog;
+    private TerminalState currentState;
+    private Thread parsingThread;
+    private volatile boolean isRecording = false;
+
+    public TerminalParserRecorder() {
+        this.outputQueue = new LinkedBlockingQueue<>();
+        this.commandLog = Collections.synchronizedList(new ArrayList<>());
+        this.currentState = new TerminalState();
+    }
+
+    /**
+     * 终端状态跟踪
+     */
+    private static class TerminalState {
+        private StringBuilder currentLine = new StringBuilder();
+        private String lastPrompt = "";
+        private String pendingCommand = "";
+        private boolean waitingForPrompt = false;
+        private int cursorPosition = 0;
+
+        public void reset() {
+            currentLine.setLength(0);
+            pendingCommand = "";
+            waitingForPrompt = false;
+            cursorPosition = 0;
+        }
+    }
+
+    /**
+     * 执行命令记录
+     */
+    public static class ExecutedCommand {
+        private final String command;
+        private final String user;
+        private final String host;
+        private final long timestamp;
+        private final String sessionId;
+        private String output;
+        private int exitCode = -1;
+
+        public ExecutedCommand(String command, String user, String host, String sessionId) {
+            this.command = command;
+            this.user = user;
+            this.host = host;
+            this.sessionId = sessionId;
+            this.timestamp = System.currentTimeMillis();
+        }
+
+        // Getters
+        public String getCommand() { return command; }
+        public String getUser() { return user; }
+        public String getHost() { return host; }
+        public long getTimestamp() { return timestamp; }
+        public String getSessionId() { return sessionId; }
+        public String getOutput() { return output; }
+        public int getExitCode() { return exitCode; }
+
+        public void setOutput(String output) { this.output = output; }
+        public void setExitCode(int exitCode) { this.exitCode = exitCode; }
+
+        @Override
+        public String toString() {
+            return String.format("[%s] %s@%s $ %s",
+                new Date(timestamp).toString(), user, host, command);
+        }
+    }
+
+    /**
+     * 连接SSH并开始记录
+     */
+    public void connect(String host, int port, String username, String password) throws Exception {
+        JSch jsch = new JSch();
+        session = jsch.getSession(username, host, port);
+        session.setPassword(password);
+        session.setConfig("StrictHostKeyChecking", "no");
+        session.connect();
+
+        channel = (ChannelShell) session.openChannel("shell");
+
+        // 设置终端类型以获得更好的解析效果
+        channel.setPtyType("vt100");
+        channel.setPtySize(80, 24, 640, 480);
+
+        // 创建输出拦截流
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        TeeOutputStream teeOut = new TeeOutputStream(System.out, baos);
+        channel.setOutputStream(teeOut);
+
+        channel.connect();
+
+        // 启动解析线程
+        startParsing(channel.getInputStream());
+        isRecording = true;
+    }
+
+    /**
+     * 开始解析终端输出
+     */
+    private void startParsing(InputStream inputStream) {
+        parsingThread = new Thread(() -> {
+            try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+                StringBuilder outputBuffer = new StringBuilder();
+                int ch;
+
+                while (isRecording && (ch = reader.read()) != -1) {
+                    char character = (char) ch;
+
+                    // 处理特殊字符
+                    if (character == '\r') {
+                        continue; // 忽略回车符
+                    } else if (character == '\n') {
+                        // 处理换行
+                        String line = outputBuffer.toString();
+                        processLine(line);
+                        outputBuffer.setLength(0);
+                    } else if (character == '\b') {
+                        // 处理退格
+                        if (outputBuffer.length() > 0) {
+                            outputBuffer.deleteCharAt(outputBuffer.length() - 1);
+                        }
+                    } else if (character == '\u001B') {
+                        // 处理ANSI转义序列
+                        String escapeSequence = readEscapeSequence(reader);
+                        processEscapeSequence(escapeSequence);
+                    } else {
+                        outputBuffer.append(character);
+                    }
+                }
+            } catch (IOException e) {
+                if (isRecording) {
+                    e.printStackTrace();
+                }
+            }
+        });
+
+        parsingThread.setDaemon(true);
+        parsingThread.start();
+    }
+
+    /**
+     * 处理每一行输出
+     */
+    private void processLine(String line) {
+        String cleanLine = cleanAnsiEscapes(line);
+
+        // 检测命令提示符
+        if (isPromptLine(cleanLine)) {
+            if (currentState.waitingForPrompt && !currentState.pendingCommand.isEmpty()) {
+                // 记录已执行的命令
+                recordCommand(currentState.pendingCommand.trim());
+                currentState.reset();
+            }
+            currentState.lastPrompt = cleanLine;
+        } else if (!cleanLine.trim().isEmpty()) {
+            // 可能是命令行
+            String potentialCommand = extractPotentialCommand(cleanLine);
+            if (potentialCommand != null) {
+                currentState.pendingCommand = potentialCommand;
+                currentState.waitingForPrompt = true;
+            }
+        }
+    }
+
+    /**
+     * 读取ANSI转义序列
+     */
+    private String readEscapeSequence(BufferedReader reader) throws IOException {
+        StringBuilder seq = new StringBuilder("\u001B");
+        int ch;
+
+        // 读取完整的转义序列
+        while ((ch = reader.read()) != -1) {
+            char c = (char) ch;
+            seq.append(c);
+
+            // 大多数转义序列以字母结束
+            if (Character.isLetter(c)) {
+                break;
+            }
+
+            // 防止无限循环
+            if (seq.length() > 20) {
+                break;
+            }
+        }
+
+        return seq.toString();
+    }
+
+    /**
+     * 处理ANSI转义序列
+     */
+    private void processEscapeSequence(String sequence) {
+        // 这里可以处理光标移动、清屏等操作
+        // 对于命令记录,主要关注的是内容而不是格式
+        if (sequence.contains("K")) {
+            // 清除行的部分内容
+            currentState.currentLine.setLength(0);
+        }
+    }
+
+    /**
+     * 清理ANSI转义字符
+     */
+    private String cleanAnsiEscapes(String input) {
+        return input.replaceAll("\\x1B\\[[0-9;]*[a-zA-Z]", "");
+    }
+
+    /**
+     * 检测是否为命令提示符行
+     */
+    private boolean isPromptLine(String line) {
+        return PROMPT_PATTERNS.matcher(line).matches() ||
+            line.endsWith("$ ") ||
+            line.endsWith("# ") ||
+            line.endsWith("> ") ||
+            line.matches(".*?\\w+@\\w+.*?[\\$#>].*");
+    }
+
+    /**
+     * 从行中提取潜在的命令
+     */
+    private String extractPotentialCommand(String line) {
+        // 移除命令提示符部分
+        String[] parts = line.split("[\\$#>]", 2);
+        if (parts.length > 1) {
+            String command = parts[1].trim();
+            if (!command.isEmpty() && isValidCommand(command)) {
+                return command;
+            }
+        }
+
+        // 如果没有找到提示符,检查是否是直接的命令
+        String trimmed = line.trim();
+        if (isValidCommand(trimmed)) {
+            return trimmed;
+        }
+
+        return null;
+    }
+
+    /**
+     * 验证是否为有效命令
+     */
+    private boolean isValidCommand(String command) {
+        if (command == null || command.isEmpty()) {
+            return false;
+        }
+
+        // 过滤掉明显不是命令的内容
+        return !command.matches(".*?\\d+\\s*$") && // 不是纯数字
+            !command.matches("^[\\s\\-=]+$") && // 不是分隔线
+            !command.startsWith("Welcome") &&   // 不是欢迎信息
+            !command.startsWith("Last login") && // 不是登录信息
+            command.length() < 1000; // 不是过长的输出
+    }
+
+    /**
+     * 记录命令
+     */
+    private void recordCommand(String command) {
+        try {
+            String username = session.getUserName();
+            String hostname = session.getHost();
+            String sessionId = Integer.toHexString(session.hashCode());
+
+            ExecutedCommand record = new ExecutedCommand(command, username, hostname, sessionId);
+            commandLog.add(record);
+
+            // 记录日志
+            System.out.println("Recorded command: " + record);
+
+            // 可以在这里添加其他处理逻辑,如写入数据库、发送审计日志等
+
+        } catch (Exception e) {
+            System.err.println("Error recording command: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 发送命令
+     */
+    public void sendCommand(String command) throws Exception {
+        if (channel != null && channel.isConnected()) {
+            OutputStream out = channel.getOutputStream();
+            out.write((command + "\n").getBytes());
+            out.flush();
+        }
+    }
+
+    /**
+     * 获取命令日志
+     */
+    public List<ExecutedCommand> getCommandLog() {
+        return new ArrayList<>(commandLog);
+    }
+
+    /**
+     * 根据条件查询命令
+     */
+    public List<ExecutedCommand> queryCommands(String userFilter, long startTime, long endTime) {
+        synchronized (commandLog) {
+            return commandLog.stream()
+                .filter(cmd -> userFilter == null || userFilter.equals(cmd.getUser()))
+                .filter(cmd -> cmd.getTimestamp() >= startTime && cmd.getTimestamp() <= endTime)
+                .collect(ArrayList::new, (list, item) -> list.add(item), (list1, list2) -> list1.addAll(list2));
+        }
+    }
+
+    /**
+     * 导出命令日志
+     */
+    public void exportToFile(String filename) throws IOException {
+        try (PrintWriter writer = new PrintWriter(new FileWriter(filename))) {
+            writer.println("Timestamp,User,Host,SessionId,Command");
+            synchronized (commandLog) {
+                for (ExecutedCommand cmd : commandLog) {
+                    writer.printf("%d,%s,%s,%s,\"%s\"%n",
+                        cmd.getTimestamp(),
+                        cmd.getUser(),
+                        cmd.getHost(),
+                        cmd.getSessionId(),
+                        cmd.getCommand().replace("\"", "\"\""));
+                }
+            }
+        }
+    }
+
+    /**
+     * 断开连接
+     */
+    public void disconnect() {
+        isRecording = false;
+
+        if (parsingThread != null) {
+            parsingThread.interrupt();
+        }
+
+        if (channel != null && channel.isConnected()) {
+            channel.disconnect();
+        }
+
+        if (session != null && session.isConnected()) {
+            session.disconnect();
+        }
+    }
+
+    /**
+     * Tee输出流,同时写入两个流
+     */
+    private static class TeeOutputStream extends OutputStream {
+        private OutputStream out1;
+        private OutputStream out2;
+
+        public TeeOutputStream(OutputStream out1, OutputStream out2) {
+            this.out1 = out1;
+            this.out2 = out2;
+        }
+
+        @Override
+        public void write(int b) throws IOException {
+            out1.write(b);
+            out2.write(b);
+        }
+
+        @Override
+        public void flush() throws IOException {
+            out1.flush();
+            out2.flush();
+        }
+
+        @Override
+        public void close() throws IOException {
+            out1.close();
+            out2.close();
+        }
+    }
+
+    /**
+     * 使用示例
+     */
+    public static void main(String[] args) {
+        TerminalParserRecorder recorder = new TerminalParserRecorder();
+
+        try {
+            recorder.connect("192.168.30.29", 22, "user", "123456+.");
+
+            // 等待连接稳定
+            Thread.sleep(2000);
+
+            // 发送测试命令
+            recorder.sendCommand("ls -la");
+            Thread.sleep(3000);
+
+            recorder.sendCommand("pwd");
+            Thread.sleep(2000);
+
+            recorder.sendCommand("history | tail -5");
+            Thread.sleep(3000);
+
+            // 查看记录的命令
+            List<ExecutedCommand> commands = recorder.getCommandLog();
+            System.out.println("\n=== Command Log ===");
+            commands.forEach(System.out::println);
+
+            // 导出到文件
+            recorder.exportToFile("command_audit.csv");
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            recorder.disconnect();
+        }
+    }
+}