Jun 19, 2026
I wanted a QQ bot setup on Linux that was not just “working for now,” but stable enough to live with. The stack I ended up with was NapCat + NoneBot2 + GsCore, with Caddy in front for HTTPS and reverse proxying. The overall design is straightforward. The actual deployment, though, was shaped by a series of very real and very ordinary problems: terminal encoding, missing CLI tools, local-only bindings, TLS validation, and even something as basic as forgotten DNS records.
This post turns that whole session into a practical guide you can follow from scratch. More importantly, it includes the failures along the way, because those are usually the parts that save the most time later.
This is the message path we ended up with:
QQ / group message
-> NapCat
-> NoneBot2
-> nonebot-plugin-genshinuid
-> GsCore
-> actual business plugin (such as GenshinUID)
For admin access from the web, we added another layer:
Browser
-> Caddy (HTTPS / reverse proxy)
-> NapCat WebUI / GsCore WebConsole
Two design decisions mattered a lot here:
6099 and 8765 directly to the public internet when a reverse proxy can keep them behind localhost.NapCat: the protocol side and QQ login layer.NoneBot2: the bot framework itself, responsible for receiving OneBot events and dispatching logic.GsCore: the Sayu core layer that provides the plugin ecosystem and a unified connection model.nonebot-plugin-genshinuid: the official NoneBot2-side plugin used to connect to GsCore.Caddy: the HTTPS and reverse proxy layer for safely exposing the admin panels.This guide assumes:
root or sudo access.The directory layout in this guide looks like this:
~/qqbot # NoneBot2 project
~/gsuid_core # GsCore installation
NapCat provides a Linux installer, and the straightforward way to start is:
curl -o napcat.sh https://nclatest.znin.net/NapNeko/NapCat-Installer/main/script/install.sh
bash napcat.sh --tui
After installation, you can usually re-enter its TUI with:
sudo napcat
The first thing we saw was not a clean UI, but broken characters like ~@~A and ~M~U. This turned out not to be a NapCat bug at all. It was a classic locale and terminal issue.
Start by checking:
locale
echo $LANG
echo $TERM
If the environment is not using UTF-8, fix it temporarily before launching NapCat:
export LANG=zh_CN.UTF-8
export LC_ALL=zh_CN.UTF-8
export TERM=xterm-256color
sudo napcat
If the server does not have the locale generated yet, install it:
sudo apt update
sudo apt install -y locales
sudo locale-gen zh_CN.UTF-8
sudo update-locale LANG=zh_CN.UTF-8
If you prefer a neutral UTF-8 setup instead of a Chinese locale, C.UTF-8 works well too:
export LANG=C.UTF-8
export LC_ALL=C.UTF-8
export TERM=xterm-256color
sudo napcat
NapCat’s WebUI typically listens on port 6099. Once it starts, check the logs for the access URL and token, scan the QQ login QR code, and set a proper WebUI password.
Create a Python virtual environment first:
mkdir -p ~/qqbot
cd ~/qqbot
python -m venv .venv --prompt nonebot2
source .venv/bin/activate
Install the base dependencies:
pip install "nonebot2[fastapi]"
pip install nonebot-adapter-onebot
Create .env:
HOST=127.0.0.1
PORT=8080
ONEBOT_ACCESS_TOKEN=replace-this-with-a-long-random-token
COMMAND_START=["/"]
COMMAND_SEP=["."]
Create bot.py:
import nonebot
from nonebot.adapters.onebot.v11 import Adapter as OneBotV11Adapter
nonebot.init()
driver = nonebot.get_driver()
driver.register_adapter(OneBotV11Adapter)
if __name__ == "__main__":
nonebot.run()
Test that it starts:
source .venv/bin/activate
python bot.py
In NapCat, create a new WebSocket client network connection and point it to:
ws://127.0.0.1:8080/onebot/v11/ws
Use the same token value as ONEBOT_ACCESS_TOKEN in your .env file.
If you get a 403, the first thing to verify is the token value. A mismatch there is by far the most common cause.
According to the official GsCore docs, install it as a separate project next to your bot:
cd ~
git clone https://github.com/Genshin-bots/gsuid_core.git --depth=1 --single-branch
cd gsuid_core
The official docs recommend uv:
uv python install 3.13
uv sync --python 3.13
uv run python -m ensurepip
Start GsCore:
uv run core
On first startup, it will generate:
~/gsuid_core/data/config.json
~/gsuid_core/data/core_config.json
The official NoneBot2-side connector for GsCore is nonebot-plugin-genshinuid.
nb was not foundWe initially followed the official pattern and ran:
nb plugin install nonebot-plugin-genshinuid
That failed with:
-bash: nb: command not found
Nothing was wrong with the project. The issue was simply that nb-cli was not installed. The nb command is not part of Python itself; it belongs to the NoneBot CLI.
There are two reasonable fixes.
nb-clicd ~/qqbot
source .venv/bin/activate
pip install nb-cli
nb plugin install nonebot-plugin-genshinuid
pipIf you already manage your bot with a hand-written bot.py, direct installation is often simpler:
cd ~/qqbot
source .venv/bin/activate
pip install nonebot-plugin-genshinuid
That second route is the one I would recommend in this setup. It is shorter and avoids pulling in an extra layer unless you actually want the CLI workflow.
.envAppend these values to your existing .env:
gsuid_core_ws_token=123
gsuid_core_host=localhost
gsuid_core_port=8765
gsuid_core_botid=NoneBot2
config.json on the GsCore sideEdit:
~/gsuid_core/data/config.json
At minimum, make sure these fields are set:
{
"HOST": "localhost",
"PORT": "8765",
"masters": ["your-qq-number"],
"WS_TOKEN": "123",
"TRUSTED_IPS": ["127.0.0.1"]
}
Two details matter here:
WS_TOKEN must match gsuid_core_ws_token in the NoneBot .envmasters should include your own QQ number, or a lot of core admin commands will not work for you laterGsCore by itself is the platform layer. The actual bot features come from business plugins.
Once your account is listed in masters, you can install one from chat:
core安装插件GenshinUID
If you prefer manual installation:
cd ~/gsuid_core/plugins
git clone -b v4 https://github.com/KimigaiiWuyi/GenshinUID.git --depth=1 --single-branch
Restart GsCore once the plugin is installed.
GsCore’s admin backend is the Web Console, usually available at:
http://127.0.0.1:8765/app
That is enough if you are accessing it locally.
When we tried to access the admin panel from the public internet, we ran into HTTP ERROR 502. The clue came from GsCore itself:
WebConsole挂载于本地, 如想外网访问请修改data/config.json中host为0.0.0.0!
That message means the web console is bound to localhost only, which is why outside access fails. The most direct fix is to change:
"HOST": "localhost"
to:
"HOST": "0.0.0.0"
That said, exposing 8765 directly is not the design I would recommend. A better pattern is to keep GsCore bound to localhost and let a reverse proxy expose it safely.
Operationally, it is cleaner not to expose 6099 and 8765 to the public internet at all. The safer layout is:
80 and 443Caddy handle HTTPS and reverse proxyingOn Ubuntu or Debian, one clean installation route is:
sudo apt update
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
Keep the backend services local:
127.0.0.1:6099127.0.0.1:8765Then configure /etc/caddy/Caddyfile like this:
{
email yourmail@example.com
}
napcat.example.com {
reverse_proxy 127.0.0.1:6099
}
gscore.example.com {
reverse_proxy 127.0.0.1:8765
}
Format, validate, and reload:
sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl restart caddy
sudo systemctl status caddy
Caddy itself started just fine, but the logs kept showing challenge failed. The root cause was not in Caddy at all. I had forgotten to add the DNS records in Cloudflare.
That type of failure usually comes down to one of these:
80 and 443 are not publicly reachable80 or 443Check recent Caddy logs:
sudo journalctl -u caddy -n 80 --no-pager
Check DNS resolution:
dig +short napcat.your-domain.com A
dig +short gscore.your-domain.com A
Check what is listening:
ss -lntp | grep ':80\|:443'
Check the firewall:
sudo ufw status
If the DNS record simply does not exist in Cloudflare, ACME validation obviously cannot succeed. It is a basic mistake, but also an extremely common one.
If the admin panels are mostly for your own use, I would tighten things up this way:
6099 or 8765 directly to the internet80 and 443Caddy in front of themOne point is worth stating explicitly: your email address is not an authentication mechanism. It is only a contact address for certificate issuance. The things that actually protect your admin panels are HTTPS, reverse proxy boundaries, panel passwords, tokens, and whether you avoided exposing the raw upstream ports.
Once everything is deployed, I recommend starting and debugging in this order:
NapCatNoneBot2GsCoreCaddyAnd when something breaks, trace the same chain in order:
Is QQ login working?
-> Is the OneBot WebSocket connected?
-> Is NoneBot running?
-> Is GsCore connected?
-> Is the plugin loaded?
-> Is the admin panel reachable?
-> Are HTTPS, DNS, and Caddy behaving correctly?
Check LANG, LC_ALL, and TERM before assuming the software is broken.
nb command usually just means nb-cli is not installedAnd if you are already managing your bot with a custom bot.py, direct pip install may be the simpler path anyway.
Once you see localhost bindings or “mounted locally” warnings, stop guessing at public-network issues and fix the binding model first.
Even in a stack with reverse proxies, certificates, and HTTPS, the most common failure point is still the boring layer underneath: DNS.
By the end of this deployment, the biggest takeaway was not “the bot is finally installed.” It was that the hard part of this kind of work is rarely the install commands themselves. What determines whether the setup stays pleasant later is whether the boundaries are clear: whether the protocol layer is separated from the framework, whether admin surfaces are exposed carelessly, whether the domains really point where you think they do, and whether the logs tell you where the failure actually is.
Once those boundaries are in place, NapCat + NoneBot2 + GsCore + Caddy is a very workable combination. It is not the lightest stack, and it is not the flashiest one either, but it is clear, stable, and perfectly suited to long-term maintenance.