feat: multi-machine + chat Léa Edge mode app

Multi-machine :
- machine_id auto (hostname_os), configurable via RPA_MACHINE_ID
- Sessions/workflows isolés par machine (dossiers séparés)
- Replay ciblé par machine (pas de fuite cross-machine)
- Endpoint GET /machines pour lister les machines connectées
- Léa affiche la machine source des workflows

Chat Léa systray :
- Edge en mode app (--app=URL) — fenêtre native sans barre d'adresse
- Toggle via menu systray "Discuter avec Léa"
- Fallback navigateur si Edge absent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-17 20:02:45 +01:00
parent 371db69543
commit 8175b39eba

View File

@@ -110,6 +110,25 @@ execution_status = {
}
command_history: List[Dict[str, Any]] = []
def _fetch_connected_machines() -> List[Dict[str, Any]]:
"""Récupérer la liste des machines connectées depuis le streaming server.
Appel non-bloquant au endpoint /api/v1/traces/stream/machines.
Retourne une liste vide si le serveur est injoignable.
"""
try:
resp = http_requests.get(
f"{STREAMING_SERVER_URL}/api/v1/traces/stream/machines",
timeout=3,
)
if resp.ok:
return resp.json().get("machines", [])
except Exception:
pass
return []
# Répertoire d'upload et chemin de la base de données
PROJECT_ROOT = Path(__file__).parent.parent
UPLOAD_DIR = PROJECT_ROOT / "data" / "uploads"
@@ -350,23 +369,36 @@ def api_status():
@app.route('/api/workflows')
def api_workflows():
"""Liste des workflows (tous répertoires confondus)."""
"""Liste des workflows (tous répertoires confondus).
Enrichit les workflows avec le machine_id source quand disponible
(attribut _machine_id ajouté par le StreamProcessor).
"""
if not matcher:
return jsonify({"workflows": [], "directories": []})
workflows = []
for wf in matcher.get_all_workflows():
workflows.append({
wf_data = {
"id": wf.workflow_id,
"name": wf.name,
"description": wf.description,
"tags": wf.tags,
"source": wf.source_dir,
})
}
# Ajouter le machine_id source si disponible (workflows appris en streaming)
machine_id = getattr(wf, '_machine_id', None)
if machine_id:
wf_data["machine_id"] = machine_id
workflows.append(wf_data)
# Récupérer la liste des machines connectées depuis le streaming server
machines = _fetch_connected_machines()
return jsonify({
"workflows": workflows,
"directories": matcher.get_directories(),
"machines": machines,
})
@@ -401,6 +433,17 @@ def api_workflows_refresh():
return jsonify({"success": False, "error": str(e)})
@app.route('/api/machines')
def api_machines():
"""Liste des machines connectées au streaming server.
Proxy vers le streaming server pour que le frontend puisse
lister les machines et cibler un replay spécifique.
"""
machines = _fetch_connected_machines()
return jsonify({"machines": machines})
@app.route('/api/search', methods=['POST'])
def api_search():
"""Rechercher des workflows."""
@@ -1377,12 +1420,21 @@ def handle_copilot_abort():
# Exécution de workflow
# =============================================================================
def _try_streaming_server_replay(workflow_id: str, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def _try_streaming_server_replay(
workflow_id: str,
params: Dict[str, Any],
machine_id: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
"""
Tenter d'exécuter un workflow via le streaming server (Agent V1).
POST http://localhost:5005/api/v1/traces/stream/replay
avec workflow_id et params.
avec workflow_id, params et optionnellement machine_id (multi-machine).
Args:
workflow_id: Identifiant du workflow à exécuter
params: Paramètres du workflow (variables)
machine_id: Machine cible pour le replay (None = auto-détection)
Returns:
dict avec le résultat si succès.
@@ -1390,18 +1442,26 @@ def _try_streaming_server_replay(workflow_id: str, params: Dict[str, Any]) -> Op
None si le serveur est injoignable (connexion refusée, timeout).
"""
try:
payload = {
"workflow_id": workflow_id,
"session_id": "", # Vide = auto-détection de la session Agent V1 active
"params": params or {},
}
# Ajouter le machine_id si spécifié (ciblage multi-machine)
if machine_id:
payload["machine_id"] = machine_id
resp = http_requests.post(
f"{STREAMING_SERVER_URL}/api/v1/traces/stream/replay",
json={
"workflow_id": workflow_id,
"session_id": "", # Vide = auto-détection de la session Agent V1 active
"params": params or {},
},
json=payload,
timeout=15,
)
if resp.status_code == 200:
data = resp.json()
logger.info(f"Workflow {workflow_id} envoyé au streaming server: {data}")
logger.info(
f"Workflow {workflow_id} envoyé au streaming server: {data}"
+ (f" (machine={machine_id})" if machine_id else "")
)
return data
else:
# Le serveur est UP mais refuse — renvoyer l'erreur pour éviter le fallback local