第58批修复: AI聊天URL解析 + 信誉积分9格GUI重构
Check Source / check (push) Successful in 5s
Auto Release / build (push) Successful in 2m34s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s

AI聊天修复(AIChatManager):
- Bug1 内置供应商URL解析: findProviderUrl把含分组名的完整数组传进分组对象查找,
  国产.get(非国产)永远null → 改为先取顶层分组,再在分组内逐层查
- Bug2 自定义供应商双重路径: models.json已设自动补齐=false,
  代码仍无条件拼chat/completions → 加autoAppendCompletions开关,
  内置供应商总是拼接,自定义受开关控制
- 提取buildRequestUrl公共方法,sendToModel和checkAutoChat共用
This commit is contained in:
youpaishidifu
2026-10-28 12:33:48 +08:00
parent a0f84ee542
commit 59bf992162
3 changed files with 414 additions and 146 deletions
+40 -17
View File
@@ -51,6 +51,9 @@ public class AIChatManager {
// 默认上下文长度 // 默认上下文长度
private String defaultContextLength = "200000"; private String defaultContextLength = "200000";
// 自定义供应商 URL 是否自动拼接 chat/completions(自动补齐开关,默认 true)
private boolean autoAppendCompletions = true;
// 空闲调度器 // 空闲调度器
private BukkitTask autoChatTask = null; private BukkitTask autoChatTask = null;
private long lastAiReplyTime = 0; private long lastAiReplyTime = 0;
@@ -75,6 +78,7 @@ public class AIChatManager {
String baseUrl; String baseUrl;
String apiKey; String apiKey;
String maxToken; String maxToken;
boolean isCustom; // true=自定义供应商(受自动补齐开关控制), false=内置供应商(总是拼接)
} }
public AIChatManager(Main plugin) { public AIChatManager(Main plugin) {
@@ -137,6 +141,15 @@ public class AIChatManager {
// 解析全局配置(配置部分 → "2") // 解析全局配置(配置部分 → "2")
if (config.has("2")) { if (config.has("2")) {
JsonObject section2 = config.getAsJsonObject("2"); JsonObject section2 = config.getAsJsonObject("2");
// 自动补齐开关:自定义供应商 baseurl 是否自动拼接 chat/completions
if (section2.has("自动补齐")) {
JsonElement autoEl = section2.get("自动补齐");
if (autoEl != null && autoEl.isJsonPrimitive()) {
String av = autoEl.getAsString();
// 支持 true/false 字符串与布尔
autoAppendCompletions = "true".equalsIgnoreCase(av);
}
}
defaultContextLength = getStr(section2, "默认上下文长度"); defaultContextLength = getStr(section2, "默认上下文长度");
if (defaultContextLength == null || defaultContextLength.isEmpty()) { if (defaultContextLength == null || defaultContextLength.isEmpty()) {
defaultContextLength = "200000"; defaultContextLength = "200000";
@@ -244,15 +257,12 @@ public class AIChatManager {
continue; continue;
} }
// 确保 baseUrl 以 / 结尾(用于拼接 chat/completions) // 存储原始 baseUrl(尾斜杠统一在发送时按自动补齐开关处理)
if (!baseUrl.endsWith("/")) {
baseUrl = baseUrl + "/";
}
ModelEndpoint endpoint = new ModelEndpoint(); ModelEndpoint endpoint = new ModelEndpoint();
endpoint.baseUrl = baseUrl; endpoint.baseUrl = baseUrl;
endpoint.apiKey = apiKey; endpoint.apiKey = apiKey;
endpoint.maxToken = maxToken; endpoint.maxToken = maxToken;
endpoint.isCustom = isCustom;
// 解析可用模型列表 // 解析可用模型列表
if (supplierConfig.has("可用模型")) { if (supplierConfig.has("可用模型")) {
@@ -280,16 +290,11 @@ public class AIChatManager {
private String findProviderUrl(String providerPath) { private String findProviderUrl(String providerPath) {
if (providerPath == null || providerPath.isEmpty()) return null; if (providerPath == null || providerPath.isEmpty()) return null;
String[] parts = providerPath.split("/"); String[] parts = providerPath.split("/");
// 第一段是顶层分组名(国产/非国产),从第二段开始在分组对象里逐层找
// 先在国产下找 String group = parts[0].trim();
if (providersConfig != null && providersConfig.has("国产")) { String[] rest = Arrays.copyOfRange(parts, 1, parts.length);
String url = findUrlInObject(providersConfig.getAsJsonObject("国产"), parts); if (providersConfig != null && providersConfig.has(group)) {
if (url != null) return url; return findUrlInObject(providersConfig.getAsJsonObject(group), rest);
}
// 再在非国产下找
if (providersConfig != null && providersConfig.has("非国产")) {
String url = findUrlInObject(providersConfig.getAsJsonObject("非国产"), parts);
if (url != null) return url;
} }
return null; return null;
} }
@@ -385,12 +390,13 @@ public class AIChatManager {
// 构建请求体 // 构建请求体
String systemPrompt = buildSystemPrompt(); String systemPrompt = buildSystemPrompt();
String jsonBody = buildChatRequest(modelFull, systemPrompt, prompt, endpoint.maxToken); String jsonBody = buildChatRequest(modelFull, systemPrompt, prompt, endpoint.maxToken);
String requestUrl = buildRequestUrl(endpoint);
// 异步发送请求 // 异步发送请求
CompletableFuture.supplyAsync(() -> { CompletableFuture.supplyAsync(() -> {
try { try {
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(endpoint.baseUrl + "chat/completions")) .uri(URI.create(requestUrl))
.header("Content-Type", "application/json; charset=utf-8") .header("Content-Type", "application/json; charset=utf-8")
.header("Authorization", "Bearer " + endpoint.apiKey) .header("Authorization", "Bearer " + endpoint.apiKey)
.POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8)) .POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
@@ -458,6 +464,22 @@ public class AIChatManager {
return sb.toString(); return sb.toString();
} }
/**
* 构建请求 URL
* 自动补齐=true:baseUrl 以 / 结尾后拼接 chat/completions(默认)
* 自动补齐=false:baseUrl 已是完整端点(如 .../v1/chat/completions),直接用
*/
private String buildRequestUrl(ModelEndpoint endpoint) {
String url = endpoint.baseUrl;
// 内置供应商的 baseUrl 总是基础地址,必须拼接;自定义供应商受自动补齐开关控制
boolean needAppend = !endpoint.isCustom || autoAppendCompletions;
if (needAppend) {
if (!url.endsWith("/")) url += "/";
url += "chat/completions";
}
return url;
}
private String buildChatRequest(String model, String systemPrompt, String userPrompt, String maxToken) { private String buildChatRequest(String model, String systemPrompt, String userPrompt, String maxToken) {
Gson gson = new Gson(); Gson gson = new Gson();
JsonObject body = new JsonObject(); JsonObject body = new JsonObject();
@@ -592,11 +614,12 @@ public class AIChatManager {
// 异步发送 // 异步发送
String systemPrompt = buildSystemPrompt(); String systemPrompt = buildSystemPrompt();
String jsonBody = buildChatRequest(defaultModelFull, systemPrompt, prompt, endpoint.maxToken); String jsonBody = buildChatRequest(defaultModelFull, systemPrompt, prompt, endpoint.maxToken);
String requestUrl = buildRequestUrl(endpoint);
CompletableFuture.supplyAsync(() -> { CompletableFuture.supplyAsync(() -> {
try { try {
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(endpoint.baseUrl + "chat/completions")) .uri(URI.create(requestUrl))
.header("Content-Type", "application/json; charset=utf-8") .header("Content-Type", "application/json; charset=utf-8")
.header("Authorization", "Bearer " + endpoint.apiKey) .header("Authorization", "Bearer " + endpoint.apiKey)
.POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8)) .POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
+366 -128
View File
@@ -18,9 +18,10 @@ import java.util.concurrent.ConcurrentHashMap;
* *
* 核心逻辑: * 核心逻辑:
* - 每人初始化100积分 * - 每人初始化100积分
* - /credits 上报好人好事/举报 * - /credits 打开9格GUI:上报好人好事/举报违规/我的积分/积分明细
* - PHP 端可提交线索 * - 管理员(Tag)额外:加分/扣分/待审批/审批记录
* - 管理员审批 → 加分/扣分 * - /credits user <玩家名> 任何人可查他人积分
* - 上报通过聊天框分步引导:填目标玩家名 → 填上报理由
* - 低于阈值 → 行为限制(可配置规则) * - 低于阈值 → 行为限制(可配置规则)
*/ */
public class CreditManager { public class CreditManager {
@@ -43,6 +44,33 @@ public class CreditManager {
// 玩家积分缓存 (playerName -> score) // 玩家积分缓存 (playerName -> score)
private final Map<String, Integer> scoreCache = new ConcurrentHashMap<>(); private final Map<String, Integer> scoreCache = new ConcurrentHashMap<>();
// GUI标题
public static final String GUI_TITLE = "§d§l信誉积分";
// GUI尺寸
private static final int GUI_SIZE = 9;
// ==================== 聊天引导状态 ====================
/** 上报/审批聊天引导状态 */
public enum FlowState {
REPORT_GOOD_TARGET, // 好人好事 → 等待目标玩家名
REPORT_GOOD_REASON, // 好人好事 → 等待上报理由
REPORT_BAD_TARGET, // 举报 → 等待目标玩家名
REPORT_BAD_REASON, // 举报 → 等待上报理由
ADD_SCORE_TARGET, // 加分 → 等待玩家名
ADD_SCORE_AMOUNT, // 加分 → 等待数值
DEDUCT_SCORE_TARGET, // 扣分 → 等待玩家名
DEDUCT_SCORE_AMOUNT // 扣分 → 等待数值
}
// 玩家当前引导状态 (playerName -> FlowState + 临时数据)
private static class FlowData {
FlowState state;
String tempName; // 临时存储的目标玩家名
}
private final Map<String, FlowData> chatFlows = new ConcurrentHashMap<>();
public CreditManager(Main plugin) { public CreditManager(Main plugin) {
this.plugin = plugin; this.plugin = plugin;
this.db = plugin.getDb(); this.db = plugin.getDb();
@@ -58,29 +86,28 @@ public class CreditManager {
// 信誉积分记录表 // 信誉积分记录表
st.execute("CREATE TABLE IF NOT EXISTS credit_records (" st.execute("CREATE TABLE IF NOT EXISTS credit_records ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT," + "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "target_player TEXT NOT NULL," // 被评价的玩家 + "target_player TEXT NOT NULL,"
+ "reporter_player TEXT NOT NULL," // 上报人 + "reporter_player TEXT NOT NULL,"
+ "type TEXT NOT NULL," // good / bad + "type TEXT NOT NULL,"
+ "content TEXT DEFAULT ''," // 详细内容 + "content TEXT DEFAULT '',"
+ "status TEXT DEFAULT 'pending'," // pending / approved / rejected + "status TEXT DEFAULT 'pending',"
+ "score_change INTEGER DEFAULT 0," // 审批后的积分变动 + "score_change INTEGER DEFAULT 0,"
+ "created_at INTEGER DEFAULT 0," // 上报时间 + "created_at INTEGER DEFAULT 0,"
+ "approved_at INTEGER DEFAULT 0," // 审批时间 + "approved_at INTEGER DEFAULT 0,"
+ "approve_reason TEXT DEFAULT ''," // 审批理由 + "approve_reason TEXT DEFAULT '',"
+ "source TEXT DEFAULT 'game'," // game / php + "source TEXT DEFAULT 'game'"
+ "FOREIGN KEY(target_player) REFERENCES users(player_name)"
+ ")"); + ")");
// 信誉限制规则表 // 信誉限制规则表
st.execute("CREATE TABLE IF NOT EXISTS credit_rules (" st.execute("CREATE TABLE IF NOT EXISTS credit_rules ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT," + "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "rule_name TEXT NOT NULL UNIQUE," // 规则名 + "rule_name TEXT NOT NULL UNIQUE,"
+ "min_score INTEGER DEFAULT 0," // 低于此分触发 + "min_score INTEGER DEFAULT 0,"
+ "max_score INTEGER DEFAULT 0," // 高于此分解除(0=无上限) + "max_score INTEGER DEFAULT 0,"
+ "restriction_type TEXT DEFAULT ''," // 限制类型 + "restriction_type TEXT DEFAULT '',"
+ "restriction_duration INTEGER DEFAULT 0," // 持续秒数(0=直到加分) + "restriction_duration INTEGER DEFAULT 0,"
+ "enabled INTEGER DEFAULT 1," // 1=启用 0=禁用 + "enabled INTEGER DEFAULT 1,"
+ "description TEXT DEFAULT ''" // 说明 + "description TEXT DEFAULT ''"
+ ")"); + ")");
// 默认规则初始化 // 默认规则初始化
@@ -99,13 +126,10 @@ public class CreditManager {
private void initDefaultRules() { private void initDefaultRules() {
try { try {
Statement st = db.getConnection().createStatement(); Statement st = db.getConnection().createStatement();
// 默认规则:低于60分禁止交易
st.execute("INSERT OR IGNORE INTO credit_rules (rule_name, min_score, max_score, restriction_type, restriction_duration, description) " st.execute("INSERT OR IGNORE INTO credit_rules (rule_name, min_score, max_score, restriction_type, restriction_duration, description) "
+ "VALUES ('禁止交易', 60, 0, 'trade', 0, '低于60分时禁止与NPC交易')"); + "VALUES ('禁止交易', 60, 0, 'trade', 0, '低于60分时禁止与NPC交易')");
// 默认规则:低于40分禁止进入PVP区域
st.execute("INSERT OR IGNORE INTO credit_rules (rule_name, min_score, max_score, restriction_type, restriction_duration, description) " st.execute("INSERT OR IGNORE INTO credit_rules (rule_name, min_score, max_score, restriction_type, restriction_duration, description) "
+ "VALUES ('禁止PVP', 40, 0, 'pvp', 0, '低于40分时禁止进入PVP竞技场')"); + "VALUES ('禁止PVP', 40, 0, 'pvp', 0, '低于40分时禁止进入PVP竞技场')");
// 默认规则:低于20分禁止使用传送
st.execute("INSERT OR IGNORE INTO credit_rules (rule_name, min_score, max_score, restriction_type, restriction_duration, description) " st.execute("INSERT OR IGNORE INTO credit_rules (rule_name, min_score, max_score, restriction_type, restriction_duration, description) "
+ "VALUES ('禁止传送', 20, 0, 'teleport', 0, '低于20分时禁止使用传送功能')"); + "VALUES ('禁止传送', 20, 0, 'teleport', 0, '低于20分时禁止使用传送功能')");
} catch (Exception ignored) {} } catch (Exception ignored) {}
@@ -182,6 +206,15 @@ public class CreditManager {
public boolean approveRecord(int recordId, boolean approved, String reason, int scoreChange, String approverName) { public boolean approveRecord(int recordId, boolean approved, String reason, int scoreChange, String approverName) {
try { try {
// 先查目标玩家
String target = null;
PreparedStatement ps2 = db.getConnection().prepareStatement(
"SELECT target_player FROM credit_records WHERE id = ?");
ps2.setInt(1, recordId);
ResultSet rs2 = ps2.executeQuery();
if (rs2.next()) target = rs2.getString("target_player");
ps2.close();
PreparedStatement ps = db.getConnection().prepareStatement( PreparedStatement ps = db.getConnection().prepareStatement(
"UPDATE credit_records SET status = ?, score_change = ?, approved_at = ?, approve_reason = ? WHERE id = ?"); "UPDATE credit_records SET status = ?, score_change = ?, approved_at = ?, approve_reason = ? WHERE id = ?");
ps.setString(1, approved ? STATUS_APPROVED : STATUS_REJECTED); ps.setString(1, approved ? STATUS_APPROVED : STATUS_REJECTED);
@@ -192,19 +225,10 @@ public class CreditManager {
ps.executeUpdate(); ps.executeUpdate();
ps.close(); ps.close();
if (approved && scoreChange != 0) { if (approved && scoreChange != 0 && target != null) {
// 查询目标玩家 addScore(target, scoreChange, "信誉积分审批(" + (approved ? "通过" : "驳回") + "): " + reason);
PreparedStatement ps2 = db.getConnection().prepareStatement( plugin.getLogger().info("[信誉积分] " + approverName + " 审批通过 " + target
"SELECT target_player FROM credit_records WHERE id = ?"); + " 积分变动: " + (scoreChange > 0 ? "+" : "") + scoreChange);
ps2.setInt(1, recordId);
ResultSet rs = ps2.executeQuery();
if (rs.next()) {
String target = rs.getString("target_player");
addScore(target, scoreChange, "信誉积分审批(" + (approved ? "通过" : "驳回") + "): " + reason);
plugin.getLogger().info("[信誉积分] " + approverName + " 审批通过 " + target
+ " 积分变动: " + (scoreChange > 0 ? "+" : "") + scoreChange);
}
ps2.close();
} }
return true; return true;
} catch (Exception e) { } catch (Exception e) {
@@ -305,7 +329,6 @@ public class CreditManager {
} }
private void checkRestrictions(String playerName, int score) { private void checkRestrictions(String playerName, int score) {
// 检查是否触发限制,通知玩家
for (Map<String, Object> rule : getEnabledRules()) { for (Map<String, Object> rule : getEnabledRules()) {
int minScore = ((Number) rule.get("min_score")).intValue(); int minScore = ((Number) rule.get("min_score")).intValue();
int maxScore = ((Number) rule.get("max_score")).intValue(); int maxScore = ((Number) rule.get("max_score")).intValue();
@@ -322,10 +345,92 @@ public class CreditManager {
} }
} }
// ==================== GUI ==================== // ==================== 9格 GUI ====================
public Inventory createMainGUI(Player p) {
Inventory inv = Bukkit.createInventory(null, GUI_SIZE, GUI_TITLE);
boolean admin = plugin.isAdmin(p);
// Slot 0: 上报好人好事
inv.setItem(0, mkItem(Material.EMERALD,
"§a§l上报好人好事",
"§7表扬其他玩家的良好行为",
"§7点击后聊天框输入目标玩家名"));
// Slot 1: 举报违规违纪
inv.setItem(1, mkItem(Material.REDSTONE,
"§c§l举报违规违纪",
"§7举报其他玩家的违规行为",
"§7点击后聊天框输入目标玩家名"));
// Slot 2: 我的积分
int myScore = getScore(p.getName());
inv.setItem(2, mkItem(Material.GOLD_INGOT,
"§e§l我的积分",
"§7当前积分: " + myScore,
"§7初始积分: " + DEFAULT_INIT_SCORE));
// Slot 3: 积分明细
inv.setItem(3, mkItem(Material.PAPER,
"§f§l积分明细",
"§7查看自己的积分变动记录",
"§7点击查看"));
// Slot 4: 加分 (管理员)
if (admin) {
inv.setItem(4, mkItem(Material.EMERALD,
"§a§l加分",
"§7管理员给玩家加分",
"§7点击后聊天框输入玩家名"));
}
// Slot 5: 扣分 (管理员)
if (admin) {
inv.setItem(5, mkItem(Material.REDSTONE,
"§c§l扣分",
"§7管理员给玩家扣分",
"§7点击后聊天框输入玩家名"));
}
// Slot 6: 待审批 (管理员)
if (admin) {
inv.setItem(6, mkItem(Material.CHEST,
"§e§l待审批",
"§7审批玩家上报的好人好事/举报",
"§7点击查看"));
}
// Slot 7: 审批记录 (管理员)
if (admin) {
inv.setItem(7, mkItem(Material.BOOK,
"§b§l审批记录",
"§7查看历史审批记录",
"§7点击查看"));
}
// Slot 8: 规则说明
inv.setItem(8, mkItem(Material.IRON_INGOT,
"§7§l规则说明",
"§7低于60分: 禁止交易",
"§7低于40分: 禁止PVP",
"§7低于20分: 禁止传送"));
// 非管理员在 4-7 槽显示锁定
if (!admin) {
for (int i = 4; i <= 7; i++) {
if (inv.getItem(i) == null) {
inv.setItem(i, mkItem(Material.RED_STAINED_GLASS_PANE,
"§c§l管理员专属",
"§7需要Tag管理员身份"));
}
}
}
return inv;
}
public Inventory createPendingGUI(Player p) { public Inventory createPendingGUI(Player p) {
Inventory inv = Bukkit.createInventory(null, 54, "§d§l信誉积分 - 待审批"); Inventory inv = Bukkit.createInventory(null, 54, GUI_TITLE + " - 待审批");
fillBg(inv); fillBg(inv);
List<Map<String, Object>> records = getPendingRecords(); List<Map<String, Object>> records = getPendingRecords();
for (int i = 0; i < records.size() && i < 49; i++) { for (int i = 0; i < records.size() && i < 49; i++) {
@@ -340,14 +445,41 @@ public class CreditManager {
"§7内容: " + r.get("content"), "§7内容: " + r.get("content"),
"§7来源: " + r.get("source"), "§7来源: " + r.get("source"),
"", "",
"§e点击审批")); "§e/credits approve " + r.get("id") + " <通过|驳回> <理由> [积分变动]"));
} }
inv.setItem(53, mkItem(Material.ARROW, "§7返回")); inv.setItem(53, mkItem(Material.ARROW, "§7返回"));
return inv; return inv;
} }
public Inventory createRecordsGUI(Player p) { public Inventory createRecordsGUI(Player p) {
Inventory inv = Bukkit.createInventory(null, 54, "§d§l信誉积分 - 历史记录"); Inventory inv = Bukkit.createInventory(null, 54, GUI_TITLE + " - 历史记录");
fillBg(inv);
// 只看自己的记录
List<Map<String, Object>> records = new ArrayList<>();
for (Map<String, Object> r : getAllRecords(49)) {
if (r.get("target_player").equals(p.getName())) {
records.add(r);
}
}
for (int i = 0; i < records.size() && i < 49; i++) {
Map<String, Object> r = records.get(i);
String status = (String) r.get("status");
String statusText = STATUS_APPROVED.equals(status) ? "§a已通过"
: STATUS_REJECTED.equals(status) ? "§c已驳回" : "§e待审批";
inv.setItem(i, mkItem(Material.PAPER,
"§e#" + r.get("id") + " " + r.get("type"),
"§7上报人: " + r.get("reporter_player"),
"§7状态: " + statusText,
"§7积分变动: " + r.get("score_change"),
"§7内容: " + r.get("content"),
"§7来源: " + r.get("source")));
}
inv.setItem(53, mkItem(Material.ARROW, "§7返回"));
return inv;
}
public Inventory createAdminRecordsGUI(Player p) {
Inventory inv = Bukkit.createInventory(null, 54, GUI_TITLE + " - 审批记录");
fillBg(inv); fillBg(inv);
List<Map<String, Object>> records = getAllRecords(49); List<Map<String, Object>> records = getAllRecords(49);
for (int i = 0; i < records.size() && i < 49; i++) { for (int i = 0; i < records.size() && i < 49; i++) {
@@ -368,33 +500,15 @@ public class CreditManager {
return inv; return inv;
} }
public Inventory createRulesGUI(Player p) {
Inventory inv = Bukkit.createInventory(null, 54, "§d§l信誉积分 - 限制规则");
fillBg(inv);
List<Map<String, Object>> rules = getEnabledRules();
for (int i = 0; i < rules.size() && i < 49; i++) {
Map<String, Object> r = rules.get(i);
inv.setItem(i, mkItem(Material.GOLD_INGOT,
"§e" + r.get("rule_name"),
"§7阈值: " + r.get("min_score"),
"§7类型: " + r.get("restriction_type"),
"§7说明: " + r.get("description")));
}
inv.setItem(53, mkItem(Material.ARROW, "§7返回"));
return inv;
}
// ==================== 命令入口 ==================== // ==================== 命令入口 ====================
public boolean handleCommand(CommandSender sender, String[] args) { public boolean handleCommand(CommandSender sender, String[] args) {
// /credits → 打开GUI
if (args.length == 0) { if (args.length == 0) {
// 无参数:显示玩家自己的积分
if (sender instanceof Player) { if (sender instanceof Player) {
String name = ((Player) sender).getName(); ((Player) sender).openInventory(createMainGUI((Player) sender));
int score = getScore(name);
sender.sendMessage("§d§l[信誉积分] §f你的当前积分: §e" + score + " §7(初始: " + DEFAULT_INIT_SCORE + ")");
} else { } else {
sender.sendMessage("§c用法: /credits [上报|审批|记录|规则] [参数]"); sender.sendMessage("§c用法: /credits [user <玩家名>]");
} }
return true; return true;
} }
@@ -402,36 +516,30 @@ public class CreditManager {
String sub = args[0].toLowerCase(); String sub = args[0].toLowerCase();
switch (sub) { switch (sub) {
case "上报": case "user":
case "report": // /credits user <玩家名> — 任何人可查他人积分
// /credits report <目标玩家> <good|bad> <内容> if (args.length < 2) {
if (args.length < 4) { sender.sendMessage("§c用法: /credits user <玩家名>");
sender.sendMessage("§c用法: /credits report <目标玩家> <good|bad> <内容>");
return true; return true;
} }
if (!(sender instanceof Player)) { String targetName = args[1];
sender.sendMessage("§c此命令只能玩家使用"); int targetScore = getScore(targetName);
return true; sender.sendMessage("§d§l[信誉积分] §f" + targetName + " §7的当前积分: §e" + targetScore
+ " §7(初始: " + DEFAULT_INIT_SCORE + ")");
// 判断是否触发限制
for (Map<String, Object> rule : getEnabledRules()) {
int minScore = ((Number) rule.get("min_score")).intValue();
if (targetScore < minScore) {
String desc = (String) rule.get("description");
sender.sendMessage("§c§l[信誉积分] §f" + targetName + " §7已触发限制: §c" + desc);
}
} }
String target = args[1];
String type = args[2].toLowerCase();
StringBuilder content = new StringBuilder();
for (int i = 3; i < args.length; i++) {
if (i > 3) content.append(" ");
content.append(args[i]);
}
if (!TYPE_GOOD.equals(type) && !TYPE_BAD.equals(type)) {
sender.sendMessage("§c类型必须是 good 或 bad");
return true;
}
submitReport(target, ((Player) sender).getName(), type, content.toString(), "game");
sender.sendMessage("§a[信誉积分] 上报成功,等待管理员审批");
return true; return true;
case "审批": case "审批":
case "approve": case "approve":
// 管理员审批 // 管理员审批
if (!(sender.hasPermission("sdf1_login.admin"))) { if (!(sender instanceof Player && plugin.isAdmin(sender))) {
sender.sendMessage("§c权限不足"); sender.sendMessage("§c权限不足");
return true; return true;
} }
@@ -472,17 +580,14 @@ public class CreditManager {
if (i > 3) reason.append(" "); if (i > 3) reason.append(" ");
reason.append(args[i]); reason.append(args[i]);
} }
// 解析积分变动(最后一个参数如果是数字)
int scoreChange = 0; int scoreChange = 0;
String reasonStr = reason.toString(); String reasonStr = reason.toString();
// 检查是否以数字结尾(积分变动在最后)
java.util.regex.Matcher m = java.util.regex.Pattern.compile("(\\d+)$").matcher(reasonStr); java.util.regex.Matcher m = java.util.regex.Pattern.compile("(\\d+)$").matcher(reasonStr);
if (approved && m.find()) { if (approved && m.find()) {
scoreChange = Integer.parseInt(m.group(1)); scoreChange = Integer.parseInt(m.group(1));
reasonStr = reasonStr.substring(0, reasonStr.length() - m.group(1).length()).trim(); reasonStr = reasonStr.substring(0, reasonStr.length() - m.group(1).length()).trim();
if (reasonStr.isEmpty()) reasonStr = "信誉积分审批"; if (reasonStr.isEmpty()) reasonStr = "信誉积分审批";
} else if (approved) { } else if (approved) {
// 默认好人好事+10,举报+10
scoreChange = 10; scoreChange = 10;
} }
String approver = sender instanceof Player ? ((Player) sender).getName() : "控制台"; String approver = sender instanceof Player ? ((Player) sender).getName() : "控制台";
@@ -497,38 +602,9 @@ public class CreditManager {
} }
return true; return true;
case "记录":
case "records":
// 查看历史记录
if (sender instanceof Player) {
((Player) sender).openInventory(createRecordsGUI((Player) sender));
} else {
List<Map<String, Object>> records = getAllRecords(20);
for (Map<String, Object> r : records) {
sender.sendMessage("§e#" + r.get("id") + " " + r.get("target_player")
+ " §7(" + r.get("type") + ") " + r.get("status")
+ " §7积分变动=" + r.get("score_change"));
}
}
return true;
case "规则":
case "rules":
// 查看限制规则
if (sender instanceof Player) {
((Player) sender).openInventory(createRulesGUI((Player) sender));
} else {
for (Map<String, Object> r : getEnabledRules()) {
sender.sendMessage("§e" + r.get("rule_name")
+ " §7阈值=" + r.get("min_score")
+ " §7类型=" + r.get("restriction_type")
+ " §7说明=" + r.get("description"));
}
}
return true;
default: default:
sender.sendMessage("§c未知子命令: " + sub); sender.sendMessage("§c未知子命令: " + sub);
sender.sendMessage("§7用法: /credits [user <玩家名>]");
return true; return true;
} }
} }
@@ -536,21 +612,189 @@ public class CreditManager {
// ==================== GUI 点击处理 ==================== // ==================== GUI 点击处理 ====================
public void handleClick(Player p, int slot, String title) { public void handleClick(Player p, int slot, String title) {
// 子GUI的返回按钮
if (slot == 53) { if (slot == 53) {
p.closeInventory(); p.closeInventory();
p.openInventory(createMainGUI(p));
return; return;
} }
// 主GUI
if (title != null && title.equals(GUI_TITLE)) {
switch (slot) {
case 0: // 上报好人好事
startFlow(p, FlowState.REPORT_GOOD_TARGET);
p.closeInventory();
p.sendMessage("§a§l[信誉积分] §f请输入要表扬的玩家名");
p.sendMessage("§7直接在聊天框输入名字即可");
break;
case 1: // 举报违规违纪
startFlow(p, FlowState.REPORT_BAD_TARGET);
p.closeInventory();
p.sendMessage("§c§l[信誉积分] §f请输入要举报的玩家名");
p.sendMessage("§7直接在聊天框输入名字即可");
break;
case 2: // 我的积分
int myScore = getScore(p.getName());
p.closeInventory();
p.sendMessage("§d§l[信誉积分] §f你的当前积分: §e" + myScore + " §7(初始: " + DEFAULT_INIT_SCORE + ")");
break;
case 3: // 积分明细
p.closeInventory();
p.openInventory(createRecordsGUI(p));
break;
case 4: // 加分 (管理员)
if (!plugin.isAdmin(p)) {
p.sendMessage("§c§l[信誉积分] §c权限不足");
return;
}
startFlow(p, FlowState.ADD_SCORE_TARGET);
p.closeInventory();
p.sendMessage("§a§l[信誉积分] §f请输入要加分的玩家名");
break;
case 5: // 扣分 (管理员)
if (!plugin.isAdmin(p)) {
p.sendMessage("§c§l[信誉积分] §c权限不足");
return;
}
startFlow(p, FlowState.DEDUCT_SCORE_TARGET);
p.closeInventory();
p.sendMessage("§c§l[信誉积分] §f请输入要扣分的玩家名");
break;
case 6: // 待审批 (管理员)
if (!plugin.isAdmin(p)) {
p.sendMessage("§c§l[信誉积分] §c权限不足");
return;
}
p.closeInventory();
p.openInventory(createPendingGUI(p));
break;
case 7: // 审批记录 (管理员)
if (!plugin.isAdmin(p)) {
p.sendMessage("§c§l[信誉积分] §c权限不足");
return;
}
p.closeInventory();
p.openInventory(createAdminRecordsGUI(p));
break;
case 8: // 规则说明
p.closeInventory();
p.sendMessage("§d§l[信誉积分] §f规则说明:");
for (Map<String, Object> r : getEnabledRules()) {
p.sendMessage("§7" + r.get("rule_name") + ": " + r.get("description"));
}
break;
}
return;
}
// 待审批GUI — 点击物品提示用命令审批
if (title != null && title.contains("待审批")) { if (title != null && title.contains("待审批")) {
p.sendMessage("§c请使用 /credits approve <id> <通过|驳回> <理由> [积分变动]"); p.sendMessage("§c请使用 /credits approve <id> <通过|驳回> <理由> [积分变动]");
p.closeInventory(); p.closeInventory();
} else if (title != null && title.contains("历史记录")) { return;
p.sendMessage("§7此GUI仅展示,请用命令管理");
} else if (title != null && title.contains("限制规则")) {
p.sendMessage("§7规则阈值低于当前积分则触发限制");
} }
} }
// ==================== 聊天引导处理 ====================
/**
* 在 Main.java onChat 中调用,处理信誉积分的聊天引导输入
* @return true=消息已消费(需取消聊天事件),false=非引导状态(放行正常聊天)
*/
public boolean handleChatInput(Player p, String msg) {
FlowData fd = chatFlows.get(p.getName());
if (fd == null) return false;
switch (fd.state) {
case REPORT_GOOD_TARGET:
fd.tempName = msg.trim();
fd.state = FlowState.REPORT_GOOD_REASON;
p.sendMessage("§a§l[信誉积分] §f已记录: " + fd.tempName);
p.sendMessage("§7请输入上报理由(好人好事)");
return true;
case REPORT_GOOD_REASON:
submitReport(fd.tempName, p.getName(), TYPE_GOOD, msg.trim(), "game");
chatFlows.remove(p.getName());
p.sendMessage("§a§l[信誉积分] §f上报成功,等待管理员审批");
return true;
case REPORT_BAD_TARGET:
fd.tempName = msg.trim();
fd.state = FlowState.REPORT_BAD_REASON;
p.sendMessage("§c§l[信誉积分] §f已记录: " + fd.tempName);
p.sendMessage("§7请输入举报理由");
return true;
case REPORT_BAD_REASON:
submitReport(fd.tempName, p.getName(), TYPE_BAD, msg.trim(), "game");
chatFlows.remove(p.getName());
p.sendMessage("§a§l[信誉积分] §f举报已提交,等待管理员审批");
return true;
case ADD_SCORE_TARGET:
fd.tempName = msg.trim();
fd.state = FlowState.ADD_SCORE_AMOUNT;
p.sendMessage("§a§l[信誉积分] §f已记录: " + fd.tempName);
p.sendMessage("§7请输入加分数值(正整数)");
return true;
case ADD_SCORE_AMOUNT:
try {
int amount = Integer.parseInt(msg.trim());
if (amount <= 0) {
p.sendMessage("§c数值必须是正整数");
fd.state = FlowState.ADD_SCORE_AMOUNT;
return true;
}
addScore(fd.tempName, amount, "管理员手动加分");
chatFlows.remove(p.getName());
p.sendMessage("§a§l[信誉积分] §f已给 " + fd.tempName + " 加分 §e+" + amount);
} catch (NumberFormatException e) {
p.sendMessage("§c请输入数字");
fd.state = FlowState.ADD_SCORE_AMOUNT;
}
return true;
case DEDUCT_SCORE_TARGET:
fd.tempName = msg.trim();
fd.state = FlowState.DEDUCT_SCORE_AMOUNT;
p.sendMessage("§c§l[信誉积分] §f已记录: " + fd.tempName);
p.sendMessage("§7请输入扣分数值(正整数)");
return true;
case DEDUCT_SCORE_AMOUNT:
try {
int amount = Integer.parseInt(msg.trim());
if (amount <= 0) {
p.sendMessage("§c数值必须是正整数");
fd.state = FlowState.DEDUCT_SCORE_AMOUNT;
return true;
}
addScore(fd.tempName, -amount, "管理员手动扣分");
chatFlows.remove(p.getName());
p.sendMessage("§c§l[信誉积分] §f已给 " + fd.tempName + " 扣分 §e-" + amount);
} catch (NumberFormatException e) {
p.sendMessage("§c请输入数字");
fd.state = FlowState.DEDUCT_SCORE_AMOUNT;
}
return true;
}
return true;
}
private void startFlow(Player p, FlowState state) {
FlowData fd = new FlowData();
fd.state = state;
chatFlows.put(p.getName(), fd);
}
/** 是否处于聊天引导状态 */
public boolean isInChatFlow(Player p) {
return chatFlows.containsKey(p.getName());
}
// ==================== GUI 工具 ==================== // ==================== GUI 工具 ====================
private void fillBg(Inventory inv) { private void fillBg(Inventory inv) {
@@ -571,17 +815,11 @@ public class CreditManager {
// ==================== PHP 端提交线索 ==================== // ==================== PHP 端提交线索 ====================
/**
* 供 PHP 端调用的提交线索接口(通过 WebManager 对账或独立 API)
*/
public boolean submitFromPhp(String targetPlayer, String reporterPlayer, String type, String content) { public boolean submitFromPhp(String targetPlayer, String reporterPlayer, String type, String content) {
submitReport(targetPlayer, reporterPlayer, type, content, "php"); submitReport(targetPlayer, reporterPlayer, type, content, "php");
return true; return true;
} }
/**
* 供 PHP 端调用的审批接口
*/
public boolean approveFromPhp(int recordId, boolean approved, String reason, int scoreChange, String approverName) { public boolean approveFromPhp(int recordId, boolean approved, String reason, int scoreChange, String approverName) {
return approveRecord(recordId, approved, reason, scoreChange, approverName); return approveRecord(recordId, approved, reason, scoreChange, approverName);
} }
+8 -1
View File
@@ -1563,7 +1563,7 @@ public class Main extends JavaPlugin
emailBindTasks.put(uid, holder); emailBindTasks.put(uid, holder);
} }
private boolean isAdmin(CommandSender sender) { public boolean isAdmin(CommandSender sender) {
if (sender instanceof Player) if (sender instanceof Player)
return ((Player) sender) return ((Player) sender)
.getScoreboardTags() .getScoreboardTags()
@@ -6195,6 +6195,13 @@ public class Main extends JavaPlugin
aiChatManager.handlePlayerChat(p, msg); aiChatManager.handlePlayerChat(p, msg);
} }
// ★ 第58批:信誉积分聊天引导 — 上报/加分/扣分分步输入
if (creditManager != null && creditManager.isInChatFlow(p)) {
e.setCancelled(true);
creditManager.handleChatInput(p, msg);
return;
}
// ★ 先探一次禁言状态:若刚好到期,isMuted() 内部会立即解禁并打上 // ★ 先探一次禁言状态:若刚好到期,isMuted() 内部会立即解禁并打上
// 「需重新人机验证」标记(2026-10-08:解禁≠解除人机验证) // 「需重新人机验证」标记(2026-10-08:解禁≠解除人机验证)
if (chatFilter != null) { if (chatFilter != null) {