第58批功能1+2: 信誉积分管理系统 + AI聊天介入
信誉积分管理系统(CreditManager): - /credits 上报好人好事/举报,每人初始化100积分 - 管理员审批后加分/扣分,低于阈值触发行为限制(可配置规则) - PHP端可提交线索,admin审批接口 - GUI: 待审批/历史记录/限制规则 - DB: credit_records + credit_rules 表 + users.credit_score列 AI聊天介入(AIChatManager): - 兼容openai协议,读models.json/供应商.json/提示词.txt配置 - @//模型名/前缀触发直接提问,私聊回复 - 空闲1-10分钟自动抓聊天互动,广播回复 - 黑名单拦截不浪费token - 对话留档issue.json,HTTP异步请求不阻塞主线程 - 已解除.gitignore对openai资源的屏蔽
This commit is contained in:
+4
-2
@@ -123,5 +123,7 @@ test.*
|
||||
!readme.md
|
||||
!src/main/resources
|
||||
!src/main/resources/**/*.*
|
||||
# 临时屏蔽resources的开发文件
|
||||
src/main/resources/openai/*
|
||||
# openai资源文件已启用(第58批 AI聊天功能)
|
||||
!src/main/resources/openai/
|
||||
!src/main/resources/openai/*.json
|
||||
!src/main/resources/openai/*.txt
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import com.google.gson.*;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URI;
|
||||
import java.net.http.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* ★ 第58批功能2: AI聊天介入
|
||||
*
|
||||
* 核心逻辑:
|
||||
* - 兼容 openai 协议,根据模型名取 api 地址和 key
|
||||
* - @//模型名/ 前缀触发直接提问
|
||||
* - 空闲时 1-10 分钟自动抓聊天互动
|
||||
* - 黑名单拦截不浪费 token
|
||||
* - 对话留档 issue.json
|
||||
*/
|
||||
public class AIChatManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
// 配置数据
|
||||
private JsonObject modelsConfig;
|
||||
private JsonObject providersConfig;
|
||||
private final Map<String, String> aliasToFullModel = new HashMap<>(); // 简称 → 模型全名
|
||||
private final Map<String, ModelEndpoint> modelEndpoints = new HashMap<>(); // 模型全名 → 端点
|
||||
private final List<String> blacklist = new ArrayList<>();
|
||||
|
||||
// 默认模型
|
||||
private String defaultModelFull = "";
|
||||
|
||||
// 人设
|
||||
private String serverName = "";
|
||||
private String assistantName = "";
|
||||
private String writingStyle = "";
|
||||
|
||||
// 发言间隔(分钟)
|
||||
private int minIntervalMin = 1;
|
||||
private int maxIntervalMin = 10;
|
||||
|
||||
// 默认上下文长度
|
||||
private String defaultContextLength = "200000";
|
||||
|
||||
// 空闲调度器
|
||||
private BukkitTask autoChatTask = null;
|
||||
private long lastAiReplyTime = 0;
|
||||
private final AtomicBoolean autoChatRunning = new AtomicBoolean(false);
|
||||
|
||||
// 最近聊天记录缓冲区
|
||||
private final List<String> recentChatBuffer = new ArrayList<>();
|
||||
private static final int MAX_BUFFER = 30;
|
||||
|
||||
// 对话留档
|
||||
private JsonObject issueRecord = new JsonObject();
|
||||
private File issueFile = null;
|
||||
|
||||
// HTTP Client
|
||||
private HttpClient httpClient;
|
||||
|
||||
// 最近提问的玩家(防止频繁请求)
|
||||
private final Map<String, Long> lastRequestTime = new ConcurrentHashMap<>();
|
||||
|
||||
// 模型端点
|
||||
static class ModelEndpoint {
|
||||
String baseUrl;
|
||||
String apiKey;
|
||||
String maxToken;
|
||||
}
|
||||
|
||||
public AIChatManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(30))
|
||||
.build();
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
// ==================== 配置加载 ====================
|
||||
|
||||
private void loadConfig() {
|
||||
try {
|
||||
// 确保 openai 目录存在
|
||||
File openaiDir = new File(plugin.getDataFolder(), "openai");
|
||||
if (!openaiDir.exists()) {
|
||||
openaiDir.mkdirs();
|
||||
copyResource("openai/models.json", new File(openaiDir, "models.json"));
|
||||
copyResource("openai/供应商.json", new File(openaiDir, "供应商.json"));
|
||||
copyResource("openai/提示词.txt", new File(openaiDir, "提示词.txt"));
|
||||
copyResource("openai/issue.json", new File(openaiDir, "issue.json"));
|
||||
plugin.getLogger().info("[AI聊天] 已复制默认配置文件到 " + openaiDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
// 读取配置
|
||||
File modelsFile = new File(openaiDir, "models.json");
|
||||
File providersFile = new File(openaiDir, "供应商.json");
|
||||
File promptFile = new File(openaiDir, "提示词.txt");
|
||||
issueFile = new File(openaiDir, "issue.json");
|
||||
|
||||
if (modelsFile.exists()) {
|
||||
modelsConfig = JsonParser.parseString(Files.readString(modelsFile.toPath(), StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
}
|
||||
if (providersFile.exists()) {
|
||||
providersConfig = JsonParser.parseString(Files.readString(providersFile.toPath(), StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
}
|
||||
|
||||
// 读取人设
|
||||
if (modelsConfig != null && modelsConfig.has("人设部分")) {
|
||||
JsonObject persona = modelsConfig.getAsJsonObject("人设部分");
|
||||
serverName = getStr(persona, "服务器名字");
|
||||
assistantName = getStr(persona, "助理名字");
|
||||
writingStyle = getStr(persona, "写法");
|
||||
}
|
||||
|
||||
// 读取配置部分
|
||||
if (modelsConfig != null && modelsConfig.has("配置部分")) {
|
||||
JsonObject config = modelsConfig.getAsJsonObject("配置部分");
|
||||
|
||||
// 解析模型配置(配置部分 → "1" → "模型配置")
|
||||
if (config.has("1")) {
|
||||
JsonObject section1 = config.getAsJsonObject("1");
|
||||
if (section1.has("模型配置")) {
|
||||
JsonObject modelConfig = section1.getAsJsonObject("模型配置");
|
||||
parseModelConfig(modelConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// 解析全局配置(配置部分 → "2")
|
||||
if (config.has("2")) {
|
||||
JsonObject section2 = config.getAsJsonObject("2");
|
||||
defaultContextLength = getStr(section2, "默认上下文长度");
|
||||
if (defaultContextLength == null || defaultContextLength.isEmpty()) {
|
||||
defaultContextLength = "200000";
|
||||
}
|
||||
|
||||
// 发言间隔
|
||||
if (section2.has("发言间隔")) {
|
||||
JsonObject interval = section2.getAsJsonObject("发言间隔");
|
||||
String minStr = getStr(interval, "最短");
|
||||
String maxStr = getStr(interval, "最长");
|
||||
if (minStr != null && !minStr.isEmpty()) {
|
||||
try { minIntervalMin = Integer.parseInt(minStr); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
if (maxStr != null && !maxStr.isEmpty()) {
|
||||
try { maxIntervalMin = Integer.parseInt(maxStr); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
// 黑名单
|
||||
if (section2.has("黑名单")) {
|
||||
JsonElement bl = section2.get("黑名单");
|
||||
if (bl.isJsonArray()) {
|
||||
for (JsonElement el : bl.getAsJsonArray()) {
|
||||
if (el.isJsonPrimitive()) {
|
||||
blacklist.add(el.getAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 读取提示词.txt — 解析 models=简称 格式
|
||||
aliasToFullModel.clear();
|
||||
if (promptFile.exists()) {
|
||||
List<String> lines = Files.readAllLines(promptFile.toPath(), StandardCharsets.UTF_8);
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
if (line.startsWith("models=")) {
|
||||
String content = line.substring("models=".length()).trim();
|
||||
if (content.contains("=")) {
|
||||
// models=模型全名=简称
|
||||
String[] parts = content.split("=", 2);
|
||||
aliasToFullModel.put(parts[1].trim(), parts[0].trim());
|
||||
} else {
|
||||
// models=简称 → 简称直接作为模型全名
|
||||
aliasToFullModel.put(content, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 读取对话留档
|
||||
if (issueFile.exists()) {
|
||||
issueRecord = JsonParser.parseString(Files.readString(issueFile.toPath(), StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
}
|
||||
|
||||
plugin.getLogger().info("[AI聊天] 配置加载完成: " + modelEndpoints.size() + "个模型, "
|
||||
+ aliasToFullModel.size() + "个别名, 黑名单" + blacklist.size() + "条");
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[AI聊天] 配置加载失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void copyResource(String resourcePath, File target) {
|
||||
try {
|
||||
InputStream is = plugin.getResource(resourcePath);
|
||||
if (is == null) return;
|
||||
Files.copy(is, target.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
is.close();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[AI聊天] 复制资源文件失败: " + resourcePath + " - " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void parseModelConfig(JsonObject modelConfig) {
|
||||
// 遍历所有供应商配置
|
||||
for (Map.Entry<String, JsonElement> entry : modelConfig.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (!entry.getValue().isJsonObject()) continue;
|
||||
JsonObject supplierConfig = entry.getValue().getAsJsonObject();
|
||||
|
||||
// 判断是内置还是自定义
|
||||
boolean isCustom = supplierConfig.has("baseurl");
|
||||
boolean isBuiltin = supplierConfig.has("供应商名字");
|
||||
|
||||
if (!isCustom && !isBuiltin) continue;
|
||||
|
||||
String baseUrl;
|
||||
String apiKey = getStr(supplierConfig, "apikey");
|
||||
String maxToken = getStr(supplierConfig, "max_token");
|
||||
if (maxToken == null || maxToken.isEmpty()) maxToken = defaultContextLength;
|
||||
|
||||
if (isCustom) {
|
||||
baseUrl = getStr(supplierConfig, "baseurl");
|
||||
} else {
|
||||
String providerName = getStr(supplierConfig, "供应商名字");
|
||||
baseUrl = findProviderUrl(providerName);
|
||||
}
|
||||
|
||||
if (baseUrl == null || baseUrl.isEmpty() || apiKey == null || apiKey.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 确保 baseUrl 以 / 结尾(用于拼接 chat/completions)
|
||||
if (!baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl + "/";
|
||||
}
|
||||
|
||||
ModelEndpoint endpoint = new ModelEndpoint();
|
||||
endpoint.baseUrl = baseUrl;
|
||||
endpoint.apiKey = apiKey;
|
||||
endpoint.maxToken = maxToken;
|
||||
|
||||
// 解析可用模型列表
|
||||
if (supplierConfig.has("可用模型")) {
|
||||
JsonElement models = supplierConfig.get("可用模型");
|
||||
if (models.isJsonArray()) {
|
||||
for (JsonElement modelEl : models.getAsJsonArray()) {
|
||||
if (modelEl.isJsonPrimitive()) {
|
||||
String modelName = modelEl.getAsString();
|
||||
modelEndpoints.put(modelName, endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 默认使用
|
||||
String defaultUse = getStr(supplierConfig, "默认使用");
|
||||
if (defaultUse != null && !defaultUse.isEmpty() && defaultModelFull.isEmpty()) {
|
||||
defaultModelFull = defaultUse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 供应商URL查找 ====================
|
||||
|
||||
private String findProviderUrl(String providerPath) {
|
||||
if (providerPath == null || providerPath.isEmpty()) return null;
|
||||
String[] parts = providerPath.split("/");
|
||||
|
||||
// 先在国产下找
|
||||
if (providersConfig != null && providersConfig.has("国产")) {
|
||||
String url = findUrlInObject(providersConfig.getAsJsonObject("国产"), parts);
|
||||
if (url != null) return url;
|
||||
}
|
||||
// 再在非国产下找
|
||||
if (providersConfig != null && providersConfig.has("非国产")) {
|
||||
String url = findUrlInObject(providersConfig.getAsJsonObject("非国产"), parts);
|
||||
if (url != null) return url;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String findUrlInObject(JsonObject obj, String[] parts) {
|
||||
JsonElement el = obj.get(parts[0].trim());
|
||||
if (el == null) return null;
|
||||
if (parts.length == 1) {
|
||||
return el.isJsonPrimitive() ? el.getAsString() : null;
|
||||
}
|
||||
if (el.isJsonObject()) {
|
||||
String[] rest = Arrays.copyOfRange(parts, 1, parts.length);
|
||||
return findUrlInObject(el.getAsJsonObject(), rest);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== 模型名解析 ====================
|
||||
|
||||
/**
|
||||
* 将简称解析为模型全名
|
||||
*/
|
||||
public String resolveModelAlias(String alias) {
|
||||
if (aliasToFullModel.containsKey(alias)) {
|
||||
return aliasToFullModel.get(alias);
|
||||
}
|
||||
// 如果不是简称,尝试直接作为模型全名
|
||||
if (modelEndpoints.containsKey(alias)) {
|
||||
return alias;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用模型简称列表
|
||||
*/
|
||||
public List<String> getAvailableModels() {
|
||||
return new ArrayList<>(aliasToFullModel.keySet());
|
||||
}
|
||||
|
||||
// ==================== HTTP 请求 ====================
|
||||
|
||||
/**
|
||||
* 向指定模型发送请求(异步)
|
||||
* @param modelAlias 模型简称
|
||||
* @param prompt 用户输入
|
||||
* @param player 发起请求的玩家(用于私聊回复)
|
||||
* @param broadcast 是否广播回复(自动互动时为 true)
|
||||
*/
|
||||
public void sendToModel(String modelAlias, String prompt, Player player, boolean broadcast) {
|
||||
// 黑名单检查
|
||||
if (isBlacklisted(prompt)) {
|
||||
plugin.getLogger().info("[AI聊天] 黑名单拦截: " + prompt.substring(0, Math.min(50, prompt.length())));
|
||||
if (player != null) {
|
||||
player.sendMessage("§c[AI聊天] §f输入内容命中黑名单,请求已拦截");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 频率限制(同一玩家10秒内只能请求一次)
|
||||
if (player != null) {
|
||||
Long last = lastRequestTime.get(player.getName());
|
||||
if (last != null && System.currentTimeMillis() - last < 10000) {
|
||||
player.sendMessage("§c[AI聊天] §f请等待10秒后再提问");
|
||||
return;
|
||||
}
|
||||
lastRequestTime.put(player.getName(), System.currentTimeMillis());
|
||||
}
|
||||
|
||||
// 解析模型名
|
||||
String modelFull = resolveModelAlias(modelAlias);
|
||||
if (modelFull == null) {
|
||||
if (player != null) {
|
||||
player.sendMessage("§c[AI聊天] §f未知模型: " + modelAlias);
|
||||
List<String> models = getAvailableModels();
|
||||
if (!models.isEmpty()) {
|
||||
player.sendMessage("§7可用模型: " + String.join(", ", models));
|
||||
} else {
|
||||
player.sendMessage("§7未配置模型,请先在 openai/models.json 和 openai/提示词.txt 中配置");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ModelEndpoint endpoint = modelEndpoints.get(modelFull);
|
||||
if (endpoint == null) {
|
||||
if (player != null) {
|
||||
player.sendMessage("§c[AI聊天] §f模型配置不存在: " + modelFull);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建请求体
|
||||
String systemPrompt = buildSystemPrompt();
|
||||
String jsonBody = buildChatRequest(modelFull, systemPrompt, prompt, endpoint.maxToken);
|
||||
|
||||
// 异步发送请求
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(endpoint.baseUrl + "chat/completions"))
|
||||
.header("Content-Type", "application/json; charset=utf-8")
|
||||
.header("Authorization", "Bearer " + endpoint.apiKey)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
|
||||
.timeout(Duration.ofSeconds(60))
|
||||
.build();
|
||||
|
||||
return httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[AI聊天] 请求异常: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}).thenAccept(response -> {
|
||||
if (response == null) {
|
||||
handleRequestFailure(prompt, "请求异常: 网络错误");
|
||||
if (player != null) {
|
||||
player.sendMessage("§c[AI聊天] §f请求失败,请稍后重试");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
String errBody = response.body();
|
||||
handleRequestFailure(prompt, "HTTP " + response.statusCode() + ": " + errBody);
|
||||
if (player != null) {
|
||||
player.sendMessage("§c[AI聊天] §f请求失败: HTTP " + response.statusCode());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
String reply = parseChatResponse(response.body());
|
||||
if (reply == null) {
|
||||
handleRequestFailure(prompt, "解析失败: " + response.body());
|
||||
if (player != null) {
|
||||
player.sendMessage("§c[AI聊天] §f响应解析失败");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送回复
|
||||
if (broadcast) {
|
||||
Bukkit.broadcastMessage("§d§l[AI] §f" + reply);
|
||||
} else if (player != null) {
|
||||
player.sendMessage("§d§l[AI] §f" + reply);
|
||||
}
|
||||
|
||||
// 记录留档
|
||||
recordIssue(prompt, reply);
|
||||
lastAiReplyTime = System.currentTimeMillis();
|
||||
});
|
||||
}
|
||||
|
||||
private String buildSystemPrompt() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (!serverName.isEmpty()) {
|
||||
sb.append("你在一个名为\"").append(serverName).append("\"的Minecraft服务器中。\n");
|
||||
}
|
||||
if (!assistantName.isEmpty()) {
|
||||
sb.append("你是服务器助理\"").append(assistantName).append("\"。\n");
|
||||
}
|
||||
sb.append("你需要用简洁友好的语气回答玩家的问题。\n");
|
||||
if (!writingStyle.isEmpty()) {
|
||||
sb.append("写法要求: ").append(writingStyle).append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String buildChatRequest(String model, String systemPrompt, String userPrompt, String maxToken) {
|
||||
Gson gson = new Gson();
|
||||
JsonObject body = new JsonObject();
|
||||
body.addProperty("model", model);
|
||||
|
||||
JsonArray messages = new JsonArray();
|
||||
JsonObject sysMsg = new JsonObject();
|
||||
sysMsg.addProperty("role", "system");
|
||||
sysMsg.addProperty("content", systemPrompt);
|
||||
messages.add(sysMsg);
|
||||
|
||||
JsonObject userMsg = new JsonObject();
|
||||
userMsg.addProperty("role", "user");
|
||||
userMsg.addProperty("content", userPrompt);
|
||||
messages.add(userMsg);
|
||||
|
||||
body.add("messages", messages);
|
||||
|
||||
// max_tokens
|
||||
try {
|
||||
int mt = Integer.parseInt(maxToken);
|
||||
if (mt > 0) {
|
||||
body.addProperty("max_tokens", mt);
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
|
||||
return gson.toJson(body);
|
||||
}
|
||||
|
||||
private String parseChatResponse(String responseBody) {
|
||||
try {
|
||||
JsonObject resp = JsonParser.parseString(responseBody).getAsJsonObject();
|
||||
if (resp.has("choices")) {
|
||||
JsonArray choices = resp.getAsJsonArray("choices");
|
||||
if (choices.size() > 0) {
|
||||
JsonObject first = choices.get(0).getAsJsonObject();
|
||||
if (first.has("message")) {
|
||||
JsonObject message = first.getAsJsonObject("message");
|
||||
if (message.has("content")) {
|
||||
return message.get("content").getAsString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRequestFailure(String prompt, String errorDetail) {
|
||||
plugin.getLogger().warning("[AI聊天] 请求失败: " + errorDetail);
|
||||
recordIssue(prompt, "[请求失败] " + errorDetail);
|
||||
}
|
||||
|
||||
// ==================== 聊天处理 ====================
|
||||
|
||||
/**
|
||||
* 处理玩家聊天消息
|
||||
* 检测 @//模型名/ 前缀触发直接提问
|
||||
*/
|
||||
public void handlePlayerChat(Player p, String message) {
|
||||
// 记录到聊天缓冲区
|
||||
String chatLine = p.getDisplayName() + ": " + message;
|
||||
recentChatBuffer.add(chatLine);
|
||||
if (recentChatBuffer.size() > MAX_BUFFER) {
|
||||
recentChatBuffer.remove(0);
|
||||
}
|
||||
|
||||
// 检测 @//模型名/ 前缀
|
||||
if (message.startsWith("@//")) {
|
||||
// 格式: @//模型名/内容
|
||||
int slashIdx = message.indexOf('/', 3);
|
||||
if (slashIdx > 0) {
|
||||
String modelAlias = message.substring(3, slashIdx);
|
||||
String prompt = message.substring(slashIdx + 1);
|
||||
if (prompt.isEmpty()) {
|
||||
p.sendMessage("§c[AI聊天] §f格式错误,应为 @//模型名/内容");
|
||||
return;
|
||||
}
|
||||
sendToModel(modelAlias, prompt, p, false);
|
||||
} else {
|
||||
p.sendMessage("§c[AI聊天] §f格式错误,应为 @//模型名/内容");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 空闲调度器 ====================
|
||||
|
||||
public void startAutoChatScheduler() {
|
||||
if (autoChatTask != null) {
|
||||
autoChatTask.cancel();
|
||||
}
|
||||
// 每1秒检查一次
|
||||
autoChatTask = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, () -> {
|
||||
try {
|
||||
checkAutoChat();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[AI聊天] 自动互动异常: " + e.getMessage());
|
||||
}
|
||||
}, 20L, 20L);
|
||||
plugin.getLogger().info("[AI聊天] 空闲自动互动调度器已启动");
|
||||
}
|
||||
|
||||
public void stopAutoChatScheduler() {
|
||||
if (autoChatTask != null) {
|
||||
autoChatTask.cancel();
|
||||
autoChatTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAutoChat() {
|
||||
if (!autoChatRunning.compareAndSet(false, true)) return;
|
||||
|
||||
try {
|
||||
// 检查是否到了自动互动时间
|
||||
long intervalMs = (minIntervalMin + (int)(Math.random() * (maxIntervalMin - minIntervalMin + 1))) * 60000L;
|
||||
|
||||
if (System.currentTimeMillis() - lastAiReplyTime < intervalMs) return;
|
||||
if (recentChatBuffer.isEmpty()) return;
|
||||
if (defaultModelFull.isEmpty()) return;
|
||||
|
||||
ModelEndpoint endpoint = modelEndpoints.get(defaultModelFull);
|
||||
if (endpoint == null) return;
|
||||
|
||||
// 构建自动互动提示词
|
||||
String recentChat = String.join("\n", recentChatBuffer);
|
||||
String prompt = "以下是服务器最近的聊天记录:\n" + recentChat + "\n\n请根据聊天内容,自然地参与对话或回答问题。";
|
||||
|
||||
plugin.getLogger().info("[AI聊天] 触发自动互动");
|
||||
|
||||
// 异步发送
|
||||
String systemPrompt = buildSystemPrompt();
|
||||
String jsonBody = buildChatRequest(defaultModelFull, systemPrompt, prompt, endpoint.maxToken);
|
||||
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(endpoint.baseUrl + "chat/completions"))
|
||||
.header("Content-Type", "application/json; charset=utf-8")
|
||||
.header("Authorization", "Bearer " + endpoint.apiKey)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
|
||||
.timeout(Duration.ofSeconds(60))
|
||||
.build();
|
||||
|
||||
return httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[AI聊天] 自动互动请求异常: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}).thenAccept(response -> {
|
||||
if (response == null || response.statusCode() != 200) {
|
||||
handleRequestFailure("[自动] " + prompt.substring(0, Math.min(100, prompt.length())),
|
||||
"自动互动失败: HTTP " + (response != null ? response.statusCode() : "N/A"));
|
||||
return;
|
||||
}
|
||||
String reply = parseChatResponse(response.body());
|
||||
if (reply != null) {
|
||||
Bukkit.broadcastMessage("§d§l[AI] §f" + reply);
|
||||
recordIssue("[自动] " + prompt.substring(0, Math.min(100, prompt.length())), reply);
|
||||
lastAiReplyTime = System.currentTimeMillis();
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
autoChatRunning.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 黑名单 ====================
|
||||
|
||||
public boolean isBlacklisted(String content) {
|
||||
if (content == null || content.isEmpty()) return false;
|
||||
for (String kw : blacklist) {
|
||||
if (kw != null && !kw.isEmpty() && content.contains(kw)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ==================== 对话留档 ====================
|
||||
|
||||
private void recordIssue(String question, String answer) {
|
||||
if (issueFile == null) return;
|
||||
try {
|
||||
issueRecord.addProperty(question, answer);
|
||||
// 原子写入
|
||||
File tmp = new File(issueFile.getParent(), "issue.tmp.json");
|
||||
Files.write(tmp.toPath(), issueRecord.toString().getBytes(StandardCharsets.UTF_8));
|
||||
Files.move(tmp.toPath(), issueFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[AI聊天] 对话留档失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
private String getStr(JsonObject obj, String key) {
|
||||
if (obj == null || !obj.has(key)) return "";
|
||||
JsonElement el = obj.get(key);
|
||||
if (el == null || el.isJsonNull()) return "";
|
||||
return el.isJsonPrimitive() ? el.getAsString() : "";
|
||||
}
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
public void onEnable() {
|
||||
startAutoChatScheduler();
|
||||
}
|
||||
|
||||
public void onDisable() {
|
||||
stopAutoChatScheduler();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,588 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import java.sql.Statement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* ★ 第58批: 信誉积分管理系统
|
||||
*
|
||||
* 核心逻辑:
|
||||
* - 每人初始化100积分
|
||||
* - /credits 上报好人好事/举报
|
||||
* - PHP 端可提交线索
|
||||
* - 管理员审批 → 加分/扣分
|
||||
* - 低于阈值 → 行为限制(可配置规则)
|
||||
*/
|
||||
public class CreditManager {
|
||||
|
||||
private final Main plugin;
|
||||
private final DatabaseManager db;
|
||||
|
||||
// 默认初始积分
|
||||
public static final int DEFAULT_INIT_SCORE = 100;
|
||||
|
||||
// 审批状态
|
||||
public static final String STATUS_PENDING = "pending";
|
||||
public static final String STATUS_APPROVED = "approved";
|
||||
public static final String STATUS_REJECTED = "rejected";
|
||||
|
||||
// 上报类型
|
||||
public static final String TYPE_GOOD = "good"; // 好人好事
|
||||
public static final String TYPE_BAD = "bad"; // 举报
|
||||
|
||||
// 玩家积分缓存 (playerName -> score)
|
||||
private final Map<String, Integer> scoreCache = new ConcurrentHashMap<>();
|
||||
|
||||
public CreditManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.db = plugin.getDb();
|
||||
ensureTables();
|
||||
}
|
||||
|
||||
// ==================== DB 初始化 ====================
|
||||
|
||||
private void ensureTables() {
|
||||
try {
|
||||
Statement st = db.getConnection().createStatement();
|
||||
|
||||
// 信誉积分记录表
|
||||
st.execute("CREATE TABLE IF NOT EXISTS credit_records ("
|
||||
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
+ "target_player TEXT NOT NULL," // 被评价的玩家
|
||||
+ "reporter_player TEXT NOT NULL," // 上报人
|
||||
+ "type TEXT NOT NULL," // good / bad
|
||||
+ "content TEXT DEFAULT ''," // 详细内容
|
||||
+ "status TEXT DEFAULT 'pending'," // pending / approved / rejected
|
||||
+ "score_change INTEGER DEFAULT 0," // 审批后的积分变动
|
||||
+ "created_at INTEGER DEFAULT 0," // 上报时间
|
||||
+ "approved_at INTEGER DEFAULT 0," // 审批时间
|
||||
+ "approve_reason TEXT DEFAULT ''," // 审批理由
|
||||
+ "source TEXT DEFAULT 'game'," // game / php
|
||||
+ "FOREIGN KEY(target_player) REFERENCES users(player_name)"
|
||||
+ ")");
|
||||
|
||||
// 信誉限制规则表
|
||||
st.execute("CREATE TABLE IF NOT EXISTS credit_rules ("
|
||||
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
+ "rule_name TEXT NOT NULL UNIQUE," // 规则名
|
||||
+ "min_score INTEGER DEFAULT 0," // 低于此分触发
|
||||
+ "max_score INTEGER DEFAULT 0," // 高于此分解除(0=无上限)
|
||||
+ "restriction_type TEXT DEFAULT ''," // 限制类型
|
||||
+ "restriction_duration INTEGER DEFAULT 0," // 持续秒数(0=直到加分)
|
||||
+ "enabled INTEGER DEFAULT 1," // 1=启用 0=禁用
|
||||
+ "description TEXT DEFAULT ''" // 说明
|
||||
+ ")");
|
||||
|
||||
// 默认规则初始化
|
||||
initDefaultRules();
|
||||
|
||||
// users 表添加 credit_score 列
|
||||
try {
|
||||
st.executeUpdate("ALTER TABLE users ADD COLUMN credit_score INTEGER DEFAULT " + DEFAULT_INIT_SCORE);
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[信誉积分] 建表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void initDefaultRules() {
|
||||
try {
|
||||
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) "
|
||||
+ "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) "
|
||||
+ "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) "
|
||||
+ "VALUES ('禁止传送', 20, 0, 'teleport', 0, '低于20分时禁止使用传送功能')");
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// ==================== 积分查询 ====================
|
||||
|
||||
public int getScore(String playerName) {
|
||||
if (scoreCache.containsKey(playerName)) {
|
||||
return scoreCache.get(playerName);
|
||||
}
|
||||
int score = DEFAULT_INIT_SCORE;
|
||||
try {
|
||||
PreparedStatement ps = db.getConnection().prepareStatement(
|
||||
"SELECT credit_score FROM users WHERE player_name = ?");
|
||||
ps.setString(1, playerName);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (rs.next()) {
|
||||
score = rs.getInt("credit_score");
|
||||
}
|
||||
ps.close();
|
||||
} catch (Exception ignored) {}
|
||||
scoreCache.put(playerName, score);
|
||||
return score;
|
||||
}
|
||||
|
||||
// ==================== 积分修改 ====================
|
||||
|
||||
public void setScore(String playerName, int score) {
|
||||
try {
|
||||
PreparedStatement ps = db.getConnection().prepareStatement(
|
||||
"UPDATE users SET credit_score = ? WHERE player_name = ?");
|
||||
ps.setInt(1, score);
|
||||
ps.setString(2, playerName);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[信誉积分] 设置积分失败: " + e.getMessage());
|
||||
}
|
||||
scoreCache.put(playerName, score);
|
||||
checkRestrictions(playerName, score);
|
||||
}
|
||||
|
||||
public void addScore(String playerName, int amount, String reason) {
|
||||
int current = getScore(playerName);
|
||||
setScore(playerName, current + amount);
|
||||
plugin.getLogger().info("[信誉积分] " + playerName + " 积分变动: "
|
||||
+ (amount > 0 ? "+" : "") + amount + " (" + reason + ")");
|
||||
}
|
||||
|
||||
// ==================== 上报 ====================
|
||||
|
||||
public void submitReport(String targetPlayer, String reporterPlayer, String type, String content, String source) {
|
||||
try {
|
||||
PreparedStatement ps = db.getConnection().prepareStatement(
|
||||
"INSERT INTO credit_records (target_player, reporter_player, type, content, status, created_at, source) "
|
||||
+ "VALUES (?, ?, ?, ?, 'pending', ?, ?)");
|
||||
ps.setString(1, targetPlayer);
|
||||
ps.setString(2, reporterPlayer);
|
||||
ps.setString(3, type);
|
||||
ps.setString(4, content);
|
||||
ps.setLong(5, System.currentTimeMillis());
|
||||
ps.setString(6, source);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
plugin.getLogger().info("[信誉积分] 上报: " + reporterPlayer + " 举报/表扬 " + targetPlayer
|
||||
+ " 类型=" + type + " 来源=" + source);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[信誉积分] 上报失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 审批 ====================
|
||||
|
||||
public boolean approveRecord(int recordId, boolean approved, String reason, int scoreChange, String approverName) {
|
||||
try {
|
||||
PreparedStatement ps = db.getConnection().prepareStatement(
|
||||
"UPDATE credit_records SET status = ?, score_change = ?, approved_at = ?, approve_reason = ? WHERE id = ?");
|
||||
ps.setString(1, approved ? STATUS_APPROVED : STATUS_REJECTED);
|
||||
ps.setInt(2, approved ? scoreChange : 0);
|
||||
ps.setLong(3, System.currentTimeMillis());
|
||||
ps.setString(4, reason);
|
||||
ps.setInt(5, recordId);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
|
||||
if (approved && scoreChange != 0) {
|
||||
// 查询目标玩家
|
||||
PreparedStatement ps2 = db.getConnection().prepareStatement(
|
||||
"SELECT target_player FROM credit_records WHERE id = ?");
|
||||
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;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[信誉积分] 审批失败: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 查询 ====================
|
||||
|
||||
public List<Map<String, Object>> getPendingRecords() {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps = db.getConnection().prepareStatement(
|
||||
"SELECT * FROM credit_records WHERE status = 'pending' ORDER BY created_at DESC");
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("id", rs.getInt("id"));
|
||||
row.put("target_player", rs.getString("target_player"));
|
||||
row.put("reporter_player", rs.getString("reporter_player"));
|
||||
row.put("type", rs.getString("type"));
|
||||
row.put("content", rs.getString("content"));
|
||||
row.put("created_at", rs.getLong("created_at"));
|
||||
row.put("source", rs.getString("source"));
|
||||
list.add(row);
|
||||
}
|
||||
ps.close();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[信誉积分] 查询待审批失败: " + e.getMessage());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getAllRecords(int limit) {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps = db.getConnection().prepareStatement(
|
||||
"SELECT * FROM credit_records ORDER BY created_at DESC LIMIT ?");
|
||||
ps.setInt(1, limit);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("id", rs.getInt("id"));
|
||||
row.put("target_player", rs.getString("target_player"));
|
||||
row.put("reporter_player", rs.getString("reporter_player"));
|
||||
row.put("type", rs.getString("type"));
|
||||
row.put("content", rs.getString("content"));
|
||||
row.put("status", rs.getString("status"));
|
||||
row.put("score_change", rs.getInt("score_change"));
|
||||
row.put("created_at", rs.getLong("created_at"));
|
||||
row.put("source", rs.getString("source"));
|
||||
list.add(row);
|
||||
}
|
||||
ps.close();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[信誉积分] 查询记录失败: " + e.getMessage());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
// ==================== 限制规则 ====================
|
||||
|
||||
public List<Map<String, Object>> getEnabledRules() {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps = db.getConnection().prepareStatement(
|
||||
"SELECT * FROM credit_rules WHERE enabled = 1");
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("id", rs.getInt("id"));
|
||||
row.put("rule_name", rs.getString("rule_name"));
|
||||
row.put("min_score", rs.getInt("min_score"));
|
||||
row.put("max_score", rs.getInt("max_score"));
|
||||
row.put("restriction_type", rs.getString("restriction_type"));
|
||||
row.put("restriction_duration", rs.getInt("restriction_duration"));
|
||||
row.put("description", rs.getString("description"));
|
||||
list.add(row);
|
||||
}
|
||||
ps.close();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[信誉积分] 查询规则失败: " + e.getMessage());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public boolean isRestricted(String playerName, String restrictionType) {
|
||||
int score = getScore(playerName);
|
||||
for (Map<String, Object> rule : getEnabledRules()) {
|
||||
int minScore = ((Number) rule.get("min_score")).intValue();
|
||||
String type = (String) rule.get("restriction_type");
|
||||
if (score < minScore && type.equals(restrictionType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void checkRestrictions(String playerName, int score) {
|
||||
// 检查是否触发限制,通知玩家
|
||||
for (Map<String, Object> rule : getEnabledRules()) {
|
||||
int minScore = ((Number) rule.get("min_score")).intValue();
|
||||
int maxScore = ((Number) rule.get("max_score")).intValue();
|
||||
String type = (String) rule.get("restriction_type");
|
||||
String desc = (String) rule.get("description");
|
||||
|
||||
Player p = Bukkit.getPlayerExact(playerName);
|
||||
if (p == null || !p.isOnline()) continue;
|
||||
|
||||
if (score < minScore && (maxScore == 0 || score <= maxScore)) {
|
||||
p.sendMessage("§c§l[信誉积分] §f你的积分 " + score + " 低于阈值 " + minScore + ","
|
||||
+ "已触发限制: " + desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== GUI ====================
|
||||
|
||||
public Inventory createPendingGUI(Player p) {
|
||||
Inventory inv = Bukkit.createInventory(null, 54, "§d§l信誉积分 - 待审批");
|
||||
fillBg(inv);
|
||||
List<Map<String, Object>> records = getPendingRecords();
|
||||
for (int i = 0; i < records.size() && i < 49; i++) {
|
||||
Map<String, Object> r = records.get(i);
|
||||
String type = (String) r.get("type");
|
||||
Material mat = TYPE_GOOD.equals(type) ? Material.EMERALD : Material.REDSTONE;
|
||||
String typeText = TYPE_GOOD.equals(type) ? "§a好人好事" : "§c举报";
|
||||
inv.setItem(i, mkItem(mat,
|
||||
"§e#" + r.get("id") + " " + r.get("target_player"),
|
||||
"§7上报人: " + r.get("reporter_player"),
|
||||
"§7类型: " + typeText,
|
||||
"§7内容: " + r.get("content"),
|
||||
"§7来源: " + r.get("source"),
|
||||
"",
|
||||
"§e点击审批"));
|
||||
}
|
||||
inv.setItem(53, mkItem(Material.ARROW, "§7返回"));
|
||||
return inv;
|
||||
}
|
||||
|
||||
public Inventory createRecordsGUI(Player p) {
|
||||
Inventory inv = Bukkit.createInventory(null, 54, "§d§l信誉积分 - 历史记录");
|
||||
fillBg(inv);
|
||||
List<Map<String, Object>> records = getAllRecords(49);
|
||||
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("target_player"),
|
||||
"§7上报人: " + r.get("reporter_player"),
|
||||
"§7类型: " + r.get("type"),
|
||||
"§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 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) {
|
||||
if (args.length == 0) {
|
||||
// 无参数:显示玩家自己的积分
|
||||
if (sender instanceof Player) {
|
||||
String name = ((Player) sender).getName();
|
||||
int score = getScore(name);
|
||||
sender.sendMessage("§d§l[信誉积分] §f你的当前积分: §e" + score + " §7(初始: " + DEFAULT_INIT_SCORE + ")");
|
||||
} else {
|
||||
sender.sendMessage("§c用法: /credits [上报|审批|记录|规则] [参数]");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
String sub = args[0].toLowerCase();
|
||||
|
||||
switch (sub) {
|
||||
case "上报":
|
||||
case "report":
|
||||
// /credits report <目标玩家> <good|bad> <内容>
|
||||
if (args.length < 4) {
|
||||
sender.sendMessage("§c用法: /credits report <目标玩家> <good|bad> <内容>");
|
||||
return true;
|
||||
}
|
||||
if (!(sender instanceof Player)) {
|
||||
sender.sendMessage("§c此命令只能玩家使用");
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
|
||||
case "审批":
|
||||
case "approve":
|
||||
// 管理员审批
|
||||
if (!(sender.hasPermission("sdf1_login.admin"))) {
|
||||
sender.sendMessage("§c权限不足");
|
||||
return true;
|
||||
}
|
||||
// /credits approve <id> <通过|驳回> <理由> [积分变动]
|
||||
if (args.length < 4) {
|
||||
// 无 id 参数 → 打开 GUI
|
||||
if (sender instanceof Player) {
|
||||
((Player) sender).openInventory(createPendingGUI((Player) sender));
|
||||
} else {
|
||||
List<Map<String, Object>> records = getPendingRecords();
|
||||
if (records.isEmpty()) {
|
||||
sender.sendMessage("§a没有待审批的记录");
|
||||
return true;
|
||||
}
|
||||
for (Map<String, Object> r : records) {
|
||||
sender.sendMessage("§e#" + r.get("id") + " " + r.get("target_player")
|
||||
+ " §7(" + r.get("type") + ") " + r.get("content"));
|
||||
}
|
||||
sender.sendMessage("§7用法: /credits approve <id> <通过|驳回> <理由> [积分变动]");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// 有 id 参数 → CLI 审批
|
||||
try {
|
||||
int recordId = Integer.parseInt(args[1]);
|
||||
String decision = args[2].toLowerCase();
|
||||
boolean approved;
|
||||
if (decision.equals("通过") || decision.equals("approve") || decision.equals("approved")) {
|
||||
approved = true;
|
||||
} else if (decision.equals("驳回") || decision.equals("reject") || decision.equals("rejected")) {
|
||||
approved = false;
|
||||
} else {
|
||||
sender.sendMessage("§c决定必须是「通过」或「驳回」");
|
||||
return true;
|
||||
}
|
||||
StringBuilder reason = new StringBuilder();
|
||||
for (int i = 3; i < args.length; i++) {
|
||||
if (i > 3) reason.append(" ");
|
||||
reason.append(args[i]);
|
||||
}
|
||||
// 解析积分变动(最后一个参数如果是数字)
|
||||
int scoreChange = 0;
|
||||
String reasonStr = reason.toString();
|
||||
// 检查是否以数字结尾(积分变动在最后)
|
||||
java.util.regex.Matcher m = java.util.regex.Pattern.compile("(\\d+)$").matcher(reasonStr);
|
||||
if (approved && m.find()) {
|
||||
scoreChange = Integer.parseInt(m.group(1));
|
||||
reasonStr = reasonStr.substring(0, reasonStr.length() - m.group(1).length()).trim();
|
||||
if (reasonStr.isEmpty()) reasonStr = "信誉积分审批";
|
||||
} else if (approved) {
|
||||
// 默认好人好事+10,举报+10
|
||||
scoreChange = 10;
|
||||
}
|
||||
String approver = sender instanceof Player ? ((Player) sender).getName() : "控制台";
|
||||
boolean ok = approveRecord(recordId, approved, reasonStr, scoreChange, approver);
|
||||
if (ok) {
|
||||
sender.sendMessage("§a审批完成: #" + recordId + " " + (approved ? "通过" : "驳回"));
|
||||
} else {
|
||||
sender.sendMessage("§c审批失败,记录不存在或已处理");
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
sender.sendMessage("§c记录ID必须是数字");
|
||||
}
|
||||
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:
|
||||
sender.sendMessage("§c未知子命令: " + sub);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== GUI 点击处理 ====================
|
||||
|
||||
public void handleClick(Player p, int slot, String title) {
|
||||
if (slot == 53) {
|
||||
p.closeInventory();
|
||||
return;
|
||||
}
|
||||
|
||||
if (title != null && title.contains("待审批")) {
|
||||
p.sendMessage("§c请使用 /credits approve <id> <通过|驳回> <理由> [积分变动]");
|
||||
p.closeInventory();
|
||||
} else if (title != null && title.contains("历史记录")) {
|
||||
p.sendMessage("§7此GUI仅展示,请用命令管理");
|
||||
} else if (title != null && title.contains("限制规则")) {
|
||||
p.sendMessage("§7规则阈值低于当前积分则触发限制");
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== GUI 工具 ====================
|
||||
|
||||
private void fillBg(Inventory inv) {
|
||||
ItemStack gl = mkItem(Material.GRAY_STAINED_GLASS_PANE, " ");
|
||||
for (int i = 0; i < inv.getSize(); i++) inv.setItem(i, gl);
|
||||
}
|
||||
|
||||
private ItemStack mkItem(Material mat, String name, String... lore) {
|
||||
ItemStack it = new ItemStack(mat);
|
||||
ItemMeta im = it.getItemMeta();
|
||||
if (im != null) {
|
||||
im.setDisplayName(name);
|
||||
if (lore.length > 0) im.setLore(Arrays.asList(lore));
|
||||
it.setItemMeta(im);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
// ==================== PHP 端提交线索 ====================
|
||||
|
||||
/**
|
||||
* 供 PHP 端调用的提交线索接口(通过 WebManager 对账或独立 API)
|
||||
*/
|
||||
public boolean submitFromPhp(String targetPlayer, String reporterPlayer, String type, String content) {
|
||||
submitReport(targetPlayer, reporterPlayer, type, content, "php");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 供 PHP 端调用的审批接口
|
||||
*/
|
||||
public boolean approveFromPhp(int recordId, boolean approved, String reason, int scoreChange, String approverName) {
|
||||
return approveRecord(recordId, approved, reason, scoreChange, approverName);
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,8 @@ public class Main extends JavaPlugin
|
||||
private CheckInManager checkIn;
|
||||
private InviteManager invite;
|
||||
private PointsManager points;
|
||||
private CreditManager creditManager;
|
||||
private AIChatManager aiChatManager;
|
||||
public GUIManager gui;
|
||||
private ChatInputManager chatInput;
|
||||
private VerificationManager verification;
|
||||
@@ -319,6 +321,10 @@ public class Main extends JavaPlugin
|
||||
return points;
|
||||
}
|
||||
|
||||
public CreditManager getCreditManager() {
|
||||
return creditManager;
|
||||
}
|
||||
|
||||
public GUIManager getGui() {
|
||||
return gui;
|
||||
}
|
||||
@@ -482,6 +488,9 @@ public class Main extends JavaPlugin
|
||||
checkIn = new CheckInManager(this);
|
||||
invite = new InviteManager(this);
|
||||
points = new PointsManager(this);
|
||||
creditManager = new CreditManager(this);
|
||||
aiChatManager = new AIChatManager(this);
|
||||
aiChatManager.onEnable();
|
||||
chatInput = new ChatInputManager();
|
||||
verification = new VerificationManager(this);
|
||||
riskControl = new RiskControlManager(this);
|
||||
@@ -1053,6 +1062,7 @@ public class Main extends JavaPlugin
|
||||
}
|
||||
if (db != null) db.close();
|
||||
if (garbage != null) garbage.close();
|
||||
if (aiChatManager != null) aiChatManager.onDisable();
|
||||
if (chatTransPool != null) {
|
||||
chatTransPool.shutdownNow();
|
||||
}
|
||||
@@ -6180,6 +6190,11 @@ public class Main extends JavaPlugin
|
||||
chatFilter.markPlayerActive(p.getName());
|
||||
}
|
||||
|
||||
// ★ 第58批:AI聊天介入 — @//模型名/ 前缀触发直接提问
|
||||
if (aiChatManager != null) {
|
||||
aiChatManager.handlePlayerChat(p, msg);
|
||||
}
|
||||
|
||||
// ★ 先探一次禁言状态:若刚好到期,isMuted() 内部会立即解禁并打上
|
||||
// 「需重新人机验证」标记(2026-10-08:解禁≠解除人机验证)
|
||||
if (chatFilter != null) {
|
||||
@@ -7162,6 +7177,15 @@ public class Main extends JavaPlugin
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== 信誉积分 GUI 点击处理 =====
|
||||
if (title != null && title.contains("信誉积分")) {
|
||||
if (creditManager != null) {
|
||||
e.setCancelled(true);
|
||||
creditManager.handleClick(p, slot, title);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== 垃圾箱 =====
|
||||
// ===== 垃圾回收站 =====
|
||||
if (title.equals("§6§l垃圾回收站")) {
|
||||
@@ -9434,6 +9458,15 @@ public class Main extends JavaPlugin
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== /credits:信誉积分管理 =====
|
||||
if (cmdName.equals("credits")) {
|
||||
if (creditManager == null) {
|
||||
sender.sendMessage("§c信誉积分模块未初始化");
|
||||
return true;
|
||||
}
|
||||
return creditManager.handleCommand(sender, args);
|
||||
}
|
||||
|
||||
|
||||
// ★ 第30批:/addblack <玩家> [时长] [理由] —— 拉黑
|
||||
if (cmdName.equals("addblack")) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"对话留档":{
|
||||
"怎么写":"键是提问,值是回答",
|
||||
"谁来写":"每次提问/控场自动写",
|
||||
"请求失败":"响应原文落盘"
|
||||
},
|
||||
"问":"答",
|
||||
"自动":"答"
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"人设部分": {
|
||||
"服务器名字": "",
|
||||
"助理名字": "",
|
||||
"写法": "1行1个"
|
||||
},
|
||||
"配置部分": {
|
||||
"1": {
|
||||
"模型配置": {
|
||||
"内置供应商": {
|
||||
"供应商名字": "",
|
||||
"apikey": "",
|
||||
"max_token": "",
|
||||
"可用模型": [],
|
||||
"默认使用": ""
|
||||
},
|
||||
"自定义供应商": {
|
||||
"baseurl": "",
|
||||
"apikey": "",
|
||||
"max_token": "",
|
||||
"可用模型": [],
|
||||
"默认使用": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"默认上下文长度": "200000",
|
||||
"发言间隔": {
|
||||
"最短": "",
|
||||
"最长": "",
|
||||
"注释": "单位分钟"
|
||||
},
|
||||
"自动补齐": {
|
||||
"开关": true,
|
||||
"备注": "如果你的厂商"
|
||||
}
|
||||
}
|
||||
},
|
||||
"填写规范": {
|
||||
"自定义": {
|
||||
"baseurl": "填写厂家给你的地址,比如https://example.com/v1",
|
||||
"apikey": "填厂家开放平台的key,比如sk-example",
|
||||
"可用模型": "填厂家给的模型名字,比如example",
|
||||
"max_token": "上下文长度,不填则使用默认20万上下文长度"
|
||||
},
|
||||
"内置供应商": {
|
||||
"供应商名字": "一层一层填,比如mimo/按量计费,deepseek等",
|
||||
"apikey": "填厂家开放平台的key,比如sk-example",
|
||||
"可用模型": "填厂家给的模型名字,比如example",
|
||||
"max_token": "上下文长度,不填则使用默认20万上下文长度"
|
||||
}
|
||||
},
|
||||
"配置填写示例": {
|
||||
"自定义供应商": {
|
||||
"baseurl": "https://example.com/v1",
|
||||
"apikey": "sk-example",
|
||||
"max_token": "1000000",
|
||||
"可用模型": [
|
||||
"example"
|
||||
]
|
||||
},
|
||||
"内置供应商": {
|
||||
"供应商名字": "mimo/token-plan",
|
||||
"apikey": "sk-example",
|
||||
"可用模型": [
|
||||
"mimo-v2.6-flash",
|
||||
"mimo-v2.6-pro"
|
||||
]
|
||||
},
|
||||
"内置供应商1": {
|
||||
"供应商名字": "deepseek",
|
||||
"apikey": "sk-example",
|
||||
"可用模型": [
|
||||
"deepseek-flash",
|
||||
"deepseek-v4-pro"
|
||||
]
|
||||
},
|
||||
"内置供应商3": {
|
||||
"供应商名字": "讯飞/讯飞星辰maas/标准",
|
||||
"apikey": "sk-example",
|
||||
"可用模型": [
|
||||
"spakr-x2.6",
|
||||
"xopglm5"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"openai协议": "内置供应商",
|
||||
"添加方法": [
|
||||
"键值对格式,键是供应商名字,值是请求地址",
|
||||
"如果供应商有区分按量计费、token-plan则用一堆花括号即可。可参考mimo的写法",
|
||||
"如果要备注,就用花括号展开,比如向Agnes、opencode一样",
|
||||
"额外说明:本文件默认会从插件内置的服务器定期拉取更新。"
|
||||
],
|
||||
"国产": {
|
||||
"mimo": {
|
||||
"按量计费": "https://api.xiaomimimo.com/v1",
|
||||
"token-plan": "https://token-plan-cn.xiaomimimo.com/v1",
|
||||
"批量推理": "https://batch-api-cn.xiaomimimo.com/v1"
|
||||
},
|
||||
"deepseek": "https://api.deepseek.com",
|
||||
"kimi": "https://api.moonshot.cn/v1",
|
||||
"智谱GLM": "https://open.bigmodel.cn/api/paas/v4",
|
||||
"通义千问": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"百度AIStudio": "https://aistudio.baidu.com/llm/lmapi/v3",
|
||||
"MiniMax": "https://api.minimax.cn/v1",
|
||||
"阶跃星辰": {
|
||||
"标准": "https://api.stepfun.com/v1",
|
||||
"step-plan": "https://api.stepfun.com/step_plan/v1"
|
||||
},
|
||||
"零一万物": "https://api.lingyiwanwu.com/v1",
|
||||
"百川": "https://api.baichuan-ai.com/v1",
|
||||
"美团": "https://api.longcat.chat/openai",
|
||||
"抖音云(豆包/火山)": "https://ark.cn-beijing.volces.com/api/v3",
|
||||
"讯飞": {
|
||||
"讯飞星火": "https://spark-api-open.xf-yun.com/v1",
|
||||
"讯飞星辰MaaS": {
|
||||
"标准": "https://maas-api.cn-huabei-1.xf-yun.com/v2",
|
||||
"Coding Plan": "https://maas-coding-api.cn-huabei-1.xf-yun.com/v2",
|
||||
"Token Plan": "https://maas-token-api.cn-huabei-1.xf-yun.com/v2"
|
||||
}
|
||||
},
|
||||
"腾讯云": {
|
||||
"Token Plan": "https://api.lkeap.cloud.tencent.com/plan/v3",
|
||||
"Coding Plan": "https://api.lkeap.cloud.tencent.com/coding/v3",
|
||||
"TokenHub(广州)": "https://tokenhub.tencentmaas.com",
|
||||
"TokenHub(新加坡)": "https://tokenhub-intl.tencentmaas.com"
|
||||
},
|
||||
"硅基流动": "https://api.siliconflow.cn/v1"
|
||||
},
|
||||
"非国产": {
|
||||
"OpenAI": "https://api.openai.com/v1",
|
||||
"Claude(Anthropic)": "https://api.anthropic.com/v1/",
|
||||
"推特(xAI Grok)": "https://api.x.ai/v1",
|
||||
"Gemini": "https://generativelanguage.googleapis.com/v1beta/openai/",
|
||||
"英伟达": "https://integrate.api.nvidia.com/v1",
|
||||
"agnes": {
|
||||
"国际版": "https://apihub.agnes-ai.com/v1",
|
||||
"中国版": "https://apihub.agnes-ai.cn/v1",
|
||||
"说明": "中国版和国际版互通key,但账号、token-plan不互通"
|
||||
},
|
||||
"opencode": {
|
||||
"zen": "https://opencode.ai/zen/v1",
|
||||
"go": "https://opencode.ai/zen/go/v1",
|
||||
"说明": "opencode zen可以通过9router或其它工具反代opencode cli终端免费使用。但不推荐,额度特别低。作者实测:反代出来跑了50万~100万左右就429限流。"
|
||||
},
|
||||
"OpenRouter": "https://openrouter.ai/api/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# 此文件填写的模型将被正是运用请求
|
||||
# 以#开头的表示注释,会被忽略
|
||||
# models=name,这个=就是模型的简称,比如你可以配置deepseek=大肥鱼,后续用户@大肥鱼 xx,就会自动向deepseek的模型发起请求
|
||||
@@ -211,3 +211,6 @@ commands:
|
||||
json:
|
||||
description: JSON消息构造器(tellraw/大标题/小标题/actionbar+颜色,需OP以上权限)
|
||||
usage: /json
|
||||
credits:
|
||||
description: 信誉积分管理(上报/审批/记录/规则)
|
||||
usage: /credits [上报|审批|记录|规则] [参数]
|
||||
|
||||
Reference in New Issue
Block a user