From 930e578cec0831b240161442a644f0e9e12037ba Mon Sep 17 00:00:00 2001 From: Miguel Date: Sun, 9 Feb 2025 17:17:40 +0100 Subject: [PATCH] Corregido problema de eschemas en el mismo lugar --- .gitignore | 27 - __pycache__/config_manager.cpython-310.pyc | Bin 12295 -> 12233 bytes app.py | 6 +- backend/script_groups/EmailCrono/config.json | 6 + .../config/__pycache__/config.cpython-310.pyc | Bin 0 -> 1808 bytes .../EmailCrono/config/beautify_rules.json | 171 +++ .../script_groups/EmailCrono/config/config.py | 44 + backend/script_groups/EmailCrono/data.json | 5 + .../script_groups/EmailCrono/description.json | 6 + .../EmailCrono/esquema_group.json | 15 + .../EmailCrono/esquema_work.json | 10 + .../__pycache__/mensaje_email.cpython-310.pyc | Bin 0 -> 3883 bytes .../EmailCrono/models/mensaje_email.py | 145 +++ .../attachment_handler.cpython-310.pyc | Bin 0 -> 897 bytes .../__pycache__/beautify.cpython-310.pyc | Bin 0 -> 6207 bytes .../__pycache__/email_parser.cpython-310.pyc | Bin 0 -> 7200 bytes .../forward_handler.cpython-310.pyc | Bin 0 -> 2999 bytes .../markdown_handler.cpython-310.pyc | Bin 0 -> 1185 bytes .../EmailCrono/utils/attachment_handler.py | 33 + .../EmailCrono/utils/beautify.py | 225 ++++ .../EmailCrono/utils/email_parser.py | 295 ++++++ .../EmailCrono/utils/markdown_handler.py | 39 + .../script_groups/EmailCrono/work_dir.json | 3 + backend/script_groups/EmailCrono/x1.py | 101 ++ commands.sh | 3 - config_manager.py | 21 +- data/esquema_general.json | 4 + services/__init__.py | 0 services/excel/__init__.py | 0 services/language/__init__.py | 0 services/llm/__init__.py | 0 services/llm/__pycache__/base.cpython-310.pyc | Bin 0 -> 1017 bytes .../__pycache__/grok_service.cpython-310.pyc | Bin 0 -> 2371 bytes .../__pycache__/llm_factory.cpython-310.pyc | Bin 0 -> 1259 bytes .../ollama_service.cpython-310.pyc | Bin 0 -> 2007 bytes .../openai_service.cpython-310.pyc | Bin 0 -> 2552 bytes services/translation/__init__.py | 0 static/css/styles.css | 104 ++ static/js/scripts.js | 847 +++++++++++++++ templates/index.html | 985 +----------------- utils/__init__.py | 0 41 files changed, 2084 insertions(+), 1011 deletions(-) delete mode 100644 .gitignore create mode 100644 backend/script_groups/EmailCrono/config.json create mode 100644 backend/script_groups/EmailCrono/config/__pycache__/config.cpython-310.pyc create mode 100644 backend/script_groups/EmailCrono/config/beautify_rules.json create mode 100644 backend/script_groups/EmailCrono/config/config.py create mode 100644 backend/script_groups/EmailCrono/data.json create mode 100644 backend/script_groups/EmailCrono/description.json create mode 100644 backend/script_groups/EmailCrono/esquema_group.json create mode 100644 backend/script_groups/EmailCrono/esquema_work.json create mode 100644 backend/script_groups/EmailCrono/models/__pycache__/mensaje_email.cpython-310.pyc create mode 100644 backend/script_groups/EmailCrono/models/mensaje_email.py create mode 100644 backend/script_groups/EmailCrono/utils/__pycache__/attachment_handler.cpython-310.pyc create mode 100644 backend/script_groups/EmailCrono/utils/__pycache__/beautify.cpython-310.pyc create mode 100644 backend/script_groups/EmailCrono/utils/__pycache__/email_parser.cpython-310.pyc create mode 100644 backend/script_groups/EmailCrono/utils/__pycache__/forward_handler.cpython-310.pyc create mode 100644 backend/script_groups/EmailCrono/utils/__pycache__/markdown_handler.cpython-310.pyc create mode 100644 backend/script_groups/EmailCrono/utils/attachment_handler.py create mode 100644 backend/script_groups/EmailCrono/utils/beautify.py create mode 100644 backend/script_groups/EmailCrono/utils/email_parser.py create mode 100644 backend/script_groups/EmailCrono/utils/markdown_handler.py create mode 100644 backend/script_groups/EmailCrono/work_dir.json create mode 100644 backend/script_groups/EmailCrono/x1.py delete mode 100644 commands.sh create mode 100644 data/esquema_general.json create mode 100644 services/__init__.py create mode 100644 services/excel/__init__.py create mode 100644 services/language/__init__.py create mode 100644 services/llm/__init__.py create mode 100644 services/llm/__pycache__/base.cpython-310.pyc create mode 100644 services/llm/__pycache__/grok_service.cpython-310.pyc create mode 100644 services/llm/__pycache__/llm_factory.cpython-310.pyc create mode 100644 services/llm/__pycache__/ollama_service.cpython-310.pyc create mode 100644 services/llm/__pycache__/openai_service.cpython-310.pyc create mode 100644 services/translation/__init__.py create mode 100644 static/css/styles.css create mode 100644 static/js/scripts.js create mode 100644 utils/__init__.py diff --git a/.gitignore b/.gitignore deleted file mode 100644 index a5a3c28..0000000 --- a/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -# Python cache files -__pycache__/ -*.py[cod] - -# Environment directories -venv/ -env/ -.env/ - -# IDE configurations -.vscode/ -.idea/ - -# Logs and data files -data/log.txt -data/data.json - -# Allow script groups and their configurations -!backend/script_groups/ -!backend/script_groups/*/ -!backend/script_groups/*/*.py -!backend/script_groups/*/schema.json -!backend/script_groups/*/esquema.json -!backend/script_groups/*/description.json - -# But ignore working directory configurations -backend/script_groups/*/work_dir.json diff --git a/__pycache__/config_manager.cpython-310.pyc b/__pycache__/config_manager.cpython-310.pyc index 88e9b57ee3a577d9acf659dc5b944a5488f815ad..a7dc8c1a6fffdc00a5b0c8a97f5e30eb884ada85 100644 GIT binary patch delta 1594 zcmY*ZO>7%Q6rML;ubtSAW5L}@|&Nty--O@Cr1vk7*cwL4xX zZCSTOkq96~93E;Vnu zJZt%Fj|hmM2;tpmvipSXHMMo$1~$ex6HOY^*rEs2YLpGIasmI1b`OR{^D+nK#78K8 z+4D)Xs#y*Bu&vvgh>DhFC+11Kt*>RchMAVc;CH~PK42GORz$>94}6Pom0jbD0BpU2 zF{ZQQs)DNTqHlC zrQ{MLim2?gLPxv1z#qw~SEB!U4QX}qdORv=wc$Hg?O0^ad6LFV;W}d`^{jVVh5SecH?~jjSl-Orx%qiiLut3giO8PNEy7VO69#RTRdCl0t5GooDf) zavKj$%3@YbWy{vYv?=HCSbA&l1B%xuwOqi3^wa8*y`8=T@xzpyI_!RWQJNjYi@hD{ zR9AZkyi3aPQSE;37XY7PrSGJBC)Nag@)BRhzxw*&3byT9gwwdPt8efSW!g_LN-#z6 qF2OVb2_{Pf6$0v%L;~7RUcvkQ3GD3ehV8hozo36e#&dY5|JZ+KY=TSx delta 1651 zcmZWpO>7%Q6y7)cV<)lW|NJ*;LW=ABD*UBLZD>PN(FCUv%Wz%UEcP60x-eb1MU7- zMLvroAeD9Tcf29K6v*886(lMDqbXTXgk7Le0!o^t0$4!hw+K68l9arwSW-%*N{JL? zd#_n?33A<4F%@IRD}tp7*68Di%OZj&Kfyay6^#?BV%X>vRF|NxS|m3~-AReo+$OWq zj8v8U|57VYL{)w=*m%xvj@>ccmP~7D9rf(UAGQ`0vu3x}Qc6%z%{ogpJw?K{E!9x= z&#IATB_dI;A}LZ)2CgbW+^m4gF80ofNyD) z3J*2_B>Y|yKgX*`rhoGfkP0r4@?kSTL%c6^DC8};wp=r!2V6H`LyCp@La68INg>%k z9zAz$P0u6-Qn{>=ugfjJxxZoOP|_*>ZK#9)9$JNVeyu+wbWes~&>Y?Q-SBwNw8$bP zNZhg?Jj4&=Z8rpk6|^k{JdbzW6}btC*UdJ(9F7&$0C+S!!$x0b`;c)*6^DR(b-1Ylmyg5Ouc^d z`^wZCOdoAF{g$f;pGkK}QD{UpXYVwt3BAMvIEvV-tw>A$)P<`wVd^{)YUMqRoe@-r zokw^Mp(2Lf*cYA5d(Rr{iFA@4PV4J>`aHXY8M6o%5iTIS&cAECu3biRk&iSDH_i() z=4KJ}*!pQ`#rX$KV{n)M)KsXW=vWf4Ec2nRrZQ`O79hdzwHy<&KWZ5Y+9h002`8C% zwLY&d3(BMNLhA{5$v)ri1e@4XbYm?OQ<>8SmOq1THEHUZ99t3gen)mQOZ=C%Q5D^P zmpcx{0j7ASJyvIr!kGduwU0-ty2p|SuO6Rw%^u>?a@JT$t_I3o9T5Vf{8(pi+ZY}! zr*(rxkyJg6IKR<30bS*Xog)O|ytezWxB5wQe7O6JniZ_$<#)Ru5~rVvAQz((VDc}b zKf^TNJQ{^XUOL)5SHQ#EOi$2s($M#mtJ!p#VFzprp%25gOgGnwx}32tkC`_{rWj z@l=<4ht&6k9=pYV7&s0?Tpdhn@1tXlZwyZV2QUGKp#T5? diff --git a/app.py b/app.py index 2ddc32a..f4ea53b 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,12 @@ -from flask import Flask, render_template, request, jsonify +from flask import Flask, render_template, request, jsonify, url_for from flask_sock import Sock from config_manager import ConfigurationManager import os import json # Added import -app = Flask(__name__) +app = Flask( + __name__, static_url_path="", static_folder="static", template_folder="templates" +) sock = Sock(app) config_manager = ConfigurationManager() diff --git a/backend/script_groups/EmailCrono/config.json b/backend/script_groups/EmailCrono/config.json new file mode 100644 index 0000000..f9b1ad9 --- /dev/null +++ b/backend/script_groups/EmailCrono/config.json @@ -0,0 +1,6 @@ +{ + "input_dir": "C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\Emails", + "output_dir": "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\04-InLavoro\\HENKEL\\93040 - HENKEL - BowlingGreen\\Description\\HENKEL - ALPLA - AUTEFA - Batch Data", + "cronologia_file": "cronologia.md", + "attachments_dir": "adjuntos" +} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/config/__pycache__/config.cpython-310.pyc b/backend/script_groups/EmailCrono/config/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b771230be7a4225f7384cf291ce9571a1ffd135b GIT binary patch literal 1808 zcmZ`(OK;mo5Z+m?Xj;}Iwv#^66b0P04U1YxPA!U}C}6Y+P(Uc)Lr}p0L2xLUfSGZBcHn8Si(AW@JZ==zRX*$Dc{C^n5?l!X|SW{q2FUmBIiqrO&T zXeqQu>heB*efE>ip2bm-_AWs^8HiVJQO*UESF4&l;kVcZX=qhfs! z`nFD`*rgMiXfBxa`8y<9Af*I;-M{Ltf&=Nn-G43)*TtyNV1rm21z}DXM5p1Z3Y9UlG8MD_}FU1oxe{;C2b_r}~f#YNqRJriTy4O;RM_e5G&m7O(0LNEn6F z@nn^|W2tX2mYX~L>lmtet%PC?#tpXKAhC2cQ%k=~ zf+(6LslLbGwmf%v_J6S+a;#fT!>PXEoLq3B0qXCI+m*n#4qqF+_Q?Vxw$HwsEIwy( zZ4_IbYJ)$f8ynuY4t0Id+!TJDe{g9yj)TC|gJ7KDbj0&U5ImWNqjE=c1KLj1%uW3% z3+v|#o^@Do-W6ecml?zTn={y+sDr1x77fR98m{JD>IH6$vI4wNF<~vT9pimd#`P_e L+;)Wr9yR~Jy+DO= literal 0 HcmV?d00001 diff --git a/backend/script_groups/EmailCrono/config/beautify_rules.json b/backend/script_groups/EmailCrono/config/beautify_rules.json new file mode 100644 index 0000000..81c9fcf --- /dev/null +++ b/backend/script_groups/EmailCrono/config/beautify_rules.json @@ -0,0 +1,171 @@ +{ + "__documentation": { + "__format": "Las reglas siguen el siguiente formato:", + "pattern": "Patrón a buscar - puede ser texto o regex", + "replacement": "Texto que reemplazará al patrón (puede estar vacío)", + "action": "Tipo de acción: replace, remove_line, remove_block, add_before, add_after", + "type": "Cómo interpretar el patrón: string, regex, left, right, substring", + "priority": "Orden de ejecución (menor número = mayor prioridad)" + }, + "__examples": { + "replace": "Reemplaza texto: reemplaza cada coincidencia por el replacement", + "remove_line": "Elimina línea: elimina la línea completa si encuentra el patrón", + "remove_block": "Elimina bloque: elimina desde el inicio hasta el fin del patrón con .....", + "add_before": "Agrega antes: inserta el replacement antes de la línea con el patrón", + "add_after": "Agrega después: inserta el replacement después de la línea con el patrón" + }, + "rules": [ + { + "__comment": "Reemplaza non-breaking space por espacio normal", + "pattern": "\u00a0", + "replacement": " ", + "action": "replace", + "type": "string", + "priority": 1 + }, + { + "__comment": "Elimina marcador de mensaje original", + "pattern": "--- Messaggio originale ---", + "replacement": "***", + "action": "remove_line", + "type": "substring", + "priority": 2 + }, + { + "__comment": "Elimina firma de dispositivo móvil", + "pattern": "(?m)^Sent from my.*$", + "replacement": "", + "action": "remove_line", + "type": "regex", + "priority": 2 + }, + { + "__comment": "Elimina aviso medioambiental", + "pattern": "(?m)^Please take care of the environment.*$", + "replacement": "", + "action": "remove_line", + "type": "regex", + "priority": 2 + }, + { + "__comment": "Elimina aviso de mensaje automático", + "pattern": "(?m)^This message is from an.*$", + "replacement": "", + "action": "remove_line", + "type": "regex", + "priority": 2 + }, + { + "__comment": "Elimina aviso de confidencialidad en italiano", + "pattern": "eventuali allegati sono confidenziali", + "replacement": "", + "action": "remove_line", + "type": "substring", + "priority": 2 + }, + { + "__comment": "Elimina aviso de confidencialidad en inglés", + "pattern": "any attachments are confidential", + "replacement": "", + "action": "remove_line", + "type": "substring", + "priority": 2 + }, + { + "__comment": "Elimina solicitud de LinkedIn", + "pattern": "Please sign up on our Linkedin", + "replacement": "", + "action": "remove_line", + "type": "left", + "priority": 2 + }, + { + "__comment": "Elimina aviso de no compartir contenido", + "pattern": "di non copiare o condividere i contenuti con nessuno", + "replacement": "", + "action": "remove_line", + "type": "substring", + "priority": 2 + }, + { + "__comment": "Elimina líneas de email individual", + "pattern": "(?m)^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "replacement": "", + "action": "remove_line", + "type": "regex", + "priority": 2 + }, + { + "__comment": "Elimina líneas con múltiples emails", + "pattern": "(?m)(?:^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\\s*(?:;\\s*)?$|^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\\s*;\\s*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}.*$)", + "replacement": "", + "action": "remove_line", + "type": "regex", + "priority": 2 + }, + { + "__comment": "Elimina línea de teléfono", + "pattern": "Phone:", + "replacement": "", + "action": "remove_line", + "type": "left", + "priority": 2 + }, + { + "__comment": "Elimina línea de móvil", + "pattern": "Mobile:", + "replacement": "", + "action": "remove_line", + "type": "left", + "priority": 2 + }, + { + "__comment": "Elimina línea de CC", + "pattern": "Cc:", + "replacement": "", + "action": "remove_line", + "type": "left", + "priority": 2 + }, + { + "__comment": "Elimina línea de destinatario (italiano)", + "pattern": "A:", + "replacement": "", + "action": "remove_line", + "type": "left", + "priority": 2 + }, + { + "__comment": "Elimina línea de destinatario", + "pattern": "To:", + "replacement": "", + "action": "remove_line", + "type": "left", + "priority": 2 + }, + { + "__comment": "Agrega separador antes del asunto", + "pattern": "Subject: ", + "replacement": "***", + "action": "add_before", + "type": "left", + "priority": 3 + }, + { + "__comment": "Elimina firma corporativa", + "pattern": "Strada Isolanda.....Website:www.vetromeccanica.it", + "replacement": "", + "action": "remove_block", + "type": "string", + "priority": 4 + }, + { + "__comment": "Elimina aviso legal largo", + "pattern": "IMPORTANT NOTICE: This message may.....without retaining any copy", + "replacement": "", + "action": "remove_block", + "type": "string", + "priority": 4 + } + ] +} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/config/config.py b/backend/script_groups/EmailCrono/config/config.py new file mode 100644 index 0000000..162a8b1 --- /dev/null +++ b/backend/script_groups/EmailCrono/config/config.py @@ -0,0 +1,44 @@ +# config/config.py +import json +import os + +class Config: + def __init__(self, config_file='config.json'): + self.config_file = config_file + self.config = self._load_config() + + def _load_config(self): + if not os.path.exists(self.config_file): + default_config = { + 'input_dir': '.', + 'output_dir': '.', + 'cronologia_file': 'cronologia.md', + 'attachments_dir': 'adjuntos' + } + self._save_config(default_config) + return default_config + + with open(self.config_file, 'r', encoding='utf-8') as f: + return json.load(f) + + def _save_config(self, config): + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=4) + + def get_input_dir(self): + return self.config.get('input_dir', '.') + + def get_output_dir(self): + return self.config.get('output_dir', '.') + + def get_cronologia_file(self): + return os.path.join( + self.get_output_dir(), + self.config.get('cronologia_file', 'cronologia.md') + ) + + def get_attachments_dir(self): + return os.path.join( + self.get_output_dir(), + self.config.get('attachments_dir', 'adjuntos') + ) \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/data.json b/backend/script_groups/EmailCrono/data.json new file mode 100644 index 0000000..7cac4f7 --- /dev/null +++ b/backend/script_groups/EmailCrono/data.json @@ -0,0 +1,5 @@ +{ + "attachments_dir": "adjuntos", + "cronologia_file": "cronologia.md", + "output_dir": "C:\\\\Users\\\\migue\\\\OneDrive\\\\Miguel\\\\Obsidean\\\\Trabajo\\\\VM\\\\04-InLavoro\\\\HENKEL\\\\93040 - HENKEL - BowlingGreen\\\\Description\\\\HENKEL - ALPLA - AUTEFA - Batch Data" +} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/description.json b/backend/script_groups/EmailCrono/description.json new file mode 100644 index 0000000..1160d57 --- /dev/null +++ b/backend/script_groups/EmailCrono/description.json @@ -0,0 +1,6 @@ +{ + "name": "Desempaquetado de Emails EML", + "description": "This script processes email files (.eml) into a chronological narrative in Markdown format, optimized for processing with Large Language Models (LLMs). It extracts essential information from emails while removing unnecessary metadata, creating a clean, temporal narrative that can be easily analyzed. ", + "version": "1.0", + "author": "Unknown" +} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/esquema_group.json b/backend/script_groups/EmailCrono/esquema_group.json new file mode 100644 index 0000000..c6a5779 --- /dev/null +++ b/backend/script_groups/EmailCrono/esquema_group.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "properties": { + "attachments_dir": { + "type": "string", + "title": "Directorio de adjuntos", + "description": "adjuntos" + }, + "cronologia_file": { + "type": "string", + "title": "Nombre del archivo de cronologia.md", + "description": "" + } + } +} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/esquema_work.json b/backend/script_groups/EmailCrono/esquema_work.json new file mode 100644 index 0000000..76dd02a --- /dev/null +++ b/backend/script_groups/EmailCrono/esquema_work.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "output_dir": { + "type": "string", + "title": "Directorio de destino de la cronologia", + "description": "" + } + } +} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/models/__pycache__/mensaje_email.cpython-310.pyc b/backend/script_groups/EmailCrono/models/__pycache__/mensaje_email.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ddc2ddf0af9b3e2e9db614528fc417396be7110d GIT binary patch literal 3883 zcmaJ^OOM;u73RHsh~h}{cs#BjuwBN^0F@m%DS{$_;o33m#OcD5qHa+{qp85*y`JG% zA~lzkA1r5OcU5H5ZE8RQy6K{;0$pV5f5B~6*>^SVqTjicNsZ$_6OGT1(mTk6A=tp^Rwe5)wE2J!d6zTlGY96->RLm)Ycu1a zAuM6PHiZ3(cOAhV8*S%%tYK`nS?P)(lUX#BZC=(#fl6f{geuSKDvkBCq(7GlY1Z-I z;!K<9l?I)o%f*`D!hFrTX0>nE5{{_7HoCTOMIEIhJh6hZCK{rN(iJUn3T0iaiqj}P zea16oa_WNq?9%JbU^hGr(TcNCl?5~DeLvi$pxYhfmN?u~eB<(#8RW!=d zo_@&tDv6WcFcET)_J-99_a!~?&C$!!?f0WN%KClUA#EUQum)@C)MEMB32)q>`yG z(~%6LV1VjNY6M}Fr2g>jcY`b%p?5JiCr#*7nvlC&zw15krJe24I5KzlpXfVeQfO$Z>OZ{p{0^`}``gA#HNc?ma5-A~U*%oW@ zHD{5Apt{4FE3iw zl&LER&CHwtHQ^K%FoQ9{pzJAQ#z9l?i9^P9=xCXm;DrMjr*I}USU#WfS-oD>wG^0O zA%$I`0gX*{7H2G(*LHI2L2%2@>%WMfMM0LJYTiTUe4&ydm5*V9D6L=9Q2ndjU72Yq z?gJPTe55NmZ+b_pnLEAs_aA?7c*H%g?E0HmYHVk#yivngGe;uG?06-5#|&LHd7 ziAyy`-NxMT2&%HF$$Dm%N!P2IdWa82*91AAfUaDDe;fwtsYss3UiDS_~%$)L1quNPw~ew5oV?W5fEPvA}D@EtRULAe~U#F%EcUY9V@6C$a2#M z7OTj%wnFkFgJd@fu9d9F`Sy0(u3Qq4z9Mz5&>&ATXT%QbOK<-4% z*#-2nCZieEXakk<3y?Ia}X&k zg;0yR05TYd6@dOgn{x*UqC`kE!;)H$u%M2l zpsFx_J~b!i6smlz&r<6cp%B=djgq8Iawy3+!?PE-J$dy%z$A7pkbZ6AYF}<;^@#`7 zU4e?SNke;Rcl<|eyP$^!rT9dv{@d^3C`=Y)GOQv@e776>Qm~f@5vV;+Qp~9OvSsc zMd+@>^zNCtKY;}eg8@DkMAB`^fuzGHX(i-Q43OyhNeeiIZC)a!q&@4kE2e8zx?=su zbJxuL;-h2VL^qc{5%AHn*PczkJ5RlK=n! literal 0 HcmV?d00001 diff --git a/backend/script_groups/EmailCrono/models/mensaje_email.py b/backend/script_groups/EmailCrono/models/mensaje_email.py new file mode 100644 index 0000000..b170920 --- /dev/null +++ b/backend/script_groups/EmailCrono/models/mensaje_email.py @@ -0,0 +1,145 @@ +# models/mensaje_email.py +import re +import hashlib +from datetime import datetime +from email.utils import parseaddr, parsedate_to_datetime + +class MensajeEmail: + def __init__(self, remitente, fecha, contenido, subject=None, adjuntos=None): + self.remitente = self._estandarizar_remitente(remitente) + self.fecha = self._estandarizar_fecha(fecha) + self.subject = subject if subject else 'Sin Asunto' + self.contenido = self._limpiar_contenido(contenido) + self.adjuntos = adjuntos if adjuntos else [] + self.hash = self._generar_hash() + + def _formatear_subject_para_link(self, subject): + """ + Formatea el subject para usarlo como ancla en links de Obsidian + Remueve caracteres especiales y espacios múltiples + """ + if not subject: + return "Sin-Asunto" + # Eliminar caracteres especiales y reemplazar espacios con guiones + formatted = re.sub(r'[^\w\s-]', '', subject) + formatted = re.sub(r'\s+', '-', formatted.strip()) + return formatted + + def _limpiar_contenido(self, contenido): + if not contenido: + return "" + + # Eliminar líneas de metadatos + lines = contenido.split('\n') + cleaned_lines = [] + + for line in lines: + # Skip metadata lines + if line.strip().startswith(('Da: ', 'Inviato: ', 'A: ', 'From: ', 'Sent: ', 'To: ')) or line.strip().startswith('Oggetto: '): + continue + # Limpiar espacios múltiples dentro de cada línea, pero mantener la línea completa + cleaned_line = re.sub(r' +', ' ', line) + cleaned_lines.append(cleaned_line) + + # Unir las líneas preservando los saltos de línea + text = '\n'.join(cleaned_lines) + + # Limpiar la combinación específica de CRLF+NBSP+CRLF + text = re.sub(r'\r?\n\xa0\r?\n', '\n', text) + + # Reemplazar CRLF por LF + text = text.replace('\r\n', '\n') + + # Reemplazar CR por LF + text = text.replace('\r', '\n') + + # Reemplazar 3 o más saltos de línea por dos + text = re.sub(r'\n{3,}', '\n\n', text) + + # Eliminar espacios al inicio y final del texto completo + return text.strip() + + def to_markdown(self): + # Hash con caracteres no título + hash_line = f"+ {self.hash}\n\n" + + # Subject como título + subject_line = f"### {self.subject if self.subject else 'Sin Asunto'}\n\n" + + # Fecha en formato legible + fecha_formato = self.fecha.strftime('%d-%m-%Y') + fecha_line = f"- {fecha_formato}\n\n" + + # Contenido del mensaje + md = f"{hash_line}{subject_line}{fecha_line}" + md += self.contenido + "\n\n" + + # Adjuntos si existen + if self.adjuntos: + md += "### Adjuntos\n" + for adj in self.adjuntos: + md += f"- [[{adj}]]\n" + md += "---\n\n" + return md + + def get_index_entry(self): + """ + Genera una entrada de lista para el índice + """ + fecha_formato = self.fecha.strftime('%d-%m-%Y') + subject_link = self._formatear_subject_para_link(self.subject) + return f"- {fecha_formato} - {self.remitente} - [[cronologia#{self.subject}|{subject_link}]]" + + def _estandarizar_remitente(self, remitente): + if 'Da:' in remitente: + remitente = remitente.split('Da:')[1].split('Inviato:')[0] + elif 'From:' in remitente: + remitente = remitente.split('From:')[1].split('Sent:')[0] + + nombre, email = parseaddr(remitente) + if not nombre and email: + nombre = email.split('@')[0] + elif not nombre and not email: + nombre_match = re.search(r'([A-Za-z\s]+)\s*<', remitente) + if nombre_match: + nombre = nombre_match.group(1) + else: + return "Remitente Desconocido" + + nombre = re.sub(r'[<>:"/\\|?*]', '', nombre.strip()) + nombre = nombre.encode('ascii', 'ignore').decode('ascii') + return nombre + + def _estandarizar_fecha(self, fecha): + if isinstance(fecha, str): + try: + return parsedate_to_datetime(fecha) + except: + return datetime.now() + return fecha + + def _generar_hash(self): + """ + Genera un hash único para el mensaje basado en una combinación de campos + que identifican únicamente el mensaje + """ + # Limpiar y normalizar el contenido para el hash + # Para el hash, sí normalizamos completamente los espacios + contenido_hash = re.sub(r'\s+', ' ', self.contenido).strip() + + # Normalizar el subject + subject_normalizado = re.sub(r'\s+', ' ', self.subject if self.subject else '').strip() + + # Crear una cadena con los elementos clave del mensaje + elementos_hash = [ + self.remitente.strip(), + self.fecha.strftime('%Y%m%d%H%M'), # Solo hasta minutos para permitir pequeñas variaciones + subject_normalizado, + contenido_hash[:500] # Usar solo los primeros 500 caracteres del contenido normalizado + ] + + # Unir todos los elementos con un separador único + texto_hash = '|'.join(elementos_hash) + + # Generar el hash + return hashlib.md5(texto_hash.encode()).hexdigest() \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/utils/__pycache__/attachment_handler.cpython-310.pyc b/backend/script_groups/EmailCrono/utils/__pycache__/attachment_handler.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a926e51c4aa48e4e60a775be238fe0790b742e03 GIT binary patch literal 897 zcmY+DPjAyO6u|wjY1(G(Ksyc&h)WR~0tq3FX&@$!Oh`jQgiMuNdu>XZM7F~=l$^#D z?E_3mtq=#klCPZh6_8*&XE4gM-ix2#^LsxhPPVerMj)#jU&dc4Lf@UUECigVp!zl# zfdo!ac=^O3u6+^`K?MDXLMrec>a%mmM7txm^RIMZ;)Cvh>bn@B63x*9Riq@^7ibR| zJSPGR@E5cqhECbMQDX2&MQhJ1=>n^wMAN3Af_=gjGyc3)vIU;Ep;E(iN-7#BFY@MOc%#oID;XOjj`Fg+RY692w$}7Ej_umN?K(Nf^x&ztie`K)M_jf-8 zZ@0ELZ{3Tc^2wcjiwCdz*m^>aG9fKd!*h70$dbchzh{H7G=qaUktv_KptMvMLrXGk zSNW^2Qjb&LbQ_GZuBwy!A zY~+!#jY9>Ke#bU)u8g#Sh}D3LX^|S}=B3$esAR}gVR+aZWvP*AEV4mb$oE;;cD*j= z79WJ&+O44^Bw9A)T87lM!SOew*5QOd9*fQBrOJ-w2pU9tBNgXHN6%+GPIgq5W>I0{ zL`U2hKAOy+&S1h*kw~?XAKTuz;7af-LeE{s2Vi{6V01|r2PDAGuHp3ypEA5k7!L3{ z_HpnlU~9O2!RYEwM%7K&)@RE7JF4f5yWRC_4Z37*K*0K2vrH6;+;-ndyIdb?LRIn` D4F>6) literal 0 HcmV?d00001 diff --git a/backend/script_groups/EmailCrono/utils/__pycache__/beautify.cpython-310.pyc b/backend/script_groups/EmailCrono/utils/__pycache__/beautify.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c12d078abcd37258aa860051a67aca56626eb341 GIT binary patch literal 6207 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 AkpKVy literal 0 HcmV?d00001 diff --git a/backend/script_groups/EmailCrono/utils/__pycache__/email_parser.cpython-310.pyc b/backend/script_groups/EmailCrono/utils/__pycache__/email_parser.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b36b5f2edc41c38dbdd99a4ae90900bc2154d8d4 GIT binary patch literal 7200 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 AE&u=k literal 0 HcmV?d00001 diff --git a/backend/script_groups/EmailCrono/utils/__pycache__/markdown_handler.cpython-310.pyc b/backend/script_groups/EmailCrono/utils/__pycache__/markdown_handler.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f80d80427828c4cd1b898a730a91b8e8adddddd GIT binary patch literal 1185 zcmaJ>&5j&35VqaF)6=sFB!?vIA<}B3bd(*4h!8}pg(!=ZLlTe{31!sL$h5t)J@nt& zp8QPQ5>mo3LcGDWIPnO)ijQ#G3lD%0OSyN0_Jpk}SCy;EUzOcmk46!K@!RB=`R^`5 ze^}-GAfS8!B<}!Yh~WZFPggv}pp$}>WJ<8LIfYZYQx{~Hd2i6vV|X7W{xL+N#|bte zql&A%91U0)Va{9U zr?@4WtlbW^&bNTWh56dSs0wc*dHp%Y$j&D~+F5(d?QrXAcMm;%(0bZi`yG-WAKg;H zI%H?M3KoOTX?^VrrTul(q10)EHf$X=T*o?Ken;8?;EGtFoo7hoedeIUDQSmo%tAJJ zgLG_j!--Lc_t0zHMtW#1t)uaJq&9S@2inmQ8@>fh7#-cRl_l|GgT=~Qs)OxMZPR#_ z)hw^(D{p&yJBn7zB64i~kN-J2IRQYPR3dK@ zV#wqp<50Hv6^SA2|MeNWn|>|ompp^x()|p)luY{zzAx&kPM0b#WLi@3J*%HrvqM_3 zf{RJ>(nLE&E>%{S&8|fw?RlGM^!@k**PaNYarn|lu*8h!R9phuBeZeTu%K{L0q$$20h{#nh3nI3Y F{0kEVG=2a8 literal 0 HcmV?d00001 diff --git a/backend/script_groups/EmailCrono/utils/attachment_handler.py b/backend/script_groups/EmailCrono/utils/attachment_handler.py new file mode 100644 index 0000000..80679cb --- /dev/null +++ b/backend/script_groups/EmailCrono/utils/attachment_handler.py @@ -0,0 +1,33 @@ +# utils/attachment_handler.py +import os +import hashlib +import re + +def guardar_adjunto(parte, dir_adjuntos): + nombre = parte.get_filename() + if not nombre: + return None + + nombre = re.sub(r'[<>:"/\\|?*]', '_', nombre) + ruta = os.path.join(dir_adjuntos, nombre) + + if os.path.exists(ruta): + contenido_nuevo = parte.get_payload(decode=True) + hash_nuevo = hashlib.md5(contenido_nuevo).hexdigest() + + with open(ruta, 'rb') as f: + hash_existente = hashlib.md5(f.read()).hexdigest() + + if hash_nuevo == hash_existente: + return ruta + + base, ext = os.path.splitext(nombre) + i = 1 + while os.path.exists(ruta): + ruta = os.path.join(dir_adjuntos, f"{base}_{i}{ext}") + i += 1 + + with open(ruta, 'wb') as f: + f.write(parte.get_payload(decode=True)) + + return ruta diff --git a/backend/script_groups/EmailCrono/utils/beautify.py b/backend/script_groups/EmailCrono/utils/beautify.py new file mode 100644 index 0000000..fb08ec3 --- /dev/null +++ b/backend/script_groups/EmailCrono/utils/beautify.py @@ -0,0 +1,225 @@ +import json +import re +from pathlib import Path +from collections import defaultdict +from enum import Enum + +class PatternType(Enum): + REGEX = "regex" + STRING = "string" + LEFT = "left" + RIGHT = "right" + SUBSTRING = "substring" + +class BeautifyProcessor: + def __init__(self, rules_file): + self.rules_by_priority = self._load_rules(rules_file) + + def _load_rules(self, rules_file): + rules_by_priority = defaultdict(list) + + try: + with open(rules_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + if not isinstance(data, dict) or 'rules' not in data: + raise ValueError("El archivo JSON debe contener un objeto con una clave 'rules'") + + for rule in data['rules']: + try: + pattern = rule['pattern'] + replacement = rule['replacement'] + action = rule['action'] + pattern_type = PatternType(rule.get('type', 'string')) + priority = int(rule.get('priority', 999)) + + # Para remove_block, convertir el patrón con ..... a una regex + if action == "remove_block": + pattern = self._convert_block_pattern_to_regex(pattern) + pattern_type = PatternType.REGEX + elif pattern_type == PatternType.REGEX: + pattern = re.compile(pattern) + + rules_by_priority[priority].append((pattern, replacement, action, pattern_type)) + + except KeyError as e: + print(f"Error en regla: falta campo requerido {e}") + continue + except ValueError as e: + print(f"Error en regla: tipo de patrón inválido {rule.get('type')}") + continue + except Exception as e: + print(f"Error procesando regla: {e}") + continue + + except json.JSONDecodeError as e: + print(f"Error decodificando JSON: {e}") + except Exception as e: + print(f"Error cargando reglas: {e}") + + return rules_by_priority + + def _convert_block_pattern_to_regex(self, pattern): + """ + Convierte un patrón de bloque con ..... en una expresión regular. + Primero maneja el comodín ..... y luego escapa el resto de caracteres especiales. + """ + # Reemplazar temporalmente los ..... con un marcador único + marker = "__BLOCK_MARKER__" + pattern = pattern.replace(".....", marker) + + # Escapar caracteres especiales + pattern = re.escape(pattern) + + # Restaurar el marcador con el patrón .*? + pattern = pattern.replace(marker, ".*?") + + return re.compile(f'(?s){pattern}') + + def _process_remove_block(self, text, pattern): + result = text + matches = list(pattern.finditer(result)) + + for match in reversed(matches): + start, end = match.span() + + line_start = result.rfind('\n', 0, start) + 1 + if line_start == 0: + line_start = 0 + + line_end = result.find('\n', end) + if line_end == -1: + line_end = len(result) + else: + line_end += 1 + + while line_start > 0 and result[line_start-1:line_start] == '\n' and \ + (line_start == 1 or result[line_start-2:line_start-1] == '\n'): + line_start -= 1 + + while line_end < len(result) and result[line_end-1:line_end] == '\n' and \ + (line_end == len(result)-1 or result[line_end:line_end+1] == '\n'): + line_end += 1 + + result = result[:line_start] + result[line_end:] + + return result + + def _line_matches(self, line, pattern, pattern_type): + line = line.strip() + if pattern_type == PatternType.REGEX: + return bool(pattern.search(line)) + elif pattern_type == PatternType.LEFT: + return line.startswith(pattern) + elif pattern_type == PatternType.RIGHT: + return line.endswith(pattern) + elif pattern_type == PatternType.SUBSTRING: + return pattern in line + elif pattern_type == PatternType.STRING: + return line == pattern + return False + + def _apply_replace(self, text, pattern, replacement, pattern_type): + if pattern_type == PatternType.REGEX: + return pattern.sub(replacement, text) + elif pattern_type == PatternType.STRING: + return text.replace(pattern, replacement) + elif pattern_type == PatternType.SUBSTRING: + return text.replace(pattern, replacement) + elif pattern_type == PatternType.LEFT: + lines = text.splitlines() + result_lines = [] + for line in lines: + if line.strip().startswith(pattern): + result_lines.append(line.replace(pattern, replacement, 1)) + else: + result_lines.append(line) + return '\n'.join(result_lines) + elif pattern_type == PatternType.RIGHT: + lines = text.splitlines() + result_lines = [] + for line in lines: + if line.strip().endswith(pattern): + result_lines.append(line[:line.rindex(pattern)] + replacement + line[line.rindex(pattern) + len(pattern):]) + else: + result_lines.append(line) + return '\n'.join(result_lines) + return text + + def process_text(self, text): + if not text: + return text + + result = text + for priority in sorted(self.rules_by_priority.keys()): + rules = self.rules_by_priority[priority] + print(f"Aplicando reglas de prioridad {priority}") + + for pattern, replacement, action, pattern_type in rules: + try: + if action == "remove_block": + result = self._process_remove_block(result, pattern) + elif action == "replace": + result = self._apply_replace(result, pattern, replacement, pattern_type) + elif action == "remove_line": + result = self._process_remove_line(result, pattern, pattern_type) + elif action in ["add_before", "add_after"]: + result = self._process_line_additions(result, pattern, replacement, action, pattern_type) + except Exception as e: + print(f"Error aplicando regla {pattern}: {e}") + continue + + return result + + def process_file(self, input_file, output_file=None): + try: + with open(input_file, 'r', encoding='utf-8') as f: + content = f.read() + + processed_content = self.process_text(content) + + output = output_file or input_file + with open(output, 'w', encoding='utf-8') as f: + f.write(processed_content) + + except Exception as e: + print(f"Error procesando archivo {input_file}: {e}") + + def _process_remove_line(self, text, pattern, pattern_type): + lines = text.splitlines() + result_lines = [] + skip_next_empty = False + + for i, line in enumerate(lines): + should_remove = self._line_matches(line, pattern, pattern_type) + + if should_remove: + if i < len(lines) - 1 and not lines[i + 1].strip(): + skip_next_empty = True + continue + + if skip_next_empty and not line.strip(): + skip_next_empty = False + continue + + result_lines.append(line) + skip_next_empty = False + + return '\n'.join(result_lines) + + def _process_line_additions(self, text, pattern, replacement, action, pattern_type): + lines = text.splitlines() + result_lines = [] + + for line in lines: + if self._line_matches(line, pattern, pattern_type): + if action == "add_before": + result_lines.append(replacement) + result_lines.append(line) + else: # add_after + result_lines.append(line) + result_lines.append(replacement) + else: + result_lines.append(line) + + return '\n'.join(result_lines) \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/utils/email_parser.py b/backend/script_groups/EmailCrono/utils/email_parser.py new file mode 100644 index 0000000..6ba70e9 --- /dev/null +++ b/backend/script_groups/EmailCrono/utils/email_parser.py @@ -0,0 +1,295 @@ +# utils/email_parser.py +import email +from email import policy +from email.parser import BytesParser +from datetime import datetime +import re +from pathlib import Path +from bs4 import BeautifulSoup +from email.utils import parsedate_to_datetime +from models.mensaje_email import MensajeEmail +from utils.attachment_handler import guardar_adjunto +import tempfile +import os + +def _get_payload_safely(parte): + """ + Obtiene el payload de una parte del email de forma segura + """ + try: + if parte.is_multipart(): + return None + payload = parte.get_payload(decode=True) + if payload is None: + return None + charset = parte.get_content_charset() or 'utf-8' + return payload.decode(charset, errors='ignore') + except Exception as e: + print(f"Error getting payload: {str(e)}") + return None + +def _extract_subject_from_text(text): + """ + Extrae el asunto de un texto dados diferentes formatos de cabecera + """ + subject_headers = { + 'Oggetto: ': 9, # Italian + 'Subject: ': 9, # English + 'Asunto: ': 8, # Spanish + 'Sujet: ': 7, # French + 'Betreff: ': 9 # German + } + + for line in text.split('\n'): + line = line.strip() + for header, offset in subject_headers.items(): + if line.startswith(header): + return line[offset:].strip() + return None + +def _should_skip_line(line): + """ + Determina si una línea debe ser omitida por ser una cabecera de email + """ + headers_to_skip = [ + 'Da: ', 'Inviato: ', 'A: ', # Italian + 'From: ', 'Sent: ', 'To: ', # English + 'De: ', 'Enviado: ', 'Para: ', # Spanish + 'Von: ', 'Gesendet: ', 'An: ', # German + 'De : ', 'Envoyé : ', 'À : ' # French + ] + return any(line.strip().startswith(header) for header in headers_to_skip) + +def _html_a_markdown(html): + """ + Convierte contenido HTML a texto markdown, extrayendo el asunto si está presente + """ + if html is None: + return (None, "") + + try: + # Limpieza básica + html = html.replace('\xa0', ' ') # NBSP a espacio normal + html = html.replace('\r\n', '\n') # CRLF a LF + html = html.replace('\r', '\n') # CR a LF + + soup = BeautifulSoup(html, 'html.parser') + + # Procesar tablas + for table in soup.find_all('table'): + try: + rows = table.find_all('tr') + if not rows: + continue + + markdown_table = [] + max_widths = [] + + # Calcular anchos máximos + for row in rows: + cells = row.find_all(['th', 'td']) + while len(max_widths) < len(cells): + max_widths.append(0) + for i, cell in enumerate(cells): + cell_text = cell.get_text().strip() + max_widths[i] = max(max_widths[i], len(cell_text)) + + # Construir tabla markdown + if max_widths: # Solo si tenemos celdas válidas + header_row = rows[0].find_all(['th', 'td']) + header = '| ' + ' | '.join(cell.get_text().strip().ljust(max_widths[i]) + for i, cell in enumerate(header_row)) + ' |' + separator = '|' + '|'.join('-' * (width + 2) for width in max_widths) + '|' + + markdown_table.append(header) + markdown_table.append(separator) + + for row in rows[1:]: + cells = row.find_all(['td', 'th']) + row_text = '| ' + ' | '.join(cell.get_text().strip().ljust(max_widths[i]) + for i, cell in enumerate(cells)) + ' |' + markdown_table.append(row_text) + + table.replace_with(soup.new_string('\n' + '\n'.join(markdown_table))) + except Exception as e: + print(f"Error procesando tabla: {str(e)}") + continue + + # Procesar saltos de línea + for br in soup.find_all('br'): + br.replace_with('\n') + + # Obtener texto limpio + text = soup.get_text() + + # Procesar líneas + cleaned_lines = [] + subject = None + + for line in text.split('\n'): + if not subject: + subject = _extract_subject_from_text(line) + + if not _should_skip_line(line): + cleaned_lines.append(line) + + final_text = '\n'.join(cleaned_lines).strip() + return (subject, final_text) + + except Exception as e: + print(f"Error en html_a_markdown: {str(e)}") + return (None, html if html else "") + +def _procesar_email_adjunto(parte, dir_adjuntos): + """ + Procesa un email que viene como adjunto dentro de otro email. + """ + try: + mensajes = [] + if parte.is_multipart(): + # Si es multipart, procesar cada subparte + for subparte in parte.walk(): + if subparte.get_content_type() == "message/rfc822": + # Si es un mensaje RFC822, obtener el payload como lista + payload = subparte.get_payload() + if isinstance(payload, list): + for msg in payload: + mensajes.extend(procesar_eml_interno(msg, dir_adjuntos)) + elif isinstance(payload, email.message.Message): + mensajes.extend(procesar_eml_interno(payload, dir_adjuntos)) + else: + # Si no es multipart, intentar procesar como mensaje único + payload = parte.get_payload() + if isinstance(payload, list): + for msg in payload: + mensajes.extend(procesar_eml_interno(msg, dir_adjuntos)) + elif isinstance(payload, email.message.Message): + mensajes.extend(procesar_eml_interno(payload, dir_adjuntos)) + + return mensajes + except Exception as e: + print(f"Error procesando email adjunto: {str(e)}") + return [] + +def procesar_eml(ruta_archivo, dir_adjuntos): + """ + Punto de entrada principal para procesar archivos .eml + """ + try: + with open(ruta_archivo, 'rb') as eml: + mensaje = BytesParser(policy=policy.default).parse(eml) + return procesar_eml_interno(mensaje, dir_adjuntos) + except Exception as e: + print(f"Error al abrir el archivo {ruta_archivo}: {str(e)}") + return [] + +def procesar_eml_interno(mensaje, dir_adjuntos): + """ + Procesa un mensaje de email, ya sea desde archivo o adjunto + """ + mensajes = [] + + try: + remitente = mensaje.get('from', '') + fecha_str = mensaje.get('date', '') + fecha = _parsear_fecha(fecha_str) + + # Get subject from email headers first + subject = mensaje.get('subject', '') + if subject: + # Try to decode if it's encoded + subject = str(email.header.make_header(email.header.decode_header(subject))) + + contenido = "" + adjuntos = [] + tiene_html = False + + # First pass: check for HTML content + if mensaje.is_multipart(): + for parte in mensaje.walk(): + if parte.get_content_type() == "text/html": + tiene_html = True + break + else: + tiene_html = mensaje.get_content_type() == "text/html" + + # Second pass: process content and attachments + if mensaje.is_multipart(): + for parte in mensaje.walk(): + content_type = parte.get_content_type() + + try: + if content_type == "text/html": + html_content = _get_payload_safely(parte) + if html_content: + part_subject, text = _html_a_markdown(html_content) + if not subject and part_subject: + subject = part_subject + if text: + contenido = text + elif content_type == "text/plain" and not tiene_html: + text = _get_payload_safely(parte) + if text: + contenido = text + elif content_type == "message/rfc822": + # Procesar email adjunto + mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos) + mensajes.extend(mensajes_adjuntos) + elif parte.get_content_disposition() == 'attachment': + nombre = parte.get_filename() + if nombre and nombre.lower().endswith('.eml'): + # Si es un archivo .eml adjunto + mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos) + mensajes.extend(mensajes_adjuntos) + else: + # Otros tipos de adjuntos + ruta_adjunto = guardar_adjunto(parte, dir_adjuntos) + if ruta_adjunto: + adjuntos.append(Path(ruta_adjunto).name) + except Exception as e: + print(f"Error procesando parte del mensaje: {str(e)}") + continue + else: + if mensaje.get_content_type() == "text/html": + html_content = _get_payload_safely(mensaje) + if html_content: + part_subject, contenido = _html_a_markdown(html_content) + if not subject and part_subject: + subject = part_subject + else: + contenido = _get_payload_safely(mensaje) or "" + + # Solo agregar el mensaje si tiene contenido útil + if contenido or subject or adjuntos: + mensajes.append(MensajeEmail( + remitente=remitente, + fecha=fecha, + contenido=contenido, + subject=subject, + adjuntos=adjuntos + )) + + except Exception as e: + print(f"Error procesando mensaje: {str(e)}") + + return mensajes + +def _parsear_fecha(fecha_str): + try: + fecha = parsedate_to_datetime(fecha_str) + return fecha.replace(tzinfo=None) # Remove timezone info + except: + try: + fecha_match = re.search(r'venerd=EC (\d{1,2}) (\w+) (\d{4}) (\d{1,2}):(\d{2})', fecha_str) + if fecha_match: + dia, mes, año, hora, minuto = fecha_match.groups() + meses_it = { + 'gennaio': 1, 'febbraio': 2, 'marzo': 3, 'aprile': 4, + 'maggio': 5, 'giugno': 6, 'luglio': 7, 'agosto': 8, + 'settembre': 9, 'ottobre': 10, 'novembre': 11, 'dicembre': 12 + } + mes_num = meses_it.get(mes.lower(), 1) + return datetime(int(año), mes_num, int(dia), int(hora), int(minuto)) + except: + pass + return datetime.now() \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/utils/markdown_handler.py b/backend/script_groups/EmailCrono/utils/markdown_handler.py new file mode 100644 index 0000000..2991e3d --- /dev/null +++ b/backend/script_groups/EmailCrono/utils/markdown_handler.py @@ -0,0 +1,39 @@ +# utils/markdown_handler.py +import os +import re +from datetime import datetime +from models.mensaje_email import MensajeEmail + +def cargar_cronologia_existente(archivo): + mensajes = [] + if not os.path.exists(archivo): + return mensajes + + with open(archivo, 'r', encoding='utf-8') as f: + contenido = f.read() + + bloques = contenido.split('---\n\n') + for bloque in bloques: + if not bloque.strip(): + continue + + match = re.match(r'## (\d{14})\|(.*?)\n\n(.*)', bloque.strip(), re.DOTALL) + if match: + fecha_str, remitente, contenido = match.groups() + fecha = datetime.strptime(fecha_str, '%Y%m%d%H%M%S') + + adjuntos = [] + if '### Adjuntos' in contenido: + contenido_principal, lista_adjuntos = contenido.split('### Adjuntos') + adjuntos = [adj.strip()[2:-2] for adj in lista_adjuntos.strip().split('\n')] + contenido = contenido_principal.strip() + + mensajes.append(MensajeEmail( + remitente=remitente, + fecha=fecha, + contenido=contenido, + adjuntos=adjuntos + )) + + return mensajes + diff --git a/backend/script_groups/EmailCrono/work_dir.json b/backend/script_groups/EmailCrono/work_dir.json new file mode 100644 index 0000000..544f154 --- /dev/null +++ b/backend/script_groups/EmailCrono/work_dir.json @@ -0,0 +1,3 @@ +{ + "path": "C:/Trabajo/VM/40 - 93040 - HENKEL - NEXT2 Problem/Reporte/Emails" +} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/x1.py b/backend/script_groups/EmailCrono/x1.py new file mode 100644 index 0000000..73e2900 --- /dev/null +++ b/backend/script_groups/EmailCrono/x1.py @@ -0,0 +1,101 @@ +""" +Script para dessasemblar los emails y generar un archivo de texto con la cronología de los mensajes. +""" + +# main.py +import os +from pathlib import Path +from utils.email_parser import procesar_eml +from utils.markdown_handler import cargar_cronologia_existente +from utils.beautify import BeautifyProcessor +from config.config import Config +import hashlib + +def generar_indice(mensajes): + """ + Genera una lista de mensajes usando el formato de Obsidian + """ + indice = "# Índice de Mensajes\n\n" + + for mensaje in mensajes: + indice += mensaje.get_index_entry() + "\n" + + indice += "\n---\n\n" + return indice + +def main(): + config = Config() + + # Debug prints + print(f"Input directory: {config.get_input_dir()}") + print(f"Output directory: {config.get_output_dir()}") + print(f"Cronologia file: {config.get_cronologia_file()}") + print(f"Attachments directory: {config.get_attachments_dir()}") + + # Obtener el directorio donde está el script actual + script_dir = os.path.dirname(os.path.abspath(__file__)) + # Construir la ruta al archivo de reglas en el subdirectorio config + beautify_rules = os.path.join(script_dir, "config", "beautify_rules.json") + beautifier = BeautifyProcessor(beautify_rules) + print(f"Beautify rules file: {beautify_rules}") + + # Ensure directories exist + os.makedirs(config.get_output_dir(), exist_ok=True) + os.makedirs(config.get_attachments_dir(), exist_ok=True) + + # Check if input directory exists and has files + input_path = Path(config.get_input_dir()) + if not input_path.exists(): + print(f"Error: Input directory {input_path} does not exist") + return + + eml_files = list(input_path.glob('*.eml')) + print(f"Found {len(eml_files)} .eml files") + + mensajes = [] + print(f"Loaded {len(mensajes)} existing messages") + mensajes_hash = {msg.hash for msg in mensajes} + + total_procesados = 0 + total_nuevos = 0 + mensajes_duplicados = 0 + + for archivo in eml_files: + print(f"\nProcessing {archivo}") + nuevos_mensajes = procesar_eml(archivo, config.get_attachments_dir()) + total_procesados += len(nuevos_mensajes) + + # Verificar duplicados y aplicar beautify solo a los mensajes nuevos + for msg in nuevos_mensajes: + if msg.hash not in mensajes_hash: + # Aplicar beautify solo si el mensaje es nuevo + msg.contenido = beautifier.process_text(msg.contenido) + mensajes.append(msg) + mensajes_hash.add(msg.hash) + total_nuevos += 1 + else: + mensajes_duplicados += 1 + + print(f"\nEstadísticas de procesamiento:") + print(f"- Total mensajes encontrados: {total_procesados}") + print(f"- Mensajes únicos añadidos: {total_nuevos}") + print(f"- Mensajes duplicados ignorados: {mensajes_duplicados}") + + # Ordenar mensajes de más reciente a más antiguo + mensajes.sort(key=lambda x: x.fecha, reverse=True) + + # Generar el índice + indice = generar_indice(mensajes) + + # Escribir el archivo con el índice y los mensajes + output_file = config.get_cronologia_file() + print(f"\nWriting {len(mensajes)} messages to {output_file}") + with open(output_file, 'w', encoding='utf-8') as f: + # Primero escribir el índice + f.write(indice) + # Luego escribir todos los mensajes + for msg in mensajes: + f.write(msg.to_markdown()) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/commands.sh b/commands.sh deleted file mode 100644 index 7c17e14..0000000 --- a/commands.sh +++ /dev/null @@ -1,3 +0,0 @@ -# Crear .gitkeep en cada directorio de script grupo -mkdir -p backend/script_groups/example_group -touch backend/script_groups/example_group/.gitkeep diff --git a/config_manager.py b/config_manager.py index 8a943de..ab5fcc0 100644 --- a/config_manager.py +++ b/config_manager.py @@ -122,22 +122,13 @@ class ConfigurationManager: # Determine schema path based on level if level == "1": - path = os.path.join(self.data_path, "esquema.json") - # Try esquema.json first, then schema.json if not found - if not os.path.exists(path): - path = os.path.join(self.data_path, "schema.json") + path = os.path.join(self.data_path, "esquema_general.json") elif level == "2": - path = os.path.join(self.script_groups_path, group, "esquema.json") - # Try esquema.json first, then schema.json if not found - if not os.path.exists(path): - path = os.path.join(self.script_groups_path, group, "schema.json") + path = os.path.join(self.script_groups_path, group, "esquema_group.json") elif level == "3": if not group: return {"type": "object", "properties": {}} - path = os.path.join(self.script_groups_path, group, "esquema.json") - # Try esquema.json first, then schema.json if not found - if not os.path.exists(path): - path = os.path.join(self.script_groups_path, group, "schema.json") + path = os.path.join(self.script_groups_path, group, "esquema_work.json") else: return {"type": "object", "properties": {}} @@ -169,11 +160,11 @@ class ConfigurationManager: try: # Determinar rutas de schema y config if level == "1": - schema_path = os.path.join(self.data_path, "esquema.json") + schema_path = os.path.join(self.data_path, "esquema_general.json") config_path = os.path.join(self.data_path, "data.json") elif level == "2": schema_path = os.path.join( - self.script_groups_path, group, "esquema.json" + self.script_groups_path, group, "esquema_group.json" ) config_path = os.path.join(self.script_groups_path, group, "data.json") elif level == "3": @@ -183,7 +174,7 @@ class ConfigurationManager: "message": "Group is required for level 3", } schema_path = os.path.join( - self.script_groups_path, group, "esquema.json" + self.script_groups_path, group, "esquema_work.json" ) config_path = ( os.path.join(self.working_directory, "data.json") diff --git a/data/esquema_general.json b/data/esquema_general.json new file mode 100644 index 0000000..1c9e43a --- /dev/null +++ b/data/esquema_general.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "properties": {} +} \ No newline at end of file diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/excel/__init__.py b/services/excel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/language/__init__.py b/services/language/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/llm/__init__.py b/services/llm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/llm/__pycache__/base.cpython-310.pyc b/services/llm/__pycache__/base.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5aadd35f6f1c65d072d208cf88b041da0aea23d GIT binary patch literal 1017 zcmZ`&OK%e~5VpPEM>b68_7Q9ckzCuK zz)#@9U-Fd`=iWFmvq_sGwU#`Yv1jM|zVW2#^C>ZBb?&WQA7a7ARG*rk`o(i@R}xO^U; zWJV=`zjmoJak6qAdGRk>iMn`&lVpp~8LI){GWF7zqv zQ`pzobV&h)Enl3eTItNH#H#PMEiaR%PO@ZGZ1@ZN#EmK^x&xo-tsC?AP`(*|)XhTW zwlU*Tu8Wy9<8xKdvGwDdW{=CV8YAt4*~0avYdC>$SnC>@ac{Y|k$do8lIvos5won& z=0Q!m3uQph!D&|J^AZJ_qAJQv7j}`DywPek2W^?8nY4P%u+`INx`(nJqq!BRw;^;- zr^<>=DX}WmX70fK5(jJjOu+k|)M3BtA|dLmQbM>`h^mqE68$Y9zRt69eG>{Hn_LL( zA*=@@sUIO9oH30r=KM+7WL7`HgAL2pL#jFggKuQ;C?cFPwo4g*McbQxS3iSu=VceU x#-^`;_!k2+9@xdKsHZKC_VKcX4bj80_u;wd?Mli!94v$Xb53`>i0;u{_78o-`TGC> literal 0 HcmV?d00001 diff --git a/services/llm/__pycache__/grok_service.cpython-310.pyc b/services/llm/__pycache__/grok_service.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e12073b1e4a17b132854bf32e7e55ee9f6bc0174 GIT binary patch literal 2371 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 literal 0 HcmV?d00001 diff --git a/services/llm/__pycache__/openai_service.cpython-310.pyc b/services/llm/__pycache__/openai_service.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e34a380b56de90f79f6dc67047144ce680e1540f GIT binary patch literal 2552 zcmZuy&5smC6z{68>F(+I*aa7L@q}(R7nw?LyS-(%_yZ-s}7cR{ez}epEi=TWZrx&6;8c7wShW$?q7N_9XTuWL3 zfvZumBy4F5CfShT6>h?%gbNZ++8}8Q|0aXWbtK3n$X2Z(U4<(F&wyv5D|&FEE&6b+ z<+?X_cj426&ZsnRy<5DMC3$l1o#y%d!@89fLZ)@c$gGqqG7}|Zd!MI>Kd#T9vnPNU z>o&P#?bwyAeM1GgL4F_xR)YRWDl23a(pE)*I>G`+?Dg4u3w!|+{*9dSyfA!NOmeZt zFQzim5|B51Qu1jrQG9VquK|p1R3;&OGH{g(#_Rq_ny`RqMUA-jzlzd{Jf~El6vYEh zoF))B)q#cT0vWKHX_*f9B&rAEyOuf#e-GXlCpRvtVk%=(=#5LUN=llUb&c*=Exh=9~4czsbP2hTWX~0yG%G!;K z+yI>S|A2(O%Nh&=FOynBR_g}ez|W%X&=lXuTq`Vv+2JM}JjwaKu*XrFp5%j$T81Mv zi?2tf?!e)-O2&pHNDC;{To!B6@KMz zTIy`&3V#(sj!Dt%F2U}F%3Vm7MF%Sw(#0Adm~IdYU$ZQ_DBZ#|Iyl{gG=feFY4nWf zRo)Y6`&E;pr`a#d`h#!oMOzP_t5(x5SgevUTtNp?*t{U+Z^%=Kboc$80hBt(K)7p09) z9JI~7G2r-G?dYk7CaRu?c&q1-U~Z`wkThlCy?*uTRlLN^1|~SLhebGdaCZ1|F1JhQ zL{jkGG+x-49X(S_QqgoyA!F#-dndqkyvgj1C+lv{0tcw#N!pAvsKS++p(AxmOtMlp zC8=ISM_)qnGLlFAQysx2N79sEOB?8sbt^3*p=&?UNv=(l$FgRk8Hxy^T7vWS61sJ^ z@pj&!kD``WfDGF6l06Q5(3>L{&YNPeq~x~~sSOm?6pg53cxZVSkZ{qGg5?-SM=nLU zyAwEb9M!962D|&d+CPMYV-Ju8 ` +
+
${script.name}
+
${script.description}
+ +
+ `).join(''); +} + +// Execute a script +async function executeScript(scriptName) { + addLogLine(`\nEjecutando script: ${scriptName}...\n`); + + const response = await fetch('/api/execute_script', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ group: currentGroup, script: scriptName }) + }); + + const result = await response.json(); + if (result.error) { + addLogLine(`\nError: ${result.error}\n`); + } +} + +// Form rendering functionality +async function renderForm(containerId, data) { + console.log(`Rendering form for ${containerId} with data:`, data); // Debug line + const container = document.getElementById(containerId); + const level = containerId.replace('level', '').split('-')[0]; + + try { + const schemaResponse = await fetch(`/api/schema/${level}?group=${currentGroup}`); + const schema = await schemaResponse.json(); + console.log(`Schema for level ${level}:`, schema); // Debug line + + if (!schema || !schema.properties || Object.keys(schema.properties).length === 0) { + container.innerHTML = '

No hay esquema definido para este nivel.

'; + return; + } + + container.innerHTML = ` +
+ ${generateFormFields(schema, data || {}, '', level)} +
+
+ +
+ `; + } catch (error) { + console.error(`Error rendering form ${containerId}:`, error); + container.innerHTML = '

Error cargando el esquema.

'; + } +} + +function generateFormFields(schema, data, prefix, level) { + console.log('Generating fields with data:', { schema, data, prefix, level }); // Debug line + let html = ''; + + if (!schema.properties) { + console.warn('Schema has no properties'); + return html; + } + + for (const [key, def] of Object.entries(schema.properties)) { + const fullKey = prefix ? `${prefix}.${key}` : key; + const value = getValue(data, fullKey); + console.log(`Field ${fullKey}:`, { definition: def, value: value }); // Debug line + + html += `
+ `; + + if (def.type === 'object') { + html += `
+ ${generateFormFields(def, data, fullKey, level)} +
`; + } else { + html += generateInputField(def, fullKey, value, level); + } + + if (def.description) { + html += `

${def.description}

`; + } + html += '
'; + } + return html; +} + +function getValue(data, path) { + console.log('Getting value for path:', { path, data }); // Debug line + if (!data || !path) return undefined; + + const value = path.split('.').reduce((obj, key) => obj?.[key], data); + console.log('Found value:', value); // Debug line + return value; +} + +// Modificar la función generateInputField para quitar el onchange +function generateInputField(def, key, value, level) { + const baseClasses = "w-full p-2 border rounded bg-green-50"; + + switch (def.type) { + case 'string': + if (def.enum) { + return ``; + } + return ``; + + case 'number': + return ``; + + case 'boolean': + return `
+ +
`; + + default: + return ``; + } +} + +async function modifySchema(level) { + try { + console.log('Loading schema for level:', level); // Debug line + const response = await fetch(`/api/schema/${level}?group=${currentGroup}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const schema = await response.json(); + console.log('Loaded schema:', schema); // Debug line + + // Show schema editor modal + const modal = document.getElementById('schema-editor'); + if (!modal) { + throw new Error('Schema editor modal not found'); + } + modal.classList.remove('hidden'); + + // Inicializar el esquema si está vacío + const finalSchema = Object.keys(schema).length === 0 ? + { type: 'object', properties: {} } : schema; + + // Inicializar editores + const jsonEditor = document.getElementById('json-editor'); + const visualEditor = document.getElementById('visual-editor'); + const schemaLevel = document.getElementById('schema-level'); + + if (!jsonEditor || !visualEditor || !schemaLevel) { + throw new Error('Required editor elements not found'); + } + + jsonEditor.value = JSON.stringify(finalSchema, null, 2); + visualEditor.innerHTML = '
' + + ''; + schemaLevel.value = level; + + // Renderizar editor visual + renderVisualEditor(finalSchema); + + // Activar pestaña visual por defecto + switchEditorMode('visual'); + } catch (error) { + console.error('Error loading schema:', error); + alert('Error cargando el esquema: ' + error.message); + } +} + +function switchEditorMode(mode) { + const visualEditor = document.getElementById('visual-editor'); + const jsonEditor = document.getElementById('json-editor'); + const visualTab = document.getElementById('visual-tab'); + const jsonTab = document.getElementById('json-tab'); + + if (mode === 'visual') { + visualEditor.classList.remove('hidden'); + jsonEditor.classList.add('hidden'); + visualTab.classList.add('border-blue-500'); + jsonTab.classList.remove('border-blue-500'); + + // Actualizar el editor visual desde JSON + try { + const schema = JSON.parse(jsonEditor.value); + renderVisualEditor(schema); + } catch (e) { + console.error('Error parsing JSON:', e); + } + } else { + visualEditor.classList.add('hidden'); + jsonEditor.classList.remove('hidden'); + visualTab.classList.remove('border-blue-500'); + jsonTab.classList.add('border-blue-500'); + + // Actualizar el JSON desde el editor visual + try { + const schema = updateVisualSchema(); + jsonEditor.value = JSON.stringify(schema, null, 2); + } catch (e) { + console.error('Error updating JSON:', e); + } + } +} + +function renderVisualEditor(schema) { + const container = document.getElementById('schema-fields'); + container.innerHTML = ''; + + Object.entries(schema.properties || {}).forEach(([key, field]) => { + container.appendChild(createFieldEditor(key, field)); + }); +} + +function createFieldEditor(key, field) { + const div = document.createElement('div'); + div.className = 'mb-6 p-4 border rounded schema-field'; + div.innerHTML = ` +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ ${field.enum ? ` +
+ + +
+ ` : ''} + + `; + return div; +} + +function updateFieldType(select) { + const fieldContainer = select.closest('.schema-field'); + const enumContainer = fieldContainer.querySelector('.enum-container'); + + if (select.value === 'enum') { + if (!enumContainer) { + const div = document.createElement('div'); + div.className = 'enum-container mt-4'; + div.innerHTML = ` + + + `; + fieldContainer.appendChild(div); + } + } else if (enumContainer) { + enumContainer.remove(); + } + updateVisualSchema(); +} + +function removeField(button) { + const fieldContainer = button.closest('.schema-field'); + fieldContainer.remove(); + updateVisualSchema(); +} + +function createEnumEditor(enumValues) { + return ` +
+ + +
+ `; +} + +function addSchemaField() { + const container = document.getElementById('schema-fields'); + const newField = createFieldEditor(`campo_${Date.now()}`, { + type: 'string', + title: 'Nuevo Campo', + description: '' + }); + container.appendChild(newField); +} + +// Funciones de actualización del esquema visual +function updateVisualSchema() { + try { + const fields = document.getElementById('schema-fields').children; + const schema = { + type: 'object', + properties: {} + }; + + Array.from(fields).forEach(field => { + const inputs = field.getElementsByTagName('input'); + const select = field.getElementsByTagName('select')[0]; + const key = inputs[0].value; + + schema.properties[key] = { + type: select.value === 'enum' ? 'string' : select.value, + title: inputs[1].value, + description: inputs[2].value + }; + + if (select.value === 'enum') { + const textarea = field.getElementsByTagName('textarea')[0]; + if (textarea) { + schema.properties[key].enum = textarea.value.split('\n').filter(v => v.trim()); + } + } + }); + + // Actualizar el JSON editor directamente + const jsonEditor = document.getElementById('json-editor'); + if (jsonEditor) { + jsonEditor.value = JSON.stringify(schema, null, 2); + } + + return schema; + } catch (error) { + console.error('Error updating schema:', error); + return null; + } +} + +async function saveSchema() { + try { + const level = document.getElementById('schema-level').value; + let schema; + + // Obtener el esquema según el modo activo + const visualEditor = document.getElementById('visual-editor'); + const jsonEditor = document.getElementById('json-editor'); + + if (!visualEditor.classList.contains('hidden')) { + schema = updateVisualSchema(); + } else { + schema = JSON.parse(jsonEditor.value); + } + + console.log('Saving schema:', schema); // Debug line + + const response = await fetch(`/api/schema/${level}?group=${currentGroup}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(schema) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + // Recargar el formulario + const configResponse = await fetch(`/api/config/${level}?group=${currentGroup}`); + const data = await configResponse.json(); + await renderForm(`level${level}-form`, data); + + // Cerrar modal + document.getElementById('schema-editor').classList.add('hidden'); + } catch (e) { + console.error('Error saving schema:', e); + alert('Error guardando esquema: ' + e.message); + } +} + +async function setWorkingDirectory() { + if (!currentGroup) { + alert('Por favor, seleccione un grupo de scripts primero'); + return; + } + + const path = document.getElementById('working-directory').value; + await updateWorkingDirectory(path); +} + +async function initWorkingDirectory() { + if (!currentGroup) return; + + const response = await fetch(`/api/working-directory/${currentGroup}`); + const result = await response.json(); + if (result.status === 'success' && result.path) { + await updateWorkingDirectory(result.path); + } +} + +async function browseDirectory() { + console.log('Current group when browsing:', currentGroup); // Debug line + if (!currentGroup) { + alert('Por favor, seleccione un grupo de scripts primero'); + return; + } + + const currentPath = document.getElementById('working-directory').value; + const response = await fetch(`/api/browse-directories?current_path=${encodeURIComponent(currentPath)}`); + const result = await response.json(); + + if (result.status === 'success') { + await updateWorkingDirectory(result.path); + } +} + +// Nueva función auxiliar para actualizar el directorio de trabajo +async function updateWorkingDirectory(path) { + console.log('Updating working directory:', { path, group: currentGroup }); // Debug line + + const response = await fetch('/api/working-directory', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + path: path, + group: currentGroup + }) + }); + + const result = await response.json(); + console.log('Update result:', result); // Debug line + + if (result.status === 'success') { + // Actualizar input + document.getElementById('working-directory').value = path; + + // Recargar configuración de nivel 3 + const configResponse = await fetch(`/api/config/3?group=${currentGroup}`); + const data = await configResponse.json(); + await renderForm('level3-form', data); + } else { + alert('Error: ' + (result.message || 'No se pudo actualizar el directorio de trabajo')); + } +} + +// Función para alternar visibilidad de una sección +function toggleConfig(sectionId) { + const content = document.getElementById(sectionId); + const button = document.querySelector(`[onclick="toggleConfig('${sectionId}')"]`); + + if (content.classList.contains('hidden')) { + content.classList.remove('hidden'); + button.innerText = 'Ocultar Configuración'; + + // Recargar la configuración al mostrar + const level = sectionId.replace('level', '').replace('-content', ''); + const formId = `level${level}-form`; + console.log(`Reloading config for level ${level}`); // Debug line + + fetch(`/api/config/${level}?group=${currentGroup}`) + .then(response => response.json()) + .then(data => renderForm(formId, data)) + .catch(error => console.error('Error reloading config:', error)); + } else { + content.classList.add('hidden'); + button.innerText = 'Mostrar Configuración'; + } +} + +async function clearLogs() { + const response = await fetch('/api/logs', { method: 'DELETE' }); + const result = await response.json(); + if (result.status === 'success') { + document.getElementById('log-area').innerHTML = ''; + } +} + +async function loadStoredLogs() { + const response = await fetch('/api/logs'); + const result = await response.json(); + const logArea = document.getElementById('log-area'); + logArea.innerHTML = result.logs; + logArea.scrollTop = logArea.scrollHeight; +} + +// Initialize on page load +async function initializeApp() { + try { + initWebSocket(); + await loadStoredLogs(); // Cargar logs almacenados + + // Primero establecer el grupo actual + const group = localStorage.getItem('selectedGroup'); + const selectElement = document.getElementById('script-group'); + if (group) { + selectElement.value = group; + } + currentGroup = selectElement.value; // Siempre establecer currentGroup con el valor actual del select + console.log('Current group initialized as:', currentGroup); // Debug line + updateGroupDescription(); // Actualizar descripción inicial + + // Configurar el evento de cambio de grupo + selectElement.addEventListener('change', async (e) => { + currentGroup = e.target.value; + localStorage.setItem('selectedGroup', e.target.value); + console.log('Group changed to:', currentGroup); // Debug line + updateGroupDescription(); // Actualizar descripción al cambiar + await initWorkingDirectory(); + await loadConfigs(); + }); + + // Luego cargar el directorio de trabajo + await initWorkingDirectory(); + + // Finalmente cargar las configuraciones + await loadConfigs(); + + // Configurar el evento de cambio de grupo + selectElement.addEventListener('change', async (e) => { + currentGroup = e.value; + localStorage.setItem('selectedGroup', e.value); + console.log('Group changed to:', currentGroup); // Debug line + await initWorkingDirectory(); + await loadConfigs(); + }); + + // Close sidebar on small screens when changing groups + if (window.innerWidth < 768) { + toggleSidebar(); + } + } catch (error) { + console.error('Error during initialization:', error); + } +} + +// Modificar la inicialización para usar la nueva función async +document.addEventListener('DOMContentLoaded', () => { + initializeApp().catch(console.error); +}); + +// Función auxiliar para obtener timestamp formateado +function getTimestamp() { + const now = new Date(); + return now.toLocaleTimeString('es-ES', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); +} + +// Función para agregar línea al log con timestamp +function addLogLine(message) { + const logArea = document.getElementById('log-area'); + const timestamp = getTimestamp(); + + // Filtrar líneas vacías y aplicar timestamp solo a líneas con contenido + const lines = message.split('\n') + .filter(line => line.trim()) // Eliminar líneas vacías + .map(line => `[${timestamp}] ${line}`) + .join('\n'); + + if (lines) { + logArea.innerHTML += lines + '\n'; + logArea.scrollTop = logArea.scrollHeight; + } +} + +function updateGroupDescription() { + const select = document.getElementById('script-group'); + const option = select.options[select.selectedIndex]; + const description = option.getAttribute('data-description'); + document.getElementById('group-description').textContent = description; +} + +function toggleSidebar() { + const sidebar = document.querySelector('.sidebar'); + const overlay = document.querySelector('.overlay'); + const schemaEditor = document.getElementById('schema-editor'); + + // No cerrar sidebar si el modal está abierto + if (!schemaEditor.classList.contains('hidden')) { + return; + } + + sidebar.classList.toggle('open'); + overlay.classList.toggle('show'); +} + +async function editGroupDescription() { + if (!currentGroup) { + alert('Por favor, seleccione un grupo de scripts primero'); + return; + } + + try { + const response = await fetch(`/api/group-description/${currentGroup}`); + if (!response.ok) throw new Error('Error cargando descripción del grupo'); + const description = await response.json(); + + // Show schema editor modal with description data + const modal = document.getElementById('schema-editor'); + const modalTitle = modal.querySelector('h3'); + const visualEditor = document.getElementById('visual-editor'); + const jsonEditor = document.getElementById('json-editor'); + const tabs = document.getElementById('editor-tabs'); + + // Configurar modal para edición de descripción + modalTitle.textContent = 'Editar Descripción del Grupo'; + tabs.classList.add('hidden'); + + // Crear el formulario en el visualEditor + visualEditor.innerHTML = ` +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ `; + visualEditor.classList.remove('hidden'); + jsonEditor.classList.add('hidden'); + + modal.classList.remove('hidden'); + + // Cambiar comportamiento de todos los botones de guardar + const saveButtons = modal.querySelectorAll('button[onclick="saveSchema()"]'); + saveButtons.forEach(btn => { + btn.onclick = async () => { + try { + const form = document.getElementById('group-description-form'); + const formData = new FormData(form); + + const updatedDescription = { + name: formData.get('name') || '', + description: formData.get('description') || '', + version: formData.get('version') || '1.0', + author: formData.get('author') || '' + }; + + const saveResponse = await fetch(`/api/group-description/${currentGroup}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updatedDescription) + }); + + if (!saveResponse.ok) throw new Error('Error guardando descripción'); + + // Restaurar modal a su estado original + modalTitle.textContent = 'Editor de Esquema'; + tabs.classList.remove('hidden'); + saveButtons.forEach(btn => btn.onclick = saveSchema); + modal.classList.add('hidden'); + + // Recargar la página para actualizar la descripción + location.reload(); + } catch (e) { + alert('Error guardando descripción: ' + e.message); + } + }; + }); + + } catch (e) { + alert('Error: ' + e.message); + } +} + +// Agregar función para recolectar datos del formulario +function collectFormData(level) { + const formContainer = document.getElementById(`level${level}-form`); + const data = {}; + + formContainer.querySelectorAll('input, select').forEach(input => { + const key = input.getAttribute('data-key'); + if (!key) return; + + let value; + if (input.type === 'checkbox') { + value = input.checked; + } else if (input.type === 'number') { + value = Number(input.value); + } else { + value = input.value; + } + + // Manejar claves anidadas (por ejemplo: "parent.child") + const keys = key.split('.'); + let current = data; + for (let i = 0; i < keys.length - 1; i++) { + current[keys[i]] = current[keys[i]] || {}; + current = current[keys[i]]; + } + current[keys[keys.length - 1]] = value; + }); + + return data; +} + +// Agregar función para guardar configuración +async function saveConfig(level) { + try { + const form = document.getElementById(`config-form-${level}`); + const formData = {}; + + // Recolectar datos de todos los inputs en el formulario + form.querySelectorAll('input, select').forEach(input => { + const key = input.getAttribute('data-key'); + if (!key) return; + + let value; + if (input.type === 'checkbox') { + value = input.checked; + } else if (input.type === 'number') { + value = Number(input.value); + } else { + value = input.value; + } + + // Manejar claves anidadas (por ejemplo: "parent.child") + const keys = key.split('.'); + let current = formData; + for (let i = 0; i < keys.length - 1; i++) { + current[keys[i]] = current[keys[i]] || {}; + current = current[keys[i]]; + } + current[keys[keys.length - 1]] = value; + }); + + // Enviar datos al servidor + const response = await fetch(`/api/config/${level}?group=${currentGroup}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + if (result.status === 'success') { + alert('Configuración guardada correctamente'); + // Recargar el formulario para mostrar los datos actualizados + const configResponse = await fetch(`/api/config/${level}?group=${currentGroup}`); + const updatedData = await configResponse.json(); + await renderForm(`level${level}-form`, updatedData); + } else { + throw new Error(result.message || 'Error desconocido'); + } + } catch (error) { + console.error('Error saving config:', error); + alert('Error guardando la configuración: ' + error.message); + } +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 9195dbf..f9e4a8d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,963 +5,7 @@ Script Parameter Manager - - + @@ -990,7 +34,7 @@
-

Configuración General (Nivel 1)

+

Configuración Base