Skip to content

QQ Voice Call Adapter

Add a real-time voice call entry to QQ (community-maintained). The QQ Voice Call Adapter adds a QQ real-time voice call entry to MaiBot: the QQ call media path (incoming calls, auto-answer, audio devices) is handled by a separate NapCat AV bridge, while persona, recent messages, memory lookups, and model routing stay inside the MaiBot plugin SDK—so Mai on the phone keeps using MaiBot's persona, memory, and model setup.

Connection direction

QQ / NapCat ← call signaling and native audio → NapCat AV bridge ← HTTP (loopback only + Bearer Token) → MaiBot plugin

The adapter does not connect to QQ directly: it polls the bridge status endpoint on the local loopback, and audio travels through virtual PulseAudio devices. The bridge only listens on loopback and authenticates the status/control endpoints with a Bearer Token. The repository does not ship QQ, NapCat, or libAVSDKPlugin.so.

Adapter repository (🌐 community-maintained):

maibot-qq-voice-callClaudiaGardner's QQ voice call adapter plugin

1. Prepare the runtime environment

Requirements

  • MaiBot1.0.0 or later; maibot-plugin-sdk 2.5.4 or later, below 3.0
  • Python — 3.12+
  • System — Linux, PulseAudio (or PipeWire's Pulse compatibility layer), plus pactl, parec, pacat, xvfb-run, curl
  • NapCat4.14.0 or later, with a Linux QQ that includes libAVSDKPlugin.so
  • DashScope — real-time ASR and real-time voice-clone TTS permissions
  • Model — a phone-reply model available in MaiBot model management (defaults to deepseek-v4-flash on the utils task)

Install system dependencies on Debian/Ubuntu, for example:

bash
sudo apt-get install pulseaudio pulseaudio-utils xvfb curl

Prepare credentials

Keys live in the MaiBot process environment—never write them into the repository or config.toml:

bash
export DASHSCOPE_API_KEY="..."
export MAIBOT_QQ_CALL_VOICE_ID="..."
  • DASHSCOPE_API_KEY — DashScope API key, shared by real-time ASR and real-time voice-clone TTS; this is the default env var name, changeable via asr.api_key_env / tts.api_key_env
  • MAIBOT_QQ_CALL_VOICE_ID — voice-clone ID for Qwen3 real-time TTS; you can also put it directly in tts.voice_id
  • MAIBOT_QQ_CALL_BRIDGE_TOKEN — bridge auth token; when unset, the adapter reads the token file pointed to by bridge.token_file (auto-generated by the installer, permissions 0600)

Install the QQ AV bridge

First install and confirm Linux QQ and NapCat can log in normally, then run the installer from the adapter repository root—--check first to self-test paths and compatibility, then install for real:

bash
./bridge/scripts/install.sh \
  --napcat-dir /path/to/QQ/resources/app/app_launcher/napcat \
  --qq-dir /path/to/QQ \
  --check

./bridge/scripts/install.sh \
  --napcat-dir /path/to/QQ/resources/app/app_launcher/napcat \
  --qq-dir /path/to/QQ

The installer:

  • Installs napcat-plugin-maibot-qq-voice-call into NapCat's separate plugins/ directory;
  • Installs the AV Host and run scripts into ~/.local/share/maibot-qq-voice-call (changeable with --install-dir);
  • Creates a random 32-byte bridge token with 0600 permissions;
  • Backs up QQ's original Loader, then installs a minimal reversible Hook tagged MAIBOT_QQ_CALL_LOADER_HOOK_V1—normal QQ processes keep loading the backed-up original entry, and only the second AV Host process loads the AVSDK.

Version-sensitive

The Loader Hook and AVSDK are version-sensitive QQ/NapCat integrations. After upgrading QQ or NapCat, re-run the installer and diagnostics, and verify with a test-account incoming call before switching back to the production account.

Start the bot QQ

After installation, fill the token file path printed by the installer into the adapter's bridge.token_file, then start bot QQ with the bridge script (it automatically brings up an isolated PulseAudio service and the AV Host):

bash
MAIBOT_QQ_CALL_BOT_UIN="BOT_QQ_NUMBER" \
  ~/.local/share/maibot-qq-voice-call/scripts/run-napcat.sh

The script creates a PulseAudio socket accessible only by the current user and provides three virtual devices to fill into the adapter config later:

  • maibot_qq_speaker.monitor — MaiBot's ASR input (the remote party's voice)
  • maibot_qq_mic — MaiBot's TTS output (the QQ microphone)
  • maibot_qq_mic_source — the default microphone source used by QQ

2. Configure the adapter connection

Start MaiBot, then fill in the plugin configuration in the WebUI; the Runner generates config.toml from it. Here is a complete, copy-ready config template (defaults match repository 0.3.4)—edit the values per the comments:

toml
[plugin]
enabled = true            # Enable the QQ voice call plugin
config_version = "0.3.4"  # Config structure version; usually leave it alone
account_id = "BOT_QQ_NUMBER"  # used for gateway status reporting
scope = "primary"             # MaiBot multi-account routing scope
log_transcripts = true        # log ASR text and replies

[bridge]
base_url = "http://127.0.0.1:6110"  # HTTP address of the NapCat AV call bridge
token_env = "MAIBOT_QQ_CALL_BRIDGE_TOKEN"  # env var holding the bridge token
token_file = "/INSTALL_DIR/runtime/control.token"  # token file; lower priority than the env var
poll_interval_seconds = 0.25  # call status poll interval (sec)
request_timeout_seconds = 5.0 # bridge request timeout (sec)

[audio]
pulse_server = "unix:/INSTALL_DIR/runtime/pulse/native"  # PulseAudio address; empty inherits process env
capture_device = "maibot_qq_speaker.monitor"  # ASR input (remote party's voice)
playback_device = "maibot_qq_mic"             # TTS output (QQ microphone)
sample_rate = 16000       # ASR input sample rate
frame_ms = 30             # voice activity detection frame length (ms)
end_of_speech_frames = 18 # silent frames required to decide speech has ended
barge_in_speech_frames = 18  # speech frames required to interrupt TTS
min_utterance_seconds = 0.7  # minimum utterance length (sec)
min_speech_seconds = 0.45    # minimum valid speech within an utterance (sec)

[asr]
backend = "dashscope-realtime"  # dashscope-realtime / maibot
api_key_env = "DASHSCOPE_API_KEY"
model = "qwen3-asr-flash-realtime"
websocket_base_url = "wss://dashscope.aliyuncs.com/api-ws/v1/realtime"
final_timeout_seconds = 1.5  # sentence-final result wait timeout (sec)

[chat]
task_name = "utils"       # MaiBot model task name; utils defaults to deepseek-v4-flash
temperature = 0.2         # call reply temperature
max_tokens = 128          # max call reply tokens
max_reply_chars = 80      # max reply chars before TTS
history_messages = 8      # history messages kept inside the call
context_recent_messages = 4  # recent QQ messages to read
context_message_chars = 120  # max chars per recent message
context_memory_chars = 1000  # max persona memory length
context_prompt_chars = 2400  # max caller context length
contextual_greeting_enabled = true  # model-generated contextual greeting when context exists
greeting_timeout_seconds = 2.5  # greeting generation timeout (sec)
greeting = ""             # fixed greeting when no context, generation fails, or timeout
system_prompt = ""        # phone-mode system prompt (model may use [WAIT] to skip unreliable input)

[tts]
backend = "dashscope-realtime"
api_key_env = "DASHSCOPE_API_KEY"
model = "qwen3-tts-vc-realtime-2026-01-15"  # real-time voice-clone TTS model
voice_id = "YOUR_CLONE_VOICE_ID"  # left empty, read from the env var
voice_id_env = "MAIBOT_QQ_CALL_VOICE_ID"
websocket_base_url = "wss://dashscope.aliyuncs.com/api-ws/v1/realtime"
sample_rate = 24000       # TTS output sample rate
playback_latency_ms = 80  # PulseAudio playback buffer (ms); too low causes stutter
gain_db = 8.0             # playback gain; too high distorts
speech_rate = 1.08        # speech rate multiplier

[memory]
enabled = true            # organize and write back to MaiBot private-chat memory after hangup
summary_task_name = "utils"  # model task for call summary and persona facts
summary_temperature = 0.2    # summary model temperature
summary_max_tokens = 320     # max summary tokens
min_turns = 1                # minimum valid turns to trigger archiving
max_turns = 24               # max valid turns kept per archive
max_transcript_chars = 6000  # max chars of valid transcript
max_summary_chars = 240      # max chars of call summary
max_facts = 6                # max key persona facts written back
include_transcript = true    # include the cleaned valid transcript in the archive
persist_private_session = true  # persist to the caller's MaiBot private-chat history
append_maisaka_context = true   # also append to the current Maisaka context
write_timeout_seconds = 20.0    # per-hangup archive timeout (sec)

Saving hot-restarts the runtime; usually no MaiBot restart is needed. The complete example is in the repository's examples/config.example.toml.

Verify and troubleshoot

After starting bot QQ and MaiBot, run the bridge diagnostic script to check dependency commands, files, the Loader Hook marker, NapCat plugin enablement, the isolated PulseAudio, the AV Host health endpoint, and the authenticated bridge endpoints:

bash
~/.local/share/maibot-qq-voice-call/scripts/doctor.sh

All passing prints all bridge checks passed.

Confirm runtime readiness — Call the plugin API github.claudiagardner.maibot-qq-voice-call.get_call_status; it returns call status, latest ASR/LLM/TTS timings, and the last memory write-back result. ready being true means the chain is ready.

Real incoming-call test — Start a voice call to bot QQ with a test account: the bridge should auto-answer, and with plugin.log_transcripts = true the MaiBot log shows ASR transcripts and replies; after hangup, an archive message starting with [QQ语音通话记录] appears in the caller's private chat, and no extra text reply is sent to QQ.

Bridge endpoint auth failure — Check that bridge.token_file points to the installer's token file, or set the MAIBOT_QQ_CALL_BRIDGE_TOKEN env var; if it does not match the bridge, the "authenticated NapCat bridge endpoint" check in the diagnostic fails.

No sound or cannot hear the other party — Confirm audio.pulse_server points to the Unix socket at runtime/pulse/native under the install dir, and capture_device / playback_device are maibot_qq_speaker.monitor and maibot_qq_mic respectively; for TTS stutter, raise tts.playback_latency_ms a bit.

Installer refuses to layer the Hook — QQ's Loader was already rewritten by an older AV Host integration; find the clean Loader saved before the upgrade and install again with --original-loader /path/to/clean-loader.js explicitly specified.

After a QQ / NapCat upgrade — Loader Hook and AVSDK are version-sensitive integrations: re-run the installer and doctor.sh, and complete a test-account call (incoming, answered, two-way audio, hangup) before swapping in the running production link without verification.

Uninstall — Run ~/.local/share/maibot-qq-voice-call/scripts/uninstall.sh; by default it keeps the runtime dir and token for easy recovery—add --purge to delete them too. Uninstall only restores the backup while the Loader marker still matches and never overwrites files newer than a QQ upgrade.