Compare commits

..

3 Commits

23 changed files with 59208 additions and 36959 deletions

2
.gitignore vendored
View File

@ -6,6 +6,8 @@ __pycache__/
# C extensions
*.so
backend/script_groups/TwinCat/
# Distribution / packaging
.Python
temp/

242
app.py
View File

@ -3,6 +3,7 @@ from flask_sock import Sock
from lib.config_manager import ConfigurationManager
from lib.launcher_manager import LauncherManager
from lib.csharp_launcher_manager import CSharpLauncherManager
from lib.python_launcher_manager import PythonLauncherManager
import os
import json # Added import
from datetime import datetime
@ -31,6 +32,9 @@ launcher_manager = LauncherManager(config_manager.data_path)
# Inicializar C# launcher manager
csharp_launcher_manager = CSharpLauncherManager(config_manager.data_path)
# Inicializar Python launcher manager
python_launcher_manager = PythonLauncherManager(config_manager.data_path)
# Lista global para mantener las conexiones WebSocket activas
websocket_connections = set()
@ -991,6 +995,199 @@ def get_all_csharp_executables(project_id):
# === FIN C# LAUNCHER APIs ===
# === PYTHON LAUNCHER APIs ===
@app.route("/api/python-projects", methods=["GET", "POST"])
def handle_python_projects():
"""Gestionar proyectos Python (GET: obtener, POST: crear)"""
if request.method == "GET":
try:
projects = python_launcher_manager.get_python_projects()
return jsonify(projects)
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # POST
try:
data = request.json
result = python_launcher_manager.add_python_project(data)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-projects/<project_id>", methods=["GET", "PUT", "DELETE"])
def handle_python_project(project_id):
"""Gestionar proyecto Python específico (GET: obtener, PUT: actualizar, DELETE: eliminar)"""
if request.method == "GET":
try:
project = python_launcher_manager.get_python_project(project_id)
if not project:
return jsonify({"error": "Project not found"}), 404
return jsonify(project)
except Exception as e:
return jsonify({"error": str(e)}), 500
elif request.method == "PUT":
try:
data = request.json
result = python_launcher_manager.update_python_project(project_id, data)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # DELETE
try:
result = python_launcher_manager.delete_python_project(project_id)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-scripts/<project_id>")
def get_python_scripts(project_id):
"""Obtener scripts de un proyecto Python"""
try:
scripts = python_launcher_manager.get_project_scripts(project_id)
return jsonify(scripts)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-scripts-all/<project_id>")
def get_all_python_scripts(project_id):
"""Obtener TODOS los scripts de un proyecto Python (incluyendo ocultos) para gestión"""
try:
scripts = python_launcher_manager.get_all_project_scripts(project_id)
return jsonify(scripts)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-script-metadata/<project_id>/<script_name>", methods=["GET", "POST"])
def handle_python_script_metadata(project_id, script_name):
"""Gestionar metadatos de un script Python específico"""
if request.method == "GET":
try:
metadata = python_launcher_manager.get_script_metadata(project_id, script_name)
return jsonify(metadata)
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # POST
try:
data = request.json
result = python_launcher_manager.update_script_metadata(project_id, script_name, data)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/execute-python-script", methods=["POST"])
def execute_python_script():
"""Ejecutar script Python con argumentos opcionales"""
try:
data = request.json
project_id = data["project_id"]
script_name = data["script_name"]
script_args = data.get("args", [])
working_dir = data.get("working_dir", None)
run_in_background = data.get("run_in_background", False) # Para servidores MCP, Flask, etc.
result = python_launcher_manager.execute_python_script(
project_id, script_name, script_args, broadcast_message, working_dir, run_in_background
)
return jsonify(result)
except Exception as e:
error_msg = f"Error ejecutando script Python: {str(e)}"
broadcast_message(error_msg)
return jsonify({"error": error_msg}), 500
@app.route("/api/python-favorites", methods=["GET", "POST"])
def handle_python_favorites():
"""Gestionar favoritos del launcher Python"""
if request.method == "GET":
try:
favorites = python_launcher_manager.get_favorites()
return jsonify({"favorites": favorites})
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # POST
try:
data = request.json
project_id = data["project_id"]
script_name = data["script_name"]
result = python_launcher_manager.toggle_favorite(project_id, script_name)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-history", methods=["GET", "DELETE"])
def handle_python_history():
"""Gestionar historial del launcher Python"""
if request.method == "GET":
try:
history = python_launcher_manager.get_history()
return jsonify({"history": history})
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # DELETE
try:
result = python_launcher_manager.clear_history()
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-categories")
def get_python_categories():
"""Obtener categorías disponibles del launcher Python"""
try:
categories = python_launcher_manager.get_categories()
return jsonify(categories)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-running-processes")
def get_python_running_processes():
"""Obtener procesos Python en ejecución"""
try:
processes = python_launcher_manager.get_running_processes()
return jsonify({"processes": processes})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-process-terminate/<int:pid>", methods=["POST"])
def terminate_python_process(pid):
"""Cerrar un proceso Python"""
try:
result = python_launcher_manager.terminate_process(pid)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-process-focus/<int:pid>", methods=["POST"])
def focus_python_process(pid):
"""Activar foco de un proceso Python"""
try:
result = python_launcher_manager.focus_process(pid)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-markdown/<project_id>")
def get_python_markdown_files(project_id):
"""Obtener archivos Markdown de un proyecto Python"""
try:
markdown_files = python_launcher_manager.get_markdown_files(project_id)
return jsonify({"files": markdown_files})
except Exception as e:
print(f"Error getting markdown files for Python project {project_id}: {e}")
# Devolver lista vacía en lugar de error para no interferir con scripts
return jsonify({"files": []})
@app.route("/api/python-markdown-content/<project_id>/<path:relative_path>")
def get_python_markdown_content(project_id, relative_path):
"""Obtener contenido de un archivo Markdown de un proyecto Python"""
try:
result = python_launcher_manager.read_markdown_file(project_id, relative_path)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
# === FIN PYTHON LAUNCHER APIs ===
# --- Helper function to find VS Code ---
def find_vscode_executable():
"""Intenta encontrar el ejecutable de VS Code en ubicaciones comunes y en el PATH."""
@ -1058,10 +1255,23 @@ def open_group_in_editor(editor, group_system, group_id):
"status": "error",
"message": f"Directorio del proyecto C# '{project['name']}' no encontrado"
}), 404
elif group_system == 'python':
project = python_launcher_manager.get_python_project(group_id)
if not project:
return jsonify({
"status": "error",
"message": f"Proyecto Python '{group_id}' no encontrado"
}), 404
script_group_path = project["directory"]
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del proyecto Python '{project['name']}' no encontrado"
}), 404
else:
return jsonify({
"status": "error",
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher' o 'csharp'"
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher', 'csharp' o 'python'"
}), 400
# Definir rutas de ejecutables
@ -1176,10 +1386,23 @@ def open_group_folder(group_system, group_id):
"status": "error",
"message": f"Directorio del proyecto C# '{project['name']}' no encontrado"
}), 404
elif group_system == 'python':
project = python_launcher_manager.get_python_project(group_id)
if not project:
return jsonify({
"status": "error",
"message": f"Proyecto Python '{group_id}' no encontrado"
}), 404
script_group_path = project["directory"]
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del proyecto Python '{project['name']}' no encontrado"
}), 404
else:
return jsonify({
"status": "error",
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher' o 'csharp'"
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher', 'csharp' o 'python'"
}), 400
# Abrir en el explorador según el sistema operativo
@ -1247,10 +1470,23 @@ def get_group_path(group_system, group_id):
"status": "error",
"message": f"Directorio del proyecto C# '{project['name']}' no encontrado"
}), 404
elif group_system == 'python':
project = python_launcher_manager.get_python_project(group_id)
if not project:
return jsonify({
"status": "error",
"message": f"Proyecto Python '{group_id}' no encontrado"
}), 404
script_group_path = project["directory"]
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del proyecto Python '{project['name']}' no encontrado"
}), 404
else:
return jsonify({
"status": "error",
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher' o 'csharp'"
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher', 'csharp' o 'python'"
}), 400
return jsonify({

View File

@ -0,0 +1,11 @@
{
"folders": [
{
"path": "."
},
{
"path": "C:/Trabajo/SIDEL/13 - E5.007560 - Modifica O&U - SAE235/Reporte/Analisis"
}
],
"settings": {}
}

View File

@ -0,0 +1,463 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para generar documentación de adaptación de IOs
entre TwinCAT y TIA Portal - Proyecto SIDEL
Autor: Generado automáticamente
Proyecto: E5.007560 - Modifica O&U - SAE235
"""
import re
import os
import sys
import pandas as pd
import json
from pathlib import Path
from typing import Dict, List, Tuple, Optional
import argparse
from collections import defaultdict
# Configurar el path al directorio raíz del proyecto
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
# Importar la función de configuración
from backend.script_utils import load_configuration
def load_tiaportal_adaptations(working_directory, file_path='IO Adapted.md'):
"""Carga las adaptaciones de TIA Portal desde el archivo markdown"""
full_file_path = os.path.join(working_directory, file_path)
print(f"Cargando adaptaciones de TIA Portal desde: {full_file_path}")
adaptations = {}
if not os.path.exists(full_file_path):
print(f"⚠️ Archivo {full_file_path} no encontrado")
return adaptations
with open(full_file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Patrones mejorados para diferentes tipos de IOs
patterns = [
# Digitales: E0.0, A0.0
r'\|\s*([EA]\d+\.\d+)\s*\|\s*([^|]+?)\s*\|',
# Analógicos: PEW100, PAW100
r'\|\s*(P[EA]W\d+)\s*\|\s*([^|]+?)\s*\|',
# Profibus: EW 1640, AW 1640
r'\|\s*([EA]W\s+\d+)\s*\|\s*([^|]+?)\s*\|'
]
for pattern in patterns:
matches = re.findall(pattern, content, re.MULTILINE)
for io_addr, master_tag in matches:
io_addr = io_addr.strip()
master_tag = master_tag.strip()
if io_addr and master_tag and not master_tag.startswith('-'):
adaptations[io_addr] = master_tag
print(f" 📍 {io_addr}{master_tag}")
print(f"✅ Cargadas {len(adaptations)} adaptaciones de TIA Portal")
return adaptations
def scan_twincat_definitions(working_directory, directory='TwinCat'):
"""Escanea archivos TwinCAT para encontrar definiciones de variables AT %"""
full_directory = os.path.join(working_directory, directory)
print(f"\n🔍 Escaneando definiciones TwinCAT en: {full_directory}")
definitions = {}
if not os.path.exists(full_directory):
print(f"⚠️ Directorio {full_directory} no encontrado")
return definitions
# Patrones para definiciones AT %
definition_patterns = [
r'(\w+)\s+AT\s+%([IQ][XWB]\d+(?:\.\d+)?)\s*:\s*(\w+);', # Activas
r'(\w+)\s+\(\*\s*AT\s+%([IQ][XWB]\d+(?:\.\d+)?)\s*\*\)\s*:\s*(\w+);', # Comentadas
]
for file_path in Path(full_directory).glob('*.scl'):
print(f" 📄 Procesando: {file_path.name}")
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
for pattern in definition_patterns:
matches = re.findall(pattern, content, re.MULTILINE | re.IGNORECASE)
for var_name, io_addr, data_type in matches:
var_name = var_name.strip()
io_addr = io_addr.strip()
data_type = data_type.strip()
definitions[var_name] = {
'address': io_addr,
'type': data_type,
'file': file_path.name,
'definition_line': content[:content.find(var_name)].count('\n') + 1
}
print(f" 🔗 {var_name} AT %{io_addr} : {data_type}")
print(f"✅ Encontradas {len(definitions)} definiciones TwinCAT")
return definitions
def scan_twincat_usage(working_directory, directory='TwinCat'):
"""Escanea archivos TwinCAT para encontrar uso de variables"""
full_directory = os.path.join(working_directory, directory)
print(f"\n🔍 Escaneando uso de variables TwinCAT en: {full_directory}")
usage_data = defaultdict(list)
if not os.path.exists(full_directory):
print(f"⚠️ Directorio {full_directory} no encontrado")
return usage_data
for file_path in Path(full_directory).glob('*.scl'):
print(f" 📄 Analizando uso en: {file_path.name}")
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
lines = f.readlines()
for line_num, line in enumerate(lines, 1):
# Buscar variables que empiecen con DI_, DO_, AI_, AO_
var_matches = re.findall(r'\b([DA][IO]_\w+)\b', line)
for var_name in var_matches:
usage_data[var_name].append({
'file': file_path.name,
'line': line_num,
'context': line.strip()[:100] + ('...' if len(line.strip()) > 100 else '')
})
print(f"✅ Encontrado uso de {len(usage_data)} variables diferentes")
return usage_data
def convert_tia_to_twincat(tia_addr):
"""Convierte direcciones TIA Portal a formato TwinCAT"""
conversions = []
# Digitales
if re.match(r'^E\d+\.\d+$', tia_addr): # E0.0 → IX0.0
twincat_addr = tia_addr.replace('E', 'IX')
conversions.append(twincat_addr)
elif re.match(r'^A\d+\.\d+$', tia_addr): # A0.0 → QX0.0
twincat_addr = tia_addr.replace('A', 'QX')
conversions.append(twincat_addr)
# Analógicos
elif re.match(r'^PEW\d+$', tia_addr): # PEW100 → IW100
twincat_addr = tia_addr.replace('PEW', 'IW')
conversions.append(twincat_addr)
elif re.match(r'^PAW\d+$', tia_addr): # PAW100 → QW100
twincat_addr = tia_addr.replace('PAW', 'QW')
conversions.append(twincat_addr)
# Profibus
elif re.match(r'^EW\s+\d+$', tia_addr): # EW 1234 → IB1234
addr_num = re.search(r'\d+', tia_addr).group()
conversions.append(f'IB{addr_num}')
elif re.match(r'^AW\s+\d+$', tia_addr): # AW 1234 → QB1234
addr_num = re.search(r'\d+', tia_addr).group()
conversions.append(f'QB{addr_num}')
return conversions
def find_variable_by_address(definitions, target_address):
"""Busca variable por dirección exacta"""
for var_name, info in definitions.items():
if info['address'] == target_address:
return var_name, info
return None, None
def find_variable_by_name_similarity(definitions, usage_data, master_tag):
"""Busca variables por similitud de nombre"""
candidates = []
# Limpiar el master tag para comparación
clean_master = re.sub(r'^[DA][IO]_', '', master_tag).lower()
# Buscar en definiciones
for var_name, info in definitions.items():
clean_var = re.sub(r'^[DA][IO]_', '', var_name).lower()
if clean_master in clean_var or clean_var in clean_master:
candidates.append((var_name, info, 'definition'))
# Buscar en uso
for var_name in usage_data.keys():
clean_var = re.sub(r'^[DA][IO]_', '', var_name).lower()
if clean_master in clean_var or clean_var in clean_master:
# Intentar encontrar la definición de esta variable
var_info = definitions.get(var_name)
if not var_info:
var_info = {'address': 'Unknown', 'type': 'Unknown', 'file': 'Not found'}
candidates.append((var_name, var_info, 'usage'))
return candidates
def analyze_adaptations(tia_adaptations, twincat_definitions, twincat_usage):
"""Analiza las correlaciones entre TIA Portal y TwinCAT"""
print(f"\n📊 Analizando correlaciones...")
results = []
matches_found = 0
for tia_addr, master_tag in tia_adaptations.items():
result = {
'tia_address': tia_addr,
'master_tag': master_tag,
'twincat_variable': None,
'twincat_address': None,
'twincat_type': None,
'match_type': None,
'definition_file': None,
'usage_files': [],
'usage_count': 0,
'confidence': 'Low'
}
# 1. Buscar por conversión directa de dirección
twincat_addresses = convert_tia_to_twincat(tia_addr)
var_found = False
for twincat_addr in twincat_addresses:
var_name, var_info = find_variable_by_address(twincat_definitions, twincat_addr)
if var_name:
result.update({
'twincat_variable': var_name,
'twincat_address': var_info['address'],
'twincat_type': var_info['type'],
'match_type': 'Address Match',
'definition_file': var_info['file'],
'confidence': 'High'
})
var_found = True
matches_found += 1
break
# 2. Si no se encontró por dirección, buscar por nombre
if not var_found:
candidates = find_variable_by_name_similarity(twincat_definitions, twincat_usage, master_tag)
if candidates:
# Tomar el mejor candidato
best_candidate = candidates[0]
var_name, var_info, source = best_candidate
result.update({
'twincat_variable': var_name,
'twincat_address': var_info.get('address', 'Unknown'),
'twincat_type': var_info.get('type', 'Unknown'),
'match_type': f'Name Similarity ({source})',
'definition_file': var_info.get('file', 'Unknown'),
'confidence': 'Medium'
})
matches_found += 1
# 3. Buscar información de uso
if result['twincat_variable']:
var_name = result['twincat_variable']
if var_name in twincat_usage:
usage_info = twincat_usage[var_name]
result['usage_files'] = list(set([u['file'] for u in usage_info]))
result['usage_count'] = len(usage_info)
results.append(result)
# Log del progreso
status = "" if result['twincat_variable'] else ""
print(f" {status} {tia_addr}{master_tag}")
if result['twincat_variable']:
print(f" 🔗 {result['twincat_variable']} AT %{result['twincat_address']}")
if result['usage_count'] > 0:
print(f" 📝 Usado en {result['usage_count']} lugares: {', '.join(result['usage_files'])}")
print(f"\n🎯 Resumen: {matches_found}/{len(tia_adaptations)} variables correlacionadas ({matches_found/len(tia_adaptations)*100:.1f}%)")
return results
def create_results_directory(working_directory):
"""Crea el directorio de resultados si no existe"""
results_dir = Path(working_directory) / 'resultados'
results_dir.mkdir(exist_ok=True)
print(f"📁 Directorio de resultados: {results_dir.absolute()}")
return results_dir
def generate_json_output(results, working_directory, output_file='io_adaptation_data.json'):
"""Genera archivo JSON con datos estructurados para análisis posterior"""
full_output_file = os.path.join(working_directory, 'resultados', output_file)
print(f"\n📄 Generando archivo JSON: {full_output_file}")
json_data = {
"metadata": {
"generated_at": pd.Timestamp.now().isoformat(),
"project": "E5.007560 - Modifica O&U - SAE235",
"total_adaptations": len(results),
"matched_variables": len([r for r in results if r['twincat_variable']]),
"high_confidence": len([r for r in results if r['confidence'] == 'High']),
"medium_confidence": len([r for r in results if r['confidence'] == 'Medium'])
},
"adaptations": []
}
for result in results:
adaptation = {
"tia_portal": {
"address": result['tia_address'],
"tag": result['master_tag']
},
"twincat": {
"variable": result['twincat_variable'],
"address": result['twincat_address'],
"data_type": result['twincat_type'],
"definition_file": result['definition_file']
},
"correlation": {
"match_type": result['match_type'],
"confidence": result['confidence'],
"found": result['twincat_variable'] is not None
},
"usage": {
"usage_count": result['usage_count'],
"usage_files": result['usage_files']
}
}
json_data["adaptations"].append(adaptation)
with open(full_output_file, 'w', encoding='utf-8') as f:
json.dump(json_data, f, indent=2, ensure_ascii=False)
print(f"✅ Archivo JSON generado: {full_output_file}")
def generate_detailed_report(results, working_directory, output_file='IO_Detailed_Analysis_Report.md'):
"""Genera un reporte detallado con tabla markdown"""
full_output_file = os.path.join(working_directory, 'resultados', output_file)
print(f"\n📄 Generando reporte detallado: {full_output_file}")
with open(full_output_file, 'w', encoding='utf-8') as f:
f.write("# Reporte Detallado de Análisis de Adaptación IO\n\n")
f.write(f"**Fecha de generación:** {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
# Estadísticas
total = len(results)
matched = len([r for r in results if r['twincat_variable']])
high_conf = len([r for r in results if r['confidence'] == 'High'])
medium_conf = len([r for r in results if r['confidence'] == 'Medium'])
f.write("## 📊 Estadísticas Generales\n\n")
f.write(f"- **Total adaptaciones procesadas:** {total}\n")
f.write(f"- **Variables encontradas:** {matched} ({matched/total*100:.1f}%)\n")
f.write(f"- **Coincidencias de alta confianza:** {high_conf}\n")
f.write(f"- **Coincidencias de media confianza:** {medium_conf}\n\n")
# Tabla de variables correlacionadas exitosamente
f.write("## ✅ Variables Correlacionadas Exitosamente\n\n")
matched_results = [r for r in results if r['twincat_variable']]
if matched_results:
# Encabezado de la tabla
f.write("| TIA Address | TIA Tag | TwinCAT Variable | TwinCAT Address | Tipo | Método | Confianza | Archivo Def. | Uso | Archivos Uso |\n")
f.write("|-------------|---------|------------------|-----------------|------|--------|-----------|--------------|-----|---------------|\n")
# Filas de datos
for result in matched_results:
usage_files_str = ', '.join(result['usage_files'][:3]) # Limitar a 3 archivos
if len(result['usage_files']) > 3:
usage_files_str += "..."
f.write(f"| {result['tia_address']} | "
f"`{result['master_tag']}` | "
f"`{result['twincat_variable']}` | "
f"`%{result['twincat_address']}` | "
f"`{result['twincat_type']}` | "
f"{result['match_type']} | "
f"{result['confidence']} | "
f"{result['definition_file']} | "
f"{result['usage_count']} | "
f"{usage_files_str} |\n")
f.write("\n")
# Tabla de variables no encontradas
f.write("## ❌ Variables No Encontradas\n\n")
unmatched_results = [r for r in results if not r['twincat_variable']]
if unmatched_results:
f.write("| TIA Address | TIA Tag |\n")
f.write("|-------------|----------|\n")
for result in unmatched_results:
f.write(f"| {result['tia_address']} | `{result['master_tag']}` |\n")
f.write(f"\n**Total no encontradas:** {len(unmatched_results)}\n\n")
# Recomendaciones
f.write("## 💡 Recomendaciones\n\n")
f.write("1. **Variables de alta confianza** pueden migrarse directamente\n")
f.write("2. **Variables de media confianza** requieren verificación manual\n")
f.write("3. **Variables no encontradas** requieren mapeo manual o pueden ser obsoletas\n")
f.write("4. Variables con uso extensivo son prioritarias para la migración\n\n")
# Resumen por confianza
f.write("## 📈 Distribución por Confianza\n\n")
f.write("| Nivel de Confianza | Cantidad | Porcentaje |\n")
f.write("|--------------------|----------|------------|\n")
f.write(f"| Alta | {high_conf} | {high_conf/total*100:.1f}% |\n")
f.write(f"| Media | {medium_conf} | {medium_conf/total*100:.1f}% |\n")
f.write(f"| No encontradas | {total-matched} | {(total-matched)/total*100:.1f}% |\n")
print(f"✅ Reporte detallado generado: {full_output_file}")
def main():
print("🚀 Iniciando análisis detallado de adaptación de IOs TwinCAT ↔ TIA Portal")
print("=" * 80)
# Cargar configuración
configs = load_configuration()
# Verificar que se cargó correctamente
if not configs:
print("Advertencia: No se pudo cargar la configuración, usando valores por defecto")
working_directory = "./"
else:
working_directory = configs.get("working_directory", "./")
# Verificar directorio de trabajo
if not os.path.exists(working_directory):
print(f"Error: El directorio de trabajo no existe: {working_directory}")
return
print(f"📁 Directorio de trabajo: {working_directory}")
# Crear directorio de resultados
results_dir = create_results_directory(working_directory)
# Cargar datos
tia_adaptations = load_tiaportal_adaptations(working_directory)
twincat_definitions = scan_twincat_definitions(working_directory)
twincat_usage = scan_twincat_usage(working_directory)
# Analizar correlaciones
results = analyze_adaptations(tia_adaptations, twincat_definitions, twincat_usage)
# Generar reportes en el directorio de resultados
generate_detailed_report(results, working_directory)
generate_json_output(results, working_directory)
# Generar CSV para análisis adicional
df = pd.DataFrame(results)
csv_file = results_dir / 'io_detailed_analysis.csv'
df.to_csv(csv_file, index=False, encoding='utf-8')
print(f"✅ Datos exportados a CSV: {csv_file}")
print(f"\n🎉 Análisis completado exitosamente!")
print(f"📁 Archivos generados en: {results_dir.absolute()}")
print(f" 📄 {results_dir / 'IO_Detailed_Analysis_Report.md'}")
print(f" 📄 {results_dir / 'io_adaptation_data.json'}")
print(f" 📄 {results_dir / 'io_detailed_analysis.csv'}")
return results
if __name__ == "__main__":
results = main()

View File

@ -0,0 +1,115 @@
# Análisis de Adaptación IO - TwinCAT ↔ TIA Portal
Scripts de análisis automático para correlacionar variables IO entre plataformas TwinCAT y TIA Portal en el proyecto SIDEL E5.007560.
## 📋 Descripción General
Este proyecto automatiza el análisis de adaptación de variables de entrada/salida (IO) entre:
- **TIA Portal** (Siemens) - Sistema actual
- **TwinCAT** (Beckhoff) - Sistema objetivo de migración
## 🔧 Scripts Incluidos
### 1. `x1_io_adaptation_script.py` - Análisis de Correlación IO
**Propósito:** Encuentra y correlaciona variables IO entre ambas plataformas generando reportes detallados.
**Archivos requeridos:**
- `IO Adapted.md` - Tabla de adaptaciones TIA Portal (debe estar en directorio raíz)
- `TwinCat/` - Directorio con archivos `.scl` de TwinCAT
- `TiaPortal/` - Directorio con archivos `.md` de TIA Portal
**Archivos generados:**
- `resultados/IO_Detailed_Analysis_Report.md` - Reporte con tablas markdown
- `resultados/io_adaptation_data.json` - Datos estructurados para análisis
- `resultados/io_detailed_analysis.csv` - Datos tabulares
### 2. `x2_code_snippets_generator.py` - Generador de Snippets de Código
**Propósito:** Genera snippets de código mostrando el uso real de cada variable en ambas plataformas.
**Archivos requeridos:**
- `resultados/io_adaptation_data.json` - Generado por el script 1
- `TwinCat/` - Directorio con archivos `.scl`
- `TiaPortal/` - Directorio con archivos `.md`
**Archivos generados:**
- `resultados/IO_Code_Snippets_Report.md` - Snippets de código con contexto
- `resultados/IO_Usage_Statistics.md` - Estadísticas de uso
## 🚀 Uso
### Paso 1: Ejecutar análisis de correlación
```bash
python x1_io_adaptation_script.py
```
### Paso 2: Generar snippets de código
```bash
python x2_code_snippets_generator.py
```
## 📁 Estructura de Directorios Requerida
```
proyecto/
├── x1_io_adaptation_script.py
├── x2_code_snippets_generator.py
├── IO Adapted.md # Tabla de adaptaciones TIA
├── TwinCat/ # Archivos .scl TwinCAT
│ ├── GLOBAL_VARIABLES_IN_OUT.scl
│ ├── INPUT.scl
│ └── ... (otros archivos .scl)
├── TiaPortal/ # Archivos .md TIA Portal
│ ├── Input.md
│ ├── Output.md
│ └── ... (otros archivos .md)
└── resultados/ # Directorio creado automáticamente
├── IO_Detailed_Analysis_Report.md
├── io_adaptation_data.json
├── io_detailed_analysis.csv
├── IO_Code_Snippets_Report.md
└── IO_Usage_Statistics.md
```
## 🔍 Funcionalidades Principales
### Script 1 - Análisis de Correlación
- ✅ Convierte direcciones TIA Portal a formato TwinCAT
- ✅ Busca variables por dirección exacta y similitud de nombres
- ✅ Calcula nivel de confianza de correlaciones
- ✅ Genera reportes en múltiples formatos (MD, JSON, CSV)
### Script 2 - Snippets de Código
- ✅ Muestra hasta 3 usos por variable por plataforma
- ✅ Contexto de 3 líneas (anterior, actual, siguiente)
- ✅ Links markdown a archivos fuente
- ✅ Estadísticas de uso y archivos más referenciados
## 📊 Resultados Típicos
- **Variables procesadas:** ~90-100 adaptaciones IO
- **Tasa de correlación:** ~70-80% de variables encontradas
- **Confianza alta:** Correlaciones por dirección exacta
- **Variable más usada:** Típicamente botones de reset/start/stop
## 🛠 Dependencias
```python
pandas
pathlib (incluida en Python 3.4+)
json (incluida en Python estándar)
re (incluida en Python estándar)
```
## 📝 Notas Importantes
1. **Orden de ejecución:** Ejecutar siempre el Script 1 antes que el Script 2
2. **Archivos fuente:** Verificar que existan los directorios TwinCat/ y TiaPortal/
3. **Codificación:** Los scripts manejan archivos con encoding UTF-8
4. **Rendimiento:** El Script 2 puede tardar algunos minutos procesando archivos grandes
## 👥 Proyecto
**Proyecto SIDEL:** E5.007560 - Modifica O&U - SAE235
**Automatización:** Migración TIA Portal → TwinCAT

View File

@ -0,0 +1,315 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para generar snippets de código de uso de variables IO
entre TwinCAT y TIA Portal - Proyecto SIDEL
Autor: Generado automáticamente
Proyecto: E5.007560 - Modifica O&U - SAE235
"""
import json
import os
import sys
import re
from pathlib import Path
from typing import Dict, List, Tuple, Optional
import pandas as pd
# Configurar el path al directorio raíz del proyecto
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
# Importar la función de configuración
from backend.script_utils import load_configuration
def load_adaptation_data(working_directory, json_file='io_adaptation_data.json'):
"""Carga los datos de adaptación desde el archivo JSON"""
full_json_file = os.path.join(working_directory, 'resultados', json_file)
print(f"📖 Cargando datos de adaptación desde: {full_json_file}")
if not os.path.exists(full_json_file):
print(f"⚠️ Archivo {full_json_file} no encontrado")
return None
with open(full_json_file, 'r', encoding='utf-8') as f:
data = json.load(f)
print(f"✅ Cargados datos de {data['metadata']['total_adaptations']} adaptaciones")
return data
def find_variable_usage_in_file(file_path, variable_name, max_occurrences=3):
"""Encuentra el uso de una variable en un archivo específico y retorna el contexto"""
if not os.path.exists(file_path):
return []
usages = []
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
lines = f.readlines()
# Buscar todas las líneas que contienen la variable
found_lines = []
for line_num, line in enumerate(lines):
# Buscar la variable como palabra completa (no como parte de otra palabra)
if re.search(rf'\b{re.escape(variable_name)}\b', line):
found_lines.append((line_num, line.strip()))
if len(found_lines) >= max_occurrences:
break
# Para cada ocurrencia, obtener contexto (línea anterior, actual, siguiente)
for line_num, line_content in found_lines:
context = {
'line_number': line_num + 1, # Convertir a 1-indexado
'before': lines[line_num - 1].strip() if line_num > 0 else "",
'current': line_content,
'after': lines[line_num + 1].strip() if line_num < len(lines) - 1 else ""
}
usages.append(context)
except Exception as e:
print(f"⚠️ Error leyendo archivo {file_path}: {e}")
return usages
def find_tia_portal_usage(adaptation, working_directory):
"""Busca el uso de variables TIA Portal en archivos markdown"""
tia_address = adaptation['tia_portal']['address']
tia_tag = adaptation['tia_portal']['tag']
# Buscar en archivos TIA Portal (principalmente en archivos .md)
tia_usages = []
# Buscar en TiaPortal/ directory
tia_portal_dir = Path(working_directory) / 'TiaPortal'
if tia_portal_dir.exists():
for md_file in tia_portal_dir.glob('*.md'):
# Buscar por dirección TIA
address_usages = find_variable_usage_in_file(md_file, tia_address, 2)
for usage in address_usages:
usage['file'] = f"TiaPortal/{md_file.name}"
usage['search_term'] = tia_address
tia_usages.append(usage)
# Buscar por tag TIA si es diferente
if tia_tag != tia_address:
tag_usages = find_variable_usage_in_file(md_file, tia_tag, 1)
for usage in tag_usages:
usage['file'] = f"TiaPortal/{md_file.name}"
usage['search_term'] = tia_tag
tia_usages.append(usage)
# Limitar total de usos TIA
if len(tia_usages) >= 3:
break
return tia_usages[:3] # Máximo 3 usos TIA
def find_twincat_usage(adaptation, working_directory):
"""Busca el uso de variables TwinCAT en archivos .scl"""
if not adaptation['correlation']['found']:
return []
variable_name = adaptation['twincat']['variable']
usage_files = adaptation['usage']['usage_files']
twincat_usages = []
# Buscar en archivos TwinCAT
twincat_dir = Path(working_directory) / 'TwinCat'
if twincat_dir.exists():
for file_name in usage_files:
file_path = twincat_dir / file_name
if file_path.exists():
usages = find_variable_usage_in_file(file_path, variable_name, 2)
for usage in usages:
usage['file'] = f"TwinCat/{file_name}"
usage['search_term'] = variable_name
twincat_usages.append(usage)
# Limitar por archivo
if len(twincat_usages) >= 3:
break
return twincat_usages[:3] # Máximo 3 usos TwinCAT
def generate_code_snippets_report(data, working_directory, output_file='IO_Code_Snippets_Report.md'):
"""Genera el reporte con snippets de código"""
full_output_file = os.path.join(working_directory, 'resultados', output_file)
print(f"\n📄 Generando reporte de snippets: {full_output_file}")
matched_adaptations = [a for a in data['adaptations'] if a['correlation']['found']]
with open(full_output_file, 'w', encoding='utf-8') as f:
f.write("# Reporte de Snippets de Código - Adaptación IO\n\n")
f.write(f"**Fecha de generación:** {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"**Proyecto:** {data['metadata']['project']}\n\n")
f.write("## 📋 Resumen\n\n")
f.write(f"- **Variables analizadas:** {len(matched_adaptations)}\n")
f.write(f"- **Snippets generados:** Se muestran hasta 3 usos por plataforma\n")
f.write(f"- **Formato:** Contexto de 3 líneas (anterior, actual, siguiente)\n\n")
f.write("---\n\n")
# Procesar cada adaptación
for i, adaptation in enumerate(matched_adaptations, 1):
tia_address = adaptation['tia_portal']['address']
tia_tag = adaptation['tia_portal']['tag']
twincat_var = adaptation['twincat']['variable']
twincat_addr = adaptation['twincat']['address']
print(f" 📝 Procesando {i}/{len(matched_adaptations)}: {tia_address}{twincat_var}")
f.write(f"## {i}. {tia_address}{twincat_var}\n\n")
f.write(f"**TIA Portal:** `{tia_tag}` (`{tia_address}`)\n")
f.write(f"**TwinCAT:** `{twincat_var}` (`%{twincat_addr}`)\n")
f.write(f"**Tipo:** `{adaptation['twincat']['data_type']}`\n\n")
# Buscar usos en TIA Portal
f.write("### 🔵 Uso en TIA Portal\n\n")
tia_usages = find_tia_portal_usage(adaptation, working_directory)
if tia_usages:
for j, usage in enumerate(tia_usages):
f.write(f"**Uso {j+1}:** [{usage['file']}]({usage['file']}) - Línea {usage['line_number']}\n\n")
f.write("```scl\n")
if usage['before']:
f.write(f"{usage['before']}\n")
f.write(f">>> {usage['current']} // ← {usage['search_term']}\n")
if usage['after']:
f.write(f"{usage['after']}\n")
f.write("```\n\n")
else:
f.write("*No se encontraron usos específicos en archivos TIA Portal.*\n\n")
# Buscar usos en TwinCAT
f.write("### 🟢 Uso en TwinCAT\n\n")
twincat_usages = find_twincat_usage(adaptation, working_directory)
if twincat_usages:
for j, usage in enumerate(twincat_usages):
f.write(f"**Uso {j+1}:** [{usage['file']}]({usage['file']}) - Línea {usage['line_number']}\n\n")
f.write("```scl\n")
if usage['before']:
f.write(f"{usage['before']}\n")
f.write(f">>> {usage['current']} // ← {usage['search_term']}\n")
if usage['after']:
f.write(f"{usage['after']}\n")
f.write("```\n\n")
else:
f.write("*Variable definida pero no se encontraron usos específicos.*\n\n")
f.write("---\n\n")
print(f"✅ Reporte de snippets generado: {full_output_file}")
def generate_summary_statistics(data, working_directory, output_file='IO_Usage_Statistics.md'):
"""Genera estadísticas de uso de las variables"""
full_output_file = os.path.join(working_directory, 'resultados', output_file)
print(f"\n📊 Generando estadísticas de uso: {full_output_file}")
matched_adaptations = [a for a in data['adaptations'] if a['correlation']['found']]
# Calcular estadísticas
total_usage = sum(a['usage']['usage_count'] for a in matched_adaptations)
variables_with_usage = len([a for a in matched_adaptations if a['usage']['usage_count'] > 0])
# Variables más usadas
most_used = sorted(matched_adaptations, key=lambda x: x['usage']['usage_count'], reverse=True)[:10]
# Archivos más referenciados
file_usage = {}
for adaptation in matched_adaptations:
for file_name in adaptation['usage']['usage_files']:
file_usage[file_name] = file_usage.get(file_name, 0) + 1
top_files = sorted(file_usage.items(), key=lambda x: x[1], reverse=True)[:10]
with open(full_output_file, 'w', encoding='utf-8') as f:
f.write("# Estadísticas de Uso de Variables IO\n\n")
f.write(f"**Fecha de generación:** {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
f.write("## 📊 Resumen General\n\n")
f.write(f"- **Variables correlacionadas:** {len(matched_adaptations)}\n")
f.write(f"- **Variables con uso documentado:** {variables_with_usage}\n")
f.write(f"- **Total de usos encontrados:** {total_usage}\n")
f.write(f"- **Promedio de usos por variable:** {total_usage/len(matched_adaptations):.1f}\n\n")
f.write("## 🔥 Top 10 Variables Más Usadas\n\n")
f.write("| Ranking | TIA Address | TwinCAT Variable | Usos | Archivos |\n")
f.write("|---------|-------------|------------------|------|----------|\n")
for i, adaptation in enumerate(most_used, 1):
files_str = ', '.join(adaptation['usage']['usage_files'][:3])
if len(adaptation['usage']['usage_files']) > 3:
files_str += '...'
f.write(f"| {i} | {adaptation['tia_portal']['address']} | "
f"`{adaptation['twincat']['variable']}` | "
f"{adaptation['usage']['usage_count']} | {files_str} |\n")
f.write("\n## 📁 Top 10 Archivos Más Referenciados\n\n")
f.write("| Ranking | Archivo | Variables Usadas |\n")
f.write("|---------|---------|------------------|\n")
for i, (file_name, count) in enumerate(top_files, 1):
f.write(f"| {i} | `{file_name}` | {count} |\n")
print(f"✅ Estadísticas de uso generadas: {full_output_file}")
def main():
print("🚀 Iniciando generación de snippets de código para adaptación IO")
print("=" * 70)
# Cargar configuración
configs = load_configuration()
# Verificar que se cargó correctamente
if not configs:
print("Advertencia: No se pudo cargar la configuración, usando valores por defecto")
working_directory = "./"
else:
working_directory = configs.get("working_directory", "./")
# Verificar directorio de trabajo
if not os.path.exists(working_directory):
print(f"Error: El directorio de trabajo no existe: {working_directory}")
return
print(f"📁 Directorio de trabajo: {working_directory}")
# Crear directorio de resultados si no existe
results_dir = Path(working_directory) / 'resultados'
results_dir.mkdir(exist_ok=True)
# Cargar datos de adaptación
data = load_adaptation_data(working_directory)
if not data:
print("❌ No se pudieron cargar los datos de adaptación")
return
# Generar reporte de snippets
generate_code_snippets_report(data, working_directory)
# Generar estadísticas de uso
generate_summary_statistics(data, working_directory)
print(f"\n🎉 Generación completada exitosamente!")
print(f"📁 Archivos generados en: {results_dir.absolute()}")
print(f" 📄 {results_dir / 'IO_Code_Snippets_Report.md'}")
print(f" 📄 {results_dir / 'IO_Usage_Statistics.md'}")
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@ -4,13 +4,7 @@
"path": "."
},
{
"path": "C:/Program Files/Siemens/Automation/Portal V19/PublicAPI/V19/Schemas"
},
{
"path": "../../../../../../Trabajo/VM/44 - 98050 - Fiera/Reporte/ExportsTia/Source/98050_PLC"
},
{
"path": "../../../../../../Trabajo/VM/22 - 93841 - Sidel - Tilting/Reporte/TiaExports"
"path": "C:/Trabajo/SIDEL/13 - E5.007560 - Modifica O&U - SAE235/Reporte/ExportTia"
}
],
"settings": {}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,90 +1,31 @@
--- Log de Ejecución: x7_clear.py ---
Grupo: XML Parser to SCL
Directorio de Trabajo: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\Source
Inicio: 2025-06-13 01:01:10
Fin: 2025-06-13 01:01:11
Duración: 0:00:00.701052
Directorio de Trabajo: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\ExportTia
Inicio: 2025-06-20 18:53:46
Fin: 2025-06-20 18:53:47
Duración: 0:00:01.131243
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
INFO: format_variable_name importado desde generators.generator_utils
=== Limpiando PLC: 98050_PLC ===
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\MiniMotor\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\MiniMotor\DBS55_PN_Extend-A\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\SICK AG\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\TRANSFER\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\ConveyorsBase\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\Motion\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\Motion\Siemens\LCamHdl_Types\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\Motion\Technology\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\SeamlessDivider\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\SeamlessDivider\Technology\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Machine\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Machine\Cycle\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcTags\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcTags\Library\Motion\Siemens\LCamHdl_Tags\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\DB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\1-AIR Philosophy\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\2-TTOP Philosophy\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\3-Motors Manage\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\3-Motors Manage\MiniMotor_PN\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\3-Motors Manage\MiniMotor_PN\MiniMotor_PN\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\HMI\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\MACHINE SIGNALS\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\OB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!!TRANSFER\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\0 - MAIN\DB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\0 - MAIN\FC\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\0 - MAIN\OB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Device\DB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Device\FB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Device\FC\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\General\DB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\General\FC\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Motor\DB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Motor\DB\Minimotor\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Motor\FC\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Motor\FC\Minimotor\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\4 - LUBE\DB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\4 - LUBE\FB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\4 - LUBE\FB\OLD\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\2 - MACHINE\DB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\2 - MACHINE\FB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\! ConveyorsSTD\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\! ConveyorsSTD\Hmi\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\! ConveyorsSTD\System\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\! ConveyorsSTD\TimeZone\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\AAA_Debug\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\AAA_VirtualMaster\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\ExchangeSignals\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\ExchangeSignals\Loop\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\HMI\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Instances\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Generic\Alarms\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Motion\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Motion\Siemens\LCamHdl_Blocks\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Motion\Technology\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Motion\Utilities\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\SeamlessDivider\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\SeamlessDivider\Technology\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Machine\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Machine\Instances\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Setup\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\TimingBelt (downstream divider)\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\TimingBelt (downstream divider)\Instances\parsing
- Eliminado directorio 'scl_output': 98050_PLC\scl_output
- Eliminado directorio 'xref_output': 98050_PLC\xref_output
- Eliminado archivo agregado: 98050_PLC\full_project_representation.md
- Eliminado log: log_98050_PLC.txt
=== Limpiando PLC: PLC ===
- Eliminado directorio de parsing: PLC\PlcDataTypes\parsing
- Eliminado directorio de parsing: PLC\PlcDataTypes_CR\parsing
- Eliminado directorio de parsing: PLC\PlcTags\parsing
- Eliminado directorio de parsing: PLC\PlcTags\IO Not in Hardware\parsing
- Eliminado directorio de parsing: PLC\ProgramBlocks_CR\parsing
- Eliminado directorio de parsing: PLC\ProgramBlocks_CR\40_10_GNS_PLCdia Main\parsing
- Eliminado directorio de parsing: PLC\ProgramBlocks_XML\parsing
- Eliminado directorio de parsing: PLC\ProgramBlocks_XML\40_10_GNS_PLCdia Main\parsing
- Eliminado directorio de parsing: PLC\SystemBlocks_CR\parsing
- Eliminado directorio 'scl_output': PLC\scl_output
- Eliminado directorio 'xref_output': PLC\xref_output
- Eliminado archivo agregado: PLC\full_project_representation.md
- Eliminado log: log_PLC.txt
--- Resumen de limpieza ---
Directorios eliminados: 70
Directorios eliminados: 11
Archivos eliminados: 2
Limpieza completada.

View File

@ -273,6 +273,25 @@ def parse_lad_fbd_network(network_element):
# --- Poblar Entradas ---
# Lista base de pines posibles (podría obtenerse de XSDs o dinámicamente)
possible_input_pins = set(["en", "in", "in1", "in2", "pre"])
# **NUEVO: Generar pines dinámicamente para compuertas OR/AND basándose en Cardinality**
if original_type in ["O", "And"]: # Compuertas lógicas
cardinality = instruction_info.get("template_values", {}).get("Card")
if cardinality:
try:
num_inputs = int(cardinality)
# Generar pines in1, in2, ..., inN
for i in range(1, num_inputs + 1):
possible_input_pins.add(f"in{i}")
print(f"INFO: Compuerta {original_type} UID {instruction_uid} con cardinalidad {num_inputs} - generando pines in1...in{num_inputs}")
except (ValueError, TypeError):
print(f"Advertencia: Cardinalidad inválida '{cardinality}' para {original_type} UID {instruction_uid}")
# Fallback a pines estándar
possible_input_pins.update(["in1", "in2"])
else:
# Sin cardinalidad explícita, usar pines estándar
possible_input_pins.update(["in1", "in2"])
# Añadir pines dinámicamente basados en el tipo de instrucción
if original_type in ["Contact", "Coil", "SCoil", "RCoil", "SdCoil"]:
possible_input_pins.add("operand")

View File

@ -276,15 +276,16 @@ def parse_part(part_element):
template_values = {}
negated_pins = {}
try:
for tv in part_element.xpath("./TemplateValue"):
for tv in part_element.xpath("./flg:TemplateValue", namespaces=ns):
tv_name = tv.get("Name")
tv_type = tv.get("Type")
if tv_name and tv_type:
template_values[tv_name] = tv_type
tv_value = tv.text.strip() if tv.text else tv_type # Obtener valor real del elemento
if tv_name:
template_values[tv_name] = tv_value
except Exception as e:
print(f"Advertencia: Error extrayendo TemplateValues Part UID={uid}: {e}")
try:
for negated_elem in part_element.xpath("./Negated"):
for negated_elem in part_element.xpath("./flg:Negated", namespaces=ns):
negated_pin_name = negated_elem.get("Name")
if negated_pin_name:
negated_pins[negated_pin_name] = True

View File

@ -15,5 +15,5 @@
"xref_source_subdir": "source"
},
"level3": {},
"working_directory": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source"
"working_directory": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia"
}

View File

@ -1,8 +1,8 @@
{
"path": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
"path": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
"history": [
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
"C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
"D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML",
"C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\IOExport"

43711
data/log.txt

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,900 @@
import os
import json
import subprocess
import sys
import threading
import time
from typing import Dict, Any, List, Optional
from datetime import datetime
import uuid
class PythonLauncherManager:
def __init__(self, data_path: str):
self.data_path = data_path
self.launcher_config_path = os.path.join(data_path, "python_launcher_projects.json")
self.favorites_path = os.path.join(data_path, "python_launcher_favorites.json")
self.history_path = os.path.join(data_path, "python_launcher_history.json")
self.script_metadata_path = os.path.join(data_path, "python_launcher_script_metadata.json")
# Procesos en ejecución para Python (servidores, etc.)
self.running_processes = {}
self.process_lock = threading.Lock()
# Inicializar archivos si no existen
self._initialize_files()
def _initialize_files(self):
"""Crear archivos de configuración por defecto si no existen"""
# Inicializar python_launcher_projects.json
if not os.path.exists(self.launcher_config_path):
default_config = {
"version": "1.0",
"projects": [],
"categories": {
"MCP Servers": {
"color": "#3B82F6",
"icon": "🔌",
"subcategories": ["Anthropic", "Custom", "OpenAI"]
},
"Flask Apps": {
"color": "#10B981",
"icon": "🌐",
"subcategories": ["API", "Web App", "Microservice"]
},
"Scripts": {
"color": "#8B5CF6",
"icon": "📜",
"subcategories": ["Automatización", "Utiles", "Procesamiento"]
},
"Bots": {
"color": "#F59E0B",
"icon": "🤖",
"subcategories": ["Discord", "Telegram", "Slack"]
},
"Data Processing": {
"color": "#EF4444",
"icon": "📊",
"subcategories": ["ETL", "Analysis", "ML"]
},
"Otros": {
"color": "#6B7280",
"icon": "📁",
"subcategories": ["Misceláneos"]
}
},
"settings": {
"default_execution_directory": "project_directory",
"enable_argument_validation": True,
"max_history_entries": 100,
"auto_cleanup_days": 30,
"default_python_env": "base"
}
}
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
json.dump(default_config, f, indent=2, ensure_ascii=False)
# Inicializar python_launcher_favorites.json
if not os.path.exists(self.favorites_path):
default_favorites = {"favorites": []}
with open(self.favorites_path, 'w', encoding='utf-8') as f:
json.dump(default_favorites, f, indent=2, ensure_ascii=False)
# Inicializar python_launcher_history.json
if not os.path.exists(self.history_path):
default_history = {
"history": [],
"settings": {
"max_entries": 100,
"auto_cleanup_days": 30,
"track_execution_time": True,
"track_arguments": True
}
}
with open(self.history_path, 'w', encoding='utf-8') as f:
json.dump(default_history, f, indent=2, ensure_ascii=False)
# Inicializar python_launcher_script_metadata.json
if not os.path.exists(self.script_metadata_path):
default_metadata = {
"version": "1.0",
"script_metadata": {}
}
with open(self.script_metadata_path, 'w', encoding='utf-8') as f:
json.dump(default_metadata, f, indent=2, ensure_ascii=False)
def get_python_projects(self) -> List[Dict[str, Any]]:
"""Obtener todos los proyectos Python"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
return config.get("projects", [])
except Exception as e:
print(f"Error loading Python projects: {e}")
return []
def get_python_project(self, project_id: str) -> Optional[Dict[str, Any]]:
"""Obtener un proyecto específico por ID"""
projects = self.get_python_projects()
for project in projects:
if project.get("id") == project_id:
return project
return None
def add_python_project(self, project_data: Dict[str, Any]) -> Dict[str, str]:
"""Agregar nuevo proyecto Python"""
try:
# Validar datos requeridos
required_fields = ["name", "directory"]
for field in required_fields:
if not project_data.get(field):
return {"status": "error", "message": f"Campo requerido: {field}"}
# Validar que el directorio existe
if not os.path.isdir(project_data["directory"]):
return {"status": "error", "message": "El directorio especificado no existe"}
# Generar ID único si no se proporciona
if not project_data.get("id"):
project_data["id"] = str(uuid.uuid4())[:8]
# Verificar que el ID no exista
if self.get_python_project(project_data["id"]):
return {"status": "error", "message": "Ya existe un proyecto con este ID"}
# Agregar campos por defecto
current_time = datetime.now().isoformat() + "Z"
project_data.setdefault("description", "")
project_data.setdefault("category", "Otros")
project_data.setdefault("version", "1.0")
project_data.setdefault("author", "")
project_data.setdefault("tags", [])
project_data.setdefault("python_env", "base") # Entorno Python por defecto
project_data.setdefault("created_date", current_time)
project_data["updated_date"] = current_time
# Cargar configuración y agregar proyecto
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
config["projects"].append(project_data)
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
return {"status": "success", "message": "Proyecto agregado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error agregando proyecto: {str(e)}"}
def update_python_project(self, project_id: str, project_data: Dict[str, Any]) -> Dict[str, str]:
"""Actualizar proyecto existente"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# Buscar y actualizar el proyecto
project_found = False
for i, project in enumerate(config["projects"]):
if project["id"] == project_id:
# Mantener ID y fechas de creación
project_data["id"] = project_id
project_data["created_date"] = project.get("created_date", datetime.now().isoformat() + "Z")
project_data["updated_date"] = datetime.now().isoformat() + "Z"
config["projects"][i] = project_data
project_found = True
break
if not project_found:
return {"status": "error", "message": "Proyecto no encontrado"}
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
return {"status": "success", "message": "Proyecto actualizado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error actualizando proyecto: {str(e)}"}
def delete_python_project(self, project_id: str) -> Dict[str, str]:
"""Eliminar proyecto Python"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# Filtrar el proyecto a eliminar
original_count = len(config["projects"])
config["projects"] = [p for p in config["projects"] if p["id"] != project_id]
if len(config["projects"]) == original_count:
return {"status": "error", "message": "Proyecto no encontrado"}
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
# Limpiar metadatos y favoritos relacionados
self._cleanup_script_metadata_for_project(project_id)
self._cleanup_favorites_for_project(project_id)
return {"status": "success", "message": "Proyecto eliminado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error eliminando proyecto: {str(e)}"}
def get_project_scripts(self, project_id: str) -> List[Dict[str, Any]]:
"""Obtener scripts de un proyecto (solo .py visibles)"""
project = self.get_python_project(project_id)
if not project:
return []
project_dir = project["directory"]
if not os.path.isdir(project_dir):
return []
scripts = []
script_metadata = self._load_script_metadata()
# Buscar archivos .py en el directorio del proyecto
for filename in os.listdir(project_dir):
if filename.endswith('.py') and not filename.startswith('__'):
script_path = os.path.join(project_dir, filename)
if os.path.isfile(script_path):
# Obtener metadatos del script
metadata_key = f"{project_id}:{filename}"
metadata = script_metadata.get("script_metadata", {}).get(metadata_key, {})
# Solo mostrar scripts no ocultos
if not metadata.get("hidden", False):
scripts.append({
"filename": filename,
"display_name": metadata.get("display_name", filename.replace('.py', '')),
"description": metadata.get("description", ""),
"tags": metadata.get("tags", []),
"arguments": metadata.get("arguments", []),
"is_server": metadata.get("is_server", False), # Indica si es un servidor que corre en background
"server_port": metadata.get("server_port", ""),
"requires_background": metadata.get("requires_background", False)
})
return sorted(scripts, key=lambda x: x["display_name"])
def get_all_project_scripts(self, project_id: str) -> List[Dict[str, Any]]:
"""Obtener TODOS los scripts de un proyecto (incluyendo ocultos) para gestión"""
project = self.get_python_project(project_id)
if not project:
return []
project_dir = project["directory"]
if not os.path.isdir(project_dir):
return []
scripts = []
script_metadata = self._load_script_metadata()
# Buscar archivos .py en el directorio del proyecto
for filename in os.listdir(project_dir):
if filename.endswith('.py') and not filename.startswith('__'):
script_path = os.path.join(project_dir, filename)
if os.path.isfile(script_path):
# Obtener metadatos del script
metadata_key = f"{project_id}:{filename}"
metadata = script_metadata.get("script_metadata", {}).get(metadata_key, {})
scripts.append({
"filename": filename,
"display_name": metadata.get("display_name", filename.replace('.py', '')),
"description": metadata.get("description", ""),
"tags": metadata.get("tags", []),
"arguments": metadata.get("arguments", []),
"hidden": metadata.get("hidden", False),
"is_server": metadata.get("is_server", False),
"server_port": metadata.get("server_port", ""),
"requires_background": metadata.get("requires_background", False)
})
return sorted(scripts, key=lambda x: x["display_name"])
def get_script_metadata(self, project_id: str, script_name: str) -> Dict[str, Any]:
"""Obtener metadatos de un script específico"""
script_metadata = self._load_script_metadata()
metadata_key = f"{project_id}:{script_name}"
return script_metadata.get("script_metadata", {}).get(metadata_key, {})
def update_script_metadata(self, project_id: str, script_name: str, metadata: Dict[str, Any]) -> Dict[str, str]:
"""Actualizar metadatos de un script"""
try:
script_metadata = self._load_script_metadata()
metadata_key = f"{project_id}:{script_name}"
if "script_metadata" not in script_metadata:
script_metadata["script_metadata"] = {}
script_metadata["script_metadata"][metadata_key] = metadata
self._save_script_metadata(script_metadata)
return {"status": "success", "message": "Metadatos actualizados exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error actualizando metadatos: {str(e)}"}
def get_available_python_envs(self) -> List[Dict[str, str]]:
"""Obtener lista de entornos de Python/Miniconda disponibles"""
try:
envs = [{"name": "base", "display_name": "Base (Sistema)", "path": sys.executable}]
# Intentar encontrar Miniconda
miniconda_paths = [
r"C:\Users\migue\miniconda3",
r"C:\ProgramData\miniconda3",
r"C:\miniconda3",
os.path.expanduser("~/miniconda3"),
os.path.expanduser("~/anaconda3")
]
for base_path in miniconda_paths:
if os.path.exists(base_path):
envs_path = os.path.join(base_path, "envs")
if os.path.exists(envs_path):
for env_name in os.listdir(envs_path):
env_path = os.path.join(envs_path, env_name)
python_exe = os.path.join(env_path, "python.exe")
if os.path.exists(python_exe):
envs.append({
"name": env_name,
"display_name": f"{env_name} (Miniconda)",
"path": python_exe
})
break # Solo usar el primer Miniconda encontrado
return envs
except Exception as e:
print(f"Error getting Python environments: {e}")
return [{"name": "base", "display_name": "Base (Sistema)", "path": sys.executable}]
def execute_python_script(self, project_id: str, script_name: str, script_args: List[str],
broadcast_func, working_dir: str = None, run_in_background: bool = False) -> Dict[str, Any]:
"""Ejecutar script Python con argumentos opcionales"""
try:
project = self.get_python_project(project_id)
if not project:
return {"error": "Proyecto no encontrado"}
# Construir ruta del script
script_path = os.path.join(project["directory"], script_name)
if not os.path.exists(script_path):
return {"error": f"Script '{script_name}' no encontrado"}
# Determinar directorio de trabajo
if working_dir and os.path.isdir(working_dir):
work_dir = working_dir
else:
work_dir = project["directory"]
# Obtener ejecutable de Python
python_env = project.get("python_env", "base")
python_exe = self._get_python_executable(python_env)
# Construir comando
cmd = [python_exe, script_path] + script_args
# ID único para esta ejecución
execution_id = str(uuid.uuid4())[:8]
start_time = time.time()
broadcast_func(f"🚀 Ejecutando script: {script_name}")
broadcast_func(f"📁 Directorio: {work_dir}")
broadcast_func(f"🐍 Python: {python_exe}")
if script_args:
broadcast_func(f"⚙️ Argumentos: {' '.join(script_args)}")
# Agregar a historial
history_entry = {
"id": execution_id,
"project_id": project_id,
"script_name": script_name,
"arguments": script_args,
"working_directory": work_dir,
"python_env": python_env,
"timestamp": datetime.now().isoformat() + "Z",
"status": "running",
"execution_time": None
}
self._add_to_history(history_entry)
# Configurar proceso
if sys.platform == "win32":
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
if run_in_background:
# Para procesos en background (servidores), crear ventana nueva
creationflags |= subprocess.CREATE_NEW_CONSOLE
else:
creationflags = 0
# Ejecutar proceso
process = subprocess.Popen(
cmd,
cwd=work_dir,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
universal_newlines=True,
creationflags=creationflags if sys.platform == "win32" else None
)
# Guardar proceso en la lista de procesos activos
with self.process_lock:
self.running_processes[process.pid] = {
"pid": process.pid,
"project_id": project_id,
"script_name": script_name,
"start_time": datetime.now().isoformat() + "Z",
"execution_id": execution_id,
"working_directory": work_dir,
"is_background": run_in_background
}
broadcast_func(f"✅ Proceso iniciado con PID: {process.pid}")
if run_in_background:
# Para procesos en background, no esperamos la salida
broadcast_func(f"🔄 Script ejecutándose en segundo plano (PID: {process.pid})")
return {
"status": "success",
"message": f"Script '{script_name}' iniciado en segundo plano",
"execution_id": execution_id,
"pid": process.pid,
"background": True
}
else:
# Para scripts normales, leer salida en tiempo real
def read_output():
try:
for line in iter(process.stdout.readline, ''):
if line:
broadcast_func(line.rstrip())
except Exception as e:
broadcast_func(f"Error leyendo salida: {e}")
finally:
if process.stdout:
process.stdout.close()
# Iniciar lectura de salida en hilo separado
output_thread = threading.Thread(target=read_output, daemon=True)
output_thread.start()
# Monitorear finalización del proceso
def monitor_completion():
try:
return_code = process.wait()
end_time = time.time()
execution_time = end_time - start_time
# Actualizar historial
self._update_history_status(execution_id, return_code, execution_time)
# Remover de procesos activos
with self.process_lock:
if process.pid in self.running_processes:
del self.running_processes[process.pid]
if return_code == 0:
broadcast_func(f"✅ Script completado exitosamente (código: {return_code})")
else:
broadcast_func(f"❌ Script terminó con errores (código: {return_code})")
broadcast_func(f"⏱️ Tiempo de ejecución: {execution_time:.2f} segundos")
except Exception as e:
broadcast_func(f"Error monitoreando proceso: {e}")
# Iniciar monitoreo en hilo separado
monitor_thread = threading.Thread(target=monitor_completion, daemon=True)
monitor_thread.start()
return {
"status": "success",
"message": f"Script '{script_name}' ejecutándose...",
"execution_id": execution_id,
"pid": process.pid,
"background": False
}
except Exception as e:
error_msg = f"Error ejecutando script Python: {str(e)}"
broadcast_func(error_msg)
return {"error": error_msg}
def _get_python_executable(self, env_name: str) -> str:
"""Obtener ejecutable de Python para el entorno especificado"""
if env_name == "base":
return sys.executable
# Intentar encontrar entorno de conda en todas las ubicaciones posibles
miniconda_paths = [
r"C:\Users\migue\miniconda3",
r"C:\ProgramData\miniconda3",
r"C:\miniconda3",
os.path.expanduser("~/miniconda3"),
os.path.expanduser("~/anaconda3")
]
for base_path in miniconda_paths:
if os.path.exists(base_path):
env_path = os.path.join(base_path, "envs", env_name)
python_exe = os.path.join(env_path, "python.exe")
if os.path.exists(python_exe):
return python_exe
# Fallback al Python del sistema
print(f"Warning: Python environment '{env_name}' not found, using system Python")
return sys.executable
def _load_script_metadata(self) -> Dict[str, Any]:
"""Cargar metadatos de scripts desde archivo"""
try:
with open(self.script_metadata_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception:
return {"version": "1.0", "script_metadata": {}}
def _save_script_metadata(self, metadata: Dict[str, Any]):
"""Guardar metadatos de scripts en archivo"""
try:
with open(self.script_metadata_path, 'w', encoding='utf-8') as f:
json.dump(metadata, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error saving script metadata: {e}")
def _cleanup_script_metadata_for_project(self, project_id: str):
"""Limpiar metadatos de scripts al eliminar un proyecto"""
try:
script_metadata = self._load_script_metadata()
if "script_metadata" in script_metadata:
# Filtrar metadatos que no pertenezcan al proyecto eliminado
script_metadata["script_metadata"] = {
k: v for k, v in script_metadata["script_metadata"].items()
if not k.startswith(f"{project_id}:")
}
self._save_script_metadata(script_metadata)
except Exception as e:
print(f"Error cleaning script metadata for project {project_id}: {e}")
def get_favorites(self) -> List[Dict[str, Any]]:
"""Obtener scripts favoritos"""
try:
with open(self.favorites_path, 'r', encoding='utf-8') as f:
favorites_data = json.load(f)
return favorites_data.get("favorites", [])
except Exception:
return []
def toggle_favorite(self, project_id: str, script_name: str) -> Dict[str, str]:
"""Agregar o quitar de favoritos"""
try:
favorites_data = {"favorites": self.get_favorites()}
# Buscar si ya está en favoritos
favorite_key = f"{project_id}:{script_name}"
existing_favorite = None
for i, fav in enumerate(favorites_data["favorites"]):
if fav.get("project_id") == project_id and fav.get("script_name") == script_name:
existing_favorite = i
break
if existing_favorite is not None:
# Quitar de favoritos
del favorites_data["favorites"][existing_favorite]
message = "Removido de favoritos"
is_favorite = False
else:
# Agregar a favoritos
project = self.get_python_project(project_id)
if project:
script_metadata = self.get_script_metadata(project_id, script_name)
favorites_data["favorites"].append({
"project_id": project_id,
"project_name": project["name"],
"script_name": script_name,
"display_name": script_metadata.get("display_name", script_name.replace('.py', '')),
"description": script_metadata.get("description", ""),
"added_date": datetime.now().isoformat() + "Z"
})
message = "Agregado a favoritos"
is_favorite = True
else:
return {"status": "error", "message": "Proyecto no encontrado"}
with open(self.favorites_path, 'w', encoding='utf-8') as f:
json.dump(favorites_data, f, indent=2, ensure_ascii=False)
return {"status": "success", "message": message, "is_favorite": is_favorite}
except Exception as e:
return {"status": "error", "message": f"Error gestionando favoritos: {str(e)}"}
def get_history(self) -> List[Dict[str, Any]]:
"""Obtener historial de ejecuciones"""
try:
with open(self.history_path, 'r', encoding='utf-8') as f:
history_data = json.load(f)
return history_data.get("history", [])
except Exception:
return []
def clear_history(self) -> Dict[str, str]:
"""Limpiar historial de ejecuciones"""
try:
history_data = {
"history": [],
"settings": {
"max_entries": 100,
"auto_cleanup_days": 30,
"track_execution_time": True,
"track_arguments": True
}
}
with open(self.history_path, 'w', encoding='utf-8') as f:
json.dump(history_data, f, indent=2, ensure_ascii=False)
return {"status": "success", "message": "Historial limpiado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error limpiando historial: {str(e)}"}
def get_categories(self) -> Dict[str, Any]:
"""Obtener categorías disponibles"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
return config.get("categories", {})
except Exception:
return {}
def _add_to_history(self, entry: Dict[str, Any]):
"""Agregar entrada al historial"""
try:
history_data = {"history": self.get_history()}
# Agregar nueva entrada al inicio
history_data["history"].insert(0, entry)
# Mantener máximo de entradas
max_entries = 100
if len(history_data["history"]) > max_entries:
history_data["history"] = history_data["history"][:max_entries]
with open(self.history_path, 'w', encoding='utf-8') as f:
json.dump(history_data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error adding to history: {e}")
def _cleanup_favorites_for_project(self, project_id: str):
"""Limpiar favoritos al eliminar un proyecto"""
try:
favorites_data = {"favorites": self.get_favorites()}
# Filtrar favoritos que no pertenezcan al proyecto eliminado
favorites_data["favorites"] = [
fav for fav in favorites_data["favorites"]
if fav.get("project_id") != project_id
]
with open(self.favorites_path, 'w', encoding='utf-8') as f:
json.dump(favorites_data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error cleaning favorites for project {project_id}: {e}")
def _update_history_status(self, execution_id: str, final_code: int, final_execution_time: float):
"""Actualizar estado final en el historial"""
try:
history_data = {"history": self.get_history()}
for entry in history_data["history"]:
if entry.get("id") == execution_id:
entry["status"] = "completed" if final_code == 0 else "error"
entry["return_code"] = final_code
entry["execution_time"] = final_execution_time
break
with open(self.history_path, 'w', encoding='utf-8') as f:
json.dump(history_data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error updating history status: {e}")
def focus_process(self, pid: int) -> Dict[str, str]:
"""Intentar dar foco a un proceso por su PID (Windows)"""
try:
if sys.platform == "win32":
import ctypes
from ctypes import wintypes
def enum_windows_proc(hwnd, pid):
if ctypes.windll.user32.IsWindowVisible(hwnd):
process_id = wintypes.DWORD()
ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(process_id))
if process_id.value == pid:
ctypes.windll.user32.SetForegroundWindow(hwnd)
return False # Detener enumeración
return True # Continuar enumeración
# Definir el tipo de callback
EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)
callback = EnumWindowsProc(enum_windows_proc)
ctypes.windll.user32.EnumWindows(callback, pid)
return {"status": "success", "message": f"Intentando dar foco al proceso {pid}"}
else:
return {"status": "info", "message": "Función de foco no disponible en esta plataforma"}
except Exception as e:
return {"status": "error", "message": f"Error dando foco al proceso: {str(e)}"}
def terminate_process(self, pid: int) -> Dict[str, str]:
"""Terminar un proceso por su PID"""
try:
with self.process_lock:
if pid in self.running_processes:
process_info = self.running_processes[pid]
del self.running_processes[pid]
# Intentar terminar el proceso
if sys.platform == "win32":
subprocess.run(["taskkill", "/F", "/PID", str(pid)],
capture_output=True, check=False)
else:
import signal
try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
pass # Proceso ya terminado
return {
"status": "success",
"message": f"Proceso {pid} terminado ({process_info.get('script_name', 'N/A')})"
}
else:
return {"status": "error", "message": "Proceso no encontrado en la lista de procesos activos"}
except Exception as e:
return {"status": "error", "message": f"Error terminando proceso: {str(e)}"}
def get_running_processes(self) -> List[Dict[str, Any]]:
"""Obtener lista de procesos en ejecución"""
try:
with self.process_lock:
processes = []
dead_pids = []
for pid, info in self.running_processes.items():
# Verificar si el proceso sigue vivo
try:
if sys.platform == "win32":
result = subprocess.run(
["tasklist", "/FI", f"PID eq {pid}"],
capture_output=True, text=True, check=False
)
if str(pid) not in result.stdout:
dead_pids.append(pid)
continue
else:
os.kill(pid, 0) # No mata el proceso, solo verifica si existe
except (ProcessLookupError, subprocess.SubprocessError):
dead_pids.append(pid)
continue
# Agregar información del proceso
project = self.get_python_project(info["project_id"])
processes.append({
"pid": pid,
"project_id": info["project_id"],
"project_name": project["name"] if project else "Proyecto no encontrado",
"script_name": info["script_name"],
"start_time": info["start_time"],
"execution_id": info["execution_id"],
"working_directory": info["working_directory"],
"is_background": info.get("is_background", False)
})
# Limpiar procesos muertos
for pid in dead_pids:
del self.running_processes[pid]
return processes
except Exception as e:
print(f"Error getting running processes: {e}")
return []
def get_markdown_files(self, project_id: str) -> List[Dict[str, Any]]:
"""Obtener archivos Markdown de un proyecto"""
try:
project = self.get_python_project(project_id)
if not project:
return []
project_dir = project["directory"]
if not os.path.isdir(project_dir):
return []
markdown_files = []
# Buscar archivos .md en el directorio del proyecto
for root, dirs, files in os.walk(project_dir):
# Excluir directorios comunes que no contienen documentación relevante
dirs[:] = [d for d in dirs if d not in ['.git', '__pycache__', '.vscode', 'node_modules']]
for filename in files:
if filename.lower().endswith('.md'):
file_path = os.path.join(root, filename)
relative_path = os.path.relpath(file_path, project_dir)
# Obtener información básica del archivo
try:
stat = os.stat(file_path)
size = stat.st_size
modified = datetime.fromtimestamp(stat.st_mtime).isoformat() + "Z"
# Intentar leer las primeras líneas para obtener el título
title = filename.replace('.md', '')
try:
with open(file_path, 'r', encoding='utf-8') as f:
first_line = f.readline().strip()
if first_line.startswith('#'):
title = first_line.lstrip('#').strip()
except Exception:
pass
markdown_files.append({
"filename": filename,
"relative_path": relative_path.replace('\\', '/'), # Normalizar separadores
"title": title,
"size": size,
"modified": modified
})
except Exception as e:
print(f"Error getting file info for {file_path}: {e}")
continue
# Ordenar por ruta relativa
return sorted(markdown_files, key=lambda x: x["relative_path"])
except Exception as e:
print(f"Error getting markdown files for project {project_id}: {e}")
return []
def read_markdown_file(self, project_id: str, relative_path: str) -> Dict[str, Any]:
"""Obtener contenido de un archivo Markdown"""
try:
project = self.get_python_project(project_id)
if not project:
return {"error": "Proyecto no encontrado"}
# Construir ruta completa y validar que esté dentro del proyecto
project_dir = os.path.abspath(project["directory"])
file_path = os.path.abspath(os.path.join(project_dir, relative_path))
# Verificar que el archivo esté dentro del directorio del proyecto (seguridad)
if not file_path.startswith(project_dir):
return {"error": "Acceso no autorizado al archivo"}
if not os.path.exists(file_path):
return {"error": "Archivo no encontrado"}
# Leer contenido del archivo
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Obtener información del archivo
stat = os.stat(file_path)
return {
"content": content,
"filename": os.path.basename(file_path),
"relative_path": relative_path,
"size": stat.st_size,
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat() + "Z"
}
except Exception as e:
return {"error": f"Error leyendo archivo: {str(e)}"}

View File

@ -620,7 +620,6 @@ class LauncherManager {
// === GESTIÓN DE GRUPOS (actualizada) ===
populateGroupForm(group) {
document.getElementById('group-id').value = group.id;
document.getElementById('group-name').value = group.name;
document.getElementById('group-description').value = group.description || '';
document.getElementById('group-category').value = group.category;
@ -630,7 +629,6 @@ class LauncherManager {
}
clearGroupForm() {
document.getElementById('group-id').value = '';
document.getElementById('group-name').value = '';
document.getElementById('group-description').value = '';
document.getElementById('group-category').value = 'Otros';
@ -642,7 +640,6 @@ class LauncherManager {
async saveGroup() {
const formData = {
id: document.getElementById('group-id').value,
name: document.getElementById('group-name').value,
description: document.getElementById('group-description').value,
category: document.getElementById('group-category').value,
@ -1380,6 +1377,15 @@ function switchTab(tabName) {
window.csharpLauncherManager.init();
}
}
// Inicializar Python launcher si es la primera vez
if (tabName === 'python') {
if (typeof initPythonLauncher === 'function') {
initPythonLauncher();
} else {
console.error('initPythonLauncher function not found! Make sure python_launcher.js is loaded.');
}
}
}
// Funciones para modales

1251
static/js/python_launcher.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -128,6 +128,17 @@
Launcher C#
</span>
</button>
<button id="python-tab" onclick="switchTab('python')"
class="tab-button py-2 px-1 border-b-2 font-medium text-sm">
<span class="flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M7 16h.01">
</path>
</svg>
Python Scripts
</span>
</button>
</nav>
</div>
</div>
@ -498,6 +509,155 @@
</div>
</div>
<!-- Tab Content: Python Scripts -->
<div id="python-content" class="tab-content hidden">
<!-- Python Project Controls -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Python Scripts - MCP Servers & Background Scripts</h2>
<button onclick="openPythonProjectEditor()"
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
onmousedown="if(!window.openPythonProjectEditor) alert('Python Launcher cargando...')">
Gestionar Proyectos
</button>
</div>
<!-- Project Selector -->
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Seleccionar Proyecto Python</label>
<div class="flex gap-2">
<div class="relative flex-1">
<select id="python-project-select" class="w-full p-3 border rounded-lg pl-12"
onchange="loadPythonScripts()">
<option value="">-- Seleccionar Proyecto --</option>
</select>
<div class="absolute left-3 top-1/2 transform -translate-y-1/2">
<div id="selected-python-project-icon"
class="w-6 h-6 bg-gray-200 rounded flex items-center justify-center text-sm">🐍
</div>
</div>
</div>
<button onclick="openPythonProjectInEditor('vscode')"
class="bg-blue-500 text-white px-4 py-3 rounded-lg hover:bg-blue-600" id="vscode-python-btn"
style="display: none;" title="Abrir proyecto en VS Code">
<img src="{{ url_for('static', filename='icons/vscode.png') }}" class="w-5 h-5"
alt="VS Code Icon">
</button>
<button onclick="openPythonProjectInEditor('cursor')"
class="bg-purple-500 text-white px-4 py-3 rounded-lg hover:bg-purple-600"
id="cursor-python-btn" style="display: none;" title="Abrir proyecto en Cursor">
<img src="{{ url_for('static', filename='icons/cursor.png') }}" class="w-5 h-5"
alt="Cursor Icon">
</button>
<button onclick="openPythonProjectFolder()"
class="bg-green-500 text-white px-4 py-3 rounded-lg hover:bg-green-600"
id="folder-python-btn" style="display: none;" title="Abrir carpeta del proyecto">
📁
</button>
<button onclick="copyPythonProjectPath()"
class="bg-gray-500 text-white px-4 py-3 rounded-lg hover:bg-gray-600"
id="copy-path-python-btn" style="display: none;" title="Copiar path del proyecto">
📋
</button>
</div>
</div>
<!-- Category Filter -->
<div class="mb-4">
<h3 class="text-sm font-medium mb-2">Filtrar por Categoría</h3>
<div class="flex flex-wrap gap-2">
<button class="python-category-btn active px-3 py-1 rounded-full text-sm border"
data-category="all" onclick="filterPythonByCategory('all')">
Todas
</button>
<button class="python-category-btn px-3 py-1 rounded-full text-sm border"
data-category="MCP Servers" onclick="filterPythonByCategory('MCP Servers')">
🔌 MCP Servers
</button>
<button class="python-category-btn px-3 py-1 rounded-full text-sm border"
data-category="Flask Apps" onclick="filterPythonByCategory('Flask Apps')">
🌐 Flask Apps
</button>
<button class="python-category-btn px-3 py-1 rounded-full text-sm border"
data-category="Scripts" onclick="filterPythonByCategory('Scripts')">
📜 Scripts
</button>
<button class="python-category-btn px-3 py-1 rounded-full text-sm border" data-category="Bots"
onclick="filterPythonByCategory('Bots')">
🤖 Bots
</button>
</div>
</div>
</div>
<!-- Python Favorites Panel -->
<div id="python-favorites-panel" class="mb-6 bg-green-50 border border-green-200 rounded-lg p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-semibold text-green-800">
⭐ Scripts Favoritos
</h3>
<span class="text-sm text-green-600" id="python-favorites-count">
0 favoritos
</span>
</div>
<div id="python-favorites-list" class="space-y-2">
<!-- Lista dinámica de favoritos Python -->
</div>
</div>
<!-- Python Scripts Grid -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Scripts Disponibles</h2>
<button onclick="openPythonScriptManager()"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
id="manage-python-scripts-btn" style="display: none;">
Gestionar Scripts
</button>
</div>
<div id="python-scripts-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Script cards dinámicos -->
</div>
</div>
<!-- Python Markdown Files Section -->
<div id="python-markdown-files-section" class="mb-6 bg-white p-6 rounded-lg shadow" style="display: none;">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">📄 Documentación (Markdown)</h2>
<span class="text-sm text-gray-500">Archivos .md en el directorio del proyecto</span>
</div>
<div id="python-markdown-files-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
<!-- Markdown files cards dinámicos -->
</div>
</div>
<!-- Running Processes Panel -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">🔄 Procesos en Ejecución</h3>
<button onclick="refreshPythonProcesses()" class="text-blue-500 hover:text-blue-700 text-sm">
Actualizar
</button>
</div>
<div id="python-running-processes" class="space-y-2">
<!-- Lista dinámica de procesos -->
</div>
</div>
<!-- History Panel -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">📋 Historial de Ejecuciones</h3>
<button onclick="clearPythonHistory()" class="text-red-500 hover:text-red-700 text-sm">
Limpiar Historial
</button>
</div>
<div id="python-history-list" class="space-y-2 max-h-64 overflow-y-auto">
<!-- Lista dinámica de historial -->
</div>
</div>
</div>
<!-- Modal: Editor de Proyectos C# -->
<div id="csharp-project-editor-modal"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
@ -749,16 +909,11 @@
<!-- Formulario de edición -->
<form id="group-form" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium mb-1">ID del Grupo</label>
<input type="text" id="group-id" class="w-full p-2 border rounded" required>
</div>
<!-- El ID se genera automáticamente basado en el nombre -->
<div>
<label class="block text-sm font-medium mb-1">Nombre</label>
<input type="text" id="group-name" class="w-full p-2 border rounded" required>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Descripción</label>
@ -1063,10 +1218,301 @@
</div>
</div>
<!-- Python Project Editor Modal -->
<div id="python-project-editor-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-screen overflow-y-auto">
<div class="p-6">
<h3 class="text-xl font-semibold mb-6">Gestionar Proyectos Python</h3>
<!-- Lista de proyectos existentes -->
<div class="mb-6">
<h4 class="font-medium mb-3">Proyectos Existentes</h4>
<div id="existing-python-projects-list"
class="space-y-2 max-h-40 overflow-y-auto border rounded p-2">
<!-- Lista dinámica -->
</div>
</div>
<!-- Formulario de edición -->
<form id="python-project-form" class="space-y-4"
onsubmit="event.preventDefault(); savePythonProject();">
<!-- El ID se genera automáticamente -->
<div>
<label class="block text-sm font-medium mb-1">Nombre</label>
<input type="text" id="python-project-name" class="w-full p-2 border rounded" required>
</div>
<div>
<label class="block text-sm font-medium mb-1">Descripción</label>
<textarea id="python-project-description" class="w-full p-2 border rounded h-20"></textarea>
</div>
<div class="grid grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium mb-1">Categoría</label>
<select id="python-project-category" class="w-full p-2 border rounded">
<option value="MCP Servers">🔗 MCP Servers</option>
<option value="Flask Apps">🌐 Flask Apps</option>
<option value="Scripts">📝 Scripts</option>
<option value="Bots">🤖 Bots</option>
<option value="Data Processing">📊 Data Processing</option>
<option value="Otros">📁 Otros</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">Versión</label>
<input type="text" id="python-project-version" class="w-full p-2 border rounded"
value="1.0">
</div>
<div>
<label class="block text-sm font-medium mb-1">Entorno Python</label>
<select id="python-project-python-env" class="w-full p-2 border rounded">
<option value="base">base</option>
</select>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Directorio</label>
<div class="flex gap-2">
<input type="text" id="python-project-directory" class="flex-1 p-2 border rounded"
required>
<button type="button" onclick="browsePythonProjectDirectory()"
class="bg-gray-500 text-white px-4 py-2 rounded">
Explorar
</button>
</div>
</div>
<div class="flex justify-end gap-3 pt-4">
<button type="button" onclick="closePythonProjectEditor()"
class="px-4 py-2 text-gray-600 hover:text-gray-800">
Cancelar
</button>
<button type="button" onclick="deletePythonProject()"
class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
id="delete-python-project-btn" style="display: none;">
Eliminar
</button>
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Guardar
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Python Script Manager Modal -->
<div id="python-script-manager-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-screen overflow-y-auto">
<div class="p-6">
<h3 class="text-xl font-semibold mb-6">Gestionar Scripts del Proyecto</h3>
<p class="text-gray-600 mb-4" id="python-script-manager-project-info">Selecciona un proyecto para
gestionar sus scripts</p>
<!-- Lista de scripts -->
<div class="space-y-3" id="python-script-manager-list">
<!-- Lista dinámica de scripts -->
</div>
<div class="flex justify-end gap-3 pt-4 mt-6 border-t">
<button type="button" onclick="closePythonScriptManager()"
class="px-4 py-2 text-gray-600 hover:text-gray-800">
Cerrar
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Python Script Metadata Editor Modal -->
<div id="python-script-metadata-editor-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-lg w-full max-h-screen overflow-y-auto">
<div class="p-6">
<h3 class="text-lg font-semibold mb-4">Editar Metadatos del Script Python</h3>
<form id="python-script-metadata-form" class="space-y-4"
onsubmit="event.preventDefault(); savePythonScriptMetadata();">
<input type="hidden" id="edit-python-meta-project-id">
<input type="hidden" id="edit-python-meta-script-name">
<div>
<label class="block text-sm font-bold mb-1">Nombre del Archivo</label>
<p id="edit-python-meta-filename-display"
class="text-sm text-gray-600 bg-gray-100 p-2 rounded border"></p>
</div>
<div>
<label for="edit-python-meta-display-name" class="block text-sm font-bold mb-2">Nombre a
Mostrar</label>
<input type="text" id="edit-python-meta-display-name" class="w-full p-2 border rounded"
required>
</div>
<div>
<label for="edit-python-meta-description" class="block text-sm font-bold mb-2">Descripción
Corta</label>
<input type="text" id="edit-python-meta-description" class="w-full p-2 border rounded">
</div>
<div>
<label for="edit-python-meta-long-description"
class="block text-sm font-bold mb-2">Descripción Larga / Ayuda</label>
<textarea id="edit-python-meta-long-description" class="w-full p-2 border rounded"
rows="5"></textarea>
<p class="text-xs text-gray-500 mt-1">Usa Markdown. Doble Enter para párrafo nuevo, dos
espacios + Enter para salto de línea simple.</p>
</div>
<div class="flex items-center">
<input type="checkbox" id="edit-python-meta-hidden" class="form-checkbox h-5 w-5 mr-2">
<label for="edit-python-meta-hidden" class="text-sm font-bold">Ocultar script (no aparecerá
en la lista de ejecución)</label>
</div>
<div class="flex justify-end gap-3 pt-4">
<button type="button" onclick="closePythonScriptMetadataEditor()"
class="px-4 py-2 text-gray-600 hover:text-gray-800">
Cancelar
</button>
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Guardar Cambios
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Python Script Description Modal -->
<div id="python-script-description-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[80vh] overflow-hidden">
<div class="p-6 border-b">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-semibold" id="python-desc-modal-script-name">Descripción del Script
</h3>
<p class="text-sm text-gray-600" id="python-desc-modal-script-file"></p>
</div>
<button onclick="closePythonScriptDescription()"
class="text-gray-500 hover:text-gray-700 text-2xl">&times;</button>
</div>
</div>
<div class="p-6 overflow-y-auto max-h-[60vh]">
<div id="python-script-description-content" class="prose prose-sm max-w-none">
<!-- Contenido markdown renderizado -->
</div>
</div>
<div class="p-4 border-t bg-gray-50 flex justify-end">
<button onclick="closePythonScriptDescription()"
class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600">
Cerrar
</button>
</div>
</div>
</div>
</div>
<!-- Python Markdown Viewer Modal -->
<div id="python-markdown-viewer-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden">
<div class="p-6 border-b">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-semibold" id="python-markdown-viewer-title">Documento Markdown</h3>
<p class="text-sm text-gray-600" id="python-markdown-viewer-path"></p>
</div>
<button onclick="closePythonMarkdownViewer()"
class="text-gray-500 hover:text-gray-700 text-2xl">&times;</button>
</div>
</div>
<div class="p-6 overflow-y-auto max-h-[75vh]">
<div id="python-markdown-viewer-content" class="prose prose-lg max-w-none">
<!-- Contenido markdown renderizado -->
</div>
</div>
<div class="p-4 border-t bg-gray-50 flex justify-end">
<button onclick="closePythonMarkdownViewer()"
class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600">
Cerrar
</button>
</div>
</div>
</div>
</div>
<!-- Python Script Options Modal -->
<div id="python-script-options-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
<div class="p-6">
<h3 class="text-lg font-semibold mb-4">Opciones de Ejecución - Python</h3>
<div id="python-script-info" class="mb-4 p-3 bg-gray-50 rounded">
<div class="font-medium" id="python-script-display-name"></div>
<div class="text-sm text-gray-600" id="python-script-description"></div>
</div>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium mb-1">
Argumentos de Línea de Comandos
</label>
<textarea id="python-script-args-input" class="w-full p-2 border rounded h-20"
placeholder="--port 8080 --debug"></textarea>
<p class="text-xs text-gray-500 mt-1">
Separar argumentos con espacios. Usar comillas para valores con espacios.
</p>
</div>
<div>
<label class="block text-sm font-medium mb-1">
Tipo de Ejecución
</label>
<div class="flex gap-2">
<label class="flex items-center">
<input type="radio" name="python-execution-type" value="false" checked class="mr-2">
<span class="text-sm">🖥️ Normal</span>
</label>
<label class="flex items-center">
<input type="radio" name="python-execution-type" value="true" class="mr-2">
<span class="text-sm">🚀 Background</span>
</label>
</div>
<p class="text-xs text-gray-500 mt-1">
Normal: Ejecuta y muestra salida. Background: Para servidores MCP, Flask apps, etc.
</p>
</div>
</div>
<div class="flex justify-end gap-3 mt-6">
<button onclick="closePythonScriptOptions()"
class="px-4 py-2 text-gray-600 hover:text-gray-800">
Cancelar
</button>
<button onclick="executePythonScriptWithOptions()"
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Ejecutar Script
</button>
</div>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
<script src="{{ url_for('static', filename='js/scripts.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/launcher.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/csharp_launcher.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/python_launcher.js') }}" defer></script>
<script>
// Inicializar markdown-it globalmente
window.markdownit = window.markdownit || markdownit;