primocode 8.0.0 → 8.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/primocode.js +12 -0
- package/lib/act.js +20 -5
- package/lib/api.js +13 -1
- package/lib/catalogo.js +5 -0
- package/lib/config.js +13 -1
- package/lib/desktop.js +3 -1
- package/lib/modo.js +8 -0
- package/lib/referencia.js +8 -4
- package/lib/tools.js +20 -22
- package/package.json +1 -1
- package/studio/server.py +91 -21
- package/studio/__pycache__/server.cpython-311.pyc +0 -0
package/bin/primocode.js
CHANGED
|
@@ -24,6 +24,18 @@ const pasta = require('../lib/pasta');
|
|
|
24
24
|
const catalogo = require('../lib/catalogo');
|
|
25
25
|
const effort = require('../lib/effort');
|
|
26
26
|
|
|
27
|
+
// Rede de segurança de último recurso: sem isto, uma exceção fora de um
|
|
28
|
+
// try/catch (ex.: dentro de um callback de socket, como o parser de SSE) não
|
|
29
|
+
// era pega por ninguém e derrubava o processo INTEIRO em silêncio — o usuário
|
|
30
|
+
// via a resposta parar no meio e cair de volta pro shell, sem mensagem
|
|
31
|
+
// nenhuma. Agora vira um erro visível, e o processo continua de pé.
|
|
32
|
+
process.on('uncaughtException', (e) => {
|
|
33
|
+
console.log('\n' + c.err(ui.friendlyError(e)));
|
|
34
|
+
});
|
|
35
|
+
process.on('unhandledRejection', (e) => {
|
|
36
|
+
console.log('\n' + c.err(ui.friendlyError(e instanceof Error ? e : new Error(String(e)))));
|
|
37
|
+
});
|
|
38
|
+
|
|
27
39
|
// A pasta de onde o comando foi dado. Não é mais onde ele trabalha (o padrão é
|
|
28
40
|
// ~/primocode), mas continua servindo de ponto de partida para achar pasta que
|
|
29
41
|
// o usuário mencionar por nome relativo.
|
package/lib/act.js
CHANGED
|
@@ -165,7 +165,7 @@ function buildMemoryContext(projectDir, projectName) {
|
|
|
165
165
|
const geral = cfg.readGeneralMemory();
|
|
166
166
|
if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
|
|
167
167
|
const projeto = cfg.readProjectMemory(projectDir);
|
|
168
|
-
if (projeto.trim()) parts.push(`[${projectName}/memory.md]\n${projeto}`);
|
|
168
|
+
if (projeto.trim()) parts.push(`[${projectName || 'projeto'}/memory.md]\n${projeto}`);
|
|
169
169
|
return parts.join('\n\n');
|
|
170
170
|
}
|
|
171
171
|
|
|
@@ -299,9 +299,19 @@ async function runAct(userPrompt, opts) {
|
|
|
299
299
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
300
300
|
logResult(tc.name, result, profundidade);
|
|
301
301
|
|
|
302
|
-
// Uma escrita bem-sucedida limpa a dívida
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
// Uma escrita bem-sucedida limpa a dívida — mas só a do MESMO
|
|
303
|
+
// arquivo. Escrever algo sem relação (ou um run_shell qualquer)
|
|
304
|
+
// não corrige um teste quebrado noutro lugar; antes isto zerava
|
|
305
|
+
// a lista inteira e o agente conseguia dizer "concluído" com uma
|
|
306
|
+
// falha real ainda pendente.
|
|
307
|
+
if (result && result.ok === false) {
|
|
308
|
+
falhasPendentes.push(`${tc.name}(${tc.arguments?.path || ''}): ${result.error}`);
|
|
309
|
+
} else if (['write_file', 'edit_file'].includes(tc.name) && tc.arguments?.path) {
|
|
310
|
+
const alvo = tc.arguments.path;
|
|
311
|
+
const mantidas = falhasPendentes.filter((f) => !f.includes(`(${alvo})`));
|
|
312
|
+
falhasPendentes.length = 0;
|
|
313
|
+
falhasPendentes.push(...mantidas);
|
|
314
|
+
}
|
|
305
315
|
|
|
306
316
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
|
|
307
317
|
}
|
|
@@ -332,7 +342,12 @@ async function runAct(userPrompt, opts) {
|
|
|
332
342
|
// Nada de arquivo mexido = nada para lembrar. E grava em `pastaAtual`, não em
|
|
333
343
|
// `projectDir`: quando a pasta do projeto nasceu no meio do caminho, a memória
|
|
334
344
|
// precisa ir para ela, não para a raiz onde a sessão começou.
|
|
335
|
-
|
|
345
|
+
//
|
|
346
|
+
// `projectName` NÃO entra na condição: ele só rotula a memória no prompt
|
|
347
|
+
// (buildMemoryContext), e sessão sem projeto nomeado (ex.: primeiro pedido
|
|
348
|
+
// antes de /project) tem `projectName` undefined mesmo já tendo mexido em
|
|
349
|
+
// arquivo de verdade — a memória era descartada nesse caso, sempre.
|
|
350
|
+
if (!subagent && temPasta) {
|
|
336
351
|
cfg.appendProjectMemory(pastaAtual, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
|
|
337
352
|
}
|
|
338
353
|
|
package/lib/api.js
CHANGED
|
@@ -90,7 +90,19 @@ function requestStream(serverUrl, endpoint, payload, onEvent, token) {
|
|
|
90
90
|
if (!line.startsWith('data: ')) continue;
|
|
91
91
|
let evt;
|
|
92
92
|
try { evt = JSON.parse(line.slice(6)); } catch { continue; }
|
|
93
|
-
onEvent
|
|
93
|
+
// onEvent roda dentro de um callback de socket, fora de
|
|
94
|
+
// qualquer try/catch de quem chamou requestStream. Uma
|
|
95
|
+
// exceção aqui não seria pega por ninguém — sem process.on
|
|
96
|
+
// ('uncaughtException') no CLI, isso derrubava o processo
|
|
97
|
+
// inteiro em silêncio (era o "cai fora" sem mensagem
|
|
98
|
+
// nenhuma). Agora vira rejeição normal da Promise.
|
|
99
|
+
try {
|
|
100
|
+
onEvent(evt);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
settled = true;
|
|
103
|
+
reject(e);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
94
106
|
if (evt.done) finish();
|
|
95
107
|
}
|
|
96
108
|
});
|
package/lib/catalogo.js
CHANGED
|
@@ -91,6 +91,11 @@ const GRUPOS = [
|
|
|
91
91
|
itens: [
|
|
92
92
|
{ tool: 'write_todos', escreve: false, faz: 'monta o plano e vai marcando o que terminou' },
|
|
93
93
|
{ tool: 'pesquisar_referencia', escreve: false, faz: 'lê docs e exemplos antes de escrever — sobe a qualidade' },
|
|
94
|
+
// Vai para memory-geral.md, FORA da pasta do projeto — por isso não
|
|
95
|
+
// é write_file/edit_file (a sandbox de resolveInProject bloqueia
|
|
96
|
+
// qualquer caminho fora do projeto, e é assim mesmo que tem que
|
|
97
|
+
// ser para os outros casos). Esta tool é a única porta pra lá.
|
|
98
|
+
{ tool: 'remember_general', escreve: false, faz: 'grava algo sobre o usuário que vale para todo projeto (memory-geral.md)' },
|
|
94
99
|
// Estas duas o loop do agente atende (lib/act.js), não o tools.execute.
|
|
95
100
|
{ tool: 'spawn_agent', escreve: false, faz: 'chama um subagente para uma parte da tarefa', noLoop: true },
|
|
96
101
|
{ tool: 'finish', escreve: false, faz: 'encerra dizendo o que fez', noLoop: true },
|
package/lib/config.js
CHANGED
|
@@ -151,6 +151,18 @@ function appendProjectMemory(dir, entry) {
|
|
|
151
151
|
} catch {}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// A contraparte de appendProjectMemory, para o que vale em TODOS os projetos
|
|
155
|
+
// (nome do usuário, preferência, decisão de longo prazo). Existia a promessa
|
|
156
|
+
// no prompt — "escreve aqui o que descobrir sobre você" — mas nenhuma função
|
|
157
|
+
// gravava de fato: só ensureStructure() criava o template uma vez.
|
|
158
|
+
function appendGeneralMemory(entry) {
|
|
159
|
+
if (!entry) return;
|
|
160
|
+
try {
|
|
161
|
+
ensureStructure();
|
|
162
|
+
fs.appendFileSync(GENERAL_MEMORY, `\n### ${today()}\n${entry}\n`, 'utf-8');
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
165
|
+
|
|
154
166
|
function loadConfig() {
|
|
155
167
|
ensureStructure();
|
|
156
168
|
let file = {};
|
|
@@ -198,5 +210,5 @@ module.exports = {
|
|
|
198
210
|
CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, ROOT_DIR, GENERAL_MEMORY,
|
|
199
211
|
ensureStructure, slugify,
|
|
200
212
|
createProjectDir, getProjectDir, listProjects, findSimilarProject,
|
|
201
|
-
readGeneralMemory, readProjectMemory, appendProjectMemory,
|
|
213
|
+
readGeneralMemory, readProjectMemory, appendProjectMemory, appendGeneralMemory,
|
|
202
214
|
};
|
package/lib/desktop.js
CHANGED
|
@@ -19,7 +19,9 @@ const os = require('os');
|
|
|
19
19
|
const PLATFORM = os.platform();
|
|
20
20
|
|
|
21
21
|
function hasCmd(cmd) {
|
|
22
|
-
|
|
22
|
+
// cmd pode vir de texto do usuário (desktop_open_app com nome sem alias
|
|
23
|
+
// conhecido) — sem aspas aqui, um ";"/"&&" no meio vira comando extra.
|
|
24
|
+
try { execSync(`which ${sh1(cmd)} || where ${sh1(cmd)}`, { stdio: 'ignore' }); return true; } catch { return false; }
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
// Detectar ferramenta disponível
|
package/lib/modo.js
CHANGED
|
@@ -37,6 +37,13 @@ const VERBOS = [
|
|
|
37
37
|
];
|
|
38
38
|
const RE_VERBO = new RegExp(`(^|\\s)(${VERBOS.join('|')})\\b`, 'i');
|
|
39
39
|
|
|
40
|
+
// Pedido sem verbo de comando, só desejo: "quero um jogo da velha", "preciso
|
|
41
|
+
// de uma landing page". Muito comum em português — e sem isto caía direto no
|
|
42
|
+
// "sem pista de tarefa" porque não tem 'criar'/'fazer' escrito. A diferença
|
|
43
|
+
// para RE_CONCEITUAL é que aqui não vem pergunta ("como", "por que", "o que
|
|
44
|
+
// é") — só o pedido do que a pessoa quer que exista.
|
|
45
|
+
const RE_DESEJO = /\b(quero|queria|querendo|gostaria de|necessito de|precis[ao] de|precisava de)\b/i;
|
|
46
|
+
|
|
40
47
|
// Coisas que só existem quando há trabalho de arquivo/máquina envolvido.
|
|
41
48
|
const RE_ARTEFATO = /\.(js|ts|tsx|jsx|html|css|json|md|py|sh|yml|yaml|txt|csv|env)\b|[\w-]+\/[\w-]/i;
|
|
42
49
|
const RE_FERRAMENTA = /\b(navegador|browser|desktop|terminal|shell|git|npm|node|deploy|servidor|localhost|porta \d+|screenshot|print da tela|mouse|teclado|janela|[áa]rea de trabalho|aplicativo|[íi]cone)\b/i;
|
|
@@ -65,6 +72,7 @@ function escolherModo(texto) {
|
|
|
65
72
|
|
|
66
73
|
if (RE_ARTEFATO.test(t)) return { modo: 'primocode', motivo: 'cita arquivo ou caminho' };
|
|
67
74
|
if (RE_VERBO.test(t)) return { modo: 'primocode', motivo: 'pede uma ação' };
|
|
75
|
+
if (RE_DESEJO.test(t)) return { modo: 'primocode', motivo: 'expressa o que quer que exista' };
|
|
68
76
|
if (RE_FERRAMENTA.test(t)) return { modo: 'primocode', motivo: 'menciona ferramenta ou máquina' };
|
|
69
77
|
if (RE_PASTA.test(t)) return { modo: 'primocode', motivo: 'fala de arquivo ou projeto' };
|
|
70
78
|
|
package/lib/referencia.js
CHANGED
|
@@ -64,11 +64,15 @@ function viaProxy(alvo, { timeout }) {
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function pegar(url, { timeout = TIMEOUT_MS } = {}) {
|
|
67
|
+
function pegar(url, { timeout = TIMEOUT_MS, saltos = 0 } = {}) {
|
|
68
68
|
return new Promise((resolve) => {
|
|
69
69
|
let req;
|
|
70
70
|
const alvo = (() => { try { return new URL(url); } catch { return null; } })();
|
|
71
71
|
if (!alvo) return resolve(null);
|
|
72
|
+
// 5 saltos cobre qualquer doc real; sem teto, um redirect em loop
|
|
73
|
+
// (A→B→A→...) trava essa fonte — e a pesquisa inteira — para sempre.
|
|
74
|
+
if (saltos > 5) return resolve(null);
|
|
75
|
+
const cliente = alvo.protocol === 'http:' ? http : https;
|
|
72
76
|
|
|
73
77
|
const opcoes = {
|
|
74
78
|
headers: {
|
|
@@ -88,12 +92,12 @@ function pegar(url, { timeout = TIMEOUT_MS } = {}) {
|
|
|
88
92
|
abrir.then((cfg) => {
|
|
89
93
|
if (!cfg) return resolve(null);
|
|
90
94
|
try {
|
|
91
|
-
req =
|
|
92
|
-
// Redirecionamento: segue
|
|
95
|
+
req = cliente.get(url, cfg, (res) => {
|
|
96
|
+
// Redirecionamento: segue até o teto de saltos acima.
|
|
93
97
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
94
98
|
res.resume();
|
|
95
99
|
const destino = new URL(res.headers.location, url).href;
|
|
96
|
-
return pegar(destino, { timeout }).then(resolve);
|
|
100
|
+
return pegar(destino, { timeout, saltos: saltos + 1 }).then(resolve);
|
|
97
101
|
}
|
|
98
102
|
if (res.statusCode !== 200) { res.resume(); return resolve(null); }
|
|
99
103
|
let corpo = '';
|
package/lib/tools.js
CHANGED
|
@@ -15,6 +15,7 @@ const browser = require('./browser');
|
|
|
15
15
|
const desktop = require('./desktop');
|
|
16
16
|
const referencia = require('./referencia');
|
|
17
17
|
const studio = require('./studio');
|
|
18
|
+
const cfg = require('./config');
|
|
18
19
|
|
|
19
20
|
const IGNORAR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache', 'vendor']);
|
|
20
21
|
const MAX_ARQUIVOS_VARREDURA = 5000;
|
|
@@ -289,7 +290,7 @@ function multiEdit({ path: rawPath, edits }, projectDir) {
|
|
|
289
290
|
if (ocorrencias > 1 && !replace_all) {
|
|
290
291
|
return { ok: false, error: `edit ${i + 1}: trecho aparece ${ocorrencias}x — use replace_all ou inclua mais contexto para torná-lo único` };
|
|
291
292
|
}
|
|
292
|
-
conteudo = replace_all ? conteudo.split(old_string).join(new_string) : conteudo.replace(old_string, new_string);
|
|
293
|
+
conteudo = replace_all ? conteudo.split(old_string).join(new_string ?? '') : conteudo.replace(old_string, new_string ?? '');
|
|
293
294
|
substituicoes++;
|
|
294
295
|
}
|
|
295
296
|
|
|
@@ -399,6 +400,16 @@ async function despachar(name, args, ctx) {
|
|
|
399
400
|
paginas: Math.max(1, Math.min(5, Number(args.paginas) || 3)),
|
|
400
401
|
});
|
|
401
402
|
|
|
403
|
+
// memory-geral.md fica FORA da pasta do projeto — write_file/edit_file
|
|
404
|
+
// não alcançam por design (resolveInProject bloqueia ../ de propósito).
|
|
405
|
+
// Esta é a única porta para lá, sem reabrir a sandbox.
|
|
406
|
+
case 'remember_general': {
|
|
407
|
+
const entry = String(args.fato || args.entry || '').trim();
|
|
408
|
+
if (!entry) return { ok: false, error: 'Diga o que lembrar (fato).' };
|
|
409
|
+
cfg.appendGeneralMemory(entry);
|
|
410
|
+
return { ok: true, gravado: entry };
|
|
411
|
+
}
|
|
412
|
+
|
|
402
413
|
// ── Navegador controlado (CDP) — o agente age numa página real ──
|
|
403
414
|
case 'browser_open': {
|
|
404
415
|
const alvo = /^https?:\/\//i.test(args.url || '')
|
|
@@ -422,7 +433,10 @@ async function despachar(name, args, ctx) {
|
|
|
422
433
|
return texto == null ? { ok: false, error: 'nada na página (ou seletor não encontrado)' } : { ok: true, text: texto };
|
|
423
434
|
}
|
|
424
435
|
case 'browser_screenshot':
|
|
425
|
-
|
|
436
|
+
// Sem path, cai num nome-padrão — mas SEMPRE dentro do projeto: sem
|
|
437
|
+
// isso o arquivo ia para o cwd real do processo, não para a pasta
|
|
438
|
+
// virtual que o CLI mostra ao usuário (o processo nunca faz chdir).
|
|
439
|
+
return browser.screenshot(resolveInProject(args.path || `screenshot-${Date.now()}.png`, projectDir));
|
|
426
440
|
case 'browser_scroll':
|
|
427
441
|
return browser.scroll(args.dy != null ? args.dy : 600);
|
|
428
442
|
case 'browser_wait':
|
|
@@ -432,7 +446,9 @@ async function despachar(name, args, ctx) {
|
|
|
432
446
|
|
|
433
447
|
// ── Desktop físico — mouse, teclado e tela do usuário ───────────
|
|
434
448
|
case 'desktop_screenshot':
|
|
435
|
-
|
|
449
|
+
// Mesmo motivo do browser_screenshot: nome-padrão também precisa
|
|
450
|
+
// resolver dentro do projeto, não do cwd real do processo.
|
|
451
|
+
return desktop.screenshot(resolveInProject(args.path || `desktop-${Date.now()}.png`, projectDir));
|
|
436
452
|
case 'desktop_click':
|
|
437
453
|
return desktop.click(args.x, args.y, args.button || 'left');
|
|
438
454
|
case 'desktop_move':
|
|
@@ -484,24 +500,6 @@ async function despachar(name, args, ctx) {
|
|
|
484
500
|
}
|
|
485
501
|
}
|
|
486
502
|
|
|
487
|
-
function applyCodeBlocks(response) {
|
|
488
|
-
const regex = /```[\w]*\n([\s\S]*?)```/g;
|
|
489
|
-
const applied = [];
|
|
490
|
-
let match;
|
|
491
|
-
while ((match = regex.exec(response || '')) !== null) {
|
|
492
|
-
const block = match[1];
|
|
493
|
-
const nome = block.match(/\/\/\s*filename:\s*(.+)/);
|
|
494
|
-
if (!nome) continue;
|
|
495
|
-
const filename = nome[1].trim();
|
|
496
|
-
const content = block.replace(/\/\/\s*filename:.+\n?/, '');
|
|
497
|
-
const created = !fs.existsSync(filename);
|
|
498
|
-
fs.mkdirSync(path.dirname(filename), { recursive: true });
|
|
499
|
-
fs.writeFileSync(filename, content, 'utf-8');
|
|
500
|
-
applied.push({ path: filename, created });
|
|
501
|
-
}
|
|
502
|
-
return applied;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
503
|
async function uninstall() {
|
|
506
504
|
const configDir = path.join(os.homedir(), '.primocode');
|
|
507
505
|
console.log('Removendo PrimoCode…');
|
|
@@ -519,7 +517,7 @@ async function uninstall() {
|
|
|
519
517
|
}
|
|
520
518
|
|
|
521
519
|
module.exports = {
|
|
522
|
-
fmtBytes, treeDir,
|
|
520
|
+
fmtBytes, treeDir, runShell, openInBrowser, runInNewTerminal,
|
|
523
521
|
execute, uninstall, resolveInProject, globToRegex, globFiles, grepFiles, editFile,
|
|
524
522
|
multiEdit, writeTodos, getTodos,
|
|
525
523
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.2",
|
|
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": {
|
package/studio/server.py
CHANGED
|
@@ -9,6 +9,7 @@ Só biblioteca padrão do Python. Nada para instalar.
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import base64
|
|
12
|
+
import functools
|
|
12
13
|
import json
|
|
13
14
|
import mimetypes
|
|
14
15
|
import os
|
|
@@ -98,10 +99,17 @@ POR_SLUG = {f["slug"]: f for f in FERRAMENTAS_INFO}
|
|
|
98
99
|
|
|
99
100
|
# -------------------------------------------------------------- utilidades --
|
|
100
101
|
|
|
101
|
-
def
|
|
102
|
+
def _normaliza(texto, tamanho):
|
|
103
|
+
"""NFKD → ASCII → não-alfanumérico vira hífen → minúsculo → corta.
|
|
104
|
+
Compartilhada por slugify() e nome_seguro(), que faziam a mesma conta
|
|
105
|
+
com um corte de tamanho diferente cada uma."""
|
|
102
106
|
texto = unicodedata.normalize("NFKD", texto or "").encode("ascii", "ignore").decode()
|
|
103
107
|
texto = re.sub(r"[^a-zA-Z0-9]+", "-", texto).strip("-").lower()
|
|
104
|
-
return (texto or "arquivo")[:
|
|
108
|
+
return (texto or "arquivo")[:tamanho]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def slugify(texto):
|
|
112
|
+
return _normaliza(texto, 40)
|
|
105
113
|
|
|
106
114
|
|
|
107
115
|
def caminho(arq_id):
|
|
@@ -163,10 +171,7 @@ mimetypes.add_type("audio/mp4", ".m4a")
|
|
|
163
171
|
|
|
164
172
|
def nome_seguro(nome):
|
|
165
173
|
base, ext = os.path.splitext(nome or "arquivo")
|
|
166
|
-
|
|
167
|
-
base = unicodedata.normalize("NFKD", base).encode("ascii", "ignore").decode()
|
|
168
|
-
base = re.sub(r"[^a-zA-Z0-9]+", "-", base).strip("-").lower()[:48] or "arquivo"
|
|
169
|
-
return base, ext
|
|
174
|
+
return _normaliza(base, 48), ext.lower()
|
|
170
175
|
|
|
171
176
|
|
|
172
177
|
def listar_midia():
|
|
@@ -218,16 +223,31 @@ def guardar_midia(dados):
|
|
|
218
223
|
|
|
219
224
|
|
|
220
225
|
def criar(dados):
|
|
221
|
-
|
|
226
|
+
# O cliente é um agente de IA falando JSON cru — um tipo errado aqui
|
|
227
|
+
# (titulo como lista, por exemplo) não pode virar AttributeError sem
|
|
228
|
+
# resposta HTTP nenhuma; melhor um 400 com o motivo.
|
|
229
|
+
ferramenta_bruta = dados.get("ferramenta")
|
|
230
|
+
if ferramenta_bruta is not None and not isinstance(ferramenta_bruta, str):
|
|
231
|
+
raise ValueError("ferramenta precisa ser texto")
|
|
232
|
+
ferramenta = (ferramenta_bruta or "prisma").lower()
|
|
222
233
|
if ferramenta not in POR_SLUG:
|
|
223
234
|
return None
|
|
224
|
-
|
|
235
|
+
|
|
236
|
+
titulo_bruto = dados.get("titulo")
|
|
237
|
+
if titulo_bruto is not None and not isinstance(titulo_bruto, str):
|
|
238
|
+
raise ValueError("titulo precisa ser texto")
|
|
239
|
+
titulo = (titulo_bruto or "Sem título").strip()
|
|
240
|
+
|
|
241
|
+
doc = dados.get("doc")
|
|
242
|
+
if doc is not None and not isinstance(doc, dict):
|
|
243
|
+
raise ValueError("doc precisa ser um objeto")
|
|
244
|
+
|
|
225
245
|
arq_id = ferramenta + "-" + slugify(titulo) + "-" + uuid.uuid4().hex[:5]
|
|
226
246
|
return gravar({
|
|
227
247
|
"id": arq_id,
|
|
228
248
|
"ferramenta": ferramenta,
|
|
229
249
|
"titulo": titulo,
|
|
230
|
-
"doc":
|
|
250
|
+
"doc": doc or exemplo(ferramenta),
|
|
231
251
|
})
|
|
232
252
|
|
|
233
253
|
|
|
@@ -311,6 +331,23 @@ def manual():
|
|
|
311
331
|
|
|
312
332
|
# ----------------------------------------------------------------- handler --
|
|
313
333
|
|
|
334
|
+
def seguro(metodo):
|
|
335
|
+
"""Qualquer exceção não prevista na rota vira 500 em JSON, não conexão
|
|
336
|
+
fechada sem resposta nenhuma — era o comportamento padrão do
|
|
337
|
+
BaseHTTPRequestHandler para exceção não tratada num do_*, e o cliente (o
|
|
338
|
+
agente de IA) não tem como interpretar uma conexão que só morre."""
|
|
339
|
+
@functools.wraps(metodo)
|
|
340
|
+
def envolto(self, *a, **kw):
|
|
341
|
+
try:
|
|
342
|
+
return metodo(self, *a, **kw)
|
|
343
|
+
except Exception as e:
|
|
344
|
+
try:
|
|
345
|
+
self.js({"erro": "erro interno: %s" % e}, 500)
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
return envolto
|
|
349
|
+
|
|
350
|
+
|
|
314
351
|
class Handler(SimpleHTTPRequestHandler):
|
|
315
352
|
server_version = "PrimoCode/1.0"
|
|
316
353
|
|
|
@@ -360,6 +397,7 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
360
397
|
|
|
361
398
|
# -------------------------------------------------------------- rotas --
|
|
362
399
|
|
|
400
|
+
@seguro
|
|
363
401
|
def do_GET(self):
|
|
364
402
|
p = urlparse(self.path).path
|
|
365
403
|
|
|
@@ -426,10 +464,17 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
426
464
|
if faixa:
|
|
427
465
|
m = re.match(r"bytes=(\d*)-(\d*)", faixa)
|
|
428
466
|
if m:
|
|
429
|
-
if m.group(1):
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
fim
|
|
467
|
+
if not m.group(1) and m.group(2):
|
|
468
|
+
# "bytes=-500" é sufixo HTTP válido: os ÚLTIMOS 500 bytes,
|
|
469
|
+
# não os primeiros 500 — sem este caso à parte, group(2)
|
|
470
|
+
# virava fim absoluto e o pedido servia o pedaço errado.
|
|
471
|
+
inicio = max(0, tamanho - int(m.group(2)))
|
|
472
|
+
fim = tamanho - 1
|
|
473
|
+
else:
|
|
474
|
+
if m.group(1):
|
|
475
|
+
inicio = int(m.group(1))
|
|
476
|
+
if m.group(2):
|
|
477
|
+
fim = min(int(m.group(2)), tamanho - 1)
|
|
433
478
|
if inicio > fim or inicio >= tamanho:
|
|
434
479
|
self.send_response(416)
|
|
435
480
|
self.send_header("Content-Range", "bytes */%d" % tamanho)
|
|
@@ -455,13 +500,18 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
455
500
|
self.wfile.write(pedaco)
|
|
456
501
|
restante -= len(pedaco)
|
|
457
502
|
|
|
503
|
+
@seguro
|
|
458
504
|
def do_POST(self):
|
|
459
|
-
|
|
505
|
+
rota = urlparse(self.path).path
|
|
506
|
+
if rota == "/api/midia":
|
|
460
507
|
corpo, status = guardar_midia(self.corpo())
|
|
461
508
|
return self.js(corpo, status)
|
|
462
509
|
|
|
463
|
-
if
|
|
464
|
-
|
|
510
|
+
if rota == "/api/arquivos":
|
|
511
|
+
try:
|
|
512
|
+
a = criar(self.corpo())
|
|
513
|
+
except ValueError as e:
|
|
514
|
+
return self.js({"erro": str(e)}, 400)
|
|
465
515
|
if not a:
|
|
466
516
|
return self.js({"erro": "ferramenta inválida",
|
|
467
517
|
"validas": list(POR_SLUG)}, 400)
|
|
@@ -475,6 +525,7 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
475
525
|
}, 201)
|
|
476
526
|
return self.js({"erro": "não encontrado"}, 404)
|
|
477
527
|
|
|
528
|
+
@seguro
|
|
478
529
|
def do_PUT(self):
|
|
479
530
|
m = re.fullmatch(r"/api/arquivos/([^/]+)", urlparse(self.path).path)
|
|
480
531
|
if not m:
|
|
@@ -483,13 +534,21 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
483
534
|
if not a:
|
|
484
535
|
return self.js({"erro": "não encontrado"}, 404)
|
|
485
536
|
dados = self.corpo()
|
|
537
|
+
# Mesma validação de criar(): sem isso, um PUT com titulo/doc do tipo
|
|
538
|
+
# errado grava sujeira que só quebra depois, no consumo (export etc).
|
|
539
|
+
if "titulo" in dados and dados["titulo"] is not None and not isinstance(dados["titulo"], str):
|
|
540
|
+
return self.js({"erro": "titulo precisa ser texto"}, 400)
|
|
541
|
+
if "doc" in dados and dados["doc"] is not None and not isinstance(dados["doc"], dict):
|
|
542
|
+
return self.js({"erro": "doc precisa ser um objeto"}, 400)
|
|
486
543
|
for chave in ("titulo", "doc"):
|
|
487
544
|
if chave in dados:
|
|
488
545
|
a[chave] = dados[chave]
|
|
489
546
|
return self.js(gravar(a))
|
|
490
547
|
|
|
548
|
+
@seguro
|
|
491
549
|
def do_DELETE(self):
|
|
492
|
-
|
|
550
|
+
rota = urlparse(self.path).path
|
|
551
|
+
m = re.fullmatch(r"/api/midia/([^/]+)", rota)
|
|
493
552
|
if m:
|
|
494
553
|
nome = unquote(m.group(1))
|
|
495
554
|
alvo = os.path.join(MIDIA, nome)
|
|
@@ -498,7 +557,7 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
498
557
|
return self.js({"ok": True})
|
|
499
558
|
return self.js({"erro": "não encontrado"}, 404)
|
|
500
559
|
|
|
501
|
-
m = re.fullmatch(r"/api/arquivos/([^/]+)",
|
|
560
|
+
m = re.fullmatch(r"/api/arquivos/([^/]+)", rota)
|
|
502
561
|
c = caminho(m.group(1)) if m else None
|
|
503
562
|
if c and os.path.exists(c):
|
|
504
563
|
os.remove(c)
|
|
@@ -535,7 +594,7 @@ def csv_celula(valor):
|
|
|
535
594
|
if valor == int(valor):
|
|
536
595
|
valor = int(valor)
|
|
537
596
|
s = "" if valor is None else str(valor)
|
|
538
|
-
return '"%s"' % s.replace('"', '""') if re.search(r'[",\n]', s) else s
|
|
597
|
+
return '"%s"' % s.replace('"', '""') if re.search(r'[",\n\r]', s) else s
|
|
539
598
|
|
|
540
599
|
|
|
541
600
|
# ------------------------------------------------- fórmulas (espelha o JS) --
|
|
@@ -602,7 +661,11 @@ def calcular_aba(aba):
|
|
|
602
661
|
linhas = [list(l) for l in aba.get("linhas", [])]
|
|
603
662
|
|
|
604
663
|
def celula(c, l, prof):
|
|
605
|
-
|
|
664
|
+
# l/c negativos (referência tipo "A0", que vira índice -1) não podem
|
|
665
|
+
# passar: índice negativo do Python "funciona" contando do fim da
|
|
666
|
+
# lista, e a última linha da planilha aparecia como resposta de uma
|
|
667
|
+
# referência inválida em vez de virar 0.
|
|
668
|
+
if prof > 24 or l < 0 or c < 0 or l >= len(linhas) or c >= len(linhas[l]):
|
|
606
669
|
return 0
|
|
607
670
|
bruto = linhas[l][c]
|
|
608
671
|
if isinstance(bruto, str) and bruto.startswith("="):
|
|
@@ -642,7 +705,14 @@ def calcular_aba(aba):
|
|
|
642
705
|
elif parte:
|
|
643
706
|
# qualquer outro argumento é uma expressão: resolve antes
|
|
644
707
|
vals.append(_num(avaliar(parte, prof)))
|
|
645
|
-
|
|
708
|
+
try:
|
|
709
|
+
# MAIOR/MENOR estouram IndexError com k maior que o intervalo;
|
|
710
|
+
# POT estoura OverflowError com base/expoente grandes demais.
|
|
711
|
+
# Uma fórmula ruim vira "#ERRO" NESSA célula, não derruba a
|
|
712
|
+
# exportação inteira (mesma regra do eval() logo abaixo).
|
|
713
|
+
return repr(f(vals))
|
|
714
|
+
except Exception:
|
|
715
|
+
return "#ERRO"
|
|
646
716
|
|
|
647
717
|
# repete até esgotar: resolve de dentro para fora em fórmula aninhada
|
|
648
718
|
e, voltas = expr, 0
|
|
Binary file
|