feat(vwb-v3): Architecture Thin Client fonctionnelle

API = Source de vérité unique (SQLite + Flask)
- Backend: API v3 avec session, workflow, capture, execute
- Frontend: Vanilla TypeScript, pas de state local
- Contrats stricts pour les actions RPA
- Drag & drop pour réorganiser les étapes
- Insertion d'étapes entre deux existantes
- Bibliothèque de captures (sessionStorage)
- Exécution avec coordonnées statiques (pyautogui)

Fonctionne mais fragile (coordonnées fixes, pas de détection visuelle)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dom
2026-01-23 12:07:13 +01:00
parent a9a53991bc
commit 858e6007f9
23 changed files with 6813 additions and 6 deletions

View File

@@ -9,7 +9,6 @@ for real-time execution updates.
from flask import Flask
from flask_cors import CORS
from flask_socketio import SocketIO
from flask_sqlalchemy import SQLAlchemy
from flask_caching import Cache
import os
from dotenv import load_dotenv
@@ -22,14 +21,15 @@ app = Flask(__name__)
# Configuration
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///workflows.db')
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///vwb_v3.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 # 10MB max upload
app.config['CACHE_TYPE'] = 'redis' if os.getenv('REDIS_URL') else 'simple'
app.config['CACHE_REDIS_URL'] = os.getenv('REDIS_URL', 'redis://localhost:6379/0')
# Initialize extensions
db = SQLAlchemy(app)
# Initialize extensions - Use db from v3 models (source of truth)
from db.models import db
db.init_app(app)
cache = Cache(app)
socketio = SocketIO(
app,
@@ -126,6 +126,40 @@ try:
except ImportError as e:
print(f"⚠️ Blueprint coaching_sessions désactivé: {e}")
# Catalogue VWB - actions VisionOnly
# V2 avec VLM (Vision Language Model) pour détection intelligente
try:
from catalog_routes_v2_vlm import catalog_bp
app.register_blueprint(catalog_bp)
print("✅ Blueprint catalog V2 VLM (Ollama qwen2.5vl) enregistré")
except ImportError as e:
print(f"⚠️ Blueprint catalog V2 VLM désactivé: {e}")
# Fallback sur la version pyautogui
try:
from catalog_routes import catalog_bp
app.register_blueprint(catalog_bp)
print("✅ Blueprint catalog (fallback pyautogui) enregistré")
except ImportError as e2:
print(f"⚠️ Blueprint catalog désactivé: {e2}")
# API Images Ancres Visuelles - stockage serveur
try:
from api.anchor_images import anchor_images_bp
app.register_blueprint(anchor_images_bp)
print("✅ Blueprint anchor_images enregistré")
except ImportError as e:
print(f"⚠️ Blueprint anchor_images désactivé: {e}")
# ============================================================
# API V3 - Thin Client Architecture (Source de Vérité Unique)
# ============================================================
try:
from api_v3 import api_v3_bp
app.register_blueprint(api_v3_bp)
print("✅ Blueprint API v3 (Thin Client) enregistré - /api/v3/*")
except ImportError as e:
print(f"⚠️ Blueprint API v3 désactivé: {e}")
# Import WebSocket handlers (optional)
try:
@@ -158,9 +192,92 @@ def handle_exception(error):
# Health check endpoint
@app.route('/health')
@app.route('/api/health')
def health_check():
"""Health check endpoint for monitoring"""
return {'status': 'healthy', 'version': '1.0.0'}
from flask import jsonify
return jsonify({'status': 'healthy', 'version': '1.0.0'})
# Workflow execution endpoint (proxy to catalog execute)
@app.route('/api/workflow/execute-step', methods=['POST'])
def execute_workflow_step():
"""Execute a workflow step via the catalog execute endpoint"""
from flask import jsonify, request
import requests
try:
data = request.get_json() or {}
step_id = data.get('stepId', f'step_{int(__import__("time").time() * 1000)}')
step_type = data.get('stepType', 'click_anchor')
parameters = data.get('parameters', {})
# DEBUG: Écrire les données reçues dans un fichier
import json as json_module
with open('/tmp/vwb_debug.log', 'a') as debug_file:
debug_file.write(f"\n{'='*60}\n")
debug_file.write(f"[execute-step] stepType={step_type}, stepId={step_id}\n")
debug_file.write(f"[execute-step] parameters keys: {list(parameters.keys())}\n")
if 'visual_anchor' in parameters:
va = parameters['visual_anchor']
debug_file.write(f"[execute-step] visual_anchor keys: {list(va.keys()) if va else 'None'}\n")
debug_file.write(f"[execute-step] visual_anchor.id: {va.get('id')}\n")
debug_file.write(f"[execute-step] visual_anchor.thumbnail_url: {va.get('thumbnail_url') or (va.get('metadata', {}) or {}).get('thumbnail_url')}\n")
debug_file.write(f"[execute-step] FULL visual_anchor: {json_module.dumps(va, default=str)[:500]}\n")
debug_file.flush()
# Convert to catalog execute format
catalog_request = {
'type': step_type,
'step_id': step_id,
'parameters': parameters
}
# Call the internal catalog execute endpoint
from catalog_routes import catalog_bp
# Direct execution via catalog
try:
# Import the execute function directly
from catalog_routes import execute_action as catalog_execute
# We need to simulate Flask request context - use internal call
from flask import current_app
with current_app.test_request_context(
'/api/vwb/catalog/execute',
method='POST',
data=__import__('json').dumps(catalog_request),
content_type='application/json'
):
response = catalog_execute()
if hasattr(response, 'get_json'):
result = response.get_json()
else:
result = __import__('json').loads(response[0].get_data(as_text=True))
# Convert to expected format
if result.get('success') and result.get('result'):
return jsonify({
'success': result['result'].get('status') == 'success',
'output': result['result'].get('output_data', {}),
'error': result['result'].get('error', {}).get('message') if result['result'].get('error') else None
})
else:
return jsonify({
'success': False,
'error': result.get('error', 'Échec de l\'exécution')
})
except Exception as inner_e:
print(f"❌ Erreur exécution interne: {inner_e}")
return jsonify({
'success': False,
'error': str(inner_e)
})
except Exception as e:
print(f"❌ Erreur execute-step: {e}")
return jsonify({
'success': False,
'error': str(e)
}), 500
# Create database tables
with app.app_context():
@@ -195,7 +312,7 @@ except Exception as e:
print(f"❌ Erreur lors de l'initialisation des services visuels: {e}")
if __name__ == '__main__':
port = int(os.getenv('PORT', 5002))
port = int(os.getenv('PORT', 5000))
debug = os.getenv('FLASK_ENV') == 'development'
socketio.run(