vibeeditor-render 0.4.0 → 0.5.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/core/ia.js CHANGED
@@ -19,7 +19,11 @@ const path = require("node:path");
19
19
  const crypto = require("node:crypto");
20
20
  const {spawn, execFile} = require("node:child_process");
21
21
 
22
- const MAX_SIMULTANEAS = 3;
22
+ // execuções ao mesmo tempo neste computador (cada uma é um `claude -p`); VIBE_IA_SIMULTANEAS muda (1 a 12)
23
+ const MAX_SIMULTANEAS = 6;
24
+ // sem notícia do site por este tempo (aba fechada ou recarregada no meio), a conversa é encerrada e libera a vaga.
25
+ // O site pergunta pelos eventos a cada ≤ 25 s e, enquanto roda uma ferramenta longa, avisa que está vivo (POST …/vivo).
26
+ const ABANDONO = 5 * 60000;
23
27
  const ESPERA_EVENTOS = 25000;
24
28
  const GUARDA_DEPOIS_DO_FIM = 10 * 60000; // o site pode reconectar e ler o fim depois
25
29
  const PREFIXO_MCP = "mcp__vibe__";
@@ -129,11 +133,14 @@ function encerrar(p) {
129
133
  }
130
134
 
131
135
  /**
132
- * opc: {dirDados, porta() (do servidor, para o MCP), env, log, spawn (injetável), acharClaude (injetável), versao}
136
+ * opc: {dirDados, porta() (do servidor, para o MCP), env, log, spawn (injetável), acharClaude (injetável), versao,
137
+ * abandono (ms, injetável nos testes)}
133
138
  */
134
139
  function criarIa(opc) {
135
140
  const env = opc.env || process.env;
136
141
  const log = opc.log || (() => {});
142
+ const maxSimultaneas = Math.min(12, Math.max(1, Math.floor(Number(env.VIBE_IA_SIMULTANEAS)) || MAX_SIMULTANEAS));
143
+ const abandono = opc.abandono || ABANDONO;
137
144
  const rodar = opc.spawn || spawn;
138
145
  const dir = path.join(opc.dirDados, "ia");
139
146
  const trabalho = path.join(dir, "trabalho"); // cwd fixo: o --resume procura a sessão pela pasta
@@ -236,8 +243,9 @@ function criarIa(opc) {
236
243
 
237
244
  async function criar(corpo) {
238
245
  const d = validarConversa(corpo);
246
+ varrer();
239
247
  const ativas = [...conversas.values()].filter((c) => !c.fim).length;
240
- if (ativas >= MAX_SIMULTANEAS) throw erro(429, `Já há ${ativas} execuções da IA neste computador. Espere uma terminar.`);
248
+ if (ativas >= maxSimultaneas) throw erro(429, `Já há ${ativas} execuções da IA neste computador (o limite é ${maxSimultaneas}). Espere uma terminar ou pare uma delas.`);
241
249
  const b = bin();
242
250
  if (!b) throw erro(409, "Não encontrei o Claude Code neste computador. Instale (code.claude.com) e faça o login com a sua conta.");
243
251
 
@@ -252,7 +260,7 @@ function criarIa(opc) {
252
260
  fs.writeFileSync(arqMcp, JSON.stringify({mcpServers: {vibe: {type: "http", url: `http://127.0.0.1:${opc.porta()}/v1/ia/mcp/${id}`, headers: {Authorization: `Bearer ${token}`}}}}));
253
261
 
254
262
  const c = {id, token, pasta, ferramentas: d.ferramentas, eventos: [], ouvintes: new Set(), pendentes: new Map(), web: new Map(),
255
- sessao: d.sessao, ultimoTexto: "", resultado: null, fim: null, proc: null, saidaErro: ""};
263
+ sessao: d.sessao, ultimoTexto: "", resultado: null, fim: null, proc: null, saidaErro: "", vistoEm: Date.now()};
256
264
  conversas.set(id, c);
257
265
 
258
266
  const p = rodar(b, argumentos({...d, arqSistema, arqMcp}), {cwd: trabalho, env: ambiente(env), windowsHide: true, stdio: ["pipe", "pipe", "pipe"]});
@@ -291,6 +299,7 @@ function criarIa(opc) {
291
299
  function eventos(id, desde, sinal) {
292
300
  const c = conversas.get(id);
293
301
  if (!c) return Promise.resolve(null);
302
+ c.vistoEm = Date.now();
294
303
  const pegar = () => ({eventos: c.eventos.slice(desde), fim: !!c.fim});
295
304
  if (c.eventos.length > desde || c.fim) return Promise.resolve(pegar());
296
305
  return new Promise((ok) => {
@@ -312,6 +321,7 @@ function criarIa(opc) {
312
321
  function resultado(id, corpo) {
313
322
  const c = conversas.get(id);
314
323
  if (!c) throw erro(404, "conversa não encontrada");
324
+ c.vistoEm = Date.now();
315
325
  const p = c.pendentes.get(String(corpo && corpo.chamada));
316
326
  if (!p) throw erro(404, "chamada não encontrada (já respondida ou a conversa terminou)");
317
327
  c.pendentes.delete(String(corpo.chamada));
@@ -319,6 +329,34 @@ function criarIa(opc) {
319
329
  return {ok: true};
320
330
  }
321
331
 
332
+ /** o site ainda está com a conversa aberta (ele chama enquanto roda uma ferramenta demorada) */
333
+ function vivo(id) {
334
+ const c = conversas.get(id);
335
+ if (!c) throw erro(404, "conversa não encontrada");
336
+ c.vistoEm = Date.now();
337
+ return {ok: true, fim: !!c.fim};
338
+ }
339
+
340
+ /**
341
+ * Conversa sem notícia do site há mais de `abandono` (a aba fechou ou recarregou no meio): o claude estaria esperando
342
+ * para sempre a resposta de uma ferramenta que ninguém vai rodar. Encerra e libera a vaga.
343
+ */
344
+ function varrer(agora = Date.now()) {
345
+ for (const c of conversas.values()) {
346
+ if (c.fim || agora - c.vistoEm <= abandono) continue;
347
+ log("ia", `conversa ${c.id.slice(0, 8)} sem o site há ${Math.round((agora - c.vistoEm) / 60000)} min: encerrada (aba fechada no meio?)`);
348
+ c.cancelado = true;
349
+ c.abandonada = true;
350
+ for (const [, p] of c.pendentes) p.ok({erro: true, conteudo: "o site fechou a conversa"});
351
+ c.pendentes.clear();
352
+ encerrar(c.proc);
353
+ // processo que não fecha (ou já tinha fechado sem avisar): a vaga não pode ficar presa
354
+ setTimeout(() => terminar(c, {motivo: "parado", sessao: c.sessao}), 15000).unref();
355
+ }
356
+ }
357
+ const relogio = setInterval(() => varrer(), 30000);
358
+ if (relogio.unref) relogio.unref();
359
+
322
360
  function cancelar(id) {
323
361
  const c = conversas.get(id);
324
362
  if (!c) throw erro(404, "conversa não encontrada");
@@ -369,7 +407,7 @@ function criarIa(opc) {
369
407
  }
370
408
  }
371
409
 
372
- return {status, criar, eventos, resultado, cancelar, mcp, cancelarTodas, _conversas: conversas};
410
+ return {status, criar, eventos, resultado, vivo, cancelar, mcp, cancelarTodas, _conversas: conversas, _varrer: varrer, maxSimultaneas};
373
411
  }
374
412
 
375
413
  module.exports = {criarIa, acharClaude, argumentos, paraMcp, validarConversa, erroDoClaude, ambiente};
package/core/servidor.js CHANGED
@@ -192,6 +192,7 @@ function criarServidor(opc) {
192
192
  }
193
193
  if (partes[4] === "resultado" && req.method === "POST") return json(200, opc.ia.resultado(id, await lerJson(req, LIMITES.jsonBytes)), cors);
194
194
  if (partes[4] === "cancelar" && req.method === "POST") return json(200, opc.ia.cancelar(id), cors);
195
+ if (partes[4] === "vivo" && req.method === "POST") return json(200, opc.ia.vivo(id), cors);
195
196
  }
196
197
  }
197
198
  if (partes[0] === "v1" && partes[1] === "jobs" && partes[2]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeeditor-render",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Vibe Render: exporta os vídeos do Editor IA (vibeeditor.com.br) no seu computador, com Remotion + Chrome Headless Shell.",
5
5
  "keywords": [
6
6
  "vibeeditor",