2 Commits
2.8 ... master

11 changed files with 429 additions and 10 deletions

View File

@@ -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);
}

View File

@@ -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");
}
}

View File

@@ -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();
}

View File

@@ -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")) {

View File

@@ -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());
}
}

View File

@@ -6,13 +6,16 @@ import com.ddaodan.MineChatGPT.Main;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.json.JSONArray;
import org.json.JSONObject;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.logging.Level;
public class RequestCoordinator {
@@ -32,6 +35,7 @@ public class RequestCoordinator {
private volatile TokenEstimator tokenEstimator;
private volatile RequestQueue<RequestJob> queue;
private volatile int dispatchTaskId = -1;
private volatile Object foliaDispatchTask;
public RequestCoordinator(Main plugin, ConfigManager configManager, ApiService apiService, UserSessionManager sessionManager) {
this.plugin = plugin;
@@ -57,10 +61,10 @@ public class RequestCoordinator {
}
public void start() {
if (dispatchTaskId != -1) {
if (dispatchTaskId != -1 || foliaDispatchTask != null) {
return;
}
dispatchTaskId = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
Runnable dispatchRunnable = () -> {
if (!configManager.isQueueEnabled()) {
return;
}
@@ -76,10 +80,30 @@ public class RequestCoordinator {
}
dispatch(job);
}
}, 1L, 1L).getTaskId();
};
if (isFoliaSchedulerAvailable()) {
try {
foliaDispatchTask = runFoliaRepeating(dispatchRunnable, 1L, 1L);
return;
} catch (Exception e) {
plugin.getLogger().log(Level.WARNING, "Failed to schedule Folia repeating task, fallback to Bukkit scheduler.", e);
}
}
dispatchTaskId = Bukkit.getScheduler().runTaskTimer(plugin, dispatchRunnable, 1L, 1L).getTaskId();
}
public void stop() {
if (foliaDispatchTask != null) {
try {
cancelFoliaTask(foliaDispatchTask);
} catch (Exception e) {
plugin.getLogger().log(Level.WARNING, "Failed to cancel Folia dispatch task.", e);
} finally {
foliaDispatchTask = null;
}
}
if (dispatchTaskId != -1) {
Bukkit.getScheduler().cancelTask(dispatchTaskId);
dispatchTaskId = -1;
@@ -163,7 +187,7 @@ public class RequestCoordinator {
}
private void handleCompletion(RequestJob job, String characterName, ApiService.ChatCompletionResult result) {
Bukkit.getScheduler().runTask(plugin, () -> {
runOnMainThread(() -> {
if (!plugin.isEnabled()) {
return;
}
@@ -270,7 +294,7 @@ public class RequestCoordinator {
private CompletableFuture<Void> runSync(Runnable runnable) {
CompletableFuture<Void> future = new CompletableFuture<>();
Bukkit.getScheduler().runTask(plugin, () -> {
runOnMainThread(() -> {
try {
runnable.run();
future.complete(null);
@@ -281,6 +305,63 @@ public class RequestCoordinator {
return future;
}
private void runOnMainThread(Runnable runnable) {
if (isFoliaSchedulerAvailable()) {
try {
runFoliaNow(runnable);
return;
} catch (Exception e) {
plugin.getLogger().log(Level.WARNING, "Failed to execute on Folia scheduler, fallback to Bukkit scheduler.", e);
}
}
Bukkit.getScheduler().runTask(plugin, runnable);
}
private Object runFoliaRepeating(Runnable runnable, long initialDelayTicks, long periodTicks) throws Exception {
Object scheduler = getGlobalRegionScheduler();
Method method = scheduler.getClass().getMethod("runAtFixedRate", Plugin.class, Consumer.class, long.class, long.class);
Consumer<Object> consumer = task -> runnable.run();
return method.invoke(scheduler, plugin, consumer, initialDelayTicks, periodTicks);
}
private void cancelFoliaTask(Object task) throws Exception {
try {
// Prefer invoking through public ScheduledTask interface when available.
Class<?> scheduledTaskInterface = Class.forName("io.papermc.paper.threadedregions.scheduler.ScheduledTask");
Method cancel = scheduledTaskInterface.getMethod("cancel");
cancel.invoke(task);
return;
} catch (ClassNotFoundException ignored) {
// Fall through to reflective invocation on task implementation.
}
Method cancel = task.getClass().getDeclaredMethod("cancel");
if (!cancel.isAccessible()) {
cancel.setAccessible(true);
}
cancel.invoke(task);
}
private void runFoliaNow(Runnable runnable) throws Exception {
Object scheduler = getGlobalRegionScheduler();
Method method = scheduler.getClass().getMethod("execute", Plugin.class, Runnable.class);
method.invoke(scheduler, plugin, runnable);
}
private Object getGlobalRegionScheduler() throws Exception {
Method method = Bukkit.getServer().getClass().getMethod("getGlobalRegionScheduler");
return method.invoke(Bukkit.getServer());
}
private boolean isFoliaSchedulerAvailable() {
try {
Bukkit.getServer().getClass().getMethod("getGlobalRegionScheduler");
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
private void trimContextToBudget(String characterPrompt, String summary, ConversationContext context) {
int budget = configManager.getMaxContextTokens();
if (budget <= 0) {

View File

@@ -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<UpdateResult> 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<Object> 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);
}
}
}

View File

@@ -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
# 其他设置

View File

@@ -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"

View File

@@ -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"

View File

@@ -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