diff --git a/MEMORY.md b/MEMORY.md index fccace2..726a205 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -165,6 +165,83 @@ create_text { fileId, pageId, text, x, y, textAlign, ... } ← PAS de parentId - Uploader sur CopyParty : `curl -X PUT http://192.168.1.150:3923/anytype/image.png --data-binary @image.png` - Puis : `upload_file_media_from_url { fileId, url: "http://192.168.1.150:3923/anytype/image.png" }` +## Penpot API Directe — Pixel-perfect (⭐ À UTILISER EN PRIORITÉ) + +### Pourquoi directe > MCP +- MCP : auto-calcule les dims texte, pas de `parentId` pour les textes → pas de contrôle pixel-perfect +- API directe : contrôle total (x, y, width, height, parentId, contenu riche) + +### Accès +- **URL backend** : `http://192.168.1.150:9003/api/rpc/command/` (port 9003 exposé = penpot-backend:6060) +- **Auth** : `Authorization: Token ` (dans l'en-tête HTTP) +- **Token** : récupérable dans Penpot UI → Profile → Access Tokens +- **Format corps** : JSON **kebab-case** (ex: `page-id`, `frame-id`, `fill-color`) +- **Réponse** : kebab-case aussi (`default-team-id`, `default-project-id`) +- ⚠️ L'accès via nginx port 9001 retourne un user vide même avec le bon token — **toujours utiliser le port 9003** + +### Workflow création poster (Node.js) +```js +// 1. Auth + IDs +const prof = await rpc('get-profile'); +const teamId = prof['default-team-id']; +const projectId = prof['default-project-id']; + +// 2. Créer fichier +const file = await rpc('create-file', { 'project-id': projectId, name: 'Mon affiche', 'is-shared': false }); +const FILE_ID = file.id; +const PAGE_ID = Object.keys(file.data?.['pages-index'] || {})[0] || file.data?.pages?.[0]; + +// 3. Ajouter shapes une par une (revn incrémental !) +for (const change of changes) { + const result = await rpc('update-file', { + id: FILE_ID, 'session-id': randomUUID(), revn: currentRevn, vern: 0, + changes: [change] + }); + currentRevn = result.revn; +} +``` + +### Format d'une change `add-obj` +```js +{ + type: 'add-obj', + id: '', + 'page-id': PAGE_ID, + 'frame-id': ROOT, // '00000000-0000-0000-0000-000000000000' = root + 'parent-id': ROOT, + obj: { + id, type: 'rect'|'text'|'frame', name, + x, y, width, height, + 'parent-id': ROOT, 'frame-id': ROOT, + fills: [{ 'fill-color': '#C0392B', 'fill-opacity': 1 }], + selrect: { x, y, width, height, x1, y1, x2, y2 }, + points: [{x,y},{x:x+w,y},{x:x+w,y:y+h},{x,y:y+h}], + transform: {a:1,b:0,c:0,d:1,e:0,f:0}, + 'transform-inverse': {a:1,b:0,c:0,d:1,e:0,f:0}, + // Pour un text : + content: { type:'root', children:[{ type:'paragraph-set', children:[{ + type:'paragraph', 'text-align':'center', children:[{ + text: 'Mon texte', + 'font-id':'gfont-work-sans', 'font-family':'Work Sans', + 'font-size':'48', 'font-weight':'700', 'font-style':'normal', + 'letter-spacing':'2', 'line-height': 1.2, 'text-decoration':'none', + fills:[{'fill-color':'#FFFFFF', 'fill-opacity':1}], + }] + }]}]} + } +} +``` + +### ⚠️ Gotchas critiques +- **Envoyer 1 change à la fois** (pas toutes en même temps) + incrémenter `revn` +- **`type: 'frame'`** échoue avec erreur `:shapes nil` → utiliser `type: 'rect'` comme fond +- **toKebab()** : tous les paramètres JS en camelCase → convertir en kebab-case avant envoi +- **`text` content** : bien mettre le champ `text:` dans le nœud enfant (sinon validation error) +- **Texte centré** : mettre `x:0, width:W` (toute la largeur) + `text-align: 'center'` dans le paragraphe + +### Script de référence +`/home/node/.openclaw/workspace/penpot-api-direct.mjs` — poster 600×900 complet, 16 shapes + ## Podcasts & Vidéos — Transcription - Je peux **récupérer et transcrire** des podcasts/vidéos en ligne - **Méthode :**