Le refresh_token est rotatif : Mac et homelab ne doivent pas le partager (sinon invalid_grant). Procédure durcie + bootstrap/vérif de la chaîne.
144 lines
7.5 KiB
Markdown
144 lines
7.5 KiB
Markdown
# AntiCoco 🥥🚫
|
|
|
|
Serveur **MCP** qui donne accès à ton compte **HelloFresh** (France, `hellofresh.fr`) pour :
|
|
lire le menu de la semaine, **exclure des ingrédients** (la **noix de coco** en priorité),
|
|
**proposer une shortlist** de recettes, puis **enregistrer ta sélection** après confirmation.
|
|
|
|
Client MCP visé : **Hermes** (Nous Research), qui tourne sur le même homelab.
|
|
|
|
> ⚠️ HelloFresh n'a pas d'API publique. AntiCoco s'appuie sur l'API interne `gw/` du site
|
|
> (non documentée, susceptible de changer) — **usage strictement personnel**.
|
|
|
|
## État (validé de bout en bout, 2026-06)
|
|
|
|
✅ **Boucle complète testée sur le vrai compte** : lecture du menu (`menus-service/menus` →
|
|
détails batch `recipes/recipes?ids=…`), filtrage coco (4/85 détectées, faux positifs des tags
|
|
internes neutralisés), proposition classée, et **écriture réelle réussie** (`PUT /v1/carts/{week}`,
|
|
HTTP 200) — sélection par index de course, ids de compte dérivés dynamiquement.
|
|
|
|
✅ **Auth autonome (pur HTTP)** : le token (30 min) est rafraîchi par un simple `POST /gw/refresh`
|
|
(le endpoint que la SPA appelle), **sans navigateur**. Le refresh_token roule par fenêtres de 60 j,
|
|
remises à zéro à chaque refresh → un homelab **allumé en continu** reste authentifié sans
|
|
intervention ni re-sync. Le navigateur headless ne sert plus que de filet de secours.
|
|
|
|
> ⚠️ La chaîne de refresh **peut casser** (homelab éteint trop longtemps, ou refresh_token
|
|
> consommé hors `_refresh_session`) → `POST /gw/refresh` renvoie `invalid_grant` et
|
|
> `auth_status()` passe à `logged_in:false`. Le seul remède est de **refaire le login CDP**
|
|
> (cf. ci-dessous). **Restaurer un ancien `.session/` ne marche pas** : les refresh_token sont
|
|
> rotatifs, les anciens sont morts.
|
|
|
|
> ⚠️ La connexion **directe** automatisée (Playwright/Chromium qui remplit le formulaire) est
|
|
> bloquée par l'anti-bot HelloFresh. La session se crée donc via **attache CDP à ton vrai Chrome**
|
|
> (`tools/attach_capture.py`), où le login marche normalement.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Hermes ──HTTP──▶ server.py (FastMCP, :9200/mcp)
|
|
├─ hellofresh/auth.py session storage_state + refresh HTTP /gw/refresh
|
|
├─ hellofresh/api.py httpx : menu, détails, deliveries, PUT cart
|
|
├─ hellofresh/filter.py exclusion (coco !) + scoring préférences
|
|
└─ config/ excludes.json · prefs.json · endpoints.json
|
|
```
|
|
|
|
## Mise en route
|
|
|
|
### 1. Installer
|
|
```bash
|
|
pip install -r requirements.txt
|
|
playwright install chromium
|
|
cp .env.example .env
|
|
```
|
|
|
|
### 2. Créer (ou ré-créer) la session — login via TON Chrome, anti-bot contourné
|
|
C'est **la seule méthode de login qui marche** : un navigateur piloté par Playwright (même avec
|
|
`channel=chrome`) est bloqué par l'anti-bot HelloFresh. On lance donc le vrai Chrome en mode debug
|
|
et on s'y **attache en CDP** (lecture seule). Même procédure pour la 1ʳᵉ session **et** pour une
|
|
ré-authentification après `invalid_grant`.
|
|
|
|
```bash
|
|
# 1) Lance ton vrai Chrome en mode debug avec un PROFIL DÉDIÉ (Chrome ≥149 refuse le debug
|
|
# sur le profil par défaut). Ta fenêtre Chrome habituelle peut rester ouverte.
|
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
|
|
--remote-debugging-port=9222 --user-data-dir="$HOME/.hf-chrome-debug" \
|
|
https://www.hellofresh.fr/my-account/deliveries/menu
|
|
|
|
# 2) Connecte-toi dans cette fenêtre (page de login = https://www.hellofresh.fr/login).
|
|
|
|
# 3) Pendant que Chrome reste ouvert et connecté, attache la capture :
|
|
python tools/attach_capture.py # capture trafic + exporte .session/storage_state.json
|
|
```
|
|
`attach_capture` doit tourner **pendant** que tu (re)charges la page menu, pour capter le trafic.
|
|
Il **écrase** `.session/storage_state.json` avec la session fraîche (cookies ~60 j) — ne pas
|
|
restaurer un vieux backup à la place (refresh_token rotatif = anciens morts). Vérifie ensuite :
|
|
`python -c "from hellofresh import auth, json; print(auth.auth_status())"` → `logged_in: true`.
|
|
`config/endpoints.json` est déjà rempli ; rejoue `attach_capture` si l'API change.
|
|
|
|
### 3. Tester en local (headless, comme le homelab)
|
|
```bash
|
|
python server.py # auth via storage_state, refresh token automatique
|
|
```
|
|
|
|
## Déploiement homelab (Docker)
|
|
|
|
`.session/` et `.env` ne sont **jamais** versionnés. Workflow :
|
|
|
|
```bash
|
|
# 1. Sur le Mac : générer la session (cf. « Mise en route » §2 → .session/storage_state.json)
|
|
|
|
# 2. Pousser le code
|
|
git add -A && git commit -m "..." && git push
|
|
|
|
# 3. Synchroniser la session : storage_state.json SEUL (cookie = refresh_token vierge).
|
|
# NE PAS copier token.json (son refresh_token a pu être consommé localement → invalide).
|
|
scp .session/storage_state.json homelab:/home/jerem/AntiCoco/.session/
|
|
|
|
# 4. Sur le homelab : supprimer l'ancien token.json (root, écrit par le conteneur) pour que
|
|
# la chaîne reparte du cookie frais, puis déployer.
|
|
ssh homelab
|
|
cd /home/jerem/AntiCoco
|
|
docker exec anticoco rm -f /app/.session/token.json # sinon un vieux RT mort persiste
|
|
git pull && docker compose up -d --build
|
|
# bootstrap + vérif de la chaîne (doit afficher True puis logged_in: true) :
|
|
docker exec anticoco python -c "from hellofresh import auth; print('refresh', bool(auth._refresh_session())); print(auth.auth_status())"
|
|
```
|
|
|
|
Vérifier : `curl -s http://127.0.0.1:9200/mcp` (le serveur répond au handshake MCP).
|
|
|
|
> ⚠️ **Un seul propriétaire du refresh_token.** Le refresh_token est **rotatif** (chaque
|
|
> `/gw/refresh` invalide le précédent). Le Mac sert UNIQUEMENT à capturer la session ; une fois
|
|
> `storage_state.json` synchronisé, **ne lance plus `server.py` ni d'appel API depuis le Mac** —
|
|
> sinon Mac et homelab se disputent le token et la chaîne casse (`invalid_grant`). Le homelab,
|
|
> seul consommateur, reste authentifié tant qu'il tourne. En cas de casse : refaire §2 + §3-4.
|
|
|
|
## Intégration Hermes
|
|
|
|
Enregistrer AntiCoco dans la config MCP de Hermes (côté homelab), URL
|
|
`http://127.0.0.1:9200/mcp` (transport streamable-http).
|
|
|
|
> Si Hermes n'accepte que le **stdio**, changer la dernière ligne de `server.py`
|
|
> (`mcp.run(transport="stdio")`) et lancer le serveur en sous-processus — le reste est identique.
|
|
|
|
## Outils MCP exposés
|
|
|
|
| Outil | Rôle |
|
|
|-------|------|
|
|
| `hf_auth_status()` | état de connexion |
|
|
| `hf_login()` | (re)connexion + capture token |
|
|
| `hf_list_weeks()` | semaines modifiables |
|
|
| `hf_next_delivery()` | **prochaine box réellement sélectionnée** (≈4 recettes) + date/cutoff, images servables — prêt Telegram |
|
|
| `hf_favorites()` | recettes **favorites** du compte (images servables) |
|
|
| `hf_get_menu(week)` | toutes les recettes, avec `contains_excluded` et `is_favorite` |
|
|
| `hf_propose(week, count=0)` | shortlist **sans coco**, classée par préférences |
|
|
| `hf_confirm_selection(week, recipe_ids)` | **écrit** la sélection (refuse la coco) |
|
|
| `hf_get_excludes()` / `hf_add_exclude(term)` / `hf_remove_exclude(term)` | gérer la liste d'exclusion |
|
|
|
|
## Configuration
|
|
|
|
- `config/excludes.json` — ingrédients bannis (matching insensible casse/accents). Coco déjà listée.
|
|
- `config/prefs.json` — mots-clés `liked`/`disliked` pour classer les propositions.
|
|
- `config/endpoints.json` — URLs gateway réelles + transfo CDN images (`image_cdn_*` : les URLs
|
|
recettes CloudFront `…/0,0/image/X` répondent en 502, réécrites vers `img.hellofresh.com/…/hellofresh_s3/image/X`).
|
|
Sélection courante via `my-deliveries/menu`, favoris via `cfs/v2/favorites/recipe`.
|
|
Re-découverte : `tools/probe_selection.py` / `tools/probe_menu_capture.py` (attache CDP).
|