feat(channels): add napcat/onebot onboarding and config UI

This commit is contained in:
argenis de la rosa 2026-03-02 14:08:08 -05:00
parent b21a1a91ac
commit 31ca8c2fed
3 changed files with 98 additions and 5 deletions

View File

@ -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

View File

@ -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<ChannelsConfig> {
"— 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<ChannelsConfig> {
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<String> = 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));
}
}

View File

@ -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',