Compare commits

..

8 Commits

Author SHA1 Message Date
李佳航
5113aa63bd Merge pull request #20 from lijiahangmax/dev
Dev
2024-06-06 13:00:36 +08:00
lijiahang
518fd8c839 🔖 升级版本. 2024-06-06 12:02:35 +08:00
lijiahang
a046faaa07 🔨 清理后提示. 2024-06-05 10:27:06 +08:00
lijiahangmax
dcf25392ff ⬆️ 升级 stylelint 版本. 2024-06-04 21:49:51 +08:00
lijiahang
7f24948efa 修改下载文件逻辑. 2024-06-04 20:01:05 +08:00
lijiahang
59d9739f36 优化文件下载方式. 2024-06-04 18:34:19 +08:00
lijiahang
26a6d08d96 🔨 修改 redisson 配置. 2024-06-04 11:33:09 +08:00
lijiahang
cd59c51344 升级依赖版本. 2024-06-04 10:47:32 +08:00
79 changed files with 1627 additions and 1049 deletions

View File

@@ -1,7 +1,7 @@
version: '3.3'
services:
orion-visor-service:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-service:2.0.4
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-service:2.0.5
ports:
- 1081:80
environment:
@@ -20,7 +20,7 @@ services:
- orion-visor-mysql
- orion-visor-redis
orion-visor-mysql:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-mysql:2.0.4
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-mysql:2.0.5
privileged: true
ports:
- 3307:3306
@@ -34,7 +34,7 @@ services:
- /data/orion-visor-space/docker-volumes/orion-visor-mysql/var-lib-mysql-files:/var/lib/mysql-files
- /data/orion-visor-space/docker-volumes/orion-visor-mysql/etc-mysql:/etc/mysql
orion-visor-redis:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-redis:2.0.4
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-redis:2.0.5
privileged: true
ports:
- 6380:6379

View File

@@ -1,5 +1,5 @@
#/bin/bash
version=2.0.4
version=2.0.5
cp -r ../../sql ./sql
docker build -t orion-visor-mysql:${version} .
rm -rf ./sql

View File

@@ -1,5 +1,5 @@
#/bin/bash
version=2.0.4
version=2.0.5
docker build -t orion-visor-redis:${version} .
docker tag orion-visor-redis:${version} registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-redis:${version}
docker push registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-redis:${version}

View File

@@ -1,5 +1,5 @@
#/bin/bash
version=2.0.4
version=2.0.5
mv ../../orion-visor-launch/target/orion-visor-launch.jar ./orion-visor-launch.jar
mv ../../orion-visor-ui/dist ./dist
docker build -t orion-visor-service:${version} .

View File

@@ -14,7 +14,7 @@
<url>https://github.com/lijiahangmax/orion-visor</url>
<properties>
<revision>2.0.4</revision>
<revision>2.0.5</revision>
<spring.boot.version>2.7.17</spring.boot.version>
<spring.boot.admin.version>2.7.15</spring.boot.admin.version>
<flatten.maven.plugin.version>1.5.0</flatten.maven.plugin.version>

View File

@@ -14,7 +14,7 @@ public interface AppConst extends OrionConst {
/**
* 同 ${orion.version} 迭代时候需要手动更改
*/
String VERSION = "2.0.4";
String VERSION = "2.0.5";
String ORION_VISOR = "orion-visor";

View File

@@ -97,4 +97,6 @@ public interface ErrorMessage {
String PLEASE_CHECK_HOST_SSH = "请检查主机 {} 是否存在/权限/SSH配置";
String CLIENT_ABORT = "手动中断";
}

View File

@@ -7,6 +7,7 @@ import com.orion.visor.framework.redis.configuration.config.RedissonConfig;
import com.orion.visor.framework.redis.core.lock.RedisLocker;
import com.orion.visor.framework.redis.core.utils.RedisUtils;
import org.redisson.api.RedissonClient;
import org.redisson.config.SingleServerConfig;
import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
@@ -58,6 +59,9 @@ public class OrionRedisAutoConfiguration {
return config -> {
config.setThreads(redissonConfig.getThreads());
config.setNettyThreads(redissonConfig.getNettyThreads());
// 单机配置
SingleServerConfig single = config.useSingleServer();
single.setConnectionMinimumIdleSize(redissonConfig.getMinimumIdleSize());
};
}

View File

@@ -24,9 +24,15 @@ public class RedissonConfig {
*/
private Integer nettyThreads;
/**
* 最小空闲连接数
*/
private Integer minimumIdleSize;
public RedissonConfig() {
this.threads = 16;
this.nettyThreads = 16;
this.minimumIdleSize = 16;
}
}

View File

@@ -18,6 +18,12 @@
"type": "java.lang.Integer",
"description": "netty 线程数.",
"defaultValue": "16"
},
{
"name": "spring.redisson.minimum-idle-size",
"type": "java.lang.Integer",
"description": "最小空闲连接数.",
"defaultValue": "16"
}
]
}

View File

@@ -14,6 +14,7 @@ spring:
redisson:
threads: 2
netty-threads: 2
minimum-idle-size: 2
mybatis-plus:
configuration:

View File

@@ -24,6 +24,7 @@ spring:
redisson:
threads: 4
netty-threads: 4
minimum-idle-size: 4
quartz:
properties:
org:

View File

@@ -19,6 +19,9 @@ spring:
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER
async:
# 异步请求时间 30min
request-timeout: 1800000
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver

View File

@@ -1,5 +1,5 @@
### 分页查询 SFTP 操作日志
POST {{baseUrl}}/asset/host-sftp-log/query
POST {{baseUrl}}/asset/host-sftp/query-log
Content-Type: application/json
Authorization: {{token}}
@@ -10,8 +10,12 @@ Authorization: {{token}}
### 删除 SFTP 操作日志
DELETE {{baseUrl}}/asset/host-sftp-log/delete?idList=1,2,3
DELETE {{baseUrl}}/asset/host-sftp/delete-log?idList=1,2,3
Authorization: {{token}}
### 下载文件
GET {{baseUrl}}/asset/host-sftp/download?channelId=123&transferToken=123
###

View File

@@ -5,11 +5,12 @@ import com.orion.visor.framework.biz.operator.log.core.annotation.OperatorLog;
import com.orion.visor.framework.common.validator.group.Page;
import com.orion.visor.framework.log.core.annotation.IgnoreLog;
import com.orion.visor.framework.log.core.enums.IgnoreLogMode;
import com.orion.visor.framework.web.core.annotation.IgnoreWrapper;
import com.orion.visor.framework.web.core.annotation.RestWrapper;
import com.orion.visor.module.asset.define.operator.HostTerminalOperatorType;
import com.orion.visor.module.asset.entity.request.host.HostSftpLogQueryRequest;
import com.orion.visor.module.asset.entity.vo.HostSftpLogVO;
import com.orion.visor.module.asset.service.HostSftpLogService;
import com.orion.visor.module.asset.service.HostSftpService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -17,44 +18,60 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* SFTP 操作日志服务 api
* SFTP 操作服务 api
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023-12-26 22:09
*/
@Tag(name = "asset - SFTP 操作日志服务")
@Tag(name = "asset - SFTP 操作服务")
@Slf4j
@Validated
@RestWrapper
@RestController
@RequestMapping("/asset/host-sftp-log")
@RequestMapping("/asset/host-sftp")
@SuppressWarnings({"ELValidationInJSP", "SpringElInspection"})
public class HostSftpLogController {
@Resource
private HostSftpLogService hostSftpLogService;
private HostSftpService hostSftpService;
@IgnoreLog(IgnoreLogMode.RET)
@PostMapping("/query")
@PostMapping("/query-log")
@Operation(summary = "分页查询 SFTP 操作日志")
@PreAuthorize("@ss.hasAnyPermission('infra:operator-log:query', 'asset:host-sftp-log:management:query')")
public DataGrid<HostSftpLogVO> getHostSftpLogPage(@Validated(Page.class) @RequestBody HostSftpLogQueryRequest request) {
return hostSftpLogService.getHostSftpLogPage(request);
return hostSftpService.getHostSftpLogPage(request);
}
@OperatorLog(HostTerminalOperatorType.DELETE_SFTP_LOG)
@DeleteMapping("/delete")
@DeleteMapping("/delete-log")
@Operation(summary = "删除 SFTP 操作日志")
@Parameter(name = "idList", description = "idList", required = true)
@PreAuthorize("@ss.hasAnyPermission('infra:operator-log:delete', 'asset:host-sftp-log:management:delete')")
public Integer deleteHostSftpLog(@RequestParam("idList") List<Long> idList) {
return hostSftpLogService.deleteHostSftpLog(idList);
return hostSftpService.deleteHostSftpLog(idList);
}
@PermitAll
@IgnoreWrapper
@IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/download")
@Operation(summary = "下载文件")
@Parameter(name = "channelId", description = "channelId", required = true)
@Parameter(name = "transferToken", description = "transferToken", required = true)
public StreamingResponseBody downloadWithTransferToken(@RequestParam("channelId") String channelId,
@RequestParam("transferToken") String transferToken,
HttpServletResponse response) {
return hostSftpService.downloadWithTransferToken(channelId, transferToken, response);
}
}

View File

@@ -2,7 +2,7 @@ package com.orion.visor.module.asset.handler.host.terminal;
import com.orion.visor.module.asset.define.AssetThreadPools;
import com.orion.visor.module.asset.handler.host.terminal.enums.InputTypeEnum;
import com.orion.visor.module.asset.handler.host.terminal.manager.TerminalManager;
import com.orion.visor.module.asset.handler.host.terminal.manager.HostTerminalManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
@@ -24,7 +24,7 @@ import javax.annotation.Resource;
public class TerminalMessageDispatcher extends AbstractWebSocketHandler {
@Resource
private TerminalManager terminalManager;
private HostTerminalManager hostTerminalManager;
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
@@ -65,7 +65,7 @@ public class TerminalMessageDispatcher extends AbstractWebSocketHandler {
String id = session.getId();
log.info("TerminalMessageDispatcher-afterConnectionClosed id: {}, code: {}, reason: {}", id, status.getCode(), status.getReason());
// 关闭会话
terminalManager.closeSession(id);
hostTerminalManager.closeSession(id);
}
}

View File

@@ -7,7 +7,7 @@ import com.orion.visor.framework.biz.operator.log.core.utils.OperatorLogs;
import com.orion.visor.framework.common.constant.ErrorMessage;
import com.orion.visor.framework.websocket.core.utils.WebSockets;
import com.orion.visor.module.asset.handler.host.terminal.enums.OutputTypeEnum;
import com.orion.visor.module.asset.handler.host.terminal.manager.TerminalManager;
import com.orion.visor.module.asset.handler.host.terminal.manager.HostTerminalManager;
import com.orion.visor.module.asset.handler.host.terminal.model.TerminalBasePayload;
import com.orion.visor.module.asset.handler.host.terminal.model.TerminalConfig;
import com.orion.visor.module.asset.handler.host.terminal.session.ITerminalSession;
@@ -27,7 +27,7 @@ import java.util.Map;
public abstract class AbstractTerminalHandler<T extends TerminalBasePayload> implements ITerminalHandler<T> {
@Resource
protected TerminalManager terminalManager;
protected HostTerminalManager hostTerminalManager;
@Resource
private OperatorLogFrameworkService operatorLogFrameworkService;
@@ -75,7 +75,7 @@ public abstract class AbstractTerminalHandler<T extends TerminalBasePayload> imp
String channelId = channel.getId();
String sessionId = payload.getSessionId();
// 获取会话并且设置参数
ITerminalSession session = terminalManager.getSession(channelId, sessionId);
ITerminalSession session = hostTerminalManager.getSession(channelId, sessionId);
if (session != null) {
TerminalConfig config = session.getConfig();
extra.put(OperatorLogs.HOST_ID, config.getHostId());

View File

@@ -30,7 +30,7 @@ public class SftpChangeModHandler extends AbstractTerminalHandler<SftpChangeModR
long startTime = System.currentTimeMillis();
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
ISftpSession session = hostTerminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
Integer mod = payload.getMod();
log.info("SftpChangeModHandler-handle start sessionId: {}, path: {}, mod: {}", sessionId, path, mod);

View File

@@ -30,7 +30,7 @@ public class SftpDownloadFlatDirectoryHandler extends AbstractTerminalHandler<Sf
public void handle(WebSocketSession channel, SftpDownloadFlatDirectoryRequest payload) {
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
ISftpSession session = hostTerminalManager.getSession(channel.getId(), sessionId);
String[] paths = payload.getPath().split("\\|");
log.info("SftpDownloadFlatDirectoryHandler-handle start sessionId: {}, paths: {}", sessionId, Arrays.toString(paths));
Exception ex = null;

View File

@@ -25,7 +25,7 @@ public class SftpGetContentHandler extends AbstractTerminalHandler<SftpBaseReque
public void handle(WebSocketSession channel, SftpBaseRequest payload) {
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
ISftpSession session = hostTerminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
log.info("SftpGetContentHandler-handle start sessionId: {}, path: {}", sessionId, path);
String content = Const.EMPTY;

View File

@@ -31,7 +31,7 @@ public class SftpListHandler extends AbstractTerminalHandler<SftpListRequest> {
public void handle(WebSocketSession channel, SftpListRequest payload) {
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
ISftpSession session = hostTerminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
log.info("SftpListHandler-handle start sessionId: {}, path: {}", sessionId, path);
Exception ex = null;

View File

@@ -30,7 +30,7 @@ public class SftpMakeDirectoryHandler extends AbstractTerminalHandler<SftpBaseRe
long startTime = System.currentTimeMillis();
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
ISftpSession session = hostTerminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
log.info("SftpMakeDirectoryHandler-handle start sessionId: {}, path: {}", sessionId, path);
Exception ex = null;

View File

@@ -30,7 +30,7 @@ public class SftpMoveHandler extends AbstractTerminalHandler<SftpMoveRequest> {
long startTime = System.currentTimeMillis();
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
ISftpSession session = hostTerminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
String target = payload.getTarget();
log.info("SftpMoveHandler-handle start sessionId: {}, path: {}, target: {}", sessionId, path, target);

View File

@@ -31,7 +31,7 @@ public class SftpRemoveHandler extends AbstractTerminalHandler<SftpBaseRequest>
long startTime = System.currentTimeMillis();
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
ISftpSession session = hostTerminalManager.getSession(channel.getId(), sessionId);
String[] paths = payload.getPath().split("\\|");
log.info("SftpRemoveHandler-handle start sessionId: {}, path: {}", sessionId, Arrays.toString(paths));
Exception ex = null;

View File

@@ -30,7 +30,7 @@ public class SftpSetContentHandler extends AbstractTerminalHandler<SftpSetConten
long startTime = System.currentTimeMillis();
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
ISftpSession session = hostTerminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
log.info("SftpSetContentHandler-handle start sessionId: {}, path: {}", sessionId, path);
Exception ex = null;

View File

@@ -30,7 +30,7 @@ public class SftpTouchHandler extends AbstractTerminalHandler<SftpBaseRequest> {
long startTime = System.currentTimeMillis();
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
ISftpSession session = hostTerminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
log.info("SftpTouchHandler-handle start sessionId: {}, path: {}", sessionId, path);
Exception ex = null;

View File

@@ -30,7 +30,7 @@ public class SftpTruncateHandler extends AbstractTerminalHandler<SftpBaseRequest
long startTime = System.currentTimeMillis();
// 获取会话
String sessionId = payload.getSessionId();
ISftpSession session = terminalManager.getSession(channel.getId(), sessionId);
ISftpSession session = hostTerminalManager.getSession(channel.getId(), sessionId);
String path = payload.getPath();
log.info("SftpTruncateHandler-handle start sessionId: {}, path: {}", sessionId, path);
Exception ex = null;

View File

@@ -20,7 +20,7 @@ public class SshInputHandler extends AbstractTerminalHandler<SshInputRequest> {
@Override
public void handle(WebSocketSession channel, SshInputRequest payload) {
// 获取会话
ISshSession session = terminalManager.getSession(channel.getId(), payload.getSessionId());
ISshSession session = hostTerminalManager.getSession(channel.getId(), payload.getSessionId());
// 处理输入
session.write(payload.getCommand());
}

View File

@@ -20,7 +20,7 @@ public class SshResizeHandler extends AbstractTerminalHandler<SshResizeRequest>
@Override
public void handle(WebSocketSession channel, SshResizeRequest payload) {
// 获取会话
ISshSession session = terminalManager.getSession(channel.getId(), payload.getSessionId());
ISshSession session = hostTerminalManager.getSession(channel.getId(), payload.getSessionId());
// 修改大小
session.resize(payload.getCols(), payload.getRows());
}

View File

@@ -117,7 +117,7 @@ public class TerminalCheckHandler extends AbstractTerminalHandler<TerminalCheckR
* @return 是否存在
*/
private boolean checkSession(WebSocketSession channel, TerminalCheckRequest payload) {
ITerminalSession session = terminalManager.getSession(channel.getId(), payload.getSessionId());
ITerminalSession session = hostTerminalManager.getSession(channel.getId(), payload.getSessionId());
if (session != null) {
this.sendCheckFailedMessage(channel, payload, ErrorMessage.SESSION_PRESENT);
return true;

View File

@@ -20,7 +20,7 @@ public class TerminalCloseHandler extends AbstractTerminalHandler<TerminalBasePa
public void handle(WebSocketSession channel, TerminalBasePayload payload) {
log.info("TerminalCloseHandler-handle start sessionId: {}", payload.getSessionId());
// 关闭会话
terminalManager.closeSession(channel.getId(), payload.getSessionId());
hostTerminalManager.closeSession(channel.getId(), payload.getSessionId());
}
}

View File

@@ -72,7 +72,7 @@ public class TerminalConnectHandler extends AbstractTerminalHandler<TerminalConn
// 连接主机
ITerminalSession session = this.connect(sessionId, connect, channel, payload);
// 添加会话到 manager
terminalManager.addSession(session);
hostTerminalManager.addSession(session);
} catch (Exception e) {
ex = e;
// 修改连接状态为失败

View File

@@ -26,7 +26,7 @@ public class TerminalPingHandler extends AbstractTerminalHandler<TerminalBasePay
// 发送 pong
this.send(channel, OutputTypeEnum.PONG.getType());
// 活跃 terminal
Map<String, ITerminalSession> sessions = terminalManager.getSession(channel.getId());
Map<String, ITerminalSession> sessions = hostTerminalManager.getSession(channel.getId());
if (!Maps.isEmpty(sessions)) {
for (ITerminalSession session : sessions.values()) {
session.keepAlive();

View File

@@ -10,14 +10,14 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 终端管理器
* 主机终端管理器
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/1/3 11:35
*/
@Component
public class TerminalManager {
public class HostTerminalManager {
/**
* 会话存储器

View File

@@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSON;
import com.orion.lang.utils.io.Streams;
import com.orion.visor.module.asset.handler.host.transfer.handler.ITransferHandler;
import com.orion.visor.module.asset.handler.host.transfer.handler.TransferHandler;
import com.orion.visor.module.asset.handler.host.transfer.manager.HostTransferManager;
import com.orion.visor.module.asset.handler.host.transfer.model.TransferOperatorRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@@ -13,7 +14,7 @@ import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Resource;
/**
* sftp 传输消息处理器
@@ -26,24 +27,30 @@ import java.util.concurrent.ConcurrentHashMap;
@Component
public class TransferMessageDispatcher extends AbstractWebSocketHandler {
private final ConcurrentHashMap<String, ITransferHandler> handlers = new ConcurrentHashMap<>();
@Resource
private HostTransferManager hostTransferManager;
@Override
public void afterConnectionEstablished(WebSocketSession session) {
log.info("TransferMessageHandler-afterConnectionEstablished id: {}", session.getId());
// 添加处理器
hostTransferManager.putHandler(session.getId(), new TransferHandler(session));
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
// 获取处理器
ITransferHandler handler = handlers.computeIfAbsent(session.getId(), s -> new TransferHandler(session));
ITransferHandler handler = hostTransferManager.getHandler(session.getId());
// 处理消息
handler.handleMessage(JSON.parseObject(message.getPayload(), TransferOperatorRequest.class));
}
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
handlers.get(session.getId()).putContent(message.getPayload().array());
}
@Override
public void afterConnectionEstablished(WebSocketSession session) {
log.info("TransferMessageHandler-afterConnectionEstablished id: {}", session.getId());
// 获取处理器
ITransferHandler handler = hostTransferManager.getHandler(session.getId());
// 添加数据
handler.putContent(message.getPayload().array());
}
@Override
@@ -56,7 +63,7 @@ public class TransferMessageDispatcher extends AbstractWebSocketHandler {
String id = session.getId();
log.info("TransferMessageHandler-afterConnectionClosed id: {}, code: {}, reason: {}", id, status.getCode(), status.getReason());
// 关闭会话
Streams.close(handlers.remove(id));
Streams.close(hostTransferManager.removeHandler(id));
}
}

View File

@@ -30,9 +30,9 @@ public enum TransferOperatorType {
UPLOAD_ERROR(TransferOperatorType.UPLOAD, "uploadError"),
/**
* 开始下载
* 初始化下载
*/
DOWNLOAD_START(TransferOperatorType.DOWNLOAD, "downloadStart"),
DOWNLOAD_INIT(TransferOperatorType.DOWNLOAD, "downloadInit"),
/**
* 中断下载
@@ -45,7 +45,7 @@ public enum TransferOperatorType {
public static final String DOWNLOAD = "DOWNLOAD";
private final String operator;
private final String kind;
private final String type;

View File

@@ -34,6 +34,16 @@ public enum TransferReceiverType {
*/
UPLOAD_ERROR("uploadError"),
/**
* 开始下载
*/
DOWNLOAD_START("downloadStart"),
/**
* 下载进度
*/
DOWNLOAD_PROGRESS("downloadProgress"),
/**
* 下载完成
*/

View File

@@ -2,6 +2,9 @@ package com.orion.visor.module.asset.handler.host.transfer.handler;
import com.orion.lang.able.SafeCloseable;
import com.orion.visor.module.asset.handler.host.transfer.model.TransferOperatorRequest;
import com.orion.visor.module.asset.handler.host.transfer.session.IDownloadSession;
import java.util.Map;
/**
* 传输处理器定义
@@ -26,4 +29,11 @@ public interface ITransferHandler extends SafeCloseable {
*/
void putContent(byte[] content);
/**
* 获取 token sessions
*
* @return token sessions
*/
Map<String, IDownloadSession> getTokenSessions();
}

View File

@@ -1,5 +1,6 @@
package com.orion.visor.module.asset.handler.host.transfer.handler;
import com.orion.lang.id.UUIds;
import com.orion.lang.utils.Exceptions;
import com.orion.lang.utils.io.Streams;
import com.orion.net.host.SessionStore;
@@ -14,6 +15,7 @@ import com.orion.visor.module.asset.handler.host.transfer.model.TransferOperator
import com.orion.visor.module.asset.handler.host.transfer.session.*;
import com.orion.visor.module.asset.handler.host.transfer.utils.TransferUtils;
import com.orion.visor.module.asset.service.HostTerminalService;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.WebSocketSession;
@@ -45,10 +47,14 @@ public class TransferHandler implements ITransferHandler {
*/
private final ConcurrentHashMap<String, ITransferHostSession> sessions;
@Getter
private final ConcurrentHashMap<String, IDownloadSession> tokenSessions;
public TransferHandler(WebSocketSession channel) {
this.channel = channel;
this.userId = WebSockets.getAttr(channel, ExtraFieldConst.USER_ID);
this.sessions = new ConcurrentHashMap<>();
this.tokenSessions = new ConcurrentHashMap<>();
}
@Override
@@ -73,9 +79,11 @@ public class TransferHandler implements ITransferHandler {
// 上传失败
((IUploadSession) currentSession).uploadError();
break;
case DOWNLOAD_START:
case DOWNLOAD_INIT:
// 开始下载
((IDownloadSession) currentSession).startDownload(payload.getPath());
String token = UUIds.random32();
tokenSessions.put(token, (IDownloadSession) currentSession);
((IDownloadSession) currentSession).downloadInit(payload.getPath(), token);
break;
case DOWNLOAD_ABORT:
// 中断下载
@@ -100,7 +108,7 @@ public class TransferHandler implements ITransferHandler {
*/
private boolean getAndInitSession(TransferOperatorRequest payload, TransferOperatorType type) {
Long hostId = payload.getHostId();
String sessionKey = hostId + "_" + type.getOperator();
String sessionKey = hostId + "_" + type.getKind();
try {
// 获取会话
ITransferHostSession session = sessions.get(sessionKey);
@@ -109,10 +117,10 @@ public class TransferHandler implements ITransferHandler {
HostTerminalConnectDTO connectInfo = hostTerminalService.getTerminalConnectInfo(this.userId, hostId);
SessionStore sessionStore = hostTerminalService.openSessionStore(connectInfo);
// 打开会话并初始化
if (TransferOperatorType.UPLOAD.equals(type.getOperator())) {
if (TransferOperatorType.UPLOAD.equals(type.getKind())) {
// 上传操作
session = new UploadSession(connectInfo, sessionStore, this.channel);
} else if (TransferOperatorType.DOWNLOAD.equals(type.getOperator())) {
} else if (TransferOperatorType.DOWNLOAD.equals(type.getKind())) {
// 下载操作
session = new DownloadSession(connectInfo, sessionStore, this.channel);
} else {
@@ -136,6 +144,7 @@ public class TransferHandler implements ITransferHandler {
public void close() {
log.info("TransferHandler.close channelId: {}", channel.getId());
sessions.values().forEach(Streams::close);
tokenSessions.clear();
}
}

View File

@@ -0,0 +1,50 @@
package com.orion.visor.module.asset.handler.host.transfer.manager;
import com.orion.visor.module.asset.handler.host.transfer.handler.ITransferHandler;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
/**
* 主机传输管理器
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/6/4 17:58
*/
@Component
public class HostTransferManager {
private final ConcurrentHashMap<String, ITransferHandler> handlers = new ConcurrentHashMap<>();
/**
* 添加处理器
*
* @param id id
* @param handler handler
*/
public void putHandler(String id, ITransferHandler handler) {
handlers.put(id, handler);
}
/**
* 获取处理器
*
* @param id id
* @return handler
*/
public ITransferHandler getHandler(String id) {
return handlers.get(id);
}
/**
* 删除处理器
*
* @param id id
* @return handler
*/
public ITransferHandler removeHandler(String id) {
return handlers.remove(id);
}
}

View File

@@ -20,7 +20,7 @@ import lombok.NoArgsConstructor;
@Schema(name = "FileOperatorRequest", description = "文件操作请求 实体对象")
public class TransferOperatorRequest {
@Schema(description = "上传路径")
@Schema(description = "文件路径")
private String path;
@Schema(description = "type")

View File

@@ -20,6 +20,9 @@ import lombok.NoArgsConstructor;
@Schema(name = "FileOperatorResponse", description = "文件操作响应 实体对象")
public class TransferOperatorResponse {
@Schema(description = "channelId")
private String channelId;
@Schema(description = "type")
private String type;
@@ -29,6 +32,12 @@ public class TransferOperatorResponse {
@Schema(description = "是否成功")
private Boolean success;
@Schema(description = "传输的大小")
private Integer currentSize;
@Schema(description = "transferToken")
private String transferToken;
@Schema(description = "消息")
private String msg;

View File

@@ -1,5 +1,6 @@
package com.orion.visor.module.asset.handler.host.transfer.session;
import com.orion.lang.define.wrapper.Ref;
import com.orion.lang.utils.Threads;
import com.orion.lang.utils.Valid;
import com.orion.lang.utils.io.Streams;
@@ -12,11 +13,13 @@ import com.orion.visor.module.asset.define.operator.HostTerminalOperatorType;
import com.orion.visor.module.asset.entity.dto.HostTerminalConnectDTO;
import com.orion.visor.module.asset.handler.host.transfer.enums.TransferReceiverType;
import com.orion.visor.module.asset.handler.host.transfer.utils.TransferUtils;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* 下载会话实现
@@ -28,6 +31,9 @@ import java.io.InputStream;
@Slf4j
public class DownloadSession extends TransferHostSession implements IDownloadSession {
@Getter
private String path;
private InputStream inputStream;
public DownloadSession(HostTerminalConnectDTO connectInfo, SessionStore sessionStore, WebSocketSession channel) {
@@ -35,7 +41,8 @@ public class DownloadSession extends TransferHostSession implements IDownloadSes
}
@Override
public void startDownload(String path) {
public void downloadInit(String path, String token) {
this.path = path;
String channelId = channel.getId();
try {
log.info("DownloadSession.startDownload open start channelId: {}, path: {}", channelId, path);
@@ -54,39 +61,17 @@ public class DownloadSession extends TransferHostSession implements IDownloadSes
}
// 打开输入流
this.inputStream = executor.openInputStream(path);
// 响应开始下载
TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_START, null, e -> {
e.setChannelId(channelId);
e.setTransferToken(token);
});
log.info("DownloadSession.startDownload open success channelId: {}, path: {}", channelId, path);
} catch (Exception e) {
log.error("DownloadSession.startDownload open error channelId: {}, path: {}", channelId, path, e);
// 响应结果
// 响应下载失败
TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_ERROR, e);
return;
}
// 异步读取文件内容
AssetThreadPools.TERMINAL_OPERATOR.execute(() -> {
Exception ex = null;
try {
byte[] buffer = new byte[Const.BUFFER_KB_32];
int len;
// 响应文件内容
while (this.inputStream != null && (len = this.inputStream.read(buffer)) != -1) {
this.channel.sendMessage(new BinaryMessage(buffer, 0, len, true));
}
log.info("DownloadSession.download finish channelId: {}, path: {}", channelId, path);
} catch (Exception e) {
log.error("DownloadSession.download error channelId: {}, path: {}", channelId, path, e);
ex = e;
}
// 关闭等待 jsch 内部处理
Threads.sleep(100);
this.closeStream();
Threads.sleep(100);
// 响应结果
if (ex == null) {
TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_FINISH, null);
} else {
TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_ERROR, ex);
}
});
}
@Override
@@ -96,9 +81,72 @@ public class DownloadSession extends TransferHostSession implements IDownloadSes
this.closeStream();
}
@Override
public void writeTo(OutputStream outputStream) {
String channelId = channel.getId();
Ref<Exception> ex = new Ref<>();
try {
byte[] buffer = new byte[Const.BUFFER_KB_32];
int len;
int i = 0;
int size = 0;
// 响应文件内容
while (this.inputStream != null && (len = this.inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
size += len;
// 不要每次都 flush 和 send > 1mb
if (i == 32) {
i = 0;
}
// 首次触发
if (i == 0) {
this.flushAndSendProgress(outputStream, size);
}
i++;
}
// 最后一次也要 flush
if (i != 0) {
this.flushAndSendProgress(outputStream, size);
}
log.info("DownloadSession.download finish channelId: {}, path: {}", channelId, path);
} catch (Exception e) {
log.error("DownloadSession.download error channelId: {}, path: {}", channelId, path, e);
ex.set(e);
}
// 异步关闭
AssetThreadPools.TERMINAL_OPERATOR.execute(() -> {
// 关闭等待 jsch 内部处理
Threads.sleep(100);
this.closeStream();
Threads.sleep(100);
// 响应结果
Exception e = ex.getValue();
if (e == null) {
TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_FINISH, null);
} else {
TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_ERROR, e);
}
});
}
/**
* 刷流 & 发送进度
*
* @param outputStream outputStream
* @param size size
* @throws IOException IOException
*/
private void flushAndSendProgress(OutputStream outputStream, int size) throws IOException {
// flush
outputStream.flush();
// send
TransferUtils.sendMessage(this.channel, TransferReceiverType.DOWNLOAD_PROGRESS, null, e -> e.setCurrentSize(size));
}
@Override
protected void closeStream() {
// 关闭 inputStream 可能会被阻塞 ???...??? 只能关闭 executor
this.path = null;
Streams.close(this.executor);
this.executor = null;
this.inputStream = null;

View File

@@ -1,5 +1,7 @@
package com.orion.visor.module.asset.handler.host.transfer.session;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
/**
* 下载会话定义
*
@@ -7,18 +9,26 @@ package com.orion.visor.module.asset.handler.host.transfer.session;
* @version 1.0.0
* @since 2024/2/22 22:25
*/
public interface IDownloadSession {
public interface IDownloadSession extends StreamingResponseBody {
/**
* 开始下载
* 初始化下载
*
* @param path path
* @param path path
* @param token token
*/
void startDownload(String path);
void downloadInit(String path, String token);
/**
* 停止下载
*/
void abortDownload();
/**
* 获取下载文件路径
*
* @return path
*/
String getPath();
}

View File

@@ -6,8 +6,11 @@ import com.orion.visor.framework.common.constant.ErrorMessage;
import com.orion.visor.framework.websocket.core.utils.WebSockets;
import com.orion.visor.module.asset.handler.host.transfer.enums.TransferReceiverType;
import com.orion.visor.module.asset.handler.host.transfer.model.TransferOperatorResponse;
import org.apache.catalina.connector.ClientAbortException;
import org.springframework.web.socket.WebSocketSession;
import java.util.function.Consumer;
/**
* 传输工具类
*
@@ -28,11 +31,26 @@ public class TransferUtils {
* @param ex ex
*/
public static void sendMessage(WebSocketSession channel, TransferReceiverType type, Exception ex) {
sendMessage(channel, type, ex, null);
}
/**
* 发送消息
*
* @param channel channel
* @param type type
* @param ex ex
* @param filler filler
*/
public static void sendMessage(WebSocketSession channel, TransferReceiverType type, Exception ex, Consumer<TransferOperatorResponse> filler) {
TransferOperatorResponse resp = TransferOperatorResponse.builder()
.type(type.getType())
.success(ex == null)
.msg(TransferUtils.getErrorMessage(ex))
.build();
if (filler != null) {
filler.accept(resp);
}
WebSockets.sendText(channel, JSON.toJSONString(resp));
}
@@ -45,9 +63,10 @@ public class TransferUtils {
public static String getErrorMessage(Exception ex) {
if (ex == null) {
return null;
}
if (ex instanceof InvalidArgumentException) {
} else if (ex instanceof InvalidArgumentException) {
return ex.getMessage();
} else if (ex instanceof ClientAbortException) {
return ErrorMessage.CLIENT_ABORT;
}
return ErrorMessage.OPERATE_ERROR;
}

View File

@@ -3,17 +3,19 @@ package com.orion.visor.module.asset.service;
import com.orion.lang.define.wrapper.DataGrid;
import com.orion.visor.module.asset.entity.request.host.HostSftpLogQueryRequest;
import com.orion.visor.module.asset.entity.vo.HostSftpLogVO;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* SFTP 操作日志 服务类
* SFTP 操作 服务类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023-12-26 22:09
*/
public interface HostSftpLogService {
public interface HostSftpService {
/**
* 分页查询 SFTP 操作日志
@@ -31,4 +33,16 @@ public interface HostSftpLogService {
*/
Integer deleteHostSftpLog(List<Long> idList);
/**
* 通过 transferToken 下载
*
* @param channelId channelId
* @param transferToken transferToken
* @param response response
* @return body
*/
StreamingResponseBody downloadWithTransferToken(String channelId,
String transferToken,
HttpServletResponse response);
}

View File

@@ -19,7 +19,7 @@ import com.orion.visor.module.asset.entity.request.host.HostConnectLogQueryReque
import com.orion.visor.module.asset.entity.vo.HostConnectLogVO;
import com.orion.visor.module.asset.enums.HostConnectStatusEnum;
import com.orion.visor.module.asset.enums.HostConnectTypeEnum;
import com.orion.visor.module.asset.handler.host.terminal.manager.TerminalManager;
import com.orion.visor.module.asset.handler.host.terminal.manager.HostTerminalManager;
import com.orion.visor.module.asset.handler.host.terminal.model.TerminalConfig;
import com.orion.visor.module.asset.handler.host.terminal.session.ITerminalSession;
import com.orion.visor.module.asset.service.HostConnectLogService;
@@ -49,7 +49,7 @@ public class HostConnectLogServiceImpl implements HostConnectLogService {
private HostConnectLogDAO hostConnectLogDAO;
@Resource
private TerminalManager terminalManager;
private HostTerminalManager hostTerminalManager;
@Override
public Long create(HostConnectTypeEnum type, HostConnectLogCreateRequest request) {
@@ -84,7 +84,7 @@ public class HostConnectLogServiceImpl implements HostConnectLogService {
@Override
public List<HostConnectLogVO> getHostConnectSessions(HostConnectLogQueryRequest request) {
// 查询全部
List<Long> idList = terminalManager.getChannelSessions()
List<Long> idList = hostTerminalManager.getChannelSessions()
.values()
.stream()
.map(ConcurrentHashMap::values)
@@ -204,7 +204,7 @@ public class HostConnectLogServiceImpl implements HostConnectLogService {
OperatorLogs.add(OperatorLogs.HOST_NAME, record.getHostName());
// 获取会话
HostConnectLogExtraDTO extra = JSON.parseObject(record.getExtraInfo(), HostConnectLogExtraDTO.class);
ITerminalSession session = terminalManager.getSession(extra.getChannelId(), extra.getSessionId());
ITerminalSession session = hostTerminalManager.getSession(extra.getChannelId(), extra.getSessionId());
if (session != null) {
// 关闭会话
session.forceOffline();

View File

@@ -2,26 +2,37 @@ package com.orion.visor.module.asset.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.orion.lang.constant.StandardContentType;
import com.orion.lang.define.wrapper.DataGrid;
import com.orion.lang.utils.Arrays1;
import com.orion.lang.utils.Strings;
import com.orion.lang.utils.io.Files1;
import com.orion.visor.framework.biz.operator.log.core.utils.OperatorLogs;
import com.orion.visor.framework.common.constant.Const;
import com.orion.visor.framework.common.constant.ErrorMessage;
import com.orion.visor.framework.common.constant.ExtraFieldConst;
import com.orion.visor.module.asset.convert.HostSftpLogConvert;
import com.orion.visor.module.asset.define.operator.HostTerminalOperatorType;
import com.orion.visor.module.asset.entity.request.host.HostSftpLogQueryRequest;
import com.orion.visor.module.asset.entity.vo.HostSftpLogVO;
import com.orion.visor.module.asset.service.HostSftpLogService;
import com.orion.visor.module.asset.handler.host.transfer.handler.ITransferHandler;
import com.orion.visor.module.asset.handler.host.transfer.manager.HostTransferManager;
import com.orion.visor.module.asset.handler.host.transfer.session.IDownloadSession;
import com.orion.visor.module.asset.service.HostSftpService;
import com.orion.visor.module.infra.api.OperatorLogApi;
import com.orion.visor.module.infra.entity.dto.operator.OperatorLogQueryDTO;
import com.orion.web.servlet.web.Servlets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Optional;
/**
* SFTP 操作日志 服务实现类
* SFTP 操作 服务实现类
*
* @author Jiahang Li
* @version 1.0.0
@@ -29,11 +40,14 @@ import java.util.List;
*/
@Slf4j
@Service
public class HostSftpLogServiceImpl implements HostSftpLogService {
public class HostSftpServiceImpl implements HostSftpService {
@Resource
private OperatorLogApi operatorLogApi;
@Resource
private HostTransferManager hostTransferManager;
@Override
public DataGrid<HostSftpLogVO> getHostSftpLogPage(HostSftpLogQueryRequest request) {
// 查询
@@ -62,6 +76,25 @@ public class HostSftpLogServiceImpl implements HostSftpLogService {
return effect;
}
@Override
public StreamingResponseBody downloadWithTransferToken(String channelId, String transferToken, HttpServletResponse response) {
// 获取会话
IDownloadSession session = Optional.ofNullable(channelId)
.map(hostTransferManager::getHandler)
.map(ITransferHandler::getTokenSessions)
.map(s -> s.remove(transferToken))
.orElse(null);
// 响应会话
if (session == null) {
Servlets.setContentType(response, StandardContentType.TEXT_HTML);
Servlets.setCharset(response, Const.UTF_8);
return outputStream -> outputStream.write(Strings.bytes(ErrorMessage.SESSION_ABSENT));
}
// 响应文件
Servlets.setAttachmentHeader(response, Files1.getFileName(session.getPath()));
return session;
}
/**
* 构建查询对象
*

View File

@@ -24,6 +24,7 @@ spring:
redisson:
threads: 2
netty-threads: 2
minimum-idle-size: 2
mybatis-plus:
lazy-initialization: true

View File

@@ -65,6 +65,9 @@ public class TerminalPreferenceModel implements PreferenceModel {
@Schema(description = "行高")
private Double lineHeight;
@Schema(description = "字间距")
private Integer letterSpacing;
@Schema(description = "文本字重")
private String fontWeight;
@@ -129,6 +132,9 @@ public class TerminalPreferenceModel implements PreferenceModel {
@Schema(description = "WebGL 渲染插件")
private Boolean enableWebglPlugin;
@Schema(description = "unicode11 插件")
private Boolean enableUnicodePlugin;
@Schema(description = "图片渲染插件")
private Boolean enableImagePlugin;

View File

@@ -24,6 +24,7 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
.fontFamily("_")
.fontSize(13)
.lineHeight(1.12)
.letterSpacing(1)
.fontWeight("normal")
.fontWeightBold("bold")
.cursorStyle("bar")
@@ -48,6 +49,7 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
String defaultPluginsSetting = TerminalPreferenceModel.PluginsSettingModel.builder()
.enableWeblinkPlugin(true)
.enableWebglPlugin(true)
.enableUnicodePlugin(true)
.enableImagePlugin(false)
.build()
.toJsonString();

View File

@@ -24,6 +24,7 @@ spring:
redisson:
threads: 2
netty-threads: 2
minimum-idle-size: 2
mybatis-plus:
lazy-initialization: true

View File

@@ -1,5 +1,5 @@
VITE_API_BASE_URL= 'http://127.0.0.1:9200/orion-visor/api'
VITE_WS_BASE_URL= 'ws://127.0.0.1:9200/orion-visor/keep-alive'
VITE_APP_VERSION= '2.0.4'
VITE_APP_VERSION= '2.0.5'
VITE_SFTP_PREVIEW_MB= 2
VITE_DEMO_MODE= false

View File

@@ -1,5 +1,5 @@
VITE_API_BASE_URL= '/orion-visor/api'
VITE_WS_BASE_URL= '/orion-visor/keep-alive'
VITE_APP_VERSION= '2.0.4'
VITE_APP_VERSION= '2.0.5'
VITE_SFTP_PREVIEW_MB= 2
VITE_DEMO_MODE= false

View File

@@ -1,7 +1,7 @@
{
"name": "orion-visor-ui",
"description": "Orion Visor UI",
"version": "2.0.4",
"version": "2.0.5",
"private": true,
"author": "Jiahang Li",
"license": "Apache 2.0",
@@ -36,6 +36,14 @@
"@dangojs/a-query-header": "^0.0.31",
"@sanqi377/arco-vue-icon-picker": "^1.0.7",
"@vueuse/core": "^9.3.0",
"@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-image": "^0.8.0",
"@xterm/addon-search": "^0.15.0",
"@xterm/addon-unicode11": "^0.8.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-webgl": "^0.18.0",
"@xterm/xterm": "^5.5.0",
"axios": "^0.24.0",
"cron-parser": "^4.9.0",
"dayjs": "^1.11.5",
@@ -53,14 +61,7 @@
"vue": "^3.2.40",
"vue-echarts": "^6.2.3",
"vue-i18n": "^9.2.2",
"vue-router": "^4.0.14",
"xterm": "^5.3.0",
"xterm-addon-canvas": "^0.5.0",
"xterm-addon-fit": "^0.8.0",
"xterm-addon-image": "^0.5.0",
"xterm-addon-search": "^0.13.0",
"xterm-addon-web-links": "^0.9.0",
"xterm-addon-webgl": "^0.16.0"
"vue-router": "^4.0.14"
},
"devDependencies": {
"@arco-plugins/vite-vue": "^1.4.5",
@@ -93,7 +94,7 @@
"rollup": "^3.9.1",
"rollup-plugin-visualizer": "^5.8.2",
"sass": "^1.69.4",
"stylelint": "^14.13.0",
"stylelint": "^14.14.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-config-recommended-vue": "^1.4.0",

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import type { DataGrid, Pagination } from '@/types/global';
import type { TableData } from '@arco-design/web-vue/es/table/interface';
import { httpBaseUrl } from '@/utils/env';
import axios from 'axios';
import qs from 'query-string';
@@ -46,17 +47,25 @@ export interface HostSftpLogExtra {
* SFTP
*/
export function getHostSftpLogPage(request: HostSftpLogQueryRequest) {
return axios.post<DataGrid<HostSftpLogQueryResponse>>('/asset/host-sftp-log/query', request);
return axios.post<DataGrid<HostSftpLogQueryResponse>>('/asset/host-sftp/query-log', request);
}
/**
* SFTP
*/
export function deleteHostSftpLog(idList: Array<number>) {
return axios.delete('/asset/host-sftp-log/delete', {
return axios.delete('/asset/host-sftp/delete-log', {
params: { idList },
paramsSerializer: params => {
return qs.stringify(params, { arrayFormat: 'comma' });
}
});
}
/**
*
*/
export function downloadWithTransferToken(channelId: string, transferToken: string) {
window.open(`${httpBaseUrl}/asset/host-sftp/download?channelId=${channelId}&transferToken=${transferToken}`, 'newWindow');
}

View File

@@ -1,8 +1,9 @@
import type { IDisposable, ITerminalInitOnlyOptions, ITerminalOptions, Terminal } from 'xterm';
import type { FitAddon } from 'xterm-addon-fit';
import type { SearchAddon } from 'xterm-addon-search';
import type { WebLinksAddon } from 'xterm-addon-web-links';
import type { WebglAddon } from 'xterm-addon-webgl';
import type { IDisposable, ITerminalInitOnlyOptions, ITerminalOptions, Terminal } from '@xterm/xterm';
import type { FitAddon } from '@xterm/addon-fit';
import type { SearchAddon } from '@xterm/addon-search';
import type { WebLinksAddon } from '@xterm/addon-web-links';
import type { WebglAddon } from '@xterm/addon-webgl';
import type { Unicode11Addon } from '@xterm/addon-unicode11';
// 执行类型
export type ExecType = 'BATCH' | 'JOB';
@@ -61,6 +62,7 @@ export const LogAppenderOptions: ITerminalOptions & ITerminalInitOnlyOptions = {
fontSize: 13,
lineHeight: 1.12,
convertEol: true,
allowProposedApi: true,
};
// dom 引用
@@ -85,6 +87,7 @@ export interface LogAddons extends Record<string, IDisposable> {
webgl: WebglAddon;
search: SearchAddon;
weblink: WebLinksAddon;
unicode: Unicode11Addon;
}
// 执行日志 appender 定义

View File

@@ -8,11 +8,12 @@ import { Message } from '@arco-design/web-vue';
import { useDebounceFn } from '@vueuse/core';
import { addEventListen, removeEventListen } from '@/utils/event';
import { copy as copyText } from '@/hooks/copy';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { SearchAddon } from 'xterm-addon-search';
import { WebLinksAddon } from 'xterm-addon-web-links';
import { WebglAddon } from 'xterm-addon-webgl';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { SearchAddon } from '@xterm/addon-search';
import { WebLinksAddon } from '@xterm/addon-web-links';
import { WebglAddon } from '@xterm/addon-webgl';
import { Unicode11Addon } from '@xterm/addon-unicode11';
// 执行日志 appender 实现
export default class LogAppender implements ILogAppender {
@@ -130,15 +131,19 @@ export default class LogAppender implements ILogAppender {
const search = new SearchAddon();
const webgl = new WebglAddon();
const weblink = new WebLinksAddon();
const unicode = new Unicode11Addon();
terminal.loadAddon(fit);
terminal.loadAddon(search);
terminal.loadAddon(webgl);
terminal.loadAddon(weblink);
terminal.loadAddon(unicode);
terminal.unicode.activeVersion = '11';
return {
fit,
search,
webgl,
weblink
weblink,
unicode
};
}

View File

@@ -176,7 +176,7 @@
import { downloadExecJobLogFile } from '@/api/job/exec-job-log';
import { downloadFile } from '@/utils/file';
import XtermSearchModal from '@/components/xtrem/search-modal/index.vue';
import 'xterm/css/xterm.css';
import '@xterm/xterm/css/xterm.css';
const props = defineProps<{
type: ExecType;

View File

@@ -58,7 +58,7 @@
</script>
<script lang="ts" setup>
import type { ISearchOptions } from 'xterm-addon-search';
import type { ISearchOptions } from '@xterm/addon-search';
import useVisible from '@/hooks/visible';
import { nextTick, ref } from 'vue';

View File

@@ -29,6 +29,7 @@ export interface TerminalDisplaySetting {
fontFamily?: string;
fontSize?: number;
lineHeight?: number;
letterSpacing?: number;
fontWeight?: string | number;
fontWeightBold?: string | number;
cursorStyle?: string;
@@ -61,6 +62,7 @@ export interface TerminalInteractSetting {
export interface TerminalPluginsSetting {
enableWeblinkPlugin: boolean;
enableWebglPlugin: boolean;
enableUnicodePlugin: boolean;
enableImagePlugin: boolean;
}

View File

@@ -139,8 +139,9 @@
onOk: async () => {
setLoading(true);
try {
// 调用删除
await clearHostConnectLog(formModel.value);
// 调用清空
const { data } = await clearHostConnectLog(formModel.value);
Message.success(`已成功清空 ${data} 条数据`);
emits('clear');
// 清空
setVisible(false);

View File

@@ -172,9 +172,9 @@
</script>
<script lang="ts" setup>
import type { HostSftpLogQueryRequest, HostSftpLogQueryResponse } from '@/api/asset/host-sftp-log';
import type { HostSftpLogQueryRequest, HostSftpLogQueryResponse } from '@/api/asset/host-sftp';
import { reactive, ref, onMounted } from 'vue';
import { getHostSftpLogPage, deleteHostSftpLog } from '@/api/asset/host-sftp-log';
import { getHostSftpLogPage, deleteHostSftpLog } from '@/api/asset/host-sftp';
import { sftpOperatorTypeKey, sftpOperatorResultKey, SftpOperatorType } from '../types/const';
import { usePagination, useRowSelection } from '@/types/table';
import { useDictStore } from '@/store';

View File

@@ -131,8 +131,9 @@
onOk: async () => {
setLoading(true);
try {
// 调用删除
await clearExecCommandLog(formModel.value);
// 调用清空
const { data } = await clearExecCommandLog(formModel.value);
Message.success(`已成功清空 ${data} 条数据`);
emits('clear');
// 清空
setVisible(false);

View File

@@ -131,8 +131,9 @@
onOk: async () => {
setLoading(true);
try {
// 调用删除
await clearUploadTask(formModel.value);
// 调用清空
const { data } = await clearUploadTask(formModel.value);
Message.success(`已成功清空 ${data} 条数据`);
emits('clear');
// 清空
setVisible(false);

View File

@@ -13,72 +13,86 @@
<a-form class="terminal-setting-form"
:model="formModel"
layout="vertical">
<a-space>
<a-row :gutter="48">
<!-- 字体样式 -->
<a-form-item field="fontFamily" label="字体样式">
<a-select v-model="formModel.fontFamily"
class="form-item-font-family"
placeholder="请选择字体样式"
:options="toOptions(fontFamilyKey)"
:allow-create="true"
:filter-option="labelFilter">
<template #option="{ data }">
<span :style="{ fontFamily: data.value }">{{ data.label }}</span>
</template>
<template #label="{ data }">
<span :style="{ fontFamily: data.value }">{{ data.label }}</span>
</template>
</a-select>
</a-form-item>
<a-col :span="12">
<a-form-item field="fontFamily" label="字体样式">
<a-select v-model="formModel.fontFamily"
placeholder="请选择字体样式"
:options="toOptions(fontFamilyKey)"
:allow-create="true"
:filter-option="labelFilter">
<template #option="{ data }">
<span :style="{ fontFamily: data.value }">{{ data.label }}</span>
</template>
<template #label="{ data }">
<span :style="{ fontFamily: data.value }">{{ data.label }}</span>
</template>
</a-select>
</a-form-item>
</a-col>
<!-- 字体大小 -->
<a-form-item field="fontSize" label="字体大小">
<a-select v-model="formModel.fontSize"
class="form-item-font-size"
placeholder="请选择字体大小"
:options="toOptions(fontSizeKey)" />
</a-form-item>
<a-col :span="12">
<a-form-item field="fontSize" label="字体大小">
<a-select v-model="formModel.fontSize"
placeholder="请选择字体大小"
:options="toOptions(fontSizeKey)" />
</a-form-item>
</a-col>
<!-- 行高 -->
<a-form-item field="lineHeight" label="行高">
<a-input-number v-model="formModel.lineHeight"
class="form-item-line-height"
placeholder="请输入行高"
:precision="2"
:min="1"
:max="2"
hide-button />
</a-form-item>
</a-space>
<a-space>
<a-col :span="12">
<a-form-item field="lineHeight" label="行高">
<a-input-number v-model="formModel.lineHeight"
placeholder="请输入行高"
:precision="2"
:step="0.05"
:min="1"
:max="2" />
</a-form-item>
</a-col>
<!-- 字间距 -->
<a-col :span="12">
<a-form-item field="lineHeight" label="字间距 (px)">
<a-input-number v-model="formModel.letterSpacing"
placeholder="请输入字间距"
:precision="0"
:step="1"
:min="-5"
:max="5" />
</a-form-item>
</a-col>
<!-- 普通文本字重 -->
<a-form-item field="fontWeight" label="普通文本字重">
<a-select v-model="formModel.fontWeight"
class="form-item-font-weight"
placeholder="请选择字重"
:options="toOptions(fontWeightKey)" />
</a-form-item>
<a-col :span="12">
<a-form-item field="fontWeight" label="普通文本字重">
<a-select v-model="formModel.fontWeight"
placeholder="请选择字重"
:options="toOptions(fontWeightKey)" />
</a-form-item>
</a-col>
<!-- 加粗文本字重 -->
<a-form-item field="fontWeightBold" label="加粗文本字重">
<a-select v-model="formModel.fontWeightBold"
class="form-item-font-bold-weight"
placeholder="请选择字重"
:options="toOptions(fontWeightKey)" />
</a-form-item>
</a-space>
<a-space>
<a-col :span="12">
<a-form-item field="fontWeightBold" label="加粗文本字重">
<a-select v-model="formModel.fontWeightBold"
placeholder="请选择字重"
:options="toOptions(fontWeightKey)" />
</a-form-item>
</a-col>
<!-- 光标样式 -->
<a-form-item field="cursorStyle" label="光标样式">
<a-radio-group type="button"
v-model="formModel.cursorStyle"
class="form-item-cursor-style usn"
:options="toRadioOptions(cursorStyleKey)" />
</a-form-item>
<a-col :span="12">
<a-form-item field="cursorStyle" label="光标样式">
<a-radio-group type="button"
v-model="formModel.cursorStyle"
class="form-item-cursor-style usn"
:options="toRadioOptions(cursorStyleKey)" />
</a-form-item>
</a-col>
<!-- 光标闪烁 -->
<a-form-item field="cursorBlink" label="光标是否闪烁">
<a-switch v-model="formModel.cursorBlink"
type="round"
class="form-item-cursor-blink" />
</a-form-item>
</a-space>
<a-col :span="12">
<a-form-item field="cursorBlink" label="光标是否闪烁">
<a-switch v-model="formModel.cursorBlink" type="round" />
</a-form-item>
</a-col>
</a-row>
</a-form>
<!-- 预览区域 -->
<div class="terminal-example">
@@ -101,7 +115,7 @@
<script lang="ts" setup>
import type { TerminalDisplaySetting } from '@/store/modules/terminal/types';
import { ref, watch } from 'vue';
import { ref, watch, onMounted } from 'vue';
import { useDictStore, useTerminalStore } from '@/store';
import { fontFamilyKey, fontSizeKey, fontWeightKey, fontFamilySuffix, cursorStyleKey } from '../../../types/terminal.const';
import { labelFilter } from '@/types/form';
@@ -112,7 +126,7 @@
const { preference, updateTerminalPreference } = useTerminalStore();
const previewTerminal = ref();
const formModel = ref<TerminalDisplaySetting>({ ...preference.displaySetting });
const formModel = ref<TerminalDisplaySetting>({});
// 监听内容变化
watch(formModel, (v) => {
@@ -137,17 +151,24 @@
previewTerminal.value.term.focus();
}, { deep: true });
// 初始化配置
onMounted(() => {
formModel.value = { ...preference.displaySetting };
});
</script>
<style lang="less" scoped>
@terminal-width: 458px;
.setting-body {
height: 248px;
height: 326px;
justify-content: space-between;
}
:deep(.arco-form) {
width: 412px;
.arco-form-item-label {
user-select: none;
}
@@ -156,31 +177,10 @@
margin-bottom: 14px;
}
.form-item-font-family {
width: 158px;
}
.form-item-font-size {
width: 148px;
}
.form-item-line-height {
width: 114px;
}
.form-item-font-weight, .form-item-font-bold-weight {
width: 178px;
}
.form-item-font-weight {
margin-right: 70px;
}
.form-item-cursor-style {
margin-right: 90px;
.arco-radio-button-content {
padding: 0 20px;
padding: 0 24px;
}
}
}

View File

@@ -21,6 +21,11 @@
</block-setting-item>
</a-row>
<a-row class="mb16" align="stretch" :gutter="16">
<!-- unicode11 插件 -->
<block-setting-item label="unicode11 插件" desc="支持 Unicode 11 字符集">
<a-switch type="round"
v-model="formModel.enableUnicodePlugin" />
</block-setting-item>
<!-- 图片渲染插件 -->
<block-setting-item label="图片渲染插件" desc="支持使用 sixel 打开图片 (一般不需要开启)">
<a-switch type="round"

View File

@@ -10,7 +10,7 @@
<script lang="ts" setup>
import type { TerminalThemeSchema } from '@/api/asset/host-terminal';
import { Terminal } from 'xterm';
import { Terminal } from '@xterm/xterm';
import { onMounted, onUnmounted, ref } from 'vue';
const props = defineProps<{

View File

@@ -8,57 +8,53 @@ export default class SftpTransferDownloader implements ISftpTransferDownloader {
public abort: boolean;
private blobArr: Array<Blob>;
private client: WebSocket;
private item: SftpTransferItem;
constructor(item: SftpTransferItem, client: WebSocket) {
this.abort = false;
this.blobArr = [];
this.item = item;
this.client = client;
}
// 开始下载
startDownload() {
initDownload() {
this.item.status = TransferStatus.TRANSFERRING;
// 发送开始下载信息
this.client?.send(JSON.stringify({
type: TransferOperatorType.DOWNLOAD_START,
type: TransferOperatorType.DOWNLOAD_INIT,
path: getPath(this.item.parentPath + '/' + this.item.name),
hostId: this.item.hostId
}));
}
// 接收 blob
resolveBlob(blob: Blob) {
this.blobArr.push(blob);
this.item.currentSize += blob.size;
}
// 下载完成
downloadFinish() {
if (this.abort) {
// 中断则不触发下载
return;
}
try {
// 触发下载
saveAs(new Blob(this.blobArr, {
type: 'application/octet-stream'
}), getFileName(this.item.name));
// 设置实际大小
this.item.currentSize = this.item.totalSize;
if (this.item.totalSize === 0) {
// 空文件直接触发下载
try {
// 触发下载
saveAs(new Blob([], {
type: 'application/octet-stream'
}), getFileName(this.item.name));
this.item.status = TransferStatus.SUCCESS;
} catch (e) {
this.item.status = TransferStatus.ERROR;
this.item.errorMessage = '保存失败';
}
} else {
this.item.status = TransferStatus.SUCCESS;
} catch (e) {
this.item.status = TransferStatus.ERROR;
this.item.errorMessage = '保存失败';
} finally {
this.blobArr = [];
}
}
// 下载失败
downloadError(msg: string | undefined) {
this.blobArr = [];
this.item.status = TransferStatus.ERROR;
this.item.errorMessage = msg || '下载失败';
}

View File

@@ -4,6 +4,7 @@ import { TransferReceiverType, TransferStatus, TransferType } from '../types/ter
import { Message } from '@arco-design/web-vue';
import { getTerminalAccessToken, openHostTransferChannel } from '@/api/asset/host-terminal';
import { nextId } from '@/utils';
import { downloadWithTransferToken } from '@/api/asset/host-sftp';
import SftpTransferUploader from './sftp-transfer-uploader';
import SftpTransferDownloader from './sftp-transfer-downloader';
@@ -144,7 +145,9 @@ export default class SftpTransferManager implements ISftpTransferManager {
// 计算传输进度
private calcProgress() {
this.transferList.forEach(item => {
item.progress = (item.currentSize / item.totalSize * 100).toFixed(2);
if (item.totalSize != 0) {
item.progress = (item.currentSize / item.totalSize * 100).toFixed(2);
}
});
}
@@ -165,7 +168,7 @@ export default class SftpTransferManager implements ISftpTransferManager {
this.uploadFile();
} else {
// 下载
this.uploadDownload();
this.downloadFile();
}
} else {
// 无任务关闭会话
@@ -175,27 +178,28 @@ export default class SftpTransferManager implements ISftpTransferManager {
// 接收消息
private async resolveMessage(message: MessageEvent) {
if (message.data instanceof Blob) {
// 二进制消息 下载数据
this.resolveDownloadBlob(message.data);
} else {
// 文本消息
const data = JSON.parse(message.data) as TransferOperatorResponse;
if (data.type === TransferReceiverType.NEXT_TRANSFER
|| data.type === TransferReceiverType.UPLOAD_FINISH
|| data.type === TransferReceiverType.UPLOAD_ERROR) {
// 执行下一个传输任务
this.resolveNextTransfer(data);
} else if (data.type === TransferReceiverType.UPLOAD_NEXT_BLOCK) {
// 接收下一块上传数据
await this.resolveUploadNextBlock();
} else if (data.type === TransferReceiverType.DOWNLOAD_FINISH) {
// 下载完成
this.resolveDownloadFinish();
} else if (data.type === TransferReceiverType.DOWNLOAD_ERROR) {
// 下载失败
this.resolveDownloadError(data.msg);
}
// 文本消息
const data = JSON.parse(message.data) as TransferOperatorResponse;
if (data.type === TransferReceiverType.NEXT_TRANSFER
|| data.type === TransferReceiverType.UPLOAD_FINISH
|| data.type === TransferReceiverType.UPLOAD_ERROR) {
// 执行下一个传输任务
this.resolveNextTransfer(data);
} else if (data.type === TransferReceiverType.UPLOAD_NEXT_BLOCK) {
// 接收下一块上传数据
await this.resolveUploadNextBlock();
} else if (data.type === TransferReceiverType.DOWNLOAD_START) {
// 开始下载
this.resolveDownloadStart(data);
} else if (data.type === TransferReceiverType.DOWNLOAD_PROGRESS) {
// 下载进度
this.resolveDownloadProgress(data);
} else if (data.type === TransferReceiverType.DOWNLOAD_FINISH) {
// 下载完成
this.resolveDownloadFinish();
} else if (data.type === TransferReceiverType.DOWNLOAD_ERROR) {
// 下载失败
this.resolveDownloadError(data.msg);
}
}
@@ -208,11 +212,11 @@ export default class SftpTransferManager implements ISftpTransferManager {
}
// 下载文件
private uploadDownload() {
private downloadFile() {
// 创建下载器
this.currentDownloader = new SftpTransferDownloader(this.currentItem as SftpTransferItem, this.client as WebSocket);
// 开始下载
this.currentDownloader.startDownload();
// 初始化下载
this.currentDownloader.initDownload();
}
// 接收下一个传输任务响应
@@ -251,9 +255,16 @@ export default class SftpTransferManager implements ISftpTransferManager {
}
}
// 接收下载数据
private resolveDownloadBlob(blob: Blob) {
this.currentDownloader?.resolveBlob(blob);
// 接收开始下载响应
private resolveDownloadStart(data: TransferOperatorResponse) {
downloadWithTransferToken(data.channelId as string, data.transferToken as string);
}
// 接收下载进度响应
private resolveDownloadProgress(data: TransferOperatorResponse) {
if (this.currentItem && data.currentSize) {
this.currentItem.currentSize = data.currentSize;
}
}
// 接收下载完成响应

View File

@@ -1,6 +1,6 @@
import type { ShortcutKey, TerminalInteractSetting, TerminalShortcutKey } from '@/store/modules/terminal/types';
import type { ISshSession, ISshSessionHandler, XtermDomRef } from '../types/terminal.type';
import type { Terminal } from 'xterm';
import type { Terminal } from '@xterm/xterm';
import useCopy from '@/hooks/copy';
import html2canvas from 'html2canvas';
import { useTerminalStore, useUserStore } from '@/store';

View File

@@ -1,16 +1,18 @@
import type { UnwrapRef } from 'vue';
import type { ISearchOptions } from '@xterm/addon-search';
import type { TerminalPreference } from '@/store/modules/terminal/types';
import type { ISshSession, ISshSessionHandler, ITerminalChannel, XtermAddons, XtermDomRef } from '../types/terminal.type';
import { useTerminalStore } from '@/store';
import { InputProtocol } from '../types/terminal.protocol';
import { fontFamilySuffix, TerminalShortcutType, TerminalStatus } from '../types/terminal.const';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { WebLinksAddon } from 'xterm-addon-web-links';
import { ISearchOptions, SearchAddon } from 'xterm-addon-search';
import { ImageAddon } from 'xterm-addon-image';
import { CanvasAddon } from 'xterm-addon-canvas';
import { WebglAddon } from 'xterm-addon-webgl';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { SearchAddon } from '@xterm/addon-search';
import { WebLinksAddon } from '@xterm/addon-web-links';
import { ImageAddon } from '@xterm/addon-image';
import { Unicode11Addon } from '@xterm/addon-unicode11';
import { CanvasAddon } from '@xterm/addon-canvas';
import { WebglAddon } from '@xterm/addon-webgl';
import { playBell } from '@/utils/bell';
import { addEventListen } from '@/utils/event';
import SshSessionHandler from './ssh-session-handler';
@@ -63,9 +65,10 @@ export default class SshSession implements ISshSession {
fastScrollModifier: !!preference.interactSetting.fastScrollModifier ? 'alt' : 'none',
altClickMovesCursor: !!preference.interactSetting.altClickMovesCursor,
rightClickSelectsWord: !!preference.interactSetting.rightClickSelectsWord,
fontFamily: preference.displaySetting.fontFamily + fontFamilySuffix,
wordSeparator: preference.interactSetting.wordSeparator,
fontFamily: preference.displaySetting.fontFamily + fontFamilySuffix,
scrollback: preference.sessionSetting.scrollBackLine,
allowProposedApi: true,
});
// 处理器
this.handler = new SshSessionHandler(this, domRef);
@@ -192,12 +195,20 @@ export default class SshSession implements ISshSession {
if (preference.pluginsSetting.enableImagePlugin) {
this.addons.image = new ImageAddon();
}
// unicode11 插件
if (preference.pluginsSetting.enableUnicodePlugin) {
this.addons.unicode = new Unicode11Addon();
}
// 加载插件
for (const addon of Object.values(this.addons)) {
if (addon) {
this.inst.loadAddon(addon);
}
}
// 设置 unicode11 版本
if (this.addons.unicode) {
this.inst.unicode.activeVersion = '11';
}
}
// 设置已连接

View File

@@ -60,7 +60,7 @@
import CommandSnippetDrawer from '@/views/host/command-snippet/components/command-snippet-drawer.vue';
import PathBookmarkDrawer from '@/views/host/path-bookmark/components/path-bookmark-drawer.vue';
import '@/assets/style/host-terminal-layout.less';
import 'xterm/css/xterm.css';
import '@xterm/xterm/css/xterm.css';
const terminalStore = useTerminalStore();
const dictStore = useDictStore();

View File

@@ -336,7 +336,7 @@ export const TransferOperatorType = {
UPLOAD_START: 'uploadStart',
UPLOAD_FINISH: 'uploadFinish',
UPLOAD_ERROR: 'uploadError',
DOWNLOAD_START: 'downloadStart',
DOWNLOAD_INIT: 'downloadInit',
DOWNLOAD_ABORT: 'downloadAbort',
};
@@ -346,6 +346,8 @@ export const TransferReceiverType = {
UPLOAD_NEXT_BLOCK: 'uploadNextBlock',
UPLOAD_FINISH: 'uploadFinish',
UPLOAD_ERROR: 'uploadError',
DOWNLOAD_START: 'downloadStart',
DOWNLOAD_PROGRESS: 'downloadProgress',
DOWNLOAD_FINISH: 'downloadFinish',
DOWNLOAD_ERROR: 'downloadError',
};

View File

@@ -1,10 +1,11 @@
import type { Terminal } from 'xterm';
import type { FitAddon } from 'xterm-addon-fit';
import type { CanvasAddon } from 'xterm-addon-canvas';
import type { WebglAddon } from 'xterm-addon-webgl';
import type { WebLinksAddon } from 'xterm-addon-web-links';
import type { ISearchOptions, SearchAddon } from 'xterm-addon-search';
import type { ImageAddon } from 'xterm-addon-image';
import type { Terminal } from '@xterm/xterm';
import type { FitAddon } from '@xterm/addon-fit';
import type { CanvasAddon } from '@xterm/addon-canvas';
import type { WebglAddon } from '@xterm/addon-webgl';
import type { WebLinksAddon } from '@xterm/addon-web-links';
import type { ISearchOptions, SearchAddon } from '@xterm/addon-search';
import type { ImageAddon } from '@xterm/addon-image';
import type { Unicode11Addon } from '@xterm/addon-unicode11';
import type { CSSProperties } from 'vue';
import type { HostQueryResponse } from '@/api/asset/host';
@@ -225,6 +226,7 @@ export interface XtermAddons {
weblink: WebLinksAddon;
search: SearchAddon;
image: ImageAddon;
unicode: Unicode11Addon;
}
// 终端会话定义
@@ -430,10 +432,8 @@ export interface ISftpTransferUploader {
export interface ISftpTransferDownloader {
// 是否中断
abort: boolean;
// 开始下载
startDownload: () => void;
// 接收 blob
resolveBlob: (blob: Blob) => void;
// 初始化下载
initDownload: () => void;
// 下载完成
downloadFinish: () => void;
// 下载失败
@@ -459,8 +459,11 @@ export interface SftpTransferItem {
// 传输操作响应
export interface TransferOperatorResponse {
channelId?: string;
type: string;
hostId?: number;
currentSize?: number;
transferToken?: string;
success: boolean;
msg?: string;
}

View File

@@ -126,8 +126,9 @@
onOk: async () => {
setLoading(true);
try {
// 调用删除
await clearExecJobLog(formModel.value);
// 调用清空
const { data } = await clearExecJobLog(formModel.value);
Message.success(`已成功清空 ${data} 条数据`);
emits('clear');
// 清空
setVisible(false);

View File

@@ -162,8 +162,9 @@
onOk: async () => {
setLoading(true);
try {
// 调用删除
await clearOperatorLog(formModel.value);
// 调用清空
const { data } = await clearOperatorLog(formModel.value);
Message.success(`已成功清空 ${data} 条数据`);
emits('clear');
// 清空
setVisible(false);

View File

@@ -22,7 +22,7 @@
</modules>
<properties>
<revision>2.0.4</revision>
<revision>2.0.5</revision>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<maven.surefire.plugin.version>3.0.0-M5</maven.surefire.plugin.version>