FiveM QBCore AI NPC聊天脚本异常:控制台有响应但UI不显示
问题:FiveM QBCore AI NPC聊天脚本中AI回复无法显示在UI聊天窗口
我正在开发基于QBCore的FiveM AI驱动NPC聊天脚本,玩家靠近NPC后按E键打开聊天窗口,发送消息后AI生成回复。目前AI回复能正常在控制台输出,但无法显示在游戏UI聊天窗口中,只有玩家消息能正常显示。
架构概述
- Server Script (server.lua):处理AI API请求,将响应返回给客户端
- Client Script (client.lua):发送玩家消息到服务器,接收AI响应并传递给NUI
- UI Scripts (index.html, style.css, script.js):管理聊天窗口并展示消息
问题诊断与修复方案
核心问题在于客户端接收服务器事件后,调用SendNUIMessage时未确保在主线程执行,导致NUI无法正确接收消息。以下是具体修复步骤:
修复步骤1:确保客户端在主线程发送NUI消息
修改client.lua中的InteractiveNPCS:ReceiveLLMResponse事件处理,将SendNUIMessage包裹在Citizen.CreateThread中:
RegisterNetEvent('InteractiveNPCS:ReceiveLLMResponse') AddEventHandler('InteractiveNPCS:ReceiveLLMResponse', function(response) print("Client: Received response - " .. response) Citizen.CreateThread(function() SendNUIMessage({ type = "npcReply", text = response }) end) end)
修复步骤2:处理HuggingFace API异常响应
服务器端补充处理API返回的错误情况(比如模型加载中),避免传递无效数据给客户端:
-- 修改server.lua中的PerformHttpRequest回调 PerformHttpRequest(endpoint, function(err, responseText, headers) if err == 200 then print("Received response from LLM API: " .. tostring(responseText)) local response = json.decode(responseText) local reply = "Sorry, I don't understand." -- 检查响应格式是否正确 if type(response) == "table" then if response[1] and response[1].generated_text then reply = response[1].generated_text elseif response.error then -- 处理模型加载中或其他API错误 reply = "Please wait, I'm still thinking..." end end print("Sending response to client: " .. reply) TriggerClientEvent('InteractiveNPCS:ReceiveLLMResponse', source, reply) else print("Error from LLM API: " .. tostring(err)) print("Response text: " .. tostring(responseText)) TriggerClientEvent('InteractiveNPCS:ReceiveLLMResponse', source, "Sorry, something went wrong.") end end, 'POST', json.encode({ inputs = text }), { ["Authorization"] = "Bearer " .. apiKey, ["Content-Type"] = "application/json" })
修复步骤3:UI端增加空消息过滤
在script.js的sendMessage函数中添加空消息判断,避免无效请求:
function sendMessage() { const text = document.getElementById('inputMessage').value; if (!text.trim()) return; // 空消息不发送 console.log("UI: Sending message - " + text); addMessageToChat("User", text); fetch(`https://${GetParentResourceName()}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) }).then(resp => resp.json()).then(resp => { if (resp === 'ok') { document.getElementById('inputMessage').value = ''; } }); }
完整修复后代码
fxmanifest.lua
fx_version 'cerulean' game 'gta5' author 'Revo' description 'Interactive NPCs with LLM' version '1.0.0' shared_script 'config.lua' client_scripts { 'client/client.lua' } server_scripts { 'server/server.lua' } files { 'ui/index.html', 'ui/style.css', 'ui/script.js' } ui_page 'ui/index.html'
config.lua
Config = {} Config.HuggingFaceAPIKey = "API-Key" -- 替换为你的HuggingFace API密钥 Config.ModelEndpoint = "https://api-inference.huggingface.co/models/facebook/blenderbot-400M-distill"
server.lua
local json = require('json') RegisterNetEvent('InteractiveNPCS:RequestLLMResponse') AddEventHandler('InteractiveNPCS:RequestLLMResponse', function(text) print("Received text from client: " .. text) local apiKey = Config.HuggingFaceAPIKey local endpoint = Config.ModelEndpoint PerformHttpRequest(endpoint, function(err, responseText, headers) if err == 200 then print("Received response from LLM API: " .. tostring(responseText)) local response = json.decode(responseText) local reply = "Sorry, I don't understand." if type(response) == "table" then if response[1] and response[1].generated_text then reply = response[1].generated_text elseif response.error then reply = "Please wait, I'm still thinking..." end end print("Sending response to client: " .. reply) TriggerClientEvent('InteractiveNPCS:ReceiveLLMResponse', source, reply) else print("Error from LLM API: " .. tostring(err)) print("Response text: " .. tostring(responseText)) TriggerClientEvent('InteractiveNPCS:ReceiveLLMResponse', source, "Sorry, something went wrong.") end end, 'POST', json.encode({ inputs = text }), { ["Authorization"] = "Bearer " .. apiKey, ["Content-Type"] = "application/json" }) end)
client.lua
local function showChat() SetNuiFocus(true, true) SendNUIMessage({ type = "show" }) end local function closeChat() SetNuiFocus(false, false) SendNUIMessage({ type = "close" }) end local function getClosestPed(coords) local handle, ped = FindFirstPed() local success local closestPed = nil local closestDistance = -1 repeat local pedCoords = GetEntityCoords(ped) local distance = #(coords - pedCoords) if closestDistance == -1 or distance < closestDistance then closestPed = ped closestDistance = distance end success, ped = FindNextPed(handle) until not success EndFindPed(handle) return closestPed end RegisterNUICallback('closeChat', function(data, cb) closeChat() cb('ok') end) RegisterNUICallback('sendMessage', function(data, cb) local text = data.text print("Client: Sending message - " .. text) local playerPed = PlayerPedId() local coords = GetEntityCoords(playerPed) local closestPed = getClosestPed(coords) if closestPed then TriggerServerEvent('InteractiveNPCS:RequestLLMResponse', text) else Citizen.CreateThread(function() SendNUIMessage({ type = "npcReply", text = "No one is around to talk to." }) end) end cb('ok') end) Citizen.CreateThread(function() while true do Citizen.Wait(0) if IsControlJustReleased(0, 38) then -- E key local playerPed = PlayerPedId() local coords = GetEntityCoords(playerPed) local closestPed = getClosestPed(coords) if closestPed then showChat() end end end end) RegisterNetEvent('InteractiveNPCS:ReceiveLLMResponse') AddEventHandler('InteractiveNPCS:ReceiveLLMResponse', function(response) print("Client: Received response - " .. response) Citizen.CreateThread(function() SendNUIMessage({ type = "npcReply", text = response }) end) end)
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive NPC Chat</title> <link rel="stylesheet" href="style.css"> </head> <body> <div id="chatContainer"> <div id="messagesContainer"></div> <div id="inputContainer"> <input type="text" id="inputMessage" placeholder="Type a message..." /> <button id="sendButton">Send</button> <button id="closeButton">X</button> </div> </div> <script src="script.js"></script> </body> </html>
style.css
body { font-family: Arial, sans-serif; margin: 0; padding: 0; display: flex; justify-content: center; align-items: flex-end; height: 100vh; background: transparent; } #chatContainer { position: fixed; bottom: 10px; width: 90%; max-width: 600px; background: rgba(0, 0, 0, 0.8); padding: 10px; border-radius: 10px; color: white; display: none; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); } #messagesContainer { max-height: 200px; overflow-y: auto; margin-bottom: 10px; padding: 5px; border: 1px solid #444; border-radius: 5px; background: rgba(0, 0, 0, 0.6); } #inputContainer { display: flex; align-items: center; } #inputMessage { flex: 1; padding: 10px; margin-right: 10px; border-radius: 5px; border: none; } button { padding: 10px 15px; background: #3498db; border: none; border-radius: 5px; color: white; cursor: pointer; margin-left: 5px; } button:hover { background: #2980b9; } .chat-message.npc { color: #ffcc00; } .chat-message.user { color: #ffffff; }
script.js
console.log("UI: Script loaded"); document.getElementById("sendButton").addEventListener("click", sendMessage); document.getElementById("closeButton").addEventListener("click", closeChat); function closeChat() { console.log("UI: Closing chat"); fetch(`https://${GetParentResourceName()}/closeChat`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }).then(resp => resp.json()).then(resp => { if (resp === 'ok') { document.getElementById('chatContainer').style.display = 'none'; } }); } function sendMessage() { const text = document.getElementById('inputMessage').value; if (!text.trim()) return; // 空消息不发送 console.log("UI: Sending message - " + text); addMessageToChat("User", text); fetch(`https://${GetParentResourceName()}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) }).then(resp => resp.json()).then(resp => { if (resp === 'ok') { document.getElementById('inputMessage').value = ''; } }); } function addMessageToChat(sender, message) { const messagesContainer = document.getElementById('messagesContainer'); const messageElement = document.createElement('div'); messageElement.className = `chat-message ${sender.toLowerCase()}`; messageElement.innerText = `${sender}: ${message}`; messagesContainer.appendChild(messageElement); messagesContainer.scrollTop = messagesContainer.scrollHeight; } window.addEventListener('message', (event) => { console.log("UI: Received message - " + JSON.stringify(event.data)); if (event.data.type === 'show') { console.log("UI: Showing chat"); document.getElementById('inputMessage').value = ''; document.getElementById('messagesContainer').innerHTML = ''; document.getElementById('chatContainer').style.display = 'block'; } else if (event.data.type === 'close') { console.log("UI: Closing chat"); document.getElementById('chatContainer').style.display = 'none'; } else if (event.data.type === 'npcReply') { console.log("UI: NPC replied with text - " + event.data.text); addMessageToChat("Random NPC", event.data.text); } });
内容的提问来源于stack exchange,提问作者ii creeper
相关产品推荐
相关产品推荐

