`scripts/atuq-captura-ia.py`: el arnés de sway headless de los guardianes + `grim` + un PNG. No es un guardián (los veredictos siguen saliendo del `dump`); es la evidencia que ningún log puede dar: que el panel se pinta y que la respuesta está ahí. Encontró tres cosas, ninguna visible en un log: 1. **la barra lateral se abría SOLA** en el primer arranque, ocupando un tercio de la ventana. Gecko lo hace al instalar una extensión con `sidebar_action`, y para un navegador que la trae de fábrica eso es imponerle un panel a todo el mundo. `"open_at_install": false`, con captura del después; 2. el diálogo «Close Firefox» en la segunda corrida — el arnés mataba el navegador sin despedirse y quedaba el `.parentlock`. Arreglado en el arnés; 3. dos barras de notificación VACÍAS en el arranque. El atajo era reportarlas como fuga de marca, y era falso: instrumentando una copia del artefacto para volcar el DOM salieron `sandbox-content-disabled` (lo apaga el propio arnés) y `startup-restore-session-suggestion` (por el cierre abrupto anterior). El texto está en el DOM y no en los píxeles — comprobado además quitando nuestro CSS, con el mismo resultado ⇒ es el render por software de la jaula. ⇒ Una captura muestra síntomas; el DOM dice de quién son. Sin ese segundo paso, dos de los tres habrían entrado al SDD como bugs nuestros. El log del navegador viaja junto a la foto, siempre: una captura muestra que algo se ve raro y no por qué. Y `test-atuq-ia.py` + `test-atuq-archivo-semantico.py` siguen verdes tras el cambio.
104 lines
4.2 KiB
JavaScript
104 lines
4.2 KiB
JavaScript
"use strict";
|
|
// ia — el puente entre la barra lateral y el modelo LOCAL (SDD 26 §6.7), v1.
|
|
//
|
|
// La página de la barra lateral no habla native messaging: habla con este fondo, y
|
|
// el fondo con `puriy-costura`. No es ceremonia — el puerto nativo tiene que
|
|
// sobrevivir a que la barra lateral se abra y se cierre, y una página que se
|
|
// destruye con cada cierre se llevaría el proceso del host (y el modelo cargado)
|
|
// con ella.
|
|
//
|
|
// ── POR QUÉ NO SE ABRE SOLA ──────────────────────────────────────────────────
|
|
// `sidebar_action.open_at_install: false`. Sin eso Gecko abre la barra lateral en el PRIMER
|
|
// arranque y se queda: una captura del arranque limpio la mostró ocupando un tercio de la ventana
|
|
// sin que nadie la pidiera. Un navegador que al abrirse te impone un panel es un navegador que se
|
|
// siente ajeno — y esto no se ve en ningún log, sólo mirando.
|
|
|
|
// ── LO QUE ESTA EXTENSIÓN NO HACE ────────────────────────────────────────────
|
|
// No manda NADA a ninguna nube, y no puede: el host habla con un `llama-server`
|
|
// que corre en esta máquina, por un socket UNIX. Tampoco manda la página que estás
|
|
// viendo: lo único que viaja es lo que escribiste.
|
|
|
|
const HOST = "puriy_costura";
|
|
|
|
let puerto = null;
|
|
let siguiente = 1;
|
|
// id de petición → resolve, para devolverle a la barra lateral SU respuesta y no
|
|
// la primera que llegue.
|
|
const enVuelo = new Map();
|
|
|
|
function di(s) {
|
|
try { dump("IA " + s + "\n"); } catch (e) {}
|
|
}
|
|
|
|
function conectar() {
|
|
try {
|
|
puerto = browser.runtime.connectNative(HOST);
|
|
} catch (e) {
|
|
di("SIN HOST " + e);
|
|
return false;
|
|
}
|
|
di("CONECTADO " + HOST);
|
|
puerto.onMessage.addListener((r) => {
|
|
const pendiente = r && r.id ? enVuelo.get(r.id) : null;
|
|
if (!pendiente) return;
|
|
enVuelo.delete(r.id);
|
|
if (!r.ok) {
|
|
di("ERROR " + (r.error || "sin causa"));
|
|
} else if (r.verb === "archive.ask") {
|
|
di("ARCHIVO " + (r.matches || []).length + " de " + r.archived +
|
|
" (indexadas " + r.indexed + ")");
|
|
for (const m of r.matches || []) {
|
|
di("COINCIDENCIA " + m.score.toFixed(4) + " " + m.page.url);
|
|
}
|
|
} else {
|
|
di("RESPUESTA tokens=" + (r.tokens === undefined ? "?" : r.tokens) +
|
|
" modelo=" + (r.model || "?") + " texto=" + JSON.stringify(r.answer || ""));
|
|
}
|
|
pendiente(r);
|
|
});
|
|
puerto.onDisconnect.addListener(() => {
|
|
const e = puerto && puerto.error ? puerto.error.message : "sin causa";
|
|
di("DESCONECTADO " + e);
|
|
puerto = null;
|
|
// Al caerse el host se cae su `llama-server`, así que lo que estaba en vuelo no
|
|
// va a volver nunca: se contesta con la causa en vez de dejar la barra lateral
|
|
// esperando para siempre.
|
|
for (const [id, resolver] of enVuelo) {
|
|
enVuelo.delete(id);
|
|
resolver({ ok: false, error: "se cortó la conexión con el host: " + e });
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// El mismo puerto sirve las dos preguntas del panel, y no es casualidad: las dos las
|
|
// contesta el MISMO host con el MISMO motor local — una con el modelo de chat y la otra
|
|
// con el de embeddings sobre lo que ya leíste (§6.3).
|
|
function pedir(verbo, campo, texto) {
|
|
if (!puerto && !conectar()) {
|
|
return Promise.resolve({ ok: false, error: "no hay host nativo en esta imagen" });
|
|
}
|
|
const id = siguiente++;
|
|
di("PIDE " + verbo + " id=" + id + " " + JSON.stringify(texto));
|
|
return new Promise((resolver) => {
|
|
enVuelo.set(id, resolver);
|
|
try {
|
|
puerto.postMessage({ id, verb: verbo, [campo]: texto });
|
|
} catch (e) {
|
|
enVuelo.delete(id);
|
|
resolver({ ok: false, error: "no pude enviar: " + e });
|
|
}
|
|
});
|
|
}
|
|
|
|
browser.runtime.onMessage.addListener((msg) => {
|
|
if (!msg || typeof msg.texto !== "string") return;
|
|
if (msg.tipo === "preguntar") return pedir("ai.ask", "prompt", msg.texto);
|
|
if (msg.tipo === "archivo") return pedir("archive.ask", "query", msg.texto);
|
|
});
|
|
|
|
di("LISTA v1 el modelo corre en esta máquina; no sale nada a la red");
|
|
// La URL base la asigna Gecko por perfil: decirla es lo que deja abrir el panel desde
|
|
// fuera (y lo que usa el guardián para no adivinar un UUID).
|
|
di("BASE " + browser.runtime.getURL(""));
|