统一技能安装与 Skills 市场接入链路。

新增公开安装工具与多来源 provider 支持,补齐管理端与目录加载逻辑,并同步更新相关测试与文档以保证可见性和路径安全。

Made-with: Cursor
This commit is contained in:
oliver 2026-04-30 22:17:50 +08:00
parent d5e30542aa
commit 4d9232f3b3
66 changed files with 7369 additions and 1099 deletions

View 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']

View 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)