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:
@@ -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..."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user