diff --git a/orion-ops-dependencies/pom.xml b/orion-ops-dependencies/pom.xml
index db8caf02..96cee127 100644
--- a/orion-ops-dependencies/pom.xml
+++ b/orion-ops-dependencies/pom.xml
@@ -81,8 +81,23 @@
orion-ops-spring-boot-starter-mybatis
${revision}
+
+ com.orion.ops
+ orion-ops-spring-boot-starter-job
+ ${revision}
+
+
+ com.orion.ops
+ orion-ops-spring-boot-starter-websocket
+ ${revision}
+
-
+
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+ ${spring.boot.version}
+
diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/pom.xml b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/pom.xml
new file mode 100644
index 00000000..a6756d40
--- /dev/null
+++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/pom.xml
@@ -0,0 +1,36 @@
+
+
+
+ com.orion.ops
+ orion-ops-framework
+ ${revision}
+
+
+ 4.0.0
+ orion-ops-spring-boot-starter-websocket
+ ${project.artifactId}
+ jar
+
+ 项目 websocket 配置包
+ https://github.com/lijiahangmax/orion-ops-pro
+
+
+
+ com.orion.ops
+ orion-ops-common
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+
+
+
+
\ No newline at end of file
diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/config/OrionWebsocketAutoConfiguration.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/config/OrionWebsocketAutoConfiguration.java
new file mode 100644
index 00000000..b91cac44
--- /dev/null
+++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/config/OrionWebsocketAutoConfiguration.java
@@ -0,0 +1,44 @@
+package com.orion.ops.framework.websocket.config;
+
+import com.orion.ops.framework.websocket.core.WebsocketContainerConfig;
+import com.orion.ops.framework.websocket.interceptor.UserHandshakeInterceptor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
+
+/**
+ * websocket 配置类
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2023/6/25 19:45
+ */
+@EnableWebSocket
+@AutoConfiguration
+@EnableConfigurationProperties(WebsocketContainerConfig.class)
+public class OrionWebsocketAutoConfiguration {
+
+ /**
+ * @return websocket 缓冲区大小配置
+ */
+ @Bean
+ public ServletServerContainerFactoryBean servletServerContainerFactoryBean(WebsocketContainerConfig config) {
+ ServletServerContainerFactoryBean factory = new ServletServerContainerFactoryBean();
+ factory.setMaxBinaryMessageBufferSize(config.getBinaryBufferSize());
+ factory.setMaxTextMessageBufferSize(config.getBinaryBufferSize());
+ factory.setMaxSessionIdleTimeout(config.getSessionIdleTimeout());
+ return factory;
+ }
+
+ /**
+ * @return 用户认证拦截器 按需注入
+ */
+ @Bean
+ public HandshakeInterceptor userHandshakeInterceptor() {
+ return new UserHandshakeInterceptor();
+ }
+
+}
diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsAttr.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsAttr.java
new file mode 100644
index 00000000..2eb2ceb9
--- /dev/null
+++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsAttr.java
@@ -0,0 +1,22 @@
+package com.orion.ops.framework.websocket.constant;
+
+/**
+ * websocket 属性
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2023/6/25 20:25
+ */
+public interface WsAttr {
+
+ String USER = "user";
+
+ String UID = "uid";
+
+ String TOKEN = "token";
+
+ String READONLY = "readonly";
+
+ String CONNECTED = "connected";
+
+}
diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsCloseCode.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsCloseCode.java
new file mode 100644
index 00000000..d279abd2
--- /dev/null
+++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsCloseCode.java
@@ -0,0 +1,137 @@
+package com.orion.ops.framework.websocket.constant;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * ws服务端关闭code
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2021/6/16 15:18
+ */
+@AllArgsConstructor
+@Getter
+public enum WsCloseCode {
+
+ /**
+ * 未查询到token
+ */
+ INCORRECT_TOKEN(4100, WsCloseReason.CLOSED_CONNECTION),
+
+ /**
+ * 伪造token
+ */
+ FORGE_TOKEN(4120, WsCloseReason.CLOSED_CONNECTION),
+
+ /**
+ * token已被绑定
+ */
+ TOKEN_BIND(4125, WsCloseReason.CLOSED_CONNECTION),
+
+ /**
+ * 未知的连接
+ */
+ UNKNOWN_CONNECT(4130, WsCloseReason.CLOSED_CONNECTION),
+
+ /**
+ * 认证失败 id不匹配
+ */
+ IDENTITY_MISMATCH(4140, WsCloseReason.IDENTITY_MISMATCH),
+
+ /**
+ * 认证信息不匹配
+ */
+ VALID(4150, WsCloseReason.AUTHENTICATION_FAILURE),
+
+ /**
+ * 机器不合法
+ */
+ INVALID_MACHINE(4200, WsCloseReason.CLOSED_CONNECTION),
+
+ /**
+ * 连接远程服务器连接超时
+ */
+ CONNECTION_TIMEOUT(4201, WsCloseReason.CONNECTION_TIMEOUT),
+
+ /**
+ * 连接远程服务器失败
+ */
+ CONNECTION_FAILURE(4202, WsCloseReason.REMOTE_SERVER_UNREACHABLE),
+
+ /**
+ * 远程服务器认证失败
+ */
+ CONNECTION_AUTH_FAILURE(4205, WsCloseReason.REMOTE_SERVER_AUTHENTICATION_FAILURE),
+
+ /**
+ * 远程服务器认证出现异常
+ */
+ CONNECTION_EXCEPTION(4210, WsCloseReason.UNABLE_TO_CONNECT_REMOTE_SERVER),
+
+ /**
+ * 机器未启用
+ */
+ MACHINE_DISABLED(4215, WsCloseReason.MACHINE_DISABLED),
+
+ /**
+ * 打开shell出现异常
+ */
+ OPEN_SHELL_EXCEPTION(4220, WsCloseReason.UNABLE_TO_CONNECT_REMOTE_SERVER),
+
+ /**
+ * 打开command出现异常
+ */
+ OPEN_COMMAND_EXCEPTION(4225, WsCloseReason.UNABLE_TO_CONNECT_REMOTE_SERVER),
+
+ /**
+ * 打开sftp出现异常
+ */
+ OPEN_SFTP_EXCEPTION(4230, WsCloseReason.UNABLE_TO_CONNECT_REMOTE_SERVER),
+
+ /**
+ * 服务出现异常
+ */
+ RUNTIME_EXCEPTION(4300, WsCloseReason.CLOSED_CONNECTION),
+
+ /**
+ * 心跳结束
+ */
+ HEART_DOWN(4310, WsCloseReason.CLOSED_CONNECTION),
+
+ /**
+ * 用户关闭
+ */
+ DISCONNECT(4320, WsCloseReason.CLOSED_CONNECTION),
+
+ /**
+ * 结束
+ */
+ EOF(4330, WsCloseReason.CLOSED_CONNECTION),
+
+ /**
+ * 读取失败
+ */
+ READ_EXCEPTION(4335, WsCloseReason.CLOSED_CONNECTION),
+
+ /**
+ * 强制下线
+ */
+ FORCED_OFFLINE(4500, WsCloseReason.FORCED_OFFLINE),
+
+ ;
+
+ private final int code;
+
+ private final String reason;
+
+ public static WsCloseCode of(int code) {
+ for (WsCloseCode value : values()) {
+ if (value.code == code) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsCloseReason.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsCloseReason.java
new file mode 100644
index 00000000..33d4da48
--- /dev/null
+++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsCloseReason.java
@@ -0,0 +1,30 @@
+package com.orion.ops.framework.websocket.constant;
+
+/**
+ * ws服务端关闭reason
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2021/6/16 15:21
+ */
+public interface WsCloseReason {
+
+ String CLOSED_CONNECTION = "closed connection...";
+
+ String IDENTITY_MISMATCH = "identity mismatch...";
+
+ String AUTHENTICATION_FAILURE = "authentication failure...";
+
+ String REMOTE_SERVER_UNREACHABLE = "remote server unreachable...";
+
+ String CONNECTION_TIMEOUT = "connection timeout...";
+
+ String REMOTE_SERVER_AUTHENTICATION_FAILURE = "remote server authentication failure...";
+
+ String MACHINE_DISABLED = "machine disabled...";
+
+ String UNABLE_TO_CONNECT_REMOTE_SERVER = "unable to connect remote server...";
+
+ String FORCED_OFFLINE = "forced offline...";
+
+}
diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsProtocol.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsProtocol.java
new file mode 100644
index 00000000..7fb23051
--- /dev/null
+++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/constant/WsProtocol.java
@@ -0,0 +1,79 @@
+package com.orion.ops.framework.websocket.constant;
+
+import com.orion.lang.utils.Exceptions;
+import com.orion.lang.utils.Strings;
+import com.orion.lang.utils.Valid;
+import lombok.AllArgsConstructor;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * ws服务端响应常量
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2021/4/16 21:48
+ */
+@AllArgsConstructor
+public enum WsProtocol {
+
+ /**
+ * 正常返回
+ */
+ OK("0"),
+
+ /**
+ * 连接成功
+ */
+ CONNECTED("1"),
+
+ /**
+ * ping
+ */
+ PING("2"),
+
+ /**
+ * pong
+ */
+ PONG("3"),
+
+ /**
+ * 未知操作
+ */
+ ERROR("4"),
+
+ ;
+
+ private final String code;
+
+ /**
+ * 分隔符
+ */
+ public static final String SYMBOL = "|";
+
+ public byte[] get() {
+ return Strings.bytes(code);
+ }
+
+ public byte[] msg(String body) {
+ Valid.notNull(body);
+ return this.msg(Strings.bytes(body));
+ }
+
+ public byte[] msg(byte[] body) {
+ return this.msg(body, 0, body.length);
+ }
+
+ public byte[] msg(byte[] body, int offset, int len) {
+ Valid.notNull(body);
+ try (ByteArrayOutputStream o = new ByteArrayOutputStream()) {
+ o.write(Strings.bytes(code + SYMBOL));
+ o.write(body, offset, len);
+ return o.toByteArray();
+ } catch (IOException e) {
+ throw Exceptions.ioRuntime(e);
+ }
+ }
+
+}
diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/core/WebsocketContainerConfig.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/core/WebsocketContainerConfig.java
new file mode 100644
index 00000000..e388766b
--- /dev/null
+++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/core/WebsocketContainerConfig.java
@@ -0,0 +1,32 @@
+package com.orion.ops.framework.websocket.core;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * websocket 配置属性
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2023/6/25 19:57
+ */
+@Data
+@ConfigurationProperties("spring.websocket")
+public class WebsocketContainerConfig {
+
+ /**
+ * 二进制消息缓冲区大小 byte
+ */
+ private Integer binaryBufferSize;
+
+ /**
+ * 文本消息缓冲区大小 byte
+ */
+ private Integer textBufferSize;
+
+ /**
+ * session 最大超时时间 ms
+ */
+ private Long sessionIdleTimeout;
+
+}
diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/interceptor/UserHandshakeInterceptor.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/interceptor/UserHandshakeInterceptor.java
new file mode 100644
index 00000000..7550e8aa
--- /dev/null
+++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/interceptor/UserHandshakeInterceptor.java
@@ -0,0 +1,34 @@
+package com.orion.ops.framework.websocket.interceptor;
+
+import com.orion.ops.framework.websocket.constant.WsAttr;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import java.util.Map;
+
+/**
+ * 用户拦截器
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2023/6/25 20:16
+ */
+public class UserHandshakeInterceptor implements HandshakeInterceptor {
+
+ @Override
+ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) {
+ // TODO 获取当前用户
+ attributes.put(WsAttr.USER, 1);
+ // if (user == null){
+ // return false;
+ // }
+ return true;
+ }
+
+ @Override
+ public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
+ }
+
+}
diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/utils/WebSockets.java b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/utils/WebSockets.java
new file mode 100644
index 00000000..4201ad05
--- /dev/null
+++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/java/com/orion/ops/framework/websocket/utils/WebSockets.java
@@ -0,0 +1,110 @@
+package com.orion.ops.framework.websocket.utils;
+
+import com.orion.lang.exception.AuthenticationException;
+import com.orion.lang.exception.ConnectionRuntimeException;
+import com.orion.lang.exception.DisabledException;
+import com.orion.lang.exception.TimeoutException;
+import com.orion.lang.utils.Exceptions;
+import com.orion.lang.utils.Urls;
+import com.orion.ops.framework.websocket.constant.WsCloseCode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * websocket 工具类
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2021/6/14 0:36
+ */
+@Slf4j
+public class WebSockets {
+
+ private WebSockets() {
+ }
+
+ /**
+ * 发送消息 忽略并发报错
+ *
+ * @param session session
+ * @param message message
+ */
+ public static void sendText(WebSocketSession session, byte[] message) {
+ if (!session.isOpen()) {
+ return;
+ }
+ try {
+ // 响应
+ session.sendMessage(new TextMessage(message));
+ } catch (IllegalStateException e) {
+ // 并发异常
+ log.error("发送消息失败 {}", Exceptions.getDigest(e));
+ } catch (IOException e) {
+ throw Exceptions.ioRuntime(e);
+ }
+ }
+
+ /**
+ * 关闭会话
+ *
+ * @param session session
+ * @param code code
+ */
+ public static void close(WebSocketSession session, WsCloseCode code) {
+ if (!session.isOpen()) {
+ return;
+ }
+ try {
+ session.close(new CloseStatus(code.getCode(), code.getReason()));
+ } catch (Exception e) {
+ log.error("websocket close failure", e);
+ }
+ }
+
+ /**
+ * 获取 urlToken
+ *
+ * @param request request
+ * @return token
+ */
+ public static String getToken(ServerHttpRequest request) {
+ return Urls.getUrlSource(Objects.requireNonNull(request.getURI().toString()));
+ }
+
+ /**
+ * 获取 urlToken
+ *
+ * @param session session
+ * @return token
+ */
+ public static String getToken(WebSocketSession session) {
+ return Urls.getUrlSource(Objects.requireNonNull(session.getUri()).toString());
+ }
+
+ /**
+ * 打开 session 异常关闭
+ *
+ * @param session session
+ * @param e e
+ */
+ public static void openSessionStoreThrowClose(WebSocketSession session, Exception e) {
+ if (Exceptions.isCausedBy(e, TimeoutException.class)) {
+ close(session, WsCloseCode.CONNECTION_TIMEOUT);
+ } else if (Exceptions.isCausedBy(e, ConnectionRuntimeException.class)) {
+ close(session, WsCloseCode.CONNECTION_FAILURE);
+ } else if (Exceptions.isCausedBy(e, AuthenticationException.class)) {
+ close(session, WsCloseCode.CONNECTION_AUTH_FAILURE);
+ } else if (Exceptions.isCausedBy(e, DisabledException.class)) {
+ close(session, WsCloseCode.MACHINE_DISABLED);
+ } else {
+ close(session, WsCloseCode.CONNECTION_EXCEPTION);
+ }
+ }
+
+}
diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000..23f104b6
--- /dev/null
+++ b/orion-ops-framework/orion-ops-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.orion.ops.framework.websocket.config.OrionWebsocketAutoConfiguration
\ No newline at end of file
diff --git a/orion-ops-framework/pom.xml b/orion-ops-framework/pom.xml
index b81d870e..7b9a53f9 100644
--- a/orion-ops-framework/pom.xml
+++ b/orion-ops-framework/pom.xml
@@ -22,6 +22,8 @@
orion-ops-spring-boot-starter-swagger
orion-ops-spring-boot-starter-datasource
orion-ops-spring-boot-starter-mybatis
+ orion-ops-spring-boot-starter-job
+ orion-ops-spring-boot-starter-websocket
\ No newline at end of file
diff --git a/orion-ops-server/pom.xml b/orion-ops-server/pom.xml
index ee95d39e..39daf032 100644
--- a/orion-ops-server/pom.xml
+++ b/orion-ops-server/pom.xml
@@ -44,6 +44,14 @@
com.orion.ops
orion-ops-spring-boot-starter-mybatis
+
+ com.orion.ops
+ orion-ops-spring-boot-starter-job
+
+
+ com.orion.ops
+ orion-ops-spring-boot-starter-websocket
+
diff --git a/orion-ops-server/src/main/resources/application.yaml b/orion-ops-server/src/main/resources/application.yaml
index 39d2142d..347ce1ef 100644
--- a/orion-ops-server/src/main/resources/application.yaml
+++ b/orion-ops-server/src/main/resources/application.yaml
@@ -19,6 +19,13 @@ spring:
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER
+ websocket:
+ # 1MB
+ binary-buffer-size: 1048576
+ # 1MB
+ text-buffer-size: 1048576
+ # 30MIN
+ session-idle-timeout: 1800000
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver