feat: /ia — chat question-and-answer backed by MiniMax
Adds an `ia` module: `/ia <pergunta>` sends the question to an OpenAI-compatible chat-completions endpoint (MiniMax by default) and posts the reply to chat. Access is gated by `canalhandia.ia`, declared `default: op` so the operator has it out of the box and LuckPerms can grant it to anyone else. The model can only ever produce chat text: - the reply goes to sendMessage and nowhere else — it is never passed to the command dispatcher; - no `tools`/`tool_choice` are sent, so there is nothing for the model to call; - the system prompt states it has no server, shell or command access; - replies are sanitised — colour codes stripped so they cannot forge server messages, newlines folded so one answer is one chat entry, and leading slashes removed so nothing reads as a command to run. The API key is deliberately not a config value, since config.yml is committed. It is read from MINIMAX_API_KEY or from plugins/Canalhandia/minimax.key, which is now gitignored. Cost is bounded by a per-player cooldown and a server-wide daily cap, both visible in /canalhandia status. The HTTP call runs off the main thread; only the delivery hops back onto it.
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
package dev.marcospaulo.canalhandia;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Question-and-answer in chat, backed by MiniMax (or any OpenAI-compatible
|
||||
* endpoint).
|
||||
*
|
||||
* <p><b>The model can only ever produce chat text.</b> The reply is passed to
|
||||
* {@code sendMessage} and nowhere else — it is never handed to the command
|
||||
* dispatcher, never written to disk, and no tool/function definitions are sent
|
||||
* in the request, so there is nothing for the model to call. A player asking it
|
||||
* to "run /op me" gets a string back, not an executed command. As a second
|
||||
* layer, {@link #sanitise} strips leading slashes so a reply cannot even be
|
||||
* mistaken for a command someone should paste.
|
||||
*
|
||||
* <p>The API key never lives in config.yml, because config.yml is committed to
|
||||
* git. It comes from the {@code MINIMAX_API_KEY} environment variable, or from
|
||||
* {@code plugins/Canalhandia/minimax.key}, which is gitignored.
|
||||
*/
|
||||
final class Ai {
|
||||
|
||||
private static final String KEY_FILE = "minimax.key";
|
||||
private static final String KEY_ENV = "MINIMAX_API_KEY";
|
||||
|
||||
private final Canalhandia plugin;
|
||||
private final HttpClient http;
|
||||
/** Per-player cooldown, so one person cannot spend the whole budget. */
|
||||
private final Map<UUID, Long> lastAsk = new HashMap<>();
|
||||
/** In-flight guard: one question per player at a time. */
|
||||
private final Map<UUID, Boolean> pending = new HashMap<>();
|
||||
|
||||
private LocalDate day = LocalDate.now();
|
||||
private int askedToday;
|
||||
|
||||
Ai(Canalhandia plugin) {
|
||||
this.plugin = plugin;
|
||||
this.http = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
}
|
||||
|
||||
/** True if a key is configured. Without one the module stays quiet. */
|
||||
boolean configured() {
|
||||
return apiKey() != null;
|
||||
}
|
||||
|
||||
private String apiKey() {
|
||||
String fromEnv = System.getenv(KEY_ENV);
|
||||
if (fromEnv != null && !fromEnv.isBlank()) {
|
||||
return fromEnv.trim();
|
||||
}
|
||||
Path file = plugin.getDataFolder().toPath().resolve(KEY_FILE);
|
||||
try {
|
||||
if (Files.isReadable(file)) {
|
||||
String key = Files.readString(file, StandardCharsets.UTF_8).trim();
|
||||
return key.isEmpty() ? null : key;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().warning("Não consegui ler " + KEY_FILE + ": " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the model on behalf of a player and delivers the answer to chat.
|
||||
*
|
||||
* <p>Returns immediately; the HTTP call runs off the main thread and the
|
||||
* reply is posted back on it.
|
||||
*/
|
||||
void ask(Player asker, String question) {
|
||||
Settings settings = plugin.settings();
|
||||
|
||||
String key = apiKey();
|
||||
if (key == null) {
|
||||
Msg.error(asker, "A IA não está configurada. Falta a chave em plugins/Canalhandia/"
|
||||
+ KEY_FILE + " ou na variável " + KEY_ENV + ".");
|
||||
return;
|
||||
}
|
||||
question = question.trim();
|
||||
if (question.length() > settings.aiMaxQuestion()) {
|
||||
Msg.error(asker, "Pergunta longa demais (máx. " + settings.aiMaxQuestion() + " caracteres).");
|
||||
return;
|
||||
}
|
||||
if (Boolean.TRUE.equals(pending.get(asker.getUniqueId()))) {
|
||||
Msg.error(asker, "Sua pergunta anterior ainda está sendo respondida.");
|
||||
return;
|
||||
}
|
||||
if (!withinDailyLimit(settings)) {
|
||||
Msg.error(asker, "O limite diário de perguntas do servidor acabou. Volta amanhã.");
|
||||
return;
|
||||
}
|
||||
long wait = cooldownRemaining(asker, settings);
|
||||
if (wait > 0) {
|
||||
Msg.error(asker, "Espere " + wait + "s antes de perguntar de novo.");
|
||||
return;
|
||||
}
|
||||
|
||||
lastAsk.put(asker.getUniqueId(), System.currentTimeMillis());
|
||||
pending.put(asker.getUniqueId(), true);
|
||||
askedToday++;
|
||||
|
||||
if (settings.aiPublic()) {
|
||||
Bukkit.broadcast(Msg.tag("IA", NamedTextColor.LIGHT_PURPLE)
|
||||
.append(Component.text(asker.getName() + " perguntou: ", NamedTextColor.GRAY)
|
||||
.decoration(TextDecoration.BOLD, false))
|
||||
.append(Component.text(question, NamedTextColor.WHITE)
|
||||
.decoration(TextDecoration.BOLD, false)));
|
||||
}
|
||||
Msg.ok(asker, "Pensando...");
|
||||
|
||||
String prompt = question;
|
||||
UUID id = asker.getUniqueId();
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
String answer;
|
||||
try {
|
||||
answer = call(key, prompt, settings);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("Falha na chamada à IA: " + e);
|
||||
answer = null;
|
||||
}
|
||||
String finalAnswer = answer;
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
pending.remove(id);
|
||||
deliver(id, finalAnswer, settings);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void deliver(UUID askerId, String answer, Settings settings) {
|
||||
Player asker = Bukkit.getPlayer(askerId);
|
||||
if (answer == null || answer.isBlank()) {
|
||||
if (asker != null) {
|
||||
Msg.error(asker, "Não consegui resposta agora. Tente de novo em instantes.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
Component message = Msg.tag("IA", NamedTextColor.LIGHT_PURPLE)
|
||||
.append(Component.text(sanitise(answer, settings.aiMaxAnswer()), NamedTextColor.WHITE)
|
||||
.decoration(TextDecoration.BOLD, false));
|
||||
if (settings.aiPublic()) {
|
||||
Bukkit.broadcast(message);
|
||||
} else if (asker != null) {
|
||||
asker.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
// --- HTTP ---------------------------------------------------------------
|
||||
|
||||
private String call(String key, String question, Settings settings) throws Exception {
|
||||
JsonArray messages = new JsonArray();
|
||||
messages.add(message("system", settings.aiInstructions()));
|
||||
messages.add(message("user", question));
|
||||
|
||||
JsonObject body = new JsonObject();
|
||||
body.addProperty("model", settings.aiModel());
|
||||
body.add("messages", messages);
|
||||
body.addProperty("max_tokens", settings.aiMaxTokens());
|
||||
body.addProperty("temperature", settings.aiTemperature());
|
||||
// No "tools" and no "tool_choice": the model is given nothing it could call.
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(settings.aiUrl()))
|
||||
.timeout(Duration.ofSeconds(settings.aiTimeoutSeconds()))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer " + key)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body.toString(), StandardCharsets.UTF_8))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() / 100 != 2) {
|
||||
plugin.getLogger().warning("IA respondeu HTTP " + response.statusCode() + ": "
|
||||
+ trim(response.body()));
|
||||
return null;
|
||||
}
|
||||
return extract(response.body());
|
||||
}
|
||||
|
||||
private static JsonObject message(String role, String content) {
|
||||
JsonObject object = new JsonObject();
|
||||
object.addProperty("role", role);
|
||||
object.addProperty("content", content);
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls the reply text out of an OpenAI-shaped response.
|
||||
*
|
||||
* <p>MiniMax returns HTTP 200 even for application-level failures, putting
|
||||
* the real outcome in {@code base_resp.status_code}, so that is checked too.
|
||||
*/
|
||||
private String extract(String json) {
|
||||
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
|
||||
if (root.has("base_resp")) {
|
||||
JsonObject base = root.getAsJsonObject("base_resp");
|
||||
int status = base.has("status_code") ? base.get("status_code").getAsInt() : 0;
|
||||
if (status != 0) {
|
||||
plugin.getLogger().warning("IA recusou: base_resp " + base);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (!root.has("choices")) {
|
||||
return null;
|
||||
}
|
||||
JsonArray choices = root.getAsJsonArray("choices");
|
||||
if (choices.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
JsonObject first = choices.get(0).getAsJsonObject();
|
||||
if (!first.has("message")) {
|
||||
return null;
|
||||
}
|
||||
JsonObject message = first.getAsJsonObject("message");
|
||||
// Reasoning models also return "reasoning_content"; only "content" is shown.
|
||||
return message.has("content") && !message.get("content").isJsonNull()
|
||||
? message.get("content").getAsString()
|
||||
: null;
|
||||
}
|
||||
|
||||
// --- limits and cleanup -------------------------------------------------
|
||||
|
||||
private boolean withinDailyLimit(Settings settings) {
|
||||
LocalDate today = LocalDate.now();
|
||||
if (!today.equals(day)) {
|
||||
day = today;
|
||||
askedToday = 0;
|
||||
}
|
||||
return askedToday < settings.aiDailyLimit();
|
||||
}
|
||||
|
||||
private long cooldownRemaining(Player asker, Settings settings) {
|
||||
Long last = lastAsk.get(asker.getUniqueId());
|
||||
if (last == null || asker.hasPermission("canalhandia.admin")) {
|
||||
return 0;
|
||||
}
|
||||
long elapsed = System.currentTimeMillis() - last;
|
||||
long window = settings.aiCooldownSeconds() * 1000L;
|
||||
return elapsed >= window ? 0 : (window - elapsed) / 1000 + 1;
|
||||
}
|
||||
|
||||
int askedToday() {
|
||||
withinDailyLimit(plugin.settings());
|
||||
return askedToday;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a model reply safe to print in chat.
|
||||
*
|
||||
* <p>Strips colour codes so the reply cannot forge server messages, folds
|
||||
* newlines so one answer stays one chat entry, and removes leading slashes
|
||||
* so nothing that comes back reads as a command to run.
|
||||
*/
|
||||
static String sanitise(String raw, int max) {
|
||||
String text = raw.replace('§', ' ')
|
||||
.replaceAll("[\\r\\n]+", " ")
|
||||
.replaceAll("\\s{2,}", " ")
|
||||
.trim();
|
||||
while (text.startsWith("/")) {
|
||||
text = text.substring(1).trim();
|
||||
}
|
||||
if (text.length() > max) {
|
||||
text = text.substring(0, max).trim() + "…";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static String trim(String text) {
|
||||
return text.length() > 300 ? text.substring(0, 300) + "…" : text;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
private Settings settings;
|
||||
private OfflineStats offlineStats;
|
||||
private Milestones milestones;
|
||||
private Ai ai;
|
||||
private NamespacedKey optOutKey;
|
||||
private BukkitTask timerTask;
|
||||
private BukkitTask milestoneTask;
|
||||
@@ -60,11 +61,12 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
settings = new Settings(this);
|
||||
offlineStats = new OfflineStats(this);
|
||||
milestones = new Milestones(this);
|
||||
ai = new Ai(this);
|
||||
optOutKey = new NamespacedKey(this, "opt_out");
|
||||
|
||||
CanalhandiaCommand root = new CanalhandiaCommand(this);
|
||||
for (String name : List.of("canalhandia", "curiosidade", "adivinha", "enquete", "ranking",
|
||||
"reagir", "reacoes", "palpite", "votar", "legal", "wow", "top", "f")) {
|
||||
"reagir", "reacoes", "palpite", "votar", "legal", "wow", "top", "f", "ia")) {
|
||||
register(name, root);
|
||||
}
|
||||
|
||||
@@ -113,6 +115,10 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
return offlineStats;
|
||||
}
|
||||
|
||||
Ai ai() {
|
||||
return ai;
|
||||
}
|
||||
|
||||
// --- scheduling ---------------------------------------------------------
|
||||
|
||||
/** Starts, stops or restarts the repeating curiosity task to match the mode. */
|
||||
|
||||
@@ -46,6 +46,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
||||
case "palpite" -> guessLatest(sender, args);
|
||||
case "votar" -> voteLatest(sender, args);
|
||||
case "reacoes" -> whoReacted(sender);
|
||||
case "ia" -> ia(sender, args);
|
||||
default -> {
|
||||
String reaction = plugin.settings().reactionForCommand(command.getName());
|
||||
if (reaction != null) {
|
||||
@@ -654,6 +655,12 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
||||
}
|
||||
}
|
||||
Msg.line(sender, "categorias ativas", enabled.isEmpty() ? "nenhuma" : String.join(", ", enabled));
|
||||
Msg.line(sender, "ia", plugin.ai().configured()
|
||||
? settings.aiModel() + " · " + plugin.ai().askedToday() + "/"
|
||||
+ settings.aiDailyLimit() + " hoje · cooldown "
|
||||
+ settings.aiCooldownSeconds() + "s · "
|
||||
+ (settings.aiPublic() ? "resposta pública" : "resposta privada")
|
||||
: "sem chave configurada");
|
||||
}
|
||||
|
||||
private String enabledModules() {
|
||||
@@ -678,6 +685,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
||||
commands.put("/enquete P | A | B", "abre uma enquete com opções clicáveis");
|
||||
commands.put("/enquete encerrar", "encerra a enquete aberta");
|
||||
commands.put("/ranking [categoria]", "mostra os placares do servidor");
|
||||
commands.put("/ia <pergunta>", "pergunta para a IA (precisa de permissão)");
|
||||
commands.put("/canalhandia status", "mostra toda a configuração");
|
||||
commands.put("/canalhandia modulos", "lista os módulos e seu estado");
|
||||
commands.put("/canalhandia plataformas", "quem está online e se é Java ou Bedrock");
|
||||
@@ -737,6 +745,35 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
||||
}
|
||||
}
|
||||
|
||||
// --- /ia ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Asks the model a question.
|
||||
*
|
||||
* <p>Gated by {@code canalhandia.ia}, which defaults to op — so the operator
|
||||
* has it out of the box and LuckPerms can hand it to anyone else with
|
||||
* {@code lp user <nome> permission set canalhandia.ia true}.
|
||||
*/
|
||||
private boolean ia(CommandSender sender, String[] args) {
|
||||
if (!sender.hasPermission("canalhandia.ia")) {
|
||||
return denied(sender);
|
||||
}
|
||||
if (!plugin.settings().moduleEnabled(Module.IA)) {
|
||||
Msg.error(sender, "O módulo de IA está desligado.");
|
||||
return true;
|
||||
}
|
||||
if (!(sender instanceof Player player)) {
|
||||
Msg.error(sender, "Só jogadores podem usar /ia.");
|
||||
return true;
|
||||
}
|
||||
if (args.length == 0) {
|
||||
Msg.error(sender, "Uso: /ia <pergunta>");
|
||||
return true;
|
||||
}
|
||||
plugin.ai().ask(player, String.join(" ", args));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean admin(CommandSender sender) {
|
||||
if (sender.hasPermission(ADMIN)) {
|
||||
return true;
|
||||
|
||||
@@ -8,7 +8,8 @@ enum Module {
|
||||
LUTO("luto", "Botão [F] nas mortes"),
|
||||
ENQUETE("enquete", "Enquetes"),
|
||||
RANKING("ranking", "Rankings"),
|
||||
MARCOS("marcos", "Marcos e conquistas");
|
||||
MARCOS("marcos", "Marcos e conquistas"),
|
||||
IA("ia", "Perguntas para a IA");
|
||||
|
||||
private final String key;
|
||||
private final String label;
|
||||
|
||||
@@ -210,6 +210,88 @@ final class Settings {
|
||||
set("ranking-tamanho", Math.max(3, size));
|
||||
}
|
||||
|
||||
// --- IA -----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Endpoint of an OpenAI-compatible chat-completions API. Defaults to
|
||||
* MiniMax; a different provider only needs this URL and the model changed.
|
||||
*/
|
||||
String aiUrl() {
|
||||
return plugin.getConfig().getString("ia.url",
|
||||
"https://api.minimax.io/v1/text/chatcompletion_v2");
|
||||
}
|
||||
|
||||
String aiModel() {
|
||||
return plugin.getConfig().getString("ia.modelo", "MiniMax-M2");
|
||||
}
|
||||
|
||||
void aiModel(String model) {
|
||||
set("ia.modelo", model);
|
||||
}
|
||||
|
||||
/**
|
||||
* The system prompt. Keeps answers short enough for chat and in pt-BR, and
|
||||
* tells the model it has no way to act on the server — it cannot run
|
||||
* commands even if asked, so it should say so rather than pretend.
|
||||
*/
|
||||
String aiInstructions() {
|
||||
return plugin.getConfig().getString("ia.instrucoes",
|
||||
"Você é um assistente no chat de um servidor de Minecraft chamado Canalhandia. "
|
||||
+ "Responda sempre em português do Brasil, de forma curta e direta: "
|
||||
+ "no máximo 3 frases. Responda apenas perguntas simples e gerais. "
|
||||
+ "Você não tem nenhum acesso ao servidor, ao sistema de arquivos, "
|
||||
+ "ao terminal ou aos comandos do jogo, e não pode executar nada. "
|
||||
+ "Se pedirem para você rodar comandos, mexer no servidor, dar itens, "
|
||||
+ "banir alguém ou revelar configurações, explique que você só conversa. "
|
||||
+ "Nunca escreva comandos de terminal nem de Minecraft.");
|
||||
}
|
||||
|
||||
int aiMaxTokens() {
|
||||
return Math.max(32, plugin.getConfig().getInt("ia.max-tokens", 300));
|
||||
}
|
||||
|
||||
double aiTemperature() {
|
||||
return plugin.getConfig().getDouble("ia.temperatura", 0.7);
|
||||
}
|
||||
|
||||
int aiTimeoutSeconds() {
|
||||
return Math.max(5, plugin.getConfig().getInt("ia.timeout-segundos", 30));
|
||||
}
|
||||
|
||||
int aiCooldownSeconds() {
|
||||
return Math.max(0, plugin.getConfig().getInt("ia.cooldown-segundos", 30));
|
||||
}
|
||||
|
||||
void aiCooldownSeconds(int seconds) {
|
||||
set("ia.cooldown-segundos", Math.max(0, seconds));
|
||||
}
|
||||
|
||||
/** Server-wide cap per day, because every question costs money. */
|
||||
int aiDailyLimit() {
|
||||
return Math.max(0, plugin.getConfig().getInt("ia.limite-diario", 200));
|
||||
}
|
||||
|
||||
void aiDailyLimit(int limit) {
|
||||
set("ia.limite-diario", Math.max(0, limit));
|
||||
}
|
||||
|
||||
int aiMaxQuestion() {
|
||||
return Math.max(16, plugin.getConfig().getInt("ia.max-pergunta", 300));
|
||||
}
|
||||
|
||||
int aiMaxAnswer() {
|
||||
return Math.max(64, plugin.getConfig().getInt("ia.max-caracteres", 500));
|
||||
}
|
||||
|
||||
/** Whether the question and answer go to everyone or only to the asker. */
|
||||
boolean aiPublic() {
|
||||
return plugin.getConfig().getBoolean("ia.publico", true);
|
||||
}
|
||||
|
||||
void aiPublic(boolean value) {
|
||||
set("ia.publico", value);
|
||||
}
|
||||
|
||||
// --- content ------------------------------------------------------------
|
||||
|
||||
boolean categoryEnabled(Category category) {
|
||||
|
||||
@@ -14,6 +14,7 @@ modulos:
|
||||
enquete: true # /enquete com votação clicável
|
||||
ranking: true # /ranking com placares do servidor
|
||||
marcos: true # avisos automáticos ao passar de 100 km, 24 horas, etc.
|
||||
ia: true # /ia <pergunta> — só para quem tem canalhandia.ia
|
||||
|
||||
# --- Curiosidades ------------------------------------------------------------
|
||||
|
||||
@@ -103,3 +104,55 @@ enquete-minutos: 5
|
||||
|
||||
# Quantas posições mostrar em cada /ranking.
|
||||
ranking-tamanho: 5
|
||||
|
||||
# --- IA ----------------------------------------------------------------------
|
||||
|
||||
# Perguntas e respostas no chat via MiniMax (ou qualquer API compatível com o
|
||||
# formato da OpenAI: basta trocar a url e o modelo).
|
||||
#
|
||||
# A CHAVE DA API NÃO FICA AQUI. Este arquivo vai para o git. A chave é lida de:
|
||||
# 1. a variável de ambiente MINIMAX_API_KEY, ou
|
||||
# 2. o arquivo plugins/Canalhandia/minimax.key (só a chave, uma linha).
|
||||
#
|
||||
# Quem pode usar: a permissão canalhandia.ia, padrão op. Para liberar alguém:
|
||||
# lp user <nome> permission set canalhandia.ia true
|
||||
# lp group <grupo> permission set canalhandia.ia true
|
||||
#
|
||||
# A IA só produz texto de chat. A resposta nunca é executada como comando, e
|
||||
# nenhuma ferramenta é oferecida ao modelo na requisição — ele não tem como
|
||||
# rodar nada no servidor, no terminal ou no jogo.
|
||||
ia:
|
||||
url: "https://api.minimax.io/v1/text/chatcompletion_v2"
|
||||
modelo: "MiniMax-M2"
|
||||
|
||||
# Tamanho da resposta pedida ao modelo, e o corte final no chat.
|
||||
max-tokens: 300
|
||||
max-caracteres: 500
|
||||
|
||||
# Tamanho máximo da pergunta, em caracteres.
|
||||
max-pergunta: 300
|
||||
|
||||
temperatura: 0.7
|
||||
timeout-segundos: 30
|
||||
|
||||
# Segundos entre perguntas do mesmo jogador. Quem tem canalhandia.admin
|
||||
# não espera.
|
||||
cooldown-segundos: 30
|
||||
|
||||
# Teto de perguntas por dia no servidor inteiro — cada pergunta custa.
|
||||
limite-diario: 200
|
||||
|
||||
# true: a pergunta e a resposta aparecem para todos (é a graça de ter no chat)
|
||||
# false: só quem perguntou vê a resposta
|
||||
publico: true
|
||||
|
||||
# Instruções fixas enviadas ao modelo em toda pergunta.
|
||||
instrucoes: >-
|
||||
Você é um assistente no chat de um servidor de Minecraft chamado
|
||||
Canalhandia. Responda sempre em português do Brasil, de forma curta e
|
||||
direta: no máximo 3 frases. Responda apenas perguntas simples e gerais.
|
||||
Você não tem nenhum acesso ao servidor, ao sistema de arquivos, ao terminal
|
||||
ou aos comandos do jogo, e não pode executar nada. Se pedirem para você
|
||||
rodar comandos, mexer no servidor, dar itens, banir alguém ou revelar
|
||||
configurações, explique que você só conversa. Nunca escreva comandos de
|
||||
terminal nem de Minecraft.
|
||||
|
||||
@@ -56,6 +56,10 @@ commands:
|
||||
f:
|
||||
description: Presta luto pela última morte.
|
||||
usage: /f
|
||||
ia:
|
||||
description: Faz uma pergunta simples para a IA.
|
||||
usage: /ia <pergunta>
|
||||
aliases: [pergunta]
|
||||
|
||||
permissions:
|
||||
# Declared explicitly: an undeclared Bukkit permission falls back to op-only,
|
||||
@@ -75,6 +79,9 @@ permissions:
|
||||
canalhandia.admin:
|
||||
description: Permite mudar módulos, modo, intervalo, categorias e reações.
|
||||
default: op
|
||||
canalhandia.ia:
|
||||
description: Permite usar /ia. Padrão op; o LuckPerms pode conceder a outros.
|
||||
default: op
|
||||
canalhandia.isento:
|
||||
description: Quem tem isto nunca é sorteado como assunto.
|
||||
default: false
|
||||
|
||||
Reference in New Issue
Block a user