primocode 7.11.0 → 7.12.1
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/lib/streaming.js +249 -71
- package/package.json +1 -1
package/lib/streaming.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* O agente age no desktop do usuário (clica, digita, abre janela), mas rápido
|
|
5
5
|
* demais para acompanhar a olho nu — e às vezes numa janela que não está na
|
|
6
|
-
* frente. O /streaming liga um servidorzinho HTTP local que
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* frente. O /streaming liga um servidorzinho HTTP local que abre um site com
|
|
7
|
+
* a cara do PrimoCode: mascote + nome no cabeçalho e, abaixo, uma "janela"
|
|
8
|
+
* com ABAS — "Ao vivo" mostra a tela agora, e cada app/janela em que o agente
|
|
9
|
+
* mexeu vira uma aba própria, com os passos e as fotos daquele app. Dá para
|
|
10
|
+
* voltar e rever o que ele já fez, passo a passo.
|
|
9
11
|
*
|
|
10
12
|
* Zero dependências: http nativo, uma página HTML embutida que consulta
|
|
11
13
|
* /state a cada 700ms. Só escuta em 127.0.0.1 — nada disso sai da máquina.
|
|
@@ -20,8 +22,11 @@ const os = require('os');
|
|
|
20
22
|
const desktop = require('./desktop');
|
|
21
23
|
|
|
22
24
|
const DIR = path.join(os.homedir(), '.primocode', 'streaming');
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
+
const MAX_LOG = 300;
|
|
26
|
+
// Fotos guardadas em disco para o "voltar no tempo" das abas. Além disso a
|
|
27
|
+
// pasta viraria um álbum infinito — as mais velhas saem.
|
|
28
|
+
const MAX_FRAMES = 80;
|
|
29
|
+
const MAX_ABAS = 12;
|
|
25
30
|
// Uma foto por ação no máximo a cada 600ms: digitar letra por letra não pode
|
|
26
31
|
// virar uma rajada de screenshots.
|
|
27
32
|
const INTERVALO_FOTO_MS = 600;
|
|
@@ -30,7 +35,10 @@ const state = {
|
|
|
30
35
|
on: false,
|
|
31
36
|
server: null,
|
|
32
37
|
url: '',
|
|
33
|
-
|
|
38
|
+
frames: [], // [{ id, hora, acao, aba }] — id é o nome do arquivo
|
|
39
|
+
proximoFrame: 1,
|
|
40
|
+
abas: [], // [{ id, nome, passos: 0 }] — "Ao vivo" é virtual, não entra aqui
|
|
41
|
+
abaAtual: null,
|
|
34
42
|
log: [],
|
|
35
43
|
erroFoto: null,
|
|
36
44
|
ultimaFoto: 0,
|
|
@@ -88,6 +96,33 @@ function descrever(name, args = {}) {
|
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
|
|
99
|
+
// ── Abas: uma por app/janela em que o agente mexeu ────────────────────────
|
|
100
|
+
// Abrir um app, focar uma janela ou abrir um site começa (ou reencontra) a
|
|
101
|
+
// aba daquele contexto. As ações seguintes caem nela — é o que deixa o
|
|
102
|
+
// "o que a IA já fez" navegável depois, em vez de um rolo único.
|
|
103
|
+
|
|
104
|
+
function nomeDeAba(name, args = {}) {
|
|
105
|
+
if (name === 'desktop_open_app') return String(args.app || args.name || '').trim();
|
|
106
|
+
if (name === 'desktop_focus') return String(args.window || args.title || '').trim();
|
|
107
|
+
if (name === 'browser_open') {
|
|
108
|
+
try { return new URL(String(args.url)).hostname || 'navegador'; } catch { return 'navegador'; }
|
|
109
|
+
}
|
|
110
|
+
if (args && args.window) return String(args.window).trim();
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function entrarNaAba(nome) {
|
|
115
|
+
const alvo = corta(nome, 24);
|
|
116
|
+
if (!alvo) return;
|
|
117
|
+
let aba = state.abas.find((a) => a.nome.toLowerCase() === alvo.toLowerCase());
|
|
118
|
+
if (!aba) {
|
|
119
|
+
aba = { id: state.abas.length + 1, nome: alvo, passos: 0 };
|
|
120
|
+
state.abas.push(aba);
|
|
121
|
+
if (state.abas.length > MAX_ABAS) state.abas.shift();
|
|
122
|
+
}
|
|
123
|
+
state.abaAtual = aba;
|
|
124
|
+
}
|
|
125
|
+
|
|
91
126
|
/**
|
|
92
127
|
* Chamada pelo loop do agente depois de CADA tool. Quando o streaming está
|
|
93
128
|
* desligado é um no-op — o caminho normal não paga nada por isto.
|
|
@@ -95,119 +130,262 @@ function descrever(name, args = {}) {
|
|
|
95
130
|
function step(name, args, result) {
|
|
96
131
|
if (!state.on) return;
|
|
97
132
|
|
|
133
|
+
const troca = nomeDeAba(name, args || {});
|
|
134
|
+
if (troca) entrarNaAba(troca);
|
|
135
|
+
if (!state.abaAtual && ACOES_NA_TELA.has(name)) entrarNaAba('Tela');
|
|
136
|
+
|
|
98
137
|
state.passo++;
|
|
99
|
-
state.
|
|
138
|
+
if (state.abaAtual) state.abaAtual.passos++;
|
|
139
|
+
|
|
140
|
+
const entrada = {
|
|
100
141
|
n: state.passo,
|
|
101
142
|
hora: new Date().toTimeString().slice(0, 8),
|
|
102
143
|
acao: descrever(name, args || {}),
|
|
103
144
|
ok: !(result && result.ok === false),
|
|
104
145
|
erro: result && result.ok === false ? corta(result.error, 120) : undefined,
|
|
105
|
-
|
|
106
|
-
|
|
146
|
+
aba: state.abaAtual ? state.abaAtual.id : null,
|
|
147
|
+
};
|
|
107
148
|
|
|
108
|
-
if (ACOES_NA_TELA.has(name))
|
|
149
|
+
if (ACOES_NA_TELA.has(name)) {
|
|
150
|
+
const frame = capturar(entrada);
|
|
151
|
+
if (frame) entrada.frame = frame;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
state.log.push(entrada);
|
|
155
|
+
if (state.log.length > MAX_LOG) state.log.splice(0, state.log.length - MAX_LOG);
|
|
109
156
|
}
|
|
110
157
|
|
|
111
|
-
function capturar() {
|
|
158
|
+
function capturar(entrada) {
|
|
112
159
|
const agora = Date.now();
|
|
113
|
-
if (agora - state.ultimaFoto < INTERVALO_FOTO_MS) return;
|
|
160
|
+
if (agora - state.ultimaFoto < INTERVALO_FOTO_MS) return null;
|
|
114
161
|
state.ultimaFoto = agora;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
162
|
+
|
|
163
|
+
const id = state.proximoFrame;
|
|
164
|
+
const arquivo = path.join(DIR, `frame-${id}.png`);
|
|
165
|
+
const r = desktop.screenshot(arquivo);
|
|
166
|
+
if (!r.ok) { if (!state.erroFoto) state.erroFoto = r.error; return null; }
|
|
167
|
+
|
|
168
|
+
state.erroFoto = null;
|
|
169
|
+
state.proximoFrame++;
|
|
170
|
+
state.frames.push({ id, hora: entrada.hora, acao: entrada.acao, aba: entrada.aba });
|
|
171
|
+
while (state.frames.length > MAX_FRAMES) {
|
|
172
|
+
const velho = state.frames.shift();
|
|
173
|
+
try { fs.unlinkSync(path.join(DIR, `frame-${velho.id}.png`)); } catch {}
|
|
174
|
+
}
|
|
175
|
+
return id;
|
|
118
176
|
}
|
|
119
177
|
|
|
120
178
|
// ── Página ────────────────────────────────────────────────────────────────
|
|
179
|
+
// O desenho combinado: mascote + PrimoCode no cabeçalho; abaixo, uma "janela"
|
|
180
|
+
// de borda branca com as três bolinhas e as ABAS; dentro, a tela que a IA
|
|
181
|
+
// está usando (ou usou, nas abas antigas), com os passos daquela aba embaixo.
|
|
182
|
+
|
|
183
|
+
// O mascote do CLI (▐▛███▜▌ …) redesenhado em SVG, célula a célula, na mesma
|
|
184
|
+
// paleta: bloco cheio na cor viva, contorno na apagada.
|
|
185
|
+
const MASCOTE_SVG = `<svg viewBox="0 0 9 3.6" aria-hidden="true">
|
|
186
|
+
<g fill="#0891b2"><rect x="1" y="0" width="1" height="1"/><rect x="7" y="0" width="1" height="1"/><rect x="0" y="1" width="1" height="1" opacity=".7"/><rect x="8" y="1" width="1" height="1" opacity=".7"/><rect x="1" y="1" width="1" height="1"/><rect x="7" y="1" width="1" height="1"/></g>
|
|
187
|
+
<g fill="#22d3ee"><rect x="2" y="0" width="5" height="1" rx=".12"/><rect x="2" y="1" width="5" height="1"/><rect x="1.9" y="2.2" width=".9" height=".5" rx=".1"/><rect x="3.4" y="2.2" width=".9" height=".5" rx=".1"/><rect x="4.9" y="2.2" width=".9" height=".5" rx=".1"/><rect x="6.4" y="2.2" width=".9" height=".5" rx=".1"/></g>
|
|
188
|
+
<g fill="#0b1220"><rect x="3" y=".35" width=".8" height=".5" rx=".1"/><rect x="5.2" y=".35" width=".8" height=".5" rx=".1"/><rect x="3.9" y="1.25" width="1.2" height=".28" rx=".14"/></g>
|
|
189
|
+
</svg>`;
|
|
190
|
+
|
|
191
|
+
// A página segue o desenho do dono, traço a traço: fundo grafite, mascote
|
|
192
|
+
// inclinado no canto, "Primocode" branco no alto; a janela de borda branca
|
|
193
|
+
// grossa e canto bem redondo; barra #232323 com as três bolinhas e as abas em
|
|
194
|
+
// texto branco centralizado, separadas por "|"; dentro, a tela preta com a
|
|
195
|
+
// imagem ao vivo. O histórico (miniaturas + passos) fica ABAIXO da janela,
|
|
196
|
+
// discreto — a moldura fica idêntica ao desenho.
|
|
121
197
|
const PAGINA = `<!doctype html>
|
|
122
198
|
<html lang="pt-BR"><head><meta charset="utf-8">
|
|
123
199
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
124
200
|
<title>PrimoCode · Ao vivo</title>
|
|
125
201
|
<style>
|
|
126
202
|
* { box-sizing: border-box; margin: 0; }
|
|
127
|
-
body { background: #
|
|
128
|
-
|
|
129
|
-
|
|
203
|
+
body { background: #262626; color: #fff; font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 0 16px 30px; }
|
|
204
|
+
|
|
205
|
+
/* Cabeçalho: mascote inclinado no canto, nome centrado */
|
|
206
|
+
#topo { position: relative; width: min(1100px, 100%); padding: 44px 0 30px; text-align: center; }
|
|
207
|
+
#topo .mascote { position: absolute; left: 6px; top: 30px; width: 96px; height: 40px; transform: rotate(-32deg); }
|
|
208
|
+
#topo .mascote svg { width: 100%; height: 100%; display: block; }
|
|
209
|
+
#topo h1 { font-size: clamp(34px, 6vw, 58px); font-weight: 600; letter-spacing: .5px; }
|
|
210
|
+
#topo .vivo { position: absolute; right: 6px; top: 46px; display: inline-flex; align-items: center; gap: 7px; color: #8a8a8a; font-size: 12px; }
|
|
211
|
+
.dot { width: 9px; height: 9px; border-radius: 50%; background: #46d160; animation: pulsa 1.4s infinite; }
|
|
130
212
|
@keyframes pulsa { 50% { opacity: .25 } }
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.
|
|
144
|
-
.
|
|
145
|
-
.
|
|
213
|
+
|
|
214
|
+
/* A janela — borda branca grossa, canto bem redondo */
|
|
215
|
+
#janela { width: min(1100px, 100%); background: #000; border: 10px solid #f5f5f5; border-radius: 46px; overflow: hidden; display: flex; flex-direction: column; }
|
|
216
|
+
#barra { position: relative; display: flex; align-items: center; background: #232323; padding: 14px 26px; min-height: 58px; }
|
|
217
|
+
#barra .b { flex: none; margin-right: 12px; }
|
|
218
|
+
#barra .b.r { width: 17px; height: 17px; border-radius: 50%; background: #d40000; }
|
|
219
|
+
#barra .b.y { width: 30px; height: 17px; border-radius: 50%; background: #ffdd00; }
|
|
220
|
+
#barra .b.g { width: 24px; height: 14px; border-radius: 50%; background: #33cc33; }
|
|
221
|
+
|
|
222
|
+
/* Abas: texto branco centralizado na barra, separadas por | */
|
|
223
|
+
#abas { position: absolute; left: 50%; transform: translateX(-50%); max-width: 62%; display: flex; align-items: center; gap: 12px; overflow-x: auto; white-space: nowrap; scrollbar-width: none; font-size: clamp(16px, 2.4vw, 24px); font-weight: 500; }
|
|
224
|
+
#abas::-webkit-scrollbar { display: none; }
|
|
225
|
+
.aba { border: 0; background: none; cursor: pointer; font: inherit; color: rgba(255,255,255,.5); padding: 2px 2px; }
|
|
226
|
+
.aba:hover { color: rgba(255,255,255,.8); }
|
|
227
|
+
.aba.ativa { color: #fff; }
|
|
228
|
+
.sep { color: rgba(255,255,255,.45); font-weight: 400; }
|
|
229
|
+
|
|
230
|
+
/* A tela */
|
|
231
|
+
#tela { position: relative; min-height: 470px; display: flex; align-items: center; justify-content: center; background: #000; }
|
|
232
|
+
#tela img { width: 100%; max-height: 64vh; object-fit: contain; display: none; }
|
|
233
|
+
#aviso { color: #fff; text-align: center; font-size: clamp(20px, 3.4vw, 34px); font-weight: 500; padding: 110px 20px; }
|
|
234
|
+
#carimbo { position: absolute; left: 16px; bottom: 12px; background: rgba(0,0,0,.75); border: 1px solid #2e2e2e; color: #bdbdbd; font-size: 12px; padding: 3px 10px; border-radius: 8px; display: none; max-width: 85%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
235
|
+
|
|
236
|
+
/* Histórico: fora da moldura, discreto, abaixo da janela */
|
|
237
|
+
#historico { width: min(1100px, 100%); padding-top: 14px; }
|
|
238
|
+
#filme { display: flex; gap: 6px; padding: 2px 2px 8px; overflow-x: auto; }
|
|
239
|
+
.foto { flex: none; border: 1px solid #3a3a3a; border-radius: 6px; background: #000; cursor: pointer; padding: 0; width: 84px; height: 50px; overflow: hidden; opacity: .7; }
|
|
240
|
+
.foto img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
241
|
+
.foto.ativa { border-color: #fff; opacity: 1; }
|
|
242
|
+
#passos { max-height: 170px; overflow-y: auto; }
|
|
243
|
+
.item { padding: 3px 6px; display: flex; gap: 8px; font-size: 13px; }
|
|
244
|
+
.item .n { color: #6f6f6f; min-width: 3ch; text-align: right; }
|
|
245
|
+
.item .ok { color: #46d160; } .item .falha { color: #e5484d; }
|
|
246
|
+
.item .txt { flex: 1; word-break: break-word; color: #d6d6d6; }
|
|
247
|
+
.item .txt .erro { color: #e5484d; font-size: 12px; display: block; }
|
|
248
|
+
.item .hora { color: #6f6f6f; font-size: 11px; }
|
|
249
|
+
.item.temfoto { cursor: pointer; } .item.temfoto:hover .txt { color: #fff; }
|
|
250
|
+
|
|
251
|
+
footer { color: #6f6f6f; font-size: 12px; padding-top: 14px; }
|
|
146
252
|
</style></head>
|
|
147
253
|
<body>
|
|
148
|
-
<
|
|
149
|
-
<
|
|
254
|
+
<div id="topo">
|
|
255
|
+
<div class="mascote">${MASCOTE_SVG}</div>
|
|
256
|
+
<h1>Primocode</h1>
|
|
257
|
+
<span class="vivo"><span class="dot"></span><span id="st">conectando…</span></span>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div id="janela">
|
|
261
|
+
<div id="barra">
|
|
262
|
+
<span class="b r"></span><span class="b y"></span><span class="b g"></span>
|
|
263
|
+
<nav id="abas"></nav>
|
|
264
|
+
</div>
|
|
150
265
|
<section id="tela">
|
|
151
|
-
<img id="frame" alt="tela
|
|
152
|
-
<div id="aviso">
|
|
266
|
+
<img id="frame" alt="tela que a IA está usando">
|
|
267
|
+
<div id="aviso">Tela que a IA está</div>
|
|
268
|
+
<div id="carimbo"></div>
|
|
153
269
|
</section>
|
|
154
|
-
|
|
155
|
-
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div id="historico">
|
|
273
|
+
<div id="filme"></div>
|
|
274
|
+
<div id="passos"></div>
|
|
275
|
+
</div>
|
|
276
|
+
<footer>só nesta máquina (127.0.0.1) · /streaming desliga</footer>
|
|
277
|
+
|
|
156
278
|
<script>
|
|
157
|
-
|
|
279
|
+
const VIVO = 0; // id da aba "Ao vivo"
|
|
280
|
+
let dados = { abas: [], frames: [], log: [] };
|
|
281
|
+
let abaSel = VIVO; // aba escolhida pelo usuário
|
|
282
|
+
let fotoSel = null; // frame fixado no filmstrip (null = mais recente)
|
|
283
|
+
let ultimoEstado = '';
|
|
284
|
+
|
|
285
|
+
function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|
286
|
+
|
|
287
|
+
function framesDaAba(id) {
|
|
288
|
+
return id === VIVO ? dados.frames : dados.frames.filter((f) => f.aba === id);
|
|
289
|
+
}
|
|
290
|
+
function logDaAba(id) {
|
|
291
|
+
return id === VIVO ? dados.log : dados.log.filter((e) => e.aba === id);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function render() {
|
|
295
|
+
// Abas: "Ao vivo" + uma por app/janela, texto separado por |
|
|
296
|
+
const nav = document.getElementById('abas');
|
|
297
|
+
const abas = [{ id: VIVO, nome: 'Ao vivo' }, ...dados.abas];
|
|
298
|
+
nav.innerHTML = abas.map((a) =>
|
|
299
|
+
'<button class="aba' + (a.id === abaSel ? ' ativa' : '') + '" data-id="' + a.id + '">' + esc(a.nome) + '</button>'
|
|
300
|
+
).join('<span class="sep">|</span>');
|
|
301
|
+
nav.querySelectorAll('.aba').forEach((b) => b.onclick = () => { abaSel = Number(b.dataset.id); fotoSel = null; render(); });
|
|
302
|
+
|
|
303
|
+
// Imagem: a fixada no filmstrip, senão a mais nova da aba
|
|
304
|
+
const fs = framesDaAba(abaSel);
|
|
305
|
+
const atual = fotoSel != null ? fs.find((f) => f.id === fotoSel) : fs[fs.length - 1];
|
|
306
|
+
const img = document.getElementById('frame');
|
|
307
|
+
const aviso = document.getElementById('aviso');
|
|
308
|
+
const carimbo = document.getElementById('carimbo');
|
|
309
|
+
if (atual) {
|
|
310
|
+
const src = '/frame?n=' + atual.id;
|
|
311
|
+
if (img.dataset.n !== String(atual.id)) { img.src = src; img.dataset.n = String(atual.id); }
|
|
312
|
+
img.style.display = 'block'; aviso.style.display = 'none';
|
|
313
|
+
carimbo.style.display = 'block';
|
|
314
|
+
carimbo.textContent = '#' + atual.id + ' · ' + atual.hora + ' · ' + atual.acao + (fotoSel != null ? ' (clique na miniatura de novo para voltar ao mais recente)' : '');
|
|
315
|
+
} else {
|
|
316
|
+
img.style.display = 'none'; aviso.style.display = 'block'; carimbo.style.display = 'none';
|
|
317
|
+
if (dados.erroFoto) aviso.textContent = 'Sem foto da tela: ' + dados.erroFoto;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Filmstrip da aba (as últimas 14)
|
|
321
|
+
const filme = document.getElementById('filme');
|
|
322
|
+
const tira = fs.slice(-14);
|
|
323
|
+
filme.innerHTML = tira.map((f) =>
|
|
324
|
+
'<button class="foto' + ((atual && f.id === atual.id) ? ' ativa' : '') + '" data-id="' + f.id + '" title="' + esc(f.acao) + '"><img loading="lazy" src="/frame?n=' + f.id + '"></button>').join('');
|
|
325
|
+
filme.querySelectorAll('.foto').forEach((b) => b.onclick = () => {
|
|
326
|
+
const id = Number(b.dataset.id);
|
|
327
|
+
fotoSel = (fotoSel === id) ? null : id; // clicar de novo volta ao "mais recente"
|
|
328
|
+
render();
|
|
329
|
+
});
|
|
330
|
+
if (fotoSel == null) filme.scrollLeft = filme.scrollWidth;
|
|
331
|
+
|
|
332
|
+
// Passos da aba (mais novos primeiro)
|
|
333
|
+
const passos = logDaAba(abaSel).slice(-40).reverse();
|
|
334
|
+
document.getElementById('passos').innerHTML = passos.map((e) =>
|
|
335
|
+
'<div class="item' + (e.frame ? ' temfoto" data-frame="' + e.frame : '') + '">'
|
|
336
|
+
+ '<span class="n">' + e.n + '</span>'
|
|
337
|
+
+ '<span class="' + (e.ok ? 'ok' : 'falha') + '">' + (e.ok ? '✔' : '✗') + '</span>'
|
|
338
|
+
+ '<span class="txt">' + esc(e.acao) + (e.erro ? '<span class="erro">' + esc(e.erro) + '</span>' : '') + '</span>'
|
|
339
|
+
+ '<span class="hora">' + e.hora + '</span></div>').join('');
|
|
340
|
+
document.querySelectorAll('.item.temfoto').forEach((el) => el.onclick = () => { fotoSel = Number(el.dataset.frame); render(); });
|
|
341
|
+
}
|
|
342
|
+
|
|
158
343
|
async function tick() {
|
|
159
344
|
try {
|
|
160
345
|
const s = await (await fetch('/state')).json();
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
img.src = '/frame?' + s.frame;
|
|
166
|
-
img.style.display = 'block';
|
|
167
|
-
document.getElementById('aviso').style.display = 'none';
|
|
168
|
-
}
|
|
169
|
-
if (s.erroFoto) {
|
|
170
|
-
document.getElementById('aviso').textContent = 'Sem foto da tela: ' + s.erroFoto;
|
|
171
|
-
}
|
|
172
|
-
const fim = s.log.length ? s.log[s.log.length - 1].n : -1;
|
|
173
|
-
if (fim !== ultimoPasso) {
|
|
174
|
-
ultimoPasso = fim;
|
|
175
|
-
const el = document.getElementById('log');
|
|
176
|
-
el.innerHTML = s.log.slice().reverse().map((e, i) =>
|
|
177
|
-
'<div class="item' + (i === 0 ? ' novo' : '') + '">'
|
|
178
|
-
+ '<span class="n">' + e.n + '</span>'
|
|
179
|
-
+ '<span class="' + (e.ok ? 'ok' : 'falha') + '">' + (e.ok ? '✔' : '✗') + '</span>'
|
|
180
|
-
+ '<span class="txt">' + esc(e.acao)
|
|
181
|
-
+ (e.erro ? '<span class="erro">' + esc(e.erro) + '</span>' : '')
|
|
182
|
-
+ '</span><span class="hora">' + e.hora + '</span></div>'
|
|
183
|
-
).join('');
|
|
184
|
-
}
|
|
346
|
+
dados = s;
|
|
347
|
+
document.getElementById('st').textContent = 'ao vivo';
|
|
348
|
+
const assinatura = JSON.stringify([s.frames.length, s.log.length, s.abas.length, s.erroFoto]);
|
|
349
|
+
if (assinatura !== ultimoEstado) { ultimoEstado = assinatura; render(); }
|
|
185
350
|
} catch (e) {
|
|
186
351
|
document.getElementById('st').textContent = 'reconectando…';
|
|
187
352
|
}
|
|
188
353
|
setTimeout(tick, 700);
|
|
189
354
|
}
|
|
190
|
-
function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|
191
355
|
tick();
|
|
192
356
|
</script>
|
|
193
357
|
</body></html>`;
|
|
194
358
|
|
|
359
|
+
|
|
195
360
|
function start() {
|
|
196
361
|
if (state.on) return Promise.resolve(state.url);
|
|
197
362
|
try { fs.mkdirSync(DIR, { recursive: true }); } catch {}
|
|
363
|
+
// Fotos de sessões passadas não pertencem a esta linha do tempo.
|
|
364
|
+
try { for (const f of fs.readdirSync(DIR)) if (/^frame-\d+\.png$/.test(f)) fs.unlinkSync(path.join(DIR, f)); } catch {}
|
|
198
365
|
|
|
199
366
|
return new Promise((resolve, reject) => {
|
|
200
367
|
const server = http.createServer((req, res) => {
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
368
|
+
const u = new URL(req.url || '/', 'http://x');
|
|
369
|
+
if (u.pathname === '/state') {
|
|
203
370
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
204
|
-
res.end(JSON.stringify({
|
|
371
|
+
res.end(JSON.stringify({
|
|
372
|
+
erroFoto: state.erroFoto,
|
|
373
|
+
abas: state.abas,
|
|
374
|
+
frames: state.frames,
|
|
375
|
+
log: state.log.slice(-120),
|
|
376
|
+
}));
|
|
205
377
|
return;
|
|
206
378
|
}
|
|
207
|
-
if (
|
|
208
|
-
|
|
379
|
+
if (u.pathname === '/frame') {
|
|
380
|
+
// O id vem de fora: só serve o arquivo se for número — nada de
|
|
381
|
+
// path traversal por aqui.
|
|
382
|
+
const id = Math.round(Number(u.searchParams.get('n')));
|
|
383
|
+
const conhecido = Number.isFinite(id) && state.frames.some((f) => f.id === id);
|
|
384
|
+
if (!conhecido) { res.writeHead(404); res.end(); return; }
|
|
385
|
+
fs.readFile(path.join(DIR, `frame-${id}.png`), (err, buf) => {
|
|
209
386
|
if (err) { res.writeHead(404); res.end(); return; }
|
|
210
|
-
|
|
387
|
+
// Cada frame é imutável: o navegador pode guardar à vontade.
|
|
388
|
+
res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'max-age=3600' });
|
|
211
389
|
res.end(buf);
|
|
212
390
|
});
|
|
213
391
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.12.1",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|