fix(chat-channel): use session agent in responding indicator and localize command messages

- Replace hardcoded "Claude Code" in agent_responding with each session's actual AgentType label; store agent_type on ActiveSession and populate it in both task-start and resume paths
- Remove silent ClaudeCode fallback in resolve_agent_type; /task now prompts the user to pick an agent when none is selected and the folder has no default
- Move session command strings out of session_commands.rs into i18n.rs with full 10-language coverage (en/zh-CN/zh-TW/ja/ko/es/de/fr/pt/ar)
- Fix French accent and wrap Latin agent name with Arabic bidi isolates (FSI/PDI) for consistent rendering across Telegram/Lark/WeiXin clients
- Release the bridge lock before the get_lang DB lookup in the content_delta flush path to avoid cross-session contention
- Log unknown agent_type fallback in conversation_service::parse_agent_type for observability
This commit is contained in:
xintaofei
2026-04-18 20:05:56 +08:00
parent dcaa4b4f5a
commit ff9fbad50a
5 changed files with 873 additions and 256 deletions

View File

@@ -715,19 +715,737 @@ pub fn unknown_command_title(lang: Lang) -> &'static str {
} }
} }
// ── Session progress messages ── // ── Session command messages ──
pub fn agent_responding(lang: Lang) -> &'static str { // Folder (/folder)
pub fn folder_title(lang: Lang) -> &'static str {
match lang { match lang {
Lang::ZhCn => "Claude Code 正在响应中...", Lang::ZhCn => "工作目录",
Lang::ZhTw => "Claude Code 正在回應中...", Lang::ZhTw => "工作目錄",
Lang::Ja => "Claude Code が応答中...", Lang::Ja => "作業フォルダ",
Lang::Ko => "Claude Code 응답 중...", Lang::Ko => "작업 폴더",
Lang::Es => "Claude Code respondiendo...", Lang::Es => "Carpeta de trabajo",
Lang::De => "Claude Code antwortet...", Lang::De => "Arbeitsordner",
Lang::Fr => "Claude Code en cours de reponse...", Lang::Fr => "Dossier de travail",
Lang::Pt => "Claude Code respondendo...", Lang::Pt => "Pasta de trabalho",
Lang::Ar => "...Claude Code يستجيب", Lang::Ar => "مجلد العمل",
Lang::En => "Claude Code is responding...", Lang::En => "Working Folder",
}
}
pub fn no_folders_found(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "没有找到项目目录。",
Lang::ZhTw => "沒有找到專案目錄。",
Lang::Ja => "フォルダが見つかりません。",
Lang::Ko => "폴더를 찾을 수 없습니다.",
Lang::Es => "No se encontraron carpetas.",
Lang::De => "Keine Ordner gefunden.",
Lang::Fr => "Aucun dossier trouvé.",
Lang::Pt => "Nenhuma pasta encontrada.",
Lang::Ar => "لم يتم العثور على مجلدات.",
Lang::En => "No folders found.",
}
}
pub fn folder_select_hint(lang: Lang, prefix: &str) -> String {
match lang {
Lang::ZhCn => format!("回复 {prefix}folder <数字> 选择目录。"),
Lang::ZhTw => format!("回覆 {prefix}folder <數字> 選擇目錄。"),
Lang::Ja => format!("{prefix}folder <番号> で選択してください。"),
Lang::Ko => format!("{prefix}folder <번호>로 선택하세요."),
Lang::Es => format!("Responde {prefix}folder <número> para seleccionar."),
Lang::De => format!("Antworte {prefix}folder <Nummer> zur Auswahl."),
Lang::Fr => format!("Répondez {prefix}folder <numéro> pour sélectionner."),
Lang::Pt => format!("Responda {prefix}folder <número> para selecionar."),
Lang::Ar => format!("أجب بـ {prefix}folder <رقم> للاختيار."),
Lang::En => format!("Reply {prefix}folder <number> to select."),
}
}
pub fn index_starts_from_one(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "序号从 1 开始。",
Lang::ZhTw => "序號從 1 開始。",
Lang::Ja => "インデックスは 1 から始まります。",
Lang::Ko => "인덱스는 1부터 시작합니다.",
Lang::Es => "El índice empieza desde 1.",
Lang::De => "Index beginnt bei 1.",
Lang::Fr => "L'index commence à 1.",
Lang::Pt => "O índice começa em 1.",
Lang::Ar => "يبدأ الفهرس من 1.",
Lang::En => "Index starts from 1.",
}
}
pub fn folder_index_out_of_range(lang: Lang, prefix: &str) -> String {
match lang {
Lang::ZhCn => format!("序号超出范围,请使用 {prefix}folder 查看列表。"),
Lang::ZhTw => format!("序號超出範圍,請使用 {prefix}folder 查看列表。"),
Lang::Ja => format!("インデックスが範囲外です。{prefix}folder でリストを確認してください。"),
Lang::Ko => format!("인덱스가 범위를 벗어났습니다. {prefix}folder로 목록을 확인하세요."),
Lang::Es => format!("Índice fuera de rango. Usa {prefix}folder para ver la lista."),
Lang::De => format!("Index außerhalb des Bereichs. {prefix}folder verwenden, um aufzulisten."),
Lang::Fr => format!("Index hors limites. Utilisez {prefix}folder pour lister."),
Lang::Pt => format!("Índice fora de intervalo. Use {prefix}folder para listar."),
Lang::Ar => format!("الفهرس خارج النطاق. استخدم {prefix}folder لعرض القائمة."),
Lang::En => format!("Index out of range. Use {prefix}folder to list."),
}
}
pub fn folder_selected_title(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "已选择目录",
Lang::ZhTw => "已選擇目錄",
Lang::Ja => "フォルダを選択しました",
Lang::Ko => "폴더 선택됨",
Lang::Es => "Carpeta seleccionada",
Lang::De => "Ordner ausgewählt",
Lang::Fr => "Dossier sélectionné",
Lang::Pt => "Pasta selecionada",
Lang::Ar => "تم اختيار المجلد",
Lang::En => "Folder Selected",
}
}
pub fn folder_not_found(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "目录不存在。",
Lang::ZhTw => "目錄不存在。",
Lang::Ja => "フォルダが見つかりません。",
Lang::Ko => "폴더를 찾을 수 없습니다.",
Lang::Es => "Carpeta no encontrada.",
Lang::De => "Ordner nicht gefunden.",
Lang::Fr => "Dossier introuvable.",
Lang::Pt => "Pasta não encontrada.",
Lang::Ar => "المجلد غير موجود.",
Lang::En => "Folder not found.",
}
}
pub fn folder_not_found_with_hint(lang: Lang, prefix: &str) -> String {
match lang {
Lang::ZhCn => format!("目录不存在,请使用 {prefix}folder 重新选择。"),
Lang::ZhTw => format!("目錄不存在,請使用 {prefix}folder 重新選擇。"),
Lang::Ja => format!("フォルダが見つかりません。{prefix}folder で選択してください。"),
Lang::Ko => format!("폴더를 찾을 수 없습니다. {prefix}folder로 선택하세요."),
Lang::Es => format!("Carpeta no encontrada. Usa {prefix}folder para seleccionar."),
Lang::De => format!("Ordner nicht gefunden. {prefix}folder verwenden, um auszuwählen."),
Lang::Fr => format!("Dossier introuvable. Utilisez {prefix}folder pour sélectionner."),
Lang::Pt => format!("Pasta não encontrada. Use {prefix}folder para selecionar."),
Lang::Ar => format!("المجلد غير موجود. استخدم {prefix}folder للاختيار."),
Lang::En => format!("Folder not found. Use {prefix}folder to select."),
}
}
pub fn no_folder_selected(lang: Lang, prefix: &str) -> String {
match lang {
Lang::ZhCn => format!("未选择工作目录,请先使用 {prefix}folder 选择。"),
Lang::ZhTw => format!("未選擇工作目錄,請先使用 {prefix}folder 選擇。"),
Lang::Ja => format!("フォルダが選択されていません。先に {prefix}folder を使用してください。"),
Lang::Ko => format!("폴더가 선택되지 않았습니다. 먼저 {prefix}folder를 사용하세요."),
Lang::Es => format!("Ninguna carpeta seleccionada. Usa {prefix}folder primero."),
Lang::De => format!("Kein Ordner ausgewählt. Zuerst {prefix}folder verwenden."),
Lang::Fr => format!("Aucun dossier sélectionné. Utilisez d'abord {prefix}folder."),
Lang::Pt => format!("Nenhuma pasta selecionada. Use {prefix}folder primeiro."),
Lang::Ar => format!("لم يتم اختيار مجلد. استخدم {prefix}folder أولاً."),
Lang::En => format!("No folder selected. Use {prefix}folder first."),
}
}
// Agent (/agent)
pub fn agent_title(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "选择 Agent",
Lang::ZhTw => "選擇 Agent",
Lang::Ja => "エージェント選択",
Lang::Ko => "에이전트 선택",
Lang::Es => "Selección de agente",
Lang::De => "Agent-Auswahl",
Lang::Fr => "Sélection d'agent",
Lang::Pt => "Seleção de agente",
Lang::Ar => "اختيار الوكيل",
Lang::En => "Agent Selection",
}
}
pub fn agent_select_hint(lang: Lang, prefix: &str) -> String {
match lang {
Lang::ZhCn => format!("回复 {prefix}agent <数字> 或 {prefix}agent <名称> 选择。"),
Lang::ZhTw => format!("回覆 {prefix}agent <數字> 或 {prefix}agent <名稱> 選擇。"),
Lang::Ja => format!("{prefix}agent <番号> または {prefix}agent <名前> で選択してください。"),
Lang::Ko => format!("{prefix}agent <번호> 또는 {prefix}agent <이름>으로 선택하세요."),
Lang::Es => format!("Responde {prefix}agent <número> o {prefix}agent <nombre> para seleccionar."),
Lang::De => format!("Antworte {prefix}agent <Nummer> oder {prefix}agent <Name> zur Auswahl."),
Lang::Fr => format!("Répondez {prefix}agent <numéro> ou {prefix}agent <nom> pour sélectionner."),
Lang::Pt => format!("Responda {prefix}agent <número> ou {prefix}agent <nome> para selecionar."),
Lang::Ar => format!("أجب بـ {prefix}agent <رقم> أو {prefix}agent <اسم> للاختيار."),
Lang::En => format!("Reply {prefix}agent <number> or {prefix}agent <name> to select."),
}
}
pub fn agent_index_out_of_range(lang: Lang, prefix: &str) -> String {
match lang {
Lang::ZhCn => format!("序号超出范围,请使用 {prefix}agent 查看列表。"),
Lang::ZhTw => format!("序號超出範圍,請使用 {prefix}agent 查看列表。"),
Lang::Ja => format!("インデックスが範囲外です。{prefix}agent でリストを確認してください。"),
Lang::Ko => format!("인덱스가 범위를 벗어났습니다. {prefix}agent로 목록을 확인하세요."),
Lang::Es => format!("Índice fuera de rango. Usa {prefix}agent para ver la lista."),
Lang::De => format!("Index außerhalb des Bereichs. {prefix}agent verwenden, um aufzulisten."),
Lang::Fr => format!("Index hors limites. Utilisez {prefix}agent pour lister."),
Lang::Pt => format!("Índice fora de intervalo. Use {prefix}agent para listar."),
Lang::Ar => format!("الفهرس خارج النطاق. استخدم {prefix}agent لعرض القائمة."),
Lang::En => format!("Index out of range. Use {prefix}agent to list."),
}
}
pub fn agent_selected_title(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "已选择 Agent",
Lang::ZhTw => "已選擇 Agent",
Lang::Ja => "エージェントを選択しました",
Lang::Ko => "에이전트 선택됨",
Lang::Es => "Agente seleccionado",
Lang::De => "Agent ausgewählt",
Lang::Fr => "Agent sélectionné",
Lang::Pt => "Agente selecionado",
Lang::Ar => "تم اختيار الوكيل",
Lang::En => "Agent Selected",
}
}
pub fn unknown_agent_label(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "未知 Agent: ",
Lang::ZhTw => "未知 Agent: ",
Lang::Ja => "不明なエージェント: ",
Lang::Ko => "알 수 없는 에이전트: ",
Lang::Es => "Agente desconocido: ",
Lang::De => "Unbekannter Agent: ",
Lang::Fr => "Agent inconnu : ",
Lang::Pt => "Agente desconhecido: ",
Lang::Ar => "وكيل غير معروف: ",
Lang::En => "Unknown agent: ",
}
}
// Task (/task)
pub fn task_usage(lang: Lang, prefix: &str) -> String {
match lang {
Lang::ZhCn => format!("用法: {prefix}task <任务描述>"),
Lang::ZhTw => format!("用法: {prefix}task <任務描述>"),
Lang::Ja => format!("使い方: {prefix}task <タスク説明>"),
Lang::Ko => format!("사용법: {prefix}task <작업 설명>"),
Lang::Es => format!("Uso: {prefix}task <descripción>"),
Lang::De => format!("Verwendung: {prefix}task <Beschreibung>"),
Lang::Fr => format!("Usage : {prefix}task <description>"),
Lang::Pt => format!("Uso: {prefix}task <descrição>"),
Lang::Ar => format!("الاستخدام: {prefix}task <الوصف>"),
Lang::En => format!("Usage: {prefix}task <description>"),
}
}
pub fn no_agent_selected(lang: Lang, prefix: &str) -> String {
match lang {
Lang::ZhCn => format!("未选择 Agent请先使用 {prefix}agent 选择,或在工作目录上设置默认 Agent。"),
Lang::ZhTw => format!("未選擇 Agent請先使用 {prefix}agent 選擇,或在工作目錄上設定預設 Agent。"),
Lang::Ja => format!("エージェントが選択されていません。{prefix}agent で選択するか、フォルダにデフォルトエージェントを設定してください。"),
Lang::Ko => format!("에이전트가 선택되지 않았습니다. {prefix}agent로 선택하거나 폴더에 기본 에이전트를 설정하세요."),
Lang::Es => format!("Ningún agente seleccionado. Usa {prefix}agent para elegir uno o define uno por defecto en la carpeta."),
Lang::De => format!("Kein Agent ausgewählt. {prefix}agent verwenden oder Standard im Ordner festlegen."),
Lang::Fr => format!("Aucun agent sélectionné. Utilisez {prefix}agent ou définissez un agent par défaut sur le dossier."),
Lang::Pt => format!("Nenhum agente selecionado. Use {prefix}agent para escolher ou defina um padrão na pasta."),
Lang::Ar => format!("لم يتم اختيار وكيل. استخدم {prefix}agent لاختيار واحد أو حدد وكيلًا افتراضيًا للمجلد."),
Lang::En => format!("No agent selected. Use {prefix}agent to pick one or set a default on the folder."),
}
}
pub fn failed_to_start_agent_label(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "启动 Agent 失败: ",
Lang::ZhTw => "啟動 Agent 失敗: ",
Lang::Ja => "エージェントの起動に失敗しました: ",
Lang::Ko => "에이전트 시작 실패: ",
Lang::Es => "Error al iniciar el agente: ",
Lang::De => "Agent konnte nicht gestartet werden: ",
Lang::Fr => "Échec du démarrage de l'agent : ",
Lang::Pt => "Falha ao iniciar o agente: ",
Lang::Ar => "فشل بدء الوكيل: ",
Lang::En => "Failed to start agent: ",
}
}
pub fn task_started_title(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "任务已启动",
Lang::ZhTw => "任務已啟動",
Lang::Ja => "タスク開始",
Lang::Ko => "작업 시작됨",
Lang::Es => "Tarea iniciada",
Lang::De => "Aufgabe gestartet",
Lang::Fr => "Tâche démarrée",
Lang::Pt => "Tarefa iniciada",
Lang::Ar => "تم بدء المهمة",
Lang::En => "Task Started",
}
}
// Sessions (/sessions)
pub fn sessions_title(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "会话列表",
Lang::ZhTw => "對話列表",
Lang::Ja => "セッション一覧",
Lang::Ko => "세션 목록",
Lang::Es => "Sesiones",
Lang::De => "Sitzungen",
Lang::Fr => "Sessions",
Lang::Pt => "Sessões",
Lang::Ar => "الجلسات",
Lang::En => "Sessions",
}
}
pub fn no_active_sessions_in_folder(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "当前目录没有进行中的会话。",
Lang::ZhTw => "當前目錄沒有進行中的對話。",
Lang::Ja => "このフォルダにアクティブなセッションはありません。",
Lang::Ko => "이 폴더에 활성 세션이 없습니다.",
Lang::Es => "No hay sesiones activas en esta carpeta.",
Lang::De => "Keine aktiven Sitzungen in diesem Ordner.",
Lang::Fr => "Aucune session active dans ce dossier.",
Lang::Pt => "Nenhuma sessão ativa nesta pasta.",
Lang::Ar => "لا توجد جلسات نشطة في هذا المجلد.",
Lang::En => "No active sessions in this folder.",
}
}
pub fn sessions_resume_hint(lang: Lang, prefix: &str) -> String {
match lang {
Lang::ZhCn => format!("回复 {prefix}resume <会话ID> 继续会话。"),
Lang::ZhTw => format!("回覆 {prefix}resume <對話ID> 繼續對話。"),
Lang::Ja => format!("{prefix}resume <ID> で続行してください。"),
Lang::Ko => format!("{prefix}resume <ID>로 계속하세요."),
Lang::Es => format!("Responde {prefix}resume <id> para continuar."),
Lang::De => format!("Antworte {prefix}resume <ID> zum Fortfahren."),
Lang::Fr => format!("Répondez {prefix}resume <id> pour continuer."),
Lang::Pt => format!("Responda {prefix}resume <id> para continuar."),
Lang::Ar => format!("أجب بـ {prefix}resume <المعرف> للاستمرار."),
Lang::En => format!("Reply {prefix}resume <id> to continue."),
}
}
// Resume (/resume)
pub fn conversation_not_found(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "会话不存在。",
Lang::ZhTw => "對話不存在。",
Lang::Ja => "会話が見つかりません。",
Lang::Ko => "대화를 찾을 수 없습니다.",
Lang::Es => "Conversación no encontrada.",
Lang::De => "Konversation nicht gefunden.",
Lang::Fr => "Conversation introuvable.",
Lang::Pt => "Conversa não encontrada.",
Lang::Ar => "المحادثة غير موجودة.",
Lang::En => "Conversation not found.",
}
}
pub fn session_resumed_title(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "会话已恢复",
Lang::ZhTw => "對話已恢復",
Lang::Ja => "セッション再開",
Lang::Ko => "세션 재개됨",
Lang::Es => "Sesión reanudada",
Lang::De => "Sitzung fortgesetzt",
Lang::Fr => "Session reprise",
Lang::Pt => "Sessão retomada",
Lang::Ar => "تم استئناف الجلسة",
Lang::En => "Session Resumed",
}
}
pub fn no_conversations_found(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "暂无会话记录。",
Lang::ZhTw => "暫無對話記錄。",
Lang::Ja => "会話記録がありません。",
Lang::Ko => "대화 기록이 없습니다.",
Lang::Es => "No hay conversaciones.",
Lang::De => "Keine Konversationen vorhanden.",
Lang::Fr => "Aucune conversation trouvée.",
Lang::Pt => "Nenhuma conversa encontrada.",
Lang::Ar => "لا توجد محادثات.",
Lang::En => "No conversations found.",
}
}
pub fn recent_conversations_title(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "最近会话",
Lang::ZhTw => "最近對話",
Lang::Ja => "最近の会話",
Lang::Ko => "최근 대화",
Lang::Es => "Conversaciones recientes",
Lang::De => "Letzte Konversationen",
Lang::Fr => "Conversations récentes",
Lang::Pt => "Conversas recentes",
Lang::Ar => "المحادثات الأخيرة",
Lang::En => "Recent Conversations",
}
}
pub fn recent_resume_hint(lang: Lang, prefix: &str) -> String {
match lang {
Lang::ZhCn => format!("回复 {prefix}resume <会话ID> 恢复会话。"),
Lang::ZhTw => format!("回覆 {prefix}resume <對話ID> 恢復對話。"),
Lang::Ja => format!("{prefix}resume <ID> でセッションを再開してください。"),
Lang::Ko => format!("{prefix}resume <ID>로 세션을 재개하세요."),
Lang::Es => format!("Responde {prefix}resume <id> para reanudar una sesión."),
Lang::De => format!("Antworte {prefix}resume <ID> zum Fortsetzen einer Sitzung."),
Lang::Fr => format!("Répondez {prefix}resume <id> pour reprendre une session."),
Lang::Pt => format!("Responda {prefix}resume <id> para retomar uma sessão."),
Lang::Ar => format!("أجب بـ {prefix}resume <المعرف> لاستئناف الجلسة."),
Lang::En => format!("Reply {prefix}resume <id> to resume a session."),
}
}
// Cancel (/cancel)
pub fn no_active_session_to_cancel(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "没有进行中的任务可取消。",
Lang::ZhTw => "沒有進行中的任務可取消。",
Lang::Ja => "キャンセルできるアクティブなセッションはありません。",
Lang::Ko => "취소할 활성 세션이 없습니다.",
Lang::Es => "No hay sesión activa para cancelar.",
Lang::De => "Keine aktive Sitzung zum Abbrechen.",
Lang::Fr => "Aucune session active à annuler.",
Lang::Pt => "Nenhuma sessão ativa para cancelar.",
Lang::Ar => "لا توجد جلسة نشطة للإلغاء.",
Lang::En => "No active session to cancel.",
}
}
pub fn task_cancelled_body(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "当前任务已取消。",
Lang::ZhTw => "當前任務已取消。",
Lang::Ja => "現在のタスクをキャンセルしました。",
Lang::Ko => "현재 작업이 취소되었습니다.",
Lang::Es => "La tarea actual ha sido cancelada.",
Lang::De => "Aktuelle Aufgabe wurde abgebrochen.",
Lang::Fr => "La tâche en cours a été annulée.",
Lang::Pt => "A tarefa atual foi cancelada.",
Lang::Ar => "تم إلغاء المهمة الحالية.",
Lang::En => "Current task has been cancelled.",
}
}
pub fn task_cancelled_title(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "任务已取消",
Lang::ZhTw => "任務已取消",
Lang::Ja => "タスクをキャンセルしました",
Lang::Ko => "작업 취소됨",
Lang::Es => "Tarea cancelada",
Lang::De => "Aufgabe abgebrochen",
Lang::Fr => "Tâche annulée",
Lang::Pt => "Tarefa cancelada",
Lang::Ar => "تم إلغاء المهمة",
Lang::En => "Task Cancelled",
}
}
// Permission (/approve, /deny)
pub fn no_active_session(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "没有活跃的会话。",
Lang::ZhTw => "沒有活躍的對話。",
Lang::Ja => "アクティブなセッションがありません。",
Lang::Ko => "활성 세션이 없습니다.",
Lang::Es => "No hay sesión activa.",
Lang::De => "Keine aktive Sitzung.",
Lang::Fr => "Aucune session active.",
Lang::Pt => "Nenhuma sessão ativa.",
Lang::Ar => "لا توجد جلسة نشطة.",
Lang::En => "No active session.",
}
}
pub fn no_active_session_found(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "未找到活跃的会话。",
Lang::ZhTw => "未找到活躍的對話。",
Lang::Ja => "アクティブなセッションが見つかりません。",
Lang::Ko => "활성 세션을 찾을 수 없습니다.",
Lang::Es => "No se encontró sesión activa.",
Lang::De => "Keine aktive Sitzung gefunden.",
Lang::Fr => "Aucune session active trouvée.",
Lang::Pt => "Nenhuma sessão ativa encontrada.",
Lang::Ar => "لم يتم العثور على جلسة نشطة.",
Lang::En => "No active session found.",
}
}
pub fn no_pending_permission(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "没有待处理的权限请求。",
Lang::ZhTw => "沒有待處理的權限請求。",
Lang::Ja => "保留中の権限要求はありません。",
Lang::Ko => "대기 중인 권한 요청이 없습니다.",
Lang::Es => "No hay solicitudes de permiso pendientes.",
Lang::De => "Keine ausstehende Berechtigungsanfrage.",
Lang::Fr => "Aucune demande d'autorisation en attente.",
Lang::Pt => "Nenhuma solicitação de permissão pendente.",
Lang::Ar => "لا توجد طلبات أذونات معلقة.",
Lang::En => "No pending permission request.",
}
}
pub fn no_valid_permission_option(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "未找到有效的权限选项。",
Lang::ZhTw => "未找到有效的權限選項。",
Lang::Ja => "有効な権限オプションが見つかりません。",
Lang::Ko => "유효한 권한 옵션을 찾을 수 없습니다.",
Lang::Es => "No se encontró una opción de permiso válida.",
Lang::De => "Keine gültige Berechtigungsoption gefunden.",
Lang::Fr => "Aucune option d'autorisation valide trouvée.",
Lang::Pt => "Nenhuma opção de permissão válida encontrada.",
Lang::Ar => "لم يتم العثور على خيار أذونات صالح.",
Lang::En => "No valid permission option found.",
}
}
pub fn failed_permission_response_label(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "权限响应失败: ",
Lang::ZhTw => "權限回應失敗: ",
Lang::Ja => "権限応答に失敗しました: ",
Lang::Ko => "권한 응답 실패: ",
Lang::Es => "Error al responder al permiso: ",
Lang::De => "Berechtigungsantwort fehlgeschlagen: ",
Lang::Fr => "Échec de la réponse à l'autorisation : ",
Lang::Pt => "Falha ao responder à permissão: ",
Lang::Ar => "فشل الاستجابة للإذن: ",
Lang::En => "Failed to respond to permission: ",
}
}
pub fn approved_label(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "已批准",
Lang::ZhTw => "已批准",
Lang::Ja => "承認済み",
Lang::Ko => "승인됨",
Lang::Es => "Aprobado",
Lang::De => "Genehmigt",
Lang::Fr => "Approuvé",
Lang::Pt => "Aprovado",
Lang::Ar => "تمت الموافقة",
Lang::En => "Approved",
}
}
pub fn denied_label(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "已拒绝",
Lang::ZhTw => "已拒絕",
Lang::Ja => "拒否",
Lang::Ko => "거부됨",
Lang::Es => "Denegado",
Lang::De => "Abgelehnt",
Lang::Fr => "Refusé",
Lang::Pt => "Negado",
Lang::Ar => "تم الرفض",
Lang::En => "Denied",
}
}
pub fn auto_approve_enabled(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "已启用自动批准。",
Lang::ZhTw => "已啟用自動批准。",
Lang::Ja => "このセッションで自動承認を有効にしました。",
Lang::Ko => "이 세션에 자동 승인을 활성화했습니다.",
Lang::Es => "Aprobación automática activada para esta sesión.",
Lang::De => "Automatische Genehmigung für diese Sitzung aktiviert.",
Lang::Fr => "Approbation automatique activée pour cette session.",
Lang::Pt => "Aprovação automática ativada para esta sessão.",
Lang::Ar => "تم تفعيل الموافقة التلقائية لهذه الجلسة.",
Lang::En => "Auto-approve enabled for this session.",
}
}
pub fn permission_response_title(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "权限响应",
Lang::ZhTw => "權限回應",
Lang::Ja => "権限応答",
Lang::Ko => "권한 응답",
Lang::Es => "Respuesta de permiso",
Lang::De => "Berechtigungsantwort",
Lang::Fr => "Réponse d'autorisation",
Lang::Pt => "Resposta de permissão",
Lang::Ar => "استجابة الإذن",
Lang::En => "Permission Response",
}
}
// Follow-up
pub fn no_active_session_use_task(lang: Lang, prefix: &str) -> String {
match lang {
Lang::ZhCn => format!("没有活跃的会话,请使用 {prefix}task 开始新任务。"),
Lang::ZhTw => format!("沒有活躍的對話,請使用 {prefix}task 開始新任務。"),
Lang::Ja => format!("アクティブなセッションがありません。{prefix}task で開始してください。"),
Lang::Ko => format!("활성 세션이 없습니다. {prefix}task로 시작하세요."),
Lang::Es => format!("No hay sesión activa. Usa {prefix}task para iniciar una."),
Lang::De => format!("Keine aktive Sitzung. {prefix}task zum Starten verwenden."),
Lang::Fr => format!("Aucune session active. Utilisez {prefix}task pour en démarrer une."),
Lang::Pt => format!("Nenhuma sessão ativa. Use {prefix}task para iniciar uma."),
Lang::Ar => format!("لا توجد جلسة نشطة. استخدم {prefix}task لبدء واحدة."),
Lang::En => format!("No active session. Use {prefix}task to start one."),
}
}
pub fn session_connection_lost(lang: Lang, prefix: &str) -> String {
match lang {
Lang::ZhCn => format!("会话连接已断开,请使用 {prefix}task 开始新任务。"),
Lang::ZhTw => format!("對話連線已斷開,請使用 {prefix}task 開始新任務。"),
Lang::Ja => format!("セッション接続が切断されました。{prefix}task で新しく開始してください。"),
Lang::Ko => format!("세션 연결이 끊어졌습니다. {prefix}task로 새로 시작하세요."),
Lang::Es => format!("Conexión de sesión perdida. Usa {prefix}task para iniciar una nueva."),
Lang::De => format!("Sitzungsverbindung verloren. {prefix}task für neue Sitzung verwenden."),
Lang::Fr => format!("Connexion de session perdue. Utilisez {prefix}task pour en démarrer une nouvelle."),
Lang::Pt => format!("Conexão da sessão perdida. Use {prefix}task para iniciar uma nova."),
Lang::Ar => format!("انقطع اتصال الجلسة. استخدم {prefix}task لبدء جلسة جديدة."),
Lang::En => format!("Session connection lost. Use {prefix}task to start a new one."),
}
}
pub fn failed_to_send_message_label(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "发送消息失败: ",
Lang::ZhTw => "發送訊息失敗: ",
Lang::Ja => "メッセージの送信に失敗しました: ",
Lang::Ko => "메시지 전송 실패: ",
Lang::Es => "Error al enviar el mensaje: ",
Lang::De => "Nachricht konnte nicht gesendet werden: ",
Lang::Fr => "Échec de l'envoi du message : ",
Lang::Pt => "Falha ao enviar mensagem: ",
Lang::Ar => "فشل إرسال الرسالة: ",
Lang::En => "Failed to send message: ",
}
}
pub fn message_sent(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "消息已发送。",
Lang::ZhTw => "訊息已發送。",
Lang::Ja => "メッセージを送信しました。",
Lang::Ko => "메시지를 보냈습니다.",
Lang::Es => "Mensaje enviado.",
Lang::De => "Nachricht gesendet.",
Lang::Fr => "Message envoyé.",
Lang::Pt => "Mensagem enviada.",
Lang::Ar => "تم إرسال الرسالة.",
Lang::En => "Message sent.",
}
}
// Internal error labels
pub fn failed_to_list_folders_label(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "列出目录失败: ",
Lang::ZhTw => "列出目錄失敗: ",
Lang::Ja => "フォルダ一覧の取得に失敗しました: ",
Lang::Ko => "폴더 목록 조회 실패: ",
Lang::Es => "Error al listar carpetas: ",
Lang::De => "Auflisten der Ordner fehlgeschlagen: ",
Lang::Fr => "Échec de la liste des dossiers : ",
Lang::Pt => "Falha ao listar pastas: ",
Lang::Ar => "فشل عرض المجلدات: ",
Lang::En => "Failed to list folders: ",
}
}
pub fn failed_to_add_folder_label(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "添加目录失败: ",
Lang::ZhTw => "新增目錄失敗: ",
Lang::Ja => "フォルダの追加に失敗しました: ",
Lang::Ko => "폴더 추가 실패: ",
Lang::Es => "Error al agregar carpeta: ",
Lang::De => "Ordner konnte nicht hinzugefügt werden: ",
Lang::Fr => "Échec de l'ajout du dossier : ",
Lang::Pt => "Falha ao adicionar pasta: ",
Lang::Ar => "فشل إضافة المجلد: ",
Lang::En => "Failed to add folder: ",
}
}
pub fn failed_to_load_context_label(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "加载上下文失败: ",
Lang::ZhTw => "載入上下文失敗: ",
Lang::Ja => "コンテキストの読み込みに失敗しました: ",
Lang::Ko => "컨텍스트 로드 실패: ",
Lang::Es => "Error al cargar contexto: ",
Lang::De => "Kontext konnte nicht geladen werden: ",
Lang::Fr => "Échec du chargement du contexte : ",
Lang::Pt => "Falha ao carregar contexto: ",
Lang::Ar => "فشل تحميل السياق: ",
Lang::En => "Failed to load context: ",
}
}
pub fn failed_to_create_conversation_label(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "创建会话失败: ",
Lang::ZhTw => "建立對話失敗: ",
Lang::Ja => "会話の作成に失敗しました: ",
Lang::Ko => "대화 생성 실패: ",
Lang::Es => "Error al crear conversación: ",
Lang::De => "Konversation konnte nicht erstellt werden: ",
Lang::Fr => "Échec de la création de la conversation : ",
Lang::Pt => "Falha ao criar conversa: ",
Lang::Ar => "فشل إنشاء المحادثة: ",
Lang::En => "Failed to create conversation: ",
}
}
pub fn failed_to_list_sessions_label(lang: Lang) -> &'static str {
match lang {
Lang::ZhCn => "列出会话失败: ",
Lang::ZhTw => "列出對話失敗: ",
Lang::Ja => "セッション一覧の取得に失敗しました: ",
Lang::Ko => "세션 목록 조회 실패: ",
Lang::Es => "Error al listar sesiones: ",
Lang::De => "Auflisten der Sitzungen fehlgeschlagen: ",
Lang::Fr => "Échec de la liste des sessions : ",
Lang::Pt => "Falha ao listar sessões: ",
Lang::Ar => "فشل عرض الجلسات: ",
Lang::En => "Failed to list sessions: ",
}
}
// ── Session progress messages ──
pub fn agent_responding(lang: Lang, agent_label: &str) -> String {
match lang {
Lang::ZhCn => format!("{agent_label} 正在响应中..."),
Lang::ZhTw => format!("{agent_label} 正在回應中..."),
Lang::Ja => format!("{agent_label} が応答中..."),
Lang::Ko => format!("{agent_label} 응답 중..."),
Lang::Es => format!("{agent_label} respondiendo..."),
Lang::De => format!("{agent_label} antwortet..."),
Lang::Fr => format!("{agent_label} en cours de réponse..."),
Lang::Pt => format!("{agent_label} respondendo..."),
// FSI/PDI (U+2068/U+2069) isolate Latin agent name inside the Arabic RTL run so
// bidi reordering stays predictable across Telegram/Lark/WeiXin clients.
Lang::Ar => format!("\u{2068}{agent_label}\u{2069} يستجيب..."),
Lang::En => format!("{agent_label} is responding..."),
} }
} }

View File

@@ -3,6 +3,7 @@ use std::time::Instant;
use crate::acp::types::PermissionOptionInfo; use crate::acp::types::PermissionOptionInfo;
use crate::chat_channel::types::SentMessageId; use crate::chat_channel::types::SentMessageId;
use crate::models::agent::AgentType;
pub struct PendingPermission { pub struct PendingPermission {
pub request_id: String, pub request_id: String,
@@ -16,6 +17,7 @@ pub struct ActiveSession {
pub sender_id: String, pub sender_id: String,
pub conversation_id: i32, pub conversation_id: i32,
pub connection_id: String, pub connection_id: String,
pub agent_type: AgentType,
pub content_buffer: String, pub content_buffer: String,
pub tool_calls: Vec<String>, pub tool_calls: Vec<String>,
/// Stores raw_input by tool_call_id for detail extraction on completion. /// Stores raw_input by tool_call_id for detail extraction on completion.

View File

@@ -48,12 +48,14 @@ async fn list_folders(
) -> 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,
Err(e) => return RichMessage::error(format!("Failed to list folders: {e}")), Err(e) => {
return RichMessage::error(format!("{}{e}", i18n::failed_to_list_folders_label(lang)));
}
}; };
if folders.is_empty() { if folders.is_empty() {
return RichMessage::info(t(lang, "No folders found.", "没有找到项目目录。")) return RichMessage::info(i18n::no_folders_found(lang))
.with_title(t(lang, "Working Folder", "工作目录")); .with_title(i18n::folder_title(lang));
} }
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)
@@ -77,18 +79,9 @@ async fn list_folders(
)); ));
} }
body.push_str(&format!( body.push_str(&format!("\n{}", i18n::folder_select_hint(lang, prefix)));
"\n{}",
tp(
lang,
prefix,
"Reply {prefix}folder <number> to select.",
"回复 {prefix}folder <数字> 选择目录。"
)
));
RichMessage::info(body.trim_end()) RichMessage::info(body.trim_end()).with_title(i18n::folder_title(lang))
.with_title(t(lang, "Working Folder", "工作目录"))
} }
async fn select_folder_by_index( async fn select_folder_by_index(
@@ -100,28 +93,25 @@ async fn select_folder_by_index(
prefix: &str, prefix: &str,
) -> RichMessage { ) -> RichMessage {
if idx == 0 { if idx == 0 {
return RichMessage::info(t(lang, "Index starts from 1.", "序号从 1 开始。")); return RichMessage::info(i18n::index_starts_from_one(lang));
} }
let folders = match folder_service::list_folders(db).await { let folders = match folder_service::list_folders(db).await {
Ok(f) => f, Ok(f) => f,
Err(e) => return RichMessage::error(format!("Failed to list folders: {e}")), Err(e) => {
return RichMessage::error(format!("{}{e}", i18n::failed_to_list_folders_label(lang)));
}
}; };
let Some(folder) = folders.get(idx - 1) else { let Some(folder) = folders.get(idx - 1) else {
return RichMessage::info(tp( return RichMessage::info(i18n::folder_index_out_of_range(lang, prefix));
lang,
prefix,
"Index out of range. Use {prefix}folder to list.",
"序号超出范围,请使用 {prefix}folder 查看列表。",
));
}; };
let _ = sender_context_service::update_folder(db, channel_id, sender_id, Some(folder.id)) let _ = sender_context_service::update_folder(db, channel_id, sender_id, Some(folder.id))
.await; .await;
RichMessage::info(format!("{} ({})", folder.name, folder.path)) RichMessage::info(format!("{} ({})", folder.name, folder.path))
.with_title(t(lang, "Folder Selected", "已选择目录")) .with_title(i18n::folder_selected_title(lang))
} }
async fn select_folder_by_path( async fn select_folder_by_path(
@@ -133,14 +123,16 @@ async fn select_folder_by_path(
) -> RichMessage { ) -> RichMessage {
let entry = match folder_service::add_folder(db, path).await { let entry = match folder_service::add_folder(db, path).await {
Ok(e) => e, Ok(e) => e,
Err(e) => return RichMessage::error(format!("Failed to add folder: {e}")), Err(e) => {
return RichMessage::error(format!("{}{e}", i18n::failed_to_add_folder_label(lang)));
}
}; };
let _ = let _ =
sender_context_service::update_folder(db, channel_id, sender_id, Some(entry.id)).await; sender_context_service::update_folder(db, channel_id, sender_id, Some(entry.id)).await;
RichMessage::info(format!("{} ({})", entry.name, entry.path)) RichMessage::info(format!("{} ({})", entry.name, entry.path))
.with_title(t(lang, "Folder Selected", "已选择目录")) .with_title(i18n::folder_selected_title(lang))
} }
// ── /agent ── // ── /agent ──
@@ -190,18 +182,9 @@ async fn list_agents(
body.push_str(&format!("{}. {}{}\n", i + 1, at, marker)); body.push_str(&format!("{}. {}{}\n", i + 1, at, marker));
} }
body.push_str(&format!( body.push_str(&format!("\n{}", i18n::agent_select_hint(lang, prefix)));
"\n{}",
tp(
lang,
prefix,
"Reply {prefix}agent <number> or {prefix}agent <name> to select.",
"回复 {prefix}agent <数字> 或 {prefix}agent <名称> 选择。"
)
));
RichMessage::info(body.trim_end()) RichMessage::info(body.trim_end()).with_title(i18n::agent_title(lang))
.with_title(t(lang, "Agent Selection", "选择 Agent"))
} }
async fn select_agent_by_index( async fn select_agent_by_index(
@@ -214,20 +197,14 @@ async fn select_agent_by_index(
) -> 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(tp( return RichMessage::info(i18n::agent_index_out_of_range(lang, prefix));
lang,
prefix,
"Index out of range. Use {prefix}agent to list.",
"序号超出范围,请使用 {prefix}agent 查看列表。",
));
} }
let at = agents[idx - 1]; let at = agents[idx - 1];
let at_str = agent_type_to_string(at); let at_str = agent_type_to_string(at);
let _ = sender_context_service::update_agent(db, channel_id, sender_id, Some(at_str)).await; let _ = sender_context_service::update_agent(db, channel_id, sender_id, Some(at_str)).await;
RichMessage::info(at.to_string()) RichMessage::info(at.to_string()).with_title(i18n::agent_selected_title(lang))
.with_title(t(lang, "Agent Selected", "已选择 Agent"))
} }
async fn select_agent_by_name( async fn select_agent_by_name(
@@ -240,19 +217,14 @@ async fn select_agent_by_name(
let at = match parse_agent_type(name) { let at = match parse_agent_type(name) {
Some(a) => a, Some(a) => a,
None => { None => {
return RichMessage::info(format!( return RichMessage::info(format!("{}{}", i18n::unknown_agent_label(lang), name));
"{}{}",
t(lang, "Unknown agent: ", "未知 Agent: "),
name
));
} }
}; };
let at_str = agent_type_to_string(at); let at_str = agent_type_to_string(at);
let _ = sender_context_service::update_agent(db, channel_id, sender_id, Some(at_str)).await; let _ = sender_context_service::update_agent(db, channel_id, sender_id, Some(at_str)).await;
RichMessage::info(at.to_string()) RichMessage::info(at.to_string()).with_title(i18n::agent_selected_title(lang))
.with_title(t(lang, "Agent Selected", "已选择 Agent"))
} }
// ── /task ── // ── /task ──
@@ -270,29 +242,21 @@ pub async fn handle_task(
prefix: &str, prefix: &str,
) -> RichMessage { ) -> RichMessage {
if task_description.is_empty() { if task_description.is_empty() {
return RichMessage::info(tp( return RichMessage::info(i18n::task_usage(lang, prefix));
lang,
prefix,
"Usage: {prefix}task <description>",
"用法: {prefix}task <任务描述>",
));
} }
// 1. Load sender context // 1. Load sender context
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,
Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), Err(e) => {
return RichMessage::error(format!("{}{e}", i18n::failed_to_load_context_label(lang)));
}
}; };
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(tp( return RichMessage::info(i18n::no_folder_selected(lang, prefix));
lang,
prefix,
"No folder selected. Use {prefix}folder first.",
"未选择工作目录,请先使用 {prefix}folder 选择。",
));
} }
}; };
@@ -300,17 +264,17 @@ 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(tp( return RichMessage::info(i18n::folder_not_found_with_hint(lang, prefix));
lang,
prefix,
"Folder not found. Use {prefix}folder to select.",
"目录不存在,请使用 {prefix}folder 重新选择。",
));
} }
}; };
// 3. Resolve agent type // 3. Resolve agent type
let agent_type = resolve_agent_type(&ctx.current_agent_type, &folder.default_agent_type); let agent_type = match resolve_agent_type(&ctx.current_agent_type, &folder.default_agent_type) {
Some(at) => at,
None => {
return RichMessage::info(i18n::no_agent_selected(lang, prefix));
}
};
// 4. Create conversation record // 4. Create conversation record
let conv = match conversation_service::create( let conv = match conversation_service::create(
@@ -323,7 +287,12 @@ pub async fn handle_task(
.await .await
{ {
Ok(c) => c, Ok(c) => c,
Err(e) => return RichMessage::error(format!("Failed to create conversation: {e}")), Err(e) => {
return RichMessage::error(format!(
"{}{e}",
i18n::failed_to_create_conversation_label(lang)
));
}
}; };
// 5. Spawn ACP agent // 5. Spawn ACP agent
@@ -350,7 +319,7 @@ pub async fn handle_task(
.await; .await;
return RichMessage::error(format!( return RichMessage::error(format!(
"{}{e}", "{}{e}",
t(lang, "Failed to start agent: ", "启动 Agent 失败: ") i18n::failed_to_start_agent_label(lang)
)); ));
} }
}; };
@@ -362,6 +331,7 @@ pub async fn handle_task(
sender_id: sender_id.to_string(), sender_id: sender_id.to_string(),
conversation_id: conv.id, conversation_id: conv.id,
connection_id: connection_id.clone(), connection_id: connection_id.clone(),
agent_type,
content_buffer: String::new(), content_buffer: String::new(),
tool_calls: Vec::new(), tool_calls: Vec::new(),
tool_call_inputs: std::collections::HashMap::new(), tool_call_inputs: std::collections::HashMap::new(),
@@ -386,7 +356,7 @@ pub async fn handle_task(
"[{}] #{} @ {}", "[{}] #{} @ {}",
agent_type, conv.id, folder.name, agent_type, conv.id, folder.name,
)) ))
.with_title(t(lang, "Task Started", "任务已启动")) .with_title(i18n::task_started_title(lang))
} }
// ── /sessions ── // ── /sessions ──
@@ -400,29 +370,22 @@ pub async fn handle_sessions(
) -> 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,
Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), Err(e) => {
return RichMessage::error(format!("{}{e}", i18n::failed_to_load_context_label(lang)));
}
}; };
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(tp( return RichMessage::info(i18n::no_folder_selected(lang, prefix));
lang,
prefix,
"No folder selected. Use {prefix}folder first.",
"未选择工作目录,请先使用 {prefix}folder 选择。",
));
} }
}; };
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(i18n::folder_not_found(lang));
lang,
"Folder not found.",
"目录不存在。",
));
} }
}; };
@@ -437,20 +400,17 @@ pub async fn handle_sessions(
.await .await
{ {
Ok(c) => c, Ok(c) => c,
Err(e) => return RichMessage::error(format!("Failed to list sessions: {e}")), Err(e) => {
return RichMessage::error(format!(
"{}{e}",
i18n::failed_to_list_sessions_label(lang)
));
}
}; };
if convs.is_empty() { if convs.is_empty() {
return RichMessage::info(t( return RichMessage::info(i18n::no_active_sessions_in_folder(lang))
lang, .with_title(format!("{} - {}", i18n::sessions_title(lang), folder.name));
"No active sessions in this folder.",
"当前目录没有进行中的会话。",
))
.with_title(format!(
"{} - {}",
t(lang, "Sessions", "会话列表"),
folder.name
));
} }
let mut body = String::new(); let mut body = String::new();
@@ -471,21 +431,10 @@ pub async fn handle_sessions(
)); ));
} }
body.push_str(&format!( body.push_str(&format!("\n{}", i18n::sessions_resume_hint(lang, prefix)));
"\n{}",
tp(
lang,
prefix,
"Reply {prefix}resume <id> to continue.",
"回复 {prefix}resume <会话ID> 继续会话。"
)
));
RichMessage::info(body.trim_end()).with_title(format!( RichMessage::info(body.trim_end())
"{} - {}", .with_title(format!("{} - {}", i18n::sessions_title(lang), folder.name))
t(lang, "Sessions", "会话列表"),
folder.name
))
} }
// ── /resume ── // ── /resume ──
@@ -516,18 +465,14 @@ pub async fn handle_resume(
let conv = match conversation_service::get_by_id(db, conversation_id).await { let conv = match conversation_service::get_by_id(db, conversation_id).await {
Ok(c) => c, Ok(c) => c,
Err(_) => { Err(_) => {
return RichMessage::info(t( return RichMessage::info(i18n::conversation_not_found(lang));
lang,
"Conversation not found.",
"会话不存在。",
));
} }
}; };
let folder = match folder_service::get_folder_by_id(db, conv.folder_id).await { let folder = match folder_service::get_folder_by_id(db, conv.folder_id).await {
Ok(Some(f)) => f, Ok(Some(f)) => f,
_ => { _ => {
return RichMessage::info(t(lang, "Folder not found.", "目录不存在。")); return RichMessage::info(i18n::folder_not_found(lang));
} }
}; };
@@ -548,7 +493,7 @@ pub async fn handle_resume(
Err(e) => { Err(e) => {
return RichMessage::error(format!( return RichMessage::error(format!(
"{}{e}", "{}{e}",
t(lang, "Failed to start agent: ", "启动 Agent 失败: ") i18n::failed_to_start_agent_label(lang)
)); ));
} }
}; };
@@ -560,6 +505,7 @@ pub async fn handle_resume(
sender_id: sender_id.to_string(), sender_id: sender_id.to_string(),
conversation_id: conv.id, conversation_id: conv.id,
connection_id: connection_id.clone(), connection_id: connection_id.clone(),
agent_type: conv.agent_type,
content_buffer: String::new(), content_buffer: String::new(),
tool_calls: Vec::new(), tool_calls: Vec::new(),
tool_call_inputs: std::collections::HashMap::new(), tool_call_inputs: std::collections::HashMap::new(),
@@ -587,7 +533,7 @@ pub async fn handle_resume(
"[{}] #{} {} @ {}", "[{}] #{} {} @ {}",
conv.agent_type, conv.id, title, folder.name, conv.agent_type, conv.id, title, folder.name,
)) ))
.with_title(t(lang, "Session Resumed", "会话已恢复")) .with_title(i18n::session_resumed_title(lang))
} }
// ── /cancel ── // ── /cancel ──
@@ -602,17 +548,15 @@ pub async fn handle_cancel(
) -> 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,
Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), Err(e) => {
return RichMessage::error(format!("{}{e}", i18n::failed_to_load_context_label(lang)));
}
}; };
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(i18n::no_active_session_to_cancel(lang));
lang,
"No active session to cancel.",
"没有进行中的任务可取消。",
));
} }
}; };
@@ -635,12 +579,8 @@ pub async fn handle_cancel(
// Clear session from context // Clear session from context
let _ = sender_context_service::clear_session(db, channel_id, sender_id).await; let _ = sender_context_service::clear_session(db, channel_id, sender_id).await;
RichMessage::info(t( RichMessage::info(i18n::task_cancelled_body(lang))
lang, .with_title(i18n::task_cancelled_title(lang))
"Current task has been cancelled.",
"当前任务已取消。",
))
.with_title(t(lang, "Task Cancelled", "任务已取消"))
} }
// ── /approve, /deny ── // ── /approve, /deny ──
@@ -658,17 +598,15 @@ pub async fn handle_permission_response(
) -> 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,
Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), Err(e) => {
return RichMessage::error(format!("{}{e}", i18n::failed_to_load_context_label(lang)));
}
}; };
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(i18n::no_active_session(lang));
lang,
"No active session.",
"没有活跃的会话。",
));
} }
}; };
@@ -677,11 +615,7 @@ pub async fn handle_permission_response(
let session = match bridge_guard.get_mut(&connection_id) { let session = match bridge_guard.get_mut(&connection_id) {
Some(s) => s, Some(s) => s,
None => { None => {
return RichMessage::info(t( return RichMessage::info(i18n::no_active_session_found(lang));
lang,
"No active session found.",
"未找到活跃的会话。",
));
} }
}; };
session.permission_pending.take() session.permission_pending.take()
@@ -690,11 +624,7 @@ pub async fn handle_permission_response(
let pending = match pending { let pending = match pending {
Some(p) => p, Some(p) => p,
None => { None => {
return RichMessage::info(t( return RichMessage::info(i18n::no_pending_permission(lang));
lang,
"No pending permission request.",
"没有待处理的权限请求。",
));
} }
}; };
@@ -716,11 +646,7 @@ pub async fn handle_permission_response(
}; };
let Some(option_id) = option_id else { let Some(option_id) = option_id else {
return RichMessage::info(t( return RichMessage::info(i18n::no_valid_permission_option(lang));
lang,
"No valid permission option found.",
"未找到有效的权限选项。",
));
}; };
if let Err(e) = conn_mgr if let Err(e) = conn_mgr
@@ -729,11 +655,7 @@ pub async fn handle_permission_response(
{ {
return RichMessage::error(format!( return RichMessage::error(format!(
"{}{e}", "{}{e}",
t( i18n::failed_permission_response_label(lang)
lang,
"Failed to respond to permission: ",
"权限响应失败: "
)
)); ));
} }
@@ -744,23 +666,16 @@ pub async fn handle_permission_response(
} }
let action = if approve { let action = if approve {
t(lang, "Approved", "已批准") i18n::approved_label(lang)
} else { } else {
t(lang, "Denied", "已拒绝") i18n::denied_label(lang)
}; };
let mut msg = RichMessage::info(format!("{}: {}", action, pending.tool_description)); let mut msg = RichMessage::info(format!("{}: {}", action, pending.tool_description));
if always && approve { if always && approve {
msg = msg.with_field( msg = msg.with_field("", i18n::auto_approve_enabled(lang));
"",
t(
lang,
"Auto-approve enabled for this session.",
"已启用自动批准。",
),
);
} }
msg.with_title(t(lang, "Permission Response", "权限响应")) msg.with_title(i18n::permission_response_title(lang))
} }
// ── follow-up (non-command text) ── // ── follow-up (non-command text) ──
@@ -777,18 +692,15 @@ pub async fn handle_followup(
) -> 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,
Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), Err(e) => {
return RichMessage::error(format!("{}{e}", i18n::failed_to_load_context_label(lang)));
}
}; };
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(tp( return RichMessage::info(i18n::no_active_session_use_task(lang, prefix));
lang,
prefix,
"No active session. Use {prefix}task to start one.",
"没有活跃的会话,请使用 {prefix}task 开始新任务。",
));
} }
}; };
@@ -799,12 +711,7 @@ 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(tp( return RichMessage::info(i18n::session_connection_lost(lang, prefix));
lang,
prefix,
"Session connection lost. Use {prefix}task to start a new one.",
"会话连接已断开,请使用 {prefix}task 开始新任务。",
));
} }
} }
@@ -819,11 +726,11 @@ pub async fn handle_followup(
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::error(format!( return RichMessage::error(format!(
"{}{e}", "{}{e}",
t(lang, "Failed to send message: ", "发送消息失败: ") i18n::failed_to_send_message_label(lang)
)); ));
} }
RichMessage::info(t(lang, "Message sent.", "消息已发送。")) RichMessage::info(i18n::message_sent(lang))
} }
// ── /resume (list recent) ── // ── /resume (list recent) ──
@@ -852,12 +759,8 @@ async fn list_recent_sessions(
}; };
if recent.is_empty() { if recent.is_empty() {
return RichMessage::info(t( return RichMessage::info(i18n::no_conversations_found(lang))
lang, .with_title(i18n::recent_conversations_title(lang));
"No conversations found.",
"暂无会话记录。",
))
.with_title(t(lang, "Recent Conversations", "最近会话"));
} }
let mut body = String::new(); let mut body = String::new();
@@ -871,37 +774,13 @@ async fn list_recent_sessions(
)); ));
} }
body.push_str(&format!( body.push_str(&format!("\n{}", i18n::recent_resume_hint(lang, prefix)));
"\n{}",
tp(
lang,
prefix,
"Reply {prefix}resume <id> to resume a session.",
"回复 {prefix}resume <会话ID> 恢复会话。"
)
));
RichMessage::info(body.trim_end()).with_title(t( RichMessage::info(body.trim_end()).with_title(i18n::recent_conversations_title(lang))
lang,
"Recent Conversations",
"最近会话",
))
} }
// ── Helpers ── // ── Helpers ──
fn t(lang: Lang, en: &str, zh: &str) -> String {
match lang {
Lang::ZhCn | Lang::ZhTw => zh.to_string(),
_ => en.to_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()
@@ -917,16 +796,13 @@ fn parse_agent_type(name: &str) -> Option<AgentType> {
fn resolve_agent_type( fn resolve_agent_type(
sender_agent: &Option<String>, sender_agent: &Option<String>,
folder_default: &Option<AgentType>, folder_default: &Option<AgentType>,
) -> AgentType { ) -> Option<AgentType> {
if let Some(ref at_str) = sender_agent { if let Some(ref at_str) = sender_agent {
if let Some(at) = parse_agent_type(at_str) { if let Some(at) = parse_agent_type(at_str) {
return at; return Some(at);
} }
} }
if let Some(at) = folder_default { folder_default.as_ref().copied()
return *at;
}
AgentType::ClaudeCode
} }
fn truncate_title(s: &str) -> String { fn truncate_title(s: &str) -> String {

View File

@@ -137,27 +137,37 @@ async fn handle_acp_event_payload(
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
let mut guard = bridge.lock().await; // Collect flush info under the lock, then release before any IO.
if let Some(session) = guard.get_mut(connection_id) { let flush_info: Option<(i32, String, Option<String>)> = {
session.content_buffer.push_str(text); let mut guard = bridge.lock().await;
match guard.get_mut(connection_id) {
if session.content_buffer.len() >= BUFFER_FLUSH_THRESHOLD Some(session) => {
&& session.last_flushed.elapsed() >= Duration::from_secs(2) session.content_buffer.push_str(text);
{ if session.content_buffer.len() >= BUFFER_FLUSH_THRESHOLD
let channel_id = session.channel_id; && session.last_flushed.elapsed() >= Duration::from_secs(2)
let last_tool = session.tool_calls.last().cloned(); {
session.last_flushed = Instant::now(); session.last_flushed = Instant::now();
Some((
let lang = get_lang(db).await; session.channel_id,
let mut status = super::i18n::agent_responding(lang).to_string(); session.agent_type.to_string(),
if let Some(tool) = last_tool { session.tool_calls.last().cloned(),
status.push_str(&format!(" | {tool}")); ))
} else {
None
}
} }
drop(guard); None => None,
let msg = RichMessage::info(status);
let _ = manager.send_to_channel(channel_id, &msg).await;
} }
};
if let Some((channel_id, agent_label, last_tool)) = flush_info {
let lang = get_lang(db).await;
let mut status = super::i18n::agent_responding(lang, &agent_label);
if let Some(tool) = last_tool {
status.push_str(&format!(" | {tool}"));
}
let msg = RichMessage::info(status);
let _ = manager.send_to_channel(channel_id, &msg).await;
} }
} }
@@ -430,6 +440,7 @@ async fn flush_progress(
manager: &ChatChannelManager, manager: &ChatChannelManager,
db: &DatabaseConnection, db: &DatabaseConnection,
) { ) {
let lang = get_lang(db).await;
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();
@@ -439,8 +450,8 @@ async fn flush_progress(
{ {
session.last_flushed = Instant::now(); session.last_flushed = Instant::now();
let last_tool = session.tool_calls.last().cloned(); let last_tool = session.tool_calls.last().cloned();
let lang = get_lang(db).await; let agent_label = session.agent_type.to_string();
let mut status = super::i18n::agent_responding(lang).to_string(); let mut status = super::i18n::agent_responding(lang, &agent_label);
if let Some(tool) = last_tool { if let Some(tool) = last_tool {
status.push_str(&format!(" | {tool}")); status.push_str(&format!(" | {tool}"));
} }

View File

@@ -99,8 +99,18 @@ pub async fn soft_delete(conn: &DatabaseConnection, conversation_id: i32) -> Res
} }
fn parse_agent_type(s: &str) -> AgentType { fn parse_agent_type(s: &str) -> AgentType {
serde_json::from_value(serde_json::Value::String(s.to_string())) match serde_json::from_value(serde_json::Value::String(s.to_string())) {
.unwrap_or(AgentType::ClaudeCode) Ok(at) => at,
Err(_) => {
// DB has a value the enum does not recognise (manual edit or removed variant).
// Fall back to ClaudeCode so the row stays readable, but log so resume-as-wrong-agent
// regressions are traceable.
eprintln!(
"[conversation_service] unknown agent_type {s:?} in DB, falling back to ClaudeCode"
);
AgentType::ClaudeCode
}
}
} }
fn conv_to_summary(r: conversation::Model) -> DbConversationSummary { fn conv_to_summary(r: conversation::Model) -> DbConversationSummary {