File size: 10,734 Bytes
2695555 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | """2.6.1: app aberto que não expõe os botões (Electron: Claude, Discord...).
Log real: "open claude and type..." -> a janela do Claude não tem árvore de
acessibilidade; a IA tentou click_element com 14 nomes diferentes (tirados
da descrição do print), todos falhando, até o limite de etapas.
O xmessage (X puro, sem acessibilidade) faz o papel do app Electron."""
import os
import shutil
import subprocess
import sys
import time
from _util import carregar_tars
tars = carregar_tars()
# ------------------------------------------------------------
# 1. Parte com a janela de verdade (precisa de X + D-Bus + xmessage)
# ------------------------------------------------------------
tem_x = (os.environ.get("DISPLAY") and os.environ.get("DBUS_SESSION_BUS_ADDRESS")
and shutil.which("xmessage") and not tars.acess.indisponivel())
if tem_x:
proc = subprocess.Popen(["xmessage", "-name", "clauded", "-title", "Claude", "Olá, eu sou um app Electron"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
limite = time.time() + 10
while time.time() < limite and not tars.encontrar_janela_por_nome("Claude"):
time.sleep(0.2)
abriu = []
tars.abrir_aplicativo = lambda app: abriu.append(app) or {"sucesso": True}
r = tars.executar_ferramenta("click_element", {"name": "New chat", "window": "Claude"})
assert not r["sucesso"] and "does not expose" in r["mensagem"] and "type_in_element" in r["mensagem"], r
assert not abriu, "a janela já estava aberta: não abre o app de novo"
r2 = tars.executar_ferramenta("list_elements", {"window": "Claude"})
if getattr(tars, "ocr", None) and tars.ocr.disponivel(): # o OCR saiu do app; o ramo fica para se voltar
# 2.9: sem árvore, a janela é lida pela tela (OCR)
assert r2["sucesso"] and "READ the window" in r2["mensagem"] and r2["textos"], r2
else:
assert not r2["sucesso"] and "Do NOT use click_element" in r2["mensagem"]
print("OK: janela aberta sem botões expostos -> diz na hora pra usar type_in_element/click_mouse (e não reabre o app)")
# type_in_element numa janela dessas: foca a janela, digita no campo com foco e aperta Enter
digitado, teclas = [], []
tars.digitar_texto = lambda txt: digitado.append(txt) or {"sucesso": True}
tars.pressionar_tecla = lambda k: teclas.append(k) or {"sucesso": True}
r3 = tars.executar_ferramenta("type_in_element", {"text": "Qual a capital da França?", "window": "Claude",
"submit": "true"})
assert r3["sucesso"] and digitado == ["Qual a capital da França?"] and teclas == ["enter"], (r3, digitado, teclas)
ativa = tars.janela_ativa_x()
# sem gerenciador de janelas (o xvfb da suíte) nenhuma janela fica "ativa": só confere quando há um
tem_gerenciador = subprocess.run(["xprop", "-root", "_NET_SUPPORTING_WM_CHECK"], capture_output=True,
text=True).stdout.count("window id") > 0 if shutil.which("xprop") else False
if tem_gerenciador:
assert ativa == tars.encontrar_janela_por_nome("Claude")["id"], "a janela do app veio pra frente antes de digitar"
print("OK: type_in_element(window, submit=true) num app sem botões: foca a janela, digita e envia")
finally:
proc.kill()
else:
print("PULADO (parte 1): precisa de DISPLAY, D-Bus, xmessage e acessibilidade")
# ------------------------------------------------------------
# 2. O loop do log: a IA insiste no click_element com nomes diferentes
# ------------------------------------------------------------
tars.carregar_modelo = lambda m, silencioso=False: True
tars.contexto_ia = lambda: 8192
tars.modo_modelo = "MANUAL"
tars.catalogar_modelos = lambda forcar=False: {}
nomes = iter(f"Botão {i}" for i in range(100))
turnos = []
def turno(modelo, msgs, tools, **kw):
turnos.append(bool(tools))
if not tools:
return {"content": "Não consegui clicar: o app não mostra os botões. Clique você no campo.", "tool_calls": []}, None
return {"content": "", "tool_calls": [{"function": {"name": "click_element",
"arguments": {"name": next(nomes), "window": "Claude"}}}]}, None
tars._executar_turno_streaming = turno
tars._DISPATCH_FERRAMENTAS["click_element"] = lambda a: {"sucesso": False, "mensagem": f"No element named '{a['name']}'."}
tars.conversa = []
resposta = tars.perguntar_modelo("qwen3:8b", "open claude and type hello", tarefa="acao")
assert "Não consegui" in resposta, resposta
assert turnos.count(True) == tars.LIMITE_FALHAS_SEGUIDAS and turnos[-1] is False, turnos
resultados = [m["content"] for m in tars.obter_sessao("qwen3:8b") if m.get("role") == "tool"]
assert "STOP" not in resultados[1] and "STOP: click_element failed 3 times" in resultados[2], resultados[:3]
print(f"OK: 3 falhas seguidas -> 'mude de estratégia'; {tars.LIMITE_FALHAS_SEGUIDAS} -> para e explica ao usuário "
f"(antes: 20 etapas)")
# 3. A mesma chamada que já falhou nem roda de novo
rodou = []
tars._DISPATCH_FERRAMENTAS["click_element"] = lambda a: rodou.append(a) or {"sucesso": False, "mensagem": "nope."}
chamadas = iter([("click_element", {"name": "Send", "window": "Claude"})] * 2 + [None])
def turno2(modelo, msgs, tools, **kw):
proxima = next(chamadas)
if proxima is None or not tools:
return {"content": "ok", "tool_calls": []}, None
return {"content": "", "tool_calls": [{"function": {"name": proxima[0], "arguments": proxima[1]}}]}, None
tars._executar_turno_streaming = turno2
tars.perguntar_modelo("qwen3:8b", "type hello in claude", tarefa="acao")
ultimo = [m["content"] for m in tars.obter_sessao("qwen3:8b") if m.get("role") == "tool"][-1]
assert len(rodou) == 1 and "already tried exactly this" in ultimo, (rodou, ultimo)
print("OK: repetir exatamente a mesma chamada que falhou não roda de novo (a IA é avisada)")
# 4. Descrição da tela de um app sem botões expostos: manda clicar por coordenada
tars._descrever_print = lambda img, pedido, modelo: ("Message box at (400, 700). Send button at (760, 700).", "gemma4:e4b")
tars._marcar_sem_arvore("Claude")
msg = tars._print_em_texto("x", "type hello", "qwen3:8b", "Claude")
assert "click_mouse(x, y, target=" in msg and "click_element(name)" not in msg, msg
msg_outro = tars._print_em_texto("x", "type hello", "qwen3:8b", "calculator")
assert "click_element(name)" in msg_outro
print("OK: com um app sem botões expostos, a descrição da tela manda usar click_mouse(x, y, target=...) e type_text")
# 5. Abrir app pede a árvore de acessibilidade ao Chromium/Electron
envs = []
class Popen:
def __init__(self, *a, **kw):
envs.append(kw.get("env") or {})
self.returncode = 0
def poll(self):
return 0
fonte = carregar_tars() # abrir_aplicativo original (a parte 1 trocou o do outro)
fonte.subprocess.Popen = Popen
fonte.encontrar_app = lambda app: {"comando": ["claude-desktop"], "nome": "Claude", "tipo": "desktop"}
fonte.listar_janelas_x = lambda: None
fonte._processo_ativo = lambda termos: True
fonte.abrir_aplicativo("claude")
assert envs and envs[-1].get("ACCESSIBILITY_ENABLED") == "1", envs
print("OK: apps são abertos com ACCESSIBILITY_ENABLED=1 (Electron/Chromium passam a expor os botões)")
# 6. Log real da 2.6.1: uma falha e a IA manda o usuário fazer na mão
chamadas_ia = iter([
("tool", "click_element", {"name": "Ask a question", "window": "Claude"}),
("texto", "I cannot directly interact with the Claude app's buttons. To ask a question, focus on the text box and type your question, then press Enter."),
("tool", "type_in_element", {"text": "What is the capital of France?", "window": "Claude", "submit": True}),
("texto", "Done: I asked Claude 'What is the capital of France?'."),
])
recebeu = []
def turno3(modelo, msgs, tools, **kw):
recebeu.append(msgs[-1]["content"])
tipo, *resto = next(chamadas_ia)
if tipo == "texto":
return {"content": resto[0], "tool_calls": []}, None
return {"content": "", "tool_calls": [{"function": {"name": resto[0], "arguments": resto[1]}}]}, None
tars._executar_turno_streaming = turno3
tars._DISPATCH_FERRAMENTAS["click_element"] = lambda a: {"sucesso": False, "mensagem": tars._mensagem_sem_arvore("Claude")}
escrito = []
tars._DISPATCH_FERRAMENTAS["type_in_element"] = lambda a: escrito.append(a) or {"sucesso": True, "mensagem": "SUCCESS: typed and sent."}
resposta = tars.perguntar_modelo("qwen3:8b", "abra o claude e faça uma pergunta simples", tarefa="acao")
assert resposta.startswith("Done") and escrito and escrito[0]["submit"] is True, (resposta, escrito)
assert any("Don't hand the task back" in r for r in recebeu)
assert not any("I cannot directly interact" in (m.get("content") or "") for m in tars.obter_sessao("qwen3:8b"))
print("OK: 'foque a caixa e digite você' depois de uma falha -> a IA é mandada fazer ela mesma, e faz")
# 7. Electron/Chromium detectado -> abre com --force-renderer-accessibility
import tempfile # noqa: E402
pasta = tempfile.mkdtemp(prefix="opentars-electron-")
for arquivo, conteudo in (("claude-desktop", b"\x7fELF..."), ("resources.pak", b"")):
with open(os.path.join(pasta, arquivo), "wb") as f:
f.write(conteudo)
script = os.path.join(pasta, "discord")
with open(script, "wb") as f:
f.write(b"#!/bin/sh\nexec /usr/lib/electron/electron /usr/lib/discord/app.asar \"$@\"\n")
comum = os.path.join(tempfile.mkdtemp(), "gnome-calculator")
with open(comum, "wb") as f:
f.write(b"\x7fELF-normal")
assert fonte._eh_electron([os.path.join(pasta, "claude-desktop")]), "resources.pak ao lado: Electron"
assert fonte._eh_electron([script]), "script lançador que chama o electron"
assert not fonte._eh_electron([comum]) and not fonte._eh_electron(["flatpak", "run", "x"])
envs.clear()
chamados = []
class PopenComando(Popen):
def __init__(self, comando, *a, **kw):
chamados.append(list(comando))
super().__init__(comando, *a, **kw)
fonte.subprocess.Popen = PopenComando
fonte.encontrar_app = lambda app: {"comando": [os.path.join(pasta, "claude-desktop")], "nome": "Claude", "tipo": "desktop"}
fonte.abrir_aplicativo("claude")
fonte.encontrar_app = lambda app: {"comando": [comum], "nome": "Calculadora", "tipo": "desktop"}
fonte.abrir_aplicativo("calculadora")
assert chamados[0][-1] == "--force-renderer-accessibility" and chamados[1] == [comum], chamados
print("OK: app Electron é aberto com --force-renderer-accessibility (os outros, do jeito normal)")
print("\nTUDO OK.")
|