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

@ -7,6 +7,7 @@ pub mod irc;
pub mod lark;
pub mod matrix;
pub mod signal;
pub mod qq;
pub mod slack;
pub mod telegram;
pub mod traits;
@ -21,6 +22,7 @@ pub use irc::IrcChannel;
pub use lark::LarkChannel;
pub use matrix::MatrixChannel;
pub use signal::SignalChannel;
pub use qq::QQChannel;
pub use slack::SlackChannel;
pub use telegram::TelegramChannel;
pub use traits::Channel;
@ -719,6 +721,7 @@ pub fn handle_command(command: crate::ChannelCommands, config: &Config) -> Resul
("IRC", config.channels_config.irc.is_some()),
("Lark", config.channels_config.lark.is_some()),
("DingTalk", config.channels_config.dingtalk.is_some()),
("QQ", config.channels_config.qq.is_some()),
] {
println!(" {} {name}", if configured { "" } else { "" });
}
@ -881,6 +884,17 @@ pub async fn doctor_channels(config: Config) -> Result<()> {
));
}
if let Some(ref qq) = config.channels_config.qq {
channels.push((
"QQ",
Arc::new(QQChannel::new(
qq.app_id.clone(),
qq.app_secret.clone(),
qq.allowed_users.clone(),
)),
));
}
if channels.is_empty() {
println!("No real-time channels configured. Run `zeroclaw onboard` first.");
return Ok(());
@ -1160,6 +1174,14 @@ pub async fn start_channels(config: Config) -> Result<()> {
)));
}
if let Some(ref qq) = config.channels_config.qq {
channels.push(Arc::new(QQChannel::new(
qq.app_id.clone(),
qq.app_secret.clone(),
qq.allowed_users.clone(),
)));
}
if channels.is_empty() {
println!("No channels configured. Run `zeroclaw onboard` to set up channels.");
return Ok(());