From 80d1dd0db5070281bd8bfbdfd8b6f2881b4b64d0 Mon Sep 17 00:00:00 2001 From: ddaodan <731882332@qq.com> Date: Sat, 14 Feb 2026 00:48:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=A3=80=E6=9F=A5=E5=92=8C=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ddaodan/MineChatGPT/CommandHandler.java | 8 +- .../ddaodan/MineChatGPT/ConfigManager.java | 25 ++ .../java/com/ddaodan/MineChatGPT/Main.java | 9 +- .../MineChatGPT/MineChatGPTTabCompleter.java | 1 + .../MineChatGPT/service/CommandService.java | 14 +- .../MineChatGPT/service/UpdateChecker.java | 264 ++++++++++++++++++ src/main/resources/config.yml | 12 + src/main/resources/lang/en.yml | 6 + src/main/resources/lang/zh.yml | 6 + src/main/resources/plugin.yml | 3 + 10 files changed, 343 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/ddaodan/MineChatGPT/service/UpdateChecker.java diff --git a/src/main/java/com/ddaodan/MineChatGPT/CommandHandler.java b/src/main/java/com/ddaodan/MineChatGPT/CommandHandler.java index 079535c..389c69b 100644 --- a/src/main/java/com/ddaodan/MineChatGPT/CommandHandler.java +++ b/src/main/java/com/ddaodan/MineChatGPT/CommandHandler.java @@ -1,8 +1,8 @@ package com.ddaodan.MineChatGPT; -import com.ddaodan.MineChatGPT.service.ApiService; import com.ddaodan.MineChatGPT.service.CommandService; import com.ddaodan.MineChatGPT.service.RequestCoordinator; +import com.ddaodan.MineChatGPT.service.UpdateChecker; import com.ddaodan.MineChatGPT.service.UserSessionManager; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -12,10 +12,10 @@ public class CommandHandler implements CommandExecutor { private final CommandService commandService; private final UserSessionManager sessionManager; - public CommandHandler(ConfigManager configManager, UserSessionManager sessionManager, RequestCoordinator requestCoordinator) { + public CommandHandler(ConfigManager configManager, UserSessionManager sessionManager, RequestCoordinator requestCoordinator, UpdateChecker updateChecker) { this.configManager = configManager; this.sessionManager = sessionManager; - this.commandService = new CommandService(configManager, requestCoordinator, sessionManager); + this.commandService = new CommandService(configManager, requestCoordinator, sessionManager, updateChecker); } @Override @@ -43,6 +43,8 @@ public class CommandHandler implements CommandExecutor { return commandService.handleCharacterCommand(sender, args, userId); } else if (subCommand.equalsIgnoreCase("stats")) { return commandService.handleStatsCommand(sender, args, userId); + } else if (subCommand.equalsIgnoreCase("checkupdate")) { + return commandService.handleCheckUpdateCommand(sender); } else { return commandService.handleAskCommand(sender, args, userId); } diff --git a/src/main/java/com/ddaodan/MineChatGPT/ConfigManager.java b/src/main/java/com/ddaodan/MineChatGPT/ConfigManager.java index c130243..2be05d7 100644 --- a/src/main/java/com/ddaodan/MineChatGPT/ConfigManager.java +++ b/src/main/java/com/ddaodan/MineChatGPT/ConfigManager.java @@ -331,4 +331,29 @@ public class ConfigManager { public int getQueueDispatchPerTick() { return Math.max(1, config.getInt("queue.dispatch_per_tick", 2)); } + + public boolean isUpdateCheckerEnabled() { + return config.getBoolean("update_checker.enabled", true); + } + public String getUpdateCheckerSource() { + return config.getString("update_checker.source", "github"); + } + public String getHelpCheckUpdateMessage() { + return languageManager.getMessage("help_checkupdate"); + } + public String getUpdateCheckingMessage() { + return languageManager.getMessage("update_checking", "&eChecking for updates..."); + } + public String getUpdateAvailableMessage() { + return languageManager.getMessage("update_available", "&aA new version is available: %s (current: %s)"); + } + public String getUpdateLatestMessage() { + return languageManager.getMessage("update_latest", "&aYou are running the latest version."); + } + public String getUpdateErrorMessage() { + return languageManager.getMessage("update_error", "&cFailed to check for updates."); + } + public String getUpdateDownloadMessage() { + return languageManager.getMessage("update_download", "&eDownload: %s"); + } } diff --git a/src/main/java/com/ddaodan/MineChatGPT/Main.java b/src/main/java/com/ddaodan/MineChatGPT/Main.java index 1306138..58ededf 100644 --- a/src/main/java/com/ddaodan/MineChatGPT/Main.java +++ b/src/main/java/com/ddaodan/MineChatGPT/Main.java @@ -10,6 +10,7 @@ public final class Main extends JavaPlugin { private com.ddaodan.MineChatGPT.service.UserSessionManager sessionManager; private com.ddaodan.MineChatGPT.service.ApiService apiService; private com.ddaodan.MineChatGPT.service.RequestCoordinator requestCoordinator; + private com.ddaodan.MineChatGPT.service.UpdateChecker updateChecker; private CommandHandler commandHandler; private MineChatGPTTabCompleter tabCompleter; @@ -21,10 +22,13 @@ public final class Main extends JavaPlugin { apiService = new com.ddaodan.MineChatGPT.service.ApiService(this, configManager); requestCoordinator = new com.ddaodan.MineChatGPT.service.RequestCoordinator(this, configManager, apiService, sessionManager); requestCoordinator.start(); - commandHandler = new CommandHandler(configManager, sessionManager, requestCoordinator); + updateChecker = new com.ddaodan.MineChatGPT.service.UpdateChecker(this, configManager); + commandHandler = new CommandHandler(configManager, sessionManager, requestCoordinator, updateChecker); tabCompleter = new MineChatGPTTabCompleter(configManager); Objects.requireNonNull(getCommand("chatgpt")).setExecutor(commandHandler); Objects.requireNonNull(getCommand("chatgpt")).setTabCompleter(tabCompleter); + getServer().getPluginManager().registerEvents(updateChecker, this); + updateChecker.checkOnStartup(); if (configManager.isDebugMode()) { getLogger().info( "DEBUG MODE IS TRUE!!!!!"); } @@ -38,6 +42,9 @@ public final class Main extends JavaPlugin { if (requestCoordinator != null) { requestCoordinator.stop(); } + if (updateChecker != null) { + updateChecker.shutdown(); + } if (apiService != null) { apiService.shutdown(); } diff --git a/src/main/java/com/ddaodan/MineChatGPT/MineChatGPTTabCompleter.java b/src/main/java/com/ddaodan/MineChatGPT/MineChatGPTTabCompleter.java index 35b6549..7e34769 100644 --- a/src/main/java/com/ddaodan/MineChatGPT/MineChatGPTTabCompleter.java +++ b/src/main/java/com/ddaodan/MineChatGPT/MineChatGPTTabCompleter.java @@ -27,6 +27,7 @@ public class MineChatGPTTabCompleter implements TabCompleter { completions.add("clear"); completions.add("character"); completions.add("stats"); + completions.add("checkupdate"); } else if (args.length == 2) { String subCommand = args[0]; if (subCommand.equalsIgnoreCase("model")) { diff --git a/src/main/java/com/ddaodan/MineChatGPT/service/CommandService.java b/src/main/java/com/ddaodan/MineChatGPT/service/CommandService.java index ba344c7..94a4354 100644 --- a/src/main/java/com/ddaodan/MineChatGPT/service/CommandService.java +++ b/src/main/java/com/ddaodan/MineChatGPT/service/CommandService.java @@ -14,11 +14,13 @@ public class CommandService { private final ConfigManager configManager; private final RequestCoordinator requestCoordinator; private final UserSessionManager sessionManager; + private final UpdateChecker updateChecker; - public CommandService(ConfigManager configManager, RequestCoordinator requestCoordinator, UserSessionManager sessionManager) { + public CommandService(ConfigManager configManager, RequestCoordinator requestCoordinator, UserSessionManager sessionManager, UpdateChecker updateChecker) { this.configManager = configManager; this.requestCoordinator = requestCoordinator; this.sessionManager = sessionManager; + this.updateChecker = updateChecker; } /** @@ -199,6 +201,15 @@ public class CommandService { return true; } + public boolean handleCheckUpdateCommand(CommandSender sender) { + if (!sender.hasPermission("minechatgpt.checkupdate")) { + sender.sendMessage(configManager.getNoPermissionMessage().replace("%s", "minechatgpt.checkupdate")); + return true; + } + updateChecker.checkAndNotify(sender); + return true; + } + /** * 发送帮助信息 * @@ -214,5 +225,6 @@ public class CommandService { sender.sendMessage(configManager.getHelpClearMessage()); sender.sendMessage(configManager.getHelpCharacterMessage()); sender.sendMessage(configManager.getHelpStatsMessage()); + sender.sendMessage(configManager.getHelpCheckUpdateMessage()); } } diff --git a/src/main/java/com/ddaodan/MineChatGPT/service/UpdateChecker.java b/src/main/java/com/ddaodan/MineChatGPT/service/UpdateChecker.java new file mode 100644 index 0000000..ad5b05a --- /dev/null +++ b/src/main/java/com/ddaodan/MineChatGPT/service/UpdateChecker.java @@ -0,0 +1,264 @@ +package com.ddaodan.MineChatGPT.service; + +import com.ddaodan.MineChatGPT.ConfigManager; +import com.ddaodan.MineChatGPT.Main; +import jodd.http.HttpRequest; +import jodd.http.HttpResponse; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.json.JSONArray; +import org.json.JSONObject; + +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Method; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.logging.Level; + +public class UpdateChecker implements Listener { + private static final String GITHUB_API = "https://api.github.com/repos/ddaodan-minecraft/minechatgpt/releases/latest"; + private static final String MODRINTH_API = "https://api.modrinth.com/v2/project/minechatgpt/version?limit=1"; + private static final String GITHUB_URL = "https://github.com/ddaodan-minecraft/minechatgpt/releases/latest"; + private static final String MODRINTH_URL = "https://modrinth.com/plugin/minechatgpt"; + + private final Main plugin; + private final ConfigManager configManager; + private final ExecutorService executor; + private volatile String latestVersion; + + public UpdateChecker(Main plugin, ConfigManager configManager) { + this.plugin = plugin; + this.configManager = configManager; + this.executor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "minechatgpt-update-checker"); + t.setDaemon(true); + return t; + }); + } + + public void shutdown() { + executor.shutdownNow(); + } + + public void checkOnStartup() { + if (!configManager.isUpdateCheckerEnabled()) { + return; + } + checkAsync().thenAccept(result -> { + if (result.error) { + plugin.getLogger().warning("Failed to check for updates."); + } else if (result.hasUpdate) { + plugin.getLogger().info("A new version is available: " + result.latestVersion + + " (current: " + result.currentVersion + ")"); + plugin.getLogger().info("Download: " + result.downloadUrl); + } else { + plugin.getLogger().info("You are running the latest version (" + result.currentVersion + ")."); + } + }); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + if (!configManager.isUpdateCheckerEnabled()) { + return; + } + Player player = event.getPlayer(); + if (!player.hasPermission("minechatgpt.checkupdate")) { + return; + } + if (latestVersion == null) { + return; + } + String currentVersion = plugin.getDescription().getVersion(); + if (isNewerVersion(latestVersion, currentVersion)) { + String downloadUrl = getDownloadUrl(); + runOnMainThreadDelayed(() -> { + if (player.isOnline()) { + player.sendMessage(configManager.getUpdateAvailableMessage() + .replaceFirst("%s", latestVersion) + .replaceFirst("%s", currentVersion)); + player.sendMessage(configManager.getUpdateDownloadMessage() + .replace("%s", downloadUrl)); + } + }, 40L); + } + } + + public void checkAndNotify(CommandSender sender) { + sender.sendMessage(configManager.getUpdateCheckingMessage()); + checkAsync().thenAccept(result -> { + runOnMainThread(() -> { + if (sender instanceof Player && !((Player) sender).isOnline()) { + return; + } + if (result.error) { + sender.sendMessage(configManager.getUpdateErrorMessage()); + return; + } + if (result.hasUpdate) { + sender.sendMessage(configManager.getUpdateAvailableMessage() + .replaceFirst("%s", result.latestVersion) + .replaceFirst("%s", result.currentVersion)); + sender.sendMessage(configManager.getUpdateDownloadMessage() + .replace("%s", result.downloadUrl)); + } else { + sender.sendMessage(configManager.getUpdateLatestMessage()); + } + }); + }); + } + + private CompletableFuture checkAsync() { + return CompletableFuture.supplyAsync(() -> { + try { + String source = configManager.getUpdateCheckerSource(); + String currentVersion = plugin.getDescription().getVersion(); + String remote; + + if ("modrinth".equalsIgnoreCase(source)) { + remote = fetchModrinthVersion(); + } else { + remote = fetchGitHubVersion(); + } + + if (remote == null) { + return UpdateResult.errorResult(); + } + + latestVersion = remote; + boolean hasUpdate = isNewerVersion(remote, currentVersion); + return new UpdateResult(false, hasUpdate, remote, currentVersion, getDownloadUrl()); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "Failed to check for updates", e); + return UpdateResult.errorResult(); + } + }, executor); + } + + private String fetchGitHubVersion() { + HttpResponse response = HttpRequest.get(GITHUB_API) + .header("Accept", "application/vnd.github.v3+json") + .header("User-Agent", "MineChatGPT/" + plugin.getDescription().getVersion()) + .connectionTimeout(10000) + .timeout(15000) + .send(); + if (response.statusCode() != 200) { + return null; + } + JSONObject json = new JSONObject(response.bodyText()); + String tagName = json.getString("tag_name"); + return tagName.startsWith("v") ? tagName.substring(1) : tagName; + } + + private String fetchModrinthVersion() { + HttpResponse response = HttpRequest.get(MODRINTH_API) + .header("User-Agent", "MineChatGPT/" + plugin.getDescription().getVersion()) + .connectionTimeout(10000) + .timeout(15000) + .send(); + if (response.statusCode() != 200) { + return null; + } + JSONArray arr = new JSONArray(response.bodyText()); + if (arr.length() == 0) { + return null; + } + return arr.getJSONObject(0).getString("version_number"); + } + + private void runOnMainThread(Runnable runnable) { + if (isFoliaSchedulerAvailable()) { + try { + Object scheduler = getGlobalRegionScheduler(); + Method method = scheduler.getClass().getMethod("execute", Plugin.class, Runnable.class); + method.invoke(scheduler, plugin, runnable); + return; + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "Failed to execute on Folia scheduler, fallback to Bukkit.", e); + } + } + Bukkit.getScheduler().runTask(plugin, runnable); + } + + private void runOnMainThreadDelayed(Runnable runnable, long delayTicks) { + if (isFoliaSchedulerAvailable()) { + try { + Object scheduler = getGlobalRegionScheduler(); + Consumer consumer = task -> runnable.run(); + Method method = scheduler.getClass().getMethod("runDelayed", Plugin.class, Consumer.class, long.class); + method.invoke(scheduler, plugin, consumer, delayTicks); + return; + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "Failed to schedule delayed Folia task, fallback to Bukkit.", e); + } + } + Bukkit.getScheduler().runTaskLater(plugin, runnable, delayTicks); + } + + private boolean isFoliaSchedulerAvailable() { + try { + Bukkit.getServer().getClass().getMethod("getGlobalRegionScheduler"); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + private Object getGlobalRegionScheduler() throws Exception { + Method method = Bukkit.getServer().getClass().getMethod("getGlobalRegionScheduler"); + return method.invoke(Bukkit.getServer()); + } + + private String getDownloadUrl() { + String source = configManager.getUpdateCheckerSource(); + return "modrinth".equalsIgnoreCase(source) ? MODRINTH_URL : GITHUB_URL; + } + + static boolean isNewerVersion(String remote, String current) { + String[] remoteParts = remote.split("\\."); + String[] currentParts = current.split("\\."); + int length = Math.max(remoteParts.length, currentParts.length); + for (int i = 0; i < length; i++) { + int r = i < remoteParts.length ? parseIntSafe(remoteParts[i]) : 0; + int c = i < currentParts.length ? parseIntSafe(currentParts[i]) : 0; + if (r > c) return true; + if (r < c) return false; + } + return false; + } + + private static int parseIntSafe(String s) { + try { + return Integer.parseInt(s.trim()); + } catch (NumberFormatException e) { + return 0; + } + } + + private static final class UpdateResult { + final boolean error; + final boolean hasUpdate; + final String latestVersion; + final String currentVersion; + final String downloadUrl; + + UpdateResult(boolean error, boolean hasUpdate, String latestVersion, String currentVersion, String downloadUrl) { + this.error = error; + this.hasUpdate = hasUpdate; + this.latestVersion = latestVersion; + this.currentVersion = currentVersion; + this.downloadUrl = downloadUrl; + } + + static UpdateResult errorResult() { + return new UpdateResult(true, false, null, null, null); + } + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index fe57eea..e58fe0b 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -257,6 +257,18 @@ characters: # Example: # Minecraft: "You are a Minecraft expert who helps players with game mechanics and building ideas." +# ====================================================== +# Update Checker Settings +# 更新检查设置 +# ====================================================== +update_checker: + # Enable automatic update checking on server start and OP join + # 启用服务器启动和OP加入时自动检查更新 + enabled: true + # Update source: "github" or "modrinth" + # 更新源:"github" 或 "modrinth" + source: "github" + # ====================================================== # Other Settings # 其他设置 diff --git a/src/main/resources/lang/en.yml b/src/main/resources/lang/en.yml index fd79054..ca5c0ad 100644 --- a/src/main/resources/lang/en.yml +++ b/src/main/resources/lang/en.yml @@ -36,3 +36,9 @@ messages: invalid_model: "&cInvalid model. Use /chatgpt modellist to see available models." available_models: "&eAvailable models:" no_permission: "&cYou do not have permission to use this command. Required permission: %s" + help_checkupdate: "&e/chatgpt checkupdate - Check for plugin updates." + update_checking: "&eChecking for updates..." + update_available: "&aA new version is available: %s (current: %s)" + update_latest: "&aYou are running the latest version." + update_error: "&cFailed to check for updates." + update_download: "&eDownload: %s" diff --git a/src/main/resources/lang/zh.yml b/src/main/resources/lang/zh.yml index 24a894b..3a33548 100644 --- a/src/main/resources/lang/zh.yml +++ b/src/main/resources/lang/zh.yml @@ -36,3 +36,9 @@ messages: invalid_model: "&c模型无效。使用 /chatgpt modellist 查看可用模型。" available_models: "&e可用模型列表:" no_permission: "&c你没有权限使用这个指令。需要的权限:%s" + help_checkupdate: "&e/chatgpt checkupdate - 检查插件更新。" + update_checking: "&e正在检查更新..." + update_available: "&a发现新版本:%s(当前版本:%s)" + update_latest: "&a你正在使用最新版本。" + update_error: "&c检查更新失败。" + update_download: "&e下载地址:%s" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 13a3d72..a8f658c 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -34,3 +34,6 @@ permissions: minechatgpt.stats: description: Allows viewing/resetting usage statistics default: op + minechatgpt.checkupdate: + description: Allows checking for plugin updates + default: op