├── docs ├── home.png ├── icon.png ├── history.png ├── welcome.png ├── collections.png ├── functional.png ├── networklog.png ├── performance.png ├── script-post.png ├── script-pre.png ├── workspaces.png ├── environments.png ├── functional_1.png ├── functional_2.png ├── history-events.png ├── history-timeline.png ├── script-snippets.png ├── collections-import.png ├── functional-results.png ├── performance-report.png ├── performance-trend.png ├── workspaces-gitcommit.png ├── performance-resultTree.png ├── performance-threadgroup-fixed.png ├── performance-threadgroup-spike.png ├── performance-threadgroup-rampup.png ├── performance-threadgroup-stairs.png └── FQA.MD ├── assets ├── linux │ └── EasyPostman.png ├── mac │ └── EasyPostman.icns ├── win │ └── EasyPostman.ico └── readme.md ├── .gitignore ├── src ├── main │ ├── resources │ │ └── icons │ │ │ ├── icon.png │ │ │ ├── nocheck.svg │ │ │ ├── git-commit.svg │ │ │ ├── git-pull.svg │ │ │ ├── arrow-down.svg │ │ │ ├── arrow-up.svg │ │ │ ├── git-push.svg │ │ │ ├── security.svg │ │ │ ├── hash.svg │ │ │ ├── ws-receive.svg │ │ │ ├── ws-send.svg │ │ │ ├── code.svg │ │ │ ├── words.svg │ │ │ ├── ws-connect.svg │ │ │ ├── wordsHovered.svg │ │ │ ├── wordsSelected.svg │ │ │ ├── detail.svg │ │ │ ├── ws-close.svg │ │ │ ├── start.svg │ │ │ ├── ws-info.svg │ │ │ ├── check.svg │ │ │ ├── connect.svg │ │ │ ├── binary.svg │ │ │ ├── connect-white.svg │ │ │ ├── warning.svg │ │ │ ├── plus.svg │ │ │ ├── performance.svg │ │ │ ├── cancel.svg │ │ │ ├── cancel-white.svg │ │ │ ├── edit.svg │ │ │ ├── fail.svg │ │ │ ├── pass.svg │ │ │ ├── save.svg │ │ │ ├── info.svg │ │ │ ├── computer.svg │ │ │ ├── switch.svg │ │ │ ├── file.svg │ │ │ ├── console.svg │ │ │ ├── download.svg │ │ │ ├── search.svg │ │ │ ├── send.svg │ │ │ ├── group.svg │ │ │ ├── http.svg │ │ │ ├── send-white.svg │ │ │ ├── workspace.svg │ │ │ ├── git.svg │ │ │ ├── sse.svg │ │ │ ├── refresh.svg │ │ │ ├── stop.svg │ │ │ ├── cookie.svg │ │ │ ├── functional.svg │ │ │ ├── time.svg │ │ │ ├── eye-open.svg │ │ │ ├── close.svg │ │ │ ├── idea-http.svg │ │ │ ├── har.svg │ │ │ ├── close-white.svg │ │ │ ├── copy.svg │ │ │ ├── local.svg │ │ │ ├── eye-close.svg │ │ │ ├── websocket.svg │ │ │ ├── expand.svg │ │ │ ├── history.svg │ │ │ ├── clear.svg │ │ │ ├── matchCase.svg │ │ │ ├── sidebar-toggle.svg │ │ │ ├── matchCaseHovered.svg │ │ │ ├── matchCaseSelected.svg │ │ │ ├── duplicate.svg │ │ │ ├── paste.svg │ │ │ ├── users.svg │ │ │ ├── user-group.svg │ │ │ ├── collapse.svg │ │ │ ├── root_group.svg │ │ │ ├── load.svg │ │ │ ├── curl.svg │ │ │ ├── help.svg │ │ │ ├── import.svg │ │ │ ├── easy.svg │ │ │ ├── export.svg │ │ │ ├── collections.svg │ │ │ ├── request.svg │ │ │ ├── csv.svg │ │ │ ├── tools.svg │ │ │ ├── format.svg │ │ │ ├── postman.svg │ │ │ └── environments.svg │ └── java │ │ └── com │ │ └── laker │ │ └── postman │ │ ├── model │ │ ├── WorkspaceType.java │ │ ├── GitAuthType.java │ │ ├── GitRepoSource.java │ │ ├── Category.java │ │ ├── CookieInfo.java │ │ ├── RemoteStatus.java │ │ ├── RedirectInfo.java │ │ ├── VariableSegment.java │ │ ├── AssertionResult.java │ │ ├── script │ │ │ ├── TestResult.java │ │ │ ├── Cookie.java │ │ │ └── TemporaryVariablesApi.java │ │ ├── HttpParam.java │ │ ├── HttpHeader.java │ │ ├── CurlRequest.java │ │ ├── EnvironmentVariable.java │ │ ├── Snippet.java │ │ ├── ConflictBlock.java │ │ ├── HttpFormUrlencoded.java │ │ ├── TabInfo.java │ │ ├── EnvironmentItem.java │ │ ├── RequestHistoryItem.java │ │ ├── RequestItemProtocolEnum.java │ │ ├── GitOperation.java │ │ ├── Workspace.java │ │ ├── UpdateCheckFrequency.java │ │ ├── MessageType.java │ │ ├── RequestResult.java │ │ ├── HttpResponse.java │ │ ├── IterationResult.java │ │ ├── BatchExecutionHistory.java │ │ ├── RequestGroup.java │ │ ├── NotificationPosition.java │ │ ├── HttpRequestItem.java │ │ ├── PreparedRequest.java │ │ ├── AuthType.java │ │ ├── UpdateInfo.java │ │ ├── HttpFormData.java │ │ ├── HttpEventInfo.java │ │ └── ClientCertificate.java │ │ ├── panel │ │ ├── performance │ │ │ ├── timer │ │ │ │ ├── TimerData.java │ │ │ │ └── TimerPropertyPanel.java │ │ │ ├── model │ │ │ │ ├── NodeType.java │ │ │ │ ├── RequestResult.java │ │ │ │ ├── ResultNodeInfo.java │ │ │ │ └── JMeterTreeNode.java │ │ │ ├── assertion │ │ │ │ └── AssertionData.java │ │ │ ├── component │ │ │ │ ├── ResultTreeCellRenderer.java │ │ │ │ └── JMeterTreeCellRenderer.java │ │ │ └── threadgroup │ │ │ │ └── ThreadGroupData.java │ │ ├── MainPanel.java │ │ ├── sidebar │ │ │ └── cookie │ │ │ │ └── CookieManagerDialog.java │ │ ├── functional │ │ │ └── table │ │ │ │ └── RunnerRowData.java │ │ ├── collections │ │ │ ├── RequestCollectionsPanel.java │ │ │ ├── right │ │ │ │ └── request │ │ │ │ │ └── sub │ │ │ │ │ └── SyntaxType.java │ │ │ └── left │ │ │ │ └── action │ │ │ │ └── TreeNodeCloner.java │ │ └── workspace │ │ │ └── components │ │ │ └── ProgressPanel.java │ │ ├── common │ │ ├── exception │ │ │ ├── GetInstanceException.java │ │ │ ├── WorkspaceCreateException.java │ │ │ └── CancelException.java │ │ ├── constants │ │ │ └── Icons.java │ │ ├── window │ │ │ └── SplashWindowInitializationException.java │ │ ├── component │ │ │ ├── button │ │ │ │ ├── CSVButton.java │ │ │ │ ├── StopButton.java │ │ │ │ ├── ClearButton.java │ │ │ │ ├── StartButton.java │ │ │ │ └── RefreshButton.java │ │ │ ├── tab │ │ │ │ └── PlusTabComponent.java │ │ │ ├── combobox │ │ │ │ ├── EnvironmentItemRenderer.java │ │ │ │ └── WorkspaceItemRenderer.java │ │ │ ├── table │ │ │ │ ├── EasyPostmanTextFieldCellEditor.java │ │ │ │ ├── EasyPostmanTextFieldCellRenderer.java │ │ │ │ └── TableUIConstants.java │ │ │ └── list │ │ │ │ └── EnvironmentListCellRenderer.java │ │ ├── SingletonBasePanel.java │ │ └── SingletonBaseMenuBar.java │ │ ├── ioc │ │ ├── PreDestroy.java │ │ ├── PostConstruct.java │ │ ├── Component.java │ │ ├── ObjectFactory.java │ │ ├── BeanException.java │ │ ├── DisposableBean.java │ │ ├── Autowired.java │ │ ├── InitializingBean.java │ │ ├── NoSuchBeanException.java │ │ ├── Scope.java │ │ ├── BeanCreationException.java │ │ ├── BeanDefinition.java │ │ └── BeanFactory.java │ │ ├── service │ │ ├── http │ │ │ ├── sse │ │ │ │ ├── SseResEventListener.java │ │ │ │ └── SseUiCallback.java │ │ │ ├── HttpSingleRequestExecutor.java │ │ │ ├── ssl │ │ │ │ ├── SSLException.java │ │ │ │ ├── CertificateErrorParser.java │ │ │ │ └── SSLValidationResult.java │ │ │ └── okhttp │ │ │ │ └── LogWebSocketListener.java │ │ ├── js │ │ │ ├── ScriptExecutionException.java │ │ │ ├── ScriptFragment.java │ │ │ └── ScriptExecutionResult.java │ │ ├── update │ │ │ ├── source │ │ │ │ ├── GiteeUpdateSource.java │ │ │ │ ├── GitHubUpdateSource.java │ │ │ │ └── UpdateSource.java │ │ │ ├── version │ │ │ │ └── VersionComparator.java │ │ │ ├── changelog │ │ │ │ └── ChangelogService.java │ │ │ └── asset │ │ │ │ └── AssetFinder.java │ │ ├── UpdateService.java │ │ ├── ExitService.java │ │ └── collections │ │ │ └── RequestsTabsService.java │ │ └── util │ │ ├── FontsUtil.java │ │ ├── TimeDisplayUtil.java │ │ ├── FileSizeDisplayUtil.java │ │ ├── JComponentUtils.java │ │ ├── JsonPathUtil.java │ │ └── XmlUtil.java └── test │ └── java │ └── com │ └── laker │ └── postman │ ├── ioc │ └── test │ │ ├── ServiceA.java │ │ └── ServiceB.java │ └── EditMenuDemo.java └── .github ├── ISSUE_TEMPLATE └── config.yml ├── workflows ├── sync-labels.yml └── codeql-analysis.yml ├── PULL_REQUEST_TEMPLATE.md └── dependabot.yml /docs/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/home.png -------------------------------------------------------------------------------- /docs/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/icon.png -------------------------------------------------------------------------------- /docs/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/history.png -------------------------------------------------------------------------------- /docs/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/welcome.png -------------------------------------------------------------------------------- /docs/collections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/collections.png -------------------------------------------------------------------------------- /docs/functional.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/functional.png -------------------------------------------------------------------------------- /docs/networklog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/networklog.png -------------------------------------------------------------------------------- /docs/performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/performance.png -------------------------------------------------------------------------------- /docs/script-post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/script-post.png -------------------------------------------------------------------------------- /docs/script-pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/script-pre.png -------------------------------------------------------------------------------- /docs/workspaces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/workspaces.png -------------------------------------------------------------------------------- /docs/environments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/environments.png -------------------------------------------------------------------------------- /docs/functional_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/functional_1.png -------------------------------------------------------------------------------- /docs/functional_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/functional_2.png -------------------------------------------------------------------------------- /docs/history-events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/history-events.png -------------------------------------------------------------------------------- /docs/history-timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/history-timeline.png -------------------------------------------------------------------------------- /docs/script-snippets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/script-snippets.png -------------------------------------------------------------------------------- /assets/linux/EasyPostman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/assets/linux/EasyPostman.png -------------------------------------------------------------------------------- /assets/mac/EasyPostman.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/assets/mac/EasyPostman.icns -------------------------------------------------------------------------------- /assets/win/EasyPostman.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/assets/win/EasyPostman.ico -------------------------------------------------------------------------------- /docs/collections-import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/collections-import.png -------------------------------------------------------------------------------- /docs/functional-results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/functional-results.png -------------------------------------------------------------------------------- /docs/performance-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/performance-report.png -------------------------------------------------------------------------------- /docs/performance-trend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/performance-trend.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dependency-reduced-pom.xml 2 | /target/ 3 | /dist/ 4 | /.idea/ 5 | /.vscode/ 6 | .DS_Store 7 | **/.DS_Store -------------------------------------------------------------------------------- /docs/workspaces-gitcommit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/workspaces-gitcommit.png -------------------------------------------------------------------------------- /docs/performance-resultTree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/performance-resultTree.png -------------------------------------------------------------------------------- /src/main/resources/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/src/main/resources/icons/icon.png -------------------------------------------------------------------------------- /docs/performance-threadgroup-fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/performance-threadgroup-fixed.png -------------------------------------------------------------------------------- /docs/performance-threadgroup-spike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/performance-threadgroup-spike.png -------------------------------------------------------------------------------- /docs/performance-threadgroup-rampup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/performance-threadgroup-rampup.png -------------------------------------------------------------------------------- /docs/performance-threadgroup-stairs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakernote/EasyPostman/HEAD/docs/performance-threadgroup-stairs.png -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/WorkspaceType.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | // 工作空间类型枚举 4 | public enum WorkspaceType { 5 | LOCAL, GIT 6 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/GitAuthType.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | // 认证类型枚举 4 | public enum GitAuthType { 5 | NONE, PASSWORD, TOKEN, SSH_KEY 6 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/performance/timer/TimerData.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.performance.timer; 2 | 3 | public class TimerData { 4 | public int delayMs = 1000; 5 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/GitRepoSource.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | // 枚举定义 4 | public enum GitRepoSource { 5 | INITIALIZED, // 本地初始化后推送到远程 6 | CLONED // 从远程仓库克隆 7 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/performance/model/NodeType.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.performance.model; 2 | 3 | // 节点类型定义 4 | public enum NodeType {ROOT, THREAD_GROUP, REQUEST, ASSERTION, TIMER} -------------------------------------------------------------------------------- /src/main/resources/icons/nocheck.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/icons/git-commit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/git-pull.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/arrow-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/arrow-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/git-push.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/security.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/hash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/ws-receive.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/ws-send.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/Category.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | public enum Category { 4 | PRE_SCRIPT, ASSERT, EXTRACT, LOCAL_VAR, ENV_VAR, ENCODE, 5 | ENCRYPT, STRING, ARRAY, JSON, DATE, REGEX, LOG, CONTROL, TOKEN, COOKIES, OTHER 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/icons/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/words.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/ws-connect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/wordsHovered.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/wordsSelected.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/detail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/ws-close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/start.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/exception/GetInstanceException.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.exception; 2 | 3 | public class GetInstanceException extends RuntimeException { 4 | public GetInstanceException(String message, Throwable cause) { 5 | super(message, cause); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/icons/ws-info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/exception/WorkspaceCreateException.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.exception; 2 | 3 | public class WorkspaceCreateException extends RuntimeException { 4 | public WorkspaceCreateException(String message, Throwable cause) { 5 | super(message, cause); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/icons/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/connect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/binary.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/connect-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/ioc/PreDestroy.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 标记一个方法在Bean销毁前执行 7 | * 该方法会在容器关闭时自动调用 8 | */ 9 | @Target(ElementType.METHOD) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface PreDestroy { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/performance/assertion/AssertionData.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.performance.assertion; 2 | 3 | public class AssertionData { 4 | public String type = "Response Code"; 5 | public String content = ""; 6 | public String operator = "="; 7 | public String value = "200"; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/icons/warning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/ioc/PostConstruct.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc; 2 | 3 | 4 | import java.lang.annotation.*; 5 | 6 | /* 标记一个方法在Bean初始化后执行 7 | * 该方法会在依赖注入完成后自动调用 8 | */ 9 | @Target(ElementType.METHOD) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface PostConstruct { 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/CookieInfo.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | public class CookieInfo { 4 | public String name; 5 | public String value; 6 | public String domain; 7 | public String path; 8 | public long expires; 9 | public boolean secure; 10 | public boolean httpOnly; 11 | } -------------------------------------------------------------------------------- /src/main/resources/icons/plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/performance.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/exception/CancelException.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.exception; 2 | 3 | public class CancelException extends RuntimeException { 4 | 5 | public CancelException() { 6 | super(); 7 | } 8 | 9 | public CancelException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/RemoteStatus.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | /** 4 | * 远程状态信息封装 5 | */ 6 | public class RemoteStatus { 7 | public boolean hasRemote = false; 8 | public boolean hasUpstream = false; 9 | public String remoteUrl; 10 | public String currentBranch; 11 | public String upstreamBranch; 12 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/http/sse/SseResEventListener.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.http.sse; 2 | 3 | import com.laker.postman.model.HttpResponse; 4 | import okhttp3.internal.sse.ServerSentEventReader; 5 | 6 | public interface SseResEventListener extends ServerSentEventReader.Callback { 7 | void onOpen(HttpResponse response); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/icons/cancel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/constants/Icons.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.constants; 2 | 3 | import javax.swing.*; 4 | import java.util.Objects; 5 | 6 | public class Icons { 7 | private Icons() { 8 | } 9 | 10 | public static final ImageIcon LOGO = new ImageIcon(Objects.requireNonNull(Icons.class.getResource("/icons/icon.png"))); 11 | } -------------------------------------------------------------------------------- /assets/readme.md: -------------------------------------------------------------------------------- 1 | - https://www.svgviewer.dev/ 2 | - https://icon-icons.com/ 3 | - https://anyconv.com/png-to-icns-converter/ 4 | - https://www.flaticon.com/ 5 | - https://www.svgrepo.com/ 6 | - https://freesvgicons.com/search?q=http 7 | - https://flowbite.com/icons/ 8 | - https://gitee.com/api/v5/swagger 9 | - https://www.remove.bg/zh 去除图片背景 10 | - https://www.photopea.com 调整大小 -------------------------------------------------------------------------------- /src/main/resources/icons/cancel-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/fail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/pass.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/save.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/RedirectInfo.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | // 重定向信息结构体 7 | public class RedirectInfo { 8 | public String url; 9 | public int statusCode; 10 | public Map> headers; 11 | public String location; 12 | public String responseBody; 13 | } -------------------------------------------------------------------------------- /src/main/resources/icons/info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/computer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/ioc/Component.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 标记一个类为组件,会被IOC容器管理 7 | */ 8 | @Target(ElementType.TYPE) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | @Documented 11 | public @interface Component { 12 | /** 13 | * Bean名称,默认为类名首字母小写 14 | */ 15 | String value() default ""; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/ioc/ObjectFactory.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc; 2 | 3 | /** 4 | * 对象工厂接口 5 | * 用于延迟创建对象,支持循环依赖解决 6 | */ 7 | @FunctionalInterface 8 | public interface ObjectFactory { 9 | 10 | /** 11 | * 获取对象实例 12 | * 13 | * @return 对象实例 14 | * @throws Exception 如果获取失败 15 | */ 16 | T getObject() throws Exception; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/main/resources/icons/switch.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/VariableSegment.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | public class VariableSegment { 4 | public final int start; 5 | public final int end; 6 | public final String name; 7 | 8 | public VariableSegment(int start, int end, String name) { 9 | this.start = start; 10 | this.end = end; 11 | this.name = name; 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/resources/icons/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/ioc/BeanException.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc; 2 | 3 | /** 4 | * IOC容器相关异常的基类 5 | */ 6 | public class BeanException extends RuntimeException { 7 | 8 | public BeanException(String message) { 9 | super(message); 10 | } 11 | 12 | public BeanException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/icons/console.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/AssertionResult.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public enum AssertionResult { 7 | PASS("✅"), 8 | 9 | FAIL("❌"), 10 | 11 | NO_TESTS("💨"); 12 | 13 | private final String displayValue; 14 | 15 | AssertionResult(String displayValue) { 16 | this.displayValue = displayValue; 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/ioc/DisposableBean.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc; 2 | 3 | /** 4 | * Bean销毁接口 5 | * 实现此接口的Bean会在容器关闭时自动调用destroy方法 6 | * 这是@PreDestroy注解的接口方式替代 7 | */ 8 | public interface DisposableBean { 9 | 10 | /** 11 | * Bean销毁前调用 12 | * 可以用于释放资源、关闭连接等清理工作 13 | * 14 | * @throws Exception 如果销毁过程中出现错误 15 | */ 16 | void destroy() throws Exception; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/main/resources/icons/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/send.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/group.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/http.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/send-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/workspace.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/ioc/Autowired.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 标记需要自动注入的字段 7 | */ 8 | @Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD}) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | @Documented 11 | public @interface Autowired { 12 | /** 13 | * 是否必须,如果为true且找不到对应的bean则抛出异常 14 | */ 15 | boolean required() default true; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/ioc/InitializingBean.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc; 2 | 3 | /** 4 | * Bean初始化接口 5 | * 实现此接口的Bean会在属性注入完成后自动调用afterPropertiesSet方法 6 | * 这是@PostConstruct注解的接口方式替代 7 | */ 8 | public interface InitializingBean { 9 | 10 | /** 11 | * Bean的所有属性被设置后调用 12 | * 可以用于初始化资源、验证配置等 13 | * 14 | * @throws Exception 如果初始化失败 15 | */ 16 | void afterPropertiesSet() throws Exception; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/icons/git.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/window/SplashWindowInitializationException.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.window; 2 | 3 | import java.io.Serial; 4 | 5 | public class SplashWindowInitializationException extends RuntimeException { 6 | @Serial 7 | private static final long serialVersionUID = 1L; 8 | 9 | public SplashWindowInitializationException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/resources/icons/sse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/http/sse/SseUiCallback.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.http.sse; 2 | 3 | import com.laker.postman.model.HttpResponse; 4 | 5 | // SSE UI回调接口 6 | public interface SseUiCallback { 7 | void onOpen(HttpResponse resp, String headersText); 8 | 9 | void onEvent(String id, String type, String data); 10 | 11 | void onClosed(HttpResponse resp); 12 | 13 | void onFailure(String errorMsg, HttpResponse resp); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/component/button/CSVButton.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.component.button; 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon; 4 | 5 | import javax.swing.*; 6 | 7 | /** 8 | * 通用开始按钮,带图标和统一样式。 9 | */ 10 | public class CSVButton extends JButton { 11 | public CSVButton() { 12 | super(" CSV "); 13 | setIcon(new FlatSVGIcon("icons/csv.svg", 20, 20)); 14 | setFocusable(false); // 去掉按钮的焦点边框 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/icons/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/script/TestResult.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model.script; 2 | 3 | public class TestResult { 4 | /** 5 | * 测试唯一标识 6 | */ 7 | public String id; 8 | public final String name; 9 | public final boolean passed; 10 | public final String message; 11 | 12 | public TestResult(String name, boolean passed, String message) { 13 | this.name = name; 14 | this.passed = passed; 15 | this.message = message; 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/resources/icons/cookie.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/performance/model/RequestResult.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.performance.model; 2 | 3 | // 用于统计每个请求的结束时间和成功状态 4 | public class RequestResult { 5 | public long endTime; 6 | public boolean success; 7 | public long responseTime; // 添加实际响应时间字段 8 | 9 | public RequestResult(long endTime, boolean success, long responseTime) { 10 | this.endTime = endTime; 11 | this.success = success; 12 | this.responseTime = responseTime; 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/resources/icons/functional.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/ioc/NoSuchBeanException.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc; 2 | 3 | /** 4 | * Bean未找到异常 5 | */ 6 | public class NoSuchBeanException extends BeanException { 7 | 8 | public NoSuchBeanException(String beanName) { 9 | super("No bean named '" + beanName + "' found in the container"); 10 | } 11 | 12 | public NoSuchBeanException(Class requiredType) { 13 | super("No bean of type '" + requiredType.getName() + "' found in the container"); 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/icons/time.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/idea-http.svg: -------------------------------------------------------------------------------- 1 | HTTP -------------------------------------------------------------------------------- /src/main/resources/icons/har.svg: -------------------------------------------------------------------------------- 1 | HAR 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/icons/close-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/HttpParam.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * HTTP Parameter model with enabled state 11 | */ 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class HttpParam implements Serializable { 16 | private static final long serialVersionUID = 1L; 17 | 18 | private boolean enabled = true; 19 | private String key = ""; 20 | private String value = ""; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/HttpHeader.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * HTTP Header model with enabled state 11 | */ 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class HttpHeader implements Serializable { 16 | private static final long serialVersionUID = 1L; 17 | 18 | private boolean enabled = true; 19 | private String key = ""; 20 | private String value = ""; 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/main/resources/icons/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💬 社区讨论区 | Community Discussions 4 | url: https://github.com/lakernote/easy-postman/discussions 5 | about: 使用问答、功能建议讨论、分享经验 | Q&A, feature discussions, and sharing experiences 6 | - name: 📖 中文文档 | Chinese Documentation 7 | url: https://github.com/lakernote/easy-postman/blob/master/README_zh.md 8 | about: 查看中文文档 | View Chinese documentation 9 | - name: 📖 English Documentation 10 | url: https://github.com/lakernote/easy-postman/blob/master/README.md 11 | about: View English documentation 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/component/button/StopButton.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.component.button; 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon; 4 | import com.laker.postman.util.I18nUtil; 5 | import com.laker.postman.util.MessageKeys; 6 | 7 | import javax.swing.*; 8 | 9 | /** 10 | * 通用停止按钮,带图标和统一样式。 11 | */ 12 | public class StopButton extends JButton { 13 | public StopButton() { 14 | super(I18nUtil.getMessage(MessageKeys.BUTTON_STOP)); 15 | setIcon(new FlatSVGIcon("icons/stop.svg", 20, 20)); 16 | setFocusable(false); // 去掉按钮的焦点边框 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/icons/local.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/component/button/ClearButton.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.component.button; 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon; 4 | import com.laker.postman.util.I18nUtil; 5 | import com.laker.postman.util.MessageKeys; 6 | 7 | import javax.swing.*; 8 | 9 | /** 10 | * 通用开始按钮,带图标和统一样式。 11 | */ 12 | public class ClearButton extends JButton { 13 | public ClearButton() { 14 | super(I18nUtil.getMessage(MessageKeys.BUTTON_CLEAR)); 15 | setIcon(new FlatSVGIcon("icons/clear.svg", 20, 20)); 16 | setFocusable(false); // 去掉按钮的焦点边框 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/component/button/StartButton.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.component.button; 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon; 4 | import com.laker.postman.util.I18nUtil; 5 | import com.laker.postman.util.MessageKeys; 6 | 7 | import javax.swing.*; 8 | 9 | /** 10 | * 通用开始按钮,带图标和统一样式。 11 | */ 12 | public class StartButton extends JButton { 13 | public StartButton() { 14 | super(I18nUtil.getMessage(MessageKeys.BUTTON_START)); 15 | setIcon(new FlatSVGIcon("icons/start.svg", 20, 20)); 16 | setFocusable(false); // 去掉按钮的焦点边框 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/icons/eye-close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/component/button/RefreshButton.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.component.button; 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon; 4 | import com.laker.postman.util.I18nUtil; 5 | import com.laker.postman.util.MessageKeys; 6 | 7 | import javax.swing.*; 8 | 9 | /** 10 | * 通用刷新按钮,带图标和统一样式。 11 | */ 12 | public class RefreshButton extends JButton { 13 | public RefreshButton() { 14 | super(I18nUtil.getMessage(MessageKeys.BUTTON_REFRESH)); 15 | setIcon(new FlatSVGIcon("icons/refresh.svg", 20, 20)); 16 | setFocusable(false); // 去掉按钮的焦点边框 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/icons/websocket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/CurlRequest.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * 临时数据结构,用于解析 curl 命令 7 | */ 8 | public class CurlRequest { 9 | public String url; 10 | public String method; 11 | public List headersList; 12 | public String body; 13 | public List paramsList; 14 | // 用于存储解析出的表单数据 (multipart/form-data) 15 | public List formDataList; 16 | // 用于存储 application/x-www-form-urlencoded 类型的数据 17 | public List urlencodedList; 18 | public boolean followRedirects = false; // 是否跟随重定向 19 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/EnvironmentVariable.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * Environment Variable model with enabled state 11 | * 用于环境变量数据,支持启用/禁用状态 12 | */ 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class EnvironmentVariable implements Serializable { 17 | private static final long serialVersionUID = 1L; 18 | 19 | private boolean enabled = true; 20 | private String key = ""; 21 | private String value = ""; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/main/resources/icons/expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/resources/icons/history.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/ioc/Scope.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 指定Bean的作用域 7 | * 默认为单例(singleton) 8 | */ 9 | @Target(ElementType.TYPE) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface Scope { 13 | 14 | /** 15 | * Bean的作用域 16 | * - "singleton": 单例模式(默认) 17 | * - "prototype": 原型模式(每次获取都创建新实例) 18 | */ 19 | String value() default "singleton"; 20 | 21 | /** 22 | * 常量:单例 23 | */ 24 | String SINGLETON = "singleton"; 25 | 26 | /** 27 | * 常量:原型 28 | */ 29 | String PROTOTYPE = "prototype"; 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/test/java/com/laker/postman/ioc/test/ServiceA.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc.test; 2 | 3 | import com.laker.postman.ioc.Autowired; 4 | import com.laker.postman.ioc.Component; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import lombok.ToString; 8 | 9 | /** 10 | * 测试循环依赖 - ServiceA 11 | */ 12 | @Component 13 | @Getter 14 | @Setter 15 | @ToString(exclude = "serviceB") // 排除循环引用字段,避免 toString 栈溢出 16 | public class ServiceA { 17 | 18 | @Autowired 19 | private ServiceB serviceB; 20 | 21 | public void doSomething() { 22 | System.out.println("ServiceA doing something, has ServiceB: " + (serviceB != null)); 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/test/java/com/laker/postman/ioc/test/ServiceB.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc.test; 2 | 3 | import com.laker.postman.ioc.Autowired; 4 | import com.laker.postman.ioc.Component; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import lombok.ToString; 8 | 9 | /** 10 | * 测试循环依赖 - ServiceB 11 | */ 12 | @Component 13 | @Getter 14 | @Setter 15 | @ToString(exclude = "serviceA") // 排除循环引用字段,避免 toString 栈溢出 16 | public class ServiceB { 17 | 18 | @Autowired 19 | private ServiceA serviceA; 20 | 21 | public void doSomething() { 22 | System.out.println("ServiceB doing something, has ServiceA: " + (serviceA != null)); 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/main/resources/icons/matchCase.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/sidebar-toggle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/matchCaseHovered.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/matchCaseSelected.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/duplicate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/Snippet.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import com.laker.postman.util.I18nUtil; 4 | 5 | /** 6 | * 代码片段数据结构 7 | */ 8 | public class Snippet { 9 | public String title; 10 | public String code; 11 | public String desc; 12 | public SnippetType type; 13 | 14 | public Snippet(SnippetType type) { 15 | this.type = type; 16 | this.title = I18nUtil.getMessage(type.titleKey); 17 | this.desc = I18nUtil.getMessage(type.descKey); 18 | this.code = type.code; 19 | } 20 | 21 | public String toString() { 22 | return title + (desc != null && !desc.isEmpty() ? (" - " + desc) : ""); 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/resources/icons/paste.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/ioc/BeanCreationException.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc; 2 | 3 | /** 4 | * Bean创建失败异常 5 | */ 6 | public class BeanCreationException extends BeanException { 7 | 8 | public BeanCreationException(String beanName, String message) { 9 | super("Failed to create bean '" + beanName + "': " + message); 10 | } 11 | 12 | public BeanCreationException(String beanName, Throwable cause) { 13 | super("Failed to create bean '" + beanName + "'", cause); 14 | } 15 | 16 | public BeanCreationException(String beanName, String message, Throwable cause) { 17 | super("Failed to create bean '" + beanName + "': " + message, cause); 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/ConflictBlock.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.List; 6 | 7 | @Getter 8 | public class ConflictBlock { 9 | public int begin; 10 | public int end; 11 | public List baseLines; 12 | public List localLines; 13 | public List remoteLines; 14 | 15 | public ConflictBlock(int begin, int end, List baseLines, List localLines, List remoteLines) { 16 | this.begin = begin; 17 | this.end = end; 18 | this.baseLines = baseLines; 19 | this.localLines = localLines; 20 | this.remoteLines = remoteLines; 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/resources/icons/users.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/HttpFormUrlencoded.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serial; 8 | import java.io.Serializable; 9 | 10 | /** 11 | * HTTP Form Urlencoded model with enabled state 12 | * 用于 application/x-www-form-urlencoded 类型的表单数据 13 | */ 14 | @Data 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class HttpFormUrlencoded implements Serializable { 18 | @Serial 19 | private static final long serialVersionUID = 1L; 20 | 21 | private boolean enabled = true; 22 | private String key = ""; 23 | private String value = ""; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/js/ScriptExecutionException.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.js; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * 脚本执行异常 7 | */ 8 | @Getter 9 | public class ScriptExecutionException extends RuntimeException { 10 | private final ScriptExecutionContext.ScriptType scriptType; 11 | 12 | public ScriptExecutionException(String message, Throwable cause) { 13 | super(message, cause); 14 | this.scriptType = null; 15 | } 16 | 17 | public ScriptExecutionException(String message, Throwable cause, ScriptExecutionContext.ScriptType scriptType) { 18 | super(message, cause); 19 | this.scriptType = scriptType; 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /src/main/resources/icons/user-group.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/collapse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/resources/icons/root_group.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/TabInfo.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import javax.swing.*; 4 | import java.util.function.Supplier; 5 | 6 | // Tab元数据结构,便于维护和扩展 7 | public class TabInfo { 8 | public String title; 9 | public Icon icon; 10 | public Supplier panelSupplier; // 用于懒加载面板 11 | public JPanel panel; 12 | 13 | public TabInfo(String title, Icon icon, Supplier panelSupplier) { 14 | this.title = title; 15 | this.icon = icon; 16 | this.panelSupplier = panelSupplier; 17 | } 18 | 19 | public JPanel getPanel() { // 懒加载面板 20 | if (panel == null) { 21 | panel = panelSupplier.get(); 22 | } 23 | return panel; 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/resources/icons/load.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/curl.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/help.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/import.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/easy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/icons/export.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/ioc/BeanDefinition.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Bean定义,描述一个Bean的元数据 7 | */ 8 | @Data 9 | public class BeanDefinition { 10 | /** 11 | * Bean名称 12 | */ 13 | private String name; 14 | 15 | /** 16 | * Bean的Class类型 17 | */ 18 | private Class beanClass; 19 | 20 | /** 21 | * 是否单例(默认为单例) 22 | */ 23 | private boolean singleton = true; 24 | 25 | 26 | public BeanDefinition(String name, Class beanClass) { 27 | this.name = name; 28 | this.beanClass = beanClass; 29 | } 30 | 31 | public BeanDefinition(String name, Class beanClass, boolean singleton) { 32 | this.name = name; 33 | this.beanClass = beanClass; 34 | this.singleton = singleton; 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/MainPanel.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel; 2 | 3 | import com.laker.postman.common.SingletonFactory; 4 | import com.laker.postman.common.SingletonBasePanel; 5 | import com.laker.postman.panel.sidebar.SidebarTabPanel; 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | import java.awt.*; 9 | 10 | /** 11 | * 包含了左侧的标签页面板和右侧的请求编辑面板。 12 | * 左侧标签页面板包含了集合、环境变量、压测三个标签页, 13 | */ 14 | @Slf4j 15 | public class MainPanel extends SingletonBasePanel { 16 | 17 | @Override 18 | protected void initUI() { 19 | setLayout(new BorderLayout()); // 设置布局为 BorderLayout 20 | // 中间SidebarTabPanel + 底部控制台面板 21 | add(SingletonFactory.getInstance(SidebarTabPanel.class), BorderLayout.CENTER); 22 | } 23 | 24 | @Override 25 | protected void registerListeners() { 26 | // no-op 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/component/tab/PlusTabComponent.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.component.tab; 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | /** 9 | * PlusTabComponent 用于显示一个加号图标的标签组件 10 | */ 11 | public class PlusTabComponent extends JPanel { 12 | public PlusTabComponent() { 13 | setOpaque(false); // 设置为透明背景 14 | setFocusable(false); // 不可获取焦点 15 | setLayout(new BorderLayout()); // 使用 BorderLayout 布局 16 | 17 | JLabel plusLabel = new JLabel(); 18 | plusLabel.setIcon(new FlatSVGIcon("icons/plus.svg", 20, 20)); // 使用SVG图标 19 | plusLabel.setHorizontalAlignment(SwingConstants.CENTER); 20 | plusLabel.setVerticalAlignment(SwingConstants.CENTER); 21 | add(plusLabel, BorderLayout.CENTER); 22 | } 23 | } -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | # 工作流名称:同步标签 2 | name: Sync Labels 3 | 4 | # 触发条件 5 | on: 6 | # 当推送到 main 分支,且 .github/labels.yml 文件发生变化时触发 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - '.github/labels.yml' 12 | # 允许手动触发工作流 13 | workflow_dispatch: 14 | 15 | jobs: 16 | sync-labels: 17 | # 运行环境:使用最新版本的 Ubuntu 18 | runs-on: ubuntu-latest 19 | # 权限设置:允许写入 issues(包括标签管理) 20 | permissions: 21 | issues: write 22 | 23 | steps: 24 | # 第一步:检出代码仓库 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | # 第二步:同步标签配置到 GitHub 仓库 29 | - name: Sync labels 30 | uses: EndBug/label-sync@v2 31 | with: 32 | # 标签配置文件路径 33 | config-file: .github/labels.yml 34 | # 使用 GitHub 提供的访问令牌进行身份验证 35 | token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/EnvironmentItem.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.Objects; 6 | 7 | /** 8 | * 环境项封装类,用于在下拉框中显示 9 | */ 10 | @Getter 11 | public class EnvironmentItem { 12 | private final Environment environment; 13 | 14 | public EnvironmentItem(Environment environment) { 15 | this.environment = environment; 16 | } 17 | 18 | @Override 19 | public boolean equals(Object o) { 20 | if (!(o instanceof EnvironmentItem that)) return false; 21 | return Objects.equals(getEnvironment().getId(), that.getEnvironment().getId()); 22 | } 23 | 24 | @Override 25 | public int hashCode() { 26 | return Objects.hashCode(environment.getId()); 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return environment.getName(); 32 | } 33 | 34 | 35 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/component/combobox/EnvironmentItemRenderer.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.component.combobox; 2 | 3 | import com.laker.postman.model.Environment; 4 | import com.laker.postman.model.EnvironmentItem; 5 | 6 | import javax.swing.*; 7 | import java.awt.*; 8 | 9 | /** 10 | * 环境下拉框的渲染器 11 | */ 12 | public class EnvironmentItemRenderer extends DefaultListCellRenderer { 13 | @Override 14 | public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { 15 | // 使用默认渲染器获取基础组件 16 | JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); 17 | if (value instanceof EnvironmentItem item) { 18 | Environment env = item.getEnvironment(); 19 | label.setText(env.getName()); 20 | } 21 | return label; 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/RequestHistoryItem.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | /** 4 | * 请求历史项,包含请求和响应的简要信息 5 | */ 6 | public class RequestHistoryItem { 7 | public final String method; 8 | public final String url; 9 | public final int responseCode; // 响应状态码 10 | public final long requestTime; // 请求时间戳 11 | public PreparedRequest request; // 原始请求对象 12 | public HttpResponse response; // 响应对象 13 | 14 | public RequestHistoryItem(PreparedRequest request, HttpResponse response, long requestTime) { 15 | this.method = request.method; 16 | this.url = request.url; 17 | this.responseCode = response.code; 18 | this.request = request; 19 | this.response = response; 20 | this.requestTime = requestTime; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return String.format("[%s] %s", method, url); 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/component/combobox/WorkspaceItemRenderer.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.component.combobox; 2 | 3 | import com.laker.postman.model.Workspace; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | /** 9 | * 工作区下拉框的渲染器 10 | */ 11 | public class WorkspaceItemRenderer extends DefaultListCellRenderer { 12 | @Override 13 | public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { 14 | JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); 15 | 16 | if (value instanceof Workspace workspace) { 17 | label.setText(workspace.getName()); 18 | // 如果名称太长,使用tooltip显示完整名称 19 | if (workspace.getName().length() > 15) { 20 | label.setToolTipText(workspace.getName()); 21 | } 22 | } 23 | return label; 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/resources/icons/collections.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/request.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/component/table/EasyPostmanTextFieldCellEditor.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.component.table; 2 | 3 | import com.laker.postman.common.component.EasyPostmanTextField; 4 | 5 | import javax.swing.*; 6 | import javax.swing.table.TableCellEditor; 7 | import java.awt.*; 8 | 9 | public class EasyPostmanTextFieldCellEditor extends AbstractCellEditor implements TableCellEditor { 10 | private final EasyPostmanTextField textField = new EasyPostmanTextField(1); 11 | 12 | public EasyPostmanTextFieldCellEditor() { 13 | textField.setBorder(null); 14 | } 15 | 16 | @Override 17 | public Object getCellEditorValue() { 18 | return textField.getText(); 19 | } 20 | 21 | @Override 22 | public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 23 | textField.setText(value == null ? "" : value.toString()); 24 | return textField; 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/util/FontsUtil.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | /** 9 | * 字体工具类,提供系统默认字体,保留完整的字体降级链以支持 emoji 等特殊字符 10 | */ 11 | @UtilityClass 12 | public class FontsUtil { 13 | 14 | /** 15 | * 获取默认字体,从 UIManager 派生以保留降级链,支持 emoji 等特殊字符 16 | * 17 | * @param style 字体样式 (Font.PLAIN, Font.BOLD, Font.ITALIC) 18 | * @param size 字体大小 19 | * @return Font 对象,从系统默认字体派生 20 | */ 21 | public static Font getDefaultFont(int style, int size) { 22 | // 从 UIManager 获取默认字体,使用 deriveFont 派生,保留降级链 23 | Font baseFont = UIManager.getFont("Label.font"); 24 | if (baseFont == null) { 25 | // 如果 UIManager 中没有,使用系统默认字体 26 | baseFont = new JLabel().getFont(); 27 | } 28 | // 使用 deriveFont 派生新字体,保留原字体的所有属性和降级链 29 | return baseFont.deriveFont(style, (float) size); 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/RequestItemProtocolEnum.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | 4 | import com.formdev.flatlaf.extras.FlatSVGIcon; 5 | import lombok.Getter; 6 | 7 | import javax.swing.*; 8 | 9 | @Getter 10 | public enum RequestItemProtocolEnum { 11 | HTTP("HTTP", new FlatSVGIcon("icons/http.svg", 24, 24)), 12 | WEBSOCKET("WebSocket", new FlatSVGIcon("icons/websocket.svg", 24, 24)), 13 | SSE("SSE", new FlatSVGIcon("icons/sse.svg", 24, 24)); 14 | 15 | private final String protocol; 16 | private final Icon icon; 17 | 18 | RequestItemProtocolEnum(String protocol, Icon icon) { 19 | this.protocol = protocol; 20 | this.icon = icon; 21 | } 22 | 23 | public boolean isWebSocketProtocol() { 24 | return this == WEBSOCKET; 25 | } 26 | 27 | public boolean isHttpProtocol() { 28 | return this == HTTP; 29 | } 30 | 31 | public boolean isSseProtocol() { 32 | return this == SSE; 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/GitOperation.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import com.laker.postman.common.constants.ModernColors; 4 | import com.laker.postman.util.I18nUtil; 5 | import com.laker.postman.util.MessageKeys; 6 | import lombok.Getter; 7 | 8 | import java.awt.*; 9 | 10 | @Getter 11 | public enum GitOperation { 12 | COMMIT(I18nUtil.getMessage(MessageKeys.GIT_OPERATION_COMMIT), "icons/git-commit.svg", ModernColors.GIT_COMMIT), 13 | PUSH(I18nUtil.getMessage(MessageKeys.GIT_OPERATION_PUSH), "icons/git-push.svg", ModernColors.GIT_PUSH), 14 | PULL(I18nUtil.getMessage(MessageKeys.GIT_OPERATION_PULL), "icons/git-pull.svg", ModernColors.GIT_PULL); 15 | 16 | private final String displayName; 17 | private final String iconName; 18 | private final Color color; 19 | 20 | GitOperation(String displayName, String iconName, Color color) { 21 | this.displayName = displayName; 22 | this.iconName = iconName; 23 | this.color = color; 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/resources/icons/csv.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/util/TimeDisplayUtil.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | /** 6 | * 时间显示工具类 7 | * 提供将毫秒时间转换为更友好的显示格式 8 | */ 9 | @UtilityClass 10 | public class TimeDisplayUtil { 11 | 12 | /** 13 | * 将毫秒时间转换为更友好的显示格式 14 | * - 小于1秒显示为 "xx ms" 15 | * - 小于1分钟显示为 "x.xx s" 16 | * - 大于等于1分钟显示为 "xm xs" 17 | * 18 | * @param elapsedTimeMillis 毫秒时间 19 | * @return 格式化后的时间字符串 20 | */ 21 | public static String formatElapsedTime(long elapsedTimeMillis) { 22 | if (elapsedTimeMillis < 1000) { 23 | return elapsedTimeMillis + " ms"; 24 | } else if (elapsedTimeMillis < 60000) { 25 | return String.format("%.2f s", elapsedTimeMillis / 1000.0); 26 | } else { 27 | int minutes = (int) (elapsedTimeMillis / 60000); 28 | int seconds = (int) ((elapsedTimeMillis % 60000) / 1000); 29 | return minutes + "m " + seconds + "s"; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/update/source/GiteeUpdateSource.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.update.source; 2 | 3 | /** 4 | * Gitee 更新源实现 5 | */ 6 | public class GiteeUpdateSource extends AbstractUpdateSource { 7 | 8 | private static final String API_URL = "https://gitee.com/api/v5/repos/lakernote/easy-postman/releases/latest"; 9 | private static final String ALL_RELEASES_API_URL = "https://gitee.com/api/v5/repos/lakernote/easy-postman/releases"; 10 | private static final String WEB_URL = "https://gitee.com/lakernote/easy-postman/releases"; 11 | private static final String NAME = "Gitee"; 12 | 13 | @Override 14 | public String getName() { 15 | return NAME; 16 | } 17 | 18 | @Override 19 | public String getApiUrl() { 20 | return API_URL; 21 | } 22 | 23 | @Override 24 | public String getAllReleasesApiUrl() { 25 | return ALL_RELEASES_API_URL; 26 | } 27 | 28 | @Override 29 | public String getWebUrl() { 30 | return WEB_URL; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/update/source/GitHubUpdateSource.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.update.source; 2 | 3 | /** 4 | * GitHub 更新源实现 5 | */ 6 | public class GitHubUpdateSource extends AbstractUpdateSource { 7 | 8 | private static final String API_URL = "https://api.github.com/repos/lakernote/easy-postman/releases/latest"; 9 | private static final String ALL_RELEASES_API_URL = "https://api.github.com/repos/lakernote/easy-postman/releases"; 10 | private static final String WEB_URL = "https://github.com/lakernote/easy-postman/releases"; 11 | private static final String NAME = "GitHub"; 12 | 13 | @Override 14 | public String getName() { 15 | return NAME; 16 | } 17 | 18 | @Override 19 | public String getApiUrl() { 20 | return API_URL; 21 | } 22 | 23 | @Override 24 | public String getAllReleasesApiUrl() { 25 | return ALL_RELEASES_API_URL; 26 | } 27 | 28 | @Override 29 | public String getWebUrl() { 30 | return WEB_URL; 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/Workspace.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import lombok.Data; 4 | 5 | // Workspace模型类 6 | @Data 7 | public class Workspace { 8 | private String id; // UUID 9 | private String name; // 工作空间名称 10 | private String description; // 工作空间描述 11 | private WorkspaceType type; // LOCAL/GIT 12 | private GitRepoSource gitRepoSource; // 仓库来源:INITIALIZED(本地初始化)/ CLONED(远程克隆) 13 | private String path; // 本地路径 14 | private String gitRemoteUrl; // Git远程仓库地址 15 | private String currentBranch; // 当前分支名称 16 | private String remoteBranch; // 当前跟踪的远程分支名称 17 | private String lastCommitId; // 最后提交ID 18 | private long createdAt; 19 | private long updatedAt; 20 | private String gitUsername; // Git 用户名 21 | private String gitPassword; // Git 密码(加密存储) 22 | private String gitToken; // Git 令牌(替代密码,优先使用) 23 | private GitAuthType gitAuthType; // 认证类型 24 | private String sshPrivateKeyPath; // SSH 私钥文件路径 25 | private String sshPassphrase; // SSH 私钥密码(可选) 26 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/sidebar/cookie/CookieManagerDialog.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.sidebar.cookie; 2 | 3 | import com.laker.postman.panel.collections.right.request.sub.CookieTablePanel; 4 | import com.laker.postman.util.I18nUtil; 5 | import com.laker.postman.util.MessageKeys; 6 | 7 | import javax.swing.*; 8 | import java.awt.*; 9 | 10 | /** 11 | * Cookie 管理器对话框 12 | * 全局 Cookie 管理界面,显示和管理所有域名的 Cookie 13 | */ 14 | public class CookieManagerDialog extends JDialog { 15 | 16 | public CookieManagerDialog(Window owner) { 17 | super(owner, I18nUtil.getMessage(MessageKeys.COOKIES_MANAGER_TITLE), ModalityType.MODELESS); 18 | initUI(); 19 | } 20 | 21 | private void initUI() { 22 | setLayout(new BorderLayout()); 23 | 24 | // 使用现有的 CookieTablePanel 25 | CookieTablePanel cookiePanel = new CookieTablePanel(); 26 | add(cookiePanel, BorderLayout.CENTER); 27 | 28 | // 设置对话框属性 29 | setSize(900, 500); 30 | setLocationRelativeTo(getOwner()); 31 | setDefaultCloseOperation(DISPOSE_ON_CLOSE); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/UpdateCheckFrequency.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | /** 4 | * 更新检查频率枚举 5 | */ 6 | public enum UpdateCheckFrequency { 7 | STARTUP("startup", 0), // 每次启动 8 | DAILY("daily", 1), // 每日 9 | WEEKLY("weekly", 7), // 每周 10 | MONTHLY("monthly", 30); // 每月 11 | 12 | private final String code; 13 | private final int days; 14 | 15 | UpdateCheckFrequency(String code, int days) { 16 | this.code = code; 17 | this.days = days; 18 | } 19 | 20 | public String getCode() { 21 | return code; 22 | } 23 | 24 | public int getDays() { 25 | return days; 26 | } 27 | 28 | /** 29 | * 是否每次启动都检查 30 | */ 31 | public boolean isAlwaysCheck() { 32 | return this == STARTUP; 33 | } 34 | 35 | public static UpdateCheckFrequency fromCode(String code) { 36 | for (UpdateCheckFrequency frequency : values()) { 37 | if (frequency.code.equals(code)) { 38 | return frequency; 39 | } 40 | } 41 | return DAILY; // 默认每日 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/resources/icons/tools.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/component/table/EasyPostmanTextFieldCellRenderer.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.component.table; 2 | 3 | import com.laker.postman.common.component.EasyPostmanTextField; 4 | 5 | import javax.swing.*; 6 | import javax.swing.table.TableCellRenderer; 7 | import java.awt.*; 8 | 9 | public class EasyPostmanTextFieldCellRenderer extends EasyPostmanTextField implements TableCellRenderer { 10 | public EasyPostmanTextFieldCellRenderer() { 11 | super(1); 12 | setBorder(null); 13 | setOpaque(true); 14 | } 15 | 16 | @Override 17 | public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 18 | setText(value == null ? "" : value.toString()); 19 | if (value == null || value.toString().isEmpty()) { 20 | setBackground(Color.WHITE); 21 | } else if (isSelected) { 22 | setBackground(table.getSelectionBackground()); 23 | } else { 24 | setBackground(table.getBackground()); 25 | } 26 | return this; 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/performance/component/ResultTreeCellRenderer.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.performance.component; 2 | 3 | import com.laker.postman.panel.performance.model.ResultNodeInfo; 4 | 5 | import javax.swing.*; 6 | import javax.swing.tree.DefaultMutableTreeNode; 7 | import javax.swing.tree.DefaultTreeCellRenderer; 8 | import java.awt.*; 9 | 10 | // 结果树渲染 11 | public class ResultTreeCellRenderer extends DefaultTreeCellRenderer { 12 | @Override 13 | public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { 14 | JLabel label = (JLabel) super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); 15 | Object userObj = ((DefaultMutableTreeNode) value).getUserObject(); 16 | if (userObj instanceof ResultNodeInfo info) { 17 | if (info.success) { 18 | label.setForeground(new Color(0, 128, 0)); 19 | } else { 20 | label.setForeground(Color.RED); 21 | } 22 | } 23 | return label; 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/performance/model/ResultNodeInfo.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.performance.model; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.laker.postman.model.HttpResponse; 5 | import com.laker.postman.model.PreparedRequest; 6 | import com.laker.postman.model.script.TestResult; 7 | 8 | import java.util.List; 9 | 10 | // 结果树节点信息 11 | public class ResultNodeInfo { 12 | public String name; 13 | public boolean success; 14 | public String errorMsg; 15 | public PreparedRequest req; 16 | public HttpResponse resp; 17 | public List testResults; 18 | 19 | public ResultNodeInfo(String name, boolean success, String errorMsg, PreparedRequest req, HttpResponse resp, List testResults) { 20 | this.name = name; 21 | this.success = success; 22 | this.errorMsg = errorMsg; 23 | this.req = req; 24 | this.resp = resp; 25 | this.testResults = testResults; 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | return name + (StrUtil.isBlank(errorMsg) ? "" : " - 错误: " + errorMsg); 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/UpdateService.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service; 2 | 3 | import com.laker.postman.ioc.Autowired; 4 | import com.laker.postman.ioc.Component; 5 | import com.laker.postman.ioc.PreDestroy; 6 | import com.laker.postman.service.update.AutoUpdateManager; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | /** 10 | * 版本更新服务 11 | */ 12 | @Slf4j 13 | @Component 14 | public class UpdateService { 15 | 16 | @Autowired 17 | private AutoUpdateManager autoUpdateManager; 18 | 19 | /** 20 | * 启动时异步检查更新 21 | */ 22 | public void checkUpdateOnStartup() { 23 | // 启动后台更新检查 24 | autoUpdateManager.startBackgroundCheck(); 25 | } 26 | 27 | /** 28 | * 手动检查更新(用于菜单调用) 29 | */ 30 | public void checkUpdateManually() { 31 | autoUpdateManager.checkForUpdateManually() 32 | .thenAccept(updateInfo -> 33 | autoUpdateManager.handleUpdateCheckResult(updateInfo, true)) 34 | .exceptionally(throwable -> { 35 | log.error("Manual update check failed", throwable); 36 | return null; 37 | }); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/functional/table/RunnerRowData.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.functional.table; 2 | 3 | import com.laker.postman.model.*; 4 | import com.laker.postman.model.script.TestResult; 5 | 6 | import java.util.List; 7 | 8 | public class RunnerRowData { 9 | public boolean selected; 10 | public String name; 11 | public String url; 12 | public String method; 13 | public long cost; 14 | public String status; 15 | public AssertionResult assertion; 16 | public HttpRequestItem requestItem; 17 | public PreparedRequest preparedRequest; 18 | public HttpResponse response; 19 | public List testResults; 20 | 21 | public RunnerRowData(HttpRequestItem item, PreparedRequest prepared) { 22 | this.selected = true; 23 | this.name = item.getName(); 24 | this.url = item.getUrl(); 25 | this.method = item.getMethod(); 26 | this.cost = 0; 27 | this.status = ""; 28 | this.assertion = AssertionResult.NO_TESTS; 29 | this.requestItem = item; 30 | this.preparedRequest = prepared; 31 | this.response = null; 32 | this.testResults = null; 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/ioc/BeanFactory.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.ioc; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | /** 6 | * IOC容器工具类,提供便捷的访问方法 7 | */ 8 | @Slf4j 9 | public class BeanFactory { 10 | 11 | private static final ApplicationContext context = ApplicationContext.getInstance(); 12 | 13 | /** 14 | * 初始化IOC容器,扫描指定包 15 | */ 16 | public static void init(String... basePackages) { 17 | log.info("Initializing IOC container, scanning packages: {}", (Object) basePackages); 18 | context.scan(basePackages); 19 | log.info("IOC container initialized successfully"); 20 | } 21 | 22 | /** 23 | * 根据名称获取Bean 24 | */ 25 | public static T getBean(String beanName) { 26 | return context.getBean(beanName); 27 | } 28 | 29 | /** 30 | * 根据类型获取Bean 31 | */ 32 | public static T getBean(Class requiredType) { 33 | return context.getBean(requiredType); 34 | } 35 | 36 | /** 37 | * 销毁所有Bean,调用@PreDestroy方法 38 | * 建议在应用程序关闭时调用此方法 39 | */ 40 | public static void destroy() { 41 | log.info("Destroying IOC container..."); 42 | context.destroy(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/resources/icons/format.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/script/Cookie.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model.script; 2 | 3 | /** 4 | * Cookie 数据对象 5 | *

6 | * 表示一个 HTTP Cookie,包含所有标准 Cookie 属性。 7 | *

8 | */ 9 | public class Cookie { 10 | /** 11 | * Cookie 名称 12 | */ 13 | public String name; 14 | 15 | /** 16 | * Cookie 值 17 | */ 18 | public String value; 19 | 20 | /** 21 | * Cookie 所属域名 22 | */ 23 | public String domain; 24 | 25 | /** 26 | * Cookie 路径 27 | */ 28 | public String path; 29 | 30 | /** 31 | * Cookie 过期时间 32 | */ 33 | public String expires; 34 | 35 | /** 36 | * Cookie 最大生存时间(秒) 37 | */ 38 | public Integer maxAge; 39 | 40 | /** 41 | * 是否仅在 HTTPS 连接中发送 42 | */ 43 | public Boolean secure; 44 | 45 | /** 46 | * 是否禁止 JavaScript 访问 47 | */ 48 | public Boolean httpOnly; 49 | 50 | /** 51 | * SameSite 属性 (Strict/Lax/None) 52 | */ 53 | public String sameSite; 54 | 55 | @Override 56 | public String toString() { 57 | return String.format("Cookie{name='%s', value='%s', domain='%s', path='%s'}", 58 | name, value, domain, path); 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/http/HttpSingleRequestExecutor.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.http; 2 | 3 | import com.laker.postman.model.HttpResponse; 4 | import com.laker.postman.model.PreparedRequest; 5 | import com.laker.postman.service.http.sse.SseResEventListener; 6 | import lombok.experimental.UtilityClass; 7 | import lombok.extern.slf4j.Slf4j; 8 | import okhttp3.WebSocketListener; 9 | import okhttp3.sse.EventSource; 10 | import okhttp3.sse.EventSourceListener; 11 | 12 | @Slf4j 13 | @UtilityClass 14 | public class HttpSingleRequestExecutor { 15 | 16 | public static HttpResponse executeHttp(PreparedRequest req) throws Exception { 17 | return HttpService.sendRequest(req, null); 18 | } 19 | 20 | public static HttpResponse executeHttp(PreparedRequest req, SseResEventListener callback) throws Exception { 21 | return HttpService.sendRequest(req, callback); 22 | } 23 | 24 | public static EventSource executeSSE(PreparedRequest req, EventSourceListener listener) { 25 | return HttpService.sendSseRequest(req, listener); 26 | } 27 | 28 | public static void executeWebSocket(PreparedRequest req, WebSocketListener listener) { 29 | HttpService.sendWebSocket(req, listener); 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/MessageType.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon; 4 | import com.laker.postman.util.I18nUtil; 5 | import com.laker.postman.util.MessageKeys; 6 | 7 | import javax.swing.*; 8 | 9 | // 消息类型 10 | public enum MessageType { 11 | SENT(I18nUtil.getMessage(MessageKeys.WEBSOCKET_TYPE_SENT), new FlatSVGIcon("icons/ws-send.svg", 16, 16)), 12 | RECEIVED(I18nUtil.getMessage(MessageKeys.WEBSOCKET_TYPE_RECEIVED), new FlatSVGIcon("icons/ws-receive.svg", 16, 16)), 13 | CONNECTED(I18nUtil.getMessage(MessageKeys.WEBSOCKET_TYPE_CONNECTED), new FlatSVGIcon("icons/ws-connect.svg", 16, 16)), 14 | CLOSED(I18nUtil.getMessage(MessageKeys.WEBSOCKET_TYPE_CLOSED), new FlatSVGIcon("icons/ws-close.svg", 16, 16)), 15 | WARNING(I18nUtil.getMessage(MessageKeys.WEBSOCKET_TYPE_WARNING), new FlatSVGIcon("icons/warning.svg", 16, 16)), 16 | INFO(I18nUtil.getMessage(MessageKeys.WEBSOCKET_TYPE_INFO), new FlatSVGIcon("icons/ws-info.svg", 16, 16)), 17 | BINARY(I18nUtil.getMessage(MessageKeys.WEBSOCKET_TYPE_BINARY), new FlatSVGIcon("icons/binary.svg", 16, 16)); 18 | public final String display; 19 | public final Icon icon; 20 | 21 | MessageType(String display, Icon icon) { 22 | this.display = display; 23 | this.icon = icon; 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/SingletonBasePanel.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common; 2 | 3 | import javax.swing.*; 4 | 5 | /** 6 | * 面板基础抽象类,统一UI初始化和监听注册结构 7 | * 提供了initUI和registerListeners两个抽象方法, 8 | * 子类需要实现这两个方法来完成具体的UI组件初始化和事件监听注册。 9 | * 所有子类必须通过SingletonFactory获取实例,禁止直接new创建实例。 10 | */ 11 | public abstract class SingletonBasePanel extends JPanel { 12 | // 使用静态线程局部变量直接控制创建状态 13 | private static final ThreadLocal CREATING_ALLOWED = ThreadLocal.withInitial(() -> Boolean.FALSE); 14 | 15 | // 构造函数设为protected,限制外部直接new 16 | protected SingletonBasePanel() { 17 | // 验证是否允许创建实例 18 | if (!CREATING_ALLOWED.get()) { 19 | throw new IllegalStateException( 20 | "SingletonBasePanel子类必须通过SingletonFactory.getInstance()获取实例,禁止直接new创建: " + 21 | this.getClass().getName()); 22 | } 23 | } 24 | 25 | public void safeInit() { 26 | initUI(); 27 | registerListeners(); 28 | } 29 | 30 | /** 31 | * 初始化UI组件 32 | */ 33 | protected abstract void initUI(); 34 | 35 | /** 36 | * 注册事件监听 37 | */ 38 | protected abstract void registerListeners(); 39 | 40 | /** 41 | * 设置是否允许创建实例,供SingletonFactory使用 42 | */ 43 | public static void setCreatingAllowed(boolean allowed) { 44 | CREATING_ALLOWED.set(allowed); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/SingletonBaseMenuBar.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common; 2 | 3 | import javax.swing.*; 4 | 5 | /** 6 | * 面板基础抽象类,统一UI初始化和监听注册结构 7 | * 提供了initUI和registerListeners两个抽象方法, 8 | * 子类需要实现这两个方法来完成具体的UI组件初始化和事件监听注册。 9 | * 所有子类必须通过SingletonFactory获取实例,禁止直接new创建实例。 10 | */ 11 | public abstract class SingletonBaseMenuBar extends JMenuBar { 12 | // 使用静态线程局部变量直接控制创建状态 13 | private static final ThreadLocal CREATING_ALLOWED = ThreadLocal.withInitial(() -> Boolean.FALSE); 14 | 15 | // 构造函数设为protected,限制外部直接new 16 | protected SingletonBaseMenuBar() { 17 | // 验证是否允许创建实例 18 | if (!CREATING_ALLOWED.get()) { 19 | throw new IllegalStateException( 20 | "SingletonBasePanel子类必须通过SingletonFactory.getInstance()获取实例,禁止直接new创建: " + 21 | this.getClass().getName()); 22 | } 23 | } 24 | 25 | public void safeInit() { 26 | initUI(); 27 | registerListeners(); 28 | } 29 | 30 | /** 31 | * 初始化UI组件 32 | */ 33 | protected abstract void initUI(); 34 | 35 | /** 36 | * 注册事件监听 37 | */ 38 | protected abstract void registerListeners(); 39 | 40 | /** 41 | * 设置是否允许创建实例,供SingletonFactory使用 42 | */ 43 | public static void setCreatingAllowed(boolean allowed) { 44 | CREATING_ALLOWED.set(allowed); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/update/source/UpdateSource.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.update.source; 2 | 3 | import cn.hutool.json.JSONArray; 4 | import cn.hutool.json.JSONObject; 5 | 6 | /** 7 | * 更新源接口 - 定义统一的版本检查和发布信息获取规范 8 | * 9 | *

不同的更新源(GitHub、Gitee 等)需要实现此接口,提供统一的访问方式。

10 | */ 11 | public interface UpdateSource { 12 | 13 | /** 14 | * 获取更新源的名称 15 | * 16 | * @return 更新源名称(如 "GitHub", "Gitee") 17 | */ 18 | String getName(); 19 | 20 | /** 21 | * 获取最新版本的 API URL 22 | * 23 | * @return API 地址 24 | */ 25 | String getApiUrl(); 26 | 27 | /** 28 | * 获取所有版本的 API URL 29 | * 30 | * @return 所有版本的 API 地址 31 | */ 32 | String getAllReleasesApiUrl(); 33 | 34 | /** 35 | * 获取网页版发布页面 URL 36 | * 37 | * @return 网页版发布页面地址 38 | */ 39 | String getWebUrl(); 40 | 41 | /** 42 | * 测试更新源的连接速度 43 | * 44 | * @return 响应时间(毫秒),如果连接失败返回 Long.MAX_VALUE 45 | */ 46 | long testConnectionSpeed(); 47 | 48 | /** 49 | * 获取最新版本发布信息 50 | * 51 | * @return 发布信息的 JSON 对象,如果获取失败返回 null 52 | */ 53 | JSONObject fetchLatestReleaseInfo(); 54 | 55 | /** 56 | * 获取所有版本发布信息 57 | * 58 | * @param limit 限制返回的版本数量,0 表示不限制 59 | * @return 发布信息的 JSON 数组,如果获取失败返回 null 60 | */ 61 | JSONArray fetchAllReleases(int limit); 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/RequestResult.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import com.laker.postman.model.script.TestResult; 4 | import lombok.Data; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | /** 10 | * 单个请求的执行结果 11 | */ 12 | @Data 13 | public class RequestResult { 14 | private final String requestName; 15 | private final String method; 16 | private final String url; 17 | private final PreparedRequest req; 18 | private final HttpResponse response; 19 | private final long cost; 20 | private final String status; 21 | private final AssertionResult assertion; 22 | private final List testResults; 23 | private final long timestamp; 24 | 25 | public RequestResult(String requestName, String method, String url, 26 | PreparedRequest req, HttpResponse response, long cost, String status, 27 | AssertionResult assertion, List testResults) { 28 | this.requestName = requestName; 29 | this.method = method; 30 | this.url = url; 31 | this.req = req; 32 | this.response = response; 33 | this.cost = cost; 34 | this.status = status; 35 | this.assertion = assertion; 36 | this.testResults = testResults != null ? new ArrayList<>(testResults) : new ArrayList<>(); 37 | this.timestamp = System.currentTimeMillis(); 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/resources/icons/postman.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/HttpResponse.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | /** 7 | * HTTP 响应类 8 | */ 9 | public class HttpResponse { 10 | public Map> headers; 11 | public String message; 12 | public String body; 13 | public int code; // 添加响应状态码字段 14 | public String threadName; // 添加线程名称字段 15 | public String filePath; // 临时文件下载路径字段 16 | public String fileName; // 如果是文件下载,从响应头中获取的文件名字段 17 | public long costMs; // 请求耗时,单位毫秒 18 | public long endTime; // 响应结束时间,单位毫秒 19 | public String protocol; // 协议类型字段,例如 HTTP/1.1 或 HTTP/2 20 | public int idleConnectionCount; // 空闲连接数 21 | public int connectionCount; // 连接总数 22 | public HttpEventInfo httpEventInfo; 23 | public long bodySize; // 响应体字节数 24 | public long headersSize; // 响应头字节数 25 | public boolean isSse = false; // 是否为SSE响应 26 | 27 | public void addHeader(String name, List value) { 28 | if (headers == null) { 29 | return; 30 | } 31 | if ("Easy-Content-Encoding".equalsIgnoreCase(name)) { 32 | headers.put("Content-Encoding", value); 33 | headers.remove("Easy-Content-Encoding"); 34 | } else if ("Easy-Content-Length".equalsIgnoreCase(name)) { 35 | headers.put("Content-Length", value); 36 | headers.remove("Easy-Content-Length"); 37 | } else { 38 | headers.put(name, value); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/IterationResult.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | /** 8 | * 单次迭代的执行结果 9 | */ 10 | public class IterationResult { 11 | private final int iterationIndex; 12 | private final Map csvData; 13 | private final List requestResults = new ArrayList<>(); 14 | private final long startTime; 15 | private long endTime; 16 | 17 | public IterationResult(int iterationIndex, Map csvData) { 18 | this.iterationIndex = iterationIndex; 19 | this.csvData = csvData; 20 | this.startTime = System.currentTimeMillis(); 21 | } 22 | 23 | public void addRequestResult(RequestResult result) { 24 | requestResults.add(result); 25 | } 26 | 27 | public void complete() { 28 | this.endTime = System.currentTimeMillis(); 29 | } 30 | 31 | public int getIterationIndex() { 32 | return iterationIndex; 33 | } 34 | 35 | public Map getCsvData() { 36 | return csvData; 37 | } 38 | 39 | public List getRequestResults() { 40 | return new ArrayList<>(requestResults); 41 | } 42 | 43 | public long getExecutionTime() { 44 | return endTime - startTime; 45 | } 46 | 47 | public long getStartTime() { 48 | return startTime; 49 | } 50 | 51 | public long getEndTime() { 52 | return endTime; 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/BatchExecutionHistory.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * 批量执行的历史记录 8 | * 记录所有轮次的执行结果 9 | */ 10 | public class BatchExecutionHistory { 11 | private final List iterations = new ArrayList<>(); 12 | private int totalIterations; 13 | private int totalRequests; 14 | private long startTime; 15 | private long endTime; 16 | 17 | public BatchExecutionHistory() { 18 | this.startTime = System.currentTimeMillis(); 19 | } 20 | 21 | public void addIteration(IterationResult iteration) { 22 | iterations.add(iteration); 23 | } 24 | 25 | public void complete() { 26 | this.endTime = System.currentTimeMillis(); 27 | } 28 | 29 | public List getIterations() { 30 | return new ArrayList<>(iterations); 31 | } 32 | 33 | public int getTotalIterations() { 34 | return totalIterations; 35 | } 36 | 37 | public void setTotalIterations(int totalIterations) { 38 | this.totalIterations = totalIterations; 39 | } 40 | 41 | public int getTotalRequests() { 42 | return totalRequests; 43 | } 44 | 45 | public void setTotalRequests(int totalRequests) { 46 | this.totalRequests = totalRequests; 47 | } 48 | 49 | public long getExecutionTime() { 50 | return endTime - startTime; 51 | } 52 | 53 | public long getStartTime() { 54 | return startTime; 55 | } 56 | 57 | public long getEndTime() { 58 | return endTime; 59 | } 60 | 61 | 62 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/RequestGroup.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.io.Serializable; 7 | import java.util.UUID; 8 | 9 | /** 10 | * RequestGroup 类表示一个请求分组的配置项 11 | * 包含分组的名称、认证、脚本等信息 12 | * 分组级别的认证和脚本会被其下的请求继承 13 | */ 14 | @Getter 15 | @Setter 16 | public class RequestGroup implements Serializable { 17 | private String id = ""; // 唯一标识符 18 | private String name = ""; // 分组名称 19 | private String authType = AuthType.NONE.getConstant(); // 认证类型(none/basic/bearer),分组默认无认证 20 | private String authUsername = ""; // Basic用户名 21 | private String authPassword = ""; // Basic密码 22 | private String authToken = ""; // Bearer Token 23 | // 前置脚本(请求前执行) 24 | private String prescript = ""; 25 | // 后置脚本(响应后执行) 26 | private String postscript = ""; 27 | 28 | public RequestGroup() { 29 | this.id = UUID.randomUUID().toString(); 30 | } 31 | 32 | public RequestGroup(String name) { 33 | this.id = UUID.randomUUID().toString(); 34 | this.name = name; 35 | } 36 | 37 | /** 38 | * 判断分组是否有认证配置 39 | */ 40 | public boolean hasAuth() { 41 | return authType != null && !AuthType.NONE.getConstant().equals(authType); 42 | } 43 | 44 | /** 45 | * 判断分组是否有前置脚本 46 | */ 47 | public boolean hasPreScript() { 48 | return prescript != null && !prescript.trim().isEmpty(); 49 | } 50 | 51 | /** 52 | * 判断分组是否有后置脚本 53 | */ 54 | public boolean hasPostScript() { 55 | return postscript != null && !postscript.trim().isEmpty(); 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/NotificationPosition.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import com.laker.postman.util.MessageKeys; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | /** 8 | * 通知位置枚举 9 | * 包含位置的顺序索引和国际化键 10 | */ 11 | @Getter 12 | @RequiredArgsConstructor 13 | public enum NotificationPosition { 14 | TOP_RIGHT(0, MessageKeys.SETTINGS_GENERAL_NOTIFICATION_POSITION_TOP_RIGHT), 15 | TOP_CENTER(1, MessageKeys.SETTINGS_GENERAL_NOTIFICATION_POSITION_TOP_CENTER), 16 | TOP_LEFT(2, MessageKeys.SETTINGS_GENERAL_NOTIFICATION_POSITION_TOP_LEFT), 17 | BOTTOM_RIGHT(3, MessageKeys.SETTINGS_GENERAL_NOTIFICATION_POSITION_BOTTOM_RIGHT), 18 | BOTTOM_CENTER(4, MessageKeys.SETTINGS_GENERAL_NOTIFICATION_POSITION_BOTTOM_CENTER), 19 | BOTTOM_LEFT(5, MessageKeys.SETTINGS_GENERAL_NOTIFICATION_POSITION_BOTTOM_LEFT), 20 | CENTER(6, MessageKeys.SETTINGS_GENERAL_NOTIFICATION_POSITION_CENTER); 21 | 22 | private final int index; 23 | private final String i18nKey; 24 | 25 | /** 26 | * 根据索引获取位置 27 | */ 28 | public static NotificationPosition fromIndex(int index) { 29 | for (NotificationPosition position : values()) { 30 | if (position.index == index) { 31 | return position; 32 | } 33 | } 34 | return BOTTOM_RIGHT; // 默认右下角 35 | } 36 | 37 | /** 38 | * 根据名称获取位置 39 | */ 40 | public static NotificationPosition fromName(String name) { 41 | try { 42 | return valueOf(name); 43 | } catch (IllegalArgumentException e) { 44 | return BOTTOM_RIGHT; // 默认右下角 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/HttpRequestItem.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.io.Serializable; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | /** 11 | * HttpRequestItem 类表示一个HTTP请求的配置项 12 | * 包含请求的基本信息、头部、参数、认证等 13 | * 每个参数都要求有默认值,便于前端展示和编辑 14 | */ 15 | @Setter 16 | @Getter 17 | public class HttpRequestItem implements Serializable { 18 | private String id = ""; // 唯一标识符 19 | private String name = ""; // 请求名称 20 | private String url = ""; // 请求URL 21 | private String method = "GET"; // 请求方法(GET, POST, PUT, DELETE等) 22 | private RequestItemProtocolEnum protocol = RequestItemProtocolEnum.HTTP; // 协议类型,默认HTTP 23 | private List headersList = new ArrayList<>(); 24 | private String bodyType = ""; // 请求体类型 25 | private String body = ""; // 请求体内容(如JSON、表单数据等) 26 | private List paramsList = new ArrayList<>(); 27 | private List formDataList = new ArrayList<>(); 28 | private List urlencodedList = new ArrayList<>(); 29 | private String authType = AuthType.INHERIT.getConstant(); // 认证类型(inherit/none/basic/bearer),默认继承 30 | private String authUsername = ""; // Basic用户名 31 | private String authPassword = ""; // Basic密码 32 | private String authToken = ""; // Bearer Token 33 | // 前置脚本(请求前执行) 34 | private String prescript = ""; 35 | // 后置脚本(响应后执行) 36 | private String postscript = ""; 37 | 38 | /** 39 | * 判断该请求是否为新建(未命名)请求 40 | */ 41 | public boolean isNewRequest() { 42 | return name == null || name.trim().isEmpty(); 43 | } 44 | 45 | 46 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/collections/RequestCollectionsPanel.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.collections; 2 | 3 | import com.laker.postman.common.SingletonFactory; 4 | import com.laker.postman.common.SingletonBasePanel; 5 | import com.laker.postman.panel.collections.left.RequestCollectionsLeftPanel; 6 | import com.laker.postman.panel.collections.right.RequestEditPanel; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | import javax.swing.*; 10 | import java.awt.*; 11 | 12 | /** 13 | * 请求集合面板,包含左侧的请求集合列表和右侧的请求编辑面板 14 | */ 15 | @Slf4j 16 | public class RequestCollectionsPanel extends SingletonBasePanel { 17 | @Override 18 | protected void initUI() { 19 | // 设置布局为 BorderLayout 20 | setLayout(new BorderLayout()); // 设置布局为 BorderLayout 21 | setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, Color.LIGHT_GRAY)); 22 | // 1.创建左侧的请求集合面板 23 | RequestCollectionsLeftPanel requestCollectionsLeftPanel = SingletonFactory.getInstance(RequestCollectionsLeftPanel.class); 24 | // 2. 创建右侧的请求编辑面板 25 | RequestEditPanel rightRequestEditPanel = SingletonFactory.getInstance(RequestEditPanel.class); // 创建请求编辑面板实例 26 | // 创建水平分割面板,将左侧的集合面板和右侧的请求编辑面板放入其中 27 | JSplitPane mainSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, requestCollectionsLeftPanel, rightRequestEditPanel); 28 | mainSplit.setContinuousLayout(true); // 分割条拖动时实时更新布局 29 | mainSplit.setDividerLocation(250); // 设置初始分割位置 30 | mainSplit.setDividerSize(2); // 设置分割条的宽度 31 | 32 | // 将分割面板添加到主面板 33 | add(mainSplit, BorderLayout.CENTER); // 将分割面板添加到主面板的中心位置 34 | } 35 | 36 | @Override 37 | protected void registerListeners() { 38 | // no op 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/resources/icons/environments.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/PreparedRequest.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | 4 | import okhttp3.Headers; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * 准备好的请求对象,包含请求的所有必要信息以及替换变量后的内容。 10 | */ 11 | public class PreparedRequest { 12 | public String id; 13 | public String url; 14 | public String method; 15 | 16 | public String body; 17 | public String bodyType; 18 | 19 | public List headersList; 20 | public List formDataList; 21 | public List urlencodedList; 22 | public List paramsList; 23 | 24 | public boolean isMultipart; 25 | public boolean followRedirects = true; // 默认自动重定向 26 | public boolean logEvent = false; // 默认不记录事件日志 27 | 28 | public Headers okHttpHeaders; // OkHttp 特有的 Headers 对象 29 | public String okHttpRequestBody; // 真实OkHttp请求体内容 30 | 31 | /** 32 | * 创建当前对象的浅拷贝 33 | * 注意:List 和 Map 对象本身不会被深拷贝,只是引用复制 34 | */ 35 | public PreparedRequest shallowCopy() { 36 | PreparedRequest copy = new PreparedRequest(); 37 | copy.id = this.id; 38 | copy.url = this.url; 39 | copy.method = this.method; 40 | copy.okHttpHeaders = this.okHttpHeaders; 41 | copy.body = this.body; 42 | copy.bodyType = this.bodyType; 43 | copy.okHttpRequestBody = this.okHttpRequestBody; 44 | copy.isMultipart = this.isMultipart; 45 | copy.followRedirects = this.followRedirects; 46 | copy.logEvent = this.logEvent; 47 | copy.headersList = this.headersList; 48 | copy.formDataList = this.formDataList; 49 | copy.urlencodedList = this.urlencodedList; 50 | copy.paramsList = this.paramsList; 51 | return copy; 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/ExitService.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service; 2 | 3 | import com.laker.postman.common.SingletonFactory; 4 | import com.laker.postman.common.exception.CancelException; 5 | import com.laker.postman.frame.MainFrame; 6 | import com.laker.postman.ioc.Component; 7 | import com.laker.postman.panel.functional.FunctionalPanel; 8 | import com.laker.postman.panel.performance.PerformancePanel; 9 | import com.laker.postman.service.collections.OpenedRequestsService; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | @Slf4j 13 | @Component 14 | public class ExitService { 15 | 16 | /** 17 | * 显示退出确认对话框,处理未保存内容。 18 | */ 19 | public void exit() { 20 | 21 | // 保存所有打开的请求(包括未保存的和已保存的) 22 | try { 23 | OpenedRequestsService.save(); 24 | } catch (CancelException e) { 25 | // 用户取消了保存操作,终止退出 26 | return; 27 | } 28 | 29 | // 保存功能测试配置 30 | try { 31 | FunctionalPanel functionalPanel = SingletonFactory.getInstance(FunctionalPanel.class); 32 | functionalPanel.save(); 33 | } catch (Exception e) { 34 | log.error("Failed to save functional test config on exit", e); 35 | } 36 | 37 | // 保存性能测试配置 38 | try { 39 | PerformancePanel performancePanel = SingletonFactory.getInstance(PerformancePanel.class); 40 | performancePanel.save(); 41 | } catch (Exception e) { 42 | log.error("Failed to save performance test config on exit", e); 43 | } 44 | 45 | // 没有未保存内容,或已处理完未保存内容,直接退出 46 | log.info("User chose to exit application"); 47 | SingletonFactory.getInstance(MainFrame.class).dispose(); 48 | System.exit(0); 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/util/FileSizeDisplayUtil.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | /** 6 | * 文件大小显示工具类 7 | * 提供将字节数转换为更友好的显示格式 8 | */ 9 | @UtilityClass 10 | public class FileSizeDisplayUtil { 11 | 12 | /** 13 | * 格式化文件大小显示,自动选择单位,保留两位小数,去除无意义的.00 14 | * 15 | * @param sizeBytes 字节数 16 | * @return 友好的显示字符串,如 1.23 MB 17 | */ 18 | public static String formatSize(int sizeBytes) { 19 | String unit; 20 | double value; 21 | if (sizeBytes >= 1024 * 1024 * 1024) { 22 | unit = "GB"; 23 | value = sizeBytes / (1024.0 * 1024 * 1024); 24 | } else if (sizeBytes >= 1024 * 1024) { 25 | unit = "MB"; 26 | value = sizeBytes / (1024.0 * 1024); 27 | } else if (sizeBytes >= 1024) { 28 | unit = "KB"; 29 | value = sizeBytes / 1024.0; 30 | } else { 31 | unit = "B"; 32 | value = sizeBytes; 33 | } 34 | if (unit.equals("B")) { 35 | return String.format("%d %s", (int) value, unit); 36 | } else { 37 | return String.format("%.2f %s", value, unit); 38 | } 39 | } 40 | 41 | /** 42 | * 格式化文件下载进度显示 43 | * 44 | * @param totalBytes 已下载字节数 45 | * @param contentLength 总字节数 46 | * @return 友好的显示字符串 47 | */ 48 | public static String formatDownloadSize(int totalBytes, int contentLength) { 49 | if (contentLength > 0) { 50 | return String.format("Downloaded: %s / %s", formatSize(totalBytes), formatSize(contentLength)); 51 | } else { 52 | return String.format("Downloaded: %s", formatSize(totalBytes)); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/update/version/VersionComparator.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.update.version; 2 | 3 | /** 4 | * 版本比较工具 5 | */ 6 | public class VersionComparator { 7 | 8 | /** 9 | * 比较两个版本号 10 | * 11 | * @param v1 版本1(可以带 'v' 前缀) 12 | * @param v2 版本2(可以带 'v' 前缀) 13 | * @return 正数表示 v1 > v2,0 表示相等,负数表示 v1 < v2 14 | */ 15 | public static int compare(String v1, String v2) { 16 | if (v1 == null || v2 == null) { 17 | return 0; 18 | } 19 | 20 | String s1 = removeVersionPrefix(v1); 21 | String s2 = removeVersionPrefix(v2); 22 | 23 | String[] arr1 = s1.split("\\."); 24 | String[] arr2 = s2.split("\\."); 25 | int len = Math.max(arr1.length, arr2.length); 26 | 27 | for (int i = 0; i < len; i++) { 28 | int n1 = i < arr1.length ? parseIntSafely(arr1[i]) : 0; 29 | int n2 = i < arr2.length ? parseIntSafely(arr2[i]) : 0; 30 | if (n1 != n2) { 31 | return Integer.compare(n1, n2); 32 | } 33 | } 34 | return 0; 35 | } 36 | 37 | /** 38 | * 移除版本号前缀(如 'v') 39 | */ 40 | private static String removeVersionPrefix(String version) { 41 | return version.startsWith("v") ? version.substring(1) : version; 42 | } 43 | 44 | /** 45 | * 安全解析整数(移除非数字字符) 46 | */ 47 | private static int parseIntSafely(String s) { 48 | try { 49 | return Integer.parseInt(s.replaceAll("\\D", "")); 50 | } catch (Exception e) { 51 | return 0; 52 | } 53 | } 54 | 55 | /** 56 | * 判断版本1是否大于版本2 57 | */ 58 | public static boolean isNewer(String v1, String v2) { 59 | return compare(v1, v2) > 0; 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/http/ssl/SSLException.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.http.ssl; 2 | 3 | /** 4 | * SSL相关异常的统一封装 5 | */ 6 | public class SSLException extends RuntimeException { 7 | 8 | private final SSLErrorType errorType; 9 | 10 | public SSLException(String message, SSLErrorType errorType) { 11 | super(message); 12 | this.errorType = errorType; 13 | } 14 | 15 | public SSLException(String message, Throwable cause, SSLErrorType errorType) { 16 | super(message, cause); 17 | this.errorType = errorType; 18 | } 19 | 20 | public SSLErrorType getErrorType() { 21 | return errorType; 22 | } 23 | 24 | /** 25 | * SSL错误类型枚举 26 | */ 27 | public enum SSLErrorType { 28 | /** 证书过期 */ 29 | CERTIFICATE_EXPIRED("Certificate expired"), 30 | /** 证书未生效 */ 31 | CERTIFICATE_NOT_YET_VALID("Certificate not yet valid"), 32 | /** 不受信任的证书 */ 33 | UNTRUSTED_CERTIFICATE("Untrusted certificate"), 34 | /** 主机名不匹配 */ 35 | HOSTNAME_MISMATCH("Hostname mismatch"), 36 | /** 证书链不完整 */ 37 | INCOMPLETE_CHAIN("Incomplete certificate chain"), 38 | /** 自签名证书 */ 39 | SELF_SIGNED_CERTIFICATE("Self-signed certificate"), 40 | /** 证书路径验证失败 */ 41 | PATH_VALIDATION_FAILED("Certificate path validation failed"), 42 | /** SSL配置错误 */ 43 | CONFIGURATION_ERROR("SSL configuration error"), 44 | /** 未知SSL错误 */ 45 | UNKNOWN("Unknown SSL error"); 46 | 47 | private final String description; 48 | 49 | SSLErrorType(String description) { 50 | this.description = description; 51 | } 52 | 53 | public String getDescription() { 54 | return description; 55 | } 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/util/JComponentUtils.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | @UtilityClass 9 | public class JComponentUtils { 10 | 11 | /** 12 | * 超出宽度显示省略号(...)。默认icon/padding宽度为30px,最小宽度为200px。 13 | * 14 | * @param text 原始文本 15 | * @param component 用于计算宽度的组件(如JList、JLabel等) 16 | * @return 适配宽度的文本,超出部分自动省略并加...,否则原文 17 | */ 18 | public static String ellipsisText(String text, JComponent component) { 19 | return ellipsisText(text, component, 30, 200); 20 | } 21 | 22 | /** 23 | * 超出宽度显示省略号(...),可自定义icon/padding宽度和最小宽度。 24 | * 25 | * @param text 原始文本 26 | * @param component 用于计算宽度的组件(如JList、JLabel等) 27 | * @param reservedWidth 预留宽度(如icon和padding,单位px)在计算文本最大宽度时,会减去30像素,剩下的空间才用于显示文本 28 | * @param minWidth 最小宽度(单位px),表示即使组件宽度为0,也假定有200像素可用来显示文本。 29 | * @return 适配宽度的文本,超出部分自动省略并加...,否则原文 30 | */ 31 | public static String ellipsisText(String text, JComponent component, int reservedWidth, int minWidth) { 32 | if (text == null) return null; 33 | FontMetrics fm = component.getFontMetrics(component.getFont()); // 获取字体度量 34 | int maxWidth = component.getWidth() > 0 ? component.getWidth() - reservedWidth : minWidth; // 预留icon和padding 35 | if (maxWidth <= 0) return text; 36 | String ellipsis = "..."; 37 | int width = fm.stringWidth(text); 38 | if (width <= maxWidth) return text; 39 | for (int i = text.length() - 1; i > 0; i--) { 40 | String sub = text.substring(0, i) + ellipsis; 41 | if (fm.stringWidth(sub) <= maxWidth) { 42 | return sub; 43 | } 44 | } 45 | return text; 46 | } 47 | } -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 📝 PR 描述 | Description 2 | 3 | 4 | 5 | ## 🔗 相关 Issue | Related Issue 6 | 7 | 8 | Fixes #(issue number) 9 | 10 | ## 🎯 改动类型 | Type of Change 11 | 12 | 13 | 14 | - [ ] 🐛 Bug 修复 | Bug fix 15 | - [ ] ✨ 新功能 | New feature 16 | - [ ] 📝 文档更新 | Documentation update 17 | - [ ] 🎨 代码优化 | Code refactoring 18 | - [ ] ⚡️ 性能优化 | Performance improvement 19 | - [ ] 🔧 配置变更 | Configuration change 20 | - [ ] 🧪 测试相关 | Test related 21 | - [ ] 🔨 构建相关 | Build related 22 | - [ ] 🌐 国际化 | Internationalization 23 | 24 | ## 📋 改动内容 | Changes Made 25 | 26 | 27 | 28 | - 29 | 30 | ## 🧪 测试 | Testing 31 | 32 | 33 | 34 | - [ ] 本地编译通过 | Local build passed 35 | - [ ] 功能测试通过 | Functional tests passed 36 | - [ ] 在以下环境测试 | Tested on: 37 | - [ ] Windows 38 | - [ ] macOS 39 | - [ ] Linux 40 | 41 | ## 📸 截图 | Screenshots 42 | 43 | 44 | 45 | ## ✅ 检查清单 | Checklist 46 | 47 | 48 | 49 | - [ ] 代码遵循项目编码规范 | Code follows the project's coding standards 50 | - [ ] 已添加必要的注释 | Added necessary comments 51 | - [ ] 文档已更新(如需要)| Documentation updated (if needed) 52 | - [ ] 没有引入新的警告 | No new warnings introduced 53 | - [ ] 改动不影响现有功能 | Changes do not affect existing functionality 54 | - [ ] 已在本地完整测试 | Fully tested locally 55 | 56 | ## 💡 其他说明 | Additional Notes 57 | 58 | 59 | 60 | --- 61 | 62 | 感谢你的贡献! | Thank you for your contribution! 🎉 63 | 64 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/AuthType.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import com.laker.postman.util.I18nUtil; 4 | import com.laker.postman.util.MessageKeys; 5 | import lombok.Getter; 6 | 7 | /** 8 | * Authentication type enum 9 | */ 10 | @Getter 11 | public enum AuthType { 12 | INHERIT("Inherit auth from parent", MessageKeys.AUTH_TYPE_INHERIT), 13 | NONE("No Auth", MessageKeys.AUTH_TYPE_NONE), 14 | BASIC("Basic Auth", MessageKeys.AUTH_TYPE_BASIC), 15 | BEARER("Bearer Token", MessageKeys.AUTH_TYPE_BEARER); 16 | 17 | private final String constant; 18 | private final String i18nKey; 19 | 20 | AuthType(String constant, String i18nKey) { 21 | this.constant = constant; 22 | this.i18nKey = i18nKey; 23 | } 24 | 25 | public String getDisplayText() { 26 | return I18nUtil.getMessage(i18nKey); 27 | } 28 | 29 | /** 30 | * Get AuthType from constant string 31 | */ 32 | public static AuthType fromConstant(String constant) { 33 | if (constant == null) { 34 | return INHERIT; 35 | } 36 | for (AuthType type : values()) { 37 | if (type.constant.equals(constant)) { 38 | return type; 39 | } 40 | } 41 | return INHERIT; 42 | } 43 | 44 | /** 45 | * Get AuthType from display text (for backward compatibility) 46 | */ 47 | public static AuthType fromDisplayText(String displayText) { 48 | if (displayText == null) { 49 | return INHERIT; 50 | } 51 | for (AuthType type : values()) { 52 | if (type.getDisplayText().equals(displayText)) { 53 | return type; 54 | } 55 | } 56 | return INHERIT; 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return getDisplayText(); 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # 工作流名称:CodeQL 代码安全分析 2 | # CodeQL 是什么? 3 | # - GitHub 的代码安全扫描工具,用于自动检测代码中的安全漏洞和编码错误 4 | # - 支持多种编程语言(Java、JavaScript、Python、C/C++等) 5 | # - 可以发现:SQL 注入、XSS、路径遍历、不安全的反序列化等安全问题 6 | name: "CodeQL" 7 | 8 | # 触发条件 9 | on: 10 | # 当代码推送到 master 分支时触发(检测新代码的安全问题) 11 | push: 12 | branches: [ master ] 13 | # 当创建针对 master 分支的 PR 时触发(在合并前发现安全问题) 14 | pull_request: 15 | branches: [ master ] 16 | # 定时扫描:每周日 UTC 02:00(北京时间 10:00)运行一次 17 | # 作用:即使代码没有变化,也能检测到新发现的安全漏洞模式 18 | schedule: 19 | - cron: '0 2 * * 0' 20 | 21 | # 权限设置:允许将安全问题写入 GitHub Security 标签页 22 | permissions: 23 | security-events: write 24 | # 添加 actions 读取权限(某些环境需要) 25 | actions: read 26 | # 添加内容读取权限 27 | contents: read 28 | 29 | jobs: 30 | analyze: 31 | name: Analyze (CodeQL) 32 | runs-on: ubuntu-latest 33 | 34 | # 超时设置:避免分析任务运行过久 35 | timeout-minutes: 360 36 | 37 | steps: 38 | # 第一步:检出代码仓库 39 | - name: Checkout repository 40 | uses: actions/checkout@v4 41 | 42 | # 第二步:初始化 CodeQL 分析环境 43 | - name: Initialize CodeQL 44 | uses: github/codeql-action/init@v3 45 | with: 46 | # 指定要分析的编程语言 47 | languages: java 48 | # 查询级别:security-extended(推荐) 49 | # - default: 基础安全检查 50 | # - security-extended: 扩展安全检查(推荐,覆盖更多安全问题) 51 | # - security-and-quality: 安全+代码质量检查(最全面但耗时更长) 52 | queries: security-extended 53 | 54 | # 第三步:自动构建项目(CodeQL 需要编译后的代码进行分析) 55 | # 对于 Java/Maven 项目,会自动执行 mvn compile 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v3 58 | 59 | # 第四步:执行 CodeQL 安全分析 60 | - name: Perform CodeQL Analysis 61 | uses: github/codeql-action/analyze@v3 62 | with: 63 | # 分析类别标识(用于在 Security 标签页中区分不同类型的扫描) 64 | category: "security" 65 | # 上传结果到 GitHub Security 标签页 66 | # 可以在仓库的 Security -> Code scanning alerts 中查看发现的问题 67 | 68 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/update/changelog/ChangelogService.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.update.changelog; 2 | 3 | import cn.hutool.json.JSONArray; 4 | import com.laker.postman.service.update.source.UpdateSource; 5 | import com.laker.postman.service.update.source.UpdateSourceSelector; 6 | import lombok.Getter; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | /** 10 | * 更新日志服务 - 负责获取和格式化更新日志 11 | */ 12 | @Slf4j 13 | public class ChangelogService { 14 | 15 | /** 16 | * -- GETTER -- 17 | * 获取更新源选择器(用于获取 Web URL) 18 | */ 19 | @Getter 20 | private final UpdateSourceSelector sourceSelector; 21 | private final ChangelogFormatter formatter; 22 | private static final int DEFAULT_LIMIT = 10; 23 | 24 | public ChangelogService() { 25 | this.sourceSelector = new UpdateSourceSelector(); 26 | this.formatter = new ChangelogFormatter(); 27 | } 28 | 29 | /** 30 | * 获取格式化的更新日志 31 | * 32 | * @return 格式化后的更新日志文本,失败返回 null 33 | */ 34 | public String getChangelog() { 35 | return getChangelog(DEFAULT_LIMIT); 36 | } 37 | 38 | /** 39 | * 获取格式化的更新日志 40 | * 41 | * @param limit 限制返回的版本数量 42 | * @return 格式化后的更新日志文本,失败返回 null 43 | */ 44 | public String getChangelog(int limit) { 45 | // 选择最佳更新源 46 | UpdateSource primarySource = sourceSelector.selectBestSource(); 47 | JSONArray releases = primarySource.fetchAllReleases(limit); 48 | 49 | if (releases == null) { 50 | // 尝试备用源 51 | UpdateSource fallbackSource = sourceSelector.getFallbackSource(primarySource); 52 | log.info("Primary source failed, trying fallback source: {}", fallbackSource.getName()); 53 | releases = fallbackSource.fetchAllReleases(limit); 54 | } 55 | 56 | if (releases == null) { 57 | return null; 58 | } 59 | 60 | return formatter.format(releases); 61 | } 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/UpdateInfo.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import cn.hutool.json.JSONObject; 4 | import lombok.Data; 5 | 6 | /** 7 | * 更新信息封装类 8 | */ 9 | @Data 10 | public class UpdateInfo { 11 | 12 | public enum Status { 13 | UPDATE_AVAILABLE, // 有新版本可用 14 | NO_UPDATE, // 没有更新 15 | CHECK_FAILED // 检查失败 16 | } 17 | 18 | private final Status status; 19 | private final String currentVersion; 20 | private final String latestVersion; 21 | private final String message; 22 | private final JSONObject releaseInfo; 23 | 24 | private UpdateInfo(Status status, String currentVersion, String latestVersion, String message, JSONObject releaseInfo) { 25 | this.status = status; 26 | this.currentVersion = currentVersion; 27 | this.latestVersion = latestVersion; 28 | this.message = message; 29 | this.releaseInfo = releaseInfo; 30 | } 31 | 32 | /** 33 | * 创建有更新可用的信息 34 | */ 35 | public static UpdateInfo updateAvailable(String currentVersion, String latestVersion, JSONObject releaseInfo) { 36 | return new UpdateInfo(Status.UPDATE_AVAILABLE, currentVersion, latestVersion, 37 | "New version available: " + latestVersion, releaseInfo); 38 | } 39 | 40 | /** 41 | * 创建无更新的信息 42 | */ 43 | public static UpdateInfo noUpdateAvailable(String message) { 44 | return new UpdateInfo(Status.NO_UPDATE, null, null, message, null); 45 | } 46 | 47 | /** 48 | * 创建检查失败的信息 49 | */ 50 | public static UpdateInfo checkFailed(String errorMessage) { 51 | return new UpdateInfo(Status.CHECK_FAILED, null, null, errorMessage, null); 52 | } 53 | 54 | /** 55 | * 是否有更新可用 56 | */ 57 | public boolean isUpdateAvailable() { 58 | return status == Status.UPDATE_AVAILABLE; 59 | } 60 | 61 | /** 62 | * 检查是否失败 63 | */ 64 | public boolean isCheckFailed() { 65 | return status == Status.CHECK_FAILED; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/HttpFormData.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * Form-Data Parameter model with enabled state 11 | * Supports both text and file types 12 | */ 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class HttpFormData implements Serializable { 17 | private static final long serialVersionUID = 1L; 18 | 19 | /** 20 | * Type constants for form-data 21 | */ 22 | public static final String TYPE_TEXT = "Text"; 23 | public static final String TYPE_FILE = "File"; 24 | 25 | private boolean enabled = true; 26 | private String key = ""; 27 | private String type = TYPE_TEXT; // "Text" or "File" 28 | private String value = ""; 29 | 30 | /** 31 | * Normalize the type to ensure consistent capitalization 32 | * 33 | * @param type the type string (case-insensitive) 34 | * @return normalized type ("Text" or "File") 35 | */ 36 | public static String normalizeType(String type) { 37 | if (type == null || type.trim().isEmpty()) { 38 | return TYPE_TEXT; 39 | } 40 | String normalized = type.trim(); 41 | if (TYPE_FILE.equalsIgnoreCase(normalized)) { 42 | return TYPE_FILE; 43 | } 44 | return TYPE_TEXT; 45 | } 46 | 47 | /** 48 | * Check if this is a text type form-data 49 | * 50 | * @return true if type is Text 51 | */ 52 | public boolean isText() { 53 | return TYPE_TEXT.equalsIgnoreCase(type); 54 | } 55 | 56 | /** 57 | * Check if this is a file type form-data 58 | * 59 | * @return true if type is File 60 | */ 61 | public boolean isFile() { 62 | return TYPE_FILE.equalsIgnoreCase(type); 63 | } 64 | 65 | /** 66 | * Set the type with automatic normalization 67 | */ 68 | public void setType(String type) { 69 | this.type = normalizeType(type); 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/HttpEventInfo.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import lombok.Data; 4 | import okhttp3.Protocol; 5 | 6 | import java.security.cert.Certificate; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | /** 11 | * 采集 HTTP 全流程事件信息 12 | */ 13 | @Data 14 | public class HttpEventInfo { 15 | // 连接信息 16 | private String localAddress; 17 | private String remoteAddress; 18 | // 各阶段时间戳 19 | private long queueStart; // newCall前的时间戳 自己额外定义的发起请求时间 20 | private long callStart; 21 | private long proxySelectStart; 22 | private long proxySelectEnd; 23 | private long dnsStart; 24 | private long dnsEnd; 25 | private long connectStart; 26 | private long secureConnectStart; 27 | private long secureConnectEnd; 28 | private long connectEnd; 29 | private long connectionAcquired; 30 | private long requestHeadersStart; 31 | private long requestHeadersEnd; 32 | private long requestBodyStart; 33 | private long requestBodyEnd; 34 | private long responseHeadersStart; 35 | private long responseHeadersEnd; 36 | private long responseBodyStart; 37 | private long responseBodyEnd; 38 | private long connectionReleased; 39 | private long callEnd; 40 | private long callFailed; 41 | private long canceled; 42 | 43 | // 耗时统计 44 | private long queueingCost; // 排队耗时 45 | private long stalledCost; // 阻塞耗时 46 | // 协议 47 | private Protocol protocol; 48 | // TLS/证书 49 | private List peerCertificates = new ArrayList<>(); 50 | private List localCertificates = new ArrayList<>(); 51 | private String tlsVersion; 52 | // 加密套件 53 | private String cipherName; 54 | // SSL 证书验证警告信息(如过期、域名不匹配、自签名等) 55 | private String sslCertWarning; 56 | // 异常 57 | private String errorMessage; 58 | private Throwable error; 59 | // 其他 60 | private String threadName; 61 | 62 | private long bodyBytesSent; 63 | private long bodyBytesReceived; 64 | private long headerBytesSent; 65 | private long headerBytesReceived; 66 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot 配置文件 2 | # 3 | # Dependabot 是什么? 4 | # - GitHub 的自动依赖更新工具,帮助保持项目依赖的安全性和时效性 5 | # - 自动检测过时的依赖包和安全漏洞(CVE) 6 | # - 自动创建 Pull Request 来更新依赖版本 7 | # 8 | # 主要作用: 9 | # 1. 🔒 安全防护:及时发现并修复依赖包中的安全漏洞 10 | # 2. 📦 版本管理:自动更新依赖到最新版本 11 | # 3. ⏰ 节省时间:无需手动检查和更新依赖 12 | # 4. 📊 可追溯性:通过 PR 记录每次依赖更新 13 | # 14 | # 如何查看? 15 | # - 仓库 Security -> Dependabot alerts:查看安全漏洞警告 16 | # - Pull requests:查看 Dependabot 创建的更新 PR 17 | 18 | version: 2 19 | updates: 20 | # Maven 项目依赖更新配置 21 | - package-ecosystem: "maven" 22 | # 扫描目录:根目录的 pom.xml 23 | directory: "/" 24 | # 更新频率:每周检查一次 25 | # 可选值:daily(每天)、weekly(每周)、monthly(每月) 26 | schedule: 27 | interval: "weekly" 28 | # 可选:指定检查时间(UTC 时区) 29 | # day: "monday" 30 | # time: "02:00" 31 | 32 | # 限制同时打开的 PR 数量,避免一次性创建太多 PR 33 | # 推荐值:3-10,根据项目活跃度调整 34 | open-pull-requests-limit: 3 35 | 36 | # 允许更新的依赖类型 37 | # - dependency-type: "all" 表示更新所有依赖(直接依赖和间接依赖) 38 | # - 其他选项:production(生产依赖)、development(开发依赖) 39 | allow: 40 | - dependency-type: "all" 41 | 42 | # 可选:将 Dependabot PR 自动分配给特定审核人员 43 | # reviewers: 44 | # - "your-github-username" 45 | 46 | # 可选:为 Dependabot PR 自动添加标签,便于管理 47 | # labels: 48 | # - "dependencies" 49 | # - "automated" 50 | 51 | # 可选:设置提交信息前缀,便于识别 52 | # commit-message: 53 | # prefix: "chore" 54 | # include: "scope" 55 | 56 | # 忽略某些依赖的更新 57 | # 使用场景:某些依赖版本需要保持稳定,不希望自动更新 58 | ignore: 59 | # 忽略 okhttp 相关依赖的更新 60 | - dependency-name: "com.squareup.okhttp3:okhttp" 61 | - dependency-name: "com.squareup.okhttp3:okhttp-*" # 忽略所有 okhttp 子模块 62 | - dependency-name: "com.squareup.okhttp3:logging-interceptor" 63 | # - dependency-name: "org.example:unwanted" 64 | # update-types: ["version-update:semver-major"] # 忽略主版本更新 65 | 66 | # 注意事项: 67 | # 1. Dependabot 会自动处理 GitHub Advisory 数据库中的安全修复 68 | # 2. 需在仓库设置中启用 "Dependabot alerts" 和 "Dependabot security updates" 69 | # 3. 路径:Settings -> Security -> Code security and analysis 70 | # 4. 安全更新的 PR 会被标记为 "security" 优先级 71 | # 5. 可以手动触发更新:在 Insights -> Dependency graph -> Dependabot 中点击 "Check for updates" 72 | 73 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/http/okhttp/LogWebSocketListener.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.http.okhttp; 2 | 3 | import com.laker.postman.panel.sidebar.ConsolePanel; 4 | import okhttp3.Response; 5 | import okhttp3.WebSocket; 6 | import okhttp3.WebSocketListener; 7 | 8 | // 日志增强WebSocketListener 9 | public class LogWebSocketListener extends WebSocketListener { 10 | private final WebSocketListener delegate; 11 | 12 | public LogWebSocketListener(WebSocketListener delegate) { 13 | this.delegate = delegate; 14 | } 15 | 16 | @Override 17 | public void onOpen(WebSocket webSocket, Response response) { 18 | ConsolePanel.appendLog("[WebSocket] onOpen: " + response, ConsolePanel.LogType.SUCCESS); 19 | delegate.onOpen(webSocket, response); 20 | } 21 | 22 | @Override 23 | public void onMessage(WebSocket webSocket, String text) { 24 | // ConsolePanel.appendLog("[WebSocket] onMessage: " + text, ConsolePanel.LogType.INFO); 25 | delegate.onMessage(webSocket, text); 26 | } 27 | 28 | @Override 29 | public void onMessage(WebSocket webSocket, okio.ByteString bytes) { 30 | // ConsolePanel.appendLog("[WebSocket] onMessage(bytes): " + bytes.hex(), ConsolePanel.LogType.INFO); 31 | delegate.onMessage(webSocket, bytes); 32 | } 33 | 34 | @Override 35 | public void onClosing(WebSocket webSocket, int code, String reason) { 36 | ConsolePanel.appendLog("[WebSocket] onClosing: code=" + code + ", reason=" + reason, ConsolePanel.LogType.WARN); 37 | delegate.onClosing(webSocket, code, reason); 38 | } 39 | 40 | @Override 41 | public void onClosed(WebSocket webSocket, int code, String reason) { 42 | ConsolePanel.appendLog("[WebSocket] onClosed: code=" + code + ", reason=" + reason, ConsolePanel.LogType.DEBUG); 43 | delegate.onClosed(webSocket, code, reason); 44 | } 45 | 46 | @Override 47 | public void onFailure(WebSocket webSocket, Throwable t, Response response) { 48 | ConsolePanel.appendLog("[WebSocket] onFailure: " + (t != null ? t.getMessage() : "Unknown error"), ConsolePanel.LogType.ERROR); 49 | delegate.onFailure(webSocket, t, response); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/ClientCertificate.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * 客户端证书配置 9 | * 用于支持 mTLS (mutual TLS) 认证 10 | */ 11 | @Data 12 | public class ClientCertificate implements Serializable { 13 | private String id; // 唯一标识符 14 | private String name; // 证书配置名称(可选) 15 | private String host; // 主机名或域名匹配模式(支持通配符,如 *.example.com) 16 | private int port; // 端口号,0 表示匹配所有端口 17 | private String certPath; // 证书文件路径(PFX/P12 或 PEM 格式) 18 | private String certType; // 证书类型:PFX, PEM 19 | private String certPassword; // 证书密码(用于 PFX/P12) 20 | private String keyPath; // 私钥文件路径(PEM 格式时使用) 21 | private String keyPassword; // 私钥密码(可选) 22 | private boolean enabled; // 是否启用此证书配置 23 | private long createdAt; // 创建时间 24 | private long updatedAt; // 更新时间 25 | 26 | // 证书类型常量 27 | public static final String CERT_TYPE_PFX = "PFX"; 28 | public static final String CERT_TYPE_PEM = "PEM"; 29 | 30 | public ClientCertificate() { 31 | this.enabled = true; 32 | this.port = 0; // 默认匹配所有端口 33 | this.certType = CERT_TYPE_PFX; 34 | this.createdAt = System.currentTimeMillis(); 35 | this.updatedAt = System.currentTimeMillis(); 36 | } 37 | 38 | /** 39 | * 检查此证书是否匹配指定的主机和端口 40 | */ 41 | public boolean matches(String targetHost, int targetPort) { 42 | if (!enabled) { 43 | return false; 44 | } 45 | 46 | // 检查端口匹配 47 | if (port != 0 && port != targetPort) { 48 | return false; 49 | } 50 | 51 | // 检查主机名匹配 52 | if (host == null || host.trim().isEmpty()) { 53 | return false; 54 | } 55 | 56 | String pattern = host.trim().toLowerCase(); 57 | String target = targetHost.toLowerCase(); 58 | 59 | // 支持通配符匹配 60 | if (pattern.startsWith("*.")) { 61 | String domain = pattern.substring(2); 62 | // 匹配 "api.example.com" 或 "example.com" 本身 63 | return target.endsWith("." + domain) || target.equals(domain); 64 | } 65 | 66 | // 精确匹配 67 | return pattern.equals(target); 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /docs/FQA.MD: -------------------------------------------------------------------------------- 1 | ## OkHttpClient 连接池参数说明 2 | 3 | 在 Easy Postman 的 OkHttpClientManager 中,使用如下方式为每个 baseUri 配置连接池: 4 | 5 | ```java 6 | .connectionPool(new ConnectionPool(maxIdleConnections, keepAliveDuration, TimeUnit.SECONDS)) 7 | ``` 8 | 9 | ### 参数解释 10 | - **maxIdleConnections**:最大空闲连接数。表示连接池中允许保持空闲状态的最大连接数量。 11 | - **keepAliveDuration**:空闲连接的最大存活时间(单位由第三个参数指定)。超过该时间未被使用的空闲连接将被关闭。 12 | - **TimeUnit.SECONDS**:指定 keepAliveDuration 的时间单位为秒。 13 | 14 | ### maxIdleConnections 详解 15 | maxIdleConnections 控制同一主机(baseUri)下,连接池中最多可以有多少个空闲连接被复用。 16 | 17 | #### 举例说明 18 | 假设 maxIdleConnections 设置为 6: 19 | - 如果你对同一个服务端(如 https://api.example.com)并发发起 10 个请求,连接池最多只会保留 6 个空闲连接。 20 | - 其余的连接在空闲后会被关闭,避免资源浪费。 21 | - 这样可以提升高并发下的性能,同时防止过多无用连接占用系统资源。 22 | 23 | #### 适用场景 24 | - **接口调试/开发环境**:通常 6 足够。 25 | - **性能压测/高并发场景**:可通过 setConnectionPoolConfig 动态调大 maxIdleConnections,例如设置为 50 或 100,以适应更多并发连接需求。 26 | 27 | ### maxIdleConnections 详解(更详细流程说明) 28 | 29 | 假设 maxIdleConnections 设置为 6,keepAliveDuration 设置为 90 秒,场景如下: 30 | 31 | #### 并发发起 10 个请求的全过程 32 | 33 | 1. **请求发起阶段** 34 | 客户端(如 Easy Postman)同时向同一服务端(如 https://api.example.com)并发发起 10 个 HTTP 请求。 35 | OkHttp 会为每个请求分配一个连接(如果连接池中有可用空闲连接,则复用,否则新建连接)。 36 | 37 | 2. **请求处理与连接归还** 38 | 10 个请求处理完毕后,这 10 个连接会被归还到连接池。 39 | 连接池会检查当前空闲连接数是否超过 maxIdleConnections(本例为 6)。 40 | 41 | 3. **空闲连接管理** 42 | 如果归还后空闲连接数超过 6,连接池会立即关闭多余的空闲连接,只保留 6 个。 43 | 哪些连接被保留、哪些被关闭由 OkHttp 内部策略决定(通常是最近最活跃的连接被保留)。 44 | 45 | 4. **keepAliveDuration 的作用** 46 | 被保留的 6 个空闲连接会在连接池中等待复用。 47 | 如果在 keepAliveDuration(如 90 秒)内没有新的请求复用这些连接,这些空闲连接会被自动关闭,释放资源。 48 | 也就是说,**maxIdleConnections** 决定了“最多能保留多少个空闲连接”,**keepAliveDuration** 决定了“空闲连接最多能保留多久”。 49 | 50 | #### 关键参数的作用 51 | 52 | - **maxIdleConnections**:决定连接池中最多能保留多少个空闲连接。超过这个数,多余的空闲连接会被立即关闭。 53 | - **keepAliveDuration**:决定空闲连接最多能保留多长时间。超过这个时间未被复用的空闲连接会被自动关闭。 54 | 55 | #### 过程示意 56 | 57 | 1. 并发 10 个请求,建立 10 个连接。 58 | 2. 请求结束,10 个连接归还到连接池。 59 | 3. 连接池只保留 6 个空闲连接,立即关闭多余的 4 个。 60 | 4. 这 6 个空闲连接在 90 秒内如果没有被新请求复用,也会被自动关闭。 61 | 62 | #### 这样做的好处 63 | 64 | - 避免了大量无用连接长期占用系统资源。 65 | - 保证高并发下的性能和资源利用率。 66 | - 连接池参数可根据实际并发量灵活调整。 67 | 68 | ### 参考 69 | - OkHttp 官方文档:[ConnectionPool](https://square.github.io/okhttp/4.x/okhttp/okhttp3/-connection-pool/) 70 | - Chrome 浏览器默认每 host 6 个连接,Easy Postman 默认采用相同策略。 71 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/performance/component/JMeterTreeCellRenderer.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.performance.component; 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon; 4 | import com.laker.postman.panel.performance.model.JMeterTreeNode; 5 | 6 | import javax.swing.*; 7 | import javax.swing.tree.DefaultMutableTreeNode; 8 | import javax.swing.tree.DefaultTreeCellRenderer; 9 | import java.awt.*; 10 | 11 | public class JMeterTreeCellRenderer extends DefaultTreeCellRenderer { 12 | @Override 13 | public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { 14 | JLabel label = (JLabel) super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); 15 | DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; 16 | Object userObj = node.getUserObject(); 17 | if (userObj instanceof JMeterTreeNode jtNode) { 18 | // 设置图标 19 | switch (jtNode.type) { 20 | case THREAD_GROUP -> label.setIcon(new FlatSVGIcon("icons/user-group.svg", 16, 16)); 21 | case REQUEST -> label.setIcon(new FlatSVGIcon("icons/http.svg", 16, 16)); 22 | case ASSERTION -> label.setIcon(new FlatSVGIcon("icons/warning.svg", 16, 16)); 23 | case TIMER -> label.setIcon(new FlatSVGIcon("icons/time.svg", 16, 16)); 24 | case ROOT -> label.setIcon(new FlatSVGIcon("icons/computer.svg", 16, 16)); 25 | } 26 | 27 | // 根据启用状态设置样式 28 | String text = jtNode.name; 29 | Font font = label.getFont(); 30 | 31 | if (!jtNode.enabled) { 32 | Color disabledColor = new Color(150, 150, 150); 33 | if (!sel) { 34 | label.setForeground(disabledColor); 35 | } 36 | label.setFont(font.deriveFont(Font.ITALIC)); 37 | label.setText("" + text + ""); 38 | } else { 39 | // 启用状态:恢复正常样式 40 | label.setFont(font.deriveFont(Font.PLAIN)); 41 | label.setText(text); 42 | } 43 | } 44 | return label; 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/performance/model/JMeterTreeNode.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.performance.model; 2 | 3 | import com.laker.postman.model.HttpRequestItem; 4 | import com.laker.postman.panel.performance.assertion.AssertionData; 5 | import com.laker.postman.panel.performance.threadgroup.ThreadGroupData; 6 | import com.laker.postman.panel.performance.timer.TimerData; 7 | 8 | public class JMeterTreeNode { 9 | public String name; 10 | public NodeType type; 11 | public HttpRequestItem httpRequestItem; // 仅REQUEST节点用 12 | public ThreadGroupData threadGroupData; // 线程组数据 13 | public AssertionData assertionData; // 断言数据 14 | public TimerData timerData; // 定时器数据 15 | public boolean enabled = true; // 是否启用,默认启用 16 | 17 | public JMeterTreeNode(String name, NodeType type) { 18 | this.name = name; 19 | this.type = type; 20 | } 21 | 22 | public JMeterTreeNode(String name, NodeType type, Object data) { 23 | this.name = name; 24 | this.type = type; 25 | switch (type) { 26 | case THREAD_GROUP -> this.threadGroupData = (ThreadGroupData) data; 27 | case REQUEST -> this.httpRequestItem = (HttpRequestItem) data; 28 | case ASSERTION -> this.assertionData = (AssertionData) data; 29 | case TIMER -> this.timerData = (TimerData) data; 30 | } 31 | } 32 | 33 | public Object getNodeData() { 34 | return switch (type) { 35 | case THREAD_GROUP -> threadGroupData; 36 | case REQUEST -> httpRequestItem; 37 | case ASSERTION -> assertionData; 38 | case TIMER -> timerData; 39 | default -> null; 40 | }; 41 | } 42 | 43 | public void setNodeData(Object data) { 44 | switch (type) { 45 | case THREAD_GROUP -> this.threadGroupData = (ThreadGroupData) data; 46 | case REQUEST -> this.httpRequestItem = (com.laker.postman.model.HttpRequestItem) data; 47 | case ASSERTION -> this.assertionData = (AssertionData) data; 48 | case TIMER -> this.timerData = (TimerData) data; 49 | } 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return name; 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/collections/RequestsTabsService.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.collections; 2 | 3 | import cn.hutool.core.text.CharSequenceUtil; 4 | import com.laker.postman.common.SingletonFactory; 5 | import com.laker.postman.common.component.tab.ClosableTabComponent; 6 | import com.laker.postman.model.HttpRequestItem; 7 | import com.laker.postman.panel.collections.right.RequestEditPanel; 8 | import com.laker.postman.panel.collections.right.request.RequestEditSubPanel; 9 | import com.laker.postman.util.I18nUtil; 10 | import com.laker.postman.util.MessageKeys; 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | import javax.swing.*; 14 | import java.awt.*; 15 | 16 | @Slf4j 17 | public class RequestsTabsService { 18 | 19 | private RequestsTabsService() { 20 | // no-op 21 | } 22 | 23 | 24 | public static RequestEditSubPanel addTab(HttpRequestItem item) { 25 | String id = item.getId(); 26 | if (id == null || id.isEmpty()) { 27 | throw new IllegalArgumentException("Request item ID cannot be null or empty"); 28 | } 29 | RequestEditSubPanel subPanel = new RequestEditSubPanel(id, item.getProtocol()); 30 | subPanel.initPanelData(item); 31 | String tabTitle = CharSequenceUtil.isNotBlank(item.getName()) ? item.getName() : I18nUtil.getMessage(MessageKeys.NEW_REQUEST); 32 | JTabbedPane tabbedPane = SingletonFactory.getInstance(RequestEditPanel.class).getTabbedPane(); 33 | tabbedPane.addTab(tabTitle, subPanel); 34 | tabbedPane.setTabComponentAt(tabbedPane.getTabCount() - 1, new ClosableTabComponent(tabTitle, item.getProtocol())); 35 | tabbedPane.setSelectedIndex(tabbedPane.getTabCount() - 1); 36 | return subPanel; 37 | } 38 | 39 | public static void updateTabNew(RequestEditSubPanel panel, boolean isNew) { 40 | JTabbedPane tabbedPane = SingletonFactory.getInstance(RequestEditPanel.class).getTabbedPane(); 41 | int idx = tabbedPane.indexOfComponent(panel); 42 | if (idx < 0) return; 43 | Component tabComp = tabbedPane.getTabComponentAt(idx); 44 | if (tabComp instanceof ClosableTabComponent closable) { 45 | closable.setNewRequest(isNew); 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/util/JsonPathUtil.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.util; 2 | 3 | import cn.hutool.json.JSONArray; 4 | import cn.hutool.json.JSONObject; 5 | import cn.hutool.json.JSONUtil; 6 | import lombok.experimental.UtilityClass; 7 | 8 | @UtilityClass 9 | public class JsonPathUtil { 10 | 11 | /** 12 | * 从JSON对象中提取指定路径的值,支持简单的$.a.b[0].c格式 13 | * 14 | * @param jsonObj 可以是JSONObject/JSONArray/String 15 | * @param path 形如$.a.b[0].c 16 | * @return 提取到的值,找不到返回null 17 | */ 18 | public static String extractJsonPath(Object jsonObj, String path) { 19 | if (jsonObj == null || path == null || path.isEmpty()) return null; 20 | if (jsonObj instanceof String str) { 21 | if (!JSONUtil.isTypeJSON(str)) return null; 22 | jsonObj = JSONUtil.parse(str); 23 | } 24 | if (path.startsWith("$.")) path = path.substring(2); 25 | String[] segments = path.split("\\."); 26 | Object current = jsonObj; 27 | for (String segment : segments) { 28 | if (current == null) return null; 29 | if (segment.contains("[") && segment.contains("]")) { 30 | String arrayName = segment.substring(0, segment.indexOf("[")); 31 | String indexStr = segment.substring(segment.indexOf("[") + 1, segment.indexOf("]")); 32 | int index; 33 | try { 34 | index = Integer.parseInt(indexStr); 35 | } catch (Exception e) { 36 | return null; 37 | } 38 | if (current instanceof JSONObject obj) { 39 | Object arr = obj.get(arrayName); 40 | if (arr instanceof JSONArray jsonArr) { 41 | if (index >= 0 && index < jsonArr.size()) { 42 | current = jsonArr.get(index); 43 | } else return null; 44 | } else return null; 45 | } else return null; 46 | } else { 47 | if (current instanceof JSONObject obj) { 48 | current = obj.get(segment); 49 | } else return null; 50 | } 51 | } 52 | return current != null ? current.toString() : null; 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/component/list/EnvironmentListCellRenderer.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.component.list; 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon; 4 | import com.laker.postman.model.Environment; 5 | import com.laker.postman.model.EnvironmentItem; 6 | import com.laker.postman.service.EnvironmentService; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | 11 | import static com.laker.postman.util.JComponentUtils.ellipsisText; 12 | 13 | // 环境列表渲染器 14 | public class EnvironmentListCellRenderer extends DefaultListCellRenderer { 15 | @Override 16 | public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { 17 | JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); 18 | if (value instanceof EnvironmentItem item) { 19 | String envName = item.getEnvironment().getName(); // 获取环境名称 20 | label.setText(ellipsisText(envName, list)); // 超出宽度显示省略号 21 | label.setToolTipText(envName); // 超出显示tip 22 | Environment active = EnvironmentService.getActiveEnvironment(); 23 | 24 | // 判断是否是激活环境 25 | boolean isActive = active != null && active.getId().equals(item.getEnvironment().getId()); 26 | 27 | // 设置图标和样式 28 | if (isActive) { 29 | label.setIcon(new FlatSVGIcon("icons/check.svg", 16, 16)); 30 | // 激活环境使用加粗字体 31 | label.setFont(label.getFont().deriveFont(Font.BOLD)); 32 | } else { 33 | label.setIcon(new FlatSVGIcon("icons/nocheck.svg", 16, 16)); 34 | // 普通环境使用常规字体 35 | label.setFont(label.getFont().deriveFont(Font.PLAIN)); 36 | } 37 | 38 | // 设置边距和对齐 39 | label.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8)); 40 | label.setIconTextGap(10); // 增大icon和文字间距 41 | label.setHorizontalAlignment(SwingConstants.LEFT); 42 | label.setHorizontalTextPosition(SwingConstants.RIGHT); 43 | 44 | // 选中状态的样式增强 45 | if (isSelected) { 46 | label.setOpaque(true); 47 | } 48 | } 49 | 50 | return label; 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/collections/right/request/sub/SyntaxType.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.collections.right.request.sub; 2 | 3 | import org.fife.ui.rsyntaxtextarea.SyntaxConstants; 4 | 5 | /** 6 | * 语法类型枚举,统一管理语法高亮类型 7 | * 添加新的语法类型只需在此枚举中添加一项即可 8 | */ 9 | public enum SyntaxType { 10 | AUTO_DETECT("Auto Detect", null), 11 | JSON("JSON", SyntaxConstants.SYNTAX_STYLE_JSON), 12 | XML("XML", SyntaxConstants.SYNTAX_STYLE_XML), 13 | HTML("HTML", SyntaxConstants.SYNTAX_STYLE_HTML), 14 | JAVASCRIPT("JavaScript", SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT), 15 | CSS("CSS", SyntaxConstants.SYNTAX_STYLE_CSS), 16 | PLAIN_TEXT("Plain Text", SyntaxConstants.SYNTAX_STYLE_NONE); 17 | 18 | private final String displayName; 19 | private final String syntaxStyle; 20 | 21 | SyntaxType(String displayName, String syntaxStyle) { 22 | this.displayName = displayName; 23 | this.syntaxStyle = syntaxStyle; 24 | } 25 | 26 | public String getDisplayName() { 27 | return displayName; 28 | } 29 | 30 | public String getSyntaxStyle() { 31 | return syntaxStyle; 32 | } 33 | 34 | /** 35 | * 获取所有显示名称数组(用于下拉框) 36 | */ 37 | public static String[] getDisplayNames() { 38 | SyntaxType[] values = values(); 39 | String[] names = new String[values.length]; 40 | for (int i = 0; i < values.length; i++) { 41 | names[i] = values[i].displayName; 42 | } 43 | return names; 44 | } 45 | 46 | /** 47 | * 根据索引获取语法类型 48 | */ 49 | public static SyntaxType getByIndex(int index) { 50 | SyntaxType[] values = values(); 51 | if (index >= 0 && index < values.length) { 52 | return values[index]; 53 | } 54 | return AUTO_DETECT; 55 | } 56 | 57 | /** 58 | * 根据语法样式获取语法类型 59 | */ 60 | public static SyntaxType getBySyntaxStyle(String syntaxStyle) { 61 | if (syntaxStyle == null) { 62 | return AUTO_DETECT; 63 | } 64 | for (SyntaxType type : values()) { 65 | if (syntaxStyle.equals(type.syntaxStyle)) { 66 | return type; 67 | } 68 | } 69 | return AUTO_DETECT; 70 | } 71 | 72 | /** 73 | * 获取索引位置 74 | */ 75 | public int getIndex() { 76 | return ordinal(); 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/http/ssl/CertificateErrorParser.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.http.ssl; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | import java.security.cert.CertificateException; 6 | 7 | /** 8 | * 证书错误信息解析器 9 | * 负责将各种证书异常转换为易读的错误信息 10 | */ 11 | @Slf4j 12 | public class CertificateErrorParser { 13 | 14 | private CertificateErrorParser() { 15 | // 工具类,隐藏构造函数 16 | } 17 | 18 | /** 19 | * 解析证书异常,返回错误类型 20 | */ 21 | public static SSLException.SSLErrorType parseErrorType(CertificateException e) { 22 | String message = e.getMessage(); 23 | if (message == null) { 24 | return SSLException.SSLErrorType.UNKNOWN; 25 | } 26 | 27 | String lowerMessage = message.toLowerCase(); 28 | 29 | if (lowerMessage.contains("certificate expired") || lowerMessage.contains("notafter")) { 30 | return SSLException.SSLErrorType.CERTIFICATE_EXPIRED; 31 | } else if (lowerMessage.contains("notbefore") || lowerMessage.contains("not yet valid")) { 32 | return SSLException.SSLErrorType.CERTIFICATE_NOT_YET_VALID; 33 | } else if (lowerMessage.contains("unable to find valid certification path") || 34 | lowerMessage.contains("pkix path building failed")) { 35 | return SSLException.SSLErrorType.UNTRUSTED_CERTIFICATE; 36 | } else if (lowerMessage.contains("pkix path validation failed")) { 37 | return SSLException.SSLErrorType.PATH_VALIDATION_FAILED; 38 | } 39 | 40 | return SSLException.SSLErrorType.UNKNOWN; 41 | } 42 | 43 | /** 44 | * 提取用户友好的错误信息 45 | */ 46 | public static String extractUserFriendlyMessage(CertificateException e) { 47 | SSLException.SSLErrorType errorType = parseErrorType(e); 48 | 49 | return switch (errorType) { 50 | case CERTIFICATE_EXPIRED -> "Certificate has expired"; 51 | case CERTIFICATE_NOT_YET_VALID -> "Certificate is not yet valid"; 52 | case UNTRUSTED_CERTIFICATE -> "Certificate is not trusted (untrusted root certificate)"; 53 | case PATH_VALIDATION_FAILED -> "Certificate path validation failed"; 54 | default -> { 55 | String message = e.getMessage(); 56 | yield message != null ? message : "Certificate validation failed"; 57 | } 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/util/XmlUtil.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | import org.w3c.dom.Document; 5 | import javax.xml.parsers.DocumentBuilder; 6 | import javax.xml.parsers.DocumentBuilderFactory; 7 | import javax.xml.transform.OutputKeys; 8 | import javax.xml.transform.Transformer; 9 | import javax.xml.transform.TransformerFactory; 10 | import javax.xml.transform.dom.DOMSource; 11 | import javax.xml.transform.stream.StreamResult; 12 | import java.io.StringReader; 13 | import java.io.StringWriter; 14 | import org.xml.sax.InputSource; 15 | 16 | @UtilityClass 17 | public class XmlUtil { 18 | /** 19 | * 判断字符串是否为合法 XML 20 | */ 21 | public static boolean isXml(String str) { 22 | if (str == null || str.trim().isEmpty()) return false; 23 | try { 24 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 25 | factory.setNamespaceAware(true); 26 | DocumentBuilder builder = factory.newDocumentBuilder(); 27 | builder.parse(new InputSource(new StringReader(str))); 28 | return true; 29 | } catch (Exception e) { 30 | return false; 31 | } 32 | } 33 | 34 | /** 35 | * 格式化 XML 字符串,避免重复插入空白行 36 | */ 37 | public static String formatXml(String xml) { 38 | if (!isXml(xml)) return xml; 39 | try { 40 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 41 | factory.setIgnoringElementContentWhitespace(true); 42 | DocumentBuilder builder = factory.newDocumentBuilder(); 43 | Document document = builder.parse(new InputSource(new StringReader(xml))); 44 | 45 | TransformerFactory tf = TransformerFactory.newInstance(); 46 | Transformer transformer = tf.newTransformer(); 47 | transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); 48 | transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 49 | transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); 50 | 51 | StringWriter writer = new StringWriter(); 52 | transformer.transform(new DOMSource(document), new StreamResult(writer)); 53 | // 去除多余空白行 54 | String result = writer.toString().replaceAll("(?m)^\s*$", ""); 55 | return result.trim(); 56 | } catch (Exception e) { 57 | return xml; 58 | } 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/http/ssl/SSLValidationResult.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.http.ssl; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | /** 8 | * SSL验证结果封装类 9 | */ 10 | public class SSLValidationResult { 11 | private final boolean valid; 12 | private final List warnings; 13 | private final List errors; 14 | 15 | private SSLValidationResult(boolean valid, List warnings, List errors) { 16 | this.valid = valid; 17 | this.warnings = warnings != null ? new ArrayList<>(warnings) : new ArrayList<>(); 18 | this.errors = errors != null ? new ArrayList<>(errors) : new ArrayList<>(); 19 | } 20 | 21 | public static SSLValidationResult success() { 22 | return new SSLValidationResult(true, Collections.emptyList(), Collections.emptyList()); 23 | } 24 | 25 | public static SSLValidationResult successWithWarnings(List warnings) { 26 | return new SSLValidationResult(true, warnings, Collections.emptyList()); 27 | } 28 | 29 | public static SSLValidationResult failure(List errors) { 30 | return new SSLValidationResult(false, Collections.emptyList(), errors); 31 | } 32 | 33 | public static SSLValidationResult failureWithWarnings(List errors, List warnings) { 34 | return new SSLValidationResult(false, warnings, errors); 35 | } 36 | 37 | public boolean isValid() { 38 | return valid; 39 | } 40 | 41 | public boolean hasWarnings() { 42 | return !warnings.isEmpty(); 43 | } 44 | 45 | public boolean hasErrors() { 46 | return !errors.isEmpty(); 47 | } 48 | 49 | public List getWarnings() { 50 | return Collections.unmodifiableList(warnings); 51 | } 52 | 53 | public List getErrors() { 54 | return Collections.unmodifiableList(errors); 55 | } 56 | 57 | public String getSummary() { 58 | List messages = new ArrayList<>(); 59 | if (!errors.isEmpty()) { 60 | messages.addAll(errors); 61 | } 62 | if (!warnings.isEmpty()) { 63 | messages.addAll(warnings); 64 | } 65 | return String.join("; ", messages); 66 | } 67 | 68 | @Override 69 | public String toString() { 70 | return "SSLValidationResult{valid=" + valid + 71 | ", warnings=" + warnings.size() + 72 | ", errors=" + errors.size() + "}"; 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/workspace/components/ProgressPanel.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.workspace.components; 2 | 3 | import com.laker.postman.util.FontsUtil; 4 | import com.laker.postman.util.I18nUtil; 5 | import com.laker.postman.util.MessageKeys; 6 | import lombok.Getter; 7 | 8 | import javax.swing.*; 9 | import javax.swing.border.TitledBorder; 10 | import java.awt.*; 11 | 12 | /** 13 | * 进度显示面板公共组件 14 | * 用于显示操作进度和状态信息 15 | */ 16 | public class ProgressPanel extends JPanel { 17 | 18 | @Getter 19 | private JProgressBar progressBar; 20 | @Getter 21 | private JLabel statusLabel; 22 | 23 | public ProgressPanel(String title) { 24 | initComponents(); 25 | setupLayout(title); 26 | } 27 | 28 | private void initComponents() { 29 | progressBar = new JProgressBar(); 30 | progressBar.setStringPainted(true); 31 | progressBar.setString(I18nUtil.getMessage(MessageKeys.PROGRESS_PANEL_READY)); 32 | 33 | statusLabel = new JLabel(I18nUtil.getMessage(MessageKeys.PROGRESS_PANEL_FILL_CONFIG)); 34 | statusLabel.setFont(FontsUtil.getDefaultFont(Font.ITALIC, 11)); 35 | } 36 | 37 | private void setupLayout(String title) { 38 | setLayout(new BorderLayout()); 39 | setBorder(BorderFactory.createTitledBorder( 40 | BorderFactory.createEtchedBorder(), 41 | title, 42 | TitledBorder.LEFT, 43 | TitledBorder.TOP, 44 | FontsUtil.getDefaultFont(Font.BOLD, 12) 45 | )); 46 | 47 | // 状态标签 48 | JPanel statusPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); 49 | statusPanel.add(statusLabel); 50 | add(statusPanel, BorderLayout.NORTH); 51 | 52 | // 进度条 53 | add(progressBar, BorderLayout.CENTER); 54 | } 55 | 56 | /** 57 | * 更新进度 58 | */ 59 | public void updateProgress(int progress, String message) { 60 | progressBar.setValue(progress); 61 | if (message != null) { 62 | statusLabel.setText(message); 63 | } 64 | } 65 | 66 | /** 67 | * 设置进度文本 68 | */ 69 | public void setProgressText(String text) { 70 | progressBar.setString(text); 71 | } 72 | 73 | /** 74 | * 重置进度面板状态 75 | */ 76 | public void reset() { 77 | progressBar.setValue(0); 78 | progressBar.setString(I18nUtil.getMessage(MessageKeys.PROGRESS_PANEL_READY)); 79 | statusLabel.setText(I18nUtil.getMessage(MessageKeys.PROGRESS_PANEL_FILL_CONFIG)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/performance/threadgroup/ThreadGroupData.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.performance.threadgroup; 2 | 3 | import com.laker.postman.util.I18nUtil; 4 | import com.laker.postman.util.MessageKeys; 5 | 6 | /** 7 | * 线程组数据模型,支持多种线程模式 8 | */ 9 | public class ThreadGroupData { 10 | // 线程组类型 11 | public enum ThreadMode { 12 | FIXED(MessageKeys.THREADGROUP_MODE_FIXED), // 固定线程数 13 | RAMP_UP(MessageKeys.THREADGROUP_MODE_RAMP_UP), // 递增线程数 14 | SPIKE(MessageKeys.THREADGROUP_MODE_SPIKE), // 尖刺模式 15 | STAIRS(MessageKeys.THREADGROUP_MODE_STAIRS); // 阶梯模式 16 | 17 | private final String messageKey; 18 | 19 | ThreadMode(String messageKey) { 20 | this.messageKey = messageKey; 21 | } 22 | 23 | public String getDisplayName() { 24 | return I18nUtil.getMessage(messageKey); 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return getDisplayName(); 30 | } 31 | } 32 | 33 | // 公共属性 34 | public ThreadMode threadMode = ThreadMode.FIXED; // 默认固定线程数 35 | public int numThreads = 20; // 固定模式-默认用户数 36 | public int duration = 60; // 所有模式-默认持续时间(秒) 37 | public int loops = 1; // 固定模式-默认循环次数 38 | public boolean useTime = true; // 是否使用时间而不是循环次数 39 | 40 | // 递增模式属性 41 | public int rampUpStartThreads = 1; // 递增起始线程数 42 | public int rampUpEndThreads = 20; // 递增最终线程数 43 | public int rampUpTime = 30; // 递增时间(秒) 44 | public int rampUpDuration = 60; // 递增模式总测试持续时间(秒) 45 | 46 | // 尖刺模式属性 47 | public int spikeMinThreads = 1; // 尖刺最小线程数 48 | public int spikeMaxThreads = 20; // 尖刺最大线程数 49 | public int spikeRampUpTime = 20; // 尖刺上升时间(秒) 50 | public int spikeHoldTime = 15; // 尖刺保持时间(秒) 51 | public int spikeRampDownTime = 20; // 尖刺下降时间(秒) 52 | public int spikeDuration = 60; // 尖刺模式总测试持续时间(秒) 53 | 54 | 55 | // 阶梯模式属性 56 | public int stairsStartThreads = 5; // 阶梯起始线程数 57 | public int stairsEndThreads = 20; // 阶梯最终线程数 58 | public int stairsStep = 5; // 阶梯步长 59 | public int stairsHoldTime = 15; // 每阶段保持时间(秒) 60 | public int stairsDuration = 60; // 阶梯模式总测试持续时间(秒) 61 | } -------------------------------------------------------------------------------- /src/test/java/com/laker/postman/EditMenuDemo.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman; 2 | 3 | import java.awt.*; 4 | import javax.swing.*; 5 | import org.fife.ui.rtextarea.*; 6 | import org.fife.ui.rsyntaxtextarea.*; 7 | 8 | /** 9 | * A simple example showing how create an "Edit" menu from the same 10 | * actions used by RSyntaxTextArea.

11 | * 12 | * This example uses RSyntaxTextArea 3.0.5. 13 | */ 14 | public class EditMenuDemo extends JFrame { 15 | 16 | public EditMenuDemo() { 17 | 18 | JPanel cp = new JPanel(new BorderLayout()); 19 | 20 | RSyntaxTextArea textArea = new RSyntaxTextArea(20, 60); 21 | textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA); 22 | textArea.setCodeFoldingEnabled(true); 23 | RTextScrollPane sp = new RTextScrollPane(textArea); 24 | cp.add(sp); 25 | 26 | setJMenuBar(createMenuBar(textArea)); 27 | 28 | setContentPane(cp); 29 | setTitle("Edit Menu Demo"); 30 | setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 31 | pack(); 32 | setLocationRelativeTo(null); 33 | 34 | } 35 | 36 | private static JMenuBar createMenuBar(RSyntaxTextArea textArea) { 37 | 38 | JMenuBar menuBar = new JMenuBar(); 39 | 40 | JMenu editMenu = new JMenu("Edit"); 41 | editMenu.add(createMenuItem(RTextArea.getAction(RTextArea.UNDO_ACTION))); 42 | editMenu.add(createMenuItem(RTextArea.getAction(RTextArea.REDO_ACTION))); 43 | editMenu.addSeparator(); 44 | editMenu.add(createMenuItem(RTextArea.getAction(RTextArea.CUT_ACTION))); 45 | editMenu.add(createMenuItem(RTextArea.getAction(RTextArea.COPY_ACTION))); 46 | editMenu.add(createMenuItem(RTextArea.getAction(RTextArea.PASTE_ACTION))); 47 | editMenu.add(createMenuItem(RTextArea.getAction(RTextArea.DELETE_ACTION))); 48 | editMenu.addSeparator(); 49 | editMenu.add(createMenuItem(RTextArea.getAction(RTextArea.SELECT_ALL_ACTION))); 50 | menuBar.add(editMenu); 51 | 52 | return menuBar; 53 | } 54 | 55 | private static JMenuItem createMenuItem(Action action) { 56 | JMenuItem item = new JMenuItem(action); 57 | item.setToolTipText(null); // Swing annoyingly adds tool tip text to the menu item 58 | return item; 59 | } 60 | 61 | public static void main(String[] args) { 62 | // Start all Swing applications on the EDT. 63 | SwingUtilities.invokeLater(new Runnable() { 64 | public void run() { 65 | new EditMenuDemo().setVisible(true); 66 | } 67 | }); 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/js/ScriptFragment.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.js; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * 脚本片段 7 | *

8 | * 表示一个可执行的脚本片段,包含脚本内容和来源标识。 9 | * 主要用于脚本继承场景,区分脚本来自请求本身还是从父分组继承。 10 | *

11 | * 12 | *

使用场景:

13 | *
    14 | *
  • 分组级别的脚本继承
  • 15 | *
  • 合并多个来源的脚本
  • 16 | *
  • 脚本来源追踪和调试
  • 17 | *
18 | * 19 | *

示例:

20 | *
{@code
 21 |  * // 创建请求自身的脚本片段
 22 |  * ScriptFragment requestScript = new ScriptFragment(
 23 |  *     "pm.environment.set('token', 'abc123');",
 24 |  *     "Request: /api/login"
 25 |  * );
 26 |  *
 27 |  * // 创建继承的脚本片段
 28 |  * ScriptFragment inheritedScript = new ScriptFragment(
 29 |  *     "pm.request.headers.add({key: 'X-API-Key', value: pm.environment.get('apiKey')});",
 30 |  *     "Group: API Requests"
 31 |  * );
 32 |  *
 33 |  * // 合并脚本
 34 |  * List fragments = Arrays.asList(inheritedScript, requestScript);
 35 |  * String mergedScript = ScriptMerger.merge(fragments);
 36 |  * }
37 | * 38 | * @see ScriptMerger 39 | * @author laker 40 | */ 41 | @Getter 42 | public class ScriptFragment { 43 | /** 44 | * 脚本内容(JavaScript 代码) 45 | */ 46 | private final String content; 47 | 48 | /** 49 | * 脚本来源标识 50 | *

51 | * 用于标识脚本来源,便于调试和追踪。例如: 52 | *

    53 | *
  • "Request: /api/users"
  • 54 | *
  • "Group: API Requests"
  • 55 | *
  • "Inherited: Root Collection"
  • 56 | *
57 | *

58 | */ 59 | private final String source; 60 | 61 | /** 62 | * 构造脚本片段(带来源标识) 63 | * 64 | * @param source 脚本来源标识 65 | * @param content 脚本内容 66 | */ 67 | public ScriptFragment(String source, String content) { 68 | this.source = source; 69 | this.content = content; 70 | } 71 | 72 | /** 73 | * 构造脚本片段(无来源标识) 74 | * 75 | * @param content 脚本内容 76 | */ 77 | public ScriptFragment(String content) { 78 | this(null, content); 79 | } 80 | 81 | /** 82 | * 工厂方法:创建带来源标识的脚本片段 83 | * 84 | * @param source 脚本来源标识 85 | * @param content 脚本内容 86 | * @return 脚本片段实例 87 | */ 88 | public static ScriptFragment of(String source, String content) { 89 | return new ScriptFragment(source, content); 90 | } 91 | 92 | /** 93 | * 工厂方法:创建无来源标识的脚本片段 94 | * 95 | * @param content 脚本内容 96 | * @return 脚本片段实例 97 | */ 98 | public static ScriptFragment of(String content) { 99 | return new ScriptFragment(content); 100 | } 101 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/model/script/TemporaryVariablesApi.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.model.script; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * 临时变量 API (pm.variables) 8 | *

9 | * 用于存储请求生命周期内的临时变量,这些变量不会被持久化, 10 | * 只在当前请求执行过程中有效。支持从 CSV 数据驱动测试等场景注入数据。 11 | *

12 | * 13 | *

使用场景:

14 | *
    15 | *
  • 存储中间计算结果
  • 16 | *
  • 在前置和后置脚本之间传递数据
  • 17 | *
  • 访问 CSV 数据驱动测试的当前行数据
  • 18 | *
19 | */ 20 | public class TemporaryVariablesApi { 21 | /** 22 | * 变量存储 Map 23 | */ 24 | private final Map variablesMap = new LinkedHashMap<>(); 25 | 26 | /** 27 | * 设置临时变量 28 | * 29 | * @param key 变量名 30 | * @param value 变量值 31 | */ 32 | public void set(String key, String value) { 33 | if (key != null && value != null) { 34 | variablesMap.put(key, value); 35 | } 36 | } 37 | 38 | /** 39 | * 设置临时变量(支持任意类型,自动转换为字符串) 40 | * 解决JavaScript中传入数字等非String类型的问题 41 | * 42 | * @param key 变量名 43 | * @param value 变量值(任意类型) 44 | */ 45 | public void set(String key, Object value) { 46 | if (key != null && value != null) { 47 | variablesMap.put(key, String.valueOf(value)); 48 | } 49 | } 50 | 51 | /** 52 | * 获取临时变量 53 | * 54 | * @param key 变量名 55 | * @return 变量值,不存在则返回 null 56 | */ 57 | public String get(String key) { 58 | return variablesMap.get(key); 59 | } 60 | 61 | /** 62 | * 检查变量是否存在 63 | * 64 | * @param key 变量名 65 | * @return 是否存在 66 | */ 67 | public boolean has(String key) { 68 | return variablesMap.containsKey(key); 69 | } 70 | 71 | /** 72 | * 删除临时变量 73 | * 74 | * @param key 变量名 75 | */ 76 | public void unset(String key) { 77 | variablesMap.remove(key); 78 | } 79 | 80 | /** 81 | * 清空所有临时变量 82 | */ 83 | public void clear() { 84 | variablesMap.clear(); 85 | } 86 | 87 | /** 88 | * 批量设置临时变量(用于 CSV 数据注入) 89 | * 90 | * @param variables 变量 Map 91 | */ 92 | public void replaceAll(Map variables) { 93 | if (variables != null) { 94 | variablesMap.clear(); 95 | variablesMap.putAll(variables); 96 | } 97 | } 98 | 99 | /** 100 | * 获取所有临时变量 101 | * 102 | * @return 变量 Map 的副本 103 | */ 104 | public Map toObject() { 105 | return new java.util.LinkedHashMap<>(variablesMap); 106 | } 107 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/performance/timer/TimerPropertyPanel.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.performance.timer; 2 | 3 | import com.laker.postman.panel.performance.model.JMeterTreeNode; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | public class TimerPropertyPanel extends JPanel { 9 | private final JSpinner delaySpinner; 10 | private JMeterTreeNode currentNode; 11 | 12 | public TimerPropertyPanel() { 13 | setLayout(new GridBagLayout()); 14 | setMaximumSize(new Dimension(420, 120)); 15 | setPreferredSize(new Dimension(380, 100)); 16 | setBorder(BorderFactory.createEmptyBorder(18, 24, 18, 24)); 17 | GridBagConstraints gbc = new GridBagConstraints(); 18 | gbc.insets = new Insets(6, 6, 6, 6); 19 | gbc.anchor = GridBagConstraints.CENTER; 20 | gbc.fill = GridBagConstraints.NONE; 21 | gbc.gridx = 0; 22 | gbc.gridy = 0; 23 | gbc.gridwidth = 1; 24 | JLabel label = new JLabel("定时(ms):"); 25 | add(label, gbc); 26 | gbc.gridx = 1; 27 | gbc.insets = new Insets(6, 0, 6, 6); // 左间距为0,右间距为6 28 | delaySpinner = new JSpinner(new SpinnerNumberModel(1000, 0, 60000, 100)); 29 | delaySpinner.setPreferredSize(new Dimension(100, 28)); 30 | add(delaySpinner, gbc); 31 | // 帮助说明 32 | gbc.gridx = 0; 33 | gbc.gridy = 1; 34 | gbc.gridwidth = 2; 35 | gbc.insets = new Insets(6, 6, 6, 6); 36 | gbc.anchor = GridBagConstraints.CENTER; 37 | gbc.fill = GridBagConstraints.HORIZONTAL; 38 | JLabel helpLabel = new JLabel("定时器会在请求前延迟指定毫秒数,适用于接口限流、节奏控制等场景。"); 39 | helpLabel.setFont(helpLabel.getFont().deriveFont(Font.PLAIN, 12f)); 40 | add(helpLabel, gbc); 41 | // 占位撑满高度 42 | gbc.gridy = 2; 43 | gbc.weighty = 1.0; 44 | gbc.fill = GridBagConstraints.BOTH; 45 | gbc.gridx = 0; 46 | gbc.gridy = 2; 47 | add(Box.createVerticalGlue(), gbc); 48 | } 49 | 50 | public void setTimerData(JMeterTreeNode node) { 51 | this.currentNode = node; 52 | TimerData data = node.timerData; 53 | if (data == null) { 54 | data = new TimerData(); 55 | node.timerData = data; 56 | } 57 | delaySpinner.setValue(data.delayMs); 58 | } 59 | 60 | public void saveTimerData() { 61 | if (currentNode == null) return; 62 | TimerData data = currentNode.timerData; 63 | if (data == null) { 64 | data = new TimerData(); 65 | currentNode.timerData = data; 66 | } 67 | data.delayMs = (Integer) delaySpinner.getValue(); 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/update/asset/AssetFinder.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.update.asset; 2 | 3 | import cn.hutool.json.JSONArray; 4 | import cn.hutool.json.JSONObject; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | /** 8 | * 资源查找器 - 从发布信息中提取合适的下载链接 9 | */ 10 | @Slf4j 11 | public class AssetFinder { 12 | 13 | private static final String BROWSER_DOWNLOAD_URL = "browser_download_url"; 14 | 15 | 16 | /** 17 | * 根据扩展名查找资源 18 | * 19 | * @param assets 资源列表 20 | * @param extension 文件扩展名(如 ".dmg", ".exe") 21 | * @return 下载 URL,未找到返回 null 22 | */ 23 | public String findByExtension(JSONArray assets, String extension) { 24 | for (int i = 0; i < assets.size(); i++) { 25 | JSONObject asset = assets.getJSONObject(i); 26 | String name = asset.getStr("name"); 27 | if (name != null && name.endsWith(extension)) { 28 | String url = asset.getStr(BROWSER_DOWNLOAD_URL); 29 | log.debug("Found asset: {} -> {}", name, url); 30 | return url; 31 | } 32 | } 33 | return null; 34 | } 35 | 36 | /** 37 | * 根据模式查找资源 38 | * 39 | * @param assets 资源列表 40 | * @param pattern 匹配模式(如 "-portable.zip") 41 | * @return 下载 URL,未找到返回 null 42 | */ 43 | public String findByPattern(JSONArray assets, String pattern) { 44 | for (int i = 0; i < assets.size(); i++) { 45 | JSONObject asset = assets.getJSONObject(i); 46 | String name = asset.getStr("name"); 47 | if (name != null && name.contains(pattern)) { 48 | String url = asset.getStr(BROWSER_DOWNLOAD_URL); 49 | log.debug("Found asset by pattern '{}': {} -> {}", pattern, name, url); 50 | return url; 51 | } 52 | } 53 | return null; 54 | } 55 | 56 | /** 57 | * 查找通用 DMG 文件(不包含架构后缀) 58 | * 59 | * @param assets 资源列表 60 | * @return 下载 URL,未找到返回 null 61 | */ 62 | public String findGenericDmg(JSONArray assets) { 63 | for (int i = 0; i < assets.size(); i++) { 64 | JSONObject asset = assets.getJSONObject(i); 65 | String name = asset.getStr("name"); 66 | if (name != null && name.endsWith(".dmg") && 67 | !name.endsWith("-intel.dmg") && 68 | !name.endsWith("-arm64.dmg") && 69 | !name.endsWith("-macos-x86_64.dmg") && 70 | !name.endsWith("-macos-arm64.dmg")) { 71 | String url = asset.getStr(BROWSER_DOWNLOAD_URL); 72 | log.debug("Found generic DMG (without architecture suffix): {} -> {}", name, url); 73 | return url; 74 | } 75 | } 76 | return null; 77 | } 78 | } -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/common/component/table/TableUIConstants.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.common.component.table; 2 | 3 | import com.laker.postman.common.constants.ModernColors; 4 | 5 | import javax.swing.*; 6 | import javax.swing.border.Border; 7 | import java.awt.*; 8 | 9 | /** 10 | * 表格UI常量类 11 | * 集中管理表格相关的UI常量,便于统一修改和维护 12 | */ 13 | public class TableUIConstants { 14 | // 文本常量 15 | public static final String SELECT_FILE_TEXT = "Select File"; 16 | public static final String FILE_TYPE = "File"; 17 | 18 | // 颜色常量 - 基础色 19 | public static final Color PRIMARY_COLOR = new Color(66, 133, 244); // 主题色 20 | 21 | // 背景色 22 | public static final Color ZEBRA_LIGHT = new Color(250, 252, 255); 23 | public static final Color ZEBRA_DARK = Color.WHITE; 24 | 25 | // 文本颜色 26 | public static final Color TEXT_SECONDARY = new Color(95, 99, 104); // 次要文本 27 | public static final Color TEXT_DISABLED = new Color(155, 155, 155); // 禁用文本 28 | 29 | // 边框和交互颜色 30 | public static final Color BORDER_COLOR = new Color(220, 225, 230); 31 | public static final Color HOVER_COLOR = new Color(230, 240, 255); 32 | 33 | // 特定用途颜色 34 | public static final Color FILE_BUTTON_TEXT_COLOR = PRIMARY_COLOR; 35 | public static final Color FILE_SELECTED_TEXT_COLOR = new Color(76, 130, 206); 36 | public static final Color FILE_EMPTY_TEXT_COLOR = TEXT_SECONDARY; 37 | 38 | // 图标大小 39 | public static final int ICON_SIZE = 14; 40 | 41 | // 边距 42 | public static final int PADDING_LEFT = 8; 43 | public static final int PADDING_RIGHT = 8; 44 | public static final int PADDING_TOP = 2; 45 | public static final int PADDING_BOTTOM = 2; 46 | 47 | /** 48 | * 创建标准按钮边框 49 | */ 50 | public static Border createButtonBorder() { 51 | return BorderFactory.createCompoundBorder( 52 | BorderFactory.createLineBorder(BORDER_COLOR), 53 | BorderFactory.createEmptyBorder( 54 | PADDING_TOP, PADDING_LEFT, PADDING_BOTTOM, PADDING_RIGHT)); 55 | } 56 | 57 | /** 58 | * 创建标准标签边框 59 | */ 60 | public static Border createLabelBorder() { 61 | return BorderFactory.createEmptyBorder( 62 | PADDING_TOP, PADDING_LEFT, PADDING_BOTTOM, PADDING_RIGHT); 63 | } 64 | 65 | /** 66 | * 获取单元格背景色 67 | */ 68 | public static Color getCellBackground(boolean isSelected, boolean isHovered, boolean isEmpty, int row, 69 | JTable table) { 70 | if (isSelected) { 71 | return table.getSelectionBackground(); 72 | } else if (isHovered) { 73 | return HOVER_COLOR; 74 | } else if (isEmpty) { 75 | return ModernColors.EMPTY_CELL; 76 | } else { 77 | return table.getBackground(); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/service/js/ScriptExecutionResult.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.service.js; 2 | 3 | import com.laker.postman.model.script.TestResult; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | /** 11 | * 脚本执行结果 12 | * 封装脚本执行的结果信息,包括成功状态、测试结果、错误信息等 13 | */ 14 | @Getter 15 | @Builder 16 | public class ScriptExecutionResult { 17 | /** 18 | * 是否执行成功 19 | */ 20 | private final boolean success; 21 | 22 | /** 23 | * 测试结果列表(仅后置脚本) 24 | */ 25 | @Builder.Default 26 | private final List testResults = new ArrayList<>(); 27 | 28 | /** 29 | * 错误信息(如果执行失败) 30 | */ 31 | private final String errorMessage; 32 | 33 | /** 34 | * 异常对象(如果执行失败) 35 | */ 36 | private final Exception exception; 37 | 38 | /** 39 | * 创建成功的结果 40 | */ 41 | public static ScriptExecutionResult success() { 42 | return ScriptExecutionResult.builder() 43 | .success(true) 44 | .build(); 45 | } 46 | 47 | /** 48 | * 创建成功的结果(带测试结果) 49 | */ 50 | public static ScriptExecutionResult success(List testResults) { 51 | return ScriptExecutionResult.builder() 52 | .success(true) 53 | .testResults(testResults != null ? new ArrayList<>(testResults) : new ArrayList<>()) 54 | .build(); 55 | } 56 | 57 | /** 58 | * 创建失败的结果 59 | */ 60 | public static ScriptExecutionResult failure(String errorMessage, Exception exception) { 61 | return ScriptExecutionResult.builder() 62 | .success(false) 63 | .errorMessage(errorMessage) 64 | .exception(exception) 65 | .build(); 66 | } 67 | 68 | /** 69 | * 创建失败的结果(带测试结果) 70 | */ 71 | public static ScriptExecutionResult failure(String errorMessage, Exception exception, List testResults) { 72 | return ScriptExecutionResult.builder() 73 | .success(false) 74 | .errorMessage(errorMessage) 75 | .exception(exception) 76 | .testResults(testResults != null ? new ArrayList<>(testResults) : new ArrayList<>()) 77 | .build(); 78 | } 79 | 80 | /** 81 | * 是否有测试结果 82 | */ 83 | public boolean hasTestResults() { 84 | return !testResults.isEmpty(); 85 | } 86 | 87 | /** 88 | * 所有测试是否通过 89 | * 注意:如果没有测试,返回true(没有失败的测试) 90 | */ 91 | public boolean allTestsPassed() { 92 | // 如果没有测试,视为通过 93 | if (!hasTestResults()) { 94 | return true; 95 | } 96 | // 如果有测试,检查是否全部通过 97 | return testResults.stream().allMatch(test -> test.passed); 98 | } 99 | } 100 | 101 | -------------------------------------------------------------------------------- /src/main/java/com/laker/postman/panel/collections/left/action/TreeNodeCloner.java: -------------------------------------------------------------------------------- 1 | package com.laker.postman.panel.collections.left.action; 2 | 3 | import com.laker.postman.model.HttpRequestItem; 4 | import com.laker.postman.util.JsonUtil; 5 | import lombok.experimental.UtilityClass; 6 | 7 | import javax.swing.tree.DefaultMutableTreeNode; 8 | 9 | import static com.laker.postman.panel.collections.left.RequestCollectionsLeftPanel.GROUP; 10 | import static com.laker.postman.panel.collections.left.RequestCollectionsLeftPanel.REQUEST; 11 | 12 | /** 13 | * 树节点克隆工具类 14 | */ 15 | @UtilityClass 16 | public class TreeNodeCloner { 17 | 18 | /** 19 | * 深拷贝分组节点及其所有子节点 20 | */ 21 | public static DefaultMutableTreeNode deepCopyGroupNode(DefaultMutableTreeNode node) { 22 | Object userObj = node.getUserObject(); 23 | Object[] obj = userObj instanceof Object[] ? ((Object[]) userObj).clone() : null; 24 | DefaultMutableTreeNode copy = new DefaultMutableTreeNode(obj); 25 | 26 | for (int i = 0; i < node.getChildCount(); i++) { 27 | DefaultMutableTreeNode child = (DefaultMutableTreeNode) node.getChildAt(i); 28 | copy.add(copyChildNode(child)); 29 | } 30 | 31 | return copy; 32 | } 33 | 34 | /** 35 | * 拷贝子节点 36 | */ 37 | private static DefaultMutableTreeNode copyChildNode(DefaultMutableTreeNode child) { 38 | Object childUserObj = child.getUserObject(); 39 | if (!(childUserObj instanceof Object[] childObj)) { 40 | return new DefaultMutableTreeNode(childUserObj); 41 | } 42 | 43 | if (GROUP.equals(childObj[0])) { 44 | return deepCopyGroupNode(child); 45 | } else if (REQUEST.equals(childObj[0])) { 46 | return copyRequestNode(childObj); 47 | } 48 | 49 | return new DefaultMutableTreeNode(childUserObj); 50 | } 51 | 52 | /** 53 | * 深拷贝请求节点 54 | */ 55 | private static DefaultMutableTreeNode copyRequestNode(Object[] childObj) { 56 | HttpRequestItem item = (HttpRequestItem) childObj[1]; 57 | HttpRequestItem copyItem = JsonUtil.deepCopy(item, HttpRequestItem.class); 58 | copyItem.setId(java.util.UUID.randomUUID().toString()); 59 | Object[] reqObj = new Object[]{REQUEST, copyItem}; 60 | return new DefaultMutableTreeNode(reqObj); 61 | } 62 | 63 | /** 64 | * 递归克隆树节点(用于创建选择树) 65 | */ 66 | public static DefaultMutableTreeNode cloneTreeNode(DefaultMutableTreeNode node) { 67 | Object userObj = node.getUserObject(); 68 | DefaultMutableTreeNode copy = new DefaultMutableTreeNode( 69 | userObj instanceof Object[] ? ((Object[]) userObj).clone() : userObj 70 | ); 71 | 72 | for (int i = 0; i < node.getChildCount(); i++) { 73 | DefaultMutableTreeNode child = (DefaultMutableTreeNode) node.getChildAt(i); 74 | copy.add(cloneTreeNode(child)); 75 | } 76 | 77 | return copy; 78 | } 79 | } 80 | 81 | --------------------------------------------------------------------------------