diff --git a/.gitignore b/.gitignore index 4cec8b3..0986269 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/src/main/java/Sdf1_login/AIChatManager.java b/src/main/java/Sdf1_login/AIChatManager.java new file mode 100644 index 0000000..bbed3db --- /dev/null +++ b/src/main/java/Sdf1_login/AIChatManager.java @@ -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 aliasToFullModel = new HashMap<>(); // 简称 → 模型全名 + private final Map modelEndpoints = new HashMap<>(); // 模型全名 → 端点 + private final List 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 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 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 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 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 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 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(); + } +} diff --git a/src/main/java/Sdf1_login/CreditManager.java b/src/main/java/Sdf1_login/CreditManager.java new file mode 100644 index 0000000..c9673c0 --- /dev/null +++ b/src/main/java/Sdf1_login/CreditManager.java @@ -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 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> getPendingRecords() { + List> 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 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> getAllRecords(int limit) { + List> 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 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> getEnabledRules() { + List> list = new ArrayList<>(); + try { + PreparedStatement ps = db.getConnection().prepareStatement( + "SELECT * FROM credit_rules WHERE enabled = 1"); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map 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 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 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> records = getPendingRecords(); + for (int i = 0; i < records.size() && i < 49; i++) { + Map 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> records = getAllRecords(49); + for (int i = 0; i < records.size() && i < 49; i++) { + Map 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> rules = getEnabledRules(); + for (int i = 0; i < rules.size() && i < 49; i++) { + Map 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 <目标玩家> <内容> + if (args.length < 4) { + sender.sendMessage("§c用法: /credits report <目标玩家> <内容>"); + 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 <通过|驳回> <理由> [积分变动] + if (args.length < 4) { + // 无 id 参数 → 打开 GUI + if (sender instanceof Player) { + ((Player) sender).openInventory(createPendingGUI((Player) sender)); + } else { + List> records = getPendingRecords(); + if (records.isEmpty()) { + sender.sendMessage("§a没有待审批的记录"); + return true; + } + for (Map r : records) { + sender.sendMessage("§e#" + r.get("id") + " " + r.get("target_player") + + " §7(" + r.get("type") + ") " + r.get("content")); + } + sender.sendMessage("§7用法: /credits approve <通过|驳回> <理由> [积分变动]"); + } + 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> records = getAllRecords(20); + for (Map 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 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 <通过|驳回> <理由> [积分变动]"); + 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); + } +} diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java index 410f60b..106c7f1 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -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(); } @@ -6179,6 +6189,11 @@ public class Main extends JavaPlugin if (chatFilter != null) { chatFilter.markPlayerActive(p.getName()); } + + // ★ 第58批:AI聊天介入 — @//模型名/ 前缀触发直接提问 + if (aiChatManager != null) { + aiChatManager.handlePlayerChat(p, msg); + } // ★ 先探一次禁言状态:若刚好到期,isMuted() 内部会立即解禁并打上 // 「需重新人机验证」标记(2026-10-08:解禁≠解除人机验证) @@ -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")) { diff --git a/src/main/resources/openai/issue.json b/src/main/resources/openai/issue.json new file mode 100644 index 0000000..ad7027d --- /dev/null +++ b/src/main/resources/openai/issue.json @@ -0,0 +1,9 @@ +{ + "对话留档":{ + "怎么写":"键是提问,值是回答", + "谁来写":"每次提问/控场自动写", + "请求失败":"响应原文落盘" + }, + "问":"答", + "自动":"答" +} \ No newline at end of file diff --git a/src/main/resources/openai/models.json b/src/main/resources/openai/models.json new file mode 100644 index 0000000..1754b30 --- /dev/null +++ b/src/main/resources/openai/models.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/src/main/resources/openai/供应商.json b/src/main/resources/openai/供应商.json new file mode 100644 index 0000000..a7f73ea --- /dev/null +++ b/src/main/resources/openai/供应商.json @@ -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" + } +} \ No newline at end of file diff --git a/src/main/resources/openai/提示词.txt b/src/main/resources/openai/提示词.txt new file mode 100644 index 0000000..0ddfe4c --- /dev/null +++ b/src/main/resources/openai/提示词.txt @@ -0,0 +1,3 @@ +# 此文件填写的模型将被正是运用请求 +# 以#开头的表示注释,会被忽略 +# models=name,这个=就是模型的简称,比如你可以配置deepseek=大肥鱼,后续用户@大肥鱼 xx,就会自动向deepseek的模型发起请求 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index a172dba..8cc79a0 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -211,3 +211,6 @@ commands: json: description: JSON消息构造器(tellraw/大标题/小标题/actionbar+颜色,需OP以上权限) usage: /json + credits: + description: 信誉积分管理(上报/审批/记录/规则) + usage: /credits [上报|审批|记录|规则] [参数]