mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-16 23:00:44 +08:00
统一技能安装与 Skills 市场接入链路。
新增公开安装工具与多来源 provider 支持,补齐管理端与目录加载逻辑,并同步更新相关测试与文档以保证可见性和路径安全。 Made-with: Cursor
This commit is contained in:
parent
d5e30542aa
commit
4d9232f3b3
66 changed files with 7369 additions and 1099 deletions
61
runtime/skills/xlsx-pro/README.md
Normal file
61
runtime/skills/xlsx-pro/README.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# XLSX Pro
|
||||
|
||||
 
|
||||
|
||||
Un skill **Clawdbot / OpenClawd** pour générer et modifier des fichiers Excel **propres** (XLSX / XLSM / CSV / TSV) avec :
|
||||
- formatage “pro”
|
||||
- **formules Excel** (au lieu de valeurs hardcodées)
|
||||
- recalcul optionnel des formules via **LibreOffice headless**
|
||||
- contrôle qualité : détection des erreurs Excel (`#REF!`, `#DIV/0!`, `#VALUE!`, `#N/A`, `#NAME?`, …)
|
||||
|
||||
## Pourquoi ce skill ?
|
||||
|
||||
`openpyxl` sait **écrire** des formules, mais ne sait pas **calculer** leurs résultats. En production, ça crée des fichiers où les formules ne sont pas évaluées et où les erreurs ne sont pas détectées.
|
||||
|
||||
`XLSX Pro` ajoute une étape serveur fiable : **recalcul via LibreOffice** + scan d’erreurs.
|
||||
|
||||
## Prérequis
|
||||
|
||||
### Python
|
||||
```bash
|
||||
pip install openpyxl pandas xlrd xlwt
|
||||
```
|
||||
|
||||
### LibreOffice (uniquement si tu veux recalculer les formules)
|
||||
Ubuntu/Debian :
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libreoffice-calc libreoffice-common
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
### 1) Générer un fichier Excel (avec openpyxl)
|
||||
Tu peux créer ton `.xlsx` comme d’habitude en Python, en mettant des **formules** dans les cellules.
|
||||
|
||||
### 2) Recalculer + valider
|
||||
```bash
|
||||
python scripts/recalc.py ton_fichier.xlsx 60
|
||||
```
|
||||
|
||||
Sortie JSON :
|
||||
- `status: success | errors_found`
|
||||
- `total_errors`
|
||||
- `error_summary` (types + emplacements)
|
||||
- `total_formulas`
|
||||
|
||||
## Bonnes pratiques (résumé)
|
||||
- **Préférer les formules Excel** plutôt que calculer en Python puis écrire des valeurs.
|
||||
- **Zéro erreur de formule** dans le livrable.
|
||||
- Si tu modifies un template existant : **respecte exactement** les styles/conventions.
|
||||
|
||||
## Troubleshooting
|
||||
- Si `soffice` est introuvable : installe LibreOffice (voir Prérequis).
|
||||
- Si le recalcul “timeout” : augmente le timeout (2e argument) et/ou teste sur un fichier plus petit.
|
||||
- Si erreur du type "macro mal configurée" / "macro not configured" : supprime le fichier de macro puis relance :
|
||||
- Linux : `~/.config/libreoffice/4/user/basic/Standard/Module1.xba`
|
||||
- macOS : `~/Library/Application Support/LibreOffice/4/user/basic/Standard/Module1.xba`
|
||||
- En conteneur (Docker) : ajoute la variable d'env `SAL_USE_VCLPLUGIN=svp` (ça évite des soucis d'UI en headless).
|
||||
|
||||
## Licence
|
||||
MIT (à ajuster si tu veux une autre licence).
|
||||
232
runtime/skills/xlsx-pro/SKILL.md
Normal file
232
runtime/skills/xlsx-pro/SKILL.md
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
---
|
||||
name: xlsx-pro
|
||||
description: "Compétence pour manipuler les fichiers Excel (.xlsx, .xlsm, .csv, .tsv). Utiliser quand l'utilisateur veut : ouvrir, lire, éditer ou créer un fichier tableur ; ajouter des colonnes, calculer des formules, formater, créer des graphiques, nettoyer des données ; convertir entre formats tabulaires. Le livrable doit être un fichier tableur. NE PAS utiliser si le livrable est un document Word, HTML, script Python standalone, ou intégration Google Sheets."
|
||||
version: "1.0.1"
|
||||
author: "Eric Barotte"
|
||||
---
|
||||
|
||||
# Compétence Excel pour OpenClawd
|
||||
|
||||
## TL;DR
|
||||
- Génère/édite des fichiers Excel avec des **formules** (pas des valeurs hardcodées).
|
||||
- Optionnel: **recalcul** via LibreOffice headless + détection d’erreurs Excel.
|
||||
- Livrable attendu: un fichier tableur propre (XLSX/XLSM/CSV/TSV).
|
||||
|
||||
|
||||
## Prérequis
|
||||
|
||||
### Dépendances Python
|
||||
```bash
|
||||
pip install openpyxl pandas xlrd xlwt
|
||||
```
|
||||
|
||||
### LibreOffice (pour recalcul des formules)
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install libreoffice-calc libreoffice-common
|
||||
```
|
||||
|
||||
## Règles de Qualité
|
||||
|
||||
### Police Professionnelle
|
||||
- Utiliser une police cohérente (Arial, Times New Roman) sauf instruction contraire
|
||||
|
||||
### Zéro Erreur de Formule
|
||||
- Tout fichier Excel DOIT être livré SANS erreurs (#REF!, #DIV/0!, #VALUE!, #N/A, #NAME?)
|
||||
|
||||
### Préservation des Templates
|
||||
- Respecter EXACTEMENT le format et style existants lors de modifications
|
||||
- Les conventions du template préexistant ont TOUJOURS priorité
|
||||
|
||||
## Standards pour Modèles Financiers
|
||||
|
||||
### Code Couleur (Standards Industrie)
|
||||
- **Texte bleu (RGB: 0,0,255)** : Inputs hardcodés, valeurs modifiables
|
||||
- **Texte noir (RGB: 0,0,0)** : TOUTES les formules et calculs
|
||||
- **Texte vert (RGB: 0,128,0)** : Liens vers autres feuilles du même classeur
|
||||
- **Texte rouge (RGB: 255,0,0)** : Liens externes vers autres fichiers
|
||||
- **Fond jaune (RGB: 255,255,0)** : Hypothèses clés ou cellules à mettre à jour
|
||||
|
||||
### Formatage des Nombres
|
||||
- **Années** : Format texte ("2024" pas "2,024")
|
||||
- **Devises** : Format $#,##0 ; spécifier unités dans les en-têtes ("Revenue ($mm)")
|
||||
- **Zéros** : Afficher comme "-" (format: "$#,##0;($#,##0);-")
|
||||
- **Pourcentages** : Format 0.0% par défaut
|
||||
- **Multiples** : Format 0.0x (EV/EBITDA, P/E)
|
||||
- **Négatifs** : Parenthèses (123) pas moins -123
|
||||
|
||||
## CRITIQUE : Utiliser des Formules, PAS des Valeurs Hardcodées
|
||||
|
||||
**TOUJOURS utiliser des formules Excel au lieu de calculer en Python et hardcoder.**
|
||||
|
||||
### ❌ MAUVAIS - Hardcoding
|
||||
```python
|
||||
# Mauvais: Calcul Python puis hardcode
|
||||
total = df['Sales'].sum()
|
||||
sheet['B10'] = total # Hardcode 5000
|
||||
|
||||
# Mauvais: Taux de croissance calculé en Python
|
||||
growth = (df.iloc[-1]['Revenue'] - df.iloc[0]['Revenue']) / df.iloc[0]['Revenue']
|
||||
sheet['C5'] = growth # Hardcode 0.15
|
||||
```
|
||||
|
||||
### ✅ CORRECT - Formules Excel
|
||||
```python
|
||||
# Bon: Laisser Excel calculer
|
||||
sheet['B10'] = '=SUM(B2:B9)'
|
||||
|
||||
# Bon: Taux de croissance en formule Excel
|
||||
sheet['C5'] = '=(C4-C2)/C2'
|
||||
|
||||
# Bon: Moyenne en fonction Excel
|
||||
sheet['D20'] = '=AVERAGE(D2:D19)'
|
||||
```
|
||||
|
||||
## Workflows
|
||||
|
||||
### Workflow Standard
|
||||
1. **Choisir l'outil** : pandas pour données, openpyxl pour formules/formatage
|
||||
2. **Créer/Charger** : Nouveau classeur ou fichier existant
|
||||
3. **Modifier** : Données, formules, formatage
|
||||
4. **Sauvegarder** : Écrire le fichier
|
||||
5. **Recalculer (OBLIGATOIRE si formules)** : `python scripts/recalc.py output.xlsx`
|
||||
6. **Vérifier et corriger** les erreurs détectées
|
||||
|
||||
### Lecture et Analyse avec pandas
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
# Lire Excel
|
||||
df = pd.read_excel('file.xlsx') # Première feuille par défaut
|
||||
all_sheets = pd.read_excel('file.xlsx', sheet_name=None) # Dict de toutes les feuilles
|
||||
|
||||
# Analyser
|
||||
df.head() # Aperçu
|
||||
df.info() # Info colonnes
|
||||
df.describe() # Statistiques
|
||||
|
||||
# Écrire
|
||||
df.to_excel('output.xlsx', index=False)
|
||||
```
|
||||
|
||||
### Création de Fichiers Excel
|
||||
```python
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment
|
||||
|
||||
wb = Workbook()
|
||||
sheet = wb.active
|
||||
|
||||
# Données
|
||||
sheet['A1'] = 'Hello'
|
||||
sheet['B1'] = 'World'
|
||||
sheet.append(['Row', 'of', 'data'])
|
||||
|
||||
# Formule
|
||||
sheet['B2'] = '=SUM(A1:A10)'
|
||||
|
||||
# Formatage
|
||||
sheet['A1'].font = Font(bold=True, color='FF0000')
|
||||
sheet['A1'].fill = PatternFill('solid', start_color='FFFF00')
|
||||
sheet['A1'].alignment = Alignment(horizontal='center')
|
||||
|
||||
# Largeur colonne
|
||||
sheet.column_dimensions['A'].width = 20
|
||||
|
||||
wb.save('output.xlsx')
|
||||
```
|
||||
|
||||
### Édition de Fichiers Existants
|
||||
```python
|
||||
from openpyxl import load_workbook
|
||||
|
||||
# Charger fichier existant
|
||||
wb = load_workbook('existing.xlsx')
|
||||
sheet = wb.active # ou wb['NomFeuille']
|
||||
|
||||
# Parcourir les feuilles
|
||||
for sheet_name in wb.sheetnames:
|
||||
sheet = wb[sheet_name]
|
||||
print(f"Feuille: {sheet_name}")
|
||||
|
||||
# Modifier
|
||||
sheet['A1'] = 'Nouvelle Valeur'
|
||||
sheet.insert_rows(2) # Insérer ligne
|
||||
sheet.delete_cols(3) # Supprimer colonne
|
||||
|
||||
# Ajouter feuille
|
||||
new_sheet = wb.create_sheet('NouvelleFeuille')
|
||||
new_sheet['A1'] = 'Data'
|
||||
|
||||
wb.save('modified.xlsx')
|
||||
```
|
||||
|
||||
## Recalcul des Formules
|
||||
|
||||
Les fichiers créés par openpyxl contiennent les formules comme chaînes mais pas les valeurs calculées. Utiliser le script `recalc.py` :
|
||||
|
||||
```bash
|
||||
python scripts/recalc.py <fichier_excel> [timeout_secondes]
|
||||
```
|
||||
|
||||
Le script :
|
||||
- Configure automatiquement la macro LibreOffice au premier lancement
|
||||
- Recalcule toutes les formules
|
||||
- Scanne TOUTES les cellules pour erreurs Excel
|
||||
- Retourne JSON avec détails et emplacements des erreurs
|
||||
|
||||
### Interprétation de la Sortie
|
||||
```json
|
||||
{
|
||||
"status": "success", // ou "errors_found"
|
||||
"total_errors": 0, // Nombre total d'erreurs
|
||||
"total_formulas": 42, // Nombre de formules
|
||||
"error_summary": { // Présent si erreurs
|
||||
"#REF!": {
|
||||
"count": 2,
|
||||
"locations": ["Sheet1!B5", "Sheet1!C10"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist de Vérification
|
||||
|
||||
### Vérifications Essentielles
|
||||
- [ ] **Tester 2-3 références** : Vérifier qu'elles tirent les bonnes valeurs
|
||||
- [ ] **Mapping colonnes** : Confirmer correspondance (colonne 64 = BL, pas BK)
|
||||
- [ ] **Offset lignes** : Excel est 1-indexé (DataFrame row 5 = Excel row 6)
|
||||
|
||||
### Pièges Courants
|
||||
- [ ] **Gestion NaN** : Vérifier valeurs nulles avec `pd.notna()`
|
||||
- [ ] **Colonnes éloignées** : Données FY souvent en colonnes 50+
|
||||
- [ ] **Correspondances multiples** : Chercher toutes les occurrences
|
||||
- [ ] **Division par zéro** : Vérifier dénominateurs (#DIV/0!)
|
||||
- [ ] **Références invalides** : Vérifier que toutes pointent vers cellules existantes (#REF!)
|
||||
- [ ] **Références inter-feuilles** : Format correct (Sheet1!A1)
|
||||
|
||||
## Bonnes Pratiques
|
||||
|
||||
### Sélection de Bibliothèque
|
||||
- **pandas** : Analyse de données, opérations en masse, export simple
|
||||
- **openpyxl** : Formatage complexe, formules, fonctionnalités Excel spécifiques
|
||||
|
||||
### Avec openpyxl
|
||||
- Indices de cellules en base 1 (row=1, column=1 = cellule A1)
|
||||
- `data_only=True` pour lire valeurs calculées
|
||||
- **Attention** : Sauvegarder après `data_only=True` remplace définitivement les formules par les valeurs
|
||||
- Pour gros fichiers : `read_only=True` ou `write_only=True`
|
||||
|
||||
### Avec pandas
|
||||
- Spécifier types de données : `pd.read_excel('file.xlsx', dtype={'id': str})`
|
||||
- Pour gros fichiers, colonnes spécifiques : `usecols=['A', 'C', 'E']`
|
||||
- Gestion des dates : `parse_dates=['date_column']`
|
||||
|
||||
## Style de Code
|
||||
|
||||
**IMPORTANT** : Code Python minimal et concis, sans commentaires superflus.
|
||||
|
||||
**Pour les fichiers Excel** :
|
||||
- Commenter les cellules avec formules complexes
|
||||
- Documenter les sources des données hardcodées
|
||||
- Inclure notes pour calculs clés
|
||||
6
runtime/skills/xlsx-pro/_meta.json
Normal file
6
runtime/skills/xlsx-pro/_meta.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"ownerId": "kn7aga06tewbtydgnw3v1468x9804b6x",
|
||||
"slug": "xlsx-pro",
|
||||
"version": "1.0.1",
|
||||
"publishedAt": 1770059662515
|
||||
}
|
||||
8
runtime/skills/xlsx-pro/scripts/office/__init__.py
Normal file
8
runtime/skills/xlsx-pro/scripts/office/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
"""
|
||||
Module office pour OpenClawd
|
||||
Gestion des opérations LibreOffice
|
||||
"""
|
||||
|
||||
from .soffice import get_soffice_env, run_soffice
|
||||
|
||||
__all__ = ['get_soffice_env', 'run_soffice']
|
||||
211
runtime/skills/xlsx-pro/scripts/office/soffice.py
Normal file
211
runtime/skills/xlsx-pro/scripts/office/soffice.py
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
"""
|
||||
Helper pour exécuter LibreOffice (soffice) dans des environnements
|
||||
où les sockets AF_UNIX peuvent être bloqués (VMs sandboxées).
|
||||
|
||||
Usage:
|
||||
from office.soffice import run_soffice, get_soffice_env
|
||||
|
||||
# Option 1 – exécuter soffice directement
|
||||
result = run_soffice(["--headless", "--convert-to", "pdf", "input.docx"])
|
||||
|
||||
# Option 2 – obtenir env dict pour vos propres appels subprocess
|
||||
env = get_soffice_env()
|
||||
subprocess.run(["soffice", ...], env=env)
|
||||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_soffice_env() -> dict:
|
||||
"""Retourne un env dict adapté pour exécuter soffice en headless.
|
||||
|
||||
Définit toujours SAL_USE_VCLPLUGIN=svp pour le rendu headless (pas de X11).
|
||||
Dans les environnements sandboxés où AF_UNIX est bloqué, ajoute aussi
|
||||
LD_PRELOAD (socket shim).
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
env["SAL_USE_VCLPLUGIN"] = "svp"
|
||||
|
||||
if _needs_shim():
|
||||
shim = _ensure_shim()
|
||||
env["LD_PRELOAD"] = str(shim)
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def run_soffice(args: list, **kwargs) -> subprocess.CompletedProcess:
|
||||
"""Exécute soffice avec les arguments donnés, appliquant le socket shim
|
||||
si nécessaire. Accepte les mêmes arguments que subprocess.run.
|
||||
|
||||
Dans les environnements sandboxés, le shim gère l'arrêt propre en appelant
|
||||
_exit(0) quand le socket listener de soffice.bin se ferme (après conversion).
|
||||
"""
|
||||
env = get_soffice_env()
|
||||
return subprocess.run(["soffice"] + args, env=env, **kwargs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internals
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_SHIM_SO = Path(tempfile.gettempdir()) / "lo_socket_shim.so"
|
||||
|
||||
|
||||
def _needs_shim() -> bool:
|
||||
"""Vérifie si les sockets AF_UNIX sont bloqués."""
|
||||
try:
|
||||
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
s.close()
|
||||
return False
|
||||
except OSError:
|
||||
return True
|
||||
|
||||
|
||||
def _ensure_shim() -> Path:
|
||||
"""Compile le shim .so s'il n'est pas déjà en cache."""
|
||||
if _SHIM_SO.exists():
|
||||
return _SHIM_SO
|
||||
|
||||
src = Path(tempfile.gettempdir()) / "lo_socket_shim.c"
|
||||
src.write_text(_SHIM_SOURCE)
|
||||
try:
|
||||
subprocess.run(
|
||||
["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
# Si gcc n'est pas disponible ou échoue, on continue sans shim
|
||||
pass
|
||||
finally:
|
||||
if src.exists():
|
||||
src.unlink()
|
||||
return _SHIM_SO
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LD_PRELOAD shim – source C
|
||||
#
|
||||
# Problème
|
||||
# --------
|
||||
# LibreOffice utilise des sockets AF_UNIX pour la gestion single-instance
|
||||
# (OSL_PIPE). Dans les environnements sandboxés, le filtre seccomp bloque
|
||||
# socket(AF_UNIX) tout en permettant socketpair(AF_UNIX). Sans ce shim,
|
||||
# soffice crash ou reste bloqué après conversion.
|
||||
#
|
||||
# Solution
|
||||
# --------
|
||||
# Intercepte les appels concernés et fournit des substituts fonctionnels.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_SHIM_SOURCE = r"""
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int (*real_socket)(int, int, int);
|
||||
static int (*real_socketpair)(int, int, int, int[2]);
|
||||
static int (*real_listen)(int, int);
|
||||
static int (*real_accept)(int, struct sockaddr *, socklen_t *);
|
||||
static int (*real_close)(int);
|
||||
static int (*real_read)(int, void *, size_t);
|
||||
|
||||
static int is_shimmed[1024];
|
||||
static int peer_of[1024];
|
||||
static int wake_r[1024];
|
||||
static int wake_w[1024];
|
||||
static int listener_fd = -1;
|
||||
|
||||
__attribute__((constructor))
|
||||
static void init(void) {
|
||||
real_socket = dlsym(RTLD_NEXT, "socket");
|
||||
real_socketpair = dlsym(RTLD_NEXT, "socketpair");
|
||||
real_listen = dlsym(RTLD_NEXT, "listen");
|
||||
real_accept = dlsym(RTLD_NEXT, "accept");
|
||||
real_close = dlsym(RTLD_NEXT, "close");
|
||||
real_read = dlsym(RTLD_NEXT, "read");
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
peer_of[i] = -1;
|
||||
wake_r[i] = -1;
|
||||
wake_w[i] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
int socket(int domain, int type, int protocol) {
|
||||
if (domain == AF_UNIX) {
|
||||
int fd = real_socket(domain, type, protocol);
|
||||
if (fd >= 0) return fd;
|
||||
int sv[2];
|
||||
if (real_socketpair(domain, type, protocol, sv) == 0) {
|
||||
if (sv[0] >= 0 && sv[0] < 1024) {
|
||||
is_shimmed[sv[0]] = 1;
|
||||
peer_of[sv[0]] = sv[1];
|
||||
int wp[2];
|
||||
if (pipe(wp) == 0) {
|
||||
wake_r[sv[0]] = wp[0];
|
||||
wake_w[sv[0]] = wp[1];
|
||||
}
|
||||
}
|
||||
return sv[0];
|
||||
}
|
||||
errno = EPERM;
|
||||
return -1;
|
||||
}
|
||||
return real_socket(domain, type, protocol);
|
||||
}
|
||||
|
||||
int listen(int sockfd, int backlog) {
|
||||
if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) {
|
||||
listener_fd = sockfd;
|
||||
return 0;
|
||||
}
|
||||
return real_listen(sockfd, backlog);
|
||||
}
|
||||
|
||||
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {
|
||||
if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) {
|
||||
if (wake_r[sockfd] >= 0) {
|
||||
char buf;
|
||||
real_read(wake_r[sockfd], &buf, 1);
|
||||
}
|
||||
errno = ECONNABORTED;
|
||||
return -1;
|
||||
}
|
||||
return real_accept(sockfd, addr, addrlen);
|
||||
}
|
||||
|
||||
int close(int fd) {
|
||||
if (fd >= 0 && fd < 1024 && is_shimmed[fd]) {
|
||||
int was_listener = (fd == listener_fd);
|
||||
is_shimmed[fd] = 0;
|
||||
|
||||
if (wake_w[fd] >= 0) {
|
||||
char c = 0;
|
||||
write(wake_w[fd], &c, 1);
|
||||
real_close(wake_w[fd]);
|
||||
wake_w[fd] = -1;
|
||||
}
|
||||
if (wake_r[fd] >= 0) { real_close(wake_r[fd]); wake_r[fd] = -1; }
|
||||
if (peer_of[fd] >= 0) { real_close(peer_of[fd]); peer_of[fd] = -1; }
|
||||
|
||||
if (was_listener)
|
||||
_exit(0);
|
||||
}
|
||||
return real_close(fd);
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
result = run_soffice(sys.argv[1:])
|
||||
sys.exit(result.returncode)
|
||||
225
runtime/skills/xlsx-pro/scripts/recalc.py
Normal file
225
runtime/skills/xlsx-pro/scripts/recalc.py
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script de recalcul des formules Excel
|
||||
Recalcule toutes les formules d'un fichier Excel via LibreOffice
|
||||
Adapté pour OpenClawd
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from office.soffice import get_soffice_env
|
||||
except ImportError:
|
||||
# Fallback si le module office n'est pas disponible
|
||||
def get_soffice_env():
|
||||
env = os.environ.copy()
|
||||
env["SAL_USE_VCLPLUGIN"] = "svp"
|
||||
return env
|
||||
|
||||
try:
|
||||
from openpyxl import load_workbook
|
||||
except ImportError:
|
||||
print("Erreur: openpyxl non installé. Exécuter: pip install openpyxl")
|
||||
sys.exit(1)
|
||||
|
||||
# Répertoire macro LibreOffice selon plateforme
|
||||
MACRO_DIR_MACOS = "~/Library/Application Support/LibreOffice/4/user/basic/Standard"
|
||||
MACRO_DIR_LINUX = "~/.config/libreoffice/4/user/basic/Standard"
|
||||
MACRO_FILENAME = "Module1.xba"
|
||||
|
||||
# Macro LibreOffice Basic pour recalcul
|
||||
RECALCULATE_MACRO = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
|
||||
<script:module xmlns:script="http://openoffice.org/2000/script" script:name="Module1" script:language="StarBasic">
|
||||
Sub RecalculateAndSave()
|
||||
ThisComponent.calculateAll()
|
||||
ThisComponent.store()
|
||||
ThisComponent.close(True)
|
||||
End Sub
|
||||
</script:module>"""
|
||||
|
||||
|
||||
def has_gtimeout():
|
||||
"""Vérifie si gtimeout est disponible sur macOS"""
|
||||
try:
|
||||
subprocess.run(
|
||||
["gtimeout", "--version"], capture_output=True, timeout=1, check=False
|
||||
)
|
||||
return True
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
return False
|
||||
|
||||
|
||||
def setup_libreoffice_macro():
|
||||
"""Configure la macro LibreOffice si pas déjà fait"""
|
||||
macro_dir = os.path.expanduser(
|
||||
MACRO_DIR_MACOS if platform.system() == "Darwin" else MACRO_DIR_LINUX
|
||||
)
|
||||
macro_file = os.path.join(macro_dir, MACRO_FILENAME)
|
||||
|
||||
# Vérifier si macro existe déjà
|
||||
if (
|
||||
os.path.exists(macro_file)
|
||||
and "RecalculateAndSave" in Path(macro_file).read_text()
|
||||
):
|
||||
return True
|
||||
|
||||
# Créer répertoire macro si nécessaire
|
||||
if not os.path.exists(macro_dir):
|
||||
try:
|
||||
subprocess.run(
|
||||
["soffice", "--headless", "--terminate_after_init"],
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
env=get_soffice_env(),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
os.makedirs(macro_dir, exist_ok=True)
|
||||
|
||||
# Écrire fichier macro
|
||||
try:
|
||||
Path(macro_file).write_text(RECALCULATE_MACRO)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def recalc(filename, timeout=30):
|
||||
"""
|
||||
Recalcule les formules d'un fichier Excel et rapporte les erreurs
|
||||
|
||||
Args:
|
||||
filename: Chemin vers le fichier Excel
|
||||
timeout: Temps max d'attente pour le recalcul (secondes)
|
||||
|
||||
Returns:
|
||||
dict avec emplacements et compteurs d'erreurs
|
||||
"""
|
||||
if not Path(filename).exists():
|
||||
return {"error": f"Fichier {filename} inexistant"}
|
||||
|
||||
abs_path = str(Path(filename).absolute())
|
||||
|
||||
if not setup_libreoffice_macro():
|
||||
return {"error": "Échec configuration macro LibreOffice"}
|
||||
|
||||
cmd = [
|
||||
"soffice",
|
||||
"--headless",
|
||||
"--norestore",
|
||||
"vnd.sun.star.script:Standard.Module1.RecalculateAndSave?language=Basic&location=application",
|
||||
abs_path,
|
||||
]
|
||||
|
||||
# Encapsuler avec timeout si disponible
|
||||
if platform.system() == "Linux":
|
||||
cmd = ["timeout", str(timeout)] + cmd
|
||||
elif platform.system() == "Darwin" and has_gtimeout():
|
||||
cmd = ["gtimeout", str(timeout)] + cmd
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, env=get_soffice_env(), timeout=timeout+10)
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"error": "Timeout lors du recalcul"}
|
||||
|
||||
if result.returncode != 0 and result.returncode != 124: # 124 = code timeout
|
||||
error_msg = result.stderr or "Erreur inconnue lors du recalcul"
|
||||
if "Module1" in error_msg or "RecalculateAndSave" not in error_msg:
|
||||
return {"error": "Macro LibreOffice mal configurée"}
|
||||
return {"error": error_msg}
|
||||
|
||||
# Vérifier erreurs Excel dans le fichier recalculé
|
||||
try:
|
||||
wb = load_workbook(filename, data_only=True)
|
||||
|
||||
excel_errors = [
|
||||
"#VALUE!",
|
||||
"#DIV/0!",
|
||||
"#REF!",
|
||||
"#NAME?",
|
||||
"#NULL!",
|
||||
"#NUM!",
|
||||
"#N/A",
|
||||
]
|
||||
error_details = {err: [] for err in excel_errors}
|
||||
total_errors = 0
|
||||
|
||||
for sheet_name in wb.sheetnames:
|
||||
ws = wb[sheet_name]
|
||||
for row in ws.iter_rows():
|
||||
for cell in row:
|
||||
if cell.value is not None and isinstance(cell.value, str):
|
||||
for err in excel_errors:
|
||||
if err in cell.value:
|
||||
location = f"{sheet_name}!{cell.coordinate}"
|
||||
error_details[err].append(location)
|
||||
total_errors += 1
|
||||
break
|
||||
|
||||
wb.close()
|
||||
|
||||
# Construire résumé
|
||||
result = {
|
||||
"status": "success" if total_errors == 0 else "errors_found",
|
||||
"total_errors": total_errors,
|
||||
"error_summary": {},
|
||||
}
|
||||
|
||||
# Ajouter catégories d'erreurs non vides
|
||||
for err_type, locations in error_details.items():
|
||||
if locations:
|
||||
result["error_summary"][err_type] = {
|
||||
"count": len(locations),
|
||||
"locations": locations[:20], # Max 20 emplacements affichés
|
||||
}
|
||||
|
||||
# Ajouter compte de formules
|
||||
wb_formulas = load_workbook(filename, data_only=False)
|
||||
formula_count = 0
|
||||
for sheet_name in wb_formulas.sheetnames:
|
||||
ws = wb_formulas[sheet_name]
|
||||
for row in ws.iter_rows():
|
||||
for cell in row:
|
||||
if (
|
||||
cell.value
|
||||
and isinstance(cell.value, str)
|
||||
and cell.value.startswith("=")
|
||||
):
|
||||
formula_count += 1
|
||||
wb_formulas.close()
|
||||
|
||||
result["total_formulas"] = formula_count
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python recalc.py <fichier_excel> [timeout_secondes]")
|
||||
print("\nRecalcule toutes les formules d'un fichier Excel via LibreOffice")
|
||||
print("\nRetourne JSON avec détails d'erreurs:")
|
||||
print(" - status: 'success' ou 'errors_found'")
|
||||
print(" - total_errors: Nombre total d'erreurs Excel")
|
||||
print(" - total_formulas: Nombre de formules dans le fichier")
|
||||
print(" - error_summary: Détail par type avec emplacements")
|
||||
print(" - #VALUE!, #DIV/0!, #REF!, #NAME?, #NULL!, #NUM!, #N/A")
|
||||
sys.exit(1)
|
||||
|
||||
filename = sys.argv[1]
|
||||
timeout = int(sys.argv[2]) if len(sys.argv) > 2 else 30
|
||||
|
||||
result = recalc(filename, timeout)
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue