落地并存测服全端:智能体 Graph/路由、后台切 8180、主账与部署脚本齐备,便于线上重发与验收。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
LibreOffice Excel→PDF 转换壳。
|
||||
|
||||
本文件职责:探测 soffice、在「调用方已占用 LibreOffice 单槽」的前提下同步转 PDF。
|
||||
禁止:在企微回调 / H5 线程调用;禁止本进程同时起两台 soffice(由 SlotPool size=1 保证)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from agent.config import Settings, get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LibreOfficeResult:
|
||||
"""转换结果;ok=False 时 error 说明原因,不抛到 Worker 主循环。"""
|
||||
|
||||
ok: bool
|
||||
pdf_path: str = ""
|
||||
error: str = ""
|
||||
returncode: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class LibreOfficeConverter:
|
||||
"""
|
||||
soffice 无界面转换器。
|
||||
|
||||
调用约定:仅从 WorkerRuntime.libreoffice 槽内进入 convert()。
|
||||
"""
|
||||
|
||||
settings: Settings
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls, settings: Optional[Settings] = None) -> "LibreOfficeConverter":
|
||||
return cls(settings=settings or get_settings())
|
||||
|
||||
def executable(self) -> Path:
|
||||
return Path(self.settings.libreoffice_path)
|
||||
|
||||
def available(self) -> bool:
|
||||
"""本机是否存在配置的 soffice 可执行文件。"""
|
||||
return self.executable().is_file()
|
||||
|
||||
def convert_xlsx_to_pdf(self, xlsx_path: str | Path) -> LibreOfficeResult:
|
||||
"""
|
||||
将 Excel 转为同目录 PDF。
|
||||
|
||||
副作用:启动 soffice 子进程(超时见 settings);成功写 PDF 文件。
|
||||
线程:必须已在 LibreOffice 单槽内。
|
||||
"""
|
||||
source = Path(xlsx_path).resolve()
|
||||
if not source.is_file():
|
||||
return LibreOfficeResult(ok=False, error="source_not_found")
|
||||
exe = self.executable()
|
||||
if not exe.is_file():
|
||||
return LibreOfficeResult(ok=False, error="libreoffice_not_installed")
|
||||
out_dir = source.parent
|
||||
target = source.with_suffix(".pdf")
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
str(exe),
|
||||
"--headless",
|
||||
"--convert-to",
|
||||
"pdf",
|
||||
"--outdir",
|
||||
str(out_dir),
|
||||
str(source),
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=float(self.settings.libreoffice_timeout_seconds),
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
logger.warning("LibreOffice 执行失败: %s", exc)
|
||||
return LibreOfficeResult(ok=False, error=f"exec_failed:{exc}")
|
||||
if completed.returncode != 0 or not target.is_file():
|
||||
stderr = (completed.stderr or b"")[:400]
|
||||
logger.warning(
|
||||
"LibreOffice 未生成 PDF rc=%s stderr=%s",
|
||||
completed.returncode,
|
||||
stderr,
|
||||
)
|
||||
return LibreOfficeResult(
|
||||
ok=False,
|
||||
error="pdf_not_generated",
|
||||
returncode=int(completed.returncode),
|
||||
)
|
||||
head = target.read_bytes()[:5]
|
||||
if head != b"%PDF-":
|
||||
target.unlink(missing_ok=True)
|
||||
return LibreOfficeResult(ok=False, error="pdf_header_invalid")
|
||||
logger.info("LibreOffice 转 PDF 成功 path=%s", target)
|
||||
return LibreOfficeResult(ok=True, pdf_path=str(target), returncode=0)
|
||||
Reference in New Issue
Block a user