├── .DS_Store
├── doc
├── env.png
├── 架构.png
├── 演示.png
├── ChatGPT交流.png
└── 架构图.drawio
├── src
├── .DS_Store
├── test
│ ├── .DS_Store
│ └── java
│ │ └── com
│ │ └── yupi
│ │ └── autoreply
│ │ ├── MainApplicationTests.java
│ │ └── api
│ │ ├── openai
│ │ └── OpenAiApiTest.java
│ │ └── zsxq
│ │ └── ZsxqApiTest.java
└── main
│ ├── resources
│ ├── banner.txt
│ ├── application.yml
│ └── META-INF
│ │ └── additional-spring-configuration-metadata.json
│ └── java
│ └── com
│ └── yupi
│ └── autoreply
│ ├── answerer
│ ├── Answerer.java
│ ├── DefaultAnswerer.java
│ └── OpenAiAnswerer.java
│ ├── controller
│ └── MainController.java
│ ├── api
│ ├── openai
│ │ ├── model
│ │ │ ├── ModelConstant.java
│ │ │ ├── CreateCompletionRequest.java
│ │ │ └── CreateCompletionResponse.java
│ │ └── OpenAiApi.java
│ └── zsxq
│ │ ├── model
│ │ ├── ListTopicsRequest.java
│ │ ├── AnswerRequest.java
│ │ ├── AnswerResponse.java
│ │ └── ListTopicsResponse.java
│ │ └── ZsxqApi.java
│ ├── model
│ └── TaskListItem.java
│ ├── monitor
│ ├── Monitor.java
│ ├── DefaultMonitor.java
│ └── ZsxqMonitor.java
│ ├── config
│ ├── ZsxqConfig.java
│ ├── OpenAiConfig.java
│ ├── CorsConfig.java
│ ├── Knife4jConfig.java
│ └── TaskConfig.java
│ ├── factory
│ ├── AnswererFactory.java
│ └── MonitorFactory.java
│ ├── common
│ ├── BaseResponse.java
│ ├── ErrorCode.java
│ └── ResultUtils.java
│ ├── exception
│ ├── BusinessException.java
│ ├── GlobalExceptionHandler.java
│ └── ThrowUtils.java
│ ├── job
│ └── JobMediator.java
│ ├── MainApplication.java
│ └── utils
│ └── SpringContextUtils.java
├── .env
├── .mvn
└── wrapper
│ └── maven-wrapper.properties
├── Dockerfile
├── README.md
├── .gitignore
├── pom.xml
├── mvnw.cmd
└── mvnw
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liyupi/yu-auto-reply/HEAD/.DS_Store
--------------------------------------------------------------------------------
/doc/env.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liyupi/yu-auto-reply/HEAD/doc/env.png
--------------------------------------------------------------------------------
/doc/架构.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liyupi/yu-auto-reply/HEAD/doc/架构.png
--------------------------------------------------------------------------------
/doc/演示.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liyupi/yu-auto-reply/HEAD/doc/演示.png
--------------------------------------------------------------------------------
/src/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liyupi/yu-auto-reply/HEAD/src/.DS_Store
--------------------------------------------------------------------------------
/doc/ChatGPT交流.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liyupi/yu-auto-reply/HEAD/doc/ChatGPT交流.png
--------------------------------------------------------------------------------
/src/test/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liyupi/yu-auto-reply/HEAD/src/test/.DS_Store
--------------------------------------------------------------------------------
/src/main/resources/banner.txt:
--------------------------------------------------------------------------------
1 |
2 | 🐟 防伪水印哈哈哈哈
3 | by 程序员鱼皮:https://github.com/liyupi
4 | 可能是最好的编程学习圈子:https://yupi.icu
5 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | OPENAI_MODEL=text-davinci-003
2 | OPENAI_API_KEY=你的API_KEY
3 | ZSXQ_COOKIE=你的星球Cookie
4 | ZSXQ_GROUP_ID=你的星球id
5 | ZSXQ_SILENCED=是否只提醒提问者
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar
3 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/answerer/Answerer.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.answerer;
2 |
3 | /**
4 | * 回答者
5 | *
6 | * @author 程序员鱼皮
7 | * @from 编程导航知识星球
8 | */
9 | public interface Answerer {
10 |
11 | /**
12 | * 回答
13 | *
14 | * @param prompt 提示语
15 | * @return 回答结果
16 | */
17 | String doAnswer(String prompt);
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/answerer/DefaultAnswerer.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.answerer;
2 |
3 | /**
4 | * 默认回答者(降级)
5 | *
6 | * @author 程序员鱼皮
7 | * @from 编程导航知识星球
8 | */
9 | public class DefaultAnswerer implements Answerer {
10 |
11 | @Override
12 | public String doAnswer(String prompt) {
13 | return "抱歉,我不理解您的问题:" + prompt;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/controller/MainController.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.controller;
2 |
3 | import lombok.extern.slf4j.Slf4j;
4 | import org.springframework.web.bind.annotation.*;
5 |
6 |
7 | /**
8 | * 开放接口
9 | *
10 | * @author 程序员鱼皮
11 | * @from 编程导航知识星球
12 | */
13 | @RestController
14 | @RequestMapping("/")
15 | @Slf4j
16 | public class MainController {
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/test/java/com/yupi/autoreply/MainApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.springframework.boot.test.context.SpringBootTest;
5 |
6 | /**
7 | * 主类测试
8 | *
9 | * @author 程序员鱼皮
10 | * @from 编程导航知识星球
11 | */
12 | @SpringBootTest
13 | class MainApplicationTests {
14 |
15 | @Test
16 | void contextLoads() {
17 |
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/api/openai/model/ModelConstant.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.api.openai.model;
2 |
3 | /**
4 | * 模型常量
5 | * 参考文档
6 | *
7 | * @author 程序员鱼皮
8 | * @from 编程导航知识星球
9 | */
10 | public interface ModelConstant {
11 |
12 | String GPT_4 = "gpt-4";
13 |
14 | String GPT_3_5_TURBO = "gpt-3.5-turbo";
15 |
16 | String TEXT_DAVINCI_003 = "text-davinci-003";
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/model/TaskListItem.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.model;
2 |
3 | import lombok.Data;
4 |
5 | /**
6 | * 单个任务配置
7 | */
8 | @Data
9 | public class TaskListItem {
10 |
11 | /**
12 | * 任务名
13 | */
14 | private String name = "";
15 |
16 | /**
17 | * 任务执行周期
18 | */
19 | private String cron = "0/30 * * * * ?";
20 |
21 | /**
22 | * 回答者
23 | */
24 | private String answerer = "openai";
25 |
26 | /**
27 | * 监控者
28 | */
29 | private String monitor = "zsxq";
30 | }
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/api/zsxq/model/ListTopicsRequest.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.api.zsxq.model;
2 |
3 | import lombok.Data;
4 |
5 | /**
6 | * 获取列表请求
7 | *
8 | * @author 程序员鱼皮
9 | * @from 编程导航知识星球
10 | */
11 | @Data
12 | public class ListTopicsRequest {
13 |
14 | /**
15 | * 星球 id
16 | */
17 | private String groupId;
18 |
19 | /**
20 | * 主题范围
21 | */
22 | private String scope;
23 |
24 | /**
25 | * 单页数量
26 | */
27 | private Integer count;
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM maven:3.8.1-jdk-8-slim as builder
2 |
3 | MAINTAINER yupi
4 |
5 | # Copy local code to the container image.
6 | WORKDIR /app
7 | COPY pom.xml .
8 | COPY src ./src
9 |
10 | # Build a release artifact.
11 | RUN mvn package -DskipTests
12 |
13 | # 声明环境变量,这样容器就可以在运行时访问它们
14 | ENV OPENAI_MODEL=text-davinci-003
15 | ENV OPENAI_API_KEY=你的API_KEY
16 | ENV ZSXQ_COOKIE=你的星球Cookie
17 | ENV ZSXQ_GROUP_ID=你的星球id
18 | # 是否只提醒提问者
19 | ENV ZSXQ_SILENCED=true
20 |
21 | # Run the web service on container startup.
22 | ENTRYPOINT ["java","-jar","/app/target/yu-auto-reply-0.0.1-SNAPSHOT.jar","--spring.profiles.active=prod"]
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/api/zsxq/model/AnswerRequest.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.api.zsxq.model;
2 |
3 | import lombok.Data;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * 回答请求
9 | *
10 | * @author 程序员鱼皮
11 | * @from 编程导航知识星球
12 | */
13 | @Data
14 | public class AnswerRequest {
15 |
16 | private String topicId;
17 |
18 | private ReqData req_data;
19 |
20 | @Data
21 | public static class ReqData {
22 |
23 | private String text;
24 |
25 | private List image_ids;
26 |
27 | private Boolean silenced;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/monitor/Monitor.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.monitor;
2 |
3 | import com.yupi.autoreply.answerer.Answerer;
4 | import com.yupi.autoreply.model.TaskListItem;
5 |
6 | /**
7 | * 抽象监控者
8 | *
9 | * @author 程序员鱼皮
10 | * @from 编程导航知识星球
11 | */
12 | public abstract class Monitor {
13 |
14 | TaskListItem taskListItem;
15 |
16 | Monitor(TaskListItem taskListItem) {
17 | this.taskListItem = taskListItem;
18 | }
19 |
20 | /**
21 | * 触发监控
22 | *
23 | * @param answerer
24 | */
25 | public abstract void onMonitor(Answerer answerer);
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/monitor/DefaultMonitor.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.monitor;
2 |
3 | import com.yupi.autoreply.answerer.Answerer;
4 | import com.yupi.autoreply.model.TaskListItem;
5 |
6 | /**
7 | * 默认监控者
8 | *
9 | * @author 程序员鱼皮
10 | * @from 编程导航知识星球
11 | */
12 | public class DefaultMonitor extends Monitor {
13 |
14 | public DefaultMonitor(TaskListItem taskListItem) {
15 | super(taskListItem);
16 | }
17 |
18 | @Override
19 | public void onMonitor(Answerer answerer) {
20 | String mockMessage = "我是一个新的消息";
21 | System.out.println(answerer.doAnswer(mockMessage));
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/config/ZsxqConfig.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.config;
2 |
3 | import lombok.Data;
4 | import org.springframework.boot.context.properties.ConfigurationProperties;
5 | import org.springframework.context.annotation.Configuration;
6 |
7 | /**
8 | * 知识星球配置
9 | *
10 | * @author 程序员鱼皮
11 | * @from 编程导航知识星球
12 | */
13 | @Configuration
14 | @ConfigurationProperties(prefix = "zsxq")
15 | @Data
16 | public class ZsxqConfig {
17 |
18 | /**
19 | * 登录 cookie
20 | */
21 | private String cookie;
22 |
23 | /**
24 | * 星球 id
25 | */
26 | private String groupId;
27 |
28 | /**
29 | * 是否提醒提问者
30 | */
31 | private Boolean silenced = true;
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/config/OpenAiConfig.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.config;
2 |
3 | import com.yupi.autoreply.api.openai.model.ModelConstant;
4 | import lombok.Data;
5 | import org.springframework.boot.context.properties.ConfigurationProperties;
6 | import org.springframework.context.annotation.Configuration;
7 |
8 | /**
9 | * OpenAi 配置
10 | *
11 | * @author 程序员鱼皮
12 | * @from 编程导航知识星球
13 | */
14 | @Configuration
15 | @ConfigurationProperties(prefix = "openai")
16 | @Data
17 | public class OpenAiConfig {
18 |
19 | /**
20 | * 模型
21 | */
22 | private String model = ModelConstant.TEXT_DAVINCI_003;
23 |
24 | /**
25 | * apiKey
26 | */
27 | private String apiKey;
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/factory/AnswererFactory.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.factory;
2 |
3 | import com.yupi.autoreply.answerer.Answerer;
4 | import com.yupi.autoreply.answerer.DefaultAnswerer;
5 | import com.yupi.autoreply.answerer.OpenAiAnswerer;
6 |
7 | /**
8 | * 回答者工厂
9 | *
10 | * @author 程序员鱼皮
11 | * @from 编程导航知识星球
12 | */
13 | public class AnswererFactory {
14 |
15 | /**
16 | * 创建回答者
17 | *
18 | * @param answerer
19 | * @return
20 | */
21 | public static Answerer createAnswerer(String answerer) {
22 | switch (answerer) {
23 | case "openai":
24 | return new OpenAiAnswerer();
25 | default:
26 | return new DefaultAnswerer();
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/api/openai/model/CreateCompletionRequest.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.api.openai.model;
2 |
3 | import lombok.Data;
4 |
5 | /**
6 | * 创建补全请求
7 | * 参考文档
8 | *
9 | * @author 程序员鱼皮
10 | * @from 编程导航知识星球
11 | */
12 | @Data
13 | public class CreateCompletionRequest {
14 |
15 | /**
16 | * 模型
17 | */
18 | private String model;
19 |
20 | /**
21 | * 提示词
22 | */
23 | private String prompt;
24 |
25 | private Integer max_tokens;
26 |
27 | private Integer temperature;
28 |
29 | private Integer top_p;
30 |
31 | private Integer n;
32 |
33 | private Boolean stream;
34 |
35 | private Integer logprobs;
36 |
37 | private String stop;
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/common/BaseResponse.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.common;
2 |
3 | import java.io.Serializable;
4 | import lombok.Data;
5 |
6 | /**
7 | * 通用返回类
8 | *
9 | * @param
10 | * @author 程序员鱼皮
11 | * @from 编程导航知识星球
12 | */
13 | @Data
14 | public class BaseResponse implements Serializable {
15 |
16 | private int code;
17 |
18 | private T data;
19 |
20 | private String message;
21 |
22 | public BaseResponse(int code, T data, String message) {
23 | this.code = code;
24 | this.data = data;
25 | this.message = message;
26 | }
27 |
28 | public BaseResponse(int code, T data) {
29 | this(code, data, "");
30 | }
31 |
32 | public BaseResponse(ErrorCode errorCode) {
33 | this(errorCode.getCode(), null, errorCode.getMessage());
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/factory/MonitorFactory.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.factory;
2 |
3 | import com.yupi.autoreply.model.TaskListItem;
4 | import com.yupi.autoreply.monitor.DefaultMonitor;
5 | import com.yupi.autoreply.monitor.Monitor;
6 | import com.yupi.autoreply.monitor.ZsxqMonitor;
7 |
8 | /**
9 | * 监视者工厂
10 | *
11 | * @author 程序员鱼皮
12 | * @from 编程导航知识星球
13 | */
14 | public class MonitorFactory {
15 |
16 | /**
17 | * 创建监视者
18 | *
19 | * @param monitor
20 | * @param taskListItem
21 | * @return
22 | */
23 | public static Monitor createMonitor(String monitor, TaskListItem taskListItem) {
24 | switch (monitor) {
25 | case "zsxq":
26 | return new ZsxqMonitor(taskListItem);
27 | default:
28 | return new DefaultMonitor(taskListItem);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/exception/BusinessException.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.exception;
2 |
3 | import com.yupi.autoreply.common.ErrorCode;
4 |
5 | /**
6 | * 自定义异常类
7 | *
8 | * @author 程序员鱼皮
9 | * @from 编程导航知识星球
10 | */
11 | public class BusinessException extends RuntimeException {
12 |
13 | /**
14 | * 错误码
15 | */
16 | private final int code;
17 |
18 | public BusinessException(int code, String message) {
19 | super(message);
20 | this.code = code;
21 | }
22 |
23 | public BusinessException(ErrorCode errorCode) {
24 | super(errorCode.getMessage());
25 | this.code = errorCode.getCode();
26 | }
27 |
28 | public BusinessException(ErrorCode errorCode, String message) {
29 | super(message);
30 | this.code = errorCode.getCode();
31 | }
32 |
33 | public int getCode() {
34 | return code;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/common/ErrorCode.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.common;
2 |
3 | /**
4 | * 自定义错误码
5 | *
6 | * @author 程序员鱼皮
7 | * @from 编程导航知识星球
8 | */
9 | public enum ErrorCode {
10 |
11 | SUCCESS(0, "ok"),
12 | PARAMS_ERROR(40000, "请求参数错误"),
13 | NOT_LOGIN_ERROR(40100, "未登录"),
14 | NO_AUTH_ERROR(40101, "无权限"),
15 | NOT_FOUND_ERROR(40400, "请求数据不存在"),
16 | FORBIDDEN_ERROR(40300, "禁止访问"),
17 | SYSTEM_ERROR(50000, "系统内部异常"),
18 | OPERATION_ERROR(50001, "操作失败");
19 |
20 | /**
21 | * 状态码
22 | */
23 | private final int code;
24 |
25 | /**
26 | * 信息
27 | */
28 | private final String message;
29 |
30 | ErrorCode(int code, String message) {
31 | this.code = code;
32 | this.message = message;
33 | }
34 |
35 | public int getCode() {
36 | return code;
37 | }
38 |
39 | public String getMessage() {
40 | return message;
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | application:
3 | name: yu-auto-reply
4 | # 支持 swagger3
5 | mvc:
6 | pathmatch:
7 | matching-strategy: ant_path_matcher
8 | server:
9 | address: 0.0.0.0
10 | port: 8080
11 | servlet:
12 | context-path: /api
13 | # openAI 配置
14 | # https://platform.openai.com/docs/api-reference
15 | openai:
16 | model: ${OPENAI_MODEL:text-davinci-003}
17 | apiKey: ${OPENAI_API_KEY:你的apiKey}
18 | # 知识星球配置
19 | # https://zsxq.com/
20 | zsxq:
21 | cookie: ${ZSXQ_COOKIE:你的星球cookie}
22 | groupId: ${ZSXQ_GROUP_ID:你的星球id}
23 | # 是否提醒提问者
24 | silenced: ${ZSXQ_SILENCED:true}
25 | # 任务配置
26 | task:
27 | # 并发
28 | concurrent:
29 | # 默认关闭(串行)
30 | enable: false
31 | # 并发大小(不填则等同于任务数,即全并发)
32 | size: 1
33 | # 任务列表,支持配置多个
34 | list:
35 | - name: task1 #任务名
36 | monitor: zsxq #监控者
37 | answerer: openai #回答者
38 | cron: '0/30 * * * * ?' #执行周期
39 | # - name: task2
40 | # monitor: default
41 | # answerer: default
42 | # cron: '0/10 * * * * ?'
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/config/CorsConfig.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.config;
2 |
3 | import org.springframework.context.annotation.Configuration;
4 | import org.springframework.web.servlet.config.annotation.CorsRegistry;
5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
6 |
7 | /**
8 | * 全局跨域配置
9 | *
10 | * @author 程序员鱼皮
11 | * @from 编程导航知识星球
12 | */
13 | @Configuration
14 | public class CorsConfig implements WebMvcConfigurer {
15 |
16 | @Override
17 | public void addCorsMappings(CorsRegistry registry) {
18 | // 覆盖所有请求
19 | registry.addMapping("/**")
20 | // 允许发送 Cookie
21 | .allowCredentials(true)
22 | // 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突)
23 | .allowedOriginPatterns("*")
24 | .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
25 | .allowedHeaders("*")
26 | .exposedHeaders("*");
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/job/JobMediator.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.job;
2 |
3 | import com.yupi.autoreply.answerer.Answerer;
4 | import com.yupi.autoreply.factory.AnswererFactory;
5 | import com.yupi.autoreply.factory.MonitorFactory;
6 | import com.yupi.autoreply.model.TaskListItem;
7 | import com.yupi.autoreply.monitor.Monitor;
8 |
9 | /**
10 | * 任务中介(负责协调监控者和回答者,把参数传给他们)
11 | *
12 | * @author 程序员鱼皮
13 | * @from 编程导航知识星球
14 | */
15 | public class JobMediator implements Runnable {
16 |
17 | private final TaskListItem taskListItem;
18 |
19 | public JobMediator(TaskListItem taskListItem) {
20 | this.taskListItem = taskListItem;
21 | }
22 |
23 | @Override
24 | public void run() {
25 | // 根据配置选择 monitor 和 answerer
26 | Monitor monitor = MonitorFactory.createMonitor(taskListItem.getMonitor(), taskListItem);
27 | Answerer answerer = AnswererFactory.createAnswerer(taskListItem.getAnswerer());
28 | // 监控并回答
29 | monitor.onMonitor(answerer);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/exception/GlobalExceptionHandler.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.exception;
2 |
3 | import com.yupi.autoreply.common.BaseResponse;
4 | import com.yupi.autoreply.common.ErrorCode;
5 | import com.yupi.autoreply.common.ResultUtils;
6 | import lombok.extern.slf4j.Slf4j;
7 | import org.springframework.web.bind.annotation.ExceptionHandler;
8 | import org.springframework.web.bind.annotation.RestControllerAdvice;
9 |
10 | /**
11 | * 全局异常处理器
12 | *
13 | * @author 程序员鱼皮
14 | * @from 编程导航知识星球
15 | */
16 | @RestControllerAdvice
17 | @Slf4j
18 | public class GlobalExceptionHandler {
19 |
20 | @ExceptionHandler(BusinessException.class)
21 | public BaseResponse> businessExceptionHandler(BusinessException e) {
22 | log.error("BusinessException", e);
23 | return ResultUtils.error(e.getCode(), e.getMessage());
24 | }
25 |
26 | @ExceptionHandler(RuntimeException.class)
27 | public BaseResponse> runtimeExceptionHandler(RuntimeException e) {
28 | log.error("RuntimeException", e);
29 | return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/api/openai/model/CreateCompletionResponse.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.api.openai.model;
2 |
3 | import lombok.Data;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * 创建补全响应
9 | * 参考文档
10 | *
11 | * @author 程序员鱼皮
12 | * @from 编程导航知识星球
13 | */
14 | @Data
15 | public class CreateCompletionResponse {
16 |
17 | private Integer created;
18 |
19 | private Usage usage;
20 |
21 | private String model;
22 |
23 | private String id;
24 |
25 | /**
26 | * 回答列表
27 | */
28 | private List choices;
29 |
30 | private String object;
31 |
32 | @Data
33 | public static class ChoicesItem {
34 |
35 | private String finishReason;
36 |
37 | private Integer index;
38 |
39 | private String text;
40 |
41 | private Integer logprobs;
42 | }
43 |
44 | @Data
45 | public static class Usage {
46 |
47 | private Integer completionTokens;
48 |
49 | private Integer promptTokens;
50 |
51 | private Integer totalTokens;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/test/java/com/yupi/autoreply/api/openai/OpenAiApiTest.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.api.openai;
2 |
3 | import com.yupi.autoreply.api.openai.model.CreateCompletionRequest;
4 | import com.yupi.autoreply.api.openai.model.CreateCompletionResponse;
5 | import org.junit.jupiter.api.Assertions;
6 | import org.junit.jupiter.api.Test;
7 | import org.springframework.boot.test.context.SpringBootTest;
8 |
9 | import javax.annotation.Resource;
10 |
11 | import static org.junit.jupiter.api.Assertions.*;
12 |
13 | /**
14 | * OpenAiApi 测试
15 | *
16 | * @author 程序员鱼皮
17 | * @from 编程导航知识星球
18 | **/
19 | @SpringBootTest
20 | class OpenAiApiTest {
21 |
22 | @Resource
23 | private OpenAiApi openAiApi;
24 |
25 | private static final String OPENAI_API_KEY = "你的 OPENAI_API_KEY";
26 |
27 | @Test
28 | void createCompletion() {
29 | CreateCompletionRequest request = new CreateCompletionRequest();
30 | request.setModel("text-davinci-003");
31 | request.setPrompt("我的提问");
32 | CreateCompletionResponse response = openAiApi.createCompletion(request, OPENAI_API_KEY);
33 | Assertions.assertNotNull(response);
34 | }
35 | }
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/exception/ThrowUtils.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.exception;
2 |
3 | import com.yupi.autoreply.common.ErrorCode;
4 |
5 | /**
6 | * 抛异常工具类
7 | *
8 | * @author 程序员鱼皮
9 | * @from 编程导航知识星球
10 | */
11 | public class ThrowUtils {
12 |
13 | /**
14 | * 条件成立则抛异常
15 | *
16 | * @param condition
17 | * @param runtimeException
18 | */
19 | public static void throwIf(boolean condition, RuntimeException runtimeException) {
20 | if (condition) {
21 | throw runtimeException;
22 | }
23 | }
24 |
25 | /**
26 | * 条件成立则抛异常
27 | *
28 | * @param condition
29 | * @param errorCode
30 | */
31 | public static void throwIf(boolean condition, ErrorCode errorCode) {
32 | throwIf(condition, new BusinessException(errorCode));
33 | }
34 |
35 | /**
36 | * 条件成立则抛异常
37 | *
38 | * @param condition
39 | * @param errorCode
40 | * @param message
41 | */
42 | public static void throwIf(boolean condition, ErrorCode errorCode, String message) {
43 | throwIf(condition, new BusinessException(errorCode, message));
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/common/ResultUtils.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.common;
2 |
3 | /**
4 | * 返回工具类
5 | *
6 | * @author 程序员鱼皮
7 | * @from 编程导航知识星球
8 | */
9 | public class ResultUtils {
10 |
11 | /**
12 | * 成功
13 | *
14 | * @param data
15 | * @param
16 | * @return
17 | */
18 | public static BaseResponse success(T data) {
19 | return new BaseResponse<>(0, data, "ok");
20 | }
21 |
22 | /**
23 | * 失败
24 | *
25 | * @param errorCode
26 | * @return
27 | */
28 | public static BaseResponse error(ErrorCode errorCode) {
29 | return new BaseResponse<>(errorCode);
30 | }
31 |
32 | /**
33 | * 失败
34 | *
35 | * @param code
36 | * @param message
37 | * @return
38 | */
39 | public static BaseResponse error(int code, String message) {
40 | return new BaseResponse(code, null, message);
41 | }
42 |
43 | /**
44 | * 失败
45 | *
46 | * @param errorCode
47 | * @return
48 | */
49 | public static BaseResponse error(ErrorCode errorCode, String message) {
50 | return new BaseResponse(errorCode.getCode(), null, message);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/MainApplication.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply;
2 |
3 | import com.yupi.autoreply.config.OpenAiConfig;
4 | import com.yupi.autoreply.config.ZsxqConfig;
5 | import com.yupi.autoreply.utils.SpringContextUtils;
6 | import lombok.extern.slf4j.Slf4j;
7 | import org.springframework.boot.CommandLineRunner;
8 | import org.springframework.boot.SpringApplication;
9 | import org.springframework.boot.autoconfigure.SpringBootApplication;
10 | import org.springframework.context.annotation.EnableAspectJAutoProxy;
11 | import org.springframework.scheduling.annotation.EnableScheduling;
12 |
13 | /**
14 | * 主类(项目启动入口)
15 | *
16 | * @author 程序员鱼皮
17 | * @from 编程导航知识星球
18 | */
19 | @SpringBootApplication
20 | @EnableScheduling
21 | @EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
22 | @Slf4j
23 | public class MainApplication implements CommandLineRunner {
24 |
25 | public static void main(String[] args) {
26 | SpringApplication.run(MainApplication.class, args);
27 | }
28 |
29 | @Override
30 | public void run(String... args) throws Exception {
31 | OpenAiConfig openAiConfig = SpringContextUtils.getBean(OpenAiConfig.class);
32 | ZsxqConfig zsxqConfig = SpringContextUtils.getBean(ZsxqConfig.class);
33 | log.info("OpenAi 配置 {}", zsxqConfig);
34 | log.info("知识星球配置 {}", openAiConfig);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/additional-spring-configuration-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "properties": [
3 | {
4 | "name": "openai.apiKey",
5 | "type": "java.lang.String",
6 | "description": "Description for openai.apiKey."
7 | },
8 | {
9 | "name": "zsxq.cookie",
10 | "type": "java.lang.String",
11 | "description": "Description for zsxq.cookie."
12 | },
13 | {
14 | "name": "openai.model",
15 | "type": "java.lang.String",
16 | "description": "Description for openai.model."
17 | },
18 | {
19 | "name": "zsxq.groupId",
20 | "type": "java.lang.String",
21 | "description": "Description for zsxq.groupId."
22 | },
23 | {
24 | "name": "zsxq.silenced",
25 | "type": "java.lang.String",
26 | "description": "Description for zsxq.silenced."
27 | },
28 | {
29 | "name": "task.cron",
30 | "type": "java.lang.String",
31 | "description": "Description for task.cron."
32 | },
33 | {
34 | "name": "task.list",
35 | "type": "java.lang.String",
36 | "description": "Description for task.list."
37 | },
38 | {
39 | "name": "task.concurrent.enable",
40 | "type": "java.lang.String",
41 | "description": "Description for task.concurrent.enable."
42 | },
43 | {
44 | "name": "task.concurrent.size",
45 | "type": "java.lang.String",
46 | "description": "Description for task.concurrent.size."
47 | }
48 | ]
49 | }
--------------------------------------------------------------------------------
/doc/架构图.drawio:
--------------------------------------------------------------------------------
1 | 7Zpbk5owFMc/TR7bAbkYHsFLu53ttFMf2n1MIQItEotx1f30PQlBReLOal2R6c7suJyTC+F/zi8XFFmD2fpDQebJZxbRDPWMaI2sIer1sOfCp3BsSodrmKUjLtKodO05JukTVU5DeZdpRBe1ipyxjKfzujNkeU5DXvORomCrerUpy+p3nZOYNhyTkGRN7/c04ol6rF5/5/9I0zip7my6XlkyI1Vl9SSLhERsteeyRsgaFIzx8mq2HtBMaFfpUrYbHyndDqygOX9Jg/HDwCfOn09+8rSkm693s1UevhMNRDePJFuqJ1aj5ZtKgoIt84iKXgxkBask5XQyJ6EoXUHMwZfwWQaWCZcRWSSyrjBU37TgdH101OZWC8ghymaUFxuoohrYWMmn8sf0lL3aRcO0lS/Zi4RVVSQqA+Jt3zuR4ELpdIpm5o1rZho3p1mF81GJWMETFrOcZPeMzZUWvyjnGzUnkCVnddnoOuU/9q4fRFfvHWUN16pnaWwqI4en+bFv7LUS5q6ZtKp25ehp1JgsDmICT8iWRUifk0LNYqSIKX+mnqePcUEzwtPH+jguH69mio/6yBsj30EjjPwxwi4aucjDwimKDIQt4cFjcT3ykNdH/kgWBcgzZdEI+f1/Q+USdJh1OrYT9D4dPQ0d7qvBYTZEEZk2UWbOcvgX/K+89LrAi2YZBQSCQAAieIHMtzvAgmW2zoJ9HRbOzOtzGLogC1YXWLA0LDgIOyiAJcOWULjCEwAgliwao8DoAB220Tod7lVXCuOclcJsbaWwu0CH3aDjy5zm4PHv4EPgICiQGyd/iLzmdHhtCmzv5vZLzanhVdeIEzP6HHouSIHTBQqcF+2Xbp6FG9gveW8sHFfH7QILroYFjDDkvC8QABY8X26cPIQ9DRSClz7CkhcM5/FmRrSNia17AXVdTPDbxum4Ov0uYNLXYKI7VhzSUZ4mHHXQ8K3bo6P9Y4VzVTo69gIKd4EOfNoi0jhr394i0jh9t76IeBqNQVEsZppyBsKBVN0Q01Lb+lkH3wJZTtv6VTc7SFLIO5iVS918V5+bpaSl2uDUboL0soOAvK7tghfsNx2wjBW7yW2aZtmBi2RpnIMZguAU/IEIRxqSzFcFszSKxG20wayH+yLxdOo82Jp4mpp4Wq8WT92bvuPROxKZU8OR0Sl/LhgLiEOax/ey2tDeeb4pSYSLQfNpJr+OT6AhhR6COUtzLjVyAvgD1QZinXFgrAOwzZ0Nf6J6wQcsh+GTVEaQkgVf0QWXweeEk5/ySdv4Atiuo+9qdhj4qpnSfOt1BsNvmXLxTHF6V8sUMHc/dpFle78YskZ/AQ==
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/config/Knife4jConfig.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.config;
2 |
3 | import org.springframework.context.annotation.Bean;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.context.annotation.Profile;
6 | import springfox.documentation.builders.ApiInfoBuilder;
7 | import springfox.documentation.builders.PathSelectors;
8 | import springfox.documentation.builders.RequestHandlerSelectors;
9 | import springfox.documentation.spi.DocumentationType;
10 | import springfox.documentation.spring.web.plugins.Docket;
11 | import springfox.documentation.swagger2.annotations.EnableSwagger2;
12 |
13 | /**
14 | * Knife4j 接口文档配置
15 | * 官方文档
16 | *
17 | * @author 程序员鱼皮
18 | * @from 编程导航知识星球
19 | */
20 | @Configuration
21 | @EnableSwagger2
22 | @Profile({"dev", "test"})
23 | public class Knife4jConfig {
24 |
25 | @Bean
26 | public Docket defaultApi2() {
27 | return new Docket(DocumentationType.SWAGGER_2)
28 | .apiInfo(new ApiInfoBuilder()
29 | .title("接口文档")
30 | .description("yu-auto-reply")
31 | .version("1.0")
32 | .build())
33 | .select()
34 | // 指定 Controller 扫描包路径
35 | .apis(RequestHandlerSelectors.basePackage("com.yupi.autoreply.controller"))
36 | .paths(PathSelectors.any())
37 | .build();
38 | }
39 | }
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/api/openai/OpenAiApi.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.api.openai;
2 |
3 | import cn.hutool.http.HttpRequest;
4 | import cn.hutool.json.JSONUtil;
5 | import com.yupi.autoreply.api.openai.model.CreateCompletionRequest;
6 | import com.yupi.autoreply.api.openai.model.CreateCompletionResponse;
7 | import com.yupi.autoreply.common.ErrorCode;
8 | import com.yupi.autoreply.exception.BusinessException;
9 | import org.apache.commons.lang3.StringUtils;
10 | import org.springframework.stereotype.Service;
11 |
12 | /**
13 | * OpenAi 接口
14 | * 参考文档
15 | *
16 | * @author 程序员鱼皮
17 | * @from 编程导航知识星球
18 | **/
19 | @Service
20 | public class OpenAiApi {
21 |
22 | /**
23 | * 补全
24 | *
25 | * @param request
26 | * @param openAiApiKey
27 | * @return
28 | */
29 | public CreateCompletionResponse createCompletion(CreateCompletionRequest request, String openAiApiKey) {
30 | if (StringUtils.isBlank(openAiApiKey)) {
31 | throw new BusinessException(ErrorCode.PARAMS_ERROR, "未传 openAiApiKey");
32 | }
33 | String url = "https://api.openai.com/v1/completions";
34 | String json = JSONUtil.toJsonStr(request);
35 | String result = HttpRequest.post(url)
36 | .header("Authorization", "Bearer " + openAiApiKey)
37 | .body(json)
38 | .execute()
39 | .body();
40 | return JSONUtil.toBean(result, CreateCompletionResponse.class);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/test/java/com/yupi/autoreply/api/zsxq/ZsxqApiTest.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.api.zsxq;
2 |
3 | import com.yupi.autoreply.api.zsxq.model.AnswerRequest;
4 | import com.yupi.autoreply.api.zsxq.model.AnswerResponse;
5 | import com.yupi.autoreply.api.zsxq.model.ListTopicsRequest;
6 | import com.yupi.autoreply.api.zsxq.model.ListTopicsResponse;
7 | import org.junit.jupiter.api.Assertions;
8 | import org.junit.jupiter.api.Test;
9 | import org.springframework.boot.test.context.SpringBootTest;
10 |
11 | import javax.annotation.Resource;
12 |
13 | /**
14 | * ZsxqApi 测试
15 | *
16 | * @author 程序员鱼皮
17 | * @from 编程导航知识星球
18 | **/
19 | @SpringBootTest
20 | class ZsxqApiTest {
21 |
22 | @Resource
23 | private ZsxqApi zsxqApi;
24 |
25 | private static final String COOKIE = "你的 COOKIE";
26 |
27 | @Test
28 | void listTopics() {
29 | ListTopicsRequest request = new ListTopicsRequest();
30 | request.setScope("unanswered_questions");
31 | request.setCount(30);
32 | request.setGroupId("知识星球id");
33 | ListTopicsResponse listTopicsResponse = zsxqApi.listTopics(request, COOKIE);
34 | Assertions.assertNotNull(listTopicsResponse);
35 | }
36 |
37 | @Test
38 | void answer() {
39 | AnswerRequest request = new AnswerRequest();
40 | request.setTopicId("问题id");
41 | AnswerRequest.ReqData reqData = new AnswerRequest.ReqData();
42 | reqData.setText("我的回答");
43 | reqData.setSilenced(true);
44 | request.setReq_data(reqData);
45 | AnswerResponse answerResponse = zsxqApi.answer(request, COOKIE);
46 | Assertions.assertNotNull(answerResponse);
47 | }
48 | }
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/utils/SpringContextUtils.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.utils;
2 |
3 | import org.springframework.beans.BeansException;
4 | import org.springframework.context.ApplicationContext;
5 | import org.springframework.context.ApplicationContextAware;
6 | import org.springframework.stereotype.Component;
7 |
8 | import javax.validation.constraints.NotNull;
9 |
10 | /**
11 | * Spring 上下文获取工具
12 | *
13 | * @author 程序员鱼皮
14 | * @from 编程导航知识星球
15 | */
16 | @Component
17 | public class SpringContextUtils implements ApplicationContextAware {
18 |
19 | private static ApplicationContext applicationContext;
20 |
21 | @Override
22 | public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
23 | SpringContextUtils.applicationContext = applicationContext;
24 | }
25 |
26 | /**
27 | * 通过名称获取 Bean
28 | *
29 | * @param beanName
30 | * @return
31 | */
32 | public static Object getBean(String beanName) {
33 | return applicationContext.getBean(beanName);
34 | }
35 |
36 | /**
37 | * 通过 class 获取 Bean
38 | *
39 | * @param beanClass
40 | * @param
41 | * @return
42 | */
43 | public static T getBean(Class beanClass) {
44 | return applicationContext.getBean(beanClass);
45 | }
46 |
47 | /**
48 | * 通过名称和类型获取 Bean
49 | *
50 | * @param beanName
51 | * @param beanClass
52 | * @param
53 | * @return
54 | */
55 | public static T getBean(String beanName, Class beanClass) {
56 | return applicationContext.getBean(beanName, beanClass);
57 | }
58 | }
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/answerer/OpenAiAnswerer.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.answerer;
2 |
3 | import com.yupi.autoreply.api.openai.OpenAiApi;
4 | import com.yupi.autoreply.api.openai.model.CreateCompletionRequest;
5 | import com.yupi.autoreply.api.openai.model.CreateCompletionResponse;
6 | import com.yupi.autoreply.config.OpenAiConfig;
7 | import com.yupi.autoreply.config.ZsxqConfig;
8 | import com.yupi.autoreply.utils.SpringContextUtils;
9 | import lombok.extern.slf4j.Slf4j;
10 |
11 | import javax.annotation.Resource;
12 | import java.util.List;
13 | import java.util.stream.Collectors;
14 |
15 | /**
16 | * OpenAi 回答者
17 | *
18 | * @author 程序员鱼皮
19 | * @from 编程导航知识星球
20 | */
21 | @Slf4j
22 | public class OpenAiAnswerer implements Answerer {
23 |
24 | private final OpenAiApi openAiApi = SpringContextUtils.getBean(OpenAiApi.class);
25 |
26 | private final OpenAiConfig openAiConfig = SpringContextUtils.getBean(OpenAiConfig.class);
27 |
28 | @Override
29 | public String doAnswer(String prompt) {
30 | CreateCompletionRequest request = new CreateCompletionRequest();
31 | request.setPrompt(prompt);
32 | request.setModel(openAiConfig.getModel());
33 | request.setTemperature(0);
34 | request.setMax_tokens(1024);
35 | CreateCompletionResponse response = openAiApi.createCompletion(request, openAiConfig.getApiKey());
36 | List choicesItemList = response.getChoices();
37 | String answer = choicesItemList.stream()
38 | .map(CreateCompletionResponse.ChoicesItem::getText)
39 | .collect(Collectors.joining());
40 | log.info("OpenAiAnswerer 回答成功 \n 答案:{}", answer);
41 | return answer;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/config/TaskConfig.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.config;
2 |
3 | import com.yupi.autoreply.job.JobMediator;
4 | import com.yupi.autoreply.model.TaskListItem;
5 | import lombok.Data;
6 | import lombok.extern.slf4j.Slf4j;
7 | import org.apache.commons.lang3.BooleanUtils;
8 | import org.apache.commons.lang3.StringUtils;
9 | import org.springframework.boot.context.properties.ConfigurationProperties;
10 | import org.springframework.context.annotation.Configuration;
11 | import org.springframework.scheduling.annotation.SchedulingConfigurer;
12 | import org.springframework.scheduling.config.ScheduledTaskRegistrar;
13 |
14 | import java.util.List;
15 | import java.util.Optional;
16 | import java.util.concurrent.Executors;
17 |
18 | /**
19 | * 任务配置
20 | *
21 | * @author 程序员鱼皮
22 | * @from 编程导航知识星球
23 | */
24 | @Configuration
25 | @ConfigurationProperties(prefix = "task")
26 | @Data
27 | @Slf4j
28 | public class TaskConfig implements SchedulingConfigurer {
29 |
30 | /**
31 | * 并发配置
32 | */
33 | private ConcurrentConfig concurrent = new ConcurrentConfig();
34 |
35 | /**
36 | * 任务列表
37 | */
38 | private List list;
39 |
40 | @Override
41 | public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
42 | if (BooleanUtils.isTrue(concurrent.getEnable())) {
43 | // 如果开启并发,默认并发度为任务数,即全并发,可通过配置更改
44 | int size = Optional.ofNullable(concurrent.getSize()).orElse(list.size());
45 | taskRegistrar.setScheduler(Executors.newScheduledThreadPool(size));
46 | }
47 | log.info("--- 任务注册开始 ---");
48 | for (int i = 0; i < list.size(); i++) {
49 | TaskListItem taskListItem = list.get(i);
50 | if (StringUtils.isBlank(taskListItem.getName())) {
51 | taskListItem.setName("task" + (i + 1));
52 | }
53 | taskRegistrar.addCronTask(new JobMediator(taskListItem), taskListItem.getCron());
54 | log.info("任务注册成功 {}", taskListItem);
55 | }
56 | log.info("--- 任务注册结果 ---");
57 | }
58 |
59 | @Data
60 | public static class ConcurrentConfig {
61 | private Boolean enable = false;
62 |
63 | private Integer size;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/api/zsxq/model/AnswerResponse.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.api.zsxq.model;
2 |
3 | import lombok.Data;
4 |
5 | /**
6 | * 回答响应
7 | *
8 | * @author 程序员鱼皮
9 | * @from 编程导航知识星球
10 | */
11 | @Data
12 | public class AnswerResponse {
13 |
14 | private RespData respData;
15 |
16 | private boolean succeeded;
17 |
18 | @Data
19 | public static class Group {
20 | private long groupId;
21 | private String name;
22 | private String type;
23 | }
24 |
25 | @Data
26 | public static class Owner {
27 | private String avatarUrl;
28 | private long userId;
29 | private String name;
30 | private String location;
31 | }
32 |
33 | @Data
34 | public static class OwnerDetail {
35 | private int questionsCount;
36 | private String joinTime;
37 | }
38 |
39 | @Data
40 | public static class Question {
41 | private Owner owner;
42 | private boolean expired;
43 | private Questionee questionee;
44 | private boolean anonymous;
45 | private OwnerDetail ownerDetail;
46 | private String ownerLocation;
47 | private String text;
48 | }
49 |
50 | @Data
51 | public static class Questionee {
52 | private String avatarUrl;
53 | private long userId;
54 | private String name;
55 | private String alias;
56 | private String description;
57 | private String location;
58 | }
59 |
60 | @Data
61 | public static class RespData {
62 | private TopicsItem topics;
63 | }
64 |
65 | @Data
66 | public static class TopicsItem {
67 | private int readingCount;
68 | private Question question;
69 | private boolean answered;
70 | private String createTime;
71 | private UserSpecific userSpecific;
72 | private int rewardsCount;
73 | private String type;
74 | private boolean digested;
75 | private int likesCount;
76 | private int commentsCount;
77 | private boolean sticky;
78 | private long topicId;
79 | private int readersCount;
80 | private Group group;
81 | }
82 |
83 | @Data
84 | public static class UserSpecific {
85 | private boolean subscribed;
86 | private boolean liked;
87 | }
88 | }
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/api/zsxq/model/ListTopicsResponse.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.api.zsxq.model;
2 |
3 | import lombok.Data;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * 获取列表响应
9 | *
10 | * @author 程序员鱼皮
11 | * @from 编程导航知识星球
12 | */
13 | @Data
14 | public class ListTopicsResponse {
15 |
16 | private RespData respData;
17 |
18 | private boolean succeeded;
19 |
20 | @Data
21 | public static class Group {
22 | private long groupId;
23 | private String name;
24 | private String type;
25 | }
26 |
27 | @Data
28 | public static class Owner {
29 | private String avatarUrl;
30 | private long userId;
31 | private String name;
32 | private String location;
33 | }
34 |
35 | @Data
36 | public static class OwnerDetail {
37 | private int questionsCount;
38 | private String joinTime;
39 | }
40 |
41 | @Data
42 | public static class Question {
43 | private Owner owner;
44 | private boolean expired;
45 | private Questionee questionee;
46 | private boolean anonymous;
47 | private OwnerDetail ownerDetail;
48 | private String ownerLocation;
49 | private String text;
50 | }
51 |
52 | @Data
53 | public static class Questionee {
54 | private String avatarUrl;
55 | private long userId;
56 | private String name;
57 | private String alias;
58 | private String description;
59 | private String location;
60 | }
61 |
62 | @Data
63 | public static class RespData {
64 | private List topics;
65 | }
66 |
67 | @Data
68 | public static class TopicsItem {
69 | private int readingCount;
70 | private Question question;
71 | private boolean answered;
72 | private String createTime;
73 | private UserSpecific userSpecific;
74 | private int rewardsCount;
75 | private String type;
76 | private boolean digested;
77 | private int likesCount;
78 | private int commentsCount;
79 | private boolean sticky;
80 | private String topicId;
81 | private int readersCount;
82 | private Group group;
83 | }
84 |
85 | @Data
86 | public static class UserSpecific {
87 | private boolean subscribed;
88 | private boolean liked;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # yu-auto-reply 自动回复
2 |
3 | > 作者:[程序员鱼皮](https://github.com/liyupi)
4 | >
5 | > 编程学习圈:[编程导航知识星球](https://yupi.icu)
6 |
7 | [toc]
8 |
9 | 基于 Java Spring Boot 的平台监控及自动回复工具,支持灵活地配置多个监控任务,支持一键部署!
10 |
11 | 演示视频:https://www.bilibili.com/video/BV1WX4y1o7aL
12 |
13 | 
14 |
15 | 本项目采用多种设计模式,解耦监控者及回答者,可以灵活配置多个不同平台的监控,并绑定不同类型的自动回复。
16 |
17 | 🙏🏻 大家喜欢这个项目的话,感谢动手点点 star,后面作者可能会官方提供更多的平台监控支持。
18 |
19 | ## 功能特性
20 |
21 | ### 监控能力
22 |
23 | - 知识星球提问监控
24 | - 默认监控(模拟数据)
25 |
26 | ### 回复能力
27 |
28 | - OpenAI 回答(支持自选模型,比如 gpt-4)
29 | - 默认监控(模拟数据)
30 |
31 | ### 配置能力
32 |
33 | - 支持配置多个任务
34 | - 每个任务可以灵活指定监控和回答方式
35 |
36 | ### 部署能力
37 |
38 | - 支持 Docker 容器化部署
39 | - 支持 Railway 一键部署
40 | - 支持动态指定环境变量来改变配置
41 |
42 | ## 快速启动
43 |
44 | 1)修改 `application.yml` 配置,主要包含 3 部分:
45 |
46 | - openAI 配置(需要有一个 API Key)
47 | - 知识星球配置(需要自行获取 cookie)
48 | - 任务配置
49 |
50 | 详细配置如下:
51 |
52 | ```yml
53 | # openAI 配置
54 | # https://platform.openai.com/docs/api-reference
55 | openai:
56 | model: ${OPENAI_MODEL:text-davinci-003}
57 | apiKey: ${OPENAI_API_KEY:你的apiKey}
58 | # 知识星球配置
59 | # https://zsxq.com/
60 | zsxq:
61 | cookie: ${ZSXQ_COOKIE:你的星球cookie}
62 | groupId: ${ZSXQ_GROUP_ID:你的星球id}
63 | # 是否提醒提问者
64 | silenced: ${ZSXQ_SILENCED:true}
65 | # 任务配置
66 | task:
67 | # 任务列表,支持配置多个
68 | list:
69 | - name: task1 #任务名
70 | monitor: zsxq #监控者
71 | answerer: openai #回答者
72 | cron: '0/30 * * * * ?' #执行周期
73 | ```
74 |
75 | 2)直接运行主类 `MainApplication` 即可
76 |
77 | ## 一键部署
78 |
79 | [](https://railway.app/template/BMJMMm?referralCode=tKgj86)
80 |
81 | 点击上述部署按钮后,会自动识别环境变量,改成自己的就可以了:
82 |
83 | 
84 |
85 | ## 架构设计
86 |
87 | 一图胜千言:
88 |
89 | 
90 |
91 | 本项目最关键的设计就是在于 **解耦监控者与回答者** ,你可以监控任何平台,并且给每个平台绑定不同的自动回答(比如 OpenAI)。
92 |
93 | 实现关键:
94 |
95 | 1. 定义 Answerer 回答者接口,统一回答的方法
96 | 2. 定义 Monitor 监控者接口,统一监控的方法,通过 Answerer 回调参数实现对监控到的消息进行自动回复
97 | 3. 使用中介者模式,用 JobMediator 类组合 Monitor 和 Answerer,而不是把回答者和监控者强绑定
98 | 4. 使用工厂模式,根据配置生成监控者和回答者
99 | 5. 使用 Spring Scheduler,读取 yml 配置来自动创建多任务
100 |
101 | ## 开发
102 |
103 | ### 自定义监控
104 |
105 | 1)编写一个类,实现 `monitor/Monitor` 抽象类
106 |
107 | 2)修改 `factory/MonitorFactory` 的 `createMonitor` 方法,补充创建你自己的监控者
108 |
109 | ### 自定义回答
110 |
111 | 1)编写一个类,实现 `answerer/Answerer` 接口
112 |
113 | 2)修改 `factory/AnswererFactory` 的 `createAnswerer` 方法,补充创建你自己的回答者
114 |
115 |
116 | ## 免费 ChatGPT 交流群
117 |
118 | 
119 |
120 |
121 | ## 欢迎贡献
122 |
123 | **作者平时非常忙** ,本项目也是仅用了几个小时抽空做的,开源出来给大家参考,但是 PR 和 Issues 响应不会很及时,感谢理解!
124 |
125 | 如有项目本身的问题,欢迎提 issues 和 PR;
126 |
127 | 如有编程方面的问题、或者需要项目教学,请看 [编程导航知识星球](https://yupi.icu)
128 |
129 |
130 | ## 问答
131 |
132 | 1)问:为什么先支持知识星球?
133 |
134 | 答:因为 OpenAI 的 API 不是免费的,星球可以限制提问次数,防止刷接口
135 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/api/zsxq/ZsxqApi.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.api.zsxq;
2 |
3 | import cn.hutool.core.bean.BeanUtil;
4 | import cn.hutool.core.util.URLUtil;
5 | import cn.hutool.http.HttpRequest;
6 | import cn.hutool.json.JSONUtil;
7 | import com.yupi.autoreply.api.zsxq.model.AnswerRequest;
8 | import com.yupi.autoreply.api.zsxq.model.AnswerResponse;
9 | import com.yupi.autoreply.api.zsxq.model.ListTopicsRequest;
10 | import com.yupi.autoreply.api.zsxq.model.ListTopicsResponse;
11 | import com.yupi.autoreply.common.ErrorCode;
12 | import com.yupi.autoreply.exception.BusinessException;
13 | import org.apache.commons.lang3.StringUtils;
14 | import org.springframework.stereotype.Service;
15 |
16 | import java.nio.charset.StandardCharsets;
17 | import java.util.Map;
18 |
19 | /**
20 | * 知识星球接口
21 | *
22 | * @author 程序员鱼皮
23 | * @from 编程导航知识星球
24 | */
25 | @Service
26 | public class ZsxqApi {
27 |
28 | private static final String USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0";
29 |
30 | /**
31 | * 获取主题列表
32 | *
33 | * @param request
34 | * @param cookie
35 | * @return
36 | */
37 | public ListTopicsResponse listTopics(ListTopicsRequest request, String cookie) {
38 | String groupId = request.getGroupId();
39 | if (StringUtils.isBlank(groupId)) {
40 | throw new BusinessException(ErrorCode.PARAMS_ERROR, "未传 groupId");
41 | }
42 | if (StringUtils.isBlank(cookie)) {
43 | throw new BusinessException(ErrorCode.PARAMS_ERROR, "未传 cookie");
44 | }
45 | String url = String.format("https://api.zsxq.com/v2/groups/%s/topics", groupId);
46 | Map stringObjectMap = BeanUtil.beanToMap(request);
47 | String query = URLUtil.buildQuery(stringObjectMap, StandardCharsets.UTF_8);
48 | String result = HttpRequest.get(url)
49 | .header("cookie", cookie)
50 | .header("user-agent", USER_AGENT)
51 | .body(query)
52 | .execute()
53 | .body();
54 | return JSONUtil.toBean(result, ListTopicsResponse.class);
55 | }
56 |
57 | /**
58 | * 回答问题
59 | *
60 | * @param request
61 | * @param cookie
62 | * @return
63 | */
64 | public AnswerResponse answer(AnswerRequest request, String cookie) {
65 | String topicId = request.getTopicId();
66 | if (StringUtils.isBlank(topicId)) {
67 | throw new BusinessException(ErrorCode.PARAMS_ERROR, "未传 topicId");
68 | }
69 | if (StringUtils.isBlank(cookie)) {
70 | throw new BusinessException(ErrorCode.PARAMS_ERROR, "未传 cookie");
71 | }
72 | String url = String.format("https://api.zsxq.com/v2/topics/%s/answer", topicId);
73 | String json = JSONUtil.toJsonStr(request);
74 | String result = HttpRequest.post(url)
75 | .header("cookie", cookie)
76 | .header("user-agent", USER_AGENT)
77 | .body(json)
78 | .execute()
79 | .body();
80 | return JSONUtil.toBean(result, AnswerResponse.class);
81 | }
82 | }
83 |
84 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 | ### Java template
35 | # Compiled class file
36 | *.class
37 |
38 | # Log file
39 | *.log
40 |
41 | # BlueJ files
42 | *.ctxt
43 |
44 | # Mobile Tools for Java (J2ME)
45 | .mtj.tmp/
46 |
47 | # Package Files #
48 | *.jar
49 | *.war
50 | *.nar
51 | *.ear
52 | *.zip
53 | *.tar.gz
54 | *.rar
55 |
56 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
57 | hs_err_pid*
58 |
59 | ### Maven template
60 | target/
61 | pom.xml.tag
62 | pom.xml.releaseBackup
63 | pom.xml.versionsBackup
64 | pom.xml.next
65 | release.properties
66 | dependency-reduced-pom.xml
67 | buildNumber.properties
68 | .mvn/timing.properties
69 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar
70 | .mvn/wrapper/maven-wrapper.jar
71 |
72 | ### JetBrains template
73 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
74 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
75 |
76 | # User-specific stuff
77 | .idea/**/workspace.xml
78 | .idea/**/tasks.xml
79 | .idea/**/usage.statistics.xml
80 | .idea/**/dictionaries
81 | .idea/**/shelf
82 |
83 | # Generated files
84 | .idea/**/contentModel.xml
85 |
86 | # Sensitive or high-churn files
87 | .idea/**/dataSources/
88 | .idea/**/dataSources.ids
89 | .idea/**/dataSources.local.xml
90 | .idea/**/sqlDataSources.xml
91 | .idea/**/dynamic.xml
92 | .idea/**/uiDesigner.xml
93 | .idea/**/dbnavigator.xml
94 |
95 | # Gradle
96 | .idea/**/gradle.xml
97 | .idea/**/libraries
98 |
99 | # Gradle and Maven with auto-import
100 | # When using Gradle or Maven with auto-import, you should exclude module files,
101 | # since they will be recreated, and may cause churn. Uncomment if using
102 | # auto-import.
103 | # .idea/artifacts
104 | # .idea/compiler.xml
105 | # .idea/jarRepositories.xml
106 | # .idea/modules.xml
107 | # .idea/*.iml
108 | # .idea/modules
109 | # *.iml
110 | # *.ipr
111 |
112 | # CMake
113 | cmake-build-*/
114 |
115 | # Mongo Explorer plugin
116 | .idea/**/mongoSettings.xml
117 |
118 | # File-based project format
119 | *.iws
120 |
121 | # IntelliJ
122 | out/
123 |
124 | # mpeltonen/sbt-idea plugin
125 | .idea_modules/
126 |
127 | # JIRA plugin
128 | atlassian-ide-plugin.xml
129 |
130 | # Cursive Clojure plugin
131 | .idea/replstate.xml
132 |
133 | # Crashlytics plugin (for Android Studio and IntelliJ)
134 | com_crashlytics_export_strings.xml
135 | crashlytics.properties
136 | crashlytics-build.properties
137 | fabric.properties
138 |
139 | # Editor-based Rest Client
140 | .idea/httpRequests
141 |
142 | # Android studio 3.1+ serialized cache file
143 | .idea/caches/build_file_checksums.ser
144 |
145 |
--------------------------------------------------------------------------------
/src/main/java/com/yupi/autoreply/monitor/ZsxqMonitor.java:
--------------------------------------------------------------------------------
1 | package com.yupi.autoreply.monitor;
2 |
3 | import cn.hutool.core.util.RandomUtil;
4 | import com.github.xiaoymin.knife4j.core.util.CollectionUtils;
5 | import com.yupi.autoreply.answerer.Answerer;
6 | import com.yupi.autoreply.api.zsxq.ZsxqApi;
7 | import com.yupi.autoreply.api.zsxq.model.AnswerRequest;
8 | import com.yupi.autoreply.api.zsxq.model.AnswerResponse;
9 | import com.yupi.autoreply.api.zsxq.model.ListTopicsRequest;
10 | import com.yupi.autoreply.api.zsxq.model.ListTopicsResponse;
11 | import com.yupi.autoreply.common.ErrorCode;
12 | import com.yupi.autoreply.config.ZsxqConfig;
13 | import com.yupi.autoreply.exception.BusinessException;
14 | import com.yupi.autoreply.model.TaskListItem;
15 | import com.yupi.autoreply.utils.SpringContextUtils;
16 | import lombok.extern.slf4j.Slf4j;
17 |
18 | import java.util.ArrayList;
19 | import java.util.List;
20 |
21 | /**
22 | * 知识星球监控者
23 | *
24 | * @author 程序员鱼皮
25 | * @from 编程导航知识星球
26 | */
27 | @Slf4j
28 | public class ZsxqMonitor extends Monitor {
29 |
30 | private final ZsxqApi zsxqApi = SpringContextUtils.getBean(ZsxqApi.class);
31 |
32 | private final ZsxqConfig zsxqConfig = SpringContextUtils.getBean(ZsxqConfig.class);
33 |
34 | public ZsxqMonitor(TaskListItem taskListItem) {
35 | super(taskListItem);
36 | }
37 |
38 | @Override
39 | public void onMonitor(Answerer answerer) {
40 | String taskName = taskListItem.getName();
41 | log.info("任务 {} 监控开始", taskName);
42 | String cookie = zsxqConfig.getCookie();
43 | // 1. 获取未回答的问题列表
44 | ListTopicsRequest listTopicsRequest = new ListTopicsRequest();
45 | listTopicsRequest.setCount(20);
46 | listTopicsRequest.setGroupId(zsxqConfig.getGroupId());
47 | listTopicsRequest.setScope("unanswered_questions");
48 | ListTopicsResponse listTopicsResponse = zsxqApi.listTopics(listTopicsRequest, zsxqConfig.getCookie());
49 | List topics = listTopicsResponse.getRespData().getTopics();
50 | if (CollectionUtils.isEmpty(topics)) {
51 | log.info("暂无新提问");
52 | return;
53 | }
54 | for (ListTopicsResponse.TopicsItem topic : topics) {
55 | String question = topic.getQuestion().getText().trim();
56 | log.info("{} 收到新提问 \n 问题:{}", taskName, question);
57 | // 2. 获取回答
58 | String answer = answerer.doAnswer(question);
59 | // 3. 回复
60 | AnswerRequest answerRequest = new AnswerRequest();
61 | answerRequest.setTopicId(topic.getTopicId());
62 | AnswerRequest.ReqData reqData = new AnswerRequest.ReqData();
63 | reqData.setSilenced(zsxqConfig.getSilenced());
64 | reqData.setText(answer);
65 | reqData.setImage_ids(new ArrayList<>());
66 | answerRequest.setReq_data(reqData);
67 | AnswerResponse answerResponse = zsxqApi.answer(answerRequest, cookie);
68 | if (answerResponse.isSucceeded()) {
69 | log.info("{} 回答成功 \n 问题:{} \n 回答:{}", taskName, question, answer);
70 | } else {
71 | log.error("{} 回答失败 \n 问题:{}", taskName, question);
72 | }
73 | // 随机缓冲
74 | try {
75 | Thread.sleep(1000 + RandomUtil.randomInt(0, 2000));
76 | } catch (InterruptedException e) {
77 | throw new BusinessException(ErrorCode.SYSTEM_ERROR, e.getMessage());
78 | }
79 | }
80 | log.info("任务 {} 监控结束", taskName);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | org.springframework.boot
8 | spring-boot-starter-parent
9 | 2.7.2
10 |
11 |
12 | com.yupi
13 | yu-auto-reply
14 | 0.0.1-SNAPSHOT
15 | yu-auto-reply
16 |
17 | 1.8
18 |
19 |
20 |
21 | org.springframework.boot
22 | spring-boot-starter-web
23 |
24 |
25 | org.springframework.boot
26 | spring-boot-starter-aop
27 |
28 |
29 |
30 | com.github.xiaoymin
31 | knife4j-spring-boot-starter
32 | 3.0.3
33 |
34 |
35 |
36 | org.apache.commons
37 | commons-lang3
38 |
39 |
40 |
41 | com.google.code.gson
42 | gson
43 | 2.9.1
44 |
45 |
46 |
47 | cn.hutool
48 | hutool-all
49 | 5.8.8
50 |
51 |
52 | org.springframework.boot
53 | spring-boot-devtools
54 | runtime
55 | true
56 |
57 |
58 | org.springframework.boot
59 | spring-boot-configuration-processor
60 | true
61 |
62 |
63 | org.projectlombok
64 | lombok
65 | true
66 |
67 |
68 | org.springframework.boot
69 | spring-boot-starter-test
70 | test
71 |
72 |
73 |
74 |
75 |
76 |
77 | org.springframework.boot
78 | spring-boot-maven-plugin
79 |
80 |
81 |
82 | org.projectlombok
83 | lombok
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM https://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
124 |
125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
127 | )
128 |
129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
131 | if exist %WRAPPER_JAR% (
132 | if "%MVNW_VERBOSE%" == "true" (
133 | echo Found %WRAPPER_JAR%
134 | )
135 | ) else (
136 | if not "%MVNW_REPOURL%" == "" (
137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
138 | )
139 | if "%MVNW_VERBOSE%" == "true" (
140 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
141 | echo Downloading from: %DOWNLOAD_URL%
142 | )
143 |
144 | powershell -Command "&{"^
145 | "$webclient = new-object System.Net.WebClient;"^
146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
148 | "}"^
149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
150 | "}"
151 | if "%MVNW_VERBOSE%" == "true" (
152 | echo Finished downloading %WRAPPER_JAR%
153 | )
154 | )
155 | @REM End of extension
156 |
157 | @REM Provide a "standardized" way to retrieve the CLI args that will
158 | @REM work with both Windows and non-Windows executions.
159 | set MAVEN_CMD_LINE_ARGS=%*
160 |
161 | %MAVEN_JAVA_EXE% ^
162 | %JVM_CONFIG_MAVEN_PROPS% ^
163 | %MAVEN_OPTS% ^
164 | %MAVEN_DEBUG_OPTS% ^
165 | -classpath %WRAPPER_JAR% ^
166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
168 | if ERRORLEVEL 1 goto error
169 | goto end
170 |
171 | :error
172 | set ERROR_CODE=1
173 |
174 | :end
175 | @endlocal & set ERROR_CODE=%ERROR_CODE%
176 |
177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
181 | :skipRcPost
182 |
183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause
185 |
186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
187 |
188 | cmd /C exit /B %ERROR_CODE%
189 |
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # https://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Maven Start Up Batch script
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # M2_HOME - location of maven2's installed home dir
31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
32 | # e.g. to debug Maven itself, use
33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
35 | # ----------------------------------------------------------------------------
36 |
37 | if [ -z "$MAVEN_SKIP_RC" ] ; then
38 |
39 | if [ -f /usr/local/etc/mavenrc ] ; then
40 | . /usr/local/etc/mavenrc
41 | fi
42 |
43 | if [ -f /etc/mavenrc ] ; then
44 | . /etc/mavenrc
45 | fi
46 |
47 | if [ -f "$HOME/.mavenrc" ] ; then
48 | . "$HOME/.mavenrc"
49 | fi
50 |
51 | fi
52 |
53 | # OS specific support. $var _must_ be set to either true or false.
54 | cygwin=false;
55 | darwin=false;
56 | mingw=false
57 | case "`uname`" in
58 | CYGWIN*) cygwin=true ;;
59 | MINGW*) mingw=true;;
60 | Darwin*) darwin=true
61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
63 | if [ -z "$JAVA_HOME" ]; then
64 | if [ -x "/usr/libexec/java_home" ]; then
65 | export JAVA_HOME="`/usr/libexec/java_home`"
66 | else
67 | export JAVA_HOME="/Library/Java/Home"
68 | fi
69 | fi
70 | ;;
71 | esac
72 |
73 | if [ -z "$JAVA_HOME" ] ; then
74 | if [ -r /etc/gentoo-release ] ; then
75 | JAVA_HOME=`java-config --jre-home`
76 | fi
77 | fi
78 |
79 | if [ -z "$M2_HOME" ] ; then
80 | ## resolve links - $0 may be a link to maven's home
81 | PRG="$0"
82 |
83 | # need this for relative symlinks
84 | while [ -h "$PRG" ] ; do
85 | ls=`ls -ld "$PRG"`
86 | link=`expr "$ls" : '.*-> \(.*\)$'`
87 | if expr "$link" : '/.*' > /dev/null; then
88 | PRG="$link"
89 | else
90 | PRG="`dirname "$PRG"`/$link"
91 | fi
92 | done
93 |
94 | saveddir=`pwd`
95 |
96 | M2_HOME=`dirname "$PRG"`/..
97 |
98 | # make it fully qualified
99 | M2_HOME=`cd "$M2_HOME" && pwd`
100 |
101 | cd "$saveddir"
102 | # echo Using m2 at $M2_HOME
103 | fi
104 |
105 | # For Cygwin, ensure paths are in UNIX format before anything is touched
106 | if $cygwin ; then
107 | [ -n "$M2_HOME" ] &&
108 | M2_HOME=`cygpath --unix "$M2_HOME"`
109 | [ -n "$JAVA_HOME" ] &&
110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
111 | [ -n "$CLASSPATH" ] &&
112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
113 | fi
114 |
115 | # For Mingw, ensure paths are in UNIX format before anything is touched
116 | if $mingw ; then
117 | [ -n "$M2_HOME" ] &&
118 | M2_HOME="`(cd "$M2_HOME"; pwd)`"
119 | [ -n "$JAVA_HOME" ] &&
120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
121 | fi
122 |
123 | if [ -z "$JAVA_HOME" ]; then
124 | javaExecutable="`which javac`"
125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
126 | # readlink(1) is not available as standard on Solaris 10.
127 | readLink=`which readlink`
128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
129 | if $darwin ; then
130 | javaHome="`dirname \"$javaExecutable\"`"
131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
132 | else
133 | javaExecutable="`readlink -f \"$javaExecutable\"`"
134 | fi
135 | javaHome="`dirname \"$javaExecutable\"`"
136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
137 | JAVA_HOME="$javaHome"
138 | export JAVA_HOME
139 | fi
140 | fi
141 | fi
142 |
143 | if [ -z "$JAVACMD" ] ; then
144 | if [ -n "$JAVA_HOME" ] ; then
145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
146 | # IBM's JDK on AIX uses strange locations for the executables
147 | JAVACMD="$JAVA_HOME/jre/sh/java"
148 | else
149 | JAVACMD="$JAVA_HOME/bin/java"
150 | fi
151 | else
152 | JAVACMD="`\\unset -f command; \\command -v java`"
153 | fi
154 | fi
155 |
156 | if [ ! -x "$JAVACMD" ] ; then
157 | echo "Error: JAVA_HOME is not defined correctly." >&2
158 | echo " We cannot execute $JAVACMD" >&2
159 | exit 1
160 | fi
161 |
162 | if [ -z "$JAVA_HOME" ] ; then
163 | echo "Warning: JAVA_HOME environment variable is not set."
164 | fi
165 |
166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
167 |
168 | # traverses directory structure from process work directory to filesystem root
169 | # first directory with .mvn subdirectory is considered project base directory
170 | find_maven_basedir() {
171 |
172 | if [ -z "$1" ]
173 | then
174 | echo "Path not specified to find_maven_basedir"
175 | return 1
176 | fi
177 |
178 | basedir="$1"
179 | wdir="$1"
180 | while [ "$wdir" != '/' ] ; do
181 | if [ -d "$wdir"/.mvn ] ; then
182 | basedir=$wdir
183 | break
184 | fi
185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
186 | if [ -d "${wdir}" ]; then
187 | wdir=`cd "$wdir/.."; pwd`
188 | fi
189 | # end of workaround
190 | done
191 | echo "${basedir}"
192 | }
193 |
194 | # concatenates all lines of a file
195 | concat_lines() {
196 | if [ -f "$1" ]; then
197 | echo "$(tr -s '\n' ' ' < "$1")"
198 | fi
199 | }
200 |
201 | BASE_DIR=`find_maven_basedir "$(pwd)"`
202 | if [ -z "$BASE_DIR" ]; then
203 | exit 1;
204 | fi
205 |
206 | ##########################################################################################
207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
208 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
209 | ##########################################################################################
210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
211 | if [ "$MVNW_VERBOSE" = true ]; then
212 | echo "Found .mvn/wrapper/maven-wrapper.jar"
213 | fi
214 | else
215 | if [ "$MVNW_VERBOSE" = true ]; then
216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
217 | fi
218 | if [ -n "$MVNW_REPOURL" ]; then
219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
220 | else
221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
222 | fi
223 | while IFS="=" read key value; do
224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
225 | esac
226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
227 | if [ "$MVNW_VERBOSE" = true ]; then
228 | echo "Downloading from: $jarUrl"
229 | fi
230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
231 | if $cygwin; then
232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
233 | fi
234 |
235 | if command -v wget > /dev/null; then
236 | if [ "$MVNW_VERBOSE" = true ]; then
237 | echo "Found wget ... using wget"
238 | fi
239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
241 | else
242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
243 | fi
244 | elif command -v curl > /dev/null; then
245 | if [ "$MVNW_VERBOSE" = true ]; then
246 | echo "Found curl ... using curl"
247 | fi
248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
249 | curl -o "$wrapperJarPath" "$jarUrl" -f
250 | else
251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
252 | fi
253 |
254 | else
255 | if [ "$MVNW_VERBOSE" = true ]; then
256 | echo "Falling back to using Java to download"
257 | fi
258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
259 | # For Cygwin, switch paths to Windows format before running javac
260 | if $cygwin; then
261 | javaClass=`cygpath --path --windows "$javaClass"`
262 | fi
263 | if [ -e "$javaClass" ]; then
264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
265 | if [ "$MVNW_VERBOSE" = true ]; then
266 | echo " - Compiling MavenWrapperDownloader.java ..."
267 | fi
268 | # Compiling the Java class
269 | ("$JAVA_HOME/bin/javac" "$javaClass")
270 | fi
271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
272 | # Running the downloader
273 | if [ "$MVNW_VERBOSE" = true ]; then
274 | echo " - Running MavenWrapperDownloader.java ..."
275 | fi
276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
277 | fi
278 | fi
279 | fi
280 | fi
281 | ##########################################################################################
282 | # End of extension
283 | ##########################################################################################
284 |
285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
286 | if [ "$MVNW_VERBOSE" = true ]; then
287 | echo $MAVEN_PROJECTBASEDIR
288 | fi
289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
290 |
291 | # For Cygwin, switch paths to Windows format before running java
292 | if $cygwin; then
293 | [ -n "$M2_HOME" ] &&
294 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
295 | [ -n "$JAVA_HOME" ] &&
296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
297 | [ -n "$CLASSPATH" ] &&
298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
299 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
301 | fi
302 |
303 | # Provide a "standardized" way to retrieve the CLI args that will
304 | # work with both Windows and non-Windows executions.
305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
306 | export MAVEN_CMD_LINE_ARGS
307 |
308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
309 |
310 | exec "$JAVACMD" \
311 | $MAVEN_OPTS \
312 | $MAVEN_DEBUG_OPTS \
313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
314 | "-Dmaven.home=${M2_HOME}" \
315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
317 |
--------------------------------------------------------------------------------