Standalone CLI and daemon for NapCat QQ bot management with skills-fs integration.
介绍博文: napcat-cli 群配置与管理速查 — 从零配置、群消息收发、Agent Wake 的完整链路。
napcat-cli provides a CLI for all NapCat API operations, a WebSocket daemon that bridges to a skills-fs HTTP provider, and an agent wake-up mechanism for integrating with Hermes or other AI agents.
napcat send group 123456 -m "Hello"
napcat send private 987654 -m "Hi"
napcat recall 1001
napcat events --limit 10
napcat daemon start
napcat wake --reason NEW_MESSAGE| Command | Description |
|---|---|
napcat api <endpoint> |
Raw API access (like gh api) |
napcat send group <id> -m "msg" |
Send group message |
napcat send private <id> -m "msg" |
Send private message |
napcat reply <id> -m "msg" |
Reply to a message |
napcat recall <msg_id> |
Recall a message |
napcat group <sub> |
Group list, members, settings |
napcat friend <sub> |
Friend list, info |
napcat file <sub> |
File upload, download, list |
napcat events |
Read events from SQLite |
napcat alerts [--clear] |
Pending alerts |
napcat status |
Bot online status |
napcat config get/set |
Configuration management |
napcat daemon start/stop/status/restart |
Watch daemon |
napcat fs tree |
Skills-fs directory tree |
napcat wake [--reason R] [--prompt P] [--transport T] [--dry-run] |
Wake the configured agent (HTTP/CLI, auto-fallback) |
napcat setup |
Interactive setup wizard |
napcat phone |
Textual phone-style TUI |
uv tool install napcat-cli # recommended — isolated install via uv (alternative: pip install napcat-cli)
napcat setup # interactive — guides token, data dir, skills-fs, wake
napcat daemon startWhat you need before setup works end-to-end:
- A running NapCat instance with HTTP API (default
http://127.0.0.1:18801) and WebSocket (default18800). - Optional:
skills-fsbinary onPATH(or built from theskills-fs/submodule) for the FUSE tree. Without it, CLI + wake still work; setup disables FUSE automatically. - Optional: Hermes (or any HTTP/CLI agent) for auto-reply wakes.
The setup wizard writes:
<data_dir>/config.json— API URL, token, self_id, ports, triggers<data_dir>/daemon.json— fields consumed bywatch.pyincludingskills_fs_*<data_dir>/skills-fs.json— full FUSE mount map with thenapcatHTTP provider (installed from the package if missing; existing/symlink configs are never overwritten)
Non-interactive mode uses defaults:
napcat setup --non-interactive # no prompts, validates token
napcat setup --yes # skip token validation
napcat setup --force # overwrite existing config.json (skills-fs.json only if broken)Full operator reference (OneBot11 empty network, Docker port cross-mapping,
WS ?access_token=, wake HTTP vs legacy CLI, PaddleOCR venv, skills-fs health):
napcat config help
# or read docs/CONFIGURATION.mdNapCat ships with OneBot11 disabled by default; you must enable it inside the
container config /app/napcat/config/onebot11_<self_id>.json — otherwise the
napcat-cli WS/HTTP connections get an immediate reset (Connection reset by peer / Not logged in), even when the QQ account itself is online.
Minimum worker config (see the bundled example
napcat_cli/data/onebot11.example.json):
- One HTTP server on container port
3000(host: any free port). - One WebSocket server on container port
3001(host: any free port). token: <random_secret>on both — keep it synchronised withnapcat-cliconfig (napcat config set token <value>). Empty token = unauthenticated.- For Docker deployments, also enable quick auto-login in
/app/napcat/config/webui.json("autoLoginAccount": "<self_id>") so a container restart uses the saved session instead of presenting a new QR code.
Docker cross-mapping note. Your
docker run -p HOST:CONTAINERmapping can transpose host and container ports. With NapCat defaults (HTTP3000, WS3001), many setups publish18800:3000and18801:3001. That means the host-side OneBot11 HTTP URL ishttp://127.0.0.1:18800and the WS URL isws://127.0.0.1:18801— note the swap relative to the conventional napcat-cli defaults (api_url=...18801,ws_port=18800). Ifnapcat statussays "Connection reset by peer" but the bot appears online in the WebUI, checkdocker port napcatand alignapi_url/ws_url/ws_portwith the actual host mapping:napcat config set api_url http://127.0.0.1:18800 # host HTTP napcat config set ws_url ws://127.0.0.1:18801 # host WS (overrides ws_port) napcat config set token <the-onebot11-token> napcat daemon start
~/.napcat-data/{config,daemon}.json are user-state files and are gitignored.
Reference copies with <SET_ME> / REPLACE_ME_WITH_RANDOM_SECRET placeholders
live in the repo for orientation:
napcat_cli/data/config.json.example— napcat-cli client (api_url, token, ws_url, ...).napcat_cli/data/daemon.json.example— daemon (watch.pyruntime config).napcat_cli/data/onebot11.example.json— NapCat container-side OneBot11 worker.
napcat daemon start prints advisory warnings if the token is empty or
OneBot URLs look mis-mapped; it never blocks startup. Run napcat daemon start
after changing any of these via napcat config set ....
When a notable QQ event arrives, the daemon wakes an external agent (Hermes by default) carrying a contextual prompt so it can read the inbox and reply. The wake mechanism is generic and pluggable — Hermes is just the default preset; any HTTP endpoint or shell command works.
QQ Event (WS) --> EventProcessor --> WakeOrchestrator (debounce/cooldown/queue)
|
Waker (auto-fallback)
HTTP API (RECOMMENDED) CLI (LEGACY — 不推荐)
POST /chat hermes -z "..."
fixed session_id --continue <session>
- Debounce (
wake_debounce_seconds, default 3): coalesces a burst of same-reason events into one wake. - Cooldown (
wake_cooldown_seconds, default 30): suppresses repeat wakes for non-urgent reasons. - Serial queue:
WakeOrchestratorhas a single worker thread — wakes to the same session are processed one at a time, preventing concurrent "split personality" responses. - Timeout:
wake_timeout(default 300s) — how long to wait for the agent to respond.
Every wake is logged to daemon.log:
[WAKE] trigger reason=AT_ME who=Alice where=group123 text='hello'
[WAKE] queued reason=AT_ME pending=1 debounce=1.0s primary=http
[WAKE] delivered reason=AT_ME transport=http detail=POST /api/sessions/.../chat -> 200
| Trigger | Behavior |
|---|---|
AT_ME, REPLY_TO_ME, DM_ME |
Near-immediate wake (cooldown bypassed). Prompt includes who/where/text, image metadata, reply chain, and skill hints. |
GROUP_TRIGGER |
Debounced wake (group trigger-word match) |
NEW_MESSAGE (not @) |
Tracked, not woken; if unread longer than wake_new_message_idle_seconds -> NEW_MESSAGE_BACKLOG wake |
NEW_FRIEND, NEW_REQUEST, BOT_BANNED, NEW_POKE, GROUP_ADMIN_CHANGE, NEW_GROUP_MEMBER, BOT_OFFLINE, ... |
Debounced + cooldown-bounded wake |
AT_ME detection supports all NapCat message formats: CQ code ([CQ:at,qq=...]), display name (@name (qq)), and message segments ({"type":"at","data":{"qq":"..."}}).
Wake transport recommendation. The HTTP API server is the recommended transport for agent wake. The CLI one-shot backend is kept as a legacy fallback for environments where running the API server isn't viable — it is known to be less reliable (process-spawn latency, quoting hazards, new-session drift, no idempotency). When
wake_primary=httpfalls back to CLI because HTTP is unreachable, the wake may appear to succeed but never deliver the prompt (only HTTP returns a realdeliveredindaemon.log). Enable the HTTP API server first — only keep CLI for diagnostic--dry-runor as last-resort fallback. SeeAGENTS.md"Critical Lessons —hermes -z -" for the most common CLI failure mode.
| Transport | Status | When it picks | Needs |
|---|---|---|---|
| HTTP API server | RECOMMENDED | primary=http or primary=http when configured + reachable |
wake_http_url + wake_http_key (bearer) |
| CLI one-shot | LEGACY / 不推荐 | primary=cli (forced) or primary=http only if HTTP is not configured/unreachable as a last-resort fallback |
Agent CLI on PATH |
wake_primary=http tries HTTP first (if configured + reachable), else falls back to CLI.
The HTTP API server is the recommended wake transport. It provides:
- Session continuity: all wakes go to the same session (no "split personality")
- Idempotency:
Idempotency-Keyheader prevents duplicate processing on retry - Persistence: session history is stored in
~/.hermes/state.db - Lower latency: no CLI process spawn overhead
Step 1: Enable Hermes API Server
Add to ~/.hermes/.env:
API_SERVER_ENABLED=true
API_SERVER_KEY=$(openssl rand -hex 32) # generate a random keyRestart the gateway:
sudo systemctl restart hermes-gateway.service
# Verify API server is listening:
curl http://127.0.0.1:8642/healthStep 2: Create a dedicated QQ session
curl -X POST http://127.0.0.1:8642/api/sessions \
-H "Authorization: Bearer <API_SERVER_KEY>" \
-H "Content-Type: application/json" \
-d '{"title": "napcat-qq"}'
# Response: {"session": {"id": "api_1784824379_59dd9495", ...}}Step 3: Configure napcat-cli
napcat config set wake_enabled true
napcat config set wake_preset hermes
napcat config set wake_primary http # try HTTP first, CLI fallback
napcat config set wake_http_url http://127.0.0.1:8642
napcat config set wake_http_key <API_SERVER_KEY>
napcat config set wake_http_session_id <SESSION_ID> # from Step 2
napcat config set wake_session napcat-qq # CLI fallback session name
napcat config set wake_timeout 300 # secondsOr edit ~/.napcat-data/daemon.json directly:
{
"wake_enabled": true,
"wake_preset": "hermes",
"wake_primary": "http",
"wake_http_url": "http://127.0.0.1:8642",
"wake_http_key": "<API_SERVER_KEY>",
"wake_http_session_id": "<SESSION_ID>",
"wake_timeout": 300.0,
"wake_cli_command": "hermes --continue {session} -z \"$(cat {prompt_file})\" --yolo --pass-session-id"
}Key env vars (also checked by Hermes preset, in priority order):
NAPCAT_WAKE_HTTP_KEY— HTTP API bearer tokenHERMES_API_KEY— alias for the above
Not recommended. Prefer Option A (HTTP). The CLI backend is kept for environments where running the API server isn't viable, but it has multiple known failure modes (process-spawn latency, quoting hazards, no idempotency, new-session drift). When
wake_primary=httpfalls back to CLI after an HTTP probe fails, the wake often appears to succeed but never delivers the prompt to the agent. Diagnose by runninggrep '\[WAKE\]' ~/.napcat-data/daemon.log— only the HTTP backend writes adelivered ... transport=http -> 200line. For agents other than HTTP, configure Option A first; only fall back to CLI when there is no other choice.
If you don't want to run the API server, napcat-cli can invoke the Hermes CLI
directly. The CLI backend writes the prompt to a temp file and passes it via
$(cat {prompt_file}) to avoid shell quoting issues.
napcat config set wake_primary cli
napcat config set wake_session napcat-qq
napcat config set wake_cli_command 'hermes --continue {session} -z "$(cat {prompt_file})" --yolo --pass-session-id'Limitations of CLI mode:
- Each invocation may create a new session if the name doesn't match an existing one
- No idempotency — retries may cause duplicate processing
- ~30-60s per invocation (CLI process startup + model inference)
- Large prompts with special characters require the
$(cat {prompt_file})pattern (not-z -, which Hermes treats as literal text "-")
napcat-cli's wake system is agent-agnostic. Any HTTP endpoint or shell command works.
napcat config set wake_preset custom
napcat config set wake_primary http
napcat config set wake_http_url http://127.0.0.1:9000
napcat config set wake_http_key my-secret-key
# The HTTP backend POSTs to: {wake_http_url}/api/sessions/{session}/chat
# Body: {"input": "<prompt>"}
# Headers: Authorization: Bearer <key>, Idempotency-Key: <unique-id>
# If your endpoint uses a different path/body format, see wake_backend.py HttpWakeBackendNot recommended. The shell-command backend is kept for legacy / experimental setups only. Prefer Custom HTTP endpoint above — see the "Two transports — HTTP recommended, CLI legacy" note at the top of this section for why.
napcat config set wake_preset custom
napcat config set wake_primary cli
napcat config set wake_cli_command 'my-agent --prompt "$(cat {prompt_file})"'
# Placeholders: {prompt_file} (temp file path), {session}, {prompt} (inline, shell-quoted)napcat config set wake_preset nonenapcat wake # reason: manual, contextual default prompt
napcat wake --reason AT_ME --prompt "hello"
napcat wake --transport http # recommended; --transport cli is legacy
napcat wake --dry-run # render the HTTP request + CLI command without executing
napcat wake test # per-transport configured + reachable probe
napcat wake sessions # list Hermes sessions (HTTP backend)
grep '\[WAKE\]' ~/.napcat-data/daemon.log # see when/why/how wakes firedThe agent replies via napcat send / napcat reply (or the skills-fs write
path) — see napcat_cli/data/SKILL.md.
-
Check daemon is running:
ps aux | grep watch.py | grep -v grep tail -5 ~/.napcat-data/daemon.log # should show recent events
-
Check Hermes API server is listening:
curl -s http://127.0.0.1:8642/health # Expected: {"status": "ok", ...} -
Test HTTP wake manually:
curl -X POST http://127.0.0.1:8642/api/sessions/<SESSION_ID>/chat \ -H "Authorization: Bearer <KEY>" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: test-001" \ -d '{"input": "ping"}' # Expected: {"message": {"content": "..."}} with a response
-
Test CLI wake manually (legacy — prefer HTTP test above):
CLI is legacy / not recommended. This reference is kept for diagnosing environments where the HTTP API server is genuinely unavailable. Only test CLI as a last resort — the HTTP test (step 3) is the one that matters.
echo "hello" > /tmp/prompt.txt hermes --continue napcat-qq -z "$(cat /tmp/prompt.txt)" --yolo --pass-session-id # Expected: agent responds within 30-60s # NOTE: -z - does NOT read stdin. Always use -z "$(cat file)" or -z "literal".
| Symptom | Cause | Fix |
|---|---|---|
| Bot doesn't respond to @mentions | Daemon not running, or crash killed event loop | ps aux | grep watch.py; check daemon.log for errors; restart daemon |
| Connection reset / Not logged in while QQ online | Empty OneBot11 network or port cross-map / missing ?access_token= |
napcat config help; enable WS+HTTP in container; align api_url/ws_url/token |
[WAKE] delivered transport=cli but no reply |
CLI is legacy and may exit 0 without delivering | Set wake_primary=http (default) + Hermes API_SERVER_KEY; expect /chat -> 200 |
[WAKE] delivered http but no reply |
Agent turn ran but did not send QQ | Check Hermes session/tools; model 502/fallback in ~/.hermes/logs/agent.log |
[WAKE] failed ... timeout |
Agent took > wake_timeout seconds |
Increase wake_timeout; HTTP turns can take 30–60s |
[WAKE] failed ... session resolution failed |
HTTP session ID not found | Run napcat wake sessions; recreate if needed |
PaddleOCR not installed |
Daemon system Python; package only in .test-venv |
Auto-bootstrap or NAPCAT_OCR_SITE_PACKAGES / NAPCAT_VENV — see napcat config help |
| skills-fs degraded | Health probe looked for root status |
Should use napcat/status + /proc/mounts; restart daemon on latest |
| Multiple replies ("split personality") | Concurrent CLI wakes | Prefer wake_primary=http |
hermes -z - sends literal "-" |
-z takes literal string |
Use -z "$(cat {prompt_file})" (legacy CLI only) |
| Events stop after a bad message | Unhandled exception in event handler | Check daemon.log for process() ERROR |
The test suite includes tests/test_lint.py which runs pyflakes and
py_compile on every file in napcat_cli/. This catches undefined names,
syntax errors, and other issues that would crash the daemon at runtime.
python -m pytest tests/test_lint.py -v
# Should pass: test_no_undefined_names, test_all_modules_compileThe wake prompt automatically includes image metadata when an image message is received:
file_id— image file IDurl— download URLsub_type— image type (0=normal, 1=sticker, 7=like)file_size— file size in bytessummary— PaddleOCR auto-recognized text (if available)reply_id— reply message ID (for tracking reply chains)
PaddleOCR is integrated. Image text is auto-recognized and included in the
summary field. The agent can also use multimodal vision to read the image URL
directly. NapCat's built-in OCR is unavailable — do not use /napcat/ocr.
| Operation | CLI | skills-fs |
|---|---|---|
| Download image | napcat get_image <url> |
/napcat/get_image |
| View message detail | napcat group <gid> get_message <mid> |
/napcat/groups/:gid/:range/:mid/:content |
| Send image | napcat send group <gid> -f <path> |
/napcat/groups/:gid/send/image |
| Variable | Default | Description |
|---|---|---|
NAPCAT_API_URL |
http://127.0.0.1:18801 |
NapCat HTTP endpoint |
NAPCAT_TOKEN |
— | API auth token |
NAPCAT_DATA_DIR |
~/.napcat-data |
Data directory |
The daemon runs an HTTP provider server (port 18820/18821) that skills-fs calls to read/write NapCat API endpoints through the virtual filesystem.
When skills_fs_enabled is true in daemon.json, the daemon spawns skills-fs
automatically with the configured mountpoint and config file.
Distribution note:
skills-fsis a compiled Go binary (git submodule pinned atcb13f37). It is not bundled in the pip/uv install ofnapcat-cli. If you need the FUSE mount (optional), build it separately:git clone https://github.com/yandu-app/skills-fs cd skills-fs && make build # produces ./skills-fs napcat config set skills_fs_binary /path/to/skills-fs napcat daemon stop && napcat daemon startOr place
skills-fson yourPATHand the daemon will find it. This feature is optional — the CLI, daemon, and wake system work without it.
Manual start:
skills-fs fuse --config ~/.napcat-data/skills-fs.json \
--mountpoint ~/.napcat-data/skills/napcat-cli --allow-othernapcat-cli/
├── napcat_cli/ # Installable package
│ ├── cli.py # CLI entry point
│ ├── wake.py # Wake command-template renderer
│ ├── wake_backend.py # Generic HTTP/CLI wake transports + Waker (auto-fallback)
│ ├── wake_presets.py # Hermes/custom/none presets -> Waker
│ ├── wake_orchestrator.py # Debounce, cooldown, backlog sweep, contextual prompts
│ ├── setup_wizard.py # Setup wizard
│ ├── daemon/ # Watch daemon, schemas
│ ├── lib/ # API, config, events
│ ├── tui/ # Textual TUI
│ └── data/ # SKILL.md, persona.md
├── pyproject.toml
├── tests/
├── skills-fs/ # Go submodule
└── tools/ # Dev utilities
python -m pytest tests/ -v
python -m build --wheel
uv build && uv publishMIT