From 31ca8c2fed02b0548f187fe3aa3b2d61ef8e8b40 Mon Sep 17 00:00:00 2001 From: argenis de la rosa Date: Mon, 2 Mar 2026 14:08:08 -0500 Subject: [PATCH] feat(channels): add napcat/onebot onboarding and config UI --- src/main.rs | 5 +- src/onboard/wizard.rs | 82 ++++++++++++++++++++- web/src/components/config/configSections.ts | 16 ++++ 3 files changed, 98 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index c1d485708..03871c169 100644 --- a/src/main.rs +++ b/src/main.rs @@ -437,13 +437,14 @@ Examples: #[arg(long, value_enum, default_value_t = QuotaFormat::Text)] format: QuotaFormat, }, - /// Manage channels (telegram, discord, slack, github) + /// Manage channels (telegram, discord, slack, qq, napcat, and more) #[command(long_about = "\ Manage communication channels. Add, remove, list, and health-check channels that connect ZeroClaw \ to messaging platforms. Supported channel types: telegram, discord, \ -slack, whatsapp, github, matrix, imessage, email. +slack, mattermost, whatsapp, matrix, signal, qq, napcat, \ +lark, feishu, dingtalk, github, nextcloud, irc, imessage, email. Examples: zeroclaw channel list diff --git a/src/onboard/wizard.rs b/src/onboard/wizard.rs index fd7b33b65..53913417d 100644 --- a/src/onboard/wizard.rs +++ b/src/onboard/wizard.rs @@ -1,5 +1,5 @@ use crate::config::schema::{ - default_nostr_relays, DingTalkConfig, IrcConfig, LarkReceiveMode, LinqConfig, + default_nostr_relays, DingTalkConfig, IrcConfig, LarkReceiveMode, LinqConfig, NapcatConfig, NextcloudTalkConfig, NostrConfig, ProgressMode, QQConfig, QQEnvironment, QQReceiveMode, SignalConfig, StreamMode, WhatsAppConfig, }; @@ -4275,6 +4275,7 @@ enum ChannelMenuChoice { NextcloudTalk, DingTalk, QqOfficial, + Napcat, LarkFeishu, Nostr, Done, @@ -4294,6 +4295,7 @@ const CHANNEL_MENU_CHOICES: &[ChannelMenuChoice] = &[ ChannelMenuChoice::NextcloudTalk, ChannelMenuChoice::DingTalk, ChannelMenuChoice::QqOfficial, + ChannelMenuChoice::Napcat, ChannelMenuChoice::LarkFeishu, ChannelMenuChoice::Nostr, ChannelMenuChoice::Done, @@ -4420,6 +4422,14 @@ fn setup_channels() -> Result { "— Tencent QQ Bot" } ), + ChannelMenuChoice::Napcat => format!( + "Napcat/OneBot {}", + if config.napcat.is_some() { + "✅ connected" + } else { + "— QQ via OneBot v11" + } + ), ChannelMenuChoice::LarkFeishu => format!( "Lark/Feishu {}", if config.lark.is_some() { @@ -5716,6 +5726,62 @@ fn setup_channels() -> Result { environment, }); } + ChannelMenuChoice::Napcat => { + // ── Napcat / OneBot ── + println!(); + println!( + " {} {}", + style("Napcat / OneBot Setup").white().bold(), + style("— QQ via OneBot v11 (Napcat)").dim() + ); + print_bullet("1. Start your Napcat/OneBot gateway"); + print_bullet("2. Enable OneBot v11 WebSocket endpoint"); + print_bullet("3. Paste the WebSocket URL and optional token below"); + println!(); + + let websocket_url: String = Input::new() + .with_prompt(" WebSocket URL") + .default("ws://127.0.0.1:3001".into()) + .interact_text()?; + let websocket_url = websocket_url.trim().to_string(); + if websocket_url.is_empty() { + println!(" {} Skipped", style("→").dim()); + continue; + } + + let api_base_url: String = Input::new() + .with_prompt(" HTTP API base URL (optional, Enter to auto-derive)") + .allow_empty(true) + .interact_text()?; + + let access_token: String = Input::new() + .with_prompt(" Access token (optional)") + .allow_empty(true) + .interact_text()?; + + let users_str: String = Input::new() + .with_prompt(" Allowed QQ user IDs (comma-separated, '*' for all)") + .allow_empty(true) + .interact_text()?; + let allowed_users: Vec = users_str + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + + config.napcat = Some(NapcatConfig { + websocket_url, + api_base_url: api_base_url.trim().to_string(), + access_token: if access_token.trim().is_empty() { + None + } else { + Some(access_token.trim().to_string()) + }, + allowed_users, + }); + + println!(" {} Napcat configured", style("✅").green().bold()); + } ChannelMenuChoice::LarkFeishu => { // ── Lark/Feishu ── println!(); @@ -8789,14 +8855,15 @@ mod tests { } #[test] - fn channel_menu_choices_include_signal_nextcloud_and_dingtalk() { + fn channel_menu_choices_include_signal_nextcloud_dingtalk_and_napcat() { assert!(channel_menu_choices().contains(&ChannelMenuChoice::Signal)); assert!(channel_menu_choices().contains(&ChannelMenuChoice::NextcloudTalk)); assert!(channel_menu_choices().contains(&ChannelMenuChoice::DingTalk)); + assert!(channel_menu_choices().contains(&ChannelMenuChoice::Napcat)); } #[test] - fn launchable_channels_include_signal_mattermost_qq_nextcloud_and_dingtalk() { + fn launchable_channels_include_signal_mattermost_qq_nextcloud_dingtalk_and_napcat() { let mut channels = ChannelsConfig::default(); assert!(!has_launchable_channels(&channels)); @@ -8848,5 +8915,14 @@ mod tests { allowed_users: vec!["*".into()], }); assert!(has_launchable_channels(&channels)); + + channels.dingtalk = None; + channels.napcat = Some(crate::config::schema::NapcatConfig { + websocket_url: "ws://127.0.0.1:3001".into(), + api_base_url: String::new(), + access_token: None, + allowed_users: vec!["*".into()], + }); + assert!(has_launchable_channels(&channels)); } } diff --git a/web/src/components/config/configSections.ts b/web/src/components/config/configSections.ts index 2304acd8c..55be73930 100644 --- a/web/src/components/config/configSections.ts +++ b/web/src/components/config/configSections.ts @@ -640,6 +640,22 @@ export const CONFIG_SECTIONS: SectionDef[] = [ ], }, + // ── Napcat (OneBot) ─────────────────────────────────────────────── + { + path: 'channels_config.napcat', + category: 'channels', + title: 'Napcat (OneBot)', + description: 'QQ via OneBot v11 (Napcat)', + icon: MessageCircle, + defaultCollapsed: true, + fields: [ + { key: 'websocket_url', label: 'WebSocket URL', type: 'text', description: 'e.g. ws://127.0.0.1:3001' }, + { key: 'api_base_url', label: 'HTTP API Base URL', type: 'text', description: 'Optional. Leave empty to auto-derive from websocket_url' }, + { key: 'access_token', label: 'Access Token', type: 'password', sensitive: true, description: 'Optional bearer token for Napcat/OneBot API' }, + { key: 'allowed_users', label: 'Allowed Users', type: 'tag-list', tagPlaceholder: "e.g. 10001 or '*'" }, + ], + }, + // ── Memory ──────────────────────────────────────────────────────── { path: 'memory',