sagadeck 1.5.0 → 1.6.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.
@@ -5,6 +5,7 @@ import {chatErrorResult,installErrorResponses} from './errors.js';
5
5
  // sagadeck Studio · Servidor HTTP local para o editor visual PowerPoint + Chat Lateral IA
6
6
  import { slideCopyRoutes } from "./slide-copy-routes.js";
7
7
  import { publicScript } from "./public-files.js";
8
+ import { iaRoutes } from "./ia-routes.js";
8
9
  import { codeWatch } from "./code-version.js";
9
10
  import {meetingRoutes,meetingPreview} from '../meeting.js';
10
11
  import { codeRoutes } from "./code-routes.js";
@@ -40,7 +41,7 @@ import { generateForStudio } from "./generate.js";
40
41
  import { runCommand, logCommand } from "../ai/commands.js";
41
42
  import { needsApproval, approvalLabel, awaitApproval, commandEnv, estimateSubmitCost } from "./command-approval.js";
42
43
  import { demoDeck, demoAssets, demoProjectFiles } from "./demo-decks.js";
43
- import { llmAvailable, llmConfig } from "../ai/llm.js";
44
+ import { llmAvailable, llmConfig, llmConfigured } from "../ai/llm.js";
44
45
  import { editDeck, textToSlide, toYaml, materializeImages } from "../ai/deck-ai.js";
45
46
  import { transformDeck, jobStatus, sourceHash } from "../ai/transform.js";
46
47
  import { sendExport, sendHtml, lightVariant } from "./exporting.js";
@@ -892,27 +893,14 @@ export function createStudioServer(deckPath = null, opts = {}) {
892
893
  return;
893
894
  }
894
895
 
895
- // Tela de configuração da IA: é do modelrelay (vale para todos os apps). Só aparece quando ele
896
- // roda nesta máquina e tem a tela; no multiusuário quem configura é o admin, pelo portal.
897
- if (pathname === "/api/ai/setup" && req.method === "GET") {
898
- let setup = null;
899
- const base = llmConfig().url.replace(/\/v1$/, "");
900
- if (!opts.multiuser && /^http:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(base)) {
901
- try {
902
- const r = await fetch(base + "/api/console/config", { signal: AbortSignal.timeout(1500) });
903
- if (r.ok) setup = base + "/";
904
- } catch {}
905
- }
906
- res.writeHead(200, { "Content-Type": "application/json" });
907
- res.end(JSON.stringify({ url: setup }));
908
- return;
909
- }
896
+ // Configurar IA (provedor, chave, modelos desta máquina): src/studio/ia-routes.js
897
+ if (pathname.startsWith("/api/ia") && await iaRoutes({ req, res, pathname, opts, readJSON })) return;
910
898
 
911
899
  if (pathname === "/api/ai/status" && req.method === "GET") {
912
900
  const cfg = llmConfig();
913
901
  const available = await llmAvailable({ force: url.searchParams.has("refresh") });
914
902
  res.writeHead(200, { "Content-Type": "application/json" });
915
- res.end(JSON.stringify({ available, url: cfg.url, textModel: cfg.textModel, imageModel: cfg.imageModel }));
903
+ res.end(JSON.stringify({ available, configured: llmConfigured(cfg), source: cfg.source, url: cfg.url, textModel: cfg.textModel, imageModel: cfg.imageModel }));
916
904
  return;
917
905
  }
918
906
 
@@ -954,7 +942,7 @@ export function createStudioServer(deckPath = null, opts = {}) {
954
942
  }
955
943
  if (!(await llmAvailable({ force: true }))) {
956
944
  res.writeHead(503, { "Content-Type": "application/json" });
957
- res.end(JSON.stringify({ error: `Nenhum LLM respondendo em ${llmConfig().url}. Rode "modelrelay serve" ou ajuste SAGADECK_LLM_URL.` }));
945
+ res.end(JSON.stringify({ error: `${llmConfigured() ? `Nenhum LLM respondendo em ${llmConfig().url}. Confira o provedor e a chave em Configurar IA.` : "A IA não está configurada: clique em Configurar IA e informe provedor e chave."}` }));
958
946
  return;
959
947
  }
960
948
  // O deck novo vai para uma pasta própria na biblioteca (regra do CLAUDE.md), no tópico do deck aberto
@@ -11,7 +11,9 @@ export async function copySlideAssets(slide, sourceFile, targetFile) {
11
11
  async function file(ref,from=source){
12
12
  const abs=path.resolve(from,ref.split(/[?#]/)[0]);const rel=path.relative(source,abs);
13
13
  if(rel.startsWith('..')||path.isAbsolute(rel))throw Error('A mídia deve estar dentro da pasta da apresentação de origem.');
14
- const real=await fs.realpath(abs);const rr=path.relative(source,real);if(rr.startsWith('..')||path.isAbsolute(rr))throw Error('Referência de mídia fora da apresentação.');
14
+ // os dois lados pelo caminho real: a pasta aberta por junção (ou nome curto do Windows, RUNNER~1) não bate com o
15
+ // caminho resolvido do arquivo e toda mídia parecia "fora da apresentação"
16
+ const real=await fs.realpath(abs);const rr=path.relative(await fs.realpath(source),real);if(rr.startsWith('..')||path.isAbsolute(rr))throw Error('Referência de mídia fora da apresentação.');
15
17
  const out=path.join(target,prefix,rel);
16
18
  if(!copied.has(abs)){copied.add(abs);await fs.mkdir(path.dirname(out),{recursive:true});await fs.copyFile(abs,out);
17
19
  if(/\.(html?|css)$/i.test(abs)){const text=await fs.readFile(abs,'utf8');for(const m of text.matchAll(/(?:src|href)\s*=\s*["']([^"']+)["']|url\(\s*["']?([^)'"\s]+)["']?\s*\)/g)){const dep=m[1]||m[2];if(local(dep))await file(dep,path.dirname(abs));}}
package/src/ai/relay.js DELETED
@@ -1,45 +0,0 @@
1
- // Studio em Node: sobe o `modelrelay serve` junto, como o sagadeck do pip já faz (python/sagadeck/llm.py).
2
- // Só quando faz sentido: ninguém escutando na porta padrão, sem SAGADECK_LLM_URL e sem SAGADECK_NO_RELAY=1.
3
- // A configuração (provedor, modelos, chave) continua sendo do modelrelay (~/.modelrelay/config.toml).
4
- import net from "node:net";
5
- import { spawn } from "node:child_process";
6
-
7
- export const RELAY_PORT = 8765;
8
-
9
- export function portOpen(port, host = "127.0.0.1", timeoutMs = 300) {
10
- return new Promise((resolve) => {
11
- const s = net.connect({ port, host });
12
- const done = (ok) => { s.destroy(); resolve(ok); };
13
- s.setTimeout(timeoutMs, () => done(false));
14
- s.once("connect", () => done(true));
15
- s.once("error", () => done(false));
16
- });
17
- }
18
-
19
- // o comando `modelrelay` (script do pip) e, se ele não estiver no PATH, o módulo pelo Python
20
- const candidates = (port) => [
21
- ["modelrelay", ["serve", "--port", String(port)]],
22
- ["python", ["-m", "modelrelay.cli", "serve", "--port", String(port)]],
23
- ["python3", ["-m", "modelrelay.cli", "serve", "--port", String(port)]],
24
- ];
25
-
26
- export async function startRelay({ env = process.env, port = RELAY_PORT, spawnFn = spawn, timeoutMs = 8000 } = {}) {
27
- if (env.SAGADECK_LLM_URL || env.SAGADECK_NO_RELAY === "1") return null;
28
- if (await portOpen(port)) return null; // já tem um rodando: o motor acha sozinho
29
- for (const [cmd, args] of candidates(port)) {
30
- let child;
31
- try { child = spawnFn(cmd, args, { stdio: "ignore", windowsHide: true, env }); } catch { continue; }
32
- const failed = await new Promise((resolve) => {
33
- let over = false;
34
- const finish = (v) => { if (!over) { over = true; clearInterval(poll); clearTimeout(limit); resolve(v); } };
35
- child.once?.("error", () => finish(true)); // comando não existe
36
- child.once?.("exit", () => finish(true)); // existe, mas sem o modelrelay ou com config inválida
37
- const poll = setInterval(async () => { if (await portOpen(port)) finish(false); }, 150);
38
- const limit = setTimeout(() => finish(true), timeoutMs);
39
- });
40
- if (failed) { try { child.kill?.(); } catch {} continue; }
41
- const stop = () => { try { child.kill?.(); } catch {} };
42
- return { url: `http://127.0.0.1:${port}/v1`, cmd, stop };
43
- }
44
- return null;
45
- }