From 793ded083bae19b6c7efd435a51edcf634c7ac1f Mon Sep 17 00:00:00 2001 From: slhaf Date: Mon, 24 Mar 2025 13:08:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=B2=E5=AE=9E=E7=8E=B0=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E6=B5=8F=E8=A7=88=E3=80=81=E8=A7=86=E9=A2=91=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E7=AD=94=E9=A2=98=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E4=B8=AD=E7=AE=80=E7=AD=94=E9=A2=98=E3=80=81=E5=88=A4=E6=96=AD?= =?UTF-8?q?=E9=A2=98=E4=BB=A5=E5=8F=8A=E5=A4=9A=E9=80=89=E9=A2=98=E7=9A=84?= =?UTF-8?q?=E7=AD=94=E6=A1=88=E5=A1=AB=E5=85=85=E5=BE=85=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?;=20=E9=83=A8=E5=88=86=E5=BC=82=E5=B8=B8=E6=8D=95=E8=8E=B7?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E9=87=8D=E5=81=9A=E7=9B=B8=E5=85=B3=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 38 ++ .idea/.gitignore | 8 + .idea/encodings.xml | 7 + .idea/misc.xml | 14 + .idea/vcs.xml | 6 + pom.xml | 78 ++++ src/main/java/work/slhaf/Main.java | 21 ++ src/main/java/work/slhaf/chat/ChatClient.java | 77 ++++ .../work/slhaf/chat/constant/Constant.java | 22 ++ .../java/work/slhaf/chat/pojo/ChatBody.java | 25 ++ .../work/slhaf/chat/pojo/ChatResponse.java | 16 + .../java/work/slhaf/chat/pojo/Message.java | 14 + .../slhaf/chat/pojo/PrimaryChatResponse.java | 111 ++++++ src/main/java/work/slhaf/config/AiConfig.java | 21 ++ src/main/java/work/slhaf/config/Config.java | 89 +++++ .../java/work/slhaf/config/CxstudyConfig.java | 22 ++ .../java/work/slhaf/config/OcrConfig.java | 20 ++ .../java/work/slhaf/pojo/OCRDataInfo.java | 186 ++++++++++ .../work/slhaf/task/cxstudy/CxstudyTask.java | 334 ++++++++++++++++++ .../task/cxstudy/pojo/answer/Answer.java | 4 + .../pojo/answer/MultiChoiceAnswer.java | 12 + .../pojo/answer/SingleChoiceAnswer.java | 10 + .../task/cxstudy/pojo/answer/TextAnswer.java | 10 + .../pojo/question/MultiChoiceQuestion.java | 13 + .../task/cxstudy/pojo/question/Question.java | 8 + .../pojo/question/SingleChoiceQuestion.java | 13 + .../cxstudy/pojo/question/TextQuestion.java | 10 + src/main/java/work/slhaf/util/AiUtil.java | 90 +++++ src/main/java/work/slhaf/util/OcrUtil.java | 84 +++++ src/test/java/ProjectTest.java | 13 + 30 files changed, 1376 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/encodings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 pom.xml create mode 100644 src/main/java/work/slhaf/Main.java create mode 100644 src/main/java/work/slhaf/chat/ChatClient.java create mode 100644 src/main/java/work/slhaf/chat/constant/Constant.java create mode 100644 src/main/java/work/slhaf/chat/pojo/ChatBody.java create mode 100644 src/main/java/work/slhaf/chat/pojo/ChatResponse.java create mode 100644 src/main/java/work/slhaf/chat/pojo/Message.java create mode 100644 src/main/java/work/slhaf/chat/pojo/PrimaryChatResponse.java create mode 100644 src/main/java/work/slhaf/config/AiConfig.java create mode 100644 src/main/java/work/slhaf/config/Config.java create mode 100644 src/main/java/work/slhaf/config/CxstudyConfig.java create mode 100644 src/main/java/work/slhaf/config/OcrConfig.java create mode 100644 src/main/java/work/slhaf/pojo/OCRDataInfo.java create mode 100644 src/main/java/work/slhaf/task/cxstudy/CxstudyTask.java create mode 100644 src/main/java/work/slhaf/task/cxstudy/pojo/answer/Answer.java create mode 100644 src/main/java/work/slhaf/task/cxstudy/pojo/answer/MultiChoiceAnswer.java create mode 100644 src/main/java/work/slhaf/task/cxstudy/pojo/answer/SingleChoiceAnswer.java create mode 100644 src/main/java/work/slhaf/task/cxstudy/pojo/answer/TextAnswer.java create mode 100644 src/main/java/work/slhaf/task/cxstudy/pojo/question/MultiChoiceQuestion.java create mode 100644 src/main/java/work/slhaf/task/cxstudy/pojo/question/Question.java create mode 100644 src/main/java/work/slhaf/task/cxstudy/pojo/question/SingleChoiceQuestion.java create mode 100644 src/main/java/work/slhaf/task/cxstudy/pojo/question/TextQuestion.java create mode 100644 src/main/java/work/slhaf/util/AiUtil.java create mode 100644 src/main/java/work/slhaf/util/OcrUtil.java create mode 100644 src/test/java/ProjectTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..fdc35ea --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3837555 --- /dev/null +++ b/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + work.slhaf + CxstudyAuto + 1.0-SNAPSHOT + + + + cn.hutool + hutool-all + 5.8.36 + + + org.seleniumhq.selenium + selenium-java + 4.29.0 + + + org.slf4j + slf4j-api + 2.0.17 + + + ch.qos.logback + logback-classic + 1.5.17 + + + com.aliyun + ocr_api20210707 + 3.1.2 + + + io.github.bonigarcia + webdrivermanager + 5.9.3 + + + org.projectlombok + lombok + 1.18.36 + provided + + + junit + junit + RELEASE + test + + + junit + junit + 4.13.2 + test + + + + + + + 21 + 21 + UTF-8 + + + \ No newline at end of file diff --git a/src/main/java/work/slhaf/Main.java b/src/main/java/work/slhaf/Main.java new file mode 100644 index 0000000..18f0a37 --- /dev/null +++ b/src/main/java/work/slhaf/Main.java @@ -0,0 +1,21 @@ +package work.slhaf; + +import lombok.extern.slf4j.Slf4j; +import work.slhaf.config.Config; +import work.slhaf.task.cxstudy.CxstudyTask; +import work.slhaf.util.AiUtil; +import work.slhaf.util.OcrUtil; + +import java.io.IOException; + +@Slf4j +public class Main { + + public static void main(String[] args) throws InterruptedException, IOException { + Config config = Config.load(); + AiUtil.load(config); + OcrUtil.load(config); + CxstudyTask.run(config); + } + +} \ No newline at end of file diff --git a/src/main/java/work/slhaf/chat/ChatClient.java b/src/main/java/work/slhaf/chat/ChatClient.java new file mode 100644 index 0000000..c5e84ad --- /dev/null +++ b/src/main/java/work/slhaf/chat/ChatClient.java @@ -0,0 +1,77 @@ +package work.slhaf.chat; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONUtil; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import work.slhaf.chat.constant.Constant; +import work.slhaf.chat.pojo.ChatBody; +import work.slhaf.chat.pojo.ChatResponse; +import work.slhaf.chat.pojo.Message; +import work.slhaf.chat.pojo.PrimaryChatResponse; + +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +public class ChatClient { + private String clientId; + + private String url; + private String apikey; + private String model; + + private int top_p; + private int temperature; + private int max_tokens; + + public ChatClient(String url,String apikey,String model){ + this.url = url; + this.apikey = apikey; + this.model = model; + } + + public ChatResponse runChat(List messages){ + HttpRequest request = HttpRequest.post(url); + request.header("Content-Type", "application/json"); + request.header("Authorization", "Bearer " + apikey); + + ChatBody body; + if (top_p > 0) { + body = ChatBody.builder() + .model(model) + .messages(messages) + .top_p(top_p) + .temperature(temperature) + .max_tokens(max_tokens) + .build(); + }else { + body = ChatBody.builder() + .model(model) + .messages(messages) + .build(); + } + + HttpResponse response = request.body(JSONUtil.toJsonStr(body)).execute(); + ChatResponse finalResponse; + try { + PrimaryChatResponse primaryChatResponse = JSONUtil.toBean(response.body(), PrimaryChatResponse.class); + finalResponse = ChatResponse.builder() + .type(Constant.Response.SUCCESS) + .message(primaryChatResponse.getChoices().get(0).getMessage().getContent()) + .usageBean(primaryChatResponse.getUsage()) + .build(); + }catch (Exception e){ + finalResponse = ChatResponse.builder() + .type(Constant.Response.ERROR) + .message(response.getStatus()+": "+response.body()) + .build(); + } + response.close(); + return finalResponse; + } + +} diff --git a/src/main/java/work/slhaf/chat/constant/Constant.java b/src/main/java/work/slhaf/chat/constant/Constant.java new file mode 100644 index 0000000..ee18e9f --- /dev/null +++ b/src/main/java/work/slhaf/chat/constant/Constant.java @@ -0,0 +1,22 @@ +package work.slhaf.chat.constant; + +public class Constant { + + public static class Character { + public static final String USER = "user"; + public static final String SYSTEM = "system"; + public static final String ASSISTANT = "assistant"; + } + + public static class Model { + public static final String DEEP_SEEK_CHAT = "deepseek-chat"; + public static final String GLM_4_FLASH = "glm-4_flash"; + public static final String GLM_4_PLUS = "glm-4_plus"; + public static final String GLM_4_0520 = "glm-4_0520"; + } + + public static class Response { + public static final String SUCCESS = "success"; + public static final String ERROR = "error"; + } +} diff --git a/src/main/java/work/slhaf/chat/pojo/ChatBody.java b/src/main/java/work/slhaf/chat/pojo/ChatBody.java new file mode 100644 index 0000000..08cd23c --- /dev/null +++ b/src/main/java/work/slhaf/chat/pojo/ChatBody.java @@ -0,0 +1,25 @@ +package work.slhaf.chat.pojo; + +import lombok.*; + +import java.util.List; + +@Builder +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatBody { + @NonNull + private String model; + @NonNull + private List messages; + @Builder.Default + private int temperature = 1; + @Builder.Default + private int top_p = 1; + private boolean stream; + @Builder.Default + private int max_tokens = 1024; + private int presence_penalty; + private int frequency_penalty; +} diff --git a/src/main/java/work/slhaf/chat/pojo/ChatResponse.java b/src/main/java/work/slhaf/chat/pojo/ChatResponse.java new file mode 100644 index 0000000..8a4b5a7 --- /dev/null +++ b/src/main/java/work/slhaf/chat/pojo/ChatResponse.java @@ -0,0 +1,16 @@ +package work.slhaf.chat.pojo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ChatResponse { + private String type; + private String message; + private PrimaryChatResponse.UsageBean usageBean; +} diff --git a/src/main/java/work/slhaf/chat/pojo/Message.java b/src/main/java/work/slhaf/chat/pojo/Message.java new file mode 100644 index 0000000..c683fab --- /dev/null +++ b/src/main/java/work/slhaf/chat/pojo/Message.java @@ -0,0 +1,14 @@ +package work.slhaf.chat.pojo; + +import lombok.*; + +@Builder +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Message { + @NonNull + private String role; + @NonNull + private String content; +} diff --git a/src/main/java/work/slhaf/chat/pojo/PrimaryChatResponse.java b/src/main/java/work/slhaf/chat/pojo/PrimaryChatResponse.java new file mode 100644 index 0000000..4266579 --- /dev/null +++ b/src/main/java/work/slhaf/chat/pojo/PrimaryChatResponse.java @@ -0,0 +1,111 @@ +package work.slhaf.chat.pojo; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class PrimaryChatResponse { + + /** + * id + */ + private String id; + /** + * object + */ + private String object; + /** + * created + */ + private int created; + /** + * model + */ + private String model; + /** + * choices + */ + private List choices; + /** + * usage + */ + private UsageBean usage; + /** + * system_fingerprint + */ + private String system_fingerprint; + + @Setter + @Getter + public static class UsageBean { + /** + * prompt_tokens + */ + private int prompt_tokens; + /** + * completion_tokens + */ + private int completion_tokens; + /** + * total_tokens + */ + private int total_tokens; + /** + * prompt_cache_hit_tokens + */ + private int prompt_cache_hit_tokens; + /** + * prompt_cache_miss_tokens + */ + private int prompt_cache_miss_tokens; + + @Override + public String toString() { + return "UsageBean{" + + "prompt_tokens=" + prompt_tokens + + ", completion_tokens=" + completion_tokens + + ", total_tokens=" + total_tokens + + ", prompt_cache_hit_tokens=" + prompt_cache_hit_tokens + + ", prompt_cache_miss_tokens=" + prompt_cache_miss_tokens + + '}'; + } + } + + @Setter + @Getter + public static class ChoicesBean { + /** + * index + */ + private int index; + /** + * message + */ + private MessageBean message; + /** + * logprobs + */ + private Object logprobs; + /** + * finish_reason + */ + private String finish_reason; + + @Setter + @Getter + public static class MessageBean { + /** + * role + */ + private String role; + /** + * content + */ + private String content; + + } + } +} diff --git a/src/main/java/work/slhaf/config/AiConfig.java b/src/main/java/work/slhaf/config/AiConfig.java new file mode 100644 index 0000000..a6668f4 --- /dev/null +++ b/src/main/java/work/slhaf/config/AiConfig.java @@ -0,0 +1,21 @@ +package work.slhaf.config; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class AiConfig { + private String apikey; + private String model; + private String baseUrl; + + @Override + public String toString() { + return """ + AiConfig: + apikey: '%s' + model: '%s' + """.formatted(apikey, model); + } +} diff --git a/src/main/java/work/slhaf/config/Config.java b/src/main/java/work/slhaf/config/Config.java new file mode 100644 index 0000000..4c07286 --- /dev/null +++ b/src/main/java/work/slhaf/config/Config.java @@ -0,0 +1,89 @@ +package work.slhaf.config; + +import cn.hutool.json.JSONConfig; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Scanner; + +@Data +@Slf4j +public class Config { + private CxstudyConfig cxstudyConfig; + private OcrConfig ocrConfig; + private AiConfig aiConfig; + + private Config() { + } + + public static Config load() throws IOException { + File file = new File("./config.json"); + boolean configExists = checkConfigExists(file); + + if (!configExists) { + generateConfig(file); + } + + return loadFromFile(file); + } + + private static void generateConfig(File file) throws IOException { + Scanner sc = new Scanner(System.in); + System.out.println("\r\n---------------\r\n"); + AiConfig aiConfig = new AiConfig(); + System.out.println("recording ai config:"); + System.out.print("apikey: "); + aiConfig.setApikey(sc.nextLine()); + System.out.print("baseUrl: "); + aiConfig.setBaseUrl(sc.nextLine()); + System.out.print("model: "); + aiConfig.setModel(sc.nextLine()); + + System.out.println("\r\n---------------\r\n"); + OcrConfig ocrConfig = new OcrConfig(); + System.out.println("recording ocr config:"); + System.out.print("accessKeyId: "); + ocrConfig.setAccessKeyId(sc.nextLine()); + System.out.print("accessKeySecret: "); + ocrConfig.setAccessKeySecret(sc.nextLine()); + + System.out.println("\r\n---------------\r\n"); + CxstudyConfig studyConfig = new CxstudyConfig(); + System.out.println("recording study config:"); + System.out.print("phone: "); + studyConfig.setPhone(sc.nextLine()); + System.out.print("password: "); + studyConfig.setPassword(sc.nextLine()); + System.out.print("target: "); + studyConfig.setTarget(sc.nextLine()); + + System.out.println("\r\n---------------\r\n"); + Config config = new Config(); + config.cxstudyConfig = studyConfig; + config.ocrConfig = ocrConfig; + config.aiConfig = aiConfig; + String configStr = JSONUtil.toJsonPrettyStr(config); + System.out.println("this is your config file: \r\n" + configStr); + + System.out.println("press ENTER to generate config: "); + sc.nextLine(); + + file.createNewFile(); + FileUtils.writeStringToFile(file, configStr, StandardCharsets.UTF_8); + System.out.println(" config generated"); + } + + private static Config loadFromFile(File file) throws IOException { + return JSONUtil.toBean(FileUtils.readFileToString(file, StandardCharsets.UTF_8), Config.class); + } + + private static boolean checkConfigExists(File file) { + return file.exists(); + } +} diff --git a/src/main/java/work/slhaf/config/CxstudyConfig.java b/src/main/java/work/slhaf/config/CxstudyConfig.java new file mode 100644 index 0000000..efd04c9 --- /dev/null +++ b/src/main/java/work/slhaf/config/CxstudyConfig.java @@ -0,0 +1,22 @@ +package work.slhaf.config; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CxstudyConfig { + private String phone; + private String password; + private String target; + + @Override + public String toString() { + return """ + CxstudyConfig: + phone: '%s' + password: '%s' + target: '%s' + """.formatted(phone, password, target); + } +} diff --git a/src/main/java/work/slhaf/config/OcrConfig.java b/src/main/java/work/slhaf/config/OcrConfig.java new file mode 100644 index 0000000..6c77ae6 --- /dev/null +++ b/src/main/java/work/slhaf/config/OcrConfig.java @@ -0,0 +1,20 @@ +package work.slhaf.config; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class OcrConfig { + private String accessKeyId; + private String accessKeySecret; + + @Override + public String toString() { + return """ + OcrConfig: + accessKeyId: '%s' + accessKeySecret: '%s' + """.formatted(accessKeyId, accessKeySecret); + } +} diff --git a/src/main/java/work/slhaf/pojo/OCRDataInfo.java b/src/main/java/work/slhaf/pojo/OCRDataInfo.java new file mode 100644 index 0000000..cb41fbf --- /dev/null +++ b/src/main/java/work/slhaf/pojo/OCRDataInfo.java @@ -0,0 +1,186 @@ +package work.slhaf.pojo; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Setter +@Getter +public class OCRDataInfo { + + /** + * algo_version + */ + private String algo_version; + /** + * 文字内容-段落 + */ + private String content; + /** + * height + */ + private int height; + /** + * orgHeight + */ + private int orgHeight; + /** + * orgWidth + */ + private int orgWidth; + /** + * prism_version + */ + private String prism_version; + /** + * prism_wnum + */ + private int prism_wnum; + /** + * prism_wordsInfo + */ + private List prism_wordsInfo; + /** + * width + */ + private int width; + + public static class PrismWordsInfoBean { + /** + * angle + */ + private int angle; + /** + * direction + */ + private int direction; + /** + * height + */ + private int height; + /** + * pos + */ + private List pos; + /** + * prob + */ + private int prob; + /** + * width + */ + private int width; + /** + * 文字内容-行 + */ + private String word; + /** + * x + */ + private int x; + /** + * y + */ + private int y; + + public int getAngle() { + return angle; + } + + public void setAngle(int angle) { + this.angle = angle; + } + + public int getDirection() { + return direction; + } + + public void setDirection(int direction) { + this.direction = direction; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public List getPos() { + return pos; + } + + public void setPos(List pos) { + this.pos = pos; + } + + public int getProb() { + return prob; + } + + public void setProb(int prob) { + this.prob = prob; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public String getWord() { + return word; + } + + public void setWord(String word) { + this.word = word; + } + + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } + + public int getY() { + return y; + } + + public void setY(int y) { + this.y = y; + } + + public static class PosBean { + /** + * x + */ + private int x; + /** + * y + */ + private int y; + + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } + + public int getY() { + return y; + } + + public void setY(int y) { + this.y = y; + } + } + } +} diff --git a/src/main/java/work/slhaf/task/cxstudy/CxstudyTask.java b/src/main/java/work/slhaf/task/cxstudy/CxstudyTask.java new file mode 100644 index 0000000..b2a5e8a --- /dev/null +++ b/src/main/java/work/slhaf/task/cxstudy/CxstudyTask.java @@ -0,0 +1,334 @@ +package work.slhaf.task.cxstudy; + +import cn.hutool.json.JSONUtil; +import io.github.bonigarcia.wdm.WebDriverManager; +import lombok.extern.slf4j.Slf4j; +import org.openqa.selenium.*; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.interactions.Actions; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import work.slhaf.config.Config; +import work.slhaf.task.cxstudy.pojo.answer.Answer; +import work.slhaf.task.cxstudy.pojo.answer.MultiChoiceAnswer; +import work.slhaf.task.cxstudy.pojo.answer.SingleChoiceAnswer; +import work.slhaf.task.cxstudy.pojo.answer.TextAnswer; +import work.slhaf.task.cxstudy.pojo.question.MultiChoiceQuestion; +import work.slhaf.task.cxstudy.pojo.question.SingleChoiceQuestion; +import work.slhaf.task.cxstudy.pojo.question.Question; +import work.slhaf.task.cxstudy.pojo.question.TextQuestion; +import work.slhaf.util.AiUtil; +import work.slhaf.util.OcrUtil; + +import java.io.File; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Slf4j +public class CxstudyTask { + + static WebDriver driver; + static Actions actions; + static WebDriverWait wait; + + public static void run(Config config) throws InterruptedException { + + String phone = config.getCxstudyConfig().getPhone(); + String passwd = config.getCxstudyConfig().getPassword(); + String targetClassName = config.getCxstudyConfig().getTarget(); + + while (true) { + try { + //注册driver + initialize(); + + //登录、跳转页面 + loginAndSwitch(phone, passwd, targetClassName); + Thread.sleep(2000); + + List toBeFinished = getTodoTasks(); + int chapterCount = 0; + while (!toBeFinished.isEmpty()) { + //记录元素对应文本 + toBeFinished.getFirst().click(); + chapterCount++; + log.info("处理第{}节", chapterCount); +// toBeFinished.get(14).click(); + wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt("iframe")); + int subTasksNum = driver.findElements(By.xpath(".//div[@class='ans-attach-ct']")).size(); + for (int i = 0; i < subTasksNum; i++) { + //检测frame内部元素 + //frame分为三层: + // 第一层:index: 6 保存所有含有内容的frame + // 第二层:index: i 视频层 i->prev_video_left + // 第三层:index: 0->newTestTitle, 2->fileBox +// int taskContainersNum = driver.findElements(By.xpath(".//div[@class='ans-attach-ct']")).size(); +// log.info("发现{}个任务", taskContainersNum); +// for (int t = 0; t < taskContainersNum; t++) { +// handleVideoTask(i); + handleTestTask(i); +// handleDocTask(i); +// } + + } + + driver.get(driver.getCurrentUrl()); + driver.switchTo().defaultContent(); + toBeFinished = getTodoTasks(); + } + + driver.quit(); + break; + } catch (Exception e) { + log.error("\r\n---------------\r\n"); + log.error(e.getLocalizedMessage()); + log.error("\r\n---------------\r\n"); + driver.quit(); + log.error("Something went wrong, will retry later.."); + Thread.sleep(5000); + } + } + } + + + private static void handleDocTask(int i) { + try { + driver.switchTo().frame(i).switchTo().frame(2); + log.info("开始处理文档浏览..."); + JavascriptExecutor js = (JavascriptExecutor) driver; + long lastHeight = (long) js.executeScript("return document.body.scrollHeight;"); + long currentHeight = 0; + int step = 2000; + while (currentHeight < lastHeight) { + js.executeScript("window.scrollBy(0, arguments[0]);", step); + Thread.sleep(600); + currentHeight = (long) js.executeScript("return window.scrollY + window.innerHeight;"); + lastHeight = (long) js.executeScript("return document.body.scrollHeight;"); + } + log.info("文档浏览任务完成!"); + } catch (Exception ignored) { + } finally { + driver.switchTo().defaultContent(); + wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt("iframe")); + } + + } + + private static void handleTestTask(int i) { + try { + driver.switchTo().frame(i).switchTo().frame(0); + wait.until(ExpectedConditions.elementToBeClickable(By.xpath(".//div[@class='singleQuesId']"))); + log.info("开始处理测验..."); + //所有题目元素 + List questions = driver.findElements(By.xpath(".//div[@class='singleQuesId']")); + log.info("发现{}个题目...", questions.size()); + for (WebElement question : questions) { + log.info("处理第{}个题目...", questions.indexOf(question)); + //获取题目截图,通过OCR获取题目信息 + WebElement questionTitleElement = question.findElement(By.xpath(".//div[@class='TiMu newTiMu']")); + File screenshot = questionTitleElement.getScreenshotAs(OutputType.FILE); + + String questionTitle = OcrUtil.getContentOfImage(screenshot); + log.info("题目信息: {}", questionTitle); + try { + //尝试获取单选选项 + //li role='radio' + question.findElement(By.xpath(".//li[@role='radio']")); + List choiceElements = question.findElements(By.xpath(".//li[@role='radio']")); + SingleChoiceQuestion singleChoiceQuestion = new SingleChoiceQuestion(); + singleChoiceQuestion.setTitle(questionTitle); + List choices = getChoices(choiceElements); + singleChoiceQuestion.setChoices(choices); + Answer primaryAnswer = getAnswer(singleChoiceQuestion); + SingleChoiceAnswer answer = (SingleChoiceAnswer) primaryAnswer; + String answerContent = answer.getAnswer(); + log.info("答案: {}", answerContent); + String xpath = ".//span[@data='" + answerContent + "']"; + question.findElement(By.xpath(xpath)).findElement(By.xpath("..")).findElement(By.xpath("..")).click(); + continue; + } catch (Exception ignored) { + } + try { + //尝试获取复选框 + //li role='checkbox' + question.findElement(By.xpath(".//li[@role='checkbox']")); + List choiceElements = question.findElements(By.xpath(".//li[@role='checkbox']")); + MultiChoiceQuestion multiChoiceQuestion = new MultiChoiceQuestion(); + multiChoiceQuestion.setTitle(questionTitle); + List choices = getChoices(choiceElements); + multiChoiceQuestion.setChoices(choices); + + continue; + } catch (Exception ignored) { + } + try { + //尝试跳转到简答题输入框 + driver.switchTo().frame("ueditor_0"); + driver.findElement(By.tagName("p")).sendKeys("answer"); + + driver.switchTo().parentFrame(); + } catch (Exception ignored) { + } + } + log.info("答题任务完成!"); + } catch (Exception ignored) { + } finally { + driver.switchTo().defaultContent(); + wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt("iframe")); + } + } + + private static List getChoices(List choiceElements) { + List choices = new ArrayList<>(); + for (WebElement choiceElement : choiceElements) { + String label = choiceElement.findElement(By.tagName("label")).getText(); + String content = choiceElement.findElement(By.tagName("a")).getText(); + choices.add(String.format("%s. %s", label, content)); + } + return choices; + } + + private static Answer getAnswer(Question question) throws InterruptedException { + Answer answer = null; + int tryTimes = 0; + while (tryTimes < 3) { + try { + String questionStr = JSONUtil.toJsonPrettyStr(question); + String responseStr = AiUtil.runChat(questionStr); + // 使用正则表达式提取Markdown代码块中的JSON内容 + String jsonPattern = "```[\\s\\S]*?\\n([\\s\\S]*?)\\n```"; + Pattern pattern = Pattern.compile(jsonPattern); + Matcher matcher = pattern.matcher(responseStr); + String jsonStr = ""; + if (matcher.find()) { + jsonStr = matcher.group(1).trim(); + } + if (jsonStr.isEmpty()) { + throw new Exception("AI 回答格式不匹配: " + jsonStr); + } + if (question instanceof SingleChoiceQuestion) { + answer = JSONUtil.toBean(jsonStr, SingleChoiceAnswer.class); + } else if (question instanceof MultiChoiceQuestion) { + answer = JSONUtil.toBean(jsonStr, MultiChoiceAnswer.class); + } else if (question instanceof TextQuestion) { + answer = JSONUtil.toBean(jsonStr, TextAnswer.class); + } + break; + } catch (Exception e) { + log.error(e.getLocalizedMessage()); + log.error("3s后重试..."); + Thread.sleep(3000); + tryTimes++; + } + } + + return answer; + } + + private static void handleVideoTask(int i) { + try { + driver.switchTo().frame(i); + wait.until(ExpectedConditions.elementToBeClickable(By.xpath(".//div[@class='prev_video_left']"))); + log.info("开始处理视频播放..."); + WebElement play = wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//button[@class='vjs-big-play-button']"))); + Thread.sleep(2000); + play.click(); + WebElement video = driver.findElement(By.id("video_html5_api")); + //获取总时长 + String totalTime = driver.findElement(By.xpath("//span[@class='vjs-duration-display']")).getText(); + // 获取视频的位置信息 + Point location = video.getLocation(); + Dimension size = video.getSize(); + + int videoX = location.getX(); + int videoY = location.getY(); + int videoWidth = size.getWidth(); + int videoHeight = size.getHeight(); + + // 确定鼠标起始位置:进入视频区域 + int startX = videoX + videoWidth / 4; + int startY = videoY + videoHeight / 4; + actions.moveToElement(video, startX, startY).perform(); + Thread.sleep(500); + + int step = 10; // 每次移动步长 + + while (true) { + for (int j = 0; j < 20; j++) { + int offsetX = (int) (Math.random() * step) - step / 2; // 随机左右移动 + int offsetY = (int) (Math.random() * step) - step / 2; // 随机上下移动 + actions.moveByOffset(offsetX, offsetY).perform(); + Thread.sleep(200); + } + String currentTime = driver.findElement(By.xpath(".//span[@class='vjs-current-time-display']")).getText(); + if (totalTime.equals("0:00") || totalTime.isEmpty()) { + totalTime = driver.findElement(By.xpath("//span[@class='vjs-duration-display']")).getText(); + } else if (currentTime.equals(totalTime)) { + log.info(totalTime); + break; + } + log.info(currentTime + "/" + totalTime); + //恢复暂停 + try { + WebElement playingButton = driver.findElement(By.xpath(".//button[@class='vjs-play-control vjs-control vjs-button vjs-paused']")); + playingButton.click(); + } catch (Exception ignored) { + } + } + log.info("视频播放任务完成!"); + } catch (Exception ignored) { + } finally { + driver.switchTo().defaultContent(); + wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt("iframe")); + } + } + + private static List getTodoTasks() { + driver.switchTo().defaultContent(); + wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@class='posCatalog_select']"))); + List toBeFinished = new ArrayList<>(); + for (WebElement webElement : driver.findElements(By.xpath("//div[@class='posCatalog_select']"))) { + try { + webElement.findElement(By.xpath(".//span[@class='orangeNew']")); + toBeFinished.add(webElement); + } catch (Exception ignored) { + } + } + log.info("获取到{}个任务", toBeFinished.size()); + return toBeFinished; + } + + private static void loginAndSwitch(String phone, String passwd, String targetClassName) { + log.info("登录中..."); + driver.get("https://i.chaoxing.com/"); + WebElement phoneInput = driver.findElement(By.xpath("//input[@id='phone']")); + phoneInput.sendKeys(phone); + WebElement passwdInput = driver.findElement(By.xpath("//input[@id='pwd']")); + passwdInput.sendKeys(passwd); + WebElement submitInput = driver.findElement(By.xpath("//button[@id='loginBtn']")); + submitInput.click(); + + wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@name='课程']"))).click(); + wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(0)); + String xpath = ".//span[@title='" + targetClassName + "']"; + String targetClassHref = wait.until(ExpectedConditions.elementToBeClickable(By.xpath(xpath))).findElement(By.xpath("..")).getDomAttribute("href"); + driver.get(targetClassHref); + + driver.findElement(By.xpath("//a[@title='章节']")).click(); + wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(0)); + wait.until(ExpectedConditions.elementToBeClickable(By.xpath(".//div[@role='link']"))).findElement(By.xpath("..")).click(); + log.info("登录成功!"); + } + + private static void initialize() { + WebDriverManager.firefoxdriver().setup(); + + driver = new FirefoxDriver(); + actions = new Actions(driver); + wait = new WebDriverWait(driver, Duration.ofSeconds(5)); + log.info("webdriver创建完毕!"); + } +} diff --git a/src/main/java/work/slhaf/task/cxstudy/pojo/answer/Answer.java b/src/main/java/work/slhaf/task/cxstudy/pojo/answer/Answer.java new file mode 100644 index 0000000..1f4d433 --- /dev/null +++ b/src/main/java/work/slhaf/task/cxstudy/pojo/answer/Answer.java @@ -0,0 +1,4 @@ +package work.slhaf.task.cxstudy.pojo.answer; + +public class Answer { +} diff --git a/src/main/java/work/slhaf/task/cxstudy/pojo/answer/MultiChoiceAnswer.java b/src/main/java/work/slhaf/task/cxstudy/pojo/answer/MultiChoiceAnswer.java new file mode 100644 index 0000000..0e532ca --- /dev/null +++ b/src/main/java/work/slhaf/task/cxstudy/pojo/answer/MultiChoiceAnswer.java @@ -0,0 +1,12 @@ +package work.slhaf.task.cxstudy.pojo.answer; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +public class MultiChoiceAnswer extends Answer { + private List answers; +} diff --git a/src/main/java/work/slhaf/task/cxstudy/pojo/answer/SingleChoiceAnswer.java b/src/main/java/work/slhaf/task/cxstudy/pojo/answer/SingleChoiceAnswer.java new file mode 100644 index 0000000..ae79c73 --- /dev/null +++ b/src/main/java/work/slhaf/task/cxstudy/pojo/answer/SingleChoiceAnswer.java @@ -0,0 +1,10 @@ +package work.slhaf.task.cxstudy.pojo.answer; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class SingleChoiceAnswer extends Answer { + private String answer; +} diff --git a/src/main/java/work/slhaf/task/cxstudy/pojo/answer/TextAnswer.java b/src/main/java/work/slhaf/task/cxstudy/pojo/answer/TextAnswer.java new file mode 100644 index 0000000..ed0021a --- /dev/null +++ b/src/main/java/work/slhaf/task/cxstudy/pojo/answer/TextAnswer.java @@ -0,0 +1,10 @@ +package work.slhaf.task.cxstudy.pojo.answer; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class TextAnswer extends Answer { + private String answer; +} diff --git a/src/main/java/work/slhaf/task/cxstudy/pojo/question/MultiChoiceQuestion.java b/src/main/java/work/slhaf/task/cxstudy/pojo/question/MultiChoiceQuestion.java new file mode 100644 index 0000000..bb639ea --- /dev/null +++ b/src/main/java/work/slhaf/task/cxstudy/pojo/question/MultiChoiceQuestion.java @@ -0,0 +1,13 @@ +package work.slhaf.task.cxstudy.pojo.question; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +public class MultiChoiceQuestion extends Question { + private String type = "多选题"; + private List choices; +} diff --git a/src/main/java/work/slhaf/task/cxstudy/pojo/question/Question.java b/src/main/java/work/slhaf/task/cxstudy/pojo/question/Question.java new file mode 100644 index 0000000..39c2539 --- /dev/null +++ b/src/main/java/work/slhaf/task/cxstudy/pojo/question/Question.java @@ -0,0 +1,8 @@ +package work.slhaf.task.cxstudy.pojo.question; + +import lombok.Data; + +@Data +public class Question { + private String title; +} diff --git a/src/main/java/work/slhaf/task/cxstudy/pojo/question/SingleChoiceQuestion.java b/src/main/java/work/slhaf/task/cxstudy/pojo/question/SingleChoiceQuestion.java new file mode 100644 index 0000000..261278e --- /dev/null +++ b/src/main/java/work/slhaf/task/cxstudy/pojo/question/SingleChoiceQuestion.java @@ -0,0 +1,13 @@ +package work.slhaf.task.cxstudy.pojo.question; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +public class SingleChoiceQuestion extends Question { + private String type = "单选题题"; + private List choices; +} diff --git a/src/main/java/work/slhaf/task/cxstudy/pojo/question/TextQuestion.java b/src/main/java/work/slhaf/task/cxstudy/pojo/question/TextQuestion.java new file mode 100644 index 0000000..604f2e7 --- /dev/null +++ b/src/main/java/work/slhaf/task/cxstudy/pojo/question/TextQuestion.java @@ -0,0 +1,10 @@ +package work.slhaf.task.cxstudy.pojo.question; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class TextQuestion extends Question{ + private String type = "简答题"; +} diff --git a/src/main/java/work/slhaf/util/AiUtil.java b/src/main/java/work/slhaf/util/AiUtil.java new file mode 100644 index 0000000..8a54746 --- /dev/null +++ b/src/main/java/work/slhaf/util/AiUtil.java @@ -0,0 +1,90 @@ +package work.slhaf.util; + +import work.slhaf.chat.ChatClient; +import work.slhaf.chat.constant.Constant; +import work.slhaf.chat.pojo.ChatResponse; +import work.slhaf.chat.pojo.Message; +import work.slhaf.config.Config; + +import java.util.ArrayList; +import java.util.List; + +public class AiUtil { + private static ChatClient chatClient; + private static final String PROMPT = """ + 你是一个智能问卷系统助手。你将接收一个JSON格式的Question对象,需要根据Question的类型和内容,生成对应的Answer JSON,并将结果放入Markdown代码块中。 + + Question JSON可能包含以下类型: + 1. 简答题(TextQuestion):包含title字段和type字段 + 2. 单选题(SingleChoiceQuestion):包含title字段、type字段和choices字段 + 3. 多选题(MultiChoiceQuestion):包含title字段、type字段和choices字段 + + 请根据以下规则生成Answer JSON: + 1. 如果Question类型是"简答题",生成TextAnswer对象,包含answer字段 + 2. 如果Question类型是"单选题",生成SingleChoiceAnswer对象,包含answer字段,且answer必须是choices中的一个选项 + 3. 如果Question类型是"多选题",生成MultiChoiceAnswer对象,包含answers字段,且answers必须是choices中的多个选项(至少选择一个)。注意:choices的格式为"A. 答案1","B. 答案2"等,你只需要返回选项字母(如"A"、"B"),不需要返回选项内容 + + 示例1: + 输入: + { + "title": "请描述你的工作经历", + "type": "简答题" + } + 输出: + ```json + { + "answer": "我有5年Java开发经验,曾参与多个大型项目..." + } + ``` + + 示例2: + 输入: + { + "title": "你最喜欢的编程语言是?", + "type": "单选题", + "choices": ["A. Java", "B. Python", "C. C++"] + } + 输出: + ```json + { + "answer": "B" + } + ``` + + 示例3: + 输入: + { + "title": "你使用过哪些数据库?", + "type": "多选题", + "choices": ["A. MySQL", "B. MongoDB", "C. Redis"] + } + 输出: + ```json + { + "answers": ["A", "C"] + } + ``` + + 注意:两种选择题的选项内容(不包含序号)可能存在乱码,当输入的Question JSON为选择题时,你需要同时根据choices和title中的内容来确定正确选项。 + 请根据接收到的Question JSON,生成对应的Answer JSON,并将结果放入Markdown代码块中。确保答案内容合理且符合问题要求。 + """; + + private AiUtil() {} + + public static void load(Config config) { + String apikey = config.getAiConfig().getApikey(); + String model = config.getAiConfig().getModel(); + String baseUrl = config.getAiConfig().getBaseUrl(); + chatClient = new ChatClient(baseUrl, apikey, model); + } + + public static String runChat(String message) { + List messages = new ArrayList<>(); + Message system = new Message(Constant.Character.SYSTEM,PROMPT); + messages.add(system); + messages.add(new Message(Constant.Character.USER,message)); + + ChatResponse chatResponse = chatClient.runChat(messages); + return chatResponse.getMessage(); + } +} diff --git a/src/main/java/work/slhaf/util/OcrUtil.java b/src/main/java/work/slhaf/util/OcrUtil.java new file mode 100644 index 0000000..c8c09c1 --- /dev/null +++ b/src/main/java/work/slhaf/util/OcrUtil.java @@ -0,0 +1,84 @@ +package work.slhaf.util; + +import cn.hutool.json.JSONUtil; +import com.aliyun.ocr_api20210707.Client; +import com.aliyun.ocr_api20210707.models.RecognizeAdvancedRequest; +import com.aliyun.ocr_api20210707.models.RecognizeAdvancedResponse; +import com.aliyun.tea.TeaException; +import com.aliyun.teautil.models.RuntimeOptions; +import lombok.extern.slf4j.Slf4j; +import work.slhaf.config.Config; +import work.slhaf.pojo.OCRDataInfo; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +@Slf4j +public class OcrUtil { + private static Client client; + + private OcrUtil() {} + + public static void load(Config basicConfig) { + //读取密钥 + String accessKeyId = basicConfig.getOcrConfig().getAccessKeyId(); + String accessKeySecret = basicConfig.getOcrConfig().getAccessKeySecret(); + com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config() + .setAccessKeyId(accessKeyId) + .setAccessKeySecret(accessKeySecret); + config.endpoint = "ocr-api.cn-hangzhou.aliyuncs.com"; + try { + client = new com.aliyun.ocr_api20210707.Client(config); + } catch (Exception e) { + log.error("创建client出错"); + e.printStackTrace(); + } + log.info("OCR client已创建"); + + } + + public static String getContentOfImage(File file) throws FileNotFoundException { + //设置请求信息 + RecognizeAdvancedRequest recognizeAdvancedRequest = new RecognizeAdvancedRequest() + .setBody(new FileInputStream(file)) + .setNeedRotate(Boolean.TRUE) + .setNeedRotate(Boolean.TRUE); + + try { + //发送请求并处理回应 + RecognizeAdvancedResponse response = client.recognizeAdvancedWithOptions(recognizeAdvancedRequest, new RuntimeOptions()); + OCRDataInfo ocrDataInfo = JSONUtil.toBean(response.getBody().getData(), OCRDataInfo.class); + if (!ocrDataInfo.getContent().isEmpty()) { + StringBuilder str = new StringBuilder(); + ocrDataInfo.getPrism_wordsInfo().forEach(prismWordsInfoBean -> { + str.append(prismWordsInfoBean.getWord()); + if (!ocrDataInfo.getPrism_wordsInfo().get(ocrDataInfo.getPrism_wordsInfo().size() - 1).equals(prismWordsInfoBean)) { + str.append("\r\n"); + } + }); + return str.toString(); + } else { + return null; + } + } catch (TeaException error) { + // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。 + // 错误 message + log.error(error.getMessage()); + // 诊断地址 + log.error(error.getData().get("Recommend").toString()); + com.aliyun.teautil.Common.assertAsString(error.message); + return "ERROR"; + } catch (Exception _error) { + TeaException error = new TeaException(_error.getMessage(), _error); + // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。 + // 错误 message + log.error(error.getMessage()); + // 诊断地址 + log.error(error.getData().get("Recommend").toString()); + com.aliyun.teautil.Common.assertAsString(error.message); + return "ERROR"; + } + } + +} diff --git a/src/test/java/ProjectTest.java b/src/test/java/ProjectTest.java new file mode 100644 index 0000000..8be733d --- /dev/null +++ b/src/test/java/ProjectTest.java @@ -0,0 +1,13 @@ +import org.junit.Test; +import work.slhaf.config.Config; + +import java.io.IOException; + +public class ProjectTest { + + @Test + public void configTest() throws IOException { + Config config = Config.load(); + System.out.println(config.toString()); + } +}