first
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# Configurazione ambiente per s2s-reasoning-engine
|
||||
#
|
||||
# Copia questo file in .env nella root del progetto o nella cartella s2s-reasoning-engine/
|
||||
# e modifica i valori secondo il tuo deployment.
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 1) LLM (OpenAI-compatible)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
OPENAI_API_KEY=sk-....................................
|
||||
OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
OPENAI_REASONING_MODEL=gpt-4.1-mini
|
||||
OPENAI_ORGANIZATION=
|
||||
OPENAI_PROJECT=
|
||||
OPENAI_REASONING_TEMPERATURE=0.2
|
||||
OPENAI_REASONING_MAX_TOKENS=512
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 2) Aquachile (profilo agente, tool MCP, autenticazione)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
S2S_AGENT_PROFILE=https://aquachile.azurianlab.com/api/v1/agents/profile
|
||||
S2S_USER_PHONE_AUTH_URL=https://aquachile.azurianlab.com/api/v1/auth/user-by-phone
|
||||
S2S_TOOL_EXECUTION_MCP_URL=https://aquachile.azurianlab.com/api/v1/mcp
|
||||
S2S_TOOL_EXECUTION_MCP_API_KEY=....................................
|
||||
S2S_TOOL_EXECUTION_MCP_API_SECRET=....................................
|
||||
S2S_SEARCH_MCP=true
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 3) Node-RED runtime
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
NODE_RED_PORT=1881
|
||||
NODE_RED_LOG_LEVEL=info
|
||||
NODE_RED_CREDENTIAL_SECRET=change-me-in-production
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 4) Integrazione con s2s UI (server.py)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# URL raggiungibile dal container Python della UI
|
||||
# (se usi rete docker condivisa è http://s2s-reasoning:1880)
|
||||
S2S_REASONING_NODE_RED_URL=http://localhost:1881
|
||||
@@ -0,0 +1,30 @@
|
||||
# s2s-reasoning-engine: Node-RED 5.0 custom image
|
||||
|
||||
# Usa Node-RED 5.x ufficiale (immagine Alpine per dimensioni ridotte)
|
||||
FROM nodered/node-red:5.0.3
|
||||
|
||||
# Porta di default di Node-RED (esposta in reti containerizzate)
|
||||
EXPOSE 1880
|
||||
|
||||
# 1) Installa nodi aggiuntivi (a livello utente node-red nella cartella dati)
|
||||
# - node-red-contrib-openai: integrazione nativa Chat Completions / Embeddings
|
||||
# - node-red-contrib-http-logger: log HTTP opzionale
|
||||
# - node-red-contrib-cache: cache in-memory per dati Aquachile (opzionale)
|
||||
# - node-red-node-rbe: report-by-exception per deduplicare eventi
|
||||
RUN cd /data && npm install --no-audit --no-fund \
|
||||
node-red-contrib-openai@0.11.0 \
|
||||
node-red-contrib-cache@1.1.1 \
|
||||
node-red-node-rbe@0.5.0
|
||||
|
||||
# 2) Copia file di configurazione e flows iniziali
|
||||
COPY settings.js /data/settings.js
|
||||
COPY flows.json /data/flows.json
|
||||
|
||||
# 3) Imposta Node-RED per caricare flows statici + disabilita progetto e auth
|
||||
# (per ambiente di sviluppo; in produzione abilitare adminAuth in settings.js)
|
||||
ENV NODE_RED_FLOWS_FILE=/data/flows.json
|
||||
ENV NODE_RED_CREDENTIAL_SECRET=s2s-reasoning-dev-secret
|
||||
ENV FLOWS=/data/flows.json
|
||||
|
||||
# Avvio Node-RED
|
||||
CMD ["npm", "start", "--", "--userDir", "/data", "--settings", "/data/settings.js"]
|
||||
@@ -0,0 +1,254 @@
|
||||
[
|
||||
{
|
||||
"id": "s2s-reasoning-tab",
|
||||
"type": "tab",
|
||||
"label": "S2S Reasoning",
|
||||
"disabled": false,
|
||||
"info": "# Flusso di reasoning S2S — FASE 1: LOG-ONLY\n\nModalità attuale: il flusso NON produce risposte, NON modifica il prompt.\nSi limita a:\n 1. Ricevere POST /api/reasoning dal server demo\n 2. Estrarre transcript, lingua, phoneNumber\n 3. Loggare la richiesta utente su console + sidebar debug\n 4. Risondere HTTP 200 con contratto standard e flag `log_only: true`\n\nContratto risposta (fase 1):\n { answer: \"\", log_only: true, intent: \"log_only\", usage: {prompt_tokens:0,completion_tokens:0,total_tokens:0}, latency_ms: <ms> }\n",
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "http-in-reasoning",
|
||||
"type": "http in",
|
||||
"z": "s2s-reasoning-tab",
|
||||
"name": "POST /api/reasoning",
|
||||
"url": "/api/reasoning",
|
||||
"method": "post",
|
||||
"upload": false,
|
||||
"swaggerDoc": "",
|
||||
"x": 160,
|
||||
"y": 160,
|
||||
"wires": [
|
||||
[
|
||||
"parse-request"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "parse-request",
|
||||
"type": "function",
|
||||
"z": "s2s-reasoning-tab",
|
||||
"name": "Parse richiesta (log-only)",
|
||||
"func": "// FASE 1 — LOG-ONLY: nessuna answer generata.\n// Valida e normalizza il payload in arrivo da server.py.\n// msg.req.body = { transcript, instructions, language, user_context, phoneNumber? }\n\nconst body = msg.req?.body || {};\nconst startedAt = Date.now();\n\nconst transcript = String(body.transcript || \"\").trim();\nconst instructions = String(body.instructions || \"\").trim();\nconst language = String(body.language || \"\").trim();\nconst userContext = String(body.user_context || body.user_context || \"\").trim();\nconst phoneNumber = String(body.phoneNumber || body.phone_number || msg.req?.query?.phoneNumber || \"\").trim();\n\nif (!transcript) {\n msg.statusCode = 400;\n msg.payload = { error: \"missing_transcript\", detail: \"transcript field is required\" };\n return [null, msg]; // uscita 2 = errore\n}\n\nconst detectedLang = language || (transcript.match(/[áéíóúñ¿¡]/i) ? \"es-CL\" : \"en-US\");\n\nmsg.state = {\n startedAt,\n transcript,\n instructionsLen: instructions.length,\n language: detectedLang,\n userContextLen: userContext.length,\n phoneNumber: phoneNumber || null,\n intent: \"log_only\",\n};\n\nmsg.payload = {\n transcript,\n language: detectedLang,\n phone: phoneNumber || \"none\",\n};\n\nreturn [msg, null];",
|
||||
"outputs": 2,
|
||||
"timeout": 0,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 400,
|
||||
"y": 160,
|
||||
"wires": [
|
||||
[
|
||||
"log-transcript-console",
|
||||
"debug-parsed"
|
||||
],
|
||||
[
|
||||
"http-response-error"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "log-transcript-console",
|
||||
"type": "function",
|
||||
"z": "s2s-reasoning-tab",
|
||||
"name": "[LOG] richiesta utente",
|
||||
"func": "// FASE 1 — Stampa la richiesta utente sul log server (Node-RED console)\n// e sul pannello debug sidebar.\n// NON produce alcuna risposta: quella arriva dal backend S2S nativo.\n\nconst state = msg.state || {};\nconst transcript = state.transcript || \"\";\nconst lang = state.language || \"\";\nconst phone = state.phoneNumber || \"none\";\nconst ts = new Date(state.startedAt || Date.now()).toISOString();\n\nconst line = `[S2S NODE_RED LOG_ONLY] ts=${ts} lang=${lang} phone=${phone} transcript=\"${transcript.replace(/\"/g, '\\\\\"')}\"`;\n\nnode.warn(line);\n\nmsg.logLine = line;\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"timeout": 0,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 660,
|
||||
"y": 120,
|
||||
"wires": [
|
||||
[
|
||||
"build-response"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "debug-parsed",
|
||||
"type": "debug",
|
||||
"z": "s2s-reasoning-tab",
|
||||
"name": "[Debug] parsed state",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": true,
|
||||
"tostatus": false,
|
||||
"complete": "state",
|
||||
"targetType": "msg",
|
||||
"status": "",
|
||||
"x": 650,
|
||||
"y": 200,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "build-response",
|
||||
"type": "function",
|
||||
"z": "s2s-reasoning-tab",
|
||||
"name": "Build 200 (log_only)",
|
||||
"func": "// FASE 1 — Costruisci la risposta HTTP 200.\n// Contratto: answer VUOTA + flag log_only=true.\n// Il client (main.js) vedrà log_only e NON interverrà:\n// - NON chiama cancelActiveResponse()\n// - NON usa appendAssistantText()\n// - NON usa speechSynthesis\n// => la risposta vocale arriva dal backend S2S nativo.\n\nconst state = msg.state || {};\nconst latencyMs = Math.max(0, Date.now() - (state.startedAt || Date.now()));\n\nmsg.statusCode = 200;\nmsg.headers = {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n};\nmsg.payload = {\n answer: \"\",\n log_only: true,\n intent: \"log_only\",\n usage: {\n prompt_tokens: 0,\n completion_tokens: 0,\n total_tokens: 0,\n },\n latency_ms: latencyMs,\n mcp_tools_used: [],\n caller_profile_keys: [],\n debug: {\n phase: \"log_only_fase1\",\n language: state.language || \"\",\n phone: state.phoneNumber || null,\n transcript_len: (state.transcript || \"\").length,\n },\n};\n\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"timeout": 0,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 920,
|
||||
"y": 160,
|
||||
"wires": [
|
||||
[
|
||||
"http-response-ok",
|
||||
"debug-response"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "http-response-ok",
|
||||
"type": "http response",
|
||||
"z": "s2s-reasoning-tab",
|
||||
"name": "HTTP 200 OK",
|
||||
"statusCode": "",
|
||||
"headers": {},
|
||||
"x": 1160,
|
||||
"y": 120,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "debug-response",
|
||||
"type": "debug",
|
||||
"z": "s2s-reasoning-tab",
|
||||
"name": "[Debug] response payload",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "payload",
|
||||
"targetType": "msg",
|
||||
"status": "",
|
||||
"x": 1180,
|
||||
"y": 200,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "http-response-error",
|
||||
"type": "http response",
|
||||
"z": "s2s-reasoning-tab",
|
||||
"name": "HTTP Error",
|
||||
"statusCode": "",
|
||||
"headers": {},
|
||||
"x": 400,
|
||||
"y": 240,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "http-in-log-response",
|
||||
"type": "http in",
|
||||
"z": "s2s-reasoning-tab",
|
||||
"name": "POST /api/log/system-response",
|
||||
"url": "/api/log/system-response",
|
||||
"method": "post",
|
||||
"upload": false,
|
||||
"swaggerDoc": "",
|
||||
"x": 160,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"parse-log-response"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "parse-log-response",
|
||||
"type": "function",
|
||||
"z": "s2s-reasoning-tab",
|
||||
"name": "Parse system-response",
|
||||
"func": "// Riceve dal server.py la risposta TTS emessa dal backend S2S nativo.\n// msg.req.body = { tts, response_id?, item_id?, client_ip?, role?, source_event?, user_transcript?, phone? }\n// FASE 1 LOG-ONLY: nessun side-effect oltre al log.\n\nconst body = msg.req?.body || {};\nconst startedAt = Date.now();\n\nconst tts = String(body.tts || body.text || body.response || \"\").trim();\nconst responseId = String(body.response_id || body.responseId || \"\").trim() || null;\nconst itemId = String(body.item_id || body.itemId || \"\").trim() || null;\nconst clientIp = String(body.client_ip || body.clientIp || body.ip || \"\").trim() || null;\nconst role = String(body.role || \"assistant\").trim() || \"assistant\";\nconst sourceEvent = String(body.source_event || body.sourceEvent || \"\").trim() || null;\nconst userTranscript = String(body.user_transcript || body.userTranscript || \"\").trim() || null;\nconst phone = String(body.phone || body.phoneNumber || \"\").trim() || null;\n\nif (!tts) {\n msg.statusCode = 400;\n msg.headers = { \"Content-Type\": \"application/json\" };\n msg.payload = { error: \"missing_tts\", detail: \"field 'tts' (or 'text'/'response') is required\" };\n return [null, msg];\n}\n\nmsg.state = {\n startedAt,\n kind: \"system_response_log\",\n role,\n tts,\n responseId,\n itemId,\n clientIp,\n sourceEvent,\n userTranscript,\n phone,\n};\n\nmsg.payload = { tts, role, source: sourceEvent, client: clientIp };\nreturn [msg, null];",
|
||||
"outputs": 2,
|
||||
"timeout": 0,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 420,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"log-system-response",
|
||||
"debug-system-response"
|
||||
],
|
||||
[
|
||||
"http-response-error"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "log-system-response",
|
||||
"type": "function",
|
||||
"z": "s2s-reasoning-tab",
|
||||
"name": "[LOG] risposta sistema",
|
||||
"func": "// Stampa la risposta del sistema (TTS dal backend nativo) sul log Node-RED.\n// Formato riga unica ricercabile con grep.\nconst state = msg.state || {};\nconst tts = state.tts || \"\";\nconst role = state.role || \"assistant\";\nconst src = state.sourceEvent || \"\";\nconst user = state.userTranscript || \"\";\nconst phone = state.phone || \"none\";\nconst ts = new Date(state.startedAt || Date.now()).toISOString();\n\nconst userShort = user.length > 120 ? user.slice(0, 117) + \"...\" : user;\nconst ttsShort = tts.length > 240 ? tts.slice(0, 237) + \"...\" : tts;\nconst esc = (s) => String(s || \"\").replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n\nconst line = `[S2S NODE_RED SYSTEM_RESPONSE] ts=${ts} role=${role} phone=${esc(phone)} user=\"${esc(userShort)}\" tts=\"${esc(ttsShort)}\"`;\n\nnode.warn(line);\nmsg.logLine = line;\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"timeout": 0,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 690,
|
||||
"y": 320,
|
||||
"wires": [
|
||||
[
|
||||
"build-log-ack"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "debug-system-response",
|
||||
"type": "debug",
|
||||
"z": "s2s-reasoning-tab",
|
||||
"name": "[Debug] system-response state",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": true,
|
||||
"tostatus": false,
|
||||
"complete": "state",
|
||||
"targetType": "msg",
|
||||
"status": "",
|
||||
"x": 710,
|
||||
"y": 400,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "build-log-ack",
|
||||
"type": "function",
|
||||
"z": "s2s-reasoning-tab",
|
||||
"name": "Build 200 (ack)",
|
||||
"func": "// Risposta minimale: 200 OK + ricevuto=true.\n// Non ritorna dati.\nconst state = msg.state || {};\nconst latencyMs = Math.max(0, Date.now() - (state.startedAt || Date.now()));\nmsg.statusCode = 200;\nmsg.headers = {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n};\nmsg.payload = {\n ricevuto: true,\n log_only: true,\n intent: \"system_response_log\",\n latency_ms: latencyMs,\n};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"timeout": 0,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 950,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"http-response-log-ack"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "http-response-log-ack",
|
||||
"type": "http response",
|
||||
"z": "s2s-reasoning-tab",
|
||||
"name": "HTTP 200 ACK",
|
||||
"statusCode": "",
|
||||
"headers": {},
|
||||
"x": 1180,
|
||||
"y": 360,
|
||||
"wires": []
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "s2s-reasoning-engine",
|
||||
"version": "1.0.0",
|
||||
"description": "Node-RED 5.0 reasoning engine per S2S/Aquachile",
|
||||
"main": "settings.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"private": "true",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.7",
|
||||
"node-red": "^5.0.3",
|
||||
"node-red-contrib-cache": "^2.0.3",
|
||||
"node-red-contrib-openai": "^1.0.3",
|
||||
"node-red-node-rbe": "^0.5.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Node-RED 5.0 settings for s2s-reasoning-engine — LOCAL INSTALL (non Docker).
|
||||
* File: <project_root>/s2s-reasoning-engine/.node-red/settings.js
|
||||
* oppure <project_root>/s2s-reasoning-engine/settings.js (master)
|
||||
*
|
||||
* Copiato automaticamente in .node-red/ da start.sh se mancante o più vecchio.
|
||||
*/
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
// userDir è sempre <s2s-reasoning-engine>/.node-red (lo script start.sh)
|
||||
const ENGINE_ROOT = path.resolve(__dirname, "..");
|
||||
const LOCAL_NODE_MODULES = path.join(ENGINE_ROOT, "node_modules");
|
||||
const USERDIR_NODE_MODULES = path.join(__dirname, "node_modules");
|
||||
|
||||
// ── Fix per editor "tslib.es6.js 404 / net::ERR_ABORTED" ─────────────────
|
||||
// Installazioni npm locali flat (default npm 3+) spostano tslib in
|
||||
// node_modules/tslib/ (root) invece che dentro
|
||||
// @node-red/editor-client/node_modules/tslib/. L'editor di Node-RED 5
|
||||
// carica `/tslib.es6.js` dalla root HTTP: se la cartella nested non esiste,
|
||||
// torna 404. Qui aggiorniamo editor-client/public/ con i file tslib mancanti
|
||||
// a ogni avvio (sopravvive a npm reinstall se tslib è in root).
|
||||
(function _installTslibVendor() {
|
||||
try {
|
||||
const tslibRoot = path.join(LOCAL_NODE_MODULES, "tslib");
|
||||
if (!fs.existsSync(path.join(tslibRoot, "tslib.es6.js"))) return;
|
||||
const pub = path.join(LOCAL_NODE_MODULES, "@node-red", "editor-client", "public");
|
||||
if (!fs.existsSync(pub)) return;
|
||||
for (const f of ["tslib.es6.js", "tslib.js", "tslib.mjs"]) {
|
||||
const src = path.join(tslibRoot, f);
|
||||
const dst = path.join(pub, f);
|
||||
if (!fs.existsSync(src)) continue;
|
||||
try {
|
||||
const s = fs.statSync(src);
|
||||
const d = fs.existsSync(dst) ? fs.statSync(dst) : null;
|
||||
if (!d || d.size !== s.size || d.mtimeMs < s.mtimeMs) {
|
||||
fs.copyFileSync(src, dst);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
} catch (e) { /* no-op */ }
|
||||
})();
|
||||
|
||||
module.exports = {
|
||||
uiPort: process.env.PORT || process.env.NODE_RED_PORT || 1881,
|
||||
uiHost: "0.0.0.0",
|
||||
|
||||
httpRequestTimeout: 120000,
|
||||
httpRequestLegacyMsgOverrides: true,
|
||||
|
||||
// ── Native HTTP helper per function node (SENZA externalModules) ─────
|
||||
// NOTA: non usare functionExternalModules: true — in Node-RED v5 quel
|
||||
// flag può bloccare il caricamento palette (es. "37/41 nodes loading"
|
||||
// durante la verifica di tutti i pkg npm).
|
||||
// Al suo posto usiamo functionGlobalContext: i moduli core Node.js sono
|
||||
// require'd QUI nel settings.js (prima dell'avvio NR) e poi i function
|
||||
// node li recuperano via global.get("_core_https") ecc.
|
||||
functionModuleRequireAllowList: ["https","http","url","path","util","fs","os","buffer","zlib","stream","crypto"],
|
||||
functionGlobalContext: {
|
||||
_core_https: require("https"),
|
||||
_core_http: require("http"),
|
||||
_core_url: require("url"),
|
||||
},
|
||||
|
||||
// adminAuth: {...} ← scommentare e valorizzare in produzione
|
||||
|
||||
// HTTPS (opzionale in produzione)
|
||||
// https: {
|
||||
// key: require("fs").readFileSync(path.join(__dirname, "privkey.pem")),
|
||||
// cert: require("fs").readFileSync(path.join(__dirname, "fullchain.pem"))
|
||||
// },
|
||||
|
||||
flowFile: process.env.FLOWS || path.join(__dirname, "flows.json"),
|
||||
flowFilePretty: true,
|
||||
credentialSecret: process.env.NODE_RED_CREDENTIAL_SECRET || "s2s-reasoning-local-dev",
|
||||
|
||||
httpNodeCors: {
|
||||
origin: "*",
|
||||
methods: "GET,PUT,POST,DELETE,OPTIONS",
|
||||
credentials: true,
|
||||
},
|
||||
csrf: false,
|
||||
|
||||
// ── IMPORTANTE: caricamento nodi extra ─────────────────────────────
|
||||
// Node-RED 5 carica i plugin NODI da:
|
||||
// 1) {userDir}/node_modules (nostro .node-red/node_modules)
|
||||
// 2) {nodesDir} (installazione root del progetto)
|
||||
// 3) npm global
|
||||
// Senza nodesDir i pacchetti installati nella ROOT del progetto non
|
||||
// vengono trovati.
|
||||
nodesDir: LOCAL_NODE_MODULES,
|
||||
|
||||
externalModules: {
|
||||
autoInstall: true, // auto-installa nodi mancanti al boot
|
||||
autoInstallRetry: 3,
|
||||
palette: {
|
||||
allowInstall: true, // permette Install dall'editor
|
||||
allowUpload: true,
|
||||
modules: ["node-red-contrib-openai", "node-red-contrib-cache", "node-red-node-rbe"],
|
||||
allowList: ["node-red-contrib-*", "node-red-node-*"],
|
||||
denyList: []
|
||||
}
|
||||
},
|
||||
|
||||
editorTheme: { projects: { enabled: false } },
|
||||
|
||||
logging: {
|
||||
console: {
|
||||
level: process.env.NODE_RED_LOG_LEVEL || process.env.LOG_LEVEL || "info",
|
||||
metrics: false,
|
||||
audit: false,
|
||||
timestamp: true,
|
||||
}
|
||||
},
|
||||
|
||||
contextStorage: {
|
||||
default: { module: "memory" },
|
||||
file: { module: "localfilesystem" }
|
||||
},
|
||||
};
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================
|
||||
# start.sh — Avvia Node-RED 5.0 in locale per s2s-reasoning-engine
|
||||
#
|
||||
# Uso:
|
||||
# ./start.sh # porta default 1881
|
||||
# PORT=1900 ./start.sh # porta custom
|
||||
# LOG_LEVEL=debug ./start.sh # log più dettagliato
|
||||
# DEBUG=nodes:* ./start.sh # debug Node-RED interno
|
||||
# Stop: Ctrl+C
|
||||
# =============================================================
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PARENT_DIR="$(dirname "$ROOT_DIR")"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 1) Carica env: prima locale .env (se esiste) poi padre ../.env
|
||||
# Il file locale sovrascrive il padre se c'è lo stesso var.
|
||||
# ----------------------------------------------------------
|
||||
set -a
|
||||
if [ -f "$ROOT_DIR/.env" ]; then
|
||||
echo "[env] loaded $ROOT_DIR/.env"
|
||||
# shellcheck disable=SC1091
|
||||
source "$ROOT_DIR/.env"
|
||||
fi
|
||||
if [ -f "$PARENT_DIR/.env" ]; then
|
||||
echo "[env] loaded $PARENT_DIR/.env"
|
||||
# shellcheck disable=SC1091
|
||||
source "$PARENT_DIR/.env"
|
||||
fi
|
||||
set +a
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 2) Parametri runtime Node-RED
|
||||
# ----------------------------------------------------------
|
||||
PORT="${PORT:-${NODE_RED_PORT:-1881}}"
|
||||
export PORT
|
||||
export NODE_RED_PORT="$PORT"
|
||||
export NODE_RED_LOG_LEVEL="${LOG_LEVEL:-${NODE_RED_LOG_LEVEL:-info}}"
|
||||
export NODE_RED_CREDENTIAL_SECRET="${NODE_RED_CREDENTIAL_SECRET:-s2s-reasoning-local-dev}"
|
||||
export FLOWS="${FLOWS:-$ROOT_DIR/.node-red/flows.json}"
|
||||
|
||||
# UserDir locale (tutto dentro la cartella corrente, NO ~/.node-red)
|
||||
USERDIR="${USERDIR:-$ROOT_DIR/.node-red}"
|
||||
SETTINGS="${SETTINGS:-$USERDIR/settings.js}"
|
||||
|
||||
# Assicura che la userDir esista e i file base siano presenti.
|
||||
mkdir -p "$USERDIR"
|
||||
if [ ! -f "$SETTINGS" ] || [ "$ROOT_DIR/settings.js" -nt "$SETTINGS" ]; then
|
||||
cp -f "$ROOT_DIR/settings.js" "$SETTINGS"
|
||||
echo "[init] copied settings.js -> $SETTINGS"
|
||||
fi
|
||||
if [ ! -f "$FLOWS" ] || [ "$ROOT_DIR/flows.json" -nt "$FLOWS" ]; then
|
||||
cp -f "$ROOT_DIR/flows.json" "$FLOWS"
|
||||
echo "[init] copied flows.json -> $FLOWS"
|
||||
fi
|
||||
|
||||
# Verifica node_modules installati
|
||||
if [ ! -d "$ROOT_DIR/node_modules/node-red" ]; then
|
||||
echo "[init] node_modules non trovati. Avvio npm install..."
|
||||
npm install --no-audit --no-fund
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════╗"
|
||||
echo "║ s2s-reasoning-engine — Node-RED v5 locale ║"
|
||||
echo "╠════════════════════════════════════════════════════════════╣"
|
||||
echo "║ Editor: http://localhost:${PORT}/ ║"
|
||||
echo "║ API: http://localhost:${PORT}/api/reasoning ║"
|
||||
echo "║ UserDir: ${USERDIR}"
|
||||
echo "║ LogLevel: ${NODE_RED_LOG_LEVEL}"
|
||||
echo "╚════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 3) Avvio Node-RED
|
||||
# ----------------------------------------------------------
|
||||
exec "$ROOT_DIR/node_modules/.bin/node-red" \
|
||||
--userDir "$USERDIR" \
|
||||
--settings "$SETTINGS" \
|
||||
--port "$PORT"
|
||||
Reference in New Issue
Block a user