File size: 9,773 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | """Compatibilidade com o PC de qualquer pessoa: endereço do Ollama,
atalhos de apps (Snap, Flatpak, nomes em português, KDE sem
gtk-launch), GPU AMD, PC sem GPU, sessão Wayland, sem display e a
confirmação de comando perigoso na interface gráfica."""
import os
import sys
import tempfile
from pathlib import Path
from _util import carregar_tars
tars = carregar_tars()
# ------------------------------------------------------------------
# OLLAMA_HOST em todos os formatos que o próprio Ollama aceita
# ------------------------------------------------------------------
casos_host = {
None: "http://127.0.0.1:11434",
"": "http://127.0.0.1:11434",
"0.0.0.0": "http://127.0.0.1:11434",
"0.0.0.0:11434": "http://127.0.0.1:11434",
":11434": "http://127.0.0.1:11434",
"192.168.0.10": "http://192.168.0.10:11434",
"192.168.0.10:8080": "http://192.168.0.10:8080",
"http://127.0.0.1:11434": "http://127.0.0.1:11434",
"http://127.0.0.1:11434/": "http://127.0.0.1:11434",
"https://ollama.exemplo.com": "https://ollama.exemplo.com:443",
"[::1]:11434": "http://[::1]:11434",
}
for entrada, esperado in casos_host.items():
obtido = tars._normalizar_ollama_host(entrada)
assert obtido == esperado, (entrada, obtido, esperado)
print("OK: OLLAMA_HOST em", len(casos_host), "formatos")
# ------------------------------------------------------------------
# Atalhos .desktop
# ------------------------------------------------------------------
tmp = Path(tempfile.mkdtemp())
apps = tmp / "applications"
apps.mkdir()
(apps / "firefox_firefox.desktop").write_text(
"[Desktop Entry]\nType=Application\nName=Firefox Web Browser\n"
"Name[pt_BR]=Navegador Firefox\nExec=/snap/bin/firefox %u\n"
"\n[Desktop Action new-private-window]\nName=Nova janela anônima\n"
"Exec=/snap/bin/firefox --private-window %u\n"
)
(apps / "org.gnome.Calculator.desktop").write_text(
"[Desktop Entry]\nType=Application\nName=Calculator\n"
"Name[pt_BR]=Calculadora\nExec=gnome-calculator\n"
)
(apps / "escondido.desktop").write_text(
"[Desktop Entry]\nType=Application\nName=Escondido\nHidden=true\nExec=x\n"
)
(apps / "link.desktop").write_text(
"[Desktop Entry]\nType=Link\nName=Um link\nURL=https://exemplo.com\n"
)
# Ubuntu/Zorin: tradução num catálogo gettext, não no arquivo
(apps / "org.gnome.Calculator.desktop").write_text(
"[Desktop Entry]\nType=Application\nName=Calculator\n"
"Exec=gnome-calculator\nX-Ubuntu-Gettext-Domain=gnome-calculator\n"
)
class _CatalogoFalso:
def gettext(self, texto):
return {"Calculator": "Calculadora"}.get(texto, texto)
_catalogos_reais = tars._catalogos_gettext
tars._catalogos_gettext = lambda dominio, grupos=(): [_CatalogoFalso()] if dominio == "gnome-calculator" else []
calc = tars._ler_desktop_entry(apps / "org.gnome.Calculator.desktop")
assert "Calculadora" in calc["outros_nomes"], calc
print("OK: nome traduzido via gettext (Ubuntu/Zorin) entra na busca")
ff = tars._ler_desktop_entry(apps / "firefox_firefox.desktop")
assert ff["exec"] == "/snap/bin/firefox %u", ff["exec"] # não o da "janela anônima"
assert "Navegador Firefox" in ff["outros_nomes"]
assert tars._ler_desktop_entry(apps / "escondido.desktop") is None
assert tars._ler_desktop_entry(apps / "link.desktop") is None
print("OK: .desktop lê só a seção principal e ignora Hidden/Link")
os.environ["XDG_DATA_DIRS"] = "/usr/share"
pastas = [str(p) for p in tars._pastas_de_aplicativos()]
assert "/var/lib/snapd/desktop/applications" in pastas, pastas
assert "/var/lib/flatpak/exports/share/applications" in pastas, pastas
print("OK: procura atalhos de Snap e Flatpak")
tars._pastas_de_aplicativos = lambda: [apps]
tars._cache_desktop["dados"] = None
tars.flatpak_apps = lambda forcar=False: []
tars.perguntar_candidatos_app = lambda alvo: []
achado = tars._buscar_app_no_sistema("calculadora")
assert achado and achado["nome"] == "Calculator", achado
print("OK: 'calculadora' acha o app 'Calculator' pelo nome em português")
# Qual lançador usar, conforme o que existe no PC
existentes = set()
tars.shutil.which = lambda nome: f"/usr/bin/{nome}" if nome in existentes else None
os.environ["XDG_DATA_DIRS"] = str(tmp)
existentes = {"gtk-launch", "gio"}
cmd = tars.comando_para_desktop_entry(apps / "org.gnome.Calculator.desktop", "gnome-calculator")
assert cmd == ["gtk-launch", "org.gnome.Calculator"], cmd
os.environ["XDG_DATA_DIRS"] = "/usr/share" # atalho fora do XDG: gtk-launch não acharia
cmd = tars.comando_para_desktop_entry(apps / "firefox_firefox.desktop", "/snap/bin/firefox %u")
assert cmd[:2] == ["gio", "launch"], cmd
existentes = {"kioclient"} # KDE sem GTK/GLib
cmd = tars.comando_para_desktop_entry(apps / "org.gnome.Calculator.desktop", "gnome-calculator")
assert cmd[:2] == ["kioclient", "exec"], cmd
existentes = set()
cmd = tars.comando_para_desktop_entry(apps / "firefox_firefox.desktop", "/snap/bin/firefox %u")
assert cmd == ["/snap/bin/firefox"], cmd
print("OK: abre atalhos com gtk-launch, gio, kioclient ou o próprio Exec=")
import shutil as _shutil
tars.shutil.which = _shutil.which
# ------------------------------------------------------------------
# GPU AMD (sysfs) e vídeo integrado ignorado
# ------------------------------------------------------------------
drm = Path(tempfile.mkdtemp())
for card, vram, usado in (("card0", 512 * 1024**2, 100 * 1024**2), ("card1", 8 * 1024**3, 2 * 1024**3)):
d = drm / card / "device"
d.mkdir(parents=True)
(d / "vendor").write_text("0x1002\n")
(d / "mem_info_vram_total").write_text(f"{vram}\n")
(d / "mem_info_vram_used").write_text(f"{usado}\n")
gpu = tars._detectar_gpu_amd(str(drm))
assert gpu == {"vram_total_gb": 8.0, "vram_livre_gb": 6.0, "fabricante": "AMD"}, gpu
assert tars._detectar_gpu_amd(str(drm / "nada")) is None
print("OK: GPU AMD detectada; vídeo integrado de 512MB ignorado")
# ------------------------------------------------------------------
# PC sem GPU: AUTO não escolhe o modelo gigante
# ------------------------------------------------------------------
def modelo(tag, params, cat="geral"):
return {
"tag": tag, "parametros_b": params, "categoria": cat,
"vram_estimado_gb": tars._estimar_vram_gb(params, "Q4_K_M"),
"capacidades": {"completion", "tools"}, "capacidades_conhecidas": True,
}
catalogo = {m["tag"]: m for m in (modelo("grande:24b", 24), modelo("medio:8b", 8.2), modelo("pequeno:4b", 4))}
tars.detectar_gpu = lambda forcar=False: None
tars.ram_total_gb = lambda: 16.0
cadeia = tars.construir_cadeia_fallback("geral", catalogo)
assert cadeia[0] == "medio:8b", cadeia
print("OK: sem GPU (16GB RAM), conversa geral usa o 8B, não o 24B:", cadeia)
tars.ram_total_gb = lambda: None # sem como saber: não penaliza
assert tars.construir_cadeia_fallback("geral", catalogo)[0] == "grande:24b"
assert tars.formatar_parametros(0.75163) == "752M"
assert tars.formatar_parametros(8.19) == "8.2B"
assert tars.formatar_parametros(24.0) == "24B"
print("OK: parâmetros formatados (752M, 8.2B, 24B)")
# ------------------------------------------------------------------
# Wayland: captura por programa; preto do XWayland é rejeitado
# ------------------------------------------------------------------
from PIL import Image
tars.SESSAO_GRAFICA = "wayland"
chamados = []
def falso_programa(cmd):
chamados.append(cmd[0])
return Image.new("RGB", (100, 50), (40, 80, 120))
tars._capturar_com_programa = falso_programa
tars.shutil.which = lambda nome: "/usr/bin/spectacle" if nome == "spectacle" else None
img = tars._capturar_tela()
assert chamados == ["spectacle"] and img.size == (100, 50), chamados
print("OK: Wayland usa o capturador do desktop (spectacle no KDE)")
import PIL.ImageGrab
tars.shutil.which = lambda nome: None
PIL.ImageGrab.grab = lambda *a, **k: Image.new("RGB", (100, 50), (0, 0, 0))
tars.pyautogui = tars._PyautoguiIndisponivel("teste")
try:
tars._capturar_tela()
raise AssertionError("tela preta no Wayland deveria virar erro")
except RuntimeError as e:
assert "gnome-screenshot" in str(e), e
print("OK: Wayland sem capturador dá erro claro dizendo o que instalar")
tars.shutil.which = _shutil.which
# Mouse/teclado no Wayland: resultado avisa a IA
tars._DISPATCH_FERRAMENTAS["press_key"] = lambda a: {"sucesso": True, "mensagem": "Tecla pressionada."}
r = tars.executar_ferramenta("press_key", {"key": "enter"})
assert "Xorg" in r["mensagem"], r
tars.SESSAO_GRAFICA = "x11"
r = tars.executar_ferramenta("press_key", {"key": "enter"})
assert "Xorg" not in r["mensagem"], r
print("OK: no Wayland, ferramentas de mouse/teclado avisam a IA")
# ------------------------------------------------------------------
# Confirmação de comando perigoso pela interface gráfica
# ------------------------------------------------------------------
perguntados = []
tars.confirmar_comando_hook = lambda cmd: perguntados.append(cmd) or False
r = tars.executar_terminal("rm -rf /tmp/qualquer")
assert perguntados == ["rm -rf /tmp/qualquer"] and not r["sucesso"], (perguntados, r)
tars.confirmar_comando_hook = None
print("OK: comando perigoso pergunta pela janela quando há interface")
# ------------------------------------------------------------------
# Sem display: pyautogui não importa, mas o openTARS carrega
# ------------------------------------------------------------------
sys.modules["pyautogui"] = None # faz "import pyautogui" falhar
sem_display = carregar_tars()
assert sem_display.ERRO_PYAUTOGUI, "deveria registrar o motivo"
r = sem_display.clicar_mouse("left", 10, 10)
assert not r["sucesso"] and "unavailable" in r["mensagem"], r
print("OK: sem display, o openTARS abre e mouse/teclado respondem com o motivo")
print("\nTUDO OK.")
|