mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-30 06:41:51 +00:00
fix(telegram): wire gt: callback dispatch for gmail-triage buttons
The gmail-triage skill's Telegram inline buttons emit callback_data of the form `gt:<verb>:<arg>`, but `_handle_callback_query` had no `gt:` branch — taps fell through silently and the spinner sat there until Telegram timed it out. Add `_handle_gmail_triage_callback`, dispatched from the existing callback router, that: - Authorizes the caller via the same `_is_callback_user_authorized` path as the approval / slash-confirm / clarify handlers. - Maps each verb to a script under `~/.hermes/scripts/gmail-triage/` and runs it async with a 60s timeout. - Splits verbs into one-shots (send / archive / draft / spam) — append the confirmation and strip the keyboard so the action can't fire twice — and sticky-state changes (mute / trust / vip ± -domain) — append the confirmation but leave the keyboard tappable so the user can stack actions on one email. - On failure: toast only, keyboard preserved so the user can retry. - Logs every callback outcome to gateway.log for debugging.
This commit is contained in:
parent
4f6fef1974
commit
1891bee9d3
1 changed files with 126 additions and 0 deletions
|
|
@ -2858,6 +2858,18 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
await self._handle_model_picker_callback(query, data, chat_id)
|
||||
return
|
||||
|
||||
# --- Gmail-triage callbacks (gt:verb:arg) ---
|
||||
if data.startswith("gt:"):
|
||||
await self._handle_gmail_triage_callback(
|
||||
query,
|
||||
data,
|
||||
query_chat_id=query_chat_id,
|
||||
query_chat_type=query_chat_type,
|
||||
query_thread_id=query_thread_id,
|
||||
query_user_name=query_user_name,
|
||||
)
|
||||
return
|
||||
|
||||
# --- Exec approval callbacks (ea:choice:id) ---
|
||||
if data.startswith("ea:"):
|
||||
parts = data.split(":", 2)
|
||||
|
|
@ -3172,6 +3184,120 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
except Exception as exc:
|
||||
logger.error("Failed to write update response from callback: %s", exc)
|
||||
|
||||
# Maps `gt:<verb>` -> (script-name, extra-args, success-label, is_state).
|
||||
# Scripts live in ~/.hermes/scripts/gmail-triage/. `arg` from the callback
|
||||
# data is always passed as the first positional arg.
|
||||
# is_state=True means the verb is a sticky sender-rule change (mute, trust,
|
||||
# vip) that should leave the keyboard tappable for follow-on actions.
|
||||
# is_state=False is a per-email one-shot (send, archive, draft, spam) that
|
||||
# strips the keyboard on success.
|
||||
_GT_VERB_DISPATCH = {
|
||||
"send": ("send-draft.sh", [], "✓ sent draft", False),
|
||||
"archive": ("archive.sh", [], "✓ archived", False),
|
||||
"draft": ("draft-blank.sh", [], "✓ drafted reply", False),
|
||||
"spam": ("spam.sh", [], "✓ marked spam", False),
|
||||
"mute": ("mute-add.sh", ["email"], "✓ muted", True),
|
||||
"mute-domain": ("mute-add.sh", ["domain"], "✓ muted domain", True),
|
||||
"trust": ("trusted-ops-add.sh", ["email"], "✓ trusted", True),
|
||||
"trust-domain": ("trusted-ops-add.sh", ["domain"], "✓ trusted domain", True),
|
||||
"vip": ("vip-add.sh", ["email"], "✓ marked VIP", True),
|
||||
"vip-domain": ("vip-add.sh", ["domain"], "✓ marked VIP domain", True),
|
||||
}
|
||||
|
||||
async def _handle_gmail_triage_callback(
|
||||
self,
|
||||
query,
|
||||
data: str,
|
||||
*,
|
||||
query_chat_id,
|
||||
query_chat_type,
|
||||
query_thread_id,
|
||||
query_user_name,
|
||||
) -> None:
|
||||
"""Dispatch a gmail-triage inline-button callback (gt:verb:arg)."""
|
||||
parts = data.split(":", 2)
|
||||
if len(parts) != 3:
|
||||
await query.answer(text="Invalid gmail-triage data.")
|
||||
return
|
||||
verb, arg = parts[1], parts[2]
|
||||
|
||||
caller_id = str(getattr(query.from_user, "id", ""))
|
||||
if not self._is_callback_user_authorized(
|
||||
caller_id,
|
||||
chat_id=query_chat_id,
|
||||
chat_type=str(query_chat_type) if query_chat_type is not None else None,
|
||||
thread_id=str(query_thread_id) if query_thread_id is not None else None,
|
||||
user_name=query_user_name,
|
||||
):
|
||||
await query.answer(text="⛔ You are not authorized to act on this email.")
|
||||
return
|
||||
|
||||
entry = self._GT_VERB_DISPATCH.get(verb)
|
||||
if not entry:
|
||||
await query.answer(text=f"Unknown verb: {verb}")
|
||||
return
|
||||
script_name, extra_args, success_label, is_state_verb = entry
|
||||
|
||||
script_path = _Path.home() / ".hermes" / "scripts" / "gmail-triage" / script_name
|
||||
if not script_path.exists():
|
||||
await query.answer(text=f"❌ {script_name} missing")
|
||||
logger.error("[%s] gmail-triage script missing: %s", self.name, script_path)
|
||||
return
|
||||
|
||||
cmd = [str(script_path), arg, *extra_args]
|
||||
success = False
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
_stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
||||
proc.communicate(), timeout=60,
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
label = success_label
|
||||
success = True
|
||||
logger.info(
|
||||
"[%s] gmail-triage callback ok: verb=%s arg=%s",
|
||||
self.name, verb, arg,
|
||||
)
|
||||
else:
|
||||
stderr_text = stderr_bytes.decode("utf-8", errors="replace").strip()
|
||||
last_line = stderr_text.splitlines()[-1] if stderr_text else f"exit {proc.returncode}"
|
||||
label = f"❌ {verb} failed: {last_line[:80]}"
|
||||
logger.error(
|
||||
"[%s] gmail-triage callback failed: verb=%s arg=%s rc=%s stderr=%s",
|
||||
self.name, verb, arg, proc.returncode, stderr_text,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
label = f"❌ {verb} timed out"
|
||||
logger.error("[%s] gmail-triage callback timed out: verb=%s arg=%s", self.name, verb, arg)
|
||||
except Exception as exc:
|
||||
label = f"❌ {verb} error: {exc}"
|
||||
logger.error(
|
||||
"[%s] gmail-triage callback exception: verb=%s arg=%s err=%s",
|
||||
self.name, verb, arg, exc, exc_info=True,
|
||||
)
|
||||
|
||||
await query.answer(text=label)
|
||||
if not success:
|
||||
return
|
||||
|
||||
user_display = getattr(query.from_user, "first_name", "User")
|
||||
original_text = (query.message.text or "") if query.message else ""
|
||||
appended = f"{original_text}\n— {label} by {user_display}"
|
||||
try:
|
||||
if is_state_verb:
|
||||
# Sticky state change: append confirmation, KEEP keyboard so
|
||||
# the user can stack further actions on this email.
|
||||
await query.edit_message_text(text=appended)
|
||||
else:
|
||||
# Per-email one-shot: strip keyboard so the action can't fire twice.
|
||||
await query.edit_message_text(text=appended, reply_markup=None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _missing_media_path_error(self, label: str, path: str) -> str:
|
||||
"""Build an actionable file-not-found error for gateway MEDIA delivery.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue