Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ AI 总结由 Java 端 MineSentinel 只读采集 observation,AstrBot 插件侧
1. 部署模式:单服 / Velocity 群组服。
2. Minecraft 服务器根目录。单服给一个根目录;Velocity 群组服请给 Velocity 根目录和每个后端服根目录。
3. AstrBot 根目录。
4. 要接收 AI 总结的 QQ 群或 UMO,例如 group:123456789 或 aiocqhttp:GroupMessage:123456789。
4. 要接收 AI 总结的 QQ 群或 UMO。优先提供 AstrBot `/sid` 输出,例如 napcat:GroupMessage:123456789;也可以提供 group:123456789 或 qq:123456789 简写
5. NapCat 安装目录如果已知可一并提供;不知道就留空,由你先自动检测。
6. 是否现在重启 MC 服务端、AstrBot 和 NapCat。

Expand All @@ -120,25 +120,26 @@ AI 总结由 Java 端 MineSentinel 只读采集 observation,AstrBot 插件侧
- 不要让 AI 输入 QQ 密码;启动 NapCat 后让用户扫码/手动登录。
- 配置 OneBot v11 WebSocket 或 HTTP,使 AstrBot 能连接 NapCat;若已有可用连接,保持原配置。
4. 从 GitHub Actions 下载两个仓库最新 successful workflow 的构建产物和源码。优先使用 gh:
- gh run list -R EllanServer/AstrBotAdapter --branch main --status success --limit 1
- gh run list -R EllanServer/AstrBotAdapter --branch codex/fix-paper-carbon-chat-forwarding --status success --limit 1
- gh run download -R EllanServer/AstrBotAdapter <run-id> --dir ./downloads/AstrBotAdapter
- gh run list -R EllanServer/astrbot_plugin_minecraft_adapter --branch master --status success --limit 1
- gh run list -R EllanServer/astrbot_plugin_minecraft_adapter --branch pyo3-mine-sentinel --status success --limit 1
- gh run download -R EllanServer/astrbot_plugin_minecraft_adapter <run-id> --dir ./downloads/astrbot_plugin_minecraft_adapter
如果没有 gh,就打开上面的 Actions 链接下载最新成功运行的 artifacts。若 Actions 没有源码 artifact,则同时下载源码 ZIP:
- https://github.com/EllanServer/AstrBotAdapter/archive/refs/heads/main.zip
- https://github.com/EllanServer/astrbot_plugin_minecraft_adapter/archive/refs/heads/master.zip
- https://github.com/EllanServer/AstrBotAdapter/archive/refs/heads/codex/fix-paper-carbon-chat-forwarding.zip
- https://github.com/EllanServer/astrbot_plugin_minecraft_adapter/archive/refs/heads/pyo3-mine-sentinel.zip
5. 安装 Java 插件:
- 单服:把最新 Backend jar 放进该服务器根目录的 plugins/。
- Velocity 群组服:把 Velocity jar 放进 Velocity 的 plugins/,把 Backend jar 放进每个后端服的 plugins/;如果产物包含 libs/,按 README 保持 libs/ 与 Velocity jar 同级。
6. 安装 AstrBot 插件:把 astrbot_plugin_minecraft_adapter 最新源码放进 AstrBot 的插件目录,目录名保持 astrbot_plugin_minecraft_adapter;如已有旧目录,先备份再覆盖。
7. 配置 Java 插件:
- 单服保持 proxyMode.enabled=false。
- Velocity 群组服先启动/读取 Velocity 端 secret,再给每个后端服写入 proxyMode.enabled=true 和 proxyMode.secret。
- 如果需要把所有 Minecraft 普通聊天转发到 AstrBot/QQ,保持 messageForward.prefix="";留空表示不再要求玩家输入 * 前缀。
- 确认 mine_sentinel.enabled、observation.enabled、chat.enabled、metrics.enabled 为 true。
8. 配置 AstrBot 插件:
- 单服在 mc_servers 中添加后端服 host/port/token。
- 群组服只添加 Velocity 端 host/port/token。
- 把 QQ 群写入 message.target_sessions,必要时写入 mine_sentinel.report.delivery_targets。
- 把 QQ 群写入 message.target_sessions,必要时写入 mine_sentinel.report.delivery_targets;优先写 /sid 输出的 UMO,简写 group:/qq: 会由 AstrBot 插件解析到当前平台
- 开启 mine_sentinel.enabled、storage.enabled、report.enabled、send_as_image、send_full_log_file。
- 确认 AstrBot 已连接 NapCat/OneBot,且目标 QQ 群可以收发消息。
9. 做备份:覆盖任何 jar、插件目录或配置文件前,先复制到带时间戳的 backup 目录。
Expand Down Expand Up @@ -171,7 +172,7 @@ AI 总结由 Java 端 MineSentinel 只读采集 observation,AstrBot 插件侧
| `server.restapi.enabled` | `true` | boolean | 是否启用 REST API |
| `server.restapi.rateLimit` | `100` | int(次/分钟) | REST 频率限制,`0` 为不限流 |
| `messageForward.enabled` | `true` | boolean | 是否启用聊天消息转发 |
| `messageForward.prefix` | `*` | string | 转发触发前缀(留空表示转发所有消息) |
| `messageForward.prefix` | `""` | string | 转发触发前缀(留空表示转发所有消息) |
| `messageForward.stripPrefix` | `true` | boolean | 转发时是否移除前缀 |
| `messageForward.incomingFormat` | `§7[§b{platform}§7] §f{username}§7: §f{content}` | string | 外来消息显示格式,支持 `{platform}` `{username}` `{content}` |
| `aiChat.group.enabled` | `true` | boolean | 是否启用群聊 AI |
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def referencedLibraryDependencies = nettyModules.collect { "io.netty:${it}:${net

def commonCompileOnlyDependencies = [
"net.kyori:adventure-api:${adventureVersion}",
"net.kyori:adventure-text-serializer-plain:${adventureVersion}",
"net.kyori:adventure-text-minimessage:${adventureVersion}",
"org.jetbrains:annotations:26.1.0",
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ public void onEnable() {

registerMineSentinelListener();
carbonChatBridge = BukkitBootstrapSupport.registerCarbonChatBridge(this, plugin, chatHandler);
BukkitBootstrapSupport.registerPaperAsyncChatListener(
this,
plugin,
chatHandler,
() -> carbonChatBridge != null && carbonChatBridge.isRegistered());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import io.github.railgun19457.astrbotadapter.core.util.JsonUtil;
import io.github.railgun19457.astrbotadapter.platform.velocity.VelocityAdapter;
import io.github.railgun19457.astrbotadapter.platform.velocity.listener.MineSentinelVelocityListener;
import io.github.railgun19457.astrbotadapter.platform.velocity.listener.VelocityCarbonChatBridge;
import io.github.railgun19457.astrbotadapter.platform.velocity.listener.VelocityPlayerListener;
import org.slf4j.Logger;

Expand All @@ -39,6 +40,7 @@ public class AstrbotAdapterVelocity extends AstrbotAdapterPlugin {
private final Logger velocityLogger;
private final Path dataDirectory;
private VelocityProxyBridge proxyBridge;
private VelocityCarbonChatBridge carbonChatBridge;

@Inject
public AstrbotAdapterVelocity(ProxyServer proxy, Logger logger, @DataDirectory Path dataDirectory) {
Expand All @@ -62,6 +64,10 @@ public void onProxyInitialize(ProxyInitializeEvent event) {

@Subscribe
public void onProxyShutdown(ProxyShutdownEvent event) {
if (carbonChatBridge != null) {
carbonChatBridge.close();
carbonChatBridge = null;
}
if (proxyBridge != null) {
proxyBridge.shutdown();
}
Expand Down Expand Up @@ -216,20 +222,35 @@ private static String getDisplayName(JsonObject data) {
* 注册Velocity事件监听器
*/
private void registerVelocityListeners() {
// Chat forwarding is handled via PMC from backend servers (handleProxyBridgeEvent),
// so no VelocityChatListener is needed here.
registerCarbonChatBridge();

// 注册玩家监听器
proxy.getEventManager().register(this,
new VelocityPlayerListener(this, proxy, notificationService));

if (mineSentinelService != null && mineSentinelService.isEnabled()) {
proxy.getEventManager().register(this, new MineSentinelVelocityListener(mineSentinelService));
proxy.getEventManager().register(this, new MineSentinelVelocityListener(
mineSentinelService,
() -> carbonChatBridge != null && carbonChatBridge.isRegistered()));
}

logger.info("Velocity事件监听器已注册");
}

private void registerCarbonChatBridge() {
carbonChatBridge = new VelocityCarbonChatBridge(
this,
proxy,
chatService,
messageForwardService,
mineSentinelService,
configManager.getConfig(),
logger);
if (!carbonChatBridge.register()) {
carbonChatBridge.startRetrying(platformAdapter.getScheduler());
}
}

/**
* Get the proxy bridge (only available when proxy bridge is enabled).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ private void parseConfig(Map<String, Object> yaml) {
Map<String, Object> forward = getMap(yaml, "messageForward");
if (forward != null) {
config.setForwardEnabled(getBoolean(forward, "enabled", true));
config.setForwardPrefix(getString(forward, "prefix", "*"));
config.setForwardPrefix(getString(forward, "prefix", ""));
config.setStripPrefix(getBoolean(forward, "stripPrefix", true));
config.setIncomingFormat(getString(forward, "incomingFormat",
"§7[§b{platform}§7] §f{username}§7: §f{content}"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class PluginConfig {

// 消息转发配置
private boolean forwardEnabled = true;
private String forwardPrefix = "*";
private String forwardPrefix = "";
private boolean stripPrefix = true;
private String incomingFormat = "§7[§b{platform}§7] §f{username}§7: §f{content}";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
import io.github.railgun19457.astrbotadapter.platform.bukkit.listener.BukkitProxyChatListener;
import io.github.railgun19457.astrbotadapter.platform.bukkit.listener.BukkitProxyPlayerListener;
import io.github.railgun19457.astrbotadapter.platform.bukkit.listener.CarbonChatBridge;
import io.github.railgun19457.astrbotadapter.platform.bukkit.listener.PaperAsyncChatListener;
import io.github.railgun19457.astrbotadapter.platform.bukkit.listener.PlayerChatHandler;
import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.java.JavaPlugin;

import java.util.function.BooleanSupplier;

/**
* Shared bootstrap helpers for Bukkit-family platforms.
*/
Expand Down Expand Up @@ -78,7 +81,42 @@ public static CarbonChatBridge registerCarbonChatBridge(
chatHandler,
plugin.getMineSentinelService(),
plugin.getPlatformAdapter().getPlatformType().supportsRegionScheduling());
return bridge.register() ? bridge : null;
if (!bridge.register()) {
bridge.startRetrying(plugin.getPlatformAdapter().getScheduler());
}
return bridge;
}

public static void registerPaperAsyncChatListener(
JavaPlugin javaPlugin,
AstrbotAdapterPlugin plugin,
PlayerChatHandler chatHandler,
BooleanSupplier carbonBridgeActive
) {
if (chatHandler == null || plugin == null || plugin.getPlatformAdapter() == null) {
return;
}
if (!hasPaperAsyncChatEvent()) {
return;
}

javaPlugin.getServer().getPluginManager().registerEvents(
new PaperAsyncChatListener(
chatHandler,
plugin.getMineSentinelService(),
carbonBridgeActive,
plugin.getPlatformAdapter().getPlatformType().supportsRegionScheduling()),
javaPlugin);
javaPlugin.getLogger().info("Paper AsyncChatEvent fallback listener enabled");
}

private static boolean hasPaperAsyncChatEvent() {
try {
Class.forName("io.papermc.paper.event.player.AsyncChatEvent");
return true;
} catch (ClassNotFoundException ex) {
return false;
}
}

public static final class ProxyModeContext {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.google.gson.JsonObject;
import io.github.railgun19457.astrbotadapter.minesentinel.service.MineSentinelService;
import io.github.railgun19457.astrbotadapter.platform.common.CommonScheduler;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
Expand All @@ -23,6 +24,9 @@ public final class CarbonChatBridge implements AutoCloseable {

private static final String[] CARBON_PLUGIN_NAMES = {"CarbonChat", "Carbon"};
private static final int SUBSCRIBE_ORDER = 100;
private static final int MAX_RETRY_ATTEMPTS = 30;
private static final long RETRY_DELAY_TICKS = 20L;
private static final long RETRY_PERIOD_TICKS = 20L;

private final JavaPlugin plugin;
private final PlayerChatHandler chatHandler;
Expand All @@ -36,6 +40,10 @@ public final class CarbonChatBridge implements AutoCloseable {
private Method eventMessageMethod;
private Method eventCancelledSetter;
private Method playerUuidMethod;
private CommonScheduler retryScheduler;
private int retryTaskId = -1;
private int retryAttempts = 0;
private volatile boolean registered = false;

public CarbonChatBridge(
JavaPlugin plugin,
Expand All @@ -51,6 +59,9 @@ public CarbonChatBridge(
}

public boolean register() {
if (registered) {
return true;
}
try {
Class<?> providerClass = loadCarbonClass("net.draycia.carbon.api.CarbonChatProvider");
Class<?> carbonChatClass = loadCarbonClass("net.draycia.carbon.api.CarbonChat");
Expand Down Expand Up @@ -83,9 +94,12 @@ public boolean register() {
false,
subscriber);

registered = true;
stopRetrying();
logger.info("CarbonChat chat bridge enabled");
return true;
} catch (ClassNotFoundException ex) {
logger.fine("CarbonChat bridge classes are not ready: " + ex.getMessage());
return false;
} catch (InvocationTargetException ex) {
Throwable cause = ex.getCause() == null ? ex : ex.getCause();
Expand All @@ -97,9 +111,41 @@ public boolean register() {
}
}

public void startRetrying(CommonScheduler scheduler) {
if (registered || retryTaskId > 0 || scheduler == null || !isCarbonPluginPresent()) {
return;
}

this.retryScheduler = scheduler;
logger.info("CarbonChat detected but chat bridge is not ready yet; retrying registration...");
retryTaskId = scheduler.runTimer(() -> {
if (registered) {
stopRetrying();
return;
}

retryAttempts++;
if (register()) {
return;
}

if (retryAttempts >= MAX_RETRY_ATTEMPTS) {
logger.warning("CarbonChat chat bridge could not be enabled after retries; "
+ "falling back to Paper AsyncChatEvent forwarding when available");
stopRetrying();
}
}, RETRY_DELAY_TICKS, RETRY_PERIOD_TICKS);
}

public boolean isRegistered() {
return registered;
}

@Override
public void close() {
if (subscription == null || disposeMethod == null) {
stopRetrying();
registered = false;
return;
}

Expand All @@ -110,9 +156,20 @@ public void close() {
logger.warning("CarbonChat bridge disposal failed: " + ex.getMessage());
} finally {
subscription = null;
registered = false;
stopRetrying();
}
}

private void stopRetrying() {
if (retryTaskId > 0 && retryScheduler != null) {
retryScheduler.cancelTask(retryTaskId);
}
retryTaskId = -1;
retryAttempts = 0;
retryScheduler = null;
}

private InvocationHandler createSubscriberHandler() {
return (proxy, method, args) -> {
if ("on".equals(method.getName()) && args != null && args.length == 1) {
Expand Down Expand Up @@ -196,10 +253,26 @@ private Class<?> loadCarbonClass(String name) throws ClassNotFoundException {
}

private ClassLoader findCarbonClassLoader() {
Plugin carbonPlugin = findCarbonPlugin();
return carbonPlugin == null ? null : carbonPlugin.getClass().getClassLoader();
}

private boolean isCarbonPluginPresent() {
return findCarbonPlugin() != null;
}

private Plugin findCarbonPlugin() {
for (String pluginName : CARBON_PLUGIN_NAMES) {
Plugin carbonPlugin = plugin.getServer().getPluginManager().getPlugin(pluginName);
if (carbonPlugin != null) {
return carbonPlugin.getClass().getClassLoader();
return carbonPlugin;
}
}

for (Plugin candidate : plugin.getServer().getPluginManager().getPlugins()) {
String name = candidate.getName();
if (name != null && (name.equalsIgnoreCase("CarbonChat") || name.equalsIgnoreCase("Carbon"))) {
return candidate;
}
}
return null;
Expand Down
Loading
Loading