---
tags: [note, vps, system, architecture, nodered, opencode, Automatisation]
type: "note"
nature: documentation # Options : portrait, lieu, info, concept, script, process, tuto, none
ressource: true
univers: PERSO
projects:
  - "[[03_PROJETS/VPS-IA]]"
date: "[[01_JOURNAL/2026/2026-09-25]]"
priority: 10
description: "Flux Node-RED orchestrant OpenCode via FluffyChat : sessions persistantes entre les messages."
---
# Utilisation de OpenCode via Node Red sur VPS OVH Perso

Ce #tuto fait suite à [[04_NOTES/VPS-IA_Installation-OpenCode|la configuration de OpenCode sur le VPS OVH Perso]] en lien avec Node Red.  

L’objectif ici est de construire un flux permettant de communiquer avec OpenCode dans FluffyChat en passant par Node-Red comme orchestrateur.

Plusieurs [[04_NOTES/VPS-IA_UseCases|cas d’usage]] ont été imaginés.

Une partie des éléments suivants sont issus à un échange avec Gemini et Claude.ai

Un point clé pour l'utilisation de Node-Red comme orchestrateur pour une discussion avec OpenCode via FluffyChat est la gestion des sessions :
1. Création à la volée
2. Persistance entre les messages pour assurer la continuité des échanges au sein d'une discussion
3. Suppression en fin de discussion ou a intervalle régulier pour libérer de la mémoire et de la place


## 📐 Vue d'ensemble de la Stack

* **Silverbullet v2 (PKM) :** Source unique de vérité pour les notes, le journal quotidien, ainsi que la configuration des agents, compétences et profils.
* **Node-RED (Orchestrateur) :** Proxy intelligent et moteur de flux d'automatisation.
* **OpenCode Server (LLM) :** Moteur d'IA exécutant les requêtes et les outils MCP.
* **FluffyChat / Matrix (Tuwunel) :** Interface conversationnelle bidirectionnelle.
* **ntfy :** Service de notifications push unidirectionnelles et rapides.

Faire un schéma (cf. projet VPS-IA)

---

## 🏷️ Nomenclature des Mentions Dynamiques (Interface FluffyChat)

Lors de l'envoi d'un message dans FluffyChat, Node-RED intercepte et parse les préfixes pour construire le contexte à la volée depuis l'API local `127.0.0.1:3000/.fs` de Silverbullet :

| Symbole | Type | Emplacement Silverbullet | Rôle |
| :--- | :--- | :--- | :--- |
| `@` | **Agent** | `/_System/Agents/{nom}.md` | Définit le rôle, le ton et le System Prompt. |
| `/` | **Fichier** | `/{chemin}.md` | Injecte le contenu d'une note en contexte. |
| `#` | **Skill** | `/_System/Skills/{nom}.md` | Fournit les instructions spécifiques / étapes à exécuter. |

Par ailleurs, le mot-clé `/clean-session` permet de clore explicitement la session en cours et donc la discussion avec OpenCode.


## 📂 Structure du répertoire Silverbullet pour les contexts, skills et agents

```text
├── 80_CONFIG/
│   ├── Agents/
│   │   ├── planner.md         # Assistant d'organisation
│   │   └── chef.md            # Assistant d'analyse & rédaction
│   ├── Contexts/
│   │   └── UserProfile.md     # Règlements généraux et profil utilisateur
│   └── Skills/
│       ├── priorites.md       # Extraction et formatage des 3 priorités du jour
│       └── grist-query.md     # Compétence nécessitant le MCP Grist
└── ...
```


## 🔌 Gestion Dynamique des MCP (Model Context Protocol)

Afin de préserver la réactivité d'OpenCode et de limiter la consommation de tokens, les serveurs MCP (**Grist**, **Silverbullet**) ne sont **pas chargés systématiquement**.

1.  **Déclaration du besoin dans les Skills :**
    
    Dans le frontmatter YAML d'une compétence (`/_System/Skills/grist-query.md`) :
    
    ```yaml
    ---
    mcp_required:
      - grist
    ---
    ```
    
2.  **Filtrage dynamique par Node-RED :**
    
    Lors de l'appel d'API vers OpenCode (`127.0.0.1:5000/api/...`), Node-RED inspecte le YAML de la skill et alimente le tableau `tools` :
   
    *   **Sans MCP :** `"tools": []` (Exécution purement textuelle, ultra-rapide).     
    *   **Avec MCP :** `"tools": ["grist_*"]` (Activation ciblée du MCP nécessaire).



----
Gestion des sessions OpenCode dans Node-Red:

Pour gérer le cycle de vie des sessions OpenCode sans encombrer la mémoire de votre serveur, l'astuce consiste à utiliser **le stockage de contexte de Node-RED (`flow` ou `global`)** ou à exploiter **les événements de Matrix** (comme le `room_id`).

  

Dans le cadre d'un salon privé Matrix (FluffyChat), le `room_id` est unique et stable. On peut donc associer 1 `room_id` Matrix à 1 `session_id` OpenCode.

  

## 1\. Stratégie de Gestion des Sessions

*   **Création à la volée :** Quand un message arrive depuis FluffyChat, Node-RED vérifie si une session OpenCode existe déjà pour ce salon Matrix. Si ce n'est pas le cas, il en crée une auprès de l'API d'OpenCode et enregistre son ID.
    
      
    
*   **Persistance :** La session reste active pour permettre une discussion fluide et contextuelle (l'IA se rappelle des messages précédents).
    
      
    
*   **Clôture explicite (`/clean_session`) :** Lorsque le mot-clé `/clean_session` est détecté, Node-RED envoie une requête `DELETE` à l'API local d'OpenCode pour supprimer la session du serveur, efface l'ID en mémoire, et confirme la suppression dans FluffyChat.
    
      
    

## 2\. Architecture du Flux Node-RED

```
[ Entrée FluffyChat ]
         │
         ▼
[ Function: Parse & Check Commands ]
         │
         ├─── (Si msg == "/clean_session") ──> [ HTTP DELETE OpenCode Session ] ──> [ Clear Memory ] ──> [ Reponse Matrix "Session fermée" ]
         │
         └─── (Message standard) ──> [ Session Active ? ]
                                            │
                                            ├─── Non ──> [ HTTP POST Create Session ] ──┐
                                            │                                           │
                                            └─── Oui ───────────────────────────────────┴──> [ Fetch SB Files (@, /, #) ] ──> [ HTTP POST Message OpenCode ] ──> [ Reponse Matrix ]
```

## 3\. Implémentation détaillée des nœuds Node-RED

### Étape 1 : Réception et Détection de la commande (`Function Node`)

Ce nœud analyse le message Matrix reçu et vérifie s'il s'agit d'une commande de nettoyage ou d'un message standard.

  

JavaScript

```
const roomId = msg.topic || msg.payload.room_id; // ID du salon Matrix
const text = (msg.payload.content?.body || msg.payload || "").trim();

// Clé de stockage propre au salon Matrix
const sessionKey = `opencode_session_${roomId}`;
const currentSessionId = flow.get(sessionKey);

if (text === "/clean_session") {
    if (!currentSessionId) {
        // Aucune session à fermer
        return [null, { payload: { roomId: roomId, message: "⚠️ Aucune session OpenCode active pour ce salon." } }];
    }
    
    // Sortie 1 : Action de suppression de la session
    return [{
        method: "DELETE",
        url: `http://127.0.0.1:5000/api/sessions/${currentSessionId}`,
        roomId: roomId,
        sessionKey: sessionKey
    }, null];
}

// Message normal : On transmet pour traitement IA
msg.sessionInfo = {
    roomId: roomId,
    sessionKey: sessionKey,
    sessionId: currentSessionId,
    rawText: text
};

// Sortie 2 : Traitement normal
return [null, msg];
```

*   Connectez la **Sortie 1** au nœud `HTTP Request` (Suppression) et la **Sortie 2** à l'étape suivante (Vérification/Création).
    
      
    

### Étape 2A : Branche de Clôture (`/clean_session`)

1.  **Nœud `HTTP Request` (DELETE) :**
    
      
    *   **Method :** `DELETE` (défini par `msg.method`)
        
          
        
    *   **URL :** `[http://127.0.0.1:5000/api/sessions/](http://127.0.0.1:5000/api/sessions/){{{sessionId}}}` (défini par `msg.url`)
        
          
        
2.  **Nœud `Function` (Clear Memory & Format Response) :**
    
      
    
    JavaScript
    
    ```
    // Effacement de la session en mémoire Node-RED
    flow.set(msg.sessionKey, null);
    
    return {
        payload: {
            roomId: msg.roomId,
            body: "🗑️ La session OpenCode a été supprimée avec succès. La prochaine interaction démarrera sur une session neuve."
        }
    };
    ```
    
3.  **Nœud `Matrix Send Message` :** Envoie la confirmation dans FluffyChat.
    
      
    

### Étape 2B : Branche Traitement de Discussion & Création à la volée

1.  **Nœud `Function` (Check or Create Session Payload) :**
    
      
    
    JavaScript
    
    ```
    const sessionInfo = msg.sessionInfo;
    
    // Si pas de session active, on prépare la requête de création
    if (!sessionInfo.sessionId) {
        msg.needNewSession = true;
        msg.method = "POST";
        msg.url = "http://127.0.0.1:5000/api/sessions";
        msg.payload = {
            title: `FluffyChat - Salon ${sessionInfo.roomId.substring(0, 8)}`
        };
    } else {
        msg.needNewSession = false;
    }
    
    return msg;
    ```
    
2.  **Nœud `Switch Node` :**
    
      
    *   Règle 1 : Si `msg.needNewSession == true` $\\rightarrow$ Nœud `HTTP Request` (POST `/api/sessions`).
        
          
        
    *   Règle 2 : Si `msg.needNewSession == false` $\\rightarrow$ Passe directement à l'extraction des fichiers Silverbullet (`@`, `/`, `#`).
        
          
        
3.  **Nœud `Function` (Save New Session ID) :** *(Placé juste après la création HTTP de la nouvelle session)*
    
      
    
    JavaScript
    
    ```
    const newSessionId = msg.payload.id || msg.payload.session_id;
    
    // On sauvegarde le nouvel ID de session pour ce salon Matrix
    flow.set(msg.sessionInfo.sessionKey, newSessionId);
    msg.sessionInfo.sessionId = newSessionId;
    
    return msg;
    ```
    

### Étape 3 : Envoi du message dans la Session OpenCode

Une fois les fichiers Silverbullet (`@agent`, `/fichier`, `#skill`) récupérés et compilés par le parser Regex :

  

1.  **Nœud `Function` (Build OpenCode Message Payload) :**
    
      
    
    JavaScript
    
    ```
    const sessionId = msg.sessionInfo.sessionId;
    
    msg.method = "POST";
    msg.url = `http://127.0.0.1:5000/api/sessions/${sessionId}/messages`;
    
    msg.payload = {
        system_prompt: msg.compiledSystemPrompt,
        message: msg.extractedUserPrompt,
        tools: msg.requiredTools || [] // Charge les MCP uniquement si nécessaire
    };
    
    return msg;
    ```
    
2.  **Nœud `HTTP Request` :** Exécute le `POST` local vers OpenCode.
    
      
    
3.  **Nœud `Matrix Send Message` :** Renvoie la réponse du LLM dans FluffyChat.
    
      
    

## 💡 Conseil bonus : Nettoyage Automatique (TTL / Garbage Collector)

Pour éviter de conserver indéfiniment en mémoire des sessions de salons abandonnés ou inutilisés depuis plusieurs jours, vous pouvez ajouter un **Cron quotidien à 03h00** dans Node-RED :

  

JavaScript

```
// Parcours des clés du stockage de flux
const keys = flow.keys();

keys.forEach(key => {
    if (key.startsWith("opencode_session_")) {
        const sessionId = flow.get(key);
        
        // Requête HTTP DELETE automatique vers 127.0.0.1:5000/api/sessions/${sessionId}
        // ...
        
        flow.set(key, null); // Libère la mémoire
    }
});
```

Cette approche vous offre un contrôle complet : la persistance au fil de la discussion, la remise à zéro manuelle instantanée via `/clean_session`, et une purge automatique la nuit.

---





## 🔄 Flux Principaux

### 1\. Synthèse Matinale Automatique (Cron 07h00)

1.  **Trigger :** Cron à 07h00 dans Node-RED.
    
      
    
2.  **Fetch SB :** Récupération en boucle via `127.0.0.1:3000/.fs` de `@planner`, `#priorites`, `/Daily/Hier.md` et `UserProfile.md`.    
3.  **Session OpenCode :** Création d'une session, envoi du prompt consolidé.    
4.  **Mise à jour :** Écriture du résultat via `PUT /.fs/Daily/Aujourdhui.md`.    
5.  **Alerte :** Push vers `ntfy` avec lien direct vers la note.

### 2\. Interaction & Capture via FluffyChat

```text
[ FluffyChat ] ──( Message )──> [ Node-RED Proxy ]
                                        │
                                        ├─► Parse @agent, /fichier, #skill
                                        ├─► GET 127.0.0.1:3000/.fs/...
                                        ├─► Extrait YAML 'mcp_required'
                                        ▼
                                [ OpenCode API Local ]
                                        │ (Traitement avec/sans MCP)
                                        ▼
[ FluffyChat / ntfy ] <──( Réponse )───┘
```


## 🔒 Principes d'Optimisation & Sécurité

1.  **Loopback local (`127.0.0.1`) :** Communication inter-applications directe sans passer par le reverse-proxy Nginx ni le réseau externe.    
2.  **API `/.fs` Silverbullet :** Isolation complète des processus YunoHost sans conflit de permissions de fichiers (`chown`/`chmod`).    
3.  **Sessions OpenCode Volatiles :** Suppression des sessions éphémères en fin de traitement (ou réutilisation du `session_id` pour la journée).


## Détail du flux pour générer un prompt OpenCode dans Node Red

**Principe**

```mermaid
flowchart LR
    N-01(NODE 01 - Check session and get parameters)
    N-02(NODE 02 - Switch get session ID)
    N-03(NODE 03 - HTTP Request session ID)
    N-04(NODE 04 - Define API AI prompt parameters)
    N-05(NODE 05 - HTTP Request API AI prompt)
    N-06(NODE 06 - Debug)

    N-01 --> N-02 --> |True| N-03 --> N-04
    N-02 --> |False| N-04
    N-04 --> N-05 --> N-06
```

Description : 
* Node 1 : function - vérifier si une session existe et préaparer si nécessaire la requête de demande de session OpenCode
* Node 2 : switch - si msg.getSessionId == True => node 4, sinon vers node 3 
* Node 3 : http request - appeler l’API AI de OpenCode pour générer un ID de session => node 4
* Node 4 : function - préparer la requête de prompt OpenCode => node 5
* Node 5 : http request - appeler l’API AI de OpenCode avec le prompt
* Node 6 : debug 


### Node 1 : Créer une session si elle n’existe pas (Function)

Ce node permet de :
1. Vérifie si une session OpenCode existe
2. Si aucune session existe (`!sessionId`) ou qu’une demande explicite de génération d’une nouvelle session transmise (`msg.opencode_session_clear == true`), une nouvelle session est initialisée dans le dossier cible et transmise au noeud suivant (HTTP Request).

La variable `msg.opencode_session_clear` permet d’appeler le noeud pour réinitialiser la session.
Généralement, le noeud reçoit aussi en entrée un variable `msg.aiMessage` qui n’est pas modifiée.

```javascript
const dir = env.get("OPENCODE_DIR") || "/var/lib/opencode";
const username = env.get("OPENCODE_SERVER_USER");
const password = env.get("OPENCODE_SERVER_PASSWORD");
const authHash = Buffer.from(`${user}:${password}`).toString("base64");
const url = env.get("OPENCODE_SERVER_URL");
const endpoint = "/api/session";
const sessionId = msg.sessionId || flow.get('opencode_session_id');

msg.getSessionId = False;

if (!sessionId or msg.opencode_session_clear) {
  msg.getSessionId = True;
  msg.method = "POST"; 
  msg.url = `${url}${endpoint}`;
  msg.headers = {
      "Authorization": `Basic ${authHash}`,
      "Content-Type": "application/json",
      "Accept": "application/json",
      "x-opencode-directory": dir
  };
  
  // Corps vide ou paramètres initiaux de session
  msg.payload = {};  
}

msg.sessionId = sessionId;
return msg;
```


### Node 2 : Router le flux selon si un ID de session OpenCode doit être demandé (Switch)

Node de type `Switch` avec 2 sorties
1. `msg.getSessionId == True`
2. `msg.getSessionId == False`


### Node 3 : Requête HTTP de récupération d’un ID de session OpenCode un prompt dans la session (HTTP Request)

Node HTTP Request simple utilisant les paramètres transmis par le node 1.

**Résultat de la réponse HTTP :** le serveur renvoie un objet JSON contenant l'identifiant de session `sessionID` (ex. `{"id": "ses_12345abc"}`).


### Node 4 : Préparer les paramètres d’appel de l’API IA d’OpenCode (Function)

Le node récupère l’ID de session, défini les paramètres et envoie la requête IA dans le node HTTP Request suivant.

```javascript
const dir = env.get("OPENCODE_DIR") || "/var/lib/opencode";
const username = env.get("OPENCODE_SERVER_USER");
const password = env.get("OPENCODE_SERVER_PASSWORD");
const authHash = Buffer.from(`${user}:${password}`).toString("base64");
const url = env.get("OPENCODE_SERVER_URL");
const sessionId = msg.payload.id ? flow.set("opencode_session_id", sessionId) : flow.get("opencode_session_id");
const endpoint = `/api/session/${sessionId}/message`;

msg.method = "POST"; 
msg.url = `${url}${endpoint}`;
msg.headers = {
    "Authorization": `Basic ${authHash}`,
    "Content-Type": "application/json",
    "Accept": "application/json",
    "x-opencode-directory": dir
};

// Payload de la requête IA
msg.payload = {
    "parts": [
        {
            "type": "text",
            "text": "Analyse l'utilisation de la RAM et de l'espace disque du VPS."
        }
    ]
};

return msg;
```


### Node 5 : Requête HTTP d’envoi du prompt à OpenCode

Node HTTP Request simple utilisant les paramètres transmis par le node 1.

**Résultat de la réponse HTTP :** le serveur renvoie un objet JSON contenant la réponse de l’API.


## Flux avancé en injectant une page SB comme skill ou contexte

### Principe

Ce flux réutilise le sous-flux précédent et le complète avec un appel à Silverbullet préalablement.

```mermaid
flowchart TD
    N-A1(NODE A1 - Define SB HTTP Request)
    N-A2(NODE A2 - HTTP SB API Request)
    N-B1(NODE B1 - Check OC session and get parameters)
    N-B2(NODE B2 - Switch get OC session ID)
    N-B3(NODE B3 - HTTP Request OC session ID)
    N-B4(NODE B4 - Define API OC prompt parameters)
    N-B5(NODE B5 - HTTP Request API OC prompt)
    N-B6(NODE B6 - Debug)

    N-A1 --> N-A2 --> N-B1
    N-B1 --> N-B2 --> |True| N-B3 --> N-B4
    N-B2 --> |False| N-B4
    N-B4 --> N-B5 --> N-B6
```

### Noeud 1 : Paramètres Silverbullet pour récupérer la page (`Function 1`)

L'API de Silverbullet sert le Markdown brut directement sur la route `/.fs/NOM_PAGE.md`.
Configuration du nœud **Function** (à placer avant un nœud **HTTP Request**) :

```javascript
// URL de Silverbullet (ajuste le port si différent, ex: 3000)
const silverbulletUrl = "http://127.0.0.1:3000";
const pageName = "CONTEXT.md"; 

msg.method = "GET";
msg.url = `${silverbulletUrl}/.fs/${pageName}`;

// Si ton Silverbullet requiert un jeton d'accès
const sbToken = env.get("SILVERBULLET_AUTH_TOKEN");
msg.headers = {
    "Accept": "text/markdown, text/plain",
    "Authorization": sbToken ? `Bearer ${sbToken}` : ""
};

return msg;
```


### Noeud 2 : Récupération de la page Silverbullet via HTTP Request (`HTTP Request 1`)

Créer un noeud HTTP Request suivant :
* URL : vide pour utiliser la valeur msg.url
* Method : “set by msg.method”
* Return : ”a UTF-8 string”

Le corps msg.payload retourné contiendra le texte brut Markdown de la note de la forme `msg.payload.parts = [{}, {}, ...]`.

Il peut être utile ici d’ajouter un noeud intermédiaire pour attribuer `msg.payload` à une autre variable comme `msg.sbContext`

### Noeud 3 : Injection du fichier Markdown dans le prompt OpenCode (`Function 2`)

Placé juste après le premier nœud HTTP Request, ce nœud construit le payload destiné à OpenCode en intégrant le contenu Markdown comme contexte système ou skill.

```javascript
// 1. Récupération du Markdown brut retourné par Silverbullet
const markdownContext = msg.sbContext;

// 2. Préparation des identifiants OpenCode
const dir = env.get("OPENCODE_DIR") || "/var/lib/opencode";
const username = env.get("OPENCODE_SERVER_USER");
const password = env.get("OPENCODE_SERVER_PASSWORD");
const authHash = Buffer.from(`${user}:${password}`).toString("base64");
const url = env.get("OPENCODE_SERVER_URL");
const sessionId = msg.payload.id || flow.get("opencode_session_id");
const endpoint = `/api/session/${sessionId}/message`;

// 3. Question / Instruction de l'utilisateur
// A termes proviedra par exemple de Tuwunel/FluffyChat
const userQuery = "Analyse l'état de mon serveur en te basant sur mes consignes.";

// 4. Construction de la requête pour OpenCode
msg.method = "POST";
msg.url = `${url}${endpoit}`;

msg.headers = {
    "Authorization": `Basic ${authHash}`,
    "Content-Type": "application/json",
    "Accept": "application/json",
    "x-opencode-directory": dir
};

msg.payload = {
    "parts": [
        {
            "type": "text",
            "text": `Voici les instructions et le contexte métier à respecter absolument :\n\n--- DEBUT CONTEXTE ---\n${markdownContext}\n--- FIN CONTEXTE ---\n\nDemande utilisateur : ${userQuery}`
        }
    ]
};

return msg;
```

Les noeuds suivants sont identiques au flux précédents (Node N-B5 sur le schéma).
