Parcourir la source

Merge remote-tracking branch 'origin/dev' into dev

# Conflicts:
#	magic-api/src/main/java/org/ssssssss/magicapi/modules/table/NamedTable.java
wangshuai il y a 3 ans
Parent
commit
41760c2533
26 fichiers modifiés avec 1245 ajouts et 475 suppressions
  1. 3 1
      magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/ClusterConfig.java
  2. 24 3
      magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/MagicAPIAutoConfiguration.java
  3. 4 0
      magic-api/pom.xml
  4. 14 0
      magic-api/src/main/java/org/ssssssss/magicapi/config/Message.java
  5. 20 0
      magic-api/src/main/java/org/ssssssss/magicapi/config/MessageType.java
  6. 99 0
      magic-api/src/main/java/org/ssssssss/magicapi/config/WebSocketSessionManager.java
  7. 57 0
      magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicDebugHandler.java
  8. 105 0
      magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicWebSocketDispatcher.java
  9. 40 15
      magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicWorkbenchController.java
  10. 44 197
      magic-api/src/main/java/org/ssssssss/magicapi/controller/RequestHandler.java
  11. 7 46
      magic-api/src/main/java/org/ssssssss/magicapi/logging/MagicLoggerContext.java
  12. 21 0
      magic-api/src/main/java/org/ssssssss/magicapi/model/Constants.java
  13. 68 0
      magic-api/src/main/java/org/ssssssss/magicapi/model/MagicConsoleSession.java
  14. 45 2
      magic-api/src/main/java/org/ssssssss/magicapi/model/MagicNotify.java
  15. 6 12
      magic-api/src/main/java/org/ssssssss/magicapi/model/RequestEntity.java
  16. 54 36
      magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/DefaultMagicAPIService.java
  17. 0 4
      magic-editor/src/console/src/api/request.js
  18. 125 109
      magic-editor/src/console/src/components/editor/magic-script-editor.vue
  19. 57 45
      magic-editor/src/console/src/components/magic-editor.vue
  20. 6 1
      magic-editor/src/console/src/components/resources/magic-api-list.vue
  21. 6 1
      magic-editor/src/console/src/components/resources/magic-function-list.vue
  22. 1 0
      magic-editor/src/console/src/scripts/contants.js
  23. 2 2
      magic-editor/src/console/src/scripts/parsing/parser.js
  24. 382 0
      magic-editor/src/console/src/scripts/reconnecting-websocket.js
  25. 11 1
      magic-editor/src/console/src/scripts/utils.js
  26. 44 0
      magic-editor/src/console/src/scripts/websocket.js

+ 3 - 1
magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/ClusterConfig.java

@@ -1,5 +1,7 @@
 package org.ssssssss.magicapi.spring.boot.starter;
 
+import java.util.UUID;
+
 /**
  * 集群配置
  * @since 1.2.0
@@ -14,7 +16,7 @@ public class ClusterConfig {
 	/**
 	 * 实例ID,集群环境下,要保证每台机器不同。默认启动后随机生成uuid
 	 */
-	private String instanceId;
+	private String instanceId = UUID.randomUUID().toString();
 
 	/**
 	 * redis 通道

+ 24 - 3
magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/MagicAPIAutoConfiguration.java

@@ -30,6 +30,10 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
 import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistration;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
 import org.ssssssss.magicapi.adapter.ColumnMapperAdapter;
 import org.ssssssss.magicapi.adapter.DialectAdapter;
 import org.ssssssss.magicapi.adapter.Resource;
@@ -73,7 +77,8 @@ import java.util.function.BiFunction;
 @ConditionalOnClass({RequestMappingHandlerMapping.class})
 @EnableConfigurationProperties(MagicAPIProperties.class)
 @Import({MagicRedisAutoConfiguration.class, MagicMongoAutoConfiguration.class, MagicSwaggerConfiguration.class, MagicJsonAutoConfiguration.class, ApplicationUriPrinter.class})
-public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
+@EnableWebSocket
+public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketConfigurer {
 
 	private static final Logger logger = LoggerFactory.getLogger(MagicAPIAutoConfiguration.class);
 
@@ -115,6 +120,8 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 	 */
 	private final ObjectProvider<List<MagicFunction>> magicFunctionsProvider;
 
+	private final ObjectProvider<MagicNotifyService> magicNotifyServiceProvider;
+
 	private final Environment environment;
 
 	private final MagicCorsFilter magicCorsFilter = new MagicCorsFilter();
@@ -140,6 +147,7 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 									 ObjectProvider<List<ColumnMapperProvider>> columnMapperProvidersProvider,
 									 ObjectProvider<List<MagicFunction>> magicFunctionsProvider,
 									 ObjectProvider<RestTemplate> restTemplateProvider,
+									 ObjectProvider<MagicNotifyService> magicNotifyServiceProvider,
 									 ObjectProvider<AuthorizationInterceptor> authorizationInterceptorProvider,
 									 Environment environment,
 									 ApplicationContext applicationContext
@@ -153,6 +161,7 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 		this.columnMapperProvidersProvider = columnMapperProvidersProvider;
 		this.magicFunctionsProvider = magicFunctionsProvider;
 		this.restTemplateProvider = restTemplateProvider;
+		this.magicNotifyServiceProvider = magicNotifyServiceProvider;
 		this.authorizationInterceptorProvider = authorizationInterceptorProvider;
 		this.environment = environment;
 		this.applicationContext = applicationContext;
@@ -347,9 +356,8 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 										   ResultProvider resultProvider,
 										   MagicDynamicDataSource magicDynamicDataSource,
 										   MagicFunctionManager magicFunctionManager,
-										   MagicNotifyService magicNotifyService,
 										   Resource workspace) {
-		return new DefaultMagicAPIService(mappingHandlerMapping, apiServiceProvider, functionServiceProvider, groupServiceProvider, resultProvider, magicDynamicDataSource, magicFunctionManager, magicNotifyService, properties.getClusterConfig().getInstanceId(), workspace, properties.isThrowException());
+		return new DefaultMagicAPIService(mappingHandlerMapping, apiServiceProvider, functionServiceProvider, groupServiceProvider, resultProvider, magicDynamicDataSource, magicFunctionManager, magicNotifyServiceProvider.getObject(), properties.getClusterConfig().getInstanceId(), workspace, properties.isThrowException());
 	}
 
 	private void setupSpringSecurity() {
@@ -576,4 +584,17 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 		return restTemplate;
 	}
 
+	@Override
+	public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
+		String web = properties.getWeb();
+		if (web != null) {
+			MagicWebSocketDispatcher dispatcher = new MagicWebSocketDispatcher(properties.getClusterConfig().getInstanceId(),magicNotifyServiceProvider.getObject(), Arrays.asList(
+					new MagicDebugHandler()
+			));
+			WebSocketHandlerRegistration registration = webSocketHandlerRegistry.addHandler(dispatcher, web + "/console");
+			if (properties.isSupportCrossDomain()) {
+				registration.setAllowedOrigins("*");
+			}
+		}
+	}
 }

+ 4 - 0
magic-api/pom.xml

@@ -22,6 +22,10 @@
             <artifactId>spring-boot-starter</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-redis</artifactId>

+ 14 - 0
magic-api/src/main/java/org/ssssssss/magicapi/config/Message.java

@@ -0,0 +1,14 @@
+package org.ssssssss.magicapi.config;
+
+import java.lang.annotation.*;
+
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Message {
+
+	/**
+	 * 消息类型
+	 */
+	MessageType value();
+}

+ 20 - 0
magic-api/src/main/java/org/ssssssss/magicapi/config/MessageType.java

@@ -0,0 +1,20 @@
+package org.ssssssss.magicapi.config;
+
+/**
+ * 消息类型
+ * */
+public enum MessageType {
+	/* S -> C message */
+	/* 日志消息 */
+	LOG,
+	/* 进入断点 */
+	BREAKPOINT,
+
+	/* C -> S message */
+	/* 设置断点 */
+	SET_BREAKPOINT,
+	/* 恢复断点 */
+	RESUME_BREAKPOINT,
+	/* 设置 Session ID */
+	SET_SESSION_ID,
+}

+ 99 - 0
magic-api/src/main/java/org/ssssssss/magicapi/config/WebSocketSessionManager.java

@@ -0,0 +1,99 @@
+package org.ssssssss.magicapi.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.socket.TextMessage;
+import org.ssssssss.magicapi.model.Constants;
+import org.ssssssss.magicapi.model.MagicConsoleSession;
+import org.ssssssss.magicapi.model.MagicNotify;
+import org.ssssssss.magicapi.provider.MagicNotifyService;
+import org.ssssssss.magicapi.utils.JsonUtils;
+import org.ssssssss.script.MagicScriptDebugContext;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class WebSocketSessionManager {
+
+	private static final Logger logger = LoggerFactory.getLogger(WebSocketSessionManager.class);
+
+	private static final Map<String, MagicConsoleSession> SESSION = new ConcurrentHashMap<>();
+
+	private static MagicNotifyService magicNotifyService;
+
+	private static String instanceId;
+
+	public static void add(MagicConsoleSession session) {
+		SESSION.put(session.getId(), session);
+	}
+
+	public static void remove(MagicConsoleSession session) {
+		if(session.getId() != null){
+			remove(session.getId());
+		}
+	}
+
+	public static void remove(String sessionId) {
+		SESSION.remove(sessionId);
+	}
+
+	public static void sendBySessionId(String sessionId, MessageType messageType, Object... values) {
+		MagicConsoleSession session = findSession(sessionId);
+		StringBuilder builder = new StringBuilder(messageType.name().toLowerCase());
+		if (values != null) {
+			for (int i = 0, len = values.length; i < len; i++) {
+				builder.append(",");
+				Object value = values[i];
+				if (i + 1 < len || value instanceof CharSequence || value instanceof Number) {
+					builder.append(value);
+				} else {
+					builder.append(JsonUtils.toJsonString(value));
+				}
+			}
+		}
+		if (session != null && session.writeable()) {
+			sendBySession(session, builder.toString());
+		} else if(magicNotifyService != null){
+			// 通知其他机器去发送消息
+			magicNotifyService.sendNotify(new MagicNotify(instanceId, Constants.NOTIFY_WS_S_C, sessionId, builder.toString()));
+		}
+	}
+
+	public static void sendBySessionId(String sessionId, String content){
+		MagicConsoleSession session = findSession(sessionId);
+		if (session != null) {
+			sendBySession(session, content);
+		}
+	}
+
+	public static void sendBySession(MagicConsoleSession session, String content){
+		try {
+			session.getWebSocketSession().sendMessage(new TextMessage(content));
+		} catch (IOException e) {
+			logger.error("发送WebSocket消息失败", e);
+		}
+	}
+
+	public static MagicConsoleSession findSession(String sessionId) {
+		return SESSION.get(sessionId);
+	}
+
+	public static void setMagicNotifyService(MagicNotifyService magicNotifyService) {
+		WebSocketSessionManager.magicNotifyService = magicNotifyService;
+	}
+
+	public static void setInstanceId(String instanceId) {
+		WebSocketSessionManager.instanceId = instanceId;
+	}
+
+	public static void createSession(String sessionId, MagicScriptDebugContext debugContext){
+		MagicConsoleSession consoleSession = SESSION.get(sessionId);
+		if(consoleSession == null){
+			consoleSession = new MagicConsoleSession(sessionId, debugContext);
+			SESSION.put(sessionId, consoleSession);
+		}else{
+			consoleSession.setMagicScriptDebugContext(debugContext);
+		}
+	}
+}

+ 57 - 0
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicDebugHandler.java

@@ -0,0 +1,57 @@
+package org.ssssssss.magicapi.controller;
+
+import org.ssssssss.magicapi.config.Message;
+import org.ssssssss.magicapi.config.MessageType;
+import org.ssssssss.magicapi.config.WebSocketSessionManager;
+import org.ssssssss.magicapi.model.MagicConsoleSession;
+import org.ssssssss.script.MagicScriptDebugContext;
+
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+
+public class MagicDebugHandler {
+
+	/**
+	 * 设置会话ID
+	 * 只在本机处理。
+	 */
+	@Message(MessageType.SET_SESSION_ID)
+	public void setSessionId(MagicConsoleSession session, String sessionId) {
+		WebSocketSessionManager.remove(session);
+		session.setId(sessionId);
+		WebSocketSessionManager.add(session);
+	}
+
+	/**
+	 * 设置断点
+	 * 当本机没有该Session时,通知其他机器处理
+	 */
+	@Message(MessageType.SET_BREAKPOINT)
+	public boolean setBreakPoint(MagicConsoleSession session, String breakpoints) {
+		MagicScriptDebugContext context = session.getMagicScriptDebugContext();
+		if (context != null) {
+			context.setBreakpoints(Stream.of(breakpoints.split(",")).map(Integer::valueOf).collect(Collectors.toList()));
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * 恢复断点
+	 * 当本机没有该Session时,通知其他机器处理
+	 */
+	@Message(MessageType.RESUME_BREAKPOINT)
+	public boolean resumeBreakpoint(MagicConsoleSession session, String stepInto) {
+		MagicScriptDebugContext context = session.getMagicScriptDebugContext();
+		if (context != null) {
+			context.setStepInto("1".equals(stepInto));
+			try {
+				context.singal();
+			} catch (InterruptedException ignored) {
+			}
+			return true;
+		}
+		return false;
+	}
+}

+ 105 - 0
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicWebSocketDispatcher.java

@@ -0,0 +1,105 @@
+package org.ssssssss.magicapi.controller;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+import org.ssssssss.magicapi.config.Message;
+import org.ssssssss.magicapi.config.WebSocketSessionManager;
+import org.ssssssss.magicapi.model.Constants;
+import org.ssssssss.magicapi.model.MagicConsoleSession;
+import org.ssssssss.magicapi.model.MagicNotify;
+import org.ssssssss.magicapi.provider.MagicNotifyService;
+import org.ssssssss.magicapi.utils.JsonUtils;
+import org.ssssssss.script.reflection.MethodInvoker;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public class MagicWebSocketDispatcher extends TextWebSocketHandler {
+
+	private static final Logger logger = LoggerFactory.getLogger(MagicWebSocketDispatcher.class);
+
+	private static final Map<String, MethodInvoker> handlers = new HashMap<>();
+
+	private final String instanceId;
+
+	private final MagicNotifyService magicNotifyService;
+
+	public MagicWebSocketDispatcher(String instanceId, MagicNotifyService magicNotifyService, List<Object> websocketMessageHandlers) {
+		this.instanceId = instanceId;
+		this.magicNotifyService = magicNotifyService;
+		WebSocketSessionManager.setMagicNotifyService(magicNotifyService);
+		WebSocketSessionManager.setInstanceId(instanceId);
+		websocketMessageHandlers.forEach(websocketMessageHandler ->
+				Stream.of(websocketMessageHandler.getClass().getDeclaredMethods())
+						.forEach(method -> handlers.put(method.getAnnotation(Message.class).value().name().toLowerCase(), new MethodInvoker(method, websocketMessageHandler)))
+		);
+	}
+
+	@Override
+	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
+		MagicConsoleSession.remove(session);
+	}
+
+	@Override
+	protected void handleTextMessage(WebSocketSession session, TextMessage message) {
+		MagicConsoleSession consoleSession = MagicConsoleSession.from(session);
+		Object returnValue = findHandleAndInvoke(consoleSession, message.getPayload());
+		// 如果未成功处理消息,则通知其他机器去处理消息
+		if (Boolean.FALSE.equals(returnValue)) {
+			magicNotifyService.sendNotify(new MagicNotify(instanceId, Constants.NOTIFY_WS_C_S, consoleSession.getId(), message.getPayload()));
+		}
+	}
+
+
+	private static Object findHandleAndInvoke(MagicConsoleSession session, String payload) {
+		// messageType[, data][,data]
+		int index = payload.indexOf(",");
+		String msgType = index == -1 ? payload : payload.substring(0, index);
+		MethodInvoker invoker = handlers.get(msgType);
+		if (invoker != null) {
+			Object returnValue;
+			try {
+				Class<?>[] pTypes = invoker.getParameterTypes();
+				int pCount = pTypes.length;
+				if (pCount == 0) {
+					returnValue = invoker.invoke0(null, null);
+				} else {
+					Object[] pValues = new Object[pCount];
+					for (int i = 0; i < pCount; i++) {
+						Class<?> pType = pTypes[i];
+						if (pType == MagicConsoleSession.class) {
+							pValues[i] = session;
+						} else if (pType == String.class) {
+							int subIndex = payload.indexOf(",", index + 1);
+							if (subIndex > -1) {
+								pValues[i] = payload.substring(index + 1, index = subIndex);
+							} else if (index > -1) {
+								pValues[i] = payload.substring(index + 1);
+							}
+						} else {
+							pValues[i] = JsonUtils.readValue(payload, pType);
+						}
+					}
+					returnValue =  invoker.invoke0(null, null, pValues);
+				}
+				return returnValue;
+			} catch (Throwable e) {
+				logger.error("WebSocket消息处理出错", e);
+			}
+		}
+		return null;
+	}
+
+	public static void processMessageReceived(String sessionId, String payload) {
+		MagicConsoleSession session = WebSocketSessionManager.findSession(sessionId);
+		if (session != null) {
+			findHandleAndInvoke(session, payload);
+		}
+	}
+}

+ 40 - 15
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicWorkbenchController.java

@@ -14,14 +14,11 @@ import org.springframework.web.bind.annotation.RequestHeader;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.multipart.MultipartFile;
-import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
-import org.ssssssss.magicapi.adapter.Resource;
 import org.ssssssss.magicapi.config.MagicConfiguration;
 import org.ssssssss.magicapi.config.Valid;
 import org.ssssssss.magicapi.exception.MagicLoginException;
 import org.ssssssss.magicapi.interceptor.Authorization;
 import org.ssssssss.magicapi.interceptor.MagicUser;
-import org.ssssssss.magicapi.logging.MagicLoggerContext;
 import org.ssssssss.magicapi.model.*;
 import org.ssssssss.magicapi.modules.ResponseModule;
 import org.ssssssss.magicapi.modules.SQLModule;
@@ -32,6 +29,7 @@ import org.ssssssss.script.MagicResourceLoader;
 import org.ssssssss.script.MagicScriptEngine;
 import org.ssssssss.script.ScriptClass;
 import org.ssssssss.script.parsing.Span;
+import org.ssssssss.script.parsing.Tokenizer;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -42,6 +40,8 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -53,6 +53,10 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 
 	private final String secretKey;
 
+	private static final Pattern SINGLE_LINE_COMMENT_TODO = Pattern.compile("((TODO)|(todo)|(fixme)|(FIXME))[ \t]+[^\n]+");
+
+	private static final Pattern MULTI_LINE_COMMENT_TODO = Pattern.compile("((TODO)|(todo)|(fixme)|(FIXME))[ \t]+[^\n(?!*/)]+");
+
 	public MagicWorkbenchController(MagicConfiguration configuration, String secretKey) {
 		super(configuration);
 		magicApiService = configuration.getMagicAPIService();
@@ -148,18 +152,6 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 	}
 
 
-	/**
-	 * 创建控制台输出
-	 */
-	@RequestMapping("/console")
-	@Valid(requireLogin = false)
-	public SseEmitter console() throws IOException {
-		String sessionId = UUID.randomUUID().toString().replace("-", "");
-		SseEmitter emitter = MagicLoggerContext.createEmitter(sessionId);
-		emitter.send(SseEmitter.event().data(sessionId).name("create"));
-		return emitter;
-	}
-
 	@RequestMapping("/options")
 	@ResponseBody
 	@Valid(requireLogin = false)
@@ -198,6 +190,39 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 		}).collect(Collectors.toList()));
 	}
 
+	@RequestMapping("/todo")
+	@ResponseBody
+	@Valid
+	public JsonBean<List<Map<String, Object>>> todo() {
+		List<MagicEntity> entities = new ArrayList<>(configuration.getMappingHandlerMapping().getApiInfos());
+		entities.addAll(configuration.getMagicFunctionManager().getFunctionInfos());
+		List<Map<String, Object>> result = new ArrayList<>();
+		for (MagicEntity entity : entities) {
+			try {
+				List<Span> comments = Tokenizer.tokenize(entity.getScript(), true).comments();
+				for (Span comment : comments) {
+					String text = comment.getText();
+					Pattern pattern = text.startsWith("//") ? SINGLE_LINE_COMMENT_TODO : MULTI_LINE_COMMENT_TODO;
+					Matcher matcher = pattern.matcher(text);
+					while(matcher.find()){
+						result.add(new HashMap<String, Object>(){
+							{
+								put("id", entity.getId());
+								put("text", matcher.group(0).trim());
+								put("line", comment.getLine().getLineNumber());
+								put("type", entity instanceof ApiInfo ? 1 : 2);
+							}
+						});
+					}
+				}
+			} catch (Exception ignored) {
+				ignored.printStackTrace();
+			}
+
+		}
+		return new JsonBean<>(result);
+	}
+
 	@RequestMapping(value = "/config-js")
 	@ResponseBody
 	@Valid(requireLogin = false)

+ 44 - 197
magic-api/src/main/java/org/ssssssss/magicapi/controller/RequestHandler.java

@@ -1,13 +1,8 @@
 package org.ssssssss.magicapi.controller;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.slf4j.event.Level;
-import org.springframework.core.io.InputStreamSource;
-import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.converter.HttpMessageConverter;
@@ -16,16 +11,15 @@ import org.springframework.http.server.ServletServerHttpRequest;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseBody;
-import org.springframework.web.context.request.RequestContextHolder;
 import org.ssssssss.magicapi.config.MagicConfiguration;
 import org.ssssssss.magicapi.config.MappingHandlerMapping;
 import org.ssssssss.magicapi.config.Valid;
+import org.ssssssss.magicapi.config.WebSocketSessionManager;
 import org.ssssssss.magicapi.context.CookieContext;
 import org.ssssssss.magicapi.context.RequestContext;
 import org.ssssssss.magicapi.context.SessionContext;
 import org.ssssssss.magicapi.exception.ValidateException;
 import org.ssssssss.magicapi.interceptor.RequestInterceptor;
-import org.ssssssss.magicapi.logging.LogInfo;
 import org.ssssssss.magicapi.logging.MagicLoggerContext;
 import org.ssssssss.magicapi.model.*;
 import org.ssssssss.magicapi.modules.ResponseModule;
@@ -44,13 +38,13 @@ import org.ssssssss.script.reflection.JavaInvoker;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
-import java.io.InputStream;
 import java.lang.reflect.Method;
 import java.math.BigDecimal;
 import java.util.*;
 import java.util.stream.Collectors;
 
 import static org.springframework.http.HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS;
+import static org.ssssssss.magicapi.config.MessageType.BREAKPOINT;
 import static org.ssssssss.magicapi.model.Constants.*;
 
 public class RequestHandler extends MagicController {
@@ -74,14 +68,9 @@ public class RequestHandler extends MagicController {
 	public Object invoke(HttpServletRequest request, HttpServletResponse response,
 						 @PathVariable(required = false) Map<String, Object> pathVariables,
 						 @RequestParam(required = false) Map<String, Object> parameters) throws Throwable {
-		RequestEntity requestEntity = new RequestEntity(request, response, isRequestedFromTest(request), parameters, pathVariables);
-		if (requestEntity.isRequestedFromTest()) {
-			response.setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_TRUE);
-			response.setHeader(ACCESS_CONTROL_EXPOSE_HEADERS, HEADER_RESPONSE_WITH_MAGIC_API);
-			if(requestEntity.isRequestedFromContinue()){
-				return invokeContinueRequest(requestEntity);
-			}
-		}
+		String sessionId = null;
+		boolean requestedFromTest = configuration.isEnableWeb() && (sessionId = request.getHeader(HEADER_REQUEST_SESSION)) != null;
+		RequestEntity requestEntity = new RequestEntity(request, response, requestedFromTest, parameters, pathVariables);
 		if (requestEntity.getApiInfo() == null) {
 			logger.error("{}找不到对应接口", request.getRequestURI());
 			return buildResult(requestEntity, API_NOT_FOUND, "接口不存在");
@@ -115,8 +104,7 @@ public class RequestHandler extends MagicController {
 				}}, BODY_INVALID);
 			}
 		} catch (ValidateException e) {
-			Object value = resultProvider.buildResult(requestEntity, RESPONSE_CODE_INVALID, e.getMessage());
-			return requestEntity.isRequestedFromTest() ? new JsonBean<>(e.getJsonCode(), value) : value;
+			return resultProvider.buildResult(requestEntity, RESPONSE_CODE_INVALID, e.getMessage());
 		} catch (Throwable root) {
 			return processException(requestEntity, root);
 		}
@@ -126,13 +114,19 @@ public class RequestHandler extends MagicController {
 		Object value;
 		// 执行前置拦截器
 		if ((value = doPreHandle(requestEntity)) != null) {
-			if (requestEntity.isRequestedFromTest()) {
-				// 修正前端显示,当拦截器返回时,原样输出显示
-				response.setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_FALSE);
-			}
 			return value;
 		}
-		return requestEntity.isRequestedFromTest() ?  invokeTestRequest(requestEntity) : invokeRequest(requestEntity);
+		if (requestedFromTest) {
+			try {
+				MagicLoggerContext.SESSION.set(sessionId);
+				return invokeRequest(requestEntity);
+			} finally {
+				MagicLoggerContext.SESSION.remove();
+				WebSocketSessionManager.remove(sessionId);
+			}
+		} else {
+			return invokeRequest(requestEntity);
+		}
 	}
 
 	private Object buildResult(RequestEntity requestEntity, JsonCode code, Object data) {
@@ -249,61 +243,6 @@ public class RequestHandler extends MagicController {
 		}
 	}
 
-	private Object invokeContinueRequest(RequestEntity requestEntity) throws Exception {
-		HttpServletRequest request = requestEntity.getRequest();
-		MagicScriptDebugContext context = MagicScriptDebugContext.getDebugContext(requestEntity.getRequestedSessionId());
-		if (context == null) {
-			return new JsonBean<>(DEBUG_SESSION_NOT_FOUND, buildResult(requestEntity, DEBUG_SESSION_NOT_FOUND, null));
-		}
-		// 重置断点
-		context.setBreakpoints(requestEntity.getRequestedBreakpoints());
-		// 步进
-		context.setStepInto(CONST_STRING_TRUE.equalsIgnoreCase(request.getHeader(HEADER_REQUEST_STEP_INTO)));
-		try {
-			context.singal();    //等待语句执行到断点或执行完毕
-		} catch (InterruptedException e) {
-			e.printStackTrace();
-		}
-		if (context.isRunning()) {    //判断是否执行完毕
-			return new JsonBodyBean<>(1000, context.getId(), resultProvider.buildResult(requestEntity, 1000, context.getId()), context.getDebugInfo());
-		} else if (context.isException()) {
-			return resolveThrowableForTest(requestEntity, (Throwable) context.getReturnValue());
-		}
-		Object value = context.getReturnValue();
-		// 执行后置拦截器
-		if ((value = doPostHandle(requestEntity, value)) != null) {
-			// 修正前端显示,当拦截器返回时,原样输出显示
-			requestEntity.getResponse().setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_FALSE);
-			// 后置拦截器不包裹
-			return value;
-		}
-		return convertResult(requestEntity, context.getReturnValue());
-	}
-
-	private Object invokeTestRequest(RequestEntity requestEntity) {
-		try {
-			// 初始化debug操作
-			MagicScriptDebugContext context = initializeDebug(requestEntity);
-			Object result = ScriptManager.executeScript(requestEntity.getApiInfo().getScript(), requestEntity.getMagicScriptContext());
-			if (context.isRunning()) {
-				return new JsonBodyBean<>(1000, context.getId(), resultProvider.buildResult(requestEntity, 1000, context.getId(), result), result);
-			} else if (context.isException()) {    //判断是否出现异常
-				return resolveThrowableForTest(requestEntity, (Throwable) context.getReturnValue());
-			}
-			Object value = result;
-			// 执行后置拦截器
-			if ((value = doPostHandle(requestEntity, value)) != null) {
-				// 修正前端显示,当拦截器返回时,原样输出显示
-				requestEntity.getResponse().setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_FALSE);
-				// 后置拦截器不包裹
-				return value;
-			}
-			return convertResult(requestEntity, result);
-		} catch (Exception e) {
-			return resolveThrowableForTest(requestEntity, e);
-		}
-	}
-
 	private Object invokeRequest(RequestEntity requestEntity) throws Throwable {
 		try {
 			Object result = ScriptManager.executeScript(requestEntity.getApiInfo().getScript(), requestEntity.getMagicScriptContext());
@@ -322,125 +261,28 @@ public class RequestHandler extends MagicController {
 	}
 
 	private Object processException(RequestEntity requestEntity, Throwable root) throws Throwable {
+		MagicScriptException se = null;
 		Throwable parent = root;
 		do {
 			if (parent instanceof MagicScriptAssertException) {
 				MagicScriptAssertException sae = (MagicScriptAssertException) parent;
 				return resultProvider.buildResult(requestEntity, sae.getCode(), sae.getMessage());
 			}
+			if (parent instanceof MagicScriptException) {
+				se = (MagicScriptException) parent;
+			}
 		} while ((parent = parent.getCause()) != null);
 		if (configuration.isThrowException()) {
 			throw root;
 		}
 		logger.error("接口{}请求出错", requestEntity.getRequest().getRequestURI(), root);
-		return resultProvider.buildException(requestEntity, root);
-	}
-
-	/**
-	 * 转换请求结果
-	 */
-	private Object convertResult(RequestEntity requestEntity, Object result) throws IOException {
-		if (result instanceof ResponseEntity) {
-			ResponseEntity<?> entity = (ResponseEntity<?>) result;
-			List<String> headers = new ArrayList<>();
-			for (Map.Entry<String, List<String>> entry : entity.getHeaders().entrySet()) {
-				String key = entry.getKey();
-				for (String value : entry.getValue()) {
-					headers.add(HEADER_PREFIX_FOR_TEST + key);
-					requestEntity.getResponse().addHeader(HEADER_PREFIX_FOR_TEST + key, value);
-				}
-			}
-			headers.add(HEADER_RESPONSE_WITH_MAGIC_API);
-			// 允许前端读取自定义的header(跨域情况)。
-			requestEntity.getResponse().setHeader(ACCESS_CONTROL_EXPOSE_HEADERS, String.join(",", headers));
-			if (entity.getHeaders().isEmpty()) {
-				return ResponseEntity.ok(new JsonBean<>(entity.getBody()));
-			}
-			return ResponseEntity.ok(new JsonBean<>(convertToBase64(entity.getBody())));
-		} else if (result instanceof ResponseModule.NullValue) {
-			// 对于return response.end() 的特殊处理
-			return new JsonBean<>(RESPONSE_CODE_SUCCESS, "empty.");
-		}
-		return new JsonBean<>(resultProvider.buildResult(requestEntity, result));
-	}
-
-	/**
-	 * 将结果转为base64
-	 */
-	private String convertToBase64(Object value) throws IOException {
-		if (value instanceof String || value instanceof Number) {
-			return convertToBase64(value.toString().getBytes());
-		} else if (value instanceof byte[]) {
-			return Base64.getEncoder().encodeToString((byte[]) value);
-		} else if (value instanceof InputStream) {
-			return convertToBase64(IOUtils.toByteArray((InputStream) value));
-		} else if (value instanceof InputStreamSource) {
-			InputStreamSource iss = (InputStreamSource) value;
-			return convertToBase64(iss.getInputStream());
-		} else {
-			return convertToBase64(new ObjectMapper().writeValueAsString(value));
-		}
-	}
-
-	/**
-	 * 解决异常
-	 */
-	private JsonBean<Object> resolveThrowableForTest(RequestEntity requestEntity, Throwable root) {
-		MagicScriptException se = null;
-		Throwable parent = root;
-		do {
-			if (parent instanceof MagicScriptAssertException) {
-				MagicScriptAssertException sae = (MagicScriptAssertException) parent;
-				return new JsonBean<>(resultProvider.buildResult(requestEntity, sae.getCode(), sae.getMessage()));
-			}
-			if (parent instanceof MagicScriptException) {
-				se = (MagicScriptException) parent;
-			}
-		} while ((parent = parent.getCause()) != null);
-		logger.error("测试脚本出错", root);
-		if (se != null) {
+		if (se != null && requestEntity.isRequestedFromTest()) {
 			Span.Line line = se.getLine();
+			requestEntity.getResponse().setHeader(ACCESS_CONTROL_EXPOSE_HEADERS, HEADER_RESPONSE_WITH_MAGIC_API);
+			requestEntity.getResponse().setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_TRUE);
 			return new JsonBodyBean<>(-1000, se.getSimpleMessage(), resultProvider.buildException(requestEntity, se), line == null ? null : Arrays.asList(line.getLineNumber(), line.getEndLineNumber(), line.getStartCol(), line.getEndCol()));
 		}
-		return new JsonBean<>(-1, root.getMessage(), resultProvider.buildException(requestEntity, root));
-	}
-
-	/**
-	 * 初始化DEBUG
-	 */
-	private MagicScriptDebugContext initializeDebug(RequestEntity requestEntity) {
-		MagicScriptDebugContext context = (MagicScriptDebugContext) requestEntity.getMagicScriptContext();
-		HttpServletRequest request = requestEntity.getRequest();
-		// 由于debug是开启一个新线程,为了防止在子线程中无法获取request对象,所以将request放在InheritableThreadLocal中。
-		RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
-
-		String sessionId = requestEntity.getRequestedSessionId();
-		// 设置断点
-		context.setBreakpoints(requestEntity.getRequestedBreakpoints());
-		context.setTimeout(configuration.getDebugTimeout());
-		context.setId(sessionId);
-		// 设置相关回调,打印日志,回收资源
-		context.onComplete(() -> {
-			if (context.isException()) {
-				MagicLoggerContext.println(new LogInfo(Level.ERROR.name().toLowerCase(), "执行脚本出错", (Throwable) context.getReturnValue()));
-			}
-			logger.info("Close Console Session : {}", sessionId);
-			RequestContext.remove();
-			MagicLoggerContext.remove(sessionId);
-		});
-		context.onStart(() -> {
-			RequestContext.setRequestEntity(requestEntity);
-			MagicLoggerContext.SESSION.set(sessionId);
-			logger.info("Create Console Session : {}", sessionId);
-		});
-		return context;
-	}
-
-	/**
-	 * 判断是否是测试请求
-	 */
-	private boolean isRequestedFromTest(HttpServletRequest request) {
-		return configuration.isEnableWeb() && request.getHeader(HEADER_REQUEST_SESSION) != null;
+		return resultProvider.buildException(requestEntity, root);
 	}
 
 	/**
@@ -466,9 +308,22 @@ public class RequestHandler extends MagicController {
 	/**
 	 * 构建 MagicScriptContext
 	 */
-	private MagicScriptContext createMagicScriptContext(RequestEntity requestEntity, Object requestBody) throws IOException {
+	private MagicScriptContext createMagicScriptContext(RequestEntity requestEntity, Object requestBody) {
+		List<Integer> breakpoints = requestEntity.getRequestedBreakpoints();
 		// 构建脚本上下文
-		MagicScriptContext context = requestEntity.isRequestedFromTest() ? new MagicScriptDebugContext() : new MagicScriptContext();
+		MagicScriptContext context;
+		// TODO 安全校验
+		if (requestEntity.isRequestedFromDebug()) {
+			MagicScriptDebugContext debugContext = new MagicScriptDebugContext(breakpoints);
+			String sessionId = requestEntity.getRequestedSessionId();
+			debugContext.setTimeout(configuration.getDebugTimeout());
+			debugContext.setId(sessionId);
+			debugContext.setCallback(variables -> WebSocketSessionManager.sendBySessionId(sessionId, BREAKPOINT, variables));
+			WebSocketSessionManager.createSession(sessionId, debugContext);
+			context = debugContext;
+		} else {
+			context = new MagicScriptContext();
+		}
 		Object wrap = requestEntity.getApiInfo().getOptionValue(Options.WRAP_REQUEST_PARAMETERS.getValue());
 		if (wrap != null && StringUtils.isNotBlank(wrap.toString())) {
 			context.set(wrap.toString(), requestEntity.getParameters());
@@ -514,19 +369,11 @@ public class RequestHandler extends MagicController {
 	 * 执行前置拦截器
 	 */
 	private Object doPreHandle(RequestEntity requestEntity) throws Exception {
-		try {
-			for (RequestInterceptor requestInterceptor : configuration.getRequestInterceptors()) {
-				Object value = requestInterceptor.preHandle(requestEntity);
-				if (value != null) {
-					return value;
-				}
-			}
-		} catch (Exception e) {
-			if (requestEntity.isRequestedFromTest()) {
-				// 修正前端显示,原样输出显示
-				requestEntity.getResponse().setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_FALSE);
+		for (RequestInterceptor requestInterceptor : configuration.getRequestInterceptors()) {
+			Object value = requestInterceptor.preHandle(requestEntity);
+			if (value != null) {
+				return value;
 			}
-			throw e;
 		}
 		return null;
 	}

+ 7 - 46
magic-api/src/main/java/org/ssssssss/magicapi/logging/MagicLoggerContext.java

@@ -1,49 +1,23 @@
 package org.ssssssss.magicapi.logging;
 
+import org.slf4j.MDC;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+import org.ssssssss.magicapi.config.MessageType;
+import org.ssssssss.magicapi.config.WebSocketSessionManager;
 import org.ssssssss.script.MagicScriptContext;
 import org.ssssssss.script.MagicScriptDebugContext;
 
 import java.io.IOException;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 
 public interface MagicLoggerContext {
 
-	String LOGGER_NAME = "MagicAPI";
-
-	Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();
+	String LOGGER_NAME = "magic";
 
 	ThreadLocal<String> SESSION = new InheritableThreadLocal<>();
 
-	/**
-	 * 创建sseEmitter推送
-	 *
-	 * @param sessionId 会话id
-	 */
-	static SseEmitter createEmitter(String sessionId) {
-		SseEmitter sseEmitter = new SseEmitter(0L);
-		emitterMap.put(sessionId, sseEmitter);
-		return sseEmitter;
-	}
-
-	/**
-	 * 删除会话
-	 *
-	 * @param sessionId 会话id
-	 */
-	static void remove(String sessionId) {
-		SseEmitter sseEmitter = emitterMap.remove(sessionId);
-		SESSION.remove();
-		if (sseEmitter != null) {
-			try {
-				sseEmitter.send(SseEmitter.event().data(sessionId).name("close"));
-			} catch (IOException ignored) {
-			}
-		}
-	}
-
-
 	/**
 	 * 打印日志
 	 *
@@ -51,22 +25,9 @@ public interface MagicLoggerContext {
 	 */
 	static void println(LogInfo logInfo) {
 		// 获取SessionId
-		MagicScriptContext context = MagicScriptContext.get();
-		String sessionId;
-		if (context instanceof MagicScriptDebugContext) {
-			sessionId = ((MagicScriptDebugContext) context).getId();
-		} else {
-			sessionId = SESSION.get();
-		}
+		String sessionId = SESSION.get();
 		if (sessionId != null) {
-			SseEmitter sseEmitter = emitterMap.get(sessionId);
-			if (sseEmitter != null) {
-				try {
-					// 推送日志事件
-					sseEmitter.send(SseEmitter.event().data(logInfo).name("log"));
-				} catch (IOException ignored) {
-				}
-			}
+			WebSocketSessionManager.sendBySessionId(sessionId, MessageType.LOG, logInfo);
 		}
 	}
 

+ 21 - 0
magic-api/src/main/java/org/ssssssss/magicapi/model/Constants.java

@@ -78,6 +78,16 @@ public class Constants {
 	 */
 	public static final String VAR_NAME_PATH_VARIABLE = "path";
 
+	/**
+	 * WebSocket存储的sessionId
+	 */
+	public static final String WS_DEBUG_SESSION_KEY = "sessionId";
+
+	/**
+	 * WebSocket存储的MagicScriptDebugContext
+	 */
+	public static final String WS_DEBUG_MAGIC_SCRIPT_CONTEXT = "magicScriptContext";
+
 	/**
 	 * 脚本中header的变量名
 	 */
@@ -184,4 +194,15 @@ public class Constants {
 	 */
 	public static final int NOTIFY_ACTION_DATASOURCE = 4;
 
+
+	/**
+	 * 通知 C -> S 的WebSocket消息
+	 */
+	public static final int NOTIFY_WS_C_S = 100;
+
+	/**
+	 * 通知 S -> C 的WebSocket消息
+	 */
+	public static final int NOTIFY_WS_S_C = 200;
+
 }

+ 68 - 0
magic-api/src/main/java/org/ssssssss/magicapi/model/MagicConsoleSession.java

@@ -0,0 +1,68 @@
+package org.ssssssss.magicapi.model;
+
+import org.springframework.web.socket.WebSocketSession;
+import org.ssssssss.script.MagicScriptDebugContext;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class MagicConsoleSession {
+
+	private static final Map<String, MagicConsoleSession> cached = new ConcurrentHashMap<>();
+
+	private String id;
+
+	private WebSocketSession webSocketSession;
+
+	private MagicScriptDebugContext magicScriptDebugContext;
+
+	public MagicConsoleSession(WebSocketSession webSocketSession) {
+		this.webSocketSession = webSocketSession;
+	}
+
+	public MagicConsoleSession(String id, MagicScriptDebugContext magicScriptDebugContext) {
+		this.id = id;
+		this.magicScriptDebugContext = magicScriptDebugContext;
+	}
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public WebSocketSession getWebSocketSession() {
+		return webSocketSession;
+	}
+
+	public void setWebSocketSession(WebSocketSession webSocketSession) {
+		this.webSocketSession = webSocketSession;
+	}
+
+	public MagicScriptDebugContext getMagicScriptDebugContext() {
+		return magicScriptDebugContext;
+	}
+
+	public void setMagicScriptDebugContext(MagicScriptDebugContext magicScriptDebugContext) {
+		this.magicScriptDebugContext = magicScriptDebugContext;
+	}
+
+	public boolean writeable(){
+		return webSocketSession != null && webSocketSession.isOpen();
+	}
+
+	public static MagicConsoleSession from(WebSocketSession session){
+		MagicConsoleSession magicConsoleSession = cached.get(session.getId());
+		if(magicConsoleSession == null){
+			magicConsoleSession = new MagicConsoleSession(session);
+			cached.put(session.getId(), magicConsoleSession);
+		}
+		return magicConsoleSession;
+	}
+
+	public static void remove(WebSocketSession session){
+		cached.remove(session.getId());
+	}
+}

+ 45 - 2
magic-api/src/main/java/org/ssssssss/magicapi/model/MagicNotify.java

@@ -3,7 +3,7 @@ package org.ssssssss.magicapi.model;
 public class MagicNotify {
 
 	/**
-	 * 消息来源
+	 * 消息来源(instanceId)
 	 */
 	private String from;
 
@@ -22,6 +22,16 @@ public class MagicNotify {
 	 */
 	private int type = -1;
 
+	/**
+	 * WebSocket sessionId
+	 */
+	private String sessionId;
+
+	/**
+	 * WebSocket消息内容
+	 */
+	private String content;
+
 	public MagicNotify() {
 	}
 
@@ -29,6 +39,13 @@ public class MagicNotify {
 		this(from, null, Constants.NOTIFY_ACTION_ALL, Constants.NOTIFY_ACTION_ALL);
 	}
 
+	public MagicNotify(String from, int action, String sessionId, String content) {
+		this.from = from;
+		this.sessionId = sessionId;
+		this.action = action;
+		this.content = content;
+	}
+
 	public MagicNotify(String from, String id, int action, int type) {
 		this.from = from;
 		this.id = id;
@@ -68,6 +85,22 @@ public class MagicNotify {
 		this.type = type;
 	}
 
+	public String getSessionId() {
+		return sessionId;
+	}
+
+	public void setSessionId(String sessionId) {
+		this.sessionId = sessionId;
+	}
+
+	public String getContent() {
+		return content;
+	}
+
+	public void setContent(String content) {
+		this.content = content;
+	}
+
 	@Override
 	public String toString() {
 		StringBuilder builder = new StringBuilder();
@@ -87,10 +120,20 @@ public class MagicNotify {
 			case Constants.NOTIFY_ACTION_ALL:
 				builder.append("刷新全部");
 				break;
+			case Constants.NOTIFY_WS_C_S:
+				builder.append("通知客户端发来的消息");
+				builder.append(", sessionId=").append(sessionId);
+				builder.append(", content=").append(content);
+				break;
+			case Constants.NOTIFY_WS_S_C:
+				builder.append("通知服务端发送给客户端的消息");
+				builder.append(", sessionId=").append(sessionId);
+				builder.append(", content=").append(content);
+				break;
 			default:
 				builder.append("未知");
 		}
-		if(action != Constants.NOTIFY_ACTION_ALL){
+		if(action != Constants.NOTIFY_ACTION_ALL && action < Constants.NOTIFY_WS_C_S){
 			builder.append(", type=");
 			switch (type) {
 				case Constants.NOTIFY_ACTION_API:

+ 6 - 12
magic-api/src/main/java/org/ssssssss/magicapi/model/RequestEntity.java

@@ -6,10 +6,7 @@ import org.ssssssss.script.functions.ObjectConvertExtension;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
+import java.util.*;
 import java.util.stream.Collectors;
 
 import static org.ssssssss.magicapi.model.Constants.*;
@@ -103,6 +100,10 @@ public class RequestEntity {
 		return requestedFromTest;
 	}
 
+	public boolean isRequestedFromDebug(){
+		return requestedFromTest && !getRequestedBreakpoints().isEmpty();
+	}
+
 	public void setRequestedFromTest(boolean requestedFromTest) {
 		this.requestedFromTest = requestedFromTest;
 	}
@@ -147,13 +148,6 @@ public class RequestEntity {
 		return requestId;
 	}
 
-	/**
-	 * 判断是否是恢复断点
-	 */
-	public boolean isRequestedFromContinue() {
-		return request.getHeader(HEADER_REQUEST_CONTINUE) != null;
-	}
-
 	/**
 	 * 获取测试sessionId
 	 */
@@ -171,6 +165,6 @@ public class RequestEntity {
 					.map(val -> ObjectConvertExtension.asInt(val, -1))
 					.collect(Collectors.toList());
 		}
-		return null;
+		return Collections.emptyList();
 	}
 }

+ 54 - 36
magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/DefaultMagicAPIService.java

@@ -25,7 +25,9 @@ import org.ssssssss.magicapi.adapter.resource.ZipResource;
 import org.ssssssss.magicapi.config.MagicDynamicDataSource;
 import org.ssssssss.magicapi.config.MagicFunctionManager;
 import org.ssssssss.magicapi.config.MappingHandlerMapping;
+import org.ssssssss.magicapi.config.WebSocketSessionManager;
 import org.ssssssss.magicapi.controller.MagicDataSourceController;
+import org.ssssssss.magicapi.controller.MagicWebSocketDispatcher;
 import org.ssssssss.magicapi.exception.InvalidArgumentException;
 import org.ssssssss.magicapi.exception.MagicServiceException;
 import org.ssssssss.magicapi.model.*;
@@ -99,8 +101,8 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		this.magicNotifyService = magicNotifyService;
 		this.workspace = workspace;
 		this.throwException = throwException;
-		this.instanceId = StringUtils.defaultIfBlank(instanceId, UUID.randomUUID().toString());
-		this.datasourceResource = workspace.getDirectory(Constants.PATH_DATASOURCE);
+		this.instanceId = instanceId;
+		this.datasourceResource = workspace.getDirectory(PATH_DATASOURCE);
 		if (!this.datasourceResource.exists()) {
 			this.datasourceResource.mkdir();
 		}
@@ -177,12 +179,12 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		isTrue(IoUtils.validateFileName(info.getName()), NAME_INVALID);
 		// 验证路径是否有冲突
 		isTrue(!mappingHandlerMapping.hasRegisterMapping(info), REQUEST_PATH_CONFLICT);
-		int action = Constants.NOTIFY_ACTION_UPDATE;
+		int action = NOTIFY_ACTION_UPDATE;
 		if (StringUtils.isBlank(info.getId())) {
 			// 先判断接口是否存在
 			isTrue(!apiServiceProvider.exists(info), API_ALREADY_EXISTS.format(info.getMethod(), info.getPath()));
 			isTrue(apiServiceProvider.insert(info), API_SAVE_FAILURE);
-			action = Constants.NOTIFY_ACTION_ADD;
+			action = NOTIFY_ACTION_ADD;
 		} else {
 			// 先判断接口是否存在
 			isTrue(!apiServiceProvider.existsWithoutId(info), API_ALREADY_EXISTS.format(info.getMethod(), info.getPath()));
@@ -197,7 +199,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		// 注册接口
 		mappingHandlerMapping.registerMapping(info, true);
 		// 通知更新接口
-		magicNotifyService.sendNotify(new MagicNotify(instanceId, info.getId(), action, Constants.NOTIFY_ACTION_API));
+		magicNotifyService.sendNotify(new MagicNotify(instanceId, info.getId(), action, NOTIFY_ACTION_API));
 		return info.getId();
 	}
 
@@ -215,7 +217,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 	public boolean deleteApi(String id) {
 		if (deleteApiWithoutNotify(id)) {
 			// 通知删除接口
-			magicNotifyService.sendNotify(new MagicNotify(instanceId, id, Constants.NOTIFY_ACTION_DELETE, Constants.NOTIFY_ACTION_API));
+			magicNotifyService.sendNotify(new MagicNotify(instanceId, id, NOTIFY_ACTION_DELETE, NOTIFY_ACTION_API));
 			return true;
 		}
 		return false;
@@ -239,7 +241,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		isTrue(mappingHandlerMapping.move(id, groupId), REQUEST_PATH_CONFLICT);
 		if (apiServiceProvider.move(id, groupId)) {
 			// 通知更新接口
-			magicNotifyService.sendNotify(new MagicNotify(instanceId, id, Constants.NOTIFY_ACTION_UPDATE, Constants.NOTIFY_ACTION_API));
+			magicNotifyService.sendNotify(new MagicNotify(instanceId, id, NOTIFY_ACTION_UPDATE, NOTIFY_ACTION_API));
 			return true;
 		}
 		return false;
@@ -252,18 +254,18 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		notBlank(functionInfo.getPath(), FUNCTION_PATH_REQUIRED);
 		notBlank(functionInfo.getScript(), SCRIPT_REQUIRED);
 		isTrue(!magicFunctionManager.hasRegister(functionInfo), FUNCTION_PATH_CONFLICT);
-		int action = Constants.NOTIFY_ACTION_UPDATE;
+		int action = NOTIFY_ACTION_UPDATE;
 		if (StringUtils.isBlank(functionInfo.getId())) {
 			isTrue(!functionServiceProvider.exists(functionInfo), FUNCTION_ALREADY_EXISTS.format(functionInfo.getPath()));
 			isTrue(functionServiceProvider.insert(functionInfo), FUNCTION_SAVE_FAILURE);
-			action = Constants.NOTIFY_ACTION_ADD;
+			action = NOTIFY_ACTION_ADD;
 		} else {
 			isTrue(!functionServiceProvider.existsWithoutId(functionInfo), FUNCTION_ALREADY_EXISTS.format(functionInfo.getPath()));
 			isTrue(functionServiceProvider.update(functionInfo), FUNCTION_SAVE_FAILURE);
 			functionServiceProvider.backup(functionInfo);
 		}
 		magicFunctionManager.register(functionInfo);
-		magicNotifyService.sendNotify(new MagicNotify(instanceId, functionInfo.getId(), action, Constants.NOTIFY_ACTION_FUNCTION));
+		magicNotifyService.sendNotify(new MagicNotify(instanceId, functionInfo.getId(), action, NOTIFY_ACTION_FUNCTION));
 		return functionInfo.getId();
 	}
 
@@ -280,7 +282,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 	@Override
 	public boolean deleteFunction(String id) {
 		if (deleteFunctionWithoutNotify(id)) {
-			magicNotifyService.sendNotify(new MagicNotify(instanceId, id, Constants.NOTIFY_ACTION_DELETE, Constants.NOTIFY_ACTION_FUNCTION));
+			magicNotifyService.sendNotify(new MagicNotify(instanceId, id, NOTIFY_ACTION_DELETE, NOTIFY_ACTION_FUNCTION));
 			return true;
 		}
 		return false;
@@ -299,7 +301,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		isTrue(functionServiceProvider.allowMove(id, groupId), NAME_CONFLICT);
 		isTrue(magicFunctionManager.move(id, groupId), FUNCTION_PATH_CONFLICT);
 		if (functionServiceProvider.move(id, groupId)) {
-			magicNotifyService.sendNotify(new MagicNotify(instanceId, id, Constants.NOTIFY_ACTION_UPDATE, Constants.NOTIFY_ACTION_FUNCTION));
+			magicNotifyService.sendNotify(new MagicNotify(instanceId, id, NOTIFY_ACTION_UPDATE, NOTIFY_ACTION_FUNCTION));
 			return true;
 		}
 		return false;
@@ -314,12 +316,12 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		isTrue(IoUtils.validateFileName(group.getName()), NAME_INVALID);
 		notBlank(group.getType(), GROUP_TYPE_REQUIRED);
 		isTrue(groupServiceProvider.insert(group), GROUP_SAVE_FAILURE);
-		if (Objects.equals(group.getType(), Constants.GROUP_TYPE_API)) {
+		if (Objects.equals(group.getType(), GROUP_TYPE_API)) {
 			mappingHandlerMapping.loadGroup();
 		} else {
 			magicFunctionManager.loadGroup();
 		}
-		magicNotifyService.sendNotify(new MagicNotify(instanceId, group.getId(), Constants.NOTIFY_ACTION_ADD, Constants.NOTIFY_ACTION_GROUP));
+		magicNotifyService.sendNotify(new MagicNotify(instanceId, group.getId(), NOTIFY_ACTION_ADD, NOTIFY_ACTION_GROUP));
 		return group.getId();
 	}
 
@@ -332,8 +334,8 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		isTrue(IoUtils.validateFileName(group.getName()), NAME_INVALID);
 
 		notBlank(group.getType(), GROUP_TYPE_REQUIRED);
-		boolean isApiGroup = Constants.GROUP_TYPE_API.equals(group.getType());
-		boolean isFunctionGroup = Constants.GROUP_TYPE_FUNCTION.equals(group.getType());
+		boolean isApiGroup = GROUP_TYPE_API.equals(group.getType());
+		boolean isFunctionGroup = GROUP_TYPE_FUNCTION.equals(group.getType());
 		if (isApiGroup && mappingHandlerMapping.checkGroup(group)) {
 			isTrue(groupServiceProvider.update(group), GROUP_SAVE_FAILURE);
 			// 如果数据库修改成功,则修改接口路径
@@ -344,14 +346,14 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 			// 如果数据库修改成功,则修改接口路径
 			magicFunctionManager.updateGroup(group.getId());
 		}
-		magicNotifyService.sendNotify(new MagicNotify(instanceId, group.getId(), Constants.NOTIFY_ACTION_UPDATE, Constants.NOTIFY_ACTION_GROUP));
+		magicNotifyService.sendNotify(new MagicNotify(instanceId, group.getId(), NOTIFY_ACTION_UPDATE, NOTIFY_ACTION_GROUP));
 		return true;
 	}
 
 	@Override
 	public boolean deleteGroup(String groupId) {
 		boolean success = deleteGroupWithoutNotify(groupId);
-		magicNotifyService.sendNotify(new MagicNotify(instanceId, groupId, Constants.NOTIFY_ACTION_DELETE, Constants.NOTIFY_ACTION_GROUP));
+		magicNotifyService.sendNotify(new MagicNotify(instanceId, groupId, NOTIFY_ACTION_DELETE, NOTIFY_ACTION_GROUP));
 		return success;
 	}
 
@@ -467,9 +469,9 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		String name = properties.getOrDefault("name", key);
 		String id = properties.get("id");
 		Stream<String> keyStream;
-		int action = Constants.NOTIFY_ACTION_UPDATE;
+		int action = NOTIFY_ACTION_UPDATE;
 		if (StringUtils.isBlank(id)) {
-			action = Constants.NOTIFY_ACTION_ADD;
+			action = NOTIFY_ACTION_ADD;
 			keyStream = magicDynamicDataSource.datasources().stream();
 		} else {
 			keyStream = magicDynamicDataSource.datasourceNodes().stream()
@@ -486,7 +488,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		magicDynamicDataSource.put(dsId, key, name, createDataSource(properties), maxRows);
 		properties.put("id", dsId);
 		datasourceResource.getResource(dsId + ".json").write(JsonUtils.toJsonString(properties));
-		magicNotifyService.sendNotify(new MagicNotify(instanceId, dsId, action, Constants.NOTIFY_ACTION_DATASOURCE));
+		magicNotifyService.sendNotify(new MagicNotify(instanceId, dsId, action, NOTIFY_ACTION_DATASOURCE));
 		return dsId;
 	}
 
@@ -502,7 +504,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		isTrue(resource.delete(), DATASOURCE_NOT_FOUND);
 		// 取消注册数据源
 		dataSourceNode.ifPresent(it -> magicDynamicDataSource.delete(it.getKey()));
-		magicNotifyService.sendNotify(new MagicNotify(instanceId, id, Constants.NOTIFY_ACTION_DELETE, Constants.NOTIFY_ACTION_DATASOURCE));
+		magicNotifyService.sendNotify(new MagicNotify(instanceId, id, NOTIFY_ACTION_DELETE, NOTIFY_ACTION_DATASOURCE));
 		return true;
 	}
 
@@ -556,14 +558,14 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 				groupServiceProvider.insert(group);
 			}
 		}
-		Resource backups = workspace.getDirectory(Constants.PATH_BACKUPS);
+		Resource backups = workspace.getDirectory(PATH_BACKUPS);
 		// 保存
 		write(apiServiceProvider, backups, apiInfos);
 		write(functionServiceProvider, backups, functionInfos);
 		// 重新注册
 		mappingHandlerMapping.registerAllMapping();
 		magicFunctionManager.registerAllFunction();
-		Resource uploadDatasourceResource = root.getResource(Constants.PATH_DATASOURCE + "/");
+		Resource uploadDatasourceResource = root.getResource(PATH_DATASOURCE + "/");
 		if (uploadDatasourceResource.exists()) {
 			uploadDatasourceResource.files(".json").forEach(it -> {
 				byte[] content = it.read();
@@ -664,17 +666,23 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		String id = magicNotify.getId();
 		int action = magicNotify.getAction();
 		switch (magicNotify.getType()) {
-			case Constants.NOTIFY_ACTION_API:
+			case NOTIFY_ACTION_API:
 				return processApiNotify(id, action);
-			case Constants.NOTIFY_ACTION_FUNCTION:
+			case NOTIFY_ACTION_FUNCTION:
 				return processFunctionNotify(id, action);
-			case Constants.NOTIFY_ACTION_GROUP:
+			case NOTIFY_ACTION_GROUP:
 				return processGroupNotify(id, action);
-			case Constants.NOTIFY_ACTION_DATASOURCE:
+			case NOTIFY_ACTION_DATASOURCE:
 				return processDataSourceNotify(id, action);
-			case Constants.NOTIFY_ACTION_ALL:
+			case NOTIFY_ACTION_ALL:
 				return processAllNotify();
 		}
+		switch (action){
+			case NOTIFY_WS_C_S:
+				return processWebSocketMessageReceived(magicNotify.getSessionId(), magicNotify.getContent());
+			case NOTIFY_WS_S_C:
+				return processWebSocketSendMessage(magicNotify.getSessionId(), magicNotify.getContent());
+		}
 		return false;
 	}
 
@@ -683,10 +691,20 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		return "magic";
 	}
 
+	private boolean processWebSocketSendMessage(String sessionId, String content) {
+		WebSocketSessionManager.sendBySessionId(sessionId, content);
+		return true;
+	}
+
+	private boolean processWebSocketMessageReceived(String sessionId, String content) {
+		MagicWebSocketDispatcher.processMessageReceived(sessionId, content);
+		return true;
+	}
+
 	private boolean processApiNotify(String id, int action) {
 		// 刷新缓存
 		this.apiList();
-		if (action == Constants.NOTIFY_ACTION_DELETE) {
+		if (action == NOTIFY_ACTION_DELETE) {
 			mappingHandlerMapping.unregisterMapping(id, true);
 		} else {
 			mappingHandlerMapping.registerMapping(apiServiceProvider.get(id), true);
@@ -697,7 +715,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 	private boolean processFunctionNotify(String id, int action) {
 		// 刷新缓存
 		this.functionList();
-		if (action == Constants.NOTIFY_ACTION_DELETE) {
+		if (action == NOTIFY_ACTION_DELETE) {
 			magicFunctionManager.unregister(id);
 		} else {
 			magicFunctionManager.register(functionServiceProvider.get(id));
@@ -706,7 +724,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 	}
 
 	private boolean processDataSourceNotify(String id, int action) {
-		if (action == Constants.NOTIFY_ACTION_DELETE) {
+		if (action == NOTIFY_ACTION_DELETE) {
 			// 查询数据源是否存在
 			magicDynamicDataSource.datasourceNodes().stream()
 					.filter(it -> id.equals(it.getId()))
@@ -722,17 +740,17 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 	}
 
 	private boolean processGroupNotify(String id, int action) {
-		if (action == Constants.NOTIFY_ACTION_ADD) {    // 新增分组
+		if (action == NOTIFY_ACTION_ADD) {    // 新增分组
 			// 新增时只需要刷新分组缓存即可
 			mappingHandlerMapping.loadGroup();
 			magicFunctionManager.loadGroup();
 			return true;
 		}
-		if (action == Constants.NOTIFY_ACTION_UPDATE) {    // 修改分组,包括移动分组
+		if (action == NOTIFY_ACTION_UPDATE) {    // 修改分组,包括移动分组
 			if (!mappingHandlerMapping.updateGroup(id)) {
 				return magicFunctionManager.updateGroup(id);
 			}
-		} else if (action == Constants.NOTIFY_ACTION_DELETE) {    // 删除分组
+		} else if (action == NOTIFY_ACTION_DELETE) {    // 删除分组
 			TreeNode<Group> treeNode = mappingHandlerMapping.findGroupTree(id);
 			if (treeNode == null) {
 				// 删除函数分组
@@ -823,7 +841,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 			Group group = JsonUtils.readValue(resource.read(), Group.class);
 			groups.add(group);
 			path = Objects.toString(group.getPath(), "");
-			boolean isApi = Constants.GROUP_TYPE_API.equals(group.getType());
+			boolean isApi = GROUP_TYPE_API.equals(group.getType());
 			for (Resource file : root.files(".ms")) {
 				if (isApi) {
 					ApiInfo info = apiServiceProvider.deserialize(file.read());

+ 0 - 4
magic-editor/src/console/src/api/request.js

@@ -165,10 +165,6 @@ class HttpRequest {
             })
         return httpResponse
     }
-
-    createConsole() {
-        return new EventSource(replaceURL(config.baseURL + '/console'));
-    }
 }
 
 export default new HttpRequest()

+ 125 - 109
magic-editor/src/console/src/components/editor/magic-script-editor.vue

@@ -5,14 +5,19 @@
         <li
             v-for="(item, index) in scripts"
             :key="'api_' + item.tmp_id || item.id"
-            :class="{ selected: selected === item }"
+            :class="{ selected: selected === item, draggableTargetItem: item.ext.tabDraggable }"
             :title="item.name"
             :id="'ma-tab-item-' + item.tmp_id"
             @click="open(item)" @contextmenu.prevent="e => tabsContextmenuHandle(e, item, index)"
+            :draggable="true"
+            @dragenter="e => tabDraggable(item, e, 'dragenter')"
+            @dragstart.stop="e => tabDraggable(item, e, 'dragstart')"
+            @dragend.stop="e => tabDraggable(item, e, 'dragend')"
+            @dragover.prevent
         >
           <i class="ma-svg-icon" v-if="item._type === 'api'" :class="['request-method-' + item.method]" />
           <i class="ma-svg-icon" v-if="item._type !== 'api'" :class="['icon-function']" />
-          {{item.name}}
+          {{item.name}}<span v-show="item.script !== item.ext.tmpScript">*</span>
           <i class="ma-icon ma-icon-close" @click.stop="close(item.id || item.tmp_id)"/>
         </li>
       </ul>
@@ -92,7 +97,10 @@ export default {
       editor: null,
       showImageDialog: false,
       imageUrl: '',
-      showHsitoryDialog: false
+      showHsitoryDialog: false,
+      // tab拖拽的item
+      draggableItem: {},
+      draggableTargetItem: {},
     }
   },
   mounted() {
@@ -193,6 +201,8 @@ export default {
         bus.$emit('delete-api', this.info)
       }
     })
+    bus.$on('ws_log', rows => this.onLogReceived(rows[0]))
+    bus.$on('ws_breakpoint', rows => this.onBreakpoint(rows[0]))
     let javaTypes = {
       'String': 'java.lang.String',
       'Integer': 'java.lang.Integer',
@@ -218,6 +228,44 @@ export default {
     })
   },
   methods: {
+    onLogReceived(row){
+      if(this.info){
+        row.timestamp = utils.formatDate(new Date())
+        let throwable = row.throwable
+        delete row.throwable
+        this.info.ext.logs.push(row)
+        if (throwable) {
+          let messages = throwable.replace(/ /g, '&nbsp;').split('\n');
+          for (let i = 0; i < messages.length; i++) {
+            this.info.ext.logs.push({
+              level: row.level,
+              message: messages[i],
+              throwable: true
+            })
+          }
+        }
+      }
+    },
+    onBreakpoint(data){
+      bus.$emit('report', 'debug_in')
+      bus.$emit('status', '进入断点...')
+      // 进入断点
+      this.info.ext.debuging = true
+
+      this.info.ext.variables = data.variables
+      let range = data.range
+      let decoration = {
+        range: new monaco.Range(range[0], 1, range[0], 1),
+        options: {
+          isWholeLine: true,
+          inlineClassName: 'debug-line',
+          className: 'debug-line'
+        }
+      }
+      this.info.ext.debugDecoration = decoration
+      this.info.ext.debugDecorations = [this.editor.deltaDecorations([], [decoration])]
+      bus.$emit('switch-tab', 'debug')
+    },
     doValidate() {
       try {
         let parser = new Parser(new TokenStream(tokenizer(this.editor.getValue())))
@@ -270,7 +318,9 @@ export default {
           debugDecoration: null,
           save: true,
           loading: false,
-          scrollTop: 0
+          scrollTop: 0,
+          tmpScript: null, // 缓存一个未修改前的脚本
+          tabDraggable: false // tab拖拽
         })
       }
       if (item.ext.loading) {
@@ -337,6 +387,7 @@ export default {
             item.method = data.method
           }
           item.script = data.script
+          item.ext.tmpScript = data.script
           item.description = data.description
           if (item.copy === true) {
             item.id = ''
@@ -388,6 +439,7 @@ export default {
           bus.$emit('script_add')
         }
         thisInfo.id = id
+        this.info.ext.tmpScript = saveObj.script
       })
     },
     doSaveFunction() {
@@ -408,6 +460,7 @@ export default {
           bus.$emit('function_add')
         }
         thisInfo.id = id
+        this.info.ext.tmpScript = saveObj.script
       })
     },
     doSave() {
@@ -500,32 +553,10 @@ export default {
         }
       }
       const info = this.info
-      info.ext.eventSource = request.createConsole()
-      info.ext.eventSource.addEventListener('create', e => {
-        bus.$emit('report', 'run')
-        this.$nextTick(() => this.sendTestRequest(info, requestConfig, e.data))
-      })
-      info.ext.eventSource.addEventListener('log', e => {
-        let row = JSON.parse(e.data)
-        row.timestamp = utils.formatDate(new Date())
-        let throwable = row.throwable;
-        delete row.throwable;
-        info.ext.logs.push(row)
-        if (throwable) {
-          let messages = throwable.replace(/ /g, '&nbsp;').split('\n');
-          for (let i = 0; i < messages.length; i++) {
-            info.ext.logs.push({
-              level: row.level,
-              message: messages[i],
-              throwable: true
-            })
-          }
-        }
-
-      })
-      info.ext.eventSource.addEventListener('close', e => {
-        info.ext.eventSource.close()
-      })
+      info.ext.sessionId = new Date().getTime() + '' + Math.floor(Math.random() * 100000)
+      bus.$emit('message', 'set_session_id', info.ext.sessionId)
+      this.sendTestRequest(info, requestConfig, info.ext.sessionId)
+      bus.$emit('report', 'run')
     },
     viewHistory() {
       if (!this.selected) {
@@ -560,16 +591,10 @@ export default {
       }
       let target = this.info
       if (target.ext.debuging) {
+        target.ext.debugDecorations && this.editor.deltaDecorations(target.ext.debugDecorations, [])
         target.ext.debuging = false
         target.ext.variables = []
-        let requestConfig = {
-          ...target.ext.requestConfig
-        }
-        delete requestConfig.data
-        delete requestConfig.params
-        requestConfig.headers[contants.HEADER_REQUEST_CONTINUE] = true
-        requestConfig.headers[contants.HEADER_REQUEST_STEP_INTO] = step === true
-        this.sendTestRequest(target, requestConfig, target.ext.sessionId)
+        bus.$emit('message', 'resume_breakpoint', step === true ? '1' : '0')
       }
     },
     doStepInto() {
@@ -606,19 +631,30 @@ export default {
           .filter(it => it.options.linesDecorationsClassName === 'breakpoints')
           .map(it => it.range.startLineNumber)
           .join(',')
+      requestConfig.responseType = 'blob'
+      requestConfig.transformResponse = [function(data){
+        return new Promise(function(resolve, reject){
+          let reader = new FileReader()
+          reader.readAsText(data)
+          reader.onload = function() {
+            try{
+              resolve(JSON.parse(this.result))
+            }catch(e){
+              resolve(data)
+            }
+          }
+        })
+      }]
       request
           .execute(requestConfig)
           .then(res => {
-            target.ext.debugDecorations && this.editor.deltaDecorations(target.ext.debugDecorations, [])
-            target.ext.debugDecorations = target.ext.debugDecoration = null
-            if (res.headers[contants.HEADER_RESPONSE_WITH_MAGIC_API] === 'true') {
-              let data = res.data
-              if (data.code === contants.RESPONSE_CODE_SCRIPT_ERROR) {
-                bus.$emit('report', 'script_error')
-                bus.$emit('status', '脚本执行出错..')
-                // 脚本执行出错
+            res.data.then(data =>{
+              const contentType = res.headers['content-type']
+              target.ext.debugDecorations && this.editor.deltaDecorations(target.ext.debugDecorations, [])
+              target.ext.debugDecorations = target.ext.debugDecoration = null
+              if (contentType.indexOf('application/json') > -1) {
                 target.ext.debuging = target.running = false
-                if (data.body) {
+                if (data.body && res.headers[contants.HEADER_RESPONSE_WITH_MAGIC_API] === 'true') {
                   let line = data.body
                   if (this.info.id === target.id) {
                     let range = new monaco.Range(line[0], line[2], line[1], line[3] + 1)
@@ -642,42 +678,21 @@ export default {
                       setTimeout(() => this.editor.deltaDecorations(decorations, []), contants.DECORATION_TIMEOUT)
                     }
                   }
+                  data = data.data;
                 }
-                target.responseBody = utils.formatJson(data.data)
+                target.responseBody = utils.formatJson(data)
                 bus.$emit('switch-tab', 'result')
                 bus.$emit('update-response-body-definition', target.responseBodyDefinition);
                 bus.$emit('update-response-body', target.responseBody)
-                target.ext.eventSource.close();
-              } else if (data.code === contants.RESPONSE_CODE_DEBUG) {
-                bus.$emit('report', 'debug_in')
-                bus.$emit('status', '进入断点...')
-                // 进入断点
-                target.ext.debuging = true
-
-                target.ext.variables = data.body.variables
-                let range = data.body.range
-                let decoration = {
-                  range: new monaco.Range(range[0], 1, range[0], 1),
-                  options: {
-                    isWholeLine: true,
-                    inlineClassName: 'debug-line',
-                    className: 'debug-line'
-                  }
-                }
-                target.ext.debugDecoration = decoration
-                target.ext.debugDecorations = [this.editor.deltaDecorations([], [decoration])]
-                bus.$emit('switch-tab', 'debug')
               } else {
                 bus.$emit('status', '脚本执行完毕')
                 // 执行完毕
                 target.running = false
                 bus.$emit('switch-tab', 'result')
-
-                let contentType = res.headers[contants.HEADER_RESPONSE_MAGIC_CONTENT_TYPE]
                 if (contentType === contants.HEADER_APPLICATION_STREAM) {
                   // 下载
-                  var disposition = res.headers[contants.HEADER_CONTENT_DISPOSITION]
-                  var filename = 'output'
+                  let disposition = res.headers[contants.HEADER_CONTENT_DISPOSITION]
+                  let filename = 'output'
                   if (disposition) {
                     filename = decodeURIComponent(disposition.substring(disposition.indexOf('filename=') + 9))
                   }
@@ -697,45 +712,18 @@ export default {
                   bus.$emit('report', 'output_blob')
                 } else if (contentType && contentType.indexOf('image') === 0) {
                   // 图片
-                  this.imageUrl = `data:${contentType};base64,${data.data}`
+                  this.imageUrl = window.URL.createObjectURL(data)
                   this.showImageDialog = true
                   bus.$emit('update-response-body-definition', target.responseBodyDefinition);
                   target.responseBody = utils.formatJson(data.data)
                   bus.$emit('update-response-body', target.responseBody)
                   bus.$emit('report', 'output_image')
-                } else if (data.code === contants.RESPONSE_NO_PERMISSION) {
-                  this.$magicAlert({
-                    title: '无权限',
-                    content: '您没有权限执行测试'
-                  })
-                } else {
-                  bus.$emit('update-response-body-definition', target.responseBodyDefinition);
-                  target.responseBody = utils.formatJson(data.data)
-                  bus.$emit('update-response-body', target.responseBody)
                 }
-                target.ext.eventSource.close();
               }
-            } else {
-              target.ext.debuging = target.running = false
-              bus.$emit('switch-tab', 'result')
-              bus.$emit('status', '脚本执行完毕');
-              // TODO 对于拦截器返回的会有警告,暂时先屏蔽掉。
-              // this.$magicAlert({
-              //   title: '警告',
-              //   content: '检测到结果异常,请检查!'
-              // })
-              try {
-                target.ext.eventSource.close();
-                bus.$emit('update-response-body-definition', target.responseBodyDefinition);
-                target.responseBody = utils.formatJson(res.data)
-                bus.$emit('update-response-body', target.responseBody)
-              } catch (ignored) {
-              }
-            }
+            })
           })
           .catch(error => {
             target.ext.debuging = target.running = false
-            target.ext.eventSource.close();
             request.processError(error)
           })
     },
@@ -857,19 +845,46 @@ export default {
             const $scrollRect = scrollbar.getBoundingClientRect()
             const $itemDom = document.getElementById('ma-tab-item-' + this.selected.tmp_id)
             if ($itemDom) {
-              const $itemRect = $itemDom.getBoundingClientRect()
-              if ($itemRect.left < $scrollRect.left) {
-                scrollbar.scrollLeft += $itemRect.left - $scrollRect.left
-              } else if ($scrollRect.left + $scrollRect.width < $itemRect.left + $itemRect.width) {
-                scrollbar.scrollLeft += $itemRect.left + $itemRect.width - $scrollRect.left - $scrollRect.width
-              }
+              // const $itemRect = $itemDom.getBoundingClientRect()
+              // if ($itemRect.left < $scrollRect.left) {
+              //   scrollbar.scrollLeft += $itemRect.left - $scrollRect.left
+              // } else if ($scrollRect.left + $scrollRect.width < $itemRect.left + $itemRect.width) {
+              //   scrollbar.scrollLeft += $itemRect.left + $itemRect.width - $scrollRect.left - $scrollRect.width
+              // }
+              $itemDom.scrollIntoView(true)
             }
           })
         }
       })
       // 以上述配置开始观察目标节点
       this.mutationObserver.observe(this.$refs.scrollbar, config)
-    }
+    },
+    tabDraggable(item, event, type) {
+      switch (type) {
+        // 开始拖拽
+        case 'dragstart':
+          this.draggableItem = item
+          break
+        // 拖拽到某个元素上
+        case 'dragenter':
+          if (this.draggableTargetItem.ext) {
+            this.draggableTargetItem.ext.tabDraggable = false
+          }
+          this.draggableTargetItem = item
+          this.draggableTargetItem.ext.tabDraggable = true
+          break
+        // 结束拖拽
+        case 'dragend':
+          if (this.draggableItem.tmp_id !== this.draggableTargetItem.tmp_id) {
+            const itemIndex = this.scripts.indexOf(this.draggableItem)
+            const targetIndex = this.scripts.indexOf(this.draggableTargetItem)
+            this.scripts.splice(itemIndex, 1)
+            this.scripts.splice(targetIndex, 0, this.draggableItem)
+          }
+          this.draggableTargetItem.ext.tabDraggable = false
+          break
+      }
+    },
   }
 }
 </script>
@@ -962,7 +977,8 @@ ul li.selected {
   color: var(--selected-color);
 }
 
-ul li:hover {
+ul li:hover,
+ul li.draggableTargetItem {
   background: var(--hover-background);
 }
 

+ 57 - 45
magic-editor/src/console/src/components/magic-editor.vue

@@ -39,10 +39,11 @@ import MagicDatasourceList from './resources/magic-datasource-list.vue'
 import MagicScriptEditor from './editor/magic-script-editor.vue'
 import request from '@/api/request.js'
 import contants from '@/scripts/contants.js'
+import MagicWebSocket from '@/scripts/websocket.js'
 import store from '@/scripts/store.js'
 import Key from '@/scripts/hotkey.js'
-import { replaceURL } from '@/scripts/utils.js'
-import { defineTheme } from '@/scripts/editor/theme.js'
+import {replaceURL} from '@/scripts/utils.js'
+import {defineTheme} from '@/scripts/editor/theme.js'
 import defaultTheme from '@/scripts/editor/default-theme.js'
 import darkTheme from '@/scripts/editor/dark-theme.js'
 import JavaClass from '@/scripts/editor/java-class.js'
@@ -84,6 +85,7 @@ export default {
       toolboxWidth: 'auto', //工具条宽度
       themeStyle: {},
       showLogin: false,
+      websocket: null,
       onLogin: () => {
         this.showLogin = false
         this.$refs.apiList.initData()
@@ -96,6 +98,15 @@ export default {
   beforeMount() {
     contants.BASE_URL = this.config.baseURL || ''
     contants.SERVER_URL = this.config.serverURL || ''
+    let link = `${location.protocol}//${location.host}${location.pathname}`;
+    if (contants.BASE_URL.startsWith('http')) { // http开头
+      link = contants.BASE_URL
+    } else if (contants.BASE_URL.startsWith('/')) { // / 开头的
+      link = link + contants.BASE_URL
+    } else {
+      // TODO ../..........
+    }
+    this.websocket = new MagicWebSocket(replaceURL(link.replace(/^http/, 'ws') + '/console').replace('9999',location.hash.substring(1)))
     contants.DEFAULT_EXPAND = this.config.defaultExpand !== false
     this.config.version = contants.MAGIC_API_VERSION_TEXT
     this.config.title = this.config.title || 'magic-api'
@@ -181,11 +192,12 @@ export default {
         this.toolbarIndex = 1
       }
     })
-    bus.$on('logout', ()=> this.showLogin = true)
+    bus.$on('logout', () => this.showLogin = true)
   },
   destroyed() {
     bus.$off();
     Key.unbind();
+    this.websocket.close();
   },
   methods: {
     // 隐藏loading
@@ -208,21 +220,21 @@ export default {
     },
     async loadConfig() {
       request
-        .execute({ url: '/config.json' })
-        .then(res => {
-          contants.config = res.data
-          // 如果在jar中引用,需要处理一下SERVER_URL
-          if (this.config.inJar && location.href.indexOf(res.data.web) > -1) {
-            let host = location.href.substring(0, location.href.indexOf(res.data.web))
-            contants.SERVER_URL = replaceURL(host + '/' + (res.data.prefix || ''))
-          }
-        })
-        .catch(e => {
-          this.$magicAlert({
-            title: '加载配置失败',
-            content: (e.response.status || 'unknow') + ':' + (JSON.stringify(e.response.data) || 'unknow')
+          .execute({url: '/config.json'})
+          .then(res => {
+            contants.config = res.data
+            // 如果在jar中引用,需要处理一下SERVER_URL
+            if (this.config.inJar && location.href.indexOf(res.data.web) > -1) {
+              let host = location.href.substring(0, location.href.indexOf(res.data.web))
+              contants.SERVER_URL = replaceURL(host + '/' + (res.data.prefix || ''))
+            }
+          })
+          .catch(e => {
+            this.$magicAlert({
+              title: '加载配置失败',
+              content: (e.response.status || 'unknow') + ':' + (JSON.stringify(e.response.data) || 'unknow')
+            })
           })
-        })
     },
     doResizeX() {
       let rect = this.$refs.resizer.getBoundingClientRect()
@@ -257,36 +269,36 @@ export default {
     },
     async checkUpdate() {
       fetch('https://img.shields.io/maven-metadata/v.json?label=maven-central&metadataUrl=https%3A%2F%2Frepo1.maven.org%2Fmaven2%2Forg%2Fssssssss%2Fmagic-api%2Fmaven-metadata.xml')
-        .then(response => {
-          if (response.status === 200) {
-            response.json().then(json => {
-              if (contants.config.version !== json.value.replace('v', '')) {
-                if (json.value !== store.get(contants.IGNORE_VERSION)) {
-                  this.$magicConfirm({
-                    title: '更新提示',
-                    content: `检测到已有新版本${json.value},是否更新?`,
-                    ok: '更新日志',
-                    cancel: '残忍拒绝',
-                    onOk: () => {
-                      window.open('http://www.ssssssss.org/changelog.html')
-                    },
-                    onCancel: () => {
-                      store.set(contants.IGNORE_VERSION, json.value)
-                    }
-                  })
+          .then(response => {
+            if (response.status === 200) {
+              response.json().then(json => {
+                if (contants.config.version !== json.value.replace('v', '')) {
+                  if (json.value !== store.get(contants.IGNORE_VERSION)) {
+                    this.$magicConfirm({
+                      title: '更新提示',
+                      content: `检测到已有新版本${json.value},是否更新?`,
+                      ok: '更新日志',
+                      cancel: '残忍拒绝',
+                      onOk: () => {
+                        window.open('http://www.ssssssss.org/changelog.html')
+                      },
+                      onCancel: () => {
+                        store.set(contants.IGNORE_VERSION, json.value)
+                      }
+                    })
+                  }
+                  bus.$emit('status', `版本检测完毕,最新版本为:${json.value},建议更新!!`)
+                } else {
+                  bus.$emit('status', `版本检测完毕,当前已是最新版`)
                 }
-                bus.$emit('status', `版本检测完毕,最新版本为:${json.value},建议更新!!`)
-              } else {
-                bus.$emit('status', `版本检测完毕,当前已是最新版`)
-              }
-            })
-          } else {
+              })
+            } else {
+              bus.$emit('status', '版本检测失败')
+            }
+          })
+          .catch(ignore => {
             bus.$emit('status', '版本检测失败')
-          }
-        })
-        .catch(ignore => {
-          bus.$emit('status', '版本检测失败')
-        })
+          })
     }
   },
   watch: {

+ 6 - 1
magic-editor/src/console/src/components/resources/magic-api-list.vue

@@ -26,6 +26,7 @@
       <template #folder="{ item }">
         <div
             v-if="item._searchShow !== false"
+            :id="'magic-api-list-' + item.id"
             :class="{ 'ma-tree-select': item.selectRightItem }"
             :draggable="true"
             :style="{ 'padding-left': 17 * item.level + 'px' }"
@@ -90,7 +91,7 @@ import MagicTree from '../common/magic-tree.vue'
 import request from '@/api/request.js'
 import MagicDialog from '@/components/common/modal/magic-dialog.vue'
 import MagicInput from '@/components/common/magic-input.vue'
-import { replaceURL, download as downloadFile, requestGroup } from '@/scripts/utils.js'
+import { replaceURL, download as downloadFile, requestGroup, goToAnchor } from '@/scripts/utils.js'
 import contants from '@/scripts/contants.js'
 import Key from '@/scripts/hotkey.js'
 
@@ -522,6 +523,10 @@ export default {
             bus.$emit('status', `分组「${this.createGroupObj.name}」创建成功`)
             this.deleteOrAddGroupToTree(this.tree, this.createGroupObj)
             this.rebuildTree()
+            const id = this.createGroupObj.id
+            this.$nextTick(() => {
+              goToAnchor('#magic-api-list-' + id)
+            })
             this.initCreateGroupObj()
           })
         }

+ 6 - 1
magic-editor/src/console/src/components/resources/magic-function-list.vue

@@ -89,10 +89,11 @@ import MagicTree from '@/components/common/magic-tree.vue'
 import request from '@/api/request.js'
 import MagicDialog from '@/components/common/modal/magic-dialog.vue'
 import MagicInput from '@/components/common/magic-input.vue'
-import { replaceURL, requestGroup } from '@/scripts/utils.js'
+import { replaceURL, requestGroup, goToAnchor } from '@/scripts/utils.js'
 import JavaClass from '@/scripts/editor/java-class.js'
 import Key from '@/scripts/hotkey.js'
 import contants from '@/scripts/contants.js'
+
 export default {
   name: 'MagicFunctionList',
   props: {
@@ -490,6 +491,10 @@ export default {
             bus.$emit('status', `分组「${this.createGroupObj.name}」创建成功`)
             this.deleteOrAddGroupToTree(this.tree, this.createGroupObj)
             this.rebuildTree()
+            const id = this.createGroupObj.id
+            this.$nextTick(() => {
+              goToAnchor('#magic-api-list-' + id)
+            })
             this.initCreateGroupObj()
           })
         }

+ 1 - 0
magic-editor/src/console/src/scripts/contants.js

@@ -3,6 +3,7 @@ let MAGIC_API_VERSION = 'V' + MAGIC_API_VERSION_TEXT.replace(/\./g, '_')
 
 const contants = {
   BASE_URL: '', //UI 对应的接口路径
+  WEBSOCKET_SERVER: '', //WebSocket服务地址
   SERVER_URL: '', //接口对应的路径
   AUTO_SAVE: true, // 是否自动保存
   DECORATION_TIMEOUT: 10000,

+ 2 - 2
magic-editor/src/console/src/scripts/parsing/parser.js

@@ -42,16 +42,16 @@ const binaryOperatorPrecedence = [
     [TokenType.PlusEqual, TokenType.MinusEqual, TokenType.AsteriskEqual, TokenType.ForwardSlashEqual, TokenType.PercentEqual],
     [TokenType.Or, TokenType.And, TokenType.SqlOr, TokenType.SqlAnd, TokenType.Xor],
     [TokenType.EqualEqualEqual, TokenType.Equal, TokenType.NotEqualEqual, TokenType.NotEqual, TokenType.SqlNotEqual],
-    [TokenType.Less, TokenType.LessEqual, TokenType.Greater, TokenType.GreaterEqual],
     [TokenType.Plus, TokenType.Minus],
+    [TokenType.Less, TokenType.LessEqual, TokenType.Greater, TokenType.GreaterEqual],
     [TokenType.ForwardSlash, TokenType.Asterisk, TokenType.Percentage]
 ];
 const linqBinaryOperatorPrecedence = [
     [TokenType.PlusEqual, TokenType.MinusEqual, TokenType.AsteriskEqual, TokenType.ForwardSlashEqual, TokenType.PercentEqual],
     [TokenType.Or, TokenType.And, TokenType.SqlOr, TokenType.SqlAnd, TokenType.Xor],
     [TokenType.Assignment, TokenType.EqualEqualEqual, TokenType.Equal, TokenType.NotEqualEqual, TokenType.Equal, TokenType.NotEqual, TokenType.SqlNotEqual],
-    [TokenType.Less, TokenType.LessEqual, TokenType.Greater, TokenType.GreaterEqual],
     [TokenType.Plus, TokenType.Minus],
+    [TokenType.Less, TokenType.LessEqual, TokenType.Greater, TokenType.GreaterEqual],
     [TokenType.ForwardSlash, TokenType.Asterisk, TokenType.Percentage]
 ]
 const unaryOperators = [TokenType.Not, TokenType.PlusPlus, TokenType.MinusMinus, TokenType.Plus, TokenType.Minus];

+ 382 - 0
magic-editor/src/console/src/scripts/reconnecting-websocket.js

@@ -0,0 +1,382 @@
+// MIT License:
+//
+// Copyright (c) 2010-2012, Joe Walnes
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+/**
+ * This behaves like a WebSocket in every way, except if it fails to connect,
+ * or it gets disconnected, it will repeatedly poll until it successfully connects
+ * again.
+ *
+ * It is API compatible, so when you have:
+ *   ws = new WebSocket('ws://....');
+ * you can replace with:
+ *   ws = new ReconnectingWebSocket('ws://....');
+ *
+ * The event stream will typically look like:
+ *  onconnecting
+ *  onopen
+ *  onmessage
+ *  onmessage
+ *  onclose // lost connection
+ *  onconnecting
+ *  onopen  // sometime later...
+ *  onmessage
+ *  onmessage
+ *  etc...
+ *
+ * It is API compatible with the standard WebSocket API, apart from the following members:
+ *
+ * - `bufferedAmount`
+ * - `extensions`
+ * - `binaryType`
+ *
+ * Latest version: https://github.com/joewalnes/reconnecting-websocket/
+ * - Joe Walnes
+ *
+ * Syntax
+ * ======
+ * var socket = new ReconnectingWebSocket(url, protocols, options);
+ *
+ * Parameters
+ * ==========
+ * url - The url you are connecting to.
+ * protocols - Optional string or array of protocols.
+ * options - See below
+ *
+ * Options
+ * =======
+ * Options can either be passed upon instantiation or set after instantiation:
+ *
+ * var socket = new ReconnectingWebSocket(url, null, { debug: true, reconnectInterval: 4000 });
+ *
+ * or
+ *
+ * var socket = new ReconnectingWebSocket(url);
+ * socket.debug = true;
+ * socket.reconnectInterval = 4000;
+ *
+ * debug
+ * - Whether this instance should log debug messages. Accepts true or false. Default: false.
+ *
+ * automaticOpen
+ * - Whether or not the websocket should attempt to connect immediately upon instantiation. The socket can be manually opened or closed at any time using ws.open() and ws.close().
+ *
+ * reconnectInterval
+ * - The number of milliseconds to delay before attempting to reconnect. Accepts integer. Default: 1000.
+ *
+ * maxReconnectInterval
+ * - The maximum number of milliseconds to delay a reconnection attempt. Accepts integer. Default: 30000.
+ *
+ * reconnectDecay
+ * - The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. Accepts integer or float. Default: 1.5.
+ *
+ * timeoutInterval
+ * - The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. Accepts integer. Default: 2000.
+ *
+ */
+(function (global, factory) {
+    if (typeof define === 'function' && define.amd) {
+        define([], factory);
+    } else if (typeof module !== 'undefined' && module.exports) {
+        module.exports = factory();
+    } else {
+        global.ReconnectingWebSocket = factory();
+    }
+})(this, function () {
+
+    if (!('WebSocket' in window)) {
+        return;
+    }
+
+    function ReconnectingWebSocket(url, protocols, options) {
+
+        // Default settings
+        var settings = {
+
+            /** Whether this instance should log debug messages. */
+            debug: false,
+
+            /** Whether or not the websocket should attempt to connect immediately upon instantiation. */
+            automaticOpen: true,
+
+            /** The number of milliseconds to delay before attempting to reconnect. */
+            reconnectInterval: 1000,
+            /** The maximum number of milliseconds to delay a reconnection attempt. */
+            maxReconnectInterval: 30000,
+            /** The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. */
+            reconnectDecay: 1.5,
+
+            /** The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. */
+            timeoutInterval: 2000,
+
+            /** The maximum number of reconnection attempts to make. Unlimited if null. */
+            maxReconnectAttempts: null,
+
+            /** The binary type, possible values 'blob' or 'arraybuffer', default 'blob'. */
+            binaryType: 'blob'
+        }
+        if (!options) {
+            options = {};
+        }
+
+        // Overwrite and define settings with options if they exist.
+        for (var key in settings) {
+            if (typeof options[key] !== 'undefined') {
+                this[key] = options[key];
+            } else {
+                this[key] = settings[key];
+            }
+        }
+
+        // These should be treated as read-only properties
+
+        /** The URL as resolved by the constructor. This is always an absolute URL. Read only. */
+        this.url = url;
+
+        /** The number of attempted reconnects since starting, or the last successful connection. Read only. */
+        this.reconnectAttempts = 0;
+
+        /**
+         * The current state of the connection.
+         * Can be one of: WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED
+         * Read only.
+         */
+        this.readyState = WebSocket.CONNECTING;
+
+        /**
+         * A string indicating the name of the sub-protocol the server selected; this will be one of
+         * the strings specified in the protocols parameter when creating the WebSocket object.
+         * Read only.
+         */
+        this.protocol = null;
+
+        // Private state variables
+
+        var self = this;
+        var ws;
+        var forcedClose = false;
+        var timedOut = false;
+        var eventTarget = document.createElement('div');
+
+        // Wire up "on*" properties as event handlers
+
+        eventTarget.addEventListener('open', function (event) {
+            self.onopen(event);
+        });
+        eventTarget.addEventListener('close', function (event) {
+            self.onclose(event);
+        });
+        eventTarget.addEventListener('connecting', function (event) {
+            self.onconnecting(event);
+        });
+        eventTarget.addEventListener('message', function (event) {
+            self.onmessage(event);
+        });
+        eventTarget.addEventListener('error', function (event) {
+            self.onerror(event);
+        });
+
+        // Expose the API required by EventTarget
+
+        this.addEventListener = eventTarget.addEventListener.bind(eventTarget);
+        this.removeEventListener = eventTarget.removeEventListener.bind(eventTarget);
+        this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget);
+
+        /**
+         * This function generates an event that is compatible with standard
+         * compliant browsers and IE9 - IE11
+         *
+         * This will prevent the error:
+         * Object doesn't support this action
+         *
+         * http://stackoverflow.com/questions/19345392/why-arent-my-parameters-getting-passed-through-to-a-dispatched-event/19345563#19345563
+         * @param s String The name that the event should use
+         * @param args Object an optional object that the event will use
+         */
+        function generateEvent(s, args) {
+            var evt = document.createEvent("CustomEvent");
+            evt.initCustomEvent(s, false, false, args);
+            return evt;
+        };
+
+        this.open = function (reconnectAttempt) {
+            ws = new WebSocket(self.url, protocols || []);
+            ws.binaryType = this.binaryType;
+
+            if (reconnectAttempt) {
+                if (this.maxReconnectAttempts && this.reconnectAttempts > this.maxReconnectAttempts) {
+                    return;
+                }
+            } else {
+                eventTarget.dispatchEvent(generateEvent('connecting'));
+                this.reconnectAttempts = 0;
+            }
+
+            if (self.debug || ReconnectingWebSocket.debugAll) {
+                console.debug('ReconnectingWebSocket', 'attempt-connect', self.url);
+            }
+
+            var localWs = ws;
+            var timeout = setTimeout(function () {
+                if (self.debug || ReconnectingWebSocket.debugAll) {
+                    console.debug('ReconnectingWebSocket', 'connection-timeout', self.url);
+                }
+                timedOut = true;
+                localWs.close();
+                timedOut = false;
+            }, self.timeoutInterval);
+
+            ws.onopen = function (event) {
+                clearTimeout(timeout);
+                if (self.debug || ReconnectingWebSocket.debugAll) {
+                    console.debug('ReconnectingWebSocket', 'onopen', self.url);
+                }
+                self.protocol = ws.protocol;
+                self.readyState = WebSocket.OPEN;
+                self.reconnectAttempts = 0;
+                var e = generateEvent('open');
+                e.isReconnect = reconnectAttempt;
+                reconnectAttempt = false;
+                eventTarget.dispatchEvent(e);
+            };
+
+            ws.onclose = function (event) {
+                clearTimeout(timeout);
+                ws = null;
+                if (forcedClose) {
+                    self.readyState = WebSocket.CLOSED;
+                    eventTarget.dispatchEvent(generateEvent('close'));
+                } else {
+                    self.readyState = WebSocket.CONNECTING;
+                    var e = generateEvent('connecting');
+                    e.code = event.code;
+                    e.reason = event.reason;
+                    e.wasClean = event.wasClean;
+                    eventTarget.dispatchEvent(e);
+                    if (!reconnectAttempt && !timedOut) {
+                        if (self.debug || ReconnectingWebSocket.debugAll) {
+                            console.debug('ReconnectingWebSocket', 'onclose', self.url);
+                        }
+                        eventTarget.dispatchEvent(generateEvent('close'));
+                    }
+
+                    var timeout = self.reconnectInterval * Math.pow(self.reconnectDecay, self.reconnectAttempts);
+                    setTimeout(function () {
+                        self.reconnectAttempts++;
+                        self.open(true);
+                    }, timeout > self.maxReconnectInterval ? self.maxReconnectInterval : timeout);
+                }
+            };
+            ws.onmessage = function (event) {
+                if (self.debug || ReconnectingWebSocket.debugAll) {
+                    console.debug('ReconnectingWebSocket', 'onmessage', self.url, event.data);
+                }
+                var e = generateEvent('message');
+                e.data = event.data;
+                eventTarget.dispatchEvent(e);
+            };
+            ws.onerror = function (event) {
+                if (self.debug || ReconnectingWebSocket.debugAll) {
+                    console.debug('ReconnectingWebSocket', 'onerror', self.url, event);
+                }
+                eventTarget.dispatchEvent(generateEvent('error'));
+            };
+        }
+
+        // Whether or not to create a websocket upon instantiation
+        if (this.automaticOpen == true) {
+            this.open(false);
+        }
+
+        /**
+         * Transmits data to the server over the WebSocket connection.
+         *
+         * @param data a text string, ArrayBuffer or Blob to send to the server.
+         */
+        this.send = function (data) {
+            if (ws) {
+                if (self.debug || ReconnectingWebSocket.debugAll) {
+                    console.debug('ReconnectingWebSocket', 'send', self.url, data);
+                }
+                return ws.send(data);
+            } else {
+                throw 'INVALID_STATE_ERR : Pausing to reconnect websocket';
+            }
+        };
+
+        /**
+         * Closes the WebSocket connection or connection attempt, if any.
+         * If the connection is already CLOSED, this method does nothing.
+         */
+        this.close = function (code, reason) {
+            // Default CLOSE_NORMAL code
+            if (typeof code == 'undefined') {
+                code = 1000;
+            }
+            forcedClose = true;
+            if (ws) {
+                ws.close(code, reason);
+            }
+        };
+
+        /**
+         * Additional public API method to refresh the connection if still open (close, re-open).
+         * For example, if the app suspects bad data / missed heart beats, it can try to refresh.
+         */
+        this.refresh = function () {
+            if (ws) {
+                ws.close();
+            }
+        };
+    }
+
+    /**
+     * An event listener to be called when the WebSocket connection's readyState changes to OPEN;
+     * this indicates that the connection is ready to send and receive data.
+     */
+    ReconnectingWebSocket.prototype.onopen = function (event) {
+    };
+    /** An event listener to be called when the WebSocket connection's readyState changes to CLOSED. */
+    ReconnectingWebSocket.prototype.onclose = function (event) {
+    };
+    /** An event listener to be called when a connection begins being attempted. */
+    ReconnectingWebSocket.prototype.onconnecting = function (event) {
+    };
+    /** An event listener to be called when a message is received from the server. */
+    ReconnectingWebSocket.prototype.onmessage = function (event) {
+    };
+    /** An event listener to be called when an error occurs. */
+    ReconnectingWebSocket.prototype.onerror = function (event) {
+    };
+
+    /**
+     * Whether all instances of ReconnectingWebSocket should log debug messages.
+     * Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true.
+     */
+    ReconnectingWebSocket.debugAll = false;
+
+    ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING;
+    ReconnectingWebSocket.OPEN = WebSocket.OPEN;
+    ReconnectingWebSocket.CLOSING = WebSocket.CLOSING;
+    ReconnectingWebSocket.CLOSED = WebSocket.CLOSED;
+
+    return ReconnectingWebSocket;
+});

+ 11 - 1
magic-editor/src/console/src/scripts/utils.js

@@ -84,4 +84,14 @@ const deepClone = (obj) => {
   }
   return o;
 }
-export {replaceURL, isVisible, formatJson, formatDate, paddingZero, download, requestGroup, deepClone}
+
+// 展示锚点对象
+const goToAnchor = (dom) => {
+  if (typeof dom === 'string') {
+    dom = document.querySelector(dom)
+  }
+  if (dom) {
+    dom.scrollIntoView(true)
+  }
+}
+export {replaceURL, isVisible, formatJson, formatDate, paddingZero, download, requestGroup, deepClone, goToAnchor}

+ 44 - 0
magic-editor/src/console/src/scripts/websocket.js

@@ -0,0 +1,44 @@
+import bus from "@/scripts/bus";
+import ReconnectingWebSocket from './reconnecting-websocket'
+function MagicWebSocket(url) {
+    this.listeners = {};
+    console.log(url)
+    this.socket = new ReconnectingWebSocket(url);
+    this.socket.onmessage = this.messageReceived;
+    bus.$on('message', (msgType, content) => {
+        if (content) {
+            this.socket.send(`${msgType},${content}`)
+        } else {
+            this.socket.send(msgType)
+        }
+    })
+}
+
+MagicWebSocket.prototype.on = function (msgType, callback) {
+    this.listeners[msgType] = this.listeners[msgType] || []
+    this.listeners[msgType].push(callback)
+}
+
+MagicWebSocket.prototype.messageReceived = function (e) {
+    let payload = e.data
+    let index = payload.indexOf(",")
+    let msgType = index === -1 ? payload : payload.substring(0, index)
+    let args = []
+    while (index > -1) {
+        payload = payload.substring(index + 1)
+        if (payload.startsWith('[') || payload.startsWith('{')) {
+            args.push(JSON.parse(payload))
+            break;
+        }
+        let newIndex = payload.indexOf(",", index + 1)
+        args.push(newIndex === -1 ? payload : payload.substring(index + 1, newIndex))
+        index = newIndex
+    }
+    console.log(msgType, args)
+    bus.$emit('ws_' + msgType, args)
+}
+MagicWebSocket.prototype.close = function () {
+    this.socket.close()
+}
+
+export default MagicWebSocket