Sync third-party and MCP marketplace plugins

Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources.
Confidence: high
Scope-risk: narrow
Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history.
Tested: Marketplace validation passed.
This commit is contained in:
KeyInfo Bot committed 2026-10-06 15:29:22 +08:00
1 parent be7ce9bda2
commit 36c089fee9
298 files changed
+32935 -11681

No files matched your search

@@ -0,0 +1,52 @@
"""Unpack or repack an OOXML document without shell-specific ZIP commands."""
import argparse
import os
import tempfile
import zipfile
from pathlib import Path, PurePosixPath
def unpack(source, destination):
root = Path(destination).resolve()
if root.exists() and any(root.iterdir()):
raise ValueError("Unpack destination must be empty")
with zipfile.ZipFile(source) as archive:
for info in archive.infolist():
name = PurePosixPath(info.filename.replace("\\", "/"))
if name.is_absolute() or ".." in name.parts or any(":" in part for part in name.parts):
raise ValueError(f"Unsafe ZIP entry: {info.filename}")
if (info.external_attr >> 16) & 0o170000 == 0o120000:
raise ValueError(f"ZIP symlink is not supported: {info.filename}")
archive.extractall(root)
def pack(source, destination):
root = Path(source).resolve()
output = Path(destination).resolve()
if output.is_relative_to(root):
raise ValueError("Output must be outside the unpacked directory")
if not (root / "[Content_Types].xml").is_file():
raise ValueError("Source must be the package root containing [Content_Types].xml")
output.parent.mkdir(parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(suffix=output.suffix, dir=output.parent)
os.close(fd)
try:
with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED) as archive:
for file in sorted(root.rglob("*")):
if file.is_symlink():
raise ValueError(f"Package symlink is not supported: {file}")
if file.is_file():
archive.write(file, file.relative_to(root).as_posix())
os.replace(temporary, output)
finally:
Path(temporary).unlink(missing_ok=True)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("action", choices=("unpack", "pack"))
parser.add_argument("input")
parser.add_argument("output")
args = parser.parse_args()
{"unpack": unpack, "pack": pack}[args.action](args.input, args.output)
+41
View File
@@ -0,0 +1,41 @@
# 简体中文与 Windows 环境
以下命令中的 `$skill` 是当前已安装 Skill 的真实目录;不要硬编码开发者机器路径。脚本使用 Python 3.10+。先检查现有 Python/Node 环境,缺少哪个依赖再安装哪个。
```powershell
$env:PYTHONUTF8 = "1"
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
python --version
node --version
```
若只有 `py`,把命令中的 `python` 替换为 `py -3`。PowerShell 调用带空格的可执行文件路径使用 `& "路径"`;Python 脚本路径作为独立参数传入,不拼接 shell 命令。中文路径、空格路径均需引号。
需要独立依赖环境时,在任务工作目录创建 `.venv`,使用 `& ".\.venv\Scripts\python.exe" -m pip install <缺失包>`。无需改变系统执行策略或激活脚本。Node 库安装到任务目录,不默认全局安装。不要假设 Codex、Claude 或容器已预装所有依赖。
## 字体与文本
- 用户未指定语言时,中文用户的说明、标题、图例和交付文案使用简体中文;引文、产品名、代码和原文专有名词保持原样。
- 新文件优先使用本机可用的微软雅黑、等线、宋体或思源黑体。先查字体;声明字体名不等于字体已安装。Word 配置 `w:rFonts/@w:eastAsia` 与 `zh-CN`,PPT 配置主题字体和文本框字体,Excel 使用 `Microsoft YaHei` 等实际存在的字体。
- PDF 优先嵌入有再分发许可的中文字体;ReportLab 简单中文文档可用 `UnicodeCIDFont('STSong-Light')`,交付前仍检查目标阅读器效果。不要用 Helvetica 绘制中文,也不要把方块字当正常输出。
- CSV 面向 Windows Excel 时可用 `utf-8-sig`;先保留已有文件编码,不要自动改写所有输入。XML、JSON 和新建脚本使用 UTF-8。
- 标点、单位、日期、人民币格式、身份证号和前导零按用户语义保留;不要默认套用美式日期或把长编号转为数字。
## 转换与预览
Office 文档转 PDF 使用随附的跨平台 `scripts/office/soffice.py`。它按 `SOFFICE_PATH`、PATH、Windows Program Files、macOS 应用目录寻找 LibreOffice,并为转换创建独立临时 profile。
```powershell
# 仅在自动检测不到时设置;不要求添加系统 PATH
$env:SOFFICE_PATH = "C:\Program Files\LibreOffice\program\soffice.com"
python "$skill\scripts\office\soffice.py" --headless --convert-to pdf --outdir ".\preview" ".\中文 文档.docx"
python "$skill\scripts\render_pdf.py" ".\preview\中文 文档.pdf" ".\preview\pages"
```
按实际需要安装 LibreOffice、`pymupdf`、`defusedxml`、`lxml` 等缺失依赖。PDF 的 `scripts/convert_pdf_to_images.py` 与其他 Skill 的 `scripts/render_pdf.py` 均使用 PyMuPDF,不需要 Poppler。`pptx` 上游缩略图脚本仍需 Poppler;无 Poppler 时走 PDF 渲染流程。
使用 Python `scripts/office_package.py unpack <输入> <空目录>` 和 `pack <包根目录> <输出>` 处理 Office ZIP,无需 `unzip`、`zip`、`rm` 或 Bash。输出必须在解包目录之外。
## 验证与交付
生成或编辑后保存到新文件;先检查内容、公式或 OOXML 结构,再渲染并查看页面,检查中文缺字、表格越界、文本遮挡和分页。转换失败或字体缺失时说明具体原因,不把“文件保存成功”当成渲染通过。保留输入,除非用户明确要求覆盖。
@@ -0,0 +1,29 @@
"""Render PDF pages without Poppler; requires PyMuPDF."""
import argparse
from pathlib import Path
import pymupdf
def convert(pdf_path, output_dir, max_dim=1600):
if max_dim <= 0:
raise ValueError("max_dim must be positive")
output = Path(output_dir)
output.mkdir(parents=True, exist_ok=True)
with pymupdf.open(pdf_path) as document:
for index, page in enumerate(document):
scale = min(max_dim / page.rect.width, max_dim / page.rect.height)
pixmap = page.get_pixmap(matrix=pymupdf.Matrix(scale, scale), alpha=False)
target = output / f"page_{index + 1}.png"
pixmap.save(str(target))
print(target)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input")
parser.add_argument("output")
parser.add_argument("--max-dim", type=int, default=1600)
args = parser.parse_args()
convert(args.input, args.output, args.max_dim)
+61
View File
@@ -0,0 +1,61 @@
"""Portable LibreOffice runner for Windows, macOS and Linux."""
import os
import platform
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
def resolve_soffice_binary():
explicit = os.environ.get("SOFFICE_PATH")
if explicit:
candidate = Path(explicit).expanduser()
if candidate.is_file():
return str(candidate)
raise FileNotFoundError(f"SOFFICE_PATH does not exist: {candidate}")
names = ("soffice.com", "soffice.exe", "soffice") if platform.system() == "Windows" else ("soffice",)
for name in names:
binary = shutil.which(name)
if binary:
return binary
candidates = [Path("/Applications/LibreOffice.app/Contents/MacOS/soffice")]
if platform.system() == "Windows":
candidates = [
Path(root) / "LibreOffice/program" / name
for root in (os.environ.get("ProgramFiles"), os.environ.get("ProgramFiles(x86)")) if root
for name in ("soffice.com", "soffice.exe")
]
for candidate in candidates:
if candidate.is_file():
return str(candidate)
raise FileNotFoundError("LibreOffice not found. Install it or set SOFFICE_PATH to its executable.")
def get_soffice_env():
env = os.environ.copy()
env["PYTHONUTF8"] = "1"
if platform.system() == "Linux":
env.setdefault("SAL_USE_VCLPLUGIN", "svp")
return env
def run_soffice(args, **kwargs):
binary = resolve_soffice_binary()
kwargs.setdefault("env", get_soffice_env())
kwargs.setdefault("timeout", 120)
if any(str(arg).startswith("-env:UserInstallation=") for arg in args):
return subprocess.run([binary, *args], **kwargs)
with tempfile.TemporaryDirectory(prefix="skill-lo-", ignore_cleanup_errors=True) as profile:
profile_arg = f"-env:UserInstallation={Path(profile).resolve().as_uri()}"
return subprocess.run([binary, profile_arg, *args], **kwargs)
if __name__ == "__main__":
try:
raise SystemExit(run_soffice(sys.argv[1:]).returncode)
except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
print(str(exc), file=sys.stderr)
raise SystemExit(1)
+38
View File
@@ -0,0 +1,38 @@
---
name: docx
description: 创建、读取、编辑和检查 Word 文档或模板(.docx/.dotx),包括中文排版、表格、批注和修订;适用于简体中文与 Windows PowerShell 环境。
license: Proprietary. LICENSE.txt has complete terms
---
# Word 文档
开始操作前读取 [references/platform.md](references/platform.md),确认 Python、Node、中文字体及需要的转换工具。命令中的脚本路径均相对本 Skill。
| 任务 | 选择 |
| --- | --- |
| 新建文档 | `docx`(Node)或 `python-docx`,按所需版式选择 |
| 提取正文或表格 | `python-docx`;需要查看修订时用 Pandoc 或检查 OOXML |
| 编辑复杂模板、批注或修订 | 保留 OOXML 结构,使用随附脚本 |
| .doc 旧文件 | LibreOffice 先转换为 .docx |
## 中文版式
未指定页面时,新文档采用 A4;已有文档沿用其页面和样式。正文中文字体设置 East Asian 字体,语言为 `zh-CN`,标题使用真正的标题样式。保持全角标点、编号、金额和原文引文;中文表格不要依赖空格对齐。
`docx` 创建时注意:表格同时指定列宽和单元格宽度;`ShadingType.CLEAR` 避免黑底;列表用 numbering 配置;`ImageRun` 指定类型;分页符放进 Paragraph。每个逻辑段落独立创建,不用换行模拟段落。需要 API 细节时按需读取 [upstream-guide.md](upstream-guide.md)。
## 编辑与审阅
先确认要清稿、批注还是保留修订;不要因文档类型自动强制红线。原有修订与批注保持不变,除非用户要求接受、拒绝或删除。
```powershell
python "$skill\scripts\office_package.py" unpack ".\输入.docx" ".\unpacked"
python "$skill\scripts\merge_runs.py" ".\unpacked"
# 按需编辑 word/document.xml,保持命名空间与节点顺序
python "$skill\scripts\office_package.py" pack ".\unpacked" ".\输出.docx"
python "$skill\scripts\office\validate.py" ".\输出.docx" --original ".\输入.docx"
```
`scripts/comment.py` 支持为文档添加批注;输出的定位节点需放到对应正文位置才能显示。`scripts/accept_changes.py` 仅在用户要求接受全部修订时使用,输出新文件;超时、文件未保存或仍有修订均算失败。
最终转换为 PDF,用 `scripts/render_pdf.py` 生成预览并查看中文字体、表格、页眉页脚、分页和页码。保留可编辑 Word 文件与用户需要的最终副本。
@@ -0,0 +1,75 @@
"""Accept tracked changes into a new DOCX using an isolated LibreOffice profile."""
import argparse
import shutil
import subprocess
import tempfile
import zipfile
from pathlib import Path
from defusedxml import ElementTree
from office.soffice import run_soffice
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 AcceptAllTrackedChanges()
Dim dispatcher As Object
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")
dispatcher.executeDispatch(ThisComponent.CurrentController.Frame, ".uno:AcceptAllTrackedChanges", "", 0, Array())
ThisComponent.store()
ThisComponent.close(True)
End Sub
</script:module>'''
WORD_NS = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
def accept_changes(input_file, output_file, timeout=120):
source = Path(input_file).resolve()
output = Path(output_file).resolve()
if source == output or not source.is_file() or source.suffix.lower() != ".docx":
return None, "Input must be an existing DOCX and output must be a different file"
output.parent.mkdir(parents=True, exist_ok=True)
try:
with tempfile.TemporaryDirectory(prefix="docx-accept-", ignore_cleanup_errors=True) as temporary:
root = Path(temporary).resolve()
profile = root / "profile"
profile_arg = f"-env:UserInstallation={profile.as_uri()}"
run_soffice([profile_arg, "--headless", "--terminate_after_init"], check=True, capture_output=True, timeout=timeout)
macro_dir = profile / "user/basic/Standard"
macro_dir.mkdir(parents=True, exist_ok=True)
(macro_dir / "Module1.xba").write_text(MACRO, encoding="utf-8")
candidate = root / "accepted.docx"
shutil.copy2(source, candidate)
before = candidate.stat().st_mtime_ns
run_soffice([
profile_arg, "--headless", "--norestore",
"vnd.sun.star.script:Standard.Module1.AcceptAllTrackedChanges?language=Basic&location=application",
str(candidate),
], check=True, capture_output=True, timeout=timeout)
if candidate.stat().st_mtime_ns == before:
return None, "LibreOffice did not save the document"
with zipfile.ZipFile(candidate) as archive:
for name in archive.namelist():
if not name.startswith("word/") or not name.endswith(".xml"):
continue
document = ElementTree.fromstring(archive.read(name))
if any(element.tag in {WORD_NS + "ins", WORD_NS + "del", WORD_NS + "moveFrom", WORD_NS + "moveTo"}
or (element.tag.startswith(WORD_NS) and element.tag.endswith("Change"))
for element in document.iter()):
return None, f"Tracked changes remain in {name}"
shutil.copy2(candidate, output)
return None, f"Accepted tracked changes: {output}"
except (OSError, subprocess.SubprocessError, zipfile.BadZipFile) as exc:
return None, f"Failed to accept tracked changes: {exc}"
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input")
parser.add_argument("output")
parser.add_argument("--timeout", type=int, default=120)
args = parser.parse_args()
_, message = accept_changes(args.input, args.output, args.timeout)
print(message)
raise SystemExit(0 if message.startswith("Accepted tracked changes:") else 1)
+35
View File
@@ -0,0 +1,35 @@
---
name: pdf
description: 读取、创建、合并、拆分、旋转、OCR 或填写 PDF 表单,处理简体中文字体与页面预览;适用于 Windows PowerShell 和跨平台终端。
license: Proprietary. LICENSE.txt has complete terms
---
# PDF 文档
先读取 [references/platform.md](references/platform.md),确认中文字体、Python 与实际需要的依赖。PDF 不等于扫描图片,先判断页面是否有可提取文字。
| 任务 | 工具 |
| --- | --- |
| 文本、页面预览、坐标检查 | PyMuPDF |
| 合并、拆分、旋转、元数据、加密 | pypdf |
| 表格提取 | pdfplumber,提取后核对表头、合并单元格与换行 |
| 新建中文 PDF | ReportLab 或可用的 Office/HTML 转 PDF 工具 |
| 扫描页 OCR | 本机可用的 OCR;Tesseract 中文需 `chi_sim` 数据 |
## 中文与排版
新文档默认简体中文、A4;已有版式或用户指定版式优先。中文必须使用实际可用的中文字体。优先嵌入授权允许的字体;ReportLab 简单文档可注册 `UnicodeCIDFont('STSong-Light')`。复制文本不乱码、渲染无缺字都需检查。
OCR 只针对扫描页或提取失败的页面;中文可用 `chi_sim+eng`,未装语言包时报告缺失。OCR 结果需核对数字、单位、姓名与表格关系,不凭猜测补全。
## 表单
涉及填写时按需读取 [forms.md](forms.md),先判断是否有 AcroForm 字段。可填表单使用随附 `extract_form_field_info.py`、`fill_fillable_fields.py`;非可填表单按页面坐标叠加,并检查原内容与填写区域。不要把签名图片视作数字签名。
## 预览与检查
```powershell
python "$skill\scripts\convert_pdf_to_images.py" ".\输入 中文.pdf" ".\preview"
```
此脚本使用 PyMuPDF,无需 Poppler,逐页渲染以避免一次加载整个大文档。合并或拆分后核对页面数量和顺序;填写后检查可见内容、字段值与页面边界。更多提取与转换方案见 [reference.md](reference.md),旧平台专用命令以本地环境指导为准。
+34
View File
@@ -0,0 +1,34 @@
---
name: pptx
description: 创建、读取、编辑和检查 PowerPoint 演示文稿或模板(.pptx/.potx),保留模板与可编辑图表,适配简体中文字体和 Windows PowerShell。
license: Proprietary. LICENSE.txt has complete terms
---
# PowerPoint 演示文稿
先读取 [references/platform.md](references/platform.md),确认字体、Node/Python 与需要的转换工具。先看用户模板或参考稿,再决定布局、配色与内容密度。
| 任务 | 选择 |
| --- | --- |
| 新建幻灯片 | `pptxgenjs`;简单任务也可使用 `python-pptx` |
| 提取文本 | `python-pptx` 或 markitdown;检查备注与隐藏页 |
| 复杂模板、母版或原生图表编辑 | OOXML 加随附脚本 |
| .ppt 旧格式 | LibreOffice 先转 .pptx |
## 中文与画布
新建稿未指定比例时用 16:9,设置画布后再添加元素。用户模板比例和主题优先。使用本机可用的微软雅黑、等线或思源黑体,同时设置主题字体与正文文本字体;简体中文语言标记使用 `zh-CN`。长中文标题主动换行,表格与数据标签保留足够空间,不能只缩小字号隐藏溢出。
`pptxgenjs` 颜色使用六位 HEX,不带 `#`;每次添加元素使用独立选项对象。文本框 margin 与字体测量要一致;原生图表、表格和文本保持可编辑。坐标单位为英寸,必须落在实际画布内。库限制、组合图表和模板 OOXML 细节按需读取 [upstream-guide.md](upstream-guide.md)。
## 模板编辑
先完成复制、删除或重排幻灯片,再改正文。使用 `scripts/add_slide.py` 注册复制页,最终页序确定后运行 `scripts/clean.py` 清理失去引用的部分。不要只复制 slide XML;关系、媒体、母版和内容类型必须一起处理。用 `scripts/office_package.py` 解包和重打包,避免依赖 Bash ZIP 命令。
```powershell
python "$skill\scripts\office\validate.py" ".\输出.pptx" --original ".\模板.pptx"
python "$skill\scripts\office\soffice.py" --headless --convert-to pdf --outdir ".\preview" ".\输出.pptx"
python "$skill\scripts\render_pdf.py" ".\preview\输出.pdf" ".\preview\pages"
```
没有模板时省略 `--original`。每页检查文字溢出、缺字、对齐、遮挡、图表标签、图片分辨率和页序。需要模板缩略图且 Poppler 可用时运行 `scripts/thumbnail.py` 并指定独立输出前缀;否则用上面的免 Poppler 逐页预览。
+35
View File
@@ -0,0 +1,35 @@
---
name: xlsx
description: 创建、读取、清理、编辑和检查 Excel 工作簿或 CSV/TSV,处理公式、图表、中文表头及 Windows Excel 编码,保留现有格式与宏。
license: Proprietary. LICENSE.txt has complete terms
---
# Excel 工作簿
先读取 [references/platform.md](references/platform.md),确认 Python、中文字体及是否需要公式计算引擎。复杂工作簿先检查公式、外部链接、宏、图表和现有样式,再选择处理方法。
- `openpyxl` 用于普通 .xlsx 单元格、公式和样式;`.xlsm` 读取时用 `keep_vba=True`,保留扩展名。不保证复杂 Excel 功能无损,必须检查实际需要保留的部分。
- `pandas` 适合表格数据清理;不要用整表重写处理需要保留样式、公式或图表的原文件。
- `.xls` 旧格式先使用兼容工具读取或转换;不要直接交给 openpyxl。
## 简体中文与数据类型
用户未指定语言时用简体中文表头、图例与说明。新文件使用可用的中文字体,表头、行高与列宽按中文实际长度设置;已有工作簿沿用其约定。身份证号、订单号、手机号和前导零存为文本,日期存真实日期值并设置明确格式,金额单位按用户要求标注。
Windows Excel 打开的新 CSV 可写 `utf-8-sig`;读取旧 CSV 先判断编码和分隔符。CSV 导出不保留多表、公式和样式,转换前明确所需交付格式。
## 公式与缓存
按用户语义写公式,验证引用范围与预期结果;不要用硬编码数值代替可重算公式。跨表引用含空格或中文标点时正确引用工作表名。合并区域只写左上角。
openpyxl 不计算公式。检查时分别用 `data_only=False` 读取公式、`data_only=True` 读取缓存;不能保存 `data_only=True` 的工作簿,否则公式会被数值覆盖。
```powershell
python "$skill\scripts\recalc.py" ".\输出 中文.xlsx" 120
```
重算会改写指定工作簿,先保留输入。脚本使用独立 LibreOffice profile,自动识别 Windows 的安装路径。检查 JSON 的 `status`、`total_errors` 和具体单元格;`errors_found` 即使进程退出码为零也未通过。含 `error` 的结果表示没有完成重算。
有外部链接、宏、动态数组或 Excel 专用函数时,优先使用目标 Excel 环境验证,不能盲目用 LibreOffice 往返保存。兼容公式、缓存丢失和链接风险按需读取 [upstream-guide.md](upstream-guide.md)。无法运行计算引擎时说明“尚未重算”,不要声称公式已验证。
交付前核对数值、公式、中文显示、筛选、冻结窗格、打印区域和图表。用新输出文件保留输入;抽查代表行、合计和异常情况,避免只检查文件能够打开。
+89
View File
@@ -0,0 +1,89 @@
"""Recalculate an XLSX through a temporary LibreOffice import/export, without macros."""
import argparse
import json
import os
import shutil
import subprocess
import tempfile
import zipfile
from pathlib import Path
from openpyxl import load_workbook
from office.soffice import run_soffice
def inspect_workbook(path):
formulas = load_workbook(path, data_only=False)
values = load_workbook(path, data_only=True)
try:
formula_cells = set()
errors = {}
for sheet in formulas:
if not hasattr(sheet, "iter_rows"):
continue
for row in sheet.iter_rows():
for cell in row:
location = f"{sheet.title}!{cell.coordinate}"
if cell.data_type == "f":
formula_cells.add(location)
cached = values[sheet.title][cell.coordinate]
if cached.data_type == "e":
errors.setdefault(str(cached.value), []).append(location)
return formula_cells, errors
finally:
formulas.close()
values.close()
def recalc(filename, timeout=120):
source = Path(filename).expanduser().resolve()
if not source.is_file() or source.suffix.lower() != ".xlsx":
return {"error": "Input must be an existing .xlsx file; use Excel for macro-enabled workbooks"}
try:
with zipfile.ZipFile(source) as archive:
if any(name.startswith("xl/externalLinks/") for name in archive.namelist()):
return {"error": "External workbook links require validation in Excel; original file was not modified"}
original_formulas, _ = inspect_workbook(source)
with tempfile.TemporaryDirectory(prefix="xlsx-recalc-", dir=source.parent, ignore_cleanup_errors=True) as temporary:
root = Path(temporary)
input_dir, output_dir = root / "input", root / "output"
input_dir.mkdir()
output_dir.mkdir()
candidate = input_dir / source.name
shutil.copy2(source, candidate)
result = run_soffice([
"--headless", "--norestore", "--convert-to", "xlsx:Calc MS Excel 2007 XML",
"--outdir", str(output_dir), str(candidate),
], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout)
recalculated = output_dir / source.name
if result.returncode or not recalculated.is_file():
return {"error": "LibreOffice did not export a recalculated workbook", "details": result.stderr or result.stdout}
formula_cells, errors = inspect_workbook(recalculated)
if original_formulas != formula_cells:
return {"error": "Formula locations changed during conversion; original file was not modified"}
error_count = sum(len(locations) for locations in errors.values())
report = {
"status": "errors_found" if error_count else "success",
"total_formulas": len(formula_cells),
"total_errors": error_count,
"error_summary": {
error: {"count": len(locations), "locations": locations[:100]}
for error, locations in errors.items()
},
}
if not errors:
os.replace(recalculated, source)
return report
except (OSError, subprocess.SubprocessError, zipfile.BadZipFile, ValueError) as exc:
return {"error": str(exc)}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input")
parser.add_argument("timeout", nargs="?", type=int, default=120)
args = parser.parse_args()
report = recalc(args.input, args.timeout)
print(json.dumps(report, ensure_ascii=False, indent=2))
raise SystemExit(1 if "error" in report else 0)