Compare commits

...

14 Commits

Author SHA1 Message Date
李佳航
032f1763f6 Merge pull request #95 from dromara/dev
Dev
2025-03-10 11:02:45 +08:00
lijiahangmax
d071ef64d8 🔨 编译问题. 2025-03-09 20:11:21 +08:00
lijiahangmax
c820443a3b 升级版本. 2025-03-09 20:00:10 +08:00
lijiahangmax
14c4e77445 修改终端提示. 2025-03-09 19:58:10 +08:00
lijiahangmax
79d9f69ed4 修改终端提示. 2025-03-08 12:44:33 +08:00
lijiahangmax
6c9065072d Merge remote-tracking branch 'origin/dev' into dev 2025-03-07 22:03:52 +08:00
lijiahang
05bc6c1fbb 修改路由规则. 2025-03-07 15:50:26 +08:00
lijiahang
a1dd9eec01 优化单元测试. 2025-03-07 14:57:26 +08:00
lijiahangmax
660df7c110 Merge remote-tracking branch 'origin/dev' into dev 2025-03-06 23:29:28 +08:00
lijiahang
093501a400 🐛 代码生成器未能读取到环境变量. 2025-03-06 10:17:48 +08:00
lijiahang
7943deb924 优化终端会话管理器. 2025-03-05 10:20:20 +08:00
lijiahangmax
490167e649 Merge remote-tracking branch 'origin/dev' into dev 2025-03-04 21:46:00 +08:00
lijiahang
8635f6bb05 🔨 修改静态资源匿名处理策略. 2025-03-04 10:27:55 +08:00
lijiahangmax
ac46dd6703 🎨 优化统计样式. 2025-03-02 20:16:09 +08:00
66 changed files with 367 additions and 291 deletions

View File

@@ -1,5 +1,5 @@
#/bin/bash
version=2.3.4
version=2.3.5
docker build -t orion-visor-adminer:${version} .
docker tag orion-visor-adminer:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:${version}
docker tag orion-visor-adminer:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:latest

View File

@@ -1,5 +1,5 @@
#/bin/bash
version=2.3.4
version=2.3.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.3.4
version=2.3.5
docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:${version}
docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-mysql:${version}
docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:${version}

View File

@@ -1,5 +1,5 @@
#/bin/bash
version=2.3.4
version=2.3.5
docker build -t orion-visor-redis:${version} .
docker tag orion-visor-redis:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:${version}
docker tag orion-visor-redis:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:latest

View File

@@ -1,5 +1,5 @@
#/bin/bash
version=2.3.4
version=2.3.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

@@ -20,21 +20,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.launch.configuration;
package org.dromara.visor.common.configuration;
import cn.orionsec.kit.spring.SpringHolder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 应用配置类
* spring 配置类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/6/20 10:34
*/
@Configuration
public class LaunchApplicationConfiguration {
public class SpringConfiguration {
/**
* @return spring 容器工具类

View File

@@ -36,7 +36,7 @@ public interface AppConst extends OrionConst {
/**
* 同 ${orion.version} 迭代时候需要手动更改
*/
String VERSION = "2.3.4";
String VERSION = "2.3.5";
/**
* 同 ${spring.application.name}

View File

@@ -14,7 +14,7 @@
<url>https://github.com/dromara/orion-visor</url>
<properties>
<revision>2.3.4</revision>
<revision>2.3.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

@@ -23,6 +23,8 @@
package org.dromara.visor.framework.mybatis.core.generator;
import cn.orionsec.kit.lang.constant.Const;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.Systems;
import cn.orionsec.kit.lang.utils.ansi.AnsiAppender;
import cn.orionsec.kit.lang.utils.ansi.style.AnsiFont;
import cn.orionsec.kit.lang.utils.ansi.style.color.AnsiForeground;
@@ -32,6 +34,8 @@ import org.dromara.visor.framework.mybatis.core.generator.template.Table;
import org.dromara.visor.framework.mybatis.core.generator.template.Template;
import java.io.File;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 代码生成器
@@ -42,6 +46,8 @@ import java.io.File;
*/
public class CodeGenerators {
private static final Pattern ENV_VAR_PATTERN = Pattern.compile("\\$\\{([^:]+):([^}]+)\\}");
public static void main(String[] args) {
// 输出路径
String outputDir = "D:/MP/";
@@ -76,11 +82,6 @@ public class CodeGenerators {
.disableUnitTest()
.enableProviderApi()
.vue("system", "message")
.dict("messageClassify", "classify", "messageClassify")
.comment("消息分类")
.fields("NOTICE", "TODO")
.labels("通知", "待办")
.valueUseFields()
.dict("messageType", "type", "messageType")
.comment("消息类型")
.fields("EXEC_FAILED", "UPLOAD_FAILED")
@@ -94,9 +95,9 @@ public class CodeGenerators {
// jdbc 配置 - 使用配置文件
File yamlFile = new File("orion-visor-launch/src/main/resources/application-dev.yaml");
YmlExt yaml = YmlExt.load(yamlFile);
String url = yaml.getValue("spring.datasource.druid.url");
String username = yaml.getValue("spring.datasource.druid.username");
String password = yaml.getValue("spring.datasource.druid.password");
String url = resolveConfigValue(yaml.getValue("spring.datasource.druid.url"));
String username = resolveConfigValue(yaml.getValue("spring.datasource.druid.username"));
String password = resolveConfigValue(yaml.getValue("spring.datasource.druid.password"));
// 执行
runGenerator(outputDir, author,
@@ -147,4 +148,31 @@ public class CodeGenerators {
System.out.print(line);
}
/**
* 解析实际的配置
*
* @param value value
* @return value
*/
private static String resolveConfigValue(String value) {
if (Strings.isBlank(value)) {
return value;
}
Matcher matcher = ENV_VAR_PATTERN.matcher(value);
StringBuffer resultString = new StringBuffer();
while (matcher.find()) {
// 环境变量名
String envVar = matcher.group(1);
// 默认值
String defaultValue = matcher.group(2);
// 获取环境变量的值
String envValue = Systems.getEnv(envVar, defaultValue);
// 替换占位符
matcher.appendReplacement(resultString, Matcher.quoteReplacement(envValue));
}
// 处理结尾的剩余部分
matcher.appendTail(resultString);
return resultString.toString();
}
}

View File

@@ -11,10 +11,10 @@ SELECT @TYPE_KEY_ID:= id FROM dict_key WHERE key_name = 'operatorLogType' AND de
INSERT INTO dict_value
(`key_id`, `key_name`, `value`, `label`, `extra`, `sort`, `create_time`, `update_time`, `creator`, `updater`, `deleted`)
VALUES
(@MODULE_KEY_ID, 'operatorLogModule', '${package.ModuleName}:${typeHyphen}', '$!{table.comment}', '{}', @MODULE_KEY_MAX_SORT + 10, now(), now(), '1', '1', 0),
(@TYPE_KEY_ID, 'operatorLogType', '${typeHyphen}:create', '创建$!{table.comment}', '{}', 10, now(), now(), '1', '1', 0),
(@TYPE_KEY_ID, 'operatorLogType', '${typeHyphen}:update', '更新$!{table.comment}', '{}', 20, now(), now(), '1', '1', 0),
(@TYPE_KEY_ID, 'operatorLogType', '${typeHyphen}:delete', '删除$!{table.comment}', '{}', 30, now(), now(), '1', '1', 0);
(@MODULE_KEY_ID, 'operatorLogModule', '${package.ModuleName}:${typeHyphen}', '$!{table.comment}', '{}', @MODULE_KEY_MAX_SORT + 10, now(), now(), 'admin', 'admin', 0),
(@TYPE_KEY_ID, 'operatorLogType', '${typeHyphen}:create', '创建$!{table.comment}', '{}', 10, now(), now(), 'admin', 'admin', 0),
(@TYPE_KEY_ID, 'operatorLogType', '${typeHyphen}:update', '更新$!{table.comment}', '{}', 20, now(), now(), 'admin', 'admin', 0),
(@TYPE_KEY_ID, 'operatorLogType', '${typeHyphen}:delete', '删除$!{table.comment}', '{}', 30, now(), now(), 'admin', 'admin', 0);
#end
#if($dictMap.entrySet().size() > 0)
@@ -23,7 +23,7 @@ VALUES
INSERT INTO dict_key
(`key_name`, `value_type`, `extra_schema`, `description`, `create_time`, `update_time`, `creator`, `updater`, `deleted`)
VALUES
('$enumEntity.value.keyName', 'STRING', '$enumEntity.value.extraSchema', '$enumEntity.value.comment', now(), now(), '1', '1', 0);
('$enumEntity.value.keyName', 'STRING', '$enumEntity.value.extraSchema', '$enumEntity.value.comment', now(), now(), 'admin', 'admin', 0);
-- 设置临时配置项id
SELECT @TMP_KEY_ID:=LAST_INSERT_ID();
@@ -35,7 +35,7 @@ VALUES
#set($count = $enumEntity.value.fields.size() - 1)
#foreach($index in [0..$count])
#set($sort = $index * 10 + 10)
(@TMP_KEY_ID, '$enumEntity.value.keyName', '$enumEntity.value.values.get($index)', '$enumEntity.value.labels.get($index)', #if($enumEntity.value.extraJson.size() > $index)'$enumEntity.value.extraJson.get($index)'#else'{}'#end, $sort, now(), now(), '1', '1', 0)#if($foreach.hasNext),#else;#end
(@TMP_KEY_ID, '$enumEntity.value.keyName', '$enumEntity.value.values.get($index)', '$enumEntity.value.labels.get($index)', #if($enumEntity.value.extraJson.size() > $index)'$enumEntity.value.extraJson.get($index)'#else'{}'#end, $sort, now(), now(), 'admin', 'admin', 0)#if($foreach.hasNext),#else;#end
#end
#end

View File

@@ -4,7 +4,7 @@
INSERT INTO system_menu
(parent_id, name, type, sort, visible, status, cache, component, creator, updater, deleted)
VALUES
(0, '${table.comment}管理', 1, 10, 1, 1, 1, '${vue.moduleEntityFirstLower}Module', '1', '1', 0);
(0, '${table.comment}管理', 1, 10, 1, 1, 1, '${vue.moduleEntityFirstLower}Module', 'admin', 'admin', 0);
-- 设置临时父菜单id
SELECT @TMP_PARENT_ID:=LAST_INSERT_ID();
@@ -13,7 +13,7 @@ SELECT @TMP_PARENT_ID:=LAST_INSERT_ID();
INSERT INTO system_menu
(parent_id, name, type, sort, visible, status, cache, component, creator, updater, deleted)
VALUES
(@TMP_PARENT_ID, '$table.comment', 2, 10, 1, 1, 1, '$vue.featureEntityFirstLower', '1', '1', 0);
(@TMP_PARENT_ID, '$table.comment', 2, 10, 1, 1, 1, '$vue.featureEntityFirstLower', 'admin', 'admin', 0);
-- 设置临时子菜单id
SELECT @TMP_SUB_ID:=LAST_INSERT_ID();
@@ -22,7 +22,7 @@ SELECT @TMP_SUB_ID:=LAST_INSERT_ID();
INSERT INTO system_menu
(parent_id, name, permission, type, sort, creator, updater, deleted)
VALUES
(@TMP_SUB_ID, '查询$table.comment', '${package.ModuleName}:${typeHyphen}:query', 3, 10, '1', '1', 0),
(@TMP_SUB_ID, '创建$table.comment', '${package.ModuleName}:${typeHyphen}:create', 3, 20, '1', '1', 0),
(@TMP_SUB_ID, '修改$table.comment', '${package.ModuleName}:${typeHyphen}:update', 3, 30, '1', '1', 0),
(@TMP_SUB_ID, '删除$table.comment', '${package.ModuleName}:${typeHyphen}:delete', 3, 40, '1', '1', 0);
(@TMP_SUB_ID, '查询$table.comment', '${package.ModuleName}:${typeHyphen}:query', 3, 10, 'admin', 'admin', 0),
(@TMP_SUB_ID, '创建$table.comment', '${package.ModuleName}:${typeHyphen}:create', 3, 20, 'admin', 'admin', 0),
(@TMP_SUB_ID, '修改$table.comment', '${package.ModuleName}:${typeHyphen}:update', 3, 30, 'admin', 'admin', 0),
(@TMP_SUB_ID, '删除$table.comment', '${package.ModuleName}:${typeHyphen}:delete', 3, 40, 'admin', 'admin', 0);

View File

@@ -8,7 +8,7 @@ const $vue.moduleConst: AppRouteRecordRaw = {
children: [
{
name: '$vue.featureEntityFirstLower',
path: '/$vue.feature',
path: '/$vue.module/$vue.feature',
component: () => import('@/views/$vue.module/$vue.feature/index.vue'),
},
],

View File

@@ -3,7 +3,7 @@ import { dateFormat } from '@/utils';
const fieldConfig = {
rowGap: '10px',
labelSpan: 8,
labelSpan: 6,
minHeight: '22px',
fields: [
{

View File

@@ -38,7 +38,27 @@ public class StaticResourceAuthorizeRequestsCustomizer extends AuthorizeRequests
@Override
public void customize(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) {
// 静态资源可匿名访问
registry.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll();
registry.antMatchers(HttpMethod.GET,
"/*.html",
"/*.css",
"/*.js",
"/*.gz",
"/*.ico",
"/*.jpg",
"/*.png",
"/*.svg",
"/*.json",
"/*.webmanifest",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/**/*.gz",
"/**/*.ico",
"/**/*.jpg",
"/**/*.png",
"/**/*.svg",
"/**/*.json"
).permitAll();
}
}

View File

@@ -24,6 +24,7 @@ package org.dromara.visor.framework.test.core.base;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import org.dromara.visor.common.configuration.SpringConfiguration;
import org.dromara.visor.framework.datasource.configuration.OrionDataSourceAutoConfiguration;
import org.dromara.visor.framework.mybatis.configuration.OrionMybatisAutoConfiguration;
import org.dromara.visor.framework.redis.configuration.OrionRedisAutoConfiguration;
@@ -57,6 +58,8 @@ import org.springframework.transaction.annotation.Transactional;
public class BaseUnitTest {
@Import({
// spring
SpringConfiguration.class,
// mock
OrionMockBeanTestConfiguration.class,
OrionMockRedisTestConfiguration.class,
@@ -74,7 +77,6 @@ public class BaseUnitTest {
RedisAutoConfiguration.class,
RedissonAutoConfiguration.class,
})
// TODO
public static class Application {
}

View File

@@ -42,7 +42,11 @@ import java.util.Optional;
* @version 1.0.0
* @since 2023/6/19 16:55
*/
@SpringBootApplication(scanBasePackages = {"org.dromara.visor.launch", "org.dromara.visor.module"})
@SpringBootApplication(scanBasePackages = {
"org.dromara.visor.launch",
"org.dromara.visor.common",
"org.dromara.visor.module"
})
public class LaunchApplication {
public static void main(String[] args) {

View File

@@ -39,9 +39,9 @@ import java.util.function.Function;
*/
public class ReplaceVersion {
private static final String TARGET_VERSION = "2.3.3";
private static final String TARGET_VERSION = "2.3.4";
private static final String REPLACE_VERSION = "2.3.4";
private static final String REPLACE_VERSION = "2.3.5";
private static final String PATH = new File("").getAbsolutePath();

View File

@@ -82,6 +82,10 @@
<groupId>org.dromara.visor</groupId>
<artifactId>orion-visor-spring-boot-starter-job</artifactId>
</dependency>
<dependency>
<groupId>org.dromara.visor</groupId>
<artifactId>orion-visor-spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -63,7 +63,7 @@ public class AssetAuthorizedDataServiceController {
@IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/current-host")
@Operation(summary = "查询当前用户已授权的主机")
public AuthorizedHostWrapperVO getCurrentAuthorizedHost(@RequestParam("type") String type) {
public AuthorizedHostWrapperVO getCurrentAuthorizedHost(@RequestParam(value = "type", required = false) String type) {
return assetAuthorizedDataService.getUserAuthorizedHost(SecurityUtils.getLoginUserId(), type);
}

View File

@@ -49,7 +49,17 @@ public interface TerminalConnectLogDAO extends IMapper<TerminalConnectLogDO> {
* @param limit limit
* @return hostId
*/
List<Long> selectLatestConnectHostId(@Param("userId") Long userId, @Param("type") String type, @Param("limit") Integer limit);
default List<Long> selectLatestConnectHostId(Long userId, String type, Integer limit) {
return this.of()
.createWrapper(true)
.select(TerminalConnectLogDO::getHostId)
.eq(TerminalConnectLogDO::getUserId, userId)
.eq(TerminalConnectLogDO::getType, type)
.orderByDesc(TerminalConnectLogDO::getId)
.then()
.limit(limit)
.list(TerminalConnectLogDO::getHostId);
}
/**
* 查询终端连接日志用户数量

View File

@@ -31,10 +31,10 @@ package org.dromara.visor.module.asset.handler.host.jsch;
*/
public interface SessionMessage {
String AUTHENTICATION_FAILURE = "authentication failed. please check the configuration. - {}";
String AUTHENTICATION_FAILURE = "身份认证失败. {}";
String SERVER_UNREACHABLE = "remote server unreachable. please check the configuration. - {}";
String SERVER_UNREACHABLE = "无法连接至服务器. {}";
String CONNECTION_TIMEOUT = "connection timeout. - {}";
String CONNECTION_TIMEOUT = "连接服务器超时. {}";
}

View File

@@ -135,13 +135,6 @@ public interface HostService {
*/
void deleteHostRelByIdListAsync(List<Long> idList);
/**
* 获取当前更新配置的 hostId
*
* @return hostId
*/
Long getCurrentUpdateConfigHostId();
/**
* 清除缓存
*/

View File

@@ -83,8 +83,6 @@ import java.util.stream.Collectors;
@Service
public class HostServiceImpl implements HostService {
private static final ThreadLocal<Long> CURRENT_UPDATE_CONFIG_ID = new ThreadLocal<>();
@Resource
private HostDAO hostDAO;
@@ -185,30 +183,25 @@ public class HostServiceImpl implements HostService {
OperatorLogs.add(ExtraFieldConst.CONFIG, param);
log.info("HostService-updateHostConfig request: {}", param);
Long id = request.getId();
try {
CURRENT_UPDATE_CONFIG_ID.set(id);
// 查询主机信息
HostDO host = hostDAO.selectById(id);
Valid.notNull(host, ErrorMessage.HOST_ABSENT);
HostTypeEnum type = Valid.valid(HostTypeEnum::of, host.getType());
GenericsDataModel beforeConfig = type.parse(host.getConfig());
GenericsDataModel newConfig = type.parse(request.getConfig());
// 添加日志参数
OperatorLogs.add(OperatorLogs.ID, id);
OperatorLogs.add(OperatorLogs.NAME, host.getName());
// 更新前校验
type.doValid(beforeConfig, newConfig);
// 修改配置
HostDO updateHost = HostDO.builder()
.id(id)
.config(newConfig.serial())
.build();
int effect = hostDAO.updateById(updateHost);
log.info("HostService-updateHostConfig effect: {}", effect);
return effect;
} finally {
CURRENT_UPDATE_CONFIG_ID.remove();
}
// 查询主机信息
HostDO host = hostDAO.selectById(id);
Valid.notNull(host, ErrorMessage.HOST_ABSENT);
HostTypeEnum type = Valid.valid(HostTypeEnum::of, host.getType());
GenericsDataModel beforeConfig = type.parse(host.getConfig());
GenericsDataModel newConfig = type.parse(request.getConfig());
// 添加日志参数
OperatorLogs.add(OperatorLogs.ID, id);
OperatorLogs.add(OperatorLogs.NAME, host.getName());
// 更新前校验
type.doValid(beforeConfig, newConfig);
// 修改配置
HostDO updateHost = HostDO.builder()
.id(id)
.config(newConfig.serial())
.build();
int effect = hostDAO.updateById(updateHost);
log.info("HostService-updateHostConfig effect: {}", effect);
return effect;
}
@Override
@@ -348,11 +341,6 @@ public class HostServiceImpl implements HostService {
dataExtraApi.deleteByRelIdList(DataExtraTypeEnum.HOST, idList);
}
@Override
public Long getCurrentUpdateConfigHostId() {
return CURRENT_UPDATE_CONFIG_ID.get();
}
@Override
public void clearCache() {
RedisMaps.scanKeysDelete(HostCacheKeyDefine.HOST_INFO.format("*"));

View File

@@ -48,6 +48,7 @@ import org.dromara.visor.module.asset.enums.HostIdentityTypeEnum;
import org.dromara.visor.module.asset.enums.HostSshAuthTypeEnum;
import org.dromara.visor.module.asset.handler.host.config.model.HostSshConfigModel;
import org.dromara.visor.module.asset.handler.host.extra.model.HostSshExtraModel;
import org.dromara.visor.module.asset.service.AssetAuthorizedDataService;
import org.dromara.visor.module.asset.service.HostConfigService;
import org.dromara.visor.module.asset.service.HostExtraService;
import org.dromara.visor.module.asset.service.TerminalService;
@@ -81,7 +82,7 @@ public class TerminalServiceImpl implements TerminalService {
private HostExtraService hostExtraService;
@Resource
private AssetAuthorizedDataServiceImpl assetAuthorizedDataService;
private AssetAuthorizedDataService assetAuthorizedDataService;
@Resource
private HostDAO hostDAO;

View File

@@ -33,16 +33,6 @@
id, user_id, username, host_id, host_name, host_address, type, session_id, status, start_time, end_time, extra_info, create_time, update_time, deleted
</sql>
<select id="selectLatestConnectHostId" resultType="java.lang.Long">
SELECT host_id
FROM terminal_connect_log
WHERE deleted = 0
AND type = #{type}
AND user_id = #{userId}
ORDER BY id DESC
LIMIT #{limit}
</select>
<select id="selectConnectLogUserCount" resultMap="CountResultMap">
SELECT DATE(create_time) connect_date, COUNT(1) total_count
FROM terminal_connect_log

View File

@@ -3,4 +3,4 @@ VITE_API_BASE_URL=http://127.0.0.1:9200/orion-visor/api
# websocket 路径
VITE_WS_BASE_URL=ws://127.0.0.1:9200/orion-visor/keep-alive
# 版本号
VITE_APP_VERSION=2.3.4
VITE_APP_VERSION=2.3.5

View File

@@ -3,4 +3,4 @@ VITE_API_BASE_URL=/orion-visor/api
# websocket 路径
VITE_WS_BASE_URL=/orion-visor/keep-alive
# 版本号
VITE_APP_VERSION=2.3.4
VITE_APP_VERSION=2.3.5

View File

@@ -1,7 +1,7 @@
{
"name": "orion-visor-ui",
"description": "Orion Visor UI",
"version": "2.3.4",
"version": "2.3.5",
"private": true,
"author": "Jiahang Li",
"license": "Apache 2.0",

View File

@@ -58,7 +58,7 @@
v-model:selected-group="selectedGroup"
:host-list="hostList"
:groups="hosts?.groupTree as any"
:nodes="treeNodes" />
:nodes="treeNodes as any" />
<!-- 列表视图 -->
<host-table v-else
v-model:selected-keys="selectedKeys"
@@ -83,13 +83,13 @@
import { onMounted, ref, watch, computed } from 'vue';
import { dataColor } from '@/utils';
import { dictKeys, NewConnectionType, newConnectionTypeKey } from './types/const';
import { useDictStore } from '@/store';
import { useCacheStore, useDictStore } from '@/store';
import { tagLabelFilter } from '@/types/form';
import { tagColor } from '@/views/asset/host-list/types/const';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import { getCurrentAuthorizedHost } from '@/api/asset/asset-authorized-data';
import { getAuthorizedHostOptions } from '@/types/options';
import { getLatestConnectHostId } from '@/api/asset/terminal-connect-log';
import HostTable from './components/host-table.vue';
import HostGroup from './components/host-group.vue';
@@ -151,10 +151,13 @@
setLoading(true);
try {
// 加载主机列表
const { data } = await getCurrentAuthorizedHost(props.type);
hosts.value = data;
const data = await useCacheStore().loadAuthorizedHosts(props.type);
// 禁用别名
data.hostList.forEach(s => s.alias = undefined as unknown as string);
// 查询最近连接的主机
const { data: latestHosts } = await getLatestConnectHostId(props.type as string, 30);
data.latestHosts = latestHosts;
hosts.value = data;
// 设置主机搜索选项
filterOptions.value = getAuthorizedHostOptions(data.hostList);
} catch (e) {

View File

@@ -27,7 +27,7 @@
<script lang="ts" setup>
import type { ExecLogQueryResponse } from '@/api/exec/exec-log';
import type { ExecType, ILogAppender } from '../const';
import { onUnmounted, ref, nextTick, onMounted } from 'vue';
import { onUnmounted, ref, nextTick, onMounted, markRaw } from 'vue';
import { getExecCommandLogStatus } from '@/api/exec/exec-command-log';
import { getExecJobLogStatus } from '@/api/exec/exec-job-log';
import { dictKeys, ExecHostStatus, ExecStatus } from '../const';
@@ -58,11 +58,11 @@
const { log_webScrollLines } = await useCacheStore().loadSystemSetting();
const scrollLines = toAnonymousNumber(log_webScrollLines) || 1000;
// 创建 appender
appender.value = new LogAppender({
appender.value = markRaw(new LogAppender({
id: record.id,
type: props.type,
scrollLines,
});
}));
// 定时查询执行状态
if (record.status === ExecStatus.WAITING ||
record.status === ExecStatus.RUNNING) {

View File

@@ -8,17 +8,17 @@ const ASSET_AUDIT: AppRouteRecordRaw = {
children: [
{
name: 'connectLog',
path: '/connect-log',
path: '/audit/connect-log',
component: () => import('@/views/asset-audit/connect-log/index.vue'),
},
{
name: 'connectSession',
path: '/connect-session',
path: '/audit/connect-session',
component: () => import('@/views/asset-audit/connect-session/index.vue'),
},
{
name: 'sftpLog',
path: '/sftp-log',
path: '/audit/sftp-log',
component: () => import('@/views/asset-audit/sftp-log/index.vue'),
},
],

View File

@@ -8,19 +8,19 @@ const ASSET: AppRouteRecordRaw = {
children: [
{
name: 'hostList',
path: '/host-list',
path: '/asset/host',
component: () => import('@/views/asset/host-list/index.vue'),
}, {
name: 'hostKey',
path: '/host-key',
path: '/asset/host-key',
component: () => import('@/views/asset/host-key/index.vue'),
}, {
name: 'hostIdentity',
path: '/host-identity',
path: '/asset/host-identity',
component: () => import('@/views/asset/host-identity/index.vue'),
}, {
name: 'assetGrant',
path: '/asset-grant',
path: '/asset/grant',
component: () => import('@/views/asset/grant/index.vue'),
},
],

View File

@@ -9,37 +9,37 @@ const EXEC: Array<AppRouteRecordRaw> = [
children: [
{
name: 'execCommand',
path: '/exec-command',
path: '/exec/command',
component: () => import('@/views/exec/exec-command/index.vue'),
},
{
name: 'execCommandLog',
path: '/exec-log',
path: '/exec/command-log',
component: () => import('@/views/exec/exec-command-log/index.vue'),
},
{
name: 'execJob',
path: '/exec-job',
path: '/exec/job',
component: () => import('@/views/exec/exec-job/index.vue'),
},
{
name: 'execJobLog',
path: '/exec-job-log',
path: '/exec/job-log',
component: () => import('@/views/exec/exec-job-log/index.vue'),
},
{
name: 'batchUpload',
path: '/batch-upload',
path: '/exec/upload',
component: () => import('@/views/exec/batch-upload/index.vue'),
},
{
name: 'uploadTask',
path: '/upload-task',
path: '/exec/upload-task',
component: () => import('@/views/exec/upload-task/index.vue'),
},
{
name: 'execTemplate',
path: '/exec-template',
path: '/exec/template',
component: () => import('@/views/exec/exec-template/index.vue'),
},
],
@@ -50,7 +50,7 @@ const EXEC: Array<AppRouteRecordRaw> = [
children: [
{
name: 'execJobLogView',
path: '/job-log-view',
path: '/exec/job-log/view',
component: () => import('@/views/exec/exec-job-log-view/index.vue'),
},
],

View File

@@ -8,17 +8,17 @@ const SYSTEM: AppRouteRecordRaw = {
children: [
{
name: 'systemMenu',
path: '/menu',
path: '/system/menu',
component: () => import('@/views/system/menu/index.vue'),
},
{
name: 'dictKey',
path: '/dict-key',
path: '/system/dict-key',
component: () => import('@/views/system/dict-key/index.vue'),
},
{
name: 'dictValue',
path: '/dict-value',
path: '/system/dict-value',
component: () => import('@/views/system/dict-value/index.vue'),
},
{

View File

@@ -8,22 +8,22 @@ const USER: AppRouteRecordRaw = {
children: [
{
name: 'role',
path: '/role',
path: '/user/role',
component: () => import('@/views/user/role/index.vue'),
},
{
name: 'user',
path: '/user',
path: '/user/list',
component: () => import('@/views/user/user/index.vue'),
},
{
name: 'userInfo',
path: '/user-info',
path: '/user/info',
component: () => import('@/views/user/info/index.vue'),
},
{
name: 'operatorLog',
path: '/operator-log',
path: '/user/operator-log',
component: () => import('@/views/user/operator-log/index.vue'),
},
],

View File

@@ -15,7 +15,7 @@ import { getHostKeyList } from '@/api/asset/host-key';
import { getHostIdentityList } from '@/api/asset/host-identity';
import { getHostGroupTree } from '@/api/asset/host-group';
import { getMenuList } from '@/api/system/menu';
import { getCurrentAuthorizedHostIdentity, getCurrentAuthorizedHostKey } from '@/api/asset/asset-authorized-data';
import { getCurrentAuthorizedHost, getCurrentAuthorizedHostIdentity, getCurrentAuthorizedHostKey } from '@/api/asset/asset-authorized-data';
import { getCommandSnippetGroupList } from '@/api/asset/command-snippet-group';
import { getExecJobList } from '@/api/exec/exec-job';
import { getPathBookmarkGroupList } from '@/api/asset/path-bookmark-group';
@@ -99,8 +99,8 @@ export default defineStore('cache', {
},
// 获取主机列表
async loadHosts(type: HostType, force = false) {
return await this.load(`host_${type}`, () => getHostList(type), ['asset:host:query'], force);
async loadHosts(type: HostType = '', force = false) {
return await this.load(`host_${type || 'ALL'}`, () => getHostList(type), ['asset:host:query'], force);
},
// 获取主机密钥列表
@@ -123,6 +123,11 @@ export default defineStore('cache', {
return await this.load(`${type}_Tags`, () => getTagList(type), undefined, force);
},
// 获取已授权的主机列表
async loadAuthorizedHosts(type: HostType = '', force = false) {
return await this.load(`authorizedHost_${type || 'ALL'}`, () => getCurrentAuthorizedHost(type), undefined, force);
},
// 获取已授权的主机密钥列表
async loadAuthorizedHostKeys(force = false) {
return await this.load('authorizedHostKeys', getCurrentAuthorizedHostKey, undefined, force);

View File

@@ -1,6 +1,7 @@
// 缓存类型
export type CacheType = 'users' | 'menus' | 'roles'
| 'hostGroups' | 'hostKeys' | 'hostIdentities' | 'host_*'
| 'hostGroups' | 'host_*' | 'authorizedHost_*'
| 'hostKeys' | 'hostIdentities'
| 'dictKeys'
| 'execJob'
| 'authorizedHostKeys' | 'authorizedHostIdentities'

View File

@@ -10,16 +10,17 @@ import type {
} from './types';
import type { ISshSession, ITerminalSession, PanelSessionTabType, TerminalPanelTabItem } from '@/views/host/terminal/types/define';
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
import { getCurrentAuthorizedHost } from '@/api/asset/asset-authorized-data';
import type { HostQueryResponse } from '@/api/asset/host';
import type { TerminalTheme, TerminalThemeSchema } from '@/api/asset/terminal';
import { getTerminalThemes } from '@/api/asset/terminal';
import { markRaw } from 'vue';
import { defineStore } from 'pinia';
import { getPreference, updatePreference } from '@/api/user/preference';
import { getLatestConnectHostId } from '@/api/asset/terminal-connect-log';
import { nextId } from '@/utils';
import { isObject } from '@/utils/is';
import { Message } from '@arco-design/web-vue';
import { useCacheStore } from '@/store';
import { PanelSessionType, TerminalTabs } from '@/views/host/terminal/types/const';
import TerminalTabManager from '@/views/host/terminal/handler/terminal-tab-manager';
import TerminalSessionManager from '@/views/host/terminal/handler/terminal-session-manager';
@@ -73,7 +74,7 @@ export default defineStore('terminal', {
hosts: {} as AuthorizedHostQueryResponse,
tabManager: new TerminalTabManager(),
panelManager: new TerminalPanelManager(),
sessionManager: new TerminalSessionManager(),
sessionManager: markRaw(new TerminalSessionManager()),
transferManager: new SftpTransferManager(),
}),
@@ -130,18 +131,18 @@ export default defineStore('terminal', {
},
// 加载主机列表
async loadHosts() {
async loadHostList() {
if (this.hosts.hostList?.length) {
return;
}
// 查询授权主机
const { data } = await getCurrentAuthorizedHost('SSH');
const data = await useCacheStore().loadAuthorizedHosts();
Object.keys(data).forEach(k => {
this.hosts[k as keyof AuthorizedHostQueryResponse] = data[k as keyof AuthorizedHostQueryResponse] as any;
});
this.hosts.latestHosts = [];
// 查询最近连接的主机
const { data: latestHosts } = await getLatestConnectHostId('SSH', 30);
const { data: latestHosts } = await getLatestConnectHostId('', 30);
this.hosts.latestHosts = latestHosts;
},

View File

@@ -1,5 +1,4 @@
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
import { dateFormat } from '@/utils';
const columns = [
{
@@ -32,8 +31,8 @@ const columns = [
title: '状态',
dataIndex: 'status',
slotName: 'status',
align: 'center',
width: 106,
align: 'left',
width: 118,
}, {
title: '留痕地址',
dataIndex: 'address',

View File

@@ -93,7 +93,7 @@
try {
// 加载组内数据
const { data } = await getHostGroupRelList(groupId as number);
const hosts = await cacheStore.loadHosts('');
const hosts = await cacheStore.loadHosts();
selectedGroupHosts.value = data.map(s => hosts.find(h => h.id === s) as HostQueryResponse)
.filter(Boolean);
} catch (e) {
@@ -138,6 +138,8 @@
idList: checkedGroups.value
});
Message.success('授权成功');
// 清空缓存
cacheStore.reset('authorizedHost_ALL', 'authorizedHost_SSH');
} catch (e) {
} finally {
setLoading(false);

View File

@@ -96,6 +96,8 @@
idList: selectedKeys.value
});
Message.success('授权成功');
// 清空缓存
cacheStore.reset('authorizedHostIdentities');
} catch (e) {
} finally {
setLoading(false);

View File

@@ -72,6 +72,8 @@
idList: selectedKeys.value
});
Message.success('授权成功');
// 清空缓存
cacheStore.reset('authorizedHostKeys');
} catch (e) {
} finally {
setLoading(false);

View File

@@ -113,7 +113,7 @@
// 加载主机列表
const loadHosts = () => {
cacheStore.loadHosts('').then(hosts => {
cacheStore.loadHosts().then(hosts => {
data.value = hosts.map(s => {
return {
value: String(s.id),

View File

@@ -3,7 +3,7 @@ import { dateFormat } from '@/utils';
const fieldConfig = {
rowGap: '10px',
labelSpan: 8,
labelSpan: 6,
minHeight: '22px',
fields: [
{

View File

@@ -19,6 +19,8 @@ const columns = [
title: '用户名',
dataIndex: 'username',
slotName: 'username',
ellipsis: true,
tooltip: true
}, {
title: '类型',
dataIndex: 'type',
@@ -28,6 +30,7 @@ const columns = [
title: '主机密钥',
dataIndex: 'keyId',
slotName: 'keyId',
width: 180,
}, {
title: '描述',
dataIndex: 'description',

View File

@@ -3,7 +3,7 @@ import { dateFormat } from '@/utils';
const fieldConfig = {
rowGap: '10px',
labelSpan: 8,
labelSpan: 6,
minHeight: '22px',
fields: [
{

View File

@@ -339,7 +339,8 @@
// 重新加载数据
fetchCardData();
// 清空缓存
cacheStore.reset('host_', 'host_SSH');
cacheStore.reset('host_ALL', 'host_SSH',
'authorizedHost_ALL', 'authorizedHost_SSH');
};
defineExpose({ reload });

View File

@@ -373,7 +373,8 @@
// 重新加载数据
fetchTableData();
// 清空缓存
cacheStore.reset('host_', 'host_SSH');
cacheStore.reset('host_ALL', 'host_SSH',
'authorizedHost_ALL', 'authorizedHost_SSH');
};
defineExpose({ reload });

View File

@@ -2,7 +2,7 @@ import type { CardField, CardFieldConfig } from '@/types/card';
const fieldConfig = {
rowGap: '10px',
labelSpan: 8,
labelSpan: 6,
minHeight: '22px',
fields: [
{

View File

@@ -135,10 +135,10 @@
const { chartOption: terminalConnectChart } = useChartOption(() => {
return {
grid: {
left: 0,
right: 0,
top: 10,
bottom: 0,
left: 8,
right: 8,
top: 8,
bottom: 8,
},
xAxis: {
type: 'category',
@@ -176,10 +176,10 @@
const { chartOption: execCommandChart } = useChartOption(() => {
return {
grid: {
left: 0,
right: 0,
top: 10,
bottom: 0,
left: 8,
right: 8,
top: 8,
bottom: 8,
},
xAxis: {
type: 'category',

View File

@@ -1,5 +1,5 @@
<template>
<div class="terminal-example" ref="terminal" />
<div class="terminal-example" ref="terminalRef" />
</template>
<script lang="ts">
@@ -11,31 +11,32 @@
<script lang="ts" setup>
import type { TerminalThemeSchema } from '@/api/asset/terminal';
import { Terminal } from '@xterm/xterm';
import { onMounted, onUnmounted, ref } from 'vue';
import { markRaw, onMounted, onUnmounted, ref } from 'vue';
const props = defineProps<{
schema: TerminalThemeSchema | Record<string, any>;
}>();
const terminal = ref();
const terminalRef = ref();
const term = ref();
onMounted(() => {
term.value = new Terminal({
const terminal = new Terminal({
theme: { ...props.schema, cursor: props.schema.background },
cols: 42,
rows: 6,
fontSize: 15,
cursorInactiveStyle: 'none',
});
term.value.open(terminal.value);
term.value.write(
terminal.open(terminalRef.value);
terminal.write(
'[root@OrionServer usr]#\r\n' +
'dr-xr-xr-x. 2 root root bin\r\n' +
'dr-xr-xr-x. 2 root root sbin\r\n' +
'drwxr-xr-x. 4 root root src\r\n' +
'lrwxrwxrwx. 1 root root tmp -> ../var/tmp '
);
term.value = markRaw(terminal);
});
defineExpose({ term });

View File

@@ -45,7 +45,7 @@
</div>
</div>
<!-- 已关闭-右侧操作 -->
<div v-if="session?.connected === false && closeMessage !== undefined"
<div v-if="session?.status.connected === false && closeMessage !== undefined"
class="sftp-table-header-right">
<!-- 错误信息 -->
<a-tag class="close-message"
@@ -54,7 +54,7 @@
已断开: {{ closeMessage }}
</a-tag>
<!-- 重连 -->
<a-tooltip v-if="session?.canReconnect"
<a-tooltip v-if="session?.status.canReconnect"
position="top"
:mini="true"
:overlay-inverse="true"
@@ -245,7 +245,7 @@
// 设置命令编辑模式
const setPathEditable = (editable: boolean) => {
// 检查是否断开
if (editable && !props.session?.connected) {
if (editable && !props.session?.status.connected) {
return;
}
pathEditable.value = editable;
@@ -267,7 +267,7 @@
// 加载文件列表
const loadFileList = (path: string = props.currentPath) => {
// 检查是否断开
if (!props.session?.connected) {
if (!props.session?.status.connected) {
return;
}
emits('loadFile', path);

View File

@@ -205,7 +205,7 @@
const clickFilename = (record: TableData) => {
if (record.isDir) {
// 检查是否断开
if (!props.session?.connected) {
if (!props.session?.status.connected) {
return;
}
// 进入文件夹
@@ -218,7 +218,7 @@
// 编辑文件
const editFile = (record: TableData) => {
// 检查是否断开
if (!props.session?.connected) {
if (!props.session?.status.connected) {
return;
}
emits('editFile', record.name, record.path);
@@ -228,7 +228,7 @@
// 删除文件
const deleteFile = (path: string) => {
// 检查是否断开
if (!props.session?.connected) {
if (!props.session?.status.connected) {
return;
}
emits('deleteFile', [path]);
@@ -237,7 +237,7 @@
// 下载文件
const downloadFile = (path: string) => {
// 检查是否断开
if (!props.session?.connected) {
if (!props.session?.status.connected) {
return;
}
emits('download', [path], false);
@@ -246,7 +246,7 @@
// 移动文件
const moveFile = (path: string) => {
// 检查是否断开
if (!props.session?.connected) {
if (!props.session?.status.connected) {
return;
}
openSftpMoveModal(props.session?.sessionId as string, path);
@@ -255,7 +255,7 @@
// 文件提权
const chmodFile = (path: string, permission: number) => {
// 检查是否断开
if (!props.session?.connected) {
if (!props.session?.status.connected) {
return;
}
openSftpChmodModal(props.session?.sessionId as string, path, permission);

View File

@@ -23,8 +23,8 @@
<!-- 连接状态 -->
<a-badge v-if="preference.actionBarSetting.connectStatus !== false"
class="status-bridge"
:status="getDictValue(sessionStatusKey, session ? session.status : 0, 'status')"
:text="getDictValue(sessionStatusKey, session ? session.status : 0)" />
:status="getDictValue(sessionStatusKey, session ? session.status.connectStatus : 0, 'status')"
:text="getDictValue(sessionStatusKey, session ? session.status.connectStatus : 0)" />
</div>
</div>
</template>

View File

@@ -64,7 +64,7 @@
// 发送命令
const writeCommand = (value: string) => {
if (session.value?.canWrite) {
if (session.value?.status.canWrite) {
session.value?.handler.pasteTrimEnd(value);
}
};

View File

@@ -1,50 +1,66 @@
import type { ITerminalSession, TerminalPanelTabItem } from '../types/define';
import type { ITerminalSession, TerminalPanelTabItem, TerminalStatus } from '../types/define';
import type { Reactive } from 'vue';
import { reactive } from 'vue';
import { TerminalSessionStatus } from '@/views/host/terminal/types/const';
// 会话基类
export default abstract class BaseSession implements ITerminalSession {
export default abstract class BaseSession<Status extends TerminalStatus> implements ITerminalSession<Status> {
public type: string;
public hostId: number;
public title: string;
public address: string;
public readonly type: string;
public readonly hostId: number;
public readonly title: string;
public readonly address: string;
public readonly status: Reactive<Status>;
public sessionId: string;
public connected: boolean;
public canReconnect: boolean;
public canWrite: boolean;
protected constructor(type: string, tab: TerminalPanelTabItem) {
protected constructor(type: string, tab: TerminalPanelTabItem, status: Partial<Status>) {
this.type = type;
this.hostId = tab.hostId;
this.title = tab.title;
this.address = tab.address;
this.sessionId = tab.sessionId;
this.connected = false;
this.canWrite = false;
this.canReconnect = false;
}
// 设置是否可写
setCanWrite(canWrite: boolean): void {
this.canWrite = canWrite;
}
// 设置已连接
setConnected(): void {
this.connected = true;
this.status = reactive({
connectStatus: TerminalSessionStatus.CONNECTING,
connected: false,
canWrite: false,
canReconnect: false,
...status,
} as Status);
}
// 连接会话
connect(): void {
this.status.connectStatus = TerminalSessionStatus.CONNECTING;
}
// 断开连接
disconnect(): void {
this.connected = false;
// 设置已关闭
this.setClosed();
}
// 关闭
close(): void {
this.connected = false;
// 设置已关闭
this.setClosed();
}
// 设置是否可写
setCanWrite(canWrite: boolean): void {
this.status.canWrite = canWrite;
}
// 设置已连接
setConnected(): void {
this.status.connected = true;
this.status.connectStatus = TerminalSessionStatus.CONNECTED;
}
// 设置已关闭
setClosed(): void {
this.status.connected = false;
this.status.canWrite = false;
this.status.connectStatus = TerminalSessionStatus.CLOSED;
}
}

View File

@@ -1,4 +1,4 @@
import type { ISftpSession, ISftpSessionResolver, ITerminalChannel, TerminalPanelTabItem } from '../types/define';
import type { ISftpSession, ISftpSessionResolver, ITerminalChannel, TerminalPanelTabItem, TerminalStatus } from '../types/define';
import { h } from 'vue';
import { InputProtocol } from '@/types/protocol/terminal.protocol';
import { PanelSessionType } from '../types/const';
@@ -6,7 +6,7 @@ import { Modal } from '@arco-design/web-vue';
import BaseSession from './base-session';
// sftp 会话实现
export default class SftpSession extends BaseSession implements ISftpSession {
export default class SftpSession extends BaseSession<TerminalStatus> implements ISftpSession {
public resolver: ISftpSessionResolver;
@@ -16,7 +16,7 @@ export default class SftpSession extends BaseSession implements ISftpSession {
constructor(tab: TerminalPanelTabItem,
channel: ITerminalChannel) {
super(PanelSessionType.SFTP.type, tab);
super(PanelSessionType.SFTP.type, tab, {});
this.channel = channel;
this.showHiddenFile = false;
this.resolver = undefined as unknown as ISftpSessionResolver;
@@ -27,13 +27,6 @@ export default class SftpSession extends BaseSession implements ISftpSession {
this.resolver = resolver;
}
// 设置已连接
setConnected(): void {
super.setConnected();
// 连接回调
this.resolver.connectCallback();
}
// 连接会话
connect(): void {
super.connect();
@@ -171,4 +164,11 @@ export default class SftpSession extends BaseSession implements ISftpSession {
});
}
// 设置已连接
setConnected(): void {
super.setConnected();
// 连接回调
this.resolver.connectCallback();
}
}

View File

@@ -91,9 +91,9 @@ export default class SshSessionHandler implements ISshSessionHandler {
case 'openSftp':
case 'uploadFile':
case 'checkAppendMissing':
return this.session.canWrite;
return this.session.status.canWrite;
case 'disconnect':
return this.session.connected;
return this.session.status.connected;
default:
return true;
}
@@ -197,7 +197,7 @@ export default class SshSessionHandler implements ISshSessionHandler {
// 字号增加
private fontSizeAdd(addSize: number) {
this.inst.options['fontSize'] = this.inst.options['fontSize'] as number + addSize;
if (this.session.connected) {
if (this.session.status.connected) {
this.session.fit();
this.inst.focus();
}

View File

@@ -2,12 +2,12 @@ import type { UnwrapRef } from 'vue';
import type { ISearchOptions } from '@xterm/addon-search';
import { SearchAddon } from '@xterm/addon-search';
import type { TerminalPreference } from '@/store/modules/terminal/types';
import type { ISshSession, ISshSessionHandler, ITerminalChannel, TerminalPanelTabItem, XtermDomRef } from '../types/define';
import type { ISshSession, ISshSessionHandler, ITerminalChannel, TerminalPanelTabItem, TerminalStatus, XtermDomRef } from '../types/define';
import type { XtermAddons } from '@/types/xterm';
import { defaultFontFamily } from '@/types/xterm';
import { useTerminalStore } from '@/store';
import { InputProtocol } from '@/types/protocol/terminal.protocol';
import { PanelSessionType, TerminalSessionStatus, TerminalShortcutType } from '../types/const';
import { PanelSessionType, TerminalShortcutType } from '../types/const';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
@@ -21,12 +21,10 @@ import SshSessionHandler from './ssh-session-handler';
import BaseSession from './base-session';
// ssh 会话实现
export default class SshSession extends BaseSession implements ISshSession {
export default class SshSession extends BaseSession<TerminalStatus> implements ISshSession {
public inst: Terminal;
public status: number;
public handler: ISshSessionHandler;
private readonly channel: ITerminalChannel;
@@ -38,10 +36,9 @@ export default class SshSession extends BaseSession implements ISshSession {
constructor(tab: TerminalPanelTabItem,
channel: ITerminalChannel,
canUseWebgl: boolean) {
super(PanelSessionType.SSH.type, tab);
super(PanelSessionType.SSH.type, tab, {});
this.channel = channel;
this.canUseWebgl = canUseWebgl;
this.status = TerminalSessionStatus.CONNECTING;
this.inst = undefined as unknown as Terminal;
this.handler = undefined as unknown as ISshSessionHandler;
this.addons = {} as XtermAddons;
@@ -93,9 +90,9 @@ export default class SshSession extends BaseSession implements ISshSession {
e.preventDefault();
}
// 检查重新连接
if (!this.connected && this.canReconnect && e.key === 'Enter') {
if (!this.status.connected && this.status.canReconnect && e.key === 'Enter') {
// 防止重复回车
this.canReconnect = false;
this.status.canReconnect = false;
// 异步作用域重新连接
setTimeout(async () => {
await useTerminalStore().reOpenSession(this.sessionId);
@@ -120,7 +117,7 @@ export default class SshSession extends BaseSession implements ISshSession {
private registerEvent(dom: HTMLElement, preference: UnwrapRef<TerminalPreference>) {
// 注册输入事件
this.inst.onData(s => {
if (!this.canWrite || !this.connected) {
if (!this.status.canWrite || !this.status.connected) {
return;
}
// 输入
@@ -145,7 +142,7 @@ export default class SshSession extends BaseSession implements ISshSession {
}
// 注册 resize 事件
this.inst.onResize(({ cols, rows }) => {
if (!this.connected) {
if (!this.status.connected) {
return;
}
this.channel.send(InputProtocol.SSH_RESIZE, {
@@ -158,7 +155,7 @@ export default class SshSession extends BaseSession implements ISshSession {
addEventListen(dom, 'contextmenu', async () => {
// 右键粘贴逻辑
if (preference.interactSetting.rightClickPaste) {
if (!this.canWrite || !this.connected) {
if (!this.status.canWrite || !this.status.connected) {
return;
}
// 未开启右键选中 || 开启并无选中的内容则粘贴
@@ -204,29 +201,9 @@ export default class SshSession extends BaseSession implements ISshSession {
}
}
// 设置已连接
setConnected(): void {
super.setConnected();
// 设置状态
this.status = TerminalSessionStatus.CONNECTED;
this.inst.focus();
}
// 设置是否可写
setCanWrite(canWrite: boolean): void {
super.setCanWrite(canWrite);
if (canWrite) {
this.inst.options.cursorBlink = useTerminalStore().preference.displaySetting.cursorBlink;
} else {
this.inst.options.cursorBlink = false;
}
}
// 连接会话
connect(): void {
super.connect();
// 设置状态
this.status = TerminalSessionStatus.CONNECTING;
// 发送会话初始化请求
this.channel.send(InputProtocol.CHECK, {
sessionId: this.sessionId,
@@ -295,4 +272,20 @@ export default class SshSession extends BaseSession implements ISshSession {
}
}
// 设置已连接
setConnected(): void {
super.setConnected();
this.inst.focus();
}
// 设置是否可写
setCanWrite(canWrite: boolean): void {
super.setCanWrite(canWrite);
if (canWrite) {
this.inst.options.cursorBlink = useTerminalStore().preference.displaySetting.cursorBlink;
} else {
this.inst.options.cursorBlink = false;
}
}
}

View File

@@ -77,7 +77,7 @@ export default class TerminalChannel implements ITerminalChannel {
private closeCallback(): void {
// 关闭时将手动触发 close 消息, 有可能是其他原因关闭的, 没有接收到 close 消息, 导致已断开是终端还是显示已连接
Object.values(this.sessionManager.sessions).forEach(s => {
if (!s?.connected) {
if (!s?.status.connected) {
return;
}
// close 消息

View File

@@ -1,7 +1,7 @@
import type { ISftpSession, ISshSession, ITerminalChannel, ITerminalOutputProcessor, ITerminalSession, ITerminalSessionManager } from '../types/define';
import type { OutputPayload } from '@/types/protocol/terminal.protocol';
import { InputProtocol } from '@/types/protocol/terminal.protocol';
import { PanelSessionType, TerminalSessionStatus } from '../types/const';
import { PanelSessionType } from '../types/const';
import { useTerminalStore } from '@/store';
import { Message } from '@arco-design/web-vue';
@@ -21,7 +21,7 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
processCheck({ sessionId, result, msg }: OutputPayload): void {
const success = !!Number.parseInt(result);
const session = this.sessionManager.getSession(sessionId);
session.canReconnect = !success;
session.status.canReconnect = !success;
// 处理
this.processWithType(session, ssh => {
// ssh 会话
@@ -35,9 +35,10 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
rows: ssh.inst.rows
});
} else {
// 设置已关闭
session.setClosed();
// 未成功展示错误信息
ssh.write(`${msg || ''}\r\n\r\n输入回车重新连接...\r\n\r\n`);
ssh.status = TerminalSessionStatus.CLOSED;
}
}, sftp => {
// sftp 会话
@@ -47,6 +48,8 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
sessionId,
});
} else {
// 设置已关闭
session.setClosed();
// 未成功提示错误信息
sftp.resolver?.onClose(false, msg);
Message.error(msg || '建立 SFTP 失败');
@@ -58,29 +61,25 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
processConnect({ sessionId, result, msg }: OutputPayload): void {
const success = !!Number.parseInt(result);
const session = this.sessionManager.getSession(sessionId);
session.canReconnect = !success;
session.status.canReconnect = !success;
if (success) {
// 设置可写
session.setCanWrite(true);
// 设置已连接
session.setConnected();
} else {
// 设置已关闭
session.setClosed();
}
// 处理
this.processWithType(session, ssh => {
// ssh 会话
if (success) {
// 设置可写
ssh.setCanWrite(true);
// 设置已连接
ssh.setConnected();
} else {
// 未成功展示错误信息
if (!success) {
// ssh 会话 未成功展示错误信息
ssh.write(`${msg || ''}\r\n\r\n输入回车重新连接...\r\n\r\n`);
ssh.status = TerminalSessionStatus.CLOSED;
}
}, sftp => {
// sftp 会话
if (success) {
// 设置可写
sftp.setCanWrite(true);
// 设置已连接
sftp.setConnected();
} else {
// 未成功提示错误信息
if (!success) {
// sftp 会话 未成功提示错误信息
sftp.resolver?.onClose(false, msg);
Message.error(msg || '打开 SFTP 失败');
}
@@ -95,8 +94,9 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
return;
}
const isForceClose = !!Number.parseInt(forceClose);
session.connected = false;
session.canReconnect = !isForceClose;
session.status.canReconnect = !isForceClose;
// 设置已关闭
session.setClosed();
// 处理
this.processWithType(session, ssh => {
// ssh 拼接关闭消息
@@ -104,13 +104,7 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
if (!isForceClose) {
ssh.write('输入回车重新连接...\r\n\r\n');
}
// 设置状态
ssh.status = TerminalSessionStatus.CLOSED;
// 设置不可写
ssh.setCanWrite(false);
}, sftp => {
// 设置不可写
sftp.setCanWrite(false);
// sftp 设置状态
sftp.resolver?.onClose(isForceClose, msg);
});

View File

@@ -104,7 +104,10 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
// 移除 session
this.sessions[sessionId] = undefined as unknown as ITerminalSession;
// session 全部关闭后 关闭 channel
if (Object.values(this.sessions).filter(Boolean).every(s => !s?.connected)) {
const allClosed = Object.values(this.sessions)
.filter(Boolean)
.every(s => !s?.status.connected);
if (allClosed) {
this.reset();
}
}

View File

@@ -79,7 +79,7 @@
const {
fetchPreference, getCurrentSession, openSession,
layoutState, preference, loadHosts, hosts, tabManager, sessionManager
layoutState, preference, loadHostList, hosts, tabManager, sessionManager
} = useTerminalStore();
const { loading, setLoading } = useLoading(true);
const { enter: enterFull, exit: exitFull } = useFullscreen();
@@ -165,7 +165,7 @@
onMounted(async () => {
try {
// 加载主机
await loadHosts();
await loadHostList();
// 默认连接主机
const connect = route.query.connect as string;
if (connect) {

View File

@@ -363,7 +363,7 @@ export const TransferReceiver = {
};
// 会话关闭信息
export const sessionCloseMsg = 'session closed...';
export const sessionCloseMsg = '会话已结束...';
// 打开 settingModal key
export const openSettingModalKey = Symbol();

View File

@@ -1,6 +1,6 @@
import type { Terminal } from '@xterm/xterm';
import type { ISearchOptions } from '@xterm/addon-search';
import type { CSSProperties } from 'vue';
import type { CSSProperties, Reactive } from 'vue';
import type { HostQueryResponse } from '@/api/asset/host';
import type { InputPayload, OutputPayload, Protocol } from '@/types/protocol/terminal.protocol';
@@ -196,38 +196,47 @@ export interface XtermDomRef {
uploadModal: any;
}
// 终端会话定义
export interface ITerminalSession {
type: string;
title: string;
address: string;
hostId: number;
sessionId: string;
// 终端状态
export interface TerminalStatus {
// 连接状态
connectStatus: number;
// 是否已连接
connected: boolean;
// 是否可以重新连接
canReconnect: boolean;
// 是否可写
canWrite: boolean;
// 是否可以重新连接
canReconnect: boolean;
}
// 终端会话定义
export interface ITerminalSession<Status extends TerminalStatus = TerminalStatus> {
readonly type: string;
readonly title: string;
readonly address: string;
readonly hostId: number;
// 终端状态
readonly status: Reactive<Status>;
sessionId: string;
// 设置是否可写
setCanWrite: (canWrite: boolean) => void;
// 设置已连接
setConnected: () => void;
// 连接会话
connect: () => void;
// 断开连接
disconnect: () => void;
// 关闭
close: () => void;
// 设置是否可写
setCanWrite: (canWrite: boolean) => void;
// 设置已连接
setConnected: () => void;
// 设置已关闭
setClosed: () => void;
}
// ssh 会话定义
export interface ISshSession extends ITerminalSession {
// terminal 实例
inst: Terminal;
// 状态
status: number;
// 处理器
handler: ISshSessionHandler;

View File

@@ -22,7 +22,7 @@
</modules>
<properties>
<revision>2.3.4</revision>
<revision>2.3.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>