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.
Design professionnel, theme clair, ancree en bas a droite de l'ecran.
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
@@ -13,6 +16,8 @@ import math
import threading
import time
from datetime import datetime
from pathlib import Path
from urllib.parse import urlparse
from typing import Any, Callable, Dict, Optional
logger = logging.getLogger(__name__)
@@ -219,7 +224,10 @@ class ChatWindow:
def toggle(self) -> None:
"""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
if self._visible:
self.hide()
@@ -228,7 +236,10 @@ class ChatWindow:
def show(self) -> None:
"""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
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)."""
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:
"""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
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:
try:
from services.anchor_image_service import (
@@ -344,6 +345,8 @@ def import_learned_workflow(workflow_id: str):
anchor_b64 = anchor_b64.split(',', 1)[1]
img_data = b64mod.b64decode(anchor_b64)
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 = {
"x": 0, "y": 0,
"width": img.width, "height": img.height

View File

@@ -2800,7 +2800,10 @@
// Utiliser le proxy du dashboard pour éviter les problèmes CORS
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() {
await Promise.all([