chocolatito-code 1.0.0 → 1.2.0
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 +155 -0
- package/dist/agent/compaction.d.ts +65 -0
- package/dist/agent/compaction.js +134 -0
- package/dist/agent/context.d.ts +3 -0
- package/dist/agent/context.js +35 -0
- package/dist/agent/loop.d.ts +91 -2
- package/dist/agent/loop.js +642 -91
- package/dist/agent/loopDetector.d.ts +57 -0
- package/dist/agent/loopDetector.js +0 -0
- package/dist/agent/mentions.d.ts +13 -0
- package/dist/agent/mentions.js +49 -0
- package/dist/agent/repoMap.d.ts +117 -0
- package/dist/agent/repoMap.js +560 -0
- package/dist/agent/result.d.ts +64 -0
- package/dist/agent/result.js +48 -0
- package/dist/agent/retry.d.ts +35 -0
- package/dist/agent/retry.js +123 -4
- package/dist/agent/salidaDelAgente.d.ts +43 -0
- package/dist/agent/salidaDelAgente.js +53 -0
- package/dist/agent/subagent.js +62 -15
- package/dist/agent/syntaxValidator.js +288 -52
- package/dist/agent/toolGate.d.ts +58 -0
- package/dist/agent/toolGate.js +111 -0
- package/dist/agent/tracker.js +4 -7
- package/dist/agent/undoManager.d.ts +44 -11
- package/dist/agent/undoManager.js +95 -24
- package/dist/agent/usageMeter.d.ts +56 -0
- package/dist/agent/usageMeter.js +51 -0
- package/dist/agent/verifier.d.ts +49 -0
- package/dist/agent/verifier.js +158 -0
- package/dist/config/constants.js +14 -0
- package/dist/config/engine.d.ts +19 -0
- package/dist/config/engine.js +35 -3
- package/dist/config/license.d.ts +5 -0
- package/dist/config/license.js +107 -9
- package/dist/config/limits.d.ts +28 -0
- package/dist/config/limits.js +51 -0
- package/dist/config/nodeVersion.d.ts +13 -0
- package/dist/config/nodeVersion.js +26 -0
- package/dist/config/permissions.d.ts +76 -1
- package/dist/config/permissions.js +379 -23
- package/dist/config/quota.js +24 -1
- package/dist/config/updater.d.ts +68 -0
- package/dist/config/updater.js +215 -0
- package/dist/hooks/manager.js +159 -14
- package/dist/index.js +240 -37
- package/dist/mcp/client.d.ts +50 -0
- package/dist/mcp/client.js +155 -19
- package/dist/mcp/ide.d.ts +76 -0
- package/dist/mcp/ide.js +160 -0
- package/dist/mcp/manager.js +9 -0
- package/dist/prompts/systemPrompt.js +12 -1
- package/dist/sessions/manager.d.ts +12 -0
- package/dist/sessions/manager.js +33 -5
- package/dist/sessions/resume.d.ts +36 -0
- package/dist/sessions/resume.js +43 -0
- package/dist/skills/manager.d.ts +1 -0
- package/dist/skills/manager.js +9 -0
- package/dist/tools/binary.d.ts +46 -0
- package/dist/tools/binary.js +100 -0
- package/dist/tools/browserCdp.js +13 -1
- package/dist/tools/browserExtension.d.ts +5 -0
- package/dist/tools/browserExtension.js +237 -22
- package/dist/tools/browserSession.d.ts +23 -0
- package/dist/tools/browserSession.js +28 -0
- package/dist/tools/computerUse.js +33 -1
- package/dist/tools/createImage.d.ts +11 -0
- package/dist/tools/createImage.js +13 -1
- package/dist/tools/definitions.js +17 -4
- package/dist/tools/editDiagnosis.d.ts +54 -0
- package/dist/tools/editDiagnosis.js +142 -0
- package/dist/tools/editFile.d.ts +1 -0
- package/dist/tools/editFile.js +118 -62
- package/dist/tools/findFiles.d.ts +21 -0
- package/dist/tools/findFiles.js +75 -6
- package/dist/tools/getSystemInfo.js +15 -7
- package/dist/tools/gitTools.d.ts +24 -0
- package/dist/tools/gitTools.js +87 -26
- package/dist/tools/grepSearch.js +111 -23
- package/dist/tools/moveCopyFile.d.ts +18 -1
- package/dist/tools/moveCopyFile.js +74 -4
- package/dist/tools/patchCascade.d.ts +57 -0
- package/dist/tools/patchCascade.js +338 -0
- package/dist/tools/runCommand.js +213 -25
- package/dist/tools/runner.js +58 -5
- package/dist/tools/toolDefsComputer.js +12 -2
- package/dist/tools/truncator.d.ts +27 -1
- package/dist/tools/truncator.js +117 -11
- package/dist/tools/viewFile.js +15 -11
- package/dist/tools/visionBridge.js +7 -0
- package/dist/tools/webSearch.d.ts +21 -0
- package/dist/tools/webSearch.js +125 -14
- package/dist/tools/writeFile.d.ts +16 -1
- package/dist/tools/writeFile.js +40 -2
- package/dist/ui/interrupt.d.ts +4 -1
- package/dist/ui/interrupt.js +59 -7
- package/dist/ui/keyboardGuard.d.ts +76 -0
- package/dist/ui/keyboardGuard.js +94 -0
- package/dist/ui/loopPrompt.d.ts +17 -0
- package/dist/ui/loopPrompt.js +59 -0
- package/dist/ui/pegados.d.ts +50 -0
- package/dist/ui/pegados.js +83 -0
- package/dist/ui/permissionPrompt.js +84 -2
- package/dist/ui/prompt.d.ts +40 -0
- package/dist/ui/prompt.js +436 -131
- package/dist/ui/reasoningStream.d.ts +8 -0
- package/dist/ui/reasoningStream.js +99 -7
- package/dist/ui/salida.d.ts +10 -0
- package/dist/ui/salida.js +50 -0
- package/extension/README.md +95 -74
- package/extension/background.js +772 -181
- package/extension/content.js +527 -46
- package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
- package/extension/iconos/128.png +0 -0
- package/extension/iconos/16.png +0 -0
- package/extension/iconos/32.png +0 -0
- package/extension/iconos/48.png +0 -0
- package/extension/iconos/logotipo.png +0 -0
- package/extension/manifest.json +28 -4
- package/extension/popup.html +16 -6
- package/extension/popup.js +28 -12
- package/package.json +3 -2
package/extension/background.js
CHANGED
|
@@ -13,18 +13,120 @@
|
|
|
13
13
|
// todas pueden trabajar.
|
|
14
14
|
const WS_PORTS = [8787, 8788, 8789, 8790];
|
|
15
15
|
const GROUP_TITLE = "Chocolatito Code";
|
|
16
|
+
const VERSION = "1.2.0";
|
|
16
17
|
|
|
17
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Tope de un comando dentro de la extension.
|
|
20
|
+
*
|
|
21
|
+
* La CLI ya tiene el suyo, pero el suyo solo hace que ella deje de esperar: la
|
|
22
|
+
* accion se queda corriendo aqui y puede llegar tarde, cuando el modelo ya ha
|
|
23
|
+
* pedido otra cosa. Cortando de este lado, lo que la CLI recibe es un error
|
|
24
|
+
* explicable en vez de un silencio.
|
|
25
|
+
*/
|
|
26
|
+
const TIMEOUT_COMANDO_MS = 45000;
|
|
27
|
+
|
|
28
|
+
/** puerto -> { socket, delay, timer, state, sessionId, reconectando } */
|
|
18
29
|
const links = new Map();
|
|
19
30
|
|
|
20
|
-
/**
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
/** Paginas donde Chrome no deja entrar a ninguna extension. */
|
|
32
|
+
const ESQUEMAS_VETADOS = /^(chrome|edge|about|devtools|view-source|chrome-untrusted|chrome-search|chrome-error):/i;
|
|
33
|
+
const TIENDAS_VETADAS = /^https:\/\/(chromewebstore\.google\.com|chrome\.google\.com\/webstore)/i;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Estado de UNA sesion de la CLI.
|
|
37
|
+
*
|
|
38
|
+
* Antes esto eran tres variables de modulo. Como la extension se conecta a los
|
|
39
|
+
* cuatro puertos desde el MISMO service worker, dos sesiones compartian indice
|
|
40
|
+
* de refs, pestana de trabajo y grupo: el snapshot de la sesion B borraba el
|
|
41
|
+
* indice de A, y el clic de A caia sobre la pestana de B. Un estado por sesion
|
|
42
|
+
* es lo que hace cierto lo que promete la CLI: que varias convivan.
|
|
43
|
+
*
|
|
44
|
+
* refIndex: ref -> { tabId, frameId }, para resolver despues los clics.
|
|
45
|
+
* propias: pestanas que ESTA sesion abrio o adopto con select. Es lo unico
|
|
46
|
+
* que puede cerrar; ver close_tab.
|
|
47
|
+
*/
|
|
48
|
+
function nuevoEstado() {
|
|
49
|
+
return { refIndex: new Map(), workingTabId: null, groupId: null, propias: new Set() };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ======================================================= estado persistente
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* En Manifest V3 el service worker se apaga cuando le parece. Cuando eso pasa,
|
|
56
|
+
* el estado en memoria —indice de refs, pestana de trabajo, grupo— desaparece
|
|
57
|
+
* entero y sin avisar: el siguiente clic responde "el ref 3 no existe" y el
|
|
58
|
+
* siguiente close_tab dice que no hay pestana de trabajo, aunque en pantalla
|
|
59
|
+
* siga todo donde estaba. La extension ya pedia el permiso `storage` en el
|
|
60
|
+
* manifiesto y no lo usaba para nada.
|
|
61
|
+
*
|
|
62
|
+
* Se guarda en `storage.session`, que vive en memoria y se borra al cerrar
|
|
63
|
+
* Chrome: es un apunte para sobrevivir a un reinicio del worker, no un
|
|
64
|
+
* historial de navegacion en el disco del usuario.
|
|
65
|
+
*/
|
|
66
|
+
const almacen = (() => {
|
|
67
|
+
try {
|
|
68
|
+
return chrome.storage && chrome.storage.session ? chrome.storage.session : null;
|
|
69
|
+
} catch (_) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
|
|
74
|
+
const claveDe = (sessionId) => "sesion:" + sessionId;
|
|
75
|
+
|
|
76
|
+
function serializar(st) {
|
|
77
|
+
return {
|
|
78
|
+
workingTabId: st.workingTabId,
|
|
79
|
+
groupId: st.groupId,
|
|
80
|
+
propias: [...st.propias],
|
|
81
|
+
refIndex: [...st.refIndex.entries()],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function deserializar(plano) {
|
|
86
|
+
const st = nuevoEstado();
|
|
87
|
+
if (!plano) return st;
|
|
88
|
+
st.workingTabId = plano.workingTabId ?? null;
|
|
89
|
+
st.groupId = plano.groupId ?? null;
|
|
90
|
+
st.propias = new Set(plano.propias || []);
|
|
91
|
+
st.refIndex = new Map(plano.refIndex || []);
|
|
92
|
+
return st;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function guardarEstado(link) {
|
|
96
|
+
if (!almacen || !link || !link.sessionId) return;
|
|
97
|
+
try {
|
|
98
|
+
await almacen.set({ [claveDe(link.sessionId)]: serializar(link.state) });
|
|
99
|
+
} catch (_) {}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function restaurarEstado(link, sessionId) {
|
|
103
|
+
link.sessionId = sessionId;
|
|
104
|
+
if (!almacen) return;
|
|
105
|
+
try {
|
|
106
|
+
const guardado = await almacen.get(claveDe(sessionId));
|
|
107
|
+
const plano = guardado && guardado[claveDe(sessionId)];
|
|
108
|
+
if (!plano) return;
|
|
109
|
+
link.state = deserializar(plano);
|
|
110
|
+
// La pestana pudo cerrarse mientras el worker estaba dormido.
|
|
111
|
+
if (link.state.workingTabId) {
|
|
112
|
+
try {
|
|
113
|
+
await chrome.tabs.get(link.state.workingTabId);
|
|
114
|
+
} catch (_) {
|
|
115
|
+
link.state.workingTabId = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (_) {}
|
|
119
|
+
}
|
|
24
120
|
|
|
25
121
|
// ============================================================ conexion
|
|
26
122
|
|
|
27
123
|
function connect(port) {
|
|
124
|
+
const existente = links.get(port);
|
|
125
|
+
// Sin esta guarda, dos reintentos solapados dejaban dos sockets vivos contra
|
|
126
|
+
// el mismo puerto: la CLI se quedaba con el ultimo y el otro seguia
|
|
127
|
+
// consumiendo un keepalive para siempre.
|
|
128
|
+
if (existente && existente.socket && existente.socket.readyState === WebSocket.OPEN) return;
|
|
129
|
+
|
|
28
130
|
let ws;
|
|
29
131
|
try {
|
|
30
132
|
ws = new WebSocket("ws://127.0.0.1:" + port);
|
|
@@ -33,16 +135,29 @@ function connect(port) {
|
|
|
33
135
|
return;
|
|
34
136
|
}
|
|
35
137
|
|
|
36
|
-
const link =
|
|
138
|
+
const link = existente || { delay: 1000, state: nuevoEstado() };
|
|
139
|
+
if (!link.state) link.state = nuevoEstado();
|
|
37
140
|
link.socket = ws;
|
|
141
|
+
link.reconectando = false;
|
|
38
142
|
links.set(port, link);
|
|
39
143
|
|
|
40
144
|
ws.onopen = () => {
|
|
41
145
|
link.delay = 1000;
|
|
42
|
-
|
|
146
|
+
// Que este puerto vuelva a aceptar conexion puede significar dos cosas muy
|
|
147
|
+
// distintas: otra sesion de la CLI (hay que empezar en limpio, heredar su
|
|
148
|
+
// pestana de trabajo es como acaban los comandos sin tabId actuando sobre
|
|
149
|
+
// una pestana ajena) o el mismo proceso de siempre despues de que Chrome
|
|
150
|
+
// apagara este worker (hay que RECUPERAR el estado, o el usuario ve como
|
|
151
|
+
// el agente se olvida de donde estaba trabajando a mitad de tarea).
|
|
152
|
+
//
|
|
153
|
+
// No se puede distinguir sin preguntar, asi que se arranca en limpio y se
|
|
154
|
+
// recupera solo si la CLI se identifica con el mismo sessionId de antes.
|
|
155
|
+
link.state = nuevoEstado();
|
|
156
|
+
link.sessionId = null;
|
|
157
|
+
sendTo(ws, { type: "hello", agent: "chocolatito-extension", version: VERSION, port, resumable: !!almacen });
|
|
43
158
|
refreshBadge();
|
|
44
|
-
//
|
|
45
|
-
//
|
|
159
|
+
// Un ping periodico mantiene viva la conexion y, con ella, el worker: la
|
|
160
|
+
// actividad de WebSocket reinicia el temporizador de inactividad de MV3.
|
|
46
161
|
clearInterval(link.timer);
|
|
47
162
|
link.timer = setInterval(() => sendTo(ws, { type: "keepalive" }), 20000);
|
|
48
163
|
};
|
|
@@ -52,13 +167,27 @@ function connect(port) {
|
|
|
52
167
|
try {
|
|
53
168
|
msg = JSON.parse(ev.data);
|
|
54
169
|
} catch (_) {
|
|
170
|
+
return; // sin id no hay a quien contestarle
|
|
171
|
+
}
|
|
172
|
+
if (!msg) return;
|
|
173
|
+
|
|
174
|
+
// La CLI se identifica al conectar. Si es la misma sesion que ya conocia
|
|
175
|
+
// este worker, se recupera su estado; si es otra, se queda el limpio.
|
|
176
|
+
if (msg.type === "session" && typeof msg.sessionId === "string") {
|
|
177
|
+
await restaurarEstado(link, msg.sessionId);
|
|
55
178
|
return;
|
|
56
179
|
}
|
|
57
|
-
|
|
180
|
+
|
|
181
|
+
if (typeof msg.id !== "number") return;
|
|
182
|
+
|
|
183
|
+
// Un comando sin nombre antes se caia por el `throw` de handle y se
|
|
184
|
+
// contestaba igual; se deja explicito para que el error diga que pasa.
|
|
58
185
|
try {
|
|
59
|
-
const data = await handle(msg);
|
|
186
|
+
const data = await conTimeout(handle(msg, link.state), TIMEOUT_COMANDO_MS, msg.cmd);
|
|
187
|
+
await guardarEstado(link);
|
|
60
188
|
sendTo(ws, { type: "result", id: msg.id, ok: true, data });
|
|
61
189
|
} catch (err) {
|
|
190
|
+
await guardarEstado(link);
|
|
62
191
|
sendTo(ws, { type: "result", id: msg.id, ok: false, error: String((err && err.message) || err) });
|
|
63
192
|
}
|
|
64
193
|
};
|
|
@@ -66,6 +195,9 @@ function connect(port) {
|
|
|
66
195
|
ws.onclose = () => {
|
|
67
196
|
clearInterval(link.timer);
|
|
68
197
|
if (link.socket === ws) link.socket = null;
|
|
198
|
+
const oldTab = link.state && link.state.workingTabId;
|
|
199
|
+
if (link.state) link.state.workingTabId = null;
|
|
200
|
+
if (oldTab) setTabOverlay(oldTab, false);
|
|
69
201
|
refreshBadge();
|
|
70
202
|
scheduleReconnect(port);
|
|
71
203
|
};
|
|
@@ -78,7 +210,14 @@ function connect(port) {
|
|
|
78
210
|
function scheduleReconnect(port) {
|
|
79
211
|
const link = links.get(port) || { delay: 1000 };
|
|
80
212
|
links.set(port, link);
|
|
81
|
-
|
|
213
|
+
// onerror cierra el socket y onclose vuelve a llamar aqui: sin esta marca se
|
|
214
|
+
// encadenaban dos temporizadores por cada caida.
|
|
215
|
+
if (link.reconectando) return;
|
|
216
|
+
link.reconectando = true;
|
|
217
|
+
setTimeout(() => {
|
|
218
|
+
link.reconectando = false;
|
|
219
|
+
connect(port);
|
|
220
|
+
}, link.delay);
|
|
82
221
|
// Tope bajo a proposito: el servidor es local y reintentar cuesta nada.
|
|
83
222
|
link.delay = Math.min((link.delay || 1000) * 1.5, 3000);
|
|
84
223
|
}
|
|
@@ -109,13 +248,66 @@ function setBadge(text) {
|
|
|
109
248
|
|
|
110
249
|
// ============================================================ utilidades
|
|
111
250
|
|
|
251
|
+
function sleep(ms) {
|
|
252
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Corta una operacion que no vuelve, con un error que dice cual era. */
|
|
256
|
+
function conTimeout(promesa, ms, etiqueta) {
|
|
257
|
+
return new Promise((resolve, reject) => {
|
|
258
|
+
const t = setTimeout(() => {
|
|
259
|
+
reject(new Error(`"${etiqueta || "el comando"}" no respondio en ${Math.round(ms / 1000)} s. ` +
|
|
260
|
+
"La pagina puede estar bloqueada o pidiendo algo en un dialogo del navegador."));
|
|
261
|
+
}, ms);
|
|
262
|
+
promesa.then(
|
|
263
|
+
(v) => { clearTimeout(t); resolve(v); },
|
|
264
|
+
(e) => { clearTimeout(t); reject(e); }
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Por que no se puede trabajar en esta URL, o null si si se puede.
|
|
271
|
+
*
|
|
272
|
+
* Chrome prohibe inyectar scripts en sus propias paginas y en la tienda. Antes
|
|
273
|
+
* el intento fallaba en `chrome.scripting.executeScript` con un error de
|
|
274
|
+
* Chrome ("Cannot access contents of the page"), que no le dice al agente ni
|
|
275
|
+
* que es culpa de la URL ni que no lo reintente.
|
|
276
|
+
*/
|
|
277
|
+
function motivoUrlVetada(url) {
|
|
278
|
+
const u = String(url || "");
|
|
279
|
+
if (ESQUEMAS_VETADOS.test(u)) {
|
|
280
|
+
return `Chrome no deja que ninguna extension actue en "${u.slice(0, 60)}". ` +
|
|
281
|
+
"Es una pagina interna del navegador: abre una pestana normal (https://...) para trabajar.";
|
|
282
|
+
}
|
|
283
|
+
if (TIENDAS_VETADAS.test(u)) {
|
|
284
|
+
return "Chrome bloquea las extensiones en la Chrome Web Store. No se puede leer ni pulsar ahi.";
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function comprobarUrl(tabId) {
|
|
290
|
+
let url;
|
|
291
|
+
try {
|
|
292
|
+
url = (await chrome.tabs.get(tabId)).url;
|
|
293
|
+
} catch (_) {
|
|
294
|
+
return; // la pestana ya no esta; el error de quien llama lo explica mejor
|
|
295
|
+
}
|
|
296
|
+
const motivo = motivoUrlVetada(url);
|
|
297
|
+
if (motivo) throw new Error(motivo);
|
|
298
|
+
}
|
|
299
|
+
|
|
112
300
|
/** Manda un mensaje al content script, inyectandolo si aun no estaba. */
|
|
113
301
|
async function tell(tabId, frameId, msg) {
|
|
114
302
|
const opts = frameId === undefined || frameId === null ? {} : { frameId };
|
|
115
303
|
try {
|
|
116
304
|
return await chrome.tabs.sendMessage(tabId, msg, opts);
|
|
117
305
|
} catch (_) {
|
|
118
|
-
// Pagina cargada antes de instalar la extension: se inyecta al vuelo.
|
|
306
|
+
// Pagina cargada antes de instalar la extension: se inyecta al vuelo. Si la
|
|
307
|
+
// URL es de las que Chrome veta, se dice claramente en vez de devolver el
|
|
308
|
+
// "Cannot access contents of the page" de Chrome, que no explica ni que es
|
|
309
|
+
// culpa de la URL ni que no hay que reintentarlo.
|
|
310
|
+
await comprobarUrl(tabId);
|
|
119
311
|
await chrome.scripting.executeScript({
|
|
120
312
|
target: frameId ? { tabId, frameIds: [frameId] } : { tabId, allFrames: true },
|
|
121
313
|
files: ["content.js"],
|
|
@@ -133,44 +325,186 @@ async function framesOf(tabId) {
|
|
|
133
325
|
}
|
|
134
326
|
}
|
|
135
327
|
|
|
136
|
-
|
|
328
|
+
/** Pestana explicita, o la de trabajo de ESTA sesion. null si no hay ninguna. */
|
|
329
|
+
async function ownTab(tabId, st) {
|
|
137
330
|
if (tabId) return tabId;
|
|
138
|
-
if (workingTabId) {
|
|
331
|
+
if (st.workingTabId) {
|
|
139
332
|
try {
|
|
140
|
-
await chrome.tabs.get(workingTabId);
|
|
141
|
-
return workingTabId;
|
|
333
|
+
await chrome.tabs.get(st.workingTabId);
|
|
334
|
+
return st.workingTabId;
|
|
142
335
|
} catch (_) {
|
|
143
|
-
workingTabId = null;
|
|
336
|
+
st.workingTabId = null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Pestana para los comandos que ACTUAN (navigate, close_tab, eval, teclas).
|
|
344
|
+
*
|
|
345
|
+
* Aqui NO se cae a la pestana activa. Esa es la que el usuario tiene delante, y
|
|
346
|
+
* cuando no habia pestana de trabajo -arranque en frio, o despues de un
|
|
347
|
+
* `done`/`ungroup`- close_tab se la cerraba con el formulario a medio rellenar y
|
|
348
|
+
* navigate se la llevaba a otra URL. El dialogo de permiso solo dice la accion
|
|
349
|
+
* ("close_tab"), asi que el usuario ni siquiera podia ver cual moria. Fallar es
|
|
350
|
+
* lo correcto: que el agente abra o seleccione una pestana suya.
|
|
351
|
+
*/
|
|
352
|
+
async function actTab(tabId, st) {
|
|
353
|
+
const id = await ownTab(tabId, st);
|
|
354
|
+
if (id) return id;
|
|
355
|
+
throw new Error(
|
|
356
|
+
"no hay pestana de trabajo: abrela con open, elige una con select, o pasa tabId. " +
|
|
357
|
+
"No se actua sobre la pestana que el usuario tiene delante."
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* La pestana que el usuario tiene delante, con varios planes B.
|
|
363
|
+
*
|
|
364
|
+
* `lastFocusedWindow` era el unico criterio, y es justo el que falla en el caso
|
|
365
|
+
* de uso que la extension anuncia: si el usuario esta en un juego a pantalla
|
|
366
|
+
* completa, Chrome no tiene ninguna ventana enfocada y la consulta vuelve
|
|
367
|
+
* vacia. El agente recibia entonces "no hay ninguna pestana disponible" con
|
|
368
|
+
* doce pestanas abiertas delante.
|
|
369
|
+
*/
|
|
370
|
+
async function pestanaVisible() {
|
|
371
|
+
const intentos = [
|
|
372
|
+
{ active: true, lastFocusedWindow: true },
|
|
373
|
+
{ active: true, currentWindow: true },
|
|
374
|
+
{ active: true },
|
|
375
|
+
];
|
|
376
|
+
for (const q of intentos) {
|
|
377
|
+
try {
|
|
378
|
+
const [t] = await chrome.tabs.query(q);
|
|
379
|
+
if (t) return t;
|
|
380
|
+
} catch (_) {}
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
const [t] = await chrome.tabs.query({});
|
|
384
|
+
if (t) return t;
|
|
385
|
+
} catch (_) {}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Pestana para los comandos de solo lectura (snapshot, getText, screenshot,
|
|
391
|
+
* waitFor, banner): mirar lo que el usuario tiene delante es aceptable.
|
|
392
|
+
*
|
|
393
|
+
* Devuelve own=false cuando es prestada, para que el que lee NO la adopte como
|
|
394
|
+
* pestana de trabajo: adoptarla convertiria el siguiente close_tab sin tabId en
|
|
395
|
+
* el cierre de la pestana del usuario, por la puerta de atras.
|
|
396
|
+
*/
|
|
397
|
+
async function readTab(tabId, st) {
|
|
398
|
+
const id = await ownTab(tabId, st);
|
|
399
|
+
if (id) return { tabId: id, own: true };
|
|
400
|
+
const activa = await pestanaVisible();
|
|
401
|
+
if (!activa) throw new Error("no hay ninguna pestana disponible");
|
|
402
|
+
return { tabId: activa.id, own: false };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Comprueba si alguna sesión activa está usando la pestaña como pestaña de trabajo (B4).
|
|
407
|
+
* Permite que dos sesiones convivan sin que el cierre de una apague el borde
|
|
408
|
+
* de la pestaña si la otra aún la está utilizando.
|
|
409
|
+
*/
|
|
410
|
+
function tabEnUso(tabId) {
|
|
411
|
+
if (!tabId) return false;
|
|
412
|
+
for (const link of links.values()) {
|
|
413
|
+
if (link.state && link.state.workingTabId === tabId) return true;
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Pide al content script encender o apagar el borde naranja y la insignia (B1-B4).
|
|
420
|
+
* Si se pide apagar pero otra sesión aún usa la pestaña, se mantiene encendido.
|
|
421
|
+
* Si el content script no está inyectado todavía (páginas internas, cargando),
|
|
422
|
+
* se captura el error silenciosamente para no impedir la acción.
|
|
423
|
+
*/
|
|
424
|
+
async function setTabOverlay(tabId, activo) {
|
|
425
|
+
if (!tabId) return;
|
|
426
|
+
if (!activo && tabEnUso(tabId)) return;
|
|
427
|
+
try {
|
|
428
|
+
await chrome.tabs.sendMessage(tabId, { cmd: "overlay", tipo: "overlay", activo: Boolean(activo) });
|
|
429
|
+
} catch (_) {
|
|
430
|
+
// Si la página aún no tiene content script o es una URL vetada, se ignora
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// D8: Detección y reporte de descargas disparadas por acciones del agente
|
|
435
|
+
const descargasRecientes = [];
|
|
436
|
+
try {
|
|
437
|
+
if (typeof chrome !== "undefined" && chrome.downloads && chrome.downloads.onCreated) {
|
|
438
|
+
chrome.downloads.onCreated.addListener((item) => {
|
|
439
|
+
descargasRecientes.push({
|
|
440
|
+
id: item.id,
|
|
441
|
+
url: item.url,
|
|
442
|
+
filename: item.filename || "",
|
|
443
|
+
state: item.state || "in_progress",
|
|
444
|
+
timestamp: Date.now(),
|
|
445
|
+
});
|
|
446
|
+
if (descargasRecientes.length > 20) descargasRecientes.shift();
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (typeof chrome !== "undefined" && chrome.downloads && chrome.downloads.onChanged) {
|
|
450
|
+
chrome.downloads.onChanged.addListener((delta) => {
|
|
451
|
+
const item = descargasRecientes.find((d) => d.id === delta.id);
|
|
452
|
+
if (item) {
|
|
453
|
+
if (delta.filename && delta.filename.current) item.filename = delta.filename.current;
|
|
454
|
+
if (delta.state && delta.state.current) item.state = delta.state.current;
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
} catch (_) {}
|
|
459
|
+
|
|
460
|
+
async function detectarDescarga(tiempoInicio) {
|
|
461
|
+
try {
|
|
462
|
+
if (typeof chrome !== "undefined" && chrome.downloads && typeof chrome.downloads.search === "function") {
|
|
463
|
+
const items = await chrome.downloads.search({
|
|
464
|
+
startedAfter: new Date(tiempoInicio).toISOString(),
|
|
465
|
+
limit: 1,
|
|
466
|
+
});
|
|
467
|
+
if (items && items.length > 0) {
|
|
468
|
+
return {
|
|
469
|
+
id: items[0].id,
|
|
470
|
+
filename: items[0].filename || "",
|
|
471
|
+
state: items[0].state || "in_progress",
|
|
472
|
+
url: items[0].url,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} catch (_) {}
|
|
477
|
+
for (let i = descargasRecientes.length - 1; i >= 0; i--) {
|
|
478
|
+
if (descargasRecientes[i].timestamp >= tiempoInicio) {
|
|
479
|
+
return descargasRecientes[i];
|
|
144
480
|
}
|
|
145
481
|
}
|
|
146
|
-
|
|
147
|
-
if (!active) throw new Error("no hay ninguna pestana disponible");
|
|
148
|
-
return active.id;
|
|
482
|
+
return null;
|
|
149
483
|
}
|
|
150
484
|
|
|
151
485
|
/**
|
|
152
486
|
* Mete la pestana en un grupo visible llamado "Chocolatito Code", para que el
|
|
153
487
|
* usuario vea de un vistazo donde esta trabajando el agente.
|
|
154
488
|
*/
|
|
155
|
-
async function groupTab(tabId) {
|
|
489
|
+
async function groupTab(tabId, st) {
|
|
156
490
|
try {
|
|
157
|
-
if (groupId !== null) {
|
|
491
|
+
if (st.groupId !== null) {
|
|
158
492
|
try {
|
|
159
|
-
await chrome.tabGroups.get(groupId);
|
|
493
|
+
await chrome.tabGroups.get(st.groupId);
|
|
160
494
|
} catch (_) {
|
|
161
|
-
groupId = null;
|
|
495
|
+
st.groupId = null;
|
|
162
496
|
}
|
|
163
497
|
}
|
|
164
498
|
const newGroupId = await chrome.tabs.group(
|
|
165
|
-
groupId === null ? { tabIds: [tabId] } : { tabIds: [tabId], groupId }
|
|
499
|
+
st.groupId === null ? { tabIds: [tabId] } : { tabIds: [tabId], groupId: st.groupId }
|
|
166
500
|
);
|
|
167
|
-
groupId = newGroupId;
|
|
168
|
-
await chrome.tabGroups.update(groupId, {
|
|
501
|
+
st.groupId = newGroupId;
|
|
502
|
+
await chrome.tabGroups.update(st.groupId, {
|
|
169
503
|
title: GROUP_TITLE,
|
|
170
504
|
color: "orange",
|
|
171
505
|
collapsed: false,
|
|
172
506
|
});
|
|
173
|
-
return groupId;
|
|
507
|
+
return st.groupId;
|
|
174
508
|
} catch (err) {
|
|
175
509
|
return null;
|
|
176
510
|
}
|
|
@@ -182,33 +516,142 @@ async function banner(tabId, text) {
|
|
|
182
516
|
} catch (_) {}
|
|
183
517
|
}
|
|
184
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Espera a que la pestana termine de cargar DE VERDAD.
|
|
521
|
+
*
|
|
522
|
+
* La version anterior solo miraba `status === "complete"`, y lo miraba justo
|
|
523
|
+
* despues de pedir la navegacion: la pestana todavia informaba del estado de la
|
|
524
|
+
* pagina ANTERIOR, asi que la espera volvia al instante y el snapshot siguiente
|
|
525
|
+
* leia la pagina vieja. Un fallo de los que no dan error: el agente cree que ya
|
|
526
|
+
* esta en la pagina nueva y describe la anterior.
|
|
527
|
+
*
|
|
528
|
+
* Ahora hay que ver primero la transicion (estado "loading" o cambio de URL) y
|
|
529
|
+
* solo despues aceptar el "complete". El evento onUpdated resuelve rapido; el
|
|
530
|
+
* sondeo esta de red por si la extension no puede suscribirse.
|
|
531
|
+
*/
|
|
532
|
+
function esperarCarga(tabId, timeout, urlAnterior) {
|
|
533
|
+
return new Promise((resolve) => {
|
|
534
|
+
const deadline = Date.now() + (timeout || 30000);
|
|
535
|
+
let vioTransicion = urlAnterior === undefined || urlAnterior === null;
|
|
536
|
+
let terminado = false;
|
|
537
|
+
let oyente = null;
|
|
538
|
+
|
|
539
|
+
const acabar = (valor) => {
|
|
540
|
+
if (terminado) return;
|
|
541
|
+
terminado = true;
|
|
542
|
+
if (oyente) {
|
|
543
|
+
try { chrome.tabs.onUpdated.removeListener(oyente); } catch (_) {}
|
|
544
|
+
}
|
|
545
|
+
resolve(valor);
|
|
546
|
+
};
|
|
185
547
|
|
|
548
|
+
try {
|
|
549
|
+
if (chrome.tabs.onUpdated && chrome.tabs.onUpdated.addListener) {
|
|
550
|
+
oyente = (id, info) => {
|
|
551
|
+
if (id !== tabId) return;
|
|
552
|
+
if (info.status === "loading" || (info.url && info.url !== urlAnterior)) vioTransicion = true;
|
|
553
|
+
if (vioTransicion && info.status === "complete") acabar(true);
|
|
554
|
+
};
|
|
555
|
+
chrome.tabs.onUpdated.addListener(oyente);
|
|
556
|
+
}
|
|
557
|
+
} catch (_) {}
|
|
186
558
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
559
|
+
const sondeo = async () => {
|
|
560
|
+
if (terminado) return;
|
|
561
|
+
try {
|
|
562
|
+
const t = await chrome.tabs.get(tabId);
|
|
563
|
+
if (!vioTransicion && (t.status === "loading" || (t.url && t.url !== urlAnterior))) {
|
|
564
|
+
vioTransicion = true;
|
|
565
|
+
}
|
|
566
|
+
if (vioTransicion && t.status === "complete") return acabar(true);
|
|
567
|
+
} catch (_) {
|
|
568
|
+
return acabar(false); // la pestana se fue
|
|
569
|
+
}
|
|
570
|
+
if (Date.now() > deadline) return acabar(false);
|
|
571
|
+
setTimeout(sondeo, 150);
|
|
572
|
+
};
|
|
573
|
+
sondeo();
|
|
574
|
+
});
|
|
575
|
+
}
|
|
199
576
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
577
|
+
// ==================================================== acciones de confianza
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Cola de adjuntos del depurador POR PESTANA.
|
|
581
|
+
*
|
|
582
|
+
* chrome.debugger.attach falla si ya hay otro depurador adjunto a esa pestana,
|
|
583
|
+
* y con cuatro sesiones hablando por el mismo worker eso pasa en cuanto dos
|
|
584
|
+
* escriben a la vez. Antes cada una lo intentaba a lo bruto y la segunda caia
|
|
585
|
+
* al camino del DOM sin que nadie supiera por que.
|
|
586
|
+
*/
|
|
587
|
+
const colaPorPestana = new Map();
|
|
588
|
+
|
|
589
|
+
function enCola(tabId, fn) {
|
|
590
|
+
const anterior = colaPorPestana.get(tabId) || Promise.resolve();
|
|
591
|
+
const turno = anterior.then(fn, fn);
|
|
592
|
+
const marca = turno.catch(() => {});
|
|
593
|
+
colaPorPestana.set(tabId, marca);
|
|
594
|
+
marca.then(() => {
|
|
595
|
+
if (colaPorPestana.get(tabId) === marca) colaPorPestana.delete(tabId);
|
|
596
|
+
});
|
|
597
|
+
return turno;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function cdp(target, method, params) {
|
|
601
|
+
return chrome.debugger.sendCommand(target, method, params || {});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Activa la pestana, ejecuta la accion por CDP y lo deja todo como estaba.
|
|
606
|
+
*
|
|
607
|
+
* El renderizador de una pestana que no esta activa descarta los eventos de
|
|
608
|
+
* entrada: por eso escribir en segundo plano dejaba el campo vacio. Se activa
|
|
609
|
+
* solo lo que dura la accion y despues se devuelve la que estaba.
|
|
610
|
+
*
|
|
611
|
+
* Importante: esto NO toca el foco del sistema operativo. Solo cambia que
|
|
612
|
+
* pestana se ve dentro de Chrome. Si el usuario esta en un juego a pantalla
|
|
613
|
+
* completa o en otra aplicacion, no nota nada.
|
|
614
|
+
*/
|
|
615
|
+
function withTrustedTab(tabId, fn, preparar) {
|
|
616
|
+
return enCola(tabId, async () => {
|
|
617
|
+
const target = { tabId };
|
|
618
|
+
let previaId = null;
|
|
619
|
+
let attached = false;
|
|
620
|
+
try {
|
|
621
|
+
const tab = await chrome.tabs.get(tabId);
|
|
622
|
+
const [activa] = await chrome.tabs.query({ active: true, windowId: tab.windowId });
|
|
623
|
+
if (activa && activa.id !== tabId) previaId = activa.id;
|
|
624
|
+
if (!tab.active) await chrome.tabs.update(tabId, { active: true });
|
|
625
|
+
await sleep(250);
|
|
626
|
+
} catch (_) {}
|
|
627
|
+
|
|
628
|
+
// `preparar` corre DENTRO de la cola y con la pestana ya activa. Ahi es
|
|
629
|
+
// donde se piden las coordenadas del elemento: pedirlas fuera —como se
|
|
630
|
+
// hacia— significa medirlas antes de que le toque el turno, y para entonces
|
|
631
|
+
// la accion anterior puede haber movido la pagina. El clic salia entonces
|
|
632
|
+
// sobre lo que hubiera quedado en ese punto, no sobre el elemento pedido.
|
|
633
|
+
const preparado = preparar ? await preparar() : undefined;
|
|
634
|
+
|
|
635
|
+
try {
|
|
636
|
+
await chrome.debugger.attach(target, "1.3");
|
|
637
|
+
attached = true;
|
|
638
|
+
return await fn(target, preparado);
|
|
639
|
+
} finally {
|
|
640
|
+
if (attached) {
|
|
641
|
+
try { await chrome.debugger.detach(target); } catch (_) {}
|
|
642
|
+
}
|
|
643
|
+
if (previaId !== null) {
|
|
644
|
+
try { await chrome.tabs.update(previaId, { active: true }); } catch (_) {}
|
|
645
|
+
}
|
|
210
646
|
}
|
|
211
|
-
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** Pide al content script donde esta el elemento, en coordenadas del viewport. */
|
|
651
|
+
async function situar(tabId, frameId, ref, queEs) {
|
|
652
|
+
const pos = await tell(tabId, frameId, { cmd: "rect", ref });
|
|
653
|
+
if (!pos || !pos.ok) throw new Error((pos && pos.error) || `no se pudo situar ${queEs}`);
|
|
654
|
+
return pos;
|
|
212
655
|
}
|
|
213
656
|
|
|
214
657
|
/**
|
|
@@ -219,26 +662,20 @@ async function withTrustedTab(tabId, fn) {
|
|
|
219
662
|
* En Flow, el boton de enviar recibia el clic sintetico y no pasaba nada.
|
|
220
663
|
*/
|
|
221
664
|
async function clickTrusted(tabId, frameId, ref) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ==================================================== escritura de confianza
|
|
239
|
-
|
|
240
|
-
function cdp(target, method, params) {
|
|
241
|
-
return chrome.debugger.sendCommand(target, method, params || {});
|
|
665
|
+
return await withTrustedTab(
|
|
666
|
+
tabId,
|
|
667
|
+
async (target, pos) => {
|
|
668
|
+
const base = { x: pos.x, y: pos.y, button: "left", clickCount: 1 };
|
|
669
|
+
await cdp(target, "Input.dispatchMouseEvent", { type: "mouseMoved", x: pos.x, y: pos.y });
|
|
670
|
+
await sleep(60);
|
|
671
|
+
await cdp(target, "Input.dispatchMouseEvent", { ...base, type: "mousePressed", buttons: 1 });
|
|
672
|
+
await sleep(60);
|
|
673
|
+
await cdp(target, "Input.dispatchMouseEvent", { ...base, type: "mouseReleased", buttons: 0 });
|
|
674
|
+
await sleep(300);
|
|
675
|
+
return { label: pos.label };
|
|
676
|
+
},
|
|
677
|
+
() => situar(tabId, frameId, ref, "el elemento")
|
|
678
|
+
);
|
|
242
679
|
}
|
|
243
680
|
|
|
244
681
|
/**
|
|
@@ -250,36 +687,10 @@ function cdp(target, method, params) {
|
|
|
250
687
|
* proporcionar una instruccion". Es exactamente lo que pasaba.
|
|
251
688
|
*
|
|
252
689
|
* Input.insertText y Input.dispatchKeyEvent los procesa el renderizador igual que
|
|
253
|
-
* si los hubiera tecleado una persona
|
|
254
|
-
* plano: no hace falta foco ni traer la ventana al frente.
|
|
690
|
+
* si los hubiera tecleado una persona.
|
|
255
691
|
*/
|
|
256
692
|
async function typeTrusted(tabId, frameId, ref, text, submit, replace) {
|
|
257
|
-
const
|
|
258
|
-
if (!pos || !pos.ok) throw new Error((pos && pos.error) || "no se pudo situar el campo");
|
|
259
|
-
|
|
260
|
-
const target = { tabId };
|
|
261
|
-
let attached = false;
|
|
262
|
-
|
|
263
|
-
// El renderizador de una pestana que no esta activa descarta los eventos de
|
|
264
|
-
// texto: por eso escribir en segundo plano dejaba el campo vacio. Se activa la
|
|
265
|
-
// pestana solo lo que dura la escritura y despues se devuelve la que estaba.
|
|
266
|
-
//
|
|
267
|
-
// Importante: esto NO toca el foco del sistema operativo. Solo cambia que
|
|
268
|
-
// pestana se ve dentro de Chrome, y se deja como estaba. Si el usuario esta en
|
|
269
|
-
// un juego a pantalla completa o en otra aplicacion, no nota nada.
|
|
270
|
-
let previaId = null;
|
|
271
|
-
try {
|
|
272
|
-
const tab = await chrome.tabs.get(tabId);
|
|
273
|
-
const [activa] = await chrome.tabs.query({ active: true, windowId: tab.windowId });
|
|
274
|
-
if (activa && activa.id !== tabId) previaId = activa.id;
|
|
275
|
-
if (!tab.active) await chrome.tabs.update(tabId, { active: true });
|
|
276
|
-
await sleep(250);
|
|
277
|
-
} catch (_) {}
|
|
278
|
-
|
|
279
|
-
try {
|
|
280
|
-
await chrome.debugger.attach(target, "1.3");
|
|
281
|
-
attached = true;
|
|
282
|
-
|
|
693
|
+
const etiqueta = await withTrustedTab(tabId, async (target, pos) => {
|
|
283
694
|
// Clic real para que el editor tome el foco de verdad.
|
|
284
695
|
const click = { x: pos.x, y: pos.y, button: "left", clickCount: 1, buttons: 1 };
|
|
285
696
|
await cdp(target, "Input.dispatchMouseEvent", { type: "mouseMoved", x: pos.x, y: pos.y });
|
|
@@ -316,51 +727,95 @@ async function typeTrusted(tabId, frameId, ref, text, submit, replace) {
|
|
|
316
727
|
await cdp(target, "Input.dispatchKeyEvent", { ...enter, type: "keyUp" });
|
|
317
728
|
await sleep(400);
|
|
318
729
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
try { await chrome.debugger.detach(target); } catch (_) {}
|
|
322
|
-
}
|
|
323
|
-
// Se devuelve la pestana que el usuario tenia delante.
|
|
324
|
-
if (previaId !== null) {
|
|
325
|
-
try { await chrome.tabs.update(previaId, { active: true }); } catch (_) {}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
730
|
+
return pos.label;
|
|
731
|
+
}, () => situar(tabId, frameId, ref, "el campo"));
|
|
328
732
|
|
|
329
733
|
const after = await tell(tabId, frameId, { cmd: "value", ref });
|
|
330
|
-
return { label:
|
|
734
|
+
return { label: etiqueta, value: after && after.ok ? after.value : "" };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Captura la pestana.
|
|
739
|
+
*
|
|
740
|
+
* Se prefiere Page.captureScreenshot del protocolo DevTools porque funciona con
|
|
741
|
+
* la pestana en segundo plano. Pero si hay otro depurador adjunto —lo mas
|
|
742
|
+
* comun: el usuario tiene DevTools abierto— el adjunto falla, y antes eso
|
|
743
|
+
* dejaba el comando sin captura y sin explicacion, mientras que escribir y
|
|
744
|
+
* pulsar si tenian camino alternativo. Ahora tambien lo tiene: se activa la
|
|
745
|
+
* pestana un instante y se usa la captura normal de Chrome.
|
|
746
|
+
*/
|
|
747
|
+
async function capturar(tabId) {
|
|
748
|
+
try {
|
|
749
|
+
return await withTrustedTab(tabId, async (target) => {
|
|
750
|
+
const res = await cdp(target, "Page.captureScreenshot", { format: "png", captureBeyondViewport: false });
|
|
751
|
+
return { data: res.data, via: "cdp" };
|
|
752
|
+
});
|
|
753
|
+
} catch (err) {
|
|
754
|
+
if (!chrome.tabs.captureVisibleTab) throw err;
|
|
755
|
+
return await enCola(tabId, async () => {
|
|
756
|
+
let previaId = null;
|
|
757
|
+
let windowId;
|
|
758
|
+
try {
|
|
759
|
+
const tab = await chrome.tabs.get(tabId);
|
|
760
|
+
windowId = tab.windowId;
|
|
761
|
+
const [activa] = await chrome.tabs.query({ active: true, windowId });
|
|
762
|
+
if (activa && activa.id !== tabId) previaId = activa.id;
|
|
763
|
+
if (!tab.active) await chrome.tabs.update(tabId, { active: true });
|
|
764
|
+
await sleep(250);
|
|
765
|
+
const dataUrl = await chrome.tabs.captureVisibleTab(windowId, { format: "png" });
|
|
766
|
+
return {
|
|
767
|
+
data: String(dataUrl).replace(/^data:image\/png;base64,/, ""),
|
|
768
|
+
via: "tabs",
|
|
769
|
+
warning: "el depurador no estaba disponible (¿DevTools abierto?): captura por la via normal",
|
|
770
|
+
};
|
|
771
|
+
} finally {
|
|
772
|
+
if (previaId !== null) {
|
|
773
|
+
try { await chrome.tabs.update(previaId, { active: true }); } catch (_) {}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
}
|
|
331
778
|
}
|
|
332
779
|
|
|
333
780
|
// ============================================================ comandos
|
|
334
781
|
|
|
335
|
-
async function handle(msg) {
|
|
782
|
+
async function handle(msg, st) {
|
|
336
783
|
const cmd = msg.cmd;
|
|
784
|
+
if (typeof cmd !== "string" || !cmd) {
|
|
785
|
+
throw new Error("mensaje sin comando");
|
|
786
|
+
}
|
|
337
787
|
|
|
338
788
|
if (cmd === "ping") {
|
|
339
|
-
return { pong: true, version:
|
|
789
|
+
return { pong: true, version: VERSION };
|
|
340
790
|
}
|
|
341
791
|
|
|
342
792
|
if (cmd === "tabs") {
|
|
343
793
|
const tabs = await chrome.tabs.query({});
|
|
344
794
|
return {
|
|
345
|
-
working: workingTabId,
|
|
795
|
+
working: st.workingTabId,
|
|
346
796
|
tabs: tabs.map((t) => ({
|
|
347
797
|
id: t.id,
|
|
348
798
|
title: t.title || "",
|
|
349
799
|
url: t.url || "",
|
|
350
800
|
active: !!t.active,
|
|
351
|
-
grouped: t.groupId !== undefined && t.groupId === groupId,
|
|
801
|
+
grouped: t.groupId !== undefined && t.groupId === st.groupId,
|
|
802
|
+
propia: st.propias.has(t.id),
|
|
352
803
|
})),
|
|
353
804
|
};
|
|
354
805
|
}
|
|
355
806
|
|
|
356
807
|
if (cmd === "open") {
|
|
808
|
+
const vetada = motivoUrlVetada(msg.url);
|
|
809
|
+
if (vetada) throw new Error(vetada);
|
|
357
810
|
// active:false es la clave: la pestana se crea SIN robar el foco ni cambiar
|
|
358
811
|
// lo que el usuario esta viendo.
|
|
359
812
|
const tab = await chrome.tabs.create({ url: msg.url, active: false });
|
|
360
|
-
workingTabId = tab.id;
|
|
361
|
-
|
|
362
|
-
await
|
|
813
|
+
st.workingTabId = tab.id;
|
|
814
|
+
st.propias.add(tab.id);
|
|
815
|
+
await esperarCarga(tab.id, msg.timeout || 30000);
|
|
816
|
+
await groupTab(tab.id, st);
|
|
363
817
|
await banner(tab.id, msg.note || "pestana abierta");
|
|
818
|
+
await setTabOverlay(tab.id, true);
|
|
364
819
|
const info = await chrome.tabs.get(tab.id);
|
|
365
820
|
return { tabId: tab.id, title: info.title || "", url: info.url || "" };
|
|
366
821
|
}
|
|
@@ -372,42 +827,78 @@ async function handle(msg) {
|
|
|
372
827
|
(t) => (t.url || "").toLowerCase().includes(needle) || (t.title || "").toLowerCase().includes(needle)
|
|
373
828
|
);
|
|
374
829
|
if (!hit) throw new Error('ninguna pestana contiene "' + msg.match + '"');
|
|
375
|
-
workingTabId
|
|
376
|
-
|
|
830
|
+
if (st.workingTabId && st.workingTabId !== hit.id) {
|
|
831
|
+
const vieja = st.workingTabId;
|
|
832
|
+
st.workingTabId = null;
|
|
833
|
+
await setTabOverlay(vieja, false);
|
|
834
|
+
}
|
|
835
|
+
st.workingTabId = hit.id;
|
|
836
|
+
// Elegirla es un acto explicito del agente: a partir de aqui es suya y la
|
|
837
|
+
// puede cerrar. Mirarla (readTab) nunca da ese derecho.
|
|
838
|
+
st.propias.add(hit.id);
|
|
839
|
+
await groupTab(hit.id, st);
|
|
377
840
|
await banner(hit.id, "pestana seleccionada");
|
|
841
|
+
await setTabOverlay(hit.id, true);
|
|
378
842
|
return { tabId: hit.id, title: hit.title || "", url: hit.url || "" };
|
|
379
843
|
}
|
|
380
844
|
|
|
381
845
|
if (cmd === "navigate") {
|
|
382
|
-
const
|
|
846
|
+
const vetada = motivoUrlVetada(msg.url);
|
|
847
|
+
if (vetada) throw new Error(vetada);
|
|
848
|
+
const tabId = await actTab(msg.tabId, st);
|
|
849
|
+
let urlAnterior;
|
|
850
|
+
try {
|
|
851
|
+
urlAnterior = (await chrome.tabs.get(tabId)).url;
|
|
852
|
+
} catch (_) {}
|
|
853
|
+
// La espera se arma ANTES de pedir la navegacion: al reves se pierde el
|
|
854
|
+
// evento de las paginas que cargan rapido.
|
|
855
|
+
const cargando = esperarCarga(tabId, msg.timeout || 30000, urlAnterior);
|
|
383
856
|
await chrome.tabs.update(tabId, { url: msg.url });
|
|
384
|
-
await
|
|
385
|
-
workingTabId = tabId;
|
|
386
|
-
|
|
857
|
+
await cargando;
|
|
858
|
+
st.workingTabId = tabId;
|
|
859
|
+
st.propias.add(tabId);
|
|
860
|
+
await groupTab(tabId, st);
|
|
387
861
|
const info = await chrome.tabs.get(tabId);
|
|
388
862
|
await banner(tabId, "navegando a " + (info.url || "").slice(0, 60));
|
|
863
|
+
await setTabOverlay(tabId, true);
|
|
389
864
|
return { tabId, title: info.title || "", url: info.url || "" };
|
|
390
865
|
}
|
|
391
866
|
|
|
392
867
|
if (cmd === "snapshot") {
|
|
393
|
-
const tabId = await
|
|
394
|
-
|
|
868
|
+
const { tabId, own } = await readTab(msg.tabId, st);
|
|
869
|
+
// Solo se adopta como pestana de trabajo si ya era nuestra o vino con tabId.
|
|
870
|
+
if (own) {
|
|
871
|
+
st.workingTabId = tabId;
|
|
872
|
+
await setTabOverlay(tabId, true);
|
|
873
|
+
}
|
|
395
874
|
await banner(tabId, "leyendo la pagina");
|
|
396
|
-
refIndex.clear();
|
|
875
|
+
st.refIndex.clear();
|
|
397
876
|
|
|
398
877
|
const frames = await framesOf(tabId);
|
|
399
878
|
const rows = [];
|
|
400
879
|
let counter = 0;
|
|
880
|
+
let fallos = 0;
|
|
401
881
|
for (const frameId of frames) {
|
|
402
882
|
let res;
|
|
403
883
|
try {
|
|
404
|
-
|
|
884
|
+
// `nuevo` va a TODOS los frames, no solo al primero: cada frame tiene su
|
|
885
|
+
// propio content script con su propia tabla de identidades, y antes solo
|
|
886
|
+
// se limpiaba la del frame con startIndex 0. Los subframes acumulaban
|
|
887
|
+
// identidades de snapshots viejos y resolvian un ref a un boton que ya
|
|
888
|
+
// no era ese.
|
|
889
|
+
res = await tell(tabId, frameId, {
|
|
890
|
+
cmd: "snapshot",
|
|
891
|
+
startIndex: counter,
|
|
892
|
+
filter: msg.filter || "",
|
|
893
|
+
nuevo: true,
|
|
894
|
+
});
|
|
405
895
|
} catch (_) {
|
|
896
|
+
fallos++;
|
|
406
897
|
continue;
|
|
407
898
|
}
|
|
408
899
|
if (!res || !res.ok) continue;
|
|
409
900
|
for (const item of res.items) {
|
|
410
|
-
refIndex.set(item.ref, { tabId, frameId });
|
|
901
|
+
st.refIndex.set(item.ref, { tabId, frameId });
|
|
411
902
|
rows.push(item);
|
|
412
903
|
}
|
|
413
904
|
counter += res.items.length;
|
|
@@ -415,23 +906,36 @@ async function handle(msg) {
|
|
|
415
906
|
|
|
416
907
|
const info = await chrome.tabs.get(tabId);
|
|
417
908
|
const max = msg.max || 60;
|
|
909
|
+
if (rows.length === 0) {
|
|
910
|
+
// Cero elementos y todos los frames fallando casi siempre es una pagina
|
|
911
|
+
// donde Chrome no deja entrar. Decirlo aqui ahorra el viaje de intentar
|
|
912
|
+
// pulsar algo y recibir un error distinto.
|
|
913
|
+
const vetada = motivoUrlVetada(info.url);
|
|
914
|
+
if (vetada) throw new Error(vetada);
|
|
915
|
+
}
|
|
418
916
|
return {
|
|
419
917
|
tabId,
|
|
420
918
|
title: info.title || "",
|
|
421
919
|
url: info.url || "",
|
|
422
920
|
truncated: rows.length > max,
|
|
921
|
+
framesSinRespuesta: fallos || undefined,
|
|
423
922
|
elements: rows.slice(0, max),
|
|
424
923
|
};
|
|
425
924
|
}
|
|
426
925
|
|
|
427
926
|
if (cmd === "click" || cmd === "type" || cmd === "press") {
|
|
428
|
-
const loc = refIndex.get(msg.ref);
|
|
927
|
+
const loc = st.refIndex.get(msg.ref);
|
|
429
928
|
if (msg.ref !== undefined && msg.ref !== null && !loc) {
|
|
430
929
|
throw new Error("el ref " + msg.ref + " no existe; vuelve a hacer snapshot");
|
|
431
930
|
}
|
|
432
|
-
|
|
931
|
+
// Sin ref (press suele venir asi) esto escribe teclas en la pagina: es
|
|
932
|
+
// actuacion, nunca sobre la pestana que el usuario esta mirando.
|
|
933
|
+
const tabId = loc ? loc.tabId : await actTab(msg.tabId, st);
|
|
433
934
|
const frameId = loc ? loc.frameId : 0;
|
|
434
|
-
workingTabId = tabId;
|
|
935
|
+
st.workingTabId = tabId;
|
|
936
|
+
await setTabOverlay(tabId, true);
|
|
937
|
+
|
|
938
|
+
const tiempoInicioAccion = Date.now() - 50;
|
|
435
939
|
|
|
436
940
|
if (cmd === "click") await banner(tabId, "pulsando un elemento");
|
|
437
941
|
if (cmd === "type") await banner(tabId, "escribiendo");
|
|
@@ -473,28 +977,63 @@ async function handle(msg) {
|
|
|
473
977
|
if (!res || !res.ok) throw new Error((res && res.error) || "el content script no respondio");
|
|
474
978
|
|
|
475
979
|
// Se deja respirar a la pagina para que reaccione antes del siguiente paso.
|
|
476
|
-
await sleep(msg.settle
|
|
980
|
+
await sleep(msg.settle === undefined ? 600 : msg.settle);
|
|
477
981
|
const info = await chrome.tabs.get(tabId);
|
|
478
|
-
|
|
982
|
+
|
|
983
|
+
// D8: Detectar si esta acción inició o completó alguna descarga de archivo
|
|
984
|
+
const descarga = await detectarDescarga(tiempoInicioAccion);
|
|
985
|
+
if (descarga) {
|
|
986
|
+
res.download = descarga;
|
|
987
|
+
res.label = (res.label ? res.label + " " : "") + `[Descarga: ${descarga.filename || descarga.url}]`;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
return {
|
|
991
|
+
tabId,
|
|
992
|
+
title: info.title || "",
|
|
993
|
+
url: info.url || "",
|
|
994
|
+
label: res.label,
|
|
995
|
+
value: res.value,
|
|
996
|
+
via: res.via,
|
|
997
|
+
warning: res.warning,
|
|
998
|
+
download: res.download,
|
|
999
|
+
};
|
|
479
1000
|
}
|
|
480
1001
|
|
|
481
1002
|
if (cmd === "getText") {
|
|
482
|
-
const tabId = await
|
|
1003
|
+
const { tabId } = await readTab(msg.tabId, st);
|
|
483
1004
|
const res = await tell(tabId, 0, { cmd: "getText" });
|
|
484
1005
|
if (!res || !res.ok) throw new Error("no se pudo leer el texto");
|
|
485
1006
|
return res;
|
|
486
1007
|
}
|
|
487
1008
|
|
|
488
1009
|
if (cmd === "eval") {
|
|
489
|
-
|
|
1010
|
+
// eval ejecuta JavaScript arbitrario en la pagina: puede pulsar, enviar
|
|
1011
|
+
// formularios y leerlo todo. Es actuacion, no lectura.
|
|
1012
|
+
const tabId = await actTab(msg.tabId, st);
|
|
490
1013
|
const res = await tell(tabId, 0, { cmd: "eval", script: msg.script });
|
|
491
|
-
if (!res || !res.ok) throw new Error("no se pudo evaluar");
|
|
1014
|
+
if (!res || !res.ok) throw new Error((res && res.error) || "no se pudo evaluar");
|
|
492
1015
|
return res;
|
|
493
1016
|
}
|
|
494
1017
|
|
|
495
1018
|
if (cmd === "waitFor") {
|
|
496
|
-
const tabId = await
|
|
497
|
-
const
|
|
1019
|
+
const { tabId } = await readTab(msg.tabId, st);
|
|
1020
|
+
const timeout = msg.timeout || 20000;
|
|
1021
|
+
const deadline = Date.now() + timeout;
|
|
1022
|
+
|
|
1023
|
+
// D3: Espera hasta que aparezca un selector CSS en el DOM
|
|
1024
|
+
if (msg.selector || msg.css) {
|
|
1025
|
+
const sel = msg.selector || msg.css;
|
|
1026
|
+
while (Date.now() < deadline) {
|
|
1027
|
+
const res = await tell(tabId, 0, { cmd: "waitForCondition", selector: sel });
|
|
1028
|
+
if (res && res.ok && res.conditionMet) {
|
|
1029
|
+
return { ok: true, found: true, selector: sel };
|
|
1030
|
+
}
|
|
1031
|
+
await sleep(300);
|
|
1032
|
+
}
|
|
1033
|
+
return { ok: false, found: false, error: `Tiempo agotado esperando el selector "${sel}"` };
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// D3: Espera hasta que aparezca un texto concreto
|
|
498
1037
|
if (msg.text) {
|
|
499
1038
|
while (Date.now() < deadline) {
|
|
500
1039
|
const res = await tell(tabId, 0, { cmd: "getText" });
|
|
@@ -503,73 +1042,125 @@ async function handle(msg) {
|
|
|
503
1042
|
}
|
|
504
1043
|
return { found: false };
|
|
505
1044
|
}
|
|
506
|
-
|
|
507
|
-
|
|
1045
|
+
|
|
1046
|
+
// D3: Espera hasta que la pantalla "esté quieta" (sin mutaciones de DOM durante quietMs)
|
|
1047
|
+
if (msg.stable || msg.quiet || msg.condition === "stable") {
|
|
1048
|
+
while (Date.now() < deadline) {
|
|
1049
|
+
const res = await tell(tabId, 0, { cmd: "waitForCondition", stable: true, quietMs: msg.quietMs || 400 });
|
|
1050
|
+
if (res && res.ok && res.conditionMet) {
|
|
1051
|
+
return { ok: true, stable: true };
|
|
1052
|
+
}
|
|
1053
|
+
await sleep(250);
|
|
1054
|
+
}
|
|
1055
|
+
return { ok: true, stable: true, warning: "Tiempo límite alcanzado esperando quietud" };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const ms = msg.ms || 2000;
|
|
1059
|
+
await sleep(ms);
|
|
1060
|
+
return { waited: ms };
|
|
508
1061
|
}
|
|
509
1062
|
|
|
510
1063
|
if (cmd === "screenshot") {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
// estuviera activa y la ventana enfocada.
|
|
514
|
-
const tabId = await resolveTab(msg.tabId);
|
|
515
|
-
const target = { tabId };
|
|
516
|
-
await chrome.debugger.attach(target, "1.3");
|
|
517
|
-
try {
|
|
518
|
-
const res = await chrome.debugger.sendCommand(target, "Page.captureScreenshot", {
|
|
519
|
-
format: "png",
|
|
520
|
-
captureBeyondViewport: false,
|
|
521
|
-
});
|
|
522
|
-
return { data: res.data };
|
|
523
|
-
} finally {
|
|
524
|
-
try { await chrome.debugger.detach(target); } catch (_) {}
|
|
525
|
-
}
|
|
1064
|
+
const { tabId } = await readTab(msg.tabId, st);
|
|
1065
|
+
return await capturar(tabId);
|
|
526
1066
|
}
|
|
527
1067
|
|
|
528
1068
|
if (cmd === "banner") {
|
|
529
|
-
const tabId = await
|
|
1069
|
+
const { tabId } = await readTab(msg.tabId, st);
|
|
530
1070
|
await banner(tabId, msg.text);
|
|
531
1071
|
return { ok: true };
|
|
532
1072
|
}
|
|
533
1073
|
|
|
534
1074
|
if (cmd === "close_tab") {
|
|
535
1075
|
// Cerrar lo que se abre es parte de dejar el navegador como se encontro.
|
|
536
|
-
|
|
1076
|
+
// Lo que NO puede pasar es cerrar lo que no se abrio.
|
|
1077
|
+
//
|
|
1078
|
+
// Sin tabId y sin pestana de trabajo, actTab falla en vez de llevarse la del
|
|
1079
|
+
// usuario. Y con una lista explicita de tabIds -por donde antes se colaba
|
|
1080
|
+
// cualquier pestana, incluida la del usuario, sin comprobacion ninguna- se
|
|
1081
|
+
// exige que cada una sea de esta sesion.
|
|
1082
|
+
let ids;
|
|
1083
|
+
if (Array.isArray(msg.tabIds)) {
|
|
1084
|
+
const ajenas = msg.tabIds.filter((id) => !st.propias.has(id) && id !== st.workingTabId);
|
|
1085
|
+
if (ajenas.length > 0) {
|
|
1086
|
+
throw new Error(
|
|
1087
|
+
"esta sesion no abrio las pestanas " + ajenas.join(", ") + ", asi que no las cierra. " +
|
|
1088
|
+
"Solo se cierran las que abrio con open o eligio con select."
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
ids = msg.tabIds;
|
|
1092
|
+
} else {
|
|
1093
|
+
ids = [await actTab(msg.tabId, st)];
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if (ids.length === 0) return { closed: 0 };
|
|
1097
|
+
if (ids.includes(st.workingTabId)) st.workingTabId = null;
|
|
1098
|
+
for (const id of ids) st.propias.delete(id);
|
|
537
1099
|
await chrome.tabs.remove(ids);
|
|
538
|
-
|
|
1100
|
+
for (const id of ids) {
|
|
1101
|
+
await setTabOverlay(id, false);
|
|
1102
|
+
}
|
|
539
1103
|
return { closed: ids.length };
|
|
540
1104
|
}
|
|
541
1105
|
|
|
542
1106
|
if (cmd === "ungroup") {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
1107
|
+
// Antes solo se sacaba del grupo la pestana de trabajo: si la sesion habia
|
|
1108
|
+
// abierto tres, las otras dos se quedaban dentro de un grupo naranja que ya
|
|
1109
|
+
// no era de nadie, y el usuario tenia que deshacerlo a mano.
|
|
1110
|
+
const aSoltar = new Set(st.propias);
|
|
1111
|
+
if (st.workingTabId) aSoltar.add(st.workingTabId);
|
|
1112
|
+
st.groupId = null;
|
|
1113
|
+
st.workingTabId = null;
|
|
1114
|
+
st.propias.clear();
|
|
1115
|
+
st.refIndex.clear();
|
|
1116
|
+
for (const id of aSoltar) {
|
|
1117
|
+
try {
|
|
1118
|
+
await chrome.tabs.ungroup(id);
|
|
1119
|
+
} catch (_) {}
|
|
1120
|
+
await banner(id, null);
|
|
1121
|
+
await setTabOverlay(id, false);
|
|
546
1122
|
}
|
|
547
|
-
|
|
548
|
-
workingTabId = null;
|
|
549
|
-
return { ok: true };
|
|
1123
|
+
return { ok: true, soltadas: aSoltar.size };
|
|
550
1124
|
}
|
|
551
1125
|
|
|
552
1126
|
throw new Error("comando desconocido: " + cmd);
|
|
553
1127
|
}
|
|
554
1128
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1129
|
+
// Una pestana cerrada por el usuario no puede seguir siendo la de trabajo de
|
|
1130
|
+
// nadie: sin esto, el siguiente comando fallaba con el error de Chrome ("No tab
|
|
1131
|
+
// with id") en vez de decir que ya no hay donde trabajar.
|
|
1132
|
+
try {
|
|
1133
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
1134
|
+
setTabOverlay(tabId, false);
|
|
1135
|
+
for (const link of links.values()) {
|
|
1136
|
+
if (!link.state) continue;
|
|
1137
|
+
link.state.propias.delete(tabId);
|
|
1138
|
+
if (link.state.workingTabId === tabId) link.state.workingTabId = null;
|
|
1139
|
+
guardarEstado(link);
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
} catch (_) {}
|
|
1143
|
+
|
|
1144
|
+
// Al navegar o recargar la página, se reinyecta el content script y el overlay
|
|
1145
|
+
// se apaga. Si la pestaña sigue siendo de trabajo para alguna sesión, se vuelve a activar (B4).
|
|
1146
|
+
try {
|
|
1147
|
+
if (typeof chrome !== "undefined" && chrome.webNavigation && chrome.webNavigation.onCompleted && typeof chrome.webNavigation.onCompleted.addListener === "function") {
|
|
1148
|
+
chrome.webNavigation.onCompleted.addListener((details) => {
|
|
1149
|
+
if (details && details.frameId === 0 && tabEnUso(details.tabId)) {
|
|
1150
|
+
setTabOverlay(details.tabId, true);
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
} catch (_) {}
|
|
558
1155
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
const t = await chrome.tabs.get(tabId);
|
|
565
|
-
if (t.status === "complete" || Date.now() > deadline) return resolve();
|
|
566
|
-
} catch (_) {
|
|
567
|
-
return resolve();
|
|
1156
|
+
try {
|
|
1157
|
+
if (typeof chrome !== "undefined" && chrome.tabs && chrome.tabs.onUpdated && typeof chrome.tabs.onUpdated.addListener === "function") {
|
|
1158
|
+
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
|
|
1159
|
+
if (changeInfo && changeInfo.status === "complete" && tabEnUso(tabId)) {
|
|
1160
|
+
setTabOverlay(tabId, true);
|
|
568
1161
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
});
|
|
573
|
-
}
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
} catch (_) {}
|
|
574
1165
|
|
|
575
1166
|
for (const p of WS_PORTS) connect(p);
|