Standalone native OpenClaw channel plugin for NapCat / OneBot v11.
This plugin is built for OpenClaw-native channel semantics first, with NapCat / QQ transport underneath. Forward WebSocket is the primary deployment path. Reverse WebSocket and HTTP fallback are implemented in the same plugin.
Supported in v1:
- Private chat ingress and replies
- Group chat ingress and
@botreplies - DM access control with
pairing,allowlist,open, anddisabled - Group access control with mention gating, allowlists, and per-group overrides
- Outbound text replies
- Outbound QQ image messages from direct image URLs or Markdown image links in model output
- Forward WebSocket transport
- Reverse WebSocket transport
- HTTP fallback transport
- Self-message suppression, event dedupe, and forward WebSocket reconnect
- Group patrol for selected groups
Implemented but not promised as a stable v1 contract:
- More advanced media and file workflows
- QQ-specific moderation actions
- Reactions
- Guild or channel variants outside plain OneBot v11 private and group messages
If you need a full QQ platform SDK, this is the wrong thing. This plugin is a production-focused OpenClaw channel transport for the common message path.
- Node.js 22 or newer
- OpenClaw installed and working
- NapCat configured with OneBot v11 enabled
- An OpenClaw agent with valid provider auth if you expect real model replies
Install dependencies and build the plugin:
npm install
npm run buildLink the plugin into OpenClaw:
openclaw plugins install --link ./Inspect that OpenClaw sees the plugin as a channel plugin:
openclaw plugins inspect napcat --runtime --jsonForward WebSocket is the default and recommended mode.
In this mode:
- NapCat exposes a OneBot v11 WebSocket endpoint
- This plugin connects to NapCat
Add a channel account:
openclaw channels add \
--channel napcat \
--account main \
--name qq-main \
--http-host 127.0.0.1 \
--http-port 3001 \
--webhook-path /onebot/v11 \
--token YOUR_NAPCAT_TOKENImportant: OpenClaw's generic setup flags use transport-neutral names. In this plugin they map like this:
--http-host->wsHost--http-port->wsPort--webhook-path->wsPath--token->accessToken
Then start the gateway:
openclaw gateway runVerify channel status:
openclaw channels list --allExpected result for a configured account looks like this:
NapCat / OneBot v11 main (qq-main): installed, configured, enabled
For forward WebSocket, configure NapCat so its OneBot v11 endpoint is available at the same host, port, path, and token used by this plugin.
Default values used by this plugin are:
- Host:
127.0.0.1 - Port:
3001 - Path:
/onebot/v11
Do not assume your NapCat build uses the same path by default. If NapCat is configured for /onebot/v11/ws, /onebot, or something else, change wsPath to match reality.
Transport can be healthy while replies still fail.
If the gateway logs an error like Missing API key for provider "openai", the NapCat transport is already working. What is broken is your OpenClaw agent auth. Configure provider auth for the target agent before calling this a transport failure.
A single account can be configured directly under channels.napcat:
{
"channels": {
"napcat": {
"name": "qq-main",
"enabled": true,
"wsMode": "forward",
"wsHost": "127.0.0.1",
"wsPort": 3001,
"wsPath": "/onebot/v11",
"accessToken": "change-me",
"ignoreSelfMessage": true,
"dmPolicy": "pairing",
"groupPolicy": "open",
"groupRequireMention": true
}
}
}You can also define named accounts under channels.napcat.accounts.
Top-level fields act as shared defaults for named accounts. If you also want a real inline default account in addition to named accounts, set a top-level name.
{
"channels": {
"napcat": {
"groupRequireMention": true,
"ignoreSelfMessage": true,
"accessToken": "shared-token",
"accounts": {
"main": {
"name": "qq-main",
"wsMode": "forward",
"wsHost": "127.0.0.1",
"wsPort": 3001,
"wsPath": "/onebot/v11"
},
"ops": {
"name": "qq-ops",
"wsMode": "reverse",
"wsHost": "0.0.0.0",
"wsPort": 6700,
"wsPath": "/onebot/v11"
}
}
}
}
}Default-account rules:
- If
accountsis absent, the inline config is the default account - If
accountsexists and also containsdefault, that nameddefaultaccount is the default account - If
accountsexists and top-levelnameis set, the inline config becomes the default account and named accounts are additional accounts - If
accountsexists but top-levelnameis not set, no syntheticdefaultaccount is created; the first named account becomes the effective default account
That last rule matters because production systems should not invent ghost accounts from schema defaults.
Default mode.
{
"wsMode": "forward",
"wsHost": "127.0.0.1",
"wsPort": 3001,
"wsPath": "/onebot/v11",
"accessToken": "change-me"
}Use this when NapCat exposes a WebSocket server and OpenClaw should connect out to it.
{
"wsMode": "reverse",
"wsHost": "0.0.0.0",
"wsPort": 6700,
"wsPath": "/onebot/v11",
"accessToken": "change-me"
}Use this when OpenClaw should host a WebSocket endpoint and NapCat should connect in.
{
"wsMode": "http",
"wsHost": "127.0.0.1",
"wsPort": 5701,
"wsPath": "/onebot/v11",
"httpBaseUrl": "http://127.0.0.1:3000",
"accessToken": "change-me"
}In HTTP mode:
- OpenClaw listens for inbound OneBot POST events on
wsHost:wsPort/wsPath - OpenClaw sends outbound actions to NapCat using
httpBaseUrl
httpBaseUrl should point at NapCat's HTTP API root, not at OpenClaw.
dmPolicy supports:
pairing: require OpenClaw pairing unless sender is already approvedallowlist: allow only explicitly approved sendersopen: allow everyonedisabled: deny all DMs
allowFrom is the DM allowlist.
groupPolicy supports:
allowlistopendisabled
groupRequireMention defaults to true.
That default is intentional. A production QQ group should not get ambient bot replies unless you explicitly choose that behavior.
groupAllowFrom restricts who may trigger replies when groupPolicy is allowlist.
Per-group overrides live under groups.<groupId>:
{
"groups": {
"123456789": {
"enabled": true,
"requireMention": false,
"allowFrom": ["10001", "10002"]
}
}
}Patrol is intentionally narrow. It only runs for groups listed in monitorGroups.
Relevant options:
monitorGroups: group IDs eligible for patrolautoIntervene: enable or disable patrolautoCheckIntervalMs: periodic patrol intervalautoCheckMessageThreshold: patrol after this many buffered messagesautoIntervenePrompt: patrol instruction templatenoReplyToken: exact token the model must return when nothing should be sent
Minimal example:
{
"monitorGroups": ["123456789"],
"autoIntervene": true,
"autoCheckIntervalMs": 300000,
"autoCheckMessageThreshold": 20,
"noReplyToken": "__NAPCAT_NO_REPLY__"
}If the model returns the configured noReplyToken exactly, the plugin suppresses the patrol reply.
The outbound adapter is intentionally simple:
- Plain reply text is sent as a QQ text message
mediaUrl,mediaUrls, and Markdown image links likeare extracted and sent as QQ image messages- Duplicate image URLs are removed
- Empty text created by stripping Markdown image links is cleaned up before sending
If you expect rich QQ-native cards, reactions, or custom structured payloads everywhere, that is outside the stable v1 contract.
Defaults are conservative:
ignoreSelfMessage: true- forward WebSocket reconnect is enabled
- inbound event dedupe is enabled
- group mention is required by default
- patrol is disabled by default
Optional inbound event logging:
inboundLogEnabledinboundLogDirinboundLogMaxLines
Before you call this deployed, verify all of this:
- NapCat and OpenClaw agree on mode, host, port, path, and token
- The target OpenClaw agent has provider auth configured
groupRequireMentionis left enabled unless you deliberately want ambient group repliesdmPolicyandgroupPolicymatch your safety modelmonitorGroupscontains only groups you actually want patrol to watch- Gateway auth token is persisted instead of relying on a startup-generated runtime token
- Reverse WebSocket or HTTP listen addresses are not exposed wider than necessary
- Inbound logging is disabled unless you truly need it and have a retention plan
Usually one of these is wrong:
wsModewsHostwsPortwsPathaccessToken
Forward WebSocket reconnect loops are a symptom, not a feature.
Check these first:
groupRequireMentionmay still betrue- the message may not actually mention the bot account
groupPolicymay bedisabledorallowlistgroups.<groupId>may override the default behavior
That means transport is fine and your OpenClaw agent auth is not.
Check httpBaseUrl. It must point at NapCat's OneBot HTTP API root.
Read the default-account rules above. The plugin will not create fake default accounts from schema defaults.
Run the full local check:
npm run checkThis runs TypeScript build plus the automated test suite.
src/index.ts: plugin entrypointsrc/channel.ts: channel definition and OpenClaw integrationsrc/config.ts: config resolution, setup mapping, and policy helperssrc/controller.ts: runtime dispatch, routing, transport lifecycle, and patrolsrc/transports.ts: forward WS, reverse WS, and HTTP transport adapterssrc/normalize.ts: OneBot v11 inbound normalizationsrc/outbound.ts: outbound text and image planningopenclaw.plugin.json: checked-in manifest and config schema
This repository targets the OpenClaw plugin contract used by openclaw@2026.5.27 and Node.js 22+.