支持渠道、指令(自定义前缀)和事件(启用/禁用)管理
This commit is contained in:
@@ -5,7 +5,10 @@ use tokio::task::JoinHandle;
|
||||
use super::command_handlers;
|
||||
use super::manager::ChatChannelManager;
|
||||
use super::types::IncomingCommand;
|
||||
use crate::db::service::chat_channel_message_log_service;
|
||||
use crate::db::service::{app_metadata_service, chat_channel_message_log_service};
|
||||
|
||||
const COMMAND_PREFIX_KEY: &str = "chat_command_prefix";
|
||||
const DEFAULT_COMMAND_PREFIX: &str = "/";
|
||||
|
||||
pub fn spawn_command_dispatcher(
|
||||
mut command_rx: mpsc::Receiver<IncomingCommand>,
|
||||
@@ -28,7 +31,13 @@ pub fn spawn_command_dispatcher(
|
||||
)
|
||||
.await;
|
||||
|
||||
let response = dispatch_command(text, &db_conn, &manager).await;
|
||||
let prefix = app_metadata_service::get_value(&db_conn, COMMAND_PREFIX_KEY)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| DEFAULT_COMMAND_PREFIX.to_string());
|
||||
|
||||
let response = dispatch_command(text, &prefix, &db_conn, &manager).await;
|
||||
|
||||
// Send response back via the same channel
|
||||
let send_result = manager.send_to_channel(cmd.channel_id, &response).await;
|
||||
@@ -59,45 +68,47 @@ pub fn spawn_command_dispatcher(
|
||||
|
||||
async fn dispatch_command(
|
||||
text: &str,
|
||||
prefix: &str,
|
||||
db: &DatabaseConnection,
|
||||
manager: &ChatChannelManager,
|
||||
) -> super::types::RichMessage {
|
||||
let parts: Vec<&str> = text.splitn(2, ' ').collect();
|
||||
// Check if text starts with the configured prefix
|
||||
if !text.starts_with(prefix) {
|
||||
return command_handlers::handle_help(prefix);
|
||||
}
|
||||
|
||||
// Strip prefix and parse command + args
|
||||
let without_prefix = &text[prefix.len()..];
|
||||
let parts: Vec<&str> = without_prefix.splitn(2, ' ').collect();
|
||||
let command = parts[0].to_lowercase();
|
||||
let args = parts.get(1).map(|s| s.trim()).unwrap_or("");
|
||||
|
||||
match command.as_str() {
|
||||
"/recent" => command_handlers::handle_recent(db).await,
|
||||
"/search" => {
|
||||
"recent" => command_handlers::handle_recent(db).await,
|
||||
"search" => {
|
||||
if args.is_empty() {
|
||||
super::types::RichMessage::info("用法: /search <关键词>")
|
||||
super::types::RichMessage::info(format!("用法: {prefix}search <关键词>"))
|
||||
.with_title("参数错误")
|
||||
} else {
|
||||
command_handlers::handle_search(db, args).await
|
||||
}
|
||||
}
|
||||
"/detail" => {
|
||||
"detail" => {
|
||||
if let Ok(id) = args.parse::<i32>() {
|
||||
command_handlers::handle_detail(db, id).await
|
||||
} else {
|
||||
super::types::RichMessage::info("用法: /detail <会话ID>")
|
||||
super::types::RichMessage::info(format!("用法: {prefix}detail <会话ID>"))
|
||||
.with_title("参数错误")
|
||||
}
|
||||
}
|
||||
"/today" => command_handlers::handle_today(db).await,
|
||||
"/status" => command_handlers::handle_status(manager).await,
|
||||
"/help" | "/start" => command_handlers::handle_help(),
|
||||
"today" => command_handlers::handle_today(db).await,
|
||||
"status" => command_handlers::handle_status(manager).await,
|
||||
"help" | "start" => command_handlers::handle_help(prefix),
|
||||
_ => {
|
||||
if text.starts_with('/') {
|
||||
super::types::RichMessage::info(format!(
|
||||
"未知命令: {}\n输入 /help 查看可用命令",
|
||||
command
|
||||
))
|
||||
.with_title("未知命令")
|
||||
} else {
|
||||
// Non-command messages are ignored
|
||||
return command_handlers::handle_help();
|
||||
}
|
||||
super::types::RichMessage::info(format!(
|
||||
"未知命令: {prefix}{command}\n输入 {prefix}help 查看可用命令",
|
||||
))
|
||||
.with_title("未知命令")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,14 +205,14 @@ pub async fn handle_status(manager: &ChatChannelManager) -> RichMessage {
|
||||
RichMessage::info(body.trim_end()).with_title("渠道状态")
|
||||
}
|
||||
|
||||
pub fn handle_help() -> RichMessage {
|
||||
RichMessage::info(
|
||||
"/recent - 最近 5 条会话\n\
|
||||
/search <关键词> - 搜索会话\n\
|
||||
/detail <ID> - 会话详情\n\
|
||||
/today - 今日活动汇总\n\
|
||||
/status - 渠道连接状态\n\
|
||||
/help - 显示帮助",
|
||||
)
|
||||
pub fn handle_help(prefix: &str) -> RichMessage {
|
||||
RichMessage::info(format!(
|
||||
"{prefix}recent - 最近 5 条会话\n\
|
||||
{prefix}search <关键词> - 搜索会话\n\
|
||||
{prefix}detail <ID> - 会话详情\n\
|
||||
{prefix}today - 今日活动汇总\n\
|
||||
{prefix}status - 渠道连接状态\n\
|
||||
{prefix}help - 显示帮助",
|
||||
))
|
||||
.with_title("Codeg Bot 帮助")
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use tokio::task::JoinHandle;
|
||||
use super::manager::ChatChannelManager;
|
||||
use super::message_formatter;
|
||||
use super::types::RichMessage;
|
||||
use crate::db::service::{chat_channel_message_log_service, chat_channel_service};
|
||||
use crate::db::service::{app_metadata_service, chat_channel_message_log_service, chat_channel_service};
|
||||
use crate::web::event_bridge::WebEventBroadcaster;
|
||||
|
||||
/// Minimum interval between pushes for the same event type per channel (debounce).
|
||||
@@ -38,6 +38,19 @@ pub fn spawn_event_subscriber(
|
||||
|
||||
let message = match parse_event(&event.channel, &event.payload) {
|
||||
Some((event_type, msg)) => {
|
||||
// Global event filter check
|
||||
let global_filter = app_metadata_service::get_value(&db_conn, "chat_event_filter")
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|json| serde_json::from_str::<Vec<String>>(&json).ok());
|
||||
|
||||
if let Some(filter) = &global_filter {
|
||||
if !filter.contains(&event_type) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check enabled channels and forward
|
||||
let channels = match chat_channel_service::list_enabled(&db_conn).await {
|
||||
Ok(c) => c,
|
||||
@@ -48,17 +61,6 @@ pub fn spawn_event_subscriber(
|
||||
};
|
||||
|
||||
for ch in &channels {
|
||||
// Check event filter
|
||||
if let Some(filter_json) = &ch.event_filter_json {
|
||||
if let Ok(filter) =
|
||||
serde_json::from_str::<Vec<String>>(filter_json)
|
||||
{
|
||||
if !filter.contains(&event_type) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce
|
||||
let key = (ch.id, event_type.clone());
|
||||
let now = Instant::now();
|
||||
|
||||
@@ -264,6 +264,85 @@ pub async fn list_chat_channel_messages_core(
|
||||
Ok(rows.into_iter().map(ChatChannelMessageLogInfo::from).collect())
|
||||
}
|
||||
|
||||
const COMMAND_PREFIX_KEY: &str = "chat_command_prefix";
|
||||
const DEFAULT_COMMAND_PREFIX: &str = "/";
|
||||
|
||||
pub async fn get_chat_command_prefix_core(
|
||||
db: &AppDatabase,
|
||||
) -> Result<String, AppCommandError> {
|
||||
let val = crate::db::service::app_metadata_service::get_value(&db.conn, COMMAND_PREFIX_KEY)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(val.unwrap_or_else(|| DEFAULT_COMMAND_PREFIX.to_string()))
|
||||
}
|
||||
|
||||
pub async fn set_chat_command_prefix_core(
|
||||
db: &AppDatabase,
|
||||
prefix: String,
|
||||
) -> Result<(), AppCommandError> {
|
||||
let trimmed = prefix.trim();
|
||||
if trimmed.is_empty()
|
||||
|| trimmed.len() > 3
|
||||
|| trimmed.chars().any(|c| c.is_alphanumeric())
|
||||
{
|
||||
return Err(AppCommandError::invalid_input(
|
||||
"Prefix must be 1-3 non-alphanumeric characters",
|
||||
));
|
||||
}
|
||||
crate::db::service::app_metadata_service::upsert_value(&db.conn, COMMAND_PREFIX_KEY, trimmed)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const EVENT_FILTER_KEY: &str = "chat_event_filter";
|
||||
|
||||
pub async fn get_chat_event_filter_core(
|
||||
db: &AppDatabase,
|
||||
) -> Result<Option<Vec<String>>, AppCommandError> {
|
||||
let val = crate::db::service::app_metadata_service::get_value(&db.conn, EVENT_FILTER_KEY)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
match val {
|
||||
Some(json) => {
|
||||
let arr: Vec<String> =
|
||||
serde_json::from_str(&json).map_err(|e| AppCommandError::invalid_input(e.to_string()))?;
|
||||
Ok(Some(arr))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_chat_event_filter_core(
|
||||
db: &AppDatabase,
|
||||
filter: Option<Vec<String>>,
|
||||
) -> Result<(), AppCommandError> {
|
||||
match filter {
|
||||
Some(arr) => {
|
||||
let json = serde_json::to_string(&arr)
|
||||
.map_err(|e| AppCommandError::invalid_input(e.to_string()))?;
|
||||
crate::db::service::app_metadata_service::upsert_value(
|
||||
&db.conn,
|
||||
EVENT_FILTER_KEY,
|
||||
&json,
|
||||
)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
}
|
||||
None => {
|
||||
// null means all events enabled — remove the key
|
||||
crate::db::service::app_metadata_service::upsert_value(
|
||||
&db.conn,
|
||||
EVENT_FILTER_KEY,
|
||||
"null",
|
||||
)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tauri commands (use tauri::State for injection)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -378,3 +457,37 @@ pub async fn list_chat_channel_messages(
|
||||
) -> Result<Vec<ChatChannelMessageLogInfo>, AppCommandError> {
|
||||
list_chat_channel_messages_core(&db, channel_id, limit, offset).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
#[tauri::command]
|
||||
pub async fn get_chat_command_prefix(
|
||||
db: tauri::State<'_, AppDatabase>,
|
||||
) -> Result<String, AppCommandError> {
|
||||
get_chat_command_prefix_core(&db).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
#[tauri::command]
|
||||
pub async fn set_chat_command_prefix(
|
||||
db: tauri::State<'_, AppDatabase>,
|
||||
prefix: String,
|
||||
) -> Result<(), AppCommandError> {
|
||||
set_chat_command_prefix_core(&db, prefix).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
#[tauri::command]
|
||||
pub async fn get_chat_event_filter(
|
||||
db: tauri::State<'_, AppDatabase>,
|
||||
) -> Result<Option<Vec<String>>, AppCommandError> {
|
||||
get_chat_event_filter_core(&db).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
#[tauri::command]
|
||||
pub async fn set_chat_event_filter(
|
||||
db: tauri::State<'_, AppDatabase>,
|
||||
filter: Option<Vec<String>>,
|
||||
) -> Result<(), AppCommandError> {
|
||||
set_chat_event_filter_core(&db, filter).await
|
||||
}
|
||||
|
||||
@@ -379,6 +379,10 @@ mod tauri_app {
|
||||
chat_channel_commands::test_chat_channel,
|
||||
chat_channel_commands::get_chat_channel_status,
|
||||
chat_channel_commands::list_chat_channel_messages,
|
||||
chat_channel_commands::get_chat_command_prefix,
|
||||
chat_channel_commands::set_chat_command_prefix,
|
||||
chat_channel_commands::get_chat_event_filter,
|
||||
chat_channel_commands::set_chat_event_filter,
|
||||
web::start_web_server,
|
||||
web::stop_web_server,
|
||||
web::get_web_server_status,
|
||||
|
||||
@@ -183,3 +183,45 @@ pub async fn list_chat_channel_messages(
|
||||
.await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetCommandPrefixParams {
|
||||
pub prefix: String,
|
||||
}
|
||||
|
||||
pub async fn get_chat_command_prefix(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<String>, AppCommandError> {
|
||||
let result = cc_commands::get_chat_command_prefix_core(&state.db).await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn set_chat_command_prefix(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<SetCommandPrefixParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
cc_commands::set_chat_command_prefix_core(&state.db, params.prefix).await?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetEventFilterParams {
|
||||
pub filter: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub async fn get_chat_event_filter(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<Option<Vec<String>>>, AppCommandError> {
|
||||
let result = cc_commands::get_chat_event_filter_core(&state.db).await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn set_chat_event_filter(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<SetEventFilterParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
cc_commands::set_chat_event_filter_core(&state.db, params.filter).await?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
@@ -193,6 +193,10 @@ pub fn build_router(state: Arc<AppState>, token: String, static_dir: std::path::
|
||||
.route("/test_chat_channel", post(handlers::chat_channel::test_chat_channel))
|
||||
.route("/get_chat_channel_status", post(handlers::chat_channel::get_chat_channel_status))
|
||||
.route("/list_chat_channel_messages", post(handlers::chat_channel::list_chat_channel_messages))
|
||||
.route("/get_chat_command_prefix", post(handlers::chat_channel::get_chat_command_prefix))
|
||||
.route("/set_chat_command_prefix", post(handlers::chat_channel::set_chat_command_prefix))
|
||||
.route("/get_chat_event_filter", post(handlers::chat_channel::get_chat_event_filter))
|
||||
.route("/set_chat_event_filter", post(handlers::chat_channel::set_chat_event_filter))
|
||||
// ─── Terminal ───
|
||||
.route("/terminal_spawn", post(handlers::terminal::terminal_spawn))
|
||||
.route("/terminal_write", post(handlers::terminal::terminal_write))
|
||||
|
||||
Reference in New Issue
Block a user