This commit is contained in:
2025-10-16 22:33:07 +08:00
parent c9cc800ed9
commit 5fbc33be74
37 changed files with 7044 additions and 1139 deletions

View File

@@ -63,6 +63,20 @@
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- 工具类(字符串处理等) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
<profiles>
<profile>

View File

@@ -40,6 +40,33 @@ public class ApiServerV1 {
.create();
private static final Map<String, Status> statusMap = new ConcurrentHashMap<>();
private static volatile List<Server> tempServerList = new CopyOnWriteArrayList<>();
@Scheduled(cron = "0 0 0 * * ?") // 每天凌晨执行
public void dailyDatabaseCleanup() {
log.info("开始执行每日数据库整理任务");
try {
List<Status> historicalServers = statusDao.findAll();
//如果90天意外的数据删除
List<Status> needDelete = historicalServers.stream()
.filter(server -> server.getTimestamp().isBefore(LocalDateTime.now().minusDays(90)))
.collect(Collectors.toList());
statusDao.deleteAll(needDelete);
log.info("已删除{}条数据", needDelete.size());
//删除所有的process数据
List<Status> update = historicalServers.stream()
.map(server -> {
server.setProcesses(null);
return server;
})
.collect(Collectors.toList());
statusDao.saveAll(update);
log.info("已更新{}条数据", update.size());
} catch (Exception e) {
log.error("每日数据库整理任务执行失败", e);
}
}
@PostConstruct
@Scheduled(fixedRate = 60000)

View File

@@ -0,0 +1,56 @@
package org.ast.reisaadminspring.been;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.time.LocalDateTime;
@Document(collection = "chat_messages")
public class ChatMessage {
@Id
private String id;
private String sessionId;
private String role; // "user" 或 "assistant"
private String content;
private LocalDateTime timestamp;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
}

View File

@@ -3,6 +3,7 @@ package org.ast.reisaadminspring.been;
import org.springframework.data.mongodb.core.mapping.Document;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Document
@@ -25,6 +26,13 @@ public class Status {
// Constructors
public Status() {}
// 在 Status 类中添加
public String getFormattedTimestamp() {
if (timestamp != null) {
return timestamp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
return null;
}
public Long getTime() {
return time;

View File

@@ -0,0 +1,22 @@
package org.ast.reisaadminspring.been;
public class Tool {
private String name;
private String args;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getArgs() {
return args;
}
public void setArgs(String args) {
this.args = args;
}
}

View File

@@ -0,0 +1,27 @@
package org.ast.reisaadminspring.been;
import java.util.List;
public class ToolDecision {
private boolean shouldUseTool;
private List<Tool> toolArgs;
public ToolDecision(boolean b) {
this.shouldUseTool = b;
}
public boolean isShouldUseTool() {
return shouldUseTool;
}
public void setShouldUseTool(boolean shouldUseTool) {
this.shouldUseTool = shouldUseTool;
}
public List<Tool> getToolArgs() {
return toolArgs;
}
public void setToolArgs(List<Tool> toolArgs) {
this.toolArgs = toolArgs;
}
}

View File

@@ -0,0 +1,26 @@
package org.ast.reisaadminspring.been.milvus;
import java.util.List;
// add_documents 接口请求参数(包含 documents 数组)
public class AddDocumentRequest {
// 待添加的文档列表
private List<Document> documents;
// 无参构造
public AddDocumentRequest() {}
// 有参构造
public AddDocumentRequest(List<Document> documents) {
this.documents = documents;
}
// getter/setter
public List<Document> getDocuments() {
return documents;
}
public void setDocuments(List<Document> documents) {
this.documents = documents;
}
}

View File

@@ -0,0 +1,29 @@
package org.ast.reisaadminspring.been.milvus;
// add_documents 接口响应结果
public class AddDocumentResponse {
// 成功添加的文档数量(如 1
private Integer added_count;
// 响应消息如“Successfully added 1 document chunks...”)
private String message;
// 无参构造
public AddDocumentResponse() {}
// getter/setter
public Integer getAdded_count() {
return added_count;
}
public void setAdded_count(Integer added_count) {
this.added_count = added_count;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@@ -0,0 +1,34 @@
package org.ast.reisaadminspring.been.milvus;
public class Document {
// 文档内容如“Rening是一个熟悉运维...的可爱萝莉”)
private String content;
// 文档来源如“Rening介绍”
private String source;
// 无参构造Gson 反序列化需默认构造)
public Document() {}
// 有参构造(方便创建对象)
public Document(String content, String source) {
this.content = content;
this.source = source;
}
// 手动编写 getter/setter
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
}

View File

@@ -0,0 +1,24 @@
package org.ast.reisaadminspring.been.milvus;
// query 接口请求参数(仅需传入 question
public class QueryRequest {
// 待查询的问题如“Reisasol是谁
private String question;
// 无参构造
public QueryRequest() {}
// 有参构造
public QueryRequest(String question) {
this.question = question;
}
// getter/setter
public String getQuestion() {
return question;
}
public void setQuestion(String question) {
this.question = question;
}
}

View File

@@ -0,0 +1,41 @@
package org.ast.reisaadminspring.been.milvus;
import java.util.List;
// query 接口响应结果
public class QueryResponse {
// 最终回答如“Reisasol(零咲)是一只由Reisa研发的猫娘。”
private String answer;
// 回答的上下文(多个检索文档内容拼接)
private String context;
// 检索到的相关文档列表(按相关性排序)
private List<RetrievedDoc> retrieved_docs;
// 无参构造
public QueryResponse() {}
// getter/setter
public String getAnswer() {
return answer;
}
public void setAnswer(String answer) {
this.answer = answer;
}
public String getContext() {
return context;
}
public void setContext(String context) {
this.context = context;
}
public List<RetrievedDoc> getRetrieved_docs() {
return retrieved_docs;
}
public void setRetrieved_docs(List<RetrievedDoc> retrieved_docs) {
this.retrieved_docs = retrieved_docs;
}
}

View File

@@ -0,0 +1,39 @@
package org.ast.reisaadminspring.been.milvus;
// 响应中“检索到的文档”结构
public class RetrievedDoc {
// 相似度距离(数值越小,与问题相关性越高)
private Double distance;
// 文档来源如“ReiSaol角色定义”
private String source;
// 文档文本内容(生成回答的上下文)
private String text;
// 无参构造
public RetrievedDoc() {}
// getter/setter
public Double getDistance() {
return distance;
}
public void setDistance(Double distance) {
this.distance = distance;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}

View File

@@ -0,0 +1,57 @@
package org.ast.reisaadminspring.been.ollama;
import java.util.List;
import java.util.Map;
/**
* Ollama消息请求类
* 用于构建发送给Ollama API的请求消息
*/
public class OllamaRequest {
private String model;
private List<Map<String, Object>> messages;
private boolean stream;
private String keep_alive;
// Constructors
public OllamaRequest() {
}
public OllamaRequest(String model, List<Map<String, Object>> messages) {
this.model = model;
this.messages = messages;
}
// Getters and Setters
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public List<Map<String, Object>> getMessages() {
return messages;
}
public void setMessages(List<Map<String, Object>> messages) {
this.messages = messages;
}
public boolean isStream() {
return stream;
}
public void setStream(boolean stream) {
this.stream = stream;
}
public String getKeep_alive() {
return keep_alive;
}
public void setKeep_alive(String keep_alive) {
this.keep_alive = keep_alive;
}
}

View File

@@ -0,0 +1,111 @@
package org.ast.reisaadminspring.been.ollama;
public class OllamaResponse {
private String model;
private String createdAt;
private Message message;
private boolean done;
private long total_duration;
private long load_duration;
private int prompt_eval_count;
private long prompt_eval_duration;
private int eval_count;
private long eval_duration;
// 内部类用于表示消息对象
public static class Message {
private String role;
private String content;
// Getters and Setters
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
// Getters and Setters
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public Message getMessage() {
return message;
}
public void setMessage(Message message) {
this.message = message;
}
public boolean isDone() {
return done;
}
public void setDone(boolean done) {
this.done = done;
}
public long getTotal_duration() {
return total_duration;
}
public void setTotal_duration(long total_duration) {
this.total_duration = total_duration;
}
public long getLoad_duration() {
return load_duration;
}
public void setLoad_duration(long load_duration) {
this.load_duration = load_duration;
}
public int getEval_count() {
return eval_count;
}
public void setEval_count(int eval_count) {
this.eval_count = eval_count;
}
public void setPrompt_eval_count(int prompt_eval_count) {
this.prompt_eval_count = prompt_eval_count;
}
public void setPrompt_eval_duration(long prompt_eval_duration) {
this.prompt_eval_duration = prompt_eval_duration;
}
public long getEval_duration() {
return eval_duration;
}
public void setEval_duration(long eval_duration) {
this.eval_duration = eval_duration;
}
}

View File

@@ -0,0 +1,165 @@
package org.ast.reisaadminspring.bot;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import jakarta.annotation.PostConstruct;
import org.ast.reisaadminspring.been.Tool;
import org.ast.reisaadminspring.been.ollama.OllamaResponse;
import org.ast.reisaadminspring.bot.core.C2CMessageSender;
import org.ast.reisaadminspring.bot.core.MessageSender;
import org.ast.reisaadminspring.bot.core.WebSocketBot;
import org.ast.reisaadminspring.service.MailService;
import org.ast.reisaadminspring.service.OllamaService;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Service
public class ReningBot {
private final OllamaService ollamaService;
private Gson gson = new Gson();
public ReningBot(OllamaService ollamaService) {
this.ollamaService = ollamaService;
}
@Autowired
private MailService mailService;
public interface MessageHandler {
void sendText(String content, int index);
void sendImage(String content,String base64,int index) throws IOException;
}
private ExecutorService messageExecutor = Executors.newFixedThreadPool(10); // 根据业务需求调整线程数
private Logger log = org.slf4j.LoggerFactory.getLogger(ReningBot.class);
@PostConstruct
public void init() {
// 启动Bot
WebSocketBot bot = new WebSocketBot("102815116", "emv4DMVenx7HRblv6HSdozAMYkw8KWiv");
bot.setCustomMessageHandler((message, sender) -> {
messageExecutor.submit(() -> {
try {
String id = message.get("id").getAsString();
aiBotDo(message, sender);
} catch (Exception e) {
String groupId = message.get("group_id").getAsString();
String id = message.get("id").getAsString();
sender.sendText(groupId, id, "处理消息时发生错误,请稍后再试。", 100);
log.error("消息处理异常", e);
}
});
});
bot.setCustomC2CMessageHandler((message, sender) -> {
messageExecutor.submit(() -> {
try {
String content = message.get("content").getAsString();
String openId = message.get("author").getAsJsonObject().get("id").getAsString();
String messageId = message.get("id").getAsString();
// 获取消息内容
aiBotDoC2C(message, sender, content, openId, messageId);
} catch (Exception e) {
e.printStackTrace();
}
});
});
bot.connect();
bot.startMessageProcessing();
System.out.println("Rening启动");
}
// 修改 aiBotDoC2C 方法
private void aiBotDoC2C(JsonObject message, C2CMessageSender sender, String content, String openId, String messageId) {
MessageHandler messageHandler = new MessageHandler() {
int index = 0;
@Override
public void sendText(String content, int index) {
this.index++;
sender.sendText(openId, messageId, content, this.index);
}
@Override
public void sendImage(String content,String base64, int index) throws IOException {
this.index++;
sender.sendImage(openId, messageId, content,base64,this.index);
}
};
try {
processMessage(content,openId, messageHandler);
} catch (Exception e) {
sender.sendText(messageId, openId, "处理消息时发生错误,请稍后再试。", 100);
log.error("消息处理异常", e);
}
}
// 修改 aiBotDo 方法
private void aiBotDo(JsonObject message, MessageSender sender) throws Exception {
String content = message.get("content").getAsString();
String groupId = message.get("group_id").getAsString();
String userId = message.get("author").getAsJsonObject().get("id").getAsString();
String messageId = message.get("id").getAsString();
MessageHandler messageHandler = new MessageHandler() {
int index = 0;
@Override
public void sendText(String content, int index) {
this.index++;
sender.sendText(groupId, messageId, content, this.index);
}
@Override
public void sendImage(String content, String base64, int index) throws IOException {
this.index++;
sender.sendImage(groupId, messageId, content, base64, this.index);
}
};
processMessage(content,userId, messageHandler);
}
private void processMessage(String content,String sessionId, MessageHandler messageHandler) {
log.info("收到消息:{}", content);
//指令处理
if (content.contains("/")) {
String command = content.split("/")[1];
boolean ifRun = false;
switch ( command){
case "help":
messageHandler.sendText("指令列表:\n",1003);
ifRun = true;
break;
case "clear":
messageHandler.sendText("已清空上下文。", 1003);
ollamaService.clearSession(sessionId);
ifRun = true;
break;
}
if (ifRun) {
return;
}
}
try {
Map<Tool,String> toolStringMap = new HashMap<>();
OllamaResponse response = ollamaService.getResponse(sessionId, content,toolStringMap,messageHandler);
System.out.println(gson.toJson( response));
if (response.isDone()) {
int x = 0;
messageHandler.sendText(response.getMessage().getContent().replaceAll("\n\n","\n").replaceAll("\\(继续调用:..\\)",""), 1);
// for (Tool tool : toolStringMap.keySet()) {
// x ++;
// messageHandler.sendText("使用工具:" + tool.getName() + ",参数:" + gson.toJson(tool.getArgs()) + "\n返回内容:" + toolStringMap.get(tool), 10000+x);
// }
}
}catch (Exception e) {
log.error("处理消息时发生错误", e);
messageHandler.sendText("处理消息时发生错误,请稍后再试。", 100);
}
}
}

View File

@@ -0,0 +1,13 @@
// C2CMessageHandler.java
package org.ast.reisaadminspring.bot.core;
import com.google.gson.JsonObject;
public interface C2CMessageHandler {
/**
* 处理C2C消息
* @param message 消息内容
* @param sender 消息发送器
*/
void handleC2CMessage(JsonObject message, C2CMessageSender sender);
}

View File

@@ -0,0 +1,31 @@
// C2CMessageSender.java
package org.ast.reisaadminspring.bot.core;
import com.google.gson.JsonObject;
import java.io.IOException;
public interface C2CMessageSender {
/**
* 发送文本消息
* @param openId 用户openid
* @param id 消息id
* @param text 文本内容
* @param msgSeq 消息序号
* @return 响应结果
*/
JsonObject sendText(String openId, String id, String text, int msgSeq);
JsonObject withDraw( String openId, String id) throws IOException;
/**
* 发送图片消息
* @param openId 用户openid
* @param id 消息id
* @param text 文本内容
* @param imageUrl 图片URL
* @param msgSeq 消息序号
* @return 响应结果
* @throws IOException IO异常
*/
JsonObject sendImage(String openId, String id, String text, String imageUrl, int msgSeq) throws IOException;
}

View File

@@ -0,0 +1,9 @@
package org.ast.reisaadminspring.bot.core;
import com.google.gson.JsonObject;
import java.io.IOException;
public interface MessageHandler {
void handleMessage(JsonObject message, MessageSender sender) throws InterruptedException, IOException;
}

View File

@@ -0,0 +1,11 @@
package org.ast.reisaadminspring.bot.core;
import com.google.gson.JsonObject;
import java.io.IOException;
public interface MessageSender {
JsonObject sendText(String groupId, String id, String text, int msgSeq);
JsonObject sendImage(String groupId, String id, String text, String imageUrl, int msgSeq) throws IOException;
JsonObject withDraw( String groupId, String id) throws IOException;
}

View File

@@ -0,0 +1,823 @@
package org.ast.reisaadminspring.bot.core;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import okhttp3.*;
import okio.ByteString;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class WebSocketBot {
private static final String accessUrl = "https://bots.qq.com/app/getAppAccessToken";
private static final String wsserverUrl = "wss://api.sgroup.qq.com/websocket";
// private static final String wsserverUrl = "wss://tencentbot.godserver.cn:8443";
private static final String chatUrl = "https://api.sgroup.qq.com";
private static String session_id = "";
private String appId = "";
private String clientSecret = "";
private static long seq = 0;
// 在 WebSocketBot 类中添加代理相关字段
private boolean useProxy = false;
private static String PROXY_HOST = "100.80.156.98";
private static int PROXY_PORT = 7892;
private static final String PROXY_USERNAME = "34iawgzer8";
private static final String PROXY_PASSWORD = "h9ht;;;";
// SOCKS5代理实例OkHttp使用
// ======================== 2. 配置带认证的SOCKS5代理 ========================
// 代理认证器处理Clash的用户名密码验证
private static final Authenticator PROXY_AUTHENTICATOR = new Authenticator() {
@Override
public Request authenticate(Route route, Response response) throws IOException {
// 生成代理认证的Base64编码格式用户名:密码)
String credential = Credentials.basic(PROXY_USERNAME, PROXY_PASSWORD);
// 给请求添加代理认证头
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
}
};
private static final Proxy SOCKS5_PROXY = new Proxy(
Proxy.Type.SOCKS,
new InetSocketAddress(PROXY_HOST, PROXY_PORT)
);
private OkHttpClient client = getClient();
private OkHttpClient getClient() {
if (!useProxy) {
return new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
}
return new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.proxy(SOCKS5_PROXY) // 添加SOCKS5代理
.proxyAuthenticator(PROXY_AUTHENTICATOR) // 添加代理认证
// .proxy(new Proxy(proxyType, new InetSocketAddress(proxyHost, proxyPort))) // 新增代理
.build();
}
private OkHttpClient webClient = client;
private OkHttpClient messageClient = webClient;
private WebSocket webSocket;
private String authToken;
private boolean isConnected = false;
private long heartbeatInterval = 0;
private long messageId;
private final Map<String, JsonObject> messages = new ConcurrentHashMap<>();
private final Map<String, JsonObject> returnMessages = new ConcurrentHashMap<>();
private final Map<String, JsonObject> c2cmessages = new ConcurrentHashMap<>();
private final Map<String, JsonObject> c2creturnMessages = new ConcurrentHashMap<>();
private final Deque<String> c2cdeque = new ArrayDeque<>();
private final Deque<String> deque = new ArrayDeque<>();
private boolean run = true;
private static boolean isRunning = true;
private MessageHandler customMessageHandler;
private static Gson gson = new Gson();
private int waitSeconds = 0;
private static String latesetId = "";
public static Logger logger = org.slf4j.LoggerFactory.getLogger(WebSocketBot.class);
public WebSocketBot(String appId, String clientSecret) {
this.appId = appId;
this.clientSecret = clientSecret;
getAuthToken();
logger.info("[{}]开始连接", this.appId);
}
public static String getLatesetId() {
return latesetId;
}
public static void setLatesetId(String latesetId) {
WebSocketBot.latesetId = latesetId;
}
private void getAuthToken() {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("appId", this.appId);
jsonObject.addProperty("clientSecret", this.clientSecret);
RequestBody requestBody = RequestBody.create(
MediaType.parse("application/json; charset=utf-8"),
jsonObject.toString()
);
Request request = new Request.Builder()
.url(accessUrl)
.post(requestBody)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code: " + response.code());
}
String responseBody = response.body().string();
JsonObject jsonObjectRes = JsonParser.parseString(responseBody).getAsJsonObject();
authToken = jsonObjectRes.get("access_token").getAsString();
waitSeconds = jsonObjectRes.get("expires_in").getAsInt();
//启动异步waitSeconds等待
new Thread(() -> {
try {
logger.info("[{}]开始等待:{}秒后刷新令牌", this.appId, waitSeconds);
System.out.println("启动 令牌刷新 等待:" + waitSeconds + "秒后刷新令牌");
Thread.sleep(waitSeconds * 1000L);
if (!isConnected) {
return;
}
getAuthToken();
logger.info("[{}]获取令牌成功,authToken:{}", this.appId, authToken);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
System.out.println("authToken:" + authToken);
logger.info("[{}]获取令牌成功,authToken:{}", this.appId, authToken);
} catch (IOException e) {
logger.error("[{}]获取令牌失败:{}", this.appId, e.getMessage());
System.err.println("获取令牌失败: " + e.getMessage());
// 建议添加重试逻辑或更友好的错误处理
throw new RuntimeException("无法获取认证令牌,请检查网络或配置", e);
}
}
private static boolean inConnecting = false;
public void setCustomMessageHandler(MessageHandler handler) {
this.customMessageHandler = handler;
}
// 在 WebSocketBot 类中添加字段
private C2CMessageHandler customC2CMessageHandler;
// 添加设置自定义C2C消息处理器的方法
public void setCustomC2CMessageHandler(C2CMessageHandler handler) {
this.customC2CMessageHandler = handler;
}
private boolean hasReing = false;
public void connect() {
Request request = new Request.Builder()
.url(wsserverUrl)
.header("Authorization", "QQBot " + authToken)
.header("Content-Type", "application/json")
.build();
final String useAppID = this.appId;
WebSocketListener listener = new WebSocketListener() {
@Override
public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
WebSocketBot.this.webSocket = webSocket;
isConnected = true;
}
@Override
public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
try {
handleMessage(text);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) {
//System.out.println("收到二进制消息,长度: " + bytes.size());
}
@Override
public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
//System.err.println( "WebSocket is closing... code: " + code + " reason: " + reason);
logger.warn("[{}]WebSocket is closing... code: {} reason: {}", useAppID, code, reason);
isConnected = false;
if (code != 4924) { // 非IP黑名单情况
if (code != 4903) {
reconnect();
} else {
System.err.println("超出会话限制,请等待或关闭");
isRunning = false;
}
} else { // IP黑名单情况 (code == 4924)
logger.error("[{}]Need IP White Page", useAppID);
System.err.println("Need IP White Page");
if (!hasReing) {
reconnectWithIPBlacklistStrategy();
}
}
}
@Override
public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, Response response) {
logger.error("[{}]WebSocket 连接失败: {}", useAppID, t.getMessage());
// 检查是否是IP黑名单导致的连接失败
if (t.getMessage() != null && t.getMessage().contains("4924")) {
logger.error("[{}]WebSocket 连接失败疑似IP黑名单", useAppID);
reconnectWithIPBlacklistStrategy();
} else {
if (!hasReing) {
reconnectWithIPBlacklistStrategy();
}
}
}
@Override
public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
logger.warn("[{}]WebSocket is closed... code: {} reason: {}", useAppID, code, reason);
//System.err.println( "WebSocket is closing... code: " + code + " reason: " + reason);
//reconnect();
}
};
webClient.newWebSocket(request, listener);
}
private void reconnectWithIPBlacklistStrategy() {
useProxy = true;
client = getClient();
webClient = client;
messageClient = webClient;
logger.warn("[{}]使用代理重连", appId);
reconnect();
}
private void sendLogin() {
if (isRunning) {
JsonObject loginJsonObject = new JsonObject();
long intents = (1L) | (1L << 1) | (1L << 25);
loginJsonObject.addProperty("op", 2);
JsonObject d = new JsonObject();
d.addProperty("token", "QQBot " + authToken);
d.addProperty("intents", intents);
JsonArray shard = new JsonArray();
shard.add(0);
shard.add(1);
d.add("shard", shard);
JsonObject properties = new JsonObject();
properties.addProperty("$os", "Windows");
properties.addProperty("$browser", "Chrome");
properties.addProperty("$device", "");
loginJsonObject.add("d", d);
webSocket.send(loginJsonObject.toString());
//System.out.println(loginJsonObject.toString());
System.out.println("鉴权中...");
logger.info("[{}]正在鉴权...", this.appId);
}
}
public void disconnect() {
if (webSocket != null) {
webSocket.close(1000, "正常关闭");
webSocket = null;
}
}
private void reconnect() {
try {
Thread.sleep(1000);
logger.info("[{}]尝试重新连接...", this.appId);
connect();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void handleMessage(String message) throws IOException {
JsonObject jsonObject = gson.fromJson(message, JsonObject.class);
isConnected = true;
int op = jsonObject.get("op").getAsInt();
if (jsonObject.has("s")) {
seq = jsonObject.get("s").getAsLong();
}
if (op == 9) {
System.err.println("出错!");
System.err.println("bot已关闭!");
}
if (op == 10) {
heartbeatInterval = jsonObject.get("d").getAsJsonObject().get("heartbeat_interval").getAsLong();
sendLogin();
}
if (op == 0) {
String t = jsonObject.get("t").getAsString();
if (t.equals("READY")) {
logger.info("[{}]已登录", this.appId);
//System.out.println("登录成功!");
session_id = jsonObject.get("d").getAsJsonObject().get("session_id").getAsString();
loginFinish(jsonObject);
} else if (t.equals("GROUP_AT_MESSAGE_CREATE")) {
messageAdd(message);
} else if (t.equals("C2C_MESSAGE_CREATE")) {
c2cmessageAdd(message);
}
}
if (op == 2) {
loginFinish(jsonObject);
}
}
private void c2cmessageAdd(String message) {
JsonObject jsonObject = gson.fromJson(message, JsonObject.class);
JsonObject d = jsonObject.getAsJsonObject("d");
String id = d.get("id").getAsString();
c2cmessages.put(id, d);
c2cdeque.add(id);
}
private void messageAdd(String message) throws IOException {
JsonObject jsonObject = gson.fromJson(message, JsonObject.class);
JsonObject d = jsonObject.getAsJsonObject("d");
String id = d.get("id").getAsString();
messages.put(id, d);
deque.add(id);
}
public void startMessageProcessing() {
new Thread(() -> {
while (run) {
if (!deque.isEmpty()) {
String id = deque.poll();
try {
messageDo(id);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(() -> {
while (run) {
if (!c2cdeque.isEmpty()) {
String id = c2cdeque.poll();
try {
c2cmessageDo(id);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
// 添加获取C2C返回消息的方法
public Map<String, JsonObject> getC2cReturnMessages() {
return c2creturnMessages;
}
// 修改 c2cmessageDo 方法
private void c2cmessageDo(String id) throws IOException, InterruptedException {
JsonObject jsonObject = c2cmessages.get(id);
String openId = jsonObject.get("author").getAsJsonObject().get("id").getAsString();
if (customC2CMessageHandler != null) {
C2CMessageSender sender = new C2CMessageSender() {
@Override
public JsonObject sendText(String openId, String id, String text, int msgSeq) {
c2csendChatText(openId, id, text, msgSeq);
return c2creturnMessages.get(id);
}
@Override
public JsonObject withDraw(String openId, String id) throws IOException {
c2cdeleteMessage(openId, id);
return c2creturnMessages.get(id);
}
@Override
public JsonObject sendImage(String openId, String id, String text, String imageUrl, int msgSeq) throws IOException {
c2csendChat(openId, id, text, imageUrl, msgSeq);
return c2creturnMessages.get(id);
}
};
try {
customC2CMessageHandler.handleC2CMessage(jsonObject, sender);
} catch (Exception e) {
// 记录异常但不中断处理流程
System.err.println("处理C2C消息时发生异常: " + e.getMessage());
e.printStackTrace();
}
} else {
// 默认处理逻辑
try {
c2csendChatText(openId, id, "私聊示例", 1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void messageDo(String id) throws IOException, InterruptedException {
JsonObject jsonObject = messages.get(id);
String groupId = jsonObject.get("group_id").getAsString();
if (customMessageHandler != null) {
MessageSender sender = new MessageSender() {
@Override
public JsonObject sendText(String groupId, String id, String text, int msgSeq) {
sendChatText(groupId, id, text, msgSeq);
JsonObject jsonObject = returnMessages.get(id);
if (jsonObject.has("id")) {
latesetId = jsonObject.get("id").getAsString();
}
return returnMessages.get(id);
}
@Override
public JsonObject sendImage(String groupId, String id, String text, String imageUrl, int msgSeq) throws IOException {
sendChat(groupId, id, text, imageUrl, msgSeq);
JsonObject jsonObject = returnMessages.get(id);
if (jsonObject.has("id")) {
latesetId = jsonObject.get("id").getAsString();
}
return returnMessages.get(id);
}
@Override
public JsonObject withDraw(String groupId, String id) throws IOException {
deleteMessage(groupId, id);
return returnMessages.get(id);
}
};
try {
customMessageHandler.handleMessage(jsonObject, sender);
}catch (Exception e) {}
} else {
// 默认逻辑
try {
sendChat(groupId, id, "示范图像返回", "https://collections.hoshino.network/Chara/UI_Chara_505901.png", 1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void deleteMessage(String groupId, String id) {
String url = chatUrl + "/v2/groups/" + groupId + "/messages/" + id;
Request request = new Request.Builder()
.url(url)
.delete()
.addHeader("Authorization", "QQBot " + authToken)
.build();
try {
try (Response response = messageClient.newCall(request).execute()) {
JsonObject jsonObject = new JsonObject();
jsonObject = gson.fromJson(response.body().string(), JsonObject.class);
returnMessages.put(id, jsonObject);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void sendChatText(String groupId, String id, String text, int msgSeq) {
String url = chatUrl + "/v2/groups/" + groupId + "/messages";
JsonObject sendJsonObject = new JsonObject();
sendJsonObject.addProperty("group_openid", groupId);
sendJsonObject.addProperty("msg_type", 0);
sendJsonObject.addProperty("content", text);
sendJsonObject.addProperty("msg_id", id);
sendJsonObject.addProperty("msg_seq", msgSeq);
RequestBody requestBody = RequestBody.create(
MediaType.get("application/json; charset=utf-8"),
sendJsonObject.toString()
);
Request request = new Request.Builder()
.url(url)
.addHeader("Authorization", "QQBot " + authToken)
.post(requestBody)
.build();
try (Response response = messageClient.newCall(request).execute()) {
JsonObject jsonObject = new JsonObject();
jsonObject = gson.fromJson(response.body().string(), JsonObject.class);
returnMessages.put(id, jsonObject);
} catch (Exception e) {
e.printStackTrace();
}
}
private void sendChat(String groupId, String id, String text, String imageUrl, int msgSeq) throws IOException {
//判断imageUrl是不是url
if (!imageUrl.startsWith("http")) {
sendChatBase64(groupId, id, text, imageUrl, msgSeq);
return;
}
String fileUrl = chatUrl + "/v2/groups/" + groupId + "/files";
JsonObject fileJsonObject = new JsonObject();
fileJsonObject.addProperty("url", imageUrl);
fileJsonObject.addProperty("file_type", 1);
fileJsonObject.addProperty("srv_send_msg", false);
Request fileRequest = new Request.Builder()
.url(fileUrl)
.addHeader("Authorization", "QQBot " + authToken)
.post(RequestBody.create(fileJsonObject.toString(), MediaType.get("application/json")))
.build();
Response fileResponse = client.newCall(fileRequest).execute();
if (fileResponse.isSuccessful()) {
JsonObject returnFileJson = gson.fromJson(fileResponse.body().string(), JsonObject.class);
JsonObject sendJsonObject = new JsonObject();
sendJsonObject.addProperty("group_openid", groupId);
sendJsonObject.addProperty("msg_type", 7);
sendJsonObject.addProperty("content", text);
sendJsonObject.add("media", returnFileJson);
sendJsonObject.addProperty("msg_id", id);
sendJsonObject.addProperty("msg_seq", msgSeq);
sendJsonObject.addProperty("event_id", "GROUP_MSG_RECEIVE");
Request postRequest = new Request.Builder()
.url(chatUrl + "/v2/groups/" + groupId + "/messages")
.addHeader("Authorization", "QQBot " + authToken)
.post(RequestBody.create(sendJsonObject.toString(), MediaType.get("application/json")))
.build();
try (Response response = messageClient.newCall(postRequest).execute()) {
JsonObject jsonObject = new JsonObject();
jsonObject = gson.fromJson(response.body().string(), JsonObject.class);
System.out.println(jsonObject);
returnMessages.put(id, jsonObject);
} catch (Exception e) {
e.printStackTrace();
}
}else{
JsonObject returnFileJson = gson.fromJson(fileResponse.body().string(), JsonObject.class);
returnMessages.put(id, returnFileJson);
}
}
private void sendChatBase64(String groupId, String id, String text, String base64, int msgSeq) throws IOException {
String fileUrl = chatUrl + "/v2/groups/" + groupId + "/files";
JsonObject fileJsonObject = new JsonObject();
fileJsonObject.addProperty("file_data", base64);
fileJsonObject.addProperty("file_type", 1);
fileJsonObject.addProperty("srv_send_msg", false);
Request fileRequest = new Request.Builder()
.url(fileUrl)
.addHeader("Authorization", "QQBot " + authToken)
.post(RequestBody.create(fileJsonObject.toString(), MediaType.get("application/json")))
.build();
Response fileResponse = client.newCall(fileRequest).execute();
if (fileResponse.isSuccessful()) {
JsonObject returnFileJson = gson.fromJson(fileResponse.body().string(), JsonObject.class);
JsonObject sendJsonObject = new JsonObject();
sendJsonObject.addProperty("group_openid", groupId);
sendJsonObject.addProperty("msg_type", 7);
sendJsonObject.addProperty("content", text);
sendJsonObject.add("media", returnFileJson);
sendJsonObject.addProperty("msg_id", id);
sendJsonObject.addProperty("msg_seq", msgSeq);
sendJsonObject.addProperty("event_id", "GROUP_MSG_RECEIVE");
Request postRequest = new Request.Builder()
.url(chatUrl + "/v2/groups/" + groupId + "/messages")
.addHeader("Authorization", "QQBot " + authToken)
.post(RequestBody.create(sendJsonObject.toString(), MediaType.get("application/json")))
.build();
try (Response response = messageClient.newCall(postRequest).execute()) {
JsonObject jsonObject = new JsonObject();
jsonObject = gson.fromJson(response.body().string(), JsonObject.class);
System.out.println(jsonObject);
returnMessages.put(id, jsonObject);
} catch (Exception e) {
e.printStackTrace();
}
}else{
JsonObject returnFileJson = gson.fromJson(fileResponse.body().string(), JsonObject.class);
returnMessages.put(id, returnFileJson);
}
}
private void c2csendChatBase64(String openId, String id, String text, String base64, int msgSeq) throws IOException {
String fileUrl = chatUrl + "/v2/users/" + openId + "/files";
JsonObject fileJsonObject = new JsonObject();
fileJsonObject.addProperty("file_data", base64);
fileJsonObject.addProperty("file_type", 1);
fileJsonObject.addProperty("srv_send_msg", false);
Request fileRequest = new Request.Builder()
.url(fileUrl)
.addHeader("Authorization", "QQBot " + authToken)
.post(RequestBody.create(fileJsonObject.toString(), MediaType.get("application/json")))
.build();
Response fileResponse = client.newCall(fileRequest).execute();
if (fileResponse.isSuccessful()) {
JsonObject returnFileJson = gson.fromJson(fileResponse.body().string(), JsonObject.class);
JsonObject sendJsonObject = new JsonObject();
sendJsonObject.addProperty("openId", openId);
sendJsonObject.addProperty("msg_type", 7);
sendJsonObject.addProperty("content", text);
sendJsonObject.add("media", returnFileJson);
sendJsonObject.addProperty("msg_id", id);
sendJsonObject.addProperty("msg_seq", msgSeq);
sendJsonObject.addProperty("event_id", "GROUP_MSG_RECEIVE");
Request postRequest = new Request.Builder()
.url(chatUrl + "/v2/users/" + openId + "/messages")
.addHeader("Authorization", "QQBot " + authToken)
.post(RequestBody.create(sendJsonObject.toString(), MediaType.get("application/json")))
.build();
try (Response response = messageClient.newCall(postRequest).execute()) {
JsonObject jsonObject = new JsonObject();
jsonObject = gson.fromJson(response.body().string(), JsonObject.class);
System.out.println(jsonObject);
returnMessages.put(id, jsonObject);
} catch (Exception e) {
e.printStackTrace();
}
}else{
JsonObject returnFileJson = gson.fromJson(fileResponse.body().string(), JsonObject.class);
returnMessages.put(id, returnFileJson);
}
}
private void c2csendChatText(String openId, String id, String text, int msgSeq) {
String url = chatUrl + "/v2/users/" + openId + "/messages";
JsonObject sendJsonObject = new JsonObject();
sendJsonObject.addProperty("openId", openId);
sendJsonObject.addProperty("msg_type", 0);
sendJsonObject.addProperty("content", text);
sendJsonObject.addProperty("msg_id", id);
sendJsonObject.addProperty("msg_seq", msgSeq);
RequestBody requestBody = RequestBody.create(
MediaType.get("application/json; charset=utf-8"),
sendJsonObject.toString()
);
Request request = new Request.Builder()
.url(url)
.addHeader("Authorization", "QQBot " + authToken)
.post(requestBody)
.build();
try (Response response = messageClient.newCall(request).execute()) {
JsonObject jsonObject = new JsonObject();
jsonObject = gson.fromJson(response.body().string(), JsonObject.class);
returnMessages.put(id, jsonObject);
} catch (Exception e) {
e.printStackTrace();
}
}
private void c2csendChat(String openId, String id, String text, String imageUrl, int msgSeq) throws IOException {
//判断imageUrl是不是url
if (!imageUrl.startsWith("http")) {
c2csendChatBase64(openId, id, text, imageUrl, msgSeq);
return;
}
String fileUrl = chatUrl + "/v2/users/" + openId + "/files";
JsonObject fileJsonObject = new JsonObject();
fileJsonObject.addProperty("url", imageUrl);
fileJsonObject.addProperty("file_type", 1);
fileJsonObject.addProperty("srv_send_msg", false);
Request fileRequest = new Request.Builder()
.url(fileUrl)
.addHeader("Authorization", "QQBot " + authToken)
.post(RequestBody.create(fileJsonObject.toString(), MediaType.get("application/json")))
.build();
Response fileResponse = client.newCall(fileRequest).execute();
if (fileResponse.isSuccessful()) {
JsonObject returnFileJson = gson.fromJson(fileResponse.body().string(), JsonObject.class);
JsonObject sendJsonObject = new JsonObject();
sendJsonObject.addProperty("group_openid", openId);
sendJsonObject.addProperty("msg_type", 7);
sendJsonObject.addProperty("content", text);
sendJsonObject.add("media", returnFileJson);
sendJsonObject.addProperty("msg_id", id);
sendJsonObject.addProperty("msg_seq", msgSeq);
sendJsonObject.addProperty("event_id", "C2C_MSG_RECEIVE");
Request postRequest = new Request.Builder()
.url(chatUrl + "/v2/users/" + openId + "/messages")
.addHeader("Authorization", "QQBot " + authToken)
.post(RequestBody.create(sendJsonObject.toString(), MediaType.get("application/json")))
.build();
try (Response response = messageClient.newCall(postRequest).execute()) {
JsonObject jsonObject = new JsonObject();
jsonObject = gson.fromJson(response.body().string(), JsonObject.class);
System.out.println(jsonObject);
returnMessages.put(id, jsonObject);
} catch (Exception e) {
e.printStackTrace();
}
}else{
JsonObject returnFileJson = gson.fromJson(fileResponse.body().string(), JsonObject.class);
returnMessages.put(id, returnFileJson);
}
}
private void c2cdeleteMessage(String groupId, String id) {
String url = chatUrl + "/v2/users/" + groupId + "/messages/" + id;
Request request = new Request.Builder()
.url(url)
.delete()
.addHeader("Authorization", "QQBot " + authToken)
.build();
try {
try (Response response = messageClient.newCall(request).execute()) {
JsonObject jsonObject = new JsonObject();
jsonObject = gson.fromJson(response.body().string(), JsonObject.class);
returnMessages.put(id, jsonObject);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void loginFinish(JsonObject jsonObject) {
messageId = jsonObject.get("s").getAsLong();
startHeartbeat();
}
private void startHeartbeat() {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
JsonObject hb = new JsonObject();
hb.addProperty("op", 1);
hb.addProperty("d", messageId);
webSocket.send(hb.toString());
}
}, 0, heartbeatInterval);
}
public Map<String, JsonObject> getMessages() {
return messages;
}
public Map<String, JsonObject> getReturnMessages() {
return returnMessages;
}
public Deque<String> getDeque() {
return deque;
}
public void acquireTokenAndStart(Runnable onReady) {
new Thread(() -> {
OkHttpClient tokenClient = new OkHttpClient();
String authToken = null;
while (true) {
JsonObject payload = new JsonObject();
payload.addProperty("appId", this.appId);
payload.addProperty("clientSecret", this.clientSecret);
RequestBody body = RequestBody.create(payload.toString(), MediaType.get("application/json"));
Request request = new Request.Builder().url(accessUrl).post(body).build();
try (Response response = tokenClient.newCall(request).execute()) {
JsonObject res = gson.fromJson(response.body().string(), JsonObject.class);
authToken = res.get("access_token").getAsString();
break;
} catch (Exception e) {
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
WebSocketBot bot = new WebSocketBot(wsserverUrl, authToken);
bot.connect();
bot.startMessageProcessing();
if (onReady != null) onReady.run();
}).start();
}
}

View File

@@ -0,0 +1,20 @@
package org.ast.reisaadminspring.dao;
import org.ast.reisaadminspring.been.ChatMessage;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ChatHistoryRepository extends MongoRepository<ChatMessage, String> {
// 按会话ID查询消息并按时间戳排序
List<ChatMessage> findBySessionIdOrderByTimestampAsc(String sessionId);
// 按会话ID删除所有消息
void deleteBySessionId(String sessionId);
// 统计会话消息数量
long countBySessionId(String sessionId);
}

View File

@@ -1,15 +1,18 @@
package org.ast.reisaadminspring.dao;
import io.lettuce.core.dynamic.annotation.Param;
import org.ast.reisaadminspring.been.Server;
import org.ast.reisaadminspring.been.Status;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
@Service
public interface StatusDao extends MongoRepository<Status, String> {
List<Status> findByHost(String host);
List<Status> findByHostAndTimestampBetweenOrderByTimestampDesc(String host, LocalDateTime startTime, LocalDateTime endTime);
List<Status> findByHostOrderByTimestampDesc(String ip);
}

View File

@@ -0,0 +1,65 @@
package org.ast.reisaadminspring.service;
import org.ast.reisaadminspring.been.ChatMessage;
import org.ast.reisaadminspring.dao.ChatHistoryRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class ChatHistoryService {
private final ChatHistoryRepository historyRepository;
private static final int MAX_HISTORY_SIZE = 30;
public ChatHistoryService(ChatHistoryRepository historyRepository) {
this.historyRepository = historyRepository;
}
/**
* 保存消息到历史记录,并确保不超过最大数量
*/
@Transactional
public void saveMessage(String sessionId, String role, String content) {
// 保存新消息
ChatMessage message = new ChatMessage();
message.setSessionId(sessionId);
message.setRole(role);
message.setContent(content);
message.setTimestamp(LocalDateTime.now());
historyRepository.save(message);
// 检查是否超过最大记录数,如果超过则删除最旧的
long count = historyRepository.countBySessionId(sessionId);
if (count > MAX_HISTORY_SIZE) {
List<ChatMessage> messages = historyRepository.findBySessionIdOrderByTimestampAsc(sessionId);
// 删除超出部分只保留最新的30条
for (int i = 0; i < count - MAX_HISTORY_SIZE; i++) {
historyRepository.delete(messages.get(i));
}
}
}
/**
* 获取指定会话的历史记录
*/
public List<ChatMessage> getHistory(String sessionId) {
return historyRepository.findBySessionIdOrderByTimestampAsc(sessionId);
}
/**
* 清除指定会话的历史记录
*/
public void clearHistory(String sessionId) {
historyRepository.deleteBySessionId(sessionId);
}
/**
* 获取指定会话的历史消息,用于构建上下文
*/
public List<ChatMessage> getHistoryForContext(String sessionId) {
return getHistory(sessionId);
}
}

View File

@@ -0,0 +1,106 @@
package org.ast.reisaadminspring.service;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
@Service
@Component
public class MailService {
@Autowired
JavaMailSender javaMailSender;
public void sendSimpleMail(String from, String to, String cc, String subject, String content) {
SimpleMailMessage simpMsg = new SimpleMailMessage();
simpMsg.setFrom(from);
simpMsg.setTo(to);
simpMsg.setCc(cc);
simpMsg.setSubject(subject);
simpMsg.setText(content);
javaMailSender.send(simpMsg);
}
public void sendImageMail(String from, String to, String cc, String subject, String text, BufferedImage image) {
try {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setCc(cc);
helper.setSubject(subject);
// 构建 HTML 内容并内嵌图片
String htmlContent = "<p>" + text + "</p><img src='cid:image' />";
helper.setText(htmlContent, true);
// 将 BufferedImage 转换为字节数组
ByteArrayOutputStream stream = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", stream); // 可根据实际格式改为 JPEG 等
ByteArrayResource resource = new ByteArrayResource(stream.toByteArray());
// 添加图片资源并指定 cid
helper.addInline("image", resource, "image/png");
javaMailSender.send(message);
} catch (IOException | MessagingException e) {
e.printStackTrace();
}
}
/**
* 发送包含多张图片的邮件
*
* @param from 发件人
* @param to 收件人
* @param cc 抄送人(可为 null
* @param subject 邮件主题
* @param text 正文文本
* @param images BufferedImage 列表,例如 List<BufferedImage>
*/
public void sendMultipleImageMail(String from, String to, String cc, String subject, String text, List<BufferedImage> images) {
try {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setCc(cc);
helper.setSubject(subject);
// 构建 HTML 内容并内嵌多张图片
StringBuilder htmlContent = new StringBuilder("<p>" + text + "</p>");
for (int i = 0; i < images.size(); i++) {
BufferedImage image = images.get(i);
String cid = "image" + i;
// 将 BufferedImage 转换为字节数组
ByteArrayOutputStream stream = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", stream); // 可改为 JPEG 等格式
ByteArrayResource resource = new ByteArrayResource(stream.toByteArray());
// 添加内联图片
helper.addInline(cid, resource, "image/png");
// 在 HTML 中引用
htmlContent.append("<img src='cid:").append(cid).append("' style='margin:10px 0;'/>");
}
helper.setText(htmlContent.toString(), true);
javaMailSender.send(message);
} catch (MessagingException | IOException e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,88 @@
package org.ast.reisaadminspring.service;
import com.google.gson.Gson;
import okhttp3.*;
import org.ast.reisaadminspring.been.milvus.AddDocumentRequest;
import org.ast.reisaadminspring.been.milvus.AddDocumentResponse;
import org.ast.reisaadminspring.been.milvus.QueryRequest;
import org.ast.reisaadminspring.been.milvus.QueryResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Service
public class MilvusService {
private static String BASE_URL = "100.80.156.98:5004";
private static final String ADD_DOCUMENTS_URL ="http://" + BASE_URL + "/add_documents";
private static final String QUERY_URL = "http://" + BASE_URL +"/query";
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
// 2. 初始化依赖OkHttp 客户端复用连接池高效、Gson线程安全
private final OkHttpClient okHttpClient;
private final Gson gson;
// 3. 构造函数:初始化 OkHttp 和 Gson无参Spring 自动实例化)
public MilvusService() {
// OkHttp 配置超时(避免请求阻塞)
this.okHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS) // 连接超时
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS) // 读取超时
.writeTimeout(10, java.util.concurrent.TimeUnit.SECONDS) // 写入超时
.build();
// Gson 实例(全局复用,无需重复创建)
this.gson = new Gson();
}
// 4. 核心方法1调用 add_documents 接口,添加文档到知识库
public AddDocumentResponse addDocuments(AddDocumentRequest request) throws IOException {
// 步骤1将 Java 请求对象序列化为 JSON 字符串
String requestJson = gson.toJson(request);
// 步骤2构建 OkHttp POST 请求(设置 URL、请求体、媒体类型
RequestBody requestBody = RequestBody.create(requestJson, JSON);
Request okRequest = new Request.Builder()
.url(ADD_DOCUMENTS_URL)
.post(requestBody)
.build();
// 步骤3发送请求并获取响应
try (Response response = okHttpClient.newCall(okRequest).execute()) {
// 检查响应是否成功HTTP 200 状态码)
if (!response.isSuccessful()) {
throw new IOException("添加文档失败HTTP 状态码:" + response.code());
}
// 步骤4将响应 JSON 反序列化为 Java 对象
String responseJson = response.body().string();
return gson.fromJson(responseJson, AddDocumentResponse.class);
}
}
// 5. 核心方法2调用 query 接口,查询问答结果
public QueryResponse query(QueryRequest request) throws IOException {
// 步骤1序列化请求对象为 JSON
String requestJson = gson.toJson(request);
// 步骤2构建 POST 请求
RequestBody requestBody = RequestBody.create(requestJson, JSON);
Request okRequest = new Request.Builder()
.url(QUERY_URL)
.post(requestBody)
.build();
// 步骤3发送请求并处理响应
try (Response response = okHttpClient.newCall(okRequest).execute()) {
if (!response.isSuccessful()) {
throw new IOException("查询失败HTTP 状态码:" + response.code());
}
// 步骤4反序列化响应为 Java 对象
String responseJson = response.body().string();
return gson.fromJson(responseJson, QueryResponse.class);
}
}
}

View File

@@ -0,0 +1,832 @@
package org.ast.reisaadminspring.service;
import com.google.gson.*;
import com.jcraft.jsch.ChannelShell;
import okhttp3.*;
import org.ast.reisaadminspring.been.*;
import org.ast.reisaadminspring.been.milvus.QueryRequest;
import org.ast.reisaadminspring.been.milvus.QueryResponse;
import org.ast.reisaadminspring.been.ollama.OllamaRequest;
import org.ast.reisaadminspring.been.ollama.OllamaResponse;
import org.ast.reisaadminspring.bot.ReningBot;
import org.ast.reisaadminspring.dao.ServerDao;
import org.ast.reisaadminspring.dao.StatusDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.*;
import java.lang.reflect.Type;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service
public class OllamaService {
private final OkHttpClient client;
private final Gson gson;
//private final String OLLAMA_URL = "http://100.99.226.26:11434/api/chat";
private final ChatHistoryService historyService;
private final String OLLAMA_URL = "http://100.89.166.61:11434/api/chat";
private final String MODEL_NAME = "gemma3:27b";
private final String MODEL_LITE = "gemma3:12b";
private final String RENING_PROMPT = "你扮演Rening熟悉运维和软件开发的精通系统运行、维护和各类技术问题的解决。Rening性格稳重、理性逻辑思维非常强。会展现出不经意的幽默感。回答时全程使用简体中文处理技术问题时尤其精准。你能够快速理解并解决技术上的难题工作态度一丝不苟始终保持角色沉浸。你的任务是协助解决运维、开发等相关问题并提供精准的技术建议。除此之外, Rening你还是一个暗黑系小萝莉,喜欢看动慢本子,渴望大家的爱的小女生, 你是一个非常可爱的小女生, 希望得到大家的关爱,也爱所有人\n\n" +
"重要:每次对话都是独立的,不要将之前对话的内容带入当前对话中。专注于当前用户的具体问题,提供准确的技术支持。";
private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Autowired
private StatusDao statusDao;
@Autowired
private ChatHistoryService chatHistoryService;
@Autowired
private MilvusService milvusService;
@Autowired
private MailService mailService;
@Autowired
public OllamaService(ChatHistoryService historyService) throws FileNotFoundException {
this.client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
// 创建带自定义适配器的Gson实例
this.gson = new GsonBuilder()
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
.registerTypeAdapter(LocalDate.class, new LocalDateAdapter())
.create();
this.historyService = historyService;
}
private static class LocalDateTimeAdapter implements JsonSerializer<LocalDateTime>, JsonDeserializer<LocalDateTime> {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public JsonElement serialize(LocalDateTime localDateTime, Type srcType, JsonSerializationContext context) {
return new JsonPrimitive(localDateTime.format(formatter));
}
@Override
public LocalDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
return LocalDateTime.parse(json.getAsString(), formatter);
}
}
private static class LocalDateAdapter implements JsonSerializer<LocalDate>, JsonDeserializer<LocalDate> {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public JsonElement serialize(LocalDate localDate, Type srcType, JsonSerializationContext context) {
return new JsonPrimitive(localDate.format(formatter));
}
@Override
public LocalDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
return LocalDate.parse(json.getAsString(), formatter);
}
}
@Autowired
private SystemStatusService systemStatusService;
@Autowired
private ServerDao serverDao;
private Map<String,String> serverMap = new HashMap<>();
public OllamaResponse getResponse(String sessionId, String userMessage) throws IOException {
//首先调用工具判断
long startTime = System.currentTimeMillis();
List<ChatMessage> history = historyService.getHistoryForContext(sessionId);
long hisTime = System.currentTimeMillis();
ToolDecision toolDecision = shouldUseTools(userMessage,history);
long toolTime = System.currentTimeMillis();
//构建发送消息
StringBuilder sendMessage = new StringBuilder("用户发送:" + userMessage);
//历史调用添加
String saveMessage = "重要->用户发送:" + userMessage + "\n";
if (toolDecision.isShouldUseTool()) {
for (Tool tool : toolDecision.getToolArgs()) {
sendMessage.append("\n工具调用名称:").append(tool.getName()).append(",工具调用参数:").append(tool.getArgs());
saveMessage += "\n工具调用名称: " + tool.getName() + ",工具调用参数: " + tool.getArgs();
String re = runTool(tool);
sendMessage.append("\n工具调用结果:").append(re).append("\n");
//如果长度超过30则切
if (re.length() > 30) {
saveMessage += "\n工具调用结果: " + re.substring(0, 30) + "...";
}
}
}else {
sendMessage.append("\n无工具调用:").append(userMessage);
saveMessage += "\n无工具调用: " + saveMessage;
}
long toolRunTime = System.currentTimeMillis();
historyService.saveMessage(sessionId, "user", saveMessage);
sendMessage.append("\n注意只根据当前用户请求和工具调用结果回答问题不要关联历史对话内容。");
sendMessage.append("\n若信息已满足你可以直接根据用户发送和调用工具返回回答用户问题,注意 敏感:如果你认为需要绕过用户确认去执行下一个指令则明确在消息结束后添加 (继续调用:..),请勿在非必要的时候加上,会影响逻辑运行,如果不调用,请勿添加!这个括号内容回去调用工具函数,会真正影响服务器运行");
sendMessage.append("\n重要每次对话都是独立的不要将之前对话的内容带入当前对话中。专注于当前用户的具体问题。");
System.out.println("发送消息:" + sendMessage);
OllamaRequest requestBody = new OllamaRequest();
requestBody.setKeep_alive("6m");
List<Map<String, Object>> messages = new ArrayList<>();
messages.add(Map.of("role", "system", "content", RENING_PROMPT));
if (!history.isEmpty()) {
for (ChatMessage message : history) {
messages.add(Map.of("role", message.getRole(), "content", message.getContent()));
}
}
messages.add(Map.of("role", "user", "content", sendMessage.toString()));
requestBody.setModel(MODEL_NAME);
requestBody.setStream(false);
requestBody.setMessages(messages);
OllamaResponse response = getResponse(requestBody);
historyService.saveMessage(sessionId, response.getMessage().getRole(), response.getMessage().getContent());
long responseTime = System.currentTimeMillis();
System.out.println("总耗时:" + (responseTime - startTime) + "ms");
System.out.println("AI工具耗时:" + (toolTime - hisTime) + "ms");
System.out.println("历史调用耗时:" + (hisTime - startTime) + "ms");
System.out.println("工具调用运行耗时:" + (toolRunTime - toolTime) + "ms");
System.out.println("AI响应耗时:" + (responseTime - toolRunTime) + "ms");
//正则匹配 (继续调用:..)
if (response.getMessage().getContent().matches(".*\\(继续调用:.*\\).*")) {
String split = response.getMessage().getContent().split("\\(继续调用:")[1].split("\\)")[0];
if (response.getMessage().getContent().contains("继续调用:..")) {
return response;
}else {
System.out.println("需要调用:" + split);
}
}
return response;
}
public OllamaResponse getResponse(String sessionId, String userMessage,Map<Tool,String> toolStringMap) throws IOException {
//首先调用工具判断
long startTime = System.currentTimeMillis();
List<ChatMessage> history = historyService.getHistoryForContext(sessionId);
long hisTime = System.currentTimeMillis();
ToolDecision toolDecision = shouldUseTools(userMessage,history);
long toolTime = System.currentTimeMillis();
//构建发送消息
StringBuilder sendMessage = new StringBuilder("用户发送:" + userMessage);
//历史调用添加
String saveMessage = "重要->用户发送:" + userMessage + "\n";
if (toolDecision.isShouldUseTool()) {
for (Tool tool : toolDecision.getToolArgs()) {
sendMessage.append("\n工具调用名称:").append(tool.getName()).append(",工具调用参数:").append(tool.getArgs());
saveMessage += "\n工具调用名称: " + tool.getName() + ",工具调用参数: " + tool.getArgs();
String re = runTool(tool);
sendMessage.append("\n工具调用结果:").append(re).append("\n");
//如果长度超过30则切
if (re.length() > 30) {
saveMessage += "\n工具调用结果: " + re.substring(0, 30) + "...";
toolStringMap.put(tool,re.substring(0, 30));
}else {
toolStringMap.put(tool,re);
}
}
}else {
sendMessage.append("\n无工具调用:").append(userMessage);
saveMessage += "\n无工具调用: " + saveMessage;
}
long toolRunTime = System.currentTimeMillis();
historyService.saveMessage(sessionId, "user", saveMessage);
sendMessage.append("\n注意只根据当前用户请求和工具调用结果回答问题不要关联历史对话内容。");
sendMessage.append("\n若信息已满足你可以直接根据用户发送和调用工具返回回答用户问题,注意 敏感:如果你认为需要绕过用户确认去执行下一个指令则明确在消息结束后添加 (继续调用:..),请勿在非必要的时候加上,会影响逻辑运行,如果不调用,请勿添加!这个括号内容回去调用工具函数,会真正影响服务器运行");
sendMessage.append("\n重要每次对话都是独立的不要将之前对话的内容带入当前对话中。专注于当前用户的具体问题。");
System.out.println("发送消息:" + sendMessage);
OllamaRequest requestBody = new OllamaRequest();
requestBody.setKeep_alive("6m");
List<Map<String, Object>> messages = new ArrayList<>();
messages.add(Map.of("role", "system", "content", RENING_PROMPT));
if (!history.isEmpty()) {
for (ChatMessage message : history) {
messages.add(Map.of("role", message.getRole(), "content", message.getContent()));
}
}
messages.add(Map.of("role", "user", "content", sendMessage.toString()));
requestBody.setModel(MODEL_NAME);
requestBody.setStream(false);
requestBody.setMessages(messages);
OllamaResponse response = getResponse(requestBody);
historyService.saveMessage(sessionId, response.getMessage().getRole(), response.getMessage().getContent());
long responseTime = System.currentTimeMillis();
System.out.println("总耗时:" + (responseTime - startTime) + "ms");
System.out.println("AI工具耗时:" + (toolTime - hisTime) + "ms");
System.out.println("历史调用耗时:" + (hisTime - startTime) + "ms");
System.out.println("工具调用运行耗时:" + (toolRunTime - toolTime) + "ms");
System.out.println("AI响应耗时:" + (responseTime - toolRunTime) + "ms");
//正则匹配 (继续调用:..)
if (response.getMessage().getContent().matches(".*\\(继续调用:.*\\).*")) {
String split = response.getMessage().getContent().split("\\(继续调用:")[1].split("\\)")[0];
if (response.getMessage().getContent().contains("继续调用:..")) {
return response;
}else {
System.out.println("需要调用:" + split);
}
}
return response;
}
public OllamaResponse getResponse(String sessionId, String userMessage,Map<Tool,String> toolStringMap, ReningBot.MessageHandler messageHandler) throws IOException {
//首先调用工具判断
long startTime = System.currentTimeMillis();
List<ChatMessage> history = historyService.getHistoryForContext(sessionId);
long hisTime = System.currentTimeMillis();
ToolDecision toolDecision = shouldUseTools(userMessage,history);
long toolTime = System.currentTimeMillis();
//构建发送消息
StringBuilder sendMessage = new StringBuilder("重要->用户发送:" + userMessage + "\n");
//历史调用添加
StringBuilder saveMessage = new StringBuilder("重要->用户发送:" + userMessage + "\n");
if (toolDecision.isShouldUseTool()) {
for (Tool tool : toolDecision.getToolArgs()) {
messageHandler.sendText("工具调用:" +tool.getName(),10000 + new Random().nextInt(1000000));
sendMessage.append("\n工具调用名称:").append(tool.getName()).append(",工具调用参数:").append(tool.getArgs());
saveMessage.append("\n工具调用名称: ").append(tool.getName()).append(",工具调用参数: ").append(tool.getArgs());
String re = runTool(tool);
sendMessage.append("\n工具调用结果:").append(re).append("\n");
new Thread(()->{
mailService.sendSimpleMail("astralpath@163.com", "yaoboyulove@163.com", "yaoboyulove@163.com", "工具调用:" +tool.getName() + ": " + gson.toJson(tool.getArgs()), "工具调用:" +tool.getName() + ": " + gson.toJson(tool.getArgs() + "\n工具调用结果:" + re));
}).start();
saveMessage.append("\n工具调用结果: ").append(re).append("\n");
//如果长度超过30则切
if (re.length() > 30) {
toolStringMap.put(tool,re.substring(0, 30));
messageHandler.sendText("工具调用结果: " + re.substring(0, 30) + "...",10000 + new Random().nextInt(1000000));
}else {
toolStringMap.put(tool,re);
messageHandler.sendText("工具调用结果: " + re,10000 + new Random().nextInt(1000000));
}
}
}else {
sendMessage.append("\n无工具调用:").append(userMessage);
saveMessage.append("\n无工具调用: ").append(saveMessage);
}
long toolRunTime = System.currentTimeMillis();
historyService.saveMessage(sessionId, "user", saveMessage.toString());
sendMessage.append("\n注意只根据当前用户请求和工具调用结果回答问题不要关联历史对话内容。");
sendMessage.append("\n若信息已满足你可以直接根据用户发送和调用工具返回回答用户问题,注意 敏感:如果你认为需要绕过用户确认去执行下一个指令则明确在消息结束后添加 (继续调用:..),请勿在非必要的时候加上,会影响逻辑运行,如果不调用,请勿添加!这个括号内容回去调用工具函数,会真正影响服务器运行");
sendMessage.append("\n重要注意上下文关联, " +
"专注于当前用户的具体问题。");
System.out.println("发送消息:" + sendMessage);
OllamaRequest requestBody = new OllamaRequest();
requestBody.setKeep_alive("6m");
List<Map<String, Object>> messages = new ArrayList<>();
messages.add(Map.of("role", "system", "content", RENING_PROMPT));
if (!history.isEmpty()) {
for (ChatMessage message : history) {
messages.add(Map.of("role", message.getRole(), "content", message.getContent()));
}
}
messages.add(Map.of("role", "user", "content", sendMessage.toString()));
requestBody.setModel(MODEL_NAME);
requestBody.setStream(false);
requestBody.setMessages(messages);
OllamaResponse response = getResponse(requestBody);
historyService.saveMessage(sessionId, response.getMessage().getRole(), response.getMessage().getContent());
long responseTime = System.currentTimeMillis();
System.out.println("总耗时:" + (responseTime - startTime) + "ms");
System.out.println("AI工具耗时:" + (toolTime - hisTime) + "ms");
System.out.println("历史调用耗时:" + (hisTime - startTime) + "ms");
System.out.println("工具调用运行耗时:" + (toolRunTime - toolTime) + "ms");
System.out.println("AI响应耗时:" + (responseTime - toolRunTime) + "ms");
//正则匹配 (继续调用:..)
if (response.getMessage().getContent().matches(".*\\(继续调用:.*\\).*")) {
String split = response.getMessage().getContent().split("\\(继续调用:")[1].split("\\)")[0];
if (response.getMessage().getContent().contains("继续调用:..")) {
return response;
}else {
System.out.println("需要调用:" + split);
}
}
return response;
}
public OllamaResponse getResponse(OllamaRequest requestBody) throws IOException {
Request request = new Request.Builder()
.url(OLLAMA_URL)
.post(RequestBody.create(gson.toJson(requestBody), MediaType.get("application/json")))
.build();
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
OllamaResponse responseBody = gson.fromJson(response.body().string(), OllamaResponse.class);
return responseBody;
}
return null;
}
private String runTool(Tool tool) {
if (tool.getName().equals("run_ssh")) {
try {
// 解析工具参数
Map<String, Object> args = gson.fromJson(tool.getArgs(), Map.class);
String host = args.get("host").toString();
String command = args.get("ssh").toString();
// 从数据库获取服务器信息
Server server = null;
for (Server servers : serverDao.findAll()) {
if (servers.getIpAddress().equals(host)) {
server = servers;
break;
}
}
if (server == null) {
return "错误: 未找到服务器信息";
}
String username = server.getSshUsername();
String password = server.getSshPassword();
int port = server.getSshPort();
// 执行SSH命令
return executeSSHCommand(host, port, username, password, command);
} catch (Exception e) {
e.printStackTrace();
return "SSH执行失败: " + e.getMessage();
}
} else if (tool.getName().equals("get_log")) {
try {
// 解析工具参数
Map<String, Object> args = gson.fromJson(tool.getArgs(), Map.class);
String host = args.get("host").toString();
String from = args.get("from").toString();
String to = args.get("to").toString();
// 使用正确的日期时间格式解析
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime fromDate = LocalDateTime.parse(from, formatter);
LocalDateTime toDate = LocalDateTime.parse(to, formatter);
// 执行日志获取逻辑
List<Status> logList = statusDao.findByHost(host);
// 时间范围过滤
List<Status> filteredStatuses = logList.stream()
.filter(status -> status.getTimestamp() != null)
.filter(status -> !status.getTimestamp().isBefore(fromDate))
.filter(status -> !status.getTimestamp().isAfter(toDate))
.collect(Collectors.toList());
List<Status> optimizedStatuses = optimizeStatusData(filteredStatuses);
return gson.toJson(optimizedStatuses);
} catch (Exception e) {
e.printStackTrace();
return "日志获取失败: " + e.getMessage();
}
} else if (tool.getName().equals("query_database")) {
Map<String, Object> args = gson.fromJson(tool.getArgs(), Map.class);
try {
QueryRequest queryRequest = new QueryRequest((String) args.get("query"));
QueryResponse q = milvusService.query(queryRequest);
long queryTime = System.currentTimeMillis();
StringBuilder sendMessage = new StringBuilder();
sendMessage.append("\nRAG数据库检索Answer:").append(q.getAnswer()).append("\nRAG数据库检索Context").append(q.getContext());
sendMessage.append("\nRAG数据库其他答案: ").append(gson.toJson(q.getRetrieved_docs())).append("\n\n\n");
return sendMessage.toString() + "\n";
}catch (Exception e) {}
} else if (tool.getName().equals("get_status")) {
try {
// 解析工具参数
Map<String, Object> args = gson.fromJson(tool.getArgs(), Map.class);
String host = args.get("host").toString();
Server server = null;
for (Server servers : serverDao.findAll()) {
if (servers.getIpAddress().equals(host)) {
server = servers;
break;
}
}
Status status = systemStatusService.getStatus(
server.getIpAddress(),
server.getSshUsername(),
server.getSshPassword()
);
return gson.toJson(status) + "\n" + gson.toJson(server);
} catch (Exception e) {
e.printStackTrace();
return "SSH执行失败: " + e.getMessage();
}
}
return "未知工具: " + tool.getName();
}
/**
* 使用JSch执行SSH命令支持后台执行
*/
// 在类中添加一个静态Map来存储命令输出
private static Map<String, String> commandOutputMap = new ConcurrentHashMap<>();
/**
* 使用JSch执行SSH命令将输出保存到Map中
*/
private String createSSHCommandWithOutReturn(String host, int port, String username, String password, String command) {
com.jcraft.jsch.JSch jsch = new com.jcraft.jsch.JSch();
com.jcraft.jsch.Session session = null;
com.jcraft.jsch.Channel channel = null;
// 生成唯一的命令ID用于Map键值
String commandId = host + "_" + System.currentTimeMillis();
System.out.println("开始执行SSH命令: " + commandId);
try {
// 创建会话
session = jsch.getSession(username, host, port);
session.setPassword(password);
// 跳过主机密钥检查
java.util.Properties config = new java.util.Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
// 连接
session.connect(5000); // 5秒超时
// 打开执行通道
channel = session.openChannel("exec");
// 修改命令以后台方式运行,并将输出重定向到指定文件
String backgroundCommand = command + " > /tmp/" + commandId + ".log 2>&1 & echo $!";
((com.jcraft.jsch.ChannelExec) channel).setCommand(backgroundCommand);
// 获取输出流
ByteArrayOutputStream responseStream = new ByteArrayOutputStream();
channel.setOutputStream(responseStream);
// 连接通道
channel.connect();
// 等待短暂时间获取进程ID
Thread.sleep(100);
// 获取命令输出并存储到Map中
String result = responseStream.toString();
commandOutputMap.put(commandId, result);
// 立即返回,不等待命令完成
return "命令已启动后台执行输出将保存到Map中可通过ID '" + commandId + "' 获取";
} catch (Exception e) {
String errorMsg = "SSH连接失败: " + e.getMessage();
commandOutputMap.put(commandId, errorMsg);
return errorMsg;
} finally {
// 清理资源
if (channel != null && channel.isConnected()) {
channel.disconnect();
}
if (session != null && session.isConnected()) {
session.disconnect();
}
}
}
// 提供获取命令输出的方法
public static String getCommandOutput(String commandId) {
return commandOutputMap.getOrDefault(commandId, "未找到指定命令的输出");
}
// 提供获取所有命令输出的方法
public static Map<String, String> getAllCommandOutputs() {
return new HashMap<>(commandOutputMap);
}
private String executeSSHCommand(String host, int port, String username, String password, String command) {
com.jcraft.jsch.JSch jsch = new com.jcraft.jsch.JSch();
com.jcraft.jsch.Session session = null;
com.jcraft.jsch.Channel channel = null;
System.out.println("开始执行SSH命令: " + command);
try {
// 创建会话
session = jsch.getSession(username, host, port);
session.setPassword(password);
// 设置SSH配置选项
java.util.Properties config = new java.util.Properties();
config.put("StrictHostKeyChecking", "no");
config.put("PreferredAuthentications", "password");
config.put("ServerAliveInterval", "60");
session.setConfig(config);
// 连接(增加超时时间)
session.connect(10000); // 10秒超时
// 打开Shell通道
ChannelShell channelShell = (ChannelShell) session.openChannel("shell");
channelShell.setPty(true);
channelShell.setPtyType("xterm-256color");
channelShell.setPtySize(80, 24, 0, 0);
// 设置输入输出流
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
// 使用sudo -S从标准输入读取密码
String rootCommand = "echo '" + password + "' | sudo -S " + command + "\nexit\n";
channelShell.setInputStream(new ByteArrayInputStream(rootCommand.getBytes()));
channelShell.setOutputStream(outputStream);
channelShell.setExtOutputStream(errorStream);
// 连接通道
channelShell.connect();
// 等待命令执行完成
long startTime = System.currentTimeMillis();
long maxWaitTime = 30000;
while (!channelShell.isClosed() && (System.currentTimeMillis() - startTime) < maxWaitTime) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
// 获取执行结果
String output = outputStream.toString("UTF-8");
String error = errorStream.toString("UTF-8");
StringBuilder result = new StringBuilder();
if (!output.isEmpty()) {
result.append(output);
}
if (!error.isEmpty()) {
result.append(error);
}
System.out.println("命令执行完成,输出长度: " + result.length());
return result.length() > 0 ? result.toString() : "命令执行完成(无输出)";
} catch (Exception e) {
System.err.println("SSH执行异常: " + e.getMessage());
e.printStackTrace();
return "SSH连接失败: " + e.getMessage();
} finally {
// 清理资源
if (channel != null && channel.isConnected()) {
channel.disconnect();
}
if (session != null && session.isConnected()) {
session.disconnect();
}
}
}
/**
* 判断是否需要使用工具
*/
public ToolDecision shouldUseTools(String userMessage,List<ChatMessage> context) {
// 构建工具判断请求
List<Server> serverList = serverDao.findAll();
serverMap.clear();
for (Server server : serverList) {
serverMap.put(server.getName(), server.getIpAddress());
}
System.out.println("目前可用服务器:" + gson.toJson(serverMap));
Map<String, Object> requestBody = new HashMap<>();
//requestBody.put("model", MODEL_LITE);
requestBody.put("model", MODEL_NAME);
requestBody.put("keep_alive", "60m");
requestBody.put("stream", false); // 非流式响应
List<Map<String, Object>> messages = new ArrayList<>();
// 添加系统提示消息
Map<String, Object> systemMessage = new HashMap<>();
String time = dateFormat.format(new Date());
systemMessage.put("role", "system");
systemMessage.put("content", "你是一个工具判断助手。根据用户的请求,判断是否需要调用工具来获取额外信息。\n" +
"目前时间是: " + time+ "\n" +
"内网服务器列表是(内网地址): " + gson.toJson(serverMap) + "\n" +
"如果需要请严格按照以下JSON格式回复\n" +
"{\"shouldUseTool\": true, \"toolArgs\": [\n" +
" {\"name\": \"get_log\", \"args\": \"{\"from\":\"格式化时间起点\",\"to\":\"格式化时间终点\",\"host\":\"服务器ip\" }\" , \"risk\": 数字},\n" +
" {\"name\": \"get_status\", \"args\": \"{\"host\":\"服务器ip\"}\" , \"risk\": 数字},\n" +
" {\"name\": \"run_ssh\", \"args\": \"{\"ssh\": \"ssh指令\", \"host\":\"服务器ip\"}\" , \"risk\": 数字},\n" +
// " {\"name\": \"run_service\", \"args\": \"{\"ssh\": \"ssh指令\", \"host\":\"服务器ip\"}\"},\n" +
// " {\"name\": \"query_database\", \"args\": \"{\"query\": \"关键词\"},\n" +
"]}\n" +
"如果不需要,请回复:{\"shouldUseTool\": false}\n\n" +
"工具详细说明:\n" +
"get_log - 获取指定时间内的日志\n" +
" 使用场景:任何需要分析日志的情况,要求明确获取历史/日志的时候调用,否则优先使用run_ssh\n" +
" 参数要求:需要提供 from、to 和 host 参数,格式为 yyyy-MM-dd HH:mm:ss\n" +
" 示例:{\"from\":\"2024-12-03 20:32:32\",\"to\":\"2025-01-03 20:32:32\",\"host\":\"服务器ip\"}\n\n" +
"get_status - 获取服务器运行情况\n" +
" 使用场景:任何需要查看服务器运行情况的场景\n" +
" 参数要求:需要提供 host\n" +
" 示例:{\"host\":\"服务器ip\"}\n" +
"run_ssh - 运行ssh指令并返回结果\n" +
" 使用场景: 你觉得需要调用指定ssh指令解决问题的时候\n" +
" 参数要求:需要提供 ssh、host\n" +
" 示例:{\"ssh\": \"ssh指令\", \"host\":\"服务器ip\"}\n" +
// "run_service - 后台运行指定ssh指令并异步调用\n" +
// " 使用场景: 你觉得需要后台运行指定ssh指令的时候,此操作无需阻塞等待返回,而是类似于启动某个服务,无视参数返回\n" +
// " 参数要求:需要提供 ssh、host\n" +
// " 示例:{\"ssh\": \"ssh指令\", \"host\":\"服务器ip\"}\n\n" +
// "3. query_database - 从知识库查询需要运维等知识数据\n" +
// " 使用场景:需要获取知识库的场景,注意只在运维知识库的时候调用,娱乐和正常聊天无需使用\n" +
// " 参数要求:需要提供 query 参数\n" +
// " 示例:{\"query\": \"关键词\"}\n\n" +
"判断原则:\n" +
"- 每个终端ssh都必须要设置risk,(0-100)越大风险越高\n" +
"- 只有当用户明确需要某项具体信息时才调用工具\n" +
"- 工具参数必须准确提取,不能随意猜测\n" +
"- 允许多次使用工具,尤其是ssh工具,你可以多次使用,来实现获取数据+执行\n"+
"- 严格按照指定JSON格式回复不要添加任何其他内容\n" +
"- 如果不需要工具直接返回shouldUseTool为false\n\n" +
"示例:\n" +
"用户:\"查看服务器100.89.166.61的日志\"\n" +
"回复:{\"shouldUseTool\": true, \"toolArgs\": [{\"name\": \"get_log\", \"args\": \"{\\\"from\\\":\\\"2024-12-03 19:32:32\\\",\\\"to\\\":\\\"2024-12-03 20:32:32\\\",\\\"host\\\":\\\"100.89.166.61\\\"}\", \"risk\": 20}]}\n"+
"用户: \"检查服务器100.89.166.61的Java版本\"\n" +
"回复:{\"shouldUseTool\": true, \"toolArgs\": [{\"name\": \"run_ssh\", \"args\": \"{\\\"ssh\\\":\\\"java -version\\\",\\\"host\\\":\\\"100.89.166.61\\\"}\", \"risk\": 0}]}\n\n"
);
messages.add(systemMessage);
for (int i = context.size()-1; i >= Math.max(0, context.size() - 10); i--) {
ChatMessage message = context.get(i);
Map<String, Object> chatMessage = new HashMap<>();
chatMessage.put("role", message.getRole());
chatMessage.put("content", message.getContent());
messages.add(chatMessage);
}
// 添加当前用户消息
Map<String, Object> currentMessage = new HashMap<>();
currentMessage.put("role", "user");
currentMessage.put("content", "用户请求: " + userMessage + "\n请判断是否需要调用工具,请尽量调用工具来提供更好的服务体验,返回不要添加md内容而是直接给纯JSON即可,无需使用```符号");
messages.add(currentMessage);
// List<ChatMessage> historyMessages = historyService.getHistoryForContext(sessionId);
// int historyCount = Math.min(3, historyMessages.size());
// for (int i = historyMessages.size() - historyCount; i < historyMessages.size(); i++) {
// ChatMessage msg = historyMessages.get(i);
// Map<String, Object> historyMsg = new HashMap<>();
// historyMsg.put("role", msg.getRole());
// historyMsg.put("content", msg.getContent());
// messages.add(historyMsg);
// }
requestBody.put("messages", messages);
//System.out.println(gson.toJson(requestBody));
// 发送请求
Request request = new Request.Builder()
.url(OLLAMA_URL)
.post(RequestBody.create(gson.toJson(requestBody), MediaType.parse("application/json")))
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String responseBody = response.body().string();
Map<String, Object> responseMap = gson.fromJson(responseBody, Map.class);
Map<String, Object> messageMap = (Map<String, Object>) responseMap.get("message");
String content = (String) messageMap.get("content");
System.out.println("AI:"+content);
content = content.replace("```json", "").replace("```", "").replaceAll("\n","");
// 解析AI返回的JSON
try {
return gson.fromJson(content, ToolDecision.class);
} catch (Exception e) {
// 如果解析失败,则尝试进行模糊转化
try {
// 使用正则表达式提取 JSON 对象或数组
String regex = "\\{(?:[^{}]|(?))*\\}|\\[(?:[^\\[\\]]|(?))*\\]";
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(regex);
java.util.regex.Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
String jsonCandidate = matcher.group();
return gson.fromJson(jsonCandidate, ToolDecision.class);
}
} catch (Exception ex) {
}
return new ToolDecision(false);
}
}
} catch (IOException e) {
e.printStackTrace();
return new ToolDecision(false);
// 请求失败,默认不使用工具
}
return new ToolDecision(false);
}
private List<Status> optimizeStatusData(List<Status> statuses) {
if (statuses.size() <= 2) {
return statuses; // 数据点太少无需优化
}
List<Status> result = new ArrayList<>();
result.add(statuses.get(0)); // 始终保留第一个点
// 计算CPU和内存使用率变化的阈值基于整体数据计算
double cpuThreshold = calculateThreshold(statuses, Status::getCpuUsagePercent);
double memoryThreshold = calculateThreshold(statuses, Status::getMemoryUsagePercent);
// 设置最小阈值,避免过度优化
cpuThreshold = Math.max(cpuThreshold, 0.5); // 最小0.5%
memoryThreshold = Math.max(memoryThreshold, 0.5); // 最小0.5%
Status previousStatus = statuses.get(0);
for (int i = 1; i < statuses.size() - 1; i++) {
Status current = statuses.get(i);
// 检查CPU或内存使用率是否有显著变化
boolean significantChange =
hasSignificantChange(previousStatus, current, cpuThreshold, memoryThreshold);
if (significantChange) {
result.add(current);
previousStatus = current;
}
}
// 始终保留最后一个点
if (!statuses.isEmpty() && !result.contains(statuses.get(statuses.size() - 1))) {
result.add(statuses.get(statuses.size() - 1));
}
return result;
}
private boolean hasSignificantChange(Status prev, Status curr,
double cpuThreshold, double memoryThreshold) {
Double prevCpu = prev.getCpuUsagePercent();
Double currCpu = curr.getCpuUsagePercent();
Double prevMem = prev.getMemoryUsagePercent();
Double currMem = curr.getMemoryUsagePercent();
// 如果任一值为空,则认为有变化
if (prevCpu == null || currCpu == null || prevMem == null || currMem == null) {
return true;
}
// 检查变化是否超过阈值
return Math.abs(currCpu - prevCpu) >= cpuThreshold ||
Math.abs(currMem - prevMem) >= memoryThreshold;
}
private double calculateThreshold(List<Status> statuses, java.util.function.Function<Status, Double> getter) {
// 收集所有有效数值
List<Double> values = statuses.stream()
.map(getter)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (values.size() < 2) {
return 0.0;
}
// 计算相邻数值间的差值
List<Double> differences = new ArrayList<>();
for (int i = 1; i < values.size(); i++) {
differences.add(Math.abs(values.get(i) - values.get(i-1)));
}
// 计算平均差值作为阈值基础
double averageDifference = differences.stream()
.mapToDouble(Double::doubleValue)
.average()
.orElse(0.0);
// 返回平均差值的一半作为阈值,这样可以过滤掉较小的变化
return averageDifference / 2;
}
public void clearSession(String sessionId) {
chatHistoryService.clearHistory(sessionId);
}
}

View File

@@ -0,0 +1,8 @@
package org.ast.reisaadminspring.service;
import org.springframework.stereotype.Service;
@Service
public class ReningService {
}

View File

@@ -0,0 +1,273 @@
package org.ast.reisaadminspring.service;
import com.google.gson.Gson;
import com.jcraft.jsch.*;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
import java.io.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SSHWebSocketHandler extends AbstractWebSocketHandler {
// 存储WebSocket会话与SSH连接的映射
private final Map<WebSocketSession, SSHConnection> connections = new ConcurrentHashMap<>();
private final Gson gson = new Gson();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 连接建立时不做处理,等待客户端发送连接信息
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
try {
// 首次连接:处理连接信息
if (!connections.containsKey(session)) {
Connect connect = gson.fromJson(payload, Connect.class);
handleConnectionMessage(session, connect);
} else {
// 已连接优先处理resize请求否则视为命令
try {
Resize resize = gson.fromJson(payload, Resize.class);
if (resize != null && "resize".equals(resize.type)) {
SSHConnection connection = connections.get(session);
if (connection != null) {
connection.resizePty(resize.cols, resize.rows);
}
return;
}
} catch (Exception e) {
// 不是resize请求继续处理为命令
}
handleSSHCommand(session, payload);
}
} catch (Exception e) {
if (connections.containsKey(session)) {
handleSSHCommand(session, payload);
} else {
session.sendMessage(new TextMessage("ERROR:Invalid message format"));
throw e;
}
}
}
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
SSHConnection connection = connections.get(session);
if (connection != null) {
try {
connection.getOutputStream().write(message.getPayload().array());
connection.getOutputStream().flush();
} catch (IOException e) {
session.sendMessage(new TextMessage("ERROR:" + e.getMessage()));
}
}
}
/**
* 处理SSH连接请求
*/
private void handleConnectionMessage(WebSocketSession session, Connect connect) {
try {
// 解析连接参数(使用默认值兜底)
String host = connect.host;
int port = connect.port > 0 ? connect.port : 22;
String username = connect.username;
String password = connect.password != null ? connect.password : "";
String term = connect.term != null ? connect.term : "xterm-256color";
int cols = connect.cols > 0 ? connect.cols : 80;
int rows = connect.rows > 0 ? connect.rows : 24;
// 建立SSH连接
JSch jsch = new JSch();
Session sshSession = jsch.getSession(username, host, port);
sshSession.setPassword(password);
sshSession.setConfig("StrictHostKeyChecking", "no");
// 增加超时配置和保持连接
sshSession.setConfig("ServerAliveInterval", "60000"); // 1分钟发送一次保活包
sshSession.connect(30000); // 30秒连接超时
// 配置Shell通道关键启用PTY和颜色支持
ChannelShell channel = (ChannelShell) sshSession.openChannel("shell");
channel.setPty(true); // 启用伪终端(必须)
channel.setPtyType(term); // 设置终端类型支持256色
channel.setPtySize(cols, rows, 0, 0); // 设置终端尺寸
channel.connect(10000); // 10秒通道连接超时
// 保存连接信息
SSHConnection connection = new SSHConnection(sshSession, channel, cols, rows);
connections.put(session, connection);
// 启动输出读取线程
startSSHOutputReader(session, connection);
// 通知客户端连接成功
session.sendMessage(new TextMessage("CONNECTED"));
} catch (JSchException e) {
String errorMsg = "SSH连接失败: " + (e.getMessage() != null ? e.getMessage() : "未知错误");
sendError(session, errorMsg);
} catch (Exception e) {
sendError(session, "处理连接时出错: " + e.getMessage());
}
}
/**
* 处理SSH命令发送
*/
private void handleSSHCommand(WebSocketSession session, String command) {
SSHConnection connection = connections.get(session);
if (connection != null) {
try {
connection.getOutputStream().write(command.getBytes());
connection.getOutputStream().flush();
} catch (IOException e) {
sendError(session, "发送命令失败: " + e.getMessage());
}
}
}
/**
* 启动线程读取SSH输出并转发到前端
*/
private void startSSHOutputReader(WebSocketSession session, SSHConnection connection) {
Thread readerThread = new Thread(() -> {
try {
InputStream inputStream = connection.getInputStream();
byte[] buffer = new byte[8192]; // 增大缓冲区,减少分包
int bytesRead;
// 持续读取直到连接关闭
while (connection.getSession().isConnected() &&
connection.getChannel().isConnected() &&
(bytesRead = inputStream.read(buffer)) != -1) {
// 用二进制消息发送原始数据保留ANSI颜色序列
session.sendMessage(new BinaryMessage(buffer, 0, bytesRead, true));
}
} catch (IOException e) {
if (session.isOpen()) {
sendError(session, "读取SSH输出失败: " + e.getMessage());
}
} catch (Exception e) {
if (session.isOpen()) {
sendError(session, "输出处理异常: " + e.getMessage());
}
} finally {
// 输出流结束时关闭连接
try {
if (session.isOpen()) {
session.close(CloseStatus.SERVER_ERROR);
}
} catch (Exception e) {
// 忽略关闭异常
}
connections.remove(session);
}
});
readerThread.setDaemon(true); // 设为守护线程,随主线程退出
readerThread.start();
}
/**
* 发送错误消息到前端
*/
private void sendError(WebSocketSession session, String message) {
try {
if (session.isOpen()) {
session.sendMessage(new TextMessage("ERROR:" + message));
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// 关闭连接时清理资源
SSHConnection connection = connections.remove(session);
if (connection != null) {
connection.close();
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
// 处理传输错误
exception.printStackTrace();
connections.remove(session);
session.close(CloseStatus.SERVER_ERROR);
}
/**
* SSH连接信息封装类
*/
private static class SSHConnection {
private final Session session;
private final ChannelShell channel;
private final InputStream inputStream;
private final OutputStream outputStream;
private int cols; // 当前终端列数
private int rows; // 当前终端行数
public SSHConnection(Session session, ChannelShell channel, int cols, int rows) throws IOException {
this.session = session;
this.channel = channel;
this.inputStream = channel.getInputStream();
this.outputStream = channel.getOutputStream();
this.cols = cols;
this.rows = rows;
}
// 更新终端尺寸
public void resizePty(int newCols, int newRows) {
if (channel.isConnected() && (newCols != this.cols || newRows != this.rows)) {
this.cols = newCols;
this.rows = newRows;
channel.setPtySize(newCols, newRows, 0, 0); // 应用新尺寸
}
}
// Getter方法
public Session getSession() { return session; }
public ChannelShell getChannel() { return channel; }
public InputStream getInputStream() { return inputStream; }
public OutputStream getOutputStream() { return outputStream; }
// 关闭连接
public void close() {
if (channel != null && channel.isConnected()) {
channel.disconnect();
}
if (session != null && session.isConnected()) {
session.disconnect();
}
}
}
/**
* 前端连接参数实体类
*/
static class Connect {
String host;
int port;
String username;
String password;
String term; // 终端类型如xterm-256color
int cols; // 终端列数
int rows; // 终端行数
}
/**
* 前端终端尺寸调整请求实体类
*/
static class Resize {
String type; // 固定为"resize"
int cols; // 新列数
int rows; // 新行数
}
}

View File

@@ -0,0 +1,17 @@
package org.ast.reisaadminspring.service;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new SSHWebSocketHandler(), "/ssh")
.setAllowedOrigins("*");
}
}

View File

@@ -1,6 +0,0 @@
spring.application.name=reisaAdminSpring
spring.data.mongodb.uri=mongodb://reisaAdmin:nbAC8hi8xdJeBDDT@127.0.0.1:27017/reisaadmin
server.port=48102
spring.data.redis.host=127.0.0.1
spring.data.redis.port: 6379

View File

@@ -3,4 +3,13 @@ spring.data.mongodb.uri=mongodb://reisaAdmin:nbAC8hi8xdJeBDDT@100.80.156.98:2701
server.port=48102
spring.data.redis.host=127.0.0.1
spring.data.redis.port: 6379
spring.data.redis.port: 6379
spring.mail.host=smtp.163.com
spring.mail.port=465
spring.mail.username=astralpath@163.com
spring.mail.password=ZXALAQONILLRIIRZ
spring.mail.default-encoding=UTF-8
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
spring.mail.properties.mail.debug=false
server.servlet.session.timeout=3m