sagadeck 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/LICENSE +21 -0
- package/README.md +305 -0
- package/SKILL.md +148 -0
- package/bin/sagadeck.js +511 -0
- package/docs/ANDAMENTO.md +1255 -0
- package/docs/EXPERIENCIAS.md +39 -0
- package/docs/PLANO-EXPERIENCIAS.md +39 -0
- package/docs/PLANO-TESTE-AULAS-TECNICAS.md +44 -0
- package/docs/PLANO-TESTE-DESINFORMACAO.md +38 -0
- package/docs/REFERENCIA.md +1131 -0
- package/package.json +59 -0
- package/src/adaptive-layouts.js +18 -0
- package/src/ai/art-direction.js +8 -0
- package/src/ai/commands.js +130 -0
- package/src/ai/context.js +245 -0
- package/src/ai/deck-ai.js +1135 -0
- package/src/ai/directions.js +74 -0
- package/src/ai/document-materials.js +49 -0
- package/src/ai/document-visuals.js +215 -0
- package/src/ai/evaluation.js +31 -0
- package/src/ai/ground.js +55 -0
- package/src/ai/llm.js +292 -0
- package/src/ai/quality.js +51 -0
- package/src/ai/relay.js +45 -0
- package/src/ai/staged-generation.js +24 -0
- package/src/ai/transform.js +858 -0
- package/src/ai/usage.js +17 -0
- package/src/ai/variety.js +80 -0
- package/src/ai/video-frames.js +24 -0
- package/src/ai/video-generation.js +126 -0
- package/src/ai/web-operation.js +31 -0
- package/src/algo-catalog.js +153 -0
- package/src/algo-trace.js +139 -0
- package/src/api-client.js +514 -0
- package/src/api-collections.js +14 -0
- package/src/api-demo.js +222 -0
- package/src/aspect.js +40 -0
- package/src/build.js +323 -0
- package/src/carousel.js +56 -0
- package/src/chrome.js +65 -0
- package/src/code-highlight.js +68 -0
- package/src/code-lab-browser.js +22 -0
- package/src/code-lab.js +3 -0
- package/src/code-language.js +52 -0
- package/src/csv.js +74 -0
- package/src/decision-lab.js +20 -0
- package/src/deck-file.js +119 -0
- package/src/diagram/napkin-examples.js +13 -0
- package/src/diagram/napkin.js +539 -0
- package/src/docx.js +128 -0
- package/src/dynamics/git-sim.js +200 -0
- package/src/dynamics/layouts.js +112 -0
- package/src/elements.js +554 -0
- package/src/experiences.js +124 -0
- package/src/exploration.js +31 -0
- package/src/export/browser.js +53 -0
- package/src/export/estudo.js +67 -0
- package/src/export/pptx.js +475 -0
- package/src/export/roteiro.js +40 -0
- package/src/export/shots.js +182 -0
- package/src/figures/charts.js +383 -0
- package/src/figures/diagrams.js +117 -0
- package/src/figures/icons.js +166 -0
- package/src/figures/material-pictos.js +20 -0
- package/src/figures/pictos.js +255 -0
- package/src/figures/points.js +106 -0
- package/src/figures/qr.js +29 -0
- package/src/figures/ufmap.js +48 -0
- package/src/fiscal/autofix.js +377 -0
- package/src/fiscal/normalize.js +115 -0
- package/src/graph-lab.js +24 -0
- package/src/hologram.js +30 -0
- package/src/identity.js +86 -0
- package/src/import/crop.js +59 -0
- package/src/import/index.js +132 -0
- package/src/import/legacy-word.js +42 -0
- package/src/import/merge.js +45 -0
- package/src/import/office.js +123 -0
- package/src/import/omml.js +69 -0
- package/src/import/pdf-render.js +56 -0
- package/src/import/pptx.js +701 -0
- package/src/import/xml.js +42 -0
- package/src/infographic.js +401 -0
- package/src/layouts.js +723 -0
- package/src/lessons.js +171 -0
- package/src/library.js +399 -0
- package/src/markup.js +116 -0
- package/src/master.js +214 -0
- package/src/mcp/server.js +371 -0
- package/src/meeting.js +42 -0
- package/src/motion.js +56 -0
- package/src/package.js +116 -0
- package/src/playground.js +19 -0
- package/src/portal-scene.js +20 -0
- package/src/preferences.js +61 -0
- package/src/protect.js +51 -0
- package/src/purpose.js +24 -0
- package/src/pytrace.js +1092 -0
- package/src/research/duckduckgo.js +73 -0
- package/src/research/research.js +202 -0
- package/src/research/svg-image.js +17 -0
- package/src/research/visuals.js +52 -0
- package/src/runtime/api-collection-ui.js +5 -0
- package/src/runtime/api-collections.js +28 -0
- package/src/runtime/api-core.js +530 -0
- package/src/runtime/api-ui.js +966 -0
- package/src/runtime/base.css +1774 -0
- package/src/runtime/calc.js +139 -0
- package/src/runtime/code-lab.js +371 -0
- package/src/runtime/decision-lab.js +49 -0
- package/src/runtime/diagram.js +341 -0
- package/src/runtime/fit.js +268 -0
- package/src/runtime/fonts/caveat.css +2 -0
- package/src/runtime/fonts/comic-neue.css +3 -0
- package/src/runtime/fonts/fredoka.css +2 -0
- package/src/runtime/fonts/index.json +10 -0
- package/src/runtime/fonts/inter.css +2 -0
- package/src/runtime/fonts/patrick-hand.css +2 -0
- package/src/runtime/fonts/plus-jakarta-sans.css +2 -0
- package/src/runtime/fonts/press-start-2p.css +2 -0
- package/src/runtime/fonts/vt323.css +2 -0
- package/src/runtime/formula.js +148 -0
- package/src/runtime/graph-lab.js +72 -0
- package/src/runtime/meeting.css +7 -0
- package/src/runtime/meeting.js +10 -0
- package/src/runtime/motion.js +15 -0
- package/src/runtime/playground.js +11 -0
- package/src/runtime/points.js +19 -0
- package/src/runtime/portal-scene.css +6 -0
- package/src/runtime/portal-scene.js +22 -0
- package/src/runtime/runtime.js +827 -0
- package/src/runtime/science.js +89 -0
- package/src/runtime/skins/aluminio.css +31 -0
- package/src/runtime/skins/arcade.css +25 -0
- package/src/runtime/skins/aurora.css +17 -0
- package/src/runtime/skins/bauhaus.css +19 -0
- package/src/runtime/skins/editorial.css +19 -0
- package/src/runtime/skins/jornal.css +17 -0
- package/src/runtime/skins/manual-noite.css +14 -0
- package/src/runtime/skins/manual.css +14 -0
- package/src/runtime/skins/noite.css +21 -0
- package/src/runtime/skins/oceano.css +17 -0
- package/src/runtime/skins/pop.css +23 -0
- package/src/runtime/skins/prata.css +18 -0
- package/src/runtime/skins/prisma.css +28 -0
- package/src/runtime/skins/rabisco.css +18 -0
- package/src/runtime/skins/relevo.css +33 -0
- package/src/runtime/skins/sinal.css +16 -0
- package/src/runtime/skins/terminal.css +23 -0
- package/src/runtime/vendor/KATEX-LICENSE.txt +21 -0
- package/src/runtime/vendor/MERMAID-LICENSE.txt +21 -0
- package/src/runtime/vendor/PLOTLY-LICENSE.txt +21 -0
- package/src/runtime/vendor/katex.css +1 -0
- package/src/runtime/vendor/mermaid.min.js +3636 -0
- package/src/runtime/vendor/plotly.min.js +3620 -0
- package/src/runtime/video-player.js +35 -0
- package/src/science.js +162 -0
- package/src/studio/ai-response.js +36 -0
- package/src/studio/api-routes.js +152 -0
- package/src/studio/assets/contexto/leia-me.md +12 -0
- package/src/studio/assets/contexto/vendas.csv +7 -0
- package/src/studio/assets/imagens/colecao-arquitetura.png +0 -0
- package/src/studio/assets/imagens/colecao-cor.png +0 -0
- package/src/studio/assets/imagens/colecao-equipe.png +0 -0
- package/src/studio/assets/imagens/lavanda-cover.jpg +0 -0
- package/src/studio/code-routes.js +32 -0
- package/src/studio/command-approval.js +48 -0
- package/src/studio/demo-decks.js +98 -0
- package/src/studio/direction-routes.js +29 -0
- package/src/studio/errors.js +65 -0
- package/src/studio/exploration-demo.js +17 -0
- package/src/studio/exporting.js +105 -0
- package/src/studio/generation-report.js +10 -0
- package/src/studio/instance.js +48 -0
- package/src/studio/layout-samples.js +152 -0
- package/src/studio/project.js +371 -0
- package/src/studio/public/api-collections.js +28 -0
- package/src/studio/public/app.js +5265 -0
- package/src/studio/public/art-preview.js +10 -0
- package/src/studio/public/calc-fields.js +11 -0
- package/src/studio/public/chat-progress.js +47 -0
- package/src/studio/public/developer-fields.js +22 -0
- package/src/studio/public/explorer.js +443 -0
- package/src/studio/public/history.js +58 -0
- package/src/studio/public/index.html +867 -0
- package/src/studio/public/inspector.js +270 -0
- package/src/studio/public/lab-fields.js +5 -0
- package/src/studio/public/library.css +275 -0
- package/src/studio/public/library.html +55 -0
- package/src/studio/public/library.js +471 -0
- package/src/studio/public/merge-decks.js +155 -0
- package/src/studio/public/review-ui.js +100 -0
- package/src/studio/public/screenshot-editor.js +140 -0
- package/src/studio/public/share-ui.js +72 -0
- package/src/studio/public/slide-clipboard.js +23 -0
- package/src/studio/public/slide-form.js +1263 -0
- package/src/studio/public/studio-awareness.js +154 -0
- package/src/studio/public/studio-next.css +303 -0
- package/src/studio/public/style.css +1262 -0
- package/src/studio/public/ui-icons.js +2 -0
- package/src/studio/public/viewers.js +372 -0
- package/src/studio/public/visual-editor.js +326 -0
- package/src/studio/public/workspace.js +18 -0
- package/src/studio/public-files.js +1 -0
- package/src/studio/request-helpers.js +41 -0
- package/src/studio/server.js +1583 -0
- package/src/studio/share-routes.js +146 -0
- package/src/studio/slide-copy-routes.js +26 -0
- package/src/studio/slide-copy.js +30 -0
- package/src/studio/snapshot.js +160 -0
- package/src/studio/style-routes.js +103 -0
- package/src/studio/template-collections.js +97 -0
- package/src/studio/versions.js +95 -0
- package/src/studio/video-download.js +29 -0
- package/src/studio/visual-keys.js +52 -0
- package/src/studio/workspaces.js +39 -0
- package/src/table.js +93 -0
- package/src/templates/scaffold.js +372 -0
- package/src/themes.js +604 -0
- package/src/trace-view.js +450 -0
- package/src/uid.js +19 -0
- package/src/video-player.js +40 -0
- package/src/visual-edits.js +99 -0
- package/src/ws.js +134 -0
- package/templates/cenario/Texto no cen/303/241rio.yaml" +171 -0
- package/templates/cenario/imagens/cidade.svg +30 -0
- package/templates/cenario/imagens/pessoa.svg +15 -0
- package/templates/cenario-e-ciencia.yaml +47 -0
- package/templates/ensaio-api.yaml +160 -0
- package/templates/exemplo-alegre.yaml +75 -0
- package/templates/exemplo-keynote.yaml +90 -0
- package/templates/exemplo-napkin.yaml +139 -0
- package/templates/exemplo.yaml +228 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
// Núcleo do slide "api" — o mesmo no navegador (runtime) e no Node (montagem e testes):
|
|
2
|
+
// caminhos em JSON ($.a.b[0]), variáveis {{nome}}, máscara de segredos e o código gerado
|
|
3
|
+
// (curl, Python, Python comentado) a partir da definição do slide. Sem dependências.
|
|
4
|
+
(function (g) {
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
// "$.a.b[0]['c d']" -> ["a", "b", 0, "c d"]
|
|
8
|
+
function parsePath(path) {
|
|
9
|
+
const s = String(path || "").trim().replace(/^\$\.?/, "");
|
|
10
|
+
const out = [];
|
|
11
|
+
const re = /\[\s*(\d+)\s*\]|\[\s*(['"])(.*?)\2\s*\]|([^.[\]]+)/g;
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(s))) out.push(m[1] != null ? Number(m[1]) : m[3] != null ? m[3] : m[4]);
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
function get(obj, path) {
|
|
17
|
+
let v = obj;
|
|
18
|
+
for (const k of parsePath(path)) {
|
|
19
|
+
if (v == null || typeof v !== "object") return undefined;
|
|
20
|
+
v = v[k];
|
|
21
|
+
}
|
|
22
|
+
return v;
|
|
23
|
+
}
|
|
24
|
+
function set(obj, path, value) {
|
|
25
|
+
const keys = parsePath(path);
|
|
26
|
+
if (!keys.length) return value;
|
|
27
|
+
let v = obj;
|
|
28
|
+
keys.forEach((k, i) => {
|
|
29
|
+
if (i === keys.length - 1) { v[k] = value; return; }
|
|
30
|
+
if (v[k] == null || typeof v[k] !== "object") v[k] = typeof keys[i + 1] === "number" ? [] : {};
|
|
31
|
+
v = v[k];
|
|
32
|
+
});
|
|
33
|
+
return obj;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// {{nome}} em qualquer texto do pedido (URL, cabeçalhos, corpo). Sem valor: fica como está.
|
|
37
|
+
const VAR = /\{\{\s*([\w.-]+)\s*\}\}/g;
|
|
38
|
+
function render(value, vars) {
|
|
39
|
+
if (typeof value === "string") return value.replace(VAR, (all, k) => (vars && vars[k] != null ? String(vars[k]) : all));
|
|
40
|
+
if (Array.isArray(value)) return value.map((x) => render(x, vars));
|
|
41
|
+
if (value && typeof value === "object" && value.__py) return value;
|
|
42
|
+
if (value && typeof value === "object") { const o = {}; for (const [k, v] of Object.entries(value)) o[render(k, vars)] = render(v, vars); return o; }
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function missing(value, vars) {
|
|
46
|
+
const out = new Set();
|
|
47
|
+
const walk = (v) => {
|
|
48
|
+
if (typeof v === "string") { let m; VAR.lastIndex = 0; while ((m = VAR.exec(v))) if (!vars || vars[m[1]] == null) out.add(m[1]); }
|
|
49
|
+
else if (Array.isArray(v)) v.forEach(walk);
|
|
50
|
+
else if (v && typeof v === "object") Object.entries(v).forEach(([k, x]) => { walk(k); walk(x); });
|
|
51
|
+
};
|
|
52
|
+
walk(value);
|
|
53
|
+
return [...out];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// dev / hom / prod pelo nome (a cor e a confirmação antes de executar em produção dependem disso)
|
|
57
|
+
function envKind(name) {
|
|
58
|
+
const n = String(name || "").toLowerCase();
|
|
59
|
+
if (/^(prod|prd|produc|production)/.test(n)) return "prod";
|
|
60
|
+
if (/^(hom|hml|homolog|stag|qa|uat|teste)/.test(n)) return "hom";
|
|
61
|
+
if (/^(dev|des|local|sandbox)/.test(n)) return "dev";
|
|
62
|
+
return "other";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// segredo na tela (projetor, gravação do workshop): só o final
|
|
66
|
+
function mask(s) {
|
|
67
|
+
s = String(s || "");
|
|
68
|
+
return s.length <= 8 ? "••••" : "••••" + s.slice(-4);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// O slide, com os padrões preenchidos. Tudo o que é específico de um serviço vem daqui.
|
|
72
|
+
function normalize(s) {
|
|
73
|
+
s = s || {};
|
|
74
|
+
const req = s.request || {};
|
|
75
|
+
const mode = ["polling", "stream", "sync", "realtime"].includes(s.mode) ? s.mode : "sync";
|
|
76
|
+
const p = s.polling || {};
|
|
77
|
+
return {
|
|
78
|
+
_normalized: true,
|
|
79
|
+
mode,
|
|
80
|
+
request: {
|
|
81
|
+
method: String(req.method || (req.body != null || req.form ? "POST" : "GET")).toUpperCase(),
|
|
82
|
+
url: String(req.url || ""),
|
|
83
|
+
headers: req.headers && typeof req.headers === "object" ? req.headers : {},
|
|
84
|
+
body: req.body,
|
|
85
|
+
form: req.form && typeof req.form === "object" ? req.form : null, // multipart: { campo: "@file" | texto }
|
|
86
|
+
auth: req.auth !== false,
|
|
87
|
+
},
|
|
88
|
+
audio: s.audio ? String(s.audio === true ? "fala.mp3" : s.audio) : null, // TTS: a resposta é áudio (nome do arquivo no código)
|
|
89
|
+
mic: !!s.mic, // STT: gravar do microfone no próprio slide
|
|
90
|
+
token: s.token || null, // este slide gera o token (ex.: Identity): caminho do token na resposta
|
|
91
|
+
file: s.file || null, // arquivo padrão, ao lado do deck (upload @file ou {{file.base64}})
|
|
92
|
+
polling: mode !== "polling" ? null : {
|
|
93
|
+
id: p.id || "$.id",
|
|
94
|
+
check: { method: String((p.check && p.check.method) || "GET").toUpperCase(), url: String((p.check && p.check.url) || ""), headers: (p.check && p.check.headers) || {}, body: p.check && p.check.body, auth: !(p.check && p.check.auth === false) },
|
|
95
|
+
status: p.status || "$.status",
|
|
96
|
+
done: [].concat(p.done || ["FINISHED", "DONE", "COMPLETED", "SUCCESS"]).map(String),
|
|
97
|
+
failed: [].concat(p.failed || ["ERROR", "FAILED", "FAILURE", "CANCELLED", "CANCELED"]).map(String),
|
|
98
|
+
interval: Number(p.interval) > 0 ? Number(p.interval) : 1,
|
|
99
|
+
timeout: Number(p.timeout) > 0 ? Number(p.timeout) : 120,
|
|
100
|
+
},
|
|
101
|
+
stream: mode !== "stream" ? null : { text: (s.stream && s.stream.text) || "$.choices[0].delta.content" },
|
|
102
|
+
realtime: mode !== "realtime" ? null : realtimeOf(s.realtime || {}, req),
|
|
103
|
+
steps: s.steps || null,
|
|
104
|
+
stepTitle: s.stepTitle || null,
|
|
105
|
+
stepText: s.stepText || null,
|
|
106
|
+
answer: s.answer || null,
|
|
107
|
+
save: s.save && typeof s.save === "object" ? s.save : {},
|
|
108
|
+
tokenVar: s.tokenVar || "API_TOKEN",
|
|
109
|
+
// aba Parâmetros: { "caminho.no.corpo": "o que faz" } — a documentação que falta
|
|
110
|
+
fields: s.fields && typeof s.fields === "object" ? s.fields : null,
|
|
111
|
+
// embeddings: gera o vetor da referência e de cada frase e compara por cosseno
|
|
112
|
+
similarity: s.similarity ? {
|
|
113
|
+
vector: s.similarity.vector || "$.data[0].embedding",
|
|
114
|
+
reference: String(s.similarity.reference || ""),
|
|
115
|
+
texts: [].concat(s.similarity.texts || []).map(String),
|
|
116
|
+
} : null,
|
|
117
|
+
// JavaScript (Node 18+, fetch nativo) só onde ele cobre: sem tempo real (WebSocket) nem similaridade
|
|
118
|
+
code: [].concat(s.code || ["curl", ...(s.realtime || s.similarity ? [] : ["javascript"]), "python", "python-comentado"]),
|
|
119
|
+
tab: s.tab || "body",
|
|
120
|
+
portal: s.portal || null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Conversa em tempo real (WebSocket): o que mandar ao conectar, como mandar áudio e texto, e como reconhecer
|
|
125
|
+
// o que chega. Os padrões seguem o formato mais comum dessas APIs; qualquer parte pode ser trocada no slide.
|
|
126
|
+
function realtimeOf(r, req) {
|
|
127
|
+
const rc = r.receive || {};
|
|
128
|
+
const one = (v, d) => (v === undefined ? d : v);
|
|
129
|
+
return {
|
|
130
|
+
url: String(r.url || req.url || ""),
|
|
131
|
+
auth: r.auth || "header", // header (Authorization: Bearer) | query:<parâmetro> | none
|
|
132
|
+
open: [].concat(r.open || []),
|
|
133
|
+
audio: {
|
|
134
|
+
rate: Number((r.audio && r.audio.rate) || 24000),
|
|
135
|
+
send: one(r.audio && r.audio.send, { type: "input_audio_buffer.append", audio: "{{audio}}" }),
|
|
136
|
+
commit: [].concat(one(r.audio && r.audio.commit, [{ type: "input_audio_buffer.commit" }, { type: "response.create" }])),
|
|
137
|
+
},
|
|
138
|
+
text: [].concat(one(r.text, [{ type: "conversation.item.create", item: { type: "message", role: "user", content: [{ type: "input_text", text: "{{text}}" }] } }, { type: "response.create" }])),
|
|
139
|
+
receive: {
|
|
140
|
+
type: rc.type || "$.type",
|
|
141
|
+
audio: rc.audio || { type: ["response.audio.delta", "response.output_audio.delta"], data: "$.delta" },
|
|
142
|
+
text: rc.text || { type: ["response.audio_transcript.delta", "response.text.delta", "response.output_text.delta", "response.output_audio_transcript.delta"], data: "$.delta" },
|
|
143
|
+
user: rc.user || { type: ["conversation.item.input_audio_transcription.completed"], data: "$.transcript" },
|
|
144
|
+
done: [].concat(rc.done || ["response.done"]),
|
|
145
|
+
error: rc.error || { type: ["error"], data: "$.error.message" },
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// o evento que chegou é deste tipo? (tipo único ou lista)
|
|
150
|
+
const isType = (ev, rule, typePath) => { if (!rule) return false; const t = get(ev, typePath); return [].concat(rule.type || rule).map(String).includes(String(t)); };
|
|
151
|
+
|
|
152
|
+
// chave estável de um slide para guardar a última resposta (modo gravado)
|
|
153
|
+
function key(s) {
|
|
154
|
+
if (s && s.id) return String(s.id);
|
|
155
|
+
const a = normalize(s);
|
|
156
|
+
const txt = [s && s.title, a.mode, a.request.method, a.request.url].join("|");
|
|
157
|
+
let h = 5381;
|
|
158
|
+
for (let i = 0; i < txt.length; i++) h = ((h << 5) + h + txt.charCodeAt(i)) >>> 0;
|
|
159
|
+
return "api-" + h.toString(36);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------- código gerado ----------
|
|
163
|
+
|
|
164
|
+
const isJsonBody = (b) => b != null && typeof b === "object";
|
|
165
|
+
const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
166
|
+
const dq = (s) => JSON.stringify(String(s));
|
|
167
|
+
|
|
168
|
+
function pyLiteral(v, ind) {
|
|
169
|
+
ind = ind || "";
|
|
170
|
+
if (v === null || v === undefined) return "None";
|
|
171
|
+
if (v === true) return "True";
|
|
172
|
+
if (v === false) return "False";
|
|
173
|
+
if (typeof v === "number") return String(v);
|
|
174
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
175
|
+
if (v.__py) return v.__py; // expressão Python crua (ex.: CONTEUDO, o arquivo em base64)
|
|
176
|
+
const next = ind + " ";
|
|
177
|
+
if (Array.isArray(v)) return v.length ? "[\n" + v.map((x) => next + pyLiteral(x, next)).join(",\n") + ",\n" + ind + "]" : "[]";
|
|
178
|
+
const ks = Object.keys(v);
|
|
179
|
+
return ks.length ? "{\n" + ks.map((k) => next + JSON.stringify(k) + ": " + pyLiteral(v[k], next)).join(",\n") + ",\n" + ind + "}" : "{}";
|
|
180
|
+
}
|
|
181
|
+
// "$.a[0].b" -> '["a"][0]["b"]'
|
|
182
|
+
const pyPath = (path) => parsePath(path).map((k) => (typeof k === "number" ? `[${k}]` : `[${JSON.stringify(k)}]`)).join("");
|
|
183
|
+
|
|
184
|
+
// Um texto de código com marcas: cada linha pode pertencer a "start", "poll" ou "done",
|
|
185
|
+
// e a apresentação acende essas linhas enquanto a etapa correspondente roda.
|
|
186
|
+
function Lines(explain, comment) {
|
|
187
|
+
const lines = [], marks = {}, cm = comment || "# ";
|
|
188
|
+
return {
|
|
189
|
+
add(text, tag, why) {
|
|
190
|
+
if (why && explain) String(why).split("\n").forEach((w) => { const m = w.match(/^(\s*)(.*)$/); this.push(m[1] + cm + m[2], tag, true); });
|
|
191
|
+
String(text).split("\n").forEach((t) => this.push(t, tag));
|
|
192
|
+
return this;
|
|
193
|
+
},
|
|
194
|
+
push(text, tag, comment) {
|
|
195
|
+
lines.push({ text, comment: !!comment });
|
|
196
|
+
if (tag) (marks[tag] = marks[tag] || []).push(lines.length);
|
|
197
|
+
return this;
|
|
198
|
+
},
|
|
199
|
+
blank() { lines.push({ text: "" }); return this; },
|
|
200
|
+
out() { return { code: lines.map((l) => l.text).join("\n"), marks, comments: lines.map((l, i) => (l.comment ? i + 1 : 0)).filter(Boolean) }; },
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function curlCmd(r, tokenVar, extra, fileName) {
|
|
205
|
+
const parts = [`curl -s${extra || ""}${r.method !== "GET" || r.body != null || r.form ? ` -X ${r.method}` : ""} ${dq(r.url)}`];
|
|
206
|
+
if (r.auth) parts.push(`-H "Authorization: Bearer $${tokenVar}"`);
|
|
207
|
+
for (const [k, v] of Object.entries(r.headers || {})) parts.push(`-H ${dq(k + ": " + v)}`);
|
|
208
|
+
if (r.form) for (const [k, v] of Object.entries(r.form)) parts.push(`-F ${dq(k + "=" + (v === "@file" ? "@" + (fileName || "arquivo") : v))}`);
|
|
209
|
+
else if (isJsonBody(r.body)) { parts.push(`-H "Content-Type: application/json"`); parts.push(`-d ${shq(JSON.stringify(r.body, null, 2))}`); }
|
|
210
|
+
else if (r.body != null) parts.push(`-d ${shq(r.body)}`);
|
|
211
|
+
return parts.join(" \\\n ");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// {{secret.nome}} no código: $NOME no curl, os.environ["NOME"] no Python (o valor nunca aparece)
|
|
215
|
+
const secretEnv = (k) => String(k).toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
216
|
+
const MARK = (k) => `@@SECRET:${secretEnv(k)}@@`;
|
|
217
|
+
const secretsIn = (v) => { const out = new Set(); JSON.stringify(v || "").replace(/\{\{\s*secret\.([\w-]+)\s*\}\}/g, (_, k) => out.add(k)); return [...out]; };
|
|
218
|
+
const fileName = (a, vars) => (vars && vars["file.name"]) || (a.file ? String(a.file).split(/[\\/]/).pop() : "arquivo");
|
|
219
|
+
const usesB64 = (a) => JSON.stringify(a.request.body || "").includes("{{file.base64}}");
|
|
220
|
+
function curl(a, vars) {
|
|
221
|
+
const L = Lines(false);
|
|
222
|
+
const name = fileName(a, vars);
|
|
223
|
+
const sec = Object.fromEntries(secretsIn(a.request).map((k) => ["secret." + k, MARK(k)]));
|
|
224
|
+
const req = render(a.request, { ...vars, ...sec, "file.name": name, "file.base64": `<${name} em base64>` });
|
|
225
|
+
if (usesB64(a)) L.add(`# o arquivo vai dentro do JSON, em base64 (gere com: base64 -w0 ${name})`, "start");
|
|
226
|
+
if (a.mode === "polling") {
|
|
227
|
+
const check = render(a.polling.check, { ...vars, id: "$ID" });
|
|
228
|
+
L.add("# 1. inicia", "start").add(curlCmd(req, a.tokenVar, "", name), "start");
|
|
229
|
+
L.add(`# → guarde ${a.polling.id.replace(/^\$\.?/, "")} da resposta em ID`, "start").blank();
|
|
230
|
+
L.add(`# 2. consulta o andamento (repita até ${a.polling.done[0]})`, "poll").add(curlCmd(check, a.tokenVar), "poll");
|
|
231
|
+
} else if (a.mode === "stream") {
|
|
232
|
+
L.add(curlCmd(req, a.tokenVar, "N", name), "start");
|
|
233
|
+
} else {
|
|
234
|
+
L.add(curlCmd(req, a.tokenVar, "", name) + (a.audio ? ` \\\n --output ${dq(a.audio)}` : ""), "start");
|
|
235
|
+
}
|
|
236
|
+
const o = L.out();
|
|
237
|
+
// dentro de '…' o shell não expande $: fecha a aspa, põe "$NOME" e reabre
|
|
238
|
+
const inSingle = (all, at) => { let n = 0; for (let i = 0; i < at; i++) if (all[i] === "'" && all[i - 1] !== "\\") n++; return n % 2 === 1; };
|
|
239
|
+
o.code = o.code.replace(/@@SECRET:([A-Z0-9_]+)@@/g, (m, k, at, all) => (inSingle(all, at) ? `'"$${k}"'` : `$${k}`));
|
|
240
|
+
return o;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function pyCall(name, r, extra, ind) {
|
|
244
|
+
ind = ind || "";
|
|
245
|
+
const args = [`${dq(r.url)}`];
|
|
246
|
+
if (r.auth || Object.keys(r.headers || {}).length) args.push("headers=HEADERS");
|
|
247
|
+
if (r.form) {
|
|
248
|
+
const files = Object.entries(r.form).filter(([, v]) => v === "@file"), data = Object.entries(r.form).filter(([, v]) => v !== "@file");
|
|
249
|
+
if (files.length) args.push(`files={${files.map(([k]) => `${JSON.stringify(k)}: open(${dq(r._fileName || "arquivo")}, "rb")`).join(", ")}}`);
|
|
250
|
+
if (data.length) args.push(`data={${data.map(([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(String(v))}`).join(", ")}}`);
|
|
251
|
+
} else if (isJsonBody(r.body)) args.push(`json=${pyLiteral(r.body, ind + " ")}`);
|
|
252
|
+
else if (r.body != null) args.push(`data=${JSON.stringify(String(r.body))}`);
|
|
253
|
+
args.push(...(extra || []));
|
|
254
|
+
return `${name} = requests.${r.method.toLowerCase()}(\n` + args.map((x) => `${ind} ${x},`).join("\n") + `\n${ind})`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function pyHeaders(L, a, r) {
|
|
258
|
+
const h = {};
|
|
259
|
+
if (r.auth) h.Authorization = "__TOKEN__";
|
|
260
|
+
Object.assign(h, r.headers || {});
|
|
261
|
+
if (!Object.keys(h).length) return;
|
|
262
|
+
const val = (v) => { const m = String(v).match(/^\{\{\s*secret\.([\w-]+)\s*\}\}$/); return m ? `os.environ[${JSON.stringify(String(m[1]).toUpperCase().replace(/[^A-Z0-9_]/g, "_"))}]` : JSON.stringify(String(v)); };
|
|
263
|
+
const body = Object.entries(h).map(([k, v]) => ` ${JSON.stringify(k)}: ${v === "__TOKEN__" ? 'f"Bearer {TOKEN}"' : val(v)},`).join("\n");
|
|
264
|
+
L.add(`HEADERS = {\n${body}\n}`, null, "cabeçalhos enviados em toda chamada");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function python(a, vars, explain) {
|
|
268
|
+
const L = Lines(explain);
|
|
269
|
+
const name = fileName(a, vars);
|
|
270
|
+
const b64 = usesB64(a);
|
|
271
|
+
const toPy = (v) => (v === "{{file.base64}}" ? { __py: "CONTEUDO" } : typeof v === "string" && /^\{\{\s*secret\.([\w-]+)\s*\}\}$/.test(v) ? { __py: `os.environ[${JSON.stringify(secretEnv(v.match(/secret\.([\w-]+)/)[1]))}]` } : Array.isArray(v) ? v.map(toPy) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, toPy(x)])) : v);
|
|
272
|
+
const req = { ...render({ ...a.request, body: toPy(a.request.body) }, { ...vars, "file.name": name }), _fileName: name };
|
|
273
|
+
const imports = ["import os"];
|
|
274
|
+
if (b64) imports.push("import base64");
|
|
275
|
+
if (a.mode === "polling") imports.push("import time");
|
|
276
|
+
if (a.mode === "stream") imports.push("import json");
|
|
277
|
+
L.add(imports.join("\n")).blank().add("import requests", null, "requests faz as chamadas HTTP (pip install requests)").blank();
|
|
278
|
+
if (req.auth || (a.polling && a.polling.check.auth)) {
|
|
279
|
+
L.add(`TOKEN = os.environ[${JSON.stringify(a.tokenVar)}]`, null, "o token de acesso vem de uma variável de ambiente, nunca escrito no código").blank();
|
|
280
|
+
}
|
|
281
|
+
pyHeaders(L, a, { ...req, auth: req.auth || !!(a.polling && a.polling.check.auth) });
|
|
282
|
+
L.blank();
|
|
283
|
+
if (b64) L.add(`CONTEUDO = base64.b64encode(open(${dq(name)}, "rb").read()).decode()`, "start", "o arquivo, em base64, vai dentro do JSON").blank();
|
|
284
|
+
|
|
285
|
+
if (a.mode === "polling") {
|
|
286
|
+
const check = render(a.polling.check, { ...vars, id: "{execucao}" });
|
|
287
|
+
L.add(pyCall("inicio", req, ["timeout=60"]), "start", "1. inicia: a resposta chega na hora, só com o código da execução");
|
|
288
|
+
L.add("inicio.raise_for_status()", "start", "para aqui se deu erro (4xx/5xx)");
|
|
289
|
+
L.add(`execucao = inicio.json()${pyPath(a.polling.id)}`, "start", "o código que identifica esta execução");
|
|
290
|
+
L.blank();
|
|
291
|
+
const url = check.url.includes("{execucao}") ? `f${dq(check.url)}` : dq(check.url);
|
|
292
|
+
L.add("while True:", "poll", "2. consulta o andamento até terminar (polling)");
|
|
293
|
+
L.add(` r = requests.${check.method.toLowerCase()}(${url}, ${check.auth || Object.keys(check.headers || {}).length ? "headers=HEADERS, " : ""}timeout=60)`, "poll");
|
|
294
|
+
L.add(" r.raise_for_status()", "poll");
|
|
295
|
+
L.add(" dados = r.json()", "poll");
|
|
296
|
+
L.add(` status = dados${pyPath(a.polling.status)}`, "poll", " o status atual da execução");
|
|
297
|
+
L.add(" print(status)", "poll");
|
|
298
|
+
L.add(` if status in (${a.polling.done.map((x) => JSON.stringify(x)).join(", ")}${a.polling.done.length === 1 ? "," : ""}):`, "poll", " terminou: sai do laço");
|
|
299
|
+
L.add(" break", "poll");
|
|
300
|
+
L.add(` if status in (${a.polling.failed.map((x) => JSON.stringify(x)).join(", ")}${a.polling.failed.length === 1 ? "," : ""}):`, "poll", " deu errado: mostra o motivo");
|
|
301
|
+
L.add(' raise RuntimeError(f"A execução falhou: {dados}")', "poll");
|
|
302
|
+
L.add(` time.sleep(${a.polling.interval})`, "poll", " espera um pouco antes de perguntar de novo");
|
|
303
|
+
L.blank();
|
|
304
|
+
if (a.steps) {
|
|
305
|
+
L.add(`for etapa in dados${pyPath(a.steps)}:`, "done", "3. o resultado de cada etapa do workflow");
|
|
306
|
+
L.add(` print(etapa${a.stepText ? pyPath(a.stepText) : ""})`, "done");
|
|
307
|
+
} else {
|
|
308
|
+
L.add(`print(dados${a.answer ? pyPath(a.answer) : ""})`, "done", "3. o resultado");
|
|
309
|
+
}
|
|
310
|
+
} else if (a.mode === "stream") {
|
|
311
|
+
L.add(pyCall("r", req, ["stream=True", "timeout=300"]).replace(/^r = /, "with ").replace(/\)$/, ") as r:"), "start", "streaming: a resposta chega aos poucos, enquanto é gerada");
|
|
312
|
+
L.add(" r.raise_for_status()", "start");
|
|
313
|
+
L.add(" for linha in r.iter_lines(decode_unicode=True):", "poll", " cada pedaço chega numa linha \"data: {...}\"");
|
|
314
|
+
L.add(' if not linha or not linha.startswith("data:"):', "poll");
|
|
315
|
+
L.add(" continue", "poll");
|
|
316
|
+
L.add(" dado = linha[5:].strip()", "poll");
|
|
317
|
+
L.add(' if dado == "[DONE]":', "poll", " o servidor avisa que terminou");
|
|
318
|
+
L.add(" break", "poll");
|
|
319
|
+
L.add(` pedaco = json.loads(dado)${pyPath(a.stream.text)}`, "poll");
|
|
320
|
+
L.add(' print(pedaco or "", end="", flush=True)', "poll", " mostra o texto conforme chega");
|
|
321
|
+
} else {
|
|
322
|
+
L.add(pyCall("resposta", req, ["timeout=60"]), "start", "a chamada: espera a resposta completa");
|
|
323
|
+
L.add("resposta.raise_for_status()", "start", "para aqui se deu erro (4xx/5xx)");
|
|
324
|
+
if (a.audio) {
|
|
325
|
+
L.add(`open(${dq(a.audio)}, "wb").write(resposta.content)`, "done", "a resposta é o áudio: salva num arquivo");
|
|
326
|
+
L.add(`print("áudio salvo em", ${dq(a.audio)})`, "done");
|
|
327
|
+
return L.out();
|
|
328
|
+
}
|
|
329
|
+
L.add("dados = resposta.json()", "done");
|
|
330
|
+
if (a.steps) {
|
|
331
|
+
L.add(`for etapa in dados${pyPath(a.steps)}:`, "done", "o resultado de cada etapa");
|
|
332
|
+
L.add(` print(etapa${a.stepText ? pyPath(a.stepText) : ""})`, "done");
|
|
333
|
+
} else {
|
|
334
|
+
L.add(`print(dados${a.answer ? pyPath(a.answer) : ""})`, "done", a.answer ? "o campo que interessa na resposta" : "a resposta inteira");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return L.out();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---------- JavaScript (Node 18+: fetch, FormData e Blob nativos; nenhuma dependência) ----------
|
|
341
|
+
function jsLiteral(v, ind) {
|
|
342
|
+
ind = ind || "";
|
|
343
|
+
if (v === null || v === undefined) return "null";
|
|
344
|
+
if (typeof v !== "object") return JSON.stringify(v);
|
|
345
|
+
if (v.__js) return v.__js; // expressão crua (ex.: CONTEUDO, process.env.SEGREDO)
|
|
346
|
+
const next = ind + " ";
|
|
347
|
+
if (Array.isArray(v)) return v.length ? "[\n" + v.map((x) => next + jsLiteral(x, next)).join(",\n") + ",\n" + ind + "]" : "[]";
|
|
348
|
+
const ks = Object.keys(v);
|
|
349
|
+
const key = (k) => (/^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k));
|
|
350
|
+
return ks.length ? "{\n" + ks.map((k) => next + key(k) + ": " + jsLiteral(v[k], next)).join(",\n") + ",\n" + ind + "}" : "{}";
|
|
351
|
+
}
|
|
352
|
+
const jsSecret = (v) => { const m = typeof v === "string" && v.match(/^\{\{\s*secret\.([\w-]+)\s*\}\}$/); return m ? { __js: `process.env.${secretEnv(m[1])}` } : null; };
|
|
353
|
+
// fetch(...) de um pedido; as linhas vão para L com a marca da etapa
|
|
354
|
+
function jsCall(L, name, r, tag, why, url) {
|
|
355
|
+
const hasHeaders = r.auth || Object.keys(r.headers || {}).length;
|
|
356
|
+
const opts = [];
|
|
357
|
+
if (r.method !== "GET") opts.push(`method: ${JSON.stringify(r.method)}`);
|
|
358
|
+
if (r.form) {
|
|
359
|
+
L.add("const form = new FormData();", tag, "multipart/form-data: campos e o arquivo");
|
|
360
|
+
for (const [k, v] of Object.entries(r.form)) {
|
|
361
|
+
if (v === "@file") L.add(`form.append(${JSON.stringify(k)}, new Blob([await readFile(${dq(r._fileName || "arquivo")})]), ${dq(r._fileName || "arquivo")});`, tag);
|
|
362
|
+
else L.add(`form.append(${JSON.stringify(k)}, ${JSON.stringify(String(v))});`, tag);
|
|
363
|
+
}
|
|
364
|
+
if (hasHeaders) opts.push("headers: HEADERS");
|
|
365
|
+
opts.push("body: form");
|
|
366
|
+
} else if (isJsonBody(r.body)) {
|
|
367
|
+
opts.push(hasHeaders ? `headers: { ...HEADERS, "Content-Type": "application/json" }` : `headers: { "Content-Type": "application/json" }`);
|
|
368
|
+
opts.push(`body: JSON.stringify(${jsLiteral(r.body, " ")})`);
|
|
369
|
+
} else {
|
|
370
|
+
if (hasHeaders) opts.push("headers: HEADERS");
|
|
371
|
+
if (r.body != null) opts.push(`body: ${JSON.stringify(String(r.body))}`);
|
|
372
|
+
}
|
|
373
|
+
const target = url || dq(r.url);
|
|
374
|
+
L.add(opts.length ? `const ${name} = await fetch(${target}, {\n${opts.map((o) => " " + o + ",").join("\n")}\n});` : `const ${name} = await fetch(${target});`, tag, why);
|
|
375
|
+
L.add(`if (!${name}.ok) throw new Error(\`HTTP \${${name}.status}: \${await ${name}.text()}\`);`, tag, "para aqui se deu erro (4xx/5xx)");
|
|
376
|
+
}
|
|
377
|
+
function javascript(a, vars, explain) {
|
|
378
|
+
const L = Lines(explain, "// ");
|
|
379
|
+
const name = fileName(a, vars);
|
|
380
|
+
const b64 = usesB64(a);
|
|
381
|
+
const toJs = (v) => (v === "{{file.base64}}" ? { __js: "CONTEUDO" } : jsSecret(v) || (Array.isArray(v) ? v.map(toJs) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, toJs(x)])) : v));
|
|
382
|
+
const req = { ...render({ ...a.request, body: toJs(a.request.body) }, { ...vars, "file.name": name }), _fileName: name };
|
|
383
|
+
const fsUse = [b64 || req.form ? "readFile" : "", a.audio ? "writeFile" : ""].filter(Boolean);
|
|
384
|
+
L.add("// Node.js 18+ · salve como exemplo.mjs e rode: node exemplo.mjs");
|
|
385
|
+
if (fsUse.length) L.add(`import { ${fsUse.join(", ")} } from "node:fs/promises";`);
|
|
386
|
+
L.blank();
|
|
387
|
+
const auth = req.auth || !!(a.polling && a.polling.check.auth);
|
|
388
|
+
if (auth) L.add(`const TOKEN = process.env.${a.tokenVar};`, null, "o token de acesso vem de uma variável de ambiente, nunca escrito no código").blank();
|
|
389
|
+
const h = [];
|
|
390
|
+
if (auth) h.push(" Authorization: `Bearer ${TOKEN}`,");
|
|
391
|
+
for (const [k, v] of Object.entries(req.headers || {})) h.push(` ${JSON.stringify(k)}: ${jsSecret(v) ? jsSecret(v).__js : JSON.stringify(String(v))},`);
|
|
392
|
+
if (h.length) L.add(`const HEADERS = {\n${h.join("\n")}\n};`, null, "cabeçalhos enviados em toda chamada").blank();
|
|
393
|
+
if (b64) L.add(`const CONTEUDO = (await readFile(${dq(name)})).toString("base64");`, "start", "o arquivo, em base64, vai dentro do JSON").blank();
|
|
394
|
+
const out = (expr, why) => {
|
|
395
|
+
if (a.steps) {
|
|
396
|
+
L.add(`for (const etapa of ${expr}${pyPath(a.steps)}) {`, "done", why || "o resultado de cada etapa");
|
|
397
|
+
L.add(` console.log(etapa${a.stepText ? pyPath(a.stepText) : ""});`, "done").add("}", "done");
|
|
398
|
+
} else L.add(`console.log(${expr}${a.answer ? pyPath(a.answer) : ""});`, "done", why || (a.answer ? "o campo que interessa na resposta" : "a resposta inteira"));
|
|
399
|
+
};
|
|
400
|
+
if (a.mode === "polling") {
|
|
401
|
+
const check = render(a.polling.check, { ...vars, id: "__EXECUCAO__" });
|
|
402
|
+
jsCall(L, "inicio", req, "start", "1. inicia: a resposta chega na hora, só com o código da execução");
|
|
403
|
+
L.add(`const execucao = (await inicio.json())${pyPath(a.polling.id)};`, "start", "o código que identifica esta execução").blank();
|
|
404
|
+
const url = check.url.includes("__EXECUCAO__") ? "`" + check.url.replace(/`/g, "\\`").replace(/__EXECUCAO__/g, "${execucao}") + "`" : dq(check.url);
|
|
405
|
+
L.add("let dados;", "poll").add("while (true) {", "poll", "2. consulta o andamento até terminar (polling)");
|
|
406
|
+
const inner = Lines(explain, "// ");
|
|
407
|
+
jsCall(inner, "r", { ...check, body: undefined }, "poll", null, url);
|
|
408
|
+
inner.out().code.split("\n").forEach((line) => L.push(" " + line, "poll", /^\s*\/\//.test(line)));
|
|
409
|
+
L.add(" dados = await r.json();", "poll");
|
|
410
|
+
L.add(` const status = dados${pyPath(a.polling.status)};`, "poll", " o status atual da execução");
|
|
411
|
+
L.add(" console.log(status);", "poll");
|
|
412
|
+
L.add(` if (${JSON.stringify(a.polling.done)}.includes(status)) break;`, "poll", " terminou: sai do laço");
|
|
413
|
+
L.add(` if (${JSON.stringify(a.polling.failed)}.includes(status)) throw new Error(\`A execução falhou: \${JSON.stringify(dados)}\`);`, "poll", " deu errado: mostra o motivo");
|
|
414
|
+
L.add(` await new Promise((ok) => setTimeout(ok, ${Math.round(a.polling.interval * 1000)}));`, "poll", " espera um pouco antes de perguntar de novo");
|
|
415
|
+
L.add("}", "poll").blank();
|
|
416
|
+
out("dados", a.steps ? "3. o resultado de cada etapa do workflow" : "3. o resultado");
|
|
417
|
+
} else if (a.mode === "stream") {
|
|
418
|
+
jsCall(L, "resposta", req, "start", "streaming: a resposta chega aos poucos, enquanto é gerada");
|
|
419
|
+
L.add("const decoder = new TextDecoder();", "poll").add('let resto = "";', "poll");
|
|
420
|
+
L.add("for await (const pedaco of resposta.body) {", "poll", "cada pedaço chega numa linha \"data: {...}\"");
|
|
421
|
+
L.add(" resto += decoder.decode(pedaco, { stream: true });", "poll");
|
|
422
|
+
L.add(" let fim;", "poll").add(' while ((fim = resto.indexOf("\\n")) >= 0) {', "poll");
|
|
423
|
+
L.add(" const linha = resto.slice(0, fim).trim();", "poll").add(" resto = resto.slice(fim + 1);", "poll");
|
|
424
|
+
L.add(' if (!linha.startsWith("data:")) continue;', "poll");
|
|
425
|
+
L.add(" const dado = linha.slice(5).trim();", "poll");
|
|
426
|
+
L.add(' if (!dado || dado === "[DONE]") continue;', "poll", " o servidor avisa que terminou");
|
|
427
|
+
L.add(` process.stdout.write(String(JSON.parse(dado)${pyPath(a.stream.text)} ?? ""));`, "poll", " mostra o texto conforme chega");
|
|
428
|
+
L.add(" }", "poll").add("}", "poll");
|
|
429
|
+
} else {
|
|
430
|
+
jsCall(L, "resposta", req, "start", "a chamada: espera a resposta completa");
|
|
431
|
+
if (a.audio) {
|
|
432
|
+
L.add(`await writeFile(${dq(a.audio)}, Buffer.from(await resposta.arrayBuffer()));`, "done", "a resposta é o áudio: salva num arquivo");
|
|
433
|
+
L.add(`console.log("áudio salvo em", ${dq(a.audio)});`, "done");
|
|
434
|
+
return L.out();
|
|
435
|
+
}
|
|
436
|
+
L.add("const dados = await resposta.json();", "done");
|
|
437
|
+
out("dados");
|
|
438
|
+
}
|
|
439
|
+
return L.out();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// similaridade por cosseno: 1 = mesma direção (mesmo sentido), 0 = nada a ver
|
|
443
|
+
function cosine(a, b) {
|
|
444
|
+
let dot = 0, na = 0, nb = 0;
|
|
445
|
+
const n = Math.min(a.length, b.length);
|
|
446
|
+
for (let i = 0; i < n; i++) { dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; }
|
|
447
|
+
return na && nb ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// código da comparação de frases: uma função embedding(), o cosseno em Python puro e o laço
|
|
451
|
+
function pythonSimilarity(a, vars, explain) {
|
|
452
|
+
const L = Lines(explain);
|
|
453
|
+
const toPy = (v) => (v === "{{text}}" ? { __py: "texto" } : Array.isArray(v) ? v.map(toPy) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, toPy(x)])) : v);
|
|
454
|
+
const req = render({ ...a.request, body: toPy(a.request.body) }, vars);
|
|
455
|
+
L.add("import math\nimport os").blank().add("import requests", null, "requests faz as chamadas HTTP (pip install requests)").blank();
|
|
456
|
+
if (req.auth) L.add(`TOKEN = os.environ[${JSON.stringify(a.tokenVar)}]`, null, "o token de acesso vem de uma variável de ambiente").blank();
|
|
457
|
+
pyHeaders(L, a, req);
|
|
458
|
+
L.blank();
|
|
459
|
+
L.add("def embedding(texto):", "start", "transforma um texto num vetor de números (o embedding)");
|
|
460
|
+
L.add(pyCall(" r", req, ["timeout=60"], " ").replace(/^ r = /, " r = "), "start");
|
|
461
|
+
L.add(" r.raise_for_status()", "start");
|
|
462
|
+
L.add(` return r.json()${pyPath(a.similarity.vector)}`, "start");
|
|
463
|
+
L.blank();
|
|
464
|
+
L.add("def cosseno(a, b):", null, "similaridade por cosseno: 1 = mesmo sentido, perto de 0 = nada a ver");
|
|
465
|
+
L.add(" produto = sum(x * y for x, y in zip(a, b))");
|
|
466
|
+
L.add(" return produto / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))");
|
|
467
|
+
L.blank();
|
|
468
|
+
L.add(`referencia = embedding(${JSON.stringify(a.similarity.reference)})`, "start", "o vetor da frase de referência");
|
|
469
|
+
L.add(`print(len(referencia), "números")`, "start");
|
|
470
|
+
L.add(`for frase in ${pyLiteral(a.similarity.texts)}:`, "poll", "compara cada frase com a referência");
|
|
471
|
+
L.add(' print(f"{cosseno(referencia, embedding(frase)):.2f} {frase}")', "poll");
|
|
472
|
+
return L.out();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function pythonRealtime(a, vars, explain) {
|
|
476
|
+
const L = Lines(explain);
|
|
477
|
+
const R = a.realtime;
|
|
478
|
+
const url = render(R.url, vars);
|
|
479
|
+
L.add("import asyncio\nimport json\nimport os").blank().add("import websockets", null, "conversa em tempo real por WebSocket (pip install websockets)").blank();
|
|
480
|
+
const q = R.auth.startsWith("query:") ? R.auth.slice(6) : null;
|
|
481
|
+
L.add(q ? `URL = f${dq(url + (url.includes("?") ? "&" : "?") + q + "={os.environ['" + a.tokenVar + "']}")}` : `URL = ${dq(url)}`);
|
|
482
|
+
if (R.auth === "header") L.add(`HEADERS = {"Authorization": f"Bearer {os.environ['${a.tokenVar}']}"}`, null, "o token vai no cabeçalho da conexão"); // aspas simples: vale para Python < 3.12
|
|
483
|
+
L.blank();
|
|
484
|
+
const textMsgs = render(R.text, { ...vars, text: "Olá! Em uma frase: o que você faz?" });
|
|
485
|
+
const textTypes = [].concat(R.receive.text.type).map((t) => JSON.stringify(t)).join(", ");
|
|
486
|
+
L.add("async def main():", "start");
|
|
487
|
+
L.add(` async with websockets.connect(URL${R.auth === "header" ? ", additional_headers=HEADERS" : ""}) as ws:`, "start", " abre a conexão (fica aberta a conversa toda)");
|
|
488
|
+
for (const m of render(R.open, vars)) L.add(` await ws.send(json.dumps(${pyLiteral(m, " ")}))`, "start", " configuração da sessão, mandada ao conectar");
|
|
489
|
+
textMsgs.forEach((m, i) => L.add(` await ws.send(json.dumps(${pyLiteral(m, " ")}))`, "start", i === 0 ? " manda uma pergunta em texto (no slide, também dá para falar)" : ""));
|
|
490
|
+
L.add(" async for mensagem in ws:", "poll", " cada evento chega como uma mensagem JSON");
|
|
491
|
+
L.add(" evento = json.loads(mensagem)", "poll");
|
|
492
|
+
L.add(` if evento${pyPath(R.receive.type)} in (${textTypes}${[].concat(R.receive.text.type).length === 1 ? "," : ""}):`, "poll", " pedaço do texto da resposta");
|
|
493
|
+
L.add(` print(evento${pyPath(R.receive.text.data)}, end="", flush=True)`, "poll");
|
|
494
|
+
L.add(` if evento${pyPath(R.receive.type)} in (${R.receive.done.map((d) => JSON.stringify(d)).join(", ")}${R.receive.done.length === 1 ? "," : ""}):`, "done", " a resposta terminou");
|
|
495
|
+
L.add(" break", "done");
|
|
496
|
+
L.blank().add("asyncio.run(main())", "start");
|
|
497
|
+
return L.out();
|
|
498
|
+
}
|
|
499
|
+
function wscat(a, vars) {
|
|
500
|
+
const L = Lines(false);
|
|
501
|
+
const R = a.realtime;
|
|
502
|
+
const url = render(R.url, vars);
|
|
503
|
+
const q = R.auth.startsWith("query:") ? R.auth.slice(6) : null;
|
|
504
|
+
L.add("# curl não fala WebSocket; o wscat (npm i -g wscat) abre a conversa no terminal", "start");
|
|
505
|
+
L.add(`wscat -c ${dq(q ? url + (url.includes("?") ? "&" : "?") + q + "=$" + a.tokenVar : url)}${R.auth === "header" ? ` \\\n -H "Authorization: Bearer $${a.tokenVar}"` : ""}`, "start");
|
|
506
|
+
L.add("# depois de conectar, cole uma mensagem por vez:", "poll");
|
|
507
|
+
for (const m of [...render(R.open, vars), ...render(R.text, { ...vars, text: "Olá!" })]) L.add(JSON.stringify(m), "poll");
|
|
508
|
+
return L.out();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const LANGS = { curl: "curl", javascript: "JavaScript", "javascript-comentado": "JavaScript comentado", python: "Python", "python-comentado": "Python comentado" };
|
|
512
|
+
function code(slide, lang, vars) {
|
|
513
|
+
const a = slide && slide._normalized ? slide : normalize(slide);
|
|
514
|
+
if (/^javascript/.test(lang || "")) {
|
|
515
|
+
// tempo real (WebSocket) e similaridade ficam no curl/Python
|
|
516
|
+
if (a.realtime || a.similarity) return { code: "// Este slide (tempo real ou similaridade) tem exemplo em curl e Python.", marks: {}, comments: [1] };
|
|
517
|
+
return javascript(a, vars || {}, lang === "javascript-comentado");
|
|
518
|
+
}
|
|
519
|
+
if (a.realtime) return lang === "curl" ? wscat(a, vars || {}) : pythonRealtime(a, vars || {}, lang === "python-comentado");
|
|
520
|
+
if (a.similarity && lang !== "curl") return pythonSimilarity(a, vars || {}, lang === "python-comentado");
|
|
521
|
+
if (a.similarity && lang === "curl") return curl(a, Object.assign({}, vars, { text: a.similarity.reference }));
|
|
522
|
+
if (lang === "curl") return curl(a, vars || {});
|
|
523
|
+
return python(a, vars || {}, lang === "python-comentado");
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// o slide mexe com arquivo? (upload @file ou {{file.…}})
|
|
527
|
+
const usesFile = (s) => { const a = s && s._normalized ? s : normalize(s); return !!a.file || a.mic || !!(a.request.form && Object.values(a.request.form).includes("@file")) || /\{\{\s*file\./.test(JSON.stringify(a.request)); };
|
|
528
|
+
const api = { isType, cosine, usesFile, parsePath, get, set, render, missing, envKind, mask, normalize, key, code, pyLiteral, pyPath, LANGS };
|
|
529
|
+
g.SagadeckApiCore = api;
|
|
530
|
+
})(typeof window !== "undefined" ? window : globalThis);
|