feat: add QQ Bot platform adapter (Official API v2)

Add full QQ Bot integration via the Official QQ Bot API (v2):
- WebSocket gateway for inbound events (C2C, group, guild, DM)
- REST API for outbound text/markdown/media messages
- Voice transcription (Tencent ASR + configurable STT provider)
- Attachment processing (images, voice, files)
- User authorization (allowlist + allow-all + DM pairing)

Integration points:
- gateway: Platform.QQ enum, adapter factory, allowlist maps
- CLI: setup wizard, gateway config, status display, tools config
- tools: send_message cross-platform routing, toolsets
- cron: delivery platform support
- docs: QQ Bot setup guide
This commit is contained in:
Junjun Zhang
2026-04-13 21:56:38 +08:00
committed by Teknium
parent eb44abd6b1
commit 87bfc28e70
18 changed files with 2679 additions and 5 deletions

View File

@@ -66,6 +66,7 @@ class Platform(Enum):
WECOM_CALLBACK = "wecom_callback"
WEIXIN = "weixin"
BLUEBUBBLES = "bluebubbles"
QQ = "qq"
@dataclass
@@ -303,6 +304,9 @@ class GatewayConfig:
# BlueBubbles uses extra dict for local server config
elif platform == Platform.BLUEBUBBLES and config.extra.get("server_url") and config.extra.get("password"):
connected.append(platform)
# QQ uses extra dict for app credentials
elif platform == Platform.QQ and config.extra.get("app_id") and config.extra.get("client_secret"):
connected.append(platform)
return connected
def get_home_channel(self, platform: Platform) -> Optional[HomeChannel]:
@@ -1109,6 +1113,32 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
name=os.getenv("BLUEBUBBLES_HOME_CHANNEL_NAME", "Home"),
)
# QQ (Official Bot API v2)
qq_app_id = os.getenv("QQ_APP_ID")
qq_client_secret = os.getenv("QQ_CLIENT_SECRET")
if qq_app_id or qq_client_secret:
if Platform.QQ not in config.platforms:
config.platforms[Platform.QQ] = PlatformConfig()
config.platforms[Platform.QQ].enabled = True
extra = config.platforms[Platform.QQ].extra
if qq_app_id:
extra["app_id"] = qq_app_id
if qq_client_secret:
extra["client_secret"] = qq_client_secret
qq_allowed_users = os.getenv("QQ_ALLOWED_USERS", "").strip()
if qq_allowed_users:
extra["allow_from"] = qq_allowed_users
qq_group_allowed = os.getenv("QQ_GROUP_ALLOWED_USERS", "").strip()
if qq_group_allowed:
extra["group_allow_from"] = qq_group_allowed
qq_home = os.getenv("QQ_HOME_CHANNEL", "").strip()
if qq_home:
config.platforms[Platform.QQ].home_channel = HomeChannel(
platform=Platform.QQ,
chat_id=qq_home,
name=os.getenv("QQ_HOME_CHANNEL_NAME", "Home"),
)
# Session settings
idle_minutes = os.getenv("SESSION_IDLE_MINUTES")
if idle_minutes:

View File

@@ -9,9 +9,11 @@ Each adapter handles:
"""
from .base import BasePlatformAdapter, MessageEvent, SendResult
from .qq import QQAdapter
__all__ = [
"BasePlatformAdapter",
"MessageEvent",
"SendResult",
"QQAdapter",
]

1915
gateway/platforms/qq.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1499,6 +1499,7 @@ class GatewayRunner:
"WECOM_CALLBACK_ALLOWED_USERS",
"WEIXIN_ALLOWED_USERS",
"BLUEBUBBLES_ALLOWED_USERS",
"QQ_ALLOWED_USERS",
"GATEWAY_ALLOWED_USERS")
)
_allow_all = os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in ("true", "1", "yes") or any(
@@ -1512,7 +1513,8 @@ class GatewayRunner:
"WECOM_ALLOW_ALL_USERS",
"WECOM_CALLBACK_ALLOW_ALL_USERS",
"WEIXIN_ALLOW_ALL_USERS",
"BLUEBUBBLES_ALLOW_ALL_USERS")
"BLUEBUBBLES_ALLOW_ALL_USERS",
"QQ_ALLOW_ALL_USERS")
)
if not _any_allowlist and not _allow_all:
logger.warning(
@@ -2255,8 +2257,12 @@ class GatewayRunner:
return None
return BlueBubblesAdapter(config)
elif platform == Platform.QQ:
from gateway.platforms.qq import QQAdapter
return QQAdapter(config)
return None
def _is_user_authorized(self, source: SessionSource) -> bool:
"""
Check if a user is authorized to use the bot.
@@ -2296,6 +2302,7 @@ class GatewayRunner:
Platform.WECOM_CALLBACK: "WECOM_CALLBACK_ALLOWED_USERS",
Platform.WEIXIN: "WEIXIN_ALLOWED_USERS",
Platform.BLUEBUBBLES: "BLUEBUBBLES_ALLOWED_USERS",
Platform.QQ: "QQ_ALLOWED_USERS",
}
platform_allow_all_map = {
Platform.TELEGRAM: "TELEGRAM_ALLOW_ALL_USERS",
@@ -2313,6 +2320,7 @@ class GatewayRunner:
Platform.WECOM_CALLBACK: "WECOM_CALLBACK_ALLOW_ALL_USERS",
Platform.WEIXIN: "WEIXIN_ALLOW_ALL_USERS",
Platform.BLUEBUBBLES: "BLUEBUBBLES_ALLOW_ALL_USERS",
Platform.QQ: "QQ_ALLOW_ALL_USERS",
}
# Per-platform allow-all flag (e.g., DISCORD_ALLOW_ALL_USERS=true)