From 68c5b922a78d0acedd606f062f25d641a0eb02d6 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Thu, 17 Sep 2026 12:39:53 +0800 Subject: [PATCH 01/96] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20ImageUtil=20?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=B1=BB=EF=BC=8C=E6=94=AF=E6=8C=81=E5=9B=BE?= =?UTF-8?q?=E5=83=8F=E8=AF=BB=E5=8F=96=E3=80=81=E8=A3=81=E5=89=AA=E3=80=81?= =?UTF-8?q?=E9=AB=98=E6=96=AF=E6=A8=A1=E7=B3=8A=E5=92=8C=E9=A9=AC=E8=B5=9B?= =?UTF-8?q?=E5=85=8B=E5=A4=84=E7=90=86=EF=BC=9B=E6=9B=B4=E6=96=B0=20QQApi?= =?UTF-8?q?=20=E5=92=8C=20RankGuessCommandHandler=20=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=A4=B4=E5=83=8F=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- src/main/java/xyz/zcraft/seira/bot/QQApi.java | 4 + src/main/java/xyz/zcraft/seira/bot/QQBot.java | 18 +- .../java/xyz/zcraft/seira/bot/WSClient.java | 1 + .../handler/RankGuessCommandHandler.java | 14 +- .../command/handler/SocialCommandHandler.java | 34 ++- .../seira/command/reply/ReplyFactory.java | 33 ++- .../zcraft/seira/command/route/Router.java | 81 +++---- .../xyz/zcraft/seira/data/UploadedImage.java | 4 + .../xyz/zcraft/seira/rankguess/HintUtil.java | 6 +- .../seira/rankguess/RankGuessGameService.java | 24 +- .../zcraft/seira/rankguess/data/Round.java | 78 ++++-- .../java/xyz/zcraft/seira/util/ImageUtil.java | 225 ++++++++++++++++++ .../xyz/zcraft/seira/util/TokenManager.java | 1 + .../xyz/zcraft/seira/util/WeightedRandom.java | 70 ++++++ 15 files changed, 492 insertions(+), 103 deletions(-) create mode 100644 src/main/java/xyz/zcraft/seira/util/ImageUtil.java create mode 100644 src/main/java/xyz/zcraft/seira/util/WeightedRandom.java diff --git a/pom.xml b/pom.xml index c003a53f..0906c1a9 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.0 + 1.12.1 25 diff --git a/src/main/java/xyz/zcraft/seira/bot/QQApi.java b/src/main/java/xyz/zcraft/seira/bot/QQApi.java index 9dde38f6..acf3edee 100644 --- a/src/main/java/xyz/zcraft/seira/bot/QQApi.java +++ b/src/main/java/xyz/zcraft/seira/bot/QQApi.java @@ -653,4 +653,8 @@ private record UploadPart(int index, String presignedUrl, long blockSize) { private record UploadPrepare(String uploadId, long blockSize, List parts, int concurrency, int retryTimeoutSeconds, int retryDelaySeconds) { } + + public static String getAvatarUrl(String appId, String openId) { + return "https://thirdqq.qlogo.cn/qqapp/" + appId + "/" + openId + "/100"; + } } diff --git a/src/main/java/xyz/zcraft/seira/bot/QQBot.java b/src/main/java/xyz/zcraft/seira/bot/QQBot.java index 7db86c07..1abac239 100644 --- a/src/main/java/xyz/zcraft/seira/bot/QQBot.java +++ b/src/main/java/xyz/zcraft/seira/bot/QQBot.java @@ -58,6 +58,7 @@ public class QQBot implements AutoCloseable, ConsoleRuntimeControl { private final AtomicBoolean closed = new AtomicBoolean(); private final AtomicReference activeClient = new AtomicReference<>(); private volatile Thread runnerThread; + final AtomicReference self = new AtomicReference<>(); public QQBot( RuntimeConfig runtimeConfig, @@ -99,6 +100,8 @@ public QQBot( Duration.ofSeconds(config.seira().effectiveMultiplayerWatchIntervalSeconds()) ); + + LOG.info("Initializing rank guess service"); this.rankGuessGameService = new RankGuessGameService(); this.attachmentHandler = new AttachmentHandler(executors.attachmentDownloads()); @@ -112,7 +115,16 @@ public QQBot( discordBridgeService, rankGuessGameService, executors.commandTasks(), - BotStat::incrementCommands + BotStat::incrementCommands, + bytes -> { + try { + return cos.uploadImage(bytes); + } catch (Exception e) { + LOG.error("Error uploading image", e); + return null; + } + }, + self::get ); } @@ -141,9 +153,9 @@ public void start() { String wssEndpoint = QQApi.getWSSEndpoint(tokenManager.getToken()); LOG.info("Endpoint: {}", wssEndpoint); - final QQUser self = QQApi.getSelf(tokenManager.getToken()); + self.set(QQApi.getSelf(tokenManager.getToken())); - LOG.info("Self info: id={}, nickname={}", self.id(), self.username()); + LOG.info("Self info: id={}, nickname={}", self.get().id(), self.get().username()); WSClient client = new WSClient( URI.create(wssEndpoint), diff --git a/src/main/java/xyz/zcraft/seira/bot/WSClient.java b/src/main/java/xyz/zcraft/seira/bot/WSClient.java index 54a4db30..15676595 100644 --- a/src/main/java/xyz/zcraft/seira/bot/WSClient.java +++ b/src/main/java/xyz/zcraft/seira/bot/WSClient.java @@ -9,6 +9,7 @@ import org.java_websocket.handshake.ServerHandshake; import xyz.zcraft.seira.bot.data.AccessToken; import xyz.zcraft.seira.bot.data.Attachment; +import xyz.zcraft.seira.bot.data.QQUser; import xyz.zcraft.seira.command.AttachmentHandler; import xyz.zcraft.seira.command.route.Router; import xyz.zcraft.seira.config.AppConfig; diff --git a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java index 2b6cc407..d50dfd51 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java @@ -13,6 +13,7 @@ import xyz.zcraft.seira.command.parse.UserRefResolution; import xyz.zcraft.seira.command.reply.ReplyFactory; import xyz.zcraft.seira.data.SendResult; +import xyz.zcraft.seira.data.UploadedImage; import xyz.zcraft.seira.data.UserRef; import xyz.zcraft.seira.db.RankGuessRecordStore; import xyz.zcraft.seira.db.UserDataStore; @@ -26,7 +27,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -44,6 +47,8 @@ public final class RankGuessCommandHandler { private final RankGuessGameService games; private final Resolver resolver; private final Predicate adminAuthorizer; + private final Function avatarUrlGetter; + private final Function imageUploader; private final Pattern BP_PATTERN = Pattern.compile("^bp(\\d+)$"); public RankGuessCommandHandler( @@ -51,13 +56,17 @@ public RankGuessCommandHandler( ReplyFactory replyFactory, RankGuessGameService games, Resolver resolver, - Predicate adminAuthorizer + Predicate adminAuthorizer, + Function avatarUrlGetter, + Function imageUploader ) { this.taskCoordinator = taskCoordinator; this.replyFactory = replyFactory; this.games = games; this.resolver = resolver; this.adminAuthorizer = adminAuthorizer; + this.avatarUrlGetter = avatarUrlGetter; + this.imageUploader = imageUploader; } private static Long parseRank(String argument) { @@ -543,7 +552,8 @@ private void start(Context ctx, boolean fromGroup) { hintSource.addAll(round.getNormalHints()); if (fromGroup) { - hintSource.addAll(round.getGroupHints()); + final Optional groupOpenIdByUid = UserDataStore.findGroupOpenIdByUid(ctx.groupId(), round.userId()); + hintSource.addAll(round.getGroupHints(avatarUrlGetter.apply(groupOpenIdByUid.orElse(null)), imageUploader)); } var hints = HintUtil.prepareHints(hintSource, maxHintCount); diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java index 1288a63b..c9a19ee2 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java @@ -32,19 +32,22 @@ public final class SocialCommandHandler { private final TaskCoordinator taskCoordinator; private final ReplyFactory replyFactory; private final Function accessTokenProvider; + private final Function avatarProvider; public SocialCommandHandler( Resolver resolver, OsuAuthHelper authHelper, TaskCoordinator taskCoordinator, ReplyFactory replyFactory, - Function accessTokenProvider + Function accessTokenProvider, + Function avatarProvider ) { this.resolver = resolver; this.authHelper = authHelper; this.taskCoordinator = taskCoordinator; this.replyFactory = replyFactory; this.accessTokenProvider = accessTokenProvider; + this.avatarProvider = avatarProvider; } public void handleMp(Context ctx) { @@ -103,11 +106,18 @@ public void handleFriendStatus(Context ctx) { boolean selfFollowed; final AtomicReference targetFollowed = new AtomicReference<>(); - final OsuToken self = authHelper.updateTokenAndGet(ctx.senderUserId()); - final List selfFollowedList = APIHelper.getFollowed(self.accessToken()).getContent(); + AtomicReference selfOsuAvatar = new AtomicReference<>("https://osu.ppy.sh/images/layout/avatar-guest.png"); + AtomicReference targetOsuAvatar = new AtomicReference<>("https://osu.ppy.sh/images/layout/avatar-guest.png"); + + final OsuToken selfToken = authHelper.updateTokenAndGet(ctx.senderUserId()); + final var selfUser = APIHelper.getSelf(selfToken.accessToken()).getContent(); + + final List selfFollowedList = APIHelper.getFollowed(selfToken.accessToken()).getContent(); updateFriends(selfId, selfFollowedList); final Set users = new HashSet<>(selfFollowedList.stream().map(FriendEntry::user).toList()); + users.add(selfUser); + selfFollowed = selfFollowedList.stream().anyMatch(e -> e.user().getId() == targetId); selfFollowedList.stream().filter(e -> e.user().getId() == targetId).findFirst().ifPresentOrElse( @@ -127,9 +137,23 @@ public void handleFriendStatus(Context ctx) { UserDataStore.storeUserInfo(users); + users.stream().filter(u -> u.getId() == selfId).findFirst().ifPresent(u -> { + selfOsuAvatar.set(u.getAvatarUrl()); + }); + + users.stream().filter(u -> u.getId() == targetId).findFirst().ifPresent(u -> { + targetOsuAvatar.set(u.getAvatarUrl()); + }); + ctx.sendReply(replyFactory.friendStatusMessage( - ctx.senderUserId(), selfId, UserDataStore.findUsername(selfId).orElse("未知"), - s, targetId, UserDataStore.findUsername(targetId).orElse("未知"), + ctx.senderUserId(), selfId, selfOsuAvatar.get(), + UserDataStore.findUsername(selfId).orElse("未知"), + avatarProvider.apply(ctx.senderUserId()), + + s, targetId, targetOsuAvatar.get(), + UserDataStore.findUsername(targetId).orElse("未知"), + avatarProvider.apply(s), + selfFollowed, targetFollowed.get() ) ); diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index 92061d15..d87e1ba8 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -100,7 +100,7 @@ public static String at(String openId) { } @SuppressWarnings("unused") - static String url(String text, String url) { + public static String url(String text, String url) { return "[" + text + "](" + url + ")"; } @@ -119,13 +119,13 @@ private static boolean isCancelableReplayStatus(String status) { || "upload_queued".equals(status) || "uploading".equals(status); } - public PendingMessage friendStatusMessage(String selfOpenId, Long selfUid, String selfUsername, - String targetOpenId, Long targetUid, String targetUsername, + public PendingMessage friendStatusMessage(String selfOpenId, Long selfUid, String selfOsuAvatar, String selfUsername, String selfAvatar, + String targetOpenId, Long targetUid, String targetOsuAvatar, String targetUsername, String targetAvatar, boolean selfFollowed, Boolean targetFollowed) { return PendingMessage.ofMarkdownRaw( Contents.friendStatusContent( - selfOpenId, selfUid, selfUsername, - targetOpenId, targetUid, targetUsername, + selfOpenId, selfUid, selfOsuAvatar, selfUsername, selfAvatar, + targetOpenId, targetUid, targetOsuAvatar, targetUsername, targetAvatar, selfFollowed, targetFollowed ) ); @@ -801,8 +801,8 @@ public static String luckContent(Context ctx, DailyLuck.Luck luck, Beatmapset ma return sb.trim(); } - public static String friendStatusContent(String selfOpenId, Long selfUid, String selfUsername, - String targetOpenId, Long targetUid, String targetUsername, + public static String friendStatusContent(String selfOpenId, Long selfUid, String selfOsuAvatar, String selfUsername, String selfAvatar, + String targetOpenId, Long targetUid, String targetOsuAvatar, String targetUsername, String targetAvatar, boolean selfFollowed, Boolean targetFollowed) { final String status; if (targetFollowed == null) { @@ -822,10 +822,21 @@ public static String friendStatusContent(String selfOpenId, Long selfUid, String status = "✕ 路人 ✕"; } } - return at(selfOpenId) + "你们的好友状态(点击打开个人主页):" + "\n" + - url(selfUsername, "https://osu.ppy.sh/users/" + selfUid) + " (" + at(selfOpenId) + ")\n" + - " " + status + "\n" + - url(targetUsername, "https://osu.ppy.sh/users/" + targetUid) + " (" + at(targetOpenId) + ")"; + + return at(selfOpenId) + """ + 你们的好友状态: + > %s: + > ![image #30px #30px](%s) __ | %s__ + + %s + + > %s: + > ![image #30px #30px](%s) __ | %s__ + """.formatted( + at(selfOpenId), selfOsuAvatar, url(selfUsername, "https://osu.ppy.sh/users/" + selfUid), + status, + at(targetOpenId), targetOsuAvatar, url(targetUsername, "https://osu.ppy.sh/users/" + targetUid) + ); } public static String missImageContent(Context ctx, String scoreId, Integer index, int size) { diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index 2e139b25..5f50123f 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -6,13 +6,16 @@ import xyz.zcraft.seira.api.data.OsuToken; import xyz.zcraft.seira.api.data.VideoRenderRecord; import xyz.zcraft.seira.bot.MessageSender; +import xyz.zcraft.seira.bot.QQApi; import xyz.zcraft.seira.bot.data.PendingMessage; +import xyz.zcraft.seira.bot.data.QQUser; import xyz.zcraft.seira.command.*; import xyz.zcraft.seira.command.handler.*; import xyz.zcraft.seira.command.parse.CommandParser; import xyz.zcraft.seira.command.parse.Resolver; import xyz.zcraft.seira.command.reply.ReplyFactory; import xyz.zcraft.seira.config.AppConfig; +import xyz.zcraft.seira.data.UploadedImage; import xyz.zcraft.seira.db.UserDataStore; import xyz.zcraft.seira.discord.DiscordBridgeService; import xyz.zcraft.seira.rankguess.RankGuessGameService; @@ -27,6 +30,7 @@ import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.function.Supplier; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; @@ -44,22 +48,18 @@ public class Router { private final Runnable commandMetric; private final CommandHandler unknownCommand; private final Executor commandExecutor; + private final Supplier selfSupplier; public Router( - MessageSender messageSender, - Supplier configSupplier, - AdminRegistry admins, - BindingService bindingService, - ScoreWatchService watchService, - MultiplayerRoomWatchService multiplayerRoomWatchService, - DiscordBridgeService discordBridgeService, - RankGuessGameService rankGuessGameService, - Executor commandExecutor, - Runnable commandMetric + MessageSender messageSender, Supplier configSupplier, AdminRegistry admins, + BindingService bindingService, ScoreWatchService watchService, MultiplayerRoomWatchService multiplayerRoomWatchService, + DiscordBridgeService discordBridgeService, RankGuessGameService rankGuessGameService, Executor commandExecutor, + Runnable commandMetric, Function imageUploader, Supplier selfSupplier ) { this.configSupplier = java.util.Objects.requireNonNull(configSupplier); this.commandExecutor = commandExecutor; this.commandMetric = java.util.Objects.requireNonNull(commandMetric); + this.selfSupplier = selfSupplier; AppConfig startupConfig = configSupplier.get(); ReplyFactory replyFactory = new ReplyFactory(configSupplier); Resolver resolver = new Resolver(); @@ -76,15 +76,10 @@ public Router( resolver, history, taskCoordinator, replyFactory, videoRenderRecord, this::getAccessTokenFor ); SocialCommandHandler socialCommands = new SocialCommandHandler( - resolver, authHelper, taskCoordinator, replyFactory, this::getAccessTokenFor + resolver, authHelper, taskCoordinator, replyFactory, this::getAccessTokenFor, this::getAvatar ); ReplayCommandHandler replayCommands = new ReplayCommandHandler( - resolver, - history, - taskCoordinator, - replyFactory, - videoRenderRecord, - replayResults + resolver, history, taskCoordinator, replyFactory, videoRenderRecord, replayResults ); GeneralCommandHandler generalCommands = new GeneralCommandHandler( messageSender, taskCoordinator, replyFactory, resolver, admins::isAdmin @@ -96,45 +91,31 @@ public Router( new MultiplayerRoomWatchCommandHandler(taskCoordinator, multiplayerRoomWatchService); DcsCommandHandler dcsCommands = new DcsCommandHandler(discordBridgeService); RankGuessCommandHandler rankGuessCommands = new RankGuessCommandHandler( - taskCoordinator, replyFactory, rankGuessGameService, resolver, admins::isAdmin + taskCoordinator, replyFactory, rankGuessGameService, resolver, admins::isAdmin, this::getAvatar, imageUploader ); this.unknownCommand = generalCommands::handleUnknown; this.commandParser = new CommandParser(resolver::sanitize); this.commandRegistry = createCommandRegistry( - bindingCommands, - scoreCommands, - beatmapCommands, - socialCommands, - replayCommands, - generalCommands, - watchCommands, - specificScoreWatchCommands, - multiplayerRoomWatchCommands, - dcsCommands, - rankGuessCommands + bindingCommands, scoreCommands, beatmapCommands, socialCommands, + replayCommands, generalCommands, watchCommands, specificScoreWatchCommands, + multiplayerRoomWatchCommands, dcsCommands, rankGuessCommands ); this.debugRoutes = new DebugRoutes( - configSupplier, - messageSender, - replyFactory, - taskCoordinator, - authHelper, - admins::isAdmin, - unknownCommand + configSupplier, messageSender, replyFactory, taskCoordinator, + authHelper, admins::isAdmin, unknownCommand ); } + private String getAvatar(String openId) { + return QQApi.getAvatarUrl(configSupplier.get().qq().appId(), openId); + } + private static CommandRegistry createCommandRegistry( - BindingCommandHandler bindingCommands, - ScoreCommandHandler scoreCommands, - BeatmapCommandHandler beatmapCommands, - SocialCommandHandler socialCommands, - ReplayCommandHandler replayCommands, - GeneralCommandHandler generalCommands, - WatchCommandHandler watchCommands, - SpecificScoreWatchCommandHandler specificScoreWatchCommands, - MultiplayerRoomWatchCommandHandler multiplayerRoomWatchCommands, - DcsCommandHandler dcsCommands, + BindingCommandHandler bindingCommands, ScoreCommandHandler scoreCommands, + BeatmapCommandHandler beatmapCommands, SocialCommandHandler socialCommands, + ReplayCommandHandler replayCommands, GeneralCommandHandler generalCommands, + WatchCommandHandler watchCommands, SpecificScoreWatchCommandHandler specificScoreWatchCommands, + MultiplayerRoomWatchCommandHandler multiplayerRoomWatchCommands, DcsCommandHandler dcsCommands, RankGuessCommandHandler rankGuessCommands ) { return CommandRegistry.builder() @@ -207,6 +188,14 @@ private void handleMessageReceived(String targetId, String groupId, String userI rawContent = rawContent.substring(selfAt.length()).trim(); } + final QQUser qqUser = selfSupplier.get(); + if (qqUser != null) { + final String selfLiteralAt = "@" + qqUser.username(); + if (rawContent.startsWith(selfLiteralAt)) { + rawContent = rawContent.substring(selfLiteralAt.length()).trim(); + } + } + CommandParser.ParseResult parseResult = commandParser.parse( rawContent, userId, groupId, messageId ); diff --git a/src/main/java/xyz/zcraft/seira/data/UploadedImage.java b/src/main/java/xyz/zcraft/seira/data/UploadedImage.java index 62c41099..a4f2d0d3 100644 --- a/src/main/java/xyz/zcraft/seira/data/UploadedImage.java +++ b/src/main/java/xyz/zcraft/seira/data/UploadedImage.java @@ -4,4 +4,8 @@ public record UploadedImage(String url, int width, int height) { public String toMarkdown() { return "![image #" + width + "px #" + height + "px](" + url + ")"; } + + public String toMarkdown(int width, int height) { + return "![image #" + width + "px #" + height + "px](" + url + ")"; + } } diff --git a/src/main/java/xyz/zcraft/seira/rankguess/HintUtil.java b/src/main/java/xyz/zcraft/seira/rankguess/HintUtil.java index 0b499022..4aa0cba3 100644 --- a/src/main/java/xyz/zcraft/seira/rankguess/HintUtil.java +++ b/src/main/java/xyz/zcraft/seira/rankguess/HintUtil.java @@ -106,9 +106,9 @@ private static RankGuessGame.Hint selectWeightedByStrength( } private static EnumMap strengthWeights(double progress) { - double[] first = {0, 48, 34, 15, 0, 0, 3}; - double[] middle = {0, 9, 28, 44, 15, 0, 4}; - double[] late = {0, 0, 4, 38, 48, 5, 5}; + double[] first = {0, 48, 34, 14, 0, 0, 4}; + double[] middle = {0, 7, 28, 44, 15, 0, 6}; + double[] late = {0, 0, 1, 38, 48, 5, 8}; double phase = progress <= 0.5 ? progress * 2 : (progress - 0.5) * 2; diff --git a/src/main/java/xyz/zcraft/seira/rankguess/RankGuessGameService.java b/src/main/java/xyz/zcraft/seira/rankguess/RankGuessGameService.java index cfc1a7b6..751db64c 100644 --- a/src/main/java/xyz/zcraft/seira/rankguess/RankGuessGameService.java +++ b/src/main/java/xyz/zcraft/seira/rankguess/RankGuessGameService.java @@ -85,12 +85,12 @@ public static boolean isOutstandingGuess( return Math.abs(guess - actualRank) <= allowedDifference; } - public static List getUsernameFeature(String username) { + public static List> getUsernameFeature(String username) { if (username == null || username.isBlank()) { return List.of(); } - final List features = new ArrayList<>(); + final List> features = new ArrayList<>(); final int leftBracket = username.indexOf("["); final int rightBracket = username.indexOf("]"); @@ -103,42 +103,42 @@ public static List getUsernameFeature(String username) { if (leftBracket == 0 && rightBracket < username.length() - 1) { // [Prefix]Example final String prefix = username.substring(0, rightBracket + 1); - features.add("有前缀 `" + prefix + "`"); + features.add(Map.entry("有前缀 `" + prefix + "`", 0.5)); } } if (username.charAt(0) == username.charAt(username.length() - 1)) { - features.add("为 `首尾一样`"); + features.add(Map.entry("为 `首尾一样`", 0.4)); } boolean hasLetter = username.chars().anyMatch(Character::isLetter); if (hasLetter) { if (Objects.equals(username, username.toUpperCase())) { - features.add("为 `全大写`"); + features.add(Map.entry("为 `全大写`", 0.2)); } else if (Objects.equals(username, username.toLowerCase())) { - features.add("为 `全小写`"); + features.add(Map.entry("为 `全小写`", 0.2)); } } - features.add("长度为 `" + username.length() + "`"); + features.add(Map.entry("长度为 `" + username.length() + "`", 0.1)); if (username.contains(" ")) { - features.add("有 `空格`"); + features.add(Map.entry("有 `空格`", 0.15)); } if (username.contains("_")) { - features.add("有 `下划线(_)`"); + features.add(Map.entry("有 `下划线(_)`", 0.15)); } if (username.contains("-")) { - features.add("有 `横杠(-)`"); + features.add(Map.entry("有 `横杠(-)`", 0.15)); } if (PREFIX_NUMBER_PATTERN.matcher(username).matches()) { - features.add("是 `一串数字一串字母`"); + features.add(Map.entry("是 `一串数字一串字母`", 0.25)); } else if (SUFFIX_NUMBER_PATTERN.matcher(username).matches()) { - features.add("是 `一串字母一串数字`"); + features.add(Map.entry("是 `一串字母一串数字`", 0.25)); } return features; diff --git a/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java b/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java index 0942c3b5..2424a38c 100644 --- a/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java +++ b/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java @@ -3,13 +3,20 @@ import xyz.zcraft.osu.model.Score; import xyz.zcraft.osu.model.UserExtended; import xyz.zcraft.seira.api.data.RandomScore; +import xyz.zcraft.seira.data.UploadedImage; import xyz.zcraft.seira.rankguess.RankGuessGame; import xyz.zcraft.seira.rankguess.RankGuessGameService; +import xyz.zcraft.seira.util.ImageUtil; +import xyz.zcraft.seira.util.WeightedRandom; +import java.awt.*; +import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; public record Round(long userId, long scoreId, int bestIndex, long actualRank, Double pp, RandomScore randomScore, boolean standard) { @@ -191,80 +198,111 @@ public LinkedList getNormalHints() { return hints; } - public LinkedList getGroupHints() { - LinkedList hints = new LinkedList<>(); + public List getGroupHints(String avatarUrl, Function imageUploader) { + WeightedRandom hintRandom = new WeightedRandom<>(); final UserExtended user = this.randomScore.user(); final UserExtended.Team team = user.getTeam(); if (team != null && team.getName() != null && team.getShortName() != null) { - hints.add(new RankGuessGame.Hint( + hintRandom.add(new RankGuessGame.Hint( "该玩家所处的队伍缩写为 `%s`".formatted(team.getShortName()), "玩家队伍缩写", RankGuessGame.Hint.HintCategory.USER, RankGuessGame.Hint.HintStrength.SPECIAL - )); + ), 2.0); } if (user.getHasSupported() && user.isSupporter()) { - hints.add(new RankGuessGame.Hint( + hintRandom.add(new RankGuessGame.Hint( "该玩家是尊贵的撒泼特!", "支持者状态", RankGuessGame.Hint.HintCategory.USER, RankGuessGame.Hint.HintStrength.SPECIAL - )); + ), 1.5); } else if (user.getHasSupported() && !user.isSupporter()) { - hints.add(new RankGuessGame.Hint( + hintRandom.add(new RankGuessGame.Hint( "该玩家的撒泼特已经过期了。", "支持者状态", RankGuessGame.Hint.HintCategory.USER, RankGuessGame.Hint.HintStrength.SPECIAL - )); + ), 1.5); } if (user.getInterests() != null && !user.getInterests().isBlank()) { - hints.add(new RankGuessGame.Hint( + hintRandom.add(new RankGuessGame.Hint( "该玩家填写的兴趣爱好为 `%s`".formatted(user.getInterests()), "玩家自述兴趣", RankGuessGame.Hint.HintCategory.USER, RankGuessGame.Hint.HintStrength.SPECIAL - )); + ), 0.5); } if (user.getLocation() != null && !user.getLocation().isBlank()) { - hints.add(new RankGuessGame.Hint( + hintRandom.add(new RankGuessGame.Hint( "该玩家填写的位置为 `%s`".formatted(user.getLocation()), "玩家自述位置", RankGuessGame.Hint.HintCategory.USER, RankGuessGame.Hint.HintStrength.SPECIAL - )); + ), 0.5); } if (user.getOccupation() != null && !user.getOccupation().isBlank()) { - hints.add(new RankGuessGame.Hint( + hintRandom.add(new RankGuessGame.Hint( "该玩家填写的职业为 `%s`".formatted(user.getOccupation()), "玩家自述职业", RankGuessGame.Hint.HintCategory.USER, RankGuessGame.Hint.HintStrength.SPECIAL - )); + ), 0.5); } - final List features = new ArrayList<>(RankGuessGameService.getUsernameFeature(user.getUsername())); + final var features = new ArrayList<>(RankGuessGameService.getUsernameFeature(user.getUsername())); if (!features.isEmpty()) { Collections.shuffle(features); for (int i = 0; i < Math.min(features.size(), 4); i++) { - final String s = features.get(i); - hints.add(new RankGuessGame.Hint( - "该玩家用户名" + s, + final var s = features.get(i); + hintRandom.add(new RankGuessGame.Hint( + "该玩家用户名" + s.getKey(), "用户名特征", RankGuessGame.Hint.HintCategory.USER, RankGuessGame.Hint.HintStrength.SPECIAL - )); + ), s.getValue()); } } - return hints; + if (avatarUrl != null) { + try { + final BufferedImage original = ImageUtil.readImage(avatarUrl); + + final BufferedImage mosaic = ImageUtil.mosaic(original, 15); + final BufferedImage blur = ImageUtil.gaussianBlur(original, 50); + + final byte[] mosaicBytes = ImageUtil.toPngBytes(mosaic); + final byte[] blurBytes = ImageUtil.toPngBytes(blur); + + final var mosaicImage = imageUploader.apply(mosaicBytes); + final var blurImage = imageUploader.apply(blurBytes); + + hintRandom.add(new RankGuessGame.Hint( + "该玩家头像: " + mosaicImage.toMarkdown(25, 25), + "玩家头像", + RankGuessGame.Hint.HintCategory.USER, + RankGuessGame.Hint.HintStrength.SPECIAL + ), 1.25); + + hintRandom.add(new RankGuessGame.Hint( + "该玩家头像: " + blurImage.toMarkdown(25, 25), + "玩家头像", + RankGuessGame.Hint.HintCategory.USER, + RankGuessGame.Hint.HintStrength.SPECIAL + ), 1.25); + } catch (Exception ignored) { + // Ignored + } + } + + return List.of(hintRandom.next()); } } diff --git a/src/main/java/xyz/zcraft/seira/util/ImageUtil.java b/src/main/java/xyz/zcraft/seira/util/ImageUtil.java new file mode 100644 index 00000000..ca6ba8ed --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/util/ImageUtil.java @@ -0,0 +1,225 @@ +package xyz.zcraft.seira.util; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.ConvolveOp; +import java.awt.image.Kernel; +import java.io.ByteArrayOutputStream; +import java.net.URI; +import java.util.Arrays; + +import static java.lang.Math.clamp; + +public class ImageUtil { + public static BufferedImage readImage(String url) throws Exception { + return ImageIO.read(URI.create(url).toURL()); + } + + public static BufferedImage crop(BufferedImage image, int x, int y, int width, int height) { + BufferedImage sub = image.getSubimage(x, y, width, height); + + BufferedImage result = new BufferedImage( + width, height, BufferedImage.TYPE_INT_ARGB + ); + + result.getGraphics().drawImage(sub, 0, 0, null); + + return result; + } + + public static BufferedImage gaussianBlur(BufferedImage image, int radius) { + if (radius <= 0) { + return image; + } + + int width = image.getWidth(); + int height = image.getHeight(); + + float[] kernel = createGaussianKernel(radius); + + int[] source = image.getRGB( + 0, 0, + width, height, + null, + 0, width + ); + + int[] temp = new int[source.length]; + int[] result = new int[source.length]; + + blurHorizontal( + source, + temp, + width, + height, + kernel, + radius + ); + + blurVertical( + temp, + result, + width, + height, + kernel, + radius + ); + + BufferedImage output = new BufferedImage( + width, + height, + BufferedImage.TYPE_INT_ARGB + ); + + output.setRGB( + 0, 0, + width, height, + result, + 0, width + ); + + return output; + } + + private static float[] createGaussianKernel(int radius) { + int size = radius * 2 + 1; + float[] kernel = new float[size]; + + // 常见经验值 + double sigma = Math.max(radius / 3.0, 0.1); + + double sum = 0; + + for (int i = -radius; i <= radius; i++) { + double value = Math.exp( + -(i * i) / (2.0 * sigma * sigma) + ); + + kernel[i + radius] = (float) value; + sum += value; + } + + // 归一化 + for (int i = 0; i < size; i++) { + kernel[i] /= (float) sum; + } + + return kernel; + } + + private static void blurHorizontal( + int[] source, + int[] target, + int width, + int height, + float[] kernel, + int radius + ) { + for (int y = 0; y < height; y++) { + int row = y * width; + + for (int x = 0; x < width; x++) { + float a = 0; + float r = 0; + float g = 0; + float b = 0; + + for (int k = -radius; k <= radius; k++) { + int sampleX = clamp(x + k, 0, width - 1); + int argb = source[row + sampleX]; + + float weight = kernel[k + radius]; + + a += ((argb >>> 24) & 0xff) * weight; + r += ((argb >>> 16) & 0xff) * weight; + g += ((argb >>> 8) & 0xff) * weight; + b += (argb & 0xff) * weight; + } + + target[row + x] = + ((clamp(Math.round(a), 0, 255)) << 24) + | ((clamp(Math.round(r), 0, 255)) << 16) + | ((clamp(Math.round(g), 0, 255)) << 8) + | clamp(Math.round(b), 0, 255); + } + } + } + + private static void blurVertical( + int[] source, + int[] target, + int width, + int height, + float[] kernel, + int radius + ) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + float a = 0; + float r = 0; + float g = 0; + float b = 0; + + for (int k = -radius; k <= radius; k++) { + int sampleY = clamp(y + k, 0, height - 1); + int argb = source[sampleY * width + x]; + + float weight = kernel[k + radius]; + + a += ((argb >>> 24) & 0xff) * weight; + r += ((argb >>> 16) & 0xff) * weight; + g += ((argb >>> 8) & 0xff) * weight; + b += (argb & 0xff) * weight; + } + + target[y * width + x] = + ((clamp(Math.round(a), 0, 255)) << 24) + | ((clamp(Math.round(r), 0, 255)) << 16) + | ((clamp(Math.round(g), 0, 255)) << 8) + | clamp(Math.round(b), 0, 255); + } + } + } + + public static BufferedImage mosaic(BufferedImage image, int blockSize) { + int width = image.getWidth(); + int height = image.getHeight(); + + int smallWidth = Math.max(1, width / blockSize); + int smallHeight = Math.max(1, height / blockSize); + + BufferedImage small = new BufferedImage(smallWidth, smallHeight, BufferedImage.TYPE_INT_ARGB); + + Graphics2D g1 = small.createGraphics(); + g1.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + g1.drawImage(image, 0, 0, smallWidth, smallHeight, null); + g1.dispose(); + + BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + + Graphics2D g2 = result.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + g2.drawImage(small, 0, 0, width, height, null); + g2.dispose(); + + return result; + } + + public static void mosaicRegion(BufferedImage image, int x, int y, int width, int height, int blockSize) { + BufferedImage region = image.getSubimage(x, y, width, height); + + BufferedImage processed = mosaic(region, blockSize); + + Graphics2D g = image.createGraphics(); + g.drawImage(processed, x, y, null); + g.dispose(); + } + + public static byte[] toPngBytes(BufferedImage image) throws Exception { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + ImageIO.write(image, "png", out); + return out.toByteArray(); + } + } +} diff --git a/src/main/java/xyz/zcraft/seira/util/TokenManager.java b/src/main/java/xyz/zcraft/seira/util/TokenManager.java index 87fb90bb..aecc987e 100644 --- a/src/main/java/xyz/zcraft/seira/util/TokenManager.java +++ b/src/main/java/xyz/zcraft/seira/util/TokenManager.java @@ -22,6 +22,7 @@ public class TokenManager implements AutoCloseable { private final AtomicBoolean started = new AtomicBoolean(); private final AtomicBoolean closed = new AtomicBoolean(); + @Getter private final String clientId; private final String clientSecret; diff --git a/src/main/java/xyz/zcraft/seira/util/WeightedRandom.java b/src/main/java/xyz/zcraft/seira/util/WeightedRandom.java new file mode 100644 index 00000000..33889aaf --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/util/WeightedRandom.java @@ -0,0 +1,70 @@ +package xyz.zcraft.seira.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.ThreadLocalRandom; + +public class WeightedRandom { + private final List items = new ArrayList<>(); + private final List cumulative = new ArrayList<>(); + + public void add(T item, double weight) { + if (weight <= 0 || Double.isNaN(weight) || Double.isInfinite(weight)) { + throw new IllegalArgumentException("Weight must be positive and finite"); + } + + items.add(item); + + double previous = cumulative.isEmpty() ? 0.0 : cumulative.getLast(); + + cumulative.add(previous + weight); + } + + public T next() { + return items.get(randomIndex()); + } + + public T getAndRemove() { + int index = randomIndex(); + + T result = items.remove(index); + + double previous = index == 0 ? 0.0 : cumulative.get(index - 1); + + double removedWeight = cumulative.get(index) - previous; + + cumulative.remove(index); + + for (int i = index; i < cumulative.size(); i++) { + cumulative.set(i, cumulative.get(i) - removedWeight); + } + + return result; + } + + private int randomIndex() { + if (items.isEmpty()) { + throw new NoSuchElementException("WeightedRandom is empty"); + } + + double total = cumulative.getLast(); + double random = ThreadLocalRandom.current().nextDouble(total); + + for (int i = 0; i < cumulative.size(); i++) { + if (random < cumulative.get(i)) { + return i; + } + } + + throw new IllegalStateException(); + } + + public boolean isEmpty() { + return items.isEmpty(); + } + + public int size() { + return items.size(); + } +} \ No newline at end of file From 2a225b6dba29cc5e99675f053eca51dbcc1b02dc Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Thu, 17 Sep 2026 12:49:01 +0800 Subject: [PATCH 02/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ReplyFactory=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E5=A5=BD=E5=8F=8B=E7=8A=B6=E6=80=81=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=E6=A0=BC=E5=BC=8F=EF=BC=8C=E5=A2=9E=E5=BC=BA=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=91=88=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../seira/command/reply/ReplyFactory.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index d87e1ba8..64afd102 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -823,20 +823,19 @@ public static String friendStatusContent(String selfOpenId, Long selfUid, String } } - return at(selfOpenId) + """ - 你们的好友状态: - > %s: - > ![image #30px #30px](%s) __ | %s__ + return """ + %s 和 %s 的好友状态: + > # ![image #30px #30px](%s) __ | %s__ - %s + # %s - > %s: - > ![image #30px #30px](%s) __ | %s__ + > # ![image #30px #30px](%s) __ | %s__ """.formatted( - at(selfOpenId), selfOsuAvatar, url(selfUsername, "https://osu.ppy.sh/users/" + selfUid), + at(selfOpenId), at(targetOpenId), + selfOsuAvatar, url(selfUsername, "https://osu.ppy.sh/users/" + selfUid), status, - at(targetOpenId), targetOsuAvatar, url(targetUsername, "https://osu.ppy.sh/users/" + targetUid) - ); + targetOsuAvatar, url(targetUsername, "https://osu.ppy.sh/users/" + targetUid) + ).trim(); } public static String missImageContent(Context ctx, String scoreId, Integer index, int size) { From 2e8e49d1604f9774d2c1ead548b0227ff8578b6a Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Thu, 17 Sep 2026 12:49:50 +0800 Subject: [PATCH 03/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ReplyFactory=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E5=A5=BD=E5=8F=8B=E7=8A=B6=E6=80=81=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=E6=A0=BC=E5=BC=8F=EF=BC=8C=E5=A2=9E=E5=BC=BA=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=91=88=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/xyz/zcraft/seira/command/reply/ReplyFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index 64afd102..58d1e75f 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -825,11 +825,11 @@ public static String friendStatusContent(String selfOpenId, Long selfUid, String return """ %s 和 %s 的好友状态: - > # ![image #30px #30px](%s) __ | %s__ + > # ![image #30px #30px](%s) __|__ %s # %s - > # ![image #30px #30px](%s) __ | %s__ + > # ![image #30px #30px](%s) __|__ %s """.formatted( at(selfOpenId), at(targetOpenId), selfOsuAvatar, url(selfUsername, "https://osu.ppy.sh/users/" + selfUid), From 9505cd3af501112b62d3c685805195a08e8beb23 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Thu, 17 Sep 2026 15:21:53 +0800 Subject: [PATCH 04/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20Round=20=E5=92=8C=20?= =?UTF-8?q?SocialCommandHandler=20=E4=B8=AD=E7=9A=84=E5=A4=B4=E5=83=8F?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/handler/SocialCommandHandler.java | 31 +++++++---- .../zcraft/seira/rankguess/data/Round.java | 53 +++++++++++++------ 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java index c9a19ee2..8104d45b 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java @@ -118,12 +118,13 @@ public void handleFriendStatus(Context ctx) { users.add(selfUser); - selfFollowed = selfFollowedList.stream().anyMatch(e -> e.user().getId() == targetId); + selfFollowed = selfFollowedList.stream() + .anyMatch(e -> e.user().getId() == targetId); - selfFollowedList.stream().filter(e -> e.user().getId() == targetId).findFirst().ifPresentOrElse( - e -> targetFollowed.set(e.mutual()), () -> { - } - ); + selfFollowedList.stream() + .filter(e -> e.user().getId() == targetId) + .findFirst() + .ifPresent(e -> targetFollowed.set(e.mutual())); if (targetFollowed.get() == null) { final List targetFollowedList; @@ -137,13 +138,21 @@ public void handleFriendStatus(Context ctx) { UserDataStore.storeUserInfo(users); - users.stream().filter(u -> u.getId() == selfId).findFirst().ifPresent(u -> { - selfOsuAvatar.set(u.getAvatarUrl()); - }); + users.stream().filter(u -> u.getId() == selfId).findFirst().ifPresent(u -> selfOsuAvatar.set(u.getAvatarUrl())); - users.stream().filter(u -> u.getId() == targetId).findFirst().ifPresent(u -> { - targetOsuAvatar.set(u.getAvatarUrl()); - }); + final var targetUser = users.stream() + .filter(u -> u.getId() == targetId) + .findFirst() + .orElseGet(() -> APIHelper.getUsers(List.of(targetId)) + .stream() + .filter(u -> u.getId() == targetId) + .findFirst() + .orElse(null) + ); + + if (targetUser != null) { + targetOsuAvatar.set(targetUser.getAvatarUrl()); + } ctx.sendReply(replyFactory.friendStatusMessage( ctx.senderUserId(), selfId, selfOsuAvatar.get(), diff --git a/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java b/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java index 2424a38c..160e7161 100644 --- a/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java +++ b/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java @@ -198,7 +198,7 @@ public LinkedList getNormalHints() { return hints; } - public List getGroupHints(String avatarUrl, Function imageUploader) { + public List getGroupHints(String qqAvatarUrl, Function imageUploader) { WeightedRandom hintRandom = new WeightedRandom<>(); final UserExtended user = this.randomScore.user(); @@ -235,7 +235,7 @@ public List getGroupHints(String avatarUrl, Function getGroupHints(String avatarUrl, Function getGroupHints(String avatarUrl, Function(RankGuessGameService.getUsernameFeature(user.getUsername())); @@ -272,29 +272,26 @@ public List getGroupHints(String avatarUrl, Function getGroupHints(String avatarUrl, Function Date: Thu, 17 Sep 2026 15:22:41 +0800 Subject: [PATCH 05/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ReplyFactory=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E7=B2=89=E4=B8=9D=E6=95=B0=E9=87=8F=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D=E4=B8=8D?= =?UTF-8?q?=E5=87=BA=E7=8E=B0=E8=B4=9F=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index 58d1e75f..79fa8ff7 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -651,7 +651,7 @@ public static String friendContent(Context ctx, sb.append("\n> 仅粉丝← ("); sb.append(onlyFollower.size()).append(" 已知"); - if (all) sb.append(" 共 ").append(self.getFollowerCount() - allMutualCount); + if (all) sb.append(" 共 ").append(Math.max(self.getFollowerCount() - allMutualCount, 0)); sb.append(")\n>"); collapsed |= appendFriends(ctx, onlyFollower, sb); From 405f01af311d6caee5f240ee105798329affc789 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Thu, 17 Sep 2026 15:29:15 +0800 Subject: [PATCH 06/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20SocialCommandHandler?= =?UTF-8?q?=20=E4=B8=AD=E7=9A=84=E7=94=A8=E6=88=B7=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E7=8A=B6=E6=80=81=E4=BF=A1=E6=81=AF=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/handler/SocialCommandHandler.java | 40 +++++++------------ .../seira/command/reply/ReplyFactory.java | 2 +- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java index 8104d45b..42fa68bf 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java @@ -95,14 +95,15 @@ public void handleFriendStatus(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "用法:/mu @someone\n> 注: 读取@需要开启权限。")); } - final String s = resolver.extractMentionedUserId(ctx.argument(0)); - final Long targetId = resolver.resolveBoundUid(s); + final UserRefResolution targetRef = resolver.resolveUserRefArgument(ctx.argument(0)); - if (targetId == null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "对方还未绑定喵")); + if (targetRef.errorMessage() != null) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + targetRef.errorMessage())); return; } + final UserExtended targetUser = APIHelper.getUserRaw(targetRef.userRef()); + boolean selfFollowed; final AtomicReference targetFollowed = new AtomicReference<>(); @@ -117,18 +118,21 @@ public void handleFriendStatus(Context ctx) { final Set users = new HashSet<>(selfFollowedList.stream().map(FriendEntry::user).toList()); users.add(selfUser); + users.add(targetUser); selfFollowed = selfFollowedList.stream() - .anyMatch(e -> e.user().getId() == targetId); + .anyMatch(e -> e.user().getId() == targetUser.getId()); selfFollowedList.stream() - .filter(e -> e.user().getId() == targetId) + .filter(e -> e.user().getId() == targetUser.getId()) .findFirst() .ifPresent(e -> targetFollowed.set(e.mutual())); - if (targetFollowed.get() == null) { + final var targetOpenId = UserDataStore.findGroupOpenIdByUid(ctx.groupId(), targetUser.getId()).orElse(null); + + if (targetFollowed.get() == null && targetOpenId != null) { final List targetFollowedList; - final OsuToken target = authHelper.updateTokenAndGet(s); + final OsuToken target = authHelper.updateTokenAndGet(targetOpenId); if (target != null) { targetFollowedList = APIHelper.getFollowed(target.accessToken()).getContent(); targetFollowed.set(targetFollowedList.stream().anyMatch(e -> e.user().getId() == selfId)); @@ -140,28 +144,14 @@ public void handleFriendStatus(Context ctx) { users.stream().filter(u -> u.getId() == selfId).findFirst().ifPresent(u -> selfOsuAvatar.set(u.getAvatarUrl())); - final var targetUser = users.stream() - .filter(u -> u.getId() == targetId) - .findFirst() - .orElseGet(() -> APIHelper.getUsers(List.of(targetId)) - .stream() - .filter(u -> u.getId() == targetId) - .findFirst() - .orElse(null) - ); - - if (targetUser != null) { - targetOsuAvatar.set(targetUser.getAvatarUrl()); - } - ctx.sendReply(replyFactory.friendStatusMessage( ctx.senderUserId(), selfId, selfOsuAvatar.get(), UserDataStore.findUsername(selfId).orElse("未知"), avatarProvider.apply(ctx.senderUserId()), - s, targetId, targetOsuAvatar.get(), - UserDataStore.findUsername(targetId).orElse("未知"), - avatarProvider.apply(s), + targetOpenId, targetUser.getId(), targetOsuAvatar.get(), + targetUser.getUsername(), + avatarProvider.apply(targetOpenId), selfFollowed, targetFollowed.get() ) diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index 79fa8ff7..9717a2da 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -831,7 +831,7 @@ public static String friendStatusContent(String selfOpenId, Long selfUid, String > # ![image #30px #30px](%s) __|__ %s """.formatted( - at(selfOpenId), at(targetOpenId), + at(selfOpenId), targetAvatar == null ? targetUsername : at(targetOpenId), selfOsuAvatar, url(selfUsername, "https://osu.ppy.sh/users/" + selfUid), status, targetOsuAvatar, url(targetUsername, "https://osu.ppy.sh/users/" + targetUid) From 390aca8fdcca5a30d41cc2bde3c54512c4247c1c Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Thu, 17 Sep 2026 15:32:52 +0800 Subject: [PATCH 07/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20SocialCommandHandler?= =?UTF-8?q?=20=E4=B8=AD=E7=9A=84=E5=A4=B4=E5=83=8F=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=B9=B6=E5=A2=9E=E5=BC=BA=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../seira/command/handler/SocialCommandHandler.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java index 42fa68bf..a0f3f2c2 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java @@ -103,15 +103,14 @@ public void handleFriendStatus(Context ctx) { } final UserExtended targetUser = APIHelper.getUserRaw(targetRef.userRef()); + final String targetOsuAvatar = targetUser.getAvatarUrl(); boolean selfFollowed; final AtomicReference targetFollowed = new AtomicReference<>(); - AtomicReference selfOsuAvatar = new AtomicReference<>("https://osu.ppy.sh/images/layout/avatar-guest.png"); - AtomicReference targetOsuAvatar = new AtomicReference<>("https://osu.ppy.sh/images/layout/avatar-guest.png"); - final OsuToken selfToken = authHelper.updateTokenAndGet(ctx.senderUserId()); final var selfUser = APIHelper.getSelf(selfToken.accessToken()).getContent(); + final String selfOsuAvatar = selfUser.getAvatarUrl(); final List selfFollowedList = APIHelper.getFollowed(selfToken.accessToken()).getContent(); updateFriends(selfId, selfFollowedList); @@ -142,14 +141,12 @@ public void handleFriendStatus(Context ctx) { UserDataStore.storeUserInfo(users); - users.stream().filter(u -> u.getId() == selfId).findFirst().ifPresent(u -> selfOsuAvatar.set(u.getAvatarUrl())); - ctx.sendReply(replyFactory.friendStatusMessage( - ctx.senderUserId(), selfId, selfOsuAvatar.get(), + ctx.senderUserId(), selfId, selfOsuAvatar, UserDataStore.findUsername(selfId).orElse("未知"), avatarProvider.apply(ctx.senderUserId()), - targetOpenId, targetUser.getId(), targetOsuAvatar.get(), + targetOpenId, targetUser.getId(), targetOsuAvatar, targetUser.getUsername(), avatarProvider.apply(targetOpenId), From 2082ac7fd6cf6f76bcf938f12da6a5dbb8a1265b Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Thu, 17 Sep 2026 15:34:47 +0800 Subject: [PATCH 08/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20SocialCommandHandler?= =?UTF-8?q?=20=E4=B8=AD=E7=9A=84=E7=94=A8=E6=88=B7=E8=BA=AB=E4=BB=BD?= =?UTF-8?q?=E5=88=A4=E6=96=AD=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BA=A4=E4=BA=92=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zcraft/seira/command/handler/SocialCommandHandler.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java index a0f3f2c2..292c9a46 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java @@ -112,6 +112,11 @@ public void handleFriendStatus(Context ctx) { final var selfUser = APIHelper.getSelf(selfToken.accessToken()).getContent(); final String selfOsuAvatar = selfUser.getAvatarUrl(); + if (targetUser.getId() == selfUser.getId()) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + " 和 `" + targetUser.getUsername() + "` 是一个人喵。")); + return; + } + final List selfFollowedList = APIHelper.getFollowed(selfToken.accessToken()).getContent(); updateFriends(selfId, selfFollowedList); final Set users = new HashSet<>(selfFollowedList.stream().map(FriendEntry::user).toList()); From 52017b70feb42c756a02f030e2624b66b6308233 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Fri, 18 Sep 2026 17:22:28 +0800 Subject: [PATCH 09/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20BeatmapPreview=20?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=88=9B=E5=BB=BA=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=97=B6=E9=97=B4=E8=8C=83=E5=9B=B4=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=B9=B6=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../java/xyz/zcraft/seira/api/APIHelper.java | 10 +++++-- .../seira/api/data/VideoRenderRecord.java | 7 +++++ .../xyz/zcraft/seira/bot/MessageSender.java | 2 +- .../xyz/zcraft/seira/bot/data/Button.java | 29 ++++++++++++++++--- .../xyz/zcraft/seira/bot/data/Message.java | 11 ++++++- .../zcraft/seira/command/TargetHistory.java | 20 ++++++------- .../zcraft/seira/command/TaskCoordinator.java | 4 +-- .../handler/BeatmapCommandHandler.java | 25 +++++++++++++--- .../command/handler/ReplayCommandHandler.java | 22 ++++++++++---- .../command/handler/ScoreCommandHandler.java | 2 +- .../command/handler/SocialCommandHandler.java | 14 +++++++-- .../seira/command/parse/TargetResolution.java | 22 ++++++++++++-- .../seira/command/reply/ReplyFactory.java | 4 ++- .../zcraft/seira/command/route/Router.java | 2 +- 15 files changed, 138 insertions(+), 38 deletions(-) diff --git a/pom.xml b/pom.xml index 0906c1a9..828c4282 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.1 + 1.12.2 25 diff --git a/src/main/java/xyz/zcraft/seira/api/APIHelper.java b/src/main/java/xyz/zcraft/seira/api/APIHelper.java index 828b2f6f..b6089440 100644 --- a/src/main/java/xyz/zcraft/seira/api/APIHelper.java +++ b/src/main/java/xyz/zcraft/seira/api/APIHelper.java @@ -665,7 +665,7 @@ public static RandomScore getRandomScoreFromUsers(List uids, JsonObject we } - public static ReplayTaskInfo createBeatmapPreviewTask(long beatmapId, String mods, + public static ReplayTaskInfo createBeatmapPreviewTask(long beatmapId, String mods, TimeDurationParser.TimeRange range, QqUploadRequest qqUpload) { JsonObject body = new JsonObject(); if (mods != null && !mods.isBlank()) { @@ -675,8 +675,14 @@ public static ReplayTaskInfo createBeatmapPreviewTask(long beatmapId, String mod body.add("qqUpload", GSON.toJsonTree(qqUpload)); } + String rangeQuery = "?"; + + if (range != null) { + rangeQuery += range.toQueryString(); + } + HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(ENDPOINT + "/replays/renders/preview/" + beatmapId)) + .uri(URI.create(ENDPOINT + "/replays/renders/preview/" + beatmapId + rangeQuery)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body.toString())) .build(); diff --git a/src/main/java/xyz/zcraft/seira/api/data/VideoRenderRecord.java b/src/main/java/xyz/zcraft/seira/api/data/VideoRenderRecord.java index 59c51ad4..0d3bb7bd 100644 --- a/src/main/java/xyz/zcraft/seira/api/data/VideoRenderRecord.java +++ b/src/main/java/xyz/zcraft/seira/api/data/VideoRenderRecord.java @@ -4,9 +4,11 @@ public class VideoRenderRecord { private final ConcurrentHashMap renderRecord = new ConcurrentHashMap<>(); + private final ConcurrentHashMap taskOwner = new ConcurrentHashMap<>(); public void updateRenderTask(String uid, String jobId) { renderRecord.put(uid, jobId); + taskOwner.put(jobId, uid); } public boolean hasRenderTask(String uid) { @@ -17,6 +19,10 @@ public String getRenderTask(String uid) { return renderRecord.get(uid); } + public String getTaskOwner(String jobId) { + return taskOwner.get(jobId); + } + @SuppressWarnings("unused") public void removeRenderTask(String uid) { renderRecord.remove(uid); @@ -24,5 +30,6 @@ public void removeRenderTask(String uid) { public void removeRenderTask(String uid, String jobId) { renderRecord.remove(uid, jobId); + taskOwner.remove(jobId, uid); } } diff --git a/src/main/java/xyz/zcraft/seira/bot/MessageSender.java b/src/main/java/xyz/zcraft/seira/bot/MessageSender.java index 11f491a3..ad38f3ab 100644 --- a/src/main/java/xyz/zcraft/seira/bot/MessageSender.java +++ b/src/main/java/xyz/zcraft/seira/bot/MessageSender.java @@ -149,7 +149,7 @@ public SentMessage sendGroupText(String groupId, String content) { public SentMessage sendGroupMarkdown(String groupId, String content) { Message message = new Message(); message.setMsgType(PendingMessage.MSG_TYPE_MARKDOWN); - message.setMarkdown(new Gson().toJsonTree(Map.of("content", content)).getAsJsonObject()); + message.setMarkdown(Message.MessageMarkdown.of(content)); return sendGroupMessage(groupId, message); } diff --git a/src/main/java/xyz/zcraft/seira/bot/data/Button.java b/src/main/java/xyz/zcraft/seira/bot/data/Button.java index a0f75ea3..e9545b0d 100644 --- a/src/main/java/xyz/zcraft/seira/bot/data/Button.java +++ b/src/main/java/xyz/zcraft/seira/bot/data/Button.java @@ -97,6 +97,16 @@ public Button permit(String userId) { return this; } + public Button modal(String content) { + if (content != null && !content.isBlank()) { + if (this.getAction() != null) { + this.getAction().setModal(Action.Modal.of(content)); + } + } + + return this; + } + public Button disable() { this.renderData.setStyle(0); @@ -114,7 +124,7 @@ public Button disable() { @Data @NoArgsConstructor @AllArgsConstructor - private static class RenderData { + public static class RenderData { private String label; @SerializedName("visited_label") private String visitedLabel; @@ -124,20 +134,31 @@ private static class RenderData { @Data @NoArgsConstructor @AllArgsConstructor - private static class Action { + public static class Action { private int type; private Permission permission; private String data; private boolean enter; private int anchor; @SerializedName("unsupport_tips") - private String unsupportTips; + private String unsupportedTips; + private Modal modal; @Data - private static class Permission { + public static class Permission { private int type; @SerializedName("specify_user_ids") private List specifyUserIds; } + + public record Modal( + String content, + @SerializedName("confirm_text") String confirmText, + @SerializedName("cancel_text") String cancelText + ) { + public static Modal of(String content) { + return new Modal(content, null, null); + } + } } } diff --git a/src/main/java/xyz/zcraft/seira/bot/data/Message.java b/src/main/java/xyz/zcraft/seira/bot/data/Message.java index 53c2c0dd..c25b59cd 100644 --- a/src/main/java/xyz/zcraft/seira/bot/data/Message.java +++ b/src/main/java/xyz/zcraft/seira/bot/data/Message.java @@ -17,7 +17,7 @@ public class Message { @SerializedName("msg_type") private int msgType; - private JsonObject markdown; + private MessageMarkdown markdown; private JsonObject keyboard; @@ -39,4 +39,13 @@ public class Message { @SerializedName("message_reference") private MessageReference messageReference; + + public record MessageMarkdown( + String content, + @SerializedName("force_verify_image_resource") Boolean forceVerifyImageResource + ) { + public static MessageMarkdown of(String content) { + return new MessageMarkdown(content, null); + } + } } diff --git a/src/main/java/xyz/zcraft/seira/command/TargetHistory.java b/src/main/java/xyz/zcraft/seira/command/TargetHistory.java index eff75bab..d8160fa3 100644 --- a/src/main/java/xyz/zcraft/seira/command/TargetHistory.java +++ b/src/main/java/xyz/zcraft/seira/command/TargetHistory.java @@ -74,13 +74,13 @@ public Ids resolve(Context ctx, Type type, TargetResolution args) { * 查找只修改本次结果;调用者显式 remember 后才更新历史。 */ public Ids resolve(Context ctx, Type type, TargetResolution args, List filters, String mod) { - ShortcutTarget target = args.target(); + ShortcutTarget target = args.getTarget(); if (target != null && target.isError()) throw new ResolutionException(target.errorMessage()); Ids previous = users.get(ctx.senderUserId()); // 省略目标时沿用三个 ID;显式输入目标时从空记忆开始。 Ids ids = target == null ? new Ids(previous) : new Ids(); - UserRef player = args.userOverride() != null ? args.userOverride() + UserRef player = args.getUserOverride() != null ? args.getUserOverride() : target == null ? null : target.userRef(); boolean selectedPlayerScore = false; @@ -132,7 +132,7 @@ public Ids resolve(Context ctx, Type type, TargetResolution args, List f // 指定用户或 Mods 时,按同一谱面重新查成绩,不能直接沿用旧成绩 ID。 // /s rs2 @用户 已经选好了该用户的 rs2,不再改查谱面最佳成绩。 - if (type == Type.SCORE && (args.userOverride() != null || mod != null) + if (type == Type.SCORE && (args.getUserOverride() != null || mod != null) && ids.scoreId != null && !selectedPlayerScore) { if (ids.beatmapId == null) { ids.beatmapId = APIHelper.getScoreBeatmapId(ids.scoreId); @@ -180,7 +180,7 @@ private UserRef requirePlayer(Context ctx, UserRef player) { * 同屏回放需要保留本地成绩本身,以便把它加入回放列表。 */ public boolean isLocalScore(Context ctx, TargetResolution args) { - if (args.target() != null) return args.target().isLocalScore(); + if (args.getTarget() != null) return args.getTarget().isLocalScore(); Ids ids = users.get(ctx.senderUserId()); return ids != null && ids.scoreId != null && isLocalId(ids.scoreId); } @@ -190,7 +190,7 @@ public TargetResolution parseArguments(Context ctx, String usage, int maxOptions } /** - * optional 判断首个参数是否是省略目标后的选项;返回的 consumedArgs 标记选项起点。 + * optional 判断首个参数是否是省略目标后的选项;返回的 getConsumedArgs 标记选项起点。 */ public TargetResolution parseArguments(Context ctx, String usage, int maxOptions, Predicate optional) { try { @@ -200,9 +200,9 @@ public TargetResolution parseArguments(Context ctx, String usage, int maxOptions args = new TargetResolution(null, 0); } else { args = resolver.resolveTargetWithOptionalMention(ctx.args(), ctx.senderUserId()); - if (args.target().isError()) throw new ResolutionException(args.target().errorMessage()); + if (args.getTarget().isError()) throw new ResolutionException(args.getTarget().errorMessage()); } - if (ctx.argumentCount() - args.consumedArgs() > maxOptions) throw new ResolutionException(usage); + if (ctx.argumentCount() - args.getConsumedArgs() > maxOptions) throw new ResolutionException(usage); return args; } catch (ResolutionException e) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + e.getMessage())); @@ -230,11 +230,11 @@ public TargetResolution parseScoreArguments(Context ctx, String usage, int maxOp if (args == null) return null; String next = args.nextArgument(ctx); if (next != null && !optional.test(next)) { - args = new TargetResolution(args.target(), args.consumedArgs() + 1, parsePlayer(next, usage)); + args = new TargetResolution(args.getTarget(), args.getConsumedArgs(), parsePlayer(next, usage)); } } - if (ctx.argumentCount() - args.consumedArgs() > maxOptions) throw new ResolutionException(usage); - for (int i = args.consumedArgs(); i < ctx.argumentCount(); i++) { + if (ctx.argumentCount() - args.getConsumedArgs() > maxOptions) throw new ResolutionException(usage); + for (int i = args.getConsumedArgs(); i < ctx.argumentCount(); i++) { if (!optional.test(ctx.argument(i))) throw new ResolutionException(usage); } return args; diff --git a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java index fe6fb070..c1a8c46c 100644 --- a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java +++ b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java @@ -164,12 +164,12 @@ public SendResult sendOutboundMessage(String targetId, String messageId, boolean if (pendingMsg instanceof MDMessage md) { message.setMsgType(PendingMessage.MSG_TYPE_MARKDOWN); - message.setMarkdown(new Gson().toJsonTree(Map.of("content", md.getMarkdown())).getAsJsonObject()); + message.setMarkdown(Message.MessageMarkdown.of(md.getMarkdown())); if (md.hasKeyboard()) { message.setKeyboard(md.getKeyboard()); } } else if (pendingMsg.getMsgType() == PendingMessage.MSG_TYPE_MARKDOWN) { - message.setMarkdown(new Gson().toJsonTree(Map.of("content", pendingMsg.getContent())).getAsJsonObject()); + message.setMarkdown(Message.MessageMarkdown.of(pendingMsg.getContent())); } else { message.setContent(pendingMsg.getContent()); } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java index 25251e58..aaf3858a 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java @@ -14,6 +14,7 @@ import xyz.zcraft.seira.command.reply.CommandUsage; import xyz.zcraft.seira.command.reply.ReplyFactory; import xyz.zcraft.seira.data.SendResult; +import xyz.zcraft.seira.util.TimeDurationParser; import java.util.List; import java.util.function.Function; @@ -56,7 +57,7 @@ public void handleDaily(Context ctx) { public void handleM(Context ctx) { var target = history.parseArguments(ctx, CommandUsage.M, 1); if (target == null) return; - try (var timing = taskCoordinator.beginRequest(ctx, "Beatmap")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Beatmap")) { var ids = history.resolve(ctx, BEATMAP, target); history.remember(ctx, ids); var response = APIHelper.getBeatmapResponse(ids.beatmapId(), target.nextArgument(ctx)); @@ -87,14 +88,30 @@ public void handleAp(Context ctx) { } public void handleBpv(Context ctx) { - var target = history.parseArguments(ctx, CommandUsage.BPV, 1, arg -> arg.startsWith("+")); + var target = history.parseArguments( + ctx, CommandUsage.BPV, 2, + arg -> arg.startsWith("+") || TimeDurationParser.isTimeRange(arg) + ); + if (target == null) return; - try (var timing = taskCoordinator.beginRequest(ctx, "Beatmap Preview Render")) { + + TimeDurationParser.TimeRange range = null; + + if (ctx.args().length > target.getConsumedArgs()) { + try { + range = TimeDurationParser.parseRange(ctx.args()[target.getConsumedArgs()]); + } catch (IllegalArgumentException e) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "无法解析时间范围")); + return; + } + } + + try (var _ = taskCoordinator.beginRequest(ctx, "Beatmap Preview Render")) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "正在获取谱面以及回放文件,请稍作等待喵...")); var qqUpload = taskCoordinator.createVideoUploadRequest(ctx); var ids = history.resolve(ctx, BEATMAP, target); history.remember(ctx, ids); - var task = APIHelper.createBeatmapPreviewTask(ids.beatmapId(), target.nextArgument(ctx), qqUpload); + var task = APIHelper.createBeatmapPreviewTask(ids.beatmapId(), target.nextArgument(ctx), range, qqUpload); videoRenderRecord.updateRenderTask(ctx.senderUserId(), task.taskId()); ctx.sendReply(replyFactory.replayMessage(ctx, task)); diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java index ef781a40..07026f44 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java @@ -17,6 +17,7 @@ import java.util.Objects; import java.util.UUID; +import java.util.function.Predicate; import static xyz.zcraft.seira.command.TargetHistory.Type.BEATMAP; import static xyz.zcraft.seira.command.TargetHistory.Type.SCORE; @@ -29,6 +30,7 @@ public final class ReplayCommandHandler { private final ReplyFactory replyFactory; private final VideoRenderRecord videoRenderRecord; private final ReplayResultStore replayResults; + private final Predicate adminAuthorizer; public ReplayCommandHandler( Resolver resolver, @@ -36,7 +38,8 @@ public ReplayCommandHandler( TaskCoordinator taskCoordinator, ReplyFactory replyFactory, VideoRenderRecord videoRenderRecord, - ReplayResultStore replayResults + ReplayResultStore replayResults, + Predicate adminAuthorizer ) { this.resolver = resolver; this.history = history; @@ -44,6 +47,7 @@ public ReplayCommandHandler( this.replyFactory = replyFactory; this.videoRenderRecord = videoRenderRecord; this.replayResults = replayResults; + this.adminAuthorizer = adminAuthorizer; } public void handleR(Context ctx) { @@ -52,9 +56,9 @@ public void handleR(Context ctx) { TimeDurationParser.TimeRange range = null; - if (ctx.args().length > target.consumedArgs()) { + if (ctx.args().length > target.getConsumedArgs()) { try { - range = TimeDurationParser.parseRange(ctx.args()[target.consumedArgs()]); + range = TimeDurationParser.parseRange(ctx.args()[target.getConsumedArgs()]); } catch (IllegalArgumentException e) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "无法解析时间范围")); return; @@ -104,7 +108,7 @@ public void handleRsc(Context ctx) { String extraUidArg = null; - int i = target.consumedArgs(); + int i = target.getConsumedArgs(); if (i < ctx.args().length) { if (ctx.args()[i].startsWith("+") || ctx.args()[i].startsWith("=")) { @@ -214,6 +218,15 @@ public void handleRcancel(Context ctx) { return; } + final String owner = videoRenderRecord.getTaskOwner(jobId); + if (owner != null) { + if (!owner.equalsIgnoreCase(ctx.senderUserId()) + && !adminAuthorizer.test(ctx.senderUserId())) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "你无权取消此任务喵。")); + return; + } + } + var result = APIHelper.cancelReplayRender(jobId); String status = Objects.toString(result.getStatus(), "unknown").toLowerCase(); String message = switch (status) { @@ -229,5 +242,4 @@ public void handleRcancel(Context ctx) { } ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + message)); } - } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java index 866b9255..e15826ef 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java @@ -309,7 +309,7 @@ public void handleMa(Context ctx) { if (target == null) return; String indexArgument = target.nextArgument(ctx); if (indexArgument != null) { - Integer index = parseMissIndex(indexArgument, target.consumedArgs() == 0); + Integer index = parseMissIndex(indexArgument, target.getConsumedArgs() == 1); if (index == null) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.MA)); return; diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java index 292c9a46..e828dcb3 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java @@ -102,6 +102,14 @@ public void handleFriendStatus(Context ctx) { return; } + if (targetRef.userRef() instanceof UserRef.ByUid byUid) { + final long uid = byUid.getUid(); + final String at = UserDataStore.findGroupOpenIdByUid(ctx.groupId(), uid) + .map(ReplyFactory::at) + .orElse(""); + ctx.sendReply(PendingMessage.ofMarkdownRaw(at) + ": [%d](%s)".formatted(uid, "https://osu.ppy.sh/users/" + uid)); + } + final UserExtended targetUser = APIHelper.getUserRaw(targetRef.userRef()); final String targetOsuAvatar = targetUser.getAvatarUrl(); @@ -288,13 +296,13 @@ public void handleLb(Context ctx) { } } else if (ctx.args().length == 1 || ctx.args().length == 2) { TargetResolution targetResolution = resolver.resolveTargetWithOptionalMention(ctx.args(), ctx.senderUserId()); - ShortcutTarget target = targetResolution.target(); + ShortcutTarget target = targetResolution.getTarget(); if (target.isError()) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + target.errorMessage())); return; } - int remainingArgs = ctx.args().length - targetResolution.consumedArgs(); + int remainingArgs = ctx.args().length - targetResolution.getConsumedArgs(); if (remainingArgs == 0) { if (ctx.groupId() != null && !ctx.groupId().isBlank()) { List groupBoundUids = UserDataStore.findBoundUidsByGroup(ctx.groupId()); @@ -328,7 +336,7 @@ public void handleLb(Context ctx) { return; } - String[] uidTokens = ctx.args()[targetResolution.consumedArgs()].split(","); + String[] uidTokens = ctx.args()[targetResolution.getConsumedArgs()].split(","); if (uidTokens.length == 0) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "玩家ID列表不能为空。用法:/lb <谱面ID或快捷查询> [玩家ID列表(逗号分隔)]")); return; diff --git a/src/main/java/xyz/zcraft/seira/command/parse/TargetResolution.java b/src/main/java/xyz/zcraft/seira/command/parse/TargetResolution.java index 39c58d5d..80adae9e 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/TargetResolution.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/TargetResolution.java @@ -1,17 +1,35 @@ package xyz.zcraft.seira.command.parse; +import lombok.Getter; import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.data.UserRef; /** * target 为 null 表示省略目标;consumedArgs 之后是指令自己的可选参数。 */ -public record TargetResolution(ShortcutTarget target, int consumedArgs, UserRef userOverride) { +@Getter +public final class TargetResolution { + private final ShortcutTarget target; + private final UserRef userOverride; + private int consumedArgs; + + public TargetResolution(ShortcutTarget target, int consumedArgs, UserRef userOverride) { + this.target = target; + this.consumedArgs = consumedArgs; + this.userOverride = userOverride; + } + public TargetResolution(ShortcutTarget target, int consumedArgs) { this(target, consumedArgs, null); } public String nextArgument(Context ctx) { - return ctx.argumentCount() > consumedArgs ? ctx.argument(consumedArgs) : null; + final String s = ctx.argumentCount() > consumedArgs ? ctx.argument(consumedArgs) : null; + consumedArgs++; + return s; + } + + public boolean hasRemaining(Context ctx) { + return ctx.argumentCount() > consumedArgs; } } diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index 9717a2da..0b0a48f7 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -1057,7 +1057,9 @@ List> replayProgressButtons(String jobId, boolean cancelable, Strin return Button.keyboard(cancelable ? Button.row( Button.command(1, "查询渲染进度", "/rstat " + jobId), - Button.command(2, "取消渲染", "/rcancel " + jobId).permit(userId) + Button.command(2, "取消渲染", "/rcancel " + jobId) + .permit(userId) + .modal("确定要取消渲染吗") ) : Button.row(Button.command(1, "查询渲染进度", "/rstat " + jobId))); } diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index 5f50123f..1e4c0242 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -79,7 +79,7 @@ public Router( resolver, authHelper, taskCoordinator, replyFactory, this::getAccessTokenFor, this::getAvatar ); ReplayCommandHandler replayCommands = new ReplayCommandHandler( - resolver, history, taskCoordinator, replyFactory, videoRenderRecord, replayResults + resolver, history, taskCoordinator, replyFactory, videoRenderRecord, replayResults, admins::isAdmin ); GeneralCommandHandler generalCommands = new GeneralCommandHandler( messageSender, taskCoordinator, replyFactory, resolver, admins::isAdmin From 4325273408f0b80aac63c73ac827efc5dbf5db6a Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Fri, 18 Sep 2026 18:10:39 +0800 Subject: [PATCH 10/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ConsoleCommandProces?= =?UTF-8?q?sor=EF=BC=8C=E6=96=B0=E5=A2=9E=20group=20=E5=AD=90=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E4=BB=A5=E8=8E=B7=E5=8F=96=E7=BE=A4=E7=BB=84=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=92=8C=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/bot/QQApi.java | 48 +++++++++++++++-- src/main/java/xyz/zcraft/seira/bot/QQBot.java | 49 ++++++++++++++--- .../zcraft/seira/bot/data/GroupBotState.java | 23 ++++++++ .../xyz/zcraft/seira/bot/data/GroupInfo.java | 15 ++++++ .../zcraft/seira/command/parse/Resolver.java | 15 ++++++ .../console/ConsoleCommandProcessor.java | 54 +++++++++++++------ .../seira/console/ConsoleRuntimeControl.java | 5 ++ 7 files changed, 181 insertions(+), 28 deletions(-) create mode 100644 src/main/java/xyz/zcraft/seira/bot/data/GroupBotState.java create mode 100644 src/main/java/xyz/zcraft/seira/bot/data/GroupInfo.java diff --git a/src/main/java/xyz/zcraft/seira/bot/QQApi.java b/src/main/java/xyz/zcraft/seira/bot/QQApi.java index acf3edee..ab3df75f 100644 --- a/src/main/java/xyz/zcraft/seira/bot/QQApi.java +++ b/src/main/java/xyz/zcraft/seira/bot/QQApi.java @@ -644,6 +644,50 @@ public static int editPanel(AccessToken accessToken, String panelId, Panel newPa } } + public static String getAvatarUrl(String appId, String openId) { + return "https://thirdqq.qlogo.cn/qqapp/" + appId + "/" + openId + "/100"; + } + + public static GroupInfo getGroupInfo(AccessToken accessToken, String groupId) { + try { + final var request = newRequestBuilder(accessToken) + .uri(URI.create(ENDPOINT + "/v2/groups/" + groupId + "/info")) + .GET() + .build(); + + final HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + LOG.error("Failed to get group info, status code: {} body={}", response.statusCode(), response.body()); + throw new RuntimeException("Failed to get group info, status code: " + response.statusCode() + " body=" + response.body()); + } + + return GSON.fromJson(response.body(), GroupInfo.class); + } catch (IOException | InterruptedException e) { + throw requestFailure(e); + } + } + + public static GroupBotState getGroupBotState(AccessToken accessToken, String groupId) { + try { + final var request = newRequestBuilder(accessToken) + .uri(URI.create(ENDPOINT + "/v2/groups/" + groupId + "/bot_state")) + .GET() + .build(); + + final HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + LOG.error("Failed to get group bot state, status code: {} body={}", response.statusCode(), response.body()); + throw new RuntimeException("Failed to get group bot state, status code: " + response.statusCode() + " body=" + response.body()); + } + + return GSON.fromJson(response.body(), GroupBotState.class); + } catch (IOException | InterruptedException e) { + throw requestFailure(e); + } + } + private record MediaDigests(String md5, String sha1, String md5First10m) { } @@ -653,8 +697,4 @@ private record UploadPart(int index, String presignedUrl, long blockSize) { private record UploadPrepare(String uploadId, long blockSize, List parts, int concurrency, int retryTimeoutSeconds, int retryDelaySeconds) { } - - public static String getAvatarUrl(String appId, String openId) { - return "https://thirdqq.qlogo.cn/qqapp/" + appId + "/" + openId + "/100"; - } } diff --git a/src/main/java/xyz/zcraft/seira/bot/QQBot.java b/src/main/java/xyz/zcraft/seira/bot/QQBot.java index 1abac239..bead781f 100644 --- a/src/main/java/xyz/zcraft/seira/bot/QQBot.java +++ b/src/main/java/xyz/zcraft/seira/bot/QQBot.java @@ -4,10 +4,7 @@ import lombok.Getter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import xyz.zcraft.seira.bot.data.Panel; -import xyz.zcraft.seira.bot.data.PanelItem; -import xyz.zcraft.seira.bot.data.PanelRecord; -import xyz.zcraft.seira.bot.data.QQUser; +import xyz.zcraft.seira.bot.data.*; import xyz.zcraft.seira.command.AttachmentHandler; import xyz.zcraft.seira.command.route.Router; import xyz.zcraft.seira.config.AppConfig; @@ -37,7 +34,7 @@ public class QQBot implements AutoCloseable, ConsoleRuntimeControl { private static final Logger LOG = LogManager.getLogger(QQBot.class); - + final AtomicReference self = new AtomicReference<>(); @Getter private final TokenManager tokenManager; @Getter @@ -58,7 +55,6 @@ public class QQBot implements AutoCloseable, ConsoleRuntimeControl { private final AtomicBoolean closed = new AtomicBoolean(); private final AtomicReference activeClient = new AtomicReference<>(); private volatile Thread runnerThread; - final AtomicReference self = new AtomicReference<>(); public QQBot( RuntimeConfig runtimeConfig, @@ -101,7 +97,6 @@ public QQBot( ); - LOG.info("Initializing rank guess service"); this.rankGuessGameService = new RankGuessGameService(); this.attachmentHandler = new AttachmentHandler(executors.attachmentDownloads()); @@ -415,4 +410,44 @@ public ConsoleCommandProcessor.ConsoleResult editPanel(String panelId, String js return ConsoleCommandProcessor.ConsoleResult.failure("Error editing panel: " + e.getMessage()); } } + + @Override + public ConsoleCommandProcessor.ConsoleResult getGroupInfo(String groupId) { + try { + final GroupInfo groupInfo = QQApi.getGroupInfo(tokenManager.getToken(), groupId); + + String sb = "=== Group info ===\n" + + "group_id: " + groupId + "\n" + + "group_name: " + groupInfo.groupName() + "\n" + + "group_finger_memo: " + groupInfo.groupFingerMemo() + "\n" + + "group_class_text: " + groupInfo.groupClassText() + "\n" + + "group_tags: " + String.join(", ", groupInfo.groupTags()) + "\n" + + "group_member_num: " + groupInfo.groupMemberNum() + "\n" + + "=================="; + + return ConsoleCommandProcessor.ConsoleResult.success(sb); + } catch (Exception e) { + return ConsoleCommandProcessor.ConsoleResult.failure("Error editing panel: " + e.getMessage()); + } + } + + @Override + public ConsoleCommandProcessor.ConsoleResult getGroupBotState(String groupId) { + try { + final GroupBotState groupBotState = QQApi.getGroupBotState(tokenManager.getToken(), groupId); + + String sb = "=== Group bot state ===\n" + + "group_id: " + groupId + "\n" + + "member_openid: " + groupBotState.memberOpenId() + "\n" + + "joined_at: " + groupBotState.joinedAt() + "\n" + + "allow_proactive_msg: " + groupBotState.allowProactiveMsg() + "\n" + + "recv_msg_setting: " + groupBotState.receiveMsgSetting() + "\n" + + "member_role: " + groupBotState.memberRole() + "\n" + + "======================="; + + return ConsoleCommandProcessor.ConsoleResult.success(sb); + } catch (Exception e) { + return ConsoleCommandProcessor.ConsoleResult.failure("Error getting group bot state: " + e.getMessage()); + } + } } diff --git a/src/main/java/xyz/zcraft/seira/bot/data/GroupBotState.java b/src/main/java/xyz/zcraft/seira/bot/data/GroupBotState.java new file mode 100644 index 00000000..862a2ad3 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/bot/data/GroupBotState.java @@ -0,0 +1,23 @@ +package xyz.zcraft.seira.bot.data; + +import com.google.gson.annotations.SerializedName; + +public record GroupBotState( + @SerializedName("member_openid") String memberOpenId, + @SerializedName("joined_at") String joinedAt, + @SerializedName("allow_proactive_msg") Boolean allowProactiveMsg, + @SerializedName("recv_msg_setting") ReceiveMsgSetting receiveMsgSetting, + @SerializedName("member_role") MemberRole memberRole +) { + public enum ReceiveMsgSetting { + @SerializedName("all") ALL, + @SerializedName("only_mention") ONLY_MENTION, + @SerializedName("mention_and_context") MENTION_AND_CONTEXT, + } + + public enum MemberRole { + @SerializedName("member") MEMBER, + @SerializedName("owner") OWNER, + @SerializedName("admin") ADMIN + } +} diff --git a/src/main/java/xyz/zcraft/seira/bot/data/GroupInfo.java b/src/main/java/xyz/zcraft/seira/bot/data/GroupInfo.java new file mode 100644 index 00000000..b1db711f --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/bot/data/GroupInfo.java @@ -0,0 +1,15 @@ +package xyz.zcraft.seira.bot.data; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public record GroupInfo( + @SerializedName("group_openid") String groupOpenId, + @SerializedName("group_name") String groupName, + @SerializedName("group_finger_memo") String groupFingerMemo, + @SerializedName("group_class_text") String groupClassText, + @SerializedName("group_tags") List groupTags, + @SerializedName("group_member_num") Integer groupMemberNum +) { +} diff --git a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java index 65ce5b6b..9197068d 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java @@ -41,6 +41,18 @@ public String sanitize(String rawContent) { } } + matcher = Patterns.SPACE_MISSING_COMMAND_PATTERN.matcher(rawContent); + if (matcher.find()) { + String command = matcher.group(1).toLowerCase(Locale.ROOT); + String target = matcher.group(2); + String remaining = rawContent.substring(matcher.end()); + if (command.equals("sm")) { + rawContent = "s m" + target + " " + remaining; + } else { + rawContent = command + " " + target + " " + remaining; + } + } + return rawContent; } @@ -315,6 +327,9 @@ private static final class Patterns { private static final Pattern COMPACT_SCORE_COMMAND_PATTERN = Pattern.compile( "(?i)^(rs|rp|bp)(\\d+)(?:-(\\d+))?(?=\\s|$)" ); + private static final Pattern SPACE_MISSING_COMMAND_PATTERN = Pattern.compile( + "^([a-zA-Z]+)(\\d+(?:#\\d+)?)" + ); private static final Pattern SET_MACRO_PATTERN = Pattern.compile("^(\\d+)#(\\d+)$"); private static final Pattern BEATMAP_MACRO_PATTERN = Pattern.compile("^m(\\d+)$"); private static final Pattern LOCAL_SCORE_PATTERN = Pattern.compile("(?i)^loc[1-9]\\d*$"); diff --git a/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java b/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java index d4d2ae1f..fce81843 100644 --- a/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java +++ b/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java @@ -21,6 +21,7 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import java.util.regex.Pattern; public final class ConsoleCommandProcessor { @@ -30,21 +31,26 @@ public final class ConsoleCommandProcessor { private static final Pattern SQL_IDENTIFIER = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*"); private static final List ROOT_COMMANDS = List.of( "help", "status", "metrics", "system", "config", "admin", "data", "send", - "watch", "cache", "gateway", "log", "inspect", "stop", "panel", "notice" - ); - private static final Map> SUBCOMMANDS = Map.of( - "config", List.of("show", "check", "reload"), - "admin", List.of("list", "check", "add", "remove"), - "data", List.of("stats", "tables", "describe", "query"), - "send", List.of("group", "private"), - "watch", List.of("status", "list", "poll", "remove", "clear"), - "cache", List.of("query", "delete", "get", "fetch"), - "gateway", List.of("status", "reconnect"), - "log", List.of("show", "level"), - "panel", List.of("list", "create", "delete", "edit", "get"), - "notice", List.of("new", "reload", "publish", "revoke", "list") + "watch", "cache", "gateway", "log", "inspect", "stop", "panel", "notice", "group" ); + private static final Map> SUBCOMMANDS; + + static { + SUBCOMMANDS = new HashMap<>(); + SUBCOMMANDS.put("config", List.of("show", "check", "reload")); + SUBCOMMANDS.put("admin", List.of("list", "check", "add", "remove")); + SUBCOMMANDS.put("data", List.of("stats", "tables", "describe", "query")); + SUBCOMMANDS.put("send", List.of("group", "private")); + SUBCOMMANDS.put("watch", List.of("status", "list", "poll", "remove", "clear")); + SUBCOMMANDS.put("cache", List.of("query", "delete", "get", "fetch")); + SUBCOMMANDS.put("gateway", List.of("status", "reconnect")); + SUBCOMMANDS.put("log", List.of("show", "level")); + SUBCOMMANDS.put("panel", List.of("list", "create", "delete", "edit", "get")); + SUBCOMMANDS.put("notice", List.of("new", "reload", "publish", "revoke", "list")); + SUBCOMMANDS.put("group", List.of("info", "state")); + } + private final RuntimeConfig runtimeConfig; private final AdminRegistry admins; private final ConsoleDataAccess dataAccess; @@ -93,10 +99,7 @@ private static String formatCacheControl(ConsoleRuntimeControl.CacheControlResul } private static ConsoleResult exact( - ConsoleInputParser.ParsedInput input, - int size, - java.util.function.Supplier action, - String usage + ConsoleInputParser.ParsedInput input, int size, Supplier action, String usage ) { return input.size() == size ? action.get() : ConsoleResult.failure(usage); } @@ -236,6 +239,7 @@ public ConsoleResult execute(String line) { case "watch" -> watch(input); case "cache" -> cache(input); case "panel" -> panel(input); + case "group" -> group(input); case "gateway" -> gateway(input); case "log" -> log(input); case "inspect" -> exact(input, 1, this::inspect, "Usage: inspect"); @@ -442,6 +446,18 @@ private ConsoleResult admin(ConsoleInputParser.ParsedInput input) { }; } + private ConsoleResult group(ConsoleInputParser.ParsedInput input) { + if (input.size() < 2) { + return ConsoleResult.failure("Usage: group [args]"); + } + + return switch (input.value(1).toLowerCase(Locale.ROOT)) { + case "info" -> input.size() == 3 ? runtimeControl.getGroupInfo(input.value(2)) : ConsoleResult.failure("Usage: group info "); + case "state" -> input.size() == 3 ? runtimeControl.getGroupBotState(input.value(2)) : ConsoleResult.failure("Usage: group state "); + default -> ConsoleResult.failure("Usage: group [args]"); + }; + } + private ConsoleResult panel(ConsoleInputParser.ParsedInput input) { if (input.size() < 2) { return ConsoleResult.failure("Usage: panel [args]"); @@ -723,6 +739,10 @@ private ConsoleResult stop(ConsoleInputParser.ParsedInput input) { return ConsoleResult.success("Graceful shutdown requested."); } + public RuntimeConfig getRuntimeConfig() { + return runtimeConfig; + } + public record ConsoleResult(boolean success, String message) { public static ConsoleResult success(String message) { return new ConsoleResult(true, message); diff --git a/src/main/java/xyz/zcraft/seira/console/ConsoleRuntimeControl.java b/src/main/java/xyz/zcraft/seira/console/ConsoleRuntimeControl.java index 379a8e2c..896ff25e 100644 --- a/src/main/java/xyz/zcraft/seira/console/ConsoleRuntimeControl.java +++ b/src/main/java/xyz/zcraft/seira/console/ConsoleRuntimeControl.java @@ -1,5 +1,6 @@ package xyz.zcraft.seira.console; +import xyz.zcraft.seira.bot.data.GroupInfo; import xyz.zcraft.seira.watch.WatchView; import java.time.Duration; @@ -37,6 +38,10 @@ public interface ConsoleRuntimeControl { ConsoleCommandProcessor.ConsoleResult editPanel(String panelId, String jsonPath); + ConsoleCommandProcessor.ConsoleResult getGroupInfo(String groupId); + + ConsoleCommandProcessor.ConsoleResult getGroupBotState(String groupId); + record RuntimeStatus( boolean running, boolean gatewayConnected, From a149cebb775f24343873809de4d83cd24b4040cc Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Fri, 18 Sep 2026 18:21:46 +0800 Subject: [PATCH 11/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ScoreCommandHandler?= =?UTF-8?q?=EF=BC=8C=E9=87=8D=E6=9E=84=20parseScoreListRange=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=B9=B6=E5=A2=9E=E5=BC=BA=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/handler/ScoreCommandHandler.java | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java index e15826ef..c6ca0a43 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java @@ -1,6 +1,7 @@ package xyz.zcraft.seira.command.handler; import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.seira.api.data.MissData; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.command.TargetHistory; @@ -54,6 +55,21 @@ static TbArguments parseTbArguments(String[] args) { return new TbArguments(days, args.length > targetIndex ? args[targetIndex] : null); } + static ScoreListRange parseScoreListRange(String value) { + Matcher matcher = SCORE_LIST_RANGE_PATTERN.matcher(value); + if (!matcher.matches()) return null; + try { + int first = Integer.parseInt(matcher.group(1)); + String endGroup = matcher.group(2); + int start = endGroup == null ? 1 : first; + int end = endGroup == null ? first : Integer.parseInt(endGroup); + if (start <= 0 || start > end || end > MAX_SCORE_LIST_COUNT) return null; + return new ScoreListRange(start, end); + } catch (NumberFormatException ignored) { + return null; + } + } + public void handleBp(Context ctx) { if (ctx.args().length == 0) { ShortcutTarget target = resolver.parseTarget("bp1", ctx.senderUserId()); @@ -249,21 +265,6 @@ private ScoreListRequest parseScoreListRequest(Context ctx, String usage) { return new ScoreListRequest(range, userRef, filters.filters()); } - static ScoreListRange parseScoreListRange(String value) { - Matcher matcher = SCORE_LIST_RANGE_PATTERN.matcher(value); - if (!matcher.matches()) return null; - try { - int first = Integer.parseInt(matcher.group(1)); - String endGroup = matcher.group(2); - int start = endGroup == null ? 1 : first; - int end = endGroup == null ? first : Integer.parseInt(endGroup); - if (start <= 0 || start > end || end > MAX_SCORE_LIST_COUNT) return null; - return new ScoreListRange(start, end); - } catch (NumberFormatException ignored) { - return null; - } - } - public void handleS(Context ctx) { var target = history.parseScoreArguments(ctx, CommandUsage.S, 1, arg -> arg.startsWith("+")); if (target == null) return; @@ -337,7 +338,12 @@ public void handleMa(Context ctx) { history.remember(ctx, ids); String scoreId = ids.scoreId(); var response = APIHelper.getScoreMissesResponse(scoreId); - ctx.sendReply(replyFactory.scoreMissesMessage(ctx, response)); + final List content = response.getContent(); + if (content.size() == 1) { + handleMa(ctx.asCommand("ma", new String[]{scoreId, "#1"}, scoreId + " #1")); + } else { + ctx.sendReply(replyFactory.scoreMissesMessage(ctx, response)); + } } } From bcfedd2f0ca17d2a4837968a141e92d0cc8d785e Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sat, 19 Sep 2026 15:45:45 +0800 Subject: [PATCH 12/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ScoreFilterArguments?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=BC=BA=E5=AD=97=E6=AE=B5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=92=8C=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../command/parse/ScoreFilterArguments.java | 22 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index 828c4282..42b5d8bd 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.2 + 1.12.3 25 diff --git a/src/main/java/xyz/zcraft/seira/command/parse/ScoreFilterArguments.java b/src/main/java/xyz/zcraft/seira/command/parse/ScoreFilterArguments.java index cce66a5a..0dc553c0 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/ScoreFilterArguments.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/ScoreFilterArguments.java @@ -13,7 +13,7 @@ public final class ScoreFilterArguments { private static final Pattern FILTER_PATTERN = Pattern.compile( "(?i)^(acc(?:uracy)?|combo|pp|time|length|len|star|stars|sr|bpm|miss|misses|score|mod|mods|rank|replay" - + "|any|title|artist|mapper|genre|language|video|storyboard|fullcombo|ar|od|cs|hp)" + + "|any|title|artist|mapper|genre|language|tag|source|video|storyboard|fullcombo|ar|od|cs|hp)" + "(>=|<=|!=|!~|>|<|=|~)(.+)$" ); private static final Pattern MISS_SHORTHAND_PATTERN = Pattern.compile("(?i)^(!?)(\\d+)miss(?:es)?$"); @@ -71,7 +71,7 @@ private static String parseOne(String token) { if (!RANKS.contains(value)) { throw new IllegalArgumentException("rank 必须是 SSH/SS/XH/X/SH/S/A/B/C/D/F"); } - } else if (Set.of("any", "title", "artist", "mapper", "genre", "language").contains(field)) { + } else if (Set.of("any", "title", "artist", "mapper", "genre", "language", "tag", "source").contains(field)) { if (!Set.of("~", "!~", "=", "!=").contains(operator)) { throw new IllegalArgumentException(field + " 仅支持 ~、!~、=、!="); } @@ -122,7 +122,7 @@ private static String parseOne(String token) { private static String normalizeField(String value) { return switch (value.toLowerCase(Locale.ROOT)) { case "acc", "accuracy" -> "acc"; - case "combo" -> "combo"; + case "combo", "cmb", "cb" -> "combo"; case "pp" -> "pp"; case "time", "length", "len" -> "time"; case "star", "stars", "sr" -> "star"; @@ -136,15 +136,17 @@ private static String normalizeField(String value) { case "mod", "mods" -> "mod"; case "rank" -> "rank"; case "any" -> "any"; - case "title" -> "title"; - case "artist" -> "artist"; - case "mapper" -> "mapper"; + case "title", "t" -> "title"; + case "artist", "a" -> "artist"; + case "mapper", "m" -> "mapper"; case "genre" -> "genre"; case "language" -> "language"; - case "video" -> "video"; - case "storyboard" -> "storyboard"; - case "fullcombo" -> "fullcombo"; - case "replay" -> "replay"; + case "tag", "tags" -> "tag"; + case "source" -> "source"; + case "video", "vid" -> "video"; + case "storyboard", "sb" -> "storyboard"; + case "fullcombo", "fc" -> "fullcombo"; + case "replay", "rep", "rp" -> "replay"; default -> throw new IllegalArgumentException("未知字段 " + value); }; } From 8d49e8a9bd85630f6568cf46a5097c02d5d64a64 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sat, 19 Sep 2026 15:58:39 +0800 Subject: [PATCH 13/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ScoreFilterArguments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zcraft/seira/command/parse/ScoreFilterArguments.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/parse/ScoreFilterArguments.java b/src/main/java/xyz/zcraft/seira/command/parse/ScoreFilterArguments.java index 0dc553c0..7c54bef0 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/ScoreFilterArguments.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/ScoreFilterArguments.java @@ -13,7 +13,7 @@ public final class ScoreFilterArguments { private static final Pattern FILTER_PATTERN = Pattern.compile( "(?i)^(acc(?:uracy)?|combo|pp|time|length|len|star|stars|sr|bpm|miss|misses|score|mod|mods|rank|replay" - + "|any|title|artist|mapper|genre|language|tag|source|video|storyboard|fullcombo|ar|od|cs|hp)" + + "|any|title|artist|mapper|genre|language|tag|source|nsfw|video|storyboard|fullcombo|ar|od|cs|hp)" + "(>=|<=|!=|!~|>|<|=|~)(.+)$" ); private static final Pattern MISS_SHORTHAND_PATTERN = Pattern.compile("(?i)^(!?)(\\d+)miss(?:es)?$"); @@ -95,7 +95,7 @@ private static String parseOne(String token) { throw new IllegalArgumentException("正则表达式无效:" + e.getDescription()); } } - } else if (Set.of("video", "storyboard", "fullcombo", "replay").contains(field)) { + } else if (Set.of("nsfw", "video", "storyboard", "fullcombo", "replay").contains(field)) { if (!Set.of("=", "!=").contains(operator)) { throw new IllegalArgumentException(field + " 仅支持 =、!="); } @@ -143,6 +143,7 @@ private static String normalizeField(String value) { case "language" -> "language"; case "tag", "tags" -> "tag"; case "source" -> "source"; + case "nsfw" -> "nsfw"; case "video", "vid" -> "video"; case "storyboard", "sb" -> "storyboard"; case "fullcombo", "fc" -> "fullcombo"; @@ -162,6 +163,8 @@ private static String expandShorthand(String value) { case "!video" -> "video=false"; case "sb", "storyboard" -> "storyboard=true"; case "!sb", "!storyboard" -> "storyboard=false"; + case "nsfw" -> "nsfw=true"; + case "!nsfw" -> "nsfw=false"; case "fc", "fullcombo" -> "fullcombo=true"; case "replay" -> "replay=true"; case "!replay" -> "replay=false"; From fb0e6df2f36ed3df9efce9a0070a13dbe350d8df Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sat, 19 Sep 2026 16:04:59 +0800 Subject: [PATCH 14/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20TaskCoordinator?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E8=AF=B7=E6=B1=82=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=E5=B9=B6=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E8=AE=A1=E6=97=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zcraft/seira/command/TaskCoordinator.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java index c1a8c46c..d4d43b34 100644 --- a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java +++ b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java @@ -20,9 +20,14 @@ import java.nio.channels.ClosedChannelException; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; +import static xyz.zcraft.seira.command.reply.ReplyFactory.cmd; public final class TaskCoordinator { private static final Logger LOG = LogManager.getLogger(TaskCoordinator.class); @@ -75,13 +80,23 @@ public CommandReplyChannel openReplyChannel( return new OutboundReplyChannel(targetId, messageId, groupMessage, queueMessageInGroup); } + private static final ScheduledExecutorService TIMEOUT_SCHEDULER = Executors.newSingleThreadScheduledExecutor(); /** * Tracks queue estimates and elapsed time; the caller executes the request directly. */ public RequestTiming beginRequest(Context ctx, String requestType) { long estimatedSeconds = apiRequestStats.estimateAndEnqueue(requestType); - RequestTiming timing = new RequestTiming(requestType); + + final var schedule = TIMEOUT_SCHEDULER.schedule( + () -> { + ctx.sendReply(at(ctx) + "请求处理时间超过预期,这可能是由于相关数据缺少缓存,请耐心等待喵。"); + }, + 60, + TimeUnit.SECONDS + ); + + RequestTiming timing = new RequestTiming(requestType, schedule); try { ctx.sendQueueNotice(PendingMessage.ofMarkdownRaw( at(ctx) + "请求已加入队列,预计等待时间" + estimatedSeconds + "秒。")); @@ -227,13 +242,16 @@ public final class RequestTiming implements AutoCloseable { private final String requestType; private final long startedAt = System.nanoTime(); private boolean closed; + private final ScheduledFuture scheduledFuture; - private RequestTiming(String requestType) { + private RequestTiming(String requestType, ScheduledFuture schedule) { this.requestType = requestType; + this.scheduledFuture = schedule; } @Override public void close() { + scheduledFuture.cancel(true); if (closed) return; closed = true; apiRequestStats.complete(requestType, From dd3c968bc158c2814e097acf97d5561c7a58a6fe Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sat, 19 Sep 2026 19:42:31 +0800 Subject: [PATCH 15/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20Context=20=E5=92=8C?= =?UTF-8?q?=20TaskCoordinator=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=AF=B9=20Markdo?= =?UTF-8?q?wn=20=E6=A0=BC=E5=BC=8F=E7=9A=84=E6=94=AF=E6=8C=81=E4=BB=A5?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E6=B6=88=E6=81=AF=E5=8F=91=E9=80=81=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/command/Context.java | 2 +- src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/Context.java b/src/main/java/xyz/zcraft/seira/command/Context.java index ccc13d70..d8067d42 100644 --- a/src/main/java/xyz/zcraft/seira/command/Context.java +++ b/src/main/java/xyz/zcraft/seira/command/Context.java @@ -70,7 +70,7 @@ public SendResult sendReply(PendingMessage message) { } public SendResult sendReply(String message) { - return requireReplies().sendReply(PendingMessage.ofString(message)); + return requireReplies().sendReply(PendingMessage.ofMarkdownRaw(message)); } /** diff --git a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java index d4d43b34..e957fcb2 100644 --- a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java +++ b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java @@ -90,7 +90,7 @@ public RequestTiming beginRequest(Context ctx, String requestType) { final var schedule = TIMEOUT_SCHEDULER.schedule( () -> { - ctx.sendReply(at(ctx) + "请求处理时间超过预期,这可能是由于相关数据缺少缓存,请耐心等待喵。"); + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "请求处理时间超过预期,这可能是由于相关数据缺少缓存,请耐心等待喵。")); }, 60, TimeUnit.SECONDS From 6827df8b8f953008e473f8c463bcb7e3f32fb835 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sat, 19 Sep 2026 20:28:03 +0800 Subject: [PATCH 16/96] =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A7=BB=E9=99=A4=20RscTarget=20?= =?UTF-8?q?=E7=B1=BB=E5=B9=B6=E4=BC=98=E5=8C=96=20TargetResolution=20?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/xyz/zcraft/seira/api/APIHelper.java | 191 ++++-------- .../zcraft/seira/command/TargetHistory.java | 277 +----------------- .../zcraft/seira/command/TargetLookup.java | 124 ++++++++ .../handler/BeatmapCommandHandler.java | 93 +++--- .../handler/RankGuessCommandHandler.java | 5 +- .../command/handler/ReplayCommandHandler.java | 90 ++++-- .../command/handler/ScoreCommandHandler.java | 240 ++++++++------- .../command/handler/SocialCommandHandler.java | 83 +++--- .../command/handler/WatchCommandHandler.java | 65 ++-- .../zcraft/seira/command/parse/Resolver.java | 42 --- .../zcraft/seira/command/parse/RscTarget.java | 4 - .../seira/command/parse/TargetArguments.java | 35 +++ .../seira/command/parse/TargetResolution.java | 35 +-- .../zcraft/seira/command/route/Router.java | 6 +- 14 files changed, 542 insertions(+), 748 deletions(-) create mode 100644 src/main/java/xyz/zcraft/seira/command/TargetLookup.java delete mode 100644 src/main/java/xyz/zcraft/seira/command/parse/RscTarget.java create mode 100644 src/main/java/xyz/zcraft/seira/command/parse/TargetArguments.java diff --git a/src/main/java/xyz/zcraft/seira/api/APIHelper.java b/src/main/java/xyz/zcraft/seira/api/APIHelper.java index b6089440..6ce32ce7 100644 --- a/src/main/java/xyz/zcraft/seira/api/APIHelper.java +++ b/src/main/java/xyz/zcraft/seira/api/APIHelper.java @@ -11,7 +11,6 @@ import xyz.zcraft.seira.api.data.*; import xyz.zcraft.seira.bot.data.FileInfo; import xyz.zcraft.seira.command.ResolutionException; -import xyz.zcraft.seira.command.parse.ShortcutTarget; import xyz.zcraft.seira.data.UserRef; import xyz.zcraft.seira.util.TimeDurationParser; @@ -301,63 +300,6 @@ public static Response getBeatmapBgResponse(long beatmapId) { return getBase64BytesResponse("/beatmaps/" + beatmapId + "/background", "获取谱面失败", null); } - public static long lookupBeatmap(ShortcutTarget target, String auth) { - long beatmapId; - if (target.isLocalScore() || "s".equals(target.macroType())) { - beatmapId = lookupScoreData(lookupScoreId(target, List.of(), null)).get("beatmap_id").getAsLong(); - } else if ("m".equals(target.macroType())) { - beatmapId = target.explicitId(); - } else if (!target.isMacro()) { - beatmapId = target.explicitId(); - } else { - try { - final String query = getBeatmapQuery(target); - - HttpRequest localRequest = HttpRequest.newBuilder() - .uri(URI.create(ENDPOINT + query)) - .header("Authorization", "Bearer " + auth) - .GET() - .build(); - - final HttpResponse send = CLIENT.send(localRequest, HttpResponse.BodyHandlers.ofString()); - - if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "查找谱面失败"); - } - - final RawResponse rawResponse = GSON.fromJson(send.body(), RawResponse.class); - - ensureApiSuccess(rawResponse, "查找谱面失败"); - - beatmapId = rawResponse.getData().getAsJsonObject().get("beatmap_id").getAsLong(); - } catch (IOException | InterruptedException e) { - throw requestFailure(e); - } - } - return beatmapId; - } - - private static String getBeatmapQuery(ShortcutTarget target) { - String query = "/beatmaps/lookup?"; - if (target.isMacro()) { - switch (target.macroType().toLowerCase()) { - case "rs", "bp", "rp" -> { - query += "&of=" + target.macroType() + "&u=" + resolveUid(target.userRef()); - query += "&i=" + target.macroIndex(); - } - case "ms" -> { - query += "&ms=" + target.explicitId(); - query += "&i=" + target.macroIndex(); - } - case "mp" -> query += "&of=mp"; - } - } else { - query = "/beatmap/lookup?m=" + target.explicitId(); - } - - return query; - } - public static Response getBeatmapsetResponse(long beatmapsetId) { return getBase64BytesResponse("/beatmapsets/" + beatmapsetId, "获取谱面集失败", null); } @@ -384,53 +326,6 @@ public static Beatmapset getBeatmapsetRaw(long id) { } } - public static long lookupBeatmapset(ShortcutTarget target, String auth) { - long beatmapsetId; - if (target.isLocalScore() || "s".equals(target.macroType())) { - return lookupBeatmapset(new ShortcutTarget(lookupBeatmap(target, auth), null, "m", null, null), auth); - } else if (!target.isMacro() || "ms".equals(target.macroType())) { - beatmapsetId = target.explicitId(); - } else { - try { - final String query = getBeatmapsetQuery(target); - - HttpRequest localRequest = HttpRequest.newBuilder() - .uri(URI.create(ENDPOINT + query)) - .header("Authorization", "Bearer " + auth) - .GET() - .build(); - - final HttpResponse send = CLIENT.send(localRequest, HttpResponse.BodyHandlers.ofString()); - - if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "查找谱面集失败"); - } - - final RawResponse rawResponse = GSON.fromJson(send.body(), RawResponse.class); - - ensureApiSuccess(rawResponse, "查找谱面集失败"); - - beatmapsetId = rawResponse.getData().getAsJsonObject().get("beatmapset_id").getAsLong(); - } catch (IOException | InterruptedException e) { - throw requestFailure(e); - } - } - return beatmapsetId; - } - - private static String getBeatmapsetQuery(ShortcutTarget target) { - String query = "/beatmapsets/lookup"; - - return switch (target.macroType().toLowerCase()) { - case "m" -> query + "?m=" + target.explicitId(); - case "ms" -> query + "?ms=" + target.explicitId(); - case "rs", "bp", "rp" -> - query + "?of=" + target.macroType() + "&i=" + target.macroIndex() + "&u=" + resolveUid(target.userRef()); - case "mp" -> query + "?of=mp"; - case null, default -> throw new ResolutionException("快捷查询格式错误。"); - }; - } - public static Response getScoreResponse(String scoreId) { return getBase64BytesResponse("/scores/" + scoreId, "获取成绩失败", null); } @@ -470,17 +365,6 @@ private static Response getBase64BytesResponse(String query, String } } - private static String getScoreQuery(ShortcutTarget target) { - return switch (target.macroType().toLowerCase()) { - case "rs", "bp", "rp" -> - "/scores/lookup?of=" + target.macroType() + "&i=" + target.macroIndex() + "&u=" + resolveUid(target.userRef()); - case "m" -> "/scores/lookup?m=" + target.explicitId() + "&u=" + resolveUid(target.userRef()); - case "ms" -> - "/scores/lookup?ms=" + target.explicitId() + "&i=" + target.macroIndex() + "&u=" + resolveUid(target.userRef()); - case null, default -> throw new IllegalArgumentException("Invalid macro type"); - }; - } - public static Response getLookupBeatmapsetResponse(long beatmapsetId, String auth) { try { final String query = "/beatmapsets/lookup?ms=" + beatmapsetId; @@ -664,7 +548,6 @@ public static RandomScore getRandomScoreFromUsers(List uids, JsonObject we } } - public static ReplayTaskInfo createBeatmapPreviewTask(long beatmapId, String mods, TimeDurationParser.TimeRange range, QqUploadRequest qqUpload) { JsonObject body = new JsonObject(); @@ -763,38 +646,62 @@ private static TimeDurationParser.TimeRange getScoreHighlight(String scoreId, in } } - public static String lookupScoreId(ShortcutTarget target, List filters, String mod) { - String scoreId; - if (target.isLocalScore()) { - scoreId = target.localScoreId(); - } else if (!target.isMacro() || "s".equals(target.macroType())) { - scoreId = String.valueOf(target.explicitId()); - } else { - try { - final String query = getScoreQuery(target) + encodeScoreFilters(filters) - + (mod == null ? "" : "&mod=" + URLEncoder.encode(mod, StandardCharsets.UTF_8)); + /** 每个查找方法只请求一个接口;目标类型转换和记忆由指令处理方法决定。 */ + public static long lookupBeatmapInSet(long setId, long index, String auth) { + return lookupTargetData("/beatmaps/lookup?ms=" + setId + "&i=" + index, auth, "查找谱面失败") + .get("beatmap_id").getAsLong(); + } - HttpRequest localRequest = HttpRequest.newBuilder() - .uri(URI.create(ENDPOINT + query)) - .GET() - .build(); + public static long lookupMultiplayerBeatmap(String auth) { + return lookupTargetData("/beatmaps/lookup?of=mp", auth, "查找谱面失败").get("beatmap_id").getAsLong(); + } - final HttpResponse send = CLIENT.send(localRequest, HttpResponse.BodyHandlers.ofString()); + public static long lookupPlayerScoreBeatmap(long uid, String list, long index, String auth) { + if (!List.of("rs", "rp", "bp").contains(list)) throw new IllegalArgumentException("Invalid score list"); + return lookupTargetData("/beatmaps/lookup?of=" + list + "&u=" + uid + "&i=" + index, + auth, "查找谱面失败").get("beatmap_id").getAsLong(); + } - if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "获取成绩失败"); - } + public static long lookupMultiplayerBeatmapset(String auth) { + return lookupTargetData("/beatmapsets/lookup?of=mp", auth, "查找谱面集失败").get("beatmapset_id").getAsLong(); + } - final RawResponse rawResponse = GSON.fromJson(send.body(), RawResponse.class); + public static long lookupBeatmapsetForBeatmap(long beatmapId, String auth) { + return lookupTargetData("/beatmapsets/lookup?m=" + beatmapId, auth, "查找谱面集失败") + .get("beatmapset_id").getAsLong(); + } - ensureApiSuccess(rawResponse, "获取成绩失败"); + public static String lookupPlayerScore(long uid, String list, long index, List filters, String mod) { + if (!List.of("rs", "rp", "bp").contains(list)) throw new IllegalArgumentException("Invalid score list"); + return lookupScore("/scores/lookup?of=" + list + "&i=" + index + "&u=" + uid, filters, mod); + } - scoreId = rawResponse.getData().getAsJsonObject().get("score_id").getAsString(); - } catch (IOException | InterruptedException e) { - throw requestFailure(e); - } + public static String lookupBeatmapScore(long beatmapId, long uid, List filters, String mod) { + return lookupScore("/scores/lookup?m=" + beatmapId + "&u=" + uid, filters, mod); + } + + public static String lookupBeatmapsetScore(long setId, long index, long uid, List filters, String mod) { + return lookupScore("/scores/lookup?ms=" + setId + "&i=" + index + "&u=" + uid, filters, mod); + } + + private static String lookupScore(String query, List filters, String mod) { + return lookupTargetData(query + encodeScoreFilters(filters) + + (mod == null ? "" : "&mod=" + URLEncoder.encode(mod, StandardCharsets.UTF_8)), + null, "获取成绩失败").get("score_id").getAsString(); + } + + private static JsonObject lookupTargetData(String query, String auth, String error) { + try { + var request = HttpRequest.newBuilder().uri(URI.create(ENDPOINT + query)).GET(); + if (auth != null) request.header("Authorization", "Bearer " + auth); + var response = CLIENT.send(request.build(), HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) throw parseHttpError(response.body(), response.statusCode(), error); + RawResponse payload = GSON.fromJson(response.body(), RawResponse.class); + ensureApiSuccess(payload, error); + return requireDataObject(payload, error); + } catch (IOException | InterruptedException e) { + throw requestFailure(e); } - return scoreId; } public static long getScoreBeatmapId(String scoreId) { diff --git a/src/main/java/xyz/zcraft/seira/command/TargetHistory.java b/src/main/java/xyz/zcraft/seira/command/TargetHistory.java index d8160fa3..affff5eb 100644 --- a/src/main/java/xyz/zcraft/seira/command/TargetHistory.java +++ b/src/main/java/xyz/zcraft/seira/command/TargetHistory.java @@ -1,285 +1,24 @@ package xyz.zcraft.seira.command; -import xyz.zcraft.seira.api.APIHelper; -import xyz.zcraft.seira.bot.data.PendingMessage; -import xyz.zcraft.seira.command.parse.Resolver; -import xyz.zcraft.seira.command.parse.ShortcutTarget; -import xyz.zcraft.seira.command.parse.TargetResolution; -import xyz.zcraft.seira.command.parse.UserRefResolution; -import xyz.zcraft.seira.data.UserRef; - -import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.function.Function; -import java.util.function.Predicate; - -import static xyz.zcraft.seira.command.reply.ReplyFactory.at; +/** Per-sender target memory. No parsing, binding lookup, API calls or replies. */ public final class TargetHistory { private final ConcurrentMap users = new ConcurrentHashMap<>(); - private final Resolver resolver; - private final Function accessToken; - public TargetHistory(Resolver resolver, Function accessToken) { - this.resolver = resolver; - this.accessToken = accessToken; - } - - private static boolean isLocalId(String id) { - return !id.chars().allMatch(Character::isDigit); - } - /** - * 显式记忆一个新目标,清除旧目标的关联 ID。 - */ - public void remember(Context ctx, long id, Type type) { - remember(ctx, Long.toString(id), type); + public Ids get(Context ctx) { + return users.get(ctx.senderUserId()); } - public void remember(Context ctx, String id, Type type) { - Ids ids = new Ids(); - switch (type) { - case BEATMAPSET -> ids.beatmapsetId = Long.parseLong(id); - case BEATMAP -> ids.beatmapId = Long.parseLong(id); - case SCORE -> ids.scoreId = id; - } - users.put(ctx.senderUserId(), ids); - } - - /** - * 只取指定类型已经记住的 ID,不进行查找。 - */ - public ShortcutTarget get(Context ctx, Type type) { - Ids ids = users.get(ctx.senderUserId()); - if (ids == null) return null; - String id = switch (type) { - case BEATMAPSET -> ids.beatmapsetId == null ? null : ids.beatmapsetId.toString(); - case BEATMAP -> ids.beatmapId == null ? null : ids.beatmapId.toString(); - case SCORE -> ids.scoreId; - }; - if (id == null) return null; - if (type == Type.SCORE && isLocalId(id)) return ShortcutTarget.localScore(id); - return new ShortcutTarget(Long.parseLong(id), null, null, null, null); + public void remember(Context ctx, Long beatmapsetId, Long beatmapId, String scoreId) { + remember(ctx, new Ids(beatmapsetId, beatmapId, scoreId)); } public void remember(Context ctx, Ids ids) { - users.put(ctx.senderUserId(), new Ids(ids)); - } - - public Ids resolve(Context ctx, Type type, TargetResolution args) { - return resolve(ctx, type, args, List.of(), null); - } - - /** - * 查找只修改本次结果;调用者显式 remember 后才更新历史。 - */ - public Ids resolve(Context ctx, Type type, TargetResolution args, List filters, String mod) { - ShortcutTarget target = args.getTarget(); - if (target != null && target.isError()) throw new ResolutionException(target.errorMessage()); - - Ids previous = users.get(ctx.senderUserId()); - // 省略目标时沿用三个 ID;显式输入目标时从空记忆开始。 - Ids ids = target == null ? new Ids(previous) : new Ids(); - UserRef player = args.getUserOverride() != null ? args.getUserOverride() - : target == null ? null : target.userRef(); - boolean selectedPlayerScore = false; - - if (target != null) { - if (target.isLocalScore()) { - ids.scoreId = target.localScoreId(); - } else if (!target.isMacro()) { - switch (type) { - case BEATMAPSET -> ids.beatmapsetId = target.explicitId(); - case BEATMAP -> ids.beatmapId = target.explicitId(); - case SCORE -> ids.scoreId = target.explicitId().toString(); - } - } else { - switch (target.macroType()) { - case "m" -> ids.beatmapId = target.explicitId(); - case "s" -> ids.scoreId = target.explicitId().toString(); - case "ms" -> { - ids.beatmapsetId = target.explicitId(); - if (type != Type.BEATMAPSET) { - if (target.macroIndex() == null) throw new ResolutionException("请指定指令目标谱面喵"); - if (type == Type.BEATMAP) { - ids.beatmapId = APIHelper.lookupBeatmap(target, accessToken.apply(ctx.senderUserId())); - } else { - player = requirePlayer(ctx, player); - ids.scoreId = APIHelper.lookupScoreId(new ShortcutTarget( - target.explicitId(), player, "ms", target.macroIndex(), null), filters, mod); - selectedPlayerScore = true; - } - } - } - case "rs", "rp", "bp" -> { - // 先把列表位置固定为实际成绩 ID,后续指令不再重新查询列表。 - player = requirePlayer(ctx, player); - ids.scoreId = APIHelper.lookupScoreId(new ShortcutTarget( - null, player, target.macroType(), target.macroIndex(), null), filters, mod); - selectedPlayerScore = true; - } - case "mp" -> { - if (type == Type.BEATMAPSET) { - ids.beatmapsetId = APIHelper.lookupBeatmapset(target, accessToken.apply(ctx.senderUserId())); - } else { - ids.beatmapId = APIHelper.lookupBeatmap(target, accessToken.apply(ctx.senderUserId())); - } - } - default -> throw new ResolutionException("未知的快捷查询"); - } - } - } - - // 指定用户或 Mods 时,按同一谱面重新查成绩,不能直接沿用旧成绩 ID。 - // /s rs2 @用户 已经选好了该用户的 rs2,不再改查谱面最佳成绩。 - if (type == Type.SCORE && (args.getUserOverride() != null || mod != null) - && ids.scoreId != null && !selectedPlayerScore) { - if (ids.beatmapId == null) { - ids.beatmapId = APIHelper.getScoreBeatmapId(ids.scoreId); - } - ids.scoreId = null; - } - - switch (type) { - case BEATMAP -> { - if (ids.beatmapId == null) { - if (ids.scoreId == null) throw new ResolutionException("请指定指令目标谱面喵"); - ids.beatmapId = APIHelper.getScoreBeatmapId(ids.scoreId); - } - } - case BEATMAPSET -> { - if (ids.beatmapsetId == null) { - if (ids.beatmapId == null && ids.scoreId != null) { - ids.beatmapId = APIHelper.getScoreBeatmapId(ids.scoreId); - } - if (ids.beatmapId == null) throw new ResolutionException("请指定指令目标喵"); - ids.beatmapsetId = APIHelper.lookupBeatmapset(new ShortcutTarget( - ids.beatmapId, null, "m", null, null), accessToken.apply(ctx.senderUserId())); - } - } - case SCORE -> { - if (ids.scoreId == null) { - if (ids.beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); - player = requirePlayer(ctx, player); - ids.scoreId = APIHelper.lookupScoreId(new ShortcutTarget( - ids.beatmapId, player, "m", null, null), filters, mod); - } - } - } - return ids; - } - - private UserRef requirePlayer(Context ctx, UserRef player) { - if (player != null) return player; - Long uid = resolver.resolveBoundUid(ctx.senderUserId()); - if (uid == null) throw new ResolutionException("请先绑定 osu! 账号,再查找记忆谱面上的成绩喵"); - return new UserRef.ByUid(uid); - } - - /** - * 同屏回放需要保留本地成绩本身,以便把它加入回放列表。 - */ - public boolean isLocalScore(Context ctx, TargetResolution args) { - if (args.getTarget() != null) return args.getTarget().isLocalScore(); - Ids ids = users.get(ctx.senderUserId()); - return ids != null && ids.scoreId != null && isLocalId(ids.scoreId); - } - - public TargetResolution parseArguments(Context ctx, String usage, int maxOptions) { - return parseArguments(ctx, usage, maxOptions, _ -> false); - } - - /** - * optional 判断首个参数是否是省略目标后的选项;返回的 getConsumedArgs 标记选项起点。 - */ - public TargetResolution parseArguments(Context ctx, String usage, int maxOptions, Predicate optional) { - try { - TargetResolution args; - if (ctx.argumentCount() == 0 || optional.test(ctx.argument(0))) { - if (!users.containsKey(ctx.senderUserId())) throw new ResolutionException(usage); - args = new TargetResolution(null, 0); - } else { - args = resolver.resolveTargetWithOptionalMention(ctx.args(), ctx.senderUserId()); - if (args.getTarget().isError()) throw new ResolutionException(args.getTarget().errorMessage()); - } - if (ctx.argumentCount() - args.getConsumedArgs() > maxOptions) throw new ResolutionException(usage); - return args; - } catch (ResolutionException e) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + e.getMessage())); - return null; - } - } - - public TargetResolution parseScoreArguments(Context ctx, String usage) { - return parseScoreArguments(ctx, usage, 0, _ -> false); - } - - /** - * 目标和可选用户在前,其余参数交给指令;这里不认识 +mod 等具体选项。 - */ - public TargetResolution parseScoreArguments(Context ctx, String usage, int maxOptions, Predicate optional) { - try { - TargetResolution args; - if (ctx.argumentCount() > 0 && resolver.looksLikeMention(ctx.argument(0)) - && (ctx.argumentCount() == 1 || optional.test(ctx.argument(1)))) { - UserRef player = parsePlayer(ctx.argument(0), usage); - if (!users.containsKey(ctx.senderUserId())) throw new ResolutionException(usage); - args = new TargetResolution(null, 1, player); - } else { - args = parseArguments(ctx, usage, Integer.MAX_VALUE, optional); - if (args == null) return null; - String next = args.nextArgument(ctx); - if (next != null && !optional.test(next)) { - args = new TargetResolution(args.getTarget(), args.getConsumedArgs(), parsePlayer(next, usage)); - } - } - if (ctx.argumentCount() - args.getConsumedArgs() > maxOptions) throw new ResolutionException(usage); - for (int i = args.getConsumedArgs(); i < ctx.argumentCount(); i++) { - if (!optional.test(ctx.argument(i))) throw new ResolutionException(usage); - } - return args; - } catch (ResolutionException e) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + e.getMessage())); - return null; - } - } - - private UserRef parsePlayer(String argument, String usage) { - UserRefResolution result = resolver.resolveUserRefArgument(argument); - if (result.errorMessage() != null) throw new ResolutionException(result.errorMessage()); - if (result.userRef() == null) throw new ResolutionException(usage); - return result.userRef(); + users.put(ctx.senderUserId(), ids); } - public enum Type {BEATMAPSET, BEATMAP, SCORE} - - // 每个调用者只保存三个 ID。成绩 ID 使用字符串以兼容 loc... 本地成绩。 - public static final class Ids { - private Long beatmapsetId; - private Long beatmapId; - private String scoreId; - - Ids() { - } - - Ids(Ids previous) { - if (previous != null) { - beatmapsetId = previous.beatmapsetId; - beatmapId = previous.beatmapId; - scoreId = previous.scoreId; - } - } - - public Long beatmapsetId() { - return beatmapsetId; - } - - public Long beatmapId() { - return beatmapId; - } - - public String scoreId() { - return scoreId; - } - } + /** Immutable snapshot: IDs belong to the same target, and missing associations remain null. */ + public record Ids(Long beatmapsetId, Long beatmapId, String scoreId) {} } diff --git a/src/main/java/xyz/zcraft/seira/command/TargetLookup.java b/src/main/java/xyz/zcraft/seira/command/TargetLookup.java new file mode 100644 index 00000000..de08ede4 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/command/TargetLookup.java @@ -0,0 +1,124 @@ +package xyz.zcraft.seira.command; + +import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.seira.command.parse.Resolver; +import xyz.zcraft.seira.command.parse.ShortcutTarget; +import xyz.zcraft.seira.data.UserRef; + +import java.util.List; +import java.util.function.Function; + +public final class TargetLookup { + private final Resolver resolver; + private final Function accessTokenProvider; + + public TargetLookup(Resolver resolver, Function accessTokenProvider) { + this.resolver = resolver; + this.accessTokenProvider = accessTokenProvider; + } + + public TargetHistory.Ids beatmap(Context ctx, ShortcutTarget input, TargetHistory.Ids previous) { + return lookup(ctx, input, previous, Type.BEATMAP, null, List.of(), null); + } + + public TargetHistory.Ids beatmapset(Context ctx, ShortcutTarget input, TargetHistory.Ids previous) { + return lookup(ctx, input, previous, Type.BEATMAPSET, null, List.of(), null); + } + + public TargetHistory.Ids score(Context ctx, ShortcutTarget input, TargetHistory.Ids previous) { + return score(ctx, input, previous, null, List.of(), null); + } + + public TargetHistory.Ids score(Context ctx, ShortcutTarget input, TargetHistory.Ids previous, + UserRef userOverride, List filters, String mod) { + return lookup(ctx, input, previous, Type.SCORE, userOverride, filters, mod); + } + + private TargetHistory.Ids lookup(Context ctx, ShortcutTarget input, TargetHistory.Ids previous, + Type type, UserRef userOverride, List filters, String mod) { + if (input != null) previous = null; + Long beatmapId = previous == null ? null : previous.beatmapId(); + Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); + String scoreId = previous == null ? null : previous.scoreId(); + UserRef player = userOverride != null ? userOverride : input == null ? null : input.userRef(); + boolean selectedPlayerScore = false; + if (input != null) { + if (input.isError()) throw new ResolutionException(input.errorMessage()); + if (input.isLocalScore()) { + scoreId = input.localScoreId(); + } else if (!input.isMacro()) { + switch (type) { + case BEATMAP -> beatmapId = input.explicitId(); + case BEATMAPSET -> beatmapsetId = input.explicitId(); + case SCORE -> scoreId = input.explicitId().toString(); + } + } else { + switch (input.macroType()) { + case "m" -> beatmapId = input.explicitId(); + case "s" -> scoreId = input.explicitId().toString(); + case "ms" -> { + beatmapsetId = input.explicitId(); + if (type != Type.BEATMAPSET) { + if (input.macroIndex() == null) throw new ResolutionException("请指定指令目标谱面喵"); + if (type == Type.BEATMAP) { + beatmapId = APIHelper.lookupBeatmapInSet(beatmapsetId, input.macroIndex(), accessTokenProvider.apply(ctx.senderUserId())); + } else { + player = requirePlayer(ctx, player); + scoreId = APIHelper.lookupBeatmapsetScore(beatmapsetId, input.macroIndex(), APIHelper.resolveUid(player), filters, mod); + selectedPlayerScore = true; + } + } + } + case "rs", "rp", "bp" -> { + player = requirePlayer(ctx, player); + scoreId = APIHelper.lookupPlayerScore(APIHelper.resolveUid(player), input.macroType(), input.macroIndex(), filters, mod); + selectedPlayerScore = true; + } + case "mp" -> { + String token = accessTokenProvider.apply(ctx.senderUserId()); + if (type == Type.BEATMAPSET) beatmapsetId = APIHelper.lookupMultiplayerBeatmapset(token); + else beatmapId = APIHelper.lookupMultiplayerBeatmap(token); + } + default -> throw new ResolutionException("未知的快捷查询"); + } + } + } + + if (type == Type.SCORE && (userOverride != null || mod != null) && scoreId != null && !selectedPlayerScore) { + if (beatmapId == null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + scoreId = null; + } + switch (type) { + case BEATMAP -> { + if (beatmapId == null) { + if (scoreId == null) throw new ResolutionException("请指定指令目标谱面喵"); + beatmapId = APIHelper.getScoreBeatmapId(scoreId); + } + } + case BEATMAPSET -> { + if (beatmapsetId == null) { + if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null) throw new ResolutionException("请指定指令目标喵"); + beatmapsetId = APIHelper.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); + } + } + case SCORE -> { + if (scoreId == null) { + if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); + player = requirePlayer(ctx, player); + scoreId = APIHelper.lookupBeatmapScore(beatmapId, APIHelper.resolveUid(player), filters, mod); + } + } + } + return new TargetHistory.Ids(beatmapsetId, beatmapId, scoreId); + } + + private UserRef requirePlayer(Context ctx, UserRef player) { + if (player != null) return player; + Long uid = resolver.resolveBoundUid(ctx.senderUserId()); + if (uid == null) throw new ResolutionException("请先绑定 osu! 账号,再查找记忆谱面上的成绩喵"); + return new UserRef.ByUid(uid); + } + + private enum Type { BEATMAP, BEATMAPSET, SCORE } +} diff --git a/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java index aaf3858a..cfe468ec 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java @@ -9,6 +9,8 @@ import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.command.TargetHistory; +import xyz.zcraft.seira.command.TargetLookup; +import xyz.zcraft.seira.command.parse.TargetArguments; import xyz.zcraft.seira.command.TaskCoordinator; import xyz.zcraft.seira.command.parse.Resolver; import xyz.zcraft.seira.command.reply.CommandUsage; @@ -19,13 +21,12 @@ import java.util.List; import java.util.function.Function; -import static xyz.zcraft.seira.command.TargetHistory.Type.BEATMAP; -import static xyz.zcraft.seira.command.TargetHistory.Type.BEATMAPSET; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; public final class BeatmapCommandHandler { private final Resolver resolver; private final TargetHistory history; + private final TargetLookup targetLookup; private final TaskCoordinator taskCoordinator; private final ReplyFactory replyFactory; private final VideoRenderRecord videoRenderRecord; @@ -41,6 +42,7 @@ public BeatmapCommandHandler( ) { this.resolver = resolver; this.history = history; + this.targetLookup = new TargetLookup(resolver, accessTokenProvider); this.taskCoordinator = taskCoordinator; this.replyFactory = replyFactory; this.videoRenderRecord = videoRenderRecord; @@ -55,53 +57,60 @@ public void handleDaily(Context ctx) { } public void handleM(Context ctx) { - var target = history.parseArguments(ctx, CommandUsage.M, 1); + var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.M, 1); if (target == null) return; try (var _ = taskCoordinator.beginRequest(ctx, "Beatmap")) { - var ids = history.resolve(ctx, BEATMAP, target); - history.remember(ctx, ids); - var response = APIHelper.getBeatmapResponse(ids.beatmapId(), target.nextArgument(ctx)); + var resolvedTarget = targetLookup.beatmap(ctx, target.target(), history.get(ctx)); + history.remember(ctx, resolvedTarget); + long beatmapId = resolvedTarget.beatmapId(); + var response = APIHelper.getBeatmapResponse(beatmapId, (ctx.argumentCount() > target.consumedArgs() ? ctx.argument(target.consumedArgs()) : null)); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.beatmapMessage(ctx, response))); } } public void handleBma(Context ctx) { - var target = history.parseArguments(ctx, CommandUsage.BMA, 1); + var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.BMA, 1); if (target == null) return; try (var timing = taskCoordinator.beginRequest(ctx, "Beatmap Analysis")) { - var ids = history.resolve(ctx, BEATMAP, target); - history.remember(ctx, ids); - var response = APIHelper.getBeatmapAnalysisResponse(ids.beatmapId(), target.nextArgument(ctx)); + var resolvedTarget = targetLookup.beatmap(ctx, target.target(), history.get(ctx)); + history.remember(ctx, resolvedTarget); + long beatmapId = resolvedTarget.beatmapId(); + var response = APIHelper.getBeatmapAnalysisResponse(beatmapId, (ctx.argumentCount() > target.consumedArgs() ? ctx.argument(target.consumedArgs()) : null)); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.beatmapMessage(ctx, response))); } } public void handleAp(Context ctx) { - var target = history.parseArguments(ctx, CommandUsage.AP, Integer.MAX_VALUE); + var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.AP, Integer.MAX_VALUE); if (target == null) return; try (var timing = taskCoordinator.beginRequest(ctx, "Audio Preview")) { - var ids = history.resolve(ctx, BEATMAPSET, target); - history.remember(ctx, ids); - long id = ids.beatmapsetId(); + var resolvedTarget = targetLookup.beatmapset(ctx, target.target(), history.get(ctx)); + history.remember(ctx, resolvedTarget); + long beatmapsetId = resolvedTarget.beatmapsetId(); + long id = beatmapsetId; ctx.sendReply(PendingMessage.ofVoiceUrl("https://b.ppy.sh/preview/" + id + ".mp3").doUpload(false)); } } public void handleBpv(Context ctx) { - var target = history.parseArguments( - ctx, CommandUsage.BPV, 2, - arg -> arg.startsWith("+") || TimeDurationParser.isTimeRange(arg) - ); - + var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.BPV, 2, arg -> arg.startsWith("+") || TimeDurationParser.isTimeRange(arg)); if (target == null) return; TimeDurationParser.TimeRange range = null; - - if (ctx.args().length > target.getConsumedArgs()) { - try { - range = TimeDurationParser.parseRange(ctx.args()[target.getConsumedArgs()]); - } catch (IllegalArgumentException e) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "无法解析时间范围")); + String mods = null; + for (int optionIndex = target.consumedArgs(); optionIndex < ctx.argumentCount(); optionIndex++) { + String option = ctx.argument(optionIndex); + if (TimeDurationParser.isTimeRange(option) && range == null) { + try { + range = TimeDurationParser.parseRange(option); + } catch (IllegalArgumentException e) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "无法解析时间范围")); + return; + } + } else if (mods == null && !TimeDurationParser.isTimeRange(option)) { + mods = option; + } else { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.BPV)); return; } } @@ -109,9 +118,10 @@ public void handleBpv(Context ctx) { try (var _ = taskCoordinator.beginRequest(ctx, "Beatmap Preview Render")) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "正在获取谱面以及回放文件,请稍作等待喵...")); var qqUpload = taskCoordinator.createVideoUploadRequest(ctx); - var ids = history.resolve(ctx, BEATMAP, target); - history.remember(ctx, ids); - var task = APIHelper.createBeatmapPreviewTask(ids.beatmapId(), target.nextArgument(ctx), range, qqUpload); + var resolvedTarget = targetLookup.beatmap(ctx, target.target(), history.get(ctx)); + history.remember(ctx, resolvedTarget); + long beatmapId = resolvedTarget.beatmapId(); + var task = APIHelper.createBeatmapPreviewTask(beatmapId, mods, range, qqUpload); videoRenderRecord.updateRenderTask(ctx.senderUserId(), task.taskId()); ctx.sendReply(replyFactory.replayMessage(ctx, task)); @@ -138,34 +148,37 @@ public void handleBpv(Context ctx) { } public void handleBgp(Context ctx) { - var target = history.parseArguments(ctx, CommandUsage.BGP, Integer.MAX_VALUE); + var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.BGP, Integer.MAX_VALUE); if (target == null) return; try (var timing = taskCoordinator.beginRequest(ctx, "Background Preview")) { - var ids = history.resolve(ctx, BEATMAP, target); - history.remember(ctx, ids); - var response = APIHelper.getBeatmapBgResponse(ids.beatmapId()); + var resolvedTarget = targetLookup.beatmap(ctx, target.target(), history.get(ctx)); + history.remember(ctx, resolvedTarget); + long beatmapId = resolvedTarget.beatmapId(); + var response = APIHelper.getBeatmapBgResponse(beatmapId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.bgpMessage(ctx, response))); } } public void handleDl(Context ctx) { - var target = history.parseArguments(ctx, CommandUsage.DL, 0); + var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.DL, 0); if (target == null) return; try (var timing = taskCoordinator.beginRequest(ctx, "Download Beatmap")) { - var ids = history.resolve(ctx, BEATMAPSET, target); - history.remember(ctx, ids); - var response = APIHelper.getLookupBeatmapsetResponse(ids.beatmapsetId(), accessTokenProvider.apply(ctx.senderUserId())); + var resolvedTarget = targetLookup.beatmapset(ctx, target.target(), history.get(ctx)); + history.remember(ctx, resolvedTarget); + long beatmapsetId = resolvedTarget.beatmapsetId(); + var response = APIHelper.getLookupBeatmapsetResponse(beatmapsetId, accessTokenProvider.apply(ctx.senderUserId())); ctx.sendReply(replyFactory.dlMessage(ctx, response)); } } public void handleMs(Context ctx) { - var target = history.parseArguments(ctx, "用法:/ms <谱面集ID 或 快捷查询>", 0); + var target = TargetArguments.parse(ctx, resolver, history.get(ctx), "用法:/ms <谱面集ID 或 快捷查询>", 0); if (target == null) return; try (var timing = taskCoordinator.beginRequest(ctx, "Beatmapset")) { - var ids = history.resolve(ctx, BEATMAPSET, target); - history.remember(ctx, ids); - var response = APIHelper.getBeatmapsetResponse(ids.beatmapsetId()); + var resolvedTarget = targetLookup.beatmapset(ctx, target.target(), history.get(ctx)); + history.remember(ctx, resolvedTarget); + long beatmapsetId = resolvedTarget.beatmapsetId(); + var response = APIHelper.getBeatmapsetResponse(beatmapsetId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.beatmapsetMessage(ctx, response))); } } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java index d50dfd51..aeff4c58 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java @@ -9,7 +9,6 @@ import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.command.TaskCoordinator; import xyz.zcraft.seira.command.parse.Resolver; -import xyz.zcraft.seira.command.parse.ShortcutTarget; import xyz.zcraft.seira.command.parse.UserRefResolution; import xyz.zcraft.seira.command.reply.ReplyFactory; import xyz.zcraft.seira.data.SendResult; @@ -417,9 +416,7 @@ private void wishScore(Context ctx, int index) { try { scoreId = Long.parseLong( - APIHelper.lookupScoreId(new ShortcutTarget( - null, new UserRef.ByUid(boundUid), "bp", (long) index, null) - , List.of(), null) + APIHelper.lookupPlayerScore(boundUid, "bp", index, List.of(), null) ); } catch (Exception e) { LOG.error("Failed to lookup score id", e); diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java index 07026f44..6eac2c35 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java @@ -7,9 +7,11 @@ import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.command.ReplayResultStore; import xyz.zcraft.seira.command.TargetHistory; +import xyz.zcraft.seira.command.TargetLookup; +import xyz.zcraft.seira.command.parse.TargetArguments; +import xyz.zcraft.seira.data.UserRef; import xyz.zcraft.seira.command.TaskCoordinator; import xyz.zcraft.seira.command.parse.Resolver; -import xyz.zcraft.seira.command.parse.RscTarget; import xyz.zcraft.seira.command.reply.CommandUsage; import xyz.zcraft.seira.command.reply.ReplyFactory; import xyz.zcraft.seira.data.SendResult; @@ -19,13 +21,12 @@ import java.util.UUID; import java.util.function.Predicate; -import static xyz.zcraft.seira.command.TargetHistory.Type.BEATMAP; -import static xyz.zcraft.seira.command.TargetHistory.Type.SCORE; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; public final class ReplayCommandHandler { private final Resolver resolver; private final TargetHistory history; + private final TargetLookup targetLookup; private final TaskCoordinator taskCoordinator; private final ReplyFactory replyFactory; private final VideoRenderRecord videoRenderRecord; @@ -39,10 +40,12 @@ public ReplayCommandHandler( ReplyFactory replyFactory, VideoRenderRecord videoRenderRecord, ReplayResultStore replayResults, - Predicate adminAuthorizer + Predicate adminAuthorizer, + java.util.function.Function accessTokenProvider ) { this.resolver = resolver; this.history = history; + this.targetLookup = new TargetLookup(resolver, accessTokenProvider); this.taskCoordinator = taskCoordinator; this.replyFactory = replyFactory; this.videoRenderRecord = videoRenderRecord; @@ -51,14 +54,14 @@ public ReplayCommandHandler( } public void handleR(Context ctx) { - var target = history.parseArguments(ctx, CommandUsage.R, 1, TimeDurationParser::isTimeRange); + var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.R, 1, TimeDurationParser::isTimeRange); if (target == null) return; TimeDurationParser.TimeRange range = null; - if (ctx.args().length > target.getConsumedArgs()) { + if (ctx.args().length > target.consumedArgs()) { try { - range = TimeDurationParser.parseRange(ctx.args()[target.getConsumedArgs()]); + range = TimeDurationParser.parseRange(ctx.args()[target.consumedArgs()]); } catch (IllegalArgumentException e) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "无法解析时间范围")); return; @@ -67,10 +70,11 @@ public void handleR(Context ctx) { try (var _ = taskCoordinator.beginRequest(ctx, "Score Render")) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "正在获取谱面以及回放文件,请稍作等待喵...")); - var ids = history.resolve(ctx, SCORE, target); - history.remember(ctx, ids); + var resolvedTarget = targetLookup.score(ctx, target.target(), history.get(ctx)); + history.remember(ctx, resolvedTarget); + String scoreId = resolvedTarget.scoreId(); var upload = taskCoordinator.createVideoUploadRequest(ctx); - var task = APIHelper.createReplayRenderTask(ids.scoreId(), range, upload); + var task = APIHelper.createReplayRenderTask(scoreId, range, upload); videoRenderRecord.updateRenderTask(ctx.senderUserId(), task.taskId()); ctx.sendReply(replyFactory.replayMessage(ctx, task)); @@ -102,13 +106,12 @@ public void handleRsc(Context ctx) { return; } - var target = history.parseArguments(ctx, CommandUsage.RSC, Integer.MAX_VALUE, - arg -> arg.startsWith("+") || arg.startsWith("=")); + var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.RSC, Integer.MAX_VALUE, arg -> arg.startsWith("+") || arg.startsWith("=")); if (target == null) return; String extraUidArg = null; - int i = target.getConsumedArgs(); + int i = target.consumedArgs(); if (i < ctx.args().length) { if (ctx.args()[i].startsWith("+") || ctx.args()[i].startsWith("=")) { @@ -119,30 +122,59 @@ public void handleRsc(Context ctx) { } } - RscTarget rscTarget = history.isLocalScore(ctx, target) && extraUidArg == null - ? new RscTarget(new String[0], null) - : resolver.resolveRscTarget(ctx.groupId(), extraUidArg); - if (rscTarget.errorMessage() != null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + rscTarget.errorMessage())); - return; + var remembered = history.get(ctx); + String localScoreId = target.target() != null + ? target.target().localScoreId() + : remembered == null ? null : remembered.scoreId(); + boolean localScore = localScoreId != null && localScoreId.startsWith("loc"); + var participants = new java.util.LinkedHashSet(); + if (!(localScore && extraUidArg == null)) { + if (extraUidArg == null || extraUidArg.trim().startsWith("+")) { + var groupUids = xyz.zcraft.seira.db.UserDataStore.findBoundUidsByGroup(ctx.groupId()); + if (groupUids.isEmpty()) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "本群还没有已绑定的玩家,请先使用 /bind")); + return; + } + groupUids.stream().map(String::valueOf).forEach(participants::add); + } + if (extraUidArg != null) { + String body = extraUidArg.trim().substring(1).trim(); + if (body.isEmpty()) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "追加ID列表不能为空。")); + return; + } + for (String token : body.split(",")) { + if (token.trim().matches("[us]?[0-9]+")) { + participants.add(token.trim()); + } else if (resolver.looksLikeMention(token)) { + var participant = resolver.resolveUserRefArgument(token); + if (participant.errorMessage() != null) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "解析 " + token + " 时出错:" + participant.errorMessage())); + return; + } + if (participant.userRef() instanceof UserRef.ByUid ref) participants.add("u" + ref.getUid()); + else if (participant.userRef() instanceof UserRef.ByUsername ref) participants.add("@" + ref.getUsername()); + } else { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "追加ID列表包含非法值。")); + return; + } + } + } } try (var _ = taskCoordinator.beginRequest(ctx, "Showcase Render")) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "正在获取谱面以及回放文件,请稍作等待喵...")); - var targetType = history.isLocalScore(ctx, target) ? SCORE : BEATMAP; - var resolved = history.resolve(ctx, targetType, target); - history.remember(ctx, resolved); + var resolvedTarget = targetLookup.beatmap(ctx, target.target(), history.get(ctx)); + history.remember(ctx, resolvedTarget); + long beatmapId = resolvedTarget.beatmapId(); var upload = taskCoordinator.createVideoUploadRequest(ctx); - String[] scoreTargets = rscTarget.targets(); - long beatmapId; - if (targetType == SCORE) { + String[] scoreTargets = participants.toArray(String[]::new); + if (localScore) { var ids = new java.util.LinkedHashSet(); - ids.add("s" + resolved.scoreId()); + ids.add("s" + localScoreId); java.util.Collections.addAll(ids, scoreTargets); scoreTargets = ids.toArray(String[]::new); - beatmapId = APIHelper.getScoreBeatmapId(resolved.scoreId()); - } else { - beatmapId = resolved.beatmapId(); + } var task = APIHelper.createReplayShowcaseTask(beatmapId, scoreTargets, upload); videoRenderRecord.updateRenderTask(ctx.senderUserId(), task.taskId()); diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java index c6ca0a43..34fa8df2 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java @@ -5,17 +5,19 @@ import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.command.TargetHistory; +import xyz.zcraft.seira.command.TargetLookup; +import xyz.zcraft.seira.command.parse.TargetArguments; +import xyz.zcraft.seira.command.parse.TargetResolution; +import xyz.zcraft.seira.data.UserRef; import xyz.zcraft.seira.command.TaskCoordinator; import xyz.zcraft.seira.command.parse.*; import xyz.zcraft.seira.command.reply.CommandUsage; import xyz.zcraft.seira.command.reply.ReplyFactory; -import xyz.zcraft.seira.data.UserRef; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import static xyz.zcraft.seira.command.TargetHistory.Type.SCORE; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; public final class ScoreCommandHandler { @@ -24,6 +26,7 @@ public final class ScoreCommandHandler { private final Resolver resolver; private final TargetHistory history; + private final TargetLookup targetLookup; private final TaskCoordinator taskCoordinator; private final ReplyFactory replyFactory; @@ -31,10 +34,12 @@ public ScoreCommandHandler( Resolver resolver, TargetHistory history, TaskCoordinator taskCoordinator, - ReplyFactory replyFactory + ReplyFactory replyFactory, + java.util.function.Function accessTokenProvider ) { this.resolver = resolver; this.history = history; + this.targetLookup = new TargetLookup(resolver, accessTokenProvider); this.taskCoordinator = taskCoordinator; this.replyFactory = replyFactory; } @@ -78,9 +83,8 @@ public void handleBp(Context ctx) { return; } try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - var ids = history.resolve(ctx, SCORE, new TargetResolution(target, 0)); - history.remember(ctx, ids); - String scoreId = ids.scoreId(); + String scoreId = APIHelper.lookupPlayerScore(APIHelper.resolveUid(target.userRef()), target.macroType(), target.macroIndex(), List.of(), null); + history.remember(ctx, null, null, scoreId); var response = APIHelper.getScoreResponse(scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } @@ -97,14 +101,17 @@ public void handleBp(Context ctx) { return; } - ScoreListRequest request = parseScoreListRequest(ctx, CommandUsage.BP); + var request = parseScoreListRequest(ctx, CommandUsage.BP); if (request == null) return; + var range = request.range(); + var userRef = request.userRef(); + var filters = request.filters(); try (var _ = taskCoordinator.beginRequest(ctx, "Best Scores")) { var response = APIHelper.getBoNResponse( - request.range().end(), - request.range().start(), - request.userRef(), - request.filters() + range.end(), + range.start(), + userRef, + filters.filters() ); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.bpMessage(ctx, response))); } @@ -118,9 +125,8 @@ public void handleRs(Context ctx, boolean includeFail) { return; } try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - var ids = history.resolve(ctx, SCORE, new TargetResolution(target, 0)); - history.remember(ctx, ids); - String scoreId = ids.scoreId(); + String scoreId = APIHelper.lookupPlayerScore(APIHelper.resolveUid(target.userRef()), target.macroType(), target.macroIndex(), List.of(), null); + history.remember(ctx, null, null, scoreId); var response = APIHelper.getScoreResponse(scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } @@ -137,15 +143,18 @@ public void handleRs(Context ctx, boolean includeFail) { return; } - ScoreListRequest request = parseScoreListRequest(ctx, CommandUsage.RS); + var request = parseScoreListRequest(ctx, CommandUsage.RS); if (request == null) return; + var range = request.range(); + var userRef = request.userRef(); + var filters = request.filters(); try (var _ = taskCoordinator.beginRequest(ctx, "Recent Score")) { var response = APIHelper.getRecentResponse( - request.range().end(), - request.range().start(), - request.userRef(), + range.end(), + range.start(), + userRef, includeFail, - request.filters() + filters.filters() ); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.rsMessage(ctx, response))); } @@ -217,58 +226,47 @@ private void handleFilteredSingleScore(Context ctx, String macroType) { ShortcutTarget target = new ShortcutTarget(null, targetUser, macroType, 1L, null); try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - var ids = history.resolve(ctx, SCORE, new TargetResolution(target, 0), filters.filters(), null); - history.remember(ctx, ids); - String scoreId = ids.scoreId(); + String scoreId = APIHelper.lookupPlayerScore(APIHelper.resolveUid(target.userRef()), target.macroType(), target.macroIndex(), filters.filters(), null); + history.remember(ctx, null, null, scoreId); var response = APIHelper.getScoreResponse(scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } } - private ScoreListRequest parseScoreListRequest(Context ctx, String usage) { - String[] args = ctx.args(); - ScoreListRange range = parseScoreListRange(args[0]); - if (range == null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + usage - + "\n数量或范围必须在 1 到 " + MAX_SCORE_LIST_COUNT + " 之间,且范围起点不能大于终点。")); - return null; - } - - int nextArg = 1; - UserRef userRef; - if (nextArg < args.length && (resolver.looksLikeMention(args[nextArg]) || resolver.looksLikeUid(args[nextArg]))) { - UserRefResolution resolution = resolver.resolveUserRefArgument(args[nextArg]); - if (resolution.errorMessage() != null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + resolution.errorMessage())); - return null; - } - if (resolution.userRef() == null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + usage)); - return null; - } - userRef = resolution.userRef(); - nextArg++; + public void handleS(Context ctx) { + // /s [目标] [玩家] [+Mods];只有真正消费的参数才推进下标。 + TargetResolution target; + UserRef userOverride = null; + int optionIndex; + boolean playerOnly = ctx.argumentCount() > 0 && resolver.looksLikeMention(ctx.argument(0)) + && (ctx.argumentCount() == 1 || ctx.argument(1).startsWith("+")); + if (playerOnly || ctx.argumentCount() == 0 || ctx.argument(0).startsWith("+")) { + target = new TargetResolution(null, 0); } else { - Long uid = resolver.resolveBoundUid(ctx.senderUserId()); - if (uid == null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.NO_BIND)); - return null; + target = resolver.resolveTargetWithOptionalMention(ctx.args(), ctx.senderUserId()); + if (target.target().isError()) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + target.target().errorMessage())); + return; } - userRef = new UserRef.ByUid(uid); } - - ScoreFilterArguments.ParseResult filters = ScoreFilterArguments.parse(args, nextArg); - if (filters.isError()) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + filters.errorMessage() + "\n" + CommandUsage.SCORE_FILTERS)); - return null; + optionIndex = target.consumedArgs(); + if (optionIndex < ctx.argumentCount() && !ctx.argument(optionIndex).startsWith("+")) { + var playerArgument = resolver.resolveUserRefArgument(ctx.argument(optionIndex)); + if (playerArgument.errorMessage() != null || playerArgument.userRef() == null) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + + (playerArgument.errorMessage() == null ? CommandUsage.S : playerArgument.errorMessage()))); + return; + } + userOverride = playerArgument.userRef(); + optionIndex++; } - return new ScoreListRequest(range, userRef, filters.filters()); - } - - public void handleS(Context ctx) { - var target = history.parseScoreArguments(ctx, CommandUsage.S, 1, arg -> arg.startsWith("+")); - if (target == null) return; - String option = target.nextArgument(ctx); + if ((target.target() == null && history.get(ctx) == null) + || ctx.argumentCount() - optionIndex > 1 + || (optionIndex < ctx.argumentCount() && !ctx.argument(optionIndex).startsWith("+"))) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.S)); + return; + } + String option = optionIndex < ctx.argumentCount() ? ctx.argument(optionIndex) : null; String mod = option == null ? null : option.substring(1).toUpperCase(java.util.Locale.ROOT); List filters = List.of(); if (mod != null) { @@ -280,70 +278,61 @@ public void handleS(Context ctx) { filters = parsed.filters(); } try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - var ids = history.resolve(ctx, SCORE, target, filters, mod); - history.remember(ctx, ids); - var response = APIHelper.getScoreResponse(ids.scoreId()); + var resolvedTarget = targetLookup.score(ctx, target.target(), history.get(ctx), userOverride, filters, mod); + history.remember(ctx, resolvedTarget); + String scoreId = resolvedTarget.scoreId(); + var response = APIHelper.getScoreResponse(scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } catch (Exception e) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + TaskCoordinator.resolveErrorMessage(e))); -// ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + TaskCoordinator.resolveErrorMessage(e) -// + "\n> Tips: 若要查找指定谱面上的成绩,请使用 /s __m__`bid`")); org.apache.logging.log4j.LogManager.getLogger(ScoreCommandHandler.class) .error("Failed to execute /s", e); } } public void handleSa(Context ctx) { - var target = history.parseArguments(ctx, CommandUsage.SA, 0); + var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.SA, 0); if (target == null) return; try (var _ = taskCoordinator.beginRequest(ctx, "Score Analysis")) { - var ids = history.resolve(ctx, SCORE, target); - history.remember(ctx, ids); - String scoreId = ids.scoreId(); + var resolvedTarget = targetLookup.score(ctx, target.target(), history.get(ctx)); + history.remember(ctx, resolvedTarget); + String scoreId = resolvedTarget.scoreId(); var response = APIHelper.getScoreAnalyzeResponse(scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreAnalyzeMessage(ctx, response))); } } public void handleMa(Context ctx) { - var target = history.parseArguments(ctx, CommandUsage.MA, 1, arg -> arg.startsWith("#")); + var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.MA, 1, arg -> arg.startsWith("#")); if (target == null) return; - String indexArgument = target.nextArgument(ctx); - if (indexArgument != null) { - Integer index = parseMissIndex(indexArgument, target.getConsumedArgs() == 1); - if (index == null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.MA)); - return; - } - try (var _ = taskCoordinator.beginRequest(ctx, "Miss Visualize")) { - var ids = history.resolve(ctx, SCORE, target); - history.remember(ctx, ids); - String scoreId = ids.scoreId(); - var misses = APIHelper.getScoreMissesResponse(scoreId).getContent(); - if (misses.isEmpty()) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "本成绩没有Miss喵~")); - return; - } - if (index <= 0 || index > misses.size()) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "Miss序号不在范围内喵(1~" + misses.size() + ")")); - return; - } - var response = APIHelper.getMissVisualizeResponse(scoreId, index); - ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.missImageMessage(ctx, scoreId, index, misses.size()))); - } + String indexArgument = (ctx.argumentCount() > target.consumedArgs() ? ctx.argument(target.consumedArgs()) : null); + Integer index = indexArgument == null ? null : parseMissIndex(indexArgument, target.target() == null); + if (indexArgument != null && index == null) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.MA)); return; } - try (var _ = taskCoordinator.beginRequest(ctx, "Get Score Misses")) { - var ids = history.resolve(ctx, SCORE, target); - history.remember(ctx, ids); - String scoreId = ids.scoreId(); + try (var _ = taskCoordinator.beginRequest(ctx, "Score Misses")) { + var resolvedTarget = targetLookup.score(ctx, target.target(), history.get(ctx)); + history.remember(ctx, resolvedTarget); + String scoreId = resolvedTarget.scoreId(); var response = APIHelper.getScoreMissesResponse(scoreId); - final List content = response.getContent(); - if (content.size() == 1) { - handleMa(ctx.asCommand("ma", new String[]{scoreId, "#1"}, scoreId + " #1")); - } else { + List misses = response.getContent(); + if (index == null && misses.size() != 1) { ctx.sendReply(replyFactory.scoreMissesMessage(ctx, response)); + return; + } + if (misses.isEmpty()) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "本成绩没有Miss喵~")); + return; + } + int selectedIndex = index == null ? 1 : index; + if (selectedIndex > misses.size()) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "Miss序号不在范围内喵(1~" + misses.size() + ")")); + return; } + var image = APIHelper.getMissVisualizeResponse(scoreId, selectedIndex); + ctx.sendReply(taskCoordinator.imageMessage(image, + replyFactory.missImageMessage(ctx, scoreId, selectedIndex, misses.size()))); } } @@ -357,13 +346,52 @@ private Integer parseMissIndex(String arg, boolean requirePrefix) { return resolver.parsePositiveInt(value); } - record TbArguments(int days, String target) { + private ScoreListRequest parseScoreListRequest(Context ctx, String usage) { + String[] args = ctx.args(); + ScoreListRange range = parseScoreListRange(args[0]); + if (range == null) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + usage + + "\n数量或范围必须在 1 到 " + MAX_SCORE_LIST_COUNT + " 之间,且范围起点不能大于终点。")); + return null; + } + + int nextArg = 1; + UserRef userRef; + if (nextArg < args.length && (resolver.looksLikeMention(args[nextArg]) || resolver.looksLikeUid(args[nextArg]))) { + UserRefResolution resolution = resolver.resolveUserRefArgument(args[nextArg]); + if (resolution.errorMessage() != null) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + resolution.errorMessage())); + return null; + } + if (resolution.userRef() == null) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + usage)); + return null; + } + userRef = resolution.userRef(); + nextArg++; + } else { + Long uid = resolver.resolveBoundUid(ctx.senderUserId()); + if (uid == null) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.NO_BIND)); + return null; + } + userRef = new UserRef.ByUid(uid); + } + + ScoreFilterArguments.ParseResult filters = ScoreFilterArguments.parse(args, nextArg); + if (filters.isError()) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + filters.errorMessage() + "\n" + CommandUsage.SCORE_FILTERS)); + return null; + } + return new ScoreListRequest(range, userRef, filters); } - record ScoreListRange(int start, int end) { + private record ScoreListRequest(ScoreListRange range, UserRef userRef, ScoreFilterArguments.ParseResult filters) {} + + record TbArguments(int days, String target) { } - private record ScoreListRequest(ScoreListRange range, UserRef userRef, java.util.List filters) { + record ScoreListRange(int start, int end) { } } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java index e828dcb3..e3250270 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java @@ -11,6 +11,7 @@ import xyz.zcraft.seira.command.TaskCoordinator; import xyz.zcraft.seira.command.parse.Resolver; import xyz.zcraft.seira.command.parse.ShortcutTarget; +import xyz.zcraft.seira.command.ResolutionException; import xyz.zcraft.seira.command.parse.TargetResolution; import xyz.zcraft.seira.command.parse.UserRefResolution; import xyz.zcraft.seira.command.reply.CommandUsage; @@ -296,64 +297,66 @@ public void handleLb(Context ctx) { } } else if (ctx.args().length == 1 || ctx.args().length == 2) { TargetResolution targetResolution = resolver.resolveTargetWithOptionalMention(ctx.args(), ctx.senderUserId()); - ShortcutTarget target = targetResolution.getTarget(); + ShortcutTarget target = targetResolution.target(); if (target.isError()) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + target.errorMessage())); return; } - int remainingArgs = ctx.args().length - targetResolution.getConsumedArgs(); - if (remainingArgs == 0) { - if (ctx.groupId() != null && !ctx.groupId().isBlank()) { - List groupBoundUids = UserDataStore.findBoundUidsByGroup(ctx.groupId()); - if (groupBoundUids.isEmpty()) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "本群还没有已绑定的玩家,请先使用 /bind")); + int remainingArgs = ctx.argumentCount() - targetResolution.consumedArgs(); + List uids = new LinkedList<>(); + if (remainingArgs == 1) { + String[] uidTokens = ctx.argument(targetResolution.consumedArgs()).split(","); + if (uidTokens.length == 0) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "玩家ID列表不能为空。")); + return; + } + for (String token : uidTokens) { + Long uid = resolver.parsePositiveLong(token.trim()); + if (uid == null) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "玩家ID列表包含非法值。")); return; } - try (var _ = taskCoordinator.beginRequest(ctx, "Map Leaderboard")) { - long beatmapId = APIHelper.lookupBeatmap(target, accessTokenProvider.apply(ctx.senderUserId())); - var response = APIHelper.getGroupLeaderboardResponse(beatmapId, groupBoundUids); - ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.lbMessage(ctx, response))); - } + uids.add(uid); + } + } else if (remainingArgs != 0) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "用法:/lb <谱面ID或快捷查询> [玩家ID列表(逗号分隔)]")); + return; + } else if (ctx.inGroup()) { + uids.addAll(UserDataStore.findBoundUidsByGroup(ctx.groupId())); + if (uids.isEmpty()) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "本群还没有已绑定的玩家,请先使用 /bind")); return; } + } else { Long uid = resolver.resolveBoundUid(ctx.senderUserId()); if (uid == null) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.NO_BIND)); return; } - - try (var _ = taskCoordinator.beginRequest(ctx, "Map Leaderboard")) { - long beatmapId = APIHelper.lookupBeatmap(target, accessTokenProvider.apply(ctx.senderUserId())); - var response = APIHelper.getGroupLeaderboardResponse(beatmapId, List.of(uid)); - ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.lbMessage(ctx, response))); - } - return; - } - - if (remainingArgs != 1) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "用法:/lb <谱面ID或快捷查询> [玩家ID列表(逗号分隔)]")); - return; - } - - String[] uidTokens = ctx.args()[targetResolution.getConsumedArgs()].split(","); - if (uidTokens.length == 0) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "玩家ID列表不能为空。用法:/lb <谱面ID或快捷查询> [玩家ID列表(逗号分隔)]")); - return; - } - - List uids = new LinkedList<>(); - for (String uidToken : uidTokens) { - Long uid = resolver.parsePositiveLong(uidToken.trim()); - if (uid == null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "玩家ID列表包含非法值。用法:/lb <谱面ID或快捷查询> [玩家ID列表(逗号分隔)]")); - return; - } uids.add(uid); } try (var _ = taskCoordinator.beginRequest(ctx, "Map Leaderboard")) { - long beatmapId = APIHelper.lookupBeatmap(target, accessTokenProvider.apply(ctx.senderUserId())); + long beatmapId; + if (target.isLocalScore()) { + beatmapId = APIHelper.getScoreBeatmapId(target.localScoreId()); + } else if (!target.isMacro() || "m".equals(target.macroType())) { + beatmapId = target.explicitId(); + } else { + switch (target.macroType()) { + case "s" -> beatmapId = APIHelper.getScoreBeatmapId(target.explicitId().toString()); + case "ms" -> beatmapId = APIHelper.lookupBeatmapInSet(target.explicitId(), target.macroIndex(), + accessTokenProvider.apply(ctx.senderUserId())); + case "rs", "rp", "bp" -> { + long uid = APIHelper.resolveUid(target.userRef()); + beatmapId = APIHelper.lookupPlayerScoreBeatmap(uid, target.macroType(), target.macroIndex(), accessTokenProvider.apply(ctx.senderUserId())); + + } + case "mp" -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + default -> throw new ResolutionException("未知的快捷查询"); + } + } var response = APIHelper.getGroupLeaderboardResponse(beatmapId, uids); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.lbMessage(ctx, response))); } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java index 4360163a..82186c12 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java @@ -16,7 +16,6 @@ import java.util.List; import java.util.Locale; import java.util.Objects; -import java.util.function.BiFunction; import java.util.function.Predicate; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; @@ -30,21 +29,12 @@ public final class WatchCommandHandler { private final TaskCoordinator taskCoordinator; private final ScoreWatchService watchService; private final Predicate adminAuthorizer; - private final BiFunction targetResolver; public WatchCommandHandler(Resolver resolver, TaskCoordinator taskCoordinator, ScoreWatchService watchService, Predicate adminAuthorizer) { this.resolver = Objects.requireNonNull(resolver); this.taskCoordinator = Objects.requireNonNull(taskCoordinator); this.watchService = watchService; this.adminAuthorizer = Objects.requireNonNull(adminAuthorizer); - this.targetResolver = this::resolveTarget; - } - - private static User findUserById(long userId) { - return APIHelper.getUsers(List.of(userId)).stream() - .filter(user -> user.getId() == userId) - .findFirst() - .orElseThrow(() -> new ResolutionException("未找到指定的玩家。")); } private static PendingMessage removedMessage(WatchView removed) { @@ -135,7 +125,22 @@ private void handleAdd(Context ctx) { String targetArgument = ctx.argument(1); try (var timing = taskCoordinator.beginRequest(ctx, "Add Score Watch")) { - WatchTarget target = targetResolver.apply(ctx.groupId(), targetArgument); + String mentionedUser = resolver.extractMentionedUserId(targetArgument); + WatchTarget target; + if (mentionedUser != null) { + if (!UserDataStore.isGroupMember(ctx.groupId(), mentionedUser)) { + throw new ResolutionException("指定的用户不在当前群聊中。"); + } + Long userId = UserDataStore.findBoundUid(mentionedUser); + if (userId == null) throw new ResolutionException("被@的用户还没有绑定玩家ID,请先让对方使用 /bind。"); + User user = APIHelper.getUsers(List.of(userId)).stream() + .filter(candidate -> candidate.getId() == userId) + .findFirst().orElseThrow(() -> new ResolutionException("未找到指定的玩家。")); + UserDataStore.storeUserInfo(user.getId(), user.getUsername()); + target = new WatchTarget(user.getId(), user.getUsername(), mentionedUser); + } else { + target = lookupGroupPlayer(ctx.groupId(), targetArgument); + } final boolean b = ctx.sendMessage(PendingMessage.ofMarkdownRaw( at(ctx) + "正在尝试添加监视..." )).success(); @@ -179,11 +184,23 @@ private void handleDelete(Context ctx) { } try (var timing = taskCoordinator.beginRequest(ctx, "Delete Score Watch")) { - WatchTarget target = resolveTarget(ctx.groupId(), targetArgument); + WatchTarget target = lookupGroupPlayer(ctx.groupId(), targetArgument); ctx.sendReply(removedMessage(watchService.remove(ctx.groupId(), target.userId()))); } } + private WatchTarget lookupGroupPlayer(String groupId, String argument) { + Long uid = resolver.parsePositiveLong(argument); + User user = uid == null ? APIHelper.lookupUser(argument).getContent() + : APIHelper.getUsers(List.of(uid)).stream() + .filter(candidate -> candidate.getId() == uid) + .findFirst().orElseThrow(() -> new ResolutionException("未找到指定的玩家。")); + String openId = UserDataStore.findGroupOpenIdByUid(groupId, user.getId()) + .orElseThrow(() -> new ResolutionException("指定的玩家不在当前群聊中,或尚未在本群完成绑定。")); + UserDataStore.storeUserInfo(user.getId(), user.getUsername()); + return new WatchTarget(user.getId(), user.getUsername(), openId); + } + private void handleList(Context ctx) { if (ctx.argumentCount() != 1) { usage(ctx); @@ -206,28 +223,4 @@ private void handleList(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(content.toString().trim())); } - private WatchTarget resolveTarget(String groupId, String argument) { - String mentionedOpenId = resolver.extractMentionedUserId(argument); - if (mentionedOpenId != null) { - if (!UserDataStore.isGroupMember(groupId, mentionedOpenId)) { - throw new ResolutionException("指定的用户不在当前群聊中。"); - } - Long userId = UserDataStore.findBoundUid(mentionedOpenId); - if (userId == null) { - throw new ResolutionException("被@的用户还没有绑定玩家ID,请先让对方使用 /bind。"); - } - User user = findUserById(userId); - UserDataStore.storeUserInfo(user.getId(), user.getUsername()); - return new WatchTarget(user.getId(), user.getUsername(), mentionedOpenId); - } - - Long explicitUserId = resolver.parsePositiveLong(argument); - User user = explicitUserId == null - ? APIHelper.lookupUser(argument).getContent() - : findUserById(explicitUserId); - String qqOpenId = UserDataStore.findGroupOpenIdByUid(groupId, user.getId()) - .orElseThrow(() -> new ResolutionException("指定的玩家不在当前群聊中,或尚未在本群完成绑定。")); - UserDataStore.storeUserInfo(user.getId(), user.getUsername()); - return new WatchTarget(user.getId(), user.getUsername(), qqOpenId); - } } diff --git a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java index 9197068d..63d00ce5 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java @@ -139,47 +139,6 @@ public UserRefResolution resolveUserRefArgument(String arg) { return new UserRefResolution(new UserRef.ByUsername(username), null); } - public RscTarget resolveRscTarget(String groupId, String extraUidArg) { - Set merged = new LinkedHashSet<>(); - - if (extraUidArg == null || extraUidArg.trim().startsWith("+")) { - List groupBoundUids = UserDataStore.findBoundUidsByGroup(groupId); - if (groupBoundUids.isEmpty()) { - return new RscTarget(null, "本群还没有已绑定的玩家,请先使用 /bind"); - } - groupBoundUids.stream().map(String::valueOf).forEach(merged::add); - } - - if (extraUidArg == null) return new RscTarget(merged.toArray(String[]::new), null); - - String trimmed = extraUidArg.trim(); - String body = trimmed.substring(1).trim(); - if (body.isEmpty()) { - return new RscTarget(null, "追加ID列表不能为空。"); - } - - String[] extraTokens = body.split(","); - for (String token : extraTokens) { - if (Patterns.RSC_TARGET_PATTERN.matcher(token.trim()).matches()) { - merged.add(token); - } else if (looksLikeMention(token)) { - final UserRefResolution userRefResolution = resolveUserRefArgument(token); - if (userRefResolution.errorMessage() != null) { - return new RscTarget(null, "解析 " + token + " 时出错:" + userRefResolution.errorMessage()); - } - if (userRefResolution.userRef() instanceof UserRef.ByUid ref) { - merged.add("u" + ref.getUid()); - } else if (userRefResolution.userRef() instanceof UserRef.ByUsername ref) { - merged.add("@" + ref.getUsername()); - } - } else { - return new RscTarget(null, "追加ID列表包含非法值。"); - } - } - - return new RscTarget(merged.toArray(String[]::new), null); - } - public Long resolveBoundUid(String senderUserId) { if (senderUserId == null || senderUserId.isBlank()) { return null; @@ -337,7 +296,6 @@ private static final class Patterns { private static final Pattern QQ_INLINE_AT_PATTERN = Pattern.compile("(<@[A-Z|0-9]{32}>)"); private static final Pattern PLAIN_AT_PATTERN = Pattern.compile("^@(\\d+)$"); private static final Pattern SEARCH_PATTERN = Pattern.compile("^(?:#(\\d+) )?(.+)$"); - private static final Pattern RSC_TARGET_PATTERN = Pattern.compile("^[us]?\\d+$"); } } diff --git a/src/main/java/xyz/zcraft/seira/command/parse/RscTarget.java b/src/main/java/xyz/zcraft/seira/command/parse/RscTarget.java deleted file mode 100644 index 9193b455..00000000 --- a/src/main/java/xyz/zcraft/seira/command/parse/RscTarget.java +++ /dev/null @@ -1,4 +0,0 @@ -package xyz.zcraft.seira.command.parse; - -public record RscTarget(String[] targets, String errorMessage) { -} diff --git a/src/main/java/xyz/zcraft/seira/command/parse/TargetArguments.java b/src/main/java/xyz/zcraft/seira/command/parse/TargetArguments.java new file mode 100644 index 00000000..2c4d898e --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/command/parse/TargetArguments.java @@ -0,0 +1,35 @@ +package xyz.zcraft.seira.command.parse; + +import xyz.zcraft.seira.bot.data.PendingMessage; +import xyz.zcraft.seira.command.Context; +import xyz.zcraft.seira.command.TargetHistory; + +import java.util.function.Predicate; + +import static xyz.zcraft.seira.command.reply.ReplyFactory.at; + +public final class TargetArguments { + private TargetArguments() {} + + public static TargetResolution parse(Context ctx, Resolver resolver, TargetHistory.Ids previous, + String usage, int maxOptions) { + return parse(ctx, resolver, previous, usage, maxOptions, _ -> false); + } + + public static TargetResolution parse(Context ctx, Resolver resolver, TargetHistory.Ids previous, + String usage, int maxOptions, Predicate option) { + TargetResolution target = ctx.argumentCount() == 0 || option.test(ctx.argument(0)) + ? new TargetResolution(null, 0) + : resolver.resolveTargetWithOptionalMention(ctx.args(), ctx.senderUserId()); + if (target.target() != null && target.target().isError()) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + target.target().errorMessage())); + return null; + } + if ((target.target() == null && previous == null) + || ctx.argumentCount() - target.consumedArgs() > maxOptions) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + usage)); + return null; + } + return target; + } +} diff --git a/src/main/java/xyz/zcraft/seira/command/parse/TargetResolution.java b/src/main/java/xyz/zcraft/seira/command/parse/TargetResolution.java index 80adae9e..d41f192c 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/TargetResolution.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/TargetResolution.java @@ -1,35 +1,4 @@ package xyz.zcraft.seira.command.parse; -import lombok.Getter; -import xyz.zcraft.seira.command.Context; -import xyz.zcraft.seira.data.UserRef; - -/** - * target 为 null 表示省略目标;consumedArgs 之后是指令自己的可选参数。 - */ -@Getter -public final class TargetResolution { - private final ShortcutTarget target; - private final UserRef userOverride; - private int consumedArgs; - - public TargetResolution(ShortcutTarget target, int consumedArgs, UserRef userOverride) { - this.target = target; - this.consumedArgs = consumedArgs; - this.userOverride = userOverride; - } - - public TargetResolution(ShortcutTarget target, int consumedArgs) { - this(target, consumedArgs, null); - } - - public String nextArgument(Context ctx) { - final String s = ctx.argumentCount() > consumedArgs ? ctx.argument(consumedArgs) : null; - consumedArgs++; - return s; - } - - public boolean hasRemaining(Context ctx) { - return ctx.argumentCount() > consumedArgs; - } -} +/** 仅保存语法解析结果。target 为 null 表示省略目标;consumedArgs 是选项起点,不会随读取改变。 */ +public record TargetResolution(ShortcutTarget target, int consumedArgs) {} diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index 1e4c0242..c300193a 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -63,14 +63,14 @@ public Router( AppConfig startupConfig = configSupplier.get(); ReplyFactory replyFactory = new ReplyFactory(configSupplier); Resolver resolver = new Resolver(); - TargetHistory history = new TargetHistory(resolver, this::getAccessTokenFor); + TargetHistory history = new TargetHistory(); ReplayResultStore replayResults = new ReplayResultStore(); VideoRenderRecord videoRenderRecord = new VideoRenderRecord(); this.taskCoordinator = new TaskCoordinator(messageSender, replayResults, discordBridgeService); this.authHelper = new OsuAuthHelper(startupConfig.binding()); BindingCommandHandler bindingCommands = new BindingCommandHandler(startupConfig, replyFactory, bindingService); ScoreCommandHandler scoreCommands = new ScoreCommandHandler( - resolver, history, taskCoordinator, replyFactory + resolver, history, taskCoordinator, replyFactory, this::getAccessTokenFor ); BeatmapCommandHandler beatmapCommands = new BeatmapCommandHandler( resolver, history, taskCoordinator, replyFactory, videoRenderRecord, this::getAccessTokenFor @@ -79,7 +79,7 @@ public Router( resolver, authHelper, taskCoordinator, replyFactory, this::getAccessTokenFor, this::getAvatar ); ReplayCommandHandler replayCommands = new ReplayCommandHandler( - resolver, history, taskCoordinator, replyFactory, videoRenderRecord, replayResults, admins::isAdmin + resolver, history, taskCoordinator, replyFactory, videoRenderRecord, replayResults, admins::isAdmin, this::getAccessTokenFor ); GeneralCommandHandler generalCommands = new GeneralCommandHandler( messageSender, taskCoordinator, replyFactory, resolver, admins::isAdmin From b8d96aa8c092e79390d464f4582af0a32d5cd4de Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sat, 19 Sep 2026 23:22:59 +0800 Subject: [PATCH 17/96] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AF=B9=20/sm=20?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E7=9A=84=E6=94=AF=E6=8C=81=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=88=90=E7=BB=A9=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zcraft/seira/command/TargetHistory.java | 2 -- .../zcraft/seira/command/TargetLookup.java | 11 +++++++++- .../command/handler/ScoreCommandHandler.java | 20 ++++++++++++++----- .../zcraft/seira/command/parse/Resolver.java | 6 +----- .../seira/command/reply/CommandUsage.java | 1 + .../zcraft/seira/command/route/Router.java | 1 + 6 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/TargetHistory.java b/src/main/java/xyz/zcraft/seira/command/TargetHistory.java index affff5eb..3aff3f6e 100644 --- a/src/main/java/xyz/zcraft/seira/command/TargetHistory.java +++ b/src/main/java/xyz/zcraft/seira/command/TargetHistory.java @@ -3,7 +3,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -/** Per-sender target memory. No parsing, binding lookup, API calls or replies. */ public final class TargetHistory { private final ConcurrentMap users = new ConcurrentHashMap<>(); @@ -19,6 +18,5 @@ public void remember(Context ctx, Ids ids) { users.put(ctx.senderUserId(), ids); } - /** Immutable snapshot: IDs belong to the same target, and missing associations remain null. */ public record Ids(Long beatmapsetId, Long beatmapId, String scoreId) {} } diff --git a/src/main/java/xyz/zcraft/seira/command/TargetLookup.java b/src/main/java/xyz/zcraft/seira/command/TargetLookup.java index de08ede4..244ec8dd 100644 --- a/src/main/java/xyz/zcraft/seira/command/TargetLookup.java +++ b/src/main/java/xyz/zcraft/seira/command/TargetLookup.java @@ -34,6 +34,15 @@ public TargetHistory.Ids score(Context ctx, ShortcutTarget input, TargetHistory. return lookup(ctx, input, previous, Type.SCORE, userOverride, filters, mod); } + public TargetHistory.Ids scoreOnBeatmap(Context ctx, ShortcutTarget input, TargetHistory.Ids previous, + UserRef player, List filters, String mod) { + UserRef targetPlayer = requirePlayer(ctx, player); + var map = beatmap(ctx, input, previous); + String scoreId = APIHelper.lookupBeatmapScore(map.beatmapId(), APIHelper.resolveUid(targetPlayer), filters, mod); + return new TargetHistory.Ids(map.beatmapsetId(), map.beatmapId(), scoreId); + } + + private TargetHistory.Ids lookup(Context ctx, ShortcutTarget input, TargetHistory.Ids previous, Type type, UserRef userOverride, List filters, String mod) { if (input != null) previous = null; @@ -120,5 +129,5 @@ private UserRef requirePlayer(Context ctx, UserRef player) { return new UserRef.ByUid(uid); } - private enum Type { BEATMAP, BEATMAPSET, SCORE } + private enum Type {BEATMAP, BEATMAPSET, SCORE} } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java index 34fa8df2..dcf3d539 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java @@ -234,7 +234,15 @@ private void handleFilteredSingleScore(Context ctx, String macroType) { } public void handleS(Context ctx) { - // /s [目标] [玩家] [+Mods];只有真正消费的参数才推进下标。 + handleScore(ctx, false); + } + + public void handleSm(Context ctx) { + handleScore(ctx, true); + } + + private void handleScore(Context ctx, boolean onBeatmap) { + String usage = onBeatmap ? CommandUsage.SM : CommandUsage.S; TargetResolution target; UserRef userOverride = null; int optionIndex; @@ -254,7 +262,7 @@ public void handleS(Context ctx) { var playerArgument = resolver.resolveUserRefArgument(ctx.argument(optionIndex)); if (playerArgument.errorMessage() != null || playerArgument.userRef() == null) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) - + (playerArgument.errorMessage() == null ? CommandUsage.S : playerArgument.errorMessage()))); + + (playerArgument.errorMessage() == null ? usage : playerArgument.errorMessage()))); return; } userOverride = playerArgument.userRef(); @@ -263,7 +271,7 @@ public void handleS(Context ctx) { if ((target.target() == null && history.get(ctx) == null) || ctx.argumentCount() - optionIndex > 1 || (optionIndex < ctx.argumentCount() && !ctx.argument(optionIndex).startsWith("+"))) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.S)); + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + usage)); return; } String option = optionIndex < ctx.argumentCount() ? ctx.argument(optionIndex) : null; @@ -278,7 +286,9 @@ public void handleS(Context ctx) { filters = parsed.filters(); } try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - var resolvedTarget = targetLookup.score(ctx, target.target(), history.get(ctx), userOverride, filters, mod); + var resolvedTarget = onBeatmap + ? targetLookup.scoreOnBeatmap(ctx, target.target(), history.get(ctx), userOverride, filters, mod) + : targetLookup.score(ctx, target.target(), history.get(ctx), userOverride, filters, mod); history.remember(ctx, resolvedTarget); String scoreId = resolvedTarget.scoreId(); var response = APIHelper.getScoreResponse(scoreId); @@ -286,7 +296,7 @@ public void handleS(Context ctx) { } catch (Exception e) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + TaskCoordinator.resolveErrorMessage(e))); org.apache.logging.log4j.LogManager.getLogger(ScoreCommandHandler.class) - .error("Failed to execute /s", e); + .error("Failed to execute /{}", ctx.command(), e); } } diff --git a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java index 63d00ce5..119ea34b 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java @@ -46,11 +46,7 @@ public String sanitize(String rawContent) { String command = matcher.group(1).toLowerCase(Locale.ROOT); String target = matcher.group(2); String remaining = rawContent.substring(matcher.end()); - if (command.equals("sm")) { - rawContent = "s m" + target + " " + remaining; - } else { - rawContent = command + " " + target + " " + remaining; - } + rawContent = command + " " + target + " " + remaining; } return rawContent; diff --git a/src/main/java/xyz/zcraft/seira/command/reply/CommandUsage.java b/src/main/java/xyz/zcraft/seira/command/reply/CommandUsage.java index a69468d6..809aa7b4 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/CommandUsage.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/CommandUsage.java @@ -14,6 +14,7 @@ public final class CommandUsage { public static final String BGP = "用法:/bgp <谱面ID 或 快捷查询>"; public static final String DL = "用法:/dl <谱面集ID 或 快捷查询>"; public static final String S = "用法:/s [成绩ID 或 快捷查询] [用户] [+Mod](省略目标时使用记忆)"; + public static final String SM = "用法:/sm [谱面ID 或 快捷查询] [@玩家] [+Mod](省略目标时使用记忆,省略玩家时使用自己的绑定账号)"; public static final String SA = "用法:/sa <成绩ID 或 快捷查询>"; public static final String MA = "用法:/ma [成绩ID 或 快捷查询] [序号];省略目标并指定序号时请使用 #序号"; public static final String R = "用法:/r [成绩ID 或 快捷查询] [[mm:ss]-[mm:ss]]"; diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index c300193a..c710c934 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -140,6 +140,7 @@ private static CommandRegistry createCommandRegistry( .register(socialCommands::handleFclear, "fclear") .register(beatmapCommands::handleDl, "dl") .register(scoreCommands::handleS, "s") + .register(scoreCommands::handleSm, "sm") .register(scoreCommands::handleSa, "sa") .register(scoreCommands::handleMa, "ma") .register(replayCommands::handleR, "r") From 089c32548ba2e1d279ca4c43d811630e1187cdc3 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 20 Sep 2026 01:01:21 +0800 Subject: [PATCH 18/96] =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A7=BB=E9=99=A4=20UserRef?= =?UTF-8?q?=E3=80=81UserRefResolution=E3=80=81TargetResolution=20=E5=92=8C?= =?UTF-8?q?=20ShortcutTarget=20=E7=B1=BB=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=9B=AE=E6=A0=87=E8=A7=A3=E6=9E=90=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/xyz/zcraft/seira/api/APIHelper.java | 53 ++- .../zcraft/seira/command/TargetLookup.java | 133 ------- .../handler/BeatmapCommandHandler.java | 255 ++++++++++--- .../handler/GeneralCommandHandler.java | 23 +- .../handler/RankGuessCommandHandler.java | 15 +- .../command/handler/ReplayCommandHandler.java | 100 ++++-- .../command/handler/ScoreCommandHandler.java | 340 +++++++++++------- .../command/handler/SocialCommandHandler.java | 91 ++--- .../zcraft/seira/command/parse/Resolver.java | 169 +-------- .../seira/command/parse/ShortcutTarget.java | 36 -- .../seira/command/parse/TargetArguments.java | 35 -- .../seira/command/parse/TargetInput.java | 58 +++ .../seira/command/parse/TargetResolution.java | 4 - .../command/parse/UserRefResolution.java | 6 - .../java/xyz/zcraft/seira/data/UserRef.java | 26 -- 15 files changed, 634 insertions(+), 710 deletions(-) delete mode 100644 src/main/java/xyz/zcraft/seira/command/TargetLookup.java delete mode 100644 src/main/java/xyz/zcraft/seira/command/parse/ShortcutTarget.java delete mode 100644 src/main/java/xyz/zcraft/seira/command/parse/TargetArguments.java create mode 100644 src/main/java/xyz/zcraft/seira/command/parse/TargetInput.java delete mode 100644 src/main/java/xyz/zcraft/seira/command/parse/TargetResolution.java delete mode 100644 src/main/java/xyz/zcraft/seira/command/parse/UserRefResolution.java delete mode 100644 src/main/java/xyz/zcraft/seira/data/UserRef.java diff --git a/src/main/java/xyz/zcraft/seira/api/APIHelper.java b/src/main/java/xyz/zcraft/seira/api/APIHelper.java index 6ce32ce7..ef61f247 100644 --- a/src/main/java/xyz/zcraft/seira/api/APIHelper.java +++ b/src/main/java/xyz/zcraft/seira/api/APIHelper.java @@ -11,7 +11,6 @@ import xyz.zcraft.seira.api.data.*; import xyz.zcraft.seira.bot.data.FileInfo; import xyz.zcraft.seira.command.ResolutionException; -import xyz.zcraft.seira.data.UserRef; import xyz.zcraft.seira.util.TimeDurationParser; import java.io.IOException; @@ -96,16 +95,15 @@ public static Response getSelf(String accessToken) { } @SuppressWarnings("unused") - public static Response getBoNResponse(int n, UserRef userRef) { - return getBoNResponse(n, userRef, List.of()); + public static Response getBoNResponse(int n, long uid) { + return getBoNResponse(n, uid, List.of()); } - public static Response getBoNResponse(int n, UserRef userRef, List filters) { - return getBoNResponse(n, 1, userRef, filters); + public static Response getBoNResponse(int n, long uid, List filters) { + return getBoNResponse(n, 1, uid, filters); } - public static Response getBoNResponse(int n, int start, UserRef userRef, List filters) { - long uid = resolveUid(userRef); + public static Response getBoNResponse(int n, int start, long uid, List filters) { return getBase64BytesResponse( "/users/" + uid + "/scores/bestof?n=" + n + encodeScoreRangeStart(start) + encodeScoreFilters(filters), "获取最好成绩失败", @@ -113,8 +111,7 @@ public static Response getBoNResponse(int n, int start, UserRef use ); } - public static Response getUserInfoResponse(UserRef userRef) { - long uid = resolveUid(userRef); + public static Response getUserInfoResponse(long uid) { return getBase64BytesResponse( "/users/" + uid, "获取玩家资料失败", @@ -122,8 +119,7 @@ public static Response getUserInfoResponse(UserRef userRef) { ); } - public static UserExtended getUserRaw(UserRef userRef) { - long uid = resolveUid(userRef); + public static UserExtended getUserRaw(long uid) { try { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(ENDPOINT + "/users/" + uid)) @@ -148,12 +144,11 @@ public static UserExtended getUserRaw(UserRef userRef) { } @SuppressWarnings("unused") - public static Response getTodayBestResponse(UserRef userRef) { - return getTodayBestResponse(userRef, 1); + public static Response getTodayBestResponse(long uid) { + return getTodayBestResponse(uid, 1); } - public static Response getTodayBestResponse(UserRef userRef, int days) { - long uid = resolveUid(userRef); + public static Response getTodayBestResponse(long uid, int days) { return getBase64BytesResponse( "/users/" + uid + "/scores/today-best?days=" + days, "获取近期BP失败", @@ -244,22 +239,21 @@ public static Response getMultiplayerRoom(String accessToken) { } @SuppressWarnings("unused") - public static Response getRecentResponse(int n, UserRef userRef, boolean includeFail) { - return getRecentResponse(n, userRef, includeFail, List.of()); + public static Response getRecentResponse(int n, long uid, boolean includeFail) { + return getRecentResponse(n, uid, includeFail, List.of()); } - public static Response getRecentResponse(int n, UserRef userRef, boolean includeFail, List filters) { - return getRecentResponse(n, 1, userRef, includeFail, filters); + public static Response getRecentResponse(int n, long uid, boolean includeFail, List filters) { + return getRecentResponse(n, 1, uid, includeFail, filters); } public static Response getRecentResponse( int n, int start, - UserRef userRef, + long uid, boolean includeFail, List filters ) { - long uid = resolveUid(userRef); return getBase64BytesResponse( "/users/" + uid + "/scores/recent?n=" + n + "&fail=" + includeFail + encodeScoreRangeStart(start) + encodeScoreFilters(filters), @@ -1020,18 +1014,17 @@ public static Response> getScoreMissesResponse(String scoreId) { } } - public static long resolveUid(UserRef userRef) { - if (userRef instanceof UserRef.ByUid byUid) { - return byUid.getUid(); - } - if (userRef instanceof UserRef.ByUsername byUsername) { - return lookupUser(byUsername.getUsername()).getContent().getId(); + public static long resolveUid(String player) { + if (player == null || player.isBlank()) throw new ResolutionException("无法识别指定的玩家"); + try { + long uid = Long.parseLong(player); + if (uid > 0) return uid; + } catch (NumberFormatException ignored) { } - throw new ResolutionException("无法识别指定的玩家"); + return lookupUser(player).getContent().getId(); } - public static long getUserRank(UserRef userRef) { - long uid = resolveUid(userRef); + public static long getUserRank(long uid) { try { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(ENDPOINT + "/users/" + uid + "/rank")) diff --git a/src/main/java/xyz/zcraft/seira/command/TargetLookup.java b/src/main/java/xyz/zcraft/seira/command/TargetLookup.java deleted file mode 100644 index 244ec8dd..00000000 --- a/src/main/java/xyz/zcraft/seira/command/TargetLookup.java +++ /dev/null @@ -1,133 +0,0 @@ -package xyz.zcraft.seira.command; - -import xyz.zcraft.seira.api.APIHelper; -import xyz.zcraft.seira.command.parse.Resolver; -import xyz.zcraft.seira.command.parse.ShortcutTarget; -import xyz.zcraft.seira.data.UserRef; - -import java.util.List; -import java.util.function.Function; - -public final class TargetLookup { - private final Resolver resolver; - private final Function accessTokenProvider; - - public TargetLookup(Resolver resolver, Function accessTokenProvider) { - this.resolver = resolver; - this.accessTokenProvider = accessTokenProvider; - } - - public TargetHistory.Ids beatmap(Context ctx, ShortcutTarget input, TargetHistory.Ids previous) { - return lookup(ctx, input, previous, Type.BEATMAP, null, List.of(), null); - } - - public TargetHistory.Ids beatmapset(Context ctx, ShortcutTarget input, TargetHistory.Ids previous) { - return lookup(ctx, input, previous, Type.BEATMAPSET, null, List.of(), null); - } - - public TargetHistory.Ids score(Context ctx, ShortcutTarget input, TargetHistory.Ids previous) { - return score(ctx, input, previous, null, List.of(), null); - } - - public TargetHistory.Ids score(Context ctx, ShortcutTarget input, TargetHistory.Ids previous, - UserRef userOverride, List filters, String mod) { - return lookup(ctx, input, previous, Type.SCORE, userOverride, filters, mod); - } - - public TargetHistory.Ids scoreOnBeatmap(Context ctx, ShortcutTarget input, TargetHistory.Ids previous, - UserRef player, List filters, String mod) { - UserRef targetPlayer = requirePlayer(ctx, player); - var map = beatmap(ctx, input, previous); - String scoreId = APIHelper.lookupBeatmapScore(map.beatmapId(), APIHelper.resolveUid(targetPlayer), filters, mod); - return new TargetHistory.Ids(map.beatmapsetId(), map.beatmapId(), scoreId); - } - - - private TargetHistory.Ids lookup(Context ctx, ShortcutTarget input, TargetHistory.Ids previous, - Type type, UserRef userOverride, List filters, String mod) { - if (input != null) previous = null; - Long beatmapId = previous == null ? null : previous.beatmapId(); - Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); - String scoreId = previous == null ? null : previous.scoreId(); - UserRef player = userOverride != null ? userOverride : input == null ? null : input.userRef(); - boolean selectedPlayerScore = false; - if (input != null) { - if (input.isError()) throw new ResolutionException(input.errorMessage()); - if (input.isLocalScore()) { - scoreId = input.localScoreId(); - } else if (!input.isMacro()) { - switch (type) { - case BEATMAP -> beatmapId = input.explicitId(); - case BEATMAPSET -> beatmapsetId = input.explicitId(); - case SCORE -> scoreId = input.explicitId().toString(); - } - } else { - switch (input.macroType()) { - case "m" -> beatmapId = input.explicitId(); - case "s" -> scoreId = input.explicitId().toString(); - case "ms" -> { - beatmapsetId = input.explicitId(); - if (type != Type.BEATMAPSET) { - if (input.macroIndex() == null) throw new ResolutionException("请指定指令目标谱面喵"); - if (type == Type.BEATMAP) { - beatmapId = APIHelper.lookupBeatmapInSet(beatmapsetId, input.macroIndex(), accessTokenProvider.apply(ctx.senderUserId())); - } else { - player = requirePlayer(ctx, player); - scoreId = APIHelper.lookupBeatmapsetScore(beatmapsetId, input.macroIndex(), APIHelper.resolveUid(player), filters, mod); - selectedPlayerScore = true; - } - } - } - case "rs", "rp", "bp" -> { - player = requirePlayer(ctx, player); - scoreId = APIHelper.lookupPlayerScore(APIHelper.resolveUid(player), input.macroType(), input.macroIndex(), filters, mod); - selectedPlayerScore = true; - } - case "mp" -> { - String token = accessTokenProvider.apply(ctx.senderUserId()); - if (type == Type.BEATMAPSET) beatmapsetId = APIHelper.lookupMultiplayerBeatmapset(token); - else beatmapId = APIHelper.lookupMultiplayerBeatmap(token); - } - default -> throw new ResolutionException("未知的快捷查询"); - } - } - } - - if (type == Type.SCORE && (userOverride != null || mod != null) && scoreId != null && !selectedPlayerScore) { - if (beatmapId == null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); - scoreId = null; - } - switch (type) { - case BEATMAP -> { - if (beatmapId == null) { - if (scoreId == null) throw new ResolutionException("请指定指令目标谱面喵"); - beatmapId = APIHelper.getScoreBeatmapId(scoreId); - } - } - case BEATMAPSET -> { - if (beatmapsetId == null) { - if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); - if (beatmapId == null) throw new ResolutionException("请指定指令目标喵"); - beatmapsetId = APIHelper.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); - } - } - case SCORE -> { - if (scoreId == null) { - if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); - player = requirePlayer(ctx, player); - scoreId = APIHelper.lookupBeatmapScore(beatmapId, APIHelper.resolveUid(player), filters, mod); - } - } - } - return new TargetHistory.Ids(beatmapsetId, beatmapId, scoreId); - } - - private UserRef requirePlayer(Context ctx, UserRef player) { - if (player != null) return player; - Long uid = resolver.resolveBoundUid(ctx.senderUserId()); - if (uid == null) throw new ResolutionException("请先绑定 osu! 账号,再查找记忆谱面上的成绩喵"); - return new UserRef.ByUid(uid); - } - - private enum Type {BEATMAP, BEATMAPSET, SCORE} -} diff --git a/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java index cfe468ec..967cb5dc 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java @@ -8,9 +8,9 @@ import xyz.zcraft.seira.api.data.VideoRenderRecord; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; +import xyz.zcraft.seira.command.ResolutionException; +import xyz.zcraft.seira.command.parse.TargetInput; import xyz.zcraft.seira.command.TargetHistory; -import xyz.zcraft.seira.command.TargetLookup; -import xyz.zcraft.seira.command.parse.TargetArguments; import xyz.zcraft.seira.command.TaskCoordinator; import xyz.zcraft.seira.command.parse.Resolver; import xyz.zcraft.seira.command.reply.CommandUsage; @@ -26,7 +26,6 @@ public final class BeatmapCommandHandler { private final Resolver resolver; private final TargetHistory history; - private final TargetLookup targetLookup; private final TaskCoordinator taskCoordinator; private final ReplyFactory replyFactory; private final VideoRenderRecord videoRenderRecord; @@ -42,7 +41,6 @@ public BeatmapCommandHandler( ) { this.resolver = resolver; this.history = history; - this.targetLookup = new TargetLookup(resolver, accessTokenProvider); this.taskCoordinator = taskCoordinator; this.replyFactory = replyFactory; this.videoRenderRecord = videoRenderRecord; @@ -57,44 +55,124 @@ public void handleDaily(Context ctx) { } public void handleM(Context ctx) { - var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.M, 1); - if (target == null) return; + var target = ctx.argumentCount() == 0 + ? TargetInput.memory() : TargetInput.read(ctx.args()); + var remembered = history.get(ctx); + if ((target.kind() == TargetInput.Kind.MEMORY && remembered == null) + || ctx.argumentCount() - target.consumedArgs() > 1) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.M)); + return; + } try (var _ = taskCoordinator.beginRequest(ctx, "Beatmap")) { - var resolvedTarget = targetLookup.beatmap(ctx, target.target(), history.get(ctx)); - history.remember(ctx, resolvedTarget); - long beatmapId = resolvedTarget.beatmapId(); + var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; + Long beatmapId = previous == null ? null : previous.beatmapId(); + Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); + String scoreId = previous == null ? null : previous.scoreId(); + switch (target.kind()) { + case ID, MAP -> beatmapId = Long.parseLong(target.id()); + case SCORE -> scoreId = target.id(); + case SET -> { + beatmapsetId = Long.parseLong(target.id()); + beatmapId = APIHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + } + case RS, RP, BP -> { + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + } + case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MEMORY -> {} + } + if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); var response = APIHelper.getBeatmapResponse(beatmapId, (ctx.argumentCount() > target.consumedArgs() ? ctx.argument(target.consumedArgs()) : null)); + history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.beatmapMessage(ctx, response))); } } public void handleBma(Context ctx) { - var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.BMA, 1); - if (target == null) return; + var target = ctx.argumentCount() == 0 + ? TargetInput.memory() : TargetInput.read(ctx.args()); + var remembered = history.get(ctx); + if ((target.kind() == TargetInput.Kind.MEMORY && remembered == null) + || ctx.argumentCount() - target.consumedArgs() > 1) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.BMA)); + return; + } try (var timing = taskCoordinator.beginRequest(ctx, "Beatmap Analysis")) { - var resolvedTarget = targetLookup.beatmap(ctx, target.target(), history.get(ctx)); - history.remember(ctx, resolvedTarget); - long beatmapId = resolvedTarget.beatmapId(); + var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; + Long beatmapId = previous == null ? null : previous.beatmapId(); + Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); + String scoreId = previous == null ? null : previous.scoreId(); + switch (target.kind()) { + case ID, MAP -> beatmapId = Long.parseLong(target.id()); + case SCORE -> scoreId = target.id(); + case SET -> { + beatmapsetId = Long.parseLong(target.id()); + beatmapId = APIHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + } + case RS, RP, BP -> { + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + } + case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MEMORY -> {} + } + if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); var response = APIHelper.getBeatmapAnalysisResponse(beatmapId, (ctx.argumentCount() > target.consumedArgs() ? ctx.argument(target.consumedArgs()) : null)); + history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.beatmapMessage(ctx, response))); } } public void handleAp(Context ctx) { - var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.AP, Integer.MAX_VALUE); - if (target == null) return; + var target = ctx.argumentCount() == 0 + ? TargetInput.memory() : TargetInput.read(ctx.args()); + var remembered = history.get(ctx); + if (target.kind() == TargetInput.Kind.MEMORY && remembered == null) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.AP)); + return; + } try (var timing = taskCoordinator.beginRequest(ctx, "Audio Preview")) { - var resolvedTarget = targetLookup.beatmapset(ctx, target.target(), history.get(ctx)); - history.remember(ctx, resolvedTarget); - long beatmapsetId = resolvedTarget.beatmapsetId(); - long id = beatmapsetId; - ctx.sendReply(PendingMessage.ofVoiceUrl("https://b.ppy.sh/preview/" + id + ".mp3").doUpload(false)); + var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; + Long beatmapId = previous == null ? null : previous.beatmapId(); + Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); + String scoreId = previous == null ? null : previous.scoreId(); + switch (target.kind()) { + case ID -> beatmapsetId = Long.parseLong(target.id()); + case MAP -> beatmapId = Long.parseLong(target.id()); + case SCORE -> scoreId = target.id(); + case SET -> beatmapsetId = Long.parseLong(target.id()); + case RS, RP, BP -> { + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + } + case MP -> beatmapsetId = APIHelper.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); + case MEMORY -> {} + } + if (beatmapsetId == null) { + if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null) throw new ResolutionException("请指定指令目标喵"); + beatmapsetId = APIHelper.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); + } + history.remember(ctx, beatmapsetId, beatmapId, scoreId); + ctx.sendReply(PendingMessage.ofVoiceUrl("https://b.ppy.sh/preview/" + beatmapsetId + ".mp3").doUpload(false)); } } public void handleBpv(Context ctx) { - var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.BPV, 2, arg -> arg.startsWith("+") || TimeDurationParser.isTimeRange(arg)); - if (target == null) return; + var target = ctx.argumentCount() == 0 || ctx.argument(0).startsWith("+") || TimeDurationParser.isTimeRange(ctx.argument(0)) + ? TargetInput.memory() : TargetInput.read(ctx.args()); + var remembered = history.get(ctx); + if ((target.kind() == TargetInput.Kind.MEMORY && remembered == null) + || ctx.argumentCount() - target.consumedArgs() > 2) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.BPV)); + return; + } TimeDurationParser.TimeRange range = null; String mods = null; @@ -118,10 +196,29 @@ public void handleBpv(Context ctx) { try (var _ = taskCoordinator.beginRequest(ctx, "Beatmap Preview Render")) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "正在获取谱面以及回放文件,请稍作等待喵...")); var qqUpload = taskCoordinator.createVideoUploadRequest(ctx); - var resolvedTarget = targetLookup.beatmap(ctx, target.target(), history.get(ctx)); - history.remember(ctx, resolvedTarget); - long beatmapId = resolvedTarget.beatmapId(); + var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; + Long beatmapId = previous == null ? null : previous.beatmapId(); + Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); + String scoreId = previous == null ? null : previous.scoreId(); + switch (target.kind()) { + case ID, MAP -> beatmapId = Long.parseLong(target.id()); + case SCORE -> scoreId = target.id(); + case SET -> { + beatmapsetId = Long.parseLong(target.id()); + beatmapId = APIHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + } + case RS, RP, BP -> { + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + } + case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MEMORY -> {} + } + if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); var task = APIHelper.createBeatmapPreviewTask(beatmapId, mods, range, qqUpload); + history.remember(ctx, beatmapsetId, beatmapId, scoreId); videoRenderRecord.updateRenderTask(ctx.senderUserId(), task.taskId()); ctx.sendReply(replyFactory.replayMessage(ctx, task)); @@ -148,37 +245,113 @@ public void handleBpv(Context ctx) { } public void handleBgp(Context ctx) { - var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.BGP, Integer.MAX_VALUE); - if (target == null) return; + var target = ctx.argumentCount() == 0 + ? TargetInput.memory() : TargetInput.read(ctx.args()); + var remembered = history.get(ctx); + if (target.kind() == TargetInput.Kind.MEMORY && remembered == null) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.BGP)); + return; + } try (var timing = taskCoordinator.beginRequest(ctx, "Background Preview")) { - var resolvedTarget = targetLookup.beatmap(ctx, target.target(), history.get(ctx)); - history.remember(ctx, resolvedTarget); - long beatmapId = resolvedTarget.beatmapId(); + var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; + Long beatmapId = previous == null ? null : previous.beatmapId(); + Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); + String scoreId = previous == null ? null : previous.scoreId(); + switch (target.kind()) { + case ID, MAP -> beatmapId = Long.parseLong(target.id()); + case SCORE -> scoreId = target.id(); + case SET -> { + beatmapsetId = Long.parseLong(target.id()); + beatmapId = APIHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + } + case RS, RP, BP -> { + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + } + case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MEMORY -> {} + } + if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); var response = APIHelper.getBeatmapBgResponse(beatmapId); + history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.bgpMessage(ctx, response))); } } public void handleDl(Context ctx) { - var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.DL, 0); - if (target == null) return; + var target = ctx.argumentCount() == 0 + ? TargetInput.memory() : TargetInput.read(ctx.args()); + var remembered = history.get(ctx); + if ((target.kind() == TargetInput.Kind.MEMORY && remembered == null) + || ctx.argumentCount() - target.consumedArgs() > 0) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.DL)); + return; + } try (var timing = taskCoordinator.beginRequest(ctx, "Download Beatmap")) { - var resolvedTarget = targetLookup.beatmapset(ctx, target.target(), history.get(ctx)); - history.remember(ctx, resolvedTarget); - long beatmapsetId = resolvedTarget.beatmapsetId(); + var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; + Long beatmapId = previous == null ? null : previous.beatmapId(); + Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); + String scoreId = previous == null ? null : previous.scoreId(); + switch (target.kind()) { + case ID -> beatmapsetId = Long.parseLong(target.id()); + case MAP -> beatmapId = Long.parseLong(target.id()); + case SCORE -> scoreId = target.id(); + case SET -> beatmapsetId = Long.parseLong(target.id()); + case RS, RP, BP -> { + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + } + case MP -> beatmapsetId = APIHelper.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); + case MEMORY -> {} + } + if (beatmapsetId == null) { + if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null) throw new ResolutionException("请指定指令目标喵"); + beatmapsetId = APIHelper.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); + } var response = APIHelper.getLookupBeatmapsetResponse(beatmapsetId, accessTokenProvider.apply(ctx.senderUserId())); + history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(replyFactory.dlMessage(ctx, response)); } } public void handleMs(Context ctx) { - var target = TargetArguments.parse(ctx, resolver, history.get(ctx), "用法:/ms <谱面集ID 或 快捷查询>", 0); - if (target == null) return; + var target = ctx.argumentCount() == 0 + ? TargetInput.memory() : TargetInput.read(ctx.args()); + var remembered = history.get(ctx); + if ((target.kind() == TargetInput.Kind.MEMORY && remembered == null) + || ctx.argumentCount() - target.consumedArgs() > 0) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "用法:/ms <谱面集ID 或 快捷查询>")); + return; + } try (var timing = taskCoordinator.beginRequest(ctx, "Beatmapset")) { - var resolvedTarget = targetLookup.beatmapset(ctx, target.target(), history.get(ctx)); - history.remember(ctx, resolvedTarget); - long beatmapsetId = resolvedTarget.beatmapsetId(); + var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; + Long beatmapId = previous == null ? null : previous.beatmapId(); + Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); + String scoreId = previous == null ? null : previous.scoreId(); + switch (target.kind()) { + case ID -> beatmapsetId = Long.parseLong(target.id()); + case MAP -> beatmapId = Long.parseLong(target.id()); + case SCORE -> scoreId = target.id(); + case SET -> beatmapsetId = Long.parseLong(target.id()); + case RS, RP, BP -> { + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + } + case MP -> beatmapsetId = APIHelper.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); + case MEMORY -> {} + } + if (beatmapsetId == null) { + if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null) throw new ResolutionException("请指定指令目标喵"); + beatmapsetId = APIHelper.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); + } var response = APIHelper.getBeatmapsetResponse(beatmapsetId); + history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.beatmapsetMessage(ctx, response))); } } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java index e20a8b10..a3dfbd8e 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java @@ -7,11 +7,9 @@ import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.command.TaskCoordinator; import xyz.zcraft.seira.command.parse.Resolver; -import xyz.zcraft.seira.command.parse.UserRefResolution; import xyz.zcraft.seira.command.reply.ReplyFactory; import xyz.zcraft.seira.data.Notice; import xyz.zcraft.seira.data.UploadedImage; -import xyz.zcraft.seira.data.UserRef; import xyz.zcraft.seira.services.DailyLuck; import xyz.zcraft.seira.services.NoticeStore; @@ -41,26 +39,11 @@ public GeneralCommandHandler( } public void handleU(Context context) { - UserRef userRef; - if (context.argumentCount() == 0) { - Long boundUid = resolver.resolveBoundUid(context.senderUserId()); - userRef = boundUid == null ? null : new UserRef.ByUid(boundUid); - } else { - UserRefResolution target = resolver.resolveUserRefArgument(context.argument(0)); - if (target.errorMessage() != null) { - context.sendReply(PendingMessage.ofMarkdownRaw(at(context) + target.errorMessage())); - return; - } - userRef = target.userRef(); - } - - if (userRef == null) { - context.sendReply(PendingMessage.ofMarkdownRaw(at(context) + "用法:/u [玩家ID/用户名/@用户]")); - return; - } + String player = resolver.player(context.argumentCount() == 0 ? null : context.argument(0), context.senderUserId()); try (var _ = taskCoordinator.beginRequest(context, "User Info")) { - var response = APIHelper.getUserInfoResponse(userRef); + long uid = APIHelper.resolveUid(player); + var response = APIHelper.getUserInfoResponse(uid); var completion = replyFactory.userInfoMessage(context, response); context.sendReply(taskCoordinator.imageMessage(response, completion)); } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java index aeff4c58..b8531999 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java @@ -9,11 +9,9 @@ import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.command.TaskCoordinator; import xyz.zcraft.seira.command.parse.Resolver; -import xyz.zcraft.seira.command.parse.UserRefResolution; import xyz.zcraft.seira.command.reply.ReplyFactory; import xyz.zcraft.seira.data.SendResult; import xyz.zcraft.seira.data.UploadedImage; -import xyz.zcraft.seira.data.UserRef; import xyz.zcraft.seira.db.RankGuessRecordStore; import xyz.zcraft.seira.db.UserDataStore; import xyz.zcraft.seira.rankguess.HintUtil; @@ -269,16 +267,9 @@ public void handleRankGuess(Context ctx) { Long rank; if (resolver.looksLikeMention(argument)) { - final UserRefResolution userRefResolution = resolver.resolveUserRefArgument(argument); - - if (userRefResolution.errorMessage() != null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + userRefResolution.errorMessage())); - return; - } - - final UserRef userRef = userRefResolution.userRef(); - - rank = APIHelper.getUserRank(userRef); + String player = resolver.player(argument, ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + rank = APIHelper.getUserRank(uid); } else { rank = parseRank(argument); if (rank == null) { diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java index 6eac2c35..664c6900 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java @@ -5,11 +5,10 @@ import xyz.zcraft.seira.api.data.VideoRenderRecord; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; +import xyz.zcraft.seira.command.ResolutionException; +import xyz.zcraft.seira.command.parse.TargetInput; import xyz.zcraft.seira.command.ReplayResultStore; import xyz.zcraft.seira.command.TargetHistory; -import xyz.zcraft.seira.command.TargetLookup; -import xyz.zcraft.seira.command.parse.TargetArguments; -import xyz.zcraft.seira.data.UserRef; import xyz.zcraft.seira.command.TaskCoordinator; import xyz.zcraft.seira.command.parse.Resolver; import xyz.zcraft.seira.command.reply.CommandUsage; @@ -18,15 +17,16 @@ import xyz.zcraft.seira.util.TimeDurationParser; import java.util.Objects; +import java.util.List; import java.util.UUID; import java.util.function.Predicate; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; public final class ReplayCommandHandler { + private final java.util.function.Function accessTokenProvider; private final Resolver resolver; private final TargetHistory history; - private final TargetLookup targetLookup; private final TaskCoordinator taskCoordinator; private final ReplyFactory replyFactory; private final VideoRenderRecord videoRenderRecord; @@ -44,8 +44,8 @@ public ReplayCommandHandler( java.util.function.Function accessTokenProvider ) { this.resolver = resolver; + this.accessTokenProvider = accessTokenProvider; this.history = history; - this.targetLookup = new TargetLookup(resolver, accessTokenProvider); this.taskCoordinator = taskCoordinator; this.replyFactory = replyFactory; this.videoRenderRecord = videoRenderRecord; @@ -54,8 +54,14 @@ public ReplayCommandHandler( } public void handleR(Context ctx) { - var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.R, 1, TimeDurationParser::isTimeRange); - if (target == null) return; + var target = ctx.argumentCount() == 0 || TimeDurationParser.isTimeRange(ctx.argument(0)) + ? TargetInput.memory() : TargetInput.read(ctx.args()); + var remembered = history.get(ctx); + if ((target.kind() == TargetInput.Kind.MEMORY && remembered == null) + || ctx.argumentCount() - target.consumedArgs() > 1) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.R)); + return; + } TimeDurationParser.TimeRange range = null; @@ -70,11 +76,36 @@ public void handleR(Context ctx) { try (var _ = taskCoordinator.beginRequest(ctx, "Score Render")) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "正在获取谱面以及回放文件,请稍作等待喵...")); - var resolvedTarget = targetLookup.score(ctx, target.target(), history.get(ctx)); - history.remember(ctx, resolvedTarget); - String scoreId = resolvedTarget.scoreId(); + var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; + Long beatmapId = previous == null ? null : previous.beatmapId(); + Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); + String scoreId = previous == null ? null : previous.scoreId(); + switch (target.kind()) { + case ID, SCORE -> scoreId = target.id(); + case MAP -> beatmapId = Long.parseLong(target.id()); + case SET -> { + beatmapsetId = Long.parseLong(target.id()); + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); + } + case RS, RP, BP -> { + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + } + case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MEMORY -> {} + } + if (scoreId == null) { + if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupBeatmapScore(beatmapId, uid, List.of(), null); + } var upload = taskCoordinator.createVideoUploadRequest(ctx); var task = APIHelper.createReplayRenderTask(scoreId, range, upload); + history.remember(ctx, beatmapsetId, beatmapId, scoreId); videoRenderRecord.updateRenderTask(ctx.senderUserId(), task.taskId()); ctx.sendReply(replyFactory.replayMessage(ctx, task)); @@ -106,8 +137,13 @@ public void handleRsc(Context ctx) { return; } - var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.RSC, Integer.MAX_VALUE, arg -> arg.startsWith("+") || arg.startsWith("=")); - if (target == null) return; + var target = ctx.argumentCount() == 0 || ctx.argument(0).startsWith("+") || ctx.argument(0).startsWith("=") + ? TargetInput.memory() : TargetInput.read(ctx.args()); + var remembered = history.get(ctx); + if (target.kind() == TargetInput.Kind.MEMORY && remembered == null) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.RSC)); + return; + } String extraUidArg = null; @@ -122,9 +158,8 @@ public void handleRsc(Context ctx) { } } - var remembered = history.get(ctx); - String localScoreId = target.target() != null - ? target.target().localScoreId() + String localScoreId = target.kind() != TargetInput.Kind.MEMORY + ? target.kind() == TargetInput.Kind.SCORE ? target.id() : null : remembered == null ? null : remembered.scoreId(); boolean localScore = localScoreId != null && localScoreId.startsWith("loc"); var participants = new java.util.LinkedHashSet(); @@ -147,13 +182,9 @@ public void handleRsc(Context ctx) { if (token.trim().matches("[us]?[0-9]+")) { participants.add(token.trim()); } else if (resolver.looksLikeMention(token)) { - var participant = resolver.resolveUserRefArgument(token); - if (participant.errorMessage() != null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "解析 " + token + " 时出错:" + participant.errorMessage())); - return; - } - if (participant.userRef() instanceof UserRef.ByUid ref) participants.add("u" + ref.getUid()); - else if (participant.userRef() instanceof UserRef.ByUsername ref) participants.add("@" + ref.getUsername()); + String player = resolver.player(token, ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + participants.add("u" + uid); } else { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "追加ID列表包含非法值。")); return; @@ -164,9 +195,27 @@ public void handleRsc(Context ctx) { try (var _ = taskCoordinator.beginRequest(ctx, "Showcase Render")) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "正在获取谱面以及回放文件,请稍作等待喵...")); - var resolvedTarget = targetLookup.beatmap(ctx, target.target(), history.get(ctx)); - history.remember(ctx, resolvedTarget); - long beatmapId = resolvedTarget.beatmapId(); + var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; + Long beatmapId = previous == null ? null : previous.beatmapId(); + Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); + String scoreId = previous == null ? null : previous.scoreId(); + switch (target.kind()) { + case ID, MAP -> beatmapId = Long.parseLong(target.id()); + case SCORE -> scoreId = target.id(); + case SET -> { + beatmapsetId = Long.parseLong(target.id()); + beatmapId = APIHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + } + case RS, RP, BP -> { + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + } + case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MEMORY -> {} + } + if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); var upload = taskCoordinator.createVideoUploadRequest(ctx); String[] scoreTargets = participants.toArray(String[]::new); if (localScore) { @@ -177,6 +226,7 @@ public void handleRsc(Context ctx) { } var task = APIHelper.createReplayShowcaseTask(beatmapId, scoreTargets, upload); + history.remember(ctx, beatmapsetId, beatmapId, scoreId); videoRenderRecord.updateRenderTask(ctx.senderUserId(), task.taskId()); ctx.sendReply(replyFactory.replayMessage(ctx, task)); diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java index dcf3d539..80704b17 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java @@ -4,13 +4,12 @@ import xyz.zcraft.seira.api.data.MissData; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; +import xyz.zcraft.seira.command.ResolutionException; +import xyz.zcraft.seira.command.parse.TargetInput; import xyz.zcraft.seira.command.TargetHistory; -import xyz.zcraft.seira.command.TargetLookup; -import xyz.zcraft.seira.command.parse.TargetArguments; -import xyz.zcraft.seira.command.parse.TargetResolution; -import xyz.zcraft.seira.data.UserRef; import xyz.zcraft.seira.command.TaskCoordinator; -import xyz.zcraft.seira.command.parse.*; +import xyz.zcraft.seira.command.parse.Resolver; +import xyz.zcraft.seira.command.parse.ScoreFilterArguments; import xyz.zcraft.seira.command.reply.CommandUsage; import xyz.zcraft.seira.command.reply.ReplyFactory; @@ -24,9 +23,9 @@ public final class ScoreCommandHandler { private static final int MAX_SCORE_LIST_COUNT = 200; private static final Pattern SCORE_LIST_RANGE_PATTERN = Pattern.compile("^(\\d+)(?:-(\\d+))?$"); + private final java.util.function.Function accessTokenProvider; private final Resolver resolver; private final TargetHistory history; - private final TargetLookup targetLookup; private final TaskCoordinator taskCoordinator; private final ReplyFactory replyFactory; @@ -38,8 +37,8 @@ public ScoreCommandHandler( java.util.function.Function accessTokenProvider ) { this.resolver = resolver; + this.accessTokenProvider = accessTokenProvider; this.history = history; - this.targetLookup = new TargetLookup(resolver, accessTokenProvider); this.taskCoordinator = taskCoordinator; this.replyFactory = replyFactory; } @@ -77,15 +76,12 @@ static ScoreListRange parseScoreListRange(String value) { public void handleBp(Context ctx) { if (ctx.args().length == 0) { - ShortcutTarget target = resolver.parseTarget("bp1", ctx.senderUserId()); - if (target.isError()) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + target.errorMessage())); - return; - } + String player = resolver.player(null, ctx.senderUserId()); try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - String scoreId = APIHelper.lookupPlayerScore(APIHelper.resolveUid(target.userRef()), target.macroType(), target.macroIndex(), List.of(), null); - history.remember(ctx, null, null, scoreId); + long uid = APIHelper.resolveUid(player); + String scoreId = APIHelper.lookupPlayerScore(uid, "bp", 1, List.of(), null); var response = APIHelper.getScoreResponse(scoreId); + history.remember(ctx, null, null, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } return; @@ -104,13 +100,14 @@ public void handleBp(Context ctx) { var request = parseScoreListRequest(ctx, CommandUsage.BP); if (request == null) return; var range = request.range(); - var userRef = request.userRef(); + var player = request.player(); var filters = request.filters(); try (var _ = taskCoordinator.beginRequest(ctx, "Best Scores")) { + long uid = APIHelper.resolveUid(player); var response = APIHelper.getBoNResponse( range.end(), range.start(), - userRef, + uid, filters.filters() ); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.bpMessage(ctx, response))); @@ -119,15 +116,12 @@ public void handleBp(Context ctx) { public void handleRs(Context ctx, boolean includeFail) { if (ctx.args().length == 0) { - ShortcutTarget target = resolver.parseTarget(ctx.command() + "1", ctx.senderUserId()); - if (target.isError()) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + target.errorMessage())); - return; - } + String player = resolver.player(null, ctx.senderUserId()); try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - String scoreId = APIHelper.lookupPlayerScore(APIHelper.resolveUid(target.userRef()), target.macroType(), target.macroIndex(), List.of(), null); - history.remember(ctx, null, null, scoreId); + long uid = APIHelper.resolveUid(player); + String scoreId = APIHelper.lookupPlayerScore(uid, ctx.command(), 1, List.of(), null); var response = APIHelper.getScoreResponse(scoreId); + history.remember(ctx, null, null, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } return; @@ -146,13 +140,14 @@ public void handleRs(Context ctx, boolean includeFail) { var request = parseScoreListRequest(ctx, CommandUsage.RS); if (request == null) return; var range = request.range(); - var userRef = request.userRef(); + var player = request.player(); var filters = request.filters(); try (var _ = taskCoordinator.beginRequest(ctx, "Recent Score")) { + long uid = APIHelper.resolveUid(player); var response = APIHelper.getRecentResponse( range.end(), range.start(), - userRef, + uid, includeFail, filters.filters() ); @@ -167,45 +162,20 @@ public void handleTb(Context ctx) { return; } - UserRef userRef; - if (request.target() != null) { - UserRefResolution resolution = resolver.resolveUserRefArgument(request.target()); - if (resolution.errorMessage() != null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + resolution.errorMessage())); - return; - } - if (resolution.userRef() == null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.TB)); - return; - } - userRef = resolution.userRef(); - } else { - Long uid = resolver.resolveBoundUid(ctx.senderUserId()); - if (uid == null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.NO_BIND)); - return; - } - userRef = new UserRef.ByUid(uid); - } - - UserRef target = userRef; + String player = resolver.player(request.target(), ctx.senderUserId()); try (var _ = taskCoordinator.beginRequest(ctx, "Recent Best Scores")) { - var response = APIHelper.getTodayBestResponse(target, request.days()); + long uid = APIHelper.resolveUid(player); + var response = APIHelper.getTodayBestResponse(uid, request.days()); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.tbMessage(ctx, response))); } } private void handleFilteredSingleScore(Context ctx, String macroType) { - UserRef targetUser = null; + String targetUser = null; int startIndex = 0; if (resolver.looksLikeMention(ctx.args()[0]) || resolver.looksLikeUid(ctx.args()[0])) { - final UserRefResolution userRefResolution = resolver.resolveUserRefArgument(ctx.args()[0]); - if (userRefResolution.errorMessage() != null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + userRefResolution.errorMessage())); - return; - } - targetUser = userRefResolution.userRef(); + targetUser = resolver.player(ctx.args()[0], ctx.senderUserId()); startIndex = 1; } @@ -215,60 +185,116 @@ private void handleFilteredSingleScore(Context ctx, String macroType) { return; } - if (targetUser == null) { - Long uid = resolver.resolveBoundUid(ctx.senderUserId()); - if (uid == null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.NO_BIND)); - return; - } - targetUser = new UserRef.ByUid(uid); - } + if (targetUser == null) targetUser = resolver.player(null, ctx.senderUserId()); - ShortcutTarget target = new ShortcutTarget(null, targetUser, macroType, 1L, null); try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - String scoreId = APIHelper.lookupPlayerScore(APIHelper.resolveUid(target.userRef()), target.macroType(), target.macroIndex(), filters.filters(), null); - history.remember(ctx, null, null, scoreId); + long uid = APIHelper.resolveUid(targetUser); + String scoreId = APIHelper.lookupPlayerScore(uid, macroType, 1, filters.filters(), null); var response = APIHelper.getScoreResponse(scoreId); + history.remember(ctx, null, null, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } } public void handleS(Context ctx) { - handleScore(ctx, false); + String usage = CommandUsage.S; + TargetInput target; + String userOverride = null; + int optionIndex; + boolean playerOnly = ctx.argumentCount() > 0 && resolver.looksLikeMention(ctx.argument(0)) + && (ctx.argumentCount() == 1 || ctx.argument(1).startsWith("+")); + if (playerOnly || ctx.argumentCount() == 0 || ctx.argument(0).startsWith("+")) { + target = TargetInput.memory(); + } else { + target = TargetInput.read(ctx.args()); + } + optionIndex = target.consumedArgs(); + if (optionIndex < ctx.argumentCount() && !ctx.argument(optionIndex).startsWith("+")) { + userOverride = resolver.player(ctx.argument(optionIndex), ctx.senderUserId()); + optionIndex++; + } + var remembered = history.get(ctx); + if ((target.kind() == TargetInput.Kind.MEMORY && remembered == null) + || ctx.argumentCount() - optionIndex > 1 + || (optionIndex < ctx.argumentCount() && !ctx.argument(optionIndex).startsWith("+"))) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + usage)); + return; + } + String option = optionIndex < ctx.argumentCount() ? ctx.argument(optionIndex) : null; + String mod = option == null ? null : option.substring(1).toUpperCase(java.util.Locale.ROOT); + List filters = List.of(); + if (mod != null) { + var parsed = ScoreFilterArguments.parse(new String[]{"mod=" + mod}, 0); + if (parsed.isError()) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + parsed.errorMessage())); + return; + } + filters = parsed.filters(); + } + try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { + var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; + Long beatmapId = previous == null ? null : previous.beatmapId(); + Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); + String scoreId = previous == null ? null : previous.scoreId(); + boolean selectedPlayerScore = false; + switch (target.kind()) { + case ID, SCORE -> scoreId = target.id(); + case MAP -> beatmapId = Long.parseLong(target.id()); + case SET -> { + beatmapsetId = Long.parseLong(target.id()); + String player = userOverride == null ? resolver.player(null, ctx.senderUserId()) : userOverride; + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, filters, mod); + selectedPlayerScore = true; + } + case RS, RP, BP -> { + String player = userOverride == null ? resolver.player(target.player(), ctx.senderUserId()) : userOverride; + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), filters, mod); + selectedPlayerScore = true; + } + case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MEMORY -> {} + } + if ((userOverride != null || mod != null) && scoreId != null && !selectedPlayerScore) { + if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + scoreId = null; + } + if (scoreId == null) { + if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); + String player = userOverride == null ? resolver.player(null, ctx.senderUserId()) : userOverride; + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupBeatmapScore(beatmapId, uid, filters, mod); + } + var response = APIHelper.getScoreResponse(scoreId); + history.remember(ctx, beatmapsetId, beatmapId, scoreId); + ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); + } catch (Exception e) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + TaskCoordinator.resolveErrorMessage(e))); + org.apache.logging.log4j.LogManager.getLogger(ScoreCommandHandler.class) + .error("Failed to execute /{}", ctx.command(), e); + } } public void handleSm(Context ctx) { - handleScore(ctx, true); - } - - private void handleScore(Context ctx, boolean onBeatmap) { - String usage = onBeatmap ? CommandUsage.SM : CommandUsage.S; - TargetResolution target; - UserRef userOverride = null; + String usage = CommandUsage.SM; + TargetInput target; + String userOverride = null; int optionIndex; boolean playerOnly = ctx.argumentCount() > 0 && resolver.looksLikeMention(ctx.argument(0)) && (ctx.argumentCount() == 1 || ctx.argument(1).startsWith("+")); if (playerOnly || ctx.argumentCount() == 0 || ctx.argument(0).startsWith("+")) { - target = new TargetResolution(null, 0); + target = TargetInput.memory(); } else { - target = resolver.resolveTargetWithOptionalMention(ctx.args(), ctx.senderUserId()); - if (target.target().isError()) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + target.target().errorMessage())); - return; - } + target = TargetInput.read(ctx.args()); } optionIndex = target.consumedArgs(); if (optionIndex < ctx.argumentCount() && !ctx.argument(optionIndex).startsWith("+")) { - var playerArgument = resolver.resolveUserRefArgument(ctx.argument(optionIndex)); - if (playerArgument.errorMessage() != null || playerArgument.userRef() == null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) - + (playerArgument.errorMessage() == null ? usage : playerArgument.errorMessage()))); - return; - } - userOverride = playerArgument.userRef(); + userOverride = resolver.player(ctx.argument(optionIndex), ctx.senderUserId()); optionIndex++; } - if ((target.target() == null && history.get(ctx) == null) + var remembered = history.get(ctx); + if ((target.kind() == TargetInput.Kind.MEMORY && remembered == null) || ctx.argumentCount() - optionIndex > 1 || (optionIndex < ctx.argumentCount() && !ctx.argument(optionIndex).startsWith("+"))) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + usage)); @@ -286,12 +312,32 @@ private void handleScore(Context ctx, boolean onBeatmap) { filters = parsed.filters(); } try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - var resolvedTarget = onBeatmap - ? targetLookup.scoreOnBeatmap(ctx, target.target(), history.get(ctx), userOverride, filters, mod) - : targetLookup.score(ctx, target.target(), history.get(ctx), userOverride, filters, mod); - history.remember(ctx, resolvedTarget); - String scoreId = resolvedTarget.scoreId(); + var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; + Long beatmapId = previous == null ? null : previous.beatmapId(); + Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); + String scoreId = previous == null ? null : previous.scoreId(); + switch (target.kind()) { + case ID, MAP -> beatmapId = Long.parseLong(target.id()); + case SCORE -> scoreId = target.id(); + case SET -> { + beatmapsetId = Long.parseLong(target.id()); + beatmapId = APIHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + } + case RS, RP, BP -> { + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + } + case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MEMORY -> {} + } + if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); + String player = userOverride == null ? resolver.player(null, ctx.senderUserId()) : userOverride; + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupBeatmapScore(beatmapId, uid, filters, mod); var response = APIHelper.getScoreResponse(scoreId); + history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } catch (Exception e) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + TaskCoordinator.resolveErrorMessage(e))); @@ -301,31 +347,93 @@ private void handleScore(Context ctx, boolean onBeatmap) { } public void handleSa(Context ctx) { - var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.SA, 0); - if (target == null) return; + var target = ctx.argumentCount() == 0 + ? TargetInput.memory() : TargetInput.read(ctx.args()); + var remembered = history.get(ctx); + if ((target.kind() == TargetInput.Kind.MEMORY && remembered == null) + || ctx.argumentCount() - target.consumedArgs() > 0) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.SA)); + return; + } try (var _ = taskCoordinator.beginRequest(ctx, "Score Analysis")) { - var resolvedTarget = targetLookup.score(ctx, target.target(), history.get(ctx)); - history.remember(ctx, resolvedTarget); - String scoreId = resolvedTarget.scoreId(); + var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; + Long beatmapId = previous == null ? null : previous.beatmapId(); + Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); + String scoreId = previous == null ? null : previous.scoreId(); + switch (target.kind()) { + case ID, SCORE -> scoreId = target.id(); + case MAP -> beatmapId = Long.parseLong(target.id()); + case SET -> { + beatmapsetId = Long.parseLong(target.id()); + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); + } + case RS, RP, BP -> { + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + } + case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MEMORY -> {} + } + if (scoreId == null) { + if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupBeatmapScore(beatmapId, uid, List.of(), null); + } var response = APIHelper.getScoreAnalyzeResponse(scoreId); + history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreAnalyzeMessage(ctx, response))); } } public void handleMa(Context ctx) { - var target = TargetArguments.parse(ctx, resolver, history.get(ctx), CommandUsage.MA, 1, arg -> arg.startsWith("#")); - if (target == null) return; + var target = ctx.argumentCount() == 0 || ctx.argument(0).startsWith("#") + ? TargetInput.memory() : TargetInput.read(ctx.args()); + var remembered = history.get(ctx); + if ((target.kind() == TargetInput.Kind.MEMORY && remembered == null) + || ctx.argumentCount() - target.consumedArgs() > 1) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.MA)); + return; + } String indexArgument = (ctx.argumentCount() > target.consumedArgs() ? ctx.argument(target.consumedArgs()) : null); - Integer index = indexArgument == null ? null : parseMissIndex(indexArgument, target.target() == null); + Integer index = indexArgument == null ? null : parseMissIndex(indexArgument, target.kind() == TargetInput.Kind.MEMORY); if (indexArgument != null && index == null) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.MA)); return; } try (var _ = taskCoordinator.beginRequest(ctx, "Score Misses")) { - var resolvedTarget = targetLookup.score(ctx, target.target(), history.get(ctx)); - history.remember(ctx, resolvedTarget); - String scoreId = resolvedTarget.scoreId(); + var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; + Long beatmapId = previous == null ? null : previous.beatmapId(); + Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); + String scoreId = previous == null ? null : previous.scoreId(); + switch (target.kind()) { + case ID, SCORE -> scoreId = target.id(); + case MAP -> beatmapId = Long.parseLong(target.id()); + case SET -> { + beatmapsetId = Long.parseLong(target.id()); + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); + } + case RS, RP, BP -> { + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + } + case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MEMORY -> {} + } + if (scoreId == null) { + if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); + String player = resolver.player(target.player(), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + scoreId = APIHelper.lookupBeatmapScore(beatmapId, uid, List.of(), null); + } var response = APIHelper.getScoreMissesResponse(scoreId); + history.remember(ctx, beatmapsetId, beatmapId, scoreId); List misses = response.getContent(); if (index == null && misses.size() != 1) { ctx.sendReply(replyFactory.scoreMissesMessage(ctx, response)); @@ -366,26 +474,12 @@ private ScoreListRequest parseScoreListRequest(Context ctx, String usage) { } int nextArg = 1; - UserRef userRef; + String player; if (nextArg < args.length && (resolver.looksLikeMention(args[nextArg]) || resolver.looksLikeUid(args[nextArg]))) { - UserRefResolution resolution = resolver.resolveUserRefArgument(args[nextArg]); - if (resolution.errorMessage() != null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + resolution.errorMessage())); - return null; - } - if (resolution.userRef() == null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + usage)); - return null; - } - userRef = resolution.userRef(); + player = resolver.player(args[nextArg], ctx.senderUserId()); nextArg++; } else { - Long uid = resolver.resolveBoundUid(ctx.senderUserId()); - if (uid == null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.NO_BIND)); - return null; - } - userRef = new UserRef.ByUid(uid); + player = resolver.player(null, ctx.senderUserId()); } ScoreFilterArguments.ParseResult filters = ScoreFilterArguments.parse(args, nextArg); @@ -393,10 +487,10 @@ private ScoreListRequest parseScoreListRequest(Context ctx, String usage) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + filters.errorMessage() + "\n" + CommandUsage.SCORE_FILTERS)); return null; } - return new ScoreListRequest(range, userRef, filters); + return new ScoreListRequest(range, player, filters); } - private record ScoreListRequest(ScoreListRange range, UserRef userRef, ScoreFilterArguments.ParseResult filters) {} + private record ScoreListRequest(ScoreListRange range, String player, ScoreFilterArguments.ParseResult filters) {} record TbArguments(int days, String target) { } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java index e3250270..216f50a2 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java @@ -10,13 +10,10 @@ import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.command.TaskCoordinator; import xyz.zcraft.seira.command.parse.Resolver; -import xyz.zcraft.seira.command.parse.ShortcutTarget; +import xyz.zcraft.seira.command.parse.TargetInput; import xyz.zcraft.seira.command.ResolutionException; -import xyz.zcraft.seira.command.parse.TargetResolution; -import xyz.zcraft.seira.command.parse.UserRefResolution; import xyz.zcraft.seira.command.reply.CommandUsage; import xyz.zcraft.seira.command.reply.ReplyFactory; -import xyz.zcraft.seira.data.UserRef; import xyz.zcraft.seira.db.UserDataStore; import xyz.zcraft.seira.util.OsuAuthHelper; @@ -94,24 +91,16 @@ public void handleFriendStatus(Context ctx) { if (ctx.argumentCount() == 0) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "用法:/mu @someone\n> 注: 读取@需要开启权限。")); - } - - final UserRefResolution targetRef = resolver.resolveUserRefArgument(ctx.argument(0)); - - if (targetRef.errorMessage() != null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + targetRef.errorMessage())); return; } - if (targetRef.userRef() instanceof UserRef.ByUid byUid) { - final long uid = byUid.getUid(); - final String at = UserDataStore.findGroupOpenIdByUid(ctx.groupId(), uid) - .map(ReplyFactory::at) - .orElse(""); - ctx.sendReply(PendingMessage.ofMarkdownRaw(at) + ": [%d](%s)".formatted(uid, "https://osu.ppy.sh/users/" + uid)); - } - - final UserExtended targetUser = APIHelper.getUserRaw(targetRef.userRef()); + String player = resolver.player(ctx.argument(0), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + final String mention = UserDataStore.findGroupOpenIdByUid(ctx.groupId(), uid) + .map(ReplyFactory::at) + .orElse(""); + ctx.sendReply(PendingMessage.ofMarkdownRaw(mention) + ": [%d](%s)".formatted(uid, "https://osu.ppy.sh/users/" + uid)); + final UserExtended targetUser = APIHelper.getUserRaw(uid); final String targetOsuAvatar = targetUser.getAvatarUrl(); boolean selfFollowed; @@ -296,17 +285,11 @@ public void handleLb(Context ctx) { ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.lbMessage(ctx, response))); } } else if (ctx.args().length == 1 || ctx.args().length == 2) { - TargetResolution targetResolution = resolver.resolveTargetWithOptionalMention(ctx.args(), ctx.senderUserId()); - ShortcutTarget target = targetResolution.target(); - if (target.isError()) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + target.errorMessage())); - return; - } - - int remainingArgs = ctx.argumentCount() - targetResolution.consumedArgs(); + var target = TargetInput.read(ctx.args()); + int remainingArgs = ctx.argumentCount() - target.consumedArgs(); List uids = new LinkedList<>(); if (remainingArgs == 1) { - String[] uidTokens = ctx.argument(targetResolution.consumedArgs()).split(","); + String[] uidTokens = ctx.argument(target.consumedArgs()).split(","); if (uidTokens.length == 0) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "玩家ID列表不能为空。")); return; @@ -338,25 +321,18 @@ public void handleLb(Context ctx) { } try (var _ = taskCoordinator.beginRequest(ctx, "Map Leaderboard")) { - long beatmapId; - if (target.isLocalScore()) { - beatmapId = APIHelper.getScoreBeatmapId(target.localScoreId()); - } else if (!target.isMacro() || "m".equals(target.macroType())) { - beatmapId = target.explicitId(); - } else { - switch (target.macroType()) { - case "s" -> beatmapId = APIHelper.getScoreBeatmapId(target.explicitId().toString()); - case "ms" -> beatmapId = APIHelper.lookupBeatmapInSet(target.explicitId(), target.macroIndex(), - accessTokenProvider.apply(ctx.senderUserId())); - case "rs", "rp", "bp" -> { - long uid = APIHelper.resolveUid(target.userRef()); - beatmapId = APIHelper.lookupPlayerScoreBeatmap(uid, target.macroType(), target.macroIndex(), accessTokenProvider.apply(ctx.senderUserId())); - - } - case "mp" -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); - default -> throw new ResolutionException("未知的快捷查询"); + long beatmapId = switch (target.kind()) { + case ID, MAP -> Long.parseLong(target.id()); + case SCORE -> APIHelper.getScoreBeatmapId(target.id()); + case SET -> APIHelper.lookupBeatmapInSet(Long.parseLong(target.id()), target.index(), + accessTokenProvider.apply(ctx.senderUserId())); + case RS, RP, BP -> { + long uid = APIHelper.resolveUid(resolver.player(target.player(), ctx.senderUserId())); + yield APIHelper.lookupPlayerScoreBeatmap(uid, target.scoreList(), target.index(), accessTokenProvider.apply(ctx.senderUserId())); } - } + case MP -> APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MEMORY -> throw new ResolutionException("请指定指令目标谱面喵"); + }; var response = APIHelper.getGroupLeaderboardResponse(beatmapId, uids); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.lbMessage(ctx, response))); } @@ -366,28 +342,13 @@ public void handleLb(Context ctx) { } public void handleSup(Context ctx) { - final UserRef target; - - if (ctx.argumentCount() == 0) { - Long targetId = resolver.resolveBoundUid(ctx.senderUserId()); - if (targetId == null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.NO_BIND)); - return; - } - target = new UserRef.ByUid(targetId); - } else if (ctx.argumentCount() == 1) { - final UserRefResolution res = resolver.resolveUserRefArgument(ctx.argument(0)); - if (res.errorMessage() != null) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + res.errorMessage())); - return; - } - target = res.userRef(); - } else { + if (ctx.argumentCount() > 1) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.SUP)); return; } - - final UserExtended user = APIHelper.getUserRaw(target); + String player = resolver.player(ctx.argumentCount() == 0 ? null : ctx.argument(0), ctx.senderUserId()); + long uid = APIHelper.resolveUid(player); + final UserExtended user = APIHelper.getUserRaw(uid); final String openId = UserDataStore.findGroupOpenIdByUid(ctx.groupId(), user.getId()).orElse(null); ctx.sendReply(replyFactory.supMessage(ctx, user.getUsername(), openId, user.isSupporter(), user.getHasSupported(), user.getSupportLevel())); diff --git a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java index 119ea34b..c076e6c4 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java @@ -1,7 +1,7 @@ package xyz.zcraft.seira.command.parse; import xyz.zcraft.seira.api.data.SearchQuery; -import xyz.zcraft.seira.data.UserRef; +import xyz.zcraft.seira.command.ResolutionException; import xyz.zcraft.seira.db.UserDataStore; import java.util.*; @@ -10,7 +10,6 @@ import java.util.regex.Pattern; public final class Resolver { - private static final ArrayList USER_MACRO_TYPES = new ArrayList<>(List.of("rs", "bp", "rp")); private final java.util.function.Function boundUid; public Resolver() { @@ -70,69 +69,22 @@ public SearchQuery resolveSearchQuery(String arg) { return null; } - public TargetResolution resolveTargetWithOptionalMention(String[] args, String senderUserId) { - if (args.length >= 2 && isUserMacro(args[1])) { - String mentionedUserId = extractMentionedUserId(args[0]); - if (mentionedUserId != null) { - Long boundUid = resolveBoundUid(mentionedUserId); - if (boundUid == null) { - return new TargetResolution(new ShortcutTarget(null, null, null, null, - "被@的用户还没有绑定玩家ID,无法使用快捷查询。"), 2); - } - return new TargetResolution(parseTarget(args[1], new UserRef.ByUid(boundUid), true, false), 2); - } - - Long uid = parsePositiveLong(args[0]); - if (uid != null) { - return new TargetResolution(parseTarget(args[1], new UserRef.ByUid(uid), false, false), 2); - } - - if (!looksLikeMention(args[0]) && !args[0].isBlank()) { - return new TargetResolution(parseTarget(args[1], new UserRef.ByUsername(args[0]), false, false), 2); - } - -// if (looksLikeMention(args[0])) { -// return new TargetResolution(new ShortcutTarget(null, null, null, null, "@用户格式无效,请使用@用户后再输入快捷查询(如 rs2)。"), 2); -// } - } - return new TargetResolution(parseTarget(args[0], senderUserId), 1); - } - - public ShortcutTarget parseTarget(String arg, String senderUserId) { - Long boundUid = resolveBoundUid(senderUserId); - UserRef userRef = boundUid == null ? null : new UserRef.ByUid(boundUid); - return parseTarget(arg, userRef, false, true); - } - - public UserRefResolution resolveUserRefArgument(String arg) { - Long explicitUid = parsePositiveLong(arg); - if (explicitUid != null) { - return new UserRefResolution(new UserRef.ByUid(explicitUid), null); - } - - String mentionedUserId = extractMentionedUserId(arg); - if (mentionedUserId != null) { - Long boundUid = resolveBoundUid(mentionedUserId); - if (boundUid == null) { - return new UserRefResolution(null, "被@的用户还没有绑定玩家ID,请先让对方使用 /bind"); - } - return new UserRefResolution(new UserRef.ByUid(boundUid), null); + public String player(String argument, String senderUserId) { + if (argument == null) { + Long uid = resolveBoundUid(senderUserId); + if (uid == null) throw new ResolutionException("你还没有绑定玩家ID,请先使用 /bind"); + return uid.toString(); } - -// if (looksLikeMention(arg)) { -// return new UserRefResolution(null, "@用户格式无效,请使用 @用户 后再输入指令。示例:/bo 5 @123456"); -// } - - String username = arg == null ? "" : arg.trim(); - if (username.startsWith("@")) { - username = username.substring(1); + String mentioned = extractMentionedUserId(argument); + if (mentioned != null) { + Long uid = resolveBoundUid(mentioned); + if (uid == null) throw new ResolutionException("被@的用户还没有绑定玩家ID,请先让对方使用 /bind"); + return uid.toString(); } - - if (username.isEmpty()) { - return new UserRefResolution(null, null); - } - - return new UserRefResolution(new UserRef.ByUsername(username), null); + String player = argument.trim(); + if (player.startsWith("@")) player = player.substring(1); + if (player.isBlank()) throw new ResolutionException("无法识别指定的玩家"); + return player; } public Long resolveBoundUid(String senderUserId) { @@ -151,93 +103,6 @@ public Integer parsePositiveInt(String value) { } } - private ShortcutTarget parseTarget(String arg, UserRef userRef, boolean mentionedUser, boolean needResolveBound) { - Matcher setMatcher = Patterns.SET_MACRO_PATTERN.matcher(arg.trim()); - if (setMatcher.matches()) { - Long setId = parsePositiveLong(setMatcher.group(1)); - Long index = parsePositiveLong(setMatcher.group(2)); - - if (setId == null || index == null || index < 1) { - return new ShortcutTarget(null, null, null, null, "谱面集索引无效。例如: 12345#2"); - } - - return new ShortcutTarget(setId, userRef, "ms", index, null); - } - - Matcher userMatcher = Patterns.USER_MACRO_PATTERN.matcher(arg.trim()); - if (userMatcher.matches()) { - String type = userMatcher.group(1).toLowerCase(); - - if (!USER_MACRO_TYPES.contains(type)) { - return new ShortcutTarget(null, null, null, null, "未知的快捷查询"); - } - - Long index = parsePositiveLong(userMatcher.group(2)); - - if (index == null) { - index = 1L; - } - - if (index < 1 || index > 200) { - return new ShortcutTarget(null, null, null, null, "快捷指令索引无效,请输入 1-200 之间的数字。例如: rs5"); - } - - if (userRef == null) { - String errorMessage; - if (needResolveBound) { - errorMessage = mentionedUser - ? "被@的用户还没有绑定玩家ID,无法使用快捷查询。" - : "你还没有绑定玩家ID,无法使用快捷查询。请先使用 /bind"; - } else { - errorMessage = "无法识别指定的玩家ID"; - } - - return new ShortcutTarget(null, null, null, null, errorMessage); - } - - return new ShortcutTarget(null, userRef, type, index, null); - } - - if ("mp".equalsIgnoreCase(arg.trim())) { - if (userRef == null) { - String errorMessage; - if (needResolveBound) { - errorMessage = mentionedUser - ? "被@的用户还没有绑定玩家ID,无法使用快捷查询。" - : "你还没有绑定玩家ID,无法使用快捷查询。请先使用 /bind"; - } else { - errorMessage = "无法识别指定的玩家ID"; - } - - return new ShortcutTarget(null, null, null, null, errorMessage); - } - - return new ShortcutTarget(null, userRef, "mp", null, null); - } - - Matcher beatmapMatcher = Patterns.BEATMAP_MACRO_PATTERN.matcher(arg.trim()); - if (beatmapMatcher.matches()) { - Long mapId = parsePositiveLong(beatmapMatcher.group(1)); - return new ShortcutTarget(mapId, userRef, "m", null, null); - } - - if (Patterns.LOCAL_SCORE_PATTERN.matcher(arg.trim()).matches()) { - return ShortcutTarget.localScore(arg.trim().toLowerCase(Locale.ROOT)); - } - - Long id = parsePositiveLong(arg); - if (id == null) { - return new ShortcutTarget(null, null, null, null, - "参数无效。请输入数字ID、本地成绩ID或快捷指令 (例如 loc123456789, rs1, 12345#2)。"); - } - - return new ShortcutTarget(id, null, null, null, null); - } - - private boolean isUserMacro(String arg) { - return Patterns.USER_MACRO_PATTERN.matcher(arg.trim()).matches(); - } - public boolean looksLikeMention(String token) { String trimmed = token == null ? "" : token.trim(); return trimmed.startsWith("@") @@ -278,16 +143,12 @@ public boolean looksLikeUid(String arg) { } private static final class Patterns { - private static final Pattern USER_MACRO_PATTERN = Pattern.compile("(?i)^(rs|bo|rp|bp)(\\d+)?$"); private static final Pattern COMPACT_SCORE_COMMAND_PATTERN = Pattern.compile( "(?i)^(rs|rp|bp)(\\d+)(?:-(\\d+))?(?=\\s|$)" ); private static final Pattern SPACE_MISSING_COMMAND_PATTERN = Pattern.compile( "^([a-zA-Z]+)(\\d+(?:#\\d+)?)" ); - private static final Pattern SET_MACRO_PATTERN = Pattern.compile("^(\\d+)#(\\d+)$"); - private static final Pattern BEATMAP_MACRO_PATTERN = Pattern.compile("^m(\\d+)$"); - private static final Pattern LOCAL_SCORE_PATTERN = Pattern.compile("(?i)^loc[1-9]\\d*$"); private static final Pattern QQ_AT_PATTERN = Pattern.compile("^<@([A-Z|0-9]{32})>$"); private static final Pattern QQ_INLINE_AT_PATTERN = Pattern.compile("(<@[A-Z|0-9]{32}>)"); private static final Pattern PLAIN_AT_PATTERN = Pattern.compile("^@(\\d+)$"); diff --git a/src/main/java/xyz/zcraft/seira/command/parse/ShortcutTarget.java b/src/main/java/xyz/zcraft/seira/command/parse/ShortcutTarget.java deleted file mode 100644 index 217e00d1..00000000 --- a/src/main/java/xyz/zcraft/seira/command/parse/ShortcutTarget.java +++ /dev/null @@ -1,36 +0,0 @@ -package xyz.zcraft.seira.command.parse; - -import xyz.zcraft.seira.data.UserRef; - -/** - * 用户输入或 API 目标。解析后的普通目标只包含 explicitId; - * 本地成绩使用 localScoreId。跨类型查找时 m/ms/s 标明原始 ID 的类型。 - */ -public record ShortcutTarget( - Long explicitId, - String localScoreId, - UserRef userRef, - String macroType, - Long macroIndex, - String errorMessage -) { - public ShortcutTarget(Long explicitId, UserRef userRef, String macroType, Long macroIndex, String errorMessage) { - this(explicitId, null, userRef, macroType, macroIndex, errorMessage); - } - - public static ShortcutTarget localScore(String localScoreId) { - return new ShortcutTarget(null, localScoreId, null, null, null, null); - } - - public boolean isMacro() { - return macroType != null; - } - - public boolean isError() { - return errorMessage != null; - } - - public boolean isLocalScore() { - return localScoreId != null; - } -} diff --git a/src/main/java/xyz/zcraft/seira/command/parse/TargetArguments.java b/src/main/java/xyz/zcraft/seira/command/parse/TargetArguments.java deleted file mode 100644 index 2c4d898e..00000000 --- a/src/main/java/xyz/zcraft/seira/command/parse/TargetArguments.java +++ /dev/null @@ -1,35 +0,0 @@ -package xyz.zcraft.seira.command.parse; - -import xyz.zcraft.seira.bot.data.PendingMessage; -import xyz.zcraft.seira.command.Context; -import xyz.zcraft.seira.command.TargetHistory; - -import java.util.function.Predicate; - -import static xyz.zcraft.seira.command.reply.ReplyFactory.at; - -public final class TargetArguments { - private TargetArguments() {} - - public static TargetResolution parse(Context ctx, Resolver resolver, TargetHistory.Ids previous, - String usage, int maxOptions) { - return parse(ctx, resolver, previous, usage, maxOptions, _ -> false); - } - - public static TargetResolution parse(Context ctx, Resolver resolver, TargetHistory.Ids previous, - String usage, int maxOptions, Predicate option) { - TargetResolution target = ctx.argumentCount() == 0 || option.test(ctx.argument(0)) - ? new TargetResolution(null, 0) - : resolver.resolveTargetWithOptionalMention(ctx.args(), ctx.senderUserId()); - if (target.target() != null && target.target().isError()) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + target.target().errorMessage())); - return null; - } - if ((target.target() == null && previous == null) - || ctx.argumentCount() - target.consumedArgs() > maxOptions) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + usage)); - return null; - } - return target; - } -} diff --git a/src/main/java/xyz/zcraft/seira/command/parse/TargetInput.java b/src/main/java/xyz/zcraft/seira/command/parse/TargetInput.java new file mode 100644 index 00000000..6d2fa2ab --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/command/parse/TargetInput.java @@ -0,0 +1,58 @@ +package xyz.zcraft.seira.command.parse; + +import xyz.zcraft.seira.command.ResolutionException; + +import java.util.Locale; +import java.util.regex.Pattern; + +public record TargetInput(Kind kind, String id, long index, String player, int consumedArgs) { + private static final Pattern PLAYER_SCORE = Pattern.compile("(?i)^(rs|rp|bp|bo)(\\d+)?$"); + private static final Pattern SET = Pattern.compile("^(\\d+)#(\\d+)$"); + + public enum Kind { MEMORY, ID, MAP, SET, SCORE, RS, RP, BP, MP } + + public static TargetInput memory() { + return new TargetInput(Kind.MEMORY, null, 1, null, 0); + } + + public static TargetInput read(String[] args) { + if (args.length == 0) return memory(); + int consumed = args.length >= 2 && PLAYER_SCORE.matcher(args[1]).matches() ? 2 : 1; + String player = consumed == 2 ? args[0] : null; + String value = args[consumed - 1].trim().toLowerCase(Locale.ROOT); + var score = PLAYER_SCORE.matcher(value); + if (score.matches()) { + if (score.group(1).equals("bo")) throw new ResolutionException("未知的快捷查询"); + long index = score.group(2) == null ? 1 : positive(score.group(2), "快捷指令索引无效,请输入 1-200 之间的数字。例如: rs5"); + if (index > 200) throw new ResolutionException("快捷指令索引无效,请输入 1-200 之间的数字。例如: rs5"); + return new TargetInput(Kind.valueOf(score.group(1).toUpperCase(Locale.ROOT)), null, index, player, consumed); + } + var set = SET.matcher(value); + if (set.matches()) { + long id = positive(set.group(1), "谱面集索引无效。例如: 12345#2"); + long index = positive(set.group(2), "谱面集索引无效。例如: 12345#2"); + return new TargetInput(Kind.SET, Long.toString(id), index, null, consumed); + } + if (value.equals("mp")) return new TargetInput(Kind.MP, null, 1, null, consumed); + if (value.matches("loc[1-9]\\d*")) return new TargetInput(Kind.SCORE, value, 1, null, consumed); + if (value.matches("m\\d+")) { + long id = positive(value.substring(1), "谱面ID无效"); + return new TargetInput(Kind.MAP, Long.toString(id), 1, null, consumed); + } + long id = positive(value, "参数无效。请输入数字ID、本地成绩ID或快捷指令 (例如 loc123456789, rs1, 12345#2)。"); + return new TargetInput(Kind.ID, Long.toString(id), 1, null, consumed); + } + + public String scoreList() { + return kind.name().toLowerCase(Locale.ROOT); + } + + private static long positive(String value, String message) { + try { + long id = Long.parseLong(value); + if (id > 0) return id; + } catch (NumberFormatException ignored) { + } + throw new ResolutionException(message); + } +} diff --git a/src/main/java/xyz/zcraft/seira/command/parse/TargetResolution.java b/src/main/java/xyz/zcraft/seira/command/parse/TargetResolution.java deleted file mode 100644 index d41f192c..00000000 --- a/src/main/java/xyz/zcraft/seira/command/parse/TargetResolution.java +++ /dev/null @@ -1,4 +0,0 @@ -package xyz.zcraft.seira.command.parse; - -/** 仅保存语法解析结果。target 为 null 表示省略目标;consumedArgs 是选项起点,不会随读取改变。 */ -public record TargetResolution(ShortcutTarget target, int consumedArgs) {} diff --git a/src/main/java/xyz/zcraft/seira/command/parse/UserRefResolution.java b/src/main/java/xyz/zcraft/seira/command/parse/UserRefResolution.java deleted file mode 100644 index 56b5a953..00000000 --- a/src/main/java/xyz/zcraft/seira/command/parse/UserRefResolution.java +++ /dev/null @@ -1,6 +0,0 @@ -package xyz.zcraft.seira.command.parse; - -import xyz.zcraft.seira.data.UserRef; - -public record UserRefResolution(UserRef userRef, String errorMessage) { -} diff --git a/src/main/java/xyz/zcraft/seira/data/UserRef.java b/src/main/java/xyz/zcraft/seira/data/UserRef.java deleted file mode 100644 index 87dc0cc1..00000000 --- a/src/main/java/xyz/zcraft/seira/data/UserRef.java +++ /dev/null @@ -1,26 +0,0 @@ -package xyz.zcraft.seira.data; - -import lombok.Getter; - -public class UserRef { - private UserRef() { - } - - public static class ByUid extends UserRef { - @Getter - private final long uid; - - public ByUid(long uid) { - this.uid = uid; - } - } - - public static class ByUsername extends UserRef { - @Getter - private final String username; - - public ByUsername(String username) { - this.username = username; - } - } -} From a4e37bb1fd9c64b16809120873fb66bbc5150af8 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 20 Sep 2026 08:52:43 +0800 Subject: [PATCH 19/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=AF=B9=20beatma?= =?UTF-8?q?p-json=20=E5=92=8C=20beatmapset-json=20=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zcraft/seira/command/handler/ScoreCommandHandler.java | 1 - .../xyz/zcraft/seira/console/ConsoleCommandProcessor.java | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java index 80704b17..5e430b08 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java @@ -497,5 +497,4 @@ record TbArguments(int days, String target) { record ScoreListRange(int start, int end) { } - } diff --git a/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java b/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java index fce81843..1dc6585a 100644 --- a/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java +++ b/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java @@ -322,7 +322,7 @@ private ConsoleResult help(ConsoleInputParser.ParsedInput input) { watch clear confirm Polling runs in the watch worker. Clearing a group requires explicit confirmation."""; case "cache" -> """ - cache + cache query reports presence at oStella and every osuRenderer worker. get includes metadata; fetch populates oStella and downstream workers; delete removes every reachable copy."""; case "gateway" -> """ @@ -671,7 +671,7 @@ private ConsoleResult gateway(ConsoleInputParser.ParsedInput input) { private ConsoleResult cache(ConsoleInputParser.ParsedInput input) { if (input.size() != 4) { return ConsoleResult.failure( - "Usage: cache " + "Usage: cache " ); } String operation = input.value(1).toLowerCase(Locale.ROOT); @@ -679,8 +679,8 @@ private ConsoleResult cache(ConsoleInputParser.ParsedInput input) { return ConsoleResult.failure("Cache operation must be query, delete, get, or fetch."); } String type = input.value(2).toUpperCase(Locale.ROOT); - if (!List.of("SCORE", "BEATMAP", "BEATMAPSET", "REPLAY").contains(type)) { - return ConsoleResult.failure("Cache type must be score, beatmap, beatmapset, or replay."); + if (!List.of("SCORE", "BEATMAP", "BEATMAPSET", "REPLAY", "BEATMAP-JSON", "BEATMAPSET-JSON").contains(type)) { + return ConsoleResult.failure("Cache type must be score, beatmap, beatmapset, replay, beatmap-json, or beatmapset-json."); } long id = positiveLong(input.value(3), "id"); return ConsoleResult.success(formatCacheControl(runtimeControl.controlCache(operation, type, id))); From a0e85496ecb6b73f9576cc5564695e5a3af6c299 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 20 Sep 2026 10:16:42 +0800 Subject: [PATCH 20/96] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AF=B9=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E4=BB=A4=E7=89=8C=E7=9A=84=E6=94=AF=E6=8C=81=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20API=20=E8=AF=B7=E6=B1=82=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/xyz/zcraft/seira/api/APIHelper.java | 77 +++++++++++-------- src/main/java/xyz/zcraft/seira/bot/QQBot.java | 6 +- .../zcraft/seira/config/OstellaConfig.java | 3 +- .../console/OstellaCacheControlClient.java | 16 +++- .../watch/OstellaMultiplayerRoomWatchApi.java | 22 ++++-- .../zcraft/seira/watch/OstellaWatchApi.java | 22 ++++-- src/main/resources/seira-example-config.yml | 1 + 7 files changed, 96 insertions(+), 51 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/api/APIHelper.java b/src/main/java/xyz/zcraft/seira/api/APIHelper.java index ef61f247..ab863d0c 100644 --- a/src/main/java/xyz/zcraft/seira/api/APIHelper.java +++ b/src/main/java/xyz/zcraft/seira/api/APIHelper.java @@ -26,7 +26,9 @@ import java.util.Map; public class APIHelper { + private static final String OSU_AUTHORIZATION_HEADER = "X-Osu-Authorization"; private static final String ENDPOINT; + private static final String TOKEN; private static final HttpClient CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofMinutes(5)).build(); private static final Gson GSON = new Gson(); private static final int REPLAY_POLL_INTERVAL_MS = 5000; @@ -34,13 +36,25 @@ public class APIHelper { static { ENDPOINT = Seira.getConfig().ostella().endpoint(); + TOKEN = Seira.getConfig().ostella().token(); + } + + private static HttpRequest.Builder requestBuilder() { + HttpRequest.Builder builder = HttpRequest.newBuilder(); + if (TOKEN != null && !TOKEN.isBlank()) { + builder.header("Authorization", "Bearer " + TOKEN); + } + return builder; + } + + private static HttpRequest.Builder withOsuAuthorization(HttpRequest.Builder builder, String accessToken) { + return builder.header(OSU_AUTHORIZATION_HEADER, "Bearer " + accessToken); } public static Response> getFollowed(String accessToken) { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = withOsuAuthorization(requestBuilder(), accessToken) .uri(URI.create(ENDPOINT + "/users/me/friends")) - .header("Authorization", "Bearer " + accessToken) .GET() .build(); @@ -70,9 +84,8 @@ public static Response> getFollowed(String accessToken) { public static Response getSelf(String accessToken) { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = withOsuAuthorization(requestBuilder(), accessToken) .uri(URI.create(ENDPOINT + "/users/me")) - .header("Authorization", "Bearer " + accessToken) .GET() .build(); @@ -121,7 +134,7 @@ public static Response getUserInfoResponse(long uid) { public static UserExtended getUserRaw(long uid) { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/users/" + uid)) .header("Accept", "application/json") .GET() @@ -174,7 +187,7 @@ public static Response getLeaderboardResponse(List uids) { public static String getDaily() { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/daily")) .GET() .build(); @@ -214,9 +227,8 @@ public static String getDaily() { public static Response getMultiplayerRoom(String accessToken) { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = withOsuAuthorization(requestBuilder(), accessToken) .uri(URI.create(ENDPOINT + "/multiplayer/rooms/current")) - .header("Authorization", "Bearer " + accessToken) .GET() .build(); @@ -300,7 +312,7 @@ public static Response getBeatmapsetResponse(long beatmapsetId) { public static Beatmapset getBeatmapsetRaw(long id) { try { - var builder = HttpRequest.newBuilder() + var builder = requestBuilder() .uri(URI.create(ENDPOINT + "/beatmapsets/" + id)) .header("Accept", "application/json"); @@ -334,7 +346,7 @@ public static Response getMissVisualizeResponse(String scoreId, int private static Response getBase64BytesResponse(String query, String failMessage, @Nullable String postBody) { try { - var builder = HttpRequest.newBuilder() + var builder = requestBuilder() .uri(URI.create(ENDPOINT + query)); if (postBody != null) { @@ -363,9 +375,8 @@ public static Response getLookupBeatmapsetResponse(long beatmapsetId, String try { final String query = "/beatmapsets/lookup?ms=" + beatmapsetId; - HttpRequest localRequest = HttpRequest.newBuilder() + HttpRequest localRequest = withOsuAuthorization(requestBuilder(), auth) .uri(URI.create(ENDPOINT + query)) - .header("Authorization", "Bearer " + auth) .GET() .build(); @@ -389,7 +400,7 @@ public static Response getLookupBeatmapsetResponse(long beatmapsetId, String public static Response> searchBeatmapSetResponse(SearchQuery query) { try { - HttpRequest localRequest = HttpRequest.newBuilder() + HttpRequest localRequest = requestBuilder() .uri(URI.create(ENDPOINT + "/beatmapsets/search?" + "q=" + URLEncoder.encode(query.query(), StandardCharsets.UTF_8))) .GET() .build(); @@ -427,7 +438,7 @@ public static ReplayTaskInfo createObscuredReplayRenderTask(long scoreId, QqUplo } TimeDurationParser.TimeRange timeRange = getScoreHighlight(scoreId, 10); - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/replays/renders/score/" + scoreId + "?obscured=true" + timeRange.toQueryString())) .header("Content-Type", "application/json") @@ -439,7 +450,7 @@ public static ReplayTaskInfo createObscuredReplayRenderTask(long scoreId, QqUplo public static RandomScore getRandomScore() { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/scores/random?min_rank=500000")) .GET() .build(); @@ -477,7 +488,7 @@ public static String getRandomScoreWeight(Long userId, JsonObject weights, boole body.add("weight_factor", weights); - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/scores/random/users/" + userId + "/weights?all=" + all)) .POST(HttpRequest.BodyPublishers.ofString(body.toString())) .build(); @@ -510,7 +521,7 @@ public static RandomScore getRandomScoreFromUsers(List uids, JsonObject we body.add("uids", uidsArray); body.add("weight_factor", weights); - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/scores/random/users")) .POST(HttpRequest.BodyPublishers.ofString(body.toString())) .build(); @@ -558,7 +569,7 @@ public static ReplayTaskInfo createBeatmapPreviewTask(long beatmapId, String mod rangeQuery += range.toQueryString(); } - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/replays/renders/preview/" + beatmapId + rangeQuery)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body.toString())) @@ -578,7 +589,7 @@ public static ReplayTaskInfo createReplayShowcaseTask(long beatmapId, String[] s if (qqUpload != null) { body.add("qqUpload", GSON.toJsonTree(qqUpload)); } - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/replays/renders/showcase/" + beatmapId)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body.toString())) @@ -600,7 +611,7 @@ public static ReplayTaskInfo createReplayRenderTask(String scoreId, timeRange = getScoreHighlight(scoreId, 5); } - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/replays/renders/score/" + scoreId + "?" + timeRange.toQueryString())) .header("Content-Type", "application/json") .POST(renderRequestBody(qqUpload)) @@ -616,7 +627,7 @@ private static TimeDurationParser.TimeRange getScoreHighlight(long scoreId, int private static TimeDurationParser.TimeRange getScoreHighlight(String scoreId, int extend) { try { - HttpRequest localRequest = HttpRequest.newBuilder() + HttpRequest localRequest = requestBuilder() .uri(URI.create(ENDPOINT + "/scores/" + scoreId + "/highlight")) .GET() .build(); @@ -686,8 +697,8 @@ private static String lookupScore(String query, List filters, String mod private static JsonObject lookupTargetData(String query, String auth, String error) { try { - var request = HttpRequest.newBuilder().uri(URI.create(ENDPOINT + query)).GET(); - if (auth != null) request.header("Authorization", "Bearer " + auth); + var request = requestBuilder().uri(URI.create(ENDPOINT + query)).GET(); + if (auth != null) withOsuAuthorization(request, auth); var response = CLIENT.send(request.build(), HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) throw parseHttpError(response.body(), response.statusCode(), error); RawResponse payload = GSON.fromJson(response.body(), RawResponse.class); @@ -704,7 +715,7 @@ public static long getScoreBeatmapId(String scoreId) { private static JsonObject lookupScoreData(String scoreId) { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/scores/lookup?s=" + scoreId)) .GET() .build(); @@ -798,7 +809,7 @@ private static FileInfo waitReplayDone(String taskId, long timeout) { private static JsonObject getReplayStatus(String taskId) { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/replays/" + taskId + "/status")) .GET() .build(); @@ -902,7 +913,7 @@ private static JsonObject requireDataObject(RawResponse payload, String message) public static RenderStat getRenderStat(String jobId) { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/replays/" + jobId + "/status")) .GET() .build(); @@ -925,7 +936,7 @@ public static RenderStat getRenderStat(String jobId) { public static RenderStat cancelReplayRender(String jobId) { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/replays/" + jobId + "/cancel")) .POST(HttpRequest.BodyPublishers.noBody()) .build(); @@ -950,7 +961,7 @@ public static ServerStatus getServerStatus() { boolean osu = false; String oStellaVersion = null; try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/health")) .GET() .build(); @@ -981,7 +992,7 @@ public static ServerStatus getServerStatus() { public static Response> getScoreMissesResponse(String scoreId) { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/scores/" + scoreId + "/misses")) .GET() .build(); @@ -1026,7 +1037,7 @@ public static long resolveUid(String player) { public static long getUserRank(long uid) { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/users/" + uid + "/rank")) .header("Content-Type", "application/json") .GET() @@ -1052,7 +1063,7 @@ public static long getUserRank(long uid) { public static Response lookupUser(String username) { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/users/lookup")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString( @@ -1083,7 +1094,7 @@ public static Response lookupUser(String username) { public static List getUsers(List u) { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/users")) .POST(HttpRequest.BodyPublishers.ofString(GSON.toJsonTree(Map.of("ids", u)).toString())) .build(); @@ -1111,7 +1122,7 @@ public static List getUsers(List u) { public static ReplayUploadInfo uploadReplay(byte[] replayBytes) { try { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/replays/upload")) .POST(HttpRequest.BodyPublishers.ofByteArray(replayBytes)) .build(); diff --git a/src/main/java/xyz/zcraft/seira/bot/QQBot.java b/src/main/java/xyz/zcraft/seira/bot/QQBot.java index bead781f..08813483 100644 --- a/src/main/java/xyz/zcraft/seira/bot/QQBot.java +++ b/src/main/java/xyz/zcraft/seira/bot/QQBot.java @@ -66,7 +66,7 @@ public QQBot( AppConfig config = runtimeConfig.current(); this.startupConfig = config; this.executors = executors; - this.cacheControlClient = new OstellaCacheControlClient(config.ostella().endpoint()); + this.cacheControlClient = new OstellaCacheControlClient(config.ostella().endpoint(), config.ostella().token()); LOG.info("Authorizing QQ API"); this.tokenManager = new TokenManager(config.qq().appId(), config.qq().appSecret()); @@ -82,7 +82,7 @@ public QQBot( LOG.info("Initializing score watch service"); this.watchService = new ScoreWatchService( - new OstellaWatchApi(config.ostella().endpoint()), + new OstellaWatchApi(config.ostella().endpoint(), config.ostella().token()), new WatchScoreNotifier(sender), new SpecificScoreNotifier(sender), new SqliteSpecificScoreWatchStore(), @@ -91,7 +91,7 @@ public QQBot( LOG.info("Initializing multiplayer room watch service"); this.multiplayerRoomWatchService = new MultiplayerRoomWatchService( - new OstellaMultiplayerRoomWatchApi(config.ostella().endpoint()), + new OstellaMultiplayerRoomWatchApi(config.ostella().endpoint(), config.ostella().token()), new QqMultiplayerRoomNotifier(sender), Duration.ofSeconds(config.seira().effectiveMultiplayerWatchIntervalSeconds()) ); diff --git a/src/main/java/xyz/zcraft/seira/config/OstellaConfig.java b/src/main/java/xyz/zcraft/seira/config/OstellaConfig.java index f1b1b537..a27df811 100644 --- a/src/main/java/xyz/zcraft/seira/config/OstellaConfig.java +++ b/src/main/java/xyz/zcraft/seira/config/OstellaConfig.java @@ -1,6 +1,7 @@ package xyz.zcraft.seira.config; public record OstellaConfig( - String endpoint + String endpoint, + String token ) { } diff --git a/src/main/java/xyz/zcraft/seira/console/OstellaCacheControlClient.java b/src/main/java/xyz/zcraft/seira/console/OstellaCacheControlClient.java index 26cb59cf..485cbfce 100644 --- a/src/main/java/xyz/zcraft/seira/console/OstellaCacheControlClient.java +++ b/src/main/java/xyz/zcraft/seira/console/OstellaCacheControlClient.java @@ -13,19 +13,27 @@ public final class OstellaCacheControlClient { private static final Gson GSON = new Gson(); private final HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); private final URI endpoint; + private final String serviceToken; public OstellaCacheControlClient(String endpoint) { + this(endpoint, null); + } + + public OstellaCacheControlClient(String endpoint, String serviceToken) { String normalized = endpoint.replaceAll("/+$", ""); this.endpoint = URI.create(normalized + "/cache/control"); + this.serviceToken = serviceToken; } public ConsoleRuntimeControl.CacheControlResult control(String operation, String type, long id) { String body = GSON.toJson(new Request(operation, type, id)); - HttpRequest request = HttpRequest.newBuilder(endpoint) + HttpRequest.Builder builder = HttpRequest.newBuilder(endpoint) .timeout(Duration.ofSeconds(40)) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) - .build(); + .header("Content-Type", "application/json"); + if (serviceToken != null && !serviceToken.isBlank()) { + builder.header("Authorization", "Bearer " + serviceToken); + } + HttpRequest request = builder.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)).build(); try { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() < 200 || response.statusCode() >= 300) { diff --git a/src/main/java/xyz/zcraft/seira/watch/OstellaMultiplayerRoomWatchApi.java b/src/main/java/xyz/zcraft/seira/watch/OstellaMultiplayerRoomWatchApi.java index 57c69ac3..e9eb6a48 100644 --- a/src/main/java/xyz/zcraft/seira/watch/OstellaMultiplayerRoomWatchApi.java +++ b/src/main/java/xyz/zcraft/seira/watch/OstellaMultiplayerRoomWatchApi.java @@ -14,15 +14,25 @@ public final class OstellaMultiplayerRoomWatchApi implements MultiplayerRoomWatchApi { private final String endpoint; + private final String serviceToken; private final HttpClient client; private final Gson gson; public OstellaMultiplayerRoomWatchApi(String endpoint) { - this(endpoint, HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(), new Gson()); + this(endpoint, null); + } + + public OstellaMultiplayerRoomWatchApi(String endpoint, String serviceToken) { + this(endpoint, serviceToken, HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(), new Gson()); } OstellaMultiplayerRoomWatchApi(String endpoint, HttpClient client, Gson gson) { + this(endpoint, null, client, gson); + } + + OstellaMultiplayerRoomWatchApi(String endpoint, String serviceToken, HttpClient client, Gson gson) { this.endpoint = endpoint.endsWith("/") ? endpoint.substring(0, endpoint.length() - 1) : endpoint; + this.serviceToken = serviceToken; this.client = client; this.gson = gson; } @@ -104,12 +114,14 @@ public byte[] renderResult(MultiplayerRoomVersion version, long roomId, long pla } private HttpResponse get(String path, HttpResponse.BodyHandler handler) { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest.Builder builder = HttpRequest.newBuilder() .uri(URI.create(endpoint + path)) .timeout(Duration.ofMinutes(2)) - .header("Accept", "application/json, image/*") - .GET() - .build(); + .header("Accept", "application/json, image/*"); + if (serviceToken != null && !serviceToken.isBlank()) { + builder.header("Authorization", "Bearer " + serviceToken); + } + HttpRequest request = builder.GET().build(); try { return client.send(request, handler); } catch (InterruptedException e) { diff --git a/src/main/java/xyz/zcraft/seira/watch/OstellaWatchApi.java b/src/main/java/xyz/zcraft/seira/watch/OstellaWatchApi.java index e06a8f4d..50ec068f 100644 --- a/src/main/java/xyz/zcraft/seira/watch/OstellaWatchApi.java +++ b/src/main/java/xyz/zcraft/seira/watch/OstellaWatchApi.java @@ -23,15 +23,25 @@ public class OstellaWatchApi implements WatchApi { }.getType(); private final String endpoint; + private final String serviceToken; private final HttpClient client; private final Gson gson; public OstellaWatchApi(String endpoint) { - this(endpoint, HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(), new Gson()); + this(endpoint, null); + } + + public OstellaWatchApi(String endpoint, String serviceToken) { + this(endpoint, serviceToken, HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(), new Gson()); } OstellaWatchApi(String endpoint, HttpClient client, Gson gson) { + this(endpoint, null, client, gson); + } + + OstellaWatchApi(String endpoint, String serviceToken, HttpClient client, Gson gson) { this.endpoint = endpoint.endsWith("/") ? endpoint.substring(0, endpoint.length() - 1) : endpoint; + this.serviceToken = serviceToken; this.client = client; this.gson = gson; } @@ -110,13 +120,15 @@ public byte[] renderScore(long userId, long scoreId) { } private HttpResponse send(String path, String body, HttpResponse.BodyHandler handler) { - HttpRequest request = HttpRequest.newBuilder() + HttpRequest.Builder builder = HttpRequest.newBuilder() .uri(URI.create(endpoint + path)) .timeout(Duration.ofMinutes(2)) .header("Content-Type", "application/json") - .header("Accept", "application/json, image/*") - .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) - .build(); + .header("Accept", "application/json, image/*"); + if (serviceToken != null && !serviceToken.isBlank()) { + builder.header("Authorization", "Bearer " + serviceToken); + } + HttpRequest request = builder.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)).build(); try { return client.send(request, handler); } catch (InterruptedException e) { diff --git a/src/main/resources/seira-example-config.yml b/src/main/resources/seira-example-config.yml index c83dd5a8..94273e3d 100644 --- a/src/main/resources/seira-example-config.yml +++ b/src/main/resources/seira-example-config.yml @@ -29,6 +29,7 @@ binding: ostella: # oStella API 的地址,由 Seira 访问,默认为本地运行的 oStella 实例 endpoint: "http://localhost:8721" + token: # === QQ机器人 配置 === qq: From ee19b1a409e7c75d962e6f607f44620ca9c76f4d Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 20 Sep 2026 10:56:08 +0800 Subject: [PATCH 21/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=B6=85=E6=97=B6=E9=80=9A=E7=9F=A5=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zcraft/seira/command/TaskCoordinator.java | 40 +++++++++++-------- .../handler/RankGuessCommandHandler.java | 2 +- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java index e957fcb2..beec95f7 100644 --- a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java +++ b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java @@ -1,6 +1,5 @@ package xyz.zcraft.seira.command; -import com.google.gson.Gson; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; @@ -19,7 +18,6 @@ import xyz.zcraft.seira.services.BotStat; import java.nio.channels.ClosedChannelException; -import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -27,11 +25,10 @@ import java.util.concurrent.atomic.AtomicInteger; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; -import static xyz.zcraft.seira.command.reply.ReplyFactory.cmd; public final class TaskCoordinator { private static final Logger LOG = LogManager.getLogger(TaskCoordinator.class); - + private static final ScheduledExecutorService TIMEOUT_SCHEDULER = Executors.newSingleThreadScheduledExecutor(); private final MessageSender messageSender; private final DiscordBridgeService discordBridgeService; private final ApiRequestStats apiRequestStats = new ApiRequestStats(); @@ -80,23 +77,25 @@ public CommandReplyChannel openReplyChannel( return new OutboundReplyChannel(targetId, messageId, groupMessage, queueMessageInGroup); } - private static final ScheduledExecutorService TIMEOUT_SCHEDULER = Executors.newSingleThreadScheduledExecutor(); - /** * Tracks queue estimates and elapsed time; the caller executes the request directly. */ - public RequestTiming beginRequest(Context ctx, String requestType) { + public RequestTiming beginRequest(Context ctx, String requestType, boolean timeoutNotify) { long estimatedSeconds = apiRequestStats.estimateAndEnqueue(requestType); - - final var schedule = TIMEOUT_SCHEDULER.schedule( - () -> { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "请求处理时间超过预期,这可能是由于相关数据缺少缓存,请耐心等待喵。")); - }, - 60, - TimeUnit.SECONDS - ); + ScheduledFuture schedule = null; + + if (timeoutNotify) { + schedule = TIMEOUT_SCHEDULER.schedule( + () -> { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "请求处理时间超过预期,这可能是由于相关数据缺少缓存,请耐心等待喵。")); + }, + 60, + TimeUnit.SECONDS + ); + } RequestTiming timing = new RequestTiming(requestType, schedule); + try { ctx.sendQueueNotice(PendingMessage.ofMarkdownRaw( at(ctx) + "请求已加入队列,预计等待时间" + estimatedSeconds + "秒。")); @@ -107,6 +106,10 @@ public RequestTiming beginRequest(Context ctx, String requestType) { } } + public RequestTiming beginRequest(Context ctx, String requestType) { + return beginRequest(ctx, requestType, true); + } + public PendingMessage imageMessage(Response response, PendingMessage completion) { UploadedImage image = messageSender.uploadImageToCos(response.getContent().bytes()); return combineImageAndCompletion(image, completion); @@ -241,8 +244,8 @@ public SendResult sendOutboundMessage(String targetId, String messageId, boolean public final class RequestTiming implements AutoCloseable { private final String requestType; private final long startedAt = System.nanoTime(); - private boolean closed; private final ScheduledFuture scheduledFuture; + private boolean closed; private RequestTiming(String requestType, ScheduledFuture schedule) { this.requestType = requestType; @@ -251,7 +254,10 @@ private RequestTiming(String requestType, ScheduledFuture schedule) { @Override public void close() { - scheduledFuture.cancel(true); + if (scheduledFuture != null) { + scheduledFuture.cancel(true); + } + if (closed) return; closed = true; apiRequestStats.complete(requestType, diff --git a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java index b8531999..bd36f636 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java @@ -436,7 +436,7 @@ private void start(Context ctx, boolean fromGroup) { } boolean activated = false; - try (var _ = taskCoordinator.beginRequest(ctx, "Rank Guess Render")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Rank Guess Render", false)) { final PendingMessage message = PendingMessage.ofMarkdownRaw(at(ctx) + RandomReply.loading()); final boolean activeMessageEnabled = ctx.sendMessage(message).success(); if (!activeMessageEnabled) { From ec81ae4993f1b5225cfbdf73458c0757ac06d1c4 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 20 Sep 2026 18:14:17 +0800 Subject: [PATCH 22/96] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20AI=20=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E5=8A=9F=E8=83=BD=EF=BC=8C=E9=87=8D=E6=9E=84=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=9C=8D=E5=8A=A1=E4=B8=8E=E6=9D=83=E9=99=90=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 - .../xyz/zcraft/seira/SeiraApplication.java | 7 +- .../xyz/zcraft/seira/ai/AgentService.java | 201 ++++++++++++++++++ .../xyz/zcraft/seira/ai/AiChatHandler.java | 122 +++++++++++ .../seira/ai/data/AppConversationBrief.java | 24 +++ .../seira/ai/data/ChatQueryResponse.java | 42 ++++ src/main/java/xyz/zcraft/seira/bot/QQBot.java | 11 +- .../xyz/zcraft/seira/command/Context.java | 1 - .../seira/command/parse/CommandParser.java | 14 +- .../zcraft/seira/command/parse/Resolver.java | 17 +- .../zcraft/seira/command/route/Router.java | 53 ++++- .../xyz/zcraft/seira/config/AppConfig.java | 14 +- .../xyz/zcraft/seira/config/LLMConfig.java | 7 + .../zcraft/seira/config/RuntimeConfig.java | 3 +- .../console/ConsoleCommandProcessor.java | 45 +++- .../zcraft/seira/services/AiPermission.java | 122 +++++++++++ src/main/resources/seira-example-config.yml | 5 + 17 files changed, 652 insertions(+), 38 deletions(-) create mode 100644 src/main/java/xyz/zcraft/seira/ai/AgentService.java create mode 100644 src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java create mode 100644 src/main/java/xyz/zcraft/seira/ai/data/AppConversationBrief.java create mode 100644 src/main/java/xyz/zcraft/seira/ai/data/ChatQueryResponse.java create mode 100644 src/main/java/xyz/zcraft/seira/config/LLMConfig.java create mode 100644 src/main/java/xyz/zcraft/seira/services/AiPermission.java diff --git a/README.md b/README.md index aa9d3200..bd863bdc 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,6 @@ Seira正在活跃开发中,在使用的过程中可能会有一些Bug,也会 省略玩家目标时,使用快捷查询前需要先执行 `/bind`,否则会提示无法使用快捷查询。 -> 由于QQ业务调整,暂时无法使用`@用户`查询绑定信息,请改用直接输入uid的方式。 - ### 回放上传 你可以在私聊中直接发送你的回放文件,Seira会将其转存。 diff --git a/src/main/java/xyz/zcraft/seira/SeiraApplication.java b/src/main/java/xyz/zcraft/seira/SeiraApplication.java index dbf49881..b0d1be9b 100644 --- a/src/main/java/xyz/zcraft/seira/SeiraApplication.java +++ b/src/main/java/xyz/zcraft/seira/SeiraApplication.java @@ -11,10 +11,7 @@ import xyz.zcraft.seira.console.JLineConsole; import xyz.zcraft.seira.console.UserDataConsoleAccess; import xyz.zcraft.seira.db.SqliteDatabase; -import xyz.zcraft.seira.services.BindingService; -import xyz.zcraft.seira.services.BotStat; -import xyz.zcraft.seira.services.DailyLuck; -import xyz.zcraft.seira.services.NoticeStore; +import xyz.zcraft.seira.services.*; import xyz.zcraft.seira.util.AdminRegistry; import xyz.zcraft.seira.util.ApplicationExecutors; @@ -47,6 +44,7 @@ public SeiraApplication(AppConfig config) { DailyLuck.initialize(config.qq().appId()); BotStat.initialize(); NoticeStore.initialize(); + AiPermission.initialize(); ApplicationExecutors createdExecutors = new ApplicationExecutors(); BindingService createdBindingService = new BindingService( @@ -97,6 +95,7 @@ public void close() { bot.close(); DailyLuck.saveToFile(); BotStat.shutdown(); + AiPermission.saveToFile(); LOG.info("Shutdown complete"); } } diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java new file mode 100644 index 00000000..7c0d952f --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -0,0 +1,201 @@ +package xyz.zcraft.seira.ai; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import xyz.zcraft.seira.ai.data.AppConversationBrief; +import xyz.zcraft.seira.ai.data.ChatQueryResponse; +import xyz.zcraft.seira.command.Context; +import xyz.zcraft.seira.config.LLMConfig; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public class AgentService { + private final Api api; + private final Map states = new ConcurrentHashMap<>(); + + public AgentService(LLMConfig config) { + this.api = new Api(config); + } + + public String input(String groupId, String openId, String input, Consumer> var) { + final StateOwner owner = StateOwner.of(groupId, openId); + + final State state = states.computeIfAbsent( + owner, + _ -> new State( + api.createConversation(groupId), + new ConcurrentHashMap<>(), + new AtomicBoolean(false) + ) + ); + + if (!state.running().compareAndSet(false, true)) { + throw new IllegalStateException("已有请求正在运行"); + } + + try { + if (var != null) { + final int oldHash = state.vars().hashCode(); + + var.accept(state.vars()); + + if (state.vars().hashCode() != oldHash) { + api.updateConversation( + groupId, state.conv().appConversationID(), state.vars() + ); + } + } + + final ChatQueryResponse response = api.chatQuery( + groupId, state.conv().appConversationID(), input + ); + + return response.answer(); + } finally { + state.running().set(false); + } + } + + public boolean isRunning(String groupId, String openId) { + final State state = states.get(StateOwner.of(groupId, openId)); + return state != null && state.running().get(); + } + + public void clearState(String groupId, String openId) { + states.remove(StateOwner.of(groupId, openId)); + } + + record State( + AppConversationBrief conv, ConcurrentHashMap vars, + AtomicBoolean running + ) { + } + + record StateOwner(String groupId, String openId) { + public static StateOwner of(String groupId, String openId) { + return new StateOwner(groupId, openId); + } + + public static StateOwner of(Context ctx) { + return new StateOwner(ctx.groupId(), ctx.senderUserId()); + } + } +} + +class Api { + private static final Logger LOG = LogManager.getLogger(Api.class); + public final HttpClient CLIENT = HttpClient.newHttpClient(); + public final Gson GSON = new Gson(); + public final String apiKey; + public final String endpoint; + + public Api(LLMConfig config) { + this.endpoint = config.baseUrl(); + this.apiKey = config.apiKey(); + } + + public AppConversationBrief createConversation(String openId) { + LOG.info("Creating conversation for user {}", openId); + JsonObject body = new JsonObject(); + body.addProperty("UserID", openId); + try { + var request = newRequest("/api/proxy/api/v1/create_conversation") + .POST(HttpRequest.BodyPublishers.ofString(body.toString())) + .build(); + + final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + + if (send.statusCode() != 200) { + throw new RuntimeException("Failed to create conversation: " + send.statusCode()); + } + + final var response = JsonParser.parseString(send.body()).getAsJsonObject(); + + final AppConversationBrief conversation = GSON.fromJson( + response.getAsJsonObject("Conversation"), + AppConversationBrief.class + ); + + LOG.info("Conversation for user {} created, id {}", openId, conversation.appConversationID()); + return conversation; + } catch (Exception e) { + throw new RuntimeException("Error creating conversation", e); + } + } + + public void updateConversation(String openId, String appConvId, Map variables) { + LOG.info("Updating conversation for user {}", openId); + + JsonObject body = new JsonObject(); + body.addProperty("UserID", openId); + body.addProperty("AppConversationID", appConvId); + body.add("Inputs", GSON.toJsonTree(variables)); + + try { + var request = newRequest("/api/proxy/api/v1/update_conversation") + .POST(HttpRequest.BodyPublishers.ofString(body.toString())) + .build(); + + final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + + if (send.statusCode() != 200) { + throw new RuntimeException("Failed to update conversation: " + send.statusCode()); + } + + LOG.info("Updated conversation for user {}", openId); + } catch (Exception e) { + throw new RuntimeException("Error updating conversation", e); + } + } + + public ChatQueryResponse chatQuery(String openId, String appConvId, String query) { + LOG.info("Running chat query for user {}", openId); + + JsonObject body = new JsonObject(); + body.addProperty("UserID", openId); + body.addProperty("AppConversationID", appConvId); + body.addProperty("Query", query); + body.addProperty("ResponseMode", "blocking"); + + try { + var request = newRequest("/api/proxy/api/v1/chat_query_v2") + .POST(HttpRequest.BodyPublishers.ofString(body.toString())) + .build(); + + final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + + if (send.statusCode() != 200) { + throw new RuntimeException("Failed to query conversation: " + send.statusCode()); + } + + final ChatQueryResponse chatQueryResponse = GSON.fromJson(send.body(), ChatQueryResponse.class); + + LOG.info( + "Chat query success for group {}, Token input:{}, output:{}", + openId, + chatQueryResponse.inputTokens(), + chatQueryResponse.outputTokens() + ); + + return chatQueryResponse; + } catch (Exception e) { + throw new RuntimeException("Error querying conversation", e); + } + } + + private HttpRequest.Builder newRequest(String path) { + return HttpRequest.newBuilder() + .uri(java.net.URI.create(this.endpoint + path)) + .header("Apikey", this.apiKey) + .header("Content-Type", "application/json"); + } +} diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java new file mode 100644 index 00000000..13fce1b8 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -0,0 +1,122 @@ +package xyz.zcraft.seira.ai; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import xyz.zcraft.seira.bot.data.GroupBotState; +import xyz.zcraft.seira.bot.data.PendingMessage; +import xyz.zcraft.seira.command.Context; +import xyz.zcraft.seira.command.parse.Resolver; +import xyz.zcraft.seira.db.UserDataStore; +import xyz.zcraft.seira.services.AiPermission; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; + +import static xyz.zcraft.seira.command.reply.ReplyFactory.at; + +public class AiChatHandler { + private static final Gson GSON = new Gson(); + private final Resolver resolver; + private final Predicate adminAuthorizer; + private final AgentService agentService; + private final Function botStateGetter; + + public AiChatHandler( + Resolver resolver, AgentService agentService, Predicate isAdmin, + Function botStateGetter + ) { + this.resolver = resolver; + this.adminAuthorizer = isAdmin; + this.agentService = agentService; + this.botStateGetter = botStateGetter; + } + + public void handleAi(Context ctx) { + if (!ctx.inGroup()) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "AI对话仅在群组中可用喵。")); + return; + } + + if (ctx.argumentCount() == 0) { + final boolean b = AiPermission.doPermit(ctx.groupId()); + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "目前AI对话在本群启用状态为:" + (b ? "√" : "×"))); + return; + } else if (ctx.argumentCount() == 1 + && List.of("on", "off", "reset").contains(ctx.argument(0).toLowerCase(Locale.ROOT))) { + if (!adminAuthorizer.test(ctx.senderUserId())) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "你无权使用该命令喵。")); + return; + } + + if ("on".equalsIgnoreCase(ctx.argument(0))) { + final GroupBotState apply = botStateGetter.apply(ctx.groupId()); + if (apply.allowProactiveMsg() && apply.receiveMsgSetting() == GroupBotState.ReceiveMsgSetting.ALL) { + AiPermission.permit(ctx.groupId()); + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已启用本群AI对话喵。")); + } else { + ctx.sendReply(PendingMessage.ofMarkdownRaw( + at(ctx) + "由于本群未配置权限或配置不完整,暂无法启用本群AI对话喵。" + + "权限配置见[这里](https://docs.seira.top/overview/use.html#extra-permission)~") + ); + } + return; + } else if ("off".equalsIgnoreCase(ctx.argument(0))) { + AiPermission.revoke(ctx.groupId()); + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已禁用本群AI对话喵。")); + return; + } else if ("reset".equalsIgnoreCase(ctx.argument(0))) { + agentService.clearState(ctx.groupId(), ctx.argument(1)); + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已重置本群AI对话状态喵。")); + return; + } + } + + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "用法:/ai [on|off]")); + } + + public void handleChat(Context ctx) { + if (agentService.isRunning(ctx.groupId(), ctx.senderUserId())) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已有一轮对话正在进行中了喵,请稍作等待~")); + return; + } + + JsonObject qqContext = new JsonObject(); + qqContext.addProperty("in_group", ctx.inGroup()); + qqContext.addProperty("sender_open_id", ctx.senderUserId()); + qqContext.addProperty("group_id", ctx.groupId()); + + Map bindings = new HashMap<>(); + Map usernames = new HashMap<>(); + + final List ids = resolver.extractAllMentionedIds(ctx.rawContent()); + ids.add(ctx.senderUserId()); + + for (String openId : ids) { + final Long uid = resolver.resolveBoundUid(ctx.senderUserId()); + if (uid != null) { + bindings.put(openId, uid); + UserDataStore.findUsername(uid).ifPresent(s -> usernames.put(uid, s)); + } + } + + qqContext.add("bindings", GSON.toJsonTree(bindings)); + qqContext.add("usernames", GSON.toJsonTree(usernames)); + + final String answer = at(ctx) + agentService.input( + ctx.groupId(), + ctx.senderUserId(), + ctx.senderUserId() + ": " + ctx.rawContent(), + var -> var.put("CONTEXT", qqContext.toString()) + ); + + if (!ctx.sendReply(PendingMessage.ofMarkdownRaw(answer)).success()) { + if (!ctx.sendMessage(PendingMessage.ofMarkdownRaw(answer)).success()) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "消息发送失败了喵。")); + } + } + } +} diff --git a/src/main/java/xyz/zcraft/seira/ai/data/AppConversationBrief.java b/src/main/java/xyz/zcraft/seira/ai/data/AppConversationBrief.java new file mode 100644 index 00000000..902f4c33 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/ai/data/AppConversationBrief.java @@ -0,0 +1,24 @@ +package xyz.zcraft.seira.ai.data; + +import com.google.gson.annotations.SerializedName; + + +/* +{ + "AppConversationID" : "danob6v71098vo7jfsq0", + "ConversationName" : "新的会话", + "CreateTime" : "2026-09-20 15:04:59", + "CreateTimestamp" : 1789887899, + "LastChatTime" : "", + "LastChatTimestamp" : 0, + "EmptyConversation" : false, + "IsPinned" : false, + "ConversationID" : "01M2YT3SNDMQ34FZF2WJD8FTKF" + } + */ +public record AppConversationBrief( + @SerializedName("AppConversationID") String appConversationID, + @SerializedName("ConversationID") String conversationID, + @SerializedName("ConversationName") String conversationName +) { +} diff --git a/src/main/java/xyz/zcraft/seira/ai/data/ChatQueryResponse.java b/src/main/java/xyz/zcraft/seira/ai/data/ChatQueryResponse.java new file mode 100644 index 00000000..3e85e447 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/ai/data/ChatQueryResponse.java @@ -0,0 +1,42 @@ +package xyz.zcraft.seira.ai.data; + + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +/* +{ + "total_tokens": 993, + "event": "message", + "task_id": "01K0P6NXPHD62CXY80J3HPJCZC", + "id": "01K0P6NXPHD62CXY80J3HPJCZC", + "conversation_id": "01K0BXVWX8CV3HX81V2SPCRP8K", + "answer": "有点遗憾知识库没找到相关内容。不过我很想知道你说“还可以”是在评价什么呀,是一部电影、一顿美食,还是其他方面呢?能多给我些提示,这样我们就能更畅快地交流啦。 ", + "created_at": 1753091868, + "latency": 4.05, + "input_tokens": 937, + "output_tokens": 56, + "start_time_first_resp": 1753091868324, + "latency_first_resp": 4050, + "think_messages": ["推理过程消息内容"], + "tool_messages": ["工具消息输出内容"] +} + */ +public record ChatQueryResponse ( + @SerializedName("total_tokens") Long totalTokens, + @SerializedName("event") String event, + @SerializedName("task_id") String taskId, + @SerializedName("id") String id, + @SerializedName("conversation_id") String conversationId, + @SerializedName("answer") String answer, + @SerializedName("created_at") Long createdAt, + @SerializedName("latency") Double latency, + @SerializedName("input_tokens") Long inputTokens, + @SerializedName("output_tokens") Long outputTokens, + @SerializedName("start_time_first_resp") Long startTimeFirstResp, + @SerializedName("latency_first_resp") Long latencyFirstResp, + @SerializedName("think_messages") List thinkMessages, + @SerializedName("tool_messages") List toolMessages +){ +} diff --git a/src/main/java/xyz/zcraft/seira/bot/QQBot.java b/src/main/java/xyz/zcraft/seira/bot/QQBot.java index 08813483..b1d77e15 100644 --- a/src/main/java/xyz/zcraft/seira/bot/QQBot.java +++ b/src/main/java/xyz/zcraft/seira/bot/QQBot.java @@ -4,6 +4,7 @@ import lombok.Getter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import xyz.zcraft.seira.ai.AgentService; import xyz.zcraft.seira.bot.data.*; import xyz.zcraft.seira.command.AttachmentHandler; import xyz.zcraft.seira.command.route.Router; @@ -44,6 +45,7 @@ public class QQBot implements AutoCloseable, ConsoleRuntimeControl { private final ScoreWatchService watchService; private final MultiplayerRoomWatchService multiplayerRoomWatchService; private final RankGuessGameService rankGuessGameService; + private final AgentService agentService; private final RealtimeServiceInterruptionNotifier interruptionNotifier; private final DiscordBridgeService discordBridgeService; private final AppConfig startupConfig; @@ -99,6 +101,10 @@ public QQBot( LOG.info("Initializing rank guess service"); this.rankGuessGameService = new RankGuessGameService(); + + LOG.info("Initializing agents service"); + this.agentService = new AgentService(config.llm()); + this.attachmentHandler = new AttachmentHandler(executors.attachmentDownloads()); this.router = new Router( sender, @@ -119,7 +125,10 @@ public QQBot( return null; } }, - self::get + self::get, + agentService, + s -> QQApi.getGroupBotState(tokenManager.getToken(), s) + ); } diff --git a/src/main/java/xyz/zcraft/seira/command/Context.java b/src/main/java/xyz/zcraft/seira/command/Context.java index d8067d42..83190fff 100644 --- a/src/main/java/xyz/zcraft/seira/command/Context.java +++ b/src/main/java/xyz/zcraft/seira/command/Context.java @@ -27,7 +27,6 @@ public Context( } public Context { - command = Objects.requireNonNull(command, "command"); args = args == null ? new String[0] : args.clone(); query = query == null ? "" : query; } diff --git a/src/main/java/xyz/zcraft/seira/command/parse/CommandParser.java b/src/main/java/xyz/zcraft/seira/command/parse/CommandParser.java index 9c341d1d..bcac396d 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/CommandParser.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/CommandParser.java @@ -1,6 +1,7 @@ package xyz.zcraft.seira.command.parse; import xyz.zcraft.seira.command.Context; +import xyz.zcraft.seira.services.AiPermission; import java.util.*; import java.util.function.UnaryOperator; @@ -52,7 +53,7 @@ public ParseResult parse(String rawContent, String senderUserId, String groupId, String normalized = rawContent.trim(); if (!normalized.startsWith(PREFIX)) { - return ParseResult.ignored(); + return ParseResult.text(new Context(senderUserId, groupId, messageId, null, null, rawContent, null)); } String body = normalized.substring(PREFIX.length()).trim(); @@ -75,8 +76,8 @@ public ParseResult parse(String rawContent, String senderUserId, String groupId, public record ParseResult(Status status, Context context) { public ParseResult { - if ((status == Status.PARSED) == (context == null)) { - throw new IllegalArgumentException("Only parsed results may contain a context"); + if ((status == Status.PARSED || status == Status.TEXT) == (context == null)) { + throw new IllegalArgumentException("Only parsed results or text may contain a context"); } } @@ -92,10 +93,15 @@ static ParseResult parsed(Context context) { return new ParseResult(Status.PARSED, Objects.requireNonNull(context)); } + static ParseResult text(Context context) { + return new ParseResult(Status.TEXT, Objects.requireNonNull(context)); + } + public enum Status { IGNORED, EMPTY_COMMAND, - PARSED + PARSED, + TEXT } } } diff --git a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java index c076e6c4..d5d51b96 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java @@ -4,6 +4,7 @@ import xyz.zcraft.seira.command.ResolutionException; import xyz.zcraft.seira.db.UserDataStore; +import java.nio.file.Path; import java.util.*; import java.util.regex.MatchResult; import java.util.regex.Matcher; @@ -129,6 +130,18 @@ public String extractMentionedUserId(String token) { return null; } + public List extractAllMentionedIds(String token) { + if (token == null) { + return List.of(); + } + + List result = new LinkedList<>(); + + Patterns.QQ_AT_PATTERN.matcher(token).results().forEach(m -> result.add(m.group(1))); + + return result; + } + public Long parsePositiveLong(String value) { try { long parsed = Long.parseLong(value); @@ -149,8 +162,8 @@ private static final class Patterns { private static final Pattern SPACE_MISSING_COMMAND_PATTERN = Pattern.compile( "^([a-zA-Z]+)(\\d+(?:#\\d+)?)" ); - private static final Pattern QQ_AT_PATTERN = Pattern.compile("^<@([A-Z|0-9]{32})>$"); - private static final Pattern QQ_INLINE_AT_PATTERN = Pattern.compile("(<@[A-Z|0-9]{32}>)"); + private static final Pattern QQ_AT_PATTERN = Pattern.compile("^<@([A-Z0-9]{32})>$"); + private static final Pattern QQ_INLINE_AT_PATTERN = Pattern.compile("(<@[A-Z0-9]{32}>)"); private static final Pattern PLAIN_AT_PATTERN = Pattern.compile("^@(\\d+)$"); private static final Pattern SEARCH_PATTERN = Pattern.compile("^(?:#(\\d+) )?(.+)$"); } diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index c710c934..e71f0824 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -3,10 +3,13 @@ import lombok.Getter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import xyz.zcraft.seira.ai.AgentService; +import xyz.zcraft.seira.ai.AiChatHandler; import xyz.zcraft.seira.api.data.OsuToken; import xyz.zcraft.seira.api.data.VideoRenderRecord; import xyz.zcraft.seira.bot.MessageSender; import xyz.zcraft.seira.bot.QQApi; +import xyz.zcraft.seira.bot.data.GroupBotState; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.bot.data.QQUser; import xyz.zcraft.seira.command.*; @@ -19,6 +22,7 @@ import xyz.zcraft.seira.db.UserDataStore; import xyz.zcraft.seira.discord.DiscordBridgeService; import xyz.zcraft.seira.rankguess.RankGuessGameService; +import xyz.zcraft.seira.services.AiPermission; import xyz.zcraft.seira.services.BindingService; import xyz.zcraft.seira.util.AdminRegistry; import xyz.zcraft.seira.util.NoticesHelper; @@ -49,12 +53,14 @@ public class Router { private final CommandHandler unknownCommand; private final Executor commandExecutor; private final Supplier selfSupplier; + private final AiChatHandler aiChatHandler; public Router( MessageSender messageSender, Supplier configSupplier, AdminRegistry admins, BindingService bindingService, ScoreWatchService watchService, MultiplayerRoomWatchService multiplayerRoomWatchService, DiscordBridgeService discordBridgeService, RankGuessGameService rankGuessGameService, Executor commandExecutor, - Runnable commandMetric, Function imageUploader, Supplier selfSupplier + Runnable commandMetric, Function imageUploader, Supplier selfSupplier, + AgentService agentService, Function botStateGetter ) { this.configSupplier = java.util.Objects.requireNonNull(configSupplier); this.commandExecutor = commandExecutor; @@ -84,6 +90,9 @@ public Router( GeneralCommandHandler generalCommands = new GeneralCommandHandler( messageSender, taskCoordinator, replyFactory, resolver, admins::isAdmin ); + this.aiChatHandler = new AiChatHandler( + resolver, agentService, admins::isAdmin, botStateGetter + ); WatchCommandHandler watchCommands = new WatchCommandHandler(resolver, taskCoordinator, watchService, admins::isAdmin); SpecificScoreWatchCommandHandler specificScoreWatchCommands = new SpecificScoreWatchCommandHandler(taskCoordinator, watchService); @@ -98,7 +107,7 @@ public Router( this.commandRegistry = createCommandRegistry( bindingCommands, scoreCommands, beatmapCommands, socialCommands, replayCommands, generalCommands, watchCommands, specificScoreWatchCommands, - multiplayerRoomWatchCommands, dcsCommands, rankGuessCommands + multiplayerRoomWatchCommands, dcsCommands, rankGuessCommands, aiChatHandler ); this.debugRoutes = new DebugRoutes( configSupplier, messageSender, replyFactory, taskCoordinator, @@ -106,17 +115,13 @@ public Router( ); } - private String getAvatar(String openId) { - return QQApi.getAvatarUrl(configSupplier.get().qq().appId(), openId); - } - private static CommandRegistry createCommandRegistry( BindingCommandHandler bindingCommands, ScoreCommandHandler scoreCommands, BeatmapCommandHandler beatmapCommands, SocialCommandHandler socialCommands, ReplayCommandHandler replayCommands, GeneralCommandHandler generalCommands, WatchCommandHandler watchCommands, SpecificScoreWatchCommandHandler specificScoreWatchCommands, MultiplayerRoomWatchCommandHandler multiplayerRoomWatchCommands, DcsCommandHandler dcsCommands, - RankGuessCommandHandler rankGuessCommands + RankGuessCommandHandler rankGuessCommands, AiChatHandler aiChatHandler ) { return CommandRegistry.builder() .register(bindingCommands::handleBind, "bind") @@ -162,9 +167,14 @@ private static CommandRegistry createCommandRegistry( .register(dcsCommands::handleDcs, "dcs") .register(rankGuessCommands::handleRankGuess, "rg") .register(generalCommands::handleNotice, "notice") + .register(aiChatHandler::handleAi, "ai") .build(); } + private String getAvatar(String openId) { + return QQApi.getAvatarUrl(configSupplier.get().qq().appId(), openId); + } + public void onPrivateMessageReceived(String userId, String messageId, String rawContent) { handleMessageReceived(userId, null, userId, messageId, rawContent, false); } @@ -183,8 +193,15 @@ private void handleMessageReceived(String targetId, String groupId, String userI rawContent = rawContent == null ? "" : rawContent.trim(); + boolean beingAt = false; + AppConfig config = configSupplier.get(); final String selfAt = "<@" + config.qq().selfId() + ">"; + + if (rawContent.contains(selfAt)) { + beingAt = true; + } + if (rawContent.startsWith(selfAt)) { rawContent = rawContent.substring(selfAt.length()).trim(); } @@ -192,6 +209,11 @@ private void handleMessageReceived(String targetId, String groupId, String userI final QQUser qqUser = selfSupplier.get(); if (qqUser != null) { final String selfLiteralAt = "@" + qqUser.username(); + + if (rawContent.contains(selfLiteralAt)) { + beingAt = true; + } + if (rawContent.startsWith(selfLiteralAt)) { rawContent = rawContent.substring(selfLiteralAt.length()).trim(); } @@ -200,15 +222,24 @@ private void handleMessageReceived(String targetId, String groupId, String userI CommandParser.ParseResult parseResult = commandParser.parse( rawContent, userId, groupId, messageId ); + if (parseResult.status() == CommandParser.ParseResult.Status.IGNORED) { return; } + if (parseResult.status() == CommandParser.ParseResult.Status.TEXT) { + CommandReplyChannel replies = taskCoordinator.openReplyChannel( + targetId, messageId, groupMessage, false + ); + + if (beingAt && AiPermission.doPermit(groupId)) { + aiChatHandler.handleChat(parseResult.context().withReplies(replies)); + } + return; + } + CommandReplyChannel replies = taskCoordinator.openReplyChannel( - targetId, - messageId, - groupMessage, - config.seira().queueMessageInGroup() + targetId, messageId, groupMessage, config.seira().queueMessageInGroup() ); if (parseResult.status() == CommandParser.ParseResult.Status.EMPTY_COMMAND && !group) { diff --git a/src/main/java/xyz/zcraft/seira/config/AppConfig.java b/src/main/java/xyz/zcraft/seira/config/AppConfig.java index 835f45ee..80e9ef65 100644 --- a/src/main/java/xyz/zcraft/seira/config/AppConfig.java +++ b/src/main/java/xyz/zcraft/seira/config/AppConfig.java @@ -7,21 +7,13 @@ public record AppConfig( QqConfig qq, CosConfig cos, DiscordConfig discord, - BridgeConfig bridge + BridgeConfig bridge, + LLMConfig llm ) { public AppConfig { discord = discord == null ? DiscordConfig.disabled() : discord; bridge = bridge == null ? BridgeConfig.defaults() : bridge; - } - - public AppConfig( - SeiraConfig seira, - OstellaConfig ostella, - BindingConfig binding, - QqConfig qq, - CosConfig cos - ) { - this(seira, ostella, binding, qq, cos, null, null); + llm = llm == null ? new LLMConfig(null, null) : llm; } } diff --git a/src/main/java/xyz/zcraft/seira/config/LLMConfig.java b/src/main/java/xyz/zcraft/seira/config/LLMConfig.java new file mode 100644 index 00000000..aaceaa26 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/config/LLMConfig.java @@ -0,0 +1,7 @@ +package xyz.zcraft.seira.config; + +public record LLMConfig( + String baseUrl, + String apiKey +) { +} diff --git a/src/main/java/xyz/zcraft/seira/config/RuntimeConfig.java b/src/main/java/xyz/zcraft/seira/config/RuntimeConfig.java index 3252f236..75cd434b 100644 --- a/src/main/java/xyz/zcraft/seira/config/RuntimeConfig.java +++ b/src/main/java/xyz/zcraft/seira/config/RuntimeConfig.java @@ -54,7 +54,8 @@ private static AppConfig mergeReloadable(AppConfig previous, AppConfig loaded) { previous.qq(), previous.cos(), previous.discord(), - previous.bridge() + previous.bridge(), + previous.llm() ); } diff --git a/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java b/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java index 1dc6585a..741598ca 100644 --- a/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java +++ b/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java @@ -9,6 +9,7 @@ import xyz.zcraft.seira.command.route.Router; import xyz.zcraft.seira.config.RuntimeConfig; import xyz.zcraft.seira.db.SqliteDatabase; +import xyz.zcraft.seira.services.AiPermission; import xyz.zcraft.seira.services.BotStat; import xyz.zcraft.seira.services.handler.ConfigHandler; import xyz.zcraft.seira.services.handler.NoticeHandler; @@ -31,7 +32,8 @@ public final class ConsoleCommandProcessor { private static final Pattern SQL_IDENTIFIER = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*"); private static final List ROOT_COMMANDS = List.of( "help", "status", "metrics", "system", "config", "admin", "data", "send", - "watch", "cache", "gateway", "log", "inspect", "stop", "panel", "notice", "group" + "watch", "cache", "gateway", "log", "inspect", "stop", "panel", "notice", "group", + "ai" ); private static final Map> SUBCOMMANDS; @@ -49,6 +51,7 @@ public final class ConsoleCommandProcessor { SUBCOMMANDS.put("panel", List.of("list", "create", "delete", "edit", "get")); SUBCOMMANDS.put("notice", List.of("new", "reload", "publish", "revoke", "list")); SUBCOMMANDS.put("group", List.of("info", "state")); + SUBCOMMANDS.put("ai", List.of("reload")); } private final RuntimeConfig runtimeConfig; @@ -241,6 +244,7 @@ public ConsoleResult execute(String line) { case "panel" -> panel(input); case "group" -> group(input); case "gateway" -> gateway(input); + case "ai" -> ai(input); case "log" -> log(input); case "inspect" -> exact(input, 1, this::inspect, "Usage: inspect"); case "stop", "shutdown", "exit", "quit" -> stop(input); @@ -256,6 +260,37 @@ public ConsoleResult execute(String line) { } } + private ConsoleResult ai(ConsoleInputParser.ParsedInput input) { + if (input.size() == 1) { + final String value = input.value(0).toLowerCase(Locale.ROOT); + if (value.equalsIgnoreCase("reload")) { + AiPermission.loadFromFile(); + return ConsoleResult.success("AI permission reloaded."); + } else if (ID_PATTERN.matcher(value).matches()) { + return ConsoleResult.success("AI chat for group " + value + " is: " + AiPermission.doPermit(value)); + } else { + return ConsoleResult.failure("Group id " + value + " not a valid id."); + } + } else if (input.size() == 2) { + final String target = input.value(0).toLowerCase(Locale.ROOT); + final String option = input.value(1).toLowerCase(Locale.ROOT); + + if (!ID_PATTERN.matcher(target).matches()) { + return ConsoleResult.failure("Group id " + target + " not a valid id."); + } + + if ("on".equalsIgnoreCase(option)) { + AiPermission.permit(target); + return ConsoleResult.success("AI chat for group " + target + " is on."); + } else if ("off".equalsIgnoreCase(option)) { + AiPermission.revoke(target); + return ConsoleResult.success("AI chat for group " + target + " is off."); + } + } + + return ConsoleResult.failure("Usage: ai "); + } + private ConsoleResult help(ConsoleInputParser.ParsedInput input) { if (input.size() > 2) { return ConsoleResult.failure("Usage: help [command]"); @@ -277,6 +312,7 @@ private ConsoleResult help(ConsoleInputParser.ParsedInput input) { Manage command panels cache Inspect/delete cache through oStella and workers + ai Manage AI chat status gateway Inspect or reconnect the QQ gateway log Inspect or change the runtime log level inspect Show the last dispatched message context @@ -325,6 +361,11 @@ private ConsoleResult help(ConsoleInputParser.ParsedInput input) { cache query reports presence at oStella and every osuRenderer worker. get includes metadata; fetch populates oStella and downstream workers; delete removes every reachable copy."""; + case "ai" -> """ + ai [on|off] + ai reload + Turn on or off AI chat function for specific group. + """; case "gateway" -> """ gateway status gateway reconnect @@ -552,6 +593,8 @@ private ConsoleResult query(String sql) { return ConsoleResult.success(formatQueryResult(dataAccess.query(sql, QUERY_ROW_LIMIT))); } + private static final Pattern ID_PATTERN = Pattern.compile("^[A-Z0-9]{32}$"); + private ConsoleResult send(ConsoleInputParser.ParsedInput input) { if (input.size() < 4) { return ConsoleResult.failure("Usage: send "); diff --git a/src/main/java/xyz/zcraft/seira/services/AiPermission.java b/src/main/java/xyz/zcraft/seira/services/AiPermission.java new file mode 100644 index 00000000..8fc4f595 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/services/AiPermission.java @@ -0,0 +1,122 @@ +package xyz.zcraft.seira.services; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.Getter; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +public class AiPermission { + private static final Object LOCK = new Object(); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Path STORE = Path.of("data", "ai-permission.json"); + private static Set groups = null; + @Getter + private static Mode mode = Mode.WHITELIST; + + public static void initialize() { + loadFromFile(); + } + + public static void loadFromFile() { + synchronized (LOCK) { + try { + if (Files.exists(STORE)) { + String json = Files.readString(STORE); + + if (!json.isBlank()) { + JsonObject obj = JsonParser.parseString(json).getAsJsonObject(); + + final PermissionSnapshot permissionSnapshot = GSON.fromJson(obj, PermissionSnapshot.class); + + mode = permissionSnapshot.mode; + groups = new HashSet<>(); + + final Set groups = permissionSnapshot.groups; + if (groups != null) { + AiPermission.groups.addAll(groups); + } + } + } + } catch (Exception e) { + throw new RuntimeException("Failed to load ai permission", e); + } + + if (mode == null) { + mode = Mode.WHITELIST; + } + + if (groups == null) { + groups = new HashSet<>(); + } + } + } + + public static void saveToFile() { + synchronized (LOCK) { + try { + Files.createDirectories(STORE.getParent()); + + PermissionSnapshot snapshot = new PermissionSnapshot(mode, groups); + + Files.writeString(STORE, GSON.toJson(snapshot)); + } catch (Exception e) { + throw new RuntimeException("Failed to load ai permission", e); + } + } + } + + public static boolean doPermit(String groupId) { + if (mode == null || groups == null) return false; + + if (mode == Mode.WHITELIST) { + return groups.contains(groupId); + } else if (mode == Mode.BLACKLIST) { + return !groups.contains(groupId); + } else { + return false; + } + } + + public static void permit(String groupId) { + if (mode == null || groups == null) return; + + if (mode == Mode.WHITELIST) { + groups.add(groupId); + } else if (mode == Mode.BLACKLIST) { + groups.remove(groupId); + } + + saveToFile(); + } + + public static void revoke(String groupId) { + if (mode == null || groups == null) return; + + if (mode == Mode.WHITELIST) { + groups.remove(groupId); + } else if (mode == Mode.BLACKLIST) { + groups.add(groupId); + } + + saveToFile(); + } + + public enum Mode { + WHITELIST, + BLACKLIST + } + + public record PermissionSnapshot( + Mode mode, + Set groups + ) { + } +} diff --git a/src/main/resources/seira-example-config.yml b/src/main/resources/seira-example-config.yml index 94273e3d..8ccd1037 100644 --- a/src/main/resources/seira-example-config.yml +++ b/src/main/resources/seira-example-config.yml @@ -71,3 +71,8 @@ bridge: # 可用占位符:{name}、{id}、{message} qqToDiscordFormat: "[{name}] {message}" discordToQqFormat: "[{name}] {message}" + +llm: + # LLM API 的地址 + baseUrl: "http://localhost:8723" + apiKey: "" From 83a28a7f6716276693addaaf07ecc32591c79922 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 20 Sep 2026 18:14:39 +0800 Subject: [PATCH 23/96] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7=E8=87=B3=201.12.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 42b5d8bd..009fcd2b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.3 + 1.12.4 25 From aac6e3fd1758434f123393921cc794fc5924217e Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 20 Sep 2026 18:24:50 +0800 Subject: [PATCH 24/96] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7=E8=87=B3=201.12.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/command/parse/Resolver.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java index d5d51b96..97764f47 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java @@ -137,7 +137,7 @@ public List extractAllMentionedIds(String token) { List result = new LinkedList<>(); - Patterns.QQ_AT_PATTERN.matcher(token).results().forEach(m -> result.add(m.group(1))); + Patterns.QQ_AT_IDS_PATTERN.matcher(token).results().forEach(m -> result.add(m.group(1))); return result; } @@ -163,6 +163,7 @@ private static final class Patterns { "^([a-zA-Z]+)(\\d+(?:#\\d+)?)" ); private static final Pattern QQ_AT_PATTERN = Pattern.compile("^<@([A-Z0-9]{32})>$"); + private static final Pattern QQ_AT_IDS_PATTERN = Pattern.compile("<@([A-Z0-9]{32})>"); private static final Pattern QQ_INLINE_AT_PATTERN = Pattern.compile("(<@[A-Z0-9]{32}>)"); private static final Pattern PLAIN_AT_PATTERN = Pattern.compile("^@(\\d+)$"); private static final Pattern SEARCH_PATTERN = Pattern.compile("^(?:#(\\d+) )?(.+)$"); From 5b5b7de14e27a8472a9a82a1f74d6a845ad3b5e2 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 20 Sep 2026 18:27:43 +0800 Subject: [PATCH 25/96] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7=E8=87=B3=201.12.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index 13fce1b8..854ca97f 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -96,7 +96,7 @@ public void handleChat(Context ctx) { ids.add(ctx.senderUserId()); for (String openId : ids) { - final Long uid = resolver.resolveBoundUid(ctx.senderUserId()); + final Long uid = resolver.resolveBoundUid(openId); if (uid != null) { bindings.put(openId, uid); UserDataStore.findUsername(uid).ifPresent(s -> usernames.put(uid, s)); From be6bdf1ed7ab8e07c6dc30e2fffb83c55f481331 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 20 Sep 2026 18:51:33 +0800 Subject: [PATCH 26/96] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=B8=85=E9=99=A4?= =?UTF-8?q?=E7=BE=A4=E7=BB=84=E5=92=8C=E7=94=A8=E6=88=B7=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=9A=84=E6=96=B9=E6=B3=95=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7=E8=87=B3=201.12.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../xyz/zcraft/seira/ai/AgentService.java | 30 +++++++++++++++++++ .../xyz/zcraft/seira/ai/AiChatHandler.java | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 009fcd2b..4d0f991f 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.4 + 1.12.5 25 diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index 7c0d952f..b2b6fb28 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -13,6 +13,8 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; @@ -74,6 +76,34 @@ public void clearState(String groupId, String openId) { states.remove(StateOwner.of(groupId, openId)); } + public void clearStateOfGroup(String groupId) { + List toRemove = new ArrayList<>(100); + + states.keySet().forEach((owner) -> { + if (owner.groupId().equals(groupId)) { + toRemove.add(owner); + } + }); + + for (StateOwner stateOwner : toRemove) { + states.remove(stateOwner); + } + } + + public void clearStateOfUser(String openId) { + List toRemove = new ArrayList<>(100); + + states.keySet().forEach((owner) -> { + if (owner.openId().equals(openId)) { + toRemove.add(owner); + } + }); + + for (StateOwner stateOwner : toRemove) { + states.remove(stateOwner); + } + } + record State( AppConversationBrief conv, ConcurrentHashMap vars, AtomicBoolean running diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index 854ca97f..cf6c89e2 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -69,7 +69,7 @@ public void handleAi(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已禁用本群AI对话喵。")); return; } else if ("reset".equalsIgnoreCase(ctx.argument(0))) { - agentService.clearState(ctx.groupId(), ctx.argument(1)); + agentService.clearStateOfGroup(ctx.groupId()); ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已重置本群AI对话状态喵。")); return; } From 34a59d4b630acfd015631adee12251cbc70e2e08 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 20 Sep 2026 18:58:59 +0800 Subject: [PATCH 27/96] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=B8=85=E9=99=A4?= =?UTF-8?q?=E7=BE=A4=E7=BB=84=E5=92=8C=E7=94=A8=E6=88=B7=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=9A=84=E6=96=B9=E6=B3=95=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7=E8=87=B3=201.12.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/ai/AgentService.java | 12 ++++++++---- src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index b2b6fb28..c56655cb 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -72,11 +72,11 @@ public boolean isRunning(String groupId, String openId) { return state != null && state.running().get(); } - public void clearState(String groupId, String openId) { - states.remove(StateOwner.of(groupId, openId)); + public boolean clearState(String groupId, String openId) { + return states.remove(StateOwner.of(groupId, openId)) != null; } - public void clearStateOfGroup(String groupId) { + public int clearStateOfGroup(String groupId) { List toRemove = new ArrayList<>(100); states.keySet().forEach((owner) -> { @@ -88,9 +88,11 @@ public void clearStateOfGroup(String groupId) { for (StateOwner stateOwner : toRemove) { states.remove(stateOwner); } + + return toRemove.size(); } - public void clearStateOfUser(String openId) { + public int clearStateOfUser(String openId) { List toRemove = new ArrayList<>(100); states.keySet().forEach((owner) -> { @@ -102,6 +104,8 @@ public void clearStateOfUser(String openId) { for (StateOwner stateOwner : toRemove) { states.remove(stateOwner); } + + return toRemove.size(); } record State( diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index cf6c89e2..983bc70c 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -69,8 +69,8 @@ public void handleAi(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已禁用本群AI对话喵。")); return; } else if ("reset".equalsIgnoreCase(ctx.argument(0))) { - agentService.clearStateOfGroup(ctx.groupId()); - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已重置本群AI对话状态喵。")); + final int i = agentService.clearStateOfGroup(ctx.groupId()); + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已重置本群" + i + "个用户的AI对话状态喵。")); return; } } From 8f17b1e57125b062c8c2792240ce2b7f23b71b9d Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 21 Sep 2026 00:55:10 +0800 Subject: [PATCH 28/96] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=B4=BB=E5=8A=A8=E7=BE=A4=E7=BB=84ID=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E6=B3=95=EF=BC=8C=E5=B9=B6=E5=9C=A8=E9=80=9A=E7=9F=A5=E4=B8=AD?= =?UTF-8?q?=E5=8C=85=E5=90=AB=E4=BB=A3=E7=90=86=E6=9C=8D=E5=8A=A1=E7=BE=A4?= =?UTF-8?q?=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/ai/AgentService.java | 9 +++++++++ src/main/java/xyz/zcraft/seira/bot/QQBot.java | 3 ++- .../seira/bot/RealtimeServiceInterruptionNotifier.java | 9 +++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index c56655cb..913e036e 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -16,9 +16,11 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; +import java.util.stream.Collectors; public class AgentService { private final Api api; @@ -123,6 +125,13 @@ public static StateOwner of(Context ctx) { return new StateOwner(ctx.groupId(), ctx.senderUserId()); } } + + public Set activeGroupIds() { + return states.entrySet().stream() + .filter(entry -> entry.getValue().running().get()) + .map(entry -> entry.getKey().groupId()) + .collect(Collectors.toSet()); + } } class Api { diff --git a/src/main/java/xyz/zcraft/seira/bot/QQBot.java b/src/main/java/xyz/zcraft/seira/bot/QQBot.java index b1d77e15..4ce7a2a1 100644 --- a/src/main/java/xyz/zcraft/seira/bot/QQBot.java +++ b/src/main/java/xyz/zcraft/seira/bot/QQBot.java @@ -312,7 +312,8 @@ public void requestStop() { RealtimeServiceInterruptionNotifier.NotificationResult result = interruptionNotifier.notifyGroups( watchService.activeTransientGroupIds(), rankGuessGameService.activeGroupIds(), - multiplayerRoomWatchService.activeGroupIds() + multiplayerRoomWatchService.activeGroupIds(), + agentService.activeGroupIds() ); if (result.failedGroups() == 0) { LOG.info("Sent restart interruption notices to {} affected groups", result.sentGroups()); diff --git a/src/main/java/xyz/zcraft/seira/bot/RealtimeServiceInterruptionNotifier.java b/src/main/java/xyz/zcraft/seira/bot/RealtimeServiceInterruptionNotifier.java index 0a75db7b..70362d3b 100644 --- a/src/main/java/xyz/zcraft/seira/bot/RealtimeServiceInterruptionNotifier.java +++ b/src/main/java/xyz/zcraft/seira/bot/RealtimeServiceInterruptionNotifier.java @@ -6,6 +6,7 @@ public final class RealtimeServiceInterruptionNotifier { private static final String SCORE_WATCH = "成绩监视"; private static final String RANK_GUESS = "猜 Rank"; private static final String MULTIPLAYER_WATCH = "MP 监视"; + private static final String AI_CHAT = "AI 对话"; private final MessageSender sender; @@ -14,9 +15,7 @@ public RealtimeServiceInterruptionNotifier(MessageSender sender) { } private static void addService( - Map> servicesByGroup, - Set groupIds, - String service + Map> servicesByGroup, Set groupIds, String service ) { Objects.requireNonNull(groupIds); groupIds.stream().sorted().forEach(groupId -> servicesByGroup @@ -33,12 +32,14 @@ private static String message(Set services) { public NotificationResult notifyGroups( Set scoreWatchGroups, Set rankGuessGroups, - Set multiplayerWatchGroups + Set multiplayerWatchGroups, + Set agentGroups ) { Map> servicesByGroup = new LinkedHashMap<>(); addService(servicesByGroup, scoreWatchGroups, SCORE_WATCH); addService(servicesByGroup, rankGuessGroups, RANK_GUESS); addService(servicesByGroup, multiplayerWatchGroups, MULTIPLAYER_WATCH); + addService(servicesByGroup, agentGroups, AI_CHAT); int sent = 0; for (Map.Entry> entry : servicesByGroup.entrySet()) { From fd5baba1f3de40f4f938f5a5e5f5eb373ebf1a0b Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 21 Sep 2026 00:57:18 +0800 Subject: [PATCH 29/96] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=9D=83=E9=99=90?= =?UTF-8?q?=E4=B8=8D=E8=B6=B3=E6=8F=90=E7=A4=BA=E4=BF=A1=E6=81=AF=EF=BC=8C?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=E5=8A=9F=E8=83=BD=E6=B5=8B=E8=AF=95=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=E5=8F=8A=E8=81=94=E7=B3=BB=E7=AE=A1=E7=90=86=E5=91=98?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index 983bc70c..8db5ec5a 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -48,7 +48,8 @@ public void handleAi(Context ctx) { } else if (ctx.argumentCount() == 1 && List.of("on", "off", "reset").contains(ctx.argument(0).toLowerCase(Locale.ROOT))) { if (!adminAuthorizer.test(ctx.senderUserId())) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "你无权使用该命令喵。")); + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "你无权使用该命令喵。\n" + + "> 由于此功能开销较大、处于测试阶段且较为不可控,暂未开放。若想要在此群中使用此功能,请联系 Bot 管理员喵。")); return; } From fc047e3b06f2e52a5cd7d80c86621ddc5a2f121d Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 21 Sep 2026 15:41:07 +0800 Subject: [PATCH 30/96] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=8E=86=E5=8F=B2=E7=9A=84=E6=96=B9=E6=B3=95?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/ai/AgentService.java | 127 +++++++++++++----- .../xyz/zcraft/seira/ai/AiChatHandler.java | 4 + .../zcraft/seira/command/route/Router.java | 3 + 3 files changed, 100 insertions(+), 34 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index 913e036e..e7dda44f 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -3,6 +3,7 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import lombok.Getter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import xyz.zcraft.seira.ai.data.AppConversationBrief; @@ -13,16 +14,14 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.stream.Collectors; public class AgentService { + public static final int CONTEXT_SIZE = 20; private final Api api; private final Map states = new ConcurrentHashMap<>(); @@ -30,52 +29,76 @@ public AgentService(LLMConfig config) { this.api = new Api(config); } + public void recordHistory(String groupId, String sender, String message) { + if (groupId == null || groupId.isEmpty() || sender == null || sender.isEmpty()) { + return; + } + + states.forEach((s, state) -> { + synchronized (state) { + if (s.groupId().equals(groupId)) { + state.getIncomingMessages().add(sender + ": " + message); + + while (state.getIncomingMessages().size() > CONTEXT_SIZE) { + state.getIncomingMessages().removeFirst(); + } + } + } + }); + } + public String input(String groupId, String openId, String input, Consumer> var) { final StateOwner owner = StateOwner.of(groupId, openId); - final State state = states.computeIfAbsent( - owner, - _ -> new State( - api.createConversation(groupId), - new ConcurrentHashMap<>(), - new AtomicBoolean(false) - ) - ); + final State state = states.computeIfAbsent(owner, _ -> State.create(api, groupId)); - if (!state.running().compareAndSet(false, true)) { + if (!state.getRunning().compareAndSet(false, true)) { throw new IllegalStateException("已有请求正在运行"); } + state.resetIfNeeded(api, groupId); + try { if (var != null) { - final int oldHash = state.vars().hashCode(); + final int oldHash = state.getVars().hashCode(); - var.accept(state.vars()); + var.accept(state.getVars()); - if (state.vars().hashCode() != oldHash) { + if (state.getVars().hashCode() != oldHash) { api.updateConversation( - groupId, state.conv().appConversationID(), state.vars() + groupId, state.getConv().appConversationID(), state.getVars() ); } } + final String query = String.join("\n", state.getIncomingMessages()) + "\n\n" + input; + + state.getIncomingMessages().clear(); + final ChatQueryResponse response = api.chatQuery( - groupId, state.conv().appConversationID(), input + groupId, state.getConv().appConversationID(), query ); + state.getIncomingMessages().add("你: " + response.answer()); + return response.answer(); } finally { - state.running().set(false); + state.getRunning().set(false); } } public boolean isRunning(String groupId, String openId) { final State state = states.get(StateOwner.of(groupId, openId)); - return state != null && state.running().get(); + return state != null && state.getRunning().get(); } public boolean clearState(String groupId, String openId) { - return states.remove(StateOwner.of(groupId, openId)) != null; + final State state = states.get(StateOwner.of(groupId, openId)); + if (state != null) { + state.getResetting().set(true); + return true; + } + return false; } public int clearStateOfGroup(String groupId) { @@ -88,7 +111,7 @@ public int clearStateOfGroup(String groupId) { }); for (StateOwner stateOwner : toRemove) { - states.remove(stateOwner); + states.get(stateOwner).getResetting().set(true); } return toRemove.size(); @@ -104,16 +127,59 @@ public int clearStateOfUser(String openId) { }); for (StateOwner stateOwner : toRemove) { - states.remove(stateOwner); + states.get(stateOwner).getResetting().set(true); } return toRemove.size(); } - record State( - AppConversationBrief conv, ConcurrentHashMap vars, - AtomicBoolean running - ) { + public Set activeGroupIds() { + return states.entrySet().stream() + .filter(entry -> entry.getValue().getRunning().get()) + .map(entry -> entry.getKey().groupId()) + .collect(Collectors.toSet()); + } + + @Getter + static final class State { + private final ConcurrentHashMap vars; + private final AtomicBoolean running; + private final AtomicBoolean resetting; + private final List incomingMessages; + private AppConversationBrief conv; + + State( + AppConversationBrief conv, + ConcurrentHashMap vars, + AtomicBoolean running, + AtomicBoolean resetting, + List incomingMessages + ) { + this.conv = conv; + this.vars = vars; + this.running = running; + this.resetting = resetting; + this.incomingMessages = incomingMessages; + } + + public static State create(Api api, String groupId) { + return new State( + api.createConversation(groupId), + new ConcurrentHashMap<>(), + new AtomicBoolean(false), + new AtomicBoolean(false), + new LinkedList<>() + ); + } + + + public void resetIfNeeded(Api api, String groupId) { + if (resetting.compareAndSet(true, false)) { + conv = api.createConversation(groupId); + vars.clear(); + incomingMessages.clear(); + } + } } record StateOwner(String groupId, String openId) { @@ -125,13 +191,6 @@ public static StateOwner of(Context ctx) { return new StateOwner(ctx.groupId(), ctx.senderUserId()); } } - - public Set activeGroupIds() { - return states.entrySet().stream() - .filter(entry -> entry.getValue().running().get()) - .map(entry -> entry.getKey().groupId()) - .collect(Collectors.toSet()); - } } class Api { diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index 8db5ec5a..eb75d639 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -120,4 +120,8 @@ public void handleChat(Context ctx) { } } } + + public void recordHistory(String groupId, String userId, String rawContent) { + agentService.recordHistory(groupId, userId, rawContent); + } } diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index e71f0824..d51f5563 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -224,6 +224,9 @@ private void handleMessageReceived(String targetId, String groupId, String userI ); if (parseResult.status() == CommandParser.ParseResult.Status.IGNORED) { + if (AiPermission.doPermit(groupId)) { + aiChatHandler.recordHistory(groupId, userId, rawContent); + } return; } From e38e7c7d1cc66dab0df97e86fdd51d4e4934d320 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 21 Sep 2026 16:40:25 +0800 Subject: [PATCH 31/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E9=80=BB=E8=BE=91=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=92=8C=E6=B8=85=E7=90=86=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E7=9A=84=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/ai/AgentService.java | 273 ++++++++++++++---- 1 file changed, 211 insertions(+), 62 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index e7dda44f..2a567a06 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -22,138 +22,271 @@ public class AgentService { public static final int CONTEXT_SIZE = 20; + private final Api api; + private final Map states = new ConcurrentHashMap<>(); + private final Map> chatLog = new ConcurrentHashMap<>(); + + private final Map stateCreationLocks = new ConcurrentHashMap<>(); + public AgentService(LLMConfig config) { this.api = new Api(config); } + /** + * API 请求失败时,把已经取出的 incomingMessages 恢复。 + */ + private static void restoreIncomingMessages( + State state, + Collection pendingMessages + ) { + if (pendingMessages.isEmpty()) { + return; + } + + synchronized (state) { + /* + * 当前 state.incomingMessages 中可能已经有 + * API 请求运行期间新收到的消息。 + * + * 所以不能直接 addAll(pendingMessages), + * 否则时间顺序会变成: + * + * 新消息 + * old pending + * + * 正确顺序应该是: + * + * old pending + * 新消息 + */ + final Deque merged = new ArrayDeque<>( + pendingMessages.size() + + state.incomingMessages.size() + ); + + merged.addAll(pendingMessages); + merged.addAll(state.incomingMessages); + + state.incomingMessages.clear(); + state.incomingMessages.addAll(merged); + + trimToContextSize(state.incomingMessages); + } + } + + /** + * 保证历史消息数量不超过 CONTEXT_SIZE。 + */ + private static void trimToContextSize(Deque messages) { + while (messages.size() > CONTEXT_SIZE) { + messages.removeFirst(); + } + } + public void recordHistory(String groupId, String sender, String message) { - if (groupId == null || groupId.isEmpty() || sender == null || sender.isEmpty()) { + if (groupId == null || groupId.isEmpty() + || sender == null || sender.isEmpty() + || message == null || message.isEmpty()) { return; } - states.forEach((s, state) -> { - synchronized (state) { - if (s.groupId().equals(groupId)) { - state.getIncomingMessages().add(sender + ": " + message); + final String historyMessage = sender + ": " + message; - while (state.getIncomingMessages().size() > CONTEXT_SIZE) { - state.getIncomingMessages().removeFirst(); - } + final Deque log = chatLog.computeIfAbsent(groupId, _ -> new ArrayDeque<>()); + + synchronized (log) { + log.addLast(historyMessage); + trimToContextSize(log); + + states.forEach((owner, state) -> { + if (!owner.groupId().equals(groupId)) { + return; } - } - }); + + synchronized (state) { + state.incomingMessages.addLast(historyMessage); + trimToContextSize(state.incomingMessages); + } + }); + } } public String input(String groupId, String openId, String input, Consumer> var) { final StateOwner owner = StateOwner.of(groupId, openId); + final State state = getOrCreateState(owner); - final State state = states.computeIfAbsent(owner, _ -> State.create(api, groupId)); - - if (!state.getRunning().compareAndSet(false, true)) { + if (!state.running.compareAndSet(false, true)) { throw new IllegalStateException("已有请求正在运行"); } - state.resetIfNeeded(api, groupId); - try { + state.resetIfNeeded(api, groupId); + if (var != null) { - final int oldHash = state.getVars().hashCode(); + final Map oldVars = new HashMap<>(state.vars); - var.accept(state.getVars()); + var.accept(state.vars); - if (state.getVars().hashCode() != oldHash) { + if (!state.vars.equals(oldVars)) { api.updateConversation( - groupId, state.getConv().appConversationID(), state.getVars() + groupId, + state.conv.appConversationID(), + state.vars ); } } - final String query = String.join("\n", state.getIncomingMessages()) + "\n\n" + input; + final Deque pendingMessages; - state.getIncomingMessages().clear(); + synchronized (state) { + pendingMessages = new ArrayDeque<>(state.incomingMessages); + state.incomingMessages.clear(); + } - final ChatQueryResponse response = api.chatQuery( - groupId, state.getConv().appConversationID(), query - ); + final String query; + + if (pendingMessages.isEmpty()) { + query = input; + } else { + query = String.join("\n", pendingMessages) + + "\n" + + "\n" + + input; + } - state.getIncomingMessages().add("你: " + response.answer()); + final ChatQueryResponse response; + + try { + response = api.chatQuery(groupId, state.conv.appConversationID(), query); + } catch (RuntimeException | Error e) { + restoreIncomingMessages(state, pendingMessages); + + throw e; + } + + synchronized (state) { + state.incomingMessages.addLast("你: " + response.answer()); + + trimToContextSize(state.incomingMessages); + } return response.answer(); + } finally { - state.getRunning().set(false); + state.running.set(false); } } public boolean isRunning(String groupId, String openId) { - final State state = states.get(StateOwner.of(groupId, openId)); - return state != null && state.getRunning().get(); + final State state = states.get( + StateOwner.of(groupId, openId) + ); + + return state != null && state.running.get(); } public boolean clearState(String groupId, String openId) { - final State state = states.get(StateOwner.of(groupId, openId)); - if (state != null) { - state.getResetting().set(true); - return true; + final State state = states.get( + StateOwner.of(groupId, openId) + ); + + if (state == null) { + return false; } - return false; + + state.resetting.set(true); + return true; } public int clearStateOfGroup(String groupId) { - List toRemove = new ArrayList<>(100); + int count = 0; - states.keySet().forEach((owner) -> { - if (owner.groupId().equals(groupId)) { - toRemove.add(owner); + for (Map.Entry entry : states.entrySet()) { + if (!entry.getKey().groupId().equals(groupId)) { + continue; } - }); - for (StateOwner stateOwner : toRemove) { - states.get(stateOwner).getResetting().set(true); + entry.getValue().resetting.set(true); + count++; } - return toRemove.size(); + return count; } public int clearStateOfUser(String openId) { - List toRemove = new ArrayList<>(100); + int count = 0; - states.keySet().forEach((owner) -> { - if (owner.openId().equals(openId)) { - toRemove.add(owner); + for (Map.Entry entry : states.entrySet()) { + if (!entry.getKey().openId().equals(openId)) { + continue; } - }); - for (StateOwner stateOwner : toRemove) { - states.get(stateOwner).getResetting().set(true); + entry.getValue().resetting.set(true); + count++; } - return toRemove.size(); + return count; } public Set activeGroupIds() { - return states.entrySet().stream() - .filter(entry -> entry.getValue().getRunning().get()) + return states.entrySet() + .stream() + .filter(entry -> entry.getValue().running.get()) .map(entry -> entry.getKey().groupId()) .collect(Collectors.toSet()); } + private State getOrCreateState(StateOwner owner) { + State state = states.get(owner); + + if (state != null) { + return state; + } + + final Object creationLock = stateCreationLocks.computeIfAbsent(owner, _ -> new Object()); + + try { + synchronized (creationLock) { + state = states.get(owner); + + if (state != null) { + return state; + } + + final String groupId = owner.groupId(); + final Deque log = chatLog.computeIfAbsent(groupId, _ -> new ArrayDeque<>()); + final AppConversationBrief conv = api.createConversation(groupId); + + synchronized (log) { + final State created = State.create(conv, new ArrayList<>(log)); + + states.put(owner, created); + + return created; + } + } + } finally { + stateCreationLocks.remove(owner, creationLock); + } + } + @Getter static final class State { private final ConcurrentHashMap vars; private final AtomicBoolean running; private final AtomicBoolean resetting; - private final List incomingMessages; + private final Deque incomingMessages; private AppConversationBrief conv; - State( + private State( AppConversationBrief conv, ConcurrentHashMap vars, AtomicBoolean running, AtomicBoolean resetting, - List incomingMessages + Deque incomingMessages ) { this.conv = conv; this.vars = vars; @@ -162,33 +295,49 @@ static final class State { this.incomingMessages = incomingMessages; } - public static State create(Api api, String groupId) { + + public static State create( + AppConversationBrief conv, + Collection incomingMessages + ) { return new State( - api.createConversation(groupId), + conv, new ConcurrentHashMap<>(), new AtomicBoolean(false), new AtomicBoolean(false), - new LinkedList<>() + new ArrayDeque<>(incomingMessages) ); } - public void resetIfNeeded(Api api, String groupId) { - if (resetting.compareAndSet(true, false)) { + if (!resetting.compareAndSet(true, false)) { + return; + } + + try { conv = api.createConversation(groupId); vars.clear(); - incomingMessages.clear(); + } catch (RuntimeException | Error e) { + resetting.set(true); + throw e; } } } + record StateOwner(String groupId, String openId) { - public static StateOwner of(String groupId, String openId) { + public static StateOwner of( + String groupId, + String openId + ) { return new StateOwner(groupId, openId); } public static StateOwner of(Context ctx) { - return new StateOwner(ctx.groupId(), ctx.senderUserId()); + return new StateOwner( + ctx.groupId(), + ctx.senderUserId() + ); } } } From df641722fcaaee4d2b93c6cb9921c137d401a20d Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 21 Sep 2026 18:07:29 +0800 Subject: [PATCH 32/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AgentService=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E6=B6=88=E6=81=AF=E6=81=A2=E5=A4=8D=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E7=A7=BB=E9=99=A4=E5=86=97=E4=BD=99=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E5=B9=B6=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/ai/AgentService.java | 56 ++++--------------- .../xyz/zcraft/seira/ai/AiChatHandler.java | 48 ++++++++-------- 2 files changed, 37 insertions(+), 67 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index 2a567a06..afa66acc 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -18,6 +18,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; public class AgentService { @@ -35,9 +36,6 @@ public AgentService(LLMConfig config) { this.api = new Api(config); } - /** - * API 请求失败时,把已经取出的 incomingMessages 恢复。 - */ private static void restoreIncomingMessages( State state, Collection pendingMessages @@ -47,21 +45,6 @@ private static void restoreIncomingMessages( } synchronized (state) { - /* - * 当前 state.incomingMessages 中可能已经有 - * API 请求运行期间新收到的消息。 - * - * 所以不能直接 addAll(pendingMessages), - * 否则时间顺序会变成: - * - * 新消息 - * old pending - * - * 正确顺序应该是: - * - * old pending - * 新消息 - */ final Deque merged = new ArrayDeque<>( pendingMessages.size() + state.incomingMessages.size() @@ -77,9 +60,6 @@ private static void restoreIncomingMessages( } } - /** - * 保证历史消息数量不超过 CONTEXT_SIZE。 - */ private static void trimToContextSize(Deque messages) { while (messages.size() > CONTEXT_SIZE) { messages.removeFirst(); @@ -114,7 +94,7 @@ public void recordHistory(String groupId, String sender, String message) { } } - public String input(String groupId, String openId, String input, Consumer> var) { + public String input(String groupId, String openId, String input, Function contextFunc) { final StateOwner owner = StateOwner.of(groupId, openId); final State state = getOrCreateState(owner); @@ -123,22 +103,6 @@ public String input(String groupId, String openId, String input, Consumer oldVars = new HashMap<>(state.vars); - - var.accept(state.vars); - - if (!state.vars.equals(oldVars)) { - api.updateConversation( - groupId, - state.conv.appConversationID(), - state.vars - ); - } - } - final Deque pendingMessages; synchronized (state) { @@ -157,6 +121,15 @@ public String input(String groupId, String openId, String input, Consumer bindings = new HashMap<>(); - Map usernames = new HashMap<>(); - - final List ids = resolver.extractAllMentionedIds(ctx.rawContent()); - ids.add(ctx.senderUserId()); - - for (String openId : ids) { - final Long uid = resolver.resolveBoundUid(openId); - if (uid != null) { - bindings.put(openId, uid); - UserDataStore.findUsername(uid).ifPresent(s -> usernames.put(uid, s)); - } - } - - qqContext.add("bindings", GSON.toJsonTree(bindings)); - qqContext.add("usernames", GSON.toJsonTree(usernames)); - final String answer = at(ctx) + agentService.input( ctx.groupId(), ctx.senderUserId(), ctx.senderUserId() + ": " + ctx.rawContent(), - var -> var.put("CONTEXT", qqContext.toString()) + input -> { + JsonObject qqContext = new JsonObject(); + qqContext.addProperty("in_group", ctx.inGroup()); + qqContext.addProperty("sender_open_id", ctx.senderUserId()); + qqContext.addProperty("group_id", ctx.groupId()); + + Map bindings = new HashMap<>(); + Map usernames = new HashMap<>(); + + final List ids = resolver.extractAllMentionedIds(input); + ids.add(ctx.senderUserId()); + + for (String openId : ids) { + final Long uid = resolver.resolveBoundUid(openId); + if (uid != null) { + bindings.put(openId, uid); + UserDataStore.findUsername(uid).ifPresent(s -> usernames.put(uid, s)); + } + } + + qqContext.add("bindings", GSON.toJsonTree(bindings)); + qqContext.add("usernames", GSON.toJsonTree(usernames)); + + return qqContext.toString(); + } ); if (!ctx.sendReply(PendingMessage.ofMarkdownRaw(answer)).success()) { From 222e61562169196e9ca5c7a0f6b968e189f7343c Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 21 Sep 2026 18:28:23 +0800 Subject: [PATCH 33/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AgentService=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E6=B6=88=E6=81=AF=E6=81=A2=E5=A4=8D=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E7=A7=BB=E9=99=A4=E5=86=97=E4=BD=99=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E5=B9=B6=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/ai/AgentService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index afa66acc..fe27be06 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -140,7 +140,7 @@ public String input(String groupId, String openId, String input, Function Date: Mon, 21 Sep 2026 18:41:38 +0800 Subject: [PATCH 34/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AgentService=20?= =?UTF-8?q?=E5=92=8C=20AiChatHandler=20=E4=B8=AD=E7=9A=84=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=8E=86=E5=8F=B2=E6=B6=88=E6=81=AF=E5=B9=B6=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E8=BE=93=E5=85=A5=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/ai/AgentService.java | 9 +++++---- src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index fe27be06..f5c09f5f 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -17,7 +17,6 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -94,7 +93,7 @@ public void recordHistory(String groupId, String sender, String message) { } } - public String input(String groupId, String openId, String input, Function contextFunc) { + public String input(String groupId, String openId, String rawContent, Function contextFunc) { final StateOwner owner = StateOwner.of(groupId, openId); final State state = getOrCreateState(owner); @@ -103,6 +102,8 @@ public String input(String groupId, String openId, String input, Function pendingMessages; synchronized (state) { @@ -113,12 +114,12 @@ public String input(String groupId, String openId, String input, Function { JsonObject qqContext = new JsonObject(); qqContext.addProperty("in_group", ctx.inGroup()); From fbd782c1d8bc11c6d2eab70a68140fbedbc35ce7 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 21 Sep 2026 19:29:24 +0800 Subject: [PATCH 35/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AgentService=20?= =?UTF-8?q?=E5=92=8C=20Router=20=E4=B8=AD=E7=9A=84=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8F=B7=E5=B9=B6=E7=A7=BB=E9=99=A4=E5=86=97?= =?UTF-8?q?=E4=BD=99=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- src/main/java/xyz/zcraft/seira/ai/AgentService.java | 1 - src/main/java/xyz/zcraft/seira/command/route/Router.java | 8 ++++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 4d0f991f..2485990d 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.5 + 1.12.6 25 diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index f5c09f5f..0c91080c 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -130,7 +130,6 @@ public String input(String groupId, String openId, String rawContent, Function Date: Tue, 22 Sep 2026 00:15:00 +0800 Subject: [PATCH 36/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ReplayCommandHandler?= =?UTF-8?q?=20=E5=92=8C=20TaskCoordinator=20=E4=B8=AD=E7=9A=84=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=B6=85=E6=97=B6=E9=80=9A=E7=9F=A5=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=B9=B6=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/xyz/zcraft/seira/bot/WSClient.java | 3 +- .../zcraft/seira/command/TaskCoordinator.java | 28 ++++++++----------- .../command/handler/ReplayCommandHandler.java | 4 +-- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/bot/WSClient.java b/src/main/java/xyz/zcraft/seira/bot/WSClient.java index 15676595..3188003e 100644 --- a/src/main/java/xyz/zcraft/seira/bot/WSClient.java +++ b/src/main/java/xyz/zcraft/seira/bot/WSClient.java @@ -9,7 +9,6 @@ import org.java_websocket.handshake.ServerHandshake; import xyz.zcraft.seira.bot.data.AccessToken; import xyz.zcraft.seira.bot.data.Attachment; -import xyz.zcraft.seira.bot.data.QQUser; import xyz.zcraft.seira.command.AttachmentHandler; import xyz.zcraft.seira.command.route.Router; import xyz.zcraft.seira.config.AppConfig; @@ -243,7 +242,7 @@ private String stripSelfMention(String content) { private void sendIdentify() { JsonObject data = new JsonObject(); data.addProperty("token", "QQBot " + tokenSupplier.get().token()); - data.addProperty("intents", 1 << 25); + data.addProperty("intents", 1 << 25 | 1 << 26); JsonObject payload = new JsonObject(); payload.addProperty("op", 2); diff --git a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java index beec95f7..f2762925 100644 --- a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java +++ b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java @@ -52,7 +52,7 @@ public static String resolveErrorMessage(Exception exception) { return ApiRequestException.getDefaultMessage(e.getErrorCode()); } case ClosedChannelException _ -> { - return "oStella API 无法连接,请稍后再试。"; + return "oStella API 无法连接,请稍后再试喵"; } case ResolutionException e -> { return e.getMessage(); @@ -65,40 +65,34 @@ public static String resolveErrorMessage(Exception exception) { } cursor = cursor.getCause(); } - return "请求处理失败,请稍后再试。"; + return "请求处理失败,请稍后再试喵"; } public CommandReplyChannel openReplyChannel( - String targetId, - String messageId, - boolean groupMessage, - boolean queueMessageInGroup + String targetId, String messageId, boolean groupMessage, boolean queueMessageInGroup ) { return new OutboundReplyChannel(targetId, messageId, groupMessage, queueMessageInGroup); } - /** - * Tracks queue estimates and elapsed time; the caller executes the request directly. - */ public RequestTiming beginRequest(Context ctx, String requestType, boolean timeoutNotify) { + return beginRequest(ctx, requestType, 60, timeoutNotify ? "请求处理时间超过预期,这可能是由于相关数据缺少缓存,请耐心等待喵。" : null); + } + + public RequestTiming beginRequest(Context ctx, String requestType, int timeout, String timeoutNotify) { long estimatedSeconds = apiRequestStats.estimateAndEnqueue(requestType); ScheduledFuture schedule = null; - if (timeoutNotify) { + if (timeoutNotify != null && !timeoutNotify.isBlank()) { schedule = TIMEOUT_SCHEDULER.schedule( - () -> { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "请求处理时间超过预期,这可能是由于相关数据缺少缓存,请耐心等待喵。")); - }, - 60, - TimeUnit.SECONDS + () -> ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + timeoutNotify)), + timeout, TimeUnit.SECONDS ); } RequestTiming timing = new RequestTiming(requestType, schedule); try { - ctx.sendQueueNotice(PendingMessage.ofMarkdownRaw( - at(ctx) + "请求已加入队列,预计等待时间" + estimatedSeconds + "秒。")); + ctx.sendQueueNotice(PendingMessage.ofMarkdownRaw(at(ctx) + "请求已加入队列,预计等待时间" + estimatedSeconds + "秒。")); return timing; } catch (RuntimeException e) { timing.close(); diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java index 664c6900..6951f083 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java @@ -74,7 +74,7 @@ public void handleR(Context ctx) { } } - try (var _ = taskCoordinator.beginRequest(ctx, "Score Render")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Score Render", false)) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "正在获取谱面以及回放文件,请稍作等待喵...")); var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; Long beatmapId = previous == null ? null : previous.beatmapId(); @@ -193,7 +193,7 @@ public void handleRsc(Context ctx) { } } - try (var _ = taskCoordinator.beginRequest(ctx, "Showcase Render")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Showcase Render", false)) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "正在获取谱面以及回放文件,请稍作等待喵...")); var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; Long beatmapId = previous == null ? null : previous.beatmapId(); From 98e1136fcc439a31db15703f213017be4ec2c45c Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 09:31:10 +0800 Subject: [PATCH 37/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AgentService=20?= =?UTF-8?q?=E5=92=8C=20AiChatHandler=20=E4=B8=AD=E7=9A=84=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=B5=81=E5=BC=8F=E5=A4=84=E7=90=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=B9=B6=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/ai/AgentService.java | 138 ++++++++++++++++-- .../xyz/zcraft/seira/ai/AiChatHandler.java | 65 +++++---- .../xyz/zcraft/seira/ai/StreamHandler.java | 9 ++ .../xyz/zcraft/seira/command/Context.java | 16 ++ 4 files changed, 192 insertions(+), 36 deletions(-) create mode 100644 src/main/java/xyz/zcraft/seira/ai/StreamHandler.java diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index 0c91080c..271e9089 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -6,19 +6,24 @@ import lombok.Getter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; import xyz.zcraft.seira.ai.data.AppConversationBrief; import xyz.zcraft.seira.ai.data.ChatQueryResponse; import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.config.LLMConfig; +import java.io.BufferedReader; +import java.io.InputStreamReader; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; public class AgentService { public static final int CONTEXT_SIZE = 20; @@ -94,6 +99,12 @@ public void recordHistory(String groupId, String sender, String message) { } public String input(String groupId, String openId, String rawContent, Function contextFunc) { + return input(groupId, openId, rawContent, contextFunc, null); + } + + public String input( + String groupId, String openId, String rawContent, Function contextFunc, StreamHandler handler + ) { final StateOwner owner = StateOwner.of(groupId, openId); final State state = getOrCreateState(owner); @@ -102,8 +113,6 @@ public String input(String groupId, String openId, String rawContent, Function pendingMessages; synchronized (state) { @@ -111,17 +120,24 @@ public String input(String groupId, String openId, String rawContent, Function { + String delta = event.get("answer").getAsString(); + + currentMessage.append(delta); + fullAnswer.append(delta); + } + + case "agent_thought", "message_output_end" -> flushMessage(currentMessage, handler); + + case "agent_error" -> { + final String errorMsg = event.get("error_msg").getAsString(); + final String errorCode = event.get("error_code").getAsString(); + handler.onError(errorCode, errorMsg); + throw new RuntimeException("Error when querying stream conversation: " + errorCode + ": " + errorMsg); + } + + case "message_cost" -> { + // token statistics + } + + case "message_end" -> ended = true; + } + + if (ended) { + break; + } + } + } + + final String result = fullAnswer.toString(); + + handler.onComplete(result); + + LOG.info("Chat query success for user {}", openId); + + return result; + } catch (Exception e) { + throw new RuntimeException("Error querying conversation", e); + } + } + + private void flushMessage(@NotNull StringBuilder currentMessage, @NotNull StreamHandler handler) { + final String message = currentMessage.toString().trim(); + + if (message.isBlank()) return; + + currentMessage.setLength(0); + + handler.onText(message); + } + private HttpRequest.Builder newRequest(String path) { return HttpRequest.newBuilder() .uri(java.net.URI.create(this.endpoint + path)) @@ -418,3 +532,5 @@ private HttpRequest.Builder newRequest(String path) { .header("Content-Type", "application/json"); } } + + diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index ed0fb72a..f08e17ed 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -85,42 +85,57 @@ public void handleChat(Context ctx) { return; } - final String answer = at(ctx) + agentService.input( + agentService.input( ctx.groupId(), ctx.senderUserId(), ctx.rawContent(), - input -> { - JsonObject qqContext = new JsonObject(); - qqContext.addProperty("in_group", ctx.inGroup()); - qqContext.addProperty("sender_open_id", ctx.senderUserId()); - qqContext.addProperty("group_id", ctx.groupId()); - - Map bindings = new HashMap<>(); - Map usernames = new HashMap<>(); - - final List ids = resolver.extractAllMentionedIds(input); - ids.add(ctx.senderUserId()); - - for (String openId : ids) { - final Long uid = resolver.resolveBoundUid(openId); - if (uid != null) { - bindings.put(openId, uid); - UserDataStore.findUsername(uid).ifPresent(s -> usernames.put(uid, s)); - } + input -> generateVar(ctx, input), + new StreamHandler() { + @Override + public void onText(String message) { + ctx.send(true, PendingMessage.ofMarkdownRaw(at(ctx) + message)); } - qqContext.add("bindings", GSON.toJsonTree(bindings)); - qqContext.add("usernames", GSON.toJsonTree(usernames)); + @Override + public void onComplete(String fullText) { + // Do nothing + } - return qqContext.toString(); + @Override + public void onError(String errorCode, String errorMsg) { + ctx.send( + true, + PendingMessage.ofMarkdownRaw(at(ctx) + "回复生成失败了喵。\n>" + errorCode + ": " + errorMsg) + ); + } } ); + } + + private String generateVar(Context ctx, String input) { + JsonObject qqContext = new JsonObject(); + qqContext.addProperty("in_group", ctx.inGroup()); + qqContext.addProperty("sender_open_id", ctx.senderUserId()); + qqContext.addProperty("group_id", ctx.groupId()); - if (!ctx.sendReply(PendingMessage.ofMarkdownRaw(answer)).success()) { - if (!ctx.sendMessage(PendingMessage.ofMarkdownRaw(answer)).success()) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "消息发送失败了喵。")); + Map bindings = new HashMap<>(); + Map usernames = new HashMap<>(); + + final List ids = resolver.extractAllMentionedIds(input); + ids.add(ctx.senderUserId()); + + for (String openId : ids) { + final Long uid = resolver.resolveBoundUid(openId); + if (uid != null) { + bindings.put(openId, uid); + UserDataStore.findUsername(uid).ifPresent(s -> usernames.put(uid, s)); } } + + qqContext.add("bindings", GSON.toJsonTree(bindings)); + qqContext.add("usernames", GSON.toJsonTree(usernames)); + + return qqContext.toString(); } public void recordHistory(String groupId, String userId, String rawContent) { diff --git a/src/main/java/xyz/zcraft/seira/ai/StreamHandler.java b/src/main/java/xyz/zcraft/seira/ai/StreamHandler.java new file mode 100644 index 00000000..5179d64e --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/ai/StreamHandler.java @@ -0,0 +1,9 @@ +package xyz.zcraft.seira.ai; + +public interface StreamHandler { + void onText(String message); + + void onComplete(String fullText); + + void onError(String errorCode, String errorMsg); +} diff --git a/src/main/java/xyz/zcraft/seira/command/Context.java b/src/main/java/xyz/zcraft/seira/command/Context.java index 83190fff..523d2b80 100644 --- a/src/main/java/xyz/zcraft/seira/command/Context.java +++ b/src/main/java/xyz/zcraft/seira/command/Context.java @@ -72,6 +72,22 @@ public SendResult sendReply(String message) { return requireReplies().sendReply(PendingMessage.ofMarkdownRaw(message)); } + public SendResult send(boolean replyFirst, PendingMessage message) { + SendResult sendResult; + if (replyFirst) { + sendResult = sendReply(message); + if (!sendResult.success()) { + sendResult = sendMessage(message); + } + } else { + sendResult = sendMessage(message); + if (!sendResult.success()) { + sendResult = sendReply(message); + } + } + return sendResult; + } + /** * Sends an active message to the same user or group, without an inbound message reference. */ From eab1dcfb75ea51fd7c3359036fa8d378f62b19a2 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 10:11:22 +0800 Subject: [PATCH 38/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E9=87=8D=E6=9E=84?= =?UTF-8?q?=20CommandReplyChannel=20=E4=B8=BA=20ReplyChannel=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=B6=88=E6=81=AF=E5=BC=95=E7=94=A8=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/ai/AgentService.java | 1 - .../xyz/zcraft/seira/ai/AiChatHandler.java | 5 +- .../java/xyz/zcraft/seira/bot/WSClient.java | 30 +++++++++- .../seira/command/CommandReplyChannel.java | 19 ------ .../xyz/zcraft/seira/command/Context.java | 28 ++++++--- .../zcraft/seira/command/ReplyChannel.java | 58 +++++++++++++++++++ .../zcraft/seira/command/TaskCoordinator.java | 45 +------------- .../handler/RankGuessCommandHandler.java | 4 +- .../zcraft/seira/command/route/Router.java | 21 ++++--- 9 files changed, 125 insertions(+), 86 deletions(-) delete mode 100644 src/main/java/xyz/zcraft/seira/command/CommandReplyChannel.java create mode 100644 src/main/java/xyz/zcraft/seira/command/ReplyChannel.java diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index 271e9089..22934c2b 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -23,7 +23,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; public class AgentService { public static final int CONTEXT_SIZE = 20; diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index f08e17ed..7502664d 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -93,7 +93,7 @@ public void handleChat(Context ctx) { new StreamHandler() { @Override public void onText(String message) { - ctx.send(true, PendingMessage.ofMarkdownRaw(at(ctx) + message)); + ctx.send(true, PendingMessage.ofMarkdownRaw(at(ctx) + message), true); } @Override @@ -105,7 +105,8 @@ public void onComplete(String fullText) { public void onError(String errorCode, String errorMsg) { ctx.send( true, - PendingMessage.ofMarkdownRaw(at(ctx) + "回复生成失败了喵。\n>" + errorCode + ": " + errorMsg) + PendingMessage.ofMarkdownRaw(at(ctx) + "回复生成失败了喵。\n>" + errorCode + ": " + errorMsg), + true ); } } diff --git a/src/main/java/xyz/zcraft/seira/bot/WSClient.java b/src/main/java/xyz/zcraft/seira/bot/WSClient.java index 3188003e..150f2386 100644 --- a/src/main/java/xyz/zcraft/seira/bot/WSClient.java +++ b/src/main/java/xyz/zcraft/seira/bot/WSClient.java @@ -147,7 +147,20 @@ private void onC2CMsg(JsonObject payload) { String content = data.get("content").getAsString(); String msgId = data.get("id").getAsString(); String openId = data.get("author").getAsJsonObject().get("user_openid").getAsString(); - router.onPrivateMessageReceived(openId, msgId, content); + + String msgIdx = null; + + final JsonArray extArr = data.get("message_scene").getAsJsonObject().get("ext").getAsJsonArray(); + + for (JsonElement elem : extArr) { + final String str = elem.getAsString(); + + if (str.startsWith("msg_idx=")) { + msgIdx = str.substring("msg_idx=".length()); + } + } + + router.onPrivateMessageReceived(openId, msgId, content, msgIdx); } private void onC2CFile(JsonObject payload) { @@ -177,6 +190,19 @@ private void onGroupMsg(JsonObject payload) { JsonObject author = data.get("author").getAsJsonObject(); String openId = author.get("member_openid").getAsString(); String groupId = data.get("group_openid").getAsString(); + + String msgIdx = null; + + final JsonArray extArr = data.get("message_scene").getAsJsonObject().get("ext").getAsJsonArray(); + + for (JsonElement elem : extArr) { + final String str = elem.getAsString(); + + if (str.startsWith("msg_idx=")) { + msgIdx = str.substring("msg_idx=".length()); + } + } + List attachments = parseAttachments(data); Map mentions = parseMentions(data); @@ -191,7 +217,7 @@ private void onGroupMsg(JsonObject payload) { mentions )); } - router.onGroupMessageReceived(groupId, openId, msgId, content); + router.onGroupMessageReceived(groupId, openId, msgId, content, msgIdx); } private Map parseMentions(JsonObject data) { diff --git a/src/main/java/xyz/zcraft/seira/command/CommandReplyChannel.java b/src/main/java/xyz/zcraft/seira/command/CommandReplyChannel.java deleted file mode 100644 index 5b9228fd..00000000 --- a/src/main/java/xyz/zcraft/seira/command/CommandReplyChannel.java +++ /dev/null @@ -1,19 +0,0 @@ -package xyz.zcraft.seira.command; - -import xyz.zcraft.seira.bot.data.PendingMessage; -import xyz.zcraft.seira.data.SendResult; - -/** - * The outbound side of one command invocation. - * - *

A reply is associated with the inbound QQ message. A proactive message is - * sent to the same conversation without that association and therefore does - * not consume the passive reply sequence.

- */ -public interface CommandReplyChannel { - SendResult sendReply(PendingMessage message); - - SendResult sendProactive(PendingMessage message); - - SendResult sendQueueNotice(PendingMessage message); -} diff --git a/src/main/java/xyz/zcraft/seira/command/Context.java b/src/main/java/xyz/zcraft/seira/command/Context.java index 523d2b80..f55b8ae6 100644 --- a/src/main/java/xyz/zcraft/seira/command/Context.java +++ b/src/main/java/xyz/zcraft/seira/command/Context.java @@ -13,7 +13,7 @@ public record Context( String[] args, String query, String rawContent, - CommandReplyChannel replies) { + ReplyChannel replies) { public Context( String senderUserId, String groupId, @@ -48,7 +48,7 @@ public boolean inGroup() { return groupId != null && !groupId.isBlank(); } - public Context withReplies(CommandReplyChannel replyChannel) { + public Context withReplies(ReplyChannel replyChannel) { return new Context( senderUserId, groupId, messageId, command, args, query, rawContent, Objects.requireNonNull(replyChannel, "replyChannel") @@ -65,7 +65,11 @@ public Context asCommand(String nextCommand, String[] nextArgs, String nextQuery * Sends a passive reply associated with the message that invoked this command. */ public SendResult sendReply(PendingMessage message) { - return requireReplies().sendReply(Objects.requireNonNull(message, "message")); + return sendReply(message, false); + } + + public SendResult sendReply(PendingMessage message, boolean ref) { + return requireReplies().sendReply(Objects.requireNonNull(message, "message"), ref); } public SendResult sendReply(String message) { @@ -73,16 +77,20 @@ public SendResult sendReply(String message) { } public SendResult send(boolean replyFirst, PendingMessage message) { + return send(replyFirst, message, false); + } + + public SendResult send(boolean replyFirst, PendingMessage message, boolean ref) { SendResult sendResult; if (replyFirst) { - sendResult = sendReply(message); + sendResult = sendReply(message, ref); if (!sendResult.success()) { - sendResult = sendMessage(message); + sendResult = sendMessage(message, ref); } } else { - sendResult = sendMessage(message); + sendResult = sendMessage(message, ref); if (!sendResult.success()) { - sendResult = sendReply(message); + sendResult = sendReply(message, ref); } } return sendResult; @@ -91,6 +99,10 @@ public SendResult send(boolean replyFirst, PendingMessage message) { /** * Sends an active message to the same user or group, without an inbound message reference. */ + public SendResult sendMessage(PendingMessage message, boolean ref) { + return requireReplies().sendProactive(Objects.requireNonNull(message, "message"), ref); + } + public SendResult sendMessage(PendingMessage message) { return requireReplies().sendProactive(Objects.requireNonNull(message, "message")); } @@ -99,7 +111,7 @@ public SendResult sendQueueNotice(PendingMessage message) { return requireReplies().sendQueueNotice(Objects.requireNonNull(message, "message")); } - private CommandReplyChannel requireReplies() { + private ReplyChannel requireReplies() { if (replies == null) { throw new IllegalStateException("This command context is not bound to a reply channel"); } diff --git a/src/main/java/xyz/zcraft/seira/command/ReplyChannel.java b/src/main/java/xyz/zcraft/seira/command/ReplyChannel.java new file mode 100644 index 00000000..34744a02 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/command/ReplyChannel.java @@ -0,0 +1,58 @@ +package xyz.zcraft.seira.command; + +import xyz.zcraft.seira.bot.data.MessageReference; +import xyz.zcraft.seira.bot.data.PendingMessage; +import xyz.zcraft.seira.data.SendResult; + +import java.util.concurrent.atomic.AtomicInteger; + +public class ReplyChannel { + private final TaskCoordinator taskCoordinator; + private final String targetId; + private final String messageId; + private final String refMsgIdx; + private final boolean groupMessage; + private final boolean queueMessageInGroup; + private final AtomicInteger passiveSequence = new AtomicInteger(1); + + ReplyChannel(TaskCoordinator taskCoordinator, + String targetId, + String messageId, + boolean groupMessage, + boolean queueMessageInGroup, + String refMsgIdx + ) { + this.taskCoordinator = taskCoordinator; + this.targetId = targetId; + this.messageId = messageId; + this.groupMessage = groupMessage; + this.queueMessageInGroup = queueMessageInGroup; + this.refMsgIdx = refMsgIdx; + } + + public synchronized SendResult sendReply(PendingMessage message) { + return taskCoordinator.sendOutboundMessage(targetId, messageId, groupMessage, message, passiveSequence); + } + + public synchronized SendResult sendReply(PendingMessage message, boolean ref) { + if (ref) message.ref(new MessageReference(refMsgIdx)); + return taskCoordinator.sendOutboundMessage(targetId, messageId, groupMessage, message, passiveSequence); + } + + public synchronized SendResult sendProactive(PendingMessage message) { + return sendReply(message, false); + } + + public synchronized SendResult sendProactive(PendingMessage message, boolean ref) { + if (ref) message.ref(new MessageReference(refMsgIdx)); + return taskCoordinator.sendOutboundMessage(targetId, null, groupMessage, message, null); + } + + public synchronized SendResult sendQueueNotice(PendingMessage message) { + if (groupMessage && !queueMessageInGroup) { + return new SendResult(true, null); + } + return sendReply(message); + } + +} diff --git a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java index f2762925..03b36105 100644 --- a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java +++ b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java @@ -68,10 +68,10 @@ public static String resolveErrorMessage(Exception exception) { return "请求处理失败,请稍后再试喵"; } - public CommandReplyChannel openReplyChannel( - String targetId, String messageId, boolean groupMessage, boolean queueMessageInGroup + public ReplyChannel openReplyChannel( + String targetId, String messageId, boolean groupMessage, boolean queueMessageInGroup, String refMsgIdx ) { - return new OutboundReplyChannel(targetId, messageId, groupMessage, queueMessageInGroup); + return new ReplyChannel(this, targetId, messageId, groupMessage, queueMessageInGroup, refMsgIdx); } public RequestTiming beginRequest(Context ctx, String requestType, boolean timeoutNotify) { @@ -258,43 +258,4 @@ public void close() { Math.max(1L, (System.nanoTime() - startedAt) / 1_000_000L)); } } - - private final class OutboundReplyChannel implements CommandReplyChannel { - private final String targetId; - private final String messageId; - private final boolean groupMessage; - private final boolean queueMessageInGroup; - private final AtomicInteger passiveSequence = new AtomicInteger(1); - - private OutboundReplyChannel( - String targetId, - String messageId, - boolean groupMessage, - boolean queueMessageInGroup - ) { - this.targetId = targetId; - this.messageId = messageId; - this.groupMessage = groupMessage; - this.queueMessageInGroup = queueMessageInGroup; - } - - @Override - public synchronized SendResult sendReply(PendingMessage message) { - return sendOutboundMessage(targetId, messageId, groupMessage, message, passiveSequence); - } - - @Override - public synchronized SendResult sendProactive(PendingMessage message) { - return sendOutboundMessage(targetId, null, groupMessage, message, null); - } - - @Override - public synchronized SendResult sendQueueNotice(PendingMessage message) { - if (groupMessage && !queueMessageInGroup) { - return new SendResult(true, null); - } - return sendReply(message); - } - - } } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java index bd36f636..0ec6af7e 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java @@ -681,9 +681,7 @@ private void end(Context ctx, boolean force) { case FINISHED -> replyFactory.rankGuessResultMessage(ctx, result.round(), result.rankType()); }; - if (!ctx.sendReply(message).success()) { - ctx.sendMessage(message); - } + ctx.send(true, message, true); } enum LeaderboardType { diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index 208bd814..fc11dbc7 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -175,15 +175,17 @@ private String getAvatar(String openId) { return QQApi.getAvatarUrl(configSupplier.get().qq().appId(), openId); } - public void onPrivateMessageReceived(String userId, String messageId, String rawContent) { - handleMessageReceived(userId, null, userId, messageId, rawContent, false); + public void onPrivateMessageReceived(String userId, String messageId, String rawContent, String msgIdx) { + handleMessageReceived(userId, null, userId, messageId, rawContent, false, msgIdx); } - public void onGroupMessageReceived(String groupId, String senderUserId, String messageId, String rawContent) { - handleMessageReceived(groupId, groupId, senderUserId, messageId, rawContent, true); + public void onGroupMessageReceived(String groupId, String senderUserId, String messageId, String rawContent, String msgIdx) { + handleMessageReceived(groupId, groupId, senderUserId, messageId, rawContent, true, msgIdx); } - private void handleMessageReceived(String targetId, String groupId, String userId, String messageId, String rawContent, boolean groupMessage) { + private void handleMessageReceived( + String targetId, String groupId, String userId, String messageId, String rawContent, boolean groupMessage, String msgIdx + ) { AtomicInteger messageSeqCounter = new AtomicInteger(1); try { final boolean group = groupMessage && groupId != null && !groupId.isBlank(); @@ -228,8 +230,8 @@ private void handleMessageReceived(String targetId, String groupId, String userI } if (parseResult.status() == CommandParser.ParseResult.Status.TEXT) { - CommandReplyChannel replies = taskCoordinator.openReplyChannel( - targetId, messageId, groupMessage, false + ReplyChannel replies = taskCoordinator.openReplyChannel( + targetId, messageId, groupMessage, false, msgIdx ); final boolean permit = AiPermission.doPermit(groupId); @@ -241,9 +243,10 @@ private void handleMessageReceived(String targetId, String groupId, String userI return; } - CommandReplyChannel replies = taskCoordinator.openReplyChannel( - targetId, messageId, groupMessage, config.seira().queueMessageInGroup() + ReplyChannel replies = taskCoordinator.openReplyChannel( + targetId, messageId, groupMessage, config.seira().queueMessageInGroup(), msgIdx ); + if (parseResult.status() == CommandParser.ParseResult.Status.EMPTY_COMMAND && !group) { replies.sendReply(PendingMessage.ofString("请输入指令。使用/help获取帮助。")); From a33e2cef1874fcda0ce711a8066c149395c2c643 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 10:39:36 +0800 Subject: [PATCH 39/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ReplyFactory=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E6=B6=88=E6=81=AF=E6=A0=BC=E5=BC=8F=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20Markdown=20=E6=A0=B7=E5=BC=8F=E4=BB=A5?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/command/reply/ReplyFactory.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index 0b0a48f7..c2bb9f9d 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -106,7 +106,7 @@ public static String url(String text, String url) { public static PendingMessage replayUploadMessage(ReplayUploadInfo info) { return PendingMessage.ofMarkdownRaw( - ("\n" + "## Replay上传成功~" + "\n" + + ("\n" + "__Replay上传成功~__" + "\n" + "> 成绩: " + s(info.scoreId()) + "\n" + "> 谱面: " + m(info.beatmapId()) + "\n" + "> 用户: " + u(info.userId(), info.username()) + "\n").trim(), @@ -643,16 +643,16 @@ public static String friendContent(Context ctx, boolean collapsed = false; - sb.append("> 好友←→ (").append(mutual.size()).append(")\n>"); + sb.append("> __好友←→ (").append(mutual.size()).append(")__ \n>"); collapsed |= appendFriends(ctx, mutual, sb); - sb.append("\n> 仅关注→ (").append(onlyFollowed.size()).append(")\n>"); + sb.append("\n> __仅关注→ (").append(onlyFollowed.size()).append(")__ \n>"); collapsed |= appendFriends(ctx, onlyFollowed, sb); - sb.append("\n> 仅粉丝← ("); + sb.append("\n> __仅粉丝← ("); sb.append(onlyFollower.size()).append(" 已知"); if (all) sb.append(" 共 ").append(Math.max(self.getFollowerCount() - allMutualCount, 0)); - sb.append(")\n>"); + sb.append(")__ \n>"); collapsed |= appendFriends(ctx, onlyFollower, sb); if (ctx.inGroup() && collapsed) { From 14172c5681a2853b8859bf85aeac051f3aa4e273 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 11:57:37 +0800 Subject: [PATCH 40/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AgentService=20?= =?UTF-8?q?=E5=92=8C=20Router=20=E4=B8=AD=E7=9A=84=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD=E4=BB=A5=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/ai/AgentService.java | 8 ++--- .../xyz/zcraft/seira/command/Context.java | 34 +++++++++---------- .../zcraft/seira/command/route/Router.java | 19 ++++++++--- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index 22934c2b..0f539994 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -25,7 +25,7 @@ import java.util.stream.Collectors; public class AgentService { - public static final int CONTEXT_SIZE = 20; + public static final int CONTEXT_SIZE = 30; private final Api api; @@ -76,7 +76,7 @@ public void recordHistory(String groupId, String sender, String message) { return; } - final String historyMessage = sender + ": " + message; + final String historyMessage = "<@" + sender + ">" + ": " + message; final Deque log = chatLog.computeIfAbsent(groupId, _ -> new ArrayDeque<>()); @@ -122,7 +122,7 @@ public String input( String query = ""; if (pendingMessages.isEmpty()) { - query = openId + ": " + rawContent; + query = "<@" + openId + ">" + ": " + rawContent; } else { if (pendingMessages.size() == CONTEXT_SIZE) { query += "====== ...历史消息较多已省略 ======"; @@ -132,7 +132,7 @@ public String input( + "====== 以上是最近的所有消息 ======\n" + "====== 以下是本次询问的内容 ======\n" + "\n" - + openId + ": " + rawContent; + + "<@" + openId + ">" + ": " + rawContent; } recordHistory(groupId, openId, rawContent); diff --git a/src/main/java/xyz/zcraft/seira/command/Context.java b/src/main/java/xyz/zcraft/seira/command/Context.java index f55b8ae6..c1130a1f 100644 --- a/src/main/java/xyz/zcraft/seira/command/Context.java +++ b/src/main/java/xyz/zcraft/seira/command/Context.java @@ -4,26 +4,17 @@ import xyz.zcraft.seira.data.SendResult; import java.util.Objects; +import java.util.function.Consumer; public record Context( - String senderUserId, - String groupId, - String messageId, - String command, - String[] args, - String query, - String rawContent, - ReplyChannel replies) { + String senderUserId, String groupId, String messageId, String command, + String[] args, String query, String rawContent, ReplyChannel replies, Consumer recorder +) { public Context( - String senderUserId, - String groupId, - String messageId, - String command, - String[] args, - String rawContent, - String query + String senderUserId, String groupId, String messageId, String command, + String[] args, String rawContent, String query ) { - this(senderUserId, groupId, messageId, command, args, query, rawContent, null); + this(senderUserId, groupId, messageId, command, args, query, rawContent, null, null); } public Context { @@ -51,13 +42,20 @@ public boolean inGroup() { public Context withReplies(ReplyChannel replyChannel) { return new Context( senderUserId, groupId, messageId, command, args, query, rawContent, - Objects.requireNonNull(replyChannel, "replyChannel") + Objects.requireNonNull(replyChannel, "replyChannel"), recorder ); } public Context asCommand(String nextCommand, String[] nextArgs, String nextQuery) { return new Context( - senderUserId, groupId, messageId, nextCommand, nextArgs, nextQuery, rawContent, replies + senderUserId, groupId, messageId, nextCommand, nextArgs, nextQuery, rawContent, replies, recorder + ); + } + + public Context withRecorder(Consumer recorder) { + return new Context( + senderUserId, groupId, messageId, command, args, query, rawContent, + replies, Objects.requireNonNull(recorder, "recorder") ); } diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index fc11dbc7..00d08365 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -229,15 +229,16 @@ private void handleMessageReceived( return; } + final boolean permitAi = AiPermission.doPermit(groupId); + if (parseResult.status() == CommandParser.ParseResult.Status.TEXT) { ReplyChannel replies = taskCoordinator.openReplyChannel( targetId, messageId, groupMessage, false, msgIdx ); - final boolean permit = AiPermission.doPermit(groupId); - if (beingAt && permit) { + if (beingAt && permitAi) { aiChatHandler.handleChat(parseResult.context().withReplies(replies)); - } else if (permit) { + } else if (permitAi) { aiChatHandler.recordHistory(groupId, userId, rawContent); } return; @@ -253,9 +254,19 @@ private void handleMessageReceived( return; } - Context context = parseResult.context().withReplies(replies); + Context context = parseResult.context() + .withReplies(replies) + .withRecorder(s -> { + if (permitAi) { + aiChatHandler.recordHistory(groupId, "Seira(你,回复" + userId + "的消息)", s); + } + }); + commandExecutor.execute(() -> { try { + if (permitAi) { + aiChatHandler.recordHistory(groupId, userId, context.rawContent()); + } LOG.info("Routing {} message : {}", groupMessage ? "group" : "private", context.rawContent()); dispatch(context); NoticesHelper.checkNotices(context); From 29427795ecf5f1da8b1db30661c709b256a05e66 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 20:08:40 +0800 Subject: [PATCH 41/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AiChatHandler=20?= =?UTF-8?q?=E5=92=8C=20Resolver=20=E4=B8=AD=E7=9A=84=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=BE=A4=E7=BB=84=E6=8F=90=E5=8F=8A=E5=8E=86=E5=8F=B2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E4=BB=A5=E5=A2=9E=E5=BC=BA=E6=B6=88=E6=81=AF=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/ai/AiChatHandler.java | 31 +++++++++++++------ .../zcraft/seira/command/parse/Resolver.java | 6 ++-- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index 7502664d..8bbb6666 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -9,10 +9,8 @@ import xyz.zcraft.seira.db.UserDataStore; import xyz.zcraft.seira.services.AiPermission; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; @@ -24,6 +22,7 @@ public class AiChatHandler { private final Predicate adminAuthorizer; private final AgentService agentService; private final Function botStateGetter; + private final Map> groupMentionedHistory = new ConcurrentHashMap<>(); public AiChatHandler( Resolver resolver, AgentService agentService, Predicate isAdmin, @@ -119,22 +118,34 @@ private String generateVar(Context ctx, String input) { qqContext.addProperty("sender_open_id", ctx.senderUserId()); qqContext.addProperty("group_id", ctx.groupId()); - Map bindings = new HashMap<>(); - Map usernames = new HashMap<>(); + Map> bindings = new HashMap<>(); + + final Deque mentionedHistory = groupMentionedHistory.computeIfAbsent(ctx.groupId(), _ -> new ArrayDeque<>(100)); + + final Set ids = resolver.extractAllMentionedIds(input); + + for (String id : ids) { + if (!mentionedHistory.contains(id)) { + mentionedHistory.push(id); + } + } + + while (mentionedHistory.size() > 64) { + mentionedHistory.removeFirst(); + } - final List ids = resolver.extractAllMentionedIds(input); ids.add(ctx.senderUserId()); + ids.addAll(mentionedHistory); for (String openId : ids) { final Long uid = resolver.resolveBoundUid(openId); if (uid != null) { - bindings.put(openId, uid); - UserDataStore.findUsername(uid).ifPresent(s -> usernames.put(uid, s)); + final String username = UserDataStore.findUsername(uid).orElse(""); + bindings.put(openId, Map.of("uid", uid.toString(), "username", username)); } } qqContext.add("bindings", GSON.toJsonTree(bindings)); - qqContext.add("usernames", GSON.toJsonTree(usernames)); return qqContext.toString(); } diff --git a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java index 97764f47..adb0e960 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java @@ -130,12 +130,12 @@ public String extractMentionedUserId(String token) { return null; } - public List extractAllMentionedIds(String token) { + public Set extractAllMentionedIds(String token) { if (token == null) { - return List.of(); + return Set.of(); } - List result = new LinkedList<>(); + Set result = new HashSet<>(); Patterns.QQ_AT_IDS_PATTERN.matcher(token).results().forEach(m -> result.add(m.group(1))); From 974aad8d688cbe688e80a571f09c6fcec1f6b56a Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 20:26:48 +0800 Subject: [PATCH 42/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=8F=91=E9=80=81=E9=80=BB=E8=BE=91=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD=E4=BB=A5?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E6=B6=88=E6=81=AF=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../xyz/zcraft/seira/bot/data/PendingMessage.java | 8 ++++++++ src/main/java/xyz/zcraft/seira/command/Context.java | 13 ++++++++++++- .../xyz/zcraft/seira/command/TaskCoordinator.java | 7 ++++++- .../java/xyz/zcraft/seira/command/route/Router.java | 1 + 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 2485990d..5af75698 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.6 + 1.12.7 25 diff --git a/src/main/java/xyz/zcraft/seira/bot/data/PendingMessage.java b/src/main/java/xyz/zcraft/seira/bot/data/PendingMessage.java index d9da7f41..4f87c143 100644 --- a/src/main/java/xyz/zcraft/seira/bot/data/PendingMessage.java +++ b/src/main/java/xyz/zcraft/seira/bot/data/PendingMessage.java @@ -95,4 +95,12 @@ public PendingMessage ref(MessageReference reference) { this.messageReference = reference; return this; } + + public String getRealContent() { + if (this instanceof MDMessage md) { + return md.getMarkdown(); + } else { + return getContent(); + } + } } diff --git a/src/main/java/xyz/zcraft/seira/command/Context.java b/src/main/java/xyz/zcraft/seira/command/Context.java index c1130a1f..9ff9febb 100644 --- a/src/main/java/xyz/zcraft/seira/command/Context.java +++ b/src/main/java/xyz/zcraft/seira/command/Context.java @@ -67,11 +67,14 @@ public SendResult sendReply(PendingMessage message) { } public SendResult sendReply(PendingMessage message, boolean ref) { + record(message); return requireReplies().sendReply(Objects.requireNonNull(message, "message"), ref); } public SendResult sendReply(String message) { - return requireReplies().sendReply(PendingMessage.ofMarkdownRaw(message)); + final PendingMessage msg = PendingMessage.ofMarkdownRaw(message); + record(msg); + return requireReplies().sendReply(msg); } public SendResult send(boolean replyFirst, PendingMessage message) { @@ -98,14 +101,17 @@ public SendResult send(boolean replyFirst, PendingMessage message, boolean ref) * Sends an active message to the same user or group, without an inbound message reference. */ public SendResult sendMessage(PendingMessage message, boolean ref) { + record(message); return requireReplies().sendProactive(Objects.requireNonNull(message, "message"), ref); } public SendResult sendMessage(PendingMessage message) { + record(message); return requireReplies().sendProactive(Objects.requireNonNull(message, "message")); } public SendResult sendQueueNotice(PendingMessage message) { + record(message); return requireReplies().sendQueueNotice(Objects.requireNonNull(message, "message")); } @@ -115,4 +121,9 @@ private ReplyChannel requireReplies() { } return replies; } + + private void record(PendingMessage message) { + if (message == null || recorder == null) return; + recorder.accept(message.getRealContent()); + } } diff --git a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java index 03b36105..877595f8 100644 --- a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java +++ b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java @@ -1,5 +1,6 @@ package xyz.zcraft.seira.command; +import lombok.Setter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; @@ -23,6 +24,8 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; @@ -232,7 +235,9 @@ public SendResult sendOutboundMessage(String targetId, String messageId, boolean discordBridgeService.acceptQqCommandReply(targetId, portableResult); } - return new SendResult(uploadResult && sentMessage != null, sentMessage); + final boolean success = uploadResult && sentMessage != null; + + return new SendResult(success, sentMessage); } public final class RequestTiming implements AutoCloseable { diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index 00d08365..13e8c763 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -34,6 +34,7 @@ import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; From a9be5a959b9c0b26242f94dbd8558e2e44f506e0 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 20:38:04 +0800 Subject: [PATCH 43/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AiChatHandler=20?= =?UTF-8?q?=E5=92=8C=20Router=20=E4=B8=AD=E7=9A=84=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E9=99=84=E4=BB=B6=E6=94=AF=E6=8C=81=E4=BB=A5=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/ai/AiChatHandler.java | 13 ++++++++++++- .../java/xyz/zcraft/seira/bot/WSClient.java | 16 ++++++++++++++-- .../zcraft/seira/command/route/Router.java | 19 +++++++++++++------ 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index 8bbb6666..f5d49d0c 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -2,6 +2,7 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; +import xyz.zcraft.seira.bot.data.Attachment; import xyz.zcraft.seira.bot.data.GroupBotState; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; @@ -151,6 +152,16 @@ private String generateVar(Context ctx, String input) { } public void recordHistory(String groupId, String userId, String rawContent) { - agentService.recordHistory(groupId, userId, rawContent); + recordHistory(groupId, userId, rawContent.trim(), List.of()); + } + + public void recordHistory(String groupId, String userId, String rawContent, List attachments) { + StringBuilder sb = new StringBuilder(rawContent.trim()); + if (attachments != null && !attachments.isEmpty()) { + for (Attachment attachment : attachments) { + sb.append("\n").append("![%s](%s)".formatted(attachment.filename(), attachment.url())); + } + } + agentService.recordHistory(groupId, userId, sb.toString()); } } diff --git a/src/main/java/xyz/zcraft/seira/bot/WSClient.java b/src/main/java/xyz/zcraft/seira/bot/WSClient.java index 150f2386..1fed923e 100644 --- a/src/main/java/xyz/zcraft/seira/bot/WSClient.java +++ b/src/main/java/xyz/zcraft/seira/bot/WSClient.java @@ -148,6 +148,17 @@ private void onC2CMsg(JsonObject payload) { String msgId = data.get("id").getAsString(); String openId = data.get("author").getAsJsonObject().get("user_openid").getAsString(); + final JsonArray attachments = data.getAsJsonArray("attachments"); + List attachmentList = new ArrayList<>(); + + if (attachments != null && !attachments.isJsonNull()) { + for (JsonElement attachmentElem : attachments) { + JsonObject attachmentObj = attachmentElem.getAsJsonObject(); + Attachment attachment = gson.fromJson(attachmentObj, Attachment.class); + attachmentList.add(attachment); + } + } + String msgIdx = null; final JsonArray extArr = data.get("message_scene").getAsJsonObject().get("ext").getAsJsonArray(); @@ -160,7 +171,7 @@ private void onC2CMsg(JsonObject payload) { } } - router.onPrivateMessageReceived(openId, msgId, content, msgIdx); + router.onPrivateMessageReceived(openId, msgId, content, msgIdx, attachmentList); } private void onC2CFile(JsonObject payload) { @@ -217,7 +228,8 @@ private void onGroupMsg(JsonObject payload) { mentions )); } - router.onGroupMessageReceived(groupId, openId, msgId, content, msgIdx); + + router.onGroupMessageReceived(groupId, openId, msgId, content, msgIdx, attachments); } private Map parseMentions(JsonObject data) { diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index 13e8c763..328a9052 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -9,6 +9,7 @@ import xyz.zcraft.seira.api.data.VideoRenderRecord; import xyz.zcraft.seira.bot.MessageSender; import xyz.zcraft.seira.bot.QQApi; +import xyz.zcraft.seira.bot.data.Attachment; import xyz.zcraft.seira.bot.data.GroupBotState; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.bot.data.QQUser; @@ -30,6 +31,7 @@ import xyz.zcraft.seira.watch.MultiplayerRoomWatchService; import xyz.zcraft.seira.watch.ScoreWatchService; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.Executor; @@ -176,16 +178,21 @@ private String getAvatar(String openId) { return QQApi.getAvatarUrl(configSupplier.get().qq().appId(), openId); } - public void onPrivateMessageReceived(String userId, String messageId, String rawContent, String msgIdx) { - handleMessageReceived(userId, null, userId, messageId, rawContent, false, msgIdx); + public void onPrivateMessageReceived( + String userId, String messageId, String rawContent, String msgIdx ,List attachments + ) { + handleMessageReceived(userId, null, userId, messageId, rawContent, false, msgIdx, attachments); } - public void onGroupMessageReceived(String groupId, String senderUserId, String messageId, String rawContent, String msgIdx) { - handleMessageReceived(groupId, groupId, senderUserId, messageId, rawContent, true, msgIdx); + public void onGroupMessageReceived( + String groupId, String senderUserId, String messageId, String rawContent, String msgIdx, List attachments + ) { + handleMessageReceived(groupId, groupId, senderUserId, messageId, rawContent, true, msgIdx, attachments); } private void handleMessageReceived( - String targetId, String groupId, String userId, String messageId, String rawContent, boolean groupMessage, String msgIdx + String targetId, String groupId, String userId, String messageId, String rawContent, + boolean groupMessage, String msgIdx, List attachments ) { AtomicInteger messageSeqCounter = new AtomicInteger(1); try { @@ -240,7 +247,7 @@ private void handleMessageReceived( if (beingAt && permitAi) { aiChatHandler.handleChat(parseResult.context().withReplies(replies)); } else if (permitAi) { - aiChatHandler.recordHistory(groupId, userId, rawContent); + aiChatHandler.recordHistory(groupId, userId, rawContent, attachments); } return; } From 3c1f63c9f35c2d0160ced08c160f95df6db9bd2c Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 20:46:18 +0800 Subject: [PATCH 44/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20Resolver=20=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E6=B6=88=E6=81=AF=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=AF=B9=E6=95=B0=E5=AD=97=E5=8A=A0?= =?UTF-8?q?=E5=8F=B7=E6=A0=BC=E5=BC=8F=E7=9A=84=E6=94=AF=E6=8C=81=E4=BB=A5?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=91=BD=E4=BB=A4=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/command/parse/Resolver.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java index adb0e960..2473d440 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java @@ -25,6 +25,8 @@ public String sanitize(String rawContent) { // Add surrounding space to <@> before expanding compact commands so /bp5<@...> is recognized. rawContent = Patterns.QQ_INLINE_AT_PATTERN.matcher(rawContent).replaceAll(r -> " " + r.group() + " "); + rawContent = Pattern.compile("(\\d+)(\\+)").matcher(rawContent).replaceAll(r -> r.group(1) + " " + r.group(2)); + Matcher matcher = Patterns.COMPACT_SCORE_COMMAND_PATTERN.matcher(rawContent); if (matcher.find()) { String type = matcher.group(1).toLowerCase(Locale.ROOT); From 69736fe1c516e1976bb2299bb753274d0fb4baf1 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 20:48:53 +0800 Subject: [PATCH 45/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20Resolver=20=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E6=B6=88=E6=81=AF=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=AF=B9=E6=95=B0=E5=AD=97=E5=8A=A0?= =?UTF-8?q?=E5=8F=B7=E6=A0=BC=E5=BC=8F=E7=9A=84=E6=94=AF=E6=8C=81=E4=BB=A5?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=91=BD=E4=BB=A4=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/command/parse/Resolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java index 2473d440..01290593 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java @@ -25,7 +25,7 @@ public String sanitize(String rawContent) { // Add surrounding space to <@> before expanding compact commands so /bp5<@...> is recognized. rawContent = Patterns.QQ_INLINE_AT_PATTERN.matcher(rawContent).replaceAll(r -> " " + r.group() + " "); - rawContent = Pattern.compile("(\\d+)(\\+)").matcher(rawContent).replaceAll(r -> r.group(1) + " " + r.group(2)); + rawContent = rawContent.replace("+", " +"); Matcher matcher = Patterns.COMPACT_SCORE_COMMAND_PATTERN.matcher(rawContent); if (matcher.find()) { From 5c8ba83e5b2a716713c2ffeee45972c183b726d0 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 21:59:02 +0800 Subject: [PATCH 46/96] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=A4=9A=E4=BA=BA?= =?UTF-8?q?=E6=88=BF=E9=97=B4=E7=9B=91=E8=A7=86=E7=9B=B8=E5=85=B3=E7=B1=BB?= =?UTF-8?q?=EF=BC=8C=E7=BB=9F=E4=B8=80=E5=91=BD=E5=90=8D=E4=B8=BA=20MPNoti?= =?UTF-8?q?fier=E3=80=81MPVersion=20=E5=92=8C=20MPWatchService=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B6=88=E6=81=AF=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/bot/QQBot.java | 16 +++---- .../MultiplayerRoomWatchCommandHandler.java | 29 +++++++------ .../zcraft/seira/command/route/Router.java | 7 ++-- .../xyz/zcraft/seira/watch/MPNotifier.java | 36 ++++++++++++++++ ...iplayerRoomVersion.java => MPVersion.java} | 6 +-- ...layerRoomWatchApi.java => MPWatchApi.java} | 18 ++++---- ...mWatchService.java => MPWatchService.java} | 40 ++++++++---------- .../seira/watch/MultiplayerRoomNotifier.java | 7 ---- .../seira/watch/MultiplayerRoomWatchApi.java | 7 ---- .../watch/QqMultiplayerRoomNotifier.java | 42 ------------------- .../xyz/zcraft/seira/watch/RoomWatchView.java | 2 +- 11 files changed, 90 insertions(+), 120 deletions(-) create mode 100644 src/main/java/xyz/zcraft/seira/watch/MPNotifier.java rename src/main/java/xyz/zcraft/seira/watch/{MultiplayerRoomVersion.java => MPVersion.java} (76%) rename src/main/java/xyz/zcraft/seira/watch/{OstellaMultiplayerRoomWatchApi.java => MPWatchApi.java} (87%) rename src/main/java/xyz/zcraft/seira/watch/{MultiplayerRoomWatchService.java => MPWatchService.java} (89%) delete mode 100644 src/main/java/xyz/zcraft/seira/watch/MultiplayerRoomNotifier.java delete mode 100644 src/main/java/xyz/zcraft/seira/watch/MultiplayerRoomWatchApi.java delete mode 100644 src/main/java/xyz/zcraft/seira/watch/QqMultiplayerRoomNotifier.java diff --git a/src/main/java/xyz/zcraft/seira/bot/QQBot.java b/src/main/java/xyz/zcraft/seira/bot/QQBot.java index 4ce7a2a1..f3c07c70 100644 --- a/src/main/java/xyz/zcraft/seira/bot/QQBot.java +++ b/src/main/java/xyz/zcraft/seira/bot/QQBot.java @@ -43,7 +43,7 @@ public class QQBot implements AutoCloseable, ConsoleRuntimeControl { @Getter private final MessageSender sender; private final ScoreWatchService watchService; - private final MultiplayerRoomWatchService multiplayerRoomWatchService; + private final MPWatchService mpWatchService; private final RankGuessGameService rankGuessGameService; private final AgentService agentService; private final RealtimeServiceInterruptionNotifier interruptionNotifier; @@ -92,9 +92,9 @@ public QQBot( ); LOG.info("Initializing multiplayer room watch service"); - this.multiplayerRoomWatchService = new MultiplayerRoomWatchService( - new OstellaMultiplayerRoomWatchApi(config.ostella().endpoint(), config.ostella().token()), - new QqMultiplayerRoomNotifier(sender), + this.mpWatchService = new MPWatchService( + new MPWatchApi(config.ostella().endpoint(), config.ostella().token()), + new MPNotifier(sender), Duration.ofSeconds(config.seira().effectiveMultiplayerWatchIntervalSeconds()) ); @@ -112,7 +112,7 @@ public QQBot( admins, bindingService, watchService, - multiplayerRoomWatchService, + mpWatchService, discordBridgeService, rankGuessGameService, executors.commandTasks(), @@ -142,7 +142,7 @@ public void start() { runnerThread = Thread.currentThread(); tokenManager.start(); watchService.start(); - multiplayerRoomWatchService.start(); + mpWatchService.start(); discordBridgeService.start(); LOG.info("Starting bot connection loop..."); @@ -239,7 +239,7 @@ public void stop() { thread.interrupt(); } watchService.close(); - multiplayerRoomWatchService.close(); + mpWatchService.close(); discordBridgeService.close(); tokenManager.close(); } @@ -312,7 +312,7 @@ public void requestStop() { RealtimeServiceInterruptionNotifier.NotificationResult result = interruptionNotifier.notifyGroups( watchService.activeTransientGroupIds(), rankGuessGameService.activeGroupIds(), - multiplayerRoomWatchService.activeGroupIds(), + mpWatchService.activeGroupIds(), agentService.activeGroupIds() ); if (result.failedGroups() == 0) { diff --git a/src/main/java/xyz/zcraft/seira/command/handler/MultiplayerRoomWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/MultiplayerRoomWatchCommandHandler.java index cf161988..a6a28768 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/MultiplayerRoomWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/MultiplayerRoomWatchCommandHandler.java @@ -9,8 +9,8 @@ import xyz.zcraft.seira.command.ResolutionException; import xyz.zcraft.seira.command.TaskCoordinator; import xyz.zcraft.seira.db.UserDataStore; -import xyz.zcraft.seira.watch.MultiplayerRoomVersion; -import xyz.zcraft.seira.watch.MultiplayerRoomWatchService; +import xyz.zcraft.seira.watch.MPVersion; +import xyz.zcraft.seira.watch.MPWatchService; import xyz.zcraft.seira.watch.RoomWatchView; import java.util.Locale; @@ -32,11 +32,11 @@ public final class MultiplayerRoomWatchCommandHandler { ); private final TaskCoordinator taskCoordinator; - private final MultiplayerRoomWatchService watchService; + private final MPWatchService watchService; public MultiplayerRoomWatchCommandHandler( TaskCoordinator taskCoordinator, - MultiplayerRoomWatchService watchService + MPWatchService watchService ) { this.taskCoordinator = Objects.requireNonNull(taskCoordinator); this.watchService = Objects.requireNonNull(watchService); @@ -48,31 +48,31 @@ static RoomTarget parseRoomTarget(String value, String explicitVersion) { } String normalized = value.trim(); String numeric = normalized; - MultiplayerRoomVersion inferredVersion = null; + MPVersion inferredVersion = null; Matcher lazerMatcher = LAZER_ROOM_URL.matcher(normalized); Matcher stableMatcher = STABLE_ROOM_URL.matcher(normalized); if (lazerMatcher.matches()) { numeric = lazerMatcher.group(1); - inferredVersion = MultiplayerRoomVersion.LAZER; + inferredVersion = MPVersion.LAZER; } else if (stableMatcher.matches()) { numeric = stableMatcher.group(1); - inferredVersion = MultiplayerRoomVersion.STABLE; + inferredVersion = MPVersion.STABLE; } else if (!normalized.matches("\\d+")) { return null; } - MultiplayerRoomVersion requestedVersion = explicitVersion == null + MPVersion requestedVersion = explicitVersion == null ? null - : MultiplayerRoomVersion.parse(explicitVersion); + : MPVersion.parse(explicitVersion); if (explicitVersion != null && requestedVersion == null) { return null; } if (inferredVersion != null && requestedVersion != null && inferredVersion != requestedVersion) { return null; } - MultiplayerRoomVersion version = inferredVersion != null + MPVersion version = inferredVersion != null ? inferredVersion - : requestedVersion == null ? MultiplayerRoomVersion.LAZER : requestedVersion; + : requestedVersion == null ? MPVersion.LAZER : requestedVersion; try { long roomId = Long.parseLong(numeric); return roomId > 0 ? new RoomTarget(roomId, version) : null; @@ -123,7 +123,7 @@ private void handleStart(Context ctx, int argumentOffset) { return; } final Response multiplayerRoom = APIHelper.getMultiplayerRoom(osuToken.accessToken()); - target = new RoomTarget(multiplayerRoom.getContent().getId(), MultiplayerRoomVersion.LAZER); + target = new RoomTarget(multiplayerRoom.getContent().getId(), MPVersion.LAZER); } else { String version = startArgumentCount == 2 ? ctx.argument(argumentOffset + 1) : null; target = parseRoomTarget(ctx.argument(argumentOffset), version); @@ -146,8 +146,7 @@ private void handleStart(Context ctx, int argumentOffset) { ctx.groupId(), ctx.senderUserId(), target.version(), target.roomId() ); ctx.sendReply(PendingMessage.ofMarkdownRaw( - at(ctx) + "已开始监视 `" + formatRoom(view) + "` 。" - + "之后完成的每张图都会自动推送结果。" + at(ctx) + "已开始监视 `" + formatRoom(view) + "` 喵。" )); } catch (IllegalArgumentException | IllegalStateException e) { throw new ResolutionException(e.getMessage()); @@ -184,6 +183,6 @@ private void handleStatus(Context ctx) { : "你当前正在监视" + formatRoom(view) + "。"))); } - record RoomTarget(long roomId, MultiplayerRoomVersion version) { + record RoomTarget(long roomId, MPVersion version) { } } diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index 328a9052..22085603 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -28,7 +28,7 @@ import xyz.zcraft.seira.util.AdminRegistry; import xyz.zcraft.seira.util.NoticesHelper; import xyz.zcraft.seira.util.OsuAuthHelper; -import xyz.zcraft.seira.watch.MultiplayerRoomWatchService; +import xyz.zcraft.seira.watch.MPWatchService; import xyz.zcraft.seira.watch.ScoreWatchService; import java.util.List; @@ -36,7 +36,6 @@ import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; @@ -60,7 +59,7 @@ public class Router { public Router( MessageSender messageSender, Supplier configSupplier, AdminRegistry admins, - BindingService bindingService, ScoreWatchService watchService, MultiplayerRoomWatchService multiplayerRoomWatchService, + BindingService bindingService, ScoreWatchService watchService, MPWatchService mpWatchService, DiscordBridgeService discordBridgeService, RankGuessGameService rankGuessGameService, Executor commandExecutor, Runnable commandMetric, Function imageUploader, Supplier selfSupplier, AgentService agentService, Function botStateGetter @@ -100,7 +99,7 @@ public Router( SpecificScoreWatchCommandHandler specificScoreWatchCommands = new SpecificScoreWatchCommandHandler(taskCoordinator, watchService); MultiplayerRoomWatchCommandHandler multiplayerRoomWatchCommands = - new MultiplayerRoomWatchCommandHandler(taskCoordinator, multiplayerRoomWatchService); + new MultiplayerRoomWatchCommandHandler(taskCoordinator, mpWatchService); DcsCommandHandler dcsCommands = new DcsCommandHandler(discordBridgeService); RankGuessCommandHandler rankGuessCommands = new RankGuessCommandHandler( taskCoordinator, replyFactory, rankGuessGameService, resolver, admins::isAdmin, this::getAvatar, imageUploader diff --git a/src/main/java/xyz/zcraft/seira/watch/MPNotifier.java b/src/main/java/xyz/zcraft/seira/watch/MPNotifier.java new file mode 100644 index 00000000..a2b38d2f --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/watch/MPNotifier.java @@ -0,0 +1,36 @@ +package xyz.zcraft.seira.watch; + +import xyz.zcraft.seira.bot.MessageSender; +import xyz.zcraft.seira.data.UploadedImage; + +import java.util.Objects; + +public final class MPNotifier { + private final MessageSender messageSender; + + public MPNotifier(MessageSender messageSender) { + this.messageSender = Objects.requireNonNull(messageSender); + } + + public boolean sendResult(MPWatchService.WatchEntry watch, String groupId, byte[] imageBytes) { + final UploadedImage uploadedImage = messageSender.uploadImageToCos(imageBytes); + + if (uploadedImage == null) return false; + + return messageSender.sendGroupMarkdown( + groupId, + """ + %s + > 多人房间监视 - %s + > %s - %s + """.formatted(uploadedImage.toMarkdown(), watch.roomName(), watch.version().value(), watch.roomId()).trim() + ) != null; + } + + public boolean sendRoomEnded(String groupId, RoomWatchSnapshot snapshot) { + return messageSender.sendGroupText( + groupId, + "多人房间“" + snapshot.roomName() + "” (#" + snapshot.roomId() + ") 已结束,监视已自动停止。" + ) != null; + } +} diff --git a/src/main/java/xyz/zcraft/seira/watch/MultiplayerRoomVersion.java b/src/main/java/xyz/zcraft/seira/watch/MPVersion.java similarity index 76% rename from src/main/java/xyz/zcraft/seira/watch/MultiplayerRoomVersion.java rename to src/main/java/xyz/zcraft/seira/watch/MPVersion.java index 0ab2ac8f..42db4c1c 100644 --- a/src/main/java/xyz/zcraft/seira/watch/MultiplayerRoomVersion.java +++ b/src/main/java/xyz/zcraft/seira/watch/MPVersion.java @@ -2,17 +2,17 @@ import java.util.Locale; -public enum MultiplayerRoomVersion { +public enum MPVersion { LAZER("lazer"), STABLE("stable"); private final String value; - MultiplayerRoomVersion(String value) { + MPVersion(String value) { this.value = value; } - public static MultiplayerRoomVersion parse(String value) { + public static MPVersion parse(String value) { if (value == null) { return null; } diff --git a/src/main/java/xyz/zcraft/seira/watch/OstellaMultiplayerRoomWatchApi.java b/src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java similarity index 87% rename from src/main/java/xyz/zcraft/seira/watch/OstellaMultiplayerRoomWatchApi.java rename to src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java index e9eb6a48..66cb83ce 100644 --- a/src/main/java/xyz/zcraft/seira/watch/OstellaMultiplayerRoomWatchApi.java +++ b/src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java @@ -12,25 +12,25 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; -public final class OstellaMultiplayerRoomWatchApi implements MultiplayerRoomWatchApi { +public final class MPWatchApi { private final String endpoint; private final String serviceToken; private final HttpClient client; private final Gson gson; - public OstellaMultiplayerRoomWatchApi(String endpoint) { + public MPWatchApi(String endpoint) { this(endpoint, null); } - public OstellaMultiplayerRoomWatchApi(String endpoint, String serviceToken) { + public MPWatchApi(String endpoint, String serviceToken) { this(endpoint, serviceToken, HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(), new Gson()); } - OstellaMultiplayerRoomWatchApi(String endpoint, HttpClient client, Gson gson) { + MPWatchApi(String endpoint, HttpClient client, Gson gson) { this(endpoint, null, client, gson); } - OstellaMultiplayerRoomWatchApi(String endpoint, String serviceToken, HttpClient client, Gson gson) { + MPWatchApi(String endpoint, String serviceToken, HttpClient client, Gson gson) { this.endpoint = endpoint.endsWith("/") ? endpoint.substring(0, endpoint.length() - 1) : endpoint; this.serviceToken = serviceToken; this.client = client; @@ -71,15 +71,14 @@ private static long requirePositive(long value, String name) { return value; } - private static MultiplayerRoomVersion requireVersion(MultiplayerRoomVersion version) { + private static MPVersion requireVersion(MPVersion version) { if (version == null) { throw new IllegalArgumentException("version is required"); } return version; } - @Override - public RoomWatchSnapshot getSnapshot(MultiplayerRoomVersion version, long roomId) { + public RoomWatchSnapshot getSnapshot(MPVersion version, long roomId) { HttpResponse response = get( "/multiplayer/rooms/" + requirePositive(roomId, "roomId") + "/watch?version=" + requireVersion(version).value(), @@ -98,8 +97,7 @@ public RoomWatchSnapshot getSnapshot(MultiplayerRoomVersion version, long roomId return snapshot; } - @Override - public byte[] renderResult(MultiplayerRoomVersion version, long roomId, long playlistItemId) { + public byte[] renderResult(MPVersion version, long roomId, long playlistItemId) { HttpResponse response = get( "/multiplayer/rooms/" + requirePositive(roomId, "roomId") + "/playlist/" + requirePositive(playlistItemId, "playlistItemId") + "/result?version=" diff --git a/src/main/java/xyz/zcraft/seira/watch/MultiplayerRoomWatchService.java b/src/main/java/xyz/zcraft/seira/watch/MPWatchService.java similarity index 89% rename from src/main/java/xyz/zcraft/seira/watch/MultiplayerRoomWatchService.java rename to src/main/java/xyz/zcraft/seira/watch/MPWatchService.java index 27c2c466..0bd47914 100644 --- a/src/main/java/xyz/zcraft/seira/watch/MultiplayerRoomWatchService.java +++ b/src/main/java/xyz/zcraft/seira/watch/MPWatchService.java @@ -10,22 +10,20 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -public final class MultiplayerRoomWatchService implements AutoCloseable { - private static final Logger LOG = LogManager.getLogger(MultiplayerRoomWatchService.class); +public final class MPWatchService implements AutoCloseable { + private static final Logger LOG = LogManager.getLogger(MPWatchService.class); private final Object lock = new Object(); private final Map> watchesByGroup = new LinkedHashMap<>(); - private final MultiplayerRoomWatchApi api; - private final MultiplayerRoomNotifier notifier; + private final MPWatchApi api; + private final MPNotifier notifier; private final Duration pollInterval; private final ScheduledExecutorService scheduler; private final AtomicBoolean started = new AtomicBoolean(); private final AtomicBoolean closed = new AtomicBoolean(); - public MultiplayerRoomWatchService( - MultiplayerRoomWatchApi api, - MultiplayerRoomNotifier notifier, - Duration pollInterval + public MPWatchService( + MPWatchApi api, MPNotifier notifier, Duration pollInterval ) { this.api = Objects.requireNonNull(api); this.notifier = Objects.requireNonNull(notifier); @@ -72,7 +70,7 @@ public void start() { public RoomWatchView watch( String groupId, String userId, - MultiplayerRoomVersion version, + MPVersion version, long roomId ) { requireIdentifier(groupId, "groupId"); @@ -124,7 +122,7 @@ public List stopAll(String groupId) { if (removed == null) { return List.of(); } - return removed.values().stream().map(MultiplayerRoomWatchService::view).toList(); + return removed.values().stream().map(MPWatchService::view).toList(); } } @@ -181,9 +179,7 @@ private void processRoom(RoomKey room, List watches) { } private boolean sendPendingResults( - WatchRef watch, - List completed, - Map rendered + WatchRef watch, List completed, Map rendered ) { for (CompletedRoomPlay play : completed) { if (wasSent(watch, play.playlistItemId())) { @@ -194,7 +190,7 @@ private boolean sendPendingResults( play.playlistItemId(), itemId -> api.renderResult(watch.entry().version, watch.entry().roomId, itemId) ); - if (!notifier.sendResult(watch.groupId(), image)) { + if (!notifier.sendResult(watch.entry(), watch.groupId(), image)) { LOG.warn( "Failed to send room {} playlist item {} to group {}", watch.entry().roomId, play.playlistItemId(), watch.groupId() @@ -290,13 +286,11 @@ public void close() { } } - private record WatchEntry(MultiplayerRoomVersion version, long roomId, String roomName, - Set sentPlaylistItemIds) { - private WatchEntry( - MultiplayerRoomVersion version, - long roomId, - String roomName, - Set sentPlaylistItemIds + public record WatchEntry( + MPVersion version, long roomId, String roomName, Set sentPlaylistItemIds + ) { + public WatchEntry( + MPVersion version, long roomId, String roomName, Set sentPlaylistItemIds ) { this.version = version; this.roomId = roomId; @@ -305,9 +299,9 @@ private WatchEntry( } } - private record WatchRef(String groupId, String userId, WatchEntry entry) { + public record WatchRef(String groupId, String userId, WatchEntry entry) { } - private record RoomKey(MultiplayerRoomVersion version, long roomId) { + public record RoomKey(MPVersion version, long roomId) { } } diff --git a/src/main/java/xyz/zcraft/seira/watch/MultiplayerRoomNotifier.java b/src/main/java/xyz/zcraft/seira/watch/MultiplayerRoomNotifier.java deleted file mode 100644 index 81394d5c..00000000 --- a/src/main/java/xyz/zcraft/seira/watch/MultiplayerRoomNotifier.java +++ /dev/null @@ -1,7 +0,0 @@ -package xyz.zcraft.seira.watch; - -public interface MultiplayerRoomNotifier { - boolean sendResult(String groupId, byte[] imageBytes); - - boolean sendRoomEnded(String groupId, RoomWatchSnapshot snapshot); -} diff --git a/src/main/java/xyz/zcraft/seira/watch/MultiplayerRoomWatchApi.java b/src/main/java/xyz/zcraft/seira/watch/MultiplayerRoomWatchApi.java deleted file mode 100644 index c724e1b2..00000000 --- a/src/main/java/xyz/zcraft/seira/watch/MultiplayerRoomWatchApi.java +++ /dev/null @@ -1,7 +0,0 @@ -package xyz.zcraft.seira.watch; - -public interface MultiplayerRoomWatchApi { - RoomWatchSnapshot getSnapshot(MultiplayerRoomVersion version, long roomId); - - byte[] renderResult(MultiplayerRoomVersion version, long roomId, long playlistItemId); -} diff --git a/src/main/java/xyz/zcraft/seira/watch/QqMultiplayerRoomNotifier.java b/src/main/java/xyz/zcraft/seira/watch/QqMultiplayerRoomNotifier.java deleted file mode 100644 index bd2f6639..00000000 --- a/src/main/java/xyz/zcraft/seira/watch/QqMultiplayerRoomNotifier.java +++ /dev/null @@ -1,42 +0,0 @@ -package xyz.zcraft.seira.watch; - -import xyz.zcraft.seira.bot.MessageSender; -import xyz.zcraft.seira.bot.data.FileInfo; -import xyz.zcraft.seira.bot.data.Message; -import xyz.zcraft.seira.bot.data.PendingMessage; - -import java.util.Base64; -import java.util.Objects; - -public final class QqMultiplayerRoomNotifier implements MultiplayerRoomNotifier { - private final MessageSender messageSender; - - public QqMultiplayerRoomNotifier(MessageSender messageSender) { - this.messageSender = Objects.requireNonNull(messageSender); - } - - @Override - public boolean sendResult(String groupId, byte[] imageBytes) { - FileInfo media = messageSender.uploadGroupMediaBase64( - groupId, - PendingMessage.FILE_TYPE_IMAGE, - Base64.getEncoder().encodeToString(imageBytes) - ); - if (media == null) { - return false; - } - - Message message = new Message(); - message.setMsgType(PendingMessage.MSG_TYPE_MEDIA); - message.setMedia(media); - return messageSender.sendGroupMessage(groupId, message) != null; - } - - @Override - public boolean sendRoomEnded(String groupId, RoomWatchSnapshot snapshot) { - return messageSender.sendGroupText( - groupId, - "多人房间“" + snapshot.roomName() + "” (#" + snapshot.roomId() + ") 已结束,监视已自动停止。" - ) != null; - } -} diff --git a/src/main/java/xyz/zcraft/seira/watch/RoomWatchView.java b/src/main/java/xyz/zcraft/seira/watch/RoomWatchView.java index bf906517..8168c6f8 100644 --- a/src/main/java/xyz/zcraft/seira/watch/RoomWatchView.java +++ b/src/main/java/xyz/zcraft/seira/watch/RoomWatchView.java @@ -1,4 +1,4 @@ package xyz.zcraft.seira.watch; -public record RoomWatchView(MultiplayerRoomVersion version, long roomId, String roomName) { +public record RoomWatchView(MPVersion version, long roomId, String roomName) { } From f2ddd2f2ac7fbf2ec99e24e58cbdecf4be4b330d Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 22:24:08 +0800 Subject: [PATCH 47/96] =?UTF-8?q?=E9=87=8D=E5=91=BD=E5=90=8D=20Multiplayer?= =?UTF-8?q?RoomWatchCommandHandler=20=E4=B8=BA=20MPWatchCommandHandler?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E5=BC=95=E7=94=A8?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=20HTTP=20=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E5=A4=B4=E4=BB=A5=E6=94=AF=E6=8C=81=20PNG=20=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...mWatchCommandHandler.java => MPWatchCommandHandler.java} | 4 ++-- src/main/java/xyz/zcraft/seira/command/route/Router.java | 6 +++--- src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/main/java/xyz/zcraft/seira/command/handler/{MultiplayerRoomWatchCommandHandler.java => MPWatchCommandHandler.java} (98%) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/MultiplayerRoomWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java similarity index 98% rename from src/main/java/xyz/zcraft/seira/command/handler/MultiplayerRoomWatchCommandHandler.java rename to src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java index a6a28768..818fd739 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/MultiplayerRoomWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java @@ -20,7 +20,7 @@ import static xyz.zcraft.seira.command.reply.ReplyFactory.at; -public final class MultiplayerRoomWatchCommandHandler { +public final class MPWatchCommandHandler { private static final String USAGE = "用法:/mpwatch [start] <房间ID> [stable|lazer];" + "/mpwatch [start] <房间链接>;/mpwatch stop [all];/mpwatch status"; @@ -34,7 +34,7 @@ public final class MultiplayerRoomWatchCommandHandler { private final TaskCoordinator taskCoordinator; private final MPWatchService watchService; - public MultiplayerRoomWatchCommandHandler( + public MPWatchCommandHandler( TaskCoordinator taskCoordinator, MPWatchService watchService ) { diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index 22085603..f9adf00c 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -98,8 +98,8 @@ public Router( WatchCommandHandler watchCommands = new WatchCommandHandler(resolver, taskCoordinator, watchService, admins::isAdmin); SpecificScoreWatchCommandHandler specificScoreWatchCommands = new SpecificScoreWatchCommandHandler(taskCoordinator, watchService); - MultiplayerRoomWatchCommandHandler multiplayerRoomWatchCommands = - new MultiplayerRoomWatchCommandHandler(taskCoordinator, mpWatchService); + MPWatchCommandHandler multiplayerRoomWatchCommands = + new MPWatchCommandHandler(taskCoordinator, mpWatchService); DcsCommandHandler dcsCommands = new DcsCommandHandler(discordBridgeService); RankGuessCommandHandler rankGuessCommands = new RankGuessCommandHandler( taskCoordinator, replyFactory, rankGuessGameService, resolver, admins::isAdmin, this::getAvatar, imageUploader @@ -122,7 +122,7 @@ private static CommandRegistry createCommandRegistry( BeatmapCommandHandler beatmapCommands, SocialCommandHandler socialCommands, ReplayCommandHandler replayCommands, GeneralCommandHandler generalCommands, WatchCommandHandler watchCommands, SpecificScoreWatchCommandHandler specificScoreWatchCommands, - MultiplayerRoomWatchCommandHandler multiplayerRoomWatchCommands, DcsCommandHandler dcsCommands, + MPWatchCommandHandler multiplayerRoomWatchCommands, DcsCommandHandler dcsCommands, RankGuessCommandHandler rankGuessCommands, AiChatHandler aiChatHandler ) { return CommandRegistry.builder() diff --git a/src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java b/src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java index 66cb83ce..9502116a 100644 --- a/src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java +++ b/src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java @@ -115,7 +115,7 @@ private HttpResponse get(String path, HttpResponse.BodyHandler handler HttpRequest.Builder builder = HttpRequest.newBuilder() .uri(URI.create(endpoint + path)) .timeout(Duration.ofMinutes(2)) - .header("Accept", "application/json, image/*"); + .header("Accept", "image/png"); if (serviceToken != null && !serviceToken.isBlank()) { builder.header("Authorization", "Bearer " + serviceToken); } From 9c0aa553cf66c0843e3675971644fb587fdc4d57 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 22:57:15 +0800 Subject: [PATCH 48/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20MPNotifier=20?= =?UTF-8?q?=E5=92=8C=20MPWatchCommandHandler=20=E4=B8=AD=E7=9A=84=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=A0=BC=E5=BC=8F=EF=BC=8C=E8=B0=83=E6=95=B4=E6=88=BF?= =?UTF-8?q?=E9=97=B4=E7=9B=91=E8=A7=86=E4=BF=A1=E6=81=AF=E7=9A=84=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zcraft/seira/command/handler/MPWatchCommandHandler.java | 4 ++-- src/main/java/xyz/zcraft/seira/watch/MPNotifier.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java index 818fd739..e4938217 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java @@ -82,7 +82,7 @@ static RoomTarget parseRoomTarget(String value, String explicitVersion) { } private static String formatRoom(RoomWatchView view) { - return view.version().value() + " 多人房间“" + view.roomName() + "” (#" + view.roomId() + ")"; + return view.version().value() + " - " + view.roomId() + " `" + view.roomName() + "`"; } private static void usage(Context ctx) { @@ -146,7 +146,7 @@ private void handleStart(Context ctx, int argumentOffset) { ctx.groupId(), ctx.senderUserId(), target.version(), target.roomId() ); ctx.sendReply(PendingMessage.ofMarkdownRaw( - at(ctx) + "已开始监视 `" + formatRoom(view) + "` 喵。" + at(ctx) + "已开始监视" + formatRoom(view) + "喵。" )); } catch (IllegalArgumentException | IllegalStateException e) { throw new ResolutionException(e.getMessage()); diff --git a/src/main/java/xyz/zcraft/seira/watch/MPNotifier.java b/src/main/java/xyz/zcraft/seira/watch/MPNotifier.java index a2b38d2f..0bc4543b 100644 --- a/src/main/java/xyz/zcraft/seira/watch/MPNotifier.java +++ b/src/main/java/xyz/zcraft/seira/watch/MPNotifier.java @@ -21,8 +21,8 @@ public boolean sendResult(MPWatchService.WatchEntry watch, String groupId, byte[ groupId, """ %s - > 多人房间监视 - %s - > %s - %s + > __%s__ + > %s mp - %s """.formatted(uploadedImage.toMarkdown(), watch.roomName(), watch.version().value(), watch.roomId()).trim() ) != null; } From d80cd759b0d146425d33d9c806110ac9d08a5d0f Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 23:05:47 +0800 Subject: [PATCH 49/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20MPWatchCommandHandle?= =?UTF-8?q?r=20=E5=92=8C=20MPWatchService=20=E4=B8=AD=E7=9A=84=E6=88=BF?= =?UTF-8?q?=E9=97=B4=E7=9B=91=E8=A7=86=E9=80=BB=E8=BE=91=EF=BC=8C=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E7=89=88=E6=9C=AC=E5=A4=84=E7=90=86=E5=92=8C=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E6=8D=95=E8=8E=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/MPWatchCommandHandler.java | 6 ++-- .../zcraft/seira/watch/MPWatchService.java | 28 +++++++++++++------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java index e4938217..4e6d2340 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java @@ -70,9 +70,7 @@ static RoomTarget parseRoomTarget(String value, String explicitVersion) { if (inferredVersion != null && requestedVersion != null && inferredVersion != requestedVersion) { return null; } - MPVersion version = inferredVersion != null - ? inferredVersion - : requestedVersion == null ? MPVersion.LAZER : requestedVersion; + MPVersion version = (inferredVersion != null ? inferredVersion : requestedVersion); try { long roomId = Long.parseLong(numeric); return roomId > 0 ? new RoomTarget(roomId, version) : null; @@ -134,7 +132,7 @@ private void handleStart(Context ctx, int argumentOffset) { return; } - try (var timing = taskCoordinator.beginRequest(ctx, "Start Multiplayer Room Watch")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Start Multiplayer Room Watch")) { if (!ctx.sendMessage(PendingMessage.ofString("正在尝试启动多人房间监视……")).success()) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "由于缺少主动消息权限,无法启动监视!权限配置请见:https://docs.seira.top/overview/use.html#extra-permission" diff --git a/src/main/java/xyz/zcraft/seira/watch/MPWatchService.java b/src/main/java/xyz/zcraft/seira/watch/MPWatchService.java index 0bd47914..561f982b 100644 --- a/src/main/java/xyz/zcraft/seira/watch/MPWatchService.java +++ b/src/main/java/xyz/zcraft/seira/watch/MPWatchService.java @@ -67,23 +67,33 @@ public void start() { ); } - public RoomWatchView watch( - String groupId, - String userId, - MPVersion version, - long roomId - ) { + public RoomWatchView watch(String groupId, String userId, MPVersion version, long roomId) { requireIdentifier(groupId, "groupId"); requireIdentifier(userId, "userId"); - Objects.requireNonNull(version); + if (roomId <= 0) { throw new IllegalArgumentException("房间 ID 必须为正整数。"); } - RoomKey room = new RoomKey(version, roomId); + RoomKey room; + RoomWatchSnapshot snapshot; + + if (version == null) { + try { + version = MPVersion.STABLE; + snapshot = api.getSnapshot(MPVersion.STABLE, roomId); + } catch (IllegalStateException e) { + version = MPVersion.LAZER; + snapshot = api.getSnapshot(MPVersion.LAZER, roomId); + } + } else { + snapshot = api.getSnapshot(version, roomId); + } + + room = new RoomKey(version, roomId); synchronized (lock) { ensureRoomAvailable(groupId, userId, room); } - RoomWatchSnapshot snapshot = api.getSnapshot(version, roomId); + if (!snapshot.active()) { throw new IllegalStateException("该多人房间已经结束,无法开始监视。"); } From f628828cdb0a3435bfde323898507eb02c3ae696 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 23:47:32 +0800 Subject: [PATCH 50/96] =?UTF-8?q?=E9=87=8D=E5=91=BD=E5=90=8D=20APIHelper?= =?UTF-8?q?=20=E4=B8=BA=20ApiHelper=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=BC=95=E7=94=A8=EF=BC=8C=E6=96=B0=E5=A2=9E=20RomAIA?= =?UTF-8?q?pi=20=E5=92=8C=20RomAIMatch=20=E7=B1=BB=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20RomAI=20=E7=9B=91=E8=A7=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + .../api/{APIHelper.java => ApiHelper.java} | 2 +- .../java/xyz/zcraft/seira/api/RomAIApi.java | 59 +++++++++++ .../xyz/zcraft/seira/api/data/RomAIMatch.java | 92 ++++++++++++++++ .../seira/command/AttachmentHandler.java | 4 +- .../seira/command/ReplayResultStore.java | 8 +- .../zcraft/seira/command/TaskCoordinator.java | 13 +-- .../handler/BeatmapCommandHandler.java | 90 ++++++++-------- .../handler/GeneralCommandHandler.java | 10 +- .../handler/MPWatchCommandHandler.java | 79 +++++++++++++- .../handler/RankGuessCommandHandler.java | 19 ++-- .../command/handler/ReplayCommandHandler.java | 42 ++++---- .../command/handler/ScoreCommandHandler.java | 100 +++++++++--------- .../command/handler/SocialCommandHandler.java | 38 +++---- .../command/handler/WatchCommandHandler.java | 8 +- .../seira/command/reply/ReplyFactory.java | 10 +- .../seira/command/route/DebugRoutes.java | 8 +- .../zcraft/seira/command/route/Router.java | 3 +- 18 files changed, 405 insertions(+), 181 deletions(-) rename src/main/java/xyz/zcraft/seira/api/{APIHelper.java => ApiHelper.java} (99%) create mode 100644 src/main/java/xyz/zcraft/seira/api/RomAIApi.java create mode 100644 src/main/java/xyz/zcraft/seira/api/data/RomAIMatch.java diff --git a/README.md b/README.md index bd863bdc..db7f7a73 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ Seira正在活跃开发中,在使用的过程中可能会有一些Bug,也会 | `/mp` | `/mp` | 多人房间列表 | | `/watch` | `/watch add/del/list [目标]` | 添加/删除/列出监视任务 | | `/mpwatch` | `/mpwatch start/stop/status [目标]` | 按群成员添加、停止或查看多人房间监视;`stop all` 可停止本群全部监视 | +| `/romai` | `/romai [目标]` | 开始监视自己或目标正在进行的RomAI比赛 | | `/wx` | `/wx start <谱面ID列表>` / `/wx stop` | 监视指定玩家在指定谱面取得的成绩,重启后自动恢复 | | `/dcs` | `/dcs start .` / `/dcs stop` | 开启或解除当前 QQ 群与 Discord 频道的双向消息同步 | | `/stat` | `/stat` | 服务状态和统计信息文本 | diff --git a/src/main/java/xyz/zcraft/seira/api/APIHelper.java b/src/main/java/xyz/zcraft/seira/api/ApiHelper.java similarity index 99% rename from src/main/java/xyz/zcraft/seira/api/APIHelper.java rename to src/main/java/xyz/zcraft/seira/api/ApiHelper.java index ab863d0c..76798886 100644 --- a/src/main/java/xyz/zcraft/seira/api/APIHelper.java +++ b/src/main/java/xyz/zcraft/seira/api/ApiHelper.java @@ -25,7 +25,7 @@ import java.util.List; import java.util.Map; -public class APIHelper { +public class ApiHelper { private static final String OSU_AUTHORIZATION_HEADER = "X-Osu-Authorization"; private static final String ENDPOINT; private static final String TOKEN; diff --git a/src/main/java/xyz/zcraft/seira/api/RomAIApi.java b/src/main/java/xyz/zcraft/seira/api/RomAIApi.java new file mode 100644 index 00000000..87ad31be --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/api/RomAIApi.java @@ -0,0 +1,59 @@ +package xyz.zcraft.seira.api; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import xyz.zcraft.seira.api.data.RomAIMatch; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.LinkedList; +import java.util.List; + +public class RomAIApi { + private static final Gson GSON = new Gson(); + private static final Logger LOG = LogManager.getLogger(RomAIApi.class); + + private static final HttpClient CLIENT = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(20)) + .build(); + + public static List getActiveMatches() { + try { + final HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://rom-ai-site.vercel.app/api/active-matches")) + .GET() + .build(); + + final HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + + final JsonArray arr = JsonParser.parseString(response.body()).getAsJsonArray(); + + List result = new LinkedList<>(); + + for (JsonElement jsonElement : arr) { + result.add(GSON.fromJson(jsonElement, RomAIMatch.class)); + } + + return result; + } catch (Exception e) { + LOG.error("Error occurred while fetching active matches", e); + throw new RuntimeException("Error occurred while fetching active matches", e); + } + } + + public static RomAIMatch getMatchFor(String username) { + final List activeMatches = getActiveMatches(); + + return activeMatches.stream() + .filter(m -> m.players().contains(username)) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/xyz/zcraft/seira/api/data/RomAIMatch.java b/src/main/java/xyz/zcraft/seira/api/data/RomAIMatch.java new file mode 100644 index 00000000..a9c368a2 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/api/data/RomAIMatch.java @@ -0,0 +1,92 @@ +package xyz.zcraft.seira.api.data; + +import java.util.List; +import java.util.Map; + +/* +{ + "_id": "example", + "osuUser1": null, + "osuUser2": null, + "players": ["example","example","example","example","example","example"], + "teams": { + "teamA": ["example","example","example"], + "teamB": ["example","example","example"] + }, + "tournament": null, + "selectedPool": null, + "customBO": 9, + "customELO": 1335, + "interactionIds": ["1551915321242239126"], + "discordChannelId": "1295392984787811380", + "isMatchmaking": false, + "lobbyId": "121100588", + "matchState": "Picking", + "score": [2,3], + "bans": ["dt2","hd2","dt1","nm6"], + "picks": [ + { + "map": 4722846, + "mod": "nm2", + "scores": [733676,342836] + } + ], + "poolOptions": [ + "nm4","nm5","hd1","hd3","hr1","hr2","hr4","dt3","dt4" + ], + "availablePools": [ + { + "maps": { + "noMod": [4168602,3596841,5019445,5375902,4706834], + "hidden": [4836596,3505413,5074450], + "hardRock": [2676188,4641931,5445369], + "doubleTime": [5014531,2246430,3632103], + "freeMod": [], + "tieBreaker": 5384525 + }, + "_id": "696d20ff8f8d4a2449abe8d1", + "name": "example", + "elo": 1334, + "__v": 0 + } + ], + "poolInfoName": "Example Tournament", + "firstPick": "example", + "secondPick": "example", + "currentMapId": 5341088, + "currentMapMod": "hr", + "timerEndsAt": "2026-09-22T15:09:19.111Z", + "startedAt": "2026-09-22T14:36:47.445Z", + "__v": 40, + "playerData": [ + { + "osuUserName": "example", + "osuUserId": 12345678, + "elo": {"3v3": 1300,"1v1": 1300,"2v2": 1300}, + "country": "CN" + } + ], + "mode": "3v3", + "avgElo": 1251, + "isLeagueMatch": false + } + */ +public record RomAIMatch( + List players, + Teams teams, + String lobbyId, + List score, + Long currentMapId, + String mode, + List playerData +) { + + public record Teams(List teamA, List teamB){} + + public record PlayerData( + String osuUserName, + Long osuUserId, + Map elo, + String country + ){} +} diff --git a/src/main/java/xyz/zcraft/seira/command/AttachmentHandler.java b/src/main/java/xyz/zcraft/seira/command/AttachmentHandler.java index 932a6014..0d0b6998 100644 --- a/src/main/java/xyz/zcraft/seira/command/AttachmentHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/AttachmentHandler.java @@ -2,7 +2,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.seira.api.ApiHelper; import xyz.zcraft.seira.api.data.ReplayUploadInfo; import xyz.zcraft.seira.bot.data.Attachment; import xyz.zcraft.seira.bot.data.PendingMessage; @@ -74,7 +74,7 @@ public void handleAttachment(Attachment attachment, Consumer msg throw new IllegalArgumentException("Replay file exceeds the 512 KiB limit"); } - final ReplayUploadInfo replayUploadInfo = APIHelper.uploadReplay(bytes); + final ReplayUploadInfo replayUploadInfo = ApiHelper.uploadReplay(bytes); msgSender.accept(ReplyFactory.replayUploadMessage(replayUploadInfo)); } catch (InterruptedException e) { diff --git a/src/main/java/xyz/zcraft/seira/command/ReplayResultStore.java b/src/main/java/xyz/zcraft/seira/command/ReplayResultStore.java index de5e26c3..ec8cdc0e 100644 --- a/src/main/java/xyz/zcraft/seira/command/ReplayResultStore.java +++ b/src/main/java/xyz/zcraft/seira/command/ReplayResultStore.java @@ -1,13 +1,13 @@ package xyz.zcraft.seira.command; -import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.seira.api.ApiHelper; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; public final class ReplayResultStore { - private final ConcurrentMap results = new ConcurrentHashMap<>(); + private final ConcurrentMap results = new ConcurrentHashMap<>(); private static String requireTaskId(String taskId) { if (taskId == null || taskId.isBlank()) { @@ -16,11 +16,11 @@ private static String requireTaskId(String taskId) { return taskId; } - void put(String taskId, APIHelper.ReplayRenderResult result) { + void put(String taskId, ApiHelper.ReplayRenderResult result) { results.put(requireTaskId(taskId), Objects.requireNonNull(result)); } - public APIHelper.ReplayRenderResult get(String taskId) { + public ApiHelper.ReplayRenderResult get(String taskId) { return results.get(requireTaskId(taskId)); } diff --git a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java index 877595f8..db96b13f 100644 --- a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java +++ b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java @@ -1,10 +1,9 @@ package xyz.zcraft.seira.command; -import lombok.Setter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; -import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.seira.api.ApiHelper; import xyz.zcraft.seira.api.ApiRequestException; import xyz.zcraft.seira.api.ReplayRenderException; import xyz.zcraft.seira.api.data.Base64Bytes; @@ -24,8 +23,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiConsumer; -import java.util.function.Consumer; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; @@ -117,22 +114,22 @@ public QqUploadRequest createVideoUploadRequest(Context ctx) { return messageSender.createVideoUploadRequest(targetId, ctx.inGroup()); } - public APIHelper.ReplayRenderResult waitForReplay(APIHelper.ReplayTaskInfo taskInfo) { + public ApiHelper.ReplayRenderResult waitForReplay(ApiHelper.ReplayTaskInfo taskInfo) { return waitForReplay(taskInfo, -1); } - public APIHelper.ReplayRenderResult waitForReplay(APIHelper.ReplayTaskInfo taskInfo, long timeout) { + public ApiHelper.ReplayRenderResult waitForReplay(ApiHelper.ReplayTaskInfo taskInfo, long timeout) { if (taskInfo == null || taskInfo.taskId() == null || taskInfo.taskId().isBlank()) { throw new IllegalArgumentException("回放任务未返回有效请求ID,无法获取视频结果。"); } - APIHelper.ReplayRenderResult result = APIHelper.waitReplayVideo(taskInfo.taskId(), timeout); + ApiHelper.ReplayRenderResult result = ApiHelper.waitReplayVideo(taskInfo.taskId(), timeout); replayResults.put(taskInfo.taskId(), result); BotStat.incrementReplays(); return result; } - public PendingMessage replayVideoMessage(APIHelper.ReplayRenderResult result) { + public PendingMessage replayVideoMessage(ApiHelper.ReplayRenderResult result) { if (result == null) { return PendingMessage.ofString("回放视频生成失败,请稍后重试。"); } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java index 967cb5dc..8bccd198 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java @@ -1,7 +1,7 @@ package xyz.zcraft.seira.command.handler; import org.jline.utils.Log; -import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.seira.api.ApiHelper; import xyz.zcraft.seira.api.data.Response; import xyz.zcraft.seira.api.data.SearchQuery; import xyz.zcraft.seira.api.data.SearchResultItem; @@ -49,7 +49,7 @@ public BeatmapCommandHandler( public void handleDaily(Context ctx) { try (var timing = taskCoordinator.beginRequest(ctx, "Daily Challenge")) { - var daily = APIHelper.getDaily(); + var daily = ApiHelper.getDaily(); ctx.sendReply(PendingMessage.ofMarkdownRaw(daily)); } } @@ -73,19 +73,19 @@ public void handleM(Context ctx) { case SCORE -> scoreId = target.id(); case SET -> { beatmapsetId = Long.parseLong(target.id()); - beatmapId = APIHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + beatmapId = ApiHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } - if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); - var response = APIHelper.getBeatmapResponse(beatmapId, (ctx.argumentCount() > target.consumedArgs() ? ctx.argument(target.consumedArgs()) : null)); + var response = ApiHelper.getBeatmapResponse(beatmapId, (ctx.argumentCount() > target.consumedArgs() ? ctx.argument(target.consumedArgs()) : null)); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.beatmapMessage(ctx, response))); } @@ -110,19 +110,19 @@ public void handleBma(Context ctx) { case SCORE -> scoreId = target.id(); case SET -> { beatmapsetId = Long.parseLong(target.id()); - beatmapId = APIHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + beatmapId = ApiHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } - if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); - var response = APIHelper.getBeatmapAnalysisResponse(beatmapId, (ctx.argumentCount() > target.consumedArgs() ? ctx.argument(target.consumedArgs()) : null)); + var response = ApiHelper.getBeatmapAnalysisResponse(beatmapId, (ctx.argumentCount() > target.consumedArgs() ? ctx.argument(target.consumedArgs()) : null)); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.beatmapMessage(ctx, response))); } @@ -148,16 +148,16 @@ public void handleAp(Context ctx) { case SET -> beatmapsetId = Long.parseLong(target.id()); case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapsetId = APIHelper.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapsetId = ApiHelper.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } if (beatmapsetId == null) { - if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标喵"); - beatmapsetId = APIHelper.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); + beatmapsetId = ApiHelper.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); } history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(PendingMessage.ofVoiceUrl("https://b.ppy.sh/preview/" + beatmapsetId + ".mp3").doUpload(false)); @@ -205,24 +205,24 @@ public void handleBpv(Context ctx) { case SCORE -> scoreId = target.id(); case SET -> { beatmapsetId = Long.parseLong(target.id()); - beatmapId = APIHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + beatmapId = ApiHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } - if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); - var task = APIHelper.createBeatmapPreviewTask(beatmapId, mods, range, qqUpload); + var task = ApiHelper.createBeatmapPreviewTask(beatmapId, mods, range, qqUpload); history.remember(ctx, beatmapsetId, beatmapId, scoreId); videoRenderRecord.updateRenderTask(ctx.senderUserId(), task.taskId()); ctx.sendReply(replyFactory.replayMessage(ctx, task)); - APIHelper.ReplayRenderResult result; + ApiHelper.ReplayRenderResult result; try { result = taskCoordinator.waitForReplay(task); @@ -262,19 +262,19 @@ public void handleBgp(Context ctx) { case SCORE -> scoreId = target.id(); case SET -> { beatmapsetId = Long.parseLong(target.id()); - beatmapId = APIHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + beatmapId = ApiHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } - if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); - var response = APIHelper.getBeatmapBgResponse(beatmapId); + var response = ApiHelper.getBeatmapBgResponse(beatmapId); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.bgpMessage(ctx, response))); } @@ -301,18 +301,18 @@ public void handleDl(Context ctx) { case SET -> beatmapsetId = Long.parseLong(target.id()); case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapsetId = APIHelper.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapsetId = ApiHelper.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } if (beatmapsetId == null) { - if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标喵"); - beatmapsetId = APIHelper.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); + beatmapsetId = ApiHelper.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); } - var response = APIHelper.getLookupBeatmapsetResponse(beatmapsetId, accessTokenProvider.apply(ctx.senderUserId())); + var response = ApiHelper.getLookupBeatmapsetResponse(beatmapsetId, accessTokenProvider.apply(ctx.senderUserId())); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(replyFactory.dlMessage(ctx, response)); } @@ -339,18 +339,18 @@ public void handleMs(Context ctx) { case SET -> beatmapsetId = Long.parseLong(target.id()); case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapsetId = APIHelper.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapsetId = ApiHelper.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } if (beatmapsetId == null) { - if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标喵"); - beatmapsetId = APIHelper.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); + beatmapsetId = ApiHelper.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); } - var response = APIHelper.getBeatmapsetResponse(beatmapsetId); + var response = ApiHelper.getBeatmapsetResponse(beatmapsetId); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.beatmapsetMessage(ctx, response))); } @@ -363,7 +363,7 @@ public void handleSms(Context ctx) { return; } try (var timing = taskCoordinator.beginRequest(ctx, "Search Beatmapset")) { - Response> searchResponse = APIHelper.searchBeatmapSetResponse(searchQuery); + Response> searchResponse = ApiHelper.searchBeatmapSetResponse(searchQuery); ctx.sendReply(replyFactory.searchMessage(ctx, searchResponse, searchQuery)); } } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java index a3dfbd8e..22bc8b70 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java @@ -1,7 +1,7 @@ package xyz.zcraft.seira.command.handler; import xyz.zcraft.osu.model.Beatmapset; -import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.seira.api.ApiHelper; import xyz.zcraft.seira.bot.MessageSender; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; @@ -42,8 +42,8 @@ public void handleU(Context context) { String player = resolver.player(context.argumentCount() == 0 ? null : context.argument(0), context.senderUserId()); try (var _ = taskCoordinator.beginRequest(context, "User Info")) { - long uid = APIHelper.resolveUid(player); - var response = APIHelper.getUserInfoResponse(uid); + long uid = ApiHelper.resolveUid(player); + var response = ApiHelper.getUserInfoResponse(uid); var completion = replyFactory.userInfoMessage(context, response); context.sendReply(taskCoordinator.imageMessage(response, completion)); } @@ -57,7 +57,7 @@ public void handleLuck(Context context) { try (var _ = taskCoordinator.beginRequest(context, "Luck")) { DailyLuck.Luck luck = DailyLuck.getLuck(context.senderUserId()); - Beatmapset mapset = APIHelper.getBeatmapsetRaw(luck.dailyMapset()); + Beatmapset mapset = ApiHelper.getBeatmapsetRaw(luck.dailyMapset()); UploadedImage cover = messageSender.uploadImageToCos(mapset.getCovers().getCover()); context.sendReply(replyFactory.luckMessage(context, luck, mapset, cover)); } @@ -79,7 +79,7 @@ public void handleFaq(Context context) { } public void handleStat(Context context) { - context.sendReply(replyFactory.statusMessage(context, APIHelper.getServerStatus())); + context.sendReply(replyFactory.statusMessage(context, ApiHelper.getServerStatus())); } public void handleUnknown(Context context) { diff --git a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java index 4e6d2340..e0c85b46 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java @@ -1,13 +1,18 @@ package xyz.zcraft.seira.command.handler; import xyz.zcraft.osu.model.MultiplayerRoom; -import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.osu.model.User; +import xyz.zcraft.osu.model.UserExtended; +import xyz.zcraft.seira.api.ApiHelper; +import xyz.zcraft.seira.api.RomAIApi; import xyz.zcraft.seira.api.data.OsuToken; import xyz.zcraft.seira.api.data.Response; +import xyz.zcraft.seira.api.data.RomAIMatch; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.command.ResolutionException; import xyz.zcraft.seira.command.TaskCoordinator; +import xyz.zcraft.seira.command.parse.Resolver; import xyz.zcraft.seira.db.UserDataStore; import xyz.zcraft.seira.watch.MPVersion; import xyz.zcraft.seira.watch.MPWatchService; @@ -19,6 +24,7 @@ import java.util.regex.Pattern; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; +import static xyz.zcraft.seira.command.reply.ReplyFactory.ms; public final class MPWatchCommandHandler { private static final String USAGE = @@ -33,12 +39,15 @@ public final class MPWatchCommandHandler { private final TaskCoordinator taskCoordinator; private final MPWatchService watchService; + private final Resolver resolver; public MPWatchCommandHandler( TaskCoordinator taskCoordinator, + Resolver resolver, MPWatchService watchService ) { this.taskCoordinator = Objects.requireNonNull(taskCoordinator); + this.resolver = Objects.requireNonNull(resolver); this.watchService = Objects.requireNonNull(watchService); } @@ -106,6 +115,72 @@ public void handleMpWatch(Context ctx) { } } + public void handleRomAI(Context ctx) { + if (!ctx.inGroup()) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "/romai 仅支持群聊使用。")); + return; + } + + String username; + if (ctx.argumentCount() == 0) { + final Long boundUid = resolver.resolveBoundUid(ctx.senderUserId()); + if (boundUid == null) { + ctx.sendReply(at(ctx) + "由于未绑定,无法查找正在进行的 RomAI 比赛喵。"); + return; + } + + final UserExtended userRaw = ApiHelper.getUserRaw(boundUid); + + username = userRaw.getUsername(); + } else if (ctx.argumentCount() == 1) { + final String player = resolver.player(ctx.argument(0), null); + final User content = ApiHelper.lookupUser(player).getContent(); + username = content.getUsername(); + } else { + ctx.sendReply(at(ctx) + "用法:/romai [@user]"); + return; + } + + final RomAIMatch match = RomAIApi.getMatchFor(username); + + if (match == null) { + ctx.sendReply(at(ctx) + "对方不在打 RomAI 喵。"); + return; + } + + final PendingMessage msg = PendingMessage.ofMarkdownRaw( + at(ctx) + """ + 正在启动 RomAI 监视喵。 + > %s - %s + > Team A: %s + > Team B: %s + """.formatted( + match.lobbyId(), + match.mode(), + String.join(", ", match.teams().teamA()), + String.join(", ", match.teams().teamB()) + )); + + try (var _ = taskCoordinator.beginRequest(ctx, "Start RomAI Watch")) { + if (!ctx.sendMessage(msg).success()) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + + "由于缺少主动消息权限,无法启动监视!权限配置请见:" + )); + return; + } + try { + RoomWatchView view = watchService.watch( + ctx.groupId(), ctx.senderUserId(), MPVersion.STABLE, Long.parseLong(match.lobbyId()) + ); + ctx.sendReply(PendingMessage.ofMarkdownRaw( + at(ctx) + "已开始监视" + formatRoom(view) + "喵。" + )); + } catch (IllegalArgumentException | IllegalStateException e) { + throw new ResolutionException(e.getMessage()); + } + } + } + private void handleStart(Context ctx, int argumentOffset) { int startArgumentCount = ctx.argumentCount() - argumentOffset; if (startArgumentCount < 0 || startArgumentCount > 2) { @@ -120,7 +195,7 @@ private void handleStart(Context ctx, int argumentOffset) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "由于未绑定账户,无法获取当前房间,请手动提供ID~")); return; } - final Response multiplayerRoom = APIHelper.getMultiplayerRoom(osuToken.accessToken()); + final Response multiplayerRoom = ApiHelper.getMultiplayerRoom(osuToken.accessToken()); target = new RoomTarget(multiplayerRoom.getContent().getId(), MPVersion.LAZER); } else { String version = startArgumentCount == 2 ? ctx.argument(argumentOffset + 1) : null; diff --git a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java index 0ec6af7e..b85df461 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java @@ -2,7 +2,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.seira.api.ApiHelper; import xyz.zcraft.seira.api.data.RandomScore; import xyz.zcraft.seira.bot.data.MessageReference; import xyz.zcraft.seira.bot.data.PendingMessage; @@ -26,7 +26,6 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -268,8 +267,8 @@ public void handleRankGuess(Context ctx) { if (resolver.looksLikeMention(argument)) { String player = resolver.player(argument, ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - rank = APIHelper.getUserRank(uid); + long uid = ApiHelper.resolveUid(player); + rank = ApiHelper.getUserRank(uid); } else { rank = parseRank(argument); if (rank == null) { @@ -330,7 +329,7 @@ private void weight(Context ctx, boolean all, String target) { reply.append(at(ctx)).append("目前%s在本群权重为 `%.2f` (%s)\n".formatted(ref, probability.weight(), factors.isBlank() ? "基础权重" : factors)); reply.append("在本群 `%d` 名玩家中,%s被选中的概率为 `%.3f%%`\n".formatted(totalPlayer, ref, probability.chance() * 100)); - final String randomScoreWeight = APIHelper.getRandomScoreWeight(boundUid, games.generateWeights(ctx.groupId()), all); + final String randomScoreWeight = ApiHelper.getRandomScoreWeight(boundUid, games.generateWeights(ctx.groupId()), all); reply.append("%s的成绩当前抽选概率:\n>".formatted(ref)).append(randomScoreWeight).append("\n"); @@ -407,7 +406,7 @@ private void wishScore(Context ctx, int index) { try { scoreId = Long.parseLong( - APIHelper.lookupPlayerScore(boundUid, "bp", index, List.of(), null) + ApiHelper.lookupPlayerScore(boundUid, "bp", index, List.of(), null) ); } catch (Exception e) { LOG.error("Failed to lookup score id", e); @@ -451,9 +450,9 @@ private void start(Context ctx, boolean fromGroup) { ctx.sendReply(PendingMessage.ofMarkdownRaw("本群没有绑定的用户,无法开始游戏喵")); return; } - randomScore = APIHelper.getRandomScoreFromUsers(uids, games.generateWeights(ctx.groupId())); + randomScore = ApiHelper.getRandomScoreFromUsers(uids, games.generateWeights(ctx.groupId())); } else { - randomScore = APIHelper.getRandomScore(); + randomScore = ApiHelper.getRandomScore(); } Round round = Round.from(randomScore, activeMessageEnabled); @@ -474,7 +473,7 @@ private void start(Context ctx, boolean fromGroup) { ctx.sendReply(PendingMessage.ofMarkdownRaw(content)); - var renderTask = APIHelper.createObscuredReplayRenderTask( + var renderTask = ApiHelper.createObscuredReplayRenderTask( round.scoreId(), taskCoordinator.createVideoUploadRequest(ctx) ); @@ -488,7 +487,7 @@ private void start(Context ctx, boolean fromGroup) { TimeUnit.SECONDS ); - APIHelper.ReplayRenderResult replay = null; + ApiHelper.ReplayRenderResult replay = null; try { replay = taskCoordinator.waitForReplay(renderTask); } catch (Exception e) { diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java index 6951f083..87835df2 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java @@ -1,7 +1,7 @@ package xyz.zcraft.seira.command.handler; import org.jline.utils.Log; -import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.seira.api.ApiHelper; import xyz.zcraft.seira.api.data.VideoRenderRecord; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; @@ -86,30 +86,30 @@ public void handleR(Context ctx) { case SET -> { beatmapsetId = Long.parseLong(target.id()); String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } if (scoreId == null) { if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupBeatmapScore(beatmapId, uid, List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupBeatmapScore(beatmapId, uid, List.of(), null); } var upload = taskCoordinator.createVideoUploadRequest(ctx); - var task = APIHelper.createReplayRenderTask(scoreId, range, upload); + var task = ApiHelper.createReplayRenderTask(scoreId, range, upload); history.remember(ctx, beatmapsetId, beatmapId, scoreId); videoRenderRecord.updateRenderTask(ctx.senderUserId(), task.taskId()); ctx.sendReply(replyFactory.replayMessage(ctx, task)); - APIHelper.ReplayRenderResult result; + ApiHelper.ReplayRenderResult result; try { result = taskCoordinator.waitForReplay(task); @@ -183,7 +183,7 @@ public void handleRsc(Context ctx) { participants.add(token.trim()); } else if (resolver.looksLikeMention(token)) { String player = resolver.player(token, ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); + long uid = ApiHelper.resolveUid(player); participants.add("u" + uid); } else { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "追加ID列表包含非法值。")); @@ -204,17 +204,17 @@ public void handleRsc(Context ctx) { case SCORE -> scoreId = target.id(); case SET -> { beatmapsetId = Long.parseLong(target.id()); - beatmapId = APIHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + beatmapId = ApiHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } - if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); var upload = taskCoordinator.createVideoUploadRequest(ctx); String[] scoreTargets = participants.toArray(String[]::new); @@ -225,12 +225,12 @@ public void handleRsc(Context ctx) { scoreTargets = ids.toArray(String[]::new); } - var task = APIHelper.createReplayShowcaseTask(beatmapId, scoreTargets, upload); + var task = ApiHelper.createReplayShowcaseTask(beatmapId, scoreTargets, upload); history.remember(ctx, beatmapsetId, beatmapId, scoreId); videoRenderRecord.updateRenderTask(ctx.senderUserId(), task.taskId()); ctx.sendReply(replyFactory.replayMessage(ctx, task)); - APIHelper.ReplayRenderResult result; + ApiHelper.ReplayRenderResult result; try { result = taskCoordinator.waitForReplay(task); @@ -270,7 +270,7 @@ public void handleRstat(Context ctx) { jobId = ctx.args()[0]; } - APIHelper.ReplayRenderResult replayResult = replayResults.get(jobId); + ApiHelper.ReplayRenderResult replayResult = replayResults.get(jobId); if (replayResult != null) { PendingMessage video = replayResult.qqFile() != null ? PendingMessage.ofUploadedVideo(replayResult.qqFile(), replayResult.videoUrl()) @@ -281,7 +281,7 @@ public void handleRstat(Context ctx) { return; } - ctx.sendReply(replyFactory.replayStatMessage(ctx, jobId, APIHelper.getRenderStat(jobId))); + ctx.sendReply(replyFactory.replayStatMessage(ctx, jobId, ApiHelper.getRenderStat(jobId))); } public void handleRcancel(Context ctx) { @@ -309,7 +309,7 @@ public void handleRcancel(Context ctx) { } } - var result = APIHelper.cancelReplayRender(jobId); + var result = ApiHelper.cancelReplayRender(jobId); String status = Objects.toString(result.getStatus(), "unknown").toLowerCase(); String message = switch (status) { case "canceled" -> "回放渲染已取消。"; diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java index 5e430b08..204228f8 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java @@ -1,6 +1,6 @@ package xyz.zcraft.seira.command.handler; -import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.seira.api.ApiHelper; import xyz.zcraft.seira.api.data.MissData; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; @@ -78,9 +78,9 @@ public void handleBp(Context ctx) { if (ctx.args().length == 0) { String player = resolver.player(null, ctx.senderUserId()); try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - long uid = APIHelper.resolveUid(player); - String scoreId = APIHelper.lookupPlayerScore(uid, "bp", 1, List.of(), null); - var response = APIHelper.getScoreResponse(scoreId); + long uid = ApiHelper.resolveUid(player); + String scoreId = ApiHelper.lookupPlayerScore(uid, "bp", 1, List.of(), null); + var response = ApiHelper.getScoreResponse(scoreId); history.remember(ctx, null, null, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } @@ -103,8 +103,8 @@ public void handleBp(Context ctx) { var player = request.player(); var filters = request.filters(); try (var _ = taskCoordinator.beginRequest(ctx, "Best Scores")) { - long uid = APIHelper.resolveUid(player); - var response = APIHelper.getBoNResponse( + long uid = ApiHelper.resolveUid(player); + var response = ApiHelper.getBoNResponse( range.end(), range.start(), uid, @@ -118,9 +118,9 @@ public void handleRs(Context ctx, boolean includeFail) { if (ctx.args().length == 0) { String player = resolver.player(null, ctx.senderUserId()); try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - long uid = APIHelper.resolveUid(player); - String scoreId = APIHelper.lookupPlayerScore(uid, ctx.command(), 1, List.of(), null); - var response = APIHelper.getScoreResponse(scoreId); + long uid = ApiHelper.resolveUid(player); + String scoreId = ApiHelper.lookupPlayerScore(uid, ctx.command(), 1, List.of(), null); + var response = ApiHelper.getScoreResponse(scoreId); history.remember(ctx, null, null, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } @@ -143,8 +143,8 @@ public void handleRs(Context ctx, boolean includeFail) { var player = request.player(); var filters = request.filters(); try (var _ = taskCoordinator.beginRequest(ctx, "Recent Score")) { - long uid = APIHelper.resolveUid(player); - var response = APIHelper.getRecentResponse( + long uid = ApiHelper.resolveUid(player); + var response = ApiHelper.getRecentResponse( range.end(), range.start(), uid, @@ -164,8 +164,8 @@ public void handleTb(Context ctx) { String player = resolver.player(request.target(), ctx.senderUserId()); try (var _ = taskCoordinator.beginRequest(ctx, "Recent Best Scores")) { - long uid = APIHelper.resolveUid(player); - var response = APIHelper.getTodayBestResponse(uid, request.days()); + long uid = ApiHelper.resolveUid(player); + var response = ApiHelper.getTodayBestResponse(uid, request.days()); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.tbMessage(ctx, response))); } } @@ -188,9 +188,9 @@ private void handleFilteredSingleScore(Context ctx, String macroType) { if (targetUser == null) targetUser = resolver.player(null, ctx.senderUserId()); try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - long uid = APIHelper.resolveUid(targetUser); - String scoreId = APIHelper.lookupPlayerScore(uid, macroType, 1, filters.filters(), null); - var response = APIHelper.getScoreResponse(scoreId); + long uid = ApiHelper.resolveUid(targetUser); + String scoreId = ApiHelper.lookupPlayerScore(uid, macroType, 1, filters.filters(), null); + var response = ApiHelper.getScoreResponse(scoreId); history.remember(ctx, null, null, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } @@ -243,30 +243,30 @@ public void handleS(Context ctx) { case SET -> { beatmapsetId = Long.parseLong(target.id()); String player = userOverride == null ? resolver.player(null, ctx.senderUserId()) : userOverride; - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, filters, mod); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, filters, mod); selectedPlayerScore = true; } case RS, RP, BP -> { String player = userOverride == null ? resolver.player(target.player(), ctx.senderUserId()) : userOverride; - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), filters, mod); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), filters, mod); selectedPlayerScore = true; } - case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } if ((userOverride != null || mod != null) && scoreId != null && !selectedPlayerScore) { - if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); scoreId = null; } if (scoreId == null) { if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); String player = userOverride == null ? resolver.player(null, ctx.senderUserId()) : userOverride; - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupBeatmapScore(beatmapId, uid, filters, mod); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupBeatmapScore(beatmapId, uid, filters, mod); } - var response = APIHelper.getScoreResponse(scoreId); + var response = ApiHelper.getScoreResponse(scoreId); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } catch (Exception e) { @@ -321,22 +321,22 @@ public void handleSm(Context ctx) { case SCORE -> scoreId = target.id(); case SET -> { beatmapsetId = Long.parseLong(target.id()); - beatmapId = APIHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + beatmapId = ApiHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } - if (beatmapId == null && scoreId != null) beatmapId = APIHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); String player = userOverride == null ? resolver.player(null, ctx.senderUserId()) : userOverride; - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupBeatmapScore(beatmapId, uid, filters, mod); - var response = APIHelper.getScoreResponse(scoreId); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupBeatmapScore(beatmapId, uid, filters, mod); + var response = ApiHelper.getScoreResponse(scoreId); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } catch (Exception e) { @@ -366,24 +366,24 @@ public void handleSa(Context ctx) { case SET -> { beatmapsetId = Long.parseLong(target.id()); String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } if (scoreId == null) { if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupBeatmapScore(beatmapId, uid, List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupBeatmapScore(beatmapId, uid, List.of(), null); } - var response = APIHelper.getScoreAnalyzeResponse(scoreId); + var response = ApiHelper.getScoreAnalyzeResponse(scoreId); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreAnalyzeMessage(ctx, response))); } @@ -415,24 +415,24 @@ public void handleMa(Context ctx) { case SET -> { beatmapsetId = Long.parseLong(target.id()); String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } if (scoreId == null) { if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - scoreId = APIHelper.lookupBeatmapScore(beatmapId, uid, List.of(), null); + long uid = ApiHelper.resolveUid(player); + scoreId = ApiHelper.lookupBeatmapScore(beatmapId, uid, List.of(), null); } - var response = APIHelper.getScoreMissesResponse(scoreId); + var response = ApiHelper.getScoreMissesResponse(scoreId); history.remember(ctx, beatmapsetId, beatmapId, scoreId); List misses = response.getContent(); if (index == null && misses.size() != 1) { @@ -448,7 +448,7 @@ public void handleMa(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "Miss序号不在范围内喵(1~" + misses.size() + ")")); return; } - var image = APIHelper.getMissVisualizeResponse(scoreId, selectedIndex); + var image = ApiHelper.getMissVisualizeResponse(scoreId, selectedIndex); ctx.sendReply(taskCoordinator.imageMessage(image, replyFactory.missImageMessage(ctx, scoreId, selectedIndex, misses.size()))); } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java index 216f50a2..0d6fc6c7 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java @@ -2,7 +2,7 @@ import xyz.zcraft.osu.model.User; import xyz.zcraft.osu.model.UserExtended; -import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.seira.api.ApiHelper; import xyz.zcraft.seira.api.data.FriendEntry; import xyz.zcraft.seira.api.data.OsuToken; import xyz.zcraft.seira.api.data.Response; @@ -62,7 +62,7 @@ public void handleMp(Context ctx) { } try (var _ = taskCoordinator.beginRequest(ctx, "Multiplayer Room")) { - var response = APIHelper.getMultiplayerRoom(token.accessToken()); + var response = ApiHelper.getMultiplayerRoom(token.accessToken()); ctx.sendReply(replyFactory.mpMessage(ctx, response)); } } @@ -95,19 +95,19 @@ public void handleFriendStatus(Context ctx) { } String player = resolver.player(ctx.argument(0), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); + long uid = ApiHelper.resolveUid(player); final String mention = UserDataStore.findGroupOpenIdByUid(ctx.groupId(), uid) .map(ReplyFactory::at) .orElse(""); ctx.sendReply(PendingMessage.ofMarkdownRaw(mention) + ": [%d](%s)".formatted(uid, "https://osu.ppy.sh/users/" + uid)); - final UserExtended targetUser = APIHelper.getUserRaw(uid); + final UserExtended targetUser = ApiHelper.getUserRaw(uid); final String targetOsuAvatar = targetUser.getAvatarUrl(); boolean selfFollowed; final AtomicReference targetFollowed = new AtomicReference<>(); final OsuToken selfToken = authHelper.updateTokenAndGet(ctx.senderUserId()); - final var selfUser = APIHelper.getSelf(selfToken.accessToken()).getContent(); + final var selfUser = ApiHelper.getSelf(selfToken.accessToken()).getContent(); final String selfOsuAvatar = selfUser.getAvatarUrl(); if (targetUser.getId() == selfUser.getId()) { @@ -115,7 +115,7 @@ public void handleFriendStatus(Context ctx) { return; } - final List selfFollowedList = APIHelper.getFollowed(selfToken.accessToken()).getContent(); + final List selfFollowedList = ApiHelper.getFollowed(selfToken.accessToken()).getContent(); updateFriends(selfId, selfFollowedList); final Set users = new HashSet<>(selfFollowedList.stream().map(FriendEntry::user).toList()); @@ -136,7 +136,7 @@ public void handleFriendStatus(Context ctx) { final List targetFollowedList; final OsuToken target = authHelper.updateTokenAndGet(targetOpenId); if (target != null) { - targetFollowedList = APIHelper.getFollowed(target.accessToken()).getContent(); + targetFollowedList = ApiHelper.getFollowed(target.accessToken()).getContent(); targetFollowed.set(targetFollowedList.stream().anyMatch(e -> e.user().getId() == selfId)); users.addAll(targetFollowedList.stream().map(FriendEntry::user).toList()); } @@ -168,8 +168,8 @@ public void handleFriendList(Context ctx, boolean all) { } try (var _ = taskCoordinator.beginRequest(ctx, "Friend List")) { - final Response self = APIHelper.getSelf(token.accessToken()); - final Response> response = APIHelper.getFollowed(token.accessToken()); + final Response self = ApiHelper.getSelf(token.accessToken()); + final Response> response = ApiHelper.getFollowed(token.accessToken()); final List friendEntries = response.getContent(); final Predicate filter; @@ -269,7 +269,7 @@ public void handleLb(Context ctx) { } try (var _ = taskCoordinator.beginRequest(ctx, "Leaderboard")) { - var response = APIHelper.getLeaderboardResponse(groupBoundUids); + var response = ApiHelper.getLeaderboardResponse(groupBoundUids); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.lbMessage(ctx, response))); } return; @@ -281,7 +281,7 @@ public void handleLb(Context ctx) { } try (var _ = taskCoordinator.beginRequest(ctx, "Leaderboard")) { - var response = APIHelper.getLeaderboardResponse(List.of(uid)); + var response = ApiHelper.getLeaderboardResponse(List.of(uid)); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.lbMessage(ctx, response))); } } else if (ctx.args().length == 1 || ctx.args().length == 2) { @@ -323,17 +323,17 @@ public void handleLb(Context ctx) { try (var _ = taskCoordinator.beginRequest(ctx, "Map Leaderboard")) { long beatmapId = switch (target.kind()) { case ID, MAP -> Long.parseLong(target.id()); - case SCORE -> APIHelper.getScoreBeatmapId(target.id()); - case SET -> APIHelper.lookupBeatmapInSet(Long.parseLong(target.id()), target.index(), + case SCORE -> ApiHelper.getScoreBeatmapId(target.id()); + case SET -> ApiHelper.lookupBeatmapInSet(Long.parseLong(target.id()), target.index(), accessTokenProvider.apply(ctx.senderUserId())); case RS, RP, BP -> { - long uid = APIHelper.resolveUid(resolver.player(target.player(), ctx.senderUserId())); - yield APIHelper.lookupPlayerScoreBeatmap(uid, target.scoreList(), target.index(), accessTokenProvider.apply(ctx.senderUserId())); + long uid = ApiHelper.resolveUid(resolver.player(target.player(), ctx.senderUserId())); + yield ApiHelper.lookupPlayerScoreBeatmap(uid, target.scoreList(), target.index(), accessTokenProvider.apply(ctx.senderUserId())); } - case MP -> APIHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> throw new ResolutionException("请指定指令目标谱面喵"); }; - var response = APIHelper.getGroupLeaderboardResponse(beatmapId, uids); + var response = ApiHelper.getGroupLeaderboardResponse(beatmapId, uids); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.lbMessage(ctx, response))); } } else { @@ -347,8 +347,8 @@ public void handleSup(Context ctx) { return; } String player = resolver.player(ctx.argumentCount() == 0 ? null : ctx.argument(0), ctx.senderUserId()); - long uid = APIHelper.resolveUid(player); - final UserExtended user = APIHelper.getUserRaw(uid); + long uid = ApiHelper.resolveUid(player); + final UserExtended user = ApiHelper.getUserRaw(uid); final String openId = UserDataStore.findGroupOpenIdByUid(ctx.groupId(), user.getId()).orElse(null); ctx.sendReply(replyFactory.supMessage(ctx, user.getUsername(), openId, user.isSupporter(), user.getHasSupported(), user.getSupportLevel())); diff --git a/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java index 82186c12..f85aa5f8 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java @@ -1,7 +1,7 @@ package xyz.zcraft.seira.command.handler; import xyz.zcraft.osu.model.User; -import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.seira.api.ApiHelper; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.command.ResolutionException; @@ -133,7 +133,7 @@ private void handleAdd(Context ctx) { } Long userId = UserDataStore.findBoundUid(mentionedUser); if (userId == null) throw new ResolutionException("被@的用户还没有绑定玩家ID,请先让对方使用 /bind。"); - User user = APIHelper.getUsers(List.of(userId)).stream() + User user = ApiHelper.getUsers(List.of(userId)).stream() .filter(candidate -> candidate.getId() == userId) .findFirst().orElseThrow(() -> new ResolutionException("未找到指定的玩家。")); UserDataStore.storeUserInfo(user.getId(), user.getUsername()); @@ -191,8 +191,8 @@ private void handleDelete(Context ctx) { private WatchTarget lookupGroupPlayer(String groupId, String argument) { Long uid = resolver.parsePositiveLong(argument); - User user = uid == null ? APIHelper.lookupUser(argument).getContent() - : APIHelper.getUsers(List.of(uid)).stream() + User user = uid == null ? ApiHelper.lookupUser(argument).getContent() + : ApiHelper.getUsers(List.of(uid)).stream() .filter(candidate -> candidate.getId() == uid) .findFirst().orElseThrow(() -> new ResolutionException("未找到指定的玩家。")); String openId = UserDataStore.findGroupOpenIdByUid(groupId, user.getId()) diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index c2bb9f9d..363fd739 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -5,7 +5,7 @@ import com.google.gson.JsonObject; import org.jetbrains.annotations.NotNull; import xyz.zcraft.osu.model.*; -import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.seira.api.ApiHelper; import xyz.zcraft.seira.api.data.*; import xyz.zcraft.seira.bot.data.Button; import xyz.zcraft.seira.bot.data.PendingMessage; @@ -317,7 +317,7 @@ public PendingMessage lbMessage(Context ctx, Response response) { ); } - public PendingMessage replayMessage(Context ctx, APIHelper.ReplayTaskInfo taskInfo) { + public PendingMessage replayMessage(Context ctx, ApiHelper.ReplayTaskInfo taskInfo) { return PendingMessage.ofMarkdownRaw( Contents.replayTaskContent(ctx, taskInfo), buttons().replayProgressButtons(taskInfo.taskId(), ctx.senderUserId()) @@ -424,7 +424,7 @@ public PendingMessage scoreMissesMessage(Context ctx, Response> s ); } - public PendingMessage statusMessage(Context ctx, APIHelper.ServerStatus status) { + public PendingMessage statusMessage(Context ctx, ApiHelper.ServerStatus status) { return PendingMessage.ofMarkdownRaw( Contents.statContent(ctx, status), null ); @@ -464,7 +464,7 @@ public PendingMessage supMessage(Context ctx, String username, String openId, Bo } private static final class Contents { - static String replayTaskContent(Context ctx, APIHelper.ReplayTaskInfo taskInfo) { + static String replayTaskContent(Context ctx, ApiHelper.ReplayTaskInfo taskInfo) { StringBuilder sb = new StringBuilder(); sb.append(at(ctx)).append("回放生成请求已提交").append("\n"); @@ -720,7 +720,7 @@ public static String scoreMissesContent(Context ctx, Response> sc return sb.toString().trim(); } - public static String statContent(Context ctx, APIHelper.ServerStatus status) { + public static String statContent(Context ctx, ApiHelper.ServerStatus status) { String stat = at(ctx) + "\n" + "## 服务器状态\n" + "> 消息网关: ✅ 正常\n" + diff --git a/src/main/java/xyz/zcraft/seira/command/route/DebugRoutes.java b/src/main/java/xyz/zcraft/seira/command/route/DebugRoutes.java index d80febc3..a90d703c 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/DebugRoutes.java +++ b/src/main/java/xyz/zcraft/seira/command/route/DebugRoutes.java @@ -4,7 +4,7 @@ import org.apache.logging.log4j.Logger; import org.bouncycastle.util.encoders.Base64Encoder; import xyz.zcraft.osu.model.UserExtended; -import xyz.zcraft.seira.api.APIHelper; +import xyz.zcraft.seira.api.ApiHelper; import xyz.zcraft.seira.api.data.FriendEntry; import xyz.zcraft.seira.api.data.OsuToken; import xyz.zcraft.seira.api.data.Response; @@ -149,7 +149,7 @@ public void handleUpdateUserInfo(Context ctx) { return; } try (var timing = taskCoordinator.beginRequest(ctx, "Update All User Info")) { - var users = APIHelper.getUsers(allUsers); + var users = ApiHelper.getUsers(allUsers); for (var user : users) { UserDataStore.storeUserInfo(user.getId(), user.getUsername()); } @@ -167,8 +167,8 @@ public void handleGetAllFriends(Context ctx) { .map(authHelper::updateTokenAndGet) .map(OsuToken::accessToken) .forEach(accessToken -> { - final Response self = APIHelper.getSelf(accessToken); - final Response> response = APIHelper.getFollowed(accessToken); + final Response self = ApiHelper.getSelf(accessToken); + final Response> response = ApiHelper.getFollowed(accessToken); final List content = response.getContent(); final List ids = content.stream().map(e -> e.user().getId()).toList(); diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index f9adf00c..c4a08f4f 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -99,7 +99,7 @@ public Router( SpecificScoreWatchCommandHandler specificScoreWatchCommands = new SpecificScoreWatchCommandHandler(taskCoordinator, watchService); MPWatchCommandHandler multiplayerRoomWatchCommands = - new MPWatchCommandHandler(taskCoordinator, mpWatchService); + new MPWatchCommandHandler(taskCoordinator, resolver, mpWatchService); DcsCommandHandler dcsCommands = new DcsCommandHandler(discordBridgeService); RankGuessCommandHandler rankGuessCommands = new RankGuessCommandHandler( taskCoordinator, replyFactory, rankGuessGameService, resolver, admins::isAdmin, this::getAvatar, imageUploader @@ -166,6 +166,7 @@ private static CommandRegistry createCommandRegistry( .register(watchCommands::handleWatch, "watch") .register(specificScoreWatchCommands::handleWx, "wx") .register(multiplayerRoomWatchCommands::handleMpWatch, "mpwatch", "mpw") + .register(multiplayerRoomWatchCommands::handleRomAI, "romai") .register(dcsCommands::handleDcs, "dcs") .register(rankGuessCommands::handleRankGuess, "rg") .register(generalCommands::handleNotice, "notice") From 295b274cfb31c76b2d038eff5b554acf72234cbe Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 22 Sep 2026 23:55:29 +0800 Subject: [PATCH 51/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20RomAIApi=20=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=20HTTP=20=E5=AE=A2=E6=88=B7=E7=AB=AF=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=EF=BC=8C=E6=B7=BB=E5=8A=A0=E4=BB=A3=E7=90=86=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=BB=A5=E9=80=82=E5=BA=94=20DiscordProxyConfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/xyz/zcraft/seira/api/RomAIApi.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/api/RomAIApi.java b/src/main/java/xyz/zcraft/seira/api/RomAIApi.java index 87ad31be..0fe597e7 100644 --- a/src/main/java/xyz/zcraft/seira/api/RomAIApi.java +++ b/src/main/java/xyz/zcraft/seira/api/RomAIApi.java @@ -6,8 +6,12 @@ import com.google.gson.JsonParser; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import xyz.zcraft.seira.Seira; import xyz.zcraft.seira.api.data.RomAIMatch; +import xyz.zcraft.seira.config.DiscordProxyConfig; +import java.net.InetSocketAddress; +import java.net.ProxySelector; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -20,9 +24,17 @@ public class RomAIApi { private static final Gson GSON = new Gson(); private static final Logger LOG = LogManager.getLogger(RomAIApi.class); - private static final HttpClient CLIENT = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(20)) - .build(); + private static final HttpClient CLIENT; + + static { + var proxy = Seira.getConfig().discord().proxy(); + CLIENT = HttpClient.newBuilder() + .proxy(ProxySelector.of( + new InetSocketAddress(proxy.host(), proxy.port()) + )) + .connectTimeout(Duration.ofSeconds(20)) + .build(); + } public static List getActiveMatches() { try { From 055bad15b16c6c7e0213c3b66f28aef777e76115 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 00:04:10 +0800 Subject: [PATCH 52/96] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20ReplyChannel=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=20sendProactive=20=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E8=B0=83=E7=94=A8=EF=BC=8C=E7=A1=AE=E4=BF=9D=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=96=B9=E6=B3=95=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/command/ReplyChannel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/command/ReplyChannel.java b/src/main/java/xyz/zcraft/seira/command/ReplyChannel.java index 34744a02..761d1ea6 100644 --- a/src/main/java/xyz/zcraft/seira/command/ReplyChannel.java +++ b/src/main/java/xyz/zcraft/seira/command/ReplyChannel.java @@ -40,7 +40,7 @@ public synchronized SendResult sendReply(PendingMessage message, boolean ref) { } public synchronized SendResult sendProactive(PendingMessage message) { - return sendReply(message, false); + return sendProactive(message, false); } public synchronized SendResult sendProactive(PendingMessage message, boolean ref) { From 4bdbbc5a7590dee95033701f463c93d628c51363 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 00:15:06 +0800 Subject: [PATCH 53/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20Round=20=E7=B1=BB?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E5=9B=BE=E5=83=8F=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E8=B0=83=E6=95=B4=E9=A9=AC=E8=B5=9B=E5=85=8B?= =?UTF-8?q?=E5=92=8C=E6=A8=A1=E7=B3=8A=E6=95=88=E6=9E=9C=E7=9A=84=E5=8F=82?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/rankguess/data/Round.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java b/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java index 160e7161..73c45a86 100644 --- a/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java +++ b/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java @@ -276,8 +276,8 @@ public List getGroupHints(String qqAvatarUrl, Function getGroupHints(String qqAvatarUrl, Function Date: Wed, 23 Sep 2026 00:30:24 +0800 Subject: [PATCH 54/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20MPWatchCommandHandle?= =?UTF-8?q?r=20=E4=B8=AD=E7=9A=84=E7=94=A8=E6=88=B7=E6=9F=A5=E6=89=BE?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BD=BF=E7=94=A8=E6=9B=B4=E5=87=86?= =?UTF-8?q?=E7=A1=AE=E7=9A=84=E6=96=B9=E6=B3=95=E8=8E=B7=E5=8F=96=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java index e0c85b46..b54fe463 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java @@ -134,7 +134,7 @@ public void handleRomAI(Context ctx) { username = userRaw.getUsername(); } else if (ctx.argumentCount() == 1) { final String player = resolver.player(ctx.argument(0), null); - final User content = ApiHelper.lookupUser(player).getContent(); + final User content = ApiHelper.getUserRaw(ApiHelper.resolveUid(player)); username = content.getUsername(); } else { ctx.sendReply(at(ctx) + "用法:/romai [@user]"); From 5dbb3783dee21156d903fb006e9448bcf30460f8 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 00:32:51 +0800 Subject: [PATCH 55/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20MPWatchCommandHandle?= =?UTF-8?q?r=20=E4=B8=AD=E7=9A=84=E6=B6=88=E6=81=AF=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E8=BF=9B=E5=9B=A2=E9=98=9F=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E7=9A=84=E6=98=BE=E7=A4=BA=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/handler/MPWatchCommandHandler.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java index b54fe463..35bc4a24 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java @@ -148,17 +148,27 @@ public void handleRomAI(Context ctx) { return; } + String teamString = ""; + + if (match.teams() != null) { + teamString = """ + > Team A: %s + > Team B: %s + """.formatted( + String.join(", ", match.teams().teamA()), + String.join(", ", match.teams().teamB()) + ); + } + final PendingMessage msg = PendingMessage.ofMarkdownRaw( at(ctx) + """ 正在启动 RomAI 监视喵。 > %s - %s - > Team A: %s - > Team B: %s + %s """.formatted( match.lobbyId(), match.mode(), - String.join(", ", match.teams().teamA()), - String.join(", ", match.teams().teamB()) + teamString )); try (var _ = taskCoordinator.beginRequest(ctx, "Start RomAI Watch")) { From e711e4af50357e5e679087d9bf60040de1809d7e Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 00:33:15 +0800 Subject: [PATCH 56/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20MPWatchCommandHandle?= =?UTF-8?q?r=20=E4=B8=AD=E7=9A=84=E6=B6=88=E6=81=AF=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E8=BF=9B=E5=9B=A2=E9=98=9F=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E7=9A=84=E6=98=BE=E7=A4=BA=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/MPWatchCommandHandler.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java index 35bc4a24..c7b6e65b 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java @@ -24,7 +24,6 @@ import java.util.regex.Pattern; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; -import static xyz.zcraft.seira.command.reply.ReplyFactory.ms; public final class MPWatchCommandHandler { private static final String USAGE = @@ -152,24 +151,25 @@ public void handleRomAI(Context ctx) { if (match.teams() != null) { teamString = """ - > Team A: %s - > Team B: %s - """.formatted( + > Team A: %s + > Team B: %s + """.formatted( String.join(", ", match.teams().teamA()), String.join(", ", match.teams().teamB()) - ); + ); } final PendingMessage msg = PendingMessage.ofMarkdownRaw( at(ctx) + """ - 正在启动 RomAI 监视喵。 - > %s - %s - %s - """.formatted( - match.lobbyId(), - match.mode(), - teamString - )); + 正在启动 RomAI 监视喵。 + > %s - %s + %s + """.formatted( + match.lobbyId(), + match.mode(), + teamString + ).trim() + ); try (var _ = taskCoordinator.beginRequest(ctx, "Start RomAI Watch")) { if (!ctx.sendMessage(msg).success()) { From 4848ca5fabf56d4de4a48b4e0e803e5e5bc5ac9d Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 00:37:20 +0800 Subject: [PATCH 57/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20MPWatchCommandHandle?= =?UTF-8?q?r=20=E4=B8=AD=E7=9A=84=E6=B6=88=E6=81=AF=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E8=BF=9B=E6=AF=94=E8=B5=9B=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E7=9A=84=E6=98=BE=E7=A4=BA=E6=96=B9=E5=BC=8F=EF=BC=9B=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20WatchCommandHandler=20=E7=9A=84=E7=94=A8=E6=B3=95?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../seira/command/handler/MPWatchCommandHandler.java | 7 +++++-- .../zcraft/seira/command/handler/WatchCommandHandler.java | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java index c7b6e65b..4d9abaec 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java @@ -147,7 +147,7 @@ public void handleRomAI(Context ctx) { return; } - String teamString = ""; + String teamString; if (match.teams() != null) { teamString = """ @@ -155,8 +155,11 @@ public void handleRomAI(Context ctx) { > Team B: %s """.formatted( String.join(", ", match.teams().teamA()), - String.join(", ", match.teams().teamB()) + String.join(", ", match.teams().teamB() + ).trim() ); + } else { + teamString = "> `%s` vs `%s`".formatted(match.players().getFirst(), match.players().getLast()); } final PendingMessage msg = PendingMessage.ofMarkdownRaw( diff --git a/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java index f85aa5f8..ebc9241b 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java @@ -23,7 +23,7 @@ public final class WatchCommandHandler { private static final int DEFAULT_DURATION_MINUTES = 10; private static final int MAX_DURATION_MINUTES = 120; - private static final String USAGE = "用法:/watch add <玩家ID/用户名/@用户> [分钟];/watch del [玩家ID/用户名/@用户];/watch list"; + private static final String USAGE = "用法:/watch add <玩家ID/用户名/@用户> [分钟,1-120];/watch del [玩家ID/用户名/@用户];/watch list"; private final Resolver resolver; private final TaskCoordinator taskCoordinator; @@ -124,7 +124,7 @@ private void handleAdd(Context ctx) { } String targetArgument = ctx.argument(1); - try (var timing = taskCoordinator.beginRequest(ctx, "Add Score Watch")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Add Score Watch")) { String mentionedUser = resolver.extractMentionedUserId(targetArgument); WatchTarget target; if (mentionedUser != null) { From 314880fe648bb2f036203a773718d7d9980bfc01 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 00:54:38 +0800 Subject: [PATCH 58/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AiChatHandler=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E7=A9=BA=E5=86=85=E5=AE=B9=E7=9A=84=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index f5d49d0c..23a15470 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -152,6 +152,10 @@ private String generateVar(Context ctx, String input) { } public void recordHistory(String groupId, String userId, String rawContent) { + if (rawContent == null || rawContent.isBlank()) { + return; + } + recordHistory(groupId, userId, rawContent.trim(), List.of()); } From 916570694a3c8b978d8c90417edf5d42446b0ac3 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 10:05:32 +0800 Subject: [PATCH 59/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20TargetInput=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E7=8E=A9=E5=AE=B6=E5=88=86=E6=95=B0=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=20"rbp"=20=E7=9A=84=E5=A4=84=E7=90=86=E5=B9=B6=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zcraft/seira/command/parse/TargetInput.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/parse/TargetInput.java b/src/main/java/xyz/zcraft/seira/command/parse/TargetInput.java index 6d2fa2ab..c85fe675 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/TargetInput.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/TargetInput.java @@ -3,10 +3,12 @@ import xyz.zcraft.seira.command.ResolutionException; import java.util.Locale; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; import java.util.regex.Pattern; public record TargetInput(Kind kind, String id, long index, String player, int consumedArgs) { - private static final Pattern PLAYER_SCORE = Pattern.compile("(?i)^(rs|rp|bp|bo)(\\d+)?$"); + private static final Pattern PLAYER_SCORE = Pattern.compile("(?i)^(rs|rp|bp)(\\d+)?$"); private static final Pattern SET = Pattern.compile("^(\\d+)#(\\d+)$"); public enum Kind { MEMORY, ID, MAP, SET, SCORE, RS, RP, BP, MP } @@ -20,11 +22,13 @@ public static TargetInput read(String[] args) { int consumed = args.length >= 2 && PLAYER_SCORE.matcher(args[1]).matches() ? 2 : 1; String player = consumed == 2 ? args[0] : null; String value = args[consumed - 1].trim().toLowerCase(Locale.ROOT); + if (value.equals("rbp")) { + value = "bp" + (ThreadLocalRandom.current().nextInt(200) + 1); + } var score = PLAYER_SCORE.matcher(value); if (score.matches()) { - if (score.group(1).equals("bo")) throw new ResolutionException("未知的快捷查询"); - long index = score.group(2) == null ? 1 : positive(score.group(2), "快捷指令索引无效,请输入 1-200 之间的数字。例如: rs5"); - if (index > 200) throw new ResolutionException("快捷指令索引无效,请输入 1-200 之间的数字。例如: rs5"); + long index = score.group(2) == null ? 1 : positive(score.group(2), "快捷指令索引无效,请输入 1-200 之间的数字。例如: rp5"); + if (index > 200) throw new ResolutionException("快捷指令索引无效,请输入 1-200 之间的数字。例如: rp5"); return new TargetInput(Kind.valueOf(score.group(1).toUpperCase(Locale.ROOT)), null, index, player, consumed); } var set = SET.matcher(value); @@ -39,7 +43,7 @@ public static TargetInput read(String[] args) { long id = positive(value.substring(1), "谱面ID无效"); return new TargetInput(Kind.MAP, Long.toString(id), 1, null, consumed); } - long id = positive(value, "参数无效。请输入数字ID、本地成绩ID或快捷指令 (例如 loc123456789, rs1, 12345#2)。"); + long id = positive(value, "参数无效。请输入数字ID、本地成绩ID或快捷指令 (例如 loc123456789, rp1, 12345#2)。"); return new TargetInput(Kind.ID, Long.toString(id), 1, null, consumed); } From e9b099e6b2bd1f6f1128aa7d2f8c599494f0f41e Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 10:25:40 +0800 Subject: [PATCH 60/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20Resolver=20=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E5=86=85=E5=AE=B9=E6=B8=85=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=AF=B9=E7=89=B9=E5=AE=9A=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E7=9A=84=E6=9B=BF=E6=8D=A2=E5=A4=84=E7=90=86=EF=BC=9B?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20ScoreFilterArguments=20=E7=9A=84=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/command/parse/Resolver.java | 12 ++++++++++++ .../seira/command/parse/ScoreFilterArguments.java | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java index 01290593..00809909 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java @@ -21,7 +21,19 @@ public Resolver(java.util.function.Function boundUid) { this.boundUid = Objects.requireNonNull(boundUid); } + private static final Map, String> ALIASES = Map.of( + List.of("+", "+"), "+", + List.of("~", "~"), "~", + List.of("=", "="), "=" + ); + public String sanitize(String rawContent) { + for (Map.Entry, String> entry : ALIASES.entrySet()) { + for (String s : entry.getKey()) { + rawContent = rawContent.replace(s, entry.getValue()); + } + } + // Add surrounding space to <@> before expanding compact commands so /bp5<@...> is recognized. rawContent = Patterns.QQ_INLINE_AT_PATTERN.matcher(rawContent).replaceAll(r -> " " + r.group() + " "); diff --git a/src/main/java/xyz/zcraft/seira/command/parse/ScoreFilterArguments.java b/src/main/java/xyz/zcraft/seira/command/parse/ScoreFilterArguments.java index 7c54bef0..195b2a43 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/ScoreFilterArguments.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/ScoreFilterArguments.java @@ -13,7 +13,8 @@ public final class ScoreFilterArguments { private static final Pattern FILTER_PATTERN = Pattern.compile( "(?i)^(acc(?:uracy)?|combo|pp|time|length|len|star|stars|sr|bpm|miss|misses|score|mod|mods|rank|replay" - + "|any|title|artist|mapper|genre|language|tag|source|nsfw|video|storyboard|fullcombo|ar|od|cs|hp)" + + "|any|title|artist|mapper|genre|language|tag|source|nsfw|video|storyboard|fullcombo" + + "|ar|od|cs|hp|t|a|cb|cmb|m|vid|sb|fc|rep|rp)" + "(>=|<=|!=|!~|>|<|=|~)(.+)$" ); private static final Pattern MISS_SHORTHAND_PATTERN = Pattern.compile("(?i)^(!?)(\\d+)miss(?:es)?$"); From 37044bcc98fdcdabd7766eca200d60c0cd725915 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 10:26:08 +0800 Subject: [PATCH 61/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20Resolver=20=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E5=91=BD=E4=BB=A4=E6=A8=A1=E5=BC=8F=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=AF=B9=20"rbp"=20=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/command/parse/Resolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java index 00809909..991e00d4 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java @@ -171,7 +171,7 @@ public boolean looksLikeUid(String arg) { private static final class Patterns { private static final Pattern COMPACT_SCORE_COMMAND_PATTERN = Pattern.compile( - "(?i)^(rs|rp|bp)(\\d+)(?:-(\\d+))?(?=\\s|$)" + "(?i)^(rs|rp|bp|rbp)(\\d+)(?:-(\\d+))?(?=\\s|$)" ); private static final Pattern SPACE_MISSING_COMMAND_PATTERN = Pattern.compile( "^([a-zA-Z]+)(\\d+(?:#\\d+)?)" From f29d3d045c7f66ccfbf853042addc46ae974563f Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 10:36:39 +0800 Subject: [PATCH 62/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AgentService=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=86=85=E5=AE=B9=E7=9A=84=E7=BC=A9=E7=9F=AD?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/xyz/zcraft/seira/ai/AgentService.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index 0f539994..eb872a77 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -69,6 +69,18 @@ private static void trimToContextSize(Deque messages) { } } + private static String shorten(String input) { + if (input == null || input.isEmpty()) { + return input; + } + + if (input.length() <= 300) { + return input; + } + + return input.substring(0, 150) + "... 已省略 ..." + input.substring(input.length() - 150); + } + public void recordHistory(String groupId, String sender, String message) { if (groupId == null || groupId.isEmpty() || sender == null || sender.isEmpty() @@ -159,7 +171,7 @@ public String input( throw e; } - recordHistory(groupId, "Seira(你,回复" + openId + "的消息)", answer); + recordHistory(groupId, "Seira(你,回复" + openId + "的消息)", shorten(answer)); return answer; } finally { state.running.set(false); @@ -531,5 +543,3 @@ private HttpRequest.Builder newRequest(String path) { .header("Content-Type", "application/json"); } } - - From 27ff6b1044011367946ba8bc973e95208d0468dd Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 14:31:29 +0800 Subject: [PATCH 63/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20MPWatchCommandHandle?= =?UTF-8?q?r=20=E5=92=8C=20MPWatchService=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=AF=B9=E8=87=AA=E5=AE=9A=E4=B9=89=20BO=20=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/api/data/RomAIMatch.java | 4 +- .../handler/MPWatchCommandHandler.java | 49 ++++++++++++------- .../zcraft/seira/watch/MPWatchService.java | 9 ++-- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/api/data/RomAIMatch.java b/src/main/java/xyz/zcraft/seira/api/data/RomAIMatch.java index a9c368a2..a4c1b8fe 100644 --- a/src/main/java/xyz/zcraft/seira/api/data/RomAIMatch.java +++ b/src/main/java/xyz/zcraft/seira/api/data/RomAIMatch.java @@ -78,7 +78,9 @@ public record RomAIMatch( List score, Long currentMapId, String mode, - List playerData + List playerData, + Integer customBO, + Integer customELO ) { public record Teams(List teamA, List teamB){} diff --git a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java index 4d9abaec..e143bb55 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java @@ -50,7 +50,7 @@ public MPWatchCommandHandler( this.watchService = Objects.requireNonNull(watchService); } - static RoomTarget parseRoomTarget(String value, String explicitVersion) { + static RoomTarget parseRoomTarget(String value, MPVersion requestedVersion, Integer bo) { if (value == null || value.isBlank()) { return null; } @@ -69,19 +69,15 @@ static RoomTarget parseRoomTarget(String value, String explicitVersion) { return null; } - MPVersion requestedVersion = explicitVersion == null - ? null - : MPVersion.parse(explicitVersion); - if (explicitVersion != null && requestedVersion == null) { - return null; - } if (inferredVersion != null && requestedVersion != null && inferredVersion != requestedVersion) { return null; } + MPVersion version = (inferredVersion != null ? inferredVersion : requestedVersion); + try { long roomId = Long.parseLong(numeric); - return roomId > 0 ? new RoomTarget(roomId, version) : null; + return roomId > 0 ? new RoomTarget(roomId, version, bo) : null; } catch (NumberFormatException ignored) { return null; } @@ -166,10 +162,11 @@ public void handleRomAI(Context ctx) { at(ctx) + """ 正在启动 RomAI 监视喵。 > %s - %s + > ELO %d · BO%d %s """.formatted( - match.lobbyId(), - match.mode(), + match.lobbyId(), match.mode(), + match.customELO(), match.customBO(), teamString ).trim() ); @@ -183,7 +180,7 @@ public void handleRomAI(Context ctx) { } try { RoomWatchView view = watchService.watch( - ctx.groupId(), ctx.senderUserId(), MPVersion.STABLE, Long.parseLong(match.lobbyId()) + ctx.groupId(), ctx.senderUserId(), MPVersion.STABLE, Long.parseLong(match.lobbyId()), match.customBO() ); ctx.sendReply(PendingMessage.ofMarkdownRaw( at(ctx) + "已开始监视" + formatRoom(view) + "喵。" @@ -196,7 +193,7 @@ public void handleRomAI(Context ctx) { private void handleStart(Context ctx, int argumentOffset) { int startArgumentCount = ctx.argumentCount() - argumentOffset; - if (startArgumentCount < 0 || startArgumentCount > 2) { + if (startArgumentCount < 0 || startArgumentCount > 3) { usage(ctx); return; } @@ -209,10 +206,25 @@ private void handleStart(Context ctx, int argumentOffset) { return; } final Response multiplayerRoom = ApiHelper.getMultiplayerRoom(osuToken.accessToken()); - target = new RoomTarget(multiplayerRoom.getContent().getId(), MPVersion.LAZER); + target = new RoomTarget(multiplayerRoom.getContent().getId(), MPVersion.LAZER, null); } else { - String version = startArgumentCount == 2 ? ctx.argument(argumentOffset + 1) : null; - target = parseRoomTarget(ctx.argument(argumentOffset), version); + MPVersion version = null; + Integer customBo = null; + for (int i = argumentOffset + 1; i < ctx.argumentCount(); i++) { + final String arg = ctx.argument(i); + final Matcher boMatcher = BO.matcher(arg); + if (boMatcher.matches()) { + customBo = Integer.valueOf(boMatcher.group(1)); + } else if ("stable".equalsIgnoreCase(arg) || "stb".equalsIgnoreCase(arg)) { + version = MPVersion.STABLE; + } else if ("lazer".equalsIgnoreCase(arg) || "lzr".equalsIgnoreCase(arg)) { + version = MPVersion.LAZER; + } else { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "未知的参数: " + arg)); + return; + } + } + target = parseRoomTarget(ctx.argument(argumentOffset), version, customBo); } if (target == null) { @@ -229,7 +241,7 @@ private void handleStart(Context ctx, int argumentOffset) { } try { RoomWatchView view = watchService.watch( - ctx.groupId(), ctx.senderUserId(), target.version(), target.roomId() + ctx.groupId(), ctx.senderUserId(), target.version(), target.roomId(), null ); ctx.sendReply(PendingMessage.ofMarkdownRaw( at(ctx) + "已开始监视" + formatRoom(view) + "喵。" @@ -240,6 +252,9 @@ private void handleStart(Context ctx, int argumentOffset) { } } + private static final Pattern BO = Pattern.compile( + "^bo(\\d+)$" + ); private void handleStop(Context ctx) { if (ctx.argumentCount() == 2 && "all".equalsIgnoreCase(ctx.argument(1))) { int stoppedCount = watchService.stopAll(ctx.groupId()).size(); @@ -269,6 +284,6 @@ private void handleStatus(Context ctx) { : "你当前正在监视" + formatRoom(view) + "。"))); } - record RoomTarget(long roomId, MPVersion version) { + record RoomTarget(long roomId, MPVersion version, Integer customBo) { } } diff --git a/src/main/java/xyz/zcraft/seira/watch/MPWatchService.java b/src/main/java/xyz/zcraft/seira/watch/MPWatchService.java index 561f982b..442a69bd 100644 --- a/src/main/java/xyz/zcraft/seira/watch/MPWatchService.java +++ b/src/main/java/xyz/zcraft/seira/watch/MPWatchService.java @@ -67,7 +67,7 @@ public void start() { ); } - public RoomWatchView watch(String groupId, String userId, MPVersion version, long roomId) { + public RoomWatchView watch(String groupId, String userId, MPVersion version, long roomId, Integer customBo) { requireIdentifier(groupId, "groupId"); requireIdentifier(userId, "userId"); @@ -100,7 +100,7 @@ public RoomWatchView watch(String groupId, String userId, MPVersion version, lon Set baseline = new LinkedHashSet<>(); snapshot.completedPlays().forEach(play -> baseline.add(play.playlistItemId())); - WatchEntry entry = new WatchEntry(version, snapshot.roomId(), snapshot.roomName(), baseline); + WatchEntry entry = new WatchEntry(version, snapshot.roomId(), snapshot.roomName(), baseline, customBo); synchronized (lock) { ensureRoomAvailable(groupId, userId, room); watchesByGroup.computeIfAbsent(groupId, ignored -> new LinkedHashMap<>()) @@ -297,15 +297,16 @@ public void close() { } public record WatchEntry( - MPVersion version, long roomId, String roomName, Set sentPlaylistItemIds + MPVersion version, long roomId, String roomName, Set sentPlaylistItemIds, Integer customBo ) { public WatchEntry( - MPVersion version, long roomId, String roomName, Set sentPlaylistItemIds + MPVersion version, long roomId, String roomName, Set sentPlaylistItemIds, Integer customBo ) { this.version = version; this.roomId = roomId; this.roomName = roomName; this.sentPlaylistItemIds = new LinkedHashSet<>(sentPlaylistItemIds); + this.customBo = customBo; } } From a26a2d73b8bf7d644dbba25b1fc36efc3d8802e1 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 14:33:58 +0800 Subject: [PATCH 64/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20MPWatchCommandHandle?= =?UTF-8?q?r=EF=BC=8C=E6=9B=B4=E6=96=B0=E5=AF=B9=E6=96=B9=E4=B8=8D?= =?UTF-8?q?=E5=9C=A8=E6=89=93=20RomAI=20=E7=9A=84=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=EF=BC=8C=E5=A2=9E=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=90=8D=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java index e143bb55..fd2c65fd 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java @@ -139,7 +139,7 @@ public void handleRomAI(Context ctx) { final RomAIMatch match = RomAIApi.getMatchFor(username); if (match == null) { - ctx.sendReply(at(ctx) + "对方不在打 RomAI 喵。"); + ctx.sendReply(at(ctx) + username + "不在打 RomAI 喵。"); return; } From f168c789761ba5d3c22b87ef3a8c7b370f291b07 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 14:52:50 +0800 Subject: [PATCH 65/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20Resolver=20=E5=92=8C?= =?UTF-8?q?=20Router=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=AF=B9=20"rbp"=20?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E7=9A=84=E6=94=AF=E6=8C=81=EF=BC=9B=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20README=20=E6=96=87=E6=A1=A3=E4=BB=A5=E5=8F=8D?= =?UTF-8?q?=E6=98=A0=E6=96=B0=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + .../command/handler/ScoreCommandHandler.java | 18 ++++++++++++++++++ .../zcraft/seira/command/parse/Resolver.java | 2 +- .../xyz/zcraft/seira/command/route/Router.java | 1 + 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index db7f7a73..48552a36 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ Seira正在活跃开发中,在使用的过程中可能会有一些Bug,也会 | `/lb` | `/lb [id] [,...]` | 列出指定谱面排行或表现分排行 | | `/daily` | `/daily` | 每日挑战信息 | | `/luck` | `/luck` | 今日人品 | +| `/rbp` | `/rbp [uid/username/@user]` | 获取随机BP | | `/mp` | `/mp` | 多人房间列表 | | `/watch` | `/watch add/del/list [目标]` | 添加/删除/列出监视任务 | | `/mpwatch` | `/mpwatch start/stop/status [目标]` | 按群成员添加、停止或查看多人房间监视;`stop all` 可停止本群全部监视 | diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java index 204228f8..c5c7df08 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java @@ -14,6 +14,7 @@ import xyz.zcraft.seira.command.reply.ReplyFactory; import java.util.List; +import java.util.concurrent.ThreadLocalRandom; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -114,6 +115,23 @@ public void handleBp(Context ctx) { } } + public void handleRbp(Context ctx) { + if (ctx.argumentCount() > 1) { + ctx.sendReply(at(ctx) + "用法:/rbp [目标]"); + return; + } + + String player = resolver.player(ctx.argumentCount() == 1 ? ctx.argument(0) : null, ctx.senderUserId()); + + try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { + long uid = ApiHelper.resolveUid(player); + String scoreId = ApiHelper.lookupPlayerScore(uid, "bp", ThreadLocalRandom.current().nextInt(200) + 1, List.of(), null); + var response = ApiHelper.getScoreResponse(scoreId); + history.remember(ctx, null, null, scoreId); + ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); + } + } + public void handleRs(Context ctx, boolean includeFail) { if (ctx.args().length == 0) { String player = resolver.player(null, ctx.senderUserId()); diff --git a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java index 991e00d4..00809909 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java @@ -171,7 +171,7 @@ public boolean looksLikeUid(String arg) { private static final class Patterns { private static final Pattern COMPACT_SCORE_COMMAND_PATTERN = Pattern.compile( - "(?i)^(rs|rp|bp|rbp)(\\d+)(?:-(\\d+))?(?=\\s|$)" + "(?i)^(rs|rp|bp)(\\d+)(?:-(\\d+))?(?=\\s|$)" ); private static final Pattern SPACE_MISSING_COMMAND_PATTERN = Pattern.compile( "^([a-zA-Z]+)(\\d+(?:#\\d+)?)" diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index c4a08f4f..6089b962 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -129,6 +129,7 @@ private static CommandRegistry createCommandRegistry( .register(bindingCommands::handleBind, "bind") .register(bindingCommands::handleUnbind, "unbind") .register(bindingCommands::handleClearHistory, "clearhistory") + .register(scoreCommands::handleRbp, "rbp") .register(scoreCommands::handleBp, "bp") .register(beatmapCommands::handleDaily, "daily") .register(socialCommands::handleMp, "mp") From 820e80f412bd79d92b93b7c5f48542e315145203 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 15:15:15 +0800 Subject: [PATCH 66/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20MPWatchApi=20?= =?UTF-8?q?=E5=92=8C=20MPWatchService=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=20BO=20=E5=8F=82=E6=95=B0=E7=9A=84?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java | 7 ++++--- src/main/java/xyz/zcraft/seira/watch/MPWatchService.java | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java b/src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java index 9502116a..0779eb6b 100644 --- a/src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java +++ b/src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java @@ -97,11 +97,12 @@ public RoomWatchSnapshot getSnapshot(MPVersion version, long roomId) { return snapshot; } - public byte[] renderResult(MPVersion version, long roomId, long playlistItemId) { + public byte[] renderResult(MPVersion version, long roomId, long playlistItemId, Integer customBo) { HttpResponse response = get( "/multiplayer/rooms/" + requirePositive(roomId, "roomId") + "/playlist/" + requirePositive(playlistItemId, "playlistItemId") + "/result?version=" - + requireVersion(version).value(), + + requireVersion(version).value() + + (customBo == null ? "" : "&bo" + customBo), HttpResponse.BodyHandlers.ofByteArray() ); ensureSuccessfulStatus(response.statusCode(), response.body(), "生成多人房间结果图片"); @@ -115,7 +116,7 @@ private HttpResponse get(String path, HttpResponse.BodyHandler handler HttpRequest.Builder builder = HttpRequest.newBuilder() .uri(URI.create(endpoint + path)) .timeout(Duration.ofMinutes(2)) - .header("Accept", "image/png"); + .header("Accept", "image/png"); if (serviceToken != null && !serviceToken.isBlank()) { builder.header("Authorization", "Bearer " + serviceToken); } diff --git a/src/main/java/xyz/zcraft/seira/watch/MPWatchService.java b/src/main/java/xyz/zcraft/seira/watch/MPWatchService.java index 442a69bd..62b8a165 100644 --- a/src/main/java/xyz/zcraft/seira/watch/MPWatchService.java +++ b/src/main/java/xyz/zcraft/seira/watch/MPWatchService.java @@ -198,7 +198,7 @@ private boolean sendPendingResults( try { byte[] image = rendered.computeIfAbsent( play.playlistItemId(), - itemId -> api.renderResult(watch.entry().version, watch.entry().roomId, itemId) + itemId -> api.renderResult(watch.entry().version, watch.entry().roomId, itemId, watch.entry().customBo()) ); if (!notifier.sendResult(watch.entry(), watch.groupId(), image)) { LOG.warn( From 0b69c8e85e355f3cac3e5cc4c872ec64423bbe31 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 15:25:54 +0800 Subject: [PATCH 67/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20MPWatchApi=20?= =?UTF-8?q?=E5=92=8C=20MPWatchService=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=20BO=20=E5=8F=82=E6=95=B0=E7=9A=84?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zcraft/seira/command/handler/MPWatchCommandHandler.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java index fd2c65fd..b353b95e 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java @@ -162,11 +162,12 @@ public void handleRomAI(Context ctx) { at(ctx) + """ 正在启动 RomAI 监视喵。 > %s - %s - > ELO %d · BO%d + > ELO %s · BO%s %s """.formatted( match.lobbyId(), match.mode(), - match.customELO(), match.customBO(), + (match.customELO() == null ? "?" : match.customELO().toString()), + (match.customBO() == null ? "?" : match.customBO().toString()), teamString ).trim() ); From c404b7c663bf6059d6cbf82cd2c2c484279aa254 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 15:33:20 +0800 Subject: [PATCH 68/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20MPWatchApi=20?= =?UTF-8?q?=E5=92=8C=20MPWatchService=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=20BO=20=E5=8F=82=E6=95=B0=E7=9A=84?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java b/src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java index 0779eb6b..cf6b7fd0 100644 --- a/src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java +++ b/src/main/java/xyz/zcraft/seira/watch/MPWatchApi.java @@ -102,7 +102,7 @@ public byte[] renderResult(MPVersion version, long roomId, long playlistItemId, "/multiplayer/rooms/" + requirePositive(roomId, "roomId") + "/playlist/" + requirePositive(playlistItemId, "playlistItemId") + "/result?version=" + requireVersion(version).value() - + (customBo == null ? "" : "&bo" + customBo), + + (customBo == null ? "" : "&bo=" + customBo), HttpResponse.BodyHandlers.ofByteArray() ); ensureSuccessfulStatus(response.statusCode(), response.body(), "生成多人房间结果图片"); From e0621518a8a92a03e7c436578314d71908c380ed Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 16:12:26 +0800 Subject: [PATCH 69/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20SocialCommandHandler?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E7=94=A8=E6=88=B7=E6=8F=90=E5=8F=8A?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/command/handler/SocialCommandHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java index 0d6fc6c7..08b9de4c 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java @@ -99,7 +99,7 @@ public void handleFriendStatus(Context ctx) { final String mention = UserDataStore.findGroupOpenIdByUid(ctx.groupId(), uid) .map(ReplyFactory::at) .orElse(""); - ctx.sendReply(PendingMessage.ofMarkdownRaw(mention) + ": [%d](%s)".formatted(uid, "https://osu.ppy.sh/users/" + uid)); + ctx.sendReply(PendingMessage.ofMarkdownRaw(mention + ": [%d](%s)".formatted(uid, "https://osu.ppy.sh/users/" + uid))); final UserExtended targetUser = ApiHelper.getUserRaw(uid); final String targetOsuAvatar = targetUser.getAvatarUrl(); From 57d9e7cff7b5b4df6f3a6c1db965c829cd9b0a0a Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Wed, 23 Sep 2026 22:49:22 +0800 Subject: [PATCH 70/96] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=AA=B0=E5=AD=90?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E9=AA=B0=E5=AD=90?= =?UTF-8?q?=E8=A1=A8=E8=BE=BE=E5=BC=8F=E8=A7=A3=E6=9E=90=E5=92=8C=E6=8E=B7?= =?UTF-8?q?=E9=AA=B0=E7=BB=93=E6=9E=9C=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 19 +++-- pom.xml | 2 +- .../handler/GeneralCommandHandler.java | 28 +++++++ .../handler/RankGuessCommandHandler.java | 2 +- .../zcraft/seira/command/route/Router.java | 1 + .../java/xyz/zcraft/seira/util/dice/Dice.java | 15 ++++ .../zcraft/seira/util/dice/expr/DiceExpr.java | 83 +++++++++++++++++++ .../util/dice/expr/part/DiceExprPart.java | 25 ++++++ .../dice/expr/part/DiceExprPartComplex.java | 39 +++++++++ .../dice/expr/part/DiceExprPartNumber.java | 17 ++++ .../dice/expr/part/DiceExprPartSingle.java | 31 +++++++ .../seira/util/dice/result/DiceResult.java | 64 ++++++++++++++ 12 files changed, 317 insertions(+), 9 deletions(-) create mode 100644 src/main/java/xyz/zcraft/seira/util/dice/Dice.java create mode 100644 src/main/java/xyz/zcraft/seira/util/dice/expr/DiceExpr.java create mode 100644 src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPart.java create mode 100644 src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPartComplex.java create mode 100644 src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPartNumber.java create mode 100644 src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPartSingle.java create mode 100644 src/main/java/xyz/zcraft/seira/util/dice/result/DiceResult.java diff --git a/README.md b/README.md index 48552a36..4c605db2 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ Seira正在活跃开发中,在使用的过程中可能会有一些Bug,也会 | `/sms` | `/sms ` | 搜索谱面集 | | `/lb` | `/lb [id] [,...]` | 列出指定谱面排行或表现分排行 | | `/daily` | `/daily` | 每日挑战信息 | +| `/roll` | `/roll [dice]` | 掷骰子 | | `/luck` | `/luck` | 今日人品 | | `/rbp` | `/rbp [uid/username/@user]` | 获取随机BP | | `/mp` | `/mp` | 多人房间列表 | @@ -110,17 +111,21 @@ Seira正在活跃开发中,在使用的过程中可能会有一些Bug,也会 `/r`和`/rsc`(回放渲染)会先返回“生成请求正在等待中,队列位置:N”,随后返回请求状态,最后在渲染完成后再发送回放视频。 -上传 osu! 服务器上不存在的 `.osr` 回放后,机器人会返回形如 `loc123456789` 的本地成绩ID;该ID可用于 `/s`、`/sa`、`/ma`、`/r`、`/rsc` 等成绩目标指令。 +上传 osu! 服务器上不存在的 `.osr` 回放后,机器人会返回形如 `loc123456789` 的本地成绩ID;该ID可用于 `/s`、`/sa`、`/ma`、`/r`、 +`/rsc` 等成绩目标指令。 ### 快捷查询 对于一些需要指定谱面ID或成绩ID的指令(如 `/m`、`/s`、`/ms` 等),支持快捷查询写法,格式为 `rs5`、`bp3`、`rp2`。 -快捷查询也可以直接作为指令使用,并在后面指定玩家,例如 `/bp5 @用户`;紧凑写法同样支持列表范围,例如 `/bp21-30`、`/rp6-10 @用户`。 +快捷查询也可以直接作为指令使用,并在后面指定玩家,例如 `/bp5 @用户`;紧凑写法同样支持列表范围,例如 `/bp21-30`、 +`/rp6-10 @用户`。 -这些指令会共享最近一次显式指定的查询目标。已有最近目标时,`/r`、`/rsc`、`/ma` 可以省略目标,例如 `/r 01:00-01:30`、`/rsc +12345,67890 -`、`/ma #2`。 +这些指令会共享最近一次显式指定的查询目标。已有最近目标时,`/r`、`/rsc`、`/ma` 可以省略目标,例如 `/r 01:00-01:30`、 +`/rsc +12345,67890 -`、`/ma #2`。 -也可以在前面写上玩家ID、用户名或@用户,例如 `123456 rs5`、`peppy bp3`、`@ABC bp3`,表示查询指定玩家的最近成绩第 5 条或最好成绩第 3 条。 +也可以在前面写上玩家ID、用户名或@用户,例如 `123456 rs5`、`peppy bp3`、`@ABC bp3`,表示查询指定玩家的最近成绩第 5 条或最好成绩第 +3 条。 - `rs5`:使用你已绑定的玩家ID,查询“最近成绩第 5 条” - `rp1`:使用你已绑定的玩家ID,查询“最近通过成绩第 1 条” @@ -139,11 +144,11 @@ Seira正在活跃开发中,在使用的过程中可能会有一些Bug,也会 ## 调试命令 > [!WARNING] -> +> > **警告:危险区域!** -> +> > 以下命令仅用于调试,使用不当可能会造成数据丢失、账号封禁等后果,且仅在调试模式启用且发送者为bpt管理员的情况下可用。 -> +> > 这些指令可能会随时进行添加、修改或删除,且不保证向后兼容。 | 命令 | 结果 | diff --git a/pom.xml b/pom.xml index 5af75698..069e1fe3 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.7 + 1.12.8 25 diff --git a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java index 22bc8b70..9ce9571c 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java @@ -12,7 +12,11 @@ import xyz.zcraft.seira.data.UploadedImage; import xyz.zcraft.seira.services.DailyLuck; import xyz.zcraft.seira.services.NoticeStore; +import xyz.zcraft.seira.util.dice.Dice; +import xyz.zcraft.seira.util.dice.expr.DiceExpr; +import xyz.zcraft.seira.util.dice.result.DiceResult; +import java.util.Objects; import java.util.function.Predicate; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; @@ -49,6 +53,30 @@ public void handleU(Context context) { } } + public void handleRoll(Context ctx) { + DiceExpr diceExpr; + + if (ctx.argumentCount() == 0) { + diceExpr = DiceExpr.HUNDRED; + } else { + try { + diceExpr = DiceExpr.parse(ctx.query()); + } catch (Exception e) { + ctx.sendReply(at(ctx) + "无法解析骰子表达式喵。"); + return; + } + } + + final Dice dice = new Dice(); + final DiceResult rollResult = dice.roll(diceExpr); + + final String rollResultStr = rollResult.toString(); + final String totalStr = String.valueOf(rollResult.total()); + + final String message = at(ctx) + diceExpr + (Objects.equals(rollResultStr, totalStr) ? "" : " = " + rollResultStr) + " = __" + totalStr + "__"; + ctx.sendReply(message.replace("*", "\\*")); + } + public void handleLuck(Context context) { if (context.argumentCount() != 0) { context.sendReply(PendingMessage.ofMarkdownRaw(at(context) + "用法:/luck")); diff --git a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java index b85df461..594235e1 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java @@ -680,7 +680,7 @@ private void end(Context ctx, boolean force) { case FINISHED -> replyFactory.rankGuessResultMessage(ctx, result.round(), result.rankType()); }; - ctx.send(true, message, true); + ctx.send(true, message, false); } enum LeaderboardType { diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index 6089b962..374e3c4f 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -159,6 +159,7 @@ private static CommandRegistry createCommandRegistry( .register(generalCommands::handleStat, "stat") .register(generalCommands::handleU, "u") .register(generalCommands::handleLuck, "luck") + .register(generalCommands::handleRoll, "roll") .register(replayCommands::handleRstat, "rstat") .register(replayCommands::handleRcancel, "rcancel") .register(generalCommands::handleInspect, "inspect") diff --git a/src/main/java/xyz/zcraft/seira/util/dice/Dice.java b/src/main/java/xyz/zcraft/seira/util/dice/Dice.java new file mode 100644 index 00000000..59b6dc14 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/util/dice/Dice.java @@ -0,0 +1,15 @@ +package xyz.zcraft.seira.util.dice; + +import xyz.zcraft.seira.util.dice.expr.DiceExpr; +import xyz.zcraft.seira.util.dice.result.DiceResult; + +import java.util.LinkedList; +import java.util.List; + +public class Dice { + public DiceResult roll(DiceExpr expression) { + final List> results = new LinkedList<>(); + expression.parts().forEach(e -> results.add(e.calculate())); + return new DiceResult(results); + } +} diff --git a/src/main/java/xyz/zcraft/seira/util/dice/expr/DiceExpr.java b/src/main/java/xyz/zcraft/seira/util/dice/expr/DiceExpr.java new file mode 100644 index 00000000..a462417b --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/util/dice/expr/DiceExpr.java @@ -0,0 +1,83 @@ +package xyz.zcraft.seira.util.dice.expr; + +import org.jetbrains.annotations.NotNull; +import xyz.zcraft.seira.util.dice.expr.part.DiceExprPart; +import xyz.zcraft.seira.util.dice.expr.part.DiceExprPartComplex; +import xyz.zcraft.seira.util.dice.expr.part.DiceExprPartNumber; +import xyz.zcraft.seira.util.dice.expr.part.DiceExprPartSingle; + +import java.util.LinkedList; +import java.util.List; + +public record DiceExpr(List parts) { + public static final DiceExpr CHECK; + public static final DiceExpr HUNDRED; + public static final DiceExpr TEN; + public static final DiceExpr SIX; + + static { + CHECK = new DiceExpr(List.of(new DiceExprPartSingle("1d100"))); + HUNDRED = new DiceExpr(List.of(new DiceExprPartSingle("1d100"))); + TEN = new DiceExpr(List.of(new DiceExprPartSingle("1d10"))); + SIX = new DiceExpr(List.of(new DiceExprPartSingle("1d6"))); + } + + public static DiceExpr parse(String str) { + str = str.replace(" ", ""); + + if (isInvalid(str)) { + throw new IllegalArgumentException("骰子表达式有误"); + } + + if (!str.startsWith("-")) str = "+" + str; + final LinkedList compStr = new LinkedList<>(); + int last = 0; + for (int i = 1; i < str.length(); i++) { + if (str.charAt(i) == '+' || str.charAt(i) == '-') { + compStr.add(str.substring(last, i)); + last = i; + } + } + + if (last != str.length() - 1) compStr.add(str.substring(last)); + + final LinkedList parts = new LinkedList<>(); + + compStr.forEach(s -> { + if (isDigit(s)) { + parts.add(new DiceExprPartNumber(s)); + } else if (s.contains("*")) { + parts.add(new DiceExprPartComplex(s)); + } else { + parts.add(new DiceExprPartSingle(s)); + } + }); + + return new DiceExpr(parts); + } + + public static boolean isDigit(String str) { + if (str == null) return false; + if (str.startsWith("+") || str.startsWith("-")) str = str.substring(1); + return str.chars().allMatch(value -> value >= '0' && value <= '9'); + } + + public static boolean isInvalid(String exp) { + if (exp == null || exp.isBlank()) return true; + return !exp.chars().allMatch(value -> (value >= '0' && value <= '9') || + value == 'd' || value == '+' || value == '*'); + } + + @NotNull + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + if (parts.getFirst().isNegative()) sb.append("-"); + sb.append(parts.getFirst().toString(false)); + for (int i = 1; i < parts.size(); i++) { + sb.append(parts.get(i).toString(true)); + } + + return sb.toString(); + } +} diff --git a/src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPart.java b/src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPart.java new file mode 100644 index 00000000..edaf43fd --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPart.java @@ -0,0 +1,25 @@ +package xyz.zcraft.seira.util.dice.expr.part; + +import lombok.Getter; + +import java.util.List; + +@Getter +public abstract class DiceExprPart { + protected final boolean negative; + protected final String origString; + + protected DiceExprPart(boolean negative, String origString) { + this.negative = negative; + this.origString = origString; + } + + public abstract List calculate(); + + public String toString(boolean signed) { + if ((origString.startsWith("-") || origString.startsWith("+")) && !signed) { + return origString.substring(1); + } + return origString; + } +} diff --git a/src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPartComplex.java b/src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPartComplex.java new file mode 100644 index 00000000..4ec2c542 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPartComplex.java @@ -0,0 +1,39 @@ +package xyz.zcraft.seira.util.dice.expr.part; + +import xyz.zcraft.seira.util.dice.expr.DiceExpr; + +import java.util.LinkedList; +import java.util.List; + +public class DiceExprPartComplex extends DiceExprPart { + final LinkedList factors; + + public DiceExprPartComplex(String origString) { + super(origString.startsWith("-"), origString); + + if (origString.startsWith("-") || origString.startsWith("+")) { + origString = origString.substring(1); + } + + final String[] split = origString.split("\\*"); + + factors = new LinkedList<>(); + + for (String s : split) { + if (DiceExpr.isDigit(s)) { + factors.add(new DiceExprPartNumber(s)); + } else { + factors.add(new DiceExprPartSingle(s)); + } + } + } + + @Override + public List calculate() { + LinkedList result = new LinkedList<>(); + for (DiceExprPart factor : factors) { + result.add(factor.calculate().getFirst()); + } + return result; + } +} diff --git a/src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPartNumber.java b/src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPartNumber.java new file mode 100644 index 00000000..e2e4390f --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPartNumber.java @@ -0,0 +1,17 @@ +package xyz.zcraft.seira.util.dice.expr.part; + +import java.util.List; + +public class DiceExprPartNumber extends DiceExprPart { + final int num; + + public DiceExprPartNumber(String string) { + super(Integer.parseInt(string) < 0, string); + this.num = Integer.parseInt(string); + } + + @Override + public List calculate() { + return List.of(num); + } +} diff --git a/src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPartSingle.java b/src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPartSingle.java new file mode 100644 index 00000000..93a8dba6 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/util/dice/expr/part/DiceExprPartSingle.java @@ -0,0 +1,31 @@ +package xyz.zcraft.seira.util.dice.expr.part; + +import java.util.List; +import java.util.Random; + +public class DiceExprPartSingle extends DiceExprPart { + final int number; + final int faces; + + public DiceExprPartSingle(String str) { + super(str.startsWith("-"), str); + if (str.startsWith("-") || str.startsWith("+")) + str = str.substring(1); + + final int split = str.indexOf("d"); + this.number = Integer.parseInt(str.substring(0, split)); + this.faces = Integer.parseInt(str.substring(split + 1)); + } + + @Override + public List calculate() { + int result = 0; + for (int i = 0; i < number; i++) { + result += new Random().nextInt(faces) + 1; + } + + if (negative) result = -result; + + return List.of(result); + } +} diff --git a/src/main/java/xyz/zcraft/seira/util/dice/result/DiceResult.java b/src/main/java/xyz/zcraft/seira/util/dice/result/DiceResult.java new file mode 100644 index 00000000..d469086a --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/util/dice/result/DiceResult.java @@ -0,0 +1,64 @@ +package xyz.zcraft.seira.util.dice.result; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class DiceResult { + private final List> parts; + + public DiceResult(List> parts) { + this.parts = parts; + } + + public DiceResult(int num) { + this.parts = List.of(List.of(num)); + } + + public int total() { + int sum = 0; + for (var factors : parts) { + int tmp = 1; + for (var factor : factors) { + tmp *= factor; + } + sum += tmp; + } + + return sum; + } + + @NotNull + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < parts.size(); i++) { + if(i == 0) { + if (parts.get(i).size() < 2) { + sb.append(parts.get(i).getFirst()); + continue; + } + } else { + if (parts.get(i).size() >= 2) sb.append("+"); + else { + if(parts.get(i).getFirst() >= 0) { + sb.append("+"); + } + sb.append(parts.get(i).getFirst()); + continue; + } + } + + for (int j = 0; j < parts.get(i).size(); j++) { + final int cur = parts.get(i).get(j); + if (j != 0) { + sb.append("*"); + } + if (cur >= 0) sb.append(cur); + else sb.append("(-").append(cur).append(")"); + } + } + + return sb.toString(); + } +} From 5c16a4ac3683d2d100fb7bfdfa4ac7e5dcd4bd35 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Fri, 25 Sep 2026 23:54:00 +0800 Subject: [PATCH 71/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AgentService=20?= =?UTF-8?q?=E5=92=8C=20AiChatHandler=EF=BC=8C=E9=87=8D=E6=9E=84=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E5=88=9B=E5=BB=BA=E5=92=8C=E6=9B=B4=E6=96=B0=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=8A=A0=E6=B8=85=E9=99=A4=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../xyz/zcraft/seira/ai/AgentService.java | 60 +++++++++++++------ .../xyz/zcraft/seira/ai/AiChatHandler.java | 4 +- .../xyz/zcraft/seira/ai/data/AgentFile.java | 11 ++++ .../zcraft/seira/command/route/Router.java | 7 ++- 5 files changed, 62 insertions(+), 22 deletions(-) create mode 100644 src/main/java/xyz/zcraft/seira/ai/data/AgentFile.java diff --git a/pom.xml b/pom.xml index 069e1fe3..d9128dcf 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.8 + 1.12.9 25 diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index eb872a77..dfefb36d 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -313,8 +313,10 @@ public void resetIfNeeded(Api api, String groupId) { } try { + api.clearConversation(groupId, conv.appConversationID()); conv = api.createConversation(groupId); vars.clear(); + incomingMessages.clear(); } catch (RuntimeException | Error e) { resetting.set(true); throw e; @@ -352,10 +354,10 @@ public Api(LLMConfig config) { this.apiKey = config.apiKey(); } - public AppConversationBrief createConversation(String openId) { - LOG.info("Creating conversation for user {}", openId); + public AppConversationBrief createConversation(String groupId) { + LOG.info("Creating conversation for id {}", groupId); JsonObject body = new JsonObject(); - body.addProperty("UserID", openId); + body.addProperty("UserID", groupId); try { var request = newRequest("/api/proxy/api/v1/create_conversation") .POST(HttpRequest.BodyPublishers.ofString(body.toString())) @@ -374,18 +376,18 @@ public AppConversationBrief createConversation(String openId) { AppConversationBrief.class ); - LOG.info("Conversation for user {} created, id {}", openId, conversation.appConversationID()); + LOG.info("Conversation for id {} created, conv id {}", groupId, conversation.appConversationID()); return conversation; } catch (Exception e) { throw new RuntimeException("Error creating conversation", e); } } - public void updateConversation(String openId, String appConvId, Map variables) { - LOG.info("Updating conversation for user {}", openId); + public void updateConversation(String groupId, String appConvId, Map variables) { + LOG.info("Updating conversation for id {}", groupId); JsonObject body = new JsonObject(); - body.addProperty("UserID", openId); + body.addProperty("UserID", groupId); body.addProperty("AppConversationID", appConvId); body.add("Inputs", GSON.toJsonTree(variables)); @@ -400,17 +402,41 @@ public void updateConversation(String openId, String appConvId, Map send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + + if (send.statusCode() != 200) { + throw new RuntimeException("Failed to clear conversation: " + send.statusCode()); + } + + LOG.info("Cleared conversation for id {}", groupId); + } catch (Exception e) { + throw new RuntimeException("Error clearing conversation", e); + } + } + + public ChatQueryResponse chatQuery(String groupId, String appConvId, String query) { + LOG.info("Running chat query for id {}", groupId); + + JsonObject body = new JsonObject(); + body.addProperty("UserID", groupId); body.addProperty("AppConversationID", appConvId); body.addProperty("Query", query); body.addProperty("ResponseMode", "blocking"); @@ -429,8 +455,8 @@ public ChatQueryResponse chatQuery(String openId, String appConvId, String query final ChatQueryResponse chatQueryResponse = GSON.fromJson(send.body(), ChatQueryResponse.class); LOG.info( - "Chat query success for user {}, Token input:{}, output:{}", - openId, + "Chat query success for id {}, Token input:{}, output:{}", + groupId, chatQueryResponse.inputTokens(), chatQueryResponse.outputTokens() ); @@ -441,11 +467,11 @@ public ChatQueryResponse chatQuery(String openId, String appConvId, String query } } - public String chatQueryStreaming(String openId, String appConvId, String query, StreamHandler handler) { - LOG.info("Running chat query for user {}", openId); + public String chatQueryStreaming(String groupId, String appConvId, String query, StreamHandler handler) { + LOG.info("Running chat query for id {}", groupId); JsonObject body = new JsonObject(); - body.addProperty("UserID", openId); + body.addProperty("UserID", groupId); body.addProperty("AppConversationID", appConvId); body.addProperty("Query", query); body.addProperty("ResponseMode", "streaming"); @@ -518,7 +544,7 @@ public String chatQueryStreaming(String openId, String appConvId, String query, handler.onComplete(result); - LOG.info("Chat query success for user {}", openId); + LOG.info("Chat query success for id {}", groupId); return result; } catch (Exception e) { diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index 23a15470..f58f9187 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -79,7 +79,7 @@ public void handleAi(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "用法:/ai [on|off]")); } - public void handleChat(Context ctx) { + public void handleChat(Context ctx, String message) { if (agentService.isRunning(ctx.groupId(), ctx.senderUserId())) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已有一轮对话正在进行中了喵,请稍作等待~")); return; @@ -88,7 +88,7 @@ public void handleChat(Context ctx) { agentService.input( ctx.groupId(), ctx.senderUserId(), - ctx.rawContent(), + message, input -> generateVar(ctx, input), new StreamHandler() { @Override diff --git a/src/main/java/xyz/zcraft/seira/ai/data/AgentFile.java b/src/main/java/xyz/zcraft/seira/ai/data/AgentFile.java new file mode 100644 index 00000000..c39d18db --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/ai/data/AgentFile.java @@ -0,0 +1,11 @@ +package xyz.zcraft.seira.ai.data; + +import com.google.gson.annotations.SerializedName; + +public record AgentFile( + @SerializedName("Name") String name, + @SerializedName("Path") String path, + @SerializedName("Size") Long size, + @SerializedName("Url") String url +) { +} diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index 374e3c4f..2f376168 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -205,6 +205,8 @@ private void handleMessageReceived( rawContent = rawContent == null ? "" : rawContent.trim(); + String msgToRecord = rawContent; + boolean beingAt = false; AppConfig config = configSupplier.get(); @@ -212,6 +214,7 @@ private void handleMessageReceived( if (rawContent.contains(selfAt)) { beingAt = true; + msgToRecord = msgToRecord.replace(selfAt, "@Seira"); } if (rawContent.startsWith(selfAt)) { @@ -247,9 +250,9 @@ private void handleMessageReceived( ); if (beingAt && permitAi) { - aiChatHandler.handleChat(parseResult.context().withReplies(replies)); + aiChatHandler.handleChat(parseResult.context().withReplies(replies), msgToRecord); } else if (permitAi) { - aiChatHandler.recordHistory(groupId, userId, rawContent, attachments); + aiChatHandler.recordHistory(groupId, userId, msgToRecord, attachments); } return; } From a33c3a85604bb74316dcfa2dcbb9c2d6f60c94f0 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 27 Sep 2026 15:28:56 +0800 Subject: [PATCH 72/96] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20AsteroidConfig=20?= =?UTF-8?q?=E5=92=8C=20AsteroidApi=EF=BC=8C=E6=94=AF=E6=8C=81=20Minecraft?= =?UTF-8?q?=20=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=8A=B6=E6=80=81=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../xyz/zcraft/seira/ai/AgentService.java | 100 +++++++-- .../java/xyz/zcraft/seira/api/ApiUtil.java | 81 +++++++ .../xyz/zcraft/seira/api/AsteroidApi.java | 52 +++++ .../api/{ApiHelper.java => OstellaApi.java} | 198 ++++++------------ .../seira/api/data/MinecraftServerStatus.java | 38 ++++ .../seira/command/AttachmentHandler.java | 4 +- .../seira/command/ReplayResultStore.java | 8 +- .../zcraft/seira/command/TaskCoordinator.java | 10 +- .../handler/BeatmapCommandHandler.java | 104 ++++----- .../handler/GeneralCommandHandler.java | 10 +- .../handler/MPWatchCommandHandler.java | 8 +- .../handler/RankGuessCommandHandler.java | 18 +- .../command/handler/ReplayCommandHandler.java | 44 ++-- .../command/handler/ScoreCommandHandler.java | 106 +++++----- .../command/handler/SocialCommandHandler.java | 38 ++-- .../command/handler/WatchCommandHandler.java | 10 +- .../seira/command/reply/ReplyFactory.java | 10 +- .../seira/command/route/DebugRoutes.java | 14 +- .../xyz/zcraft/seira/config/AppConfig.java | 3 +- .../zcraft/seira/config/AsteroidConfig.java | 7 + .../zcraft/seira/config/RuntimeConfig.java | 3 +- src/main/resources/seira-example-config.yml | 5 + 23 files changed, 529 insertions(+), 344 deletions(-) create mode 100644 src/main/java/xyz/zcraft/seira/api/ApiUtil.java create mode 100644 src/main/java/xyz/zcraft/seira/api/AsteroidApi.java rename src/main/java/xyz/zcraft/seira/api/{ApiHelper.java => OstellaApi.java} (84%) create mode 100644 src/main/java/xyz/zcraft/seira/api/data/MinecraftServerStatus.java create mode 100644 src/main/java/xyz/zcraft/seira/config/AsteroidConfig.java diff --git a/pom.xml b/pom.xml index d9128dcf..013af415 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.9 + 1.12.10 25 diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index dfefb36d..435ef8de 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -7,6 +7,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; +import xyz.zcraft.seira.ai.data.AgentFile; import xyz.zcraft.seira.ai.data.AppConversationBrief; import xyz.zcraft.seira.ai.data.ChatQueryResponse; import xyz.zcraft.seira.command.Context; @@ -22,17 +23,28 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; public class AgentService { public static final int CONTEXT_SIZE = 30; - + // + // + private static final Pattern QQ_FACE = Pattern.compile( + "", Pattern.CASE_INSENSITIVE + ); + // ![ECA4B87395655D69E285314BAB3A2105.jpg](https://multimedia.nt.qq.com.cn/download?appid=14....9_U&spec=0) + private static final Pattern QQ_MEME = Pattern.compile( + "(?: !\\[.*]\\(.*\\))?", Pattern.CASE_INSENSITIVE + ); + // + private static final Pattern QQ_MEME_ALT = Pattern.compile( + "", Pattern.CASE_INSENSITIVE + ); private final Api api; - private final Map states = new ConcurrentHashMap<>(); - private final Map> chatLog = new ConcurrentHashMap<>(); - private final Map stateCreationLocks = new ConcurrentHashMap<>(); public AgentService(LLMConfig config) { @@ -81,6 +93,46 @@ private static String shorten(String input) { return input.substring(0, 150) + "... 已省略 ..." + input.substring(input.length() - 150); } + private static String parseQqMeme(String original) { + final Matcher qqFaceMatcher = QQ_FACE.matcher(original); + + if (qqFaceMatcher.matches()) { + original = qqFaceMatcher.replaceAll(r -> { + final String extBase64 = r.group(2); + final String ext = new String(Base64.getDecoder().decode(extBase64)); + final String text = JsonParser.parseString(ext).getAsJsonObject().get("text").getAsString(); + return "[表情:" + text + "]"; + }); + } + + final Matcher qqMemeMatcher = QQ_MEME.matcher(original); + + if (qqMemeMatcher.matches()) { + original = qqMemeMatcher.replaceAll("[表情]"); + } + + final Matcher qqMemeAltMatcher = QQ_MEME_ALT.matcher(original); + + if (qqMemeAltMatcher.matches()) { + original = qqMemeAltMatcher.replaceAll(r -> { + final String extBase64 = r.group(1); + final String ext = new String(Base64.getDecoder().decode(extBase64)); + final String text = JsonParser.parseString(ext).getAsJsonObject().get("text").getAsString(); + return "[表情:" + text + "]"; + }); + } + + return original; + } + + private static String processMessage(String original) { + try { + return parseQqMeme(original); + } catch (Exception e) { + return original; + } + } + public void recordHistory(String groupId, String sender, String message) { if (groupId == null || groupId.isEmpty() || sender == null || sender.isEmpty() @@ -88,7 +140,7 @@ public void recordHistory(String groupId, String sender, String message) { return; } - final String historyMessage = "<@" + sender + ">" + ": " + message; + final String historyMessage = "**<@" + sender + ">**" + ": " + processMessage(message); final Deque log = chatLog.computeIfAbsent(groupId, _ -> new ArrayDeque<>()); @@ -110,11 +162,20 @@ public void recordHistory(String groupId, String sender, String message) { } public String input(String groupId, String openId, String rawContent, Function contextFunc) { - return input(groupId, openId, rawContent, contextFunc, null); + return input(groupId, openId, rawContent, contextFunc, null, null); + } + + public String input( + String groupId, String openId, String rawContent, + Function contextFunc, StreamHandler handler + ) { + return input(groupId, openId, rawContent, contextFunc, handler, null); } public String input( - String groupId, String openId, String rawContent, Function contextFunc, StreamHandler handler + String groupId, String openId, String rawContent, + Function contextFunc, StreamHandler handler, + List attachments ) { final StateOwner owner = StateOwner.of(groupId, openId); final State state = getOrCreateState(owner); @@ -133,8 +194,10 @@ public String input( String query = ""; + final String message = processMessage(rawContent); + if (pendingMessages.isEmpty()) { - query = "<@" + openId + ">" + ": " + rawContent; + query = "**<@" + openId + ">**" + ": " + message; } else { if (pendingMessages.size() == CONTEXT_SIZE) { query += "====== ...历史消息较多已省略 ======"; @@ -144,10 +207,10 @@ public String input( + "====== 以上是最近的所有消息 ======\n" + "====== 以下是本次询问的内容 ======\n" + "\n" - + "<@" + openId + ">" + ": " + rawContent; + + "**<@" + openId + ">**" + ": " + message; } - recordHistory(groupId, openId, rawContent); + recordHistory(groupId, openId, message); state.resetIfNeeded(api, groupId); @@ -160,9 +223,9 @@ public String input( final String answer; try { if (handler != null) { - answer = api.chatQueryStreaming(groupId, state.conv.appConversationID(), query, handler); + answer = api.chatQueryStreaming(groupId, state.conv.appConversationID(), query, handler, attachments); } else { - var response = api.chatQuery(groupId, state.conv.appConversationID(), query); + var response = api.chatQuery(groupId, state.conv.appConversationID(), query, attachments); answer = response.answer(); } } catch (RuntimeException | Error e) { @@ -324,7 +387,6 @@ public void resetIfNeeded(Api api, String groupId) { } } - record StateOwner(String groupId, String openId) { public static StateOwner of( String groupId, @@ -432,7 +494,7 @@ public void clearConversation(String groupId, String appConvId) { } } - public ChatQueryResponse chatQuery(String groupId, String appConvId, String query) { + public ChatQueryResponse chatQuery(String groupId, String appConvId, String query, List attachments) { LOG.info("Running chat query for id {}", groupId); JsonObject body = new JsonObject(); @@ -441,6 +503,10 @@ public ChatQueryResponse chatQuery(String groupId, String appConvId, String quer body.addProperty("Query", query); body.addProperty("ResponseMode", "blocking"); + if (attachments != null) { + body.add("QueryExtends", GSON.toJsonTree(Map.of("Files", attachments))); + } + try { var request = newRequest("/api/proxy/api/v1/chat_query_v2") .POST(HttpRequest.BodyPublishers.ofString(body.toString())) @@ -467,7 +533,7 @@ public ChatQueryResponse chatQuery(String groupId, String appConvId, String quer } } - public String chatQueryStreaming(String groupId, String appConvId, String query, StreamHandler handler) { + public String chatQueryStreaming(String groupId, String appConvId, String query, StreamHandler handler, List attachments) { LOG.info("Running chat query for id {}", groupId); JsonObject body = new JsonObject(); @@ -476,6 +542,10 @@ public String chatQueryStreaming(String groupId, String appConvId, String query, body.addProperty("Query", query); body.addProperty("ResponseMode", "streaming"); + if (attachments != null) { + body.add("QueryExtends", GSON.toJsonTree(Map.of("Files", attachments))); + } + try { var request = newRequest("/api/proxy/api/v1/chat_query_v2") .POST(HttpRequest.BodyPublishers.ofString(body.toString())) diff --git a/src/main/java/xyz/zcraft/seira/api/ApiUtil.java b/src/main/java/xyz/zcraft/seira/api/ApiUtil.java new file mode 100644 index 00000000..ad86e174 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/api/ApiUtil.java @@ -0,0 +1,81 @@ +package xyz.zcraft.seira.api; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import xyz.zcraft.seira.api.data.RawResponse; + +import java.nio.charset.StandardCharsets; + +public class ApiUtil { + private static final Gson GSON = new Gson(); + + public static RuntimeException parseHttpError(String responseBody, int statusCode, String fallbackMessage) { + Integer errorCode = null; + String message = fallbackMessage; + try { + JsonObject root = GSON.fromJson(responseBody, JsonObject.class); + if (root != null) { + if (root.has("data") && root.get("data").isJsonObject()) { + JsonObject data = root.getAsJsonObject("data"); + errorCode = readCodeFromJsonObject(data); + } + if (errorCode == null) { + errorCode = readCodeFromJsonObject(root); + } + } + } catch (Exception ignored) { + } + + if (statusCode == 500) { + message += "(" + (errorCode == null ? "未知错误码" : errorCode) + " / HTTP " + statusCode + " / 发生了一个内部错误)"; + } + + return new ApiRequestException(errorCode, message); + } + + static Integer readCodeFromJsonObject(JsonObject object) { + if (object == null || !object.has("code") || !object.get("code").isJsonPrimitive()) { + return null; + } + try { + return object.get("code").getAsInt(); + } catch (Exception ignored) { + return null; + } + } + + static void ensureApiSuccess(RawResponse payload, String fallbackMessage) { + if (payload == null) { + throw new RuntimeException(fallbackMessage); + } + if (!payload.isSuccess()) { + Integer errorCode = extractErrorCode(payload); + String message = payload.getMessage() != null ? payload.getMessage() : fallbackMessage; + throw new ApiRequestException(errorCode, message); + } + } + + static RuntimeException parseHttpError(byte[] responseBody, int statusCode, String fallbackMessage) { + String bodyAsText = responseBody == null ? null : new String(responseBody, StandardCharsets.UTF_8); + return parseHttpError(bodyAsText, statusCode, fallbackMessage); + } + + static Integer extractErrorCode(RawResponse payload) { + if (payload.getData() != null && payload.getData().isJsonObject()) { + JsonObject data = payload.getData().getAsJsonObject(); + return readCodeFromJsonObject(data); + } + return null; + } + + static boolean codeNotOk(int statusCode) { + return statusCode < 200 || statusCode >= 300; + } + + static JsonObject requireDataObject(RawResponse payload, String message) { + if (payload.getData() == null || !payload.getData().isJsonObject()) { + throw new RuntimeException(message); + } + return payload.getData().getAsJsonObject(); + } +} diff --git a/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java b/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java new file mode 100644 index 00000000..b0641e89 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java @@ -0,0 +1,52 @@ +package xyz.zcraft.seira.api; + +import com.google.gson.Gson; +import xyz.zcraft.seira.Seira; +import xyz.zcraft.seira.api.data.MinecraftServerStatus; + +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; + +public class AsteroidApi { + private static final String ENDPOINT; + private static final String TOKEN; + private static final HttpClient CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofMinutes(5)).build(); + private static final Gson GSON = new Gson(); + + static { + ENDPOINT = Seira.getConfig().asteroid().endpoint(); + TOKEN = Seira.getConfig().asteroid().token(); + } + + private static HttpRequest.Builder requestBuilder(String path) { + HttpRequest.Builder builder = HttpRequest.newBuilder(); + if (TOKEN != null && !TOKEN.isBlank()) { + builder.header("Authorization", "Bearer " + TOKEN); + } + builder.uri(URI.create(ENDPOINT + path)); + return builder; + } + + public static MinecraftServerStatus getMinecraftServerStatus(String addr) { + try { + var request = requestBuilder("/minecraft/servers/" + URLEncoder.encode(addr, StandardCharsets.UTF_8) + "/status") + .GET() + .build(); + + final HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw ApiUtil.parseHttpError(response.body(), response.statusCode(), "获取 MC 服务器状态失败"); + } + + return GSON.fromJson(response.body(), MinecraftServerStatus.class); + } catch (Exception e) { + throw new RuntimeException("获取 MC 服务器状态失败", e); + } + } +} diff --git a/src/main/java/xyz/zcraft/seira/api/ApiHelper.java b/src/main/java/xyz/zcraft/seira/api/OstellaApi.java similarity index 84% rename from src/main/java/xyz/zcraft/seira/api/ApiHelper.java rename to src/main/java/xyz/zcraft/seira/api/OstellaApi.java index 76798886..caf476e3 100644 --- a/src/main/java/xyz/zcraft/seira/api/ApiHelper.java +++ b/src/main/java/xyz/zcraft/seira/api/OstellaApi.java @@ -25,7 +25,7 @@ import java.util.List; import java.util.Map; -public class ApiHelper { +public class OstellaApi { private static final String OSU_AUTHORIZATION_HEADER = "X-Osu-Authorization"; private static final String ENDPOINT; private static final String TOKEN; @@ -61,11 +61,11 @@ public static Response> getFollowed(String accessToken) { final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "获取多人房间失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "获取多人房间失败"); } final RawResponse r = GSON.fromJson(send.body(), RawResponse.class); - ensureApiSuccess(r, "获取多人房间失败"); + ApiUtil.ensureApiSuccess(r, "获取多人房间失败"); final JsonArray data = r.getData().getAsJsonArray(); LinkedList followed = new LinkedList<>(); @@ -92,11 +92,11 @@ public static Response getSelf(String accessToken) { final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "获取用户信息失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "获取用户信息失败"); } final RawResponse r = GSON.fromJson(send.body(), RawResponse.class); - ensureApiSuccess(r, "获取用户信息失败"); + ApiUtil.ensureApiSuccess(r, "获取用户信息失败"); final var data = r.getData().getAsJsonObject(); return Response.fromHeaders(send.headers()) @@ -143,11 +143,11 @@ public static UserExtended getUserRaw(long uid) { final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "获取用户信息失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "获取用户信息失败"); } final RawResponse r = GSON.fromJson(send.body(), RawResponse.class); - ensureApiSuccess(r, "获取用户信息失败"); + ApiUtil.ensureApiSuccess(r, "获取用户信息失败"); final var data = r.getData().getAsJsonObject(); return GSON.fromJson(data, UserExtended.class); @@ -195,11 +195,11 @@ public static String getDaily() { final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "获取每日挑战失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "获取每日挑战失败"); } final RawResponse r = GSON.fromJson(send.body(), RawResponse.class); - ensureApiSuccess(r, "获取每日挑战失败"); + ApiUtil.ensureApiSuccess(r, "获取每日挑战失败"); final JsonObject data = r.getData().getAsJsonObject(); String mods = null; @@ -235,11 +235,11 @@ public static Response getMultiplayerRoom(String accessToken) { final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "获取多人房间失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "获取多人房间失败"); } final RawResponse r = GSON.fromJson(send.body(), RawResponse.class); - ensureApiSuccess(r, "获取多人房间失败"); + ApiUtil.ensureApiSuccess(r, "获取多人房间失败"); final JsonObject data = r.getData().getAsJsonObject(); return Response.fromHeaders(send.headers()) @@ -319,11 +319,11 @@ public static Beatmapset getBeatmapsetRaw(long id) { final HttpResponse send = CLIENT.send(builder.build(), HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "获取谱面集失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "获取谱面集失败"); } final RawResponse r = GSON.fromJson(send.body(), RawResponse.class); - ensureApiSuccess(r, "获取谱面集失败"); + ApiUtil.ensureApiSuccess(r, "获取谱面集失败"); final JsonObject data = r.getData().getAsJsonObject(); return GSON.fromJson(data, Beatmapset.class); @@ -358,7 +358,7 @@ private static Response getBase64BytesResponse(String query, String final HttpResponse send = CLIENT.send(builder.build(), HttpResponse.BodyHandlers.ofByteArray()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), failMessage); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), failMessage); } byte[] imageBytes = send.body(); @@ -383,11 +383,11 @@ public static Response getLookupBeatmapsetResponse(long beatmapsetId, String final HttpResponse send = CLIENT.send(localRequest, HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "获取谱面集失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "获取谱面集失败"); } final RawResponse rawResponse = GSON.fromJson(send.body(), RawResponse.class); - ensureApiSuccess(rawResponse, "查找谱面集失败"); + ApiUtil.ensureApiSuccess(rawResponse, "查找谱面集失败"); final JsonObject data = rawResponse.getData().getAsJsonObject(); return Response.fromHeaders(send.headers()) @@ -408,11 +408,11 @@ public static Response> searchBeatmapSetResponse(SearchQu final var send = CLIENT.send(localRequest, HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "搜索谱面集失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "搜索谱面集失败"); } final RawResponse rawResponse = GSON.fromJson(send.body(), RawResponse.class); - ensureApiSuccess(rawResponse, "搜索谱面集失败"); + ApiUtil.ensureApiSuccess(rawResponse, "搜索谱面集失败"); final JsonArray data = rawResponse.getData().getAsJsonArray(); final LinkedList items = new LinkedList<>(); @@ -456,13 +456,13 @@ public static RandomScore getRandomScore() { .build(); HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); - if (codeNotOk(response.statusCode())) { - throw parseHttpError(response.body(), response.statusCode(), "获取随机成绩失败"); + if (ApiUtil.codeNotOk(response.statusCode())) { + throw ApiUtil.parseHttpError(response.body(), response.statusCode(), "获取随机成绩失败"); } RawResponse payload = GSON.fromJson(response.body(), RawResponse.class); - ensureApiSuccess(payload, "获取随机成绩失败"); - JsonObject data = requireDataObject(payload, "随机成绩响应缺少data"); + ApiUtil.ensureApiSuccess(payload, "获取随机成绩失败"); + JsonObject data = ApiUtil.requireDataObject(payload, "随机成绩响应缺少data"); if (!data.has("user") || !data.get("user").isJsonObject() || !data.has("score") || !data.get("score").isJsonObject()) { throw new RuntimeException("随机成绩响应缺少用户或成绩数据"); @@ -494,12 +494,12 @@ public static String getRandomScoreWeight(Long userId, JsonObject weights, boole .build(); HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); - if (codeNotOk(response.statusCode())) { - throw parseHttpError(response.body(), response.statusCode(), "获取成绩权重失败"); + if (ApiUtil.codeNotOk(response.statusCode())) { + throw ApiUtil.parseHttpError(response.body(), response.statusCode(), "获取成绩权重失败"); } RawResponse payload = GSON.fromJson(response.body(), RawResponse.class); - ensureApiSuccess(payload, "获取成绩权重失败"); + ApiUtil.ensureApiSuccess(payload, "获取成绩权重失败"); return payload.getData().getAsString(); } catch (IOException e) { @@ -527,13 +527,13 @@ public static RandomScore getRandomScoreFromUsers(List uids, JsonObject we .build(); HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); - if (codeNotOk(response.statusCode())) { - throw parseHttpError(response.body(), response.statusCode(), "获取随机成绩失败"); + if (ApiUtil.codeNotOk(response.statusCode())) { + throw ApiUtil.parseHttpError(response.body(), response.statusCode(), "获取随机成绩失败"); } RawResponse payload = GSON.fromJson(response.body(), RawResponse.class); - ensureApiSuccess(payload, "获取随机成绩失败"); - JsonObject data = requireDataObject(payload, "随机成绩响应缺少data"); + ApiUtil.ensureApiSuccess(payload, "获取随机成绩失败"); + JsonObject data = ApiUtil.requireDataObject(payload, "随机成绩响应缺少data"); if (!data.has("user") || !data.get("user").isJsonObject() || !data.has("score") || !data.get("score").isJsonObject()) { throw new RuntimeException("随机成绩响应缺少用户或成绩数据"); @@ -635,11 +635,11 @@ private static TimeDurationParser.TimeRange getScoreHighlight(String scoreId, in final var send = CLIENT.send(localRequest, HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "高光获取失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "高光获取失败"); } final RawResponse rawResponse = GSON.fromJson(send.body(), RawResponse.class); - ensureApiSuccess(rawResponse, "高光获取失败"); + ApiUtil.ensureApiSuccess(rawResponse, "高光获取失败"); final JsonObject data = rawResponse.getData().getAsJsonObject(); return new TimeDurationParser.TimeRange( @@ -700,10 +700,10 @@ private static JsonObject lookupTargetData(String query, String auth, String err var request = requestBuilder().uri(URI.create(ENDPOINT + query)).GET(); if (auth != null) withOsuAuthorization(request, auth); var response = CLIENT.send(request.build(), HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() != 200) throw parseHttpError(response.body(), response.statusCode(), error); + if (response.statusCode() != 200) throw ApiUtil.parseHttpError(response.body(), response.statusCode(), error); RawResponse payload = GSON.fromJson(response.body(), RawResponse.class); - ensureApiSuccess(payload, error); - return requireDataObject(payload, error); + ApiUtil.ensureApiSuccess(payload, error); + return ApiUtil.requireDataObject(payload, error); } catch (IOException | InterruptedException e) { throw requestFailure(e); } @@ -721,11 +721,11 @@ private static JsonObject lookupScoreData(String scoreId) { .build(); HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { - throw parseHttpError(response.body(), response.statusCode(), "获取本地成绩信息失败"); + throw ApiUtil.parseHttpError(response.body(), response.statusCode(), "获取本地成绩信息失败"); } RawResponse payload = GSON.fromJson(response.body(), RawResponse.class); - ensureApiSuccess(payload, "获取本地成绩信息失败"); - return requireDataObject(payload, "本地成绩响应缺少data"); + ApiUtil.ensureApiSuccess(payload, "获取本地成绩信息失败"); + return ApiUtil.requireDataObject(payload, "本地成绩响应缺少data"); } catch (IOException | InterruptedException e) { throw requestFailure(e); } @@ -736,13 +736,13 @@ private static ReplayTaskInfo getReplayTaskInfo(HttpRequest request) { try { HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); RawResponse payload = GSON.fromJson(response.body(), RawResponse.class); - if (codeNotOk(response.statusCode())) { - throw parseHttpError(response.body(), response.statusCode(), "回放渲染请求失败"); + if (ApiUtil.codeNotOk(response.statusCode())) { + throw ApiUtil.parseHttpError(response.body(), response.statusCode(), "回放渲染请求失败"); } - ensureApiSuccess(payload, "回放渲染请求失败"); + ApiUtil.ensureApiSuccess(payload, "回放渲染请求失败"); - JsonObject data = requireDataObject(payload, "回放渲染请求缺少任务信息"); + JsonObject data = ApiUtil.requireDataObject(payload, "回放渲染请求缺少任务信息"); if (!data.has("id") || data.get("id").isJsonNull()) { throw new RuntimeException("回放渲染请求缺少任务ID"); @@ -815,11 +815,11 @@ private static JsonObject getReplayStatus(String taskId) { .build(); HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); RawResponse payload = GSON.fromJson(response.body(), RawResponse.class); - if (codeNotOk(response.statusCode())) { - throw parseHttpError(response.body(), response.statusCode(), "查询回放渲染状态失败"); + if (ApiUtil.codeNotOk(response.statusCode())) { + throw ApiUtil.parseHttpError(response.body(), response.statusCode(), "查询回放渲染状态失败"); } - ensureApiSuccess(payload, "查询回放渲染状态失败"); - JsonObject data = requireDataObject(payload, "回放渲染状态响应缺少data"); + ApiUtil.ensureApiSuccess(payload, "查询回放渲染状态失败"); + JsonObject data = ApiUtil.requireDataObject(payload, "回放渲染状态响应缺少data"); if (!data.has("status") || data.get("status").isJsonNull()) { throw new RuntimeException("回放渲染状态响应缺少status"); } @@ -841,76 +841,6 @@ private static HttpRequest.BodyPublisher renderRequestBody(QqUploadRequest qqUpl return HttpRequest.BodyPublishers.ofString(body.toString(), StandardCharsets.UTF_8); } - private static void ensureApiSuccess(RawResponse payload, String fallbackMessage) { - if (payload == null) { - throw new RuntimeException(fallbackMessage); - } - if (!payload.isSuccess()) { - Integer errorCode = extractErrorCode(payload); - String message = payload.getMessage() != null ? payload.getMessage() : fallbackMessage; - throw new ApiRequestException(errorCode, message); - } - } - - private static RuntimeException parseHttpError(String responseBody, int statusCode, String fallbackMessage) { - Integer errorCode = null; - String message = fallbackMessage; - try { - JsonObject root = GSON.fromJson(responseBody, JsonObject.class); - if (root != null) { - if (root.has("data") && root.get("data").isJsonObject()) { - JsonObject data = root.getAsJsonObject("data"); - errorCode = readCodeFromJsonObject(data); - } - if (errorCode == null) { - errorCode = readCodeFromJsonObject(root); - } - } - } catch (Exception ignored) { - } - - if (statusCode == 500) { - message += "(" + (errorCode == null ? "未知错误码" : errorCode) + " / HTTP " + statusCode + " / 发生了一个内部错误)"; - } - - return new ApiRequestException(errorCode, message); - } - - private static RuntimeException parseHttpError(byte[] responseBody, int statusCode, String fallbackMessage) { - String bodyAsText = responseBody == null ? null : new String(responseBody, StandardCharsets.UTF_8); - return parseHttpError(bodyAsText, statusCode, fallbackMessage); - } - - private static Integer extractErrorCode(RawResponse payload) { - if (payload.getData() != null && payload.getData().isJsonObject()) { - JsonObject data = payload.getData().getAsJsonObject(); - return readCodeFromJsonObject(data); - } - return null; - } - - private static boolean codeNotOk(int statusCode) { - return statusCode < 200 || statusCode >= 300; - } - - private static Integer readCodeFromJsonObject(JsonObject object) { - if (object == null || !object.has("code") || !object.get("code").isJsonPrimitive()) { - return null; - } - try { - return object.get("code").getAsInt(); - } catch (Exception ignored) { - return null; - } - } - - private static JsonObject requireDataObject(RawResponse payload, String message) { - if (payload.getData() == null || !payload.getData().isJsonObject()) { - throw new RuntimeException(message); - } - return payload.getData().getAsJsonObject(); - } - public static RenderStat getRenderStat(String jobId) { try { HttpRequest request = requestBuilder() @@ -921,11 +851,11 @@ public static RenderStat getRenderStat(String jobId) { final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "获取渲染进度失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "获取渲染进度失败"); } final RawResponse r = GSON.fromJson(send.body(), RawResponse.class); - ensureApiSuccess(r, "获取渲染进度失败"); + ApiUtil.ensureApiSuccess(r, "获取渲染进度失败"); final JsonObject data = r.getData().getAsJsonObject(); return GSON.fromJson(data, RenderStat.class); @@ -942,12 +872,12 @@ public static RenderStat cancelReplayRender(String jobId) { .build(); HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); - if (codeNotOk(response.statusCode())) { - throw parseHttpError(response.body(), response.statusCode(), "取消回放渲染失败"); + if (ApiUtil.codeNotOk(response.statusCode())) { + throw ApiUtil.parseHttpError(response.body(), response.statusCode(), "取消回放渲染失败"); } RawResponse payload = GSON.fromJson(response.body(), RawResponse.class); - ensureApiSuccess(payload, "取消回放渲染失败"); - return GSON.fromJson(requireDataObject(payload, "取消回放渲染响应缺少data"), RenderStat.class); + ApiUtil.ensureApiSuccess(payload, "取消回放渲染失败"); + return GSON.fromJson(ApiUtil.requireDataObject(payload, "取消回放渲染响应缺少data"), RenderStat.class); } catch (IOException e) { throw requestFailure(e); } catch (InterruptedException e) { @@ -1000,11 +930,11 @@ public static Response> getScoreMissesResponse(String scoreId) { final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "获取 Miss 数据失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "获取 Miss 数据失败"); } final RawResponse r = GSON.fromJson(send.body(), RawResponse.class); - ensureApiSuccess(r, "获取 Miss 数据失败"); + ApiUtil.ensureApiSuccess(r, "获取 Miss 数据失败"); final JsonArray data = r.getData().getAsJsonArray(); List misses = new LinkedList<>(); @@ -1046,12 +976,12 @@ public static long getUserRank(long uid) { final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "获取玩家Rank失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "获取玩家Rank失败"); } final RawResponse response = GSON.fromJson(send.body(), RawResponse.class); - ensureApiSuccess(response, "获取玩家Rank失败"); - final JsonObject data = requireDataObject(response, "获取玩家Rank响应缺少用户数据"); + ApiUtil.ensureApiSuccess(response, "获取玩家Rank失败"); + final JsonObject data = ApiUtil.requireDataObject(response, "获取玩家Rank响应缺少用户数据"); return data.get("global_rank").getAsLong(); } catch (IOException e) { throw requestFailure(e); @@ -1074,12 +1004,12 @@ public static Response lookupUser(String username) { final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "查找玩家失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "查找玩家失败"); } final RawResponse response = GSON.fromJson(send.body(), RawResponse.class); - ensureApiSuccess(response, "查找玩家失败"); - final JsonObject data = requireDataObject(response, "查找玩家响应缺少用户数据"); + ApiUtil.ensureApiSuccess(response, "查找玩家失败"); + final JsonObject data = ApiUtil.requireDataObject(response, "查找玩家响应缺少用户数据"); return Response.fromHeaders(send.headers()) .content(GSON.fromJson(data, User.class)) @@ -1102,11 +1032,11 @@ public static List getUsers(List u) { final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200) { - throw parseHttpError(send.body(), send.statusCode(), "获取用户信息失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "获取用户信息失败"); } final RawResponse r = GSON.fromJson(send.body(), RawResponse.class); - ensureApiSuccess(r, "获取用户信息失败"); + ApiUtil.ensureApiSuccess(r, "获取用户信息失败"); final JsonArray data = r.getData().getAsJsonArray(); List users = new LinkedList<>(); @@ -1130,19 +1060,19 @@ public static ReplayUploadInfo uploadReplay(byte[] replayBytes) { final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (send.statusCode() != 200 && send.statusCode() != 404) { - throw parseHttpError(send.body(), send.statusCode(), "回放上传失败"); + throw ApiUtil.parseHttpError(send.body(), send.statusCode(), "回放上传失败"); } final RawResponse r = GSON.fromJson(send.body(), RawResponse.class); if (send.statusCode() == 404) { - final Integer errCode = extractErrorCode(r); + final Integer errCode = ApiUtil.extractErrorCode(r); if (errCode != null && errCode == ErrorCode.NO_SCORE_FOUND.getCode()) { throw new ApiRequestException(ErrorCode.NO_SCORE_FOUND.getCode(), "回放上传失败:无法获取对应的成绩"); } } - ensureApiSuccess(r, "回放上传失败"); + ApiUtil.ensureApiSuccess(r, "回放上传失败"); return GSON.fromJson(r.getData().getAsJsonObject(), ReplayUploadInfo.class); } catch (IOException | InterruptedException e) { diff --git a/src/main/java/xyz/zcraft/seira/api/data/MinecraftServerStatus.java b/src/main/java/xyz/zcraft/seira/api/data/MinecraftServerStatus.java new file mode 100644 index 00000000..39fb8582 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/api/data/MinecraftServerStatus.java @@ -0,0 +1,38 @@ +package xyz.zcraft.seira.api.data; + +import com.google.gson.JsonElement; + +import java.util.List; + +public record MinecraftServerStatus( + String host, + Integer port, + Long latency, + Status status +) { + public record Status( + Version version, + Players players, + String description, + JsonElement descriptionRaw, + String favicon + ){} + + public record Version( + String name, + Integer protocol + ) { + } + + public record Players( + Long max, + Long online, + List samples + ) { + public record Sample( + String id, + String name + ) { + } + } +} diff --git a/src/main/java/xyz/zcraft/seira/command/AttachmentHandler.java b/src/main/java/xyz/zcraft/seira/command/AttachmentHandler.java index 0d0b6998..8182e7c0 100644 --- a/src/main/java/xyz/zcraft/seira/command/AttachmentHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/AttachmentHandler.java @@ -2,7 +2,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import xyz.zcraft.seira.api.ApiHelper; +import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.data.ReplayUploadInfo; import xyz.zcraft.seira.bot.data.Attachment; import xyz.zcraft.seira.bot.data.PendingMessage; @@ -74,7 +74,7 @@ public void handleAttachment(Attachment attachment, Consumer msg throw new IllegalArgumentException("Replay file exceeds the 512 KiB limit"); } - final ReplayUploadInfo replayUploadInfo = ApiHelper.uploadReplay(bytes); + final ReplayUploadInfo replayUploadInfo = OstellaApi.uploadReplay(bytes); msgSender.accept(ReplyFactory.replayUploadMessage(replayUploadInfo)); } catch (InterruptedException e) { diff --git a/src/main/java/xyz/zcraft/seira/command/ReplayResultStore.java b/src/main/java/xyz/zcraft/seira/command/ReplayResultStore.java index ec8cdc0e..b4d7182e 100644 --- a/src/main/java/xyz/zcraft/seira/command/ReplayResultStore.java +++ b/src/main/java/xyz/zcraft/seira/command/ReplayResultStore.java @@ -1,13 +1,13 @@ package xyz.zcraft.seira.command; -import xyz.zcraft.seira.api.ApiHelper; +import xyz.zcraft.seira.api.OstellaApi; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; public final class ReplayResultStore { - private final ConcurrentMap results = new ConcurrentHashMap<>(); + private final ConcurrentMap results = new ConcurrentHashMap<>(); private static String requireTaskId(String taskId) { if (taskId == null || taskId.isBlank()) { @@ -16,11 +16,11 @@ private static String requireTaskId(String taskId) { return taskId; } - void put(String taskId, ApiHelper.ReplayRenderResult result) { + void put(String taskId, OstellaApi.ReplayRenderResult result) { results.put(requireTaskId(taskId), Objects.requireNonNull(result)); } - public ApiHelper.ReplayRenderResult get(String taskId) { + public OstellaApi.ReplayRenderResult get(String taskId) { return results.get(requireTaskId(taskId)); } diff --git a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java index db96b13f..be052fdf 100644 --- a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java +++ b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java @@ -3,7 +3,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; -import xyz.zcraft.seira.api.ApiHelper; +import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.ApiRequestException; import xyz.zcraft.seira.api.ReplayRenderException; import xyz.zcraft.seira.api.data.Base64Bytes; @@ -114,22 +114,22 @@ public QqUploadRequest createVideoUploadRequest(Context ctx) { return messageSender.createVideoUploadRequest(targetId, ctx.inGroup()); } - public ApiHelper.ReplayRenderResult waitForReplay(ApiHelper.ReplayTaskInfo taskInfo) { + public OstellaApi.ReplayRenderResult waitForReplay(OstellaApi.ReplayTaskInfo taskInfo) { return waitForReplay(taskInfo, -1); } - public ApiHelper.ReplayRenderResult waitForReplay(ApiHelper.ReplayTaskInfo taskInfo, long timeout) { + public OstellaApi.ReplayRenderResult waitForReplay(OstellaApi.ReplayTaskInfo taskInfo, long timeout) { if (taskInfo == null || taskInfo.taskId() == null || taskInfo.taskId().isBlank()) { throw new IllegalArgumentException("回放任务未返回有效请求ID,无法获取视频结果。"); } - ApiHelper.ReplayRenderResult result = ApiHelper.waitReplayVideo(taskInfo.taskId(), timeout); + OstellaApi.ReplayRenderResult result = OstellaApi.waitReplayVideo(taskInfo.taskId(), timeout); replayResults.put(taskInfo.taskId(), result); BotStat.incrementReplays(); return result; } - public PendingMessage replayVideoMessage(ApiHelper.ReplayRenderResult result) { + public PendingMessage replayVideoMessage(OstellaApi.ReplayRenderResult result) { if (result == null) { return PendingMessage.ofString("回放视频生成失败,请稍后重试。"); } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java index 8bccd198..37bbdca7 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java @@ -1,7 +1,7 @@ package xyz.zcraft.seira.command.handler; import org.jline.utils.Log; -import xyz.zcraft.seira.api.ApiHelper; +import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.data.Response; import xyz.zcraft.seira.api.data.SearchQuery; import xyz.zcraft.seira.api.data.SearchResultItem; @@ -48,8 +48,8 @@ public BeatmapCommandHandler( } public void handleDaily(Context ctx) { - try (var timing = taskCoordinator.beginRequest(ctx, "Daily Challenge")) { - var daily = ApiHelper.getDaily(); + try (var _ = taskCoordinator.beginRequest(ctx, "Daily Challenge")) { + var daily = OstellaApi.getDaily(); ctx.sendReply(PendingMessage.ofMarkdownRaw(daily)); } } @@ -73,19 +73,19 @@ public void handleM(Context ctx) { case SCORE -> scoreId = target.id(); case SET -> { beatmapsetId = Long.parseLong(target.id()); - beatmapId = ApiHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + beatmapId = OstellaApi.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = OstellaApi.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } - if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = OstellaApi.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); - var response = ApiHelper.getBeatmapResponse(beatmapId, (ctx.argumentCount() > target.consumedArgs() ? ctx.argument(target.consumedArgs()) : null)); + var response = OstellaApi.getBeatmapResponse(beatmapId, (ctx.argumentCount() > target.consumedArgs() ? ctx.argument(target.consumedArgs()) : null)); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.beatmapMessage(ctx, response))); } @@ -100,7 +100,7 @@ public void handleBma(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.BMA)); return; } - try (var timing = taskCoordinator.beginRequest(ctx, "Beatmap Analysis")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Beatmap Analysis")) { var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; Long beatmapId = previous == null ? null : previous.beatmapId(); Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); @@ -110,19 +110,19 @@ public void handleBma(Context ctx) { case SCORE -> scoreId = target.id(); case SET -> { beatmapsetId = Long.parseLong(target.id()); - beatmapId = ApiHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + beatmapId = OstellaApi.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = OstellaApi.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } - if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = OstellaApi.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); - var response = ApiHelper.getBeatmapAnalysisResponse(beatmapId, (ctx.argumentCount() > target.consumedArgs() ? ctx.argument(target.consumedArgs()) : null)); + var response = OstellaApi.getBeatmapAnalysisResponse(beatmapId, (ctx.argumentCount() > target.consumedArgs() ? ctx.argument(target.consumedArgs()) : null)); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.beatmapMessage(ctx, response))); } @@ -136,7 +136,7 @@ public void handleAp(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.AP)); return; } - try (var timing = taskCoordinator.beginRequest(ctx, "Audio Preview")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Audio Preview")) { var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; Long beatmapId = previous == null ? null : previous.beatmapId(); Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); @@ -148,16 +148,16 @@ public void handleAp(Context ctx) { case SET -> beatmapsetId = Long.parseLong(target.id()); case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapsetId = ApiHelper.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapsetId = OstellaApi.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } if (beatmapsetId == null) { - if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = OstellaApi.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标喵"); - beatmapsetId = ApiHelper.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); + beatmapsetId = OstellaApi.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); } history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(PendingMessage.ofVoiceUrl("https://b.ppy.sh/preview/" + beatmapsetId + ".mp3").doUpload(false)); @@ -205,24 +205,24 @@ public void handleBpv(Context ctx) { case SCORE -> scoreId = target.id(); case SET -> { beatmapsetId = Long.parseLong(target.id()); - beatmapId = ApiHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + beatmapId = OstellaApi.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = OstellaApi.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } - if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = OstellaApi.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); - var task = ApiHelper.createBeatmapPreviewTask(beatmapId, mods, range, qqUpload); + var task = OstellaApi.createBeatmapPreviewTask(beatmapId, mods, range, qqUpload); history.remember(ctx, beatmapsetId, beatmapId, scoreId); videoRenderRecord.updateRenderTask(ctx.senderUserId(), task.taskId()); ctx.sendReply(replyFactory.replayMessage(ctx, task)); - ApiHelper.ReplayRenderResult result; + OstellaApi.ReplayRenderResult result; try { result = taskCoordinator.waitForReplay(task); @@ -252,7 +252,7 @@ public void handleBgp(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.BGP)); return; } - try (var timing = taskCoordinator.beginRequest(ctx, "Background Preview")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Background Preview")) { var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; Long beatmapId = previous == null ? null : previous.beatmapId(); Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); @@ -262,19 +262,19 @@ public void handleBgp(Context ctx) { case SCORE -> scoreId = target.id(); case SET -> { beatmapsetId = Long.parseLong(target.id()); - beatmapId = ApiHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + beatmapId = OstellaApi.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = OstellaApi.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } - if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = OstellaApi.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); - var response = ApiHelper.getBeatmapBgResponse(beatmapId); + var response = OstellaApi.getBeatmapBgResponse(beatmapId); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.bgpMessage(ctx, response))); } @@ -289,7 +289,7 @@ public void handleDl(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + CommandUsage.DL)); return; } - try (var timing = taskCoordinator.beginRequest(ctx, "Download Beatmap")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Download Beatmap")) { var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; Long beatmapId = previous == null ? null : previous.beatmapId(); Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); @@ -301,18 +301,18 @@ public void handleDl(Context ctx) { case SET -> beatmapsetId = Long.parseLong(target.id()); case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapsetId = ApiHelper.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapsetId = OstellaApi.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } if (beatmapsetId == null) { - if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = OstellaApi.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标喵"); - beatmapsetId = ApiHelper.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); + beatmapsetId = OstellaApi.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); } - var response = ApiHelper.getLookupBeatmapsetResponse(beatmapsetId, accessTokenProvider.apply(ctx.senderUserId())); + var response = OstellaApi.getLookupBeatmapsetResponse(beatmapsetId, accessTokenProvider.apply(ctx.senderUserId())); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(replyFactory.dlMessage(ctx, response)); } @@ -327,7 +327,7 @@ public void handleMs(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "用法:/ms <谱面集ID 或 快捷查询>")); return; } - try (var timing = taskCoordinator.beginRequest(ctx, "Beatmapset")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Beatmapset")) { var previous = target.kind() == TargetInput.Kind.MEMORY ? remembered : null; Long beatmapId = previous == null ? null : previous.beatmapId(); Long beatmapsetId = previous == null ? null : previous.beatmapsetId(); @@ -339,18 +339,18 @@ public void handleMs(Context ctx) { case SET -> beatmapsetId = Long.parseLong(target.id()); case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapsetId = ApiHelper.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapsetId = OstellaApi.lookupMultiplayerBeatmapset(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } if (beatmapsetId == null) { - if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = OstellaApi.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标喵"); - beatmapsetId = ApiHelper.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); + beatmapsetId = OstellaApi.lookupBeatmapsetForBeatmap(beatmapId, accessTokenProvider.apply(ctx.senderUserId())); } - var response = ApiHelper.getBeatmapsetResponse(beatmapsetId); + var response = OstellaApi.getBeatmapsetResponse(beatmapsetId); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.beatmapsetMessage(ctx, response))); } @@ -362,8 +362,8 @@ public void handleSms(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "用法:/sms [#页数] <搜索关键字>")); return; } - try (var timing = taskCoordinator.beginRequest(ctx, "Search Beatmapset")) { - Response> searchResponse = ApiHelper.searchBeatmapSetResponse(searchQuery); + try (var _ = taskCoordinator.beginRequest(ctx, "Search Beatmapset")) { + Response> searchResponse = OstellaApi.searchBeatmapSetResponse(searchQuery); ctx.sendReply(replyFactory.searchMessage(ctx, searchResponse, searchQuery)); } } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java index 9ce9571c..ea5eff5d 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java @@ -1,7 +1,7 @@ package xyz.zcraft.seira.command.handler; import xyz.zcraft.osu.model.Beatmapset; -import xyz.zcraft.seira.api.ApiHelper; +import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.bot.MessageSender; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; @@ -46,8 +46,8 @@ public void handleU(Context context) { String player = resolver.player(context.argumentCount() == 0 ? null : context.argument(0), context.senderUserId()); try (var _ = taskCoordinator.beginRequest(context, "User Info")) { - long uid = ApiHelper.resolveUid(player); - var response = ApiHelper.getUserInfoResponse(uid); + long uid = OstellaApi.resolveUid(player); + var response = OstellaApi.getUserInfoResponse(uid); var completion = replyFactory.userInfoMessage(context, response); context.sendReply(taskCoordinator.imageMessage(response, completion)); } @@ -85,7 +85,7 @@ public void handleLuck(Context context) { try (var _ = taskCoordinator.beginRequest(context, "Luck")) { DailyLuck.Luck luck = DailyLuck.getLuck(context.senderUserId()); - Beatmapset mapset = ApiHelper.getBeatmapsetRaw(luck.dailyMapset()); + Beatmapset mapset = OstellaApi.getBeatmapsetRaw(luck.dailyMapset()); UploadedImage cover = messageSender.uploadImageToCos(mapset.getCovers().getCover()); context.sendReply(replyFactory.luckMessage(context, luck, mapset, cover)); } @@ -107,7 +107,7 @@ public void handleFaq(Context context) { } public void handleStat(Context context) { - context.sendReply(replyFactory.statusMessage(context, ApiHelper.getServerStatus())); + context.sendReply(replyFactory.statusMessage(context, OstellaApi.getServerStatus())); } public void handleUnknown(Context context) { diff --git a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java index b353b95e..f0829fd0 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java @@ -3,7 +3,7 @@ import xyz.zcraft.osu.model.MultiplayerRoom; import xyz.zcraft.osu.model.User; import xyz.zcraft.osu.model.UserExtended; -import xyz.zcraft.seira.api.ApiHelper; +import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.RomAIApi; import xyz.zcraft.seira.api.data.OsuToken; import xyz.zcraft.seira.api.data.Response; @@ -124,12 +124,12 @@ public void handleRomAI(Context ctx) { return; } - final UserExtended userRaw = ApiHelper.getUserRaw(boundUid); + final UserExtended userRaw = OstellaApi.getUserRaw(boundUid); username = userRaw.getUsername(); } else if (ctx.argumentCount() == 1) { final String player = resolver.player(ctx.argument(0), null); - final User content = ApiHelper.getUserRaw(ApiHelper.resolveUid(player)); + final User content = OstellaApi.getUserRaw(OstellaApi.resolveUid(player)); username = content.getUsername(); } else { ctx.sendReply(at(ctx) + "用法:/romai [@user]"); @@ -206,7 +206,7 @@ private void handleStart(Context ctx, int argumentOffset) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "由于未绑定账户,无法获取当前房间,请手动提供ID~")); return; } - final Response multiplayerRoom = ApiHelper.getMultiplayerRoom(osuToken.accessToken()); + final Response multiplayerRoom = OstellaApi.getMultiplayerRoom(osuToken.accessToken()); target = new RoomTarget(multiplayerRoom.getContent().getId(), MPVersion.LAZER, null); } else { MPVersion version = null; diff --git a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java index 594235e1..2187072a 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java @@ -2,7 +2,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import xyz.zcraft.seira.api.ApiHelper; +import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.data.RandomScore; import xyz.zcraft.seira.bot.data.MessageReference; import xyz.zcraft.seira.bot.data.PendingMessage; @@ -267,8 +267,8 @@ public void handleRankGuess(Context ctx) { if (resolver.looksLikeMention(argument)) { String player = resolver.player(argument, ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - rank = ApiHelper.getUserRank(uid); + long uid = OstellaApi.resolveUid(player); + rank = OstellaApi.getUserRank(uid); } else { rank = parseRank(argument); if (rank == null) { @@ -329,7 +329,7 @@ private void weight(Context ctx, boolean all, String target) { reply.append(at(ctx)).append("目前%s在本群权重为 `%.2f` (%s)\n".formatted(ref, probability.weight(), factors.isBlank() ? "基础权重" : factors)); reply.append("在本群 `%d` 名玩家中,%s被选中的概率为 `%.3f%%`\n".formatted(totalPlayer, ref, probability.chance() * 100)); - final String randomScoreWeight = ApiHelper.getRandomScoreWeight(boundUid, games.generateWeights(ctx.groupId()), all); + final String randomScoreWeight = OstellaApi.getRandomScoreWeight(boundUid, games.generateWeights(ctx.groupId()), all); reply.append("%s的成绩当前抽选概率:\n>".formatted(ref)).append(randomScoreWeight).append("\n"); @@ -406,7 +406,7 @@ private void wishScore(Context ctx, int index) { try { scoreId = Long.parseLong( - ApiHelper.lookupPlayerScore(boundUid, "bp", index, List.of(), null) + OstellaApi.lookupPlayerScore(boundUid, "bp", index, List.of(), null) ); } catch (Exception e) { LOG.error("Failed to lookup score id", e); @@ -450,9 +450,9 @@ private void start(Context ctx, boolean fromGroup) { ctx.sendReply(PendingMessage.ofMarkdownRaw("本群没有绑定的用户,无法开始游戏喵")); return; } - randomScore = ApiHelper.getRandomScoreFromUsers(uids, games.generateWeights(ctx.groupId())); + randomScore = OstellaApi.getRandomScoreFromUsers(uids, games.generateWeights(ctx.groupId())); } else { - randomScore = ApiHelper.getRandomScore(); + randomScore = OstellaApi.getRandomScore(); } Round round = Round.from(randomScore, activeMessageEnabled); @@ -473,7 +473,7 @@ private void start(Context ctx, boolean fromGroup) { ctx.sendReply(PendingMessage.ofMarkdownRaw(content)); - var renderTask = ApiHelper.createObscuredReplayRenderTask( + var renderTask = OstellaApi.createObscuredReplayRenderTask( round.scoreId(), taskCoordinator.createVideoUploadRequest(ctx) ); @@ -487,7 +487,7 @@ private void start(Context ctx, boolean fromGroup) { TimeUnit.SECONDS ); - ApiHelper.ReplayRenderResult replay = null; + OstellaApi.ReplayRenderResult replay = null; try { replay = taskCoordinator.waitForReplay(renderTask); } catch (Exception e) { diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java index 87835df2..db0d0fba 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java @@ -1,7 +1,7 @@ package xyz.zcraft.seira.command.handler; import org.jline.utils.Log; -import xyz.zcraft.seira.api.ApiHelper; +import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.data.VideoRenderRecord; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; @@ -86,30 +86,30 @@ public void handleR(Context ctx) { case SET -> { beatmapsetId = Long.parseLong(target.id()); String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = OstellaApi.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } if (scoreId == null) { if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupBeatmapScore(beatmapId, uid, List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupBeatmapScore(beatmapId, uid, List.of(), null); } var upload = taskCoordinator.createVideoUploadRequest(ctx); - var task = ApiHelper.createReplayRenderTask(scoreId, range, upload); + var task = OstellaApi.createReplayRenderTask(scoreId, range, upload); history.remember(ctx, beatmapsetId, beatmapId, scoreId); videoRenderRecord.updateRenderTask(ctx.senderUserId(), task.taskId()); ctx.sendReply(replyFactory.replayMessage(ctx, task)); - ApiHelper.ReplayRenderResult result; + OstellaApi.ReplayRenderResult result; try { result = taskCoordinator.waitForReplay(task); @@ -160,7 +160,7 @@ public void handleRsc(Context ctx) { String localScoreId = target.kind() != TargetInput.Kind.MEMORY ? target.kind() == TargetInput.Kind.SCORE ? target.id() : null - : remembered == null ? null : remembered.scoreId(); + : remembered.scoreId(); boolean localScore = localScoreId != null && localScoreId.startsWith("loc"); var participants = new java.util.LinkedHashSet(); if (!(localScore && extraUidArg == null)) { @@ -183,7 +183,7 @@ public void handleRsc(Context ctx) { participants.add(token.trim()); } else if (resolver.looksLikeMention(token)) { String player = resolver.player(token, ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); + long uid = OstellaApi.resolveUid(player); participants.add("u" + uid); } else { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "追加ID列表包含非法值。")); @@ -204,17 +204,17 @@ public void handleRsc(Context ctx) { case SCORE -> scoreId = target.id(); case SET -> { beatmapsetId = Long.parseLong(target.id()); - beatmapId = ApiHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + beatmapId = OstellaApi.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = OstellaApi.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } - if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = OstellaApi.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); var upload = taskCoordinator.createVideoUploadRequest(ctx); String[] scoreTargets = participants.toArray(String[]::new); @@ -225,12 +225,12 @@ public void handleRsc(Context ctx) { scoreTargets = ids.toArray(String[]::new); } - var task = ApiHelper.createReplayShowcaseTask(beatmapId, scoreTargets, upload); + var task = OstellaApi.createReplayShowcaseTask(beatmapId, scoreTargets, upload); history.remember(ctx, beatmapsetId, beatmapId, scoreId); videoRenderRecord.updateRenderTask(ctx.senderUserId(), task.taskId()); ctx.sendReply(replyFactory.replayMessage(ctx, task)); - ApiHelper.ReplayRenderResult result; + OstellaApi.ReplayRenderResult result; try { result = taskCoordinator.waitForReplay(task); @@ -270,7 +270,7 @@ public void handleRstat(Context ctx) { jobId = ctx.args()[0]; } - ApiHelper.ReplayRenderResult replayResult = replayResults.get(jobId); + OstellaApi.ReplayRenderResult replayResult = replayResults.get(jobId); if (replayResult != null) { PendingMessage video = replayResult.qqFile() != null ? PendingMessage.ofUploadedVideo(replayResult.qqFile(), replayResult.videoUrl()) @@ -281,7 +281,7 @@ public void handleRstat(Context ctx) { return; } - ctx.sendReply(replyFactory.replayStatMessage(ctx, jobId, ApiHelper.getRenderStat(jobId))); + ctx.sendReply(replyFactory.replayStatMessage(ctx, jobId, OstellaApi.getRenderStat(jobId))); } public void handleRcancel(Context ctx) { @@ -309,7 +309,7 @@ public void handleRcancel(Context ctx) { } } - var result = ApiHelper.cancelReplayRender(jobId); + var result = OstellaApi.cancelReplayRender(jobId); String status = Objects.toString(result.getStatus(), "unknown").toLowerCase(); String message = switch (status) { case "canceled" -> "回放渲染已取消。"; diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java index c5c7df08..c89182a7 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java @@ -1,6 +1,6 @@ package xyz.zcraft.seira.command.handler; -import xyz.zcraft.seira.api.ApiHelper; +import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.data.MissData; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; @@ -79,9 +79,9 @@ public void handleBp(Context ctx) { if (ctx.args().length == 0) { String player = resolver.player(null, ctx.senderUserId()); try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - long uid = ApiHelper.resolveUid(player); - String scoreId = ApiHelper.lookupPlayerScore(uid, "bp", 1, List.of(), null); - var response = ApiHelper.getScoreResponse(scoreId); + long uid = OstellaApi.resolveUid(player); + String scoreId = OstellaApi.lookupPlayerScore(uid, "bp", 1, List.of(), null); + var response = OstellaApi.getScoreResponse(scoreId); history.remember(ctx, null, null, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } @@ -104,8 +104,8 @@ public void handleBp(Context ctx) { var player = request.player(); var filters = request.filters(); try (var _ = taskCoordinator.beginRequest(ctx, "Best Scores")) { - long uid = ApiHelper.resolveUid(player); - var response = ApiHelper.getBoNResponse( + long uid = OstellaApi.resolveUid(player); + var response = OstellaApi.getBoNResponse( range.end(), range.start(), uid, @@ -124,9 +124,9 @@ public void handleRbp(Context ctx) { String player = resolver.player(ctx.argumentCount() == 1 ? ctx.argument(0) : null, ctx.senderUserId()); try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - long uid = ApiHelper.resolveUid(player); - String scoreId = ApiHelper.lookupPlayerScore(uid, "bp", ThreadLocalRandom.current().nextInt(200) + 1, List.of(), null); - var response = ApiHelper.getScoreResponse(scoreId); + long uid = OstellaApi.resolveUid(player); + String scoreId = OstellaApi.lookupPlayerScore(uid, "bp", ThreadLocalRandom.current().nextInt(200) + 1, List.of(), null); + var response = OstellaApi.getScoreResponse(scoreId); history.remember(ctx, null, null, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } @@ -136,9 +136,9 @@ public void handleRs(Context ctx, boolean includeFail) { if (ctx.args().length == 0) { String player = resolver.player(null, ctx.senderUserId()); try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - long uid = ApiHelper.resolveUid(player); - String scoreId = ApiHelper.lookupPlayerScore(uid, ctx.command(), 1, List.of(), null); - var response = ApiHelper.getScoreResponse(scoreId); + long uid = OstellaApi.resolveUid(player); + String scoreId = OstellaApi.lookupPlayerScore(uid, ctx.command(), 1, List.of(), null); + var response = OstellaApi.getScoreResponse(scoreId); history.remember(ctx, null, null, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } @@ -161,8 +161,8 @@ public void handleRs(Context ctx, boolean includeFail) { var player = request.player(); var filters = request.filters(); try (var _ = taskCoordinator.beginRequest(ctx, "Recent Score")) { - long uid = ApiHelper.resolveUid(player); - var response = ApiHelper.getRecentResponse( + long uid = OstellaApi.resolveUid(player); + var response = OstellaApi.getRecentResponse( range.end(), range.start(), uid, @@ -182,8 +182,8 @@ public void handleTb(Context ctx) { String player = resolver.player(request.target(), ctx.senderUserId()); try (var _ = taskCoordinator.beginRequest(ctx, "Recent Best Scores")) { - long uid = ApiHelper.resolveUid(player); - var response = ApiHelper.getTodayBestResponse(uid, request.days()); + long uid = OstellaApi.resolveUid(player); + var response = OstellaApi.getTodayBestResponse(uid, request.days()); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.tbMessage(ctx, response))); } } @@ -206,9 +206,9 @@ private void handleFilteredSingleScore(Context ctx, String macroType) { if (targetUser == null) targetUser = resolver.player(null, ctx.senderUserId()); try (var _ = taskCoordinator.beginRequest(ctx, "Score")) { - long uid = ApiHelper.resolveUid(targetUser); - String scoreId = ApiHelper.lookupPlayerScore(uid, macroType, 1, filters.filters(), null); - var response = ApiHelper.getScoreResponse(scoreId); + long uid = OstellaApi.resolveUid(targetUser); + String scoreId = OstellaApi.lookupPlayerScore(uid, macroType, 1, filters.filters(), null); + var response = OstellaApi.getScoreResponse(scoreId); history.remember(ctx, null, null, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } @@ -261,30 +261,30 @@ public void handleS(Context ctx) { case SET -> { beatmapsetId = Long.parseLong(target.id()); String player = userOverride == null ? resolver.player(null, ctx.senderUserId()) : userOverride; - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, filters, mod); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, filters, mod); selectedPlayerScore = true; } case RS, RP, BP -> { String player = userOverride == null ? resolver.player(target.player(), ctx.senderUserId()) : userOverride; - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), filters, mod); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupPlayerScore(uid, target.scoreList(), target.index(), filters, mod); selectedPlayerScore = true; } - case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = OstellaApi.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } if ((userOverride != null || mod != null) && scoreId != null && !selectedPlayerScore) { - if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null) beatmapId = OstellaApi.getScoreBeatmapId(scoreId); scoreId = null; } if (scoreId == null) { if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); String player = userOverride == null ? resolver.player(null, ctx.senderUserId()) : userOverride; - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupBeatmapScore(beatmapId, uid, filters, mod); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupBeatmapScore(beatmapId, uid, filters, mod); } - var response = ApiHelper.getScoreResponse(scoreId); + var response = OstellaApi.getScoreResponse(scoreId); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } catch (Exception e) { @@ -339,22 +339,22 @@ public void handleSm(Context ctx) { case SCORE -> scoreId = target.id(); case SET -> { beatmapsetId = Long.parseLong(target.id()); - beatmapId = ApiHelper.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); + beatmapId = OstellaApi.lookupBeatmapInSet(beatmapsetId, target.index(), accessTokenProvider.apply(ctx.senderUserId())); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = OstellaApi.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } - if (beatmapId == null && scoreId != null) beatmapId = ApiHelper.getScoreBeatmapId(scoreId); + if (beatmapId == null && scoreId != null) beatmapId = OstellaApi.getScoreBeatmapId(scoreId); if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); String player = userOverride == null ? resolver.player(null, ctx.senderUserId()) : userOverride; - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupBeatmapScore(beatmapId, uid, filters, mod); - var response = ApiHelper.getScoreResponse(scoreId); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupBeatmapScore(beatmapId, uid, filters, mod); + var response = OstellaApi.getScoreResponse(scoreId); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreMessage(ctx, response))); } catch (Exception e) { @@ -384,24 +384,24 @@ public void handleSa(Context ctx) { case SET -> { beatmapsetId = Long.parseLong(target.id()); String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = OstellaApi.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } if (scoreId == null) { if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupBeatmapScore(beatmapId, uid, List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupBeatmapScore(beatmapId, uid, List.of(), null); } - var response = ApiHelper.getScoreAnalyzeResponse(scoreId); + var response = OstellaApi.getScoreAnalyzeResponse(scoreId); history.remember(ctx, beatmapsetId, beatmapId, scoreId); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.scoreAnalyzeMessage(ctx, response))); } @@ -433,24 +433,24 @@ public void handleMa(Context ctx) { case SET -> { beatmapsetId = Long.parseLong(target.id()); String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupBeatmapsetScore(beatmapsetId, target.index(), uid, List.of(), null); } case RS, RP, BP -> { String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupPlayerScore(uid, target.scoreList(), target.index(), List.of(), null); } - case MP -> beatmapId = ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> beatmapId = OstellaApi.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> {} } if (scoreId == null) { if (beatmapId == null) throw new ResolutionException("请指定指令目标谱面喵"); String player = resolver.player(target.player(), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - scoreId = ApiHelper.lookupBeatmapScore(beatmapId, uid, List.of(), null); + long uid = OstellaApi.resolveUid(player); + scoreId = OstellaApi.lookupBeatmapScore(beatmapId, uid, List.of(), null); } - var response = ApiHelper.getScoreMissesResponse(scoreId); + var response = OstellaApi.getScoreMissesResponse(scoreId); history.remember(ctx, beatmapsetId, beatmapId, scoreId); List misses = response.getContent(); if (index == null && misses.size() != 1) { @@ -466,7 +466,7 @@ public void handleMa(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "Miss序号不在范围内喵(1~" + misses.size() + ")")); return; } - var image = ApiHelper.getMissVisualizeResponse(scoreId, selectedIndex); + var image = OstellaApi.getMissVisualizeResponse(scoreId, selectedIndex); ctx.sendReply(taskCoordinator.imageMessage(image, replyFactory.missImageMessage(ctx, scoreId, selectedIndex, misses.size()))); } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java index 08b9de4c..9a1006ce 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java @@ -2,7 +2,7 @@ import xyz.zcraft.osu.model.User; import xyz.zcraft.osu.model.UserExtended; -import xyz.zcraft.seira.api.ApiHelper; +import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.data.FriendEntry; import xyz.zcraft.seira.api.data.OsuToken; import xyz.zcraft.seira.api.data.Response; @@ -62,7 +62,7 @@ public void handleMp(Context ctx) { } try (var _ = taskCoordinator.beginRequest(ctx, "Multiplayer Room")) { - var response = ApiHelper.getMultiplayerRoom(token.accessToken()); + var response = OstellaApi.getMultiplayerRoom(token.accessToken()); ctx.sendReply(replyFactory.mpMessage(ctx, response)); } } @@ -95,19 +95,19 @@ public void handleFriendStatus(Context ctx) { } String player = resolver.player(ctx.argument(0), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); + long uid = OstellaApi.resolveUid(player); final String mention = UserDataStore.findGroupOpenIdByUid(ctx.groupId(), uid) .map(ReplyFactory::at) .orElse(""); ctx.sendReply(PendingMessage.ofMarkdownRaw(mention + ": [%d](%s)".formatted(uid, "https://osu.ppy.sh/users/" + uid))); - final UserExtended targetUser = ApiHelper.getUserRaw(uid); + final UserExtended targetUser = OstellaApi.getUserRaw(uid); final String targetOsuAvatar = targetUser.getAvatarUrl(); boolean selfFollowed; final AtomicReference targetFollowed = new AtomicReference<>(); final OsuToken selfToken = authHelper.updateTokenAndGet(ctx.senderUserId()); - final var selfUser = ApiHelper.getSelf(selfToken.accessToken()).getContent(); + final var selfUser = OstellaApi.getSelf(selfToken.accessToken()).getContent(); final String selfOsuAvatar = selfUser.getAvatarUrl(); if (targetUser.getId() == selfUser.getId()) { @@ -115,7 +115,7 @@ public void handleFriendStatus(Context ctx) { return; } - final List selfFollowedList = ApiHelper.getFollowed(selfToken.accessToken()).getContent(); + final List selfFollowedList = OstellaApi.getFollowed(selfToken.accessToken()).getContent(); updateFriends(selfId, selfFollowedList); final Set users = new HashSet<>(selfFollowedList.stream().map(FriendEntry::user).toList()); @@ -136,7 +136,7 @@ public void handleFriendStatus(Context ctx) { final List targetFollowedList; final OsuToken target = authHelper.updateTokenAndGet(targetOpenId); if (target != null) { - targetFollowedList = ApiHelper.getFollowed(target.accessToken()).getContent(); + targetFollowedList = OstellaApi.getFollowed(target.accessToken()).getContent(); targetFollowed.set(targetFollowedList.stream().anyMatch(e -> e.user().getId() == selfId)); users.addAll(targetFollowedList.stream().map(FriendEntry::user).toList()); } @@ -168,8 +168,8 @@ public void handleFriendList(Context ctx, boolean all) { } try (var _ = taskCoordinator.beginRequest(ctx, "Friend List")) { - final Response self = ApiHelper.getSelf(token.accessToken()); - final Response> response = ApiHelper.getFollowed(token.accessToken()); + final Response self = OstellaApi.getSelf(token.accessToken()); + final Response> response = OstellaApi.getFollowed(token.accessToken()); final List friendEntries = response.getContent(); final Predicate filter; @@ -269,7 +269,7 @@ public void handleLb(Context ctx) { } try (var _ = taskCoordinator.beginRequest(ctx, "Leaderboard")) { - var response = ApiHelper.getLeaderboardResponse(groupBoundUids); + var response = OstellaApi.getLeaderboardResponse(groupBoundUids); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.lbMessage(ctx, response))); } return; @@ -281,7 +281,7 @@ public void handleLb(Context ctx) { } try (var _ = taskCoordinator.beginRequest(ctx, "Leaderboard")) { - var response = ApiHelper.getLeaderboardResponse(List.of(uid)); + var response = OstellaApi.getLeaderboardResponse(List.of(uid)); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.lbMessage(ctx, response))); } } else if (ctx.args().length == 1 || ctx.args().length == 2) { @@ -323,17 +323,17 @@ public void handleLb(Context ctx) { try (var _ = taskCoordinator.beginRequest(ctx, "Map Leaderboard")) { long beatmapId = switch (target.kind()) { case ID, MAP -> Long.parseLong(target.id()); - case SCORE -> ApiHelper.getScoreBeatmapId(target.id()); - case SET -> ApiHelper.lookupBeatmapInSet(Long.parseLong(target.id()), target.index(), + case SCORE -> OstellaApi.getScoreBeatmapId(target.id()); + case SET -> OstellaApi.lookupBeatmapInSet(Long.parseLong(target.id()), target.index(), accessTokenProvider.apply(ctx.senderUserId())); case RS, RP, BP -> { - long uid = ApiHelper.resolveUid(resolver.player(target.player(), ctx.senderUserId())); - yield ApiHelper.lookupPlayerScoreBeatmap(uid, target.scoreList(), target.index(), accessTokenProvider.apply(ctx.senderUserId())); + long uid = OstellaApi.resolveUid(resolver.player(target.player(), ctx.senderUserId())); + yield OstellaApi.lookupPlayerScoreBeatmap(uid, target.scoreList(), target.index(), accessTokenProvider.apply(ctx.senderUserId())); } - case MP -> ApiHelper.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); + case MP -> OstellaApi.lookupMultiplayerBeatmap(accessTokenProvider.apply(ctx.senderUserId())); case MEMORY -> throw new ResolutionException("请指定指令目标谱面喵"); }; - var response = ApiHelper.getGroupLeaderboardResponse(beatmapId, uids); + var response = OstellaApi.getGroupLeaderboardResponse(beatmapId, uids); ctx.sendReply(taskCoordinator.imageMessage(response, replyFactory.lbMessage(ctx, response))); } } else { @@ -347,8 +347,8 @@ public void handleSup(Context ctx) { return; } String player = resolver.player(ctx.argumentCount() == 0 ? null : ctx.argument(0), ctx.senderUserId()); - long uid = ApiHelper.resolveUid(player); - final UserExtended user = ApiHelper.getUserRaw(uid); + long uid = OstellaApi.resolveUid(player); + final UserExtended user = OstellaApi.getUserRaw(uid); final String openId = UserDataStore.findGroupOpenIdByUid(ctx.groupId(), user.getId()).orElse(null); ctx.sendReply(replyFactory.supMessage(ctx, user.getUsername(), openId, user.isSupporter(), user.getHasSupported(), user.getSupportLevel())); diff --git a/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java index ebc9241b..7ab1f4b5 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java @@ -1,7 +1,7 @@ package xyz.zcraft.seira.command.handler; import xyz.zcraft.osu.model.User; -import xyz.zcraft.seira.api.ApiHelper; +import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.command.ResolutionException; @@ -133,7 +133,7 @@ private void handleAdd(Context ctx) { } Long userId = UserDataStore.findBoundUid(mentionedUser); if (userId == null) throw new ResolutionException("被@的用户还没有绑定玩家ID,请先让对方使用 /bind。"); - User user = ApiHelper.getUsers(List.of(userId)).stream() + User user = OstellaApi.getUsers(List.of(userId)).stream() .filter(candidate -> candidate.getId() == userId) .findFirst().orElseThrow(() -> new ResolutionException("未找到指定的玩家。")); UserDataStore.storeUserInfo(user.getId(), user.getUsername()); @@ -183,7 +183,7 @@ private void handleDelete(Context ctx) { return; } - try (var timing = taskCoordinator.beginRequest(ctx, "Delete Score Watch")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Delete Score Watch")) { WatchTarget target = lookupGroupPlayer(ctx.groupId(), targetArgument); ctx.sendReply(removedMessage(watchService.remove(ctx.groupId(), target.userId()))); } @@ -191,8 +191,8 @@ private void handleDelete(Context ctx) { private WatchTarget lookupGroupPlayer(String groupId, String argument) { Long uid = resolver.parsePositiveLong(argument); - User user = uid == null ? ApiHelper.lookupUser(argument).getContent() - : ApiHelper.getUsers(List.of(uid)).stream() + User user = uid == null ? OstellaApi.lookupUser(argument).getContent() + : OstellaApi.getUsers(List.of(uid)).stream() .filter(candidate -> candidate.getId() == uid) .findFirst().orElseThrow(() -> new ResolutionException("未找到指定的玩家。")); String openId = UserDataStore.findGroupOpenIdByUid(groupId, user.getId()) diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index 363fd739..4e0ceb92 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -5,7 +5,7 @@ import com.google.gson.JsonObject; import org.jetbrains.annotations.NotNull; import xyz.zcraft.osu.model.*; -import xyz.zcraft.seira.api.ApiHelper; +import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.data.*; import xyz.zcraft.seira.bot.data.Button; import xyz.zcraft.seira.bot.data.PendingMessage; @@ -317,7 +317,7 @@ public PendingMessage lbMessage(Context ctx, Response response) { ); } - public PendingMessage replayMessage(Context ctx, ApiHelper.ReplayTaskInfo taskInfo) { + public PendingMessage replayMessage(Context ctx, OstellaApi.ReplayTaskInfo taskInfo) { return PendingMessage.ofMarkdownRaw( Contents.replayTaskContent(ctx, taskInfo), buttons().replayProgressButtons(taskInfo.taskId(), ctx.senderUserId()) @@ -424,7 +424,7 @@ public PendingMessage scoreMissesMessage(Context ctx, Response> s ); } - public PendingMessage statusMessage(Context ctx, ApiHelper.ServerStatus status) { + public PendingMessage statusMessage(Context ctx, OstellaApi.ServerStatus status) { return PendingMessage.ofMarkdownRaw( Contents.statContent(ctx, status), null ); @@ -464,7 +464,7 @@ public PendingMessage supMessage(Context ctx, String username, String openId, Bo } private static final class Contents { - static String replayTaskContent(Context ctx, ApiHelper.ReplayTaskInfo taskInfo) { + static String replayTaskContent(Context ctx, OstellaApi.ReplayTaskInfo taskInfo) { StringBuilder sb = new StringBuilder(); sb.append(at(ctx)).append("回放生成请求已提交").append("\n"); @@ -720,7 +720,7 @@ public static String scoreMissesContent(Context ctx, Response> sc return sb.toString().trim(); } - public static String statContent(Context ctx, ApiHelper.ServerStatus status) { + public static String statContent(Context ctx, OstellaApi.ServerStatus status) { String stat = at(ctx) + "\n" + "## 服务器状态\n" + "> 消息网关: ✅ 正常\n" + diff --git a/src/main/java/xyz/zcraft/seira/command/route/DebugRoutes.java b/src/main/java/xyz/zcraft/seira/command/route/DebugRoutes.java index a90d703c..6ce50ce2 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/DebugRoutes.java +++ b/src/main/java/xyz/zcraft/seira/command/route/DebugRoutes.java @@ -4,7 +4,7 @@ import org.apache.logging.log4j.Logger; import org.bouncycastle.util.encoders.Base64Encoder; import xyz.zcraft.osu.model.UserExtended; -import xyz.zcraft.seira.api.ApiHelper; +import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.data.FriendEntry; import xyz.zcraft.seira.api.data.OsuToken; import xyz.zcraft.seira.api.data.Response; @@ -148,8 +148,8 @@ public void handleUpdateUserInfo(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "用户信息更新失败")); return; } - try (var timing = taskCoordinator.beginRequest(ctx, "Update All User Info")) { - var users = ApiHelper.getUsers(allUsers); + try (var _ = taskCoordinator.beginRequest(ctx, "Update All User Info")) { + var users = OstellaApi.getUsers(allUsers); for (var user : users) { UserDataStore.storeUserInfo(user.getId(), user.getUsername()); } @@ -158,7 +158,7 @@ public void handleUpdateUserInfo(Context ctx) { } public void handleGetAllFriends(Context ctx) { - try (var timing = taskCoordinator.beginRequest(ctx, "Get All Friends")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Get All Friends")) { try { final List allOsuTokens = UserDataStore.getAllOsuTokens(); allOsuTokens @@ -167,8 +167,8 @@ public void handleGetAllFriends(Context ctx) { .map(authHelper::updateTokenAndGet) .map(OsuToken::accessToken) .forEach(accessToken -> { - final Response self = ApiHelper.getSelf(accessToken); - final Response> response = ApiHelper.getFollowed(accessToken); + final Response self = OstellaApi.getSelf(accessToken); + final Response> response = OstellaApi.getFollowed(accessToken); final List content = response.getContent(); final List ids = content.stream().map(e -> e.user().getId()).toList(); @@ -207,7 +207,7 @@ public void handleGetAllFriends(Context ctx) { } public void handleValidateToken(Context ctx) { - try (var timing = taskCoordinator.beginRequest(ctx, "Validate Token")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Validate Token")) { int updated = 0, removed = 0; try { final List allOsuTokens = UserDataStore.getAllOsuTokens(); diff --git a/src/main/java/xyz/zcraft/seira/config/AppConfig.java b/src/main/java/xyz/zcraft/seira/config/AppConfig.java index 80e9ef65..a1a7264b 100644 --- a/src/main/java/xyz/zcraft/seira/config/AppConfig.java +++ b/src/main/java/xyz/zcraft/seira/config/AppConfig.java @@ -8,7 +8,8 @@ public record AppConfig( CosConfig cos, DiscordConfig discord, BridgeConfig bridge, - LLMConfig llm + LLMConfig llm, + AsteroidConfig asteroid ) { public AppConfig { discord = discord == null ? DiscordConfig.disabled() : discord; diff --git a/src/main/java/xyz/zcraft/seira/config/AsteroidConfig.java b/src/main/java/xyz/zcraft/seira/config/AsteroidConfig.java new file mode 100644 index 00000000..57b0ab9e --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/config/AsteroidConfig.java @@ -0,0 +1,7 @@ +package xyz.zcraft.seira.config; + +public record AsteroidConfig( + String endpoint, + String token +) { +} diff --git a/src/main/java/xyz/zcraft/seira/config/RuntimeConfig.java b/src/main/java/xyz/zcraft/seira/config/RuntimeConfig.java index 75cd434b..a040769d 100644 --- a/src/main/java/xyz/zcraft/seira/config/RuntimeConfig.java +++ b/src/main/java/xyz/zcraft/seira/config/RuntimeConfig.java @@ -55,7 +55,8 @@ private static AppConfig mergeReloadable(AppConfig previous, AppConfig loaded) { previous.cos(), previous.discord(), previous.bridge(), - previous.llm() + previous.llm(), + previous.asteroid() ); } diff --git a/src/main/resources/seira-example-config.yml b/src/main/resources/seira-example-config.yml index 8ccd1037..b3c9d8e1 100644 --- a/src/main/resources/seira-example-config.yml +++ b/src/main/resources/seira-example-config.yml @@ -76,3 +76,8 @@ llm: # LLM API 的地址 baseUrl: "http://localhost:8723" apiKey: "" + +asteroid: + # asteroid API 的地址 + endpoint: "http://localhost:8728" + token: From 209efad9356eeb4db5491a453e8353089fbee829 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 27 Sep 2026 16:18:16 +0800 Subject: [PATCH 73/96] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20/mc=20=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=EF=BC=8C=E6=94=AF=E6=8C=81=E8=8E=B7=E5=8F=96=E6=8C=87?= =?UTF-8?q?=E5=AE=9A=20Minecraft=20=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=8A=B6?= =?UTF-8?q?=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + .../xyz/zcraft/seira/api/AsteroidApi.java | 6 ++- .../handler/GeneralCommandHandler.java | 45 +++++++++++++++++++ .../zcraft/seira/command/route/Router.java | 1 + 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c605db2..cf2cf30c 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ Seira正在活跃开发中,在使用的过程中可能会有一些Bug,也会 | `/watch` | `/watch add/del/list [目标]` | 添加/删除/列出监视任务 | | `/mpwatch` | `/mpwatch start/stop/status [目标]` | 按群成员添加、停止或查看多人房间监视;`stop all` 可停止本群全部监视 | | `/romai` | `/romai [目标]` | 开始监视自己或目标正在进行的RomAI比赛 | +| `/mc` | `/mc <服务器地址>` | 获取指定MC服务器状态 | | `/wx` | `/wx start <谱面ID列表>` / `/wx stop` | 监视指定玩家在指定谱面取得的成绩,重启后自动恢复 | | `/dcs` | `/dcs start .` / `/dcs stop` | 开启或解除当前 QQ 群与 Discord 频道的双向消息同步 | | `/stat` | `/stat` | 服务状态和统计信息文本 | diff --git a/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java b/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java index b0641e89..5ccf8256 100644 --- a/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java +++ b/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java @@ -3,6 +3,7 @@ import com.google.gson.Gson; import xyz.zcraft.seira.Seira; import xyz.zcraft.seira.api.data.MinecraftServerStatus; +import xyz.zcraft.seira.api.data.RawResponse; import java.net.URI; import java.net.URLEncoder; @@ -44,7 +45,10 @@ public static MinecraftServerStatus getMinecraftServerStatus(String addr) { throw ApiUtil.parseHttpError(response.body(), response.statusCode(), "获取 MC 服务器状态失败"); } - return GSON.fromJson(response.body(), MinecraftServerStatus.class); + final RawResponse r = GSON.fromJson(response.body(), RawResponse.class); + ApiUtil.ensureApiSuccess(r, "获取 MC 服务器状态失败"); + + return GSON.fromJson(r.getData(), MinecraftServerStatus.class); } catch (Exception e) { throw new RuntimeException("获取 MC 服务器状态失败", e); } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java index ea5eff5d..1dc39436 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java @@ -1,7 +1,9 @@ package xyz.zcraft.seira.command.handler; import xyz.zcraft.osu.model.Beatmapset; +import xyz.zcraft.seira.api.AsteroidApi; import xyz.zcraft.seira.api.OstellaApi; +import xyz.zcraft.seira.api.data.MinecraftServerStatus; import xyz.zcraft.seira.bot.MessageSender; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; @@ -142,4 +144,47 @@ public void handleNotice(Context context) { context.sendReply(PendingMessage.ofMarkdownRaw(sb.toString())); } + + public void handleMc(Context ctx) { + if (ctx.argumentCount() != 1) { + ctx.sendReply(at(ctx) + "用法:/mc <服务器地址>"); + return; + } + + try { + final String address = ctx.argument(0); + + final var probe = AsteroidApi.getMinecraftServerStatus(address); + + final var status = probe.status(); + final var players = status.players(); + final var samples = players.samples(); + + String playersSample = ""; + + if (samples != null) { + playersSample = String.join(", ", samples.stream().map(MinecraftServerStatus.Players.Sample::name).toList()); + } + + ctx.sendReply(at(ctx) + """ + `%s` 的服务器状态: + - 版本: `%s` + - 描述: `%s` + - 延迟: `%d` ms + - 在线人数: `%d` / `%d` + - 在线玩家: [%s] + """.formatted( + address, + status.version().name(), + status.description(), + probe.latency(), + players.online(), + players.max(), + playersSample + ) + ); + } catch (Exception e) { + ctx.sendReply(at(ctx) + "状态获取失败了喵,请稍后再试。"); + } + } } diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index 2f376168..59e77cc2 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -173,6 +173,7 @@ private static CommandRegistry createCommandRegistry( .register(rankGuessCommands::handleRankGuess, "rg") .register(generalCommands::handleNotice, "notice") .register(aiChatHandler::handleAi, "ai") + .register(generalCommands::handleMc, "mc") .build(); } From 6d012ac693edcd84f63fdd030d4eb70df6938bc4 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 27 Sep 2026 16:40:37 +0800 Subject: [PATCH 74/96] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20/@=20=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=EF=BC=8C=E6=94=AF=E6=8C=81=E8=8E=B7=E5=8F=96=E8=87=AA?= =?UTF-8?q?=E5=B7=B1=E6=88=96=E6=8C=87=E5=AE=9A=E7=94=A8=E6=88=B7=E7=9A=84?= =?UTF-8?q?=E6=96=87=E5=AD=97=E7=89=88=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + .../handler/GeneralCommandHandler.java | 11 ++++++++ .../zcraft/seira/command/parse/Resolver.java | 13 +++++++--- .../seira/command/reply/ReplyFactory.java | 25 +++++++++++++++++++ .../zcraft/seira/command/route/Router.java | 1 + 5 files changed, 48 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cf2cf30c..89649a18 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ Seira正在活跃开发中,在使用的过程中可能会有一些Bug,也会 | `/sa` | `/sa ` | 获取指定成绩分析图 | | `/ma` | `/ma [id/locId/rsN/bpN] [n/#n]` | 获取指定或最近目标成绩的Miss分析;省略目标时用`#n`指定Miss | | `/u` | `/u [uid/username/@user]` | 获取指定用户信息 | +| `/@` | `/@[someone]` | 获取自己或指定用户的文字版信息 | | `/r` | `/r [id/locId/rsN/bpN] [[mm:ss]-[mm:ss]]` | 生成并发送指定或最近目标的回放视频。省略范围时自动识别高光,使用`-`渲染整个回放 | | `/rg` | `/rg ` | 猜 Rank 游戏及个人战绩查询 | | `/rsc` | `/rsc [id/locId/rsN/bpN] [+,...]` | 生成并发送指定或最近目标的成绩同屏回放视频;追加用户和范围顺序不限 | diff --git a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java index 1dc39436..6ac6f80f 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java @@ -22,6 +22,7 @@ import java.util.function.Predicate; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; +import static xyz.zcraft.seira.command.reply.ReplyFactory.u; public final class GeneralCommandHandler { private final MessageSender messageSender; @@ -187,4 +188,14 @@ public void handleMc(Context ctx) { ctx.sendReply(at(ctx) + "状态获取失败了喵,请稍后再试。"); } } + + public void handleUx(Context ctx) { + String player = resolver.player(ctx.argumentCount() == 0 ? null : ctx.argument(0), ctx.senderUserId()); + + try (var _ = taskCoordinator.beginRequest(ctx, "User Info Short")) { + long uid = OstellaApi.resolveUid(player); + var user = OstellaApi.getUserRaw(uid); + ctx.sendReply(replyFactory.userInfoShortMessage(ctx, user)); + } + } } diff --git a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java index 00809909..0d638cb1 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java @@ -34,6 +34,13 @@ public String sanitize(String rawContent) { } } + if (rawContent.trim().equalsIgnoreCase("@") + || rawContent.trim().equalsIgnoreCase("//")) { + return "ux"; + } else if (looksLikeMention(rawContent.trim())) { + return "ux " + rawContent; + } + // Add surrounding space to <@> before expanding compact commands so /bp5<@...> is recognized. rawContent = Patterns.QQ_INLINE_AT_PATTERN.matcher(rawContent).replaceAll(r -> " " + r.group() + " "); @@ -87,18 +94,18 @@ public SearchQuery resolveSearchQuery(String arg) { public String player(String argument, String senderUserId) { if (argument == null) { Long uid = resolveBoundUid(senderUserId); - if (uid == null) throw new ResolutionException("你还没有绑定玩家ID,请先使用 /bind"); + if (uid == null) throw new ResolutionException("你还没有绑定玩家ID,请先使用 /bind 喵"); return uid.toString(); } String mentioned = extractMentionedUserId(argument); if (mentioned != null) { Long uid = resolveBoundUid(mentioned); - if (uid == null) throw new ResolutionException("被@的用户还没有绑定玩家ID,请先让对方使用 /bind"); + if (uid == null) throw new ResolutionException("被@的用户还没有绑定玩家ID,请先让对方使用 /bind 喵"); return uid.toString(); } String player = argument.trim(); if (player.startsWith("@")) player = player.substring(1); - if (player.isBlank()) throw new ResolutionException("无法识别指定的玩家"); + if (player.isBlank()) throw new ResolutionException("无法识别指定的玩家喵"); return player; } diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index 4e0ceb92..0d7bf7d3 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -463,6 +463,10 @@ public PendingMessage supMessage(Context ctx, String username, String openId, Bo return PendingMessage.ofMarkdownRaw(Contents.supContent(ctx, username, openId, isSupporter, hasSupported, supportLevel)); } + public PendingMessage userInfoShortMessage(Context ctx, UserExtended user) { + return PendingMessage.ofMarkdownRaw(Contents.userInfoShortContent(ctx, user)); + } + private static final class Contents { static String replayTaskContent(Context ctx, OstellaApi.ReplayTaskInfo taskInfo) { StringBuilder sb = new StringBuilder(); @@ -874,6 +878,27 @@ public static String supContent( return sb.toString().trim(); } + + public static String userInfoShortContent(Context ctx, UserExtended user) { + final Duration playTime = Duration.ofSeconds(user.getStatistics().getPlayTime()); + return """ + %s `%s` 的用户信息 + > - PP: %.2f + > - Rank: #%,d (%s #%,d) + > - 准确率: %.2f%% + > - 游玩次数: %,d + > - 获得总分: %,d + > - 游玩时间: %dd %dh %dm + """.formatted( + at(ctx), user.getUsername(), + user.getStatistics().getPp(), + user.getStatistics().getGlobalRank(), user.getCountry().getCode(), user.getStatistics().getRank().getCountry(), + user.getStatistics().getAccuracy() * 100, + user.getStatistics().getPlayCount(), + user.getStatistics().getRankedScore(), + playTime.toDaysPart(), playTime.toHoursPart(), playTime.toMinutesPart() + ); + } } private record Buttons(String directUrl) { diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index 59e77cc2..7e6a1bb8 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -158,6 +158,7 @@ private static CommandRegistry createCommandRegistry( .register(socialCommands::handleLb, "lb") .register(generalCommands::handleStat, "stat") .register(generalCommands::handleU, "u") + .register(generalCommands::handleUx, "ux") .register(generalCommands::handleLuck, "luck") .register(generalCommands::handleRoll, "roll") .register(replayCommands::handleRstat, "rstat") From 97e60098ac946639f046ac38a49f060ec53982ea Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 27 Sep 2026 17:15:53 +0800 Subject: [PATCH 75/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AgentService=20?= =?UTF-8?q?=E5=92=8C=20AiChatHandler=EF=BC=8C=E9=87=8D=E6=9E=84=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=BC=95=E7=94=A8=E5=86=85=E5=AE=B9=E5=92=8C=E9=99=84?= =?UTF-8?q?=E4=BB=B6=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../xyz/zcraft/seira/ai/AgentService.java | 56 +++++++++---------- .../xyz/zcraft/seira/ai/AiChatHandler.java | 20 ++++++- .../java/xyz/zcraft/seira/bot/WSClient.java | 19 ++++++- .../xyz/zcraft/seira/bot/data/MsgElem.java | 14 +++++ .../zcraft/seira/command/route/Router.java | 22 ++++---- 6 files changed, 90 insertions(+), 43 deletions(-) create mode 100644 src/main/java/xyz/zcraft/seira/bot/data/MsgElem.java diff --git a/pom.xml b/pom.xml index 013af415..73e5ca8e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.10 + 1.12.11 25 diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index 435ef8de..67b5b44c 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -161,21 +161,10 @@ public void recordHistory(String groupId, String sender, String message) { } } - public String input(String groupId, String openId, String rawContent, Function contextFunc) { - return input(groupId, openId, rawContent, contextFunc, null, null); - } - - public String input( - String groupId, String openId, String rawContent, - Function contextFunc, StreamHandler handler - ) { - return input(groupId, openId, rawContent, contextFunc, handler, null); - } - public String input( String groupId, String openId, String rawContent, Function contextFunc, StreamHandler handler, - List attachments + List attachments, String refContent ) { final StateOwner owner = StateOwner.of(groupId, openId); final State state = getOrCreateState(owner); @@ -192,22 +181,29 @@ public String input( state.incomingMessages.clear(); } - String query = ""; + var query = new StringBuilder(); final String message = processMessage(rawContent); if (pendingMessages.isEmpty()) { - query = "**<@" + openId + ">**" + ": " + message; + query.append("**<@").append(openId).append(">**").append(": ").append(message); } else { if (pendingMessages.size() == CONTEXT_SIZE) { - query += "====== ...历史消息较多已省略 ======"; + query.append("====== ...历史消息较多已省略 ======"); + } + query.append(String.join("\n", pendingMessages)) + .append("\n") + .append("====== 以上是最近的所有消息 ======\n"); + + if (refContent != null && !refContent.isBlank()) { + query.append("====== 以下本次询问引用的消息 ======\n") + .append(refContent) + .append("\n"); } - query += String.join("\n", pendingMessages) - + "\n" - + "====== 以上是最近的所有消息 ======\n" - + "====== 以下是本次询问的内容 ======\n" - + "\n" - + "**<@" + openId + ">**" + ": " + message; + + query.append("====== 以下是本次询问的内容 ======\n") + .append("\n") + .append("**<@").append(openId).append(">**").append(": ").append(message); } recordHistory(groupId, openId, message); @@ -215,7 +211,7 @@ public String input( state.resetIfNeeded(api, groupId); if (contextFunc != null) { - state.getVars().put("CONTEXT", contextFunc.apply(query)); + state.getVars().put("CONTEXT", contextFunc.apply(query.toString())); } api.updateConversation(groupId, state.conv.appConversationID(), state.vars); @@ -223,9 +219,9 @@ public String input( final String answer; try { if (handler != null) { - answer = api.chatQueryStreaming(groupId, state.conv.appConversationID(), query, handler, attachments); + answer = api.chatQueryStreaming(groupId, state.conv.appConversationID(), query.toString(), handler, attachments); } else { - var response = api.chatQuery(groupId, state.conv.appConversationID(), query, attachments); + var response = api.chatQuery(groupId, state.conv.appConversationID(), query.toString(), attachments); answer = response.answer(); } } catch (RuntimeException | Error e) { @@ -494,7 +490,9 @@ public void clearConversation(String groupId, String appConvId) { } } - public ChatQueryResponse chatQuery(String groupId, String appConvId, String query, List attachments) { + public ChatQueryResponse chatQuery( + String groupId, String appConvId, String query, List attachments + ) { LOG.info("Running chat query for id {}", groupId); JsonObject body = new JsonObject(); @@ -503,7 +501,7 @@ public ChatQueryResponse chatQuery(String groupId, String appConvId, String quer body.addProperty("Query", query); body.addProperty("ResponseMode", "blocking"); - if (attachments != null) { + if (attachments != null && !attachments.isEmpty()) { body.add("QueryExtends", GSON.toJsonTree(Map.of("Files", attachments))); } @@ -533,7 +531,9 @@ public ChatQueryResponse chatQuery(String groupId, String appConvId, String quer } } - public String chatQueryStreaming(String groupId, String appConvId, String query, StreamHandler handler, List attachments) { + public String chatQueryStreaming( + String groupId, String appConvId, String query, StreamHandler handler, List attachments + ) { LOG.info("Running chat query for id {}", groupId); JsonObject body = new JsonObject(); @@ -542,7 +542,7 @@ public String chatQueryStreaming(String groupId, String appConvId, String query, body.addProperty("Query", query); body.addProperty("ResponseMode", "streaming"); - if (attachments != null) { + if (attachments != null && !attachments.isEmpty()) { body.add("QueryExtends", GSON.toJsonTree(Map.of("Files", attachments))); } diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index f58f9187..e11a9fc8 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -2,8 +2,10 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; +import xyz.zcraft.seira.ai.data.AgentFile; import xyz.zcraft.seira.bot.data.Attachment; import xyz.zcraft.seira.bot.data.GroupBotState; +import xyz.zcraft.seira.bot.data.MsgElem; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.command.parse.Resolver; @@ -79,12 +81,24 @@ public void handleAi(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "用法:/ai [on|off]")); } - public void handleChat(Context ctx, String message) { + public void handleChat(Context ctx, String message, List elems) { if (agentService.isRunning(ctx.groupId(), ctx.senderUserId())) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已有一轮对话正在进行中了喵,请稍作等待~")); return; } + final List attachments = elems.stream() + .filter(e -> e.attachments() != null) + .flatMap(e -> e.attachments().stream()) + .map(e -> new AgentFile(e.filename(), null, e.size(), e.url())) + .toList(); + + final var refContent = elems.stream() + .filter(e -> e.content() != null && !e.content().isBlank()) + .findFirst() + .map(MsgElem::content) + .orElse(null); + agentService.input( ctx.groupId(), ctx.senderUserId(), @@ -109,7 +123,9 @@ public void onError(String errorCode, String errorMsg) { true ); } - } + }, + attachments, + refContent ); } diff --git a/src/main/java/xyz/zcraft/seira/bot/WSClient.java b/src/main/java/xyz/zcraft/seira/bot/WSClient.java index 1fed923e..77b57b38 100644 --- a/src/main/java/xyz/zcraft/seira/bot/WSClient.java +++ b/src/main/java/xyz/zcraft/seira/bot/WSClient.java @@ -9,6 +9,7 @@ import org.java_websocket.handshake.ServerHandshake; import xyz.zcraft.seira.bot.data.AccessToken; import xyz.zcraft.seira.bot.data.Attachment; +import xyz.zcraft.seira.bot.data.MsgElem; import xyz.zcraft.seira.command.AttachmentHandler; import xyz.zcraft.seira.command.route.Router; import xyz.zcraft.seira.config.AppConfig; @@ -171,7 +172,14 @@ private void onC2CMsg(JsonObject payload) { } } - router.onPrivateMessageReceived(openId, msgId, content, msgIdx, attachmentList); + List msgElemList = new ArrayList<>(); + + if (data.has("msg_elements")) { + data.get("msg_elements").getAsJsonArray() + .forEach(elem -> msgElemList.add(gson.fromJson(elem, MsgElem.class))); + } + + router.onPrivateMessageReceived(openId, msgId, content, msgIdx, attachmentList, msgElemList); } private void onC2CFile(JsonObject payload) { @@ -214,6 +222,13 @@ private void onGroupMsg(JsonObject payload) { } } + List msgElemList = new ArrayList<>(); + + if (data.has("msg_elements")) { + data.get("msg_elements").getAsJsonArray() + .forEach(elem -> msgElemList.add(gson.fromJson(elem, MsgElem.class))); + } + List attachments = parseAttachments(data); Map mentions = parseMentions(data); @@ -229,7 +244,7 @@ private void onGroupMsg(JsonObject payload) { )); } - router.onGroupMessageReceived(groupId, openId, msgId, content, msgIdx, attachments); + router.onGroupMessageReceived(groupId, openId, msgId, content, msgIdx, attachments, msgElemList); } private Map parseMentions(JsonObject data) { diff --git a/src/main/java/xyz/zcraft/seira/bot/data/MsgElem.java b/src/main/java/xyz/zcraft/seira/bot/data/MsgElem.java new file mode 100644 index 00000000..fa6f884e --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/bot/data/MsgElem.java @@ -0,0 +1,14 @@ +package xyz.zcraft.seira.bot.data; + + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public record MsgElem( + List attachments, + String content, + @SerializedName("message_type") Integer messageType, + @SerializedName("msg_idx") String msgIdx +) { +} diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index 7e6a1bb8..a5e0ef18 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -9,10 +9,7 @@ import xyz.zcraft.seira.api.data.VideoRenderRecord; import xyz.zcraft.seira.bot.MessageSender; import xyz.zcraft.seira.bot.QQApi; -import xyz.zcraft.seira.bot.data.Attachment; -import xyz.zcraft.seira.bot.data.GroupBotState; -import xyz.zcraft.seira.bot.data.PendingMessage; -import xyz.zcraft.seira.bot.data.QQUser; +import xyz.zcraft.seira.bot.data.*; import xyz.zcraft.seira.command.*; import xyz.zcraft.seira.command.handler.*; import xyz.zcraft.seira.command.parse.CommandParser; @@ -34,6 +31,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; @@ -183,20 +181,24 @@ private String getAvatar(String openId) { } public void onPrivateMessageReceived( - String userId, String messageId, String rawContent, String msgIdx ,List attachments + String userId, String messageId, String rawContent, + String msgIdx ,List attachments, List msgElems ) { - handleMessageReceived(userId, null, userId, messageId, rawContent, false, msgIdx, attachments); + handleMessageReceived(userId, null, userId, messageId, rawContent, false, msgIdx, attachments, msgElems); } public void onGroupMessageReceived( - String groupId, String senderUserId, String messageId, String rawContent, String msgIdx, List attachments + String groupId, String senderUserId, + String messageId, String rawContent, + String msgIdx, List attachments, + List msgElems ) { - handleMessageReceived(groupId, groupId, senderUserId, messageId, rawContent, true, msgIdx, attachments); + handleMessageReceived(groupId, groupId, senderUserId, messageId, rawContent, true, msgIdx, attachments, msgElems); } private void handleMessageReceived( String targetId, String groupId, String userId, String messageId, String rawContent, - boolean groupMessage, String msgIdx, List attachments + boolean groupMessage, String msgIdx, List attachments, List msgElems ) { AtomicInteger messageSeqCounter = new AtomicInteger(1); try { @@ -252,7 +254,7 @@ private void handleMessageReceived( ); if (beingAt && permitAi) { - aiChatHandler.handleChat(parseResult.context().withReplies(replies), msgToRecord); + aiChatHandler.handleChat(parseResult.context().withReplies(replies), msgToRecord, msgElems); } else if (permitAi) { aiChatHandler.recordHistory(groupId, userId, msgToRecord, attachments); } From 801a9dd231515e54d655aa60acd0077c9b6db1b1 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 27 Sep 2026 19:36:59 +0800 Subject: [PATCH 76/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ApiUtil=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20requireResultArray=20=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E4=BB=A5=E7=A1=AE=E4=BF=9D=E5=93=8D=E5=BA=94=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=B8=BA=E7=BB=93=E6=9E=9C=E6=95=B0=E7=BB=84=EF=BC=9B=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20AiChatHandler=EF=BC=8C=E8=BF=87=E6=BB=A4=E6=97=A0?= =?UTF-8?q?=E6=95=88=E7=9A=84=E9=99=84=E4=BB=B6=EF=BC=9B=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20AgentFile=20=E7=9A=84=E6=9C=89=E6=95=88=E6=80=A7=E6=A3=80?= =?UTF-8?q?=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/ai/AiChatHandler.java | 1 + .../xyz/zcraft/seira/ai/data/AgentFile.java | 3 +++ .../java/xyz/zcraft/seira/api/ApiUtil.java | 18 ++++++++++++++++++ .../java/xyz/zcraft/seira/api/OstellaApi.java | 8 ++++---- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index e11a9fc8..60a58c25 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -91,6 +91,7 @@ public void handleChat(Context ctx, String message, List elems) { .filter(e -> e.attachments() != null) .flatMap(e -> e.attachments().stream()) .map(e -> new AgentFile(e.filename(), null, e.size(), e.url())) + .filter(AgentFile::isValid) .toList(); final var refContent = elems.stream() diff --git a/src/main/java/xyz/zcraft/seira/ai/data/AgentFile.java b/src/main/java/xyz/zcraft/seira/ai/data/AgentFile.java index c39d18db..8615e2d2 100644 --- a/src/main/java/xyz/zcraft/seira/ai/data/AgentFile.java +++ b/src/main/java/xyz/zcraft/seira/ai/data/AgentFile.java @@ -8,4 +8,7 @@ public record AgentFile( @SerializedName("Size") Long size, @SerializedName("Url") String url ) { + public boolean isValid() { + return name != null && url != null; + } } diff --git a/src/main/java/xyz/zcraft/seira/api/ApiUtil.java b/src/main/java/xyz/zcraft/seira/api/ApiUtil.java index ad86e174..70ca2bc3 100644 --- a/src/main/java/xyz/zcraft/seira/api/ApiUtil.java +++ b/src/main/java/xyz/zcraft/seira/api/ApiUtil.java @@ -1,6 +1,8 @@ package xyz.zcraft.seira.api; import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import xyz.zcraft.seira.api.data.RawResponse; @@ -78,4 +80,20 @@ static JsonObject requireDataObject(RawResponse payload, String message) { } return payload.getData().getAsJsonObject(); } + + static JsonArray requireResultArray(RawResponse payload, String message) { + JsonElement data = payload.getData(); + if (data != null && data.isJsonArray()) { + return data.getAsJsonArray(); + } + if (data == null || !data.isJsonObject()) { + throw new RuntimeException(message); + } + + JsonElement result = data.getAsJsonObject().get("result"); + if (result == null || !result.isJsonArray()) { + throw new RuntimeException(message); + } + return result.getAsJsonArray(); + } } diff --git a/src/main/java/xyz/zcraft/seira/api/OstellaApi.java b/src/main/java/xyz/zcraft/seira/api/OstellaApi.java index caf476e3..191c4360 100644 --- a/src/main/java/xyz/zcraft/seira/api/OstellaApi.java +++ b/src/main/java/xyz/zcraft/seira/api/OstellaApi.java @@ -66,7 +66,7 @@ public static Response> getFollowed(String accessToken) { final RawResponse r = GSON.fromJson(send.body(), RawResponse.class); ApiUtil.ensureApiSuccess(r, "获取多人房间失败"); - final JsonArray data = r.getData().getAsJsonArray(); + final JsonArray data = ApiUtil.requireResultArray(r, "获取好友响应缺少结果数组"); LinkedList followed = new LinkedList<>(); @@ -413,7 +413,7 @@ public static Response> searchBeatmapSetResponse(SearchQu final RawResponse rawResponse = GSON.fromJson(send.body(), RawResponse.class); ApiUtil.ensureApiSuccess(rawResponse, "搜索谱面集失败"); - final JsonArray data = rawResponse.getData().getAsJsonArray(); + final JsonArray data = ApiUtil.requireResultArray(rawResponse, "搜索谱面集响应缺少结果数组"); final LinkedList items = new LinkedList<>(); @@ -935,7 +935,7 @@ public static Response> getScoreMissesResponse(String scoreId) { final RawResponse r = GSON.fromJson(send.body(), RawResponse.class); ApiUtil.ensureApiSuccess(r, "获取 Miss 数据失败"); - final JsonArray data = r.getData().getAsJsonArray(); + final JsonArray data = ApiUtil.requireResultArray(r, "获取 Miss 数据响应缺少结果数组"); List misses = new LinkedList<>(); for (JsonElement datum : data) { @@ -1037,7 +1037,7 @@ public static List getUsers(List u) { final RawResponse r = GSON.fromJson(send.body(), RawResponse.class); ApiUtil.ensureApiSuccess(r, "获取用户信息失败"); - final JsonArray data = r.getData().getAsJsonArray(); + final JsonArray data = ApiUtil.requireResultArray(r, "获取用户信息响应缺少结果数组"); List users = new LinkedList<>(); for (JsonElement datum : data) { From aa1ffdeb2598d4e57817fac207b9fd69d3b43160 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Sun, 27 Sep 2026 19:37:51 +0800 Subject: [PATCH 77/96] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7=E8=87=B3=201.12.12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 73e5ca8e..655747db 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.11 + 1.12.12 25 From 399fd8087437415c3fd20ad5a02aa70b608f94f9 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 28 Sep 2026 14:23:25 +0800 Subject: [PATCH 78/96] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=81=9C=E6=AD=A2?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E9=80=9A=E8=BF=87=E5=91=BD=E4=BB=A4=E5=81=9C=E6=AD=A2=E5=BD=93?= =?UTF-8?q?=E5=89=8DAI=E5=AF=B9=E8=AF=9D=EF=BC=9B=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../xyz/zcraft/seira/ai/AgentService.java | 73 ++++++++++++++++++- .../xyz/zcraft/seira/ai/AiChatHandler.java | 30 +++++++- .../java/xyz/zcraft/seira/api/OstellaApi.java | 22 ++++-- .../seira/command/reply/ReplyFactory.java | 1 + 5 files changed, 118 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index 655747db..d69fb1f4 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.12 + 1.12.13 25 diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index 67b5b44c..f4d0912f 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -4,6 +4,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import lombok.Getter; +import lombok.Setter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; @@ -22,6 +23,7 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -219,7 +221,14 @@ public String input( final String answer; try { if (handler != null) { - answer = api.chatQueryStreaming(groupId, state.conv.appConversationID(), query.toString(), handler, attachments); + answer = api.chatQueryStreaming( + groupId, + state.conv.appConversationID(), + query.toString(), + handler, + attachments, + state::setRunningMessageId + ); } else { var response = api.chatQuery(groupId, state.conv.appConversationID(), query.toString(), attachments); answer = response.answer(); @@ -237,6 +246,33 @@ public String input( } } + public StopStatus requireStop(String groupId, String openId) { + final StateOwner owner = StateOwner.of(groupId, openId); + final State state = states.get(owner); + + if (state == null || !state.running.get()) { + return StopStatus.NO_CONVERSATION; + } + + if (state.getRunningMessageId() == null) { + return StopStatus.NOT_SUPPORTED; + } + + try { + api.stopConversation(state.getRunningMessageId(), groupId); + return StopStatus.SUCCESS; + } catch (Exception e) { + return StopStatus.FAILED; + } + } + + public enum StopStatus { + SUCCESS, + NO_CONVERSATION, + NOT_SUPPORTED, + FAILED; + } + public boolean isRunning(String groupId, String openId) { final State state = states.get( StateOwner.of(groupId, openId) @@ -337,6 +373,8 @@ static final class State { private final AtomicBoolean resetting; private final Deque incomingMessages; private AppConversationBrief conv; + @Setter + private volatile String runningMessageId; private State( AppConversationBrief conv, @@ -490,6 +528,30 @@ public void clearConversation(String groupId, String appConvId) { } } + public void stopConversation(String messageId, String groupId) { + LOG.info("Stopping conversation for id {}", groupId); + + JsonObject body = new JsonObject(); + body.addProperty("UserID", groupId); + body.addProperty("MessageID", messageId); + + try { + var request = newRequest("/api/proxy/api/v1/stop_message") + .POST(HttpRequest.BodyPublishers.ofString(body.toString())) + .build(); + + final HttpResponse send = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + + if (send.statusCode() != 200) { + throw new RuntimeException("Failed to stop conversation: " + send.statusCode()); + } + + LOG.info("Stopped conversation for id {}", groupId); + } catch (Exception e) { + throw new RuntimeException("Error clearing conversation", e); + } + } + public ChatQueryResponse chatQuery( String groupId, String appConvId, String query, List attachments ) { @@ -532,7 +594,9 @@ public ChatQueryResponse chatQuery( } public String chatQueryStreaming( - String groupId, String appConvId, String query, StreamHandler handler, List attachments + String groupId, String appConvId, String query, + StreamHandler handler, List attachments, + Consumer taskIdSetter ) { LOG.info("Running chat query for id {}", groupId); @@ -579,6 +643,11 @@ public String chatQueryStreaming( JsonObject event = JsonParser.parseString(data).getAsJsonObject(); String eventType = event.get("event").getAsString(); + String taskId = event.get("task_id").getAsString(); + + if (taskIdSetter != null && taskId != null && !taskId.isEmpty()) { + taskIdSetter.accept(taskId); + } switch (eventType) { case "message" -> { diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index 60a58c25..076571d8 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -18,6 +18,7 @@ import java.util.function.Predicate; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; +import static xyz.zcraft.seira.command.reply.ReplyFactory.cmd; public class AiChatHandler { private static final Gson GSON = new Gson(); @@ -48,7 +49,7 @@ public void handleAi(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "目前AI对话在本群启用状态为:" + (b ? "√" : "×"))); return; } else if (ctx.argumentCount() == 1 - && List.of("on", "off", "reset").contains(ctx.argument(0).toLowerCase(Locale.ROOT))) { + && List.of("on", "off", "reset", "resetgroup", "resetme", "stop").contains(ctx.argument(0).toLowerCase(Locale.ROOT))) { if (!adminAuthorizer.test(ctx.senderUserId())) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "你无权使用该命令喵。\n" + "> 由于此功能开销较大、处于测试阶段且较为不可控,暂未开放。若想要在此群中使用此功能,请联系 Bot 管理员喵。")); @@ -72,8 +73,30 @@ public void handleAi(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已禁用本群AI对话喵。")); return; } else if ("reset".equalsIgnoreCase(ctx.argument(0))) { + agentService.clearState(ctx.groupId(), ctx.senderUserId()); + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已重置你在本群的AI对话状态喵。")); + return; + } else if ("resetme".equalsIgnoreCase(ctx.argument(0))) { + final int i = agentService.clearStateOfUser(ctx.senderUserId()); + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已重置你在" + i + "个群中的AI对话状态喵。")); + return; + } else if ("resetgroup".equalsIgnoreCase(ctx.argument(0))) { + if (!adminAuthorizer.test(ctx.senderUserId())) { + ctx.sendReply(at(ctx) + "你无权使用该命令喵。"); + return; + } final int i = agentService.clearStateOfGroup(ctx.groupId()); ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已重置本群" + i + "个用户的AI对话状态喵。")); + return; + } else if ("stop".equalsIgnoreCase(ctx.argument(0))) { + final AgentService.StopStatus stopStatus = agentService.requireStop(ctx.groupId(), ctx.senderUserId()); + ctx.sendReply(at(ctx) + switch (stopStatus) { + case SUCCESS -> "已停止你在本群的AI对话喵。"; + case FAILED -> "停止AI对话失败了喵。"; + case NO_CONVERSATION -> "目前没有运行中的对话喵。"; + case NOT_SUPPORTED -> "当前不支持停止AI对话。"; + }); + return; } } @@ -83,7 +106,10 @@ public void handleAi(Context ctx) { public void handleChat(Context ctx, String message, List elems) { if (agentService.isRunning(ctx.groupId(), ctx.senderUserId())) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已有一轮对话正在进行中了喵,请稍作等待~")); + ctx.sendReply(PendingMessage.ofMarkdownRaw( + at(ctx) + + "已有一轮对话正在进行中了喵,请稍作等待或" + cmd("/ai stop", "取消对话") + "~") + ); return; } diff --git a/src/main/java/xyz/zcraft/seira/api/OstellaApi.java b/src/main/java/xyz/zcraft/seira/api/OstellaApi.java index 191c4360..fbec79ba 100644 --- a/src/main/java/xyz/zcraft/seira/api/OstellaApi.java +++ b/src/main/java/xyz/zcraft/seira/api/OstellaApi.java @@ -651,7 +651,9 @@ private static TimeDurationParser.TimeRange getScoreHighlight(String scoreId, in } } - /** 每个查找方法只请求一个接口;目标类型转换和记忆由指令处理方法决定。 */ + /** + * 每个查找方法只请求一个接口;目标类型转换和记忆由指令处理方法决定。 + */ public static long lookupBeatmapInSet(long setId, long index, String auth) { return lookupTargetData("/beatmaps/lookup?ms=" + setId + "&i=" + index, auth, "查找谱面失败") .get("beatmap_id").getAsLong(); @@ -691,7 +693,7 @@ public static String lookupBeatmapsetScore(long setId, long index, long uid, Lis private static String lookupScore(String query, List filters, String mod) { return lookupTargetData(query + encodeScoreFilters(filters) - + (mod == null ? "" : "&mod=" + URLEncoder.encode(mod, StandardCharsets.UTF_8)), + + (mod == null ? "" : "&mod=" + URLEncoder.encode(mod, StandardCharsets.UTF_8)), null, "获取成绩失败").get("score_id").getAsString(); } @@ -700,7 +702,8 @@ private static JsonObject lookupTargetData(String query, String auth, String err var request = requestBuilder().uri(URI.create(ENDPOINT + query)).GET(); if (auth != null) withOsuAuthorization(request, auth); var response = CLIENT.send(request.build(), HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() != 200) throw ApiUtil.parseHttpError(response.body(), response.statusCode(), error); + if (response.statusCode() != 200) + throw ApiUtil.parseHttpError(response.body(), response.statusCode(), error); RawResponse payload = GSON.fromJson(response.body(), RawResponse.class); ApiUtil.ensureApiSuccess(payload, error); return ApiUtil.requireDataObject(payload, error); @@ -890,6 +893,8 @@ public static ServerStatus getServerStatus() { boolean oStella = false; boolean osu = false; String oStellaVersion = null; + int allWorkers = 0; + int onlineWorkers = 0; try { HttpRequest request = requestBuilder() .uri(URI.create(ENDPOINT + "/health")) @@ -912,12 +917,18 @@ public static ServerStatus getServerStatus() { if (data.has("osu_api") && !data.get("osu_api").isJsonNull()) { osu = data.get("osu_api").getAsBoolean(); } + if (data.has("all_render_workers") && !data.get("all_render_workers").isJsonNull()) { + allWorkers = data.get("all_render_workers").getAsInt(); + } + if (data.has("online_render_workers") && !data.get("online_render_workers").isJsonNull()) { + onlineWorkers = data.get("online_render_workers").getAsInt(); + } } } } catch (Exception _) { } - return new ServerStatus(true, oStella, oStellaVersion, osu); + return new ServerStatus(true, oStella, oStellaVersion, allWorkers, onlineWorkers, osu); } public static Response> getScoreMissesResponse(String scoreId) { @@ -1087,7 +1098,8 @@ private static RuntimeException requestFailure(Exception exception) { return new RuntimeException(exception); } - public record ServerStatus(boolean gateway, boolean oStella, String oStellaVersion, boolean osu) { + public record ServerStatus(boolean gateway, boolean oStella, String oStellaVersion, + int allWorkers, int onlineWorkers, boolean osu) { } public record ReplayRenderResult(String videoUrl, String taskId, FileInfo qqFile) { diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index 0d7bf7d3..52e10566 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -731,6 +731,7 @@ public static String statContent(Context ctx, OstellaApi.ServerStatus status) { "> oStella API: " + (status.oStella() ? "✅ 正常" : "❌ 无法访问") + "\n"; if (status.oStella()) { + stat += "> osuRenderer: " + (status.onlineWorkers() + " / " + status.allWorkers()) + "\n"; stat += "> osu! API: " + (status.osu() ? "✅ 正常" : "❌ 无法访问") + "\n"; } From 7417539e3d33eb544d2603a5f14e9d560ce20ae8 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 28 Sep 2026 14:28:02 +0800 Subject: [PATCH 79/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=9C=A8=E7=BA=BF?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E8=80=85=E7=8A=B6=E6=80=81=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=EF=BC=8C=E6=8F=90=E5=8D=87=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index 52e10566..31751f2a 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -731,7 +731,8 @@ public static String statContent(Context ctx, OstellaApi.ServerStatus status) { "> oStella API: " + (status.oStella() ? "✅ 正常" : "❌ 无法访问") + "\n"; if (status.oStella()) { - stat += "> osuRenderer: " + (status.onlineWorkers() + " / " + status.allWorkers()) + "\n"; + stat += "> osuRenderer: " + (status.onlineWorkers() + " / " + status.allWorkers()) + + (status.onlineWorkers() > 0 ? " (✅在线)" : " (❌全部离线)") + "\n"; stat += "> osu! API: " + (status.osu() ? "✅ 正常" : "❌ 无法访问") + "\n"; } From 971cf5776d1f73c0f91996271c6f5aefb6749218 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 28 Sep 2026 14:47:26 +0800 Subject: [PATCH 80/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=9C=A8=E7=BA=BF?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E8=80=85=E7=8A=B6=E6=80=81=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=EF=BC=8C=E6=8F=90=E5=8D=87=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- src/main/java/xyz/zcraft/seira/bot/QQBot.java | 2 +- ...stellaWatchApi.java => ScoreWatchApi.java} | 15 ++++------ .../zcraft/seira/watch/ScoreWatchService.java | 6 ++-- .../java/xyz/zcraft/seira/watch/WatchApi.java | 14 --------- .../seira/watch/WatchScoreNotifier.java | 30 ++++++++++--------- 6 files changed, 27 insertions(+), 42 deletions(-) rename src/main/java/xyz/zcraft/seira/watch/{OstellaWatchApi.java => ScoreWatchApi.java} (91%) delete mode 100644 src/main/java/xyz/zcraft/seira/watch/WatchApi.java diff --git a/pom.xml b/pom.xml index d69fb1f4..31d189e8 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.13 + 1.12.14 25 diff --git a/src/main/java/xyz/zcraft/seira/bot/QQBot.java b/src/main/java/xyz/zcraft/seira/bot/QQBot.java index f3c07c70..7c733fc7 100644 --- a/src/main/java/xyz/zcraft/seira/bot/QQBot.java +++ b/src/main/java/xyz/zcraft/seira/bot/QQBot.java @@ -84,7 +84,7 @@ public QQBot( LOG.info("Initializing score watch service"); this.watchService = new ScoreWatchService( - new OstellaWatchApi(config.ostella().endpoint(), config.ostella().token()), + new ScoreWatchApi(config.ostella().endpoint(), config.ostella().token()), new WatchScoreNotifier(sender), new SpecificScoreNotifier(sender), new SqliteSpecificScoreWatchStore(), diff --git a/src/main/java/xyz/zcraft/seira/watch/OstellaWatchApi.java b/src/main/java/xyz/zcraft/seira/watch/ScoreWatchApi.java similarity index 91% rename from src/main/java/xyz/zcraft/seira/watch/OstellaWatchApi.java rename to src/main/java/xyz/zcraft/seira/watch/ScoreWatchApi.java index 50ec068f..62bb1796 100644 --- a/src/main/java/xyz/zcraft/seira/watch/OstellaWatchApi.java +++ b/src/main/java/xyz/zcraft/seira/watch/ScoreWatchApi.java @@ -18,7 +18,7 @@ import java.util.List; import java.util.Map; -public class OstellaWatchApi implements WatchApi { +public class ScoreWatchApi { private static final Type SCORE_MAP_TYPE = new TypeToken>>() { }.getType(); @@ -27,19 +27,19 @@ public class OstellaWatchApi implements WatchApi { private final HttpClient client; private final Gson gson; - public OstellaWatchApi(String endpoint) { + public ScoreWatchApi(String endpoint) { this(endpoint, null); } - public OstellaWatchApi(String endpoint, String serviceToken) { + public ScoreWatchApi(String endpoint, String serviceToken) { this(endpoint, serviceToken, HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(), new Gson()); } - OstellaWatchApi(String endpoint, HttpClient client, Gson gson) { + ScoreWatchApi(String endpoint, HttpClient client, Gson gson) { this(endpoint, null, client, gson); } - OstellaWatchApi(String endpoint, String serviceToken, HttpClient client, Gson gson) { + ScoreWatchApi(String endpoint, String serviceToken, HttpClient client, Gson gson) { this.endpoint = endpoint.endsWith("/") ? endpoint.substring(0, endpoint.length() - 1) : endpoint; this.serviceToken = serviceToken; this.client = client; @@ -68,7 +68,6 @@ private static void ensureSuccessfulStatus(int statusCode, Object body, String a throw new IllegalStateException(action + "失败: HTTP " + statusCode + " " + detail); } - @Override public Map> getRecentScores(Collection userIds, int limit) { JsonObject body = new JsonObject(); body.add("user_ids", gson.toJsonTree(userIds)); @@ -101,7 +100,6 @@ public Map> getRecentScores(Collection userIds, in return Map.copyOf(scores); } - @Override public byte[] renderScore(long userId, long scoreId) { JsonObject body = new JsonObject(); body.addProperty("name", Long.toString(userId)); @@ -123,8 +121,7 @@ private HttpResponse send(String path, String body, HttpResponse.BodyHand HttpRequest.Builder builder = HttpRequest.newBuilder() .uri(URI.create(endpoint + path)) .timeout(Duration.ofMinutes(2)) - .header("Content-Type", "application/json") - .header("Accept", "application/json, image/*"); + .header("Content-Type", "application/json"); if (serviceToken != null && !serviceToken.isBlank()) { builder.header("Authorization", "Bearer " + serviceToken); } diff --git a/src/main/java/xyz/zcraft/seira/watch/ScoreWatchService.java b/src/main/java/xyz/zcraft/seira/watch/ScoreWatchService.java index d0631a6d..86c04c4a 100644 --- a/src/main/java/xyz/zcraft/seira/watch/ScoreWatchService.java +++ b/src/main/java/xyz/zcraft/seira/watch/ScoreWatchService.java @@ -20,7 +20,7 @@ public final class ScoreWatchService implements AutoCloseable { private final Object lock = new Object(); private final Map> watchesByGroup = new LinkedHashMap<>(); private final Map specificWatchesByGroup = new LinkedHashMap<>(); - private final WatchApi api; + private final ScoreWatchApi api; private final WatchScoreNotifier notifier; private final SpecificScoreNotifier specificNotifier; private final SpecificScoreWatchStore specificWatchStore; @@ -31,7 +31,7 @@ public final class ScoreWatchService implements AutoCloseable { private final AtomicBoolean closed = new AtomicBoolean(); public ScoreWatchService( - WatchApi api, + ScoreWatchApi api, WatchScoreNotifier notifier, SpecificScoreNotifier specificNotifier, SpecificScoreWatchStore specificWatchStore, @@ -320,7 +320,7 @@ private void sendNewScores(WatchRef watch, List scores, Map api.renderScore(watch.entry.target.userId(), score.scoreId()) ); - if (!notifier.sendScore(watch.groupId, image)) { + if (!notifier.sendScore(watch.groupId, score, image)) { LOG.warn("Failed to send watched score {} to group {}", score.scoreId(), watch.groupId); return; } diff --git a/src/main/java/xyz/zcraft/seira/watch/WatchApi.java b/src/main/java/xyz/zcraft/seira/watch/WatchApi.java deleted file mode 100644 index 30bc9d11..00000000 --- a/src/main/java/xyz/zcraft/seira/watch/WatchApi.java +++ /dev/null @@ -1,14 +0,0 @@ -package xyz.zcraft.seira.watch; - -import java.util.Collection; -import java.util.List; -import java.util.Map; - -/** - * Backend boundary used by the score watch domain service. - */ -public interface WatchApi { - Map> getRecentScores(Collection userIds, int limit); - - byte[] renderScore(long userId, long scoreId); -} diff --git a/src/main/java/xyz/zcraft/seira/watch/WatchScoreNotifier.java b/src/main/java/xyz/zcraft/seira/watch/WatchScoreNotifier.java index 9003f73c..bd9839fd 100644 --- a/src/main/java/xyz/zcraft/seira/watch/WatchScoreNotifier.java +++ b/src/main/java/xyz/zcraft/seira/watch/WatchScoreNotifier.java @@ -1,13 +1,18 @@ package xyz.zcraft.seira.watch; +import xyz.zcraft.osu.model.Score; import xyz.zcraft.seira.bot.MessageSender; import xyz.zcraft.seira.bot.data.FileInfo; import xyz.zcraft.seira.bot.data.Message; import xyz.zcraft.seira.bot.data.PendingMessage; +import xyz.zcraft.seira.data.UploadedImage; import java.util.Base64; import java.util.Objects; +import static xyz.zcraft.seira.command.reply.ReplyFactory.cmd; +import static xyz.zcraft.seira.command.reply.ReplyFactory.s; + public final class WatchScoreNotifier { private final MessageSender messageSender; @@ -15,20 +20,17 @@ public WatchScoreNotifier(MessageSender messageSender) { this.messageSender = Objects.requireNonNull(messageSender); } - public boolean sendScore(String groupId, byte[] imageBytes) { - String base64 = Base64.getEncoder().encodeToString(imageBytes); - FileInfo media = messageSender.uploadGroupMediaBase64( + public boolean sendScore(String groupId, RecentScore score, byte[] imageBytes) { + final UploadedImage uploadedImage = messageSender.uploadImageToCos(imageBytes); + + if (uploadedImage == null) return false; + + return messageSender.sendGroupMarkdown( groupId, - PendingMessage.FILE_TYPE_IMAGE, - base64 - ); - if (media == null) { - return false; - } - - Message message = new Message(); - message.setMsgType(PendingMessage.MSG_TYPE_MEDIA); - message.setMedia(media); - return messageSender.sendGroupMessage(groupId, message) != null; + """ + %s + > ID %s + """.formatted(uploadedImage.toMarkdown(), s(score.scoreId())).trim() + ) != null; } } From 506103a889bed00a178449a83c3292769ec9844d Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 28 Sep 2026 14:52:44 +0800 Subject: [PATCH 81/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AiChatHandler?= =?UTF-8?q?=EF=BC=8C=E8=B0=83=E6=95=B4=E6=9D=83=E9=99=90=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=8F=AA=E6=9C=89?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E5=8F=AF=E4=BB=A5=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E7=89=B9=E5=AE=9A=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/ai/AiChatHandler.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index 076571d8..01f4354a 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -50,13 +50,13 @@ public void handleAi(Context ctx) { return; } else if (ctx.argumentCount() == 1 && List.of("on", "off", "reset", "resetgroup", "resetme", "stop").contains(ctx.argument(0).toLowerCase(Locale.ROOT))) { - if (!adminAuthorizer.test(ctx.senderUserId())) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "你无权使用该命令喵。\n" + - "> 由于此功能开销较大、处于测试阶段且较为不可控,暂未开放。若想要在此群中使用此功能,请联系 Bot 管理员喵。")); - return; - } - if ("on".equalsIgnoreCase(ctx.argument(0))) { + if (!adminAuthorizer.test(ctx.senderUserId())) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "你无权使用该命令喵。\n" + + "> 由于此功能开销较大、处于测试阶段且较为不可控,暂未开放。若想要在此群中使用此功能,请联系 Bot 管理员喵。")); + return; + } + final GroupBotState apply = botStateGetter.apply(ctx.groupId()); if (apply.allowProactiveMsg() && apply.receiveMsgSetting() == GroupBotState.ReceiveMsgSetting.ALL) { AiPermission.permit(ctx.groupId()); @@ -69,6 +69,11 @@ public void handleAi(Context ctx) { } return; } else if ("off".equalsIgnoreCase(ctx.argument(0))) { + if (!adminAuthorizer.test(ctx.senderUserId())) { + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "你无权使用该命令喵。")); + return; + } + AiPermission.revoke(ctx.groupId()); ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已禁用本群AI对话喵。")); return; @@ -82,7 +87,7 @@ public void handleAi(Context ctx) { return; } else if ("resetgroup".equalsIgnoreCase(ctx.argument(0))) { if (!adminAuthorizer.test(ctx.senderUserId())) { - ctx.sendReply(at(ctx) + "你无权使用该命令喵。"); + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "你无权使用该命令喵。")); return; } final int i = agentService.clearStateOfGroup(ctx.groupId()); From 181591d790964430f72b383651304e6715b5f2c0 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 28 Sep 2026 15:00:44 +0800 Subject: [PATCH 82/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=A4=BE=E4=BA=A4?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E5=A4=84=E7=90=86=EF=BC=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=B8=BB=E9=A1=B5=E9=93=BE=E6=8E=A5=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E5=A2=9E=E5=BC=BA=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/command/handler/SocialCommandHandler.java | 2 +- src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java index 9a1006ce..e0b8c1b1 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java @@ -99,7 +99,7 @@ public void handleFriendStatus(Context ctx) { final String mention = UserDataStore.findGroupOpenIdByUid(ctx.groupId(), uid) .map(ReplyFactory::at) .orElse(""); - ctx.sendReply(PendingMessage.ofMarkdownRaw(mention + ": [%d](%s)".formatted(uid, "https://osu.ppy.sh/users/" + uid))); + ctx.sendReply(PendingMessage.ofMarkdownRaw(mention + ": [%d(点击打开主页)](%s)".formatted(uid, "https://osu.ppy.sh/users/" + uid))); final UserExtended targetUser = OstellaApi.getUserRaw(uid); final String targetOsuAvatar = targetUser.getAvatarUrl(); diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index 31751f2a..b58744d5 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -796,8 +796,7 @@ public static String bgpContent(Context context, Response response) { public static String luckContent(Context ctx, DailyLuck.Luck luck, Beatmapset mapset, UploadedImage cover) { final List list = mapset.getBeatmaps().stream().map(Beatmap::getDifficultyRating).sorted().toList(); - String sb = at(ctx) + "\n" + - "## 今日运势" + "\n" + + String sb = at(ctx) + "你的今日运势" + "\n" + "> 人品值: **" + luck.luck() + "**/100\n" + "> 宜: " + luck.ups() + "\n" + "> 忌: " + luck.downs() + "\n\n" + From 9b1be31af997ba1b38b3f9679524e2c2e1b630ae Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 28 Sep 2026 15:03:35 +0800 Subject: [PATCH 83/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=A4=BE=E4=BA=A4?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E5=A4=84=E7=90=86=EF=BC=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=B8=BB=E9=A1=B5=E9=93=BE=E6=8E=A5=E6=96=87?= =?UTF-8?q?=E6=9C=AC=EF=BC=8C=E5=A2=9E=E5=BC=BA=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/command/handler/SocialCommandHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java index e0b8c1b1..8dff3d5a 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java @@ -99,7 +99,7 @@ public void handleFriendStatus(Context ctx) { final String mention = UserDataStore.findGroupOpenIdByUid(ctx.groupId(), uid) .map(ReplyFactory::at) .orElse(""); - ctx.sendReply(PendingMessage.ofMarkdownRaw(mention + ": [%d(点击打开主页)](%s)".formatted(uid, "https://osu.ppy.sh/users/" + uid))); + ctx.sendReply(PendingMessage.ofMarkdownRaw(mention + ": [%d(点击打开)](%s)".formatted(uid, "https://osu.ppy.sh/users/" + uid))); final UserExtended targetUser = OstellaApi.getUserRaw(uid); final String targetOsuAvatar = targetUser.getAvatarUrl(); From 9d5cfc3019a8404c0674353802ae3a9722b50149 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 28 Sep 2026 15:08:33 +0800 Subject: [PATCH 84/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=8F=91=E9=80=81=EF=BC=8C=E6=94=AF=E6=8C=81=E7=A7=81=E8=81=8A?= =?UTF-8?q?=E5=92=8C=E7=BE=A4=E8=81=8A=E7=9A=84Markdown=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E6=8F=90=E5=8D=87=E6=B6=88=E6=81=AF=E8=A1=A8=E7=8E=B0?= =?UTF-8?q?=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- src/main/java/xyz/zcraft/seira/bot/MessageSender.java | 7 +++++++ .../xyz/zcraft/seira/console/ConsoleCommandProcessor.java | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 31d189e8..f07e66ad 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.14 + 1.12.15 25 diff --git a/src/main/java/xyz/zcraft/seira/bot/MessageSender.java b/src/main/java/xyz/zcraft/seira/bot/MessageSender.java index ad38f3ab..9ade9d9d 100644 --- a/src/main/java/xyz/zcraft/seira/bot/MessageSender.java +++ b/src/main/java/xyz/zcraft/seira/bot/MessageSender.java @@ -153,6 +153,13 @@ public SentMessage sendGroupMarkdown(String groupId, String content) { return sendGroupMessage(groupId, message); } + public SentMessage sendPrivateMarkdown(String userId, String content) { + Message message = new Message(); + message.setMsgType(PendingMessage.MSG_TYPE_MARKDOWN); + message.setMarkdown(Message.MessageMarkdown.of(content)); + return sendPrivateMessage(userId, message); + } + private FileInfo retryUpload(Supplier operation, long baseDelayMillis, String description) { for (int attempt = 1; attempt <= MAX_UPLOAD_ATTEMPTS; attempt++) { try { diff --git a/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java b/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java index 741598ca..0bf52249 100644 --- a/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java +++ b/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java @@ -614,8 +614,8 @@ private ConsoleResult send(ConsoleInputParser.ParsedInput input) { } boolean sent = switch (targetType) { - case "group" -> messenger.sendGroupText(targetId, content) != null; - case "private" -> messenger.sendPrivateText(targetId, content) != null; + case "group" -> messenger.sendGroupMarkdown(targetId, content) != null; + case "private" -> messenger.sendPrivateMarkdown(targetId, content) != null; default -> throw new IllegalArgumentException("Message type must be 'group' or 'private'."); }; return sent From ac4a63b3217025d228a397dce5665f98bedbf831 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 28 Sep 2026 15:33:25 +0800 Subject: [PATCH 85/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AiChatHandler?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=BC=BA=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=EF=BC=8C=E6=8F=90=E4=BE=9B=E9=87=8D=E7=BD=AE?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E7=9A=84=E5=BB=BA=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index 01f4354a..9a925eb7 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -151,7 +151,11 @@ public void onComplete(String fullText) { public void onError(String errorCode, String errorMsg) { ctx.send( true, - PendingMessage.ofMarkdownRaw(at(ctx) + "回复生成失败了喵。\n>" + errorCode + ": " + errorMsg), + PendingMessage.ofMarkdownRaw( + at(ctx) + "回复生成失败了喵。\n" + + "> " + errorCode + ": " + errorMsg + "\n" + + "> 若重复出现错误,请尝试" + cmd("/ai reset", "重置会话") + ), true ); } From ec78bdda94952390ad348edbbbfabd809159937d Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 28 Sep 2026 18:14:56 +0800 Subject: [PATCH 86/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AgentService=20?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/xyz/zcraft/seira/ai/AgentService.java | 18 ++++++++++-------- .../xyz/zcraft/seira/ai/AiChatHandler.java | 10 +++++++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/AgentService.java index f4d0912f..6ff24646 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/AgentService.java @@ -266,13 +266,6 @@ public StopStatus requireStop(String groupId, String openId) { } } - public enum StopStatus { - SUCCESS, - NO_CONVERSATION, - NOT_SUPPORTED, - FAILED; - } - public boolean isRunning(String groupId, String openId) { final State state = states.get( StateOwner.of(groupId, openId) @@ -366,6 +359,13 @@ private State getOrCreateState(StateOwner owner) { } } + public enum StopStatus { + SUCCESS, + NO_CONVERSATION, + NOT_SUPPORTED, + FAILED + } + @Getter static final class State { private final ConcurrentHashMap vars; @@ -694,7 +694,9 @@ public String chatQueryStreaming( private void flushMessage(@NotNull StringBuilder currentMessage, @NotNull StreamHandler handler) { final String message = currentMessage.toString().trim(); - if (message.isBlank()) return; + if (message.isBlank() || "大模型接口调用出错,请联系管理员".equals(message)) { + return; + } currentMessage.setLength(0); diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index 9a925eb7..640bdd31 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -110,9 +110,10 @@ public void handleAi(Context ctx) { } public void handleChat(Context ctx, String message, List elems) { + final String at = at(ctx); if (agentService.isRunning(ctx.groupId(), ctx.senderUserId())) { ctx.sendReply(PendingMessage.ofMarkdownRaw( - at(ctx) + + at + "已有一轮对话正在进行中了喵,请稍作等待或" + cmd("/ai stop", "取消对话") + "~") ); return; @@ -139,7 +140,10 @@ public void handleChat(Context ctx, String message, List elems) { new StreamHandler() { @Override public void onText(String message) { - ctx.send(true, PendingMessage.ofMarkdownRaw(at(ctx) + message), true); + if (!message.trim().startsWith(at.trim())) { + message = at + message; + } + ctx.send(true, PendingMessage.ofMarkdownRaw(message), true); } @Override @@ -152,7 +156,7 @@ public void onError(String errorCode, String errorMsg) { ctx.send( true, PendingMessage.ofMarkdownRaw( - at(ctx) + "回复生成失败了喵。\n" + + at + "回复生成失败了喵。\n" + "> " + errorCode + ": " + errorMsg + "\n" + "> 若重复出现错误,请尝试" + cmd("/ai reset", "重置会话") ), From 4a371042ea086d1b14cfba4cb57b137b809d00c3 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 28 Sep 2026 18:39:41 +0800 Subject: [PATCH 87/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=20AsteroidApi=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E5=8A=9F=E8=83=BD=E5=B9=B6=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../xyz/zcraft/seira/api/AsteroidApi.java | 23 +++++++++++++++++++ .../seira/command/reply/ReplyFactory.java | 7 ++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index f07e66ad..a59ebd1b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.15 + 1.12.16 25 diff --git a/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java b/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java index 5ccf8256..ca9baeb2 100644 --- a/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java +++ b/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java @@ -1,6 +1,8 @@ package xyz.zcraft.seira.api; import com.google.gson.Gson; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import xyz.zcraft.seira.Seira; import xyz.zcraft.seira.api.data.MinecraftServerStatus; import xyz.zcraft.seira.api.data.RawResponse; @@ -53,4 +55,25 @@ public static MinecraftServerStatus getMinecraftServerStatus(String addr) { throw new RuntimeException("获取 MC 服务器状态失败", e); } } + + public static boolean getServerStatus() { + try { + var request = requestBuilder("/health") + .GET() + .build(); + + final HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + return true; + } + } catch (Exception e) { + LOG.error("Failed to get server status", e); + } + + LOG.warn("Asteroid server is down."); + return false; + } + + private static final Logger LOG = LogManager.getLogger(AsteroidApi.class); } diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index b58744d5..83cfaa2a 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -5,6 +5,7 @@ import com.google.gson.JsonObject; import org.jetbrains.annotations.NotNull; import xyz.zcraft.osu.model.*; +import xyz.zcraft.seira.api.AsteroidApi; import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.data.*; import xyz.zcraft.seira.bot.data.Button; @@ -731,11 +732,13 @@ public static String statContent(Context ctx, OstellaApi.ServerStatus status) { "> oStella API: " + (status.oStella() ? "✅ 正常" : "❌ 无法访问") + "\n"; if (status.oStella()) { - stat += "> osuRenderer: " + (status.onlineWorkers() + " / " + status.allWorkers()) + stat += "> ↳ osuRenderer: " + (status.onlineWorkers() + " / " + status.allWorkers()) + (status.onlineWorkers() > 0 ? " (✅在线)" : " (❌全部离线)") + "\n"; - stat += "> osu! API: " + (status.osu() ? "✅ 正常" : "❌ 无法访问") + "\n"; + stat += "> ↳ osu! API: " + (status.osu() ? "✅ 正常" : "❌ 无法访问") + "\n"; } + stat += "> Asteroid API: " + (AsteroidApi.getServerStatus() ? "✅ 正常" : "❌ 无法访问") + "\n"; + String version = "## 版本信息" + "\n" + "> SeiraCore: " + VersionInfo.getVersion() + "\n"; From 830ddf3bdd734b8ea1e1373d6467ffbc6f73a92e Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 28 Sep 2026 18:41:39 +0800 Subject: [PATCH 88/96] =?UTF-8?q?=E4=BC=98=E5=8C=96import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/api/RomAIApi.java | 1 - src/main/java/xyz/zcraft/seira/bot/MessageSender.java | 2 -- .../java/xyz/zcraft/seira/command/TaskCoordinator.java | 2 +- .../seira/command/handler/BeatmapCommandHandler.java | 2 +- .../seira/command/handler/GeneralCommandHandler.java | 3 +-- .../seira/command/handler/ReplayCommandHandler.java | 10 +++------- .../seira/command/handler/ScoreCommandHandler.java | 2 +- .../seira/command/handler/SocialCommandHandler.java | 2 +- .../xyz/zcraft/seira/command/parse/CommandParser.java | 1 - .../java/xyz/zcraft/seira/command/parse/Resolver.java | 1 - .../xyz/zcraft/seira/command/parse/TargetInput.java | 1 - .../xyz/zcraft/seira/command/reply/ReplyFactory.java | 9 ++++----- .../java/xyz/zcraft/seira/command/route/Router.java | 1 - .../zcraft/seira/console/ConsoleRuntimeControl.java | 1 - src/main/java/xyz/zcraft/seira/db/UserDataStore.java | 1 - .../java/xyz/zcraft/seira/rankguess/data/Round.java | 2 -- .../java/xyz/zcraft/seira/services/AiPermission.java | 2 -- src/main/java/xyz/zcraft/seira/util/ImageUtil.java | 3 --- .../xyz/zcraft/seira/watch/WatchScoreNotifier.java | 6 ------ 19 files changed, 12 insertions(+), 40 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/api/RomAIApi.java b/src/main/java/xyz/zcraft/seira/api/RomAIApi.java index 0fe597e7..e83096fb 100644 --- a/src/main/java/xyz/zcraft/seira/api/RomAIApi.java +++ b/src/main/java/xyz/zcraft/seira/api/RomAIApi.java @@ -8,7 +8,6 @@ import org.apache.logging.log4j.Logger; import xyz.zcraft.seira.Seira; import xyz.zcraft.seira.api.data.RomAIMatch; -import xyz.zcraft.seira.config.DiscordProxyConfig; import java.net.InetSocketAddress; import java.net.ProxySelector; diff --git a/src/main/java/xyz/zcraft/seira/bot/MessageSender.java b/src/main/java/xyz/zcraft/seira/bot/MessageSender.java index 9ade9d9d..3579d6cd 100644 --- a/src/main/java/xyz/zcraft/seira/bot/MessageSender.java +++ b/src/main/java/xyz/zcraft/seira/bot/MessageSender.java @@ -1,6 +1,5 @@ package xyz.zcraft.seira.bot; -import com.google.gson.Gson; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import xyz.zcraft.seira.api.data.QqUploadRequest; @@ -12,7 +11,6 @@ import xyz.zcraft.seira.services.CosService; import xyz.zcraft.seira.util.TokenManager; -import java.util.Map; import java.util.function.Supplier; public class MessageSender { diff --git a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java index be052fdf..22006e83 100644 --- a/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java +++ b/src/main/java/xyz/zcraft/seira/command/TaskCoordinator.java @@ -3,8 +3,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; -import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.ApiRequestException; +import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.ReplayRenderException; import xyz.zcraft.seira.api.data.Base64Bytes; import xyz.zcraft.seira.api.data.QqUploadRequest; diff --git a/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java index 37bbdca7..ebee30e0 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/BeatmapCommandHandler.java @@ -9,10 +9,10 @@ import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.command.ResolutionException; -import xyz.zcraft.seira.command.parse.TargetInput; import xyz.zcraft.seira.command.TargetHistory; import xyz.zcraft.seira.command.TaskCoordinator; import xyz.zcraft.seira.command.parse.Resolver; +import xyz.zcraft.seira.command.parse.TargetInput; import xyz.zcraft.seira.command.reply.CommandUsage; import xyz.zcraft.seira.command.reply.ReplyFactory; import xyz.zcraft.seira.data.SendResult; diff --git a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java index 6ac6f80f..9b930879 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java @@ -22,7 +22,6 @@ import java.util.function.Predicate; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; -import static xyz.zcraft.seira.command.reply.ReplyFactory.u; public final class GeneralCommandHandler { private final MessageSender messageSender; @@ -110,7 +109,7 @@ public void handleFaq(Context context) { } public void handleStat(Context context) { - context.sendReply(replyFactory.statusMessage(context, OstellaApi.getServerStatus())); + context.sendReply(replyFactory.statusMessage(context, OstellaApi.getServerStatus(), AsteroidApi.getServerStatus())); } public void handleUnknown(Context context) { diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java index db0d0fba..df9877aa 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ReplayCommandHandler.java @@ -4,20 +4,16 @@ import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.data.VideoRenderRecord; import xyz.zcraft.seira.bot.data.PendingMessage; -import xyz.zcraft.seira.command.Context; -import xyz.zcraft.seira.command.ResolutionException; -import xyz.zcraft.seira.command.parse.TargetInput; -import xyz.zcraft.seira.command.ReplayResultStore; -import xyz.zcraft.seira.command.TargetHistory; -import xyz.zcraft.seira.command.TaskCoordinator; +import xyz.zcraft.seira.command.*; import xyz.zcraft.seira.command.parse.Resolver; +import xyz.zcraft.seira.command.parse.TargetInput; import xyz.zcraft.seira.command.reply.CommandUsage; import xyz.zcraft.seira.command.reply.ReplyFactory; import xyz.zcraft.seira.data.SendResult; import xyz.zcraft.seira.util.TimeDurationParser; -import java.util.Objects; import java.util.List; +import java.util.Objects; import java.util.UUID; import java.util.function.Predicate; diff --git a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java index c89182a7..086a34e4 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/ScoreCommandHandler.java @@ -5,11 +5,11 @@ import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.command.ResolutionException; -import xyz.zcraft.seira.command.parse.TargetInput; import xyz.zcraft.seira.command.TargetHistory; import xyz.zcraft.seira.command.TaskCoordinator; import xyz.zcraft.seira.command.parse.Resolver; import xyz.zcraft.seira.command.parse.ScoreFilterArguments; +import xyz.zcraft.seira.command.parse.TargetInput; import xyz.zcraft.seira.command.reply.CommandUsage; import xyz.zcraft.seira.command.reply.ReplyFactory; diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java index 8dff3d5a..a7c10ade 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SocialCommandHandler.java @@ -8,10 +8,10 @@ import xyz.zcraft.seira.api.data.Response; import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.command.Context; +import xyz.zcraft.seira.command.ResolutionException; import xyz.zcraft.seira.command.TaskCoordinator; import xyz.zcraft.seira.command.parse.Resolver; import xyz.zcraft.seira.command.parse.TargetInput; -import xyz.zcraft.seira.command.ResolutionException; import xyz.zcraft.seira.command.reply.CommandUsage; import xyz.zcraft.seira.command.reply.ReplyFactory; import xyz.zcraft.seira.db.UserDataStore; diff --git a/src/main/java/xyz/zcraft/seira/command/parse/CommandParser.java b/src/main/java/xyz/zcraft/seira/command/parse/CommandParser.java index bcac396d..108fafb3 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/CommandParser.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/CommandParser.java @@ -1,7 +1,6 @@ package xyz.zcraft.seira.command.parse; import xyz.zcraft.seira.command.Context; -import xyz.zcraft.seira.services.AiPermission; import java.util.*; import java.util.function.UnaryOperator; diff --git a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java index 0d638cb1..d6583391 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/Resolver.java @@ -4,7 +4,6 @@ import xyz.zcraft.seira.command.ResolutionException; import xyz.zcraft.seira.db.UserDataStore; -import java.nio.file.Path; import java.util.*; import java.util.regex.MatchResult; import java.util.regex.Matcher; diff --git a/src/main/java/xyz/zcraft/seira/command/parse/TargetInput.java b/src/main/java/xyz/zcraft/seira/command/parse/TargetInput.java index c85fe675..92cf495a 100644 --- a/src/main/java/xyz/zcraft/seira/command/parse/TargetInput.java +++ b/src/main/java/xyz/zcraft/seira/command/parse/TargetInput.java @@ -3,7 +3,6 @@ import xyz.zcraft.seira.command.ResolutionException; import java.util.Locale; -import java.util.Random; import java.util.concurrent.ThreadLocalRandom; import java.util.regex.Pattern; diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index 83cfaa2a..57df47f1 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -5,7 +5,6 @@ import com.google.gson.JsonObject; import org.jetbrains.annotations.NotNull; import xyz.zcraft.osu.model.*; -import xyz.zcraft.seira.api.AsteroidApi; import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.data.*; import xyz.zcraft.seira.bot.data.Button; @@ -425,9 +424,9 @@ public PendingMessage scoreMissesMessage(Context ctx, Response> s ); } - public PendingMessage statusMessage(Context ctx, OstellaApi.ServerStatus status) { + public PendingMessage statusMessage(Context ctx, OstellaApi.ServerStatus status, boolean asteroid) { return PendingMessage.ofMarkdownRaw( - Contents.statContent(ctx, status), null + Contents.statContent(ctx, status, asteroid), null ); } @@ -725,7 +724,7 @@ public static String scoreMissesContent(Context ctx, Response> sc return sb.toString().trim(); } - public static String statContent(Context ctx, OstellaApi.ServerStatus status) { + public static String statContent(Context ctx, OstellaApi.ServerStatus status, boolean asteroid) { String stat = at(ctx) + "\n" + "## 服务器状态\n" + "> 消息网关: ✅ 正常\n" + @@ -737,7 +736,7 @@ public static String statContent(Context ctx, OstellaApi.ServerStatus status) { stat += "> ↳ osu! API: " + (status.osu() ? "✅ 正常" : "❌ 无法访问") + "\n"; } - stat += "> Asteroid API: " + (AsteroidApi.getServerStatus() ? "✅ 正常" : "❌ 无法访问") + "\n"; + stat += "> Asteroid API: " + (asteroid ? "✅ 正常" : "❌ 无法访问") + "\n"; String version = "## 版本信息" + "\n" + "> SeiraCore: " + VersionInfo.getVersion() + "\n"; diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index a5e0ef18..45c8b195 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -31,7 +31,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; diff --git a/src/main/java/xyz/zcraft/seira/console/ConsoleRuntimeControl.java b/src/main/java/xyz/zcraft/seira/console/ConsoleRuntimeControl.java index 896ff25e..b53fae1a 100644 --- a/src/main/java/xyz/zcraft/seira/console/ConsoleRuntimeControl.java +++ b/src/main/java/xyz/zcraft/seira/console/ConsoleRuntimeControl.java @@ -1,6 +1,5 @@ package xyz.zcraft.seira.console; -import xyz.zcraft.seira.bot.data.GroupInfo; import xyz.zcraft.seira.watch.WatchView; import java.time.Duration; diff --git a/src/main/java/xyz/zcraft/seira/db/UserDataStore.java b/src/main/java/xyz/zcraft/seira/db/UserDataStore.java index 19b204c3..56e29129 100644 --- a/src/main/java/xyz/zcraft/seira/db/UserDataStore.java +++ b/src/main/java/xyz/zcraft/seira/db/UserDataStore.java @@ -10,7 +10,6 @@ import java.sql.*; import java.util.*; -import java.util.stream.Collectors; public final class UserDataStore { private static final Logger LOG = LogManager.getLogger(UserDataStore.class); diff --git a/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java b/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java index 73c45a86..4fc4eeb2 100644 --- a/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java +++ b/src/main/java/xyz/zcraft/seira/rankguess/data/Round.java @@ -9,14 +9,12 @@ import xyz.zcraft.seira.util.ImageUtil; import xyz.zcraft.seira.util.WeightedRandom; -import java.awt.*; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.function.Function; -import java.util.function.Supplier; public record Round(long userId, long scoreId, int bestIndex, long actualRank, Double pp, RandomScore randomScore, boolean standard) { diff --git a/src/main/java/xyz/zcraft/seira/services/AiPermission.java b/src/main/java/xyz/zcraft/seira/services/AiPermission.java index 8fc4f595..658660f7 100644 --- a/src/main/java/xyz/zcraft/seira/services/AiPermission.java +++ b/src/main/java/xyz/zcraft/seira/services/AiPermission.java @@ -9,8 +9,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; import java.util.Set; public class AiPermission { diff --git a/src/main/java/xyz/zcraft/seira/util/ImageUtil.java b/src/main/java/xyz/zcraft/seira/util/ImageUtil.java index ca6ba8ed..84211ccc 100644 --- a/src/main/java/xyz/zcraft/seira/util/ImageUtil.java +++ b/src/main/java/xyz/zcraft/seira/util/ImageUtil.java @@ -3,11 +3,8 @@ import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; -import java.awt.image.ConvolveOp; -import java.awt.image.Kernel; import java.io.ByteArrayOutputStream; import java.net.URI; -import java.util.Arrays; import static java.lang.Math.clamp; diff --git a/src/main/java/xyz/zcraft/seira/watch/WatchScoreNotifier.java b/src/main/java/xyz/zcraft/seira/watch/WatchScoreNotifier.java index bd9839fd..4181115c 100644 --- a/src/main/java/xyz/zcraft/seira/watch/WatchScoreNotifier.java +++ b/src/main/java/xyz/zcraft/seira/watch/WatchScoreNotifier.java @@ -1,16 +1,10 @@ package xyz.zcraft.seira.watch; -import xyz.zcraft.osu.model.Score; import xyz.zcraft.seira.bot.MessageSender; -import xyz.zcraft.seira.bot.data.FileInfo; -import xyz.zcraft.seira.bot.data.Message; -import xyz.zcraft.seira.bot.data.PendingMessage; import xyz.zcraft.seira.data.UploadedImage; -import java.util.Base64; import java.util.Objects; -import static xyz.zcraft.seira.command.reply.ReplyFactory.cmd; import static xyz.zcraft.seira.command.reply.ReplyFactory.s; public final class WatchScoreNotifier { From 13a178fb232252f61b7989e8c275cca31e66f091 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 28 Sep 2026 18:43:36 +0800 Subject: [PATCH 89/96] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20AsteroidApi=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/zcraft/seira/api/AsteroidApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java b/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java index ca9baeb2..56df3239 100644 --- a/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java +++ b/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java @@ -64,7 +64,7 @@ public static boolean getServerStatus() { final HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() != 200) { + if (response.statusCode() == 200) { return true; } } catch (Exception e) { From 36671cccbd45381642413ed7b61d5ce25e3464a7 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 28 Sep 2026 19:28:28 +0800 Subject: [PATCH 90/96] =?UTF-8?q?=E9=87=8D=E6=9E=84=20AiChatHandler?= =?UTF-8?q?=EF=BC=8C=E6=9B=BF=E6=8D=A2=20AgentService=20=E4=B8=BA=20ChatPr?= =?UTF-8?q?ovider=EF=BC=8C=E5=A2=9E=E5=BC=BA=E4=BB=A3=E7=A0=81=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/ai/AiChatHandler.java | 21 +++++----- .../seira/ai/provider/ChatProvider.java | 39 +++++++++++++++++++ .../seira/ai/provider/ChatProviders.java | 9 +++++ .../HiAgentProvider.java} | 23 +++++------ src/main/java/xyz/zcraft/seira/bot/QQBot.java | 11 +++--- .../zcraft/seira/command/route/Router.java | 6 +-- 6 files changed, 80 insertions(+), 29 deletions(-) create mode 100644 src/main/java/xyz/zcraft/seira/ai/provider/ChatProvider.java create mode 100644 src/main/java/xyz/zcraft/seira/ai/provider/ChatProviders.java rename src/main/java/xyz/zcraft/seira/ai/{AgentService.java => provider/HiAgentProvider.java} (98%) diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index 640bdd31..43a4249c 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -3,6 +3,7 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; import xyz.zcraft.seira.ai.data.AgentFile; +import xyz.zcraft.seira.ai.provider.ChatProvider; import xyz.zcraft.seira.bot.data.Attachment; import xyz.zcraft.seira.bot.data.GroupBotState; import xyz.zcraft.seira.bot.data.MsgElem; @@ -24,17 +25,17 @@ public class AiChatHandler { private static final Gson GSON = new Gson(); private final Resolver resolver; private final Predicate adminAuthorizer; - private final AgentService agentService; + private final ChatProvider chatProvider; private final Function botStateGetter; private final Map> groupMentionedHistory = new ConcurrentHashMap<>(); public AiChatHandler( - Resolver resolver, AgentService agentService, Predicate isAdmin, + Resolver resolver, ChatProvider chatProvider, Predicate isAdmin, Function botStateGetter ) { this.resolver = resolver; this.adminAuthorizer = isAdmin; - this.agentService = agentService; + this.chatProvider = chatProvider; this.botStateGetter = botStateGetter; } @@ -78,11 +79,11 @@ public void handleAi(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已禁用本群AI对话喵。")); return; } else if ("reset".equalsIgnoreCase(ctx.argument(0))) { - agentService.clearState(ctx.groupId(), ctx.senderUserId()); + chatProvider.clearState(ctx.groupId(), ctx.senderUserId()); ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已重置你在本群的AI对话状态喵。")); return; } else if ("resetme".equalsIgnoreCase(ctx.argument(0))) { - final int i = agentService.clearStateOfUser(ctx.senderUserId()); + final int i = chatProvider.clearStateOfUser(ctx.senderUserId()); ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已重置你在" + i + "个群中的AI对话状态喵。")); return; } else if ("resetgroup".equalsIgnoreCase(ctx.argument(0))) { @@ -90,11 +91,11 @@ public void handleAi(Context ctx) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "你无权使用该命令喵。")); return; } - final int i = agentService.clearStateOfGroup(ctx.groupId()); + final int i = chatProvider.clearStateOfGroup(ctx.groupId()); ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已重置本群" + i + "个用户的AI对话状态喵。")); return; } else if ("stop".equalsIgnoreCase(ctx.argument(0))) { - final AgentService.StopStatus stopStatus = agentService.requireStop(ctx.groupId(), ctx.senderUserId()); + final ChatProvider.StopStatus stopStatus = chatProvider.requireStop(ctx.groupId(), ctx.senderUserId()); ctx.sendReply(at(ctx) + switch (stopStatus) { case SUCCESS -> "已停止你在本群的AI对话喵。"; case FAILED -> "停止AI对话失败了喵。"; @@ -111,7 +112,7 @@ public void handleAi(Context ctx) { public void handleChat(Context ctx, String message, List elems) { final String at = at(ctx); - if (agentService.isRunning(ctx.groupId(), ctx.senderUserId())) { + if (chatProvider.isRunning(ctx.groupId(), ctx.senderUserId())) { ctx.sendReply(PendingMessage.ofMarkdownRaw( at + "已有一轮对话正在进行中了喵,请稍作等待或" + cmd("/ai stop", "取消对话") + "~") @@ -132,7 +133,7 @@ public void handleChat(Context ctx, String message, List elems) { .map(MsgElem::content) .orElse(null); - agentService.input( + chatProvider.input( ctx.groupId(), ctx.senderUserId(), message, @@ -222,6 +223,6 @@ public void recordHistory(String groupId, String userId, String rawContent, List sb.append("\n").append("![%s](%s)".formatted(attachment.filename(), attachment.url())); } } - agentService.recordHistory(groupId, userId, sb.toString()); + chatProvider.recordHistory(groupId, userId, sb.toString()); } } diff --git a/src/main/java/xyz/zcraft/seira/ai/provider/ChatProvider.java b/src/main/java/xyz/zcraft/seira/ai/provider/ChatProvider.java new file mode 100644 index 00000000..0282c534 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/ai/provider/ChatProvider.java @@ -0,0 +1,39 @@ +package xyz.zcraft.seira.ai.provider; + +import xyz.zcraft.seira.ai.StreamHandler; +import xyz.zcraft.seira.ai.data.AgentFile; + +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +public interface ChatProvider { + int CONTEXT_SIZE = 30; + + void recordHistory(String groupId, String sender, String message); + + String input( + String groupId, String openId, String rawContent, + Function contextFunc, StreamHandler handler, + List attachments, String refContent + ); + + StopStatus requireStop(String groupId, String openId); + + boolean isRunning(String groupId, String openId); + + boolean clearState(String groupId, String openId); + + int clearStateOfGroup(String groupId); + + int clearStateOfUser(String openId); + + Set activeGroupIds(); + + enum StopStatus { + SUCCESS, + NO_CONVERSATION, + NOT_SUPPORTED, + FAILED + } +} diff --git a/src/main/java/xyz/zcraft/seira/ai/provider/ChatProviders.java b/src/main/java/xyz/zcraft/seira/ai/provider/ChatProviders.java new file mode 100644 index 00000000..700e7189 --- /dev/null +++ b/src/main/java/xyz/zcraft/seira/ai/provider/ChatProviders.java @@ -0,0 +1,9 @@ +package xyz.zcraft.seira.ai.provider; + +import xyz.zcraft.seira.config.LLMConfig; + +public class ChatProviders { + public static ChatProvider newHiAgentChatProvider(LLMConfig config) { + return new HiAgentProvider(config); + } +} diff --git a/src/main/java/xyz/zcraft/seira/ai/AgentService.java b/src/main/java/xyz/zcraft/seira/ai/provider/HiAgentProvider.java similarity index 98% rename from src/main/java/xyz/zcraft/seira/ai/AgentService.java rename to src/main/java/xyz/zcraft/seira/ai/provider/HiAgentProvider.java index 6ff24646..7831999c 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AgentService.java +++ b/src/main/java/xyz/zcraft/seira/ai/provider/HiAgentProvider.java @@ -1,4 +1,4 @@ -package xyz.zcraft.seira.ai; +package xyz.zcraft.seira.ai.provider; import com.google.gson.Gson; import com.google.gson.JsonObject; @@ -8,6 +8,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; +import xyz.zcraft.seira.ai.StreamHandler; import xyz.zcraft.seira.ai.data.AgentFile; import xyz.zcraft.seira.ai.data.AppConversationBrief; import xyz.zcraft.seira.ai.data.ChatQueryResponse; @@ -29,8 +30,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -public class AgentService { - public static final int CONTEXT_SIZE = 30; +class HiAgentProvider implements ChatProvider { // // private static final Pattern QQ_FACE = Pattern.compile( @@ -49,7 +49,7 @@ public class AgentService { private final Map> chatLog = new ConcurrentHashMap<>(); private final Map stateCreationLocks = new ConcurrentHashMap<>(); - public AgentService(LLMConfig config) { + protected HiAgentProvider(LLMConfig config) { this.api = new Api(config); } @@ -135,6 +135,7 @@ private static String processMessage(String original) { } } + @Override public void recordHistory(String groupId, String sender, String message) { if (groupId == null || groupId.isEmpty() || sender == null || sender.isEmpty() @@ -163,6 +164,7 @@ public void recordHistory(String groupId, String sender, String message) { } } + @Override public String input( String groupId, String openId, String rawContent, Function contextFunc, StreamHandler handler, @@ -246,6 +248,7 @@ public String input( } } + @Override public StopStatus requireStop(String groupId, String openId) { final StateOwner owner = StateOwner.of(groupId, openId); final State state = states.get(owner); @@ -266,6 +269,7 @@ public StopStatus requireStop(String groupId, String openId) { } } + @Override public boolean isRunning(String groupId, String openId) { final State state = states.get( StateOwner.of(groupId, openId) @@ -274,6 +278,7 @@ public boolean isRunning(String groupId, String openId) { return state != null && state.running.get(); } + @Override public boolean clearState(String groupId, String openId) { final State state = states.get( StateOwner.of(groupId, openId) @@ -287,6 +292,7 @@ public boolean clearState(String groupId, String openId) { return true; } + @Override public int clearStateOfGroup(String groupId) { int count = 0; @@ -302,6 +308,7 @@ public int clearStateOfGroup(String groupId) { return count; } + @Override public int clearStateOfUser(String openId) { int count = 0; @@ -317,6 +324,7 @@ public int clearStateOfUser(String openId) { return count; } + @Override public Set activeGroupIds() { return states.entrySet() .stream() @@ -359,13 +367,6 @@ private State getOrCreateState(StateOwner owner) { } } - public enum StopStatus { - SUCCESS, - NO_CONVERSATION, - NOT_SUPPORTED, - FAILED - } - @Getter static final class State { private final ConcurrentHashMap vars; diff --git a/src/main/java/xyz/zcraft/seira/bot/QQBot.java b/src/main/java/xyz/zcraft/seira/bot/QQBot.java index 7c733fc7..d9b01304 100644 --- a/src/main/java/xyz/zcraft/seira/bot/QQBot.java +++ b/src/main/java/xyz/zcraft/seira/bot/QQBot.java @@ -4,7 +4,8 @@ import lombok.Getter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import xyz.zcraft.seira.ai.AgentService; +import xyz.zcraft.seira.ai.provider.ChatProvider; +import xyz.zcraft.seira.ai.provider.ChatProviders; import xyz.zcraft.seira.bot.data.*; import xyz.zcraft.seira.command.AttachmentHandler; import xyz.zcraft.seira.command.route.Router; @@ -45,7 +46,7 @@ public class QQBot implements AutoCloseable, ConsoleRuntimeControl { private final ScoreWatchService watchService; private final MPWatchService mpWatchService; private final RankGuessGameService rankGuessGameService; - private final AgentService agentService; + private final ChatProvider chatProvider; private final RealtimeServiceInterruptionNotifier interruptionNotifier; private final DiscordBridgeService discordBridgeService; private final AppConfig startupConfig; @@ -103,7 +104,7 @@ public QQBot( this.rankGuessGameService = new RankGuessGameService(); LOG.info("Initializing agents service"); - this.agentService = new AgentService(config.llm()); + this.chatProvider = ChatProviders.newHiAgentChatProvider(config.llm()); this.attachmentHandler = new AttachmentHandler(executors.attachmentDownloads()); this.router = new Router( @@ -126,7 +127,7 @@ public QQBot( } }, self::get, - agentService, + chatProvider, s -> QQApi.getGroupBotState(tokenManager.getToken(), s) ); @@ -313,7 +314,7 @@ public void requestStop() { watchService.activeTransientGroupIds(), rankGuessGameService.activeGroupIds(), mpWatchService.activeGroupIds(), - agentService.activeGroupIds() + chatProvider.activeGroupIds() ); if (result.failedGroups() == 0) { LOG.info("Sent restart interruption notices to {} affected groups", result.sentGroups()); diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index 45c8b195..e5a4537a 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -3,7 +3,7 @@ import lombok.Getter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import xyz.zcraft.seira.ai.AgentService; +import xyz.zcraft.seira.ai.provider.ChatProvider; import xyz.zcraft.seira.ai.AiChatHandler; import xyz.zcraft.seira.api.data.OsuToken; import xyz.zcraft.seira.api.data.VideoRenderRecord; @@ -59,7 +59,7 @@ public Router( BindingService bindingService, ScoreWatchService watchService, MPWatchService mpWatchService, DiscordBridgeService discordBridgeService, RankGuessGameService rankGuessGameService, Executor commandExecutor, Runnable commandMetric, Function imageUploader, Supplier selfSupplier, - AgentService agentService, Function botStateGetter + ChatProvider chatProvider, Function botStateGetter ) { this.configSupplier = java.util.Objects.requireNonNull(configSupplier); this.commandExecutor = commandExecutor; @@ -90,7 +90,7 @@ public Router( messageSender, taskCoordinator, replyFactory, resolver, admins::isAdmin ); this.aiChatHandler = new AiChatHandler( - resolver, agentService, admins::isAdmin, botStateGetter + resolver, chatProvider, admins::isAdmin, botStateGetter ); WatchCommandHandler watchCommands = new WatchCommandHandler(resolver, taskCoordinator, watchService, admins::isAdmin); SpecificScoreWatchCommandHandler specificScoreWatchCommands = From ba74366cbb963f14d89a9e2f1b5caaa9ac0d4b7e Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Mon, 28 Sep 2026 20:42:30 +0800 Subject: [PATCH 91/96] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=9D=83=E9=99=90?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=93=BE=E6=8E=A5=EF=BC=8C=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E5=B8=B8=E9=87=8F=E6=9B=BF=E4=BB=A3=E7=A1=AC=E7=BC=96=E7=A0=81?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=BC=BA=E4=BB=A3=E7=A0=81=E5=8F=AF=E7=BB=B4?= =?UTF-8?q?=E6=8A=A4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../xyz/zcraft/seira/ai/AiChatHandler.java | 3 +- .../command/handler/DcsCommandHandler.java | 3 +- .../handler/GeneralCommandHandler.java | 4 + .../handler/MPWatchCommandHandler.java | 5 +- .../handler/RankGuessCommandHandler.java | 3 +- .../SpecificScoreWatchCommandHandler.java | 5 +- .../command/handler/WatchCommandHandler.java | 3 +- .../seira/command/reply/ReplyFactory.java | 86 +++++++++++++------ .../zcraft/seira/command/route/Router.java | 1 + 10 files changed, 80 insertions(+), 35 deletions(-) diff --git a/pom.xml b/pom.xml index a59ebd1b..1b46429f 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.16 + 1.12.17 25 diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index 43a4249c..34d0aa74 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -18,6 +18,7 @@ import java.util.function.Function; import java.util.function.Predicate; +import static xyz.zcraft.seira.command.reply.ReplyFactory.ExternalUrls.PERMISSION; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; import static xyz.zcraft.seira.command.reply.ReplyFactory.cmd; @@ -65,7 +66,7 @@ public void handleAi(Context ctx) { } else { ctx.sendReply(PendingMessage.ofMarkdownRaw( at(ctx) + "由于本群未配置权限或配置不完整,暂无法启用本群AI对话喵。" + - "权限配置见[这里](https://docs.seira.top/overview/use.html#extra-permission)~") + "权限配置见[这里](" + PERMISSION + ")~") ); } return; diff --git a/src/main/java/xyz/zcraft/seira/command/handler/DcsCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/DcsCommandHandler.java index e598e5e7..3d8990ed 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/DcsCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/DcsCommandHandler.java @@ -8,6 +8,7 @@ import java.util.Locale; import java.util.Objects; +import static xyz.zcraft.seira.command.reply.ReplyFactory.ExternalUrls.PERMISSION; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; public final class DcsCommandHandler { @@ -52,7 +53,7 @@ private void handleStart(Context ctx) { final boolean b = ctx.sendMessage(PendingMessage.ofString("正在尝试开启 Discord 消息同步,请稍候...")).success(); if (!b) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "由于缺少主动消息权限,无法添加消息同步!权限配置请见[这里](https://docs.seira.top/overview/use.html#extra-permission)~")); + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "由于缺少主动消息权限,无法添加消息同步!权限配置请见[这里](" + PERMISSION + ")~")); return; } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java index 9b930879..9684be8f 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java @@ -104,6 +104,10 @@ public void handleHelp(Context context) { context.sendReply(replyFactory.helpMessage(context)); } + public void handleUsages(Context context) { + context.sendReply(replyFactory.usagesMessage(context)); + } + public void handleFaq(Context context) { context.sendReply(replyFactory.faqMessage(context)); } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java index f0829fd0..fcd97ce4 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/MPWatchCommandHandler.java @@ -23,6 +23,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static xyz.zcraft.seira.command.reply.ReplyFactory.ExternalUrls.PERMISSION; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; public final class MPWatchCommandHandler { @@ -175,7 +176,7 @@ public void handleRomAI(Context ctx) { try (var _ = taskCoordinator.beginRequest(ctx, "Start RomAI Watch")) { if (!ctx.sendMessage(msg).success()) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + - "由于缺少主动消息权限,无法启动监视!权限配置请见:" + "由于缺少主动消息权限,无法启动监视!权限配置请见[这里](" + PERMISSION + ")~" )); return; } @@ -236,7 +237,7 @@ private void handleStart(Context ctx, int argumentOffset) { try (var _ = taskCoordinator.beginRequest(ctx, "Start Multiplayer Room Watch")) { if (!ctx.sendMessage(PendingMessage.ofString("正在尝试启动多人房间监视……")).success()) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + - "由于缺少主动消息权限,无法启动监视!权限配置请见:https://docs.seira.top/overview/use.html#extra-permission" + "由于缺少主动消息权限,无法启动监视!权限配置请见[这里](" + PERMISSION + ")~" )); return; } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java index 2187072a..6d9f9bf1 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/RankGuessCommandHandler.java @@ -29,6 +29,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static xyz.zcraft.seira.command.reply.ReplyFactory.ExternalUrls.PERMISSION; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; import static xyz.zcraft.seira.command.reply.ReplyFactory.cmd; @@ -468,7 +469,7 @@ private void start(Context ctx, boolean fromGroup) { content += ",正在渲染回放片段..."; if (!activeMessageEnabled) { - content += "\n\n> 提示: 由于缺少主动消息权限,阶段提示与自动结束已禁用。稍后需要使用 `/rg end` 手动结束。权限配置请见[这里](https://docs.seira.top/overview/use.html#extra-permission)。"; + content += "\n\n> 提示: 由于缺少主动消息权限,阶段提示与自动结束已禁用。稍后需要使用 `/rg end` 手动结束。权限配置请见[这里](" + PERMISSION + ")。"; } ctx.sendReply(PendingMessage.ofMarkdownRaw(content)); diff --git a/src/main/java/xyz/zcraft/seira/command/handler/SpecificScoreWatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/SpecificScoreWatchCommandHandler.java index cce656b4..2f8e3f3f 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/SpecificScoreWatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/SpecificScoreWatchCommandHandler.java @@ -11,6 +11,7 @@ import java.util.Objects; import java.util.Set; +import static xyz.zcraft.seira.command.reply.ReplyFactory.ExternalUrls.PERMISSION; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; public final class SpecificScoreWatchCommandHandler { @@ -74,10 +75,10 @@ private void handleStart(Context ctx) { return; } - try (var timing = taskCoordinator.beginRequest(ctx, "Start Specific Score Watch")) { + try (var _ = taskCoordinator.beginRequest(ctx, "Start Specific Score Watch")) { if (!ctx.sendMessage(PendingMessage.ofString("正在尝试启动指定谱面成绩监视……")).success()) { ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + - "由于缺少主动消息权限,无法启动监视!权限配置请见:https://docs.seira.top/overview/use.html#extra-permission" + "由于缺少主动消息权限,无法启动监视!权限配置请见:" + PERMISSION )); return; } diff --git a/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java index 7ab1f4b5..4a4ef459 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/WatchCommandHandler.java @@ -18,6 +18,7 @@ import java.util.Objects; import java.util.function.Predicate; +import static xyz.zcraft.seira.command.reply.ReplyFactory.ExternalUrls.PERMISSION; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; public final class WatchCommandHandler { @@ -145,7 +146,7 @@ private void handleAdd(Context ctx) { at(ctx) + "正在尝试添加监视..." )).success(); if (!b) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "由于缺少主动消息权限,无法添加监视!权限配置请见[这里](https://docs.seira.top/overview/use.html#extra-permission)~")); + ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "由于缺少主动消息权限,无法添加监视!权限配置请见[这里](" + PERMISSION + ")~")); return; } diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index 57df47f1..fe77eafe 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -27,6 +27,8 @@ import java.util.function.Supplier; import java.util.stream.Stream; +import static xyz.zcraft.seira.command.reply.ReplyFactory.ExternalUrls.*; + public final class ReplyFactory { private final Supplier configSupplier; @@ -434,6 +436,10 @@ public PendingMessage helpMessage(Context ctx) { return PendingMessage.ofMarkdownRaw(Contents.helpContent(ctx)); } + public PendingMessage usagesMessage(Context ctx) { + return PendingMessage.ofMarkdownRaw(Contents.usagesContent(ctx)); + } + public PendingMessage faqMessage(Context ctx) { return PendingMessage.ofMarkdownRaw(Contents.faqContent(ctx)); } @@ -467,6 +473,15 @@ public PendingMessage userInfoShortMessage(Context ctx, UserExtended user) { return PendingMessage.ofMarkdownRaw(Contents.userInfoShortContent(ctx, user)); } + public static class ExternalUrls { + public static final String COMMANDS = "https://docs.seira.top/overview/commands.html"; + public static final String PERMISSION = "https://docs.seira.top/overview/use.html#extra-permission"; + public static final String CHANNEL = "https://docs.seira.top/overview/use.html#extra-permission"; + public static final String CHANGELOG = "https://docs.seira.top/overview/changelog.html"; + public static final String FAQ = "https://docs.seira.top/overview/faq.html"; + public static final String GITHUB = "https://github.com/BotSeira"; + } + private static final class Contents { static String replayTaskContent(Context ctx, OstellaApi.ReplayTaskInfo taskInfo) { StringBuilder sb = new StringBuilder(); @@ -758,32 +773,51 @@ public static String statContent(Context ctx, OstellaApi.ServerStatus status, bo } public static String helpContent(Context ctx) { - return at(ctx) + "\n" + - """ - 常用指令: - > /bind - 绑定你的玩家ID - > /rp - 获取最近通过的一个成绩 - > /bo [个数] [玩家ID] - 获取一个或多个最佳成绩 - > /rp [个数] [玩家ID] - 获取最近通过一个或多个成绩 - > /tb [#天数] [玩家ID] - 获取近N天达成的BP - > /s <成绩ID或快捷查询> - 获取指定成绩 - > /m <谱面ID或快捷查询> - 获取谱面 - > /bma <谱面ID或快捷查询> [Mod] - 分析谱面PP构成和类型 - > /ms <谱面集ID或快捷查询> - 获取谱面集 - > /r [成绩ID或快捷查询] [[mm:ss]-[mm:ss]] - 生成成绩高光视频或指定片段 - > /rcancel <任务ID> - 取消回放渲染任务 - > /rg - 猜 Rank 游戏与个人战绩 - > /lb <谱面ID> [玩家ID列表] - 获取指定谱面排行榜 - > /watch add <玩家ID/用户名/@用户> [分钟] - 监视群友的新成绩 - > /wx start <谱面ID列表> - 监视指定玩家在指定谱面的成绩 - > /mpwatch [start] <房间ID> [stable|lazer] - 监视多人房间的逐图结果(链接可自动识别版本,stop all 停止本群全部监视) - > /f - 获取好友列表 - - 详细指令列表请在 [这里](https://docs.seira.top/overview/commands.html) 查看 - 配置额外权限请在 [这里](https://docs.seira.top/overview/use.html#extra-permission) 查看 - """ + "\n" - + "当前版本: " + VersionInfo.getVersion() + " [更新日志](https://docs.seira.top/overview/changelog.html)" + "\n" - + "[常见问题](https://docs.seira.top/overview/faq.html)" + " " + cmd("/stat", "状态信息").trim(); + return at(ctx) + "常用指令: \n" + """ + > /rp - 获取最近通过的一个或多个成绩 + > /bp - 获取一个或多个最佳成绩 + > /tb - 获取近日BP + > /s - 获取指定成绩 + > /m - 获取谱面 + > /r - 生成成绩高光视频或指定片段 + > /rg - 猜 Rank 游戏 + > /watch - 监视群友的新成绩 + > /mpwatch - 监视多人房间的逐图结果 + > /f - 获取好友列表 + + [详细指令列表](%s) | [配置额外权限](%s) + [加入官方频道](%s) | [查看常见问题](%s) + [查看更新日志](%s) | %s + %s | [Github主页](%s) + + 当前版本: %s + """.formatted( + COMMANDS, PERMISSION, + CHANNEL, FAQ, + CHANGELOG, cmd("/stat", "查看状态信息"), + cmd("/usages", "查看用法示例"), GITHUB, + VersionInfo.getVersion() + ) + "\n"; + } + + public static String usagesContent(Context ctx) { + return at(ctx) + "部分指令示例\n" + + "> 注意:所有指令中的@均需要开启权限才能正常读取。权限配置见 [这里]( " + PERMISSION + " )~\n" + """ + > /rp -> 查看最近通过的一个成绩 + > /rp1-20 -> 查看最近通过的1到20个成绩 + > /bp1-20 -> 查看20个最佳成绩 + > /bp1-20 @peppy acc>95 -> 查看指定玩家BP1-20中准确率大于95%的成绩 + > /sa bp2 -> 查看BP2的成绩分析 + > /tb #7 @peppy -> 查看指定玩家近7天的新BP + > /@peppy -> 查看指定玩家的基本信息 + > /rg group -> 开始群组猜 Rank 游戏 + > /m @peppy rp2 -> 查看指定玩家最近第2条成绩的谱面 + > /dl mp -> 获取所在lazer多人房间当前谱面的镜像下载链接 + > /r rp -> 渲染最近通过的成绩的高光片段回放视频 + > /r @peppy bp2 90- -> 渲染指定玩家BP2从1:30开始的回放视频 + > /mpw -> 开始多人房间监视 + > /romai @peppy -> 开始监视指定玩家所在的RomAI对局 + """; } public static String faqContent(Context ctx) { diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index e5a4537a..bfff7d9d 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -162,6 +162,7 @@ private static CommandRegistry createCommandRegistry( .register(replayCommands::handleRcancel, "rcancel") .register(generalCommands::handleInspect, "inspect") .register(generalCommands::handleHelp, "help") + .register(generalCommands::handleUsages, "usages") .register(generalCommands::handleFaq, "faq") .register(watchCommands::handleWatch, "watch") .register(specificScoreWatchCommands::handleWx, "wx") From ca6a6e469a7d8d65a6634344174667b5195d17c8 Mon Sep 17 00:00:00 2001 From: Zayrex Date: Tue, 29 Sep 2026 01:46:24 +0800 Subject: [PATCH 92/96] Update CHANNEL URL in ExternalUrls class --- src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index fe77eafe..faa0530e 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -476,7 +476,7 @@ public PendingMessage userInfoShortMessage(Context ctx, UserExtended user) { public static class ExternalUrls { public static final String COMMANDS = "https://docs.seira.top/overview/commands.html"; public static final String PERMISSION = "https://docs.seira.top/overview/use.html#extra-permission"; - public static final String CHANNEL = "https://docs.seira.top/overview/use.html#extra-permission"; + public static final String CHANNEL = "https://pd.qq.com/s/f9icas5gj?b=5"; public static final String CHANGELOG = "https://docs.seira.top/overview/changelog.html"; public static final String FAQ = "https://docs.seira.top/overview/faq.html"; public static final String GITHUB = "https://github.com/BotSeira"; From 9ea63781f181343a84a0cc29bc87078371e3aedb Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 29 Sep 2026 13:33:44 +0800 Subject: [PATCH 93/96] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8=E5=9C=B0=E5=9D=80=E9=AA=8C=E8=AF=81=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E5=96=84=E4=BB=A3=E7=A0=81=E5=8F=AF=E8=AF=BB?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/xyz/zcraft/seira/bot/WSClient.java | 17 +++++++++++++++-- .../command/handler/GeneralCommandHandler.java | 12 ++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/bot/WSClient.java b/src/main/java/xyz/zcraft/seira/bot/WSClient.java index 77b57b38..380ca9ec 100644 --- a/src/main/java/xyz/zcraft/seira/bot/WSClient.java +++ b/src/main/java/xyz/zcraft/seira/bot/WSClient.java @@ -152,7 +152,7 @@ private void onC2CMsg(JsonObject payload) { final JsonArray attachments = data.getAsJsonArray("attachments"); List attachmentList = new ArrayList<>(); - if (attachments != null && !attachments.isJsonNull()) { + if (attachments != null && !attachments.isJsonNull()) { for (JsonElement attachmentElem : attachments) { JsonObject attachmentObj = attachmentElem.getAsJsonObject(); Attachment attachment = gson.fromJson(attachmentObj, Attachment.class); @@ -295,7 +295,20 @@ private String stripSelfMention(String content) { private void sendIdentify() { JsonObject data = new JsonObject(); data.addProperty("token", "QQBot " + tokenSupplier.get().token()); - data.addProperty("intents", 1 << 25 | 1 << 26); + /* + GUILDS (1 << 0) + GUILD_MEMBERS (1 << 1) + GUILD_MESSAGES (1 << 9) // 消息事件,仅 *私域* 机器人能够设置此 intents。 + GUILD_MESSAGE_REACTIONS (1 << 10) + DIRECT_MESSAGE (1 << 12) + GROUP_AND_C2C_EVENT (1 << 25) + INTERACTION (1 << 26) + MESSAGE_AUDIT (1 << 27) + FORUMS_EVENT (1 << 28) // 论坛事件,仅 *私域* 机器人能够设置此 intents。 + AUDIO_ACTION (1 << 29) + PUBLIC_GUILD_MESSAGES (1 << 30) // 消息事件,此为公域的消息事件 + */ + data.addProperty("intents", 1 << 25 | 1 << 26 | 1 << 28 | 1 << 30); JsonObject payload = new JsonObject(); payload.addProperty("op", 2); diff --git a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java index 9684be8f..329ee9a4 100644 --- a/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java +++ b/src/main/java/xyz/zcraft/seira/command/handler/GeneralCommandHandler.java @@ -20,6 +20,7 @@ import java.util.Objects; import java.util.function.Predicate; +import java.util.regex.Pattern; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; @@ -142,13 +143,15 @@ public void handleNotice(Context context) { .filter(Notice::isActive) .findFirst() .ifPresentOrElse(notice -> { - sb.append(at(context)).append("公告#").append(notice.id()).append(" ").append(notice.title()).append("\n"); + sb.append(at(context)).append("公告 `#").append(notice.id()).append("` - `").append(notice.title()).append("`\n"); sb.append(NoticeStore.getContentFor(notice)); }, () -> sb.append(at(context)).append("未找到公告#").append(noticeId)); context.sendReply(PendingMessage.ofMarkdownRaw(sb.toString())); } + private static final Pattern SERVER_PATTERN = Pattern.compile("^(?:https?://)?([a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)+)(?::[0-9]+)?$"); + public void handleMc(Context ctx) { if (ctx.argumentCount() != 1) { ctx.sendReply(at(ctx) + "用法:/mc <服务器地址>"); @@ -158,6 +161,11 @@ public void handleMc(Context ctx) { try { final String address = ctx.argument(0); + if (!SERVER_PATTERN.matcher(address).matches()) { + ctx.sendReply(at(ctx) + "服务器地址无效喵。"); + return; + } + final var probe = AsteroidApi.getMinecraftServerStatus(address); final var status = probe.status(); @@ -188,7 +196,7 @@ public void handleMc(Context ctx) { ) ); } catch (Exception e) { - ctx.sendReply(at(ctx) + "状态获取失败了喵,请稍后再试。"); + ctx.sendReply(at(ctx) + "无法获取目标服务器状态喵,这可能是因为目标服务器未开启或者存在网络问题。"); } } From ebc91d0bb51902f545b0b91d43adb50c36c79059 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 29 Sep 2026 14:17:15 +0800 Subject: [PATCH 94/96] =?UTF-8?q?=E9=87=8D=E6=9E=84=20AiChatHandler=20?= =?UTF-8?q?=E5=92=8C=20AiPermission=EF=BC=8C=E4=BC=98=E5=8C=96=E6=9D=83?= =?UTF-8?q?=E9=99=90=E7=AE=A1=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E4=BB=A3=E7=A0=81=E5=8F=AF=E8=AF=BB=E6=80=A7=E5=92=8C?= =?UTF-8?q?=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + pom.xml | 2 +- .../xyz/zcraft/seira/ai/AiChatHandler.java | 124 ++++++++++++------ .../seira/ai/provider/ChatProvider.java | 2 + .../seira/ai/provider/HiAgentProvider.java | 16 +++ .../zcraft/seira/command/route/Router.java | 5 +- .../console/ConsoleCommandProcessor.java | 53 ++++++-- .../zcraft/seira/services/AiPermission.java | 78 +++++++++-- 8 files changed, 224 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 89649a18..9fac1310 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ Seira正在活跃开发中,在使用的过程中可能会有一些Bug,也会 | `/mpwatch` | `/mpwatch start/stop/status [目标]` | 按群成员添加、停止或查看多人房间监视;`stop all` 可停止本群全部监视 | | `/romai` | `/romai [目标]` | 开始监视自己或目标正在进行的RomAI比赛 | | `/mc` | `/mc <服务器地址>` | 获取指定MC服务器状态 | +| `/ai` | `/ai [on/off/reset/reset all]` | 开启/关闭/重置本群的AI对话 | | `/wx` | `/wx start <谱面ID列表>` / `/wx stop` | 监视指定玩家在指定谱面取得的成绩,重启后自动恢复 | | `/dcs` | `/dcs start .` / `/dcs stop` | 开启或解除当前 QQ 群与 Discord 频道的双向消息同步 | | `/stat` | `/stat` | 服务状态和统计信息文本 | diff --git a/pom.xml b/pom.xml index 1b46429f..68f6da38 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.17 + 1.12.18 25 diff --git a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java index 34d0aa74..0097e4b8 100644 --- a/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java +++ b/src/main/java/xyz/zcraft/seira/ai/AiChatHandler.java @@ -18,6 +18,7 @@ import java.util.function.Function; import java.util.function.Predicate; +import static xyz.zcraft.seira.command.reply.ReplyFactory.ExternalUrls.CHANNEL; import static xyz.zcraft.seira.command.reply.ReplyFactory.ExternalUrls.PERMISSION; import static xyz.zcraft.seira.command.reply.ReplyFactory.at; import static xyz.zcraft.seira.command.reply.ReplyFactory.cmd; @@ -42,27 +43,33 @@ public AiChatHandler( public void handleAi(Context ctx) { if (!ctx.inGroup()) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "AI对话仅在群组中可用喵。")); + ctx.sendReply(at(ctx) + "AI对话仅在群组中可用喵。"); return; } if (ctx.argumentCount() == 0) { - final boolean b = AiPermission.doPermit(ctx.groupId()); - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "目前AI对话在本群启用状态为:" + (b ? "√" : "×"))); + final boolean b = AiPermission.permits(ctx.groupId()); + final boolean c = AiPermission.isActivated(ctx.groupId()); + ctx.sendReply(at(ctx) + "目前AI对话在本群状态\n" + + "> 已授权:" + (b ? "√" : "×") + "\n" + + "> 已启用:" + (c ? "√" : "×") + ); return; } else if (ctx.argumentCount() == 1 - && List.of("on", "off", "reset", "resetgroup", "resetme", "stop").contains(ctx.argument(0).toLowerCase(Locale.ROOT))) { + && List.of("on", "off", "grant", "revoke", "reset", "stop").contains(ctx.argument(0).toLowerCase(Locale.ROOT))) { if ("on".equalsIgnoreCase(ctx.argument(0))) { - if (!adminAuthorizer.test(ctx.senderUserId())) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "你无权使用该命令喵。\n" + - "> 由于此功能开销较大、处于测试阶段且较为不可控,暂未开放。若想要在此群中使用此功能,请联系 Bot 管理员喵。")); - return; - } - final GroupBotState apply = botStateGetter.apply(ctx.groupId()); if (apply.allowProactiveMsg() && apply.receiveMsgSetting() == GroupBotState.ReceiveMsgSetting.ALL) { - AiPermission.permit(ctx.groupId()); - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已启用本群AI对话喵。")); + if (AiPermission.permits(ctx.groupId())) { + AiPermission.activate(ctx.groupId()); + ctx.sendReply(at(ctx) + "已启用本群AI对话喵。"); + } else { + if (adminAuthorizer.test(ctx.senderUserId())) { + ctx.sendReply(at(ctx) + "已授权并启用本群AI对话喵。"); + } else { + ctx.sendReply(at(ctx) + "本群无此功能权限,请 [联系 Bot 管理员](%s) 喵。".formatted(CHANNEL)); + } + } } else { ctx.sendReply(PendingMessage.ofMarkdownRaw( at(ctx) + "由于本群未配置权限或配置不完整,暂无法启用本群AI对话喵。" + @@ -71,29 +78,36 @@ public void handleAi(Context ctx) { } return; } else if ("off".equalsIgnoreCase(ctx.argument(0))) { - if (!adminAuthorizer.test(ctx.senderUserId())) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "你无权使用该命令喵。")); - return; + if (AiPermission.isActivated(ctx.groupId())) { + AiPermission.deactivate(ctx.groupId()); + ctx.sendReply(at(ctx) + "已禁用本群AI对话喵。"); + } else { + ctx.sendReply(at(ctx) + "本群AI对话还未启用喵。"); } - - AiPermission.revoke(ctx.groupId()); - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已禁用本群AI对话喵。")); return; - } else if ("reset".equalsIgnoreCase(ctx.argument(0))) { - chatProvider.clearState(ctx.groupId(), ctx.senderUserId()); - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已重置你在本群的AI对话状态喵。")); - return; - } else if ("resetme".equalsIgnoreCase(ctx.argument(0))) { - final int i = chatProvider.clearStateOfUser(ctx.senderUserId()); - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已重置你在" + i + "个群中的AI对话状态喵。")); + } else if ("grant".equalsIgnoreCase(ctx.argument(0))) { + if (adminAuthorizer.test(ctx.senderUserId())) { + AiPermission.grant(ctx.groupId()); + ctx.sendReply(at(ctx) + "已授予本群AI对话权限喵。"); + } else { + ctx.sendReply(at(ctx) + "你无权使用该命令喵。"); + } return; - } else if ("resetgroup".equalsIgnoreCase(ctx.argument(0))) { - if (!adminAuthorizer.test(ctx.senderUserId())) { - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "你无权使用该命令喵。")); - return; + } else if ("revoke".equalsIgnoreCase(ctx.argument(0))) { + if (adminAuthorizer.test(ctx.senderUserId())) { + AiPermission.revoke(ctx.groupId()); + if (AiPermission.isActivated(ctx.groupId())) { + ctx.sendReply(at(ctx) + "已停用并取消本群AI对话权限喵。"); + } else { + ctx.sendReply(at(ctx) + "已撤销本群AI对话权限喵。"); + } + } else { + ctx.sendReply(at(ctx) + "你无权使用该命令喵。"); } - final int i = chatProvider.clearStateOfGroup(ctx.groupId()); - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "已重置本群" + i + "个用户的AI对话状态喵。")); + return; + } else if ("reset".equalsIgnoreCase(ctx.argument(0))) { + chatProvider.clearState(ctx.groupId(), ctx.senderUserId()); + ctx.sendReply(at(ctx) + "已重置你在本群的AI对话状态喵。"); return; } else if ("stop".equalsIgnoreCase(ctx.argument(0))) { final ChatProvider.StopStatus stopStatus = chatProvider.requireStop(ctx.groupId(), ctx.senderUserId()); @@ -104,19 +118,55 @@ public void handleAi(Context ctx) { case NOT_SUPPORTED -> "当前不支持停止AI对话。"; }); + return; + } + } else if (ctx.argumentCount() == 2 && ctx.argument(0).equalsIgnoreCase("reset")) { + if ("group".equalsIgnoreCase(ctx.argument(1))) { + if (!adminAuthorizer.test(ctx.senderUserId())) { + ctx.sendReply(at(ctx) + "你无权使用该命令喵。"); + return; + } + final int i = chatProvider.clearStateOfGroup(ctx.groupId()); + ctx.sendReply(at(ctx) + "已重置本群" + i + "个用户的AI对话状态喵。"); + return; + } else if ("all".equalsIgnoreCase(ctx.argument(1))) { + final int i = chatProvider.clearStateOfUser(ctx.senderUserId()); + ctx.sendReply(at(ctx) + "已重置你在" + i + "个群中的AI对话状态喵。"); + return; + } + } else if (ctx.argumentCount() == 2 && ctx.argument(0).equalsIgnoreCase("parallel")) { + if (!adminAuthorizer.test(ctx.senderUserId())) { + ctx.sendReply(at(ctx) + "你无权使用该命令喵。"); + return; + } + + final Integer n = resolver.parsePositiveInt(ctx.argument(1)); + if (n != null) { + AiPermission.setParallel(ctx.groupId(), n); + ctx.sendReply(at(ctx) + "已设置本群的最大并行AI对话数量为" + n + "个喵。"); return; } } - ctx.sendReply(PendingMessage.ofMarkdownRaw(at(ctx) + "用法:/ai [on|off]")); + ctx.sendReply(at(ctx) + "用法:/ai [on|off|reset|stop]"); } public void handleChat(Context ctx, String message, List elems) { + if (!AiPermission.isActivated(ctx.groupId())) { + return; + } + final String at = at(ctx); if (chatProvider.isRunning(ctx.groupId(), ctx.senderUserId())) { - ctx.sendReply(PendingMessage.ofMarkdownRaw( - at + - "已有一轮对话正在进行中了喵,请稍作等待或" + cmd("/ai stop", "取消对话") + "~") + ctx.sendReply( + at + "你已有一轮对话正在进行中了喵,请稍作等待或" + cmd("/ai stop", "取消对话") + "~" + ); + return; + } + + if (chatProvider.runningCount(ctx.groupId()) >= AiPermission.getParallel(ctx.groupId())) { + ctx.sendReply( + at + "当前群聊中的同时运行对话数量已达到上限,无法开始新的对话喵,请稍作等待。" ); return; } @@ -159,8 +209,8 @@ public void onError(String errorCode, String errorMsg) { true, PendingMessage.ofMarkdownRaw( at + "回复生成失败了喵。\n" + - "> " + errorCode + ": " + errorMsg + "\n" + - "> 若重复出现错误,请尝试" + cmd("/ai reset", "重置会话") + "> " + errorCode + ": " + errorMsg + "\n" + + "> 若重复出现错误,请尝试" + cmd("/ai reset", "重置会话") ), true ); diff --git a/src/main/java/xyz/zcraft/seira/ai/provider/ChatProvider.java b/src/main/java/xyz/zcraft/seira/ai/provider/ChatProvider.java index 0282c534..370c57bf 100644 --- a/src/main/java/xyz/zcraft/seira/ai/provider/ChatProvider.java +++ b/src/main/java/xyz/zcraft/seira/ai/provider/ChatProvider.java @@ -22,6 +22,8 @@ String input( boolean isRunning(String groupId, String openId); + int runningCount(String groupId); + boolean clearState(String groupId, String openId); int clearStateOfGroup(String groupId); diff --git a/src/main/java/xyz/zcraft/seira/ai/provider/HiAgentProvider.java b/src/main/java/xyz/zcraft/seira/ai/provider/HiAgentProvider.java index 7831999c..6c178a56 100644 --- a/src/main/java/xyz/zcraft/seira/ai/provider/HiAgentProvider.java +++ b/src/main/java/xyz/zcraft/seira/ai/provider/HiAgentProvider.java @@ -14,6 +14,7 @@ import xyz.zcraft.seira.ai.data.ChatQueryResponse; import xyz.zcraft.seira.command.Context; import xyz.zcraft.seira.config.LLMConfig; +import xyz.zcraft.seira.services.AiPermission; import java.io.BufferedReader; import java.io.InputStreamReader; @@ -278,6 +279,21 @@ public boolean isRunning(String groupId, String openId) { return state != null && state.running.get(); } + @Override + public int runningCount(String groupId) { + int count = 0; + + for (Map.Entry entry : states.entrySet()) { + if (!entry.getKey().groupId().equals(groupId)) { + continue; + } + + count++; + } + + return count; + } + @Override public boolean clearState(String groupId, String openId) { final State state = states.get( diff --git a/src/main/java/xyz/zcraft/seira/command/route/Router.java b/src/main/java/xyz/zcraft/seira/command/route/Router.java index bfff7d9d..f4082c71 100644 --- a/src/main/java/xyz/zcraft/seira/command/route/Router.java +++ b/src/main/java/xyz/zcraft/seira/command/route/Router.java @@ -246,14 +246,15 @@ private void handleMessageReceived( return; } - final boolean permitAi = AiPermission.doPermit(groupId); + final boolean permitAi = AiPermission.permits(groupId); + final boolean activatedAi = AiPermission.isActivated(groupId); if (parseResult.status() == CommandParser.ParseResult.Status.TEXT) { ReplyChannel replies = taskCoordinator.openReplyChannel( targetId, messageId, groupMessage, false, msgIdx ); - if (beingAt && permitAi) { + if (beingAt && permitAi && activatedAi) { aiChatHandler.handleChat(parseResult.context().withReplies(replies), msgToRecord, msgElems); } else if (permitAi) { aiChatHandler.recordHistory(groupId, userId, msgToRecord, attachments); diff --git a/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java b/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java index 0bf52249..5f8b51ef 100644 --- a/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java +++ b/src/main/java/xyz/zcraft/seira/console/ConsoleCommandProcessor.java @@ -37,6 +37,7 @@ public final class ConsoleCommandProcessor { ); private static final Map> SUBCOMMANDS; + private static final Pattern ID_PATTERN = Pattern.compile("^[A-Z0-9]{32}$"); static { SUBCOMMANDS = new HashMap<>(); @@ -59,7 +60,6 @@ public final class ConsoleCommandProcessor { private final ConsoleDataAccess dataAccess; private final MessageSender messenger; private final ConsoleRuntimeControl runtimeControl; - private final ConfigHandler configHandler; private final NoticeHandler noticeHandler; @@ -155,6 +155,17 @@ private static long positiveLong(String value, String name) { } } catch (NumberFormatException ignored) { } + throw new IllegalArgumentException(name + " must be a positive long."); + } + + private static int positiveInt(String value, String name) { + try { + int parsed = Integer.parseInt(value); + if (parsed > 0) { + return parsed; + } + } catch (NumberFormatException ignored) { + } throw new IllegalArgumentException(name + " must be a positive integer."); } @@ -267,7 +278,7 @@ private ConsoleResult ai(ConsoleInputParser.ParsedInput input) { AiPermission.loadFromFile(); return ConsoleResult.success("AI permission reloaded."); } else if (ID_PATTERN.matcher(value).matches()) { - return ConsoleResult.success("AI chat for group " + value + " is: " + AiPermission.doPermit(value)); + return ConsoleResult.success("AI chat for group " + value + " is: " + AiPermission.permits(value)); } else { return ConsoleResult.failure("Group id " + value + " not a valid id."); } @@ -280,11 +291,35 @@ private ConsoleResult ai(ConsoleInputParser.ParsedInput input) { } if ("on".equalsIgnoreCase(option)) { - AiPermission.permit(target); - return ConsoleResult.success("AI chat for group " + target + " is on."); + AiPermission.permits(target); + AiPermission.activate(target); + return ConsoleResult.success("AI chat for group " + target + " is activated."); } else if ("off".equalsIgnoreCase(option)) { + AiPermission.deactivate(target); + return ConsoleResult.success("AI chat for group " + target + " is deactivated."); + } else if ("grant".equalsIgnoreCase(option)) { + AiPermission.grant(target); + return ConsoleResult.success("AI chat for group " + target + " is granted."); + } else if ("revoke".equalsIgnoreCase(option)) { AiPermission.revoke(target); - return ConsoleResult.success("AI chat for group " + target + " is off."); + return ConsoleResult.success("AI chat for group " + target + " is revoked."); + } else if ("parallel".equalsIgnoreCase(option)) { + final int parallel = AiPermission.getParallel(target); + return ConsoleResult.success("Parallel count of AI chat for group " + target + " is " + parallel + "."); + } + } else if (input.size() == 3) { + final String target = input.value(0).toLowerCase(Locale.ROOT); + final String option = input.value(1).toLowerCase(Locale.ROOT); + final String value = input.value(2).toLowerCase(Locale.ROOT); + + if (!ID_PATTERN.matcher(target).matches()) { + return ConsoleResult.failure("Group id " + target + " not a valid id."); + } + + if (option.equalsIgnoreCase("parallel")) { + final int l = positiveInt(value, "parallel count"); + AiPermission.setParallel(target, l); + return ConsoleResult.success("Parallel count of AI chat for group " + target + " is now set to " + l + "."); } } @@ -493,8 +528,10 @@ private ConsoleResult group(ConsoleInputParser.ParsedInput input) { } return switch (input.value(1).toLowerCase(Locale.ROOT)) { - case "info" -> input.size() == 3 ? runtimeControl.getGroupInfo(input.value(2)) : ConsoleResult.failure("Usage: group info "); - case "state" -> input.size() == 3 ? runtimeControl.getGroupBotState(input.value(2)) : ConsoleResult.failure("Usage: group state "); + case "info" -> + input.size() == 3 ? runtimeControl.getGroupInfo(input.value(2)) : ConsoleResult.failure("Usage: group info "); + case "state" -> + input.size() == 3 ? runtimeControl.getGroupBotState(input.value(2)) : ConsoleResult.failure("Usage: group state "); default -> ConsoleResult.failure("Usage: group [args]"); }; } @@ -593,8 +630,6 @@ private ConsoleResult query(String sql) { return ConsoleResult.success(formatQueryResult(dataAccess.query(sql, QUERY_ROW_LIMIT))); } - private static final Pattern ID_PATTERN = Pattern.compile("^[A-Z0-9]{32}$"); - private ConsoleResult send(ConsoleInputParser.ParsedInput input) { if (input.size() < 4) { return ConsoleResult.failure("Usage: send "); diff --git a/src/main/java/xyz/zcraft/seira/services/AiPermission.java b/src/main/java/xyz/zcraft/seira/services/AiPermission.java index 658660f7..93e32608 100644 --- a/src/main/java/xyz/zcraft/seira/services/AiPermission.java +++ b/src/main/java/xyz/zcraft/seira/services/AiPermission.java @@ -8,8 +8,11 @@ import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; public class AiPermission { private static final Object LOCK = new Object(); @@ -18,6 +21,8 @@ public class AiPermission { private static Set groups = null; @Getter private static Mode mode = Mode.WHITELIST; + private static Set activated = null; + private static Map parallel = null; public static void initialize() { loadFromFile(); @@ -32,15 +37,27 @@ public static void loadFromFile() { if (!json.isBlank()) { JsonObject obj = JsonParser.parseString(json).getAsJsonObject(); - final PermissionSnapshot permissionSnapshot = GSON.fromJson(obj, PermissionSnapshot.class); + final PermissionSnapshot snapshot = GSON.fromJson(obj, PermissionSnapshot.class); - mode = permissionSnapshot.mode; - groups = new HashSet<>(); + AiPermission.mode = snapshot.mode; + AiPermission.groups = new HashSet<>(); + AiPermission.activated = new HashSet<>(); + AiPermission.parallel = new HashMap<>(); - final Set groups = permissionSnapshot.groups; + final Set groups = snapshot.groups; if (groups != null) { AiPermission.groups.addAll(groups); } + + final Set activated = snapshot.activated; + if (activated != null) { + AiPermission.activated.addAll(activated); + } + + final Map parallel = snapshot.parallel; + if (parallel != null) { + AiPermission.parallel.putAll(parallel); + } } } } catch (Exception e) { @@ -54,6 +71,14 @@ public static void loadFromFile() { if (groups == null) { groups = new HashSet<>(); } + + if (activated == null) { + activated = new HashSet<>(); + } + + if (parallel == null) { + parallel = new ConcurrentHashMap<>(); + } } } @@ -62,7 +87,7 @@ public static void saveToFile() { try { Files.createDirectories(STORE.getParent()); - PermissionSnapshot snapshot = new PermissionSnapshot(mode, groups); + PermissionSnapshot snapshot = new PermissionSnapshot(mode, groups, activated, parallel); Files.writeString(STORE, GSON.toJson(snapshot)); } catch (Exception e) { @@ -71,7 +96,7 @@ public static void saveToFile() { } } - public static boolean doPermit(String groupId) { + public static boolean permits(String groupId) { if (mode == null || groups == null) return false; if (mode == Mode.WHITELIST) { @@ -83,7 +108,7 @@ public static boolean doPermit(String groupId) { } } - public static void permit(String groupId) { + public static void grant(String groupId) { if (mode == null || groups == null) return; if (mode == Mode.WHITELIST) { @@ -104,6 +129,41 @@ public static void revoke(String groupId) { groups.add(groupId); } + if (activated != null) { + activated.remove(groupId); + } + + saveToFile(); + } + + public static int getParallel(String groupId) { + if (parallel == null) return 1; + return parallel.getOrDefault(groupId, 1); + } + + public static void activate(String groupId) { + if (activated == null) return; + activated.add(groupId); + + saveToFile(); + } + + public static void deactivate(String groupId) { + if (activated == null) return; + activated.remove(groupId); + + saveToFile(); + } + + public static boolean isActivated(String groupId) { + if (activated == null) return false; + return activated.contains(groupId); + } + + public static void setParallel(String groupId, int parallel) { + if (AiPermission.parallel == null) return; + AiPermission.parallel.put(groupId, parallel); + saveToFile(); } @@ -114,7 +174,9 @@ public enum Mode { public record PermissionSnapshot( Mode mode, - Set groups + Set groups, + Set activated, + Map parallel ) { } } From 8b2885df14344a1f4ff5df2bf9bea3d64239b115 Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 29 Sep 2026 14:22:53 +0800 Subject: [PATCH 95/96] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E8=87=B31.13.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 68f6da38..d3b44572 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.BotSeira SeiraCore - 1.12.18 + 1.13.0 25 diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index faa0530e..342f9bb8 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -782,7 +782,7 @@ public static String helpContent(Context ctx) { > /r - 生成成绩高光视频或指定片段 > /rg - 猜 Rank 游戏 > /watch - 监视群友的新成绩 - > /mpwatch - 监视多人房间的逐图结果 + > /mpw - 监视多人房间的逐图结果 > /f - 获取好友列表 [详细指令列表](%s) | [配置额外权限](%s) From 45b32ec9274543dd5c271e96d407daa5ceb6815d Mon Sep 17 00:00:00 2001 From: ZayrexDev Date: Tue, 29 Sep 2026 15:03:47 +0800 Subject: [PATCH 96/96] =?UTF-8?q?=E9=87=8D=E6=9E=84=20AsteroidApi=20?= =?UTF-8?q?=E5=92=8C=20ReplyFactory=EF=BC=8C=E4=BC=98=E5=8C=96=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8=E7=8A=B6=E6=80=81=E8=8E=B7=E5=8F=96=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=BC=BA=E4=BB=A3=E7=A0=81=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xyz/zcraft/seira/api/AsteroidApi.java | 21 +++++++++++++++---- .../seira/command/reply/ReplyFactory.java | 11 +++++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java b/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java index 56df3239..0293d97f 100644 --- a/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java +++ b/src/main/java/xyz/zcraft/seira/api/AsteroidApi.java @@ -1,6 +1,8 @@ package xyz.zcraft.seira.api; import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import xyz.zcraft.seira.Seira; @@ -56,23 +58,34 @@ public static MinecraftServerStatus getMinecraftServerStatus(String addr) { } } - public static boolean getServerStatus() { + public record ServerStatus(boolean online, String version){} + + public static ServerStatus getServerStatus() { try { var request = requestBuilder("/health") .GET() .build(); final HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + final RawResponse rawResponse = GSON.fromJson(response.body(), RawResponse.class); - if (response.statusCode() == 200) { - return true; + if (response.statusCode() == 200 + && response.body() != null + && rawResponse != null + && rawResponse.isSuccess()) { + final JsonElement rawResponseData = rawResponse.getData(); + if (rawResponseData != null && rawResponseData.isJsonObject()) { + JsonObject data = rawResponseData.getAsJsonObject(); + return new ServerStatus(true, data.get("version").getAsString()); + } + return new ServerStatus(true, "?"); } } catch (Exception e) { LOG.error("Failed to get server status", e); } LOG.warn("Asteroid server is down."); - return false; + return new ServerStatus(false, null); } private static final Logger LOG = LogManager.getLogger(AsteroidApi.class); diff --git a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java index 342f9bb8..84b14bf1 100644 --- a/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java +++ b/src/main/java/xyz/zcraft/seira/command/reply/ReplyFactory.java @@ -5,6 +5,7 @@ import com.google.gson.JsonObject; import org.jetbrains.annotations.NotNull; import xyz.zcraft.osu.model.*; +import xyz.zcraft.seira.api.AsteroidApi; import xyz.zcraft.seira.api.OstellaApi; import xyz.zcraft.seira.api.data.*; import xyz.zcraft.seira.bot.data.Button; @@ -426,7 +427,7 @@ public PendingMessage scoreMissesMessage(Context ctx, Response> s ); } - public PendingMessage statusMessage(Context ctx, OstellaApi.ServerStatus status, boolean asteroid) { + public PendingMessage statusMessage(Context ctx, OstellaApi.ServerStatus status, AsteroidApi.ServerStatus asteroid) { return PendingMessage.ofMarkdownRaw( Contents.statContent(ctx, status, asteroid), null ); @@ -739,7 +740,7 @@ public static String scoreMissesContent(Context ctx, Response> sc return sb.toString().trim(); } - public static String statContent(Context ctx, OstellaApi.ServerStatus status, boolean asteroid) { + public static String statContent(Context ctx, OstellaApi.ServerStatus status, AsteroidApi.ServerStatus asteroid) { String stat = at(ctx) + "\n" + "## 服务器状态\n" + "> 消息网关: ✅ 正常\n" + @@ -751,7 +752,7 @@ public static String statContent(Context ctx, OstellaApi.ServerStatus status, bo stat += "> ↳ osu! API: " + (status.osu() ? "✅ 正常" : "❌ 无法访问") + "\n"; } - stat += "> Asteroid API: " + (asteroid ? "✅ 正常" : "❌ 无法访问") + "\n"; + stat += "> Asteroid API: " + (asteroid.online() ? "✅ 正常" : "❌ 无法访问") + "\n"; String version = "## 版本信息" + "\n" + "> SeiraCore: " + VersionInfo.getVersion() + "\n"; @@ -760,6 +761,10 @@ public static String statContent(Context ctx, OstellaApi.ServerStatus status, bo version += "> oStella: " + status.oStellaVersion() + "\n"; } + if (asteroid.online() && asteroid.version() != null) { + version += "> Asteroid: " + asteroid.version() + "\n"; + } + String res = "## 统计信息\n" + "> Seira已经" + "\n" +