Agent IA planificateur EVCS en MODE OMBRE (Advisor Shadow V1)
- Workflow n8n tdfInZns7cqo8fU2 actif (15 min): snapshot HA compact -> prompt FR -> LLM local Spark (modèle dynamique via /v1/models) avec fallback OpenRouter claude-haiku-4.5 (credential chiffré, clé hors git) -> validation stricte (enums, bornes, rejet total si invalide) -> helpers advisor_* + divergence + compteurs accord/divergence + logbook. - GARANTIE OMBRE structurelle: écritures limitées à advisor_*/logbook, vérifiée par tests/advisor/run.mjs (16 tests, bloquants au push). - Kill-switch input_boolean.evcs_advisor_enabled. - Extension R2 multi-workflows (lib-node-map WORKFLOWS[], build/extract/push par fichier, n8n-create.mjs, gen-advisor-workflow.mjs). - Helpers HA via package NFS + carte dashboard EVCS Master (backup fait). - 1re reco réelle: auto_solar_plus, cible 35%, accord avec le réel. - Fix sécurité n8n-push: plus de fallback d'id implicite vers le workflow EVCS.
This commit is contained in:
214
tools/gen-advisor-workflow.mjs
Normal file
214
tools/gen-advisor-workflow.mjs
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env node
|
||||
// Génère workflows/evcs-advisor-shadow-v1.json (squelette : nodes, connexions,
|
||||
// credentials, positions). Le CODE des nodes est ensuite injecté depuis
|
||||
// src/advisor/ par tools/n8n-build.mjs (source de vérité R2).
|
||||
// Rejouable : écrase le fichier local (pas le serveur).
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
const HA_CRED = { id: 'uv3sknYYigxBkaVN', name: 'Bearer Auth account 2' };
|
||||
const OPENROUTER_CRED = { id: 'eOM9xofDHT0KMoeY', name: 'OpenRouter Advisor' };
|
||||
const SPARK_URL = 'http://10.10.10.8:8000';
|
||||
const CLOUD_MODEL = 'anthropic/claude-haiku-4.5';
|
||||
const HA_URL = 'http://10.10.10.17:8123';
|
||||
|
||||
const nodes = [
|
||||
{
|
||||
name: 'Schedule Trigger',
|
||||
type: 'n8n-nodes-base.scheduleTrigger',
|
||||
typeVersion: 1.2,
|
||||
position: [0, 0],
|
||||
parameters: { rule: { interval: [{ field: 'minutes', minutesInterval: 15 }] } },
|
||||
},
|
||||
{
|
||||
name: 'HA Snapshot Advisor',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [200, 0],
|
||||
parameters: {
|
||||
method: 'POST',
|
||||
url: `${HA_URL}/api/template`,
|
||||
authentication: 'genericCredentialType',
|
||||
genericAuthType: 'httpBearerAuth',
|
||||
sendHeaders: true,
|
||||
headerParameters: {
|
||||
parameters: [
|
||||
{ name: 'Content-Type', value: 'application/json' },
|
||||
{ name: 'Accept', value: 'text/plain' },
|
||||
],
|
||||
},
|
||||
sendBody: true,
|
||||
bodyParameters: { parameters: [{ name: 'template', value: '(injecté depuis src/advisor)' }] },
|
||||
options: { timeout: 15000 },
|
||||
},
|
||||
credentials: { httpBearerAuth: HA_CRED },
|
||||
},
|
||||
{
|
||||
name: 'BUILD_PROMPT',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [400, 0],
|
||||
parameters: { jsCode: '// injecté depuis src/advisor/BUILD_PROMPT.js' },
|
||||
},
|
||||
{
|
||||
name: 'IF_ENABLED',
|
||||
type: 'n8n-nodes-base.if',
|
||||
typeVersion: 2.2,
|
||||
position: [600, 0],
|
||||
parameters: {
|
||||
conditions: {
|
||||
options: { caseSensitive: true, leftValue: '', typeValidation: 'loose', version: 2 },
|
||||
combinator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
leftValue: '={{ $json.enabled }}',
|
||||
rightValue: '',
|
||||
operator: { type: 'boolean', operation: 'true', singleValue: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'LLM_LOCAL_MODELS',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [800, -100],
|
||||
parameters: {
|
||||
method: 'GET',
|
||||
url: `${SPARK_URL}/v1/models`,
|
||||
options: { timeout: 5000 },
|
||||
},
|
||||
onError: 'continueRegularOutput',
|
||||
alwaysOutputData: true,
|
||||
},
|
||||
{
|
||||
name: 'LLM_LOCAL',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [1000, -100],
|
||||
parameters: {
|
||||
method: 'POST',
|
||||
url: `${SPARK_URL}/v1/chat/completions`,
|
||||
sendHeaders: true,
|
||||
headerParameters: { parameters: [{ name: 'Content-Type', value: 'application/json' }] },
|
||||
sendBody: true,
|
||||
specifyBody: 'json',
|
||||
jsonBody:
|
||||
"={{ JSON.stringify({ model: $('LLM_LOCAL_MODELS').first().json.data[0].id, messages: $('BUILD_PROMPT').first().json.messages, temperature: 0.1, max_tokens: 6000 }) }}",
|
||||
options: { timeout: 120000 },
|
||||
},
|
||||
onError: 'continueRegularOutput',
|
||||
alwaysOutputData: true,
|
||||
},
|
||||
{
|
||||
name: 'IF_LOCAL_OK',
|
||||
type: 'n8n-nodes-base.if',
|
||||
typeVersion: 2.2,
|
||||
position: [1200, -100],
|
||||
parameters: {
|
||||
conditions: {
|
||||
options: { caseSensitive: true, leftValue: '', typeValidation: 'loose', version: 2 },
|
||||
combinator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
leftValue: "={{ $json.choices !== undefined && $json.choices.length > 0 }}",
|
||||
rightValue: '',
|
||||
operator: { type: 'boolean', operation: 'true', singleValue: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'LLM_CLOUD',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [1400, 40],
|
||||
parameters: {
|
||||
method: 'POST',
|
||||
url: 'https://openrouter.ai/api/v1/chat/completions',
|
||||
authentication: 'genericCredentialType',
|
||||
genericAuthType: 'httpHeaderAuth',
|
||||
sendHeaders: true,
|
||||
headerParameters: { parameters: [{ name: 'Content-Type', value: 'application/json' }] },
|
||||
sendBody: true,
|
||||
specifyBody: 'json',
|
||||
jsonBody:
|
||||
`={{ JSON.stringify({ model: '${CLOUD_MODEL}', messages: $('BUILD_PROMPT').first().json.messages, temperature: 0.1, max_tokens: 1000 }) }}`,
|
||||
options: { timeout: 60000 },
|
||||
},
|
||||
credentials: { httpHeaderAuth: OPENROUTER_CRED },
|
||||
onError: 'continueRegularOutput',
|
||||
alwaysOutputData: true,
|
||||
},
|
||||
{
|
||||
name: 'VALIDATE_PARSE',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [1600, -30],
|
||||
parameters: { jsCode: '// injecté depuis src/advisor/VALIDATE_PARSE.js' },
|
||||
},
|
||||
{
|
||||
name: 'BUILD_HA_CALLS',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [1800, -30],
|
||||
parameters: { jsCode: '// injecté depuis src/advisor/BUILD_HA_CALLS.js' },
|
||||
},
|
||||
{
|
||||
name: 'Execute HA Service Advisor',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [2000, -30],
|
||||
parameters: {
|
||||
method: 'POST',
|
||||
url: "={{ 'http://10.10.10.17:8123/api/services/' + $json.domain + '/' + $json.service }}",
|
||||
authentication: 'genericCredentialType',
|
||||
genericAuthType: 'httpBearerAuth',
|
||||
sendHeaders: true,
|
||||
headerParameters: {
|
||||
parameters: [
|
||||
{ name: 'Content-Type', value: 'application/json' },
|
||||
{ name: 'accept', value: 'application/json' },
|
||||
],
|
||||
},
|
||||
sendBody: true,
|
||||
specifyBody: 'json',
|
||||
jsonBody: '={{ $json.data }}',
|
||||
options: { timeout: 15000 },
|
||||
},
|
||||
credentials: { httpBearerAuth: HA_CRED },
|
||||
onError: 'continueRegularOutput',
|
||||
},
|
||||
];
|
||||
|
||||
const connections = {
|
||||
'Schedule Trigger': { main: [[{ node: 'HA Snapshot Advisor', type: 'main', index: 0 }]] },
|
||||
'HA Snapshot Advisor': { main: [[{ node: 'BUILD_PROMPT', type: 'main', index: 0 }]] },
|
||||
BUILD_PROMPT: { main: [[{ node: 'IF_ENABLED', type: 'main', index: 0 }]] },
|
||||
IF_ENABLED: { main: [[{ node: 'LLM_LOCAL_MODELS', type: 'main', index: 0 }], []] },
|
||||
LLM_LOCAL_MODELS: { main: [[{ node: 'LLM_LOCAL', type: 'main', index: 0 }]] },
|
||||
LLM_LOCAL: { main: [[{ node: 'IF_LOCAL_OK', type: 'main', index: 0 }]] },
|
||||
IF_LOCAL_OK: {
|
||||
main: [
|
||||
[{ node: 'VALIDATE_PARSE', type: 'main', index: 0 }],
|
||||
[{ node: 'LLM_CLOUD', type: 'main', index: 0 }],
|
||||
],
|
||||
},
|
||||
LLM_CLOUD: { main: [[{ node: 'VALIDATE_PARSE', type: 'main', index: 0 }]] },
|
||||
VALIDATE_PARSE: { main: [[{ node: 'BUILD_HA_CALLS', type: 'main', index: 0 }]] },
|
||||
BUILD_HA_CALLS: { main: [[{ node: 'Execute HA Service Advisor', type: 'main', index: 0 }]] },
|
||||
};
|
||||
|
||||
const workflow = {
|
||||
name: 'EVCS Advisor Shadow V1',
|
||||
nodes,
|
||||
connections,
|
||||
settings: { executionOrder: 'v1', callerPolicy: 'workflowsFromSameOwner' },
|
||||
staticData: null,
|
||||
};
|
||||
|
||||
fs.writeFileSync('workflows/evcs-advisor-shadow-v1.json', JSON.stringify(workflow, null, 2) + '\n');
|
||||
console.log(`Squelette écrit: workflows/evcs-advisor-shadow-v1.json (${nodes.length} nodes)`);
|
||||
console.log('→ lancer: node tools/n8n-build.mjs workflows/evcs-advisor-shadow-v1.json');
|
||||
@@ -1,19 +1,40 @@
|
||||
// R2 — Cartographie src/nodes/ ↔ workflow JSON (source de vérité : src/nodes/).
|
||||
// R2 — Cartographie src/ ↔ workflows JSON (source de vérité : src/).
|
||||
// Multi-workflows depuis l'Advisor (08/2026).
|
||||
// Utilisé par tools/n8n-extract.mjs (JSON → src) et tools/n8n-build.mjs (src → JSON).
|
||||
|
||||
export const WORKFLOW_FILE = 'workflows/evcs-surplus-controller-v2.6.json';
|
||||
export const SRC_DIR = 'src/nodes';
|
||||
|
||||
export const NODE_MAP = [
|
||||
{ file: 'INPUT_PARSE.js', node: 'INPUT_PARSE', kind: 'jsCode' },
|
||||
{ file: 'WINDOWS_SOURCEV2.js', node: 'WINDOWS_SOURCEV2', kind: 'jsCode' },
|
||||
{ file: 'BATTERY_CLOUD_LOCKS.js', node: 'BATTERY_CLOUD_LOCKS', kind: 'jsCode' },
|
||||
{ file: 'DECISION_ENGINEV2.js', node: 'DECISION_ENGINEV2', kind: 'jsCode' },
|
||||
{ file: 'HA_SERVICE_CALLS_OUTPUT.js', node: 'HA_SERVICE_CALLS_OUTPUT', kind: 'jsCode' },
|
||||
{ file: 'BUILD_ACTIONS.js', node: 'Build EVCS Surplus Controller V2.0 Actions', kind: 'jsCode' },
|
||||
{ file: 'HA_SNAPSHOT_TEMPLATE.json.tpl', node: 'HA Snapshot EVCS Surplus Controller V2.2', kind: 'template' },
|
||||
export const WORKFLOWS = [
|
||||
{
|
||||
id: 'evcs',
|
||||
workflowFile: 'workflows/evcs-surplus-controller-v2.6.json',
|
||||
srcDir: 'src/nodes',
|
||||
nodes: [
|
||||
{ file: 'INPUT_PARSE.js', node: 'INPUT_PARSE', kind: 'jsCode' },
|
||||
{ file: 'WINDOWS_SOURCEV2.js', node: 'WINDOWS_SOURCEV2', kind: 'jsCode' },
|
||||
{ file: 'BATTERY_CLOUD_LOCKS.js', node: 'BATTERY_CLOUD_LOCKS', kind: 'jsCode' },
|
||||
{ file: 'DECISION_ENGINEV2.js', node: 'DECISION_ENGINEV2', kind: 'jsCode' },
|
||||
{ file: 'HA_SERVICE_CALLS_OUTPUT.js', node: 'HA_SERVICE_CALLS_OUTPUT', kind: 'jsCode' },
|
||||
{ file: 'BUILD_ACTIONS.js', node: 'Build EVCS Surplus Controller V2.0 Actions', kind: 'jsCode' },
|
||||
{ file: 'HA_SNAPSHOT_TEMPLATE.json.tpl', node: 'HA Snapshot EVCS Surplus Controller V2.2', kind: 'template' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'advisor',
|
||||
workflowFile: 'workflows/evcs-advisor-shadow-v1.json',
|
||||
srcDir: 'src/advisor',
|
||||
nodes: [
|
||||
{ file: 'BUILD_PROMPT.js', node: 'BUILD_PROMPT', kind: 'jsCode' },
|
||||
{ file: 'VALIDATE_PARSE.js', node: 'VALIDATE_PARSE', kind: 'jsCode' },
|
||||
{ file: 'BUILD_HA_CALLS.js', node: 'BUILD_HA_CALLS', kind: 'jsCode' },
|
||||
{ file: 'HA_SNAPSHOT_TEMPLATE.json.tpl', node: 'HA Snapshot Advisor', kind: 'template' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Rétro-compatibilité (outils historiques mono-workflow)
|
||||
export const WORKFLOW_FILE = WORKFLOWS[0].workflowFile;
|
||||
export const SRC_DIR = WORKFLOWS[0].srcDir;
|
||||
export const NODE_MAP = WORKFLOWS[0].nodes;
|
||||
|
||||
export function getCode(workflowNode, kind) {
|
||||
if (kind === 'jsCode') return workflowNode.parameters.jsCode;
|
||||
if (kind === 'template') {
|
||||
@@ -30,3 +51,8 @@ export function setCode(workflowNode, kind, code) {
|
||||
}
|
||||
throw new Error(`kind inconnu: ${kind}`);
|
||||
}
|
||||
|
||||
export function findWorkflowByFile(inputPath) {
|
||||
const base = inputPath.replace(/\\/g, '/').split('/').pop();
|
||||
return WORKFLOWS.find(w => w.workflowFile.endsWith(base));
|
||||
}
|
||||
|
||||
@@ -1,38 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
// R2 — BUILD : src/nodes/*.js → workflow JSON (src/ = SOURCE DE VÉRITÉ, src gagne).
|
||||
// Usage : node tools/n8n-build.mjs [--check]
|
||||
// --check : vérifie que le JSON est déjà synchronisé avec src/ (exit 1 sinon),
|
||||
// sans rien écrire.
|
||||
// R2 — BUILD : src/ → workflows JSON (src = SOURCE DE VÉRITÉ, src gagne).
|
||||
// Usage : node tools/n8n-build.mjs [--check] [fichierWorkflow]
|
||||
// --check : vérifie la synchro sans écrire (exit 1 si désynchronisé)
|
||||
// fichierWorkflow : limite à un workflow (sinon tous ceux du node-map)
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { WORKFLOW_FILE, SRC_DIR, NODE_MAP, getCode, setCode } from './lib-node-map.mjs';
|
||||
import { WORKFLOWS, getCode, setCode, findWorkflowByFile } from './lib-node-map.mjs';
|
||||
|
||||
const CHECK = process.argv.includes('--check');
|
||||
const wf = JSON.parse(fs.readFileSync(WORKFLOW_FILE, 'utf8'));
|
||||
const fileArg = process.argv.slice(2).find(a => !a.startsWith('--'));
|
||||
const targets = fileArg ? [findWorkflowByFile(fileArg)].filter(Boolean) : WORKFLOWS;
|
||||
if (fileArg && !targets.length) {
|
||||
console.log(`(${fileArg} non enregistré dans le node-map : rien à builder)`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let changes = 0;
|
||||
for (const { file, node, kind } of NODE_MAP) {
|
||||
const srcPath = path.join(SRC_DIR, file);
|
||||
if (!fs.existsSync(srcPath)) throw new Error(`Fichier source manquant: ${srcPath}`);
|
||||
const srcCode = fs.readFileSync(srcPath, 'utf8');
|
||||
const n = wf.nodes.find(x => x.name === node);
|
||||
if (!n) throw new Error(`Node introuvable dans le JSON: ${node}`);
|
||||
if (getCode(n, kind) !== srcCode) {
|
||||
changes++;
|
||||
console.log(`${CHECK ? 'DÉSYNCHRONISÉ' : 'inject'}: ${file} → "${node}"`);
|
||||
if (!CHECK) setCode(n, kind, srcCode);
|
||||
let totalChanges = 0;
|
||||
for (const w of targets) {
|
||||
if (!fs.existsSync(w.workflowFile)) {
|
||||
console.log(`(skip ${w.id}: ${w.workflowFile} absent)`);
|
||||
continue;
|
||||
}
|
||||
const wf = JSON.parse(fs.readFileSync(w.workflowFile, 'utf8'));
|
||||
let changes = 0;
|
||||
for (const { file, node, kind } of w.nodes) {
|
||||
const srcPath = path.join(w.srcDir, file);
|
||||
if (!fs.existsSync(srcPath)) throw new Error(`Fichier source manquant: ${srcPath}`);
|
||||
const srcCode = fs.readFileSync(srcPath, 'utf8');
|
||||
const n = wf.nodes.find(x => x.name === node);
|
||||
if (!n) throw new Error(`Node introuvable dans ${w.workflowFile}: ${node}`);
|
||||
if (getCode(n, kind) !== srcCode) {
|
||||
changes++;
|
||||
console.log(`${CHECK ? 'DÉSYNCHRONISÉ' : 'inject'}: ${w.srcDir}/${file} → "${node}"`);
|
||||
if (!CHECK) setCode(n, kind, srcCode);
|
||||
}
|
||||
}
|
||||
if (!CHECK && changes) {
|
||||
fs.writeFileSync(w.workflowFile, JSON.stringify(wf, null, 2) + '\n');
|
||||
}
|
||||
totalChanges += changes;
|
||||
console.log(`${w.id}: ${changes ? changes + ' node(s) ' + (CHECK ? 'désynchronisé(s)' : 'injecté(s)') : 'synchronisé'}`);
|
||||
}
|
||||
|
||||
if (CHECK) {
|
||||
console.log(changes ? `\n${changes} node(s) désynchronisé(s) : lancer node tools/n8n-build.mjs` : 'JSON synchronisé avec src/nodes/');
|
||||
process.exit(changes ? 1 : 0);
|
||||
}
|
||||
|
||||
if (changes) {
|
||||
fs.writeFileSync(WORKFLOW_FILE, JSON.stringify(wf, null, 2) + '\n');
|
||||
console.log(`\nBuild OK: ${changes} node(s) injecté(s) dans ${WORKFLOW_FILE}`);
|
||||
} else {
|
||||
console.log('Build: aucun changement (déjà synchronisé)');
|
||||
}
|
||||
if (CHECK) process.exit(totalChanges ? 1 : 0);
|
||||
|
||||
37
tools/n8n-create.mjs
Normal file
37
tools/n8n-create.mjs
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
// Crée un workflow n8n depuis un fichier JSON local (POST /api/v1/workflows).
|
||||
// Usage: node tools/n8n-create.mjs <fichier.json>
|
||||
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
const INPUT = process.argv[2];
|
||||
if (!INPUT) { console.error('Usage: node tools/n8n-create.mjs <fichier.json>'); process.exit(1); }
|
||||
const local = JSON.parse(fs.readFileSync(INPUT, 'utf8'));
|
||||
|
||||
const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.config/opencode/opencode.json'), 'utf8'));
|
||||
const env = cfg.mcp['n8n-mcp'].environment;
|
||||
const url = env.N8N_API_URL.replace(/\/api\/v1\/?$/, '');
|
||||
const headers = { 'X-N8N-API-KEY': env.N8N_API_KEY, 'Content-Type': 'application/json' };
|
||||
|
||||
const body = {
|
||||
name: local.name,
|
||||
nodes: local.nodes,
|
||||
connections: local.connections,
|
||||
settings: {
|
||||
executionOrder: local.settings?.executionOrder || 'v1',
|
||||
callerPolicy: local.settings?.callerPolicy || 'workflowsFromSameOwner',
|
||||
},
|
||||
};
|
||||
|
||||
const res = await fetch(`${url}/api/v1/workflows`, { method: 'POST', headers, body: JSON.stringify(body) });
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
console.error(`CRÉATION REFUSÉE: ${res.status} ${res.statusText}`);
|
||||
console.error(text.slice(0, 1000));
|
||||
process.exit(1);
|
||||
}
|
||||
const created = JSON.parse(text);
|
||||
console.log(`Créé: "${created.name}" id=${created.id} (active=${created.active})`);
|
||||
console.log(`→ reporter l'id dans le fichier local si besoin: ${INPUT}`);
|
||||
@@ -1,21 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
// R2 — EXTRACT : workflow JSON → src/nodes/*.js (+ template snapshot).
|
||||
// À utiliser UNIQUEMENT pour (re)synchroniser src/ après une modification faite
|
||||
// directement dans l'UI n8n (à éviter). En temps normal : éditer src/nodes/,
|
||||
// puis tools/n8n-push.mjs reconstruit le JSON automatiquement.
|
||||
// R2 — EXTRACT : workflows JSON → src/ (resynchronisation après édition UI n8n — à éviter).
|
||||
// Usage : node tools/n8n-extract.mjs [fichierWorkflow]
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { WORKFLOW_FILE, SRC_DIR, NODE_MAP, getCode } from './lib-node-map.mjs';
|
||||
import { WORKFLOWS, getCode, findWorkflowByFile } from './lib-node-map.mjs';
|
||||
|
||||
const wf = JSON.parse(fs.readFileSync(WORKFLOW_FILE, 'utf8'));
|
||||
fs.mkdirSync(SRC_DIR, { recursive: true });
|
||||
const fileArg = process.argv.slice(2).find(a => !a.startsWith('--'));
|
||||
const targets = fileArg ? [findWorkflowByFile(fileArg)].filter(Boolean) : WORKFLOWS;
|
||||
|
||||
for (const { file, node, kind } of NODE_MAP) {
|
||||
const n = wf.nodes.find(x => x.name === node);
|
||||
if (!n) throw new Error(`Node introuvable: ${node}`);
|
||||
const code = getCode(n, kind);
|
||||
fs.writeFileSync(path.join(SRC_DIR, file), code);
|
||||
console.log(`${file} ← "${node}" (${code.length} chars)`);
|
||||
for (const w of targets) {
|
||||
if (!fs.existsSync(w.workflowFile)) {
|
||||
console.log(`(skip ${w.id}: ${w.workflowFile} absent)`);
|
||||
continue;
|
||||
}
|
||||
const wf = JSON.parse(fs.readFileSync(w.workflowFile, 'utf8'));
|
||||
fs.mkdirSync(w.srcDir, { recursive: true });
|
||||
for (const { file, node, kind } of w.nodes) {
|
||||
const n = wf.nodes.find(x => x.name === node);
|
||||
if (!n) throw new Error(`Node introuvable: ${node} (${w.workflowFile})`);
|
||||
const code = getCode(n, kind);
|
||||
fs.writeFileSync(path.join(w.srcDir, file), code);
|
||||
console.log(`${w.srcDir}/${file} ← "${node}" (${code.length} chars)`);
|
||||
}
|
||||
}
|
||||
console.log(`\nExtraction OK dans ${SRC_DIR}/`);
|
||||
console.log('\nExtraction OK');
|
||||
|
||||
@@ -15,14 +15,14 @@ if (!INPUT) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// R2 : src/nodes/ = SOURCE DE VÉRITÉ pour le workflow EVCS.
|
||||
// Le build injecte src/nodes/*.js dans le JSON AVANT toute vérification :
|
||||
// toute édition faite directement dans le JSON (ou l'UI n8n non ré-extraite)
|
||||
// est écrasée par src/. Workflow EVCS uniquement.
|
||||
if (path.basename(INPUT) === 'evcs-surplus-controller-v2.6.json' && fs.existsSync('src/nodes')) {
|
||||
console.log('Build src/nodes/ → JSON (source de vérité : src/)...');
|
||||
// R2 : src/ = SOURCE DE VÉRITÉ pour les workflows enregistrés dans le node-map.
|
||||
// Le build injecte src/**/*.js dans le JSON AVANT toute vérification.
|
||||
const { findWorkflowByFile } = await import('./lib-node-map.mjs');
|
||||
const registered = findWorkflowByFile(INPUT);
|
||||
if (registered) {
|
||||
console.log(`Build ${registered.srcDir}/ → JSON (source de vérité : src/)...`);
|
||||
const { spawnSync } = await import('child_process');
|
||||
const b = spawnSync('node', ['tools/n8n-build.mjs'], { stdio: 'inherit' });
|
||||
const b = spawnSync('node', ['tools/n8n-build.mjs', INPUT], { stdio: 'inherit' });
|
||||
if (b.status !== 0) {
|
||||
console.error('ABANDON: échec du build src → JSON.');
|
||||
process.exit(1);
|
||||
@@ -30,7 +30,11 @@ if (path.basename(INPUT) === 'evcs-surplus-controller-v2.6.json' && fs.existsSyn
|
||||
}
|
||||
|
||||
const local = JSON.parse(fs.readFileSync(INPUT, 'utf8'));
|
||||
const WORKFLOW_ID = process.argv[3] || local.id || 'ATswvGfG4KJUls0K';
|
||||
const WORKFLOW_ID = process.argv[3] || local.id;
|
||||
if (!WORKFLOW_ID) {
|
||||
console.error('ABANDON: aucun id de workflow (ni argument, ni champ id dans le JSON).');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function loadApiConfig() {
|
||||
if (process.env.N8N_API_KEY) {
|
||||
@@ -101,20 +105,25 @@ if (syntaxErrors) {
|
||||
}
|
||||
console.log('Syntaxe OK sur tous les nodes jsCode.');
|
||||
|
||||
// 3bis. Suite golden (R3) : obligatoire pour le workflow EVCS, sauf --skip-golden
|
||||
// 3bis. Suites de tests (R3) : bloquantes par workflow, sauf --skip-golden
|
||||
const isEvcsWorkflow = path.basename(INPUT) === 'evcs-surplus-controller-v2.6.json';
|
||||
const isAdvisorWorkflow = path.basename(INPUT) === 'evcs-advisor-shadow-v1.json';
|
||||
const skipGolden = process.argv.includes('--skip-golden');
|
||||
if (isEvcsWorkflow && fs.existsSync('tests/golden/run.mjs')) {
|
||||
const testSuite =
|
||||
isEvcsWorkflow && fs.existsSync('tests/golden/run.mjs') ? 'tests/golden/run.mjs' :
|
||||
isAdvisorWorkflow && fs.existsSync('tests/advisor/run.mjs') ? 'tests/advisor/run.mjs' :
|
||||
null;
|
||||
if (testSuite) {
|
||||
if (skipGolden) {
|
||||
console.log('⚠️ Suite golden SAUTÉE (--skip-golden) — à n\'utiliser qu\'en connaissance de cause.');
|
||||
console.log('⚠️ Suite de tests SAUTÉE (--skip-golden) — à n\'utiliser qu\'en connaissance de cause.');
|
||||
} else {
|
||||
console.log('Exécution de la suite golden (tests/golden/run.mjs)...');
|
||||
console.log(`Exécution de la suite de tests (${testSuite})...`);
|
||||
const { spawnSync } = await import('child_process');
|
||||
const g = spawnSync('node', ['tests/golden/run.mjs'], { stdio: 'inherit' });
|
||||
const g = spawnSync('node', [testSuite], { stdio: 'inherit' });
|
||||
if (g.status !== 0) {
|
||||
console.error('ABANDON: la suite golden échoue. Rien n\'a été poussé.');
|
||||
console.error('ABANDON: la suite de tests échoue. Rien n\'a été poussé.');
|
||||
console.error('(Évolution volontaire du comportement ? Mettre à jour les attendus');
|
||||
console.error(' dans tests/golden/scenarios.mjs dans le même commit, puis re-pousser.)');
|
||||
console.error(' de la suite dans le même commit, puis re-pousser.)');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user