node-red-contrib-linux-copilot 1.2.9 → 1.2.11
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/linux-copilot.html +22 -31
- package/linux-copilot.js +86 -8
- package/package.json +1 -1
package/linux-copilot.html
CHANGED
|
@@ -1,59 +1,50 @@
|
|
|
1
1
|
<script type="text/javascript">
|
|
2
2
|
RED.nodes.registerType('linux-copilot',{
|
|
3
|
-
category: '
|
|
4
|
-
color: '#
|
|
3
|
+
category: 'advanced',
|
|
4
|
+
color: '#E2D96E',
|
|
5
5
|
defaults: {
|
|
6
6
|
name: {value:""},
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
prioDS: {value:"1"},
|
|
8
|
+
prioOR: {value:"2"},
|
|
9
|
+
prioGEM: {value:"3"},
|
|
10
|
+
chatId: {value:"1457427557"}
|
|
11
11
|
},
|
|
12
12
|
credentials: {
|
|
13
|
-
|
|
13
|
+
deepseekKey: {type:"password"},
|
|
14
14
|
openrouterKey: {type:"password"},
|
|
15
|
-
|
|
15
|
+
geminiKey: {type:"password"}
|
|
16
16
|
},
|
|
17
17
|
inputs:1,
|
|
18
18
|
outputs:1,
|
|
19
|
-
icon: "
|
|
20
|
-
label: function() { return this.name||"linux-copilot"; }
|
|
19
|
+
icon: "font-awesome/fa-terminal",
|
|
20
|
+
label: function() { return this.name || "linux-copilot"; }
|
|
21
21
|
});
|
|
22
22
|
</script>
|
|
23
23
|
|
|
24
24
|
<script type="text/html" data-template-name="linux-copilot">
|
|
25
25
|
<div class="form-row">
|
|
26
26
|
<label for="node-input-name"><i class="fa fa-tag"></i> Nom</label>
|
|
27
|
-
<input type="text" id="node-input-name" placeholder="
|
|
27
|
+
<input type="text" id="node-input-name" placeholder="Linux Copilot">
|
|
28
28
|
</div>
|
|
29
29
|
<div class="form-row">
|
|
30
30
|
<label for="node-input-chatId"><i class="fa fa-comment"></i> Chat ID</label>
|
|
31
|
-
<input type="text" id="node-input-chatId"
|
|
31
|
+
<input type="text" id="node-input-chatId">
|
|
32
32
|
</div>
|
|
33
33
|
<hr>
|
|
34
|
-
<h4>
|
|
34
|
+
<h4>Priorités (1 = Premier utilisé)</h4>
|
|
35
35
|
<div class="form-row">
|
|
36
|
-
<label for="node-input-
|
|
37
|
-
<input type="
|
|
36
|
+
<label for="node-input-prioDS">DeepSeek</label>
|
|
37
|
+
<input type="number" id="node-input-prioDS" style="width:50px" min="1">
|
|
38
|
+
<input type="password" id="node-input-deepseekKey" placeholder="Clé API" style="width:200px">
|
|
38
39
|
</div>
|
|
39
40
|
<div class="form-row">
|
|
40
|
-
<label for="node-input-
|
|
41
|
-
<input type="number" id="node-input-
|
|
41
|
+
<label for="node-input-prioOR">OpenRouter</label>
|
|
42
|
+
<input type="number" id="node-input-prioOR" style="width:50px" min="1">
|
|
43
|
+
<input type="password" id="node-input-openrouterKey" placeholder="Clé API" style="width:200px">
|
|
42
44
|
</div>
|
|
43
45
|
<div class="form-row">
|
|
44
|
-
<label for="node-input-
|
|
45
|
-
<input type="
|
|
46
|
-
|
|
47
|
-
<div class="form-row">
|
|
48
|
-
<label for="node-input-prioOR">Prio OpenRouter</label>
|
|
49
|
-
<input type="number" id="node-input-prioOR" style="width:70px">
|
|
50
|
-
</div>
|
|
51
|
-
<div class="form-row">
|
|
52
|
-
<label for="node-input-geminiKey"><i class="fa fa-key"></i> Gemini Key</label>
|
|
53
|
-
<input type="password" id="node-input-geminiKey">
|
|
54
|
-
</div>
|
|
55
|
-
<div class="form-row">
|
|
56
|
-
<label for="node-input-prioGEM">Prio Gemini</label>
|
|
57
|
-
<input type="number" id="node-input-prioGEM" style="width:70px">
|
|
46
|
+
<label for="node-input-prioGEM">Gemini</label>
|
|
47
|
+
<input type="number" id="node-input-prioGEM" style="width:50px" min="1">
|
|
48
|
+
<input type="password" id="node-input-geminiKey" placeholder="Clé API" style="width:200px">
|
|
58
49
|
</div>
|
|
59
50
|
</script>
|
package/linux-copilot.js
CHANGED
|
@@ -7,20 +7,98 @@ module.exports = function(RED) {
|
|
|
7
7
|
const node = this;
|
|
8
8
|
|
|
9
9
|
const omniPrompt = `Tu es un Expert Linux SRE polyglotte.
|
|
10
|
-
|
|
10
|
+
RÈGLES CRUCIALES :
|
|
11
|
+
1. LANGUE : Détecte la langue de l'utilisateur. Tu DOIS répondre EXCLUSIVEMENT dans cette langue (si on te parle en Français, réponds en Français; si c'est en Espagnol, réponds en Espagnol).
|
|
12
|
+
2. ANALYSE : Analyse brièvement les résultats techniques.
|
|
13
|
+
3. ACTION : Propose la commande suivante pour continuer le diagnostic.
|
|
14
|
+
4. FORMAT JSON : {"speech": "ton explication dans la langue de l'utilisateur", "cmd": "commande linux ou none"}`;
|
|
15
|
+
|
|
16
|
+
const formatHistory = (history) => history.map(h => ({
|
|
17
|
+
role: h.role === "model" ? "assistant" : h.role,
|
|
18
|
+
content: String(h.content).substring(0, 800)
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const engines = {
|
|
22
|
+
gemini: async (history, key) => {
|
|
23
|
+
const res = await axios.post(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${key.trim()}`, {
|
|
24
|
+
contents: history.map(h => ({ role: h.role === "assistant" ? "model" : h.role, parts: [{ text: h.content }] })),
|
|
25
|
+
system_instruction: { parts: [{ text: omniPrompt }] },
|
|
26
|
+
generationConfig: { responseMimeType: "application/json" }
|
|
27
|
+
}, { timeout: 15000 });
|
|
28
|
+
return JSON.parse(res.data.candidates[0].content.parts[0].text);
|
|
29
|
+
},
|
|
30
|
+
openrouter: async (history, key) => {
|
|
31
|
+
const res = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
|
|
32
|
+
model: "meta-llama/llama-3.3-70b-instruct:free",
|
|
33
|
+
messages: [{ role: 'system', content: omniPrompt }, ...formatHistory(history)]
|
|
34
|
+
}, { headers: { 'Authorization': `Bearer ${key.trim()}`, 'Content-Type': 'application/json' }, timeout: 25000 });
|
|
35
|
+
const match = res.data.choices[0].message.content.match(/\{[\s\S]*\}/);
|
|
36
|
+
return JSON.parse(match ? match[0] : res.data.choices[0].message.content);
|
|
37
|
+
},
|
|
38
|
+
deepseek: async (history, key) => {
|
|
39
|
+
const res = await axios.post('https://api.deepseek.com/chat/completions', {
|
|
40
|
+
model: "deepseek-chat",
|
|
41
|
+
messages: [{ role: 'system', content: omniPrompt }, ...formatHistory(history)],
|
|
42
|
+
response_format: { type: 'json_object' }
|
|
43
|
+
}, { headers: { 'Authorization': `Bearer ${key.trim()}` }, timeout: 20000 });
|
|
44
|
+
return JSON.parse(res.data.choices[0].message.content);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
11
47
|
|
|
12
48
|
node.on('input', async function(msg) {
|
|
13
49
|
const chatId = msg.payload.chatId || config.chatId || "1457427557";
|
|
50
|
+
let userText = msg.payload.content || (typeof msg.payload === 'string' ? msg.payload : "");
|
|
51
|
+
let loopCount = msg.loopCount || 0;
|
|
52
|
+
|
|
53
|
+
if (loopCount > 4) return node.status({fill:"blue", text:"Fin de séquence"});
|
|
54
|
+
|
|
14
55
|
let history = node.context().get('history') || [];
|
|
56
|
+
if (userText.toLowerCase() === "reset") {
|
|
57
|
+
node.context().set('history', []);
|
|
58
|
+
return node.send({ payload: { chatId, type: "message", content: "♻️ Historique effacé." } });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!userText) return;
|
|
62
|
+
history.push({ role: "user", content: userText });
|
|
63
|
+
|
|
64
|
+
let queue = [
|
|
65
|
+
{ id: 'deepseek', p: parseInt(config.prioDS) || 1, k: node.credentials.deepseekKey, n: "DeepSeek" },
|
|
66
|
+
{ id: 'openrouter', p: parseInt(config.prioOR) || 2, k: node.credentials.openrouterKey, n: "OpenRouter" },
|
|
67
|
+
{ id: 'gemini', p: parseInt(config.prioGEM) || 3, k: node.credentials.geminiKey, n: "Gemini" }
|
|
68
|
+
].filter(q => q.k).sort((a, b) => a.p - b.p);
|
|
69
|
+
|
|
70
|
+
let aiData = null;
|
|
71
|
+
let engineUsed = "";
|
|
72
|
+
|
|
73
|
+
for (let e of queue) {
|
|
74
|
+
try {
|
|
75
|
+
node.status({fill:"yellow", text: `Appel ${e.n}...`});
|
|
76
|
+
aiData = await engines[e.id](history, e.k);
|
|
77
|
+
if (aiData && (aiData.speech || aiData.cmd)) { engineUsed = e.n; break; }
|
|
78
|
+
} catch (err) { node.warn(`Échec ${e.n}`); }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!aiData) return node.status({fill:"red", text:"Erreur API"});
|
|
82
|
+
|
|
83
|
+
node.send({ payload: { chatId, type: "message", content: `🤖 <b>${engineUsed}</b> : ${aiData.speech}`, options: { parse_mode: "HTML" } } });
|
|
84
|
+
|
|
85
|
+
let cmd = (aiData.cmd || "").trim();
|
|
86
|
+
const safeWords = ['ls', 'df', 'free', 'uptime', 'top', 'ps', 'cat', 'grep', 'iostat', 'netstat', 'ss', 'ip', 'systemctl', 'journalctl'];
|
|
15
87
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
88
|
+
if (cmd && cmd !== "none" && safeWords.some(w => cmd.includes(w))) {
|
|
89
|
+
if (cmd.startsWith("top") && !cmd.includes("-b")) cmd = "top -b -n 1 | head -n 12";
|
|
90
|
+
exec(cmd, { timeout: 10000 }, (err, stdout, stderr) => {
|
|
91
|
+
let res = (stdout || stderr || "OK").substring(0, 800);
|
|
92
|
+
node.send({ payload: { chatId, type: "message", content: `📟 <b>Terminal (${cmd})</b> :\n<pre>${res}</pre>`, options: { parse_mode: "HTML" } } });
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
node.emit("input", { payload: { chatId, content: `RÉSULTAT ${cmd} :\n${res}` }, loopCount: loopCount + 1 });
|
|
95
|
+
}, 1200);
|
|
96
|
+
});
|
|
23
97
|
}
|
|
98
|
+
|
|
99
|
+
history.push({ role: "assistant", content: aiData.speech || "Action" });
|
|
100
|
+
node.context().set('history', history.slice(-10));
|
|
101
|
+
node.status({fill:"green", text:`Réponse via ${engineUsed}`});
|
|
24
102
|
});
|
|
25
103
|
}
|
|
26
104
|
RED.nodes.registerType('linux-copilot', LinuxCopilotNode, {
|