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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user