node-red-contrib-linux-copilot 1.2.10 → 1.2.12

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.
@@ -1,31 +1,84 @@
1
1
  <script type="text/javascript">
2
- RED.nodes.registerType('linux-copilot',{
3
- category: 'function',
4
- color: '#a6bbcf',
5
- defaults: { name: {value:""}, chatId: {value:""} },
6
- credentials: {
7
- geminiKey: {type:"password"},
8
- deepseekKey: {type:"password"}
2
+ RED.nodes.registerType('linux-copilot', {
3
+ category: 'advanced',
4
+ color: '#E2D96E',
5
+ defaults: {
6
+ name: { value: "" },
7
+ chatId: { value: "" },
8
+ // On garde les priorités au cas où vous voudriez les ajuster plus tard
9
+ prioGEM: { value: "1" },
10
+ prioDS: { value: "2" },
11
+ prioOR: { value: "3" }
9
12
  },
10
- inputs:1, outputs:1,
11
- label: function() { return this.name||"linux-copilot"; }
13
+ credentials: {
14
+ geminiKey: { type: "password" },
15
+ deepseekKey: { type: "password" },
16
+ openrouterKey: { type: "password" }
17
+ },
18
+ inputs: 1,
19
+ outputs: 1,
20
+ icon: "font-awesome/fa-terminal",
21
+ label: function() {
22
+ return this.name || "Linux Copilot (Multi-LLM)";
23
+ },
24
+ oneditprepare: function() {
25
+ // Initialisation si nécessaire
26
+ }
12
27
  });
13
28
  </script>
29
+
14
30
  <script type="text/html" data-template-name="linux-copilot">
15
31
  <div class="form-row">
16
- <label for="node-input-name">Nom</label>
17
- <input type="text" id="node-input-name">
32
+ <label for="node-input-name"><i class="fa fa-tag"></i> Nom</label>
33
+ <input type="text" id="node-input-name" placeholder="Mon Expert SRE">
34
+ </div>
35
+
36
+ <div class="form-row">
37
+ <label for="node-input-chatId"><i class="fa fa-comment"></i> Chat ID</label>
38
+ <input type="text" id="node-input-chatId" placeholder="ID Telegram ou autre">
18
39
  </div>
40
+
41
+ <hr align="middle">
42
+ <h4><i class="fa fa-key"></i> Clés API & Modèles</h4>
43
+ <p style="font-size: 0.9em; color: #666;">
44
+ Le système tentera d'utiliser <b>Gemini/Gemma</b> en priorité, puis <b>DeepSeek</b>,
45
+ et enfin les modèles gratuits d'<b>OpenRouter</b> (Llama 3.3, Qwen, Olmo).
46
+ </p>
47
+
19
48
  <div class="form-row">
20
- <label for="node-input-chatId">Chat ID Telegram</label>
21
- <input type="text" id="node-input-chatId">
49
+ <label for="node-input-geminiKey"><i class="fa fa-google"></i> Google AI</label>
50
+ <input type="password" id="node-input-geminiKey" placeholder="Clé API Gemini (Gemma 3)">
22
51
  </div>
52
+
23
53
  <div class="form-row">
24
- <label for="node-input-deepseekKey">DeepSeek Key</label>
25
- <input type="password" id="node-input-deepseekKey">
54
+ <label for="node-input-deepseekKey"><i class="fa fa-code"></i> DeepSeek</label>
55
+ <input type="password" id="node-input-deepseekKey" placeholder="Clé API DeepSeek">
26
56
  </div>
57
+
27
58
  <div class="form-row">
28
- <label for="node-input-geminiKey">Gemini Key</label>
29
- <input type="password" id="node-input-geminiKey">
59
+ <label for="node-input-openrouterKey"><i class="fa fa-rocket"></i> OpenRouter</label>
60
+ <input type="password" id="node-input-openrouterKey" placeholder="Clé API OpenRouter (Modèles Free)">
61
+ </div>
62
+
63
+ <hr align="middle">
64
+ <div class="form-tips">
65
+ <b>Conseil :</b> Laissez un champ vide si vous n'avez pas la clé. Le nœud sautera automatiquement les modèles correspondants sans bloquer le diagnostic.
30
66
  </div>
31
67
  </script>
68
+
69
+ <script type="text/html" data-help-name="linux-copilot">
70
+ <p>Un expert Linux SRE capable de diagnostiquer votre système en cascade.</p>
71
+ <h3>Fonctionnement</h3>
72
+ <ol>
73
+ <li>Analyse le texte ou le résultat de commande reçu.</li>
74
+ <li>Interroge le meilleur modèle disponible (Failover).</li>
75
+ <li>Exécute automatiquement des commandes de diagnostic sécurisées (ls, df, top, etc.).</li>
76
+ <li>Boucle jusqu'à résolution ou fin de séquence (5 itérations max).</li>
77
+ </ol>
78
+ <h3>Modèles inclus</h3>
79
+ <ul>
80
+ <li><b>Gemini 2.0 Flash / Gemma 3</b> : Priorité haute, rapide.</li>
81
+ <li><b>DeepSeek V3</b> : Analyse de code et logs.</li>
82
+ <li><b>Qwen/Llama/Olmo</b> : Relève via OpenRouter si nécessaire.</li>
83
+ </ul>
84
+ </script>
package/linux-copilot.js CHANGED
@@ -7,23 +7,127 @@ module.exports = function(RED) {
7
7
  const node = this;
8
8
 
9
9
  const omniPrompt = `Tu es un Expert Linux SRE polyglotte.
10
- FORMAT JSON : {"speech": "ton explication", "cmd": "commande linux ou none"}`;
10
+ RÈGLES CRUCIALES :
11
+ 1. LANGUE : Réponds TOUJOURS dans la langue de l'utilisateur.
12
+ 2. ANALYSE : Analyse brièvement les résultats techniques.
13
+ 3. ACTION : Propose la commande linux suivante.
14
+ 4. FORMAT JSON STRICT : {"speech": "ton explication", "cmd": "commande ou none"}`;
15
+
16
+ const engines = {
17
+ // Moteur Google (Gemini 2.0 Flash & Gemma 3)
18
+ google: async (history, key, model) => {
19
+ const res = await axios.post(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key.trim()}`, {
20
+ contents: history.map(h => ({
21
+ role: h.role === "assistant" ? "model" : h.role,
22
+ parts: [{ text: h.content }]
23
+ })),
24
+ system_instruction: { parts: [{ text: omniPrompt }] },
25
+ generationConfig: { responseMimeType: "application/json", temperature: 0.1 }
26
+ }, { timeout: 12000 });
27
+ return JSON.parse(res.data.candidates[0].content.parts[0].text);
28
+ },
29
+ // Moteur OpenRouter (Pour tous les modèles gratuits : DeepSeek, Llama, Qwen, Olmo)
30
+ openrouter: async (history, key, model) => {
31
+ const res = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
32
+ model: model,
33
+ messages: [{ role: 'system', content: omniPrompt }, ...history.map(h => ({
34
+ role: h.role === "model" ? "assistant" : h.role,
35
+ content: String(h.content)
36
+ }))]
37
+ }, {
38
+ headers: { 'Authorization': `Bearer ${key.trim()}`, 'Content-Type': 'application/json' },
39
+ timeout: 20000
40
+ });
41
+ const content = res.data.choices[0].message.content;
42
+ const match = content.match(/\{[\s\S]*\}/);
43
+ return JSON.parse(match ? match[0] : content);
44
+ }
45
+ };
11
46
 
12
47
  node.on('input', async function(msg) {
13
- const chatId = msg.payload.chatId || config.chatId || "1457427557";
48
+ const chatId = msg.payload.chatId || config.chatId;
49
+ let userText = msg.payload.content || (typeof msg.payload === 'string' ? msg.payload : "");
50
+ let loopCount = msg.loopCount || 0;
51
+
52
+ if (loopCount > 5) return node.status({fill:"blue", text:"Séquence terminée"});
53
+
14
54
  let history = node.context().get('history') || [];
55
+ if (userText.toLowerCase() === "reset") {
56
+ node.context().set('history', []);
57
+ return node.send({ payload: { chatId, content: "♻️ Historique effacé." } });
58
+ }
59
+
60
+ if (!userText) return;
61
+ history.push({ role: "user", content: userText });
62
+
63
+ // LA LISTE DE SECOURS (ORDRE DE PRIORITÉ)
64
+ const queue = [
65
+ { type: 'google', m: "gemini-2.0-flash", n: "Gemini 2.0 Flash", k: node.credentials.geminiKey },
66
+ { type: 'openrouter', m: "deepseek/deepseek-chat", n: "DeepSeek V3", k: node.credentials.deepseekKey },
67
+ { type: 'openrouter', m: "qwen/qwen-2.5-coder-32b-instruct:free", n: "Qwen 2.5 Coder", k: node.credentials.openrouterKey },
68
+ { type: 'openrouter', m: "meta-llama/llama-3.3-70b-instruct:free", n: "Llama 3.3 Free", k: node.credentials.openrouterKey },
69
+ { type: 'openrouter', m: "google/gemini-2.0-flash-001", n: "Gemini (via OpenRouter)", k: node.credentials.openrouterKey },
70
+ { type: 'openrouter', m: "allenai/olmo-3-32b-instruct", n: "Olmo 3", k: node.credentials.openrouterKey }
71
+ ].filter(q => q.k);
72
+
73
+ let aiData = null;
74
+ let engineUsed = "";
75
+
76
+ for (let e of queue) {
77
+ try {
78
+ node.status({fill:"yellow", text: `Essai ${e.n}...`});
79
+ aiData = await engines[e.type](history, e.k, e.m);
80
+ if (aiData && (aiData.speech || aiData.cmd)) {
81
+ engineUsed = e.n;
82
+ break;
83
+ }
84
+ } catch (err) {
85
+ node.warn(`Échec ${e.n}: ${err.message}`);
86
+ }
87
+ }
88
+
89
+ if (!aiData) {
90
+ node.status({fill:"red", text:"Tous les modèles ont échoué"});
91
+ return;
92
+ }
93
+
94
+ // Réponse à l'utilisateur
95
+ node.send({ payload: {
96
+ chatId,
97
+ type: "message",
98
+ content: `🤖 [${engineUsed}] : ${aiData.speech}`,
99
+ options: { parse_mode: "HTML" }
100
+ }});
101
+
102
+ // Gestion du Terminal
103
+ let cmd = (aiData.cmd || "").trim();
104
+ const safeCommands = ['ls', 'df', 'free', 'uptime', 'ps', 'cat', 'grep', 'ss', 'ip', 'systemctl', 'journalctl', 'uname', 'docker', 'top -b -n 1'];
15
105
 
16
- // Logique simplifiée pour test
17
- node.status({fill:"yellow", text: "Appel AI..."});
18
- try {
19
- // Votre logique axios ici...
20
- node.status({fill:"green", text:"OK"});
21
- } catch (err) {
22
- node.status({fill:"red", text:"Erreur"});
106
+ if (cmd && cmd !== "none" && safeCommands.some(w => cmd.includes(w.split(' ')[0]))) {
107
+ exec(cmd, { timeout: 12000 }, (err, stdout, stderr) => {
108
+ let res = (stdout || stderr || "Commande exécutée (sans retour)").substring(0, 1000);
109
+ node.send({ payload: { chatId, type: "message", content: `📟 <b>Terminal (${cmd})</b> :\n<pre>${res}</pre>`, options: { parse_mode: "HTML" } } });
110
+
111
+ // On renvoie le résultat à l'IA pour l'étape suivante
112
+ setTimeout(() => {
113
+ node.emit("input", {
114
+ payload: { chatId, content: `RÉSULTAT DE ${cmd} :\n${res}` },
115
+ loopCount: loopCount + 1
116
+ });
117
+ }, 1000);
118
+ });
23
119
  }
120
+
121
+ history.push({ role: "assistant", content: aiData.speech });
122
+ node.context().set('history', history.slice(-10));
123
+ node.status({fill:"green", text:`Répondu par ${engineUsed}`});
24
124
  });
25
125
  }
26
126
  RED.nodes.registerType('linux-copilot', LinuxCopilotNode, {
27
- credentials: { geminiKey: {type:"password"}, openrouterKey: {type:"password"}, deepseekKey: {type:"password"} }
127
+ credentials: {
128
+ geminiKey: {type:"password"},
129
+ openrouterKey: {type:"password"},
130
+ deepseekKey: {type:"password"}
131
+ }
28
132
  });
29
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-linux-copilot",
3
- "version": "1.2.10",
3
+ "version": "1.2.12",
4
4
  "description": "AI Linux Copilot",
5
5
  "main": "linux-copilot.js",
6
6
  "node-red": {