Traitement par lot, menu IA et vérification du script
Le menu
onOpen est un déclencheur simple : une fonction au nom réservé que Google lance automatiquement à l'ouverture du classeur.
function onOpen() {
SpreadsheetApp.getUi().createMenu('IA')
.addItem('Traiter les e-mails', 'traiterEmails')
.addToUi();
}Une réponse structurée par e-mail
On demande un JSON conforme à un schéma, avec des listes fermées (enum) :
function analyserEmail_(objet, texte) {
const schema = {
type: 'OBJECT',
properties: {
resume: { type: 'STRING' },
categorie: { type: 'STRING', enum: CATEGORIES },
priorite: { type: 'STRING', enum: ['haute', 'normale', 'basse'] },
},
required: ['resume', 'categorie', 'priorite'],
};
const prompt = 'Tu tries la boîte de réception d\'une petite entreprise. ' + ... + texte;
return JSON.parse(appelerGemini_(prompt, schema));
}La boucle sur la feuille
const COL = { OBJET: 1, TEXTE: 2, RESUME: 3, CATEGORIE: 4, PRIORITE: 5, STATUT: 6 };
const LIMITE_MS = 5 * 60 * 1000; // on s'arrête avant la limite de 6 min par exécution
function traiterEmails() {
const debut = Date.now();
const feuille = SpreadsheetApp.getActive().getSheetByName('Emails');
const valeurs = feuille.getDataRange().getValues(); // ligne 0 = en-têtes
let traitees = 0;
for (let i = 1; i < valeurs.length; i++) {
if (valeurs[i][COL.STATUT - 1]) continue; // déjà traité ou validé : on n'y touche pas
if (Date.now() - debut > LIMITE_MS) break; // relancez le menu pour continuer
const r = analyserEmail_(valeurs[i][COL.OBJET - 1], valeurs[i][COL.TEXTE - 1]);
feuille.getRange(i + 1, COL.RESUME, 1, 4).setValues([[r.resume, r.categorie, r.priorite, 'À valider']]);
traitees++;
}
SpreadsheetApp.getActive().toast(traitees + ' e-mail(s) traité(s). Relisez la colonne Statut.');
}flowchart TD
S["Menu IA > Traiter les e-mails"] --> L["Lire toute la feuille en une fois"]
L --> B{"Ligne suivante ?"}
B -- "non" --> T["toast : n e-mails traités"]
B -- "oui" --> St{"Statut rempli ?"}
St -- "oui" --> B
St -- "non" --> Tm{"Plus de 5 min écoulées ?"}
Tm -- "oui" --> T
Tm -- "non" --> G["analyserEmail_() puis écrire C:F + 'À valider'"]
G --> B
| Choix de conception | Raison |
|---|---|
Lire la feuille en une fois (getValues) | chaque lecture de cellule est un appel lent ; une seule lecture est bien plus rapide |
| Écrire après chaque ligne | si le script s'arrête, le travail déjà fait est gardé |
| Sauter les lignes avec un statut | relancer ne repaie pas ce qui est fait et n'écrase jamais une ligne validée |
| S'arrêter à 5 min | la limite est de 6 min par exécution : on garde une marge |
| Statut « À valider » | aucune action automatique : un humain relit |
Vous relancez le menu après qu'il s'est arrêté à la ligne 120 sur 300. Que se passe-t-il ?
La colonne Statut sert de marque-page : toute ligne qui a un statut (À valider, Validé…) est ignorée. C'est aussi ce qui protège les lignes déjà relues par un humain.
Pourquoi écrire le résultat dans la feuille après chaque e-mail, plutôt qu'une seule fois à la fin ?
Une écriture à la fin serait un peu plus rapide, mais une erreur à la ligne 150 ferait perdre 149 résultats déjà payés. On choisit la sûreté.
Vérifier le script hors de Google
Le fichier .gs ne s'exécute que chez Google, mais c'est du JavaScript : on peut le vérifier sur son ordinateur avec Node.js.
- Syntaxe :
node --checkrefuse l'extension.gs, on copie donc le fichier en.js. Les appels àSpreadsheetApp,UrlFetchApp… ne gênent pas :--checkne fait que lire le code sans l'exécuter. - Comportement : test_gs.mjs exécute le script avec des doublures des services Google et une feuille « Emails » en mémoire.
cp resumer.gs /tmp/resumer.js && node --check /tmp/resumer.js && echo "syntaxe OK"
node test_gs.mjs # doublures, sans clé
GEMINI_API_KEY=... REEL=1 node test_gs.mjs # vrais appels à Gemini via curlctx.traiterEmails()
assert.equal(lignes[1][5], 'À valider')
assert.deepEqual(lignes[3], ['Déjà vu', 'Texte', 'ok', 'information', 'basse', 'Validé']) // intacteExercice : un statut « Erreur »
Aujourd'hui, si Gemini échoue sur une ligne, toute l'exécution s'arrête. Modifiez la boucle pour écrire Erreur : <message> dans la colonne Statut de la ligne fautive et continuer avec la suivante.
Voir la solution
try {
const r = analyserEmail_(valeurs[i][COL.OBJET - 1], valeurs[i][COL.TEXTE - 1]);
feuille.getRange(i + 1, COL.RESUME, 1, 4).setValues([[r.resume, r.categorie, r.priorite, 'À valider']]);
traitees++;
} catch (e) {
feuille.getRange(i + 1, COL.STATUT).setValue('Erreur : ' + String(e.message).slice(0, 80));
}Pour retraiter une ligne en erreur, il suffit de vider sa cellule Statut et de relancer le menu.
Résultat obtenu
Vérifications exécutées le 2026-10-15 :
node --check OK
RESUMER(A2) -> Le client exige d'être remboursé sous 48 heures pour sa commande 1042 cassée, sinon il laissera un avis.
| Le client demande le remboursement sous 48 heures de sa commande n° 1042 arrivée cassée, sous peine de laisser un avis négatif. | reclamation | haute | À valider
| Le client demande si l'entreprise est ouverte le samedi matin. | information | basse | À valider
| ok | information | basse | Validé
2 e-mail(s) traité(s). Relisez la colonne Statut. (3 appels réels)
OKTrois appels réels : un pour RESUMER (le second, identique, est servi par le cache) et deux pour les e-mails sans statut. La ligne « Validé » n'a pas été touchée.
- J'ai un menu IA qui traite la feuille Emails
- Je sais pourquoi le script lit en une fois, écrit à chaque ligne et s'arrête à 5 minutes
- Je sais vérifier la syntaxe d'un
.gsavecnode --check - Aucune ligne validée n'est jamais écrasée