You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 13:28:09