├── README.md
├── accounts.yaml
├── application.yaml
├── docs
└── feishu.md
├── pom.xml
├── remote_deploy.sh
├── src
├── main
│ └── java
│ │ └── com
│ │ └── rawchen
│ │ └── feishubot
│ │ ├── FeishuBotApplication.java
│ │ ├── config
│ │ └── FeiShuConfig.java
│ │ ├── controller
│ │ └── EventController.java
│ │ ├── entity
│ │ ├── Account.java
│ │ ├── AccountList.java
│ │ ├── Conversation.java
│ │ ├── Mode.java
│ │ ├── Status.java
│ │ ├── gpt
│ │ │ ├── Answer.java
│ │ │ ├── Author.java
│ │ │ ├── Content.java
│ │ │ ├── ErrorCode.java
│ │ │ ├── Message.java
│ │ │ ├── Model.java
│ │ │ └── Models.java
│ │ └── gptRequestBody
│ │ │ └── CreateConversationBody.java
│ │ ├── handler
│ │ ├── EventHandler.java
│ │ └── MessageHandler.java
│ │ ├── scheduling
│ │ └── ScheduledTask.java
│ │ ├── service
│ │ ├── AccountService.java
│ │ ├── MessageService.java
│ │ └── UserService.java
│ │ └── util
│ │ ├── MessageCard.java
│ │ ├── MessageContent.java
│ │ ├── StringUtil.java
│ │ ├── Task.java
│ │ ├── TaskPool.java
│ │ └── chatgpt
│ │ ├── AccountPool.java
│ │ ├── AccountUtil.java
│ │ ├── AnswerProcess.java
│ │ ├── ChatService.java
│ │ ├── ConversationPool.java
│ │ └── RequestIdSet.java
└── test
│ └── java
│ └── com
│ └── rawchen
│ └── feishubot
│ └── FeishuBotApplicationTests.java
├── start.sh
└── stop.sh
/README.md:
--------------------------------------------------------------------------------
1 | # 简介
2 |
3 | 集成ChatGPT的飞书机器人。使用方式:创建群后添加机器人到群中,@机器人发送内容,也可以私聊机器人发送内容。
4 |
5 | 该项目通过chatgpt账号,与[自建ChatGPT接口代理](https://github.com/linweiyuan/go-chatgpt-api)方式无需科学上网也能使用chatgpt服务。
6 |
7 | 该项目使用accesstoken方式,无需花费apikey额度,只要有chatgpt账号就能使用。
8 |
9 | 默认使用3.5模型,plus账户也可以接入4.0模型。
10 |
11 | 一个账号同一时间只能一次对话,因此对于多人同时使用时,通过配置多账号,自动切换空闲账号服务。
12 |
13 | ## 效果
14 | 
15 | 
16 |
17 | ## 功能支持
18 |
19 | - [x] 飞书集成
20 | - [x] 流式显示
21 | - [x] 支持配置多账户
22 | - [x] 多人使用自动切换账号服务
23 |
24 | ## 运行环境
25 |
26 | JDK1.8
27 |
28 | ## 飞书配置
29 |
30 | [飞书机器人配置和部署](docs/feishu.md)
--------------------------------------------------------------------------------
/accounts.yaml:
--------------------------------------------------------------------------------
1 | accounts:
2 | - account:
3 | token: sk-mNI2dbRxxxxxxxxxxxxxxxxxxxxx
4 |
--------------------------------------------------------------------------------
/application.yaml:
--------------------------------------------------------------------------------
1 | server:
2 | port: 9001
3 |
4 | feishu:
5 | appId: cli_a44axxxxxxxxxxx
6 | appSecret: 9GLUNqVTusxAxxxxxxxxxxxxxxxxxxxxx
7 | encryptKey:
8 | verificationToken:
9 |
10 | proxy:
11 | url: https://api.baizhi.ai
12 |
13 | logging:
14 | level:
15 | root: info
16 | com.rawchen: debug
17 | com.lark.oapi.event.EventDispatcher: off
18 | org.apache.coyote.http11.Http11Processor: warn
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/docs/feishu.md:
--------------------------------------------------------------------------------
1 | 1. 飞书创建自建应用,添加机器人能力
2 | 2. 添加4个权限:以应用身份读取通讯录/接收群聊中@机器人消息事件/读取用户发给机器人的单聊消息/以应用的身份发消息
3 | 3. 权限管理中“通讯录权限范围”选为全部成员
4 | 4. 添加1个事件:接收消息v2.0
5 | 5. 配置accounts.yaml添加至少一个chatgpt账号,配置application.yaml添加应用凭证和事件凭证4条
6 | 6. mvn构建源代码为jar包,上传至服务器。上传两配置文件至同级目录。
7 | 7. 执行'nohup java -jar xxx.jar >> app.log &'
8 | 8. 跑成功后配置自建应用的事件订阅请求地址:http://服务器ip:9001/chatEvent
9 | 9. 自建应用版本发布,可用范围选为“所有员工”
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.7.12
9 |
10 |
11 | com.rawchen
12 | FeishuBot
13 | 0.0.1-SNAPSHOT
14 | FeishuBot
15 | FeishuBot
16 |
17 | 1.8
18 |
19 |
20 |
21 |
22 | Central Repository
23 | https://repo1.maven.org/maven2/
24 |
25 |
26 |
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-web
31 |
32 |
33 |
34 | org.springframework.boot
35 | spring-boot-starter-test
36 | test
37 |
38 |
39 |
40 | com.larksuite.oapi
41 | oapi-sdk
42 | 2.3.5
43 |
44 |
45 |
46 |
47 | oapi-sdk-servlet-ext
48 | com.larksuite.oapi
49 | 1.0.0-rc3
50 |
51 |
52 | oapi-sdk
53 | com.larksuite.oapi
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | org.projectlombok
65 | lombok
66 |
67 |
68 |
69 | cn.hutool
70 | hutool-all
71 | 5.8.19
72 |
73 |
74 |
75 | com.alibaba
76 | fastjson
77 | 1.2.61
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | org.springframework.boot
86 | spring-boot-maven-plugin
87 |
88 | true
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/remote_deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | echo '---------------开始自动部署---------------'
4 |
5 | # 项目根目录
6 | ROOT_PATH=$(cd $(dirname $0);cd .; pwd)
7 |
8 | # 项目名称
9 | PROJECT_NAME=FeishuBot
10 |
11 | # LINUX部署目录
12 | DEPLOY_PATH=root/FeishuBot
13 |
14 | # 服务器地址
15 | SERVER_IP=xxx.xx.xx.xx
16 |
17 | echo '---------------项目本地路径---------------'
18 |
19 | echo ${ROOT_PATH} && cd ${ROOT_PATH}
20 |
21 | echo '---------------开始项目打包---------------'
22 |
23 | mvn clean package -Dmaven.test.skip=true
24 |
25 | mv target/${PROJECT_NAME}-0.0.1-SNAPSHOT.jar target/${PROJECT_NAME}.jar
26 |
27 | echo '---------------开始上传项目---------------'
28 |
29 | scp -r -i id_rsa.pem -P 22 target/${PROJECT_NAME}.jar root@${SERVER_IP}:/${DEPLOY_PATH}
30 |
31 | echo '---------------kill源进程---------------'
32 |
33 | ssh -i id_rsa.pem -p 22 root@${SERVER_IP} << EOF
34 |
35 | cd /${DEPLOY_PATH};
36 |
37 | ps -ef | grep ${PROJECT_NAME}.jar | grep -v 'grep' | cut -c 9-15 | xargs kill -s 9
38 |
39 | echo '---------------启动项目中---------------'
40 |
41 | ./start.sh
42 |
43 | tail -f -n 50 app.log
44 |
45 | EOF
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/FeishuBotApplication.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot;
2 |
3 | import cn.hutool.extra.spring.SpringUtil;
4 | import com.lark.oapi.ws.Client;
5 | import com.rawchen.feishubot.handler.EventHandler;
6 | import org.springframework.boot.SpringApplication;
7 | import org.springframework.boot.autoconfigure.SpringBootApplication;
8 | import org.springframework.scheduling.annotation.EnableScheduling;
9 |
10 | @SpringBootApplication
11 | @EnableScheduling
12 | public class FeishuBotApplication {
13 |
14 | public static void main(String[] args) {
15 | SpringApplication.run(FeishuBotApplication.class, args);
16 |
17 | Client cli = new Client.Builder("cli_a44axxxxxxxxx", "9GLUNqVTusxxxxxxxxxxxxxxxxxxx")
18 | .eventHandler(EventHandler.EVENT_HANDLER)
19 | .build();
20 | cli.start();
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/config/FeiShuConfig.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.config;
2 |
3 | import com.lark.oapi.Client;
4 | import com.lark.oapi.sdk.servlet.ext.ServletAdapter;
5 | import org.springframework.beans.factory.annotation.Value;
6 | import org.springframework.context.annotation.Bean;
7 | import org.springframework.context.annotation.Configuration;
8 |
9 | @Configuration
10 | public class FeiShuConfig {
11 |
12 | @Value("${feishu.appId}")
13 | private String appId;
14 |
15 | @Value("${feishu.appSecret}")
16 | private String appSecret;
17 |
18 | @Bean
19 | public ServletAdapter getServletAdapter() {
20 | return new ServletAdapter();
21 | }
22 |
23 | @Bean
24 | public Client getClient() {
25 | return Client.newBuilder(appId, appSecret).build();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/controller/EventController.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.controller;
2 |
3 | import cn.hutool.json.JSONUtil;
4 | import com.lark.oapi.event.EventDispatcher;
5 | import com.lark.oapi.sdk.servlet.ext.ServletAdapter;
6 | import com.rawchen.feishubot.entity.Conversation;
7 | import com.rawchen.feishubot.handler.MessageHandler;
8 | import com.rawchen.feishubot.util.chatgpt.ConversationPool;
9 | import lombok.RequiredArgsConstructor;
10 | import lombok.extern.slf4j.Slf4j;
11 | import org.springframework.beans.factory.annotation.Value;
12 | import org.springframework.web.bind.annotation.*;
13 |
14 | import javax.servlet.http.HttpServletRequest;
15 | import javax.servlet.http.HttpServletResponse;
16 |
17 | @RestController
18 | @RequiredArgsConstructor
19 | @Slf4j
20 | public class EventController {
21 |
22 | // protected final MessageHandler messageHandler;
23 |
24 | @Value("${feishu.verificationToken}")
25 | private String verificationToken;
26 |
27 | @Value("${feishu.encryptKey}")
28 | private String encryptionKey;
29 |
30 | private EventDispatcher EVENT_DISPATCHER;
31 |
32 | protected final ConversationPool conversationPool;
33 |
34 | protected final ServletAdapter servletAdapter;
35 |
36 | //处理事件回调
37 | @RequestMapping("/chatEvent")
38 | public void event(HttpServletRequest request, HttpServletResponse response)
39 | throws Throwable {
40 | if (EVENT_DISPATCHER == null) {
41 | // init();
42 | }
43 |
44 | servletAdapter.handleEvent(request, response, EVENT_DISPATCHER);
45 | }
46 |
47 |
48 | /**
49 | * 处理消息卡片事件回调
50 | *
51 | * @param body
52 | * @return
53 | */
54 | @PostMapping("/cardEvent")
55 | @ResponseBody
56 | public String event(@RequestBody String body) {
57 | // log.debug("收到消息卡片事件: {}", body);
58 | // JSONObject payload = new JSONObject(body);
59 | // if (payload.opt("challenge") != null) {
60 | // JSONObject res = new JSONObject();
61 | // res.put("challenge", payload.get("challenge"));
62 | // return res.toString();
63 | // }
64 | //
65 | // String chatId = String.valueOf(payload.get("open_chat_id"));
66 | //
67 | // JSONObject action = (JSONObject) payload.get("action");
68 | // String option = (String) action.get("option");
69 | //
70 | // Conversation bean = JSONUtil.toBean(option, Conversation.class);
71 | // log.debug("收到模型选择: {}", bean);
72 | // if (bean.getParentMessageId() == null) {
73 | // bean.setParentMessageId("");
74 | // }
75 | // if (bean.getConversationId() == null) {
76 | // bean.setConversationId("");
77 | // }
78 | // bean.setChatId(chatId);
79 | // conversationPool.addConversation(chatId, bean);
80 | return "";
81 | }
82 |
83 | @GetMapping("/ping")
84 | @ResponseBody
85 | public String ping() {
86 | return "pong";
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/entity/Account.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.entity;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class Account {
7 | private String account;
8 | private String password;
9 | private String token;
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/entity/AccountList.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.entity;
2 |
3 | import lombok.Data;
4 |
5 | import java.util.List;
6 |
7 | @Data
8 | public class AccountList {
9 | private List accounts;
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/entity/Conversation.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.entity;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class Conversation {
7 | /**
8 | * gpt的会话id
9 | */
10 | public String conversationId;
11 | /**
12 | * 对话id,用于区分不同用户
13 | */
14 | public String chatId;
15 | /**
16 | * 服务该会话的gpt账号
17 | */
18 | public String account;
19 | /**
20 | * 服务该会话的gpt模型
21 | */
22 | public String model;
23 | /**
24 | * gpt中的上下文消息id
25 | */
26 | public String parentMessageId;
27 | public volatile Status status;
28 | /**
29 | * 消息中的标题
30 | */
31 | public String title;
32 |
33 | /**
34 | * 回复模式
35 | */
36 | public Mode mode;
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/entity/Mode.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.entity;
2 |
3 | public enum Mode {
4 | /**
5 | * 快速回复模式,只要问就找可用的账号服务
6 | */
7 | FAST,
8 | /**
9 | * 保持会话模式,尽可能使用同一个账号服务
10 | */
11 | KEEP
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/entity/Status.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.entity;
2 |
3 | public enum Status {
4 | RUNNING,
5 | FINISHED,
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/entity/gpt/Answer.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.entity.gpt;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class Answer {
7 |
8 | private int seq;
9 |
10 | /**
11 | * 是否正常返回GPT对话结果
12 | */
13 | private boolean success;
14 | private Message message;
15 | private String conversationId;
16 | private Object error;
17 | /**
18 | * 对话响应是否结束
19 | */
20 | private boolean finished;
21 | /**
22 | * 对话响应的文本
23 | */
24 | private String answer;
25 |
26 | /**
27 | * 错误响应码
28 | */
29 | private int errorCode;
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/entity/gpt/Author.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.entity.gpt;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class Author {
7 | private String role;
8 | private String name;
9 | private String metadata;
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/entity/gpt/Content.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.entity.gpt;
2 |
3 | import lombok.Data;
4 |
5 | import java.util.List;
6 |
7 | @Data
8 | public class Content {
9 | private String content_type;
10 | private List parts;
11 | private String text;
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/entity/gpt/ErrorCode.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.entity.gpt;
2 |
3 | import java.util.HashMap;
4 | import java.util.Map;
5 |
6 | public class ErrorCode {
7 |
8 | public static final int INVALID_JWT = 1;
9 |
10 | /**
11 | * 账号繁忙
12 | */
13 | public static final int BUSY = 2;
14 | public static final int INVALID_API_KEY = 3;
15 |
16 | /**
17 | * 4.0 3小时25次的对话限制
18 | */
19 | public static final int CHAT_LIMIT = 4;
20 |
21 |
22 | public static final int RESPONSE_ERROR = 5;
23 |
24 | public static final int ACCOUNT_DEACTIVATED = 6;
25 |
26 | public static final Map map = new HashMap<>();
27 |
28 | static {
29 | map.put(INVALID_JWT, "无效的access token");
30 | map.put(BUSY, "账号繁忙中");
31 | map.put(INVALID_API_KEY, "无效的api key");
32 | map.put(CHAT_LIMIT, "4.0接口被限制了");
33 | map.put(RESPONSE_ERROR, "响应错误");
34 | map.put(ACCOUNT_DEACTIVATED, "账号被停用");
35 | }
36 |
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/entity/gpt/Message.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.entity.gpt;
2 |
3 | import lombok.Data;
4 |
5 | /**
6 | * gpt对话响应对象
7 | */
8 | @Data
9 | public class Message {
10 | private String id;
11 | private Author author;
12 | private Double createTime;
13 | private Object updateTime;
14 | private Content content;
15 | private String status;
16 | private Boolean endTurn;
17 | private Double weight;
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/entity/gpt/Model.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.entity.gpt;
2 |
3 | import lombok.Data;
4 |
5 | import java.util.List;
6 |
7 | @Data
8 | /**
9 | * openai 查询模型接口返回的模型信息
10 | */
11 | public class Model {
12 | private String slug;
13 | private String max_tokens;
14 | private String title;
15 | private String description;
16 | private List tags;
17 | private String capabilities;
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/entity/gpt/Models.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.entity.gpt;
2 |
3 | import java.util.HashMap;
4 | import java.util.HashSet;
5 | import java.util.Map;
6 | import java.util.Set;
7 |
8 | public class Models {
9 |
10 | /**
11 | * plus用户默认模型
12 | */
13 | // public static final String PLUS_DEFAULT_MODEL = "Default (GPT-3.5)";
14 | public static final String PLUS_DEFAULT_MODEL = "gpt-3.5-turbo";
15 |
16 | /**
17 | * 普通用户默认模型
18 | */
19 | public static String NORMAL_DEFAULT_MODEL;
20 |
21 | /**
22 | * 空模型
23 | */
24 | public static final String EMPTY_MODEL = "Empty";
25 |
26 | /**
27 | * 模型title和对应模型
28 | */
29 | public static Map modelMap = new HashMap<>();
30 |
31 | /**
32 | * plus模型title池 用于判断请求是否是plus模型
33 | */
34 | public static Set plusModelTitle = new HashSet<>();
35 |
36 | /**
37 | * normal模型title池
38 | */
39 | public static Set normalModelTitle = new HashSet<>();
40 |
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/entity/gptRequestBody/CreateConversationBody.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.entity.gptRequestBody;
2 |
3 | import com.alibaba.fastjson.JSONArray;
4 | import com.alibaba.fastjson.JSONObject;
5 |
6 | import java.util.ArrayList;
7 | import java.util.HashMap;
8 | import java.util.Map;
9 |
10 | /**
11 | * 构造ChatGpt对话api请求体
12 | */
13 | public class CreateConversationBody {
14 | public static String of(String messageId, String text, String parentMessageId, String conversationId, String model) {
15 | JSONObject param = new JSONObject();
16 | // body.put("action", "next");
17 | // JSONArray messages = new JSONArray();
18 | // JSONObject message = new JSONObject();
19 | // message.put("id", messageId);
20 | // JSONObject author = new JSONObject();
21 | // author.put("role", "user");
22 | // message.put("author", author);
23 | // JSONObject content = new JSONObject();
24 | // content.put("content_type", "text");
25 | // JSONArray parts = new JSONArray();
26 | // parts.put(text);
27 | // content.put("parts", parts);
28 | // message.put("content", content);
29 | // messages.put(message);
30 | // body.put("messages", messages);
31 | // body.put("parent_message_id", parentMessageId);
32 | // body.put("conversation_id", conversationId);
33 | // body.put("model", model);
34 | // body.put("timezone_offset_min", -480);
35 | // body.put("history_and_training_disabled", false);
36 |
37 |
38 | JSONObject message = new JSONObject();
39 | message.put("content", text);
40 | message.put("role", "user");
41 | // Message message = new Message().setRole("user").setContent(text);
42 | JSONArray messages = new JSONArray();
43 | messages.add(message);
44 | param.put("messages", messages);
45 | param.put("model", model);
46 | // param.put("model", "gpt-4-turbo");
47 | param.put("stream", false);
48 |
49 | return param.toJSONString();
50 | }
51 |
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/handler/EventHandler.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.handler;
2 |
3 | import cn.hutool.core.date.DateUtil;
4 | import cn.hutool.extra.spring.SpringUtil;
5 | import cn.hutool.json.JSONUtil;
6 | import com.lark.oapi.core.request.EventReq;
7 | import com.lark.oapi.event.CustomEventHandler;
8 | import com.lark.oapi.event.EventDispatcher;
9 | import com.lark.oapi.service.im.ImService;
10 | import com.lark.oapi.service.im.v1.model.P2MessageReceiveV1;
11 | import lombok.Data;
12 | import lombok.extern.slf4j.Slf4j;
13 | import org.springframework.stereotype.Component;
14 |
15 | import java.nio.charset.StandardCharsets;
16 |
17 | /**
18 | * 事件处理器
19 | *
20 | * @author shuangquan.chen
21 | * @date 2024-04-29 16:15
22 | */
23 | @Slf4j
24 | @Component
25 | @Data
26 | public class EventHandler {
27 |
28 | public static final EventDispatcher EVENT_HANDLER = EventDispatcher.newBuilder("", "")
29 | .onP2MessageReceiveV1(new ImService.P2MessageReceiveV1Handler() {
30 | @Override
31 | public void handle(P2MessageReceiveV1 event) {
32 | //处理消息事件
33 | try {
34 | // log.info("收到消息: {}", Jsons.DEFAULT.toJson(event));
35 | MessageHandler messageHandler = SpringUtil.getBean(MessageHandler.class);
36 | // log.info("event: {}", JSONUtil.toJsonStr(event));
37 | messageHandler.process(event);
38 | } catch (Exception e) {
39 | log.error("处理消息失败", e);
40 | throw new RuntimeException(e);
41 | }
42 | }
43 | })
44 | .build();
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/handler/MessageHandler.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.handler;
2 |
3 | import cn.hutool.core.date.DateUtil;
4 | import cn.hutool.json.JSONUtil;
5 | import com.alibaba.fastjson.JSONObject;
6 | import com.lark.oapi.Client;
7 | import com.lark.oapi.service.contact.v3.model.User;
8 | import com.lark.oapi.service.im.v1.model.*;
9 | import com.rawchen.feishubot.entity.Conversation;
10 | import com.rawchen.feishubot.entity.Mode;
11 | import com.rawchen.feishubot.entity.Status;
12 | import com.rawchen.feishubot.entity.gpt.Answer;
13 | import com.rawchen.feishubot.entity.gpt.ErrorCode;
14 | import com.rawchen.feishubot.entity.gpt.Models;
15 | import com.rawchen.feishubot.service.MessageService;
16 | import com.rawchen.feishubot.service.UserService;
17 | import com.rawchen.feishubot.util.chatgpt.AccountPool;
18 | import com.rawchen.feishubot.util.chatgpt.ChatService;
19 | import com.rawchen.feishubot.util.chatgpt.ConversationPool;
20 | import com.rawchen.feishubot.util.chatgpt.RequestIdSet;
21 | import lombok.RequiredArgsConstructor;
22 | import lombok.extern.slf4j.Slf4j;
23 | import org.springframework.stereotype.Component;
24 | import org.springframework.util.StringUtils;
25 |
26 | import java.util.HashMap;
27 | import java.util.Map;
28 | import java.util.concurrent.TimeUnit;
29 |
30 | @Component
31 | @Slf4j
32 | @RequiredArgsConstructor
33 | public class MessageHandler {
34 |
35 |
36 | protected final Client client;
37 | protected final AccountPool accountPool;
38 | protected final UserService userService;
39 | protected final MessageService messageService;
40 | protected final ConversationPool conversationPool;
41 |
42 | /**
43 | * 飞书推送事件会重复,用于去重
44 | *
45 | * @param event
46 | * @return
47 | */
48 | private boolean checkInvalidEvent(P2MessageReceiveV1 event) {
49 | // String requestId = event.getRequestId();
50 | String requestId = event.getHeader().getEventId();
51 | // 根据内存记录的消息事件id去重
52 | if (RequestIdSet.requestIdSet.contains(requestId)) {
53 | //log.warn("重复请求,requestId:{}", requestId);
54 | return false;
55 | }
56 | RequestIdSet.requestIdSet.add(requestId);
57 |
58 | String createTime = event.getEvent().getMessage().getCreateTime();
59 | Long createTimeLong = Long.valueOf(createTime);
60 | Long now = System.currentTimeMillis();
61 | if (now - createTimeLong > 1000 * 10) {
62 | // 根据消息事件的创建时间去重(如果现在的时间比创建时间大10秒就说明是重复的,正常服务器时间同步正确应该只有1s的差别)
63 | //log.warn("消息过期,requestId:{}", requestId);
64 | return false;
65 | }
66 |
67 | P2MessageReceiveV1Data messageEvent = event.getEvent();
68 | EventMessage message = messageEvent.getMessage();
69 |
70 | String chatType = message.getChatType();
71 | String msgType = message.getMessageType();
72 |
73 | log.info("对话类型: {},消息类型: {}", chatType, msgType);
74 |
75 |
76 | // 只处理私聊文本消息
77 | // if (!chatType.equals("p2p") || !msgType.equals("text")) {
78 | // log.warn("不支持的ChatType或MsgType,ChatGpt不处理");
79 | // return false;
80 | // }
81 |
82 | // 不是group群组@机器人的消息和机器人私聊消息,或者不是文本就不处理
83 | if ((!chatType.equals("group") && !chatType.equals("p2p")) || !msgType.equals("text")) {
84 | log.warn("不支持的ChatType或MsgType,ChatGpt不处理");
85 | return false;
86 | }
87 | return true;
88 | }
89 |
90 |
91 | public void process(P2MessageReceiveV1 event) throws Exception {
92 | P2MessageReceiveV1Data messageEvent = event.getEvent();
93 | EventMessage message = messageEvent.getMessage();
94 | // 如果是重复消息不处理
95 | if (!checkInvalidEvent(event)) {
96 | return;
97 | }
98 | log.info("事件时间: {}", DateUtil.formatDateTime(DateUtil.date(Long.parseLong(event.getEvent().getMessage().getCreateTime()))));
99 | JSONObject jsonObject = JSONObject.parseObject(message.getContent());
100 | String text = jsonObject.getString("text");
101 |
102 | // 去掉事件获取开头的"@_user_1"
103 | if (text.startsWith("@_user_1")) {
104 | text = text.substring(9);
105 | }
106 |
107 | try {
108 | User user = userService.getUserByOpenId(event.getEvent().getSender().getSenderId().getOpenId());
109 | String name = user.getName();
110 | log.info("{}: {}", name, text);
111 | } catch (Exception e) {
112 | log.error("获取用户信息失败", e);
113 | }
114 |
115 | if (text == null || text.equals("")) {
116 | return;
117 | }
118 | String chatId = message.getChatId();
119 | //尝试从会话池中获取会话,从而保持上下文
120 | Conversation conversation = conversationPool.getConversation(chatId);
121 | String model;
122 |
123 | String account = null;
124 | ChatService chatService;
125 |
126 | if (conversation == null) {
127 | //如果没有会话,新建会话,采用默认3.5的模型
128 | conversation = new Conversation();
129 | chatService = accountPool.getFreeChatService(Models.EMPTY_MODEL);
130 | if (chatService != null) {
131 | // if (chatService.getLevel() == 4) {
132 | if (chatService.getLevel() == 3) {
133 | model = Models.PLUS_DEFAULT_MODEL;
134 | } else {
135 | model = Models.NORMAL_DEFAULT_MODEL;
136 | }
137 |
138 | conversation.setChatId(chatId);
139 | conversation.setModel(model);
140 | //默认使用keep模式
141 | conversation.setMode(Mode.FAST);
142 |
143 | conversationPool.addConversation(chatId, conversation);
144 | } else {
145 | model = null;
146 | }
147 |
148 |
149 | } else {
150 |
151 | //新会话意图
152 | model = conversation.getModel();
153 | chatService = accountPool.getFreeChatService(model);
154 | }
155 |
156 |
157 | if (chatService == null) {
158 | if (accountPool.getSize() == 0) {
159 | messageService.sendTextMessageByChatId(chatId, "服务器未配置可用账户");
160 | return;
161 | }
162 |
163 | //如果是fast模式,则需要切换账号服务
164 | // if (conversation.getMode() == Mode.FAST) {
165 | // chatService = accountPool.getFreeChatService(Models.EMPTY_MODEL);
166 | // if (chatService == null) {
167 | // messageService.sendTextMessageByChatId(chatId, "目前无空闲该模型,请稍后再试,或者更换模型");
168 | // return;
169 | // } else {
170 | // conversation.setAccount(chatService.getAccount());
171 | //
172 | // if (chatService.getLevel() == 4) {
173 | // conversation.setModel(Models.PLUS_DEFAULT_MODEL);
174 | // } else {
175 | // conversation.setModel(Models.NORMAL_DEFAULT_MODEL);
176 | // }
177 | // }
178 | // }
179 | }
180 |
181 | //账号池里所有的账号都在运行
182 | if (chatService == null) {
183 | messageService.sendTextMessageByChatId(chatId, "目前无空闲账号,请稍后再试");
184 | return;
185 | }
186 |
187 | String firstText = "正在生成中,请稍后...";
188 |
189 | if (chatService.getStatus() == Status.RUNNING) {
190 | //fast模式,切换账号,创建新会话
191 | chatService = accountPool.getFreeChatService(Models.EMPTY_MODEL);
192 | if (chatService.getStatus() == Status.RUNNING) {
193 | firstText = "目前该账号正在运行,请稍等...";
194 | } else {
195 | conversation.setAccount(chatService.getAccount());
196 | //fast模式下,切换账号和原账号模型保持一致
197 | //如果原账号是plus模型,则切换到的是normal账号,则用normal的模型
198 | if (chatService.getLevel() == 4) {
199 | if (Models.plusModelTitle.contains(conversation.getModel())) {
200 | conversation.setModel(conversation.getModel());
201 | } else {
202 | conversation.setModel(Models.PLUS_DEFAULT_MODEL);
203 | }
204 | } else {
205 | conversation.setModel(Models.NORMAL_DEFAULT_MODEL);
206 | }
207 | }
208 | }
209 | conversation.setAccount(chatService.getAccount());
210 | chatService.setStatus(Status.RUNNING);
211 | conversation.setStatus(Status.RUNNING);
212 |
213 | String title;
214 | if (!StringUtils.hasLength(chatService.getAccount())) {
215 | title = model;
216 | } else {
217 | title = model + " : " + chatService.getAccount();
218 | }
219 |
220 | if (chatService.getLevel() == 4) {
221 | title += " [plus] ";
222 | }
223 |
224 | String modelParam = Models.PLUS_DEFAULT_MODEL;
225 | title = modelParam;
226 | // model = modelParam;
227 |
228 | // title += "「" + conversation.getMode() + "」";
229 |
230 |
231 | CreateMessageResp resp = messageService.sendGptAnswerMessage(chatId, title, firstText);
232 | String messageId = resp.getData().getMessageId();
233 |
234 | // Map selections = createSelection(conversation);
235 |
236 | // String modelParam = Models.modelMap.get(conversation.getModel()).getSlug();
237 |
238 | // log.info("新建会话");
239 | conversation.setTitle(title);
240 | String finalTitle = title;
241 | ChatService finalChatService = chatService;
242 | chatService.newChat(text, modelParam, answer -> {
243 | processAnswer(answer, finalTitle, chatId, finalChatService, messageId, event, model);
244 | });
245 |
246 |
247 | // log.info("服务完成: {}|{}|{}|{}", chatService.getAccount(), model, chatId, messageId);
248 | log.info("服务完成: {}|{}|{}|{}\n", chatService.getAccessToken(), model, chatId, messageId);
249 | }
250 |
251 | private Map createSelection(Conversation conversation) {
252 | //将curConversation转换成json格式
253 | String conversationStr = JSONUtil.toJsonStr(conversation);
254 | Map selections = new HashMap<>();
255 | selections.put("使用当前上下文", conversationStr);
256 |
257 | for (String s : Models.normalModelTitle) {
258 | Conversation normalModelConversation = new Conversation();
259 | normalModelConversation.setModel(s);
260 | selections.put(s, JSONUtil.toJsonStr(normalModelConversation));
261 | }
262 |
263 | if (AccountPool.plusPool.isEmpty()) {
264 | return selections;
265 | }
266 |
267 | for (String modelTitle : Models.plusModelTitle) {
268 | Conversation plusModelConversation = new Conversation();
269 | plusModelConversation.setModel(modelTitle);
270 | selections.put(modelTitle, JSONUtil.toJsonStr(plusModelConversation));
271 | }
272 | return selections;
273 | }
274 |
275 | private void processAnswer(Answer answer, String title, String chatId, ChatService chatService, String messageId, P2MessageReceiveV1 event, String model) throws Exception {
276 | if (!answer.isSuccess()) {
277 | // gpt请求失败
278 | if (answer.getErrorCode() == ErrorCode.INVALID_JWT || answer.getErrorCode() == ErrorCode.INVALID_API_KEY) {
279 |
280 | messageService.modifyGptAnswerMessageCard(messageId, title, (String) answer.getError());
281 | boolean build = chatService.build();
282 | if (build) {
283 | accountPool.modifyChatService(chatService);
284 | RequestIdSet.requestIdSet.remove(event.getRequestId());
285 | process(event);
286 | } else {
287 | messageService.modifyGptAnswerMessageCard(messageId, title, "账号重新登录失败");
288 | }
289 | return;
290 | } else if (answer.getErrorCode() == ErrorCode.ACCOUNT_DEACTIVATED) {
291 | answer.setError(ErrorCode.map.get(ErrorCode.ACCOUNT_DEACTIVATED));
292 | log.error("账号{} {}", chatService.getAccount(), ErrorCode.map.get(ErrorCode.ACCOUNT_DEACTIVATED));
293 | AccountPool.removeAccount(chatService.getAccount());
294 | }
295 | messageService.modifyGptAnswerMessageCard(messageId, title, (String) answer.getError());
296 | chatService.setStatus(Status.FINISHED);
297 | conversationPool.getConversation(chatId).setStatus(Status.FINISHED);
298 | return;
299 | }
300 | Conversation conversation = conversationPool.getConversation(chatId);
301 | // conversation.setParentMessageId(answer.getMessage().getId());
302 | // conversation.setConversationId(answer.getConversationId());
303 |
304 | if (!answer.isFinished()) {
305 | chatService.setStatus(Status.RUNNING);
306 | conversation.setStatus(Status.RUNNING);
307 | } else {
308 | chatService.setStatus(Status.FINISHED);
309 | conversation.setStatus(Status.FINISHED);
310 | // conversation.setChatId(chatId);
311 | // conversation.setParentMessageId(answer.getMessage().getId());
312 | // conversation.setConversationId(answer.getConversationId());
313 | // conversation.setAccount(chatService.getAccount());
314 | // conversation.setModel(model);
315 | // conversation.setTitle(title);
316 | }
317 |
318 | // selections.put("使用当前上下文", JSONUtil.toJsonStr(conversation));
319 |
320 | String content = answer.getAnswer();
321 | if (content == null || content.equals("")) {
322 | return;
323 | }
324 | PatchMessageResp resp1 = messageService.modifyGptAnswerMessageCard(messageId, title, content);
325 | if (resp1.getCode() != 0) {
326 | log.error("code: {}", resp1.getCode());
327 | log.error("msg: {}", resp1.getMsg());
328 | log.error("error msg: {}", resp1.getError().getMessage());
329 | }
330 |
331 | if (answer.isFinished() && resp1.getCode() == 230020) {
332 | //保证最后完整的gpt响应 不会被飞书消息频率限制
333 | while (answer.isFinished() && resp1.getCode() != 0) {
334 | log.warn("重试中 code: {} msg: {}", resp1.getCode(), resp1.getMsg());
335 | TimeUnit.MILLISECONDS.sleep(500);
336 | resp1 = messageService.modifyGptAnswerMessageCard(messageId, title, content);
337 | }
338 | log.info("重试成功");
339 | }
340 | }
341 | }
342 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/scheduling/ScheduledTask.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.scheduling;
2 |
3 | import com.rawchen.feishubot.util.chatgpt.AccountUtil;
4 | import lombok.extern.slf4j.Slf4j;
5 | import org.springframework.scheduling.annotation.Scheduled;
6 | import org.springframework.stereotype.Component;
7 |
8 | @Component
9 | @Slf4j
10 | public class ScheduledTask {
11 | // @Scheduled(cron = "0 0 0 ? * MON")
12 | public void refreshAccountToken() {
13 | log.info("开始刷新账号token");
14 | try {
15 | AccountUtil.refreshToken();
16 | } catch (Exception e) {
17 | log.error("刷新账号token失败", e);
18 | }
19 | }
20 |
21 | /**
22 | * 每隔两个小时执行一次
23 | */
24 | // @Scheduled(initialDelay = 1000 * 60, fixedRate = 1000 * 60 * 60 * 2)
25 | public void checkAccountLevel() {
26 | try {
27 | AccountUtil.queryAccountLevel();
28 | } catch (Exception e) {
29 | log.error("检查账号等级失败", e);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/service/AccountService.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.service;
2 |
3 | import com.rawchen.feishubot.entity.Account;
4 | import com.rawchen.feishubot.util.chatgpt.AccountUtil;
5 | import org.springframework.stereotype.Service;
6 |
7 | import java.util.List;
8 |
9 | @Service
10 | public class AccountService {
11 | /**
12 | * 从配置文件读取账号信息
13 | *
14 | * @return
15 | */
16 | public List getAccounts() {
17 | return AccountUtil.readAccounts().getAccounts();
18 | }
19 |
20 | /**
21 | * 更新账号的token到配置文件
22 | *
23 | * @param accountName
24 | * @param newToken
25 | */
26 | public void updateTokenForAccount(String accountName, String newToken) {
27 | AccountUtil.updateToken(accountName, newToken);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/service/MessageService.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.service;
2 |
3 | import com.lark.oapi.Client;
4 | import com.lark.oapi.service.im.v1.model.*;
5 | import com.rawchen.feishubot.util.MessageCard;
6 | import com.rawchen.feishubot.util.MessageContent;
7 | import com.rawchen.feishubot.util.StringUtil;
8 | import lombok.RequiredArgsConstructor;
9 | import lombok.extern.slf4j.Slf4j;
10 | import org.springframework.stereotype.Service;
11 |
12 | import java.util.Map;
13 | import java.util.UUID;
14 |
15 | @Service
16 | @RequiredArgsConstructor
17 | @Slf4j
18 | /**
19 | * 用于飞书机器人发送消息
20 | */
21 | public class MessageService {
22 | protected final Client client;
23 |
24 |
25 | private CreateMessageReq createMessageReq(String receiveIdType, String receiveId, String msgType, String content) {
26 |
27 | return CreateMessageReq.newBuilder()
28 | .receiveIdType(receiveIdType)
29 | .createMessageReqBody(CreateMessageReqBody.newBuilder()
30 | .receiveId(receiveId)
31 | .msgType(msgType)
32 | .content(content)
33 | .uuid(UUID.randomUUID().toString())
34 | .build())
35 | .build();
36 | }
37 |
38 | private PatchMessageReq createPatchMessageReq(String messageId, String content) {
39 |
40 | return PatchMessageReq.newBuilder()
41 | .messageId(messageId)
42 | .patchMessageReqBody(PatchMessageReqBody.newBuilder()
43 | .content(content)
44 | .build())
45 | .build();
46 | }
47 |
48 | /**
49 | * 发送文本消息
50 | *
51 | * @param chatId 用于表示聊天的id
52 | * @param text 发送的文本
53 | * @return
54 | * @throws Exception
55 | */
56 | public CreateMessageResp sendTextMessageByChatId(String chatId, String text) throws Exception {
57 | CreateMessageReq req = createMessageReq("chat_id", chatId, "text", MessageContent.ofText(text));
58 | return getCreateMessageResp(req);
59 | }
60 |
61 | private CreateMessageResp getCreateMessageResp(CreateMessageReq req) throws Exception {
62 | CreateMessageResp resp = null;
63 |
64 | resp = client.im().message().create(req);
65 | if (!resp.success()) {
66 | log.error(String.format("code:%s,msg:%s,reqId:%s"
67 | , resp.getCode(), resp.getMsg(), resp.getRequestId()));
68 | throw new Exception("发送text消息失败");
69 | }
70 | return resp;
71 | }
72 |
73 | /**
74 | * 发送卡片消息
75 | *
76 | * @param chatId
77 | * @param text
78 | * @return
79 | * @throws Exception
80 | */
81 | public CreateMessageResp sendCardMessage(String chatId, String text) throws Exception {
82 | CreateMessageReq messageReq = createMessageReq("chat_id", chatId, "interactive", text);
83 | return getCreateMessageResp(messageReq);
84 | }
85 |
86 | /**
87 | * 发送用户ChatGpt回复消息卡片的消息
88 | *
89 | * @param chatId
90 | * @param title
91 | * @param answer
92 | * @return
93 | * @throws Exception
94 | */
95 | public CreateMessageResp sendGptAnswerMessage(String chatId, String title, String answer) throws Exception {
96 | return sendCardMessage(chatId, MessageCard.ofGptAnswerMessageCard(title, answer));
97 | }
98 |
99 | public CreateMessageResp sendGptAnswerMessageWithSelection(String chatId, String title, String answer, Map selections) throws Exception {
100 | return sendCardMessage(chatId, MessageCard.ofGptAnswerMessageCardWithSelection(title, answer, selections));
101 | }
102 |
103 | /**
104 | * 修改类型为消息卡片的消息
105 | *
106 | * @param messageId
107 | * @param content
108 | * @return
109 | * @throws Exception
110 | */
111 | public PatchMessageResp modifyMessageCard(String messageId, String content) throws Exception {
112 | PatchMessageReq req = createPatchMessageReq(messageId, content);
113 | return client.im().message().patch(req);
114 | }
115 |
116 | /**
117 | * 修改用于ChatGpt回复的消息卡片的消息
118 | *
119 | * @param messageId
120 | * @param title
121 | * @param answer
122 | * @return
123 | * @throws Exception
124 | */
125 | public PatchMessageResp modifyGptAnswerMessageCard(String messageId, String title, String answer) throws Exception {
126 | return modifyMessageCard(messageId, MessageCard.ofGptAnswerMessageCard(title, answer));
127 | }
128 |
129 |
130 | public PatchMessageResp modifyGptAnswerMessageCardWithSelection(String messageId, String title, String answer, Map selections) throws Exception {
131 | return modifyMessageCard(messageId, MessageCard.ofGptAnswerMessageCardWithSelection(title, answer, selections));
132 | }
133 |
134 |
135 | }
136 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/service/UserService.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.service;
2 |
3 | import com.lark.oapi.Client;
4 | import com.lark.oapi.service.contact.v3.model.GetUserReq;
5 | import com.lark.oapi.service.contact.v3.model.GetUserResp;
6 | import com.lark.oapi.service.contact.v3.model.User;
7 | import lombok.RequiredArgsConstructor;
8 | import lombok.extern.slf4j.Slf4j;
9 | import org.springframework.stereotype.Service;
10 |
11 | @Service
12 | @Slf4j
13 | @RequiredArgsConstructor
14 | public class UserService {
15 | protected final Client client;
16 |
17 | public User getUserByOpenId(String openId) throws Exception {
18 | // 创建请求对象
19 | GetUserReq req = GetUserReq.newBuilder()
20 | .userId(openId)
21 | .userIdType("open_id")
22 | .departmentIdType("open_department_id")
23 | .build();
24 | // 发起请求
25 | GetUserResp resp = client.contact().user().get(req);
26 |
27 | // 处理服务端错误
28 | if (!resp.success()) {
29 | log.error("code:{},msg:{},reqId:{}"
30 | , resp.getCode(), resp.getMsg(), resp.getRequestId());
31 | throw new Exception("获取用户信息失败");
32 | }
33 | return resp.getData().getUser();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/util/MessageCard.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.util;
2 |
3 | import com.alibaba.fastjson.JSONArray;
4 | import com.alibaba.fastjson.JSONObject;
5 | import lombok.extern.slf4j.Slf4j;
6 |
7 | import java.util.Map;
8 |
9 | /**
10 | * 机器人回复消息卡片消息格式
11 | */
12 | @Slf4j
13 | public class MessageCard {
14 | private static JSONObject gptAnswerMessageCard;
15 |
16 | private static boolean hasSelection = false;
17 |
18 | private static void initChatGptAnswerMessageCard() {
19 | JSONObject markdownElement = new JSONObject();
20 | markdownElement.put("tag", "markdown");
21 | markdownElement.put("content", "");
22 |
23 | JSONObject title = new JSONObject();
24 | title.put("content", "");
25 | title.put("tag", "plain_text");
26 |
27 | JSONObject header = new JSONObject();
28 | header.put("template", "blue");
29 | header.put("title", title);
30 |
31 | JSONObject config = new JSONObject();
32 | config.put("wide_screen_mode", true);
33 | config.put("update_multi", true);
34 |
35 | // JSONObject line = new JSONObject();
36 | // line.put("tag", "hr");
37 |
38 | JSONObject jsonMessage = new JSONObject();
39 | // jsonMessage.put("elements", new JSONArray().put(markdownElement).put(line));
40 | JSONArray elements = new JSONArray();
41 | elements.add(markdownElement);
42 | jsonMessage.put("elements", elements);
43 |
44 | JSONObject jsonBody = new JSONObject();
45 | jsonBody.put("header", header);
46 | jsonBody.put("config", config);
47 | jsonBody.put("schema", "2.0");
48 | jsonBody.put("body", jsonMessage);
49 |
50 | gptAnswerMessageCard = jsonBody;
51 | }
52 |
53 | /**
54 | * 普通消息卡片格式
55 | *
56 | * @param title
57 | * @param content
58 | * @return
59 | */
60 | public static String ofGptAnswerMessageCard(String title, String content) {
61 | if (gptAnswerMessageCard == null) {
62 | initChatGptAnswerMessageCard();
63 | }
64 | JSONObject markdownElement = gptAnswerMessageCard.getJSONObject("body").getJSONArray("elements").getJSONObject(0);
65 | // 目前消息卡片不支持“```”代码块样式输出,因此可能会将代码块中一起出现“<”、“!”内容误识别xss注入后删掉
66 | // https://open.feishu.cn/document/common-capabilities/message-card/message-cards-content/using-markdown-tags
67 | // 卡片 JSON 2.0已支持基本的Markdown格式,除了![]()此格式飞书对链接做了校验,只能允许上传图片接口获取的key
68 | // https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-components/content-components/rich-text
69 | content = StringUtil.replaceSpecialSymbol(content);
70 | markdownElement.put("content", content);
71 | JSONObject titleObject = gptAnswerMessageCard.getJSONObject("header").getJSONObject("title");
72 | titleObject.put("content", title);
73 | return gptAnswerMessageCard.toString();
74 | }
75 |
76 | /**
77 | * 带模型选择的消息卡片
78 | *
79 | * @param title
80 | * @param content
81 | * @param selections
82 | * @return
83 | */
84 | public static String ofGptAnswerMessageCardWithSelection(String title, String content, Map selections) {
85 | if (hasSelection) {
86 | gptAnswerMessageCard = null;
87 | }
88 | ofGptAnswerMessageCard(title, content);
89 | JSONObject selection = new JSONObject();
90 | JSONArray array = new JSONArray();
91 | array.add(gptAnswerMessageCard.get("elements"));
92 |
93 | JSONObject tip = new JSONObject();
94 | array.add(tip);
95 | tip.put("tag", "div");
96 | JSONObject tipText = new JSONObject();
97 | tipText.put("content", "请选择接下来对话模型(不选则默认当前模型)");
98 | tipText.put("tag", "plain_text");
99 | tip.put("text", tipText);
100 |
101 | array.add(selection);
102 | selection.put("tag", "action");
103 |
104 | JSONArray actions = new JSONArray();
105 | selection.put("actions", actions);
106 |
107 | JSONObject action = new JSONObject();
108 | action.put("tag", "select_static");
109 | actions.add(action);
110 |
111 | JSONObject placeholder = new JSONObject();
112 | placeholder.put("content", "当前会话模型");
113 | placeholder.put("tag", "plain_text");
114 |
115 | action.put("placeholder", placeholder);
116 |
117 | JSONArray options = new JSONArray();
118 | action.put("options", options);
119 |
120 | for (String s : selections.keySet()) {
121 | JSONObject option = new JSONObject();
122 | options.add(option);
123 | option.put("value", selections.get(s));
124 | JSONObject text = new JSONObject();
125 | text.put("content", s);
126 | text.put("tag", "plain_text");
127 | option.put("text", text);
128 | }
129 | hasSelection = true;
130 | return gptAnswerMessageCard.toString();
131 | }
132 |
133 |
134 | }
135 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/util/MessageContent.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.util;
2 |
3 | public class MessageContent {
4 |
5 | public static String ofText(String text) {
6 | return String.format("{\"text\":\"%s\"}", text);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/util/StringUtil.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.util;
2 |
3 | import cn.hutool.core.util.StrUtil;
4 | import lombok.extern.slf4j.Slf4j;
5 |
6 | import java.util.regex.Matcher;
7 | import java.util.regex.Pattern;
8 |
9 | /**
10 | * @author RawChen
11 | * @date 2023-08-04
12 | */
13 | @Slf4j
14 | public class StringUtil {
15 | public static String replaceSpecialSymbol(String str) {
16 |
17 | if (StrUtil.isEmpty(str)) {
18 | return "";
19 | }
20 |
21 | Pattern pattern = Pattern.compile("!\\[([^\\]]*)]\\(([^\\)]*)\\)");
22 | Matcher matcher = pattern.matcher(str);
23 | while (matcher.find()) {
24 | // str = str.replaceAll("!\\[([^\\]]*)]\\(([^\\)]*)\\)", "``````");
25 | str = str.replaceAll("!\\[([^\\]]*)]\\(([^\\)]*)\\)", "");
26 | // String url = matcher.group(2);
27 | // String description = matcher.group(1);
28 | // System.out.println("URL: " + url);
29 | // System.out.println("Description: " + description);
30 | }
31 |
32 | // str = str.replace(">", ">");
33 | // str = str.replace("<", "<");
34 | // str = str.replace("~", "∼");
35 | // str = str.replace("-", "-");
36 | // str = str.replace("!", "!");
37 | // str = str.replace("*", "*");
38 | // str = str.replace("/", "/");
39 | // str = str.replace("\\", "\");
40 | // str = str.replace("[", "[");
41 | // str = str.replace("]", "]");
42 | // str = str.replace("(", "(");
43 | // str = str.replace(")", ")");
44 | // str = str.replace("#", "#");
45 | // str = str.replace(":", ":");
46 | // str = str.replace("+", "+");
47 | // str = str.replace("\"", """);
48 | // str = str.replace("'", "'");
49 | // str = str.replace("`", "`");
50 | // str = str.replace("$", "$");
51 | // str = str.replace("_", "_");
52 | // str = str.replace("-", "-");
53 |
54 | return str;
55 | }
56 |
57 | public static String escape(String input) {
58 | StringBuilder sb = new StringBuilder();
59 |
60 | for (int i = 0; i < input.length(); i++) {
61 | char c = input.charAt(i);
62 |
63 | switch (c) {
64 | case '\n':
65 | sb.append("\\n");
66 | break;
67 | case '\t':
68 | sb.append("\\t");
69 | break;
70 | case '\r':
71 | sb.append("\\r");
72 | break;
73 | case '\f':
74 | sb.append("\\f");
75 | break;
76 | case '\b':
77 | sb.append("\\b");
78 | break;
79 | case '\\':
80 | sb.append("\\\\");
81 | break;
82 | case '\'':
83 | sb.append("\\'");
84 | break;
85 | case '\"':
86 | sb.append("\\\"");
87 | break;
88 | default:
89 | sb.append(c);
90 | }
91 | }
92 |
93 | return sb.toString();
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/util/Task.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.util;
2 |
3 | import com.rawchen.feishubot.entity.gpt.Answer;
4 | import com.rawchen.feishubot.util.chatgpt.AnswerProcess;
5 | import lombok.Data;
6 | import lombok.extern.slf4j.Slf4j;
7 |
8 | @Data
9 | @Slf4j
10 | public class Task {
11 | private AnswerProcess process;
12 | private Answer answer;
13 | private String account;
14 |
15 | public Task(AnswerProcess process, Answer answer, String account) {
16 | this.process = process;
17 | this.answer = answer;
18 | this.account = account;
19 | }
20 |
21 | public void run() {
22 | try {
23 | process.process(answer);
24 | } catch (Exception e) {
25 | log.error("处理ChatGpt响应出错", e);
26 | log.error(answer.toString());
27 |
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/util/TaskPool.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.util;
2 |
3 | import java.util.HashMap;
4 | import java.util.List;
5 | import java.util.Map;
6 | import java.util.Set;
7 | import java.util.concurrent.BlockingQueue;
8 | import java.util.concurrent.LinkedBlockingQueue;
9 |
10 |
11 | public class TaskPool {
12 | private static final Map> taskPool = new HashMap<>();
13 |
14 | public static void init(List accounts) {
15 | for (String account : accounts) {
16 | taskPool.put(account, new LinkedBlockingQueue<>());
17 | }
18 | }
19 |
20 | public static void addTask(Task task) throws InterruptedException {
21 | BlockingQueue queue = taskPool.get(task.getAccount());
22 | queue.put(task);
23 | }
24 |
25 | public static void runTask() {
26 | Set accounts = taskPool.keySet();
27 | for (String account : accounts) {
28 | new Thread(() -> {
29 | BlockingQueue queue = taskPool.get(account);
30 | while (true) {
31 | try {
32 | Task task = queue.take();
33 | task.run();
34 | } catch (InterruptedException e) {
35 | e.printStackTrace();
36 | }
37 | }
38 | }).start();
39 | }
40 | }
41 |
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/util/chatgpt/AccountPool.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.util.chatgpt;
2 |
3 | import com.rawchen.feishubot.entity.Account;
4 | import com.rawchen.feishubot.entity.Status;
5 | import com.rawchen.feishubot.entity.gpt.Models;
6 | import com.rawchen.feishubot.service.AccountService;
7 | import com.rawchen.feishubot.util.TaskPool;
8 | import lombok.Data;
9 | import lombok.RequiredArgsConstructor;
10 | import lombok.extern.slf4j.Slf4j;
11 | import org.springframework.beans.factory.annotation.Value;
12 | import org.springframework.core.env.Environment;
13 | import org.springframework.stereotype.Component;
14 | import org.springframework.util.StringUtils;
15 |
16 | import javax.annotation.PostConstruct;
17 | import java.util.ArrayList;
18 | import java.util.HashMap;
19 | import java.util.List;
20 | import java.util.Map;
21 |
22 | /**
23 | * 账号池
24 | */
25 | @Component
26 | @RequiredArgsConstructor
27 | @Slf4j
28 | @Data
29 |
30 | public class AccountPool {
31 | protected final AccountService accountService;
32 |
33 | protected final Environment environment;
34 |
35 | private int size;
36 |
37 | @Value("${proxy.url}")
38 | private String proxyUrl;
39 |
40 | public static Map accountPool = new HashMap<>();
41 |
42 | public static Map normalPool = new HashMap<>();
43 |
44 | public static Map plusPool = new HashMap<>();
45 |
46 | /**
47 | * 初始化账号池
48 | */
49 | @PostConstruct
50 | public void init() {
51 | List accounts = accountService.getAccounts();
52 | List usefulAccounts = new ArrayList<>();
53 | for (Account account : accounts) {
54 |
55 | ChatService chatService = new ChatService(account.getAccount(), account.getPassword(), account.getToken(), proxyUrl);
56 | if (account.getToken() == null) {
57 | log.debug("账号{}未登录", account.getAccount());
58 | boolean ok = chatService.build();
59 | if (ok) {
60 | log.debug("账号{}登录成功", account.getAccount());
61 | account.setToken(chatService.getAccessToken());
62 | // accountService.updateTokenForAccount(account.getAccount(), chatService.getAccessToken());
63 |
64 | } else {
65 | //ChatGpt登录失败
66 | log.error("账号{}登录失败", account.getAccount());
67 | continue;
68 | }
69 | }
70 |
71 | //查询账号是否plus用户
72 | // boolean b = chatService.queryAccountLevel();
73 | //如果token失效,重新登录,更新token,重新查询一次
74 | // if (!b) {
75 | // log.debug("账号{}登录失效", account.getAccount());
76 | // log.debug("账号{}重新登录", account.getAccount());
77 | // boolean ok = chatService.build();
78 | // if (ok) {
79 | // //重新登录成功
80 | // account.setToken(chatService.getAccessToken());
81 | // accountService.updateTokenForAccount(account.getAccount(), chatService.getAccessToken());
82 | // b = chatService.queryAccountLevel();
83 | // if (!b) {
84 | // continue;
85 | // }
86 | // } else {
87 | // //ChatGpt登录失败
88 | // log.error("账号{}登录失败", account.getAccount());
89 | // continue;
90 | // }
91 | // }
92 |
93 |
94 | usefulAccounts.add(account.getAccount());
95 | accountPool.put(account.getAccount(), chatService);
96 | size++;
97 | if (chatService.getLevel() == 3) {
98 | // log.info("账号{}为normal用户", account.getAccount());
99 | log.info("账号{}为normal用户", account.getToken());
100 | normalPool.put(account.getAccount(), chatService);
101 | }
102 | if (chatService.getLevel() == 4) {
103 | // log.info("账号{}为plus用户", account.getAccount());
104 | log.info("账号{}为plus用户", account.getToken());
105 | plusPool.put(account.getAccount(), chatService);
106 | }
107 | }
108 |
109 | log.info("normal账号池共{}个账号", normalPool.size());
110 | log.info("plus账号池共{}个账号", plusPool.size());
111 | TaskPool.init(usefulAccounts);
112 | TaskPool.runTask();
113 | }
114 |
115 |
116 | /**
117 | * 需要plus模型,则从plus账号池中获取
118 | * 需要normal模型,则从normal账号池中获取
119 | *
120 | * @param model 模型title
121 | * @return
122 | */
123 | public ChatService getFreeChatService(String model) {
124 | if (!StringUtils.hasText(model)) {
125 | model = Models.NORMAL_DEFAULT_MODEL;
126 | }
127 |
128 | List plusAccountList = new ArrayList<>();
129 | for (String s : plusPool.keySet()) {
130 | ChatService chatService = plusPool.get(s);
131 | if (chatService.getStatus() == Status.FINISHED) {
132 | plusAccountList.add(chatService);
133 | }
134 | }
135 |
136 | //如果plus账号池中有账号,且需要plus模型,则从plus账号池中获取
137 | //如果需要的模型是空模型(新建会话,使用对应账号默认模型就行),也可以从plus账号池中获取
138 | if (plusAccountList.size() > 0 && (Models.plusModelTitle.contains(model) || model.equals(Models.EMPTY_MODEL))) {
139 | return plusAccountList.get((int) (Math.random() * plusAccountList.size()));
140 | }
141 |
142 | if (Models.plusModelTitle.contains(model)) {
143 | //如果需要plus模型,但是没有plus账号,返回null
144 | return null;
145 | }
146 | List normalAccountList = new ArrayList<>();
147 | for (String s : normalPool.keySet()) {
148 | ChatService chatService = normalPool.get(s);
149 | if (chatService.getStatus() == Status.FINISHED) {
150 | normalAccountList.add(chatService);
151 | }
152 | }
153 | if (normalAccountList.size() == 0) {
154 | return null;
155 | } else {
156 | return normalAccountList.get((int) (Math.random() * normalAccountList.size()));
157 | }
158 | }
159 |
160 | public void modifyChatService(ChatService chatService) {
161 | log.info("修改账号{}信息", chatService.getAccount());
162 | Account account = new Account();
163 | account.setAccount(chatService.getAccount());
164 | account.setToken(chatService.getAccessToken());
165 | account.setPassword(chatService.getPassword());
166 | accountService.updateTokenForAccount(chatService.getAccount(), chatService.getAccessToken());
167 | }
168 |
169 | public ChatService getChatService(String account) {
170 | if (account == null || account.equals("")) {
171 | if (plusPool.containsKey(account)) {
172 | return getFreeChatService(Models.PLUS_DEFAULT_MODEL);
173 | } else {
174 | return getFreeChatService(Models.NORMAL_DEFAULT_MODEL);
175 | }
176 | }
177 | return accountPool.get(account);
178 | }
179 |
180 | public static void removeAccount(String account) {
181 | log.info("移除账号{}", account);
182 | accountPool.remove(account);
183 | normalPool.remove(account);
184 | plusPool.remove(account);
185 | }
186 |
187 | }
188 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/util/chatgpt/AccountUtil.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.util.chatgpt;
2 |
3 | import com.rawchen.feishubot.entity.Account;
4 | import com.rawchen.feishubot.entity.AccountList;
5 | import lombok.extern.slf4j.Slf4j;
6 | import org.springframework.core.io.FileSystemResource;
7 | import org.yaml.snakeyaml.DumperOptions;
8 | import org.yaml.snakeyaml.Yaml;
9 | import org.yaml.snakeyaml.constructor.Constructor;
10 | import org.yaml.snakeyaml.representer.Representer;
11 |
12 | import java.io.FileWriter;
13 | import java.io.IOException;
14 | import java.io.InputStream;
15 | import java.util.List;
16 | import java.util.Set;
17 |
18 | /**
19 | * 操作account配置文件工具类
20 | */
21 | @Slf4j
22 | public class AccountUtil {
23 | private static final String YAML_PATH = "accounts.yaml";
24 |
25 | public static AccountList readAccounts() {
26 | Yaml yaml = new Yaml(new Constructor(AccountList.class));
27 | try (InputStream in = new FileSystemResource(YAML_PATH).getInputStream()) {
28 | return yaml.loadAs(in, AccountList.class);
29 | } catch (IOException e) {
30 | throw new RuntimeException("Failed to read YAML", e);
31 | }
32 | }
33 |
34 | public static void writeAccounts(AccountList accountList) {
35 | DumperOptions options = new DumperOptions();
36 | options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
37 | options.setPrettyFlow(true);
38 | Yaml yaml = new Yaml(new Representer(), options);
39 | try (FileWriter writer = new FileWriter(YAML_PATH)) {
40 | yaml.dump(accountList, writer);
41 | } catch (IOException e) {
42 | throw new RuntimeException("Failed to write YAML", e);
43 | }
44 | }
45 |
46 | public static void queryAccountLevel() {
47 | Set accounts = AccountPool.accountPool.keySet();
48 | for (String account : accounts) {
49 | ChatService chatService = AccountPool.accountPool.get(account);
50 | if (chatService != null) {
51 | int level = chatService.getLevel();
52 | chatService.queryAccountLevel();
53 | if (level != chatService.getLevel()) {
54 | log.info("账户「{}」等级变更: {} -> {}", account, level, chatService.getLevel());
55 | }
56 | //如果是plus账号降级
57 | //从plus账号池移动到普通账号池
58 | if (level == 4 && chatService.getLevel() == 3) {
59 | AccountPool.plusPool.remove(account);
60 | AccountPool.normalPool.put(account, chatService);
61 | }
62 |
63 | //如果是普通账号升级
64 | //从普通账号池移动到plus账号池
65 | if (level == 3 && chatService.getLevel() == 4) {
66 | AccountPool.normalPool.remove(account);
67 | AccountPool.plusPool.put(account, chatService);
68 | }
69 | }
70 | }
71 | }
72 |
73 | public static void updateToken(String accountName, String newToken) {
74 | AccountList accountList = readAccounts();
75 | List accounts = accountList.getAccounts();
76 | for (Account account : accounts) {
77 | if (account.getAccount().equals(accountName)) {
78 | account.setToken(newToken);
79 | break;
80 | }
81 | }
82 | writeAccounts(accountList);
83 | }
84 |
85 | public static void refreshToken() {
86 | AccountList accountList = readAccounts();
87 | List accounts = accountList.getAccounts();
88 | for (Account account : accounts) {
89 | if (account.getAccount() != null && account.getPassword() != null) {
90 | ChatService chatService = AccountPool.accountPool.get(account.getAccount());
91 | if (chatService != null) {
92 | chatService.refreshToken();
93 | }
94 | }
95 | }
96 | writeAccounts(accountList);
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/util/chatgpt/AnswerProcess.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.util.chatgpt;
2 |
3 | import com.rawchen.feishubot.entity.gpt.Answer;
4 |
5 | public interface AnswerProcess {
6 |
7 | void process(Answer answer) throws Exception;
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/util/chatgpt/ChatService.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.util.chatgpt;
2 |
3 | import cn.hutool.core.util.EscapeUtil;
4 | import cn.hutool.http.HttpRequest;
5 | import cn.hutool.http.HttpResponse;
6 | import cn.hutool.http.HttpUtil;
7 | import cn.hutool.json.JSONUtil;
8 | import com.alibaba.fastjson.JSONArray;
9 | import com.alibaba.fastjson.JSONObject;
10 | import com.rawchen.feishubot.entity.Status;
11 | import com.rawchen.feishubot.entity.gpt.*;
12 | import com.rawchen.feishubot.entity.gptRequestBody.CreateConversationBody;
13 | import com.rawchen.feishubot.util.StringUtil;
14 | import com.rawchen.feishubot.util.Task;
15 | import com.rawchen.feishubot.util.TaskPool;
16 | import lombok.Data;
17 | import lombok.extern.slf4j.Slf4j;
18 | import java.util.List;
19 | import java.util.UUID;
20 | import java.util.concurrent.Semaphore;
21 |
22 | @Data
23 | @Slf4j
24 | public class ChatService {
25 |
26 | private static final String ACCOUNT_LEVEL_URL = "/chatgpt/backend-api/models?history_and_training_disabled=false";
27 | private String account;
28 | private String password;
29 | private String accessToken;
30 | private int level;
31 | private volatile Status status;
32 | private Semaphore semaphore = new Semaphore(1);
33 |
34 | private StringBuilder errorString;
35 |
36 | private String proxyUrl;
37 |
38 | private static final String LOGIN_URL = "/chatgpt/login";
39 | private static final String CHAT_URL = "/v1/chat/completions";
40 | private static final String LIST_URL = "/chatgpt/backend-api/conversations?offset=0&limit=20";
41 | private static final String GEN_TITLE_URL = "/chatgpt/backend-api/conversation/gen_title/";
42 |
43 |
44 | public ChatService() {
45 | this.status = Status.FINISHED;
46 | }
47 |
48 | public ChatService(String account, String password, String accessToken, String proxyUrl) {
49 | this.accessToken = accessToken;
50 | this.account = account;
51 | this.password = password;
52 | this.proxyUrl = proxyUrl;
53 | this.status = Status.FINISHED;
54 | this.level = 3;
55 | }
56 |
57 | /**
58 | * 账户构建:接口登录获取token
59 | * 目前转为三方获取,因此无需登录,直接使用token
60 | *
61 | * @return
62 | */
63 | public boolean build() {
64 | // if (password == null || password.equals("")) {
65 | // log.error("账号{}密码为空", account);
66 | // return false;
67 | // }
68 | // log.info("账号{}开始登录", account);
69 | // String loginUrl = proxyUrl + LOGIN_URL;
70 | // HashMap paramMap = new HashMap<>();
71 | // paramMap.put("username", account);
72 | // paramMap.put("password", password);
73 | //
74 | // String params = JSONUtil.toJsonPrettyStr(paramMap);
75 | // String result = HttpUtil.post(loginUrl, params);
76 | //
77 | // JSONObject jsonObject = new JSONObject(result);
78 | //
79 | // if (jsonObject.opt("errorMessage") != null) {
80 | // log.error("账号{}登录失败:{}", account, jsonObject.opt("errorMessage"));
81 | // return false;
82 | // }
83 | // accessTokenaccessToken = jsonObject.optString("accessToken");
84 | // log.info("账号{}登录成功", account);
85 | return true;
86 | }
87 |
88 | public String getToken() {
89 | return "Bearer " + accessToken;
90 | }
91 |
92 | private void chat(String content, String model, AnswerProcess process, String parentMessageId, String conversationId) throws InterruptedException {
93 | semaphore.acquire();
94 | try {
95 | String createConversationUrl = proxyUrl + CHAT_URL;
96 | UUID uuid = UUID.randomUUID();
97 | String messageId = uuid.toString();
98 |
99 | String param = CreateConversationBody.of(messageId, content, parentMessageId, conversationId, model);
100 | // log.info("param: {}", param);
101 | post(param, createConversationUrl, process);
102 | } finally {
103 | semaphore.release();
104 | }
105 | }
106 |
107 | /**
108 | * 新建会话
109 | *
110 | * @param content 对话内容
111 | * @param model 模型
112 | * @param process 回调
113 | * @throws InterruptedException
114 | */
115 | public void newChat(String content, String model, AnswerProcess process) throws InterruptedException {
116 | chat(content, model, process, "", "");
117 | }
118 |
119 | /**
120 | * 继续会话
121 | *
122 | * @param content 对话内容
123 | * @param model 模型
124 | * @param parentMessageId 父消息id
125 | * @param conversationId 会话id
126 | * @param process 回调
127 | * @throws InterruptedException
128 | */
129 | public void keepChat(String content, String model, String parentMessageId, String conversationId, AnswerProcess process) throws InterruptedException {
130 | chat(content, model, process, parentMessageId, conversationId);
131 | }
132 |
133 | public void genTitle(String conversationId) {
134 | String listUrl = proxyUrl + GEN_TITLE_URL + conversationId;
135 | HttpResponse response = HttpRequest.get(listUrl).header("Authorization", getToken()).execute();
136 | log.info(response.body());
137 | }
138 |
139 |
140 | public void getConversationList() {
141 | String listUrl = proxyUrl + LIST_URL;
142 | HttpResponse response = HttpRequest.get(listUrl).header("Authorization", getToken()).execute();
143 | }
144 |
145 | /**
146 | * 向gpt发起请求
147 | *
148 | * @param param 请求参数
149 | * @param urlStr 请求的地址
150 | * @param process 响应处理器
151 | */
152 | private void post(String param, String urlStr, AnswerProcess process) {
153 | Answer answer = new Answer();
154 | try {
155 | String body = HttpRequest.post(urlStr)
156 | .header("Authorization", getToken())
157 | .body(param)
158 | .execute()
159 | .body();
160 | // log.info("Chat API Result: {}", body);
161 | JSONObject jsonObject = JSONObject.parseObject(body);
162 | JSONArray choices = jsonObject.getJSONArray("choices");
163 | if (choices == null || jsonObject.getJSONObject("error") != null) {
164 | JSONObject error = jsonObject.getJSONObject("error");
165 | if (error != null) {
166 | String msg = error.getString("message");
167 | log.error("Chat API Result Error: {}", body);
168 | //异步处理
169 | answer.setSuccess(true);
170 | answer.setFinished(true);
171 | answer.setAnswer(msg);
172 | TaskPool.addTask(new Task(process, answer, this.account));
173 | }
174 | } else {
175 | JSONObject choiceOne = choices.getJSONObject(0);
176 | JSONObject messageObject = choiceOne.getJSONObject("message");
177 | String messageContent = messageObject.getString("content");
178 | log.info("返回结果: {}", StringUtil.escape(messageContent));
179 | //异步处理
180 | answer.setSuccess(true);
181 | answer.setFinished(true);
182 | answer.setAnswer(messageContent);
183 | TaskPool.addTask(new Task(process, answer, this.account));
184 | }
185 |
186 |
187 |
188 | // 获取并处理响应
189 | // int status = connection.getResponseCode();
190 | // Reader streamReader = null;
191 | // boolean error = false;
192 | // errorString = new StringBuilder();
193 | // if (status > 299) {
194 | // streamReader = new InputStreamReader(connection.getErrorStream());
195 | // log.error("请求失败,状态码:{}", status);
196 | // error = true;
197 | // } else {
198 | // streamReader = new InputStreamReader(connection.getInputStream());
199 | // }
200 |
201 | // BufferedReader reader = new BufferedReader(streamReader);
202 | // String line;
203 |
204 | // int count = 0;
205 | // while ((line = reader.readLine()) != null) {
206 | // if (line.length() == 0) {
207 | // continue;
208 | // }
209 | // if (error) {
210 | // errorString.append(line);
211 | // log.error(line);
212 | //
213 | // continue;
214 | // }
215 | //
216 | // try {
217 | // count++;
218 | // answer = parse(line);
219 | //
220 | // if (answer == null) {
221 | // continue;
222 | // }
223 | //
224 | // answer.setSeq(count);
225 | // //每10行 才处理一次 为了防止飞书发消息太快被限制频率
226 | // if (answer.isSuccess() && !answer.isFinished() && count % 10 != 0) {
227 | // continue;
228 | // }
229 | //
230 | // if (answer.isSuccess() && !answer.getMessage().getAuthor().getRole().equals("assistant")) {
231 | // continue;
232 | // }
233 | //
234 | // //异步处理
235 | // TaskPool.addTask(new Task(process, answer, this.account));
236 | // } catch (Exception e) {
237 | // log.error("解析ChatGpt响应出错", e);
238 | // log.error(line);
239 | // }
240 | // }
241 |
242 | // if (error) {
243 | // answer = new Answer();
244 | // answer.setError(errorString.toString());
245 | // answer.setErrorCode(ErrorCode.RESPONSE_ERROR);
246 | // answer.setSuccess(false);
247 | //
248 | // try {
249 | // JSONObject jsonObject = new JSONObject(errorString.toString());
250 | // String detail = jsonObject.optString("detail");
251 | // if (detail != null) {
252 | // JSONObject detailObject = new JSONObject(detail);
253 | // String code = detailObject.optString("code");
254 | // if (code.equals("account_deactivated")) {
255 | // answer.setErrorCode(ErrorCode.ACCOUNT_DEACTIVATED);
256 | // }
257 | // }
258 | //
259 | //
260 | // } catch (JSONException ignored) {
261 | // }
262 | // TaskPool.addTask(new Task(process, answer, this.account));
263 | // }
264 |
265 | // reader.close();
266 | // connection.disconnect();
267 | } catch (Exception e) {
268 | log.error("请求出错", e);
269 | }
270 | }
271 |
272 |
273 | // private Answer parse(String body) {
274 | // System.out.println(body);
275 | // Answer answer;
276 | //
277 | // if (body.startsWith("data: [DONE]") || body.startsWith("data: {\"conversation_id\"")) {
278 | // return null;
279 | // }
280 | // if (body.startsWith("data:")) {
281 | //
282 | // body = body.substring(body.indexOf("{"));
283 | // answer = JSONUtil.toBean(body, Answer.class);
284 | // answer.setSuccess(true);
285 | // if (answer.getMessage().getStatus().equals("finished_successfully")) {
286 | // answer.setFinished(true);
287 | // }
288 | // Message message = answer.getMessage();
289 | // Content content = message.getContent();
290 | // List parts = content.getParts();
291 | // if (parts != null) {
292 | // String part = parts.get(0);
293 | // answer.setAnswer(part);
294 | // }
295 | // if (content.getText() != null) {
296 | // answer.setAnswer(content.getText());
297 | // }
298 | //
299 | // } else {
300 | // answer = new Answer();
301 | // answer.setSuccess(false);
302 | // JSONObject jsonObject = new JSONObject(body);
303 | // String detail = jsonObject.optString("detail");
304 | // if (detail != null && detail.contains("Only one message")) {
305 | // log.warn("账号{}忙碌中", account);
306 | // answer.setErrorCode(ErrorCode.BUSY);
307 | // answer.setError(detail);
308 | // return answer;
309 | // }
310 | // if (detail != null && detail.contains("code")) {
311 | // JSONObject error = jsonObject.optJSONObject("detail");
312 | // String code = (String) error.opt("code");
313 | // if (code.equals("invalid_jwt")) {
314 | // answer.setErrorCode(ErrorCode.INVALID_JWT);
315 | // } else if (code.equals("invalid_api_key")) {
316 | // answer.setErrorCode(ErrorCode.INVALID_API_KEY);
317 | // } else if (code.equals("model_cap_exceeded")) {
318 | // answer.setErrorCode(ErrorCode.CHAT_LIMIT);
319 | // } else {
320 | // log.error(body);
321 | // log.warn("账号{} token失效", account);
322 | // }
323 | // answer.setError(error.get("message"));
324 | // return answer;
325 | // }
326 | // log.error("未知错误:{}", body);
327 | // log.error("账号{}未知错误:{}", account, body);
328 | // answer.setError(body);
329 | // }
330 | // return answer;
331 | // }
332 |
333 | public void refreshToken() {
334 | build();
335 | }
336 |
337 | /**
338 | * 查询账号可用模型从而判断账号是否plus用户
339 | *
340 | * @return 查询是否成功 不成功的原因一般为token失效
341 | */
342 | public boolean queryAccountLevel() {
343 | String url = proxyUrl + ACCOUNT_LEVEL_URL;
344 | HttpResponse response = HttpRequest.get(url).header("Authorization", getToken()).execute();
345 | String body = response.body();
346 | JSONObject jsonObject = JSONObject.parseObject(body);
347 | String models = jsonObject.getString("models");
348 | if (models == null || models.length() == 0) {
349 | log.warn("账号{}查询模型解析失败 : {}", account, body);
350 | return false;
351 | }
352 | cn.hutool.json.JSONArray objects = JSONUtil.parseArray(models);
353 | List list = JSONUtil.toList(objects, Model.class);
354 | boolean plus = false;
355 | for (Model model : list) {
356 | Models.modelMap.put(model.getTitle(), model);
357 | if (model.getSlug().startsWith("gpt-4")) {
358 | Models.plusModelTitle.add(model.getTitle());
359 | plus = true;
360 | } else {
361 | Models.normalModelTitle.add(model.getTitle());
362 | Models.NORMAL_DEFAULT_MODEL = model.getTitle();
363 | }
364 | }
365 | this.setLevel(plus ? 4 : 3);
366 | return true;
367 | }
368 | }
369 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/util/chatgpt/ConversationPool.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.util.chatgpt;
2 |
3 | import com.rawchen.feishubot.entity.Conversation;
4 | import lombok.extern.slf4j.Slf4j;
5 | import org.springframework.stereotype.Component;
6 |
7 | import java.util.HashMap;
8 | import java.util.Map;
9 |
10 | /**
11 | * 会话池
12 | * 记录用户和对应gpt会话上下文的对应关系
13 | * 通过chatId来区分用户,一个用户对应一个会话
14 | * 从而实现gpt具有上下文能力
15 | */
16 | @Component
17 | @Slf4j
18 | public class ConversationPool {
19 | public volatile Map conversationMap = new HashMap<>();
20 |
21 | public void addConversation(String chatId, Conversation conversation) {
22 | // log.info("chatId:{} 会话变更前:{}", chatId, conversationMap.get(chatId));
23 | // log.info("chatId:{} 会话变更后:{}", chatId, conversation);
24 | conversationMap.put(chatId, conversation);
25 | }
26 |
27 | public Conversation getConversation(String chatId) {
28 | return conversationMap.get(chatId);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/rawchen/feishubot/util/chatgpt/RequestIdSet.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot.util.chatgpt;
2 |
3 | import java.util.HashSet;
4 | import java.util.Set;
5 |
6 | /**
7 | * 用来保存飞书推送过来的请求id
8 | * 事件可能会重复推送,所以需要去重
9 | */
10 | public class RequestIdSet {
11 | public static final Set requestIdSet = new HashSet<>();
12 | }
13 |
--------------------------------------------------------------------------------
/src/test/java/com/rawchen/feishubot/FeishuBotApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.rawchen.feishubot;
2 |
3 | import com.lark.oapi.service.im.v1.model.CreateMessageResp;
4 | import com.lark.oapi.service.im.v1.model.PatchMessageResp;
5 | import com.rawchen.feishubot.service.MessageService;
6 | import org.junit.jupiter.api.Test;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.boot.test.context.SpringBootTest;
9 |
10 | @SpringBootTest
11 | class FeishuBotApplicationTests {
12 |
13 | @Autowired
14 | MessageService messageService;
15 |
16 | @Test
17 | void contextLoads() {
18 | }
19 |
20 | @Test
21 | void test01() throws Exception {
22 | messageService.sendTextMessageByChatId("oc_e44b7351982677b1d768c3730cc37c13", "123");
23 | }
24 |
25 | @Test
26 | void test02() throws Exception {
27 | CreateMessageResp createMessageResp = messageService.sendGptAnswerMessage("oc_e44b7351982677b1d768c3730cc37c13", "title123", "12345");
28 | System.out.println(createMessageResp.getData().getMessageId());
29 | }
30 |
31 | @Test
32 | void test03() throws Exception {
33 | messageService.modifyMessageCard("om_ff84ab31f48201d498f24aca3aa5f23e", "34235235");
34 | }
35 |
36 | @Test
37 | void test04() throws Exception {
38 | String i = "A";
39 | while (true) {
40 | String temp = i + "A";
41 | PatchMessageResp patchMessageResp = messageService.modifyGptAnswerMessageCard("om_ff84ab31f48201d498f24aca3aa5f23e", "title123", temp);
42 | System.out.println(patchMessageResp.getMsg());
43 | i = temp;
44 | try {
45 | Thread.sleep(500);
46 | } catch (InterruptedException e) {
47 | e.printStackTrace();
48 | }
49 | }
50 |
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | nohup java -Xmn48m -Xms128m -Xmx128m -Xss256k -jar FeishuBot.jar >> app.log &
3 | echo $! > /var/run/FeishuBot.pid
4 |
--------------------------------------------------------------------------------
/stop.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | PID=$(cat /var/run/FeishuBot.pid)
3 | kill -9 $PID
4 |
--------------------------------------------------------------------------------