支持定时任务
This commit is contained in:
parent
ac22b10597
commit
81930ab61c
@ -22,6 +22,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
@ -388,6 +389,18 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
|
||||
return new DataSourceMagicDynamicRegistry(dataSourceInfoMagicResourceStorage, magicDynamicDataSource);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public TaskInfoMagicResourceStorage taskInfoMagicResourceStorage() {
|
||||
return new TaskInfoMagicResourceStorage();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public TaskMagicDynamicRegistry taskMagicDynamicRegistry(TaskInfoMagicResourceStorage taskInfoMagicResourceStorage, TaskScheduler taskScheduler) {
|
||||
return new TaskMagicDynamicRegistry(taskInfoMagicResourceStorage, taskScheduler);
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(MagicNotifyService.class)
|
||||
@ -572,6 +585,7 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
|
||||
mapping.registerController(magicWorkbenchController)
|
||||
.registerController(new MagicResourceController(configuration))
|
||||
.registerController(new MagicDataSourceController(configuration))
|
||||
.registerController(new MagicTaskController(configuration))
|
||||
.registerController(new MagicBackupController(configuration));
|
||||
}
|
||||
// 注册接收推送的接口
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
package org.ssssssss.magicapi.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.ssssssss.magicapi.config.MagicConfiguration;
|
||||
import org.ssssssss.magicapi.model.DebugRequest;
|
||||
import org.ssssssss.magicapi.model.JsonBean;
|
||||
import org.ssssssss.magicapi.model.MagicEntity;
|
||||
import org.ssssssss.magicapi.script.ScriptManager;
|
||||
import org.ssssssss.script.MagicScriptDebugContext;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
public class MagicTaskController extends MagicController implements MagicExceptionHandler{
|
||||
|
||||
public MagicTaskController(MagicConfiguration configuration) {
|
||||
super(configuration);
|
||||
}
|
||||
|
||||
@PostMapping("/task/execute")
|
||||
@ResponseBody
|
||||
public JsonBean<Object> execute(String id, HttpServletRequest request){
|
||||
MagicEntity entity = MagicConfiguration.getMagicResourceService().file(id);
|
||||
notNull(entity, FILE_NOT_FOUND);
|
||||
String script = entity.getScript();
|
||||
DebugRequest debugRequest = DebugRequest.create(request);
|
||||
MagicScriptDebugContext magicScriptContext = debugRequest.createMagicScriptContext(configuration.getDebugTimeout());
|
||||
return new JsonBean<>(ScriptManager.executeScript(script, magicScriptContext));
|
||||
}
|
||||
}
|
||||
@ -168,7 +168,8 @@ public class RequestHandler extends MagicController {
|
||||
return afterCompletion(requestEntity, value);
|
||||
}
|
||||
if (requestedFromTest) {
|
||||
String sessionAndScriptId = requestEntity.getRequestedClientId() + requestEntity.getRequestedScriptId();
|
||||
DebugRequest debugRequest = requestEntity.getDebugRequest();
|
||||
String sessionAndScriptId = debugRequest.getRequestedClientId() + debugRequest.getRequestedScriptId();
|
||||
try {
|
||||
if (context instanceof MagicScriptDebugContext) {
|
||||
WebSocketSessionManager.addMagicScriptContext(sessionAndScriptId, (MagicScriptDebugContext) context);
|
||||
@ -358,8 +359,8 @@ public class RequestHandler extends MagicController {
|
||||
} while ((parent = parent.getCause()) != null);
|
||||
if (se != null && requestEntity.isRequestedFromTest()) {
|
||||
Span.Line line = se.getLine();
|
||||
WebSocketSessionManager.sendByClientId(requestEntity.getRequestedClientId(), EXCEPTION, Arrays.asList(
|
||||
requestEntity.getRequestedScriptId(),
|
||||
WebSocketSessionManager.sendByClientId(requestEntity.getDebugRequest().getRequestedClientId(), EXCEPTION, Arrays.asList(
|
||||
requestEntity.getDebugRequest().getRequestedScriptId(),
|
||||
se.getSimpleMessage(),
|
||||
line == null ? null : Arrays.asList(line.getLineNumber(), line.getEndLineNumber(), line.getStartCol(), line.getEndCol())
|
||||
));
|
||||
@ -395,24 +396,13 @@ public class RequestHandler extends MagicController {
|
||||
* 构建 MagicScriptContext
|
||||
*/
|
||||
private MagicScriptContext createMagicScriptContext(String scriptName, RequestEntity requestEntity) {
|
||||
List<Integer> breakpoints = requestEntity.getRequestedBreakpoints();
|
||||
DebugRequest debugRequest = requestEntity.getDebugRequest();
|
||||
List<Integer> breakpoints = debugRequest.getRequestedBreakpoints();
|
||||
// 构建脚本上下文
|
||||
MagicScriptContext context;
|
||||
// TODO 安全校验
|
||||
if (requestEntity.isRequestedFromDebug() && breakpoints.size() > 0) {
|
||||
MagicScriptDebugContext debugContext = new MagicScriptDebugContext(requestEntity.getRequestedBreakpoints());
|
||||
String scriptId = requestEntity.getRequestedScriptId();
|
||||
String clientId = requestEntity.getRequestedClientId();
|
||||
debugContext.setTimeout(configuration.getDebugTimeout());
|
||||
debugContext.setId(scriptId);
|
||||
debugContext.setCallback(variables -> {
|
||||
List<Map<String, Object>> varList = (List<Map<String, Object>>) variables.get("variables");
|
||||
varList.stream().filter(it -> it.containsKey("value")).forEach(variable -> {
|
||||
variable.put("value", JsonUtils.toJsonStringWithoutLog(variable.get("value")));
|
||||
});
|
||||
WebSocketSessionManager.sendByClientId(clientId, BREAKPOINT, scriptId, variables);
|
||||
});
|
||||
context = debugContext;
|
||||
context = debugRequest.createMagicScriptContext(configuration.getDebugTimeout());
|
||||
} else {
|
||||
context = new MagicScriptContext();
|
||||
}
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
package org.ssssssss.magicapi.model;
|
||||
|
||||
import org.ssssssss.magicapi.config.WebSocketSessionManager;
|
||||
import org.ssssssss.magicapi.utils.JsonUtils;
|
||||
import org.ssssssss.script.MagicScriptDebugContext;
|
||||
import org.ssssssss.script.functions.ObjectConvertExtension;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.ssssssss.magicapi.config.MessageType.BREAKPOINT;
|
||||
import static org.ssssssss.magicapi.model.Constants.*;
|
||||
|
||||
public class DebugRequest {
|
||||
|
||||
private HttpServletRequest request;
|
||||
|
||||
private DebugRequest(HttpServletRequest request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
public static DebugRequest create(HttpServletRequest request){
|
||||
return new DebugRequest(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得断点
|
||||
*/
|
||||
public List<Integer> getRequestedBreakpoints() {
|
||||
String breakpoints = request.getHeader(HEADER_REQUEST_BREAKPOINTS);
|
||||
if (breakpoints != null) {
|
||||
return Arrays.stream(breakpoints.split(","))
|
||||
.map(val -> ObjectConvertExtension.asInt(val, -1))
|
||||
.filter(it -> it > 0)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取测试scriptId
|
||||
*/
|
||||
public String getRequestedScriptId() {
|
||||
return request.getHeader(HEADER_REQUEST_SCRIPT_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取测试clientId
|
||||
*/
|
||||
public String getRequestedClientId() {
|
||||
return request.getHeader(HEADER_REQUEST_CLIENT_ID);
|
||||
}
|
||||
|
||||
public MagicScriptDebugContext createMagicScriptContext(int debugTimeout){
|
||||
MagicScriptDebugContext debugContext = new MagicScriptDebugContext(getRequestedBreakpoints());
|
||||
String scriptId = getRequestedScriptId();
|
||||
String clientId = getRequestedClientId();
|
||||
debugContext.setTimeout(debugTimeout);
|
||||
debugContext.setId(scriptId);
|
||||
debugContext.setCallback(variables -> {
|
||||
List<Map<String, Object>> varList = (List<Map<String, Object>>) variables.get("variables");
|
||||
varList.stream().filter(it -> it.containsKey("value")).forEach(variable -> {
|
||||
variable.put("value", JsonUtils.toJsonStringWithoutLog(variable.get("value")));
|
||||
});
|
||||
WebSocketSessionManager.sendByClientId(clientId, BREAKPOINT, scriptId, variables);
|
||||
});
|
||||
return debugContext;
|
||||
}
|
||||
}
|
||||
@ -55,6 +55,8 @@ public interface JsonCodeConstants {
|
||||
|
||||
JsonCode GROUP_ID_REQUIRED = new JsonCode(0, "请选择分组");
|
||||
|
||||
JsonCode CRON_ID_REQUIRED = new JsonCode(0, "cron表达式不能为空");
|
||||
|
||||
JsonCode NAME_INVALID = new JsonCode(0, "名称不能包含特殊字符,只允许中文、数字、字母以及+_-.()的组合且不能.开头");
|
||||
|
||||
JsonCode DATASOURCE_KEY_INVALID = new JsonCode(0, "数据源Key不能包含特殊字符,只允许中文、数字、字母以及_组合");
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
package org.ssssssss.magicapi.model;
|
||||
|
||||
import org.ssssssss.script.MagicScriptContext;
|
||||
import org.ssssssss.script.functions.ObjectConvertExtension;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.ssssssss.magicapi.model.Constants.*;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 请求信息
|
||||
@ -27,6 +24,7 @@ public class RequestEntity {
|
||||
private Map<String, Object> pathVariables;
|
||||
private MagicScriptContext magicScriptContext;
|
||||
private Object requestBody;
|
||||
private DebugRequest debugRequest;
|
||||
|
||||
private Map<String, Object> headers;
|
||||
|
||||
@ -53,6 +51,7 @@ public class RequestEntity {
|
||||
|
||||
public RequestEntity request(HttpServletRequest request) {
|
||||
this.request = request;
|
||||
this.debugRequest = DebugRequest.create(request);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -75,7 +74,7 @@ public class RequestEntity {
|
||||
}
|
||||
|
||||
public boolean isRequestedFromDebug() {
|
||||
return requestedFromTest && !getRequestedBreakpoints().isEmpty();
|
||||
return requestedFromTest && !this.debugRequest.getRequestedBreakpoints().isEmpty();
|
||||
}
|
||||
|
||||
public Map<String, Object> getParameters() {
|
||||
@ -134,31 +133,7 @@ public class RequestEntity {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取测试scriptId
|
||||
*/
|
||||
public String getRequestedScriptId() {
|
||||
return request.getHeader(HEADER_REQUEST_SCRIPT_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取测试clientId
|
||||
*/
|
||||
public String getRequestedClientId() {
|
||||
return request.getHeader(HEADER_REQUEST_CLIENT_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得断点
|
||||
*/
|
||||
public List<Integer> getRequestedBreakpoints() {
|
||||
String breakpoints = request.getHeader(HEADER_REQUEST_BREAKPOINTS);
|
||||
if (breakpoints != null) {
|
||||
return Arrays.stream(breakpoints.split(","))
|
||||
.map(val -> ObjectConvertExtension.asInt(val, -1))
|
||||
.filter(it -> it > 0)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
public DebugRequest getDebugRequest() {
|
||||
return debugRequest;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
package org.ssssssss.magicapi.model;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class TaskInfo extends PathMagicEntity{
|
||||
|
||||
/**
|
||||
* cron 表达式
|
||||
*/
|
||||
private String cron;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private boolean enabled;
|
||||
|
||||
|
||||
public String getCron() {
|
||||
return cron;
|
||||
}
|
||||
|
||||
public void setCron(String cron) {
|
||||
this.cron = cron;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public TaskInfo copy() {
|
||||
TaskInfo info = new TaskInfo();
|
||||
super.copyTo(info);
|
||||
info.setCron(this.cron);
|
||||
info.setEnabled(this.enabled);
|
||||
return info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MagicEntity simple() {
|
||||
TaskInfo info = new TaskInfo();
|
||||
super.simple(info);
|
||||
return info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
if (!super.equals(o)) return false;
|
||||
TaskInfo taskInfo = (TaskInfo) o;
|
||||
return Objects.equals(id, taskInfo.id) &&
|
||||
Objects.equals(path, taskInfo.path) &&
|
||||
Objects.equals(script, taskInfo.script) &&
|
||||
Objects.equals(name, taskInfo.name) &&
|
||||
Objects.equals(cron, taskInfo.cron) &&
|
||||
Objects.equals(enabled, taskInfo.enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, path, script, name, groupId, cron, enabled);
|
||||
}
|
||||
}
|
||||
@ -68,9 +68,7 @@ public interface MagicResourceStorage<T extends MagicEntity> {
|
||||
return read(resource.read());
|
||||
}
|
||||
|
||||
default String buildMappingKey(T entity) {
|
||||
return null;
|
||||
}
|
||||
String buildMappingKey(T entity);
|
||||
|
||||
default String buildScriptName(T entity) {
|
||||
return null;
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
package org.ssssssss.magicapi.service.impl;
|
||||
|
||||
import org.ssssssss.magicapi.model.ApiInfo;
|
||||
import org.ssssssss.magicapi.model.TaskInfo;
|
||||
|
||||
public class TaskInfoMagicResourceStorage extends AbstractPathMagicResourceStorage<TaskInfo> {
|
||||
|
||||
@Override
|
||||
public String folder() {
|
||||
return "task";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<TaskInfo> magicClass() {
|
||||
return TaskInfo.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(TaskInfo entity) {
|
||||
notBlank(entity.getCron(), CRON_ID_REQUIRED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildMappingKey(TaskInfo info) {
|
||||
return buildMappingKey(info, magicResourceService.getGroupPath(info.getGroupId()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package org.ssssssss.magicapi.service.impl;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.config.CronTask;
|
||||
import org.ssssssss.magicapi.event.FileEvent;
|
||||
import org.ssssssss.magicapi.event.GroupEvent;
|
||||
import org.ssssssss.magicapi.model.TaskInfo;
|
||||
import org.ssssssss.magicapi.provider.MagicResourceStorage;
|
||||
import org.ssssssss.magicapi.script.ScriptManager;
|
||||
import org.ssssssss.magicapi.service.AbstractMagicDynamicRegistry;
|
||||
import org.ssssssss.script.MagicScriptContext;
|
||||
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
public class TaskMagicDynamicRegistry extends AbstractMagicDynamicRegistry<TaskInfo> {
|
||||
|
||||
private final TaskScheduler taskScheduler;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TaskMagicDynamicRegistry.class);
|
||||
|
||||
public TaskMagicDynamicRegistry(MagicResourceStorage<TaskInfo> magicResourceStorage, TaskScheduler taskScheduler) {
|
||||
super(magicResourceStorage);
|
||||
this.taskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
@EventListener(condition = "#event.type == 'task'")
|
||||
public void onFileEvent(FileEvent event) {
|
||||
processEvent(event);
|
||||
}
|
||||
|
||||
@EventListener(condition = "#event.type == 'task'")
|
||||
public void onGroupEvent(GroupEvent event) {
|
||||
processEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean register(MappingNode<TaskInfo> mappingNode) {
|
||||
TaskInfo info = mappingNode.getEntity();
|
||||
CronTask cronTask = new CronTask(() -> {
|
||||
try {
|
||||
ScriptManager.executeScript(info.getScript(), new MagicScriptContext());
|
||||
} catch (Exception e) {
|
||||
logger.error("定时任务执行出错", e);
|
||||
}
|
||||
}, info.getCron());
|
||||
mappingNode.setMappingData(taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger()));
|
||||
if(taskScheduler != null){
|
||||
logger.debug("注册定时任务:[{}, {}, {}]", info.getName(), info.getPath(), info.getCron());
|
||||
} else {
|
||||
logger.debug("注册定时任务失败:[{}, {}, {}], 当前 TaskScheduler 为空", info.getName(), info.getPath(), info.getCron());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unregister(MappingNode<TaskInfo> mappingNode) {
|
||||
TaskInfo info = mappingNode.getEntity();
|
||||
logger.debug("取消注册定时任务:[{}, {}, {}]", info.getName(), info.getPath(), info.getCron());
|
||||
ScheduledFuture<?> scheduledFuture = (ScheduledFuture<?>) mappingNode.getMappingData();
|
||||
if(scheduledFuture != null){
|
||||
try {
|
||||
scheduledFuture.cancel(true);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
||||
import"./app.e9d5d532.js";import"./vue.7304e5c5.js";import"./vendor.5f04ef2d.js";import"./axios.23e7b955.js";const s=function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function n(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerpolicy&&(r.referrerPolicy=e.referrerpolicy),e.crossorigin==="use-credentials"?r.credentials="include":e.crossorigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=n(e);fetch(e.href,r)}};s();
|
||||
import"./app.968aee6d.js";import"./vue.7304e5c5.js";import"./vendor.5f04ef2d.js";import"./axios.23e7b955.js";const s=function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function n(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerpolicy&&(r.referrerPolicy=e.referrerpolicy),e.crossorigin==="use-credentials"?r.credentials="include":e.crossorigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=n(e);fetch(e.href,r)}};s();
|
||||
File diff suppressed because one or more lines are too long
1
magic-editor/src/console/dist/assets/style.a7af0077.css
vendored
Normal file
1
magic-editor/src/console/dist/assets/style.a7af0077.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
magic-editor/src/console/dist/index.html
vendored
6
magic-editor/src/console/dist/index.html
vendored
@ -23,12 +23,12 @@
|
||||
@keyframes stretch {0% {transform: scale(1);}25% {transform: scale(1.2);}50% {transform: scale(1);}100% {transform: scale(1);}}
|
||||
@keyframes blink-loading {0% {opacity: 1;}50% {opacity: 0.5;}100% {opacity: 1;}}
|
||||
</style>
|
||||
<script type="module" crossorigin src="./assets/index.dd6bc269.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index.2cdad179.js"></script>
|
||||
<link rel="modulepreload" href="./assets/vue.7304e5c5.js">
|
||||
<link rel="modulepreload" href="./assets/axios.23e7b955.js">
|
||||
<link rel="modulepreload" href="./assets/vendor.5f04ef2d.js">
|
||||
<link rel="modulepreload" href="./assets/app.e9d5d532.js">
|
||||
<link rel="stylesheet" href="./assets/style.079cdc93.css">
|
||||
<link rel="modulepreload" href="./assets/app.968aee6d.js">
|
||||
<link rel="stylesheet" href="./assets/style.a7af0077.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="magic-loading-wrapper" id="magic-loading-wrapper">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user