optimize: channel Message Commands — Multilingual Support and Prefixes

This commit is contained in:
xintaofei
2026-04-01 16:22:54 +08:00
parent 05214d09de
commit adb5829613
4 changed files with 247 additions and 99 deletions

View File

@@ -156,7 +156,7 @@ async fn dispatch_command(
}; };
if has_session { if has_session {
return session_commands::handle_followup( return session_commands::handle_followup(
db, text, channel_id, sender_id, conn_mgr, bridge, lang, db, text, channel_id, sender_id, conn_mgr, bridge, lang, prefix,
) )
.await; .await;
} }
@@ -193,23 +193,23 @@ async fn dispatch_command(
// Session commands // Session commands
"folder" => { "folder" => {
session_commands::handle_folder(db, args, channel_id, sender_id, lang).await session_commands::handle_folder(db, args, channel_id, sender_id, lang, prefix).await
} }
"agent" => { "agent" => {
session_commands::handle_agent(db, args, channel_id, sender_id, lang).await session_commands::handle_agent(db, args, channel_id, sender_id, lang, prefix).await
} }
"task" | "do" => { "task" | "do" => {
session_commands::handle_task( session_commands::handle_task(
db, args, channel_id, sender_id, conn_mgr, emitter, bridge, lang, db, args, channel_id, sender_id, conn_mgr, emitter, bridge, lang, prefix,
) )
.await .await
} }
"sessions" => { "sessions" => {
session_commands::handle_sessions(db, channel_id, sender_id, lang).await session_commands::handle_sessions(db, channel_id, sender_id, lang, prefix).await
} }
"resume" => { "resume" => {
session_commands::handle_resume( session_commands::handle_resume(
db, args, channel_id, sender_id, conn_mgr, emitter, bridge, lang, db, args, channel_id, sender_id, conn_mgr, emitter, bridge, lang, prefix,
) )
.await .await
} }

View File

@@ -610,14 +610,14 @@ pub fn help_title(lang: Lang) -> &'static str {
pub fn help_body(lang: Lang, prefix: &str) -> String { pub fn help_body(lang: Lang, prefix: &str) -> String {
match lang { match lang {
Lang::ZhCn => format!( Lang::ZhCn => format!(
"📂 {prefix}folder - 选择工作目录\n\ "{prefix}folder - 选择工作目录\n\
🤖 {prefix}agent - 选择 Agent\n\ {prefix}agent - 选择 Agent\n\
🚀 {prefix}task <描述> - 创建会话并执行任务\n\ {prefix}task <描述> - 创建会话并执行任务\n\
📋 {prefix}sessions - 当前目录的活跃会话\n\ {prefix}sessions - 当前目录的活跃会话\n\
▶️ {prefix}resume <ID> - 恢复已有会话\n\ {prefix}resume <ID> - 恢复已有会话\n\
⏹️ {prefix}cancel - 取消当前任务\n\ {prefix}cancel - 取消当前任务\n\
{prefix}approve [always] - 批准权限请求\n\ {prefix}approve [always] - 批准权限请求\n\
{prefix}deny - 拒绝权限请求\n\ {prefix}deny - 拒绝权限请求\n\
\n\ \n\
{prefix}recent - 最近 5 条会话\n\ {prefix}recent - 最近 5 条会话\n\
{prefix}search <关键词> - 搜索会话\n\ {prefix}search <关键词> - 搜索会话\n\
@@ -626,81 +626,169 @@ pub fn help_body(lang: Lang, prefix: &str) -> String {
{prefix}status - 渠道连接状态\n\ {prefix}status - 渠道连接状态\n\
{prefix}help - 显示帮助\n\ {prefix}help - 显示帮助\n\
\n\ \n\
💡 有活跃会话时,直接发文本即可继续对话" 有活跃会话时,直接发文本即可继续对话"
), ),
Lang::ZhTw => format!( Lang::ZhTw => format!(
"{prefix}recent - 最近 5 條對話\n\ "{prefix}folder - 選擇工作目錄\n\
{prefix}agent - 選擇 Agent\n\
{prefix}task <描述> - 建立對話並執行任務\n\
{prefix}sessions - 當前目錄的活躍對話\n\
{prefix}resume <ID> - 恢復已有對話\n\
{prefix}cancel - 取消當前任務\n\
{prefix}approve [always] - 批准權限請求\n\
{prefix}deny - 拒絕權限請求\n\
\n\
{prefix}recent - 最近 5 條對話\n\
{prefix}search <關鍵字> - 搜尋對話\n\ {prefix}search <關鍵字> - 搜尋對話\n\
{prefix}detail <ID> - 對話詳情\n\ {prefix}detail <ID> - 對話詳情\n\
{prefix}today - 今日活動匯總\n\ {prefix}today - 今日活動匯總\n\
{prefix}status - 頻道連線狀態\n\ {prefix}status - 頻道連線狀態\n\
{prefix}help - 顯示幫助" {prefix}help - 顯示幫助\n\
\n\
有活躍對話時,直接發文字即可繼續對話"
), ),
Lang::Ja => format!( Lang::Ja => format!(
"{prefix}recent - 最新5件のセッション\n\ "{prefix}folder - 作業フォルダを選択\n\
{prefix}agent - エージェントを選択\n\
{prefix}task <説明> - セッションを作成してタスクを実行\n\
{prefix}sessions - フォルダ内のアクティブセッション\n\
{prefix}resume <ID> - セッションを再開\n\
{prefix}cancel - 現在のタスクをキャンセル\n\
{prefix}approve [always] - 権限を承認\n\
{prefix}deny - 権限を拒否\n\
\n\
{prefix}recent - 最新5件のセッション\n\
{prefix}search <キーワード> - セッション検索\n\ {prefix}search <キーワード> - セッション検索\n\
{prefix}detail <ID> - セッション詳細\n\ {prefix}detail <ID> - セッション詳細\n\
{prefix}today - 本日の活動まとめ\n\ {prefix}today - 本日の活動まとめ\n\
{prefix}status - チャンネル接続状況\n\ {prefix}status - チャンネル接続状況\n\
{prefix}help - ヘルプを表示" {prefix}help - ヘルプを表示\n\
\n\
セッションがアクティブな場合、テキストを送信するだけで会話を続けられます"
), ),
Lang::Ko => format!( Lang::Ko => format!(
"{prefix}recent - 최근 5개 대화\n\ "{prefix}folder - 작업 폴더 선택\n\
{prefix}agent - 에이전트 선택\n\
{prefix}task <설명> - 세션 생성 및 작업 실행\n\
{prefix}sessions - 폴더 내 활성 세션\n\
{prefix}resume <ID> - 세션 재개\n\
{prefix}cancel - 현재 작업 취소\n\
{prefix}approve [always] - 권한 승인\n\
{prefix}deny - 권한 거부\n\
\n\
{prefix}recent - 최근 5개 대화\n\
{prefix}search <키워드> - 대화 검색\n\ {prefix}search <키워드> - 대화 검색\n\
{prefix}detail <ID> - 대화 상세\n\ {prefix}detail <ID> - 대화 상세\n\
{prefix}today - 오늘의 활동 요약\n\ {prefix}today - 오늘의 활동 요약\n\
{prefix}status - 채널 연결 상태\n\ {prefix}status - 채널 연결 상태\n\
{prefix}help - 도움말 표시" {prefix}help - 도움말 표시\n\
\n\
세션이 활성화된 경우 텍스트를 보내면 대화를 계속할 수 있습니다"
), ),
Lang::Es => format!( Lang::Es => format!(
"{prefix}recent - 5 conversaciones más recientes\n\ "{prefix}folder - Seleccionar carpeta de trabajo\n\
{prefix}agent - Seleccionar agente\n\
{prefix}task <desc> - Crear sesion y ejecutar tarea\n\
{prefix}sessions - Sesiones activas en la carpeta\n\
{prefix}resume <ID> - Reanudar una sesion\n\
{prefix}cancel - Cancelar tarea actual\n\
{prefix}approve [always] - Aprobar permiso\n\
{prefix}deny - Denegar permiso\n\
\n\
{prefix}recent - 5 conversaciones mas recientes\n\
{prefix}search <palabra> - Buscar conversaciones\n\ {prefix}search <palabra> - Buscar conversaciones\n\
{prefix}detail <ID> - Detalles de conversación\n\ {prefix}detail <ID> - Detalles de conversacion\n\
{prefix}today - Resumen de hoy\n\ {prefix}today - Resumen de hoy\n\
{prefix}status - Estado de canales\n\ {prefix}status - Estado de canales\n\
{prefix}help - Mostrar ayuda" {prefix}help - Mostrar ayuda\n\
\n\
Cuando hay una sesion activa, simplemente escriba texto para continuar"
), ),
Lang::De => format!( Lang::De => format!(
"{prefix}recent - 5 neueste Sitzungen\n\ "{prefix}folder - Arbeitsordner auswahlen\n\
{prefix}agent - Agent auswahlen\n\
{prefix}task <Beschreibung> - Sitzung erstellen und Aufgabe ausfuhren\n\
{prefix}sessions - Aktive Sitzungen im Ordner\n\
{prefix}resume <ID> - Sitzung fortsetzen\n\
{prefix}cancel - Aktuelle Aufgabe abbrechen\n\
{prefix}approve [always] - Berechtigung genehmigen\n\
{prefix}deny - Berechtigung verweigern\n\
\n\
{prefix}recent - 5 neueste Sitzungen\n\
{prefix}search <Stichwort> - Sitzungen suchen\n\ {prefix}search <Stichwort> - Sitzungen suchen\n\
{prefix}detail <ID> - Sitzungsdetails\n\ {prefix}detail <ID> - Sitzungsdetails\n\
{prefix}today - Heutige Zusammenfassung\n\ {prefix}today - Heutige Zusammenfassung\n\
{prefix}status - Kanalstatus\n\ {prefix}status - Kanalstatus\n\
{prefix}help - Hilfe anzeigen" {prefix}help - Hilfe anzeigen\n\
\n\
Bei aktiver Sitzung einfach Text eingeben, um das Gesprach fortzusetzen"
), ),
Lang::Fr => format!( Lang::Fr => format!(
"{prefix}recent - 5 dernières sessions\n\ "{prefix}folder - Selectionner le dossier de travail\n\
{prefix}search <mot-clé> - Rechercher des sessions\n\ {prefix}agent - Selectionner l'agent\n\
{prefix}detail <ID> - Détails de la session\n\ {prefix}task <desc> - Creer une session et executer une tache\n\
{prefix}today - Résumé du jour\n\ {prefix}sessions - Sessions actives dans le dossier\n\
{prefix}resume <ID> - Reprendre une session\n\
{prefix}cancel - Annuler la tache en cours\n\
{prefix}approve [always] - Approuver la permission\n\
{prefix}deny - Refuser la permission\n\
\n\
{prefix}recent - 5 dernieres sessions\n\
{prefix}search <mot-cle> - Rechercher des sessions\n\
{prefix}detail <ID> - Details de la session\n\
{prefix}today - Resume du jour\n\
{prefix}status - Statut des canaux\n\ {prefix}status - Statut des canaux\n\
{prefix}help - Afficher l'aide" {prefix}help - Afficher l'aide\n\
\n\
Lorsqu'une session est active, envoyez du texte pour continuer la conversation"
), ),
Lang::Pt => format!( Lang::Pt => format!(
"{prefix}recent - 5 sessões mais recentes\n\ "{prefix}folder - Selecionar pasta de trabalho\n\
{prefix}search <palavra> - Buscar sessões\n\ {prefix}agent - Selecionar agente\n\
{prefix}detail <ID> - Detalhes da sessão\n\ {prefix}task <desc> - Criar sessao e executar tarefa\n\
{prefix}sessions - Sessoes ativas na pasta\n\
{prefix}resume <ID> - Retomar uma sessao\n\
{prefix}cancel - Cancelar tarefa atual\n\
{prefix}approve [always] - Aprovar permissao\n\
{prefix}deny - Negar permissao\n\
\n\
{prefix}recent - 5 sessoes mais recentes\n\
{prefix}search <palavra> - Buscar sessoes\n\
{prefix}detail <ID> - Detalhes da sessao\n\
{prefix}today - Resumo de hoje\n\ {prefix}today - Resumo de hoje\n\
{prefix}status - Status dos canais\n\ {prefix}status - Status dos canais\n\
{prefix}help - Mostrar ajuda" {prefix}help - Mostrar ajuda\n\
\n\
Quando uma sessao esta ativa, basta digitar texto para continuar a conversa"
), ),
Lang::Ar => format!( Lang::Ar => format!(
"{prefix}recent - أحدث 5 جلسات\n\ "{prefix}folder - اختيار مجلد العمل\n\
{prefix}agent - اختيار الوكيل\n\
{prefix}task <وصف> - انشاء جلسة وتنفيذ مهمة\n\
{prefix}sessions - الجلسات النشطة في المجلد\n\
{prefix}resume <ID> - استئناف جلسة\n\
{prefix}cancel - الغاء المهمة الحالية\n\
{prefix}approve [always] - الموافقة على الاذن\n\
{prefix}deny - رفض الاذن\n\
\n\
{prefix}recent - احدث 5 جلسات\n\
{prefix}search <كلمة> - البحث في الجلسات\n\ {prefix}search <كلمة> - البحث في الجلسات\n\
{prefix}detail <ID> - تفاصيل الجلسة\n\ {prefix}detail <ID> - تفاصيل الجلسة\n\
{prefix}today - ملخص اليوم\n\ {prefix}today - ملخص اليوم\n\
{prefix}status - حالة القنوات\n\ {prefix}status - حالة القنوات\n\
{prefix}help - عرض المساعدة" {prefix}help - عرض المساعدة\n\
\n\
عندما تكون الجلسة نشطة، ارسل نصا لمتابعة المحادثة"
), ),
Lang::En => format!( Lang::En => format!(
"📂 {prefix}folder - Select working folder\n\ "{prefix}folder - Select working folder\n\
🤖 {prefix}agent - Select agent\n\ {prefix}agent - Select agent\n\
🚀 {prefix}task <desc> - Create session & run task\n\ {prefix}task <desc> - Create session & run task\n\
📋 {prefix}sessions - Active sessions in folder\n\ {prefix}sessions - Active sessions in folder\n\
▶️ {prefix}resume <ID> - Resume a session\n\ {prefix}resume <ID> - Resume a session\n\
⏹️ {prefix}cancel - Cancel current task\n\ {prefix}cancel - Cancel current task\n\
{prefix}approve [always] - Approve permission\n\ {prefix}approve [always] - Approve permission\n\
{prefix}deny - Deny permission\n\ {prefix}deny - Deny permission\n\
\n\ \n\
{prefix}recent - 5 most recent conversations\n\ {prefix}recent - 5 most recent conversations\n\
{prefix}search <keyword> - Search conversations\n\ {prefix}search <keyword> - Search conversations\n\
@@ -709,7 +797,7 @@ pub fn help_body(lang: Lang, prefix: &str) -> String {
{prefix}status - Channel connection status\n\ {prefix}status - Channel connection status\n\
{prefix}help - Show help\n\ {prefix}help - Show help\n\
\n\ \n\
💡 When a session is active, just type text to continue the conversation" When a session is active, just type text to continue the conversation"
), ),
} }
} }
@@ -810,3 +898,20 @@ pub fn unknown_command_title(lang: Lang) -> &'static str {
Lang::En => "Unknown Command", Lang::En => "Unknown Command",
} }
} }
// ── Session progress messages ──
pub fn agent_responding(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "Claude Code 正在响应中...",
Lang::ZhTw => "Claude Code 正在回應中...",
Lang::Ja => "Claude Code が応答中...",
Lang::Ko => "Claude Code 응답 중...",
Lang::Es => "Claude Code respondiendo...",
Lang::De => "Claude Code antwortet...",
Lang::Fr => "Claude Code en cours de reponse...",
Lang::Pt => "Claude Code respondendo...",
Lang::Ar => "...Claude Code يستجيب",
Lang::En => "Claude Code is responding...",
}
}

View File

@@ -24,14 +24,15 @@ pub async fn handle_folder(
channel_id: i32, channel_id: i32,
sender_id: &str, sender_id: &str,
lang: Lang, lang: Lang,
prefix: &str,
) -> RichMessage { ) -> RichMessage {
if args.is_empty() { if args.is_empty() {
return list_folders(db, channel_id, sender_id, lang).await; return list_folders(db, channel_id, sender_id, lang, prefix).await;
} }
// Try parse as index (1-based) // Try parse as index (1-based)
if let Ok(idx) = args.parse::<usize>() { if let Ok(idx) = args.parse::<usize>() {
return select_folder_by_index(db, idx, channel_id, sender_id, lang).await; return select_folder_by_index(db, idx, channel_id, sender_id, lang, prefix).await;
} }
// Treat as path // Treat as path
@@ -43,6 +44,7 @@ async fn list_folders(
channel_id: i32, channel_id: i32,
sender_id: &str, sender_id: &str,
lang: Lang, lang: Lang,
prefix: &str,
) -> RichMessage { ) -> RichMessage {
let folders = match folder_service::list_folders(db).await { let folders = match folder_service::list_folders(db).await {
Ok(f) => f, Ok(f) => f,
@@ -77,10 +79,11 @@ async fn list_folders(
body.push_str(&format!( body.push_str(&format!(
"\n{}", "\n{}",
t( tp(
lang, lang,
"Reply /folder <number> to select.", prefix,
"回复 /folder <数字> 选择目录。" "Reply {prefix}folder <number> to select.",
"回复 {prefix}folder <数字> 选择目录。"
) )
)); ));
@@ -94,6 +97,7 @@ async fn select_folder_by_index(
channel_id: i32, channel_id: i32,
sender_id: &str, sender_id: &str,
lang: Lang, lang: Lang,
prefix: &str,
) -> RichMessage { ) -> RichMessage {
if idx == 0 { if idx == 0 {
return RichMessage::info(t(lang, "Index starts from 1.", "序号从 1 开始。")); return RichMessage::info(t(lang, "Index starts from 1.", "序号从 1 开始。"));
@@ -105,10 +109,11 @@ async fn select_folder_by_index(
}; };
let Some(folder) = folders.get(idx - 1) else { let Some(folder) = folders.get(idx - 1) else {
return RichMessage::info(t( return RichMessage::info(tp(
lang, lang,
"Index out of range. Use /folder to list.", prefix,
"序号超出范围,请使用 /folder 查看列表。", "Index out of range. Use {prefix}folder to list.",
"序号超出范围,请使用 {prefix}folder 查看列表。",
)); ));
}; };
@@ -146,14 +151,15 @@ pub async fn handle_agent(
channel_id: i32, channel_id: i32,
sender_id: &str, sender_id: &str,
lang: Lang, lang: Lang,
prefix: &str,
) -> RichMessage { ) -> RichMessage {
if args.is_empty() { if args.is_empty() {
return list_agents(db, channel_id, sender_id, lang).await; return list_agents(db, channel_id, sender_id, lang, prefix).await;
} }
// Try parse as index // Try parse as index
if let Ok(idx) = args.parse::<usize>() { if let Ok(idx) = args.parse::<usize>() {
return select_agent_by_index(db, idx, channel_id, sender_id, lang).await; return select_agent_by_index(db, idx, channel_id, sender_id, lang, prefix).await;
} }
// Try parse as agent type name // Try parse as agent type name
@@ -165,6 +171,7 @@ async fn list_agents(
channel_id: i32, channel_id: i32,
sender_id: &str, sender_id: &str,
lang: Lang, lang: Lang,
prefix: &str,
) -> RichMessage { ) -> RichMessage {
let agents = all_acp_agents(); let agents = all_acp_agents();
let ctx = sender_context_service::get_or_create(db, channel_id, sender_id) let ctx = sender_context_service::get_or_create(db, channel_id, sender_id)
@@ -185,10 +192,11 @@ async fn list_agents(
body.push_str(&format!( body.push_str(&format!(
"\n{}", "\n{}",
t( tp(
lang, lang,
"Reply /agent <number> or /agent <name> to select.", prefix,
"回复 /agent <数字> 或 /agent <名称> 选择。" "Reply {prefix}agent <number> or {prefix}agent <name> to select.",
"回复 {prefix}agent <数字> 或 {prefix}agent <名称> 选择。"
) )
)); ));
@@ -202,13 +210,15 @@ async fn select_agent_by_index(
channel_id: i32, channel_id: i32,
sender_id: &str, sender_id: &str,
lang: Lang, lang: Lang,
prefix: &str,
) -> RichMessage { ) -> RichMessage {
let agents = all_acp_agents(); let agents = all_acp_agents();
if idx == 0 || idx > agents.len() { if idx == 0 || idx > agents.len() {
return RichMessage::info(t( return RichMessage::info(tp(
lang, lang,
"Index out of range. Use /agent to list.", prefix,
"序号超出范围,请使用 /agent 查看列表。", "Index out of range. Use {prefix}agent to list.",
"序号超出范围,请使用 {prefix}agent 查看列表。",
)); ));
} }
@@ -257,12 +267,14 @@ pub async fn handle_task(
emitter: &EventEmitter, emitter: &EventEmitter,
bridge: &Arc<Mutex<SessionBridge>>, bridge: &Arc<Mutex<SessionBridge>>,
lang: Lang, lang: Lang,
prefix: &str,
) -> RichMessage { ) -> RichMessage {
if task_description.is_empty() { if task_description.is_empty() {
return RichMessage::info(t( return RichMessage::info(tp(
lang, lang,
"Usage: /task <description>", prefix,
"用法: /task <任务描述>", "Usage: {prefix}task <description>",
"用法: {prefix}task <任务描述>",
)); ));
} }
@@ -275,10 +287,11 @@ pub async fn handle_task(
let folder_id = match ctx.current_folder_id { let folder_id = match ctx.current_folder_id {
Some(id) => id, Some(id) => id,
None => { None => {
return RichMessage::info(t( return RichMessage::info(tp(
lang, lang,
"No folder selected. Use /folder first.", prefix,
"未选择工作目录,请先使用 /folder 选择。", "No folder selected. Use {prefix}folder first.",
"未选择工作目录,请先使用 {prefix}folder 选择。",
)); ));
} }
}; };
@@ -287,10 +300,11 @@ pub async fn handle_task(
let folder = match folder_service::get_folder_by_id(db, folder_id).await { let folder = match folder_service::get_folder_by_id(db, folder_id).await {
Ok(Some(f)) => f, Ok(Some(f)) => f,
_ => { _ => {
return RichMessage::info(t( return RichMessage::info(tp(
lang, lang,
"Folder not found. Use /folder to select.", prefix,
"目录不存在,请使用 /folder 重新选择。", "Folder not found. Use {prefix}folder to select.",
"目录不存在,请使用 {prefix}folder 重新选择。",
)); ));
} }
}; };
@@ -381,6 +395,7 @@ pub async fn handle_sessions(
channel_id: i32, channel_id: i32,
sender_id: &str, sender_id: &str,
lang: Lang, lang: Lang,
prefix: &str,
) -> RichMessage { ) -> RichMessage {
let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await {
Ok(c) => c, Ok(c) => c,
@@ -390,10 +405,11 @@ pub async fn handle_sessions(
let folder_id = match ctx.current_folder_id { let folder_id = match ctx.current_folder_id {
Some(id) => id, Some(id) => id,
None => { None => {
return RichMessage::info(t( return RichMessage::info(tp(
lang, lang,
"No folder selected. Use /folder first.", prefix,
"未选择工作目录,请先使用 /folder 选择。", "No folder selected. Use {prefix}folder first.",
"未选择工作目录,请先使用 {prefix}folder 选择。",
)); ));
} }
}; };
@@ -456,10 +472,11 @@ pub async fn handle_sessions(
body.push_str(&format!( body.push_str(&format!(
"\n{}", "\n{}",
t( tp(
lang, lang,
"Reply /resume <id> to continue.", prefix,
"回复 /resume <ID> 继续会话。" "Reply {prefix}resume <id> to continue.",
"回复 {prefix}resume <ID> 继续会话。"
) )
)); ));
@@ -482,14 +499,16 @@ pub async fn handle_resume(
emitter: &EventEmitter, emitter: &EventEmitter,
bridge: &Arc<Mutex<SessionBridge>>, bridge: &Arc<Mutex<SessionBridge>>,
lang: Lang, lang: Lang,
prefix: &str,
) -> RichMessage { ) -> RichMessage {
let conversation_id: i32 = match args.parse() { let conversation_id: i32 = match args.parse() {
Ok(id) => id, Ok(id) => id,
Err(_) => { Err(_) => {
return RichMessage::info(t( return RichMessage::info(tp(
lang, lang,
"Usage: /resume <conversation_id>", prefix,
"用法: /resume <会话ID>", "Usage: {prefix}resume <conversation_id>",
"用法: {prefix}resume <会话ID>",
)); ));
} }
}; };
@@ -753,6 +772,7 @@ pub async fn handle_followup(
conn_mgr: &ConnectionManager, conn_mgr: &ConnectionManager,
bridge: &Arc<Mutex<SessionBridge>>, bridge: &Arc<Mutex<SessionBridge>>,
lang: Lang, lang: Lang,
prefix: &str,
) -> RichMessage { ) -> RichMessage {
let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await {
Ok(c) => c, Ok(c) => c,
@@ -762,10 +782,11 @@ pub async fn handle_followup(
let connection_id = match &ctx.current_connection_id { let connection_id = match &ctx.current_connection_id {
Some(id) => id.clone(), Some(id) => id.clone(),
None => { None => {
return RichMessage::info(t( return RichMessage::info(tp(
lang, lang,
"No active session. Use /task to start one.", prefix,
"没有活跃的会话,请使用 /task 开始新任务。", "No active session. Use {prefix}task to start one.",
"没有活跃的会话,请使用 {prefix}task 开始新任务。",
)); ));
} }
}; };
@@ -777,10 +798,11 @@ pub async fn handle_followup(
// Connection lost, clear context // Connection lost, clear context
drop(bridge_guard); drop(bridge_guard);
let _ = sender_context_service::clear_session(db, channel_id, sender_id).await; let _ = sender_context_service::clear_session(db, channel_id, sender_id).await;
return RichMessage::info(t( return RichMessage::info(tp(
lang, lang,
"Session connection lost. Use /task to start a new one.", prefix,
"会话连接已断开,请使用 /task 开始新任务。", "Session connection lost. Use {prefix}task to start a new one.",
"会话连接已断开,请使用 {prefix}task 开始新任务。",
)); ));
} }
} }
@@ -812,6 +834,11 @@ fn t(lang: Lang, en: &str, zh: &str) -> String {
} }
} }
/// Like `t()` but replaces `{prefix}` placeholders with the actual command prefix.
fn tp(lang: Lang, prefix: &str, en: &str, zh: &str) -> String {
t(lang, en, zh).replace("{prefix}", prefix)
}
fn agent_type_to_string(at: AgentType) -> String { fn agent_type_to_string(at: AgentType) -> String {
serde_json::to_value(at) serde_json::to_value(at)
.ok() .ok()

View File

@@ -17,10 +17,12 @@ use crate::web::event_bridge::WebEventBroadcaster;
use super::manager::ChatChannelManager; use super::manager::ChatChannelManager;
const FLUSH_INTERVAL_SECS: u64 = 5; const FLUSH_INTERVAL_SECS: u64 = 10;
const BUFFER_FLUSH_THRESHOLD: usize = 500; const BUFFER_FLUSH_THRESHOLD: usize = 500;
const MAX_MESSAGE_LEN: usize = 2000; const MAX_MESSAGE_LEN: usize = 2000;
const MESSAGE_LANGUAGE_KEY: &str = "chat_message_language"; const MESSAGE_LANGUAGE_KEY: &str = "chat_message_language";
const COMMAND_PREFIX_KEY: &str = "chat_command_prefix";
const DEFAULT_COMMAND_PREFIX: &str = "/";
pub fn spawn_session_event_subscriber( pub fn spawn_session_event_subscriber(
broadcaster: Arc<WebEventBroadcaster>, broadcaster: Arc<WebEventBroadcaster>,
@@ -59,7 +61,7 @@ pub fn spawn_session_event_subscriber(
} }
_ = tokio::time::sleep(Duration::from_secs(FLUSH_INTERVAL_SECS)) => { _ = tokio::time::sleep(Duration::from_secs(FLUSH_INTERVAL_SECS)) => {
if last_heartbeat.elapsed() >= Duration::from_secs(FLUSH_INTERVAL_SECS) { if last_heartbeat.elapsed() >= Duration::from_secs(FLUSH_INTERVAL_SECS) {
flush_progress(&bridge, &manager).await; flush_progress(&bridge, &manager, &db_conn).await;
last_heartbeat = Instant::now(); last_heartbeat = Instant::now();
} }
} }
@@ -77,6 +79,14 @@ async fn get_lang(db: &DatabaseConnection) -> Lang {
.unwrap_or_default() .unwrap_or_default()
} }
async fn get_prefix(db: &DatabaseConnection) -> String {
app_metadata_service::get_value(db, COMMAND_PREFIX_KEY)
.await
.ok()
.flatten()
.unwrap_or_else(|| DEFAULT_COMMAND_PREFIX.to_string())
}
async fn handle_acp_event_payload( async fn handle_acp_event_payload(
payload: &serde_json::Value, payload: &serde_json::Value,
bridge: &Arc<Mutex<SessionBridge>>, bridge: &Arc<Mutex<SessionBridge>>,
@@ -135,11 +145,11 @@ async fn handle_acp_event_payload(
&& session.last_flushed.elapsed() >= Duration::from_secs(2) && session.last_flushed.elapsed() >= Duration::from_secs(2)
{ {
let channel_id = session.channel_id; let channel_id = session.channel_id;
let buf_len = session.content_buffer.len();
let last_tool = session.tool_calls.last().cloned(); let last_tool = session.tool_calls.last().cloned();
session.last_flushed = Instant::now(); session.last_flushed = Instant::now();
let mut status = format!("... ({buf_len} chars)"); let lang = get_lang(db).await;
let mut status = super::i18n::agent_responding(lang).to_string();
if let Some(tool) = last_tool { if let Some(tool) = last_tool {
status.push_str(&format!(" | {tool}")); status.push_str(&format!(" | {tool}"));
} }
@@ -252,12 +262,13 @@ async fn handle_acp_event_payload(
drop(guard); drop(guard);
let lang = get_lang(db).await; let lang = get_lang(db).await;
let prefix = get_prefix(db).await;
let body = match lang { let body = match lang {
Lang::ZhCn | Lang::ZhTw => { Lang::ZhCn | Lang::ZhTw => {
format!("Agent 请求权限: {tool_desc}\n\n/approve 批准 | /deny 拒绝 | /approve always 自动批准") format!("Agent 请求权限: {tool_desc}\n\n{prefix}approve 批准 | {prefix}deny 拒绝 | {prefix}approve always 自动批准")
} }
_ => { _ => {
format!("Agent requests permission: {tool_desc}\n\n/approve | /deny | /approve always") format!("Agent requests permission: {tool_desc}\n\n{prefix}approve | {prefix}deny | {prefix}approve always")
} }
}; };
@@ -387,7 +398,11 @@ async fn handle_acp_event_payload(
} }
} }
async fn flush_progress(bridge: &Arc<Mutex<SessionBridge>>, manager: &ChatChannelManager) { async fn flush_progress(
bridge: &Arc<Mutex<SessionBridge>>,
manager: &ChatChannelManager,
db: &DatabaseConnection,
) {
let updates: Vec<(i32, String)> = { let updates: Vec<(i32, String)> = {
let mut guard = bridge.lock().await; let mut guard = bridge.lock().await;
let mut out = Vec::new(); let mut out = Vec::new();
@@ -395,13 +410,14 @@ async fn flush_progress(bridge: &Arc<Mutex<SessionBridge>>, manager: &ChatChanne
if !session.content_buffer.is_empty() if !session.content_buffer.is_empty()
&& session.last_flushed.elapsed() >= Duration::from_secs(FLUSH_INTERVAL_SECS) && session.last_flushed.elapsed() >= Duration::from_secs(FLUSH_INTERVAL_SECS)
{ {
let buf_len = session.content_buffer.len();
let tool_count = session.tool_calls.len();
session.last_flushed = Instant::now(); session.last_flushed = Instant::now();
out.push(( let last_tool = session.tool_calls.last().cloned();
session.channel_id, let lang = get_lang(db).await;
format!("... ({buf_len} chars, {tool_count} tools)"), let mut status = super::i18n::agent_responding(lang).to_string();
)); if let Some(tool) = last_tool {
status.push_str(&format!(" | {tool}"));
}
out.push((session.channel_id, status));
} }
} }
out out