fix(qw4): câblage polling frontend → streaming pour PauseDialog
Avant ce fix, le frontend VWB ne savait pas qu'un replay Agent V1 (Windows) était en pause supervisée : le seul polling (App.tsx) interrogeait /execute/status (exécution locale Linux) et n'avait jamais l'info safety_checks / pause_message du replay distant. Côté backend (dag_execute.py) : - ajout du proxy GET /api/v3/replay/state/<replay_id> qui forward vers /api/v1/traces/stream/replay/<id> avec Bearer token. Côté frontend : - ExecutionControls : nouvelle prop onWindowsReplayStarted, appelée avec le replay_id retourné par /api/v3/execute-windows. - App.tsx : nouveau state streamingReplayId + useEffect qui poll /api/v3/replay/state/<id> toutes les secondes et fusionne status, pause_message, pause_reason, safety_checks dans appState.execution. Le PauseDialog existant s'affiche donc automatiquement quand status = paused_need_help. Le polling s'arrête quand le replay est completed/error/cancelled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1155,3 +1155,34 @@ def replay_resume_proxy():
|
||||
'detail': f'Streaming server non disponible ({streaming_url})'}), 502
|
||||
except req.RequestException as e:
|
||||
return jsonify({'error': 'streaming_unreachable', 'detail': str(e)}), 502
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# QW4 — Proxy GET /api/v3/replay/state/<replay_id> → streaming /replay/{id}
|
||||
# Forward Bearer token vers le serveur streaming.
|
||||
# Permet à App.tsx de récupérer le state du replay actif (Agent V1 Windows)
|
||||
# pour afficher PauseDialog quand status = paused_need_help avec safety_checks.
|
||||
# ---------------------------------------------------------------------------
|
||||
@api_v3_bp.route('/replay/state/<replay_id>', methods=['GET'])
|
||||
def replay_state_proxy(replay_id):
|
||||
"""Proxy QW4 vers le serveur streaming pour récupérer le state replay actif."""
|
||||
import requests as req
|
||||
|
||||
streaming_url = os.environ.get('RPA_STREAMING_URL', 'http://localhost:5005')
|
||||
token = os.environ.get('RPA_API_TOKEN', '')
|
||||
headers = {}
|
||||
if token:
|
||||
headers['Authorization'] = f'Bearer {token}'
|
||||
|
||||
try:
|
||||
resp = req.get(
|
||||
f'{streaming_url}/api/v1/traces/stream/replay/{replay_id}',
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
return resp.content, resp.status_code, {'Content-Type': 'application/json'}
|
||||
except req.ConnectionError:
|
||||
return jsonify({'error': 'streaming_unreachable',
|
||||
'detail': f'Streaming server non disponible ({streaming_url})'}), 502
|
||||
except req.RequestException as e:
|
||||
return jsonify({'error': 'streaming_unreachable', 'detail': str(e)}), 502
|
||||
|
||||
@@ -62,6 +62,13 @@ function App() {
|
||||
const [healingCandidates, setHealingCandidates] = useState<any[]>([]);
|
||||
const [healingStepInfo, setHealingStepInfo] = useState<any>(null);
|
||||
|
||||
// QW4 — Replay streaming Windows en cours (Agent V1 distant).
|
||||
// Quand un replay distant est lancé via ExecutionControls "→ Windows",
|
||||
// ExecutionControls appelle setStreamingReplayId(replay_id) et un useEffect
|
||||
// poll /api/v3/replay/state/<id> pour fusionner safety_checks + pause_*
|
||||
// dans appState.execution → PauseDialog s'affiche.
|
||||
const [streamingReplayId, setStreamingReplayId] = useState<string | null>(null);
|
||||
|
||||
// Charger l'état initial
|
||||
const loadState = useCallback(async () => {
|
||||
try {
|
||||
@@ -123,6 +130,62 @@ function App() {
|
||||
return () => clearInterval(interval);
|
||||
}, [isExecutionRunning, loadState]);
|
||||
|
||||
// QW4 — Polling state replay streaming (Agent V1 Windows distant)
|
||||
// Tourne dès qu'un replay distant a été lancé. Récupère safety_checks,
|
||||
// pause_message, pause_reason et les fusionne dans appState.execution
|
||||
// pour que PauseDialog s'affiche quand status = paused_need_help.
|
||||
useEffect(() => {
|
||||
if (!streamingReplayId) return;
|
||||
|
||||
let stopped = false;
|
||||
const pollReplay = async () => {
|
||||
try {
|
||||
const resp = await fetch(`/api/v3/replay/state/${streamingReplayId}`);
|
||||
if (!resp.ok) return;
|
||||
const state = await resp.json();
|
||||
if (stopped) return;
|
||||
|
||||
// Fusionner dans appState.execution sans écraser le reste.
|
||||
setAppState(prev => {
|
||||
if (!prev) return prev;
|
||||
const prevExec = prev.execution || {
|
||||
id: streamingReplayId,
|
||||
workflow_id: prev.session?.active_workflow_id || '',
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
current_step_index: 0,
|
||||
completed_steps: 0,
|
||||
failed_steps: 0,
|
||||
total_steps: 0,
|
||||
};
|
||||
return {
|
||||
...prev,
|
||||
execution: {
|
||||
...prevExec,
|
||||
status: state.status || prevExec.status,
|
||||
pause_message: state.pause_message || state.message,
|
||||
pause_reason: state.pause_reason,
|
||||
safety_checks: state.safety_checks || [],
|
||||
replay_id: streamingReplayId,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Stopper le polling si le replay est terminé / annulé.
|
||||
if (state.status && ['completed', 'error', 'cancelled'].includes(state.status)) {
|
||||
setStreamingReplayId(null);
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore (le serveur streaming peut être momentanément indispo)
|
||||
}
|
||||
};
|
||||
|
||||
// Tick immédiat puis toutes les 1s.
|
||||
pollReplay();
|
||||
const interval = setInterval(pollReplay, 1000);
|
||||
return () => { stopped = true; clearInterval(interval); };
|
||||
}, [streamingReplayId]);
|
||||
|
||||
// Convertir les étapes en nœuds React Flow
|
||||
// Les edges ne sont générées automatiquement que lors du premier chargement
|
||||
// d'un workflow. Ensuite, les connexions manuelles de l'utilisateur sont préservées.
|
||||
@@ -452,6 +515,7 @@ function App() {
|
||||
execution={appState?.execution || null}
|
||||
onStart={handleStartExecution}
|
||||
onStop={handleStopExecution}
|
||||
onWindowsReplayStarted={(replayId) => setStreamingReplayId(replayId)}
|
||||
/>
|
||||
<ConfidenceDashboard
|
||||
isExecutionRunning={isExecutionRunning}
|
||||
|
||||
@@ -9,9 +9,12 @@ interface Props {
|
||||
execution: Execution | null;
|
||||
onStart: () => void;
|
||||
onStop: () => void;
|
||||
// QW4 — Notifie App.tsx quand un replay streaming Windows est lancé,
|
||||
// pour qu'il poll /api/v3/replay/state/<id> et affiche PauseDialog au besoin.
|
||||
onWindowsReplayStarted?: (replayId: string) => void;
|
||||
}
|
||||
|
||||
export default function ExecutionControls({ execution, onStart, onStop }: Props) {
|
||||
export default function ExecutionControls({ execution, onStart, onStop, onWindowsReplayStarted }: Props) {
|
||||
const isRunning = execution?.status === 'running' || execution?.status === 'paused';
|
||||
const [windowsStatus, setWindowsStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle');
|
||||
|
||||
@@ -56,6 +59,11 @@ export default function ExecutionControls({ execution, onStart, onStop }: Props)
|
||||
const result = await resp.json();
|
||||
if (result.replay_id) {
|
||||
setWindowsStatus('sent');
|
||||
// QW4 — propage le replay_id à App.tsx pour activer le polling
|
||||
// /api/v3/replay/state/<id> (PauseDialog si paused_need_help).
|
||||
if (onWindowsReplayStarted) {
|
||||
try { onWindowsReplayStarted(result.replay_id); } catch {}
|
||||
}
|
||||
alert('Replay lancé ! Réduisez cette fenêtre maintenant.\nLes actions commenceront dans 5 secondes.');
|
||||
setTimeout(() => setWindowsStatus('idle'), 5000);
|
||||
} else {
|
||||
@@ -75,9 +83,27 @@ export default function ExecutionControls({ execution, onStart, onStop }: Props)
|
||||
{!isRunning ? (
|
||||
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
||||
{userOS === 'linux' ? (
|
||||
<button className="btn-start" onClick={onStart} title="Exécuter sur cet écran">
|
||||
Exécuter
|
||||
</button>
|
||||
<>
|
||||
<button className="btn-start" onClick={onStart} title="Exécuter sur cet écran (Linux local)">
|
||||
Exécuter
|
||||
</button>
|
||||
<button
|
||||
className="btn-start"
|
||||
onClick={handleExecuteWindows}
|
||||
disabled={windowsStatus === 'sending'}
|
||||
style={{
|
||||
background: windowsStatus === 'sent' ? '#22c55e' : windowsStatus === 'error' ? '#ef4444' : '#0078d4',
|
||||
fontSize: '12px',
|
||||
opacity: windowsStatus === 'sending' ? 0.6 : 1,
|
||||
}}
|
||||
title="Exécuter sur le PC Windows (Agent V1)"
|
||||
>
|
||||
{windowsStatus === 'sending' ? 'Envoi...' :
|
||||
windowsStatus === 'sent' ? 'Lancé !' :
|
||||
windowsStatus === 'error' ? 'Erreur' :
|
||||
'→ Windows'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className="btn-start"
|
||||
|
||||
Reference in New Issue
Block a user