add new tool: to_captcha
This commit is contained in:
parent
50589232d6
commit
f1f32d8366
14 changed files with 1008 additions and 130 deletions
|
|
@ -1922,6 +1922,7 @@ class HermesCLI:
|
|||
platform="cli",
|
||||
session_db=self._session_db,
|
||||
clarify_callback=self._clarify_callback,
|
||||
captcha_callback=self._captcha_callback,
|
||||
reasoning_callback=(
|
||||
self._stream_reasoning_delta if (self.streaming_enabled and self.show_reasoning)
|
||||
else self._on_reasoning if (self.show_reasoning or self.verbose)
|
||||
|
|
@ -5113,6 +5114,40 @@ class HermesCLI:
|
|||
"Use your best judgement to make the choice and proceed."
|
||||
)
|
||||
|
||||
def _captcha_callback(self, payload):
|
||||
"""Prompt the user to complete a paused CAPTCHA flow in the live browser."""
|
||||
import time as _time
|
||||
|
||||
timeout = int((payload.get("verification") or {}).get("max_wait_seconds", 900))
|
||||
response_queue = queue.Queue()
|
||||
self._captcha_state = {
|
||||
"payload": payload,
|
||||
"response_queue": response_queue,
|
||||
}
|
||||
self._captcha_deadline = _time.monotonic() + timeout
|
||||
self._invalidate()
|
||||
|
||||
last_refresh = _time.monotonic()
|
||||
while True:
|
||||
try:
|
||||
result = response_queue.get(timeout=1)
|
||||
self._captcha_deadline = 0
|
||||
return result
|
||||
except queue.Empty:
|
||||
remaining = self._captcha_deadline - _time.monotonic()
|
||||
if remaining <= 0:
|
||||
break
|
||||
now = _time.monotonic()
|
||||
if now - last_refresh >= 5.0:
|
||||
last_refresh = now
|
||||
self._invalidate()
|
||||
|
||||
self._captcha_state = None
|
||||
self._captcha_deadline = 0
|
||||
self._invalidate()
|
||||
_cprint(f"\n{_DIM}(captcha wait timed out after {timeout}s){_RST}")
|
||||
return {"action": "timeout", "user_response": ""}
|
||||
|
||||
def _sudo_password_callback(self) -> str:
|
||||
"""
|
||||
Prompt for sudo password through the prompt_toolkit UI.
|
||||
|
|
@ -5812,6 +5847,8 @@ class HermesCLI:
|
|||
return [("class:sudo-prompt", f"🔑 {state_suffix}")]
|
||||
if self._approval_state:
|
||||
return [("class:prompt-working", f"⚠ {state_suffix}")]
|
||||
if self._captcha_state:
|
||||
return [("class:prompt-working", f"🧩 {state_suffix}")]
|
||||
if self._clarify_freetext:
|
||||
return [("class:clarify-selected", f"✎ {state_suffix}")]
|
||||
if self._clarify_state:
|
||||
|
|
@ -5878,6 +5915,7 @@ class HermesCLI:
|
|||
sudo_widget,
|
||||
secret_widget,
|
||||
approval_widget,
|
||||
captcha_widget,
|
||||
clarify_widget,
|
||||
spinner_widget,
|
||||
spacer,
|
||||
|
|
@ -5900,6 +5938,7 @@ class HermesCLI:
|
|||
sudo_widget,
|
||||
secret_widget,
|
||||
approval_widget,
|
||||
captcha_widget,
|
||||
clarify_widget,
|
||||
spinner_widget,
|
||||
spacer,
|
||||
|
|
@ -5983,6 +6022,10 @@ class HermesCLI:
|
|||
self._approval_deadline = 0
|
||||
self._approval_lock = threading.Lock() # serialize concurrent approval prompts (delegation race fix)
|
||||
|
||||
# CAPTCHA / human verification prompt state
|
||||
self._captcha_state = None # dict with payload + response_queue
|
||||
self._captcha_deadline = 0
|
||||
|
||||
# Slash command loading state
|
||||
self._command_running = False
|
||||
self._command_status = ""
|
||||
|
|
@ -6058,6 +6101,23 @@ class HermesCLI:
|
|||
event.app.invalidate()
|
||||
return
|
||||
|
||||
# --- CAPTCHA prompt: accept ready/done/abort style input ---
|
||||
if self._captcha_state:
|
||||
text = event.app.current_buffer.text.strip()
|
||||
normalized = text.lower()
|
||||
if normalized in {"abort", "cancel", "stop"}:
|
||||
action = "abort"
|
||||
elif text:
|
||||
action = "ready"
|
||||
else:
|
||||
return
|
||||
self._captcha_state["response_queue"].put({"action": action, "user_response": text})
|
||||
self._captcha_state = None
|
||||
self._captcha_deadline = 0
|
||||
event.app.current_buffer.reset()
|
||||
event.app.invalidate()
|
||||
return
|
||||
|
||||
# --- Clarify freetext mode: user typed their own answer ---
|
||||
if self._clarify_freetext and self._clarify_state:
|
||||
text = event.app.current_buffer.text.strip()
|
||||
|
|
@ -6194,7 +6254,7 @@ class HermesCLI:
|
|||
# Buffer.auto_up/auto_down handle both: cursor movement when multi-line,
|
||||
# history browsing when on the first/last line (or single-line input).
|
||||
_normal_input = Condition(
|
||||
lambda: not self._clarify_state and not self._approval_state and not self._sudo_state and not self._secret_state
|
||||
lambda: not self._clarify_state and not self._approval_state and not self._captcha_state and not self._sudo_state and not self._secret_state
|
||||
)
|
||||
|
||||
@kb.add('up', filter=_normal_input)
|
||||
|
|
@ -6261,6 +6321,14 @@ class HermesCLI:
|
|||
event.app.invalidate()
|
||||
return
|
||||
|
||||
if self._captcha_state:
|
||||
self._captcha_state["response_queue"].put({"action": "timeout", "user_response": ""})
|
||||
self._captcha_state = None
|
||||
self._captcha_deadline = 0
|
||||
event.app.current_buffer.reset()
|
||||
event.app.invalidate()
|
||||
return
|
||||
|
||||
# Cancel clarify prompt
|
||||
if self._clarify_state:
|
||||
self._clarify_state["response_queue"].put(
|
||||
|
|
@ -6334,7 +6402,7 @@ class HermesCLI:
|
|||
# Guard: don't START recording during agent run or interactive prompts
|
||||
if cli_ref._agent_running:
|
||||
return
|
||||
if cli_ref._clarify_state or cli_ref._sudo_state or cli_ref._approval_state:
|
||||
if cli_ref._clarify_state or cli_ref._sudo_state or cli_ref._approval_state or cli_ref._captcha_state:
|
||||
return
|
||||
# Guard: don't start while a previous stop/transcribe cycle is
|
||||
# still running — recorder.stop() holds AudioRecorder._lock and
|
||||
|
|
@ -6554,6 +6622,8 @@ class HermesCLI:
|
|||
return "type secret (hidden), Enter to skip"
|
||||
if cli_ref._approval_state:
|
||||
return ""
|
||||
if cli_ref._captcha_state:
|
||||
return "type ready/done after you solve the challenge, or abort to cancel"
|
||||
if cli_ref._clarify_freetext:
|
||||
return "type your answer here and press Enter"
|
||||
if cli_ref._clarify_state:
|
||||
|
|
@ -6597,6 +6667,13 @@ class HermesCLI:
|
|||
('class:clarify-countdown', f' ({remaining}s)'),
|
||||
]
|
||||
|
||||
if cli_ref._captcha_state:
|
||||
remaining = max(0, int(cli_ref._captcha_deadline - _time.monotonic()))
|
||||
return [
|
||||
('class:hint', " complete the challenge in the browser, then type 'ready'"),
|
||||
('class:clarify-countdown', f' ({remaining}s)'),
|
||||
]
|
||||
|
||||
if cli_ref._clarify_state:
|
||||
remaining = max(0, int(cli_ref._clarify_deadline - _time.monotonic()))
|
||||
countdown = f' ({remaining}s)' if cli_ref._clarify_deadline else ''
|
||||
|
|
@ -6619,7 +6696,7 @@ class HermesCLI:
|
|||
return []
|
||||
|
||||
def get_hint_height():
|
||||
if cli_ref._sudo_state or cli_ref._secret_state or cli_ref._approval_state or cli_ref._clarify_state or cli_ref._command_running:
|
||||
if cli_ref._sudo_state or cli_ref._secret_state or cli_ref._approval_state or cli_ref._captcha_state or cli_ref._clarify_state or cli_ref._command_running:
|
||||
return 1
|
||||
# Keep a 1-line spacer while agent runs so output doesn't push
|
||||
# right up against the top rule of the input area
|
||||
|
|
@ -6644,7 +6721,7 @@ class HermesCLI:
|
|||
height=get_hint_height,
|
||||
)
|
||||
|
||||
# --- Clarify tool: dynamic display widget for questions + choices ---
|
||||
# --- Interactive panels: CAPTCHA + clarify ---
|
||||
|
||||
def _panel_box_width(title: str, content_lines: list[str], min_width: int = 46, max_width: int = 76) -> int:
|
||||
"""Choose a stable panel width wide enough for the title and content."""
|
||||
|
|
@ -6672,6 +6749,45 @@ class HermesCLI:
|
|||
def _append_blank_panel_line(lines, border_style: str, box_width: int) -> None:
|
||||
lines.append((border_style, "│" + (" " * box_width) + "│\n"))
|
||||
|
||||
def _get_captcha_display():
|
||||
state = cli_ref._captcha_state
|
||||
if not state:
|
||||
return []
|
||||
|
||||
payload = state.get("payload") or {}
|
||||
title = "Manual CAPTCHA Required"
|
||||
browser_view_url = payload.get("browser_view_url") or "Browser view URL is not configured."
|
||||
body_lines = [
|
||||
payload.get("instructions") or "Open the live browser and complete the verification challenge.",
|
||||
f"Type: {payload.get('captcha_type', 'unknown')}",
|
||||
f"Task ID: {payload.get('task_id', '')}",
|
||||
f"Browser: {browser_view_url}",
|
||||
"When the challenge disappears, type 'ready' or 'done' and press Enter.",
|
||||
"Type 'abort' to stop this browser task.",
|
||||
]
|
||||
box_width = _panel_box_width(title, body_lines, min_width=56, max_width=94)
|
||||
inner_text_width = max(8, box_width - 2)
|
||||
lines = []
|
||||
lines.append(('class:captcha-border', '╭─ '))
|
||||
lines.append(('class:captcha-title', title))
|
||||
lines.append(('class:captcha-border', ' ' + ('─' * max(0, box_width - len(title) - 3)) + '╮\n'))
|
||||
_append_blank_panel_line(lines, 'class:captcha-border', box_width)
|
||||
for text in body_lines:
|
||||
style = 'class:captcha-link' if text.startswith("Browser: ") else 'class:captcha-text'
|
||||
for wrapped in _wrap_panel_text(text, inner_text_width):
|
||||
_append_panel_line(lines, 'class:captcha-border', style, wrapped, box_width)
|
||||
_append_blank_panel_line(lines, 'class:captcha-border', box_width)
|
||||
lines.append(('class:captcha-border', '╰' + ('─' * box_width) + '╯\n'))
|
||||
return lines
|
||||
|
||||
captcha_widget = ConditionalContainer(
|
||||
Window(
|
||||
FormattedTextControl(_get_captcha_display),
|
||||
wrap_lines=True,
|
||||
),
|
||||
filter=Condition(lambda: cli_ref._captcha_state is not None),
|
||||
)
|
||||
|
||||
def _get_clarify_display():
|
||||
"""Build styled text for the clarify question/choices panel."""
|
||||
state = cli_ref._clarify_state
|
||||
|
|
@ -6897,6 +7013,7 @@ class HermesCLI:
|
|||
sudo_widget=sudo_widget,
|
||||
secret_widget=secret_widget,
|
||||
approval_widget=approval_widget,
|
||||
captcha_widget=captcha_widget,
|
||||
clarify_widget=clarify_widget,
|
||||
spinner_widget=spinner_widget,
|
||||
spacer=spacer,
|
||||
|
|
@ -6954,6 +7071,11 @@ class HermesCLI:
|
|||
'approval-cmd': '#AAAAAA italic',
|
||||
'approval-choice': '#AAAAAA',
|
||||
'approval-selected': '#FFD700 bold',
|
||||
# CAPTCHA panel
|
||||
'captcha-border': '#CD7F32',
|
||||
'captcha-title': '#FFBF00 bold',
|
||||
'captcha-text': '#FFF8DC',
|
||||
'captcha-link': '#87CEEB underline',
|
||||
# Voice mode
|
||||
'voice-prompt': '#87CEEB',
|
||||
'voice-recording': '#FF4444 bold',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue