3 Commits

Author SHA1 Message Date
Dom
2b1743c206 fix(poc-agent): ouvrir le chat Lea DGX si Tk est indisponible
Some checks failed
tests / Lint (ruff + black) (push) Failing after 1m43s
tests / Tests unitaires (sans GPU) (push) Failing after 1m46s
tests / Tests sécurité (critique) (push) Has been skipped
2026-06-15 21:32:54 +02:00
Dom
48879fb849 fix(vwb): conservation des données de position des anchors Lea lors de l'import
- Supprime le 'pop' de '_anchor_bbox' qui jetait les coordonnées de position (x_pct, y_pct).
- Conserve ces données dans les paramètres du step pour que le frontend puisse les utiliser pour afficher la zone ciblée.
- Évite la création d'une bounding box factice (écran entier) qui rendait le crop de l'ancre inutile.
- Impact isolé à la route d'import, aucun impact sur le runtime d'exécution de Léa ni sur DETTE-015.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-06-15 18:13:29 +02:00
Dom
c12fd8e1c1 fix(dashboard): VWB import URL dynamique pour éviter hardcoded localhost
- Remplace l'URL hardcodée 'http://localhost:5002' par une construction dynamique basée sur l'origine actuelle.
- Permet les tests d'import depuis la VM ou le poste de test via l'IP du banc (ex: 192.168.1.45) sans échec CORS/routage.
- Respecte la règle POC DGX : pas de localhost comme preuve produit.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-06-15 18:13:22 +02:00
3 changed files with 94 additions and 4 deletions

View File

@@ -5,6 +5,9 @@ Fenetre de chat Lea integree au systray — version tkinter native.
Remplace l'approche Edge browser par une vraie fenetre tkinter integree. Remplace l'approche Edge browser par une vraie fenetre tkinter integree.
Design professionnel, theme clair, ancree en bas a droite de l'ecran. Design professionnel, theme clair, ancree en bas a droite de l'ecran.
Tourne dans son propre thread daemon pour ne pas bloquer pystray. Tourne dans son propre thread daemon pour ne pas bloquer pystray.
Le runtime Python embedded Windows ne contient pas toujours Tcl/Tk. Dans ce
cas, le menu "Discuter avec Lea" ouvre le chat DGX dans le navigateur.
""" """
import logging import logging
@@ -13,6 +16,8 @@ import math
import threading import threading
import time import time
from datetime import datetime from datetime import datetime
from pathlib import Path
from urllib.parse import urlparse
from typing import Any, Callable, Dict, Optional from typing import Any, Callable, Dict, Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -219,7 +224,10 @@ class ChatWindow:
def toggle(self) -> None: def toggle(self) -> None:
"""Afficher/masquer la fenetre de chat.""" """Afficher/masquer la fenetre de chat."""
if self._destroyed or self._root is None: if self._destroyed:
return
if self._root is None:
self._open_browser_fallback()
return return
if self._visible: if self._visible:
self.hide() self.hide()
@@ -228,7 +236,10 @@ class ChatWindow:
def show(self) -> None: def show(self) -> None:
"""Afficher la fenetre.""" """Afficher la fenetre."""
if self._destroyed or self._root is None: if self._destroyed:
return
if self._root is None:
self._open_browser_fallback()
return return
self._root.after(0, self._do_show) self._root.after(0, self._do_show)
@@ -257,6 +268,79 @@ class ChatWindow:
"""Mettre a jour le client serveur (appele si cree apres la fenetre).""" """Mettre a jour le client serveur (appele si cree apres la fenetre)."""
self._server_client = server_client self._server_client = server_client
def _chat_url(self) -> str:
"""Retourne l'URL web du chat, derivee de la config serveur."""
configured_url = self._chat_url_from_server_url(self._configured_server_url())
if self._server_client is not None:
chat_base = getattr(self._server_client, "_chat_base", None)
if chat_base:
chat_base = str(chat_base).rstrip("/")
if not self._is_local_url(chat_base):
return chat_base
if configured_url:
return configured_url
if configured_url:
return configured_url
host = (self._server_host or "localhost").strip()
if host.startswith(("http://", "https://")):
parsed = urlparse(host)
scheme = parsed.scheme or "http"
hostname = parsed.hostname or "localhost"
return f"{scheme}://{hostname}:{self._chat_port}"
return f"http://{host}:{self._chat_port}"
@staticmethod
def _is_local_url(url: str) -> bool:
try:
host = urlparse(url).hostname
except Exception:
return False
return host in {"localhost", "127.0.0.1", "::1"}
def _chat_url_from_server_url(self, server_url: Optional[str]) -> Optional[str]:
if not server_url:
return None
try:
parsed = urlparse(server_url.strip())
except Exception:
return None
if not parsed.hostname or parsed.hostname in {"localhost", "127.0.0.1", "::1"}:
return None
scheme = parsed.scheme or "http"
return f"{scheme}://{parsed.hostname}:{self._chat_port}"
def _configured_server_url(self) -> Optional[str]:
env_url = os.environ.get("RPA_SERVER_URL", "").strip()
if env_url:
return env_url
try:
# Installed layout: <app>/agent_v1/ui/chat_window.py.
for parent in Path(__file__).resolve().parents:
cfg = parent / "config.txt"
if cfg.exists():
for line in cfg.read_text(encoding="utf-8", errors="ignore").splitlines():
if line.startswith("RPA_SERVER_URL="):
return line.split("=", 1)[1].strip()
except Exception:
logger.debug("Lecture config.txt pour chat_url impossible", exc_info=True)
return None
def _open_browser_fallback(self) -> None:
"""Fallback POC quand tkinter est absent du Python embedded."""
url = self._chat_url()
try:
import webbrowser
if webbrowser.open(url, new=1):
logger.info("ChatWindow indisponible, chat ouvert dans le navigateur: %s", url)
else:
logger.warning("ChatWindow indisponible, ouverture navigateur refusee: %s", url)
except Exception as exc:
logger.error("Impossible d'ouvrir le chat dans le navigateur (%s): %s", url, exc)
def _on_shared_state_change(self, state) -> None: def _on_shared_state_change(self, state) -> None:
"""Callback appele quand l'etat partage change (depuis le systray ou ailleurs). """Callback appele quand l'etat partage change (depuis le systray ou ailleurs).

View File

@@ -330,7 +330,8 @@ def import_learned_workflow(workflow_id: str):
# Extraire et sauvegarder le screenshot d'ancre si présent # Extraire et sauvegarder le screenshot d'ancre si présent
anchor_b64 = params.pop("_anchor_image_base64", None) anchor_b64 = params.pop("_anchor_image_base64", None)
params.pop("_anchor_bbox", None) # NE PAS supprimer _anchor_bbox : on le conserve dans params pour que le frontend puisse lire x_pct/y_pct
# et afficher la zone ciblée, au lieu de le jeter et de créer une bbox factice.
if anchor_b64: if anchor_b64:
try: try:
from services.anchor_image_service import ( from services.anchor_image_service import (
@@ -344,6 +345,8 @@ def import_learned_workflow(workflow_id: str):
anchor_b64 = anchor_b64.split(',', 1)[1] anchor_b64 = anchor_b64.split(',', 1)[1]
img_data = b64mod.b64decode(anchor_b64) img_data = b64mod.b64decode(anchor_b64)
img = Image.open(BytesIO(img_data)) img = Image.open(BytesIO(img_data))
# Fallback sécurisé pour le service de crop si _anchor_bbox n'a pas le format attendu,
# mais les données x_pct/y_pct restent intactes dans params pour le frontend.
bbox = { bbox = {
"x": 0, "y": 0, "x": 0, "y": 0,
"width": img.width, "height": img.height "width": img.width, "height": img.height

View File

@@ -2800,7 +2800,10 @@
// Utiliser le proxy du dashboard pour éviter les problèmes CORS // Utiliser le proxy du dashboard pour éviter les problèmes CORS
const STREAMING_BASE = '/api/streaming'; const STREAMING_BASE = '/api/streaming';
const VWB_IMPORT_URL = 'http://localhost:5002/api/workflows/import-core'; // Construire VWB_IMPORT_URL dynamiquement à partir de l'origine actuelle (ex: http://192.168.1.45:5001 -> http://192.168.1.45:5002)
// pour éviter le hardcoded localhost et permettre les tests depuis la VM/poste via l'IP du banc.
const VWB_BASE = window.location.origin.replace(/:\d+$/, ':5002');
const VWB_IMPORT_URL = `${VWB_BASE}/api/workflows/import-core`;
async function refreshStreaming() { async function refreshStreaming() {
await Promise.all([ await Promise.all([