mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-28 03:40:09 +08:00
feat(channels): add messaging channels and built-in qq bot
This commit is contained in:
@@ -121,6 +121,22 @@ pub fn read_openclaw_config() -> Result<Value, String> {
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// 供其他模块复用:读取 openclaw.json 为 JSON Value
|
||||
pub fn load_openclaw_json() -> Result<Value, String> {
|
||||
read_openclaw_config()
|
||||
}
|
||||
|
||||
/// 供其他模块复用:将 JSON Value 写回 openclaw.json(含备份和清理)
|
||||
pub fn save_openclaw_json(config: &Value) -> Result<(), String> {
|
||||
write_openclaw_config(config.clone())
|
||||
}
|
||||
|
||||
/// 供其他模块复用:触发 Gateway 重载
|
||||
pub async fn do_reload_gateway(app: &tauri::AppHandle) -> Result<String, String> {
|
||||
let _ = app; // 预留扩展用
|
||||
reload_gateway().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn write_openclaw_config(config: Value) -> Result<(), String> {
|
||||
let path = super::openclaw_dir().join("openclaw.json");
|
||||
|
||||
570
src-tauri/src/commands/messaging.rs
Normal file
570
src-tauri/src/commands/messaging.rs
Normal file
@@ -0,0 +1,570 @@
|
||||
/// 消息渠道管理
|
||||
/// 负责 Telegram / Discord / QQ Bot 等消息渠道的配置持久化与凭证校验
|
||||
/// 配置写入 openclaw.json 的 channels / plugins 节点
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
/// 读取指定平台的当前配置(从 openclaw.json 中提取表单可用的值)
|
||||
#[tauri::command]
|
||||
pub async fn read_platform_config(platform: String) -> Result<Value, String> {
|
||||
let cfg = super::config::load_openclaw_json()?;
|
||||
|
||||
// 从已有配置中提取用户可编辑字段
|
||||
let saved = cfg
|
||||
.get("channels")
|
||||
.and_then(|c| c.get(&platform))
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null);
|
||||
|
||||
if saved.is_null() {
|
||||
return Ok(json!({ "exists": false }));
|
||||
}
|
||||
|
||||
let mut form = Map::new();
|
||||
|
||||
match platform.as_str() {
|
||||
"discord" => {
|
||||
// Discord 配置在 openclaw.json 中是展开的 guilds 结构
|
||||
// 需要反向提取成表单字段:token, guildId, channelId
|
||||
if let Some(t) = saved.get("token").and_then(|v| v.as_str()) {
|
||||
form.insert("token".into(), Value::String(t.into()));
|
||||
}
|
||||
if let Some(guilds) = saved.get("guilds").and_then(|v| v.as_object()) {
|
||||
if let Some(gid) = guilds.keys().next() {
|
||||
form.insert("guildId".into(), Value::String(gid.clone()));
|
||||
if let Some(channels) = guilds[gid].get("channels").and_then(|v| v.as_object())
|
||||
{
|
||||
let cids: Vec<&String> =
|
||||
channels.keys().filter(|k| k.as_str() != "*").collect();
|
||||
if let Some(cid) = cids.first() {
|
||||
form.insert("channelId".into(), Value::String((*cid).clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"telegram" => {
|
||||
// Telegram: botToken 直接保存, allowFrom 数组需要拼回逗号字符串
|
||||
if let Some(t) = saved.get("botToken").and_then(|v| v.as_str()) {
|
||||
form.insert("botToken".into(), Value::String(t.into()));
|
||||
}
|
||||
if let Some(arr) = saved.get("allowFrom").and_then(|v| v.as_array()) {
|
||||
let users: Vec<&str> = arr.iter().filter_map(|v| v.as_str()).collect();
|
||||
form.insert("allowedUsers".into(), Value::String(users.join(", ")));
|
||||
}
|
||||
}
|
||||
"qqbot" => {
|
||||
// QQ Bot: token 格式为 "AppID:AppSecret",拆分回表单字段
|
||||
if let Some(t) = saved.get("token").and_then(|v| v.as_str()) {
|
||||
if let Some((app_id, app_secret)) = t.split_once(':') {
|
||||
form.insert("appId".into(), Value::String(app_id.into()));
|
||||
form.insert("appSecret".into(), Value::String(app_secret.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// 通用:原样返回字符串类型字段
|
||||
if let Some(obj) = saved.as_object() {
|
||||
for (k, v) in obj {
|
||||
if k == "enabled" {
|
||||
continue;
|
||||
}
|
||||
if let Some(s) = v.as_str() {
|
||||
form.insert(k.clone(), Value::String(s.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(json!({ "exists": true, "values": Value::Object(form) }))
|
||||
}
|
||||
|
||||
/// 保存平台配置到 openclaw.json
|
||||
/// 前端传入的是表单字段,后端负责转换成 OpenClaw 要求的结构
|
||||
#[tauri::command]
|
||||
pub async fn save_messaging_platform(
|
||||
platform: String,
|
||||
form: Value,
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<Value, String> {
|
||||
let mut cfg = super::config::load_openclaw_json()?;
|
||||
|
||||
let channels = cfg
|
||||
.as_object_mut()
|
||||
.ok_or("配置格式错误")?
|
||||
.entry("channels")
|
||||
.or_insert_with(|| json!({}));
|
||||
let channels_map = channels.as_object_mut().ok_or("channels 节点格式错误")?;
|
||||
|
||||
let form_obj = form.as_object().ok_or("表单数据格式错误")?;
|
||||
|
||||
match platform.as_str() {
|
||||
"discord" => {
|
||||
let mut entry = Map::new();
|
||||
|
||||
// Bot Token
|
||||
if let Some(t) = form_obj.get("token").and_then(|v| v.as_str()) {
|
||||
entry.insert("token".into(), Value::String(t.trim().into()));
|
||||
}
|
||||
entry.insert("enabled".into(), Value::Bool(true));
|
||||
entry.insert("groupPolicy".into(), Value::String("allowlist".into()));
|
||||
entry.insert("dm".into(), json!({ "enabled": false }));
|
||||
entry.insert(
|
||||
"retry".into(),
|
||||
json!({
|
||||
"attempts": 3,
|
||||
"minDelayMs": 500,
|
||||
"maxDelayMs": 30000,
|
||||
"jitter": 0.1
|
||||
}),
|
||||
);
|
||||
|
||||
// guildId + channelId 展开为 guilds 嵌套结构
|
||||
let guild_id = form_obj
|
||||
.get("guildId")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
if !guild_id.is_empty() {
|
||||
let channel_id = form_obj
|
||||
.get("channelId")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
let channel_key = if channel_id.is_empty() {
|
||||
"*".to_string()
|
||||
} else {
|
||||
channel_id
|
||||
};
|
||||
entry.insert(
|
||||
"guilds".into(),
|
||||
json!({
|
||||
guild_id: {
|
||||
"users": ["*"],
|
||||
"requireMention": true,
|
||||
"channels": {
|
||||
channel_key: { "allow": true, "requireMention": true }
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
channels_map.insert("discord".into(), Value::Object(entry));
|
||||
}
|
||||
"telegram" => {
|
||||
let mut entry = Map::new();
|
||||
|
||||
if let Some(t) = form_obj.get("botToken").and_then(|v| v.as_str()) {
|
||||
entry.insert("botToken".into(), Value::String(t.trim().into()));
|
||||
}
|
||||
entry.insert("enabled".into(), Value::Bool(true));
|
||||
|
||||
// allowedUsers 逗号字符串 → allowFrom 数组
|
||||
if let Some(users_str) = form_obj.get("allowedUsers").and_then(|v| v.as_str()) {
|
||||
let users: Vec<Value> = users_str
|
||||
.split(',')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| Value::String(s.into()))
|
||||
.collect();
|
||||
if !users.is_empty() {
|
||||
entry.insert("allowFrom".into(), Value::Array(users));
|
||||
}
|
||||
}
|
||||
|
||||
channels_map.insert("telegram".into(), Value::Object(entry));
|
||||
}
|
||||
"qqbot" => {
|
||||
let app_id = form_obj
|
||||
.get("appId")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
let app_secret = form_obj
|
||||
.get("appSecret")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if app_id.is_empty() || app_secret.is_empty() {
|
||||
return Err("AppID 和 AppSecret 不能为空".into());
|
||||
}
|
||||
|
||||
let token = format!("{}:{}", app_id, app_secret);
|
||||
let mut entry = Map::new();
|
||||
entry.insert("token".into(), Value::String(token));
|
||||
entry.insert("enabled".into(), Value::Bool(true));
|
||||
|
||||
channels_map.insert("qqbot".into(), Value::Object(entry));
|
||||
}
|
||||
_ => {
|
||||
// 通用平台:直接保存表单字段
|
||||
let mut entry = Map::new();
|
||||
for (k, v) in form_obj {
|
||||
entry.insert(k.clone(), v.clone());
|
||||
}
|
||||
entry.insert("enabled".into(), Value::Bool(true));
|
||||
channels_map.insert(platform.clone(), Value::Object(entry));
|
||||
}
|
||||
}
|
||||
|
||||
// 写回配置并重载 Gateway
|
||||
super::config::save_openclaw_json(&cfg)?;
|
||||
|
||||
// 触发 Gateway 重载使配置生效
|
||||
let _ = super::config::do_reload_gateway(&app).await;
|
||||
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
|
||||
/// 删除指定平台配置
|
||||
#[tauri::command]
|
||||
pub async fn remove_messaging_platform(
|
||||
platform: String,
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<Value, String> {
|
||||
let mut cfg = super::config::load_openclaw_json()?;
|
||||
|
||||
if let Some(channels) = cfg.get_mut("channels").and_then(|c| c.as_object_mut()) {
|
||||
channels.remove(&platform);
|
||||
}
|
||||
|
||||
super::config::save_openclaw_json(&cfg)?;
|
||||
let _ = super::config::do_reload_gateway(&app).await;
|
||||
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
|
||||
/// 切换平台启用/禁用
|
||||
#[tauri::command]
|
||||
pub async fn toggle_messaging_platform(
|
||||
platform: String,
|
||||
enabled: bool,
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<Value, String> {
|
||||
let mut cfg = super::config::load_openclaw_json()?;
|
||||
|
||||
if let Some(entry) = cfg
|
||||
.get_mut("channels")
|
||||
.and_then(|c| c.get_mut(&platform))
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
entry.insert("enabled".into(), Value::Bool(enabled));
|
||||
} else {
|
||||
return Err(format!("平台 {} 未配置", platform));
|
||||
}
|
||||
|
||||
super::config::save_openclaw_json(&cfg)?;
|
||||
let _ = super::config::do_reload_gateway(&app).await;
|
||||
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
|
||||
/// 在线校验 Bot 凭证(调用平台 API 验证 Token 是否有效)
|
||||
#[tauri::command]
|
||||
pub async fn verify_bot_token(
|
||||
platform: String,
|
||||
form: Value,
|
||||
) -> Result<Value, String> {
|
||||
let form_obj = form.as_object().ok_or("表单数据格式错误")?;
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()
|
||||
.map_err(|e| format!("HTTP 客户端初始化失败: {}", e))?;
|
||||
|
||||
match platform.as_str() {
|
||||
"discord" => verify_discord(&client, form_obj).await,
|
||||
"telegram" => verify_telegram(&client, form_obj).await,
|
||||
"qqbot" => verify_qqbot(&client, form_obj).await,
|
||||
_ => Ok(json!({
|
||||
"valid": true,
|
||||
"warnings": ["该平台暂不支持在线校验"]
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// 列出当前已配置的平台清单
|
||||
#[tauri::command]
|
||||
pub async fn list_configured_platforms() -> Result<Value, String> {
|
||||
let cfg = super::config::load_openclaw_json()?;
|
||||
let mut result: Vec<Value> = vec![];
|
||||
|
||||
if let Some(channels) = cfg.get("channels").and_then(|c| c.as_object()) {
|
||||
for (name, val) in channels {
|
||||
let enabled = val.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
|
||||
result.push(json!({
|
||||
"id": name,
|
||||
"enabled": enabled
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(json!(result))
|
||||
}
|
||||
|
||||
// ── Discord 凭证校验 ──────────────────────────────────────
|
||||
|
||||
async fn verify_discord(
|
||||
client: &reqwest::Client,
|
||||
form: &Map<String, Value>,
|
||||
) -> Result<Value, String> {
|
||||
let token = form
|
||||
.get("token")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
if token.is_empty() {
|
||||
return Ok(json!({ "valid": false, "errors": ["Bot Token 不能为空"] }));
|
||||
}
|
||||
|
||||
// 验证 Bot Token
|
||||
let me_resp = client
|
||||
.get("https://discord.com/api/v10/users/@me")
|
||||
.header("Authorization", format!("Bot {}", token))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Discord API 连接失败: {}", e))?;
|
||||
|
||||
if me_resp.status() == 401 {
|
||||
return Ok(json!({ "valid": false, "errors": ["Bot Token 无效,请检查后重试"] }));
|
||||
}
|
||||
if !me_resp.status().is_success() {
|
||||
return Ok(json!({
|
||||
"valid": false,
|
||||
"errors": [format!("Discord API 返回异常: {}", me_resp.status())]
|
||||
}));
|
||||
}
|
||||
|
||||
let me: Value = me_resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("解析响应失败: {}", e))?;
|
||||
if me.get("bot").and_then(|v| v.as_bool()) != Some(true) {
|
||||
return Ok(json!({
|
||||
"valid": false,
|
||||
"errors": ["提供的 Token 不属于 Bot 账号,请使用 Bot Token"]
|
||||
}));
|
||||
}
|
||||
|
||||
let bot_name = me
|
||||
.get("username")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("未知");
|
||||
let mut details = vec![format!("Bot: @{}", bot_name)];
|
||||
|
||||
// 验证 Guild(可选)
|
||||
let guild_id = form
|
||||
.get("guildId")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
if !guild_id.is_empty() {
|
||||
match client
|
||||
.get(format!("https://discord.com/api/v10/guilds/{}", guild_id))
|
||||
.header("Authorization", format!("Bot {}", token))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
let guild: Value = resp.json().await.unwrap_or_default();
|
||||
let name = guild.get("name").and_then(|v| v.as_str()).unwrap_or("?");
|
||||
details.push(format!("服务器: {}", name));
|
||||
}
|
||||
Ok(resp) if resp.status().as_u16() == 403 || resp.status().as_u16() == 404 => {
|
||||
return Ok(json!({
|
||||
"valid": false,
|
||||
"errors": [format!("无法访问服务器 {},请确认 Bot 已加入该服务器", guild_id)]
|
||||
}));
|
||||
}
|
||||
_ => {
|
||||
details.push("服务器 ID 未能验证(网络问题)".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"valid": true,
|
||||
"errors": [],
|
||||
"details": details
|
||||
}))
|
||||
}
|
||||
|
||||
// ── QQ Bot 凭证校验 ──────────────────────────────────────
|
||||
|
||||
async fn verify_qqbot(
|
||||
client: &reqwest::Client,
|
||||
form: &Map<String, Value>,
|
||||
) -> Result<Value, String> {
|
||||
let app_id = form
|
||||
.get("appId")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
let app_secret = form
|
||||
.get("appSecret")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
|
||||
if app_id.is_empty() {
|
||||
return Ok(json!({ "valid": false, "errors": ["AppID 不能为空"] }));
|
||||
}
|
||||
if app_secret.is_empty() {
|
||||
return Ok(json!({ "valid": false, "errors": ["AppSecret 不能为空"] }));
|
||||
}
|
||||
|
||||
// 通过 QQ Bot API 获取 access_token 验证凭证
|
||||
let resp = client
|
||||
.post("https://bots.qq.com/app/getAppAccessToken")
|
||||
.json(&json!({
|
||||
"appId": app_id,
|
||||
"clientSecret": app_secret
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("QQ Bot API 连接失败: {}", e))?;
|
||||
|
||||
let body: Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("解析响应失败: {}", e))?;
|
||||
|
||||
if body.get("access_token").and_then(|v| v.as_str()).is_some() {
|
||||
Ok(json!({
|
||||
"valid": true,
|
||||
"errors": [],
|
||||
"details": [format!("AppID: {}", app_id)]
|
||||
}))
|
||||
} else {
|
||||
let msg = body
|
||||
.get("message")
|
||||
.or_else(|| body.get("msg"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("凭证无效,请检查 AppID 和 AppSecret");
|
||||
Ok(json!({
|
||||
"valid": false,
|
||||
"errors": [msg]
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// ── QQ Bot 插件安装(带日志流) ──────────────────────────
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_qqbot_plugin(app: tauri::AppHandle) -> Result<String, String> {
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::process::Stdio;
|
||||
use tauri::Emitter;
|
||||
|
||||
let _ = app.emit("plugin-log", "正在安装 QQBot 社区插件 @sliverp/qqbot ...");
|
||||
let _ = app.emit("plugin-progress", 10);
|
||||
|
||||
let mut child = crate::utils::openclaw_command()
|
||||
.args(["plugins", "install", "@sliverp/qqbot@latest"])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("启动 openclaw 失败: {}", e))?;
|
||||
|
||||
let stderr = child.stderr.take();
|
||||
let app2 = app.clone();
|
||||
let handle = std::thread::spawn(move || {
|
||||
if let Some(pipe) = stderr {
|
||||
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
|
||||
let _ = app2.emit("plugin-log", &line);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let _ = app.emit("plugin-progress", 30);
|
||||
|
||||
let mut progress = 30;
|
||||
if let Some(pipe) = child.stdout.take() {
|
||||
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
|
||||
let _ = app.emit("plugin-log", &line);
|
||||
if progress < 90 {
|
||||
progress += 10;
|
||||
let _ = app.emit("plugin-progress", progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = handle.join();
|
||||
let _ = app.emit("plugin-progress", 95);
|
||||
|
||||
let status = child.wait().map_err(|e| format!("等待安装进程失败: {}", e))?;
|
||||
let _ = app.emit("plugin-progress", 100);
|
||||
|
||||
if !status.success() {
|
||||
let _ = app.emit("plugin-log", "QQBot 插件安装失败");
|
||||
return Err("插件安装失败,请查看日志".into());
|
||||
}
|
||||
|
||||
let _ = app.emit("plugin-log", "QQBot 插件安装完成");
|
||||
Ok("安装成功".into())
|
||||
}
|
||||
|
||||
// ── Telegram 凭证校验 ─────────────────────────────────────
|
||||
|
||||
async fn verify_telegram(
|
||||
client: &reqwest::Client,
|
||||
form: &Map<String, Value>,
|
||||
) -> Result<Value, String> {
|
||||
let bot_token = form
|
||||
.get("botToken")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
if bot_token.is_empty() {
|
||||
return Ok(json!({ "valid": false, "errors": ["Bot Token 不能为空"] }));
|
||||
}
|
||||
|
||||
let allowed = form
|
||||
.get("allowedUsers")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
if allowed.is_empty() {
|
||||
return Ok(json!({ "valid": false, "errors": ["至少需要填写一个允许的用户 ID"] }));
|
||||
}
|
||||
|
||||
let url = format!("https://api.telegram.org/bot{}/getMe", bot_token);
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Telegram API 连接失败: {}", e))?;
|
||||
|
||||
let body: Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("解析响应失败: {}", e))?;
|
||||
|
||||
if body.get("ok").and_then(|v| v.as_bool()) == Some(true) {
|
||||
let username = body
|
||||
.get("result")
|
||||
.and_then(|r| r.get("username"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("未知");
|
||||
Ok(json!({
|
||||
"valid": true,
|
||||
"errors": [],
|
||||
"details": [format!("Bot: @{}", username)]
|
||||
}))
|
||||
} else {
|
||||
let desc = body
|
||||
.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Token 无效");
|
||||
Ok(json!({
|
||||
"valid": false,
|
||||
"errors": [desc]
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub mod device;
|
||||
pub mod extensions;
|
||||
pub mod logs;
|
||||
pub mod memory;
|
||||
pub mod messaging;
|
||||
pub mod pairing;
|
||||
pub mod service;
|
||||
pub mod skills;
|
||||
|
||||
@@ -4,7 +4,8 @@ mod tray;
|
||||
mod utils;
|
||||
|
||||
use commands::{
|
||||
agent, assistant, config, device, extensions, logs, memory, pairing, service, skills, update,
|
||||
agent, assistant, config, device, extensions, logs, memory, messaging, pairing, service, skills,
|
||||
update,
|
||||
};
|
||||
|
||||
pub fn run() {
|
||||
@@ -141,6 +142,14 @@ pub fn run() {
|
||||
assistant::assistant_save_image,
|
||||
assistant::assistant_load_image,
|
||||
assistant::assistant_delete_image,
|
||||
// 消息渠道管理
|
||||
messaging::read_platform_config,
|
||||
messaging::save_messaging_platform,
|
||||
messaging::remove_messaging_platform,
|
||||
messaging::toggle_messaging_platform,
|
||||
messaging::verify_bot_token,
|
||||
messaging::list_configured_platforms,
|
||||
messaging::install_qqbot_plugin,
|
||||
// Skills 管理(openclaw skills CLI)
|
||||
skills::skills_list,
|
||||
skills::skills_info,
|
||||
|
||||
Reference in New Issue
Block a user