QQ Bot connects to Apus via the official QQ Bot API (WebSocket gateway). C2C private chat and group @-mentions are the primary chat types, with rich media (images, voice, video, files). Guild channel messages are supported for text and remote-URL images only; voice, video, file uploads, and local/Base64 images are not available in guild channels. Reactions and threads are not supported anywhere.

Status: official downloadable plugin.

Install

apus plugins install @tencent-connect/apus-qqbot

If the bot was installed as @openclaw/qqbot under plugin id qqbot, apus plugins update qqbot and apus update rewrite it to @tencent-connect/apus-qqbot under plugin id apus-qqbot. Channel config stays under channels.qqbot.

Setup

  1. Go to the QQ Open Platform and scan the QR code with your phone QQ to register / log in.
  2. Click Create Bot to create a new QQ bot.
  3. Find AppID and AppSecret on the bot's settings page and copy them.

Save the AppSecret before leaving the QQ Open Platform page; otherwise, you will need to regenerate it.

  1. Add the channel:
apus channels add --channel qqbot --token "AppID:AppSecret"
  1. Check apus channels status --probe; start the Gateway if it is offline. Config changes follow hot reload.

Inbound durability

For QQ gateway turn events, Apus persists the raw event before advancing the saved gateway resume sequence. Pending or retryable turns survive a Gateway restart, remain serialized per conversation, and use the provider event ID to suppress duplicate queue entries while the active or retained completion record exists.

If durable admission fails, Apus terminates the current gateway socket without advancing the sequence. The reconnect/resume path can then request the uncommitted event again. Delivery is still at least once across the queue-to-agent boundary, so a crash during handoff can replay a turn.

Interactive setup:

apus channels add

The wizard also offers QR-code binding as an alternative to typing AppID/AppSecret manually: scan the code with the phone app tied to the target QQ Bot to complete binding. Apus persists the returned credentials under the account's config scope.

Configure

Minimal config:

{
  channels: {
    qqbot: {
      enabled: true,
      dmPolicy: "open",
      allowFrom: ["apus:approval-disabled"],
      appId: "YOUR_APP_ID",
      clientSecret: "YOUR_APP_SECRET",
    },
  },
}

These examples keep direct messages open with dmPolicy: "open" while the apus:approval-disabled marker leaves native approval actions disabled. To enable those actions, replace the marker with specific approver QQ OpenIDs.

Default-account env vars (top-level account only):

Use a SecretRef to keep the AppSecret out of the channel config. For an environment-backed secret:

{
  channels: {
    qqbot: {
      enabled: true,
      dmPolicy: "open",
      allowFrom: ["apus:approval-disabled"],
      appId: "YOUR_APP_ID",
      clientSecret: {
        source: "env",
        provider: "default",
        id: "QQBOT_CLIENT_SECRET",
      },
    },
  },
}

For a file-backed AppSecret, configure a singleValue file provider:

{
  secrets: {
    providers: {
      qqbot_secret: {
        source: "file",
        path: "/path/to/qqbot-secret.txt",
        mode: "singleValue",
      },
    },
  },
  channels: {
    qqbot: {
      enabled: true,
      dmPolicy: "open",
      allowFrom: ["apus:approval-disabled"],
      appId: "YOUR_APP_ID",
      clientSecret: { source: "file", provider: "qqbot_secret", id: "value" },
    },
  },
}

Notes:

Streaming

{
  channels: {
    qqbot: {
      streaming: {
        mode: "partial", // block streaming: "partial" (default) or "off"
        nativeTransport: true, // use QQ's official C2C stream_messages API for DMs
      },
    },
  },
}

Access policy

Multi-account setup

Run multiple QQ bots under a single Apus instance:

{
  channels: {
    qqbot: {
      enabled: true,
      dmPolicy: "open",
      allowFrom: ["apus:approval-disabled"],
      appId: "111111111",
      clientSecret: { source: "env", provider: "default", id: "QQBOT_DEFAULT_SECRET" },
      accounts: {
        bot2: {
          enabled: true,
          dmPolicy: "open",
          allowFrom: ["apus:approval-disabled"],
          appId: "222222222",
          clientSecret: { source: "env", provider: "default", id: "QQBOT_BOT2_SECRET" },
        },
      },
    },
  },
}

Each account owns an isolated WebSocket connection, API client, and token cache, keyed by appId. Log lines are tagged with the owning account id so diagnostics stay separable when you run several bots under one Gateway.

At startup, a missing SecretRef value makes only its account unavailable; healthy siblings and the Gateway remain available. An explicit failed reference does not fall back to environment or plugin backup credentials. Malformed refs and unknown secret providers still fail startup or reload. On reload, an unchanged account can retain its last-known-good credential; see Secrets runtime model.

Add a second bot via CLI:

apus channels add --channel qqbot --account bot2 --token "222222222:secret-of-bot-2"

Group chats

Group support uses QQ group OpenIDs, not display names. Add the bot to a group, then mention it or configure the group to run without a mention.

{
  channels: {
    qqbot: {
      groupPolicy: "allowlist",
      groupAllowFrom: ["member_openid"],
      groups: {
        "*": {
          requireMention: true,
          commandLevel: "all",
          historyLimit: 50,
          tools: { deny: ["exec", "read", "write"] },
        },
        GROUP_OPENID: {
          name: "Release room",
          requireMention: false,
          ignoreOtherMentions: true,
          commandLevel: "safety",
          historyLimit: 20,
          prompt: "Keep replies short and operational.",
        },
      },
    },
  },
}

groups["*"] sets defaults for every group; a concrete groups.GROUP_OPENID entry overrides those defaults for one group. Group settings:

Field Default Description
requireMention true Require an @-mention before the bot replies.
commandLevel all Which built-in slash commands can run in the group (see below).
ignoreOtherMentions false Drop messages that mention someone else but not the bot.
historyLimit 50 Recent non-mention messages kept as context for the next mentioned turn. 0 disables history.
tools — Allow/deny tools for the whole group.
toolsBySender — Per-sender tool overrides; see Groups.
name openid prefix Friendly label used in logs and group context.
prompt built-in default Per-group behavior prompt appended to the agent context.

commandLevel accepts:

Level Behavior
all Existing built-in commands stay available. Some stay hidden from menus but authorized users can still run them in the group.
safety /help, /btw, /stop stay visible in the group; sensitive commands (/config, /tools, /bash, etc.) must be run in private chat.
strict Only group-session controls needed for strict operation are allowed. /stop still works so an authorized sender can interrupt an active run.

Old QQBot toolPolicy entries are retired. Run apus doctor --fix to migrate them to tools.

Activation modes are mention and always. requireMention: true maps to mention; requireMention: false maps to always. A session-level activation override, when present, wins over config.

The inbound queue is per peer. Group peers get a larger queue cap (50 vs. 20 for direct peers), evict bot-authored messages before human ones when full, and merge bursts of normal group messages into one attributed turn. Slash commands run one by one, independent of any merge batch.

Voice (STT / TTS)

STT and TTS support two-level configuration with priority fallback:

Setting Plugin-specific Framework fallback
STT channels.qqbot.stt first audio-capable tools.media.models[] entry
TTS channels.qqbot.tts, channels.qqbot.accounts.<id>.tts tts
{
  channels: {
    qqbot: {
      stt: {
        provider: "your-provider",
        model: "your-stt-model",
      },
      tts: {
        provider: "your-provider",
        model: "your-tts-model",
        voice: "your-voice",
      },
      accounts: {
        "qq-main": {
          tts: {
            providers: {
              openai: { voice: "shimmer" },
            },
          },
        },
      },
    },
  },
}

Set enabled: false on either to disable. Account-level TTS overrides use the same shape as tts and deep-merge over channel/global TTS config.

STT requests time out after 60 seconds by default. Plugin-specific STT uses the selected models.providers.<id>.timeoutSeconds override. Framework audio STT uses the selected audio-capable tools.media.models[] entry's timeoutSeconds, then the selected provider override.

Inbound QQ voice attachments are exposed to agents as audio media metadata while keeping raw voice files out of generic MediaPaths. [[audio_as_voice]] in a plain-text reply synthesizes TTS and sends a native QQ voice message when TTS is configured.

Outbound audio upload/transcode behavior can also be tuned with channels.qqbot.audioFormatPolicy:

Target formats

Format Description
qqbot:c2c:OPENID Private chat (C2C)
qqbot:group:GROUP_OPENID Group chat
qqbot:channel:CHANNEL_ID Guild channel

Each bot has its own set of user OpenIDs. An OpenID received by Bot A cannot be used to send messages via Bot B.

Slash commands

Built-in commands intercepted before the AI queue:

Command Auth Scope Description
/bot-ping — any Latency test
/bot-help — any List all commands
/bot-me — private only Show the sender's QQ user ID (openid) for allowFrom / groupAllowFrom setup
/bot-version — private only Show the Apus framework version and plugin version
/bot-upgrade — private only Show the QQBot upgrade guide link
/bot-approve allowlist private only Manage command-execution approval config (on / off / always / reset / status)
/bot-logs allowlist private only Export recent gateway logs as a file
/bot-clear-storage allowlist private only Delete cached downloads under the QQBot media directory
/bot-streaming allowlist private only Toggle C2C streaming replies
/bot-group-allways allowlist private only Toggle the default group activation mode (mention-required vs. always-on)

Append ? to any command for usage help (for example /bot-upgrade ?).

"Auth: allowlist" commands additionally require the sender's openid in an explicit non-wildcard allowFrom list (groupAllowFrom takes precedence for group-issued commands, falling back to allowFrom). A wildcard allowFrom: ["*"] permits chat but not these commands. Running one of them outside private chat, or without authorization, returns a hint rather than silently dropping the message.

/bot-me, /bot-version, and /bot-upgrade are private-chat-only but do not require the allowlist — any C2C sender can run them.

When QQ Bot exec approvals use the default same-chat fallback, native approval button clicks follow the same explicit non-wildcard command allowlist. To grant approval-only access without broader command access, configure channels.qqbot.execApprovals.approvers. Native exec approvals are enabled by default.

Media and storage

Troubleshooting

Related