2 Commits

Author SHA1 Message Date
egutierrez e1b756ac99 feat: Implement cookie extraction script for Chrome v20 and enhance browser interaction 2025-06-01 15:31:13 +02:00
egutierrez 628cddc3ae feat: Enhance logging and add chart endpoints
- Updated LoggerDB to remove all active sinks on initialization.
- Added a new PostgresCredencial setup in notas_mmr.py for database connection.
- Replaced print statements with logger calls for better logging in notas_mmr.py.
- Introduced new FastAPI endpoints for various chart types (bar, line, pie, scatter).
- Created Editor_biblioteca.css for styling the rich text editor.
- Implemented Editor_Test.tsx to test the rich text editor functionality.
2025-06-01 00:33:48 +02:00
26 changed files with 5809 additions and 409 deletions
@@ -0,0 +1,57 @@
from fastapi import APIRouter
router = APIRouter()
@router.get("/bar")
def get_bar_chart():
return {
"xAxis": {"type": "category", "data": ["Ene", "Feb", "Mar", "Abr"]},
"yAxis": {"type": "value"},
"series": [{"data": [5, 20, 36, 10], "type": "bar"}]
}
@router.get("/line")
def get_line_chart():
return {
"xAxis": {"type": "category", "data": ["Semana 1", "Semana 2", "Semana 3", "Semana 4"]},
"yAxis": {"type": "value"},
"series": [{"data": [15, 25, 18, 30], "type": "line"}]
}
@router.get("/pie")
def get_pie_chart():
return {
"tooltip": {"trigger": "item"},
"legend": {"top": "5%", "left": "center"},
"series": [{
"name": "Accesos",
"type": "pie",
"radius": ["40%", "70%"],
"avoidLabelOverlap": False,
"itemStyle": {"borderRadius": 10, "borderColor": "#fff", "borderWidth": 2},
"label": {"show": False, "position": "center"},
"emphasis": {
"label": {"show": True, "fontSize": 16, "fontWeight": "bold"}
},
"labelLine": {"show": False},
"data": [
{"value": 1048, "name": "Search"},
{"value": 735, "name": "Direct"},
{"value": 580, "name": "Email"},
{"value": 484, "name": "Union Ads"},
{"value": 300, "name": "Video Ads"}
]
}]
}
@router.get("/scatter")
def get_scatter_chart():
return {
"xAxis": {},
"yAxis": {},
"series": [{
"symbolSize": 20,
"data": [[10, 8], [20, 20], [30, 10], [40, 30], [50, 15]],
"type": "scatter"
}]
}
@@ -8,6 +8,10 @@ from backend.db.conexion import get_conexion
from backend.services.text_manager_srvc import * from backend.services.text_manager_srvc import *
from src.ConexionSql.Postgres_conexion import PostgresConexion from src.ConexionSql.Postgres_conexion import PostgresConexion
from entrypoint.init_db import db_credencial
from src.Logger.logger_db import LoggerDB, logger
LoggerDB(db_credencial, "logger_textos", created_by="sistema")
router = APIRouter() router = APIRouter()
+3 -1
View File
@@ -1,8 +1,10 @@
# backend/api/router.py # backend/api/router.py
from fastapi import APIRouter from fastapi import APIRouter
from backend.api.v1.endpoints import ping, text_manager_endpoint from backend.api.v1.endpoints import ping, text_manager_endpoint, charts_examples as charts
router = APIRouter() router = APIRouter()
router.include_router(ping.router, prefix="/api/v1/ping") router.include_router(ping.router, prefix="/api/v1/ping")
router.include_router(text_manager_endpoint.router, prefix="/api/v1/text_manager") router.include_router(text_manager_endpoint.router, prefix="/api/v1/text_manager")
router.include_router(charts.router, prefix="/api/v1/charts")
+21 -24
View File
@@ -8,46 +8,48 @@ from src.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca, NotaRe
from sqlalchemy import MetaData from sqlalchemy import MetaData
from backend.schemas.text_manager_schema import NotaInput from backend.schemas.text_manager_schema import NotaInput
from entrypoint.init_db import db_credencial
from src.Logger.logger_db import LoggerDB, logger
LoggerDB(db_credencial, "logger_textos", created_by="sistema")
def crear_biblioteca(nombre_biblioteca: str, conexion: PostgresConexion, descripcion: str = None): def crear_biblioteca(nombre_biblioteca: str, conexion: PostgresConexion, descripcion: str = None):
print("[INICIO] Creando biblioteca...") logger.info("[INICIO] Creando biblioteca...")
try: try:
print("[Paso 1] Obteniendo credencial...") logger.info("[Paso 1] Obteniendo credencial...")
cred_repo = OpenAICredencialRepo(conexion) cred_repo = OpenAICredencialRepo(conexion)
credencial = cred_repo.get_by_id("OPAK20250513-61b29978b7604031014") credencial = cred_repo.get_by_id("OPAK20250513-61b29978b7604031014")
print("[OK] Credencial obtenida:", credencial.titulo if credencial else "❌ None") logger.debug(f"[OK] Credencial obtenida: {credencial.titulo if credencial else '❌ None'}")
print("[Paso 2] Instanciando embedder...") logger.info("[Paso 2] Instanciando embedder...")
embedder = OpenAIEmbedder(credencial, model="text-embedding-3-large") embedder = OpenAIEmbedder(credencial, model="text-embedding-3-large")
print("[OK] Embedder instanciado") logger.debug("[OK] Embedder instanciado")
print("[Paso 3] Instanciando biblioteca...") logger.info("[Paso 3] Instanciando biblioteca...")
biblioteca = Biblioteca( biblioteca = Biblioteca(
nombre=nombre_biblioteca, nombre=nombre_biblioteca,
embedder=embedder, embedder=embedder,
descripcion=descripcion descripcion=descripcion
) )
print(f"[OK] Biblioteca instanciada con ID: {biblioteca.id}") logger.debug(f"[OK] Biblioteca instanciada con ID: {biblioteca.id}")
print("[Paso 4] Guardando en base de datos...") logger.info("[Paso 4] Guardando en base de datos...")
repo = BibliotecaRepo(conexion) repo = BibliotecaRepo(conexion)
repo.add(biblioteca=biblioteca) repo.add(biblioteca=biblioteca)
print("[OK] Biblioteca guardada") logger.success("[OK] Biblioteca guardada")
print("[Paso 5] Generando modelo de notas...") logger.info("[Paso 5] Generando modelo de notas...")
biblioteca.generar_modelo_notas(conexion) biblioteca.generar_modelo_notas(conexion)
print("[OK] Modelo de notas generado") logger.success("[OK] Modelo de notas generado")
print("[FIN] Biblioteca creada correctamente") logger.success("[FIN] Biblioteca creada correctamente")
return { return {
"mensaje": f"Biblioteca '{nombre_biblioteca}' creada con éxito.", "mensaje": f"Biblioteca '{nombre_biblioteca}' creada con éxito.",
"id": biblioteca.id "id": biblioteca.id
} }
except Exception as e: except Exception as e:
print("[ERROR] Ocurrió una excepción:", str(e)) logger.exception("[ERROR] Ocurrió una excepción:")
raise raise
@@ -64,6 +66,7 @@ def listar_bibliotecas(conexion: PostgresConexion) -> list[dict]:
for b in bibliotecas for b in bibliotecas
] ]
def agregar_nota_a_biblioteca( def agregar_nota_a_biblioteca(
conexion: PostgresConexion, conexion: PostgresConexion,
biblioteca_id: str, biblioteca_id: str,
@@ -73,25 +76,19 @@ def agregar_nota_a_biblioteca(
conexiones: list[str] = None, conexiones: list[str] = None,
resumen: str = "" resumen: str = ""
): ):
# Obtener la biblioteca
repo_biblioteca = BibliotecaRepo(conexion) repo_biblioteca = BibliotecaRepo(conexion)
biblioteca = repo_biblioteca.get_by_id(biblioteca_id) biblioteca = repo_biblioteca.get_by_id(biblioteca_id)
if biblioteca is None: if biblioteca is None:
raise ValueError(f"No se encontró la biblioteca con ID {biblioteca_id}") raise ValueError(f"No se encontró la biblioteca con ID {biblioteca_id}")
# Crear objeto Nota
nota = Nota( nota = Nota(
titulo=titulo, titulo=titulo,
texto=texto, texto=texto,
tags=tags or [], tags=tags or [],
conexiones=conexiones or [], conexiones=conexiones or [],
resumen=resumen or "", resumen=resumen or "",
# vector=biblioteca.embedder.embed_text(texto),
# vector_resumen=biblioteca.embedder.embed_text(resumen) if resumen else None
) )
# Mostrar atributos seguros logger.debug(
print(
f"[DEBUG] Nota creada: titulo='{nota.titulo}', " f"[DEBUG] Nota creada: titulo='{nota.titulo}', "
f"texto_len={len(nota.texto)}, " f"texto_len={len(nota.texto)}, "
f"tags={len(nota.tags)}, " f"tags={len(nota.tags)}, "
@@ -99,7 +96,6 @@ def agregar_nota_a_biblioteca(
f"resumen_len={len(nota.resumen)}" f"resumen_len={len(nota.resumen)}"
) )
# Preparar tabla y modelo de nota
metadata = MetaData() metadata = MetaData()
tabla, ModeloNota = generar_tabla_nota_para_biblioteca( tabla, ModeloNota = generar_tabla_nota_para_biblioteca(
biblioteca.nombre, biblioteca.nombre,
@@ -108,7 +104,6 @@ def agregar_nota_a_biblioteca(
) )
metadata.create_all(conexion.get_engine()) metadata.create_all(conexion.get_engine())
# Guardar la nota
repo_nota = NotaRepo(conexion.get_session(), ModeloNota) repo_nota = NotaRepo(conexion.get_session(), ModeloNota)
nota_id = repo_nota.add(nota) nota_id = repo_nota.add(nota)
@@ -117,7 +112,7 @@ def agregar_nota_a_biblioteca(
"nota_id": nota_id "nota_id": nota_id
} }
print(f"[SUCCESS] {resultado['mensaje']}") logger.success(f"[SUCCESS] {resultado['mensaje']}")
return resultado return resultado
@@ -160,6 +155,7 @@ def eliminar_nota(conexion: PostgresConexion, biblioteca_id: str, nota_id: str)
fue_eliminada = repo_nota.delete_by_id(nota_id) fue_eliminada = repo_nota.delete_by_id(nota_id)
if fue_eliminada: if fue_eliminada:
logger.success(f"Nota '{nota_id}' eliminada correctamente.")
return {"mensaje": f"Nota '{nota_id}' eliminada correctamente."} return {"mensaje": f"Nota '{nota_id}' eliminada correctamente."}
else: else:
raise ValueError(f"No se encontró la nota con ID: {nota_id}") raise ValueError(f"No se encontró la nota con ID: {nota_id}")
@@ -186,6 +182,7 @@ def actualizar_nota(conexion: PostgresConexion, biblioteca_id: str, nota_id: str
fue_actualizada = repo_nota.update(nota_id, nota_actualizada) fue_actualizada = repo_nota.update(nota_id, nota_actualizada)
if fue_actualizada: if fue_actualizada:
logger.success(f"Nota '{nota_id}' actualizada correctamente.")
return {"mensaje": f"Nota '{nota_id}' actualizada correctamente."} return {"mensaje": f"Nota '{nota_id}' actualizada correctamente."}
else: else:
raise ValueError(f"No se encontró la nota con ID: {nota_id}") raise ValueError(f"No se encontró la nota con ID: {nota_id}")
+2924 -18
View File
File diff suppressed because it is too large Load Diff
+11 -3
View File
@@ -20,18 +20,25 @@
"storybook:build": "storybook build" "storybook:build": "storybook build"
}, },
"dependencies": { "dependencies": {
"@mantine/core": "8.0.0", "@mantine/core": "^8.0.1",
"@mantine/hooks": "8.0.0", "@mantine/hooks": "^8.0.1",
"@mantine/tiptap": "^8.0.1",
"@react-three/fiber": "^9.1.2", "@react-three/fiber": "^9.1.2",
"@tabler/icons": "^3.31.0", "@tabler/icons": "^3.31.0",
"@tabler/icons-react": "^3.31.0", "@tabler/icons-react": "^3.31.0",
"@tiptap/react": "^2.12.0",
"@tiptap/starter-kit": "^2.12.0",
"@uiw/react-markdown-preview": "^5.1.4",
"@uiw/react-md-editor": "^4.0.7",
"axios": "^1.9.0", "axios": "^1.9.0",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"echarts-for-react": "^3.0.2", "echarts-for-react": "^3.0.2",
"marked": "^15.0.12",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-rnd": "^10.5.2", "react-rnd": "^10.5.2",
"react-router-dom": "^7.4.0" "react-router-dom": "^7.4.0",
"turndown": "^7.2.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.23.0", "@eslint/js": "^9.23.0",
@@ -46,6 +53,7 @@
"@types/react": "^19.0.12", "@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@types/three": "^0.176.0", "@types/three": "^0.176.0",
"@types/turndown": "^5.0.5",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.23.0", "eslint": "^9.23.0",
"eslint-config-mantine": "^4.0.3", "eslint-config-mantine": "^4.0.3",
+41 -12
View File
@@ -6,34 +6,63 @@ import { Grid_Dashboard } from './pages/Grid_dashboard'; // Ajusta si está en o
import { Biblioteca } from './pages/Biblioteca'; import { Biblioteca } from './pages/Biblioteca';
import { VisualizacionesRandom } from './pages/Visualizaciones_Random'; import { VisualizacionesRandom } from './pages/Visualizaciones_Random';
import { Camara_noir } from './pages/Camaras_noir'; import { Camara_noir } from './pages/Camaras_noir';
import EditorTest from "./pages/Editor_Test"
const router = createBrowserRouter([ const router = createBrowserRouter([
// Home Principal
{ {
path: '/', path: '/',
element: <HomePage />, element: <HomePage />,
}, },
// LLMs
{ {
path: '/Consulta_API', path: '/llms/Biblioteca',
element: <Consulta_API />,
},
{
path: '/Grid_Dashboard',
element: <Grid_Dashboard />,
},
{
path: '/Biblioteca',
element: <Biblioteca />, element: <Biblioteca />,
}, },
{ {
path: '/analytics/Visualizaciones_Random', path: '/llms/editortest',
element: <VisualizacionesRandom />, element: <EditorTest />,
}, },
// Camara
{ {
path: '/analytics/Camaras', path: '/camara/principal',
element: <Camara_noir />, element: <Camara_noir />,
}, },
// Experimentos
{
path: '/experiments/Consulta_API',
element: <Consulta_API />,
},
{
path: '/experiments/Grid_Dashboard',
element: <Grid_Dashboard />,
},
{
path: '/experiments/Visualizaciones_Random',
element: <VisualizacionesRandom />,
},
// Error 404
{ {
path: '*', path: '*',
element: <Error_404 />, element: <Error_404 />,
+3 -1
View File
@@ -11,7 +11,9 @@ export { default as IconSettings } from './outlined/settings.svg?react';
export { default as IconArrowBarLeft } from './outlined/arrow-bar-left.svg?react'; export { default as IconArrowBarLeft } from './outlined/arrow-bar-left.svg?react';
export { default as IconArrowBarRight } from './outlined/arrow-bar-right.svg?react'; export { default as IconArrowBarRight } from './outlined/arrow-bar-right.svg?react';
export { default as IconCheck } from './outlined/check.svg?react'; export { default as IconCheck } from './outlined/check.svg?react';
export { default as CameraPlus } from './outlined/camera-plus.svg?react';
export { default as Flask } from './outlined/flask.svg?react';
export { default as Users } from './outlined/users.svg?react';
// FILLED // FILLED
export { default as IconHomeFilled } from './filled/home.svg?react'; export { default as IconHomeFilled } from './filled/home.svg?react';
@@ -0,0 +1,43 @@
/* Editor_biblioteca.css */
/* En Editor_biblioteca.css */
.tiptap {
min-height: 200px;
padding: 8px;
/* white-space: pre-wrap; */
font-size: 14px;
line-height: 1.4;
}
.tiptap p {
margin: 0 !important;
padding: 0 !important;
}
.tiptap h1, .tiptap h2, .tiptap h3 {
margin-top: 0.8em;
margin-bottom: 0.4em;
}
.tiptap blockquote {
margin: 0.6em 0;
/* padding-left: 1em; */
border-left: 3px solid #888;
color: #aaa;
}
.mantine-RichTextEditor-toolbar {
background-color: #1e1e1e; /* o el color de tu layout */
border-radius: 6px;
padding: 6px 8px;
}
.mantine-RichTextEditor-controlIcon {
color: white !important;
stroke: white !important;
}
.mantine-RichTextEditor-control {
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
}
+6 -5
View File
@@ -7,14 +7,15 @@ import {
IconHome2, IconHome2,
IconSettings, IconSettings,
IconUserOutline as IconUser, IconUserOutline as IconUser,
CameraPlus,
Flask,
Users
} from '../assets/icons'; // ajusta según tu estructura de proyecto } from '../assets/icons'; // ajusta según tu estructura de proyecto
export const mainLinksdata = [ export const mainLinksdata = [
{ icon: IconHome2, label: 'Home' }, { icon: IconHome2, label: 'Home' },
{ icon: IconGauge, label: 'Dashboard' }, { icon: Users, label: 'AgentesLLMs' },
{ icon: IconDeviceDesktopAnalytics, label: 'Analytics' }, { icon: CameraPlus, label: 'Camera' },
{ icon: IconCalendarStats, label: 'Releases' }, { icon: Flask, label: 'Experimentos' },
{ icon: IconUser, label: 'Account' },
{ icon: IconFingerprint, label: 'Security' },
{ icon: IconSettings, label: 'Settings' }, { icon: IconSettings, label: 'Settings' },
]; ];
+25 -22
View File
@@ -1,35 +1,38 @@
// src/data/submenuLinks.ts // src/data/submenuLinks.ts
export const submenuLinks = { export const submenuLinks = {
// Home Principal
Home: [ Home: [
{ label: 'Inicio', to: '/' }, { label: 'Inicio', to: '/' },
{ label: 'Consulta Api', to: '/Consulta_API' },
{ label: 'Biblioteca', to: '/Biblioteca' },
], ],
Dashboard: [
{ label: 'Resumen', to: '/dashboard/resumen' }, // Experimentos
{ label: 'Grid_Dashboard', to: '/Grid_Dashboard' }, Experimentos: [
{ label: 'Estadísticas', to: '/dashboard/estadisticas' }, { label: 'Consulta Api', to: '/experiments/Consulta_API' },
{ label: 'Usuarios', to: '/dashboard/usuarios' }, { label: 'Visualizaciones_Random', to: '/experiments/Visualizaciones_Random' },
{ label: 'Grid_Dashboard', to: '/experiments/Grid_Dashboard' },
], ],
Analytics: [
{ label: 'Visualizaciones_Random', to: '/analytics/Visualizaciones_Random' }, // Camara
{ label: 'Camaras', to: '/analytics/Camaras' }, Camera: [
{ label: 'Tendencias', to: '/analytics/tendencias' }, { label: 'Camara principal', to: '/camara/principal' },
], ],
Releases: [
{ label: 'Notas de versión', to: '/releases/notas-de-version' }, // LLms
{ label: 'Historial', to: '/releases/historial' },
], AgentesLLMs: [
Account: [ { label: 'LLMs', to: '/llms' },
{ label: 'Perfil', to: '/account/perfil' }, { label: 'Chat', to: '/llms/chat' },
{ label: 'Suscripciones', to: '/account/suscripciones' }, { label: 'Documentos', to: '/llms/documentos' },
], { label: 'Biblioteca', to: '/llms/Biblioteca' },
Security: [ { label: 'test', to: '/llms/editortest' },
{ label: 'Contraseña', to: '/security/contraseña' },
{ label: '2FA', to: '/security/2fa' },
], ],
// Settings
Settings: [ Settings: [
{ label: 'Preferencias', to: '/settings/preferencias' }, { label: 'Preferencias', to: '/settings/preferencias' },
{ label: 'Notificaciones', to: '/settings/notificaciones' }, { label: 'Notificaciones', to: '/settings/notificaciones' },
+175 -229
View File
@@ -1,8 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
AppShell,
Stack, Stack,
Card,
Text, Text,
Title, Title,
ScrollArea, ScrollArea,
@@ -10,17 +8,25 @@ import {
Button, Button,
TextInput, TextInput,
Modal, Modal,
Box, Box
Loader,
Textarea
} from '@mantine/core'; } from '@mantine/core';
import { AppShellWithMenu } from '../components/Appshell/Appshell'; import { AppShellWithMenu } from '../components/Appshell/Appshell';
import axios from 'axios'; import axios from 'axios';
import { RichTextEditor } from '@mantine/tiptap';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import '@mantine/tiptap/styles.css';
import TurndownService from 'turndown';
import { marked } from 'marked';
import '../components/Editor_biblioteca.css';
type Nota = { type Nota = {
id: string; id: string;
titulo: string; titulo: string;
texto: string; texto: string; // Markdown
}; };
type Biblioteca = { type Biblioteca = {
@@ -30,30 +36,66 @@ type Biblioteca = {
notas: Nota[]; notas: Nota[];
}; };
const turndownService = new TurndownService({
headingStyle: 'atx',
bulletListMarker: '-',
codeBlockStyle: 'fenced',
emDelimiter: '*',
strongDelimiter: '**',
});
export function Biblioteca() { export function Biblioteca() {
const [bibliotecas, setBibliotecas] = useState<Biblioteca[]>([]); const [bibliotecas, setBibliotecas] = useState<Biblioteca[]>([]);
const [bibliotecaSeleccionada, setBibliotecaSeleccionada] = useState<Biblioteca | null>(null); const [bibliotecaSeleccionada, setBibliotecaSeleccionada] = useState<Biblioteca | null>(null);
const [modalAbierto, setModalAbierto] = useState(false); const [notaSeleccionada, setNotaSeleccionada] = useState<Nota | null>(null);
const [tituloNota, setTituloNota] = useState('');
const [contenidoNota, setContenidoNota] = useState('');
const [loadingNotas, setLoadingNotas] = useState(false);
const [notaEnEdicion, setNotaEnEdicion] = useState<Nota | null>(null);
const [modalEditarAbierto, setModalEditarAbierto] = useState(false);
const [modalNuevaBiblio, setModalNuevaBiblio] = useState(false); const [modalNuevaBiblio, setModalNuevaBiblio] = useState(false);
const [nombreBiblio, setNombreBiblio] = useState(''); const [nombreBiblio, setNombreBiblio] = useState('');
const [descripcionBiblio, setDescripcionBiblio] = useState(''); const [descripcionBiblio, setDescripcionBiblio] = useState('');
const [loadingNuevaBiblio, setLoadingNuevaBiblio] = useState(false); const [loadingNuevaBiblio, setLoadingNuevaBiblio] = useState(false);
const editor = useEditor({
extensions: [StarterKit],
content: '',
onUpdate: ({ editor }) => {
const html = editor.getHTML();
const markdown = turndownService.turndown(html);
setNotaSeleccionada((prev) =>
prev ? { ...prev, texto: markdown } : null
);
}
});
useEffect(() => {
console.log('🟡 editor:', editor);
console.log('🟠 isDestroyed:', editor?.isDestroyed);
console.log('🟢 isEditable:', editor?.isEditable);
}, [editor]);
useEffect(() => {
fetchBibliotecas();
}, []);
useEffect(() => {
if (!editor || !notaSeleccionada || editor.isDestroyed) return;
(async () => {
try {
const markdown = notaSeleccionada.texto;
const html = await marked.parse(markdown);
editor.commands.setContent(html);
setTimeout(() => editor.commands.focus(), 100);
} catch (err) {
console.error('❌ Error al hacer setContent:', err);
}
})();
}, [notaSeleccionada?.id, editor]);
const fetchBibliotecas = async () => { const fetchBibliotecas = async () => {
try { try {
const res = await axios.get('/api/v1/text_manager/list'); const res = await axios.get('/api/v1/text_manager/list');
console.log('📦 Respuesta del backend:', res.data); if (!Array.isArray(res.data)) return;
if (!Array.isArray(res.data)) {
console.error('❌ La respuesta no es un array:', res.data);
return;
}
const bibliotecasConNotas = await Promise.all( const bibliotecasConNotas = await Promise.all(
res.data.map(async (biblio: Omit<Biblioteca, 'notas'>) => { res.data.map(async (biblio: Omit<Biblioteca, 'notas'>) => {
@@ -68,119 +110,59 @@ export function Biblioteca() {
} }
}; };
const crearBiblioteca = async () => { const crearBiblioteca = async () => {
setLoadingNuevaBiblio(true); // 🔄 Activa el loader en el botón setLoadingNuevaBiblio(true);
try {
// Llamada a backend
await axios.post('/api/v1/text_manager/biblioteca', {
nombre_biblioteca: nombreBiblio,
descripcion: descripcionBiblio,
});
// 🧼 Limpia formularios
setNombreBiblio('');
setDescripcionBiblio('');
// 🔒 Cierra el modal
setModalNuevaBiblio(false);
// 🔄 Refresca la lista de bibliotecas
await fetchBibliotecas();
} catch (error) {
console.error('❌ Error al crear biblioteca:', error);
} finally {
setLoadingNuevaBiblio(false); // ✅ Apaga el loader
}
};
const agregarNota = async () => {
if (!bibliotecaSeleccionada) return;
try { try {
await axios.post(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}`, { await axios.post('/api/v1/text_manager/biblioteca', {
titulo: tituloNota, nombre_biblioteca: nombreBiblio,
texto: contenidoNota, descripcion: descripcionBiblio,
tags: [],
conexiones: [],
resumen: '',
}); });
setNombreBiblio('');
setLoadingNotas(true); setDescripcionBiblio('');
const nuevasNotas = await axios.get(`/api/v1/text_manager/nota/list/${bibliotecaSeleccionada.id}`); setModalNuevaBiblio(false);
const nuevasBibliotecas = bibliotecas.map((b) => await fetchBibliotecas();
b.id === bibliotecaSeleccionada.id ? { ...b, notas: nuevasNotas.data as Nota[] } : b
);
setBibliotecas(nuevasBibliotecas);
setBibliotecaSeleccionada(nuevasBibliotecas.find((b) => b.id === bibliotecaSeleccionada.id) || null);
setTituloNota('');
setContenidoNota('');
setModalAbierto(false);
} catch (error) { } catch (error) {
console.error('Error al agregar nota:', error); console.error('Error al crear biblioteca:', error);
} finally { } finally {
setLoadingNotas(false); setLoadingNuevaBiblio(false);
} }
}; };
useEffect(() => {
fetchBibliotecas();
}, []);
// Eliminar nota
const eliminarNota = async (notaId: string) => {
if (!bibliotecaSeleccionada) return;
try {
await axios.delete(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaId}`);
// Solo actualiza la biblioteca actual
const nuevasNotas = await axios.get(`/api/v1/text_manager/nota/list/${bibliotecaSeleccionada.id}`);
const nuevasBibliotecas = bibliotecas.map((b) =>
b.id === bibliotecaSeleccionada.id ? { ...b, notas: nuevasNotas.data as Nota[] } : b
);
setBibliotecas(nuevasBibliotecas);
setBibliotecaSeleccionada(nuevasBibliotecas.find(b => b.id === bibliotecaSeleccionada.id) || null);
} catch (error) {
console.error("Error al eliminar nota:", error);
}
};
// Editar nota
const abrirModalEditar = (nota: Nota) => {
setNotaEnEdicion(nota);
setModalEditarAbierto(true);
};
// Guardar cambios de edición
const guardarEdicionNota = async () => { const guardarEdicionNota = async () => {
if (!notaEnEdicion || !bibliotecaSeleccionada) return; if (!notaSeleccionada || !bibliotecaSeleccionada) return;
try { try {
await axios.put(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaEnEdicion.id}`, { await axios.put(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaSeleccionada.id}`, {
titulo: notaEnEdicion.titulo, titulo: notaSeleccionada.titulo,
texto: notaEnEdicion.texto, texto: notaSeleccionada.texto,
tags: [], tags: [],
conexiones: [], conexiones: [],
resumen: "" resumen: ""
}); });
setModalEditarAbierto(false);
setNotaEnEdicion(null);
await fetchBibliotecas(); await fetchBibliotecas();
} catch (error) { } catch (error) {
console.error("Error al actualizar nota:", error); console.error("Error al actualizar nota:", error);
} }
}; };
const eliminarNota = async (notaId: string) => {
if (!bibliotecaSeleccionada) return;
try {
await axios.delete(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaId}`);
await fetchBibliotecas();
setNotaSeleccionada(null);
} catch (error) {
console.error("Error al eliminar nota:", error);
}
};
return ( return (
<AppShellWithMenu> <AppShellWithMenu>
<Box display="flex" h="100%"> <Box display="flex" h="100%" style={{ overflow: 'hidden' }}>
<Box w={240} p="md"> <Box w={240} p="md">
<ScrollArea h="100%"> <ScrollArea h="100%">
<Stack> <Stack gap="md">
<Button color="teal" onClick={fetchBibliotecas}>🔄 Recuperar bibliotecas</Button> <Button color="teal" onClick={fetchBibliotecas}>🔄 Recuperar bibliotecas</Button>
<Button color="grape" variant="outline" onClick={() => setModalNuevaBiblio(true)}> Nueva biblioteca</Button> <Button color="grape" variant="outline" onClick={() => setModalNuevaBiblio(true)}> Nueva biblioteca</Button>
{bibliotecas.map((biblio) => ( {bibliotecas.map((biblio) => (
<Button <Button
key={biblio.id} key={biblio.id}
@@ -188,7 +170,10 @@ const crearBiblioteca = async () => {
fullWidth fullWidth
variant={biblio.id === bibliotecaSeleccionada?.id ? 'filled' : 'light'} variant={biblio.id === bibliotecaSeleccionada?.id ? 'filled' : 'light'}
color="blue" color="blue"
onClick={() => setBibliotecaSeleccionada(biblio)} onClick={() => {
setBibliotecaSeleccionada(biblio);
setNotaSeleccionada(null);
}}
> >
{biblio.nombre} {biblio.nombre}
</Button> </Button>
@@ -197,131 +182,97 @@ const crearBiblioteca = async () => {
</ScrollArea> </ScrollArea>
</Box> </Box>
<Box p="md" style={{ flex: 1 }}> <Box w={240} p="md">
{bibliotecaSeleccionada ? ( <ScrollArea h="100%">
<Stack> <Stack gap="md">
<Title order={2}>{bibliotecaSeleccionada.nombre}</Title> <Title order={4}>Notas</Title>
<Group> <Button
<Button onClick={() => setModalAbierto(true)}>Agregar nota</Button> color="green"
</Group> variant="outline"
<Group> fullWidth
{loadingNotas ? ( onClick={async () => {
<Loader /> if (!bibliotecaSeleccionada) return;
) : ( try {
bibliotecaSeleccionada.notas.map((nota) => ( const res = await axios.post(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}`, {
titulo: 'Nueva nota',
texto: '',
tags: [],
conexiones: [],
resumen: ''
});
const nuevaNota: Nota = res.data;
await fetchBibliotecas();
setNotaSeleccionada(nuevaNota);
} catch (error) {
console.error('Error al crear nota:', error);
}
}}
>
Nueva nota
</Button>
{bibliotecaSeleccionada?.notas.map((nota) => (
<Button
key={nota.id}
fullWidth
variant={notaSeleccionada?.id === nota.id ? 'filled' : 'light'}
color="gray"
onClick={() => setNotaSeleccionada(nota)}
>
{nota.titulo}
</Button>
))}
</Stack>
</ScrollArea>
</Box>
// Cards de notas <Box p="md" style={{ flex: 1, overflow: 'hidden', minWidth: 0 }}>
{notaSeleccionada ? (
<Card <Stack gap="sm">
key={nota.id} <TextInput
shadow="sm" label="Título"
padding="lg" size="lg"
radius="md" styles={{ input: { fontSize: 20, fontWeight: 600 } }}
withBorder value={notaSeleccionada.titulo}
style={{ onChange={(e) =>
width: 300, setNotaSeleccionada((prev) => prev ? { ...prev, titulo: e.currentTarget.value } : null)
height: 250, // Altura fija para asegurar la separación }
display: 'flex', />
flexDirection: 'column', {editor && !editor.isDestroyed && (
justifyContent: 'space-between', <RichTextEditor
}} editor={editor}
> miw={0}
<div> style={{ fontSize: 14, minHeight: 200 }}
<Title order={4} style={{ marginBottom: 10 }}>{nota.titulo}</Title> classNames={{ content: 'tiptap' }}
<Text>{nota.texto}</Text> >
</div> <RichTextEditor.Toolbar sticky stickyOffset={0}>
<RichTextEditor.ControlsGroup>
<Box mt="md" style={{ display: 'flex', justifyContent: 'flex-end' }}> <RichTextEditor.Bold />
<Button <RichTextEditor.Italic />
size="xs" <RichTextEditor.Strikethrough />
variant="light" <RichTextEditor.ClearFormatting />
color="blue" <RichTextEditor.H1 />
onClick={() => abrirModalEditar(nota)} <RichTextEditor.H2 />
> <RichTextEditor.Blockquote />
Editar <RichTextEditor.CodeBlock />
</Button> </RichTextEditor.ControlsGroup>
</Box> </RichTextEditor.Toolbar>
</Card> {/* tabIndex removido */}
<RichTextEditor.Content className="tiptap" />
// Fin de notas en cards </RichTextEditor>
)}
)) <Group mt="sm">
)} <Button color="blue" onClick={guardarEdicionNota}>💾 Guardar</Button>
<Button color="red" onClick={() => eliminarNota(notaSeleccionada.id)}>🗑 Eliminar</Button>
</Group> </Group>
</Stack> </Stack>
) : ( ) : (
<Stack> <Text>Selecciona una nota para editar</Text>
<Text>Selecciona una biblioteca</Text>
</Stack>
)} )}
</Box> </Box>
</Box> </Box>
{/* Modal para agregar */} <Modal opened={modalNuevaBiblio} onClose={() => setModalNuevaBiblio(false)} title="Crear nueva biblioteca">
<Modal opened={modalAbierto} onClose={() => setModalAbierto(false)} title="Agregar nueva nota"> <Stack gap="md">
<Stack>
<TextInput
label="Título"
value={tituloNota}
onChange={(event) => setTituloNota(event.currentTarget.value)}
/>
<Textarea
label="Contenido"
minRows={6}
autosize
value={contenidoNota}
onChange={(event) => setContenidoNota(event.currentTarget.value)}
/>
<Button onClick={agregarNota}>Guardar</Button>
</Stack>
</Modal>
{/* Modal para editar */}
<Modal opened={modalEditarAbierto} onClose={() => setModalEditarAbierto(false)} title="Editar nota">
<Stack>
<TextInput
label="Título"
value={notaEnEdicion?.titulo || ""}
onChange={(e) =>
setNotaEnEdicion((prev) => (prev ? { ...prev, titulo: e.currentTarget.value } : null))
}
/>
<Textarea
label="Contenido"
minRows={6}
autosize
value={notaEnEdicion?.texto || ""}
onChange={(e) =>
setNotaEnEdicion((prev) => (prev ? { ...prev, texto: e.currentTarget.value } : null))
}
/>
<Group grow>
<Button color="blue" onClick={guardarEdicionNota}>
💾 Guardar cambios
</Button>
<Button
color="red"
onClick={async () => {
if (!notaEnEdicion || !bibliotecaSeleccionada) return;
await eliminarNota(notaEnEdicion.id);
setModalEditarAbierto(false);
setNotaEnEdicion(null);
}}
>
🗑 Eliminar nota
</Button>
</Group>
</Stack>
</Modal>
{/* Modal para crear una biblioteca */}
<Modal
opened={modalNuevaBiblio}
onClose={() => setModalNuevaBiblio(false)}
title="Crear nueva biblioteca"
>
<Stack>
<TextInput <TextInput
label="Nombre" label="Nombre"
value={nombreBiblio} value={nombreBiblio}
@@ -334,14 +285,9 @@ const crearBiblioteca = async () => {
onChange={(e) => setDescripcionBiblio(e.currentTarget.value)} onChange={(e) => setDescripcionBiblio(e.currentTarget.value)}
disabled={loadingNuevaBiblio} disabled={loadingNuevaBiblio}
/> />
<Button onClick={crearBiblioteca} loading={loadingNuevaBiblio}> <Button onClick={crearBiblioteca} loading={loadingNuevaBiblio}>Crear</Button>
Crear
</Button>
</Stack> </Stack>
</Modal> </Modal>
</AppShellWithMenu> </AppShellWithMenu>
); );
} }
+27
View File
@@ -0,0 +1,27 @@
import { RichTextEditor } from '@mantine/tiptap';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import '@mantine/tiptap/styles.css';
export default function EditorTest() {
const editor = useEditor({
extensions: [StarterKit],
content: '<p>Prueba aquí. Presiona ENTER o ESPACIO.</p>',
});
return (
<div style={{ padding: 40 }}>
{editor && (
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky stickyOffset={0}>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
)}
</div>
);
}
@@ -10,7 +10,7 @@ function useChartOption(endpoint: string) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
fetch(`/api/${endpoint}`) fetch(`/api/v1/charts/${endpoint}`)
.then((res) => res.json()) .then((res) => res.json())
.then((json) => setOption(json)) .then((json) => setOption(json))
.catch(console.error) .catch(console.error)
+1714 -18
View File
File diff suppressed because it is too large Load Diff
+179
View File
@@ -0,0 +1,179 @@
import os
import sys
import json
import binascii
import ctypes
import base64
import sqlite3
import pandas as pd
import pathlib
from Crypto.Cipher import AES, ChaCha20_Poly1305
from pypsexec.client import Client
"""
Este script extrae cookies v20 de Google Chrome y las guarda en un archivo CSV.
Requiere privilegios de administrador para acceder a los datos de Chrome.
Conseguido para poder extraer cookies de Chrome v20, que utiliza un nuevo formato de cifrado.
"""
def is_admin():
try:
return ctypes.windll.shell32.IsUserAnAdmin() != 0
except:
return False
def get_app_bound_key(local_state_path):
with open(local_state_path, "r", encoding="utf-8") as f:
local_state = json.load(f)
return local_state["os_crypt"]["app_bound_encrypted_key"]
def decrypt_app_bound_key(encrypted_key_b64):
arguments = "-c \"" + """import win32crypt
import binascii
encrypted_key = win32crypt.CryptUnprotectData(binascii.a2b_base64('{}'), None, None, None, 0)
print(binascii.b2a_base64(encrypted_key[1]).decode())
""".replace("\n", ";") + "\""
c = Client("localhost")
c.connect()
decrypted_key = None
try:
c.create_service()
assert(binascii.a2b_base64(encrypted_key_b64)[:4] == b"APPB")
stripped_key_b64 = binascii.b2a_base64(binascii.a2b_base64(encrypted_key_b64)[4:]).decode().strip()
encrypted_key_b64_sys, _, _ = c.run_executable(
sys.executable,
arguments=arguments.format(stripped_key_b64),
use_system_account=True
)
decrypted_key_b64, _, _ = c.run_executable(
sys.executable,
arguments=arguments.format(encrypted_key_b64_sys.decode().strip()),
use_system_account=False
)
decrypted_key = binascii.a2b_base64(decrypted_key_b64)[-61:]
finally:
c.remove_service()
c.disconnect()
return decrypted_key
def decrypt_final_key(encrypted_key):
aes_key = bytes.fromhex("B31C6E241AC846728DA9C1FAC4936651CFFB944D143AB816276BCC6DA0284787")
chacha20_key = bytes.fromhex("E98F37D7F4E1FA433D19304DC2258042090E2D1D7EEA7670D41F738D08729660")
flag = encrypted_key[0]
iv = encrypted_key[1:13]
ciphertext = encrypted_key[13:45]
tag = encrypted_key[45:]
if flag == 1:
cipher = AES.new(aes_key, AES.MODE_GCM, nonce=iv)
elif flag == 2:
cipher = ChaCha20_Poly1305.new(key=chacha20_key, nonce=iv)
else:
raise ValueError(f"Unsupported flag: {flag}")
return cipher.decrypt_and_verify(ciphertext, tag)
def decrypt_cookie_v20(encrypted_value, key):
cookie_iv = encrypted_value[3:15]
encrypted_cookie = encrypted_value[15:-16]
cookie_tag = encrypted_value[-16:]
cookie_cipher = AES.new(key, AES.MODE_GCM, nonce=cookie_iv)
decrypted_cookie = cookie_cipher.decrypt_and_verify(encrypted_cookie, cookie_tag)
return decrypted_cookie[32:].decode('utf-8')
def extract_all_v20_cookies():
user_profile = os.environ['USERPROFILE']
local_state_path = rf"{user_profile}\AppData\Local\Google\Chrome\User Data\Local State"
base_profile_path = rf"{user_profile}\AppData\Local\Google\Chrome\User Data"
app_bound_key_b64 = get_app_bound_key(local_state_path)
decrypted_key_raw = decrypt_app_bound_key(app_bound_key_b64)
final_key = decrypt_final_key(decrypted_key_raw)
perfiles_invalidos = {"System Profile", "Guest Profile", "CrashpadMetrics"}
perfiles = [
name for name in os.listdir(base_profile_path)
if os.path.isdir(os.path.join(base_profile_path, name))
and name not in perfiles_invalidos
and os.path.exists(os.path.join(base_profile_path, name, "Network", "Cookies"))
]
all_cookies = []
for profile in perfiles:
db_path = os.path.join(base_profile_path, profile, "Network", "Cookies")
con = sqlite3.connect(pathlib.Path(db_path).as_uri() + "?mode=ro", uri=True)
cur = con.cursor()
r = cur.execute("SELECT host_key, name, path, is_secure, is_httponly, expires_utc, last_access_utc, CAST(encrypted_value AS BLOB) from cookies;")
cookies = cur.fetchall()
con.close()
for row in cookies:
host, name, path, is_secure, is_httponly, expires_utc, last_access_utc, encrypted_value = row
encrypted_value_b64 = base64.b64encode(encrypted_value).decode()
if encrypted_value.startswith(b"v20"):
try:
value = decrypt_cookie_v20(encrypted_value, final_key)
print(f"[✓] {host} {name}: {value}")
all_cookies.append({
"host": host,
"name": name,
"path": path,
"value": value,
"encrypted_value_b64": encrypted_value_b64,
"expires_utc": expires_utc,
"is_secure": is_secure,
"is_httponly": is_httponly,
"last_access_utc": last_access_utc,
"profile": profile,
"is_decrypted": True,
"decrypt_error": ""
})
except Exception as e:
print(f"[x] Error decrypting {host} {name}: {e}")
all_cookies.append({
"host": host,
"name": name,
"path": path,
"value": "",
"encrypted_value_b64": encrypted_value_b64,
"expires_utc": expires_utc,
"is_secure": is_secure,
"is_httponly": is_httponly,
"last_access_utc": last_access_utc,
"profile": profile,
"is_decrypted": False,
"decrypt_error": str(e)
})
return pd.DataFrame(all_cookies)
if __name__ == "__main__":
if not is_admin():
input("Este script necesita ejecutarse como administrador. Presiona Enter para reiniciar con privilegios...")
ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join([sys.argv[0]] + sys.argv[1:]), None, 1)
sys.exit()
print("[*] Extrayendo cookies v20 desde todos los perfiles...")
df = extract_all_v20_cookies()
df.to_csv("cookies_extraidas.csv", index=False, encoding="utf-8")
print(f"[✓] Cookies v20 extraídas: {len(df)}")
print("[✓] Guardado en 'cookies_extraidas.csv'")
@@ -0,0 +1,87 @@
import asyncio
import os
import pyperclip
import re
from src.ScrappingWeb.Scrapper import Scrapper
def sanitizar(nombre: str) -> str:
return re.sub(r'[\\/*?:"<>|]', "_", nombre).strip()[:100]
OUTPUT_DIR = "esquemas_json"
os.makedirs(OUTPUT_DIR, exist_ok=True)
async def main():
ws_id = "F51AC05B27E1DEC4011E67369781596C"
ws_url = f"ws://127.0.0.1:9222/devtools/page/{ws_id}"
scrapper = Scrapper(debugging_url="http://127.0.0.1:9222")
print("🔌 Conectando a pestaña específica...")
tab = scrapper.get_tab(ws_url) or scrapper.get_tab(ws_id)
if not tab:
nuevas_tabs = await scrapper.obtener_tabs_existentes()
tab = next((t for t in nuevas_tabs if t.ws_url.rsplit("/", 1)[-1] == ws_id), None)
if not tab:
print("⚠️ La pestaña con ese ID no se encontró.")
return
elementos = await tab.get_elements_by_css_selector(
"#_0rif_bq-resource-tree > div.cfctest-tree-main.ng-tns-c3578326070-0 > ul > cfc-virtual-scroller > div > div.item-container > div > li"
)
for i, elemento in enumerate(elementos[:12]):
print(f"🖱️ Click #{i + 1}")
clickeable = await elemento.encontrar_hijo_clickeable()
if clickeable:
await clickeable.click()
else:
print(f"⚠️ No se encontró subelemento clickeable en #{i+1}")
continue
await asyncio.sleep(1)
texto_crudo = await elemento.obtener_texto()
nombre_archivo = sanitizar(texto_crudo or f"esquema_item_{i+1}")
print(f"📄 Nombre base del archivo: {nombre_archivo}.txt")
# ✅ Ejecutar JS en el navegador para simular flujo de copia
await tab.evaluar_js("""
(() => {
const boton = document.querySelector('button[id^="_0rif_bqui-table-copy-schema-btn"] span.mdc-button__label > span');
if (boton) boton.click();
})()
""")
await asyncio.sleep(1)
await tab.evaluar_js("""
(() => {
const overlays = document.querySelectorAll("div.cdk-overlay-pane");
for (let overlay of overlays) {
const items = overlay.querySelectorAll("cfc-menu-item .cfc-menu-item-label");
for (let item of items) {
if (item.textContent.includes("Copiar como JSON")) {
item.click();
break;
}
}
}
})()
""")
await asyncio.sleep(1.5)
try:
texto_json = pyperclip.paste()
file_path = os.path.join(OUTPUT_DIR, f"{nombre_archivo}.txt")
with open(file_path, "w", encoding="utf-8") as f:
f.write(texto_json)
print(f"✅ Guardado: {file_path}")
except Exception as e:
print(f"❌ Error al leer el portapapeles o guardar archivo: {e}")
if __name__ == "__main__":
asyncio.run(main())
+80
View File
@@ -0,0 +1,80 @@
import subprocess
import os
import time
import signal
def iniciar_chrome(chrome_path,
user_data_dir,
headless=False,
debugging_port=9222,
user_agent=None,
):
# Asegúrate de que el directorio del perfil exista
os.makedirs(user_data_dir, exist_ok=True)
# Lista de argumentos para Chrome
chrome_args = [
f"--remote-debugging-port={debugging_port}",
f"--user-data-dir={user_data_dir}",
"--disable-blink-features=AutomationControlled",
"--no-sandbox",
"--disable-web-security",
"--disable-extensions",
"--disable-dev-shm-usage",
"--disable-infobars",
"--disable-popup-blocking",
"--disable-default-apps",
"--mute-audio",
"--window-size=1024,1024",
]
if not headless:
pass
else:
chrome_args.append("--headless=new") # para versiones recientes de Chrome
if not user_agent:
pass
else:
chrome_args.append(f"--user-agent={user_agent}")
# Comando para iniciar Chrome
chrome_process = subprocess.Popen([chrome_path] + chrome_args)
try:
print(f"Chrome iniciado (headless={headless}). Presiona Ctrl+C para salir.")
while True:
if chrome_process.poll() is not None:
print("Chrome se ha cerrado.")
break
time.sleep(1)
except KeyboardInterrupt:
print("Terminando proceso de Chrome...")
chrome_process.terminate()
try:
chrome_process.wait(timeout=5)
except subprocess.TimeoutExpired:
chrome_process.kill()
print("Chrome cerrado correctamente.")
# Ruta al ejecutable de Chrome
chrome_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
# Directorio para el perfil de usuario
user_data_dir = os.path.abspath("./Perfiles_usuario/chrome_profile")
# Puerto para la depuración remota
port = 9222
user_agent= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
# Llama a la función con True o False
iniciar_chrome(chrome_path=chrome_path,
user_data_dir=user_data_dir,
debugging_port=port,
headless=False,
user_agent=user_agent) # Cambia a True para modo headless
+122
View File
@@ -0,0 +1,122 @@
import asyncio
import os
import re
from src.ScrappingWeb.Navegador import Navegador
from src.ScrappingWeb.Scrapper import Scrapper
from src.ScrappingWeb.Tab import Tab
import aiohttp
import csv
async def esperar_chrome_listo(port, timeout=10):
url = f"http://127.0.0.1:{port}/json"
for _ in range(timeout * 2):
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
if resp.status == 200:
return
except Exception:
pass
await asyncio.sleep(0.5)
raise TimeoutError(f"Chrome en puerto {port} no respondió dentro del tiempo esperado.")
chrome_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
def sanitizar_nombre(nombre: str) -> str:
# Eliminar caracteres inválidos para nombre de archivo
return re.sub(r'[\\/*?:"<>|]', "_", nombre).strip()[:100]
async def iniciar_y_scrapear(id: int):
user_data_dir = os.path.abspath(f"./Perfiles_usuario/chrome_profile_{id}")
port = 9222 + id
navegador = Navegador(
chrome_path=chrome_path,
user_data_dir=user_data_dir,
id=id,
download_dir=os.path.join(user_data_dir, "downloads"),
debugging_port=port,
headless=False,
user_agent=f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/{100+id}.0.0.0 Safari/537.36"
)
# Iniciar navegador en background
asyncio.create_task(navegador.iniciar())
# Esperamos a que el navegador esté listo
await esperar_chrome_listo(port)
# Conectarse con el scraper al navegador
scrapper = Scrapper(debugging_url=f"http://127.0.0.1:{port}")
tab = await scrapper.nueva_tab("", wait_time=6)
# Ejecutar acciones desde la clase Tab
ua = await tab.obtener_user_agent()
print(f"🧭 [{id}] User-Agent:", ua)
title = await tab.evaluar_js("document.title")
print(f"📄 [{id}] Título:", title)
# botones= await tab.get_elements_by_css_selector("#mw-content-text > div.mw-content-ltr.mw-parser-output > figure:nth-child(27) > a > img")
# for boton in botones:
# await boton.click()
# # Crear carpeta si no existe
# os.makedirs("wikipedia_md", exist_ok=True)
# # Guardar el HTML completo
# html = await tab.obtener_html_completo()
# with open(f"contenido.html", "w", encoding="utf-8") as f:
# f.write(html)
# # Leer enlaces del CSV
# with open("enlaces_extraidos.csv", "r", encoding="utf-8") as f:
# reader = csv.reader(f)
# next(reader) # saltar encabezados
# enlaces = list(reader)
# for texto, enlace in enlaces:
# nombre_archivo = sanitizar_nombre(texto or "sin_titulo") + ".png"
# ruta_archivo = os.path.join("wikipedia", nombre_archivo)
# try:
# print(f"🌐 Visitando: {enlace}")
# tab = await scrapper.nueva_tab(enlace, wait_time=6)
# await tab.capturar_screenshot(ruta_archivo)
# print(f"📸 Captura guardada: {ruta_archivo}")
# await tab.cerrar()
# except Exception as e:
# print(f"❌ Error con {enlace}: {e}")
# await tab.capturar_screenshot(f"screenshot_{id}.png")
# html = await tab.obtener_html_completo()
# print(html)
# with open("contenido.html", "w", encoding="utf-8") as f:
# f.write(html)
# Extraer enlaces y guardarlos en CSV
# # # Cerrar tab y navegador si quieres
# await asyncio.sleep(10)
# await tab.cerrar()
# await navegador.cerrar()
async def main():
tareas = [iniciar_y_scrapear(i) for i in range(1)]
await asyncio.gather(*tareas)
if __name__ == "__main__":
asyncio.run(main())
+5 -3
View File
@@ -11,9 +11,9 @@ class LoggerDB:
_sink_removido = False # ← evita múltiples remove() si se crean varias instancias _sink_removido = False # ← evita múltiples remove() si se crean varias instancias
def __init__(self, credencial: PostgresCredencial, nombre_tabla: str, created_by: str = None): def __init__(self, credencial: PostgresCredencial, nombre_tabla: str, created_by: str = None):
if not LoggerDB._sink_removido:
logger.remove() # 🧹 elimina impresión en terminal # 🔥 Elimina todos los sinks activos, incluso los automáticos
LoggerDB._sink_removido = True logger.remove()
self.conexion = PostgresConexion(credencial) self.conexion = PostgresConexion(credencial)
self.engine = self.conexion.get_engine() self.engine = self.conexion.get_engine()
@@ -28,6 +28,8 @@ class LoggerDB:
def _generar_modelo_logger(self): def _generar_modelo_logger(self):
class LoggerTable(Model_base): class LoggerTable(Model_base):
__tablename__ = self.nombre_tabla __tablename__ = self.nombre_tabla
__table_args__ = {'extend_existing': True} # 👈 Esta línea evita el error
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
nivel = Column(String, nullable=False) nivel = Column(String, nullable=False)
mensaje = Column(Text, nullable=False) mensaje = Column(Text, nullable=False)
+100 -26
View File
@@ -1,54 +1,58 @@
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
import random import random
import asyncio import asyncio
import json
if TYPE_CHECKING: if TYPE_CHECKING:
from src.ScrappingWeb.Tab import Tab from .Tab import Tab
class ElementoWeb: class ElementoWeb:
def __init__(self, tab: "Tab", object_id: str): def __init__(self, tab: "Tab", object_id: Optional[str]):
self.tab = tab self.tab = tab
self.object_id = object_id self.object_id = object_id
self._node_id = None # Lazy resolved
@classmethod
def from_node(cls, tab: "Tab", node_id: int) -> "ElementoWeb":
inst = cls(tab, object_id=None)
inst._node_id = node_id
return inst
async def _asegurar_object_id(self):
if not self.object_id and self._node_id:
try:
resolved = await self.tab._enviar("DOM.resolveNode", {"nodeId": self._node_id})
self.object_id = resolved["object"]["objectId"]
except Exception as e:
print(f"⚠️ No se pudo resolver objectId desde nodeId: {e}")
async def scroll_into_view(self): async def scroll_into_view(self):
try: try:
await self._asegurar_object_id()
await self.tab._enviar("Runtime.callFunctionOn", { await self.tab._enviar("Runtime.callFunctionOn", {
"objectId": self.object_id, "objectId": self.object_id,
"functionDeclaration": "function() { this.scrollIntoView({block: 'center'}); }", "functionDeclaration": "function() { this.scrollIntoView({block: 'center'}); }",
"awaitPromise": True "awaitPromise": True
}) })
print("📜 Elemento desplazado a la vista.") if self.tab.verbose:
print("📜 Elemento desplazado a la vista.")
except Exception as e: except Exception as e:
print(f"⚠️ Error al hacer scroll hacia el elemento: {e}") print(f"⚠️ Error al hacer scroll hacia el elemento: {e}")
@classmethod
def from_node(cls, tab: "Tab", node_id: int) -> "ElementoWeb":
# Creamos un objectId a partir del nodeId usando DOM.resolveNode
cls._node_id = node_id
cls._resolved_object_id = None # Lazy resolution opcional
return cls(tab, object_id=None)
async def click(self): async def click(self):
try: try:
await self.scroll_into_view() await self.scroll_into_view()
await self._asegurar_object_id()
# Resolver objectId si es necesario
if not self.object_id and hasattr(self, "_node_id"):
resolved = await self.tab._enviar("DOM.resolveNode", {"nodeId": self._node_id})
self.object_id = resolved["object"]["objectId"]
if not self.object_id: if not self.object_id:
raise ValueError("No se puede obtener objectId del elemento para hacer click.") raise ValueError("No se puede obtener objectId del elemento para hacer click.")
# Obtener nodeId # Intenta obtener coordenadas del nodo
node_result = await self.tab._enviar("DOM.describeNode", { node_result = await self.tab._enviar("DOM.describeNode", {
"objectId": self.object_id "objectId": self.object_id
}) })
node_id = node_result["node"]["nodeId"] node_id = node_result["node"]["nodeId"]
# Obtener coordenadas con fallback
try: try:
box_model = await self.tab._enviar("DOM.getBoxModel", {"nodeId": node_id}) box_model = await self.tab._enviar("DOM.getBoxModel", {"nodeId": node_id})
content = box_model["model"]["content"] content = box_model["model"]["content"]
@@ -60,7 +64,12 @@ class ElementoWeb:
x = (quad[0] + quad[4]) / 2 x = (quad[0] + quad[4]) / 2
y = (quad[1] + quad[5]) / 2 y = (quad[1] + quad[5]) / 2
# Simular movimiento humano del mouse # 🧠 Enfocar el elemento antes de clickear
await self.tab._enviar("DOM.focus", {
"objectId": self.object_id
})
# 🎯 Movimiento humanoide opcional
start_x, start_y = x + random.uniform(-100, 100), y + random.uniform(-100, 100) start_x, start_y = x + random.uniform(-100, 100), y + random.uniform(-100, 100)
steps = random.randint(5, 12) steps = random.randint(5, 12)
for i in range(1, steps + 1): for i in range(1, steps + 1):
@@ -73,7 +82,7 @@ class ElementoWeb:
}) })
await asyncio.sleep(random.uniform(0.01, 0.05)) await asyncio.sleep(random.uniform(0.01, 0.05))
# Click humano # 👆 Mouse Down
await self.tab._enviar("Input.dispatchMouseEvent", { await self.tab._enviar("Input.dispatchMouseEvent", {
"type": "mousePressed", "type": "mousePressed",
"x": x, "x": x,
@@ -81,7 +90,10 @@ class ElementoWeb:
"button": "left", "button": "left",
"clickCount": 1 "clickCount": 1
}) })
await asyncio.sleep(random.uniform(0.05, 0.15)) await asyncio.sleep(random.uniform(0.05, 0.15))
# 👇 Mouse Up
await self.tab._enviar("Input.dispatchMouseEvent", { await self.tab._enviar("Input.dispatchMouseEvent", {
"type": "mouseReleased", "type": "mouseReleased",
"x": x, "x": x,
@@ -90,27 +102,89 @@ class ElementoWeb:
"clickCount": 1 "clickCount": 1
}) })
print(f"🖱️ Click humano simulado en ({x:.1f}, {y:.1f})") await asyncio.sleep(random.uniform(0.01, 0.05))
# 🖱️ Click manual adicional
await self.tab._enviar("Input.dispatchMouseEvent", {
"type": "mouseClicked",
"x": x,
"y": y,
"button": "left",
"clickCount": 1
})
if self.tab.verbose:
print(f"🖱️ Click humano simulado en ({x:.1f}, {y:.1f})")
except Exception as e: except Exception as e:
print(f"⚠️ Error al hacer click físico: {e}") print(f"⚠️ Error al hacer click físico: {e}")
print("🧪 Intentando fallback con JavaScript click()...") print("🧪 Intentando fallback con JavaScript click()...")
await self.click_js() await self.click_js()
async def click_js(self): async def click_js(self):
try: try:
await self._asegurar_object_id()
if not self.object_id:
print("⚠️ No se puede hacer click JS: objectId no disponible.")
return
await self.tab._enviar("Runtime.callFunctionOn", { await self.tab._enviar("Runtime.callFunctionOn", {
"objectId": self.object_id, "objectId": self.object_id,
"functionDeclaration": "function() { this.click(); }", "functionDeclaration": "function() { this.click(); }",
"awaitPromise": True "awaitPromise": True
}) })
print("🖱️ Click simulado por JavaScript (element.click())") if self.tab.verbose:
print("🖱️ Click simulado por JavaScript (element.click())")
except Exception as e: except Exception as e:
print(f"⚠️ Error al ejecutar click en JS: {e}") print(f"⚠️ Error al ejecutar click en JS: {e}")
async def obtener_texto(self) -> Optional[str]: async def obtener_texto(self) -> Optional[str]:
return await self.tab.evaluar_js(f'document.getElementById("{self.object_id}").textContent') try:
await self._asegurar_object_id()
result = await self.tab._enviar("Runtime.callFunctionOn", {
"objectId": self.object_id,
"functionDeclaration": "function() { return this.textContent; }",
"returnByValue": True
})
return result.get("result", {}).get("value")
except Exception as e:
print(f"⚠️ Error al obtener texto del elemento: {e}")
return None
async def escribir_texto(self, texto: str): async def escribir_texto(self, texto: str):
await self.tab.evaluar_js(f'document.getElementById("{self.object_id}").value = "{texto}"') try:
await self._asegurar_object_id()
await self.tab._enviar("Runtime.callFunctionOn", {
"objectId": self.object_id,
"functionDeclaration": f"function() {{ this.value = {json.dumps(texto)}; this.dispatchEvent(new Event('input')); }}",
"awaitPromise": True
})
if self.tab.verbose:
print(f"⌨️ Texto escrito en elemento: '{texto}'")
except Exception as e:
print(f"⚠️ Error al escribir texto: {e}")
async def encontrar_hijo_clickeable(self) -> Optional["ElementoWeb"]:
try:
await self._asegurar_object_id()
resultado = await self.tab._enviar("Runtime.callFunctionOn", {
"objectId": self.object_id,
"functionDeclaration": """
function() {
const candidatos = this.querySelectorAll("span, div, a, button");
for (const el of candidatos) {
const style = window.getComputedStyle(el);
const visible = style.display !== "none" && style.visibility !== "hidden";
const interactivo = style.pointerEvents !== "none";
if (visible && interactivo) return el;
}
return this;
}
""",
"returnByValue": False
})
if "result" in resultado and "objectId" in resultado["result"]:
return ElementoWeb(self.tab, resultado["result"]["objectId"])
except Exception as e:
print(f"⚠️ No se pudo encontrar hijo clickeable: {e}")
return None
+2 -2
View File
@@ -87,9 +87,9 @@ class Navegador:
f"--user-data-dir={self.user_data_dir}", f"--user-data-dir={self.user_data_dir}",
"--disable-blink-features=AutomationControlled", "--disable-blink-features=AutomationControlled",
"--no-sandbox", "--no-sandbox",
"--disable-web-security", # "--disable-web-security",
# "--disable-extensions", # "--disable-extensions",
"--disable-dev-shm-usage", # "--disable-dev-shm-usage",
"--disable-infobars", "--disable-infobars",
"--disable-popup-blocking", "--disable-popup-blocking",
"--disable-default-apps", "--disable-default-apps",
+72 -3
View File
@@ -2,7 +2,10 @@ import aiohttp
import websockets import websockets
import json import json
import asyncio import asyncio
from src.ScrappingWeb.Tab import Tab from .Tab import Tab
from typing import Optional
class Scrapper: class Scrapper:
def __init__(self, debugging_url: str = "http://127.0.0.1:9222"): def __init__(self, debugging_url: str = "http://127.0.0.1:9222"):
@@ -56,14 +59,80 @@ class Scrapper:
raise RuntimeError("No se pudo obtener el WebSocket de la nueva pestaña") raise RuntimeError("No se pudo obtener el WebSocket de la nueva pestaña")
async def nueva_tab(self, url: str, wait_time: float = 5.0) -> Tab: async def nueva_tab(self, url: str = "", wait_time: float = 5.0) -> Tab:
websocket_url = await self._crear_tab_websocket_url() websocket_url = await self._crear_tab_websocket_url()
tab = await Tab.crear_desde_websocket(websocket_url) tab = await Tab.crear_desde_websocket(websocket_url)
self.tabs.append(tab) self.tabs.append(tab)
await tab.navegar(url, wait_time)
if url:
print(f"🌍 Navegando a: {url}")
await tab.navegar(url, wait_time)
else:
print("⚠️ No se especificó URL. La pestaña se creó pero no se navegó a ninguna página.")
return tab return tab
async def cerrar_todos(self): async def cerrar_todos(self):
for tab in list(self.tabs): for tab in list(self.tabs):
await tab.cerrar() await tab.cerrar()
self.tabs.clear() self.tabs.clear()
def get_tab(self, identifier: str) -> Optional[Tab]:
"""
Devuelve una instancia de Tab según su WebSocket URL o su ID final (extraído del WebSocket URL).
Acepta:
- ws_url completo: ws://127.0.0.1:9222/devtools/page/XYZ
- id directo: XYZ
"""
for tab in self.tabs:
# Comparar directamente contra ws_url
if tab.ws_url == identifier:
return tab
# Comparar contra el ID extraído
ws_id = tab.ws_url.rsplit("/", 1)[-1]
if ws_id == identifier:
return tab
return None
async def obtener_tabs_existentes(self) -> list[Tab]:
"""
Recupera todas las pestañas de tipo 'page' que no están ya en self.tabs,
las conecta y devuelve como lista. Muestra resumen limpio por consola.
"""
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.debugging_url}/json") as resp:
if resp.status != 200:
raise RuntimeError("No se pudo obtener la lista de pestañas")
tabs_info = await resp.json()
print("\n🧾 Pestañas activas (filtradas: solo 'type': 'page'):\n")
nuevas_tabs = []
for idx, tab_info in enumerate(tabs_info, start=1):
tipo = tab_info.get("type")
if tipo != "page":
continue # Filtrar todo lo que no sea página visible
ws_url = tab_info.get("webSocketDebuggerUrl")
tab_id = tab_info.get("id")
title = tab_info.get("title", "<Sin título>")
url = tab_info.get("url", "<Sin URL>")
# Verifica si ya la tienes cargada
if any(t.ws_url == ws_url for t in self.tabs):
continue
# Conectar
try:
tab = await Tab.crear_desde_websocket(ws_url)
self.tabs.append(tab)
nuevas_tabs.append(tab)
except Exception as e:
print(f"⚠️ No se pudo conectar a pestaña {tab_id}: {e}")
if not nuevas_tabs:
print("⚠️ No se encontraron nuevas pestañas para agregar.\n")
return nuevas_tabs
+73 -31
View File
@@ -2,21 +2,29 @@ import asyncio
import json import json
import base64 import base64
import websockets import websockets
from typing import Optional from typing import Optional, List
from typing import List from .ElementoWeb import ElementoWeb
from src.ScrappingWeb.ElementoWeb import ElementoWeb import os
class Tab: class Tab:
def __init__(self, websocket: websockets.WebSocketClientProtocol, ws_url: str): def __init__(self, websocket: websockets.WebSocketClientProtocol, ws_url: str, verbose: bool = True):
self.websocket = websocket self.websocket = websocket
self.ws_url = ws_url self.ws_url = ws_url
self._message_id = 0 self._message_id = 0
self._pending = {} self._pending = {}
self._load_event = asyncio.Event() self._load_event = asyncio.Event()
self.verbose = verbose
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
await self.cerrar()
@classmethod @classmethod
async def crear_desde_websocket(cls, ws_url: str) -> "Tab": async def crear_desde_websocket(cls, ws_url: str) -> "Tab":
websocket = await websockets.connect(ws_url) websocket = await websockets.connect(ws_url, max_size=10 * 1024 * 1024)
tab = cls(websocket, ws_url) tab = cls(websocket, ws_url)
asyncio.create_task(tab._recibir_eventos()) asyncio.create_task(tab._recibir_eventos())
await tab._enviar("Page.enable") await tab._enviar("Page.enable")
@@ -28,11 +36,14 @@ class Tab:
data = json.loads(mensaje) data = json.loads(mensaje)
if "id" in data and data["id"] in self._pending: if "id" in data and data["id"] in self._pending:
future = self._pending.pop(data["id"]) future = self._pending.pop(data["id"])
future.set_result(data.get("result")) if "result" in data:
future.set_result(data["result"])
elif "error" in data:
future.set_exception(Exception(data["error"]))
elif data.get("method") == "Page.loadEventFired": elif data.get("method") == "Page.loadEventFired":
self._load_event.set() self._load_event.set()
async def _enviar(self, metodo: str, parametros: Optional[dict] = None) -> dict: async def _enviar(self, metodo: str, parametros: Optional[dict] = None, timeout: float = 10.0) -> dict:
self._message_id += 1 self._message_id += 1
msg_id = self._message_id msg_id = self._message_id
mensaje = { mensaje = {
@@ -44,15 +55,17 @@ class Tab:
future = asyncio.get_event_loop().create_future() future = asyncio.get_event_loop().create_future()
self._pending[msg_id] = future self._pending[msg_id] = future
await self.websocket.send(json.dumps(mensaje)) await self.websocket.send(json.dumps(mensaje))
return await future return await asyncio.wait_for(future, timeout=timeout)
async def navegar(self, url: str, wait_time: float = 5.0): async def navegar(self, url: str, wait_time: float = 5.0):
self._load_event.clear() self._load_event.clear()
print(f"🌍 Navegando a: {url}") if self.verbose:
print(f"🌍 Navegando a: {url}")
await self._enviar("Page.navigate", {"url": url}) await self._enviar("Page.navigate", {"url": url})
try: try:
await asyncio.wait_for(self._load_event.wait(), timeout=wait_time) await asyncio.wait_for(self._load_event.wait(), timeout=wait_time)
print("✅ Página cargada correctamente.") if self.verbose:
print("✅ Página cargada correctamente.")
except asyncio.TimeoutError: except asyncio.TimeoutError:
print(f"⚠️ Tiempo de espera agotado ({wait_time}s) al cargar la página.") print(f"⚠️ Tiempo de espera agotado ({wait_time}s) al cargar la página.")
@@ -62,11 +75,40 @@ class Tab:
"expression": js_code, "expression": js_code,
"returnByValue": True "returnByValue": True
}) })
return result["result"]["value"] if "exceptionDetails" in result:
raise Exception(result["exceptionDetails"])
return result.get("result", {}).get("value")
except Exception as e: except Exception as e:
print(f"⚠️ Error al ejecutar JS: {e}") print(f"⚠️ Error al ejecutar JS: {e}")
return None return None
async def inyectar_archivo_js(self, ruta_archivo: str, reemplazos: dict = None) -> Optional[str]:
if not os.path.exists(ruta_archivo):
print(f"❌ Archivo JS no encontrado: {ruta_archivo}")
return None
with open(ruta_archivo, "r", encoding="utf-8") as f:
js_code = f.read()
if reemplazos:
for key, value in reemplazos.items():
js_code = js_code.replace(f"{{{{{key}}}}}", str(value))
# 🔧 Eliminamos el `return` externo
js_code_final = f"(async () => {{\n{js_code}\n}})();"
try:
result = await self._enviar("Runtime.evaluate", {
"expression": js_code_final,
"returnByValue": True
})
if "exceptionDetails" in result:
raise Exception(result["exceptionDetails"])
return result.get("result", {}).get("value")
except Exception as e:
print(f"⚠️ Error al inyectar JS desde {ruta_archivo}: {e}")
return None
async def obtener_user_agent(self) -> Optional[str]: async def obtener_user_agent(self) -> Optional[str]:
return await self.evaluar_js("navigator.userAgent") return await self.evaluar_js("navigator.userAgent")
@@ -76,66 +118,57 @@ class Tab:
data = result["data"] data = result["data"]
with open(output_path, "wb") as f: with open(output_path, "wb") as f:
f.write(base64.b64decode(data)) f.write(base64.b64decode(data))
print(f"📸 Screenshot guardado como {output_path}") if self.verbose:
print(f"📸 Screenshot guardado como {output_path}")
except Exception as e: except Exception as e:
print(f"⚠️ Error al capturar screenshot: {e}") print(f"⚠️ Error al capturar screenshot: {e}")
async def cerrar(self): async def cerrar(self):
try: try:
await self.websocket.close() if not self.websocket.closed:
print("🛑 WebSocket cerrado.") await self.websocket.close()
if self.verbose:
print("🛑 WebSocket cerrado.")
except Exception as e: except Exception as e:
print(f"⚠️ Error al cerrar pestaña: {e}") print(f"⚠️ Error al cerrar pestaña: {e}")
async def obtener_html_completo(self) -> Optional[str]: async def obtener_html_completo(self) -> Optional[str]:
"""
Devuelve el HTML completo de la página actual.
"""
try: try:
result = await self._enviar("Runtime.evaluate", { result = await self._enviar("Runtime.evaluate", {
"expression": "document.documentElement.outerHTML", "expression": "document.documentElement.outerHTML",
"returnByValue": True "returnByValue": True
}) })
html = result["result"]["value"] return result.get("result", {}).get("value")
print("📄 HTML completo obtenido.")
return html
except Exception as e: except Exception as e:
print(f"⚠️ Error al obtener HTML: {e}") print(f"⚠️ Error al obtener HTML: {e}")
return None return None
async def obtener_dominio(self) -> Optional[str]: async def obtener_dominio(self) -> Optional[str]:
"""
Devuelve el dominio (hostname) de la página actual, por ejemplo: 'example.com'.
"""
try: try:
dominio = await self.evaluar_js("window.location.hostname") dominio = await self.evaluar_js("window.location.hostname")
print(f"🌐 Dominio actual: {dominio}") if self.verbose and dominio:
print(f"🌐 Dominio actual: {dominio}")
return dominio return dominio
except Exception as e: except Exception as e:
print(f"⚠️ Error al obtener dominio: {e}") print(f"⚠️ Error al obtener dominio: {e}")
return None return None
async def get_element_by_selector_node(self, selector: str) -> Optional["ElementoWeb"]: async def get_element_by_selector_node(self, selector: str) -> Optional["ElementoWeb"]:
try: try:
# Obtener nodo raíz del documento
doc = await self._enviar("DOM.getDocument") doc = await self._enviar("DOM.getDocument")
root_node_id = doc["root"]["nodeId"] root_node_id = doc["root"]["nodeId"]
# Buscar el nodo desde el DOM (más confiable que Runtime.evaluate)
result = await self._enviar("DOM.querySelector", { result = await self._enviar("DOM.querySelector", {
"nodeId": root_node_id, "nodeId": root_node_id,
"selector": selector "selector": selector
}) })
node_id = result["nodeId"] node_id = result.get("nodeId")
if not node_id: if not node_id:
print(f"⚠️ Nodo no encontrado con selector: {selector}") print(f"⚠️ Nodo no encontrado con selector: {selector}")
return None return None
return ElementoWeb.from_node(self, node_id=node_id) return ElementoWeb.from_node(self, node_id=node_id)
except Exception as e: except Exception as e:
print(f"⚠️ Error al buscar nodo desde DOM.querySelector: {e}") print(f"⚠️ Error al buscar nodo desde DOM.querySelector: {e}")
return None return None
@@ -157,8 +190,17 @@ class Tab:
for prop in props["result"]: for prop in props["result"]:
if "value" in prop and "objectId" in prop["value"]: if "value" in prop and "objectId" in prop["value"]:
elementos.append(ElementoWeb(self, prop["value"]["objectId"])) elementos.append(ElementoWeb(self, prop["value"]["objectId"]))
print(f"🔍 Se encontraron {len(elementos)} elementos con el selector CSS '{selector}'.") if self.verbose:
print(f"🔍 Se encontraron {len(elementos)} elementos con el selector CSS '{selector}'.")
return elementos return elementos
except Exception as e: except Exception as e:
print(f"⚠️ Error al buscar elementos por selector CSS '{selector}': {e}") print(f"⚠️ Error al buscar elementos por selector CSS '{selector}': {e}")
return [] return []
async def enfocar(self):
try:
await self._enviar("Page.bringToFront")
if self.verbose:
print("🪟 Pestaña enfocada (bringToFront).")
except Exception as e:
print(f"⚠️ Error al enfocar pestaña: {e}")
+31 -7
View File
@@ -13,6 +13,30 @@ from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base from src.ArquitectureLayer.Repo import Repo_base
from src.Credenciales.postgres_credencial import PostgresCredencial # Asegúrate de tener esta clase implementada correctamente
titulo = os.getenv('DB_TITLE')
usuario = os.getenv('DB_USER')
passwrd = os.getenv('DB_PASSWORD')
host = os.getenv('DB_HOST')
port = os.getenv('DB_PORT')
db_name = os.getenv('DB_NAME')
db_credencial = PostgresCredencial(
titulo=titulo,
user=usuario,
password=passwrd,
host=host,
port=port,
dbname=db_name
)
# from entrypoint.init_db import db_credencial
from src.Logger.logger_db import LoggerDB, logger
LoggerDB(db_credencial, "logger_textos", created_by="sistema")
from src.base import Base # Este es tu declarative_base() from src.base import Base # Este es tu declarative_base()
@@ -37,11 +61,11 @@ def generar_tabla_nota_para_biblioteca(biblioteca_nombre: str, vector_dim: int,
Genera una tabla dinámica y modelo ORM para una biblioteca dada, con campos vectoriales y campos del sistema. Genera una tabla dinámica y modelo ORM para una biblioteca dada, con campos vectoriales y campos del sistema.
""" """
try: try:
print(f"[INFO] Generando tabla para biblioteca: '{biblioteca_nombre}' con dimensión de vector: {vector_dim}") logger.info(f"Generando tabla para biblioteca: '{biblioteca_nombre}' con dimensión de vector: {vector_dim}")
# Nombre SQL-safe # Nombre SQL-safe
nombre_tabla = re.sub(r"[^a-zA-Z0-9_]", "_", biblioteca_nombre.strip().lower()) nombre_tabla = re.sub(r"[^a-zA-Z0-9_]", "_", biblioteca_nombre.strip().lower())
print(f"[DEBUG] Nombre de tabla SQL-safe: '{nombre_tabla}'") logger.debug(f"Nombre de tabla SQL-safe: '{nombre_tabla}'")
# Modelo ORM dinámico # Modelo ORM dinámico
class NotaModel(Base, Model_base): class NotaModel(Base, Model_base):
@@ -57,15 +81,15 @@ def generar_tabla_nota_para_biblioteca(biblioteca_nombre: str, vector_dim: int,
vector = Column(Vector(vector_dim), nullable=True) vector = Column(Vector(vector_dim), nullable=True)
vector_resumen = Column(Vector(vector_dim), nullable=True) vector_resumen = Column(Vector(vector_dim), nullable=True)
print(f"[INFO] Modelo ORM 'NotaModel' creado para la tabla '{nombre_tabla}'") logger.info(f"Modelo ORM 'NotaModel' creado para la tabla '{nombre_tabla}'")
print(f"[DEBUG] Columnas del modelo: {[c.name for c in NotaModel.__table__.columns]}") logger.debug(f"Columnas del modelo: {[c.name for c in NotaModel.__table__.columns]}")
print(f"[DEBUG] Tipos de columnas: {[str(c.type) for c in NotaModel.__table__.columns]}") logger.debug(f"Tipos de columnas: {[str(c.type) for c in NotaModel.__table__.columns]}")
print(f"[DEBUG] Claves primarias: {[c.name for c in NotaModel.__table__.primary_key]}") logger.debug(f"Claves primarias: {[c.name for c in NotaModel.__table__.primary_key]}")
return NotaModel.__table__, NotaModel return NotaModel.__table__, NotaModel
except Exception as e: except Exception as e:
print(f"[ERROR] Error al generar la tabla y modelo ORM para '{biblioteca_nombre}': {e}") logger.error(f"Error al generar la tabla y modelo ORM para '{biblioteca_nombre}': {e}")
raise raise
# ---------------------- # ----------------------