feat(channels): add QQ Official channel via Tencent Bot SDK

Implement QQ Official messaging channel using OAuth2 authentication
with Discord-like WebSocket gateway protocol for events.

- Add QQChannel with send/listen/health_check support
- Add QQConfig (app_id, app_secret, allowed_users)
- OAuth2 token refresh and WebSocket heartbeat management
- Message deduplication with capacity-based eviction
- Support both C2C (private) and group AT messages
- Integrate with onboard wizard, integrations registry, and channel
  list/doctor commands
- Include unit tests for user allowlist, deduplication, and config
This commit is contained in:
elonf 2026-02-17 10:22:23 +08:00 committed by Chummy
parent d94d7baa14
commit ed71bce447
5 changed files with 659 additions and 5 deletions

View file

@ -1283,6 +1283,7 @@ pub struct ChannelsConfig {
pub irc: Option<IrcConfig>,
pub lark: Option<LarkConfig>,
pub dingtalk: Option<DingTalkConfig>,
pub qq: Option<QQConfig>,
}
impl Default for ChannelsConfig {
@ -1301,6 +1302,7 @@ impl Default for ChannelsConfig {
irc: None,
lark: None,
dingtalk: None,
qq: None,
}
}
}
@ -1632,6 +1634,18 @@ pub struct DingTalkConfig {
pub allowed_users: Vec<String>,
}
/// QQ Official Bot configuration (Tencent QQ Bot SDK)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QQConfig {
/// App ID from QQ Bot developer console
pub app_id: String,
/// App Secret from QQ Bot developer console
pub app_secret: String,
/// Allowed user IDs. Empty = deny all, "*" = allow all
#[serde(default)]
pub allowed_users: Vec<String>,
}
// ── Config impl ──────────────────────────────────────────────────
impl Default for Config {
@ -2173,6 +2187,7 @@ default_temperature = 0.7
irc: None,
lark: None,
dingtalk: None,
qq: None,
},
memory: MemoryConfig::default(),
tunnel: TunnelConfig::default(),
@ -2587,6 +2602,7 @@ tool_dispatcher = "xml"
irc: None,
lark: None,
dingtalk: None,
qq: None,
};
let toml_str = toml::to_string_pretty(&c).unwrap();
let parsed: ChannelsConfig = toml::from_str(&toml_str).unwrap();
@ -2748,6 +2764,7 @@ channel_id = "C123"
irc: None,
lark: None,
dingtalk: None,
qq: None,
};
let toml_str = toml::to_string_pretty(&c).unwrap();
let parsed: ChannelsConfig = toml::from_str(&toml_str).unwrap();