Chat : un simple apostrophe dans un chuchotement a un bot faisait tomber le royaume
Quand un joueur chuchote a un playerbot, le core cherche une reponse toute faite
dans ai_talk_whisper et injectait le message BRUT dans la requete :
WHERE '%s' REGEXP cname
Une apostrophe - « c est », « j ai », une phrase de francais sur deux - cassait
la requete. MySQLConnection::_HandleMySQLErrno repond a une erreur SQL par un
abandon du processus : n importe quel joueur faisait donc tomber le serveur en
adressant la parole a un bot, et pouvait y injecter du SQL au passage.
Trois crashs SIGSEGV en une soiree (23h06, 23h44, 23h58), tous avec la meme pile
HandleChatMessage -> DatabaseWorkerPool::Query -> _HandleMySQLErrno -> Abort. Le
defaut preexistait, mais le module de dialogue l a reveille : jusqu ici personne
n adressait la parole aux playerbots.
Le message est desormais echappe et borne a 255 caracteres.
Second changement, sans rapport : Mistral devient un fournisseur a part entiere.
Il parlait deja le dialecte d OpenAI, mais « mistral » n etait qu un alias : sans
adresse explicite, la cle du joueur partait chez OpenAI, qui la rejetait par un
401 incomprehensible. Il a maintenant son adresse par defaut, et l aide en jeu
signale que Gemini ne repond pas depuis un serveur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -335,12 +335,20 @@ void WorldSession::HandleChatMessage(ChatMsg type, uint32 lang, std::string msg,
|
|||||||
LocaleConstant locale = sender->GetSession()->GetSessionDbcLocale();
|
LocaleConstant locale = sender->GetSession()->GetSessionDbcLocale();
|
||||||
QueryResult result;
|
QueryResult result;
|
||||||
|
|
||||||
|
// SylvaniaCore : le message etait injecte brut dans la requete. Une
|
||||||
|
// apostrophe - "c'est", "j'ai" - la cassait, et le core repond a
|
||||||
|
// une erreur SQL par un abandon du processus : n importe quel joueur
|
||||||
|
// faisait tomber le royaume en chuchotant a un playerbot, et pouvait
|
||||||
|
// y injecter du SQL. On echappe, et on borne la longueur au passage.
|
||||||
|
std::string safeMsg = msg.size() > 255 ? msg.substr(0, 255) : msg;
|
||||||
|
WorldDatabase.EscapeString(safeMsg);
|
||||||
|
|
||||||
// Search for locale-specific response first
|
// Search for locale-specific response first
|
||||||
result = WorldDatabase.PQuery(
|
result = WorldDatabase.PQuery(
|
||||||
"SELECT `reply` FROM `ai_talk_whisper_locale` "
|
"SELECT `reply` FROM `ai_talk_whisper_locale` "
|
||||||
"WHERE locale = %u AND '%s' REGEXP cname "
|
"WHERE locale = %u AND '%s' REGEXP cname "
|
||||||
"ORDER BY RAND() LIMIT 1",
|
"ORDER BY RAND() LIMIT 1",
|
||||||
locale, msg.c_str()
|
locale, safeMsg.c_str()
|
||||||
);
|
);
|
||||||
|
|
||||||
// If nothing is found, English fallback
|
// If nothing is found, English fallback
|
||||||
@@ -350,7 +358,7 @@ void WorldSession::HandleChatMessage(ChatMsg type, uint32 lang, std::string msg,
|
|||||||
"SELECT `reply` FROM `ai_talk_whisper` "
|
"SELECT `reply` FROM `ai_talk_whisper` "
|
||||||
"WHERE '%s' REGEXP cname "
|
"WHERE '%s' REGEXP cname "
|
||||||
"ORDER BY RAND() LIMIT 1",
|
"ORDER BY RAND() LIMIT 1",
|
||||||
msg.c_str()
|
safeMsg.c_str()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ namespace
|
|||||||
char const* OPENAI_DEFAULT_URL = "https://api.openai.com/v1/chat/completions";
|
char const* OPENAI_DEFAULT_URL = "https://api.openai.com/v1/chat/completions";
|
||||||
char const* ANTHROPIC_DEFAULT_URL = "https://api.anthropic.com/v1/messages";
|
char const* ANTHROPIC_DEFAULT_URL = "https://api.anthropic.com/v1/messages";
|
||||||
char const* GEMINI_DEFAULT_HOST = "https://generativelanguage.googleapis.com/v1beta/models/";
|
char const* GEMINI_DEFAULT_HOST = "https://generativelanguage.googleapis.com/v1beta/models/";
|
||||||
|
char const* MISTRAL_DEFAULT_URL = "https://api.mistral.ai/v1/chat/completions";
|
||||||
|
|
||||||
std::string JsonEscape(std::string const& text)
|
std::string JsonEscape(std::string const& text)
|
||||||
{
|
{
|
||||||
@@ -272,6 +273,7 @@ char const* MercenaryChatMgr::GetProviderName(uint8 provider)
|
|||||||
case MERC_LLM_OPENAI: return "OpenAI";
|
case MERC_LLM_OPENAI: return "OpenAI";
|
||||||
case MERC_LLM_ANTHROPIC: return "Anthropic";
|
case MERC_LLM_ANTHROPIC: return "Anthropic";
|
||||||
case MERC_LLM_GEMINI: return "Gemini";
|
case MERC_LLM_GEMINI: return "Gemini";
|
||||||
|
case MERC_LLM_MISTRAL: return "Mistral";
|
||||||
default: return "inconnu";
|
default: return "inconnu";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,8 +283,10 @@ uint8 MercenaryChatMgr::ParseProvider(std::string const& name)
|
|||||||
std::string lowered = name;
|
std::string lowered = name;
|
||||||
std::transform(lowered.begin(), lowered.end(), lowered.begin(), ::tolower);
|
std::transform(lowered.begin(), lowered.end(), lowered.begin(), ::tolower);
|
||||||
|
|
||||||
|
if (lowered == "mistral" || lowered == "lechat")
|
||||||
|
return MERC_LLM_MISTRAL;
|
||||||
if (lowered == "openai" || lowered == "compatible" || lowered == "openrouter"
|
if (lowered == "openai" || lowered == "compatible" || lowered == "openrouter"
|
||||||
|| lowered == "mistral" || lowered == "groq" || lowered == "ollama")
|
|| lowered == "groq" || lowered == "ollama" || lowered == "cerebras")
|
||||||
return MERC_LLM_OPENAI;
|
return MERC_LLM_OPENAI;
|
||||||
if (lowered == "anthropic" || lowered == "claude")
|
if (lowered == "anthropic" || lowered == "claude")
|
||||||
return MERC_LLM_ANTHROPIC;
|
return MERC_LLM_ANTHROPIC;
|
||||||
@@ -336,9 +340,10 @@ bool MercenaryChatMgr::HandleApiCommand(Player* owner, std::string const& argume
|
|||||||
{
|
{
|
||||||
handler.PSendSysMessage("|cff00ff00[Mercenaire]|r Pour delier la langue de votre mercenaire, fournissez votre propre cle :");
|
handler.PSendSysMessage("|cff00ff00[Mercenaire]|r Pour delier la langue de votre mercenaire, fournissez votre propre cle :");
|
||||||
handler.PSendSysMessage(" |cffffd100!api <fournisseur> <modele> <cle>|r");
|
handler.PSendSysMessage(" |cffffd100!api <fournisseur> <modele> <cle>|r");
|
||||||
handler.PSendSysMessage(" fournisseurs : |cffffd100openai|r, |cffffd100anthropic|r, |cffffd100gemini|r");
|
handler.PSendSysMessage(" fournisseurs : |cffffd100mistral|r, |cffffd100anthropic|r, |cffffd100openai|r, |cffffd100gemini|r");
|
||||||
handler.PSendSysMessage(" exemple : |cffffd100!api anthropic claude-sonnet-5 sk-ant-...|r");
|
handler.PSendSysMessage(" exemple : |cffffd100!api mistral mistral-small-latest VOTRE_CLE|r");
|
||||||
handler.PSendSysMessage(" service compatible OpenAI : ajoutez l'adresse en fin de ligne.");
|
handler.PSendSysMessage(" |cffff8800Gemini ne repond pas depuis ce royaume|r (Google refuse les serveurs).");
|
||||||
|
handler.PSendSysMessage(" service compatible OpenAI (Groq, OpenRouter...) : ajoutez l'adresse en fin de ligne.");
|
||||||
handler.PSendSysMessage(" |cffffd100!api off|r retire votre cle immediatement.");
|
handler.PSendSysMessage(" |cffffd100!api off|r retire votre cle immediatement.");
|
||||||
handler.PSendSysMessage("Votre cle n'est jamais enregistree : elle vit en memoire et disparait a la rupture du contrat ou a votre deconnexion.");
|
handler.PSendSysMessage("Votre cle n'est jamais enregistree : elle vit en memoire et disparait a la rupture du contrat ou a votre deconnexion.");
|
||||||
return true;
|
return true;
|
||||||
@@ -591,6 +596,8 @@ std::string MercenaryChatMgr::BuildUrl(MercenaryChatRequest const& request)
|
|||||||
{
|
{
|
||||||
case MERC_LLM_ANTHROPIC:
|
case MERC_LLM_ANTHROPIC:
|
||||||
return ANTHROPIC_DEFAULT_URL;
|
return ANTHROPIC_DEFAULT_URL;
|
||||||
|
case MERC_LLM_MISTRAL:
|
||||||
|
return MISTRAL_DEFAULT_URL;
|
||||||
case MERC_LLM_GEMINI:
|
case MERC_LLM_GEMINI:
|
||||||
{
|
{
|
||||||
// Gemini porte la cle dans l adresse et le modele dans le chemin.
|
// Gemini porte la cle dans l adresse et le modele dans le chemin.
|
||||||
@@ -711,21 +718,41 @@ bool MercenaryChatMgr::Perform(MercenaryChatRequest const& request, std::string&
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (httpCode == 401 || httpCode == 403)
|
|
||||||
{
|
|
||||||
error = "Votre cle a ete refusee par le fournisseur.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (httpCode == 429)
|
|
||||||
{
|
|
||||||
error = "Votre fournisseur refuse le rythme : trop de demandes.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (httpCode >= 400)
|
if (httpCode >= 400)
|
||||||
{
|
{
|
||||||
|
// Le fournisseur explique presque toujours son refus dans le corps de la
|
||||||
|
// reponse - modele inconnu, quota depasse, cle sans droits. Renvoyer un
|
||||||
|
// code nu au joueur lui cachait justement la seule information utile.
|
||||||
|
std::string detail;
|
||||||
|
size_t const errorSection = body.find("\"error\"");
|
||||||
|
if (errorSection != std::string::npos)
|
||||||
|
FindJsonString(body, "message", errorSection, detail);
|
||||||
|
|
||||||
std::ostringstream text;
|
std::ostringstream text;
|
||||||
text << "Le fournisseur a repondu par une erreur (" << httpCode << ").";
|
if (httpCode == 401 || httpCode == 403)
|
||||||
|
text << "Votre cle a ete refusee";
|
||||||
|
else if (httpCode == 404)
|
||||||
|
text << "Modele introuvable pour votre cle";
|
||||||
|
else if (httpCode == 429)
|
||||||
|
text << "Votre fournisseur refuse le rythme";
|
||||||
|
else
|
||||||
|
text << "Le fournisseur a repondu par une erreur";
|
||||||
|
|
||||||
|
text << " (" << httpCode << ")";
|
||||||
|
if (!detail.empty())
|
||||||
|
text << " : " << Flatten(detail, 180);
|
||||||
|
else
|
||||||
|
text << ".";
|
||||||
|
|
||||||
error = text.str();
|
error = text.str();
|
||||||
|
|
||||||
|
// Trace de diagnostic : le corps envoye, jamais l adresse - chez Gemini
|
||||||
|
// la cle voyage dans l URL. Le refus d un fournisseur porte presque
|
||||||
|
// toujours sur ce qu on lui a transmis.
|
||||||
|
TC_LOG_ERROR("server.worldserver", "Mercenaires (dialogue): refus %ld du fournisseur %s. Corps envoye : %s",
|
||||||
|
httpCode, GetProviderName(request.provider), payload.c_str());
|
||||||
|
TC_LOG_ERROR("server.worldserver", "Mercenaires (dialogue): reponse recue : %s", body.c_str());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,9 +48,12 @@ class Player;
|
|||||||
|
|
||||||
enum MercenaryLlmProvider
|
enum MercenaryLlmProvider
|
||||||
{
|
{
|
||||||
MERC_LLM_OPENAI = 0, // et tout service compatible : Mistral, Groq, OpenRouter, Ollama...
|
MERC_LLM_OPENAI = 0, // et tout service compatible : Groq, OpenRouter, Ollama...
|
||||||
MERC_LLM_ANTHROPIC = 1,
|
MERC_LLM_ANTHROPIC = 1,
|
||||||
MERC_LLM_GEMINI = 2,
|
MERC_LLM_GEMINI = 2,
|
||||||
|
// Mistral parle le dialecte d OpenAI : seule son adresse par defaut change.
|
||||||
|
// Il a droit a son propre nom pour que le joueur n ait pas a la coller.
|
||||||
|
MERC_LLM_MISTRAL = 3,
|
||||||
MERC_LLM_UNKNOWN = 255
|
MERC_LLM_UNKNOWN = 255
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user