project-foundry 1.0.0 → 1.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/foundry.js +3 -0
- package/package.json +1 -1
- package/src/api-client.js +52 -14
- package/src/daemon.js +11 -4
- package/src/local-printer-executor.js +120 -0
- package/src/tunnel-client.js +11 -42
package/bin/foundry.js
CHANGED
|
@@ -126,6 +126,9 @@ program
|
|
|
126
126
|
} catch (err) {
|
|
127
127
|
s.stop(pc.red('Erro durante o pareamento'));
|
|
128
128
|
p.log.error(err.message || 'Falha ao registrar nó');
|
|
129
|
+
if (err.cause) {
|
|
130
|
+
p.log.error(pc.dim(`Detalhes técnicos: ${err.cause?.message || err.cause?.code || err.cause}`));
|
|
131
|
+
}
|
|
129
132
|
process.exit(1);
|
|
130
133
|
}
|
|
131
134
|
});
|
package/package.json
CHANGED
package/src/api-client.js
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
|
+
import https from 'node:https';
|
|
2
|
+
import http from 'node:http';
|
|
1
3
|
import os from 'node:os';
|
|
2
4
|
|
|
5
|
+
async function postJson(targetUrl, payload) {
|
|
6
|
+
const parsed = new URL(targetUrl);
|
|
7
|
+
const isHttps = parsed.protocol === 'https:';
|
|
8
|
+
const transport = isHttps ? https : http;
|
|
9
|
+
const postData = JSON.stringify(payload);
|
|
10
|
+
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const req = transport.request(parsed, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
family: 4,
|
|
15
|
+
rejectUnauthorized: false,
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
19
|
+
'User-Agent': 'Project-Foundry-CLI/1.0',
|
|
20
|
+
},
|
|
21
|
+
}, (res) => {
|
|
22
|
+
let body = '';
|
|
23
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
24
|
+
res.on('end', () => {
|
|
25
|
+
try {
|
|
26
|
+
const parsedBody = body ? JSON.parse(body) : {};
|
|
27
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
28
|
+
resolve(parsedBody.data);
|
|
29
|
+
} else {
|
|
30
|
+
const err = new Error(parsedBody?.error?.message || `HTTP ${res.statusCode}: ${body.slice(0, 100)}`);
|
|
31
|
+
err.code = parsedBody?.error?.code;
|
|
32
|
+
reject(err);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
reject(new Error(`Resposta inválida do servidor: HTTP ${res.statusCode}`));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
req.on('error', (err) => {
|
|
41
|
+
reject(new Error(`Falha de conexão com ${parsed.host} (${err.code || err.message})`));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
req.setTimeout(15000, () => {
|
|
45
|
+
req.destroy();
|
|
46
|
+
reject(new Error(`Tempo limite esgotado ao contatar ${parsed.host}`));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
req.write(postData);
|
|
50
|
+
req.end();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
3
54
|
export async function registerNodeWithCloud({ serverUrl, token, name }) {
|
|
4
55
|
const url = `${serverUrl.replace(/\/$/, '')}/api/v1/nodes/register`;
|
|
5
56
|
|
|
@@ -11,20 +62,7 @@ export async function registerNodeWithCloud({ serverUrl, token, name }) {
|
|
|
11
62
|
name: name || `Servidor ${os.hostname()}`,
|
|
12
63
|
};
|
|
13
64
|
|
|
14
|
-
|
|
15
|
-
method: 'POST',
|
|
16
|
-
headers: { 'Content-Type': 'application/json' },
|
|
17
|
-
body: JSON.stringify(payload),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const body = await response.json().catch(() => ({}));
|
|
21
|
-
|
|
22
|
-
if (!response.ok) {
|
|
23
|
-
const errorMsg = body?.error?.message || 'Falha ao registrar nó na nuvem';
|
|
24
|
-
throw new Error(errorMsg);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return body.data;
|
|
65
|
+
return await postJson(url, payload);
|
|
28
66
|
}
|
|
29
67
|
|
|
30
68
|
export async function fetchServerTelemetry(port = 3010) {
|
package/src/daemon.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { readConfig } from './config.js';
|
|
@@ -75,10 +76,14 @@ export function startDaemon() {
|
|
|
75
76
|
|
|
76
77
|
fileStream.on('finish', async () => {
|
|
77
78
|
try {
|
|
78
|
-
// Encaminha requisição completa com cabeçalhos para a nuvem
|
|
79
79
|
const cloudUrl = `${config.serverUrl.replace(/\/$/, '')}${url.pathname}`;
|
|
80
|
-
const
|
|
80
|
+
const isHttps = cloudUrl.startsWith('https:');
|
|
81
|
+
const transport = isHttps ? https : http;
|
|
82
|
+
|
|
83
|
+
const cloudReq = transport.request(cloudUrl, {
|
|
81
84
|
method: 'POST',
|
|
85
|
+
family: 4,
|
|
86
|
+
rejectUnauthorized: false,
|
|
82
87
|
headers: {
|
|
83
88
|
...req.headers,
|
|
84
89
|
host: new URL(config.serverUrl).host,
|
|
@@ -111,14 +116,16 @@ export function startDaemon() {
|
|
|
111
116
|
return;
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
// Proxy reverso
|
|
119
|
+
// Proxy reverso para todas as outras requisições
|
|
115
120
|
try {
|
|
116
121
|
const targetUrl = new URL(req.url, config.serverUrl);
|
|
117
122
|
const isHttps = targetUrl.protocol === 'https:';
|
|
118
|
-
const transport = isHttps ?
|
|
123
|
+
const transport = isHttps ? https : http;
|
|
119
124
|
|
|
120
125
|
const proxyReq = transport.request(targetUrl, {
|
|
121
126
|
method: req.method,
|
|
127
|
+
family: 4,
|
|
128
|
+
rejectUnauthorized: false,
|
|
122
129
|
headers: {
|
|
123
130
|
...req.headers,
|
|
124
131
|
host: targetUrl.host,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export async function executeLocalPrinterAction({ action, printerId, connectionConfig, payload }) {
|
|
2
|
+
if (!connectionConfig) {
|
|
3
|
+
throw new Error('Configuração de conexão da impressora não foi fornecida pelo servidor');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const type = connectionConfig.type;
|
|
7
|
+
const baseUrl = (connectionConfig.url || `http://${connectionConfig.host}:${connectionConfig.port || 80}`).replace(/\/$/, '');
|
|
8
|
+
const apiKey = connectionConfig.apiKey;
|
|
9
|
+
const headers = apiKey ? { 'X-Api-Key': apiKey } : {};
|
|
10
|
+
|
|
11
|
+
// 1. Verificação de Conexão
|
|
12
|
+
if (action === 'connect') {
|
|
13
|
+
if (type === 'klipper') {
|
|
14
|
+
const infoRes = await fetch(`${baseUrl}/server/info`, { headers, signal: AbortSignal.timeout(8000) });
|
|
15
|
+
if (!infoRes.ok) {
|
|
16
|
+
throw new Error(`Moonraker retornou HTTP ${infoRes.status}: ${infoRes.statusText}`);
|
|
17
|
+
}
|
|
18
|
+
const info = await infoRes.json();
|
|
19
|
+
const klippyState = info?.result?.klippy_state || 'ready';
|
|
20
|
+
|
|
21
|
+
let status = klippyState === 'ready' ? 'IDLE' : 'ERROR';
|
|
22
|
+
let message = `Klipper: ${klippyState}`;
|
|
23
|
+
let temperatures = {};
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const telRes = await fetch(
|
|
27
|
+
`${baseUrl}/printer/objects/query?print_stats&extruder&heater_bed`,
|
|
28
|
+
{ headers, signal: AbortSignal.timeout(5000) }
|
|
29
|
+
);
|
|
30
|
+
if (telRes.ok) {
|
|
31
|
+
const tel = await telRes.json();
|
|
32
|
+
const stats = tel?.result?.status?.print_stats || {};
|
|
33
|
+
const ext = tel?.result?.status?.extruder || {};
|
|
34
|
+
const bed = tel?.result?.status?.heater_bed || {};
|
|
35
|
+
const st = String(stats.state || '').toLowerCase();
|
|
36
|
+
if (st === 'printing') status = 'PRINTING';
|
|
37
|
+
else if (st === 'paused') status = 'PAUSED';
|
|
38
|
+
|
|
39
|
+
temperatures = {
|
|
40
|
+
hotend: ext.temperature,
|
|
41
|
+
hotendTarget: ext.target,
|
|
42
|
+
bed: bed.temperature,
|
|
43
|
+
bedTarget: bed.target,
|
|
44
|
+
};
|
|
45
|
+
if (stats.message) message = stats.message;
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
online: true,
|
|
51
|
+
status,
|
|
52
|
+
message,
|
|
53
|
+
temperatures,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (type === 'octoprint') {
|
|
58
|
+
const verRes = await fetch(`${baseUrl}/api/version`, { headers, signal: AbortSignal.timeout(8000) });
|
|
59
|
+
if (!verRes.ok) {
|
|
60
|
+
throw new Error(`OctoPrint retornou HTTP ${verRes.status}: ${verRes.statusText}`);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
online: true,
|
|
64
|
+
status: 'IDLE',
|
|
65
|
+
message: 'OctoPrint conectado com sucesso',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw new Error(`Protocolo de impressora "${type}" não suportado para conexão direta`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 2. Snapshot de Câmera Local
|
|
73
|
+
if (action === 'camera/snapshot') {
|
|
74
|
+
const camUrl = connectionConfig.cameraSnapshotUrl || connectionConfig.cameraUrl || `${baseUrl}/webcam/?action=snapshot`;
|
|
75
|
+
const camRes = await fetch(camUrl, { signal: AbortSignal.timeout(10000) });
|
|
76
|
+
if (!camRes.ok) {
|
|
77
|
+
throw new Error(`Câmera retornou HTTP ${camRes.status}: ${camRes.statusText}`);
|
|
78
|
+
}
|
|
79
|
+
const buf = await camRes.arrayBuffer();
|
|
80
|
+
return { base64: Buffer.from(buf).toString('base64') };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 3. Controle da Impressora (Pausa, Emergência, Motores, etc.)
|
|
84
|
+
if (action === 'control') {
|
|
85
|
+
const cmd = payload?.command;
|
|
86
|
+
if (type === 'klipper') {
|
|
87
|
+
const cmdMap = {
|
|
88
|
+
pause: '/printer/print/pause',
|
|
89
|
+
resume: '/printer/print/resume',
|
|
90
|
+
cancel: '/printer/print/cancel',
|
|
91
|
+
emergency_stop: '/printer/emergency_stop',
|
|
92
|
+
restart: '/printer/restart',
|
|
93
|
+
firmware_restart: '/printer/firmware_restart',
|
|
94
|
+
};
|
|
95
|
+
const endpoint = cmdMap[cmd];
|
|
96
|
+
if (!endpoint) throw new Error(`Comando de controle "${cmd}" não suportado em Klipper`);
|
|
97
|
+
const res = await fetch(`${baseUrl}${endpoint}`, { method: 'POST', headers, signal: AbortSignal.timeout(8000) });
|
|
98
|
+
return { success: res.ok };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 4. Comandos de Trabalho (Job Pause / Resume / Cancel)
|
|
103
|
+
if (action.startsWith('job/')) {
|
|
104
|
+
const jobCmd = action.replace('job/', '');
|
|
105
|
+
if (type === 'klipper') {
|
|
106
|
+
const cmdMap = {
|
|
107
|
+
pause: '/printer/print/pause',
|
|
108
|
+
resume: '/printer/print/resume',
|
|
109
|
+
cancel: '/printer/print/cancel',
|
|
110
|
+
};
|
|
111
|
+
const endpoint = cmdMap[jobCmd];
|
|
112
|
+
if (endpoint) {
|
|
113
|
+
const res = await fetch(`${baseUrl}${endpoint}`, { method: 'POST', headers, signal: AbortSignal.timeout(8000) });
|
|
114
|
+
return { success: res.ok };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw new Error(`Ação "${action}" não suportada`);
|
|
120
|
+
}
|
package/src/tunnel-client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
2
|
import { readConfig } from './config.js';
|
|
3
|
+
import { executeLocalPrinterAction } from './local-printer-executor.js';
|
|
3
4
|
|
|
4
5
|
export class EdgeTunnelClient {
|
|
5
6
|
constructor() {
|
|
@@ -45,6 +46,7 @@ export class EdgeTunnelClient {
|
|
|
45
46
|
try {
|
|
46
47
|
this.ws = new WebSocket(url, {
|
|
47
48
|
rejectUnauthorized: false,
|
|
49
|
+
family: 4,
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
this.ws.on('open', () => {
|
|
@@ -89,52 +91,19 @@ export class EdgeTunnelClient {
|
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
async handleCloudCommand(msg) {
|
|
92
|
-
const config = readConfig();
|
|
93
|
-
const localPort = config?.localPort || 3010;
|
|
94
|
-
const localBase = `http://127.0.0.1:${localPort}/api/v1`;
|
|
95
|
-
const headers = {
|
|
96
|
-
'Content-Type': 'application/json',
|
|
97
|
-
'X-Api-Key': config.apiKey,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
94
|
try {
|
|
101
95
|
let resultData = null;
|
|
102
96
|
|
|
103
|
-
if (msg.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
} else if (msg.action === 'control') {
|
|
111
|
-
const cRes = await fetch(`${localBase}/printers/${msg.printerId}/control`, {
|
|
112
|
-
method: 'POST',
|
|
113
|
-
headers,
|
|
114
|
-
body: JSON.stringify(msg.payload || {}),
|
|
115
|
-
});
|
|
116
|
-
const cJson = await cRes.json();
|
|
117
|
-
if (!cRes.ok) throw new Error(cJson?.error?.message || 'Falha no comando de controle');
|
|
118
|
-
resultData = cJson.data;
|
|
119
|
-
} else if (msg.action === 'connect') {
|
|
120
|
-
const connRes = await fetch(`${localBase}/printers/${msg.printerId}/connect`, {
|
|
121
|
-
method: 'POST',
|
|
122
|
-
headers,
|
|
123
|
-
body: JSON.stringify({}),
|
|
124
|
-
});
|
|
125
|
-
const connJson = await connRes.json();
|
|
126
|
-
if (!connRes.ok) throw new Error(connJson?.error?.message || 'Falha ao conectar à impressora local');
|
|
127
|
-
resultData = connJson.data;
|
|
128
|
-
} else if (msg.action.startsWith('job/')) {
|
|
129
|
-
const jobAction = msg.action.replace('job/', '');
|
|
130
|
-
const jRes = await fetch(`${localBase}/jobs/${msg.payload?.jobId}/${jobAction}`, {
|
|
131
|
-
method: 'POST',
|
|
132
|
-
headers,
|
|
133
|
-
body: JSON.stringify({}),
|
|
97
|
+
if (msg.payload?.connectionConfig) {
|
|
98
|
+
// Executa diretamente na rede local (LAN) da farm
|
|
99
|
+
resultData = await executeLocalPrinterAction({
|
|
100
|
+
action: msg.action,
|
|
101
|
+
printerId: msg.printerId,
|
|
102
|
+
connectionConfig: msg.payload.connectionConfig,
|
|
103
|
+
payload: msg.payload,
|
|
134
104
|
});
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
resultData = jJson.data;
|
|
105
|
+
} else {
|
|
106
|
+
throw new Error('Configuração de conexão da impressora não foi fornecida pelo servidor central');
|
|
138
107
|
}
|
|
139
108
|
|
|
140
109
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|