theus-cli-linux-x64 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/RELEASE.json +6 -6
- package/bin/theus +0 -0
- package/package.json +1 -1
- package/vendor/chrome-extension/README.md +142 -0
- package/vendor/chrome-extension/background.js +540 -0
- package/vendor/chrome-extension/cdp-core.js +400 -0
- package/vendor/chrome-extension/cdp-core.test.ts +321 -0
- package/vendor/chrome-extension/cdp.js +672 -0
- package/vendor/chrome-extension/icons/icon128.png +0 -0
- package/vendor/chrome-extension/icons/icon16.png +0 -0
- package/vendor/chrome-extension/icons/icon32.png +0 -0
- package/vendor/chrome-extension/icons/icon48.png +0 -0
- package/vendor/chrome-extension/manifest.json +36 -0
- package/vendor/chrome-extension/popup.html +78 -0
- package/vendor/chrome-extension/popup.js +47 -0
- package/vendor/chrome-extension/recorder.js +316 -0
- package/vendor/theus-arsenal/.theus-plugin/plugin.json +1 -1
- package/vendor/theus-arsenal/README.md +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "Theus en Chrome",
|
|
4
|
+
"version": "0.3.0",
|
|
5
|
+
"description": "Conecta tu navegador con Theus: el CLI ve tus pestañas, lee páginas, hace capturas y ejecuta acciones que tú autorizas.",
|
|
6
|
+
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAocpSkZEGHsT5kMKBGvvbJaKq7E+JzkAOdTAmmLyWjpKkzTlu7rkjnwhByTr2wjfToqvH7cfyRYUg98HoxO80Pqck3lWhMhqWro8VfGvIQyxoGJenF9nG4At9HWZIdPlVZp6npgLgUUuGXRpWLbqJvbmfMGjIQTrp9gStkatCFQWzK04PrGadbxHM05K+YNedA7/+4O95bsGccHgo7lCpQ71s47KKYrDC7EKRaAIIvkq34mJiy4T+LxMP4iSLqTHBC+nt0sDvT+UZ39Q51LNAW1VNYqG5/E2NPzzCjnn06Qzsotbo6zu6YW6VQbYQIqgu/OLeV/1aGqhrL0mxdHqLlwIDAQAB",
|
|
7
|
+
"permissions": [
|
|
8
|
+
"nativeMessaging",
|
|
9
|
+
"tabs",
|
|
10
|
+
"scripting",
|
|
11
|
+
"activeTab",
|
|
12
|
+
"debugger"
|
|
13
|
+
],
|
|
14
|
+
"host_permissions": [
|
|
15
|
+
"<all_urls>"
|
|
16
|
+
],
|
|
17
|
+
"background": {
|
|
18
|
+
"service_worker": "background.js"
|
|
19
|
+
},
|
|
20
|
+
"action": {
|
|
21
|
+
"default_popup": "popup.html",
|
|
22
|
+
"default_title": "Theus en Chrome",
|
|
23
|
+
"default_icon": {
|
|
24
|
+
"16": "icons/icon16.png",
|
|
25
|
+
"32": "icons/icon32.png",
|
|
26
|
+
"48": "icons/icon48.png",
|
|
27
|
+
"128": "icons/icon128.png"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"icons": {
|
|
31
|
+
"16": "icons/icon16.png",
|
|
32
|
+
"32": "icons/icon32.png",
|
|
33
|
+
"48": "icons/icon48.png",
|
|
34
|
+
"128": "icons/icon128.png"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="es">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Theus en Chrome</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
9
|
+
background: #0d0d10;
|
|
10
|
+
color: #f2f2f2;
|
|
11
|
+
margin: 0;
|
|
12
|
+
min-width: 260px;
|
|
13
|
+
padding: 16px;
|
|
14
|
+
}
|
|
15
|
+
h1 {
|
|
16
|
+
font-size: 14px;
|
|
17
|
+
margin: 0 0 12px;
|
|
18
|
+
letter-spacing: 0.04em;
|
|
19
|
+
}
|
|
20
|
+
.fila {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: 8px;
|
|
24
|
+
margin: 8px 0;
|
|
25
|
+
font-size: 13px;
|
|
26
|
+
}
|
|
27
|
+
.punto {
|
|
28
|
+
width: 9px;
|
|
29
|
+
height: 9px;
|
|
30
|
+
border-radius: 50%;
|
|
31
|
+
background: #666;
|
|
32
|
+
flex: none;
|
|
33
|
+
}
|
|
34
|
+
.punto.ok { background: #3ecf6a; }
|
|
35
|
+
.punto.mal { background: #d9534f; }
|
|
36
|
+
.nota {
|
|
37
|
+
margin-top: 12px;
|
|
38
|
+
font-size: 11px;
|
|
39
|
+
color: #9a9aa2;
|
|
40
|
+
line-height: 1.5;
|
|
41
|
+
}
|
|
42
|
+
#error {
|
|
43
|
+
margin-top: 8px;
|
|
44
|
+
font-size: 11px;
|
|
45
|
+
color: #e0a1a1;
|
|
46
|
+
word-break: break-word;
|
|
47
|
+
display: none;
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
50
|
+
</head>
|
|
51
|
+
<body>
|
|
52
|
+
<h1>THEUS EN CHROME</h1>
|
|
53
|
+
<div class="fila">
|
|
54
|
+
<span id="punto-host" class="punto"></span>
|
|
55
|
+
<span id="texto-host">Comprobando host nativo…</span>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="fila">
|
|
58
|
+
<span id="punto-cli" class="punto"></span>
|
|
59
|
+
<span id="texto-cli">Comprobando CLI…</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div id="error"></div>
|
|
62
|
+
<p class="nota">
|
|
63
|
+
El host nativo lo sirve el CLI de Theus. Si aparece desconectado,
|
|
64
|
+
ejecuta <strong>theus --chrome</strong> en tu terminal y reabre este
|
|
65
|
+
panel. El comando <strong>/chrome</strong> dentro del CLI muestra el
|
|
66
|
+
estado completo de la integración.
|
|
67
|
+
</p>
|
|
68
|
+
<p class="nota">
|
|
69
|
+
Theus puede controlar tus pestañas: clics, tecleo, capturas de pantalla
|
|
70
|
+
y lectura de consola y de red. Mientras una pestaña esté bajo control,
|
|
71
|
+
Chrome muestra la barra «Theus en Chrome ha empezado a depurar este
|
|
72
|
+
navegador»: es un aviso del propio Chrome, inevitable, y desaparece al
|
|
73
|
+
terminar. Si pulsas «Cancelar» en esa barra, el control se corta (Theus
|
|
74
|
+
lo retoma en la siguiente acción).
|
|
75
|
+
</p>
|
|
76
|
+
<script src="popup.js"></script>
|
|
77
|
+
</body>
|
|
78
|
+
</html>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Popup de estado de «Theus en Chrome».
|
|
2
|
+
function pintar(estado) {
|
|
3
|
+
const puntoHost = document.getElementById('punto-host')
|
|
4
|
+
const textoHost = document.getElementById('texto-host')
|
|
5
|
+
const puntoCli = document.getElementById('punto-cli')
|
|
6
|
+
const textoCli = document.getElementById('texto-cli')
|
|
7
|
+
const errorEl = document.getElementById('error')
|
|
8
|
+
|
|
9
|
+
if (estado && estado.hostConnected) {
|
|
10
|
+
puntoHost.className = 'punto ok'
|
|
11
|
+
textoHost.textContent = 'Host nativo: conectado'
|
|
12
|
+
} else {
|
|
13
|
+
puntoHost.className = 'punto mal'
|
|
14
|
+
textoHost.textContent = 'Host nativo: desconectado'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (estado && estado.cliConnected) {
|
|
18
|
+
puntoCli.className = 'punto ok'
|
|
19
|
+
textoCli.textContent =
|
|
20
|
+
estado.cliClients > 1
|
|
21
|
+
? `CLI de Theus: conectado (${estado.cliClients} sesiones)`
|
|
22
|
+
: 'CLI de Theus: conectado'
|
|
23
|
+
} else {
|
|
24
|
+
puntoCli.className = 'punto mal'
|
|
25
|
+
textoCli.textContent = 'CLI de Theus: sin sesión conectada'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (estado && estado.lastError) {
|
|
29
|
+
errorEl.style.display = 'block'
|
|
30
|
+
errorEl.textContent = `Último error: ${estado.lastError}`
|
|
31
|
+
} else {
|
|
32
|
+
errorEl.style.display = 'none'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function refrescar() {
|
|
37
|
+
chrome.runtime.sendMessage({ type: 'get_state' }, estado => {
|
|
38
|
+
if (chrome.runtime.lastError) {
|
|
39
|
+
pintar(null)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
pintar(estado)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
refrescar()
|
|
47
|
+
setInterval(refrescar, 2000)
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// Theus en Chrome — grabador de Kuti (fase 1), CONTENT SCRIPT.
|
|
2
|
+
//
|
|
3
|
+
// chrome.debugger NO reporta el input del usuario (el dominio Input de CDP es
|
|
4
|
+
// solo INYECCIÓN). Para GRABAR lo que la persona hace de verdad hace falta un
|
|
5
|
+
// content script que escuche los eventos del DOM. Este archivo lo inyecta
|
|
6
|
+
// background.js con chrome.scripting.executeScript cuando arranca /grabar auto,
|
|
7
|
+
// y lo reinyecta tras cada navegación (el content script muere al cambiar de
|
|
8
|
+
// página).
|
|
9
|
+
//
|
|
10
|
+
// Por cada evento calcula un DESCRIPTOR del elemento por INTENCIÓN (rol ARIA,
|
|
11
|
+
// nombre accesible, texto visible, selector css corto, name/id/placeholder/
|
|
12
|
+
// tipo/autocomplete) y lo manda a background.js por chrome.runtime.sendMessage.
|
|
13
|
+
//
|
|
14
|
+
// PRIVACIDAD: nunca se envía el texto tecleado carácter a carácter. El valor
|
|
15
|
+
// final de un campo se toma del evento `change` (o del `focusout`). Los campos
|
|
16
|
+
// sensibles (password, autocomplete cc-*, name/id/placeholder con clave/token/
|
|
17
|
+
// cvv…) se registran SIN su valor: background/CLI los volverán {{secreto}}.
|
|
18
|
+
|
|
19
|
+
;(function () {
|
|
20
|
+
// Idempotencia: si ya estamos grabando en este documento, no dupliques
|
|
21
|
+
// escuchas (una reinyección accidental no debe multiplicar eventos).
|
|
22
|
+
if (window.__kutiGrabadorActivo) return
|
|
23
|
+
window.__kutiGrabadorActivo = true
|
|
24
|
+
|
|
25
|
+
const RE_NOMBRE_SECRETO =
|
|
26
|
+
/(contrase|clave|password|passwd|\bpwd\b|secret|token|api[\s_-]?key|\bcvv\b|\bcvc\b|\bcsc\b|\botp\b|\bpin\b|codigo\s*de\s*seguridad|security\s*code)/i
|
|
27
|
+
|
|
28
|
+
// Recuerda el último valor enviado por elemento, para no duplicar `escribir`
|
|
29
|
+
// cuando change y focusout disparan por el mismo cambio.
|
|
30
|
+
const ultimoEnviado = new WeakMap()
|
|
31
|
+
// Marca de «sucio»: un input cambió y aún no se ha confirmado con change.
|
|
32
|
+
const sucios = new WeakSet()
|
|
33
|
+
|
|
34
|
+
function enviar(evento) {
|
|
35
|
+
try {
|
|
36
|
+
chrome.runtime.sendMessage({ type: 'kuti_recorder_event', evento })
|
|
37
|
+
} catch (_e) {
|
|
38
|
+
// El contexto de la extensión puede haberse invalidado (recarga): ignora.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---- Descriptor del elemento ------------------------------------------
|
|
43
|
+
|
|
44
|
+
function rolImplicito(el) {
|
|
45
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : ''
|
|
46
|
+
if (tag === 'a' && el.hasAttribute('href')) return 'link'
|
|
47
|
+
if (tag === 'button') return 'button'
|
|
48
|
+
if (tag === 'select') return 'combobox'
|
|
49
|
+
if (tag === 'textarea') return 'textbox'
|
|
50
|
+
if (tag === 'summary') return 'button'
|
|
51
|
+
if (tag === 'input') {
|
|
52
|
+
const t = (el.getAttribute('type') || 'text').toLowerCase()
|
|
53
|
+
if (t === 'checkbox') return 'checkbox'
|
|
54
|
+
if (t === 'radio') return 'radio'
|
|
55
|
+
if (t === 'button' || t === 'submit' || t === 'reset') return 'button'
|
|
56
|
+
if (t === 'range') return 'slider'
|
|
57
|
+
return 'textbox'
|
|
58
|
+
}
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function textoDe(el) {
|
|
63
|
+
const t = (el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim()
|
|
64
|
+
return t ? t.slice(0, 200) : ''
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function nombreAccesible(el) {
|
|
68
|
+
const aria = el.getAttribute && el.getAttribute('aria-label')
|
|
69
|
+
if (aria && aria.trim()) return aria.trim()
|
|
70
|
+
const labelledby = el.getAttribute && el.getAttribute('aria-labelledby')
|
|
71
|
+
if (labelledby) {
|
|
72
|
+
const partes = labelledby
|
|
73
|
+
.split(/\s+/)
|
|
74
|
+
.map(id => {
|
|
75
|
+
const ref = document.getElementById(id)
|
|
76
|
+
return ref ? textoDe(ref) : ''
|
|
77
|
+
})
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
if (partes.length) return partes.join(' ').slice(0, 200)
|
|
80
|
+
}
|
|
81
|
+
// <label for="id"> o <label> envolvente.
|
|
82
|
+
if (el.id) {
|
|
83
|
+
const lab = document.querySelector(`label[for="${cssEscape(el.id)}"]`)
|
|
84
|
+
if (lab) {
|
|
85
|
+
const t = textoDe(lab)
|
|
86
|
+
if (t) return t
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (el.closest) {
|
|
90
|
+
const lab = el.closest('label')
|
|
91
|
+
if (lab) {
|
|
92
|
+
const t = textoDe(lab)
|
|
93
|
+
if (t) return t
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : ''
|
|
97
|
+
// Botones/enlaces: su texto ES su nombre.
|
|
98
|
+
if (tag === 'button' || tag === 'a' || tag === 'summary') {
|
|
99
|
+
const t = textoDe(el)
|
|
100
|
+
if (t) return t
|
|
101
|
+
}
|
|
102
|
+
if (tag === 'input') {
|
|
103
|
+
const type = (el.getAttribute('type') || 'text').toLowerCase()
|
|
104
|
+
if ((type === 'submit' || type === 'button') && el.value) return el.value
|
|
105
|
+
}
|
|
106
|
+
const title = el.getAttribute && el.getAttribute('title')
|
|
107
|
+
if (title && title.trim()) return title.trim()
|
|
108
|
+
const ph = el.getAttribute && el.getAttribute('placeholder')
|
|
109
|
+
if (ph && ph.trim()) return ph.trim()
|
|
110
|
+
const name = el.getAttribute && el.getAttribute('name')
|
|
111
|
+
if (name && name.trim()) return name.trim()
|
|
112
|
+
return ''
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function cssEscape(s) {
|
|
116
|
+
if (window.CSS && typeof window.CSS.escape === 'function') return CSS.escape(s)
|
|
117
|
+
return String(s).replace(/["\\#.:>\s]/g, '\\$&')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function selectorCorto(el) {
|
|
121
|
+
if (el.id) return `#${cssEscape(el.id)}`
|
|
122
|
+
const partes = []
|
|
123
|
+
let nodo = el
|
|
124
|
+
while (nodo && nodo.nodeType === 1 && partes.length < 5) {
|
|
125
|
+
let parte = nodo.tagName.toLowerCase()
|
|
126
|
+
if (nodo.id) {
|
|
127
|
+
partes.unshift(`#${cssEscape(nodo.id)}`)
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
const padre = nodo.parentElement
|
|
131
|
+
if (padre) {
|
|
132
|
+
const hermanos = Array.from(padre.children).filter(
|
|
133
|
+
c => c.tagName === nodo.tagName,
|
|
134
|
+
)
|
|
135
|
+
if (hermanos.length > 1) {
|
|
136
|
+
parte += `:nth-of-type(${hermanos.indexOf(nodo) + 1})`
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
partes.unshift(parte)
|
|
140
|
+
nodo = nodo.parentElement
|
|
141
|
+
}
|
|
142
|
+
return partes.join(' > ')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function tipoDe(el) {
|
|
146
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : ''
|
|
147
|
+
if (tag === 'input') return (el.getAttribute('type') || 'text').toLowerCase()
|
|
148
|
+
if (tag === 'select') return 'select'
|
|
149
|
+
if (tag === 'textarea') return 'textarea'
|
|
150
|
+
return tag
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function descriptor(el) {
|
|
154
|
+
const d = {}
|
|
155
|
+
const rol = (el.getAttribute && el.getAttribute('role')) || rolImplicito(el)
|
|
156
|
+
if (rol) d.rol = rol
|
|
157
|
+
const nombre = nombreAccesible(el)
|
|
158
|
+
if (nombre) d.nombre = nombre
|
|
159
|
+
const texto = textoDe(el)
|
|
160
|
+
if (texto) d.texto = texto
|
|
161
|
+
const css = selectorCorto(el)
|
|
162
|
+
if (css) d.css = css
|
|
163
|
+
const name = el.getAttribute && el.getAttribute('name')
|
|
164
|
+
if (name) d.name = name
|
|
165
|
+
if (el.id) d.id = el.id
|
|
166
|
+
const ph = el.getAttribute && el.getAttribute('placeholder')
|
|
167
|
+
if (ph) d.placeholder = ph
|
|
168
|
+
d.tipo = tipoDe(el)
|
|
169
|
+
const auto = el.getAttribute && el.getAttribute('autocomplete')
|
|
170
|
+
if (auto) d.autocomplete = auto
|
|
171
|
+
return d
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function esSecreto(el, d) {
|
|
175
|
+
const tipo = (d.tipo || '').toLowerCase()
|
|
176
|
+
if (tipo === 'password') return true
|
|
177
|
+
const auto = (d.autocomplete || '').toLowerCase().trim()
|
|
178
|
+
if (auto.startsWith('cc-') || auto === 'one-time-code') return true
|
|
179
|
+
const heno = [d.name, d.id, d.placeholder, d.nombre]
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.join(' ')
|
|
182
|
+
.normalize('NFD')
|
|
183
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
184
|
+
return RE_NOMBRE_SECRETO.test(heno)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function valorDe(el, d) {
|
|
188
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : ''
|
|
189
|
+
if (tag === 'select') {
|
|
190
|
+
const opt = el.options && el.options[el.selectedIndex]
|
|
191
|
+
return opt ? (opt.textContent || opt.value || '').trim() : el.value || ''
|
|
192
|
+
}
|
|
193
|
+
if (tag === 'input') {
|
|
194
|
+
const t = (d.tipo || 'text')
|
|
195
|
+
if (t === 'checkbox' || t === 'radio') return el.checked ? 'true' : 'false'
|
|
196
|
+
return el.value != null ? String(el.value) : ''
|
|
197
|
+
}
|
|
198
|
+
if (tag === 'textarea') return el.value != null ? String(el.value) : ''
|
|
199
|
+
if (el.isContentEditable) return textoDe(el)
|
|
200
|
+
return ''
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function esCampo(el) {
|
|
204
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : ''
|
|
205
|
+
return (
|
|
206
|
+
tag === 'input' ||
|
|
207
|
+
tag === 'select' ||
|
|
208
|
+
tag === 'textarea' ||
|
|
209
|
+
(el.isContentEditable === true)
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---- Envío de un cambio de campo (change/focusout) --------------------
|
|
214
|
+
|
|
215
|
+
function emitirCambio(el) {
|
|
216
|
+
if (!el || !esCampo(el)) return
|
|
217
|
+
const d = descriptor(el)
|
|
218
|
+
const secreto = esSecreto(el, d)
|
|
219
|
+
const valor = secreto ? undefined : valorDe(el, d)
|
|
220
|
+
// Dedupe: mismo valor ya enviado → no repitas.
|
|
221
|
+
const firma = secreto ? '\u0000secreto' : String(valor)
|
|
222
|
+
if (ultimoEnviado.get(el) === firma) return
|
|
223
|
+
ultimoEnviado.set(el, firma)
|
|
224
|
+
sucios.delete(el)
|
|
225
|
+
const evento = { t: 'change', objetivo: d }
|
|
226
|
+
if (secreto) evento.secreto = true
|
|
227
|
+
else evento.valor = valor
|
|
228
|
+
enviar(evento)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---- Escuchas ---------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
function onClick(e) {
|
|
234
|
+
const el = e.target && e.target.closest ? e.target.closest('*') : e.target
|
|
235
|
+
if (!el || el.nodeType !== 1) return
|
|
236
|
+
// Los selects/checks/radios se registran por su change, no por el clic.
|
|
237
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : ''
|
|
238
|
+
const tipo = tipoDe(el)
|
|
239
|
+
if (tag === 'select' || tipo === 'checkbox' || tipo === 'radio') return
|
|
240
|
+
// Interesan controles y elementos accionables; ignora clics en el vacío.
|
|
241
|
+
const accionable =
|
|
242
|
+
el.closest('a, button, [role="button"], [onclick], summary, input, label') ||
|
|
243
|
+
el
|
|
244
|
+
enviar({ t: 'click', objetivo: descriptor(accionable) })
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function onInput(e) {
|
|
248
|
+
const el = e.target
|
|
249
|
+
if (el && esCampo(el)) sucios.add(el)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function onChange(e) {
|
|
253
|
+
emitirCambio(e.target)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function onFocusOut(e) {
|
|
257
|
+
// Red de seguridad: si el input cambió pero no disparó change (SPAs), el
|
|
258
|
+
// valor final igual se captura al perder el foco.
|
|
259
|
+
const el = e.target
|
|
260
|
+
if (el && sucios.has(el)) emitirCambio(el)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const TECLAS_SUELTAS = new Set(['Enter', 'Tab', 'Escape'])
|
|
264
|
+
|
|
265
|
+
function onKeyDown(e) {
|
|
266
|
+
// Solo teclas «de control» y atajos; NUNCA el texto tecleado.
|
|
267
|
+
const combo = e.ctrlKey || e.metaKey || e.altKey
|
|
268
|
+
if (!combo && !TECLAS_SUELTAS.has(e.key)) return
|
|
269
|
+
// Antes de una tecla de acción, asegura el valor del campo con foco.
|
|
270
|
+
const activo = document.activeElement
|
|
271
|
+
if (activo && sucios.has(activo)) emitirCambio(activo)
|
|
272
|
+
let tecla = ''
|
|
273
|
+
if (e.ctrlKey) tecla += 'ctrl+'
|
|
274
|
+
if (e.metaKey) tecla += 'meta+'
|
|
275
|
+
if (e.altKey) tecla += 'alt+'
|
|
276
|
+
if (e.shiftKey && (combo || e.key.length > 1)) tecla += 'shift+'
|
|
277
|
+
tecla += e.key
|
|
278
|
+
enviar({ t: 'key', tecla })
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function onSubmit(e) {
|
|
282
|
+
const el = e.target
|
|
283
|
+
enviar({ t: 'submit', objetivo: el ? descriptor(el) : undefined })
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const opciones = { capture: true, passive: true }
|
|
287
|
+
document.addEventListener('click', onClick, opciones)
|
|
288
|
+
document.addEventListener('input', onInput, opciones)
|
|
289
|
+
document.addEventListener('change', onChange, opciones)
|
|
290
|
+
document.addEventListener('focusout', onFocusOut, opciones)
|
|
291
|
+
document.addEventListener('keydown', onKeyDown, true)
|
|
292
|
+
document.addEventListener('submit', onSubmit, opciones)
|
|
293
|
+
|
|
294
|
+
function desmontar() {
|
|
295
|
+
document.removeEventListener('click', onClick, opciones)
|
|
296
|
+
document.removeEventListener('input', onInput, opciones)
|
|
297
|
+
document.removeEventListener('change', onChange, opciones)
|
|
298
|
+
document.removeEventListener('focusout', onFocusOut, opciones)
|
|
299
|
+
document.removeEventListener('keydown', onKeyDown, true)
|
|
300
|
+
document.removeEventListener('submit', onSubmit, opciones)
|
|
301
|
+
window.__kutiGrabadorActivo = false
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// background.js manda parar cuando el usuario cierra la grabación.
|
|
305
|
+
chrome.runtime.onMessage.addListener(function alParar(msg) {
|
|
306
|
+
if (msg && msg.type === 'kuti_recorder_stop') {
|
|
307
|
+
desmontar()
|
|
308
|
+
try {
|
|
309
|
+
chrome.runtime.onMessage.removeListener(alParar)
|
|
310
|
+
} catch (_e) {
|
|
311
|
+
// ignore
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return false
|
|
315
|
+
})
|
|
316
|
+
})()
|