SAP Extension Creator
Aperçu
Créez des extensions SAP Super Agent Party — des paquets autonomes qui enrichissent la plateforme avec une interface utilisateur de chat personnalisée et des outils. Deux modes sont supportés :
- Extension statique : Interface frontend HTML/CSS/JS pure, servie directement par SAP depuis le dossier d'extension
- Extension Node.js : Full-stack avec backend Express, géré automatiquement par SAP (
npm install+node index.js <port>)
Les deux modes supportent l'enregistrement d'outils MCP (le message de protocole register_node_extension_mcp fonctionne pour TOUTE extension via WebSocket, malgré le « node » dans son nom).
Arbre de décision rapide
L'utilisateur veut créer une extension ?
├─ Besoin d'interface seulement (chat, affichage, interactions simples) ? → Extension statique
└─ Besoin de logique backend (appels API, BD, traitement de fichiers) ? → Extension Node.js
Fichiers principaux que chaque extension doit avoir
| Fichier | Requis | Objectif |
|---|---|---|
package.json |
✅ | Métadonnées, dépendances, config de fenêtre |
index.html |
✅ | Interface principale (page HTML complète, app mono-fichier) |
index.js |
Node seulement | Point d'entrée Node.js |
node_modules/ |
Node seulement | Installé automatiquement par SAP via npm install |
Flux de travail
Étape 1 : Rassembler les exigences
Posez à l'utilisateur :
- Nom de l'extension ? (format avec tirets, ex.
ma-widget-meteo) - Description ? (une phrase)
- Statique ou Node.js ? (Node.js seulement si logique backend/code côté serveur requis)
- Pour Node.js : quelles dépendances npm ?
- Doit-elle enregistrer des outils personnalisés pour l'IA ? (fonctionne dans les deux modes via WebSocket MCP)
- URL du dépôt GitHub ? (optionnel, pour les mises à jour)
- Fenêtre transparente ? (sans cadre, toujours au premier plan — pour mini-widgets comme les contrôleurs musicaux)
- Taille par défaut de la fenêtre ? (largeur/hauteur en pixels)
Étape 2 : Scaffolder l'extension
Utilisez les modèles dans assets/ comme points de départ :
- Statique : Copiez
assets/static-template/ - Node.js : Copiez
assets/node-template/
Créez le répertoire d'extension sous l'espace de travail (l'utilisateur l'installera plus tard dans le dossier extensions/ de SAP).
Étape 3 : Écrire package.json
Consultez references/package-json-spec.md pour la référence complète des champs. Minimum :
{
"name": "ma-extension",
"version": "1.0.0",
"description": "Ce qu'elle fait",
"author": "votre-nom",
"repository": "https://github.com/user/repo",
"backupRepository": "https://gitee.com/user/repo",
"category": "Tools"
}
Pour les extensions Node.js, incluez aussi :
{
"main": "index.js",
"nodePort": 0,
"dependencies": { "express": "^5.1.0" }
}
Pour les widgets transparents/sans cadre (ex. mini contrôleurs musicaux, panneaux flottants) :
{
"transparent": true,
"width": 280,
"height": 80
}
Quand transparent: true, SAP crée une fenêtre sans cadre, transparente et toujours au premier plan (voir le gestionnaire open-extension-window dans main.js). Utilisez ceci pour les widgets de superposition compacts.
Étape 4 : Écrire index.html
La page HTML est rendue à l'intérieur d'une BrowserWindow Electron (directement ou via une iframe). Motifs clés :
- Autonome : L'extension est un fichier HTML unique avec tout le CSS/JS intégré ou chargé depuis un CDN. Pour les extensions Node.js, les ressources statiques sont servies depuis le répertoire d'extension.
- Font Awesome : Utilisez le CDN pour assurer un chargement fiable dans les deux modes :
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">Évitez les chemins relatifs comme
../../fontawesome/— ils peuvent fonctionner pour les extensions statiques mais cassent pour les extensions Node.js (chemins de service différents). - Mode sombre/clair : Toujours supporter les deux (voir la section « Theme & i18n » ci-dessous).
- i18n (chinois/anglais) : Toujours supporter une interface bilingue (voir la section « Theme & i18n » ci-dessous).
- Connexion WebSocket : Connectez-vous à
ws://host/wspour la messagerie et MCP. - ID de l'extension : Parsez
window.location.pathnamepour/extensions/{ext_id}/. - Rendu des messages : Écoutez les événements
messages_updateetbroadcast_messages. - Envoyer l'entrée utilisateur : Envoyez
set_user_inputpuistrigger_send_message.
Étape 5 : Écrire index.js (Node.js seulement)
Consultez references/node-entry-spec.md pour le protocole complet. Le point d'entrée :
- Reçoit un numéro de port via
process.argv[2] - Démarre un serveur Express sur ce port à
127.0.0.1 - Sert les fichiers statiques depuis son propre répertoire
- Expose un endpoint
/healthpour les vérifications de disponibilité - SAP fait un reverse-proxy des requêtes vers l'extension
Étape 6 : Implémenter l'enregistrement d'outils (optionnel, fonctionne dans les deux modes)
Les extensions peuvent enregistrer des outils que l'agent IA peut appeler — via WebSocket dans le frontend (statique et Node.js). Le cycle de vie MCP a trois étapes obligatoires :
DÉMARRAGE → ws.onopen → registerMcpTools()
RUNTIME → ws.onmessage → handleMcpCall() quand l'IA appelle un outil
ARRÊT → window.beforeunload → unregisterMcpTools()
① Enregistrer au démarrage — toujours dans ws.onopen, en utilisant une fonction dédiée :
function registerMcpTools() {
getExtId();
ws.send(JSON.stringify({
type: 'register_node_extension_mcp',
data: {
ext_id: MY_EXT_ID,
tools: [{
name: `${MY_EXT_ID}_my_tool`,
description: 'Ce que cet outil fait (utilisez la langue de l\'utilisateur)',
parameters: {
type: 'object',
properties: {
param1: { type: 'string', description: '...' }
},
required: ['param1']
}
}]
}
}));
}
② Gérer les appels d'outils — l'agent IA appelle votre outil :
async function handleMcpCall(data) {
const { ext_id, tool_name, tool_params, call_id } = data;
if (ext_id !== MY_EXT_ID && !tool_name.includes(MY_EXT_ID)) return;
// ... exécutez la logique, puis :
ws.send(JSON.stringify({
type: 'mcp_tool_result',
data: { call_id, result: 'output' }
}));
}
③ Désenregistrer à l'arrêt — DOIT envoyer unregister_node_extension_mcp avant la fermeture de la fenêtre :
function unregisterMcpTools() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'unregister_node_extension_mcp', data: { ext_id: MY_EXT_ID } }));
}
}
window.addEventListener('beforeunload', () => { unregisterMcpTools(); });
Règle clé : L'enregistrement et le désenregistrement DOIVENT être dans des fonctions nommées séparées (registerMcpTools / unregisterMcpTools), PAS du code en ligne. Cela rend le cycle de vie explicite et facile à comprendre pour l'IA.
Si une extension n'a pas d'outils MCP, les trois fonctions peuvent être supprimées.
Consultez sap-lx-music/index.html pour un exemple complet d'implémentation MCP réelle (extension statique avec 12+ outils enregistrés).
Theme & i18n (Mode sombre/clair + Bilingue)
Chaque extension doit supporter le mode sombre/clair et l'interface bilingue chinois/anglais. Ne codez pas en dur un seul schéma de couleurs de thème — utilisez des variables CSS pour que chaque extension puisse avoir sa propre identité.
Motif des variables CSS
Définissez le thème clair dans :root et remplacez dans body.dark :
:root {
--bg: #ffffff;
--bg-secondary: #f5f5f5;
--text: #333333;
--text-sub: #888888;
--accent: #ec4141; /* couleur de marque propre à l'extension */
--accent-hover: #d73a3a;
--border: rgba(0,0,0,0.08);
--transition: 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
--font: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", sans-serif;
}
body.dark {
--bg: #2b2b2b;
--bg-secondary: #222222;
--text: #e0e0e0;
--text-sub: #888888;
--border: rgba(255,255,255,0.06);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%; font-family: var(--font);
background: var(--bg); color: var(--text);
transition: background var(--transition);
}
Basculement de mode sombre
function initTheme() {
const saved = localStorage.getItem('myext_dark');
if (saved === 'dark' || (!saved && matchMedia('(prefers-color-scheme:dark)').matches)) {
document.body.classList.add('dark');
}
}
function toggleDarkMode() {
const isDark = document.body.classList.toggle('dark');
localStorage.setItem('myext_dark', isDark ? 'dark' : 'light');
}
Motif i18n
const i18n = {
zh: {
welcome: '欢迎使用我的扩展',
send: '发送',
// ... toutes les chaînes UI
},
en: {
welcome: 'Welcome to My Extension',
send: 'Send',
// ...
}
};
let lang = localStorage.getItem('myext_lang') || 'zh';
function t(k) { return i18n[lang]?.[k] || i18n.zh[k] || k; }
function toggleLanguage() {
lang = lang === 'zh' ? 'en' : 'zh';
localStorage.setItem('myext_lang', lang);
updateAllTexts(); // re-rendu toute l'interface dépendante de i18n
}
Lors de l'enregistrement des outils MCP, définissez description et parameters dans la langue actuelle de l'utilisateur pour une meilleure interaction avec l'IA.
Responsive Design
Chaque extension doit fonctionner correctement sur différentes tailles de fenêtre. Motifs critiques :
Meta Viewport (REQUIS)
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
Requêtes média CSS
Utilisez des points d'arrêt pour adapter la mise en page à des tailles réduites :
@media (max-width: 900px) {
/* empilages verticaux, réduction du padding */
}
@media (max-width: 600px) {
/* masquage d'éléments secondaires, contrôles compacts */
}
Pratiques responsive clés :
- Utilisez les unités
vwpour les largeurs en secours (ex.width: 65vw; max-width: 360px) - Utilisez les mises en page
flexavecflex-wrapqui s'adaptent naturellement - Masquez les éléments non essentiels sur petits écrans (
display: none) - Réduisez les tailles de police et le padding aux points d'arrêt
Compatibilité iframe
Les extensions peuvent être rendues à l'intérieur d'une iframe (selon la configuration de SAP). Assurez-vous que :
- Détection de l'ID d'extension : Utilisez
window.location.pathname(fonctionne dans les contextes direct et iframe) :function getExtId() { try { const match = window.location.pathname.match(/\/extensions\/([^\/]+)/); return match ? match[1] : 'unknown'; } catch(e) { return 'unknown'; } } - Connexion WebSocket : Utilisez
location.host(non codé en dur) :const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(`${proto}//${location.host}/ws`); - Fermeture de fenêtre :
window.close()fonctionne dans les contextes direct et iframe - Évitez les hypothèses
window.top/window.parent— votre extension peut être la fenêtre de niveau supérieur - Font Awesome via CDN assure que les icônes se chargent indépendamment du chemin de service
Fenêtre transparente / Mode compact
Quand transparent: true est défini dans package.json, SAP crée une fenêtre sans cadre transparente. L'extension doit implémenter le mode compact pour fonctionner correctement.
Comment SAP crée les fenêtres transparentes
Depuis main.js, quand extension.transparent est true :
{
frame: false,
transparent: true,
alwaysOnTop: true,
skipTaskbar: false,
hasShadow: false,
backgroundColor: 'rgba(0, 0, 0, 0)',
}
CSS de mode compact (REQUIS pour les extensions transparentes)
/* Arrière-plans transparents */
body.compact { background: transparent !important; }
html.compact { background: transparent !important; }
/* Régions draggables — rend les éléments structurels déplaçables pour les fenêtres sans cadre */
body.compact header,
body.compact footer,
body.compact #inputBar {
-webkit-app-region: drag;
}
/* Les éléments interactifs DOIVENT se désabonner du drag */
body.compact button,
body.compact input,
body.compact textarea,
body.compact select,
body.compact a,
body.compact .compact-close-btn {
-webkit-app-region: no-drag;
}
/* Bouton de fermeture compact (cercle rouge, en haut à droite) */
.compact-close-btn { display: none; }
body.compact .compact-close-btn {
display: flex;
position: absolute;
top: 5px; right: 5px;
width: 20px; height: 20px;
background: rgb(255, 57, 57);
border: none; border-radius: 50%;
color: #fff;
align-items: center; justify-content: center;
font-size: 10px; cursor: pointer;
transition: 0.2s;
z-index: 100;
-webkit-app-region: no-drag;
}
body.compact .compact-close-btn:hover { background: #ec4141; }
Détection du mode compact (REQUIS)
function checkCompactMode() {
if (window.innerHeight < 200) {
document.documentElement.classList.add('compact');
document.body.classList.add('compact');
} else {
document.documentElement.classList.remove('compact');
document.body.classList.remove('compact');
}
}
function closeWindow() { window.close(); }
checkCompactMode();
window.addEventListener('resize', checkCompactMode);
Positionnement du bouton de fermeture
Le HTML du bouton de fermeture doit être placé au niveau du body (pas imbriqué dans des conteneurs), généralement juste après <body> :
<body>
<button class="compact-close-btn" onclick="closeWindow()" title="关闭窗口">
<i class="fa-solid fa-xmark"></i>
</button>
<!-- reste du contenu -->
</body>
Pour les mini-widgets transparents, vous pouvez aussi placer le bouton de fermeture à l'intérieur d'un conteneur de contenu et le rendre visible au survol — consultez sap-lx-music pour ce motif.
Utilisation des iframes pour les schémas d'URL personnalisés
Si votre extension doit invoquer des URLs de protocole personnalisé (ex. lxmusic://, myapp://), utilisez une technique d'iframe cachée :
function invokeScheme(url) {
let iframe = document.getElementById('scheme-invoker');
if (!iframe) {
iframe = document.createElement('iframe');
iframe.id = 'scheme-invoker';
iframe.style.display = 'none';
document.body.appendChild(iframe);
}
iframe.src = url;
}
Cela évite les blocages de pop-up de window.open() et fonctionne de manière fiable à l'intérieur d'Electron.
Référence du protocole WebSocket
| Type de message | Direction | Objectif |
|---|---|---|
get_messages |
→ SAP | Demander l'historique des messages actuel |
messages_update |
← SAP | Liste des messages mise à jour |
broadcast_messages |
← SAP | Diffusion de mise à jour de message |
set_user_input |
→ SAP | Mettre à jour le texte d'entrée utilisateur |
trigger_send_message |
→ SAP | Envoyer l'entrée actuelle en tant que message utilisateur |
trigger_clear_message |
→ SAP | Effacer tous les messages |
register_node_extension_mcp |
→ SAP | Enregistrer les outils MCP (fonctionne pour statique ET Node.js) |
unregister_node_extension_mcp |
→ SAP | Désenregistrer à la fermeture de page |
mcp_registered |
← SAP | Confirmation de l'enregistrement |
call_mcp_tool |
← SAP | L'agent IA appelle un outil enregistré |
mcp_tool_result |
→ SAP | Retourner le résultat d'exécution de l'outil |
trigger_close_extension |
→ SAP | Demander la fermeture de la fenêtre d'extension |
API HTTP de chat simple (/simple_chat)
SAP expose un endpoint HTTP sans état POST /simple_chat que les extensions peuvent appeler pour des tâches IA ponctuelles — traduction, synthèse, Q&A rapide, génération de code — sans passer par le flux de chat WebSocket et sans ajouter de messages à l'historique des conversations.
C'est idéal quand votre extension a besoin d'un appel IA rapide et à tour unique : traduire du texte, résumer du contenu, extraire des mots-clés, classifier une entrée, etc.
Quand utiliser /simple_chat vs WebSocket
| Fonctionnalité | API HTTP /simple_chat |
WebSocket (trigger_send_message) |
|---|---|---|
| Historique des conversations | ❌ Sans état — pas d'historique | ✅ Historique de chat complet |
| Messages affichés dans l'UI | ❌ Non ajoutés au chat | ✅ Rendus dans la liste des messages |
| Cas d'usage | Ponctuel : traduire, résumer, classifier | Chat multi-tour, tâches d'agent |
| Format de réponse | JSON compatible OpenAI / flux NDJSON | événements messages_update / broadcast_messages |
| Vitesse | Utilise la config fast du client SAP |
Utilise le fournisseur de modèle actif actuel |
Endpoint
POST /simple_chat
Content-Type: application/json
L'endpoint est sur la même origine que l'extension, utilisez donc une URL relative :
const res = await fetch('/simple_chat', { ... });
Format de requête
{
"messages": [
{ "role": "system", "content": "You are a professional translator." },
{ "role": "user", "content": "Translate 'Hello world' to Chinese." }
],
"stream": false,
"temperature": 0.7
}
| Champ | Type | Requis | Description |
|---|---|---|---|
messages |
array | ✅ | Tableau d'objets {role, content} (system/user/assistant) |
stream |
boolean | ❌ (défaut false) |
true pour streaming, false pour réponse JSON en une seule fois |
temperature |
number | ❌ (défaut des paramètres) | 0–2, plus bas = plus déterministe |
Réponse non-streaming (stream: false)
Retourne un objet JSON ChatCompletion compatible OpenAI standard :
{
"id": "chatcmpl-xxx",
"object": "chat.completion",
"created": 1234567890,
"model": "gpt-4o",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "你好世界"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 20,
"completion_tokens": 5,
"total_tokens": 25
}
}
Accédez au résultat : data.choices[0].message.content
Réponse streaming (stream: true)
Retourne du NDJSON (un objet JSON par ligne), correspondant au format de streaming d'OpenAI. Chaque ligne contient un chunk delta :
{"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
{"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"你好"},"finish_reason":null}]}
{"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"世界"},"finish_reason":null}]}
{"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
Note : Le stream n'envoie PAS de marqueur [DONE]. Détectez la fin en vérifiant choices[0].finish_reason.
Exemples d'utilisation JavaScript
Non-streaming (Appel simple en une seule fois)
/**
* Appelez le /simple_chat de SAP pour une tâche IA ponctuelle.
* @param {Array} messages - [{role, content}, ...]
* @param {number} [temperature=0.7]
* @returns {Promise<object>} ChatCompletion compatible OpenAI
*/
async function simpleChat(messages, temperature = 0.7) {
const res = await fetch('/simple_chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages, stream: false, temperature })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error?.message || `HTTP ${res.status}`);
}
return await res.json();
}
// ---------- Exemples pratiques ----------
// Traduction
async function translate(text, targetLang = 'Chinese') {
const res = await simpleChat([
{ role: 'system', content: `You are a translator. Translate to ${targetLang}. Reply ONLY with the translation, no explanations.` },
{ role: 'user', content: text }
]);
return res.choices[0].message.content;
}
// Synthèse
async function summarize(text, maxWords = 50) {
const res = await simpleChat([
{ role: 'system', content: `Summarize in ≤${maxWords} words. Reply ONLY with the summary.` },
{ role: 'user', content: text }
]);
return res.choices[0].message.content;
}
// Classification rapide
async function classify(text, labels) {
const res = await simpleChat([
{ role: 'system', content: `Classify into one of: ${labels.join(', ')}. Reply ONLY with the label.` },
{ role: 'user', content: text }
]);
return res.choices[0].message.content.trim();
}
Streaming (Affichage en temps réel)
/**
* Appelez /simple_chat avec streaming. Produit des chaînes de contenu delta.
* @param {Array} messages
* @param {number} [temperature=0.7]
* @returns {AsyncGenerator<string>} Produit les chunks delta de contenu
*/
async function* simpleChatStream(messages, temperature = 0.7) {
const res = await fetch('/simple_chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages, stream: true, temperature })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error?.message || `HTTP ${res.status}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split('\n');
buf = lines.pop(); // garder la ligne incomplète dans le buffer
for (const line of lines) {
if (!line.trim()) continue;
try {
const chunk = JSON.parse(line);
const content = chunk.choices?.[0]?.delta?.content;
if (content) yield content;
if (chunk.choices?.[0]?.finish_reason === 'stop') return;
} catch(e) { /* ignorer les erreurs de parsing pour les lignes partielles */ }
}
}
}
// Utilisation : rendu de la réponse streaming dans un élément
const el = document.getElementById('output');
el.textContent = '';
for await (const chunk of simpleChatStream([
{ role: 'user', content: 'Write a haiku about coding.' }
])) {
el.textContent += chunk;
}
Gestion des erreurs
En cas d'erreur, l'endpoint retourne un objet JSON avec un champ error :
{
"error": {
"message": "No model providers configured",
"type": "server_error",
"code": 500
}
}
Vérifiez toujours res.ok et parsez le corps de l'erreur.
Notes importantes pour /simple_chat
- Sans état : Chaque appel est indépendant. Aucun contexte de conversation n'est conservé entre les appels.
- Pas d'impact UI : Les résultats ne sont PAS affichés dans la fenêtre de chat principal. Votre extension gère le rendu.
- Utilise le client fast : L'endpoint utilise la configuration du fournisseur de modèle « fast » de SAP. Cela peut être un modèle différent du chat principal.
- Même origine seulement : Les extensions sont servies depuis la même origine, donc pas de problèmes CORS. Utilisez une URL relative (
/simple_chat). - Pas un remplacement pour les outils MCP : Si vous avez besoin que l'agent IA appelle votre extension, enregistrez des outils MCP via WebSocket.
/simple_chatest pour votre extension d'appeler l'IA, pas l'inverse.
Notes importantes
- Format de l'ID d'extension :
{owner}_{repo}(ex.heshengtao_sap-example) - nodePort: 0 signifie auto-attribution d'un port libre (plage 3100-13999)
- Toujours enregistrer le gestionnaire
beforeunloadpour envoyerunregister_node_extension_mcp - MCP fonctionne dans les deux modes statique et Node.js — le nom du type de message
register_node_extension_mcpest historique ; il fonctionne via WebSocket depuis n'importe quelle extension. Suivez toujours le cycle de vie à trois étapes :registerMcpTools()à l'ouverture WS,handleMcpCall()lors de l'appel d'outil,unregisterMcpTools()sur beforeunload - Font Awesome : Utilisez toujours le CDN (
cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css). Les chemins relatifs comme../../fontawesome/ne fonctionnent PAS pour les extensions Node.js (ils sont servis depuis Express, pas depuis le répertoire statique de SAP) - Couleurs de thème : Chaque extension définit sa propre identité via les variables CSS sur
:rootetbody.dark. Ne forcez PAS les couleurs de thème de SAP - Implémentez toujours le mode sombre/clair et i18n chinois/anglais bilingue comme fonctionnalité de base
- Fenêtres transparentes : Implémentez toujours le mode compact. Sans
-webkit-app-region: drag, les fenêtres sans cadre ne peuvent pas être déplacées. Sans-webkit-app-region: no-dragsur les éléments interactifs, les boutons deviennent non-cliquables - Bouton de fermeture : Pour les fenêtres transparentes/sans cadre, l'extension DOIT fournir son propre bouton de fermeture puisqu'il n'y a pas de barre de titre native
Implémentations de référence
Étudiez ces extensions réelles pour les motifs :
- sap-lx-music — Extension statique avec MCP, mode compact transparent, thème sombre/clair, i18n, invocation de schéma personnalisé
- sap-example (heshengtao_sap-example) — Extension basique d'interface de chat statique
- sap-example-with-node (heshengtao_sap-example-with-node) — Extension Node.js avec backend Express
Ressources
assets/
assets/static-template/— Modèle de démarrage complet pour les extensions statiquesassets/node-template/— Modèle de démarrage complet pour les extensions Node.js
references/
references/package-json-spec.md— Référence complète des champs package.jsonreferences/node-entry-spec.md— Spécification du point d'entrée Node.js et du cycle de vie